[
  {
    "path": ".claude/agents/docs-reviewer.md",
    "content": "---\nname: docs-reviewer\ndescription: \"Lean docs reviewer that dispatches reviews docs for a particular skill.\"\nmodel: opus\ncolor: cyan\n---\n\nYou are a direct, critical, expert reviewer for React documentation. \n\nYour role is to use given skills to validate given doc pages for consistency, correctness, and adherence to established patterns.\n\nComplete this process:\n\n## Phase 1: Task Creation\n1. CRITICAL: Read the skill requested.\n2. Understand the skill's requirements.\n3. Create a task list to validate skills requirements.\n\n## Phase 2: Validate\n\n1. Read the docs files given.\n2. Review each file with the task list to verify.\n\n## Phase 3: Respond\n\nYou must respond with a checklist of the issues you identified, and line number.\n\nDO NOT respond with passed validations, ONLY respond with the problems. \n"
  },
  {
    "path": ".claude/settings.json",
    "content": "{\n  \"skills\": {\n    \"suggest\": [\n      {\n        \"pattern\": \"src/content/learn/**/*.md\",\n        \"skill\": \"docs-writer-learn\"\n      },\n      {\n        \"pattern\": \"src/content/reference/**/*.md\",\n        \"skill\": \"docs-writer-reference\"\n      }\n    ]\n  },\n  \"permissions\": {\n    \"allow\": [\n      \"Skill(docs-voice)\",\n      \"Skill(docs-components)\",\n      \"Skill(docs-sandpack)\",\n      \"Skill(docs-rsc-sandpack)\",\n      \"Skill(docs-writer-learn)\",\n      \"Skill(docs-writer-reference)\",\n      \"Bash(yarn lint:*)\",\n      \"Bash(yarn lint-heading-ids:*)\",\n      \"Bash(yarn lint:fix:*)\",\n      \"Bash(yarn tsc:*)\",\n      \"Bash(yarn check-all:*)\",\n      \"Bash(yarn fix-headings:*)\",\n      \"Bash(yarn deadlinks:*)\",\n      \"Bash(yarn prettier:diff:*)\"\n    ]\n  }\n}\n"
  },
  {
    "path": ".claude/skills/docs-components/SKILL.md",
    "content": "---\nname: docs-components\ndescription: Comprehensive MDX component patterns (Note, Pitfall, DeepDive, Recipes, etc.) for all documentation types. Authoritative source for component usage, examples, and heading conventions.\n---\n\n# MDX Component Patterns\n\n## Quick Reference\n\n### Component Decision Tree\n\n| Need | Component |\n|------|-----------|\n| Helpful tip or terminology | `<Note>` |\n| Common mistake warning | `<Pitfall>` |\n| Advanced technical explanation | `<DeepDive>` |\n| Canary-only feature | `<Canary>` or `<CanaryBadge />` |\n| Server Components only | `<RSC>` |\n| Deprecated API | `<Deprecated>` |\n| Experimental/WIP | `<Wip>` |\n| Visual diagram | `<Diagram>` |\n| Multiple related examples | `<Recipes>` |\n| Interactive code | `<Sandpack>` (see `/docs-sandpack`) |\n| Console error display | `<ConsoleBlock>` |\n| End-of-page exercises | `<Challenges>` (Learn pages only) |\n\n### Heading Level Conventions\n\n| Component | Heading Level |\n|-----------|---------------|\n| DeepDive title | `####` (h4) |\n| Titled Pitfall | `#####` (h5) |\n| Titled Note | `####` (h4) |\n| Recipe items | `####` (h4) |\n| Challenge items | `####` (h4) |\n\n### Callout Spacing Rules\n\nCallout components (Note, Pitfall, DeepDive) require a **blank line after the opening tag** before content begins.\n\n**Never place consecutively:**\n- `<Pitfall>` followed by `<Pitfall>` - Combine into one with titled subsections, or separate with prose\n- `<Note>` followed by `<Note>` - Combine into one, or separate with prose\n\n**Allowed consecutive patterns:**\n- `<DeepDive>` followed by `<DeepDive>` - OK for multi-part explorations (see useMemo.md)\n- `<Pitfall>` followed by `<DeepDive>` - OK when DeepDive explains \"why\" behind the Pitfall\n\n**Separation content:** Prose paragraphs, code examples (Sandpack), or section headers.\n\n**Why:** Consecutive warnings create a \"wall of cautions\" that overwhelms readers and causes important warnings to be skimmed.\n\n**Incorrect:**\n```mdx\n<Pitfall>\nDon't do X.\n</Pitfall>\n\n<Pitfall>\nDon't do Y.\n</Pitfall>\n```\n\n**Correct - combined:**\n```mdx\n<Pitfall>\n\n##### Don't do X {/*pitfall-x*/}\nExplanation.\n\n##### Don't do Y {/*pitfall-y*/}\nExplanation.\n\n</Pitfall>\n```\n\n**Correct - separated:**\n```mdx\n<Pitfall>\nDon't do X.\n</Pitfall>\n\nThis leads to another common mistake:\n\n<Pitfall>\nDon't do Y.\n</Pitfall>\n```\n\n---\n\n## `<Note>`\n\nImportant clarifications, conventions, or tips. Less severe than Pitfall.\n\n### Simple Note\n\n```mdx\n<Note>\n\nThe optimization of caching return values is known as [_memoization_](https://en.wikipedia.org/wiki/Memoization).\n\n</Note>\n```\n\n### Note with Title\n\nUse `####` (h4) heading with an ID.\n\n```mdx\n<Note>\n\n#### There is no directive for Server Components. {/*no-directive*/}\n\nA common misunderstanding is that Server Components are denoted by `\"use server\"`, but there is no directive for Server Components. The `\"use server\"` directive is for Server Functions.\n\n</Note>\n```\n\n### Version-Specific Note\n\n```mdx\n<Note>\n\nStarting in React 19, you can render `<SomeContext>` as a provider.\n\nIn older versions of React, use `<SomeContext.Provider>`.\n\n</Note>\n```\n\n---\n\n## `<Pitfall>`\n\nCommon mistakes that cause bugs. Use for errors readers will likely make.\n\n### Simple Pitfall\n\n```mdx\n<Pitfall>\n\nWe recommend defining components as functions instead of classes. [See how to migrate.](#alternatives)\n\n</Pitfall>\n```\n\n### Titled Pitfall\n\nUse `#####` (h5) heading with an ID.\n\n```mdx\n<Pitfall>\n\n##### Calling different memoized functions will read from different caches. {/*pitfall-different-caches*/}\n\nTo access the same cache, components must call the same memoized function.\n\n</Pitfall>\n```\n\n### Pitfall with Wrong/Right Code\n\n```mdx\n<Pitfall>\n\n##### `useFormStatus` will not return status information for a `<form>` rendered in the same component. {/*pitfall-same-component*/}\n\n```js\nfunction Form() {\n  // 🔴 `pending` will never be true\n  const { pending } = useFormStatus();\n  return <form action={submit}></form>;\n}\n```\n\nInstead call `useFormStatus` from inside a component located inside `<form>`.\n\n</Pitfall>\n```\n\n---\n\n## `<DeepDive>`\n\nOptional deep technical content. **First child must be `####` heading with ID.**\n\n### Standard DeepDive\n\n```mdx\n<DeepDive>\n\n#### Is using an updater always preferred? {/*is-updater-preferred*/}\n\nYou might hear a recommendation to always write code like `setAge(a => a + 1)` if the state you're setting is calculated from the previous state. There's no harm in it, but it's also not always necessary.\n\nIn most cases, there is no difference between these two approaches. React always makes sure that for intentional user actions, like clicks, the `age` state variable would be updated before the next click.\n\n</DeepDive>\n```\n\n### Comparison DeepDive\n\nFor comparing related concepts:\n\n```mdx\n<DeepDive>\n\n#### When should I use `cache`, `memo`, or `useMemo`? {/*cache-memo-usememo*/}\n\nAll mentioned APIs offer memoization but differ in what they memoize, who can access the cache, and when their cache is invalidated.\n\n#### `useMemo` {/*deep-dive-usememo*/}\n\nIn general, you should use `useMemo` for caching expensive computations in Client Components across renders.\n\n#### `cache` {/*deep-dive-cache*/}\n\nIn general, you should use `cache` in Server Components to memoize work that can be shared across components.\n\n</DeepDive>\n```\n\n---\n\n## `<Recipes>`\n\nMultiple related examples showing variations. Each recipe needs `<Solution />`.\n\n```mdx\n<Recipes titleText=\"Basic useState examples\" titleId=\"examples-basic\">\n\n#### Counter (number) {/*counter-number*/}\n\nIn this example, the `count` state variable holds a number.\n\n<Sandpack>\n{/* code */}\n</Sandpack>\n\n<Solution />\n\n#### Text field (string) {/*text-field-string*/}\n\nIn this example, the `text` state variable holds a string.\n\n<Sandpack>\n{/* code */}\n</Sandpack>\n\n<Solution />\n\n</Recipes>\n```\n\n**Common titleText/titleId combinations:**\n- \"Basic [hookName] examples\" / `examples-basic`\n- \"Examples of [concept]\" / `examples-[concept]`\n- \"The difference between [A] and [B]\" / `examples-[topic]`\n\n---\n\n## `<Challenges>`\n\nEnd-of-page exercises. **Learn pages only.** Each challenge needs problem + solution Sandpack.\n\n```mdx\n<Challenges>\n\n#### Fix the bug {/*fix-the-bug*/}\n\nProblem description...\n\n<Hint>\nOptional hint text.\n</Hint>\n\n<Sandpack>\n{/* problem code */}\n</Sandpack>\n\n<Solution>\n\nExplanation...\n\n<Sandpack>\n{/* solution code */}\n</Sandpack>\n\n</Solution>\n\n</Challenges>\n```\n\n**Guidelines:**\n- Only at end of standard Learn pages\n- No Challenges in chapter intros or tutorials\n- Each challenge has `####` heading with ID\n\n---\n\n## `<Deprecated>`\n\nFor deprecated APIs. Content should explain what to use instead.\n\n### Page-Level Deprecation\n\n```mdx\n<Deprecated>\n\nIn React 19, `forwardRef` is no longer necessary. Pass `ref` as a prop instead.\n\n`forwardRef` will be deprecated in a future release. Learn more [here](/blog/2024/04/25/react-19#ref-as-a-prop).\n\n</Deprecated>\n```\n\n### Method-Level Deprecation\n\n```mdx\n### `componentWillMount()` {/*componentwillmount*/}\n\n<Deprecated>\n\nThis API has been renamed from `componentWillMount` to [`UNSAFE_componentWillMount`.](#unsafe_componentwillmount)\n\nRun the [`rename-unsafe-lifecycles` codemod](codemod-link) to automatically update.\n\n</Deprecated>\n```\n\n---\n\n## `<RSC>`\n\nFor APIs that only work with React Server Components.\n\n### Basic RSC\n\n```mdx\n<RSC>\n\n`cache` is only for use with [React Server Components](/reference/rsc/server-components).\n\n</RSC>\n```\n\n### Extended RSC (for Server Functions)\n\n```mdx\n<RSC>\n\nServer Functions are for use in [React Server Components](/reference/rsc/server-components).\n\n**Note:** Until September 2024, we referred to all Server Functions as \"Server Actions\".\n\n</RSC>\n```\n\n---\n\n## `<Canary>` and `<CanaryBadge />`\n\nFor features only available in Canary releases.\n\n### Canary Wrapper (inline in Intro)\n\n```mdx\n<Intro>\n\n`<Fragment>` lets you group elements without a wrapper node.\n\n<Canary>Fragments can also accept refs, enabling interaction with underlying DOM nodes.</Canary>\n\n</Intro>\n```\n\n### CanaryBadge in Section Headings\n\n```mdx\n### <CanaryBadge /> FragmentInstance {/*fragmentinstance*/}\n```\n\n### CanaryBadge in Props Lists\n\n```mdx\n* <CanaryBadge /> **optional** `ref`: A ref object from `useRef` or callback function.\n```\n\n### CanaryBadge in Caveats\n\n```mdx\n* <CanaryBadge /> If you want to pass `ref` to a Fragment, you can't use the `<>...</>` syntax.\n```\n\n---\n\n## `<Diagram>`\n\nVisual explanations of module dependencies, render trees, or data flow.\n\n```mdx\n<Diagram name=\"use_client_module_dependency\" height={250} width={545} alt=\"A tree graph with the top node representing the module 'App.js'. 'App.js' has three children...\">\n`'use client'` segments the module dependency tree, marking `InspirationGenerator.js` and all dependencies as client-rendered.\n</Diagram>\n```\n\n**Attributes:**\n- `name`: Diagram identifier (used for image file)\n- `height`: Height in pixels\n- `width`: Width in pixels\n- `alt`: Accessible description of the diagram\n\n---\n\n## `<CodeStep>` (Use Sparingly)\n\nNumbered callouts in prose. Pairs with code block annotations.\n\n### Syntax\n\nIn code blocks:\n```mdx\n```js [[1, 4, \"age\"], [2, 4, \"setAge\"], [3, 4, \"42\"]]\nimport { useState } from 'react';\n\nfunction MyComponent() {\n  const [age, setAge] = useState(42);\n}\n```\n```\n\nFormat: `[[step_number, line_number, \"text_to_highlight\"], ...]`\n\nIn prose:\n```mdx\n1. The <CodeStep step={1}>current state</CodeStep> initially set to the <CodeStep step={3}>initial value</CodeStep>.\n2. The <CodeStep step={2}>`set` function</CodeStep> that lets you change it.\n```\n\n### Guidelines\n\n- Maximum 2-3 different colors per explanation\n- Don't highlight every keyword - only key concepts\n- Use for terms in prose, not entire code blocks\n- Maintain consistent usage within a section\n\n✅ **Good use** - highlighting key concepts:\n```mdx\nReact will compare the <CodeStep step={2}>dependencies</CodeStep> with the dependencies you passed...\n```\n\n🚫 **Avoid** - excessive highlighting:\n```mdx\nWhen an <CodeStep step={1}>Activity</CodeStep> boundary is <CodeStep step={2}>hidden</CodeStep> during its <CodeStep step={3}>initial</CodeStep> render...\n```\n\n---\n\n## `<ConsoleBlock>`\n\nDisplay console output (errors, warnings, logs).\n\n```mdx\n<ConsoleBlock level=\"error\">\nUncaught Error: Too many re-renders.\n</ConsoleBlock>\n```\n\n**Levels:** `error`, `warning`, `info`\n\n---\n\n## Component Usage by Page Type\n\n### Reference Pages\n\nFor component placement rules specific to Reference pages, invoke `/docs-writer-reference`.\n\nKey placement patterns:\n- `<RSC>` goes before `<Intro>` at top of page\n- `<Deprecated>` goes after `<Intro>` for page-level deprecation\n- `<Deprecated>` goes after method heading for method-level deprecation\n- `<Canary>` wrapper goes inline within `<Intro>`\n- `<CanaryBadge />` appears in headings, props lists, and caveats\n\n### Learn Pages\n\nFor Learn page structure and patterns, invoke `/docs-writer-learn`.\n\nKey usage patterns:\n- Challenges only at end of standard Learn pages\n- No Challenges in chapter intros or tutorials\n- DeepDive for optional advanced content\n- CodeStep should be used sparingly\n\n### Blog Pages\n\nFor Blog page structure and patterns, invoke `/docs-writer-blog`.\n\nKey usage patterns:\n- Generally avoid deep technical components\n- Note and Pitfall OK for clarifications\n- Prefer inline explanations over DeepDive\n\n---\n\n## Other Available Components\n\n**Version/Status:** `<Experimental>`, `<ExperimentalBadge />`, `<RSCBadge />`, `<NextMajor>`, `<Wip>`\n\n**Visuals:** `<DiagramGroup>`, `<Illustration>`, `<IllustrationBlock>`, `<CodeDiagram>`, `<FullWidth>`\n\n**Console:** `<ConsoleBlockMulti>`, `<ConsoleLogLine>`\n\n**Specialized:** `<TerminalBlock>`, `<BlogCard>`, `<TeamMember>`, `<YouTubeIframe>`, `<ErrorDecoder />`, `<LearnMore>`, `<Math>`, `<MathI>`, `<LanguageList>`\n\nSee existing docs for usage examples of these components.\n"
  },
  {
    "path": ".claude/skills/docs-rsc-sandpack/SKILL.md",
    "content": "---\nname: docs-rsc-sandpack\ndescription: Use when adding interactive RSC (React Server Components) code examples to React docs using <SandpackRSC>, or when modifying the RSC sandpack infrastructure.\n---\n\n# RSC Sandpack Patterns\n\nFor general Sandpack conventions (code style, naming, file naming, line highlighting, hidden files, CSS guidelines), see `/docs-sandpack`. This skill covers only RSC-specific patterns.\n\n## Quick Start Template\n\nMinimal single-file `<SandpackRSC>` example:\n\n```mdx\n<SandpackRSC>\n\n` ` `js src/App.js\nexport default function App() {\n  return <h1>Hello from a Server Component!</h1>;\n}\n` ` `\n\n</SandpackRSC>\n```\n\n---\n\n## How It Differs from `<Sandpack>`\n\n| Feature | `<Sandpack>` | `<SandpackRSC>` |\n|---------|-------------|-----------------|\n| Execution model | All code runs in iframe | Server code runs in Web Worker, client code in iframe |\n| `'use client'` directive | Ignored (everything is client) | Required to mark client components |\n| `'use server'` directive | Not supported | Marks Server Functions callable from client |\n| `async` components | Not supported | Supported (server components can be async) |\n| External dependencies | Supported via `package.json` | Not supported (only React + react-dom) |\n| Entry point | `App.js` with `export default` | `src/App.js` with `export default` |\n| Component tag | `<Sandpack>` | `<SandpackRSC>` |\n\n---\n\n## File Directives\n\nFiles are classified by the directive at the top of the file:\n\n| Directive | Where it runs | Rules |\n|-----------|--------------|-------|\n| (none) | Web Worker (server) | Default. Can be `async`. Can import other server files. Cannot use hooks, event handlers, or browser APIs. |\n| `'use client'` | Sandpack iframe (browser) | Must be first statement. Can use hooks, event handlers, browser APIs. Cannot be `async`. Cannot import server files. |\n| `'use server'` | Web Worker (server) | Marks Server Functions. Can be module-level (all exports are actions) or function-level. Callable from client via props or form `action`. |\n\n---\n\n## Common Patterns\n\n### 1. Server + Client Components\n\n```mdx\n<SandpackRSC>\n\n` ` `js src/App.js\nimport Counter from './Counter';\n\nexport default function App() {\n  return (\n    <div>\n      <h1>Server-rendered heading</h1>\n      <Counter />\n    </div>\n  );\n}\n` ` `\n\n` ` `js src/Counter.js\n'use client';\n\nimport { useState } from 'react';\n\nexport default function Counter() {\n  const [count, setCount] = useState(0);\n  return (\n    <button onClick={() => setCount(count + 1)}>\n      Count: {count}\n    </button>\n  );\n}\n` ` `\n\n</SandpackRSC>\n```\n\n### 2. Async Server Component with Suspense\n\n```mdx\n<SandpackRSC>\n\n` ` `js src/App.js\nimport { Suspense } from 'react';\nimport Albums from './Albums';\n\nexport default function App() {\n  return (\n    <Suspense fallback={<p>Loading...</p>}>\n      <Albums />\n    </Suspense>\n  );\n}\n` ` `\n\n` ` `js src/Albums.js\nasync function fetchAlbums() {\n  await new Promise(resolve => setTimeout(resolve, 1000));\n  return ['Abbey Road', 'Let It Be', 'Revolver'];\n}\n\nexport default async function Albums() {\n  const albums = await fetchAlbums();\n  return (\n    <ul>\n      {albums.map(album => (\n        <li key={album}>{album}</li>\n      ))}\n    </ul>\n  );\n}\n` ` `\n\n</SandpackRSC>\n```\n\n### 3. Server Functions (Actions)\n\n```mdx\n<SandpackRSC>\n\n` ` `js src/App.js\nimport { addLike, getLikeCount } from './actions';\nimport LikeButton from './LikeButton';\n\nexport default async function App() {\n  const count = await getLikeCount();\n  return (\n    <div>\n      <p>Likes: {count}</p>\n      <LikeButton addLike={addLike} />\n    </div>\n  );\n}\n` ` `\n\n` ` `js src/actions.js\n'use server';\n\nlet count = 0;\n\nexport async function addLike() {\n  count++;\n}\n\nexport async function getLikeCount() {\n  return count;\n}\n` ` `\n\n` ` `js src/LikeButton.js\n'use client';\n\nexport default function LikeButton({ addLike }) {\n  return (\n    <form action={addLike}>\n      <button type=\"submit\">Like</button>\n    </form>\n  );\n}\n` ` `\n\n</SandpackRSC>\n```\n\n---\n\n## File Structure Requirements\n\n### Entry Point\n\n- **`src/App.js` is required** as the main entry point\n- Must have `export default` (function component)\n- Case-insensitive fallback: `src/app.js` also works\n\n### Auto-Injected Infrastructure Files\n\nThese files are automatically injected by `sandpack-rsc-setup.ts` and should never be included in MDX:\n\n| File | Purpose |\n|------|---------|\n| `/src/index.js` | Bootstraps the RSC pipeline |\n| `/src/rsc-client.js` | Client bridge — creates Worker, consumes Flight stream |\n| `/src/rsc-server.js` | Wraps pre-bundled worker runtime as ES module |\n| `/node_modules/__webpack_shim__/index.js` | Minimal webpack compatibility layer |\n| `/node_modules/__rsdw_client__/index.js` | `react-server-dom-webpack/client` as local dependency |\n\n### No External Dependencies\n\n`<SandpackRSC>` does not support external npm packages. Only `react` and `react-dom` are available. Do not include `package.json` in RSC examples.\n\n---\n\n## Architecture Reference\n\n### Three-Layer Architecture\n\n```\nreact.dev page (Next.js)\n  ┌─────────────────────────────────────────┐\n  │  <SandpackRSC>                          │\n  │  ┌─────────┐  ┌──────────────────────┐  │\n  │  │ Editor  │  │ Preview (iframe)     │  │\n  │  │ App.js  │  │ Client React app     │  │\n  │  │ (edit)  │  │ consumes Flight      │  │\n  │  │         │  │ stream from Worker   │  │\n  │  └─────────┘  └──────────┬───────────┘  │\n  └───────────────────────────┼─────────────┘\n                              │ postMessage\n  ┌───────────────────────────▼─────────────┐\n  │  Web Worker (Blob URL)                  │\n  │  - React server build (pre-bundled)     │\n  │  - react-server-dom-webpack/server      │\n  │  - webpack shim                         │\n  │  - User server code (Sucrase → CJS)    │\n  └─────────────────────────────────────────┘\n```\n\n### Key Source Files\n\n| File                                                            | Purpose                                                                        |\n|-----------------------------------------------------------------|--------------------------------------------------------------------------------|\n| `src/components/MDX/Sandpack/sandpack-rsc/RscFileBridge.tsx`    | Monitors Sandpack; posts raw files to iframe                                   |\n| `src/components/MDX/Sandpack/SandpackRSCRoot.tsx`               | SandpackProvider setup, custom bundler URL, UI layout                          |\n| `src/components/MDX/Sandpack/templateRSC.ts`                    | RSC template files                                                             |\n| `.../sandbox-code/src/__react_refresh_init__.js`                | React Refresh shim                                                             |\n| `.../sandbox-code/src/rsc-server.js`                            | Worker runtime: module system, Sucrase compilation, `renderToReadableStream()` |\n| `.../sandbox-code/src/rsc-client.source.js`                     | Client bridge: Worker creation, file classification, Flight stream consumption |\n| `.../sandbox-code/src/webpack-shim.js`                          | Minimal `__webpack_require__` / `__webpack_module_cache__` shim                |\n| `.../sandbox-code/src/worker-bundle.dist.js`                    | Pre-bundled IIFE (generated): React server + RSDW/server + Sucrase             |\n| `scripts/buildRscWorker.mjs`                                    | esbuild script: bundles rsc-server.js into worker-bundle.dist.js               |\n\n---\n\n## Build System\n\n### Rebuilding the Worker Bundle\n\nAfter modifying `rsc-server.js` or `webpack-shim.js`:\n\n```bash\nnode scripts/buildRscWorker.mjs\n```\n\nThis runs esbuild with:\n- `format: 'iife'`, `platform: 'browser'`\n- `conditions: ['react-server', 'browser']` (activates React server export conditions)\n- `minify: true`\n- Prepends `webpack-shim.js` to the output\n\n### Raw-Loader Configuration\n\nIn `templateRSC.js` files are loaded as raw strings with the `!raw-loader`.\n\nThe strings are necessary to provide to Sandpack as local files (skips Sandpack bundling). \n\n\n### Development Commands\n\n```bash\nnode scripts/buildRscWorker.mjs   # Rebuild worker bundle after source changes\nyarn dev                           # Start dev server to test examples\n```\n"
  },
  {
    "path": ".claude/skills/docs-sandpack/SKILL.md",
    "content": "---\nname: docs-sandpack\ndescription: Use when adding interactive code examples to React docs.\n---\n\n# Sandpack Patterns\n\n## Quick Start Template\n\nMost examples are single-file. Copy this and modify:\n\n```mdx\n<Sandpack>\n\n` ` `js\nimport { useState } from 'react';\n\nexport default function Example() {\n  const [value, setValue] = useState(0);\n\n  return (\n    <button onClick={() => setValue(value + 1)}>\n      Clicked {value} times\n    </button>\n  );\n}\n` ` `\n\n</Sandpack>\n```\n\n---\n\n## File Naming\n\n| Pattern | Usage |\n|---------|-------|\n| ` ```js ` | Main file (no prefix) |\n| ` ```js src/FileName.js ` | Supporting files |\n| ` ```js src/File.js active ` | Active file (reference pages) |\n| ` ```js src/data.js hidden ` | Hidden files |\n| ` ```css ` | CSS styles |\n| ` ```json package.json ` | External dependencies |\n\n**Critical:** Main file must have `export default`.\n\n## Line Highlighting\n\n```mdx\n```js {2-4}\nfunction Example() {\n  // Lines 2-4\n  // will be\n  // highlighted\n  return null;\n}\n```\n\n## Code References (numbered callouts)\n\n```mdx\n```js [[1, 4, \"age\"], [2, 4, \"setAge\"]]\n// Creates numbered markers pointing to \"age\" and \"setAge\" on line 4\n```\n\n## Expected Errors (intentionally broken examples)\n\n```mdx\n```js {expectedErrors: {'react-compiler': [7]}}\n// Line 7 shows as expected error\n```\n\n## Multi-File Example\n\n```mdx\n<Sandpack>\n\n```js src/App.js\nimport Gallery from './Gallery.js';\n\nexport default function App() {\n  return <Gallery />;\n}\n```\n\n```js src/Gallery.js\nexport default function Gallery() {\n  return <h1>Gallery</h1>;\n}\n```\n\n```css\nh1 { color: purple; }\n```\n\n</Sandpack>\n```\n\n## External Dependencies\n\n```mdx\n<Sandpack>\n\n```js\nimport { useImmer } from 'use-immer';\n// ...\n```\n\n```json package.json\n{\n  \"dependencies\": {\n    \"immer\": \"1.7.3\",\n    \"use-immer\": \"0.5.1\",\n    \"react\": \"latest\",\n    \"react-dom\": \"latest\",\n    \"react-scripts\": \"latest\"\n  }\n}\n```\n\n</Sandpack>\n```\n\n## Code Style in Sandpack (Required)\n\nSandpack examples are held to strict code style standards:\n\n1. **Function declarations** for components (not arrows)\n2. **`e`** for event parameters\n3. **Single quotes** in JSX\n4. **`const`** unless reassignment needed\n5. **Spaces in destructuring**: `({ props })` not `({props})`\n6. **Two-line createRoot**: separate declaration and render call\n7. **Multiline if statements**: always use braces\n\n### Don't Create Hydration Mismatches\n\nSandpack examples must produce the same output on server and client:\n\n```js\n// 🚫 This will cause hydration warnings\nexport default function App() {\n  const isClient = typeof window !== 'undefined';\n  return <div>{isClient ? 'Client' : 'Server'}</div>;\n}\n```\n\n### Use Ref for Non-Rendered State\n\n```js\n// 🚫 Don't trigger re-renders for non-visual state\nconst [mounted, setMounted] = useState(false);\nuseEffect(() => { setMounted(true); }, []);\n\n// ✅ Use ref instead\nconst mounted = useRef(false);\nuseEffect(() => { mounted.current = true; }, []);\n```\n\n## forwardRef and memo Patterns\n\n### forwardRef - Use Named Function\n```js\n// ✅ Named function for DevTools display name\nconst MyInput = forwardRef(function MyInput(props, ref) {\n  return <input {...props} ref={ref} />;\n});\n\n// 🚫 Anonymous loses name\nconst MyInput = forwardRef((props, ref) => { ... });\n```\n\n### memo - Use Named Function\n```js\n// ✅ Preserves component name\nconst Greeting = memo(function Greeting({ name }) {\n  return <h1>Hello, {name}</h1>;\n});\n```\n\n## Line Length\n\n- Prose: ~80 characters\n- Code: ~60-70 characters\n- Break long lines to avoid horizontal scrolling\n\n## Anti-Patterns\n\n| Pattern | Problem | Fix |\n|---------|---------|-----|\n| `const Comp = () => {}` | Not standard | `function Comp() {}` |\n| `onClick={(event) => ...}` | Conflicts with global | `onClick={(e) => ...}` |\n| `useState` for non-rendered values | Re-renders | Use `useRef` |\n| Reading `window` during render | Hydration mismatch | Check in useEffect |\n| Single-line if without braces | Harder to debug | Use multiline with braces |\n| Chained `createRoot().render()` | Less clear | Two statements |\n| `//...` without space | Inconsistent | `// ...` with space |\n| Tabs | Inconsistent | 2 spaces |\n| `ReactDOM.render` | Deprecated | Use `createRoot` |\n| Fake package names | Confusing | Use `'./your-storage-layer'` |\n| `PropsWithChildren` | Outdated | `children?: ReactNode` |\n| Missing `key` in lists | Warnings | Always include key |\n\n## Additional Code Quality Rules\n\n### Always Include Keys in Lists\n```js\n// ✅ Correct\n{items.map(item => <li key={item.id}>{item.name}</li>)}\n\n// 🚫 Wrong - missing key\n{items.map(item => <li>{item.name}</li>)}\n```\n\n### Use Realistic Import Paths\n```js\n// ✅ Correct - descriptive path\nimport { fetchData } from './your-data-layer';\n\n// 🚫 Wrong - looks like a real npm package\nimport { fetchData } from 'cool-data-lib';\n```\n\n### Console.log Labels\n```js\n// ✅ Correct - labeled for clarity\nconsole.log('User:', user);\nconsole.log('Component Stack:', errorInfo.componentStack);\n\n// 🚫 Wrong - unlabeled\nconsole.log(user);\n```\n\n### Keep Delays Reasonable\n```js\n// ✅ Correct - 1-1.5 seconds\nsetTimeout(() => setLoading(false), 1000);\n\n// 🚫 Wrong - too long, feels sluggish\nsetTimeout(() => setLoading(false), 3000);\n```\n\n## Updating Line Highlights\n\nWhen modifying code in examples with line highlights (`{2-4}`), **always update the highlight line numbers** to match the new code. Incorrect line numbers cause rendering crashes.\n\n## File Name Conventions\n\n- Capitalize file names for component files: `Gallery.js` not `gallery.js`\n- After initially explaining files are in `src/`, refer to files by name only: `Gallery.js` not `src/Gallery.js`\n\n## Naming Conventions in Code\n\n**Components:** PascalCase\n- `Profile`, `Avatar`, `TodoList`, `PackingList`\n\n**State variables:** Destructured pattern\n- `const [count, setCount] = useState(0)`\n- Booleans: `[isOnline, setIsOnline]`, `[isPacked, setIsPacked]`\n- Status strings: `'typing'`, `'submitting'`, `'success'`, `'error'`\n\n**Event handlers:**\n- `handleClick`, `handleSubmit`, `handleAddTask`\n\n**Props for callbacks:**\n- `onClick`, `onChange`, `onAddTask`, `onSelect`\n\n**Custom Hooks:**\n- `useOnlineStatus`, `useChatRoom`, `useFormInput`\n\n**Reducer actions:**\n- Past tense: `'added'`, `'changed'`, `'deleted'`\n- Snake_case compounds: `'changed_selection'`, `'sent_message'`\n\n**Updater functions:** Single letter\n- `setCount(n => n + 1)`\n\n### Pedagogical Code Markers\n\n**Wrong vs right code:**\n```js\n// 🔴 Avoid: redundant state and unnecessary Effect\n// ✅ Good: calculated during rendering\n```\n\n**Console.log for lifecycle teaching:**\n```js\nconsole.log('✅ Connecting...');\nconsole.log('❌ Disconnected.');\n```\n\n### Server/Client Labeling\n\n```js\n// Server Component\nasync function Notes() {\n  const notes = await db.notes.getAll();\n}\n\n// Client Component\n\"use client\"\nexport default function Expandable({children}) {\n  const [expanded, setExpanded] = useState(false);\n}\n```\n\n### Bundle Size Annotations\n\n```js\nimport marked from 'marked'; // 35.9K (11.2K gzipped)\nimport sanitizeHtml from 'sanitize-html'; // 206K (63.3K gzipped)\n```\n\n---\n\n## Sandpack Example Guidelines\n\n### Package.json Rules\n\n**Include package.json when:**\n- Using external npm packages (immer, remarkable, leaflet, toastify-js, etc.)\n- Demonstrating experimental/canary React features\n- Requiring specific React versions (`react: beta`, `react: 19.0.0-rc-*`)\n\n**Omit package.json when:**\n- Example uses only built-in React features\n- No external dependencies needed\n- Teaching basic hooks, state, or components\n\n**Always mark package.json as hidden:**\n```mdx\n```json package.json hidden\n{\n  \"dependencies\": {\n    \"react\": \"latest\",\n    \"react-dom\": \"latest\",\n    \"react-scripts\": \"latest\",\n    \"immer\": \"1.7.3\"\n  }\n}\n```\n```\n\n**Version conventions:**\n- Use `\"latest\"` for stable features\n- Use exact versions only when compatibility requires it\n- Include minimal dependencies (just what the example needs)\n\n### Hidden File Patterns\n\n**Always hide these file types:**\n\n| File Type | Reason |\n|-----------|--------|\n| `package.json` | Configuration not the teaching point |\n| `sandbox.config.json` | Sandbox setup is boilerplate |\n| `public/index.html` | HTML structure not the focus |\n| `src/data.js` | When it contains sample/mock data |\n| `src/api.js` | When showing API usage, not implementation |\n| `src/styles.css` | When styling is not the lesson |\n| `src/router.js` | Supporting infrastructure |\n| `src/actions.js` | Server action implementation details |\n\n**Rationale:**\n- Reduces cognitive load\n- Keeps focus on the primary concept\n- Creates cleaner, more focused examples\n\n**Example:**\n```mdx\n```js src/data.js hidden\nexport const items = [\n  { id: 1, name: 'Item 1' },\n  { id: 2, name: 'Item 2' },\n];\n```\n```\n\n### Active File Patterns\n\n**Mark as active when:**\n- File contains the primary teaching concept\n- Learner should focus on this code first\n- Component demonstrates the hook/pattern being taught\n\n**Effect of the `active` marker:**\n- Sets initial editor tab focus when Sandpack loads\n- Signals \"this is what you should study\"\n- Works with hidden files to create focused examples\n\n**Most common active file:** `src/index.js` or `src/App.js`\n\n**Example:**\n```mdx\n```js src/App.js active\n// This file will be focused when example loads\nexport default function App() {\n  // ...\n}\n```\n```\n\n### File Structure Guidelines\n\n| Scenario | Structure | Reason |\n|----------|-----------|--------|\n| Basic hook usage | Single file | Simple, focused |\n| Teaching imports | 2-3 files | Shows modularity |\n| Context patterns | 4-5 files | Realistic structure |\n| Complex state | 3+ files | Separation of concerns |\n\n**Single File Examples (70% of cases):**\n- Use for simple concepts\n- 50-200 lines typical\n- Best for: Counter, text inputs, basic hooks\n\n**Multi-File Examples (30% of cases):**\n- Use when teaching modularity/imports\n- Use for context patterns (4-5 files)\n- Use when component is reused\n\n**File Naming:**\n- Main component: `App.js` (capitalized)\n- Component files: `Gallery.js`, `Button.js` (capitalized)\n- Data files: `data.js` (lowercase)\n- Utility files: `utils.js` (lowercase)\n- Context files: `TasksContext.js` (named after what they provide)\n\n### Code Size Limits\n\n- Single file: **<200 lines**\n- Multi-file total: **150-300 lines**\n- Main component: **100-150 lines**\n- Supporting files: **20-40 lines each**\n\n### CSS Guidelines\n\n**Always:**\n- Include minimal CSS for demo interactivity\n- Use semantic class names (`.panel`, `.button-primary`, `.panel-dark`)\n- Support light/dark themes when showing UI concepts\n- Keep CSS visible (never hidden)\n\n**Size Guidelines:**\n- Minimal (5-10 lines): Basic button styling, spacing\n- Medium (15-30 lines): Panel styling, form layouts\n- Complex (40+ lines): Only for layout-focused examples\n"
  },
  {
    "path": ".claude/skills/docs-voice/SKILL.md",
    "content": "---\nname: docs-voice\ndescription: Use when writing any React documentation. Provides voice, tone, and style rules for all doc types.\n---\n\n# React Docs Voice & Style\n\n## Universal Rules\n\n- **Capitalize React terms** when referring to the React concept in headings or as standalone concepts:\n  - Core: Hook, Effect, State, Context, Ref, Component, Fragment\n  - Concurrent: Transition, Action, Suspense\n  - Server: Server Component, Client Component, Server Function, Server Action\n  - Patterns: Error Boundary\n  - Canary: Activity, View Transition, Transition Type\n  - **In prose:** Use lowercase when paired with descriptors: \"state variable\", \"state updates\", \"event handler\". Capitalize when the concept stands alone or in headings: \"State is isolated and private\"\n  - General usage stays lowercase: \"the page transitions\", \"takes an action\"\n- **Product names:** ESLint, TypeScript, JavaScript, Next.js (not lowercase)\n- **Bold** for key concepts: **state variable**, **event handler**\n- **Italics** for new terms being defined: *event handlers*\n- **Inline code** for APIs: `useState`, `startTransition`, `<Suspense>`\n- **Avoid:** \"simple\", \"easy\", \"just\", time estimates\n- Frame differences as \"capabilities\" not \"advantages/disadvantages\"\n- Avoid passive voice and jargon\n\n## Tone by Page Type\n\n| Type | Tone | Example |\n|------|------|---------|\n| Learn | Conversational | \"Here's what that looks like...\", \"You might be wondering...\" |\n| Reference | Technical | \"Call `useState` at the top level...\", \"This Hook returns...\" |\n| Blog | Accurate | Focus on facts, not marketing |\n\n**Note:** Pitfall and DeepDive components can use slightly more conversational phrasing (\"You might wonder...\", \"It might be tempting...\") even in Reference pages, since they're explanatory asides.\n\n## Avoiding Jargon\n\n**Pattern:** Explain behavior first, then name it.\n\n✅ \"React waits until all code in event handlers runs before processing state updates. This is called *batching*.\"\n\n❌ \"React uses batching to process state updates atomically.\"\n\n**Terms to avoid or explain:**\n| Jargon | Plain Language |\n|--------|----------------|\n| atomic | all-or-nothing, batched together |\n| idempotent | same inputs, same output |\n| deterministic | predictable, same result every time |\n| memoize | remember the result, skip recalculating |\n| referentially transparent | (avoid - describe the behavior) |\n| invariant | rule that must always be true |\n| reify | (avoid - describe what's being created) |\n\n**Allowed technical terms in Reference pages:**\n- \"stale closures\" - standard JS/React term, can be used in Caveats\n- \"stable identity\" - React term for consistent object references across renders\n- \"reactive\" - React term for values that trigger re-renders when changed\n- These don't need explanation in Reference pages (readers are expected to know them)\n\n**Use established analogies sparingly—once when introducing a concept, not repeatedly:**\n\n| Concept | Analogy |\n|---------|---------|\n| Components/React | Kitchen (components as cooks, React as waiter) |\n| Render phases | Restaurant ordering (trigger/render/commit) |\n| State batching | Waiter collecting full order before going to kitchen |\n| State behavior | Snapshot/photograph in time |\n| State storage | React storing state \"on a shelf\" |\n| State purpose | Component's memory |\n| Pure functions | Recipes (same ingredients → same dish) |\n| Pure functions | Math formulas (y = 2x) |\n| Props | Adjustable \"knobs\" |\n| Children prop | \"Hole\" to be filled by parent |\n| Keys | File names in a folder |\n| Curly braces in JSX | \"Window into JavaScript\" |\n| Declarative UI | Taxi driver (destination, not turn-by-turn) |\n| Imperative UI | Turn-by-turn navigation |\n| State structure | Database normalization |\n| Refs | \"Secret pocket\" React doesn't track |\n| Effects/Refs | \"Escape hatch\" from React |\n| Context | CSS inheritance / \"Teleportation\" |\n| Custom Hooks | Design system |\n\n## Common Prose Patterns\n\n**Wrong vs Right code:**\n```mdx\n\\`\\`\\`js\n// 🚩 Don't mutate state:\nobj.x = 10;\n\\`\\`\\`\n\n\\`\\`\\`js\n// ✅ Replace with new object:\nsetObj({ ...obj, x: 10 });\n\\`\\`\\`\n```\n\n**Table comparisons:**\n```mdx\n| passing a function | calling a function |\n| `onClick={handleClick}` | `onClick={handleClick()}` |\n```\n\n**Linking:**\n```mdx\n[Read about state](/learn/state-a-components-memory)\n[See `useState` reference](/reference/react/useState)\n```\n\n## Code Style\n\n- Prefer JSX over createElement\n- Use const/let, never var\n- Prefer named function declarations for top-level functions\n- Arrow functions for callbacks that need `this` preservation\n\n## Version Documentation\n\nWhen APIs change between versions:\n\n```mdx\nStarting in React 19, render `<Context>` as a provider:\n\\`\\`\\`js\n<SomeContext value={value}>{children}</SomeContext>\n\\`\\`\\`\n\nIn older versions:\n\\`\\`\\`js\n<SomeContext.Provider value={value}>{children}</SomeContext.Provider>\n\\`\\`\\`\n```\n\nPatterns:\n- \"Starting in React 19...\" for new APIs\n- \"In older versions of React...\" for legacy patterns\n"
  },
  {
    "path": ".claude/skills/docs-writer-blog/SKILL.md",
    "content": "---\nname: docs-writer-blog\ndescription: Use when writing or editing files in src/content/blog/. Provides blog post structure and conventions.\n---\n\n# Blog Post Writer\n\n## Persona\n\n**Voice:** Official React team voice\n**Tone:** Accurate, professional, forward-looking\n\n## Voice & Style\n\nFor tone, capitalization, jargon, and prose patterns, invoke `/docs-voice`.\n\n---\n\n## Frontmatter Schema\n\nAll blog posts use this YAML frontmatter structure:\n\n```yaml\n---\ntitle: \"Title in Quotes\"\nauthor: Author Name(s)\ndate: YYYY/MM/DD\ndescription: One or two sentence summary.\n---\n```\n\n### Field Details\n\n| Field | Format | Example |\n|-------|--------|---------|\n| `title` | Quoted string | `\"React v19\"`, `\"React Conf 2024 Recap\"` |\n| `author` | Unquoted, comma + \"and\" for multiple | `The React Team`, `Dan Abramov and Lauren Tan` |\n| `date` | `YYYY/MM/DD` with forward slashes | `2024/12/05` |\n| `description` | 1-2 sentences, often mirrors intro | Summarizes announcement or content |\n\n### Title Patterns by Post Type\n\n| Type | Pattern | Example |\n|------|---------|---------|\n| Release | `\"React vX.Y\"` or `\"React X.Y\"` | `\"React v19\"` |\n| Upgrade | `\"React [VERSION] Upgrade Guide\"` | `\"How to Upgrade to React 18\"` |\n| Labs | `\"React Labs: [Topic] – [Month Year]\"` | `\"React Labs: What We've Been Working On – February 2024\"` |\n| Conf | `\"React Conf [YEAR] Recap\"` | `\"React Conf 2024 Recap\"` |\n| Feature | `\"Introducing [Feature]\"` or descriptive | `\"Introducing react.dev\"` |\n| Security | `\"[Severity] Security Vulnerability in [Component]\"` | `\"Critical Security Vulnerability in React Server Components\"` |\n\n---\n\n## Author Byline\n\nImmediately after frontmatter, add a byline:\n\n```markdown\n---\n\nMonth DD, YYYY by [Author Name](social-link)\n\n---\n```\n\n### Conventions\n\n- Full date spelled out: `December 05, 2024`\n- Team posts link to `/community/team`: `[The React Team](/community/team)`\n- Individual authors link to Twitter/X or Bluesky\n- Multiple authors: Oxford comma before \"and\"\n- Followed by horizontal rule `---`\n\n**Examples:**\n\n```markdown\nDecember 05, 2024 by [The React Team](/community/team)\n\n---\n```\n\n```markdown\nMay 3, 2023 by [Dan Abramov](https://bsky.app/profile/danabra.mov), [Sophie Alpert](https://twitter.com/sophiebits), and [Andrew Clark](https://twitter.com/acdlite)\n\n---\n```\n\n---\n\n## Universal Post Structure\n\nAll blog posts follow this structure:\n\n1. **Frontmatter** (YAML)\n2. **Author byline** with date\n3. **Horizontal rule** (`---`)\n4. **`<Intro>` component** (1-3 sentences)\n5. **Horizontal rule** (`---`) (optional)\n6. **Main content sections** (H2 with IDs)\n7. **Closing section** (Changelog, Thanks, etc.)\n\n---\n\n## Post Type Templates\n\n### Major Release Announcement\n\n```markdown\n---\ntitle: \"React vX.Y\"\nauthor: The React Team\ndate: YYYY/MM/DD\ndescription: React X.Y is now available on npm! In this post, we'll give an overview of the new features.\n---\n\nMonth DD, YYYY by [The React Team](/community/team)\n\n---\n\n<Intro>\n\nReact vX.Y is now available on npm!\n\n</Intro>\n\nIn our [Upgrade Guide](/blog/YYYY/MM/DD/react-xy-upgrade-guide), we shared step-by-step instructions for upgrading. In this post, we'll give an overview of what's new.\n\n- [What's new in React X.Y](#whats-new)\n- [Improvements](#improvements)\n- [How to upgrade](#how-to-upgrade)\n\n---\n\n## What's new in React X.Y {/*whats-new*/}\n\n### Feature Name {/*feature-name*/}\n\n[Problem this solves. Before/after code examples.]\n\nFor more information, see the docs for [`Feature`](/reference/react/Feature).\n\n---\n\n## Improvements in React X.Y {/*improvements*/}\n\n### Improvement Name {/*improvement-name*/}\n\n[Description of improvement.]\n\n---\n\n## How to upgrade {/*how-to-upgrade*/}\n\nSee [How to Upgrade to React X.Y](/blog/YYYY/MM/DD/react-xy-upgrade-guide) for step-by-step instructions.\n\n---\n\n## Changelog {/*changelog*/}\n\n### React {/*react*/}\n\n* Add `useNewHook` for [purpose]. ([#12345](https://github.com/facebook/react/pull/12345) by [@contributor](https://github.com/contributor))\n\n---\n\n_Thanks to [Name](url) for reviewing this post._\n```\n\n### Upgrade Guide\n\n```markdown\n---\ntitle: \"React [VERSION] Upgrade Guide\"\nauthor: Author Name\ndate: YYYY/MM/DD\ndescription: Step-by-step instructions for upgrading to React [VERSION].\n---\n\nMonth DD, YYYY by [Author Name](social-url)\n\n---\n\n<Intro>\n\n[Summary of upgrade and what this guide covers.]\n\n</Intro>\n\n<Note>\n\n#### Stepping stone version {/*stepping-stone*/}\n\n[If applicable, describe intermediate upgrade steps.]\n\n</Note>\n\nIn this post, we will guide you through the steps for upgrading:\n\n- [Installing](#installing)\n- [Codemods](#codemods)\n- [Breaking changes](#breaking-changes)\n- [New deprecations](#new-deprecations)\n\n---\n\n## Installing {/*installing*/}\n\n```bash\nnpm install --save-exact react@^X.Y.Z react-dom@^X.Y.Z\n```\n\n## Codemods {/*codemods*/}\n\n<Note>\n\n#### Run all React [VERSION] codemods {/*run-all-codemods*/}\n\n```bash\nnpx codemod@latest react/[VERSION]/migration-recipe\n```\n\n</Note>\n\n## Breaking changes {/*breaking-changes*/}\n\n### Removed: `apiName` {/*removed-api-name*/}\n\n`apiName` was deprecated in [Month YYYY (vX.X.X)](link).\n\n```js\n// Before\n[old code]\n\n// After\n[new code]\n```\n\n<Note>\n\nCodemod [description]:\n\n```bash\nnpx codemod@latest react/[VERSION]/codemod-name\n```\n\n</Note>\n\n## New deprecations {/*new-deprecations*/}\n\n### Deprecated: `apiName` {/*deprecated-api-name*/}\n\n[Explanation and migration path.]\n\n---\n\nThanks to [Contributor](link) for reviewing this post.\n```\n\n### React Labs Research Update\n\n```markdown\n---\ntitle: \"React Labs: What We've Been Working On – [Month Year]\"\nauthor: Author1, Author2, and Author3\ndate: YYYY/MM/DD\ndescription: In React Labs posts, we write about projects in active research and development.\n---\n\nMonth DD, YYYY by [Author1](url), [Author2](url), and [Author3](url)\n\n---\n\n<Intro>\n\nIn React Labs posts, we write about projects in active research and development. We've made significant progress since our [last update](/blog/previous-labs-post), and we'd like to share our progress.\n\n</Intro>\n\n[Optional: Roadmap disclaimer about timelines]\n\n---\n\n## Feature Name {/*feature-name*/}\n\n<Note>\n\n`<FeatureName />` is now available in React's Canary channel.\n\n</Note>\n\n[Description of feature, motivation, current status.]\n\n### Subsection {/*subsection*/}\n\n[Details, examples, use cases.]\n\n---\n\n## Research Area {/*research-area*/}\n\n[Problem space description. Status communication.]\n\nThis research is still early. We'll share more when we're further along.\n\n---\n\n_Thanks to [Reviewer](url) for reviewing this post._\n\nThanks for reading, and see you in the next update!\n```\n\n### React Conf Recap\n\n```markdown\n---\ntitle: \"React Conf [YEAR] Recap\"\nauthor: Author1 and Author2\ndate: YYYY/MM/DD\ndescription: Last week we hosted React Conf [YEAR]. In this post, we'll summarize the talks and announcements.\n---\n\nMonth DD, YYYY by [Author1](url) and [Author2](url)\n\n---\n\n<Intro>\n\nLast week we hosted React Conf [YEAR] [where we announced [key announcements]].\n\n</Intro>\n\n---\n\nThe entire [day 1](youtube-url) and [day 2](youtube-url) streams are available online.\n\n## Day 1 {/*day-1*/}\n\n_[Watch the full day 1 stream here.](youtube-url)_\n\n[Description of day 1 opening and keynote highlights.]\n\nWatch the full day 1 keynote here:\n\n<YouTubeIframe src=\"https://www.youtube.com/embed/VIDEO_ID\" />\n\n## Day 2 {/*day-2*/}\n\n_[Watch the full day 2 stream here.](youtube-url)_\n\n[Day 2 summary.]\n\n<YouTubeIframe src=\"https://www.youtube.com/embed/VIDEO_ID\" />\n\n## Q&A {/*q-and-a*/}\n\n* [Q&A Title](youtube-url) hosted by [Host](url)\n\n## And more... {/*and-more*/}\n\nWe also heard talks including:\n* [Talk Title](youtube-url) by [Speaker](url)\n\n## Thank you {/*thank-you*/}\n\nThank you to all the staff, speakers, and participants who made React Conf [YEAR] possible.\n\nSee you next time!\n```\n\n### Feature/Tool Announcement\n\n```markdown\n---\ntitle: \"Introducing [Feature Name]\"\nauthor: Author Name\ndate: YYYY/MM/DD\ndescription: Today we are announcing [feature]. In this post, we'll explain [what this post covers].\n---\n\nMonth DD, YYYY by [Author Name](url)\n\n---\n\n<Intro>\n\nToday we are [excited/thrilled] to announce [feature]. [What this means for users.]\n\n</Intro>\n\n---\n\n## tl;dr {/*tldr*/}\n\n* Key announcement point with [relevant link](/path).\n* What users can do now.\n* Availability or adoption information.\n\n## What is [Feature]? {/*what-is-feature*/}\n\n[Explanation of the feature/tool.]\n\n## Why we built this {/*why-we-built-this*/}\n\n[Motivation, history, problem being solved.]\n\n## Getting started {/*getting-started*/}\n\nTo install [feature]:\n\n<TerminalBlock>\nnpm install package-name\n</TerminalBlock>\n\n[You can find more documentation here.](/path/to/docs)\n\n## What's next {/*whats-next*/}\n\n[Future plans and next steps.]\n\n## Thank you {/*thank-you*/}\n\n[Acknowledgments to contributors.]\n\n---\n\nThanks to [Reviewer](url) for reviewing this post.\n```\n\n### Security Announcement\n\n```markdown\n---\ntitle: \"[Severity] Security Vulnerability in [Component]\"\nauthor: The React Team\ndate: YYYY/MM/DD\ndescription: Brief summary of the vulnerability. A fix has been published. We recommend upgrading immediately.\n\n---\n\nMonth DD, YYYY by [The React Team](/community/team)\n\n---\n\n<Intro>\n\n[One or two sentences summarizing the vulnerability.]\n\nWe recommend upgrading immediately.\n\n</Intro>\n\n---\n\nOn [date], [researcher] reported a security vulnerability that allows [description].\n\nThis vulnerability was disclosed as [CVE-YYYY-NNNNN](https://www.cve.org/CVERecord?id=CVE-YYYY-NNNNN) and is rated CVSS [score].\n\nThe vulnerability is present in versions [list] of:\n\n* [package-name](https://www.npmjs.com/package/package-name)\n\n## Immediate Action Required {/*immediate-action-required*/}\n\nA fix was introduced in versions [linked versions]. Upgrade immediately.\n\n### Affected frameworks {/*affected-frameworks*/}\n\n[List of affected frameworks with npm links.]\n\n### Vulnerability overview {/*vulnerability-overview*/}\n\n[Technical explanation of the vulnerability.]\n\n## Update Instructions {/*update-instructions*/}\n\n### Framework Name {/*update-framework-name*/}\n\n```bash\nnpm install package@version\n```\n\n## Timeline {/*timeline*/}\n\n* **November 29th**: [Researcher] reported the vulnerability.\n* **December 1st**: Fix was created and validated.\n* **December 3rd**: Fix published and CVE disclosed.\n\n## Attribution {/*attribution*/}\n\nThank you to [Researcher Name](url) for discovering and reporting this vulnerability.\n```\n\n---\n\n## Heading Conventions\n\n### ID Syntax\n\nAll headings require IDs using CSS comment syntax:\n\n```markdown\n## Heading Text {/*heading-id*/}\n```\n\n### ID Rules\n\n- Lowercase\n- Kebab-case (hyphens for spaces)\n- Remove special characters (apostrophes, colons, backticks)\n- Concise but descriptive\n\n### Heading Patterns\n\n| Context | Example |\n|---------|---------|\n| Feature section | `## New Feature: Automatic Batching {/*new-feature-automatic-batching*/}` |\n| New hook | `### New hook: \\`useActionState\\` {/*new-hook-useactionstate*/}` |\n| API in backticks | `### \\`<Activity />\\` {/*activity*/}` |\n| Removed API | `#### Removed: \\`propTypes\\` {/*removed-proptypes*/}` |\n| tl;dr section | `## tl;dr {/*tldr*/}` |\n\n---\n\n## Component Usage Guide\n\n### Blog-Appropriate Components\n\n| Component | Usage in Blog |\n|-----------|---------------|\n| `<Intro>` | **Required** - Opening summary after byline |\n| `<Note>` | Callouts, caveats, important clarifications |\n| `<Pitfall>` | Warnings about common mistakes |\n| `<DeepDive>` | Optional technical deep dives (use sparingly) |\n| `<TerminalBlock>` | CLI/installation commands |\n| `<ConsoleBlock>` | Console error/warning output |\n| `<ConsoleBlockMulti>` | Multi-line console output |\n| `<YouTubeIframe>` | Conference video embeds |\n| `<Diagram>` | Visual explanations |\n| `<InlineToc />` | Auto-generated table of contents |\n\n### `<Intro>` Pattern\n\nAlways wrap opening paragraph:\n\n```markdown\n<Intro>\n\nReact 19 is now available on npm!\n\n</Intro>\n```\n\n### `<Note>` Patterns\n\n**Simple note:**\n```markdown\n<Note>\n\nFor React Native users, React 18 ships with the New Architecture.\n\n</Note>\n```\n\n**Titled note (H4 inside):**\n```markdown\n<Note>\n\n#### React 18.3 has also been published {/*react-18-3*/}\n\nTo help with the upgrade, we've published `react@18.3`...\n\n</Note>\n```\n\n### `<TerminalBlock>` Pattern\n\n```markdown\n<TerminalBlock>\nnpm install react@latest react-dom@latest\n</TerminalBlock>\n```\n\n### `<YouTubeIframe>` Pattern\n\n```markdown\n<YouTubeIframe src=\"https://www.youtube.com/embed/VIDEO_ID\" />\n```\n\n---\n\n## Link Patterns\n\n### Internal Links\n\n| Type | Pattern | Example |\n|------|---------|---------|\n| Blog post | `/blog/YYYY/MM/DD/slug` | `/blog/2024/12/05/react-19` |\n| API reference | `/reference/react/HookName` | `/reference/react/useState` |\n| Learn section | `/learn/topic-name` | `/learn/react-compiler` |\n| Community | `/community/team` | `/community/team` |\n\n### External Links\n\n| Type | Pattern |\n|------|---------|\n| GitHub PR | `[#12345](https://github.com/facebook/react/pull/12345)` |\n| GitHub user | `[@username](https://github.com/username)` |\n| Twitter/X | `[@username](https://x.com/username)` |\n| Bluesky | `[Name](https://bsky.app/profile/handle)` |\n| CVE | `[CVE-YYYY-NNNNN](https://www.cve.org/CVERecord?id=CVE-YYYY-NNNNN)` |\n| npm package | `[package](https://www.npmjs.com/package/package)` |\n\n### \"See docs\" Pattern\n\n```markdown\nFor more information, see the docs for [`useActionState`](/reference/react/useActionState).\n```\n\n---\n\n## Changelog Format\n\n### Bullet Pattern\n\n```markdown\n* Add `useTransition` for concurrent rendering. ([#10426](https://github.com/facebook/react/pull/10426) by [@acdlite](https://github.com/acdlite))\n* Fix `useReducer` observing incorrect props. ([#22445](https://github.com/facebook/react/pull/22445) by [@josephsavona](https://github.com/josephsavona))\n```\n\n**Structure:** `Verb` + backticked API + description + `([#PR](url) by [@user](url))`\n\n**Verbs:** Add, Fix, Remove, Make, Improve, Allow, Deprecate\n\n### Section Organization\n\n```markdown\n## Changelog {/*changelog*/}\n\n### React {/*react*/}\n\n* [changes]\n\n### React DOM {/*react-dom*/}\n\n* [changes]\n```\n\n---\n\n## Acknowledgments Format\n\n### Post-closing Thanks\n\n```markdown\n---\n\nThanks to [Name](url), [Name](url), and [Name](url) for reviewing this post.\n```\n\nOr italicized:\n\n```markdown\n_Thanks to [Name](url) for reviewing this post._\n```\n\n### Update Notes\n\nFor post-publication updates:\n\n```markdown\n<Note>\n\n[Updated content]\n\n-----\n\n_Updated January 26, 2026._\n\n</Note>\n```\n\n---\n\n## Tone & Length by Post Type\n\n| Type | Tone | Length | Key Elements |\n|------|------|--------|--------------|\n| Release | Celebratory, informative | Medium-long | Feature overview, upgrade link, changelog |\n| Upgrade | Instructional, precise | Long | Step-by-step, codemods, breaking changes |\n| Labs | Transparent, exploratory | Medium | Status updates, roadmap disclaimers |\n| Conf | Enthusiastic, community-focused | Medium | YouTube embeds, speaker credits |\n| Feature | Excited, explanatory | Medium | tl;dr, \"why\", getting started |\n| Security | Urgent, factual | Short-medium | Immediate action, timeline, CVE |\n\n---\n\n## Do's and Don'ts\n\n**Do:**\n- Focus on facts over marketing\n- Say \"upcoming\" explicitly for unreleased features\n- Include FAQ sections for major announcements\n- Credit contributors and link to GitHub\n- Use \"we\" voice for team posts\n- Link to upgrade guides from release posts\n- Include table of contents for long posts\n- End with acknowledgments\n\n**Don't:**\n- Promise features not yet available\n- Rewrite history (add update notes instead)\n- Break existing URLs\n- Use hyperbolic language (\"revolutionary\", \"game-changing\")\n- Skip the `<Intro>` component\n- Forget heading IDs\n- Use heavy component nesting in blogs\n- Make time estimates or predictions\n\n---\n\n## Updating Old Posts\n\n- Never break existing URLs; add redirects when URLs change\n- Don't rewrite history; add update notes instead:\n  ```markdown\n  <Note>\n\n  [Updated information]\n\n  -----\n\n  _Updated Month Year._\n\n  </Note>\n  ```\n\n---\n\n## Critical Rules\n\n1. **Heading IDs required:** `## Title {/*title-id*/}`\n2. **`<Intro>` required:** Every post starts with `<Intro>` component\n3. **Byline required:** Date + linked author(s) after frontmatter\n4. **Date format:** Frontmatter uses `YYYY/MM/DD`, byline uses `Month DD, YYYY`\n5. **Link to docs:** New APIs must link to reference documentation\n6. **Security posts:** Always include \"We recommend upgrading immediately\"\n\n---\n\n## Components Reference\n\nFor complete MDX component patterns, invoke `/docs-components`.\n\nBlog posts commonly use: `<Intro>`, `<Note>`, `<Pitfall>`, `<TerminalBlock>`, `<ConsoleBlock>`, `<YouTubeIframe>`, `<DeepDive>`, `<Diagram>`.\n\nPrefer inline explanations over heavy component usage.\n"
  },
  {
    "path": ".claude/skills/docs-writer-learn/SKILL.md",
    "content": "---\nname: docs-writer-learn\ndescription: Use when writing or editing files in src/content/learn/. Provides Learn page structure and tone.\n---\n\n# Learn Page Writer\n\n## Persona\n\n**Voice:** Patient teacher guiding a friend through concepts\n**Tone:** Conversational, warm, encouraging\n\n## Voice & Style\n\nFor tone, capitalization, jargon, and prose patterns, invoke `/docs-voice`.\n\n## Page Structure Variants\n\n### 1. Standard Learn Page (Most Common)\n\n```mdx\n---\ntitle: Page Title\n---\n\n<Intro>\n1-3 sentences introducing the concept. Use *italics* for new terms.\n</Intro>\n\n<YouWillLearn>\n\n* Learning outcome 1\n* Learning outcome 2\n* Learning outcome 3-5\n\n</YouWillLearn>\n\n## Section Name {/*section-id*/}\n\nContent with Sandpack examples, Pitfalls, Notes, DeepDives...\n\n## Another Section {/*another-section*/}\n\nMore content...\n\n<Recap>\n\n* Summary point 1\n* Summary point 2\n* Summary points 3-9\n\n</Recap>\n\n<Challenges>\n\n#### Challenge title {/*challenge-id*/}\n\nDescription...\n\n<Hint>\nOptional guidance (single paragraph)\n</Hint>\n\n<Sandpack>\n{/* Starting code */}\n</Sandpack>\n\n<Solution>\nExplanation...\n\n<Sandpack>\n{/* Fixed code */}\n</Sandpack>\n</Solution>\n\n</Challenges>\n```\n\n### 2. Chapter Introduction Page\n\nFor pages that introduce a chapter (like describing-the-ui.md, managing-state.md):\n\n```mdx\n<YouWillLearn isChapter={true}>\n\n* [Sub-page title](/learn/sub-page-name) to learn...\n* [Another page](/learn/another-page) to learn...\n\n</YouWillLearn>\n\n## Preview Section {/*section-id*/}\n\nPreview description with mini Sandpack example\n\n<LearnMore path=\"/learn/sub-page-name\">\n\nRead **[Page Title](/learn/sub-page-name)** to learn how to...\n\n</LearnMore>\n\n## What's next? {/*whats-next*/}\n\nHead over to [First Page](/learn/first-page) to start reading this chapter page by page!\n```\n\n**Important:** Chapter intro pages do NOT include `<Recap>` or `<Challenges>` sections.\n\n### 3. Tutorial Page\n\nFor step-by-step tutorials (like tutorial-tic-tac-toe.md):\n\n```mdx\n<Intro>\nBrief statement of what will be built\n</Intro>\n\n<Note>\nAlternative learning path offered\n</Note>\n\nTable of contents (prose listing of major sections)\n\n## Setup {/*setup*/}\n...\n\n## Main Content {/*main-content*/}\nProgressive code building with ### subsections\n\nNo YouWillLearn, Recap, or Challenges\n\nEnds with ordered list of \"extra credit\" improvements\n```\n\n### 4. Reference-Style Learn Page\n\nFor pages with heavy API documentation (like typescript.md):\n\n```mdx\n<YouWillLearn>\n\n* [Link to section](#section-anchor)\n* [Link to another section](#another-section)\n\n</YouWillLearn>\n\n## Sections with ### subsections\n\n## Further learning {/*further-learning*/}\n\nNo Recap or Challenges\n```\n\n## Heading ID Conventions\n\nAll headings require IDs in `{/*kebab-case*/}` format:\n\n```markdown\n## Section Title {/*section-title*/}\n### Subsection Title {/*subsection-title*/}\n#### DeepDive Title {/*deepdive-title*/}\n```\n\n**ID Generation Rules:**\n- Lowercase everything\n- Replace spaces with hyphens\n- Remove apostrophes, quotes\n- Remove or convert special chars (`:`, `?`, `!`, `.`, parentheses)\n\n**Examples:**\n- \"What's React?\" → `{/*whats-react*/}`\n- \"Step 1: Create the context\" → `{/*step-1-create-the-context*/}`\n- \"Conditional (ternary) operator (? :)\" → `{/*conditional-ternary-operator--*/}`\n\n## Teaching Patterns\n\n### Problem-First Teaching\n\nShow broken/problematic code BEFORE the solution:\n\n1. Present problematic approach with `// 🔴 Avoid:` comment\n2. Explain WHY it's wrong (don't just say it is)\n3. Show the solution with `// ✅ Good:` comment\n4. Invite experimentation\n\n### Progressive Complexity\n\nBuild understanding in layers:\n1. Show simplest working version\n2. Identify limitation or repetition\n3. Introduce solution incrementally\n4. Show complete solution\n5. Invite experimentation: \"Try changing...\"\n\n### Numbered Step Patterns\n\nFor multi-step processes:\n\n**As section headings:**\n```markdown\n### Step 1: Action to take {/*step-1-action*/}\n### Step 2: Next action {/*step-2-next-action*/}\n```\n\n**As inline lists:**\n```markdown\nTo implement this:\n1. **Declare** `inputRef` with the `useRef` Hook.\n2. **Pass it** as `<input ref={inputRef}>`.\n3. **Read** the input DOM node from `inputRef.current`.\n```\n\n### Interactive Invitations\n\nAfter Sandpack examples, encourage experimentation:\n- \"Try changing X to Y. See how...?\"\n- \"Try it in the sandbox above!\"\n- \"Click each button separately:\"\n- \"Have a guess!\"\n- \"Verify that...\"\n\n### Decision Questions\n\nHelp readers build intuition:\n> \"When you're not sure whether some code should be in an Effect or in an event handler, ask yourself *why* this code needs to run.\"\n\n## Component Placement Order\n\n1. `<Intro>` - First after frontmatter\n2. `<YouWillLearn>` - After Intro (standard/chapter pages)\n3. Body content with `<Note>`, `<Pitfall>`, `<DeepDive>` placed contextually\n4. `<Recap>` - Before Challenges (standard pages only)\n5. `<Challenges>` - End of page (standard pages only)\n\nFor component structure and syntax, invoke `/docs-components`.\n\n## Code Examples\n\nFor Sandpack file structure, naming conventions, code style, and pedagogical markers, invoke `/docs-sandpack`.\n\n## Cross-Referencing\n\n### When to Link\n\n**Link to /learn:**\n- Explaining concepts or mental models\n- Teaching how things work together\n- Tutorials and guides\n- \"Why\" questions\n\n**Link to /reference:**\n- API details, Hook signatures\n- Parameter lists and return values\n- Rules and restrictions\n- \"What exactly\" questions\n\n### Link Formats\n\n```markdown\n[concept name](/learn/page-name)\n[`useState`](/reference/react/useState)\n[section link](/learn/page-name#section-id)\n[MDN](https://developer.mozilla.org/...)\n```\n\n## Section Dividers\n\n**Important:** Learn pages typically do NOT use `---` dividers. The heading hierarchy provides sufficient structure. Only consider dividers in exceptional cases like separating main content from meta/contribution sections.\n\n## Do's and Don'ts\n\n**Do:**\n- Use \"you\" to address the reader\n- Show broken code before fixes\n- Explain behavior before naming concepts\n- Build concepts progressively\n- Include interactive Sandpack examples\n- Use established analogies consistently\n- Place Pitfalls AFTER explaining concepts\n- Invite experimentation with \"Try...\" phrases\n\n**Don't:**\n- Use \"simple\", \"easy\", \"just\", or time estimates\n- Reference concepts not yet introduced\n- Skip required components for page type\n- Use passive voice without reason\n- Place Pitfalls before teaching the concept\n- Use `---` dividers between sections\n- Create unnecessary abstraction in examples\n- Place consecutive Pitfalls or Notes without separating prose (combine or separate)\n\n## Critical Rules\n\n1. **All headings require IDs:** `## Title {/*title-id*/}`\n2. **Chapter intros use `isChapter={true}` and `<LearnMore>`**\n3. **Tutorial pages omit YouWillLearn/Recap/Challenges**\n4. **Problem-first teaching:** Show broken → explain → fix\n5. **No consecutive Pitfalls/Notes:** See `/docs-components` Callout Spacing Rules\n\nFor component patterns, invoke `/docs-components`. For Sandpack patterns, invoke `/docs-sandpack`.\n"
  },
  {
    "path": ".claude/skills/docs-writer-reference/SKILL.md",
    "content": "---\nname: docs-writer-reference\ndescription: Reference page structure, templates, and writing patterns for src/content/reference/. For components, see /docs-components. For code examples, see /docs-sandpack.\n---\n\n# Reference Page Writer\n\n## Quick Reference\n\n### Page Type Decision Tree\n\n1. Is it a Hook? Use **Type A (Hook/Function)**\n2. Is it a React component (`<Something>`)? Use **Type B (Component)**\n3. Is it a compiler configuration option? Use **Type C (Configuration)**\n4. Is it a directive (`'use something'`)? Use **Type D (Directive)**\n5. Is it an ESLint rule? Use **Type E (ESLint Rule)**\n6. Is it listing multiple APIs? Use **Type F (Index/Category)**\n\n### Component Selection\n\nFor component selection and patterns, invoke `/docs-components`.\n\n---\n\n## Voice & Style\n\n**Voice:** Authoritative technical reference writer\n**Tone:** Precise, comprehensive, neutral\n\nFor tone, capitalization, jargon, and prose patterns, invoke `/docs-voice`.\n\n**Do:**\n- Start with single-line description: \"`useState` is a React Hook that lets you...\"\n- Include Parameters, Returns, Caveats sections for every API\n- Document edge cases most developers will encounter\n- Use section dividers between major sections\n- Include \"See more examples below\" links\n- Be assertive, not hedging - \"This is designed for...\" not \"This helps avoid issues with...\"\n- State facts, not benefits - \"The callback always accesses the latest values\" not \"This helps avoid stale closures\"\n- Use minimal but meaningful names - `onEvent` or `onTick` over `onSomething`\n\n**Don't:**\n- Skip the InlineToc component\n- Omit error cases or caveats\n- Use conversational language\n- Mix teaching with reference (that's Learn's job)\n- Document past bugs or fixed issues\n- Include niche edge cases (e.g., `this` binding, rare class patterns)\n- Add phrases explaining \"why you'd want this\" - the Usage section examples do that\n- Exception: Pitfall and DeepDive asides can use slightly conversational phrasing\n\n---\n\n## Page Templates\n\n### Type A: Hook/Function\n\n**When to use:** Documenting React hooks and standalone functions (useState, useEffect, memo, lazy, etc.)\n\n```mdx\n---\ntitle: hookName\n---\n\n<Intro>\n\n`hookName` is a React Hook that lets you [brief description].\n\n```js\nconst result = hookName(arg)\n```\n\n</Intro>\n\n<InlineToc />\n\n---\n\n## Reference {/*reference*/}\n\n### `hookName(arg)` {/*hookname*/}\n\nCall `hookName` at the top level of your component to...\n\n```js\n[signature example with annotations]\n```\n\n[See more examples below.](#usage)\n\n#### Parameters {/*parameters*/}\n* `arg`: Description of the parameter.\n\n#### Returns {/*returns*/}\nDescription of return value.\n\n#### Caveats {/*caveats*/}\n* Important caveat about usage.\n\n---\n\n## Usage {/*usage*/}\n\n### Common Use Case {/*common-use-case*/}\nExplanation with Sandpack examples...\n\n---\n\n## Troubleshooting {/*troubleshooting*/}\n\n### Common Problem {/*common-problem*/}\nHow to solve it...\n```\n\n---\n\n### Type B: Component\n\n**When to use:** Documenting React components (Suspense, Fragment, Activity, StrictMode)\n\n```mdx\n---\ntitle: <ComponentName>\n---\n\n<Intro>\n\n`<ComponentName>` lets you [primary action].\n\n```js\n<ComponentName prop={value}>\n  <Children />\n</ComponentName>\n```\n\n</Intro>\n\n<InlineToc />\n\n---\n\n## Reference {/*reference*/}\n\n### `<ComponentName>` {/*componentname*/}\n\n[Component purpose and behavior]\n\n#### Props {/*props*/}\n\n* `propName`: Description of the prop...\n* **optional** `optionalProp`: Description...\n\n#### Caveats {/*caveats*/}\n\n* [Caveats specific to this component]\n```\n\n**Key differences from Hook pages:**\n- Title uses JSX syntax: `<ComponentName>`\n- Uses `#### Props` instead of `#### Parameters`\n- Reference heading uses JSX: `` ### `<ComponentName>` ``\n\n---\n\n### Type C: Configuration\n\n**When to use:** Documenting React Compiler configuration options\n\n```mdx\n---\ntitle: optionName\n---\n\n<Intro>\n\nThe `optionName` option [controls/specifies/determines] [what it does].\n\n</Intro>\n\n```js\n{\n  optionName: 'value' // Quick example\n}\n```\n\n<InlineToc />\n\n---\n\n## Reference {/*reference*/}\n\n### `optionName` {/*optionname*/}\n\n[Description of the option's purpose]\n\n#### Type {/*type*/}\n\n```\n'value1' | 'value2' | 'value3'\n```\n\n#### Default value {/*default-value*/}\n\n`'value1'`\n\n#### Options {/*options*/}\n\n- **`'value1'`** (default): Description\n- **`'value2'`**: Description\n- **`'value3'`**: Description\n\n#### Caveats {/*caveats*/}\n\n* [Usage caveats]\n```\n\n---\n\n### Type D: Directive\n\n**When to use:** Documenting directives like 'use server', 'use client', 'use memo'\n\n```mdx\n---\ntitle: \"'use directive'\"\ntitleForTitleTag: \"'use directive' directive\"\n---\n\n<RSC>\n\n`'use directive'` is for use with [React Server Components](/reference/rsc/server-components).\n\n</RSC>\n\n<Intro>\n\n`'use directive'` marks [what it marks] for [purpose].\n\n```js {1}\nfunction MyComponent() {\n  'use directive';\n  // ...\n}\n```\n\n</Intro>\n\n<InlineToc />\n\n---\n\n## Reference {/*reference*/}\n\n### `'use directive'` {/*use-directive*/}\n\nAdd `'use directive'` at the beginning of [location] to [action].\n\n#### Caveats {/*caveats*/}\n\n* `'use directive'` must be at the very beginning...\n* The directive must be written with single or double quotes, not backticks.\n* [Other placement/syntax caveats]\n```\n\n**Key characteristics:**\n- Title includes quotes: `title: \"'use server'\"`\n- Uses `titleForTitleTag` for browser tab title\n- `<RSC>` block appears before `<Intro>`\n- Caveats focus on placement and syntax requirements\n\n---\n\n### Type E: ESLint Rule\n\n**When to use:** Documenting ESLint plugin rules\n\n```mdx\n---\ntitle: rule-name\n---\n\n<Intro>\nValidates that [what the rule checks].\n</Intro>\n\n## Rule Details {/*rule-details*/}\n\n[Explanation of why this rule exists and React's underlying assumptions]\n\n## Common Violations {/*common-violations*/}\n\n[Description of violation patterns]\n\n### Invalid {/*invalid*/}\n\nExamples of incorrect code for this rule:\n\n```js\n// X Missing dependency\nuseEffect(() => {\n  console.log(count);\n}, []); // Missing 'count'\n```\n\n### Valid {/*valid*/}\n\nExamples of correct code for this rule:\n\n```js\n// checkmark All dependencies included\nuseEffect(() => {\n  console.log(count);\n}, [count]);\n```\n\n## Troubleshooting {/*troubleshooting*/}\n\n### [Problem description] {/*problem-slug*/}\n\n[Solution]\n\n## Options {/*options*/}\n\n[Configuration options if applicable]\n```\n\n**Key characteristics:**\n- Intro is a single \"Validates that...\" sentence\n- Uses \"Invalid\"/\"Valid\" sections with emoji-prefixed code comments\n- Rule Details explains \"why\" not just \"what\"\n\n---\n\n### Type F: Index/Category\n\n**When to use:** Overview pages listing multiple APIs in a category\n\n```mdx\n---\ntitle: \"Built-in React [Type]\"\n---\n\n<Intro>\n\n*Concept* let you [purpose]. Brief scope statement.\n\n</Intro>\n\n---\n\n## Category Name {/*category-name*/}\n\n*Concept* explanation with [Learn section link](/learn/topic).\n\nTo [action], use one of these [Type]:\n\n* [`apiName`](/reference/react/apiName) lets you [action].\n* [`apiName`](/reference/react/apiName) declares [thing].\n\n```js\nfunction Example() {\n  const value = useHookName(args);\n}\n```\n\n---\n\n## Your own [Type] {/*your-own-type*/}\n\nYou can also [define your own](/learn/topic) as JavaScript functions.\n```\n\n**Key characteristics:**\n- Title format: \"Built-in React [Type]\"\n- Italicized concept definitions\n- Horizontal rules between sections\n- Closes with \"Your own [Type]\" section\n\n---\n\n## Advanced Patterns\n\n### Multi-Function Documentation\n\n**When to use:** When a hook returns a function that needs its own documentation (useState's setter, useReducer's dispatch)\n\n```md\n### `hookName(args)` {/*hookname*/}\n\n[Main hook documentation]\n\n#### Parameters {/*parameters*/}\n#### Returns {/*returns*/}\n#### Caveats {/*caveats*/}\n\n---\n\n### `set` functions, like `setSomething(nextState)` {/*setstate*/}\n\nThe `set` function returned by `hookName` lets you [action].\n\n#### Parameters {/*setstate-parameters*/}\n#### Returns {/*setstate-returns*/}\n#### Caveats {/*setstate-caveats*/}\n```\n\n**Key conventions:**\n- Horizontal rule (`---`) separates main hook from returned function\n- Heading IDs include prefix: `{/*setstate-parameters*/}` vs `{/*parameters*/}`\n- Use generic names: \"set functions\" not \"setCount\"\n\n---\n\n### Compound Return Objects\n\n**When to use:** When a function returns an object with multiple properties/methods (createContext)\n\n```md\n### `createContext(defaultValue)` {/*createcontext*/}\n\n[Main function documentation]\n\n#### Returns {/*returns*/}\n\n`createContext` returns a context object.\n\n**The context object itself does not hold any information.** It represents...\n\n* `SomeContext` lets you provide the context value.\n* `SomeContext.Consumer` is an alternative way to read context.\n\n---\n\n### `SomeContext` Provider {/*provider*/}\n\n[Documentation for Provider]\n\n#### Props {/*provider-props*/}\n\n---\n\n### `SomeContext.Consumer` {/*consumer*/}\n\n[Documentation for Consumer]\n\n#### Props {/*consumer-props*/}\n```\n\n---\n\n## Writing Patterns\n\n### Opening Lines by Page Type\n\n| Page Type | Pattern | Example |\n|-----------|---------|---------|\n| Hook | `` `hookName` is a React Hook that lets you [action]. `` | \"`useState` is a React Hook that lets you add a state variable to your component.\" |\n| Component | `` `<ComponentName>` lets you [action]. `` | \"`<Suspense>` lets you display a fallback until its children have finished loading.\" |\n| API | `` `apiName` lets you [action]. `` | \"`memo` lets you skip re-rendering a component when its props are unchanged.\" |\n| Configuration | `` The `optionName` option [controls/specifies/determines] [what]. `` | \"The `target` option specifies which React version the compiler generates code for.\" |\n| Directive | `` `'directive'` [marks/opts/prevents] [what] for [purpose]. `` | \"`'use server'` marks a function as callable from the client.\" |\n| ESLint Rule | `` Validates that [condition]. `` | \"Validates that dependency arrays for React hooks contain all necessary dependencies.\" |\n\n---\n\n### Parameter Patterns\n\n**Simple parameter:**\n```md\n* `paramName`: Description of what it does.\n```\n\n**Optional parameter:**\n```md\n* **optional** `paramName`: Description of what it does.\n```\n\n**Parameter with special function behavior:**\n```md\n* `initialState`: The value you want the state to be initially. It can be a value of any type, but there is a special behavior for functions. This argument is ignored after the initial render.\n  * If you pass a function as `initialState`, it will be treated as an _initializer function_. It should be pure, should take no arguments, and should return a value of any type.\n```\n\n**Callback parameter with sub-parameters:**\n```md\n* `subscribe`: A function that takes a single `callback` argument and subscribes it to the store. When the store changes, it should invoke the provided `callback`. The `subscribe` function should return a function that cleans up the subscription.\n```\n\n**Nested options object:**\n```md\n* **optional** `options`: An object with options for this React root.\n  * **optional** `onCaughtError`: Callback called when React catches an error in an Error Boundary.\n  * **optional** `onUncaughtError`: Callback called when an error is thrown and not caught.\n  * **optional** `identifierPrefix`: A string prefix React uses for IDs generated by `useId`.\n```\n\n---\n\n### Return Value Patterns\n\n**Single value return:**\n```md\n`hookName` returns the current value. The value will be the same as `initialValue` during the first render.\n```\n\n**Array return (numbered list):**\n```md\n`useState` returns an array with exactly two values:\n\n1. The current state. During the first render, it will match the `initialState` you have passed.\n2. The [`set` function](#setstate) that lets you update the state to a different value and trigger a re-render.\n```\n\n**Object return (bulleted list):**\n```md\n`createElement` returns a React element object with a few properties:\n\n* `type`: The `type` you have passed.\n* `props`: The `props` you have passed except for `ref` and `key`.\n* `ref`: The `ref` you have passed. If missing, `null`.\n* `key`: The `key` you have passed, coerced to a string. If missing, `null`.\n```\n\n**Promise return:**\n```md\n`prerender` returns a Promise:\n- If rendering is successful, the Promise will resolve to an object containing:\n  - `prelude`: a [Web Stream](MDN-link) of HTML.\n  - `postponed`: a JSON-serializable object for resumption.\n- If rendering fails, the Promise will be rejected.\n```\n\n**Wrapped function return:**\n```md\n`cache` returns a cached version of `fn` with the same type signature. It does not call `fn` in the process.\n\nWhen calling `cachedFn` with given arguments, it first checks if a cached result exists. If cached, it returns the result. If not, it calls `fn`, stores the result, and returns it.\n```\n\n---\n\n### Caveats Patterns\n\n**Standard Hook caveat (almost always first for Hooks):**\n```md\n* `useXxx` is a Hook, so you can only call it **at the top level of your component** or your own Hooks. You can't call it inside loops or conditions. If you need that, extract a new component and move the state into it.\n```\n\n**Stable identity caveat (for returned functions):**\n```md\n* The `set` function has a stable identity, so you will often see it omitted from Effect dependencies, but including it will not cause the Effect to fire.\n```\n\n**Strict Mode caveat:**\n```md\n* In Strict Mode, React will **call your render function twice** in order to help you find accidental impurities. This is development-only behavior and does not affect production.\n```\n\n**Caveat with code example:**\n```md\n* It's not recommended to _suspend_ a render based on a store value returned by `useSyncExternalStore`. For example, the following is discouraged:\n\n  ```js\n  const selectedProductId = useSyncExternalStore(...);\n  const data = use(fetchItem(selectedProductId)) // X Don't suspend based on store value\n  ```\n```\n\n**Canary caveat:**\n```md\n* <CanaryBadge /> If you want to pass `ref` to a Fragment, you can't use the `<>...</>` syntax.\n```\n\n---\n\n### Troubleshooting Patterns\n\n**Heading format (first person problem statements):**\n```md\n### I've updated the state, but logging gives me the old value {/*old-value*/}\n\n### My initializer or updater function runs twice {/*runs-twice*/}\n\n### I want to read the latest state from a callback {/*read-latest-state*/}\n```\n\n**Error message format:**\n```md\n### I'm getting an error: \"Too many re-renders\" {/*too-many-rerenders*/}\n\n### I'm getting an error: \"Rendered more hooks than during the previous render\" {/*more-hooks*/}\n```\n\n**Lint error format:**\n```md\n### I'm getting a lint error: \"[exact error message]\" {/*lint-error-slug*/}\n```\n\n**Problem-solution structure:**\n1. State the problem with code showing the issue\n2. Explain why it happens\n3. Provide the solution with corrected code\n4. Link to Learn section for deeper understanding\n\n---\n\n### Code Comment Conventions\n\nFor code comment conventions (wrong/right, legacy/recommended, server/client labeling, bundle size annotations), invoke `/docs-sandpack`.\n\n---\n\n### Link Description Patterns\n\n| Pattern | Example |\n|---------|---------|\n| \"lets you\" + action | \"`memo` lets you skip re-rendering when props are unchanged.\" |\n| \"declares\" + thing | \"`useState` declares a state variable that you can update directly.\" |\n| \"reads\" + thing | \"`useContext` reads and subscribes to a context.\" |\n| \"connects\" + thing | \"`useEffect` connects a component to an external system.\" |\n| \"Used with\" | \"Used with [`useContext`.](/reference/react/useContext)\" |\n| \"Similar to\" | \"Similar to [`useTransition`.](/reference/react/useTransition)\" |\n\n---\n\n## Component Patterns\n\nFor comprehensive MDX component patterns (Note, Pitfall, DeepDive, Recipes, Deprecated, RSC, Canary, Diagram, Code Steps), invoke `/docs-components`.\n\nFor Sandpack-specific patterns and code style, invoke `/docs-sandpack`.\n\n### Reference-Specific Component Rules\n\n**Component placement in Reference pages:**\n- `<RSC>` goes before `<Intro>` at top of page\n- `<Deprecated>` goes after `<Intro>` for page-level deprecation\n- `<Deprecated>` goes after method heading for method-level deprecation\n- `<Canary>` wrapper goes inline within `<Intro>`\n- `<CanaryBadge />` appears in headings, props lists, and caveats\n\n**Troubleshooting-specific components:**\n- Use first-person problem headings\n- Cross-reference Pitfall IDs when relevant\n\n**Callout spacing:**\n- Never place consecutive Pitfalls or consecutive Notes\n- Combine related warnings into one with titled subsections, or separate with prose/code\n- Consecutive DeepDives OK for multi-part explorations\n- See `/docs-components` Callout Spacing Rules\n\n---\n\n## Content Principles\n\n### Intro Section\n- **One sentence, ~15 words max** - State what the Hook does, not how it works\n- ✅ \"`useEffectEvent` is a React Hook that lets you separate events from Effects.\"\n- ❌ \"`useEffectEvent` is a React Hook that lets you extract non-reactive logic from your Effects into a reusable function called an Effect Event.\"\n\n### Reference Code Example\n- Show just the API call (5-10 lines), not a full component\n- Move full component examples to Usage section\n\n### Usage Section Structure\n1. **First example: Core mental model** - Show the canonical use case with simplest concrete example\n2. **Subsequent examples: Canonical use cases** - Name the *why* (e.g., \"Avoid reconnecting to external systems\"), show a concrete *how*\n   - Prefer broad canonical use cases over multiple narrow concrete examples\n   - The section title IS the teaching - \"When would I use this?\" should be answered by the heading\n\n### What to Include vs. Exclude\n- **Never** document past bugs or fixed issues\n- **Include** edge cases most developers will encounter\n- **Exclude** niche edge cases (e.g., `this` binding, rare class patterns)\n\n### Caveats Section\n- Include rules the linter enforces or that cause immediate errors\n- Include fundamental usage restrictions\n- Exclude implementation details unless they affect usage\n- Exclude repetition of things explained elsewhere\n- Keep each caveat to one sentence when possible\n\n### Troubleshooting Section\n- Error headings only: \"I'm getting an error: '[message]'\" format\n- Never document past bugs - if it's fixed, it doesn't belong here\n- Focus on errors developers will actually encounter today\n\n### DeepDive Content\n- **Goldilocks principle** - Deep enough for curious developers, short enough to not overwhelm\n- Answer \"why is it designed this way?\" - not exhaustive technical details\n- Readers who skip it should miss nothing essential for using the API\n- If the explanation is getting long, you're probably explaining too much\n\n---\n\n## Domain-Specific Guidance\n\n### Hooks\n\n**Returned function documentation:**\n- Document setter/dispatch functions as separate `###` sections\n- Use generic names: \"set functions\" not \"setCount\"\n- Include stable identity caveat for returned functions\n\n**Dependency array documentation:**\n- List what counts as reactive values\n- Explain when dependencies are ignored\n- Link to removing effect dependencies guide\n\n**Recipes usage:**\n- Group related examples with meaningful titleText\n- Each recipe has brief intro, Sandpack, and `<Solution />`\n\n---\n\n### Components\n\n**Props documentation:**\n- Use `#### Props` instead of `#### Parameters`\n- Mark optional props with `**optional**` prefix\n- Use `<CanaryBadge />` inline for canary-only props\n\n**JSX syntax in titles/headings:**\n- Frontmatter title: `title: <Suspense>`\n- Reference heading: `` ### `<Suspense>` {/*suspense*/} ``\n\n---\n\n### React-DOM\n\n**Common props linking:**\n```md\n`<input>` supports all [common element props.](/reference/react-dom/components/common#common-props)\n```\n\n**Props categorization:**\n- Controlled vs uncontrolled props grouped separately\n- Form-specific props documented with action patterns\n- MDN links for standard HTML attributes\n\n**Environment-specific notes:**\n```mdx\n<Note>\n\nThis API is specific to Node.js. Environments with [Web Streams](MDN-link), like Deno and modern edge runtimes, should use [`renderToReadableStream`](/reference/react-dom/server/renderToReadableStream) instead.\n\n</Note>\n```\n\n**Progressive enhancement:**\n- Document benefits for users without JavaScript\n- Explain Server Function + form action integration\n- Show hidden form field and `.bind()` patterns\n\n---\n\n### RSC\n\n**RSC banner (before Intro):**\nAlways place `<RSC>` component before `<Intro>` for Server Component-only APIs.\n\n**Serialization type lists:**\nWhen documenting Server Function arguments, list supported types:\n```md\nSupported types for Server Function arguments:\n\n* Primitives\n  * [string](MDN-link)\n  * [number](MDN-link)\n* Iterables containing serializable values\n  * [Array](MDN-link)\n  * [Map](MDN-link)\n\nNotably, these are not supported:\n* React elements, or [JSX](/learn/writing-markup-with-jsx)\n* Functions (other than Server Functions)\n```\n\n**Bundle size comparisons:**\n- Show \"Not included in bundle\" for server-only imports\n- Annotate client bundle sizes with gzip: `// 35.9K (11.2K gzipped)`\n\n---\n\n### Compiler\n\n**Configuration page structure:**\n- Type (union type or interface)\n- Default value\n- Options/Valid values with descriptions\n\n**Directive documentation:**\n- Placement requirements are critical\n- Mode interaction tables showing combinations\n- \"Use sparingly\" + \"Plan for removal\" patterns for escape hatches\n\n**Library author guides:**\n- Audience-first intro\n- Benefits/Why section\n- Numbered step-by-step setup\n\n---\n\n### ESLint\n\n**Rule Details section:**\n- Explain \"why\" not just \"what\"\n- Focus on React's underlying assumptions\n- Describe consequences of violations\n\n**Invalid/Valid sections:**\n- Standard intro: \"Examples of [in]correct code for this rule:\"\n- Use X emoji for invalid, checkmark for valid\n- Show inline comments explaining the violation\n\n**Configuration options:**\n- Show shared settings (preferred)\n- Show rule-level options (backward compatibility)\n- Note precedence when both exist\n\n---\n\n## Edge Cases\n\nFor deprecated, canary, and version-specific component patterns (placement, syntax, examples), invoke `/docs-components`.\n\n**Quick placement rules:**\n- `<Deprecated>` after `<Intro>` for page-level, after heading for method-level\n- `<Canary>` wrapper inline in Intro, `<CanaryBadge />` in headings/props/caveats\n- Version notes use `<Note>` with \"Starting in React 19...\" pattern\n\n**Removed APIs on index pages:**\n```md\n## Removed APIs {/*removed-apis*/}\n\nThese APIs were removed in React 19:\n\n* [`render`](https://18.react.dev/reference/react-dom/render): use [`createRoot`](/reference/react-dom/client/createRoot) instead.\n```\n\nLink to previous version docs (18.react.dev) for removed API documentation.\n\n---\n\n## Critical Rules\n\n1. **Heading IDs required:** `## Title {/*title-id*/}` (lowercase, hyphens)\n2. **Sandpack main file needs `export default`**\n3. **Active file syntax:** ` ```js src/File.js active `\n4. **Error headings in Troubleshooting:** Use `### I'm getting an error: \"[message]\" {/*id*/}`\n5. **Section dividers (`---`)** required between headings (see Section Dividers below)\n6. **InlineToc required:** Always include `<InlineToc />` after Intro\n7. **Consistent parameter format:** Use `* \\`paramName\\`: description` with `**optional**` prefix for optional params\n8. **Numbered lists for array returns:** When hooks return arrays, use numbered lists in Returns section\n9. **Generic names for returned functions:** Use \"set functions\" not \"setCount\"\n10. **Props vs Parameters:** Use `#### Props` for Components (Type B), `#### Parameters` for Hooks/APIs (Type A)\n11. **RSC placement:** `<RSC>` component goes before `<Intro>`, not after\n12. **Canary markers:** Use `<Canary>` wrapper inline in Intro, `<CanaryBadge />` in headings/props\n13. **Deprecated placement:** `<Deprecated>` goes after `<Intro>` for page-level, after heading for method-level\n14. **Code comment emojis:** Use X for wrong, checkmark for correct in code examples\n15. **No consecutive Pitfalls/Notes:** Combine into one component with titled subsections, or separate with prose/code (see `/docs-components`)\n\nFor component heading level conventions (DeepDive, Pitfall, Note, Recipe headings), see `/docs-components`.\n\n### Section Dividers\n\nUse `---` horizontal rules to visually separate major sections:\n\n- **After `<InlineToc />`** - Before `## Reference` heading\n- **Between API subsections** - Between different function/hook definitions (e.g., between `useState()` and `set functions`)\n- **Before `## Usage`** - Separates API reference from examples\n- **Before `## Troubleshooting`** - Separates content from troubleshooting\n- **Between EVERY Usage subsections** - When switching to a new major use case\n\nAlways have a blank line before and after `---`.\n\n### Section ID Conventions\n\n| Section | ID Format |\n|---------|-----------|\n| Main function | `{/*functionname*/}` |\n| Returned function | `{/*setstate*/}`, `{/*dispatch*/}` |\n| Sub-section of returned function | `{/*setstate-parameters*/}` |\n| Troubleshooting item | `{/*problem-description-slug*/}` |\n| Pitfall | `{/*pitfall-description*/}` |\n| Deep dive | `{/*deep-dive-topic*/}` |\n"
  },
  {
    "path": ".claude/skills/react-expert/SKILL.md",
    "content": "---\nname: react-expert\ndescription: Use when researching React APIs or concepts for documentation. Use when you need authoritative usage examples, caveats, warnings, or errors for a React feature.\n---\n\n# React Expert Research Skill\n\n## Overview\n\nThis skill produces exhaustive documentation research on any React API or concept by searching authoritative sources (tests, source code, PRs, issues) rather than relying on LLM training knowledge.\n\n<CRITICAL>\n**Skepticism Mandate:** You must be skeptical of your own knowledge. Claude is often trained on outdated or incorrect React patterns. Treat source material as the sole authority. If findings contradict your prior understanding, explicitly flag this discrepancy.\n\n**Red Flags - STOP if you catch yourself thinking:**\n- \"I know this API does X\" → Find source evidence first\n- \"Common pattern is Y\" → Verify in test files\n- Generating example code → Must have source file reference\n</CRITICAL>\n\n## Invocation\n\n```\n/react-expert useTransition\n/react-expert suspense boundaries\n/react-expert startTransition\n```\n\n## Sources (Priority Order)\n\n1. **React Repo Tests** - Most authoritative for actual behavior\n2. **React Source Code** - Warnings, errors, implementation details\n3. **Git History** - Commit messages with context\n4. **GitHub PRs & Comments** - Design rationale (via `gh` CLI)\n5. **GitHub Issues** - Confusion/questions (facebook/react + reactjs/react.dev)\n6. **React Working Group** - Design discussions for newer APIs\n7. **Flow Types** - Source of truth for type signatures\n8. **TypeScript Types** - Note discrepancies with Flow\n9. **Current react.dev docs** - Baseline (not trusted as complete)\n\n**No web search** - No Stack Overflow, blog posts, or web searches. GitHub API via `gh` CLI is allowed.\n\n## Workflow\n\n### Step 1: Setup React Repo\n\nFirst, ensure the React repo is available locally:\n\n```bash\n# Check if React repo exists, clone or update\nif [ -d \".claude/react\" ]; then\n  cd .claude/react && git pull origin main\nelse\n  git clone --depth=100 https://github.com/facebook/react.git .claude/react\nfi\n```\n\nGet the current commit hash for the research document:\n```bash\ncd .claude/react && git rev-parse --short HEAD\n```\n\n### Step 2: Dispatch 6 Parallel Research Agents\n\nSpawn these agents IN PARALLEL using the Task tool. Each agent receives the skepticism preamble:\n\n> \"You are researching React's `<TOPIC>`. CRITICAL: Do NOT rely on your prior knowledge about this API. Your training may contain outdated or incorrect patterns. Only report what you find in the source files. If your findings contradict common understanding, explicitly highlight this discrepancy.\"\n\n| Agent | subagent_type | Focus | Instructions |\n|-------|---------------|-------|--------------|\n| test-explorer | Explore | Test files for usage patterns | Search `.claude/react/packages/*/src/__tests__/` for test files mentioning the topic. Extract actual usage examples WITH file paths and line numbers. |\n| source-explorer | Explore | Warnings/errors in source | Search `.claude/react/packages/*/src/` for console.error, console.warn, and error messages mentioning the topic. Document trigger conditions. |\n| git-historian | Explore | Commit messages | Run `git log --all --grep=\"<topic>\" --oneline -50` in `.claude/react`. Read full commit messages for context. |\n| pr-researcher | Explore | PRs introducing/modifying API | Run `gh pr list -R facebook/react --search \"<topic>\" --state all --limit 20`. Read key PR descriptions and comments. |\n| issue-hunter | Explore | Issues showing confusion | Search issues in both `facebook/react` and `reactjs/react.dev` repos. Look for common questions and misunderstandings. |\n| types-inspector | Explore | Flow + TypeScript signatures | Find Flow types in `.claude/react/packages/*/src/*.js` (look for `@flow` annotations). Find TS types in `.claude/react/packages/*/index.d.ts`. Note discrepancies. |\n\n### Step 3: Agent Prompts\n\nUse these exact prompts when spawning agents:\n\n#### test-explorer\n```\nYou are researching React's <TOPIC>.\n\nCRITICAL: Do NOT rely on your prior knowledge about this API. Your training may contain outdated or incorrect patterns. Only report what you find in the source files.\n\nYour task: Find test files in .claude/react that demonstrate <TOPIC> usage.\n\n1. Search for test files: Glob for `**/__tests__/**/*<topic>*` and `**/__tests__/**/*.js` then grep for <topic>\n2. For each relevant test file, extract:\n   - The test description (describe/it blocks)\n   - The actual usage code\n   - Any assertions about behavior\n   - Edge cases being tested\n3. Report findings with exact file paths and line numbers\n\nFormat your output as:\n## Test File: <path>\n### Test: \"<test description>\"\n```javascript\n<exact code from test>\n```\n**Behavior:** <what the test asserts>\n```\n\n#### source-explorer\n```\nYou are researching React's <TOPIC>.\n\nCRITICAL: Do NOT rely on your prior knowledge about this API. Only report what you find in the source files.\n\nYour task: Find warnings, errors, and implementation details for <TOPIC>.\n\n1. Search .claude/react/packages/*/src/ for:\n   - console.error mentions of <topic>\n   - console.warn mentions of <topic>\n   - Error messages mentioning <topic>\n   - The main implementation file\n2. For each warning/error, document:\n   - The exact message text\n   - The condition that triggers it\n   - The source file and line number\n\nFormat your output as:\n## Warnings & Errors\n| Message | Trigger Condition | Source |\n|---------|------------------|--------|\n| \"<exact message>\" | <condition> | <file:line> |\n\n## Implementation Notes\n<key details from source code>\n```\n\n#### git-historian\n```\nYou are researching React's <TOPIC>.\n\nCRITICAL: Do NOT rely on your prior knowledge. Only report what you find in git history.\n\nYour task: Find commit messages that explain <TOPIC> design decisions.\n\n1. Run: cd .claude/react && git log --all --grep=\"<topic>\" --oneline -50\n2. For significant commits, read full message: git show <hash> --stat\n3. Look for:\n   - Initial introduction of the API\n   - Bug fixes (reveal edge cases)\n   - Behavior changes\n   - Deprecation notices\n\nFormat your output as:\n## Key Commits\n### <short hash> - <subject>\n**Date:** <date>\n**Context:** <why this change was made>\n**Impact:** <what behavior changed>\n```\n\n#### pr-researcher\n```\nYou are researching React's <TOPIC>.\n\nCRITICAL: Do NOT rely on your prior knowledge. Only report what you find in PRs.\n\nYour task: Find PRs that introduced or modified <TOPIC>.\n\n1. Run: gh pr list -R facebook/react --search \"<topic>\" --state all --limit 20 --json number,title,url\n2. For promising PRs, read details: gh pr view <number> -R facebook/react\n3. Look for:\n   - The original RFC/motivation\n   - Design discussions in comments\n   - Alternative approaches considered\n   - Breaking changes\n\nFormat your output as:\n## Key PRs\n### PR #<number>: <title>\n**URL:** <url>\n**Summary:** <what it introduced/changed>\n**Design Rationale:** <why this approach>\n**Discussion Highlights:** <key points from comments>\n```\n\n#### issue-hunter\n```\nYou are researching React's <TOPIC>.\n\nCRITICAL: Do NOT rely on your prior knowledge. Only report what you find in issues.\n\nYour task: Find issues that reveal common confusion about <TOPIC>.\n\n1. Search facebook/react: gh issue list -R facebook/react --search \"<topic>\" --state all --limit 20 --json number,title,url\n2. Search reactjs/react.dev: gh issue list -R reactjs/react.dev --search \"<topic>\" --state all --limit 20 --json number,title,url\n3. For each issue, identify:\n   - What the user was confused about\n   - What the resolution was\n   - Any gotchas revealed\n\nFormat your output as:\n## Common Confusion\n### Issue #<number>: <title>\n**Repo:** <facebook/react or reactjs/react.dev>\n**Confusion:** <what they misunderstood>\n**Resolution:** <correct understanding>\n**Gotcha:** <if applicable>\n```\n\n#### types-inspector\n```\nYou are researching React's <TOPIC>.\n\nCRITICAL: Do NOT rely on your prior knowledge. Only report what you find in type definitions.\n\nYour task: Find and compare Flow and TypeScript type signatures for <TOPIC>.\n\n1. Flow types (source of truth): Search .claude/react/packages/*/src/*.js for @flow annotations related to <topic>\n2. TypeScript types: Search .claude/react/packages/*/index.d.ts and @types/react\n3. Compare and note any discrepancies\n\nFormat your output as:\n## Flow Types (Source of Truth)\n**File:** <path>\n```flow\n<exact type definition>\n```\n\n## TypeScript Types\n**File:** <path>\n```typescript\n<exact type definition>\n```\n\n## Discrepancies\n<any differences between Flow and TS definitions>\n```\n\n### Step 4: Synthesize Results\n\nAfter all agents complete, combine their findings into a single research document.\n\n**DO NOT add information from your own knowledge.** Only include what agents found in sources.\n\n### Step 5: Save Output\n\nWrite the final document to `.claude/research/<topic>.md`\n\nReplace spaces in topic with hyphens (e.g., \"suspense boundaries\" → \"suspense-boundaries.md\")\n\n## Output Document Template\n\n```markdown\n# React Research: <topic>\n\n> Generated by /react-expert on YYYY-MM-DD\n> Sources: React repo (commit <hash>), N PRs, M issues\n\n## Summary\n\n[Brief summary based SOLELY on source findings, not prior knowledge]\n\n## API Signature\n\n### Flow Types (Source of Truth)\n\n[From types-inspector agent]\n\n### TypeScript Types\n\n[From types-inspector agent]\n\n### Discrepancies\n\n[Any differences between Flow and TS]\n\n## Usage Examples\n\n### From Tests\n\n[From test-explorer agent - with file:line references]\n\n### From PRs/Issues\n\n[Real-world patterns from discussions]\n\n## Caveats & Gotchas\n\n[Each with source link]\n\n- **<gotcha>** - Source: <link>\n\n## Warnings & Errors\n\n| Message | Trigger Condition | Source File |\n|---------|------------------|-------------|\n[From source-explorer agent]\n\n## Common Confusion\n\n[From issue-hunter agent]\n\n## Design Decisions\n\n[From git-historian and pr-researcher agents]\n\n## Source Links\n\n### Commits\n- <hash>: <description>\n\n### Pull Requests\n- PR #<number>: <title> - <url>\n\n### Issues\n- Issue #<number>: <title> - <url>\n```\n\n## Common Mistakes to Avoid\n\n1. **Trusting prior knowledge** - If you \"know\" something about the API, find the source evidence anyway\n2. **Generating example code** - Every code example must come from an actual source file\n3. **Skipping agents** - All 6 agents must run; each provides unique perspective\n4. **Summarizing without sources** - Every claim needs a file:line or PR/issue reference\n5. **Using web search** - No Stack Overflow, no blog posts, no social media\n\n## Verification Checklist\n\nBefore finalizing the research document:\n\n- [ ] React repo is at `.claude/react` with known commit hash\n- [ ] All 6 agents were spawned in parallel\n- [ ] Every code example has a source file reference\n- [ ] Warnings/errors table has source locations\n- [ ] No claims made without source evidence\n- [ ] Discrepancies between Flow/TS types documented\n- [ ] Source links section is complete\n"
  },
  {
    "path": ".claude/skills/review-docs/SKILL.md",
    "content": "---\nname: review-docs\ndescription: Use when reviewing React documentation for structure, components, and style compliance\n---\nCRITICAL: do not load these skills yourself.\n\nRun these tasks in parallel for the given file(s). Each agent checks different aspects—not all apply to every file:\n\n- [ ] Ask docs-reviewer agent to review {files} with docs-writer-learn (only for files in src/content/learn/).\n- [ ] Ask docs-reviewer agent to review {files} with docs-writer-reference (only for files in src/content/reference/).\n- [ ] Ask docs-reviewer agent to review {files} with docs-writer-blog (only for files in src/content/blog/).\n- [ ] Ask docs-reviewer agent to review {files} with docs-voice (all documentation files).\n- [ ] Ask docs-reviewer agent to review {files} with docs-components (all documentation files).\n- [ ] Ask docs-reviewer agent to review {files} with docs-sandpack (files containing Sandpack examples).\n\nIf no file is specified, check git status for modified MDX files in `src/content/`.\n\nThe docs-reviewer will return a checklist of the issues it found. Respond with the full checklist and line numbers from all agents, and prompt the user to create a plan to fix these issues.\n\n\n"
  },
  {
    "path": ".claude/skills/write/SKILL.md",
    "content": "---\nname: write\ndescription: Use when creating new React documentation pages or updating existing ones. Accepts instructions like \"add optimisticKey reference docs\", \"update ViewTransition with Activity\", or \"transition learn docs\".\n---\n\n# Documentation Writer\n\nOrchestrates research, writing, and review for React documentation.\n\n## Invocation\n\n```\n/write add optimisticKey              → creates new reference docs\n/write update ViewTransition Activity → updates ViewTransition docs to cover Activity\n/write transition learn docs          → creates new learn docs for transitions\n/write blog post for React 20         → creates a new blog post\n```\n\n## Workflow\n\n```dot\ndigraph write_flow {\n    rankdir=TB;\n    \"Parse intent\" [shape=box];\n    \"Research (parallel)\" [shape=box];\n    \"Synthesize plan\" [shape=box];\n    \"Write docs\" [shape=box];\n    \"Review docs\" [shape=box];\n    \"Issues found?\" [shape=diamond];\n    \"Done\" [shape=doublecircle];\n\n    \"Parse intent\" -> \"Research (parallel)\";\n    \"Research (parallel)\" -> \"Synthesize plan\";\n    \"Synthesize plan\" -> \"Write docs\";\n    \"Write docs\" -> \"Review docs\";\n    \"Review docs\" -> \"Issues found?\";\n    \"Issues found?\" -> \"Write docs\" [label=\"yes - fix\"];\n    \"Issues found?\" -> \"Done\" [label=\"no\"];\n}\n```\n\n### Step 1: Parse Intent\n\nDetermine from the user's instruction:\n\n| Field | How to determine |\n|-------|------------------|\n| **Action** | \"add\"/\"create\"/\"new\" = new page; \"update\"/\"edit\"/\"with\" = modify existing |\n| **Topic** | The React API or concept (e.g., `optimisticKey`, `ViewTransition`, `transitions`) |\n| **Doc type** | \"reference\" (default for APIs/hooks/components), \"learn\" (for concepts/guides), \"blog\" (for announcements) |\n| **Target file** | For updates: find existing file in `src/content/`. For new: determine path from doc type |\n\nIf the intent is ambiguous, ask the user to clarify before proceeding.\n\n### Step 2: Research (Parallel Agents)\n\nSpawn these agents **in parallel**:\n\n#### Agent 1: React Expert Research\n\nUse a Task agent (subagent_type: `general-purpose`) to invoke `/react-expert <topic>`. This researches the React source code, tests, PRs, issues, and type signatures.\n\n**Prompt:**\n```\nInvoke the /react-expert skill for <TOPIC>. Follow the skill's full workflow:\nsetup the React repo, dispatch all 6 research agents in parallel, synthesize\nresults, and save to .claude/research/<topic>.md. Return the full research document.\n```\n\n#### Agent 2: Existing Docs Audit\n\nUse a Task agent (subagent_type: `Explore`) to find and read existing documentation for the topic.\n\n**Prompt:**\n```\nFind all existing documentation related to <TOPIC> in this repo:\n1. Search src/content/ for files mentioning <TOPIC>\n2. Read any matching files fully\n3. For updates: identify what sections exist and what's missing\n4. For new pages: identify related pages to understand linking/cross-references\n5. Check src/sidebarLearn.json and src/sidebarReference.json for navigation placement\n\nReturn: list of existing files with summaries, navigation structure, and gaps.\n```\n\n#### Agent 3: Use Case Research\n\nUse a Task agent (subagent_type: `general-purpose`) with web search to find common use cases and patterns.\n\n**Prompt:**\n```\nSearch the web for common use cases and patterns for React's <TOPIC>.\nFocus on:\n1. Real-world usage patterns developers actually need\n2. Common mistakes or confusion points\n3. Migration patterns (if replacing an older API)\n4. Framework integration patterns (Next.js, Remix, etc.)\n\nReturn a summary of the top 5-8 use cases with brief code sketches.\nDo NOT search Stack Overflow. Focus on official docs, GitHub discussions,\nand high-quality technical blogs.\n```\n\n### Step 3: Synthesize Writing Plan\n\nAfter all research agents complete, create a writing plan that includes:\n\n1. **Page type** (from docs-writer-reference decision tree or learn/blog type)\n2. **File path** for the new or updated file\n3. **Outline** with section headings matching the appropriate template\n4. **Content notes** for each section, drawn from research:\n   - API signature and parameters (from react-expert types)\n   - Usage examples (from react-expert tests + use case research)\n   - Caveats and pitfalls (from react-expert warnings/errors/issues)\n   - Cross-references to related pages (from docs audit)\n5. **Navigation changes** needed (sidebar JSON updates)\n\nPresent this plan to the user and confirm before proceeding.\n\n### Step 4: Write Documentation\n\nDispatch a Task agent (subagent_type: `general-purpose`) to write the documentation.\n\n**The agent prompt MUST include:**\n\n1. The full writing plan from Step 3\n2. An instruction to invoke the appropriate skill:\n   - `/docs-writer-reference` for reference pages\n   - `/docs-writer-learn` for learn pages\n   - `/docs-writer-blog` for blog posts\n3. An instruction to invoke `/docs-components` for MDX component patterns\n4. An instruction to invoke `/docs-sandpack` if adding interactive code examples\n5. The research document content (key findings, not the full dump)\n\n**Prompt template:**\n```\nYou are writing React documentation. Follow these steps:\n\n1. Invoke /docs-writer-<TYPE> to load the page template and conventions\n2. Invoke /docs-components to load MDX component patterns\n3. Invoke /docs-sandpack if you need interactive code examples\n4. Write the documentation following the plan below\n\nPLAN:\n<writing plan from Step 3>\n\nRESEARCH FINDINGS:\n<key findings from Step 2 agents>\n\nWrite the file to: <target file path>\nAlso update <sidebar JSON> if adding a new page.\n```\n\n### Step 5: Review Documentation\n\nInvoke `/review-docs` on the written files. This dispatches parallel review agents checking:\n- Structure compliance (docs-writer-*)\n- Voice and style (docs-voice)\n- Component usage (docs-components)\n- Sandpack patterns (docs-sandpack)\n\n### Step 6: Fix Issues\n\nIf the review finds issues:\n1. Present the review checklist to the user\n2. Fix the issues identified\n3. Re-run `/review-docs` on the fixed files\n4. Repeat until clean\n\n## Important Rules\n\n- **Always research before writing.** Never write docs from LLM knowledge alone.\n- **Always confirm the plan** with the user before writing.\n- **Always review** with `/review-docs` after writing.\n- **Match existing patterns.** Read neighboring docs to match style and depth.\n- **Update navigation.** New pages need sidebar entries.\n"
  },
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*]\ncharset = utf-8\nend_of_line = lf\ninsert_final_newline = true\nindent_style = space\n"
  },
  {
    "path": ".eslintignore",
    "content": "scripts\nplugins\nnext.config.js\n.claude/\nworker-bundle.dist.js\n"
  },
  {
    "path": ".eslintrc",
    "content": "{\n  \"root\": true,\n  \"extends\": \"next/core-web-vitals\",\n  \"parser\": \"@typescript-eslint/parser\",\n  \"plugins\": [\"@typescript-eslint\", \"eslint-plugin-react-compiler\", \"local-rules\"],\n  \"rules\": {\n    \"no-unused-vars\": \"off\",\n    \"@typescript-eslint/no-unused-vars\": [\"error\", {\"varsIgnorePattern\": \"^_\"}],\n    \"react-hooks/exhaustive-deps\": \"error\",\n    \"react/no-unknown-property\": [\"error\", {\"ignore\": [\"meta\"]}],\n    \"react-compiler/react-compiler\": \"error\",\n    \"local-rules/lint-markdown-code-blocks\": \"error\"\n  },\n  \"env\": {\n    \"node\": true,\n    \"commonjs\": true,\n    \"browser\": true,\n    \"es6\": true\n  },\n  \"overrides\": [\n    {\n      \"files\": [\"src/content/**/*.md\"],\n      \"parser\": \"./eslint-local-rules/parser\",\n      \"parserOptions\": {\n        \"sourceType\": \"module\"\n      },\n      \"rules\": {\n        \"no-unused-vars\": \"off\",\n        \"@typescript-eslint/no-unused-vars\": \"off\",\n        \"react-hooks/exhaustive-deps\": \"off\",\n        \"react/no-unknown-property\": \"off\",\n        \"react-compiler/react-compiler\": \"off\",\n        \"local-rules/lint-markdown-code-blocks\": \"error\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": ".gitattributes",
    "content": "# https://git-scm.com/docs/gitattributes\n\n# Ensure consistent EOL(LF) for all files that Git considers text files.\n* text=auto eol=lf\n"
  },
  {
    "path": ".github/CODEOWNERS",
    "content": "*       @hg-pyun @gnujoow @eomttt @lumirlumir\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "github: [lumirlumir]\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/0-bug.yml",
    "content": "name: \"🐛 Report a bug\"\ndescription: \"Report a problem on the website.\"\ntitle: \"[Bug]: \"\nlabels: [\"bug: unconfirmed\"]\nbody:\n  - type: textarea\n    attributes:\n      label: Summary\n      description: |\n        A clear and concise summary of what the bug is.\n      placeholder: |\n        Example bug report:\n        When I click the \"Submit\" button on \"Feedback\", nothing happens.\n    validations:\n      required: true\n  - type: input\n    attributes:\n      label: Page\n      description: |\n        What page(s) did you encounter this bug on?\n      placeholder: |\n        https://react.dev/\n    validations:\n      required: true\n  - type: textarea\n    attributes:\n      label: Details\n      description: |\n        Please provide any additional details about the bug.\n      placeholder: |\n        Example details:\n        The \"Submit\" button is unresponsive. I've tried refreshing the page and using a different browser, but the issue persists.\n    validations:\n      required: false\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/1-typo.yml",
    "content": "name: \"🤦 Typo or mistake\"\ndescription: \"Report a typo or mistake in the docs.\"\ntitle: \"[Typo]: \"\nlabels: [\"type: typos\"]\nbody:\n  - type: textarea\n    attributes:\n      label: Summary\n      description: |\n        A clear and concise summary of what the mistake is.\n      placeholder: |\n        Example:\n        The code example on the \"useReducer\" page includes an unused variable `nextId`.\n    validations:\n      required: true\n  - type: input\n    attributes:\n      label: Page\n      description: |\n        What page is the typo on?\n      placeholder: |\n        https://react.dev/\n    validations:\n      required: true\n  - type: textarea\n    attributes:\n      label: Details\n      description: |\n        Please provide a explanation for why this is a mistake.\n      placeholder: |\n        Example mistake:\n        In the \"useReducer\" section of the \"API Reference\" page, the code example under \"Writing a reducer function\" includes an unused variable `nextId` that should be removed.\n    validations:\n      required: false\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/3-framework.yml",
    "content": "name: \"📄 Suggest new framework\"\ndescription: \"I am a framework author applying to be included as a recommended framework.\"\ntitle: \"[Framework]: \"\nlabels: [\"type: framework\"]\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        ## Apply to be included as a recommended React framework\n\n        _This form is for framework authors to apply to be included as a recommended [React framework](https://react.dev/learn/creating-a-react-app). If you are not a framework author, please contact the authors before submitting._\n        \n        Our goal when recommending a framework is to start developers with a React project that solves common problems like code splitting, data fetching, routing, and HTML generation without any extra work later. We believe this will allow users to get started quickly with React, and scale their app to production.\n        \n        While we understand that many frameworks may want to be featured, this page is not a place to advertise every possible React framework or all frameworks that you can add React to. There are many great frameworks that offer support for React that are not listed in our guides. The frameworks we recommend have invested significantly in the React ecosystem, and collaborated with the React team to be compatible with our [full-stack React architecture vision](https://react.dev/learn/creating-a-react-app#which-features-make-up-the-react-teams-full-stack-architecture-vision).\n        \n        To be included, frameworks must meet the following criteria:\n        \n        - **Free & open-source**: must be open source and free to use.\n        - **Well maintained**. must be actively maintained, providing bug fixes and improvements.\n        - **Active community**: must have a sufficiently large and active community to support users.\n        - **Clear onboarding**: must have clear install steps to install the React version of the framework.\n        - **Ecosystem compatibility**: must support using the full range of libraries and tools in the React ecosystem.\n        - **Self-hosting option**: must support an option to self-host applications without losing access to features.\n        - **Developer experience**. must allow developers to be productive by supporting features like Fast Refresh.\n        - **User experience**. must provide built-in support for common problems like routing and data-fetching.\n        - **Compatible with our future vision for React**. React evolves over time, and frameworks that do not align with React’s direction risk isolating their users from the main React ecosystem over time. To be included on this page we must feel confident that the framework is setting its users up for success with React over time.\n        \n        Please note, we have reviewed most of the popular frameworks available today, so it is unlikely we have not considered your framework already. But if you think we missed something, please complete the application below.\n  - type: input\n    attributes:\n      label: Name\n      description: |\n        What is the name of your framework?\n    validations:\n      required: true\n  - type: input\n    attributes:\n      label: Homepage\n      description: |\n        What is the URL of your homepage?\n    validations:\n      required: true\n  - type: input\n    attributes:\n      label: Install instructions\n      description: |\n        What is the URL of your getting started guide?\n    validations:\n      required: true\n  - type: dropdown\n    attributes:\n      label: Is your framework open source?\n      description: |\n        We only recommend free and open source frameworks.\n      options:\n        - 'No'\n        - 'Yes'\n    validations:\n      required: true\n  - type: textarea\n    attributes:\n      label: Well maintained\n      description: |\n        Please describe how your framework is actively maintained. Include recent releases, bug fixes, and improvements as examples.\n    validations:\n      required: true\n  - type: textarea\n    attributes:\n      label: Active community\n      description: |\n        Please describe your community. Include the size of your community, and links to community resources.\n    validations:\n      required: true\n  - type: textarea\n    attributes:\n      label: Clear onboarding\n      description: |\n        Please describe how a user can install your framework with React. Include links to any relevant documentation.\n    validations:\n      required: true\n  - type: textarea\n    attributes:\n      label: Ecosystem compatibility\n      description: |\n        Please describe any limitations your framework has with the React ecosystem. Include any libraries or tools that are not compatible with your framework.\n    validations:\n      required: true\n  - type: textarea\n    attributes:\n      label: Self-hosting option\n      description: |\n        Please describe how your framework supports self-hosting. Include any limitations to features when self-hosting. Also include whether you require a server to deploy your framework.\n    validations:\n      required: true\n  - type: textarea\n    attributes:\n      label: Developer Experience\n      description: |\n        Please describe how your framework provides a great developer experience. Include any limitations to React features like React DevTools, Chrome DevTools, and Fast Refresh.\n    validations:\n      required: true\n  - type: textarea\n    attributes:\n      label: User Experience\n      description: |\n        Please describe how your framework helps developers create high quality user experiences by solving common use-cases. Include specifics for how your framework offers built-in support for code-splitting, routing, HTML generation, and data-fetching in a way that avoids client/server waterfalls by default. Include details on how you offer features such as SSG and SSR.\n    validations:\n      required: true\n  - type: textarea\n    attributes:\n      label: Compatible with our future vision for React\n      description: |\n        Please describe how your framework aligns with our future vision for React. Include how your framework will evolve with React over time, and your plans to support future React features like React Server Components.\n    validations:\n      required: true\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "contact_links:\n  - name: 📃 Bugs in React\n    url: https://github.com/facebook/react/issues/new/choose\n    about: This issue tracker is not for bugs in React. Please file React issues here.\n  - name: 🤔 Questions and Help\n    url: https://reactjs.org/community/support.html\n    about: This issue tracker is not for support questions. Please refer to the React community's help and discussion forums.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/need-translation.md",
    "content": "---\nname: 번역 필요\nabout: 번역이 필요한 문서에 대해 이 템플릿을 사용해주세요\ntitle: '[번역 필요]: '\nlabels: 'need translation'\nassignees: ''\n---\n\n## Summary\n\n<!-- 번역이 필요한 부분의 요약 -->\n\n## Page\n\n<!-- 번역이 필요한 문서의 페이지 -->\n<!-- 예시: https://ko.react.dev/reference/react/useState#parameters -->\n\n## Details\n\n<!-- 번역이 필요한 부분의 상세 내용 -->\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/term.md",
    "content": "---\nname: Term\nabout: 문서에서 사용되는 용어 번역에 논의가 필요한 경우 이 템플릿을 사용해주세요\ntitle: ''\nlabels: discussion, term\nassignees: ''\n---\n\n## 논의하고자 하는 용어\n\n## 해당 용어가 등장하는 원문의 문장\n\n---\n\n## 논의가 완료된 후\n\n1. 아래 코드를 업데이트 해주세요.\n    - [ ] [`/textlint/data/rules/translateGlossary.js`](https://github.com/reactjs/ko.react.dev/blob/main/textlint/data/rules/translateGlossary.js)\n\n2. 용어 사전에 업데이트된 내역이 정상 반영되었나 확인해주세요. (해당 내역은 husky의 pre-commit 훅을 통해 자동 업데이트 됩니다.)\n    - [ ] [`/wiki/translate-glossary.md`](https://github.com/reactjs/ko.react.dev/blob/main/wiki/translate-glossary.md)\n"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "content": "<!--\n  PR을 보내주셔서 감사합니다! 여러분과 같은 기여자들이 React를 더욱 멋지게 만듭니다!\n\n  기존 이슈와 관련된 PR이라면, 아래에 이슈 번호를 추가해주세요.\n-->\n\n# <!-- 제목을 작성해주세요. -->\n\n<!--\n  어떤 종류의 PR인지 상세 내용을 작성해주세요.\n-->\n\n## 필수 확인 사항\n\n- [ ] [기여자 행동 강령 규약<sup>Code of Conduct</sup>](https://github.com/reactjs/ko.react.dev/blob/main/CODE_OF_CONDUCT.md)\n- [ ] [기여 가이드라인<sup>Contributing</sup>](https://github.com/reactjs/ko.react.dev/blob/main/CONTRIBUTING.md)\n- [ ] [공통 스타일 가이드<sup>Universal Style Guide</sup>](https://github.com/reactjs/ko.react.dev/blob/main/wiki/universal-style-guide.md)\n- [ ] [번역을 위한 모범 사례<sup>Best Practices for Translation</sup>](https://github.com/reactjs/ko.react.dev/blob/main/wiki/best-practices-for-translation.md)\n- [ ] [번역 용어 정리<sup>Translate Glossary</sup>](https://github.com/reactjs/ko.react.dev/blob/main/wiki/translate-glossary.md)\n- [ ] [`textlint` 가이드<sup>Textlint Guide</sup>](https://github.com/reactjs/ko.react.dev/blob/main/wiki/textlint-guide.md)\n- [ ] [맞춤법 검사<sup>Spelling Check</sup>](https://nara-speller.co.kr/speller/)\n\n## 선택 확인 사항\n\n- [ ] 번역 초안 작성<sup>Draft Translation</sup>\n- [ ] 리뷰 반영<sup>Resolve Reviews</sup>\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n  - package-ecosystem: \"npm\"\n    directory: \"/\"\n    schedule:\n      interval: \"weekly\"\n    # Disable Dependabot. Doing it here so it propagates to translation forks.\n    open-pull-requests-limit: 0\n"
  },
  {
    "path": ".github/workflows/analyze.yml",
    "content": "name: Analyze Bundle\n\non:\n  pull_request:\n  push:\n    branches:\n      - main # change this if your default branch is named differently\n  workflow_dispatch:\n\npermissions: {}\n\njobs:\n  analyze:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Set up node\n        uses: actions/setup-node@v4\n        with:\n          node-version: '20.x'\n          cache: yarn\n          cache-dependency-path: yarn.lock\n\n      - name: Restore cached node_modules\n        uses: actions/cache@v4\n        with:\n          path: '**/node_modules'\n          key: node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}\n\n      - name: Install deps\n        run: yarn install --frozen-lockfile\n\n      - name: Restore next build\n        uses: actions/cache@v4\n        id: restore-build-cache\n        env:\n          cache-name: cache-next-build\n        with:\n          path: .next/cache\n          # change this if you prefer a more strict cache\n          key: ${{ runner.os }}-build-${{ env.cache-name }}\n\n      - name: Build next.js app\n        # change this if your site requires a custom build command\n        run: ./node_modules/.bin/next build\n\n      # Here's the first place where next-bundle-analysis' own script is used\n      # This step pulls the raw bundle stats for the current bundle\n      - name: Analyze bundle\n        run: npx -p nextjs-bundle-analysis@0.5.0 report\n\n      - name: Upload bundle\n        uses: actions/upload-artifact@v4\n        with:\n          path: .next/analyze/__bundle_analysis.json\n          name: bundle_analysis.json\n\n      - name: Download base branch bundle stats\n        uses: dawidd6/action-download-artifact@268677152d06ba59fcec7a7f0b5d961b6ccd7e1e\n        if: success() && github.event.number\n        with:\n          workflow: analyze.yml\n          branch: ${{ github.event.pull_request.base.ref }}\n          name: bundle_analysis.json\n          path: .next/analyze/base/bundle\n\n      # And here's the second place - this runs after we have both the current and\n      # base branch bundle stats, and will compare them to determine what changed.\n      # There are two configurable arguments that come from package.json:\n      #\n      # - budget: optional, set a budget (bytes) against which size changes are measured\n      #           it's set to 350kb here by default, as informed by the following piece:\n      #           https://infrequently.org/2021/03/the-performance-inequality-gap/\n      #\n      # - red-status-percentage: sets the percent size increase where you get a red\n      #                          status indicator, defaults to 20%\n      #\n      # Either of these arguments can be changed or removed by editing the `nextBundleAnalysis`\n      # entry in your package.json file.\n      - name: Compare with base branch bundle\n        if: success() && github.event.number\n        run: ls -laR .next/analyze/base && npx -p nextjs-bundle-analysis compare\n\n      - name: Upload analysis comment\n        uses: actions/upload-artifact@v4\n        with:\n          name: analysis_comment.txt\n          path: .next/analyze/__bundle_analysis_comment.txt\n\n      - name: Save PR number\n        run: echo ${{ github.event.number }} > ./pr_number\n\n      - name: Upload PR number\n        uses: actions/upload-artifact@v4\n        with:\n          name: pr_number\n          path: ./pr_number\n\n      # The actual commenting happens in the other action, matching the guidance in\n      # https://securitylab.github.com/research/github-actions-preventing-pwn-requests/\n"
  },
  {
    "path": ".github/workflows/analyze_comment.yml",
    "content": "name: Analyze Bundle (Comment)\n\non:\n  workflow_run:\n    workflows: ['Analyze Bundle']\n    types:\n      - completed\n\npermissions:\n  contents: read\n  issues: write\n  pull-requests: write\n  \njobs:\n  comment:\n    runs-on: ubuntu-latest\n    if: >\n      ${{ github.event.workflow_run.event == 'pull_request' &&\n      github.event.workflow_run.conclusion == 'success' }}\n    steps:\n      - name: Download base branch bundle stats\n        uses: dawidd6/action-download-artifact@268677152d06ba59fcec7a7f0b5d961b6ccd7e1e\n        with:\n          workflow: analyze.yml\n          run_id: ${{ github.event.workflow_run.id }}\n          name: analysis_comment.txt\n          path: analysis_comment.txt\n\n      - name: Download PR number\n        uses: dawidd6/action-download-artifact@268677152d06ba59fcec7a7f0b5d961b6ccd7e1e\n        with:\n          workflow: analyze.yml\n          run_id: ${{ github.event.workflow_run.id }}\n          name: pr_number\n          path: pr_number\n\n      - name: Get comment body\n        id: get-comment-body\n        if: success()\n        run: |\n          echo 'body<<EOF' >> $GITHUB_OUTPUT\n          echo '' >>  $GITHUB_OUTPUT\n          echo '## Size changes' >>  $GITHUB_OUTPUT\n          echo '' >>  $GITHUB_OUTPUT\n          echo '<details>' >>  $GITHUB_OUTPUT\n          echo '' >>  $GITHUB_OUTPUT\n          cat analysis_comment.txt/__bundle_analysis_comment.txt >> $GITHUB_OUTPUT\n          echo '' >>  $GITHUB_OUTPUT\n          echo '</details>' >>  $GITHUB_OUTPUT\n          echo '' >>  $GITHUB_OUTPUT\n          echo 'EOF' >> $GITHUB_OUTPUT\n          pr_number=$(cat pr_number/pr_number)\n          echo \"pr-number=$pr_number\" >> $GITHUB_OUTPUT\n\n      - name: Comment\n        uses: marocchino/sticky-pull-request-comment@52423e01640425a022ef5fd42c6fb5f633a02728\n        with:\n          header: next-bundle-analysis\n          number: ${{ steps.get-comment-body.outputs.pr-number }}\n          message: ${{ steps.get-comment-body.outputs.body }}\n"
  },
  {
    "path": ".github/workflows/site_lint.yml",
    "content": "name: Site Lint / Heading ID check\n\non:\n  push:\n    branches:\n      - main # change this if your default branch is named differently\n  pull_request:\n    types: [opened, synchronize, reopened]\n\npermissions: {}\n\njobs:\n  lint:\n    runs-on: ubuntu-latest\n\n    name: Lint on node 20.x and ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v4\n      - name: Use Node.js 20.x\n        uses: actions/setup-node@v4\n        with:\n          node-version: 20.x\n          cache: yarn\n          cache-dependency-path: yarn.lock\n\n      - name: Restore cached node_modules\n        uses: actions/cache@v4\n        with:\n          path: '**/node_modules'\n          key: node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}\n\n      - name: Install deps\n        run: yarn install --frozen-lockfile\n\n      - name: Lint codebase\n        run: yarn ci-check\n"
  },
  {
    "path": ".github/workflows/textlint_lint.yml",
    "content": "name: Textlint Lint\n\non:\n  push:\n    branches:\n      - main\n    paths:\n      - 'src/**/*.md'\n      - 'textlint/**/*.js'\n      - '.github/workflows/textlint_lint.yml'\n      - 'package.json'\n\n  pull_request:\n    types:\n      - opened\n      - synchronize\n      - reopened\n    paths:\n      - 'src/**/*.md'\n      - 'textlint/**/*.js'\n      - '.github/workflows/textlint_lint.yml'\n      - 'package.json'\n\njobs:\n  Lint:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Set up checkout\n        uses: actions/checkout@v4\n\n      - name: Set up node\n        uses: actions/setup-node@v4\n        with:\n          node-version: 20.x\n          cache: yarn\n\n      - name: Set up cache\n        uses: actions/cache@v4\n        with:\n          path: ~/.yarn-cache\n          key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}\n\n      - name: Install dependencies\n        run: yarn install --frozen-lockfile\n        # The `--frozen-lockfile` flag in Yarn ensures that dependencies are installed without modifying the `yarn.lock` file. It is useful for maintaining consistency in CI/CD environments by preventing unexpected changes to the lock file and ensuring that the same versions of dependencies are installed.\n\n      - name: Lint\n        run: yarn textlint-lint\n"
  },
  {
    "path": ".github/workflows/textlint_test.yml",
    "content": "name: Textlint Test\n\non:\n  push:\n    branches:\n      - main\n    paths:\n      - 'textlint/**/*.js'\n      - '.github/workflows/textlint_test.yml'\n\n  pull_request:\n    types:\n      - opened\n      - synchronize\n      - reopened\n    paths:\n      - 'textlint/**/*.js'\n      - '.github/workflows/textlint_test.yml'\n\njobs:\n  Test:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Set up checkout\n        uses: actions/checkout@v4\n\n      - name: Set up node\n        uses: actions/setup-node@v4\n        with:\n          node-version: 20.x\n          cache: yarn\n\n      - name: Set up cache\n        uses: actions/cache@v4\n        with:\n          path: ~/.yarn-cache\n          key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}\n\n      - name: Install dependencies\n        run: yarn install --frozen-lockfile\n        # The `--frozen-lockfile` flag in Yarn ensures that dependencies are installed without modifying the `yarn.lock` file. It is useful for maintaining consistency in CI/CD environments by preventing unexpected changes to the lock file and ensuring that the same versions of dependencies are installed.\n\n      - name: Test\n        run: yarn textlint-test\n"
  },
  {
    "path": ".gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\nnode_modules\n/.pnp\n.pnp.js\n\n# testing\n/coverage\n\n# next.js\n/.next/\n/out/\n\n# production\n/build\n\n# misc\n.DS_Store\n*.pem\ntsconfig.tsbuildinfo\n\n# debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# local env files\n.env.local\n.env.development.local\n.env.test.local\n.env.production.local\n\n# vercel\n.vercel\n\n# external fonts\npublic/fonts/**/Optimistic_*.woff2\n\n# rss\npublic/rss.xml\n\n# code editor\n.cursor\n.idea\n*.code-workspace\n\n# claude local settings\n.claude/*.local.*\n.claude/react/\n\n# worktrees\n.worktrees/\n"
  },
  {
    "path": ".husky/common.sh",
    "content": "#!/bin/sh\ncommand_exists () {\n  command -v \"$1\" >/dev/null 2>&1\n}\n\n# Windows 10, Git Bash and Yarn workaround\nif command_exists winpty && test -t 1; then\n  exec < /dev/tty\nfi\n"
  },
  {
    "path": ".husky/pre-commit",
    "content": "#!/bin/sh\n. \"$(dirname \"$0\")/_/husky.sh\"\n. \"$(dirname \"$0\")/common.sh\"\n\nyarn lint-staged\n"
  },
  {
    "path": ".prettierignore",
    "content": "src/content/**/*.md\nsrc/components/MDX/Sandpack/sandpack-rsc/sandbox-code/src/worker-bundle.dist.js\n"
  },
  {
    "path": ".prettierrc",
    "content": "{\n  \"bracketSpacing\": false,\n  \"singleQuote\": true,\n  \"bracketSameLine\": true,\n  \"trailingComma\": \"es5\",\n  \"printWidth\": 80,\n  \"overrides\": [\n    {\n      \"files\": \"*.css\",\n      \"options\": {\n        \"parser\": \"css\"\n      }\n    },\n    {\n      \"files\": \"*.md\",\n      \"options\": {\n        \"parser\": \"mdx\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": ".textlintrc.js",
    "content": "module.exports = {\n  rules: {\n    'allowed-uris': {\n      disallowed: {\n        /**\n         * Disallow URIs starting with the following strings:\n         * - https://ko.react.dev\n         * - http://ko.react.dev\n         *\n         * For example,\n         * `https://ko.react.dev/reference/rules` can be replaced with `/reference/rules`.\n         */\n        links: [/https?:\\/\\/ko\\.react\\.dev/g],\n      },\n    },\n  },\n  filters: {\n    comments: true,\n  },\n};\n"
  },
  {
    "path": ".vscode/extensions.json",
    "content": "{\n  \"recommendations\": [\n    \"dbaeumer.vscode-eslint\",\n    \"esbenp.prettier-vscode\",\n    \"editorconfig.editorconfig\"\n  ]\n}\n"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n  \"editor.formatOnSave\": true,\n  \"editor.formatOnPaste\": true,\n  \"editor.defaultFormatter\": \"esbenp.prettier-vscode\",\n  \"editor.codeActionsOnSave\": {\n    \"source.fixAll.eslint\": \"always\"\n  }\n}\n"
  },
  {
    "path": "CLAUDE.md",
    "content": "# CLAUDE.md\n\nThis file provides guidance to Claude Code when working with this repository.\n\n## Project Overview\n\nThis is the React documentation website (react.dev), built with Next.js 15.1.11 and React 19. Documentation is written in MDX format.\n\n## Development Commands\n\n```bash\nyarn build         # Production build\nyarn lint          # Run ESLint\nyarn lint:fix      # Auto-fix lint issues\nyarn tsc           # TypeScript type checking\nyarn check-all     # Run prettier, lint:fix, tsc, and rss together\n```\n\n## Project Structure\n\n```\nsrc/\n├── content/           # Documentation content (MDX files)\n│   ├── learn/         # Tutorial/learning content\n│   ├── reference/     # API reference docs\n│   ├── blog/          # Blog posts\n│   └── community/     # Community pages\n├── components/        # React components\n├── pages/             # Next.js pages\n├── hooks/             # Custom React hooks\n├── utils/             # Utility functions\n└── styles/            # CSS/Tailwind styles\n```\n\n## Code Conventions\n\n### TypeScript/React\n- Functional components only\n- Tailwind CSS for styling\n\n### Documentation Style\n\nWhen editing files in `src/content/`, the appropriate skill will be auto-suggested:\n- `src/content/learn/` - Learn page structure and tone\n- `src/content/reference/` - Reference page structure and tone\n\nFor MDX components (DeepDive, Pitfall, Note, etc.), invoke `/docs-components`.\nFor Sandpack code examples, invoke `/docs-sandpack`.\n\nSee `.claude/docs/react-docs-patterns.md` for comprehensive style guidelines.\n\nPrettier is used for formatting (config in `.prettierrc`).\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# 기여자 행동 강령 규약\n\n## 서약\n\n우리는 구성원, 기여자 및 리더로서 커뮤니티에 참여하여\n연령, 신체 크기, 눈에 보이거나 보이지 않는 장애, 민족성, 성별, 성 정체성과 표현,\n경력, 학력, 사회 경제적 지위, 국적, 외모, 인종, 카스트 제도, 피부색, 종교\n또는 성적 정체성과 성적 성향에 관계없이 모든 사람을 차별하지 않을 것을 서약한다.\n\n우리는 개방적이고 친근하며 다양하고 포용적이며 건강한 커뮤니티에 기여하는\n방식으로 행동하고 상호작용할 것을 서약한다.\n\n## 표준\n\n커뮤니티의 긍정적인 환경을 위해 기여자가 해야 할 행동은 다음과 같다.\n\n- 다른 사람들에 대한 친절과 공감 표현\n- 서로 다른 의견 및 관점, 경험에 대한 존중\n- 건설적인 피드백을 제공 및 열린 마음으로 수락\n- 책임을 받아들이고 실수로 인해 영향을 받은 사람들에게 사과하며 경험을 통해 배움\n- 개인뿐만 아닌 전체 커뮤니티를 위한 최선의 방법에 집중\n\n하지말아야 할 행동은 다음과 같다.\n\n- 성적인 언어와 이미지 사용, 성적 관심이나 어떤 종류의 접근\n- 소모적인 논쟁, 모욕적 또는 비하하는 댓글과 개인적 또는 정치적인 공격\n- 공개적이거나 개인적인 괴롭힘\n- 동의없는 집주소 또는 이메일 주소 등의 개인 정보의 공개\n- 부적절한 것으로 간주될 수 있는 다른 행위\n\n## 집행 책임\n\n커뮤니티 리더는 허용되는 행동의 기준을 명확히 하고 집행할 책임이 있으며\n부적절하다고 여겨지는 모든 행동, 위협, 공격 또는 피해에 대해 적절하고\n공정한 행동을 취한다.\n\n프로젝트 유지자는 이 행동 강령을 따르지 않은 댓글, 커밋, 코드, 위키 편집,\n이슈와 그 외 다른 기여를 삭제, 수정 또는 거부할 권리와 책임이 있다. 또한,\n부적당하거나 험악하거나 공격적이거나 해롭다고 생각하는 다른 행동을 한 기여자를\n일시적 또는 영구적으로 퇴장시킬 수 있다.\n\n커뮤니티 리더는 이 행동 강령을 따르지 않는 댓글, 커밋, 코드, 위키 편집,\n이슈와 그 외 다른 기여를 삭제, 수정 또는 거부할 권리와 책임이 있으며,\n적절한 경우 중재적 의사결정에 대한 이유를 전달할 것이다.\n\n## 범위\n\n이 행동 강령은 개인이 공개 영역에서 커뮤니티를 공식적으로 대표할 때를\n포함하여 모든 커뮤니티 영역에 적용된다.\n커뮤니티 대표의 예로 공식 이메일 주소 사용, 공식 소셜 미디어 계정을 통한 게시,\n온/오프라인 이벤트에서 임명된 대표자의 활동이 있다.\n\n## 집행\n\n모욕적이거나 괴롭힘 또는 그 외 하지말아야 할 행동을 발견하면\n<opensource-conduct@fb.com>을 통해 집행 책임이 있는 커뮤니티 리더에게 보고한다.\n모든 불만사항은 신속하고 공정하게 검토되고 조사될 것이다.\n\n커뮤니티 리더는 사건의 보고자의 사생활과 안전을 존중할 의무가 있다.\n\n## 집행 지침\n\n커뮤니티 리더는 행동 강령 위반으로 간주되는 행동에 대한 결과를 결정할 때,\n다음의 커뮤니티 영향 지침을 준수한다:\n\n### 1. 정정\n\n**커뮤니티 영향**: 커뮤니티 내 부적절한 언어 사용이나\n비전문적인 행동 또는 불쾌함을 주는 행동.\n\n**결과**: 커뮤니티 리더가 별도로 위반에 대한 명확성과 부적절함에 대한\n이유를 설명하고 서면 경고.\n공개 사과를 요청할 수 있다.\n\n### 2. 경고\n\n**커뮤니티 영향**: 단일 사고 또는 연속된 행동 위반.\n\n**결과**: 지속적인 행동에 대한 결과에 대해 경고.\n특정 기간동안 행동 강령을 시행하는 사람들과의 원치 않는 상호작용을 포함한\n관련된 사람들과의 상호작용 금지. 소셜 미디어와 같은 외부 채널뿐만 아닌\n커뮤니티 공간에서의 상호작용도 금지된다.\n이 조항을 위반하면 일시적 혹은 영구적으로 제재로 이어질 수 있다.\n\n### 3. 일시적인 제재\n\n**커뮤니티 영향**: 지속적으로 부적절한 행동을 포함한\n심각한 커뮤니티 기준 위반.\n\n**결과**: 특정 기간동안 커뮤니티와의 어떠한 종류의 상호작용이나\n공개적 소통이 일시적 제재.\n이 기간동안 행동 강령을 시행하는 사람들과의 원치 않는 상호작용을 포함한\n관련된 사람들과의 상호작용 금지. 소셜 미디어와 같은 외부 채널뿐만 아닌\n커뮤니티 공간에서의 상호작용도 금지된다.\n이 조항을 위반하면 일시적 혹은 영구적으로 제재로 이어질 수 있다.\n\n### 4. 영구 제재\n\n**커뮤니티 영향**: 지속적인 부적절한 행동, 개인적인 괴롭힘 또는\n개인의 계급에 대한 공격이나 폄하를 포함한 커뮤니티 표준 위반 패턴을 보임.\n\n**결과**: 커뮤니티와의 모든 종류의 공개적 교류를 영구적으로 제재.\n\n## 참고\n\n이 행동 강령은 [기여자 규약][homepage] 의 2.1 버전을 변형하였습니다. 그 내용은\n[https://www.contributor-covenant.org/ko/version/2/1/code-of-conduct.html][v2.1]\n에서 확인할 수 있습니다.\n\n커뮤니티 영향 지침은 [Mozilla's code of conduct enforcement ladder][Mozilla CoC]\n에서 영감을 얻었습니다.\n\n이 행동 강령에 관련한 일반적인 질문에 대한 대답은\n[https://www.contributor-covenant.org/faq][FAQ]를 참고할 수 있습니다.\n번역본은 [https://www.contributor-covenant.org/translations][translations]에서\n볼 수 있습니다.\n\n[homepage]: https://www.contributor-covenant.org\n[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html\n[Mozilla CoC]: https://github.com/mozilla/diversity\n[FAQ]: https://www.contributor-covenant.org/faq\n[translations]: https://www.contributor-covenant.org/translations\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# 기여하기\n\nReact 문서 기여에 관심을 가져주셔서 감사합니다!\n\n## 행동 강령\n\n페이스북<sup>Facebook</sup>은 프로젝트 참가자가 준수해야 하는 행동 강령을 채택했습니다. [전문을 읽어보세요](https://code.facebook.com/codeofconduct). 어떤 행동이 허용되고 허용되지 않는지 확인할 수 있습니다.\n\n## 기술 문서 작성 팁\n\n기술 문서를 작성할 때 염두에 두어야 할 사항에 대한 [좋은 요약](https://medium.com/@kvosswinkel/coding-like-a-journalist-ee52360a16bc)입니다.\n\n## 글에 대한 가이드라인\n\n**섹션마다 의도적으로 다른 스타일을 사용합니다.**\n\n이 문서는 다양한 학습 스타일과 사용 사례를 고려하여 분할되어 있습니다. 본문을 수정할 때는 주변 글의 톤<sup>Tone</sup>과 스타일<sup>Style</sup>에 맞게 작성하도록 주의하세요. 새로운 글을 작성할 때는 같은 섹션에 있는 다른 글들과 톤을 맞추도록 하세요. 각 섹션의 의도와 동기는 아래에서 확인할 수 있습니다.\n\n**[React 학습하기](https://ko.react.dev/learn)** 섹션은 기초 개념을 단계별로 소개하기 위해 만들어졌습니다. 여기서 제공되는 글들은 이전에 설명된 지식을 바탕으로 하므로, 글 간 앞뒤 개념이 중복되거나 꼬이지 않도록 주의하세요. 독자는 첫 번째 글부터 마지막 글까지 순서대로 읽으며 개념을 익힐 수 있어야 하며, 추가 설명을 위해 미리 앞선 개념들을 살펴보지 않도록 해야 합니다. 이런 이유로 상태<sup>State</sup>는 이벤트<sup>Event</sup>보다 먼저 설명되고, 'React로 사고하기' 파트에서 `ref`를 사용하지 않는 등 특정 순서가 정해져 있습니다. 동시에 'React 학습하기'는 React 개념에 대한 참고 자료 역할을 하므로, 개념들에 대한 정의와 상호 관계를 엄격하게 다루어야 합니다.\n\n**[API 참고서](https://ko.react.dev/reference/react)** 섹션은 개념이 아닌 API별로 정리되어 있으며, 가능한 한 모든 경우를 포함하는 것을 목표로 합니다. 'React 학습하기'에서 간단히 다뤘거나 생략한 예외 사항<sup>Edge Cases</sup> 혹은 권장 사항<sup>Recommendations</sup>은 해당 API의 레퍼런스 문서에 추가로 언급해야 합니다.\n\n**스스로 작성한 지침<sup>Instructions</sup>을 실천해 보세요.**\n\n예를 들어, 단계별 가이드를 작성한다면, 직접 그 지침을 따라가 보며 누락된 내용이나 순서가 맞지 않는 부분을 찾아보세요. 실제로 지침을 순서대로 진행하다보면, 작성자가 설명하지 않은 배경지식이 있거나, 단계가 뒤섞여 있는 등의 문제를 발견할 수 있습니다. 가능하다면 다른 사람에게 지침을 따라보게 하고, 그들이 어려움을 겪는 부분을 관찰하는 것도 좋은 방법입니다. 사소해 보이지만 예상치 못한 곳에서 문제가 생길 수 있습니다.\n\n## 코드 예시에 대한 가이드라인\n\n### 구문<sup>Syntax</sup>\n\n#### 가능하면 `createElement` 대신 JSX를 사용하세요\n\n단, `createElement` 자체를 설명해야 하는 경우는 예외입니다.\n\n#### 가능하면 `const`, 필요한 경우에는 `let`을 사용하고, `var`는 사용하지 마세요\n\nES5만 다루는 경우라면 이 규칙은 무시하세요.\n\n#### ES5의 기능만으로 간단하게 작성할 수 있는 경우, ES6의 기능을 무조건적으로 사용하지 마세요\n\nES6가 아직 낯선 사람도 많습니다. 이미 여러 곳에서 `const` / `let`, 클래스, 화살표 함수 등을 사용하고 있지만, 그에 상응하는 ES5 코드가 간단하고 가독성이 좋다면 ES5를 사용하는 것도 고려하세요.\n\n특히 최상위 함수에서는 `const myFunction = () => ...`과 같은 화살표 함수 대신에 이름 있는 `function` 선언을 선호합니다. 하지만 컴포넌트 내 `this` 컨텍스트를 유지해야 하는 경우에는 화살표 함수를 사용하세요. 새로운 문법을 사용할 때는 장단점을 모두 따져보고 결정하세요.\n\n#### 아직 표준화되지 않은 기능은 사용하지 마세요\n\n예를 들어, 다음 코드처럼 작성하지 마세요.\n\n```js\nclass MyComponent extends React.Component {\n  state = {value: ''};\n  handleChange = (e) => {\n    this.setState({value: e.target.value});\n  };\n}\n```\n\n대신, 다음처럼 작성하세요.\n\n```js\nclass MyComponent extends React.Component {\n  constructor(props) {\n    super(props);\n    this.handleChange = this.handleChange.bind(this);\n    this.state = {value: ''};\n  }\n  handleChange(e) {\n    this.setState({value: e.target.value});\n  }\n}\n```\n\n실험적인 제안<sup>Experimental Proposal</sup>에 대해 설명하는 경우라면 예외로 하되, 코드와 주변 글에서 실험적임<sup>Experimental</sup>을 명시하세요.\n\n### 스타일\n\n- 세미콜론을 사용하세요.\n- 함수 이름과 괄호 사이에는 공백을 넣지 마세요. (`method () {}`가 아닌, `method() {}` 형태.)\n- 고민될 때는 [Prettier](https://prettier.io/playground/)의 기본 스타일을 따르세요.\n- Hooks, Effects, Transitions 같은 React 관련 개념은 항상 대문자로 시작하세요.\n\n### 하이라이팅\n\n마크다운<sup>Markdown</sup>의 코드 블록에서는 `js`를 사용하세요:\n\n````\n```js\n// 코드\n```\n````\n\n간혹 숫자와 함께 사용되는 블록이 있습니다.\n이는 특정 줄을 강조하기 위한 용도입니다.\n\n한 줄을 강조하는 예시.\n\n````\n```js {2}\nfunction hello() {\n  // 이 줄이 강조됩니다\n}\n```\n````\n\n일정 범위를 강조하는 예시.\n\n````\n```js {2-4}\nfunction hello() {\n  // 여기부터\n  // 시작해서\n  // 여기까지 강조됩니다\n}\n```\n````\n\n여러 범위를 강조하는 예시.\n\n````\n```js {2-4,6}\nfunction hello() {\n  // 여기부터\n  // 시작해서\n  // 여기까지 강조됩니다\n  console.log('hello');\n  // 이 줄도 강조됩니다\n  console.log('there');\n}\n```\n````\n\n코드를 이동하거나 순서를 바꿨다면, 강조하는 줄도 같이 수정해야 한다는 점을 잊지 마세요.\n\n강조 기능은 독자가 놓치기 쉬운 구체적인 부분에 주의를 환기해주므로 적극적으로 사용하길 권장합니다.\n"
  },
  {
    "path": "LICENSE-DOCS.md",
    "content": "Attribution 4.0 International\n\n=======================================================================\n\nCreative Commons Corporation (\"Creative Commons\") is not a law firm and\ndoes not provide legal services or legal advice. Distribution of\nCreative Commons public licenses does not create a lawyer-client or\nother relationship. Creative Commons makes its licenses and related\ninformation available on an \"as-is\" basis. Creative Commons gives no\nwarranties regarding its licenses, any material licensed under their\nterms and conditions, or any related information. Creative Commons\ndisclaims all liability for damages resulting from their use to the\nfullest extent possible.\n\nUsing Creative Commons Public Licenses\n\nCreative Commons public licenses provide a standard set of terms and\nconditions that creators and other rights holders may use to share\noriginal works of authorship and other material subject to copyright\nand certain other rights specified in the public license below. The\nfollowing considerations are for informational purposes only, are not\nexhaustive, and do not form part of our licenses.\n\n     Considerations for licensors: Our public licenses are\n     intended for use by those authorized to give the public\n     permission to use material in ways otherwise restricted by\n     copyright and certain other rights. Our licenses are\n     irrevocable. Licensors should read and understand the terms\n     and conditions of the license they choose before applying it.\n     Licensors should also secure all rights necessary before\n     applying our licenses so that the public can reuse the\n     material as expected. Licensors should clearly mark any\n     material not subject to the license. This includes other CC-\n     licensed material, or material used under an exception or\n     limitation to copyright. More considerations for licensors:\n  wiki.creativecommons.org/Considerations_for_licensors\n\n     Considerations for the public: By using one of our public\n     licenses, a licensor grants the public permission to use the\n     licensed material under specified terms and conditions. If\n     the licensor's permission is not necessary for any reason--for\n     example, because of any applicable exception or limitation to\n     copyright--then that use is not regulated by the license. Our\n     licenses grant only permissions under copyright and certain\n     other rights that a licensor has authority to grant. Use of\n     the licensed material may still be restricted for other\n     reasons, including because others have copyright or other\n     rights in the material. A licensor may make special requests,\n     such as asking that all changes be marked or described.\n     Although not required by our licenses, you are encouraged to\n     respect those requests where reasonable. More_considerations\n     for the public:\n  wiki.creativecommons.org/Considerations_for_licensees\n\n=======================================================================\n\nCreative Commons Attribution 4.0 International Public License\n\nBy exercising the Licensed Rights (defined below), You accept and agree\nto be bound by the terms and conditions of this Creative Commons\nAttribution 4.0 International Public License (\"Public License\"). To the\nextent this Public License may be interpreted as a contract, You are\ngranted the Licensed Rights in consideration of Your acceptance of\nthese terms and conditions, and the Licensor grants You such rights in\nconsideration of benefits the Licensor receives from making the\nLicensed Material available under these terms and conditions.\n\n\nSection 1 -- Definitions.\n\n  a. Adapted Material means material subject to Copyright and Similar\n     Rights that is derived from or based upon the Licensed Material\n     and in which the Licensed Material is translated, altered,\n     arranged, transformed, or otherwise modified in a manner requiring\n     permission under the Copyright and Similar Rights held by the\n     Licensor. For purposes of this Public License, where the Licensed\n     Material is a musical work, performance, or sound recording,\n     Adapted Material is always produced where the Licensed Material is\n     synched in timed relation with a moving image.\n\n  b. Adapter's License means the license You apply to Your Copyright\n     and Similar Rights in Your contributions to Adapted Material in\n     accordance with the terms and conditions of this Public License.\n\n  c. Copyright and Similar Rights means copyright and/or similar rights\n     closely related to copyright including, without limitation,\n     performance, broadcast, sound recording, and Sui Generis Database\n     Rights, without regard to how the rights are labeled or\n     categorized. For purposes of this Public License, the rights\n     specified in Section 2(b)(1)-(2) are not Copyright and Similar\n     Rights.\n\n  d. Effective Technological Measures means those measures that, in the\n     absence of proper authority, may not be circumvented under laws\n     fulfilling obligations under Article 11 of the WIPO Copyright\n     Treaty adopted on December 20, 1996, and/or similar international\n     agreements.\n\n  e. Exceptions and Limitations means fair use, fair dealing, and/or\n     any other exception or limitation to Copyright and Similar Rights\n     that applies to Your use of the Licensed Material.\n\n  f. Licensed Material means the artistic or literary work, database,\n     or other material to which the Licensor applied this Public\n     License.\n\n  g. Licensed Rights means the rights granted to You subject to the\n     terms and conditions of this Public License, which are limited to\n     all Copyright and Similar Rights that apply to Your use of the\n     Licensed Material and that the Licensor has authority to license.\n\n  h. Licensor means the individual(s) or entity(ies) granting rights\n     under this Public License.\n\n  i. Share means to provide material to the public by any means or\n     process that requires permission under the Licensed Rights, such\n     as reproduction, public display, public performance, distribution,\n     dissemination, communication, or importation, and to make material\n     available to the public including in ways that members of the\n     public may access the material from a place and at a time\n     individually chosen by them.\n\n  j. Sui Generis Database Rights means rights other than copyright\n     resulting from Directive 96/9/EC of the European Parliament and of\n     the Council of 11 March 1996 on the legal protection of databases,\n     as amended and/or succeeded, as well as other essentially\n     equivalent rights anywhere in the world.\n\n  k. You means the individual or entity exercising the Licensed Rights\n     under this Public License. Your has a corresponding meaning.\n\n\nSection 2 -- Scope.\n\n  a. License grant.\n\n       1. Subject to the terms and conditions of this Public License,\n          the Licensor hereby grants You a worldwide, royalty-free,\n          non-sublicensable, non-exclusive, irrevocable license to\n          exercise the Licensed Rights in the Licensed Material to:\n\n            a. reproduce and Share the Licensed Material, in whole or\n               in part; and\n\n            b. produce, reproduce, and Share Adapted Material.\n\n       2. Exceptions and Limitations. For the avoidance of doubt, where\n          Exceptions and Limitations apply to Your use, this Public\n          License does not apply, and You do not need to comply with\n          its terms and conditions.\n\n       3. Term. The term of this Public License is specified in Section\n          6(a).\n\n       4. Media and formats; technical modifications allowed. The\n          Licensor authorizes You to exercise the Licensed Rights in\n          all media and formats whether now known or hereafter created,\n          and to make technical modifications necessary to do so. The\n          Licensor waives and/or agrees not to assert any right or\n          authority to forbid You from making technical modifications\n          necessary to exercise the Licensed Rights, including\n          technical modifications necessary to circumvent Effective\n          Technological Measures. For purposes of this Public License,\n          simply making modifications authorized by this Section 2(a)\n          (4) never produces Adapted Material.\n\n       5. Downstream recipients.\n\n            a. Offer from the Licensor -- Licensed Material. Every\n               recipient of the Licensed Material automatically\n               receives an offer from the Licensor to exercise the\n               Licensed Rights under the terms and conditions of this\n               Public License.\n\n            b. No downstream restrictions. You may not offer or impose\n               any additional or different terms or conditions on, or\n               apply any Effective Technological Measures to, the\n               Licensed Material if doing so restricts exercise of the\n               Licensed Rights by any recipient of the Licensed\n               Material.\n\n       6. No endorsement. Nothing in this Public License constitutes or\n          may be construed as permission to assert or imply that You\n          are, or that Your use of the Licensed Material is, connected\n          with, or sponsored, endorsed, or granted official status by,\n          the Licensor or others designated to receive attribution as\n          provided in Section 3(a)(1)(A)(i).\n\n  b. Other rights.\n\n       1. Moral rights, such as the right of integrity, are not\n          licensed under this Public License, nor are publicity,\n          privacy, and/or other similar personality rights; however, to\n          the extent possible, the Licensor waives and/or agrees not to\n          assert any such rights held by the Licensor to the limited\n          extent necessary to allow You to exercise the Licensed\n          Rights, but not otherwise.\n\n       2. Patent and trademark rights are not licensed under this\n          Public License.\n\n       3. To the extent possible, the Licensor waives any right to\n          collect royalties from You for the exercise of the Licensed\n          Rights, whether directly or through a collecting society\n          under any voluntary or waivable statutory or compulsory\n          licensing scheme. In all other cases the Licensor expressly\n          reserves any right to collect such royalties.\n\n\nSection 3 -- License Conditions.\n\nYour exercise of the Licensed Rights is expressly made subject to the\nfollowing conditions.\n\n  a. Attribution.\n\n       1. If You Share the Licensed Material (including in modified\n          form), You must:\n\n            a. retain the following if it is supplied by the Licensor\n               with the Licensed Material:\n\n                 i. identification of the creator(s) of the Licensed\n                    Material and any others designated to receive\n                    attribution, in any reasonable manner requested by\n                    the Licensor (including by pseudonym if\n                    designated);\n\n                ii. a copyright notice;\n\n               iii. a notice that refers to this Public License;\n\n                iv. a notice that refers to the disclaimer of\n                    warranties;\n\n                 v. a URI or hyperlink to the Licensed Material to the\n                    extent reasonably practicable;\n\n            b. indicate if You modified the Licensed Material and\n               retain an indication of any previous modifications; and\n\n            c. indicate the Licensed Material is licensed under this\n               Public License, and include the text of, or the URI or\n               hyperlink to, this Public License.\n\n       2. You may satisfy the conditions in Section 3(a)(1) in any\n          reasonable manner based on the medium, means, and context in\n          which You Share the Licensed Material. For example, it may be\n          reasonable to satisfy the conditions by providing a URI or\n          hyperlink to a resource that includes the required\n          information.\n\n       3. If requested by the Licensor, You must remove any of the\n          information required by Section 3(a)(1)(A) to the extent\n          reasonably practicable.\n\n       4. If You Share Adapted Material You produce, the Adapter's\n          License You apply must not prevent recipients of the Adapted\n          Material from complying with this Public License.\n\n\nSection 4 -- Sui Generis Database Rights.\n\nWhere the Licensed Rights include Sui Generis Database Rights that\napply to Your use of the Licensed Material:\n\n  a. for the avoidance of doubt, Section 2(a)(1) grants You the right\n     to extract, reuse, reproduce, and Share all or a substantial\n     portion of the contents of the database;\n\n  b. if You include all or a substantial portion of the database\n     contents in a database in which You have Sui Generis Database\n     Rights, then the database in which You have Sui Generis Database\n     Rights (but not its individual contents) is Adapted Material; and\n\n  c. You must comply with the conditions in Section 3(a) if You Share\n     all or a substantial portion of the contents of the database.\n\nFor the avoidance of doubt, this Section 4 supplements and does not\nreplace Your obligations under this Public License where the Licensed\nRights include other Copyright and Similar Rights.\n\n\nSection 5 -- Disclaimer of Warranties and Limitation of Liability.\n\n  a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE\n     EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS\n     AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF\n     ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,\n     IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,\n     WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR\n     PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,\n     ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT\n     KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT\n     ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.\n\n  b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE\n     TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,\n     NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,\n     INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,\n     COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR\n     USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN\n     ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR\n     DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR\n     IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.\n\n  c. The disclaimer of warranties and limitation of liability provided\n     above shall be interpreted in a manner that, to the extent\n     possible, most closely approximates an absolute disclaimer and\n     waiver of all liability.\n\n\nSection 6 -- Term and Termination.\n\n  a. This Public License applies for the term of the Copyright and\n     Similar Rights licensed here. However, if You fail to comply with\n     this Public License, then Your rights under this Public License\n     terminate automatically.\n\n  b. Where Your right to use the Licensed Material has terminated under\n     Section 6(a), it reinstates:\n\n       1. automatically as of the date the violation is cured, provided\n          it is cured within 30 days of Your discovery of the\n          violation; or\n\n       2. upon express reinstatement by the Licensor.\n\n     For the avoidance of doubt, this Section 6(b) does not affect any\n     right the Licensor may have to seek remedies for Your violations\n     of this Public License.\n\n  c. For the avoidance of doubt, the Licensor may also offer the\n     Licensed Material under separate terms or conditions or stop\n     distributing the Licensed Material at any time; however, doing so\n     will not terminate this Public License.\n\n  d. Sections 1, 5, 6, 7, and 8 survive termination of this Public\n     License.\n\n\nSection 7 -- Other Terms and Conditions.\n\n  a. The Licensor shall not be bound by any additional or different\n     terms or conditions communicated by You unless expressly agreed.\n\n  b. Any arrangements, understandings, or agreements regarding the\n     Licensed Material not stated herein are separate from and\n     independent of the terms and conditions of this Public License.\n\n\nSection 8 -- Interpretation.\n\n  a. For the avoidance of doubt, this Public License does not, and\n     shall not be interpreted to, reduce, limit, restrict, or impose\n     conditions on any use of the Licensed Material that could lawfully\n     be made without permission under this Public License.\n\n  b. To the extent possible, if any provision of this Public License is\n     deemed unenforceable, it shall be automatically reformed to the\n     minimum extent necessary to make it enforceable. If the provision\n     cannot be reformed, it shall be severed from this Public License\n     without affecting the enforceability of the remaining terms and\n     conditions.\n\n  c. No term or condition of this Public License will be waived and no\n     failure to comply consented to unless expressly agreed to by the\n     Licensor.\n\n  d. Nothing in this Public License constitutes or may be interpreted\n     as a limitation upon, or waiver of, any privileges and immunities\n     that apply to the Licensor or You, including from the legal\n     processes of any jurisdiction or authority.\n\n\n=======================================================================\n\nCreative Commons is not a party to its public licenses.\nNotwithstanding, Creative Commons may elect to apply one of its public\nlicenses to material it publishes and in those instances will be\nconsidered the \"Licensor.\" Except for the limited purpose of indicating\nthat material is shared under a Creative Commons public license or as\notherwise permitted by the Creative Commons policies published at\ncreativecommons.org/policies, Creative Commons does not authorize the\nuse of the trademark \"Creative Commons\" or any other trademark or logo\nof Creative Commons without its prior written consent including,\nwithout limitation, in connection with any unauthorized modifications\nto any of its public licenses or any other arrangements,\nunderstandings, or agreements concerning use of licensed material. For\nthe avoidance of doubt, this paragraph does not form part of the public\nlicenses.\n\nCreative Commons may be contacted at creativecommons.org.\n"
  },
  {
    "path": "README.md",
    "content": "# ko.react.dev\n\n[![React Korea 디스코드 채널](https://img.shields.io/badge/discord-channel?style=for-the-badge&logo=discord&logoSize=100&label=React%20Korea&labelColor=%2323272F&color=%23149ECA)](https://discord.gg/YXdTyCh5KF)\n\n## 한국어 번역 정보\n\n### 가이드\n\n번역 혹은 기여를 진행할 때, 아래 가이드를 따라주세요.\n\n1. [기여 가이드라인<sup>Contributing</sup>](/CONTRIBUTING.md) 및 [기여자 행동 강령 규약<sup>Code of Conduct</sup>](/CODE_OF_CONDUCT.md)을 따르고 있습니다.\n1. [공통 스타일 가이드<sup>Universal Style Guide</sup>](/wiki/universal-style-guide.md)를 확인해주세요.\n1. [번역을 위한 모범 사례<sup>Best Practices for Translation</sup>](/wiki/best-practices-for-translation.md)를 따라주세요.\n1. 공통된 단어 번역을 위해 [번역 용어 정리<sup>Translate Glossary</sup>](/wiki/translate-glossary.md)를 참고해주세요.\n1. 끌어오기 요청<sup>Pull Request</sup>시 테스트를 통과하지 못할 경우, [`textlint` 가이드<sup>Textlint Guide</sup>](/wiki/textlint-guide.md)를 참고해주세요.\n1. 마지막으로 [맞춤법 검사<sup>Spelling Check</sup>](https://nara-speller.co.kr/speller/)를 진행해주세요.\n\n이 저장소<sup>Repository</sup>는 [ko.react.dev](https://ko.react.dev/)의 소스 코드와 개발 문서를 포함하고 있습니다.\n\n## 시작하기\n\n### 사전 준비\n\n1. Git\n1. Node: v16.8.0 이상의 모든 버전\n1. Yarn v1(`yarn@1.22.22`): [Yarn 설치 안내](https://yarnpkg.com/lang/en/docs/install/) 참고\n1. 포크<sup>Fork</sup>한 개인 저장소\n1. 로컬에 클론<sup>Clone</sup>한 [ko.react.dev 저장소](https://github.com/reactjs/ko.react.dev)\n\n### 설치\n\n1. `cd ko.react.dev`를 실행하여 프로젝트 경로로 이동합니다.\n1. `yarn` 명령어를 실행하여 npm 의존성 모듈들을 설치합니다.\n\n### 개발 서버 실행하기\n\n1. `yarn dev` 명령어를 사용하여 개발 서버를 시작합니다. (powered by [Next.js](https://nextjs.org).)\n1. `open http://localhost:3000` 명령어를 사용하여 선호하는 브라우저로 접속하세요.\n\n## 기여 방법\n\n### 가이드라인\n\n이 문서는 목적이 다른 여러 섹션으로 나뉩니다. 문장을 추가할 계획이라면, 적절한 섹션에 대한 [기여 가이드라인<sup>Contributing</sup>](/CONTRIBUTING.md)을 숙지하는 것이 도움이 될 것입니다.\n\n### 분기<sup>Branch</sup> 만들기\n\n1. `ko.react.dev` 로컬 저장소에서 `git checkout main`을 실행합니다.\n1. `git pull origin main`을 실행하여 최신 코드를 가져올 수 있습니다.\n1. `git checkout -b the-name-of-my-branch`를 실행하여 분기<sup>Branch</sup>를 만듭니다. (이때, `the-name-of-my-branch`를 적절한 이름으로 교체.)\n\n### 수정하기\n\n1. [\"개발 서버 실행하기\"](#개발-서버-실행하기) 부분을 따릅니다.\n1. 파일을 저장하고 브라우저에서 확인합니다.\n1. `src` 안에 있는 React 컴포넌트가 수정될 경우 hot-reload가 적용됩니다.\n1. `content` 안에 있는 마크다운 파일이 수정될 경우 hot-reload가 적용됩니다.\n1. 플러그인을 사용하는 경우, `.cache` 디렉토리를 제거한 후 서버를 재시작해야 합니다.\n\n### 수정사항 검사하기\n\n1. 가능하다면, 변경한 부분에 대해서 많이 사용하는 브라우저의 최신 버전에서 시각적으로 제대로 적용되었는지 확인해주세요. (데스크탑과 모바일 모두.)\n1. 프로젝트 루트에서 `yarn check-all`을 실행합니다. (이 명령어는 Prettier, ESLint, 그리고 타입 유효성 검사를 진행합니다.)\n\n### 푸시<sup>Push</sup> 하기\n\n1. `git add -A && git commit -m \"My message\"`를 실행하여 변경한 파일들을 커밋<sup>commit</sup> 해주세요. (이때, `My message` 부분을 `Fix header logo on Android` 같은 커밋 메시지로 교체.)\n1. `git push my-fork-name the-name-of-my-branch`\n1. [ko.react.dev 저장소](https://github.com/reactjs/ko.react.dev)에서 최근에 푸시된 분기<sup>Branch</sup>를 볼 수 있습니다.\n1. 깃허브<sup>GitHub</sup> 지침을 따라주세요.\n1. 가능하다면 시각적으로 변화된 부분의 스크린샷을 첨부해주세요. 변경 사항이 깃허브<sup>GitHub</sup>에 푸시<sup>Push</sup>되면 미리보기 빌드가 트리거됩니다.\n\n## 문제 해결하기\n\n`yarn cache-reset` 명령어를 사용하여 로컬 캐시를 초기화합니다.\n\n## 번역\n\n`react.dev` 번역에 흥미가 있다면, [translations.react.dev](https://translations.react.dev/)에서 현재 번역이 얼마나 진행되었는지 확인해주세요.\n\n번역하려는 언어가 아직 진행되지 않았다면, 해당 언어에 대해 새롭게 만들 수 있습니다. [translations.react.dev 저장소](https://github.com/reactjs/translations.react.dev)를 참고해주세요.\n\n## 저작권\n\n위 내용에 대한 저작권은 [react.dev](https://react.dev)가 가지고 있으며, [LICENSE-DOCS.md](/LICENSE-DOCS.md)에서 볼 수 있는 CC-BY-4.0 라이센스를 따릅니다.\n"
  },
  {
    "path": "colors.js",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nmodule.exports = {\n  // Text colors\n  primary: '#23272F', // gray-90\n  'primary-dark': '#F6F7F9', // gray-5\n  secondary: '#404756', // gray-70\n  'secondary-dark': '#EBECF0', // gray-10\n  tertiary: '#5E687E', // gray-50\n  'tertiary-dark': '#99A1B3', // gray-30\n  link: '#087EA4', // blue-50\n  'link-dark': '#58C4DC', // blue-40\n  syntax: '#EBECF0', // gray-10\n  wash: '#FFFFFF',\n  'wash-dark': '#23272F', // gray-90\n  card: '#F6F7F9', // gray-05\n  'card-dark': '#343A46', // gray-80\n  highlight: '#E6F7FF', // blue-10\n  'highlight-dark': 'rgba(88,175,223,.1)',\n  border: '#EBECF0', // gray-10\n  'border-dark': '#343A46', // gray-80\n  'secondary-button': '#EBECF0', // gray-10\n  'secondary-button-dark': '#404756', // gray-70\n  brand: '#087EA4', // blue-40\n  'brand-dark': '#58C4DC', // blue-40\n\n  // Gray\n  'gray-95': '#16181D',\n  'gray-90': '#23272F',\n  'gray-80': '#343A46',\n  'gray-70': '#404756',\n  'gray-60': '#4E5769',\n  'gray-50': '#5E687E',\n  'gray-40': '#78839B',\n  'gray-30': '#99A1B3',\n  'gray-20': '#BCC1CD',\n  'gray-15': '#D0D3DC',\n  'gray-10': '#EBECF0',\n  'gray-5': '#F6F7F9',\n\n  // Blue\n  'blue-80': '#043849',\n  'blue-60': '#045975',\n  'blue-50': '#087EA4',\n  'blue-40': '#149ECA', // Brand Blue\n  'blue-30': '#58C4DC', // unused\n  'blue-20': '#ABE2ED',\n  'blue-10': '#E6F7FF', // todo: doesn't match illustrations\n  'blue-5': '#E6F6FA',\n\n  // Yellow\n  'yellow-60': '#B65700',\n  'yellow-50': '#C76A15',\n  'yellow-40': '#DB7D27', // unused\n  'yellow-30': '#FABD62', // unused\n  'yellow-20': '#FCDEB0', // unused\n  'yellow-10': '#FDE7C7',\n  'yellow-5': '#FEF5E7',\n\n  // Purple\n  'purple-60': '#2B3491', // unused\n  'purple-50': '#575FB7',\n  'purple-40': '#6B75DB',\n  'purple-30': '#8891EC',\n  'purple-20': '#C3C8F5', // unused\n  'purple-10': '#E7E9FB',\n  'purple-5': '#F3F4FD',\n\n  // Green\n  'green-60': '#2B6E62',\n  'green-50': '#388F7F',\n  'green-40': '#44AC99',\n  'green-30': '#7FCCBF',\n  'green-20': '#ABDED5',\n  'green-10': '#E5F5F2',\n  'green-5': '#F4FBF9',\n\n  // RED\n  'red-60': '#712D28',\n  'red-50': '#A6423A', // unused\n  'red-40': '#C1554D',\n  'red-30': '#D07D77',\n  'red-20': '#E5B7B3', // unused\n  'red-10': '#F2DBD9', // unused\n  'red-5': '#FAF1F0',\n\n  // MISC\n  'code-block': '#99a1b30f', // gray-30 @ 6%\n  'gradient-blue': '#58C4DC', // Only used for the landing gradient for now.\n  github: {\n    highlight: '#fffbdd',\n  },\n};\n"
  },
  {
    "path": "eslint-local-rules/__tests__/fixtures/src/content/basic-error.md",
    "content": "```jsx\nimport {useState} from 'react';\nfunction Counter() {\n  const [count, setCount] = useState(0);\n  setCount(count + 1);\n  return <div>{count}</div>;\n}\n```\n"
  },
  {
    "path": "eslint-local-rules/__tests__/fixtures/src/content/duplicate-metadata.md",
    "content": "```jsx title=\"Counter\" {expectedErrors: {'react-compiler': [99]}} {expectedErrors: {'react-compiler': [2]}}\nimport {useState} from 'react';\nfunction Counter() {\n  const [count, setCount] = useState(0);\n  setCount(count + 1);\n  return <div>{count}</div>;\n}\n```\n"
  },
  {
    "path": "eslint-local-rules/__tests__/fixtures/src/content/malformed-metadata.md",
    "content": "```jsx {expectedErrors: {'react-compiler': 'invalid'}}\nimport {useState} from 'react';\nfunction Counter() {\n  const [count, setCount] = useState(0);\n  setCount(count + 1);\n  return <div>{count}</div>;\n}\n```\n"
  },
  {
    "path": "eslint-local-rules/__tests__/fixtures/src/content/mixed-language.md",
    "content": "```bash\nsetCount()\n```\n\n```txt\nimport {useState} from 'react';\n```\n"
  },
  {
    "path": "eslint-local-rules/__tests__/fixtures/src/content/stale-expected-error.md",
    "content": "```jsx {expectedErrors: {'react-compiler': [3]}}\nfunction Hello() {\n  return <h1>Hello</h1>;\n}\n```\n"
  },
  {
    "path": "eslint-local-rules/__tests__/fixtures/src/content/suppressed-error.md",
    "content": "```jsx {expectedErrors: {'react-compiler': [4]}}\nimport {useState} from 'react';\nfunction Counter() {\n  const [count, setCount] = useState(0);\n  setCount(count + 1);\n  return <div>{count}</div>;\n}\n```\n"
  },
  {
    "path": "eslint-local-rules/__tests__/lint-markdown-code-blocks.test.js",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nconst assert = require('assert');\nconst fs = require('fs');\nconst path = require('path');\nconst {ESLint} = require('eslint');\nconst plugin = require('..');\n\nconst FIXTURES_DIR = path.join(__dirname, 'fixtures', 'src', 'content');\nconst PARSER_PATH = path.join(__dirname, '..', 'parser.js');\n\nfunction createESLint({fix = false} = {}) {\n  return new ESLint({\n    useEslintrc: false,\n    fix,\n    plugins: {\n      'local-rules': plugin,\n    },\n    overrideConfig: {\n      parser: PARSER_PATH,\n      plugins: ['local-rules'],\n      rules: {\n        'local-rules/lint-markdown-code-blocks': 'error',\n      },\n      parserOptions: {\n        sourceType: 'module',\n      },\n    },\n  });\n}\n\nfunction readFixture(name) {\n  return fs.readFileSync(path.join(FIXTURES_DIR, name), 'utf8');\n}\n\nasync function lintFixture(name, {fix = false} = {}) {\n  const eslint = createESLint({fix});\n  const filePath = path.join(FIXTURES_DIR, name);\n  const markdown = readFixture(name);\n  const [result] = await eslint.lintText(markdown, {filePath});\n  return result;\n}\n\nasync function run() {\n  const basicResult = await lintFixture('basic-error.md');\n  assert.strictEqual(basicResult.messages.length, 1, 'expected one diagnostic');\n  assert(\n    basicResult.messages[0].message.includes('Calling setState during render'),\n    'expected message to mention setState during render'\n  );\n\n  const suppressedResult = await lintFixture('suppressed-error.md');\n  assert.strictEqual(\n    suppressedResult.messages.length,\n    0,\n    'expected suppression metadata to silence diagnostic'\n  );\n\n  const staleResult = await lintFixture('stale-expected-error.md');\n  assert.strictEqual(\n    staleResult.messages.length,\n    1,\n    'expected stale metadata error'\n  );\n  assert.strictEqual(\n    staleResult.messages[0].message,\n    'React Compiler expected error on line 3 was not triggered'\n  );\n\n  const duplicateResult = await lintFixture('duplicate-metadata.md');\n  assert.strictEqual(\n    duplicateResult.messages.length,\n    2,\n    'expected duplicate metadata to surface compiler diagnostic and stale metadata notice'\n  );\n  const duplicateFixed = await lintFixture('duplicate-metadata.md', {\n    fix: true,\n  });\n  assert(\n    duplicateFixed.output.includes(\"{expectedErrors: {'react-compiler': [4]}}\"),\n    'expected duplicates to be rewritten to a single canonical block'\n  );\n  assert(\n    !duplicateFixed.output.includes('[99]'),\n    'expected stale line numbers to be removed from metadata'\n  );\n\n  const mixedLanguageResult = await lintFixture('mixed-language.md');\n  assert.strictEqual(\n    mixedLanguageResult.messages.length,\n    0,\n    'expected non-js code fences to be ignored'\n  );\n\n  const malformedResult = await lintFixture('malformed-metadata.md');\n  assert.strictEqual(\n    malformedResult.messages.length,\n    1,\n    'expected malformed metadata to fall back to compiler diagnostics'\n  );\n  const malformedFixed = await lintFixture('malformed-metadata.md', {\n    fix: true,\n  });\n  assert(\n    malformedFixed.output.includes(\"{expectedErrors: {'react-compiler': [4]}}\"),\n    'expected malformed metadata to be replaced with canonical form'\n  );\n}\n\nrun().catch((error) => {\n  console.error(error);\n  process.exitCode = 1;\n});\n"
  },
  {
    "path": "eslint-local-rules/index.js",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nconst lintMarkdownCodeBlocks = require('./rules/lint-markdown-code-blocks');\n\nmodule.exports = {\n  rules: {\n    'lint-markdown-code-blocks': lintMarkdownCodeBlocks,\n  },\n};\n"
  },
  {
    "path": "eslint-local-rules/package.json",
    "content": "{\n  \"name\": \"eslint-plugin-local-rules\",\n  \"version\": \"0.0.0\",\n  \"main\": \"index.js\",\n  \"private\": \"true\",\n  \"scripts\": {\n    \"test\": \"node __tests__/lint-markdown-code-blocks.test.js\"\n  },\n  \"devDependencies\": {\n    \"eslint-mdx\": \"^2\"\n  }\n}\n"
  },
  {
    "path": "eslint-local-rules/parser.js",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nmodule.exports = require('eslint-mdx');\n"
  },
  {
    "path": "eslint-local-rules/rules/diagnostics.js",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nfunction getRelativeLine(loc) {\n  return loc?.start?.line ?? loc?.line ?? 1;\n}\n\nfunction getRelativeColumn(loc) {\n  return loc?.start?.column ?? loc?.column ?? 0;\n}\n\nfunction getRelativeEndLine(loc, fallbackLine) {\n  if (loc?.end?.line != null) {\n    return loc.end.line;\n  }\n  if (loc?.line != null) {\n    return loc.line;\n  }\n  return fallbackLine;\n}\n\nfunction getRelativeEndColumn(loc, fallbackColumn) {\n  if (loc?.end?.column != null) {\n    return loc.end.column;\n  }\n  if (loc?.column != null) {\n    return loc.column;\n  }\n  return fallbackColumn;\n}\n\n/**\n * @param {import('./markdown').MarkdownCodeBlock} block\n * @param {Array<{detail: any, loc: any, message: string}>} diagnostics\n * @returns {Array<{detail: any, message: string, relativeStartLine: number, markdownLoc: {start: {line: number, column: number}, end: {line: number, column: number}}}>}\n */\nfunction normalizeDiagnostics(block, diagnostics) {\n  return diagnostics.map(({detail, loc, message}) => {\n    const relativeStartLine = Math.max(getRelativeLine(loc), 1);\n    const relativeStartColumn = Math.max(getRelativeColumn(loc), 0);\n    const relativeEndLine = Math.max(\n      getRelativeEndLine(loc, relativeStartLine),\n      relativeStartLine\n    );\n    const relativeEndColumn = Math.max(\n      getRelativeEndColumn(loc, relativeStartColumn),\n      relativeStartColumn\n    );\n\n    const markdownStartLine = block.codeStartLine + relativeStartLine - 1;\n    const markdownEndLine = block.codeStartLine + relativeEndLine - 1;\n\n    return {\n      detail,\n      message,\n      relativeStartLine,\n      markdownLoc: {\n        start: {\n          line: markdownStartLine,\n          column: relativeStartColumn,\n        },\n        end: {\n          line: markdownEndLine,\n          column: relativeEndColumn,\n        },\n      },\n    };\n  });\n}\n\nmodule.exports = {\n  normalizeDiagnostics,\n};\n"
  },
  {
    "path": "eslint-local-rules/rules/lint-markdown-code-blocks.js",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nconst {\n  buildFenceLine,\n  getCompilerExpectedLines,\n  getSortedUniqueNumbers,\n  hasCompilerEntry,\n  metadataEquals,\n  metadataHasExpectedErrorsToken,\n  removeCompilerExpectedLines,\n  setCompilerExpectedLines,\n} = require('./metadata');\nconst {normalizeDiagnostics} = require('./diagnostics');\nconst {parseMarkdownFile} = require('./markdown');\nconst {runReactCompiler} = require('./react-compiler');\n\nmodule.exports = {\n  meta: {\n    type: 'problem',\n    docs: {\n      description: 'Run React Compiler on markdown code blocks',\n      category: 'Possible Errors',\n    },\n    fixable: 'code',\n    hasSuggestions: true,\n    schema: [],\n  },\n\n  create(context) {\n    return {\n      Program(node) {\n        const filename = context.getFilename();\n        if (!filename.endsWith('.md') || !filename.includes('src/content')) {\n          return;\n        }\n\n        const sourceCode = context.getSourceCode();\n        const {blocks} = parseMarkdownFile(sourceCode.text, filename);\n        // For each supported code block, run the compiler and reconcile metadata.\n        for (const block of blocks) {\n          const compilerResult = runReactCompiler(\n            block.code,\n            `${filename}#codeblock`\n          );\n\n          const expectedLines = getCompilerExpectedLines(block.metadata);\n          const expectedLineSet = new Set(expectedLines);\n          const diagnostics = normalizeDiagnostics(\n            block,\n            compilerResult.diagnostics\n          );\n\n          const errorLines = new Set();\n          const unexpectedDiagnostics = [];\n\n          for (const diagnostic of diagnostics) {\n            const line = diagnostic.relativeStartLine;\n            errorLines.add(line);\n            if (!expectedLineSet.has(line)) {\n              unexpectedDiagnostics.push(diagnostic);\n            }\n          }\n\n          const normalizedErrorLines = getSortedUniqueNumbers(\n            Array.from(errorLines)\n          );\n          const missingExpectedLines = expectedLines.filter(\n            (line) => !errorLines.has(line)\n          );\n\n          const desiredMetadata = normalizedErrorLines.length\n            ? setCompilerExpectedLines(block.metadata, normalizedErrorLines)\n            : removeCompilerExpectedLines(block.metadata);\n\n          // Compute canonical metadata and attach an autofix when it differs.\n          const metadataChanged = !metadataEquals(\n            block.metadata,\n            desiredMetadata\n          );\n          const replacementLine = buildFenceLine(block.lang, desiredMetadata);\n          const replacementDiffers = block.fence.rawText !== replacementLine;\n          const applyReplacementFix = replacementDiffers\n            ? (fixer) =>\n                fixer.replaceTextRange(block.fence.range, replacementLine)\n            : null;\n\n          const hasDuplicateMetadata =\n            block.metadata.hadDuplicateExpectedErrors;\n          const hasExpectedErrorsMetadata = metadataHasExpectedErrorsToken(\n            block.metadata\n          );\n\n          const shouldFixUnexpected =\n            Boolean(applyReplacementFix) &&\n            normalizedErrorLines.length > 0 &&\n            (metadataChanged ||\n              hasDuplicateMetadata ||\n              !hasExpectedErrorsMetadata);\n\n          let fixAlreadyAttached = false;\n\n          for (const diagnostic of unexpectedDiagnostics) {\n            const reportData = {\n              node,\n              loc: diagnostic.markdownLoc,\n              message: diagnostic.message,\n            };\n\n            if (\n              shouldFixUnexpected &&\n              applyReplacementFix &&\n              !fixAlreadyAttached\n            ) {\n              reportData.fix = applyReplacementFix;\n              reportData.suggest = [\n                {\n                  desc: 'Add expectedErrors metadata to suppress these errors',\n                  fix: applyReplacementFix,\n                },\n              ];\n              fixAlreadyAttached = true;\n            }\n\n            context.report(reportData);\n          }\n\n          // Assert that expectedErrors is actually needed\n          if (\n            Boolean(applyReplacementFix) &&\n            missingExpectedLines.length > 0 &&\n            hasCompilerEntry(block.metadata)\n          ) {\n            const plural = missingExpectedLines.length > 1;\n            const message = plural\n              ? `React Compiler expected errors on lines ${missingExpectedLines.join(\n                  ', '\n                )} were not triggered`\n              : `React Compiler expected error on line ${missingExpectedLines[0]} was not triggered`;\n\n            const reportData = {\n              node,\n              loc: {\n                start: {\n                  line: block.position.start.line,\n                  column: 0,\n                },\n                end: {\n                  line: block.position.start.line,\n                  column: block.fence.rawText.length,\n                },\n              },\n              message,\n            };\n\n            if (!fixAlreadyAttached && applyReplacementFix) {\n              reportData.fix = applyReplacementFix;\n              fixAlreadyAttached = true;\n            } else if (applyReplacementFix) {\n              reportData.suggest = [\n                {\n                  desc: 'Remove stale expectedErrors metadata',\n                  fix: applyReplacementFix,\n                },\n              ];\n            }\n\n            context.report(reportData);\n          }\n        }\n      },\n    };\n  },\n};\n"
  },
  {
    "path": "eslint-local-rules/rules/markdown.js",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nconst remark = require('remark');\nconst {parseFenceMetadata} = require('./metadata');\n\n/**\n * @typedef {Object} MarkdownCodeBlock\n * @property {string} code\n * @property {number} codeStartLine\n * @property {{start: {line: number, column: number}, end: {line: number, column: number}}} position\n * @property {{lineIndex: number, rawText: string, metaText: string, range: [number, number]}} fence\n * @property {string} filePath\n * @property {string} lang\n * @property {import('./metadata').FenceMetadata} metadata\n */\n\nconst SUPPORTED_LANGUAGES = new Set([\n  'js',\n  'jsx',\n  'javascript',\n  'ts',\n  'tsx',\n  'typescript',\n]);\n\nfunction computeLineOffsets(lines) {\n  const offsets = [];\n  let currentOffset = 0;\n\n  for (const line of lines) {\n    offsets.push(currentOffset);\n    currentOffset += line.length + 1;\n  }\n\n  return offsets;\n}\n\nfunction parseMarkdownFile(content, filePath) {\n  const tree = remark().parse(content);\n  const lines = content.split('\\n');\n  const lineOffsets = computeLineOffsets(lines);\n  const blocks = [];\n\n  function traverse(node) {\n    if (!node || typeof node !== 'object') {\n      return;\n    }\n\n    if (node.type === 'code') {\n      const rawLang = node.lang || '';\n      const normalizedLang = rawLang.toLowerCase();\n      if (!normalizedLang || !SUPPORTED_LANGUAGES.has(normalizedLang)) {\n        return;\n      }\n\n      const fenceLineIndex = (node.position?.start?.line ?? 1) - 1;\n      const fenceStartOffset = node.position?.start?.offset ?? 0;\n      const fenceLine = lines[fenceLineIndex] ?? '';\n      const fenceEndOffset = fenceStartOffset + fenceLine.length;\n\n      let metaText = '';\n      if (fenceLine) {\n        const prefixMatch = fenceLine.match(/^`{3,}\\s*/);\n        const prefixLength = prefixMatch ? prefixMatch[0].length : 3;\n        metaText = fenceLine.slice(prefixLength + rawLang.length);\n      } else if (node.meta) {\n        metaText = ` ${node.meta}`;\n      }\n\n      const metadata = parseFenceMetadata(metaText);\n\n      blocks.push({\n        lang: rawLang || normalizedLang,\n        metadata,\n        filePath,\n        code: node.value || '',\n        codeStartLine: (node.position?.start?.line ?? 1) + 1,\n        position: {\n          start: {\n            line: fenceLineIndex + 1,\n            column: (node.position?.start?.column ?? 1) - 1,\n          },\n          end: {\n            line: fenceLineIndex + 1,\n            column: (node.position?.start?.column ?? 1) - 1 + fenceLine.length,\n          },\n        },\n        fence: {\n          lineIndex: fenceLineIndex,\n          rawText: fenceLine,\n          metaText,\n          range: [fenceStartOffset, fenceEndOffset],\n        },\n      });\n      return;\n    }\n\n    if ('children' in node && Array.isArray(node.children)) {\n      for (const child of node.children) {\n        traverse(child);\n      }\n    }\n  }\n\n  traverse(tree);\n\n  return {\n    content,\n    blocks,\n    lines,\n    lineOffsets,\n  };\n}\n\nmodule.exports = {\n  SUPPORTED_LANGUAGES,\n  computeLineOffsets,\n  parseMarkdownFile,\n};\n"
  },
  {
    "path": "eslint-local-rules/rules/metadata.js",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/**\n * @typedef {{type: 'text', raw: string}} TextToken\n * @typedef {{\n *   type: 'expectedErrors',\n *   entries: Record<string, Array<number>>,\n *   raw?: string,\n * }} ExpectedErrorsToken\n * @typedef {TextToken | ExpectedErrorsToken} MetadataToken\n *\n * @typedef {{\n *   leading: string,\n *   trailing: string,\n *   tokens: Array<MetadataToken>,\n *   parseError: boolean,\n *   hadDuplicateExpectedErrors: boolean,\n * }} FenceMetadata\n */\n\nconst EXPECTED_ERRORS_BLOCK_REGEX = /\\{\\s*expectedErrors\\s*:/;\nconst REACT_COMPILER_KEY = 'react-compiler';\n\nfunction getSortedUniqueNumbers(values) {\n  return Array.from(new Set(values))\n    .filter((value) => typeof value === 'number' && !Number.isNaN(value))\n    .sort((a, b) => a - b);\n}\n\nfunction tokenizeMeta(body) {\n  if (!body) {\n    return [];\n  }\n\n  const tokens = [];\n  let current = '';\n  let depth = 0;\n\n  for (let i = 0; i < body.length; i++) {\n    const char = body[i];\n    if (char === '{') {\n      depth++;\n    } else if (char === '}') {\n      depth = Math.max(depth - 1, 0);\n    }\n\n    if (char === ' ' && depth === 0) {\n      if (current) {\n        tokens.push(current);\n        current = '';\n      }\n      continue;\n    }\n\n    current += char;\n  }\n\n  if (current) {\n    tokens.push(current);\n  }\n\n  return tokens;\n}\n\nfunction normalizeEntryValues(values) {\n  if (!Array.isArray(values)) {\n    return [];\n  }\n  return getSortedUniqueNumbers(values);\n}\n\nfunction parseExpectedErrorsEntries(rawEntries) {\n  const normalized = rawEntries\n    .replace(/([{,]\\s*)([a-zA-Z_$][\\w$]*)\\s*:/g, '$1\"$2\":')\n    .replace(/'([^']*)'/g, '\"$1\"');\n\n  const parsed = JSON.parse(normalized);\n  const entries = {};\n\n  if (parsed && typeof parsed === 'object') {\n    for (const [key, value] of Object.entries(parsed)) {\n      entries[key] = normalizeEntryValues(\n        Array.isArray(value) ? value.flat() : value\n      );\n    }\n  }\n\n  return entries;\n}\n\nfunction parseExpectedErrorsToken(tokenText) {\n  const match = tokenText.match(\n    /^\\{\\s*expectedErrors\\s*:\\s*(\\{[\\s\\S]*\\})\\s*\\}$/\n  );\n  if (!match) {\n    return null;\n  }\n\n  const entriesSource = match[1];\n  let parseError = false;\n  let entries;\n\n  try {\n    entries = parseExpectedErrorsEntries(entriesSource);\n  } catch (error) {\n    parseError = true;\n    entries = {};\n  }\n\n  return {\n    token: {\n      type: 'expectedErrors',\n      entries,\n      raw: tokenText,\n    },\n    parseError,\n  };\n}\n\nfunction parseFenceMetadata(metaText) {\n  if (!metaText) {\n    return {\n      leading: '',\n      trailing: '',\n      tokens: [],\n      parseError: false,\n      hadDuplicateExpectedErrors: false,\n    };\n  }\n\n  const leading = metaText.match(/^\\s*/)?.[0] ?? '';\n  const trailing = metaText.match(/\\s*$/)?.[0] ?? '';\n  const bodyStart = leading.length;\n  const bodyEnd = metaText.length - trailing.length;\n  const body = metaText.slice(bodyStart, bodyEnd).trim();\n\n  if (!body) {\n    return {\n      leading,\n      trailing,\n      tokens: [],\n      parseError: false,\n      hadDuplicateExpectedErrors: false,\n    };\n  }\n\n  const tokens = [];\n  let parseError = false;\n  let sawExpectedErrors = false;\n  let hadDuplicateExpectedErrors = false;\n\n  for (const rawToken of tokenizeMeta(body)) {\n    const normalizedToken = rawToken.trim();\n    if (!normalizedToken) {\n      continue;\n    }\n\n    if (EXPECTED_ERRORS_BLOCK_REGEX.test(normalizedToken)) {\n      const parsed = parseExpectedErrorsToken(normalizedToken);\n      if (parsed) {\n        if (sawExpectedErrors) {\n          hadDuplicateExpectedErrors = true;\n          // Drop duplicates. We'll rebuild the canonical block on write.\n          continue;\n        }\n        tokens.push(parsed.token);\n        parseError = parseError || parsed.parseError;\n        sawExpectedErrors = true;\n        continue;\n      }\n    }\n\n    tokens.push({type: 'text', raw: normalizedToken});\n  }\n\n  return {\n    leading,\n    trailing,\n    tokens,\n    parseError,\n    hadDuplicateExpectedErrors,\n  };\n}\n\nfunction cloneMetadata(metadata) {\n  return {\n    leading: metadata.leading,\n    trailing: metadata.trailing,\n    parseError: metadata.parseError,\n    hadDuplicateExpectedErrors: metadata.hadDuplicateExpectedErrors,\n    tokens: metadata.tokens.map((token) => {\n      if (token.type === 'expectedErrors') {\n        const clonedEntries = {};\n        for (const [key, value] of Object.entries(token.entries)) {\n          clonedEntries[key] = [...value];\n        }\n        return {type: 'expectedErrors', entries: clonedEntries};\n      }\n      return {type: 'text', raw: token.raw};\n    }),\n  };\n}\n\nfunction findExpectedErrorsToken(metadata) {\n  return (\n    metadata.tokens.find((token) => token.type === 'expectedErrors') || null\n  );\n}\n\nfunction getCompilerExpectedLines(metadata) {\n  const token = findExpectedErrorsToken(metadata);\n  if (!token) {\n    return [];\n  }\n  return getSortedUniqueNumbers(token.entries[REACT_COMPILER_KEY] || []);\n}\n\nfunction hasCompilerEntry(metadata) {\n  const token = findExpectedErrorsToken(metadata);\n  return Boolean(token && token.entries[REACT_COMPILER_KEY]?.length);\n}\n\nfunction metadataHasExpectedErrorsToken(metadata) {\n  return Boolean(findExpectedErrorsToken(metadata));\n}\n\nfunction stringifyExpectedErrorsToken(token) {\n  const entries = token.entries || {};\n  const keys = Object.keys(entries).filter((key) => entries[key].length > 0);\n  if (keys.length === 0) {\n    return '';\n  }\n\n  keys.sort();\n\n  const segments = keys.map((key) => {\n    const values = entries[key];\n    return `'${key}': [${values.join(', ')}]`;\n  });\n\n  return `{expectedErrors: {${segments.join(', ')}}}`;\n}\n\nfunction stringifyFenceMetadata(metadata) {\n  if (!metadata.tokens.length) {\n    return '';\n  }\n\n  const parts = metadata.tokens\n    .map((token) => {\n      if (token.type === 'expectedErrors') {\n        return stringifyExpectedErrorsToken(token);\n      }\n      return token.raw;\n    })\n    .filter(Boolean);\n\n  if (!parts.length) {\n    return '';\n  }\n\n  const leading = metadata.leading || ' ';\n  const trailing = metadata.trailing ? metadata.trailing.trimEnd() : '';\n  const body = parts.join(' ');\n  return `${leading}${body}${trailing}`;\n}\n\nfunction buildFenceLine(lang, metadata) {\n  const meta = stringifyFenceMetadata(metadata);\n  return meta ? `\\`\\`\\`${lang}${meta}` : `\\`\\`\\`${lang}`;\n}\n\nfunction metadataEquals(a, b) {\n  if (a.leading !== b.leading || a.trailing !== b.trailing) {\n    return false;\n  }\n\n  if (a.tokens.length !== b.tokens.length) {\n    return false;\n  }\n\n  for (let i = 0; i < a.tokens.length; i++) {\n    const left = a.tokens[i];\n    const right = b.tokens[i];\n    if (left.type !== right.type) {\n      return false;\n    }\n    if (left.type === 'text') {\n      if (left.raw !== right.raw) {\n        return false;\n      }\n    } else {\n      const leftKeys = Object.keys(left.entries).sort();\n      const rightKeys = Object.keys(right.entries).sort();\n      if (leftKeys.length !== rightKeys.length) {\n        return false;\n      }\n      for (let j = 0; j < leftKeys.length; j++) {\n        if (leftKeys[j] !== rightKeys[j]) {\n          return false;\n        }\n        const lValues = getSortedUniqueNumbers(left.entries[leftKeys[j]]);\n        const rValues = getSortedUniqueNumbers(right.entries[rightKeys[j]]);\n        if (lValues.length !== rValues.length) {\n          return false;\n        }\n        for (let k = 0; k < lValues.length; k++) {\n          if (lValues[k] !== rValues[k]) {\n            return false;\n          }\n        }\n      }\n    }\n  }\n\n  return true;\n}\n\nfunction normalizeMetadata(metadata) {\n  const normalized = cloneMetadata(metadata);\n  normalized.hadDuplicateExpectedErrors = false;\n  normalized.parseError = false;\n  if (!normalized.tokens.length) {\n    normalized.leading = '';\n    normalized.trailing = '';\n  }\n  return normalized;\n}\n\nfunction setCompilerExpectedLines(metadata, lines) {\n  const normalizedLines = getSortedUniqueNumbers(lines);\n  if (normalizedLines.length === 0) {\n    return removeCompilerExpectedLines(metadata);\n  }\n\n  const next = cloneMetadata(metadata);\n  let token = findExpectedErrorsToken(next);\n  if (!token) {\n    token = {type: 'expectedErrors', entries: {}};\n    next.tokens = [token, ...next.tokens];\n  }\n\n  token.entries[REACT_COMPILER_KEY] = normalizedLines;\n  return normalizeMetadata(next);\n}\n\nfunction removeCompilerExpectedLines(metadata) {\n  const next = cloneMetadata(metadata);\n  const token = findExpectedErrorsToken(next);\n  if (!token) {\n    return normalizeMetadata(next);\n  }\n\n  delete token.entries[REACT_COMPILER_KEY];\n\n  const hasEntries = Object.values(token.entries).some(\n    (value) => Array.isArray(value) && value.length > 0\n  );\n\n  if (!hasEntries) {\n    next.tokens = next.tokens.filter((item) => item !== token);\n  }\n\n  return normalizeMetadata(next);\n}\n\nmodule.exports = {\n  buildFenceLine,\n  getCompilerExpectedLines,\n  getSortedUniqueNumbers,\n  hasCompilerEntry,\n  metadataEquals,\n  metadataHasExpectedErrorsToken,\n  parseFenceMetadata,\n  removeCompilerExpectedLines,\n  setCompilerExpectedLines,\n  stringifyFenceMetadata,\n};\n"
  },
  {
    "path": "eslint-local-rules/rules/react-compiler.js",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nconst {transformFromAstSync} = require('@babel/core');\nconst {parse: babelParse} = require('@babel/parser');\nconst BabelPluginReactCompiler = require('babel-plugin-react-compiler').default;\nconst {\n  parsePluginOptions,\n  validateEnvironmentConfig,\n} = require('babel-plugin-react-compiler');\n\nconst COMPILER_OPTIONS = {\n  noEmit: true,\n  panicThreshold: 'none',\n  environment: validateEnvironmentConfig({\n    validateRefAccessDuringRender: true,\n    validateNoSetStateInRender: true,\n    validateNoSetStateInEffects: true,\n    validateNoJSXInTryStatements: true,\n    validateNoImpureFunctionsInRender: true,\n    validateStaticComponents: true,\n    validateNoFreezingKnownMutableFunctions: true,\n    validateNoVoidUseMemo: true,\n    validateNoCapitalizedCalls: [],\n    validateHooksUsage: true,\n    validateNoDerivedComputationsInEffects: true,\n  }),\n};\n\nfunction hasRelevantCode(code) {\n  const functionPattern = /^(export\\s+)?(default\\s+)?function\\s+\\w+/m;\n  const arrowPattern =\n    /^(export\\s+)?(const|let|var)\\s+\\w+\\s*=\\s*(\\([^)]*\\)|\\w+)\\s*=>/m;\n  const hasImports = /^import\\s+/m.test(code);\n\n  return functionPattern.test(code) || arrowPattern.test(code) || hasImports;\n}\n\nfunction runReactCompiler(code, filename) {\n  const result = {\n    sourceCode: code,\n    events: [],\n  };\n\n  if (!hasRelevantCode(code)) {\n    return {...result, diagnostics: []};\n  }\n\n  const options = parsePluginOptions({\n    ...COMPILER_OPTIONS,\n  });\n\n  options.logger = {\n    logEvent: (_, event) => {\n      if (event.kind === 'CompileError') {\n        const category = event.detail?.category;\n        if (category === 'Todo' || category === 'Invariant') {\n          return;\n        }\n        result.events.push(event);\n      }\n    },\n  };\n\n  try {\n    const ast = babelParse(code, {\n      sourceFilename: filename,\n      sourceType: 'module',\n      plugins: ['jsx', 'typescript'],\n    });\n\n    transformFromAstSync(ast, code, {\n      filename,\n      highlightCode: false,\n      retainLines: true,\n      plugins: [[BabelPluginReactCompiler, options]],\n      sourceType: 'module',\n      configFile: false,\n      babelrc: false,\n    });\n  } catch (error) {\n    return {...result, diagnostics: []};\n  }\n\n  const diagnostics = [];\n\n  for (const event of result.events) {\n    if (event.kind !== 'CompileError') {\n      continue;\n    }\n\n    const detail = event.detail;\n    if (!detail) {\n      continue;\n    }\n\n    const loc =\n      typeof detail.primaryLocation === 'function'\n        ? detail.primaryLocation()\n        : null;\n\n    if (loc == null || typeof loc === 'symbol') {\n      continue;\n    }\n\n    const message =\n      typeof detail.printErrorMessage === 'function'\n        ? detail.printErrorMessage(result.sourceCode, {eslint: true})\n        : detail.description || 'Unknown React Compiler error';\n\n    diagnostics.push({detail, loc, message});\n  }\n\n  return {...result, diagnostics};\n}\n\nmodule.exports = {\n  hasRelevantCode,\n  runReactCompiler,\n};\n"
  },
  {
    "path": "eslint.config.mjs",
    "content": "/* eslint-disable import/no-anonymous-default-export */\nimport md from 'eslint-markdown';\n\nexport default [\n  {\n    ignores: ['**/*.js', '**/*.mjs', '**/*.cjs'],\n  },\n  {\n    ...md.configs.base,\n    files: ['src/content/**/*.md'],\n    rules: {\n      'md/no-curly-quote': [\n        'error',\n        {\n          checkLeftSingleQuotationMark: false,\n          checkRightSingleQuotationMark: false,\n        },\n      ],\n      'md/no-double-space': 'error',\n      'md/no-git-conflict-marker': ['error', {skipCode: false}],\n      'md/no-irregular-whitespace': [\n        'error',\n        {skipCode: false, skipInlineCode: false},\n      ],\n    },\n  },\n];\n"
  },
  {
    "path": "lint-staged.config.js",
    "content": "module.exports = {\n  '*': 'yarn editorconfig-checker',\n  '*.{js,ts,jsx,tsx,css}': 'yarn prettier',\n  'src/**/*.md': ['yarn fix-headings', 'yarn textlint-lint'],\n  'textlint/**/*.js': 'yarn textlint-test',\n  'textlint/data/rules/translateGlossary.js': 'yarn textlint-docs',\n  'textlint/generators/genTranslateGlossaryDocs.js': 'yarn textlint-docs',\n};\n"
  },
  {
    "path": "next-env.d.ts",
    "content": "/// <reference types=\"next\" />\n/// <reference types=\"next/image-types/global\" />\n\n// NOTE: This file should not be edited\n// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.\n"
  },
  {
    "path": "next.config.js",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\n/**\n * @type {import('next').NextConfig}\n **/\nconst nextConfig = {\n  pageExtensions: ['jsx', 'js', 'ts', 'tsx', 'mdx', 'md'],\n  reactStrictMode: true,\n  experimental: {\n    scrollRestoration: true,\n    reactCompiler: true,\n  },\n  async rewrites() {\n    return {\n      beforeFiles: [\n        // Serve markdown when Accept header prefers text/markdown\n        // Useful for LLM agents - https://www.skeptrune.com/posts/use-the-accept-header-to-serve-markdown-instead-of-html-to-llms/\n        {\n          source: '/:path((?!llms.txt).*)',\n          has: [\n            {\n              type: 'header',\n              key: 'accept',\n              value: '(.*text/markdown.*)',\n            },\n          ],\n          destination: '/api/md/:path*',\n        },\n        // Explicit .md extension also serves markdown\n        {\n          source: '/:path*.md',\n          destination: '/api/md/:path*',\n        },\n      ],\n    };\n  },\n  env: {},\n  webpack: (config, {dev, isServer, ...options}) => {\n    if (process.env.ANALYZE) {\n      const {BundleAnalyzerPlugin} = require('webpack-bundle-analyzer');\n      config.plugins.push(\n        new BundleAnalyzerPlugin({\n          analyzerMode: 'static',\n          reportFilename: options.isServer\n            ? '../analyze/server.html'\n            : './analyze/client.html',\n        })\n      );\n    }\n\n    // Don't bundle the shim unnecessarily.\n    config.resolve.alias['use-sync-external-store/shim'] = 'react';\n\n    // ESLint depends on the CommonJS version of esquery,\n    // but Webpack loads the ESM version by default. This\n    // alias ensures the correct version is used.\n    //\n    // More info:\n    // https://github.com/reactjs/react.dev/pull/8115\n    config.resolve.alias['esquery'] = 'esquery/dist/esquery.min.js';\n\n    const {IgnorePlugin, NormalModuleReplacementPlugin} = require('webpack');\n    config.plugins.push(\n      new NormalModuleReplacementPlugin(\n        /^raf$/,\n        require.resolve('./src/utils/rafShim.js')\n      ),\n      new NormalModuleReplacementPlugin(\n        /^process$/,\n        require.resolve('./src/utils/processShim.js')\n      ),\n      new IgnorePlugin({\n        checkResource(resource, context) {\n          if (\n            /\\/eslint\\/lib\\/rules$/.test(context) &&\n            /\\.\\/[\\w-]+(\\.js)?$/.test(resource)\n          ) {\n            // Skips imports of built-in rules that ESLint\n            // tries to carry into the bundle by default.\n            // We only want the engine and the React rules.\n            return true;\n          }\n          return false;\n        },\n      })\n    );\n\n    return config;\n  },\n};\n\nmodule.exports = nextConfig;\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"private\": true,\n  \"license\": \"CC\",\n  \"scripts\": {\n    \"analyze\": \"ANALYZE=true next build\",\n    \"dev\": \"next-remote-watch ./src/content\",\n    \"prebuild:rsc\": \"node scripts/buildRscWorker.mjs\",\n    \"build\": \"yarn cache-reset && node scripts/buildRscWorker.mjs && next build && node --experimental-modules ./scripts/downloadFonts.mjs\",\n    \"lint\": \"next lint && eslint \\\"src/content/**/*.md\\\"\",\n    \"lint:fix\": \"next lint --fix && eslint \\\"src/content/**/*.md\\\" --fix\",\n    \"format:source\": \"prettier --config .prettierrc --write \\\"{plugins,src}/**/*.{js,ts,jsx,tsx,css}\\\"\",\n    \"nit:source\": \"prettier --config .prettierrc --list-different \\\"{plugins,src}/**/*.{js,ts,jsx,tsx,css}\\\"\",\n    \"prettier\": \"yarn format:source\",\n    \"prettier:diff\": \"yarn nit:source\",\n    \"lint-heading-ids\": \"node scripts/headingIdLinter.js\",\n    \"fix-headings\": \"node scripts/headingIdLinter.js --fix\",\n    \"ci-check\": \"npm-run-all prettier:diff --parallel lint tsc lint-heading-ids rss deadlinks lint-editorconfig\",\n    \"tsc\": \"tsc --noEmit\",\n    \"start\": \"next start\",\n    \"postinstall\": \"yarn --cwd eslint-local-rules install && is-ci || husky install .husky\",\n    \"check-all\": \"npm-run-all prettier lint:fix tsc rss\",\n    \"rss\": \"node scripts/generateRss.js\",\n    \"cache-reset\": \"rm -rf node_modules/.cache && rm -rf .next && yarn cache clean\",\n    \"lint-editorconfig\": \"yarn editorconfig-checker\",\n    \"textlint-test\": \"yarn mocha ./textlint/tests/utils && yarn mocha ./textlint/tests/rules\",\n    \"textlint-docs\": \"node ./textlint/generators/genTranslateGlossaryDocs.js && git add wiki/translate-glossary.md\",\n    \"textlint-lint\": \"yarn textlint ./src/content --rulesdir ./textlint/rules -f pretty-error && npx --yes eslint@9 -c eslint.config.mjs\",\n    \"deadlinks\": \"node scripts/deadLinkChecker.js\",\n    \"copyright\": \"node scripts/copyright.js\",\n    \"test:eslint-local-rules\": \"yarn --cwd eslint-local-rules test\"\n  },\n  \"dependencies\": {\n    \"@codesandbox/sandpack-react\": \"2.13.5\",\n    \"@docsearch/css\": \"^3.8.3\",\n    \"@docsearch/react\": \"^3.8.3\",\n    \"@headlessui/react\": \"^1.7.0\",\n    \"@radix-ui/react-context-menu\": \"^2.1.5\",\n    \"body-scroll-lock\": \"^3.1.3\",\n    \"classnames\": \"^2.2.6\",\n    \"debounce\": \"^1.2.1\",\n    \"github-slugger\": \"^1.3.0\",\n    \"next\": \"15.4.10\",\n    \"next-remote-watch\": \"^1.0.0\",\n    \"parse-numeric-range\": \"^1.2.0\",\n    \"raw-loader\": \"^4.0.2\",\n    \"react\": \"^19.0.0\",\n    \"react-collapsed\": \"4.0.4\",\n    \"react-dom\": \"^19.0.0\",\n    \"remark-frontmatter\": \"^4.0.1\",\n    \"remark-gfm\": \"^3.0.1\"\n  },\n  \"devDependencies\": {\n    \"@babel/core\": \"^7.12.9\",\n    \"@babel/plugin-transform-modules-commonjs\": \"^7.18.6\",\n    \"@babel/preset-react\": \"^7.18.6\",\n    \"@mdx-js/mdx\": \"^2.1.3\",\n    \"@types/body-scroll-lock\": \"^2.6.1\",\n    \"@types/classnames\": \"^2.2.10\",\n    \"@types/debounce\": \"^1.2.1\",\n    \"@types/github-slugger\": \"^1.3.0\",\n    \"@types/mdx-js__react\": \"^1.5.2\",\n    \"@types/node\": \"^14.6.4\",\n    \"@types/parse-numeric-range\": \"^0.0.1\",\n    \"@types/react\": \"^19.0.0\",\n    \"@types/react-dom\": \"^19.0.0\",\n    \"@typescript-eslint/eslint-plugin\": \"^5.36.2\",\n    \"@typescript-eslint/parser\": \"^5.36.2\",\n    \"asyncro\": \"^3.0.0\",\n    \"autoprefixer\": \"^10.4.2\",\n    \"babel-eslint\": \"10.x\",\n    \"babel-plugin-react-compiler\": \"19.0.0-beta-e552027-20250112\",\n    \"chalk\": \"4.1.2\",\n    \"editorconfig-checker\": \"^6.0.1\",\n    \"esbuild\": \"^0.24.0\",\n    \"eslint\": \"7.x\",\n    \"eslint-config-next\": \"12.0.3\",\n    \"eslint-config-react-app\": \"^5.2.1\",\n    \"eslint-plugin-flowtype\": \"4.x\",\n    \"eslint-plugin-import\": \"2.x\",\n    \"eslint-plugin-jsx-a11y\": \"6.x\",\n    \"eslint-plugin-local-rules\": \"link:eslint-local-rules\",\n    \"eslint-markdown\": \"^0.5.0\",\n    \"eslint-plugin-react\": \"7.x\",\n    \"eslint-plugin-react-compiler\": \"^19.0.0-beta-e552027-20250112\",\n    \"eslint-plugin-react-hooks\": \"^0.0.0-experimental-fabef7a6b-20221215\",\n    \"fs-extra\": \"^9.0.1\",\n    \"globby\": \"^11.0.1\",\n    \"gray-matter\": \"^4.0.2\",\n    \"husky\": \"^7.0.4\",\n    \"is-ci\": \"^3.0.1\",\n    \"lint-staged\": \"^15.3.0\",\n    \"mdast-util-to-string\": \"^1.1.0\",\n    \"metro-cache\": \"0.72.2\",\n    \"mocha\": \"^10.6.0\",\n    \"npm-run-all\": \"^4.1.5\",\n    \"postcss\": \"^8.4.5\",\n    \"postcss-flexbugs-fixes\": \"4.2.1\",\n    \"postcss-preset-env\": \"^6.7.0\",\n    \"prettier\": \"^2.5.1\",\n    \"react-server-dom-webpack\": \"^19.2.4\",\n    \"reading-time\": \"^1.2.0\",\n    \"remark\": \"^12.0.1\",\n    \"remark-external-links\": \"^7.0.0\",\n    \"remark-html\": \"^12.0.0\",\n    \"remark-images\": \"^2.0.0\",\n    \"remark-slug\": \"^7.0.0\",\n    \"remark-unwrap-images\": \"^2.0.0\",\n    \"retext\": \"^7.0.1\",\n    \"retext-smartypants\": \"^4.0.0\",\n    \"rss\": \"^1.2.2\",\n    \"tailwindcss\": \"^3.4.1\",\n    \"textlint\": \"^14.0.4\",\n    \"textlint-filter-rule-comments\": \"^1.2.2\",\n    \"textlint-rule-allowed-uris\": \"^2.0.0\",\n    \"textlint-tester\": \"^14.0.4\",\n    \"typescript\": \"^5.7.2\",\n    \"unist-util-visit\": \"^2.0.3\",\n    \"webpack-bundle-analyzer\": \"^4.5.0\"\n  },\n  \"engines\": {\n    \"node\": \">=16.8.0\"\n  },\n  \"nextBundleAnalysis\": {\n    \"budget\": null,\n    \"budgetPercentIncreaseRed\": 10,\n    \"showDetails\": true\n  },\n  \"packageManager\": \"yarn@1.22.22\"\n}\n"
  },
  {
    "path": "plugins/markdownToHtml.js",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nconst remark = require('remark');\nconst externalLinks = require('remark-external-links'); // Add _target and rel to external links\nconst customHeaders = require('./remark-header-custom-ids'); // Custom header id's for i18n\nconst images = require('remark-images'); // Improved image syntax\nconst unrwapImages = require('remark-unwrap-images'); // Removes <p> wrapper around images\nconst smartyPants = require('./remark-smartypants'); // Cleans up typography\nconst html = require('remark-html');\n\nmodule.exports = {\n  remarkPlugins: [\n    externalLinks,\n    customHeaders,\n    images,\n    unrwapImages,\n    smartyPants,\n  ],\n  markdownToHtml,\n};\n\nasync function markdownToHtml(markdown) {\n  const result = await remark()\n    .use(externalLinks)\n    .use(customHeaders)\n    .use(images)\n    .use(unrwapImages)\n    .use(smartyPants)\n    .use(html)\n    .process(markdown);\n  return result.toString();\n}\n"
  },
  {
    "path": "plugins/remark-header-custom-ids.js",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*!\n * Based on 'gatsby-remark-autolink-headers'\n * Original Author: Kyle Mathews <mathews.kyle@gmail.com>\n * Updated by Jared Palmer;\n * Copyright (c) 2015 Gatsbyjs\n */\n\nconst toString = require('mdast-util-to-string');\nconst visit = require('unist-util-visit');\nconst toSlug = require('github-slugger').slug;\n\nfunction patch(context, key, value) {\n  if (!context[key]) {\n    context[key] = value;\n  }\n  return context[key];\n}\n\nconst svgIcon = `<svg aria-hidden=\"true\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg>`;\n\nmodule.exports = ({icon = svgIcon, className = `anchor`} = {}) => {\n  return function transformer(tree) {\n    const ids = new Set();\n    visit(tree, 'heading', (node) => {\n      let children = [...node.children];\n      let id;\n      if (children[children.length - 1].type === 'mdxTextExpression') {\n        // # My header {/*my-header*/}\n        id = children.pop().value;\n        const isValidCustomId = id.startsWith('/*') && id.endsWith('*/');\n        if (!isValidCustomId) {\n          throw Error(\n            'Expected header ID to be like: {/*some-header*/}. ' +\n              'Instead, received: ' +\n              id\n          );\n        }\n        id = id.slice(2, id.length - 2);\n        if (id !== toSlug(id)) {\n          throw Error(\n            'Expected header ID to be a valid slug. You specified: {/*' +\n              id +\n              '*/}. Replace it with: {/*' +\n              toSlug(id) +\n              '*/}'\n          );\n        }\n      } else {\n        // # My header\n        id = toSlug(toString(node));\n      }\n\n      if (ids.has(id)) {\n        throw Error(\n          'Cannot have a duplicate header with id \"' +\n            id +\n            '\" on the page. ' +\n            'Rename the section or give it an explicit unique ID. ' +\n            'For example: #### Arguments {/*setstate-arguments*/}'\n        );\n      }\n      ids.add(id);\n\n      const data = patch(node, 'data', {});\n      patch(data, 'id', id);\n      patch(data, 'htmlAttributes', {});\n      patch(data, 'hProperties', {});\n      patch(data.htmlAttributes, 'id', id);\n      patch(data.hProperties, 'id', id);\n    });\n  };\n};\n"
  },
  {
    "path": "plugins/remark-smartypants.js",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*!\n * Based on 'silvenon/remark-smartypants'\n * https://github.com/silvenon/remark-smartypants/pull/80\n */\n\nconst visit = require('unist-util-visit');\nconst retext = require('retext');\nconst smartypants = require('retext-smartypants');\n\nfunction check(node, parent) {\n  if (node.data?.skipSmartyPants) return false;\n  if (parent.tagName === 'script') return false;\n  if (parent.tagName === 'style') return false;\n  return true;\n}\n\nfunction markSkip(node) {\n  if (!node) return;\n  node.data ??= {};\n  node.data.skipSmartyPants = true;\n  if (Array.isArray(node.children)) {\n    for (const child of node.children) {\n      markSkip(child);\n    }\n  }\n}\n\nmodule.exports = function (options) {\n  const processor = retext().use(smartypants, {\n    ...options,\n    // Do not replace ellipses, dashes, backticks because they change string\n    // length, and we couldn't guarantee right splice of text in second visit of\n    // tree\n    ellipses: false,\n    dashes: false,\n    backticks: false,\n  });\n\n  const processor2 = retext().use(smartypants, {\n    ...options,\n    // Do not replace quotes because they are already replaced in the first\n    // processor\n    quotes: false,\n  });\n\n  function transformer(tree) {\n    let allText = '';\n    let startIndex = 0;\n    const textOrInlineCodeNodes = [];\n\n    visit(tree, 'mdxJsxFlowElement', (node) => {\n      if (['TerminalBlock'].includes(node.name)) {\n        markSkip(node); // Mark all children to skip smarty pants\n      }\n    });\n\n    visit(tree, ['text', 'inlineCode'], (node, _, parent) => {\n      if (check(node, parent)) {\n        if (node.type === 'text') allText += node.value;\n        // for the case when inlineCode contains just one part of quote: `foo'bar`\n        else allText += 'A'.repeat(node.value.length);\n        textOrInlineCodeNodes.push(node);\n      }\n    });\n\n    // Concat all text into one string, to properly replace quotes around non-\"text\" nodes\n    allText = String(processor.processSync(allText));\n\n    for (const node of textOrInlineCodeNodes) {\n      const endIndex = startIndex + node.value.length;\n      if (node.type === 'text') {\n        const processedText = allText.slice(startIndex, endIndex);\n        node.value = String(processor2.processSync(processedText));\n      }\n      startIndex = endIndex;\n    }\n  }\n\n  return transformer;\n};\n"
  },
  {
    "path": "postcss.config.js",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nmodule.exports = {\n  plugins: {\n    tailwindcss: {},\n    autoprefixer: {},\n    'postcss-flexbugs-fixes': {},\n    'postcss-preset-env': {\n      autoprefixer: {\n        flexbox: 'no-2009',\n      },\n      stage: 3,\n      features: {\n        'custom-properties': false,\n      },\n    },\n  },\n};\n"
  },
  {
    "path": "public/.well-known/atproto-did",
    "content": "did:plc:uorpbnp2q32vuvyeruwauyhe\n"
  },
  {
    "path": "public/browserconfig.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<browserconfig>\n    <msapplication>\n        <tile>\n            <square150x150logo src=\"/mstile-150x150.png\"/>\n            <TileColor>#2b5797</TileColor>\n        </tile>\n    </msapplication>\n</browserconfig>\n"
  },
  {
    "path": "public/html/single-file-example.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title>Hello World</title>\n    <script src=\"https://unpkg.com/react@18/umd/react.development.js\"></script>\n    <script src=\"https://unpkg.com/react-dom@18/umd/react-dom.development.js\"></script>\n\n    <!-- Don't use this in production—do this: https://reactjs.org/docs/add-react-to-a-website#add-jsx-to-a-project -->\n    <script src=\"https://unpkg.com/babel-standalone@6/babel.min.js\"></script>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n    <script type=\"text/babel\">\n\n      ReactDOM.render(\n        <h1>Hello, world!</h1>,\n        document.getElementById('root')\n      );\n\n    </script>\n    <!--\n      Note: this page is a great way to try React but it's not suitable for production.\n      It slowly compiles JSX with Babel in the browser and uses a large development build of React.\n\n      Read this section for a production-ready setup with JSX:\n      https://reactjs.org/docs/docs/add-react-to-a-website#try-react-with-jsx\n\n      In a larger project, you can use an integrated toolchain that includes JSX instead:\n      https://reactjs.org/docs/start-a-new-react-project\n\n      You can also use React without JSX, in which case you can remove Babel:\n      https://reactjs.org/docs/add-react-to-a-website#add-react-in-one-minute\n    -->\n  </body>\n</html>\n"
  },
  {
    "path": "public/js/jsfiddle-integration-babel.js",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n// Do not delete or move this file.\n// Many fiddles reference it so we have to keep it here.\n(function () {\n  var tag = document.querySelector(\n    'script[type=\"application/javascript;version=1.7\"]'\n  );\n  if (!tag || tag.textContent.indexOf('window.onload=function(){') !== -1) {\n    alert(\n      'Bad JSFiddle configuration, please fork the original React JSFiddle'\n    );\n  }\n  tag.setAttribute('type', 'text/babel');\n  tag.textContent = tag.textContent.replace(/^\\/\\/<!\\[CDATA\\[/, '');\n})();\n"
  },
  {
    "path": "public/js/jsfiddle-integration.js",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n// Do not delete or move this file.\n// Many fiddles reference it so we have to keep it here.\n(function () {\n  var tag = document.querySelector(\n    'script[type=\"application/javascript;version=1.7\"]'\n  );\n  if (!tag || tag.textContent.indexOf('window.onload=function(){') !== -1) {\n    alert(\n      'Bad JSFiddle configuration, please fork the original React JSFiddle'\n    );\n  }\n  tag.setAttribute('type', 'text/jsx;harmony=true');\n  tag.textContent = tag.textContent.replace(/^\\/\\/<!\\[CDATA\\[/, '');\n})();\n"
  },
  {
    "path": "public/robots.txt",
    "content": "User-agent: *\nDisallow:\n"
  },
  {
    "path": "public/site.webmanifest",
    "content": "{\n    \"name\": \"React\",\n    \"short_name\": \"React\",\n    \"icons\": [\n        {\n            \"src\": \"/android-chrome-192x192.png\",\n            \"sizes\": \"192x192\",\n            \"type\": \"image/png\"\n        },\n        {\n            \"src\": \"/android-chrome-384x384.png\",\n            \"sizes\": \"384x384\",\n            \"type\": \"image/png\"\n        }\n    ],\n    \"theme_color\": \"#23272f\",\n    \"background_color\": \"#23272f\",\n    \"display\": \"standalone\"\n}\n"
  },
  {
    "path": "scripts/buildRscWorker.mjs",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nimport * as esbuild from 'esbuild';\nimport fs from 'fs';\nimport path from 'path';\nimport {fileURLToPath} from 'url';\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\nconst root = path.resolve(__dirname, '..');\nconst sandboxBase = path.resolve(\n  root,\n  'src/components/MDX/Sandpack/sandpack-rsc/sandbox-code/src'\n);\n\n// 1. Bundle the server Worker runtime (React server build + RSDW/server.browser + Sucrase → IIFE)\n// Minified because this runs inside a Web Worker (not parsed by Sandpack's Babel).\nconst workerOutfile = path.resolve(sandboxBase, 'worker-bundle.dist.js');\nawait esbuild.build({\n  entryPoints: [path.resolve(sandboxBase, 'rsc-server.js')],\n  bundle: true,\n  format: 'iife',\n  platform: 'browser',\n  conditions: ['react-server', 'browser'],\n  outfile: workerOutfile,\n  define: {'process.env.NODE_ENV': '\"production\"'},\n  minify: true,\n});\n\n// Post-process worker bundle:\n// Prepend the webpack shim so __webpack_require__ (used by react-server-dom-webpack)\n// is defined before the IIFE evaluates. The shim sets globalThis.__webpack_require__,\n// which is accessible as a bare identifier since globalThis IS the Worker's global scope.\nlet workerCode = fs.readFileSync(workerOutfile, 'utf8');\n\nconst shimPath = path.resolve(sandboxBase, 'webpack-shim.js');\nconst shimCode = fs.readFileSync(shimPath, 'utf8');\nworkerCode = shimCode + '\\n' + workerCode;\n\nfs.writeFileSync(workerOutfile, workerCode);\n"
  },
  {
    "path": "scripts/copyright.js",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n'use strict';\n\nconst fs = require('fs');\nconst glob = require('glob');\n\nconst META_COPYRIGHT_COMMENT_BLOCK =\n  `/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */`.trim() + '\\n\\n';\n\nconst files = glob.sync('**/*.{js,ts,tsx,jsx,rs}', {\n  ignore: [\n    '**/dist/**',\n    '**/node_modules/**',\n    '**/tests/fixtures/**',\n    '**/__tests__/fixtures/**',\n  ],\n});\n\nconst updatedFiles = new Map();\nlet hasErrors = false;\nfiles.forEach((file) => {\n  try {\n    const result = processFile(file);\n    if (result != null) {\n      updatedFiles.set(file, result);\n    }\n  } catch (e) {\n    console.error(e);\n    hasErrors = true;\n  }\n});\nif (hasErrors) {\n  console.error('Update failed');\n  process.exit(1);\n} else {\n  for (const [file, source] of updatedFiles) {\n    fs.writeFileSync(file, source, 'utf8');\n  }\n  console.log('Update complete');\n}\n\nfunction processFile(file) {\n  if (fs.lstatSync(file).isDirectory()) {\n    return;\n  }\n  let source = fs.readFileSync(file, 'utf8');\n  let shebang = '';\n\n  if (source.startsWith('#!')) {\n    const newlineIndex = source.indexOf('\\n');\n    if (newlineIndex === -1) {\n      shebang = `${source}\\n`;\n      source = '';\n    } else {\n      shebang = source.slice(0, newlineIndex + 1);\n      source = source.slice(newlineIndex + 1);\n    }\n  }\n\n  if (source.indexOf(META_COPYRIGHT_COMMENT_BLOCK) === 0) {\n    return null;\n  }\n  if (/^\\/\\*\\*/.test(source)) {\n    source = source.replace(/\\/\\*\\*[^\\/]+\\/\\s+/, META_COPYRIGHT_COMMENT_BLOCK);\n  } else {\n    source = `${META_COPYRIGHT_COMMENT_BLOCK}${source}`;\n  }\n\n  if (shebang) {\n    return `${shebang}${source}`;\n  }\n  return source;\n}\n"
  },
  {
    "path": "scripts/deadLinkChecker.js",
    "content": "#!/usr/bin/env node\n/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nconst fs = require('fs');\nconst path = require('path');\nconst globby = require('globby');\nconst chalk = require('chalk');\n\nconst CONTENT_DIR = path.join(__dirname, '../src/content');\nconst PUBLIC_DIR = path.join(__dirname, '../public');\nconst fileCache = new Map();\nconst anchorMap = new Map(); // Map<filepath, Set<anchorId>>\nconst contributorMap = new Map(); // Map<anchorId, URL>\nconst redirectMap = new Map(); // Map<source, destination>\nlet errorCodes = new Set();\n\nasync function readFileWithCache(filePath) {\n  if (!fileCache.has(filePath)) {\n    try {\n      const content = await fs.promises.readFile(filePath, 'utf8');\n      fileCache.set(filePath, content);\n    } catch (error) {\n      throw new Error(`Failed to read file ${filePath}: ${error.message}`);\n    }\n  }\n  return fileCache.get(filePath);\n}\n\nasync function fileExists(filePath) {\n  try {\n    await fs.promises.access(filePath, fs.constants.R_OK);\n    return true;\n  } catch {\n    return false;\n  }\n}\n\nfunction getMarkdownFiles() {\n  // Convert Windows paths to POSIX for globby compatibility\n  const baseDir = CONTENT_DIR.replace(/\\\\/g, '/');\n  const patterns = [\n    path.posix.join(baseDir, '**/*.md'),\n    path.posix.join(baseDir, '**/*.mdx'),\n  ];\n  return globby.sync(patterns);\n}\n\nfunction extractAnchorsFromContent(content) {\n  const anchors = new Set();\n\n  // MDX-style heading IDs: {/*anchor-id*/}\n  const mdxPattern = /\\{\\/\\*([a-zA-Z0-9-_]+)\\*\\/\\}/g;\n  let match;\n  while ((match = mdxPattern.exec(content)) !== null) {\n    anchors.add(match[1].toLowerCase());\n  }\n\n  // HTML id attributes\n  const htmlIdPattern = /\\sid=[\"']([a-zA-Z0-9-_]+)[\"']/g;\n  while ((match = htmlIdPattern.exec(content)) !== null) {\n    anchors.add(match[1].toLowerCase());\n  }\n\n  // Markdown heading with explicit ID: ## Heading {#anchor-id}\n  const markdownHeadingPattern = /^#+\\s+.*\\{#([a-zA-Z0-9-_]+)\\}/gm;\n  while ((match = markdownHeadingPattern.exec(content)) !== null) {\n    anchors.add(match[1].toLowerCase());\n  }\n\n  return anchors;\n}\n\nasync function buildAnchorMap(files) {\n  for (const filePath of files) {\n    const content = await readFileWithCache(filePath);\n    const anchors = extractAnchorsFromContent(content);\n    if (anchors.size > 0) {\n      anchorMap.set(filePath, anchors);\n    }\n  }\n}\n\nfunction extractLinksFromContent(content) {\n  const linkPattern = /\\[([^\\]]*)\\]\\(([^)]+)\\)/g;\n  const links = [];\n  let match;\n\n  while ((match = linkPattern.exec(content)) !== null) {\n    const [, linkText, linkUrl] = match;\n    if (linkUrl.startsWith('/') && !linkUrl.startsWith('//')) {\n      const lines = content.substring(0, match.index).split('\\n');\n      const line = lines.length;\n      const lastLineStart =\n        lines.length > 1 ? content.lastIndexOf('\\n', match.index - 1) + 1 : 0;\n      const column = match.index - lastLineStart + 1;\n\n      links.push({\n        text: linkText,\n        url: linkUrl,\n        line,\n        column,\n      });\n    }\n  }\n\n  return links;\n}\n\nasync function findTargetFile(urlPath) {\n  // Check if it's an image or static asset that might be in the public directory\n  const imageExtensions = [\n    '.png',\n    '.jpg',\n    '.jpeg',\n    '.gif',\n    '.svg',\n    '.ico',\n    '.webp',\n  ];\n  const hasImageExtension = imageExtensions.some((ext) =>\n    urlPath.toLowerCase().endsWith(ext)\n  );\n\n  if (hasImageExtension || urlPath.includes('.')) {\n    // Check in public directory (with and without leading slash)\n    const publicPaths = [\n      path.join(PUBLIC_DIR, urlPath),\n      path.join(PUBLIC_DIR, urlPath.substring(1)),\n    ];\n\n    for (const p of publicPaths) {\n      if (await fileExists(p)) {\n        return p;\n      }\n    }\n  }\n\n  const possiblePaths = [\n    path.join(CONTENT_DIR, urlPath + '.md'),\n    path.join(CONTENT_DIR, urlPath + '.mdx'),\n    path.join(CONTENT_DIR, urlPath, 'index.md'),\n    path.join(CONTENT_DIR, urlPath, 'index.mdx'),\n    // Without leading slash\n    path.join(CONTENT_DIR, urlPath.substring(1) + '.md'),\n    path.join(CONTENT_DIR, urlPath.substring(1) + '.mdx'),\n    path.join(CONTENT_DIR, urlPath.substring(1), 'index.md'),\n    path.join(CONTENT_DIR, urlPath.substring(1), 'index.mdx'),\n  ];\n\n  for (const p of possiblePaths) {\n    if (await fileExists(p)) {\n      return p;\n    }\n  }\n  return null;\n}\n\nasync function validateLink(link) {\n  const urlAnchorPattern = /#([a-zA-Z0-9-_]+)$/;\n  const anchorMatch = link.url.match(urlAnchorPattern);\n  const urlWithoutAnchor = link.url.replace(urlAnchorPattern, '');\n\n  if (urlWithoutAnchor === '/') {\n    return {valid: true};\n  }\n\n  // Check for redirects\n  if (redirectMap.has(urlWithoutAnchor)) {\n    const redirectDestination = redirectMap.get(urlWithoutAnchor);\n    if (\n      redirectDestination.startsWith('http://') ||\n      redirectDestination.startsWith('https://')\n    ) {\n      return {valid: true};\n    }\n    const redirectedLink = {\n      ...link,\n      url: redirectDestination + (anchorMatch ? anchorMatch[0] : ''),\n    };\n    return validateLink(redirectedLink);\n  }\n\n  // Check if it's an error code link\n  const errorCodeMatch = urlWithoutAnchor.match(/^\\/errors\\/(\\d+)$/);\n  if (errorCodeMatch) {\n    const code = errorCodeMatch[1];\n    if (!errorCodes.has(code)) {\n      return {\n        valid: false,\n        reason: `Error code ${code} not found in React error codes`,\n      };\n    }\n    return {valid: true};\n  }\n\n  // Check if it's a contributor link on the team or acknowledgements page\n  if (\n    anchorMatch &&\n    (urlWithoutAnchor === '/community/team' ||\n      urlWithoutAnchor === '/community/acknowledgements')\n  ) {\n    const anchorId = anchorMatch[1].toLowerCase();\n    if (contributorMap.has(anchorId)) {\n      const correctUrl = contributorMap.get(anchorId);\n      if (correctUrl !== link.url) {\n        return {\n          valid: false,\n          reason: `Contributor link should be updated to: ${correctUrl}`,\n        };\n      }\n      return {valid: true};\n    } else {\n      return {\n        valid: false,\n        reason: `Contributor link not found`,\n      };\n    }\n  }\n\n  const targetFile = await findTargetFile(urlWithoutAnchor);\n\n  if (!targetFile) {\n    return {\n      valid: false,\n      reason: `Target file not found for: ${urlWithoutAnchor}`,\n    };\n  }\n\n  // Only check anchors for content files, not static assets\n  if (anchorMatch && targetFile.startsWith(CONTENT_DIR)) {\n    const anchorId = anchorMatch[1].toLowerCase();\n\n    // TODO handle more special cases. These are usually from custom MDX components that include\n    // a Heading from src/components/MDX/Heading.tsx which automatically injects an anchor tag.\n    switch (anchorId) {\n      case 'challenges':\n      case 'recap': {\n        return {valid: true};\n      }\n    }\n\n    const fileAnchors = anchorMap.get(targetFile);\n\n    if (!fileAnchors || !fileAnchors.has(anchorId)) {\n      return {\n        valid: false,\n        reason: `Anchor #${anchorMatch[1]} not found in ${path.relative(\n          CONTENT_DIR,\n          targetFile\n        )}`,\n      };\n    }\n  }\n\n  return {valid: true};\n}\n\nasync function processFile(filePath) {\n  const content = await readFileWithCache(filePath);\n  const links = extractLinksFromContent(content);\n  const deadLinks = [];\n\n  for (const link of links) {\n    const result = await validateLink(link);\n    if (!result.valid) {\n      deadLinks.push({\n        file: path.relative(process.cwd(), filePath),\n        line: link.line,\n        column: link.column,\n        text: link.text,\n        url: link.url,\n        reason: result.reason,\n      });\n    }\n  }\n\n  return {deadLinks, totalLinks: links.length};\n}\n\nasync function buildContributorMap() {\n  const teamFile = path.join(CONTENT_DIR, 'community/team.md');\n  const teamContent = await readFileWithCache(teamFile);\n\n  const teamMemberPattern = /<TeamMember[^>]*permalink=[\"']([^\"']+)[\"']/g;\n  let match;\n\n  while ((match = teamMemberPattern.exec(teamContent)) !== null) {\n    const permalink = match[1];\n    contributorMap.set(permalink, `/community/team#${permalink}`);\n  }\n\n  const ackFile = path.join(CONTENT_DIR, 'community/acknowledgements.md');\n  const ackContent = await readFileWithCache(ackFile);\n  const contributorPattern = /\\*\\s*\\[([^\\]]+)\\]\\(([^)]+)\\)/g;\n\n  while ((match = contributorPattern.exec(ackContent)) !== null) {\n    const name = match[1];\n    const url = match[2];\n    const hyphenatedName = name.toLowerCase().replace(/\\s+/g, '-');\n    if (!contributorMap.has(hyphenatedName)) {\n      contributorMap.set(hyphenatedName, url);\n    }\n  }\n}\n\nasync function fetchErrorCodes() {\n  try {\n    const response = await fetch(\n      'https://raw.githubusercontent.com/facebook/react/main/scripts/error-codes/codes.json'\n    );\n    if (!response.ok) {\n      throw new Error(`Failed to fetch error codes: ${response.status}`);\n    }\n    const codes = await response.json();\n    errorCodes = new Set(Object.keys(codes));\n    console.log(chalk.gray(`Fetched ${errorCodes.size} React error codes`));\n  } catch (error) {\n    throw new Error(`Failed to fetch error codes: ${error.message}`);\n  }\n}\n\nasync function buildRedirectsMap() {\n  try {\n    const vercelConfigPath = path.join(__dirname, '../vercel.json');\n    const vercelConfig = JSON.parse(\n      await fs.promises.readFile(vercelConfigPath, 'utf8')\n    );\n\n    if (vercelConfig.redirects) {\n      for (const redirect of vercelConfig.redirects) {\n        redirectMap.set(redirect.source, redirect.destination);\n      }\n      console.log(\n        chalk.gray(`Loaded ${redirectMap.size} redirects from vercel.json`)\n      );\n    }\n  } catch (error) {\n    console.log(\n      chalk.yellow(\n        `Warning: Could not load redirects from vercel.json: ${error.message}\\n`\n      )\n    );\n  }\n}\n\nasync function main() {\n  const files = getMarkdownFiles();\n  console.log(chalk.gray(`Checking ${files.length} markdown files...`));\n\n  await fetchErrorCodes();\n  await buildRedirectsMap();\n  await buildContributorMap();\n  await buildAnchorMap(files);\n\n  const filePromises = files.map((filePath) => processFile(filePath));\n  const results = await Promise.all(filePromises);\n  const deadLinks = results.flatMap((r) => r.deadLinks);\n  const totalLinks = results.reduce((sum, r) => sum + r.totalLinks, 0);\n\n  if (deadLinks.length > 0) {\n    console.log('\\n');\n    for (const link of deadLinks) {\n      console.log(chalk.yellow(`${link.file}:${link.line}:${link.column}`));\n      console.log(chalk.reset(`  Link text: ${link.text}`));\n      console.log(chalk.reset(`  URL: ${link.url}`));\n      console.log(`  ${chalk.red('✗')} ${chalk.red(link.reason)}\\n`);\n    }\n\n    console.log(\n      chalk.red(\n        `\\nFound ${deadLinks.length} dead link${\n          deadLinks.length > 1 ? 's' : ''\n        } out of ${totalLinks} total links\\n`\n      )\n    );\n    process.exit(1);\n  }\n\n  console.log(chalk.green(`\\n✓ All ${totalLinks} links are valid!\\n`));\n  process.exit(0);\n}\n\nmain().catch((error) => {\n  console.log(chalk.red(`Error: ${error.message}`));\n  process.exit(1);\n});\n"
  },
  {
    "path": "scripts/downloadFonts.mjs",
    "content": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport { exec } from 'child_process';\nimport { mkdir, promises as fsPromises } from 'fs';\nimport { dirname } from 'path';\nimport { promisify } from 'util';\n\nconst execAsync = promisify(exec);\n\n// Taken from Downloads on https://www.facebook.com/brand/meta/typography/.\n// To refresh the list, go to the Conf website's public/fonts/ folder and run this:\n// printf \"\\n[\\n%s\\n]\\n\" \"$(find . -type f ! -path \"*/.*\" -name \"*.woff2\" | sed 's|^./||' | sort | awk '{printf \"  \\\"%s\\\",\\n\", $0}' | sed '$s/,$//')\"\nconst paths = [\n  \"Optimistic_Display_Arbc_W_Bd.woff2\",\n  \"Optimistic_Display_Arbc_W_Md.woff2\",\n  \"Optimistic_Display_Arbc_W_SBd.woff2\",\n  \"Optimistic_Display_Cyrl_W_Bd.woff2\",\n  \"Optimistic_Display_Cyrl_W_Md.woff2\",\n  \"Optimistic_Display_Cyrl_W_SBd.woff2\",\n  \"Optimistic_Display_Deva_W_Bd.woff2\",\n  \"Optimistic_Display_Deva_W_Md.woff2\",\n  \"Optimistic_Display_Deva_W_SBd.woff2\",\n  \"Optimistic_Display_Viet_W_Bd.woff2\",\n  \"Optimistic_Display_Viet_W_Md.woff2\",\n  \"Optimistic_Display_Viet_W_SBd.woff2\",\n  \"Optimistic_Display_W_Bd.woff2\",\n  \"Optimistic_Display_W_BdIt.woff2\",\n  \"Optimistic_Display_W_Lt.woff2\",\n  \"Optimistic_Display_W_Md.woff2\",\n  \"Optimistic_Display_W_MdIt.woff2\",\n  \"Optimistic_Display_W_SBd.woff2\",\n  \"Optimistic_Display_W_SBdIt.woff2\",\n  \"Optimistic_Display_W_XBd.woff2\",\n  \"Optimistic_Display_W_XLt.woff2\",\n  \"Optimistic_Text_Arbc_W_Bd.woff2\",\n  \"Optimistic_Text_Arbc_W_Md.woff2\",\n  \"Optimistic_Text_Arbc_W_Rg.woff2\",\n  \"Optimistic_Text_Arbc_W_XBd.woff2\",\n  \"Optimistic_Text_Cyrl_W_Bd.woff2\",\n  \"Optimistic_Text_Cyrl_W_Md.woff2\",\n  \"Optimistic_Text_Cyrl_W_Rg.woff2\",\n  \"Optimistic_Text_Cyrl_W_XBd.woff2\",\n  \"Optimistic_Text_Deva_W_Bd.woff2\",\n  \"Optimistic_Text_Deva_W_Md.woff2\",\n  \"Optimistic_Text_Deva_W_Rg.woff2\",\n  \"Optimistic_Text_Deva_W_XBd.woff2\",\n  \"Optimistic_Text_Viet_W_Bd.woff2\",\n  \"Optimistic_Text_Viet_W_Md.woff2\",\n  \"Optimistic_Text_Viet_W_Rg.woff2\",\n  \"Optimistic_Text_Viet_W_XBd.woff2\",\n  \"Optimistic_Text_W_Bd.woff2\",\n  \"Optimistic_Text_W_BdIt.woff2\",\n  \"Optimistic_Text_W_It.woff2\",\n  \"Optimistic_Text_W_Md.woff2\",\n  \"Optimistic_Text_W_MdIt.woff2\",\n  \"Optimistic_Text_W_Rg.woff2\",\n  \"Optimistic_Text_W_XBd.woff2\",\n  \"Optimistic_Text_W_XBdIt.woff2\"\n];\n\nconst baseURL = \"https://conf.reactjs.org/fonts/\";\nconst outputDir = \"public/fonts/\";\n\nawait Promise.all(\n  paths.map(async (path) => {\n    const localPath = `${outputDir}${path}`;\n    const localDir = dirname(localPath);\n    await fsPromises.mkdir(localDir, { recursive: true });\n\n    const command = `curl ${baseURL}${path} --output ${localPath}`;\n    await execAsync(command);\n    console.log(`Downloaded ${path}`);\n  })\n);\n\nconsole.log(\"All fonts downloaded.\");\n"
  },
  {
    "path": "scripts/generateRss.js",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\nconst {generateRssFeed} = require('../src/utils/rss');\n\ngenerateRssFeed();\n"
  },
  {
    "path": "scripts/headingIDHelpers/generateHeadingIDs.js",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n// To do: Make this ESM.\n// To do: properly check heading numbers (headings with the same text get\n// numbered, this script doesn’t check that).\n\nconst assert = require('assert');\nconst fs = require('fs');\nconst GithubSlugger = require('github-slugger');\nconst walk = require('./walk');\n\nlet modules;\n\nfunction stripLinks(line) {\n  return line.replace(/\\[([^\\]]+)\\]\\([^)]+\\)/, (match, p1) => p1);\n}\n\nfunction addHeaderID(line, slugger) {\n  // check if we're a header at all\n  if (!line.startsWith('#')) {\n    return line;\n  }\n\n  const match =\n    /^(#+\\s+)(.+?)(\\s*\\{(?:\\/\\*|#)([^\\}\\*\\/]+)(?:\\*\\/)?\\}\\s*)?$/.exec(line);\n  const before = match[1] + match[2];\n  const proc = modules\n    .unified()\n    .use(modules.remarkParse)\n    .use(modules.remarkSlug);\n  const tree = proc.runSync(proc.parse(before));\n  const head = tree.children[0];\n  assert(\n    head && head.type === 'heading',\n    'expected `' +\n      before +\n      '` to be a heading, is it using a normal space after `#`?'\n  );\n  const autoId = head.data.id;\n  const existingId = match[4];\n  const id = existingId || autoId;\n  // Ignore numbers:\n  const cleanExisting = existingId\n    ? existingId.replace(/-\\d+$/, '')\n    : undefined;\n  const cleanAuto = autoId.replace(/-\\d+$/, '');\n\n  if (cleanExisting && cleanExisting !== cleanAuto) {\n    console.log(\n      'Note: heading `%s` has a different ID (`%s`) than what GH generates for it: `%s`:',\n      before,\n      existingId,\n      autoId\n    );\n  }\n\n  return match[1] + match[2] + ' {/*' + id + '*/}';\n}\n\nfunction addHeaderIDs(lines) {\n  // Sluggers should be per file\n  const slugger = new GithubSlugger();\n  let inCode = false;\n  const results = [];\n  lines.forEach((line) => {\n    // Ignore code blocks\n    if (line.startsWith('```')) {\n      inCode = !inCode;\n      results.push(line);\n      return;\n    }\n    if (inCode) {\n      results.push(line);\n      return;\n    }\n\n    results.push(addHeaderID(line, slugger));\n  });\n  return results;\n}\n\nasync function main(paths) {\n  paths = paths.length === 0 ? ['src/content'] : paths;\n\n  const [unifiedMod, remarkParseMod, remarkSlugMod] = await Promise.all([\n    import('unified'),\n    import('remark-parse'),\n    import('remark-slug'),\n  ]);\n  const unified = unifiedMod.unified;\n  const remarkParse = remarkParseMod.default;\n  const remarkSlug = remarkSlugMod.default;\n  modules = {unified, remarkParse, remarkSlug};\n  const files = paths.map((path) => [...walk(path)]).flat();\n\n  files.forEach((file) => {\n    if (!(file.endsWith('.md') || file.endsWith('.mdx'))) {\n      return;\n    }\n\n    const content = fs.readFileSync(file, 'utf8');\n    const lines = content.split('\\n');\n    const updatedLines = addHeaderIDs(lines);\n    fs.writeFileSync(file, updatedLines.join('\\n'));\n  });\n}\n\nmodule.exports = main;\n"
  },
  {
    "path": "scripts/headingIDHelpers/validateHeadingIDs.js",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nconst fs = require('fs');\nconst walk = require('./walk');\n\n/**\n * Validate if there is a custom heading id and exit if there isn't a heading\n * @param {string} line\n * @returns\n */\nfunction validateHeaderId(line) {\n  if (!line.startsWith('#')) {\n    return;\n  }\n\n  const match = /\\{\\/\\*(.*?)\\*\\/}/.exec(line);\n  const id = match;\n  if (!id) {\n    console.error('Run yarn fix-headings to generate headings.');\n    process.exit(1);\n  }\n}\n\n/**\n * Loops through the lines to skip code blocks\n * @param {Array<string>} lines\n */\nfunction validateHeaderIds(lines) {\n  let inCode = false;\n  const results = [];\n  lines.forEach((line) => {\n    // Ignore code blocks\n    if (line.startsWith('```')) {\n      inCode = !inCode;\n\n      results.push(line);\n      return;\n    }\n    if (inCode) {\n      results.push(line);\n      return;\n    }\n    validateHeaderId(line);\n  });\n}\n/**\n * paths are basically array of path for which we have to validate heading IDs\n * @param {Array<string>} paths\n */\nasync function main(paths) {\n  paths = paths.length === 0 ? ['src/content'] : paths;\n  const files = paths.map((path) => [...walk(path)]).flat();\n\n  files.forEach((file) => {\n    if (!(file.endsWith('.md') || file.endsWith('.mdx'))) {\n      return;\n    }\n\n    const content = fs.readFileSync(file, 'utf8');\n    const lines = content.split('\\n');\n    validateHeaderIds(lines);\n  });\n}\n\nmodule.exports = main;\n"
  },
  {
    "path": "scripts/headingIDHelpers/walk.js",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nconst fs = require('fs');\n\nmodule.exports = function walk(dir) {\n  let results = [];\n  /**\n   * If the param is a directory we can return the file\n   */\n  if (dir.includes('md')) {\n    return [dir];\n  }\n  const list = fs.readdirSync(dir);\n  list.forEach(function (file) {\n    file = dir + '/' + file;\n    const stat = fs.statSync(file);\n    if (stat && stat.isDirectory()) {\n      /* Recurse into a subdirectory */\n      results = results.concat(walk(file));\n    } else {\n      /* Is a file */\n      results.push(file);\n    }\n  });\n  return results;\n};\n"
  },
  {
    "path": "scripts/headingIdLinter.js",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nconst validateHeaderIds = require('./headingIDHelpers/validateHeadingIDs');\nconst generateHeadingIds = require('./headingIDHelpers/generateHeadingIDs');\n\n/**\n * yarn lint-heading-ids --> Checks all files and causes an error if heading ID is missing\n * yarn lint-heading-ids --fix --> Fixes all markdown file's heading IDs\n * yarn lint-heading-ids path/to/markdown.md --> Checks that particular file for missing heading ID (path can denote a directory or particular file)\n * yarn lint-heading-ids --fix path/to/markdown.md --> Fixes that particular file's markdown IDs (path can denote a directory or particular file)\n */\n\nconst markdownPaths = process.argv.slice(2);\nif (markdownPaths.includes('--fix')) {\n  generateHeadingIds(markdownPaths.filter((path) => path !== '--fix'));\n} else {\n  validateHeaderIds(markdownPaths);\n}\n"
  },
  {
    "path": "src/components/Breadcrumbs.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport {Fragment} from 'react';\nimport Link from 'next/link';\nimport type {RouteItem} from 'components/Layout/getRouteMeta';\n\nfunction Breadcrumbs({breadcrumbs}: {breadcrumbs: RouteItem[]}) {\n  return (\n    <div className=\"flex flex-wrap\">\n      {breadcrumbs.map(\n        (crumb, i) =>\n          crumb.path &&\n          !crumb.skipBreadcrumb && (\n            <div className=\"flex mb-3 mt-0.5 items-center\" key={i}>\n              <Fragment key={crumb.path}>\n                <Link\n                  href={crumb.path}\n                  className=\"text-link dark:text-link-dark text-sm tracking-wide font-bold uppercase me-1 hover:underline\">\n                  {crumb.title}\n                </Link>\n                <span className=\"inline-block me-1 text-link dark:text-link-dark text-lg rtl:rotate-180\">\n                  <svg\n                    width=\"20\"\n                    height=\"20\"\n                    viewBox=\"0 0 20 20\"\n                    fill=\"none\"\n                    xmlns=\"http://www.w3.org/2000/svg\">\n                    <path\n                      d=\"M6.86612 13.6161C6.37796 14.1043 6.37796 14.8957 6.86612 15.3839C7.35427 15.872 8.14572 15.872 8.63388 15.3839L13.1339 10.8839C13.622 10.3957 13.622 9.60428 13.1339 9.11612L8.63388 4.61612C8.14572 4.12797 7.35427 4.12797 6.86612 4.61612C6.37796 5.10428 6.37796 5.89573 6.86612 6.38388L10.4822 10L6.86612 13.6161Z\"\n                      fill=\"currentColor\"\n                    />\n                  </svg>\n                </span>\n              </Fragment>\n            </div>\n          )\n      )}\n    </div>\n  );\n}\n\nexport default Breadcrumbs;\n"
  },
  {
    "path": "src/components/Button.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport * as React from 'react';\nimport cn from 'classnames';\n\ninterface ButtonProps {\n  children: React.ReactNode;\n  onClick?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;\n  active?: boolean;\n  className?: string;\n  style?: Record<string, string>;\n}\n\nexport function Button({\n  children,\n  onClick,\n  active = false,\n  className,\n  style,\n}: ButtonProps) {\n  return (\n    <button\n      style={style}\n      onMouseDown={(evt) => {\n        evt.preventDefault();\n        evt.stopPropagation();\n      }}\n      onClick={onClick}\n      className={cn(\n        className,\n        'text-base leading-tight font-bold rounded-full py-2 px-4 focus:outline focus:outline-offset-2 focus:outline-link dark:focus:outline-link-dark inline-flex items-center my-1',\n        {\n          'bg-link border-link text-white hover:bg-link focus:bg-link active:bg-link':\n            active,\n          'bg-transparent text-primary dark:text-primary-dark active:text-primary shadow-secondary-button-stroke dark:shadow-secondary-button-stroke-dark hover:bg-gray-40/5 active:bg-gray-40/10  hover:dark:bg-gray-60/5 active:dark:bg-gray-60/10':\n            !active,\n        }\n      )}>\n      {children}\n    </button>\n  );\n}\n\nexport default Button;\n"
  },
  {
    "path": "src/components/ButtonLink.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport cn from 'classnames';\nimport NextLink from 'next/link';\n\ninterface ButtonLinkProps {\n  size?: 'md' | 'lg';\n  type?: 'primary' | 'secondary';\n  label?: string;\n  target?: '_self' | '_blank';\n}\n\nfunction ButtonLink({\n  href,\n  className,\n  children,\n  type = 'primary',\n  size = 'md',\n  label,\n  target = '_self',\n  ...props\n}: React.AnchorHTMLAttributes<HTMLAnchorElement> & ButtonLinkProps) {\n  const classes = cn(\n    className,\n    'active:scale-[.98] transition-transform inline-flex font-bold items-center outline-none focus:outline-none focus-visible:outline focus-visible:outline-link focus:outline-offset-2 focus-visible:dark:focus:outline-link-dark leading-snug',\n    {\n      'bg-link text-white dark:bg-brand-dark dark:text-secondary hover:bg-opacity-80':\n        type === 'primary',\n      'text-primary dark:text-primary-dark shadow-secondary-button-stroke dark:shadow-secondary-button-stroke-dark hover:bg-gray-40/5 active:bg-gray-40/10 hover:dark:bg-gray-60/5 active:dark:bg-gray-60/10':\n        type === 'secondary',\n      'text-lg py-3 rounded-full px-4 sm:px-6': size === 'lg',\n      'text-base rounded-full px-4 py-2': size === 'md',\n    }\n  );\n  return (\n    <NextLink\n      href={href as string}\n      className={classes}\n      {...props}\n      aria-label={label}\n      target={target}>\n      {children}\n    </NextLink>\n  );\n}\n\nexport default ButtonLink;\n"
  },
  {
    "path": "src/components/DocsFooter.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport NextLink from 'next/link';\nimport {memo} from 'react';\nimport cn from 'classnames';\nimport {IconNavArrow} from './Icon/IconNavArrow';\nimport type {RouteMeta} from './Layout/getRouteMeta';\n\nexport type DocsPageFooterProps = Pick<\n  RouteMeta,\n  'route' | 'nextRoute' | 'prevRoute'\n>;\n\nfunction areEqual(prevProps: DocsPageFooterProps, props: DocsPageFooterProps) {\n  return prevProps.route?.path === props.route?.path;\n}\n\nexport const DocsPageFooter = memo<DocsPageFooterProps>(\n  function DocsPageFooter({nextRoute, prevRoute, route}) {\n    if (!route || route?.heading) {\n      return null;\n    }\n\n    return (\n      <>\n        {prevRoute?.path || nextRoute?.path ? (\n          <>\n            <div className=\"grid grid-cols-1 gap-4 py-4 mx-auto max-w-7xl md:grid-cols-2 md:py-12\">\n              {prevRoute?.path ? (\n                <FooterLink\n                  type=\"이전\"\n                  title={prevRoute.title}\n                  href={prevRoute.path}\n                />\n              ) : (\n                <div />\n              )}\n\n              {nextRoute?.path ? (\n                <FooterLink\n                  type=\"다음\"\n                  title={nextRoute.title}\n                  href={nextRoute.path}\n                />\n              ) : (\n                <div />\n              )}\n            </div>\n          </>\n        ) : null}\n      </>\n    );\n  },\n  areEqual\n);\n\nfunction FooterLink({\n  href,\n  title,\n  type,\n}: {\n  href: string;\n  title: string;\n  type: '이전' | '다음';\n}) {\n  return (\n    <NextLink\n      href={href}\n      className={cn(\n        'flex gap-x-4 md:gap-x-6 items-center w-full md:min-w-80 md:w-fit md:max-w-md px-4 md:px-5 py-6 border-2 border-transparent text-base leading-base text-link dark:text-link-dark rounded-lg group focus:text-link dark:focus:text-link-dark focus:bg-highlight focus:border-link dark:focus:bg-highlight-dark dark:focus:border-link-dark focus:border-opacity-100 focus:border-2 focus:ring-1 focus:ring-offset-4 focus:ring-blue-40 active:ring-0 active:ring-offset-0 hover:bg-gray-5 dark:hover:bg-gray-80',\n        {\n          'flex-row-reverse justify-self-end text-end': type === '다음',\n        }\n      )}>\n      <IconNavArrow\n        className=\"inline text-tertiary dark:text-tertiary-dark group-focus:text-link dark:group-focus:text-link-dark\"\n        displayDirection={type === '이전' ? 'start' : 'end'}\n      />\n      <div className=\"flex flex-col overflow-hidden\">\n        <span className=\"text-sm font-bold tracking-wide no-underline uppercase text-secondary dark:text-secondary-dark group-focus:text-link dark:group-focus:text-link-dark group-focus:text-opacity-100\">\n          {type === '이전' ? '이전' : '다음'}\n        </span>\n        <span className=\"text-lg break-words group-hover:underline\">\n          {title}\n        </span>\n      </div>\n    </NextLink>\n  );\n}\n"
  },
  {
    "path": "src/components/ErrorDecoderContext.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n// Error Decoder requires reading pregenerated error message from getStaticProps,\n// but MDX component doesn't support props. So we use React Context to populate\n// the value without prop-drilling.\n// TODO: Replace with React.cache + React.use when migrating to Next.js App Router\n\nimport {createContext, useContext} from 'react';\n\nconst notInErrorDecoderContext = Symbol('not in error decoder context');\n\nexport const ErrorDecoderContext = createContext<\n  | {errorMessage: string | null; errorCode: string | null}\n  | typeof notInErrorDecoderContext\n>(notInErrorDecoderContext);\n\nexport const useErrorDecoderParams = () => {\n  const params = useContext(ErrorDecoderContext);\n\n  if (params === notInErrorDecoderContext) {\n    throw new Error('useErrorDecoder must be used in error decoder pages only');\n  }\n\n  return params;\n};\n"
  },
  {
    "path": "src/components/ExternalLink.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\nimport type {DetailedHTMLProps, AnchorHTMLAttributes} from 'react';\n\nexport function ExternalLink({\n  href,\n  target,\n  children,\n  ...props\n}: DetailedHTMLProps<\n  AnchorHTMLAttributes<HTMLAnchorElement>,\n  HTMLAnchorElement\n>) {\n  return (\n    <a href={href} target={target ?? '_blank'} rel=\"noopener\" {...props}>\n      {children}\n    </a>\n  );\n}\n"
  },
  {
    "path": "src/components/Icon/IconArrow.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport {memo} from 'react';\nimport cn from 'classnames';\nimport type {SVGProps} from 'react';\n\nexport const IconArrow = memo<\n  SVGProps<SVGSVGElement> & {\n    /**\n     * The direction the arrow should point.\n     * `start` and `end` are relative to the current locale.\n     * for example, in LTR, `start` is left and `end` is right.\n     */\n    displayDirection: 'start' | 'end' | 'right' | 'left' | 'up' | 'down';\n  }\n>(function IconArrow({displayDirection, className, ...rest}) {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      viewBox=\"0 0 24 24\"\n      width=\"1.33em\"\n      height=\"1.33em\"\n      fill=\"currentColor\"\n      {...rest}\n      className={cn(className, {\n        'rotate-180': displayDirection === 'right',\n        'rotate-180 rtl:rotate-0': displayDirection === 'end',\n      })}>\n      <path fill=\"none\" d=\"M0 0h24v24H0z\" />\n      <path d=\"M7.828 11H20v2H7.828l5.364 5.364-1.414 1.414L4 12l7.778-7.778 1.414 1.414z\" />\n    </svg>\n  );\n});\n"
  },
  {
    "path": "src/components/Icon/IconArrowSmall.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport {memo} from 'react';\nimport cn from 'classnames';\nimport type {SVGProps} from 'react';\n\nexport const IconArrowSmall = memo<\n  SVGProps<SVGSVGElement> & {\n    /**\n     * The direction the arrow should point.\n     * `start` and `end` are relative to the current locale.\n     * for example, in LTR, `start` is left and `end` is right.\n     */\n    displayDirection: 'start' | 'end' | 'right' | 'left' | 'up' | 'down';\n  }\n>(function IconArrowSmall({displayDirection, className, ...rest}) {\n  const classes = cn(className, {\n    'rotate-180': displayDirection === 'left',\n    'rotate-180 rtl:rotate-0': displayDirection === 'start',\n    'rtl:rotate-180': displayDirection === 'end',\n    'rotate-90': displayDirection === 'down',\n  });\n  return (\n    <svg\n      width=\"1em\"\n      height=\"1em\"\n      viewBox=\"0 0 20 20\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      className={classes}\n      {...rest}>\n      <path\n        d=\"M6.86612 13.6161C6.37796 14.1043 6.37796 14.8957 6.86612 15.3839C7.35427 15.872 8.14572 15.872 8.63388 15.3839L13.1339 10.8839C13.622 10.3957 13.622 9.60428 13.1339 9.11612L8.63388 4.61612C8.14572 4.12797 7.35427 4.12797 6.86612 4.61612C6.37796 5.10428 6.37796 5.89573 6.86612 6.38388L10.4822 10L6.86612 13.6161Z\"\n        fill=\"currentColor\"></path>\n    </svg>\n  );\n});\n"
  },
  {
    "path": "src/components/Icon/IconBsky.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport {memo} from 'react';\nimport type {SVGProps} from 'react';\n\nexport const IconBsky = memo<SVGProps<SVGSVGElement>>(function IconBsky(props) {\n  return (\n    <svg\n      aria-label=\"Bluesky\"\n      viewBox=\"0 0 16 16\"\n      height=\"1.25em\"\n      width=\"1.25em\"\n      fill=\"currentColor\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      {...props}>\n      <path\n        className=\"x19hqcy\"\n        d=\"M3.468 1.948C5.303 3.325 7.276 6.118 8 7.616c.725-1.498 2.697-4.29 4.532-5.668C13.855.955 16 .186 16 2.632c0 .489-.28 4.105-.444 4.692-.572 2.04-2.653 2.561-4.504 2.246 3.236.551 4.06 2.375 2.281 4.2-3.376 3.464-4.852-.87-5.23-1.98-.07-.204-.103-.3-.103-.218 0-.081-.033.014-.102.218-.379 1.11-1.855 5.444-5.231 1.98-1.778-1.825-.955-3.65 2.28-4.2-1.85.315-3.932-.205-4.503-2.246C.28 6.737 0 3.12 0 2.632 0 .186 2.145.955 3.468 1.948Z\"></path>\n    </svg>\n  );\n});\n"
  },
  {
    "path": "src/components/Icon/IconCanary.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport {memo} from 'react';\n\nexport const IconCanary = memo<\n  JSX.IntrinsicElements['svg'] & {title?: string; size?: 's' | 'md'}\n>(function IconCanary(\n  {className, title, size} = {\n    className: undefined,\n    title: undefined,\n    size: 'md',\n  }\n) {\n  return (\n    <svg\n      className={className}\n      width={size === 's' ? '12px' : '20px'}\n      height={size === 's' ? '12px' : '20px'}\n      viewBox=\"0 0 20 20\"\n      version=\"1.1\"\n      xmlns=\"http://www.w3.org/2000/svg\">\n      {title && <title>{title}</title>}\n      <g stroke=\"none\" strokeWidth=\"1\" fill=\"none\" fillRule=\"evenodd\">\n        <g\n          id=\"noun-labs-1201738-(2)\"\n          transform=\"translate(2, 0)\"\n          fill=\"currentColor\"\n          fillRule=\"nonzero\">\n          <path\n            d=\"M10.2865804,5.55665262 L10.2865804,2.22331605 L10.8591544,2.22331605 C11.0103911,2.22244799 11.1551447,2.16342155 11.2617505,2.05914367 C11.3684534,1.95486857 11.4282767,1.81370176 11.4282767,1.66667106 L11.4282767,0.556642208 C11.4282767,0.40907262 11.3678934,0.26747526 11.2605218,0.16308627 C11.1531503,0.0587028348 11.0074938,0 10.8556998,0 L5.14338868,0 C4.9915947,0 4.84594391,0.0587028348 4.73856664,0.16308627 C4.63119507,0.267469704 4.57081178,0.40907262 4.57081178,0.556642208 L4.57081178,1.66667106 C4.57081178,1.81434899 4.63119507,1.95594912 4.73856664,2.06033811 C4.8459382,2.16472155 4.9915947,2.22331605 5.14338868,2.22331605 L5.71596273,2.22331605 L5.71596273,5.55665262 C5.71596273,8.38665538 2.97295619,9.88999017 0.651686904,15.5566623 C-0.0957823782,17.360053 -2.00560068,20 7.99951567,20 C18.004632,20 16.0948137,17.3600252 15.3507732,15.5566623 C13.0124432,9.88999017 10.2865804,8.38665538 10.2865804,5.55665262 Z M9.89570197,10.709991 C10.0921412,10.709991 10.2805515,10.7858383 10.4193876,10.9209301 C10.5583466,11.0559135 10.6363652,11.2390693 10.6363652,11.4300417 C10.6363652,11.6210141 10.5583466,11.8040698 10.4193876,11.9391533 C10.2805401,12.0741367 10.0921412,12.1499813 9.89570197,12.1499813 C9.6992627,12.1499813 9.51096673,12.074134 9.37201631,11.9391533 C9.23316875,11.8040615 9.15515307,11.6210141 9.15515307,11.4300417 C9.15515307,11.2390693 9.2331716,11.0559024 9.37201631,10.9209301 C9.57264221,10.7258996 9.61239426,10.709991 9.89570197,10.709991 Z M8.98919546,9.04212824 C9.09790709,9.14792278 9.15884755,9.29158681 9.1585213,9.44110085 C9.15829001,9.59073155 9.09678989,9.73407335 8.98763252,9.83954568 C8.87847514,9.945018 8.73069852,10.0039347 8.57678157,10.0033977 C8.42286747,10.0027392 8.27565088,9.94273467 8.16727355,9.83639845 C8.05900765,9.73006224 7.99873866,9.58628988 7.99963013,9.43664806 C8.00052304,9.28788403 8.0620221,9.14542556 8.17051087,9.04048101 C8.27911107,8.93555591 8.42599335,8.87663641 8.57913312,8.87663641 C8.73291864,8.87665585 8.88047525,8.93622535 8.98919546,9.04212824 Z M7.99965585,17.9999981 C4.91377349,17.9999981 3.29882839,17.7332867 2.51364277,17.4999976 C2.37780966,17.4476975 2.26954376,17.3439641 2.21396931,17.2125528 C2.15838628,17.0811499 2.16006066,16.9334692 2.21876871,16.8033858 C2.6144474,15.5921346 3.14916224,14.4280501 3.81316983,13.3333824 C5.980145,9.82337899 8.22941036,13.8867718 10.0980836,13.8867718 C11.9666996,13.8867718 11.4695868,12.1534924 12.1827971,13.3333824 C12.8511505,14.4269112 13.3916656,15.5896902 13.794259,16.8000524 C13.8533022,16.9322137 13.8537479,17.0822749 13.7952635,17.2147751 C13.7368889,17.3472613 13.6248314,17.4504531 13.4856467,17.5000531 C12.6833967,17.7332867 11.0855382,17.9999981 7.99965585,17.9999981 Z\"\n            id=\"Shape\"></path>\n        </g>\n      </g>\n    </svg>\n  );\n});\n"
  },
  {
    "path": "src/components/Icon/IconChevron.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport {memo} from 'react';\nimport cn from 'classnames';\n\nexport const IconChevron = memo<\n  JSX.IntrinsicElements['svg'] & {\n    /**\n     * The direction the arrow should point.\n     * `start` and `end` are relative to the current locale.\n     * for example, in LTR, `start` is left and `end` is right.\n     */\n    displayDirection: 'start' | 'end' | 'right' | 'left' | 'up' | 'down';\n  }\n>(function IconChevron({className, displayDirection}) {\n  const classes = cn(\n    {\n      'rotate-0': displayDirection === 'down',\n      'rotate-90': displayDirection === 'left',\n      'rotate-180': displayDirection === 'up',\n      '-rotate-90': displayDirection === 'right',\n      'rotate-90 rtl:-rotate-90': displayDirection === 'start',\n      '-rotate-90 rtl:rotate-90': displayDirection === 'end',\n    },\n    className\n  );\n  return (\n    <svg\n      className={classes}\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width=\"20\"\n      height=\"20\"\n      viewBox=\"0 0 20 20\">\n      <g fill=\"none\" fillRule=\"evenodd\" transform=\"translate(-446 -398)\">\n        <path\n          fill=\"currentColor\"\n          fillRule=\"nonzero\"\n          d=\"M95.8838835,240.366117 C95.3957281,239.877961 94.6042719,239.877961 94.1161165,240.366117 C93.6279612,240.854272 93.6279612,241.645728 94.1161165,242.133883 L98.6161165,246.633883 C99.1042719,247.122039 99.8957281,247.122039 100.383883,246.633883 L104.883883,242.133883 C105.372039,241.645728 105.372039,240.854272 104.883883,240.366117 C104.395728,239.877961 103.604272,239.877961 103.116117,240.366117 L99.5,243.982233 L95.8838835,240.366117 Z\"\n          transform=\"translate(356.5 164.5)\"\n        />\n        <polygon points=\"446 418 466 418 466 398 446 398\" />\n      </g>\n    </svg>\n  );\n});\n"
  },
  {
    "path": "src/components/Icon/IconClose.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport {memo} from 'react';\nimport type {SVGProps} from 'react';\n\nexport const IconClose = memo<SVGProps<SVGSVGElement>>(function IconClose(\n  props\n) {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width=\"1.33em\"\n      height=\"1.33em\"\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth={2}\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      {...props}>\n      <line x1={18} y1={6} x2={6} y2={18} />\n      <line x1={6} y1={6} x2={18} y2={18} />\n    </svg>\n  );\n});\n"
  },
  {
    "path": "src/components/Icon/IconCodeBlock.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport {memo} from 'react';\n\nexport const IconCodeBlock = memo<JSX.IntrinsicElements['svg']>(\n  function IconCodeBlock({className}) {\n    return (\n      <svg\n        className={className}\n        width=\"1.33em\"\n        height=\"1em\"\n        viewBox=\"0 0 24 18\"\n        fill=\"none\"\n        xmlns=\"http://www.w3.org/2000/svg\">\n        <path\n          d=\"M24 9L18.343 14.657L16.929 13.243L21.172 9L16.929 4.757L18.343 3.343L24 9ZM2.828 9L7.071 13.243L5.657 14.657L0 9L5.657 3.343L7.07 4.757L2.828 9ZM9.788 18H7.66L14.212 0H16.34L9.788 18Z\"\n          fill=\"currentColor\"\n        />\n      </svg>\n    );\n  }\n);\n"
  },
  {
    "path": "src/components/Icon/IconCopy.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport {memo} from 'react';\n\nexport const IconCopy = memo<JSX.IntrinsicElements['svg']>(function IconCopy({\n  className,\n}) {\n  return (\n    <svg\n      className={className}\n      width=\"1em\"\n      height=\"1em\"\n      viewBox=\"0 0 18 18\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\">\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M5.40382 15.3671C5.03332 15.1901 4.70081 14.9381 4.42481 14.6286C4.34831 14.5431 4.23931 14.5001 4.12981 14.5206L3.66181 14.6081C3.33531 14.6691 3.02032 14.4361 2.96232 14.0876L1.30981 4.12512C1.28181 3.95662 1.31731 3.7861 1.40981 3.6456C1.50231 3.5051 1.64082 3.41162 1.79932 3.38162L3.22131 3.00012C3.37681 2.97062 3.48981 2.82761 3.48981 2.65961V1.9101C3.48981 1.8276 3.49381 1.74561 3.49931 1.66461C3.50931 1.53461 3.35181 1.57211 3.35181 1.57211L1.64381 2.0076C1.18481 2.0936 0.751316 2.32662 0.451316 2.70612C0.0808162 3.17362 -0.0686885 3.77259 0.0293115 4.36459L1.68231 14.3281C1.84531 15.3081 2.65031 16.0001 3.55631 16.0001C3.66531 16.0001 3.77631 15.9896 3.88731 15.9691L5.36632 15.6916C5.52332 15.6626 5.54982 15.4366 5.40382 15.3671ZM14.9331 4.55801H12.9116C12.1351 4.55801 11.5001 3.91502 11.5001 3.12952V1.06802C11.5001 0.480524 11.0196 0 10.4321 0H7.44161C6.36911 0 5.50011 0.879508 5.50011 1.96451V12.1665C5.50011 13.179 6.33412 14 7.36262 14H14.1371C15.1661 14 16.0001 13.179 16.0001 12.1665V5.625C16.0001 5.038 15.5201 4.55801 14.9331 4.55801Z\"\n        fill=\"currentColor\"\n      />{' '}\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M12.5888 0.0914385C12.4493 0.00843847 12.5158 0.252449 12.5158 0.252449C12.5653 0.428449 12.5918 0.613451 12.5918 0.804451V2.90296C12.5918 3.17646 12.8158 3.40046 13.0903 3.40046H15.1718C15.3883 3.40046 15.5968 3.43495 15.7918 3.49845C15.7918 3.49845 15.9373 3.50844 15.9008 3.43494C15.8383 3.33744 15.7673 3.24494 15.6833 3.16044L12.8303 0.289467C12.7558 0.214467 12.6743 0.149438 12.5888 0.0914385Z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  );\n});\n"
  },
  {
    "path": "src/components/Icon/IconDeepDive.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport {memo} from 'react';\n\nexport const IconDeepDive = memo<JSX.IntrinsicElements['svg']>(\n  function IconDeepDive({className}) {\n    return (\n      <svg\n        className={className}\n        width=\"1.5em\"\n        height=\"1.5em\"\n        viewBox=\"0 0 72 72\"\n        fill=\"none\"\n        xmlns=\"http://www.w3.org/2000/svg\">\n        <path\n          fillRule=\"evenodd\"\n          clipRule=\"evenodd\"\n          d=\"M34.7409 59.7228L32.9567 58.9094C27.2672 56.3157 20.7328 56.3157 15.0433 58.9094C12.6018 60.0224 9.39163 59.0275 8.44602 56.0621C7.45647 52.9589 5.99975 46.5898 6 35.9997C6.00029 23.5648 8.00803 18.3599 9.11099 16.4196C9.67795 15.4222 10.5255 14.8455 11.2254 14.5264L12.0179 14.1651C19.6351 10.6926 28.4011 10.6738 36 14.1733C43.5989 10.6738 52.3649 10.6926 59.9821 14.1651L60.7746 14.5264C61.4745 14.8455 62.3221 15.4222 62.889 16.4196C63.992 18.3599 65.9997 23.5648 66 35.9997C66.0002 46.5898 64.5435 52.9589 63.554 56.0621C62.6084 59.0275 59.3982 60.0224 56.9567 58.9094C51.2672 56.3157 44.7328 56.3157 39.0433 58.9094L37.2591 59.7228C37.1986 59.7508 37.1373 59.7767 37.0753 59.8004C36.4484 60.0411 35.7556 60.0653 35.1102 59.8648C34.9847 59.8258 34.8613 59.7784 34.7409 59.7228ZM14.5068 19.6246C20.3733 16.9501 27.0874 16.8775 33 19.4067V52.473C26.7613 50.32 19.9378 50.471 13.7811 52.9261C13.0005 49.9843 11.9998 44.547 12 35.9998C12.0002 25.5786 13.4879 21.1893 14.1179 19.8018L14.5068 19.6246ZM39 52.473C45.2387 50.32 52.0622 50.471 58.2189 52.9261C58.9995 49.9843 60.0002 44.547 60 35.9998C59.9998 25.5786 58.5121 21.1893 57.8821 19.8018L57.4932 19.6246C51.6267 16.9501 44.9126 16.8775 39 19.4067V52.473Z\"\n          fill=\"currentColor\"\n        />\n      </svg>\n    );\n  }\n);\n"
  },
  {
    "path": "src/components/Icon/IconDownload.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport {memo} from 'react';\n\nexport const IconDownload = memo<JSX.IntrinsicElements['svg']>(\n  function IconDownload({className}) {\n    return (\n      <svg\n        width=\"1em\"\n        height=\"1em\"\n        viewBox=\"0 0 24 24\"\n        fill=\"none\"\n        xmlns=\"http://www.w3.org/2000/svg\"\n        className={className}>\n        <path\n          d=\"M20.5 22H3.5C3.10218 22 2.72064 21.842 2.43934 21.5607C2.15804 21.2794 2 20.8978 2 20.5V15.5C2 15.3674 2.05268 15.2402 2.14645 15.1464C2.24021 15.0527 2.36739 15 2.5 15H3.5C3.63261 15 3.75979 15.0527 3.85355 15.1464C3.94732 15.2402 4 15.3674 4 15.5V20H20V15.5C20 15.3674 20.0527 15.2402 20.1464 15.1464C20.2402 15.0527 20.3674 15 20.5 15H21.5C21.6326 15 21.7598 15.0527 21.8536 15.1464C21.9473 15.2402 22 15.3674 22 15.5V20.5C22 20.8978 21.842 21.2794 21.5607 21.5607C21.2794 21.842 20.8978 22 20.5 22Z\"\n          fill=\"currentColor\"\n        />\n        <path\n          d=\"M10.9999 2.5V13.79L8.81994 11.61C8.72479 11.5178 8.59747 11.4662 8.46494 11.4662C8.33241 11.4662 8.20509 11.5178 8.10994 11.61L7.39994 12.32C7.30769 12.4151 7.2561 12.5425 7.2561 12.675C7.2561 12.8075 7.30769 12.9348 7.39994 13.03L10.9399 16.56C11.0785 16.7003 11.2436 16.8117 11.4255 16.8877C11.6075 16.9637 11.8027 17.0029 11.9999 17.0029C12.1971 17.0029 12.3924 16.9637 12.5743 16.8877C12.7563 16.8117 12.9214 16.7003 13.0599 16.56L16.5999 13C16.6922 12.9048 16.7438 12.7775 16.7438 12.645C16.7438 12.5125 16.6922 12.3851 16.5999 12.29L15.8899 11.58C15.7948 11.4878 15.6675 11.4362 15.5349 11.4362C15.4024 11.4362 15.2751 11.4878 15.1799 11.58L12.9999 13.79V2.5C12.9999 2.36739 12.9473 2.24021 12.8535 2.14645C12.7597 2.05268 12.6325 2 12.4999 2H11.4999C11.3673 2 11.2402 2.05268 11.1464 2.14645C11.0526 2.24021 10.9999 2.36739 10.9999 2.5Z\"\n          fill=\"currentColor\"\n        />\n      </svg>\n    );\n  }\n);\n"
  },
  {
    "path": "src/components/Icon/IconError.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport {memo} from 'react';\n\nexport const IconError = memo<JSX.IntrinsicElements['svg']>(function IconError({\n  className,\n}) {\n  return (\n    <svg\n      className={className}\n      width=\"1.33em\"\n      height=\"1.33em\"\n      viewBox=\"0 0 24 24\"\n      xmlns=\"http://www.w3.org/2000/svg\">\n      <circle cx=\"10.1626\" cy=\"9.99951\" r=\"9.47021\" fill=\"currentColor\" />\n      <path d=\"M6.22705 5.95996L14.2798 14.0127\" stroke=\"white\" />\n      <path d=\"M14.2798 5.95996L6.22705 14.0127\" stroke=\"white\" />\n    </svg>\n  );\n});\n"
  },
  {
    "path": "src/components/Icon/IconExperimental.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport {memo} from 'react';\n\nexport const IconExperimental = memo<\n  JSX.IntrinsicElements['svg'] & {title?: string; size?: 's' | 'md'}\n>(function IconExperimental(\n  {className, title, size} = {\n    className: undefined,\n    title: undefined,\n    size: 'md',\n  }\n) {\n  return (\n    <svg\n      className={className}\n      width={size === 's' ? '12px' : '20px'}\n      height={size === 's' ? '12px' : '20px'}\n      viewBox=\"0 0 20 20\"\n      version=\"1.1\"\n      xmlns=\"http://www.w3.org/2000/svg\">\n      {title && <title>{title}</title>}\n      <g stroke=\"none\" strokeWidth=\"1\" fill=\"none\" fillRule=\"evenodd\">\n        <g\n          id=\"noun-labs-1201738-(2)\"\n          transform=\"translate(2, 0)\"\n          fill=\"currentColor\"\n          fillRule=\"nonzero\">\n          <path\n            d=\"M10.2865804,5.55665262 L10.2865804,2.22331605 L10.8591544,2.22331605 C11.0103911,2.22244799 11.1551447,2.16342155 11.2617505,2.05914367 C11.3684534,1.95486857 11.4282767,1.81370176 11.4282767,1.66667106 L11.4282767,0.556642208 C11.4282767,0.40907262 11.3678934,0.26747526 11.2605218,0.16308627 C11.1531503,0.0587028348 11.0074938,0 10.8556998,0 L5.14338868,0 C4.9915947,0 4.84594391,0.0587028348 4.73856664,0.16308627 C4.63119507,0.267469704 4.57081178,0.40907262 4.57081178,0.556642208 L4.57081178,1.66667106 C4.57081178,1.81434899 4.63119507,1.95594912 4.73856664,2.06033811 C4.8459382,2.16472155 4.9915947,2.22331605 5.14338868,2.22331605 L5.71596273,2.22331605 L5.71596273,5.55665262 C5.71596273,8.38665538 2.97295619,9.88999017 0.651686904,15.5566623 C-0.0957823782,17.360053 -2.00560068,20 7.99951567,20 C18.004632,20 16.0948137,17.3600252 15.3507732,15.5566623 C13.0124432,9.88999017 10.2865804,8.38665538 10.2865804,5.55665262 Z M9.89570197,10.709991 C10.0921412,10.709991 10.2805515,10.7858383 10.4193876,10.9209301 C10.5583466,11.0559135 10.6363652,11.2390693 10.6363652,11.4300417 C10.6363652,11.6210141 10.5583466,11.8040698 10.4193876,11.9391533 C10.2805401,12.0741367 10.0921412,12.1499813 9.89570197,12.1499813 C9.6992627,12.1499813 9.51096673,12.074134 9.37201631,11.9391533 C9.23316875,11.8040615 9.15515307,11.6210141 9.15515307,11.4300417 C9.15515307,11.2390693 9.2331716,11.0559024 9.37201631,10.9209301 C9.57264221,10.7258996 9.61239426,10.709991 9.89570197,10.709991 Z M8.98919546,9.04212824 C9.09790709,9.14792278 9.15884755,9.29158681 9.1585213,9.44110085 C9.15829001,9.59073155 9.09678989,9.73407335 8.98763252,9.83954568 C8.87847514,9.945018 8.73069852,10.0039347 8.57678157,10.0033977 C8.42286747,10.0027392 8.27565088,9.94273467 8.16727355,9.83639845 C8.05900765,9.73006224 7.99873866,9.58628988 7.99963013,9.43664806 C8.00052304,9.28788403 8.0620221,9.14542556 8.17051087,9.04048101 C8.27911107,8.93555591 8.42599335,8.87663641 8.57913312,8.87663641 C8.73291864,8.87665585 8.88047525,8.93622535 8.98919546,9.04212824 Z M7.99965585,17.9999981 C4.91377349,17.9999981 3.29882839,17.7332867 2.51364277,17.4999976 C2.37780966,17.4476975 2.26954376,17.3439641 2.21396931,17.2125528 C2.15838628,17.0811499 2.16006066,16.9334692 2.21876871,16.8033858 C2.6144474,15.5921346 3.14916224,14.4280501 3.81316983,13.3333824 C5.980145,9.82337899 8.22941036,13.8867718 10.0980836,13.8867718 C11.9666996,13.8867718 11.4695868,12.1534924 12.1827971,13.3333824 C12.8511505,14.4269112 13.3916656,15.5896902 13.794259,16.8000524 C13.8533022,16.9322137 13.8537479,17.0822749 13.7952635,17.2147751 C13.7368889,17.3472613 13.6248314,17.4504531 13.4856467,17.5000531 C12.6833967,17.7332867 11.0855382,17.9999981 7.99965585,17.9999981 Z\"\n            id=\"Shape\"></path>\n        </g>\n      </g>\n    </svg>\n  );\n});\n"
  },
  {
    "path": "src/components/Icon/IconFacebookCircle.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport {memo} from 'react';\nimport type {SVGProps} from 'react';\n\nexport const IconFacebookCircle = memo<SVGProps<SVGSVGElement>>(\n  function IconFacebookCircle(props) {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 24 24\"\n        width=\"1.33em\"\n        height=\"1.33em\"\n        fill=\"currentColor\"\n        {...props}>\n        <path fill=\"none\" d=\"M0 0h24v24H0z\" />\n        <path d=\"M12 2C6.477 2 2 6.477 2 12c0 4.991 3.657 9.128 8.438 9.879V14.89h-2.54V12h2.54V9.797c0-2.506 1.492-3.89 3.777-3.89 1.094 0 2.238.195 2.238.195v2.46h-1.26c-1.243 0-1.63.771-1.63 1.562V12h2.773l-.443 2.89h-2.33v6.989C18.343 21.129 22 16.99 22 12c0-5.523-4.477-10-10-10z\" />\n      </svg>\n    );\n  }\n);\n"
  },
  {
    "path": "src/components/Icon/IconGitHub.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport {memo} from 'react';\nimport type {SVGProps} from 'react';\n\nexport const IconGitHub = memo<SVGProps<SVGSVGElement>>(function IconGitHub(\n  props\n) {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width=\"1.5em\"\n      height=\"1.5em\"\n      viewBox=\"0 -2 24 24\"\n      fill=\"currentColor\"\n      {...props}>\n      <path d=\"M10 0a10 10 0 0 0-3.16 19.49c.5.1.68-.22.68-.48l-.01-1.7c-2.78.6-3.37-1.34-3.37-1.34-.46-1.16-1.11-1.47-1.11-1.47-.9-.62.07-.6.07-.6 1 .07 1.53 1.03 1.53 1.03.9 1.52 2.34 1.08 2.91.83.1-.65.35-1.09.63-1.34-2.22-.25-4.55-1.11-4.55-4.94 0-1.1.39-1.99 1.03-2.69a3.6 3.6 0 0 1 .1-2.64s.84-.27 2.75 1.02a9.58 9.58 0 0 1 5 0c1.91-1.3 2.75-1.02 2.75-1.02.55 1.37.2 2.4.1 2.64.64.7 1.03 1.6 1.03 2.69 0 3.84-2.34 4.68-4.57 4.93.36.31.68.92.68 1.85l-.01 2.75c0 .26.18.58.69.48A10 10 0 0 0 10 0\"></path>\n    </svg>\n  );\n});\n"
  },
  {
    "path": "src/components/Icon/IconHamburger.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport {memo} from 'react';\nimport type {SVGProps} from 'react';\n\nexport const IconHamburger = memo<SVGProps<SVGSVGElement>>(\n  function IconHamburger(props) {\n    return (\n      <svg\n        width=\"1.33em\"\n        height=\"1.33em\"\n        viewBox=\"0 0 24 24\"\n        fill=\"none\"\n        stroke=\"currentColor\"\n        strokeWidth=\"2\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n        {...props}>\n        <line x1=\"3\" y1=\"12\" x2=\"21\" y2=\"12\"></line>\n        <line x1=\"3\" y1=\"6\" x2=\"21\" y2=\"6\"></line>\n        <line x1=\"3\" y1=\"18\" x2=\"21\" y2=\"18\"></line>\n      </svg>\n    );\n  }\n);\n"
  },
  {
    "path": "src/components/Icon/IconHint.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport {memo} from 'react';\nimport cn from 'classnames';\n\nexport const IconHint = memo<JSX.IntrinsicElements['svg']>(function IconHint({\n  className,\n}) {\n  return (\n    <svg\n      className={cn('inline -mt-0.5', className)}\n      width=\"12\"\n      height=\"14\"\n      viewBox=\"0 0 12 15\"\n      xmlns=\"http://www.w3.org/2000/svg\">\n      <path\n        d=\"M4.53487 11H5.21954V7.66665H6.55287V11H7.23754C7.32554 10.1986 7.7342 9.53732 8.39754 8.81532C8.47287 8.73398 8.9522 8.23732 9.00887 8.16665C9.47973 7.5784 9.77486 6.86913 9.86028 6.1205C9.9457 5.37187 9.81794 4.61434 9.4917 3.93514C9.16547 3.25594 8.65402 2.6827 8.01628 2.28143C7.37853 1.88016 6.64041 1.66719 5.88692 1.66703C5.13344 1.66686 4.39523 1.87953 3.75731 2.28052C3.11939 2.68152 2.60771 3.25454 2.28118 3.9336C1.95465 4.61266 1.82656 5.37014 1.91167 6.1188C1.99677 6.86747 2.2916 7.57687 2.7622 8.16532C2.81954 8.23665 3.3002 8.73398 3.3742 8.81465C4.0382 9.53732 4.44687 10.1986 4.53487 11ZM4.55287 12.3333V13H7.21954V12.3333H4.55287ZM1.7222 8.99998C1.09433 8.21551 0.700836 7.26963 0.587047 6.2713C0.473258 5.27296 0.643804 4.26279 1.07904 3.35715C1.51428 2.4515 2.19649 1.68723 3.04711 1.15237C3.89772 0.617512 4.88213 0.333824 5.88692 0.333984C6.89172 0.334145 7.87604 0.61815 8.72648 1.15328C9.57692 1.68841 10.2589 2.4529 10.6938 3.35869C11.1288 4.26447 11.299 5.27469 11.1849 6.27299C11.0708 7.27129 10.677 8.21705 10.0489 9.00132C9.63554 9.51598 8.55287 10.3333 8.55287 11.3333V13C8.55287 13.3536 8.41239 13.6927 8.16235 13.9428C7.9123 14.1928 7.57316 14.3333 7.21954 14.3333H4.55287C4.19925 14.3333 3.86011 14.1928 3.61006 13.9428C3.36001 13.6927 3.21954 13.3536 3.21954 13V11.3333C3.21954 10.3333 2.1362 9.51598 1.7222 8.99998Z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  );\n});\n"
  },
  {
    "path": "src/components/Icon/IconInstagram.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport {memo} from 'react';\nimport type {SVGProps} from 'react';\n\nexport const IconInstagram = memo<SVGProps<SVGSVGElement>>(\n  function IconInstagram(props) {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 24 24\"\n        width=\"1.33em\"\n        height=\"1.33em\"\n        fill=\"currentColor\"\n        {...props}>\n        <path fill=\"none\" d=\"M0 0h24v24H0z\" />\n        <path d=\"M12 9a3 3 0 1 0 0 6 3 3 0 0 0 0-6zm0-2a5 5 0 1 1 0 10 5 5 0 0 1 0-10zm6.5-.25a1.25 1.25 0 0 1-2.5 0 1.25 1.25 0 0 1 2.5 0zM12 4c-2.474 0-2.878.007-4.029.058-.784.037-1.31.142-1.798.332-.434.168-.747.369-1.08.703a2.89 2.89 0 0 0-.704 1.08c-.19.49-.295 1.015-.331 1.798C4.006 9.075 4 9.461 4 12c0 2.474.007 2.878.058 4.029.037.783.142 1.31.331 1.797.17.435.37.748.702 1.08.337.336.65.537 1.08.703.494.191 1.02.297 1.8.333C9.075 19.994 9.461 20 12 20c2.474 0 2.878-.007 4.029-.058.782-.037 1.309-.142 1.797-.331.433-.169.748-.37 1.08-.702.337-.337.538-.65.704-1.08.19-.493.296-1.02.332-1.8.052-1.104.058-1.49.058-4.029 0-2.474-.007-2.878-.058-4.029-.037-.782-.142-1.31-.332-1.798a2.911 2.911 0 0 0-.703-1.08 2.884 2.884 0 0 0-1.08-.704c-.49-.19-1.016-.295-1.798-.331C14.925 4.006 14.539 4 12 4zm0-2c2.717 0 3.056.01 4.122.06 1.065.05 1.79.217 2.428.465.66.254 1.216.598 1.772 1.153a4.908 4.908 0 0 1 1.153 1.772c.247.637.415 1.363.465 2.428.047 1.066.06 1.405.06 4.122 0 2.717-.01 3.056-.06 4.122-.05 1.065-.218 1.79-.465 2.428a4.883 4.883 0 0 1-1.153 1.772 4.915 4.915 0 0 1-1.772 1.153c-.637.247-1.363.415-2.428.465-1.066.047-1.405.06-4.122.06-2.717 0-3.056-.01-4.122-.06-1.065-.05-1.79-.218-2.428-.465a4.89 4.89 0 0 1-1.772-1.153 4.904 4.904 0 0 1-1.153-1.772c-.248-.637-.415-1.363-.465-2.428C2.013 15.056 2 14.717 2 12c0-2.717.01-3.056.06-4.122.05-1.066.217-1.79.465-2.428a4.88 4.88 0 0 1 1.153-1.772A4.897 4.897 0 0 1 5.45 2.525c.638-.248 1.362-.415 2.428-.465C8.944 2.013 9.283 2 12 2z\" />\n      </svg>\n    );\n  }\n);\n"
  },
  {
    "path": "src/components/Icon/IconLink.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport {memo} from 'react';\nimport type {SVGProps} from 'react';\n\nexport const IconLink = memo<SVGProps<SVGSVGElement>>(function IconLink(props) {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width=\"1.33em\"\n      height=\"1.33em\"\n      viewBox=\"0 -2 24 24\"\n      fill=\"currentColor\"\n      {...props}>\n      <path\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n        d=\"M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m13.35-.622l1.757-1.757a4.5 4.5 0 00-6.364-6.364l-4.5 4.5a4.5 4.5 0 001.242 7.244\"\n      />\n    </svg>\n  );\n});\n"
  },
  {
    "path": "src/components/Icon/IconNavArrow.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport {memo} from 'react';\nimport cn from 'classnames';\n\nexport const IconNavArrow = memo<\n  JSX.IntrinsicElements['svg'] & {\n    /**\n     * The direction the arrow should point.\n     * `start` and `end` are relative to the current locale.\n     * for example, in LTR, `start` is left and `end` is right.\n     */\n    displayDirection: 'start' | 'end' | 'right' | 'left' | 'down';\n  }\n>(function IconNavArrow({displayDirection = 'start', className}) {\n  const classes = cn(\n    'duration-100 ease-in transition',\n    {\n      'rotate-0': displayDirection === 'down',\n      'rotate-90': displayDirection === 'left',\n      '-rotate-90': displayDirection === 'right',\n      'rotate-90 rtl:-rotate-90': displayDirection === 'start',\n      '-rotate-90 rtl:rotate-90': displayDirection === 'end',\n    },\n    className\n  );\n\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width=\"20\"\n      height=\"20\"\n      viewBox=\"0 0 20 20\"\n      className={classes}\n      style={{minWidth: 20, minHeight: 20}}>\n      <g fill=\"none\" fillRule=\"evenodd\" transform=\"translate(-446 -398)\">\n        <path\n          fill=\"currentColor\"\n          fillRule=\"nonzero\"\n          d=\"M95.8838835,240.366117 C95.3957281,239.877961 94.6042719,239.877961 94.1161165,240.366117 C93.6279612,240.854272 93.6279612,241.645728 94.1161165,242.133883 L98.6161165,246.633883 C99.1042719,247.122039 99.8957281,247.122039 100.383883,246.633883 L104.883883,242.133883 C105.372039,241.645728 105.372039,240.854272 104.883883,240.366117 C104.395728,239.877961 103.604272,239.877961 103.116117,240.366117 L99.5,243.982233 L95.8838835,240.366117 Z\"\n          transform=\"translate(356.5 164.5)\"\n        />\n        <polygon points=\"446 418 466 418 466 398 446 398\" />\n      </g>\n    </svg>\n  );\n});\n"
  },
  {
    "path": "src/components/Icon/IconNewPage.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport {memo} from 'react';\nimport type {SVGProps} from 'react';\n\nexport const IconNewPage = memo<SVGProps<SVGSVGElement>>(function IconNewPage(\n  props\n) {\n  return (\n    <svg\n      width=\"1em\"\n      height=\"1em\"\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      {...props}>\n      <path\n        d=\"M20.5001 2H15.5001C15.3675 2 15.2403 2.05268 15.1465 2.14645C15.0528 2.24021 15.0001 2.36739 15.0001 2.5V3.5C15.0001 3.63261 15.0528 3.75979 15.1465 3.85355C15.2403 3.94732 15.3675 4 15.5001 4H18.5901L7.6501 14.94C7.60323 14.9865 7.56604 15.0418 7.54065 15.1027C7.51527 15.1636 7.5022 15.229 7.5022 15.295C7.5022 15.361 7.51527 15.4264 7.54065 15.4873C7.56604 15.5482 7.60323 15.6035 7.6501 15.65L8.3501 16.35C8.39658 16.3969 8.45188 16.4341 8.51281 16.4594C8.57374 16.4848 8.63909 16.4979 8.7051 16.4979C8.7711 16.4979 8.83646 16.4848 8.89738 16.4594C8.95831 16.4341 9.01362 16.3969 9.0601 16.35L20.0001 5.41V8.5C20.0001 8.63261 20.0528 8.75979 20.1465 8.85355C20.2403 8.94732 20.3675 9 20.5001 9H21.5001C21.6327 9 21.7599 8.94732 21.8537 8.85355C21.9474 8.75979 22.0001 8.63261 22.0001 8.5V3.5C22.0001 3.10218 21.8421 2.72064 21.5608 2.43934C21.2795 2.15804 20.8979 2 20.5001 2V2Z\"\n        fill=\"currentColor\"\n      />\n      <path\n        d=\"M21.5 13H20.5C20.3674 13 20.2402 13.0527 20.1464 13.1464C20.0527 13.2402 20 13.3674 20 13.5V20H4V4H10.5C10.6326 4 10.7598 3.94732 10.8536 3.85355C10.9473 3.75979 11 3.63261 11 3.5V2.5C11 2.36739 10.9473 2.24021 10.8536 2.14645C10.7598 2.05268 10.6326 2 10.5 2H3.5C3.10218 2 2.72064 2.15804 2.43934 2.43934C2.15804 2.72064 2 3.10218 2 3.5V20.5C2 20.8978 2.15804 21.2794 2.43934 21.5607C2.72064 21.842 3.10218 22 3.5 22H20.5C20.8978 22 21.2794 21.842 21.5607 21.5607C21.842 21.2794 22 20.8978 22 20.5V13.5C22 13.3674 21.9473 13.2402 21.8536 13.1464C21.7598 13.0527 21.6326 13 21.5 13Z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  );\n});\n"
  },
  {
    "path": "src/components/Icon/IconNote.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport {memo} from 'react';\n\nexport const IconNote = memo<JSX.IntrinsicElements['svg']>(function IconNote({\n  className,\n}) {\n  return (\n    <svg\n      className={className}\n      width=\"2em\"\n      height=\"2em\"\n      viewBox=\"0 0 72 72\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\">\n      <g clipPath=\"url(#clip0_40_48064)\">\n        <path\n          d=\"M24 27C24 25.3431 25.3431 24 27 24H45C46.6569 24 48 25.3431 48 27C48 28.6569 46.6569 30 45 30H27C25.3431 30 24 28.6569 24 27Z\"\n          fill=\"currentColor\"\n        />\n        <path\n          d=\"M24 39C24 37.3431 25.3431 36 27 36H39C40.6569 36 42 37.3431 42 39C42 40.6569 40.6569 42 39 42H27C25.3431 42 24 40.6569 24 39Z\"\n          fill=\"currentColor\"\n        />\n        <path\n          fillRule=\"evenodd\"\n          clipRule=\"evenodd\"\n          d=\"M12 18C12 13.0294 16.0294 9 21 9H51C55.9706 9 60 13.0294 60 18V54C60 58.9706 55.9706 63 51 63H21C16.0294 63 12 58.9706 12 54V18ZM21 15H51C52.6569 15 54 16.3431 54 18V54C54 55.6569 52.6569 57 51 57H21C19.3431 57 18 55.6569 18 54V18C18 16.3431 19.3431 15 21 15Z\"\n          fill=\"currentColor\"\n        />\n      </g>\n      <defs>\n        <clipPath id=\"clip0_40_48064\">\n          <rect width=\"72\" height=\"72\" fill=\"white\" />\n        </clipPath>\n      </defs>\n    </svg>\n  );\n});\n"
  },
  {
    "path": "src/components/Icon/IconPitfall.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport {memo} from 'react';\n\nexport const IconPitfall = memo<JSX.IntrinsicElements['svg']>(\n  function IconPitfall({className}) {\n    return (\n      <svg\n        className={className}\n        width=\"2em\"\n        height=\"2em\"\n        viewBox=\"0 0 72 72\"\n        fill=\"none\"\n        xmlns=\"http://www.w3.org/2000/svg\">\n        <g clipPath=\"url(#clip0_738_836)\">\n          <path\n            fillRule=\"evenodd\"\n            clipRule=\"evenodd\"\n            d=\"M27 48L27 57.3409L40.0772 48L55.6975 48C57.1595 48 58.1986 47.0112 58.3851 45.8604C59.1824 40.9398 60 34.619 60 29.625C60 24.7282 59.2125 18.7546 58.4302 14.0813C58.2445 12.9721 57.2326 12 55.7805 12L16.2195 12C14.7674 12 13.7555 12.9721 13.5698 14.0813C12.7875 18.7546 12 24.7282 12 29.625C12 34.619 12.8176 40.9398 13.6149 45.8604C13.8014 47.0112 14.8404 48 16.3025 48H27ZM42 54H55.6975C59.9534 54 63.6271 51.0213 64.3078 46.8201C65.1161 41.8322 66 35.1209 66 29.625C66 24.2196 65.1449 17.8522 64.3478 13.0906C63.6513 8.93026 59.9987 6 55.7805 6H16.2195C12.0013 6 8.34867 8.93026 7.65218 13.0906C6.85505 17.8522 6 24.2196 6 29.625C6 35.1209 6.88391 41.8322 7.69215 46.8201C8.37291 51.0213 12.0466 54 16.3025 54H21L21 63.1704C21 65.6106 23.7581 67.0299 25.7437 65.6116L42 54ZM39 39.3686C39 40.9422 38 41.9912 36 41.9912C34 41.9912 33 40.9422 33 39.3686C33 37.7951 34 36.746 36 36.746C38 36.746 39 37.7951 39 39.3686ZM38.1771 20.2412C38.1771 18.9986 37.1697 17.9912 35.9271 17.9912C34.6845 17.9912 33.6771 18.9986 33.6771 20.2412V31.5956C33.6771 32.8382 34.6845 33.8456 35.9271 33.8456C37.1697 33.8456 38.1771 32.8382 38.1771 31.5956V20.2412Z\"\n            fill=\"currentColor\"\n          />\n        </g>\n        <defs>\n          <clipPath id=\"clip0_738_836\">\n            <rect width=\"72\" height=\"72\" fill=\"white\" />\n          </clipPath>\n        </defs>\n      </svg>\n    );\n  }\n);\n"
  },
  {
    "path": "src/components/Icon/IconRestart.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport {memo} from 'react';\n\nexport const IconRestart = memo<JSX.IntrinsicElements['svg']>(\n  function IconRestart({className}) {\n    return (\n      <svg\n        width=\"1em\"\n        height=\"1em\"\n        viewBox=\"0 0 24 24\"\n        fill=\"none\"\n        xmlns=\"http://www.w3.org/2000/svg\"\n        className={className}>\n        <path\n          d=\"M13.8982 5.20844C12.4626 4.88688 10.9686 4.93769 9.55821 5.35604L11.8524 3.06184C11.8989 3.0154 11.9357 2.96028 11.9608 2.89961C11.986 2.83894 11.9989 2.77391 11.9989 2.70824C11.9989 2.64256 11.986 2.57754 11.9608 2.51686C11.9357 2.45619 11.8989 2.40107 11.8524 2.35464L11.1456 1.64784C11.0992 1.60139 11.0441 1.56455 10.9834 1.53942C10.9227 1.51428 10.8577 1.50134 10.792 1.50134C10.7263 1.50134 10.6613 1.51428 10.6006 1.53942C10.54 1.56455 10.4848 1.60139 10.4384 1.64784L6.14571 5.94054C6.00654 6.07969 5.89615 6.2449 5.82083 6.42673C5.74551 6.60855 5.70675 6.80343 5.70675 7.00024C5.70675 7.19704 5.74551 7.39192 5.82083 7.57374C5.89615 7.75557 6.00654 7.92078 6.14571 8.05994L10.4387 12.3529C10.5325 12.4465 10.6595 12.4991 10.792 12.4991C10.9245 12.4991 11.0516 12.4465 11.1453 12.3529L11.8527 11.6455C11.9463 11.5518 11.9989 11.4247 11.9989 11.2922C11.9989 11.1598 11.9463 11.0327 11.8527 10.9389L8.77481 7.86104C9.99795 7.16236 11.415 6.8801 12.8125 7.05678C14.21 7.23347 15.5122 7.85953 16.523 8.84064C17.5338 9.82176 18.1983 11.1048 18.4165 12.4964C18.6347 13.888 18.3947 15.3129 17.7328 16.5562C17.0708 17.7996 16.0227 18.7942 14.7463 19.3902C13.47 19.9861 12.0345 20.1511 10.6563 19.8603C9.27798 19.5695 8.03152 18.8387 7.10469 17.778C6.17786 16.7172 5.62086 15.384 5.51761 13.9791C5.51156 13.8512 5.45689 13.7303 5.36477 13.6413C5.27265 13.5522 5.15001 13.5017 5.02191 13.5H4.02081C3.95297 13.4996 3.88574 13.5129 3.8232 13.5392C3.76065 13.5655 3.70408 13.6042 3.6569 13.6529C3.60972 13.7017 3.57291 13.7595 3.54869 13.8228C3.52448 13.8862 3.51336 13.9538 3.51601 14.0216C3.61349 15.5965 4.1473 17.1132 5.0577 18.4019C5.9681 19.6906 7.21917 20.7006 8.6709 21.3188C10.1226 21.937 11.7178 22.139 13.2778 21.9022C14.8378 21.6654 16.3011 20.9992 17.504 19.978C18.7069 18.9569 19.6019 17.6212 20.0889 16.1203C20.5759 14.6195 20.6356 13.0128 20.2614 11.4799C19.8872 9.94705 19.0938 8.54858 17.97 7.44098C16.8462 6.33339 15.4363 5.56037 13.8982 5.20844V5.20844Z\"\n          fill=\"currentColor\"\n        />\n      </svg>\n    );\n  }\n);\n"
  },
  {
    "path": "src/components/Icon/IconRocket.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport {memo} from 'react';\n\nexport const IconRocket = memo<\n  JSX.IntrinsicElements['svg'] & {title?: string; size?: 's' | 'md'}\n>(function IconRocket({className, size = 'md'}) {\n  return (\n    <svg\n      className={className}\n      aria-hidden=\"true\"\n      width={size === 's' ? '1.2em' : '1.5em'}\n      height={size === 's' ? '1.2em' : '1.5em'}\n      fill=\"currentColor\"\n      version=\"1.1\"\n      viewBox=\"0 0 1200 1200\"\n      xmlns=\"http://www.w3.org/2000/svg\">\n      <g fillRule=\"evenodd\">\n        <path d=\"m911.8 288.2c65.051 65.051 65.051 170.6 0 235.65-65.051 65.051-170.6 65.051-235.65 0-65.051-65.051-65.051-170.6 0-235.65 65.051-65.051 170.6-65.051 235.65 0zm-53.051 53.051c-35.75-35.801-93.801-35.801-129.55 0-35.801 35.75-35.801 93.801 0 129.55 35.75 35.801 93.801 35.801 129.55 0 35.801-35.75 35.801-93.801 0-129.55z\" />\n        <path d=\"m1122.2 103.4s96.648 328.1-194.4 619.1c-130.75 130.75-303.25 226.75-440.75 250.5-12.102 2.0508-24.449-1.8984-33.102-10.648l-231.55-234.8c-8.6484-8.8008-12.449-21.301-10.102-33.398 26.102-135.4 135.45-292.2 265.2-421.95 291-291.05 619.1-194.4 619.1-194.4 12.352 3.6016 22 13.25 25.602 25.602zm-67.5 41.898c-70.898-12.898-308.6-35.602-524.15 179.9-112.35 112.35-210.4 245.4-240.4 364.25 0 0 203.05 205.9 203.1 205.9 121.75-26.852 268.4-112.75 381.55-225.9 215.5-215.55 192.8-453.25 179.9-524.15z\" />\n        <path d=\"m151.55 543.85 124 20.648c20.398 3.3984 34.25 22.75 30.801 43.148-3.3984 20.449-22.699 34.25-43.148 30.852l-144.35-24.051c-22.148-3.6992-40.699-18.949-48.602-40-7.9492-21.051-4.0508-44.699 10.199-62.148l122.85-150.15c15.051-18.398 36.898-30 60.551-32.148l179.55-16.301c20.602-1.8984 38.852 13.352 40.75 33.949 1.8516 20.602-13.352 38.852-33.949 40.75l-179.55 16.301c-3.6484 0.35156-7 2.1016-9.3008 4.9492z\" />\n        <path d=\"m656.15 1048.4 134.2-109.8c2.8516-2.3008 4.6016-5.6484 4.9492-9.3008l16.301-179.55c1.8984-20.602 20.148-35.801 40.75-33.949 20.602 1.8984 35.852 20.148 33.949 40.75l-16.301 179.55c-2.1484 23.648-13.75 45.5-32.148 60.551l-150.15 122.85c-17.449 14.25-41.102 18.148-62.148 10.199-21.051-7.8984-36.301-26.449-40-48.602l-29.25-175.7c-3.3984-20.398 10.398-39.75 30.801-43.148 20.449-3.3984 39.75 10.449 43.148 30.852l25.898 155.3z\" />\n        <path d=\"m310.9 560.4c-14.648-14.648-14.648-38.398 0-53.051 14.648-14.648 38.398-14.648 53.051 0l328.7 328.7c14.648 14.648 14.648 38.398 0 53.051-14.648 14.648-38.398 14.648-53.051 0z\" />\n        <path d=\"m383.95 982.15c14.648-14.602 38.398-14.602 53.051 0 14.602 14.648 14.602 38.398 0 53.051l-91.352 91.301c-14.602 14.648-38.398 14.648-53 0-14.648-14.602-14.648-38.398 0-53z\" />\n        <path d=\"m237.85 909.1c14.648-14.602 38.398-14.602 53.051 0 14.602 14.648 14.602 38.398 0 53.051l-127.85 127.85c-14.648 14.648-38.398 14.648-53.051 0-14.648-14.648-14.648-38.398 0-53.051z\" />\n        <path d=\"m164.8 763c14.648-14.602 38.398-14.602 53.051 0 14.602 14.648 14.602 38.398 0 53.051l-91.352 91.301c-14.602 14.648-38.398 14.648-53 0-14.648-14.602-14.648-38.398 0-53z\" />\n      </g>\n    </svg>\n  );\n});\n"
  },
  {
    "path": "src/components/Icon/IconRss.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport {memo} from 'react';\nimport type {SVGProps} from 'react';\n\nexport const IconRss = memo<SVGProps<SVGSVGElement>>(function IconRss(props) {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width=\"1em\"\n      height=\"1em\"\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth={2}\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      {...props}>\n      <path d=\"M4 11a9 9 0 0 1 9 9\" />\n      <path d=\"M4 4a16 16 0 0 1 16 16\" />\n      <circle cx={5} cy={19} r={1} />\n    </svg>\n  );\n});\n"
  },
  {
    "path": "src/components/Icon/IconSearch.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport {memo} from 'react';\nimport type {SVGProps} from 'react';\n\nexport const IconSearch = memo<SVGProps<SVGSVGElement>>(function IconSearch(\n  props\n) {\n  return (\n    <svg width=\"1em\" height=\"1em\" viewBox=\"0 0 20 20\" {...props}>\n      <path\n        d=\"M14.386 14.386l4.0877 4.0877-4.0877-4.0877c-2.9418 2.9419-7.7115 2.9419-10.6533 0-2.9419-2.9418-2.9419-7.7115 0-10.6533 2.9418-2.9419 7.7115-2.9419 10.6533 0 2.9419 2.9418 2.9419 7.7115 0 10.6533z\"\n        stroke=\"currentColor\"\n        fill=\"none\"\n        strokeWidth=\"2\"\n        fillRule=\"evenodd\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"></path>\n    </svg>\n  );\n});\n"
  },
  {
    "path": "src/components/Icon/IconSolution.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport {memo} from 'react';\nimport cn from 'classnames';\n\nexport const IconSolution = memo<JSX.IntrinsicElements['svg']>(\n  function IconSolution({className}) {\n    return (\n      <svg\n        className={cn('inline', className)}\n        width=\"0.75em\"\n        height=\"0.75em\"\n        viewBox=\"0 0 13 13\"\n        xmlns=\"http://www.w3.org/2000/svg\">\n        <path\n          d=\"M2.21908 8.74479V12.7448H0.885742V0.078125H7.14041C7.26418 0.0781911 7.3855 0.112714 7.49076 0.177827C7.59602 0.242939 7.68108 0.336071 7.73641 0.446792L8.21908 1.41146H12.2191C12.3959 1.41146 12.5655 1.4817 12.6905 1.60672C12.8155 1.73174 12.8857 1.90131 12.8857 2.07812V9.41146C12.8857 9.58827 12.8155 9.75784 12.6905 9.88286C12.5655 10.0079 12.3959 10.0781 12.2191 10.0781H7.96441C7.84063 10.0781 7.71932 10.0435 7.61406 9.97842C7.50879 9.91331 7.42374 9.82018 7.36841 9.70946L6.88574 8.74479H2.21908ZM2.21908 1.41146V7.41146H7.70974L8.37641 8.74479H11.5524V2.74479H7.39508L6.72841 1.41146H2.21908Z\"\n          fill=\"currentColor\"\n        />\n      </svg>\n    );\n  }\n);\n"
  },
  {
    "path": "src/components/Icon/IconTerminal.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport {memo} from 'react';\n\nexport const IconTerminal = memo<JSX.IntrinsicElements['svg']>(\n  function IconTerminal({className}) {\n    return (\n      <svg\n        className={className}\n        width=\"1em\"\n        height=\"1em\"\n        viewBox=\"0 0 18 18\"\n        fill=\"none\"\n        xmlns=\"http://www.w3.org/2000/svg\">\n        <path\n          d=\"M2.40299 2.61279H14.403C14.5798 2.61279 14.7494 2.68303 14.8744 2.80806C14.9994 2.93308 15.0697 3.10265 15.0697 3.27946V13.9461C15.0697 14.1229 14.9994 14.2925 14.8744 14.4175C14.7494 14.5426 14.5798 14.6128 14.403 14.6128H2.40299C2.22618 14.6128 2.05661 14.5426 1.93159 14.4175C1.80657 14.2925 1.73633 14.1229 1.73633 13.9461V3.27946C1.73633 3.10265 1.80657 2.93308 1.93159 2.80806C2.05661 2.68303 2.22618 2.61279 2.40299 2.61279ZM8.403 10.6128V11.9461H12.403V10.6128H8.403ZM6.01233 8.61279L4.12699 10.4981L5.06966 11.4415L7.89833 8.61279L5.06966 5.78413L4.12699 6.72746L6.01233 8.61279Z\"\n          fill=\"currentColor\"\n        />\n      </svg>\n    );\n  }\n);\n"
  },
  {
    "path": "src/components/Icon/IconThreads.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport {memo} from 'react';\nimport type {SVGProps} from 'react';\n\nexport const IconThreads = memo<SVGProps<SVGSVGElement>>(function IconThreads(\n  props\n) {\n  return (\n    <svg\n      aria-label=\"Threads\"\n      viewBox=\"0 0 192 192\"\n      height=\"1.40em\"\n      width=\"1.40em\"\n      fill=\"currentColor\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      {...props}>\n      <path\n        className=\"x19hqcy\"\n        d=\"M141.537 88.9883C140.71 88.5919 139.87 88.2104 139.019 87.8451C137.537 60.5382 122.616 44.905 97.5619 44.745C97.4484 44.7443 97.3355 44.7443 97.222 44.7443C82.2364 44.7443 69.7731 51.1409 62.102 62.7807L75.881 72.2328C81.6116 63.5383 90.6052 61.6848 97.2286 61.6848C97.3051 61.6848 97.3819 61.6848 97.4576 61.6855C105.707 61.7381 111.932 64.1366 115.961 68.814C118.893 72.2193 120.854 76.925 121.825 82.8638C114.511 81.6207 106.601 81.2385 98.145 81.7233C74.3247 83.0954 59.0111 96.9879 60.0396 116.292C60.5615 126.084 65.4397 134.508 73.775 140.011C80.8224 144.663 89.899 146.938 99.3323 146.423C111.79 145.74 121.563 140.987 128.381 132.296C133.559 125.696 136.834 117.143 138.28 106.366C144.217 109.949 148.617 114.664 151.047 120.332C155.179 129.967 155.42 145.8 142.501 158.708C131.182 170.016 117.576 174.908 97.0135 175.059C74.2042 174.89 56.9538 167.575 45.7381 153.317C35.2355 139.966 29.8077 120.682 29.6052 96C29.8077 71.3178 35.2355 52.0336 45.7381 38.6827C56.9538 24.4249 74.2039 17.11 97.0132 16.9405C119.988 17.1113 137.539 24.4614 149.184 38.788C154.894 45.8136 159.199 54.6488 162.037 64.9503L178.184 60.6422C174.744 47.9622 169.331 37.0357 161.965 27.974C147.036 9.60668 125.202 0.195148 97.0695 0H96.9569C68.8816 0.19447 47.2921 9.6418 32.7883 28.0793C19.8819 44.4864 13.2244 67.3157 13.0007 95.9325L13 96L13.0007 96.0675C13.2244 124.684 19.8819 147.514 32.7883 163.921C47.2921 182.358 68.8816 191.806 96.9569 192H97.0695C122.03 191.827 139.624 185.292 154.118 170.811C173.081 151.866 172.51 128.119 166.26 113.541C161.776 103.087 153.227 94.5962 141.537 88.9883ZM98.4405 129.507C88.0005 130.095 77.1544 125.409 76.6196 115.372C76.2232 107.93 81.9158 99.626 99.0812 98.6368C101.047 98.5234 102.976 98.468 104.871 98.468C111.106 98.468 116.939 99.0737 122.242 100.233C120.264 124.935 108.662 128.946 98.4405 129.507Z\"></path>\n    </svg>\n  );\n});\n"
  },
  {
    "path": "src/components/Icon/IconTwitter.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport {memo} from 'react';\nimport type {SVGProps} from 'react';\n\nexport const IconTwitter = memo<SVGProps<SVGSVGElement>>(function IconTwitter(\n  props\n) {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      viewBox=\"0 0 512 512\"\n      height=\"1.30em\"\n      width=\"1.30em\"\n      fill=\"currentColor\"\n      {...props}>\n      <path fill=\"none\" d=\"M0 0h24v24H0z\" />\n      <path d=\"M389.2 48h70.6L305.6 224.2 487 464H345L233.7 318.6 106.5 464H35.8L200.7 275.5 26.8 48H172.4L272.9 180.9 389.2 48zM364.4 421.8h39.1L151.1 88h-42L364.4 421.8z\" />\n    </svg>\n  );\n});\n"
  },
  {
    "path": "src/components/Icon/IconWarning.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport {memo} from 'react';\n\nexport const IconWarning = memo<JSX.IntrinsicElements['svg']>(\n  function IconWarning({className}) {\n    return (\n      <svg\n        className={className}\n        width=\"2em\"\n        height=\"2em\"\n        viewBox=\"0 0 72 72\"\n        xmlns=\"http://www.w3.org/2000/svg\">\n        <g>\n          <path\n            fillRule=\"evenodd\"\n            clipRule=\"evenodd\"\n            d=\"M36 63C50.9117 63 63 50.9117 63 36C63 21.0883 50.9117 9 36 9C21.0883 9 9 21.0883 9 36C9 50.9117 21.0883 63 36 63ZM36 69C54.2254 69 69 54.2254 69 36C69 17.7746 54.2254 3 36 3C17.7746 3 3 17.7746 3 36C3 54.2254 17.7746 69 36 69ZM39.7515 47.9926C39.7515 49.7926 38.5015 50.9926 36.0015 50.9926C33.5015 50.9926 32.2515 49.7926 32.2515 47.9926C32.2515 46.1926 33.5015 44.9926 36.0015 44.9926C38.5015 44.9926 39.7515 46.1926 39.7515 47.9926ZM38.6265 23.6199C38.6265 22.1701 37.4512 20.9949 36.0015 20.9949C34.5517 20.9949 33.3765 22.1701 33.3765 23.6199V38.5443C33.3765 39.9941 34.5517 41.1693 36.0015 41.1693C37.4512 41.1693 38.6265 39.9941 38.6265 38.5443V23.6199Z\"\n            fill=\"currentColor\"\n          />\n        </g>\n      </svg>\n    );\n  }\n);\n"
  },
  {
    "path": "src/components/Layout/Feedback.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport {useState} from 'react';\nimport {useRouter} from 'next/router';\nimport cn from 'classnames';\n\nexport function Feedback({onSubmit = () => {}}: {onSubmit?: () => void}) {\n  const {asPath} = useRouter();\n  const cleanedPath = asPath.split(/[\\?\\#]/)[0];\n  // Reset on route changes.\n  return <SendFeedback key={cleanedPath} onSubmit={onSubmit} />;\n}\n\nconst thumbsUpIcon = (\n  <svg\n    width=\"16\"\n    height=\"18\"\n    viewBox=\"0 0 16 18\"\n    fill=\"none\"\n    xmlns=\"http://www.w3.org/2000/svg\">\n    <path\n      fillRule=\"evenodd\"\n      clipRule=\"evenodd\"\n      d=\"M9.36603 0.384603C9.36605 0.384617 9.36601 0.384588 9.36603 0.384603L9.45902 0.453415C9.99732 0.851783 10.3873 1.42386 10.5654 2.07648C10.7435 2.72909 10.6993 3.42385 10.44 4.04763L9.27065 6.86008H12.6316C13.5249 6.86008 14.3817 7.22121 15.0134 7.86402C15.6451 8.50683 16 9.37868 16 10.2877V13.7154C16 14.8518 15.5564 15.9416 14.7668 16.7451C13.9771 17.5486 12.9062 18 11.7895 18H5.05263C3.71259 18 2.42743 17.4583 1.47988 16.4941C0.532325 15.5299 0 14.2221 0 12.8585V11.2511C2.40928e-06 9.87711 0.463526 8.54479 1.31308 7.47688L6.66804 0.745592C6.98662 0.345136 7.44414 0.08434 7.94623 0.0171605C8.4483 -0.0500155 8.95656 0.0815891 9.36603 0.384603ZM8.37542 1.77064C8.31492 1.72587 8.23987 1.70646 8.16579 1.71637C8.09171 1.72628 8.02415 1.76477 7.97708 1.82393L2.62213 8.55522C2.0153 9.31801 1.68421 10.2697 1.68421 11.2511V12.8585C1.68421 13.7676 2.03909 14.6394 2.67079 15.2822C3.30249 15.925 4.15927 16.2862 5.05263 16.2862H11.7895C12.4595 16.2862 13.1021 16.0153 13.5759 15.5332C14.0496 15.0511 14.3158 14.3972 14.3158 13.7154V10.2877C14.3158 9.83321 14.1383 9.39729 13.8225 9.07588C13.5066 8.75448 13.0783 8.57392 12.6316 8.57392H8C7.71763 8.57392 7.45405 8.4299 7.29806 8.19039C7.14206 7.95087 7.11442 7.64774 7.22445 7.38311L8.88886 3.37986C9 3.11253 9.01896 2.81477 8.94262 2.53507C8.8663 2.25541 8.69921 2.01027 8.46853 1.83954L8.37542 1.77064Z\"\n      fill=\"currentColor\"\n    />\n  </svg>\n);\n\nconst thumbsDownIcon = (\n  <svg\n    width=\"16\"\n    height=\"18\"\n    viewBox=\"0 0 16 18\"\n    fill=\"none\"\n    xmlns=\"http://www.w3.org/2000/svg\">\n    <path\n      fillRule=\"evenodd\"\n      clipRule=\"evenodd\"\n      d=\"M6.63397 17.6154C6.63395 17.6154 6.63399 17.6154 6.63397 17.6154L6.54098 17.5466C6.00268 17.1482 5.61269 16.5761 5.43458 15.9235C5.25648 15.2709 5.30069 14.5761 5.56004 13.9524L6.72935 11.1399L3.36842 11.1399C2.47506 11.1399 1.61829 10.7788 0.986585 10.136C0.354883 9.49316 8.1991e-07 8.62132 8.99384e-07 7.71225L1.19904e-06 4.28458C1.29838e-06 3.14824 0.443605 2.05844 1.23323 1.25492C2.02286 0.451403 3.09383 -1.12829e-06 4.21053 -1.03067e-06L10.9474 -4.41715e-07C12.2874 -3.24565e-07 13.5726 0.541687 14.5201 1.50591C15.4677 2.47013 16 3.77789 16 5.1415L16 6.74893C16 8.12289 15.5365 9.45521 14.6869 10.5231L9.33196 17.2544C9.01338 17.6549 8.55586 17.9157 8.05377 17.9828C7.5517 18.05 7.04344 17.9184 6.63397 17.6154ZM7.62458 16.2294C7.68508 16.2741 7.76013 16.2935 7.83421 16.2836C7.90829 16.2737 7.97585 16.2352 8.02292 16.1761L13.3779 9.44478C13.9847 8.68199 14.3158 7.73033 14.3158 6.74892L14.3158 5.1415C14.3158 4.23242 13.9609 3.36058 13.3292 2.71777C12.6975 2.07496 11.8407 1.71383 10.9474 1.71383L4.21053 1.71383C3.5405 1.71383 2.89793 1.98468 2.42415 2.46679C1.95038 2.94889 1.68421 3.60277 1.68421 4.28458L1.68421 7.71225C1.68421 8.16679 1.86166 8.60271 2.1775 8.92411C2.49335 9.24552 2.92174 9.42608 3.36842 9.42608L8 9.42608C8.28237 9.42608 8.54595 9.5701 8.70195 9.80961C8.85794 10.0491 8.88558 10.3523 8.77555 10.6169L7.11114 14.6201C7 14.8875 6.98105 15.1852 7.05738 15.4649C7.1337 15.7446 7.30079 15.9897 7.53147 16.1605L7.62458 16.2294Z\"\n      fill=\"currentColor\"\n    />\n  </svg>\n);\n\nfunction sendGAEvent(isPositive: boolean) {\n  const category = isPositive ? 'like_button' : 'dislike_button';\n  const value = isPositive ? 1 : 0;\n  // Fragile. Don't change unless you've tested the network payload\n  // and verified that the right events actually show up in GA.\n  // @ts-ignore\n  gtag('event', 'feedback', {\n    event_category: category,\n    event_label: window.location.pathname,\n    event_value: value,\n  });\n}\n\nfunction SendFeedback({onSubmit}: {onSubmit: () => void}) {\n  const [isSubmitted, setIsSubmitted] = useState(false);\n  return (\n    <div\n      className={cn(\n        'max-w-96 w-80 lg:w-auto py-3 shadow-lg rounded-lg m-4 bg-wash dark:bg-gray-95 px-4 flex',\n        {exit: isSubmitted}\n      )}>\n      <p className=\"w-full text-lg font-bold text-primary dark:text-primary-dark me-4\">\n        {isSubmitted\n          ? '피드백을 보내주셔서 감사합니다!'\n          : '이 페이지가 도움이 되었나요?'}\n      </p>\n      {!isSubmitted && (\n        <button\n          aria-label=\"Yes\"\n          className=\"px-3 rounded-lg bg-secondary-button dark:bg-secondary-button-dark text-primary dark:text-primary-dark me-2\"\n          onClick={() => {\n            setIsSubmitted(true);\n            onSubmit();\n            sendGAEvent(true);\n          }}>\n          {thumbsUpIcon}\n        </button>\n      )}\n      {!isSubmitted && (\n        <button\n          aria-label=\"No\"\n          className=\"px-3 rounded-lg bg-secondary-button dark:bg-secondary-button-dark text-primary dark:text-primary-dark\"\n          onClick={() => {\n            setIsSubmitted(true);\n            onSubmit();\n            sendGAEvent(false);\n          }}>\n          {thumbsDownIcon}\n        </button>\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/components/Layout/Footer.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport * as React from 'react';\nimport NextLink from 'next/link';\nimport cn from 'classnames';\nimport {ExternalLink} from 'components/ExternalLink';\nimport {IconFacebookCircle} from 'components/Icon/IconFacebookCircle';\nimport {IconTwitter} from 'components/Icon/IconTwitter';\nimport {IconBsky} from 'components/Icon/IconBsky';\nimport {IconGitHub} from 'components/Icon/IconGitHub';\n\nexport function Footer() {\n  const socialLinkClasses = 'hover:text-primary dark:text-primary-dark';\n  return (\n    <footer className={cn('text-secondary dark:text-secondary-dark')}>\n      <div className=\"grid grid-cols-2 md:grid-cols-3 xl:grid-cols-5 gap-x-12 gap-y-8 max-w-7xl mx-auto\">\n        <div className=\"col-span-2 md:col-span-1 justify-items-start mt-3.5\">\n          <ExternalLink\n            href=\"https://opensource.fb.com/\"\n            aria-label=\"Meta Open Source\">\n            <div>\n              <svg\n                width=\"160\"\n                height=\"19\"\n                viewBox=\"0 0 160 19\"\n                fill=\"none\"\n                xmlns=\"http://www.w3.org/2000/svg\"\n                className=\"text-primary dark:text-primary-dark\">\n                <path\n                  d=\"M22.0605 3.62598H24.3349L28.202 10.6212L32.0691 3.62598H34.2942V15.1206H32.4387V6.31077L29.0476 12.4111H27.307L23.9162 6.31077V15.1206H22.0605V3.62598Z\"\n                  fill=\"currentColor\"\n                />\n                <path\n                  d=\"M40.2785 15.3259C39.4191 15.3259 38.6638 15.1357 38.0124 14.7554C37.367 14.3812 36.8394 13.8336 36.4895 13.1747C36.1253 12.5015 35.9433 11.7297 35.9434 10.8594C35.9434 9.97825 36.1213 9.19824 36.4771 8.5194C36.8329 7.84077 37.3269 7.30982 37.9592 6.92653C38.5913 6.54347 39.3179 6.3519 40.139 6.35181C40.9546 6.35181 41.6566 6.54477 42.2449 6.9307C42.8334 7.31658 43.2863 7.85713 43.6038 8.55232C43.9212 9.24748 44.08 10.063 44.0801 10.9989V11.5081H37.7826C37.8975 12.2088 38.1808 12.7602 38.6323 13.1625C39.0839 13.5648 39.6546 13.7659 40.3443 13.766C40.8971 13.766 41.3733 13.6839 41.7729 13.5196C42.1723 13.3554 42.5473 13.1063 42.8977 12.7724L43.8831 13.9794C42.9031 14.8771 41.7016 15.326 40.2785 15.3259ZM41.6334 8.50718C41.2447 8.11027 40.7356 7.91184 40.1062 7.91189C39.4931 7.91189 38.9799 8.11439 38.5667 8.51941C38.1533 8.92464 37.8919 9.46931 37.7826 10.1534H42.2984C42.2436 9.45273 42.0219 8.90398 41.6334 8.50716V8.50718Z\"\n                  fill=\"currentColor\"\n                />\n                <path\n                  d=\"M46.3308 8.07609H44.623V6.55715H46.3308V4.04468H48.1209V6.55715H50.7153V8.07609H48.1209V11.9267C48.1209 12.5672 48.2303 13.0243 48.4492 13.298C48.6682 13.5717 49.0431 13.7086 49.5741 13.7084C49.7742 13.7102 49.9743 13.7006 50.1734 13.6797C50.3376 13.6606 50.5183 13.6346 50.7153 13.6017V15.1043C50.4905 15.1692 50.2614 15.2186 50.0297 15.252C49.7647 15.2911 49.4971 15.3103 49.2292 15.3095C47.2969 15.3095 46.3308 14.2531 46.3308 12.1403L46.3308 8.07609Z\"\n                  fill=\"currentColor\"\n                />\n                <path\n                  d=\"M60.0415 15.1207H58.2844V13.9219C57.9815 14.3629 57.572 14.7202 57.094 14.9606C56.6123 15.204 56.0649 15.3258 55.4519 15.3259C54.6966 15.3259 54.0274 15.133 53.4444 14.7472C52.8614 14.3611 52.4029 13.8302 52.0692 13.1543C51.7353 12.4784 51.5684 11.7052 51.5684 10.8348C51.5684 9.95904 51.738 9.1845 52.0774 8.5112C52.4167 7.83795 52.8861 7.30972 53.4855 6.92653C54.0847 6.54347 54.773 6.3519 55.5503 6.35181C56.1361 6.35181 56.6616 6.46538 57.1269 6.69253C57.5858 6.91465 57.9833 7.24591 58.2844 7.65731V6.55718H60.0415V15.1207ZM58.2516 9.55395C58.06 9.06686 57.7576 8.68232 57.3444 8.40033C56.9311 8.11861 56.4535 7.97771 55.9116 7.97762C55.1452 7.97762 54.5349 8.23487 54.0807 8.74939C53.6264 9.2639 53.3993 9.95905 53.3993 10.8349C53.3993 11.7162 53.6182 12.4141 54.0561 12.9285C54.4939 13.443 55.0877 13.7003 55.8377 13.7003C56.3906 13.7003 56.8833 13.5579 57.3156 13.2733C57.7405 12.9979 58.068 12.5957 58.2516 12.1238V9.55395Z\"\n                  fill=\"currentColor\"\n                />\n                <path\n                  d=\"M64.4113 11.7585C64.1266 11.0332 63.9843 10.2382 63.9844 9.3733C63.9844 8.50853 64.1267 7.71351 64.4113 6.98824C64.6823 6.28392 65.0929 5.6416 65.6182 5.09981C66.1399 4.56496 66.7659 4.14298 67.4573 3.86003C68.1634 3.56716 68.9379 3.4207 69.7808 3.42065C70.6238 3.42065 71.3984 3.56711 72.1045 3.86003C72.796 4.14302 73.422 4.56499 73.9437 5.09981C74.4689 5.64171 74.8795 6.284 75.1507 6.98824C75.4351 7.71351 75.5774 8.50853 75.5776 9.3733C75.5776 10.2382 75.4353 11.0333 75.1507 11.7585C74.8795 12.4627 74.4689 13.105 73.9437 13.6469C73.422 14.1818 72.796 14.6038 72.1045 14.8867C71.3984 15.1794 70.6239 15.3259 69.7808 15.3259C68.938 15.3259 68.1635 15.1795 67.4573 14.8867C66.7658 14.6038 66.1399 14.1818 65.6182 13.6469C65.0929 13.1051 64.6823 12.4628 64.4113 11.7585ZM73.6152 9.3733C73.6152 8.54697 73.451 7.81763 73.1226 7.18529C72.7942 6.55303 72.3413 6.05904 71.7637 5.70331C71.1862 5.34753 70.5252 5.16962 69.7808 5.16958C69.0365 5.16958 68.3756 5.34749 67.7981 5.70331C67.2205 6.05909 66.7676 6.55308 66.4392 7.18529C66.1108 7.81741 65.9466 8.54674 65.9466 9.3733C65.9466 10.1999 66.1108 10.9293 66.4392 11.5615C66.7677 12.1937 67.2206 12.6877 67.7981 13.0434C68.3755 13.3993 69.0364 13.5773 69.7808 13.5772C70.5252 13.5772 71.1862 13.3993 71.7637 13.0434C72.3413 12.6877 72.7942 12.1937 73.1226 11.5615C73.451 10.9292 73.6152 10.1998 73.6152 9.3733V9.3733Z\"\n                  fill=\"currentColor\"\n                />\n                <path\n                  d=\"M77.2188 6.55718H78.9839V7.74763C79.2856 7.30796 79.6938 6.95201 80.1705 6.71309C80.6492 6.47237 81.1952 6.35194 81.8084 6.35181C82.5637 6.35181 83.2342 6.54477 83.8199 6.9307C84.4056 7.31658 84.8641 7.84615 85.1952 8.5194C85.5263 9.19264 85.6919 9.96719 85.6919 10.843C85.6919 11.7133 85.5223 12.4865 85.1829 13.1625C84.8436 13.8386 84.3742 14.3681 83.7747 14.7512C83.1755 15.1343 82.4873 15.3258 81.71 15.3259C81.1353 15.3259 80.618 15.2165 80.1581 14.9976C79.7045 14.7837 79.31 14.4624 79.0087 14.0616V18.569H77.2188V6.55718ZM79.9159 13.2774C80.3291 13.5594 80.8067 13.7004 81.3487 13.7003C82.1148 13.7003 82.7251 13.443 83.1796 12.9285C83.6339 12.414 83.861 11.7188 83.861 10.843C83.861 9.96172 83.6421 9.26383 83.2042 8.74937C82.7662 8.23486 82.1723 7.9776 81.4226 7.9776C80.8697 7.9776 80.377 8.11989 79.9447 8.40448C79.5197 8.67987 79.1923 9.08202 79.0087 9.55393V12.1238C79.2002 12.611 79.5026 12.9956 79.9159 13.2774Z\"\n                  fill=\"currentColor\"\n                />\n                <path\n                  d=\"M91.177 15.3259C90.3176 15.3259 89.5622 15.1357 88.9109 14.7554C88.2654 14.3812 87.7377 13.8336 87.3878 13.1747C87.0236 12.5015 86.8417 11.7297 86.8418 10.8594C86.8418 9.97825 87.0197 9.19824 87.3754 8.5194C87.7312 7.84077 88.2252 7.30982 88.8574 6.92653C89.4896 6.54347 90.2163 6.3519 91.0373 6.35181C91.8528 6.35181 92.5548 6.54477 93.1434 6.9307C93.7317 7.31658 94.1846 7.85713 94.5022 8.55232C94.8196 9.24748 94.9782 10.063 94.9783 10.9989V11.5081H88.6809C88.7958 12.2088 89.0791 12.7602 89.5308 13.1625C89.9824 13.5648 90.553 13.7659 91.2426 13.766C91.7954 13.766 92.2716 13.6839 92.6712 13.5196C93.0708 13.3554 93.4457 13.1063 93.796 12.7724L94.7812 13.9794C93.8014 14.8771 92.6 15.326 91.177 15.3259ZM92.5315 8.50718C92.1428 8.11027 91.6338 7.91184 91.0044 7.91189C90.3914 7.91189 89.8782 8.11439 89.465 8.51941C89.0517 8.92464 88.7903 9.46931 88.6809 10.1534H93.1966C93.1419 9.45273 92.9202 8.90398 92.5315 8.50716V8.50718Z\"\n                  fill=\"currentColor\"\n                />\n                <path\n                  d=\"M96.4883 6.55718H98.2536V7.80515C98.9158 6.83621 99.8381 6.35176 101.021 6.35181C102.039 6.35181 102.821 6.66928 103.369 7.30422C103.916 7.93929 104.19 8.84793 104.19 10.0301V15.1207H102.4V10.2436C102.4 9.44454 102.258 8.85615 101.973 8.47842C101.688 8.10074 101.242 7.9119 100.635 7.9119C100.104 7.9119 99.6356 8.04872 99.2307 8.32238C98.8255 8.59617 98.508 8.97932 98.2783 9.47183V15.1207H96.4883L96.4883 6.55718Z\"\n                  fill=\"currentColor\"\n                />\n                <path\n                  d=\"M116.875 11.8694C116.875 12.9586 116.499 13.8071 115.746 14.4147C114.994 15.0222 113.914 15.3259 112.507 15.3259C111.451 15.3259 110.511 15.1262 109.687 14.7266C108.863 14.3272 108.221 13.7469 107.762 12.9859L109.157 11.8858C109.54 12.466 110.019 12.8971 110.594 13.1789C111.169 13.4609 111.829 13.6018 112.573 13.6018C113.323 13.6018 113.906 13.4594 114.322 13.1747C114.738 12.8902 114.946 12.5043 114.946 12.0171C114.946 11.5957 114.801 11.2468 114.511 10.9703C114.221 10.694 113.728 10.4846 113.033 10.3422L111.309 9.98094C109.196 9.54304 108.139 8.47567 108.139 6.77883C108.139 6.10558 108.315 5.51714 108.665 5.01352C109.015 4.51002 109.512 4.11867 110.155 3.83947C110.798 3.56031 111.552 3.4207 112.417 3.42065C114.338 3.42065 115.775 4.16509 116.727 5.65397L115.315 6.66391C114.976 6.14939 114.572 5.76759 114.104 5.51849C113.636 5.26943 113.068 5.14492 112.401 5.14497C111.662 5.14497 111.088 5.28041 110.681 5.55128C110.273 5.82225 110.069 6.20952 110.069 6.7131C110.069 7.09629 110.202 7.40557 110.467 7.64091C110.732 7.87626 111.196 8.0651 111.859 8.20744L113.583 8.56874C115.778 9.02855 116.875 10.1288 116.875 11.8694Z\"\n                  fill=\"currentColor\"\n                />\n                <path\n                  d=\"M118.677 13.1831C118.308 12.5097 118.123 11.7297 118.123 10.843C118.123 9.95083 118.308 9.16809 118.677 8.4948C119.034 7.83483 119.569 7.28851 120.221 6.91833C120.88 6.54065 121.645 6.3518 122.516 6.35181C123.386 6.35181 124.151 6.54065 124.81 6.91833C125.463 7.28862 125.998 7.83491 126.354 8.4948C126.724 9.16805 126.908 9.95079 126.908 10.843C126.908 11.7297 126.724 12.5097 126.354 13.1831C125.998 13.8429 125.463 14.3892 124.81 14.7594C124.151 15.1371 123.386 15.3259 122.516 15.3259C121.651 15.3259 120.887 15.1371 120.225 14.7594C119.571 14.3902 119.034 13.8438 118.677 13.1831ZM125.077 10.843C125.077 9.98377 124.843 9.29408 124.375 8.77396C123.907 8.25393 123.288 7.99394 122.516 7.994C121.744 7.994 121.124 8.25398 120.656 8.77396C120.188 9.29399 119.954 9.98368 119.954 10.843C119.954 11.6969 120.188 12.3839 120.656 12.9039C121.124 13.4239 121.744 13.6839 122.516 13.6839C123.288 13.6839 123.907 13.4239 124.375 12.9039C124.843 12.3839 125.077 11.6969 125.077 10.843Z\"\n                  fill=\"currentColor\"\n                />\n                <path\n                  d=\"M135.907 15.1206H134.141V13.8891C133.484 14.847 132.576 15.3259 131.415 15.3259C130.408 15.3259 129.635 15.0084 129.096 14.3735C128.557 13.7385 128.287 12.8299 128.287 11.6475V6.55713H130.077V11.4341C130.077 12.2278 130.217 12.8148 130.496 13.1953C130.775 13.5758 131.213 13.766 131.809 13.7659C132.324 13.7659 132.781 13.6305 133.18 13.3595C133.58 13.0885 133.892 12.7095 134.116 12.2223V6.55713H135.907L135.907 15.1206Z\"\n                  fill=\"currentColor\"\n                />\n                <path\n                  d=\"M137.877 6.55733H139.642V7.8709C140.195 6.91311 140.95 6.43417 141.908 6.43408C142.22 6.43408 142.475 6.46146 142.672 6.5162V8.16659C142.403 8.12858 142.131 8.10936 141.859 8.10907C140.786 8.10907 140.055 8.59074 139.667 9.55408V15.1208H137.877L137.877 6.55733Z\"\n                  fill=\"currentColor\"\n                />\n                <path\n                  d=\"M143.737 8.49063C144.079 7.83229 144.601 7.28535 145.244 6.9143C145.889 6.53944 146.645 6.35194 147.51 6.35181C149.119 6.35181 150.318 6.98952 151.106 8.26496L149.71 9.20918C149.431 8.78768 149.119 8.47975 148.774 8.2854C148.429 8.09118 148.013 7.99405 147.526 7.994C146.782 7.994 146.18 8.25534 145.72 8.778C145.26 9.30085 145.03 9.98647 145.03 10.8348C145.03 11.727 145.255 12.4249 145.703 12.9285C146.152 13.4321 146.785 13.6839 147.6 13.6839C148.034 13.6874 148.463 13.5845 148.848 13.3841C149.218 13.1962 149.536 12.9215 149.776 12.5836L151.024 13.6839C150.186 14.7786 149.031 15.326 147.559 15.3259C146.678 15.3259 145.91 15.1412 145.256 14.7718C144.609 14.4099 144.081 13.8679 143.737 13.2117C143.378 12.5413 143.199 11.7544 143.199 10.8512C143.199 9.95352 143.378 9.16666 143.737 8.49063Z\"\n                  fill=\"currentColor\"\n                />\n                <path\n                  d=\"M156.195 15.3259C155.335 15.3259 154.58 15.1357 153.928 14.7554C153.283 14.3812 152.755 13.8336 152.405 13.1747C152.041 12.5015 151.859 11.7297 151.859 10.8594C151.859 9.97825 152.037 9.19824 152.393 8.5194C152.749 7.84077 153.243 7.30982 153.875 6.92653C154.507 6.54347 155.234 6.3519 156.055 6.35181C156.87 6.35181 157.572 6.54477 158.161 6.9307C158.749 7.31658 159.202 7.85713 159.52 8.55232C159.837 9.24748 159.996 10.063 159.996 10.9989V11.5081H153.698C153.814 12.2088 154.097 12.7602 154.548 13.1625C155 13.5648 155.571 13.7659 156.26 13.766C156.813 13.766 157.289 13.6839 157.689 13.5196C158.088 13.3554 158.463 13.1063 158.814 12.7724L159.799 13.9794C158.819 14.8771 157.618 15.326 156.195 15.3259ZM157.549 8.50718C157.161 8.11027 156.651 7.91184 156.022 7.91189C155.409 7.91189 154.896 8.11439 154.483 8.51941C154.069 8.92464 153.808 9.46931 153.698 10.1534H158.214C158.159 9.45273 157.938 8.90398 157.549 8.50716V8.50718Z\"\n                  fill=\"currentColor\"\n                />\n                <path\n                  d=\"M5.26022 3.23511C5.25436 3.23511 5.24854 3.23513 5.24268 3.23516L5.21875 5.21191C5.22423 5.21185 5.22969 5.2118 5.23518 5.2118C6.53629 5.2118 7.54551 6.23768 9.73906 9.93252L9.87278 10.1575L9.88153 10.1722L11.1094 8.32979L11.1009 8.31556C10.812 7.84556 10.5344 7.41312 10.2681 7.01826C9.95934 6.56075 9.66404 6.15204 9.37746 5.78713C7.92635 3.93952 6.71249 3.23511 5.26022 3.23511Z\"\n                  fill=\"url(#paint0_linear_627_396207)\"\n                />\n                <path\n                  d=\"M5.24198 3.23516C3.78266 3.24267 2.49251 4.18633 1.56092 5.63032C1.55819 5.63455 1.55546 5.63879 1.55273 5.64302L3.26279 6.57377C3.26556 6.56957 3.26836 6.56535 3.27114 6.56117C3.81514 5.7421 4.49212 5.21969 5.21805 5.21191C5.22353 5.21185 5.229 5.21181 5.23448 5.21181L5.2595 3.23511C5.25364 3.23511 5.24783 3.23513 5.24198 3.23516Z\"\n                  fill=\"url(#paint1_linear_627_396207)\"\n                />\n                <path\n                  d=\"M1.56088 5.63037C1.55816 5.6346 1.55543 5.63884 1.5527 5.64307C0.94054 6.596 0.484192 7.76537 0.237567 9.02689C0.236499 9.03235 0.235435 9.03781 0.234375 9.04329L2.15555 9.49659C2.15655 9.49111 2.15756 9.48562 2.15857 9.48015C2.36393 8.37149 2.75488 7.34323 3.26274 6.57382C3.26552 6.56962 3.26831 6.5654 3.2711 6.56122L1.56088 5.63037Z\"\n                  fill=\"url(#paint2_linear_627_396207)\"\n                />\n                <path\n                  d=\"M2.15979 9.48011L0.238778 9.02686C0.23771 9.03231 0.236646 9.03778 0.235585 9.04325C0.101104 9.73704 0.0326863 10.442 0.03125 11.1487C0.03125 11.1544 0.03125 11.1601 0.03125 11.1658L2.00149 11.3421C2.00133 11.3364 2.00117 11.3307 2.00103 11.3249C2.00007 11.284 1.99958 11.2424 1.99956 11.2003C2.00054 10.6288 2.05316 10.0586 2.15678 9.49655C2.15776 9.49107 2.15878 9.48558 2.15979 9.48011Z\"\n                  fill=\"url(#paint3_linear_627_396207)\"\n                />\n                <path\n                  d=\"M2.06148 11.9568C2.02614 11.7537 2.00611 11.5482 2.00156 11.3421C2.0014 11.3363 2.00124 11.3307 2.0011 11.3249L0.031335 11.1487C0.031335 11.1544 0.031335 11.1601 0.031335 11.1658V11.1669C0.0292535 11.5801 0.0653944 11.9925 0.139296 12.399C0.140327 12.4045 0.14134 12.4099 0.142386 12.4154L2.06448 11.9732C2.06345 11.9678 2.06247 11.9623 2.06148 11.9568Z\"\n                  fill=\"url(#paint4_linear_627_396207)\"\n                />\n                <path\n                  d=\"M2.50976 12.9765C2.29536 12.7425 2.14362 12.405 2.06386 11.9732C2.06285 11.9678 2.06187 11.9623 2.06088 11.9568L0.138672 12.399C0.139703 12.4045 0.140716 12.4099 0.141762 12.4154C0.28705 13.1782 0.571996 13.8139 0.980035 14.2949C0.983663 14.2991 0.987305 14.3034 0.990959 14.3077L2.52121 12.9888C2.51738 12.9848 2.51355 12.9807 2.50976 12.9765Z\"\n                  fill=\"url(#paint5_linear_627_396207)\"\n                />\n                <path\n                  d=\"M8.20487 7.50854C7.04655 9.28523 6.34486 10.3996 6.34486 10.3996C4.80187 12.8183 4.26806 13.3604 3.409 13.3604C3.05054 13.3604 2.75107 13.2328 2.52164 12.9888C2.51782 12.9848 2.51398 12.9807 2.51019 12.9765L0.980469 14.2949C0.984097 14.2991 0.987738 14.3034 0.991392 14.3077C1.5548 14.9644 2.35009 15.3288 3.33393 15.3288C4.82242 15.3288 5.89296 14.6271 7.79608 11.3004C7.79608 11.3004 8.58943 9.8994 9.1352 8.93436C8.79713 8.38854 8.48948 7.91597 8.20487 7.50854Z\"\n                  fill=\"#0082FB\"\n                />\n                <path\n                  d=\"M10.2688 4.7041C10.2649 4.70825 10.261 4.71248 10.2571 4.71664C9.94322 5.05596 9.64935 5.41323 9.37695 5.78663C9.66354 6.15154 9.95939 6.56105 10.2682 7.01855C10.6321 6.45684 10.9718 6.00189 11.3048 5.6532C11.3087 5.64907 11.3126 5.64504 11.3166 5.64094L10.2688 4.7041Z\"\n                  fill=\"url(#paint6_linear_627_396207)\"\n                />\n                <path\n                  d=\"M15.8912 4.53007C15.0834 3.71396 14.1202 3.23511 13.0905 3.23511C12.0047 3.23511 11.0914 3.83012 10.2677 4.70423C10.2637 4.70837 10.2598 4.71261 10.2559 4.71677L11.3036 5.65333C11.3075 5.6492 11.3114 5.64517 11.3154 5.64107C11.858 5.0766 12.3832 4.79478 12.9654 4.79478H12.9654C13.592 4.79478 14.1786 5.08975 14.6867 5.60687C14.6906 5.61092 14.6946 5.61494 14.6986 5.61902L15.9032 4.54221C15.8992 4.53815 15.8952 4.53412 15.8912 4.53007Z\"\n                  fill=\"#0082FB\"\n                />\n                <path\n                  d=\"M18.2273 10.8885C18.1821 8.26813 17.2651 5.92556 15.904 4.54218C15.9 4.53811 15.896 4.53408 15.892 4.53003L14.6875 5.60684C14.6915 5.61089 14.6954 5.61491 14.6994 5.61899C15.7233 6.67077 16.4256 8.6271 16.4895 10.8879C16.4897 10.8936 16.4898 10.8993 16.49 10.905L18.2276 10.9056C18.2275 10.8999 18.2274 10.8942 18.2273 10.8885Z\"\n                  fill=\"url(#paint7_linear_627_396207)\"\n                />\n                <path\n                  d=\"M18.2262 10.9056C18.2261 10.8999 18.226 10.8942 18.2259 10.8885L16.4881 10.8879C16.4883 10.8936 16.4884 10.8993 16.4886 10.905C16.4914 11.0111 16.4928 11.1179 16.4928 11.2253C16.4928 11.8417 16.4007 12.34 16.2135 12.6997C16.2107 12.705 16.2079 12.7104 16.2051 12.7157L17.5007 14.0632C17.504 14.0583 17.5071 14.0535 17.5103 14.0486C17.9807 13.3228 18.2276 12.3145 18.2276 11.0918C18.2276 11.0296 18.2272 10.9675 18.2262 10.9056Z\"\n                  fill=\"url(#paint8_linear_627_396207)\"\n                />\n                <path\n                  d=\"M16.2158 12.6997C16.213 12.705 16.2102 12.7104 16.2074 12.7157C16.0453 13.0189 15.814 13.2212 15.5117 13.3096L16.1024 15.1711C16.1806 15.1445 16.2567 15.1147 16.3308 15.0816C16.353 15.0718 16.3749 15.0616 16.3967 15.0512C16.4092 15.0452 16.4217 15.0391 16.4341 15.0329C16.8281 14.8341 17.1672 14.5417 17.4217 14.1812C17.438 14.1587 17.4541 14.1359 17.47 14.1127C17.4811 14.0963 17.4921 14.0798 17.5031 14.0632C17.5063 14.0583 17.5094 14.0534 17.5126 14.0485L16.2158 12.6997Z\"\n                  fill=\"url(#paint9_linear_627_396207)\"\n                />\n                <path\n                  d=\"M15.1349 13.3603C14.9481 13.3648 14.7626 13.3286 14.5911 13.2544L13.9863 15.1602C14.3262 15.2763 14.6889 15.3287 15.0932 15.3287C15.4415 15.3319 15.7878 15.2768 16.1179 15.1654L15.5273 13.3046C15.4001 13.3427 15.2678 13.3615 15.1349 13.3603Z\"\n                  fill=\"url(#paint10_linear_627_396207)\"\n                />\n                <path\n                  d=\"M13.9243 12.7085C13.9206 12.7042 13.9168 12.6999 13.9131 12.6956L12.5215 14.1429C12.5254 14.147 12.5293 14.1512 12.5332 14.1553C13.0167 14.6706 13.4784 14.9903 14.0021 15.1657L14.6064 13.2613C14.3857 13.1665 14.1723 12.9947 13.9243 12.7085Z\"\n                  fill=\"url(#paint11_linear_627_396207)\"\n                />\n                <path\n                  d=\"M13.9142 12.6956C13.4968 12.2101 12.9804 11.4019 12.1682 10.095L11.1097 8.32966L11.1012 8.31543L9.87305 10.1573L9.8818 10.172L10.6318 11.4337C11.3588 12.6503 11.9511 13.5304 12.5226 14.1428C12.5265 14.147 12.5304 14.1512 12.5343 14.1553L13.9254 12.7085C13.9217 12.7042 13.918 12.6999 13.9142 12.6956Z\"\n                  fill=\"url(#paint12_linear_627_396207)\"\n                />\n                <defs>\n                  <linearGradient\n                    id=\"paint0_linear_627_396207\"\n                    x1=\"10.2933\"\n                    y1=\"9.42293\"\n                    x2=\"6.21654\"\n                    y2=\"4.08099\"\n                    gradientUnits=\"userSpaceOnUse\">\n                    <stop offset=\"0.0006\" stopColor=\"#0867DF\" />\n                    <stop offset=\"0.4539\" stopColor=\"#0668E1\" />\n                    <stop offset=\"0.8591\" stopColor=\"#0064E0\" />\n                  </linearGradient>\n                  <linearGradient\n                    id=\"paint1_linear_627_396207\"\n                    x1=\"2.35598\"\n                    y1=\"5.96246\"\n                    x2=\"5.15084\"\n                    y2=\"3.84063\"\n                    gradientUnits=\"userSpaceOnUse\">\n                    <stop offset=\"0.1323\" stopColor=\"#0064DF\" />\n                    <stop offset=\"0.9988\" stopColor=\"#0064E0\" />\n                  </linearGradient>\n                  <linearGradient\n                    id=\"paint2_linear_627_396207\"\n                    x1=\"1.17132\"\n                    y1=\"9.07623\"\n                    x2=\"2.29244\"\n                    y2=\"6.25404\"\n                    gradientUnits=\"userSpaceOnUse\">\n                    <stop offset=\"0.0147\" stopColor=\"#0072EC\" />\n                    <stop offset=\"0.6881\" stopColor=\"#0064DF\" />\n                  </linearGradient>\n                  <linearGradient\n                    id=\"paint3_linear_627_396207\"\n                    x1=\"1.02028\"\n                    y1=\"11.115\"\n                    x2=\"1.15\"\n                    y2=\"9.39138\"\n                    gradientUnits=\"userSpaceOnUse\">\n                    <stop offset=\"0.0731\" stopColor=\"#007CF6\" />\n                    <stop offset=\"0.9943\" stopColor=\"#0072EC\" />\n                  </linearGradient>\n                  <linearGradient\n                    id=\"paint4_linear_627_396207\"\n                    x1=\"1.0917\"\n                    y1=\"12.0512\"\n                    x2=\"0.998912\"\n                    y2=\"11.3606\"\n                    gradientUnits=\"userSpaceOnUse\">\n                    <stop offset=\"0.0731\" stopColor=\"#007FF9\" />\n                    <stop offset=\"1\" stopColor=\"#007CF6\" />\n                  </linearGradient>\n                  <linearGradient\n                    id=\"paint5_linear_627_396207\"\n                    x1=\"1.03663\"\n                    y1=\"12.2326\"\n                    x2=\"1.61491\"\n                    y2=\"13.4591\"\n                    gradientUnits=\"userSpaceOnUse\">\n                    <stop offset=\"0.0731\" stopColor=\"#007FF9\" />\n                    <stop offset=\"1\" stopColor=\"#0082FB\" />\n                  </linearGradient>\n                  <linearGradient\n                    id=\"paint6_linear_627_396207\"\n                    x1=\"9.92449\"\n                    y1=\"6.29781\"\n                    x2=\"10.689\"\n                    y2=\"5.24046\"\n                    gradientUnits=\"userSpaceOnUse\">\n                    <stop offset=\"0.2799\" stopColor=\"#007FF8\" />\n                    <stop offset=\"0.9141\" stopColor=\"#0082FB\" />\n                  </linearGradient>\n                  <linearGradient\n                    id=\"paint7_linear_627_396207\"\n                    x1=\"15.7367\"\n                    y1=\"4.92752\"\n                    x2=\"17.3361\"\n                    y2=\"10.8108\"\n                    gradientUnits=\"userSpaceOnUse\">\n                    <stop stopColor=\"#0082FB\" />\n                    <stop offset=\"0.9995\" stopColor=\"#0081FA\" />\n                  </linearGradient>\n                  <linearGradient\n                    id=\"paint8_linear_627_396207\"\n                    x1=\"17.7208\"\n                    y1=\"11.0359\"\n                    x2=\"16.7086\"\n                    y2=\"13.0813\"\n                    gradientUnits=\"userSpaceOnUse\">\n                    <stop offset=\"0.0619\" stopColor=\"#0081FA\" />\n                    <stop offset=\"1\" stopColor=\"#0080F9\" />\n                  </linearGradient>\n                  <linearGradient\n                    id=\"paint9_linear_627_396207\"\n                    x1=\"15.9065\"\n                    y1=\"14.1657\"\n                    x2=\"16.8526\"\n                    y2=\"13.5213\"\n                    gradientUnits=\"userSpaceOnUse\">\n                    <stop stopColor=\"#027AF3\" />\n                    <stop offset=\"1\" stopColor=\"#0080F9\" />\n                  </linearGradient>\n                  <linearGradient\n                    id=\"paint10_linear_627_396207\"\n                    x1=\"14.4218\"\n                    y1=\"14.2915\"\n                    x2=\"15.7366\"\n                    y2=\"14.2915\"\n                    gradientUnits=\"userSpaceOnUse\">\n                    <stop stopColor=\"#0377EF\" />\n                    <stop offset=\"0.9994\" stopColor=\"#0279F1\" />\n                  </linearGradient>\n                  <linearGradient\n                    id=\"paint11_linear_627_396207\"\n                    x1=\"13.2783\"\n                    y1=\"13.5675\"\n                    x2=\"14.2235\"\n                    y2=\"14.1236\"\n                    gradientUnits=\"userSpaceOnUse\">\n                    <stop offset=\"0.0019\" stopColor=\"#0471E9\" />\n                    <stop offset=\"1\" stopColor=\"#0377EF\" />\n                  </linearGradient>\n                  <linearGradient\n                    id=\"paint12_linear_627_396207\"\n                    x1=\"10.3961\"\n                    y1=\"9.46696\"\n                    x2=\"13.424\"\n                    y2=\"13.274\"\n                    gradientUnits=\"userSpaceOnUse\">\n                    <stop offset=\"0.2765\" stopColor=\"#0867DF\" />\n                    <stop offset=\"1\" stopColor=\"#0471E9\" />\n                  </linearGradient>\n                </defs>\n              </svg>\n            </div>\n          </ExternalLink>\n\n          <div\n            className=\"text-xs text-left rtl:text-right mt-2 pe-0.5\"\n            dir=\"ltr\">\n            Copyright &copy; Meta Platforms, Inc\n          </div>\n          <div\n            className=\"uwu-visible text-xs cursor-pointer hover:text-link hover:dark:text-link-dark hover:underline\"\n            onClick={() => {\n              // @ts-ignore\n              window.__setUwu(false);\n            }}>\n            no uwu plz\n          </div>\n          <div\n            className=\"uwu-hidden text-xs cursor-pointer hover:text-link hover:dark:text-link-dark hover:underline\"\n            onClick={() => {\n              // @ts-ignore\n              window.__setUwu(true);\n            }}>\n            uwu?\n          </div>\n          <div className=\"uwu-visible text-xs\">\n            Logo by\n            <ExternalLink\n              className=\"ms-1\"\n              href=\"https://twitter.com/sawaratsuki1004\">\n              @sawaratsuki1004\n            </ExternalLink>\n          </div>\n        </div>\n        <div className=\"flex flex-col\">\n          <FooterLink href=\"/learn\" isHeader={true}>\n            React 학습하기\n          </FooterLink>\n          <FooterLink href=\"/learn/\">빠르게 시작하기</FooterLink>\n          <FooterLink href=\"/learn/installation\">설치하기</FooterLink>\n          <FooterLink href=\"/learn/describing-the-ui\">UI 표현하기</FooterLink>\n          <FooterLink href=\"/learn/adding-interactivity\">\n            상호작용성 더하기\n          </FooterLink>\n          <FooterLink href=\"/learn/managing-state\">State 관리하기</FooterLink>\n          <FooterLink href=\"/learn/escape-hatches\">탈출구</FooterLink>\n        </div>\n        <div className=\"flex flex-col\">\n          <FooterLink href=\"/reference/react\" isHeader={true}>\n            API 참고서\n          </FooterLink>\n          <FooterLink href=\"/reference/react\">React APIs</FooterLink>\n          <FooterLink href=\"/reference/react-dom\">React DOM APIs</FooterLink>\n        </div>\n        <div className=\"md:col-start-2 xl:col-start-4 flex flex-col\">\n          <FooterLink href=\"/community\" isHeader={true}>\n            커뮤니티\n          </FooterLink>\n          <FooterLink href=\"https://github.com/facebook/react/blob/main/CODE_OF_CONDUCT.md\">\n            행동 강령\n          </FooterLink>\n          <FooterLink href=\"/community/team\">팀 소개</FooterLink>\n          <FooterLink href=\"/community/docs-contributors\">\n            문서 기여자\n          </FooterLink>\n          <FooterLink href=\"/community/acknowledgements\">감사의 말</FooterLink>\n        </div>\n        <div className=\"flex flex-col\">\n          <FooterLink isHeader={true}>더 보기</FooterLink>\n          <FooterLink href=\"/blog\">블로그</FooterLink>\n          <FooterLink href=\"https://reactnative.dev/\">React Native</FooterLink>\n          <FooterLink href=\"https://opensource.facebook.com/legal/privacy\">\n            개인 정보 보호\n          </FooterLink>\n          <FooterLink href=\"https://opensource.fb.com/legal/terms/\">\n            약관\n          </FooterLink>\n          <div className=\"flex flex-row items-center mt-8 gap-x-2\">\n            <ExternalLink\n              aria-label=\"React on Facebook\"\n              href=\"https://www.facebook.com/react\"\n              className={socialLinkClasses}>\n              <IconFacebookCircle />\n            </ExternalLink>\n            <ExternalLink\n              aria-label=\"React on Twitter\"\n              href=\"https://twitter.com/reactjs\"\n              className={socialLinkClasses}>\n              <IconTwitter />\n            </ExternalLink>\n            <ExternalLink\n              aria-label=\"React on Bluesky\"\n              href=\"https://bsky.app/profile/react.dev\"\n              className={socialLinkClasses}>\n              <IconBsky />\n            </ExternalLink>\n            <ExternalLink\n              aria-label=\"React on Github\"\n              href=\"https://github.com/facebook/react\"\n              className={socialLinkClasses}>\n              <IconGitHub />\n            </ExternalLink>\n          </div>\n        </div>\n      </div>\n    </footer>\n  );\n}\n\nfunction FooterLink({\n  href,\n  children,\n  isHeader = false,\n}: {\n  href?: string;\n  children: React.ReactNode;\n  isHeader?: boolean;\n}) {\n  const classes = cn('border-b inline-block border-transparent', {\n    'text-sm text-primary dark:text-primary-dark': !isHeader,\n    'text-md text-secondary dark:text-secondary-dark my-2 font-bold': isHeader,\n    'hover:border-gray-10': href,\n  });\n\n  if (!href) {\n    return <div className={classes}>{children}</div>;\n  }\n\n  if (href.startsWith('https://')) {\n    return (\n      <div>\n        <ExternalLink href={href} className={classes}>\n          {children}\n        </ExternalLink>\n      </div>\n    );\n  }\n\n  return (\n    <div>\n      <NextLink href={href} className={classes}>\n        {children}\n      </NextLink>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/components/Layout/HomeContent.js",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport {\n  createContext,\n  memo,\n  useState,\n  useContext,\n  useId,\n  Suspense,\n  useEffect,\n  useRef,\n  useTransition,\n} from 'react';\nimport cn from 'classnames';\nimport NextLink from 'next/link';\n\nimport ButtonLink from '../ButtonLink';\nimport {IconRestart} from '../Icon/IconRestart';\nimport BlogCard from 'components/MDX/BlogCard';\nimport {IconChevron} from 'components/Icon/IconChevron';\nimport {IconSearch} from 'components/Icon/IconSearch';\nimport {Logo} from 'components/Logo';\nimport Link from 'components/MDX/Link';\nimport CodeBlock from 'components/MDX/CodeBlock';\nimport {ExternalLink} from 'components/ExternalLink';\nimport sidebarBlog from '../../sidebarBlog.json';\nimport * as React from 'react';\nimport Image from 'next/image';\n\nfunction Section({children, background = null}) {\n  return (\n    <div\n      className={cn(\n        'mx-auto flex flex-col w-full',\n        background === null && 'max-w-7xl',\n        background === 'left-card' &&\n          'bg-gradient-left dark:bg-gradient-left-dark border-t border-primary/10 dark:border-primary-dark/10 ',\n        background === 'right-card' &&\n          'bg-gradient-right dark:bg-gradient-right-dark border-t border-primary/5 dark:border-primary-dark/5'\n      )}\n      style={{\n        contain: 'content',\n      }}>\n      <div className=\"flex-col gap-2 flex grow w-full my-20 lg:my-32 mx-auto items-center\">\n        {children}\n      </div>\n    </div>\n  );\n}\n\nfunction Header({children}) {\n  return (\n    <h2 className=\"leading-xl font-display text-primary dark:text-primary-dark font-semibold text-5xl lg:text-6xl -mt-4 mb-7 w-full max-w-3xl\">\n      {children}\n    </h2>\n  );\n}\n\nfunction Para({children}) {\n  return (\n    <p className=\"max-w-3xl mx-auto text-lg lg:text-xl text-secondary dark:text-secondary-dark leading-normal\">\n      {children}\n    </p>\n  );\n}\n\nfunction Br({breakPointPrefix = 'lg'}) {\n  const breakPointClass = {\n    lg: 'lg:inline',\n    xl: 'xl:inline',\n  }[breakPointPrefix];\n\n  return <br className={cn('hidden', breakPointClass)} />;\n}\n\nfunction Center({children}) {\n  return (\n    <div className=\"px-5 lg:px-0 max-w-4xl lg:text-center text-white text-opacity-80 flex flex-col items-center justify-center\">\n      {children}\n    </div>\n  );\n}\n\nfunction FullBleed({children}) {\n  return (\n    <div className=\"max-w-7xl mx-auto flex flex-col w-full\">{children}</div>\n  );\n}\n\nfunction CurrentTime() {\n  const [date, setDate] = useState(new Date());\n  const currentTime = date.toLocaleTimeString([], {\n    hour: 'numeric',\n    minute: 'numeric',\n  });\n  useEffect(() => {\n    const msPerMinute = 60 * 1000;\n    let nextMinute = Math.floor(+date / msPerMinute + 1) * msPerMinute;\n\n    const timeout = setTimeout(() => {\n      if (Date.now() > nextMinute) {\n        setDate(new Date());\n      }\n    }, nextMinute - Date.now());\n    return () => clearTimeout(timeout);\n  }, [date]);\n\n  return <span suppressHydrationWarning>{currentTime}</span>;\n}\n\nconst blogSidebar = sidebarBlog.routes[1];\nif (blogSidebar.path !== '/blog') {\n  throw Error('Could not find the blog route in sidebarBlog.json');\n}\nconst recentPosts = blogSidebar.routes.slice(0, 4).map((entry) => ({\n  title: entry.titleForHomepage,\n  icon: entry.icon,\n  date: entry.date,\n  url: entry.path,\n}));\n\nexport function HomeContent() {\n  return (\n    <>\n      <div className=\"ps-0\">\n        <div className=\"mx-5 mt-12 lg:mt-24 mb-20 lg:mb-32 flex flex-col justify-center\">\n          <div className=\"uwu-visible flex justify-center\">\n            <Image\n              alt=\"logo by @sawaratsuki1004\"\n              title=\"logo by @sawaratsuki1004\"\n              loading=\"eager\"\n              width={313}\n              height={160}\n              src=\"/images/uwu.png\"\n            />\n          </div>\n          <Logo\n            className={cn(\n              'uwu-hidden mt-4 mb-3 text-brand dark:text-brand-dark w-24 lg:w-28 self-center text-sm me-0 flex origin-center transition-all ease-in-out'\n            )}\n          />\n          <h1 className=\"uwu-hidden text-5xl font-display lg:text-6xl self-center flex font-semibold leading-snug text-primary dark:text-primary-dark\">\n            React\n          </h1>\n          <p className=\"text-4xl font-display max-w-lg md:max-w-full py-1 text-center text-secondary dark:text-primary-dark leading-snug self-center\">\n            웹 및 네이티브 사용자 인터페이스를 위한 라이브러리\n          </p>\n          <div className=\"mt-5 self-center flex gap-2 w-full sm:w-auto flex-col sm:flex-row\">\n            <ButtonLink\n              href={'/learn'}\n              type=\"primary\"\n              size=\"lg\"\n              className=\"w-full sm:w-auto justify-center\"\n              label=\"Learn React\">\n              React 학습하기\n            </ButtonLink>\n            <ButtonLink\n              href={'/reference/react'}\n              type=\"secondary\"\n              size=\"lg\"\n              className=\"w-full sm:w-auto justify-center\"\n              label=\"API Reference\">\n              API 참고서\n            </ButtonLink>\n          </div>\n        </div>\n\n        <Section background=\"left-card\">\n          <Center>\n            <Header>\n              컴포넌트를 사용하여\n              <Br /> 사용자 인터페이스 만들기\n            </Header>\n            <Para>\n              React를 사용하면 컴포넌트라 불리는 조각들을 사용하여 사용자\n              인터페이스를 만들 수 있습니다. <Code>Thumbnail</Code>,{' '}\n              <Code>LikeButton</Code>, 그리고 <Code>Video</Code> 같은 컴포넌트를\n              만들어 보세요. 그런 다음 전체 화면, 페이지 및 앱에서 이들을 결합할\n              수 있습니다.\n            </Para>\n          </Center>\n          <FullBleed>\n            <Example1 />\n          </FullBleed>\n          <Center>\n            <Para>\n              혼자서 작업하든, 수천 명의 다른 개발자와 함께 작업하든, React를\n              사용하는 느낌은 동일합니다. 개인, 팀, 조직에서 작성한 컴포넌트를\n              원활하게 결합할 수 있도록 설계하였습니다.\n            </Para>\n          </Center>\n        </Section>\n\n        <Section background=\"right-card\">\n          <Center>\n            <Header>\n              코드와 마크업으로\n              <Br /> 컴포넌트 작성하기\n            </Header>\n            <Para>\n              React 컴포넌트는 자바스크립트 함수입니다. 조건부로 내용을\n              표시하려면 <Code>if</Code> 문을 사용할 수 있습니다. 목록을\n              표시하려면 배열에 <Code>map()</Code>을 사용할 수 있습니다. React를\n              배우는 것은 프로그래밍을 배우는 것입니다.\n            </Para>\n          </Center>\n          <FullBleed>\n            <Example2 />\n          </FullBleed>\n          <Center>\n            <Para>\n              이 마크업 구문을 JSX라 부릅니다. 이것은 React에 의해서 대중화된\n              자바스크립트 구문의 확장입니다. JSX 마크업을 관련된 렌더링 로직과\n              가까이 두면, React 컴포넌트를 쉽게 만들고 관리하고 삭제할 수\n              있습니다.\n            </Para>\n          </Center>\n        </Section>\n\n        <Section background=\"left-card\">\n          <Center>\n            <Header>\n              필요한 곳에\n              <Br /> 상호작용 요소 추가하기\n            </Header>\n            <Para>\n              React 컴포넌트는 데이터를 받고 화면에 표시할 내용을 반환합니다.\n              사용자가 입력란에 입력하는 것과 같이 상호작용에 응답하여 새\n              데이터를 전달할 수 있습니다. 그런 다음 React는 새 데이터와\n              일치하도록 화면을 업데이트합니다.\n            </Para>\n          </Center>\n          <FullBleed>\n            <Example3 />\n          </FullBleed>\n          <Center>\n            <Para>\n              전체 페이지를 React로 빌드할 필요는 없습니다. React를 기존 HTML\n              페이지에 추가하고, 페이지 어디에서나 상호작용하는 React 컴포넌트를\n              렌더링할 수 있습니다.\n            </Para>\n            <div className=\"flex justify-start w-full lg:justify-center\">\n              <CTA\n                color=\"gray\"\n                icon=\"code\"\n                href=\"/learn/add-react-to-an-existing-project\">\n                페이지에 React 추가하기\n              </CTA>\n            </div>\n          </Center>\n        </Section>\n\n        <Section background=\"right-card\">\n          <Center>\n            <Header>\n              프레임워크를 통해\n              <Br /> 풀스택으로 만들기\n            </Header>\n            <Para>\n              React는 라이브러리입니다. 컴포넌트를 조합할 수 있도록 도와주지만,\n              라우팅이나 데이터를 가져오는 방법을 규정하지는 않습니다. React로\n              완전한 앱을 만들려면,{' '}\n              <Link href=\"https://nextjs.org\">Next.js</Link> 또는{' '}\n              <Link href=\"https://reactrouter.com\">React Router</Link> 같은\n              풀스택 React 프레임워크를 추천합니다.\n            </Para>\n          </Center>\n          <FullBleed>\n            <Example4 />\n          </FullBleed>\n          <Center>\n            <Para>\n              React는 아키텍처이기도 합니다. 이를 구현하는 프레임워크는 서버에서\n              실행되는 비동기 컴포넌트 혹은 빌드 중에 실행되는 비동기\n              컴포넌트에서 데이터를 가져올 수 있도록 합니다. 파일이나\n              데이터베이스에서 데이터를 읽고, 이를 상호작용하는 컴포넌트에\n              전달할 수 있습니다.\n            </Para>\n            <div className=\"flex justify-start w-full lg:justify-center\">\n              <CTA\n                color=\"gray\"\n                icon=\"framework\"\n                href=\"/learn/creating-a-react-app\">\n                프레임워크로 시작하기\n              </CTA>\n            </div>\n          </Center>\n        </Section>\n        <Section background=\"left-card\">\n          <div className=\"mx-auto flex flex-col w-full\">\n            <div className=\"mx-auto max-w-4xl lg:text-center items-center px-5 flex flex-col\">\n              <Header>모든 플랫폼에서 사용하기</Header>\n              <Para>\n                사람들은 다양한 이유로 웹과 네이티브 앱을 좋아합니다. React는\n                동일한 기술을 사용하여 웹 앱과 네이티브 앱을 모두 만들 수\n                있습니다. 각 플랫폼의 장점을 활용하여 모든 플랫폼에서 적합한\n                인터페이스를 구현할 수 있습니다.\n              </Para>\n            </div>\n            <div className=\"max-w-7xl mx-auto flex flex-col lg:flex-row mt-16 mb-20 lg:mb-28 px-5 gap-20 lg:gap-5\">\n              <div className=\"relative lg:w-6/12 flex\">\n                <div className=\"absolute -bottom-8 lg:-bottom-10 z-10 w-full\">\n                  <WebIcons />\n                </div>\n                <BrowserChrome hasRefresh={false} domain=\"example.com\">\n                  <div className=\"relative overflow-hidden\">\n                    <div className=\"absolute inset-0 bg-gradient-right\" />\n                    <div className=\"bg-wash relative h-14 w-full\" />\n                    <div className=\"relative flex items-start justify-center flex-col flex-1 pb-16 pt-5 gap-3 px-5 lg:px-10 lg:pt-8\">\n                      <h4 className=\"leading-tight text-primary font-semibold text-3xl lg:text-4xl\">\n                        웹에 충실하기\n                      </h4>\n                      <p className=\"lg:text-xl leading-normal text-secondary\">\n                        사람들은 웹 앱이 빠르게 로드되길 기대합니다. 서버에서\n                        React를 사용하면 데이터를 가져오는 동안 HTML을\n                        스트리밍하여 자바스크립트 코드가 로드되기 전에 남은\n                        내용을 점진적으로 채울 수 있습니다. 클라이언트에서\n                        React는 표준 웹 API를 사용하여 렌더링 중에도 UI가\n                        반응하도록 유지할 수 있습니다.\n                      </p>\n                    </div>\n                  </div>\n                </BrowserChrome>\n              </div>\n              <div className=\"relative lg:w-6/12 flex\">\n                <div className=\"absolute -bottom-8 lg:-bottom-10 z-10 w-full\">\n                  <NativeIcons />\n                </div>\n                <figure className=\"mx-auto max-w-3xl h-auto\">\n                  <div className=\"p-2.5 bg-gray-95 dark:bg-black rounded-2xl shadow-nav dark:shadow-nav-dark\">\n                    <div className=\"bg-gradient-right dark:bg-gradient-right-dark px-3 sm:px-3 pb-12 lg:pb-20 rounded-lg overflow-hidden\">\n                      <div className=\"select-none w-full h-14 flex flex-row items-start pt-3 -mb-2.5 justify-between text-tertiary dark:text-tertiary-dark\">\n                        <span className=\"uppercase tracking-wide leading-none font-bold text-sm text-tertiary dark:text-tertiary-dark\">\n                          <CurrentTime />\n                        </span>\n                        <div className=\"gap-2 flex -mt-0.5\">\n                          <svg\n                            width=\"16\"\n                            height=\"20\"\n                            viewBox=\"0 0 72 72\"\n                            fill=\"none\"\n                            xmlns=\"http://www.w3.org/2000/svg\">\n                            <path\n                              fillRule=\"evenodd\"\n                              clipRule=\"evenodd\"\n                              d=\"M34.852 6.22836C35.973 5.76401 37.2634 6.02068 38.1214 6.87868L53.1214 21.8787C53.7485 22.5058 54.066 23.3782 53.9886 24.2617C53.9113 25.1451 53.447 25.9491 52.7205 26.4577L39.0886 36.0003L52.7204 45.5423C53.447 46.0508 53.9113 46.8548 53.9886 47.7383C54.066 48.6218 53.7485 49.4942 53.1214 50.1213L38.1214 65.1213C37.2634 65.9793 35.973 66.236 34.852 65.7716C33.731 65.3073 33.0001 64.2134 33.0001 63V40.2624L22.7205 47.4583C21.3632 48.4085 19.4926 48.0784 18.5424 46.721C17.5922 45.3637 17.9223 43.4931 19.2797 42.543L28.6258 36.0004L19.2797 29.4583C17.9224 28.5082 17.5922 26.6376 18.5424 25.2803C19.4925 23.9229 21.3631 23.5928 22.7204 24.5429L33.0001 31.7384V9C33.0001 7.78661 33.731 6.6927 34.852 6.22836ZM39.0001 43.2622L46.3503 48.4072L39.0001 55.7574V43.2622ZM39.0001 28.7382V16.2426L46.3503 23.5929L39.0001 28.7382Z\"\n                              fill=\"currentColor\"\n                            />\n                          </svg>\n\n                          <svg\n                            width=\"16\"\n                            height=\"20\"\n                            viewBox=\"0 0 72 72\"\n                            fill=\"none\"\n                            xmlns=\"http://www.w3.org/2000/svg\">\n                            <path\n                              d=\"M9 27C9.82864 27 10.5788 26.664 11.1217 26.1209C11.2116 26.0355 11.3037 25.9526 11.397 25.871C11.625 25.6714 11.9885 25.3677 12.4871 24.9938C13.4847 24.2455 15.0197 23.219 17.0912 22.1833C21.2243 20.1167 27.5179 18 35.9996 18C44.4813 18 50.7748 20.1167 54.9079 22.1833C56.9794 23.219 58.5144 24.2455 59.5121 24.9938C59.6056 25.0639 60.8802 26.1233 60.8802 26.1233C61.423 26.6652 62.1724 27 63 27C64.6569 27 66 25.6569 66 24C66 22.8871 65.3475 22.0506 64.5532 21.3556C64.2188 21.0629 63.7385 20.6635 63.1121 20.1938C61.8597 19.2545 60.0197 18.031 57.5912 16.8167C52.7243 14.3833 45.5179 12 35.9996 12C26.4813 12 19.2748 14.3833 14.4079 16.8167C11.9794 18.031 10.1394 19.2545 8.88706 20.1938C8.26066 20.6635 7.78035 21.0629 7.44593 21.3556C7.2605 21.5178 7.07794 21.6834 6.9016 21.8555C6.33334 22.417 6 23.1999 6 24C6 25.6569 7.34315 27 9 27Z\"\n                              fill=\"currentColor\"\n                            />\n                            <path\n                              fillRule=\"evenodd\"\n                              clipRule=\"evenodd\"\n                              d=\"M26.1116 48.631C24.2868 50.4378 21 49.0661 21 46.5C21 45.6707 21.3365 44.92 21.8804 44.3769C21.9856 44.2702 22.0973 44.1695 22.209 44.0697C22.3915 43.9065 22.6466 43.6885 22.9713 43.4344C23.6195 42.9271 24.5536 42.2694 25.7509 41.6163C28.1445 40.3107 31.6365 39 35.9999 39C40.3634 39 43.8554 40.3107 46.249 41.6163C47.4463 42.2694 48.3804 42.9271 49.0286 43.4344C50.0234 44.213 51 45.134 51 46.5C51 48.1569 49.6569 49.5 48 49.5C47.1724 49.5 46.4231 49.1649 45.8803 48.623C45.7028 48.4617 45.5197 48.3073 45.3307 48.1594C44.9007 47.8229 44.2411 47.3556 43.3759 46.8837C41.6445 45.9393 39.1365 45 35.9999 45C32.8634 45 30.3554 45.9393 28.624 46.8837C27.7588 47.3556 27.0992 47.8229 26.6692 48.1594C26.3479 48.4109 26.155 48.5899 26.1116 48.631Z\"\n                              fill=\"currentColor\"\n                            />\n                            <path\n                              d=\"M36 63C39.3137 63 42 60.3137 42 57C42 53.6863 39.3137 51 36 51C32.6863 51 30 53.6863 30 57C30 60.3137 32.6863 63 36 63Z\"\n                              fill=\"currentColor\"\n                            />\n                            <path\n                              d=\"M15 39C13.3431 39 12 37.6569 12 36C12 34.3892 13.3933 33.3427 14.5534 32.4503C15.5841 31.6574 17.0871 30.6231 19.04 29.5952C22.9506 27.537 28.6773 25.5 35.9997 25.5C43.3222 25.5 49.0488 27.537 52.9595 29.5952C54.9123 30.6231 56.4154 31.6574 57.4461 32.4503C57.9619 32.847 58.361 33.1846 58.6407 33.4324C59.4024 34.1073 60 34.9345 60 36C60 37.6569 58.6569 39 57 39C56.1737 39 55.4255 38.6662 54.8829 38.1258C54.5371 37.7978 54.1653 37.4964 53.7878 37.206C52.9903 36.5926 51.7746 35.7519 50.165 34.9048C46.9506 33.213 42.1773 31.5 35.9997 31.5C29.8222 31.5 25.0488 33.213 21.8345 34.9048C20.2248 35.7519 19.0091 36.5926 18.2117 37.206C17.6144 37.6654 17.2549 37.9951 17.1459 38.098C16.5581 38.6591 15.8222 39 15 39Z\"\n                              fill=\"currentColor\"\n                            />\n                          </svg>\n                          <svg\n                            width=\"20\"\n                            height=\"20\"\n                            viewBox=\"0 0 72 72\"\n                            fill=\"none\"\n                            xmlns=\"http://www.w3.org/2000/svg\">\n                            <path\n                              d=\"M12.9533 26.0038C13.224 24.7829 14.3285 24 15.579 24H50.421C51.6715 24 52.776 24.7829 53.0467 26.0038C53.4754 27.937 54 31.2691 54 36C54 40.7309 53.4754 44.063 53.0467 45.9962C52.776 47.2171 51.6715 48 50.421 48H15.579C14.3285 48 13.224 47.2171 12.9533 45.9962C12.5246 44.063 12 40.7309 12 36C12 31.2691 12.5246 27.937 12.9533 26.0038Z\"\n                              fill=\"currentColor\"\n                            />\n                            <path\n                              fillRule=\"evenodd\"\n                              clipRule=\"evenodd\"\n                              d=\"M12.7887 15C8.77039 15 5.23956 17.668 4.48986 21.6158C3.74326 25.5473 3 30.7737 3 36C3 41.2263 3.74326 46.4527 4.48986 50.3842C5.23956 54.332 8.77039 57 12.7887 57H53.2113C57.2296 57 60.7604 54.332 61.5101 50.3842C61.8155 48.7765 62.1202 46.9522 62.3738 45H63.7918C64.5731 45 65.3283 44.8443 66 44.5491C67.2821 43.9857 68.2596 42.9142 68.5322 41.448C68.7927 40.0466 69 38.2306 69 36C69 33.7694 68.7927 31.9534 68.5322 30.552C68.2596 29.0858 67.2821 28.0143 66 27.4509C65.3283 27.1557 64.5731 27 63.7918 27H62.3738C62.1202 25.0478 61.8155 23.2235 61.5101 21.6158C60.7604 17.668 57.2296 15 53.2113 15H12.7887ZM53.2113 21H12.7887C11.3764 21 10.5466 21.8816 10.3845 22.7352C9.67563 26.4681 9 31.29 9 36C9 40.71 9.67563 45.5319 10.3845 49.2648C10.5466 50.1184 11.3764 51 12.7887 51H53.2113C54.6236 51 55.4534 50.1184 55.6155 49.2648C56.3244 45.5319 57 40.71 57 36C57 31.29 56.3244 26.4681 55.6155 22.7352C55.4534 21.8816 54.6236 21 53.2113 21Z\"\n                              fill=\"currentColor\"\n                            />\n                          </svg>\n                        </div>\n                      </div>\n                      <div className=\"flex flex-col items-start justify-center pt-0 gap-3 px-2.5 lg:pt-8 lg:px-8\">\n                        <h4 className=\"leading-tight text-primary dark:text-primary-dark font-semibold text-3xl lg:text-4xl\">\n                          진정한 네이티브에서 사용하기\n                        </h4>\n                        <p className=\"h-full lg:text-xl text-secondary dark:text-secondary-dark leading-normal\">\n                          사람들은 네이티브<sup>Native</sup> 앱이 해당 플랫폼의\n                          모습과 느낌을 갖기를 기대합니다.{' '}\n                          <Link href=\"https://reactnative.dev\">\n                            React Native\n                          </Link>\n                          와{' '}\n                          <Link href=\"https://github.com/expo/expo\">Expo</Link>\n                          를 사용하면 React를 통해 Android, iOS 등을 위한 앱을\n                          빌드할 수 있습니다. UI가 진정한 네이티브이기 때문에\n                          네이티브처럼 보이고 느껴집니다. 이것은 웹 뷰\n                          <sup>Web View</sup>가 아닙니다. React 컴포넌트들은\n                          실제 Android, iOS 플랫폼에서 제공하는 뷰\n                          <sup>View</sup>를 렌더링합니다.\n                        </p>\n                      </div>\n                    </div>\n                  </div>\n                </figure>\n              </div>\n            </div>\n            <div className=\"px-5 lg:px-0 max-w-4xl mx-auto lg:text-center text-secondary dark:text-secondary-dark\">\n              <Para>\n                React를 사용하면 웹 및 네이티브 개발자가 될 수 있습니다. 사용자\n                경험의 저하 없이 여러 플랫폼에 출시할 수 있습니다. 조직에서는\n                플랫폼 간의 격차를 줄이고, 기능을 완전히 소유하는 팀을 구성할 수\n                있습니다.\n              </Para>\n              <div className=\"flex justify-start w-full lg:justify-center\">\n                <CTA color=\"gray\" icon=\"native\" href=\"https://reactnative.dev/\">\n                  네이티브 플랫폼에서 React 사용하기\n                </CTA>\n              </div>\n            </div>\n          </div>\n        </Section>\n\n        <Section background=\"right-card\">\n          <div className=\"max-w-7xl mx-auto flex flex-col lg:flex-row px-5\">\n            <div className=\"max-w-3xl lg:max-w-7xl gap-5 flex flex-col lg:flex-row lg:px-5\">\n              <div className=\"w-full lg:w-6/12 max-w-3xl flex flex-col items-start justify-start lg:ps-5 lg:pe-10\">\n                <Header>\n                  새로운 기능에 맞춰\n                  <Br breakPointPrefix=\"xl\" /> 업그레이드 하기\n                </Header>\n                <Para>\n                  React는 변화에 신중하게 접근합니다. 모든 React 커밋은 10억명\n                  이상의 사용자가 있는 비즈니스의 크리티컬한 영역에서 테스트를\n                  진행합니다. Meta에서는 10만 개 이상의 React 컴포넌트가 모든\n                  마이그레이션 전략을 검증합니다.\n                </Para>\n                <div className=\"order-last pt-5\">\n                  <Para>\n                    React 팀은 항상 React를 개선하는 방법을 연구합니다. 몇 년이\n                    걸리는 연구도 있습니다. React는 연구 아이디어를 제품에\n                    적용하는 데에 높은 기준을 가지고 있습니다. 검증된 접근\n                    방식만이 React 일부가 됩니다.\n                  </Para>\n                  <div className=\"hidden lg:flex justify-start w-full\">\n                    <CTA color=\"gray\" icon=\"news\" href=\"/blog\">\n                      더 많은 React 뉴스 읽기\n                    </CTA>\n                  </div>\n                </div>\n              </div>\n              <div className=\"w-full lg:w-6/12\">\n                <p className=\"uppercase tracking-wide font-bold text-sm text-tertiary dark:text-tertiary-dark flex flex-row gap-2 items-center mt-5 lg:-mt-2 w-full\">\n                  <IconChevron />\n                  최신 React 뉴스\n                </p>\n                <div className=\"flex-col sm:flex-row flex-wrap flex gap-5 text-start my-5\">\n                  <div className=\"flex-1 min-w-[40%] text-start\">\n                    <BlogCard {...recentPosts[0]} />\n                  </div>\n                  <div className=\"flex-1 min-w-[40%] text-start\">\n                    <BlogCard {...recentPosts[1]} />\n                  </div>\n                  <div className=\"flex-1 min-w-[40%] text-start\">\n                    <BlogCard {...recentPosts[2]} />\n                  </div>\n                  <div className=\"hidden sm:flex-1 sm:inline\">\n                    <BlogCard {...recentPosts[3]} />\n                  </div>\n                </div>\n                <div className=\"flex lg:hidden justify-start w-full\">\n                  <CTA color=\"gray\" icon=\"news\" href=\"/blog\">\n                    React 뉴스 더 보기\n                  </CTA>\n                </div>\n              </div>\n            </div>\n          </div>\n        </Section>\n\n        <Section background=\"left-card\">\n          <div className=\"w-full\">\n            <div className=\"mx-auto flex flex-col max-w-4xl\">\n              <Center>\n                <Header>수백만 명이 있는 커뮤니티</Header>\n                <Para>\n                  여러분은 혼자가 아닙니다. 전세계의 200만 명이 넘는 개발자들이\n                  React 문서를 매달 방문합니다. React는 사람들과 팀이 동의할 수\n                  있는 것입니다.\n                </Para>\n              </Center>\n            </div>\n            <CommunityGallery />\n            <div className=\"mx-auto flex flex-col max-w-4xl\">\n              <Center>\n                <Para>\n                  이것이 바로 React가 단순한 라이브러리, 아키텍처, 혹은 생태계\n                  그 이상인 이유입니다. React는 바로 커뮤니티입니다. 도움을\n                  요청하고, 기회를 찾고, 새로운 친구를 만날 수 있는 곳입니다.\n                  개발자와 디자이너, 초보자와 전문가, 연구원과 예술가, 교사와\n                  학생을 만날 수 있습니다. 배경은 모두 다를 수 있지만, React를\n                  통해 함께 사용자 인터페이스를 만들 수 있습니다.\n                </Para>\n              </Center>\n            </div>\n          </div>\n\n          <div className=\"mt-20 px-5 lg:px-0 mb-6 max-w-4xl text-center text-opacity-80\">\n            <div className=\"uwu-visible flex justify-center\">\n              <img\n                alt=\"logo by @sawaratsuki1004\"\n                title=\"logo by @sawaratsuki1004\"\n                className=\"uwu-visible mb-10 lg:mb-8 h-24 lg:h-32\"\n                src=\"/images/uwu.png\"\n              />\n            </div>\n            <Logo className=\"uwu-hidden text-brand dark:text-brand-dark w-24 lg:w-28 mb-10 lg:mb-8 mt-12 h-auto mx-auto self-start\" />\n            <Header>\n              React 커뮤니티에\n              <Br /> 오신 것을 환영합니다\n            </Header>\n            <ButtonLink\n              href={'/learn'}\n              type=\"primary\"\n              size=\"lg\"\n              label=\"Take the Tutorial\">\n              시작하기\n            </ButtonLink>\n          </div>\n        </Section>\n      </div>\n    </>\n  );\n}\n\nfunction CTA({children, icon, href}) {\n  let Tag;\n  let extraProps;\n  if (href.startsWith('https://')) {\n    Tag = ExternalLink;\n  } else {\n    Tag = NextLink;\n    extraProps = {legacyBehavior: false};\n  }\n  return (\n    <Tag\n      {...extraProps}\n      href={href}\n      className=\"focus:outline-none focus-visible:outline focus-visible:outline-link focus:outline-offset-2 focus-visible:dark:focus:outline-link-dark group cursor-pointer w-auto justify-center inline-flex font-bold items-center mt-10 outline-none hover:bg-gray-40/5 active:bg-gray-40/10 hover:dark:bg-gray-60/5 active:dark:bg-gray-60/10 leading-tight hover:bg-opacity-80 text-lg py-2.5 rounded-full px-4 sm:px-6 ease-in-out shadow-secondary-button-stroke dark:shadow-secondary-button-stroke-dark text-primary dark:text-primary-dark\">\n      {icon === 'native' && (\n        <svg\n          className=\"me-2.5 text-primary dark:text-primary-dark\"\n          fill=\"none\"\n          width=\"24\"\n          height=\"24\"\n          viewBox=\"0 0 72 72\"\n          aria-hidden=\"true\">\n          <g clipPath=\"url(#clip0_8_10998)\">\n            <path\n              d=\"M54.0001 15H18.0001C16.3432 15 15.0001 16.3431 15.0001 18V42H33V48H12.9567L9.10021 57L24.0006 57C24.0006 55.3431 25.3437 54 27.0006 54H33V57.473C33 59.3786 33.3699 61.2582 34.0652 63H9.10021C4.79287 63 1.88869 58.596 3.5852 54.6368L9.0001 42V18C9.0001 13.0294 13.0295 9 18.0001 9H54.0001C58.9707 9 63.0001 13.0294 63.0001 18V25.4411C62.0602 25.0753 61.0589 24.8052 60.0021 24.6458C59.0567 24.5032 58.0429 24.3681 57.0001 24.2587V18C57.0001 16.3431 55.6569 15 54.0001 15Z\"\n              fill=\"currentColor\"\n            />\n            <path\n              d=\"M48 42C48 40.3431 49.3431 39 51 39H54C55.6569 39 57 40.3431 57 42C57 43.6569 55.6569 45 54 45H51C49.3431 45 48 43.6569 48 42Z\"\n              fill=\"currentColor\"\n            />\n            <path\n              fillRule=\"evenodd\"\n              clipRule=\"evenodd\"\n              d=\"M45.8929 30.5787C41.8093 31.1947 39 34.8257 39 38.9556V57.473C39 61.6028 41.8093 65.2339 45.8929 65.8499C48.0416 66.174 50.3981 66.4286 52.5 66.4286C54.6019 66.4286 56.9584 66.174 59.1071 65.8499C63.1907 65.2339 66 61.6028 66 57.473V38.9556C66 34.8258 63.1907 31.1947 59.1071 30.5787C56.9584 30.2545 54.6019 30 52.5 30C50.3981 30 48.0416 30.2545 45.8929 30.5787ZM60 57.473V38.9556C60 37.4615 59.0438 36.637 58.2121 36.5116C56.2014 36.2082 54.1763 36 52.5 36C50.8237 36 48.7986 36.2082 46.7879 36.5116C45.9562 36.637 45 37.4615 45 38.9556V57.473C45 58.9671 45.9562 59.7916 46.7879 59.917C48.7986 60.2203 50.8237 60.4286 52.5 60.4286C54.1763 60.4286 56.2014 60.2203 58.2121 59.917C59.0438 59.7916 60 58.9671 60 57.473Z\"\n              fill=\"currentColor\"\n            />\n          </g>\n          <defs>\n            <clipPath id=\"clip0_8_10998\">\n              <rect width=\"72\" height=\"72\" fill=\"white\" />\n            </clipPath>\n          </defs>\n        </svg>\n      )}\n      {icon === 'framework' && (\n        <svg\n          className=\"me-2.5 text-primary dark:text-primary-dark\"\n          fill=\"none\"\n          width=\"24\"\n          height=\"24\"\n          viewBox=\"0 0 72 72\"\n          aria-hidden=\"true\">\n          <g clipPath=\"url(#clip0_10_21081)\">\n            <path\n              fillRule=\"evenodd\"\n              clipRule=\"evenodd\"\n              d=\"M44.9136 29.0343C46.8321 26.9072 48 24.09 48 21C48 14.3726 42.6274 9 36 9C29.3726 9 24 14.3726 24 21C24 24.0904 25.1682 26.9079 27.0871 29.0351L21.0026 39.3787C20.0429 39.1315 19.0368 39 18 39C11.3726 39 6 44.3726 6 51C6 57.6274 11.3726 63 18 63C23.5915 63 28.2898 59.1757 29.6219 54H42.3781C43.7102 59.1757 48.4085 63 54 63C60.6274 63 66 57.6274 66 51C66 44.3726 60.6274 39 54 39C52.9614 39 51.9537 39.1319 50.9926 39.38L44.9136 29.0343ZM42 21C42 24.3137 39.3137 27 36 27C32.6863 27 30 24.3137 30 21C30 17.6863 32.6863 15 36 15C39.3137 15 42 17.6863 42 21ZM39.9033 32.3509C38.6796 32.7716 37.3665 33 36 33C34.6338 33 33.321 32.7717 32.0975 32.3512L26.2523 42.288C27.8635 43.8146 29.0514 45.7834 29.6219 48H42.3781C42.9482 45.785 44.1348 43.8175 45.7441 42.2913L39.9033 32.3509ZM54 57C50.6863 57 48 54.3137 48 51C48 47.6863 50.6863 45 54 45C57.3137 45 60 47.6863 60 51C60 54.3137 57.3137 57 54 57ZM24 51C24 47.6863 21.3137 45 18 45C14.6863 45 12 47.6863 12 51C12 54.3137 14.6863 57 18 57C21.3137 57 24 54.3137 24 51Z\"\n              fill=\"currentColor\"\n            />\n          </g>\n          <defs>\n            <clipPath id=\"clip0_10_21081\">\n              <rect width=\"72\" height=\"72\" fill=\"white\" />\n            </clipPath>\n          </defs>\n        </svg>\n      )}\n      {icon === 'code' && (\n        <svg\n          className=\"me-2.5 text-primary dark:text-primary-dark\"\n          fill=\"none\"\n          width=\"24\"\n          height=\"24\"\n          viewBox=\"0 0 72 72\"\n          aria-hidden=\"true\">\n          <g clipPath=\"url(#clip0_8_9064)\">\n            <path\n              d=\"M44.7854 22.1142C45.4008 20.5759 44.6525 18.83 43.1142 18.2146C41.5758 17.5993 39.8299 18.3475 39.2146 19.8859L27.2146 49.8859C26.5992 51.4242 27.3475 53.1702 28.8858 53.7855C30.4242 54.4008 32.1701 53.6526 32.7854 52.1142L44.7854 22.1142Z\"\n              fill=\"currentColor\"\n            />\n            <path\n              d=\"M9.87868 38.1214C8.70711 36.9498 8.70711 35.0503 9.87868 33.8787L18.8787 24.8787C20.0503 23.7072 21.9497 23.7072 23.1213 24.8787C24.2929 26.0503 24.2929 27.9498 23.1213 29.1214L16.2426 36.0001L23.1213 42.8787C24.2929 44.0503 24.2929 45.9498 23.1213 47.1214C21.9497 48.293 20.0503 48.293 18.8787 47.1214L9.87868 38.1214Z\"\n              fill=\"currentColor\"\n            />\n            <path\n              d=\"M62.1213 33.8787L53.1213 24.8787C51.9497 23.7072 50.0503 23.7072 48.8787 24.8787C47.7071 26.0503 47.7071 27.9498 48.8787 29.1214L55.7574 36.0001L48.8787 42.8787C47.7071 44.0503 47.7071 45.9498 48.8787 47.1214C50.0503 48.293 51.9497 48.293 53.1213 47.1214L62.1213 38.1214C63.2929 36.9498 63.2929 35.0503 62.1213 33.8787Z\"\n              fill=\"currentColor\"\n            />\n          </g>\n          <defs>\n            <clipPath id=\"clip0_8_9064\">\n              <rect width=\"72\" height=\"72\" fill=\"white\" />\n            </clipPath>\n          </defs>\n        </svg>\n      )}\n      {icon === 'news' && (\n        <svg\n          className=\"me-2.5 text-primary dark:text-primary-dark\"\n          fill=\"none\"\n          width=\"24\"\n          height=\"24\"\n          viewBox=\"0 0 72 72\"\n          aria-hidden=\"true\">\n          <path\n            fillRule=\"evenodd\"\n            clipRule=\"evenodd\"\n            d=\"M12.7101 56.3758C13.0724 56.7251 13.6324 57 14.3887 57H57.6113C58.3676 57 58.9276 56.7251 59.2899 56.3758C59.6438 56.0346 59.8987 55.5407 59.9086 54.864C59.9354 53.022 59.9591 50.7633 59.9756 48H12.0244C12.0409 50.7633 12.0645 53.022 12.0914 54.864C12.1013 55.5407 12.3562 56.0346 12.7101 56.3758ZM12.0024 42H59.9976C59.9992 41.0437 60 40.0444 60 39C60 29.5762 59.9327 22.5857 59.8589 17.7547C59.8359 16.2516 58.6168 15 56.9938 15L15.0062 15C13.3832 15 12.1641 16.2516 12.1411 17.7547C12.0673 22.5857 12 29.5762 12 39C12 40.0444 12.0008 41.0437 12.0024 42ZM65.8582 17.6631C65.7843 12.8227 61.8348 9 56.9938 9H15.0062C10.1652 9 6.21572 12.8227 6.1418 17.6631C6.06753 22.5266 6 29.5477 6 39C6 46.2639 6.03988 51.3741 6.09205 54.9515C6.15893 59.537 9.80278 63 14.3887 63H57.6113C62.1972 63 65.8411 59.537 65.9079 54.9515C65.9601 51.3741 66 46.2639 66 39C66 29.5477 65.9325 22.5266 65.8582 17.6631ZM39 21C37.3431 21 36 22.3431 36 24C36 25.6569 37.3431 27 39 27H51C52.6569 27 54 25.6569 54 24C54 22.3431 52.6569 21 51 21H39ZM36 33C36 31.3431 37.3431 30 39 30H51C52.6569 30 54 31.3431 54 33C54 34.6569 52.6569 36 51 36H39C37.3431 36 36 34.6569 36 33ZM24 33C27.3137 33 30 30.3137 30 27C30 23.6863 27.3137 21 24 21C20.6863 21 18 23.6863 18 27C18 30.3137 20.6863 33 24 33Z\"\n            fill=\"currentColor\"\n          />\n        </svg>\n      )}\n      {children}\n      <svg\n        className=\"text-primary dark:text-primary-dark rtl:rotate-180\"\n        fill=\"none\"\n        width=\"24\"\n        height=\"24\"\n        viewBox=\"0 0 72 72\"\n        aria-hidden=\"true\">\n        <path\n          className=\"transition-transform ease-in-out translate-x-[-8px] group-hover:translate-x-[8px]\"\n          fillRule=\"evenodd\"\n          clipRule=\"evenodd\"\n          d=\"M40.0001 19.0245C41.0912 17.7776 42.9864 17.6513 44.2334 18.7423L58.9758 33.768C59.6268 34.3377 60.0002 35.1607 60.0002 36.0257C60.0002 36.8908 59.6268 37.7138 58.9758 38.2835L44.2335 53.3078C42.9865 54.3988 41.0913 54.2725 40.0002 53.0256C38.9092 51.7786 39.0355 49.8835 40.2824 48.7924L52.4445 36.0257L40.2823 23.2578C39.0354 22.1667 38.9091 20.2714 40.0001 19.0245Z\"\n          fill=\"currentColor\"\n        />\n        <path\n          className=\"opacity-0 ease-in-out transition-opacity group-hover:opacity-100\"\n          d=\"M60 36.0273C60 37.6842 58.6569 39.0273 57 39.0273H15C13.3431 39.0273 12 37.6842 12 36.0273C12 34.3704 13.3431 33.0273 15 33.0273H57C58.6569 33.0273 60 34.3704 60 36.0273Z\"\n          fill=\"currentColor\"\n        />\n      </svg>\n    </Tag>\n  );\n}\n\nconst reactConf2021Cover = '/images/home/conf2021/cover.svg';\nconst reactConf2019Cover = '/images/home/conf2019/cover.svg';\nconst communityImages = [\n  {\n    src: '/images/home/community/react_conf_fun.webp',\n    alt: 'People singing karaoke at React Conf',\n  },\n  {\n    src: '/images/home/community/react_india_sunil.webp',\n    alt: 'Sunil Pai speaking at React India',\n  },\n  {\n    src: '/images/home/community/react_conf_hallway.webp',\n    alt: 'A hallway conversation between two people at React Conf',\n  },\n  {\n    src: '/images/home/community/react_india_hallway.webp',\n    alt: 'A hallway conversation at React India',\n  },\n  {\n    src: '/images/home/community/react_conf_elizabet.webp',\n    alt: 'Elizabet Oliveira speaking at React Conf',\n  },\n  {\n    src: '/images/home/community/react_india_selfie.webp',\n    alt: 'People taking a group selfie at React India',\n  },\n  {\n    src: '/images/home/community/react_conf_nat.webp',\n    alt: 'Nat Alison speaking at React Conf',\n  },\n  {\n    src: '/images/home/community/react_india_team.webp',\n    alt: 'Organizers greeting attendees at React India',\n  },\n];\n\nfunction CommunityGallery() {\n  const ref = useRef();\n\n  const [shouldPlay, setShouldPlay] = useState(false);\n  useEffect(() => {\n    const observer = new IntersectionObserver(\n      (entries) => {\n        entries.forEach((entry) => {\n          setShouldPlay(entry.isIntersecting);\n        });\n      },\n      {\n        root: null,\n        rootMargin: `${window.innerHeight}px 0px`,\n      }\n    );\n    observer.observe(ref.current);\n    return () => observer.disconnect();\n  }, []);\n\n  const [isLazy, setIsLazy] = useState(true);\n  // Either wait until we're scrolling close...\n  useEffect(() => {\n    if (!isLazy) {\n      return;\n    }\n    const rootVertical = parseInt(window.innerHeight * 2.5);\n    const observer = new IntersectionObserver(\n      (entries) => {\n        entries.forEach((entry) => {\n          if (entry.isIntersecting) {\n            setIsLazy(false);\n          }\n        });\n      },\n      {\n        root: null,\n        rootMargin: `${rootVertical}px 0px`,\n      }\n    );\n    observer.observe(ref.current);\n    return () => observer.disconnect();\n  }, [isLazy]);\n  // ... or until it's been a while after hydration.\n  useEffect(() => {\n    const timeout = setTimeout(() => {\n      setIsLazy(false);\n    }, 20 * 1000);\n    return () => clearTimeout(timeout);\n  }, []);\n\n  return (\n    <div ref={ref} className=\"relative flex overflow-x-clip w-auto\">\n      <div\n        className=\"w-full py-12 lg:py-20 whitespace-nowrap flex flex-row animate-marquee lg:animate-large-marquee\"\n        style={{\n          animationPlayState: shouldPlay ? 'running' : 'paused',\n        }}>\n        <CommunityImages isLazy={isLazy} />\n      </div>\n      <div\n        aria-hidden=\"true\"\n        className=\"w-full absolute top-0 py-12 lg:py-20 whitespace-nowrap flex flex-row animate-marquee2 lg:animate-large-marquee2\"\n        style={{\n          animationPlayState: shouldPlay ? 'running' : 'paused',\n        }}>\n        <CommunityImages isLazy={isLazy} />\n      </div>\n    </div>\n  );\n}\n\nconst CommunityImages = memo(function CommunityImages({isLazy}) {\n  return (\n    <>\n      {communityImages.map(({src, alt}, i) => (\n        <div\n          key={i}\n          className={cn(\n            `group flex justify-center px-5 min-w-[50%] lg:min-w-[25%] rounded-2xl`\n          )}>\n          <div\n            className={cn(\n              'h-auto rounded-2xl before:rounded-2xl before:absolute before:pointer-events-none before:inset-0 before:transition-opacity before:-z-1 before:shadow-lg lg:before:shadow-2xl before:opacity-0 before:group-hover:opacity-100  transition-transform ease-in-out duration-300',\n              i % 2 === 0\n                ? 'rotate-2 group-hover:rotate-[-1deg] group-hover:scale-110'\n                : 'group-hover:rotate-1 group-hover:scale-110 rotate-[-2deg]'\n            )}>\n            <div\n              className={cn(\n                'overflow-clip relative before:absolute before:inset-0 before:pointer-events-none before:-translate-x-full group-hover:before:animate-[shimmer_1s_forwards] before:bg-gradient-to-r before:from-transparent before:via-white/10 before:to-transparent transition-transform ease-in-out duration-300'\n              )}>\n              <img\n                loading={isLazy ? 'lazy' : 'eager'}\n                src={src}\n                alt={alt}\n                className=\"aspect-[4/3] h-full w-full flex object-cover rounded-2xl bg-gray-10 dark:bg-gray-80\"\n              />\n            </div>\n          </div>\n        </div>\n      ))}\n    </>\n  );\n});\n\nfunction ExampleLayout({\n  filename,\n  left,\n  right,\n  activeArea,\n  hoverTopOffset = 0,\n}) {\n  const contentRef = useRef(null);\n  useNestedScrollLock(contentRef);\n\n  const [overlayStyles, setOverlayStyles] = useState([]);\n  useEffect(() => {\n    if (activeArea) {\n      const nodes = contentRef.current.querySelectorAll(\n        '[data-hover=\"' + activeArea.name + '\"]'\n      );\n      const nextOverlayStyles = Array.from(nodes)\n        .map((node) => {\n          const parentRect = contentRef.current.getBoundingClientRect();\n          const nodeRect = node.getBoundingClientRect();\n          let top = Math.round(nodeRect.top - parentRect.top) - 8;\n          let bottom = Math.round(nodeRect.bottom - parentRect.top) + 8;\n          let left = Math.round(nodeRect.left - parentRect.left) - 8;\n          let right = Math.round(nodeRect.right - parentRect.left) + 8;\n          top = Math.max(top, hoverTopOffset);\n          bottom = Math.min(bottom, parentRect.height - 12);\n          if (top >= bottom) {\n            return null;\n          }\n          return {\n            width: right - left + 'px',\n            height: bottom - top + 'px',\n            transform: `translate(${left}px, ${top}px)`,\n          };\n        })\n        .filter((s) => s !== null);\n      setOverlayStyles(nextOverlayStyles);\n    }\n  }, [activeArea, hoverTopOffset]);\n  return (\n    <div className=\"lg:ps-10 lg:pe-5 w-full\">\n      <div className=\"mt-12 mb-2 lg:my-16 max-w-7xl mx-auto flex flex-col w-full lg:rounded-2xl lg:bg-card lg:dark:bg-card-dark\">\n        <div className=\"flex-col gap-0 lg:gap-5 lg:rounded-2xl lg:bg-gray-10 lg:dark:bg-gray-70 shadow-inner-border dark:shadow-inner-border-dark lg:flex-row flex grow w-full mx-auto items-center bg-cover bg-center lg:bg-right ltr:lg:bg-[length:60%_100%] bg-no-repeat bg-meta-gradient dark:bg-meta-gradient-dark\">\n          <div className=\"lg:-m-5 h-full shadow-nav dark:shadow-nav-dark lg:rounded-2xl bg-wash dark:bg-gray-95 w-full flex grow flex-col\">\n            <div className=\"w-full bg-card dark:bg-wash-dark lg:rounded-t-2xl border-b border-black/5 dark:border-white/5\">\n              <h3 className=\"text-sm my-1 mx-5 text-tertiary dark:text-tertiary-dark select-none text-start\">\n                {filename}\n              </h3>\n            </div>\n            {left}\n          </div>\n          <div\n            ref={contentRef}\n            className=\"relative mt-0 lg:-my-20 w-full p-2.5 xs:p-5 lg:p-10 flex grow justify-center\"\n            dir=\"ltr\">\n            {right}\n            <div\n              className={cn(\n                'absolute z-10 inset-0 pointer-events-none transition-opacity transform-gpu',\n                activeArea ? 'opacity-100' : 'opacity-0'\n              )}>\n              {overlayStyles.map((styles, i) => (\n                <div\n                  key={i}\n                  className=\"top-0 start-0 bg-blue-30/5 border-2 border-link dark:border-link-dark absolute rounded-lg\"\n                  style={styles}\n                />\n              ))}\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n\nfunction useCodeHover(areas) {\n  const [hoverLine, setHoverLine] = useState(null);\n  const area = areas.get(hoverLine);\n  let meta;\n  if (area) {\n    const highlightLines = area.lines ?? [hoverLine];\n    meta = '```js {' + highlightLines.map((l) => l + 1).join(',') + '}';\n  }\n  return [area, meta, setHoverLine];\n}\n\nconst example1Areas = new Map([\n  [2, {name: 'Video'}],\n  [3, {name: 'Thumbnail'}],\n  [4, {name: 'a'}],\n  [5, {name: 'h3'}],\n  [6, {name: 'p'}],\n  [7, {name: 'a'}],\n  [8, {name: 'LikeButton'}],\n  [9, {name: 'Video'}],\n]);\n\nfunction Example1() {\n  const [area, meta, onLineHover] = useCodeHover(example1Areas);\n  return (\n    <ExampleLayout\n      filename=\"Video.js\"\n      activeArea={area}\n      left={\n        <CodeBlock\n          onLineHover={onLineHover}\n          isFromPackageImport={false}\n          noShadow={true}\n          noMargin={true}>\n          <div meta={meta}>{`function Video({ video }) {\n  return (\n    <div>\n      <Thumbnail video={video} />\n      <a href={video.url}>\n        <h3>{video.title}</h3>\n        <p>{video.description}</p>\n      </a>\n      <LikeButton video={video} />\n    </div>\n  );\n}\n          `}</div>\n        </CodeBlock>\n      }\n      right={\n        <ExamplePanel height=\"113px\">\n          <Video\n            video={{\n              id: 'ex1-0',\n              title: 'My video',\n              description: 'Video description',\n              image: 'blue',\n              url: null,\n            }}\n          />\n        </ExamplePanel>\n      }\n    />\n  );\n}\n\nconst example2Areas = new Map([\n  [8, {name: 'VideoList'}],\n  [9, {name: 'h2'}],\n  [11, {name: 'Video', lines: [11]}],\n  [13, {name: 'VideoList'}],\n]);\n\nfunction Example2() {\n  const [area, meta, onLineHover] = useCodeHover(example2Areas);\n  const videos = [\n    {\n      id: 'ex2-0',\n      title: 'First video',\n      description: 'Video description',\n      image: 'blue',\n    },\n    {\n      id: 'ex2-1',\n      title: 'Second video',\n      description: 'Video description',\n      image: 'red',\n    },\n    {\n      id: 'ex2-2',\n      title: 'Third video',\n      description: 'Video description',\n      image: 'green',\n    },\n  ];\n\n  return (\n    <ExampleLayout\n      filename=\"VideoList.js\"\n      activeArea={area}\n      left={\n        <CodeBlock\n          onLineHover={onLineHover}\n          isFromPackageImport={false}\n          noShadow={true}\n          noMargin={true}>\n          <div meta={meta}>{`function VideoList({ videos, emptyHeading }) {\n  const count = videos.length;\n  let heading = emptyHeading;\n  if (count > 0) {\n    const noun = count > 1 ? 'Videos' : 'Video';\n    heading = count + ' ' + noun;\n  }\n  return (\n    <section>\n      <h2>{heading}</h2>\n      {videos.map(video =>\n        <Video key={video.id} video={video} />\n      )}\n    </section>\n  );\n}`}</div>\n        </CodeBlock>\n      }\n      right={\n        <ExamplePanel height=\"22rem\" noShadow={false} noPadding={true}>\n          <div className=\"m-4\">\n            <VideoList videos={videos} />\n          </div>\n        </ExamplePanel>\n      }\n    />\n  );\n}\n\nconst example3Areas = new Map([\n  [6, {name: 'SearchableVideoList'}],\n  [7, {name: 'SearchInput', lines: [7, 8, 9]}],\n  [8, {name: 'SearchInput', lines: [7, 8, 9]}],\n  [9, {name: 'SearchInput', lines: [7, 8, 9]}],\n  [10, {name: 'VideoList', lines: [10, 11, 12]}],\n  [11, {name: 'VideoList', lines: [10, 11, 12]}],\n  [12, {name: 'VideoList', lines: [10, 11, 12]}],\n  [13, {name: 'SearchableVideoList'}],\n]);\n\nfunction Example3() {\n  const [area, meta, onLineHover] = useCodeHover(example3Areas);\n  const videos = [\n    {\n      id: 'vids-0',\n      title: 'React: The Documentary',\n      description: 'The origin story of React',\n      image: '/images/home/videos/documentary.webp',\n      url: 'https://www.youtube.com/watch?v=8pDqJVdNa44',\n    },\n    {\n      id: 'vids-1',\n      title: 'Rethinking Best Practices',\n      description: 'Pete Hunt (2013)',\n      image: '/images/home/videos/rethinking.jpg',\n      url: 'https://www.youtube.com/watch?v=x7cQ3mrcKaY',\n    },\n    {\n      id: 'vids-2',\n      title: 'Introducing React Native',\n      description: 'Tom Occhino (2015)',\n      image: '/images/home/videos/rn.jpg',\n      url: 'https://www.youtube.com/watch?v=KVZ-P-ZI6W4',\n    },\n    {\n      id: 'vids-3',\n      title: 'Introducing React Hooks',\n      description: 'Sophie Alpert and Dan Abramov (2018)',\n      image: '/images/home/videos/hooks.jpg',\n      url: 'https://www.youtube.com/watch?v=V-QO-KO90iQ',\n    },\n    {\n      id: 'vids-4',\n      title: 'Introducing Server Components',\n      description: 'Dan Abramov and Lauren Tan (2020)',\n      image: '/images/home/videos/rsc.jpg',\n      url: 'https://www.youtube.com/watch?v=TQQPAU21ZUw',\n    },\n  ];\n\n  return (\n    <ExampleLayout\n      filename=\"SearchableVideoList.js\"\n      activeArea={area}\n      hoverTopOffset={60}\n      left={\n        <CodeBlock\n          onLineHover={onLineHover}\n          isFromPackageImport={false}\n          noShadow={true}\n          noMargin={true}>\n          <div meta={meta}>{`import { useState } from 'react';\n\nfunction SearchableVideoList({ videos }) {\n  const [searchText, setSearchText] = useState('');\n  const foundVideos = filterVideos(videos, searchText);\n  return (\n    <>\n      <SearchInput\n        value={searchText}\n        onChange={newText => setSearchText(newText)} />\n      <VideoList\n        videos={foundVideos}\n        emptyHeading={\\`No matches for “\\${searchText}”\\`} />\n    </>\n  );\n}`}</div>\n        </CodeBlock>\n      }\n      right={\n        <BrowserChrome domain=\"example.com\" path={'videos.html'}>\n          <ExamplePanel\n            noShadow={false}\n            noPadding={true}\n            contentMarginTop=\"72px\"\n            height=\"30rem\">\n            <h1 className=\"mx-4 mb-1 font-bold text-3xl text-primary\">\n              React Videos\n            </h1>\n            <p className=\"mx-4 mb-0 leading-snug text-secondary text-xl\">\n              A brief history of React\n            </p>\n            <div className=\"px-4 pb-4\">\n              <SearchableVideoList videos={videos} />\n            </div>\n          </ExamplePanel>\n        </BrowserChrome>\n      }\n    />\n  );\n}\n\nconst example4Areas = new Map([\n  [6, {name: 'ConferenceLayout'}],\n  [7, {name: 'Suspense'}],\n  [8, {name: 'SearchableVideoList'}],\n  [9, {name: 'Suspense'}],\n  [10, {name: 'ConferenceLayout'}],\n  [17, {name: 'SearchableVideoList'}],\n]);\n\nfunction Example4() {\n  const [area, meta, onLineHover] = useCodeHover(example4Areas);\n  const [slug, setSlug] = useState('react-conf-2021');\n  const [animate, setAnimate] = useState(false);\n\n  function navigate(newSlug) {\n    setSlug(newSlug);\n    setAnimate(true);\n  }\n\n  return (\n    <ExampleLayout\n      filename=\"confs/[slug].js\"\n      activeArea={area}\n      hoverTopOffset={60}\n      left={\n        <CodeBlock\n          onLineHover={onLineHover}\n          isFromPackageImport={false}\n          noShadow={true}\n          noMargin={true}>\n          <div meta={meta}>{`import { db } from './database.js';\nimport { Suspense } from 'react';\n\nasync function ConferencePage({ slug }) {\n  const conf = await db.Confs.find({ slug });\n  return (\n    <ConferenceLayout conf={conf}>\n      <Suspense fallback={<TalksLoading />}>\n        <Talks confId={conf.id} />\n      </Suspense>\n    </ConferenceLayout>\n  );\n}\n\nasync function Talks({ confId }) {\n  const talks = await db.Talks.findAll({ confId });\n  const videos = talks.map(talk => talk.video);\n  return <SearchableVideoList videos={videos} />;\n}`}</div>\n        </CodeBlock>\n      }\n      right={\n        <NavContext value={{slug, navigate}}>\n          <BrowserChrome\n            domain=\"example.com\"\n            path={'confs/' + slug}\n            hasRefresh={true}\n            hasPulse={true}>\n            <ExamplePanel\n              noPadding={true}\n              noShadow={true}\n              contentMarginTop=\"56px\"\n              height=\"35rem\">\n              <Suspense fallback={null}>\n                <div style={{animation: animate ? 'fadein 200ms' : null}}>\n                  <link rel=\"preload\" href={reactConf2019Cover} as=\"image\" />\n                  <link rel=\"preload\" href={reactConf2021Cover} as=\"image\" />\n                  <ConferencePage slug={slug} />\n                </div>\n              </Suspense>\n            </ExamplePanel>\n          </BrowserChrome>\n        </NavContext>\n      }\n    />\n  );\n}\n\nfunction useNestedScrollLock(ref) {\n  useEffect(() => {\n    let node = ref.current;\n    let isLocked = false;\n    let lastScroll = performance.now();\n\n    function handleScroll() {\n      if (!isLocked) {\n        isLocked = true;\n        node.style.pointerEvents = 'none';\n      }\n      lastScroll = performance.now();\n    }\n\n    function updateLock() {\n      if (isLocked && performance.now() - lastScroll > 150) {\n        isLocked = false;\n        node.style.pointerEvents = '';\n      }\n    }\n\n    window.addEventListener('scroll', handleScroll);\n    const interval = setInterval(updateLock, 60);\n    return () => {\n      window.removeEventListener('scroll', handleScroll);\n      clearInterval(interval);\n    };\n  }, [ref]);\n}\n\nfunction ExamplePanel({\n  children,\n  noPadding,\n  noShadow,\n  height,\n  contentMarginTop,\n}) {\n  return (\n    <div\n      className={cn(\n        'max-w-3xl rounded-2xl mx-auto text-secondary leading-normal bg-white overflow-hidden w-full overflow-y-auto',\n        noShadow ? 'shadow-none' : 'shadow-nav dark:shadow-nav-dark'\n      )}\n      style={{height}}>\n      <div\n        className={noPadding ? 'p-0' : 'p-4'}\n        style={{contentVisibility: 'auto', marginTop: contentMarginTop}}>\n        {children}\n      </div>\n    </div>\n  );\n}\n\nconst NavContext = createContext(null);\n\nfunction BrowserChrome({children, hasPulse, hasRefresh, domain, path}) {\n  const [restartId, setRestartId] = useState(0);\n  const isPulsing = hasPulse && restartId === 0;\n  const [shouldAnimatePulse, setShouldAnimatePulse] = useState(false);\n  const refreshRef = useRef(null);\n\n  useEffect(() => {\n    if (!isPulsing) {\n      return;\n    }\n    const observer = new IntersectionObserver(\n      (entries) => {\n        entries.forEach((entry) => {\n          setShouldAnimatePulse(entry.isIntersecting);\n        });\n      },\n      {\n        root: null,\n        rootMargin: `0px 0px`,\n      }\n    );\n    observer.observe(refreshRef.current);\n    return () => observer.disconnect();\n  }, [isPulsing]);\n\n  function handleRestart() {\n    confCache = new Map();\n    talksCache = new Map();\n    setRestartId((i) => i + 1);\n  }\n\n  return (\n    <div className=\"mx-auto max-w-3xl shadow-nav dark:shadow-nav-dark relative overflow-hidden w-full dark:border-opacity-10 rounded-2xl\">\n      <div className=\"w-full h-14 rounded-t-2xl shadow-outer-border backdrop-filter overflow-hidden backdrop-blur-lg backdrop-saturate-200 bg-white bg-opacity-90 z-10 absolute top-0 px-3 gap-2 flex flex-row items-center\">\n        <div className=\"select-none h-8 relative bg-gray-30/20 text-sm text-tertiary text-center rounded-full w-full flex-row flex space-between items-center\">\n          {hasRefresh && <div className=\"h-4 w-6\" />}\n          <div className=\"w-full leading-snug flex flex-row items-center justify-center\">\n            <svg\n              className=\"text-tertiary me-1 opacity-60\"\n              width=\"12\"\n              height=\"12\"\n              viewBox=\"0 0 44 44\"\n              fill=\"none\"\n              xmlns=\"http://www.w3.org/2000/svg\">\n              <path\n                fillRule=\"evenodd\"\n                clipRule=\"evenodd\"\n                d=\"M22 4C17.0294 4 13 8.0294 13 13V16H12.3103C10.5296 16 8.8601 16.8343 8.2855 18.5198C7.6489 20.387 7 23.4148 7 28C7 32.5852 7.6489 35.613 8.2855 37.4802C8.8601 39.1657 10.5296 40 12.3102 40H31.6897C33.4704 40 35.1399 39.1657 35.7145 37.4802C36.3511 35.613 37 32.5852 37 28C37 23.4148 36.3511 20.387 35.7145 18.5198C35.1399 16.8343 33.4704 16 31.6897 16H31V13C31 8.0294 26.9706 4 22 4ZM25 16V13C25 11.3431 23.6569 10 22 10C20.3431 10 19 11.3431 19 13V16H25Z\"\n                fill=\"currentColor\"\n              />\n            </svg>\n\n            <span className=\"text-gray-30\">\n              {domain}\n              {path != null && '/'}\n            </span>\n            {path}\n          </div>\n          {hasRefresh && (\n            <div\n              ref={refreshRef}\n              className={cn(\n                'relative rounded-full flex justify-center items-center ',\n                isPulsing && shouldAnimatePulse && 'animation-pulse-button'\n              )}>\n              {isPulsing && shouldAnimatePulse && (\n                <div className=\"z-0 absolute shadow-[0_0_0_8px_rgba(0,0,0,0.5)] inset-0 rounded-full animation-pulse-shadow\" />\n              )}\n              <button\n                aria-label=\"Reload\"\n                onClick={handleRestart}\n                className={\n                  'z-10 flex items-center p-1.5 rounded-full cursor-pointer justify-center' +\n                  // bg-transparent hover:bg-gray-20/50,\n                  // but opaque to obscure the pulsing wave.\n                  ' bg-[#ebecef] hover:bg-[#d3d7de]'\n                }>\n                <IconRestart className=\"text-tertiary text-lg\" />\n              </button>\n            </div>\n          )}\n        </div>\n        {restartId > 0 && (\n          <div\n            key={restartId}\n            className=\"z-10 loading h-0.5 bg-link transition-all duration-200 absolute bottom-0 start-0\"\n            style={{\n              animation: `progressbar ${loadTalksDelay + 100}ms ease-in-out`,\n            }}\n          />\n        )}\n      </div>\n      <div className=\"h-full flex flex-1\" key={restartId}>\n        {children}\n      </div>\n    </div>\n  );\n}\n\nfunction ConferencePage({slug}) {\n  const conf = use(fetchConf(slug));\n  return (\n    <ConferenceLayout conf={conf}>\n      <div data-hover=\"Suspense\">\n        <Suspense fallback={<TalksLoading />}>\n          <Talks confId={conf.id} />\n        </Suspense>\n      </div>\n    </ConferenceLayout>\n  );\n}\n\nfunction TalksLoading() {\n  return (\n    <div className=\"flex flex-col items-center h-[25rem] overflow-hidden\">\n      <div className=\"w-full\">\n        <div className=\"relative overflow-hidden before:-skew-x-12 before:absolute before:inset-0 before:-translate-x-full before:animate-[shimmer_2.5s_infinite] before:bg-gradient-to-r before:from-transparent before:via-white/50 before:to-transparent\">\n          <div className=\"space-y-4\">\n            <div className=\"pt-4 pb-1\">\n              <div className=\"h-10 w-full rounded-full bg-gray-10\"></div>\n            </div>\n            <div className=\"pb-1\">\n              <div className=\"h-5 w-20 rounded-lg bg-gray-10\"></div>\n            </div>\n            <div className=\"flex flex-row items-center gap-3\">\n              <div className=\"aspect-video w-32 xs:w-36 rounded-lg bg-gray-10\"></div>\n              <div className=\"flex flex-col gap-2\">\n                <div className=\"h-3 w-40 rounded-lg bg-gray-10\"></div>\n                <div className=\"h-3 w-32 rounded-lg bg-gray-10\"></div>\n                <div className=\"h-3 w-24 rounded-lg bg-gray-10\"></div>\n              </div>\n            </div>\n            <div className=\"flex flex-row items-center gap-3\">\n              <div className=\"aspect-video w-32 xs:w-36 rounded-lg bg-gray-10\"></div>\n              <div className=\"flex flex-col gap-2\">\n                <div className=\"h-3 w-40 rounded-lg bg-gray-10\"></div>\n                <div className=\"h-3 w-32 rounded-lg bg-gray-10\"></div>\n                <div className=\"h-3 w-24 rounded-lg bg-gray-10\"></div>\n              </div>\n            </div>\n            <div className=\"flex flex-row items-center gap-3\">\n              <div className=\"aspect-video w-32 xs:w-36 rounded-lg bg-gray-10\"></div>\n              <div className=\"flex flex-col gap-2\">\n                <div className=\"h-3 w-40 rounded-lg bg-gray-10\"></div>\n                <div className=\"h-3 w-32 rounded-lg bg-gray-10\"></div>\n                <div className=\"h-3 w-24 rounded-lg bg-gray-10\"></div>\n              </div>\n            </div>\n            <div className=\"flex flex-row items-center gap-3\">\n              <div className=\"aspect-video w-32 xs:w-36 rounded-lg bg-gray-10\"></div>\n              <div className=\"flex flex-col gap-2\">\n                <div className=\"h-3 w-40 rounded-lg bg-gray-10\"></div>\n                <div className=\"h-3 w-32 rounded-lg bg-gray-10\"></div>\n                <div className=\"h-3 w-24 rounded-lg bg-gray-10\"></div>\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n\nfunction Talks({confId}) {\n  const videos = use(fetchTalks(confId));\n  return <SearchableVideoList videos={videos} />;\n}\n\nfunction SearchableVideoList({videos}) {\n  const [searchText, setSearchText] = useState('');\n  const foundVideos = filterVideos(videos, searchText);\n  return (\n    <div className=\"mt-3\" data-hover=\"SearchableVideoList\">\n      <SearchInput value={searchText} onChange={setSearchText} />\n      <VideoList\n        videos={foundVideos}\n        emptyHeading={`No matches for “${searchText}”`}\n      />\n    </div>\n  );\n}\n\nfunction filterVideos(videos, query) {\n  const keywords = query\n    .toLowerCase()\n    .split(' ')\n    .filter((s) => s !== '');\n  if (keywords.length === 0) {\n    return videos;\n  }\n  return videos.filter((video) => {\n    const words = (video.title + ' ' + video.description)\n      .toLowerCase()\n      .split(' ');\n    return keywords.every((kw) => words.some((w) => w.startsWith(kw)));\n  });\n}\n\nfunction VideoList({videos, emptyHeading}) {\n  let heading = emptyHeading;\n  const count = videos.length;\n  if (count > 0) {\n    const noun = count > 1 ? 'Videos' : 'Video';\n    heading = count + ' ' + noun;\n  }\n  return (\n    <section className=\"relative\" data-hover=\"VideoList\">\n      <h2\n        className=\"font-bold text-xl text-primary mb-4 leading-snug\"\n        data-hover=\"h2\">\n        {heading}\n      </h2>\n      <div className=\"flex flex-col gap-4\">\n        {videos.map((video) => (\n          <Video key={video.id} video={video} />\n        ))}\n      </div>\n    </section>\n  );\n}\n\nfunction SearchInput({value, onChange}) {\n  const id = useId();\n  return (\n    <form\n      className=\"mb-3 py-1\"\n      data-hover=\"SearchInput\"\n      onSubmit={(e) => e.preventDefault()}>\n      <label htmlFor={id} className=\"sr-only\">\n        Search\n      </label>\n      <div className=\"relative w-full\">\n        <div className=\"absolute inset-y-0 start-0 flex items-center ps-4 pointer-events-none\">\n          <IconSearch className=\"text-gray-30 w-4\" />\n        </div>\n        <input\n          type=\"text\"\n          id={id}\n          className=\"flex ps-11 py-4 h-10 w-full text-start bg-secondary-button outline-none betterhover:hover:bg-opacity-80 pointer items-center text-primary rounded-full align-middle text-base\"\n          placeholder=\"Search\"\n          value={value}\n          onChange={(e) => onChange(e.target.value)}\n        />\n      </div>\n    </form>\n  );\n}\n\nfunction ConferenceLayout({conf, children}) {\n  const {slug, navigate} = useContext(NavContext);\n  const [isPending, startTransition] = useTransition();\n  return (\n    <div\n      className={cn(\n        'transition-opacity delay-100',\n        isPending ? 'opacity-90' : 'opacity-100'\n      )}\n      data-hover=\"ConferenceLayout\">\n      <Cover background={conf.cover}>\n        <select\n          aria-label=\"Event\"\n          defaultValue={slug}\n          onChange={(e) => {\n            startTransition(() => {\n              navigate(e.target.value);\n            });\n          }}\n          className=\"appearance-none pe-8 ps-2 bg-transparent text-primary-dark text-2xl font-bold mb-0.5\"\n          style={{\n            backgroundSize: '4px 4px, 4px 4px',\n            backgroundRepeat: 'no-repeat',\n            backgroundPosition:\n              'calc(100% - 20px) calc(1px + 50%),calc(100% - 16px) calc(1px + 50%)',\n            backgroundImage:\n              'linear-gradient(45deg,transparent 50%,currentColor 50%),linear-gradient(135deg,currentColor 50%,transparent 50%)',\n          }}>\n          <option\n            className=\"bg-wash dark:bg-wash-dark text-primary dark:text-primary-dark\"\n            value=\"react-conf-2021\">\n            React Conf 2021\n          </option>\n          <option\n            className=\"bg-wash dark:bg-wash-dark text-primary dark:text-primary-dark\"\n            value=\"react-conf-2019\">\n            React Conf 2019\n          </option>\n        </select>\n      </Cover>\n      <div className=\"px-4 pb-4\" key={conf.id}>\n        {children}\n      </div>\n    </div>\n  );\n}\n\nfunction Cover({background, children}) {\n  return (\n    <div className=\"h-40 overflow-hidden relative items-center flex\">\n      <div className=\"absolute inset-0 px-4 py-2 flex items-end bg-gradient-to-t from-black/40 via-black/0\">\n        {children}\n      </div>\n      <img\n        src={background}\n        width={500}\n        height={263}\n        alt=\"\"\n        className=\"w-full object-cover\"\n      />\n    </div>\n  );\n}\n\nfunction Video({video}) {\n  return (\n    <div className=\"flex flex-row items-center gap-3\" data-hover=\"Video\">\n      <Thumbnail video={video} />\n      <a\n        href={video.url}\n        target=\"_blank\"\n        rel=\"noreferrer\"\n        className=\"outline-link dark:outline-link outline-offset-4 group flex flex-col flex-1 gap-0.5\"\n        data-hover=\"a\">\n        <h3\n          className={cn(\n            'text-base leading-tight text-primary font-bold',\n            video.url && 'group-hover:underline'\n          )}\n          data-hover=\"h3\">\n          {video.title}\n        </h3>\n        <p className=\"text-tertiary text-sm leading-snug\" data-hover=\"p\">\n          {video.description}\n        </p>\n      </a>\n      <LikeButton video={video} />\n    </div>\n  );\n}\n\nfunction Code({children}) {\n  return (\n    <code\n      dir=\"ltr\"\n      className=\"font-mono inline rounded-lg bg-gray-15/40 dark:bg-secondary-button-dark py-0.5 px-1 text-left\">\n      {children}\n    </code>\n  );\n}\n\nfunction Thumbnail({video}) {\n  const {image} = video;\n  return (\n    <a\n      data-hover=\"Thumbnail\"\n      href={video.url}\n      target=\"_blank\"\n      rel=\"noreferrer\"\n      aria-hidden=\"true\"\n      tabIndex={-1}\n      className={cn(\n        'outline-link dark:outline-link outline-offset-2 aspect-video w-32 xs:w-36 select-none flex-col shadow-inner-border rounded-lg flex items-center overflow-hidden justify-center align-middle text-white/50 bg-cover bg-white bg-[conic-gradient(at_top_right,_var(--tw-gradient-stops))]',\n        image === 'blue' && 'from-yellow-50 via-blue-50 to-purple-60',\n        image === 'red' && 'from-yellow-50 via-red-50 to-purple-60',\n        image === 'green' && 'from-yellow-50 via-green-50 to-purple-60',\n        image === 'purple' && 'from-yellow-50 via-purple-50 to-purple-60',\n        typeof image === 'object' && 'from-gray-80 via-gray-95 to-gray-70',\n        video.url && 'hover:opacity-95 transition-opacity'\n      )}\n      style={{\n        backgroundImage:\n          typeof image === 'string' && image.startsWith('/')\n            ? 'url(' + image + ')'\n            : null,\n      }}>\n      {typeof image !== 'string' ? (\n        <>\n          <div className=\"transition-opacity mt-2.5 -space-x-2 flex flex-row w-full justify-center\">\n            {image.speakers.map((src, i) => (\n              <img\n                key={i}\n                className=\"h-8 w-8 border-2 shadow-md border-gray-70 object-cover rounded-full\"\n                src={src}\n                alt=\"\"\n              />\n            ))}\n          </div>\n          <div className=\"mt-1\">\n            <span className=\"inline-flex text-xs font-normal items-center text-primary-dark py-1 whitespace-nowrap outline-link px-1.5 rounded-lg\">\n              <Logo className=\"text-xs me-1 w-4 h-4 text-brand text-brand-dark\" />\n              React Conf\n            </span>\n          </div>\n        </>\n      ) : image.startsWith('/') ? null : (\n        <ThumbnailPlaceholder />\n      )}\n    </a>\n  );\n}\n\nfunction ThumbnailPlaceholder() {\n  return (\n    <svg\n      className=\"drop-shadow-xl\"\n      width=\"36\"\n      height=\"36\"\n      viewBox=\"0 0 72 72\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\">\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M36 69C54.2254 69 69 54.2254 69 36C69 17.7746 54.2254 3 36 3C17.7746 3 3 17.7746 3 36C3 54.2254 17.7746 69 36 69ZM52.1716 38.6337L28.4366 51.5801C26.4374 52.6705 24 51.2235 24 48.9464V23.0536C24 20.7764 26.4374 19.3295 28.4366 20.4199L52.1716 33.3663C54.2562 34.5034 54.2562 37.4966 52.1716 38.6337Z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  );\n}\n\n// A hack since we don't actually have a backend.\n// Unlike local state, this survives videos being filtered.\nconst likedVideos = new Set();\n\nfunction LikeButton({video}) {\n  const [isLiked, setIsLiked] = useState(() => likedVideos.has(video.id));\n  const [animate, setAnimate] = useState(false);\n  return (\n    <button\n      data-hover=\"LikeButton\"\n      className={cn(\n        'outline-none focus:bg-red-50/5 focus:text-red-50 relative flex items-center justify-center w-10 h-10 cursor-pointer rounded-full hover:bg-card active:scale-95 active:bg-red-50/5 active:text-red-50',\n        isLiked ? 'text-red-50' : 'text-tertiary'\n      )}\n      aria-label={isLiked ? 'Unsave' : 'Save'}\n      onClick={() => {\n        const nextIsLiked = !isLiked;\n        if (nextIsLiked) {\n          likedVideos.add(video.id);\n        } else {\n          likedVideos.delete(video.id);\n        }\n        setAnimate(true);\n        setIsLiked(nextIsLiked);\n      }}>\n      <svg\n        className=\"absolute overflow-visible\"\n        viewBox=\"0 0 24 24\"\n        fill=\"none\"\n        xmlns=\"http://www.w3.org/2000/svg\">\n        <circle\n          className={cn(\n            'text-red-50/50 origin-center transition-all ease-in-out',\n            isLiked && animate && 'animate-[circle_.3s_forwards]'\n          )}\n          cx=\"12\"\n          cy=\"12\"\n          r=\"11.5\"\n          fill=\"transparent\"\n          strokeWidth=\"0\"\n          stroke=\"currentColor\"\n        />\n      </svg>\n      {isLiked ? (\n        <svg\n          className={cn(\n            'w-6 h-6 origin-center transition-all ease-in-out',\n            isLiked && animate && 'animate-[scale_.35s_ease-in-out_forwards]'\n          )}\n          viewBox=\"0 0 24 24\"\n          fill=\"none\"\n          xmlns=\"http://www.w3.org/2000/svg\">\n          <path\n            d=\"M12 23a.496.496 0 0 1-.26-.074C7.023 19.973 0 13.743 0 8.68c0-4.12 2.322-6.677 6.058-6.677 2.572 0 5.108 2.387 5.134 2.41l.808.771.808-.771C12.834 4.387 15.367 2 17.935 2 21.678 2 24 4.558 24 8.677c0 5.06-7.022 11.293-11.74 14.246a.496.496 0 0 1-.26.074V23z\"\n            fill=\"currentColor\"\n          />\n        </svg>\n      ) : (\n        <svg\n          className=\"w-6 h-6\"\n          viewBox=\"0 0 24 24\"\n          fill=\"none\"\n          xmlns=\"http://www.w3.org/2000/svg\">\n          <path\n            fillRule=\"evenodd\"\n            clipRule=\"evenodd\"\n            d=\"m12 5.184-.808-.771-.004-.004C11.065 4.299 8.522 2.003 6 2.003c-3.736 0-6 2.558-6 6.677 0 4.47 5.471 9.848 10 13.079.602.43 1.187.82 1.74 1.167A.497.497 0 0 0 12 23v-.003c.09 0 .182-.026.26-.074C16.977 19.97 24 13.737 24 8.677 24 4.557 21.743 2 18 2c-2.569 0-5.166 2.387-5.192 2.413L12 5.184zm-.002 15.525c2.071-1.388 4.477-3.342 6.427-5.47C20.72 12.733 22 10.401 22 8.677c0-1.708-.466-2.855-1.087-3.55C20.316 4.459 19.392 4 18 4c-.726 0-1.63.364-2.5.9-.67.412-1.148.82-1.266.92-.03.025-.037.031-.019.014l-.013.013L12 7.949 9.832 5.88a10.08 10.08 0 0 0-1.33-.977C7.633 4.367 6.728 4.003 6 4.003c-1.388 0-2.312.459-2.91 1.128C2.466 5.826 2 6.974 2 8.68c0 1.726 1.28 4.058 3.575 6.563 1.948 2.127 4.352 4.078 6.423 5.466z\"\n            fill=\"currentColor\"\n          />\n        </svg>\n      )}\n    </button>\n  );\n}\nfunction SvgContainer({children}) {\n  return (\n    <svg\n      className=\"w-16 h-16 lg:w-20 lg:h-20 rounded-2xl lg:rounded-3xl shadow-nav bg-wash\"\n      viewBox=\"0 0 120 120\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\">\n      {children}\n    </svg>\n  );\n}\n\nfunction NativeIcons() {\n  return (\n    <div className=\"flex items-center justify-center gap-5\">\n      <SvgContainer>\n        <path\n          d=\"M89.9356 44.0658C89.4752 44.4231 81.3451 49.0042 81.3451 59.1906C81.3451 70.9729 91.6903 75.1411 91.9999 75.2443C91.9523 75.4984 90.3564 80.9529 86.5455 86.5105C83.1474 91.4013 79.5984 96.2841 74.1995 96.2841C68.8006 96.2841 67.4112 93.148 61.1787 93.148C55.105 93.148 52.9454 96.3873 48.007 96.3873C43.0686 96.3873 39.6229 91.8618 35.6611 86.3041C31.072 79.7778 27.3643 69.639 27.3643 60.0163C27.3643 44.5819 37.3998 36.3963 47.2766 36.3963C52.5246 36.3963 56.8993 39.842 60.1942 39.842C63.3303 39.842 68.221 36.1898 74.1916 36.1898C76.4543 36.1898 84.5844 36.3963 89.9356 44.0658ZM71.3572 29.6556C73.8264 26.7259 75.573 22.6609 75.573 18.5958C75.573 18.0321 75.5254 17.4605 75.4222 17C71.4048 17.1509 66.6252 19.6756 63.7432 23.0182C61.4804 25.5906 59.3685 29.6556 59.3685 33.7762C59.3685 34.3955 59.4717 35.0148 59.5193 35.2133C59.7734 35.2609 60.1863 35.3165 60.5991 35.3165C64.2036 35.3165 68.7371 32.9029 71.3572 29.6556Z\"\n          fill=\"black\"\n        />\n      </SvgContainer>\n      <SvgContainer>\n        <path\n          d=\"M22.1378 84.6843C19.1119 84.6843 16 87.1151 16 91.363C16 95.259 18.7358 98 22.1378 98C24.9457 98 26.1963 96.1105 26.1963 96.1105V96.9286C26.2071 97.1436 26.2971 97.3469 26.4489 97.4991C26.6008 97.6513 26.8036 97.7416 27.018 97.7523H29.0473V84.9626H26.1963V86.5864C26.1963 86.5864 24.9346 84.6843 22.1378 84.6843ZM22.6458 87.3002C25.1359 87.3002 26.4434 89.4958 26.4434 91.3686C26.4434 93.4557 24.8916 95.4371 22.65 95.4371C20.7775 95.4371 18.9023 93.9163 18.9023 91.3422C18.9009 89.0102 20.5152 87.2932 22.6458 87.2932V87.3002Z\"\n          fill=\"black\"\n        />\n        <path\n          d=\"M33.01 97.7636C32.9013 97.7667 32.793 97.7475 32.692 97.7072C32.5909 97.6669 32.4991 97.6063 32.4222 97.5292C32.3452 97.4521 32.2848 97.3601 32.2446 97.2587C32.2044 97.1574 32.1852 97.0489 32.1883 96.9399V84.9628H35.0407V86.5462C35.6861 85.5722 36.9478 84.6692 38.8855 84.6692C42.0529 84.6692 43.7435 87.1988 43.7435 89.5655V97.7636H41.7573C41.5268 97.7625 41.3061 97.6703 41.1431 97.5069C40.9801 97.3435 40.8881 97.1223 40.887 96.8912V90.2C40.887 88.8893 40.0861 87.2962 38.2317 87.2962C36.2316 87.2962 35.0393 89.1912 35.0393 90.975V97.7636H33.01Z\"\n          fill=\"black\"\n        />\n        <path\n          d=\"M52.0506 84.6843C49.0248 84.6843 45.9128 87.1151 45.9128 91.363C45.9128 95.259 48.6486 98 52.0506 98C54.8641 98 56.1133 96.1105 56.1133 96.1105V96.9286C56.1241 97.1436 56.2141 97.3469 56.3659 97.4991C56.5178 97.6513 56.7206 97.7416 56.9351 97.7523H58.9643V78.5747H56.1133V86.5919C56.1133 86.5919 54.8475 84.6843 52.0506 84.6843ZM52.5586 87.3002C55.0487 87.3002 56.3562 89.4958 56.3562 91.3686C56.3562 93.4557 54.8045 95.4371 52.5628 95.4371C50.6904 95.4371 48.8152 93.9163 48.8152 91.3422C48.8138 89.0102 50.4225 87.2932 52.5586 87.2932V87.3002Z\"\n          fill=\"black\"\n        />\n        <path\n          d=\"M62.9232 97.7634C62.8145 97.7665 62.7064 97.7473 62.6054 97.707C62.5044 97.6667 62.4126 97.6061 62.3358 97.5289C62.259 97.4518 62.1987 97.3598 62.1587 97.2584C62.1186 97.1571 62.0996 97.0487 62.1029 96.9397V84.9626H64.9539V87.0942C65.4438 85.9004 66.5029 84.8179 68.385 84.8179C68.7253 84.8211 69.0648 84.8532 69.3997 84.9139V87.8692C68.9654 87.7128 68.5078 87.631 68.0464 87.6271C66.0462 87.6271 64.9539 89.5222 64.9539 91.3073V97.7634H62.9232Z\"\n          fill=\"black\"\n        />\n        <path\n          d=\"M86.6997 97.7635C86.591 97.7668 86.4826 97.7477 86.3815 97.7075C86.2803 97.6673 86.1884 97.6067 86.1114 97.5295C86.0345 97.4524 85.9741 97.3603 85.9339 97.2589C85.8938 97.1574 85.8748 97.0489 85.878 96.9398V84.9626H88.7318V97.7635H86.6997Z\"\n          fill=\"black\"\n        />\n        <path\n          d=\"M97.089 84.6843C94.0632 84.6843 90.9526 87.1151 90.9526 91.363C90.9526 95.259 93.6884 98 97.089 98C99.897 98 101.149 96.1105 101.149 96.1105V96.9286C101.16 97.1436 101.25 97.3469 101.402 97.4991C101.553 97.6513 101.756 97.7416 101.971 97.7523H104V78.5747H101.149V86.5919C101.149 86.5919 99.8873 84.6843 97.089 84.6843ZM97.5971 87.3002C100.095 87.3002 101.395 89.4958 101.395 91.3686C101.395 93.4557 99.8429 95.4371 97.6026 95.4371C95.7288 95.4371 93.855 93.9163 93.855 91.3422C93.8536 89.0102 95.4678 87.2932 97.5971 87.2932V87.3002Z\"\n          fill=\"black\"\n        />\n        <path\n          d=\"M87.2813 82.2103C88.3231 82.2103 89.1676 81.3637 89.1676 80.3194C89.1676 79.2751 88.3231 78.4285 87.2813 78.4285C86.2395 78.4285 85.395 79.2751 85.395 80.3194C85.395 81.3637 86.2395 82.2103 87.2813 82.2103Z\"\n          fill=\"black\"\n        />\n        <path\n          d=\"M76.9184 84.6731C73.7496 84.6731 70.2712 87.0496 70.2712 91.3407C70.2712 95.2547 73.236 97.9999 76.9143 97.9999C81.4475 97.9999 83.66 94.3475 83.66 91.3643C83.66 87.705 80.8104 84.6731 76.9212 84.6731H76.9184ZM76.9282 87.3432C79.1198 87.3432 80.7549 89.1131 80.7549 91.3476C80.7549 93.6226 79.0199 95.3827 76.9351 95.3827C75.0002 95.3827 73.1194 93.8035 73.1194 91.3922C73.1194 88.9405 74.9086 87.3488 76.9282 87.3488V87.3432Z\"\n          fill=\"black\"\n        />\n        <path\n          d=\"M81.4769 35.895L88.7723 23.2263C88.9677 22.8863 89.021 22.4826 88.9207 22.1034C88.8203 21.7241 88.5743 21.4 88.2365 21.2018C88.0696 21.1035 87.8848 21.0394 87.693 21.0133C87.5011 20.9871 87.306 20.9993 87.119 21.0493C86.9319 21.0992 86.7566 21.1859 86.6031 21.3043C86.4497 21.4227 86.3213 21.5704 86.2253 21.7389L78.8355 34.5704C73.196 31.988 66.85 30.5493 60.0237 30.5493C53.1975 30.5493 46.8501 31.988 41.212 34.5704L33.8208 21.7389C33.7265 21.565 33.5983 21.4118 33.4439 21.2884C33.2895 21.165 33.1119 21.0739 32.9217 21.0205C32.7316 20.9671 32.5326 20.9524 32.3367 20.9774C32.1408 21.0025 31.9519 21.0666 31.7811 21.1661C31.6104 21.2656 31.4613 21.3984 31.3427 21.5567C31.224 21.715 31.1383 21.8955 31.0904 22.0876C31.0426 22.2797 31.0337 22.4794 31.0643 22.675C31.0948 22.8706 31.1642 23.0581 31.2683 23.2263L38.5623 35.895C25.9827 42.7268 17.4645 55.4915 16.0557 70.4337H104C102.587 55.4915 94.0661 42.7268 81.4769 35.895ZM39.8365 58.053C39.1072 58.0533 38.3943 57.8368 37.7878 57.4308C37.1814 57.0248 36.7086 56.4477 36.4294 55.7723C36.1502 55.097 36.0771 54.3538 36.2193 53.6368C36.3615 52.9199 36.7126 52.2612 37.2283 51.7443C37.744 51.2274 38.401 50.8754 39.1162 50.7329C39.8315 50.5903 40.5728 50.6636 41.2465 50.9435C41.9202 51.2234 42.496 51.6973 42.9009 52.3052C43.3059 52.9131 43.5219 53.6278 43.5217 54.3589C43.5209 55.3389 43.132 56.2785 42.4405 56.9712C41.749 57.6639 40.8113 58.053 39.8337 58.053H39.8365ZM80.2068 58.053C79.4776 58.053 78.7648 57.8363 78.1585 57.4301C77.5523 57.024 77.0798 56.4467 76.8008 55.7714C76.5218 55.096 76.4488 54.3529 76.5912 53.636C76.7336 52.9191 77.0848 52.2606 77.6005 51.7438C78.1162 51.2271 78.7733 50.8752 79.4885 50.7328C80.2037 50.5903 80.945 50.6637 81.6186 50.9436C82.2922 51.2235 82.8679 51.6974 83.2728 52.3053C83.6777 52.9133 83.8937 53.6279 83.8934 54.3589C83.8932 54.8443 83.7976 55.3249 83.6121 55.7733C83.4266 56.2217 83.1548 56.629 82.8122 56.9721C82.4695 57.3152 82.0629 57.5872 81.6154 57.7727C81.1679 57.9581 80.6883 58.0534 80.2041 58.053H80.2068Z\"\n          fill=\"#32DE84\"\n        />\n      </SvgContainer>\n    </div>\n  );\n}\n\nfunction WebIcons() {\n  return (\n    <div className=\"flex items-center justify-center gap-3\">\n      <SvgContainer>\n        <g clipPath=\"url(#ee)\">\n          <path\n            d=\"m60 81.99c12.15 0 22-9.8497 22-22 0-12.15-9.8497-22-22-22s-22 9.8498-22 22c0 12.15 9.8497 22 22 22z\"\n            fill=\"#fff\"\n          />\n          <path\n            d=\"m60 38h38.099c-3.8606-6.6892-9.4144-12.244-16.103-16.106-6.6884-3.862-14.276-5.8948-21.999-5.8943-7.7232 5e-4 -15.31 2.0345-21.998 5.8973-6.6879 3.8629-12.241 9.4184-16.101 16.108l19.05 32.995 0.017-0.0044c-1.9378-3.3417-2.9604-7.1352-2.9648-10.998-0.0043-3.8629 1.0098-7.6586 2.9401-11.005 1.9303-3.346 4.7086-6.124 8.0548-8.0539 3.3463-1.93 7.1422-2.9437 11.005-2.9389z\"\n            fill=\"url(#cc)\"\n          />\n          <path\n            d=\"m60 77.417c9.619 0 17.417-7.7977 17.417-17.417 0-9.6189-7.7977-17.417-17.417-17.417-9.6189 0-17.417 7.7977-17.417 17.417 0 9.619 7.7977 17.417 17.417 17.417z\"\n            fill=\"#1A73E8\"\n          />\n          <path\n            d=\"m79.05 71.006-19.05 32.994c7.7233 1e-3 15.311-2.031 21.999-5.8925 6.6886-3.8614 12.243-9.4158 16.104-16.105 3.8607-6.6888 5.8927-14.276 5.8917-22-1e-3 -7.7232-2.036-15.31-5.8996-21.997h-38.099l-0.0045 0.017c3.8629-0.0074 7.6595 1.0036 11.007 2.9313 3.3476 1.9277 6.1278 4.7038 8.0604 8.0485s2.9492 7.1398 2.9474 11.003c-0.0017 3.8629-1.0219 7.657-2.9575 11z\"\n            fill=\"url(#bb)\"\n          />\n          <path\n            d=\"m40.949 71.006-19.049-32.995c-3.8626 6.688-5.8963 14.275-5.8966 21.998s2.0328 15.31 5.8949 21.999 9.4171 12.242 16.106 16.102c6.6893 3.8603 14.277 5.8913 22 5.8893l19.049-32.994-0.0123-0.0124c-1.925 3.349-4.699 6.1314-8.0422 8.0666s-7.1375 2.9549-11 2.9562-7.6579-1.0158-11.002-2.9488c-3.3445-1.9329-6.1203-4.7134-8.0476-8.0612z\"\n            fill=\"url(#aa)\"\n          />\n        </g>\n        <defs>\n          <linearGradient\n            id=\"cc\"\n            x1=\"21.898\"\n            x2=\"98.099\"\n            y1=\"43.5\"\n            y2=\"43.5\"\n            gradientUnits=\"userSpaceOnUse\">\n            <stop stopColor=\"#D93025\" offset=\"0\" />\n            <stop stopColor=\"#EA4335\" offset=\"1\" />\n          </linearGradient>\n          <linearGradient\n            id=\"bb\"\n            x1=\"53.99\"\n            x2=\"92.09\"\n            y1=\"103.41\"\n            y2=\"37.42\"\n            gradientUnits=\"userSpaceOnUse\">\n            <stop stopColor=\"#FCC934\" offset=\"0\" />\n            <stop stopColor=\"#FBBC04\" offset=\"1\" />\n          </linearGradient>\n          <linearGradient\n            id=\"aa\"\n            x1=\"64.763\"\n            x2=\"26.663\"\n            y1=\"101.25\"\n            y2=\"35.261\"\n            gradientUnits=\"userSpaceOnUse\">\n            <stop stopColor=\"#1E8E3E\" offset=\"0\" />\n            <stop stopColor=\"#34A853\" offset=\"1\" />\n          </linearGradient>\n          <clipPath id=\"ee\">\n            <rect\n              transform=\"translate(16 16)\"\n              width=\"88\"\n              height=\"88\"\n              fill=\"#fff\"\n            />\n          </clipPath>\n        </defs>\n      </SvgContainer>\n\n      <SvgContainer>\n        <path\n          d=\"m101.3 42.856c-1.9371-4.6598-5.8655-9.691-8.9417-11.282 2.1941 4.2494 3.7168 8.8131 4.5137 13.529l0.0081 0.0748c-5.0393-12.564-13.585-17.63-20.564-28.66-0.3605-0.5624-0.7106-1.1313-1.05-1.7066-0.175-0.3005-0.3388-0.6074-0.491-0.92-0.2895-0.5606-0.5126-1.153-0.6647-1.7653 2e-4 -0.0282-0.01-0.0556-0.0287-0.0768s-0.0445-0.0348-0.0725-0.0382c-0.0275-0.0075-0.0565-0.0075-0.084 0-0.0057 0-0.0149 0.0104-0.0218 0.0127s-0.0219 0.0126-0.0322 0.0172l0.0172-0.0299c-11.195 6.555-14.994 18.69-15.343 24.76-4.4708 0.3074-8.7453 1.9549-12.266 4.7277-0.3673-0.3111-0.7512-0.6021-1.15-0.8717-1.0156-3.5547-1.0588-7.3168-0.1253-10.894-4.1114 1.9917-7.7645 4.8151-10.728 8.2915h-0.0207c-1.7664-2.239-1.6422-9.622-1.541-11.164-0.5225 0.21-1.0214 0.4749-1.4881 0.7901-1.5593 1.1129-3.0171 2.3617-4.3562 3.7317-1.5259 1.5472-2.9196 3.2193-4.1664 4.9991v0.0069-0.0081c-2.8656 4.0625-4.898 8.6523-5.98 13.504l-0.0598 0.2944c-0.1644 0.9247-0.3105 1.8525-0.4382 2.783 0 0.0333-0.0069 0.0644-0.0103 0.0977-0.3902 2.0279-0.6319 4.0815-0.7234 6.1445v0.23c0.0098 11.16 4.2056 21.91 11.758 30.126 7.5526 8.216 17.912 13.3 29.032 14.247s22.19-2.312 31.023-9.132c8.8332-6.8204 14.786-16.706 16.683-27.704 0.075-0.575 0.136-1.1443 0.203-1.725 0.918-7.5875-0.076-15.284-2.891-22.389zm-51.371 34.889c0.2082 0.1001 0.4037 0.2082 0.6176 0.3036l0.031 0.0196c-0.2162-0.1035-0.4324-0.2112-0.6486-0.3232zm46.954-32.556v-0.0425l0.0081 0.0471-0.0081-0.0046z\"\n          fill=\"url(#l)\"\n        />\n        <path\n          d=\"m101.3 42.856c-1.9371-4.6598-5.8655-9.691-8.9417-11.282 2.1941 4.2494 3.7168 8.8131 4.5137 13.529v0.0426l0.0081 0.0471c3.4349 9.8307 2.9384 20.609-1.3869 30.082-5.1083 10.961-17.473 22.195-36.828 21.649-20.913-0.5923-39.33-16.11-42.773-36.436-0.6268-3.205 0-4.83 0.3151-7.4347-0.4299 2.0233-0.6697 4.0823-0.7165 6.1502v0.23c0.0098 11.16 4.2056 21.91 11.758 30.126 7.5526 8.216 17.912 13.3 29.032 14.247s22.19-2.312 31.023-9.1321c8.8332-6.8204 14.786-16.706 16.683-27.704 0.075-0.575 0.136-1.1443 0.203-1.725 0.918-7.5875-0.076-15.284-2.891-22.389z\"\n          fill=\"url(#i)\"\n        />\n        <path\n          d=\"m101.3 42.856c-1.9371-4.6598-5.8655-9.691-8.9417-11.282 2.1941 4.2494 3.7168 8.8131 4.5137 13.529v0.0426l0.0081 0.0471c3.4349 9.8307 2.9384 20.609-1.3869 30.082-5.1083 10.961-17.473 22.195-36.828 21.649-20.913-0.5923-39.33-16.11-42.773-36.436-0.6268-3.205 0-4.83 0.3151-7.4347-0.4299 2.0233-0.6697 4.0823-0.7165 6.1502v0.23c0.0098 11.16 4.2056 21.91 11.758 30.126 7.5526 8.216 17.912 13.3 29.032 14.247s22.19-2.312 31.023-9.1321c8.8332-6.8204 14.786-16.706 16.683-27.704 0.075-0.575 0.136-1.1443 0.203-1.725 0.918-7.5875-0.076-15.284-2.891-22.389z\"\n          fill=\"url(#h)\"\n        />\n        <path\n          d=\"m79.644 48.095c0.0966 0.0678 0.1863 0.1357 0.2772 0.2035-1.1196-1.9852-2.5133-3.8028-4.14-5.3992-13.853-13.855-3.6306-30.042-1.9067-30.864l0.0172-0.0253c-11.195 6.555-14.994 18.69-15.343 24.76 0.5198-0.0357 1.035-0.0794 1.5663-0.0794 3.9723 0.0073 7.8719 1.0664 11.302 3.0696 3.4303 2.0031 6.2689 4.879 8.2272 8.335z\"\n          fill=\"url(#g)\"\n        />\n        <path\n          d=\"m60.144 50.862c-0.0736 1.1086-3.9905 4.9323-5.3602 4.9323-12.674 0-14.732 7.6671-14.732 7.6671 0.5612 6.4561 5.06 11.774 10.498 14.587 0.2484 0.1288 0.5002 0.2449 0.7521 0.3588 0.4362 0.1932 0.8724 0.3718 1.3087 0.5359 1.8664 0.6605 3.8212 1.0377 5.7994 1.1189 22.215 1.0419 26.518-26.565 10.487-34.576 3.7818-0.492 7.6116 0.4378 10.747 2.6094-1.9583-3.4561-4.7969-6.3319-8.2272-8.3351-3.4302-2.0031-7.3298-3.0622-11.302-3.0695-0.529 0-1.0465 0.0437-1.5663 0.0794-4.4708 0.3073-8.7453 1.9548-12.266 4.7276 0.6797 0.575 1.4467 1.3432 3.0625 2.936 3.0245 2.9796 10.781 6.0662 10.798 6.4285z\"\n          fill=\"url(#f)\"\n        />\n        <path\n          d=\"m60.144 50.862c-0.0736 1.1086-3.9905 4.9323-5.3602 4.9323-12.674 0-14.732 7.6671-14.732 7.6671 0.5612 6.4561 5.06 11.774 10.498 14.587 0.2484 0.1288 0.5002 0.2449 0.7521 0.3588 0.4362 0.1932 0.8724 0.3718 1.3087 0.5359 1.8664 0.6605 3.8212 1.0377 5.7994 1.1189 22.215 1.0419 26.518-26.565 10.487-34.576 3.7818-0.492 7.6116 0.4378 10.747 2.6094-1.9583-3.4561-4.7969-6.3319-8.2272-8.3351-3.4302-2.0031-7.3298-3.0622-11.302-3.0695-0.529 0-1.0465 0.0437-1.5663 0.0794-4.4708 0.3073-8.7453 1.9548-12.266 4.7276 0.6797 0.575 1.4467 1.3432 3.0625 2.936 3.0245 2.9796 10.781 6.0662 10.798 6.4285z\"\n          fill=\"url(#e)\"\n        />\n        <path\n          d=\"m44.205 40.015c0.3611 0.23 0.6589 0.4301 0.92 0.6107-1.0156-3.5547-1.0589-7.3168-0.1254-10.894-4.1113 1.9917-7.7644 4.8151-10.728 8.2915 0.2173-0.0057 6.6826-0.1219 9.9337 1.9918z\"\n          fill=\"url(#d)\"\n        />\n        <path\n          d=\"m15.902 60.487c3.4397 20.325 21.86 35.843 42.773 36.436 19.354 0.5474 31.719-10.688 36.828-21.649 4.3254-9.4729 4.8223-20.251 1.3869-30.082v-0.0425c0-0.0334-0.0069-0.0529 0-0.0426l0.0081 0.0748c1.5812 10.324-3.6697 20.325-11.878 27.088l-0.0253 0.0575c-15.994 13.026-31.301 7.8591-34.399 5.75-0.2162-0.1035-0.4324-0.2112-0.6486-0.3231-9.3253-4.4574-13.178-12.954-12.352-20.24-2.2137 0.0326-4.3893-0.5774-6.2632-1.7561-1.874-1.1788-3.3659-2.8757-4.295-4.8852 2.4479-1.4997 5.2391-2.3475 8.1075-2.4626 2.8685-0.1152 5.7186 0.5062 8.2789 1.8048 5.2783 2.3962 11.285 2.6323 16.735 0.6578-0.0173-0.3622-7.774-3.45-10.798-6.4285-1.6158-1.5927-2.3828-2.3598-3.0625-2.9359-0.3673-0.3112-0.7512-0.6022-1.15-0.8717-0.2645-0.1806-0.5623-0.3761-0.92-0.6107-3.251-2.1137-9.7163-1.9975-9.9302-1.9918h-0.0207c-1.7664-2.239-1.6422-9.622-1.541-11.164-0.5226 0.21-1.0214 0.4749-1.4881 0.7901-1.5594 1.1129-3.0171 2.3617-4.3562 3.7317-1.5314 1.5428-2.9309 3.2111-4.1837 4.9876v0.0069-0.0081c-2.8656 4.0624-4.898 8.6523-5.98 13.504-0.0219 0.0908-1.6054 7.0138-0.8246 10.604z\"\n          fill=\"url(#c)\"\n        />\n        <path\n          d=\"m75.784 42.9c1.6271 1.5982 3.0208 3.4178 4.14 5.405 0.2449 0.1852 0.4738 0.3692 0.6681 0.5474 10.105 9.315 4.8105 22.482 4.416 23.42 8.2087-6.7632 13.455-16.765 11.878-27.088-5.0416-12.57-13.587-17.635-20.567-28.666-0.3605-0.5624-0.7106-1.1313-1.05-1.7066-0.175-0.3005-0.3388-0.6074-0.491-0.92-0.2895-0.5606-0.5126-1.153-0.6647-1.7653 2e-4 -0.0282-0.01-0.0556-0.0287-0.0768s-0.0445-0.0348-0.0725-0.0382c-0.0275-0.0075-0.0565-0.0075-0.084 0-0.0057 0-0.0149 0.0104-0.0218 0.0127s-0.0219 0.0126-0.0322 0.0172c-1.7239 0.8177-11.946 17.004 1.909 30.859z\"\n          fill=\"url(#b)\"\n        />\n        <path\n          d=\"m80.585 48.846c-0.2142-0.1927-0.4371-0.3754-0.6682-0.5474-0.0908-0.0679-0.1805-0.1357-0.2771-0.2036-3.1351-2.1715-6.965-3.1014-10.747-2.6093 16.031 8.0155 11.73 35.618-10.487 34.576-1.9781-0.0812-3.933-0.4584-5.7994-1.119-0.4362-0.1633-0.8725-0.3419-1.3087-0.5359-0.2519-0.115-0.5037-0.23-0.7521-0.3588l0.031 0.0196c3.0981 2.1148 18.4 7.2818 34.399-5.75l0.0253-0.0575c0.399-0.9315 5.6936-14.102-4.416-23.414z\"\n          fill=\"url(#a)\"\n        />\n        <path\n          d=\"m40.052 63.462s2.0573-7.667 14.732-7.667c1.3696 0 5.29-3.8238 5.3601-4.9324-5.4501 1.9745-11.456 1.7384-16.735-0.6578-2.5602-1.2986-5.4104-1.9199-8.2788-1.8048-2.8684 0.1152-5.6596 0.963-8.1075 2.4626 0.9291 2.0095 2.421 3.7064 4.2949 4.8852 1.874 1.1788 4.0496 1.7888 6.2632 1.7561-0.8257 7.2875 3.0268 15.784 12.352 20.24 0.2081 0.1 0.4036 0.2081 0.6175 0.3036-5.4429-2.8118-9.9371-8.1294-10.498-14.586z\"\n          fill=\"url(#k)\"\n        />\n        <path\n          d=\"m101.3 42.856c-1.9371-4.6598-5.8655-9.691-8.9417-11.282 2.1941 4.2494 3.7168 8.8131 4.5137 13.529l0.0081 0.0748c-5.0393-12.564-13.585-17.63-20.564-28.66-0.3605-0.5624-0.7106-1.1313-1.05-1.7066-0.175-0.3005-0.3388-0.6074-0.491-0.92-0.2895-0.5606-0.5126-1.153-0.6647-1.7653 2e-4 -0.0282-0.01-0.0556-0.0287-0.0768s-0.0445-0.0348-0.0725-0.0382c-0.0275-0.0075-0.0565-0.0075-0.084 0-0.0057 0-0.0149 0.0104-0.0218 0.0127s-0.0219 0.0126-0.0322 0.0172l0.0172-0.0299c-11.195 6.555-14.994 18.69-15.343 24.76 0.5198-0.0356 1.035-0.0793 1.5663-0.0793 3.9723 0.0073 7.8719 1.0663 11.302 3.0695 3.4303 2.0032 6.2689 4.879 8.2272 8.335-3.1351-2.1715-6.9649-3.1014-10.747-2.6093 16.031 8.0155 11.73 35.618-10.487 34.576-1.9782-0.0813-3.933-0.4584-5.7994-1.119-0.4363-0.1633-0.8725-0.3419-1.3087-0.5359-0.2519-0.115-0.5037-0.23-0.7521-0.3588l0.031 0.0196c-0.2162-0.1035-0.4324-0.2112-0.6486-0.3232 0.2082 0.1001 0.4037 0.2082 0.6176 0.3036-5.443-2.8129-9.9372-8.1305-10.498-14.587 0 0 2.0574-7.667 14.732-7.667 1.3697 0 5.29-3.8238 5.3602-4.9324-0.0173-0.3622-7.774-3.45-10.798-6.4285-1.6158-1.5927-2.3828-2.3598-3.0625-2.9359-0.3673-0.3111-0.7512-0.6021-1.15-0.8717-1.0156-3.5547-1.0588-7.3168-0.1253-10.894-4.1114 1.9917-7.7645 4.8151-10.728 8.2915h-0.0207c-1.7664-2.239-1.6422-9.622-1.541-11.164-0.5225 0.21-1.0214 0.4749-1.4881 0.7901-1.5593 1.1129-3.0171 2.3617-4.3562 3.7317-1.5259 1.5472-2.9196 3.2193-4.1664 4.9991v0.0069-0.0081c-2.8656 4.0625-4.898 8.6523-5.98 13.504l-0.0598 0.2944c-0.084 0.3921-0.46 2.3839-0.5141 2.8117v0c-0.3439 2.0561-0.5636 4.131-0.6578 6.2135v0.23c0.0098 11.16 4.2056 21.91 11.758 30.126 7.5526 8.216 17.912 13.3 29.032 14.247s22.19-2.312 31.023-9.132c8.8332-6.8204 14.786-16.706 16.683-27.704 0.075-0.575 0.136-1.1443 0.203-1.725 0.918-7.5875-0.076-15.284-2.891-22.389z\"\n          fill=\"url(#j)\"\n        />\n        <defs>\n          <linearGradient\n            id=\"l\"\n            x1=\"95.404\"\n            x2=\"21.414\"\n            y1=\"26.252\"\n            y2=\"97.638\"\n            gradientUnits=\"userSpaceOnUse\">\n            <stop stopColor=\"#FFF44F\" offset=\".048\" />\n            <stop stopColor=\"#FFE847\" offset=\".111\" />\n            <stop stopColor=\"#FFC830\" offset=\".225\" />\n            <stop stopColor=\"#FF980E\" offset=\".368\" />\n            <stop stopColor=\"#FF8B16\" offset=\".401\" />\n            <stop stopColor=\"#FF672A\" offset=\".462\" />\n            <stop stopColor=\"#FF3647\" offset=\".534\" />\n            <stop stopColor=\"#E31587\" offset=\".705\" />\n          </linearGradient>\n          <radialGradient\n            id=\"i\"\n            cx=\"0\"\n            cy=\"0\"\n            r=\"1\"\n            gradientTransform=\"translate(91.985 22.211) scale(92.916 92.917)\"\n            gradientUnits=\"userSpaceOnUse\">\n            <stop stopColor=\"#FFBD4F\" offset=\".129\" />\n            <stop stopColor=\"#FFAC31\" offset=\".186\" />\n            <stop stopColor=\"#FF9D17\" offset=\".247\" />\n            <stop stopColor=\"#FF980E\" offset=\".283\" />\n            <stop stopColor=\"#FF563B\" offset=\".403\" />\n            <stop stopColor=\"#FF3750\" offset=\".467\" />\n            <stop stopColor=\"#F5156C\" offset=\".71\" />\n            <stop stopColor=\"#EB0878\" offset=\".782\" />\n            <stop stopColor=\"#E50080\" offset=\".86\" />\n          </radialGradient>\n          <radialGradient\n            id=\"h\"\n            cx=\"0\"\n            cy=\"0\"\n            r=\"1\"\n            gradientTransform=\"translate(58.032 60.198) scale(92.916 92.917)\"\n            gradientUnits=\"userSpaceOnUse\">\n            <stop stopColor=\"#960E18\" offset=\".3\" />\n            <stop stopColor=\"#B11927\" stopOpacity=\".74\" offset=\".351\" />\n            <stop stopColor=\"#DB293D\" stopOpacity=\".343\" offset=\".435\" />\n            <stop stopColor=\"#F5334B\" stopOpacity=\".094\" offset=\".497\" />\n            <stop stopColor=\"#FF3750\" stopOpacity=\"0\" offset=\".53\" />\n          </radialGradient>\n          <radialGradient\n            id=\"g\"\n            cx=\"0\"\n            cy=\"0\"\n            r=\"1\"\n            gradientTransform=\"translate(69.234 1.1246) scale(67.314)\"\n            gradientUnits=\"userSpaceOnUse\">\n            <stop stopColor=\"#FFF44F\" offset=\".132\" />\n            <stop stopColor=\"#FFDC3E\" offset=\".252\" />\n            <stop stopColor=\"#FF9D12\" offset=\".506\" />\n            <stop stopColor=\"#FF980E\" offset=\".526\" />\n          </radialGradient>\n          <radialGradient\n            id=\"f\"\n            cx=\"0\"\n            cy=\"0\"\n            r=\"1\"\n            gradientTransform=\"translate(47.755 84.468) scale(44.242 44.242)\"\n            gradientUnits=\"userSpaceOnUse\">\n            <stop stopColor=\"#3A8EE6\" offset=\".353\" />\n            <stop stopColor=\"#5C79F0\" offset=\".472\" />\n            <stop stopColor=\"#9059FF\" offset=\".669\" />\n            <stop stopColor=\"#C139E6\" offset=\"1\" />\n          </radialGradient>\n          <radialGradient\n            id=\"e\"\n            cx=\"0\"\n            cy=\"0\"\n            r=\"1\"\n            gradientTransform=\"translate(63.11 52.583) rotate(-13.592) scale(23.457 27.462)\"\n            gradientUnits=\"userSpaceOnUse\">\n            <stop stopColor=\"#9059FF\" stopOpacity=\"0\" offset=\".206\" />\n            <stop stopColor=\"#8C4FF3\" stopOpacity=\".064\" offset=\".278\" />\n            <stop stopColor=\"#7716A8\" stopOpacity=\".45\" offset=\".747\" />\n            <stop stopColor=\"#6E008B\" stopOpacity=\".6\" offset=\".975\" />\n          </radialGradient>\n          <radialGradient\n            id=\"d\"\n            cx=\"0\"\n            cy=\"0\"\n            r=\"1\"\n            gradientTransform=\"translate(56.859 18.409) scale(31.827)\"\n            gradientUnits=\"userSpaceOnUse\">\n            <stop stopColor=\"#FFE226\" offset=\"0\" />\n            <stop stopColor=\"#FFDB27\" offset=\".121\" />\n            <stop stopColor=\"#FFC82A\" offset=\".295\" />\n            <stop stopColor=\"#FFA930\" offset=\".502\" />\n            <stop stopColor=\"#FF7E37\" offset=\".732\" />\n            <stop stopColor=\"#FF7139\" offset=\".792\" />\n          </radialGradient>\n          <radialGradient\n            id=\"c\"\n            cx=\"0\"\n            cy=\"0\"\n            r=\"1\"\n            gradientTransform=\"translate(81.876 -1.7782) scale(135.79)\"\n            gradientUnits=\"userSpaceOnUse\">\n            <stop stopColor=\"#FFF44F\" offset=\".113\" />\n            <stop stopColor=\"#FF980E\" offset=\".456\" />\n            <stop stopColor=\"#FF5634\" offset=\".622\" />\n            <stop stopColor=\"#FF3647\" offset=\".716\" />\n            <stop stopColor=\"#E31587\" offset=\".904\" />\n          </radialGradient>\n          <radialGradient\n            id=\"b\"\n            cx=\"0\"\n            cy=\"0\"\n            r=\"1\"\n            gradientTransform=\"translate(70.431 5.7724) rotate(83.976) scale(99.526 65.318)\"\n            gradientUnits=\"userSpaceOnUse\">\n            <stop stopColor=\"#FFF44F\" offset=\"0\" />\n            <stop stopColor=\"#FFE847\" offset=\".06\" />\n            <stop stopColor=\"#FFC830\" offset=\".168\" />\n            <stop stopColor=\"#FF980E\" offset=\".304\" />\n            <stop stopColor=\"#FF8B16\" offset=\".356\" />\n            <stop stopColor=\"#FF672A\" offset=\".455\" />\n            <stop stopColor=\"#FF3647\" offset=\".57\" />\n            <stop stopColor=\"#E31587\" offset=\".737\" />\n          </radialGradient>\n          <radialGradient\n            id=\"a\"\n            cx=\"0\"\n            cy=\"0\"\n            r=\"1\"\n            gradientTransform=\"translate(56.11 30.198) scale(84.778)\"\n            gradientUnits=\"userSpaceOnUse\">\n            <stop stopColor=\"#FFF44F\" offset=\".137\" />\n            <stop stopColor=\"#FF980E\" offset=\".48\" />\n            <stop stopColor=\"#FF5634\" offset=\".592\" />\n            <stop stopColor=\"#FF3647\" offset=\".655\" />\n            <stop stopColor=\"#E31587\" offset=\".904\" />\n          </radialGradient>\n          <radialGradient\n            id=\"k\"\n            cx=\"0\"\n            cy=\"0\"\n            r=\"1\"\n            gradientTransform=\"translate(78.488 35.16) scale(92.789)\"\n            gradientUnits=\"userSpaceOnUse\">\n            <stop stopColor=\"#FFF44F\" offset=\".094\" />\n            <stop stopColor=\"#FFE141\" offset=\".231\" />\n            <stop stopColor=\"#FFAF1E\" offset=\".509\" />\n            <stop stopColor=\"#FF980E\" offset=\".626\" />\n          </radialGradient>\n          <linearGradient\n            id=\"j\"\n            x1=\"94.515\"\n            x2=\"31.557\"\n            y1=\"25.87\"\n            y2=\"88.827\"\n            gradientUnits=\"userSpaceOnUse\">\n            <stop stopColor=\"#FFF44F\" stopOpacity=\".8\" offset=\".167\" />\n            <stop stopColor=\"#FFF44F\" stopOpacity=\".634\" offset=\".266\" />\n            <stop stopColor=\"#FFF44F\" stopOpacity=\".217\" offset=\".489\" />\n            <stop stopColor=\"#FFF44F\" stopOpacity=\"0\" offset=\".6\" />\n          </linearGradient>\n        </defs>\n      </SvgContainer>\n      <SvgContainer>\n        <path\n          d=\"m60 104c24.3 0 44-19.7 44-44s-19.7-44-44-44-44 19.7-44 44 19.7 44 44 44z\"\n          fill=\"url(#aaa)\"\n        />\n        <path\n          d=\"m60.002 100.1v-6.756\"\n          stroke=\"#fff\"\n          strokeLinecap=\"square\"\n          strokeWidth=\".84706\"\n        />\n        <path\n          d=\"m60.002 26.624v-6.7563\"\n          stroke=\"#fff\"\n          strokeLinecap=\"square\"\n          strokeWidth=\".84706\"\n        />\n        <path\n          d=\"m53.018 99.512 1.1732-6.6536m11.585-65.704 1.1732-6.6536\"\n          stroke=\"#fff\"\n          strokeLinecap=\"square\"\n          strokeWidth=\".84706\"\n        />\n        <path\n          d=\"m46.278 97.689 2.3108-6.3488m22.819-62.694 2.3108-6.3488\"\n          stroke=\"#fff\"\n          strokeLinecap=\"square\"\n          strokeWidth=\".84706\"\n        />\n        <path\n          d=\"m39.91 94.765 3.3781-5.851m33.359-57.779 3.3782-5.8511\"\n          stroke=\"#fff\"\n          strokeLinecap=\"square\"\n          strokeWidth=\".84706\"\n        />\n        <path\n          d=\"m34.252 90.737 4.3428-5.1756m42.885-51.109 4.3429-5.1756\"\n          stroke=\"#fff\"\n          strokeLinecap=\"square\"\n          strokeWidth=\".84706\"\n        />\n        <path\n          d=\"m29.227 85.774 5.1756-4.3428m51.109-42.885 5.1756-4.3428\"\n          stroke=\"#fff\"\n          strokeLinecap=\"square\"\n          strokeWidth=\".84706\"\n        />\n        <path\n          d=\"m25.244 80.033 5.8511-3.3781m57.779-33.359 5.8511-3.3781\"\n          stroke=\"#fff\"\n          strokeLinecap=\"square\"\n          strokeWidth=\".84706\"\n        />\n        <path\n          d=\"m22.238 73.725 6.3488-2.3108m62.694-22.819 6.3488-2.3108\"\n          stroke=\"#fff\"\n          strokeLinecap=\"square\"\n          strokeWidth=\".84706\"\n        />\n        <path\n          d=\"m20.469 67.009 6.6535-1.1733m65.704-11.585 6.6536-1.1732\"\n          stroke=\"#fff\"\n          strokeLinecap=\"square\"\n          strokeWidth=\".84706\"\n        />\n        <path\n          d=\"m19.9 60.002h6.7563m66.718 0h6.7558\"\n          stroke=\"#fff\"\n          strokeLinecap=\"square\"\n          strokeWidth=\".84706\"\n        />\n        <path\n          d=\"m20.488 53.062 6.6536 1.1732m65.704 11.585 6.6536 1.1732\"\n          stroke=\"#fff\"\n          strokeLinecap=\"square\"\n          strokeWidth=\".84706\"\n        />\n        <path\n          d=\"m22.309 46.295 6.3488 2.3108m62.694 22.819 6.3488 2.3108\"\n          stroke=\"#fff\"\n          strokeLinecap=\"square\"\n          strokeWidth=\".84706\"\n        />\n        <path\n          d=\"m25.229 39.99 5.8511 3.3781m57.779 33.359 5.851 3.3781\"\n          stroke=\"#fff\"\n          strokeLinecap=\"square\"\n          strokeWidth=\".84706\"\n        />\n        <path\n          d=\"m29.24 34.19 5.1756 4.3428m51.109 42.885 5.1756 4.3429\"\n          stroke=\"#fff\"\n          strokeLinecap=\"square\"\n          strokeWidth=\".84706\"\n        />\n        <path\n          d=\"m34.166 29.281 4.3428 5.1756m42.885 51.109 4.3428 5.1755\"\n          stroke=\"#fff\"\n          strokeLinecap=\"square\"\n          strokeWidth=\".84706\"\n        />\n        <path\n          d=\"m39.918 25.256 3.3781 5.8511m33.359 57.779 3.3781 5.8511\"\n          stroke=\"#fff\"\n          strokeLinecap=\"square\"\n          strokeWidth=\".84706\"\n        />\n        <path\n          d=\"m46.28 22.3 2.3108 6.3487m22.819 62.694 2.3107 6.3488\"\n          stroke=\"#fff\"\n          strokeLinecap=\"square\"\n          strokeWidth=\".84706\"\n        />\n        <path\n          d=\"m52.992 20.554 1.1733 6.6536m11.585 65.704 1.1732 6.6536\"\n          stroke=\"#fff\"\n          strokeLinecap=\"square\"\n          strokeWidth=\".84706\"\n        />\n        <path\n          d=\"m56.484 99.953 0.2945-3.3653m6.4037-73.194 0.2944-3.3653\"\n          stroke=\"#fff\"\n          strokeLinecap=\"square\"\n          strokeWidth=\".84706\"\n        />\n        <path\n          d=\"m49.629 98.788 0.8743-3.263m19.016-70.97 0.8743-3.263\"\n          stroke=\"#fff\"\n          strokeLinecap=\"square\"\n          strokeWidth=\".84706\"\n        />\n        <path\n          d=\"m43.024 96.33 1.4276-3.0616m31.052-66.59 1.4277-3.0616\"\n          stroke=\"#fff\"\n          strokeLinecap=\"square\"\n          strokeWidth=\".84706\"\n        />\n        <path\n          d=\"m36.978 92.875 1.9376-2.7672m42.143-60.186 1.9376-2.7672\"\n          stroke=\"#fff\"\n          strokeLinecap=\"square\"\n          strokeWidth=\".84706\"\n        />\n        <path\n          d=\"m31.658 88.369 2.3887-2.3886m51.954-51.954 2.3887-2.3886\"\n          stroke=\"#fff\"\n          strokeLinecap=\"square\"\n          strokeWidth=\".84706\"\n        />\n        <path\n          d=\"m27.141 83.051 2.7672-1.9376m60.186-42.143 2.7672-1.9376\"\n          stroke=\"#fff\"\n          strokeLinecap=\"square\"\n          strokeWidth=\".84706\"\n        />\n        <path\n          d=\"m23.653 76.966 3.0616-1.4277m66.59-31.052 3.0617-1.4276\"\n          stroke=\"#fff\"\n          strokeLinecap=\"square\"\n          strokeWidth=\".84706\"\n        />\n        <path\n          d=\"m21.24 70.41 3.263-0.8744m70.97-19.016 3.263-0.8743\"\n          stroke=\"#fff\"\n          strokeLinecap=\"square\"\n          strokeWidth=\".84706\"\n        />\n        <path\n          d=\"m20 63.507 3.3653-0.2944m73.194-6.4037 3.3652-0.2944\"\n          stroke=\"#fff\"\n          strokeLinecap=\"square\"\n          strokeWidth=\".84706\"\n        />\n        <path\n          d=\"m20.03 56.514 3.3653 0.2944m73.194 6.4037 3.3653 0.2944\"\n          stroke=\"#fff\"\n          strokeLinecap=\"square\"\n          strokeWidth=\".84706\"\n        />\n        <path\n          d=\"m21.19 49.657 3.2631 0.8743m70.97 19.016 3.263 0.8744\"\n          stroke=\"#fff\"\n          strokeLinecap=\"square\"\n          strokeWidth=\".84706\"\n        />\n        <path\n          d=\"m23.624 43.042 3.0616 1.4276m66.59 31.052 3.0616 1.4277\"\n          stroke=\"#fff\"\n          strokeLinecap=\"square\"\n          strokeWidth=\".84706\"\n        />\n        <path\n          d=\"m27.127 37.029 2.7671 1.9376m60.186 42.143 2.7672 1.9376\"\n          stroke=\"#fff\"\n          strokeLinecap=\"square\"\n          strokeWidth=\".84706\"\n        />\n        <path\n          d=\"m31.654 31.661 2.3887 2.3887m51.954 51.954 2.3887 2.3887\"\n          stroke=\"#fff\"\n          strokeLinecap=\"square\"\n          strokeWidth=\".84706\"\n        />\n        <path\n          d=\"m36.935 27.134 1.9376 2.7672m42.143 60.186 1.9376 2.7672\"\n          stroke=\"#fff\"\n          strokeLinecap=\"square\"\n          strokeWidth=\".84706\"\n        />\n        <path\n          d=\"m43.044 23.676 1.4276 3.0616m31.052 66.59 1.4277 3.0616\"\n          stroke=\"#fff\"\n          strokeLinecap=\"square\"\n          strokeWidth=\".84706\"\n        />\n        <path\n          d=\"m49.59 21.297 0.8743 3.263m19.016 70.97 0.8743 3.263\"\n          stroke=\"#fff\"\n          strokeLinecap=\"square\"\n          strokeWidth=\".84706\"\n        />\n        <path\n          d=\"m56.449 20.063 0.2944 3.3653m6.4037 73.194 0.2944 3.3652\"\n          stroke=\"#fff\"\n          strokeLinecap=\"square\"\n          strokeWidth=\".84706\"\n        />\n        <path\n          d=\"m91.222 33.87-34.71 22.211-27.785 30.15 34.879-21.704 27.616-30.656z\"\n          clipRule=\"evenodd\"\n          fill=\"#fff\"\n          fillRule=\"evenodd\"\n        />\n        <path\n          d=\"m91.222 33.87-34.71 22.211 7.094 8.4453 27.616-30.656z\"\n          clipRule=\"evenodd\"\n          fill=\"#FF3B30\"\n          fillRule=\"evenodd\"\n        />\n        <defs>\n          <linearGradient\n            id=\"aaa\"\n            x1=\"59.999\"\n            x2=\"59.999\"\n            y1=\"104.01\"\n            y2=\"15.992\"\n            gradientUnits=\"userSpaceOnUse\">\n            <stop stopColor=\"#1E6FF1\" offset=\"0\" />\n            <stop stopColor=\"#28CEFB\" offset=\"1\" />\n          </linearGradient>\n        </defs>\n      </SvgContainer>\n    </div>\n  );\n}\n\n// TODO: upgrade React and use the built-in version.\nfunction use(promise) {\n  if (promise.status === 'fulfilled') {\n    return promise.value;\n  } else if (promise.status === 'rejected') {\n    throw promise.reason;\n  } else if (promise.status === 'pending') {\n    throw promise;\n  } else {\n    promise.status = 'pending';\n    promise.then(\n      (result) => {\n        promise.status = 'fulfilled';\n        promise.value = result;\n      },\n      (reason) => {\n        promise.status = 'rejected';\n        promise.reason = reason;\n      }\n    );\n    throw promise;\n  }\n}\n\nlet confCache = new Map();\nlet talksCache = new Map();\nconst loadConfDelay = 250;\nconst loadTalksDelay = 1000;\n\nfunction fetchConf(slug) {\n  if (confCache.has(slug)) {\n    return confCache.get(slug);\n  }\n  const promise = new Promise((resolve) => {\n    setTimeout(() => {\n      if (slug === 'react-conf-2021') {\n        resolve({\n          id: 0,\n          cover: reactConf2021Cover,\n          name: 'React Conf 2021',\n        });\n      } else if (slug === 'react-conf-2019') {\n        resolve({\n          id: 1,\n          cover: reactConf2019Cover,\n          name: 'React Conf 2019',\n        });\n      }\n    }, loadConfDelay);\n  });\n  confCache.set(slug, promise);\n  return promise;\n}\n\nfunction fetchTalks(confId) {\n  if (talksCache.has(confId)) {\n    return talksCache.get(confId);\n  }\n  const promise = new Promise((resolve) => {\n    setTimeout(() => {\n      if (confId === 0) {\n        resolve([\n          {\n            id: 'conf-2021-0',\n            title: 'React 18 Keynote',\n            description: 'The React Team',\n            url: 'https://www.youtube.com/watch?v=FZ0cG47msEk&list=PLNG_1j3cPCaZZ7etkzWA7JfdmKWT0pMsa&index=1',\n            image: {\n              speakers: [\n                '/images/home/conf2021/andrew.jpg',\n                '/images/home/conf2021/lauren.jpg',\n                '/images/home/conf2021/juan.jpg',\n                '/images/home/conf2021/rick.jpg',\n              ],\n            },\n          },\n          {\n            id: 'conf-2021-1',\n            title: 'React 18 for App Developers',\n            description: 'Shruti Kapoor',\n            url: 'https://www.youtube.com/watch?v=ytudH8je5ko&list=PLNG_1j3cPCaZZ7etkzWA7JfdmKWT0pMsa&index=2',\n            image: {\n              speakers: ['/images/home/conf2021/shruti.jpg'],\n            },\n          },\n          {\n            id: 'conf-2021-2',\n            title: 'Streaming Server Rendering with Suspense',\n            description: 'Shaundai Person',\n            url: 'https://www.youtube.com/watch?v=pj5N-Khihgc&list=PLNG_1j3cPCaZZ7etkzWA7JfdmKWT0pMsa&index=3',\n            image: {\n              speakers: ['/images/home/conf2021/shaundai.jpg'],\n            },\n          },\n          {\n            id: 'conf-2021-3',\n            title: 'The First React Working Group',\n            description: 'Aakansha Doshi',\n            url: 'https://www.youtube.com/watch?v=qn7gRClrC9U&list=PLNG_1j3cPCaZZ7etkzWA7JfdmKWT0pMsa&index=4',\n            image: {\n              speakers: ['/images/home/conf2021/aakansha.jpg'],\n            },\n          },\n          {\n            id: 'conf-2021-4',\n            title: 'React Developer Tooling',\n            description: 'Brian Vaughn',\n            url: 'https://www.youtube.com/watch?v=oxDfrke8rZg&list=PLNG_1j3cPCaZZ7etkzWA7JfdmKWT0pMsa&index=5',\n            image: {\n              speakers: ['/images/home/conf2021/brian.jpg'],\n            },\n          },\n          {\n            id: 'conf-2021-5',\n            title: 'React without memo',\n            description: 'Xuan Huang (黄玄)',\n            url: 'https://www.youtube.com/watch?v=lGEMwh32soc&list=PLNG_1j3cPCaZZ7etkzWA7JfdmKWT0pMsa&index=6',\n            image: {\n              speakers: ['/images/home/conf2021/xuan.jpg'],\n            },\n          },\n          {\n            id: 'conf-2021-6',\n            title: 'React Docs Keynote',\n            description: 'Rachel Nabors',\n            url: 'https://www.youtube.com/watch?v=mneDaMYOKP8&list=PLNG_1j3cPCaZZ7etkzWA7JfdmKWT0pMsa&index=7',\n            image: {\n              speakers: ['/images/home/conf2021/rachel.jpg'],\n            },\n          },\n          {\n            id: 'conf-2021-7',\n            title: 'Things I Learnt from the New React Docs',\n            description: \"Debbie O'Brien\",\n            url: 'https://www.youtube.com/watch?v=-7odLW_hG7s&list=PLNG_1j3cPCaZZ7etkzWA7JfdmKWT0pMsa&index=8',\n            image: {\n              speakers: ['/images/home/conf2021/debbie.jpg'],\n            },\n          },\n          {\n            id: 'conf-2021-8',\n            title: 'Learning in the Browser',\n            description: 'Sarah Rainsberger',\n            url: 'https://www.youtube.com/watch?v=5X-WEQflCL0&list=PLNG_1j3cPCaZZ7etkzWA7JfdmKWT0pMsa&index=9',\n            image: {\n              speakers: ['/images/home/conf2021/sarah.jpg'],\n            },\n          },\n          {\n            id: 'conf-2021-9',\n            title: 'The ROI of Designing with React',\n            description: 'Linton Ye',\n            url: 'https://www.youtube.com/watch?v=7cPWmID5XAk&list=PLNG_1j3cPCaZZ7etkzWA7JfdmKWT0pMsa&index=10',\n            image: {\n              speakers: ['/images/home/conf2021/linton.jpg'],\n            },\n          },\n          {\n            id: 'conf-2021-10',\n            title: 'Interactive Playgrounds with React',\n            description: 'Delba de Oliveira',\n            url: 'https://www.youtube.com/watch?v=zL8cz2W0z34&list=PLNG_1j3cPCaZZ7etkzWA7JfdmKWT0pMsa&index=11',\n            image: {\n              speakers: ['/images/home/conf2021/delba.jpg'],\n            },\n          },\n          {\n            id: 'conf-2021-11',\n            title: 'Re-introducing Relay',\n            description: 'Robert Balicki',\n            url: 'https://www.youtube.com/watch?v=lhVGdErZuN4&list=PLNG_1j3cPCaZZ7etkzWA7JfdmKWT0pMsa&index=12',\n            image: {\n              speakers: ['/images/home/conf2021/robert.jpg'],\n            },\n          },\n          {\n            id: 'conf-2021-12',\n            title: 'React Native Desktop',\n            description: 'Eric Rozell and Steven Moyes',\n            url: 'https://www.youtube.com/watch?v=9L4FFrvwJwY&list=PLNG_1j3cPCaZZ7etkzWA7JfdmKWT0pMsa&index=13',\n            image: {\n              speakers: [\n                '/images/home/conf2021/eric.jpg',\n                '/images/home/conf2021/steven.jpg',\n              ],\n            },\n          },\n          {\n            id: 'conf-2021-13',\n            title: 'On-device Machine Learning for React Native',\n            description: 'Roman Rädle',\n            url: 'https://www.youtube.com/watch?v=NLj73vrc2I8&list=PLNG_1j3cPCaZZ7etkzWA7JfdmKWT0pMsa&index=14',\n            image: {\n              speakers: ['/images/home/conf2021/roman.jpg'],\n            },\n          },\n          {\n            id: 'conf-2021-14',\n            title: 'React 18 for External Store Libraries',\n            description: 'Daishi Kato',\n            url: 'https://www.youtube.com/watch?v=oPfSC5bQPR8&list=PLNG_1j3cPCaZZ7etkzWA7JfdmKWT0pMsa&index=15',\n            image: {\n              speakers: ['/images/home/conf2021/daishi.jpg'],\n            },\n          },\n          {\n            id: 'conf-2021-15',\n            title: 'Building Accessible Components with React 18',\n            description: 'Diego Haz',\n            url: 'https://www.youtube.com/watch?v=dcm8fjBfro8&list=PLNG_1j3cPCaZZ7etkzWA7JfdmKWT0pMsa&index=16',\n            image: {\n              speakers: ['/images/home/conf2021/diego.jpg'],\n            },\n          },\n          {\n            id: 'conf-2021-16',\n            title: 'Accessible Japanese Form Components with React',\n            description: 'Tafu Nakazaki',\n            url: 'https://www.youtube.com/watch?v=S4a0QlsH0pU&list=PLNG_1j3cPCaZZ7etkzWA7JfdmKWT0pMsa&index=17',\n            image: {\n              speakers: ['/images/home/conf2021/tafu.jpg'],\n            },\n          },\n          {\n            id: 'conf-2021-17',\n            title: 'UI Tools for Artists',\n            description: 'Lyle Troxell',\n            url: 'https://www.youtube.com/watch?v=b3l4WxipFsE&list=PLNG_1j3cPCaZZ7etkzWA7JfdmKWT0pMsa&index=18',\n            image: {\n              speakers: ['/images/home/conf2021/lyle.jpg'],\n            },\n          },\n          {\n            id: 'conf-2021-18',\n            title: 'Hydrogen + React 18',\n            description: 'Helen Lin',\n            url: 'https://www.youtube.com/watch?v=HS6vIYkSNks&list=PLNG_1j3cPCaZZ7etkzWA7JfdmKWT0pMsa&index=19',\n            image: {\n              speakers: ['/images/home/conf2021/helen.jpg'],\n            },\n          },\n        ]);\n      } else if (confId === 1) {\n        resolve([\n          {\n            id: 'conf-2019-0',\n            title: 'Keynote (Part 1)',\n            description: 'Tom Occhino',\n            url: 'https://www.youtube.com/watch?v=QnZHO7QvjaM&list=PLPxbbTqCLbGHPxZpw4xj_Wwg8-fdNxJRh',\n            image: {\n              speakers: ['/images/home/conf2019/tom.jpg'],\n            },\n          },\n          {\n            id: 'conf-2019-1',\n            title: 'Keynote (Part 2)',\n            description: 'Yuzhi Zheng',\n            url: 'https://www.youtube.com/watch?v=uXEEL9mrkAQ&list=PLPxbbTqCLbGHPxZpw4xj_Wwg8-fdNxJRh&index=2',\n            image: {\n              speakers: ['https://conf2019.reactjs.org/img/speakers/yuzhi.jpg'],\n            },\n          },\n          {\n            id: 'conf-2019-2',\n            title: 'Building The New Facebook With React and Relay (Part 1)',\n            description: 'Frank Yan',\n            url: 'https://www.youtube.com/watch?v=9JZHodNR184&list=PLPxbbTqCLbGHPxZpw4xj_Wwg8-fdNxJRh&index=3',\n            image: {\n              speakers: ['/images/home/conf2019/frank.jpg'],\n            },\n          },\n          {\n            id: 'conf-2019-3',\n            title: 'Building The New Facebook With React and Relay (Part 2)',\n            description: 'Ashley Watkins',\n            url: 'https://www.youtube.com/watch?v=KT3XKDBZW7M&list=PLPxbbTqCLbGHPxZpw4xj_Wwg8-fdNxJRh&index=4',\n            image: {\n              speakers: ['/images/home/conf2019/ashley.jpg'],\n            },\n          },\n          {\n            id: 'conf-2019-4',\n            title: 'How Our Team Is Using React Native to Save The World',\n            description: 'Tania Papazafeiropoulou',\n            url: 'https://www.youtube.com/watch?v=zVHWugBPGBE&list=PLPxbbTqCLbGHPxZpw4xj_Wwg8-fdNxJRh&index=5',\n            image: {\n              speakers: ['/images/home/conf2019/tania.jpg'],\n            },\n          },\n          {\n            id: 'conf-2019-5',\n            title:\n              'Using Hooks and Codegen to Bring the Benefits of GraphQL to REST APIs',\n            description: 'Tejas Kumar',\n            url: 'https://www.youtube.com/watch?v=cdsnzfJUqm0&list=PLPxbbTqCLbGHPxZpw4xj_Wwg8-fdNxJRh&index=6',\n            image: {\n              speakers: ['/images/home/conf2019/tejas.jpg'],\n            },\n          },\n          {\n            id: 'conf-2019-6',\n            title: 'Building a Custom React Renderer',\n            description: 'Sophie Alpert',\n            url: 'https://www.youtube.com/watch?v=CGpMlWVcHok&list=PLPxbbTqCLbGHPxZpw4xj_Wwg8-fdNxJRh&index=7',\n            image: {\n              speakers: ['/images/home/conf2019/sophie.jpg'],\n            },\n          },\n          {\n            id: 'conf-2019-7',\n            title: 'Is React Translated Yet?',\n            description: 'Nat Alison',\n            url: 'https://www.youtube.com/watch?v=lLE4Jqaek5k&list=PLPxbbTqCLbGHPxZpw4xj_Wwg8-fdNxJRh&index=12',\n            image: {\n              speakers: ['/images/home/conf2019/nat.jpg'],\n            },\n          },\n          {\n            id: 'conf-2019-8',\n            title: 'Building (And Re-Building) the Airbnb Design System',\n            description: 'Maja Wichrowska and Tae Kim',\n            url: 'https://www.youtube.com/watch?v=fHQ1WSx41CA&list=PLPxbbTqCLbGHPxZpw4xj_Wwg8-fdNxJRh&index=13',\n            image: {\n              speakers: [\n                '/images/home/conf2019/maja.jpg',\n                '/images/home/conf2019/tae.jpg',\n              ],\n            },\n          },\n          {\n            id: 'conf-2019-9',\n            title: 'Accessibility Is a Marathon, Not a Sprint',\n            description: 'Brittany Feenstra',\n            url: 'https://www.youtube.com/watch?v=ONSD-t4gBb8&list=PLPxbbTqCLbGHPxZpw4xj_Wwg8-fdNxJRh&index=14',\n            image: {\n              speakers: ['/images/home/conf2019/brittany.jpg'],\n            },\n          },\n          {\n            id: 'conf-2019-10',\n            title: 'The State of React State in 2019',\n            description: 'Becca Bailey',\n            url: 'https://www.youtube.com/watch?v=wUMMUyQtMSg&list=PLPxbbTqCLbGHPxZpw4xj_Wwg8-fdNxJRh&index=15',\n            image: {\n              speakers: ['/images/home/conf2019/becca.jpg'],\n            },\n          },\n          {\n            id: 'conf-2019-11',\n            title: 'Let’s Program Like It’s 1999',\n            description: 'Lee Byron',\n            url: 'https://www.youtube.com/watch?v=vG8WpLr6y_U&list=PLPxbbTqCLbGHPxZpw4xj_Wwg8-fdNxJRh&index=16',\n            image: {\n              speakers: ['/images/home/conf2019/lee.jpg'],\n            },\n          },\n          {\n            id: 'conf-2019-12',\n            title: 'React Developer Tooling',\n            description: 'Brian Vaughn',\n            url: 'https://www.youtube.com/watch?v=Mjrfb1r3XEM&list=PLPxbbTqCLbGHPxZpw4xj_Wwg8-fdNxJRh&index=17',\n            image: {\n              speakers: ['/images/home/conf2019/brian.jpg'],\n            },\n          },\n          {\n            id: 'conf-2019-13',\n            title: 'Data Fetching With Suspense In Relay',\n            description: 'Joe Savona',\n            url: 'https://www.youtube.com/watch?v=Tl0S7QkxFE4&list=PLPxbbTqCLbGHPxZpw4xj_Wwg8-fdNxJRh&index=18',\n            image: {\n              speakers: ['/images/home/conf2019/joe.jpg'],\n            },\n          },\n          {\n            id: 'conf-2019-14',\n            title: 'Automatic Visualizations of the Frontend',\n            description: 'Cameron Yick',\n            url: 'https://www.youtube.com/watch?v=SbreAPNmZOk&list=PLPxbbTqCLbGHPxZpw4xj_Wwg8-fdNxJRh&index=19',\n            image: {\n              speakers: ['/images/home/conf2019/cameron.jpg'],\n            },\n          },\n          {\n            id: 'conf-2019-15',\n            title: 'React Is Fiction',\n            description: 'Jenn Creighton',\n            url: 'https://www.youtube.com/watch?v=kqh4lz2Lkzs&list=PLPxbbTqCLbGHPxZpw4xj_Wwg8-fdNxJRh&index=20',\n            image: {\n              speakers: ['/images/home/conf2019/jenn.jpg'],\n            },\n          },\n          {\n            id: 'conf-2019-16',\n            title: 'Progressive Web Animations',\n            description: 'Alexandra Holachek',\n            url: 'https://www.youtube.com/watch?v=laPsceJ4tTY&list=PLPxbbTqCLbGHPxZpw4xj_Wwg8-fdNxJRh&index=21',\n            image: {\n              speakers: ['/images/home/conf2019/alexandra.jpg'],\n            },\n          },\n          {\n            id: 'conf-2019-17',\n            title:\n              'Creating Games, Animations and Interactions with the Wick Editor',\n            description: 'Luca Damasco',\n            url: 'https://www.youtube.com/watch?v=laPsceJ4tTY&list=PLPxbbTqCLbGHPxZpw4xj_Wwg8-fdNxJRh&index=21',\n            image: {\n              speakers: ['/images/home/conf2019/luca.jpg'],\n            },\n          },\n          {\n            id: 'conf-2019-18',\n            title: 'Building React-Select',\n            description: 'Jed Watson',\n            url: 'https://www.youtube.com/watch?v=yS0jUnmBujE&list=PLPxbbTqCLbGHPxZpw4xj_Wwg8-fdNxJRh&index=25',\n            image: {\n              speakers: ['/images/home/conf2019/jed.jpg'],\n            },\n          },\n          {\n            id: 'conf-2019-19',\n            title: 'Promoting Transparency in Government Spending with React',\n            description: 'Lizzie Salita',\n            url: 'https://www.youtube.com/watch?v=CVfXICcNfHE&list=PLPxbbTqCLbGHPxZpw4xj_Wwg8-fdNxJRh&index=26',\n            image: {\n              speakers: ['/images/home/conf2019/lizzie.jpg'],\n            },\n          },\n          {\n            id: 'conf-2019-20',\n            title: 'Wonder-driven Development: Using React to Make a Spaceship',\n            description: 'Alex Anderson',\n            url: 'https://www.youtube.com/watch?v=aV0uOPWHKt4&list=PLPxbbTqCLbGHPxZpw4xj_Wwg8-fdNxJRh&index=27',\n            image: {\n              speakers: ['/images/home/conf2019/alex.jpg'],\n            },\n          },\n        ]);\n      }\n    }, loadTalksDelay);\n  });\n  talksCache.set(confId, promise);\n  return promise;\n}\n"
  },
  {
    "path": "src/components/Layout/Page.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport {Suspense} from 'react';\nimport * as React from 'react';\nimport {useRouter} from 'next/router';\nimport {SidebarNav} from './SidebarNav';\nimport {Footer} from './Footer';\nimport {Toc} from './Toc';\n// import SocialBanner from '../SocialBanner';\nimport {DocsPageFooter} from 'components/DocsFooter';\nimport {Seo} from 'components/Seo';\nimport PageHeading from 'components/PageHeading';\nimport {getRouteMeta} from './getRouteMeta';\nimport {TocContext} from '../MDX/TocContext';\nimport {Languages, LanguagesContext} from '../MDX/LanguagesContext';\nimport type {TocItem} from 'components/MDX/TocContext';\nimport type {RouteItem} from 'components/Layout/getRouteMeta';\nimport {HomeContent} from './HomeContent';\nimport {TopNav} from './TopNav';\nimport cn from 'classnames';\nimport Head from 'next/head';\n\nimport(/* webpackPrefetch: true */ '../MDX/CodeBlock/CodeBlock');\n\ninterface PageProps {\n  children: React.ReactNode;\n  toc: Array<TocItem>;\n  routeTree: RouteItem;\n  meta: {\n    title?: string;\n    titleForTitleTag?: string;\n    version?: 'experimental' | 'canary';\n    description?: string;\n  };\n  section: 'learn' | 'reference' | 'community' | 'blog' | 'home' | 'unknown';\n  languages?: Languages | null;\n}\n\nexport function Page({\n  children,\n  toc,\n  routeTree,\n  meta,\n  section,\n  languages = null,\n}: PageProps) {\n  const {asPath} = useRouter();\n  const cleanedPath = asPath.split(/[\\?\\#]/)[0];\n  const {route, nextRoute, prevRoute, breadcrumbs, order} = getRouteMeta(\n    cleanedPath,\n    routeTree\n  );\n  const title = meta.title || route?.title || '';\n  const version = meta.version;\n  const description = meta.description || route?.description || '';\n  const isHomePage = cleanedPath === '/';\n  const isBlogIndex = cleanedPath === '/blog';\n\n  let content;\n  if (isHomePage) {\n    content = <HomeContent />;\n  } else {\n    content = (\n      <div className=\"ps-0\">\n        <div\n          className={cn(\n            section === 'blog' && 'mx-auto px-0 lg:px-4 max-w-5xl'\n          )}>\n          <PageHeading\n            title={title}\n            version={version}\n            description={description}\n            tags={route?.tags}\n            breadcrumbs={breadcrumbs}\n          />\n        </div>\n        <div className=\"px-5 sm:px-12\">\n          <div\n            className={cn(\n              'max-w-7xl mx-auto',\n              section === 'blog' && 'lg:flex lg:flex-col lg:items-center'\n            )}>\n            <TocContext value={toc}>\n              <LanguagesContext value={languages}>{children}</LanguagesContext>\n            </TocContext>\n          </div>\n          {!isBlogIndex && (\n            <DocsPageFooter\n              route={route}\n              nextRoute={nextRoute}\n              prevRoute={prevRoute}\n            />\n          )}\n        </div>\n      </div>\n    );\n  }\n\n  let hasColumns = true;\n  let showSidebar = true;\n  let showToc = true;\n  if (isHomePage || isBlogIndex) {\n    hasColumns = false;\n    showSidebar = false;\n    showToc = false;\n  } else if (section === 'blog') {\n    showToc = false;\n    hasColumns = false;\n    showSidebar = false;\n  }\n\n  let searchOrder;\n  if (section === 'learn' || (section === 'blog' && !isBlogIndex)) {\n    searchOrder = order;\n  }\n\n  return (\n    <>\n      <Seo\n        title={title}\n        titleForTitleTag={meta.titleForTitleTag}\n        isHomePage={isHomePage}\n        image={`/images/og-` + section + '.png'}\n        searchOrder={searchOrder}\n      />\n      {(isHomePage || isBlogIndex) && (\n        <Head>\n          <link\n            rel=\"alternate\"\n            type=\"application/rss+xml\"\n            title=\"React Blog RSS Feed\"\n            href=\"/rss.xml\"\n          />\n        </Head>\n      )}\n      {/* <SocialBanner /> */}\n      <TopNav\n        section={section}\n        routeTree={routeTree}\n        breadcrumbs={breadcrumbs}\n      />\n      <div\n        className={cn(\n          hasColumns &&\n            'grid grid-cols-only-content lg:grid-cols-sidebar-content 2xl:grid-cols-sidebar-content-toc'\n        )}>\n        {showSidebar && (\n          <div className=\"lg:-mt-16 z-10\">\n            <div className=\"fixed top-0 py-0 shadow lg:pt-16 lg:sticky start-0 end-0 lg:shadow-none\">\n              <SidebarNav\n                key={section}\n                routeTree={routeTree}\n                breadcrumbs={breadcrumbs}\n              />\n            </div>\n          </div>\n        )}\n        {/* No fallback UI so need to be careful not to suspend directly inside. */}\n        <Suspense fallback={null}>\n          <main className=\"min-w-0 isolate\">\n            <article\n              className=\"font-normal break-words text-primary dark:text-primary-dark\"\n              key={asPath}>\n              {content}\n            </article>\n            <div\n              className={cn(\n                'self-stretch w-full',\n                isHomePage && 'bg-wash dark:bg-gray-95 mt-[-1px]'\n              )}>\n              {!isHomePage && (\n                <div className=\"w-full px-5 pt-10 mx-auto sm:px-12 md:px-12 md:pt-12 lg:pt-10\">\n                  <hr className=\"mx-auto max-w-7xl border-border dark:border-border-dark\" />\n                </div>\n              )}\n              <div\n                className={cn(\n                  'py-12 px-5 sm:px-12 md:px-12 sm:py-12 md:py-16 lg:py-14',\n                  isHomePage && 'lg:pt-0'\n                )}>\n                <Footer />\n              </div>\n            </div>\n          </main>\n        </Suspense>\n        <div className=\"hidden -mt-16 lg:max-w-custom-xs 2xl:block\">\n          {showToc && toc.length > 0 && <Toc headings={toc} key={asPath} />}\n        </div>\n      </div>\n    </>\n  );\n}\n"
  },
  {
    "path": "src/components/Layout/Sidebar/SidebarButton.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport * as React from 'react';\nimport cn from 'classnames';\nimport {IconNavArrow} from 'components/Icon/IconNavArrow';\n\ninterface SidebarButtonProps {\n  title: string;\n  heading: boolean;\n  level: number;\n  onClick: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;\n  isExpanded?: boolean;\n  isBreadcrumb?: boolean;\n}\n\nexport function SidebarButton({\n  title,\n  heading,\n  level,\n  onClick,\n  isExpanded,\n  isBreadcrumb,\n}: SidebarButtonProps) {\n  return (\n    <div\n      className={cn({\n        'my-1': heading || level === 1,\n        'my-3': level > 1,\n      })}>\n      <button\n        className={cn(\n          'p-2 pe-2 ps-5 w-full rounded-e-lg text-start hover:bg-gray-5 dark:hover:bg-gray-80 relative flex items-center justify-between',\n          {\n            'p-2 text-base': level > 1,\n            'text-link bg-highlight dark:bg-highlight-dark text-base font-bold hover:bg-highlight dark:hover:bg-highlight-dark hover:text-link dark:hover:text-link-dark':\n              !heading && isBreadcrumb && !isExpanded,\n            'p-4 my-6 text-2xl lg:my-auto lg:text-sm font-bold': heading,\n            'p-2 hover:text-gray-70 text-base font-bold text-primary dark:text-primary-dark':\n              !heading && !isBreadcrumb,\n            'text-primary dark:text-primary-dark': heading && !isBreadcrumb,\n            'text-primary dark:text-primary-dark text-base font-bold bg-card dark:bg-card-dark':\n              !heading && isExpanded,\n          }\n        )}\n        onClick={onClick}>\n        {title}\n        {typeof isExpanded && !heading && (\n          <span className=\"pe-2 text-gray-30\">\n            <IconNavArrow displayDirection={isExpanded ? 'down' : 'end'} />\n          </span>\n        )}\n      </button>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/components/Layout/Sidebar/SidebarLink.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\n/* eslint-disable jsx-a11y/no-static-element-interactions */\n/* eslint-disable jsx-a11y/click-events-have-key-events */\nimport {useRef, useEffect} from 'react';\nimport * as React from 'react';\nimport cn from 'classnames';\nimport {IconNavArrow} from 'components/Icon/IconNavArrow';\nimport {IconCanary} from 'components/Icon/IconCanary';\nimport {IconExperimental} from 'components/Icon/IconExperimental';\nimport Link from 'next/link';\n\ninterface SidebarLinkProps {\n  href: string;\n  selected?: boolean;\n  title: string;\n  level: number;\n  version?: 'canary' | 'major' | 'experimental' | 'rc';\n  icon?: React.ReactNode;\n  isExpanded?: boolean;\n  hideArrow?: boolean;\n  isPending: boolean;\n}\n\nexport function SidebarLink({\n  href,\n  selected = false,\n  title,\n  version,\n  level,\n  isExpanded,\n  hideArrow,\n  isPending,\n}: SidebarLinkProps) {\n  const ref = useRef<HTMLAnchorElement>(null);\n\n  useEffect(() => {\n    if (selected && ref && ref.current) {\n      // @ts-ignore\n      if (typeof ref.current.scrollIntoViewIfNeeded === 'function') {\n        // @ts-ignore\n        ref.current.scrollIntoViewIfNeeded();\n      }\n    }\n  }, [ref, selected]);\n\n  let target = '';\n  if (href.startsWith('https://')) {\n    target = '_blank';\n  }\n  return (\n    <Link\n      href={href}\n      ref={ref}\n      title={title}\n      target={target}\n      passHref\n      aria-current={selected ? 'page' : undefined}\n      className={cn(\n        'p-2 pe-2 w-full rounded-none lg:rounded-e-2xl text-start hover:bg-gray-5 dark:hover:bg-gray-80 relative flex items-center justify-between',\n        {\n          'text-sm ps-6': level > 0,\n          'ps-5': level < 2,\n          'text-base font-bold': level === 0,\n          'text-primary dark:text-primary-dark': level === 0 && !selected,\n          'text-base text-secondary dark:text-secondary-dark':\n            level > 0 && !selected,\n          'text-base text-link dark:text-link-dark bg-highlight dark:bg-highlight-dark border-blue-40 hover:bg-highlight hover:text-link dark:hover:bg-highlight-dark dark:hover:text-link-dark':\n            selected,\n          'dark:bg-gray-70 bg-gray-3 dark:hover:bg-gray-70 hover:bg-gray-3':\n            isPending,\n        }\n      )}>\n      {/* This here needs to be refactored ofc */}\n      <div>\n        {title}{' '}\n        {version === 'major' && (\n          <span\n            title=\"- This feature is available in React 19 beta and the React canary channel\"\n            className={`text-xs px-1 ms-1 rounded bg-gray-10 dark:bg-gray-40 dark:bg-opacity-20 text-gray-40 dark:text-gray-40`}>\n            React 19\n          </span>\n        )}\n        {version === 'canary' && (\n          <IconCanary\n            title=\" - This feature is available in the latest Canary version of React\"\n            className=\"ms-1 text-gray-30 dark:text-gray-60 inline-block w-3.5 h-3.5 align-[-3px]\"\n          />\n        )}\n        {version === 'experimental' && (\n          <IconExperimental\n            title=\" - This feature is available in the latest Experimental version of React\"\n            className=\"ms-1 text-gray-30 dark:text-gray-60 inline-block w-3.5 h-3.5 align-[-3px]\"\n          />\n        )}\n        {version === 'rc' && (\n          <IconCanary\n            title=\" - This feature is available in the latest RC version\"\n            className=\"ms-1 text-gray-30 dark:text-gray-60 inline-block w-3.5 h-3.5 align-[-3px]\"\n          />\n        )}\n      </div>\n\n      {isExpanded != null && !hideArrow && (\n        <span\n          className={cn('pe-1', {\n            'text-link dark:text-link-dark': isExpanded,\n            'text-tertiary dark:text-tertiary-dark': !isExpanded,\n          })}>\n          <IconNavArrow displayDirection={isExpanded ? 'down' : 'end'} />\n        </span>\n      )}\n    </Link>\n  );\n}\n"
  },
  {
    "path": "src/components/Layout/Sidebar/SidebarRouteTree.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport {useRef, useLayoutEffect, Fragment} from 'react';\n\nimport cn from 'classnames';\nimport {useRouter} from 'next/router';\nimport {SidebarLink} from './SidebarLink';\nimport {useCollapse} from 'react-collapsed';\nimport usePendingRoute from 'hooks/usePendingRoute';\nimport type {RouteItem} from 'components/Layout/getRouteMeta';\nimport {siteConfig} from 'siteConfig';\n\ninterface SidebarRouteTreeProps {\n  isForceExpanded: boolean;\n  breadcrumbs: RouteItem[];\n  routeTree: RouteItem;\n  level?: number;\n}\n\nfunction CollapseWrapper({\n  isExpanded,\n  duration,\n  children,\n}: {\n  isExpanded: boolean;\n  duration: number;\n  children: any;\n}) {\n  const ref = useRef<HTMLDivElement | null>(null);\n  const timeoutRef = useRef<number | null>(null);\n  const {getCollapseProps} = useCollapse({\n    isExpanded,\n    duration,\n  });\n\n  // Disable pointer events while animating.\n  const isExpandedRef = useRef(isExpanded);\n  if (typeof window !== 'undefined') {\n    // eslint-disable-next-line react-compiler/react-compiler\n    // eslint-disable-next-line react-hooks/rules-of-hooks\n    useLayoutEffect(() => {\n      const wasExpanded = isExpandedRef.current;\n      if (wasExpanded === isExpanded) {\n        return;\n      }\n      isExpandedRef.current = isExpanded;\n      if (ref.current !== null) {\n        const node: HTMLDivElement = ref.current;\n        node.style.pointerEvents = 'none';\n        if (timeoutRef.current !== null) {\n          window.clearTimeout(timeoutRef.current);\n        }\n        timeoutRef.current = window.setTimeout(() => {\n          node.style.pointerEvents = '';\n        }, duration + 100);\n      }\n    });\n  }\n\n  return (\n    <div\n      ref={ref}\n      className={cn(isExpanded ? 'opacity-100' : 'opacity-50')}\n      style={{\n        transition: `opacity ${duration}ms ease-in-out`,\n      }}>\n      <div {...getCollapseProps()}>{children}</div>\n    </div>\n  );\n}\n\nexport function SidebarRouteTree({\n  isForceExpanded,\n  breadcrumbs,\n  routeTree,\n  level = 0,\n}: SidebarRouteTreeProps) {\n  const slug = useRouter().asPath.split(/[\\?\\#]/)[0];\n  const pendingRoute = usePendingRoute();\n  const currentRoutes = routeTree.routes as RouteItem[];\n  return (\n    <ul>\n      {currentRoutes.map(\n        (\n          {\n            path,\n            title,\n            routes,\n            version,\n            heading,\n            hasSectionHeader,\n            sectionHeader,\n          },\n          index\n        ) => {\n          const selected = slug === path;\n          let listItem = null;\n          if (!path || heading) {\n            // if current route item has no path and children treat it as an API sidebar heading\n            listItem = (\n              <SidebarRouteTree\n                level={level + 1}\n                isForceExpanded={isForceExpanded}\n                routeTree={{title, routes}}\n                breadcrumbs={[]}\n              />\n            );\n          } else if (routes) {\n            // if route has a path and child routes, treat it as an expandable sidebar item\n            const isBreadcrumb =\n              breadcrumbs.length > 1 &&\n              breadcrumbs[breadcrumbs.length - 1].path === path;\n            const isExpanded = isForceExpanded || isBreadcrumb || selected;\n            listItem = (\n              <li key={`${title}-${path}-${level}-heading`}>\n                <SidebarLink\n                  key={`${title}-${path}-${level}-link`}\n                  href={path}\n                  isPending={pendingRoute === path}\n                  selected={selected}\n                  level={level}\n                  title={title}\n                  version={version}\n                  isExpanded={isExpanded}\n                  hideArrow={isForceExpanded}\n                />\n                <CollapseWrapper duration={250} isExpanded={isExpanded}>\n                  <SidebarRouteTree\n                    isForceExpanded={isForceExpanded}\n                    routeTree={{title, routes}}\n                    breadcrumbs={breadcrumbs}\n                    level={level + 1}\n                  />\n                </CollapseWrapper>\n              </li>\n            );\n          } else {\n            // if route has a path and no child routes, treat it as a sidebar link\n            listItem = (\n              <li key={`${title}-${path}-${level}-link`}>\n                <SidebarLink\n                  isPending={pendingRoute === path}\n                  href={path}\n                  selected={selected}\n                  level={level}\n                  title={title}\n                  version={version}\n                />\n              </li>\n            );\n          }\n          if (hasSectionHeader) {\n            let sectionHeaderText =\n              sectionHeader != null\n                ? sectionHeader.replace('{{version}}', siteConfig.version)\n                : '';\n            return (\n              <Fragment key={`${sectionHeaderText}-${level}-separator`}>\n                {index !== 0 && (\n                  <li\n                    role=\"separator\"\n                    className=\"mt-4 mb-2 ms-5 border-b border-border dark:border-border-dark\"\n                  />\n                )}\n                <h3\n                  className={cn(\n                    'mb-1 text-sm font-bold ms-5 text-tertiary dark:text-tertiary-dark',\n                    index !== 0 && 'mt-2'\n                  )}>\n                  {sectionHeaderText}\n                </h3>\n              </Fragment>\n            );\n          } else {\n            return listItem;\n          }\n        }\n      )}\n    </ul>\n  );\n}\n"
  },
  {
    "path": "src/components/Layout/Sidebar/index.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nexport {SidebarButton} from './SidebarButton';\nexport {SidebarLink} from './SidebarLink';\nexport {SidebarRouteTree} from './SidebarRouteTree';\n"
  },
  {
    "path": "src/components/Layout/SidebarNav/SidebarNav.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport {Suspense} from 'react';\nimport * as React from 'react';\nimport cn from 'classnames';\nimport {SidebarRouteTree} from '../Sidebar/SidebarRouteTree';\nimport type {RouteItem} from '../getRouteMeta';\n\ndeclare global {\n  interface Window {\n    __theme: string;\n    __setPreferredTheme: (theme: string) => void;\n  }\n}\n\nexport default function SidebarNav({\n  routeTree,\n  breadcrumbs,\n}: {\n  routeTree: RouteItem;\n  breadcrumbs: RouteItem[];\n}) {\n  // HACK. Fix up the data structures instead.\n  if ((routeTree as any).routes.length === 1) {\n    routeTree = (routeTree as any).routes[0];\n  }\n\n  return (\n    <div\n      className={cn(\n        'sticky top-0 lg:bottom-0 lg:h-[calc(100vh-4rem)] flex flex-col'\n      )}>\n      <div\n        className=\"overflow-y-scroll no-bg-scrollbar lg:w-[342px] grow bg-wash dark:bg-wash-dark\"\n        style={{\n          overscrollBehavior: 'contain',\n        }}>\n        <aside\n          className={cn(\n            `lg:grow flex-col w-full pb-8 lg:pb-0 lg:max-w-custom-xs z-10 hidden lg:block`\n          )}>\n          <nav\n            role=\"navigation\"\n            style={{'--bg-opacity': '.2'} as React.CSSProperties} // Need to cast here because CSS vars aren't considered valid in TS types (cuz they could be anything)\n            className=\"w-full pt-6 scrolling-touch lg:h-auto grow pe-0 lg:pe-5 lg:pb-16 md:pt-4 lg:pt-4 scrolling-gpu\">\n            {/* No fallback UI so need to be careful not to suspend directly inside. */}\n            <Suspense fallback={null}>\n              <SidebarRouteTree\n                routeTree={routeTree}\n                breadcrumbs={breadcrumbs}\n                isForceExpanded={false}\n              />\n            </Suspense>\n            <div className=\"h-20\" />\n          </nav>\n        </aside>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/components/Layout/SidebarNav/index.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nexport {default as SidebarNav} from './SidebarNav';\n"
  },
  {
    "path": "src/components/Layout/Toc.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport cx from 'classnames';\nimport {useTocHighlight} from './useTocHighlight';\nimport type {Toc} from '../MDX/TocContext';\n\nexport function Toc({headings}: {headings: Toc}) {\n  const {currentIndex} = useTocHighlight();\n  // TODO: We currently have a mismatch between the headings in the document\n  // and the headings we find in MarkdownPage (i.e. we don't find Recap or Challenges).\n  // Select the max TOC item we have here for now, but remove this after the fix.\n  const selectedIndex = Math.min(currentIndex, headings.length - 1);\n  return (\n    <nav role=\"navigation\" className=\"pt-20 sticky top-0 end-0\">\n      {headings.length > 0 && (\n        <h2 className=\"mb-3 lg:mb-3 uppercase tracking-wide font-bold text-sm text-secondary dark:text-secondary-dark px-4 w-full\">\n          이 페이지의 내용\n        </h2>\n      )}\n      <div\n        className=\"h-full overflow-y-auto ps-4 max-h-[calc(100vh-7.5rem)]\"\n        style={{\n          overscrollBehavior: 'contain',\n        }}>\n        <ul className=\"space-y-2 pb-16\">\n          {headings.length > 0 &&\n            headings.map((h, i) => {\n              if (!h.url && process.env.NODE_ENV === 'development') {\n                console.error('Heading does not have URL');\n              }\n              return (\n                <li\n                  key={`heading-${h.url}-${i}`}\n                  className={cx(\n                    'text-sm px-2 rounded-s-xl',\n                    selectedIndex === i\n                      ? 'bg-highlight dark:bg-highlight-dark'\n                      : null,\n                    {\n                      'ps-4': h?.depth === 3,\n                      hidden: h.depth && h.depth > 3,\n                    }\n                  )}>\n                  <a\n                    className={cx(\n                      selectedIndex === i\n                        ? 'text-link dark:text-link-dark font-bold'\n                        : 'text-secondary dark:text-secondary-dark',\n                      'block hover:text-link dark:hover:text-link-dark leading-normal py-2'\n                    )}\n                    href={h.url}>\n                    {h.text}\n                  </a>\n                </li>\n              );\n            })}\n        </ul>\n      </div>\n    </nav>\n  );\n}\n"
  },
  {
    "path": "src/components/Layout/TopNav/BrandMenu.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nimport * as ContextMenu from '@radix-ui/react-context-menu';\nimport {IconCopy} from 'components/Icon/IconCopy';\nimport {IconDownload} from 'components/Icon/IconDownload';\nimport {IconNewPage} from 'components/Icon/IconNewPage';\nimport {ExternalLink} from 'components/ExternalLink';\nimport {IconClose} from '../../Icon/IconClose';\n\nfunction MenuItem({\n  children,\n  onSelect,\n}: {\n  children: React.ReactNode;\n  onSelect?: () => void;\n}) {\n  return (\n    <ContextMenu.Item\n      className=\"flex items-center hover:bg-border dark:hover:bg-border-dark ps-6 pe-4 py-2 w-full text-base cursor-pointer\"\n      onSelect={onSelect}>\n      {children}\n    </ContextMenu.Item>\n  );\n}\n\nfunction DownloadMenuItem({\n  fileName,\n  href,\n  children,\n}: {\n  fileName: string;\n  href: string;\n  children: React.ReactNode;\n}) {\n  return (\n    <a download={fileName} href={href} className=\"flex items-center w-full\">\n      <MenuItem>{children}</MenuItem>\n    </a>\n  );\n}\n\nexport default function BrandMenu({children}: {children: React.ReactNode}) {\n  return (\n    <ContextMenu.Root>\n      <ContextMenu.Trigger className=\"flex items-center\">\n        {children}\n      </ContextMenu.Trigger>\n      <ContextMenu.Portal>\n        <ContextMenu.Content\n          className=\"hidden lg:block z-50 mt-6 bg-wash border border-border dark:border-border-dark dark:bg-wash-dark rounded min-w-56 overflow-hidden shadow\"\n          // @ts-ignore\n          sideOffset={0}\n          align=\"end\">\n          <ContextMenu.Label className=\"ps-4 pt-2 text-base text-tertiary dark:text-tertiary-dark\">\n            Dark Mode\n          </ContextMenu.Label>\n          <DownloadMenuItem\n            fileName=\"react_logo_dark.svg\"\n            href=\"/images/brand/logo_dark.svg\">\n            <span className=\"w-8\">\n              <IconDownload />\n            </span>\n            <span>Logo SVG</span>\n          </DownloadMenuItem>\n          <DownloadMenuItem\n            fileName=\"react_wordmark_dark.svg\"\n            href=\"/images/brand/wordmark_dark.svg\">\n            <span className=\"w-8\">\n              <IconDownload />\n            </span>\n            <span>Wordmark SVG</span>\n          </DownloadMenuItem>\n          <MenuItem\n            onSelect={async () => {\n              await navigator.clipboard.writeText('#58C4DC');\n            }}>\n            <span className=\"w-8\">\n              <IconCopy />\n            </span>\n            <span>Copy dark mode color</span>\n          </MenuItem>\n          <ContextMenu.Label className=\"ps-4 text-base text-tertiary dark:text-tertiary-dark\">\n            Light Mode\n          </ContextMenu.Label>\n          <DownloadMenuItem\n            fileName=\"react_logo_light.svg\"\n            href=\"/images/brand/logo_light.svg\">\n            <span className=\"w-8\">\n              <IconDownload />\n            </span>\n            <span>Logo SVG</span>\n          </DownloadMenuItem>\n          <DownloadMenuItem\n            fileName=\"react_wordmark_light.svg\"\n            href=\"/images/brand/wordmark_light.svg\">\n            <span className=\"w-8\">\n              <IconDownload />\n            </span>\n            <span>Wordmark SVG</span>\n          </DownloadMenuItem>\n          <MenuItem\n            onSelect={async () => {\n              await navigator.clipboard.writeText('#087EA4');\n            }}>\n            <span className=\"w-8\">\n              <IconCopy />\n            </span>\n            <span>Copy light mode color</span>\n          </MenuItem>\n          <div className=\"uwu-visible flex flex-col\">\n            <ContextMenu.Separator className=\"\" />\n            <ContextMenu.Label className=\"ps-4 text-base text-tertiary dark:text-tertiary-dark\">\n              uwu\n            </ContextMenu.Label>\n            <MenuItem\n              onSelect={() => {\n                // @ts-ignore\n                window.__setUwu(false);\n              }}>\n              <span className=\"w-8\">\n                <IconClose />\n              </span>\n              <span>Turn off</span>\n            </MenuItem>\n            <DownloadMenuItem fileName=\"react_uwu_png\" href=\"/images/uwu.png\">\n              <span className=\"w-8\">\n                <IconDownload />\n              </span>\n              <span>Logo PNG</span>\n            </DownloadMenuItem>\n\n            <ExternalLink\n              className=\"flex items-center\"\n              href=\"https://github.com/SAWARATSUKI/KawaiiLogos\">\n              <MenuItem>\n                <span className=\"w-8\">\n                  <IconNewPage />\n                </span>\n                <span>Logo by @sawaratsuki1004</span>\n              </MenuItem>\n            </ExternalLink>\n          </div>\n        </ContextMenu.Content>\n      </ContextMenu.Portal>\n    </ContextMenu.Root>\n  );\n}\n"
  },
  {
    "path": "src/components/Layout/TopNav/TopNav.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport {\n  useState,\n  useRef,\n  useCallback,\n  useEffect,\n  startTransition,\n  Suspense,\n} from 'react';\nimport Image from 'next/image';\nimport * as React from 'react';\nimport cn from 'classnames';\nimport NextLink from 'next/link';\nimport {useRouter} from 'next/router';\nimport {disableBodyScroll, enableBodyScroll} from 'body-scroll-lock';\n\nimport {IconClose} from 'components/Icon/IconClose';\nimport {IconHamburger} from 'components/Icon/IconHamburger';\nimport {IconSearch} from 'components/Icon/IconSearch';\nimport {Search} from 'components/Search';\nimport {Logo} from '../../Logo';\nimport {SidebarRouteTree} from '../Sidebar';\nimport type {RouteItem} from '../getRouteMeta';\nimport {siteConfig} from 'siteConfig';\nimport BrandMenu from './BrandMenu';\n\ndeclare global {\n  interface Window {\n    __theme: string;\n    __setPreferredTheme: (theme: string) => void;\n  }\n}\n\nconst darkIcon = (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width=\"28\"\n    height=\"28\"\n    viewBox=\"0 0 32 32\">\n    <g fill=\"none\" fillRule=\"evenodd\" transform=\"translate(-440 -200)\">\n      <path\n        fill=\"currentColor\"\n        fillRule=\"nonzero\"\n        stroke=\"currentColor\"\n        strokeWidth={0.5}\n        d=\"M102,21 C102,18.1017141 103.307179,15.4198295 105.51735,13.6246624 C106.001939,13.2310647 105.821611,12.4522936 105.21334,12.3117518 C104.322006,12.1058078 103.414758,12 102.5,12 C95.8722864,12 90.5,17.3722864 90.5,24 C90.5,30.6277136 95.8722864,36 102.5,36 C106.090868,36 109.423902,34.4109093 111.690274,31.7128995 C112.091837,31.2348572 111.767653,30.5041211 111.143759,30.4810139 C106.047479,30.2922628 102,26.1097349 102,21 Z M102.5,34.5 C96.7007136,34.5 92,29.7992864 92,24 C92,18.2007136 96.7007136,13.5 102.5,13.5 C102.807386,13.5 103.113925,13.5136793 103.419249,13.5407785 C101.566047,15.5446378 100.5,18.185162 100.5,21 C100.5,26.3198526 104.287549,30.7714322 109.339814,31.7756638 L109.516565,31.8092927 C107.615276,33.5209452 105.138081,34.5 102.5,34.5 Z\"\n        transform=\"translate(354.5 192)\"\n      />\n      <polygon points=\"444 228 468 228 468 204 444 204\" />\n    </g>\n  </svg>\n);\n\nconst lightIcon = (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width=\"32\"\n    height=\"32\"\n    viewBox=\"0 0 32 32\">\n    <g fill=\"none\" fillRule=\"evenodd\" transform=\"translate(-442 -200)\">\n      <g fill=\"currentColor\" transform=\"translate(356 144)\">\n        <path\n          fillRule=\"nonzero\"\n          d=\"M108.5 24C108.5 27.5902136 105.590214 30.5 102 30.5 98.4097864 30.5 95.5 27.5902136 95.5 24 95.5 20.4097864 98.4097864 17.5 102 17.5 105.590214 17.5 108.5 20.4097864 108.5 24zM107 24C107 21.2382136 104.761786 19 102 19 99.2382136 19 97 21.2382136 97 24 97 26.7617864 99.2382136 29 102 29 104.761786 29 107 26.7617864 107 24zM101 12.75L101 14.75C101 15.1642136 101.335786 15.5 101.75 15.5 102.164214 15.5 102.5 15.1642136 102.5 14.75L102.5 12.75C102.5 12.3357864 102.164214 12 101.75 12 101.335786 12 101 12.3357864 101 12.75zM95.7255165 14.6323616L96.7485165 16.4038616C96.9556573 16.7625614 97.4143618 16.8854243 97.7730616 16.6782835 98.1317614 16.4711427 98.2546243 16.0124382 98.0474835 15.6537384L97.0244835 13.8822384C96.8173427 13.5235386 96.3586382 13.4006757 95.9999384 13.6078165 95.6412386 13.8149573 95.5183757 14.2736618 95.7255165 14.6323616zM91.8822384 19.0244835L93.6537384 20.0474835C94.0124382 20.2546243 94.4711427 20.1317614 94.6782835 19.7730616 94.8854243 19.4143618 94.7625614 18.9556573 94.4038616 18.7485165L92.6323616 17.7255165C92.2736618 17.5183757 91.8149573 17.6412386 91.6078165 17.9999384 91.4006757 18.3586382 91.5235386 18.8173427 91.8822384 19.0244835zM90.75 25L92.75 25C93.1642136 25 93.5 24.6642136 93.5 24.25 93.5 23.8357864 93.1642136 23.5 92.75 23.5L90.75 23.5C90.3357864 23.5 90 23.8357864 90 24.25 90 24.6642136 90.3357864 25 90.75 25zM92.6323616 30.2744835L94.4038616 29.2514835C94.7625614 29.0443427 94.8854243 28.5856382 94.6782835 28.2269384 94.4711427 27.8682386 94.0124382 27.7453757 93.6537384 27.9525165L91.8822384 28.9755165C91.5235386 29.1826573 91.4006757 29.6413618 91.6078165 30.0000616 91.8149573 30.3587614 92.2736618 30.4816243 92.6323616 30.2744835zM97.0244835 34.1177616L98.0474835 32.3462616C98.2546243 31.9875618 98.1317614 31.5288573 97.7730616 31.3217165 97.4143618 31.1145757 96.9556573 31.2374386 96.7485165 31.5961384L95.7255165 33.3676384C95.5183757 33.7263382 95.6412386 34.1850427 95.9999384 34.3921835 96.3586382 34.5993243 96.8173427 34.4764614 97.0244835 34.1177616zM103 35.25L103 33.25C103 32.8357864 102.664214 32.5 102.25 32.5 101.835786 32.5 101.5 32.8357864 101.5 33.25L101.5 35.25C101.5 35.6642136 101.835786 36 102.25 36 102.664214 36 103 35.6642136 103 35.25zM108.274483 33.3676384L107.251483 31.5961384C107.044343 31.2374386 106.585638 31.1145757 106.226938 31.3217165 105.868239 31.5288573 105.745376 31.9875618 105.952517 32.3462616L106.975517 34.1177616C107.182657 34.4764614 107.641362 34.5993243 108.000062 34.3921835 108.358761 34.1850427 108.481624 33.7263382 108.274483 33.3676384zM112.117762 28.9755165L110.346262 27.9525165C109.987562 27.7453757 109.528857 27.8682386 109.321717 28.2269384 109.114576 28.5856382 109.237439 29.0443427 109.596138 29.2514835L111.367638 30.2744835C111.726338 30.4816243 112.185043 30.3587614 112.392183 30.0000616 112.599324 29.6413618 112.476461 29.1826573 112.117762 28.9755165zM113.25 23L111.25 23C110.835786 23 110.5 23.3357864 110.5 23.75 110.5 24.1642136 110.835786 24.5 111.25 24.5L113.25 24.5C113.664214 24.5 114 24.1642136 114 23.75 114 23.3357864 113.664214 23 113.25 23zM111.367638 17.7255165L109.596138 18.7485165C109.237439 18.9556573 109.114576 19.4143618 109.321717 19.7730616 109.528857 20.1317614 109.987562 20.2546243 110.346262 20.0474835L112.117762 19.0244835C112.476461 18.8173427 112.599324 18.3586382 112.392183 17.9999384 112.185043 17.6412386 111.726338 17.5183757 111.367638 17.7255165zM106.975517 13.8822384L105.952517 15.6537384C105.745376 16.0124382 105.868239 16.4711427 106.226938 16.6782835 106.585638 16.8854243 107.044343 16.7625614 107.251483 16.4038616L108.274483 14.6323616C108.481624 14.2736618 108.358761 13.8149573 108.000062 13.6078165 107.641362 13.4006757 107.182657 13.5235386 106.975517 13.8822384z\"\n          transform=\"translate(0 48)\"\n          stroke=\"currentColor\"\n          strokeWidth={0.25}\n        />\n        <path\n          d=\"M98.6123,60.1372 C98.6123,59.3552 98.8753,58.6427 99.3368,58.0942 C99.5293,57.8657 99.3933,57.5092 99.0943,57.5017 C99.0793,57.5012 99.0633,57.5007 99.0483,57.5007 C97.1578,57.4747 95.5418,59.0312 95.5008,60.9217 C95.4578,62.8907 97.0408,64.5002 98.9998,64.5002 C99.7793,64.5002 100.4983,64.2452 101.0798,63.8142 C101.3183,63.6372 101.2358,63.2627 100.9478,63.1897 C99.5923,62.8457 98.6123,61.6072 98.6123,60.1372\"\n          transform=\"translate(3 11)\"\n        />\n      </g>\n      <polygon points=\"444 228 468 228 468 204 444 204\" />\n    </g>\n  </svg>\n);\n\nconst languageIcon = (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width=\"24\"\n    height=\"24\"\n    viewBox=\"0 0 24 24\">\n    <path\n      fill=\"currentColor\"\n      d=\" M12.87 15.07l-2.54-2.51.03-.03c1.74-1.94 2.98-4.17 3.71-6.53H17V4h-7V2H8v2H1v1.99h11.17C11.5 7.92 10.44 9.75 9 11.35 8.07 10.32 7.3 9.19 6.69 8h-2c.73 1.63 1.73 3.17 2.98 4.56l-5.09 5.02L4 19l5-5 3.11 3.11.76-2.04zM18.5 10h-2L12 22h2l1.12-3h4.75L21 22h2l-4.5-12zm-2.62 7l1.62-4.33L19.12 17h-3.24z \"\n    />\n  </svg>\n);\n\nconst githubIcon = (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width=\"20\"\n    height=\"20\"\n    viewBox=\"0 0 24 24\">\n    <g fill=\"currentColor\">\n      <path d=\"M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z\" />\n    </g>\n  </svg>\n);\n\nfunction Link({\n  href,\n  children,\n  ...props\n}: React.AnchorHTMLAttributes<HTMLAnchorElement>) {\n  return (\n    <NextLink\n      href={`${href}`}\n      className=\"inline leading-normal transition duration-100 ease-in border-b border-opacity-0 text-primary dark:text-primary-dark hover:text-link hover:dark:text-link-dark border-link hover:border-opacity-100\"\n      {...props}>\n      {children}\n    </NextLink>\n  );\n}\n\nfunction NavItem({url, isActive, children}: any) {\n  return (\n    <div className=\"flex flex-auto\">\n      <Link\n        href={url}\n        className={cn(\n          'active:scale-95 transition-transform w-full text-center outline-link py-1.5 px-1.5 xs:px-3 sm:px-4 rounded-full capitalize whitespace-nowrap',\n          !isActive && 'hover:bg-primary/5 hover:dark:bg-primary-dark/5',\n          isActive &&\n            'bg-highlight dark:bg-highlight-dark text-link dark:text-link-dark'\n        )}>\n        {children}\n      </Link>\n    </div>\n  );\n}\n\nfunction Kbd(props: {children?: React.ReactNode; wide?: boolean}) {\n  const {wide, ...rest} = props;\n  const width = wide ? 'w-10' : 'w-5';\n\n  return (\n    <kbd\n      className={`${width} h-5 border border-transparent me-1 bg-wash dark:bg-wash-dark text-gray-30 align-middle p-0 inline-flex justify-center items-center text-xs text-center rounded-md`}\n      {...rest}\n    />\n  );\n}\n\nexport default function TopNav({\n  routeTree,\n  breadcrumbs,\n  section,\n}: {\n  routeTree: RouteItem;\n  breadcrumbs: RouteItem[];\n  section: 'learn' | 'reference' | 'community' | 'blog' | 'home' | 'unknown';\n}) {\n  const [isMenuOpen, setIsMenuOpen] = useState(false);\n  const [showSearch, setShowSearch] = useState(false);\n  const [isScrolled, setIsScrolled] = useState(false);\n  const scrollParentRef = useRef<HTMLDivElement>(null);\n  const {asPath} = useRouter();\n\n  // HACK. Fix up the data structures instead.\n  if ((routeTree as any).routes.length === 1) {\n    routeTree = (routeTree as any).routes[0];\n  }\n\n  // While the overlay is open, disable body scroll.\n  useEffect(() => {\n    if (isMenuOpen) {\n      const preferredScrollParent = scrollParentRef.current!;\n      disableBodyScroll(preferredScrollParent);\n      return () => enableBodyScroll(preferredScrollParent);\n    } else {\n      return undefined;\n    }\n  }, [isMenuOpen]);\n\n  // Close the overlay on any navigation.\n  useEffect(() => {\n    setIsMenuOpen(false);\n  }, [asPath]);\n\n  // Also close the overlay if the window gets resized past mobile layout.\n  // (This is also important because we don't want to keep the body locked!)\n  useEffect(() => {\n    const media = window.matchMedia(`(max-width: 1023px)`);\n\n    function closeIfNeeded() {\n      if (!media.matches) {\n        setIsMenuOpen(false);\n      }\n    }\n\n    closeIfNeeded();\n    media.addEventListener('change', closeIfNeeded);\n    return () => {\n      media.removeEventListener('change', closeIfNeeded);\n    };\n  }, []);\n\n  const scrollDetectorRef = useRef(null);\n  useEffect(() => {\n    const observer = new IntersectionObserver(\n      (entries) => {\n        entries.forEach((entry) => {\n          setIsScrolled(!entry.isIntersecting);\n        });\n      },\n      {\n        root: null,\n        rootMargin: `0px 0px`,\n        threshold: 0,\n      }\n    );\n    observer.observe(scrollDetectorRef.current!);\n    return () => observer.disconnect();\n  }, []);\n\n  const onOpenSearch = useCallback(() => {\n    startTransition(() => {\n      setShowSearch(true);\n    });\n  }, []);\n  const onCloseSearch = useCallback(() => {\n    setShowSearch(false);\n  }, []);\n\n  return (\n    <>\n      <Search\n        isOpen={showSearch}\n        onOpen={onOpenSearch}\n        onClose={onCloseSearch}\n      />\n      <div ref={scrollDetectorRef} />\n      <div\n        className={cn(\n          isMenuOpen\n            ? 'h-screen sticky top-0 lg:bottom-0 lg:h-screen flex flex-col shadow-nav dark:shadow-nav-dark z-20'\n            : 'z-40 sticky top-0'\n        )}>\n        <nav\n          className={cn(\n            'duration-300 backdrop-filter backdrop-blur-lg backdrop-saturate-200 transition-shadow bg-opacity-90 items-center w-full flex justify-between bg-wash dark:bg-wash-dark dark:bg-opacity-95 px-1.5 lg:pe-5 lg:ps-4 z-40',\n            {'dark:shadow-nav-dark shadow-nav': isScrolled || isMenuOpen}\n          )}>\n          <div className=\"flex items-center justify-between w-full h-16 gap-0 sm:gap-3\">\n            <div className=\"flex flex-row 3xl:flex-1 items-centers\">\n              <button\n                type=\"button\"\n                aria-label=\"Menu\"\n                onClick={() => setIsMenuOpen(!isMenuOpen)}\n                className={cn(\n                  'active:scale-95 transition-transform flex lg:hidden w-12 h-12 rounded-full items-center justify-center hover:bg-primary/5 hover:dark:bg-primary-dark/5 outline-link',\n                  {\n                    'text-link dark:text-link-dark': isMenuOpen,\n                  }\n                )}>\n                {isMenuOpen ? <IconClose /> : <IconHamburger />}\n              </button>\n              <BrandMenu>\n                <div className=\"flex items-center\">\n                  <div className=\"uwu-visible flex items-center justify-center h-full\">\n                    <NextLink\n                      href=\"/\"\n                      className=\"active:scale-95 transition-transform\">\n                      <Image\n                        alt=\"logo by @sawaratsuki1004\"\n                        title=\"logo by @sawaratsuki1004\"\n                        className=\"h-8\"\n                        priority\n                        width={63}\n                        height={32}\n                        src=\"/images/uwu.png\"\n                      />\n                    </NextLink>\n                  </div>\n                  <div className=\"uwu-hidden\">\n                    <NextLink\n                      href=\"/\"\n                      className={`active:scale-95 overflow-hidden transition-transform relative items-center text-primary dark:text-primary-dark p-1 whitespace-nowrap outline-link rounded-full 3xl:rounded-xl inline-flex text-lg font-normal gap-2`}>\n                      <Logo\n                        className={cn(\n                          'text-sm me-0 w-10 h-10 text-brand dark:text-brand-dark flex origin-center transition-all ease-in-out'\n                        )}\n                      />\n                      <span className=\"sr-only 3xl:not-sr-only\">React</span>\n                    </NextLink>\n                  </div>\n                </div>\n              </BrandMenu>\n              <div className=\"flex flex-column justify-center items-center\">\n                <NextLink\n                  href=\"/versions\"\n                  className=\" flex py-2 flex-column justify-center items-center text-gray-50 dark:text-gray-30 hover:text-link hover:dark:text-link-dark hover:underline text-sm ms-1 cursor-pointer\">\n                  v{siteConfig.version}\n                </NextLink>\n              </div>\n            </div>\n            <div className=\"items-center justify-center flex-1 hidden w-full md:flex 3xl:w-auto 3xl:shrink-0 3xl:justify-center\">\n              <button\n                type=\"button\"\n                className={cn(\n                  'flex 3xl:w-[56rem] 3xl:mx-0 relative ps-4 pe-1 py-1 h-10 bg-gray-30/20 dark:bg-gray-40/20 outline-none focus:outline-link betterhover:hover:bg-opacity-80 pointer items-center text-start w-full text-gray-30 rounded-full align-middle text-base'\n                )}\n                onClick={onOpenSearch}>\n                <IconSearch className=\"align-middle me-3 text-gray-30 shrink-0 group-betterhover:hover:text-gray-70\" />\n                검색\n                <span className=\"hidden ms-auto sm:flex item-center me-1\">\n                  <Kbd data-platform=\"mac\">⌘</Kbd>\n                  <Kbd data-platform=\"win\" wide>\n                    Ctrl\n                  </Kbd>\n                  <Kbd>K</Kbd>\n                </span>\n              </button>\n            </div>\n            <div className=\"text-base justify-center items-center gap-1.5 flex 3xl:flex-1 flex-row 3xl:justify-end\">\n              <div className=\"mx-2.5 gap-1.5 hidden lg:flex\">\n                <NavItem isActive={section === 'learn'} url=\"/learn\">\n                  학습하기\n                </NavItem>\n                <NavItem\n                  isActive={section === 'reference'}\n                  url=\"/reference/react\">\n                  API 참고서\n                </NavItem>\n                <NavItem isActive={section === 'community'} url=\"/community\">\n                  커뮤니티\n                </NavItem>\n                <NavItem isActive={section === 'blog'} url=\"/blog\">\n                  블로그\n                </NavItem>\n              </div>\n              <div className=\"flex w-full md:hidden\"></div>\n              <div className=\"flex items-center -space-x-2.5 xs:space-x-0 \">\n                <div className=\"flex md:hidden\">\n                  <button\n                    aria-label=\"Search\"\n                    type=\"button\"\n                    className=\"flex items-center justify-center w-12 h-12 transition-transform rounded-full active:scale-95 md:hidden hover:bg-secondary-button hover:dark:bg-secondary-button-dark outline-link\"\n                    onClick={onOpenSearch}>\n                    <IconSearch className=\"w-5 h-5 align-middle\" />\n                  </button>\n                </div>\n                <div className=\"flex dark:hidden\">\n                  <button\n                    type=\"button\"\n                    aria-label=\"Use Dark Mode\"\n                    onClick={() => {\n                      window.__setPreferredTheme('dark');\n                    }}\n                    className=\"flex items-center justify-center w-12 h-12 transition-transform rounded-full active:scale-95 hover:bg-primary/5 hover:dark:bg-primary-dark/5 outline-link\">\n                    {darkIcon}\n                  </button>\n                </div>\n                <div className=\"hidden dark:flex\">\n                  <button\n                    type=\"button\"\n                    aria-label=\"Use Light Mode\"\n                    onClick={() => {\n                      window.__setPreferredTheme('light');\n                    }}\n                    className=\"flex items-center justify-center w-12 h-12 transition-transform rounded-full active:scale-95 hover:bg-primary/5 hover:dark:bg-primary-dark/5 outline-link\">\n                    {lightIcon}\n                  </button>\n                </div>\n                <div className=\"flex\">\n                  <Link\n                    href=\"/community/translations\"\n                    aria-label=\"Translations\"\n                    className=\"active:scale-95 transition-transform flex w-12 h-12 rounded-full items-center justify-center hover:bg-primary/5 hover:dark:bg-primary-dark/5 outline-link\">\n                    {languageIcon}\n                  </Link>\n                </div>\n                <div className=\"flex\">\n                  <Link\n                    href=\"https://github.com/facebook/react/releases\"\n                    target=\"_blank\"\n                    rel=\"noreferrer noopener\"\n                    aria-label=\"Open on GitHub\"\n                    className=\"flex items-center justify-center w-12 h-12 transition-transform rounded-full active:scale-95 hover:bg-primary/5 hover:dark:bg-primary-dark/5 outline-link\">\n                    {githubIcon}\n                  </Link>\n                </div>\n              </div>\n            </div>\n          </div>\n        </nav>\n\n        {isMenuOpen && (\n          <div\n            ref={scrollParentRef}\n            className=\"overflow-y-scroll isolate no-bg-scrollbar lg:w-[342px] grow bg-wash dark:bg-wash-dark\">\n            <aside\n              className={cn(\n                `lg:grow lg:flex flex-col w-full pb-8 lg:pb-0 lg:max-w-custom-xs z-40`,\n                isMenuOpen ? 'block z-30' : 'hidden lg:block'\n              )}>\n              <nav\n                role=\"navigation\"\n                style={{'--bg-opacity': '.2'} as React.CSSProperties} // Need to cast here because CSS vars aren't considered valid in TS types (cuz they could be anything)\n                className=\"w-full pt-4 scrolling-touch lg:h-auto grow pe-0 lg:pe-5 lg:py-6 md:pt-4 lg:pt-4 scrolling-gpu\">\n                {/* No fallback UI so need to be careful not to suspend directly inside. */}\n                <Suspense fallback={null}>\n                  <div className=\"ps-3 xs:ps-5 xs:gap-0.5 xs:text-base overflow-x-auto flex flex-row lg:hidden text-base font-bold text-secondary dark:text-secondary-dark\">\n                    <NavItem isActive={section === 'learn'} url=\"/learn\">\n                      학습하기\n                    </NavItem>\n                    <NavItem\n                      isActive={section === 'reference'}\n                      url=\"/reference/react\">\n                      API 참고서\n                    </NavItem>\n                    <NavItem\n                      isActive={section === 'community'}\n                      url=\"/community\">\n                      커뮤니티\n                    </NavItem>\n                    <NavItem isActive={section === 'blog'} url=\"/blog\">\n                      블로그\n                    </NavItem>\n                  </div>\n                  <div\n                    role=\"separator\"\n                    className=\"mt-4 mb-2 border-b ms-5 border-border dark:border-border-dark\"\n                  />\n                  <SidebarRouteTree\n                    // Don't share state between the desktop and mobile versions.\n                    // This avoids unnecessary animations and visual flicker.\n                    key={isMenuOpen ? 'mobile-overlay' : 'desktop-or-hidden'}\n                    routeTree={routeTree}\n                    breadcrumbs={breadcrumbs}\n                    isForceExpanded={isMenuOpen}\n                  />\n                </Suspense>\n                <div className=\"h-16\" />\n              </nav>\n            </aside>\n          </div>\n        )}\n      </div>\n    </>\n  );\n}\n"
  },
  {
    "path": "src/components/Layout/TopNav/index.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nexport {default as TopNav} from './TopNav';\n"
  },
  {
    "path": "src/components/Layout/getRouteMeta.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\n/**\n * While Next.js provides file-based routing, we still need to construct\n * a sidebar for navigation and provide each markdown page\n * previous/next links and titles. To do this, we construct a nested\n * route object that is infinitely nestable.\n */\n\nexport type RouteTag =\n  | 'foundation'\n  | 'intermediate'\n  | 'advanced'\n  | 'experimental'\n  | 'deprecated';\n\nexport interface RouteItem {\n  /** Page title (for the sidebar) */\n  title: string;\n  /** Optional version flag for heading */\n  version?: 'canary' | 'major';\n  /** Optional page description for heading */\n  description?: string;\n  /* Additional meta info for page tagging */\n  tags?: RouteTag[];\n  /** Path to page */\n  path?: string;\n  /** Whether the entry is a heading */\n  heading?: boolean;\n  /** List of sub-routes */\n  routes?: RouteItem[];\n  /** Adds a section header above the route item */\n  hasSectionHeader?: boolean;\n  /** Title of section header */\n  sectionHeader?: string;\n  /** Whether it should be omitted in breadcrumbs */\n  skipBreadcrumb?: boolean;\n}\n\nexport interface Routes {\n  /** List of routes */\n  routes: RouteItem[];\n}\n\n/** Routing metadata about a given route and it's siblings and parent */\nexport interface RouteMeta {\n  /** The previous route */\n  prevRoute?: RouteItem;\n  /** The next route */\n  nextRoute?: RouteItem;\n  /** The current route */\n  route?: RouteItem;\n  /** Trail of parent routes */\n  breadcrumbs?: RouteItem[];\n  /** Order in the section */\n  order?: number;\n}\n\ntype TraversalContext = RouteMeta & {\n  currentIndex: number;\n};\n\nexport function getRouteMeta(cleanedPath: string, routeTree: RouteItem) {\n  const breadcrumbs = getBreadcrumbs(cleanedPath, routeTree);\n  const ctx: TraversalContext = {\n    currentIndex: 0,\n  };\n  buildRouteMeta(cleanedPath, routeTree, ctx);\n  const {currentIndex: _, ...meta} = ctx;\n  return {\n    ...meta,\n    breadcrumbs: breadcrumbs.length > 0 ? breadcrumbs : [routeTree],\n  };\n}\n\n// Performs a depth-first search to find the current route and its previous/next route\nfunction buildRouteMeta(\n  searchPath: string,\n  currentRoute: RouteItem,\n  ctx: TraversalContext\n) {\n  ctx.currentIndex++;\n\n  const {routes} = currentRoute;\n\n  if (ctx.route && !ctx.nextRoute) {\n    ctx.nextRoute = currentRoute;\n  }\n\n  if (currentRoute.path === searchPath) {\n    ctx.route = currentRoute;\n    ctx.order = ctx.currentIndex;\n    // If we've found a deeper match, reset the previously stored next route.\n    // TODO: this only works reliably if deeper matches are first in the tree.\n    // We should revamp all of this to be more explicit.\n    ctx.nextRoute = undefined;\n  }\n\n  if (!ctx.route) {\n    ctx.prevRoute = currentRoute;\n  }\n\n  if (!routes) {\n    return;\n  }\n\n  for (const route of routes) {\n    buildRouteMeta(searchPath, route, ctx);\n  }\n}\n\n// iterates the route tree from the current route to find its ancestors for breadcrumbs\nfunction getBreadcrumbs(\n  path: string,\n  currentRoute: RouteItem,\n  breadcrumbs: RouteItem[] = []\n): RouteItem[] {\n  if (currentRoute.path === path) {\n    return breadcrumbs;\n  }\n\n  if (!currentRoute.routes) {\n    return [];\n  }\n\n  for (const route of currentRoute.routes) {\n    const childRoute = getBreadcrumbs(path, route, [\n      ...breadcrumbs,\n      currentRoute,\n    ]);\n    if (childRoute?.length) {\n      return childRoute;\n    }\n  }\n\n  return [];\n}\n"
  },
  {
    "path": "src/components/Layout/useTocHighlight.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport {useState, useRef, useEffect} from 'react';\n\nconst TOP_OFFSET = 85;\n\nexport function getHeaderAnchors(): HTMLAnchorElement[] {\n  return Array.prototype.filter.call(\n    document.getElementsByClassName('mdx-header-anchor'),\n    function (testElement) {\n      return (\n        testElement.parentNode.nodeName === 'H1' ||\n        testElement.parentNode.nodeName === 'H2' ||\n        testElement.parentNode.nodeName === 'H3'\n      );\n    }\n  );\n}\n\n/**\n * Sets up Table of Contents highlighting.\n */\nexport function useTocHighlight() {\n  const [currentIndex, setCurrentIndex] = useState<number>(0);\n  const timeoutRef = useRef<number | null>(null);\n\n  useEffect(() => {\n    function updateActiveLink() {\n      const pageHeight = document.body.scrollHeight;\n      const scrollPosition = window.scrollY + window.innerHeight;\n      const headersAnchors = getHeaderAnchors();\n\n      if (scrollPosition >= 0 && pageHeight - scrollPosition <= 0) {\n        // Scrolled to bottom of page.\n        setCurrentIndex(headersAnchors.length - 1);\n        return;\n      }\n\n      let index = -1;\n      while (index < headersAnchors.length - 1) {\n        const headerAnchor = headersAnchors[index + 1];\n        const {top} = headerAnchor.getBoundingClientRect();\n\n        if (top >= TOP_OFFSET) {\n          break;\n        }\n        index += 1;\n      }\n\n      setCurrentIndex(Math.max(index, 0));\n    }\n\n    function throttledUpdateActiveLink() {\n      if (timeoutRef.current === null) {\n        timeoutRef.current = window.setTimeout(() => {\n          timeoutRef.current = null;\n          updateActiveLink();\n        }, 100);\n      }\n    }\n\n    document.addEventListener('scroll', throttledUpdateActiveLink);\n    document.addEventListener('resize', throttledUpdateActiveLink);\n\n    updateActiveLink();\n\n    return () => {\n      if (timeoutRef.current != null) {\n        clearTimeout(timeoutRef.current);\n        timeoutRef.current = null;\n      }\n      document.removeEventListener('scroll', throttledUpdateActiveLink);\n      document.removeEventListener('resize', throttledUpdateActiveLink);\n    };\n  }, []);\n\n  return {\n    currentIndex,\n  };\n}\n"
  },
  {
    "path": "src/components/Logo.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\nimport type {SVGProps} from 'react';\n\nexport function Logo(props: SVGProps<SVGSVGElement>) {\n  return (\n    <svg\n      width=\"100%\"\n      height=\"100%\"\n      viewBox=\"-10.5 -9.45 21 18.9\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      {...props}>\n      <circle cx=\"0\" cy=\"0\" r=\"2\" fill=\"currentColor\" />\n      <g stroke=\"currentColor\" strokeWidth=\"1\" fill=\"none\">\n        <ellipse rx=\"10\" ry=\"4.5\" />\n        <ellipse rx=\"10\" ry=\"4.5\" transform=\"rotate(60)\" />\n        <ellipse rx=\"10\" ry=\"4.5\" transform=\"rotate(120)\" />\n      </g>\n    </svg>\n  );\n}\n"
  },
  {
    "path": "src/components/MDX/BlogCard.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport * as React from 'react';\nimport Link from 'next/link';\n\nexport interface BlogCardProps {\n  title?: string;\n  badge?: boolean;\n  icon?: string;\n  date?: string;\n  url?: string;\n  children?: React.ReactNode;\n}\n\nfunction BlogCard({title, badge, date, icon, url, children}: BlogCardProps) {\n  return (\n    <Link\n      href={url as string}\n      passHref\n      className=\"block h-full w-full rounded-2xl outline-none focus:outline-none focus-visible:outline focus-visible:outline-link focus:outline-offset-2 focus-visible:dark:focus:outline-link-dark\">\n      <div className=\"justify-between p-5 sm:p-5 cursor-pointer w-full h-full flex flex-col flex-1 shadow-secondary-button-stroke dark:shadow-secondary-button-stroke-dark hover:bg-gray-40/5 active:bg-gray-40/10  hover:dark:bg-gray-60/5 active:dark:bg-gray-60/10 rounded-2xl text-xl text-primary dark:text-primary-dark leading-relaxed\">\n        <div className=\"flex flex-row gap-3 w-full\">\n          <h2 className=\"font-semibold flex-1 text-2xl lg:text-3xl hover:underline leading-snug mb-4\">\n            {title}\n          </h2>\n        </div>\n        <div>\n          <div className=\"flex flex-row justify-start gap-2 items-center text-base text-tertiary dark:text-tertiary-dark\">\n            {icon === 'labs' && (\n              <svg\n                className=\"w-6 h-6 text-tertiary dark:text-tertiary-dark\"\n                viewBox=\"0 0 72 72\"\n                fill=\"none\"\n                xmlns=\"http://www.w3.org/2000/svg\">\n                <path\n                  fillRule=\"evenodd\"\n                  clipRule=\"evenodd\"\n                  d=\"M27.4865 9C25.8297 9 24.4865 10.3431 24.4865 12C24.4865 13.6569 25.8297 15 27.4865 15V31.1087C27.4865 32.3397 27.1078 33.5409 26.4019 34.5494L13.095 53.5592C10.3114 57.5359 13.1563 63 18.0104 63H54.9626C59.8167 63 62.6616 57.5359 59.878 53.5592L46.5711 34.5494C45.8652 33.5409 45.4865 32.3397 45.4865 31.1087V15C47.1434 15 48.4865 13.6569 48.4865 12C48.4865 10.3431 47.1434 9 45.4865 9H27.4865ZM39.4865 31.1087V15H33.4865V31.1087C33.4865 33.5707 32.7292 35.9732 31.3173 37.9902L28.5104 42H44.4626L41.6557 37.9902C40.2438 35.9732 39.4865 33.5707 39.4865 31.1087ZM18.0104 57L24.3104 48H48.6626L54.9626 57H18.0104Z\"\n                  fill=\"currentColor\"\n                />\n              </svg>\n            )}\n            {icon === 'blog' && (\n              <svg\n                className=\"w-6 h-6 text-tertiary dark:text-tertiary-dark\"\n                viewBox=\"0 0 72 72\"\n                fill=\"none\"\n                xmlns=\"http://www.w3.org/2000/svg\">\n                <path\n                  fillRule=\"evenodd\"\n                  clipRule=\"evenodd\"\n                  d=\"M12.7101 56.3758C13.0724 56.7251 13.6324 57 14.3887 57H57.6113C58.3676 57 58.9276 56.7251 59.2899 56.3758C59.6438 56.0346 59.8987 55.5407 59.9086 54.864C59.9354 53.022 59.9591 50.7633 59.9756 48H12.0244C12.0409 50.7633 12.0645 53.022 12.0914 54.864C12.1013 55.5407 12.3562 56.0346 12.7101 56.3758ZM12.0024 42H59.9976C59.9992 41.0437 60 40.0444 60 39C60 29.5762 59.9327 22.5857 59.8589 17.7547C59.8359 16.2516 58.6168 15 56.9938 15L15.0062 15C13.3832 15 12.1641 16.2516 12.1411 17.7547C12.0673 22.5857 12 29.5762 12 39C12 40.0444 12.0008 41.0437 12.0024 42ZM65.8582 17.6631C65.7843 12.8227 61.8348 9 56.9938 9H15.0062C10.1652 9 6.21572 12.8227 6.1418 17.6631C6.06753 22.5266 6 29.5477 6 39C6 46.2639 6.03988 51.3741 6.09205 54.9515C6.15893 59.537 9.80278 63 14.3887 63H57.6113C62.1972 63 65.8411 59.537 65.9079 54.9515C65.9601 51.3741 66 46.2639 66 39C66 29.5477 65.9325 22.5266 65.8582 17.6631ZM39 21C37.3431 21 36 22.3431 36 24C36 25.6569 37.3431 27 39 27H51C52.6569 27 54 25.6569 54 24C54 22.3431 52.6569 21 51 21H39ZM36 33C36 31.3431 37.3431 30 39 30H51C52.6569 30 54 31.3431 54 33C54 34.6569 52.6569 36 51 36H39C37.3431 36 36 34.6569 36 33ZM24 33C27.3137 33 30 30.3137 30 27C30 23.6863 27.3137 21 24 21C20.6863 21 18 23.6863 18 27C18 30.3137 20.6863 33 24 33Z\"\n                  fill=\"currentColor\"\n                />\n              </svg>\n            )}\n            {date}\n            {badge ? (\n              <div className=\"h-fit px-1 bg-highlight dark:bg-highlight-dark rounded uppercase text-link dark:text-link-dark font-bold tracking-wide text-xs whitespace-nowrap\">\n                New\n              </div>\n            ) : null}\n          </div>\n          <span className=\"text-base text-secondary dark:text-secondary-dark\">\n            {children}\n          </span>\n          {children != null && (\n            <div className=\"text-link text-base dark:text-link-dark hover:underline mt-4\">\n              더 보기\n            </div>\n          )}\n        </div>\n      </div>\n    </Link>\n  );\n}\n\nexport default BlogCard;\n"
  },
  {
    "path": "src/components/MDX/Challenges/Challenge.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport {useState} from 'react';\nimport cn from 'classnames';\nimport {Button} from 'components/Button';\nimport {ChallengeContents} from './Challenges';\nimport {IconHint} from '../../Icon/IconHint';\nimport {IconSolution} from '../../Icon/IconSolution';\nimport {IconArrowSmall} from '../../Icon/IconArrowSmall';\nimport {H4} from '../Heading';\n\ninterface ChallengeProps {\n  isRecipes?: boolean;\n  totalChallenges: number;\n  currentChallenge: ChallengeContents;\n  hasNextChallenge: boolean;\n  handleClickNextChallenge: () => void;\n}\n\nexport function Challenge({\n  isRecipes,\n  totalChallenges,\n  currentChallenge,\n  hasNextChallenge,\n  handleClickNextChallenge,\n}: ChallengeProps) {\n  const [showHint, setShowHint] = useState(false);\n  const [showSolution, setShowSolution] = useState(false);\n\n  const toggleHint = () => {\n    if (showSolution && !showHint) {\n      setShowSolution(false);\n    }\n    setShowHint((hint) => !hint);\n  };\n\n  const toggleSolution = () => {\n    if (showHint && !showSolution) {\n      setShowHint(false);\n    }\n    setShowSolution((solution) => !solution);\n  };\n\n  return (\n    <div className=\"p-5 sm:py-8 sm:px-8\">\n      <div>\n        <H4\n          className=\"text-xl text-primary dark:text-primary-dark mb-2 mt-0 font-medium\"\n          id={currentChallenge.id}>\n          <div className=\"font-bold block md:inline\">\n            {isRecipes ? '예시' : '챌린지'} {currentChallenge.order} of{' '}\n            {totalChallenges}\n            <span className=\"text-primary dark:text-primary-dark\">: </span>\n          </div>\n          {currentChallenge.name}\n        </H4>\n        {currentChallenge.content}\n      </div>\n      <div className=\"flex justify-between items-center mt-4\">\n        {currentChallenge.hint ? (\n          <div>\n            <Button className=\"mr-2\" onClick={toggleHint} active={showHint}>\n              <IconHint className=\"me-1.5\" />{' '}\n              {showHint ? '힌트 숨기기' : '힌트 보기'}\n            </Button>\n            <Button\n              className=\"me-2\"\n              onClick={toggleSolution}\n              active={showSolution}>\n              <IconSolution className=\"me-1.5\" />{' '}\n              {showSolution ? '정답 숨기기' : '정답 보기'}\n            </Button>\n          </div>\n        ) : (\n          !isRecipes && (\n            <Button\n              className=\"me-2\"\n              onClick={toggleSolution}\n              active={showSolution}>\n              <IconSolution className=\"me-1.5\" />{' '}\n              {showSolution ? '정답 숨기기' : '정답 보기'}\n            </Button>\n          )\n        )}\n\n        {hasNextChallenge && (\n          <Button\n            className={cn(\n              isRecipes\n                ? 'bg-purple-50 border-purple-50 hover:bg-purple-50 focus:bg-purple-50 active:bg-purple-50'\n                : 'bg-link dark:bg-link-dark'\n            )}\n            onClick={handleClickNextChallenge}\n            active>\n            다음 {isRecipes ? '예시' : '챌린지'}\n            <IconArrowSmall displayDirection=\"end\" className=\"block ms-1.5\" />\n          </Button>\n        )}\n      </div>\n      {showHint && currentChallenge.hint}\n\n      {showSolution && (\n        <div className=\"mt-6\">\n          <h3 className=\"text-2xl font-bold text-primary dark:text-primary-dark\">\n            해설\n          </h3>\n          {currentChallenge.solution}\n          <div className=\"flex justify-between items-center mt-4\">\n            <Button onClick={() => setShowSolution(false)}>정답 닫기</Button>\n            {hasNextChallenge && (\n              <Button\n                className={cn(\n                  isRecipes ? 'bg-purple-50' : 'bg-link dark:bg-link-dark'\n                )}\n                onClick={handleClickNextChallenge}\n                active>\n                다음 챌린지\n                <IconArrowSmall\n                  displayDirection=\"end\"\n                  className=\"block ms-1.5\"\n                />\n              </Button>\n            )}\n          </div>\n        </div>\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/components/MDX/Challenges/Challenges.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport {Children, useRef, useEffect, useState} from 'react';\nimport * as React from 'react';\nimport cn from 'classnames';\nimport {H2} from 'components/MDX/Heading';\nimport {H4} from 'components/MDX/Heading';\nimport {Challenge} from './Challenge';\nimport {Navigation} from './Navigation';\nimport {useRouter} from 'next/router';\n\ninterface ChallengesProps {\n  children: React.ReactElement[];\n  isRecipes?: boolean;\n  titleText?: string;\n  titleId?: string;\n  noTitle?: boolean;\n}\n\nexport interface ChallengeContents {\n  id: string;\n  name: string;\n  order: number;\n  content: React.ReactNode;\n  solution: React.ReactNode;\n  hint?: React.ReactNode;\n}\n\nconst parseChallengeContents = (\n  children: React.ReactElement[]\n): ChallengeContents[] => {\n  const contents: ChallengeContents[] = [];\n\n  if (!children) {\n    return contents;\n  }\n\n  let challenge: Partial<ChallengeContents> = {};\n  let content: React.ReactElement[] = [];\n  Children.forEach(children, (child) => {\n    const {props, type} = child as React.ReactElement<{\n      children?: string;\n      id?: string;\n    }>;\n    switch ((type as any).mdxName) {\n      case 'Solution': {\n        challenge.solution = child;\n        challenge.content = content;\n        contents.push(challenge as ChallengeContents);\n        challenge = {};\n        content = [];\n        break;\n      }\n      case 'Hint': {\n        challenge.hint = child;\n        break;\n      }\n      case 'h4': {\n        challenge.order = contents.length + 1;\n        challenge.name = props.children;\n        challenge.id = props.id;\n        break;\n      }\n      default: {\n        content.push(child);\n      }\n    }\n  });\n\n  return contents;\n};\n\nenum QueuedScroll {\n  INIT = 'init',\n  NEXT = 'next',\n}\n\nexport function Challenges({\n  children,\n  isRecipes,\n  noTitle,\n  titleText = isRecipes ? '예시 살펴보기' : '챌린지 도전하기',\n  titleId = isRecipes ? 'examples' : 'challenges',\n}: ChallengesProps) {\n  const challenges = parseChallengeContents(children);\n  const totalChallenges = challenges.length;\n  const scrollAnchorRef = useRef<HTMLDivElement>(null);\n  const queuedScrollRef = useRef<undefined | QueuedScroll>(QueuedScroll.INIT);\n  const [activeIndex, setActiveIndex] = useState(0);\n  const currentChallenge = challenges[activeIndex];\n  const {asPath} = useRouter();\n\n  useEffect(() => {\n    if (queuedScrollRef.current === QueuedScroll.INIT) {\n      const initIndex = challenges.findIndex(\n        (challenge) => challenge.id === asPath.split('#')[1]\n      );\n      if (initIndex === -1) {\n        queuedScrollRef.current = undefined;\n      } else if (initIndex !== activeIndex) {\n        setActiveIndex(initIndex);\n      }\n    }\n    if (queuedScrollRef.current) {\n      scrollAnchorRef.current!.scrollIntoView({\n        block: 'start',\n        ...(queuedScrollRef.current === QueuedScroll.NEXT && {\n          behavior: 'smooth',\n        }),\n      });\n      queuedScrollRef.current = undefined;\n    }\n  }, [activeIndex, asPath, challenges]);\n\n  const handleChallengeChange = (index: number) => {\n    setActiveIndex(index);\n  };\n\n  const Heading = isRecipes ? H4 : H2;\n  return (\n    <div className=\"max-w-7xl mx-auto py-4 w-full\">\n      <div\n        className={cn(\n          'border-gray-10 bg-card dark:bg-card-dark shadow-inner rounded-none -mx-5 sm:mx-auto sm:rounded-2xl'\n        )}>\n        <div ref={scrollAnchorRef} className=\"py-2 px-5 sm:px-8 pb-0 md:pb-0\">\n          {!noTitle && (\n            <Heading\n              id={titleId}\n              className={cn(\n                'mb-2 leading-10 relative',\n                isRecipes\n                  ? 'text-xl text-purple-50 dark:text-purple-30'\n                  : 'text-3xl text-link'\n              )}>\n              {titleText}\n            </Heading>\n          )}\n          {totalChallenges > 1 && (\n            <Navigation\n              currentChallenge={currentChallenge}\n              challenges={challenges}\n              handleChange={handleChallengeChange}\n              isRecipes={isRecipes}\n            />\n          )}\n        </div>\n        <Challenge\n          key={currentChallenge.id}\n          isRecipes={isRecipes}\n          currentChallenge={currentChallenge}\n          totalChallenges={totalChallenges}\n          hasNextChallenge={activeIndex < totalChallenges - 1}\n          handleClickNextChallenge={() => {\n            setActiveIndex((i) => i + 1);\n            queuedScrollRef.current = QueuedScroll.NEXT;\n          }}\n        />\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/components/MDX/Challenges/Navigation.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport {useRef, useCallback, useEffect, createRef} from 'react';\nimport cn from 'classnames';\nimport {IconChevron} from 'components/Icon/IconChevron';\nimport {ChallengeContents} from './Challenges';\nimport {debounce} from 'debounce';\n\nexport function Navigation({\n  challenges,\n  handleChange,\n  currentChallenge,\n  isRecipes,\n}: {\n  challenges: ChallengeContents[];\n  handleChange: (index: number) => void;\n  currentChallenge: ChallengeContents;\n  isRecipes?: boolean;\n}) {\n  const containerRef = useRef<HTMLDivElement>(null);\n  const challengesNavRef = useRef(\n    challenges.map(() => createRef<HTMLButtonElement>())\n  );\n  const scrollPos = currentChallenge.order - 1;\n  const canScrollLeft = scrollPos > 0;\n  const canScrollRight = scrollPos < challenges.length - 1;\n\n  const handleScrollRight = () => {\n    if (scrollPos < challenges.length - 1) {\n      const currentNavRef = challengesNavRef.current[scrollPos + 1].current;\n      if (!currentNavRef) {\n        return;\n      }\n      if (containerRef.current) {\n        containerRef.current.scrollLeft = currentNavRef.offsetLeft;\n      }\n      handleChange(scrollPos + 1);\n    }\n  };\n\n  const handleScrollLeft = () => {\n    if (scrollPos > 0) {\n      const currentNavRef = challengesNavRef.current[scrollPos - 1].current;\n      if (!currentNavRef) {\n        return;\n      }\n      if (containerRef.current) {\n        containerRef.current.scrollLeft = currentNavRef.offsetLeft;\n      }\n      handleChange(scrollPos - 1);\n    }\n  };\n\n  const handleSelectNav = (index: number) => {\n    const currentNavRef = challengesNavRef.current[index].current;\n    if (containerRef.current) {\n      containerRef.current.scrollLeft = currentNavRef?.offsetLeft || 0;\n    }\n    handleChange(index);\n  };\n\n  const handleResize = useCallback(() => {\n    if (containerRef.current) {\n      const el = containerRef.current;\n      el.scrollLeft =\n        challengesNavRef.current[scrollPos].current?.offsetLeft || 0;\n    }\n  }, [containerRef, challengesNavRef, scrollPos]);\n\n  useEffect(() => {\n    handleResize();\n    const debouncedHandleResize = debounce(handleResize, 200);\n    window.addEventListener('resize', debouncedHandleResize);\n    return () => {\n      window.removeEventListener('resize', debouncedHandleResize);\n    };\n  }, [handleResize]);\n\n  return (\n    <div className=\"flex items-center justify-between\">\n      <div className=\"overflow-hidden\">\n        <div\n          ref={containerRef}\n          className=\"flex relative transition-transform content-box overflow-x-auto\">\n          {challenges.map(({name, id, order}, index) => (\n            <button\n              className={cn(\n                'py-2 me-4 text-base border-b-4 duration-100 ease-in transition whitespace-nowrap text-ellipsis',\n                isRecipes &&\n                  currentChallenge.id === id &&\n                  'text-purple-50 border-purple-50 hover:text-purple-50 dark:text-purple-30 dark:border-purple-30 dark:hover:text-purple-30',\n                !isRecipes &&\n                  currentChallenge.id === id &&\n                  'text-link border-link hover:text-link dark:text-link-dark dark:border-link-dark dark:hover:text-link-dark'\n              )}\n              onClick={() => handleSelectNav(index)}\n              key={`button-${id}`}\n              ref={challengesNavRef.current[index]}>\n              {order}. {name}\n            </button>\n          ))}\n        </div>\n      </div>\n      <div className=\"flex z-10 pb-2 ps-2\">\n        <button\n          onClick={handleScrollLeft}\n          aria-label=\"Scroll left\"\n          className={cn(\n            'bg-secondary-button dark:bg-secondary-button-dark h-8 px-2 rounded-l rtl:rounded-r rtl:rounded-l-none border-gray-20 border-r rtl:border-l rtl:border-r-0',\n            {\n              'text-primary dark:text-primary-dark': canScrollLeft,\n              'text-gray-30': !canScrollLeft,\n            }\n          )}>\n          <IconChevron displayDirection=\"start\" />\n        </button>\n        <button\n          onClick={handleScrollRight}\n          aria-label=\"Scroll right\"\n          className={cn(\n            'bg-secondary-button dark:bg-secondary-button-dark h-8 px-2 rounded-e',\n            {\n              'text-primary dark:text-primary-dark': canScrollRight,\n              'text-gray-30': !canScrollRight,\n            }\n          )}>\n          <IconChevron displayDirection=\"end\" />\n        </button>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/components/MDX/Challenges/index.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport * as React from 'react';\nexport {Challenges} from './Challenges';\n\nexport function Hint({children}: {children: React.ReactNode}) {\n  return <div>{children}</div>;\n}\n\nexport function Solution({children}: {children: React.ReactNode}) {\n  return <div>{children}</div>;\n}\n"
  },
  {
    "path": "src/components/MDX/CodeBlock/CodeBlock.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport cn from 'classnames';\nimport {HighlightStyle} from '@codemirror/language';\nimport {highlightTree} from '@lezer/highlight';\nimport {javascript} from '@codemirror/lang-javascript';\nimport {html} from '@codemirror/lang-html';\nimport {css} from '@codemirror/lang-css';\nimport rangeParser from 'parse-numeric-range';\nimport {tags} from '@lezer/highlight';\n\nimport {CustomTheme} from '../Sandpack/Themes';\n\ninterface InlineHighlight {\n  step: number;\n  line: number;\n  startColumn: number;\n  endColumn: number;\n}\n\nconst jsxLang = javascript({jsx: true, typescript: false});\nconst cssLang = css();\nconst htmlLang = html();\n\nconst CodeBlock = function CodeBlock({\n  children: {\n    props: {className = 'language-js', children: code = '', meta},\n  },\n  noMargin,\n  noShadow,\n  onLineHover,\n}: {\n  children: React.ReactNode & {\n    props: {\n      className: string;\n      children?: string;\n      meta?: string;\n    };\n  };\n  className?: string;\n  noMargin?: boolean;\n  noShadow?: boolean;\n  onLineHover?: (lineNumber: number | null) => void;\n}) {\n  code = code.trimEnd();\n  let lang = jsxLang;\n  if (className === 'language-css') {\n    lang = cssLang;\n  } else if (className === 'language-html') {\n    lang = htmlLang;\n  }\n  const tree = lang.language.parser.parse(code);\n  let tokenStarts = new Map();\n  let tokenEnds = new Map();\n  const highlightTheme = getSyntaxHighlight(CustomTheme);\n  highlightTree(tree, highlightTheme, (from, to, className) => {\n    tokenStarts.set(from, className);\n    tokenEnds.set(to, className);\n  });\n\n  const highlightedLines = new Map();\n  const lines = code.split('\\n');\n  const lineDecorators = getLineDecorators(code, meta);\n  for (let decorator of lineDecorators) {\n    highlightedLines.set(decorator.line - 1, decorator.className);\n  }\n\n  const inlineDecorators = getInlineDecorators(code, meta);\n  const decoratorStarts = new Map();\n  const decoratorEnds = new Map();\n  for (let decorator of inlineDecorators) {\n    // Find where inline highlight starts and ends.\n    let decoratorStart = 0;\n    for (let i = 0; i < decorator.line - 1; i++) {\n      decoratorStart += lines[i].length + 1;\n    }\n    decoratorStart += decorator.startColumn;\n    const decoratorEnd =\n      decoratorStart + (decorator.endColumn - decorator.startColumn);\n    if (decoratorStarts.has(decoratorStart)) {\n      throw Error('Already opened decorator at ' + decoratorStart);\n    }\n    decoratorStarts.set(decoratorStart, decorator.className);\n    if (decoratorEnds.has(decoratorEnd)) {\n      throw Error('Already closed decorator at ' + decoratorEnd);\n    }\n    decoratorEnds.set(decoratorEnd, decorator.className);\n  }\n\n  // Produce output based on tokens and decorators.\n  // We assume tokens never overlap other tokens, and\n  // decorators never overlap with other decorators.\n  // However, tokens and decorators may mutually overlap.\n  // In that case, decorators always take precedence.\n  let currentDecorator = null;\n  let currentToken = null;\n  let buffer = '';\n  let lineIndex = 0;\n  let lineOutput = [];\n  let finalOutput = [];\n  for (let i = 0; i < code.length; i++) {\n    if (tokenEnds.has(i)) {\n      if (!currentToken) {\n        throw Error('Cannot close token at ' + i + ' because it was not open.');\n      }\n      if (!currentDecorator) {\n        lineOutput.push(\n          <span key={i + '/t'} className={currentToken}>\n            {buffer}\n          </span>\n        );\n        buffer = '';\n      }\n      currentToken = null;\n    }\n    if (decoratorEnds.has(i)) {\n      if (!currentDecorator) {\n        throw Error(\n          'Cannot close decorator at ' + i + ' because it was not open.'\n        );\n      }\n      lineOutput.push(\n        <span key={i + '/d'} className={currentDecorator}>\n          {buffer}\n        </span>\n      );\n      buffer = '';\n      currentDecorator = null;\n    }\n    if (decoratorStarts.has(i)) {\n      if (currentDecorator) {\n        throw Error(\n          'Cannot open decorator at ' + i + ' before closing last one.'\n        );\n      }\n      if (currentToken) {\n        lineOutput.push(\n          <span key={i + 'd'} className={currentToken}>\n            {buffer}\n          </span>\n        );\n        buffer = '';\n      } else {\n        lineOutput.push(buffer);\n        buffer = '';\n      }\n      currentDecorator = decoratorStarts.get(i);\n    }\n    if (tokenStarts.has(i)) {\n      if (currentToken) {\n        throw Error('Cannot open token at ' + i + ' before closing last one.');\n      }\n      currentToken = tokenStarts.get(i);\n      if (!currentDecorator) {\n        lineOutput.push(buffer);\n        buffer = '';\n      }\n    }\n    if (code[i] === '\\n') {\n      lineOutput.push(buffer);\n      buffer = '';\n      const currentLineIndex = lineIndex;\n      finalOutput.push(\n        <div\n          key={lineIndex}\n          className={'cm-line ' + (highlightedLines.get(lineIndex) ?? '')}\n          onMouseEnter={\n            onLineHover ? () => onLineHover(currentLineIndex) : undefined\n          }>\n          {lineOutput}\n          <br />\n        </div>\n      );\n      lineOutput = [];\n      lineIndex++;\n    } else {\n      buffer += code[i];\n    }\n  }\n  if (currentDecorator) {\n    lineOutput.push(\n      <span key={'end/d'} className={currentDecorator}>\n        {buffer}\n      </span>\n    );\n  } else if (currentToken) {\n    lineOutput.push(\n      <span key={'end/t'} className={currentToken}>\n        {buffer}\n      </span>\n    );\n  } else {\n    lineOutput.push(buffer);\n  }\n  finalOutput.push(\n    <div\n      key={lineIndex}\n      className={'cm-line ' + (highlightedLines.get(lineIndex) ?? '')}\n      onMouseEnter={onLineHover ? () => onLineHover(lineIndex) : undefined}>\n      {lineOutput}\n    </div>\n  );\n\n  return (\n    <div\n      dir=\"ltr\"\n      className={cn(\n        'sandpack sandpack--codeblock',\n        'rounded-2xl h-full w-full overflow-x-auto flex items-center bg-wash dark:bg-gray-95 shadow-lg',\n        !noMargin && 'my-8',\n        noShadow &&\n          'shadow-none rounded-2xl overflow-hidden w-full flex bg-transparent'\n      )}\n      style={{contain: 'content'}}>\n      <div className=\"sp-wrapper\">\n        <div className=\"sp-stack\">\n          <div className=\"sp-code-editor\">\n            <pre className=\"sp-cm sp-pristine sp-javascript flex align-start\">\n              <code\n                className=\"sp-pre-placeholder grow-[2]\"\n                onMouseLeave={\n                  onLineHover ? () => onLineHover(null) : undefined\n                }>\n                {finalOutput}\n              </code>\n            </pre>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n};\n\nexport default CodeBlock;\n\nfunction classNameToken(name: string): string {\n  return `sp-syntax-${name}`;\n}\n\nfunction getSyntaxHighlight(theme: any): HighlightStyle {\n  return HighlightStyle.define([\n    {tag: tags.link, textdecorator: 'underline'},\n    {tag: tags.emphasis, fontStyle: 'italic'},\n    {tag: tags.strong, fontWeight: 'bold'},\n\n    {\n      tag: tags.keyword,\n      class: classNameToken('keyword'),\n    },\n    {\n      tag: [tags.atom, tags.number, tags.bool],\n      class: classNameToken('static'),\n    },\n    {\n      tag: tags.standard(tags.tagName),\n      class: classNameToken('tag'),\n    },\n    {tag: tags.variableName, class: classNameToken('plain')},\n    {\n      // Highlight function call\n      tag: tags.function(tags.variableName),\n      class: classNameToken('definition'),\n    },\n    {\n      // Highlight function definition differently (eg: functional component def in React)\n      tag: [tags.definition(tags.function(tags.variableName)), tags.tagName],\n      class: classNameToken('definition'),\n    },\n    {\n      tag: tags.propertyName,\n      class: classNameToken('property'),\n    },\n    {\n      tag: [tags.literal, tags.inserted],\n      class: classNameToken(theme.syntax.string ? 'string' : 'static'),\n    },\n    {\n      tag: tags.punctuation,\n      class: classNameToken('punctuation'),\n    },\n    {\n      tag: [tags.comment, tags.quote],\n      class: classNameToken('comment'),\n    },\n  ]);\n}\n\nfunction getLineDecorators(\n  code: string,\n  meta?: string\n): Array<{\n  line: number;\n  className: string;\n}> {\n  if (!meta) {\n    return [];\n  }\n  const linesToHighlight = getHighlightLines(meta);\n  const highlightedLineConfig = linesToHighlight.map((line) => {\n    return {\n      className: 'bg-github-highlight dark:bg-opacity-10',\n      line,\n    };\n  });\n  return highlightedLineConfig;\n}\n\nfunction getInlineDecorators(\n  code: string,\n  meta?: string\n): Array<{\n  step: number;\n  line: number;\n  startColumn: number;\n  endColumn: number;\n  className: string;\n}> {\n  if (!meta) {\n    return [];\n  }\n  const inlineHighlightLines = getInlineHighlights(meta, code);\n  const inlineHighlightConfig = inlineHighlightLines.map(\n    (line: InlineHighlight) => ({\n      ...line,\n      elementAttributes: {'data-step': `${line.step}`},\n      className: cn(\n        'code-step bg-opacity-10 dark:bg-opacity-20 relative rounded px-1 py-[1.5px] border-b-[2px] border-opacity-60',\n        {\n          'bg-blue-40 border-blue-40 text-blue-60 dark:text-blue-30':\n            line.step === 1,\n          'bg-yellow-40 border-yellow-40 text-yellow-60 dark:text-yellow-30':\n            line.step === 2,\n          'bg-purple-40 border-purple-40 text-purple-60 dark:text-purple-30':\n            line.step === 3,\n          'bg-green-40 border-green-40 text-green-60 dark:text-green-30':\n            line.step === 4,\n          // TODO: Some codeblocks use up to 6 steps.\n        }\n      ),\n    })\n  );\n  return inlineHighlightConfig;\n}\n\n/**\n *\n * @param meta string provided after the language in a markdown block\n * @returns array of lines to highlight\n * @example\n * ```js {1-3,7} [[1, 1, 20, 33], [2, 4, 4, 8]] App.js active\n * ...\n * ```\n *\n * -> The meta is `{1-3,7} [[1, 1, 20, 33], [2, 4, 4, 8]] App.js active`\n */\nfunction getHighlightLines(meta: string): number[] {\n  const HIGHLIGHT_REGEX = /{([\\d,-]+)}/;\n  const parsedMeta = HIGHLIGHT_REGEX.exec(meta);\n  if (!parsedMeta) {\n    return [];\n  }\n  return rangeParser(parsedMeta[1]);\n}\n\n/**\n *\n * @param meta string provided after the language in a markdown block\n * @returns InlineHighlight[]\n * @example\n * ```js {1-3,7} [[1, 1, 'count'], [2, 4, 'setCount']] App.js active\n * ...\n * ```\n *\n * -> The meta is `{1-3,7} [[1, 1, 'count', [2, 4, 'setCount']] App.js active`\n */\nfunction getInlineHighlights(meta: string, code: string) {\n  const INLINE_HEIGHT_REGEX = /(\\[\\[.*\\]\\])/;\n  const parsedMeta = INLINE_HEIGHT_REGEX.exec(meta);\n  if (!parsedMeta) {\n    return [];\n  }\n\n  const lines = code.split('\\n');\n  const encodedHighlights = JSON.parse(parsedMeta[1]);\n  return encodedHighlights.map(([step, lineNo, substr, fromIndex]: any[]) => {\n    const line = lines[lineNo - 1];\n    let index = line.indexOf(substr);\n    const lastIndex = line.lastIndexOf(substr);\n    if (index !== lastIndex) {\n      if (fromIndex === undefined) {\n        throw Error(\n          \"Found '\" +\n            substr +\n            \"' twice. Specify fromIndex as the fourth value in the tuple.\"\n        );\n      }\n      index = line.indexOf(substr, fromIndex);\n    }\n    if (index === -1) {\n      throw Error(\"Could not find: '\" + substr + \"'\");\n    }\n    return {\n      step,\n      line: lineNo,\n      startColumn: index,\n      endColumn: index + substr.length,\n    };\n  });\n}\n"
  },
  {
    "path": "src/components/MDX/CodeBlock/index.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport * as React from 'react';\nimport cn from 'classnames';\nimport {lazy, memo, Suspense} from 'react';\nconst CodeBlock = lazy(() => import('./CodeBlock'));\n\nexport default memo(function CodeBlockWrapper(props: {\n  children: React.ReactNode & {\n    props: {\n      className: string;\n      children: string;\n      meta?: string;\n    };\n  };\n  isFromPackageImport: boolean;\n  noMargin?: boolean;\n  noMarkers?: boolean;\n}): any {\n  const {children, isFromPackageImport} = props;\n  return (\n    <Suspense\n      fallback={\n        <pre\n          translate=\"no\"\n          dir=\"ltr\"\n          className={cn(\n            'rounded-lg leading-6 h-full w-full overflow-x-auto flex items-center bg-wash dark:bg-gray-95 shadow-lg text-[13.6px] overflow-hidden',\n            !isFromPackageImport && 'my-8'\n          )}>\n          <div className=\"py-[18px] ps-5 font-normal \">\n            <p className=\"sp-pre-placeholder overflow-hidden\">{children}</p>\n          </div>\n        </pre>\n      }>\n      <CodeBlock {...props} />\n    </Suspense>\n  );\n});\n"
  },
  {
    "path": "src/components/MDX/CodeDiagram.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport {Children} from 'react';\nimport * as React from 'react';\nimport CodeBlock from './CodeBlock';\n\ninterface CodeDiagramProps {\n  children: React.ReactNode;\n  flip?: boolean;\n}\n\nexport function CodeDiagram({children, flip = false}: CodeDiagramProps) {\n  const illustration = Children.toArray(children).filter((child: any) => {\n    return child.type === 'img';\n  });\n  const content = Children.toArray(children).map((child: any) => {\n    if (child.type?.mdxName === 'pre') {\n      return (\n        <CodeBlock\n          key={child.key}\n          {...child.props}\n          noMargin={true}\n          noMarkers={true}\n        />\n      );\n    } else if (child.type === 'img') {\n      return null;\n    } else {\n      return child;\n    }\n  });\n  if (flip) {\n    return (\n      <section className=\"my-8 grid grid-cols-1 lg:grid-cols-2 gap-x-8 gap-y-4\">\n        {illustration}\n        <div className=\"flex flex-col justify-center\">{content}</div>\n      </section>\n    );\n  }\n  return (\n    <section className=\"my-8 grid grid-cols-1 lg:grid-cols-2 gap-x-8 gap-y-4\">\n      <div className=\"flex flex-col justify-center\">{content}</div>\n      <div className=\"py-4\">{illustration}</div>\n    </section>\n  );\n}\n"
  },
  {
    "path": "src/components/MDX/ConsoleBlock.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport {isValidElement} from 'react';\nimport * as React from 'react';\nimport cn from 'classnames';\nimport {IconWarning} from '../Icon/IconWarning';\nimport {IconError} from '../Icon/IconError';\n\ntype LogLevel = 'warning' | 'error' | 'info';\n\ninterface ConsoleBlockProps {\n  level?: LogLevel;\n  children: React.ReactNode;\n}\n\ninterface ConsoleBlockMultiProps {\n  children: React.ReactNode;\n}\n\nconst Box = ({\n  width = '60px',\n  height = '17px',\n  className,\n  customStyles,\n}: {\n  width?: string;\n  height?: string;\n  className?: string;\n  customStyles?: Record<string, string>;\n}) => (\n  <div className={className} style={{width, height, ...customStyles}}></div>\n);\n\nexport function ConsoleBlock({level = 'error', children}: ConsoleBlockProps) {\n  let message: React.ReactNode | null;\n  if (typeof children === 'string') {\n    message = children;\n  } else if (isValidElement(children)) {\n    message = (children as React.ReactElement<{children?: React.ReactNode}>)\n      .props.children;\n  }\n\n  return (\n    <div\n      className=\"console-block mb-4 text-secondary bg-wash dark:bg-wash-dark rounded-lg\"\n      translate=\"no\"\n      dir=\"ltr\">\n      <div className=\"flex w-full rounded-t-lg bg-gray-200 dark:bg-gray-80\">\n        <div className=\"px-4 py-2 border-gray-300 dark:border-gray-90 border-r\">\n          <Box className=\"bg-gray-300 dark:bg-gray-70\" width=\"15px\" />\n        </div>\n        <div className=\"flex text-sm px-4\">\n          <div className=\"border-b-2 border-gray-300 dark:border-gray-90 text-tertiary dark:text-tertiary-dark\">\n            Console\n          </div>\n          <div className=\"px-4 py-2 flex\">\n            <Box className=\"me-2 bg-gray-300 dark:bg-gray-70\" />\n            <Box className=\"me-2 hidden md:block bg-gray-300 dark:bg-gray-70\" />\n            <Box className=\"hidden md:block bg-gray-300 dark:bg-gray-70\" />\n          </div>\n        </div>\n      </div>\n      <div\n        className={cn(\n          'flex px-4 pt-4 pb-6 items-center content-center font-mono text-code rounded-b-md',\n          {\n            'bg-red-30 text-red-50 dark:text-red-30 bg-opacity-5':\n              level === 'error',\n            'bg-yellow-5 text-yellow-50': level === 'warning',\n            'bg-gray-5 text-secondary dark:text-secondary-dark':\n              level === 'info',\n          }\n        )}>\n        {level === 'error' && <IconError className=\"self-start mt-1.5\" />}\n        {level === 'warning' && <IconWarning className=\"self-start mt-1\" />}\n        <div className=\"px-3\">{message}</div>\n      </div>\n    </div>\n  );\n}\n\nexport function ConsoleBlockMulti({children}: ConsoleBlockMultiProps) {\n  return (\n    <div\n      className=\"console-block mb-4 text-secondary bg-wash dark:bg-wash-dark rounded-lg\"\n      translate=\"no\"\n      dir=\"ltr\">\n      <div className=\"flex w-full rounded-t-lg bg-gray-200 dark:bg-gray-80\">\n        <div className=\"px-4 py-2 border-gray-300 dark:border-gray-90 border-r\">\n          <Box className=\"bg-gray-300 dark:bg-gray-70\" width=\"15px\" />\n        </div>\n        <div className=\"flex text-sm px-4\">\n          <div className=\"border-b-2 border-gray-300 dark:border-gray-90 text-tertiary dark:text-tertiary-dark\">\n            Console\n          </div>\n          <div className=\"px-4 py-2 flex\">\n            <Box className=\"me-2 bg-gray-300 dark:bg-gray-70\" />\n            <Box className=\"me-2 hidden md:block bg-gray-300 dark:bg-gray-70\" />\n            <Box className=\"hidden md:block bg-gray-300 dark:bg-gray-70\" />\n          </div>\n        </div>\n      </div>\n      <div className=\"grid grid-cols-1 divide-y divide-gray-300 dark:divide-gray-70 text-base\">\n        {children}\n      </div>\n    </div>\n  );\n}\n\nexport function ConsoleLogLine({children, level}: ConsoleBlockProps) {\n  let message: React.ReactNode | null;\n  if (typeof children === 'string') {\n    message = children;\n  } else if (isValidElement(children)) {\n    message = (children as React.ReactElement<{children?: React.ReactNode}>)\n      .props.children;\n  } else if (Array.isArray(children)) {\n    message = children.reduce((result, child) => {\n      if (typeof child === 'string') {\n        result += child;\n      } else if (isValidElement(child)) {\n        // @ts-ignore\n        result += child.props.children;\n      }\n      return result;\n    }, '');\n  }\n\n  return (\n    <div\n      className={cn(\n        'ps-4 pe-2 pt-1 pb-2 grid grid-cols-[18px_auto] font-mono rounded-b-md',\n        {\n          'bg-red-30 text-red-50 dark:text-red-30 bg-opacity-5':\n            level === 'error',\n          'bg-yellow-5 text-yellow-50': level === 'warning',\n          'bg-gray-5 text-secondary dark:text-secondary-dark': level === 'info',\n        }\n      )}>\n      {level === 'error' && (\n        <IconError className=\"self-start mt-1.5 text-[.7rem] w-6\" />\n      )}\n      {level === 'warning' && (\n        <IconWarning className=\"self-start mt-1 text-[.65rem] w-6\" />\n      )}\n      <div className=\"px-2 pt-1 whitespace-break-spaces text-code leading-tight\">\n        {message}\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/components/MDX/Diagram.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport Image from 'next/image';\n\ninterface DiagramProps {\n  name: string;\n  alt: string;\n  height: number;\n  width: number;\n  children: string;\n  captionPosition: 'top' | 'bottom' | null;\n}\n\nfunction Caption({text}: {text: string}) {\n  return (\n    <div className=\"w-full flex justify-center\">\n      <figcaption className=\"p-1 sm:p-2 mt-0 sm:mt-0 text-gray-40 text-base lg:text-lg text-center leading-tight table-caption max-w-lg\">\n        {text}\n      </figcaption>\n    </div>\n  );\n}\n\nexport function Diagram({\n  name,\n  alt,\n  height,\n  width,\n  children,\n  captionPosition,\n}: DiagramProps) {\n  return (\n    <figure className=\"flex flex-col px-0 p-0 sm:p-10 first:mt-0 mt-10 sm:mt-0 justify-center items-center\">\n      {captionPosition === 'top' && <Caption text={children} />}\n      <div className=\"dark-image\">\n        <Image\n          src={`/images/docs/diagrams/${name}.dark.png`}\n          alt={alt}\n          height={height}\n          width={width}\n        />\n      </div>\n      <div className=\"light-image\">\n        <Image\n          src={`/images/docs/diagrams/${name}.png`}\n          alt={alt}\n          height={height}\n          width={width}\n        />\n      </div>\n      {(!captionPosition || captionPosition === 'bottom') && (\n        <Caption text={children} />\n      )}\n    </figure>\n  );\n}\n\nexport default Diagram;\n"
  },
  {
    "path": "src/components/MDX/DiagramGroup.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport {ReactNode} from 'react';\n\ninterface DiagramGroupProps {\n  children: ReactNode;\n}\n\nexport function DiagramGroup({children}: DiagramGroupProps) {\n  return (\n    <div className=\"flex flex-col sm:flex-row py-2 sm:p-0 sm:space-y-0 justify-center items-start sm:items-center w-full\">\n      {children}\n    </div>\n  );\n}\n\nexport default DiagramGroup;\n"
  },
  {
    "path": "src/components/MDX/ErrorDecoder.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nimport {useEffect, useState} from 'react';\nimport {useErrorDecoderParams} from '../ErrorDecoderContext';\nimport cn from 'classnames';\n\nfunction replaceArgs(\n  msg: string,\n  argList: Array<string | undefined>,\n  replacer = '[missing argument]'\n): string {\n  let argIdx = 0;\n  return msg.replace(/%s/g, function () {\n    const arg = argList[argIdx++];\n    // arg can be an empty string: ?args[0]=&args[1]=count\n    return arg === undefined ? replacer : arg;\n  });\n}\n\n/**\n * Sindre Sorhus <https://sindresorhus.com>\n * Released under MIT license\n * https://github.com/sindresorhus/linkify-urls/blob/b2397096df152e2f799011f7a48e5f73b4bf1c7e/index.js#L5C1-L7C1\n *\n * The regex is used to extract URL from the string for linkify.\n */\nconst urlRegex = () =>\n  /((?:https?(?::\\/\\/))(?:www\\.)?(?:[a-zA-Z\\d-_.]+(?:(?:\\.|@)[a-zA-Z\\d]{2,})|localhost)(?:(?:[-a-zA-Z\\d:%_+.~#!?&//=@]*)(?:[,](?![\\s]))*)*)/g;\n\n// When the message contains a URL (like https://fb.me/react-refs-must-have-owner),\n// make it a clickable link.\nfunction urlify(str: string): React.ReactNode[] {\n  const segments = str.split(urlRegex());\n\n  return segments.map((message, i) => {\n    if (i % 2 === 1) {\n      return (\n        <a\n          key={i}\n          target=\"_blank\"\n          className=\"underline\"\n          rel=\"noopener noreferrer\"\n          href={message}>\n          {message}\n        </a>\n      );\n    }\n    return message;\n  });\n}\n\n// `?args[]=foo&args[]=bar`\n// or `// ?args[0]=foo&args[1]=bar`\nfunction parseQueryString(search: string): Array<string | undefined> {\n  const rawQueryString = search.substring(1);\n  if (!rawQueryString) {\n    return [];\n  }\n\n  const args: Array<string | undefined> = [];\n\n  const queries = rawQueryString.split('&');\n  for (let i = 0; i < queries.length; i++) {\n    const query = decodeURIComponent(queries[i]);\n    if (query.startsWith('args[')) {\n      args.push(query.slice(query.indexOf(']=') + 2));\n    }\n  }\n\n  return args;\n}\n\nexport default function ErrorDecoder() {\n  const {errorMessage, errorCode} = useErrorDecoderParams();\n  /** error messages that contain %s require reading location.search */\n  const hasParams = errorMessage?.includes('%s');\n  const [message, setMessage] = useState<React.ReactNode | null>(() =>\n    errorMessage ? urlify(errorMessage) : null\n  );\n\n  const [isReady, setIsReady] = useState(errorMessage == null || !hasParams);\n\n  useEffect(() => {\n    if (errorMessage == null || !hasParams) {\n      return;\n    }\n    const args = parseQueryString(window.location.search);\n    let message = errorMessage;\n    if (errorCode === '418') {\n      // Hydration errors have a %s for the diff, but we don't add that to the args for security reasons.\n      message = message.replace(/%s$/, '');\n\n      // Before React 19.1, the error message didn't have an arg, and was always HTML.\n      if (args.length === 0) {\n        args.push('HTML');\n      } else if (args.length === 1 && args[0] === '') {\n        args[0] = 'HTML';\n      }\n    }\n\n    setMessage(urlify(replaceArgs(message, args, '[missing argument]')));\n    setIsReady(true);\n  }, [errorCode, hasParams, errorMessage]);\n\n  return (\n    <code\n      className={cn(\n        'whitespace-pre-line block bg-red-100 text-red-600 py-4 px-6 mt-5 rounded-lg',\n        isReady ? 'opacity-100' : 'opacity-0'\n      )}>\n      <b>{message}</b>\n    </code>\n  );\n}\n"
  },
  {
    "path": "src/components/MDX/ExpandableCallout.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport * as React from 'react';\nimport cn from 'classnames';\nimport {IconNote} from '../Icon/IconNote';\nimport {IconWarning} from '../Icon/IconWarning';\nimport {IconPitfall} from '../Icon/IconPitfall';\nimport {IconCanary} from '../Icon/IconCanary';\nimport {IconRocket} from '../Icon/IconRocket';\n\ntype CalloutVariants =\n  | 'deprecated'\n  | 'pitfall'\n  | 'note'\n  | 'wip'\n  | 'canary'\n  | 'experimental'\n  | 'rc'\n  | 'major'\n  | 'rsc';\n\ninterface ExpandableCalloutProps {\n  children: React.ReactNode;\n  type: CalloutVariants;\n}\n\nconst variantMap = {\n  deprecated: {\n    title: '더 이상 사용되지 않습니다!',\n    Icon: IconWarning,\n    containerClasses: 'bg-red-5 dark:bg-red-60 dark:bg-opacity-20',\n    textColor: 'text-red-50 dark:text-red-40',\n    overlayGradient:\n      'linear-gradient(rgba(249, 247, 243, 0), rgba(249, 247, 243, 1)',\n  },\n  note: {\n    title: '중요합니다!',\n    Icon: IconNote,\n    containerClasses:\n      'bg-green-5 dark:bg-green-60 dark:bg-opacity-20 text-primary dark:text-primary-dark text-lg',\n    textColor: 'text-green-60 dark:text-green-40',\n    overlayGradient:\n      'linear-gradient(rgba(245, 249, 248, 0), rgba(245, 249, 248, 1)',\n  },\n  rc: {\n    title: 'RC',\n    Icon: IconCanary,\n    containerClasses:\n      'bg-gray-5 dark:bg-gray-60 dark:bg-opacity-20 text-primary dark:text-primary-dark text-lg',\n    textColor: 'text-gray-60 dark:text-gray-30',\n    overlayGradient:\n      'linear-gradient(rgba(245, 249, 248, 0), rgba(245, 249, 248, 1)',\n  },\n  canary: {\n    title: 'Canary',\n    Icon: IconCanary,\n    containerClasses:\n      'bg-gray-5 dark:bg-gray-60 dark:bg-opacity-20 text-primary dark:text-primary-dark text-lg',\n    textColor: 'text-gray-60 dark:text-gray-30',\n    overlayGradient:\n      'linear-gradient(rgba(245, 249, 248, 0), rgba(245, 249, 248, 1)',\n  },\n  experimental: {\n    title: '실험적 기능',\n    Icon: IconCanary,\n    containerClasses:\n      'bg-green-5 dark:bg-green-60 dark:bg-opacity-20 text-primary dark:text-primary-dark text-lg',\n    textColor: 'text-green-60 dark:text-green-40',\n    overlayGradient:\n      'linear-gradient(rgba(245, 249, 248, 0), rgba(245, 249, 248, 1)',\n  },\n  pitfall: {\n    title: '주의하세요!',\n    Icon: IconPitfall,\n    containerClasses: 'bg-yellow-5 dark:bg-yellow-60 dark:bg-opacity-20',\n    textColor: 'text-yellow-50 dark:text-yellow-40',\n    overlayGradient:\n      'linear-gradient(rgba(249, 247, 243, 0), rgba(249, 247, 243, 1)',\n  },\n  wip: {\n    title: '개발중이에요!',\n    Icon: IconNote,\n    containerClasses: 'bg-yellow-5 dark:bg-yellow-60 dark:bg-opacity-20',\n    textColor: 'text-yellow-50 dark:text-yellow-40',\n    overlayGradient:\n      'linear-gradient(rgba(249, 247, 243, 0), rgba(249, 247, 243, 1)',\n  },\n  major: {\n    title: 'React 19',\n    Icon: IconRocket,\n    containerClasses: 'bg-blue-10 dark:bg-blue-60 dark:bg-opacity-20',\n    textColor: 'text-blue-50 dark:text-blue-40',\n    overlayGradient:\n      'linear-gradient(rgba(249, 247, 243, 0), rgba(249, 247, 243, 1)',\n  },\n  rsc: {\n    title: 'React 서버 컴포넌트',\n    Icon: null,\n    containerClasses: 'bg-blue-10 dark:bg-blue-60 dark:bg-opacity-20',\n    textColor: 'text-blue-50 dark:text-blue-40',\n    overlayGradient:\n      'linear-gradient(rgba(249, 247, 243, 0), rgba(249, 247, 243, 1)',\n  },\n};\n\nfunction ExpandableCallout({children, type = 'note'}: ExpandableCalloutProps) {\n  const variant = variantMap[type];\n\n  return (\n    <div\n      className={cn(\n        'expandable-callout',\n        'pt-8 pb-4 px-5 sm:px-8 my-8 relative rounded-none shadow-inner-border -mx-5 sm:mx-auto sm:rounded-2xl',\n        variant.containerClasses\n      )}>\n      <h3 className={cn('text-2xl font-display font-bold', variant.textColor)}>\n        {variant.Icon && (\n          <variant.Icon\n            className={cn('inline me-2 mb-1 text-lg', variant.textColor)}\n          />\n        )}\n        {variant.title}\n      </h3>\n      <div className=\"relative\">\n        <div className=\"py-2\">{children}</div>\n      </div>\n    </div>\n  );\n}\n\nexport default ExpandableCallout;\n"
  },
  {
    "path": "src/components/MDX/ExpandableExample.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport * as React from 'react';\nimport cn from 'classnames';\nimport {IconChevron} from '../Icon/IconChevron';\nimport {IconDeepDive} from '../Icon/IconDeepDive';\nimport {IconCodeBlock} from '../Icon/IconCodeBlock';\nimport {Button} from '../Button';\nimport {H4} from './Heading';\nimport {useRouter} from 'next/router';\nimport {useEffect, useRef, useState} from 'react';\n\ninterface ExpandableExampleProps {\n  children: React.ReactNode;\n  excerpt?: string;\n  type: 'DeepDive' | 'Example';\n}\n\nfunction ExpandableExample({children, excerpt, type}: ExpandableExampleProps) {\n  if (!Array.isArray(children) || children[0].type.mdxName !== 'h4') {\n    throw Error(\n      `Expandable content ${type} is missing a corresponding title at the beginning`\n    );\n  }\n  const isDeepDive = type === 'DeepDive';\n  const isExample = type === 'Example';\n  const id = children[0].props.id;\n\n  const {asPath} = useRouter();\n  const shouldAutoExpand = id === asPath.split('#')[1];\n  const queuedExpandRef = useRef<boolean>(shouldAutoExpand);\n  const [isExpanded, setIsExpanded] = useState(false);\n\n  useEffect(() => {\n    if (queuedExpandRef.current) {\n      queuedExpandRef.current = false;\n      setIsExpanded(true);\n    }\n  }, []);\n\n  return (\n    <details\n      open={isExpanded}\n      onToggle={(e: any) => {\n        setIsExpanded(e.currentTarget!.open);\n      }}\n      className={cn(\n        'my-12 rounded-2xl shadow-inner-border dark:shadow-inner-border-dark relative',\n        {\n          'dark:bg-opacity-20 dark:bg-purple-60 bg-purple-5': isDeepDive,\n          'dark:bg-opacity-20 dark:bg-yellow-60 bg-yellow-5': isExample,\n        }\n      )}>\n      <summary\n        className=\"list-none p-8\"\n        tabIndex={-1 /* there's a button instead */}\n        onClick={(e) => {\n          // We toggle using a button instead of this whole area,\n          // with an escape case for the header anchor link\n          if (!(e.target instanceof SVGElement)) {\n            e.preventDefault();\n          }\n        }}>\n        <h5\n          className={cn('mb-4 uppercase font-bold flex items-center text-sm', {\n            'dark:text-purple-30 text-purple-50': isDeepDive,\n            'dark:text-yellow-30 text-yellow-60': isExample,\n          })}>\n          {isDeepDive && (\n            <>\n              <IconDeepDive className=\"inline me-2 dark:text-purple-30 text-purple-40\" />\n              자세히 살펴보기\n            </>\n          )}\n          {isExample && (\n            <>\n              <IconCodeBlock className=\"inline me-2 dark:text-yellow-30 text-yellow-50\" />\n              Example\n            </>\n          )}\n        </h5>\n        <div className=\"mb-4\">\n          <H4\n            id={id}\n            className=\"text-xl font-bold text-primary dark:text-primary-dark\">\n            {children[0].props.children}\n          </H4>\n          {excerpt && <div>{excerpt}</div>}\n        </div>\n        <Button\n          active={true}\n          className={cn({\n            'bg-purple-50 border-purple-50 hover:bg-purple-40 focus:bg-purple-50 active:bg-purple-50':\n              isDeepDive,\n            'bg-yellow-50 border-yellow-50 hover:bg-yellow-40 focus:bg-yellow-50 active:bg-yellow-50':\n              isExample,\n          })}\n          onClick={() => setIsExpanded((current) => !current)}>\n          <span className=\"me-1\">\n            <IconChevron displayDirection={isExpanded ? 'up' : 'down'} />\n          </span>\n          {isExpanded ? '숨기기' : '자세히 보기'}\n        </Button>\n      </summary>\n      <div\n        className={cn('p-8 border-t', {\n          'dark:border-purple-60 border-purple-10 ': isDeepDive,\n          'dark:border-yellow-60 border-yellow-50': isExample,\n        })}>\n        {children.slice(1)}\n      </div>\n    </details>\n  );\n}\n\nexport default ExpandableExample;\n"
  },
  {
    "path": "src/components/MDX/Heading.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport cn from 'classnames';\nimport * as React from 'react';\nimport {forwardRefWithAs} from 'utils/forwardRefWithAs';\nexport interface HeadingProps {\n  className?: string;\n  isPageAnchor?: boolean;\n  children: React.ReactNode;\n  id?: string;\n  as?: any;\n}\n\nconst Heading = forwardRefWithAs<HeadingProps, 'div'>(function Heading(\n  {as: Comp = 'div', className, children, id, isPageAnchor = true, ...props},\n  ref\n) {\n  let label = 'Link for this heading';\n  if (typeof children === 'string') {\n    label = 'Link for ' + children;\n  }\n\n  return (\n    <Comp id={id} {...props} ref={ref} className={cn('mdx-heading', className)}>\n      {children}\n      {isPageAnchor && (\n        <a\n          href={`#${id}`}\n          aria-label={label}\n          title={label}\n          className={cn(\n            'mdx-header-anchor',\n            Comp === 'h1' ? 'hidden' : 'inline-block'\n          )}>\n          <svg\n            width=\"1em\"\n            height=\"1em\"\n            viewBox=\"0 0 13 13\"\n            xmlns=\"http://www.w3.org/2000/svg\"\n            className=\"text-gray-70 ms-2 h-5 w-5\">\n            <g fill=\"currentColor\" fillRule=\"evenodd\">\n              <path d=\"M7.778 7.975a2.5 2.5 0 0 0 .347-3.837L6.017 2.03a2.498 2.498 0 0 0-3.542-.007 2.5 2.5 0 0 0 .006 3.543l1.153 1.15c.07-.29.154-.563.25-.773.036-.077.084-.16.14-.25L3.18 4.85a1.496 1.496 0 0 1 .002-2.12 1.496 1.496 0 0 1 2.12 0l2.124 2.123a1.496 1.496 0 0 1-.333 2.37c.16.246.42.504.685.752z\" />\n              <path d=\"M5.657 4.557a2.5 2.5 0 0 0-.347 3.837l2.108 2.108a2.498 2.498 0 0 0 3.542.007 2.5 2.5 0 0 0-.006-3.543L9.802 5.815c-.07.29-.154.565-.25.774-.036.076-.084.16-.14.25l.842.84c.585.587.59 1.532 0 2.122-.587.585-1.532.59-2.12 0L6.008 7.68a1.496 1.496 0 0 1 .332-2.372c-.16-.245-.42-.503-.685-.75z\" />\n            </g>\n          </svg>\n        </a>\n      )}\n    </Comp>\n  );\n});\n\nexport const H1 = ({className, ...props}: HeadingProps) => (\n  <Heading\n    as=\"h1\"\n    className={cn(className, 'text-5xl font-display font-bold leading-tight')}\n    {...props}\n  />\n);\n\nexport const H2 = ({className, ...props}: HeadingProps) => (\n  <Heading\n    as=\"h2\"\n    className={cn(\n      'text-3xl font-display leading-10 text-primary dark:text-primary-dark font-bold my-6',\n      className\n    )}\n    {...props}\n  />\n);\n\nexport const H3 = ({className, ...props}: HeadingProps) => (\n  <Heading\n    as=\"h3\"\n    className={cn(\n      className,\n      'text-2xl font-display leading-9 text-primary dark:text-primary-dark font-bold my-6'\n    )}\n    {...props}\n  />\n);\n\nexport const H4 = ({className, ...props}: HeadingProps) => (\n  <Heading\n    as=\"h4\"\n    className={cn(className, 'text-xl font-display font-bold leading-9 my-4')}\n    {...props}\n  />\n);\n\nexport const H5 = ({className, ...props}: HeadingProps) => (\n  <Heading\n    as=\"h5\"\n    className={cn(className, 'text-lg font-display font-bold leading-9 my-2')}\n    {...props}\n  />\n);\n"
  },
  {
    "path": "src/components/MDX/InlineCode.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport cn from 'classnames';\nimport type {HTMLAttributes} from 'react';\n\ninterface InlineCodeProps {\n  isLink?: boolean;\n  meta?: string;\n}\nfunction InlineCode({\n  isLink,\n  ...props\n}: HTMLAttributes<HTMLElement> & InlineCodeProps) {\n  return (\n    <code\n      dir=\"ltr\" // This is needed to prevent the code from inheriting the RTL direction of <html> in case of RTL languages to avoid like `()console.log` to be rendered as `console.log()`\n      className={cn(\n        'inline text-code text-secondary dark:text-secondary-dark px-1 rounded-md no-underline',\n        {\n          'bg-gray-30 bg-opacity-10 py-px': !isLink,\n          'bg-highlight dark:bg-highlight-dark py-0': isLink,\n        }\n      )}\n      {...props}\n    />\n  );\n}\n\nexport default InlineCode;\n"
  },
  {
    "path": "src/components/MDX/Intro.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport * as React from 'react';\n\nexport interface IntroProps {\n  children?: React.ReactNode;\n}\n\nfunction Intro({children}: IntroProps) {\n  return (\n    <div className=\"font-display text-xl text-primary dark:text-primary-dark leading-relaxed\">\n      {children}\n    </div>\n  );\n}\n\nexport default Intro;\n"
  },
  {
    "path": "src/components/MDX/LanguagesContext.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport {createContext} from 'react';\n\nexport type LanguageItem = {\n  code: string;\n  name: string;\n  enName: string;\n};\nexport type Languages = Array<LanguageItem>;\n\nexport const LanguagesContext = createContext<Languages | null>(null);\n"
  },
  {
    "path": "src/components/MDX/Link.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport {Children, cloneElement} from 'react';\nimport NextLink from 'next/link';\nimport cn from 'classnames';\n\nimport {ExternalLink} from 'components/ExternalLink';\n\nfunction Link({\n  href,\n  className,\n  children,\n  ...props\n}: React.AnchorHTMLAttributes<HTMLAnchorElement>) {\n  const classes =\n    'inline text-link dark:text-link-dark border-b border-link border-opacity-0 hover:border-opacity-100 duration-100 ease-in transition leading-normal';\n  const modifiedChildren = Children.toArray(children).map((child: any) => {\n    if (child.type?.mdxName && child.type?.mdxName === 'inlineCode') {\n      return cloneElement(child, {\n        isLink: true,\n      });\n    }\n    return child;\n  });\n\n  if (!href) {\n    // eslint-disable-next-line jsx-a11y/anchor-has-content\n    return <a href={href} className={className} {...props} />;\n  }\n  return (\n    <>\n      {href.startsWith('https://') ? (\n        <ExternalLink href={href} className={cn(classes, className)} {...props}>\n          {modifiedChildren}\n        </ExternalLink>\n      ) : href.startsWith('#') ? (\n        // eslint-disable-next-line jsx-a11y/anchor-has-content\n        <a className={cn(classes, className)} href={href} {...props}>\n          {modifiedChildren}\n        </a>\n      ) : (\n        <NextLink href={href} className={cn(classes, className)} {...props}>\n          {modifiedChildren}\n        </NextLink>\n      )}\n    </>\n  );\n}\n\nexport default Link;\n"
  },
  {
    "path": "src/components/MDX/MDXComponents.module.css",
    "content": "/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\n/* Stop purging. */\n.markdown p {\n  @apply mb-4 leading-7 whitespace-pre-wrap text-gray-70;\n}\n\n.markdown ol {\n  @apply mb-4 ms-8 list-decimal;\n}\n\n.markdown ul {\n  @apply mb-4 ms-8 list-disc;\n}\n\n.markdown h1 {\n  @apply mb-6 text-4xl font-extrabold tracking-tight;\n}\n\n.markdown h2 {\n  @apply mt-12 mb-4 text-3xl font-extrabold tracking-tight;\n}\n.markdown h3 {\n  @apply mt-8 mb-3 text-2xl font-extrabold tracking-tight;\n}\n.markdown h4 {\n  @apply mt-8 mb-3 text-xl font-extrabold tracking-tight;\n}\n\n.markdown code {\n  @apply text-gray-70 bg-card dark:bg-card-dark p-1 rounded-lg no-underline;\n  font-size: 90%;\n}\n\n.markdown li {\n  @apply mb-2;\n}\n\n.markdown a {\n  @apply inline text-link dark:text-link-dark break-normal border-b border-link border-opacity-0 hover:border-opacity-100 duration-100 ease-in transition leading-normal;\n}\n"
  },
  {
    "path": "src/components/MDX/MDXComponents.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport {Children, useContext, useMemo} from 'react';\nimport * as React from 'react';\nimport cn from 'classnames';\nimport type {HTMLAttributes} from 'react';\n\nimport CodeBlock from './CodeBlock';\nimport {CodeDiagram} from './CodeDiagram';\nimport {ConsoleBlock, ConsoleLogLine, ConsoleBlockMulti} from './ConsoleBlock';\nimport ExpandableCallout from './ExpandableCallout';\nimport ExpandableExample from './ExpandableExample';\nimport {H1, H2, H3, H4, H5} from './Heading';\nimport InlineCode from './InlineCode';\nimport Intro from './Intro';\nimport BlogCard from './BlogCard';\nimport Link from './Link';\nimport {PackageImport} from './PackageImport';\nimport Recap from './Recap';\nimport {SandpackClient as Sandpack, SandpackRSC} from './Sandpack';\nimport SandpackWithHTMLOutput from './SandpackWithHTMLOutput';\nimport Diagram from './Diagram';\nimport DiagramGroup from './DiagramGroup';\nimport SimpleCallout from './SimpleCallout';\nimport TerminalBlock from './TerminalBlock';\nimport YouWillLearnCard from './YouWillLearnCard';\nimport {Challenges, Hint, Solution} from './Challenges';\nimport {IconNavArrow} from '../Icon/IconNavArrow';\nimport ButtonLink from 'components/ButtonLink';\nimport {TocContext} from './TocContext';\nimport type {Toc, TocItem} from './TocContext';\nimport {TeamMember} from './TeamMember';\nimport {LanguagesContext} from './LanguagesContext';\nimport {finishedTranslations} from 'utils/finishedTranslations';\n\nimport ErrorDecoder from './ErrorDecoder';\nimport {IconCanary} from '../Icon/IconCanary';\nimport {IconExperimental} from 'components/Icon/IconExperimental';\n\nfunction CodeStep({children, step}: {children: any; step: number}) {\n  return (\n    <span\n      data-step={step}\n      className={cn(\n        'code-step bg-opacity-10 dark:bg-opacity-20 relative rounded px-[6px] py-[1.5px] border-b-[2px] border-opacity-60',\n        {\n          'bg-blue-40 border-blue-40 text-blue-60 dark:text-blue-30':\n            step === 1,\n          'bg-yellow-40 border-yellow-40 text-yellow-60 dark:text-yellow-30':\n            step === 2,\n          'bg-purple-40 border-purple-40 text-purple-60 dark:text-purple-30':\n            step === 3,\n          'bg-green-40 border-green-40 text-green-60 dark:text-green-30':\n            step === 4,\n        }\n      )}>\n      {children}\n    </span>\n  );\n}\n\nconst P = (p: HTMLAttributes<HTMLParagraphElement>) => (\n  <p className=\"whitespace-pre-wrap my-4\" {...p} />\n);\n\nconst Strong = (strong: HTMLAttributes<HTMLElement>) => (\n  <strong className=\"font-bold\" {...strong} />\n);\n\nconst OL = (p: HTMLAttributes<HTMLOListElement>) => (\n  <ol className=\"ms-6 my-3 list-decimal\" {...p} />\n);\nconst LI = (p: HTMLAttributes<HTMLLIElement>) => (\n  <li className=\"leading-relaxed mb-1\" {...p} />\n);\nconst UL = (p: HTMLAttributes<HTMLUListElement>) => (\n  <ul className=\"ms-6 my-3 list-disc\" {...p} />\n);\n\nconst Divider = () => (\n  <hr className=\"my-6 block border-b border-t-0 border-border dark:border-border-dark\" />\n);\nconst Wip = ({children}: {children: React.ReactNode}) => (\n  <ExpandableCallout type=\"wip\">{children}</ExpandableCallout>\n);\nconst Pitfall = ({children}: {children: React.ReactNode}) => (\n  <ExpandableCallout type=\"pitfall\">{children}</ExpandableCallout>\n);\nconst Deprecated = ({children}: {children: React.ReactNode}) => (\n  <ExpandableCallout type=\"deprecated\">{children}</ExpandableCallout>\n);\nconst Note = ({children}: {children: React.ReactNode}) => (\n  <ExpandableCallout type=\"note\">{children}</ExpandableCallout>\n);\n\nconst Canary = ({children}: {children: React.ReactNode}) => (\n  <ExpandableCallout type=\"canary\">{children}</ExpandableCallout>\n);\n\nconst RC = ({children}: {children: React.ReactNode}) => (\n  <ExpandableCallout type=\"rc\">{children}</ExpandableCallout>\n);\n\nconst Experimental = ({children}: {children: React.ReactNode}) => (\n  <ExpandableCallout type=\"experimental\">{children}</ExpandableCallout>\n);\n\nconst NextMajor = ({children}: {children: React.ReactNode}) => (\n  <ExpandableCallout type=\"major\">{children}</ExpandableCallout>\n);\n\nconst RSC = ({children}: {children: React.ReactNode}) => (\n  <ExpandableCallout type=\"rsc\">{children}</ExpandableCallout>\n);\n\nconst CanaryBadge = ({title}: {title: string}) => (\n  <span\n    title={title}\n    className={\n      'text-base font-display px-1 py-0.5 font-bold bg-gray-10 dark:bg-gray-60 text-gray-60 dark:text-gray-10 rounded'\n    }>\n    <IconCanary\n      size=\"s\"\n      className={'inline me-1 mb-0.5 text-sm text-gray-60 dark:text-gray-10'}\n    />\n    Canary only\n  </span>\n);\n\nconst ExperimentalBadge = ({title}: {title: string}) => (\n  <span\n    title={title}\n    className={\n      'text-base font-display px-1 py-0.5 font-bold bg-gray-10 dark:bg-gray-60 text-gray-60 dark:text-gray-10 rounded'\n    }>\n    <IconExperimental\n      size=\"s\"\n      className={'inline me-1 mb-0.5 text-sm text-gray-60 dark:text-gray-10'}\n    />\n    Experimental only\n  </span>\n);\n\nconst NextMajorBadge = ({title}: {title: string}) => (\n  <span\n    title={title}\n    className={\n      'text-base font-display px-2 py-0.5 font-bold bg-blue-10 dark:bg-blue-60 text-gray-60 dark:text-gray-10 rounded'\n    }>\n    React 19\n  </span>\n);\n\nconst RSCBadge = ({title}: {title: string}) => (\n  <span\n    title={title}\n    className={\n      'text-base font-display px-2 py-0.5 font-bold bg-blue-10 dark:bg-blue-50 text-gray-60 dark:text-gray-10 rounded'\n    }>\n    RSC\n  </span>\n);\n\nconst Blockquote = ({children, ...props}: HTMLAttributes<HTMLQuoteElement>) => {\n  return (\n    <blockquote\n      className=\"mdx-blockquote py-4 px-8 my-8 shadow-inner-border dark:shadow-inner-border-dark bg-highlight dark:bg-highlight-dark bg-opacity-50 rounded-2xl leading-6 flex relative\"\n      {...props}>\n      <span className=\"block relative\">{children}</span>\n    </blockquote>\n  );\n};\n\nfunction LearnMore({\n  children,\n  path,\n}: {\n  title: string;\n  path?: string;\n  children: any;\n}) {\n  return (\n    <>\n      <section className=\"p-8 mt-16 mb-16 flex flex-row shadow-inner-border dark:shadow-inner-border-dark justify-between items-center bg-card dark:bg-card-dark rounded-2xl\">\n        <div className=\"flex-col\">\n          <h2 className=\"text-primary font-display dark:text-primary-dark font-bold text-2xl leading-tight\">\n            이 주제를 배울 준비가 되셨나요?\n          </h2>\n          {children}\n          {path ? (\n            <ButtonLink\n              className=\"mt-1\"\n              label=\"Read More\"\n              href={path}\n              type=\"primary\">\n              더 보기\n              <IconNavArrow displayDirection=\"end\" className=\"inline ms-1\" />\n            </ButtonLink>\n          ) : null}\n        </div>\n      </section>\n      <hr className=\"border-border dark:border-border-dark mb-14\" />\n    </>\n  );\n}\n\nfunction ReadBlogPost({path}: {path: string}) {\n  return (\n    <ButtonLink className=\"mt-1\" label=\"Read Post\" href={path} type=\"primary\">\n      Read Post\n      <IconNavArrow displayDirection=\"end\" className=\"inline ms-1\" />\n    </ButtonLink>\n  );\n}\n\nfunction Math({children}: {children: any}) {\n  return (\n    <span\n      style={{\n        fontFamily: 'STIXGeneral-Regular, Georgia, serif',\n        fontSize: '1.2rem',\n      }}>\n      {children}\n    </span>\n  );\n}\n\nfunction MathI({children}: {children: any}) {\n  return (\n    <span\n      style={{\n        fontFamily: 'STIXGeneral-Italic, Georgia, serif',\n        fontSize: '1.2rem',\n      }}>\n      {children}\n    </span>\n  );\n}\n\nfunction YouWillLearn({\n  children,\n  isChapter,\n}: {\n  children: any;\n  isChapter?: boolean;\n}) {\n  let title = isChapter ? '이 장에서는' : '학습 내용';\n  return <SimpleCallout title={title}>{children}</SimpleCallout>;\n}\n\n// TODO: typing.\nfunction Recipes(props: any) {\n  return <Challenges {...props} isRecipes={true} />;\n}\n\nfunction AuthorCredit({\n  author = 'Rachel Lee Nabors',\n  authorLink = 'https://nearestnabors.com/',\n}: {\n  author: string;\n  authorLink: string;\n}) {\n  return (\n    <div className=\"sr-only group-hover:not-sr-only group-focus-within:not-sr-only hover:sr-only\">\n      <p className=\"bg-card dark:bg-card-dark text-center text-sm text-secondary dark:text-secondary-dark leading-tight p-2 rounded-lg absolute start-1/2 -top-4 -translate-x-1/2 -translate-y-full group-hover:flex group-hover:opacity-100 after:content-[''] after:absolute after:start-1/2 after:top-[95%] after:-translate-x-1/2 after:border-8 after:border-x-transparent after:border-b-transparent after:border-t-card after:dark:border-t-card-dark opacity-0 transition-opacity duration-300\">\n        <cite>\n          Illustrated by{' '}\n          {authorLink ? (\n            <a\n              target=\"_blank\"\n              rel=\"noreferrer\"\n              className=\"text-link dark:text-link-dark\"\n              href={authorLink}>\n              {author}\n            </a>\n          ) : (\n            author\n          )}\n        </cite>\n      </p>\n    </div>\n  );\n}\n\nconst IllustrationContext = React.createContext<{\n  isInBlock?: boolean;\n}>({\n  isInBlock: false,\n});\n\nfunction Illustration({\n  caption,\n  src,\n  alt,\n  author,\n  authorLink,\n}: {\n  caption: string;\n  src: string;\n  alt: string;\n  author: string;\n  authorLink: string;\n}) {\n  const {isInBlock} = React.useContext(IllustrationContext);\n\n  return (\n    <div className=\"relative group before:absolute before:-inset-y-16 before:inset-x-0 my-16 mx-0 2xl:mx-auto max-w-4xl 2xl:max-w-6xl\">\n      <figure className=\"my-8 flex justify-center\">\n        <img\n          src={src}\n          alt={alt}\n          style={{maxHeight: 300}}\n          className=\"rounded-lg\"\n        />\n        {caption ? (\n          <figcaption className=\"text-center leading-tight mt-4\">\n            {caption}\n          </figcaption>\n        ) : null}\n      </figure>\n      {!isInBlock && <AuthorCredit author={author} authorLink={authorLink} />}\n    </div>\n  );\n}\n\nconst isInBlockTrue = {isInBlock: true};\n\nfunction IllustrationBlock({\n  sequential,\n  author,\n  authorLink,\n  children,\n}: {\n  author: string;\n  authorLink: string;\n  sequential: boolean;\n  children: any;\n}) {\n  const imageInfos = Children.toArray(children).map(\n    (child: any) => child.props\n  );\n  const images = imageInfos.map((info, index) => (\n    <figure key={index}>\n      <div className=\"bg-white rounded-lg p-4 flex-1 flex xl:p-6 justify-center items-center my-4\">\n        <img\n          className=\"text-primary\"\n          src={info.src}\n          alt={info.alt}\n          height={info.height}\n        />\n      </div>\n      {info.caption ? (\n        <figcaption className=\"text-secondary dark:text-secondary-dark text-center leading-tight mt-4\">\n          {info.caption}\n        </figcaption>\n      ) : null}\n    </figure>\n  ));\n  return (\n    <IllustrationContext value={isInBlockTrue}>\n      <div className=\"relative group before:absolute before:-inset-y-16 before:inset-x-0 my-16 mx-0 2xl:mx-auto max-w-4xl 2xl:max-w-6xl\">\n        {sequential ? (\n          <ol className=\"mdx-illustration-block flex\">\n            {images.map((x: any, i: number) => (\n              <li className=\"flex-1\" key={i}>\n                {x}\n              </li>\n            ))}\n          </ol>\n        ) : (\n          <div className=\"mdx-illustration-block\">{images}</div>\n        )}\n        <AuthorCredit author={author} authorLink={authorLink} />\n      </div>\n    </IllustrationContext>\n  );\n}\n\ntype NestedTocRoot = {\n  item: null;\n  children: Array<NestedTocNode>;\n};\n\ntype NestedTocNode = {\n  item: TocItem;\n  children: Array<NestedTocNode>;\n};\n\nfunction calculateNestedToc(toc: Toc): NestedTocRoot {\n  const currentAncestors = new Map<number, NestedTocNode | NestedTocRoot>();\n  const root: NestedTocRoot = {\n    item: null,\n    children: [],\n  };\n  const startIndex = 1; // Skip \"Overview\"\n  for (let i = startIndex; i < toc.length; i++) {\n    const item = toc[i];\n    const currentParent: NestedTocNode | NestedTocRoot =\n      currentAncestors.get(item.depth - 1) || root;\n    const node: NestedTocNode = {\n      item,\n      children: [],\n    };\n    currentParent.children.push(node);\n    currentAncestors.set(item.depth, node);\n  }\n  return root;\n}\n\nfunction InlineToc() {\n  const toc = useContext(TocContext);\n  const root = useMemo(() => calculateNestedToc(toc), [toc]);\n  if (root.children.length < 2) {\n    return null;\n  }\n  return <InlineTocItem items={root.children} />;\n}\n\nfunction InlineTocItem({items}: {items: Array<NestedTocNode>}) {\n  return (\n    <UL>\n      {items.map((node) => (\n        <LI key={node.item.url}>\n          <Link href={node.item.url}>{node.item.text}</Link>\n          {node.children.length > 0 && <InlineTocItem items={node.children} />}\n        </LI>\n      ))}\n    </UL>\n  );\n}\n\ntype TranslationProgress = 'complete' | 'in-progress';\n\nfunction LanguageList({progress}: {progress: TranslationProgress}) {\n  const allLanguages = React.useContext(LanguagesContext) ?? [];\n  const languages = allLanguages\n    .filter(\n      ({code}) =>\n        code !== 'en' &&\n        (progress === 'complete'\n          ? finishedTranslations.includes(code)\n          : !finishedTranslations.includes(code))\n    )\n    .sort((a, b) => a.enName.localeCompare(b.enName));\n  return (\n    <UL>\n      {languages.map(({code, name, enName}) => {\n        return (\n          <LI key={code}>\n            <Link href={`https://${code}.react.dev/`}>\n              {enName} ({name})\n            </Link>{' '}\n            &mdash;{' '}\n            <Link href={`https://github.com/reactjs/${code}.react.dev`}>\n              Contribute\n            </Link>\n          </LI>\n        );\n      })}\n    </UL>\n  );\n}\n\nfunction YouTubeIframe(props: any) {\n  return (\n    <div className=\"relative h-0 overflow-hidden pt-[56.25%]\">\n      <iframe\n        className=\"absolute inset-0 w-full h-full\"\n        frameBorder=\"0\"\n        allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share\"\n        allowFullScreen\n        title=\"YouTube video player\"\n        {...props}\n      />\n    </div>\n  );\n}\n\nfunction Image(props: any) {\n  const {alt, ...rest} = props;\n  return <img alt={alt} className=\"max-w-[calc(min(700px,100%))]\" {...rest} />;\n}\n\nexport const MDXComponents = {\n  p: P,\n  strong: Strong,\n  blockquote: Blockquote,\n  ol: OL,\n  ul: UL,\n  li: LI,\n  h1: H1,\n  h2: H2,\n  h3: H3,\n  h4: H4,\n  h5: H5,\n  hr: Divider,\n  a: Link,\n  img: Image,\n  BlogCard,\n  code: InlineCode,\n  pre: CodeBlock,\n  CodeDiagram,\n  ConsoleBlock,\n  ConsoleBlockMulti,\n  ConsoleLogLine,\n  DeepDive: (props: {\n    children: React.ReactNode;\n    title: string;\n    excerpt: string;\n  }) => <ExpandableExample {...props} type=\"DeepDive\" />,\n  Diagram,\n  DiagramGroup,\n  FullWidth({children}: {children: any}) {\n    return children;\n  },\n  MaxWidth({children}: {children: any}) {\n    return <div className=\"max-w-4xl ms-0 2xl:mx-auto\">{children}</div>;\n  },\n  Pitfall,\n  Deprecated,\n  Wip,\n  Illustration,\n  IllustrationBlock,\n  Intro,\n  InlineToc,\n  LanguageList,\n  LearnMore,\n  Math,\n  MathI,\n  Note,\n  RC,\n  Canary,\n  Experimental,\n  ExperimentalBadge,\n  CanaryBadge,\n  NextMajor,\n  NextMajorBadge,\n  RSC,\n  RSCBadge,\n  PackageImport,\n  ReadBlogPost,\n  Recap,\n  Recipes,\n  Sandpack,\n  SandpackRSC,\n  SandpackWithHTMLOutput,\n  TeamMember,\n  TerminalBlock,\n  YouWillLearn,\n  YouWillLearnCard,\n  Challenges,\n  Hint,\n  Solution,\n  CodeStep,\n  YouTubeIframe,\n  ErrorDecoder,\n};\n\nfor (let key in MDXComponents) {\n  if (MDXComponents.hasOwnProperty(key)) {\n    const MDXComponent: any = (MDXComponents as any)[key];\n    MDXComponent.mdxName = key;\n  }\n}\n"
  },
  {
    "path": "src/components/MDX/PackageImport.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport {Children} from 'react';\nimport * as React from 'react';\nimport CodeBlock from './CodeBlock';\n\ninterface PackageImportProps {\n  children: React.ReactNode;\n}\n\nexport function PackageImport({children}: PackageImportProps) {\n  const terminal = Children.toArray(children).filter((child: any) => {\n    return child.type?.mdxName !== 'pre';\n  });\n  const code = Children.toArray(children).map((child: any, i: number) => {\n    if (child.type?.mdxName === 'pre') {\n      return (\n        <CodeBlock\n          {...child.props}\n          isFromPackageImport\n          key={i}\n          noMargin={true}\n          noMarkers={true}\n        />\n      );\n    } else {\n      return null;\n    }\n  });\n  return (\n    <section className=\"my-8 grid grid-cols-1 lg:grid-cols-2 gap-x-8 gap-y-4\">\n      <div className=\"flex flex-col justify-center\">{terminal}</div>\n      <div className=\"flex flex-col justify-center\">{code}</div>\n    </section>\n  );\n}\n"
  },
  {
    "path": "src/components/MDX/Recap.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport * as React from 'react';\nimport {H2} from './Heading';\n\ninterface RecapProps {\n  children: React.ReactNode;\n}\n\nfunction Recap({children}: RecapProps) {\n  return (\n    <section>\n      <H2 isPageAnchor id=\"recap\">\n        요약\n      </H2>\n      {children}\n    </section>\n  );\n}\n\nexport default Recap;\n"
  },
  {
    "path": "src/components/MDX/Sandpack/ClearButton.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport * as React from 'react';\nimport {IconClose} from '../../Icon/IconClose';\nexport interface ClearButtonProps {\n  onClear: () => void;\n}\n\nexport function ClearButton({onClear}: ClearButtonProps) {\n  return (\n    <button\n      className=\"text-sm text-primary dark:text-primary-dark inline-flex items-center hover:text-link duration-100 ease-in transition mx-1\"\n      onClick={onClear}\n      title=\"모든 편집 내용을 지우고 샌드박스를 다시 로딩합니다.\"\n      type=\"button\">\n      <IconClose className=\"inline mx-1 relative\" />\n      <span className=\"hidden md:block\">초기화</span>\n    </button>\n  );\n}\n"
  },
  {
    "path": "src/components/MDX/Sandpack/Console.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\nimport cn from 'classnames';\nimport {useState, useRef, useEffect} from 'react';\nimport {IconChevron} from 'components/Icon/IconChevron';\n\nimport {\n  SandpackCodeViewer,\n  useSandpack,\n} from '@codesandbox/sandpack-react/unstyled';\nimport type {SandpackMessageConsoleMethods} from '@codesandbox/sandpack-client';\n\nconst getType = (\n  message: SandpackMessageConsoleMethods\n): 'info' | 'warning' | 'error' => {\n  if (message === 'log' || message === 'info') {\n    return 'info';\n  }\n\n  if (message === 'warn') {\n    return 'warning';\n  }\n\n  return 'error';\n};\n\nconst getColor = (message: SandpackMessageConsoleMethods): string => {\n  if (message === 'warn') {\n    return 'text-yellow-50';\n  } else if (message === 'error') {\n    return 'text-red-40';\n  } else {\n    return 'text-secondary dark:text-secondary-dark';\n  }\n};\n\n// based on https://github.com/tmpfs/format-util/blob/0e62d430efb0a1c51448709abd3e2406c14d8401/format.js#L1\n// based on https://developer.mozilla.org/en-US/docs/Web/API/console#Using_string_substitutions\n// Implements s, d, i and f placeholders\nfunction formatStr(...inputArgs: any[]): any[] {\n  const maybeMessage = inputArgs[0];\n  if (typeof maybeMessage !== 'string') {\n    return inputArgs;\n  }\n  // If the first argument is a string, check for substitutions.\n  const args = inputArgs.slice(1);\n  let formatted: string = String(maybeMessage);\n  if (args.length) {\n    const REGEXP = /(%?)(%([jds]))/g;\n\n    formatted = formatted.replace(REGEXP, (match, escaped, ptn, flag) => {\n      let arg = args.shift();\n      switch (flag) {\n        case 's':\n          arg += '';\n          break;\n        case 'd':\n        case 'i':\n          arg = parseInt(arg, 10).toString();\n          break;\n        case 'f':\n          arg = parseFloat(arg).toString();\n          break;\n      }\n      if (!escaped) {\n        return arg;\n      }\n      args.unshift(arg);\n      return match;\n    });\n  }\n\n  // Arguments that remain after formatting.\n  if (args.length) {\n    for (let i = 0; i < args.length; i++) {\n      formatted += ' ' + String(args[i]);\n    }\n  }\n\n  // Update escaped %% values.\n  return [formatted.replace(/%{2,2}/g, '%')];\n}\n\ntype ConsoleData = Array<{\n  data: Array<string | Record<string, string>>;\n  id: string;\n  method: SandpackMessageConsoleMethods;\n}>;\n\nconst MAX_MESSAGE_COUNT = 100;\n\nexport const SandpackConsole = ({visible}: {visible: boolean}) => {\n  const {listen} = useSandpack();\n  const [logs, setLogs] = useState<ConsoleData>([]);\n  const wrapperRef = useRef<HTMLDivElement>(null);\n\n  useEffect(() => {\n    let isActive = true;\n    const unsubscribe = listen((message) => {\n      if (!isActive) {\n        console.warn('Received an unexpected log from Sandpack.');\n        return;\n      }\n      if (\n        (message.type === 'start' && message.firstLoad) ||\n        message.type === 'refresh'\n      ) {\n        setLogs([]);\n      }\n      if (message.type === 'console' && message.codesandbox) {\n        setLogs((prev) => {\n          const newLogs = message.log\n            .filter((consoleData) => {\n              if (!consoleData.method || !consoleData.data) {\n                return false;\n              }\n              if (\n                typeof consoleData.data[0] === 'string' &&\n                consoleData.data[0].indexOf('The above error occurred') !== -1\n              ) {\n                // Don't show React error addendum because\n                // we have a custom error overlay.\n                return false;\n              }\n              return true;\n            })\n            .map((consoleData) => {\n              return {\n                ...consoleData,\n                data: formatStr(...consoleData.data),\n              };\n            });\n          let messages = [...prev, ...newLogs];\n          while (messages.length > MAX_MESSAGE_COUNT) {\n            messages.shift();\n          }\n          return messages;\n        });\n      }\n    });\n\n    return () => {\n      unsubscribe();\n      isActive = false;\n    };\n  }, [listen]);\n\n  const [isExpanded, setIsExpanded] = useState(true);\n\n  useEffect(() => {\n    if (wrapperRef.current) {\n      wrapperRef.current.scrollTop = wrapperRef.current.scrollHeight;\n    }\n  }, [logs]);\n\n  if (!visible || logs.length === 0) {\n    return null;\n  }\n\n  return (\n    <div className=\"absolute dark:border-gray-700 bg-white dark:bg-gray-95 border-t bottom-0 w-full dark:text-white\">\n      <div className=\"flex justify-between\">\n        <button\n          className=\"flex items-center p-1\"\n          onClick={() => setIsExpanded(!isExpanded)}>\n          <IconChevron displayDirection={isExpanded ? 'down' : 'right'} />\n          <span className=\"ps-1 text-sm\">Console ({logs.length})</span>\n        </button>\n        <button\n          className=\"p-1\"\n          onClick={() => {\n            setLogs([]);\n          }}>\n          <svg\n            viewBox=\"0 0 24 24\"\n            width=\"18\"\n            height=\"18\"\n            stroke=\"currentColor\"\n            strokeWidth=\"2\"\n            fill=\"none\"\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\">\n            <circle cx=\"12\" cy=\"12\" r=\"10\"></circle>\n            <line x1=\"4.93\" y1=\"4.93\" x2=\"19.07\" y2=\"19.07\"></line>\n          </svg>\n        </button>\n      </div>\n      {isExpanded && (\n        <div className=\"w-full h-full border-t bg-white dark:border-gray-700 dark:bg-gray-95 min-h-[28px] console\">\n          <div className=\"max-h-40 h-auto overflow-auto\" ref={wrapperRef}>\n            {logs.map(({data, id, method}) => {\n              return (\n                <div\n                  key={id}\n                  className={cn(\n                    'first:border-none border-t dark:border-gray-700 text-md p-1 ps-2 leading-6 font-mono min-h-[32px] whitespace-pre-wrap',\n                    `console-${getType(method)}`,\n                    getColor(method)\n                  )}>\n                  <span className=\"console-message\">\n                    {data.map((msg, index) => {\n                      if (typeof msg === 'string') {\n                        return <span key={`${msg}-${index}`}>{msg}</span>;\n                      }\n\n                      let children;\n                      if (msg != null && typeof msg['@t'] === 'string') {\n                        // CodeSandbox wraps custom types\n                        children = msg['@t'];\n                      } else {\n                        try {\n                          children = JSON.stringify(msg, null, 2);\n                        } catch (error) {\n                          try {\n                            children = Object.prototype.toString.call(msg);\n                          } catch (err) {\n                            children = '[' + typeof msg + ']';\n                          }\n                        }\n                      }\n\n                      return (\n                        <span\n                          className={cn('console-span')}\n                          key={`${msg}-${index}`}>\n                          <SandpackCodeViewer\n                            initMode=\"user-visible\"\n                            showTabs={false}\n                            // fileType=\"js\"\n                            code={children}\n                          />\n                        </span>\n                      );\n                    })}\n                  </span>\n                </div>\n              );\n            })}\n          </div>\n        </div>\n      )}\n    </div>\n  );\n};\n"
  },
  {
    "path": "src/components/MDX/Sandpack/CustomPreset.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\nimport {memo, useRef, useState} from 'react';\nimport {flushSync} from 'react-dom';\nimport {\n  useSandpack,\n  useActiveCode,\n  SandpackCodeEditor,\n  SandpackLayout,\n} from '@codesandbox/sandpack-react/unstyled';\nimport cn from 'classnames';\n\nimport {IconChevron} from 'components/Icon/IconChevron';\nimport {NavigationBar} from './NavigationBar';\nimport {Preview} from './Preview';\n\nimport {useSandpackLint} from './useSandpackLint';\n\nexport const CustomPreset = memo(function CustomPreset({\n  providedFiles,\n  showOpenInCodeSandbox = true,\n}: {\n  providedFiles: Array<string>;\n  showOpenInCodeSandbox?: boolean;\n}) {\n  const {lintErrors, lintExtensions} = useSandpackLint();\n  const {sandpack} = useSandpack();\n  const {code} = useActiveCode();\n  const {activeFile} = sandpack;\n  const lineCountRef = useRef<{[key: string]: number}>({});\n  if (!lineCountRef.current[activeFile]) {\n    // eslint-disable-next-line react-compiler/react-compiler\n    lineCountRef.current[activeFile] = code.split('\\n').length;\n  }\n  const lineCount = lineCountRef.current[activeFile];\n  const isExpandable = lineCount > 16;\n  return (\n    <SandboxShell\n      providedFiles={providedFiles}\n      lintErrors={lintErrors}\n      lintExtensions={lintExtensions}\n      isExpandable={isExpandable}\n      showOpenInCodeSandbox={showOpenInCodeSandbox}\n    />\n  );\n});\n\nconst SandboxShell = memo(function SandboxShell({\n  providedFiles,\n  lintErrors,\n  lintExtensions,\n  isExpandable,\n  showOpenInCodeSandbox,\n}: {\n  providedFiles: Array<string>;\n  lintErrors: Array<any>;\n  lintExtensions: Array<any>;\n  isExpandable: boolean;\n  showOpenInCodeSandbox: boolean;\n}) {\n  const containerRef = useRef<HTMLDivElement>(null);\n  const [isExpanded, setIsExpanded] = useState(false);\n  return (\n    <>\n      <div\n        className=\"shadow-lg dark:shadow-lg-dark rounded-lg\"\n        ref={containerRef}\n        style={{\n          contain: 'content',\n        }}>\n        <NavigationBar\n          providedFiles={providedFiles}\n          showOpenInCodeSandbox={showOpenInCodeSandbox}\n        />\n        <SandpackLayout\n          className={cn(\n            !(isExpandable || isExpanded) && 'rounded-b-lg overflow-hidden',\n            isExpanded && 'sp-layout-expanded'\n          )}>\n          <Editor lintExtensions={lintExtensions} />\n          <Preview\n            className=\"order-last xl:order-2\"\n            isExpanded={isExpanded}\n            lintErrors={lintErrors}\n          />\n          {(isExpandable || isExpanded) && (\n            <button\n              translate=\"yes\"\n              className=\"sandpack-expand flex text-base justify-between dark:border-card-dark bg-wash dark:bg-card-dark items-center z-10 p-1 w-full order-2 xl:order-last border-b-1 relative top-0\"\n              onClick={() => {\n                const nextIsExpanded = !isExpanded;\n                flushSync(() => {\n                  setIsExpanded(nextIsExpanded);\n                });\n                if (!nextIsExpanded && containerRef.current !== null) {\n                  // @ts-ignore\n                  if (containerRef.current.scrollIntoViewIfNeeded) {\n                    // @ts-ignore\n                    containerRef.current.scrollIntoViewIfNeeded();\n                  } else {\n                    containerRef.current.scrollIntoView({\n                      block: 'nearest',\n                      inline: 'nearest',\n                    });\n                  }\n                }\n              }}>\n              <span className=\"flex p-2 focus:outline-none text-primary dark:text-primary-dark leading-[20px]\">\n                <IconChevron\n                  className=\"inline me-1.5 text-xl\"\n                  displayDirection={isExpanded ? 'up' : 'down'}\n                />\n                {isExpanded ? '간략히 보기' : '자세히 보기'}\n              </span>\n            </button>\n          )}\n        </SandpackLayout>\n      </div>\n    </>\n  );\n});\n\nconst Editor = memo(function Editor({\n  lintExtensions,\n}: {\n  lintExtensions: Array<any>;\n}) {\n  return (\n    <SandpackCodeEditor\n      showLineNumbers\n      showInlineErrors\n      showTabs={false}\n      showRunButton={false}\n      extensions={lintExtensions}\n    />\n  );\n});\n"
  },
  {
    "path": "src/components/MDX/Sandpack/DownloadButton.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport {useSyncExternalStore} from 'react';\nimport {useSandpack} from '@codesandbox/sandpack-react/unstyled';\nimport {IconDownload} from '../../Icon/IconDownload';\nimport {AppJSPath, StylesCSSPath, SUPPORTED_FILES} from './createFileMap';\nexport interface DownloadButtonProps {}\n\nlet supportsImportMap = false;\n\nfunction subscribe(cb: () => void) {\n  // This shouldn't actually need to update, but this works around\n  // https://github.com/facebook/react/issues/26095\n  let timeout = setTimeout(() => {\n    supportsImportMap =\n      (HTMLScriptElement as any).supports &&\n      (HTMLScriptElement as any).supports('importmap');\n    cb();\n  }, 0);\n  return () => clearTimeout(timeout);\n}\n\nfunction useSupportsImportMap() {\n  function getCurrentValue() {\n    return supportsImportMap;\n  }\n  function getServerSnapshot() {\n    return false;\n  }\n\n  return useSyncExternalStore(subscribe, getCurrentValue, getServerSnapshot);\n}\n\nexport function DownloadButton({\n  providedFiles,\n}: {\n  providedFiles: Array<string>;\n}) {\n  const {sandpack} = useSandpack();\n  const supported = useSupportsImportMap();\n  if (!supported) {\n    return null;\n  }\n  if (providedFiles.some((file) => !SUPPORTED_FILES.includes(file))) {\n    return null;\n  }\n\n  const downloadHTML = () => {\n    const css = sandpack.files[StylesCSSPath]?.code ?? '';\n    const code = sandpack.files[AppJSPath]?.code ?? '';\n    const blob = new Blob([\n      `<!DOCTYPE html>\n<html>\n<body>\n  <div id=\"root\"></div>\n</body>\n<!-- This setup is not suitable for production. -->\n<!-- Only use it in development! -->\n<script src=\"https://unpkg.com/@babel/standalone/babel.min.js\"></script>\n<script async src=\"https://ga.jspm.io/npm:es-module-shims@1.7.0/dist/es-module-shims.js\"></script>\n<script type=\"importmap\">\n{\n  \"imports\": {\n    \"react\": \"https://esm.sh/react?dev\",\n    \"react-dom/client\": \"https://esm.sh/react-dom/client?dev\"\n  }\n}\n</script>\n<script type=\"text/babel\" data-type=\"module\">\nimport React, { StrictMode } from 'react';\nimport { createRoot } from 'react-dom/client';\n\n${code.replace('export default ', 'let App = ')}\n\nconst root = createRoot(document.getElementById('root'));\nroot.render(\n  <StrictMode>\n    <App />\n  </StrictMode>\n);\n</script>\n<style>\n${css}\n</style>\n</html>`,\n    ]);\n    const url = window.URL.createObjectURL(blob);\n    const a = document.createElement('a');\n    a.style.display = 'none';\n    a.href = url;\n    a.download = 'sandbox.html';\n    document.body.appendChild(a);\n    a.click();\n    window.URL.revokeObjectURL(url);\n  };\n\n  return (\n    <button\n      className=\"text-sm text-primary dark:text-primary-dark inline-flex items-center hover:text-link duration-100 ease-in transition mx-1\"\n      onClick={downloadHTML}\n      title=\"샌드박스를 다운로드합니다.\"\n      type=\"button\">\n      <IconDownload className=\"inline me-1\" /> 다운로드\n    </button>\n  );\n}\n"
  },
  {
    "path": "src/components/MDX/Sandpack/ErrorMessage.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\ninterface ErrorType {\n  title?: string;\n  message: string;\n  column?: number;\n  line?: number;\n  path?: string;\n}\n\nexport function ErrorMessage({error, ...props}: {error: ErrorType}) {\n  const {message, title} = error;\n\n  return (\n    <div className=\"bg-white border-2 border-red-40 rounded-lg p-6\" {...props}>\n      <h2 className=\"text-red-40 text-xl mb-4\">{title || 'Error'}</h2>\n      <pre className=\"text-secondary whitespace-pre-wrap break-words leading-tight\">\n        {message}\n      </pre>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/components/MDX/Sandpack/LoadingOverlay.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nimport {useState} from 'react';\n\nimport {\n  LoadingOverlayState,\n  OpenInCodeSandboxButton,\n  useSandpack,\n} from '@codesandbox/sandpack-react/unstyled';\nimport {useEffect} from 'react';\n\nconst FADE_ANIMATION_DURATION = 200;\n\nexport const LoadingOverlay = ({\n  clientId,\n  dependenciesLoading,\n  forceLoading,\n}: {\n  clientId: string;\n  dependenciesLoading: boolean;\n  forceLoading: boolean;\n} & React.HTMLAttributes<HTMLDivElement>): React.ReactNode | null => {\n  const loadingOverlayState = useLoadingOverlayState(\n    clientId,\n    dependenciesLoading,\n    forceLoading\n  );\n\n  if (loadingOverlayState === 'HIDDEN') {\n    return null;\n  }\n\n  if (loadingOverlayState === 'TIMEOUT') {\n    return (\n      <div className=\"sp-overlay sp-error\">\n        <div className=\"sp-error-message\">\n          Unable to establish connection with the sandpack bundler. Make sure\n          you are online or try again later. If the problem persists, please\n          report it via{' '}\n          <a\n            className=\"sp-error-message\"\n            href=\"mailto:hello@codesandbox.io?subject=Sandpack Timeout Error\">\n            email\n          </a>{' '}\n          or submit an issue on{' '}\n          <a\n            className=\"sp-error-message\"\n            href=\"https://github.com/codesandbox/sandpack/issues\"\n            rel=\"noreferrer noopener\"\n            target=\"_blank\">\n            GitHub.\n          </a>\n        </div>\n      </div>\n    );\n  }\n\n  const stillLoading =\n    loadingOverlayState === 'LOADING' || loadingOverlayState === 'PRE_FADING';\n\n  return (\n    <div\n      className=\"sp-overlay sp-loading\"\n      style={{\n        opacity: stillLoading ? 1 : 0,\n        transition: `opacity ${FADE_ANIMATION_DURATION}ms ease-out`,\n      }}>\n      <div className=\"sp-cube-wrapper\" title=\"Open in CodeSandbox\">\n        {/* @ts-ignore: the OpenInCodeSandboxButton type from '@codesandbox/sandpack-react/unstyled' is incompatible with JSX in React 19 */}\n        <OpenInCodeSandboxButton />\n        <div className=\"sp-cube\">\n          <div className=\"sp-sides\">\n            <div className=\"top\" />\n            <div className=\"right\" />\n            <div className=\"bottom\" />\n            <div className=\"left\" />\n            <div className=\"front\" />\n            <div className=\"back\" />\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n};\n\nconst useLoadingOverlayState = (\n  clientId: string,\n  dependenciesLoading: boolean,\n  forceLoading: boolean\n): LoadingOverlayState => {\n  const {sandpack, listen} = useSandpack();\n  const [state, setState] = useState<LoadingOverlayState>('HIDDEN');\n\n  if (state !== 'LOADING' && forceLoading) {\n    setState('LOADING');\n  }\n\n  /**\n   * Sandpack listener\n   */\n  const sandpackIdle = sandpack.status === 'idle';\n  useEffect(() => {\n    const unsubscribe = listen((message) => {\n      if (message.type === 'done') {\n        setState((prev) => {\n          return prev === 'LOADING' ? 'PRE_FADING' : 'HIDDEN';\n        });\n      }\n    }, clientId);\n\n    return () => {\n      unsubscribe();\n    };\n  }, [listen, clientId, sandpackIdle]);\n\n  /**\n   * Fading transient state\n   */\n  useEffect(() => {\n    let fadeTimeout: ReturnType<typeof setTimeout>;\n\n    if (state === 'PRE_FADING' && !dependenciesLoading) {\n      setState('FADING');\n    } else if (state === 'FADING') {\n      fadeTimeout = setTimeout(\n        () => setState('HIDDEN'),\n        FADE_ANIMATION_DURATION\n      );\n    }\n\n    return () => {\n      clearTimeout(fadeTimeout);\n    };\n  }, [state, dependenciesLoading]);\n\n  if (sandpack.status === 'timeout') {\n    return 'TIMEOUT';\n  }\n\n  if (sandpack.status !== 'running') {\n    return 'HIDDEN';\n  }\n\n  return state;\n};\n"
  },
  {
    "path": "src/components/MDX/Sandpack/NavigationBar.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport {\n  useRef,\n  useInsertionEffect,\n  useCallback,\n  useState,\n  useEffect,\n  Fragment,\n} from 'react';\nimport cn from 'classnames';\nimport {\n  FileTabs,\n  useSandpack,\n  useSandpackNavigation,\n} from '@codesandbox/sandpack-react/unstyled';\nimport {OpenInCodeSandboxButton} from './OpenInCodeSandboxButton';\nimport {ReloadButton} from './ReloadButton';\nimport {ClearButton} from './ClearButton';\nimport {DownloadButton} from './DownloadButton';\nimport {IconChevron} from '../../Icon/IconChevron';\nimport {Listbox} from '@headlessui/react';\nimport {OpenInTypeScriptPlaygroundButton} from './OpenInTypeScriptPlayground';\n\nexport function useEvent(fn: any): any {\n  const ref = useRef(null);\n  useInsertionEffect(() => {\n    ref.current = fn;\n  }, [fn]);\n  return useCallback((...args: any) => {\n    const f = ref.current!;\n    // @ts-ignore\n    return f(...args);\n  }, []);\n}\n\nconst getFileName = (filePath: string): string => {\n  const lastIndexOfSlash = filePath.lastIndexOf('/');\n  return filePath.slice(lastIndexOfSlash + 1);\n};\n\nexport function NavigationBar({\n  providedFiles,\n  showOpenInCodeSandbox = true,\n}: {\n  providedFiles: Array<string>;\n  showOpenInCodeSandbox?: boolean;\n}) {\n  const {sandpack} = useSandpack();\n  const containerRef = useRef<HTMLDivElement | null>(null);\n  const tabsRef = useRef<HTMLDivElement | null>(null);\n  // By default, show the dropdown because all tabs may not fit.\n  // We don't know whether they'll fit or not until after hydration:\n  const [showDropdown, setShowDropdown] = useState(true);\n  const {activeFile, setActiveFile, visibleFiles, clients} = sandpack;\n  const clientId = Object.keys(clients)[0];\n  const {refresh} = useSandpackNavigation(clientId);\n  const isMultiFile = visibleFiles.length > 1;\n  const hasJustToggledDropdown = useRef(false);\n\n  // Keep track of whether we can show all tabs or just the dropdown.\n  const onContainerResize = useEvent((containerWidth: number) => {\n    if (hasJustToggledDropdown.current === true) {\n      // Ignore changes likely caused by ourselves.\n      hasJustToggledDropdown.current = false;\n      return;\n    }\n    if (tabsRef.current === null) {\n      // Some ResizeObserver calls come after unmount.\n      return;\n    }\n    const tabsWidth = tabsRef.current.getBoundingClientRect().width;\n    const needsDropdown = tabsWidth >= containerWidth;\n    if (needsDropdown !== showDropdown) {\n      hasJustToggledDropdown.current = true;\n      setShowDropdown(needsDropdown);\n    }\n  });\n\n  useEffect(() => {\n    if (isMultiFile) {\n      const resizeObserver = new ResizeObserver((entries) => {\n        for (const entry of entries) {\n          if (entry.contentBoxSize) {\n            const contentBoxSize = Array.isArray(entry.contentBoxSize)\n              ? entry.contentBoxSize[0]\n              : entry.contentBoxSize;\n            const width = contentBoxSize.inlineSize;\n            onContainerResize(width);\n          }\n        }\n      });\n      const container = containerRef.current!;\n      resizeObserver.observe(container);\n      return () => resizeObserver.unobserve(container);\n    } else {\n      return;\n    }\n\n    // Note: in a real useEvent, onContainerResize would be omitted.\n  }, [isMultiFile, onContainerResize]);\n\n  const handleClear = () => {\n    /**\n     * resetAllFiles must come first, otherwise\n     * the previous content will appear for a second\n     * when the iframe loads.\n     *\n     * Plus, it should only prompt if there's any file changes\n     */\n    if (\n      sandpack.editorState === 'dirty' &&\n      confirm('모든 수정 사항이 초기화됩니다. 계속하시겠습니까?')\n    ) {\n      sandpack.resetAllFiles();\n    }\n    refresh();\n  };\n\n  const handleReload = () => {\n    refresh();\n  };\n\n  return (\n    <div className=\"bg-wash dark:bg-card-dark flex justify-between items-center relative z-10 border-b border-border dark:border-border-dark rounded-t-lg text-lg\">\n      {/* If Prettier reformats this block, the two @ts-ignore directives will no longer be adjacent to the problematic lines, causing TypeScript errors */}\n      {/* prettier-ignore */}\n      <div className=\"flex-1 grow min-w-0 px-4 lg:px-6\">\n        {/* @ts-ignore: the Listbox type from '@headlessui/react' is incompatible with JSX in React 19 */}\n        <Listbox value={activeFile} onChange={setActiveFile}>\n          <div ref={containerRef}>\n            <div className=\"relative overflow-hidden\">\n              <div\n                ref={tabsRef}\n                className={cn(\n                  // The container for all tabs is always in the DOM, but\n                  // not always visible. This lets us measure how much space\n                  // the tabs would take if displayed. We use this to decide\n                  // whether to keep showing the dropdown, or show all tabs.\n                  'w-[fit-content]',\n                  showDropdown ? 'invisible' : ''\n                )}>\n                {/* @ts-ignore: the FileTabs type from '@codesandbox/sandpack-react/unstyled' is incompatible with JSX in React 19 */}\n                <FileTabs />\n              </div>\n              {/* @ts-ignore: the Listbox type from '@headlessui/react' is incompatible with JSX in React 19 */}\n              <Listbox.Button as={Fragment}>\n                {({open}) => (\n                  // If tabs don't fit, display the dropdown instead.\n                  // The dropdown is absolutely positioned inside the\n                  // space that's taken by the (invisible) tab list.\n                  <button\n                    className={cn(\n                      'absolute top-0 start-[2px]',\n                      !showDropdown && 'invisible'\n                    )}>\n                    <span\n                      className={cn(\n                        'h-full py-2 px-1 mt-px -mb-px flex border-b text-link dark:text-link-dark border-link dark:border-link-dark items-center text-md leading-tight truncate'\n                      )}\n                      style={{maxWidth: '160px'}}>\n                      {getFileName(activeFile)}\n                      {isMultiFile && (\n                        <span className=\"ms-2\">\n                          <IconChevron\n                            displayDirection={open ? 'up' : 'down'}\n                          />\n                        </span>\n                      )}\n                    </span>\n                  </button>\n                )}\n              </Listbox.Button>\n            </div>\n          </div>\n          {/* @ts-ignore: the Listbox type from '@headlessui/react' is incompatible with JSX in React 19 */}\n          {isMultiFile && showDropdown && (<Listbox.Options className=\"absolute mt-0.5 bg-card dark:bg-card-dark px-2 inset-x-0 mx-0 rounded-b-lg border-1 border-border dark:border-border-dark rounded-sm shadow-md\">\n              {/* @ts-ignore: the Listbox type from '@headlessui/react' is incompatible with JSX in React 19 */}\n              {visibleFiles.map((filePath: string) => (<Listbox.Option key={filePath} value={filePath} as={Fragment}>\n                  {({active}) => (\n                    <li\n                      className={cn(\n                        'text-md mx-2 my-4 cursor-pointer',\n                        active && 'text-link dark:text-link-dark'\n                      )}>\n                      {getFileName(filePath)}\n                    </li>\n                  )}\n                </Listbox.Option>\n              ))}\n            </Listbox.Options>\n          )}\n        </Listbox>\n      </div>\n      <div\n        className=\"px-3 flex items-center justify-end text-start\"\n        translate=\"yes\">\n        <DownloadButton providedFiles={providedFiles} />\n        <ReloadButton onReload={handleReload} />\n        <ClearButton onClear={handleClear} />\n        {showOpenInCodeSandbox && <OpenInCodeSandboxButton />}\n        {activeFile.endsWith('.tsx') && (\n          <OpenInTypeScriptPlaygroundButton\n            content={sandpack.files[activeFile]?.code || ''}\n          />\n        )}\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/components/MDX/Sandpack/OpenInCodeSandboxButton.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport {UnstyledOpenInCodeSandboxButton} from '@codesandbox/sandpack-react/unstyled';\nimport {IconNewPage} from '../../Icon/IconNewPage';\n\nexport const OpenInCodeSandboxButton = () => {\n  return (\n    <UnstyledOpenInCodeSandboxButton\n      className=\"text-sm text-primary dark:text-primary-dark inline-flex items-center hover:text-link duration-100 ease-in transition mx-1 ms-2 md:ms-1\"\n      title=\"CodeSandbox에서 편집합니다.\">\n      <IconNewPage\n        className=\"inline mx-1 relative top-[1px]\"\n        width=\"1em\"\n        height=\"1em\"\n      />\n      <span className=\"hidden md:block\">포크</span>\n    </UnstyledOpenInCodeSandboxButton>\n  );\n};\n"
  },
  {
    "path": "src/components/MDX/Sandpack/OpenInTypeScriptPlayground.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport {IconNewPage} from '../../Icon/IconNewPage';\n\nexport const OpenInTypeScriptPlaygroundButton = (props: {content: string}) => {\n  const contentWithReactImport = `import * as React from 'react';\\n\\n${props.content}`;\n  return (\n    <a\n      className=\"text-sm text-primary dark:text-primary-dark inline-flex items-center hover:text-link duration-100 ease-in transition mx-1 ml-2 md:ml-1\"\n      href={`https://www.typescriptlang.org/play#src=${encodeURIComponent(\n        contentWithReactImport\n      )}`}\n      title=\"Open in TypeScript Playground\"\n      target=\"_blank\"\n      rel=\"noreferrer\">\n      <IconNewPage\n        className=\"inline mx-1 relative top-[1px]\"\n        width=\"1em\"\n        height=\"1em\"\n      />\n      <span className=\"hidden md:block\">TypeScript Playground</span>\n    </a>\n  );\n};\n"
  },
  {
    "path": "src/components/MDX/Sandpack/Preview.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\n// eslint-disable-next-line react-compiler/react-compiler\n/* eslint-disable react-hooks/exhaustive-deps */\nimport {useRef, useState, useEffect, useMemo, useId} from 'react';\nimport {useSandpack, SandpackStack} from '@codesandbox/sandpack-react/unstyled';\nimport cn from 'classnames';\nimport {ErrorMessage} from './ErrorMessage';\nimport {SandpackConsole} from './Console';\nimport type {LintDiagnostic} from './useSandpackLint';\nimport {CSSProperties} from 'react';\nimport {LoadingOverlay} from './LoadingOverlay';\n\ntype CustomPreviewProps = {\n  className?: string;\n  isExpanded: boolean;\n  lintErrors: LintDiagnostic;\n};\n\nfunction useDebounced(value: any): any {\n  const ref = useRef<any>(null);\n  const [saved, setSaved] = useState(value);\n  useEffect(() => {\n    clearTimeout(ref.current);\n    ref.current = setTimeout(() => {\n      setSaved(value);\n    }, 300);\n  }, [value]);\n  return saved;\n}\n\nexport function Preview({\n  isExpanded,\n  className,\n  lintErrors,\n}: CustomPreviewProps) {\n  const {sandpack, listen} = useSandpack();\n  const [bundlerIsReady, setBundlerIsReady] = useState(false);\n  const [showLoading, setShowLoading] = useState(false);\n  const [iframeComputedHeight, setComputedAutoHeight] = useState<number | null>(\n    null\n  );\n\n  let {error: rawError, registerBundler, unregisterBundler} = sandpack;\n\n  if (\n    rawError &&\n    rawError.message === '_csbRefreshUtils.prelude is not a function'\n  ) {\n    // Work around a noisy internal error.\n    rawError = null;\n  }\n\n  // When throwing a new Error in Sandpack - we want to disable the dev error dialog\n  // to show the Error Boundary fallback\n  if (rawError && rawError.message.includes('Example Error:')) {\n    rawError = null;\n  }\n\n  // Memoized because it's fed to debouncing.\n  const firstLintError = useMemo(() => {\n    if (lintErrors.length === 0) {\n      return null;\n    } else {\n      const {line, column, message} = lintErrors[0];\n      return {\n        title: 'Lint Error',\n        message: `${line}:${column} - ${message}`,\n      };\n    }\n  }, [lintErrors]);\n\n  if (rawError == null || rawError.title === 'Runtime Exception') {\n    if (firstLintError !== null) {\n      rawError = firstLintError;\n    }\n  }\n\n  if (rawError != null && rawError.title === 'Runtime Exception') {\n    rawError.title = 'Runtime Error';\n  }\n\n  // It changes too fast, causing flicker.\n  const error = useDebounced(rawError);\n\n  const clientId = useId();\n  const iframeRef = useRef<HTMLIFrameElement | null>(null);\n\n  const sandpackIdle = sandpack.status === 'idle';\n\n  useEffect(function createBundler() {\n    const iframeElement = iframeRef.current!;\n    registerBundler(iframeElement, clientId);\n\n    return () => {\n      unregisterBundler(clientId);\n    };\n  }, []);\n\n  useEffect(\n    function bundlerListener() {\n      let timeout: ReturnType<typeof setTimeout>;\n\n      const unsubscribe = listen((message) => {\n        if (message.type === 'resize') {\n          setComputedAutoHeight(message.height);\n        } else if (message.type === 'start') {\n          if (message.firstLoad) {\n            setBundlerIsReady(false);\n          }\n\n          /**\n           * The spinner component transition might be longer than\n           * the bundler loading, so we only show the spinner if\n           * it takes more than 500s to load the bundler.\n           */\n          timeout = setTimeout(() => {\n            setShowLoading(true);\n          }, 500);\n        } else if (message.type === 'done') {\n          setBundlerIsReady(true);\n          setShowLoading(false);\n          clearTimeout(timeout);\n        }\n      }, clientId);\n\n      return () => {\n        clearTimeout(timeout);\n        setBundlerIsReady(false);\n        setComputedAutoHeight(null);\n        unsubscribe();\n      };\n    },\n    [sandpackIdle]\n  );\n\n  // WARNING:\n  // The layout and styling here is convoluted and really easy to break.\n  // If you make changes to it, you need to test different cases:\n  // - Content -> (compile | runtime) error -> content editing flow should work.\n  // - Errors should expand parent height rather than scroll.\n  // - Long sandboxes should scroll unless \"show more\" is toggled.\n  // - Expanded sandboxes (\"show more\") have sticky previews and errors.\n  // - Sandboxes have autoheight based on content.\n  // - That autoheight should be measured correctly! (Check some long ones.)\n  // - You shouldn't see nested scrolls (that means autoheight is borked).\n  // - Ideally you shouldn't see a blank preview tile while recompiling.\n  // - Container shouldn't be horizontally scrollable (even while loading).\n  // - It should work on mobile.\n  // The best way to test it is to actually go through some challenges.\n\n  const hideContent = error || !iframeComputedHeight || !bundlerIsReady;\n\n  const iframeWrapperPosition = (): CSSProperties => {\n    if (hideContent) {\n      return {position: 'relative'};\n    }\n\n    if (isExpanded) {\n      return {position: 'sticky', top: 'calc(2em + 64px)'};\n    }\n\n    return {};\n  };\n\n  return (\n    <SandpackStack className={className}>\n      <div\n        className={cn(\n          'p-0 sm:p-2 md:p-4 lg:p-8 bg-card dark:bg-wash-dark h-full relative md:rounded-b-lg lg:rounded-b-none',\n          // Allow content to be scrolled if it's too high to fit.\n          // Note we don't want this in the expanded state\n          // because it breaks position: sticky (and isn't needed anyway).\n          !isExpanded && (error || bundlerIsReady) ? 'overflow-auto' : null\n        )}>\n        <div style={iframeWrapperPosition()}>\n          <iframe\n            ref={iframeRef}\n            className={cn(\n              'rounded-t-none bg-white md:shadow-md sm:rounded-lg w-full max-w-full transition-opacity',\n              // We can't *actually* hide content because that would\n              // break calculating the computed height in the iframe\n              // (which we're using for autosizing). This is noticeable\n              // if you make a compiler error and then fix it with code\n              // that expands the content. You want to measure that.\n              hideContent\n                ? 'absolute opacity-0 pointer-events-none duration-75'\n                : 'opacity-100 duration-150'\n            )}\n            title=\"Sandbox Preview\"\n            style={{\n              height: iframeComputedHeight || '15px',\n              zIndex: isExpanded ? 'initial' : -1,\n            }}\n          />\n        </div>\n\n        {error && (\n          <div\n            className={cn(\n              'z-50',\n              // This isn't absolutely positioned so that\n              // the errors can also expand the parent height.\n              isExpanded ? 'sticky top-8 ' : null\n            )}>\n            <ErrorMessage error={error} />\n          </div>\n        )}\n\n        <LoadingOverlay\n          clientId={clientId}\n          dependenciesLoading={!bundlerIsReady && iframeComputedHeight === null}\n          forceLoading={showLoading}\n        />\n      </div>\n      <SandpackConsole visible={!error} />\n    </SandpackStack>\n  );\n}\n"
  },
  {
    "path": "src/components/MDX/Sandpack/ReloadButton.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport * as React from 'react';\nimport {IconRestart} from '../../Icon/IconRestart';\nexport interface ReloadButtonProps {\n  onReload: () => void;\n}\n\nexport function ReloadButton({onReload}: ReloadButtonProps) {\n  return (\n    <button\n      className=\"text-sm text-primary dark:text-primary-dark inline-flex items-center hover:text-link duration-100 ease-in transition mx-1\"\n      onClick={onReload}\n      title=\"편집 내용을 유지하고 샌드박스를 새로고침합니다.\"\n      type=\"button\">\n      <IconRestart className=\"inline mx-1 relative\" />\n      <span className=\"hidden md:block\">새로고침</span>\n    </button>\n  );\n}\n"
  },
  {
    "path": "src/components/MDX/Sandpack/ResetButton.tsx",
    "content": "/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport * as React from 'react';\nimport {IconRestart} from '../../Icon/IconRestart';\nexport interface ResetButtonProps {\n  onReset: () => void;\n}\n\nexport function ResetButton({onReset}: ResetButtonProps) {\n  return (\n    <button\n      className=\"text-sm text-primary dark:text-primary-dark inline-flex items-center hover:text-link duration-100 ease-in transition mx-1\"\n      onClick={onReset}\n      title=\"Reset Sandbox\"\n      type=\"button\">\n      <IconRestart className=\"inline mx-1 relative\" /> 초기화\n    </button>\n  );\n}\n"
  },
  {
    "path": "src/components/MDX/Sandpack/SandpackRSCRoot.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport {Children} from 'react';\nimport * as React from 'react';\nimport {SandpackProvider} from '@codesandbox/sandpack-react/unstyled';\nimport {SandpackLogLevel} from '@codesandbox/sandpack-client';\nimport {CustomPreset} from './CustomPreset';\nimport {createFileMap} from './createFileMap';\nimport {CustomTheme} from './Themes';\nimport {templateRSC} from './templateRSC';\nimport {RscFileBridge} from './sandpack-rsc/RscFileBridge';\n\ntype SandpackProps = {\n  children: React.ReactNode;\n  autorun?: boolean;\n};\n\nconst sandboxStyle = `\n* {\n  box-sizing: border-box;\n}\n\nbody {\n  font-family: sans-serif;\n  margin: 20px;\n  padding: 0;\n}\n\nh1 {\n  margin-top: 0;\n  font-size: 22px;\n}\n\nh2 {\n  margin-top: 0;\n  font-size: 20px;\n}\n\nh3 {\n  margin-top: 0;\n  font-size: 18px;\n}\n\nh4 {\n  margin-top: 0;\n  font-size: 16px;\n}\n\nh5 {\n  margin-top: 0;\n  font-size: 14px;\n}\n\nh6 {\n  margin-top: 0;\n  font-size: 12px;\n}\n\ncode {\n  font-size: 1.2em;\n}\n\nul {\n  padding-inline-start: 20px;\n}\n`.trim();\n\nfunction SandpackRSCRoot(props: SandpackProps) {\n  const {children, autorun = true} = props;\n  const codeSnippets = Children.toArray(children) as React.ReactElement[];\n  const files = createFileMap(codeSnippets);\n\n  if ('/index.html' in files) {\n    throw new Error(\n      'You cannot use `index.html` file in sandboxes. ' +\n        'Only `public/index.html` is respected by Sandpack and CodeSandbox (where forks are created).'\n    );\n  }\n\n  files['/src/styles.css'] = {\n    code: [sandboxStyle, files['/src/styles.css']?.code ?? ''].join('\\n\\n'),\n    hidden: !files['/src/styles.css']?.visible,\n  };\n\n  return (\n    <div className=\"sandpack sandpack--playground w-full my-8\" dir=\"ltr\">\n      <SandpackProvider\n        files={{...templateRSC, ...files}}\n        theme={CustomTheme}\n        customSetup={{\n          dependencies: {},\n        }}\n        options={{\n          autorun,\n          initMode: 'user-visible',\n          initModeObserverOptions: {rootMargin: '1400px 0px'},\n          bundlerURL: 'https://786946de.sandpack-bundler-4bw.pages.dev',\n          logLevel: SandpackLogLevel.None,\n        }}>\n        <RscFileBridge />\n        <CustomPreset\n          providedFiles={Object.keys(files)}\n          showOpenInCodeSandbox={false}\n        />\n      </SandpackProvider>\n    </div>\n  );\n}\n\nexport default SandpackRSCRoot;\n"
  },
  {
    "path": "src/components/MDX/Sandpack/SandpackRoot.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport {Children} from 'react';\nimport * as React from 'react';\nimport {SandpackProvider} from '@codesandbox/sandpack-react/unstyled';\nimport {SandpackLogLevel} from '@codesandbox/sandpack-client';\nimport {CustomPreset} from './CustomPreset';\nimport {createFileMap} from './createFileMap';\nimport {CustomTheme} from './Themes';\nimport {template} from './template';\n\ntype SandpackProps = {\n  children: React.ReactNode;\n  autorun?: boolean;\n};\n\nconst sandboxStyle = `\n* {\n  box-sizing: border-box;\n}\n\nbody {\n  font-family: sans-serif;\n  margin: 20px;\n  padding: 0;\n}\n\nh1 {\n  margin-top: 0;\n  font-size: 22px;\n}\n\nh2 {\n  margin-top: 0;\n  font-size: 20px;\n}\n\nh3 {\n  margin-top: 0;\n  font-size: 18px;\n}\n\nh4 {\n  margin-top: 0;\n  font-size: 16px;\n}\n\nh5 {\n  margin-top: 0;\n  font-size: 14px;\n}\n\nh6 {\n  margin-top: 0;\n  font-size: 12px;\n}\n\ncode {\n  font-size: 1.2em;\n}\n\nul {\n  padding-inline-start: 20px;\n}\n`.trim();\n\nfunction SandpackRoot(props: SandpackProps) {\n  let {children, autorun = true} = props;\n  const codeSnippets = Children.toArray(children) as React.ReactElement[];\n  const files = createFileMap(codeSnippets);\n\n  if ('/index.html' in files) {\n    throw new Error(\n      'You cannot use `index.html` file in sandboxes. ' +\n        'Only `public/index.html` is respected by Sandpack and CodeSandbox (where forks are created).'\n    );\n  }\n\n  files['/src/styles.css'] = {\n    code: [sandboxStyle, files['/src/styles.css']?.code ?? ''].join('\\n\\n'),\n    hidden: !files['/src/styles.css']?.visible,\n  };\n\n  return (\n    <div className=\"sandpack sandpack--playground w-full my-8\" dir=\"ltr\">\n      <SandpackProvider\n        files={{...template, ...files}}\n        theme={CustomTheme}\n        customSetup={{\n          environment: 'create-react-app',\n        }}\n        options={{\n          autorun,\n          initMode: 'user-visible',\n          initModeObserverOptions: {rootMargin: '1400px 0px'},\n          bundlerURL: 'https://786946de.sandpack-bundler-4bw.pages.dev',\n          logLevel: SandpackLogLevel.None,\n        }}>\n        <CustomPreset providedFiles={Object.keys(files)} />\n      </SandpackProvider>\n    </div>\n  );\n}\n\nexport default SandpackRoot;\n"
  },
  {
    "path": "src/components/MDX/Sandpack/Themes.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport tailwindConfig from '../../../../tailwind.config';\n\nexport const CustomTheme = {\n  colors: {\n    accent: 'inherit',\n    base: 'inherit',\n    clickable: 'inherit',\n    disabled: 'inherit',\n    error: 'inherit',\n    errorSurface: 'inherit',\n    hover: 'inherit',\n    surface1: 'inherit',\n    surface2: 'inherit',\n    surface3: 'inherit',\n    warning: 'inherit',\n    warningSurface: 'inherit',\n  },\n  syntax: {\n    plain: 'inherit',\n    comment: 'inherit',\n    keyword: 'inherit',\n    tag: 'inherit',\n    punctuation: 'inherit',\n    definition: 'inherit',\n    property: 'inherit',\n    static: 'inherit',\n    string: 'inherit',\n  },\n  font: {\n    body: tailwindConfig.theme.extend.fontFamily.text\n      .join(', ')\n      .replace(/\"/gm, ''),\n    mono: tailwindConfig.theme.extend.fontFamily.mono\n      .join(', ')\n      .replace(/\"/gm, ''),\n    size: tailwindConfig.theme.extend.fontSize.code,\n    lineHeight: '24px',\n  },\n};\n"
  },
  {
    "path": "src/components/MDX/Sandpack/createFileMap.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport type {SandpackFile} from '@codesandbox/sandpack-react/unstyled';\nimport type {PropsWithChildren, ReactElement, HTMLAttributes} from 'react';\n\nexport const AppJSPath = `/src/App.js`;\nexport const StylesCSSPath = `/src/styles.css`;\nexport const SUPPORTED_FILES = [AppJSPath, StylesCSSPath];\n\n/**\n * Tokenize meta attributes while ignoring brace-wrapped metadata (e.g. {expectedErrors: …}).\n */\nfunction splitMeta(meta: string): string[] {\n  const tokens: string[] = [];\n  let current = '';\n  let depth = 0;\n  const trimmed = meta.trim();\n\n  for (let ii = 0; ii < trimmed.length; ii++) {\n    const char = trimmed[ii];\n\n    if (char === '{') {\n      if (depth === 0 && current) {\n        tokens.push(current);\n        current = '';\n      }\n      depth += 1;\n      continue;\n    }\n\n    if (char === '}') {\n      if (depth > 0) {\n        depth -= 1;\n      }\n      if (depth === 0) {\n        current = '';\n      }\n      if (depth < 0) {\n        throw new Error(`Unexpected closing brace in meta: ${meta}`);\n      }\n      continue;\n    }\n\n    if (depth > 0) {\n      continue;\n    }\n\n    if (/\\s/.test(char)) {\n      if (current) {\n        tokens.push(current);\n        current = '';\n      }\n      continue;\n    }\n\n    current += char;\n  }\n\n  if (current) {\n    tokens.push(current);\n  }\n\n  if (depth !== 0) {\n    throw new Error(`Unclosed brace in meta: ${meta}`);\n  }\n\n  return tokens;\n}\n\nexport const createFileMap = (codeSnippets: any) => {\n  return codeSnippets.reduce(\n    (result: Record<string, SandpackFile>, codeSnippet: React.ReactElement) => {\n      if (\n        (codeSnippet.type as any).mdxName !== 'pre' &&\n        codeSnippet.type !== 'pre'\n      ) {\n        return result;\n      }\n      const {props} = (\n        codeSnippet.props as PropsWithChildren<{\n          children: ReactElement<\n            HTMLAttributes<HTMLDivElement> & {meta?: string}\n          >;\n        }>\n      ).children;\n      let filePath; // path in the folder structure\n      let fileHidden = false; // if the file is available as a tab\n      let fileActive = false; // if the file tab is shown by default\n\n      if (props.meta) {\n        const tokens = splitMeta(props.meta);\n        const name = tokens.find(\n          (token) => token.includes('/') || token.includes('.')\n        );\n        if (name) {\n          filePath = name.startsWith('/') ? name : `/${name}`;\n        }\n        if (tokens.includes('hidden')) {\n          fileHidden = true;\n        }\n        if (tokens.includes('active')) {\n          fileActive = true;\n        }\n      } else {\n        if (props.className === 'language-js') {\n          filePath = AppJSPath;\n        } else if (props.className === 'language-css') {\n          filePath = StylesCSSPath;\n        } else {\n          throw new Error(\n            `Code block is missing a filename: ${props.children}`\n          );\n        }\n      }\n\n      if (!filePath) {\n        if (props.className === 'language-js') {\n          filePath = AppJSPath;\n        } else if (props.className === 'language-css') {\n          filePath = StylesCSSPath;\n        } else {\n          throw new Error(\n            `Code block is missing a filename: ${props.children}`\n          );\n        }\n      }\n\n      if (result[filePath]) {\n        throw new Error(\n          `File ${filePath} was defined multiple times. Each file snippet should have a unique path name`\n        );\n      }\n      result[filePath] = {\n        code: (props.children || '') as string,\n        hidden: fileHidden,\n        active: fileActive,\n      };\n\n      return result;\n    },\n    {}\n  );\n};\n"
  },
  {
    "path": "src/components/MDX/Sandpack/index.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport {lazy, memo, Children, Suspense} from 'react';\nimport {AppJSPath, createFileMap} from './createFileMap';\n\nconst SandpackRoot = lazy(() => import('./SandpackRoot'));\n\nconst SandpackGlimmer = ({code}: {code: string}) => (\n  <div className=\"sandpack sandpack--playground my-8\">\n    <div className=\"sp-wrapper\">\n      <div className=\"shadow-lg dark:shadow-lg-dark rounded-lg\">\n        <div className=\"bg-wash h-10 dark:bg-card-dark flex justify-between items-center relative z-10 border-b border-border dark:border-border-dark rounded-t-lg rounded-b-none\">\n          <div className=\"px-4 lg:px-6\">\n            <div className=\"sp-tabs\"></div>\n          </div>\n          <div className=\"px-3 flex items-center justify-end grow text-right\"></div>\n        </div>\n        <div className=\"sp-layout min-h-[216px] flex items-stretch flex-wrap\">\n          <div className=\"sp-stack sp-editor max-h-[406px] h-auto overflow-auto\">\n            <div className=\"sp-code-editor\">\n              <div className=\"sp-cm sp-pristine\">\n                <div className=\"cm-editor\">\n                  <div>\n                    <div className=\"cm-gutters ps-9 sticky min-h-[192px]\">\n                      <div className=\"cm-gutter cm-lineNumbers whitespace-pre sp-pre-placeholder\">\n                        {code}\n                      </div>\n                    </div>\n                  </div>\n                </div>\n              </div>\n            </div>\n          </div>\n          <div className=\"sp-stack order-last xl:order-2 max-h-[406px] h-auto\">\n            <div className=\"p-0 sm:p-2 md:p-4 lg:p-8 bg-card dark:bg-wash-dark h-full relative rounded-b-lg lg:rounded-b-none overflow-auto\"></div>\n          </div>\n          {code.split('\\n').length > 16 && (\n            <div className=\"flex h-[45px] text-base justify-between dark:border-card-dark bg-wash dark:bg-card-dark items-center z-10 rounded-t-none p-1 w-full order-2 xl:order-last border-b-1 relative top-0\"></div>\n          )}\n        </div>\n      </div>\n    </div>\n  </div>\n);\n\nexport const SandpackClient = memo(function SandpackWrapper(props: any): any {\n  const codeSnippet = createFileMap(Children.toArray(props.children));\n\n  // To set the active file in the fallback we have to find the active file first.\n  // If there are no active files we fallback to App.js as default.\n  let activeCodeSnippet = Object.keys(codeSnippet).filter(\n    (fileName) =>\n      codeSnippet[fileName]?.active === true &&\n      codeSnippet[fileName]?.hidden === false\n  );\n  let activeCode;\n  if (!activeCodeSnippet.length) {\n    activeCode = codeSnippet[AppJSPath].code;\n  } else {\n    activeCode = codeSnippet[activeCodeSnippet[0]].code;\n  }\n\n  return (\n    <Suspense fallback={<SandpackGlimmer code={activeCode} />}>\n      <SandpackRoot {...props} />\n    </Suspense>\n  );\n});\n\nconst SandpackRSCRoot = lazy(() => import('./SandpackRSCRoot'));\n\nexport const SandpackRSC = memo(function SandpackRSCWrapper(props: {\n  children: React.ReactNode;\n}): any {\n  const codeSnippet = createFileMap(Children.toArray(props.children));\n\n  // To set the active file in the fallback we have to find the active file first.\n  // If there are no active files we fallback to App.js as default.\n  let activeCodeSnippet = Object.keys(codeSnippet).filter(\n    (fileName) =>\n      codeSnippet[fileName]?.active === true &&\n      codeSnippet[fileName]?.hidden === false\n  );\n  let activeCode;\n  if (!activeCodeSnippet.length) {\n    activeCode = codeSnippet[AppJSPath]?.code ?? '';\n  } else {\n    activeCode = codeSnippet[activeCodeSnippet[0]].code;\n  }\n\n  return (\n    <Suspense fallback={<SandpackGlimmer code={activeCode} />}>\n      <SandpackRSCRoot>{props.children}</SandpackRSCRoot>\n    </Suspense>\n  );\n});\n"
  },
  {
    "path": "src/components/MDX/Sandpack/runESLint.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n// @ts-nocheck\n\nimport {Linter} from 'eslint/lib/linter/linter';\n\nimport type {Diagnostic} from '@codemirror/lint';\nimport type {Text} from '@codemirror/text';\n\nconst getCodeMirrorPosition = (\n  doc: Text,\n  {line, column}: {line: number; column?: number}\n): number => {\n  return doc.line(line).from + (column ?? 0) - 1;\n};\n\nconst linter = new Linter();\n\nconst reactRules = require('eslint-plugin-react-hooks').rules;\nlinter.defineRules({\n  'react-hooks/rules-of-hooks': reactRules['rules-of-hooks'],\n  'react-hooks/exhaustive-deps': reactRules['exhaustive-deps'],\n});\n\nconst options = {\n  parserOptions: {\n    ecmaVersion: 12,\n    sourceType: 'module',\n    ecmaFeatures: {jsx: true},\n  },\n  rules: {\n    'react-hooks/rules-of-hooks': 'error',\n    'react-hooks/exhaustive-deps': 'error',\n  },\n};\n\nexport const runESLint = (\n  doc: Text\n): {errors: any[]; codeMirrorErrors: Diagnostic[]} => {\n  const codeString = doc.toString();\n  const errors = linter.verify(codeString, options) as any[];\n\n  const severity = {\n    1: 'warning',\n    2: 'error',\n  };\n\n  const codeMirrorErrors = errors\n    .map((error) => {\n      if (!error) return undefined;\n\n      const from = getCodeMirrorPosition(doc, {\n        line: error.line,\n        column: error.column,\n      });\n\n      const to = getCodeMirrorPosition(doc, {\n        line: error.endLine ?? error.line,\n        column: error.endColumn ?? error.column,\n      });\n\n      return {\n        ruleId: error.ruleId,\n        from,\n        to,\n        severity: severity[error.severity],\n        message: error.message,\n      };\n    })\n    .filter(Boolean) as Diagnostic[];\n\n  return {\n    codeMirrorErrors,\n    errors: errors.map((item) => {\n      return {\n        ...item,\n        severity: severity[item.severity],\n      };\n    }),\n  };\n};\n"
  },
  {
    "path": "src/components/MDX/Sandpack/sandpack-rsc/RscFileBridge.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nimport {useEffect, useRef} from 'react';\nimport {useSandpack} from '@codesandbox/sandpack-react/unstyled';\n\n/**\n * Bridges file contents from the Sandpack editor to the RSC client entry\n * running inside the iframe. When the Sandpack bundler finishes compiling,\n * reads all raw file contents and posts them to the iframe via postMessage.\n */\nexport function RscFileBridge() {\n  const {sandpack, dispatch, listen} = useSandpack();\n  const filesRef = useRef(sandpack.files);\n\n  // TODO: fix this with useEffectEvent\n  // eslint-disable-next-line react-compiler/react-compiler\n  filesRef.current = sandpack.files;\n\n  useEffect(() => {\n    const unsubscribe = listen((msg: any) => {\n      if (msg.type !== 'done') return;\n\n      const files: Record<string, string> = {};\n      for (const [path, file] of Object.entries(filesRef.current)) {\n        files[path] = file.code;\n      }\n\n      dispatch({type: 'rsc-file-update', files} as any);\n    });\n\n    return unsubscribe;\n  }, [dispatch, listen]);\n\n  return null;\n}\n"
  },
  {
    "path": "src/components/MDX/Sandpack/sandpack-rsc/sandbox-code/src/__react_refresh_init__.js",
    "content": "// Must run before React loads. Creates __REACT_DEVTOOLS_GLOBAL_HOOK__ so\n// React's renderer injects into it, enabling react-refresh to work.\nif (typeof window !== 'undefined' && !window.__REACT_DEVTOOLS_GLOBAL_HOOK__) {\n  var nextID = 0;\n  window.__REACT_DEVTOOLS_GLOBAL_HOOK__ = {\n    renderers: new Map(),\n    supportsFiber: true,\n    inject: function (injected) {\n      var id = nextID++;\n      this.renderers.set(id, injected);\n      return id;\n    },\n    onScheduleFiberRoot: function () {},\n    onCommitFiberRoot: function () {},\n    onCommitFiberUnmount: function () {},\n  };\n}\n"
  },
  {
    "path": "src/components/MDX/Sandpack/sandpack-rsc/sandbox-code/src/rsc-client.js",
    "content": "// RSC Client Entry Point\n// Runs inside the Sandpack iframe. Orchestrates the RSC pipeline:\n// 1. Creates a Web Worker from pre-bundled server runtime\n// 2. Receives file updates from parent (RscFileBridge) via postMessage\n// 3. Sends all user files to Worker for directive detection + compilation\n// 4. Worker compiles with Sucrase + executes, sends back Flight chunks\n// 5. Renders the Flight stream result with React\n\n// Minimal webpack shim for RSDW compatibility.\n// Works in both browser (window) and worker (self) contexts via globalThis.\n\nimport * as React from 'react';\nimport * as ReactJSXRuntime from 'react/jsx-runtime';\nimport * as ReactJSXDevRuntime from 'react/jsx-dev-runtime';\nimport {useState, startTransition, use} from 'react';\nimport {jsx} from 'react/jsx-runtime';\nimport {createRoot} from 'react-dom/client';\n\nimport rscServerForWorker from './rsc-server.js';\n\nimport './__webpack_shim__';\nimport {\n  createFromReadableStream,\n  encodeReply,\n} from 'react-server-dom-webpack/client.browser';\n\nimport {\n  injectIntoGlobalHook,\n  register as refreshRegister,\n  performReactRefresh,\n  isLikelyComponentType,\n} from 'react-refresh/runtime';\n\n// Patch the DevTools hook to capture renderer helpers and track roots.\n// Must run after react-dom evaluates (injects renderer) but before createRoot().\ninjectIntoGlobalHook(window);\n\nexport function initClient() {\n  // Create Worker from pre-bundled server runtime\n  var blob = new Blob([rscServerForWorker], {type: 'application/javascript'});\n  var workerUrl = URL.createObjectURL(blob);\n  var worker = new Worker(workerUrl);\n\n  // Render tracking\n  var nextStreamId = 0;\n  var chunkControllers = {};\n  var setCurrentPromise;\n  var firstRender = true;\n  var workerReady = false;\n  var pendingFiles = null;\n\n  function Root({initialPromise}) {\n    var _state = useState(initialPromise);\n    setCurrentPromise = _state[1];\n    return use(_state[0]);\n  }\n\n  // Set up React root\n  var initialResolve;\n  var initialPromise = new Promise(function (resolve) {\n    initialResolve = resolve;\n  });\n\n  var rootEl = document.getElementById('root');\n  if (!rootEl) throw new Error('#root element not found');\n  var root = createRoot(rootEl, {\n    onUncaughtError: function (error) {\n      var msg =\n        error && error.digest\n          ? error.digest\n          : error && error.message\n          ? error.message\n          : String(error);\n      console.error(msg);\n      showError(msg);\n    },\n  });\n  startTransition(function () {\n    root.render(jsx(Root, {initialPromise: initialPromise}));\n  });\n\n  // Error overlay — rendered inside the iframe via DOM so it doesn't\n  // interfere with Sandpack's parent-frame message protocol.\n  function showError(message) {\n    var overlay = document.getElementById('rsc-error-overlay');\n    if (!overlay) {\n      overlay = document.createElement('div');\n      overlay.id = 'rsc-error-overlay';\n      overlay.style.cssText =\n        'background:#fff;border:2px solid #c00;border-radius:8px;padding:16px;margin:20px;font-family:sans-serif;';\n      var heading = document.createElement('h2');\n      heading.style.cssText = 'color:#c00;margin:0 0 8px;font-size:16px;';\n      heading.textContent = 'Server Error';\n      overlay.appendChild(heading);\n      var pre = document.createElement('pre');\n      pre.style.cssText =\n        'margin:0;white-space:pre-wrap;word-break:break-word;font-size:14px;color:#222;';\n      overlay.appendChild(pre);\n      rootEl.parentNode.insertBefore(overlay, rootEl);\n    }\n    overlay.lastChild.textContent = message;\n    overlay.style.display = '';\n    rootEl.style.display = 'none';\n  }\n\n  function clearError() {\n    var overlay = document.getElementById('rsc-error-overlay');\n    if (overlay) overlay.style.display = 'none';\n    rootEl.style.display = '';\n  }\n\n  // Worker message handler\n  worker.onmessage = function (e) {\n    var msg = e.data;\n    if (msg.type === 'ready') {\n      workerReady = true;\n      if (pendingFiles) {\n        processFiles(pendingFiles);\n        pendingFiles = null;\n      }\n    } else if (msg.type === 'deploy-result') {\n      clearError();\n      // Register compiled client modules in the webpack cache before rendering\n      if (msg.result && msg.result.compiledClients) {\n        registerClientModules(\n          msg.result.compiledClients,\n          msg.result.clientEntries || {}\n        );\n      }\n      triggerRender();\n    } else if (msg.type === 'rsc-chunk') {\n      handleChunk(msg);\n    } else if (msg.type === 'rsc-error') {\n      showError(msg.error);\n    }\n  };\n\n  function callServer(id, args) {\n    return encodeReply(args).then(function (body) {\n      nextStreamId++;\n      var reqId = nextStreamId;\n\n      var stream = new ReadableStream({\n        start: function (controller) {\n          chunkControllers[reqId] = controller;\n        },\n      });\n\n      // FormData is not structured-cloneable for postMessage.\n      // Serialize to an array of entries; the worker reconstructs it.\n      var encodedArgs;\n      if (typeof body === 'string') {\n        encodedArgs = body;\n      } else {\n        var entries = [];\n        body.forEach(function (value, key) {\n          entries.push([key, value]);\n        });\n        encodedArgs = {__formData: entries};\n      }\n\n      worker.postMessage({\n        type: 'callAction',\n        requestId: reqId,\n        actionId: id,\n        encodedArgs: encodedArgs,\n      });\n\n      var response = createFromReadableStream(stream, {\n        callServer: callServer,\n      });\n\n      // Update UI with re-rendered root\n      startTransition(function () {\n        updateUI(\n          Promise.resolve(response).then(function (v) {\n            return v.root;\n          })\n        );\n      });\n\n      // Return action's return value (for useActionState support)\n      return Promise.resolve(response).then(function (v) {\n        return v.returnValue;\n      });\n    });\n  }\n\n  function triggerRender() {\n    // Close any in-flight streams from previous renders\n    Object.keys(chunkControllers).forEach(function (id) {\n      try {\n        chunkControllers[id].close();\n      } catch (e) {}\n      delete chunkControllers[id];\n    });\n\n    nextStreamId++;\n    var reqId = nextStreamId;\n\n    var stream = new ReadableStream({\n      start: function (controller) {\n        chunkControllers[reqId] = controller;\n      },\n    });\n\n    worker.postMessage({type: 'render', requestId: reqId});\n\n    var promise = createFromReadableStream(stream, {\n      callServer: callServer,\n    });\n\n    updateUI(promise);\n  }\n\n  function handleChunk(msg) {\n    var ctrl = chunkControllers[msg.requestId];\n    if (!ctrl) return;\n    if (msg.done) {\n      ctrl.close();\n      delete chunkControllers[msg.requestId];\n    } else {\n      ctrl.enqueue(msg.chunk);\n    }\n  }\n\n  function updateUI(promise) {\n    if (firstRender) {\n      firstRender = false;\n      if (initialResolve) initialResolve(promise);\n    } else {\n      startTransition(function () {\n        if (setCurrentPromise) setCurrentPromise(promise);\n      });\n    }\n  }\n\n  // File update handler — receives raw file contents from RscFileBridge\n  window.addEventListener('message', function (e) {\n    var msg = e.data;\n    if (msg.type !== 'rsc-file-update') return;\n    if (!workerReady) {\n      pendingFiles = msg.files;\n      return;\n    }\n    processFiles(msg.files);\n  });\n\n  function processFiles(files) {\n    console.clear();\n    var userFiles = {};\n    Object.keys(files).forEach(function (filePath) {\n      if (!filePath.match(/\\.(js|jsx|ts|tsx)$/)) return;\n      if (filePath.indexOf('node_modules') !== -1) return;\n      if (filePath === '/src/index.js') return;\n      if (filePath === '/src/rsc-client.js') return;\n      if (filePath === '/src/rsc-server.js') return;\n      if (filePath === '/src/__webpack_shim__.js') return;\n      if (filePath === '/src/__react_refresh_init__.js') return;\n      userFiles[filePath] = files[filePath];\n    });\n    worker.postMessage({\n      type: 'deploy',\n      files: userFiles,\n    });\n  }\n\n  // Resolve relative paths (e.g., './Button' from '/src/Counter.js' → '/src/Button')\n  function resolvePath(from, to) {\n    if (!to.startsWith('.')) return to;\n    var parts = from.split('/');\n    parts.pop();\n    var toParts = to.split('/');\n    for (var i = 0; i < toParts.length; i++) {\n      if (toParts[i] === '.') continue;\n      if (toParts[i] === '..') {\n        parts.pop();\n        continue;\n      }\n      parts.push(toParts[i]);\n    }\n    return parts.join('/');\n  }\n\n  // Evaluate compiled client modules and register them in the webpack cache\n  // so RSDW client can resolve them via __webpack_require__.\n  function registerClientModules(compiledClients, clientEntries) {\n    // Clear stale client modules from previous deploys\n    Object.keys(globalThis.__webpack_module_cache__).forEach(function (key) {\n      delete globalThis.__webpack_module_cache__[key];\n    });\n\n    // Store all compiled code for lazy evaluation\n    var allCompiled = compiledClients;\n\n    function evaluateModule(moduleId) {\n      if (globalThis.__webpack_module_cache__[moduleId]) {\n        var cached = globalThis.__webpack_module_cache__[moduleId];\n        return cached.exports !== undefined ? cached.exports : cached;\n      }\n      var code = allCompiled[moduleId];\n      if (!code)\n        throw new Error('Client require: module \"' + moduleId + '\" not found');\n\n      var mod = {exports: {}};\n      // Register before evaluating to handle circular deps\n      var cacheEntry = {exports: mod.exports};\n      globalThis.__webpack_module_cache__[moduleId] = cacheEntry;\n\n      var clientRequire = function (id) {\n        if (id === 'react') return React;\n        if (id === 'react/jsx-runtime') return ReactJSXRuntime;\n        if (id === 'react/jsx-dev-runtime') return ReactJSXDevRuntime;\n        if (id.endsWith('.css')) return {};\n        var resolvedId = id.startsWith('.') ? resolvePath(moduleId, id) : id;\n        var candidates = [resolvedId];\n        var exts = ['.js', '.jsx', '.ts', '.tsx'];\n        for (var i = 0; i < exts.length; i++) {\n          candidates.push(resolvedId + exts[i]);\n        }\n        for (var j = 0; j < candidates.length; j++) {\n          if (\n            allCompiled[candidates[j]] ||\n            globalThis.__webpack_module_cache__[candidates[j]]\n          ) {\n            return evaluateModule(candidates[j]);\n          }\n        }\n        throw new Error('Client require: module \"' + id + '\" not found');\n      };\n\n      try {\n        new Function('module', 'exports', 'require', 'React', code)(\n          mod,\n          mod.exports,\n          clientRequire,\n          React\n        );\n      } catch (err) {\n        console.error('Error executing client module ' + moduleId + ':', err);\n        return mod.exports;\n      }\n      // Update the SAME cache entry's exports (don't replace the wrapper)\n      cacheEntry.exports = mod.exports;\n      return mod.exports;\n    }\n\n    // Eagerly evaluate 'use client' entry points; their imports resolve lazily\n    Object.keys(clientEntries).forEach(function (moduleId) {\n      evaluateModule(moduleId);\n    });\n\n    // Register all evaluated components with react-refresh for Fast Refresh.\n    // This creates stable \"component families\" so React can preserve state\n    // across re-evaluations when component identity changes.\n    Object.keys(globalThis.__webpack_module_cache__).forEach(function (\n      moduleId\n    ) {\n      var moduleExports = globalThis.__webpack_module_cache__[moduleId];\n      var exports =\n        moduleExports.exports !== undefined\n          ? moduleExports.exports\n          : moduleExports;\n      if (exports && typeof exports === 'object') {\n        for (var key in exports) {\n          var exportValue = exports[key];\n          if (isLikelyComponentType(exportValue)) {\n            refreshRegister(exportValue, moduleId + ' %exports% ' + key);\n          }\n        }\n      }\n      if (typeof exports === 'function' && isLikelyComponentType(exports)) {\n        refreshRegister(exports, moduleId + ' %exports% default');\n      }\n    });\n\n    // Tell React about updated component families so it can\n    // preserve state for components whose implementation changed.\n    performReactRefresh();\n  }\n}\n"
  },
  {
    "path": "src/components/MDX/Sandpack/sandpack-rsc/sandbox-code/src/rsc-server.js",
    "content": "// Server Worker for RSC Sandboxes\n// Runs inside a Blob URL Web Worker.\n// Pre-bundled by esbuild with React (server build), react-server-dom-webpack/server.browser, and Sucrase.\n\n// IMPORTANT\n// If this file changes, run:\n//   yarn prebuild:rsc\n\nvar React = require('react');\nvar ReactJSXRuntime = require('react/jsx-runtime');\nvar RSDWServer = require('react-server-dom-webpack/server.browser');\nvar Sucrase = require('sucrase');\nvar acorn = require('acorn-loose');\n\nvar deployed = null;\n\n// Module map proxy: generates module references on-demand for client components.\n// When server code imports a 'use client' file, it gets a proxy reference\n// that serializes into the Flight stream.\nfunction createModuleMap() {\n  return new Proxy(\n    {},\n    {\n      get: function (target, key) {\n        if (key in target) return target[key];\n        var parts = String(key).split('#');\n        var moduleId = parts[0];\n        var exportName = parts[1] || 'default';\n        var entry = {\n          id: moduleId,\n          chunks: [moduleId],\n          name: exportName,\n          async: true,\n        };\n        target[key] = entry;\n        return entry;\n      },\n    }\n  );\n}\n\n// Server actions registry\nvar serverActionsRegistry = {};\n\nfunction registerServerReference(impl, moduleId, name) {\n  var ref = RSDWServer.registerServerReference(impl, moduleId, name);\n  var id = moduleId + '#' + name;\n  serverActionsRegistry[id] = impl;\n  return ref;\n}\n\n// Detect 'use client' / 'use server' directives using acorn-loose,\n// matching the same approach as react-server-dom-webpack/node-register.\nfunction parseDirective(code) {\n  if (code.indexOf('use client') === -1 && code.indexOf('use server') === -1) {\n    return null;\n  }\n  try {\n    var body = acorn.parse(code, {\n      ecmaVersion: '2024',\n      sourceType: 'source',\n    }).body;\n  } catch (x) {\n    return null;\n  }\n  for (var i = 0; i < body.length; i++) {\n    var node = body[i];\n    if (node.type !== 'ExpressionStatement' || !node.directive) break;\n    if (node.directive === 'use client') return 'use client';\n    if (node.directive === 'use server') return 'use server';\n  }\n  return null;\n}\n\n// Transform inline 'use server' functions (inside function bodies) into\n// registered server references. Module-level 'use server' is handled\n// separately by executeModule.\nfunction transformInlineServerActions(code) {\n  if (code.indexOf('use server') === -1) return code;\n  var ast;\n  try {\n    ast = acorn.parse(code, {ecmaVersion: '2024', sourceType: 'source'});\n  } catch (x) {\n    return code;\n  }\n\n  var edits = [];\n  var counter = 0;\n\n  function visit(node, fnDepth) {\n    if (!node || typeof node !== 'object') return;\n    var isFn =\n      node.type === 'FunctionDeclaration' ||\n      node.type === 'FunctionExpression' ||\n      node.type === 'ArrowFunctionExpression';\n\n    // Only look for 'use server' inside nested functions (fnDepth > 0)\n    if (\n      isFn &&\n      fnDepth > 0 &&\n      node.body &&\n      node.body.type === 'BlockStatement'\n    ) {\n      var body = node.body.body;\n      for (var s = 0; s < body.length; s++) {\n        var stmt = body[s];\n        if (stmt.type !== 'ExpressionStatement') break;\n        if (stmt.directive === 'use server') {\n          edits.push({\n            funcStart: node.start,\n            funcEnd: node.end,\n            dStart: stmt.start,\n            dEnd: stmt.end,\n            name: node.id ? node.id.name : 'action' + counter,\n            isDecl: node.type === 'FunctionDeclaration',\n          });\n          counter++;\n          return; // don't recurse into this function\n        }\n        if (!stmt.directive) break;\n      }\n    }\n\n    var nextDepth = isFn ? fnDepth + 1 : fnDepth;\n    for (var key in node) {\n      if (key === 'start' || key === 'end' || key === 'type') continue;\n      var child = node[key];\n      if (Array.isArray(child)) {\n        for (var i = 0; i < child.length; i++) {\n          if (child[i] && typeof child[i].type === 'string') {\n            visit(child[i], nextDepth);\n          }\n        }\n      } else if (child && typeof child.type === 'string') {\n        visit(child, nextDepth);\n      }\n    }\n  }\n\n  ast.body.forEach(function (stmt) {\n    visit(stmt, 0);\n  });\n  if (edits.length === 0) return code;\n\n  // Apply in reverse order to preserve positions\n  edits.sort(function (a, b) {\n    return b.funcStart - a.funcStart;\n  });\n\n  var result = code;\n  for (var i = 0; i < edits.length; i++) {\n    var e = edits[i];\n    // Remove the 'use server' directive + trailing whitespace\n    var dEnd = e.dEnd;\n    var ch = result.charAt(dEnd);\n    while (\n      dEnd < result.length &&\n      (ch === ' ' || ch === '\\n' || ch === '\\r' || ch === '\\t')\n    ) {\n      dEnd++;\n      ch = result.charAt(dEnd);\n    }\n    result = result.slice(0, e.dStart) + result.slice(dEnd);\n    var removed = dEnd - e.dStart;\n    var adjEnd = e.funcEnd - removed;\n\n    // Wrap function with __rsa (register server action)\n    var funcCode = result.slice(e.funcStart, adjEnd);\n    if (e.isDecl) {\n      // async function foo() { ... } →\n      // var foo = __rsa(async function foo() { ... }, 'foo');\n      result =\n        result.slice(0, e.funcStart) +\n        'var ' +\n        e.name +\n        ' = __rsa(' +\n        funcCode +\n        \", '\" +\n        e.name +\n        \"');\" +\n        result.slice(adjEnd);\n    } else {\n      // expression/arrow: just wrap in __rsa(...)\n      result =\n        result.slice(0, e.funcStart) +\n        '__rsa(' +\n        funcCode +\n        \", '\" +\n        e.name +\n        \"')\" +\n        result.slice(adjEnd);\n    }\n  }\n\n  return result;\n}\n\n// Resolve relative paths (e.g., './Counter.js' from '/src/App.js' → '/src/Counter.js')\nfunction resolvePath(from, to) {\n  if (!to.startsWith('.')) return to;\n  var parts = from.split('/');\n  parts.pop(); // remove filename\n  var toParts = to.split('/');\n  for (var i = 0; i < toParts.length; i++) {\n    if (toParts[i] === '.') continue;\n    if (toParts[i] === '..') {\n      parts.pop();\n      continue;\n    }\n    parts.push(toParts[i]);\n  }\n  return parts.join('/');\n}\n\n// Deploy new server code into the Worker\n// Receives raw source files — compiles them with Sucrase before execution.\nfunction deploy(files) {\n  serverActionsRegistry = {};\n\n  // Build a require function for the server module scope\n  var modules = {\n    react: React,\n    'react/jsx-runtime': ReactJSXRuntime,\n  };\n\n  // Compile all files first, then execute on-demand via require.\n  // This avoids ordering issues where a file imports another that hasn't been executed yet.\n  var compiled = {};\n  var compileError = null;\n  Object.keys(files).forEach(function (filePath) {\n    if (compileError) return;\n    try {\n      compiled[filePath] = Sucrase.transform(files[filePath], {\n        transforms: ['jsx', 'imports'],\n        jsxRuntime: 'automatic',\n        production: true,\n      }).code;\n    } catch (err) {\n      compileError = filePath + ': ' + (err.message || String(err));\n    }\n  });\n\n  if (compileError) {\n    return {type: 'error', error: compileError};\n  }\n\n  // Resolve a module id relative to a requesting file\n  function resolveModuleId(from, id) {\n    if (modules[id]) return id;\n    if (id.startsWith('.')) {\n      var resolved = resolvePath(from, id);\n      if (modules[resolved] || compiled[resolved]) return resolved;\n      var exts = ['.js', '.jsx', '.ts', '.tsx'];\n      for (var ei = 0; ei < exts.length; ei++) {\n        var withExt = resolved + exts[ei];\n        if (modules[withExt] || compiled[withExt]) return withExt;\n      }\n    }\n    return id;\n  }\n\n  // Execute a module lazily and cache its exports\n  var executing = {};\n  var detectedClientFiles = {};\n\n  function executeModule(filePath) {\n    if (modules[filePath]) return modules[filePath];\n    if (!compiled[filePath]) {\n      throw new Error('Module \"' + filePath + '\" not found');\n    }\n    if (executing[filePath]) {\n      // Circular dependency — return partially populated exports\n      return executing[filePath].exports;\n    }\n\n    // Replicate node-register's _compile hook:\n    // detect directives BEFORE executing the module.\n    var directive = parseDirective(files[filePath]);\n\n    if (directive === 'use client') {\n      // Don't execute — return a client module proxy (same as node-register)\n      modules[filePath] = RSDWServer.createClientModuleProxy(filePath);\n      detectedClientFiles[filePath] = true;\n      return modules[filePath];\n    }\n\n    var mod = {exports: {}};\n    executing[filePath] = mod;\n\n    var localRequire = function (id) {\n      if (id.endsWith('.css')) return {};\n      var resolved = resolveModuleId(filePath, id);\n      if (modules[resolved]) return modules[resolved];\n      return executeModule(resolved);\n    };\n\n    // Transform inline 'use server' functions before execution\n    var codeToExecute = compiled[filePath];\n    if (directive !== 'use server') {\n      codeToExecute = transformInlineServerActions(codeToExecute);\n    }\n\n    new Function(\n      'module',\n      'exports',\n      'require',\n      'React',\n      '__rsa',\n      codeToExecute\n    )(mod, mod.exports, localRequire, React, function (fn, name) {\n      return registerServerReference(fn, filePath, name);\n    });\n\n    modules[filePath] = mod.exports;\n\n    if (directive === 'use server') {\n      // Execute normally, then register server references (same as node-register)\n      var exportNames = Object.keys(mod.exports);\n      for (var i = 0; i < exportNames.length; i++) {\n        var name = exportNames[i];\n        if (typeof mod.exports[name] === 'function') {\n          registerServerReference(mod.exports[name], filePath, name);\n        }\n      }\n    }\n\n    delete executing[filePath];\n    return mod.exports;\n  }\n\n  // Execute all files (order no longer matters — require triggers lazy execution)\n  var mainModule = {exports: {}};\n  Object.keys(compiled).forEach(function (filePath) {\n    executeModule(filePath);\n    if (\n      filePath === '/src/App.js' ||\n      filePath === './App.js' ||\n      filePath === './src/App.js'\n    ) {\n      mainModule.exports = modules[filePath];\n    }\n  });\n\n  deployed = {\n    module: mainModule.exports,\n  };\n\n  // Collect only client-reachable compiled code.\n  // Start from 'use client' entries and trace their require() calls.\n  var clientReachable = {};\n  function traceClientDeps(filePath) {\n    if (clientReachable[filePath]) return;\n    clientReachable[filePath] = true;\n    var code = compiled[filePath];\n    if (!code) return;\n    var requireRegex = /require\\([\"']([^\"']+)[\"']\\)/g;\n    var match;\n    while ((match = requireRegex.exec(code)) !== null) {\n      var dep = match[1];\n      if (\n        dep === 'react' ||\n        dep === 'react/jsx-runtime' ||\n        dep === 'react/jsx-dev-runtime' ||\n        dep.endsWith('.css')\n      )\n        continue;\n      var resolved = resolveModuleId(filePath, dep);\n      if (compiled[resolved]) {\n        traceClientDeps(resolved);\n      }\n    }\n  }\n  Object.keys(detectedClientFiles).forEach(function (filePath) {\n    traceClientDeps(filePath);\n  });\n\n  var clientCompiled = {};\n  Object.keys(clientReachable).forEach(function (filePath) {\n    clientCompiled[filePath] = compiled[filePath];\n  });\n\n  return {\n    type: 'deployed',\n    compiledClients: clientCompiled,\n    clientEntries: detectedClientFiles,\n  };\n}\n\n// Render the deployed app to a Flight stream\nfunction render() {\n  if (!deployed) throw new Error('No code deployed');\n  var App = deployed.module.default || deployed.module;\n  var element = React.createElement(App);\n  return RSDWServer.renderToReadableStream(element, createModuleMap(), {\n    onError: function (err) {\n      console.error('[RSC Server Error]', err);\n      return msg;\n    },\n  });\n}\n\n// Execute a server action and re-render\nfunction callAction(actionId, encodedArgs) {\n  if (!deployed) throw new Error('No code deployed');\n  var action = serverActionsRegistry[actionId];\n  if (!action) throw new Error('Action \"' + actionId + '\" not found');\n  // Reconstruct FormData from serialized entries (postMessage can't clone FormData)\n  var decoded = encodedArgs;\n  if (\n    typeof encodedArgs !== 'string' &&\n    encodedArgs &&\n    encodedArgs.__formData\n  ) {\n    decoded = new FormData();\n    for (var i = 0; i < encodedArgs.__formData.length; i++) {\n      decoded.append(\n        encodedArgs.__formData[i][0],\n        encodedArgs.__formData[i][1]\n      );\n    }\n  }\n  return Promise.resolve(RSDWServer.decodeReply(decoded)).then(function (args) {\n    var resultPromise = Promise.resolve(action.apply(null, args));\n    return resultPromise.then(function () {\n      var App = deployed.module.default || deployed.module;\n      return RSDWServer.renderToReadableStream(\n        {root: React.createElement(App), returnValue: resultPromise},\n        createModuleMap(),\n        {\n          onError: function (err) {\n            console.error('[RSC Server Error]', err);\n            return msg;\n          },\n        }\n      );\n    });\n  });\n}\n\n// Stream chunks back to the main thread via postMessage\nfunction sendStream(requestId, stream) {\n  var reader = stream.getReader();\n  function pump() {\n    return reader.read().then(function (result) {\n      if (result.done) {\n        self.postMessage({type: 'rsc-chunk', requestId: requestId, done: true});\n        return;\n      }\n      self.postMessage(\n        {\n          type: 'rsc-chunk',\n          requestId: requestId,\n          done: false,\n          chunk: result.value,\n        },\n        [result.value.buffer]\n      );\n      return pump();\n    });\n  }\n  pump().catch(function (err) {\n    self.postMessage({\n      type: 'rsc-error',\n      requestId: requestId,\n      error: String(err),\n    });\n  });\n}\n\n// RPC message handler\nself.onmessage = function (e) {\n  var msg = e.data;\n  if (msg.type === 'deploy') {\n    try {\n      var result = deploy(msg.files);\n      if (result && result.type === 'error') {\n        self.postMessage({\n          type: 'rsc-error',\n          error: result.error,\n        });\n      } else if (result) {\n        self.postMessage({\n          type: 'deploy-result',\n          result: result,\n        });\n      }\n    } catch (err) {\n      self.postMessage({\n        type: 'rsc-error',\n        error: String(err),\n      });\n    }\n  } else if (msg.type === 'render') {\n    try {\n      var streamPromise = render();\n      Promise.resolve(streamPromise)\n        .then(function (stream) {\n          sendStream(msg.requestId, stream);\n        })\n        .catch(function (err) {\n          self.postMessage({\n            type: 'rsc-error',\n            requestId: msg.requestId,\n            error: String(err),\n          });\n        });\n    } catch (err) {\n      self.postMessage({\n        type: 'rsc-error',\n        requestId: msg.requestId,\n        error: String(err),\n      });\n    }\n  } else if (msg.type === 'callAction') {\n    try {\n      callAction(msg.actionId, msg.encodedArgs)\n        .then(function (stream) {\n          sendStream(msg.requestId, stream);\n        })\n        .catch(function (err) {\n          self.postMessage({\n            type: 'rsc-error',\n            requestId: msg.requestId,\n            error: String(err),\n          });\n        });\n    } catch (err) {\n      self.postMessage({\n        type: 'rsc-error',\n        requestId: msg.requestId,\n        error: String(err),\n      });\n    }\n  }\n};\n\nself.postMessage({type: 'ready'});\n"
  },
  {
    "path": "src/components/MDX/Sandpack/sandpack-rsc/sandbox-code/src/webpack-shim.js",
    "content": "// Minimal webpack shim for RSDW compatibility.\n// Works in both browser (window) and worker (self) contexts via globalThis.\n\nvar moduleCache = {};\n\nglobalThis.__webpack_module_cache__ = moduleCache;\n\nglobalThis.__webpack_require__ = function (moduleId) {\n  var cached = moduleCache[moduleId];\n  if (cached) return cached.exports !== undefined ? cached.exports : cached;\n  throw new Error('Module \"' + moduleId + '\" not found in webpack shim cache');\n};\n\nglobalThis.__webpack_chunk_load__ = function () {\n  return Promise.resolve();\n};\n\nglobalThis.__webpack_require__.u = function (chunkId) {\n  return chunkId;\n};\n\nglobalThis.__webpack_get_script_filename__ = function (chunkId) {\n  return chunkId;\n};\n"
  },
  {
    "path": "src/components/MDX/Sandpack/sandpack-rsc/sandbox-code/src/worker-bundle.dist.js",
    "content": "// Minimal webpack shim for RSDW compatibility.\n// Works in both browser (window) and worker (self) contexts via globalThis.\n\nvar moduleCache = {};\n\nglobalThis.__webpack_module_cache__ = moduleCache;\n\nglobalThis.__webpack_require__ = function (moduleId) {\n  var cached = moduleCache[moduleId];\n  if (cached) return cached.exports !== undefined ? cached.exports : cached;\n  throw new Error('Module \"' + moduleId + '\" not found in webpack shim cache');\n};\n\nglobalThis.__webpack_chunk_load__ = function () {\n  return Promise.resolve();\n};\n\nglobalThis.__webpack_require__.u = function (chunkId) {\n  return chunkId;\n};\n\nglobalThis.__webpack_get_script_filename__ = function (chunkId) {\n  return chunkId;\n};\n\n('use strict');\n(() => {\n  var Z = (e, t) => () => (t || e((t = {exports: {}}).exports, t), t.exports);\n  var Wc = Z((ht) => {\n    'use strict';\n    var ei = {H: null, A: null};\n    function Yo(e) {\n      var t = 'https://react.dev/errors/' + e;\n      if (1 < arguments.length) {\n        t += '?args[]=' + encodeURIComponent(arguments[1]);\n        for (var s = 2; s < arguments.length; s++)\n          t += '&args[]=' + encodeURIComponent(arguments[s]);\n      }\n      return (\n        'Minified React error #' +\n        e +\n        '; visit ' +\n        t +\n        ' for the full message or use the non-minified dev environment for full errors and additional helpful warnings.'\n      );\n    }\n    var jc = Array.isArray,\n      Jo = Symbol.for('react.transitional.element'),\n      Af = Symbol.for('react.portal'),\n      Pf = Symbol.for('react.fragment'),\n      Nf = Symbol.for('react.strict_mode'),\n      Rf = Symbol.for('react.profiler'),\n      Lf = Symbol.for('react.forward_ref'),\n      Of = Symbol.for('react.suspense'),\n      Df = Symbol.for('react.memo'),\n      Uc = Symbol.for('react.lazy'),\n      $c = Symbol.iterator;\n    function Mf(e) {\n      return e === null || typeof e != 'object'\n        ? null\n        : ((e = ($c && e[$c]) || e['@@iterator']),\n          typeof e == 'function' ? e : null);\n    }\n    var Hc = Object.prototype.hasOwnProperty,\n      Ff = Object.assign;\n    function Qo(e, t, s, i, r, a) {\n      return (\n        (s = a.ref),\n        {$$typeof: Jo, type: e, key: t, ref: s !== void 0 ? s : null, props: a}\n      );\n    }\n    function Bf(e, t) {\n      return Qo(e.type, t, void 0, void 0, void 0, e.props);\n    }\n    function Zo(e) {\n      return typeof e == 'object' && e !== null && e.$$typeof === Jo;\n    }\n    function Vf(e) {\n      var t = {'=': '=0', ':': '=2'};\n      return (\n        '$' +\n        e.replace(/[=:]/g, function (s) {\n          return t[s];\n        })\n      );\n    }\n    var qc = /\\/+/g;\n    function zo(e, t) {\n      return typeof e == 'object' && e !== null && e.key != null\n        ? Vf('' + e.key)\n        : t.toString(36);\n    }\n    function Kc() {}\n    function jf(e) {\n      switch (e.status) {\n        case 'fulfilled':\n          return e.value;\n        case 'rejected':\n          throw e.reason;\n        default:\n          switch (\n            (typeof e.status == 'string'\n              ? e.then(Kc, Kc)\n              : ((e.status = 'pending'),\n                e.then(\n                  function (t) {\n                    e.status === 'pending' &&\n                      ((e.status = 'fulfilled'), (e.value = t));\n                  },\n                  function (t) {\n                    e.status === 'pending' &&\n                      ((e.status = 'rejected'), (e.reason = t));\n                  }\n                )),\n            e.status)\n          ) {\n            case 'fulfilled':\n              return e.value;\n            case 'rejected':\n              throw e.reason;\n          }\n      }\n      throw e;\n    }\n    function Zs(e, t, s, i, r) {\n      var a = typeof e;\n      (a === 'undefined' || a === 'boolean') && (e = null);\n      var u = !1;\n      if (e === null) u = !0;\n      else\n        switch (a) {\n          case 'bigint':\n          case 'string':\n          case 'number':\n            u = !0;\n            break;\n          case 'object':\n            switch (e.$$typeof) {\n              case Jo:\n              case Af:\n                u = !0;\n                break;\n              case Uc:\n                return (u = e._init), Zs(u(e._payload), t, s, i, r);\n            }\n        }\n      if (u)\n        return (\n          (r = r(e)),\n          (u = i === '' ? '.' + zo(e, 0) : i),\n          jc(r)\n            ? ((s = ''),\n              u != null && (s = u.replace(qc, '$&/') + '/'),\n              Zs(r, t, s, '', function (g) {\n                return g;\n              }))\n            : r != null &&\n              (Zo(r) &&\n                (r = Bf(\n                  r,\n                  s +\n                    (r.key == null || (e && e.key === r.key)\n                      ? ''\n                      : ('' + r.key).replace(qc, '$&/') + '/') +\n                    u\n                )),\n              t.push(r)),\n          1\n        );\n      u = 0;\n      var d = i === '' ? '.' : i + ':';\n      if (jc(e))\n        for (var y = 0; y < e.length; y++)\n          (i = e[y]), (a = d + zo(i, y)), (u += Zs(i, t, s, a, r));\n      else if (((y = Mf(e)), typeof y == 'function'))\n        for (e = y.call(e), y = 0; !(i = e.next()).done; )\n          (i = i.value), (a = d + zo(i, y++)), (u += Zs(i, t, s, a, r));\n      else if (a === 'object') {\n        if (typeof e.then == 'function') return Zs(jf(e), t, s, i, r);\n        throw (\n          ((t = String(e)),\n          Error(\n            Yo(\n              31,\n              t === '[object Object]'\n                ? 'object with keys {' + Object.keys(e).join(', ') + '}'\n                : t\n            )\n          ))\n        );\n      }\n      return u;\n    }\n    function _r(e, t, s) {\n      if (e == null) return e;\n      var i = [],\n        r = 0;\n      return (\n        Zs(e, i, '', '', function (a) {\n          return t.call(s, a, r++);\n        }),\n        i\n      );\n    }\n    function $f(e) {\n      if (e._status === -1) {\n        var t = e._result;\n        (t = t()),\n          t.then(\n            function (s) {\n              (e._status === 0 || e._status === -1) &&\n                ((e._status = 1), (e._result = s));\n            },\n            function (s) {\n              (e._status === 0 || e._status === -1) &&\n                ((e._status = 2), (e._result = s));\n            }\n          ),\n          e._status === -1 && ((e._status = 0), (e._result = t));\n      }\n      if (e._status === 1) return e._result.default;\n      throw e._result;\n    }\n    function qf() {\n      return new WeakMap();\n    }\n    function Xo() {\n      return {s: 0, v: void 0, o: null, p: null};\n    }\n    ht.Children = {\n      map: _r,\n      forEach: function (e, t, s) {\n        _r(\n          e,\n          function () {\n            t.apply(this, arguments);\n          },\n          s\n        );\n      },\n      count: function (e) {\n        var t = 0;\n        return (\n          _r(e, function () {\n            t++;\n          }),\n          t\n        );\n      },\n      toArray: function (e) {\n        return (\n          _r(e, function (t) {\n            return t;\n          }) || []\n        );\n      },\n      only: function (e) {\n        if (!Zo(e)) throw Error(Yo(143));\n        return e;\n      },\n    };\n    ht.Fragment = Pf;\n    ht.Profiler = Rf;\n    ht.StrictMode = Nf;\n    ht.Suspense = Of;\n    ht.__SERVER_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE = ei;\n    ht.cache = function (e) {\n      return function () {\n        var t = ei.A;\n        if (!t) return e.apply(null, arguments);\n        var s = t.getCacheForType(qf);\n        (t = s.get(e)), t === void 0 && ((t = Xo()), s.set(e, t)), (s = 0);\n        for (var i = arguments.length; s < i; s++) {\n          var r = arguments[s];\n          if (typeof r == 'function' || (typeof r == 'object' && r !== null)) {\n            var a = t.o;\n            a === null && (t.o = a = new WeakMap()),\n              (t = a.get(r)),\n              t === void 0 && ((t = Xo()), a.set(r, t));\n          } else\n            (a = t.p),\n              a === null && (t.p = a = new Map()),\n              (t = a.get(r)),\n              t === void 0 && ((t = Xo()), a.set(r, t));\n        }\n        if (t.s === 1) return t.v;\n        if (t.s === 2) throw t.v;\n        try {\n          var u = e.apply(null, arguments);\n          return (s = t), (s.s = 1), (s.v = u);\n        } catch (d) {\n          throw ((u = t), (u.s = 2), (u.v = d), d);\n        }\n      };\n    };\n    ht.cloneElement = function (e, t, s) {\n      if (e == null) throw Error(Yo(267, e));\n      var i = Ff({}, e.props),\n        r = e.key,\n        a = void 0;\n      if (t != null)\n        for (u in (t.ref !== void 0 && (a = void 0),\n        t.key !== void 0 && (r = '' + t.key),\n        t))\n          !Hc.call(t, u) ||\n            u === 'key' ||\n            u === '__self' ||\n            u === '__source' ||\n            (u === 'ref' && t.ref === void 0) ||\n            (i[u] = t[u]);\n      var u = arguments.length - 2;\n      if (u === 1) i.children = s;\n      else if (1 < u) {\n        for (var d = Array(u), y = 0; y < u; y++) d[y] = arguments[y + 2];\n        i.children = d;\n      }\n      return Qo(e.type, r, void 0, void 0, a, i);\n    };\n    ht.createElement = function (e, t, s) {\n      var i,\n        r = {},\n        a = null;\n      if (t != null)\n        for (i in (t.key !== void 0 && (a = '' + t.key), t))\n          Hc.call(t, i) &&\n            i !== 'key' &&\n            i !== '__self' &&\n            i !== '__source' &&\n            (r[i] = t[i]);\n      var u = arguments.length - 2;\n      if (u === 1) r.children = s;\n      else if (1 < u) {\n        for (var d = Array(u), y = 0; y < u; y++) d[y] = arguments[y + 2];\n        r.children = d;\n      }\n      if (e && e.defaultProps)\n        for (i in ((u = e.defaultProps), u)) r[i] === void 0 && (r[i] = u[i]);\n      return Qo(e, a, void 0, void 0, null, r);\n    };\n    ht.createRef = function () {\n      return {current: null};\n    };\n    ht.forwardRef = function (e) {\n      return {$$typeof: Lf, render: e};\n    };\n    ht.isValidElement = Zo;\n    ht.lazy = function (e) {\n      return {$$typeof: Uc, _payload: {_status: -1, _result: e}, _init: $f};\n    };\n    ht.memo = function (e, t) {\n      return {$$typeof: Df, type: e, compare: t === void 0 ? null : t};\n    };\n    ht.use = function (e) {\n      return ei.H.use(e);\n    };\n    ht.useCallback = function (e, t) {\n      return ei.H.useCallback(e, t);\n    };\n    ht.useDebugValue = function () {};\n    ht.useId = function () {\n      return ei.H.useId();\n    };\n    ht.useMemo = function (e, t) {\n      return ei.H.useMemo(e, t);\n    };\n    ht.version = '19.0.0';\n  });\n  var Li = Z((e_, Gc) => {\n    'use strict';\n    Gc.exports = Wc();\n  });\n  var Xc = Z((Oi) => {\n    'use strict';\n    var Kf = Li(),\n      Uf = Symbol.for('react.transitional.element'),\n      Hf = Symbol.for('react.fragment');\n    if (!Kf.__SERVER_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE)\n      throw Error(\n        'The \"react\" package in this environment is not configured correctly. The \"react-server\" condition must be enabled in any environment that runs React Server Components.'\n      );\n    function zc(e, t, s) {\n      var i = null;\n      if (\n        (s !== void 0 && (i = '' + s),\n        t.key !== void 0 && (i = '' + t.key),\n        'key' in t)\n      ) {\n        s = {};\n        for (var r in t) r !== 'key' && (s[r] = t[r]);\n      } else s = t;\n      return (\n        (t = s.ref),\n        {$$typeof: Uf, type: e, key: i, ref: t !== void 0 ? t : null, props: s}\n      );\n    }\n    Oi.Fragment = Hf;\n    Oi.jsx = zc;\n    Oi.jsxDEV = void 0;\n    Oi.jsxs = zc;\n  });\n  var Jc = Z((n_, Yc) => {\n    'use strict';\n    Yc.exports = Xc();\n  });\n  var Qc = Z((jn) => {\n    'use strict';\n    var Wf = Li();\n    function ns() {}\n    var Sn = {\n      d: {\n        f: ns,\n        r: function () {\n          throw Error(\n            'Invalid form element. requestFormReset must be passed a form that was rendered by React.'\n          );\n        },\n        D: ns,\n        C: ns,\n        L: ns,\n        m: ns,\n        X: ns,\n        S: ns,\n        M: ns,\n      },\n      p: 0,\n      findDOMNode: null,\n    };\n    if (!Wf.__SERVER_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE)\n      throw Error(\n        'The \"react\" package in this environment is not configured correctly. The \"react-server\" condition must be enabled in any environment that runs React Server Components.'\n      );\n    function br(e, t) {\n      if (e === 'font') return '';\n      if (typeof t == 'string') return t === 'use-credentials' ? t : '';\n    }\n    jn.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE = Sn;\n    jn.preconnect = function (e, t) {\n      typeof e == 'string' &&\n        (t\n          ? ((t = t.crossOrigin),\n            (t =\n              typeof t == 'string'\n                ? t === 'use-credentials'\n                  ? t\n                  : ''\n                : void 0))\n          : (t = null),\n        Sn.d.C(e, t));\n    };\n    jn.prefetchDNS = function (e) {\n      typeof e == 'string' && Sn.d.D(e);\n    };\n    jn.preinit = function (e, t) {\n      if (typeof e == 'string' && t && typeof t.as == 'string') {\n        var s = t.as,\n          i = br(s, t.crossOrigin),\n          r = typeof t.integrity == 'string' ? t.integrity : void 0,\n          a = typeof t.fetchPriority == 'string' ? t.fetchPriority : void 0;\n        s === 'style'\n          ? Sn.d.S(e, typeof t.precedence == 'string' ? t.precedence : void 0, {\n              crossOrigin: i,\n              integrity: r,\n              fetchPriority: a,\n            })\n          : s === 'script' &&\n            Sn.d.X(e, {\n              crossOrigin: i,\n              integrity: r,\n              fetchPriority: a,\n              nonce: typeof t.nonce == 'string' ? t.nonce : void 0,\n            });\n      }\n    };\n    jn.preinitModule = function (e, t) {\n      if (typeof e == 'string')\n        if (typeof t == 'object' && t !== null) {\n          if (t.as == null || t.as === 'script') {\n            var s = br(t.as, t.crossOrigin);\n            Sn.d.M(e, {\n              crossOrigin: s,\n              integrity: typeof t.integrity == 'string' ? t.integrity : void 0,\n              nonce: typeof t.nonce == 'string' ? t.nonce : void 0,\n            });\n          }\n        } else t == null && Sn.d.M(e);\n    };\n    jn.preload = function (e, t) {\n      if (\n        typeof e == 'string' &&\n        typeof t == 'object' &&\n        t !== null &&\n        typeof t.as == 'string'\n      ) {\n        var s = t.as,\n          i = br(s, t.crossOrigin);\n        Sn.d.L(e, s, {\n          crossOrigin: i,\n          integrity: typeof t.integrity == 'string' ? t.integrity : void 0,\n          nonce: typeof t.nonce == 'string' ? t.nonce : void 0,\n          type: typeof t.type == 'string' ? t.type : void 0,\n          fetchPriority:\n            typeof t.fetchPriority == 'string' ? t.fetchPriority : void 0,\n          referrerPolicy:\n            typeof t.referrerPolicy == 'string' ? t.referrerPolicy : void 0,\n          imageSrcSet:\n            typeof t.imageSrcSet == 'string' ? t.imageSrcSet : void 0,\n          imageSizes: typeof t.imageSizes == 'string' ? t.imageSizes : void 0,\n          media: typeof t.media == 'string' ? t.media : void 0,\n        });\n      }\n    };\n    jn.preloadModule = function (e, t) {\n      if (typeof e == 'string')\n        if (t) {\n          var s = br(t.as, t.crossOrigin);\n          Sn.d.m(e, {\n            as: typeof t.as == 'string' && t.as !== 'script' ? t.as : void 0,\n            crossOrigin: s,\n            integrity: typeof t.integrity == 'string' ? t.integrity : void 0,\n          });\n        } else Sn.d.m(e);\n    };\n    jn.version = '19.0.0';\n  });\n  var eu = Z((i_, Zc) => {\n    'use strict';\n    Zc.exports = Qc();\n  });\n  var Zu = Z((En) => {\n    'use strict';\n    var Gf = eu(),\n      zf = Li(),\n      Tu = new MessageChannel(),\n      ku = [];\n    Tu.port1.onmessage = function () {\n      var e = ku.shift();\n      e && e();\n    };\n    function Bi(e) {\n      ku.push(e), Tu.port2.postMessage(null);\n    }\n    function Xf(e) {\n      setTimeout(function () {\n        throw e;\n      });\n    }\n    var Yf = Promise,\n      vu =\n        typeof queueMicrotask == 'function'\n          ? queueMicrotask\n          : function (e) {\n              Yf.resolve(null).then(e).catch(Xf);\n            },\n      ln = null,\n      cn = 0;\n    function Cr(e, t) {\n      if (t.byteLength !== 0)\n        if (2048 < t.byteLength)\n          0 < cn &&\n            (e.enqueue(new Uint8Array(ln.buffer, 0, cn)),\n            (ln = new Uint8Array(2048)),\n            (cn = 0)),\n            e.enqueue(t);\n        else {\n          var s = ln.length - cn;\n          s < t.byteLength &&\n            (s === 0\n              ? e.enqueue(ln)\n              : (ln.set(t.subarray(0, s), cn),\n                e.enqueue(ln),\n                (t = t.subarray(s))),\n            (ln = new Uint8Array(2048)),\n            (cn = 0)),\n            ln.set(t, cn),\n            (cn += t.byteLength);\n        }\n      return !0;\n    }\n    var Jf = new TextEncoder();\n    function pn(e) {\n      return Jf.encode(e);\n    }\n    function la(e) {\n      return e.byteLength;\n    }\n    function xu(e, t) {\n      typeof e.error == 'function' ? e.error(t) : e.close();\n    }\n    var rs = Symbol.for('react.client.reference'),\n      Ar = Symbol.for('react.server.reference');\n    function ti(e, t, s) {\n      return Object.defineProperties(e, {\n        $$typeof: {value: rs},\n        $$id: {value: t},\n        $$async: {value: s},\n      });\n    }\n    var Qf = Function.prototype.bind,\n      Zf = Array.prototype.slice;\n    function gu() {\n      var e = Qf.apply(this, arguments);\n      if (this.$$typeof === Ar) {\n        var t = Zf.call(arguments, 1),\n          s = {value: Ar},\n          i = {value: this.$$id};\n        return (\n          (t = {value: this.$$bound ? this.$$bound.concat(t) : t}),\n          Object.defineProperties(e, {\n            $$typeof: s,\n            $$id: i,\n            $$bound: t,\n            bind: {value: gu, configurable: !0},\n          })\n        );\n      }\n      return e;\n    }\n    var ed = {\n        value: function () {\n          return 'function () { [omitted code] }';\n        },\n        configurable: !0,\n        writable: !0,\n      },\n      td = Promise.prototype,\n      nd = {\n        get: function (e, t) {\n          switch (t) {\n            case '$$typeof':\n              return e.$$typeof;\n            case '$$id':\n              return e.$$id;\n            case '$$async':\n              return e.$$async;\n            case 'name':\n              return e.name;\n            case 'displayName':\n              return;\n            case 'defaultProps':\n              return;\n            case '_debugInfo':\n              return;\n            case 'toJSON':\n              return;\n            case Symbol.toPrimitive:\n              return Object.prototype[Symbol.toPrimitive];\n            case Symbol.toStringTag:\n              return Object.prototype[Symbol.toStringTag];\n            case 'Provider':\n              throw Error(\n                'Cannot render a Client Context Provider on the Server. Instead, you can export a Client Component wrapper that itself renders a Client Context Provider.'\n              );\n            case 'then':\n              throw Error(\n                'Cannot await or return from a thenable. You cannot await a client module from a server component.'\n              );\n          }\n          throw Error(\n            'Cannot access ' +\n              (String(e.name) + '.' + String(t)) +\n              ' on the server. You cannot dot into a client module from a server component. You can only pass the imported name through.'\n          );\n        },\n        set: function () {\n          throw Error('Cannot assign to a client module from a server module.');\n        },\n      };\n    function tu(e, t) {\n      switch (t) {\n        case '$$typeof':\n          return e.$$typeof;\n        case '$$id':\n          return e.$$id;\n        case '$$async':\n          return e.$$async;\n        case 'name':\n          return e.name;\n        case 'defaultProps':\n          return;\n        case '_debugInfo':\n          return;\n        case 'toJSON':\n          return;\n        case Symbol.toPrimitive:\n          return Object.prototype[Symbol.toPrimitive];\n        case Symbol.toStringTag:\n          return Object.prototype[Symbol.toStringTag];\n        case '__esModule':\n          var s = e.$$id;\n          return (\n            (e.default = ti(\n              function () {\n                throw Error(\n                  'Attempted to call the default export of ' +\n                    s +\n                    \" from the server but it's on the client. It's not possible to invoke a client function from the server, it can only be rendered as a Component or passed to props of a Client Component.\"\n                );\n              },\n              e.$$id + '#',\n              e.$$async\n            )),\n            !0\n          );\n        case 'then':\n          if (e.then) return e.then;\n          if (e.$$async) return;\n          var i = ti({}, e.$$id, !0),\n            r = new Proxy(i, _u);\n          return (\n            (e.status = 'fulfilled'),\n            (e.value = r),\n            (e.then = ti(\n              function (a) {\n                return Promise.resolve(a(r));\n              },\n              e.$$id + '#then',\n              !1\n            ))\n          );\n      }\n      if (typeof t == 'symbol')\n        throw Error(\n          'Cannot read Symbol exports. Only named exports are supported on a client module imported on the server.'\n        );\n      return (\n        (i = e[t]),\n        i ||\n          ((i = ti(\n            function () {\n              throw Error(\n                'Attempted to call ' +\n                  String(t) +\n                  '() from the server but ' +\n                  String(t) +\n                  \" is on the client. It's not possible to invoke a client function from the server, it can only be rendered as a Component or passed to props of a Client Component.\"\n              );\n            },\n            e.$$id + '#' + t,\n            e.$$async\n          )),\n          Object.defineProperty(i, 'name', {value: t}),\n          (i = e[t] = new Proxy(i, nd))),\n        i\n      );\n    }\n    var _u = {\n        get: function (e, t) {\n          return tu(e, t);\n        },\n        getOwnPropertyDescriptor: function (e, t) {\n          var s = Object.getOwnPropertyDescriptor(e, t);\n          return (\n            s ||\n              ((s = {\n                value: tu(e, t),\n                writable: !1,\n                configurable: !1,\n                enumerable: !1,\n              }),\n              Object.defineProperty(e, t, s)),\n            s\n          );\n        },\n        getPrototypeOf: function () {\n          return td;\n        },\n        set: function () {\n          throw Error('Cannot assign to a client module from a server module.');\n        },\n      },\n      bu = Gf.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE,\n      qn = bu.d;\n    bu.d = {f: qn.f, r: qn.r, D: sd, C: id, L: Sr, m: Cu, X: od, S: rd, M: ad};\n    function sd(e) {\n      if (typeof e == 'string' && e) {\n        var t = st || null;\n        if (t) {\n          var s = t.hints,\n            i = 'D|' + e;\n          s.has(i) || (s.add(i), Ut(t, 'D', e));\n        } else qn.D(e);\n      }\n    }\n    function id(e, t) {\n      if (typeof e == 'string') {\n        var s = st || null;\n        if (s) {\n          var i = s.hints,\n            r = 'C|' + (t ?? 'null') + '|' + e;\n          i.has(r) ||\n            (i.add(r),\n            typeof t == 'string' ? Ut(s, 'C', [e, t]) : Ut(s, 'C', e));\n        } else qn.C(e, t);\n      }\n    }\n    function Sr(e, t, s) {\n      if (typeof e == 'string') {\n        var i = st || null;\n        if (i) {\n          var r = i.hints,\n            a = 'L';\n          if (t === 'image' && s) {\n            var u = s.imageSrcSet,\n              d = s.imageSizes,\n              y = '';\n            typeof u == 'string' && u !== ''\n              ? ((y += '[' + u + ']'),\n                typeof d == 'string' && (y += '[' + d + ']'))\n              : (y += '[][]' + e),\n              (a += '[image]' + y);\n          } else a += '[' + t + ']' + e;\n          r.has(a) ||\n            (r.add(a),\n            (s = ji(s)) ? Ut(i, 'L', [e, t, s]) : Ut(i, 'L', [e, t]));\n        } else qn.L(e, t, s);\n      }\n    }\n    function Cu(e, t) {\n      if (typeof e == 'string') {\n        var s = st || null;\n        if (s) {\n          var i = s.hints,\n            r = 'm|' + e;\n          return i.has(r)\n            ? void 0\n            : (i.add(r), (t = ji(t)) ? Ut(s, 'm', [e, t]) : Ut(s, 'm', e));\n        }\n        qn.m(e, t);\n      }\n    }\n    function rd(e, t, s) {\n      if (typeof e == 'string') {\n        var i = st || null;\n        if (i) {\n          var r = i.hints,\n            a = 'S|' + e;\n          return r.has(a)\n            ? void 0\n            : (r.add(a),\n              (s = ji(s))\n                ? Ut(i, 'S', [e, typeof t == 'string' ? t : 0, s])\n                : typeof t == 'string'\n                ? Ut(i, 'S', [e, t])\n                : Ut(i, 'S', e));\n        }\n        qn.S(e, t, s);\n      }\n    }\n    function od(e, t) {\n      if (typeof e == 'string') {\n        var s = st || null;\n        if (s) {\n          var i = s.hints,\n            r = 'X|' + e;\n          return i.has(r)\n            ? void 0\n            : (i.add(r), (t = ji(t)) ? Ut(s, 'X', [e, t]) : Ut(s, 'X', e));\n        }\n        qn.X(e, t);\n      }\n    }\n    function ad(e, t) {\n      if (typeof e == 'string') {\n        var s = st || null;\n        if (s) {\n          var i = s.hints,\n            r = 'M|' + e;\n          return i.has(r)\n            ? void 0\n            : (i.add(r), (t = ji(t)) ? Ut(s, 'M', [e, t]) : Ut(s, 'M', e));\n        }\n        qn.M(e, t);\n      }\n    }\n    function ji(e) {\n      if (e == null) return null;\n      var t = !1,\n        s = {},\n        i;\n      for (i in e) e[i] != null && ((t = !0), (s[i] = e[i]));\n      return t ? s : null;\n    }\n    function ld(e, t, s) {\n      switch (t) {\n        case 'img':\n          t = s.src;\n          var i = s.srcSet;\n          if (\n            !(\n              s.loading === 'lazy' ||\n              (!t && !i) ||\n              (typeof t != 'string' && t != null) ||\n              (typeof i != 'string' && i != null) ||\n              s.fetchPriority === 'low' ||\n              e & 3\n            ) &&\n            (typeof t != 'string' ||\n              t[4] !== ':' ||\n              (t[0] !== 'd' && t[0] !== 'D') ||\n              (t[1] !== 'a' && t[1] !== 'A') ||\n              (t[2] !== 't' && t[2] !== 'T') ||\n              (t[3] !== 'a' && t[3] !== 'A')) &&\n            (typeof i != 'string' ||\n              i[4] !== ':' ||\n              (i[0] !== 'd' && i[0] !== 'D') ||\n              (i[1] !== 'a' && i[1] !== 'A') ||\n              (i[2] !== 't' && i[2] !== 'T') ||\n              (i[3] !== 'a' && i[3] !== 'A'))\n          ) {\n            var r = typeof s.sizes == 'string' ? s.sizes : void 0,\n              a = s.crossOrigin;\n            Sr(t || '', 'image', {\n              imageSrcSet: i,\n              imageSizes: r,\n              crossOrigin:\n                typeof a == 'string'\n                  ? a === 'use-credentials'\n                    ? a\n                    : ''\n                  : void 0,\n              integrity: s.integrity,\n              type: s.type,\n              fetchPriority: s.fetchPriority,\n              referrerPolicy: s.referrerPolicy,\n            });\n          }\n          return e;\n        case 'link':\n          if (\n            ((t = s.rel),\n            (i = s.href),\n            !(\n              e & 1 ||\n              s.itemProp != null ||\n              typeof t != 'string' ||\n              typeof i != 'string' ||\n              i === ''\n            ))\n          )\n            switch (t) {\n              case 'preload':\n                Sr(i, s.as, {\n                  crossOrigin: s.crossOrigin,\n                  integrity: s.integrity,\n                  nonce: s.nonce,\n                  type: s.type,\n                  fetchPriority: s.fetchPriority,\n                  referrerPolicy: s.referrerPolicy,\n                  imageSrcSet: s.imageSrcSet,\n                  imageSizes: s.imageSizes,\n                  media: s.media,\n                });\n                break;\n              case 'modulepreload':\n                Cu(i, {\n                  as: s.as,\n                  crossOrigin: s.crossOrigin,\n                  integrity: s.integrity,\n                  nonce: s.nonce,\n                });\n                break;\n              case 'stylesheet':\n                Sr(i, 'stylesheet', {\n                  crossOrigin: s.crossOrigin,\n                  integrity: s.integrity,\n                  nonce: s.nonce,\n                  type: s.type,\n                  fetchPriority: s.fetchPriority,\n                  referrerPolicy: s.referrerPolicy,\n                  media: s.media,\n                });\n            }\n          return e;\n        case 'picture':\n          return e | 2;\n        case 'noscript':\n          return e | 1;\n        default:\n          return e;\n      }\n    }\n    var ca = Symbol.for('react.temporary.reference'),\n      cd = {\n        get: function (e, t) {\n          switch (t) {\n            case '$$typeof':\n              return e.$$typeof;\n            case 'name':\n              return;\n            case 'displayName':\n              return;\n            case 'defaultProps':\n              return;\n            case '_debugInfo':\n              return;\n            case 'toJSON':\n              return;\n            case Symbol.toPrimitive:\n              return Object.prototype[Symbol.toPrimitive];\n            case Symbol.toStringTag:\n              return Object.prototype[Symbol.toStringTag];\n            case 'Provider':\n              throw Error(\n                'Cannot render a Client Context Provider on the Server. Instead, you can export a Client Component wrapper that itself renders a Client Context Provider.'\n              );\n            case 'then':\n              return;\n          }\n          throw Error(\n            'Cannot access ' +\n              String(t) +\n              ' on the server. You cannot dot into a temporary client reference from a server component. You can only pass the value through to the client.'\n          );\n        },\n        set: function () {\n          throw Error(\n            'Cannot assign to a temporary client reference from a server module.'\n          );\n        },\n      };\n    function ud(e, t) {\n      var s = Object.defineProperties(\n        function () {\n          throw Error(\n            \"Attempted to call a temporary Client Reference from the server but it is on the client. It's not possible to invoke a client function from the server, it can only be rendered as a Component or passed to props of a Client Component.\"\n          );\n        },\n        {$$typeof: {value: ca}}\n      );\n      return (s = new Proxy(s, cd)), e.set(s, t), s;\n    }\n    var pd = Symbol.for('react.element'),\n      In = Symbol.for('react.transitional.element'),\n      ua = Symbol.for('react.fragment'),\n      nu = Symbol.for('react.context'),\n      wu = Symbol.for('react.forward_ref'),\n      hd = Symbol.for('react.suspense'),\n      fd = Symbol.for('react.suspense_list'),\n      Su = Symbol.for('react.memo'),\n      $i = Symbol.for('react.lazy'),\n      dd = Symbol.for('react.memo_cache_sentinel');\n    Symbol.for('react.postpone');\n    var su = Symbol.iterator;\n    function Iu(e) {\n      return e === null || typeof e != 'object'\n        ? null\n        : ((e = (su && e[su]) || e['@@iterator']),\n          typeof e == 'function' ? e : null);\n    }\n    var Ss = Symbol.asyncIterator;\n    function Cs() {}\n    var pa = Error(\n      \"Suspense Exception: This is not a real error! It's an implementation detail of `use` to interrupt the current render. You must either rethrow it immediately, or move the `use` call outside of the `try/catch` block. Capturing without rethrowing will lead to unexpected behavior.\\n\\nTo handle async errors, wrap your component in an error boundary, or call the promise's `.catch` method and pass the result to `use`.\"\n    );\n    function md(e, t, s) {\n      switch (\n        ((s = e[s]),\n        s === void 0 ? e.push(t) : s !== t && (t.then(Cs, Cs), (t = s)),\n        t.status)\n      ) {\n        case 'fulfilled':\n          return t.value;\n        case 'rejected':\n          throw t.reason;\n        default:\n          switch (\n            (typeof t.status == 'string'\n              ? t.then(Cs, Cs)\n              : ((e = t),\n                (e.status = 'pending'),\n                e.then(\n                  function (i) {\n                    if (t.status === 'pending') {\n                      var r = t;\n                      (r.status = 'fulfilled'), (r.value = i);\n                    }\n                  },\n                  function (i) {\n                    if (t.status === 'pending') {\n                      var r = t;\n                      (r.status = 'rejected'), (r.reason = i);\n                    }\n                  }\n                )),\n            t.status)\n          ) {\n            case 'fulfilled':\n              return t.value;\n            case 'rejected':\n              throw t.reason;\n          }\n          throw ((Ir = t), pa);\n      }\n    }\n    var Ir = null;\n    function Eu() {\n      if (Ir === null)\n        throw Error(\n          'Expected a suspended thenable. This is a bug in React. Please file an issue.'\n        );\n      var e = Ir;\n      return (Ir = null), e;\n    }\n    var Mi = null,\n      ta = 0,\n      ni = null;\n    function Au() {\n      var e = ni || [];\n      return (ni = null), e;\n    }\n    var Pu = {\n      readContext: na,\n      use: kd,\n      useCallback: function (e) {\n        return e;\n      },\n      useContext: na,\n      useEffect: Vt,\n      useImperativeHandle: Vt,\n      useLayoutEffect: Vt,\n      useInsertionEffect: Vt,\n      useMemo: function (e) {\n        return e();\n      },\n      useReducer: Vt,\n      useRef: Vt,\n      useState: Vt,\n      useDebugValue: function () {},\n      useDeferredValue: Vt,\n      useTransition: Vt,\n      useSyncExternalStore: Vt,\n      useId: Td,\n      useHostTransitionStatus: Vt,\n      useFormState: Vt,\n      useActionState: Vt,\n      useOptimistic: Vt,\n      useMemoCache: function (e) {\n        for (var t = Array(e), s = 0; s < e; s++) t[s] = dd;\n        return t;\n      },\n      useCacheRefresh: function () {\n        return yd;\n      },\n    };\n    Pu.useEffectEvent = Vt;\n    function Vt() {\n      throw Error('This Hook is not supported in Server Components.');\n    }\n    function yd() {\n      throw Error(\n        'Refreshing the cache is not supported in Server Components.'\n      );\n    }\n    function na() {\n      throw Error('Cannot read a Client Context from a Server Component.');\n    }\n    function Td() {\n      if (Mi === null)\n        throw Error('useId can only be used while React is rendering');\n      var e = Mi.identifierCount++;\n      return '_' + Mi.identifierPrefix + 'S_' + e.toString(32) + '_';\n    }\n    function kd(e) {\n      if ((e !== null && typeof e == 'object') || typeof e == 'function') {\n        if (typeof e.then == 'function') {\n          var t = ta;\n          return (ta += 1), ni === null && (ni = []), md(ni, e, t);\n        }\n        e.$$typeof === nu && na();\n      }\n      throw e.$$typeof === rs\n        ? e.value != null && e.value.$$typeof === nu\n          ? Error('Cannot read a Client Context from a Server Component.')\n          : Error('Cannot use() an already resolved Client Reference.')\n        : Error('An unsupported type was passed to use(): ' + String(e));\n    }\n    var iu = {\n        getCacheForType: function (e) {\n          var t = (t = st || null) ? t.cache : new Map(),\n            s = t.get(e);\n          return s === void 0 && ((s = e()), t.set(e, s)), s;\n        },\n        cacheSignal: function () {\n          var e = st || null;\n          return e ? e.cacheController.signal : null;\n        },\n      },\n      Is = zf.__SERVER_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;\n    if (!Is)\n      throw Error(\n        'The \"react\" package in this environment is not configured correctly. The \"react-server\" condition must be enabled in any environment that runs React Server Components.'\n      );\n    var kn = Array.isArray,\n      ii = Object.getPrototypeOf;\n    function Nu(e) {\n      return (e = Object.prototype.toString.call(e)), e.slice(8, e.length - 1);\n    }\n    function ru(e) {\n      switch (typeof e) {\n        case 'string':\n          return JSON.stringify(10 >= e.length ? e : e.slice(0, 10) + '...');\n        case 'object':\n          return kn(e)\n            ? '[...]'\n            : e !== null && e.$$typeof === sa\n            ? 'client'\n            : ((e = Nu(e)), e === 'Object' ? '{...}' : e);\n        case 'function':\n          return e.$$typeof === sa\n            ? 'client'\n            : (e = e.displayName || e.name)\n            ? 'function ' + e\n            : 'function';\n        default:\n          return String(e);\n      }\n    }\n    function Er(e) {\n      if (typeof e == 'string') return e;\n      switch (e) {\n        case hd:\n          return 'Suspense';\n        case fd:\n          return 'SuspenseList';\n      }\n      if (typeof e == 'object')\n        switch (e.$$typeof) {\n          case wu:\n            return Er(e.render);\n          case Su:\n            return Er(e.type);\n          case $i:\n            var t = e._payload;\n            e = e._init;\n            try {\n              return Er(e(t));\n            } catch {}\n        }\n      return '';\n    }\n    var sa = Symbol.for('react.client.reference');\n    function _s(e, t) {\n      var s = Nu(e);\n      if (s !== 'Object' && s !== 'Array') return s;\n      s = -1;\n      var i = 0;\n      if (kn(e)) {\n        for (var r = '[', a = 0; a < e.length; a++) {\n          0 < a && (r += ', ');\n          var u = e[a];\n          (u = typeof u == 'object' && u !== null ? _s(u) : ru(u)),\n            '' + a === t\n              ? ((s = r.length), (i = u.length), (r += u))\n              : (r =\n                  10 > u.length && 40 > r.length + u.length\n                    ? r + u\n                    : r + '...');\n        }\n        r += ']';\n      } else if (e.$$typeof === In) r = '<' + Er(e.type) + '/>';\n      else {\n        if (e.$$typeof === sa) return 'client';\n        for (r = '{', a = Object.keys(e), u = 0; u < a.length; u++) {\n          0 < u && (r += ', ');\n          var d = a[u],\n            y = JSON.stringify(d);\n          (r += ('\"' + d + '\"' === y ? d : y) + ': '),\n            (y = e[d]),\n            (y = typeof y == 'object' && y !== null ? _s(y) : ru(y)),\n            d === t\n              ? ((s = r.length), (i = y.length), (r += y))\n              : (r =\n                  10 > y.length && 40 > r.length + y.length\n                    ? r + y\n                    : r + '...');\n        }\n        r += '}';\n      }\n      return t === void 0\n        ? r\n        : -1 < s && 0 < i\n        ? ((e = ' '.repeat(s) + '^'.repeat(i)),\n          `\n  ` +\n            r +\n            `\n  ` +\n            e)\n        : `\n  ` + r;\n    }\n    var Nr = Object.prototype.hasOwnProperty,\n      vd = Object.prototype,\n      Es = JSON.stringify;\n    function xd(e) {\n      console.error(e);\n    }\n    function Ru(e, t, s, i, r, a, u, d, y) {\n      if (Is.A !== null && Is.A !== iu)\n        throw Error(\n          'Currently React only supports one RSC renderer at a time.'\n        );\n      Is.A = iu;\n      var g = new Set(),\n        L = [],\n        p = new Set();\n      (this.type = e),\n        (this.status = 10),\n        (this.flushScheduled = !1),\n        (this.destination = this.fatalError = null),\n        (this.bundlerConfig = s),\n        (this.cache = new Map()),\n        (this.cacheController = new AbortController()),\n        (this.pendingChunks = this.nextChunkId = 0),\n        (this.hints = p),\n        (this.abortableTasks = g),\n        (this.pingedTasks = L),\n        (this.completedImportChunks = []),\n        (this.completedHintChunks = []),\n        (this.completedRegularChunks = []),\n        (this.completedErrorChunks = []),\n        (this.writtenSymbols = new Map()),\n        (this.writtenClientReferences = new Map()),\n        (this.writtenServerReferences = new Map()),\n        (this.writtenObjects = new WeakMap()),\n        (this.temporaryReferences = y),\n        (this.identifierPrefix = d || ''),\n        (this.identifierCount = 1),\n        (this.taintCleanupQueue = []),\n        (this.onError = i === void 0 ? xd : i),\n        (this.onPostpone = r === void 0 ? Cs : r),\n        (this.onAllReady = a),\n        (this.onFatalError = u),\n        (e = os(this, t, null, !1, 0, g)),\n        L.push(e);\n    }\n    var st = null;\n    function ou(e, t, s) {\n      var i = os(\n        e,\n        s,\n        t.keyPath,\n        t.implicitSlot,\n        t.formatContext,\n        e.abortableTasks\n      );\n      switch (s.status) {\n        case 'fulfilled':\n          return (i.model = s.value), Vi(e, i), i.id;\n        case 'rejected':\n          return Un(e, i, s.reason), i.id;\n        default:\n          if (e.status === 12)\n            return (\n              e.abortableTasks.delete(i),\n              e.type === 21\n                ? (ri(i), oi(i, e))\n                : ((t = e.fatalError), ha(i), fa(i, e, t)),\n              i.id\n            );\n          typeof s.status != 'string' &&\n            ((s.status = 'pending'),\n            s.then(\n              function (r) {\n                s.status === 'pending' &&\n                  ((s.status = 'fulfilled'), (s.value = r));\n              },\n              function (r) {\n                s.status === 'pending' &&\n                  ((s.status = 'rejected'), (s.reason = r));\n              }\n            ));\n      }\n      return (\n        s.then(\n          function (r) {\n            (i.model = r), Vi(e, i);\n          },\n          function (r) {\n            i.status === 0 && (Un(e, i, r), un(e));\n          }\n        ),\n        i.id\n      );\n    }\n    function gd(e, t, s) {\n      function i(g) {\n        if (y.status === 0)\n          if (g.done)\n            (y.status = 1),\n              (g =\n                y.id.toString(16) +\n                `:C\n`),\n              e.completedRegularChunks.push(pn(g)),\n              e.abortableTasks.delete(y),\n              e.cacheController.signal.removeEventListener('abort', a),\n              un(e),\n              Or(e);\n          else\n            try {\n              (y.model = g.value),\n                e.pendingChunks++,\n                Bu(e, y),\n                un(e),\n                d.read().then(i, r);\n            } catch (L) {\n              r(L);\n            }\n      }\n      function r(g) {\n        y.status === 0 &&\n          (e.cacheController.signal.removeEventListener('abort', a),\n          Un(e, y, g),\n          un(e),\n          d.cancel(g).then(r, r));\n      }\n      function a() {\n        if (y.status === 0) {\n          var g = e.cacheController.signal;\n          g.removeEventListener('abort', a),\n            (g = g.reason),\n            e.type === 21\n              ? (e.abortableTasks.delete(y), ri(y), oi(y, e))\n              : (Un(e, y, g), un(e)),\n            d.cancel(g).then(r, r);\n        }\n      }\n      var u = s.supportsBYOB;\n      if (u === void 0)\n        try {\n          s.getReader({mode: 'byob'}).releaseLock(), (u = !0);\n        } catch {\n          u = !1;\n        }\n      var d = s.getReader(),\n        y = os(\n          e,\n          t.model,\n          t.keyPath,\n          t.implicitSlot,\n          t.formatContext,\n          e.abortableTasks\n        );\n      return (\n        e.pendingChunks++,\n        (t =\n          y.id.toString(16) +\n          ':' +\n          (u ? 'r' : 'R') +\n          `\n`),\n        e.completedRegularChunks.push(pn(t)),\n        e.cacheController.signal.addEventListener('abort', a),\n        d.read().then(i, r),\n        Ct(y.id)\n      );\n    }\n    function _d(e, t, s, i) {\n      function r(y) {\n        if (d.status === 0)\n          if (y.done) {\n            if (((d.status = 1), y.value === void 0))\n              var g =\n                d.id.toString(16) +\n                `:C\n`;\n            else\n              try {\n                var L = bs(e, y.value, 0);\n                g =\n                  d.id.toString(16) +\n                  ':C' +\n                  Es(Ct(L)) +\n                  `\n`;\n              } catch (p) {\n                a(p);\n                return;\n              }\n            e.completedRegularChunks.push(pn(g)),\n              e.abortableTasks.delete(d),\n              e.cacheController.signal.removeEventListener('abort', u),\n              un(e),\n              Or(e);\n          } else\n            try {\n              (d.model = y.value),\n                e.pendingChunks++,\n                Bu(e, d),\n                un(e),\n                i.next().then(r, a);\n            } catch (p) {\n              a(p);\n            }\n      }\n      function a(y) {\n        d.status === 0 &&\n          (e.cacheController.signal.removeEventListener('abort', u),\n          Un(e, d, y),\n          un(e),\n          typeof i.throw == 'function' && i.throw(y).then(a, a));\n      }\n      function u() {\n        if (d.status === 0) {\n          var y = e.cacheController.signal;\n          y.removeEventListener('abort', u);\n          var g = y.reason;\n          e.type === 21\n            ? (e.abortableTasks.delete(d), ri(d), oi(d, e))\n            : (Un(e, d, y.reason), un(e)),\n            typeof i.throw == 'function' && i.throw(g).then(a, a);\n        }\n      }\n      s = s === i;\n      var d = os(\n        e,\n        t.model,\n        t.keyPath,\n        t.implicitSlot,\n        t.formatContext,\n        e.abortableTasks\n      );\n      return (\n        e.pendingChunks++,\n        (t =\n          d.id.toString(16) +\n          ':' +\n          (s ? 'x' : 'X') +\n          `\n`),\n        e.completedRegularChunks.push(pn(t)),\n        e.cacheController.signal.addEventListener('abort', u),\n        i.next().then(r, a),\n        Ct(d.id)\n      );\n    }\n    function Ut(e, t, s) {\n      (s = Es(s)),\n        (t = pn(\n          ':H' +\n            t +\n            s +\n            `\n`\n        )),\n        e.completedHintChunks.push(t),\n        un(e);\n    }\n    function bd(e) {\n      if (e.status === 'fulfilled') return e.value;\n      throw e.status === 'rejected' ? e.reason : e;\n    }\n    function Cd(e, t, s) {\n      switch (s.status) {\n        case 'fulfilled':\n          return s.value;\n        case 'rejected':\n          break;\n        default:\n          typeof s.status != 'string' &&\n            ((s.status = 'pending'),\n            s.then(\n              function (i) {\n                s.status === 'pending' &&\n                  ((s.status = 'fulfilled'), (s.value = i));\n              },\n              function (i) {\n                s.status === 'pending' &&\n                  ((s.status = 'rejected'), (s.reason = i));\n              }\n            ));\n      }\n      return {$$typeof: $i, _payload: s, _init: bd};\n    }\n    function au() {}\n    function wd(e, t, s, i) {\n      if (typeof i != 'object' || i === null || i.$$typeof === rs) return i;\n      if (typeof i.then == 'function') return Cd(e, t, i);\n      var r = Iu(i);\n      return r\n        ? ((e = {}),\n          (e[Symbol.iterator] = function () {\n            return r.call(i);\n          }),\n          e)\n        : typeof i[Ss] != 'function' ||\n          (typeof ReadableStream == 'function' && i instanceof ReadableStream)\n        ? i\n        : ((e = {}),\n          (e[Ss] = function () {\n            return i[Ss]();\n          }),\n          e);\n    }\n    function lu(e, t, s, i, r) {\n      var a = t.thenableState;\n      if (\n        ((t.thenableState = null),\n        (ta = 0),\n        (ni = a),\n        (r = i(r, void 0)),\n        e.status === 12)\n      )\n        throw (\n          (typeof r == 'object' &&\n            r !== null &&\n            typeof r.then == 'function' &&\n            r.$$typeof !== rs &&\n            r.then(au, au),\n          null)\n        );\n      return (\n        (r = wd(e, t, i, r)),\n        (i = t.keyPath),\n        (a = t.implicitSlot),\n        s !== null\n          ? (t.keyPath = i === null ? s : i + ',' + s)\n          : i === null && (t.implicitSlot = !0),\n        (e = qi(e, t, Lr, '', r)),\n        (t.keyPath = i),\n        (t.implicitSlot = a),\n        e\n      );\n    }\n    function cu(e, t, s) {\n      return t.keyPath !== null\n        ? ((e = [In, ua, t.keyPath, {children: s}]), t.implicitSlot ? [e] : e)\n        : s;\n    }\n    var is = 0;\n    function uu(e, t) {\n      return (\n        (t = os(\n          e,\n          t.model,\n          t.keyPath,\n          t.implicitSlot,\n          t.formatContext,\n          e.abortableTasks\n        )),\n        Vi(e, t),\n        ws(t.id)\n      );\n    }\n    function ia(e, t, s, i, r, a) {\n      if (r != null)\n        throw Error(\n          'Refs cannot be used in Server Components, nor passed to Client Components.'\n        );\n      if (typeof s == 'function' && s.$$typeof !== rs && s.$$typeof !== ca)\n        return lu(e, t, i, s, a);\n      if (s === ua && i === null)\n        return (\n          (s = t.implicitSlot),\n          t.keyPath === null && (t.implicitSlot = !0),\n          (a = qi(e, t, Lr, '', a.children)),\n          (t.implicitSlot = s),\n          a\n        );\n      if (s != null && typeof s == 'object' && s.$$typeof !== rs)\n        switch (s.$$typeof) {\n          case $i:\n            var u = s._init;\n            if (((s = u(s._payload)), e.status === 12)) throw null;\n            return ia(e, t, s, i, r, a);\n          case wu:\n            return lu(e, t, i, s.render, a);\n          case Su:\n            return ia(e, t, s.type, i, r, a);\n        }\n      else\n        typeof s == 'string' &&\n          ((r = t.formatContext),\n          (u = ld(r, s, a)),\n          r !== u && a.children != null && bs(e, a.children, u));\n      return (\n        (e = i),\n        (i = t.keyPath),\n        e === null ? (e = i) : i !== null && (e = i + ',' + e),\n        (a = [In, s, e, a]),\n        (t = t.implicitSlot && e !== null ? [a] : a),\n        t\n      );\n    }\n    function Vi(e, t) {\n      var s = e.pingedTasks;\n      s.push(t),\n        s.length === 1 &&\n          ((e.flushScheduled = e.destination !== null),\n          e.type === 21 || e.status === 10\n            ? vu(function () {\n                return ra(e);\n              })\n            : Bi(function () {\n                return ra(e);\n              }));\n    }\n    function os(e, t, s, i, r, a) {\n      e.pendingChunks++;\n      var u = e.nextChunkId++;\n      typeof t != 'object' ||\n        t === null ||\n        s !== null ||\n        i ||\n        e.writtenObjects.set(t, Ct(u));\n      var d = {\n        id: u,\n        status: 0,\n        model: t,\n        keyPath: s,\n        implicitSlot: i,\n        formatContext: r,\n        ping: function () {\n          return Vi(e, d);\n        },\n        toJSON: function (y, g) {\n          is += y.length;\n          var L = d.keyPath,\n            p = d.implicitSlot;\n          try {\n            var h = qi(e, d, this, y, g);\n          } catch (x) {\n            if (\n              ((y = d.model),\n              (y =\n                typeof y == 'object' &&\n                y !== null &&\n                (y.$$typeof === In || y.$$typeof === $i)),\n              e.status === 12)\n            )\n              (d.status = 3),\n                e.type === 21\n                  ? ((L = e.nextChunkId++), (L = y ? ws(L) : Ct(L)), (h = L))\n                  : ((L = e.fatalError), (h = y ? ws(L) : Ct(L)));\n            else if (\n              ((g = x === pa ? Eu() : x),\n              typeof g == 'object' && g !== null && typeof g.then == 'function')\n            ) {\n              h = os(\n                e,\n                d.model,\n                d.keyPath,\n                d.implicitSlot,\n                d.formatContext,\n                e.abortableTasks\n              );\n              var T = h.ping;\n              g.then(T, T),\n                (h.thenableState = Au()),\n                (d.keyPath = L),\n                (d.implicitSlot = p),\n                (h = y ? ws(h.id) : Ct(h.id));\n            } else\n              (d.keyPath = L),\n                (d.implicitSlot = p),\n                e.pendingChunks++,\n                (L = e.nextChunkId++),\n                (p = Kn(e, g, d)),\n                Rr(e, L, p),\n                (h = y ? ws(L) : Ct(L));\n          }\n          return h;\n        },\n        thenableState: null,\n      };\n      return a.add(d), d;\n    }\n    function Ct(e) {\n      return '$' + e.toString(16);\n    }\n    function ws(e) {\n      return '$L' + e.toString(16);\n    }\n    function Lu(e, t, s) {\n      return (\n        (e = Es(s)),\n        (t =\n          t.toString(16) +\n          ':' +\n          e +\n          `\n`),\n        pn(t)\n      );\n    }\n    function pu(e, t, s, i) {\n      var r = i.$$async ? i.$$id + '#async' : i.$$id,\n        a = e.writtenClientReferences,\n        u = a.get(r);\n      if (u !== void 0) return t[0] === In && s === '1' ? ws(u) : Ct(u);\n      try {\n        var d = e.bundlerConfig,\n          y = i.$$id;\n        u = '';\n        var g = d[y];\n        if (g) u = g.name;\n        else {\n          var L = y.lastIndexOf('#');\n          if ((L !== -1 && ((u = y.slice(L + 1)), (g = d[y.slice(0, L)])), !g))\n            throw Error(\n              'Could not find the module \"' +\n                y +\n                '\" in the React Client Manifest. This is probably a bug in the React Server Components bundler.'\n            );\n        }\n        if (g.async === !0 && i.$$async === !0)\n          throw Error(\n            'The module \"' +\n              y +\n              '\" is marked as an async ESM module but was loaded as a CJS proxy. This is probably a bug in the React Server Components bundler.'\n          );\n        var p =\n          g.async === !0 || i.$$async === !0\n            ? [g.id, g.chunks, u, 1]\n            : [g.id, g.chunks, u];\n        e.pendingChunks++;\n        var h = e.nextChunkId++,\n          T = Es(p),\n          x =\n            h.toString(16) +\n            ':I' +\n            T +\n            `\n`,\n          w = pn(x);\n        return (\n          e.completedImportChunks.push(w),\n          a.set(r, h),\n          t[0] === In && s === '1' ? ws(h) : Ct(h)\n        );\n      } catch (S) {\n        return (\n          e.pendingChunks++,\n          (t = e.nextChunkId++),\n          (s = Kn(e, S, null)),\n          Rr(e, t, s),\n          Ct(t)\n        );\n      }\n    }\n    function bs(e, t, s) {\n      return (t = os(e, t, null, !1, s, e.abortableTasks)), Fu(e, t), t.id;\n    }\n    function Yt(e, t, s) {\n      e.pendingChunks++;\n      var i = e.nextChunkId++;\n      return Kt(e, i, t, s, !1), Ct(i);\n    }\n    function Sd(e, t) {\n      function s(y) {\n        if (u.status === 0)\n          if (y.done)\n            e.cacheController.signal.removeEventListener('abort', r), Vi(e, u);\n          else return a.push(y.value), d.read().then(s).catch(i);\n      }\n      function i(y) {\n        u.status === 0 &&\n          (e.cacheController.signal.removeEventListener('abort', r),\n          Un(e, u, y),\n          un(e),\n          d.cancel(y).then(i, i));\n      }\n      function r() {\n        if (u.status === 0) {\n          var y = e.cacheController.signal;\n          y.removeEventListener('abort', r),\n            (y = y.reason),\n            e.type === 21\n              ? (e.abortableTasks.delete(u), ri(u), oi(u, e))\n              : (Un(e, u, y), un(e)),\n            d.cancel(y).then(i, i);\n        }\n      }\n      var a = [t.type],\n        u = os(e, a, null, !1, 0, e.abortableTasks),\n        d = t.stream().getReader();\n      return (\n        e.cacheController.signal.addEventListener('abort', r),\n        d.read().then(s).catch(i),\n        '$B' + u.id.toString(16)\n      );\n    }\n    var ss = !1;\n    function qi(e, t, s, i, r) {\n      if (((t.model = r), r === In)) return '$';\n      if (r === null) return null;\n      if (typeof r == 'object') {\n        switch (r.$$typeof) {\n          case In:\n            var a = null,\n              u = e.writtenObjects;\n            if (t.keyPath === null && !t.implicitSlot) {\n              var d = u.get(r);\n              if (d !== void 0)\n                if (ss === r) ss = null;\n                else return d;\n              else\n                i.indexOf(':') === -1 &&\n                  ((s = u.get(s)),\n                  s !== void 0 && ((a = s + ':' + i), u.set(r, a)));\n            }\n            return 3200 < is\n              ? uu(e, t)\n              : ((i = r.props),\n                (s = i.ref),\n                (e = ia(e, t, r.type, r.key, s !== void 0 ? s : null, i)),\n                typeof e == 'object' &&\n                  e !== null &&\n                  a !== null &&\n                  (u.has(e) || u.set(e, a)),\n                e);\n          case $i:\n            if (3200 < is) return uu(e, t);\n            if (\n              ((t.thenableState = null),\n              (i = r._init),\n              (r = i(r._payload)),\n              e.status === 12)\n            )\n              throw null;\n            return qi(e, t, Lr, '', r);\n          case pd:\n            throw Error(`A React Element from an older version of React was rendered. This is not supported. It can happen if:\n- Multiple copies of the \"react\" package is used.\n- A library pre-bundled an old copy of \"react\" or \"react/jsx-runtime\".\n- A compiler tries to \"inline\" JSX instead of using the runtime.`);\n        }\n        if (r.$$typeof === rs) return pu(e, s, i, r);\n        if (\n          e.temporaryReferences !== void 0 &&\n          ((a = e.temporaryReferences.get(r)), a !== void 0)\n        )\n          return '$T' + a;\n        if (\n          ((a = e.writtenObjects), (u = a.get(r)), typeof r.then == 'function')\n        ) {\n          if (u !== void 0) {\n            if (t.keyPath !== null || t.implicitSlot)\n              return '$@' + ou(e, t, r).toString(16);\n            if (ss === r) ss = null;\n            else return u;\n          }\n          return (e = '$@' + ou(e, t, r).toString(16)), a.set(r, e), e;\n        }\n        if (u !== void 0)\n          if (ss === r) {\n            if (u !== Ct(t.id)) return u;\n            ss = null;\n          } else return u;\n        else if (i.indexOf(':') === -1 && ((u = a.get(s)), u !== void 0)) {\n          if (((d = i), kn(s) && s[0] === In))\n            switch (i) {\n              case '1':\n                d = 'type';\n                break;\n              case '2':\n                d = 'key';\n                break;\n              case '3':\n                d = 'props';\n                break;\n              case '4':\n                d = '_owner';\n            }\n          a.set(r, u + ':' + d);\n        }\n        if (kn(r)) return cu(e, t, r);\n        if (r instanceof Map)\n          return (r = Array.from(r)), '$Q' + bs(e, r, 0).toString(16);\n        if (r instanceof Set)\n          return (r = Array.from(r)), '$W' + bs(e, r, 0).toString(16);\n        if (typeof FormData == 'function' && r instanceof FormData)\n          return (r = Array.from(r.entries())), '$K' + bs(e, r, 0).toString(16);\n        if (r instanceof Error) return '$Z';\n        if (r instanceof ArrayBuffer) return Yt(e, 'A', new Uint8Array(r));\n        if (r instanceof Int8Array) return Yt(e, 'O', r);\n        if (r instanceof Uint8Array) return Yt(e, 'o', r);\n        if (r instanceof Uint8ClampedArray) return Yt(e, 'U', r);\n        if (r instanceof Int16Array) return Yt(e, 'S', r);\n        if (r instanceof Uint16Array) return Yt(e, 's', r);\n        if (r instanceof Int32Array) return Yt(e, 'L', r);\n        if (r instanceof Uint32Array) return Yt(e, 'l', r);\n        if (r instanceof Float32Array) return Yt(e, 'G', r);\n        if (r instanceof Float64Array) return Yt(e, 'g', r);\n        if (r instanceof BigInt64Array) return Yt(e, 'M', r);\n        if (r instanceof BigUint64Array) return Yt(e, 'm', r);\n        if (r instanceof DataView) return Yt(e, 'V', r);\n        if (typeof Blob == 'function' && r instanceof Blob) return Sd(e, r);\n        if ((a = Iu(r)))\n          return (\n            (i = a.call(r)),\n            i === r\n              ? ((r = Array.from(i)), '$i' + bs(e, r, 0).toString(16))\n              : cu(e, t, Array.from(i))\n          );\n        if (typeof ReadableStream == 'function' && r instanceof ReadableStream)\n          return gd(e, t, r);\n        if (((a = r[Ss]), typeof a == 'function'))\n          return (\n            t.keyPath !== null\n              ? ((e = [In, ua, t.keyPath, {children: r}]),\n                (e = t.implicitSlot ? [e] : e))\n              : ((i = a.call(r)), (e = _d(e, t, r, i))),\n            e\n          );\n        if (r instanceof Date) return '$D' + r.toJSON();\n        if (((e = ii(r)), e !== vd && (e === null || ii(e) !== null)))\n          throw Error(\n            'Only plain objects, and a few built-ins, can be passed to Client Components from Server Components. Classes or null prototypes are not supported.' +\n              _s(s, i)\n          );\n        return r;\n      }\n      if (typeof r == 'string')\n        return (\n          (is += r.length),\n          r[r.length - 1] === 'Z' && s[i] instanceof Date\n            ? '$D' + r\n            : 1024 <= r.length && la !== null\n            ? (e.pendingChunks++, (t = e.nextChunkId++), Du(e, t, r, !1), Ct(t))\n            : ((e = r[0] === '$' ? '$' + r : r), e)\n        );\n      if (typeof r == 'boolean') return r;\n      if (typeof r == 'number')\n        return Number.isFinite(r)\n          ? r === 0 && 1 / r === -1 / 0\n            ? '$-0'\n            : r\n          : r === 1 / 0\n          ? '$Infinity'\n          : r === -1 / 0\n          ? '$-Infinity'\n          : '$NaN';\n      if (typeof r > 'u') return '$undefined';\n      if (typeof r == 'function') {\n        if (r.$$typeof === rs) return pu(e, s, i, r);\n        if (r.$$typeof === Ar)\n          return (\n            (t = e.writtenServerReferences),\n            (i = t.get(r)),\n            i !== void 0\n              ? (e = '$h' + i.toString(16))\n              : ((i = r.$$bound),\n                (i = i === null ? null : Promise.resolve(i)),\n                (e = bs(e, {id: r.$$id, bound: i}, 0)),\n                t.set(r, e),\n                (e = '$h' + e.toString(16))),\n            e\n          );\n        if (\n          e.temporaryReferences !== void 0 &&\n          ((e = e.temporaryReferences.get(r)), e !== void 0)\n        )\n          return '$T' + e;\n        throw r.$$typeof === ca\n          ? Error(\n              'Could not reference an opaque temporary reference. This is likely due to misconfiguring the temporaryReferences options on the server.'\n            )\n          : /^on[A-Z]/.test(i)\n          ? Error(\n              'Event handlers cannot be passed to Client Component props.' +\n                _s(s, i) +\n                `\nIf you need interactivity, consider converting part of this to a Client Component.`\n            )\n          : Error(\n              'Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with \"use server\". Or maybe you meant to call this function rather than return it.' +\n                _s(s, i)\n            );\n      }\n      if (typeof r == 'symbol') {\n        if (((t = e.writtenSymbols), (a = t.get(r)), a !== void 0))\n          return Ct(a);\n        if (((a = r.description), Symbol.for(a) !== r))\n          throw Error(\n            'Only global symbols received from Symbol.for(...) can be passed to Client Components. The symbol Symbol.for(' +\n              (r.description + ') cannot be found among global symbols.') +\n              _s(s, i)\n          );\n        return (\n          e.pendingChunks++,\n          (i = e.nextChunkId++),\n          (s = Lu(e, i, '$S' + a)),\n          e.completedImportChunks.push(s),\n          t.set(r, i),\n          Ct(i)\n        );\n      }\n      if (typeof r == 'bigint') return '$n' + r.toString(10);\n      throw Error(\n        'Type ' +\n          typeof r +\n          ' is not supported in Client Component props.' +\n          _s(s, i)\n      );\n    }\n    function Kn(e, t) {\n      var s = st;\n      st = null;\n      try {\n        var i = e.onError,\n          r = i(t);\n      } finally {\n        st = s;\n      }\n      if (r != null && typeof r != 'string')\n        throw Error(\n          'onError returned something with a type other than \"string\". onError should return a string and may return null or undefined but must not return anything else. It received something of type \"' +\n            typeof r +\n            '\" instead'\n        );\n      return r || '';\n    }\n    function Ki(e, t) {\n      var s = e.onFatalError;\n      s(t),\n        e.destination !== null\n          ? ((e.status = 14), xu(e.destination, t))\n          : ((e.status = 13), (e.fatalError = t)),\n        e.cacheController.abort(\n          Error('The render was aborted due to a fatal error.', {cause: t})\n        );\n    }\n    function Rr(e, t, s) {\n      (s = {digest: s}),\n        (t =\n          t.toString(16) +\n          ':E' +\n          Es(s) +\n          `\n`),\n        (t = pn(t)),\n        e.completedErrorChunks.push(t);\n    }\n    function Ou(e, t, s) {\n      (t =\n        t.toString(16) +\n        ':' +\n        s +\n        `\n`),\n        (t = pn(t)),\n        e.completedRegularChunks.push(t);\n    }\n    function Kt(e, t, s, i, r) {\n      r ? e.pendingDebugChunks++ : e.pendingChunks++,\n        (r = new Uint8Array(i.buffer, i.byteOffset, i.byteLength)),\n        (i = 2048 < i.byteLength ? r.slice() : r),\n        (r = i.byteLength),\n        (t = t.toString(16) + ':' + s + r.toString(16) + ','),\n        (t = pn(t)),\n        e.completedRegularChunks.push(t, i);\n    }\n    function Du(e, t, s, i) {\n      if (la === null)\n        throw Error(\n          'Existence of byteLengthOfChunk should have already been checked. This is a bug in React.'\n        );\n      i ? e.pendingDebugChunks++ : e.pendingChunks++,\n        (s = pn(s)),\n        (i = s.byteLength),\n        (t = t.toString(16) + ':T' + i.toString(16) + ','),\n        (t = pn(t)),\n        e.completedRegularChunks.push(t, s);\n    }\n    function Mu(e, t, s) {\n      var i = t.id;\n      typeof s == 'string' && la !== null\n        ? Du(e, i, s, !1)\n        : s instanceof ArrayBuffer\n        ? Kt(e, i, 'A', new Uint8Array(s), !1)\n        : s instanceof Int8Array\n        ? Kt(e, i, 'O', s, !1)\n        : s instanceof Uint8Array\n        ? Kt(e, i, 'o', s, !1)\n        : s instanceof Uint8ClampedArray\n        ? Kt(e, i, 'U', s, !1)\n        : s instanceof Int16Array\n        ? Kt(e, i, 'S', s, !1)\n        : s instanceof Uint16Array\n        ? Kt(e, i, 's', s, !1)\n        : s instanceof Int32Array\n        ? Kt(e, i, 'L', s, !1)\n        : s instanceof Uint32Array\n        ? Kt(e, i, 'l', s, !1)\n        : s instanceof Float32Array\n        ? Kt(e, i, 'G', s, !1)\n        : s instanceof Float64Array\n        ? Kt(e, i, 'g', s, !1)\n        : s instanceof BigInt64Array\n        ? Kt(e, i, 'M', s, !1)\n        : s instanceof BigUint64Array\n        ? Kt(e, i, 'm', s, !1)\n        : s instanceof DataView\n        ? Kt(e, i, 'V', s, !1)\n        : ((s = Es(s, t.toJSON)), Ou(e, t.id, s));\n    }\n    function Un(e, t, s) {\n      (t.status = 4),\n        (s = Kn(e, s, t)),\n        Rr(e, t.id, s),\n        e.abortableTasks.delete(t),\n        Or(e);\n    }\n    var Lr = {};\n    function Fu(e, t) {\n      if (t.status === 0) {\n        t.status = 5;\n        var s = is;\n        try {\n          ss = t.model;\n          var i = qi(e, t, Lr, '', t.model);\n          if (\n            ((ss = i),\n            (t.keyPath = null),\n            (t.implicitSlot = !1),\n            typeof i == 'object' && i !== null)\n          )\n            e.writtenObjects.set(i, Ct(t.id)), Mu(e, t, i);\n          else {\n            var r = Es(i);\n            Ou(e, t.id, r);\n          }\n          (t.status = 1), e.abortableTasks.delete(t), Or(e);\n        } catch (y) {\n          if (e.status === 12)\n            if ((e.abortableTasks.delete(t), (t.status = 0), e.type === 21))\n              ri(t), oi(t, e);\n            else {\n              var a = e.fatalError;\n              ha(t), fa(t, e, a);\n            }\n          else {\n            var u = y === pa ? Eu() : y;\n            if (\n              typeof u == 'object' &&\n              u !== null &&\n              typeof u.then == 'function'\n            ) {\n              (t.status = 0), (t.thenableState = Au());\n              var d = t.ping;\n              u.then(d, d);\n            } else Un(e, t, u);\n          }\n        } finally {\n          is = s;\n        }\n      }\n    }\n    function Bu(e, t) {\n      var s = is;\n      try {\n        Mu(e, t, t.model);\n      } finally {\n        is = s;\n      }\n    }\n    function ra(e) {\n      var t = Is.H;\n      Is.H = Pu;\n      var s = st;\n      Mi = st = e;\n      try {\n        var i = e.pingedTasks;\n        e.pingedTasks = [];\n        for (var r = 0; r < i.length; r++) Fu(e, i[r]);\n        ai(e);\n      } catch (a) {\n        Kn(e, a, null), Ki(e, a);\n      } finally {\n        (Is.H = t), (Mi = null), (st = s);\n      }\n    }\n    function ha(e) {\n      e.status === 0 && (e.status = 3);\n    }\n    function fa(e, t, s) {\n      e.status === 3 &&\n        ((s = Ct(s)), (e = Lu(t, e.id, s)), t.completedErrorChunks.push(e));\n    }\n    function ri(e) {\n      e.status === 0 && (e.status = 3);\n    }\n    function oi(e, t) {\n      e.status === 3 && t.pendingChunks--;\n    }\n    function ai(e) {\n      var t = e.destination;\n      if (t !== null) {\n        (ln = new Uint8Array(2048)), (cn = 0);\n        try {\n          for (var s = e.completedImportChunks, i = 0; i < s.length; i++)\n            e.pendingChunks--, Cr(t, s[i]);\n          s.splice(0, i);\n          var r = e.completedHintChunks;\n          for (i = 0; i < r.length; i++) Cr(t, r[i]);\n          r.splice(0, i);\n          var a = e.completedRegularChunks;\n          for (i = 0; i < a.length; i++) e.pendingChunks--, Cr(t, a[i]);\n          a.splice(0, i);\n          var u = e.completedErrorChunks;\n          for (i = 0; i < u.length; i++) e.pendingChunks--, Cr(t, u[i]);\n          u.splice(0, i);\n        } finally {\n          (e.flushScheduled = !1),\n            ln &&\n              0 < cn &&\n              (t.enqueue(new Uint8Array(ln.buffer, 0, cn)),\n              (ln = null),\n              (cn = 0));\n        }\n      }\n      e.pendingChunks === 0 &&\n        (12 > e.status &&\n          e.cacheController.abort(\n            Error(\n              'This render completed successfully. All cacheSignals are now aborted to allow clean up of any unused resources.'\n            )\n          ),\n        e.destination !== null &&\n          ((e.status = 14), e.destination.close(), (e.destination = null)));\n    }\n    function Vu(e) {\n      (e.flushScheduled = e.destination !== null),\n        vu(function () {\n          return ra(e);\n        }),\n        Bi(function () {\n          e.status === 10 && (e.status = 11);\n        });\n    }\n    function un(e) {\n      e.flushScheduled === !1 &&\n        e.pingedTasks.length === 0 &&\n        e.destination !== null &&\n        ((e.flushScheduled = !0),\n        Bi(function () {\n          (e.flushScheduled = !1), ai(e);\n        }));\n    }\n    function Or(e) {\n      e.abortableTasks.size === 0 && ((e = e.onAllReady), e());\n    }\n    function ju(e, t) {\n      if (e.status === 13) (e.status = 14), xu(t, e.fatalError);\n      else if (e.status !== 14 && e.destination === null) {\n        e.destination = t;\n        try {\n          ai(e);\n        } catch (s) {\n          Kn(e, s, null), Ki(e, s);\n        }\n      }\n    }\n    function Id(e, t) {\n      try {\n        t.forEach(function (i) {\n          return oi(i, e);\n        });\n        var s = e.onAllReady;\n        s(), ai(e);\n      } catch (i) {\n        Kn(e, i, null), Ki(e, i);\n      }\n    }\n    function Ed(e, t, s) {\n      try {\n        t.forEach(function (r) {\n          return fa(r, e, s);\n        });\n        var i = e.onAllReady;\n        i(), ai(e);\n      } catch (r) {\n        Kn(e, r, null), Ki(e, r);\n      }\n    }\n    function si(e, t) {\n      if (!(11 < e.status))\n        try {\n          (e.status = 12), e.cacheController.abort(t);\n          var s = e.abortableTasks;\n          if (0 < s.size)\n            if (e.type === 21)\n              s.forEach(function (d) {\n                return ri(d, e);\n              }),\n                Bi(function () {\n                  return Id(e, s);\n                });\n            else {\n              var i =\n                  t === void 0\n                    ? Error(\n                        'The render was aborted by the server without a reason.'\n                      )\n                    : typeof t == 'object' &&\n                      t !== null &&\n                      typeof t.then == 'function'\n                    ? Error(\n                        'The render was aborted by the server with a promise.'\n                      )\n                    : t,\n                r = Kn(e, i, null),\n                a = e.nextChunkId++;\n              (e.fatalError = a),\n                e.pendingChunks++,\n                Rr(e, a, r, i, !1, null),\n                s.forEach(function (d) {\n                  return ha(d, e, a);\n                }),\n                Bi(function () {\n                  return Ed(e, s, a);\n                });\n            }\n          else {\n            var u = e.onAllReady;\n            u(), ai(e);\n          }\n        } catch (d) {\n          Kn(e, d, null), Ki(e, d);\n        }\n    }\n    function $u(e, t) {\n      var s = '',\n        i = e[t];\n      if (i) s = i.name;\n      else {\n        var r = t.lastIndexOf('#');\n        if ((r !== -1 && ((s = t.slice(r + 1)), (i = e[t.slice(0, r)])), !i))\n          throw Error(\n            'Could not find the module \"' +\n              t +\n              '\" in the React Server Manifest. This is probably a bug in the React Server Components bundler.'\n          );\n      }\n      return i.async ? [i.id, i.chunks, s, 1] : [i.id, i.chunks, s];\n    }\n    var wr = new Map();\n    function hu(e) {\n      var t = __webpack_require__(e);\n      return typeof t.then != 'function' || t.status === 'fulfilled'\n        ? null\n        : (t.then(\n            function (s) {\n              (t.status = 'fulfilled'), (t.value = s);\n            },\n            function (s) {\n              (t.status = 'rejected'), (t.reason = s);\n            }\n          ),\n          t);\n    }\n    function Ad() {}\n    function qu(e) {\n      for (var t = e[1], s = [], i = 0; i < t.length; ) {\n        var r = t[i++],\n          a = t[i++],\n          u = wr.get(r);\n        u === void 0\n          ? (Ku.set(r, a),\n            (a = __webpack_chunk_load__(r)),\n            s.push(a),\n            (u = wr.set.bind(wr, r, null)),\n            a.then(u, Ad),\n            wr.set(r, a))\n          : u !== null && s.push(u);\n      }\n      return e.length === 4\n        ? s.length === 0\n          ? hu(e[0])\n          : Promise.all(s).then(function () {\n              return hu(e[0]);\n            })\n        : 0 < s.length\n        ? Promise.all(s)\n        : null;\n    }\n    function Fi(e) {\n      var t = __webpack_require__(e[0]);\n      if (e.length === 4 && typeof t.then == 'function')\n        if (t.status === 'fulfilled') t = t.value;\n        else throw t.reason;\n      if (e[2] === '*') return t;\n      if (e[2] === '') return t.__esModule ? t.default : t;\n      if (Nr.call(t, e[2])) return t[e[2]];\n    }\n    var Ku = new Map(),\n      Pd = __webpack_require__.u;\n    __webpack_require__.u = function (e) {\n      var t = Ku.get(e);\n      return t !== void 0 ? t : Pd(e);\n    };\n    var Dr = Symbol();\n    function Ot(e, t, s) {\n      (this.status = e), (this.value = t), (this.reason = s);\n    }\n    Ot.prototype = Object.create(Promise.prototype);\n    Ot.prototype.then = function (e, t) {\n      switch (this.status) {\n        case 'resolved_model':\n          Br(this);\n      }\n      switch (this.status) {\n        case 'fulfilled':\n          if (typeof e == 'function') {\n            for (var s = this.value, i = 0, r = new Set(); s instanceof Ot; ) {\n              if ((i++, s === this || r.has(s) || 1e3 < i)) {\n                typeof t == 'function' &&\n                  t(Error('Cannot have cyclic thenables.'));\n                return;\n              }\n              if ((r.add(s), s.status === 'fulfilled')) s = s.value;\n              else break;\n            }\n            e(this.value);\n          }\n          break;\n        case 'pending':\n        case 'blocked':\n          typeof e == 'function' &&\n            (this.value === null && (this.value = []), this.value.push(e)),\n            typeof t == 'function' &&\n              (this.reason === null && (this.reason = []), this.reason.push(t));\n          break;\n        default:\n          typeof t == 'function' && t(this.reason);\n      }\n    };\n    var Uu = Object.prototype,\n      Hu = Array.prototype;\n    function Mr(e, t, s, i) {\n      for (var r = 0; r < t.length; r++) {\n        var a = t[r];\n        typeof a == 'function' ? a(s) : zu(e, a, s, i.reason);\n      }\n    }\n    function da(e, t, s) {\n      for (var i = 0; i < t.length; i++) {\n        var r = t[i];\n        typeof r == 'function' ? r(s) : Pr(e, r.handler, s);\n      }\n    }\n    function Fr(e, t, s) {\n      if (t.status !== 'pending' && t.status !== 'blocked') t.reason.error(s);\n      else {\n        var i = t.reason;\n        (t.status = 'rejected'), (t.reason = s), i !== null && da(e, i, s);\n      }\n    }\n    function Wu(e, t, s) {\n      var i = {};\n      return new Ot('resolved_model', t, ((i.id = s), (i[Dr] = e), i));\n    }\n    function Gu(e, t, s, i) {\n      if (t.status !== 'pending')\n        (t = t.reason),\n          s[0] === 'C'\n            ? t.close(s === 'C' ? '\"$undefined\"' : s.slice(1))\n            : t.enqueueModel(s);\n      else {\n        var r = t.value,\n          a = t.reason;\n        if (\n          ((t.status = 'resolved_model'),\n          (t.value = s),\n          (s = {}),\n          (t.reason = ((s.id = i), (s[Dr] = e), s)),\n          r !== null)\n        )\n          switch ((Br(t), t.status)) {\n            case 'fulfilled':\n              Mr(e, r, t.value, t);\n              break;\n            case 'blocked':\n            case 'pending':\n              if (t.value) for (e = 0; e < r.length; e++) t.value.push(r[e]);\n              else t.value = r;\n              if (t.reason) {\n                if (a) for (r = 0; r < a.length; r++) t.reason.push(a[r]);\n              } else t.reason = a;\n              break;\n            case 'rejected':\n              a && da(e, a, t.reason);\n          }\n      }\n    }\n    function fu(e, t, s) {\n      var i = {};\n      return new Ot(\n        'resolved_model',\n        (s ? '{\"done\":true,\"value\":' : '{\"done\":false,\"value\":') + t + '}',\n        ((i.id = -1), (i[Dr] = e), i)\n      );\n    }\n    function ea(e, t, s, i) {\n      Gu(\n        e,\n        t,\n        (i ? '{\"done\":true,\"value\":' : '{\"done\":false,\"value\":') + s + '}',\n        -1\n      );\n    }\n    function Nd(e, t, s, i) {\n      function r(L) {\n        var p = d.reason,\n          h = d;\n        (h.status = 'rejected'),\n          (h.value = null),\n          (h.reason = L),\n          p !== null && da(e, p, L),\n          Pr(e, g, L);\n      }\n      var a = t.id;\n      if (typeof a != 'string' || i === 'then') return null;\n      var u = t.$$promise;\n      if (u !== void 0)\n        return u.status === 'fulfilled'\n          ? ((u = u.value), i === '__proto__' ? null : (s[i] = u))\n          : (Ye\n              ? ((a = Ye), a.deps++)\n              : (a = Ye =\n                  {\n                    chunk: null,\n                    value: null,\n                    reason: null,\n                    deps: 1,\n                    errored: !1,\n                  }),\n            u.then(aa.bind(null, e, a, s, i), Pr.bind(null, e, a)),\n            null);\n      var d = new Ot('blocked', null, null);\n      t.$$promise = d;\n      var y = $u(e._bundlerConfig, a);\n      if (((u = t.bound), (a = qu(y))))\n        u instanceof Ot && (a = Promise.all([a, u]));\n      else if (u instanceof Ot) a = Promise.resolve(u);\n      else return (u = Fi(y)), (a = d), (a.status = 'fulfilled'), (a.value = u);\n      if (Ye) {\n        var g = Ye;\n        g.deps++;\n      } else\n        g = Ye = {chunk: null, value: null, reason: null, deps: 1, errored: !1};\n      return (\n        a.then(function () {\n          var L = Fi(y);\n          if (t.bound) {\n            var p = t.bound.value;\n            if (((p = kn(p) ? p.slice(0) : []), 1e3 < p.length)) {\n              r(\n                Error(\n                  'Server Function has too many bound arguments. Received ' +\n                    p.length +\n                    ' but the limit is 1000.'\n                )\n              );\n              return;\n            }\n            p.unshift(null), (L = L.bind.apply(L, p));\n          }\n          p = d.value;\n          var h = d;\n          (h.status = 'fulfilled'),\n            (h.value = L),\n            (h.reason = null),\n            p !== null && Mr(e, p, L, h),\n            aa(e, g, s, i, L);\n        }, r),\n        null\n      );\n    }\n    function oa(e, t, s, i, r, a) {\n      if (typeof i == 'string') return Fd(e, t, s, i, r, a);\n      if (typeof i == 'object' && i !== null)\n        if (\n          (r !== void 0 &&\n            e._temporaryReferences !== void 0 &&\n            e._temporaryReferences.set(i, r),\n          kn(i))\n        ) {\n          if (a === null) {\n            var u = {count: 0, fork: !1};\n            e._rootArrayContexts.set(i, u);\n          } else u = a;\n          for (\n            1 < i.length && (u.fork = !0), $n(u, i.length + 1, e), t = 0;\n            t < i.length;\n            t++\n          )\n            i[t] = oa(\n              e,\n              i,\n              '' + t,\n              i[t],\n              r !== void 0 ? r + ':' + t : void 0,\n              u\n            );\n        } else\n          for (u in i)\n            Nr.call(i, u) &&\n              (u === '__proto__'\n                ? delete i[u]\n                : ((t =\n                    r !== void 0 && u.indexOf(':') === -1\n                      ? r + ':' + u\n                      : void 0),\n                  (t = oa(e, i, u, i[u], t, null)),\n                  t !== void 0 ? (i[u] = t) : delete i[u]));\n      return i;\n    }\n    function $n(e, t, s) {\n      if ((e.count += t) > s._arraySizeLimit && e.fork)\n        throw Error(\n          'Maximum array nesting exceeded. Large nested arrays can be dangerous. Try adding intermediate objects.'\n        );\n    }\n    var Ye = null;\n    function Br(e) {\n      var t = Ye;\n      Ye = null;\n      var s = e.reason,\n        i = s[Dr];\n      (s = s.id), (s = s === -1 ? void 0 : s.toString(16));\n      var r = e.value;\n      (e.status = 'blocked'), (e.value = null), (e.reason = null);\n      try {\n        var a = JSON.parse(r);\n        r = {count: 0, fork: !1};\n        var u = oa(i, {'': a}, '', a, s, r),\n          d = e.value;\n        if (d !== null)\n          for (e.value = null, e.reason = null, a = 0; a < d.length; a++) {\n            var y = d[a];\n            typeof y == 'function' ? y(u) : zu(i, y, u, r);\n          }\n        if (Ye !== null) {\n          if (Ye.errored) throw Ye.reason;\n          if (0 < Ye.deps) {\n            (Ye.value = u), (Ye.reason = r), (Ye.chunk = e);\n            return;\n          }\n        }\n        (e.status = 'fulfilled'), (e.value = u), (e.reason = r);\n      } catch (g) {\n        (e.status = 'rejected'), (e.reason = g);\n      } finally {\n        Ye = t;\n      }\n    }\n    function Rd(e, t) {\n      (e._closed = !0),\n        (e._closedReason = t),\n        e._chunks.forEach(function (s) {\n          s.status === 'pending'\n            ? Fr(e, s, t)\n            : s.status === 'fulfilled' &&\n              s.reason !== null &&\n              ((s = s.reason), typeof s.error == 'function' && s.error(t));\n        });\n    }\n    function Vr(e, t) {\n      var s = e._chunks,\n        i = s.get(t);\n      return (\n        i ||\n          ((i = e._formData.get(e._prefix + t)),\n          (i =\n            typeof i == 'string'\n              ? Wu(e, i, t)\n              : e._closed\n              ? new Ot('rejected', null, e._closedReason)\n              : new Ot('pending', null, null)),\n          s.set(t, i)),\n        i\n      );\n    }\n    function zu(e, t, s, i) {\n      var r = t.handler,\n        a = t.parentObject,\n        u = t.key,\n        d = t.map,\n        y = t.path;\n      try {\n        for (var g = 0, L = e._rootArrayContexts, p = 1; p < y.length; p++) {\n          var h = y[p];\n          if (\n            typeof s != 'object' ||\n            s === null ||\n            (ii(s) !== Uu && ii(s) !== Hu) ||\n            !Nr.call(s, h)\n          )\n            throw Error('Invalid reference.');\n          if (((s = s[h]), kn(s))) (g = 0), (i = L.get(s) || i);\n          else if (((i = null), typeof s == 'string')) g = s.length;\n          else if (typeof s == 'bigint') {\n            var T = Math.abs(Number(s));\n            g = T === 0 ? 1 : Math.floor(Math.log10(T)) + 1;\n          } else g = ArrayBuffer.isView(s) ? s.byteLength : 0;\n        }\n        var x = d(e, s, a, u),\n          w = t.arrayRoot;\n        w !== null &&\n          (i !== null\n            ? (i.fork && (w.fork = !0), $n(w, i.count, e))\n            : 0 < g && $n(w, g, e));\n      } catch (S) {\n        Pr(e, r, S);\n        return;\n      }\n      aa(e, r, a, u, x);\n    }\n    function aa(e, t, s, i, r) {\n      i !== '__proto__' && (s[i] = r),\n        i === '' && t.value === null && (t.value = r),\n        t.deps--,\n        t.deps === 0 &&\n          ((s = t.chunk),\n          s !== null &&\n            s.status === 'blocked' &&\n            ((i = s.value),\n            (s.status = 'fulfilled'),\n            (s.value = t.value),\n            (s.reason = t.reason),\n            i !== null && Mr(e, i, t.value, s)));\n    }\n    function Pr(e, t, s) {\n      t.errored ||\n        ((t.errored = !0),\n        (t.value = null),\n        (t.reason = s),\n        (t = t.chunk),\n        t !== null && t.status === 'blocked' && Fr(e, t, s));\n    }\n    function Di(e, t, s, i, r, a) {\n      t = t.split(':');\n      var u = parseInt(t[0], 16),\n        d = Vr(e, u);\n      switch (d.status) {\n        case 'resolved_model':\n          Br(d);\n      }\n      switch (d.status) {\n        case 'fulfilled':\n          (u = d.value), (d = d.reason);\n          for (var y = 0, g = e._rootArrayContexts, L = 1; L < t.length; L++) {\n            if (\n              ((y = t[L]),\n              typeof u != 'object' ||\n                u === null ||\n                (ii(u) !== Uu && ii(u) !== Hu) ||\n                !Nr.call(u, y))\n            )\n              throw Error('Invalid reference.');\n            (u = u[y]),\n              kn(u)\n                ? ((y = 0), (d = g.get(u) || d))\n                : ((d = null),\n                  typeof u == 'string'\n                    ? (y = u.length)\n                    : typeof u == 'bigint'\n                    ? ((y = Math.abs(Number(u))),\n                      (y = y === 0 ? 1 : Math.floor(Math.log10(y)) + 1))\n                    : (y = ArrayBuffer.isView(u) ? u.byteLength : 0));\n          }\n          return (\n            (s = a(e, u, s, i)),\n            r !== null &&\n              (d !== null\n                ? (d.fork && (r.fork = !0), $n(r, d.count, e))\n                : 0 < y && $n(r, y, e)),\n            s\n          );\n        case 'blocked':\n          return (\n            Ye\n              ? ((e = Ye), e.deps++)\n              : (e = Ye =\n                  {\n                    chunk: null,\n                    value: null,\n                    reason: null,\n                    deps: 1,\n                    errored: !1,\n                  }),\n            (r = {\n              handler: e,\n              parentObject: s,\n              key: i,\n              map: a,\n              path: t,\n              arrayRoot: r,\n            }),\n            d.value === null ? (d.value = [r]) : d.value.push(r),\n            d.reason === null ? (d.reason = [r]) : d.reason.push(r),\n            null\n          );\n        case 'pending':\n          throw Error('Invalid forward reference.');\n        default:\n          return (\n            Ye\n              ? ((Ye.errored = !0), (Ye.value = null), (Ye.reason = d.reason))\n              : (Ye = {\n                  chunk: null,\n                  value: null,\n                  reason: d.reason,\n                  deps: 0,\n                  errored: !0,\n                }),\n            null\n          );\n      }\n    }\n    function Ld(e, t) {\n      if (!kn(t)) throw Error('Invalid Map initializer.');\n      if (t.$$consumed === !0) throw Error('Already initialized Map.');\n      return (e = new Map(t)), (t.$$consumed = !0), e;\n    }\n    function Od(e, t) {\n      if (!kn(t)) throw Error('Invalid Set initializer.');\n      if (t.$$consumed === !0) throw Error('Already initialized Set.');\n      return (e = new Set(t)), (t.$$consumed = !0), e;\n    }\n    function Dd(e, t) {\n      if (!kn(t)) throw Error('Invalid Iterator initializer.');\n      if (t.$$consumed === !0) throw Error('Already initialized Iterator.');\n      return (e = t[Symbol.iterator]()), (t.$$consumed = !0), e;\n    }\n    function Md(e, t, s, i) {\n      return i === 'then' && typeof t == 'function' ? null : t;\n    }\n    function Jt(e, t, s, i, r, a, u) {\n      function d(L) {\n        if (!g.errored) {\n          (g.errored = !0), (g.value = null), (g.reason = L);\n          var p = g.chunk;\n          p !== null && p.status === 'blocked' && Fr(e, p, L);\n        }\n      }\n      t = parseInt(t.slice(2), 16);\n      var y = e._prefix + t;\n      if (((i = e._chunks), i.has(t)))\n        throw Error('Already initialized typed array.');\n      if (\n        (i.set(\n          t,\n          new Ot('rejected', null, Error('Already initialized typed array.'))\n        ),\n        (t = e._formData.get(y).arrayBuffer()),\n        Ye)\n      ) {\n        var g = Ye;\n        g.deps++;\n      } else\n        g = Ye = {chunk: null, value: null, reason: null, deps: 1, errored: !1};\n      return (\n        t.then(function (L) {\n          try {\n            u !== null && $n(u, L.byteLength, e);\n            var p = s === ArrayBuffer ? L : new s(L);\n            y !== '__proto__' && (r[a] = p),\n              a === '' && g.value === null && (g.value = p);\n          } catch (h) {\n            d(h);\n            return;\n          }\n          g.deps--,\n            g.deps === 0 &&\n              ((L = g.chunk),\n              L !== null &&\n                L.status === 'blocked' &&\n                ((p = L.value),\n                (L.status = 'fulfilled'),\n                (L.value = g.value),\n                (L.reason = null),\n                p !== null && Mr(e, p, g.value, L)));\n        }, d),\n        null\n      );\n    }\n    function Xu(e, t, s, i) {\n      var r = e._chunks;\n      for (\n        s = new Ot('fulfilled', s, i),\n          r.set(t, s),\n          e = e._formData.getAll(e._prefix + t),\n          t = 0;\n        t < e.length;\n        t++\n      )\n        (r = e[t]),\n          typeof r == 'string' &&\n            (r[0] === 'C'\n              ? i.close(r === 'C' ? '\"$undefined\"' : r.slice(1))\n              : i.enqueueModel(r));\n    }\n    function du(e, t, s) {\n      function i(g) {\n        s !== 'bytes' || ArrayBuffer.isView(g)\n          ? r.enqueue(g)\n          : y.error(Error('Invalid data for bytes stream.'));\n      }\n      if (((t = parseInt(t.slice(2), 16)), e._chunks.has(t)))\n        throw Error('Already initialized stream.');\n      var r = null,\n        a = !1,\n        u = new ReadableStream({\n          type: s,\n          start: function (g) {\n            r = g;\n          },\n        }),\n        d = null,\n        y = {\n          enqueueModel: function (g) {\n            if (d === null) {\n              var L = Wu(e, g, -1);\n              Br(L),\n                L.status === 'fulfilled'\n                  ? i(L.value)\n                  : (L.then(i, y.error), (d = L));\n            } else {\n              L = d;\n              var p = new Ot('pending', null, null);\n              p.then(i, y.error),\n                (d = p),\n                L.then(function () {\n                  d === p && (d = null), Gu(e, p, g, -1);\n                });\n            }\n          },\n          close: function () {\n            if (!a)\n              if (((a = !0), d === null)) r.close();\n              else {\n                var g = d;\n                (d = null),\n                  g.then(function () {\n                    return r.close();\n                  });\n              }\n          },\n          error: function (g) {\n            if (!a)\n              if (((a = !0), d === null)) r.error(g);\n              else {\n                var L = d;\n                (d = null),\n                  L.then(function () {\n                    return r.error(g);\n                  });\n              }\n          },\n        };\n      return Xu(e, t, u, y), u;\n    }\n    function ma(e) {\n      this.next = e;\n    }\n    ma.prototype = {};\n    ma.prototype[Ss] = function () {\n      return this;\n    };\n    function mu(e, t, s) {\n      if (((t = parseInt(t.slice(2), 16)), e._chunks.has(t)))\n        throw Error('Already initialized stream.');\n      var i = [],\n        r = !1,\n        a = 0,\n        u = {};\n      return (\n        (u =\n          ((u[Ss] = function () {\n            var d = 0;\n            return new ma(function (y) {\n              if (y !== void 0)\n                throw Error(\n                  'Values cannot be passed to next() of AsyncIterables passed to Client Components.'\n                );\n              if (d === i.length) {\n                if (r)\n                  return new Ot('fulfilled', {done: !0, value: void 0}, null);\n                i[d] = new Ot('pending', null, null);\n              }\n              return i[d++];\n            });\n          }),\n          u)),\n        (s = s ? u[Ss]() : u),\n        Xu(e, t, s, {\n          enqueueModel: function (d) {\n            a === i.length ? (i[a] = fu(e, d, !1)) : ea(e, i[a], d, !1), a++;\n          },\n          close: function (d) {\n            if (!r)\n              for (\n                r = !0,\n                  a === i.length ? (i[a] = fu(e, d, !0)) : ea(e, i[a], d, !0),\n                  a++;\n                a < i.length;\n\n              )\n                ea(e, i[a++], '\"$undefined\"', !0);\n          },\n          error: function (d) {\n            if (!r)\n              for (\n                r = !0,\n                  a === i.length && (i[a] = new Ot('pending', null, null));\n                a < i.length;\n\n              )\n                Fr(e, i[a++], d);\n          },\n        }),\n        s\n      );\n    }\n    function Fd(e, t, s, i, r, a) {\n      if (i[0] === '$') {\n        switch (i[1]) {\n          case '$':\n            return a !== null && $n(a, i.length - 1, e), i.slice(1);\n          case '@':\n            return (t = parseInt(i.slice(2), 16)), Vr(e, t);\n          case 'h':\n            return (a = i.slice(2)), Di(e, a, t, s, null, Nd);\n          case 'T':\n            if (r === void 0 || e._temporaryReferences === void 0)\n              throw Error(\n                'Could not reference an opaque temporary reference. This is likely due to misconfiguring the temporaryReferences options on the server.'\n              );\n            return ud(e._temporaryReferences, r);\n          case 'Q':\n            return (a = i.slice(2)), Di(e, a, t, s, null, Ld);\n          case 'W':\n            return (a = i.slice(2)), Di(e, a, t, s, null, Od);\n          case 'K':\n            for (\n              t = i.slice(2),\n                t = e._prefix + t + '_',\n                s = new FormData(),\n                e = e._formData,\n                a = Array.from(e.keys()),\n                i = 0;\n              i < a.length;\n              i++\n            )\n              if (((r = a[i]), r.startsWith(t))) {\n                for (\n                  var u = e.getAll(r), d = r.slice(t.length), y = 0;\n                  y < u.length;\n                  y++\n                )\n                  s.append(d, u[y]);\n                e.delete(r);\n              }\n            return s;\n          case 'i':\n            return (a = i.slice(2)), Di(e, a, t, s, null, Dd);\n          case 'I':\n            return 1 / 0;\n          case '-':\n            return i === '$-0' ? -0 : -1 / 0;\n          case 'N':\n            return NaN;\n          case 'u':\n            return;\n          case 'D':\n            return new Date(Date.parse(i.slice(2)));\n          case 'n':\n            if (((t = i.slice(2)), 300 < t.length))\n              throw Error(\n                'BigInt is too large. Received ' +\n                  t.length +\n                  ' digits but the limit is 300.'\n              );\n            return a !== null && $n(a, t.length, e), BigInt(t);\n          case 'A':\n            return Jt(e, i, ArrayBuffer, 1, t, s, a);\n          case 'O':\n            return Jt(e, i, Int8Array, 1, t, s, a);\n          case 'o':\n            return Jt(e, i, Uint8Array, 1, t, s, a);\n          case 'U':\n            return Jt(e, i, Uint8ClampedArray, 1, t, s, a);\n          case 'S':\n            return Jt(e, i, Int16Array, 2, t, s, a);\n          case 's':\n            return Jt(e, i, Uint16Array, 2, t, s, a);\n          case 'L':\n            return Jt(e, i, Int32Array, 4, t, s, a);\n          case 'l':\n            return Jt(e, i, Uint32Array, 4, t, s, a);\n          case 'G':\n            return Jt(e, i, Float32Array, 4, t, s, a);\n          case 'g':\n            return Jt(e, i, Float64Array, 8, t, s, a);\n          case 'M':\n            return Jt(e, i, BigInt64Array, 8, t, s, a);\n          case 'm':\n            return Jt(e, i, BigUint64Array, 8, t, s, a);\n          case 'V':\n            return Jt(e, i, DataView, 1, t, s, a);\n          case 'B':\n            return (\n              (t = parseInt(i.slice(2), 16)), e._formData.get(e._prefix + t)\n            );\n          case 'R':\n            return du(e, i, void 0);\n          case 'r':\n            return du(e, i, 'bytes');\n          case 'X':\n            return mu(e, i, !1);\n          case 'x':\n            return mu(e, i, !0);\n        }\n        return (i = i.slice(1)), Di(e, i, t, s, a, Md);\n      }\n      return a !== null && $n(a, i.length, e), i;\n    }\n    function Yu(e, t, s) {\n      var i =\n          3 < arguments.length && arguments[3] !== void 0\n            ? arguments[3]\n            : new FormData(),\n        r =\n          4 < arguments.length && arguments[4] !== void 0 ? arguments[4] : 1e6,\n        a = new Map();\n      return {\n        _bundlerConfig: e,\n        _prefix: t,\n        _formData: i,\n        _chunks: a,\n        _closed: !1,\n        _closedReason: null,\n        _temporaryReferences: s,\n        _rootArrayContexts: new WeakMap(),\n        _arraySizeLimit: r,\n      };\n    }\n    function Ju(e) {\n      Rd(e, Error('Connection closed.'));\n    }\n    function yu(e, t) {\n      var s = t.id;\n      if (typeof s != 'string') return null;\n      var i = $u(e, s);\n      return (\n        (e = qu(i)),\n        (t = t.bound),\n        t instanceof Promise\n          ? Promise.all([t, e]).then(function (r) {\n              r = r[0];\n              var a = Fi(i);\n              if (1e3 < r.length)\n                throw Error(\n                  'Server Function has too many bound arguments. Received ' +\n                    r.length +\n                    ' but the limit is 1000.'\n                );\n              return a.bind.apply(a, [null].concat(r));\n            })\n          : e\n          ? Promise.resolve(e).then(function () {\n              return Fi(i);\n            })\n          : Promise.resolve(Fi(i))\n      );\n    }\n    function Qu(e, t, s, i) {\n      if (\n        ((e = Yu(t, s, void 0, e, i)),\n        Ju(e),\n        (e = Vr(e, 0)),\n        e.then(function () {}),\n        e.status !== 'fulfilled')\n      )\n        throw e.reason;\n      return e.value;\n    }\n    En.createClientModuleProxy = function (e) {\n      return (e = ti({}, e, !1)), new Proxy(e, _u);\n    };\n    En.createTemporaryReferenceSet = function () {\n      return new WeakMap();\n    };\n    En.decodeAction = function (e, t) {\n      var s = new FormData(),\n        i = null,\n        r = new Set();\n      return (\n        e.forEach(function (a, u) {\n          u.startsWith('$ACTION_')\n            ? u.startsWith('$ACTION_REF_')\n              ? r.has(u) ||\n                (r.add(u),\n                (a = '$ACTION_' + u.slice(12) + ':'),\n                (a = Qu(e, t, a)),\n                (i = yu(t, a)))\n              : u.startsWith('$ACTION_ID_') &&\n                !r.has(u) &&\n                (r.add(u), (a = u.slice(11)), (i = yu(t, {id: a, bound: null})))\n            : s.append(u, a);\n        }),\n        i === null\n          ? null\n          : i.then(function (a) {\n              return a.bind(null, s);\n            })\n      );\n    };\n    En.decodeFormState = function (e, t, s) {\n      var i = t.get('$ACTION_KEY');\n      if (typeof i != 'string') return Promise.resolve(null);\n      var r = null;\n      if (\n        (t.forEach(function (u, d) {\n          d.startsWith('$ACTION_REF_') &&\n            ((u = '$ACTION_' + d.slice(12) + ':'), (r = Qu(t, s, u)));\n        }),\n        r === null)\n      )\n        return Promise.resolve(null);\n      var a = r.id;\n      return Promise.resolve(r.bound).then(function (u) {\n        return u === null ? null : [e, i, a, u.length - 1];\n      });\n    };\n    En.decodeReply = function (e, t, s) {\n      if (typeof e == 'string') {\n        var i = new FormData();\n        i.append('0', e), (e = i);\n      }\n      return (\n        (e = Yu(\n          t,\n          '',\n          s ? s.temporaryReferences : void 0,\n          e,\n          s ? s.arraySizeLimit : void 0\n        )),\n        (t = Vr(e, 0)),\n        Ju(e),\n        t\n      );\n    };\n    En.prerender = function (e, t, s) {\n      return new Promise(function (i, r) {\n        var a = new Ru(\n          21,\n          e,\n          t,\n          s ? s.onError : void 0,\n          s ? s.onPostpone : void 0,\n          function () {\n            var y = new ReadableStream(\n              {\n                type: 'bytes',\n                pull: function (g) {\n                  ju(a, g);\n                },\n                cancel: function (g) {\n                  (a.destination = null), si(a, g);\n                },\n              },\n              {highWaterMark: 0}\n            );\n            i({prelude: y});\n          },\n          r,\n          s ? s.identifierPrefix : void 0,\n          s ? s.temporaryReferences : void 0\n        );\n        if (s && s.signal) {\n          var u = s.signal;\n          if (u.aborted) si(a, u.reason);\n          else {\n            var d = function () {\n              si(a, u.reason), u.removeEventListener('abort', d);\n            };\n            u.addEventListener('abort', d);\n          }\n        }\n        Vu(a);\n      });\n    };\n    En.registerClientReference = function (e, t, s) {\n      return ti(e, t + '#' + s, !1);\n    };\n    En.registerServerReference = function (e, t, s) {\n      return Object.defineProperties(e, {\n        $$typeof: {value: Ar},\n        $$id: {value: s === null ? t : t + '#' + s, configurable: !0},\n        $$bound: {value: null, configurable: !0},\n        bind: {value: gu, configurable: !0},\n        toString: ed,\n      });\n    };\n    En.renderToReadableStream = function (e, t, s) {\n      var i = new Ru(\n        20,\n        e,\n        t,\n        s ? s.onError : void 0,\n        s ? s.onPostpone : void 0,\n        Cs,\n        Cs,\n        s ? s.identifierPrefix : void 0,\n        s ? s.temporaryReferences : void 0\n      );\n      if (s && s.signal) {\n        var r = s.signal;\n        if (r.aborted) si(i, r.reason);\n        else {\n          var a = function () {\n            si(i, r.reason), r.removeEventListener('abort', a);\n          };\n          r.addEventListener('abort', a);\n        }\n      }\n      return new ReadableStream(\n        {\n          type: 'bytes',\n          start: function () {\n            Vu(i);\n          },\n          pull: function (u) {\n            ju(i, u);\n          },\n          cancel: function (u) {\n            (i.destination = null), si(i, u);\n          },\n        },\n        {highWaterMark: 0}\n      );\n    };\n  });\n  var e1 = Z((Wn) => {\n    'use strict';\n    var Hn;\n    Hn = Zu();\n    Wn.renderToReadableStream = Hn.renderToReadableStream;\n    Wn.decodeReply = Hn.decodeReply;\n    Wn.decodeAction = Hn.decodeAction;\n    Wn.decodeFormState = Hn.decodeFormState;\n    Wn.registerServerReference = Hn.registerServerReference;\n    Wn.registerClientReference = Hn.registerClientReference;\n    Wn.createClientModuleProxy = Hn.createClientModuleProxy;\n    Wn.createTemporaryReferenceSet = Hn.createTemporaryReferenceSet;\n  });\n  var It = Z((ya) => {\n    'use strict';\n    Object.defineProperty(ya, '__esModule', {value: !0});\n    var t1;\n    (function (e) {\n      e[(e.NONE = 0)] = 'NONE';\n      let s = 1;\n      e[(e._abstract = s)] = '_abstract';\n      let i = s + 1;\n      e[(e._accessor = i)] = '_accessor';\n      let r = i + 1;\n      e[(e._as = r)] = '_as';\n      let a = r + 1;\n      e[(e._assert = a)] = '_assert';\n      let u = a + 1;\n      e[(e._asserts = u)] = '_asserts';\n      let d = u + 1;\n      e[(e._async = d)] = '_async';\n      let y = d + 1;\n      e[(e._await = y)] = '_await';\n      let g = y + 1;\n      e[(e._checks = g)] = '_checks';\n      let L = g + 1;\n      e[(e._constructor = L)] = '_constructor';\n      let p = L + 1;\n      e[(e._declare = p)] = '_declare';\n      let h = p + 1;\n      e[(e._enum = h)] = '_enum';\n      let T = h + 1;\n      e[(e._exports = T)] = '_exports';\n      let x = T + 1;\n      e[(e._from = x)] = '_from';\n      let w = x + 1;\n      e[(e._get = w)] = '_get';\n      let S = w + 1;\n      e[(e._global = S)] = '_global';\n      let A = S + 1;\n      e[(e._implements = A)] = '_implements';\n      let U = A + 1;\n      e[(e._infer = U)] = '_infer';\n      let M = U + 1;\n      e[(e._interface = M)] = '_interface';\n      let c = M + 1;\n      e[(e._is = c)] = '_is';\n      let R = c + 1;\n      e[(e._keyof = R)] = '_keyof';\n      let W = R + 1;\n      e[(e._mixins = W)] = '_mixins';\n      let X = W + 1;\n      e[(e._module = X)] = '_module';\n      let ie = X + 1;\n      e[(e._namespace = ie)] = '_namespace';\n      let pe = ie + 1;\n      e[(e._of = pe)] = '_of';\n      let ae = pe + 1;\n      e[(e._opaque = ae)] = '_opaque';\n      let He = ae + 1;\n      e[(e._out = He)] = '_out';\n      let qe = He + 1;\n      e[(e._override = qe)] = '_override';\n      let Bt = qe + 1;\n      e[(e._private = Bt)] = '_private';\n      let mt = Bt + 1;\n      e[(e._protected = mt)] = '_protected';\n      let kt = mt + 1;\n      e[(e._proto = kt)] = '_proto';\n      let At = kt + 1;\n      e[(e._public = At)] = '_public';\n      let tt = At + 1;\n      e[(e._readonly = tt)] = '_readonly';\n      let nt = tt + 1;\n      e[(e._require = nt)] = '_require';\n      let _t = nt + 1;\n      e[(e._satisfies = _t)] = '_satisfies';\n      let ct = _t + 1;\n      e[(e._set = ct)] = '_set';\n      let wt = ct + 1;\n      e[(e._static = wt)] = '_static';\n      let $t = wt + 1;\n      e[(e._symbol = $t)] = '_symbol';\n      let Pt = $t + 1;\n      e[(e._type = Pt)] = '_type';\n      let qt = Pt + 1;\n      e[(e._unique = qt)] = '_unique';\n      let Tn = qt + 1;\n      e[(e._using = Tn)] = '_using';\n    })(t1 || (ya.ContextualKeyword = t1 = {}));\n  });\n  var be = Z((jr) => {\n    'use strict';\n    Object.defineProperty(jr, '__esModule', {value: !0});\n    var q;\n    (function (e) {\n      e[(e.PRECEDENCE_MASK = 15)] = 'PRECEDENCE_MASK';\n      let s = 16;\n      e[(e.IS_KEYWORD = s)] = 'IS_KEYWORD';\n      let i = 32;\n      e[(e.IS_ASSIGN = i)] = 'IS_ASSIGN';\n      let r = 64;\n      e[(e.IS_RIGHT_ASSOCIATIVE = r)] = 'IS_RIGHT_ASSOCIATIVE';\n      let a = 128;\n      e[(e.IS_PREFIX = a)] = 'IS_PREFIX';\n      let u = 256;\n      e[(e.IS_POSTFIX = u)] = 'IS_POSTFIX';\n      let d = 512;\n      e[(e.IS_EXPRESSION_START = d)] = 'IS_EXPRESSION_START';\n      let y = 512;\n      e[(e.num = y)] = 'num';\n      let g = 1536;\n      e[(e.bigint = g)] = 'bigint';\n      let L = 2560;\n      e[(e.decimal = L)] = 'decimal';\n      let p = 3584;\n      e[(e.regexp = p)] = 'regexp';\n      let h = 4608;\n      e[(e.string = h)] = 'string';\n      let T = 5632;\n      e[(e.name = T)] = 'name';\n      let x = 6144;\n      e[(e.eof = x)] = 'eof';\n      let w = 7680;\n      e[(e.bracketL = w)] = 'bracketL';\n      let S = 8192;\n      e[(e.bracketR = S)] = 'bracketR';\n      let A = 9728;\n      e[(e.braceL = A)] = 'braceL';\n      let U = 10752;\n      e[(e.braceBarL = U)] = 'braceBarL';\n      let M = 11264;\n      e[(e.braceR = M)] = 'braceR';\n      let c = 12288;\n      e[(e.braceBarR = c)] = 'braceBarR';\n      let R = 13824;\n      e[(e.parenL = R)] = 'parenL';\n      let W = 14336;\n      e[(e.parenR = W)] = 'parenR';\n      let X = 15360;\n      e[(e.comma = X)] = 'comma';\n      let ie = 16384;\n      e[(e.semi = ie)] = 'semi';\n      let pe = 17408;\n      e[(e.colon = pe)] = 'colon';\n      let ae = 18432;\n      e[(e.doubleColon = ae)] = 'doubleColon';\n      let He = 19456;\n      e[(e.dot = He)] = 'dot';\n      let qe = 20480;\n      e[(e.question = qe)] = 'question';\n      let Bt = 21504;\n      e[(e.questionDot = Bt)] = 'questionDot';\n      let mt = 22528;\n      e[(e.arrow = mt)] = 'arrow';\n      let kt = 23552;\n      e[(e.template = kt)] = 'template';\n      let At = 24576;\n      e[(e.ellipsis = At)] = 'ellipsis';\n      let tt = 25600;\n      e[(e.backQuote = tt)] = 'backQuote';\n      let nt = 27136;\n      e[(e.dollarBraceL = nt)] = 'dollarBraceL';\n      let _t = 27648;\n      e[(e.at = _t)] = 'at';\n      let ct = 29184;\n      e[(e.hash = ct)] = 'hash';\n      let wt = 29728;\n      e[(e.eq = wt)] = 'eq';\n      let $t = 30752;\n      e[(e.assign = $t)] = 'assign';\n      let Pt = 32640;\n      e[(e.preIncDec = Pt)] = 'preIncDec';\n      let qt = 33664;\n      e[(e.postIncDec = qt)] = 'postIncDec';\n      let Tn = 34432;\n      e[(e.bang = Tn)] = 'bang';\n      let V = 35456;\n      e[(e.tilde = V)] = 'tilde';\n      let G = 35841;\n      e[(e.pipeline = G)] = 'pipeline';\n      let J = 36866;\n      e[(e.nullishCoalescing = J)] = 'nullishCoalescing';\n      let re = 37890;\n      e[(e.logicalOR = re)] = 'logicalOR';\n      let ve = 38915;\n      e[(e.logicalAND = ve)] = 'logicalAND';\n      let he = 39940;\n      e[(e.bitwiseOR = he)] = 'bitwiseOR';\n      let Ie = 40965;\n      e[(e.bitwiseXOR = Ie)] = 'bitwiseXOR';\n      let Ee = 41990;\n      e[(e.bitwiseAND = Ee)] = 'bitwiseAND';\n      let Le = 43015;\n      e[(e.equality = Le)] = 'equality';\n      let Xe = 44040;\n      e[(e.lessThan = Xe)] = 'lessThan';\n      let We = 45064;\n      e[(e.greaterThan = We)] = 'greaterThan';\n      let Ke = 46088;\n      e[(e.relationalOrEqual = Ke)] = 'relationalOrEqual';\n      let ut = 47113;\n      e[(e.bitShiftL = ut)] = 'bitShiftL';\n      let pt = 48137;\n      e[(e.bitShiftR = pt)] = 'bitShiftR';\n      let bt = 49802;\n      e[(e.plus = bt)] = 'plus';\n      let yt = 50826;\n      e[(e.minus = yt)] = 'minus';\n      let vt = 51723;\n      e[(e.modulo = vt)] = 'modulo';\n      let bn = 52235;\n      e[(e.star = bn)] = 'star';\n      let Dn = 53259;\n      e[(e.slash = Dn)] = 'slash';\n      let Ge = 54348;\n      e[(e.exponent = Ge)] = 'exponent';\n      let St = 55296;\n      e[(e.jsxName = St)] = 'jsxName';\n      let ot = 56320;\n      e[(e.jsxText = ot)] = 'jsxText';\n      let zt = 57344;\n      e[(e.jsxEmptyText = zt)] = 'jsxEmptyText';\n      let Xt = 58880;\n      e[(e.jsxTagStart = Xt)] = 'jsxTagStart';\n      let te = 59392;\n      e[(e.jsxTagEnd = te)] = 'jsxTagEnd';\n      let Cn = 60928;\n      e[(e.typeParameterStart = Cn)] = 'typeParameterStart';\n      let Zn = 61440;\n      e[(e.nonNullAssertion = Zn)] = 'nonNullAssertion';\n      let _i = 62480;\n      e[(e._break = _i)] = '_break';\n      let Mn = 63504;\n      e[(e._case = Mn)] = '_case';\n      let xs = 64528;\n      e[(e._catch = xs)] = '_catch';\n      let Ds = 65552;\n      e[(e._continue = Ds)] = '_continue';\n      let bi = 66576;\n      e[(e._debugger = bi)] = '_debugger';\n      let es = 67600;\n      e[(e._default = es)] = '_default';\n      let Nt = 68624;\n      e[(e._do = Nt)] = '_do';\n      let Rt = 69648;\n      e[(e._else = Rt)] = '_else';\n      let Ue = 70672;\n      e[(e._finally = Ue)] = '_finally';\n      let wn = 71696;\n      e[(e._for = wn)] = '_for';\n      let de = 73232;\n      e[(e._function = de)] = '_function';\n      let Ms = 73744;\n      e[(e._if = Ms)] = '_if';\n      let gs = 74768;\n      e[(e._return = gs)] = '_return';\n      let Ci = 75792;\n      e[(e._switch = Ci)] = '_switch';\n      let ts = 77456;\n      e[(e._throw = ts)] = '_throw';\n      let rn = 77840;\n      e[(e._try = rn)] = '_try';\n      let wi = 78864;\n      e[(e._var = wi)] = '_var';\n      let Fn = 79888;\n      e[(e._let = Fn)] = '_let';\n      let Bn = 80912;\n      e[(e._const = Bn)] = '_const';\n      let Fs = 81936;\n      e[(e._while = Fs)] = '_while';\n      let Si = 82960;\n      e[(e._with = Si)] = '_with';\n      let Bs = 84496;\n      e[(e._new = Bs)] = '_new';\n      let Vs = 85520;\n      e[(e._this = Vs)] = '_this';\n      let js = 86544;\n      e[(e._super = js)] = '_super';\n      let $s = 87568;\n      e[(e._class = $s)] = '_class';\n      let qs = 88080;\n      e[(e._extends = qs)] = '_extends';\n      let Ii = 89104;\n      e[(e._export = Ii)] = '_export';\n      let Ei = 90640;\n      e[(e._import = Ei)] = '_import';\n      let Ai = 91664;\n      e[(e._yield = Ai)] = '_yield';\n      let Pi = 92688;\n      e[(e._null = Pi)] = '_null';\n      let Ks = 93712;\n      e[(e._true = Ks)] = '_true';\n      let Us = 94736;\n      e[(e._false = Us)] = '_false';\n      let Hs = 95256;\n      e[(e._in = Hs)] = '_in';\n      let Ws = 96280;\n      e[(e._instanceof = Ws)] = '_instanceof';\n      let Gs = 97936;\n      e[(e._typeof = Gs)] = '_typeof';\n      let zs = 98960;\n      e[(e._void = zs)] = '_void';\n      let jo = 99984;\n      e[(e._delete = jo)] = '_delete';\n      let $o = 100880;\n      e[(e._async = $o)] = '_async';\n      let mr = 101904;\n      e[(e._get = mr)] = '_get';\n      let qo = 102928;\n      e[(e._set = qo)] = '_set';\n      let Ni = 103952;\n      e[(e._declare = Ni)] = '_declare';\n      let yr = 104976;\n      e[(e._readonly = yr)] = '_readonly';\n      let Ko = 106e3;\n      e[(e._abstract = Ko)] = '_abstract';\n      let le = 107024;\n      e[(e._static = le)] = '_static';\n      let Xs = 107536;\n      e[(e._public = Xs)] = '_public';\n      let on = 108560;\n      e[(e._private = on)] = '_private';\n      let Uo = 109584;\n      e[(e._protected = Uo)] = '_protected';\n      let Ho = 110608;\n      e[(e._override = Ho)] = '_override';\n      let Tr = 112144;\n      e[(e._as = Tr)] = '_as';\n      let Wo = 113168;\n      e[(e._enum = Wo)] = '_enum';\n      let Go = 114192;\n      e[(e._type = Go)] = '_type';\n      let kr = 115216;\n      e[(e._implements = kr)] = '_implements';\n    })(q || (jr.TokenType = q = {}));\n    function Bd(e) {\n      switch (e) {\n        case q.num:\n          return 'num';\n        case q.bigint:\n          return 'bigint';\n        case q.decimal:\n          return 'decimal';\n        case q.regexp:\n          return 'regexp';\n        case q.string:\n          return 'string';\n        case q.name:\n          return 'name';\n        case q.eof:\n          return 'eof';\n        case q.bracketL:\n          return '[';\n        case q.bracketR:\n          return ']';\n        case q.braceL:\n          return '{';\n        case q.braceBarL:\n          return '{|';\n        case q.braceR:\n          return '}';\n        case q.braceBarR:\n          return '|}';\n        case q.parenL:\n          return '(';\n        case q.parenR:\n          return ')';\n        case q.comma:\n          return ',';\n        case q.semi:\n          return ';';\n        case q.colon:\n          return ':';\n        case q.doubleColon:\n          return '::';\n        case q.dot:\n          return '.';\n        case q.question:\n          return '?';\n        case q.questionDot:\n          return '?.';\n        case q.arrow:\n          return '=>';\n        case q.template:\n          return 'template';\n        case q.ellipsis:\n          return '...';\n        case q.backQuote:\n          return '`';\n        case q.dollarBraceL:\n          return '${';\n        case q.at:\n          return '@';\n        case q.hash:\n          return '#';\n        case q.eq:\n          return '=';\n        case q.assign:\n          return '_=';\n        case q.preIncDec:\n          return '++/--';\n        case q.postIncDec:\n          return '++/--';\n        case q.bang:\n          return '!';\n        case q.tilde:\n          return '~';\n        case q.pipeline:\n          return '|>';\n        case q.nullishCoalescing:\n          return '??';\n        case q.logicalOR:\n          return '||';\n        case q.logicalAND:\n          return '&&';\n        case q.bitwiseOR:\n          return '|';\n        case q.bitwiseXOR:\n          return '^';\n        case q.bitwiseAND:\n          return '&';\n        case q.equality:\n          return '==/!=';\n        case q.lessThan:\n          return '<';\n        case q.greaterThan:\n          return '>';\n        case q.relationalOrEqual:\n          return '<=/>=';\n        case q.bitShiftL:\n          return '<<';\n        case q.bitShiftR:\n          return '>>/>>>';\n        case q.plus:\n          return '+';\n        case q.minus:\n          return '-';\n        case q.modulo:\n          return '%';\n        case q.star:\n          return '*';\n        case q.slash:\n          return '/';\n        case q.exponent:\n          return '**';\n        case q.jsxName:\n          return 'jsxName';\n        case q.jsxText:\n          return 'jsxText';\n        case q.jsxEmptyText:\n          return 'jsxEmptyText';\n        case q.jsxTagStart:\n          return 'jsxTagStart';\n        case q.jsxTagEnd:\n          return 'jsxTagEnd';\n        case q.typeParameterStart:\n          return 'typeParameterStart';\n        case q.nonNullAssertion:\n          return 'nonNullAssertion';\n        case q._break:\n          return 'break';\n        case q._case:\n          return 'case';\n        case q._catch:\n          return 'catch';\n        case q._continue:\n          return 'continue';\n        case q._debugger:\n          return 'debugger';\n        case q._default:\n          return 'default';\n        case q._do:\n          return 'do';\n        case q._else:\n          return 'else';\n        case q._finally:\n          return 'finally';\n        case q._for:\n          return 'for';\n        case q._function:\n          return 'function';\n        case q._if:\n          return 'if';\n        case q._return:\n          return 'return';\n        case q._switch:\n          return 'switch';\n        case q._throw:\n          return 'throw';\n        case q._try:\n          return 'try';\n        case q._var:\n          return 'var';\n        case q._let:\n          return 'let';\n        case q._const:\n          return 'const';\n        case q._while:\n          return 'while';\n        case q._with:\n          return 'with';\n        case q._new:\n          return 'new';\n        case q._this:\n          return 'this';\n        case q._super:\n          return 'super';\n        case q._class:\n          return 'class';\n        case q._extends:\n          return 'extends';\n        case q._export:\n          return 'export';\n        case q._import:\n          return 'import';\n        case q._yield:\n          return 'yield';\n        case q._null:\n          return 'null';\n        case q._true:\n          return 'true';\n        case q._false:\n          return 'false';\n        case q._in:\n          return 'in';\n        case q._instanceof:\n          return 'instanceof';\n        case q._typeof:\n          return 'typeof';\n        case q._void:\n          return 'void';\n        case q._delete:\n          return 'delete';\n        case q._async:\n          return 'async';\n        case q._get:\n          return 'get';\n        case q._set:\n          return 'set';\n        case q._declare:\n          return 'declare';\n        case q._readonly:\n          return 'readonly';\n        case q._abstract:\n          return 'abstract';\n        case q._static:\n          return 'static';\n        case q._public:\n          return 'public';\n        case q._private:\n          return 'private';\n        case q._protected:\n          return 'protected';\n        case q._override:\n          return 'override';\n        case q._as:\n          return 'as';\n        case q._enum:\n          return 'enum';\n        case q._type:\n          return 'type';\n        case q._implements:\n          return 'implements';\n        default:\n          return '';\n      }\n    }\n    jr.formatTokenType = Bd;\n  });\n  var qr = Z((Ui) => {\n    'use strict';\n    Object.defineProperty(Ui, '__esModule', {value: !0});\n    var Vd = It(),\n      jd = be(),\n      Ta = class {\n        constructor(t, s, i) {\n          (this.startTokenIndex = t),\n            (this.endTokenIndex = s),\n            (this.isFunctionScope = i);\n        }\n      };\n    Ui.Scope = Ta;\n    var $r = class {\n      constructor(t, s, i, r, a, u, d, y, g, L, p, h, T) {\n        (this.potentialArrowAt = t),\n          (this.noAnonFunctionType = s),\n          (this.inDisallowConditionalTypesContext = i),\n          (this.tokensLength = r),\n          (this.scopesLength = a),\n          (this.pos = u),\n          (this.type = d),\n          (this.contextualKeyword = y),\n          (this.start = g),\n          (this.end = L),\n          (this.isType = p),\n          (this.scopeDepth = h),\n          (this.error = T);\n      }\n    };\n    Ui.StateSnapshot = $r;\n    var ka = class e {\n      constructor() {\n        e.prototype.__init.call(this),\n          e.prototype.__init2.call(this),\n          e.prototype.__init3.call(this),\n          e.prototype.__init4.call(this),\n          e.prototype.__init5.call(this),\n          e.prototype.__init6.call(this),\n          e.prototype.__init7.call(this),\n          e.prototype.__init8.call(this),\n          e.prototype.__init9.call(this),\n          e.prototype.__init10.call(this),\n          e.prototype.__init11.call(this),\n          e.prototype.__init12.call(this),\n          e.prototype.__init13.call(this);\n      }\n      __init() {\n        this.potentialArrowAt = -1;\n      }\n      __init2() {\n        this.noAnonFunctionType = !1;\n      }\n      __init3() {\n        this.inDisallowConditionalTypesContext = !1;\n      }\n      __init4() {\n        this.tokens = [];\n      }\n      __init5() {\n        this.scopes = [];\n      }\n      __init6() {\n        this.pos = 0;\n      }\n      __init7() {\n        this.type = jd.TokenType.eof;\n      }\n      __init8() {\n        this.contextualKeyword = Vd.ContextualKeyword.NONE;\n      }\n      __init9() {\n        this.start = 0;\n      }\n      __init10() {\n        this.end = 0;\n      }\n      __init11() {\n        this.isType = !1;\n      }\n      __init12() {\n        this.scopeDepth = 0;\n      }\n      __init13() {\n        this.error = null;\n      }\n      snapshot() {\n        return new $r(\n          this.potentialArrowAt,\n          this.noAnonFunctionType,\n          this.inDisallowConditionalTypesContext,\n          this.tokens.length,\n          this.scopes.length,\n          this.pos,\n          this.type,\n          this.contextualKeyword,\n          this.start,\n          this.end,\n          this.isType,\n          this.scopeDepth,\n          this.error\n        );\n      }\n      restoreFromSnapshot(t) {\n        (this.potentialArrowAt = t.potentialArrowAt),\n          (this.noAnonFunctionType = t.noAnonFunctionType),\n          (this.inDisallowConditionalTypesContext =\n            t.inDisallowConditionalTypesContext),\n          (this.tokens.length = t.tokensLength),\n          (this.scopes.length = t.scopesLength),\n          (this.pos = t.pos),\n          (this.type = t.type),\n          (this.contextualKeyword = t.contextualKeyword),\n          (this.start = t.start),\n          (this.end = t.end),\n          (this.isType = t.isType),\n          (this.scopeDepth = t.scopeDepth),\n          (this.error = t.error);\n      }\n    };\n    Ui.default = ka;\n  });\n  var Qt = Z((Kr) => {\n    'use strict';\n    Object.defineProperty(Kr, '__esModule', {value: !0});\n    var as;\n    (function (e) {\n      e[(e.backSpace = 8)] = 'backSpace';\n      let s = 10;\n      e[(e.lineFeed = s)] = 'lineFeed';\n      let i = 9;\n      e[(e.tab = i)] = 'tab';\n      let r = 13;\n      e[(e.carriageReturn = r)] = 'carriageReturn';\n      let a = 14;\n      e[(e.shiftOut = a)] = 'shiftOut';\n      let u = 32;\n      e[(e.space = u)] = 'space';\n      let d = 33;\n      e[(e.exclamationMark = d)] = 'exclamationMark';\n      let y = 34;\n      e[(e.quotationMark = y)] = 'quotationMark';\n      let g = 35;\n      e[(e.numberSign = g)] = 'numberSign';\n      let L = 36;\n      e[(e.dollarSign = L)] = 'dollarSign';\n      let p = 37;\n      e[(e.percentSign = p)] = 'percentSign';\n      let h = 38;\n      e[(e.ampersand = h)] = 'ampersand';\n      let T = 39;\n      e[(e.apostrophe = T)] = 'apostrophe';\n      let x = 40;\n      e[(e.leftParenthesis = x)] = 'leftParenthesis';\n      let w = 41;\n      e[(e.rightParenthesis = w)] = 'rightParenthesis';\n      let S = 42;\n      e[(e.asterisk = S)] = 'asterisk';\n      let A = 43;\n      e[(e.plusSign = A)] = 'plusSign';\n      let U = 44;\n      e[(e.comma = U)] = 'comma';\n      let M = 45;\n      e[(e.dash = M)] = 'dash';\n      let c = 46;\n      e[(e.dot = c)] = 'dot';\n      let R = 47;\n      e[(e.slash = R)] = 'slash';\n      let W = 48;\n      e[(e.digit0 = W)] = 'digit0';\n      let X = 49;\n      e[(e.digit1 = X)] = 'digit1';\n      let ie = 50;\n      e[(e.digit2 = ie)] = 'digit2';\n      let pe = 51;\n      e[(e.digit3 = pe)] = 'digit3';\n      let ae = 52;\n      e[(e.digit4 = ae)] = 'digit4';\n      let He = 53;\n      e[(e.digit5 = He)] = 'digit5';\n      let qe = 54;\n      e[(e.digit6 = qe)] = 'digit6';\n      let Bt = 55;\n      e[(e.digit7 = Bt)] = 'digit7';\n      let mt = 56;\n      e[(e.digit8 = mt)] = 'digit8';\n      let kt = 57;\n      e[(e.digit9 = kt)] = 'digit9';\n      let At = 58;\n      e[(e.colon = At)] = 'colon';\n      let tt = 59;\n      e[(e.semicolon = tt)] = 'semicolon';\n      let nt = 60;\n      e[(e.lessThan = nt)] = 'lessThan';\n      let _t = 61;\n      e[(e.equalsTo = _t)] = 'equalsTo';\n      let ct = 62;\n      e[(e.greaterThan = ct)] = 'greaterThan';\n      let wt = 63;\n      e[(e.questionMark = wt)] = 'questionMark';\n      let $t = 64;\n      e[(e.atSign = $t)] = 'atSign';\n      let Pt = 65;\n      e[(e.uppercaseA = Pt)] = 'uppercaseA';\n      let qt = 66;\n      e[(e.uppercaseB = qt)] = 'uppercaseB';\n      let Tn = 67;\n      e[(e.uppercaseC = Tn)] = 'uppercaseC';\n      let V = 68;\n      e[(e.uppercaseD = V)] = 'uppercaseD';\n      let G = 69;\n      e[(e.uppercaseE = G)] = 'uppercaseE';\n      let J = 70;\n      e[(e.uppercaseF = J)] = 'uppercaseF';\n      let re = 71;\n      e[(e.uppercaseG = re)] = 'uppercaseG';\n      let ve = 72;\n      e[(e.uppercaseH = ve)] = 'uppercaseH';\n      let he = 73;\n      e[(e.uppercaseI = he)] = 'uppercaseI';\n      let Ie = 74;\n      e[(e.uppercaseJ = Ie)] = 'uppercaseJ';\n      let Ee = 75;\n      e[(e.uppercaseK = Ee)] = 'uppercaseK';\n      let Le = 76;\n      e[(e.uppercaseL = Le)] = 'uppercaseL';\n      let Xe = 77;\n      e[(e.uppercaseM = Xe)] = 'uppercaseM';\n      let We = 78;\n      e[(e.uppercaseN = We)] = 'uppercaseN';\n      let Ke = 79;\n      e[(e.uppercaseO = Ke)] = 'uppercaseO';\n      let ut = 80;\n      e[(e.uppercaseP = ut)] = 'uppercaseP';\n      let pt = 81;\n      e[(e.uppercaseQ = pt)] = 'uppercaseQ';\n      let bt = 82;\n      e[(e.uppercaseR = bt)] = 'uppercaseR';\n      let yt = 83;\n      e[(e.uppercaseS = yt)] = 'uppercaseS';\n      let vt = 84;\n      e[(e.uppercaseT = vt)] = 'uppercaseT';\n      let bn = 85;\n      e[(e.uppercaseU = bn)] = 'uppercaseU';\n      let Dn = 86;\n      e[(e.uppercaseV = Dn)] = 'uppercaseV';\n      let Ge = 87;\n      e[(e.uppercaseW = Ge)] = 'uppercaseW';\n      let St = 88;\n      e[(e.uppercaseX = St)] = 'uppercaseX';\n      let ot = 89;\n      e[(e.uppercaseY = ot)] = 'uppercaseY';\n      let zt = 90;\n      e[(e.uppercaseZ = zt)] = 'uppercaseZ';\n      let Xt = 91;\n      e[(e.leftSquareBracket = Xt)] = 'leftSquareBracket';\n      let te = 92;\n      e[(e.backslash = te)] = 'backslash';\n      let Cn = 93;\n      e[(e.rightSquareBracket = Cn)] = 'rightSquareBracket';\n      let Zn = 94;\n      e[(e.caret = Zn)] = 'caret';\n      let _i = 95;\n      e[(e.underscore = _i)] = 'underscore';\n      let Mn = 96;\n      e[(e.graveAccent = Mn)] = 'graveAccent';\n      let xs = 97;\n      e[(e.lowercaseA = xs)] = 'lowercaseA';\n      let Ds = 98;\n      e[(e.lowercaseB = Ds)] = 'lowercaseB';\n      let bi = 99;\n      e[(e.lowercaseC = bi)] = 'lowercaseC';\n      let es = 100;\n      e[(e.lowercaseD = es)] = 'lowercaseD';\n      let Nt = 101;\n      e[(e.lowercaseE = Nt)] = 'lowercaseE';\n      let Rt = 102;\n      e[(e.lowercaseF = Rt)] = 'lowercaseF';\n      let Ue = 103;\n      e[(e.lowercaseG = Ue)] = 'lowercaseG';\n      let wn = 104;\n      e[(e.lowercaseH = wn)] = 'lowercaseH';\n      let de = 105;\n      e[(e.lowercaseI = de)] = 'lowercaseI';\n      let Ms = 106;\n      e[(e.lowercaseJ = Ms)] = 'lowercaseJ';\n      let gs = 107;\n      e[(e.lowercaseK = gs)] = 'lowercaseK';\n      let Ci = 108;\n      e[(e.lowercaseL = Ci)] = 'lowercaseL';\n      let ts = 109;\n      e[(e.lowercaseM = ts)] = 'lowercaseM';\n      let rn = 110;\n      e[(e.lowercaseN = rn)] = 'lowercaseN';\n      let wi = 111;\n      e[(e.lowercaseO = wi)] = 'lowercaseO';\n      let Fn = 112;\n      e[(e.lowercaseP = Fn)] = 'lowercaseP';\n      let Bn = 113;\n      e[(e.lowercaseQ = Bn)] = 'lowercaseQ';\n      let Fs = 114;\n      e[(e.lowercaseR = Fs)] = 'lowercaseR';\n      let Si = 115;\n      e[(e.lowercaseS = Si)] = 'lowercaseS';\n      let Bs = 116;\n      e[(e.lowercaseT = Bs)] = 'lowercaseT';\n      let Vs = 117;\n      e[(e.lowercaseU = Vs)] = 'lowercaseU';\n      let js = 118;\n      e[(e.lowercaseV = js)] = 'lowercaseV';\n      let $s = 119;\n      e[(e.lowercaseW = $s)] = 'lowercaseW';\n      let qs = 120;\n      e[(e.lowercaseX = qs)] = 'lowercaseX';\n      let Ii = 121;\n      e[(e.lowercaseY = Ii)] = 'lowercaseY';\n      let Ei = 122;\n      e[(e.lowercaseZ = Ei)] = 'lowercaseZ';\n      let Ai = 123;\n      e[(e.leftCurlyBrace = Ai)] = 'leftCurlyBrace';\n      let Pi = 124;\n      e[(e.verticalBar = Pi)] = 'verticalBar';\n      let Ks = 125;\n      e[(e.rightCurlyBrace = Ks)] = 'rightCurlyBrace';\n      let Us = 126;\n      e[(e.tilde = Us)] = 'tilde';\n      let Hs = 160;\n      e[(e.nonBreakingSpace = Hs)] = 'nonBreakingSpace';\n      let Ws = 5760;\n      e[(e.oghamSpaceMark = Ws)] = 'oghamSpaceMark';\n      let Gs = 8232;\n      e[(e.lineSeparator = Gs)] = 'lineSeparator';\n      let zs = 8233;\n      e[(e.paragraphSeparator = zs)] = 'paragraphSeparator';\n    })(as || (Kr.charCodes = as = {}));\n    function $d(e) {\n      return (\n        (e >= as.digit0 && e <= as.digit9) ||\n        (e >= as.lowercaseA && e <= as.lowercaseF) ||\n        (e >= as.uppercaseA && e <= as.uppercaseF)\n      );\n    }\n    Kr.isDigit = $d;\n  });\n  var Zt = Z((ft) => {\n    'use strict';\n    Object.defineProperty(ft, '__esModule', {value: !0});\n    function qd(e) {\n      return e && e.__esModule ? e : {default: e};\n    }\n    var Kd = qr(),\n      Ud = qd(Kd),\n      Hd = Qt();\n    ft.isJSXEnabled;\n    ft.isTypeScriptEnabled;\n    ft.isFlowEnabled;\n    ft.state;\n    ft.input;\n    ft.nextContextId;\n    function Wd() {\n      return ft.nextContextId++;\n    }\n    ft.getNextContextId = Wd;\n    function Gd(e) {\n      if ('pos' in e) {\n        let t = n1(e.pos);\n        (e.message += ` (${t.line}:${t.column})`), (e.loc = t);\n      }\n      return e;\n    }\n    ft.augmentError = Gd;\n    var Ur = class {\n      constructor(t, s) {\n        (this.line = t), (this.column = s);\n      }\n    };\n    ft.Loc = Ur;\n    function n1(e) {\n      let t = 1,\n        s = 1;\n      for (let i = 0; i < e; i++)\n        ft.input.charCodeAt(i) === Hd.charCodes.lineFeed ? (t++, (s = 1)) : s++;\n      return new Ur(t, s);\n    }\n    ft.locationForIndex = n1;\n    function zd(e, t, s, i) {\n      (ft.input = e),\n        (ft.state = new Ud.default()),\n        (ft.nextContextId = 1),\n        (ft.isJSXEnabled = t),\n        (ft.isTypeScriptEnabled = s),\n        (ft.isFlowEnabled = i);\n    }\n    ft.initParser = zd;\n  });\n  var cs = Z((tn) => {\n    'use strict';\n    Object.defineProperty(tn, '__esModule', {value: !0});\n    var ls = xt(),\n      As = be(),\n      Hr = Qt(),\n      en = Zt();\n    function Xd(e) {\n      return en.state.contextualKeyword === e;\n    }\n    tn.isContextual = Xd;\n    function Yd(e) {\n      let t = ls.lookaheadTypeAndKeyword.call(void 0);\n      return t.type === As.TokenType.name && t.contextualKeyword === e;\n    }\n    tn.isLookaheadContextual = Yd;\n    function s1(e) {\n      return (\n        en.state.contextualKeyword === e &&\n        ls.eat.call(void 0, As.TokenType.name)\n      );\n    }\n    tn.eatContextual = s1;\n    function Jd(e) {\n      s1(e) || Wr();\n    }\n    tn.expectContextual = Jd;\n    function i1() {\n      return (\n        ls.match.call(void 0, As.TokenType.eof) ||\n        ls.match.call(void 0, As.TokenType.braceR) ||\n        r1()\n      );\n    }\n    tn.canInsertSemicolon = i1;\n    function r1() {\n      let e = en.state.tokens[en.state.tokens.length - 1],\n        t = e ? e.end : 0;\n      for (let s = t; s < en.state.start; s++) {\n        let i = en.input.charCodeAt(s);\n        if (\n          i === Hr.charCodes.lineFeed ||\n          i === Hr.charCodes.carriageReturn ||\n          i === 8232 ||\n          i === 8233\n        )\n          return !0;\n      }\n      return !1;\n    }\n    tn.hasPrecedingLineBreak = r1;\n    function Qd() {\n      let e = ls.nextTokenStart.call(void 0);\n      for (let t = en.state.end; t < e; t++) {\n        let s = en.input.charCodeAt(t);\n        if (\n          s === Hr.charCodes.lineFeed ||\n          s === Hr.charCodes.carriageReturn ||\n          s === 8232 ||\n          s === 8233\n        )\n          return !0;\n      }\n      return !1;\n    }\n    tn.hasFollowingLineBreak = Qd;\n    function o1() {\n      return ls.eat.call(void 0, As.TokenType.semi) || i1();\n    }\n    tn.isLineTerminator = o1;\n    function Zd() {\n      o1() || Wr('Unexpected token, expected \";\"');\n    }\n    tn.semicolon = Zd;\n    function em(e) {\n      ls.eat.call(void 0, e) ||\n        Wr(\n          `Unexpected token, expected \"${As.formatTokenType.call(void 0, e)}\"`\n        );\n    }\n    tn.expect = em;\n    function Wr(e = 'Unexpected token', t = en.state.start) {\n      if (en.state.error) return;\n      let s = new SyntaxError(e);\n      (s.pos = t),\n        (en.state.error = s),\n        (en.state.pos = en.input.length),\n        ls.finishToken.call(void 0, As.TokenType.eof);\n    }\n    tn.unexpected = Wr;\n  });\n  var xa = Z((Ps) => {\n    'use strict';\n    Object.defineProperty(Ps, '__esModule', {value: !0});\n    var va = Qt(),\n      tm = [\n        9,\n        11,\n        12,\n        va.charCodes.space,\n        va.charCodes.nonBreakingSpace,\n        va.charCodes.oghamSpaceMark,\n        8192,\n        8193,\n        8194,\n        8195,\n        8196,\n        8197,\n        8198,\n        8199,\n        8200,\n        8201,\n        8202,\n        8239,\n        8287,\n        12288,\n        65279,\n      ];\n    Ps.WHITESPACE_CHARS = tm;\n    var nm = /(?:\\s|\\/\\/.*|\\/\\*[^]*?\\*\\/)*/g;\n    Ps.skipWhiteSpace = nm;\n    var sm = new Uint8Array(65536);\n    Ps.IS_WHITESPACE = sm;\n    for (let e of Ps.WHITESPACE_CHARS) Ps.IS_WHITESPACE[e] = 1;\n  });\n  var li = Z((vn) => {\n    'use strict';\n    Object.defineProperty(vn, '__esModule', {value: !0});\n    var a1 = Qt(),\n      im = xa();\n    function rm(e) {\n      if (e < 48) return e === 36;\n      if (e < 58) return !0;\n      if (e < 65) return !1;\n      if (e < 91) return !0;\n      if (e < 97) return e === 95;\n      if (e < 123) return !0;\n      if (e < 128) return !1;\n      throw new Error('Should not be called with non-ASCII char code.');\n    }\n    var om = new Uint8Array(65536);\n    vn.IS_IDENTIFIER_CHAR = om;\n    for (let e = 0; e < 128; e++) vn.IS_IDENTIFIER_CHAR[e] = rm(e) ? 1 : 0;\n    for (let e = 128; e < 65536; e++) vn.IS_IDENTIFIER_CHAR[e] = 1;\n    for (let e of im.WHITESPACE_CHARS) vn.IS_IDENTIFIER_CHAR[e] = 0;\n    vn.IS_IDENTIFIER_CHAR[8232] = 0;\n    vn.IS_IDENTIFIER_CHAR[8233] = 0;\n    var am = vn.IS_IDENTIFIER_CHAR.slice();\n    vn.IS_IDENTIFIER_START = am;\n    for (let e = a1.charCodes.digit0; e <= a1.charCodes.digit9; e++)\n      vn.IS_IDENTIFIER_START[e] = 0;\n  });\n  var l1 = Z((ga) => {\n    'use strict';\n    Object.defineProperty(ga, '__esModule', {value: !0});\n    var ge = It(),\n      Ce = be(),\n      lm = new Int32Array([\n        -1,\n        27,\n        783,\n        918,\n        1755,\n        2376,\n        2862,\n        3483,\n        -1,\n        3699,\n        -1,\n        4617,\n        4752,\n        4833,\n        5130,\n        5508,\n        5940,\n        -1,\n        6480,\n        6939,\n        7749,\n        8181,\n        8451,\n        8613,\n        -1,\n        8829,\n        -1,\n        -1,\n        -1,\n        54,\n        243,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        432,\n        -1,\n        -1,\n        -1,\n        675,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        81,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        108,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        135,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        162,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        189,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        216,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        ge.ContextualKeyword._abstract << 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        270,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        297,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        324,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        351,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        378,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        405,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        ge.ContextualKeyword._accessor << 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        ge.ContextualKeyword._as << 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        459,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        594,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        486,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        513,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        540,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        ge.ContextualKeyword._assert << 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        567,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        ge.ContextualKeyword._asserts << 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        621,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        648,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        ge.ContextualKeyword._async << 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        702,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        729,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        756,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        ge.ContextualKeyword._await << 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        810,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        837,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        864,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        891,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        (Ce.TokenType._break << 1) + 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        945,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        1107,\n        -1,\n        -1,\n        -1,\n        1242,\n        -1,\n        -1,\n        1350,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        972,\n        1026,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        999,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        (Ce.TokenType._case << 1) + 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        1053,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        1080,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        (Ce.TokenType._catch << 1) + 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        1134,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        1161,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        1188,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        1215,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        ge.ContextualKeyword._checks << 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        1269,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        1296,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        1323,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        (Ce.TokenType._class << 1) + 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        1377,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        1404,\n        1620,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        1431,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        (Ce.TokenType._const << 1) + 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        1458,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        1485,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        1512,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        1539,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        1566,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        1593,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        ge.ContextualKeyword._constructor << 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        1647,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        1674,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        1701,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        1728,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        (Ce.TokenType._continue << 1) + 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        1782,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        2349,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        1809,\n        1971,\n        -1,\n        -1,\n        2106,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        2241,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        1836,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        1863,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        1890,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        1917,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        1944,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        (Ce.TokenType._debugger << 1) + 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        1998,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        2025,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        2052,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        2079,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        ge.ContextualKeyword._declare << 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        2133,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        2160,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        2187,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        2214,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        (Ce.TokenType._default << 1) + 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        2268,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        2295,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        2322,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        (Ce.TokenType._delete << 1) + 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        (Ce.TokenType._do << 1) + 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        2403,\n        -1,\n        2484,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        2565,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        2430,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        2457,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        (Ce.TokenType._else << 1) + 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        2511,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        2538,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        ge.ContextualKeyword._enum << 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        2592,\n        -1,\n        -1,\n        -1,\n        2727,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        2619,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        2646,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        2673,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        (Ce.TokenType._export << 1) + 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        2700,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        ge.ContextualKeyword._exports << 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        2754,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        2781,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        2808,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        2835,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        (Ce.TokenType._extends << 1) + 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        2889,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        2997,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        3159,\n        -1,\n        -1,\n        3213,\n        -1,\n        -1,\n        3294,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        2916,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        2943,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        2970,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        (Ce.TokenType._false << 1) + 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        3024,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        3051,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        3078,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        3105,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        3132,\n        -1,\n        (Ce.TokenType._finally << 1) + 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        3186,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        (Ce.TokenType._for << 1) + 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        3240,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        3267,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        ge.ContextualKeyword._from << 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        3321,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        3348,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        3375,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        3402,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        3429,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        3456,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        (Ce.TokenType._function << 1) + 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        3510,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        3564,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        3537,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        ge.ContextualKeyword._get << 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        3591,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        3618,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        3645,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        3672,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        ge.ContextualKeyword._global << 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        3726,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        3753,\n        4077,\n        -1,\n        -1,\n        -1,\n        -1,\n        4590,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        (Ce.TokenType._if << 1) + 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        3780,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        3807,\n        -1,\n        -1,\n        3996,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        3834,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        3861,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        3888,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        3915,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        3942,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        3969,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        ge.ContextualKeyword._implements << 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        4023,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        4050,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        (Ce.TokenType._import << 1) + 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        (Ce.TokenType._in << 1) + 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        4104,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        4185,\n        4401,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        4131,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        4158,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        ge.ContextualKeyword._infer << 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        4212,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        4239,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        4266,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        4293,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        4320,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        4347,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        4374,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        (Ce.TokenType._instanceof << 1) + 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        4428,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        4455,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        4482,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        4509,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        4536,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        4563,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        ge.ContextualKeyword._interface << 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        ge.ContextualKeyword._is << 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        4644,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        4671,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        4698,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        4725,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        ge.ContextualKeyword._keyof << 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        4779,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        4806,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        (Ce.TokenType._let << 1) + 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        4860,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        4995,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        4887,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        4914,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        4941,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        4968,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        ge.ContextualKeyword._mixins << 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        5022,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        5049,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        5076,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        5103,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        ge.ContextualKeyword._module << 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        5157,\n        -1,\n        -1,\n        -1,\n        5373,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        5427,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        5184,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        5211,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        5238,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        5265,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        5292,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        5319,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        5346,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        ge.ContextualKeyword._namespace << 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        5400,\n        -1,\n        -1,\n        -1,\n        (Ce.TokenType._new << 1) + 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        5454,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        5481,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        (Ce.TokenType._null << 1) + 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        5535,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        5562,\n        -1,\n        -1,\n        -1,\n        -1,\n        5697,\n        5751,\n        -1,\n        -1,\n        -1,\n        -1,\n        ge.ContextualKeyword._of << 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        5589,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        5616,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        5643,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        5670,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        ge.ContextualKeyword._opaque << 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        5724,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        ge.ContextualKeyword._out << 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        5778,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        5805,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        5832,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        5859,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        5886,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        5913,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        ge.ContextualKeyword._override << 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        5967,\n        -1,\n        -1,\n        6345,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        5994,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        6129,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        6021,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        6048,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        6075,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        6102,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        ge.ContextualKeyword._private << 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        6156,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        6183,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        6318,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        6210,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        6237,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        6264,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        6291,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        ge.ContextualKeyword._protected << 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        ge.ContextualKeyword._proto << 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        6372,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        6399,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        6426,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        6453,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        ge.ContextualKeyword._public << 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        6507,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        6534,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        6696,\n        -1,\n        -1,\n        6831,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        6561,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        6588,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        6615,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        6642,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        6669,\n        -1,\n        ge.ContextualKeyword._readonly << 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        6723,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        6750,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        6777,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        6804,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        ge.ContextualKeyword._require << 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        6858,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        6885,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        6912,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        (Ce.TokenType._return << 1) + 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        6966,\n        -1,\n        -1,\n        -1,\n        7182,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        7236,\n        7371,\n        -1,\n        7479,\n        -1,\n        7614,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        6993,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        7020,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        7047,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        7074,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        7101,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        7128,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        7155,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        ge.ContextualKeyword._satisfies << 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        7209,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        ge.ContextualKeyword._set << 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        7263,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        7290,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        7317,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        7344,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        ge.ContextualKeyword._static << 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        7398,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        7425,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        7452,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        (Ce.TokenType._super << 1) + 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        7506,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        7533,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        7560,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        7587,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        (Ce.TokenType._switch << 1) + 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        7641,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        7668,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        7695,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        7722,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        ge.ContextualKeyword._symbol << 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        7776,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        7938,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        8046,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        7803,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        7857,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        7830,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        (Ce.TokenType._this << 1) + 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        7884,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        7911,\n        -1,\n        -1,\n        -1,\n        (Ce.TokenType._throw << 1) + 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        7965,\n        -1,\n        -1,\n        -1,\n        8019,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        7992,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        (Ce.TokenType._true << 1) + 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        (Ce.TokenType._try << 1) + 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        8073,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        8100,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        ge.ContextualKeyword._type << 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        8127,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        8154,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        (Ce.TokenType._typeof << 1) + 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        8208,\n        -1,\n        -1,\n        -1,\n        -1,\n        8343,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        8235,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        8262,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        8289,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        8316,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        ge.ContextualKeyword._unique << 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        8370,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        8397,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        8424,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        ge.ContextualKeyword._using << 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        8478,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        8532,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        8505,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        (Ce.TokenType._var << 1) + 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        8559,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        8586,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        (Ce.TokenType._void << 1) + 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        8640,\n        8748,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        8667,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        8694,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        8721,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        (Ce.TokenType._while << 1) + 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        8775,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        8802,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        (Ce.TokenType._with << 1) + 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        8856,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        8883,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        8910,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        8937,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        (Ce.TokenType._yield << 1) + 1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n        -1,\n      ]);\n    ga.READ_WORD_TREE = lm;\n  });\n  var h1 = Z((ba) => {\n    'use strict';\n    Object.defineProperty(ba, '__esModule', {value: !0});\n    var xn = Zt(),\n      us = Qt(),\n      c1 = li(),\n      _a = xt(),\n      u1 = l1(),\n      p1 = be();\n    function cm() {\n      let e = 0,\n        t = 0,\n        s = xn.state.pos;\n      for (\n        ;\n        s < xn.input.length &&\n        ((t = xn.input.charCodeAt(s)),\n        !(t < us.charCodes.lowercaseA || t > us.charCodes.lowercaseZ));\n\n      ) {\n        let r = u1.READ_WORD_TREE[e + (t - us.charCodes.lowercaseA) + 1];\n        if (r === -1) break;\n        (e = r), s++;\n      }\n      let i = u1.READ_WORD_TREE[e];\n      if (i > -1 && !c1.IS_IDENTIFIER_CHAR[t]) {\n        (xn.state.pos = s),\n          i & 1\n            ? _a.finishToken.call(void 0, i >>> 1)\n            : _a.finishToken.call(void 0, p1.TokenType.name, i >>> 1);\n        return;\n      }\n      for (; s < xn.input.length; ) {\n        let r = xn.input.charCodeAt(s);\n        if (c1.IS_IDENTIFIER_CHAR[r]) s++;\n        else if (r === us.charCodes.backslash) {\n          if (\n            ((s += 2), xn.input.charCodeAt(s) === us.charCodes.leftCurlyBrace)\n          ) {\n            for (\n              ;\n              s < xn.input.length &&\n              xn.input.charCodeAt(s) !== us.charCodes.rightCurlyBrace;\n\n            )\n              s++;\n            s++;\n          }\n        } else if (\n          r === us.charCodes.atSign &&\n          xn.input.charCodeAt(s + 1) === us.charCodes.atSign\n        )\n          s += 2;\n        else break;\n      }\n      (xn.state.pos = s), _a.finishToken.call(void 0, p1.TokenType.name);\n    }\n    ba.default = cm;\n  });\n  var xt = Z((Be) => {\n    'use strict';\n    Object.defineProperty(Be, '__esModule', {value: !0});\n    function um(e) {\n      return e && e.__esModule ? e : {default: e};\n    }\n    var b = Zt(),\n      ci = cs(),\n      F = Qt(),\n      d1 = li(),\n      wa = xa(),\n      pm = It(),\n      hm = h1(),\n      fm = um(hm),\n      ne = be(),\n      it;\n    (function (e) {\n      e[(e.Access = 0)] = 'Access';\n      let s = 1;\n      e[(e.ExportAccess = s)] = 'ExportAccess';\n      let i = s + 1;\n      e[(e.TopLevelDeclaration = i)] = 'TopLevelDeclaration';\n      let r = i + 1;\n      e[(e.FunctionScopedDeclaration = r)] = 'FunctionScopedDeclaration';\n      let a = r + 1;\n      e[(e.BlockScopedDeclaration = a)] = 'BlockScopedDeclaration';\n      let u = a + 1;\n      e[(e.ObjectShorthandTopLevelDeclaration = u)] =\n        'ObjectShorthandTopLevelDeclaration';\n      let d = u + 1;\n      e[(e.ObjectShorthandFunctionScopedDeclaration = d)] =\n        'ObjectShorthandFunctionScopedDeclaration';\n      let y = d + 1;\n      e[(e.ObjectShorthandBlockScopedDeclaration = y)] =\n        'ObjectShorthandBlockScopedDeclaration';\n      let g = y + 1;\n      e[(e.ObjectShorthand = g)] = 'ObjectShorthand';\n      let L = g + 1;\n      e[(e.ImportDeclaration = L)] = 'ImportDeclaration';\n      let p = L + 1;\n      e[(e.ObjectKey = p)] = 'ObjectKey';\n      let h = p + 1;\n      e[(e.ImportAccess = h)] = 'ImportAccess';\n    })(it || (Be.IdentifierRole = it = {}));\n    var f1;\n    (function (e) {\n      e[(e.NoChildren = 0)] = 'NoChildren';\n      let s = 1;\n      e[(e.OneChild = s)] = 'OneChild';\n      let i = s + 1;\n      e[(e.StaticChildren = i)] = 'StaticChildren';\n      let r = i + 1;\n      e[(e.KeyAfterPropSpread = r)] = 'KeyAfterPropSpread';\n    })(f1 || (Be.JSXRole = f1 = {}));\n    function dm(e) {\n      let t = e.identifierRole;\n      return (\n        t === it.TopLevelDeclaration ||\n        t === it.FunctionScopedDeclaration ||\n        t === it.BlockScopedDeclaration ||\n        t === it.ObjectShorthandTopLevelDeclaration ||\n        t === it.ObjectShorthandFunctionScopedDeclaration ||\n        t === it.ObjectShorthandBlockScopedDeclaration\n      );\n    }\n    Be.isDeclaration = dm;\n    function mm(e) {\n      let t = e.identifierRole;\n      return (\n        t === it.FunctionScopedDeclaration ||\n        t === it.BlockScopedDeclaration ||\n        t === it.ObjectShorthandFunctionScopedDeclaration ||\n        t === it.ObjectShorthandBlockScopedDeclaration\n      );\n    }\n    Be.isNonTopLevelDeclaration = mm;\n    function ym(e) {\n      let t = e.identifierRole;\n      return (\n        t === it.TopLevelDeclaration ||\n        t === it.ObjectShorthandTopLevelDeclaration ||\n        t === it.ImportDeclaration\n      );\n    }\n    Be.isTopLevelDeclaration = ym;\n    function Tm(e) {\n      let t = e.identifierRole;\n      return (\n        t === it.TopLevelDeclaration ||\n        t === it.BlockScopedDeclaration ||\n        t === it.ObjectShorthandTopLevelDeclaration ||\n        t === it.ObjectShorthandBlockScopedDeclaration\n      );\n    }\n    Be.isBlockScopedDeclaration = Tm;\n    function km(e) {\n      let t = e.identifierRole;\n      return (\n        t === it.FunctionScopedDeclaration ||\n        t === it.ObjectShorthandFunctionScopedDeclaration\n      );\n    }\n    Be.isFunctionScopedDeclaration = km;\n    function vm(e) {\n      return (\n        e.identifierRole === it.ObjectShorthandTopLevelDeclaration ||\n        e.identifierRole === it.ObjectShorthandBlockScopedDeclaration ||\n        e.identifierRole === it.ObjectShorthandFunctionScopedDeclaration\n      );\n    }\n    Be.isObjectShorthandDeclaration = vm;\n    var Hi = class {\n      constructor() {\n        (this.type = b.state.type),\n          (this.contextualKeyword = b.state.contextualKeyword),\n          (this.start = b.state.start),\n          (this.end = b.state.end),\n          (this.scopeDepth = b.state.scopeDepth),\n          (this.isType = b.state.isType),\n          (this.identifierRole = null),\n          (this.jsxRole = null),\n          (this.shadowsGlobal = !1),\n          (this.isAsyncOperation = !1),\n          (this.contextId = null),\n          (this.rhsEndIndex = null),\n          (this.isExpression = !1),\n          (this.numNullishCoalesceStarts = 0),\n          (this.numNullishCoalesceEnds = 0),\n          (this.isOptionalChainStart = !1),\n          (this.isOptionalChainEnd = !1),\n          (this.subscriptStartIndex = null),\n          (this.nullishStartIndex = null);\n      }\n    };\n    Be.Token = Hi;\n    function zr() {\n      b.state.tokens.push(new Hi()), k1();\n    }\n    Be.next = zr;\n    function xm() {\n      b.state.tokens.push(new Hi()), (b.state.start = b.state.pos), Km();\n    }\n    Be.nextTemplateToken = xm;\n    function gm() {\n      b.state.type === ne.TokenType.assign && --b.state.pos, jm();\n    }\n    Be.retokenizeSlashAsRegex = gm;\n    function _m(e) {\n      for (let s = b.state.tokens.length - e; s < b.state.tokens.length; s++)\n        b.state.tokens[s].isType = !0;\n      let t = b.state.isType;\n      return (b.state.isType = !0), t;\n    }\n    Be.pushTypeContext = _m;\n    function bm(e) {\n      b.state.isType = e;\n    }\n    Be.popTypeContext = bm;\n    function m1(e) {\n      return Sa(e) ? (zr(), !0) : !1;\n    }\n    Be.eat = m1;\n    function Cm(e) {\n      let t = b.state.isType;\n      (b.state.isType = !0), m1(e), (b.state.isType = t);\n    }\n    Be.eatTypeToken = Cm;\n    function Sa(e) {\n      return b.state.type === e;\n    }\n    Be.match = Sa;\n    function wm() {\n      let e = b.state.snapshot();\n      zr();\n      let t = b.state.type;\n      return b.state.restoreFromSnapshot(e), t;\n    }\n    Be.lookaheadType = wm;\n    var Gr = class {\n      constructor(t, s) {\n        (this.type = t), (this.contextualKeyword = s);\n      }\n    };\n    Be.TypeAndKeyword = Gr;\n    function Sm() {\n      let e = b.state.snapshot();\n      zr();\n      let t = b.state.type,\n        s = b.state.contextualKeyword;\n      return b.state.restoreFromSnapshot(e), new Gr(t, s);\n    }\n    Be.lookaheadTypeAndKeyword = Sm;\n    function y1() {\n      return T1(b.state.pos);\n    }\n    Be.nextTokenStart = y1;\n    function T1(e) {\n      wa.skipWhiteSpace.lastIndex = e;\n      let t = wa.skipWhiteSpace.exec(b.input);\n      return e + t[0].length;\n    }\n    Be.nextTokenStartSince = T1;\n    function Im() {\n      return b.input.charCodeAt(y1());\n    }\n    Be.lookaheadCharCode = Im;\n    function k1() {\n      if (\n        (x1(), (b.state.start = b.state.pos), b.state.pos >= b.input.length)\n      ) {\n        let e = b.state.tokens;\n        e.length >= 2 &&\n          e[e.length - 1].start >= b.input.length &&\n          e[e.length - 2].start >= b.input.length &&\n          ci.unexpected.call(void 0, 'Unexpectedly reached the end of input.'),\n          Ve(ne.TokenType.eof);\n        return;\n      }\n      Em(b.input.charCodeAt(b.state.pos));\n    }\n    Be.nextToken = k1;\n    function Em(e) {\n      d1.IS_IDENTIFIER_START[e] ||\n      e === F.charCodes.backslash ||\n      (e === F.charCodes.atSign &&\n        b.input.charCodeAt(b.state.pos + 1) === F.charCodes.atSign)\n        ? fm.default.call(void 0)\n        : _1(e);\n    }\n    function Am() {\n      for (\n        ;\n        b.input.charCodeAt(b.state.pos) !== F.charCodes.asterisk ||\n        b.input.charCodeAt(b.state.pos + 1) !== F.charCodes.slash;\n\n      )\n        if ((b.state.pos++, b.state.pos > b.input.length)) {\n          ci.unexpected.call(void 0, 'Unterminated comment', b.state.pos - 2);\n          return;\n        }\n      b.state.pos += 2;\n    }\n    function v1(e) {\n      let t = b.input.charCodeAt((b.state.pos += e));\n      if (b.state.pos < b.input.length)\n        for (\n          ;\n          t !== F.charCodes.lineFeed &&\n          t !== F.charCodes.carriageReturn &&\n          t !== F.charCodes.lineSeparator &&\n          t !== F.charCodes.paragraphSeparator &&\n          ++b.state.pos < b.input.length;\n\n        )\n          t = b.input.charCodeAt(b.state.pos);\n    }\n    Be.skipLineComment = v1;\n    function x1() {\n      for (; b.state.pos < b.input.length; ) {\n        let e = b.input.charCodeAt(b.state.pos);\n        switch (e) {\n          case F.charCodes.carriageReturn:\n            b.input.charCodeAt(b.state.pos + 1) === F.charCodes.lineFeed &&\n              ++b.state.pos;\n          case F.charCodes.lineFeed:\n          case F.charCodes.lineSeparator:\n          case F.charCodes.paragraphSeparator:\n            ++b.state.pos;\n            break;\n          case F.charCodes.slash:\n            switch (b.input.charCodeAt(b.state.pos + 1)) {\n              case F.charCodes.asterisk:\n                (b.state.pos += 2), Am();\n                break;\n              case F.charCodes.slash:\n                v1(2);\n                break;\n              default:\n                return;\n            }\n            break;\n          default:\n            if (wa.IS_WHITESPACE[e]) ++b.state.pos;\n            else return;\n        }\n      }\n    }\n    Be.skipSpace = x1;\n    function Ve(e, t = pm.ContextualKeyword.NONE) {\n      (b.state.end = b.state.pos),\n        (b.state.type = e),\n        (b.state.contextualKeyword = t);\n    }\n    Be.finishToken = Ve;\n    function Pm() {\n      let e = b.input.charCodeAt(b.state.pos + 1);\n      if (e >= F.charCodes.digit0 && e <= F.charCodes.digit9) {\n        b1(!0);\n        return;\n      }\n      e === F.charCodes.dot &&\n      b.input.charCodeAt(b.state.pos + 2) === F.charCodes.dot\n        ? ((b.state.pos += 3), Ve(ne.TokenType.ellipsis))\n        : (++b.state.pos, Ve(ne.TokenType.dot));\n    }\n    function Nm() {\n      b.input.charCodeAt(b.state.pos + 1) === F.charCodes.equalsTo\n        ? Fe(ne.TokenType.assign, 2)\n        : Fe(ne.TokenType.slash, 1);\n    }\n    function Rm(e) {\n      let t =\n          e === F.charCodes.asterisk ? ne.TokenType.star : ne.TokenType.modulo,\n        s = 1,\n        i = b.input.charCodeAt(b.state.pos + 1);\n      e === F.charCodes.asterisk &&\n        i === F.charCodes.asterisk &&\n        (s++,\n        (i = b.input.charCodeAt(b.state.pos + 2)),\n        (t = ne.TokenType.exponent)),\n        i === F.charCodes.equalsTo &&\n          b.input.charCodeAt(b.state.pos + 2) !== F.charCodes.greaterThan &&\n          (s++, (t = ne.TokenType.assign)),\n        Fe(t, s);\n    }\n    function Lm(e) {\n      let t = b.input.charCodeAt(b.state.pos + 1);\n      if (t === e) {\n        b.input.charCodeAt(b.state.pos + 2) === F.charCodes.equalsTo\n          ? Fe(ne.TokenType.assign, 3)\n          : Fe(\n              e === F.charCodes.verticalBar\n                ? ne.TokenType.logicalOR\n                : ne.TokenType.logicalAND,\n              2\n            );\n        return;\n      }\n      if (e === F.charCodes.verticalBar) {\n        if (t === F.charCodes.greaterThan) {\n          Fe(ne.TokenType.pipeline, 2);\n          return;\n        } else if (t === F.charCodes.rightCurlyBrace && b.isFlowEnabled) {\n          Fe(ne.TokenType.braceBarR, 2);\n          return;\n        }\n      }\n      if (t === F.charCodes.equalsTo) {\n        Fe(ne.TokenType.assign, 2);\n        return;\n      }\n      Fe(\n        e === F.charCodes.verticalBar\n          ? ne.TokenType.bitwiseOR\n          : ne.TokenType.bitwiseAND,\n        1\n      );\n    }\n    function Om() {\n      b.input.charCodeAt(b.state.pos + 1) === F.charCodes.equalsTo\n        ? Fe(ne.TokenType.assign, 2)\n        : Fe(ne.TokenType.bitwiseXOR, 1);\n    }\n    function Dm(e) {\n      let t = b.input.charCodeAt(b.state.pos + 1);\n      if (t === e) {\n        Fe(ne.TokenType.preIncDec, 2);\n        return;\n      }\n      t === F.charCodes.equalsTo\n        ? Fe(ne.TokenType.assign, 2)\n        : e === F.charCodes.plusSign\n        ? Fe(ne.TokenType.plus, 1)\n        : Fe(ne.TokenType.minus, 1);\n    }\n    function Mm() {\n      let e = b.input.charCodeAt(b.state.pos + 1);\n      if (e === F.charCodes.lessThan) {\n        if (b.input.charCodeAt(b.state.pos + 2) === F.charCodes.equalsTo) {\n          Fe(ne.TokenType.assign, 3);\n          return;\n        }\n        b.state.isType\n          ? Fe(ne.TokenType.lessThan, 1)\n          : Fe(ne.TokenType.bitShiftL, 2);\n        return;\n      }\n      e === F.charCodes.equalsTo\n        ? Fe(ne.TokenType.relationalOrEqual, 2)\n        : Fe(ne.TokenType.lessThan, 1);\n    }\n    function g1() {\n      if (b.state.isType) {\n        Fe(ne.TokenType.greaterThan, 1);\n        return;\n      }\n      let e = b.input.charCodeAt(b.state.pos + 1);\n      if (e === F.charCodes.greaterThan) {\n        let t =\n          b.input.charCodeAt(b.state.pos + 2) === F.charCodes.greaterThan\n            ? 3\n            : 2;\n        if (b.input.charCodeAt(b.state.pos + t) === F.charCodes.equalsTo) {\n          Fe(ne.TokenType.assign, t + 1);\n          return;\n        }\n        Fe(ne.TokenType.bitShiftR, t);\n        return;\n      }\n      e === F.charCodes.equalsTo\n        ? Fe(ne.TokenType.relationalOrEqual, 2)\n        : Fe(ne.TokenType.greaterThan, 1);\n    }\n    function Fm() {\n      b.state.type === ne.TokenType.greaterThan && ((b.state.pos -= 1), g1());\n    }\n    Be.rescan_gt = Fm;\n    function Bm(e) {\n      let t = b.input.charCodeAt(b.state.pos + 1);\n      if (t === F.charCodes.equalsTo) {\n        Fe(\n          ne.TokenType.equality,\n          b.input.charCodeAt(b.state.pos + 2) === F.charCodes.equalsTo ? 3 : 2\n        );\n        return;\n      }\n      if (e === F.charCodes.equalsTo && t === F.charCodes.greaterThan) {\n        (b.state.pos += 2), Ve(ne.TokenType.arrow);\n        return;\n      }\n      Fe(e === F.charCodes.equalsTo ? ne.TokenType.eq : ne.TokenType.bang, 1);\n    }\n    function Vm() {\n      let e = b.input.charCodeAt(b.state.pos + 1),\n        t = b.input.charCodeAt(b.state.pos + 2);\n      e === F.charCodes.questionMark && !(b.isFlowEnabled && b.state.isType)\n        ? t === F.charCodes.equalsTo\n          ? Fe(ne.TokenType.assign, 3)\n          : Fe(ne.TokenType.nullishCoalescing, 2)\n        : e === F.charCodes.dot &&\n          !(t >= F.charCodes.digit0 && t <= F.charCodes.digit9)\n        ? ((b.state.pos += 2), Ve(ne.TokenType.questionDot))\n        : (++b.state.pos, Ve(ne.TokenType.question));\n    }\n    function _1(e) {\n      switch (e) {\n        case F.charCodes.numberSign:\n          ++b.state.pos, Ve(ne.TokenType.hash);\n          return;\n        case F.charCodes.dot:\n          Pm();\n          return;\n        case F.charCodes.leftParenthesis:\n          ++b.state.pos, Ve(ne.TokenType.parenL);\n          return;\n        case F.charCodes.rightParenthesis:\n          ++b.state.pos, Ve(ne.TokenType.parenR);\n          return;\n        case F.charCodes.semicolon:\n          ++b.state.pos, Ve(ne.TokenType.semi);\n          return;\n        case F.charCodes.comma:\n          ++b.state.pos, Ve(ne.TokenType.comma);\n          return;\n        case F.charCodes.leftSquareBracket:\n          ++b.state.pos, Ve(ne.TokenType.bracketL);\n          return;\n        case F.charCodes.rightSquareBracket:\n          ++b.state.pos, Ve(ne.TokenType.bracketR);\n          return;\n        case F.charCodes.leftCurlyBrace:\n          b.isFlowEnabled &&\n          b.input.charCodeAt(b.state.pos + 1) === F.charCodes.verticalBar\n            ? Fe(ne.TokenType.braceBarL, 2)\n            : (++b.state.pos, Ve(ne.TokenType.braceL));\n          return;\n        case F.charCodes.rightCurlyBrace:\n          ++b.state.pos, Ve(ne.TokenType.braceR);\n          return;\n        case F.charCodes.colon:\n          b.input.charCodeAt(b.state.pos + 1) === F.charCodes.colon\n            ? Fe(ne.TokenType.doubleColon, 2)\n            : (++b.state.pos, Ve(ne.TokenType.colon));\n          return;\n        case F.charCodes.questionMark:\n          Vm();\n          return;\n        case F.charCodes.atSign:\n          ++b.state.pos, Ve(ne.TokenType.at);\n          return;\n        case F.charCodes.graveAccent:\n          ++b.state.pos, Ve(ne.TokenType.backQuote);\n          return;\n        case F.charCodes.digit0: {\n          let t = b.input.charCodeAt(b.state.pos + 1);\n          if (\n            t === F.charCodes.lowercaseX ||\n            t === F.charCodes.uppercaseX ||\n            t === F.charCodes.lowercaseO ||\n            t === F.charCodes.uppercaseO ||\n            t === F.charCodes.lowercaseB ||\n            t === F.charCodes.uppercaseB\n          ) {\n            $m();\n            return;\n          }\n        }\n        case F.charCodes.digit1:\n        case F.charCodes.digit2:\n        case F.charCodes.digit3:\n        case F.charCodes.digit4:\n        case F.charCodes.digit5:\n        case F.charCodes.digit6:\n        case F.charCodes.digit7:\n        case F.charCodes.digit8:\n        case F.charCodes.digit9:\n          b1(!1);\n          return;\n        case F.charCodes.quotationMark:\n        case F.charCodes.apostrophe:\n          qm(e);\n          return;\n        case F.charCodes.slash:\n          Nm();\n          return;\n        case F.charCodes.percentSign:\n        case F.charCodes.asterisk:\n          Rm(e);\n          return;\n        case F.charCodes.verticalBar:\n        case F.charCodes.ampersand:\n          Lm(e);\n          return;\n        case F.charCodes.caret:\n          Om();\n          return;\n        case F.charCodes.plusSign:\n        case F.charCodes.dash:\n          Dm(e);\n          return;\n        case F.charCodes.lessThan:\n          Mm();\n          return;\n        case F.charCodes.greaterThan:\n          g1();\n          return;\n        case F.charCodes.equalsTo:\n        case F.charCodes.exclamationMark:\n          Bm(e);\n          return;\n        case F.charCodes.tilde:\n          Fe(ne.TokenType.tilde, 1);\n          return;\n        default:\n          break;\n      }\n      ci.unexpected.call(\n        void 0,\n        `Unexpected character '${String.fromCharCode(e)}'`,\n        b.state.pos\n      );\n    }\n    Be.getTokenFromCode = _1;\n    function Fe(e, t) {\n      (b.state.pos += t), Ve(e);\n    }\n    function jm() {\n      let e = b.state.pos,\n        t = !1,\n        s = !1;\n      for (;;) {\n        if (b.state.pos >= b.input.length) {\n          ci.unexpected.call(void 0, 'Unterminated regular expression', e);\n          return;\n        }\n        let i = b.input.charCodeAt(b.state.pos);\n        if (t) t = !1;\n        else {\n          if (i === F.charCodes.leftSquareBracket) s = !0;\n          else if (i === F.charCodes.rightSquareBracket && s) s = !1;\n          else if (i === F.charCodes.slash && !s) break;\n          t = i === F.charCodes.backslash;\n        }\n        ++b.state.pos;\n      }\n      ++b.state.pos, C1(), Ve(ne.TokenType.regexp);\n    }\n    function Ca() {\n      for (;;) {\n        let e = b.input.charCodeAt(b.state.pos);\n        if (\n          (e >= F.charCodes.digit0 && e <= F.charCodes.digit9) ||\n          e === F.charCodes.underscore\n        )\n          b.state.pos++;\n        else break;\n      }\n    }\n    function $m() {\n      for (b.state.pos += 2; ; ) {\n        let t = b.input.charCodeAt(b.state.pos);\n        if (\n          (t >= F.charCodes.digit0 && t <= F.charCodes.digit9) ||\n          (t >= F.charCodes.lowercaseA && t <= F.charCodes.lowercaseF) ||\n          (t >= F.charCodes.uppercaseA && t <= F.charCodes.uppercaseF) ||\n          t === F.charCodes.underscore\n        )\n          b.state.pos++;\n        else break;\n      }\n      b.input.charCodeAt(b.state.pos) === F.charCodes.lowercaseN\n        ? (++b.state.pos, Ve(ne.TokenType.bigint))\n        : Ve(ne.TokenType.num);\n    }\n    function b1(e) {\n      let t = !1,\n        s = !1;\n      e || Ca();\n      let i = b.input.charCodeAt(b.state.pos);\n      if (\n        (i === F.charCodes.dot &&\n          (++b.state.pos, Ca(), (i = b.input.charCodeAt(b.state.pos))),\n        (i === F.charCodes.uppercaseE || i === F.charCodes.lowercaseE) &&\n          ((i = b.input.charCodeAt(++b.state.pos)),\n          (i === F.charCodes.plusSign || i === F.charCodes.dash) &&\n            ++b.state.pos,\n          Ca(),\n          (i = b.input.charCodeAt(b.state.pos))),\n        i === F.charCodes.lowercaseN\n          ? (++b.state.pos, (t = !0))\n          : i === F.charCodes.lowercaseM && (++b.state.pos, (s = !0)),\n        t)\n      ) {\n        Ve(ne.TokenType.bigint);\n        return;\n      }\n      if (s) {\n        Ve(ne.TokenType.decimal);\n        return;\n      }\n      Ve(ne.TokenType.num);\n    }\n    function qm(e) {\n      for (b.state.pos++; ; ) {\n        if (b.state.pos >= b.input.length) {\n          ci.unexpected.call(void 0, 'Unterminated string constant');\n          return;\n        }\n        let t = b.input.charCodeAt(b.state.pos);\n        if (t === F.charCodes.backslash) b.state.pos++;\n        else if (t === e) break;\n        b.state.pos++;\n      }\n      b.state.pos++, Ve(ne.TokenType.string);\n    }\n    function Km() {\n      for (;;) {\n        if (b.state.pos >= b.input.length) {\n          ci.unexpected.call(void 0, 'Unterminated template');\n          return;\n        }\n        let e = b.input.charCodeAt(b.state.pos);\n        if (\n          e === F.charCodes.graveAccent ||\n          (e === F.charCodes.dollarSign &&\n            b.input.charCodeAt(b.state.pos + 1) === F.charCodes.leftCurlyBrace)\n        ) {\n          if (b.state.pos === b.state.start && Sa(ne.TokenType.template))\n            if (e === F.charCodes.dollarSign) {\n              (b.state.pos += 2), Ve(ne.TokenType.dollarBraceL);\n              return;\n            } else {\n              ++b.state.pos, Ve(ne.TokenType.backQuote);\n              return;\n            }\n          Ve(ne.TokenType.template);\n          return;\n        }\n        e === F.charCodes.backslash && b.state.pos++, b.state.pos++;\n      }\n    }\n    function C1() {\n      for (; b.state.pos < b.input.length; ) {\n        let e = b.input.charCodeAt(b.state.pos);\n        if (d1.IS_IDENTIFIER_CHAR[e]) b.state.pos++;\n        else if (e === F.charCodes.backslash) {\n          if (\n            ((b.state.pos += 2),\n            b.input.charCodeAt(b.state.pos) === F.charCodes.leftCurlyBrace)\n          ) {\n            for (\n              ;\n              b.state.pos < b.input.length &&\n              b.input.charCodeAt(b.state.pos) !== F.charCodes.rightCurlyBrace;\n\n            )\n              b.state.pos++;\n            b.state.pos++;\n          }\n        } else break;\n      }\n    }\n    Be.skipWord = C1;\n  });\n  var Wi = Z((Ia) => {\n    'use strict';\n    Object.defineProperty(Ia, '__esModule', {value: !0});\n    var w1 = be();\n    function Um(e, t = e.currentIndex()) {\n      let s = t + 1;\n      if (Xr(e, s)) {\n        let i = e.identifierNameAtIndex(t);\n        return {isType: !1, leftName: i, rightName: i, endIndex: s};\n      }\n      if ((s++, Xr(e, s)))\n        return {isType: !0, leftName: null, rightName: null, endIndex: s};\n      if ((s++, Xr(e, s)))\n        return {\n          isType: !1,\n          leftName: e.identifierNameAtIndex(t),\n          rightName: e.identifierNameAtIndex(t + 2),\n          endIndex: s,\n        };\n      if ((s++, Xr(e, s)))\n        return {isType: !0, leftName: null, rightName: null, endIndex: s};\n      throw new Error(`Unexpected import/export specifier at ${t}`);\n    }\n    Ia.default = Um;\n    function Xr(e, t) {\n      let s = e.tokens[t];\n      return s.type === w1.TokenType.braceR || s.type === w1.TokenType.comma;\n    }\n  });\n  var S1 = Z((Ea) => {\n    'use strict';\n    Object.defineProperty(Ea, '__esModule', {value: !0});\n    Ea.default = new Map([\n      ['quot', '\"'],\n      ['amp', '&'],\n      ['apos', \"'\"],\n      ['lt', '<'],\n      ['gt', '>'],\n      ['nbsp', '\\xA0'],\n      ['iexcl', '\\xA1'],\n      ['cent', '\\xA2'],\n      ['pound', '\\xA3'],\n      ['curren', '\\xA4'],\n      ['yen', '\\xA5'],\n      ['brvbar', '\\xA6'],\n      ['sect', '\\xA7'],\n      ['uml', '\\xA8'],\n      ['copy', '\\xA9'],\n      ['ordf', '\\xAA'],\n      ['laquo', '\\xAB'],\n      ['not', '\\xAC'],\n      ['shy', '\\xAD'],\n      ['reg', '\\xAE'],\n      ['macr', '\\xAF'],\n      ['deg', '\\xB0'],\n      ['plusmn', '\\xB1'],\n      ['sup2', '\\xB2'],\n      ['sup3', '\\xB3'],\n      ['acute', '\\xB4'],\n      ['micro', '\\xB5'],\n      ['para', '\\xB6'],\n      ['middot', '\\xB7'],\n      ['cedil', '\\xB8'],\n      ['sup1', '\\xB9'],\n      ['ordm', '\\xBA'],\n      ['raquo', '\\xBB'],\n      ['frac14', '\\xBC'],\n      ['frac12', '\\xBD'],\n      ['frac34', '\\xBE'],\n      ['iquest', '\\xBF'],\n      ['Agrave', '\\xC0'],\n      ['Aacute', '\\xC1'],\n      ['Acirc', '\\xC2'],\n      ['Atilde', '\\xC3'],\n      ['Auml', '\\xC4'],\n      ['Aring', '\\xC5'],\n      ['AElig', '\\xC6'],\n      ['Ccedil', '\\xC7'],\n      ['Egrave', '\\xC8'],\n      ['Eacute', '\\xC9'],\n      ['Ecirc', '\\xCA'],\n      ['Euml', '\\xCB'],\n      ['Igrave', '\\xCC'],\n      ['Iacute', '\\xCD'],\n      ['Icirc', '\\xCE'],\n      ['Iuml', '\\xCF'],\n      ['ETH', '\\xD0'],\n      ['Ntilde', '\\xD1'],\n      ['Ograve', '\\xD2'],\n      ['Oacute', '\\xD3'],\n      ['Ocirc', '\\xD4'],\n      ['Otilde', '\\xD5'],\n      ['Ouml', '\\xD6'],\n      ['times', '\\xD7'],\n      ['Oslash', '\\xD8'],\n      ['Ugrave', '\\xD9'],\n      ['Uacute', '\\xDA'],\n      ['Ucirc', '\\xDB'],\n      ['Uuml', '\\xDC'],\n      ['Yacute', '\\xDD'],\n      ['THORN', '\\xDE'],\n      ['szlig', '\\xDF'],\n      ['agrave', '\\xE0'],\n      ['aacute', '\\xE1'],\n      ['acirc', '\\xE2'],\n      ['atilde', '\\xE3'],\n      ['auml', '\\xE4'],\n      ['aring', '\\xE5'],\n      ['aelig', '\\xE6'],\n      ['ccedil', '\\xE7'],\n      ['egrave', '\\xE8'],\n      ['eacute', '\\xE9'],\n      ['ecirc', '\\xEA'],\n      ['euml', '\\xEB'],\n      ['igrave', '\\xEC'],\n      ['iacute', '\\xED'],\n      ['icirc', '\\xEE'],\n      ['iuml', '\\xEF'],\n      ['eth', '\\xF0'],\n      ['ntilde', '\\xF1'],\n      ['ograve', '\\xF2'],\n      ['oacute', '\\xF3'],\n      ['ocirc', '\\xF4'],\n      ['otilde', '\\xF5'],\n      ['ouml', '\\xF6'],\n      ['divide', '\\xF7'],\n      ['oslash', '\\xF8'],\n      ['ugrave', '\\xF9'],\n      ['uacute', '\\xFA'],\n      ['ucirc', '\\xFB'],\n      ['uuml', '\\xFC'],\n      ['yacute', '\\xFD'],\n      ['thorn', '\\xFE'],\n      ['yuml', '\\xFF'],\n      ['OElig', '\\u0152'],\n      ['oelig', '\\u0153'],\n      ['Scaron', '\\u0160'],\n      ['scaron', '\\u0161'],\n      ['Yuml', '\\u0178'],\n      ['fnof', '\\u0192'],\n      ['circ', '\\u02C6'],\n      ['tilde', '\\u02DC'],\n      ['Alpha', '\\u0391'],\n      ['Beta', '\\u0392'],\n      ['Gamma', '\\u0393'],\n      ['Delta', '\\u0394'],\n      ['Epsilon', '\\u0395'],\n      ['Zeta', '\\u0396'],\n      ['Eta', '\\u0397'],\n      ['Theta', '\\u0398'],\n      ['Iota', '\\u0399'],\n      ['Kappa', '\\u039A'],\n      ['Lambda', '\\u039B'],\n      ['Mu', '\\u039C'],\n      ['Nu', '\\u039D'],\n      ['Xi', '\\u039E'],\n      ['Omicron', '\\u039F'],\n      ['Pi', '\\u03A0'],\n      ['Rho', '\\u03A1'],\n      ['Sigma', '\\u03A3'],\n      ['Tau', '\\u03A4'],\n      ['Upsilon', '\\u03A5'],\n      ['Phi', '\\u03A6'],\n      ['Chi', '\\u03A7'],\n      ['Psi', '\\u03A8'],\n      ['Omega', '\\u03A9'],\n      ['alpha', '\\u03B1'],\n      ['beta', '\\u03B2'],\n      ['gamma', '\\u03B3'],\n      ['delta', '\\u03B4'],\n      ['epsilon', '\\u03B5'],\n      ['zeta', '\\u03B6'],\n      ['eta', '\\u03B7'],\n      ['theta', '\\u03B8'],\n      ['iota', '\\u03B9'],\n      ['kappa', '\\u03BA'],\n      ['lambda', '\\u03BB'],\n      ['mu', '\\u03BC'],\n      ['nu', '\\u03BD'],\n      ['xi', '\\u03BE'],\n      ['omicron', '\\u03BF'],\n      ['pi', '\\u03C0'],\n      ['rho', '\\u03C1'],\n      ['sigmaf', '\\u03C2'],\n      ['sigma', '\\u03C3'],\n      ['tau', '\\u03C4'],\n      ['upsilon', '\\u03C5'],\n      ['phi', '\\u03C6'],\n      ['chi', '\\u03C7'],\n      ['psi', '\\u03C8'],\n      ['omega', '\\u03C9'],\n      ['thetasym', '\\u03D1'],\n      ['upsih', '\\u03D2'],\n      ['piv', '\\u03D6'],\n      ['ensp', '\\u2002'],\n      ['emsp', '\\u2003'],\n      ['thinsp', '\\u2009'],\n      ['zwnj', '\\u200C'],\n      ['zwj', '\\u200D'],\n      ['lrm', '\\u200E'],\n      ['rlm', '\\u200F'],\n      ['ndash', '\\u2013'],\n      ['mdash', '\\u2014'],\n      ['lsquo', '\\u2018'],\n      ['rsquo', '\\u2019'],\n      ['sbquo', '\\u201A'],\n      ['ldquo', '\\u201C'],\n      ['rdquo', '\\u201D'],\n      ['bdquo', '\\u201E'],\n      ['dagger', '\\u2020'],\n      ['Dagger', '\\u2021'],\n      ['bull', '\\u2022'],\n      ['hellip', '\\u2026'],\n      ['permil', '\\u2030'],\n      ['prime', '\\u2032'],\n      ['Prime', '\\u2033'],\n      ['lsaquo', '\\u2039'],\n      ['rsaquo', '\\u203A'],\n      ['oline', '\\u203E'],\n      ['frasl', '\\u2044'],\n      ['euro', '\\u20AC'],\n      ['image', '\\u2111'],\n      ['weierp', '\\u2118'],\n      ['real', '\\u211C'],\n      ['trade', '\\u2122'],\n      ['alefsym', '\\u2135'],\n      ['larr', '\\u2190'],\n      ['uarr', '\\u2191'],\n      ['rarr', '\\u2192'],\n      ['darr', '\\u2193'],\n      ['harr', '\\u2194'],\n      ['crarr', '\\u21B5'],\n      ['lArr', '\\u21D0'],\n      ['uArr', '\\u21D1'],\n      ['rArr', '\\u21D2'],\n      ['dArr', '\\u21D3'],\n      ['hArr', '\\u21D4'],\n      ['forall', '\\u2200'],\n      ['part', '\\u2202'],\n      ['exist', '\\u2203'],\n      ['empty', '\\u2205'],\n      ['nabla', '\\u2207'],\n      ['isin', '\\u2208'],\n      ['notin', '\\u2209'],\n      ['ni', '\\u220B'],\n      ['prod', '\\u220F'],\n      ['sum', '\\u2211'],\n      ['minus', '\\u2212'],\n      ['lowast', '\\u2217'],\n      ['radic', '\\u221A'],\n      ['prop', '\\u221D'],\n      ['infin', '\\u221E'],\n      ['ang', '\\u2220'],\n      ['and', '\\u2227'],\n      ['or', '\\u2228'],\n      ['cap', '\\u2229'],\n      ['cup', '\\u222A'],\n      ['int', '\\u222B'],\n      ['there4', '\\u2234'],\n      ['sim', '\\u223C'],\n      ['cong', '\\u2245'],\n      ['asymp', '\\u2248'],\n      ['ne', '\\u2260'],\n      ['equiv', '\\u2261'],\n      ['le', '\\u2264'],\n      ['ge', '\\u2265'],\n      ['sub', '\\u2282'],\n      ['sup', '\\u2283'],\n      ['nsub', '\\u2284'],\n      ['sube', '\\u2286'],\n      ['supe', '\\u2287'],\n      ['oplus', '\\u2295'],\n      ['otimes', '\\u2297'],\n      ['perp', '\\u22A5'],\n      ['sdot', '\\u22C5'],\n      ['lceil', '\\u2308'],\n      ['rceil', '\\u2309'],\n      ['lfloor', '\\u230A'],\n      ['rfloor', '\\u230B'],\n      ['lang', '\\u2329'],\n      ['rang', '\\u232A'],\n      ['loz', '\\u25CA'],\n      ['spades', '\\u2660'],\n      ['clubs', '\\u2663'],\n      ['hearts', '\\u2665'],\n      ['diams', '\\u2666'],\n    ]);\n  });\n  var Pa = Z((Aa) => {\n    'use strict';\n    Object.defineProperty(Aa, '__esModule', {value: !0});\n    function Hm(e) {\n      let [t, s] = I1(e.jsxPragma || 'React.createElement'),\n        [i, r] = I1(e.jsxFragmentPragma || 'React.Fragment');\n      return {base: t, suffix: s, fragmentBase: i, fragmentSuffix: r};\n    }\n    Aa.default = Hm;\n    function I1(e) {\n      let t = e.indexOf('.');\n      return t === -1 && (t = e.length), [e.slice(0, t), e.slice(t)];\n    }\n  });\n  var hn = Z((Ra) => {\n    'use strict';\n    Object.defineProperty(Ra, '__esModule', {value: !0});\n    var Na = class {\n      getPrefixCode() {\n        return '';\n      }\n      getHoistedCode() {\n        return '';\n      }\n      getSuffixCode() {\n        return '';\n      }\n    };\n    Ra.default = Na;\n  });\n  var Da = Z((Jr) => {\n    'use strict';\n    Object.defineProperty(Jr, '__esModule', {value: !0});\n    function Oa(e) {\n      return e && e.__esModule ? e : {default: e};\n    }\n    var Wm = S1(),\n      Gm = Oa(Wm),\n      Yr = xt(),\n      Re = be(),\n      An = Qt(),\n      zm = Pa(),\n      Xm = Oa(zm),\n      Ym = hn(),\n      Jm = Oa(Ym),\n      La = class e extends Jm.default {\n        __init() {\n          this.lastLineNumber = 1;\n        }\n        __init2() {\n          this.lastIndex = 0;\n        }\n        __init3() {\n          this.filenameVarName = null;\n        }\n        __init4() {\n          this.esmAutomaticImportNameResolutions = {};\n        }\n        __init5() {\n          this.cjsAutomaticModuleNameResolutions = {};\n        }\n        constructor(t, s, i, r, a) {\n          super(),\n            (this.rootTransformer = t),\n            (this.tokens = s),\n            (this.importProcessor = i),\n            (this.nameManager = r),\n            (this.options = a),\n            e.prototype.__init.call(this),\n            e.prototype.__init2.call(this),\n            e.prototype.__init3.call(this),\n            e.prototype.__init4.call(this),\n            e.prototype.__init5.call(this),\n            (this.jsxPragmaInfo = Xm.default.call(void 0, a)),\n            (this.isAutomaticRuntime = a.jsxRuntime === 'automatic'),\n            (this.jsxImportSource = a.jsxImportSource || 'react');\n        }\n        process() {\n          return this.tokens.matches1(Re.TokenType.jsxTagStart)\n            ? (this.processJSXTag(), !0)\n            : !1;\n        }\n        getPrefixCode() {\n          let t = '';\n          if (\n            (this.filenameVarName &&\n              (t += `const ${this.filenameVarName} = ${JSON.stringify(\n                this.options.filePath || ''\n              )};`),\n            this.isAutomaticRuntime)\n          )\n            if (this.importProcessor)\n              for (let [s, i] of Object.entries(\n                this.cjsAutomaticModuleNameResolutions\n              ))\n                t += `var ${i} = require(\"${s}\");`;\n            else {\n              let {createElement: s, ...i} =\n                this.esmAutomaticImportNameResolutions;\n              s &&\n                (t += `import {createElement as ${s}} from \"${this.jsxImportSource}\";`);\n              let r = Object.entries(i)\n                .map(([a, u]) => `${a} as ${u}`)\n                .join(', ');\n              if (r) {\n                let a =\n                  this.jsxImportSource +\n                  (this.options.production\n                    ? '/jsx-runtime'\n                    : '/jsx-dev-runtime');\n                t += `import {${r}} from \"${a}\";`;\n              }\n            }\n          return t;\n        }\n        processJSXTag() {\n          let {jsxRole: t, start: s} = this.tokens.currentToken(),\n            i = this.options.production ? null : this.getElementLocationCode(s);\n          this.isAutomaticRuntime && t !== Yr.JSXRole.KeyAfterPropSpread\n            ? this.transformTagToJSXFunc(i, t)\n            : this.transformTagToCreateElement(i);\n        }\n        getElementLocationCode(t) {\n          return `lineNumber: ${this.getLineNumberForIndex(t)}`;\n        }\n        getLineNumberForIndex(t) {\n          let s = this.tokens.code;\n          for (; this.lastIndex < t && this.lastIndex < s.length; )\n            s[this.lastIndex] ===\n              `\n` && this.lastLineNumber++,\n              this.lastIndex++;\n          return this.lastLineNumber;\n        }\n        transformTagToJSXFunc(t, s) {\n          let i = s === Yr.JSXRole.StaticChildren;\n          this.tokens.replaceToken(this.getJSXFuncInvocationCode(i));\n          let r = null;\n          if (this.tokens.matches1(Re.TokenType.jsxTagEnd))\n            this.tokens.replaceToken(`${this.getFragmentCode()}, {`),\n              this.processAutomaticChildrenAndEndProps(s);\n          else {\n            if (\n              (this.processTagIntro(),\n              this.tokens.appendCode(', {'),\n              (r = this.processProps(!0)),\n              this.tokens.matches2(Re.TokenType.slash, Re.TokenType.jsxTagEnd))\n            )\n              this.tokens.appendCode('}');\n            else if (this.tokens.matches1(Re.TokenType.jsxTagEnd))\n              this.tokens.removeToken(),\n                this.processAutomaticChildrenAndEndProps(s);\n            else\n              throw new Error('Expected either /> or > at the end of the tag.');\n            r && this.tokens.appendCode(`, ${r}`);\n          }\n          for (\n            this.options.production ||\n              (r === null && this.tokens.appendCode(', void 0'),\n              this.tokens.appendCode(`, ${i}, ${this.getDevSource(t)}, this`)),\n              this.tokens.removeInitialToken();\n            !this.tokens.matches1(Re.TokenType.jsxTagEnd);\n\n          )\n            this.tokens.removeToken();\n          this.tokens.replaceToken(')');\n        }\n        transformTagToCreateElement(t) {\n          if (\n            (this.tokens.replaceToken(this.getCreateElementInvocationCode()),\n            this.tokens.matches1(Re.TokenType.jsxTagEnd))\n          )\n            this.tokens.replaceToken(`${this.getFragmentCode()}, null`),\n              this.processChildren(!0);\n          else if (\n            (this.processTagIntro(),\n            this.processPropsObjectWithDevInfo(t),\n            !this.tokens.matches2(Re.TokenType.slash, Re.TokenType.jsxTagEnd))\n          )\n            if (this.tokens.matches1(Re.TokenType.jsxTagEnd))\n              this.tokens.removeToken(), this.processChildren(!0);\n            else\n              throw new Error('Expected either /> or > at the end of the tag.');\n          for (\n            this.tokens.removeInitialToken();\n            !this.tokens.matches1(Re.TokenType.jsxTagEnd);\n\n          )\n            this.tokens.removeToken();\n          this.tokens.replaceToken(')');\n        }\n        getJSXFuncInvocationCode(t) {\n          return this.options.production\n            ? t\n              ? this.claimAutoImportedFuncInvocation('jsxs', '/jsx-runtime')\n              : this.claimAutoImportedFuncInvocation('jsx', '/jsx-runtime')\n            : this.claimAutoImportedFuncInvocation(\n                'jsxDEV',\n                '/jsx-dev-runtime'\n              );\n        }\n        getCreateElementInvocationCode() {\n          if (this.isAutomaticRuntime)\n            return this.claimAutoImportedFuncInvocation('createElement', '');\n          {\n            let {jsxPragmaInfo: t} = this;\n            return `${\n              (this.importProcessor &&\n                this.importProcessor.getIdentifierReplacement(t.base)) ||\n              t.base\n            }${t.suffix}(`;\n          }\n        }\n        getFragmentCode() {\n          if (this.isAutomaticRuntime)\n            return this.claimAutoImportedName(\n              'Fragment',\n              this.options.production ? '/jsx-runtime' : '/jsx-dev-runtime'\n            );\n          {\n            let {jsxPragmaInfo: t} = this;\n            return (\n              ((this.importProcessor &&\n                this.importProcessor.getIdentifierReplacement(\n                  t.fragmentBase\n                )) ||\n                t.fragmentBase) + t.fragmentSuffix\n            );\n          }\n        }\n        claimAutoImportedFuncInvocation(t, s) {\n          let i = this.claimAutoImportedName(t, s);\n          return this.importProcessor ? `${i}.call(void 0, ` : `${i}(`;\n        }\n        claimAutoImportedName(t, s) {\n          if (this.importProcessor) {\n            let i = this.jsxImportSource + s;\n            return (\n              this.cjsAutomaticModuleNameResolutions[i] ||\n                (this.cjsAutomaticModuleNameResolutions[i] =\n                  this.importProcessor.getFreeIdentifierForPath(i)),\n              `${this.cjsAutomaticModuleNameResolutions[i]}.${t}`\n            );\n          } else\n            return (\n              this.esmAutomaticImportNameResolutions[t] ||\n                (this.esmAutomaticImportNameResolutions[t] =\n                  this.nameManager.claimFreeName(`_${t}`)),\n              this.esmAutomaticImportNameResolutions[t]\n            );\n        }\n        processTagIntro() {\n          let t = this.tokens.currentIndex() + 1;\n          for (\n            ;\n            this.tokens.tokens[t].isType ||\n            (!this.tokens.matches2AtIndex(\n              t - 1,\n              Re.TokenType.jsxName,\n              Re.TokenType.jsxName\n            ) &&\n              !this.tokens.matches2AtIndex(\n                t - 1,\n                Re.TokenType.greaterThan,\n                Re.TokenType.jsxName\n              ) &&\n              !this.tokens.matches1AtIndex(t, Re.TokenType.braceL) &&\n              !this.tokens.matches1AtIndex(t, Re.TokenType.jsxTagEnd) &&\n              !this.tokens.matches2AtIndex(\n                t,\n                Re.TokenType.slash,\n                Re.TokenType.jsxTagEnd\n              ));\n\n          )\n            t++;\n          if (t === this.tokens.currentIndex() + 1) {\n            let s = this.tokens.identifierName();\n            A1(s) && this.tokens.replaceToken(`'${s}'`);\n          }\n          for (; this.tokens.currentIndex() < t; )\n            this.rootTransformer.processToken();\n        }\n        processPropsObjectWithDevInfo(t) {\n          let s = this.options.production\n            ? ''\n            : `__self: this, __source: ${this.getDevSource(t)}`;\n          if (\n            !this.tokens.matches1(Re.TokenType.jsxName) &&\n            !this.tokens.matches1(Re.TokenType.braceL)\n          ) {\n            s\n              ? this.tokens.appendCode(`, {${s}}`)\n              : this.tokens.appendCode(', null');\n            return;\n          }\n          this.tokens.appendCode(', {'),\n            this.processProps(!1),\n            s ? this.tokens.appendCode(` ${s}}`) : this.tokens.appendCode('}');\n        }\n        processProps(t) {\n          let s = null;\n          for (;;) {\n            if (this.tokens.matches2(Re.TokenType.jsxName, Re.TokenType.eq)) {\n              let i = this.tokens.identifierName();\n              if (t && i === 'key') {\n                s !== null && this.tokens.appendCode(s.replace(/[^\\n]/g, '')),\n                  this.tokens.removeToken(),\n                  this.tokens.removeToken();\n                let r = this.tokens.snapshot();\n                this.processPropValue(),\n                  (s = this.tokens.dangerouslyGetAndRemoveCodeSinceSnapshot(r));\n                continue;\n              } else\n                this.processPropName(i),\n                  this.tokens.replaceToken(': '),\n                  this.processPropValue();\n            } else if (this.tokens.matches1(Re.TokenType.jsxName)) {\n              let i = this.tokens.identifierName();\n              this.processPropName(i), this.tokens.appendCode(': true');\n            } else if (this.tokens.matches1(Re.TokenType.braceL))\n              this.tokens.replaceToken(''),\n                this.rootTransformer.processBalancedCode(),\n                this.tokens.replaceToken('');\n            else break;\n            this.tokens.appendCode(',');\n          }\n          return s;\n        }\n        processPropName(t) {\n          t.includes('-')\n            ? this.tokens.replaceToken(`'${t}'`)\n            : this.tokens.copyToken();\n        }\n        processPropValue() {\n          this.tokens.matches1(Re.TokenType.braceL)\n            ? (this.tokens.replaceToken(''),\n              this.rootTransformer.processBalancedCode(),\n              this.tokens.replaceToken(''))\n            : this.tokens.matches1(Re.TokenType.jsxTagStart)\n            ? this.processJSXTag()\n            : this.processStringPropValue();\n        }\n        processStringPropValue() {\n          let t = this.tokens.currentToken(),\n            s = this.tokens.code.slice(t.start + 1, t.end - 1),\n            i = E1(s),\n            r = Zm(s);\n          this.tokens.replaceToken(r + i);\n        }\n        processAutomaticChildrenAndEndProps(t) {\n          t === Yr.JSXRole.StaticChildren\n            ? (this.tokens.appendCode(' children: ['),\n              this.processChildren(!1),\n              this.tokens.appendCode(']}'))\n            : (t === Yr.JSXRole.OneChild &&\n                this.tokens.appendCode(' children: '),\n              this.processChildren(!1),\n              this.tokens.appendCode('}'));\n        }\n        processChildren(t) {\n          let s = t;\n          for (;;) {\n            if (\n              this.tokens.matches2(Re.TokenType.jsxTagStart, Re.TokenType.slash)\n            )\n              return;\n            let i = !1;\n            if (this.tokens.matches1(Re.TokenType.braceL))\n              this.tokens.matches2(Re.TokenType.braceL, Re.TokenType.braceR)\n                ? (this.tokens.replaceToken(''), this.tokens.replaceToken(''))\n                : (this.tokens.replaceToken(s ? ', ' : ''),\n                  this.rootTransformer.processBalancedCode(),\n                  this.tokens.replaceToken(''),\n                  (i = !0));\n            else if (this.tokens.matches1(Re.TokenType.jsxTagStart))\n              this.tokens.appendCode(s ? ', ' : ''),\n                this.processJSXTag(),\n                (i = !0);\n            else if (\n              this.tokens.matches1(Re.TokenType.jsxText) ||\n              this.tokens.matches1(Re.TokenType.jsxEmptyText)\n            )\n              i = this.processChildTextElement(s);\n            else\n              throw new Error('Unexpected token when processing JSX children.');\n            i && (s = !0);\n          }\n        }\n        processChildTextElement(t) {\n          let s = this.tokens.currentToken(),\n            i = this.tokens.code.slice(s.start, s.end),\n            r = E1(i),\n            a = Qm(i);\n          return a === '\"\"'\n            ? (this.tokens.replaceToken(r), !1)\n            : (this.tokens.replaceToken(`${t ? ', ' : ''}${a}${r}`), !0);\n        }\n        getDevSource(t) {\n          return `{fileName: ${this.getFilenameVarName()}, ${t}}`;\n        }\n        getFilenameVarName() {\n          return (\n            this.filenameVarName ||\n              (this.filenameVarName =\n                this.nameManager.claimFreeName('_jsxFileName')),\n            this.filenameVarName\n          );\n        }\n      };\n    Jr.default = La;\n    function A1(e) {\n      let t = e.charCodeAt(0);\n      return t >= An.charCodes.lowercaseA && t <= An.charCodes.lowercaseZ;\n    }\n    Jr.startsWithLowerCase = A1;\n    function Qm(e) {\n      let t = '',\n        s = '',\n        i = !1,\n        r = !1;\n      for (let a = 0; a < e.length; a++) {\n        let u = e[a];\n        if (u === ' ' || u === '\t' || u === '\\r') i || (s += u);\n        else if (\n          u ===\n          `\n`\n        )\n          (s = ''), (i = !0);\n        else {\n          if ((r && i && (t += ' '), (t += s), (s = ''), u === '&')) {\n            let {entity: d, newI: y} = P1(e, a + 1);\n            (a = y - 1), (t += d);\n          } else t += u;\n          (r = !0), (i = !1);\n        }\n      }\n      return i || (t += s), JSON.stringify(t);\n    }\n    function E1(e) {\n      let t = 0,\n        s = 0;\n      for (let i of e)\n        i ===\n        `\n`\n          ? (t++, (s = 0))\n          : i === ' ' && s++;\n      return (\n        `\n`.repeat(t) + ' '.repeat(s)\n      );\n    }\n    function Zm(e) {\n      let t = '';\n      for (let s = 0; s < e.length; s++) {\n        let i = e[s];\n        if (\n          i ===\n          `\n`\n        )\n          if (/\\s/.test(e[s + 1]))\n            for (t += ' '; s < e.length && /\\s/.test(e[s + 1]); ) s++;\n          else\n            t += `\n`;\n        else if (i === '&') {\n          let {entity: r, newI: a} = P1(e, s + 1);\n          (t += r), (s = a - 1);\n        } else t += i;\n      }\n      return JSON.stringify(t);\n    }\n    function P1(e, t) {\n      let s = '',\n        i = 0,\n        r,\n        a = t;\n      if (e[a] === '#') {\n        let u = 10;\n        a++;\n        let d;\n        if (e[a] === 'x')\n          for (u = 16, a++, d = a; a < e.length && ty(e.charCodeAt(a)); ) a++;\n        else for (d = a; a < e.length && ey(e.charCodeAt(a)); ) a++;\n        if (e[a] === ';') {\n          let y = e.slice(d, a);\n          y && (a++, (r = String.fromCodePoint(parseInt(y, u))));\n        }\n      } else\n        for (; a < e.length && i++ < 10; ) {\n          let u = e[a];\n          if ((a++, u === ';')) {\n            r = Gm.default.get(s);\n            break;\n          }\n          s += u;\n        }\n      return r ? {entity: r, newI: a} : {entity: '&', newI: t};\n    }\n    function ey(e) {\n      return e >= An.charCodes.digit0 && e <= An.charCodes.digit9;\n    }\n    function ty(e) {\n      return (\n        (e >= An.charCodes.digit0 && e <= An.charCodes.digit9) ||\n        (e >= An.charCodes.lowercaseA && e <= An.charCodes.lowercaseF) ||\n        (e >= An.charCodes.uppercaseA && e <= An.charCodes.uppercaseF)\n      );\n    }\n  });\n  var Fa = Z((Ma) => {\n    'use strict';\n    Object.defineProperty(Ma, '__esModule', {value: !0});\n    function ny(e) {\n      return e && e.__esModule ? e : {default: e};\n    }\n    var Qr = xt(),\n      ui = be(),\n      sy = Da(),\n      iy = Pa(),\n      ry = ny(iy);\n    function oy(e, t) {\n      let s = ry.default.call(void 0, t),\n        i = new Set();\n      for (let r = 0; r < e.tokens.length; r++) {\n        let a = e.tokens[r];\n        if (\n          (a.type === ui.TokenType.name &&\n            !a.isType &&\n            (a.identifierRole === Qr.IdentifierRole.Access ||\n              a.identifierRole === Qr.IdentifierRole.ObjectShorthand ||\n              a.identifierRole === Qr.IdentifierRole.ExportAccess) &&\n            !a.shadowsGlobal &&\n            i.add(e.identifierNameForToken(a)),\n          a.type === ui.TokenType.jsxTagStart && i.add(s.base),\n          a.type === ui.TokenType.jsxTagStart &&\n            r + 1 < e.tokens.length &&\n            e.tokens[r + 1].type === ui.TokenType.jsxTagEnd &&\n            (i.add(s.base), i.add(s.fragmentBase)),\n          a.type === ui.TokenType.jsxName &&\n            a.identifierRole === Qr.IdentifierRole.Access)\n        ) {\n          let u = e.identifierNameForToken(a);\n          (!sy.startsWithLowerCase.call(void 0, u) ||\n            e.tokens[r + 1].type === ui.TokenType.dot) &&\n            i.add(e.identifierNameForToken(a));\n        }\n      }\n      return i;\n    }\n    Ma.getNonTypeIdentifiers = oy;\n  });\n  var N1 = Z((Va) => {\n    'use strict';\n    Object.defineProperty(Va, '__esModule', {value: !0});\n    function ay(e) {\n      return e && e.__esModule ? e : {default: e};\n    }\n    var ly = xt(),\n      Zr = It(),\n      me = be(),\n      cy = Wi(),\n      uy = ay(cy),\n      py = Fa(),\n      Ba = class e {\n        __init() {\n          this.nonTypeIdentifiers = new Set();\n        }\n        __init2() {\n          this.importInfoByPath = new Map();\n        }\n        __init3() {\n          this.importsToReplace = new Map();\n        }\n        __init4() {\n          this.identifierReplacements = new Map();\n        }\n        __init5() {\n          this.exportBindingsByLocalName = new Map();\n        }\n        constructor(t, s, i, r, a, u) {\n          (this.nameManager = t),\n            (this.tokens = s),\n            (this.enableLegacyTypeScriptModuleInterop = i),\n            (this.options = r),\n            (this.isTypeScriptTransformEnabled = a),\n            (this.helperManager = u),\n            e.prototype.__init.call(this),\n            e.prototype.__init2.call(this),\n            e.prototype.__init3.call(this),\n            e.prototype.__init4.call(this),\n            e.prototype.__init5.call(this);\n        }\n        preprocessTokens() {\n          for (let t = 0; t < this.tokens.tokens.length; t++)\n            this.tokens.matches1AtIndex(t, me.TokenType._import) &&\n              !this.tokens.matches3AtIndex(\n                t,\n                me.TokenType._import,\n                me.TokenType.name,\n                me.TokenType.eq\n              ) &&\n              this.preprocessImportAtIndex(t),\n              this.tokens.matches1AtIndex(t, me.TokenType._export) &&\n                !this.tokens.matches2AtIndex(\n                  t,\n                  me.TokenType._export,\n                  me.TokenType.eq\n                ) &&\n                this.preprocessExportAtIndex(t);\n          this.generateImportReplacements();\n        }\n        pruneTypeOnlyImports() {\n          this.nonTypeIdentifiers = py.getNonTypeIdentifiers.call(\n            void 0,\n            this.tokens,\n            this.options\n          );\n          for (let [t, s] of this.importInfoByPath.entries()) {\n            if (\n              s.hasBareImport ||\n              s.hasStarExport ||\n              s.exportStarNames.length > 0 ||\n              s.namedExports.length > 0\n            )\n              continue;\n            [\n              ...s.defaultNames,\n              ...s.wildcardNames,\n              ...s.namedImports.map(({localName: r}) => r),\n            ].every((r) => this.isTypeName(r)) &&\n              this.importsToReplace.set(t, '');\n          }\n        }\n        isTypeName(t) {\n          return (\n            this.isTypeScriptTransformEnabled && !this.nonTypeIdentifiers.has(t)\n          );\n        }\n        generateImportReplacements() {\n          for (let [t, s] of this.importInfoByPath.entries()) {\n            let {\n              defaultNames: i,\n              wildcardNames: r,\n              namedImports: a,\n              namedExports: u,\n              exportStarNames: d,\n              hasStarExport: y,\n            } = s;\n            if (\n              i.length === 0 &&\n              r.length === 0 &&\n              a.length === 0 &&\n              u.length === 0 &&\n              d.length === 0 &&\n              !y\n            ) {\n              this.importsToReplace.set(t, `require('${t}');`);\n              continue;\n            }\n            let g = this.getFreeIdentifierForPath(t),\n              L;\n            this.enableLegacyTypeScriptModuleInterop\n              ? (L = g)\n              : (L = r.length > 0 ? r[0] : this.getFreeIdentifierForPath(t));\n            let p = `var ${g} = require('${t}');`;\n            if (r.length > 0)\n              for (let h of r) {\n                let T = this.enableLegacyTypeScriptModuleInterop\n                  ? g\n                  : `${this.helperManager.getHelperName(\n                      'interopRequireWildcard'\n                    )}(${g})`;\n                p += ` var ${h} = ${T};`;\n              }\n            else\n              d.length > 0 && L !== g\n                ? (p += ` var ${L} = ${this.helperManager.getHelperName(\n                    'interopRequireWildcard'\n                  )}(${g});`)\n                : i.length > 0 &&\n                  L !== g &&\n                  (p += ` var ${L} = ${this.helperManager.getHelperName(\n                    'interopRequireDefault'\n                  )}(${g});`);\n            for (let {importedName: h, localName: T} of u)\n              p += ` ${this.helperManager.getHelperName(\n                'createNamedExportFrom'\n              )}(${g}, '${T}', '${h}');`;\n            for (let h of d) p += ` exports.${h} = ${L};`;\n            y &&\n              (p += ` ${this.helperManager.getHelperName(\n                'createStarExport'\n              )}(${g});`),\n              this.importsToReplace.set(t, p);\n            for (let h of i) this.identifierReplacements.set(h, `${L}.default`);\n            for (let {importedName: h, localName: T} of a)\n              this.identifierReplacements.set(T, `${g}.${h}`);\n          }\n        }\n        getFreeIdentifierForPath(t) {\n          let s = t.split('/'),\n            r = s[s.length - 1].replace(/\\W/g, '');\n          return this.nameManager.claimFreeName(`_${r}`);\n        }\n        preprocessImportAtIndex(t) {\n          let s = [],\n            i = [],\n            r = [];\n          if (\n            (t++,\n            ((this.tokens.matchesContextualAtIndex(\n              t,\n              Zr.ContextualKeyword._type\n            ) ||\n              this.tokens.matches1AtIndex(t, me.TokenType._typeof)) &&\n              !this.tokens.matches1AtIndex(t + 1, me.TokenType.comma) &&\n              !this.tokens.matchesContextualAtIndex(\n                t + 1,\n                Zr.ContextualKeyword._from\n              )) ||\n              this.tokens.matches1AtIndex(t, me.TokenType.parenL))\n          )\n            return;\n          if (\n            (this.tokens.matches1AtIndex(t, me.TokenType.name) &&\n              (s.push(this.tokens.identifierNameAtIndex(t)),\n              t++,\n              this.tokens.matches1AtIndex(t, me.TokenType.comma) && t++),\n            this.tokens.matches1AtIndex(t, me.TokenType.star) &&\n              ((t += 2), i.push(this.tokens.identifierNameAtIndex(t)), t++),\n            this.tokens.matches1AtIndex(t, me.TokenType.braceL))\n          ) {\n            let d = this.getNamedImports(t + 1);\n            t = d.newIndex;\n            for (let y of d.namedImports)\n              y.importedName === 'default' ? s.push(y.localName) : r.push(y);\n          }\n          if (\n            (this.tokens.matchesContextualAtIndex(\n              t,\n              Zr.ContextualKeyword._from\n            ) && t++,\n            !this.tokens.matches1AtIndex(t, me.TokenType.string))\n          )\n            throw new Error(\n              'Expected string token at the end of import statement.'\n            );\n          let a = this.tokens.stringValueAtIndex(t),\n            u = this.getImportInfo(a);\n          u.defaultNames.push(...s),\n            u.wildcardNames.push(...i),\n            u.namedImports.push(...r),\n            s.length === 0 &&\n              i.length === 0 &&\n              r.length === 0 &&\n              (u.hasBareImport = !0);\n        }\n        preprocessExportAtIndex(t) {\n          if (\n            this.tokens.matches2AtIndex(\n              t,\n              me.TokenType._export,\n              me.TokenType._var\n            ) ||\n            this.tokens.matches2AtIndex(\n              t,\n              me.TokenType._export,\n              me.TokenType._let\n            ) ||\n            this.tokens.matches2AtIndex(\n              t,\n              me.TokenType._export,\n              me.TokenType._const\n            )\n          )\n            this.preprocessVarExportAtIndex(t);\n          else if (\n            this.tokens.matches2AtIndex(\n              t,\n              me.TokenType._export,\n              me.TokenType._function\n            ) ||\n            this.tokens.matches2AtIndex(\n              t,\n              me.TokenType._export,\n              me.TokenType._class\n            )\n          ) {\n            let s = this.tokens.identifierNameAtIndex(t + 2);\n            this.addExportBinding(s, s);\n          } else if (\n            this.tokens.matches3AtIndex(\n              t,\n              me.TokenType._export,\n              me.TokenType.name,\n              me.TokenType._function\n            )\n          ) {\n            let s = this.tokens.identifierNameAtIndex(t + 3);\n            this.addExportBinding(s, s);\n          } else\n            this.tokens.matches2AtIndex(\n              t,\n              me.TokenType._export,\n              me.TokenType.braceL\n            )\n              ? this.preprocessNamedExportAtIndex(t)\n              : this.tokens.matches2AtIndex(\n                  t,\n                  me.TokenType._export,\n                  me.TokenType.star\n                ) && this.preprocessExportStarAtIndex(t);\n        }\n        preprocessVarExportAtIndex(t) {\n          let s = 0;\n          for (let i = t + 2; ; i++)\n            if (\n              this.tokens.matches1AtIndex(i, me.TokenType.braceL) ||\n              this.tokens.matches1AtIndex(i, me.TokenType.dollarBraceL) ||\n              this.tokens.matches1AtIndex(i, me.TokenType.bracketL)\n            )\n              s++;\n            else if (\n              this.tokens.matches1AtIndex(i, me.TokenType.braceR) ||\n              this.tokens.matches1AtIndex(i, me.TokenType.bracketR)\n            )\n              s--;\n            else {\n              if (s === 0 && !this.tokens.matches1AtIndex(i, me.TokenType.name))\n                break;\n              if (this.tokens.matches1AtIndex(1, me.TokenType.eq)) {\n                let r = this.tokens.currentToken().rhsEndIndex;\n                if (r == null)\n                  throw new Error('Expected = token with an end index.');\n                i = r - 1;\n              } else {\n                let r = this.tokens.tokens[i];\n                if (ly.isDeclaration.call(void 0, r)) {\n                  let a = this.tokens.identifierNameAtIndex(i);\n                  this.identifierReplacements.set(a, `exports.${a}`);\n                }\n              }\n            }\n        }\n        preprocessNamedExportAtIndex(t) {\n          t += 2;\n          let {newIndex: s, namedImports: i} = this.getNamedImports(t);\n          if (\n            ((t = s),\n            this.tokens.matchesContextualAtIndex(t, Zr.ContextualKeyword._from))\n          )\n            t++;\n          else {\n            for (let {importedName: u, localName: d} of i)\n              this.addExportBinding(u, d);\n            return;\n          }\n          if (!this.tokens.matches1AtIndex(t, me.TokenType.string))\n            throw new Error(\n              'Expected string token at the end of import statement.'\n            );\n          let r = this.tokens.stringValueAtIndex(t);\n          this.getImportInfo(r).namedExports.push(...i);\n        }\n        preprocessExportStarAtIndex(t) {\n          let s = null;\n          if (\n            (this.tokens.matches3AtIndex(\n              t,\n              me.TokenType._export,\n              me.TokenType.star,\n              me.TokenType._as\n            )\n              ? ((t += 3), (s = this.tokens.identifierNameAtIndex(t)), (t += 2))\n              : (t += 3),\n            !this.tokens.matches1AtIndex(t, me.TokenType.string))\n          )\n            throw new Error(\n              'Expected string token at the end of star export statement.'\n            );\n          let i = this.tokens.stringValueAtIndex(t),\n            r = this.getImportInfo(i);\n          s !== null ? r.exportStarNames.push(s) : (r.hasStarExport = !0);\n        }\n        getNamedImports(t) {\n          let s = [];\n          for (;;) {\n            if (this.tokens.matches1AtIndex(t, me.TokenType.braceR)) {\n              t++;\n              break;\n            }\n            let i = uy.default.call(void 0, this.tokens, t);\n            if (\n              ((t = i.endIndex),\n              i.isType ||\n                s.push({importedName: i.leftName, localName: i.rightName}),\n              this.tokens.matches2AtIndex(\n                t,\n                me.TokenType.comma,\n                me.TokenType.braceR\n              ))\n            ) {\n              t += 2;\n              break;\n            } else if (this.tokens.matches1AtIndex(t, me.TokenType.braceR)) {\n              t++;\n              break;\n            } else if (this.tokens.matches1AtIndex(t, me.TokenType.comma)) t++;\n            else\n              throw new Error(\n                `Unexpected token: ${JSON.stringify(this.tokens.tokens[t])}`\n              );\n          }\n          return {newIndex: t, namedImports: s};\n        }\n        getImportInfo(t) {\n          let s = this.importInfoByPath.get(t);\n          if (s) return s;\n          let i = {\n            defaultNames: [],\n            wildcardNames: [],\n            namedImports: [],\n            namedExports: [],\n            hasBareImport: !1,\n            exportStarNames: [],\n            hasStarExport: !1,\n          };\n          return this.importInfoByPath.set(t, i), i;\n        }\n        addExportBinding(t, s) {\n          this.exportBindingsByLocalName.has(t) ||\n            this.exportBindingsByLocalName.set(t, []),\n            this.exportBindingsByLocalName.get(t).push(s);\n        }\n        claimImportCode(t) {\n          let s = this.importsToReplace.get(t);\n          return this.importsToReplace.set(t, ''), s || '';\n        }\n        getIdentifierReplacement(t) {\n          return this.identifierReplacements.get(t) || null;\n        }\n        resolveExportBinding(t) {\n          let s = this.exportBindingsByLocalName.get(t);\n          return !s || s.length === 0\n            ? null\n            : s.map((i) => `exports.${i}`).join(' = ');\n        }\n        getGlobalNames() {\n          return new Set([\n            ...this.identifierReplacements.keys(),\n            ...this.exportBindingsByLocalName.keys(),\n          ]);\n        }\n      };\n    Va.default = Ba;\n  });\n  var L1 = Z((eo, R1) => {\n    (function (e, t) {\n      typeof eo == 'object' && typeof R1 < 'u'\n        ? t(eo)\n        : typeof define == 'function' && define.amd\n        ? define(['exports'], t)\n        : ((e = typeof globalThis < 'u' ? globalThis : e || self),\n          t((e.setArray = {})));\n    })(eo, function (e) {\n      'use strict';\n      (e.get = void 0), (e.put = void 0), (e.pop = void 0);\n      class t {\n        constructor() {\n          (this._indexes = {__proto__: null}), (this.array = []);\n        }\n      }\n      (e.get = (s, i) => s._indexes[i]),\n        (e.put = (s, i) => {\n          let r = e.get(s, i);\n          if (r !== void 0) return r;\n          let {array: a, _indexes: u} = s;\n          return (u[i] = a.push(i) - 1);\n        }),\n        (e.pop = (s) => {\n          let {array: i, _indexes: r} = s;\n          if (i.length === 0) return;\n          let a = i.pop();\n          r[a] = void 0;\n        }),\n        (e.SetArray = t),\n        Object.defineProperty(e, '__esModule', {value: !0});\n    });\n  });\n  var ja = Z((to, O1) => {\n    (function (e, t) {\n      typeof to == 'object' && typeof O1 < 'u'\n        ? t(to)\n        : typeof define == 'function' && define.amd\n        ? define(['exports'], t)\n        : ((e = typeof globalThis < 'u' ? globalThis : e || self),\n          t((e.sourcemapCodec = {})));\n    })(to, function (e) {\n      'use strict';\n      let i =\n          'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/',\n        r = new Uint8Array(64),\n        a = new Uint8Array(128);\n      for (let w = 0; w < i.length; w++) {\n        let S = i.charCodeAt(w);\n        (r[w] = S), (a[S] = w);\n      }\n      let u =\n        typeof TextDecoder < 'u'\n          ? new TextDecoder()\n          : typeof Buffer < 'u'\n          ? {\n              decode(w) {\n                return Buffer.from(\n                  w.buffer,\n                  w.byteOffset,\n                  w.byteLength\n                ).toString();\n              },\n            }\n          : {\n              decode(w) {\n                let S = '';\n                for (let A = 0; A < w.length; A++)\n                  S += String.fromCharCode(w[A]);\n                return S;\n              },\n            };\n      function d(w) {\n        let S = new Int32Array(5),\n          A = [],\n          U = 0;\n        do {\n          let M = y(w, U),\n            c = [],\n            R = !0,\n            W = 0;\n          S[0] = 0;\n          for (let X = U; X < M; X++) {\n            let ie;\n            X = g(w, X, S, 0);\n            let pe = S[0];\n            pe < W && (R = !1),\n              (W = pe),\n              L(w, X, M)\n                ? ((X = g(w, X, S, 1)),\n                  (X = g(w, X, S, 2)),\n                  (X = g(w, X, S, 3)),\n                  L(w, X, M)\n                    ? ((X = g(w, X, S, 4)), (ie = [pe, S[1], S[2], S[3], S[4]]))\n                    : (ie = [pe, S[1], S[2], S[3]]))\n                : (ie = [pe]),\n              c.push(ie);\n          }\n          R || p(c), A.push(c), (U = M + 1);\n        } while (U <= w.length);\n        return A;\n      }\n      function y(w, S) {\n        let A = w.indexOf(';', S);\n        return A === -1 ? w.length : A;\n      }\n      function g(w, S, A, U) {\n        let M = 0,\n          c = 0,\n          R = 0;\n        do {\n          let X = w.charCodeAt(S++);\n          (R = a[X]), (M |= (R & 31) << c), (c += 5);\n        } while (R & 32);\n        let W = M & 1;\n        return (M >>>= 1), W && (M = -2147483648 | -M), (A[U] += M), S;\n      }\n      function L(w, S, A) {\n        return S >= A ? !1 : w.charCodeAt(S) !== 44;\n      }\n      function p(w) {\n        w.sort(h);\n      }\n      function h(w, S) {\n        return w[0] - S[0];\n      }\n      function T(w) {\n        let S = new Int32Array(5),\n          A = 1024 * 16,\n          U = A - 36,\n          M = new Uint8Array(A),\n          c = M.subarray(0, U),\n          R = 0,\n          W = '';\n        for (let X = 0; X < w.length; X++) {\n          let ie = w[X];\n          if (\n            (X > 0 && (R === A && ((W += u.decode(M)), (R = 0)), (M[R++] = 59)),\n            ie.length !== 0)\n          ) {\n            S[0] = 0;\n            for (let pe = 0; pe < ie.length; pe++) {\n              let ae = ie[pe];\n              R > U && ((W += u.decode(c)), M.copyWithin(0, U, R), (R -= U)),\n                pe > 0 && (M[R++] = 44),\n                (R = x(M, R, S, ae, 0)),\n                ae.length !== 1 &&\n                  ((R = x(M, R, S, ae, 1)),\n                  (R = x(M, R, S, ae, 2)),\n                  (R = x(M, R, S, ae, 3)),\n                  ae.length !== 4 && (R = x(M, R, S, ae, 4)));\n            }\n          }\n        }\n        return W + u.decode(M.subarray(0, R));\n      }\n      function x(w, S, A, U, M) {\n        let c = U[M],\n          R = c - A[M];\n        (A[M] = c), (R = R < 0 ? (-R << 1) | 1 : R << 1);\n        do {\n          let W = R & 31;\n          (R >>>= 5), R > 0 && (W |= 32), (w[S++] = r[W]);\n        } while (R > 0);\n        return S;\n      }\n      (e.decode = d),\n        (e.encode = T),\n        Object.defineProperty(e, '__esModule', {value: !0});\n    });\n  });\n  var D1 = Z(($a, qa) => {\n    (function (e, t) {\n      typeof $a == 'object' && typeof qa < 'u'\n        ? (qa.exports = t())\n        : typeof define == 'function' && define.amd\n        ? define(t)\n        : ((e = typeof globalThis < 'u' ? globalThis : e || self),\n          (e.resolveURI = t()));\n    })($a, function () {\n      'use strict';\n      let e = /^[\\w+.-]+:\\/\\//,\n        t =\n          /^([\\w+.-]+:)\\/\\/([^@/#?]*@)?([^:/#?]*)(:\\d+)?(\\/[^#?]*)?(\\?[^#]*)?(#.*)?/,\n        s = /^file:(?:\\/\\/((?![a-z]:)[^/#?]*)?)?(\\/?[^#?]*)(\\?[^#]*)?(#.*)?/i;\n      var i;\n      (function (A) {\n        (A[(A.Empty = 1)] = 'Empty'),\n          (A[(A.Hash = 2)] = 'Hash'),\n          (A[(A.Query = 3)] = 'Query'),\n          (A[(A.RelativePath = 4)] = 'RelativePath'),\n          (A[(A.AbsolutePath = 5)] = 'AbsolutePath'),\n          (A[(A.SchemeRelative = 6)] = 'SchemeRelative'),\n          (A[(A.Absolute = 7)] = 'Absolute');\n      })(i || (i = {}));\n      function r(A) {\n        return e.test(A);\n      }\n      function a(A) {\n        return A.startsWith('//');\n      }\n      function u(A) {\n        return A.startsWith('/');\n      }\n      function d(A) {\n        return A.startsWith('file:');\n      }\n      function y(A) {\n        return /^[.?#]/.test(A);\n      }\n      function g(A) {\n        let U = t.exec(A);\n        return p(\n          U[1],\n          U[2] || '',\n          U[3],\n          U[4] || '',\n          U[5] || '/',\n          U[6] || '',\n          U[7] || ''\n        );\n      }\n      function L(A) {\n        let U = s.exec(A),\n          M = U[2];\n        return p(\n          'file:',\n          '',\n          U[1] || '',\n          '',\n          u(M) ? M : '/' + M,\n          U[3] || '',\n          U[4] || ''\n        );\n      }\n      function p(A, U, M, c, R, W, X) {\n        return {\n          scheme: A,\n          user: U,\n          host: M,\n          port: c,\n          path: R,\n          query: W,\n          hash: X,\n          type: i.Absolute,\n        };\n      }\n      function h(A) {\n        if (a(A)) {\n          let M = g('http:' + A);\n          return (M.scheme = ''), (M.type = i.SchemeRelative), M;\n        }\n        if (u(A)) {\n          let M = g('http://foo.com' + A);\n          return (M.scheme = ''), (M.host = ''), (M.type = i.AbsolutePath), M;\n        }\n        if (d(A)) return L(A);\n        if (r(A)) return g(A);\n        let U = g('http://foo.com/' + A);\n        return (\n          (U.scheme = ''),\n          (U.host = ''),\n          (U.type = A\n            ? A.startsWith('?')\n              ? i.Query\n              : A.startsWith('#')\n              ? i.Hash\n              : i.RelativePath\n            : i.Empty),\n          U\n        );\n      }\n      function T(A) {\n        if (A.endsWith('/..')) return A;\n        let U = A.lastIndexOf('/');\n        return A.slice(0, U + 1);\n      }\n      function x(A, U) {\n        w(U, U.type),\n          A.path === '/' ? (A.path = U.path) : (A.path = T(U.path) + A.path);\n      }\n      function w(A, U) {\n        let M = U <= i.RelativePath,\n          c = A.path.split('/'),\n          R = 1,\n          W = 0,\n          X = !1;\n        for (let pe = 1; pe < c.length; pe++) {\n          let ae = c[pe];\n          if (!ae) {\n            X = !0;\n            continue;\n          }\n          if (((X = !1), ae !== '.')) {\n            if (ae === '..') {\n              W ? ((X = !0), W--, R--) : M && (c[R++] = ae);\n              continue;\n            }\n            (c[R++] = ae), W++;\n          }\n        }\n        let ie = '';\n        for (let pe = 1; pe < R; pe++) ie += '/' + c[pe];\n        (!ie || (X && !ie.endsWith('/..'))) && (ie += '/'), (A.path = ie);\n      }\n      function S(A, U) {\n        if (!A && !U) return '';\n        let M = h(A),\n          c = M.type;\n        if (U && c !== i.Absolute) {\n          let W = h(U),\n            X = W.type;\n          switch (c) {\n            case i.Empty:\n              M.hash = W.hash;\n            case i.Hash:\n              M.query = W.query;\n            case i.Query:\n            case i.RelativePath:\n              x(M, W);\n            case i.AbsolutePath:\n              (M.user = W.user), (M.host = W.host), (M.port = W.port);\n            case i.SchemeRelative:\n              M.scheme = W.scheme;\n          }\n          X > c && (c = X);\n        }\n        w(M, c);\n        let R = M.query + M.hash;\n        switch (c) {\n          case i.Hash:\n          case i.Query:\n            return R;\n          case i.RelativePath: {\n            let W = M.path.slice(1);\n            return W ? (y(U || A) && !y(W) ? './' + W + R : W + R) : R || '.';\n          }\n          case i.AbsolutePath:\n            return M.path + R;\n          default:\n            return M.scheme + '//' + M.user + M.host + M.port + M.path + R;\n        }\n      }\n      return S;\n    });\n  });\n  var F1 = Z((no, M1) => {\n    (function (e, t) {\n      typeof no == 'object' && typeof M1 < 'u'\n        ? t(no, ja(), D1())\n        : typeof define == 'function' && define.amd\n        ? define(\n            [\n              'exports',\n              '@jridgewell/sourcemap-codec',\n              '@jridgewell/resolve-uri',\n            ],\n            t\n          )\n        : ((e = typeof globalThis < 'u' ? globalThis : e || self),\n          t((e.traceMapping = {}), e.sourcemapCodec, e.resolveURI));\n    })(no, function (e, t, s) {\n      'use strict';\n      function i(V) {\n        return V && typeof V == 'object' && 'default' in V ? V : {default: V};\n      }\n      var r = i(s);\n      function a(V, G) {\n        return G && !G.endsWith('/') && (G += '/'), r.default(V, G);\n      }\n      function u(V) {\n        if (!V) return '';\n        let G = V.lastIndexOf('/');\n        return V.slice(0, G + 1);\n      }\n      let d = 0,\n        y = 1,\n        g = 2,\n        L = 3,\n        p = 4,\n        h = 1,\n        T = 2;\n      function x(V, G) {\n        let J = w(V, 0);\n        if (J === V.length) return V;\n        G || (V = V.slice());\n        for (let re = J; re < V.length; re = w(V, re + 1)) V[re] = A(V[re], G);\n        return V;\n      }\n      function w(V, G) {\n        for (let J = G; J < V.length; J++) if (!S(V[J])) return J;\n        return V.length;\n      }\n      function S(V) {\n        for (let G = 1; G < V.length; G++) if (V[G][d] < V[G - 1][d]) return !1;\n        return !0;\n      }\n      function A(V, G) {\n        return G || (V = V.slice()), V.sort(U);\n      }\n      function U(V, G) {\n        return V[d] - G[d];\n      }\n      let M = !1;\n      function c(V, G, J, re) {\n        for (; J <= re; ) {\n          let ve = J + ((re - J) >> 1),\n            he = V[ve][d] - G;\n          if (he === 0) return (M = !0), ve;\n          he < 0 ? (J = ve + 1) : (re = ve - 1);\n        }\n        return (M = !1), J - 1;\n      }\n      function R(V, G, J) {\n        for (let re = J + 1; re < V.length && V[re][d] === G; J = re++);\n        return J;\n      }\n      function W(V, G, J) {\n        for (let re = J - 1; re >= 0 && V[re][d] === G; J = re--);\n        return J;\n      }\n      function X() {\n        return {lastKey: -1, lastNeedle: -1, lastIndex: -1};\n      }\n      function ie(V, G, J, re) {\n        let {lastKey: ve, lastNeedle: he, lastIndex: Ie} = J,\n          Ee = 0,\n          Le = V.length - 1;\n        if (re === ve) {\n          if (G === he) return (M = Ie !== -1 && V[Ie][d] === G), Ie;\n          G >= he ? (Ee = Ie === -1 ? 0 : Ie) : (Le = Ie);\n        }\n        return (\n          (J.lastKey = re), (J.lastNeedle = G), (J.lastIndex = c(V, G, Ee, Le))\n        );\n      }\n      function pe(V, G) {\n        let J = G.map(He);\n        for (let re = 0; re < V.length; re++) {\n          let ve = V[re];\n          for (let he = 0; he < ve.length; he++) {\n            let Ie = ve[he];\n            if (Ie.length === 1) continue;\n            let Ee = Ie[y],\n              Le = Ie[g],\n              Xe = Ie[L],\n              We = J[Ee],\n              Ke = We[Le] || (We[Le] = []),\n              ut = G[Ee],\n              pt = R(Ke, Xe, ie(Ke, Xe, ut, Le));\n            ae(Ke, (ut.lastIndex = pt + 1), [Xe, re, Ie[d]]);\n          }\n        }\n        return J;\n      }\n      function ae(V, G, J) {\n        for (let re = V.length; re > G; re--) V[re] = V[re - 1];\n        V[G] = J;\n      }\n      function He() {\n        return {__proto__: null};\n      }\n      let qe = function (V, G) {\n        let J = typeof V == 'string' ? JSON.parse(V) : V;\n        if (!('sections' in J)) return new wt(J, G);\n        let re = [],\n          ve = [],\n          he = [],\n          Ie = [];\n        Bt(J, G, re, ve, he, Ie, 0, 0, 1 / 0, 1 / 0);\n        let Ee = {\n          version: 3,\n          file: J.file,\n          names: Ie,\n          sources: ve,\n          sourcesContent: he,\n          mappings: re,\n        };\n        return e.presortedDecodedMap(Ee);\n      };\n      function Bt(V, G, J, re, ve, he, Ie, Ee, Le, Xe) {\n        let {sections: We} = V;\n        for (let Ke = 0; Ke < We.length; Ke++) {\n          let {map: ut, offset: pt} = We[Ke],\n            bt = Le,\n            yt = Xe;\n          if (Ke + 1 < We.length) {\n            let vt = We[Ke + 1].offset;\n            (bt = Math.min(Le, Ie + vt.line)),\n              bt === Le\n                ? (yt = Math.min(Xe, Ee + vt.column))\n                : bt < Le && (yt = Ee + vt.column);\n          }\n          mt(ut, G, J, re, ve, he, Ie + pt.line, Ee + pt.column, bt, yt);\n        }\n      }\n      function mt(V, G, J, re, ve, he, Ie, Ee, Le, Xe) {\n        if ('sections' in V) return Bt(...arguments);\n        let We = new wt(V, G),\n          Ke = re.length,\n          ut = he.length,\n          pt = e.decodedMappings(We),\n          {resolvedSources: bt, sourcesContent: yt} = We;\n        if ((kt(re, bt), kt(he, We.names), yt)) kt(ve, yt);\n        else for (let vt = 0; vt < bt.length; vt++) ve.push(null);\n        for (let vt = 0; vt < pt.length; vt++) {\n          let bn = Ie + vt;\n          if (bn > Le) return;\n          let Dn = At(J, bn),\n            Ge = vt === 0 ? Ee : 0,\n            St = pt[vt];\n          for (let ot = 0; ot < St.length; ot++) {\n            let zt = St[ot],\n              Xt = Ge + zt[d];\n            if (bn === Le && Xt >= Xe) return;\n            if (zt.length === 1) {\n              Dn.push([Xt]);\n              continue;\n            }\n            let te = Ke + zt[y],\n              Cn = zt[g],\n              Zn = zt[L];\n            Dn.push(\n              zt.length === 4 ? [Xt, te, Cn, Zn] : [Xt, te, Cn, Zn, ut + zt[p]]\n            );\n          }\n        }\n      }\n      function kt(V, G) {\n        for (let J = 0; J < G.length; J++) V.push(G[J]);\n      }\n      function At(V, G) {\n        for (let J = V.length; J <= G; J++) V[J] = [];\n        return V[G];\n      }\n      let tt = '`line` must be greater than 0 (lines start at line 1)',\n        nt =\n          '`column` must be greater than or equal to 0 (columns start at column 0)',\n        _t = -1,\n        ct = 1;\n      (e.encodedMappings = void 0),\n        (e.decodedMappings = void 0),\n        (e.traceSegment = void 0),\n        (e.originalPositionFor = void 0),\n        (e.generatedPositionFor = void 0),\n        (e.eachMapping = void 0),\n        (e.sourceContentFor = void 0),\n        (e.presortedDecodedMap = void 0),\n        (e.decodedMap = void 0),\n        (e.encodedMap = void 0);\n      class wt {\n        constructor(G, J) {\n          let re = typeof G == 'string';\n          if (!re && G._decodedMemo) return G;\n          let ve = re ? JSON.parse(G) : G,\n            {\n              version: he,\n              file: Ie,\n              names: Ee,\n              sourceRoot: Le,\n              sources: Xe,\n              sourcesContent: We,\n            } = ve;\n          (this.version = he),\n            (this.file = Ie),\n            (this.names = Ee),\n            (this.sourceRoot = Le),\n            (this.sources = Xe),\n            (this.sourcesContent = We);\n          let Ke = a(Le || '', u(J));\n          this.resolvedSources = Xe.map((pt) => a(pt || '', Ke));\n          let {mappings: ut} = ve;\n          typeof ut == 'string'\n            ? ((this._encoded = ut), (this._decoded = void 0))\n            : ((this._encoded = void 0), (this._decoded = x(ut, re))),\n            (this._decodedMemo = X()),\n            (this._bySources = void 0),\n            (this._bySourceMemos = void 0);\n        }\n      }\n      (e.encodedMappings = (V) => {\n        var G;\n        return (G = V._encoded) !== null && G !== void 0\n          ? G\n          : (V._encoded = t.encode(V._decoded));\n      }),\n        (e.decodedMappings = (V) =>\n          V._decoded || (V._decoded = t.decode(V._encoded))),\n        (e.traceSegment = (V, G, J) => {\n          let re = e.decodedMappings(V);\n          return G >= re.length ? null : Tn(re[G], V._decodedMemo, G, J, ct);\n        }),\n        (e.originalPositionFor = (V, {line: G, column: J, bias: re}) => {\n          if ((G--, G < 0)) throw new Error(tt);\n          if (J < 0) throw new Error(nt);\n          let ve = e.decodedMappings(V);\n          if (G >= ve.length) return Pt(null, null, null, null);\n          let he = Tn(ve[G], V._decodedMemo, G, J, re || ct);\n          if (he == null || he.length == 1) return Pt(null, null, null, null);\n          let {names: Ie, resolvedSources: Ee} = V;\n          return Pt(\n            Ee[he[y]],\n            he[g] + 1,\n            he[L],\n            he.length === 5 ? Ie[he[p]] : null\n          );\n        }),\n        (e.generatedPositionFor = (\n          V,\n          {source: G, line: J, column: re, bias: ve}\n        ) => {\n          if ((J--, J < 0)) throw new Error(tt);\n          if (re < 0) throw new Error(nt);\n          let {sources: he, resolvedSources: Ie} = V,\n            Ee = he.indexOf(G);\n          if ((Ee === -1 && (Ee = Ie.indexOf(G)), Ee === -1))\n            return qt(null, null);\n          let Le =\n              V._bySources ||\n              (V._bySources = pe(\n                e.decodedMappings(V),\n                (V._bySourceMemos = he.map(X))\n              )),\n            Xe = V._bySourceMemos,\n            We = Le[Ee][J];\n          if (We == null) return qt(null, null);\n          let Ke = Tn(We, Xe[Ee], J, re, ve || ct);\n          return Ke == null ? qt(null, null) : qt(Ke[h] + 1, Ke[T]);\n        }),\n        (e.eachMapping = (V, G) => {\n          let J = e.decodedMappings(V),\n            {names: re, resolvedSources: ve} = V;\n          for (let he = 0; he < J.length; he++) {\n            let Ie = J[he];\n            for (let Ee = 0; Ee < Ie.length; Ee++) {\n              let Le = Ie[Ee],\n                Xe = he + 1,\n                We = Le[0],\n                Ke = null,\n                ut = null,\n                pt = null,\n                bt = null;\n              Le.length !== 1 &&\n                ((Ke = ve[Le[1]]), (ut = Le[2] + 1), (pt = Le[3])),\n                Le.length === 5 && (bt = re[Le[4]]),\n                G({\n                  generatedLine: Xe,\n                  generatedColumn: We,\n                  source: Ke,\n                  originalLine: ut,\n                  originalColumn: pt,\n                  name: bt,\n                });\n            }\n          }\n        }),\n        (e.sourceContentFor = (V, G) => {\n          let {sources: J, resolvedSources: re, sourcesContent: ve} = V;\n          if (ve == null) return null;\n          let he = J.indexOf(G);\n          return he === -1 && (he = re.indexOf(G)), he === -1 ? null : ve[he];\n        }),\n        (e.presortedDecodedMap = (V, G) => {\n          let J = new wt($t(V, []), G);\n          return (J._decoded = V.mappings), J;\n        }),\n        (e.decodedMap = (V) => $t(V, e.decodedMappings(V))),\n        (e.encodedMap = (V) => $t(V, e.encodedMappings(V)));\n      function $t(V, G) {\n        return {\n          version: V.version,\n          file: V.file,\n          names: V.names,\n          sourceRoot: V.sourceRoot,\n          sources: V.sources,\n          sourcesContent: V.sourcesContent,\n          mappings: G,\n        };\n      }\n      function Pt(V, G, J, re) {\n        return {source: V, line: G, column: J, name: re};\n      }\n      function qt(V, G) {\n        return {line: V, column: G};\n      }\n      function Tn(V, G, J, re, ve) {\n        let he = ie(V, re, G, J);\n        return (\n          M ? (he = (ve === _t ? R : W)(V, re, he)) : ve === _t && he++,\n          he === -1 || he === V.length ? null : V[he]\n        );\n      }\n      (e.AnyMap = qe),\n        (e.GREATEST_LOWER_BOUND = ct),\n        (e.LEAST_UPPER_BOUND = _t),\n        (e.TraceMap = wt),\n        Object.defineProperty(e, '__esModule', {value: !0});\n    });\n  });\n  var V1 = Z((so, B1) => {\n    (function (e, t) {\n      typeof so == 'object' && typeof B1 < 'u'\n        ? t(so, L1(), ja(), F1())\n        : typeof define == 'function' && define.amd\n        ? define(\n            [\n              'exports',\n              '@jridgewell/set-array',\n              '@jridgewell/sourcemap-codec',\n              '@jridgewell/trace-mapping',\n            ],\n            t\n          )\n        : ((e = typeof globalThis < 'u' ? globalThis : e || self),\n          t((e.genMapping = {}), e.setArray, e.sourcemapCodec, e.traceMapping));\n    })(so, function (e, t, s, i) {\n      'use strict';\n      (e.addSegment = void 0),\n        (e.addMapping = void 0),\n        (e.maybeAddSegment = void 0),\n        (e.maybeAddMapping = void 0),\n        (e.setSourceContent = void 0),\n        (e.toDecodedMap = void 0),\n        (e.toEncodedMap = void 0),\n        (e.fromMap = void 0),\n        (e.allMappings = void 0);\n      let L;\n      class p {\n        constructor({file: R, sourceRoot: W} = {}) {\n          (this._names = new t.SetArray()),\n            (this._sources = new t.SetArray()),\n            (this._sourcesContent = []),\n            (this._mappings = []),\n            (this.file = R),\n            (this.sourceRoot = W);\n        }\n      }\n      (e.addSegment = (c, R, W, X, ie, pe, ae, He) =>\n        L(!1, c, R, W, X, ie, pe, ae, He)),\n        (e.maybeAddSegment = (c, R, W, X, ie, pe, ae, He) =>\n          L(!0, c, R, W, X, ie, pe, ae, He)),\n        (e.addMapping = (c, R) => M(!1, c, R)),\n        (e.maybeAddMapping = (c, R) => M(!0, c, R)),\n        (e.setSourceContent = (c, R, W) => {\n          let {_sources: X, _sourcesContent: ie} = c;\n          ie[t.put(X, R)] = W;\n        }),\n        (e.toDecodedMap = (c) => {\n          let {\n            file: R,\n            sourceRoot: W,\n            _mappings: X,\n            _sources: ie,\n            _sourcesContent: pe,\n            _names: ae,\n          } = c;\n          return (\n            w(X),\n            {\n              version: 3,\n              file: R || void 0,\n              names: ae.array,\n              sourceRoot: W || void 0,\n              sources: ie.array,\n              sourcesContent: pe,\n              mappings: X,\n            }\n          );\n        }),\n        (e.toEncodedMap = (c) => {\n          let R = e.toDecodedMap(c);\n          return Object.assign(Object.assign({}, R), {\n            mappings: s.encode(R.mappings),\n          });\n        }),\n        (e.allMappings = (c) => {\n          let R = [],\n            {_mappings: W, _sources: X, _names: ie} = c;\n          for (let pe = 0; pe < W.length; pe++) {\n            let ae = W[pe];\n            for (let He = 0; He < ae.length; He++) {\n              let qe = ae[He],\n                Bt = {line: pe + 1, column: qe[0]},\n                mt,\n                kt,\n                At;\n              qe.length !== 1 &&\n                ((mt = X.array[qe[1]]),\n                (kt = {line: qe[2] + 1, column: qe[3]}),\n                qe.length === 5 && (At = ie.array[qe[4]])),\n                R.push({generated: Bt, source: mt, original: kt, name: At});\n            }\n          }\n          return R;\n        }),\n        (e.fromMap = (c) => {\n          let R = new i.TraceMap(c),\n            W = new p({file: R.file, sourceRoot: R.sourceRoot});\n          return (\n            S(W._names, R.names),\n            S(W._sources, R.sources),\n            (W._sourcesContent = R.sourcesContent || R.sources.map(() => null)),\n            (W._mappings = i.decodedMappings(R)),\n            W\n          );\n        }),\n        (L = (c, R, W, X, ie, pe, ae, He, qe) => {\n          let {\n              _mappings: Bt,\n              _sources: mt,\n              _sourcesContent: kt,\n              _names: At,\n            } = R,\n            tt = h(Bt, W),\n            nt = T(tt, X);\n          if (!ie) return c && A(tt, nt) ? void 0 : x(tt, nt, [X]);\n          let _t = t.put(mt, ie),\n            ct = He ? t.put(At, He) : -1;\n          if (\n            (_t === kt.length && (kt[_t] = qe ?? null),\n            !(c && U(tt, nt, _t, pe, ae, ct)))\n          )\n            return x(tt, nt, He ? [X, _t, pe, ae, ct] : [X, _t, pe, ae]);\n        });\n      function h(c, R) {\n        for (let W = c.length; W <= R; W++) c[W] = [];\n        return c[R];\n      }\n      function T(c, R) {\n        let W = c.length;\n        for (let X = W - 1; X >= 0; W = X--) {\n          let ie = c[X];\n          if (R >= ie[0]) break;\n        }\n        return W;\n      }\n      function x(c, R, W) {\n        for (let X = c.length; X > R; X--) c[X] = c[X - 1];\n        c[R] = W;\n      }\n      function w(c) {\n        let {length: R} = c,\n          W = R;\n        for (let X = W - 1; X >= 0 && !(c[X].length > 0); W = X, X--);\n        W < R && (c.length = W);\n      }\n      function S(c, R) {\n        for (let W = 0; W < R.length; W++) t.put(c, R[W]);\n      }\n      function A(c, R) {\n        return R === 0 ? !0 : c[R - 1].length === 1;\n      }\n      function U(c, R, W, X, ie, pe) {\n        if (R === 0) return !1;\n        let ae = c[R - 1];\n        return ae.length === 1\n          ? !1\n          : W === ae[1] &&\n              X === ae[2] &&\n              ie === ae[3] &&\n              pe === (ae.length === 5 ? ae[4] : -1);\n      }\n      function M(c, R, W) {\n        let {generated: X, source: ie, original: pe, name: ae, content: He} = W;\n        if (!ie)\n          return L(c, R, X.line - 1, X.column, null, null, null, null, null);\n        let qe = ie;\n        return L(\n          c,\n          R,\n          X.line - 1,\n          X.column,\n          qe,\n          pe.line - 1,\n          pe.column,\n          ae,\n          He\n        );\n      }\n      (e.GenMapping = p), Object.defineProperty(e, '__esModule', {value: !0});\n    });\n  });\n  var $1 = Z((Ka) => {\n    'use strict';\n    Object.defineProperty(Ka, '__esModule', {value: !0});\n    var Gi = V1(),\n      j1 = Qt();\n    function hy({code: e, mappings: t}, s, i, r, a) {\n      let u = fy(r, a),\n        d = new Gi.GenMapping({file: i.compiledFilename}),\n        y = 0,\n        g = t[0];\n      for (; g === void 0 && y < t.length - 1; ) y++, (g = t[y]);\n      let L = 0,\n        p = 0;\n      g !== p && Gi.maybeAddSegment.call(void 0, d, L, 0, s, L, 0);\n      for (let w = 0; w < e.length; w++) {\n        if (w === g) {\n          let S = g - p,\n            A = u[y];\n          for (\n            Gi.maybeAddSegment.call(void 0, d, L, S, s, L, A);\n            (g === w || g === void 0) && y < t.length - 1;\n\n          )\n            y++, (g = t[y]);\n        }\n        e.charCodeAt(w) === j1.charCodes.lineFeed &&\n          (L++,\n          (p = w + 1),\n          g !== p && Gi.maybeAddSegment.call(void 0, d, L, 0, s, L, 0));\n      }\n      let {\n        sourceRoot: h,\n        sourcesContent: T,\n        ...x\n      } = Gi.toEncodedMap.call(void 0, d);\n      return x;\n    }\n    Ka.default = hy;\n    function fy(e, t) {\n      let s = new Array(t.length),\n        i = 0,\n        r = t[i].start,\n        a = 0;\n      for (let u = 0; u < e.length; u++)\n        u === r && ((s[i] = r - a), i++, (r = t[i].start)),\n          e.charCodeAt(u) === j1.charCodes.lineFeed && (a = u + 1);\n      return s;\n    }\n  });\n  var q1 = Z((Ha) => {\n    'use strict';\n    Object.defineProperty(Ha, '__esModule', {value: !0});\n    var dy = {\n        require: `\n    import {createRequire as CREATE_REQUIRE_NAME} from \"module\";\n    const require = CREATE_REQUIRE_NAME(import.meta.url);\n  `,\n        interopRequireWildcard: `\n    function interopRequireWildcard(obj) {\n      if (obj && obj.__esModule) {\n        return obj;\n      } else {\n        var newObj = {};\n        if (obj != null) {\n          for (var key in obj) {\n            if (Object.prototype.hasOwnProperty.call(obj, key)) {\n              newObj[key] = obj[key];\n            }\n          }\n        }\n        newObj.default = obj;\n        return newObj;\n      }\n    }\n  `,\n        interopRequireDefault: `\n    function interopRequireDefault(obj) {\n      return obj && obj.__esModule ? obj : { default: obj };\n    }\n  `,\n        createNamedExportFrom: `\n    function createNamedExportFrom(obj, localName, importedName) {\n      Object.defineProperty(exports, localName, {enumerable: true, configurable: true, get: () => obj[importedName]});\n    }\n  `,\n        createStarExport: `\n    function createStarExport(obj) {\n      Object.keys(obj)\n        .filter((key) => key !== \"default\" && key !== \"__esModule\")\n        .forEach((key) => {\n          if (exports.hasOwnProperty(key)) {\n            return;\n          }\n          Object.defineProperty(exports, key, {enumerable: true, configurable: true, get: () => obj[key]});\n        });\n    }\n  `,\n        nullishCoalesce: `\n    function nullishCoalesce(lhs, rhsFn) {\n      if (lhs != null) {\n        return lhs;\n      } else {\n        return rhsFn();\n      }\n    }\n  `,\n        asyncNullishCoalesce: `\n    async function asyncNullishCoalesce(lhs, rhsFn) {\n      if (lhs != null) {\n        return lhs;\n      } else {\n        return await rhsFn();\n      }\n    }\n  `,\n        optionalChain: `\n    function optionalChain(ops) {\n      let lastAccessLHS = undefined;\n      let value = ops[0];\n      let i = 1;\n      while (i < ops.length) {\n        const op = ops[i];\n        const fn = ops[i + 1];\n        i += 2;\n        if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) {\n          return undefined;\n        }\n        if (op === 'access' || op === 'optionalAccess') {\n          lastAccessLHS = value;\n          value = fn(value);\n        } else if (op === 'call' || op === 'optionalCall') {\n          value = fn((...args) => value.call(lastAccessLHS, ...args));\n          lastAccessLHS = undefined;\n        }\n      }\n      return value;\n    }\n  `,\n        asyncOptionalChain: `\n    async function asyncOptionalChain(ops) {\n      let lastAccessLHS = undefined;\n      let value = ops[0];\n      let i = 1;\n      while (i < ops.length) {\n        const op = ops[i];\n        const fn = ops[i + 1];\n        i += 2;\n        if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) {\n          return undefined;\n        }\n        if (op === 'access' || op === 'optionalAccess') {\n          lastAccessLHS = value;\n          value = await fn(value);\n        } else if (op === 'call' || op === 'optionalCall') {\n          value = await fn((...args) => value.call(lastAccessLHS, ...args));\n          lastAccessLHS = undefined;\n        }\n      }\n      return value;\n    }\n  `,\n        optionalChainDelete: `\n    function optionalChainDelete(ops) {\n      const result = OPTIONAL_CHAIN_NAME(ops);\n      return result == null ? true : result;\n    }\n  `,\n        asyncOptionalChainDelete: `\n    async function asyncOptionalChainDelete(ops) {\n      const result = await ASYNC_OPTIONAL_CHAIN_NAME(ops);\n      return result == null ? true : result;\n    }\n  `,\n      },\n      Ua = class e {\n        __init() {\n          this.helperNames = {};\n        }\n        __init2() {\n          this.createRequireName = null;\n        }\n        constructor(t) {\n          (this.nameManager = t),\n            e.prototype.__init.call(this),\n            e.prototype.__init2.call(this);\n        }\n        getHelperName(t) {\n          let s = this.helperNames[t];\n          return (\n            s ||\n            ((s = this.nameManager.claimFreeName(`_${t}`)),\n            (this.helperNames[t] = s),\n            s)\n          );\n        }\n        emitHelpers() {\n          let t = '';\n          this.helperNames.optionalChainDelete &&\n            this.getHelperName('optionalChain'),\n            this.helperNames.asyncOptionalChainDelete &&\n              this.getHelperName('asyncOptionalChain');\n          for (let [s, i] of Object.entries(dy)) {\n            let r = this.helperNames[s],\n              a = i;\n            s === 'optionalChainDelete'\n              ? (a = a.replace(\n                  'OPTIONAL_CHAIN_NAME',\n                  this.helperNames.optionalChain\n                ))\n              : s === 'asyncOptionalChainDelete'\n              ? (a = a.replace(\n                  'ASYNC_OPTIONAL_CHAIN_NAME',\n                  this.helperNames.asyncOptionalChain\n                ))\n              : s === 'require' &&\n                (this.createRequireName === null &&\n                  (this.createRequireName =\n                    this.nameManager.claimFreeName('_createRequire')),\n                (a = a.replace(\n                  /CREATE_REQUIRE_NAME/g,\n                  this.createRequireName\n                ))),\n              r &&\n                ((t += ' '),\n                (t += a.replace(s, r).replace(/\\s+/g, ' ').trim()));\n          }\n          return t;\n        }\n      };\n    Ha.HelperManager = Ua;\n  });\n  var H1 = Z((ro) => {\n    'use strict';\n    Object.defineProperty(ro, '__esModule', {value: !0});\n    var Wa = xt(),\n      io = be();\n    function my(e, t, s) {\n      U1(e, s) && yy(e, t, s);\n    }\n    ro.default = my;\n    function U1(e, t) {\n      for (let s of e.tokens)\n        if (\n          s.type === io.TokenType.name &&\n          Wa.isNonTopLevelDeclaration.call(void 0, s) &&\n          t.has(e.identifierNameForToken(s))\n        )\n          return !0;\n      return !1;\n    }\n    ro.hasShadowedGlobals = U1;\n    function yy(e, t, s) {\n      let i = [],\n        r = t.length - 1;\n      for (let a = e.tokens.length - 1; ; a--) {\n        for (; i.length > 0 && i[i.length - 1].startTokenIndex === a + 1; )\n          i.pop();\n        for (; r >= 0 && t[r].endTokenIndex === a + 1; ) i.push(t[r]), r--;\n        if (a < 0) break;\n        let u = e.tokens[a],\n          d = e.identifierNameForToken(u);\n        if (i.length > 1 && u.type === io.TokenType.name && s.has(d)) {\n          if (Wa.isBlockScopedDeclaration.call(void 0, u))\n            K1(i[i.length - 1], e, d);\n          else if (Wa.isFunctionScopedDeclaration.call(void 0, u)) {\n            let y = i.length - 1;\n            for (; y > 0 && !i[y].isFunctionScope; ) y--;\n            if (y < 0) throw new Error('Did not find parent function scope.');\n            K1(i[y], e, d);\n          }\n        }\n      }\n      if (i.length > 0)\n        throw new Error('Expected empty scope stack after processing file.');\n    }\n    function K1(e, t, s) {\n      for (let i = e.startTokenIndex; i < e.endTokenIndex; i++) {\n        let r = t.tokens[i];\n        (r.type === io.TokenType.name || r.type === io.TokenType.jsxName) &&\n          t.identifierNameForToken(r) === s &&\n          (r.shadowsGlobal = !0);\n      }\n    }\n  });\n  var W1 = Z((Ga) => {\n    'use strict';\n    Object.defineProperty(Ga, '__esModule', {value: !0});\n    var Ty = be();\n    function ky(e, t) {\n      let s = [];\n      for (let i of t)\n        i.type === Ty.TokenType.name && s.push(e.slice(i.start, i.end));\n      return s;\n    }\n    Ga.default = ky;\n  });\n  var G1 = Z((Xa) => {\n    'use strict';\n    Object.defineProperty(Xa, '__esModule', {value: !0});\n    function vy(e) {\n      return e && e.__esModule ? e : {default: e};\n    }\n    var xy = W1(),\n      gy = vy(xy),\n      za = class e {\n        __init() {\n          this.usedNames = new Set();\n        }\n        constructor(t, s) {\n          e.prototype.__init.call(this),\n            (this.usedNames = new Set(gy.default.call(void 0, t, s)));\n        }\n        claimFreeName(t) {\n          let s = this.findFreeName(t);\n          return this.usedNames.add(s), s;\n        }\n        findFreeName(t) {\n          if (!this.usedNames.has(t)) return t;\n          let s = 2;\n          for (; this.usedNames.has(t + String(s)); ) s++;\n          return t + String(s);\n        }\n      };\n    Xa.default = za;\n  });\n  var oo = Z((Pn) => {\n    'use strict';\n    var _y =\n      (Pn && Pn.__extends) ||\n      (function () {\n        var e = function (t, s) {\n          return (\n            (e =\n              Object.setPrototypeOf ||\n              ({__proto__: []} instanceof Array &&\n                function (i, r) {\n                  i.__proto__ = r;\n                }) ||\n              function (i, r) {\n                for (var a in r) r.hasOwnProperty(a) && (i[a] = r[a]);\n              }),\n            e(t, s)\n          );\n        };\n        return function (t, s) {\n          e(t, s);\n          function i() {\n            this.constructor = t;\n          }\n          t.prototype =\n            s === null\n              ? Object.create(s)\n              : ((i.prototype = s.prototype), new i());\n        };\n      })();\n    Object.defineProperty(Pn, '__esModule', {value: !0});\n    Pn.DetailContext = Pn.NoopContext = Pn.VError = void 0;\n    var z1 = (function (e) {\n      _y(t, e);\n      function t(s, i) {\n        var r = e.call(this, i) || this;\n        return (r.path = s), Object.setPrototypeOf(r, t.prototype), r;\n      }\n      return t;\n    })(Error);\n    Pn.VError = z1;\n    var by = (function () {\n      function e() {}\n      return (\n        (e.prototype.fail = function (t, s, i) {\n          return !1;\n        }),\n        (e.prototype.unionResolver = function () {\n          return this;\n        }),\n        (e.prototype.createContext = function () {\n          return this;\n        }),\n        (e.prototype.resolveUnion = function (t) {}),\n        e\n      );\n    })();\n    Pn.NoopContext = by;\n    var X1 = (function () {\n      function e() {\n        (this._propNames = ['']), (this._messages = [null]), (this._score = 0);\n      }\n      return (\n        (e.prototype.fail = function (t, s, i) {\n          return (\n            this._propNames.push(t),\n            this._messages.push(s),\n            (this._score += i),\n            !1\n          );\n        }),\n        (e.prototype.unionResolver = function () {\n          return new Cy();\n        }),\n        (e.prototype.resolveUnion = function (t) {\n          for (\n            var s, i, r = t, a = null, u = 0, d = r.contexts;\n            u < d.length;\n            u++\n          ) {\n            var y = d[u];\n            (!a || y._score >= a._score) && (a = y);\n          }\n          a &&\n            a._score > 0 &&\n            ((s = this._propNames).push.apply(s, a._propNames),\n            (i = this._messages).push.apply(i, a._messages));\n        }),\n        (e.prototype.getError = function (t) {\n          for (var s = [], i = this._propNames.length - 1; i >= 0; i--) {\n            var r = this._propNames[i];\n            t += typeof r == 'number' ? '[' + r + ']' : r ? '.' + r : '';\n            var a = this._messages[i];\n            a && s.push(t + ' ' + a);\n          }\n          return new z1(t, s.join('; '));\n        }),\n        (e.prototype.getErrorDetail = function (t) {\n          for (var s = [], i = this._propNames.length - 1; i >= 0; i--) {\n            var r = this._propNames[i];\n            t += typeof r == 'number' ? '[' + r + ']' : r ? '.' + r : '';\n            var a = this._messages[i];\n            a && s.push({path: t, message: a});\n          }\n          for (var u = null, i = s.length - 1; i >= 0; i--)\n            u && (s[i].nested = [u]), (u = s[i]);\n          return u;\n        }),\n        e\n      );\n    })();\n    Pn.DetailContext = X1;\n    var Cy = (function () {\n      function e() {\n        this.contexts = [];\n      }\n      return (\n        (e.prototype.createContext = function () {\n          var t = new X1();\n          return this.contexts.push(t), t;\n        }),\n        e\n      );\n    })();\n  });\n  var sl = Z((ce) => {\n    'use strict';\n    var nn =\n      (ce && ce.__extends) ||\n      (function () {\n        var e = function (t, s) {\n          return (\n            (e =\n              Object.setPrototypeOf ||\n              ({__proto__: []} instanceof Array &&\n                function (i, r) {\n                  i.__proto__ = r;\n                }) ||\n              function (i, r) {\n                for (var a in r) r.hasOwnProperty(a) && (i[a] = r[a]);\n              }),\n            e(t, s)\n          );\n        };\n        return function (t, s) {\n          e(t, s);\n          function i() {\n            this.constructor = t;\n          }\n          t.prototype =\n            s === null\n              ? Object.create(s)\n              : ((i.prototype = s.prototype), new i());\n        };\n      })();\n    Object.defineProperty(ce, '__esModule', {value: !0});\n    ce.basicTypes =\n      ce.BasicType =\n      ce.TParamList =\n      ce.TParam =\n      ce.param =\n      ce.TFunc =\n      ce.func =\n      ce.TProp =\n      ce.TOptional =\n      ce.opt =\n      ce.TIface =\n      ce.iface =\n      ce.TEnumLiteral =\n      ce.enumlit =\n      ce.TEnumType =\n      ce.enumtype =\n      ce.TIntersection =\n      ce.intersection =\n      ce.TUnion =\n      ce.union =\n      ce.TTuple =\n      ce.tuple =\n      ce.TArray =\n      ce.array =\n      ce.TLiteral =\n      ce.lit =\n      ce.TName =\n      ce.name =\n      ce.TType =\n        void 0;\n    var Q1 = oo(),\n      Ht = (function () {\n        function e() {}\n        return e;\n      })();\n    ce.TType = Ht;\n    function ps(e) {\n      return typeof e == 'string' ? Z1(e) : e;\n    }\n    function Qa(e, t) {\n      var s = e[t];\n      if (!s) throw new Error('Unknown type ' + t);\n      return s;\n    }\n    function Z1(e) {\n      return new Za(e);\n    }\n    ce.name = Z1;\n    var Za = (function (e) {\n      nn(t, e);\n      function t(s) {\n        var i = e.call(this) || this;\n        return (i.name = s), (i._failMsg = 'is not a ' + s), i;\n      }\n      return (\n        (t.prototype.getChecker = function (s, i, r) {\n          var a = this,\n            u = Qa(s, this.name),\n            d = u.getChecker(s, i, r);\n          return u instanceof Dt || u instanceof t\n            ? d\n            : function (y, g) {\n                return d(y, g) ? !0 : g.fail(null, a._failMsg, 0);\n              };\n        }),\n        t\n      );\n    })(Ht);\n    ce.TName = Za;\n    function wy(e) {\n      return new el(e);\n    }\n    ce.lit = wy;\n    var el = (function (e) {\n      nn(t, e);\n      function t(s) {\n        var i = e.call(this) || this;\n        return (\n          (i.value = s),\n          (i.name = JSON.stringify(s)),\n          (i._failMsg = 'is not ' + i.name),\n          i\n        );\n      }\n      return (\n        (t.prototype.getChecker = function (s, i) {\n          var r = this;\n          return function (a, u) {\n            return a === r.value ? !0 : u.fail(null, r._failMsg, -1);\n          };\n        }),\n        t\n      );\n    })(Ht);\n    ce.TLiteral = el;\n    function Sy(e) {\n      return new ep(ps(e));\n    }\n    ce.array = Sy;\n    var ep = (function (e) {\n      nn(t, e);\n      function t(s) {\n        var i = e.call(this) || this;\n        return (i.ttype = s), i;\n      }\n      return (\n        (t.prototype.getChecker = function (s, i) {\n          var r = this.ttype.getChecker(s, i);\n          return function (a, u) {\n            if (!Array.isArray(a)) return u.fail(null, 'is not an array', 0);\n            for (var d = 0; d < a.length; d++) {\n              var y = r(a[d], u);\n              if (!y) return u.fail(d, null, 1);\n            }\n            return !0;\n          };\n        }),\n        t\n      );\n    })(Ht);\n    ce.TArray = ep;\n    function Iy() {\n      for (var e = [], t = 0; t < arguments.length; t++) e[t] = arguments[t];\n      return new tp(\n        e.map(function (s) {\n          return ps(s);\n        })\n      );\n    }\n    ce.tuple = Iy;\n    var tp = (function (e) {\n      nn(t, e);\n      function t(s) {\n        var i = e.call(this) || this;\n        return (i.ttypes = s), i;\n      }\n      return (\n        (t.prototype.getChecker = function (s, i) {\n          var r = this.ttypes.map(function (u) {\n              return u.getChecker(s, i);\n            }),\n            a = function (u, d) {\n              if (!Array.isArray(u)) return d.fail(null, 'is not an array', 0);\n              for (var y = 0; y < r.length; y++) {\n                var g = r[y](u[y], d);\n                if (!g) return d.fail(y, null, 1);\n              }\n              return !0;\n            };\n          return i\n            ? function (u, d) {\n                return a(u, d)\n                  ? u.length <= r.length\n                    ? !0\n                    : d.fail(r.length, 'is extraneous', 2)\n                  : !1;\n              }\n            : a;\n        }),\n        t\n      );\n    })(Ht);\n    ce.TTuple = tp;\n    function Ey() {\n      for (var e = [], t = 0; t < arguments.length; t++) e[t] = arguments[t];\n      return new np(\n        e.map(function (s) {\n          return ps(s);\n        })\n      );\n    }\n    ce.union = Ey;\n    var np = (function (e) {\n      nn(t, e);\n      function t(s) {\n        var i = e.call(this) || this;\n        i.ttypes = s;\n        var r = s\n            .map(function (u) {\n              return u instanceof Za || u instanceof el ? u.name : null;\n            })\n            .filter(function (u) {\n              return u;\n            }),\n          a = s.length - r.length;\n        return (\n          r.length\n            ? (a > 0 && r.push(a + ' more'),\n              (i._failMsg = 'is none of ' + r.join(', ')))\n            : (i._failMsg = 'is none of ' + a + ' types'),\n          i\n        );\n      }\n      return (\n        (t.prototype.getChecker = function (s, i) {\n          var r = this,\n            a = this.ttypes.map(function (u) {\n              return u.getChecker(s, i);\n            });\n          return function (u, d) {\n            for (var y = d.unionResolver(), g = 0; g < a.length; g++) {\n              var L = a[g](u, y.createContext());\n              if (L) return !0;\n            }\n            return d.resolveUnion(y), d.fail(null, r._failMsg, 0);\n          };\n        }),\n        t\n      );\n    })(Ht);\n    ce.TUnion = np;\n    function Ay() {\n      for (var e = [], t = 0; t < arguments.length; t++) e[t] = arguments[t];\n      return new sp(\n        e.map(function (s) {\n          return ps(s);\n        })\n      );\n    }\n    ce.intersection = Ay;\n    var sp = (function (e) {\n      nn(t, e);\n      function t(s) {\n        var i = e.call(this) || this;\n        return (i.ttypes = s), i;\n      }\n      return (\n        (t.prototype.getChecker = function (s, i) {\n          var r = new Set(),\n            a = this.ttypes.map(function (u) {\n              return u.getChecker(s, i, r);\n            });\n          return function (u, d) {\n            var y = a.every(function (g) {\n              return g(u, d);\n            });\n            return y ? !0 : d.fail(null, null, 0);\n          };\n        }),\n        t\n      );\n    })(Ht);\n    ce.TIntersection = sp;\n    function Py(e) {\n      return new tl(e);\n    }\n    ce.enumtype = Py;\n    var tl = (function (e) {\n      nn(t, e);\n      function t(s) {\n        var i = e.call(this) || this;\n        return (\n          (i.members = s),\n          (i.validValues = new Set()),\n          (i._failMsg = 'is not a valid enum value'),\n          (i.validValues = new Set(\n            Object.keys(s).map(function (r) {\n              return s[r];\n            })\n          )),\n          i\n        );\n      }\n      return (\n        (t.prototype.getChecker = function (s, i) {\n          var r = this;\n          return function (a, u) {\n            return r.validValues.has(a) ? !0 : u.fail(null, r._failMsg, 0);\n          };\n        }),\n        t\n      );\n    })(Ht);\n    ce.TEnumType = tl;\n    function Ny(e, t) {\n      return new ip(e, t);\n    }\n    ce.enumlit = Ny;\n    var ip = (function (e) {\n      nn(t, e);\n      function t(s, i) {\n        var r = e.call(this) || this;\n        return (\n          (r.enumName = s),\n          (r.prop = i),\n          (r._failMsg = 'is not ' + s + '.' + i),\n          r\n        );\n      }\n      return (\n        (t.prototype.getChecker = function (s, i) {\n          var r = this,\n            a = Qa(s, this.enumName);\n          if (!(a instanceof tl))\n            throw new Error(\n              'Type ' + this.enumName + ' used in enumlit is not an enum type'\n            );\n          var u = a.members[this.prop];\n          if (!a.members.hasOwnProperty(this.prop))\n            throw new Error(\n              'Unknown value ' +\n                this.enumName +\n                '.' +\n                this.prop +\n                ' used in enumlit'\n            );\n          return function (d, y) {\n            return d === u ? !0 : y.fail(null, r._failMsg, -1);\n          };\n        }),\n        t\n      );\n    })(Ht);\n    ce.TEnumLiteral = ip;\n    function Ry(e) {\n      return Object.keys(e).map(function (t) {\n        return Ly(t, e[t]);\n      });\n    }\n    function Ly(e, t) {\n      return t instanceof nl ? new Ja(e, t.ttype, !0) : new Ja(e, ps(t), !1);\n    }\n    function Oy(e, t) {\n      return new rp(e, Ry(t));\n    }\n    ce.iface = Oy;\n    var rp = (function (e) {\n      nn(t, e);\n      function t(s, i) {\n        var r = e.call(this) || this;\n        return (\n          (r.bases = s),\n          (r.props = i),\n          (r.propSet = new Set(\n            i.map(function (a) {\n              return a.name;\n            })\n          )),\n          r\n        );\n      }\n      return (\n        (t.prototype.getChecker = function (s, i, r) {\n          var a = this,\n            u = this.bases.map(function (h) {\n              return Qa(s, h).getChecker(s, i);\n            }),\n            d = this.props.map(function (h) {\n              return h.ttype.getChecker(s, i);\n            }),\n            y = new Q1.NoopContext(),\n            g = this.props.map(function (h, T) {\n              return !h.isOpt && !d[T](void 0, y);\n            }),\n            L = function (h, T) {\n              if (typeof h != 'object' || h === null)\n                return T.fail(null, 'is not an object', 0);\n              for (var x = 0; x < u.length; x++) if (!u[x](h, T)) return !1;\n              for (var x = 0; x < d.length; x++) {\n                var w = a.props[x].name,\n                  S = h[w];\n                if (S === void 0) {\n                  if (g[x]) return T.fail(w, 'is missing', 1);\n                } else {\n                  var A = d[x](S, T);\n                  if (!A) return T.fail(w, null, 1);\n                }\n              }\n              return !0;\n            };\n          if (!i) return L;\n          var p = this.propSet;\n          return (\n            r &&\n              (this.propSet.forEach(function (h) {\n                return r.add(h);\n              }),\n              (p = r)),\n            function (h, T) {\n              if (!L(h, T)) return !1;\n              for (var x in h)\n                if (!p.has(x)) return T.fail(x, 'is extraneous', 2);\n              return !0;\n            }\n          );\n        }),\n        t\n      );\n    })(Ht);\n    ce.TIface = rp;\n    function Dy(e) {\n      return new nl(ps(e));\n    }\n    ce.opt = Dy;\n    var nl = (function (e) {\n      nn(t, e);\n      function t(s) {\n        var i = e.call(this) || this;\n        return (i.ttype = s), i;\n      }\n      return (\n        (t.prototype.getChecker = function (s, i) {\n          var r = this.ttype.getChecker(s, i);\n          return function (a, u) {\n            return a === void 0 || r(a, u);\n          };\n        }),\n        t\n      );\n    })(Ht);\n    ce.TOptional = nl;\n    var Ja = (function () {\n      function e(t, s, i) {\n        (this.name = t), (this.ttype = s), (this.isOpt = i);\n      }\n      return e;\n    })();\n    ce.TProp = Ja;\n    function My(e) {\n      for (var t = [], s = 1; s < arguments.length; s++)\n        t[s - 1] = arguments[s];\n      return new op(new lp(t), ps(e));\n    }\n    ce.func = My;\n    var op = (function (e) {\n      nn(t, e);\n      function t(s, i) {\n        var r = e.call(this) || this;\n        return (r.paramList = s), (r.result = i), r;\n      }\n      return (\n        (t.prototype.getChecker = function (s, i) {\n          return function (r, a) {\n            return typeof r == 'function'\n              ? !0\n              : a.fail(null, 'is not a function', 0);\n          };\n        }),\n        t\n      );\n    })(Ht);\n    ce.TFunc = op;\n    function Fy(e, t, s) {\n      return new ap(e, ps(t), !!s);\n    }\n    ce.param = Fy;\n    var ap = (function () {\n      function e(t, s, i) {\n        (this.name = t), (this.ttype = s), (this.isOpt = i);\n      }\n      return e;\n    })();\n    ce.TParam = ap;\n    var lp = (function (e) {\n      nn(t, e);\n      function t(s) {\n        var i = e.call(this) || this;\n        return (i.params = s), i;\n      }\n      return (\n        (t.prototype.getChecker = function (s, i) {\n          var r = this,\n            a = this.params.map(function (g) {\n              return g.ttype.getChecker(s, i);\n            }),\n            u = new Q1.NoopContext(),\n            d = this.params.map(function (g, L) {\n              return !g.isOpt && !a[L](void 0, u);\n            }),\n            y = function (g, L) {\n              if (!Array.isArray(g)) return L.fail(null, 'is not an array', 0);\n              for (var p = 0; p < a.length; p++) {\n                var h = r.params[p];\n                if (g[p] === void 0) {\n                  if (d[p]) return L.fail(h.name, 'is missing', 1);\n                } else {\n                  var T = a[p](g[p], L);\n                  if (!T) return L.fail(h.name, null, 1);\n                }\n              }\n              return !0;\n            };\n          return i\n            ? function (g, L) {\n                return y(g, L)\n                  ? g.length <= a.length\n                    ? !0\n                    : L.fail(a.length, 'is extraneous', 2)\n                  : !1;\n              }\n            : y;\n        }),\n        t\n      );\n    })(Ht);\n    ce.TParamList = lp;\n    var Dt = (function (e) {\n      nn(t, e);\n      function t(s, i) {\n        var r = e.call(this) || this;\n        return (r.validator = s), (r.message = i), r;\n      }\n      return (\n        (t.prototype.getChecker = function (s, i) {\n          var r = this;\n          return function (a, u) {\n            return r.validator(a) ? !0 : u.fail(null, r.message, 0);\n          };\n        }),\n        t\n      );\n    })(Ht);\n    ce.BasicType = Dt;\n    ce.basicTypes = {\n      any: new Dt(function (e) {\n        return !0;\n      }, 'is invalid'),\n      number: new Dt(function (e) {\n        return typeof e == 'number';\n      }, 'is not a number'),\n      object: new Dt(function (e) {\n        return typeof e == 'object' && e;\n      }, 'is not an object'),\n      boolean: new Dt(function (e) {\n        return typeof e == 'boolean';\n      }, 'is not a boolean'),\n      string: new Dt(function (e) {\n        return typeof e == 'string';\n      }, 'is not a string'),\n      symbol: new Dt(function (e) {\n        return typeof e == 'symbol';\n      }, 'is not a symbol'),\n      void: new Dt(function (e) {\n        return e == null;\n      }, 'is not void'),\n      undefined: new Dt(function (e) {\n        return e === void 0;\n      }, 'is not undefined'),\n      null: new Dt(function (e) {\n        return e === null;\n      }, 'is not null'),\n      never: new Dt(function (e) {\n        return !1;\n      }, 'is unexpected'),\n      Date: new Dt(Y1('[object Date]'), 'is not a Date'),\n      RegExp: new Dt(Y1('[object RegExp]'), 'is not a RegExp'),\n    };\n    var By = Object.prototype.toString;\n    function Y1(e) {\n      return function (t) {\n        return typeof t == 'object' && t && By.call(t) === e;\n      };\n    }\n    typeof Buffer < 'u' &&\n      (ce.basicTypes.Buffer = new Dt(function (e) {\n        return Buffer.isBuffer(e);\n      }, 'is not a Buffer'));\n    var Vy = function (e) {\n      ce.basicTypes[e.name] = new Dt(function (t) {\n        return t instanceof e;\n      }, 'is not a ' + e.name);\n    };\n    for (\n      ao = 0,\n        Ya = [\n          Int8Array,\n          Uint8Array,\n          Uint8ClampedArray,\n          Int16Array,\n          Uint16Array,\n          Int32Array,\n          Uint32Array,\n          Float32Array,\n          Float64Array,\n          ArrayBuffer,\n        ];\n      ao < Ya.length;\n      ao++\n    )\n      (J1 = Ya[ao]), Vy(J1);\n    var J1, ao, Ya;\n  });\n  var il = Z((we) => {\n    'use strict';\n    var jy =\n      (we && we.__spreadArrays) ||\n      function () {\n        for (var e = 0, t = 0, s = arguments.length; t < s; t++)\n          e += arguments[t].length;\n        for (var i = Array(e), r = 0, t = 0; t < s; t++)\n          for (var a = arguments[t], u = 0, d = a.length; u < d; u++, r++)\n            i[r] = a[u];\n        return i;\n      };\n    Object.defineProperty(we, '__esModule', {value: !0});\n    we.Checker = we.createCheckers = void 0;\n    var zi = sl(),\n      pi = oo(),\n      ze = sl();\n    Object.defineProperty(we, 'TArray', {\n      enumerable: !0,\n      get: function () {\n        return ze.TArray;\n      },\n    });\n    Object.defineProperty(we, 'TEnumType', {\n      enumerable: !0,\n      get: function () {\n        return ze.TEnumType;\n      },\n    });\n    Object.defineProperty(we, 'TEnumLiteral', {\n      enumerable: !0,\n      get: function () {\n        return ze.TEnumLiteral;\n      },\n    });\n    Object.defineProperty(we, 'TFunc', {\n      enumerable: !0,\n      get: function () {\n        return ze.TFunc;\n      },\n    });\n    Object.defineProperty(we, 'TIface', {\n      enumerable: !0,\n      get: function () {\n        return ze.TIface;\n      },\n    });\n    Object.defineProperty(we, 'TLiteral', {\n      enumerable: !0,\n      get: function () {\n        return ze.TLiteral;\n      },\n    });\n    Object.defineProperty(we, 'TName', {\n      enumerable: !0,\n      get: function () {\n        return ze.TName;\n      },\n    });\n    Object.defineProperty(we, 'TOptional', {\n      enumerable: !0,\n      get: function () {\n        return ze.TOptional;\n      },\n    });\n    Object.defineProperty(we, 'TParam', {\n      enumerable: !0,\n      get: function () {\n        return ze.TParam;\n      },\n    });\n    Object.defineProperty(we, 'TParamList', {\n      enumerable: !0,\n      get: function () {\n        return ze.TParamList;\n      },\n    });\n    Object.defineProperty(we, 'TProp', {\n      enumerable: !0,\n      get: function () {\n        return ze.TProp;\n      },\n    });\n    Object.defineProperty(we, 'TTuple', {\n      enumerable: !0,\n      get: function () {\n        return ze.TTuple;\n      },\n    });\n    Object.defineProperty(we, 'TType', {\n      enumerable: !0,\n      get: function () {\n        return ze.TType;\n      },\n    });\n    Object.defineProperty(we, 'TUnion', {\n      enumerable: !0,\n      get: function () {\n        return ze.TUnion;\n      },\n    });\n    Object.defineProperty(we, 'TIntersection', {\n      enumerable: !0,\n      get: function () {\n        return ze.TIntersection;\n      },\n    });\n    Object.defineProperty(we, 'array', {\n      enumerable: !0,\n      get: function () {\n        return ze.array;\n      },\n    });\n    Object.defineProperty(we, 'enumlit', {\n      enumerable: !0,\n      get: function () {\n        return ze.enumlit;\n      },\n    });\n    Object.defineProperty(we, 'enumtype', {\n      enumerable: !0,\n      get: function () {\n        return ze.enumtype;\n      },\n    });\n    Object.defineProperty(we, 'func', {\n      enumerable: !0,\n      get: function () {\n        return ze.func;\n      },\n    });\n    Object.defineProperty(we, 'iface', {\n      enumerable: !0,\n      get: function () {\n        return ze.iface;\n      },\n    });\n    Object.defineProperty(we, 'lit', {\n      enumerable: !0,\n      get: function () {\n        return ze.lit;\n      },\n    });\n    Object.defineProperty(we, 'name', {\n      enumerable: !0,\n      get: function () {\n        return ze.name;\n      },\n    });\n    Object.defineProperty(we, 'opt', {\n      enumerable: !0,\n      get: function () {\n        return ze.opt;\n      },\n    });\n    Object.defineProperty(we, 'param', {\n      enumerable: !0,\n      get: function () {\n        return ze.param;\n      },\n    });\n    Object.defineProperty(we, 'tuple', {\n      enumerable: !0,\n      get: function () {\n        return ze.tuple;\n      },\n    });\n    Object.defineProperty(we, 'union', {\n      enumerable: !0,\n      get: function () {\n        return ze.union;\n      },\n    });\n    Object.defineProperty(we, 'intersection', {\n      enumerable: !0,\n      get: function () {\n        return ze.intersection;\n      },\n    });\n    Object.defineProperty(we, 'BasicType', {\n      enumerable: !0,\n      get: function () {\n        return ze.BasicType;\n      },\n    });\n    var $y = oo();\n    Object.defineProperty(we, 'VError', {\n      enumerable: !0,\n      get: function () {\n        return $y.VError;\n      },\n    });\n    function qy() {\n      for (var e = [], t = 0; t < arguments.length; t++) e[t] = arguments[t];\n      for (\n        var s = Object.assign.apply(Object, jy([{}, zi.basicTypes], e)),\n          i = {},\n          r = 0,\n          a = e;\n        r < a.length;\n        r++\n      )\n        for (var u = a[r], d = 0, y = Object.keys(u); d < y.length; d++) {\n          var g = y[d];\n          i[g] = new cp(s, u[g]);\n        }\n      return i;\n    }\n    we.createCheckers = qy;\n    var cp = (function () {\n      function e(t, s, i) {\n        if (\n          (i === void 0 && (i = 'value'),\n          (this.suite = t),\n          (this.ttype = s),\n          (this._path = i),\n          (this.props = new Map()),\n          s instanceof zi.TIface)\n        )\n          for (var r = 0, a = s.props; r < a.length; r++) {\n            var u = a[r];\n            this.props.set(u.name, u.ttype);\n          }\n        (this.checkerPlain = this.ttype.getChecker(t, !1)),\n          (this.checkerStrict = this.ttype.getChecker(t, !0));\n      }\n      return (\n        (e.prototype.setReportedPath = function (t) {\n          this._path = t;\n        }),\n        (e.prototype.check = function (t) {\n          return this._doCheck(this.checkerPlain, t);\n        }),\n        (e.prototype.test = function (t) {\n          return this.checkerPlain(t, new pi.NoopContext());\n        }),\n        (e.prototype.validate = function (t) {\n          return this._doValidate(this.checkerPlain, t);\n        }),\n        (e.prototype.strictCheck = function (t) {\n          return this._doCheck(this.checkerStrict, t);\n        }),\n        (e.prototype.strictTest = function (t) {\n          return this.checkerStrict(t, new pi.NoopContext());\n        }),\n        (e.prototype.strictValidate = function (t) {\n          return this._doValidate(this.checkerStrict, t);\n        }),\n        (e.prototype.getProp = function (t) {\n          var s = this.props.get(t);\n          if (!s) throw new Error('Type has no property ' + t);\n          return new e(this.suite, s, this._path + '.' + t);\n        }),\n        (e.prototype.methodArgs = function (t) {\n          var s = this._getMethod(t);\n          return new e(this.suite, s.paramList);\n        }),\n        (e.prototype.methodResult = function (t) {\n          var s = this._getMethod(t);\n          return new e(this.suite, s.result);\n        }),\n        (e.prototype.getArgs = function () {\n          if (!(this.ttype instanceof zi.TFunc))\n            throw new Error('getArgs() applied to non-function');\n          return new e(this.suite, this.ttype.paramList);\n        }),\n        (e.prototype.getResult = function () {\n          if (!(this.ttype instanceof zi.TFunc))\n            throw new Error('getResult() applied to non-function');\n          return new e(this.suite, this.ttype.result);\n        }),\n        (e.prototype.getType = function () {\n          return this.ttype;\n        }),\n        (e.prototype._doCheck = function (t, s) {\n          var i = new pi.NoopContext();\n          if (!t(s, i)) {\n            var r = new pi.DetailContext();\n            throw (t(s, r), r.getError(this._path));\n          }\n        }),\n        (e.prototype._doValidate = function (t, s) {\n          var i = new pi.NoopContext();\n          if (t(s, i)) return null;\n          var r = new pi.DetailContext();\n          return t(s, r), r.getErrorDetail(this._path);\n        }),\n        (e.prototype._getMethod = function (t) {\n          var s = this.props.get(t);\n          if (!s) throw new Error('Type has no property ' + t);\n          if (!(s instanceof zi.TFunc))\n            throw new Error('Property ' + t + ' is not a method');\n          return s;\n        }),\n        e\n      );\n    })();\n    we.Checker = cp;\n  });\n  var up = Z((Gn) => {\n    'use strict';\n    Object.defineProperty(Gn, '__esModule', {value: !0});\n    function Ky(e) {\n      if (e && e.__esModule) return e;\n      var t = {};\n      if (e != null)\n        for (var s in e)\n          Object.prototype.hasOwnProperty.call(e, s) && (t[s] = e[s]);\n      return (t.default = e), t;\n    }\n    var Uy = il(),\n      Qe = Ky(Uy),\n      Hy = Qe.union(\n        Qe.lit('jsx'),\n        Qe.lit('typescript'),\n        Qe.lit('flow'),\n        Qe.lit('imports'),\n        Qe.lit('react-hot-loader'),\n        Qe.lit('jest')\n      );\n    Gn.Transform = Hy;\n    var Wy = Qe.iface([], {compiledFilename: 'string'});\n    Gn.SourceMapOptions = Wy;\n    var Gy = Qe.iface([], {\n      transforms: Qe.array('Transform'),\n      disableESTransforms: Qe.opt('boolean'),\n      jsxRuntime: Qe.opt(\n        Qe.union(Qe.lit('classic'), Qe.lit('automatic'), Qe.lit('preserve'))\n      ),\n      production: Qe.opt('boolean'),\n      jsxImportSource: Qe.opt('string'),\n      jsxPragma: Qe.opt('string'),\n      jsxFragmentPragma: Qe.opt('string'),\n      preserveDynamicImport: Qe.opt('boolean'),\n      injectCreateRequireForImportRequire: Qe.opt('boolean'),\n      enableLegacyTypeScriptModuleInterop: Qe.opt('boolean'),\n      enableLegacyBabel5ModuleInterop: Qe.opt('boolean'),\n      sourceMapOptions: Qe.opt('SourceMapOptions'),\n      filePath: Qe.opt('string'),\n    });\n    Gn.Options = Gy;\n    var zy = {\n      Transform: Gn.Transform,\n      SourceMapOptions: Gn.SourceMapOptions,\n      Options: Gn.Options,\n    };\n    Gn.default = zy;\n  });\n  var pp = Z((rl) => {\n    'use strict';\n    Object.defineProperty(rl, '__esModule', {value: !0});\n    function Xy(e) {\n      return e && e.__esModule ? e : {default: e};\n    }\n    var Yy = il(),\n      Jy = up(),\n      Qy = Xy(Jy),\n      {Options: Zy} = Yy.createCheckers.call(void 0, Qy.default);\n    function eT(e) {\n      Zy.strictCheck(e);\n    }\n    rl.validateOptions = eT;\n  });\n  var lo = Z((Nn) => {\n    'use strict';\n    Object.defineProperty(Nn, '__esModule', {value: !0});\n    var tT = Ji(),\n      hp = hi(),\n      Mt = xt(),\n      Xi = It(),\n      fn = be(),\n      gt = Zt(),\n      Yi = Ns(),\n      ol = cs();\n    function nT() {\n      Mt.next.call(void 0), Yi.parseMaybeAssign.call(void 0, !1);\n    }\n    Nn.parseSpread = nT;\n    function fp(e) {\n      Mt.next.call(void 0), ll(e);\n    }\n    Nn.parseRest = fp;\n    function dp(e) {\n      Yi.parseIdentifier.call(void 0), mp(e);\n    }\n    Nn.parseBindingIdentifier = dp;\n    function sT() {\n      Yi.parseIdentifier.call(void 0),\n        (gt.state.tokens[gt.state.tokens.length - 1].identifierRole =\n          Mt.IdentifierRole.ImportDeclaration);\n    }\n    Nn.parseImportedIdentifier = sT;\n    function mp(e) {\n      let t;\n      gt.state.scopeDepth === 0\n        ? (t = Mt.IdentifierRole.TopLevelDeclaration)\n        : e\n        ? (t = Mt.IdentifierRole.BlockScopedDeclaration)\n        : (t = Mt.IdentifierRole.FunctionScopedDeclaration),\n        (gt.state.tokens[gt.state.tokens.length - 1].identifierRole = t);\n    }\n    Nn.markPriorBindingIdentifier = mp;\n    function ll(e) {\n      switch (gt.state.type) {\n        case fn.TokenType._this: {\n          let t = Mt.pushTypeContext.call(void 0, 0);\n          Mt.next.call(void 0), Mt.popTypeContext.call(void 0, t);\n          return;\n        }\n        case fn.TokenType._yield:\n        case fn.TokenType.name: {\n          (gt.state.type = fn.TokenType.name), dp(e);\n          return;\n        }\n        case fn.TokenType.bracketL: {\n          Mt.next.call(void 0), yp(fn.TokenType.bracketR, e, !0);\n          return;\n        }\n        case fn.TokenType.braceL:\n          Yi.parseObj.call(void 0, !0, e);\n          return;\n        default:\n          ol.unexpected.call(void 0);\n      }\n    }\n    Nn.parseBindingAtom = ll;\n    function yp(e, t, s = !1, i = !1, r = 0) {\n      let a = !0,\n        u = !1,\n        d = gt.state.tokens.length;\n      for (; !Mt.eat.call(void 0, e) && !gt.state.error; )\n        if (\n          (a\n            ? (a = !1)\n            : (ol.expect.call(void 0, fn.TokenType.comma),\n              (gt.state.tokens[gt.state.tokens.length - 1].contextId = r),\n              !u &&\n                gt.state.tokens[d].isType &&\n                ((gt.state.tokens[gt.state.tokens.length - 1].isType = !0),\n                (u = !0))),\n          !(s && Mt.match.call(void 0, fn.TokenType.comma)))\n        ) {\n          if (Mt.eat.call(void 0, e)) break;\n          if (Mt.match.call(void 0, fn.TokenType.ellipsis)) {\n            fp(t),\n              Tp(),\n              Mt.eat.call(void 0, fn.TokenType.comma),\n              ol.expect.call(void 0, e);\n            break;\n          } else iT(i, t);\n        }\n    }\n    Nn.parseBindingList = yp;\n    function iT(e, t) {\n      e &&\n        hp.tsParseModifiers.call(void 0, [\n          Xi.ContextualKeyword._public,\n          Xi.ContextualKeyword._protected,\n          Xi.ContextualKeyword._private,\n          Xi.ContextualKeyword._readonly,\n          Xi.ContextualKeyword._override,\n        ]),\n        al(t),\n        Tp(),\n        al(t, !0);\n    }\n    function Tp() {\n      gt.isFlowEnabled\n        ? tT.flowParseAssignableListItemTypes.call(void 0)\n        : gt.isTypeScriptEnabled &&\n          hp.tsParseAssignableListItemTypes.call(void 0);\n    }\n    function al(e, t = !1) {\n      if ((t || ll(e), !Mt.eat.call(void 0, fn.TokenType.eq))) return;\n      let s = gt.state.tokens.length - 1;\n      Yi.parseMaybeAssign.call(void 0),\n        (gt.state.tokens[s].rhsEndIndex = gt.state.tokens.length);\n    }\n    Nn.parseMaybeDefault = al;\n  });\n  var hi = Z((Oe) => {\n    'use strict';\n    Object.defineProperty(Oe, '__esModule', {value: !0});\n    var v = xt(),\n      oe = It(),\n      k = be(),\n      I = Zt(),\n      _e = Ns(),\n      di = lo(),\n      Rn = nr(),\n      H = cs(),\n      rT = vl();\n    function ul() {\n      return v.match.call(void 0, k.TokenType.name);\n    }\n    function oT() {\n      return (\n        v.match.call(void 0, k.TokenType.name) ||\n        !!(I.state.type & k.TokenType.IS_KEYWORD) ||\n        v.match.call(void 0, k.TokenType.string) ||\n        v.match.call(void 0, k.TokenType.num) ||\n        v.match.call(void 0, k.TokenType.bigint) ||\n        v.match.call(void 0, k.TokenType.decimal)\n      );\n    }\n    function _p() {\n      let e = I.state.snapshot();\n      return (\n        v.next.call(void 0),\n        (v.match.call(void 0, k.TokenType.bracketL) ||\n          v.match.call(void 0, k.TokenType.braceL) ||\n          v.match.call(void 0, k.TokenType.star) ||\n          v.match.call(void 0, k.TokenType.ellipsis) ||\n          v.match.call(void 0, k.TokenType.hash) ||\n          oT()) &&\n        !H.hasPrecedingLineBreak.call(void 0)\n          ? !0\n          : (I.state.restoreFromSnapshot(e), !1)\n      );\n    }\n    function bp(e) {\n      for (; dl(e) !== null; );\n    }\n    Oe.tsParseModifiers = bp;\n    function dl(e) {\n      if (!v.match.call(void 0, k.TokenType.name)) return null;\n      let t = I.state.contextualKeyword;\n      if (e.indexOf(t) !== -1 && _p()) {\n        switch (t) {\n          case oe.ContextualKeyword._readonly:\n            I.state.tokens[I.state.tokens.length - 1].type =\n              k.TokenType._readonly;\n            break;\n          case oe.ContextualKeyword._abstract:\n            I.state.tokens[I.state.tokens.length - 1].type =\n              k.TokenType._abstract;\n            break;\n          case oe.ContextualKeyword._static:\n            I.state.tokens[I.state.tokens.length - 1].type =\n              k.TokenType._static;\n            break;\n          case oe.ContextualKeyword._public:\n            I.state.tokens[I.state.tokens.length - 1].type =\n              k.TokenType._public;\n            break;\n          case oe.ContextualKeyword._private:\n            I.state.tokens[I.state.tokens.length - 1].type =\n              k.TokenType._private;\n            break;\n          case oe.ContextualKeyword._protected:\n            I.state.tokens[I.state.tokens.length - 1].type =\n              k.TokenType._protected;\n            break;\n          case oe.ContextualKeyword._override:\n            I.state.tokens[I.state.tokens.length - 1].type =\n              k.TokenType._override;\n            break;\n          case oe.ContextualKeyword._declare:\n            I.state.tokens[I.state.tokens.length - 1].type =\n              k.TokenType._declare;\n            break;\n          default:\n            break;\n        }\n        return t;\n      }\n      return null;\n    }\n    Oe.tsParseModifier = dl;\n    function Zi() {\n      for (\n        _e.parseIdentifier.call(void 0);\n        v.eat.call(void 0, k.TokenType.dot);\n\n      )\n        _e.parseIdentifier.call(void 0);\n    }\n    function aT() {\n      Zi(),\n        !H.hasPrecedingLineBreak.call(void 0) &&\n          v.match.call(void 0, k.TokenType.lessThan) &&\n          yi();\n    }\n    function lT() {\n      v.next.call(void 0), tr();\n    }\n    function cT() {\n      v.next.call(void 0);\n    }\n    function uT() {\n      H.expect.call(void 0, k.TokenType._typeof),\n        v.match.call(void 0, k.TokenType._import) ? Cp() : Zi(),\n        !H.hasPrecedingLineBreak.call(void 0) &&\n          v.match.call(void 0, k.TokenType.lessThan) &&\n          yi();\n    }\n    function Cp() {\n      H.expect.call(void 0, k.TokenType._import),\n        H.expect.call(void 0, k.TokenType.parenL),\n        H.expect.call(void 0, k.TokenType.string),\n        H.expect.call(void 0, k.TokenType.parenR),\n        v.eat.call(void 0, k.TokenType.dot) && Zi(),\n        v.match.call(void 0, k.TokenType.lessThan) && yi();\n    }\n    function pT() {\n      v.eat.call(void 0, k.TokenType._const);\n      let e = v.eat.call(void 0, k.TokenType._in),\n        t = H.eatContextual.call(void 0, oe.ContextualKeyword._out);\n      v.eat.call(void 0, k.TokenType._const),\n        (e || t) && !v.match.call(void 0, k.TokenType.name)\n          ? (I.state.tokens[I.state.tokens.length - 1].type = k.TokenType.name)\n          : _e.parseIdentifier.call(void 0),\n        v.eat.call(void 0, k.TokenType._extends) && rt(),\n        v.eat.call(void 0, k.TokenType.eq) && rt();\n    }\n    function mi() {\n      v.match.call(void 0, k.TokenType.lessThan) && uo();\n    }\n    Oe.tsTryParseTypeParameters = mi;\n    function uo() {\n      let e = v.pushTypeContext.call(void 0, 0);\n      for (\n        v.match.call(void 0, k.TokenType.lessThan) ||\n        v.match.call(void 0, k.TokenType.typeParameterStart)\n          ? v.next.call(void 0)\n          : H.unexpected.call(void 0);\n        !v.eat.call(void 0, k.TokenType.greaterThan) && !I.state.error;\n\n      )\n        pT(), v.eat.call(void 0, k.TokenType.comma);\n      v.popTypeContext.call(void 0, e);\n    }\n    function ml(e) {\n      let t = e === k.TokenType.arrow;\n      mi(),\n        H.expect.call(void 0, k.TokenType.parenL),\n        I.state.scopeDepth++,\n        hT(!1),\n        I.state.scopeDepth--,\n        (t || v.match.call(void 0, e)) && Qi(e);\n    }\n    function hT(e) {\n      di.parseBindingList.call(void 0, k.TokenType.parenR, e);\n    }\n    function co() {\n      v.eat.call(void 0, k.TokenType.comma) || H.semicolon.call(void 0);\n    }\n    function kp() {\n      ml(k.TokenType.colon), co();\n    }\n    function fT() {\n      let e = I.state.snapshot();\n      v.next.call(void 0);\n      let t =\n        v.eat.call(void 0, k.TokenType.name) &&\n        v.match.call(void 0, k.TokenType.colon);\n      return I.state.restoreFromSnapshot(e), t;\n    }\n    function wp() {\n      if (!(v.match.call(void 0, k.TokenType.bracketL) && fT())) return !1;\n      let e = v.pushTypeContext.call(void 0, 0);\n      return (\n        H.expect.call(void 0, k.TokenType.bracketL),\n        _e.parseIdentifier.call(void 0),\n        tr(),\n        H.expect.call(void 0, k.TokenType.bracketR),\n        er(),\n        co(),\n        v.popTypeContext.call(void 0, e),\n        !0\n      );\n    }\n    function vp(e) {\n      v.eat.call(void 0, k.TokenType.question),\n        !e &&\n        (v.match.call(void 0, k.TokenType.parenL) ||\n          v.match.call(void 0, k.TokenType.lessThan))\n          ? (ml(k.TokenType.colon), co())\n          : (er(), co());\n    }\n    function dT() {\n      if (\n        v.match.call(void 0, k.TokenType.parenL) ||\n        v.match.call(void 0, k.TokenType.lessThan)\n      ) {\n        kp();\n        return;\n      }\n      if (v.match.call(void 0, k.TokenType._new)) {\n        v.next.call(void 0),\n          v.match.call(void 0, k.TokenType.parenL) ||\n          v.match.call(void 0, k.TokenType.lessThan)\n            ? kp()\n            : vp(!1);\n        return;\n      }\n      let e = !!dl([oe.ContextualKeyword._readonly]);\n      wp() ||\n        ((H.isContextual.call(void 0, oe.ContextualKeyword._get) ||\n          H.isContextual.call(void 0, oe.ContextualKeyword._set)) &&\n          _p(),\n        _e.parsePropertyName.call(void 0, -1),\n        vp(e));\n    }\n    function mT() {\n      Sp();\n    }\n    function Sp() {\n      for (\n        H.expect.call(void 0, k.TokenType.braceL);\n        !v.eat.call(void 0, k.TokenType.braceR) && !I.state.error;\n\n      )\n        dT();\n    }\n    function yT() {\n      let e = I.state.snapshot(),\n        t = TT();\n      return I.state.restoreFromSnapshot(e), t;\n    }\n    function TT() {\n      return (\n        v.next.call(void 0),\n        v.eat.call(void 0, k.TokenType.plus) ||\n        v.eat.call(void 0, k.TokenType.minus)\n          ? H.isContextual.call(void 0, oe.ContextualKeyword._readonly)\n          : (H.isContextual.call(void 0, oe.ContextualKeyword._readonly) &&\n              v.next.call(void 0),\n            !v.match.call(void 0, k.TokenType.bracketL) ||\n            (v.next.call(void 0), !ul())\n              ? !1\n              : (v.next.call(void 0), v.match.call(void 0, k.TokenType._in)))\n      );\n    }\n    function kT() {\n      _e.parseIdentifier.call(void 0),\n        H.expect.call(void 0, k.TokenType._in),\n        rt();\n    }\n    function vT() {\n      H.expect.call(void 0, k.TokenType.braceL),\n        v.match.call(void 0, k.TokenType.plus) ||\n        v.match.call(void 0, k.TokenType.minus)\n          ? (v.next.call(void 0),\n            H.expectContextual.call(void 0, oe.ContextualKeyword._readonly))\n          : H.eatContextual.call(void 0, oe.ContextualKeyword._readonly),\n        H.expect.call(void 0, k.TokenType.bracketL),\n        kT(),\n        H.eatContextual.call(void 0, oe.ContextualKeyword._as) && rt(),\n        H.expect.call(void 0, k.TokenType.bracketR),\n        v.match.call(void 0, k.TokenType.plus) ||\n        v.match.call(void 0, k.TokenType.minus)\n          ? (v.next.call(void 0), H.expect.call(void 0, k.TokenType.question))\n          : v.eat.call(void 0, k.TokenType.question),\n        LT(),\n        H.semicolon.call(void 0),\n        H.expect.call(void 0, k.TokenType.braceR);\n    }\n    function xT() {\n      for (\n        H.expect.call(void 0, k.TokenType.bracketL);\n        !v.eat.call(void 0, k.TokenType.bracketR) && !I.state.error;\n\n      )\n        gT(), v.eat.call(void 0, k.TokenType.comma);\n    }\n    function gT() {\n      v.eat.call(void 0, k.TokenType.ellipsis)\n        ? rt()\n        : (rt(), v.eat.call(void 0, k.TokenType.question)),\n        v.eat.call(void 0, k.TokenType.colon) && rt();\n    }\n    function _T() {\n      H.expect.call(void 0, k.TokenType.parenL),\n        rt(),\n        H.expect.call(void 0, k.TokenType.parenR);\n    }\n    function bT() {\n      for (\n        v.nextTemplateToken.call(void 0), v.nextTemplateToken.call(void 0);\n        !v.match.call(void 0, k.TokenType.backQuote) && !I.state.error;\n\n      )\n        H.expect.call(void 0, k.TokenType.dollarBraceL),\n          rt(),\n          v.nextTemplateToken.call(void 0),\n          v.nextTemplateToken.call(void 0);\n      v.next.call(void 0);\n    }\n    var hs;\n    (function (e) {\n      e[(e.TSFunctionType = 0)] = 'TSFunctionType';\n      let s = 1;\n      e[(e.TSConstructorType = s)] = 'TSConstructorType';\n      let i = s + 1;\n      e[(e.TSAbstractConstructorType = i)] = 'TSAbstractConstructorType';\n    })(hs || (hs = {}));\n    function cl(e) {\n      e === hs.TSAbstractConstructorType &&\n        H.expectContextual.call(void 0, oe.ContextualKeyword._abstract),\n        (e === hs.TSConstructorType || e === hs.TSAbstractConstructorType) &&\n          H.expect.call(void 0, k.TokenType._new);\n      let t = I.state.inDisallowConditionalTypesContext;\n      (I.state.inDisallowConditionalTypesContext = !1),\n        ml(k.TokenType.arrow),\n        (I.state.inDisallowConditionalTypesContext = t);\n    }\n    function CT() {\n      switch (I.state.type) {\n        case k.TokenType.name:\n          aT();\n          return;\n        case k.TokenType._void:\n        case k.TokenType._null:\n          v.next.call(void 0);\n          return;\n        case k.TokenType.string:\n        case k.TokenType.num:\n        case k.TokenType.bigint:\n        case k.TokenType.decimal:\n        case k.TokenType._true:\n        case k.TokenType._false:\n          _e.parseLiteral.call(void 0);\n          return;\n        case k.TokenType.minus:\n          v.next.call(void 0), _e.parseLiteral.call(void 0);\n          return;\n        case k.TokenType._this: {\n          cT(),\n            H.isContextual.call(void 0, oe.ContextualKeyword._is) &&\n              !H.hasPrecedingLineBreak.call(void 0) &&\n              lT();\n          return;\n        }\n        case k.TokenType._typeof:\n          uT();\n          return;\n        case k.TokenType._import:\n          Cp();\n          return;\n        case k.TokenType.braceL:\n          yT() ? vT() : mT();\n          return;\n        case k.TokenType.bracketL:\n          xT();\n          return;\n        case k.TokenType.parenL:\n          _T();\n          return;\n        case k.TokenType.backQuote:\n          bT();\n          return;\n        default:\n          if (I.state.type & k.TokenType.IS_KEYWORD) {\n            v.next.call(void 0),\n              (I.state.tokens[I.state.tokens.length - 1].type =\n                k.TokenType.name);\n            return;\n          }\n          break;\n      }\n      H.unexpected.call(void 0);\n    }\n    function wT() {\n      for (\n        CT();\n        !H.hasPrecedingLineBreak.call(void 0) &&\n        v.eat.call(void 0, k.TokenType.bracketL);\n\n      )\n        v.eat.call(void 0, k.TokenType.bracketR) ||\n          (rt(), H.expect.call(void 0, k.TokenType.bracketR));\n    }\n    function ST() {\n      if (\n        (H.expectContextual.call(void 0, oe.ContextualKeyword._infer),\n        _e.parseIdentifier.call(void 0),\n        v.match.call(void 0, k.TokenType._extends))\n      ) {\n        let e = I.state.snapshot();\n        H.expect.call(void 0, k.TokenType._extends);\n        let t = I.state.inDisallowConditionalTypesContext;\n        (I.state.inDisallowConditionalTypesContext = !0),\n          rt(),\n          (I.state.inDisallowConditionalTypesContext = t),\n          (I.state.error ||\n            (!I.state.inDisallowConditionalTypesContext &&\n              v.match.call(void 0, k.TokenType.question))) &&\n            I.state.restoreFromSnapshot(e);\n      }\n    }\n    function pl() {\n      if (\n        H.isContextual.call(void 0, oe.ContextualKeyword._keyof) ||\n        H.isContextual.call(void 0, oe.ContextualKeyword._unique) ||\n        H.isContextual.call(void 0, oe.ContextualKeyword._readonly)\n      )\n        v.next.call(void 0), pl();\n      else if (H.isContextual.call(void 0, oe.ContextualKeyword._infer)) ST();\n      else {\n        let e = I.state.inDisallowConditionalTypesContext;\n        (I.state.inDisallowConditionalTypesContext = !1),\n          wT(),\n          (I.state.inDisallowConditionalTypesContext = e);\n      }\n    }\n    function xp() {\n      if (\n        (v.eat.call(void 0, k.TokenType.bitwiseAND),\n        pl(),\n        v.match.call(void 0, k.TokenType.bitwiseAND))\n      )\n        for (; v.eat.call(void 0, k.TokenType.bitwiseAND); ) pl();\n    }\n    function IT() {\n      if (\n        (v.eat.call(void 0, k.TokenType.bitwiseOR),\n        xp(),\n        v.match.call(void 0, k.TokenType.bitwiseOR))\n      )\n        for (; v.eat.call(void 0, k.TokenType.bitwiseOR); ) xp();\n    }\n    function ET() {\n      return v.match.call(void 0, k.TokenType.lessThan)\n        ? !0\n        : v.match.call(void 0, k.TokenType.parenL) && PT();\n    }\n    function AT() {\n      if (\n        v.match.call(void 0, k.TokenType.name) ||\n        v.match.call(void 0, k.TokenType._this)\n      )\n        return v.next.call(void 0), !0;\n      if (\n        v.match.call(void 0, k.TokenType.braceL) ||\n        v.match.call(void 0, k.TokenType.bracketL)\n      ) {\n        let e = 1;\n        for (v.next.call(void 0); e > 0 && !I.state.error; )\n          v.match.call(void 0, k.TokenType.braceL) ||\n          v.match.call(void 0, k.TokenType.bracketL)\n            ? e++\n            : (v.match.call(void 0, k.TokenType.braceR) ||\n                v.match.call(void 0, k.TokenType.bracketR)) &&\n              e--,\n            v.next.call(void 0);\n        return !0;\n      }\n      return !1;\n    }\n    function PT() {\n      let e = I.state.snapshot(),\n        t = NT();\n      return I.state.restoreFromSnapshot(e), t;\n    }\n    function NT() {\n      return (\n        v.next.call(void 0),\n        !!(\n          v.match.call(void 0, k.TokenType.parenR) ||\n          v.match.call(void 0, k.TokenType.ellipsis) ||\n          (AT() &&\n            (v.match.call(void 0, k.TokenType.colon) ||\n              v.match.call(void 0, k.TokenType.comma) ||\n              v.match.call(void 0, k.TokenType.question) ||\n              v.match.call(void 0, k.TokenType.eq) ||\n              (v.match.call(void 0, k.TokenType.parenR) &&\n                (v.next.call(void 0),\n                v.match.call(void 0, k.TokenType.arrow)))))\n        )\n      );\n    }\n    function Qi(e) {\n      let t = v.pushTypeContext.call(void 0, 0);\n      H.expect.call(void 0, e), OT() || rt(), v.popTypeContext.call(void 0, t);\n    }\n    function RT() {\n      v.match.call(void 0, k.TokenType.colon) && Qi(k.TokenType.colon);\n    }\n    function er() {\n      v.match.call(void 0, k.TokenType.colon) && tr();\n    }\n    Oe.tsTryParseTypeAnnotation = er;\n    function LT() {\n      v.eat.call(void 0, k.TokenType.colon) && rt();\n    }\n    function OT() {\n      let e = I.state.snapshot();\n      return H.isContextual.call(void 0, oe.ContextualKeyword._asserts)\n        ? (v.next.call(void 0),\n          H.eatContextual.call(void 0, oe.ContextualKeyword._is)\n            ? (rt(), !0)\n            : ul() || v.match.call(void 0, k.TokenType._this)\n            ? (v.next.call(void 0),\n              H.eatContextual.call(void 0, oe.ContextualKeyword._is) && rt(),\n              !0)\n            : (I.state.restoreFromSnapshot(e), !1))\n        : ul() || v.match.call(void 0, k.TokenType._this)\n        ? (v.next.call(void 0),\n          H.isContextual.call(void 0, oe.ContextualKeyword._is) &&\n          !H.hasPrecedingLineBreak.call(void 0)\n            ? (v.next.call(void 0), rt(), !0)\n            : (I.state.restoreFromSnapshot(e), !1))\n        : !1;\n    }\n    function tr() {\n      let e = v.pushTypeContext.call(void 0, 0);\n      H.expect.call(void 0, k.TokenType.colon),\n        rt(),\n        v.popTypeContext.call(void 0, e);\n    }\n    Oe.tsParseTypeAnnotation = tr;\n    function rt() {\n      if (\n        (hl(),\n        I.state.inDisallowConditionalTypesContext ||\n          H.hasPrecedingLineBreak.call(void 0) ||\n          !v.eat.call(void 0, k.TokenType._extends))\n      )\n        return;\n      let e = I.state.inDisallowConditionalTypesContext;\n      (I.state.inDisallowConditionalTypesContext = !0),\n        hl(),\n        (I.state.inDisallowConditionalTypesContext = e),\n        H.expect.call(void 0, k.TokenType.question),\n        rt(),\n        H.expect.call(void 0, k.TokenType.colon),\n        rt();\n    }\n    Oe.tsParseType = rt;\n    function DT() {\n      return (\n        H.isContextual.call(void 0, oe.ContextualKeyword._abstract) &&\n        v.lookaheadType.call(void 0) === k.TokenType._new\n      );\n    }\n    function hl() {\n      if (ET()) {\n        cl(hs.TSFunctionType);\n        return;\n      }\n      if (v.match.call(void 0, k.TokenType._new)) {\n        cl(hs.TSConstructorType);\n        return;\n      } else if (DT()) {\n        cl(hs.TSAbstractConstructorType);\n        return;\n      }\n      IT();\n    }\n    Oe.tsParseNonConditionalType = hl;\n    function MT() {\n      let e = v.pushTypeContext.call(void 0, 1);\n      rt(),\n        H.expect.call(void 0, k.TokenType.greaterThan),\n        v.popTypeContext.call(void 0, e),\n        _e.parseMaybeUnary.call(void 0);\n    }\n    Oe.tsParseTypeAssertion = MT;\n    function FT() {\n      if (v.eat.call(void 0, k.TokenType.jsxTagStart)) {\n        I.state.tokens[I.state.tokens.length - 1].type =\n          k.TokenType.typeParameterStart;\n        let e = v.pushTypeContext.call(void 0, 1);\n        for (\n          ;\n          !v.match.call(void 0, k.TokenType.greaterThan) && !I.state.error;\n\n        )\n          rt(), v.eat.call(void 0, k.TokenType.comma);\n        rT.nextJSXTagToken.call(void 0), v.popTypeContext.call(void 0, e);\n      }\n    }\n    Oe.tsTryParseJSXTypeArgument = FT;\n    function Ip() {\n      for (; !v.match.call(void 0, k.TokenType.braceL) && !I.state.error; )\n        BT(), v.eat.call(void 0, k.TokenType.comma);\n    }\n    function BT() {\n      Zi(), v.match.call(void 0, k.TokenType.lessThan) && yi();\n    }\n    function VT() {\n      di.parseBindingIdentifier.call(void 0, !1),\n        mi(),\n        v.eat.call(void 0, k.TokenType._extends) && Ip(),\n        Sp();\n    }\n    function jT() {\n      di.parseBindingIdentifier.call(void 0, !1),\n        mi(),\n        H.expect.call(void 0, k.TokenType.eq),\n        rt(),\n        H.semicolon.call(void 0);\n    }\n    function $T() {\n      if (\n        (v.match.call(void 0, k.TokenType.string)\n          ? _e.parseLiteral.call(void 0)\n          : _e.parseIdentifier.call(void 0),\n        v.eat.call(void 0, k.TokenType.eq))\n      ) {\n        let e = I.state.tokens.length - 1;\n        _e.parseMaybeAssign.call(void 0),\n          (I.state.tokens[e].rhsEndIndex = I.state.tokens.length);\n      }\n    }\n    function yl() {\n      for (\n        di.parseBindingIdentifier.call(void 0, !1),\n          H.expect.call(void 0, k.TokenType.braceL);\n        !v.eat.call(void 0, k.TokenType.braceR) && !I.state.error;\n\n      )\n        $T(), v.eat.call(void 0, k.TokenType.comma);\n    }\n    function Tl() {\n      H.expect.call(void 0, k.TokenType.braceL),\n        Rn.parseBlockBody.call(void 0, k.TokenType.braceR);\n    }\n    function fl() {\n      di.parseBindingIdentifier.call(void 0, !1),\n        v.eat.call(void 0, k.TokenType.dot) ? fl() : Tl();\n    }\n    function Ep() {\n      H.isContextual.call(void 0, oe.ContextualKeyword._global)\n        ? _e.parseIdentifier.call(void 0)\n        : v.match.call(void 0, k.TokenType.string)\n        ? _e.parseExprAtom.call(void 0)\n        : H.unexpected.call(void 0),\n        v.match.call(void 0, k.TokenType.braceL)\n          ? Tl()\n          : H.semicolon.call(void 0);\n    }\n    function Ap() {\n      di.parseImportedIdentifier.call(void 0),\n        H.expect.call(void 0, k.TokenType.eq),\n        KT(),\n        H.semicolon.call(void 0);\n    }\n    Oe.tsParseImportEqualsDeclaration = Ap;\n    function qT() {\n      return (\n        H.isContextual.call(void 0, oe.ContextualKeyword._require) &&\n        v.lookaheadType.call(void 0) === k.TokenType.parenL\n      );\n    }\n    function KT() {\n      qT() ? UT() : Zi();\n    }\n    function UT() {\n      H.expectContextual.call(void 0, oe.ContextualKeyword._require),\n        H.expect.call(void 0, k.TokenType.parenL),\n        v.match.call(void 0, k.TokenType.string) || H.unexpected.call(void 0),\n        _e.parseLiteral.call(void 0),\n        H.expect.call(void 0, k.TokenType.parenR);\n    }\n    function HT() {\n      if (H.isLineTerminator.call(void 0)) return !1;\n      switch (I.state.type) {\n        case k.TokenType._function: {\n          let e = v.pushTypeContext.call(void 0, 1);\n          v.next.call(void 0);\n          let t = I.state.start;\n          return (\n            Rn.parseFunction.call(void 0, t, !0),\n            v.popTypeContext.call(void 0, e),\n            !0\n          );\n        }\n        case k.TokenType._class: {\n          let e = v.pushTypeContext.call(void 0, 1);\n          return (\n            Rn.parseClass.call(void 0, !0, !1),\n            v.popTypeContext.call(void 0, e),\n            !0\n          );\n        }\n        case k.TokenType._const:\n          if (\n            v.match.call(void 0, k.TokenType._const) &&\n            H.isLookaheadContextual.call(void 0, oe.ContextualKeyword._enum)\n          ) {\n            let e = v.pushTypeContext.call(void 0, 1);\n            return (\n              H.expect.call(void 0, k.TokenType._const),\n              H.expectContextual.call(void 0, oe.ContextualKeyword._enum),\n              (I.state.tokens[I.state.tokens.length - 1].type =\n                k.TokenType._enum),\n              yl(),\n              v.popTypeContext.call(void 0, e),\n              !0\n            );\n          }\n        case k.TokenType._var:\n        case k.TokenType._let: {\n          let e = v.pushTypeContext.call(void 0, 1);\n          return (\n            Rn.parseVarStatement.call(\n              void 0,\n              I.state.type !== k.TokenType._var\n            ),\n            v.popTypeContext.call(void 0, e),\n            !0\n          );\n        }\n        case k.TokenType.name: {\n          let e = v.pushTypeContext.call(void 0, 1),\n            t = I.state.contextualKeyword,\n            s = !1;\n          return (\n            t === oe.ContextualKeyword._global\n              ? (Ep(), (s = !0))\n              : (s = po(t, !0)),\n            v.popTypeContext.call(void 0, e),\n            s\n          );\n        }\n        default:\n          return !1;\n      }\n    }\n    function gp() {\n      return po(I.state.contextualKeyword, !0);\n    }\n    function WT(e) {\n      switch (e) {\n        case oe.ContextualKeyword._declare: {\n          let t = I.state.tokens.length - 1;\n          if (HT()) return (I.state.tokens[t].type = k.TokenType._declare), !0;\n          break;\n        }\n        case oe.ContextualKeyword._global:\n          if (v.match.call(void 0, k.TokenType.braceL)) return Tl(), !0;\n          break;\n        default:\n          return po(e, !1);\n      }\n      return !1;\n    }\n    function po(e, t) {\n      switch (e) {\n        case oe.ContextualKeyword._abstract:\n          if (fi(t) && v.match.call(void 0, k.TokenType._class))\n            return (\n              (I.state.tokens[I.state.tokens.length - 1].type =\n                k.TokenType._abstract),\n              Rn.parseClass.call(void 0, !0, !1),\n              !0\n            );\n          break;\n        case oe.ContextualKeyword._enum:\n          if (fi(t) && v.match.call(void 0, k.TokenType.name))\n            return (\n              (I.state.tokens[I.state.tokens.length - 1].type =\n                k.TokenType._enum),\n              yl(),\n              !0\n            );\n          break;\n        case oe.ContextualKeyword._interface:\n          if (fi(t) && v.match.call(void 0, k.TokenType.name)) {\n            let s = v.pushTypeContext.call(void 0, t ? 2 : 1);\n            return VT(), v.popTypeContext.call(void 0, s), !0;\n          }\n          break;\n        case oe.ContextualKeyword._module:\n          if (fi(t)) {\n            if (v.match.call(void 0, k.TokenType.string)) {\n              let s = v.pushTypeContext.call(void 0, t ? 2 : 1);\n              return Ep(), v.popTypeContext.call(void 0, s), !0;\n            } else if (v.match.call(void 0, k.TokenType.name)) {\n              let s = v.pushTypeContext.call(void 0, t ? 2 : 1);\n              return fl(), v.popTypeContext.call(void 0, s), !0;\n            }\n          }\n          break;\n        case oe.ContextualKeyword._namespace:\n          if (fi(t) && v.match.call(void 0, k.TokenType.name)) {\n            let s = v.pushTypeContext.call(void 0, t ? 2 : 1);\n            return fl(), v.popTypeContext.call(void 0, s), !0;\n          }\n          break;\n        case oe.ContextualKeyword._type:\n          if (fi(t) && v.match.call(void 0, k.TokenType.name)) {\n            let s = v.pushTypeContext.call(void 0, t ? 2 : 1);\n            return jT(), v.popTypeContext.call(void 0, s), !0;\n          }\n          break;\n        default:\n          break;\n      }\n      return !1;\n    }\n    function fi(e) {\n      return e ? (v.next.call(void 0), !0) : !H.isLineTerminator.call(void 0);\n    }\n    function GT() {\n      let e = I.state.snapshot();\n      return (\n        uo(),\n        Rn.parseFunctionParams.call(void 0),\n        RT(),\n        H.expect.call(void 0, k.TokenType.arrow),\n        I.state.error\n          ? (I.state.restoreFromSnapshot(e), !1)\n          : (_e.parseFunctionBody.call(void 0, !0), !0)\n      );\n    }\n    function kl() {\n      I.state.type === k.TokenType.bitShiftL &&\n        ((I.state.pos -= 1), v.finishToken.call(void 0, k.TokenType.lessThan)),\n        yi();\n    }\n    function yi() {\n      let e = v.pushTypeContext.call(void 0, 0);\n      for (\n        H.expect.call(void 0, k.TokenType.lessThan);\n        !v.eat.call(void 0, k.TokenType.greaterThan) && !I.state.error;\n\n      )\n        rt(), v.eat.call(void 0, k.TokenType.comma);\n      v.popTypeContext.call(void 0, e);\n    }\n    function zT() {\n      if (v.match.call(void 0, k.TokenType.name))\n        switch (I.state.contextualKeyword) {\n          case oe.ContextualKeyword._abstract:\n          case oe.ContextualKeyword._declare:\n          case oe.ContextualKeyword._enum:\n          case oe.ContextualKeyword._interface:\n          case oe.ContextualKeyword._module:\n          case oe.ContextualKeyword._namespace:\n          case oe.ContextualKeyword._type:\n            return !0;\n          default:\n            break;\n        }\n      return !1;\n    }\n    Oe.tsIsDeclarationStart = zT;\n    function XT(e, t) {\n      if (\n        (v.match.call(void 0, k.TokenType.colon) && Qi(k.TokenType.colon),\n        !v.match.call(void 0, k.TokenType.braceL) &&\n          H.isLineTerminator.call(void 0))\n      ) {\n        let s = I.state.tokens.length - 1;\n        for (\n          ;\n          s >= 0 &&\n          (I.state.tokens[s].start >= e ||\n            I.state.tokens[s].type === k.TokenType._default ||\n            I.state.tokens[s].type === k.TokenType._export);\n\n        )\n          (I.state.tokens[s].isType = !0), s--;\n        return;\n      }\n      _e.parseFunctionBody.call(void 0, !1, t);\n    }\n    Oe.tsParseFunctionBodyAndFinish = XT;\n    function YT(e, t, s) {\n      if (\n        !H.hasPrecedingLineBreak.call(void 0) &&\n        v.eat.call(void 0, k.TokenType.bang)\n      ) {\n        I.state.tokens[I.state.tokens.length - 1].type =\n          k.TokenType.nonNullAssertion;\n        return;\n      }\n      if (\n        v.match.call(void 0, k.TokenType.lessThan) ||\n        v.match.call(void 0, k.TokenType.bitShiftL)\n      ) {\n        let i = I.state.snapshot();\n        if (!t && _e.atPossibleAsync.call(void 0) && GT()) return;\n        if (\n          (kl(),\n          !t && v.eat.call(void 0, k.TokenType.parenL)\n            ? ((I.state.tokens[I.state.tokens.length - 1].subscriptStartIndex =\n                e),\n              _e.parseCallExpressionArguments.call(void 0))\n            : v.match.call(void 0, k.TokenType.backQuote)\n            ? _e.parseTemplate.call(void 0)\n            : (I.state.type === k.TokenType.greaterThan ||\n                (I.state.type !== k.TokenType.parenL &&\n                  I.state.type & k.TokenType.IS_EXPRESSION_START &&\n                  !H.hasPrecedingLineBreak.call(void 0))) &&\n              H.unexpected.call(void 0),\n          I.state.error)\n        )\n          I.state.restoreFromSnapshot(i);\n        else return;\n      } else\n        !t &&\n          v.match.call(void 0, k.TokenType.questionDot) &&\n          v.lookaheadType.call(void 0) === k.TokenType.lessThan &&\n          (v.next.call(void 0),\n          (I.state.tokens[e].isOptionalChainStart = !0),\n          (I.state.tokens[I.state.tokens.length - 1].subscriptStartIndex = e),\n          yi(),\n          H.expect.call(void 0, k.TokenType.parenL),\n          _e.parseCallExpressionArguments.call(void 0));\n      _e.baseParseSubscript.call(void 0, e, t, s);\n    }\n    Oe.tsParseSubscript = YT;\n    function JT() {\n      if (v.eat.call(void 0, k.TokenType._import))\n        return (\n          H.isContextual.call(void 0, oe.ContextualKeyword._type) &&\n            v.lookaheadType.call(void 0) !== k.TokenType.eq &&\n            H.expectContextual.call(void 0, oe.ContextualKeyword._type),\n          Ap(),\n          !0\n        );\n      if (v.eat.call(void 0, k.TokenType.eq))\n        return _e.parseExpression.call(void 0), H.semicolon.call(void 0), !0;\n      if (H.eatContextual.call(void 0, oe.ContextualKeyword._as))\n        return (\n          H.expectContextual.call(void 0, oe.ContextualKeyword._namespace),\n          _e.parseIdentifier.call(void 0),\n          H.semicolon.call(void 0),\n          !0\n        );\n      if (H.isContextual.call(void 0, oe.ContextualKeyword._type)) {\n        let e = v.lookaheadType.call(void 0);\n        (e === k.TokenType.braceL || e === k.TokenType.star) &&\n          v.next.call(void 0);\n      }\n      return !1;\n    }\n    Oe.tsTryParseExport = JT;\n    function QT() {\n      if (\n        (_e.parseIdentifier.call(void 0),\n        v.match.call(void 0, k.TokenType.comma) ||\n          v.match.call(void 0, k.TokenType.braceR))\n      ) {\n        I.state.tokens[I.state.tokens.length - 1].identifierRole =\n          v.IdentifierRole.ImportDeclaration;\n        return;\n      }\n      if (\n        (_e.parseIdentifier.call(void 0),\n        v.match.call(void 0, k.TokenType.comma) ||\n          v.match.call(void 0, k.TokenType.braceR))\n      ) {\n        (I.state.tokens[I.state.tokens.length - 1].identifierRole =\n          v.IdentifierRole.ImportDeclaration),\n          (I.state.tokens[I.state.tokens.length - 2].isType = !0),\n          (I.state.tokens[I.state.tokens.length - 1].isType = !0);\n        return;\n      }\n      if (\n        (_e.parseIdentifier.call(void 0),\n        v.match.call(void 0, k.TokenType.comma) ||\n          v.match.call(void 0, k.TokenType.braceR))\n      ) {\n        (I.state.tokens[I.state.tokens.length - 3].identifierRole =\n          v.IdentifierRole.ImportAccess),\n          (I.state.tokens[I.state.tokens.length - 1].identifierRole =\n            v.IdentifierRole.ImportDeclaration);\n        return;\n      }\n      _e.parseIdentifier.call(void 0),\n        (I.state.tokens[I.state.tokens.length - 3].identifierRole =\n          v.IdentifierRole.ImportAccess),\n        (I.state.tokens[I.state.tokens.length - 1].identifierRole =\n          v.IdentifierRole.ImportDeclaration),\n        (I.state.tokens[I.state.tokens.length - 4].isType = !0),\n        (I.state.tokens[I.state.tokens.length - 3].isType = !0),\n        (I.state.tokens[I.state.tokens.length - 2].isType = !0),\n        (I.state.tokens[I.state.tokens.length - 1].isType = !0);\n    }\n    Oe.tsParseImportSpecifier = QT;\n    function ZT() {\n      if (\n        (_e.parseIdentifier.call(void 0),\n        v.match.call(void 0, k.TokenType.comma) ||\n          v.match.call(void 0, k.TokenType.braceR))\n      ) {\n        I.state.tokens[I.state.tokens.length - 1].identifierRole =\n          v.IdentifierRole.ExportAccess;\n        return;\n      }\n      if (\n        (_e.parseIdentifier.call(void 0),\n        v.match.call(void 0, k.TokenType.comma) ||\n          v.match.call(void 0, k.TokenType.braceR))\n      ) {\n        (I.state.tokens[I.state.tokens.length - 1].identifierRole =\n          v.IdentifierRole.ExportAccess),\n          (I.state.tokens[I.state.tokens.length - 2].isType = !0),\n          (I.state.tokens[I.state.tokens.length - 1].isType = !0);\n        return;\n      }\n      if (\n        (_e.parseIdentifier.call(void 0),\n        v.match.call(void 0, k.TokenType.comma) ||\n          v.match.call(void 0, k.TokenType.braceR))\n      ) {\n        I.state.tokens[I.state.tokens.length - 3].identifierRole =\n          v.IdentifierRole.ExportAccess;\n        return;\n      }\n      _e.parseIdentifier.call(void 0),\n        (I.state.tokens[I.state.tokens.length - 3].identifierRole =\n          v.IdentifierRole.ExportAccess),\n        (I.state.tokens[I.state.tokens.length - 4].isType = !0),\n        (I.state.tokens[I.state.tokens.length - 3].isType = !0),\n        (I.state.tokens[I.state.tokens.length - 2].isType = !0),\n        (I.state.tokens[I.state.tokens.length - 1].isType = !0);\n    }\n    Oe.tsParseExportSpecifier = ZT;\n    function ek() {\n      if (\n        H.isContextual.call(void 0, oe.ContextualKeyword._abstract) &&\n        v.lookaheadType.call(void 0) === k.TokenType._class\n      )\n        return (\n          (I.state.type = k.TokenType._abstract),\n          v.next.call(void 0),\n          Rn.parseClass.call(void 0, !0, !0),\n          !0\n        );\n      if (H.isContextual.call(void 0, oe.ContextualKeyword._interface)) {\n        let e = v.pushTypeContext.call(void 0, 2);\n        return (\n          po(oe.ContextualKeyword._interface, !0),\n          v.popTypeContext.call(void 0, e),\n          !0\n        );\n      }\n      return !1;\n    }\n    Oe.tsTryParseExportDefaultExpression = ek;\n    function tk() {\n      if (I.state.type === k.TokenType._const) {\n        let e = v.lookaheadTypeAndKeyword.call(void 0);\n        if (\n          e.type === k.TokenType.name &&\n          e.contextualKeyword === oe.ContextualKeyword._enum\n        )\n          return (\n            H.expect.call(void 0, k.TokenType._const),\n            H.expectContextual.call(void 0, oe.ContextualKeyword._enum),\n            (I.state.tokens[I.state.tokens.length - 1].type =\n              k.TokenType._enum),\n            yl(),\n            !0\n          );\n      }\n      return !1;\n    }\n    Oe.tsTryParseStatementContent = tk;\n    function nk(e) {\n      let t = I.state.tokens.length;\n      bp([\n        oe.ContextualKeyword._abstract,\n        oe.ContextualKeyword._readonly,\n        oe.ContextualKeyword._declare,\n        oe.ContextualKeyword._static,\n        oe.ContextualKeyword._override,\n      ]);\n      let s = I.state.tokens.length;\n      if (wp()) {\n        let r = e ? t - 1 : t;\n        for (let a = r; a < s; a++) I.state.tokens[a].isType = !0;\n        return !0;\n      }\n      return !1;\n    }\n    Oe.tsTryParseClassMemberWithIsStatic = nk;\n    function sk(e) {\n      WT(e) || H.semicolon.call(void 0);\n    }\n    Oe.tsParseIdentifierStatement = sk;\n    function ik() {\n      let e = H.eatContextual.call(void 0, oe.ContextualKeyword._declare);\n      e &&\n        (I.state.tokens[I.state.tokens.length - 1].type = k.TokenType._declare);\n      let t = !1;\n      if (v.match.call(void 0, k.TokenType.name))\n        if (e) {\n          let s = v.pushTypeContext.call(void 0, 2);\n          (t = gp()), v.popTypeContext.call(void 0, s);\n        } else t = gp();\n      if (!t)\n        if (e) {\n          let s = v.pushTypeContext.call(void 0, 2);\n          Rn.parseStatement.call(void 0, !0), v.popTypeContext.call(void 0, s);\n        } else Rn.parseStatement.call(void 0, !0);\n    }\n    Oe.tsParseExportDeclaration = ik;\n    function rk(e) {\n      if (\n        (e &&\n          (v.match.call(void 0, k.TokenType.lessThan) ||\n            v.match.call(void 0, k.TokenType.bitShiftL)) &&\n          kl(),\n        H.eatContextual.call(void 0, oe.ContextualKeyword._implements))\n      ) {\n        I.state.tokens[I.state.tokens.length - 1].type =\n          k.TokenType._implements;\n        let t = v.pushTypeContext.call(void 0, 1);\n        Ip(), v.popTypeContext.call(void 0, t);\n      }\n    }\n    Oe.tsAfterParseClassSuper = rk;\n    function ok() {\n      mi();\n    }\n    Oe.tsStartParseObjPropValue = ok;\n    function ak() {\n      mi();\n    }\n    Oe.tsStartParseFunctionParams = ak;\n    function lk() {\n      let e = v.pushTypeContext.call(void 0, 0);\n      H.hasPrecedingLineBreak.call(void 0) ||\n        v.eat.call(void 0, k.TokenType.bang),\n        er(),\n        v.popTypeContext.call(void 0, e);\n    }\n    Oe.tsAfterParseVarHead = lk;\n    function ck() {\n      v.match.call(void 0, k.TokenType.colon) && tr();\n    }\n    Oe.tsStartParseAsyncArrowFromCallExpression = ck;\n    function uk(e, t) {\n      return I.isJSXEnabled ? Pp(e, t) : Np(e, t);\n    }\n    Oe.tsParseMaybeAssign = uk;\n    function Pp(e, t) {\n      if (!v.match.call(void 0, k.TokenType.lessThan))\n        return _e.baseParseMaybeAssign.call(void 0, e, t);\n      let s = I.state.snapshot(),\n        i = _e.baseParseMaybeAssign.call(void 0, e, t);\n      if (I.state.error) I.state.restoreFromSnapshot(s);\n      else return i;\n      return (\n        (I.state.type = k.TokenType.typeParameterStart),\n        uo(),\n        (i = _e.baseParseMaybeAssign.call(void 0, e, t)),\n        i || H.unexpected.call(void 0),\n        i\n      );\n    }\n    Oe.tsParseMaybeAssignWithJSX = Pp;\n    function Np(e, t) {\n      if (!v.match.call(void 0, k.TokenType.lessThan))\n        return _e.baseParseMaybeAssign.call(void 0, e, t);\n      let s = I.state.snapshot();\n      uo();\n      let i = _e.baseParseMaybeAssign.call(void 0, e, t);\n      if ((i || H.unexpected.call(void 0), I.state.error))\n        I.state.restoreFromSnapshot(s);\n      else return i;\n      return _e.baseParseMaybeAssign.call(void 0, e, t);\n    }\n    Oe.tsParseMaybeAssignWithoutJSX = Np;\n    function pk() {\n      if (v.match.call(void 0, k.TokenType.colon)) {\n        let e = I.state.snapshot();\n        Qi(k.TokenType.colon),\n          H.canInsertSemicolon.call(void 0) && H.unexpected.call(void 0),\n          v.match.call(void 0, k.TokenType.arrow) || H.unexpected.call(void 0),\n          I.state.error && I.state.restoreFromSnapshot(e);\n      }\n      return v.eat.call(void 0, k.TokenType.arrow);\n    }\n    Oe.tsParseArrow = pk;\n    function hk() {\n      let e = v.pushTypeContext.call(void 0, 0);\n      v.eat.call(void 0, k.TokenType.question),\n        er(),\n        v.popTypeContext.call(void 0, e);\n    }\n    Oe.tsParseAssignableListItemTypes = hk;\n    function fk() {\n      (v.match.call(void 0, k.TokenType.lessThan) ||\n        v.match.call(void 0, k.TokenType.bitShiftL)) &&\n        kl(),\n        Rn.baseParseMaybeDecoratorArguments.call(void 0);\n    }\n    Oe.tsParseMaybeDecoratorArguments = fk;\n  });\n  var vl = Z((fo) => {\n    'use strict';\n    Object.defineProperty(fo, '__esModule', {value: !0});\n    var Se = xt(),\n      Me = be(),\n      fe = Zt(),\n      ho = Ns(),\n      fs = cs(),\n      at = Qt(),\n      Rp = li(),\n      dk = hi();\n    function mk() {\n      let e = !1,\n        t = !1;\n      for (;;) {\n        if (fe.state.pos >= fe.input.length) {\n          fs.unexpected.call(void 0, 'Unterminated JSX contents');\n          return;\n        }\n        let s = fe.input.charCodeAt(fe.state.pos);\n        if (s === at.charCodes.lessThan || s === at.charCodes.leftCurlyBrace) {\n          if (fe.state.pos === fe.state.start) {\n            if (s === at.charCodes.lessThan) {\n              fe.state.pos++,\n                Se.finishToken.call(void 0, Me.TokenType.jsxTagStart);\n              return;\n            }\n            Se.getTokenFromCode.call(void 0, s);\n            return;\n          }\n          e && !t\n            ? Se.finishToken.call(void 0, Me.TokenType.jsxEmptyText)\n            : Se.finishToken.call(void 0, Me.TokenType.jsxText);\n          return;\n        }\n        s === at.charCodes.lineFeed\n          ? (e = !0)\n          : s !== at.charCodes.space &&\n            s !== at.charCodes.carriageReturn &&\n            s !== at.charCodes.tab &&\n            (t = !0),\n          fe.state.pos++;\n      }\n    }\n    function yk(e) {\n      for (fe.state.pos++; ; ) {\n        if (fe.state.pos >= fe.input.length) {\n          fs.unexpected.call(void 0, 'Unterminated string constant');\n          return;\n        }\n        if (fe.input.charCodeAt(fe.state.pos) === e) {\n          fe.state.pos++;\n          break;\n        }\n        fe.state.pos++;\n      }\n      Se.finishToken.call(void 0, Me.TokenType.string);\n    }\n    function Tk() {\n      let e;\n      do {\n        if (fe.state.pos > fe.input.length) {\n          fs.unexpected.call(void 0, 'Unexpectedly reached the end of input.');\n          return;\n        }\n        e = fe.input.charCodeAt(++fe.state.pos);\n      } while (Rp.IS_IDENTIFIER_CHAR[e] || e === at.charCodes.dash);\n      Se.finishToken.call(void 0, Me.TokenType.jsxName);\n    }\n    function xl() {\n      dn();\n    }\n    function Lp(e) {\n      if ((xl(), !Se.eat.call(void 0, Me.TokenType.colon))) {\n        fe.state.tokens[fe.state.tokens.length - 1].identifierRole = e;\n        return;\n      }\n      xl();\n    }\n    function Op() {\n      let e = fe.state.tokens.length;\n      Lp(Se.IdentifierRole.Access);\n      let t = !1;\n      for (; Se.match.call(void 0, Me.TokenType.dot); ) (t = !0), dn(), xl();\n      if (!t) {\n        let s = fe.state.tokens[e],\n          i = fe.input.charCodeAt(s.start);\n        i >= at.charCodes.lowercaseA &&\n          i <= at.charCodes.lowercaseZ &&\n          (s.identifierRole = null);\n      }\n    }\n    function kk() {\n      switch (fe.state.type) {\n        case Me.TokenType.braceL:\n          Se.next.call(void 0), ho.parseExpression.call(void 0), dn();\n          return;\n        case Me.TokenType.jsxTagStart:\n          Mp(), dn();\n          return;\n        case Me.TokenType.string:\n          dn();\n          return;\n        default:\n          fs.unexpected.call(\n            void 0,\n            'JSX value should be either an expression or a quoted JSX text'\n          );\n      }\n    }\n    function vk() {\n      fs.expect.call(void 0, Me.TokenType.ellipsis),\n        ho.parseExpression.call(void 0);\n    }\n    function xk(e) {\n      if (Se.match.call(void 0, Me.TokenType.jsxTagEnd)) return !1;\n      Op(), fe.isTypeScriptEnabled && dk.tsTryParseJSXTypeArgument.call(void 0);\n      let t = !1;\n      for (\n        ;\n        !Se.match.call(void 0, Me.TokenType.slash) &&\n        !Se.match.call(void 0, Me.TokenType.jsxTagEnd) &&\n        !fe.state.error;\n\n      ) {\n        if (Se.eat.call(void 0, Me.TokenType.braceL)) {\n          (t = !0),\n            fs.expect.call(void 0, Me.TokenType.ellipsis),\n            ho.parseMaybeAssign.call(void 0),\n            dn();\n          continue;\n        }\n        t &&\n          fe.state.end - fe.state.start === 3 &&\n          fe.input.charCodeAt(fe.state.start) === at.charCodes.lowercaseK &&\n          fe.input.charCodeAt(fe.state.start + 1) === at.charCodes.lowercaseE &&\n          fe.input.charCodeAt(fe.state.start + 2) === at.charCodes.lowercaseY &&\n          (fe.state.tokens[e].jsxRole = Se.JSXRole.KeyAfterPropSpread),\n          Lp(Se.IdentifierRole.ObjectKey),\n          Se.match.call(void 0, Me.TokenType.eq) && (dn(), kk());\n      }\n      let s = Se.match.call(void 0, Me.TokenType.slash);\n      return s && dn(), s;\n    }\n    function gk() {\n      Se.match.call(void 0, Me.TokenType.jsxTagEnd) || Op();\n    }\n    function Dp() {\n      let e = fe.state.tokens.length - 1;\n      fe.state.tokens[e].jsxRole = Se.JSXRole.NoChildren;\n      let t = 0;\n      if (!xk(e))\n        for (Ti(); ; )\n          switch (fe.state.type) {\n            case Me.TokenType.jsxTagStart:\n              if ((dn(), Se.match.call(void 0, Me.TokenType.slash))) {\n                dn(),\n                  gk(),\n                  fe.state.tokens[e].jsxRole !==\n                    Se.JSXRole.KeyAfterPropSpread &&\n                    (t === 1\n                      ? (fe.state.tokens[e].jsxRole = Se.JSXRole.OneChild)\n                      : t > 1 &&\n                        (fe.state.tokens[e].jsxRole =\n                          Se.JSXRole.StaticChildren));\n                return;\n              }\n              t++, Dp(), Ti();\n              break;\n            case Me.TokenType.jsxText:\n              t++, Ti();\n              break;\n            case Me.TokenType.jsxEmptyText:\n              Ti();\n              break;\n            case Me.TokenType.braceL:\n              Se.next.call(void 0),\n                Se.match.call(void 0, Me.TokenType.ellipsis)\n                  ? (vk(), Ti(), (t += 2))\n                  : (Se.match.call(void 0, Me.TokenType.braceR) ||\n                      (t++, ho.parseExpression.call(void 0)),\n                    Ti());\n              break;\n            default:\n              fs.unexpected.call(void 0);\n              return;\n          }\n    }\n    function Mp() {\n      dn(), Dp();\n    }\n    fo.jsxParseElement = Mp;\n    function dn() {\n      fe.state.tokens.push(new Se.Token()),\n        Se.skipSpace.call(void 0),\n        (fe.state.start = fe.state.pos);\n      let e = fe.input.charCodeAt(fe.state.pos);\n      if (Rp.IS_IDENTIFIER_START[e]) Tk();\n      else if (\n        e === at.charCodes.quotationMark ||\n        e === at.charCodes.apostrophe\n      )\n        yk(e);\n      else\n        switch ((++fe.state.pos, e)) {\n          case at.charCodes.greaterThan:\n            Se.finishToken.call(void 0, Me.TokenType.jsxTagEnd);\n            break;\n          case at.charCodes.lessThan:\n            Se.finishToken.call(void 0, Me.TokenType.jsxTagStart);\n            break;\n          case at.charCodes.slash:\n            Se.finishToken.call(void 0, Me.TokenType.slash);\n            break;\n          case at.charCodes.equalsTo:\n            Se.finishToken.call(void 0, Me.TokenType.eq);\n            break;\n          case at.charCodes.leftCurlyBrace:\n            Se.finishToken.call(void 0, Me.TokenType.braceL);\n            break;\n          case at.charCodes.dot:\n            Se.finishToken.call(void 0, Me.TokenType.dot);\n            break;\n          case at.charCodes.colon:\n            Se.finishToken.call(void 0, Me.TokenType.colon);\n            break;\n          default:\n            fs.unexpected.call(void 0);\n        }\n    }\n    fo.nextJSXTagToken = dn;\n    function Ti() {\n      fe.state.tokens.push(new Se.Token()),\n        (fe.state.start = fe.state.pos),\n        mk();\n    }\n  });\n  var Bp = Z((yo) => {\n    'use strict';\n    Object.defineProperty(yo, '__esModule', {value: !0});\n    var mo = xt(),\n      ki = be(),\n      Fp = Zt(),\n      _k = Ns(),\n      bk = Ji(),\n      Ck = hi();\n    function wk(e) {\n      if (mo.match.call(void 0, ki.TokenType.question)) {\n        let t = mo.lookaheadType.call(void 0);\n        if (\n          t === ki.TokenType.colon ||\n          t === ki.TokenType.comma ||\n          t === ki.TokenType.parenR\n        )\n          return;\n      }\n      _k.baseParseConditional.call(void 0, e);\n    }\n    yo.typedParseConditional = wk;\n    function Sk() {\n      mo.eatTypeToken.call(void 0, ki.TokenType.question),\n        mo.match.call(void 0, ki.TokenType.colon) &&\n          (Fp.isTypeScriptEnabled\n            ? Ck.tsParseTypeAnnotation.call(void 0)\n            : Fp.isFlowEnabled && bk.flowParseTypeAnnotation.call(void 0));\n    }\n    yo.typedParseParenItem = Sk;\n  });\n  var Ns = Z((et) => {\n    'use strict';\n    Object.defineProperty(et, '__esModule', {value: !0});\n    var Yn = Ji(),\n      Ik = vl(),\n      Vp = Bp(),\n      ms = hi(),\n      K = xt(),\n      zn = It(),\n      jp = qr(),\n      B = be(),\n      $p = Qt(),\n      Ek = li(),\n      j = Zt(),\n      ds = lo(),\n      gn = nr(),\n      Pe = cs(),\n      vo = class {\n        constructor(t) {\n          this.stop = t;\n        }\n      };\n    et.StopState = vo;\n    function sr(e = !1) {\n      if ((mn(e), K.match.call(void 0, B.TokenType.comma)))\n        for (; K.eat.call(void 0, B.TokenType.comma); ) mn(e);\n    }\n    et.parseExpression = sr;\n    function mn(e = !1, t = !1) {\n      return j.isTypeScriptEnabled\n        ? ms.tsParseMaybeAssign.call(void 0, e, t)\n        : j.isFlowEnabled\n        ? Yn.flowParseMaybeAssign.call(void 0, e, t)\n        : qp(e, t);\n    }\n    et.parseMaybeAssign = mn;\n    function qp(e, t) {\n      if (K.match.call(void 0, B.TokenType._yield)) return Hk(), !1;\n      (K.match.call(void 0, B.TokenType.parenL) ||\n        K.match.call(void 0, B.TokenType.name) ||\n        K.match.call(void 0, B.TokenType._yield)) &&\n        (j.state.potentialArrowAt = j.state.start);\n      let s = Ak(e);\n      return (\n        t && wl(),\n        j.state.type & B.TokenType.IS_ASSIGN\n          ? (K.next.call(void 0), mn(e), !1)\n          : s\n      );\n    }\n    et.baseParseMaybeAssign = qp;\n    function Ak(e) {\n      return Nk(e) ? !0 : (Pk(e), !1);\n    }\n    function Pk(e) {\n      j.isTypeScriptEnabled || j.isFlowEnabled\n        ? Vp.typedParseConditional.call(void 0, e)\n        : Kp(e);\n    }\n    function Kp(e) {\n      K.eat.call(void 0, B.TokenType.question) &&\n        (mn(), Pe.expect.call(void 0, B.TokenType.colon), mn(e));\n    }\n    et.baseParseConditional = Kp;\n    function Nk(e) {\n      let t = j.state.tokens.length;\n      return rr() ? !0 : (To(t, -1, e), !1);\n    }\n    function To(e, t, s) {\n      if (\n        j.isTypeScriptEnabled &&\n        (B.TokenType._in & B.TokenType.PRECEDENCE_MASK) > t &&\n        !Pe.hasPrecedingLineBreak.call(void 0) &&\n        (Pe.eatContextual.call(void 0, zn.ContextualKeyword._as) ||\n          Pe.eatContextual.call(void 0, zn.ContextualKeyword._satisfies))\n      ) {\n        let r = K.pushTypeContext.call(void 0, 1);\n        ms.tsParseType.call(void 0),\n          K.popTypeContext.call(void 0, r),\n          K.rescan_gt.call(void 0),\n          To(e, t, s);\n        return;\n      }\n      let i = j.state.type & B.TokenType.PRECEDENCE_MASK;\n      if (i > 0 && (!s || !K.match.call(void 0, B.TokenType._in)) && i > t) {\n        let r = j.state.type;\n        K.next.call(void 0),\n          r === B.TokenType.nullishCoalescing &&\n            (j.state.tokens[j.state.tokens.length - 1].nullishStartIndex = e);\n        let a = j.state.tokens.length;\n        rr(),\n          To(a, r & B.TokenType.IS_RIGHT_ASSOCIATIVE ? i - 1 : i, s),\n          r === B.TokenType.nullishCoalescing &&\n            (j.state.tokens[e].numNullishCoalesceStarts++,\n            j.state.tokens[j.state.tokens.length - 1].numNullishCoalesceEnds++),\n          To(e, t, s);\n      }\n    }\n    function rr() {\n      if (\n        j.isTypeScriptEnabled &&\n        !j.isJSXEnabled &&\n        K.eat.call(void 0, B.TokenType.lessThan)\n      )\n        return ms.tsParseTypeAssertion.call(void 0), !1;\n      if (\n        Pe.isContextual.call(void 0, zn.ContextualKeyword._module) &&\n        K.lookaheadCharCode.call(void 0) === $p.charCodes.leftCurlyBrace &&\n        !Pe.hasFollowingLineBreak.call(void 0)\n      )\n        return Wk(), !1;\n      if (j.state.type & B.TokenType.IS_PREFIX)\n        return K.next.call(void 0), rr(), !1;\n      if (Up()) return !0;\n      for (\n        ;\n        j.state.type & B.TokenType.IS_POSTFIX &&\n        !Pe.canInsertSemicolon.call(void 0);\n\n      )\n        j.state.type === B.TokenType.preIncDec &&\n          (j.state.type = B.TokenType.postIncDec),\n          K.next.call(void 0);\n      return !1;\n    }\n    et.parseMaybeUnary = rr;\n    function Up() {\n      let e = j.state.tokens.length;\n      return _o()\n        ? !0\n        : (bl(e),\n          j.state.tokens.length > e &&\n            j.state.tokens[e].isOptionalChainStart &&\n            (j.state.tokens[j.state.tokens.length - 1].isOptionalChainEnd = !0),\n          !1);\n    }\n    et.parseExprSubscripts = Up;\n    function bl(e, t = !1) {\n      j.isFlowEnabled ? Yn.flowParseSubscripts.call(void 0, e, t) : Hp(e, t);\n    }\n    function Hp(e, t = !1) {\n      let s = new vo(!1);\n      do Rk(e, t, s);\n      while (!s.stop && !j.state.error);\n    }\n    et.baseParseSubscripts = Hp;\n    function Rk(e, t, s) {\n      j.isTypeScriptEnabled\n        ? ms.tsParseSubscript.call(void 0, e, t, s)\n        : j.isFlowEnabled\n        ? Yn.flowParseSubscript.call(void 0, e, t, s)\n        : Wp(e, t, s);\n    }\n    function Wp(e, t, s) {\n      if (!t && K.eat.call(void 0, B.TokenType.doubleColon))\n        Cl(), (s.stop = !0), bl(e, t);\n      else if (K.match.call(void 0, B.TokenType.questionDot)) {\n        if (\n          ((j.state.tokens[e].isOptionalChainStart = !0),\n          t && K.lookaheadType.call(void 0) === B.TokenType.parenL)\n        ) {\n          s.stop = !0;\n          return;\n        }\n        K.next.call(void 0),\n          (j.state.tokens[j.state.tokens.length - 1].subscriptStartIndex = e),\n          K.eat.call(void 0, B.TokenType.bracketL)\n            ? (sr(), Pe.expect.call(void 0, B.TokenType.bracketR))\n            : K.eat.call(void 0, B.TokenType.parenL)\n            ? ko()\n            : xo();\n      } else if (K.eat.call(void 0, B.TokenType.dot))\n        (j.state.tokens[j.state.tokens.length - 1].subscriptStartIndex = e),\n          xo();\n      else if (K.eat.call(void 0, B.TokenType.bracketL))\n        (j.state.tokens[j.state.tokens.length - 1].subscriptStartIndex = e),\n          sr(),\n          Pe.expect.call(void 0, B.TokenType.bracketR);\n      else if (!t && K.match.call(void 0, B.TokenType.parenL))\n        if (Gp()) {\n          let i = j.state.snapshot(),\n            r = j.state.tokens.length;\n          K.next.call(void 0),\n            (j.state.tokens[j.state.tokens.length - 1].subscriptStartIndex = e);\n          let a = j.getNextContextId.call(void 0);\n          (j.state.tokens[j.state.tokens.length - 1].contextId = a),\n            ko(),\n            (j.state.tokens[j.state.tokens.length - 1].contextId = a),\n            Lk() &&\n              (j.state.restoreFromSnapshot(i),\n              (s.stop = !0),\n              j.state.scopeDepth++,\n              gn.parseFunctionParams.call(void 0),\n              Ok(r));\n        } else {\n          K.next.call(void 0),\n            (j.state.tokens[j.state.tokens.length - 1].subscriptStartIndex = e);\n          let i = j.getNextContextId.call(void 0);\n          (j.state.tokens[j.state.tokens.length - 1].contextId = i),\n            ko(),\n            (j.state.tokens[j.state.tokens.length - 1].contextId = i);\n        }\n      else K.match.call(void 0, B.TokenType.backQuote) ? Sl() : (s.stop = !0);\n    }\n    et.baseParseSubscript = Wp;\n    function Gp() {\n      return (\n        j.state.tokens[j.state.tokens.length - 1].contextualKeyword ===\n          zn.ContextualKeyword._async && !Pe.canInsertSemicolon.call(void 0)\n      );\n    }\n    et.atPossibleAsync = Gp;\n    function ko() {\n      let e = !0;\n      for (; !K.eat.call(void 0, B.TokenType.parenR) && !j.state.error; ) {\n        if (e) e = !1;\n        else if (\n          (Pe.expect.call(void 0, B.TokenType.comma),\n          K.eat.call(void 0, B.TokenType.parenR))\n        )\n          break;\n        Zp(!1);\n      }\n    }\n    et.parseCallExpressionArguments = ko;\n    function Lk() {\n      return (\n        K.match.call(void 0, B.TokenType.colon) ||\n        K.match.call(void 0, B.TokenType.arrow)\n      );\n    }\n    function Ok(e) {\n      j.isTypeScriptEnabled\n        ? ms.tsStartParseAsyncArrowFromCallExpression.call(void 0)\n        : j.isFlowEnabled &&\n          Yn.flowStartParseAsyncArrowFromCallExpression.call(void 0),\n        Pe.expect.call(void 0, B.TokenType.arrow),\n        ir(e);\n    }\n    function Cl() {\n      let e = j.state.tokens.length;\n      _o(), bl(e, !0);\n    }\n    function _o() {\n      if (K.eat.call(void 0, B.TokenType.modulo)) return Xn(), !1;\n      if (\n        K.match.call(void 0, B.TokenType.jsxText) ||\n        K.match.call(void 0, B.TokenType.jsxEmptyText)\n      )\n        return zp(), !1;\n      if (K.match.call(void 0, B.TokenType.lessThan) && j.isJSXEnabled)\n        return (\n          (j.state.type = B.TokenType.jsxTagStart),\n          Ik.jsxParseElement.call(void 0),\n          K.next.call(void 0),\n          !1\n        );\n      let e = j.state.potentialArrowAt === j.state.start;\n      switch (j.state.type) {\n        case B.TokenType.slash:\n        case B.TokenType.assign:\n          K.retokenizeSlashAsRegex.call(void 0);\n        case B.TokenType._super:\n        case B.TokenType._this:\n        case B.TokenType.regexp:\n        case B.TokenType.num:\n        case B.TokenType.bigint:\n        case B.TokenType.decimal:\n        case B.TokenType.string:\n        case B.TokenType._null:\n        case B.TokenType._true:\n        case B.TokenType._false:\n          return K.next.call(void 0), !1;\n        case B.TokenType._import:\n          return (\n            K.next.call(void 0),\n            K.match.call(void 0, B.TokenType.dot) &&\n              ((j.state.tokens[j.state.tokens.length - 1].type =\n                B.TokenType.name),\n              K.next.call(void 0),\n              Xn()),\n            !1\n          );\n        case B.TokenType.name: {\n          let t = j.state.tokens.length,\n            s = j.state.start,\n            i = j.state.contextualKeyword;\n          return (\n            Xn(),\n            i === zn.ContextualKeyword._await\n              ? (Uk(), !1)\n              : i === zn.ContextualKeyword._async &&\n                K.match.call(void 0, B.TokenType._function) &&\n                !Pe.canInsertSemicolon.call(void 0)\n              ? (K.next.call(void 0), gn.parseFunction.call(void 0, s, !1), !1)\n              : e &&\n                i === zn.ContextualKeyword._async &&\n                !Pe.canInsertSemicolon.call(void 0) &&\n                K.match.call(void 0, B.TokenType.name)\n              ? (j.state.scopeDepth++,\n                ds.parseBindingIdentifier.call(void 0, !1),\n                Pe.expect.call(void 0, B.TokenType.arrow),\n                ir(t),\n                !0)\n              : K.match.call(void 0, B.TokenType._do) &&\n                !Pe.canInsertSemicolon.call(void 0)\n              ? (K.next.call(void 0), gn.parseBlock.call(void 0), !1)\n              : e &&\n                !Pe.canInsertSemicolon.call(void 0) &&\n                K.match.call(void 0, B.TokenType.arrow)\n              ? (j.state.scopeDepth++,\n                ds.markPriorBindingIdentifier.call(void 0, !1),\n                Pe.expect.call(void 0, B.TokenType.arrow),\n                ir(t),\n                !0)\n              : ((j.state.tokens[j.state.tokens.length - 1].identifierRole =\n                  K.IdentifierRole.Access),\n                !1)\n          );\n        }\n        case B.TokenType._do:\n          return K.next.call(void 0), gn.parseBlock.call(void 0), !1;\n        case B.TokenType.parenL:\n          return Xp(e);\n        case B.TokenType.bracketL:\n          return K.next.call(void 0), Qp(B.TokenType.bracketR, !0), !1;\n        case B.TokenType.braceL:\n          return Yp(!1, !1), !1;\n        case B.TokenType._function:\n          return Dk(), !1;\n        case B.TokenType.at:\n          gn.parseDecorators.call(void 0);\n        case B.TokenType._class:\n          return gn.parseClass.call(void 0, !1), !1;\n        case B.TokenType._new:\n          return Bk(), !1;\n        case B.TokenType.backQuote:\n          return Sl(), !1;\n        case B.TokenType.doubleColon:\n          return K.next.call(void 0), Cl(), !1;\n        case B.TokenType.hash: {\n          let t = K.lookaheadCharCode.call(void 0);\n          return (\n            Ek.IS_IDENTIFIER_START[t] || t === $p.charCodes.backslash\n              ? xo()\n              : K.next.call(void 0),\n            !1\n          );\n        }\n        default:\n          return Pe.unexpected.call(void 0), !1;\n      }\n    }\n    et.parseExprAtom = _o;\n    function xo() {\n      K.eat.call(void 0, B.TokenType.hash), Xn();\n    }\n    function Dk() {\n      let e = j.state.start;\n      Xn(),\n        K.eat.call(void 0, B.TokenType.dot) && Xn(),\n        gn.parseFunction.call(void 0, e, !1);\n    }\n    function zp() {\n      K.next.call(void 0);\n    }\n    et.parseLiteral = zp;\n    function Mk() {\n      Pe.expect.call(void 0, B.TokenType.parenL),\n        sr(),\n        Pe.expect.call(void 0, B.TokenType.parenR);\n    }\n    et.parseParenExpression = Mk;\n    function Xp(e) {\n      let t = j.state.snapshot(),\n        s = j.state.tokens.length;\n      Pe.expect.call(void 0, B.TokenType.parenL);\n      let i = !0;\n      for (; !K.match.call(void 0, B.TokenType.parenR) && !j.state.error; ) {\n        if (i) i = !1;\n        else if (\n          (Pe.expect.call(void 0, B.TokenType.comma),\n          K.match.call(void 0, B.TokenType.parenR))\n        )\n          break;\n        if (K.match.call(void 0, B.TokenType.ellipsis)) {\n          ds.parseRest.call(void 0, !1), wl();\n          break;\n        } else mn(!1, !0);\n      }\n      return (\n        Pe.expect.call(void 0, B.TokenType.parenR),\n        e && Fk() && gl()\n          ? (j.state.restoreFromSnapshot(t),\n            j.state.scopeDepth++,\n            gn.parseFunctionParams.call(void 0),\n            gl(),\n            ir(s),\n            j.state.error ? (j.state.restoreFromSnapshot(t), Xp(!1), !1) : !0)\n          : !1\n      );\n    }\n    function Fk() {\n      return (\n        K.match.call(void 0, B.TokenType.colon) ||\n        !Pe.canInsertSemicolon.call(void 0)\n      );\n    }\n    function gl() {\n      return j.isTypeScriptEnabled\n        ? ms.tsParseArrow.call(void 0)\n        : j.isFlowEnabled\n        ? Yn.flowParseArrow.call(void 0)\n        : K.eat.call(void 0, B.TokenType.arrow);\n    }\n    et.parseArrow = gl;\n    function wl() {\n      (j.isTypeScriptEnabled || j.isFlowEnabled) &&\n        Vp.typedParseParenItem.call(void 0);\n    }\n    function Bk() {\n      if (\n        (Pe.expect.call(void 0, B.TokenType._new),\n        K.eat.call(void 0, B.TokenType.dot))\n      ) {\n        Xn();\n        return;\n      }\n      Vk(),\n        j.isFlowEnabled && Yn.flowStartParseNewArguments.call(void 0),\n        K.eat.call(void 0, B.TokenType.parenL) && Qp(B.TokenType.parenR);\n    }\n    function Vk() {\n      Cl(), K.eat.call(void 0, B.TokenType.questionDot);\n    }\n    function Sl() {\n      for (\n        K.nextTemplateToken.call(void 0), K.nextTemplateToken.call(void 0);\n        !K.match.call(void 0, B.TokenType.backQuote) && !j.state.error;\n\n      )\n        Pe.expect.call(void 0, B.TokenType.dollarBraceL),\n          sr(),\n          K.nextTemplateToken.call(void 0),\n          K.nextTemplateToken.call(void 0);\n      K.next.call(void 0);\n    }\n    et.parseTemplate = Sl;\n    function Yp(e, t) {\n      let s = j.getNextContextId.call(void 0),\n        i = !0;\n      for (\n        K.next.call(void 0),\n          j.state.tokens[j.state.tokens.length - 1].contextId = s;\n        !K.eat.call(void 0, B.TokenType.braceR) && !j.state.error;\n\n      ) {\n        if (i) i = !1;\n        else if (\n          (Pe.expect.call(void 0, B.TokenType.comma),\n          K.eat.call(void 0, B.TokenType.braceR))\n        )\n          break;\n        let r = !1;\n        if (K.match.call(void 0, B.TokenType.ellipsis)) {\n          let a = j.state.tokens.length;\n          if (\n            (ds.parseSpread.call(void 0),\n            e &&\n              (j.state.tokens.length === a + 2 &&\n                ds.markPriorBindingIdentifier.call(void 0, t),\n              K.eat.call(void 0, B.TokenType.braceR)))\n          )\n            break;\n          continue;\n        }\n        e || (r = K.eat.call(void 0, B.TokenType.star)),\n          !e && Pe.isContextual.call(void 0, zn.ContextualKeyword._async)\n            ? (r && Pe.unexpected.call(void 0),\n              Xn(),\n              K.match.call(void 0, B.TokenType.colon) ||\n                K.match.call(void 0, B.TokenType.parenL) ||\n                K.match.call(void 0, B.TokenType.braceR) ||\n                K.match.call(void 0, B.TokenType.eq) ||\n                K.match.call(void 0, B.TokenType.comma) ||\n                (K.match.call(void 0, B.TokenType.star) &&\n                  (K.next.call(void 0), (r = !0)),\n                go(s)))\n            : go(s),\n          Kk(e, t, s);\n      }\n      j.state.tokens[j.state.tokens.length - 1].contextId = s;\n    }\n    et.parseObj = Yp;\n    function jk(e) {\n      return (\n        !e &&\n        (K.match.call(void 0, B.TokenType.string) ||\n          K.match.call(void 0, B.TokenType.num) ||\n          K.match.call(void 0, B.TokenType.bracketL) ||\n          K.match.call(void 0, B.TokenType.name) ||\n          !!(j.state.type & B.TokenType.IS_KEYWORD))\n      );\n    }\n    function $k(e, t) {\n      let s = j.state.start;\n      return K.match.call(void 0, B.TokenType.parenL)\n        ? (e && Pe.unexpected.call(void 0), _l(s, !1), !0)\n        : jk(e)\n        ? (go(t), _l(s, !1), !0)\n        : !1;\n    }\n    function qk(e, t) {\n      if (K.eat.call(void 0, B.TokenType.colon)) {\n        e ? ds.parseMaybeDefault.call(void 0, t) : mn(!1);\n        return;\n      }\n      let s;\n      e\n        ? j.state.scopeDepth === 0\n          ? (s = K.IdentifierRole.ObjectShorthandTopLevelDeclaration)\n          : t\n          ? (s = K.IdentifierRole.ObjectShorthandBlockScopedDeclaration)\n          : (s = K.IdentifierRole.ObjectShorthandFunctionScopedDeclaration)\n        : (s = K.IdentifierRole.ObjectShorthand),\n        (j.state.tokens[j.state.tokens.length - 1].identifierRole = s),\n        ds.parseMaybeDefault.call(void 0, t, !0);\n    }\n    function Kk(e, t, s) {\n      j.isTypeScriptEnabled\n        ? ms.tsStartParseObjPropValue.call(void 0)\n        : j.isFlowEnabled && Yn.flowStartParseObjPropValue.call(void 0),\n        $k(e, s) || qk(e, t);\n    }\n    function go(e) {\n      j.isFlowEnabled && Yn.flowParseVariance.call(void 0),\n        K.eat.call(void 0, B.TokenType.bracketL)\n          ? ((j.state.tokens[j.state.tokens.length - 1].contextId = e),\n            mn(),\n            Pe.expect.call(void 0, B.TokenType.bracketR),\n            (j.state.tokens[j.state.tokens.length - 1].contextId = e))\n          : (K.match.call(void 0, B.TokenType.num) ||\n            K.match.call(void 0, B.TokenType.string) ||\n            K.match.call(void 0, B.TokenType.bigint) ||\n            K.match.call(void 0, B.TokenType.decimal)\n              ? _o()\n              : xo(),\n            (j.state.tokens[j.state.tokens.length - 1].identifierRole =\n              K.IdentifierRole.ObjectKey),\n            (j.state.tokens[j.state.tokens.length - 1].contextId = e));\n    }\n    et.parsePropertyName = go;\n    function _l(e, t) {\n      let s = j.getNextContextId.call(void 0);\n      j.state.scopeDepth++;\n      let i = j.state.tokens.length,\n        r = t;\n      gn.parseFunctionParams.call(void 0, r, s), Jp(e, s);\n      let a = j.state.tokens.length;\n      j.state.scopes.push(new jp.Scope(i, a, !0)), j.state.scopeDepth--;\n    }\n    et.parseMethod = _l;\n    function ir(e) {\n      Il(!0);\n      let t = j.state.tokens.length;\n      j.state.scopes.push(new jp.Scope(e, t, !0)), j.state.scopeDepth--;\n    }\n    et.parseArrowExpression = ir;\n    function Jp(e, t = 0) {\n      j.isTypeScriptEnabled\n        ? ms.tsParseFunctionBodyAndFinish.call(void 0, e, t)\n        : j.isFlowEnabled\n        ? Yn.flowParseFunctionBodyAndFinish.call(void 0, t)\n        : Il(!1, t);\n    }\n    et.parseFunctionBodyAndFinish = Jp;\n    function Il(e, t = 0) {\n      e && !K.match.call(void 0, B.TokenType.braceL)\n        ? mn()\n        : gn.parseBlock.call(void 0, !0, t);\n    }\n    et.parseFunctionBody = Il;\n    function Qp(e, t = !1) {\n      let s = !0;\n      for (; !K.eat.call(void 0, e) && !j.state.error; ) {\n        if (s) s = !1;\n        else if (\n          (Pe.expect.call(void 0, B.TokenType.comma), K.eat.call(void 0, e))\n        )\n          break;\n        Zp(t);\n      }\n    }\n    function Zp(e) {\n      (e && K.match.call(void 0, B.TokenType.comma)) ||\n        (K.match.call(void 0, B.TokenType.ellipsis)\n          ? (ds.parseSpread.call(void 0), wl())\n          : K.match.call(void 0, B.TokenType.question)\n          ? K.next.call(void 0)\n          : mn(!1, !0));\n    }\n    function Xn() {\n      K.next.call(void 0),\n        (j.state.tokens[j.state.tokens.length - 1].type = B.TokenType.name);\n    }\n    et.parseIdentifier = Xn;\n    function Uk() {\n      rr();\n    }\n    function Hk() {\n      K.next.call(void 0),\n        !K.match.call(void 0, B.TokenType.semi) &&\n          !Pe.canInsertSemicolon.call(void 0) &&\n          (K.eat.call(void 0, B.TokenType.star), mn());\n    }\n    function Wk() {\n      Pe.expectContextual.call(void 0, zn.ContextualKeyword._module),\n        Pe.expect.call(void 0, B.TokenType.braceL),\n        gn.parseBlockBody.call(void 0, B.TokenType.braceR);\n    }\n  });\n  var Ji = Z((Je) => {\n    'use strict';\n    Object.defineProperty(Je, '__esModule', {value: !0});\n    var C = xt(),\n      ye = It(),\n      _ = be(),\n      ue = Zt(),\n      je = Ns(),\n      ys = nr(),\n      z = cs();\n    function Gk(e) {\n      return (\n        (e.type === _.TokenType.name || !!(e.type & _.TokenType.IS_KEYWORD)) &&\n        e.contextualKeyword !== ye.ContextualKeyword._from\n      );\n    }\n    function Ln(e) {\n      let t = C.pushTypeContext.call(void 0, 0);\n      z.expect.call(void 0, e || _.TokenType.colon),\n        Wt(),\n        C.popTypeContext.call(void 0, t);\n    }\n    function eh() {\n      z.expect.call(void 0, _.TokenType.modulo),\n        z.expectContextual.call(void 0, ye.ContextualKeyword._checks),\n        C.eat.call(void 0, _.TokenType.parenL) &&\n          (je.parseExpression.call(void 0),\n          z.expect.call(void 0, _.TokenType.parenR));\n    }\n    function Pl() {\n      let e = C.pushTypeContext.call(void 0, 0);\n      z.expect.call(void 0, _.TokenType.colon),\n        C.match.call(void 0, _.TokenType.modulo)\n          ? eh()\n          : (Wt(), C.match.call(void 0, _.TokenType.modulo) && eh()),\n        C.popTypeContext.call(void 0, e);\n    }\n    function zk() {\n      C.next.call(void 0), Nl(!0);\n    }\n    function Xk() {\n      C.next.call(void 0),\n        je.parseIdentifier.call(void 0),\n        C.match.call(void 0, _.TokenType.lessThan) && On(),\n        z.expect.call(void 0, _.TokenType.parenL),\n        Al(),\n        z.expect.call(void 0, _.TokenType.parenR),\n        Pl(),\n        z.semicolon.call(void 0);\n    }\n    function El() {\n      C.match.call(void 0, _.TokenType._class)\n        ? zk()\n        : C.match.call(void 0, _.TokenType._function)\n        ? Xk()\n        : C.match.call(void 0, _.TokenType._var)\n        ? Yk()\n        : z.eatContextual.call(void 0, ye.ContextualKeyword._module)\n        ? C.eat.call(void 0, _.TokenType.dot)\n          ? Zk()\n          : Jk()\n        : z.isContextual.call(void 0, ye.ContextualKeyword._type)\n        ? e0()\n        : z.isContextual.call(void 0, ye.ContextualKeyword._opaque)\n        ? t0()\n        : z.isContextual.call(void 0, ye.ContextualKeyword._interface)\n        ? n0()\n        : C.match.call(void 0, _.TokenType._export)\n        ? Qk()\n        : z.unexpected.call(void 0);\n    }\n    function Yk() {\n      C.next.call(void 0), oh(), z.semicolon.call(void 0);\n    }\n    function Jk() {\n      for (\n        C.match.call(void 0, _.TokenType.string)\n          ? je.parseExprAtom.call(void 0)\n          : je.parseIdentifier.call(void 0),\n          z.expect.call(void 0, _.TokenType.braceL);\n        !C.match.call(void 0, _.TokenType.braceR) && !ue.state.error;\n\n      )\n        C.match.call(void 0, _.TokenType._import)\n          ? (C.next.call(void 0), ys.parseImport.call(void 0))\n          : z.unexpected.call(void 0);\n      z.expect.call(void 0, _.TokenType.braceR);\n    }\n    function Qk() {\n      z.expect.call(void 0, _.TokenType._export),\n        C.eat.call(void 0, _.TokenType._default)\n          ? C.match.call(void 0, _.TokenType._function) ||\n            C.match.call(void 0, _.TokenType._class)\n            ? El()\n            : (Wt(), z.semicolon.call(void 0))\n          : C.match.call(void 0, _.TokenType._var) ||\n            C.match.call(void 0, _.TokenType._function) ||\n            C.match.call(void 0, _.TokenType._class) ||\n            z.isContextual.call(void 0, ye.ContextualKeyword._opaque)\n          ? El()\n          : C.match.call(void 0, _.TokenType.star) ||\n            C.match.call(void 0, _.TokenType.braceL) ||\n            z.isContextual.call(void 0, ye.ContextualKeyword._interface) ||\n            z.isContextual.call(void 0, ye.ContextualKeyword._type) ||\n            z.isContextual.call(void 0, ye.ContextualKeyword._opaque)\n          ? ys.parseExport.call(void 0)\n          : z.unexpected.call(void 0);\n    }\n    function Zk() {\n      z.expectContextual.call(void 0, ye.ContextualKeyword._exports),\n        vi(),\n        z.semicolon.call(void 0);\n    }\n    function e0() {\n      C.next.call(void 0), Ll();\n    }\n    function t0() {\n      C.next.call(void 0), Ol(!0);\n    }\n    function n0() {\n      C.next.call(void 0), Nl();\n    }\n    function Nl(e = !1) {\n      if (\n        (So(),\n        C.match.call(void 0, _.TokenType.lessThan) && On(),\n        C.eat.call(void 0, _.TokenType._extends))\n      )\n        do bo();\n        while (!e && C.eat.call(void 0, _.TokenType.comma));\n      if (z.isContextual.call(void 0, ye.ContextualKeyword._mixins)) {\n        C.next.call(void 0);\n        do bo();\n        while (C.eat.call(void 0, _.TokenType.comma));\n      }\n      if (z.isContextual.call(void 0, ye.ContextualKeyword._implements)) {\n        C.next.call(void 0);\n        do bo();\n        while (C.eat.call(void 0, _.TokenType.comma));\n      }\n      Co(e, !1, e);\n    }\n    function bo() {\n      sh(!1), C.match.call(void 0, _.TokenType.lessThan) && Rs();\n    }\n    function Rl() {\n      Nl();\n    }\n    function So() {\n      je.parseIdentifier.call(void 0);\n    }\n    function Ll() {\n      So(),\n        C.match.call(void 0, _.TokenType.lessThan) && On(),\n        Ln(_.TokenType.eq),\n        z.semicolon.call(void 0);\n    }\n    function Ol(e) {\n      z.expectContextual.call(void 0, ye.ContextualKeyword._type),\n        So(),\n        C.match.call(void 0, _.TokenType.lessThan) && On(),\n        C.match.call(void 0, _.TokenType.colon) && Ln(_.TokenType.colon),\n        e || Ln(_.TokenType.eq),\n        z.semicolon.call(void 0);\n    }\n    function s0() {\n      Fl(), oh(), C.eat.call(void 0, _.TokenType.eq) && Wt();\n    }\n    function On() {\n      let e = C.pushTypeContext.call(void 0, 0);\n      C.match.call(void 0, _.TokenType.lessThan) ||\n      C.match.call(void 0, _.TokenType.typeParameterStart)\n        ? C.next.call(void 0)\n        : z.unexpected.call(void 0);\n      do\n        s0(),\n          C.match.call(void 0, _.TokenType.greaterThan) ||\n            z.expect.call(void 0, _.TokenType.comma);\n      while (!C.match.call(void 0, _.TokenType.greaterThan) && !ue.state.error);\n      z.expect.call(void 0, _.TokenType.greaterThan),\n        C.popTypeContext.call(void 0, e);\n    }\n    Je.flowParseTypeParameterDeclaration = On;\n    function Rs() {\n      let e = C.pushTypeContext.call(void 0, 0);\n      for (\n        z.expect.call(void 0, _.TokenType.lessThan);\n        !C.match.call(void 0, _.TokenType.greaterThan) && !ue.state.error;\n\n      )\n        Wt(),\n          C.match.call(void 0, _.TokenType.greaterThan) ||\n            z.expect.call(void 0, _.TokenType.comma);\n      z.expect.call(void 0, _.TokenType.greaterThan),\n        C.popTypeContext.call(void 0, e);\n    }\n    function i0() {\n      if (\n        (z.expectContextual.call(void 0, ye.ContextualKeyword._interface),\n        C.eat.call(void 0, _.TokenType._extends))\n      )\n        do bo();\n        while (C.eat.call(void 0, _.TokenType.comma));\n      Co(!1, !1, !1);\n    }\n    function Dl() {\n      C.match.call(void 0, _.TokenType.num) ||\n      C.match.call(void 0, _.TokenType.string)\n        ? je.parseExprAtom.call(void 0)\n        : je.parseIdentifier.call(void 0);\n    }\n    function r0() {\n      C.lookaheadType.call(void 0) === _.TokenType.colon ? (Dl(), Ln()) : Wt(),\n        z.expect.call(void 0, _.TokenType.bracketR),\n        Ln();\n    }\n    function o0() {\n      Dl(),\n        z.expect.call(void 0, _.TokenType.bracketR),\n        z.expect.call(void 0, _.TokenType.bracketR),\n        C.match.call(void 0, _.TokenType.lessThan) ||\n        C.match.call(void 0, _.TokenType.parenL)\n          ? Ml()\n          : (C.eat.call(void 0, _.TokenType.question), Ln());\n    }\n    function Ml() {\n      for (\n        C.match.call(void 0, _.TokenType.lessThan) && On(),\n          z.expect.call(void 0, _.TokenType.parenL);\n        !C.match.call(void 0, _.TokenType.parenR) &&\n        !C.match.call(void 0, _.TokenType.ellipsis) &&\n        !ue.state.error;\n\n      )\n        wo(),\n          C.match.call(void 0, _.TokenType.parenR) ||\n            z.expect.call(void 0, _.TokenType.comma);\n      C.eat.call(void 0, _.TokenType.ellipsis) && wo(),\n        z.expect.call(void 0, _.TokenType.parenR),\n        Ln();\n    }\n    function a0() {\n      Ml();\n    }\n    function Co(e, t, s) {\n      let i;\n      for (\n        t && C.match.call(void 0, _.TokenType.braceBarL)\n          ? (z.expect.call(void 0, _.TokenType.braceBarL),\n            (i = _.TokenType.braceBarR))\n          : (z.expect.call(void 0, _.TokenType.braceL),\n            (i = _.TokenType.braceR));\n        !C.match.call(void 0, i) && !ue.state.error;\n\n      ) {\n        if (s && z.isContextual.call(void 0, ye.ContextualKeyword._proto)) {\n          let r = C.lookaheadType.call(void 0);\n          r !== _.TokenType.colon &&\n            r !== _.TokenType.question &&\n            (C.next.call(void 0), (e = !1));\n        }\n        if (e && z.isContextual.call(void 0, ye.ContextualKeyword._static)) {\n          let r = C.lookaheadType.call(void 0);\n          r !== _.TokenType.colon &&\n            r !== _.TokenType.question &&\n            C.next.call(void 0);\n        }\n        if ((Fl(), C.eat.call(void 0, _.TokenType.bracketL)))\n          C.eat.call(void 0, _.TokenType.bracketL) ? o0() : r0();\n        else if (\n          C.match.call(void 0, _.TokenType.parenL) ||\n          C.match.call(void 0, _.TokenType.lessThan)\n        )\n          a0();\n        else {\n          if (\n            z.isContextual.call(void 0, ye.ContextualKeyword._get) ||\n            z.isContextual.call(void 0, ye.ContextualKeyword._set)\n          ) {\n            let r = C.lookaheadType.call(void 0);\n            (r === _.TokenType.name ||\n              r === _.TokenType.string ||\n              r === _.TokenType.num) &&\n              C.next.call(void 0);\n          }\n          l0();\n        }\n        c0();\n      }\n      z.expect.call(void 0, i);\n    }\n    function l0() {\n      if (C.match.call(void 0, _.TokenType.ellipsis)) {\n        if (\n          (z.expect.call(void 0, _.TokenType.ellipsis),\n          C.eat.call(void 0, _.TokenType.comma) ||\n            C.eat.call(void 0, _.TokenType.semi),\n          C.match.call(void 0, _.TokenType.braceR))\n        )\n          return;\n        Wt();\n      } else\n        Dl(),\n          C.match.call(void 0, _.TokenType.lessThan) ||\n          C.match.call(void 0, _.TokenType.parenL)\n            ? Ml()\n            : (C.eat.call(void 0, _.TokenType.question), Ln());\n    }\n    function c0() {\n      !C.eat.call(void 0, _.TokenType.semi) &&\n        !C.eat.call(void 0, _.TokenType.comma) &&\n        !C.match.call(void 0, _.TokenType.braceR) &&\n        !C.match.call(void 0, _.TokenType.braceBarR) &&\n        z.unexpected.call(void 0);\n    }\n    function sh(e) {\n      for (\n        e || je.parseIdentifier.call(void 0);\n        C.eat.call(void 0, _.TokenType.dot);\n\n      )\n        je.parseIdentifier.call(void 0);\n    }\n    function u0() {\n      sh(!0), C.match.call(void 0, _.TokenType.lessThan) && Rs();\n    }\n    function p0() {\n      z.expect.call(void 0, _.TokenType._typeof), ih();\n    }\n    function h0() {\n      for (\n        z.expect.call(void 0, _.TokenType.bracketL);\n        ue.state.pos < ue.input.length &&\n        !C.match.call(void 0, _.TokenType.bracketR) &&\n        (Wt(), !C.match.call(void 0, _.TokenType.bracketR));\n\n      )\n        z.expect.call(void 0, _.TokenType.comma);\n      z.expect.call(void 0, _.TokenType.bracketR);\n    }\n    function wo() {\n      let e = C.lookaheadType.call(void 0);\n      e === _.TokenType.colon || e === _.TokenType.question\n        ? (je.parseIdentifier.call(void 0),\n          C.eat.call(void 0, _.TokenType.question),\n          Ln())\n        : Wt();\n    }\n    function Al() {\n      for (\n        ;\n        !C.match.call(void 0, _.TokenType.parenR) &&\n        !C.match.call(void 0, _.TokenType.ellipsis) &&\n        !ue.state.error;\n\n      )\n        wo(),\n          C.match.call(void 0, _.TokenType.parenR) ||\n            z.expect.call(void 0, _.TokenType.comma);\n      C.eat.call(void 0, _.TokenType.ellipsis) && wo();\n    }\n    function ih() {\n      let e = !1,\n        t = ue.state.noAnonFunctionType;\n      switch (ue.state.type) {\n        case _.TokenType.name: {\n          if (z.isContextual.call(void 0, ye.ContextualKeyword._interface)) {\n            i0();\n            return;\n          }\n          je.parseIdentifier.call(void 0), u0();\n          return;\n        }\n        case _.TokenType.braceL:\n          Co(!1, !1, !1);\n          return;\n        case _.TokenType.braceBarL:\n          Co(!1, !0, !1);\n          return;\n        case _.TokenType.bracketL:\n          h0();\n          return;\n        case _.TokenType.lessThan:\n          On(),\n            z.expect.call(void 0, _.TokenType.parenL),\n            Al(),\n            z.expect.call(void 0, _.TokenType.parenR),\n            z.expect.call(void 0, _.TokenType.arrow),\n            Wt();\n          return;\n        case _.TokenType.parenL:\n          if (\n            (C.next.call(void 0),\n            !C.match.call(void 0, _.TokenType.parenR) &&\n              !C.match.call(void 0, _.TokenType.ellipsis))\n          )\n            if (C.match.call(void 0, _.TokenType.name)) {\n              let s = C.lookaheadType.call(void 0);\n              e = s !== _.TokenType.question && s !== _.TokenType.colon;\n            } else e = !0;\n          if (e)\n            if (\n              ((ue.state.noAnonFunctionType = !1),\n              Wt(),\n              (ue.state.noAnonFunctionType = t),\n              ue.state.noAnonFunctionType ||\n                !(\n                  C.match.call(void 0, _.TokenType.comma) ||\n                  (C.match.call(void 0, _.TokenType.parenR) &&\n                    C.lookaheadType.call(void 0) === _.TokenType.arrow)\n                ))\n            ) {\n              z.expect.call(void 0, _.TokenType.parenR);\n              return;\n            } else C.eat.call(void 0, _.TokenType.comma);\n          Al(),\n            z.expect.call(void 0, _.TokenType.parenR),\n            z.expect.call(void 0, _.TokenType.arrow),\n            Wt();\n          return;\n        case _.TokenType.minus:\n          C.next.call(void 0), je.parseLiteral.call(void 0);\n          return;\n        case _.TokenType.string:\n        case _.TokenType.num:\n        case _.TokenType._true:\n        case _.TokenType._false:\n        case _.TokenType._null:\n        case _.TokenType._this:\n        case _.TokenType._void:\n        case _.TokenType.star:\n          C.next.call(void 0);\n          return;\n        default:\n          if (ue.state.type === _.TokenType._typeof) {\n            p0();\n            return;\n          } else if (ue.state.type & _.TokenType.IS_KEYWORD) {\n            C.next.call(void 0),\n              (ue.state.tokens[ue.state.tokens.length - 1].type =\n                _.TokenType.name);\n            return;\n          }\n      }\n      z.unexpected.call(void 0);\n    }\n    function f0() {\n      for (\n        ih();\n        !z.canInsertSemicolon.call(void 0) &&\n        (C.match.call(void 0, _.TokenType.bracketL) ||\n          C.match.call(void 0, _.TokenType.questionDot));\n\n      )\n        C.eat.call(void 0, _.TokenType.questionDot),\n          z.expect.call(void 0, _.TokenType.bracketL),\n          C.eat.call(void 0, _.TokenType.bracketR) ||\n            (Wt(), z.expect.call(void 0, _.TokenType.bracketR));\n    }\n    function rh() {\n      C.eat.call(void 0, _.TokenType.question) ? rh() : f0();\n    }\n    function th() {\n      rh(),\n        !ue.state.noAnonFunctionType &&\n          C.eat.call(void 0, _.TokenType.arrow) &&\n          Wt();\n    }\n    function nh() {\n      for (\n        C.eat.call(void 0, _.TokenType.bitwiseAND), th();\n        C.eat.call(void 0, _.TokenType.bitwiseAND);\n\n      )\n        th();\n    }\n    function d0() {\n      for (\n        C.eat.call(void 0, _.TokenType.bitwiseOR), nh();\n        C.eat.call(void 0, _.TokenType.bitwiseOR);\n\n      )\n        nh();\n    }\n    function Wt() {\n      d0();\n    }\n    function vi() {\n      Ln();\n    }\n    Je.flowParseTypeAnnotation = vi;\n    function oh() {\n      je.parseIdentifier.call(void 0),\n        C.match.call(void 0, _.TokenType.colon) && vi();\n    }\n    function Fl() {\n      (C.match.call(void 0, _.TokenType.plus) ||\n        C.match.call(void 0, _.TokenType.minus)) &&\n        (C.next.call(void 0),\n        (ue.state.tokens[ue.state.tokens.length - 1].isType = !0));\n    }\n    Je.flowParseVariance = Fl;\n    function m0(e) {\n      C.match.call(void 0, _.TokenType.colon) && Pl(),\n        je.parseFunctionBody.call(void 0, !1, e);\n    }\n    Je.flowParseFunctionBodyAndFinish = m0;\n    function y0(e, t, s) {\n      if (\n        C.match.call(void 0, _.TokenType.questionDot) &&\n        C.lookaheadType.call(void 0) === _.TokenType.lessThan\n      ) {\n        if (t) {\n          s.stop = !0;\n          return;\n        }\n        C.next.call(void 0),\n          Rs(),\n          z.expect.call(void 0, _.TokenType.parenL),\n          je.parseCallExpressionArguments.call(void 0);\n        return;\n      } else if (!t && C.match.call(void 0, _.TokenType.lessThan)) {\n        let i = ue.state.snapshot();\n        if (\n          (Rs(),\n          z.expect.call(void 0, _.TokenType.parenL),\n          je.parseCallExpressionArguments.call(void 0),\n          ue.state.error)\n        )\n          ue.state.restoreFromSnapshot(i);\n        else return;\n      }\n      je.baseParseSubscript.call(void 0, e, t, s);\n    }\n    Je.flowParseSubscript = y0;\n    function T0() {\n      if (C.match.call(void 0, _.TokenType.lessThan)) {\n        let e = ue.state.snapshot();\n        Rs(), ue.state.error && ue.state.restoreFromSnapshot(e);\n      }\n    }\n    Je.flowStartParseNewArguments = T0;\n    function k0() {\n      if (\n        C.match.call(void 0, _.TokenType.name) &&\n        ue.state.contextualKeyword === ye.ContextualKeyword._interface\n      ) {\n        let e = C.pushTypeContext.call(void 0, 0);\n        return C.next.call(void 0), Rl(), C.popTypeContext.call(void 0, e), !0;\n      } else if (z.isContextual.call(void 0, ye.ContextualKeyword._enum))\n        return ah(), !0;\n      return !1;\n    }\n    Je.flowTryParseStatement = k0;\n    function v0() {\n      return z.isContextual.call(void 0, ye.ContextualKeyword._enum)\n        ? (ah(), !0)\n        : !1;\n    }\n    Je.flowTryParseExportDefaultExpression = v0;\n    function x0(e) {\n      if (e === ye.ContextualKeyword._declare) {\n        if (\n          C.match.call(void 0, _.TokenType._class) ||\n          C.match.call(void 0, _.TokenType.name) ||\n          C.match.call(void 0, _.TokenType._function) ||\n          C.match.call(void 0, _.TokenType._var) ||\n          C.match.call(void 0, _.TokenType._export)\n        ) {\n          let t = C.pushTypeContext.call(void 0, 1);\n          El(), C.popTypeContext.call(void 0, t);\n        }\n      } else if (C.match.call(void 0, _.TokenType.name)) {\n        if (e === ye.ContextualKeyword._interface) {\n          let t = C.pushTypeContext.call(void 0, 1);\n          Rl(), C.popTypeContext.call(void 0, t);\n        } else if (e === ye.ContextualKeyword._type) {\n          let t = C.pushTypeContext.call(void 0, 1);\n          Ll(), C.popTypeContext.call(void 0, t);\n        } else if (e === ye.ContextualKeyword._opaque) {\n          let t = C.pushTypeContext.call(void 0, 1);\n          Ol(!1), C.popTypeContext.call(void 0, t);\n        }\n      }\n      z.semicolon.call(void 0);\n    }\n    Je.flowParseIdentifierStatement = x0;\n    function g0() {\n      return (\n        z.isContextual.call(void 0, ye.ContextualKeyword._type) ||\n        z.isContextual.call(void 0, ye.ContextualKeyword._interface) ||\n        z.isContextual.call(void 0, ye.ContextualKeyword._opaque) ||\n        z.isContextual.call(void 0, ye.ContextualKeyword._enum)\n      );\n    }\n    Je.flowShouldParseExportDeclaration = g0;\n    function _0() {\n      return (\n        C.match.call(void 0, _.TokenType.name) &&\n        (ue.state.contextualKeyword === ye.ContextualKeyword._type ||\n          ue.state.contextualKeyword === ye.ContextualKeyword._interface ||\n          ue.state.contextualKeyword === ye.ContextualKeyword._opaque ||\n          ue.state.contextualKeyword === ye.ContextualKeyword._enum)\n      );\n    }\n    Je.flowShouldDisallowExportDefaultSpecifier = _0;\n    function b0() {\n      if (z.isContextual.call(void 0, ye.ContextualKeyword._type)) {\n        let e = C.pushTypeContext.call(void 0, 1);\n        C.next.call(void 0),\n          C.match.call(void 0, _.TokenType.braceL)\n            ? (ys.parseExportSpecifiers.call(void 0),\n              ys.parseExportFrom.call(void 0))\n            : Ll(),\n          C.popTypeContext.call(void 0, e);\n      } else if (z.isContextual.call(void 0, ye.ContextualKeyword._opaque)) {\n        let e = C.pushTypeContext.call(void 0, 1);\n        C.next.call(void 0), Ol(!1), C.popTypeContext.call(void 0, e);\n      } else if (z.isContextual.call(void 0, ye.ContextualKeyword._interface)) {\n        let e = C.pushTypeContext.call(void 0, 1);\n        C.next.call(void 0), Rl(), C.popTypeContext.call(void 0, e);\n      } else ys.parseStatement.call(void 0, !0);\n    }\n    Je.flowParseExportDeclaration = b0;\n    function C0() {\n      return (\n        C.match.call(void 0, _.TokenType.star) ||\n        (z.isContextual.call(void 0, ye.ContextualKeyword._type) &&\n          C.lookaheadType.call(void 0) === _.TokenType.star)\n      );\n    }\n    Je.flowShouldParseExportStar = C0;\n    function w0() {\n      if (z.eatContextual.call(void 0, ye.ContextualKeyword._type)) {\n        let e = C.pushTypeContext.call(void 0, 2);\n        ys.baseParseExportStar.call(void 0), C.popTypeContext.call(void 0, e);\n      } else ys.baseParseExportStar.call(void 0);\n    }\n    Je.flowParseExportStar = w0;\n    function S0(e) {\n      if (\n        (e && C.match.call(void 0, _.TokenType.lessThan) && Rs(),\n        z.isContextual.call(void 0, ye.ContextualKeyword._implements))\n      ) {\n        let t = C.pushTypeContext.call(void 0, 0);\n        C.next.call(void 0),\n          (ue.state.tokens[ue.state.tokens.length - 1].type =\n            _.TokenType._implements);\n        do So(), C.match.call(void 0, _.TokenType.lessThan) && Rs();\n        while (C.eat.call(void 0, _.TokenType.comma));\n        C.popTypeContext.call(void 0, t);\n      }\n    }\n    Je.flowAfterParseClassSuper = S0;\n    function I0() {\n      C.match.call(void 0, _.TokenType.lessThan) &&\n        (On(),\n        C.match.call(void 0, _.TokenType.parenL) || z.unexpected.call(void 0));\n    }\n    Je.flowStartParseObjPropValue = I0;\n    function E0() {\n      let e = C.pushTypeContext.call(void 0, 0);\n      C.eat.call(void 0, _.TokenType.question),\n        C.match.call(void 0, _.TokenType.colon) && vi(),\n        C.popTypeContext.call(void 0, e);\n    }\n    Je.flowParseAssignableListItemTypes = E0;\n    function A0() {\n      if (\n        C.match.call(void 0, _.TokenType._typeof) ||\n        z.isContextual.call(void 0, ye.ContextualKeyword._type)\n      ) {\n        let e = C.lookaheadTypeAndKeyword.call(void 0);\n        (Gk(e) ||\n          e.type === _.TokenType.braceL ||\n          e.type === _.TokenType.star) &&\n          C.next.call(void 0);\n      }\n    }\n    Je.flowStartParseImportSpecifiers = A0;\n    function P0() {\n      let e =\n        ue.state.contextualKeyword === ye.ContextualKeyword._type ||\n        ue.state.type === _.TokenType._typeof;\n      e ? C.next.call(void 0) : je.parseIdentifier.call(void 0),\n        z.isContextual.call(void 0, ye.ContextualKeyword._as) &&\n        !z.isLookaheadContextual.call(void 0, ye.ContextualKeyword._as)\n          ? (je.parseIdentifier.call(void 0),\n            (e &&\n              !C.match.call(void 0, _.TokenType.name) &&\n              !(ue.state.type & _.TokenType.IS_KEYWORD)) ||\n              je.parseIdentifier.call(void 0))\n          : (e &&\n              (C.match.call(void 0, _.TokenType.name) ||\n                ue.state.type & _.TokenType.IS_KEYWORD) &&\n              je.parseIdentifier.call(void 0),\n            z.eatContextual.call(void 0, ye.ContextualKeyword._as) &&\n              je.parseIdentifier.call(void 0));\n    }\n    Je.flowParseImportSpecifier = P0;\n    function N0() {\n      if (C.match.call(void 0, _.TokenType.lessThan)) {\n        let e = C.pushTypeContext.call(void 0, 0);\n        On(), C.popTypeContext.call(void 0, e);\n      }\n    }\n    Je.flowStartParseFunctionParams = N0;\n    function R0() {\n      C.match.call(void 0, _.TokenType.colon) && vi();\n    }\n    Je.flowAfterParseVarHead = R0;\n    function L0() {\n      if (C.match.call(void 0, _.TokenType.colon)) {\n        let e = ue.state.noAnonFunctionType;\n        (ue.state.noAnonFunctionType = !0),\n          vi(),\n          (ue.state.noAnonFunctionType = e);\n      }\n    }\n    Je.flowStartParseAsyncArrowFromCallExpression = L0;\n    function O0(e, t) {\n      if (C.match.call(void 0, _.TokenType.lessThan)) {\n        let s = ue.state.snapshot(),\n          i = je.baseParseMaybeAssign.call(void 0, e, t);\n        if (ue.state.error)\n          ue.state.restoreFromSnapshot(s),\n            (ue.state.type = _.TokenType.typeParameterStart);\n        else return i;\n        let r = C.pushTypeContext.call(void 0, 0);\n        if (\n          (On(),\n          C.popTypeContext.call(void 0, r),\n          (i = je.baseParseMaybeAssign.call(void 0, e, t)),\n          i)\n        )\n          return !0;\n        z.unexpected.call(void 0);\n      }\n      return je.baseParseMaybeAssign.call(void 0, e, t);\n    }\n    Je.flowParseMaybeAssign = O0;\n    function D0() {\n      if (C.match.call(void 0, _.TokenType.colon)) {\n        let e = C.pushTypeContext.call(void 0, 0),\n          t = ue.state.snapshot(),\n          s = ue.state.noAnonFunctionType;\n        (ue.state.noAnonFunctionType = !0),\n          Pl(),\n          (ue.state.noAnonFunctionType = s),\n          z.canInsertSemicolon.call(void 0) && z.unexpected.call(void 0),\n          C.match.call(void 0, _.TokenType.arrow) || z.unexpected.call(void 0),\n          ue.state.error && ue.state.restoreFromSnapshot(t),\n          C.popTypeContext.call(void 0, e);\n      }\n      return C.eat.call(void 0, _.TokenType.arrow);\n    }\n    Je.flowParseArrow = D0;\n    function M0(e, t = !1) {\n      if (\n        ue.state.tokens[ue.state.tokens.length - 1].contextualKeyword ===\n          ye.ContextualKeyword._async &&\n        C.match.call(void 0, _.TokenType.lessThan)\n      ) {\n        let s = ue.state.snapshot();\n        if (F0() && !ue.state.error) return;\n        ue.state.restoreFromSnapshot(s);\n      }\n      je.baseParseSubscripts.call(void 0, e, t);\n    }\n    Je.flowParseSubscripts = M0;\n    function F0() {\n      ue.state.scopeDepth++;\n      let e = ue.state.tokens.length;\n      return (\n        ys.parseFunctionParams.call(void 0),\n        je.parseArrow.call(void 0)\n          ? (je.parseArrowExpression.call(void 0, e), !0)\n          : !1\n      );\n    }\n    function ah() {\n      z.expectContextual.call(void 0, ye.ContextualKeyword._enum),\n        (ue.state.tokens[ue.state.tokens.length - 1].type = _.TokenType._enum),\n        je.parseIdentifier.call(void 0),\n        B0();\n    }\n    function B0() {\n      z.eatContextual.call(void 0, ye.ContextualKeyword._of) &&\n        C.next.call(void 0),\n        z.expect.call(void 0, _.TokenType.braceL),\n        V0(),\n        z.expect.call(void 0, _.TokenType.braceR);\n    }\n    function V0() {\n      for (\n        ;\n        !C.match.call(void 0, _.TokenType.braceR) &&\n        !ue.state.error &&\n        !C.eat.call(void 0, _.TokenType.ellipsis);\n\n      )\n        j0(),\n          C.match.call(void 0, _.TokenType.braceR) ||\n            z.expect.call(void 0, _.TokenType.comma);\n    }\n    function j0() {\n      je.parseIdentifier.call(void 0),\n        C.eat.call(void 0, _.TokenType.eq) && C.next.call(void 0);\n    }\n  });\n  var nr = Z((Tt) => {\n    'use strict';\n    Object.defineProperty(Tt, '__esModule', {value: !0});\n    var $0 = Ul(),\n      Ft = Ji(),\n      dt = hi(),\n      $ = xt(),\n      ke = It(),\n      Ts = qr(),\n      D = be(),\n      lh = Qt(),\n      P = Zt(),\n      De = Ns(),\n      ks = lo(),\n      ee = cs();\n    function q0() {\n      if (\n        (ql(D.TokenType.eof),\n        P.state.scopes.push(new Ts.Scope(0, P.state.tokens.length, !0)),\n        P.state.scopeDepth !== 0)\n      )\n        throw new Error(\n          `Invalid scope depth at end of file: ${P.state.scopeDepth}`\n        );\n      return new $0.File(P.state.tokens, P.state.scopes);\n    }\n    Tt.parseTopLevel = q0;\n    function _n(e) {\n      (P.isFlowEnabled && Ft.flowTryParseStatement.call(void 0)) ||\n        ($.match.call(void 0, D.TokenType.at) && $l(), K0(e));\n    }\n    Tt.parseStatement = _n;\n    function K0(e) {\n      if (P.isTypeScriptEnabled && dt.tsTryParseStatementContent.call(void 0))\n        return;\n      let t = P.state.type;\n      switch (t) {\n        case D.TokenType._break:\n        case D.TokenType._continue:\n          H0();\n          return;\n        case D.TokenType._debugger:\n          W0();\n          return;\n        case D.TokenType._do:\n          G0();\n          return;\n        case D.TokenType._for:\n          z0();\n          return;\n        case D.TokenType._function:\n          if ($.lookaheadType.call(void 0) === D.TokenType.dot) break;\n          e || ee.unexpected.call(void 0), J0();\n          return;\n        case D.TokenType._class:\n          e || ee.unexpected.call(void 0), Io(!0);\n          return;\n        case D.TokenType._if:\n          Q0();\n          return;\n        case D.TokenType._return:\n          Z0();\n          return;\n        case D.TokenType._switch:\n          ev();\n          return;\n        case D.TokenType._throw:\n          tv();\n          return;\n        case D.TokenType._try:\n          sv();\n          return;\n        case D.TokenType._let:\n        case D.TokenType._const:\n          e || ee.unexpected.call(void 0);\n        case D.TokenType._var:\n          Vl(t !== D.TokenType._var);\n          return;\n        case D.TokenType._while:\n          iv();\n          return;\n        case D.TokenType.braceL:\n          gi();\n          return;\n        case D.TokenType.semi:\n          rv();\n          return;\n        case D.TokenType._export:\n        case D.TokenType._import: {\n          let r = $.lookaheadType.call(void 0);\n          if (r === D.TokenType.parenL || r === D.TokenType.dot) break;\n          $.next.call(void 0), t === D.TokenType._import ? xh() : Th();\n          return;\n        }\n        case D.TokenType.name:\n          if (P.state.contextualKeyword === ke.ContextualKeyword._async) {\n            let r = P.state.start,\n              a = P.state.snapshot();\n            if (\n              ($.next.call(void 0),\n              $.match.call(void 0, D.TokenType._function) &&\n                !ee.canInsertSemicolon.call(void 0))\n            ) {\n              ee.expect.call(void 0, D.TokenType._function), lr(r, !0);\n              return;\n            } else P.state.restoreFromSnapshot(a);\n          } else if (\n            P.state.contextualKeyword === ke.ContextualKeyword._using &&\n            !ee.hasFollowingLineBreak.call(void 0) &&\n            $.lookaheadType.call(void 0) === D.TokenType.name\n          ) {\n            Vl(!0);\n            return;\n          }\n        default:\n          break;\n      }\n      let s = P.state.tokens.length;\n      De.parseExpression.call(void 0);\n      let i = null;\n      if (P.state.tokens.length === s + 1) {\n        let r = P.state.tokens[P.state.tokens.length - 1];\n        r.type === D.TokenType.name && (i = r.contextualKeyword);\n      }\n      if (i == null) {\n        ee.semicolon.call(void 0);\n        return;\n      }\n      $.eat.call(void 0, D.TokenType.colon) ? ov() : av(i);\n    }\n    function $l() {\n      for (; $.match.call(void 0, D.TokenType.at); ) ph();\n    }\n    Tt.parseDecorators = $l;\n    function ph() {\n      if (($.next.call(void 0), $.eat.call(void 0, D.TokenType.parenL)))\n        De.parseExpression.call(void 0),\n          ee.expect.call(void 0, D.TokenType.parenR);\n      else {\n        for (\n          De.parseIdentifier.call(void 0);\n          $.eat.call(void 0, D.TokenType.dot);\n\n        )\n          De.parseIdentifier.call(void 0);\n        U0();\n      }\n    }\n    function U0() {\n      P.isTypeScriptEnabled\n        ? dt.tsParseMaybeDecoratorArguments.call(void 0)\n        : hh();\n    }\n    function hh() {\n      $.eat.call(void 0, D.TokenType.parenL) &&\n        De.parseCallExpressionArguments.call(void 0);\n    }\n    Tt.baseParseMaybeDecoratorArguments = hh;\n    function H0() {\n      $.next.call(void 0),\n        ee.isLineTerminator.call(void 0) ||\n          (De.parseIdentifier.call(void 0), ee.semicolon.call(void 0));\n    }\n    function W0() {\n      $.next.call(void 0), ee.semicolon.call(void 0);\n    }\n    function G0() {\n      $.next.call(void 0),\n        _n(!1),\n        ee.expect.call(void 0, D.TokenType._while),\n        De.parseParenExpression.call(void 0),\n        $.eat.call(void 0, D.TokenType.semi);\n    }\n    function z0() {\n      P.state.scopeDepth++;\n      let e = P.state.tokens.length;\n      Y0();\n      let t = P.state.tokens.length;\n      P.state.scopes.push(new Ts.Scope(e, t, !1)), P.state.scopeDepth--;\n    }\n    function X0() {\n      return !(\n        !ee.isContextual.call(void 0, ke.ContextualKeyword._using) ||\n        ee.isLookaheadContextual.call(void 0, ke.ContextualKeyword._of)\n      );\n    }\n    function Y0() {\n      $.next.call(void 0);\n      let e = !1;\n      if (\n        (ee.isContextual.call(void 0, ke.ContextualKeyword._await) &&\n          ((e = !0), $.next.call(void 0)),\n        ee.expect.call(void 0, D.TokenType.parenL),\n        $.match.call(void 0, D.TokenType.semi))\n      ) {\n        e && ee.unexpected.call(void 0), Bl();\n        return;\n      }\n      if (\n        $.match.call(void 0, D.TokenType._var) ||\n        $.match.call(void 0, D.TokenType._let) ||\n        $.match.call(void 0, D.TokenType._const) ||\n        X0()\n      ) {\n        if (\n          ($.next.call(void 0),\n          fh(!0, P.state.type !== D.TokenType._var),\n          $.match.call(void 0, D.TokenType._in) ||\n            ee.isContextual.call(void 0, ke.ContextualKeyword._of))\n        ) {\n          ch(e);\n          return;\n        }\n        Bl();\n        return;\n      }\n      if (\n        (De.parseExpression.call(void 0, !0),\n        $.match.call(void 0, D.TokenType._in) ||\n          ee.isContextual.call(void 0, ke.ContextualKeyword._of))\n      ) {\n        ch(e);\n        return;\n      }\n      e && ee.unexpected.call(void 0), Bl();\n    }\n    function J0() {\n      let e = P.state.start;\n      $.next.call(void 0), lr(e, !0);\n    }\n    function Q0() {\n      $.next.call(void 0),\n        De.parseParenExpression.call(void 0),\n        _n(!1),\n        $.eat.call(void 0, D.TokenType._else) && _n(!1);\n    }\n    function Z0() {\n      $.next.call(void 0),\n        ee.isLineTerminator.call(void 0) ||\n          (De.parseExpression.call(void 0), ee.semicolon.call(void 0));\n    }\n    function ev() {\n      $.next.call(void 0),\n        De.parseParenExpression.call(void 0),\n        P.state.scopeDepth++;\n      let e = P.state.tokens.length;\n      for (\n        ee.expect.call(void 0, D.TokenType.braceL);\n        !$.match.call(void 0, D.TokenType.braceR) && !P.state.error;\n\n      )\n        if (\n          $.match.call(void 0, D.TokenType._case) ||\n          $.match.call(void 0, D.TokenType._default)\n        ) {\n          let s = $.match.call(void 0, D.TokenType._case);\n          $.next.call(void 0),\n            s && De.parseExpression.call(void 0),\n            ee.expect.call(void 0, D.TokenType.colon);\n        } else _n(!0);\n      $.next.call(void 0);\n      let t = P.state.tokens.length;\n      P.state.scopes.push(new Ts.Scope(e, t, !1)), P.state.scopeDepth--;\n    }\n    function tv() {\n      $.next.call(void 0),\n        De.parseExpression.call(void 0),\n        ee.semicolon.call(void 0);\n    }\n    function nv() {\n      ks.parseBindingAtom.call(void 0, !0),\n        P.isTypeScriptEnabled && dt.tsTryParseTypeAnnotation.call(void 0);\n    }\n    function sv() {\n      if (\n        ($.next.call(void 0), gi(), $.match.call(void 0, D.TokenType._catch))\n      ) {\n        $.next.call(void 0);\n        let e = null;\n        if (\n          ($.match.call(void 0, D.TokenType.parenL) &&\n            (P.state.scopeDepth++,\n            (e = P.state.tokens.length),\n            ee.expect.call(void 0, D.TokenType.parenL),\n            nv(),\n            ee.expect.call(void 0, D.TokenType.parenR)),\n          gi(),\n          e != null)\n        ) {\n          let t = P.state.tokens.length;\n          P.state.scopes.push(new Ts.Scope(e, t, !1)), P.state.scopeDepth--;\n        }\n      }\n      $.eat.call(void 0, D.TokenType._finally) && gi();\n    }\n    function Vl(e) {\n      $.next.call(void 0), fh(!1, e), ee.semicolon.call(void 0);\n    }\n    Tt.parseVarStatement = Vl;\n    function iv() {\n      $.next.call(void 0), De.parseParenExpression.call(void 0), _n(!1);\n    }\n    function rv() {\n      $.next.call(void 0);\n    }\n    function ov() {\n      _n(!0);\n    }\n    function av(e) {\n      P.isTypeScriptEnabled\n        ? dt.tsParseIdentifierStatement.call(void 0, e)\n        : P.isFlowEnabled\n        ? Ft.flowParseIdentifierStatement.call(void 0, e)\n        : ee.semicolon.call(void 0);\n    }\n    function gi(e = !1, t = 0) {\n      let s = P.state.tokens.length;\n      P.state.scopeDepth++,\n        ee.expect.call(void 0, D.TokenType.braceL),\n        t && (P.state.tokens[P.state.tokens.length - 1].contextId = t),\n        ql(D.TokenType.braceR),\n        t && (P.state.tokens[P.state.tokens.length - 1].contextId = t);\n      let i = P.state.tokens.length;\n      P.state.scopes.push(new Ts.Scope(s, i, e)), P.state.scopeDepth--;\n    }\n    Tt.parseBlock = gi;\n    function ql(e) {\n      for (; !$.eat.call(void 0, e) && !P.state.error; ) _n(!0);\n    }\n    Tt.parseBlockBody = ql;\n    function Bl() {\n      ee.expect.call(void 0, D.TokenType.semi),\n        $.match.call(void 0, D.TokenType.semi) ||\n          De.parseExpression.call(void 0),\n        ee.expect.call(void 0, D.TokenType.semi),\n        $.match.call(void 0, D.TokenType.parenR) ||\n          De.parseExpression.call(void 0),\n        ee.expect.call(void 0, D.TokenType.parenR),\n        _n(!1);\n    }\n    function ch(e) {\n      e\n        ? ee.eatContextual.call(void 0, ke.ContextualKeyword._of)\n        : $.next.call(void 0),\n        De.parseExpression.call(void 0),\n        ee.expect.call(void 0, D.TokenType.parenR),\n        _n(!1);\n    }\n    function fh(e, t) {\n      for (;;) {\n        if ((lv(t), $.eat.call(void 0, D.TokenType.eq))) {\n          let s = P.state.tokens.length - 1;\n          De.parseMaybeAssign.call(void 0, e),\n            (P.state.tokens[s].rhsEndIndex = P.state.tokens.length);\n        }\n        if (!$.eat.call(void 0, D.TokenType.comma)) break;\n      }\n    }\n    function lv(e) {\n      ks.parseBindingAtom.call(void 0, e),\n        P.isTypeScriptEnabled\n          ? dt.tsAfterParseVarHead.call(void 0)\n          : P.isFlowEnabled && Ft.flowAfterParseVarHead.call(void 0);\n    }\n    function lr(e, t, s = !1) {\n      $.match.call(void 0, D.TokenType.star) && $.next.call(void 0),\n        t &&\n          !s &&\n          !$.match.call(void 0, D.TokenType.name) &&\n          !$.match.call(void 0, D.TokenType._yield) &&\n          ee.unexpected.call(void 0);\n      let i = null;\n      $.match.call(void 0, D.TokenType.name) &&\n        (t || ((i = P.state.tokens.length), P.state.scopeDepth++),\n        ks.parseBindingIdentifier.call(void 0, !1));\n      let r = P.state.tokens.length;\n      P.state.scopeDepth++, dh(), De.parseFunctionBodyAndFinish.call(void 0, e);\n      let a = P.state.tokens.length;\n      P.state.scopes.push(new Ts.Scope(r, a, !0)),\n        P.state.scopeDepth--,\n        i !== null &&\n          (P.state.scopes.push(new Ts.Scope(i, a, !0)), P.state.scopeDepth--);\n    }\n    Tt.parseFunction = lr;\n    function dh(e = !1, t = 0) {\n      P.isTypeScriptEnabled\n        ? dt.tsStartParseFunctionParams.call(void 0)\n        : P.isFlowEnabled && Ft.flowStartParseFunctionParams.call(void 0),\n        ee.expect.call(void 0, D.TokenType.parenL),\n        t && (P.state.tokens[P.state.tokens.length - 1].contextId = t),\n        ks.parseBindingList.call(void 0, D.TokenType.parenR, !1, !1, e, t),\n        t && (P.state.tokens[P.state.tokens.length - 1].contextId = t);\n    }\n    Tt.parseFunctionParams = dh;\n    function Io(e, t = !1) {\n      let s = P.getNextContextId.call(void 0);\n      $.next.call(void 0),\n        (P.state.tokens[P.state.tokens.length - 1].contextId = s),\n        (P.state.tokens[P.state.tokens.length - 1].isExpression = !e);\n      let i = null;\n      e || ((i = P.state.tokens.length), P.state.scopeDepth++), hv(e, t), fv();\n      let r = P.state.tokens.length;\n      if (\n        (cv(s),\n        !P.state.error &&\n          ((P.state.tokens[r].contextId = s),\n          (P.state.tokens[P.state.tokens.length - 1].contextId = s),\n          i !== null))\n      ) {\n        let a = P.state.tokens.length;\n        P.state.scopes.push(new Ts.Scope(i, a, !1)), P.state.scopeDepth--;\n      }\n    }\n    Tt.parseClass = Io;\n    function mh() {\n      return (\n        $.match.call(void 0, D.TokenType.eq) ||\n        $.match.call(void 0, D.TokenType.semi) ||\n        $.match.call(void 0, D.TokenType.braceR) ||\n        $.match.call(void 0, D.TokenType.bang) ||\n        $.match.call(void 0, D.TokenType.colon)\n      );\n    }\n    function yh() {\n      return (\n        $.match.call(void 0, D.TokenType.parenL) ||\n        $.match.call(void 0, D.TokenType.lessThan)\n      );\n    }\n    function cv(e) {\n      for (\n        ee.expect.call(void 0, D.TokenType.braceL);\n        !$.eat.call(void 0, D.TokenType.braceR) && !P.state.error;\n\n      ) {\n        if ($.eat.call(void 0, D.TokenType.semi)) continue;\n        if ($.match.call(void 0, D.TokenType.at)) {\n          ph();\n          continue;\n        }\n        let t = P.state.start;\n        uv(t, e);\n      }\n    }\n    function uv(e, t) {\n      P.isTypeScriptEnabled &&\n        dt.tsParseModifiers.call(void 0, [\n          ke.ContextualKeyword._declare,\n          ke.ContextualKeyword._public,\n          ke.ContextualKeyword._protected,\n          ke.ContextualKeyword._private,\n          ke.ContextualKeyword._override,\n        ]);\n      let s = !1;\n      if (\n        $.match.call(void 0, D.TokenType.name) &&\n        P.state.contextualKeyword === ke.ContextualKeyword._static\n      ) {\n        if ((De.parseIdentifier.call(void 0), yh())) {\n          or(e, !1);\n          return;\n        } else if (mh()) {\n          ar();\n          return;\n        }\n        if (\n          ((P.state.tokens[P.state.tokens.length - 1].type =\n            D.TokenType._static),\n          (s = !0),\n          $.match.call(void 0, D.TokenType.braceL))\n        ) {\n          (P.state.tokens[P.state.tokens.length - 1].contextId = t), gi();\n          return;\n        }\n      }\n      pv(e, s, t);\n    }\n    function pv(e, t, s) {\n      if (\n        P.isTypeScriptEnabled &&\n        dt.tsTryParseClassMemberWithIsStatic.call(void 0, t)\n      )\n        return;\n      if ($.eat.call(void 0, D.TokenType.star)) {\n        xi(s), or(e, !1);\n        return;\n      }\n      xi(s);\n      let i = !1,\n        r = P.state.tokens[P.state.tokens.length - 1];\n      r.contextualKeyword === ke.ContextualKeyword._constructor && (i = !0),\n        jl(),\n        yh()\n          ? or(e, i)\n          : mh()\n          ? ar()\n          : r.contextualKeyword === ke.ContextualKeyword._async &&\n            !ee.isLineTerminator.call(void 0)\n          ? ((P.state.tokens[P.state.tokens.length - 1].type =\n              D.TokenType._async),\n            $.match.call(void 0, D.TokenType.star) && $.next.call(void 0),\n            xi(s),\n            jl(),\n            or(e, !1))\n          : (r.contextualKeyword === ke.ContextualKeyword._get ||\n              r.contextualKeyword === ke.ContextualKeyword._set) &&\n            !(\n              ee.isLineTerminator.call(void 0) &&\n              $.match.call(void 0, D.TokenType.star)\n            )\n          ? (r.contextualKeyword === ke.ContextualKeyword._get\n              ? (P.state.tokens[P.state.tokens.length - 1].type =\n                  D.TokenType._get)\n              : (P.state.tokens[P.state.tokens.length - 1].type =\n                  D.TokenType._set),\n            xi(s),\n            or(e, !1))\n          : r.contextualKeyword === ke.ContextualKeyword._accessor &&\n            !ee.isLineTerminator.call(void 0)\n          ? (xi(s), ar())\n          : ee.isLineTerminator.call(void 0)\n          ? ar()\n          : ee.unexpected.call(void 0);\n    }\n    function or(e, t) {\n      P.isTypeScriptEnabled\n        ? dt.tsTryParseTypeParameters.call(void 0)\n        : P.isFlowEnabled &&\n          $.match.call(void 0, D.TokenType.lessThan) &&\n          Ft.flowParseTypeParameterDeclaration.call(void 0),\n        De.parseMethod.call(void 0, e, t);\n    }\n    function xi(e) {\n      De.parsePropertyName.call(void 0, e);\n    }\n    Tt.parseClassPropertyName = xi;\n    function jl() {\n      if (P.isTypeScriptEnabled) {\n        let e = $.pushTypeContext.call(void 0, 0);\n        $.eat.call(void 0, D.TokenType.question),\n          $.popTypeContext.call(void 0, e);\n      }\n    }\n    Tt.parsePostMemberNameModifiers = jl;\n    function ar() {\n      if (\n        (P.isTypeScriptEnabled\n          ? ($.eatTypeToken.call(void 0, D.TokenType.bang),\n            dt.tsTryParseTypeAnnotation.call(void 0))\n          : P.isFlowEnabled &&\n            $.match.call(void 0, D.TokenType.colon) &&\n            Ft.flowParseTypeAnnotation.call(void 0),\n        $.match.call(void 0, D.TokenType.eq))\n      ) {\n        let e = P.state.tokens.length;\n        $.next.call(void 0),\n          De.parseMaybeAssign.call(void 0),\n          (P.state.tokens[e].rhsEndIndex = P.state.tokens.length);\n      }\n      ee.semicolon.call(void 0);\n    }\n    Tt.parseClassProperty = ar;\n    function hv(e, t = !1) {\n      (P.isTypeScriptEnabled &&\n        (!e || t) &&\n        ee.isContextual.call(void 0, ke.ContextualKeyword._implements)) ||\n        ($.match.call(void 0, D.TokenType.name) &&\n          ks.parseBindingIdentifier.call(void 0, !0),\n        P.isTypeScriptEnabled\n          ? dt.tsTryParseTypeParameters.call(void 0)\n          : P.isFlowEnabled &&\n            $.match.call(void 0, D.TokenType.lessThan) &&\n            Ft.flowParseTypeParameterDeclaration.call(void 0));\n    }\n    function fv() {\n      let e = !1;\n      $.eat.call(void 0, D.TokenType._extends)\n        ? (De.parseExprSubscripts.call(void 0), (e = !0))\n        : (e = !1),\n        P.isTypeScriptEnabled\n          ? dt.tsAfterParseClassSuper.call(void 0, e)\n          : P.isFlowEnabled && Ft.flowAfterParseClassSuper.call(void 0, e);\n    }\n    function Th() {\n      let e = P.state.tokens.length - 1;\n      (P.isTypeScriptEnabled && dt.tsTryParseExport.call(void 0)) ||\n        (Tv()\n          ? kv()\n          : yv()\n          ? (De.parseIdentifier.call(void 0),\n            $.match.call(void 0, D.TokenType.comma) &&\n            $.lookaheadType.call(void 0) === D.TokenType.star\n              ? (ee.expect.call(void 0, D.TokenType.comma),\n                ee.expect.call(void 0, D.TokenType.star),\n                ee.expectContextual.call(void 0, ke.ContextualKeyword._as),\n                De.parseIdentifier.call(void 0))\n              : kh(),\n            cr())\n          : $.eat.call(void 0, D.TokenType._default)\n          ? dv()\n          : xv()\n          ? mv()\n          : (Kl(), cr()),\n        (P.state.tokens[e].rhsEndIndex = P.state.tokens.length));\n    }\n    Tt.parseExport = Th;\n    function dv() {\n      if (\n        (P.isTypeScriptEnabled &&\n          dt.tsTryParseExportDefaultExpression.call(void 0)) ||\n        (P.isFlowEnabled && Ft.flowTryParseExportDefaultExpression.call(void 0))\n      )\n        return;\n      let e = P.state.start;\n      $.eat.call(void 0, D.TokenType._function)\n        ? lr(e, !0, !0)\n        : ee.isContextual.call(void 0, ke.ContextualKeyword._async) &&\n          $.lookaheadType.call(void 0) === D.TokenType._function\n        ? (ee.eatContextual.call(void 0, ke.ContextualKeyword._async),\n          $.eat.call(void 0, D.TokenType._function),\n          lr(e, !0, !0))\n        : $.match.call(void 0, D.TokenType._class)\n        ? Io(!0, !0)\n        : $.match.call(void 0, D.TokenType.at)\n        ? ($l(), Io(!0, !0))\n        : (De.parseMaybeAssign.call(void 0), ee.semicolon.call(void 0));\n    }\n    function mv() {\n      P.isTypeScriptEnabled\n        ? dt.tsParseExportDeclaration.call(void 0)\n        : P.isFlowEnabled\n        ? Ft.flowParseExportDeclaration.call(void 0)\n        : _n(!0);\n    }\n    function yv() {\n      if (P.isTypeScriptEnabled && dt.tsIsDeclarationStart.call(void 0))\n        return !1;\n      if (\n        P.isFlowEnabled &&\n        Ft.flowShouldDisallowExportDefaultSpecifier.call(void 0)\n      )\n        return !1;\n      if ($.match.call(void 0, D.TokenType.name))\n        return P.state.contextualKeyword !== ke.ContextualKeyword._async;\n      if (!$.match.call(void 0, D.TokenType._default)) return !1;\n      let e = $.nextTokenStart.call(void 0),\n        t = $.lookaheadTypeAndKeyword.call(void 0),\n        s =\n          t.type === D.TokenType.name &&\n          t.contextualKeyword === ke.ContextualKeyword._from;\n      if (t.type === D.TokenType.comma) return !0;\n      if (s) {\n        let i = P.input.charCodeAt($.nextTokenStartSince.call(void 0, e + 4));\n        return (\n          i === lh.charCodes.quotationMark || i === lh.charCodes.apostrophe\n        );\n      }\n      return !1;\n    }\n    function kh() {\n      $.eat.call(void 0, D.TokenType.comma) && Kl();\n    }\n    function cr() {\n      ee.eatContextual.call(void 0, ke.ContextualKeyword._from) &&\n        (De.parseExprAtom.call(void 0), gh()),\n        ee.semicolon.call(void 0);\n    }\n    Tt.parseExportFrom = cr;\n    function Tv() {\n      return P.isFlowEnabled\n        ? Ft.flowShouldParseExportStar.call(void 0)\n        : $.match.call(void 0, D.TokenType.star);\n    }\n    function kv() {\n      P.isFlowEnabled ? Ft.flowParseExportStar.call(void 0) : vh();\n    }\n    function vh() {\n      ee.expect.call(void 0, D.TokenType.star),\n        ee.isContextual.call(void 0, ke.ContextualKeyword._as) ? vv() : cr();\n    }\n    Tt.baseParseExportStar = vh;\n    function vv() {\n      $.next.call(void 0),\n        (P.state.tokens[P.state.tokens.length - 1].type = D.TokenType._as),\n        De.parseIdentifier.call(void 0),\n        kh(),\n        cr();\n    }\n    function xv() {\n      return (\n        (P.isTypeScriptEnabled && dt.tsIsDeclarationStart.call(void 0)) ||\n        (P.isFlowEnabled && Ft.flowShouldParseExportDeclaration.call(void 0)) ||\n        P.state.type === D.TokenType._var ||\n        P.state.type === D.TokenType._const ||\n        P.state.type === D.TokenType._let ||\n        P.state.type === D.TokenType._function ||\n        P.state.type === D.TokenType._class ||\n        ee.isContextual.call(void 0, ke.ContextualKeyword._async) ||\n        $.match.call(void 0, D.TokenType.at)\n      );\n    }\n    function Kl() {\n      let e = !0;\n      for (\n        ee.expect.call(void 0, D.TokenType.braceL);\n        !$.eat.call(void 0, D.TokenType.braceR) && !P.state.error;\n\n      ) {\n        if (e) e = !1;\n        else if (\n          (ee.expect.call(void 0, D.TokenType.comma),\n          $.eat.call(void 0, D.TokenType.braceR))\n        )\n          break;\n        gv();\n      }\n    }\n    Tt.parseExportSpecifiers = Kl;\n    function gv() {\n      if (P.isTypeScriptEnabled) {\n        dt.tsParseExportSpecifier.call(void 0);\n        return;\n      }\n      De.parseIdentifier.call(void 0),\n        (P.state.tokens[P.state.tokens.length - 1].identifierRole =\n          $.IdentifierRole.ExportAccess),\n        ee.eatContextual.call(void 0, ke.ContextualKeyword._as) &&\n          De.parseIdentifier.call(void 0);\n    }\n    function _v() {\n      let e = P.state.snapshot();\n      return (\n        ee.expectContextual.call(void 0, ke.ContextualKeyword._module),\n        ee.eatContextual.call(void 0, ke.ContextualKeyword._from)\n          ? ee.isContextual.call(void 0, ke.ContextualKeyword._from)\n            ? (P.state.restoreFromSnapshot(e), !0)\n            : (P.state.restoreFromSnapshot(e), !1)\n          : $.match.call(void 0, D.TokenType.comma)\n          ? (P.state.restoreFromSnapshot(e), !1)\n          : (P.state.restoreFromSnapshot(e), !0)\n      );\n    }\n    function bv() {\n      ee.isContextual.call(void 0, ke.ContextualKeyword._module) &&\n        _v() &&\n        $.next.call(void 0);\n    }\n    function xh() {\n      if (\n        P.isTypeScriptEnabled &&\n        $.match.call(void 0, D.TokenType.name) &&\n        $.lookaheadType.call(void 0) === D.TokenType.eq\n      ) {\n        dt.tsParseImportEqualsDeclaration.call(void 0);\n        return;\n      }\n      if (\n        P.isTypeScriptEnabled &&\n        ee.isContextual.call(void 0, ke.ContextualKeyword._type)\n      ) {\n        let e = $.lookaheadTypeAndKeyword.call(void 0);\n        if (\n          e.type === D.TokenType.name &&\n          e.contextualKeyword !== ke.ContextualKeyword._from\n        ) {\n          if (\n            (ee.expectContextual.call(void 0, ke.ContextualKeyword._type),\n            $.lookaheadType.call(void 0) === D.TokenType.eq)\n          ) {\n            dt.tsParseImportEqualsDeclaration.call(void 0);\n            return;\n          }\n        } else\n          (e.type === D.TokenType.star || e.type === D.TokenType.braceL) &&\n            ee.expectContextual.call(void 0, ke.ContextualKeyword._type);\n      }\n      $.match.call(void 0, D.TokenType.string) ||\n        (bv(),\n        wv(),\n        ee.expectContextual.call(void 0, ke.ContextualKeyword._from)),\n        De.parseExprAtom.call(void 0),\n        gh(),\n        ee.semicolon.call(void 0);\n    }\n    Tt.parseImport = xh;\n    function Cv() {\n      return $.match.call(void 0, D.TokenType.name);\n    }\n    function uh() {\n      ks.parseImportedIdentifier.call(void 0);\n    }\n    function wv() {\n      P.isFlowEnabled && Ft.flowStartParseImportSpecifiers.call(void 0);\n      let e = !0;\n      if (!(Cv() && (uh(), !$.eat.call(void 0, D.TokenType.comma)))) {\n        if ($.match.call(void 0, D.TokenType.star)) {\n          $.next.call(void 0),\n            ee.expectContextual.call(void 0, ke.ContextualKeyword._as),\n            uh();\n          return;\n        }\n        for (\n          ee.expect.call(void 0, D.TokenType.braceL);\n          !$.eat.call(void 0, D.TokenType.braceR) && !P.state.error;\n\n        ) {\n          if (e) e = !1;\n          else if (\n            ($.eat.call(void 0, D.TokenType.colon) &&\n              ee.unexpected.call(\n                void 0,\n                'ES2015 named imports do not destructure. Use another statement for destructuring after the import.'\n              ),\n            ee.expect.call(void 0, D.TokenType.comma),\n            $.eat.call(void 0, D.TokenType.braceR))\n          )\n            break;\n          Sv();\n        }\n      }\n    }\n    function Sv() {\n      if (P.isTypeScriptEnabled) {\n        dt.tsParseImportSpecifier.call(void 0);\n        return;\n      }\n      if (P.isFlowEnabled) {\n        Ft.flowParseImportSpecifier.call(void 0);\n        return;\n      }\n      ks.parseImportedIdentifier.call(void 0),\n        ee.isContextual.call(void 0, ke.ContextualKeyword._as) &&\n          ((P.state.tokens[P.state.tokens.length - 1].identifierRole =\n            $.IdentifierRole.ImportAccess),\n          $.next.call(void 0),\n          ks.parseImportedIdentifier.call(void 0));\n    }\n    function gh() {\n      ee.isContextual.call(void 0, ke.ContextualKeyword._assert) &&\n        !ee.hasPrecedingLineBreak.call(void 0) &&\n        ($.next.call(void 0), De.parseObj.call(void 0, !1, !1));\n    }\n  });\n  var Ch = Z((Wl) => {\n    'use strict';\n    Object.defineProperty(Wl, '__esModule', {value: !0});\n    var _h = xt(),\n      bh = Qt(),\n      Hl = Zt(),\n      Iv = nr();\n    function Ev() {\n      return (\n        Hl.state.pos === 0 &&\n          Hl.input.charCodeAt(0) === bh.charCodes.numberSign &&\n          Hl.input.charCodeAt(1) === bh.charCodes.exclamationMark &&\n          _h.skipLineComment.call(void 0, 2),\n        _h.nextToken.call(void 0),\n        Iv.parseTopLevel.call(void 0)\n      );\n    }\n    Wl.parseFile = Ev;\n  });\n  var Ul = Z((Ao) => {\n    'use strict';\n    Object.defineProperty(Ao, '__esModule', {value: !0});\n    var Eo = Zt(),\n      Av = Ch(),\n      Gl = class {\n        constructor(t, s) {\n          (this.tokens = t), (this.scopes = s);\n        }\n      };\n    Ao.File = Gl;\n    function Pv(e, t, s, i) {\n      if (i && s)\n        throw new Error('Cannot combine flow and typescript plugins.');\n      Eo.initParser.call(void 0, e, t, s, i);\n      let r = Av.parseFile.call(void 0);\n      if (Eo.state.error) throw Eo.augmentError.call(void 0, Eo.state.error);\n      return r;\n    }\n    Ao.parse = Pv;\n  });\n  var wh = Z((zl) => {\n    'use strict';\n    Object.defineProperty(zl, '__esModule', {value: !0});\n    var Nv = It();\n    function Rv(e) {\n      let t = e.currentIndex(),\n        s = 0,\n        i = e.currentToken();\n      do {\n        let r = e.tokens[t];\n        if (\n          (r.isOptionalChainStart && s++,\n          r.isOptionalChainEnd && s--,\n          (s += r.numNullishCoalesceStarts),\n          (s -= r.numNullishCoalesceEnds),\n          r.contextualKeyword === Nv.ContextualKeyword._await &&\n            r.identifierRole == null &&\n            r.scopeDepth === i.scopeDepth)\n        )\n          return !0;\n        t += 1;\n      } while (s > 0 && t < e.tokens.length);\n      return !1;\n    }\n    zl.default = Rv;\n  });\n  var Sh = Z((Yl) => {\n    'use strict';\n    Object.defineProperty(Yl, '__esModule', {value: !0});\n    function Lv(e) {\n      return e && e.__esModule ? e : {default: e};\n    }\n    var Po = be(),\n      Ov = wh(),\n      Dv = Lv(Ov),\n      Xl = class e {\n        __init() {\n          this.resultCode = '';\n        }\n        __init2() {\n          this.resultMappings = new Array(this.tokens.length);\n        }\n        __init3() {\n          this.tokenIndex = 0;\n        }\n        constructor(t, s, i, r, a) {\n          (this.code = t),\n            (this.tokens = s),\n            (this.isFlowEnabled = i),\n            (this.disableESTransforms = r),\n            (this.helperManager = a),\n            e.prototype.__init.call(this),\n            e.prototype.__init2.call(this),\n            e.prototype.__init3.call(this);\n        }\n        snapshot() {\n          return {resultCode: this.resultCode, tokenIndex: this.tokenIndex};\n        }\n        restoreToSnapshot(t) {\n          (this.resultCode = t.resultCode), (this.tokenIndex = t.tokenIndex);\n        }\n        dangerouslyGetAndRemoveCodeSinceSnapshot(t) {\n          let s = this.resultCode.slice(t.resultCode.length);\n          return (this.resultCode = t.resultCode), s;\n        }\n        reset() {\n          (this.resultCode = ''),\n            (this.resultMappings = new Array(this.tokens.length)),\n            (this.tokenIndex = 0);\n        }\n        matchesContextualAtIndex(t, s) {\n          return (\n            this.matches1AtIndex(t, Po.TokenType.name) &&\n            this.tokens[t].contextualKeyword === s\n          );\n        }\n        identifierNameAtIndex(t) {\n          return this.identifierNameForToken(this.tokens[t]);\n        }\n        identifierNameAtRelativeIndex(t) {\n          return this.identifierNameForToken(this.tokenAtRelativeIndex(t));\n        }\n        identifierName() {\n          return this.identifierNameForToken(this.currentToken());\n        }\n        identifierNameForToken(t) {\n          return this.code.slice(t.start, t.end);\n        }\n        rawCodeForToken(t) {\n          return this.code.slice(t.start, t.end);\n        }\n        stringValueAtIndex(t) {\n          return this.stringValueForToken(this.tokens[t]);\n        }\n        stringValue() {\n          return this.stringValueForToken(this.currentToken());\n        }\n        stringValueForToken(t) {\n          return this.code.slice(t.start + 1, t.end - 1);\n        }\n        matches1AtIndex(t, s) {\n          return this.tokens[t].type === s;\n        }\n        matches2AtIndex(t, s, i) {\n          return this.tokens[t].type === s && this.tokens[t + 1].type === i;\n        }\n        matches3AtIndex(t, s, i, r) {\n          return (\n            this.tokens[t].type === s &&\n            this.tokens[t + 1].type === i &&\n            this.tokens[t + 2].type === r\n          );\n        }\n        matches1(t) {\n          return this.tokens[this.tokenIndex].type === t;\n        }\n        matches2(t, s) {\n          return (\n            this.tokens[this.tokenIndex].type === t &&\n            this.tokens[this.tokenIndex + 1].type === s\n          );\n        }\n        matches3(t, s, i) {\n          return (\n            this.tokens[this.tokenIndex].type === t &&\n            this.tokens[this.tokenIndex + 1].type === s &&\n            this.tokens[this.tokenIndex + 2].type === i\n          );\n        }\n        matches4(t, s, i, r) {\n          return (\n            this.tokens[this.tokenIndex].type === t &&\n            this.tokens[this.tokenIndex + 1].type === s &&\n            this.tokens[this.tokenIndex + 2].type === i &&\n            this.tokens[this.tokenIndex + 3].type === r\n          );\n        }\n        matches5(t, s, i, r, a) {\n          return (\n            this.tokens[this.tokenIndex].type === t &&\n            this.tokens[this.tokenIndex + 1].type === s &&\n            this.tokens[this.tokenIndex + 2].type === i &&\n            this.tokens[this.tokenIndex + 3].type === r &&\n            this.tokens[this.tokenIndex + 4].type === a\n          );\n        }\n        matchesContextual(t) {\n          return this.matchesContextualAtIndex(this.tokenIndex, t);\n        }\n        matchesContextIdAndLabel(t, s) {\n          return this.matches1(t) && this.currentToken().contextId === s;\n        }\n        previousWhitespaceAndComments() {\n          let t = this.code.slice(\n            this.tokenIndex > 0 ? this.tokens[this.tokenIndex - 1].end : 0,\n            this.tokenIndex < this.tokens.length\n              ? this.tokens[this.tokenIndex].start\n              : this.code.length\n          );\n          return this.isFlowEnabled && (t = t.replace(/@flow/g, '')), t;\n        }\n        replaceToken(t) {\n          (this.resultCode += this.previousWhitespaceAndComments()),\n            this.appendTokenPrefix(),\n            (this.resultMappings[this.tokenIndex] = this.resultCode.length),\n            (this.resultCode += t),\n            this.appendTokenSuffix(),\n            this.tokenIndex++;\n        }\n        replaceTokenTrimmingLeftWhitespace(t) {\n          (this.resultCode += this.previousWhitespaceAndComments().replace(\n            /[^\\r\\n]/g,\n            ''\n          )),\n            this.appendTokenPrefix(),\n            (this.resultMappings[this.tokenIndex] = this.resultCode.length),\n            (this.resultCode += t),\n            this.appendTokenSuffix(),\n            this.tokenIndex++;\n        }\n        removeInitialToken() {\n          this.replaceToken('');\n        }\n        removeToken() {\n          this.replaceTokenTrimmingLeftWhitespace('');\n        }\n        removeBalancedCode() {\n          let t = 0;\n          for (; !this.isAtEnd(); ) {\n            if (this.matches1(Po.TokenType.braceL)) t++;\n            else if (this.matches1(Po.TokenType.braceR)) {\n              if (t === 0) return;\n              t--;\n            }\n            this.removeToken();\n          }\n        }\n        copyExpectedToken(t) {\n          if (this.tokens[this.tokenIndex].type !== t)\n            throw new Error(`Expected token ${t}`);\n          this.copyToken();\n        }\n        copyToken() {\n          (this.resultCode += this.previousWhitespaceAndComments()),\n            this.appendTokenPrefix(),\n            (this.resultMappings[this.tokenIndex] = this.resultCode.length),\n            (this.resultCode += this.code.slice(\n              this.tokens[this.tokenIndex].start,\n              this.tokens[this.tokenIndex].end\n            )),\n            this.appendTokenSuffix(),\n            this.tokenIndex++;\n        }\n        copyTokenWithPrefix(t) {\n          (this.resultCode += this.previousWhitespaceAndComments()),\n            this.appendTokenPrefix(),\n            (this.resultCode += t),\n            (this.resultMappings[this.tokenIndex] = this.resultCode.length),\n            (this.resultCode += this.code.slice(\n              this.tokens[this.tokenIndex].start,\n              this.tokens[this.tokenIndex].end\n            )),\n            this.appendTokenSuffix(),\n            this.tokenIndex++;\n        }\n        appendTokenPrefix() {\n          let t = this.currentToken();\n          if (\n            ((t.numNullishCoalesceStarts || t.isOptionalChainStart) &&\n              (t.isAsyncOperation = Dv.default.call(void 0, this)),\n            !this.disableESTransforms)\n          ) {\n            if (t.numNullishCoalesceStarts)\n              for (let s = 0; s < t.numNullishCoalesceStarts; s++)\n                t.isAsyncOperation\n                  ? ((this.resultCode += 'await '),\n                    (this.resultCode += this.helperManager.getHelperName(\n                      'asyncNullishCoalesce'\n                    )))\n                  : (this.resultCode +=\n                      this.helperManager.getHelperName('nullishCoalesce')),\n                  (this.resultCode += '(');\n            t.isOptionalChainStart &&\n              (t.isAsyncOperation && (this.resultCode += 'await '),\n              this.tokenIndex > 0 &&\n              this.tokenAtRelativeIndex(-1).type === Po.TokenType._delete\n                ? t.isAsyncOperation\n                  ? (this.resultCode += this.helperManager.getHelperName(\n                      'asyncOptionalChainDelete'\n                    ))\n                  : (this.resultCode += this.helperManager.getHelperName(\n                      'optionalChainDelete'\n                    ))\n                : t.isAsyncOperation\n                ? (this.resultCode +=\n                    this.helperManager.getHelperName('asyncOptionalChain'))\n                : (this.resultCode +=\n                    this.helperManager.getHelperName('optionalChain')),\n              (this.resultCode += '(['));\n          }\n        }\n        appendTokenSuffix() {\n          let t = this.currentToken();\n          if (\n            (t.isOptionalChainEnd &&\n              !this.disableESTransforms &&\n              (this.resultCode += '])'),\n            t.numNullishCoalesceEnds && !this.disableESTransforms)\n          )\n            for (let s = 0; s < t.numNullishCoalesceEnds; s++)\n              this.resultCode += '))';\n        }\n        appendCode(t) {\n          this.resultCode += t;\n        }\n        currentToken() {\n          return this.tokens[this.tokenIndex];\n        }\n        currentTokenCode() {\n          let t = this.currentToken();\n          return this.code.slice(t.start, t.end);\n        }\n        tokenAtRelativeIndex(t) {\n          return this.tokens[this.tokenIndex + t];\n        }\n        currentIndex() {\n          return this.tokenIndex;\n        }\n        nextToken() {\n          if (this.tokenIndex === this.tokens.length)\n            throw new Error('Unexpectedly reached end of input.');\n          this.tokenIndex++;\n        }\n        previousToken() {\n          this.tokenIndex--;\n        }\n        finish() {\n          if (this.tokenIndex !== this.tokens.length)\n            throw new Error(\n              'Tried to finish processing tokens before reaching the end.'\n            );\n          return (\n            (this.resultCode += this.previousWhitespaceAndComments()),\n            {code: this.resultCode, mappings: this.resultMappings}\n          );\n        }\n        isAtEnd() {\n          return this.tokenIndex === this.tokens.length;\n        }\n      };\n    Yl.default = Xl;\n  });\n  var Ah = Z((Ql) => {\n    'use strict';\n    Object.defineProperty(Ql, '__esModule', {value: !0});\n    var Ih = It(),\n      Ne = be();\n    function Mv(e, t, s, i) {\n      let r = t.snapshot(),\n        a = Fv(t),\n        u = [],\n        d = [],\n        y = [],\n        g = null,\n        L = [],\n        p = [],\n        h = t.currentToken().contextId;\n      if (h == null)\n        throw new Error(\n          'Expected non-null class context ID on class open-brace.'\n        );\n      for (t.nextToken(); !t.matchesContextIdAndLabel(Ne.TokenType.braceR, h); )\n        if (\n          t.matchesContextual(Ih.ContextualKeyword._constructor) &&\n          !t.currentToken().isType\n        )\n          ({constructorInitializerStatements: u, constructorInsertPos: g} =\n            Eh(t));\n        else if (t.matches1(Ne.TokenType.semi))\n          i || p.push({start: t.currentIndex(), end: t.currentIndex() + 1}),\n            t.nextToken();\n        else if (t.currentToken().isType) t.nextToken();\n        else {\n          let T = t.currentIndex(),\n            x = !1,\n            w = !1,\n            S = !1;\n          for (; No(t.currentToken()); )\n            t.matches1(Ne.TokenType._static) && (x = !0),\n              t.matches1(Ne.TokenType.hash) && (w = !0),\n              (t.matches1(Ne.TokenType._declare) ||\n                t.matches1(Ne.TokenType._abstract)) &&\n                (S = !0),\n              t.nextToken();\n          if (x && t.matches1(Ne.TokenType.braceL)) {\n            Jl(t, h);\n            continue;\n          }\n          if (w) {\n            Jl(t, h);\n            continue;\n          }\n          if (\n            t.matchesContextual(Ih.ContextualKeyword._constructor) &&\n            !t.currentToken().isType\n          ) {\n            ({constructorInitializerStatements: u, constructorInsertPos: g} =\n              Eh(t));\n            continue;\n          }\n          let A = t.currentIndex();\n          if (\n            (Bv(t),\n            t.matches1(Ne.TokenType.lessThan) ||\n              t.matches1(Ne.TokenType.parenL))\n          ) {\n            Jl(t, h);\n            continue;\n          }\n          for (; t.currentToken().isType; ) t.nextToken();\n          if (t.matches1(Ne.TokenType.eq)) {\n            let U = t.currentIndex(),\n              M = t.currentToken().rhsEndIndex;\n            if (M == null)\n              throw new Error(\n                'Expected rhsEndIndex on class field assignment.'\n              );\n            for (t.nextToken(); t.currentIndex() < M; ) e.processToken();\n            let c;\n            x\n              ? ((c = s.claimFreeName('__initStatic')), y.push(c))\n              : ((c = s.claimFreeName('__init')), d.push(c)),\n              L.push({\n                initializerName: c,\n                equalsIndex: U,\n                start: A,\n                end: t.currentIndex(),\n              });\n          } else (!i || S) && p.push({start: T, end: t.currentIndex()});\n        }\n      return (\n        t.restoreToSnapshot(r),\n        i\n          ? {\n              headerInfo: a,\n              constructorInitializerStatements: u,\n              instanceInitializerNames: [],\n              staticInitializerNames: [],\n              constructorInsertPos: g,\n              fields: [],\n              rangesToRemove: p,\n            }\n          : {\n              headerInfo: a,\n              constructorInitializerStatements: u,\n              instanceInitializerNames: d,\n              staticInitializerNames: y,\n              constructorInsertPos: g,\n              fields: L,\n              rangesToRemove: p,\n            }\n      );\n    }\n    Ql.default = Mv;\n    function Jl(e, t) {\n      for (e.nextToken(); e.currentToken().contextId !== t; ) e.nextToken();\n      for (; No(e.tokenAtRelativeIndex(-1)); ) e.previousToken();\n    }\n    function Fv(e) {\n      let t = e.currentToken(),\n        s = t.contextId;\n      if (s == null) throw new Error('Expected context ID on class token.');\n      let i = t.isExpression;\n      if (i == null) throw new Error('Expected isExpression on class token.');\n      let r = null,\n        a = !1;\n      for (\n        e.nextToken(),\n          e.matches1(Ne.TokenType.name) && (r = e.identifierName());\n        !e.matchesContextIdAndLabel(Ne.TokenType.braceL, s);\n\n      )\n        e.matches1(Ne.TokenType._extends) &&\n          !e.currentToken().isType &&\n          (a = !0),\n          e.nextToken();\n      return {isExpression: i, className: r, hasSuperclass: a};\n    }\n    function Eh(e) {\n      let t = [];\n      e.nextToken();\n      let s = e.currentToken().contextId;\n      if (s == null)\n        throw new Error(\n          'Expected context ID on open-paren starting constructor params.'\n        );\n      for (; !e.matchesContextIdAndLabel(Ne.TokenType.parenR, s); )\n        if (e.currentToken().contextId === s) {\n          if ((e.nextToken(), No(e.currentToken()))) {\n            for (e.nextToken(); No(e.currentToken()); ) e.nextToken();\n            let a = e.currentToken();\n            if (a.type !== Ne.TokenType.name)\n              throw new Error(\n                'Expected identifier after access modifiers in constructor arg.'\n              );\n            let u = e.identifierNameForToken(a);\n            t.push(`this.${u} = ${u}`);\n          }\n        } else e.nextToken();\n      e.nextToken();\n      let i = e.currentIndex(),\n        r = !1;\n      for (; !e.matchesContextIdAndLabel(Ne.TokenType.braceR, s); ) {\n        if (!r && e.matches2(Ne.TokenType._super, Ne.TokenType.parenL)) {\n          e.nextToken();\n          let a = e.currentToken().contextId;\n          if (a == null)\n            throw new Error('Expected a context ID on the super call');\n          for (; !e.matchesContextIdAndLabel(Ne.TokenType.parenR, a); )\n            e.nextToken();\n          (i = e.currentIndex()), (r = !0);\n        }\n        e.nextToken();\n      }\n      return (\n        e.nextToken(),\n        {constructorInitializerStatements: t, constructorInsertPos: i}\n      );\n    }\n    function No(e) {\n      return [\n        Ne.TokenType._async,\n        Ne.TokenType._get,\n        Ne.TokenType._set,\n        Ne.TokenType.plus,\n        Ne.TokenType.minus,\n        Ne.TokenType._readonly,\n        Ne.TokenType._static,\n        Ne.TokenType._public,\n        Ne.TokenType._private,\n        Ne.TokenType._protected,\n        Ne.TokenType._override,\n        Ne.TokenType._abstract,\n        Ne.TokenType.star,\n        Ne.TokenType._declare,\n        Ne.TokenType.hash,\n      ].includes(e.type);\n    }\n    function Bv(e) {\n      if (e.matches1(Ne.TokenType.bracketL)) {\n        let s = e.currentToken().contextId;\n        if (s == null)\n          throw new Error(\n            'Expected class context ID on computed name open bracket.'\n          );\n        for (; !e.matchesContextIdAndLabel(Ne.TokenType.bracketR, s); )\n          e.nextToken();\n        e.nextToken();\n      } else e.nextToken();\n    }\n  });\n  var ec = Z((Zl) => {\n    'use strict';\n    Object.defineProperty(Zl, '__esModule', {value: !0});\n    var Ph = be();\n    function Vv(e) {\n      if (\n        (e.removeInitialToken(),\n        e.removeToken(),\n        e.removeToken(),\n        e.removeToken(),\n        e.matches1(Ph.TokenType.parenL))\n      )\n        e.removeToken(), e.removeToken(), e.removeToken();\n      else\n        for (; e.matches1(Ph.TokenType.dot); ) e.removeToken(), e.removeToken();\n    }\n    Zl.default = Vv;\n  });\n  var tc = Z((Ro) => {\n    'use strict';\n    Object.defineProperty(Ro, '__esModule', {value: !0});\n    var jv = xt(),\n      $v = be(),\n      qv = {typeDeclarations: new Set(), valueDeclarations: new Set()};\n    Ro.EMPTY_DECLARATION_INFO = qv;\n    function Kv(e) {\n      let t = new Set(),\n        s = new Set();\n      for (let i = 0; i < e.tokens.length; i++) {\n        let r = e.tokens[i];\n        r.type === $v.TokenType.name &&\n          jv.isTopLevelDeclaration.call(void 0, r) &&\n          (r.isType\n            ? t.add(e.identifierNameForToken(r))\n            : s.add(e.identifierNameForToken(r)));\n      }\n      return {typeDeclarations: t, valueDeclarations: s};\n    }\n    Ro.default = Kv;\n  });\n  var sc = Z((nc) => {\n    'use strict';\n    Object.defineProperty(nc, '__esModule', {value: !0});\n    var Uv = It(),\n      Nh = be();\n    function Hv(e) {\n      e.matches2(Nh.TokenType.name, Nh.TokenType.braceL) &&\n        e.matchesContextual(Uv.ContextualKeyword._assert) &&\n        (e.removeToken(),\n        e.removeToken(),\n        e.removeBalancedCode(),\n        e.removeToken());\n    }\n    nc.removeMaybeImportAssertion = Hv;\n  });\n  var rc = Z((ic) => {\n    'use strict';\n    Object.defineProperty(ic, '__esModule', {value: !0});\n    var Rh = be();\n    function Wv(e, t, s) {\n      if (!e) return !1;\n      let i = t.currentToken();\n      if (i.rhsEndIndex == null)\n        throw new Error('Expected non-null rhsEndIndex on export token.');\n      let r = i.rhsEndIndex - t.currentIndex();\n      if (\n        r !== 3 &&\n        !(r === 4 && t.matches1AtIndex(i.rhsEndIndex - 1, Rh.TokenType.semi))\n      )\n        return !1;\n      let a = t.tokenAtRelativeIndex(2);\n      if (a.type !== Rh.TokenType.name) return !1;\n      let u = t.identifierNameForToken(a);\n      return s.typeDeclarations.has(u) && !s.valueDeclarations.has(u);\n    }\n    ic.default = Wv;\n  });\n  var Oh = Z((ac) => {\n    'use strict';\n    Object.defineProperty(ac, '__esModule', {value: !0});\n    function ur(e) {\n      return e && e.__esModule ? e : {default: e};\n    }\n    var Lo = xt(),\n      Ls = It(),\n      N = be(),\n      Gv = ec(),\n      zv = ur(Gv),\n      Lh = tc(),\n      Xv = ur(Lh),\n      Yv = Wi(),\n      Jv = ur(Yv),\n      Oo = sc(),\n      Qv = rc(),\n      Zv = ur(Qv),\n      ex = hn(),\n      tx = ur(ex),\n      oc = class e extends tx.default {\n        __init() {\n          this.hadExport = !1;\n        }\n        __init2() {\n          this.hadNamedExport = !1;\n        }\n        __init3() {\n          this.hadDefaultExport = !1;\n        }\n        constructor(t, s, i, r, a, u, d, y, g, L) {\n          super(),\n            (this.rootTransformer = t),\n            (this.tokens = s),\n            (this.importProcessor = i),\n            (this.nameManager = r),\n            (this.helperManager = a),\n            (this.reactHotLoaderTransformer = u),\n            (this.enableLegacyBabel5ModuleInterop = d),\n            (this.enableLegacyTypeScriptModuleInterop = y),\n            (this.isTypeScriptTransformEnabled = g),\n            (this.preserveDynamicImport = L),\n            e.prototype.__init.call(this),\n            e.prototype.__init2.call(this),\n            e.prototype.__init3.call(this),\n            (this.declarationInfo = g\n              ? Xv.default.call(void 0, s)\n              : Lh.EMPTY_DECLARATION_INFO);\n        }\n        getPrefixCode() {\n          let t = '';\n          return (\n            this.hadExport &&\n              (t +=\n                'Object.defineProperty(exports, \"__esModule\", {value: true});'),\n            t\n          );\n        }\n        getSuffixCode() {\n          return this.enableLegacyBabel5ModuleInterop &&\n            this.hadDefaultExport &&\n            !this.hadNamedExport\n            ? `\nmodule.exports = exports.default;\n`\n            : '';\n        }\n        process() {\n          return this.tokens.matches3(\n            N.TokenType._import,\n            N.TokenType.name,\n            N.TokenType.eq\n          )\n            ? this.processImportEquals()\n            : this.tokens.matches1(N.TokenType._import)\n            ? (this.processImport(), !0)\n            : this.tokens.matches2(N.TokenType._export, N.TokenType.eq)\n            ? (this.tokens.replaceToken('module.exports'), !0)\n            : this.tokens.matches1(N.TokenType._export) &&\n              !this.tokens.currentToken().isType\n            ? ((this.hadExport = !0), this.processExport())\n            : this.tokens.matches2(N.TokenType.name, N.TokenType.postIncDec) &&\n              this.processPostIncDec()\n            ? !0\n            : this.tokens.matches1(N.TokenType.name) ||\n              this.tokens.matches1(N.TokenType.jsxName)\n            ? this.processIdentifier()\n            : this.tokens.matches1(N.TokenType.eq)\n            ? this.processAssignment()\n            : this.tokens.matches1(N.TokenType.assign)\n            ? this.processComplexAssignment()\n            : this.tokens.matches1(N.TokenType.preIncDec)\n            ? this.processPreIncDec()\n            : !1;\n        }\n        processImportEquals() {\n          let t = this.tokens.identifierNameAtIndex(\n            this.tokens.currentIndex() + 1\n          );\n          return (\n            this.importProcessor.isTypeName(t)\n              ? zv.default.call(void 0, this.tokens)\n              : this.tokens.replaceToken('const'),\n            !0\n          );\n        }\n        processImport() {\n          if (this.tokens.matches2(N.TokenType._import, N.TokenType.parenL)) {\n            if (this.preserveDynamicImport) {\n              this.tokens.copyToken();\n              return;\n            }\n            let s = this.enableLegacyTypeScriptModuleInterop\n              ? ''\n              : `${this.helperManager.getHelperName(\n                  'interopRequireWildcard'\n                )}(`;\n            this.tokens.replaceToken(\n              `Promise.resolve().then(() => ${s}require`\n            );\n            let i = this.tokens.currentToken().contextId;\n            if (i == null)\n              throw new Error(\n                'Expected context ID on dynamic import invocation.'\n              );\n            for (\n              this.tokens.copyToken();\n              !this.tokens.matchesContextIdAndLabel(N.TokenType.parenR, i);\n\n            )\n              this.rootTransformer.processToken();\n            this.tokens.replaceToken(s ? ')))' : '))');\n            return;\n          }\n          if (this.removeImportAndDetectIfType()) this.tokens.removeToken();\n          else {\n            let s = this.tokens.stringValue();\n            this.tokens.replaceTokenTrimmingLeftWhitespace(\n              this.importProcessor.claimImportCode(s)\n            ),\n              this.tokens.appendCode(this.importProcessor.claimImportCode(s));\n          }\n          Oo.removeMaybeImportAssertion.call(void 0, this.tokens),\n            this.tokens.matches1(N.TokenType.semi) && this.tokens.removeToken();\n        }\n        removeImportAndDetectIfType() {\n          if (\n            (this.tokens.removeInitialToken(),\n            this.tokens.matchesContextual(Ls.ContextualKeyword._type) &&\n              !this.tokens.matches1AtIndex(\n                this.tokens.currentIndex() + 1,\n                N.TokenType.comma\n              ) &&\n              !this.tokens.matchesContextualAtIndex(\n                this.tokens.currentIndex() + 1,\n                Ls.ContextualKeyword._from\n              ))\n          )\n            return this.removeRemainingImport(), !0;\n          if (\n            this.tokens.matches1(N.TokenType.name) ||\n            this.tokens.matches1(N.TokenType.star)\n          )\n            return this.removeRemainingImport(), !1;\n          if (this.tokens.matches1(N.TokenType.string)) return !1;\n          let t = !1;\n          for (; !this.tokens.matches1(N.TokenType.string); )\n            ((!t && this.tokens.matches1(N.TokenType.braceL)) ||\n              this.tokens.matches1(N.TokenType.comma)) &&\n              (this.tokens.removeToken(),\n              (this.tokens.matches2(N.TokenType.name, N.TokenType.comma) ||\n                this.tokens.matches2(N.TokenType.name, N.TokenType.braceR) ||\n                this.tokens.matches4(\n                  N.TokenType.name,\n                  N.TokenType.name,\n                  N.TokenType.name,\n                  N.TokenType.comma\n                ) ||\n                this.tokens.matches4(\n                  N.TokenType.name,\n                  N.TokenType.name,\n                  N.TokenType.name,\n                  N.TokenType.braceR\n                )) &&\n                (t = !0)),\n              this.tokens.removeToken();\n          return !t;\n        }\n        removeRemainingImport() {\n          for (; !this.tokens.matches1(N.TokenType.string); )\n            this.tokens.removeToken();\n        }\n        processIdentifier() {\n          let t = this.tokens.currentToken();\n          if (t.shadowsGlobal) return !1;\n          if (t.identifierRole === Lo.IdentifierRole.ObjectShorthand)\n            return this.processObjectShorthand();\n          if (t.identifierRole !== Lo.IdentifierRole.Access) return !1;\n          let s = this.importProcessor.getIdentifierReplacement(\n            this.tokens.identifierNameForToken(t)\n          );\n          if (!s) return !1;\n          let i = this.tokens.currentIndex() + 1;\n          for (\n            ;\n            i < this.tokens.tokens.length &&\n            this.tokens.tokens[i].type === N.TokenType.parenR;\n\n          )\n            i++;\n          return (\n            this.tokens.tokens[i].type === N.TokenType.parenL\n              ? this.tokens.tokenAtRelativeIndex(1).type ===\n                  N.TokenType.parenL &&\n                this.tokens.tokenAtRelativeIndex(-1).type !== N.TokenType._new\n                ? (this.tokens.replaceToken(`${s}.call(void 0, `),\n                  this.tokens.removeToken(),\n                  this.rootTransformer.processBalancedCode(),\n                  this.tokens.copyExpectedToken(N.TokenType.parenR))\n                : this.tokens.replaceToken(`(0, ${s})`)\n              : this.tokens.replaceToken(s),\n            !0\n          );\n        }\n        processObjectShorthand() {\n          let t = this.tokens.identifierName(),\n            s = this.importProcessor.getIdentifierReplacement(t);\n          return s ? (this.tokens.replaceToken(`${t}: ${s}`), !0) : !1;\n        }\n        processExport() {\n          if (\n            this.tokens.matches2(N.TokenType._export, N.TokenType._enum) ||\n            this.tokens.matches3(\n              N.TokenType._export,\n              N.TokenType._const,\n              N.TokenType._enum\n            )\n          )\n            return !1;\n          if (this.tokens.matches2(N.TokenType._export, N.TokenType._default))\n            return (\n              (this.hadDefaultExport = !0),\n              this.tokens.matches3(\n                N.TokenType._export,\n                N.TokenType._default,\n                N.TokenType._enum\n              )\n                ? !1\n                : (this.processExportDefault(), !0)\n            );\n          if (\n            ((this.hadNamedExport = !0),\n            this.tokens.matches2(N.TokenType._export, N.TokenType._var) ||\n              this.tokens.matches2(N.TokenType._export, N.TokenType._let) ||\n              this.tokens.matches2(N.TokenType._export, N.TokenType._const))\n          )\n            return this.processExportVar(), !0;\n          if (\n            this.tokens.matches2(N.TokenType._export, N.TokenType._function) ||\n            this.tokens.matches3(\n              N.TokenType._export,\n              N.TokenType.name,\n              N.TokenType._function\n            )\n          )\n            return this.processExportFunction(), !0;\n          if (\n            this.tokens.matches2(N.TokenType._export, N.TokenType._class) ||\n            this.tokens.matches3(\n              N.TokenType._export,\n              N.TokenType._abstract,\n              N.TokenType._class\n            ) ||\n            this.tokens.matches2(N.TokenType._export, N.TokenType.at)\n          )\n            return this.processExportClass(), !0;\n          if (this.tokens.matches2(N.TokenType._export, N.TokenType.braceL))\n            return this.processExportBindings(), !0;\n          if (this.tokens.matches2(N.TokenType._export, N.TokenType.star))\n            return this.processExportStar(), !0;\n          if (\n            this.tokens.matches2(N.TokenType._export, N.TokenType.name) &&\n            this.tokens.matchesContextualAtIndex(\n              this.tokens.currentIndex() + 1,\n              Ls.ContextualKeyword._type\n            )\n          ) {\n            if (\n              (this.tokens.removeInitialToken(),\n              this.tokens.removeToken(),\n              this.tokens.matches1(N.TokenType.braceL))\n            ) {\n              for (; !this.tokens.matches1(N.TokenType.braceR); )\n                this.tokens.removeToken();\n              this.tokens.removeToken();\n            } else\n              this.tokens.removeToken(),\n                this.tokens.matches1(N.TokenType._as) &&\n                  (this.tokens.removeToken(), this.tokens.removeToken());\n            return (\n              this.tokens.matchesContextual(Ls.ContextualKeyword._from) &&\n                this.tokens.matches1AtIndex(\n                  this.tokens.currentIndex() + 1,\n                  N.TokenType.string\n                ) &&\n                (this.tokens.removeToken(),\n                this.tokens.removeToken(),\n                Oo.removeMaybeImportAssertion.call(void 0, this.tokens)),\n              !0\n            );\n          } else throw new Error('Unrecognized export syntax.');\n        }\n        processAssignment() {\n          let t = this.tokens.currentIndex(),\n            s = this.tokens.tokens[t - 1];\n          if (\n            s.isType ||\n            s.type !== N.TokenType.name ||\n            s.shadowsGlobal ||\n            (t >= 2 && this.tokens.matches1AtIndex(t - 2, N.TokenType.dot)) ||\n            (t >= 2 &&\n              [N.TokenType._var, N.TokenType._let, N.TokenType._const].includes(\n                this.tokens.tokens[t - 2].type\n              ))\n          )\n            return !1;\n          let i = this.importProcessor.resolveExportBinding(\n            this.tokens.identifierNameForToken(s)\n          );\n          return i\n            ? (this.tokens.copyToken(), this.tokens.appendCode(` ${i} =`), !0)\n            : !1;\n        }\n        processComplexAssignment() {\n          let t = this.tokens.currentIndex(),\n            s = this.tokens.tokens[t - 1];\n          if (\n            s.type !== N.TokenType.name ||\n            s.shadowsGlobal ||\n            (t >= 2 && this.tokens.matches1AtIndex(t - 2, N.TokenType.dot))\n          )\n            return !1;\n          let i = this.importProcessor.resolveExportBinding(\n            this.tokens.identifierNameForToken(s)\n          );\n          return i\n            ? (this.tokens.appendCode(` = ${i}`), this.tokens.copyToken(), !0)\n            : !1;\n        }\n        processPreIncDec() {\n          let t = this.tokens.currentIndex(),\n            s = this.tokens.tokens[t + 1];\n          if (\n            s.type !== N.TokenType.name ||\n            s.shadowsGlobal ||\n            (t + 2 < this.tokens.tokens.length &&\n              (this.tokens.matches1AtIndex(t + 2, N.TokenType.dot) ||\n                this.tokens.matches1AtIndex(t + 2, N.TokenType.bracketL) ||\n                this.tokens.matches1AtIndex(t + 2, N.TokenType.parenL)))\n          )\n            return !1;\n          let i = this.tokens.identifierNameForToken(s),\n            r = this.importProcessor.resolveExportBinding(i);\n          return r\n            ? (this.tokens.appendCode(`${r} = `), this.tokens.copyToken(), !0)\n            : !1;\n        }\n        processPostIncDec() {\n          let t = this.tokens.currentIndex(),\n            s = this.tokens.tokens[t],\n            i = this.tokens.tokens[t + 1];\n          if (\n            s.type !== N.TokenType.name ||\n            s.shadowsGlobal ||\n            (t >= 1 && this.tokens.matches1AtIndex(t - 1, N.TokenType.dot))\n          )\n            return !1;\n          let r = this.tokens.identifierNameForToken(s),\n            a = this.importProcessor.resolveExportBinding(r);\n          if (!a) return !1;\n          let u = this.tokens.rawCodeForToken(i),\n            d = this.importProcessor.getIdentifierReplacement(r) || r;\n          if (u === '++')\n            this.tokens.replaceToken(`(${d} = ${a} = ${d} + 1, ${d} - 1)`);\n          else if (u === '--')\n            this.tokens.replaceToken(`(${d} = ${a} = ${d} - 1, ${d} + 1)`);\n          else throw new Error(`Unexpected operator: ${u}`);\n          return this.tokens.removeToken(), !0;\n        }\n        processExportDefault() {\n          if (\n            this.tokens.matches4(\n              N.TokenType._export,\n              N.TokenType._default,\n              N.TokenType._function,\n              N.TokenType.name\n            ) ||\n            (this.tokens.matches5(\n              N.TokenType._export,\n              N.TokenType._default,\n              N.TokenType.name,\n              N.TokenType._function,\n              N.TokenType.name\n            ) &&\n              this.tokens.matchesContextualAtIndex(\n                this.tokens.currentIndex() + 2,\n                Ls.ContextualKeyword._async\n              ))\n          ) {\n            this.tokens.removeInitialToken(), this.tokens.removeToken();\n            let t = this.processNamedFunction();\n            this.tokens.appendCode(` exports.default = ${t};`);\n          } else if (\n            this.tokens.matches4(\n              N.TokenType._export,\n              N.TokenType._default,\n              N.TokenType._class,\n              N.TokenType.name\n            ) ||\n            this.tokens.matches5(\n              N.TokenType._export,\n              N.TokenType._default,\n              N.TokenType._abstract,\n              N.TokenType._class,\n              N.TokenType.name\n            ) ||\n            this.tokens.matches3(\n              N.TokenType._export,\n              N.TokenType._default,\n              N.TokenType.at\n            )\n          ) {\n            this.tokens.removeInitialToken(),\n              this.tokens.removeToken(),\n              this.copyDecorators(),\n              this.tokens.matches1(N.TokenType._abstract) &&\n                this.tokens.removeToken();\n            let t = this.rootTransformer.processNamedClass();\n            this.tokens.appendCode(` exports.default = ${t};`);\n          } else if (\n            Zv.default.call(\n              void 0,\n              this.isTypeScriptTransformEnabled,\n              this.tokens,\n              this.declarationInfo\n            )\n          )\n            this.tokens.removeInitialToken(),\n              this.tokens.removeToken(),\n              this.tokens.removeToken();\n          else if (this.reactHotLoaderTransformer) {\n            let t = this.nameManager.claimFreeName('_default');\n            this.tokens.replaceToken(`let ${t}; exports.`),\n              this.tokens.copyToken(),\n              this.tokens.appendCode(` = ${t} =`),\n              this.reactHotLoaderTransformer.setExtractedDefaultExportName(t);\n          } else\n            this.tokens.replaceToken('exports.'),\n              this.tokens.copyToken(),\n              this.tokens.appendCode(' =');\n        }\n        copyDecorators() {\n          for (; this.tokens.matches1(N.TokenType.at); )\n            if (\n              (this.tokens.copyToken(),\n              this.tokens.matches1(N.TokenType.parenL))\n            )\n              this.tokens.copyExpectedToken(N.TokenType.parenL),\n                this.rootTransformer.processBalancedCode(),\n                this.tokens.copyExpectedToken(N.TokenType.parenR);\n            else {\n              for (\n                this.tokens.copyExpectedToken(N.TokenType.name);\n                this.tokens.matches1(N.TokenType.dot);\n\n              )\n                this.tokens.copyExpectedToken(N.TokenType.dot),\n                  this.tokens.copyExpectedToken(N.TokenType.name);\n              this.tokens.matches1(N.TokenType.parenL) &&\n                (this.tokens.copyExpectedToken(N.TokenType.parenL),\n                this.rootTransformer.processBalancedCode(),\n                this.tokens.copyExpectedToken(N.TokenType.parenR));\n            }\n        }\n        processExportVar() {\n          this.isSimpleExportVar()\n            ? this.processSimpleExportVar()\n            : this.processComplexExportVar();\n        }\n        isSimpleExportVar() {\n          let t = this.tokens.currentIndex();\n          if ((t++, t++, !this.tokens.matches1AtIndex(t, N.TokenType.name)))\n            return !1;\n          for (\n            t++;\n            t < this.tokens.tokens.length && this.tokens.tokens[t].isType;\n\n          )\n            t++;\n          return !!this.tokens.matches1AtIndex(t, N.TokenType.eq);\n        }\n        processSimpleExportVar() {\n          this.tokens.removeInitialToken(), this.tokens.copyToken();\n          let t = this.tokens.identifierName();\n          for (; !this.tokens.matches1(N.TokenType.eq); )\n            this.rootTransformer.processToken();\n          let s = this.tokens.currentToken().rhsEndIndex;\n          if (s == null) throw new Error('Expected = token with an end index.');\n          for (; this.tokens.currentIndex() < s; )\n            this.rootTransformer.processToken();\n          this.tokens.appendCode(`; exports.${t} = ${t}`);\n        }\n        processComplexExportVar() {\n          this.tokens.removeInitialToken(), this.tokens.removeToken();\n          let t = this.tokens.matches1(N.TokenType.braceL);\n          t && this.tokens.appendCode('(');\n          let s = 0;\n          for (;;)\n            if (\n              this.tokens.matches1(N.TokenType.braceL) ||\n              this.tokens.matches1(N.TokenType.dollarBraceL) ||\n              this.tokens.matches1(N.TokenType.bracketL)\n            )\n              s++, this.tokens.copyToken();\n            else if (\n              this.tokens.matches1(N.TokenType.braceR) ||\n              this.tokens.matches1(N.TokenType.bracketR)\n            )\n              s--, this.tokens.copyToken();\n            else {\n              if (\n                s === 0 &&\n                !this.tokens.matches1(N.TokenType.name) &&\n                !this.tokens.currentToken().isType\n              )\n                break;\n              if (this.tokens.matches1(N.TokenType.eq)) {\n                let i = this.tokens.currentToken().rhsEndIndex;\n                if (i == null)\n                  throw new Error('Expected = token with an end index.');\n                for (; this.tokens.currentIndex() < i; )\n                  this.rootTransformer.processToken();\n              } else {\n                let i = this.tokens.currentToken();\n                if (Lo.isDeclaration.call(void 0, i)) {\n                  let r = this.tokens.identifierName(),\n                    a = this.importProcessor.getIdentifierReplacement(r);\n                  if (a === null)\n                    throw new Error(\n                      `Expected a replacement for ${r} in \\`export var\\` syntax.`\n                    );\n                  Lo.isObjectShorthandDeclaration.call(void 0, i) &&\n                    (a = `${r}: ${a}`),\n                    this.tokens.replaceToken(a);\n                } else this.rootTransformer.processToken();\n              }\n            }\n          if (t) {\n            let i = this.tokens.currentToken().rhsEndIndex;\n            if (i == null)\n              throw new Error('Expected = token with an end index.');\n            for (; this.tokens.currentIndex() < i; )\n              this.rootTransformer.processToken();\n            this.tokens.appendCode(')');\n          }\n        }\n        processExportFunction() {\n          this.tokens.replaceToken('');\n          let t = this.processNamedFunction();\n          this.tokens.appendCode(` exports.${t} = ${t};`);\n        }\n        processNamedFunction() {\n          if (this.tokens.matches1(N.TokenType._function))\n            this.tokens.copyToken();\n          else if (\n            this.tokens.matches2(N.TokenType.name, N.TokenType._function)\n          ) {\n            if (!this.tokens.matchesContextual(Ls.ContextualKeyword._async))\n              throw new Error('Expected async keyword in function export.');\n            this.tokens.copyToken(), this.tokens.copyToken();\n          }\n          if (\n            (this.tokens.matches1(N.TokenType.star) && this.tokens.copyToken(),\n            !this.tokens.matches1(N.TokenType.name))\n          )\n            throw new Error('Expected identifier for exported function name.');\n          let t = this.tokens.identifierName();\n          if ((this.tokens.copyToken(), this.tokens.currentToken().isType))\n            for (\n              this.tokens.removeInitialToken();\n              this.tokens.currentToken().isType;\n\n            )\n              this.tokens.removeToken();\n          return (\n            this.tokens.copyExpectedToken(N.TokenType.parenL),\n            this.rootTransformer.processBalancedCode(),\n            this.tokens.copyExpectedToken(N.TokenType.parenR),\n            this.rootTransformer.processPossibleTypeRange(),\n            this.tokens.copyExpectedToken(N.TokenType.braceL),\n            this.rootTransformer.processBalancedCode(),\n            this.tokens.copyExpectedToken(N.TokenType.braceR),\n            t\n          );\n        }\n        processExportClass() {\n          this.tokens.removeInitialToken(),\n            this.copyDecorators(),\n            this.tokens.matches1(N.TokenType._abstract) &&\n              this.tokens.removeToken();\n          let t = this.rootTransformer.processNamedClass();\n          this.tokens.appendCode(` exports.${t} = ${t};`);\n        }\n        processExportBindings() {\n          this.tokens.removeInitialToken(), this.tokens.removeToken();\n          let t = [];\n          for (;;) {\n            if (this.tokens.matches1(N.TokenType.braceR)) {\n              this.tokens.removeToken();\n              break;\n            }\n            let s = Jv.default.call(void 0, this.tokens);\n            for (; this.tokens.currentIndex() < s.endIndex; )\n              this.tokens.removeToken();\n            if (!s.isType && !this.shouldElideExportedIdentifier(s.leftName)) {\n              let i = s.leftName,\n                r = s.rightName,\n                a = this.importProcessor.getIdentifierReplacement(i);\n              t.push(`exports.${r} = ${a || i};`);\n            }\n            if (this.tokens.matches1(N.TokenType.braceR)) {\n              this.tokens.removeToken();\n              break;\n            }\n            if (this.tokens.matches2(N.TokenType.comma, N.TokenType.braceR)) {\n              this.tokens.removeToken(), this.tokens.removeToken();\n              break;\n            } else if (this.tokens.matches1(N.TokenType.comma))\n              this.tokens.removeToken();\n            else\n              throw new Error(\n                `Unexpected token: ${JSON.stringify(\n                  this.tokens.currentToken()\n                )}`\n              );\n          }\n          if (this.tokens.matchesContextual(Ls.ContextualKeyword._from)) {\n            this.tokens.removeToken();\n            let s = this.tokens.stringValue();\n            this.tokens.replaceTokenTrimmingLeftWhitespace(\n              this.importProcessor.claimImportCode(s)\n            ),\n              Oo.removeMaybeImportAssertion.call(void 0, this.tokens);\n          } else this.tokens.appendCode(t.join(' '));\n          this.tokens.matches1(N.TokenType.semi) && this.tokens.removeToken();\n        }\n        processExportStar() {\n          for (\n            this.tokens.removeInitialToken();\n            !this.tokens.matches1(N.TokenType.string);\n\n          )\n            this.tokens.removeToken();\n          let t = this.tokens.stringValue();\n          this.tokens.replaceTokenTrimmingLeftWhitespace(\n            this.importProcessor.claimImportCode(t)\n          ),\n            Oo.removeMaybeImportAssertion.call(void 0, this.tokens),\n            this.tokens.matches1(N.TokenType.semi) && this.tokens.removeToken();\n        }\n        shouldElideExportedIdentifier(t) {\n          return (\n            this.isTypeScriptTransformEnabled &&\n            !this.declarationInfo.valueDeclarations.has(t)\n          );\n        }\n      };\n    ac.default = oc;\n  });\n  var Bh = Z((cc) => {\n    'use strict';\n    Object.defineProperty(cc, '__esModule', {value: !0});\n    function pr(e) {\n      return e && e.__esModule ? e : {default: e};\n    }\n    var Jn = It(),\n      se = be(),\n      nx = ec(),\n      sx = pr(nx),\n      Fh = tc(),\n      ix = pr(Fh),\n      rx = Wi(),\n      Dh = pr(rx),\n      ox = Fa(),\n      Mh = sc(),\n      ax = rc(),\n      lx = pr(ax),\n      cx = hn(),\n      ux = pr(cx),\n      lc = class extends ux.default {\n        constructor(t, s, i, r, a, u) {\n          super(),\n            (this.tokens = t),\n            (this.nameManager = s),\n            (this.helperManager = i),\n            (this.reactHotLoaderTransformer = r),\n            (this.isTypeScriptTransformEnabled = a),\n            (this.nonTypeIdentifiers = a\n              ? ox.getNonTypeIdentifiers.call(void 0, t, u)\n              : new Set()),\n            (this.declarationInfo = a\n              ? ix.default.call(void 0, t)\n              : Fh.EMPTY_DECLARATION_INFO),\n            (this.injectCreateRequireForImportRequire =\n              !!u.injectCreateRequireForImportRequire);\n        }\n        process() {\n          if (\n            this.tokens.matches3(\n              se.TokenType._import,\n              se.TokenType.name,\n              se.TokenType.eq\n            )\n          )\n            return this.processImportEquals();\n          if (\n            this.tokens.matches4(\n              se.TokenType._import,\n              se.TokenType.name,\n              se.TokenType.name,\n              se.TokenType.eq\n            ) &&\n            this.tokens.matchesContextualAtIndex(\n              this.tokens.currentIndex() + 1,\n              Jn.ContextualKeyword._type\n            )\n          ) {\n            this.tokens.removeInitialToken();\n            for (let t = 0; t < 7; t++) this.tokens.removeToken();\n            return !0;\n          }\n          if (this.tokens.matches2(se.TokenType._export, se.TokenType.eq))\n            return this.tokens.replaceToken('module.exports'), !0;\n          if (\n            this.tokens.matches5(\n              se.TokenType._export,\n              se.TokenType._import,\n              se.TokenType.name,\n              se.TokenType.name,\n              se.TokenType.eq\n            ) &&\n            this.tokens.matchesContextualAtIndex(\n              this.tokens.currentIndex() + 2,\n              Jn.ContextualKeyword._type\n            )\n          ) {\n            this.tokens.removeInitialToken();\n            for (let t = 0; t < 8; t++) this.tokens.removeToken();\n            return !0;\n          }\n          if (this.tokens.matches1(se.TokenType._import))\n            return this.processImport();\n          if (this.tokens.matches2(se.TokenType._export, se.TokenType._default))\n            return this.processExportDefault();\n          if (this.tokens.matches2(se.TokenType._export, se.TokenType.braceL))\n            return this.processNamedExports();\n          if (\n            this.tokens.matches2(se.TokenType._export, se.TokenType.name) &&\n            this.tokens.matchesContextualAtIndex(\n              this.tokens.currentIndex() + 1,\n              Jn.ContextualKeyword._type\n            )\n          ) {\n            if (\n              (this.tokens.removeInitialToken(),\n              this.tokens.removeToken(),\n              this.tokens.matches1(se.TokenType.braceL))\n            ) {\n              for (; !this.tokens.matches1(se.TokenType.braceR); )\n                this.tokens.removeToken();\n              this.tokens.removeToken();\n            } else\n              this.tokens.removeToken(),\n                this.tokens.matches1(se.TokenType._as) &&\n                  (this.tokens.removeToken(), this.tokens.removeToken());\n            return (\n              this.tokens.matchesContextual(Jn.ContextualKeyword._from) &&\n                this.tokens.matches1AtIndex(\n                  this.tokens.currentIndex() + 1,\n                  se.TokenType.string\n                ) &&\n                (this.tokens.removeToken(),\n                this.tokens.removeToken(),\n                Mh.removeMaybeImportAssertion.call(void 0, this.tokens)),\n              !0\n            );\n          }\n          return !1;\n        }\n        processImportEquals() {\n          let t = this.tokens.identifierNameAtIndex(\n            this.tokens.currentIndex() + 1\n          );\n          return (\n            this.isTypeName(t)\n              ? sx.default.call(void 0, this.tokens)\n              : this.injectCreateRequireForImportRequire\n              ? (this.tokens.replaceToken('const'),\n                this.tokens.copyToken(),\n                this.tokens.copyToken(),\n                this.tokens.replaceToken(\n                  this.helperManager.getHelperName('require')\n                ))\n              : this.tokens.replaceToken('const'),\n            !0\n          );\n        }\n        processImport() {\n          if (this.tokens.matches2(se.TokenType._import, se.TokenType.parenL))\n            return !1;\n          let t = this.tokens.snapshot();\n          if (this.removeImportTypeBindings()) {\n            for (\n              this.tokens.restoreToSnapshot(t);\n              !this.tokens.matches1(se.TokenType.string);\n\n            )\n              this.tokens.removeToken();\n            this.tokens.removeToken(),\n              Mh.removeMaybeImportAssertion.call(void 0, this.tokens),\n              this.tokens.matches1(se.TokenType.semi) &&\n                this.tokens.removeToken();\n          }\n          return !0;\n        }\n        removeImportTypeBindings() {\n          if (\n            (this.tokens.copyExpectedToken(se.TokenType._import),\n            this.tokens.matchesContextual(Jn.ContextualKeyword._type) &&\n              !this.tokens.matches1AtIndex(\n                this.tokens.currentIndex() + 1,\n                se.TokenType.comma\n              ) &&\n              !this.tokens.matchesContextualAtIndex(\n                this.tokens.currentIndex() + 1,\n                Jn.ContextualKeyword._from\n              ))\n          )\n            return !0;\n          if (this.tokens.matches1(se.TokenType.string))\n            return this.tokens.copyToken(), !1;\n          this.tokens.matchesContextual(Jn.ContextualKeyword._module) &&\n            this.tokens.matchesContextualAtIndex(\n              this.tokens.currentIndex() + 2,\n              Jn.ContextualKeyword._from\n            ) &&\n            this.tokens.copyToken();\n          let t = !1,\n            s = !1;\n          if (\n            (this.tokens.matches1(se.TokenType.name) &&\n              (this.isTypeName(this.tokens.identifierName())\n                ? (this.tokens.removeToken(),\n                  this.tokens.matches1(se.TokenType.comma) &&\n                    this.tokens.removeToken())\n                : ((t = !0),\n                  this.tokens.copyToken(),\n                  this.tokens.matches1(se.TokenType.comma) &&\n                    ((s = !0), this.tokens.removeToken()))),\n            this.tokens.matches1(se.TokenType.star))\n          )\n            this.isTypeName(this.tokens.identifierNameAtRelativeIndex(2))\n              ? (this.tokens.removeToken(),\n                this.tokens.removeToken(),\n                this.tokens.removeToken())\n              : (s && this.tokens.appendCode(','),\n                (t = !0),\n                this.tokens.copyExpectedToken(se.TokenType.star),\n                this.tokens.copyExpectedToken(se.TokenType.name),\n                this.tokens.copyExpectedToken(se.TokenType.name));\n          else if (this.tokens.matches1(se.TokenType.braceL)) {\n            for (\n              s && this.tokens.appendCode(','), this.tokens.copyToken();\n              !this.tokens.matches1(se.TokenType.braceR);\n\n            ) {\n              let i = Dh.default.call(void 0, this.tokens);\n              if (i.isType || this.isTypeName(i.rightName)) {\n                for (; this.tokens.currentIndex() < i.endIndex; )\n                  this.tokens.removeToken();\n                this.tokens.matches1(se.TokenType.comma) &&\n                  this.tokens.removeToken();\n              } else {\n                for (t = !0; this.tokens.currentIndex() < i.endIndex; )\n                  this.tokens.copyToken();\n                this.tokens.matches1(se.TokenType.comma) &&\n                  this.tokens.copyToken();\n              }\n            }\n            this.tokens.copyExpectedToken(se.TokenType.braceR);\n          }\n          return !t;\n        }\n        isTypeName(t) {\n          return (\n            this.isTypeScriptTransformEnabled && !this.nonTypeIdentifiers.has(t)\n          );\n        }\n        processExportDefault() {\n          if (\n            lx.default.call(\n              void 0,\n              this.isTypeScriptTransformEnabled,\n              this.tokens,\n              this.declarationInfo\n            )\n          )\n            return (\n              this.tokens.removeInitialToken(),\n              this.tokens.removeToken(),\n              this.tokens.removeToken(),\n              !0\n            );\n          if (\n            !(\n              this.tokens.matches4(\n                se.TokenType._export,\n                se.TokenType._default,\n                se.TokenType._function,\n                se.TokenType.name\n              ) ||\n              (this.tokens.matches5(\n                se.TokenType._export,\n                se.TokenType._default,\n                se.TokenType.name,\n                se.TokenType._function,\n                se.TokenType.name\n              ) &&\n                this.tokens.matchesContextualAtIndex(\n                  this.tokens.currentIndex() + 2,\n                  Jn.ContextualKeyword._async\n                )) ||\n              this.tokens.matches4(\n                se.TokenType._export,\n                se.TokenType._default,\n                se.TokenType._class,\n                se.TokenType.name\n              ) ||\n              this.tokens.matches5(\n                se.TokenType._export,\n                se.TokenType._default,\n                se.TokenType._abstract,\n                se.TokenType._class,\n                se.TokenType.name\n              )\n            ) &&\n            this.reactHotLoaderTransformer\n          ) {\n            let s = this.nameManager.claimFreeName('_default');\n            return (\n              this.tokens.replaceToken(`let ${s}; export`),\n              this.tokens.copyToken(),\n              this.tokens.appendCode(` ${s} =`),\n              this.reactHotLoaderTransformer.setExtractedDefaultExportName(s),\n              !0\n            );\n          }\n          return !1;\n        }\n        processNamedExports() {\n          if (!this.isTypeScriptTransformEnabled) return !1;\n          for (\n            this.tokens.copyExpectedToken(se.TokenType._export),\n              this.tokens.copyExpectedToken(se.TokenType.braceL);\n            !this.tokens.matches1(se.TokenType.braceR);\n\n          ) {\n            let t = Dh.default.call(void 0, this.tokens);\n            if (t.isType || this.shouldElideExportedName(t.leftName)) {\n              for (; this.tokens.currentIndex() < t.endIndex; )\n                this.tokens.removeToken();\n              this.tokens.matches1(se.TokenType.comma) &&\n                this.tokens.removeToken();\n            } else {\n              for (; this.tokens.currentIndex() < t.endIndex; )\n                this.tokens.copyToken();\n              this.tokens.matches1(se.TokenType.comma) &&\n                this.tokens.copyToken();\n            }\n          }\n          return this.tokens.copyExpectedToken(se.TokenType.braceR), !0;\n        }\n        shouldElideExportedName(t) {\n          return (\n            this.isTypeScriptTransformEnabled &&\n            this.declarationInfo.typeDeclarations.has(t) &&\n            !this.declarationInfo.valueDeclarations.has(t)\n          );\n        }\n      };\n    cc.default = lc;\n  });\n  var jh = Z((pc) => {\n    'use strict';\n    Object.defineProperty(pc, '__esModule', {value: !0});\n    function px(e) {\n      return e && e.__esModule ? e : {default: e};\n    }\n    var Vh = It(),\n      sn = be(),\n      hx = hn(),\n      fx = px(hx),\n      uc = class extends fx.default {\n        constructor(t, s, i) {\n          super(),\n            (this.rootTransformer = t),\n            (this.tokens = s),\n            (this.isImportsTransformEnabled = i);\n        }\n        process() {\n          return this.rootTransformer.processPossibleArrowParamEnd() ||\n            this.rootTransformer.processPossibleAsyncArrowWithTypeParams() ||\n            this.rootTransformer.processPossibleTypeRange()\n            ? !0\n            : this.tokens.matches1(sn.TokenType._enum)\n            ? (this.processEnum(), !0)\n            : this.tokens.matches2(sn.TokenType._export, sn.TokenType._enum)\n            ? (this.processNamedExportEnum(), !0)\n            : this.tokens.matches3(\n                sn.TokenType._export,\n                sn.TokenType._default,\n                sn.TokenType._enum\n              )\n            ? (this.processDefaultExportEnum(), !0)\n            : !1;\n        }\n        processNamedExportEnum() {\n          if (this.isImportsTransformEnabled) {\n            this.tokens.removeInitialToken();\n            let t = this.tokens.identifierNameAtRelativeIndex(1);\n            this.processEnum(), this.tokens.appendCode(` exports.${t} = ${t};`);\n          } else this.tokens.copyToken(), this.processEnum();\n        }\n        processDefaultExportEnum() {\n          this.tokens.removeInitialToken(), this.tokens.removeToken();\n          let t = this.tokens.identifierNameAtRelativeIndex(1);\n          this.processEnum(),\n            this.isImportsTransformEnabled\n              ? this.tokens.appendCode(` exports.default = ${t};`)\n              : this.tokens.appendCode(` export default ${t};`);\n        }\n        processEnum() {\n          this.tokens.replaceToken('const'),\n            this.tokens.copyExpectedToken(sn.TokenType.name);\n          let t = !1;\n          this.tokens.matchesContextual(Vh.ContextualKeyword._of) &&\n            (this.tokens.removeToken(),\n            (t = this.tokens.matchesContextual(Vh.ContextualKeyword._symbol)),\n            this.tokens.removeToken());\n          let s = this.tokens.matches3(\n            sn.TokenType.braceL,\n            sn.TokenType.name,\n            sn.TokenType.eq\n          );\n          this.tokens.appendCode(' = require(\"flow-enums-runtime\")');\n          let i = !t && !s;\n          for (\n            this.tokens.replaceTokenTrimmingLeftWhitespace(\n              i ? '.Mirrored([' : '({'\n            );\n            !this.tokens.matches1(sn.TokenType.braceR);\n\n          ) {\n            if (this.tokens.matches1(sn.TokenType.ellipsis)) {\n              this.tokens.removeToken();\n              break;\n            }\n            this.processEnumElement(t, s),\n              this.tokens.matches1(sn.TokenType.comma) &&\n                this.tokens.copyToken();\n          }\n          this.tokens.replaceToken(i ? ']);' : '});');\n        }\n        processEnumElement(t, s) {\n          if (t) {\n            let i = this.tokens.identifierName();\n            this.tokens.copyToken(), this.tokens.appendCode(`: Symbol(\"${i}\")`);\n          } else\n            s\n              ? (this.tokens.copyToken(),\n                this.tokens.replaceTokenTrimmingLeftWhitespace(':'),\n                this.tokens.copyToken())\n              : this.tokens.replaceToken(`\"${this.tokens.identifierName()}\"`);\n        }\n      };\n    pc.default = uc;\n  });\n  var $h = Z((fc) => {\n    'use strict';\n    Object.defineProperty(fc, '__esModule', {value: !0});\n    function dx(e) {\n      return e && e.__esModule ? e : {default: e};\n    }\n    function mx(e) {\n      let t,\n        s = e[0],\n        i = 1;\n      for (; i < e.length; ) {\n        let r = e[i],\n          a = e[i + 1];\n        if (\n          ((i += 2),\n          (r === 'optionalAccess' || r === 'optionalCall') && s == null)\n        )\n          return;\n        r === 'access' || r === 'optionalAccess'\n          ? ((t = s), (s = a(s)))\n          : (r === 'call' || r === 'optionalCall') &&\n            ((s = a((...u) => s.call(t, ...u))), (t = void 0));\n      }\n      return s;\n    }\n    var Qn = be(),\n      yx = hn(),\n      Tx = dx(yx),\n      Do = 'jest',\n      kx = ['mock', 'unmock', 'enableAutomock', 'disableAutomock'],\n      hc = class e extends Tx.default {\n        __init() {\n          this.hoistedFunctionNames = [];\n        }\n        constructor(t, s, i, r) {\n          super(),\n            (this.rootTransformer = t),\n            (this.tokens = s),\n            (this.nameManager = i),\n            (this.importProcessor = r),\n            e.prototype.__init.call(this);\n        }\n        process() {\n          return this.tokens.currentToken().scopeDepth === 0 &&\n            this.tokens.matches4(\n              Qn.TokenType.name,\n              Qn.TokenType.dot,\n              Qn.TokenType.name,\n              Qn.TokenType.parenL\n            ) &&\n            this.tokens.identifierName() === Do\n            ? mx([\n                this,\n                'access',\n                (t) => t.importProcessor,\n                'optionalAccess',\n                (t) => t.getGlobalNames,\n                'call',\n                (t) => t(),\n                'optionalAccess',\n                (t) => t.has,\n                'call',\n                (t) => t(Do),\n              ])\n              ? !1\n              : this.extractHoistedCalls()\n            : !1;\n        }\n        getHoistedCode() {\n          return this.hoistedFunctionNames.length > 0\n            ? this.hoistedFunctionNames.map((t) => `${t}();`).join('')\n            : '';\n        }\n        extractHoistedCalls() {\n          this.tokens.removeToken();\n          let t = !1;\n          for (\n            ;\n            this.tokens.matches3(\n              Qn.TokenType.dot,\n              Qn.TokenType.name,\n              Qn.TokenType.parenL\n            );\n\n          ) {\n            let s = this.tokens.identifierNameAtIndex(\n              this.tokens.currentIndex() + 1\n            );\n            if (kx.includes(s)) {\n              let r = this.nameManager.claimFreeName('__jestHoist');\n              this.hoistedFunctionNames.push(r),\n                this.tokens.replaceToken(`function ${r}(){${Do}.`),\n                this.tokens.copyToken(),\n                this.tokens.copyToken(),\n                this.rootTransformer.processBalancedCode(),\n                this.tokens.copyExpectedToken(Qn.TokenType.parenR),\n                this.tokens.appendCode(';}'),\n                (t = !1);\n            } else\n              t ? this.tokens.copyToken() : this.tokens.replaceToken(`${Do}.`),\n                this.tokens.copyToken(),\n                this.tokens.copyToken(),\n                this.rootTransformer.processBalancedCode(),\n                this.tokens.copyExpectedToken(Qn.TokenType.parenR),\n                (t = !0);\n          }\n          return !0;\n        }\n      };\n    fc.default = hc;\n  });\n  var qh = Z((mc) => {\n    'use strict';\n    Object.defineProperty(mc, '__esModule', {value: !0});\n    function vx(e) {\n      return e && e.__esModule ? e : {default: e};\n    }\n    var xx = be(),\n      gx = hn(),\n      _x = vx(gx),\n      dc = class extends _x.default {\n        constructor(t) {\n          super(), (this.tokens = t);\n        }\n        process() {\n          if (this.tokens.matches1(xx.TokenType.num)) {\n            let t = this.tokens.currentTokenCode();\n            if (t.includes('_'))\n              return this.tokens.replaceToken(t.replace(/_/g, '')), !0;\n          }\n          return !1;\n        }\n      };\n    mc.default = dc;\n  });\n  var Uh = Z((Tc) => {\n    'use strict';\n    Object.defineProperty(Tc, '__esModule', {value: !0});\n    function bx(e) {\n      return e && e.__esModule ? e : {default: e};\n    }\n    var Kh = be(),\n      Cx = hn(),\n      wx = bx(Cx),\n      yc = class extends wx.default {\n        constructor(t, s) {\n          super(), (this.tokens = t), (this.nameManager = s);\n        }\n        process() {\n          return this.tokens.matches2(Kh.TokenType._catch, Kh.TokenType.braceL)\n            ? (this.tokens.copyToken(),\n              this.tokens.appendCode(\n                ` (${this.nameManager.claimFreeName('e')})`\n              ),\n              !0)\n            : !1;\n        }\n      };\n    Tc.default = yc;\n  });\n  var Hh = Z((vc) => {\n    'use strict';\n    Object.defineProperty(vc, '__esModule', {value: !0});\n    function Sx(e) {\n      return e && e.__esModule ? e : {default: e};\n    }\n    var jt = be(),\n      Ix = hn(),\n      Ex = Sx(Ix),\n      kc = class extends Ex.default {\n        constructor(t, s) {\n          super(), (this.tokens = t), (this.nameManager = s);\n        }\n        process() {\n          if (this.tokens.matches1(jt.TokenType.nullishCoalescing)) {\n            let i = this.tokens.currentToken();\n            return (\n              this.tokens.tokens[i.nullishStartIndex].isAsyncOperation\n                ? this.tokens.replaceTokenTrimmingLeftWhitespace(\n                    ', async () => ('\n                  )\n                : this.tokens.replaceTokenTrimmingLeftWhitespace(', () => ('),\n              !0\n            );\n          }\n          if (\n            this.tokens.matches1(jt.TokenType._delete) &&\n            this.tokens.tokenAtRelativeIndex(1).isOptionalChainStart\n          )\n            return this.tokens.removeInitialToken(), !0;\n          let s = this.tokens.currentToken().subscriptStartIndex;\n          if (\n            s != null &&\n            this.tokens.tokens[s].isOptionalChainStart &&\n            this.tokens.tokenAtRelativeIndex(-1).type !== jt.TokenType._super\n          ) {\n            let i = this.nameManager.claimFreeName('_'),\n              r;\n            if (\n              (s > 0 &&\n              this.tokens.matches1AtIndex(s - 1, jt.TokenType._delete) &&\n              this.isLastSubscriptInChain()\n                ? (r = `${i} => delete ${i}`)\n                : (r = `${i} => ${i}`),\n              this.tokens.tokens[s].isAsyncOperation && (r = `async ${r}`),\n              this.tokens.matches2(\n                jt.TokenType.questionDot,\n                jt.TokenType.parenL\n              ) ||\n                this.tokens.matches2(\n                  jt.TokenType.questionDot,\n                  jt.TokenType.lessThan\n                ))\n            )\n              this.justSkippedSuper() && this.tokens.appendCode('.bind(this)'),\n                this.tokens.replaceTokenTrimmingLeftWhitespace(\n                  `, 'optionalCall', ${r}`\n                );\n            else if (\n              this.tokens.matches2(\n                jt.TokenType.questionDot,\n                jt.TokenType.bracketL\n              )\n            )\n              this.tokens.replaceTokenTrimmingLeftWhitespace(\n                `, 'optionalAccess', ${r}`\n              );\n            else if (this.tokens.matches1(jt.TokenType.questionDot))\n              this.tokens.replaceTokenTrimmingLeftWhitespace(\n                `, 'optionalAccess', ${r}.`\n              );\n            else if (this.tokens.matches1(jt.TokenType.dot))\n              this.tokens.replaceTokenTrimmingLeftWhitespace(\n                `, 'access', ${r}.`\n              );\n            else if (this.tokens.matches1(jt.TokenType.bracketL))\n              this.tokens.replaceTokenTrimmingLeftWhitespace(\n                `, 'access', ${r}[`\n              );\n            else if (this.tokens.matches1(jt.TokenType.parenL))\n              this.justSkippedSuper() && this.tokens.appendCode('.bind(this)'),\n                this.tokens.replaceTokenTrimmingLeftWhitespace(\n                  `, 'call', ${r}(`\n                );\n            else\n              throw new Error(\n                'Unexpected subscript operator in optional chain.'\n              );\n            return !0;\n          }\n          return !1;\n        }\n        isLastSubscriptInChain() {\n          let t = 0;\n          for (let s = this.tokens.currentIndex() + 1; ; s++) {\n            if (s >= this.tokens.tokens.length)\n              throw new Error(\n                'Reached the end of the code while finding the end of the access chain.'\n              );\n            if (\n              (this.tokens.tokens[s].isOptionalChainStart\n                ? t++\n                : this.tokens.tokens[s].isOptionalChainEnd && t--,\n              t < 0)\n            )\n              return !0;\n            if (t === 0 && this.tokens.tokens[s].subscriptStartIndex != null)\n              return !1;\n          }\n        }\n        justSkippedSuper() {\n          let t = 0,\n            s = this.tokens.currentIndex() - 1;\n          for (;;) {\n            if (s < 0)\n              throw new Error(\n                'Reached the start of the code while finding the start of the access chain.'\n              );\n            if (\n              (this.tokens.tokens[s].isOptionalChainStart\n                ? t--\n                : this.tokens.tokens[s].isOptionalChainEnd && t++,\n              t < 0)\n            )\n              return !1;\n            if (t === 0 && this.tokens.tokens[s].subscriptStartIndex != null)\n              return this.tokens.tokens[s - 1].type === jt.TokenType._super;\n            s--;\n          }\n        }\n      };\n    vc.default = kc;\n  });\n  var Gh = Z((gc) => {\n    'use strict';\n    Object.defineProperty(gc, '__esModule', {value: !0});\n    function Ax(e) {\n      return e && e.__esModule ? e : {default: e};\n    }\n    var Wh = xt(),\n      Et = be(),\n      Px = hn(),\n      Nx = Ax(Px),\n      xc = class extends Nx.default {\n        constructor(t, s, i, r) {\n          super(),\n            (this.rootTransformer = t),\n            (this.tokens = s),\n            (this.importProcessor = i),\n            (this.options = r);\n        }\n        process() {\n          let t = this.tokens.currentIndex();\n          if (this.tokens.identifierName() === 'createReactClass') {\n            let s =\n              this.importProcessor &&\n              this.importProcessor.getIdentifierReplacement('createReactClass');\n            return (\n              s\n                ? this.tokens.replaceToken(`(0, ${s})`)\n                : this.tokens.copyToken(),\n              this.tryProcessCreateClassCall(t),\n              !0\n            );\n          }\n          if (\n            this.tokens.matches3(\n              Et.TokenType.name,\n              Et.TokenType.dot,\n              Et.TokenType.name\n            ) &&\n            this.tokens.identifierName() === 'React' &&\n            this.tokens.identifierNameAtIndex(\n              this.tokens.currentIndex() + 2\n            ) === 'createClass'\n          ) {\n            let s =\n              (this.importProcessor &&\n                this.importProcessor.getIdentifierReplacement('React')) ||\n              'React';\n            return (\n              s\n                ? (this.tokens.replaceToken(s),\n                  this.tokens.copyToken(),\n                  this.tokens.copyToken())\n                : (this.tokens.copyToken(),\n                  this.tokens.copyToken(),\n                  this.tokens.copyToken()),\n              this.tryProcessCreateClassCall(t),\n              !0\n            );\n          }\n          return !1;\n        }\n        tryProcessCreateClassCall(t) {\n          let s = this.findDisplayName(t);\n          s &&\n            this.classNeedsDisplayName() &&\n            (this.tokens.copyExpectedToken(Et.TokenType.parenL),\n            this.tokens.copyExpectedToken(Et.TokenType.braceL),\n            this.tokens.appendCode(`displayName: '${s}',`),\n            this.rootTransformer.processBalancedCode(),\n            this.tokens.copyExpectedToken(Et.TokenType.braceR),\n            this.tokens.copyExpectedToken(Et.TokenType.parenR));\n        }\n        findDisplayName(t) {\n          return t < 2\n            ? null\n            : this.tokens.matches2AtIndex(\n                t - 2,\n                Et.TokenType.name,\n                Et.TokenType.eq\n              )\n            ? this.tokens.identifierNameAtIndex(t - 2)\n            : t >= 2 &&\n              this.tokens.tokens[t - 2].identifierRole ===\n                Wh.IdentifierRole.ObjectKey\n            ? this.tokens.identifierNameAtIndex(t - 2)\n            : this.tokens.matches2AtIndex(\n                t - 2,\n                Et.TokenType._export,\n                Et.TokenType._default\n              )\n            ? this.getDisplayNameFromFilename()\n            : null;\n        }\n        getDisplayNameFromFilename() {\n          let s = (this.options.filePath || 'unknown').split('/'),\n            i = s[s.length - 1],\n            r = i.lastIndexOf('.'),\n            a = r === -1 ? i : i.slice(0, r);\n          return a === 'index' && s[s.length - 2] ? s[s.length - 2] : a;\n        }\n        classNeedsDisplayName() {\n          let t = this.tokens.currentIndex();\n          if (!this.tokens.matches2(Et.TokenType.parenL, Et.TokenType.braceL))\n            return !1;\n          let s = t + 1,\n            i = this.tokens.tokens[s].contextId;\n          if (i == null)\n            throw new Error(\n              'Expected non-null context ID on object open-brace.'\n            );\n          for (; t < this.tokens.tokens.length; t++) {\n            let r = this.tokens.tokens[t];\n            if (r.type === Et.TokenType.braceR && r.contextId === i) {\n              t++;\n              break;\n            }\n            if (\n              this.tokens.identifierNameAtIndex(t) === 'displayName' &&\n              this.tokens.tokens[t].identifierRole ===\n                Wh.IdentifierRole.ObjectKey &&\n              r.contextId === i\n            )\n              return !1;\n          }\n          if (t === this.tokens.tokens.length)\n            throw new Error(\n              'Unexpected end of input when processing React class.'\n            );\n          return (\n            this.tokens.matches1AtIndex(t, Et.TokenType.parenR) ||\n            this.tokens.matches2AtIndex(\n              t,\n              Et.TokenType.comma,\n              Et.TokenType.parenR\n            )\n          );\n        }\n      };\n    gc.default = xc;\n  });\n  var Xh = Z((bc) => {\n    'use strict';\n    Object.defineProperty(bc, '__esModule', {value: !0});\n    function Rx(e) {\n      return e && e.__esModule ? e : {default: e};\n    }\n    var zh = xt(),\n      Lx = hn(),\n      Ox = Rx(Lx),\n      _c = class e extends Ox.default {\n        __init() {\n          this.extractedDefaultExportName = null;\n        }\n        constructor(t, s) {\n          super(),\n            (this.tokens = t),\n            (this.filePath = s),\n            e.prototype.__init.call(this);\n        }\n        setExtractedDefaultExportName(t) {\n          this.extractedDefaultExportName = t;\n        }\n        getPrefixCode() {\n          return `\n      (function () {\n        var enterModule = require('react-hot-loader').enterModule;\n        enterModule && enterModule(module);\n      })();`\n            .replace(/\\s+/g, ' ')\n            .trim();\n        }\n        getSuffixCode() {\n          let t = new Set();\n          for (let i of this.tokens.tokens)\n            !i.isType &&\n              zh.isTopLevelDeclaration.call(void 0, i) &&\n              i.identifierRole !== zh.IdentifierRole.ImportDeclaration &&\n              t.add(this.tokens.identifierNameForToken(i));\n          let s = Array.from(t).map((i) => ({\n            variableName: i,\n            uniqueLocalName: i,\n          }));\n          return (\n            this.extractedDefaultExportName &&\n              s.push({\n                variableName: this.extractedDefaultExportName,\n                uniqueLocalName: 'default',\n              }),\n            `\n;(function () {\n  var reactHotLoader = require('react-hot-loader').default;\n  var leaveModule = require('react-hot-loader').leaveModule;\n  if (!reactHotLoader) {\n    return;\n  }\n${s.map(\n  ({variableName: i, uniqueLocalName: r}) =>\n    `  reactHotLoader.register(${i}, \"${r}\", ${JSON.stringify(\n      this.filePath || ''\n    )});`\n).join(`\n`)}\n  leaveModule(module);\n})();`\n          );\n        }\n        process() {\n          return !1;\n        }\n      };\n    bc.default = _c;\n  });\n  var Jh = Z((Cc) => {\n    'use strict';\n    Object.defineProperty(Cc, '__esModule', {value: !0});\n    var Yh = li(),\n      Dx = new Set([\n        'break',\n        'case',\n        'catch',\n        'class',\n        'const',\n        'continue',\n        'debugger',\n        'default',\n        'delete',\n        'do',\n        'else',\n        'export',\n        'extends',\n        'finally',\n        'for',\n        'function',\n        'if',\n        'import',\n        'in',\n        'instanceof',\n        'new',\n        'return',\n        'super',\n        'switch',\n        'this',\n        'throw',\n        'try',\n        'typeof',\n        'var',\n        'void',\n        'while',\n        'with',\n        'yield',\n        'enum',\n        'implements',\n        'interface',\n        'let',\n        'package',\n        'private',\n        'protected',\n        'public',\n        'static',\n        'await',\n        'false',\n        'null',\n        'true',\n      ]);\n    function Mx(e) {\n      if (e.length === 0 || !Yh.IS_IDENTIFIER_START[e.charCodeAt(0)]) return !1;\n      for (let t = 1; t < e.length; t++)\n        if (!Yh.IS_IDENTIFIER_CHAR[e.charCodeAt(t)]) return !1;\n      return !Dx.has(e);\n    }\n    Cc.default = Mx;\n  });\n  var ef = Z((Sc) => {\n    'use strict';\n    Object.defineProperty(Sc, '__esModule', {value: !0});\n    function Zh(e) {\n      return e && e.__esModule ? e : {default: e};\n    }\n    var $e = be(),\n      Fx = Jh(),\n      Qh = Zh(Fx),\n      Bx = hn(),\n      Vx = Zh(Bx),\n      wc = class extends Vx.default {\n        constructor(t, s, i) {\n          super(),\n            (this.rootTransformer = t),\n            (this.tokens = s),\n            (this.isImportsTransformEnabled = i);\n        }\n        process() {\n          return this.rootTransformer.processPossibleArrowParamEnd() ||\n            this.rootTransformer.processPossibleAsyncArrowWithTypeParams() ||\n            this.rootTransformer.processPossibleTypeRange()\n            ? !0\n            : this.tokens.matches1($e.TokenType._public) ||\n              this.tokens.matches1($e.TokenType._protected) ||\n              this.tokens.matches1($e.TokenType._private) ||\n              this.tokens.matches1($e.TokenType._abstract) ||\n              this.tokens.matches1($e.TokenType._readonly) ||\n              this.tokens.matches1($e.TokenType._override) ||\n              this.tokens.matches1($e.TokenType.nonNullAssertion)\n            ? (this.tokens.removeInitialToken(), !0)\n            : this.tokens.matches1($e.TokenType._enum) ||\n              this.tokens.matches2($e.TokenType._const, $e.TokenType._enum)\n            ? (this.processEnum(), !0)\n            : this.tokens.matches2($e.TokenType._export, $e.TokenType._enum) ||\n              this.tokens.matches3(\n                $e.TokenType._export,\n                $e.TokenType._const,\n                $e.TokenType._enum\n              )\n            ? (this.processEnum(!0), !0)\n            : !1;\n        }\n        processEnum(t = !1) {\n          for (\n            this.tokens.removeInitialToken();\n            this.tokens.matches1($e.TokenType._const) ||\n            this.tokens.matches1($e.TokenType._enum);\n\n          )\n            this.tokens.removeToken();\n          let s = this.tokens.identifierName();\n          this.tokens.removeToken(),\n            t &&\n              !this.isImportsTransformEnabled &&\n              this.tokens.appendCode('export '),\n            this.tokens.appendCode(`var ${s}; (function (${s})`),\n            this.tokens.copyExpectedToken($e.TokenType.braceL),\n            this.processEnumBody(s),\n            this.tokens.copyExpectedToken($e.TokenType.braceR),\n            t && this.isImportsTransformEnabled\n              ? this.tokens.appendCode(`)(${s} || (exports.${s} = ${s} = {}));`)\n              : this.tokens.appendCode(`)(${s} || (${s} = {}));`);\n        }\n        processEnumBody(t) {\n          let s = null;\n          for (; !this.tokens.matches1($e.TokenType.braceR); ) {\n            let {nameStringCode: i, variableName: r} = this.extractEnumKeyInfo(\n              this.tokens.currentToken()\n            );\n            this.tokens.removeInitialToken(),\n              this.tokens.matches3(\n                $e.TokenType.eq,\n                $e.TokenType.string,\n                $e.TokenType.comma\n              ) ||\n              this.tokens.matches3(\n                $e.TokenType.eq,\n                $e.TokenType.string,\n                $e.TokenType.braceR\n              )\n                ? this.processStringLiteralEnumMember(t, i, r)\n                : this.tokens.matches1($e.TokenType.eq)\n                ? this.processExplicitValueEnumMember(t, i, r)\n                : this.processImplicitValueEnumMember(t, i, r, s),\n              this.tokens.matches1($e.TokenType.comma) &&\n                this.tokens.removeToken(),\n              r != null ? (s = r) : (s = `${t}[${i}]`);\n          }\n        }\n        extractEnumKeyInfo(t) {\n          if (t.type === $e.TokenType.name) {\n            let s = this.tokens.identifierNameForToken(t);\n            return {\n              nameStringCode: `\"${s}\"`,\n              variableName: Qh.default.call(void 0, s) ? s : null,\n            };\n          } else if (t.type === $e.TokenType.string) {\n            let s = this.tokens.stringValueForToken(t);\n            return {\n              nameStringCode: this.tokens.code.slice(t.start, t.end),\n              variableName: Qh.default.call(void 0, s) ? s : null,\n            };\n          } else\n            throw new Error(\n              'Expected name or string at beginning of enum element.'\n            );\n        }\n        processStringLiteralEnumMember(t, s, i) {\n          i != null\n            ? (this.tokens.appendCode(`const ${i}`),\n              this.tokens.copyToken(),\n              this.tokens.copyToken(),\n              this.tokens.appendCode(`; ${t}[${s}] = ${i};`))\n            : (this.tokens.appendCode(`${t}[${s}]`),\n              this.tokens.copyToken(),\n              this.tokens.copyToken(),\n              this.tokens.appendCode(';'));\n        }\n        processExplicitValueEnumMember(t, s, i) {\n          let r = this.tokens.currentToken().rhsEndIndex;\n          if (r == null)\n            throw new Error('Expected rhsEndIndex on enum assign.');\n          if (i != null) {\n            for (\n              this.tokens.appendCode(`const ${i}`), this.tokens.copyToken();\n              this.tokens.currentIndex() < r;\n\n            )\n              this.rootTransformer.processToken();\n            this.tokens.appendCode(`; ${t}[${t}[${s}] = ${i}] = ${s};`);\n          } else {\n            for (\n              this.tokens.appendCode(`${t}[${t}[${s}]`),\n                this.tokens.copyToken();\n              this.tokens.currentIndex() < r;\n\n            )\n              this.rootTransformer.processToken();\n            this.tokens.appendCode(`] = ${s};`);\n          }\n        }\n        processImplicitValueEnumMember(t, s, i, r) {\n          let a = r != null ? `${r} + 1` : '0';\n          i != null && (this.tokens.appendCode(`const ${i} = ${a}; `), (a = i)),\n            this.tokens.appendCode(`${t}[${t}[${s}] = ${a}] = ${s};`);\n        }\n      };\n    Sc.default = wc;\n  });\n  var tf = Z((Ec) => {\n    'use strict';\n    Object.defineProperty(Ec, '__esModule', {value: !0});\n    function yn(e) {\n      return e && e.__esModule ? e : {default: e};\n    }\n    var jx = It(),\n      lt = be(),\n      $x = Ah(),\n      qx = yn($x),\n      Kx = Oh(),\n      Ux = yn(Kx),\n      Hx = Bh(),\n      Wx = yn(Hx),\n      Gx = jh(),\n      zx = yn(Gx),\n      Xx = $h(),\n      Yx = yn(Xx),\n      Jx = Da(),\n      Qx = yn(Jx),\n      Zx = qh(),\n      eg = yn(Zx),\n      tg = Uh(),\n      ng = yn(tg),\n      sg = Hh(),\n      ig = yn(sg),\n      rg = Gh(),\n      og = yn(rg),\n      ag = Xh(),\n      lg = yn(ag),\n      cg = ef(),\n      ug = yn(cg),\n      Ic = class e {\n        __init() {\n          this.transformers = [];\n        }\n        __init2() {\n          this.generatedVariables = [];\n        }\n        constructor(t, s, i, r) {\n          e.prototype.__init.call(this),\n            e.prototype.__init2.call(this),\n            (this.nameManager = t.nameManager),\n            (this.helperManager = t.helperManager);\n          let {tokenProcessor: a, importProcessor: u} = t;\n          (this.tokens = a),\n            (this.isImportsTransformEnabled = s.includes('imports')),\n            (this.isReactHotLoaderTransformEnabled =\n              s.includes('react-hot-loader')),\n            (this.disableESTransforms = !!r.disableESTransforms),\n            r.disableESTransforms ||\n              (this.transformers.push(new ig.default(a, this.nameManager)),\n              this.transformers.push(new eg.default(a)),\n              this.transformers.push(new ng.default(a, this.nameManager))),\n            s.includes('jsx') &&\n              (r.jsxRuntime !== 'preserve' &&\n                this.transformers.push(\n                  new Qx.default(this, a, u, this.nameManager, r)\n                ),\n              this.transformers.push(new og.default(this, a, u, r)));\n          let d = null;\n          if (s.includes('react-hot-loader')) {\n            if (!r.filePath)\n              throw new Error(\n                'filePath is required when using the react-hot-loader transform.'\n              );\n            (d = new lg.default(a, r.filePath)), this.transformers.push(d);\n          }\n          if (s.includes('imports')) {\n            if (u === null)\n              throw new Error(\n                'Expected non-null importProcessor with imports transform enabled.'\n              );\n            this.transformers.push(\n              new Ux.default(\n                this,\n                a,\n                u,\n                this.nameManager,\n                this.helperManager,\n                d,\n                i,\n                !!r.enableLegacyTypeScriptModuleInterop,\n                s.includes('typescript'),\n                !!r.preserveDynamicImport\n              )\n            );\n          } else\n            this.transformers.push(\n              new Wx.default(\n                a,\n                this.nameManager,\n                this.helperManager,\n                d,\n                s.includes('typescript'),\n                r\n              )\n            );\n          s.includes('flow') &&\n            this.transformers.push(\n              new zx.default(this, a, s.includes('imports'))\n            ),\n            s.includes('typescript') &&\n              this.transformers.push(\n                new ug.default(this, a, s.includes('imports'))\n              ),\n            s.includes('jest') &&\n              this.transformers.push(\n                new Yx.default(this, a, this.nameManager, u)\n              );\n        }\n        transform() {\n          this.tokens.reset(), this.processBalancedCode();\n          let s = this.isImportsTransformEnabled ? '\"use strict\";' : '';\n          for (let u of this.transformers) s += u.getPrefixCode();\n          (s += this.helperManager.emitHelpers()),\n            (s += this.generatedVariables.map((u) => ` var ${u};`).join(''));\n          for (let u of this.transformers) s += u.getHoistedCode();\n          let i = '';\n          for (let u of this.transformers) i += u.getSuffixCode();\n          let r = this.tokens.finish(),\n            {code: a} = r;\n          if (a.startsWith('#!')) {\n            let u = a.indexOf(`\n`);\n            return (\n              u === -1 &&\n                ((u = a.length),\n                (a += `\n`)),\n              {\n                code: a.slice(0, u + 1) + s + a.slice(u + 1) + i,\n                mappings: this.shiftMappings(r.mappings, s.length),\n              }\n            );\n          } else\n            return {\n              code: s + a + i,\n              mappings: this.shiftMappings(r.mappings, s.length),\n            };\n        }\n        processBalancedCode() {\n          let t = 0,\n            s = 0;\n          for (; !this.tokens.isAtEnd(); ) {\n            if (\n              this.tokens.matches1(lt.TokenType.braceL) ||\n              this.tokens.matches1(lt.TokenType.dollarBraceL)\n            )\n              t++;\n            else if (this.tokens.matches1(lt.TokenType.braceR)) {\n              if (t === 0) return;\n              t--;\n            }\n            if (this.tokens.matches1(lt.TokenType.parenL)) s++;\n            else if (this.tokens.matches1(lt.TokenType.parenR)) {\n              if (s === 0) return;\n              s--;\n            }\n            this.processToken();\n          }\n        }\n        processToken() {\n          if (this.tokens.matches1(lt.TokenType._class)) {\n            this.processClass();\n            return;\n          }\n          for (let t of this.transformers) if (t.process()) return;\n          this.tokens.copyToken();\n        }\n        processNamedClass() {\n          if (!this.tokens.matches2(lt.TokenType._class, lt.TokenType.name))\n            throw new Error('Expected identifier for exported class name.');\n          let t = this.tokens.identifierNameAtIndex(\n            this.tokens.currentIndex() + 1\n          );\n          return this.processClass(), t;\n        }\n        processClass() {\n          let t = qx.default.call(\n              void 0,\n              this,\n              this.tokens,\n              this.nameManager,\n              this.disableESTransforms\n            ),\n            s =\n              (t.headerInfo.isExpression || !t.headerInfo.className) &&\n              t.staticInitializerNames.length +\n                t.instanceInitializerNames.length >\n                0,\n            i = t.headerInfo.className;\n          s &&\n            ((i = this.nameManager.claimFreeName('_class')),\n            this.generatedVariables.push(i),\n            this.tokens.appendCode(` (${i} =`));\n          let a = this.tokens.currentToken().contextId;\n          if (a == null)\n            throw new Error('Expected class to have a context ID.');\n          for (\n            this.tokens.copyExpectedToken(lt.TokenType._class);\n            !this.tokens.matchesContextIdAndLabel(lt.TokenType.braceL, a);\n\n          )\n            this.processToken();\n          this.processClassBody(t, i);\n          let u = t.staticInitializerNames.map((d) => `${i}.${d}()`);\n          s\n            ? this.tokens.appendCode(\n                `, ${u.map((d) => `${d}, `).join('')}${i})`\n              )\n            : t.staticInitializerNames.length > 0 &&\n              this.tokens.appendCode(` ${u.map((d) => `${d};`).join(' ')}`);\n        }\n        processClassBody(t, s) {\n          let {\n              headerInfo: i,\n              constructorInsertPos: r,\n              constructorInitializerStatements: a,\n              fields: u,\n              instanceInitializerNames: d,\n              rangesToRemove: y,\n            } = t,\n            g = 0,\n            L = 0,\n            p = this.tokens.currentToken().contextId;\n          if (p == null)\n            throw new Error('Expected non-null context ID on class.');\n          this.tokens.copyExpectedToken(lt.TokenType.braceL),\n            this.isReactHotLoaderTransformEnabled &&\n              this.tokens.appendCode(\n                '__reactstandin__regenerateByEval(key, code) {this[key] = eval(code);}'\n              );\n          let h = a.length + d.length > 0;\n          if (r === null && h) {\n            let T = this.makeConstructorInitCode(a, d, s);\n            if (i.hasSuperclass) {\n              let x = this.nameManager.claimFreeName('args');\n              this.tokens.appendCode(\n                `constructor(...${x}) { super(...${x}); ${T}; }`\n              );\n            } else this.tokens.appendCode(`constructor() { ${T}; }`);\n          }\n          for (\n            ;\n            !this.tokens.matchesContextIdAndLabel(lt.TokenType.braceR, p);\n\n          )\n            if (g < u.length && this.tokens.currentIndex() === u[g].start) {\n              let T = !1;\n              for (\n                this.tokens.matches1(lt.TokenType.bracketL)\n                  ? this.tokens.copyTokenWithPrefix(\n                      `${u[g].initializerName}() {this`\n                    )\n                  : this.tokens.matches1(lt.TokenType.string) ||\n                    this.tokens.matches1(lt.TokenType.num)\n                  ? (this.tokens.copyTokenWithPrefix(\n                      `${u[g].initializerName}() {this[`\n                    ),\n                    (T = !0))\n                  : this.tokens.copyTokenWithPrefix(\n                      `${u[g].initializerName}() {this.`\n                    );\n                this.tokens.currentIndex() < u[g].end;\n\n              )\n                T &&\n                  this.tokens.currentIndex() === u[g].equalsIndex &&\n                  this.tokens.appendCode(']'),\n                  this.processToken();\n              this.tokens.appendCode('}'), g++;\n            } else if (\n              L < y.length &&\n              this.tokens.currentIndex() >= y[L].start\n            ) {\n              for (\n                this.tokens.currentIndex() < y[L].end &&\n                this.tokens.removeInitialToken();\n                this.tokens.currentIndex() < y[L].end;\n\n              )\n                this.tokens.removeToken();\n              L++;\n            } else\n              this.tokens.currentIndex() === r\n                ? (this.tokens.copyToken(),\n                  h &&\n                    this.tokens.appendCode(\n                      `;${this.makeConstructorInitCode(a, d, s)};`\n                    ),\n                  this.processToken())\n                : this.processToken();\n          this.tokens.copyExpectedToken(lt.TokenType.braceR);\n        }\n        makeConstructorInitCode(t, s, i) {\n          return [...t, ...s.map((r) => `${i}.prototype.${r}.call(this)`)].join(\n            ';'\n          );\n        }\n        processPossibleArrowParamEnd() {\n          if (\n            this.tokens.matches2(lt.TokenType.parenR, lt.TokenType.colon) &&\n            this.tokens.tokenAtRelativeIndex(1).isType\n          ) {\n            let t = this.tokens.currentIndex() + 1;\n            for (; this.tokens.tokens[t].isType; ) t++;\n            if (this.tokens.matches1AtIndex(t, lt.TokenType.arrow)) {\n              for (\n                this.tokens.removeInitialToken();\n                this.tokens.currentIndex() < t;\n\n              )\n                this.tokens.removeToken();\n              return this.tokens.replaceTokenTrimmingLeftWhitespace(') =>'), !0;\n            }\n          }\n          return !1;\n        }\n        processPossibleAsyncArrowWithTypeParams() {\n          if (\n            !this.tokens.matchesContextual(jx.ContextualKeyword._async) &&\n            !this.tokens.matches1(lt.TokenType._async)\n          )\n            return !1;\n          let t = this.tokens.tokenAtRelativeIndex(1);\n          if (t.type !== lt.TokenType.lessThan || !t.isType) return !1;\n          let s = this.tokens.currentIndex() + 1;\n          for (; this.tokens.tokens[s].isType; ) s++;\n          if (this.tokens.matches1AtIndex(s, lt.TokenType.parenL)) {\n            for (\n              this.tokens.replaceToken('async ('),\n                this.tokens.removeInitialToken();\n              this.tokens.currentIndex() < s;\n\n            )\n              this.tokens.removeToken();\n            return (\n              this.tokens.removeToken(),\n              this.processBalancedCode(),\n              this.processToken(),\n              !0\n            );\n          }\n          return !1;\n        }\n        processPossibleTypeRange() {\n          if (this.tokens.currentToken().isType) {\n            for (\n              this.tokens.removeInitialToken();\n              this.tokens.currentToken().isType;\n\n            )\n              this.tokens.removeToken();\n            return !0;\n          }\n          return !1;\n        }\n        shiftMappings(t, s) {\n          for (let i = 0; i < t.length; i++) {\n            let r = t[i];\n            r !== void 0 && (t[i] = r + s);\n          }\n          return t;\n        }\n      };\n    Ec.default = Ic;\n  });\n  var rf = Z((hr) => {\n    'use strict';\n    hr.__esModule = !0;\n    hr.LinesAndColumns = void 0;\n    var Mo = `\n`,\n      nf = '\\r',\n      sf = (function () {\n        function e(t) {\n          this.string = t;\n          for (var s = [0], i = 0; i < t.length; )\n            switch (t[i]) {\n              case Mo:\n                (i += Mo.length), s.push(i);\n                break;\n              case nf:\n                (i += nf.length), t[i] === Mo && (i += Mo.length), s.push(i);\n                break;\n              default:\n                i++;\n                break;\n            }\n          this.offsets = s;\n        }\n        return (\n          (e.prototype.locationForIndex = function (t) {\n            if (t < 0 || t > this.string.length) return null;\n            for (var s = 0, i = this.offsets; i[s + 1] <= t; ) s++;\n            var r = t - i[s];\n            return {line: s, column: r};\n          }),\n          (e.prototype.indexForLocation = function (t) {\n            var s = t.line,\n              i = t.column;\n            return s < 0 ||\n              s >= this.offsets.length ||\n              i < 0 ||\n              i > this.lengthOfLine(s)\n              ? null\n              : this.offsets[s] + i;\n          }),\n          (e.prototype.lengthOfLine = function (t) {\n            var s = this.offsets[t],\n              i =\n                t === this.offsets.length - 1\n                  ? this.string.length\n                  : this.offsets[t + 1];\n            return i - s;\n          }),\n          e\n        );\n      })();\n    hr.LinesAndColumns = sf;\n    hr.default = sf;\n  });\n  var of = Z((Ac) => {\n    'use strict';\n    Object.defineProperty(Ac, '__esModule', {value: !0});\n    function pg(e) {\n      return e && e.__esModule ? e : {default: e};\n    }\n    var hg = rf(),\n      fg = pg(hg),\n      dg = be();\n    function mg(e, t) {\n      if (t.length === 0) return '';\n      let s = Object.keys(t[0]).filter(\n          (h) =>\n            h !== 'type' &&\n            h !== 'value' &&\n            h !== 'start' &&\n            h !== 'end' &&\n            h !== 'loc'\n        ),\n        i = Object.keys(t[0].type).filter(\n          (h) => h !== 'label' && h !== 'keyword'\n        ),\n        r = ['Location', 'Label', 'Raw', ...s, ...i],\n        a = new fg.default(e),\n        u = [r, ...t.map(y)],\n        d = r.map(() => 0);\n      for (let h of u)\n        for (let T = 0; T < h.length; T++) d[T] = Math.max(d[T], h[T].length);\n      return u.map((h) => h.map((T, x) => T.padEnd(d[x])).join(' ')).join(`\n`);\n      function y(h) {\n        let T = e.slice(h.start, h.end);\n        return [\n          L(h.start, h.end),\n          dg.formatTokenType.call(void 0, h.type),\n          yg(String(T), 14),\n          ...s.map((x) => g(h[x], x)),\n          ...i.map((x) => g(h.type[x], x)),\n        ];\n      }\n      function g(h, T) {\n        return h === !0 ? T : h === !1 || h === null ? '' : String(h);\n      }\n      function L(h, T) {\n        return `${p(h)}-${p(T)}`;\n      }\n      function p(h) {\n        let T = a.locationForIndex(h);\n        return T ? `${T.line + 1}:${T.column + 1}` : 'Unknown';\n      }\n    }\n    Ac.default = mg;\n    function yg(e, t) {\n      return e.length > t ? `${e.slice(0, t - 3)}...` : e;\n    }\n  });\n  var af = Z((Pc) => {\n    'use strict';\n    Object.defineProperty(Pc, '__esModule', {value: !0});\n    function Tg(e) {\n      return e && e.__esModule ? e : {default: e};\n    }\n    var Gt = be(),\n      kg = Wi(),\n      vg = Tg(kg);\n    function xg(e) {\n      let t = new Set();\n      for (let s = 0; s < e.tokens.length; s++)\n        e.matches1AtIndex(s, Gt.TokenType._import) &&\n          !e.matches3AtIndex(\n            s,\n            Gt.TokenType._import,\n            Gt.TokenType.name,\n            Gt.TokenType.eq\n          ) &&\n          gg(e, s, t);\n      return t;\n    }\n    Pc.default = xg;\n    function gg(e, t, s) {\n      t++,\n        !e.matches1AtIndex(t, Gt.TokenType.parenL) &&\n          (e.matches1AtIndex(t, Gt.TokenType.name) &&\n            (s.add(e.identifierNameAtIndex(t)),\n            t++,\n            e.matches1AtIndex(t, Gt.TokenType.comma) && t++),\n          e.matches1AtIndex(t, Gt.TokenType.star) &&\n            ((t += 2), s.add(e.identifierNameAtIndex(t)), t++),\n          e.matches1AtIndex(t, Gt.TokenType.braceL) && (t++, _g(e, t, s)));\n    }\n    function _g(e, t, s) {\n      for (;;) {\n        if (e.matches1AtIndex(t, Gt.TokenType.braceR)) return;\n        let i = vg.default.call(void 0, e, t);\n        if (\n          ((t = i.endIndex),\n          i.isType || s.add(i.rightName),\n          e.matches2AtIndex(t, Gt.TokenType.comma, Gt.TokenType.braceR))\n        )\n          return;\n        if (e.matches1AtIndex(t, Gt.TokenType.braceR)) return;\n        if (e.matches1AtIndex(t, Gt.TokenType.comma)) t++;\n        else\n          throw new Error(`Unexpected token: ${JSON.stringify(e.tokens[t])}`);\n      }\n    }\n  });\n  var uf = Z((fr) => {\n    'use strict';\n    Object.defineProperty(fr, '__esModule', {value: !0});\n    function vs(e) {\n      return e && e.__esModule ? e : {default: e};\n    }\n    var bg = N1(),\n      Cg = vs(bg),\n      wg = $1(),\n      Sg = vs(wg),\n      Ig = q1(),\n      Eg = H1(),\n      lf = vs(Eg),\n      Ag = G1(),\n      Pg = vs(Ag),\n      Ng = pp(),\n      Rg = Ul(),\n      Lg = Sh(),\n      Og = vs(Lg),\n      Dg = tf(),\n      Mg = vs(Dg),\n      Fg = of(),\n      Bg = vs(Fg),\n      Vg = af(),\n      jg = vs(Vg);\n    function $g() {\n      return '3.32.0';\n    }\n    fr.getVersion = $g;\n    function qg(e, t) {\n      Ng.validateOptions.call(void 0, t);\n      try {\n        let s = cf(e, t),\n          r = new Mg.default(\n            s,\n            t.transforms,\n            !!t.enableLegacyBabel5ModuleInterop,\n            t\n          ).transform(),\n          a = {code: r.code};\n        if (t.sourceMapOptions) {\n          if (!t.filePath)\n            throw new Error(\n              'filePath must be specified when generating a source map.'\n            );\n          a = {\n            ...a,\n            sourceMap: Sg.default.call(\n              void 0,\n              r,\n              t.filePath,\n              t.sourceMapOptions,\n              e,\n              s.tokenProcessor.tokens\n            ),\n          };\n        }\n        return a;\n      } catch (s) {\n        throw (\n          (t.filePath &&\n            (s.message = `Error transforming ${t.filePath}: ${s.message}`),\n          s)\n        );\n      }\n    }\n    fr.transform = qg;\n    function Kg(e, t) {\n      let s = cf(e, t).tokenProcessor.tokens;\n      return Bg.default.call(void 0, e, s);\n    }\n    fr.getFormattedTokens = Kg;\n    function cf(e, t) {\n      let s = t.transforms.includes('jsx'),\n        i = t.transforms.includes('typescript'),\n        r = t.transforms.includes('flow'),\n        a = t.disableESTransforms === !0,\n        u = Rg.parse.call(void 0, e, s, i, r),\n        d = u.tokens,\n        y = u.scopes,\n        g = new Pg.default(e, d),\n        L = new Ig.HelperManager(g),\n        p = new Og.default(e, d, r, a, L),\n        h = !!t.enableLegacyTypeScriptModuleInterop,\n        T = null;\n      return (\n        t.transforms.includes('imports')\n          ? ((T = new Cg.default(\n              g,\n              p,\n              h,\n              t,\n              t.transforms.includes('typescript'),\n              L\n            )),\n            T.preprocessTokens(),\n            lf.default.call(void 0, p, y, T.getGlobalNames()),\n            t.transforms.includes('typescript') && T.pruneTypeOnlyImports())\n          : t.transforms.includes('typescript') &&\n            lf.default.call(void 0, p, y, jg.default.call(void 0, p)),\n        {\n          tokenProcessor: p,\n          scopes: y,\n          nameManager: g,\n          importProcessor: T,\n          helperManager: L,\n        }\n      );\n    }\n  });\n  var hf = Z((Fo, pf) => {\n    (function (e, t) {\n      typeof Fo == 'object' && typeof pf < 'u'\n        ? t(Fo)\n        : typeof define == 'function' && define.amd\n        ? define(['exports'], t)\n        : ((e = typeof globalThis < 'u' ? globalThis : e || self),\n          t((e.acorn = {})));\n    })(Fo, function (e) {\n      'use strict';\n      var t = [\n          509, 0, 227, 0, 150, 4, 294, 9, 1368, 2, 2, 1, 6, 3, 41, 2, 5, 0, 166,\n          1, 574, 3, 9, 9, 7, 9, 32, 4, 318, 1, 80, 3, 71, 10, 50, 3, 123, 2,\n          54, 14, 32, 10, 3, 1, 11, 3, 46, 10, 8, 0, 46, 9, 7, 2, 37, 13, 2, 9,\n          6, 1, 45, 0, 13, 2, 49, 13, 9, 3, 2, 11, 83, 11, 7, 0, 3, 0, 158, 11,\n          6, 9, 7, 3, 56, 1, 2, 6, 3, 1, 3, 2, 10, 0, 11, 1, 3, 6, 4, 4, 68, 8,\n          2, 0, 3, 0, 2, 3, 2, 4, 2, 0, 15, 1, 83, 17, 10, 9, 5, 0, 82, 19, 13,\n          9, 214, 6, 3, 8, 28, 1, 83, 16, 16, 9, 82, 12, 9, 9, 7, 19, 58, 14, 5,\n          9, 243, 14, 166, 9, 71, 5, 2, 1, 3, 3, 2, 0, 2, 1, 13, 9, 120, 6, 3,\n          6, 4, 0, 29, 9, 41, 6, 2, 3, 9, 0, 10, 10, 47, 15, 343, 9, 54, 7, 2,\n          7, 17, 9, 57, 21, 2, 13, 123, 5, 4, 0, 2, 1, 2, 6, 2, 0, 9, 9, 49, 4,\n          2, 1, 2, 4, 9, 9, 330, 3, 10, 1, 2, 0, 49, 6, 4, 4, 14, 10, 5350, 0,\n          7, 14, 11465, 27, 2343, 9, 87, 9, 39, 4, 60, 6, 26, 9, 535, 9, 470, 0,\n          2, 54, 8, 3, 82, 0, 12, 1, 19628, 1, 4178, 9, 519, 45, 3, 22, 543, 4,\n          4, 5, 9, 7, 3, 6, 31, 3, 149, 2, 1418, 49, 513, 54, 5, 49, 9, 0, 15,\n          0, 23, 4, 2, 14, 1361, 6, 2, 16, 3, 6, 2, 1, 2, 4, 101, 0, 161, 6, 10,\n          9, 357, 0, 62, 13, 499, 13, 245, 1, 2, 9, 726, 6, 110, 6, 6, 9, 4759,\n          9, 787719, 239,\n        ],\n        s = [\n          0, 11, 2, 25, 2, 18, 2, 1, 2, 14, 3, 13, 35, 122, 70, 52, 268, 28, 4,\n          48, 48, 31, 14, 29, 6, 37, 11, 29, 3, 35, 5, 7, 2, 4, 43, 157, 19, 35,\n          5, 35, 5, 39, 9, 51, 13, 10, 2, 14, 2, 6, 2, 1, 2, 10, 2, 14, 2, 6, 2,\n          1, 4, 51, 13, 310, 10, 21, 11, 7, 25, 5, 2, 41, 2, 8, 70, 5, 3, 0, 2,\n          43, 2, 1, 4, 0, 3, 22, 11, 22, 10, 30, 66, 18, 2, 1, 11, 21, 11, 25,\n          71, 55, 7, 1, 65, 0, 16, 3, 2, 2, 2, 28, 43, 28, 4, 28, 36, 7, 2, 27,\n          28, 53, 11, 21, 11, 18, 14, 17, 111, 72, 56, 50, 14, 50, 14, 35, 39,\n          27, 10, 22, 251, 41, 7, 1, 17, 2, 60, 28, 11, 0, 9, 21, 43, 17, 47,\n          20, 28, 22, 13, 52, 58, 1, 3, 0, 14, 44, 33, 24, 27, 35, 30, 0, 3, 0,\n          9, 34, 4, 0, 13, 47, 15, 3, 22, 0, 2, 0, 36, 17, 2, 24, 20, 1, 64, 6,\n          2, 0, 2, 3, 2, 14, 2, 9, 8, 46, 39, 7, 3, 1, 3, 21, 2, 6, 2, 1, 2, 4,\n          4, 0, 19, 0, 13, 4, 31, 9, 2, 0, 3, 0, 2, 37, 2, 0, 26, 0, 2, 0, 45,\n          52, 19, 3, 21, 2, 31, 47, 21, 1, 2, 0, 185, 46, 42, 3, 37, 47, 21, 0,\n          60, 42, 14, 0, 72, 26, 38, 6, 186, 43, 117, 63, 32, 7, 3, 0, 3, 7, 2,\n          1, 2, 23, 16, 0, 2, 0, 95, 7, 3, 38, 17, 0, 2, 0, 29, 0, 11, 39, 8, 0,\n          22, 0, 12, 45, 20, 0, 19, 72, 200, 32, 32, 8, 2, 36, 18, 0, 50, 29,\n          113, 6, 2, 1, 2, 37, 22, 0, 26, 5, 2, 1, 2, 31, 15, 0, 328, 18, 16, 0,\n          2, 12, 2, 33, 125, 0, 80, 921, 103, 110, 18, 195, 2637, 96, 16, 1071,\n          18, 5, 26, 3994, 6, 582, 6842, 29, 1763, 568, 8, 30, 18, 78, 18, 29,\n          19, 47, 17, 3, 32, 20, 6, 18, 433, 44, 212, 63, 129, 74, 6, 0, 67, 12,\n          65, 1, 2, 0, 29, 6135, 9, 1237, 42, 9, 8936, 3, 2, 6, 2, 1, 2, 290,\n          16, 0, 30, 2, 3, 0, 15, 3, 9, 395, 2309, 106, 6, 12, 4, 8, 8, 9, 5991,\n          84, 2, 70, 2, 1, 3, 0, 3, 1, 3, 3, 2, 11, 2, 0, 2, 6, 2, 64, 2, 3, 3,\n          7, 2, 6, 2, 27, 2, 3, 2, 4, 2, 0, 4, 6, 2, 339, 3, 24, 2, 24, 2, 30,\n          2, 24, 2, 30, 2, 24, 2, 30, 2, 24, 2, 30, 2, 24, 2, 7, 1845, 30, 7, 5,\n          262, 61, 147, 44, 11, 6, 17, 0, 322, 29, 19, 43, 485, 27, 229, 29, 3,\n          0, 496, 6, 2, 3, 2, 1, 2, 14, 2, 196, 60, 67, 8, 0, 1205, 3, 2, 26, 2,\n          1, 2, 0, 3, 0, 2, 9, 2, 3, 2, 0, 2, 0, 7, 0, 5, 0, 2, 0, 2, 0, 2, 2,\n          2, 1, 2, 0, 3, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 1, 2, 0, 3, 3, 2, 6, 2,\n          3, 2, 3, 2, 0, 2, 9, 2, 16, 6, 2, 2, 4, 2, 16, 4421, 42719, 33, 4153,\n          7, 221, 3, 5761, 15, 7472, 16, 621, 2467, 541, 1507, 4938, 6, 4191,\n        ],\n        i =\n          '\\u200C\\u200D\\xB7\\u0300-\\u036F\\u0387\\u0483-\\u0487\\u0591-\\u05BD\\u05BF\\u05C1\\u05C2\\u05C4\\u05C5\\u05C7\\u0610-\\u061A\\u064B-\\u0669\\u0670\\u06D6-\\u06DC\\u06DF-\\u06E4\\u06E7\\u06E8\\u06EA-\\u06ED\\u06F0-\\u06F9\\u0711\\u0730-\\u074A\\u07A6-\\u07B0\\u07C0-\\u07C9\\u07EB-\\u07F3\\u07FD\\u0816-\\u0819\\u081B-\\u0823\\u0825-\\u0827\\u0829-\\u082D\\u0859-\\u085B\\u0897-\\u089F\\u08CA-\\u08E1\\u08E3-\\u0903\\u093A-\\u093C\\u093E-\\u094F\\u0951-\\u0957\\u0962\\u0963\\u0966-\\u096F\\u0981-\\u0983\\u09BC\\u09BE-\\u09C4\\u09C7\\u09C8\\u09CB-\\u09CD\\u09D7\\u09E2\\u09E3\\u09E6-\\u09EF\\u09FE\\u0A01-\\u0A03\\u0A3C\\u0A3E-\\u0A42\\u0A47\\u0A48\\u0A4B-\\u0A4D\\u0A51\\u0A66-\\u0A71\\u0A75\\u0A81-\\u0A83\\u0ABC\\u0ABE-\\u0AC5\\u0AC7-\\u0AC9\\u0ACB-\\u0ACD\\u0AE2\\u0AE3\\u0AE6-\\u0AEF\\u0AFA-\\u0AFF\\u0B01-\\u0B03\\u0B3C\\u0B3E-\\u0B44\\u0B47\\u0B48\\u0B4B-\\u0B4D\\u0B55-\\u0B57\\u0B62\\u0B63\\u0B66-\\u0B6F\\u0B82\\u0BBE-\\u0BC2\\u0BC6-\\u0BC8\\u0BCA-\\u0BCD\\u0BD7\\u0BE6-\\u0BEF\\u0C00-\\u0C04\\u0C3C\\u0C3E-\\u0C44\\u0C46-\\u0C48\\u0C4A-\\u0C4D\\u0C55\\u0C56\\u0C62\\u0C63\\u0C66-\\u0C6F\\u0C81-\\u0C83\\u0CBC\\u0CBE-\\u0CC4\\u0CC6-\\u0CC8\\u0CCA-\\u0CCD\\u0CD5\\u0CD6\\u0CE2\\u0CE3\\u0CE6-\\u0CEF\\u0CF3\\u0D00-\\u0D03\\u0D3B\\u0D3C\\u0D3E-\\u0D44\\u0D46-\\u0D48\\u0D4A-\\u0D4D\\u0D57\\u0D62\\u0D63\\u0D66-\\u0D6F\\u0D81-\\u0D83\\u0DCA\\u0DCF-\\u0DD4\\u0DD6\\u0DD8-\\u0DDF\\u0DE6-\\u0DEF\\u0DF2\\u0DF3\\u0E31\\u0E34-\\u0E3A\\u0E47-\\u0E4E\\u0E50-\\u0E59\\u0EB1\\u0EB4-\\u0EBC\\u0EC8-\\u0ECE\\u0ED0-\\u0ED9\\u0F18\\u0F19\\u0F20-\\u0F29\\u0F35\\u0F37\\u0F39\\u0F3E\\u0F3F\\u0F71-\\u0F84\\u0F86\\u0F87\\u0F8D-\\u0F97\\u0F99-\\u0FBC\\u0FC6\\u102B-\\u103E\\u1040-\\u1049\\u1056-\\u1059\\u105E-\\u1060\\u1062-\\u1064\\u1067-\\u106D\\u1071-\\u1074\\u1082-\\u108D\\u108F-\\u109D\\u135D-\\u135F\\u1369-\\u1371\\u1712-\\u1715\\u1732-\\u1734\\u1752\\u1753\\u1772\\u1773\\u17B4-\\u17D3\\u17DD\\u17E0-\\u17E9\\u180B-\\u180D\\u180F-\\u1819\\u18A9\\u1920-\\u192B\\u1930-\\u193B\\u1946-\\u194F\\u19D0-\\u19DA\\u1A17-\\u1A1B\\u1A55-\\u1A5E\\u1A60-\\u1A7C\\u1A7F-\\u1A89\\u1A90-\\u1A99\\u1AB0-\\u1ABD\\u1ABF-\\u1ACE\\u1B00-\\u1B04\\u1B34-\\u1B44\\u1B50-\\u1B59\\u1B6B-\\u1B73\\u1B80-\\u1B82\\u1BA1-\\u1BAD\\u1BB0-\\u1BB9\\u1BE6-\\u1BF3\\u1C24-\\u1C37\\u1C40-\\u1C49\\u1C50-\\u1C59\\u1CD0-\\u1CD2\\u1CD4-\\u1CE8\\u1CED\\u1CF4\\u1CF7-\\u1CF9\\u1DC0-\\u1DFF\\u200C\\u200D\\u203F\\u2040\\u2054\\u20D0-\\u20DC\\u20E1\\u20E5-\\u20F0\\u2CEF-\\u2CF1\\u2D7F\\u2DE0-\\u2DFF\\u302A-\\u302F\\u3099\\u309A\\u30FB\\uA620-\\uA629\\uA66F\\uA674-\\uA67D\\uA69E\\uA69F\\uA6F0\\uA6F1\\uA802\\uA806\\uA80B\\uA823-\\uA827\\uA82C\\uA880\\uA881\\uA8B4-\\uA8C5\\uA8D0-\\uA8D9\\uA8E0-\\uA8F1\\uA8FF-\\uA909\\uA926-\\uA92D\\uA947-\\uA953\\uA980-\\uA983\\uA9B3-\\uA9C0\\uA9D0-\\uA9D9\\uA9E5\\uA9F0-\\uA9F9\\uAA29-\\uAA36\\uAA43\\uAA4C\\uAA4D\\uAA50-\\uAA59\\uAA7B-\\uAA7D\\uAAB0\\uAAB2-\\uAAB4\\uAAB7\\uAAB8\\uAABE\\uAABF\\uAAC1\\uAAEB-\\uAAEF\\uAAF5\\uAAF6\\uABE3-\\uABEA\\uABEC\\uABED\\uABF0-\\uABF9\\uFB1E\\uFE00-\\uFE0F\\uFE20-\\uFE2F\\uFE33\\uFE34\\uFE4D-\\uFE4F\\uFF10-\\uFF19\\uFF3F\\uFF65',\n        r =\n          '\\xAA\\xB5\\xBA\\xC0-\\xD6\\xD8-\\xF6\\xF8-\\u02C1\\u02C6-\\u02D1\\u02E0-\\u02E4\\u02EC\\u02EE\\u0370-\\u0374\\u0376\\u0377\\u037A-\\u037D\\u037F\\u0386\\u0388-\\u038A\\u038C\\u038E-\\u03A1\\u03A3-\\u03F5\\u03F7-\\u0481\\u048A-\\u052F\\u0531-\\u0556\\u0559\\u0560-\\u0588\\u05D0-\\u05EA\\u05EF-\\u05F2\\u0620-\\u064A\\u066E\\u066F\\u0671-\\u06D3\\u06D5\\u06E5\\u06E6\\u06EE\\u06EF\\u06FA-\\u06FC\\u06FF\\u0710\\u0712-\\u072F\\u074D-\\u07A5\\u07B1\\u07CA-\\u07EA\\u07F4\\u07F5\\u07FA\\u0800-\\u0815\\u081A\\u0824\\u0828\\u0840-\\u0858\\u0860-\\u086A\\u0870-\\u0887\\u0889-\\u088E\\u08A0-\\u08C9\\u0904-\\u0939\\u093D\\u0950\\u0958-\\u0961\\u0971-\\u0980\\u0985-\\u098C\\u098F\\u0990\\u0993-\\u09A8\\u09AA-\\u09B0\\u09B2\\u09B6-\\u09B9\\u09BD\\u09CE\\u09DC\\u09DD\\u09DF-\\u09E1\\u09F0\\u09F1\\u09FC\\u0A05-\\u0A0A\\u0A0F\\u0A10\\u0A13-\\u0A28\\u0A2A-\\u0A30\\u0A32\\u0A33\\u0A35\\u0A36\\u0A38\\u0A39\\u0A59-\\u0A5C\\u0A5E\\u0A72-\\u0A74\\u0A85-\\u0A8D\\u0A8F-\\u0A91\\u0A93-\\u0AA8\\u0AAA-\\u0AB0\\u0AB2\\u0AB3\\u0AB5-\\u0AB9\\u0ABD\\u0AD0\\u0AE0\\u0AE1\\u0AF9\\u0B05-\\u0B0C\\u0B0F\\u0B10\\u0B13-\\u0B28\\u0B2A-\\u0B30\\u0B32\\u0B33\\u0B35-\\u0B39\\u0B3D\\u0B5C\\u0B5D\\u0B5F-\\u0B61\\u0B71\\u0B83\\u0B85-\\u0B8A\\u0B8E-\\u0B90\\u0B92-\\u0B95\\u0B99\\u0B9A\\u0B9C\\u0B9E\\u0B9F\\u0BA3\\u0BA4\\u0BA8-\\u0BAA\\u0BAE-\\u0BB9\\u0BD0\\u0C05-\\u0C0C\\u0C0E-\\u0C10\\u0C12-\\u0C28\\u0C2A-\\u0C39\\u0C3D\\u0C58-\\u0C5A\\u0C5D\\u0C60\\u0C61\\u0C80\\u0C85-\\u0C8C\\u0C8E-\\u0C90\\u0C92-\\u0CA8\\u0CAA-\\u0CB3\\u0CB5-\\u0CB9\\u0CBD\\u0CDD\\u0CDE\\u0CE0\\u0CE1\\u0CF1\\u0CF2\\u0D04-\\u0D0C\\u0D0E-\\u0D10\\u0D12-\\u0D3A\\u0D3D\\u0D4E\\u0D54-\\u0D56\\u0D5F-\\u0D61\\u0D7A-\\u0D7F\\u0D85-\\u0D96\\u0D9A-\\u0DB1\\u0DB3-\\u0DBB\\u0DBD\\u0DC0-\\u0DC6\\u0E01-\\u0E30\\u0E32\\u0E33\\u0E40-\\u0E46\\u0E81\\u0E82\\u0E84\\u0E86-\\u0E8A\\u0E8C-\\u0EA3\\u0EA5\\u0EA7-\\u0EB0\\u0EB2\\u0EB3\\u0EBD\\u0EC0-\\u0EC4\\u0EC6\\u0EDC-\\u0EDF\\u0F00\\u0F40-\\u0F47\\u0F49-\\u0F6C\\u0F88-\\u0F8C\\u1000-\\u102A\\u103F\\u1050-\\u1055\\u105A-\\u105D\\u1061\\u1065\\u1066\\u106E-\\u1070\\u1075-\\u1081\\u108E\\u10A0-\\u10C5\\u10C7\\u10CD\\u10D0-\\u10FA\\u10FC-\\u1248\\u124A-\\u124D\\u1250-\\u1256\\u1258\\u125A-\\u125D\\u1260-\\u1288\\u128A-\\u128D\\u1290-\\u12B0\\u12B2-\\u12B5\\u12B8-\\u12BE\\u12C0\\u12C2-\\u12C5\\u12C8-\\u12D6\\u12D8-\\u1310\\u1312-\\u1315\\u1318-\\u135A\\u1380-\\u138F\\u13A0-\\u13F5\\u13F8-\\u13FD\\u1401-\\u166C\\u166F-\\u167F\\u1681-\\u169A\\u16A0-\\u16EA\\u16EE-\\u16F8\\u1700-\\u1711\\u171F-\\u1731\\u1740-\\u1751\\u1760-\\u176C\\u176E-\\u1770\\u1780-\\u17B3\\u17D7\\u17DC\\u1820-\\u1878\\u1880-\\u18A8\\u18AA\\u18B0-\\u18F5\\u1900-\\u191E\\u1950-\\u196D\\u1970-\\u1974\\u1980-\\u19AB\\u19B0-\\u19C9\\u1A00-\\u1A16\\u1A20-\\u1A54\\u1AA7\\u1B05-\\u1B33\\u1B45-\\u1B4C\\u1B83-\\u1BA0\\u1BAE\\u1BAF\\u1BBA-\\u1BE5\\u1C00-\\u1C23\\u1C4D-\\u1C4F\\u1C5A-\\u1C7D\\u1C80-\\u1C8A\\u1C90-\\u1CBA\\u1CBD-\\u1CBF\\u1CE9-\\u1CEC\\u1CEE-\\u1CF3\\u1CF5\\u1CF6\\u1CFA\\u1D00-\\u1DBF\\u1E00-\\u1F15\\u1F18-\\u1F1D\\u1F20-\\u1F45\\u1F48-\\u1F4D\\u1F50-\\u1F57\\u1F59\\u1F5B\\u1F5D\\u1F5F-\\u1F7D\\u1F80-\\u1FB4\\u1FB6-\\u1FBC\\u1FBE\\u1FC2-\\u1FC4\\u1FC6-\\u1FCC\\u1FD0-\\u1FD3\\u1FD6-\\u1FDB\\u1FE0-\\u1FEC\\u1FF2-\\u1FF4\\u1FF6-\\u1FFC\\u2071\\u207F\\u2090-\\u209C\\u2102\\u2107\\u210A-\\u2113\\u2115\\u2118-\\u211D\\u2124\\u2126\\u2128\\u212A-\\u2139\\u213C-\\u213F\\u2145-\\u2149\\u214E\\u2160-\\u2188\\u2C00-\\u2CE4\\u2CEB-\\u2CEE\\u2CF2\\u2CF3\\u2D00-\\u2D25\\u2D27\\u2D2D\\u2D30-\\u2D67\\u2D6F\\u2D80-\\u2D96\\u2DA0-\\u2DA6\\u2DA8-\\u2DAE\\u2DB0-\\u2DB6\\u2DB8-\\u2DBE\\u2DC0-\\u2DC6\\u2DC8-\\u2DCE\\u2DD0-\\u2DD6\\u2DD8-\\u2DDE\\u3005-\\u3007\\u3021-\\u3029\\u3031-\\u3035\\u3038-\\u303C\\u3041-\\u3096\\u309B-\\u309F\\u30A1-\\u30FA\\u30FC-\\u30FF\\u3105-\\u312F\\u3131-\\u318E\\u31A0-\\u31BF\\u31F0-\\u31FF\\u3400-\\u4DBF\\u4E00-\\uA48C\\uA4D0-\\uA4FD\\uA500-\\uA60C\\uA610-\\uA61F\\uA62A\\uA62B\\uA640-\\uA66E\\uA67F-\\uA69D\\uA6A0-\\uA6EF\\uA717-\\uA71F\\uA722-\\uA788\\uA78B-\\uA7CD\\uA7D0\\uA7D1\\uA7D3\\uA7D5-\\uA7DC\\uA7F2-\\uA801\\uA803-\\uA805\\uA807-\\uA80A\\uA80C-\\uA822\\uA840-\\uA873\\uA882-\\uA8B3\\uA8F2-\\uA8F7\\uA8FB\\uA8FD\\uA8FE\\uA90A-\\uA925\\uA930-\\uA946\\uA960-\\uA97C\\uA984-\\uA9B2\\uA9CF\\uA9E0-\\uA9E4\\uA9E6-\\uA9EF\\uA9FA-\\uA9FE\\uAA00-\\uAA28\\uAA40-\\uAA42\\uAA44-\\uAA4B\\uAA60-\\uAA76\\uAA7A\\uAA7E-\\uAAAF\\uAAB1\\uAAB5\\uAAB6\\uAAB9-\\uAABD\\uAAC0\\uAAC2\\uAADB-\\uAADD\\uAAE0-\\uAAEA\\uAAF2-\\uAAF4\\uAB01-\\uAB06\\uAB09-\\uAB0E\\uAB11-\\uAB16\\uAB20-\\uAB26\\uAB28-\\uAB2E\\uAB30-\\uAB5A\\uAB5C-\\uAB69\\uAB70-\\uABE2\\uAC00-\\uD7A3\\uD7B0-\\uD7C6\\uD7CB-\\uD7FB\\uF900-\\uFA6D\\uFA70-\\uFAD9\\uFB00-\\uFB06\\uFB13-\\uFB17\\uFB1D\\uFB1F-\\uFB28\\uFB2A-\\uFB36\\uFB38-\\uFB3C\\uFB3E\\uFB40\\uFB41\\uFB43\\uFB44\\uFB46-\\uFBB1\\uFBD3-\\uFD3D\\uFD50-\\uFD8F\\uFD92-\\uFDC7\\uFDF0-\\uFDFB\\uFE70-\\uFE74\\uFE76-\\uFEFC\\uFF21-\\uFF3A\\uFF41-\\uFF5A\\uFF66-\\uFFBE\\uFFC2-\\uFFC7\\uFFCA-\\uFFCF\\uFFD2-\\uFFD7\\uFFDA-\\uFFDC',\n        a = {\n          3: 'abstract boolean byte char class double enum export extends final float goto implements import int interface long native package private protected public short static super synchronized throws transient volatile',\n          5: 'class enum extends super const export import',\n          6: 'enum',\n          strict:\n            'implements interface let package private protected public static yield',\n          strictBind: 'eval arguments',\n        },\n        u =\n          'break case catch continue debugger default do else finally for function if return switch throw try var while with null true false instanceof typeof void delete new in this',\n        d = {\n          5: u,\n          '5module': u + ' export import',\n          6: u + ' const class extends export import super',\n        },\n        y = /^in(stanceof)?$/,\n        g = new RegExp('[' + r + ']'),\n        L = new RegExp('[' + r + i + ']');\n      function p(n, o) {\n        for (var l = 65536, f = 0; f < o.length; f += 2) {\n          if (((l += o[f]), l > n)) return !1;\n          if (((l += o[f + 1]), l >= n)) return !0;\n        }\n        return !1;\n      }\n      function h(n, o) {\n        return n < 65\n          ? n === 36\n          : n < 91\n          ? !0\n          : n < 97\n          ? n === 95\n          : n < 123\n          ? !0\n          : n <= 65535\n          ? n >= 170 && g.test(String.fromCharCode(n))\n          : o === !1\n          ? !1\n          : p(n, s);\n      }\n      function T(n, o) {\n        return n < 48\n          ? n === 36\n          : n < 58\n          ? !0\n          : n < 65\n          ? !1\n          : n < 91\n          ? !0\n          : n < 97\n          ? n === 95\n          : n < 123\n          ? !0\n          : n <= 65535\n          ? n >= 170 && L.test(String.fromCharCode(n))\n          : o === !1\n          ? !1\n          : p(n, s) || p(n, t);\n      }\n      var x = function (o, l) {\n        l === void 0 && (l = {}),\n          (this.label = o),\n          (this.keyword = l.keyword),\n          (this.beforeExpr = !!l.beforeExpr),\n          (this.startsExpr = !!l.startsExpr),\n          (this.isLoop = !!l.isLoop),\n          (this.isAssign = !!l.isAssign),\n          (this.prefix = !!l.prefix),\n          (this.postfix = !!l.postfix),\n          (this.binop = l.binop || null),\n          (this.updateContext = null);\n      };\n      function w(n, o) {\n        return new x(n, {beforeExpr: !0, binop: o});\n      }\n      var S = {beforeExpr: !0},\n        A = {startsExpr: !0},\n        U = {};\n      function M(n, o) {\n        return o === void 0 && (o = {}), (o.keyword = n), (U[n] = new x(n, o));\n      }\n      var c = {\n          num: new x('num', A),\n          regexp: new x('regexp', A),\n          string: new x('string', A),\n          name: new x('name', A),\n          privateId: new x('privateId', A),\n          eof: new x('eof'),\n          bracketL: new x('[', {beforeExpr: !0, startsExpr: !0}),\n          bracketR: new x(']'),\n          braceL: new x('{', {beforeExpr: !0, startsExpr: !0}),\n          braceR: new x('}'),\n          parenL: new x('(', {beforeExpr: !0, startsExpr: !0}),\n          parenR: new x(')'),\n          comma: new x(',', S),\n          semi: new x(';', S),\n          colon: new x(':', S),\n          dot: new x('.'),\n          question: new x('?', S),\n          questionDot: new x('?.'),\n          arrow: new x('=>', S),\n          template: new x('template'),\n          invalidTemplate: new x('invalidTemplate'),\n          ellipsis: new x('...', S),\n          backQuote: new x('`', A),\n          dollarBraceL: new x('${', {beforeExpr: !0, startsExpr: !0}),\n          eq: new x('=', {beforeExpr: !0, isAssign: !0}),\n          assign: new x('_=', {beforeExpr: !0, isAssign: !0}),\n          incDec: new x('++/--', {prefix: !0, postfix: !0, startsExpr: !0}),\n          prefix: new x('!/~', {beforeExpr: !0, prefix: !0, startsExpr: !0}),\n          logicalOR: w('||', 1),\n          logicalAND: w('&&', 2),\n          bitwiseOR: w('|', 3),\n          bitwiseXOR: w('^', 4),\n          bitwiseAND: w('&', 5),\n          equality: w('==/!=/===/!==', 6),\n          relational: w('</>/<=/>=', 7),\n          bitShift: w('<</>>/>>>', 8),\n          plusMin: new x('+/-', {\n            beforeExpr: !0,\n            binop: 9,\n            prefix: !0,\n            startsExpr: !0,\n          }),\n          modulo: w('%', 10),\n          star: w('*', 10),\n          slash: w('/', 10),\n          starstar: new x('**', {beforeExpr: !0}),\n          coalesce: w('??', 1),\n          _break: M('break'),\n          _case: M('case', S),\n          _catch: M('catch'),\n          _continue: M('continue'),\n          _debugger: M('debugger'),\n          _default: M('default', S),\n          _do: M('do', {isLoop: !0, beforeExpr: !0}),\n          _else: M('else', S),\n          _finally: M('finally'),\n          _for: M('for', {isLoop: !0}),\n          _function: M('function', A),\n          _if: M('if'),\n          _return: M('return', S),\n          _switch: M('switch'),\n          _throw: M('throw', S),\n          _try: M('try'),\n          _var: M('var'),\n          _const: M('const'),\n          _while: M('while', {isLoop: !0}),\n          _with: M('with'),\n          _new: M('new', {beforeExpr: !0, startsExpr: !0}),\n          _this: M('this', A),\n          _super: M('super', A),\n          _class: M('class', A),\n          _extends: M('extends', S),\n          _export: M('export'),\n          _import: M('import', A),\n          _null: M('null', A),\n          _true: M('true', A),\n          _false: M('false', A),\n          _in: M('in', {beforeExpr: !0, binop: 7}),\n          _instanceof: M('instanceof', {beforeExpr: !0, binop: 7}),\n          _typeof: M('typeof', {beforeExpr: !0, prefix: !0, startsExpr: !0}),\n          _void: M('void', {beforeExpr: !0, prefix: !0, startsExpr: !0}),\n          _delete: M('delete', {beforeExpr: !0, prefix: !0, startsExpr: !0}),\n        },\n        R = /\\r\\n?|\\n|\\u2028|\\u2029/,\n        W = new RegExp(R.source, 'g');\n      function X(n) {\n        return n === 10 || n === 13 || n === 8232 || n === 8233;\n      }\n      function ie(n, o, l) {\n        l === void 0 && (l = n.length);\n        for (var f = o; f < l; f++) {\n          var m = n.charCodeAt(f);\n          if (X(m))\n            return f < l - 1 && m === 13 && n.charCodeAt(f + 1) === 10\n              ? f + 2\n              : f + 1;\n        }\n        return -1;\n      }\n      var pe = /[\\u1680\\u2000-\\u200a\\u202f\\u205f\\u3000\\ufeff]/,\n        ae = /(?:\\s|\\/\\/.*|\\/\\*[^]*?\\*\\/)*/g,\n        He = Object.prototype,\n        qe = He.hasOwnProperty,\n        Bt = He.toString,\n        mt =\n          Object.hasOwn ||\n          function (n, o) {\n            return qe.call(n, o);\n          },\n        kt =\n          Array.isArray ||\n          function (n) {\n            return Bt.call(n) === '[object Array]';\n          },\n        At = Object.create(null);\n      function tt(n) {\n        return (\n          At[n] || (At[n] = new RegExp('^(?:' + n.replace(/ /g, '|') + ')$'))\n        );\n      }\n      function nt(n) {\n        return n <= 65535\n          ? String.fromCharCode(n)\n          : ((n -= 65536),\n            String.fromCharCode((n >> 10) + 55296, (n & 1023) + 56320));\n      }\n      var _t =\n          /(?:[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|(?:[^\\uD800-\\uDBFF]|^)[\\uDC00-\\uDFFF])/,\n        ct = function (o, l) {\n          (this.line = o), (this.column = l);\n        };\n      ct.prototype.offset = function (o) {\n        return new ct(this.line, this.column + o);\n      };\n      var wt = function (o, l, f) {\n        (this.start = l),\n          (this.end = f),\n          o.sourceFile !== null && (this.source = o.sourceFile);\n      };\n      function $t(n, o) {\n        for (var l = 1, f = 0; ; ) {\n          var m = ie(n, f, o);\n          if (m < 0) return new ct(l, o - f);\n          ++l, (f = m);\n        }\n      }\n      var Pt = {\n          ecmaVersion: null,\n          sourceType: 'script',\n          onInsertedSemicolon: null,\n          onTrailingComma: null,\n          allowReserved: null,\n          allowReturnOutsideFunction: !1,\n          allowImportExportEverywhere: !1,\n          allowAwaitOutsideFunction: null,\n          allowSuperOutsideMethod: null,\n          allowHashBang: !1,\n          checkPrivateFields: !0,\n          locations: !1,\n          onToken: null,\n          onComment: null,\n          ranges: !1,\n          program: null,\n          sourceFile: null,\n          directSourceFile: null,\n          preserveParens: !1,\n        },\n        qt = !1;\n      function Tn(n) {\n        var o = {};\n        for (var l in Pt) o[l] = n && mt(n, l) ? n[l] : Pt[l];\n        if (\n          (o.ecmaVersion === 'latest'\n            ? (o.ecmaVersion = 1e8)\n            : o.ecmaVersion == null\n            ? (!qt &&\n                typeof console == 'object' &&\n                console.warn &&\n                ((qt = !0),\n                console.warn(`Since Acorn 8.0.0, options.ecmaVersion is required.\nDefaulting to 2020, but this will stop working in the future.`)),\n              (o.ecmaVersion = 11))\n            : o.ecmaVersion >= 2015 && (o.ecmaVersion -= 2009),\n          o.allowReserved == null && (o.allowReserved = o.ecmaVersion < 5),\n          (!n || n.allowHashBang == null) &&\n            (o.allowHashBang = o.ecmaVersion >= 14),\n          kt(o.onToken))\n        ) {\n          var f = o.onToken;\n          o.onToken = function (m) {\n            return f.push(m);\n          };\n        }\n        return kt(o.onComment) && (o.onComment = V(o, o.onComment)), o;\n      }\n      function V(n, o) {\n        return function (l, f, m, E, O, Y) {\n          var Q = {type: l ? 'Block' : 'Line', value: f, start: m, end: E};\n          n.locations && (Q.loc = new wt(this, O, Y)),\n            n.ranges && (Q.range = [m, E]),\n            o.push(Q);\n        };\n      }\n      var G = 1,\n        J = 2,\n        re = 4,\n        ve = 8,\n        he = 16,\n        Ie = 32,\n        Ee = 64,\n        Le = 128,\n        Xe = 256,\n        We = 512,\n        Ke = G | J | Xe;\n      function ut(n, o) {\n        return J | (n ? re : 0) | (o ? ve : 0);\n      }\n      var pt = 0,\n        bt = 1,\n        yt = 2,\n        vt = 3,\n        bn = 4,\n        Dn = 5,\n        Ge = function (o, l, f) {\n          (this.options = o = Tn(o)),\n            (this.sourceFile = o.sourceFile),\n            (this.keywords = tt(\n              d[\n                o.ecmaVersion >= 6\n                  ? 6\n                  : o.sourceType === 'module'\n                  ? '5module'\n                  : 5\n              ]\n            ));\n          var m = '';\n          o.allowReserved !== !0 &&\n            ((m = a[o.ecmaVersion >= 6 ? 6 : o.ecmaVersion === 5 ? 5 : 3]),\n            o.sourceType === 'module' && (m += ' await')),\n            (this.reservedWords = tt(m));\n          var E = (m ? m + ' ' : '') + a.strict;\n          (this.reservedWordsStrict = tt(E)),\n            (this.reservedWordsStrictBind = tt(E + ' ' + a.strictBind)),\n            (this.input = String(l)),\n            (this.containsEsc = !1),\n            f\n              ? ((this.pos = f),\n                (this.lineStart =\n                  this.input.lastIndexOf(\n                    `\n`,\n                    f - 1\n                  ) + 1),\n                (this.curLine = this.input\n                  .slice(0, this.lineStart)\n                  .split(R).length))\n              : ((this.pos = this.lineStart = 0), (this.curLine = 1)),\n            (this.type = c.eof),\n            (this.value = null),\n            (this.start = this.end = this.pos),\n            (this.startLoc = this.endLoc = this.curPosition()),\n            (this.lastTokEndLoc = this.lastTokStartLoc = null),\n            (this.lastTokStart = this.lastTokEnd = this.pos),\n            (this.context = this.initialContext()),\n            (this.exprAllowed = !0),\n            (this.inModule = o.sourceType === 'module'),\n            (this.strict = this.inModule || this.strictDirective(this.pos)),\n            (this.potentialArrowAt = -1),\n            (this.potentialArrowInForAwait = !1),\n            (this.yieldPos = this.awaitPos = this.awaitIdentPos = 0),\n            (this.labels = []),\n            (this.undefinedExports = Object.create(null)),\n            this.pos === 0 &&\n              o.allowHashBang &&\n              this.input.slice(0, 2) === '#!' &&\n              this.skipLineComment(2),\n            (this.scopeStack = []),\n            this.enterScope(G),\n            (this.regexpState = null),\n            (this.privateNameStack = []);\n        },\n        St = {\n          inFunction: {configurable: !0},\n          inGenerator: {configurable: !0},\n          inAsync: {configurable: !0},\n          canAwait: {configurable: !0},\n          allowSuper: {configurable: !0},\n          allowDirectSuper: {configurable: !0},\n          treatFunctionsAsVar: {configurable: !0},\n          allowNewDotTarget: {configurable: !0},\n          inClassStaticBlock: {configurable: !0},\n        };\n      (Ge.prototype.parse = function () {\n        var o = this.options.program || this.startNode();\n        return this.nextToken(), this.parseTopLevel(o);\n      }),\n        (St.inFunction.get = function () {\n          return (this.currentVarScope().flags & J) > 0;\n        }),\n        (St.inGenerator.get = function () {\n          return (this.currentVarScope().flags & ve) > 0;\n        }),\n        (St.inAsync.get = function () {\n          return (this.currentVarScope().flags & re) > 0;\n        }),\n        (St.canAwait.get = function () {\n          for (var n = this.scopeStack.length - 1; n >= 0; n--) {\n            var o = this.scopeStack[n],\n              l = o.flags;\n            if (l & (Xe | We)) return !1;\n            if (l & J) return (l & re) > 0;\n          }\n          return (\n            (this.inModule && this.options.ecmaVersion >= 13) ||\n            this.options.allowAwaitOutsideFunction\n          );\n        }),\n        (St.allowSuper.get = function () {\n          var n = this.currentThisScope(),\n            o = n.flags;\n          return (o & Ee) > 0 || this.options.allowSuperOutsideMethod;\n        }),\n        (St.allowDirectSuper.get = function () {\n          return (this.currentThisScope().flags & Le) > 0;\n        }),\n        (St.treatFunctionsAsVar.get = function () {\n          return this.treatFunctionsAsVarInScope(this.currentScope());\n        }),\n        (St.allowNewDotTarget.get = function () {\n          for (var n = this.scopeStack.length - 1; n >= 0; n--) {\n            var o = this.scopeStack[n],\n              l = o.flags;\n            if (l & (Xe | We) || (l & J && !(l & he))) return !0;\n          }\n          return !1;\n        }),\n        (St.inClassStaticBlock.get = function () {\n          return (this.currentVarScope().flags & Xe) > 0;\n        }),\n        (Ge.extend = function () {\n          for (var o = [], l = arguments.length; l--; ) o[l] = arguments[l];\n          for (var f = this, m = 0; m < o.length; m++) f = o[m](f);\n          return f;\n        }),\n        (Ge.parse = function (o, l) {\n          return new this(l, o).parse();\n        }),\n        (Ge.parseExpressionAt = function (o, l, f) {\n          var m = new this(f, o, l);\n          return m.nextToken(), m.parseExpression();\n        }),\n        (Ge.tokenizer = function (o, l) {\n          return new this(l, o);\n        }),\n        Object.defineProperties(Ge.prototype, St);\n      var ot = Ge.prototype,\n        zt = /^(?:'((?:\\\\[^]|[^'\\\\])*?)'|\"((?:\\\\[^]|[^\"\\\\])*?)\")/;\n      (ot.strictDirective = function (n) {\n        if (this.options.ecmaVersion < 5) return !1;\n        for (;;) {\n          (ae.lastIndex = n), (n += ae.exec(this.input)[0].length);\n          var o = zt.exec(this.input.slice(n));\n          if (!o) return !1;\n          if ((o[1] || o[2]) === 'use strict') {\n            ae.lastIndex = n + o[0].length;\n            var l = ae.exec(this.input),\n              f = l.index + l[0].length,\n              m = this.input.charAt(f);\n            return (\n              m === ';' ||\n              m === '}' ||\n              (R.test(l[0]) &&\n                !(\n                  /[(`.[+\\-/*%<>=,?^&]/.test(m) ||\n                  (m === '!' && this.input.charAt(f + 1) === '=')\n                ))\n            );\n          }\n          (n += o[0].length),\n            (ae.lastIndex = n),\n            (n += ae.exec(this.input)[0].length),\n            this.input[n] === ';' && n++;\n        }\n      }),\n        (ot.eat = function (n) {\n          return this.type === n ? (this.next(), !0) : !1;\n        }),\n        (ot.isContextual = function (n) {\n          return this.type === c.name && this.value === n && !this.containsEsc;\n        }),\n        (ot.eatContextual = function (n) {\n          return this.isContextual(n) ? (this.next(), !0) : !1;\n        }),\n        (ot.expectContextual = function (n) {\n          this.eatContextual(n) || this.unexpected();\n        }),\n        (ot.canInsertSemicolon = function () {\n          return (\n            this.type === c.eof ||\n            this.type === c.braceR ||\n            R.test(this.input.slice(this.lastTokEnd, this.start))\n          );\n        }),\n        (ot.insertSemicolon = function () {\n          if (this.canInsertSemicolon())\n            return (\n              this.options.onInsertedSemicolon &&\n                this.options.onInsertedSemicolon(\n                  this.lastTokEnd,\n                  this.lastTokEndLoc\n                ),\n              !0\n            );\n        }),\n        (ot.semicolon = function () {\n          !this.eat(c.semi) && !this.insertSemicolon() && this.unexpected();\n        }),\n        (ot.afterTrailingComma = function (n, o) {\n          if (this.type === n)\n            return (\n              this.options.onTrailingComma &&\n                this.options.onTrailingComma(\n                  this.lastTokStart,\n                  this.lastTokStartLoc\n                ),\n              o || this.next(),\n              !0\n            );\n        }),\n        (ot.expect = function (n) {\n          this.eat(n) || this.unexpected();\n        }),\n        (ot.unexpected = function (n) {\n          this.raise(n ?? this.start, 'Unexpected token');\n        });\n      var Xt = function () {\n        this.shorthandAssign =\n          this.trailingComma =\n          this.parenthesizedAssign =\n          this.parenthesizedBind =\n          this.doubleProto =\n            -1;\n      };\n      (ot.checkPatternErrors = function (n, o) {\n        if (n) {\n          n.trailingComma > -1 &&\n            this.raiseRecoverable(\n              n.trailingComma,\n              'Comma is not permitted after the rest element'\n            );\n          var l = o ? n.parenthesizedAssign : n.parenthesizedBind;\n          l > -1 &&\n            this.raiseRecoverable(\n              l,\n              o ? 'Assigning to rvalue' : 'Parenthesized pattern'\n            );\n        }\n      }),\n        (ot.checkExpressionErrors = function (n, o) {\n          if (!n) return !1;\n          var l = n.shorthandAssign,\n            f = n.doubleProto;\n          if (!o) return l >= 0 || f >= 0;\n          l >= 0 &&\n            this.raise(\n              l,\n              'Shorthand property assignments are valid only in destructuring patterns'\n            ),\n            f >= 0 &&\n              this.raiseRecoverable(f, 'Redefinition of __proto__ property');\n        }),\n        (ot.checkYieldAwaitInDefaultParams = function () {\n          this.yieldPos &&\n            (!this.awaitPos || this.yieldPos < this.awaitPos) &&\n            this.raise(\n              this.yieldPos,\n              'Yield expression cannot be a default value'\n            ),\n            this.awaitPos &&\n              this.raise(\n                this.awaitPos,\n                'Await expression cannot be a default value'\n              );\n        }),\n        (ot.isSimpleAssignTarget = function (n) {\n          return n.type === 'ParenthesizedExpression'\n            ? this.isSimpleAssignTarget(n.expression)\n            : n.type === 'Identifier' || n.type === 'MemberExpression';\n        });\n      var te = Ge.prototype;\n      te.parseTopLevel = function (n) {\n        var o = Object.create(null);\n        for (n.body || (n.body = []); this.type !== c.eof; ) {\n          var l = this.parseStatement(null, !0, o);\n          n.body.push(l);\n        }\n        if (this.inModule)\n          for (\n            var f = 0, m = Object.keys(this.undefinedExports);\n            f < m.length;\n            f += 1\n          ) {\n            var E = m[f];\n            this.raiseRecoverable(\n              this.undefinedExports[E].start,\n              \"Export '\" + E + \"' is not defined\"\n            );\n          }\n        return (\n          this.adaptDirectivePrologue(n.body),\n          this.next(),\n          (n.sourceType = this.options.sourceType),\n          this.finishNode(n, 'Program')\n        );\n      };\n      var Cn = {kind: 'loop'},\n        Zn = {kind: 'switch'};\n      (te.isLet = function (n) {\n        if (this.options.ecmaVersion < 6 || !this.isContextual('let'))\n          return !1;\n        ae.lastIndex = this.pos;\n        var o = ae.exec(this.input),\n          l = this.pos + o[0].length,\n          f = this.input.charCodeAt(l);\n        if (f === 91 || f === 92) return !0;\n        if (n) return !1;\n        if (f === 123 || (f > 55295 && f < 56320)) return !0;\n        if (h(f, !0)) {\n          for (var m = l + 1; T((f = this.input.charCodeAt(m)), !0); ) ++m;\n          if (f === 92 || (f > 55295 && f < 56320)) return !0;\n          var E = this.input.slice(l, m);\n          if (!y.test(E)) return !0;\n        }\n        return !1;\n      }),\n        (te.isAsyncFunction = function () {\n          if (this.options.ecmaVersion < 8 || !this.isContextual('async'))\n            return !1;\n          ae.lastIndex = this.pos;\n          var n = ae.exec(this.input),\n            o = this.pos + n[0].length,\n            l;\n          return (\n            !R.test(this.input.slice(this.pos, o)) &&\n            this.input.slice(o, o + 8) === 'function' &&\n            (o + 8 === this.input.length ||\n              !(\n                T((l = this.input.charCodeAt(o + 8))) ||\n                (l > 55295 && l < 56320)\n              ))\n          );\n        }),\n        (te.isUsingKeyword = function (n, o) {\n          if (\n            this.options.ecmaVersion < 17 ||\n            !this.isContextual(n ? 'await' : 'using')\n          )\n            return !1;\n          ae.lastIndex = this.pos;\n          var l = ae.exec(this.input),\n            f = this.pos + l[0].length;\n          if (R.test(this.input.slice(this.pos, f))) return !1;\n          if (n) {\n            var m = f + 5,\n              E;\n            if (\n              this.input.slice(f, m) !== 'using' ||\n              m === this.input.length ||\n              T((E = this.input.charCodeAt(m))) ||\n              (E > 55295 && E < 56320)\n            )\n              return !1;\n            ae.lastIndex = m;\n            var O = ae.exec(this.input);\n            if (O && R.test(this.input.slice(m, m + O[0].length))) return !1;\n          }\n          if (o) {\n            var Y = f + 2,\n              Q;\n            if (\n              this.input.slice(f, Y) === 'of' &&\n              (Y === this.input.length ||\n                (!T((Q = this.input.charCodeAt(Y))) &&\n                  !(Q > 55295 && Q < 56320)))\n            )\n              return !1;\n          }\n          var Te = this.input.charCodeAt(f);\n          return h(Te, !0) || Te === 92;\n        }),\n        (te.isAwaitUsing = function (n) {\n          return this.isUsingKeyword(!0, n);\n        }),\n        (te.isUsing = function (n) {\n          return this.isUsingKeyword(!1, n);\n        }),\n        (te.parseStatement = function (n, o, l) {\n          var f = this.type,\n            m = this.startNode(),\n            E;\n          switch ((this.isLet(n) && ((f = c._var), (E = 'let')), f)) {\n            case c._break:\n            case c._continue:\n              return this.parseBreakContinueStatement(m, f.keyword);\n            case c._debugger:\n              return this.parseDebuggerStatement(m);\n            case c._do:\n              return this.parseDoStatement(m);\n            case c._for:\n              return this.parseForStatement(m);\n            case c._function:\n              return (\n                n &&\n                  (this.strict || (n !== 'if' && n !== 'label')) &&\n                  this.options.ecmaVersion >= 6 &&\n                  this.unexpected(),\n                this.parseFunctionStatement(m, !1, !n)\n              );\n            case c._class:\n              return n && this.unexpected(), this.parseClass(m, !0);\n            case c._if:\n              return this.parseIfStatement(m);\n            case c._return:\n              return this.parseReturnStatement(m);\n            case c._switch:\n              return this.parseSwitchStatement(m);\n            case c._throw:\n              return this.parseThrowStatement(m);\n            case c._try:\n              return this.parseTryStatement(m);\n            case c._const:\n            case c._var:\n              return (\n                (E = E || this.value),\n                n && E !== 'var' && this.unexpected(),\n                this.parseVarStatement(m, E)\n              );\n            case c._while:\n              return this.parseWhileStatement(m);\n            case c._with:\n              return this.parseWithStatement(m);\n            case c.braceL:\n              return this.parseBlock(!0, m);\n            case c.semi:\n              return this.parseEmptyStatement(m);\n            case c._export:\n            case c._import:\n              if (this.options.ecmaVersion > 10 && f === c._import) {\n                ae.lastIndex = this.pos;\n                var O = ae.exec(this.input),\n                  Y = this.pos + O[0].length,\n                  Q = this.input.charCodeAt(Y);\n                if (Q === 40 || Q === 46)\n                  return this.parseExpressionStatement(\n                    m,\n                    this.parseExpression()\n                  );\n              }\n              return (\n                this.options.allowImportExportEverywhere ||\n                  (o ||\n                    this.raise(\n                      this.start,\n                      \"'import' and 'export' may only appear at the top level\"\n                    ),\n                  this.inModule ||\n                    this.raise(\n                      this.start,\n                      \"'import' and 'export' may appear only with 'sourceType: module'\"\n                    )),\n                f === c._import ? this.parseImport(m) : this.parseExport(m, l)\n              );\n            default:\n              if (this.isAsyncFunction())\n                return (\n                  n && this.unexpected(),\n                  this.next(),\n                  this.parseFunctionStatement(m, !0, !n)\n                );\n              var Te = this.isAwaitUsing(!1)\n                ? 'await using'\n                : this.isUsing(!1)\n                ? 'using'\n                : null;\n              if (Te)\n                return (\n                  o &&\n                    this.options.sourceType === 'script' &&\n                    this.raise(\n                      this.start,\n                      'Using declaration cannot appear in the top level when source type is `script`'\n                    ),\n                  Te === 'await using' &&\n                    (this.canAwait ||\n                      this.raise(\n                        this.start,\n                        'Await using cannot appear outside of async function'\n                      ),\n                    this.next()),\n                  this.next(),\n                  this.parseVar(m, !1, Te),\n                  this.semicolon(),\n                  this.finishNode(m, 'VariableDeclaration')\n                );\n              var xe = this.value,\n                Ze = this.parseExpression();\n              return f === c.name &&\n                Ze.type === 'Identifier' &&\n                this.eat(c.colon)\n                ? this.parseLabeledStatement(m, xe, Ze, n)\n                : this.parseExpressionStatement(m, Ze);\n          }\n        }),\n        (te.parseBreakContinueStatement = function (n, o) {\n          var l = o === 'break';\n          this.next(),\n            this.eat(c.semi) || this.insertSemicolon()\n              ? (n.label = null)\n              : this.type !== c.name\n              ? this.unexpected()\n              : ((n.label = this.parseIdent()), this.semicolon());\n          for (var f = 0; f < this.labels.length; ++f) {\n            var m = this.labels[f];\n            if (\n              (n.label == null || m.name === n.label.name) &&\n              ((m.kind != null && (l || m.kind === 'loop')) || (n.label && l))\n            )\n              break;\n          }\n          return (\n            f === this.labels.length && this.raise(n.start, 'Unsyntactic ' + o),\n            this.finishNode(n, l ? 'BreakStatement' : 'ContinueStatement')\n          );\n        }),\n        (te.parseDebuggerStatement = function (n) {\n          return (\n            this.next(),\n            this.semicolon(),\n            this.finishNode(n, 'DebuggerStatement')\n          );\n        }),\n        (te.parseDoStatement = function (n) {\n          return (\n            this.next(),\n            this.labels.push(Cn),\n            (n.body = this.parseStatement('do')),\n            this.labels.pop(),\n            this.expect(c._while),\n            (n.test = this.parseParenExpression()),\n            this.options.ecmaVersion >= 6 ? this.eat(c.semi) : this.semicolon(),\n            this.finishNode(n, 'DoWhileStatement')\n          );\n        }),\n        (te.parseForStatement = function (n) {\n          this.next();\n          var o =\n            this.options.ecmaVersion >= 9 &&\n            this.canAwait &&\n            this.eatContextual('await')\n              ? this.lastTokStart\n              : -1;\n          if (\n            (this.labels.push(Cn),\n            this.enterScope(0),\n            this.expect(c.parenL),\n            this.type === c.semi)\n          )\n            return o > -1 && this.unexpected(o), this.parseFor(n, null);\n          var l = this.isLet();\n          if (this.type === c._var || this.type === c._const || l) {\n            var f = this.startNode(),\n              m = l ? 'let' : this.value;\n            return (\n              this.next(),\n              this.parseVar(f, !0, m),\n              this.finishNode(f, 'VariableDeclaration'),\n              this.parseForAfterInit(n, f, o)\n            );\n          }\n          var E = this.isContextual('let'),\n            O = !1,\n            Y = this.isUsing(!0)\n              ? 'using'\n              : this.isAwaitUsing(!0)\n              ? 'await using'\n              : null;\n          if (Y) {\n            var Q = this.startNode();\n            return (\n              this.next(),\n              Y === 'await using' && this.next(),\n              this.parseVar(Q, !0, Y),\n              this.finishNode(Q, 'VariableDeclaration'),\n              this.parseForAfterInit(n, Q, o)\n            );\n          }\n          var Te = this.containsEsc,\n            xe = new Xt(),\n            Ze = this.start,\n            Lt =\n              o > -1\n                ? this.parseExprSubscripts(xe, 'await')\n                : this.parseExpression(!0, xe);\n          return this.type === c._in ||\n            (O = this.options.ecmaVersion >= 6 && this.isContextual('of'))\n            ? (o > -1\n                ? (this.type === c._in && this.unexpected(o), (n.await = !0))\n                : O &&\n                  this.options.ecmaVersion >= 8 &&\n                  (Lt.start === Ze &&\n                  !Te &&\n                  Lt.type === 'Identifier' &&\n                  Lt.name === 'async'\n                    ? this.unexpected()\n                    : this.options.ecmaVersion >= 9 && (n.await = !1)),\n              E &&\n                O &&\n                this.raise(\n                  Lt.start,\n                  \"The left-hand side of a for-of loop may not start with 'let'.\"\n                ),\n              this.toAssignable(Lt, !1, xe),\n              this.checkLValPattern(Lt),\n              this.parseForIn(n, Lt))\n            : (this.checkExpressionErrors(xe, !0),\n              o > -1 && this.unexpected(o),\n              this.parseFor(n, Lt));\n        }),\n        (te.parseForAfterInit = function (n, o, l) {\n          return (this.type === c._in ||\n            (this.options.ecmaVersion >= 6 && this.isContextual('of'))) &&\n            o.declarations.length === 1\n            ? (this.options.ecmaVersion >= 9 &&\n                (this.type === c._in\n                  ? l > -1 && this.unexpected(l)\n                  : (n.await = l > -1)),\n              this.parseForIn(n, o))\n            : (l > -1 && this.unexpected(l), this.parseFor(n, o));\n        }),\n        (te.parseFunctionStatement = function (n, o, l) {\n          return this.next(), this.parseFunction(n, Mn | (l ? 0 : xs), !1, o);\n        }),\n        (te.parseIfStatement = function (n) {\n          return (\n            this.next(),\n            (n.test = this.parseParenExpression()),\n            (n.consequent = this.parseStatement('if')),\n            (n.alternate = this.eat(c._else)\n              ? this.parseStatement('if')\n              : null),\n            this.finishNode(n, 'IfStatement')\n          );\n        }),\n        (te.parseReturnStatement = function (n) {\n          return (\n            !this.inFunction &&\n              !this.options.allowReturnOutsideFunction &&\n              this.raise(this.start, \"'return' outside of function\"),\n            this.next(),\n            this.eat(c.semi) || this.insertSemicolon()\n              ? (n.argument = null)\n              : ((n.argument = this.parseExpression()), this.semicolon()),\n            this.finishNode(n, 'ReturnStatement')\n          );\n        }),\n        (te.parseSwitchStatement = function (n) {\n          this.next(),\n            (n.discriminant = this.parseParenExpression()),\n            (n.cases = []),\n            this.expect(c.braceL),\n            this.labels.push(Zn),\n            this.enterScope(0);\n          for (var o, l = !1; this.type !== c.braceR; )\n            if (this.type === c._case || this.type === c._default) {\n              var f = this.type === c._case;\n              o && this.finishNode(o, 'SwitchCase'),\n                n.cases.push((o = this.startNode())),\n                (o.consequent = []),\n                this.next(),\n                f\n                  ? (o.test = this.parseExpression())\n                  : (l &&\n                      this.raiseRecoverable(\n                        this.lastTokStart,\n                        'Multiple default clauses'\n                      ),\n                    (l = !0),\n                    (o.test = null)),\n                this.expect(c.colon);\n            } else\n              o || this.unexpected(),\n                o.consequent.push(this.parseStatement(null));\n          return (\n            this.exitScope(),\n            o && this.finishNode(o, 'SwitchCase'),\n            this.next(),\n            this.labels.pop(),\n            this.finishNode(n, 'SwitchStatement')\n          );\n        }),\n        (te.parseThrowStatement = function (n) {\n          return (\n            this.next(),\n            R.test(this.input.slice(this.lastTokEnd, this.start)) &&\n              this.raise(this.lastTokEnd, 'Illegal newline after throw'),\n            (n.argument = this.parseExpression()),\n            this.semicolon(),\n            this.finishNode(n, 'ThrowStatement')\n          );\n        });\n      var _i = [];\n      (te.parseCatchClauseParam = function () {\n        var n = this.parseBindingAtom(),\n          o = n.type === 'Identifier';\n        return (\n          this.enterScope(o ? Ie : 0),\n          this.checkLValPattern(n, o ? bn : yt),\n          this.expect(c.parenR),\n          n\n        );\n      }),\n        (te.parseTryStatement = function (n) {\n          if (\n            (this.next(),\n            (n.block = this.parseBlock()),\n            (n.handler = null),\n            this.type === c._catch)\n          ) {\n            var o = this.startNode();\n            this.next(),\n              this.eat(c.parenL)\n                ? (o.param = this.parseCatchClauseParam())\n                : (this.options.ecmaVersion < 10 && this.unexpected(),\n                  (o.param = null),\n                  this.enterScope(0)),\n              (o.body = this.parseBlock(!1)),\n              this.exitScope(),\n              (n.handler = this.finishNode(o, 'CatchClause'));\n          }\n          return (\n            (n.finalizer = this.eat(c._finally) ? this.parseBlock() : null),\n            !n.handler &&\n              !n.finalizer &&\n              this.raise(n.start, 'Missing catch or finally clause'),\n            this.finishNode(n, 'TryStatement')\n          );\n        }),\n        (te.parseVarStatement = function (n, o, l) {\n          return (\n            this.next(),\n            this.parseVar(n, !1, o, l),\n            this.semicolon(),\n            this.finishNode(n, 'VariableDeclaration')\n          );\n        }),\n        (te.parseWhileStatement = function (n) {\n          return (\n            this.next(),\n            (n.test = this.parseParenExpression()),\n            this.labels.push(Cn),\n            (n.body = this.parseStatement('while')),\n            this.labels.pop(),\n            this.finishNode(n, 'WhileStatement')\n          );\n        }),\n        (te.parseWithStatement = function (n) {\n          return (\n            this.strict && this.raise(this.start, \"'with' in strict mode\"),\n            this.next(),\n            (n.object = this.parseParenExpression()),\n            (n.body = this.parseStatement('with')),\n            this.finishNode(n, 'WithStatement')\n          );\n        }),\n        (te.parseEmptyStatement = function (n) {\n          return this.next(), this.finishNode(n, 'EmptyStatement');\n        }),\n        (te.parseLabeledStatement = function (n, o, l, f) {\n          for (var m = 0, E = this.labels; m < E.length; m += 1) {\n            var O = E[m];\n            O.name === o &&\n              this.raise(l.start, \"Label '\" + o + \"' is already declared\");\n          }\n          for (\n            var Y = this.type.isLoop\n                ? 'loop'\n                : this.type === c._switch\n                ? 'switch'\n                : null,\n              Q = this.labels.length - 1;\n            Q >= 0;\n            Q--\n          ) {\n            var Te = this.labels[Q];\n            if (Te.statementStart === n.start)\n              (Te.statementStart = this.start), (Te.kind = Y);\n            else break;\n          }\n          return (\n            this.labels.push({name: o, kind: Y, statementStart: this.start}),\n            (n.body = this.parseStatement(\n              f ? (f.indexOf('label') === -1 ? f + 'label' : f) : 'label'\n            )),\n            this.labels.pop(),\n            (n.label = l),\n            this.finishNode(n, 'LabeledStatement')\n          );\n        }),\n        (te.parseExpressionStatement = function (n, o) {\n          return (\n            (n.expression = o),\n            this.semicolon(),\n            this.finishNode(n, 'ExpressionStatement')\n          );\n        }),\n        (te.parseBlock = function (n, o, l) {\n          for (\n            n === void 0 && (n = !0),\n              o === void 0 && (o = this.startNode()),\n              o.body = [],\n              this.expect(c.braceL),\n              n && this.enterScope(0);\n            this.type !== c.braceR;\n\n          ) {\n            var f = this.parseStatement(null);\n            o.body.push(f);\n          }\n          return (\n            l && (this.strict = !1),\n            this.next(),\n            n && this.exitScope(),\n            this.finishNode(o, 'BlockStatement')\n          );\n        }),\n        (te.parseFor = function (n, o) {\n          return (\n            (n.init = o),\n            this.expect(c.semi),\n            (n.test = this.type === c.semi ? null : this.parseExpression()),\n            this.expect(c.semi),\n            (n.update = this.type === c.parenR ? null : this.parseExpression()),\n            this.expect(c.parenR),\n            (n.body = this.parseStatement('for')),\n            this.exitScope(),\n            this.labels.pop(),\n            this.finishNode(n, 'ForStatement')\n          );\n        }),\n        (te.parseForIn = function (n, o) {\n          var l = this.type === c._in;\n          return (\n            this.next(),\n            o.type === 'VariableDeclaration' &&\n              o.declarations[0].init != null &&\n              (!l ||\n                this.options.ecmaVersion < 8 ||\n                this.strict ||\n                o.kind !== 'var' ||\n                o.declarations[0].id.type !== 'Identifier') &&\n              this.raise(\n                o.start,\n                (l ? 'for-in' : 'for-of') +\n                  ' loop variable declaration may not have an initializer'\n              ),\n            (n.left = o),\n            (n.right = l ? this.parseExpression() : this.parseMaybeAssign()),\n            this.expect(c.parenR),\n            (n.body = this.parseStatement('for')),\n            this.exitScope(),\n            this.labels.pop(),\n            this.finishNode(n, l ? 'ForInStatement' : 'ForOfStatement')\n          );\n        }),\n        (te.parseVar = function (n, o, l, f) {\n          for (n.declarations = [], n.kind = l; ; ) {\n            var m = this.startNode();\n            if (\n              (this.parseVarId(m, l),\n              this.eat(c.eq)\n                ? (m.init = this.parseMaybeAssign(o))\n                : !f &&\n                  l === 'const' &&\n                  !(\n                    this.type === c._in ||\n                    (this.options.ecmaVersion >= 6 && this.isContextual('of'))\n                  )\n                ? this.unexpected()\n                : !f &&\n                  (l === 'using' || l === 'await using') &&\n                  this.options.ecmaVersion >= 17 &&\n                  this.type !== c._in &&\n                  !this.isContextual('of')\n                ? this.raise(\n                    this.lastTokEnd,\n                    'Missing initializer in ' + l + ' declaration'\n                  )\n                : !f &&\n                  m.id.type !== 'Identifier' &&\n                  !(o && (this.type === c._in || this.isContextual('of')))\n                ? this.raise(\n                    this.lastTokEnd,\n                    'Complex binding patterns require an initialization value'\n                  )\n                : (m.init = null),\n              n.declarations.push(this.finishNode(m, 'VariableDeclarator')),\n              !this.eat(c.comma))\n            )\n              break;\n          }\n          return n;\n        }),\n        (te.parseVarId = function (n, o) {\n          (n.id =\n            o === 'using' || o === 'await using'\n              ? this.parseIdent()\n              : this.parseBindingAtom()),\n            this.checkLValPattern(n.id, o === 'var' ? bt : yt, !1);\n        });\n      var Mn = 1,\n        xs = 2,\n        Ds = 4;\n      (te.parseFunction = function (n, o, l, f, m) {\n        this.initFunction(n),\n          (this.options.ecmaVersion >= 9 ||\n            (this.options.ecmaVersion >= 6 && !f)) &&\n            (this.type === c.star && o & xs && this.unexpected(),\n            (n.generator = this.eat(c.star))),\n          this.options.ecmaVersion >= 8 && (n.async = !!f),\n          o & Mn &&\n            ((n.id = o & Ds && this.type !== c.name ? null : this.parseIdent()),\n            n.id &&\n              !(o & xs) &&\n              this.checkLValSimple(\n                n.id,\n                this.strict || n.generator || n.async\n                  ? this.treatFunctionsAsVar\n                    ? bt\n                    : yt\n                  : vt\n              ));\n        var E = this.yieldPos,\n          O = this.awaitPos,\n          Y = this.awaitIdentPos;\n        return (\n          (this.yieldPos = 0),\n          (this.awaitPos = 0),\n          (this.awaitIdentPos = 0),\n          this.enterScope(ut(n.async, n.generator)),\n          o & Mn || (n.id = this.type === c.name ? this.parseIdent() : null),\n          this.parseFunctionParams(n),\n          this.parseFunctionBody(n, l, !1, m),\n          (this.yieldPos = E),\n          (this.awaitPos = O),\n          (this.awaitIdentPos = Y),\n          this.finishNode(\n            n,\n            o & Mn ? 'FunctionDeclaration' : 'FunctionExpression'\n          )\n        );\n      }),\n        (te.parseFunctionParams = function (n) {\n          this.expect(c.parenL),\n            (n.params = this.parseBindingList(\n              c.parenR,\n              !1,\n              this.options.ecmaVersion >= 8\n            )),\n            this.checkYieldAwaitInDefaultParams();\n        }),\n        (te.parseClass = function (n, o) {\n          this.next();\n          var l = this.strict;\n          (this.strict = !0), this.parseClassId(n, o), this.parseClassSuper(n);\n          var f = this.enterClassBody(),\n            m = this.startNode(),\n            E = !1;\n          for (m.body = [], this.expect(c.braceL); this.type !== c.braceR; ) {\n            var O = this.parseClassElement(n.superClass !== null);\n            O &&\n              (m.body.push(O),\n              O.type === 'MethodDefinition' && O.kind === 'constructor'\n                ? (E &&\n                    this.raiseRecoverable(\n                      O.start,\n                      'Duplicate constructor in the same class'\n                    ),\n                  (E = !0))\n                : O.key &&\n                  O.key.type === 'PrivateIdentifier' &&\n                  bi(f, O) &&\n                  this.raiseRecoverable(\n                    O.key.start,\n                    \"Identifier '#\" + O.key.name + \"' has already been declared\"\n                  ));\n          }\n          return (\n            (this.strict = l),\n            this.next(),\n            (n.body = this.finishNode(m, 'ClassBody')),\n            this.exitClassBody(),\n            this.finishNode(n, o ? 'ClassDeclaration' : 'ClassExpression')\n          );\n        }),\n        (te.parseClassElement = function (n) {\n          if (this.eat(c.semi)) return null;\n          var o = this.options.ecmaVersion,\n            l = this.startNode(),\n            f = '',\n            m = !1,\n            E = !1,\n            O = 'method',\n            Y = !1;\n          if (this.eatContextual('static')) {\n            if (o >= 13 && this.eat(c.braceL))\n              return this.parseClassStaticBlock(l), l;\n            this.isClassElementNameStart() || this.type === c.star\n              ? (Y = !0)\n              : (f = 'static');\n          }\n          if (\n            ((l.static = Y),\n            !f &&\n              o >= 8 &&\n              this.eatContextual('async') &&\n              ((this.isClassElementNameStart() || this.type === c.star) &&\n              !this.canInsertSemicolon()\n                ? (E = !0)\n                : (f = 'async')),\n            !f && (o >= 9 || !E) && this.eat(c.star) && (m = !0),\n            !f && !E && !m)\n          ) {\n            var Q = this.value;\n            (this.eatContextual('get') || this.eatContextual('set')) &&\n              (this.isClassElementNameStart() ? (O = Q) : (f = Q));\n          }\n          if (\n            (f\n              ? ((l.computed = !1),\n                (l.key = this.startNodeAt(\n                  this.lastTokStart,\n                  this.lastTokStartLoc\n                )),\n                (l.key.name = f),\n                this.finishNode(l.key, 'Identifier'))\n              : this.parseClassElementName(l),\n            o < 13 || this.type === c.parenL || O !== 'method' || m || E)\n          ) {\n            var Te = !l.static && es(l, 'constructor'),\n              xe = Te && n;\n            Te &&\n              O !== 'method' &&\n              this.raise(\n                l.key.start,\n                \"Constructor can't have get/set modifier\"\n              ),\n              (l.kind = Te ? 'constructor' : O),\n              this.parseClassMethod(l, m, E, xe);\n          } else this.parseClassField(l);\n          return l;\n        }),\n        (te.isClassElementNameStart = function () {\n          return (\n            this.type === c.name ||\n            this.type === c.privateId ||\n            this.type === c.num ||\n            this.type === c.string ||\n            this.type === c.bracketL ||\n            this.type.keyword\n          );\n        }),\n        (te.parseClassElementName = function (n) {\n          this.type === c.privateId\n            ? (this.value === 'constructor' &&\n                this.raise(\n                  this.start,\n                  \"Classes can't have an element named '#constructor'\"\n                ),\n              (n.computed = !1),\n              (n.key = this.parsePrivateIdent()))\n            : this.parsePropertyName(n);\n        }),\n        (te.parseClassMethod = function (n, o, l, f) {\n          var m = n.key;\n          n.kind === 'constructor'\n            ? (o && this.raise(m.start, \"Constructor can't be a generator\"),\n              l && this.raise(m.start, \"Constructor can't be an async method\"))\n            : n.static &&\n              es(n, 'prototype') &&\n              this.raise(\n                m.start,\n                'Classes may not have a static property named prototype'\n              );\n          var E = (n.value = this.parseMethod(o, l, f));\n          return (\n            n.kind === 'get' &&\n              E.params.length !== 0 &&\n              this.raiseRecoverable(E.start, 'getter should have no params'),\n            n.kind === 'set' &&\n              E.params.length !== 1 &&\n              this.raiseRecoverable(\n                E.start,\n                'setter should have exactly one param'\n              ),\n            n.kind === 'set' &&\n              E.params[0].type === 'RestElement' &&\n              this.raiseRecoverable(\n                E.params[0].start,\n                'Setter cannot use rest params'\n              ),\n            this.finishNode(n, 'MethodDefinition')\n          );\n        }),\n        (te.parseClassField = function (n) {\n          return (\n            es(n, 'constructor')\n              ? this.raise(\n                  n.key.start,\n                  \"Classes can't have a field named 'constructor'\"\n                )\n              : n.static &&\n                es(n, 'prototype') &&\n                this.raise(\n                  n.key.start,\n                  \"Classes can't have a static field named 'prototype'\"\n                ),\n            this.eat(c.eq)\n              ? (this.enterScope(We | Ee),\n                (n.value = this.parseMaybeAssign()),\n                this.exitScope())\n              : (n.value = null),\n            this.semicolon(),\n            this.finishNode(n, 'PropertyDefinition')\n          );\n        }),\n        (te.parseClassStaticBlock = function (n) {\n          n.body = [];\n          var o = this.labels;\n          for (\n            this.labels = [], this.enterScope(Xe | Ee);\n            this.type !== c.braceR;\n\n          ) {\n            var l = this.parseStatement(null);\n            n.body.push(l);\n          }\n          return (\n            this.next(),\n            this.exitScope(),\n            (this.labels = o),\n            this.finishNode(n, 'StaticBlock')\n          );\n        }),\n        (te.parseClassId = function (n, o) {\n          this.type === c.name\n            ? ((n.id = this.parseIdent()),\n              o && this.checkLValSimple(n.id, yt, !1))\n            : (o === !0 && this.unexpected(), (n.id = null));\n        }),\n        (te.parseClassSuper = function (n) {\n          n.superClass = this.eat(c._extends)\n            ? this.parseExprSubscripts(null, !1)\n            : null;\n        }),\n        (te.enterClassBody = function () {\n          var n = {declared: Object.create(null), used: []};\n          return this.privateNameStack.push(n), n.declared;\n        }),\n        (te.exitClassBody = function () {\n          var n = this.privateNameStack.pop(),\n            o = n.declared,\n            l = n.used;\n          if (this.options.checkPrivateFields)\n            for (\n              var f = this.privateNameStack.length,\n                m = f === 0 ? null : this.privateNameStack[f - 1],\n                E = 0;\n              E < l.length;\n              ++E\n            ) {\n              var O = l[E];\n              mt(o, O.name) ||\n                (m\n                  ? m.used.push(O)\n                  : this.raiseRecoverable(\n                      O.start,\n                      \"Private field '#\" +\n                        O.name +\n                        \"' must be declared in an enclosing class\"\n                    ));\n            }\n        });\n      function bi(n, o) {\n        var l = o.key.name,\n          f = n[l],\n          m = 'true';\n        return (\n          o.type === 'MethodDefinition' &&\n            (o.kind === 'get' || o.kind === 'set') &&\n            (m = (o.static ? 's' : 'i') + o.kind),\n          (f === 'iget' && m === 'iset') ||\n          (f === 'iset' && m === 'iget') ||\n          (f === 'sget' && m === 'sset') ||\n          (f === 'sset' && m === 'sget')\n            ? ((n[l] = 'true'), !1)\n            : f\n            ? !0\n            : ((n[l] = m), !1)\n        );\n      }\n      function es(n, o) {\n        var l = n.computed,\n          f = n.key;\n        return (\n          !l &&\n          ((f.type === 'Identifier' && f.name === o) ||\n            (f.type === 'Literal' && f.value === o))\n        );\n      }\n      (te.parseExportAllDeclaration = function (n, o) {\n        return (\n          this.options.ecmaVersion >= 11 &&\n            (this.eatContextual('as')\n              ? ((n.exported = this.parseModuleExportName()),\n                this.checkExport(o, n.exported, this.lastTokStart))\n              : (n.exported = null)),\n          this.expectContextual('from'),\n          this.type !== c.string && this.unexpected(),\n          (n.source = this.parseExprAtom()),\n          this.options.ecmaVersion >= 16 &&\n            (n.attributes = this.parseWithClause()),\n          this.semicolon(),\n          this.finishNode(n, 'ExportAllDeclaration')\n        );\n      }),\n        (te.parseExport = function (n, o) {\n          if ((this.next(), this.eat(c.star)))\n            return this.parseExportAllDeclaration(n, o);\n          if (this.eat(c._default))\n            return (\n              this.checkExport(o, 'default', this.lastTokStart),\n              (n.declaration = this.parseExportDefaultDeclaration()),\n              this.finishNode(n, 'ExportDefaultDeclaration')\n            );\n          if (this.shouldParseExportStatement())\n            (n.declaration = this.parseExportDeclaration(n)),\n              n.declaration.type === 'VariableDeclaration'\n                ? this.checkVariableExport(o, n.declaration.declarations)\n                : this.checkExport(o, n.declaration.id, n.declaration.id.start),\n              (n.specifiers = []),\n              (n.source = null),\n              this.options.ecmaVersion >= 16 && (n.attributes = []);\n          else {\n            if (\n              ((n.declaration = null),\n              (n.specifiers = this.parseExportSpecifiers(o)),\n              this.eatContextual('from'))\n            )\n              this.type !== c.string && this.unexpected(),\n                (n.source = this.parseExprAtom()),\n                this.options.ecmaVersion >= 16 &&\n                  (n.attributes = this.parseWithClause());\n            else {\n              for (var l = 0, f = n.specifiers; l < f.length; l += 1) {\n                var m = f[l];\n                this.checkUnreserved(m.local),\n                  this.checkLocalExport(m.local),\n                  m.local.type === 'Literal' &&\n                    this.raise(\n                      m.local.start,\n                      'A string literal cannot be used as an exported binding without `from`.'\n                    );\n              }\n              (n.source = null),\n                this.options.ecmaVersion >= 16 && (n.attributes = []);\n            }\n            this.semicolon();\n          }\n          return this.finishNode(n, 'ExportNamedDeclaration');\n        }),\n        (te.parseExportDeclaration = function (n) {\n          return this.parseStatement(null);\n        }),\n        (te.parseExportDefaultDeclaration = function () {\n          var n;\n          if (this.type === c._function || (n = this.isAsyncFunction())) {\n            var o = this.startNode();\n            return (\n              this.next(),\n              n && this.next(),\n              this.parseFunction(o, Mn | Ds, !1, n)\n            );\n          } else if (this.type === c._class) {\n            var l = this.startNode();\n            return this.parseClass(l, 'nullableID');\n          } else {\n            var f = this.parseMaybeAssign();\n            return this.semicolon(), f;\n          }\n        }),\n        (te.checkExport = function (n, o, l) {\n          n &&\n            (typeof o != 'string' &&\n              (o = o.type === 'Identifier' ? o.name : o.value),\n            mt(n, o) &&\n              this.raiseRecoverable(l, \"Duplicate export '\" + o + \"'\"),\n            (n[o] = !0));\n        }),\n        (te.checkPatternExport = function (n, o) {\n          var l = o.type;\n          if (l === 'Identifier') this.checkExport(n, o, o.start);\n          else if (l === 'ObjectPattern')\n            for (var f = 0, m = o.properties; f < m.length; f += 1) {\n              var E = m[f];\n              this.checkPatternExport(n, E);\n            }\n          else if (l === 'ArrayPattern')\n            for (var O = 0, Y = o.elements; O < Y.length; O += 1) {\n              var Q = Y[O];\n              Q && this.checkPatternExport(n, Q);\n            }\n          else\n            l === 'Property'\n              ? this.checkPatternExport(n, o.value)\n              : l === 'AssignmentPattern'\n              ? this.checkPatternExport(n, o.left)\n              : l === 'RestElement' && this.checkPatternExport(n, o.argument);\n        }),\n        (te.checkVariableExport = function (n, o) {\n          if (n)\n            for (var l = 0, f = o; l < f.length; l += 1) {\n              var m = f[l];\n              this.checkPatternExport(n, m.id);\n            }\n        }),\n        (te.shouldParseExportStatement = function () {\n          return (\n            this.type.keyword === 'var' ||\n            this.type.keyword === 'const' ||\n            this.type.keyword === 'class' ||\n            this.type.keyword === 'function' ||\n            this.isLet() ||\n            this.isAsyncFunction()\n          );\n        }),\n        (te.parseExportSpecifier = function (n) {\n          var o = this.startNode();\n          return (\n            (o.local = this.parseModuleExportName()),\n            (o.exported = this.eatContextual('as')\n              ? this.parseModuleExportName()\n              : o.local),\n            this.checkExport(n, o.exported, o.exported.start),\n            this.finishNode(o, 'ExportSpecifier')\n          );\n        }),\n        (te.parseExportSpecifiers = function (n) {\n          var o = [],\n            l = !0;\n          for (this.expect(c.braceL); !this.eat(c.braceR); ) {\n            if (l) l = !1;\n            else if ((this.expect(c.comma), this.afterTrailingComma(c.braceR)))\n              break;\n            o.push(this.parseExportSpecifier(n));\n          }\n          return o;\n        }),\n        (te.parseImport = function (n) {\n          return (\n            this.next(),\n            this.type === c.string\n              ? ((n.specifiers = _i), (n.source = this.parseExprAtom()))\n              : ((n.specifiers = this.parseImportSpecifiers()),\n                this.expectContextual('from'),\n                (n.source =\n                  this.type === c.string\n                    ? this.parseExprAtom()\n                    : this.unexpected())),\n            this.options.ecmaVersion >= 16 &&\n              (n.attributes = this.parseWithClause()),\n            this.semicolon(),\n            this.finishNode(n, 'ImportDeclaration')\n          );\n        }),\n        (te.parseImportSpecifier = function () {\n          var n = this.startNode();\n          return (\n            (n.imported = this.parseModuleExportName()),\n            this.eatContextual('as')\n              ? (n.local = this.parseIdent())\n              : (this.checkUnreserved(n.imported), (n.local = n.imported)),\n            this.checkLValSimple(n.local, yt),\n            this.finishNode(n, 'ImportSpecifier')\n          );\n        }),\n        (te.parseImportDefaultSpecifier = function () {\n          var n = this.startNode();\n          return (\n            (n.local = this.parseIdent()),\n            this.checkLValSimple(n.local, yt),\n            this.finishNode(n, 'ImportDefaultSpecifier')\n          );\n        }),\n        (te.parseImportNamespaceSpecifier = function () {\n          var n = this.startNode();\n          return (\n            this.next(),\n            this.expectContextual('as'),\n            (n.local = this.parseIdent()),\n            this.checkLValSimple(n.local, yt),\n            this.finishNode(n, 'ImportNamespaceSpecifier')\n          );\n        }),\n        (te.parseImportSpecifiers = function () {\n          var n = [],\n            o = !0;\n          if (\n            this.type === c.name &&\n            (n.push(this.parseImportDefaultSpecifier()), !this.eat(c.comma))\n          )\n            return n;\n          if (this.type === c.star)\n            return n.push(this.parseImportNamespaceSpecifier()), n;\n          for (this.expect(c.braceL); !this.eat(c.braceR); ) {\n            if (o) o = !1;\n            else if ((this.expect(c.comma), this.afterTrailingComma(c.braceR)))\n              break;\n            n.push(this.parseImportSpecifier());\n          }\n          return n;\n        }),\n        (te.parseWithClause = function () {\n          var n = [];\n          if (!this.eat(c._with)) return n;\n          this.expect(c.braceL);\n          for (var o = {}, l = !0; !this.eat(c.braceR); ) {\n            if (l) l = !1;\n            else if ((this.expect(c.comma), this.afterTrailingComma(c.braceR)))\n              break;\n            var f = this.parseImportAttribute(),\n              m = f.key.type === 'Identifier' ? f.key.name : f.key.value;\n            mt(o, m) &&\n              this.raiseRecoverable(\n                f.key.start,\n                \"Duplicate attribute key '\" + m + \"'\"\n              ),\n              (o[m] = !0),\n              n.push(f);\n          }\n          return n;\n        }),\n        (te.parseImportAttribute = function () {\n          var n = this.startNode();\n          return (\n            (n.key =\n              this.type === c.string\n                ? this.parseExprAtom()\n                : this.parseIdent(this.options.allowReserved !== 'never')),\n            this.expect(c.colon),\n            this.type !== c.string && this.unexpected(),\n            (n.value = this.parseExprAtom()),\n            this.finishNode(n, 'ImportAttribute')\n          );\n        }),\n        (te.parseModuleExportName = function () {\n          if (this.options.ecmaVersion >= 13 && this.type === c.string) {\n            var n = this.parseLiteral(this.value);\n            return (\n              _t.test(n.value) &&\n                this.raise(\n                  n.start,\n                  'An export name cannot include a lone surrogate.'\n                ),\n              n\n            );\n          }\n          return this.parseIdent(!0);\n        }),\n        (te.adaptDirectivePrologue = function (n) {\n          for (var o = 0; o < n.length && this.isDirectiveCandidate(n[o]); ++o)\n            n[o].directive = n[o].expression.raw.slice(1, -1);\n        }),\n        (te.isDirectiveCandidate = function (n) {\n          return (\n            this.options.ecmaVersion >= 5 &&\n            n.type === 'ExpressionStatement' &&\n            n.expression.type === 'Literal' &&\n            typeof n.expression.value == 'string' &&\n            (this.input[n.start] === '\"' || this.input[n.start] === \"'\")\n          );\n        });\n      var Nt = Ge.prototype;\n      (Nt.toAssignable = function (n, o, l) {\n        if (this.options.ecmaVersion >= 6 && n)\n          switch (n.type) {\n            case 'Identifier':\n              this.inAsync &&\n                n.name === 'await' &&\n                this.raise(\n                  n.start,\n                  \"Cannot use 'await' as identifier inside an async function\"\n                );\n              break;\n            case 'ObjectPattern':\n            case 'ArrayPattern':\n            case 'AssignmentPattern':\n            case 'RestElement':\n              break;\n            case 'ObjectExpression':\n              (n.type = 'ObjectPattern'), l && this.checkPatternErrors(l, !0);\n              for (var f = 0, m = n.properties; f < m.length; f += 1) {\n                var E = m[f];\n                this.toAssignable(E, o),\n                  E.type === 'RestElement' &&\n                    (E.argument.type === 'ArrayPattern' ||\n                      E.argument.type === 'ObjectPattern') &&\n                    this.raise(E.argument.start, 'Unexpected token');\n              }\n              break;\n            case 'Property':\n              n.kind !== 'init' &&\n                this.raise(\n                  n.key.start,\n                  \"Object pattern can't contain getter or setter\"\n                ),\n                this.toAssignable(n.value, o);\n              break;\n            case 'ArrayExpression':\n              (n.type = 'ArrayPattern'),\n                l && this.checkPatternErrors(l, !0),\n                this.toAssignableList(n.elements, o);\n              break;\n            case 'SpreadElement':\n              (n.type = 'RestElement'),\n                this.toAssignable(n.argument, o),\n                n.argument.type === 'AssignmentPattern' &&\n                  this.raise(\n                    n.argument.start,\n                    'Rest elements cannot have a default value'\n                  );\n              break;\n            case 'AssignmentExpression':\n              n.operator !== '=' &&\n                this.raise(\n                  n.left.end,\n                  \"Only '=' operator can be used for specifying default value.\"\n                ),\n                (n.type = 'AssignmentPattern'),\n                delete n.operator,\n                this.toAssignable(n.left, o);\n              break;\n            case 'ParenthesizedExpression':\n              this.toAssignable(n.expression, o, l);\n              break;\n            case 'ChainExpression':\n              this.raiseRecoverable(\n                n.start,\n                'Optional chaining cannot appear in left-hand side'\n              );\n              break;\n            case 'MemberExpression':\n              if (!o) break;\n            default:\n              this.raise(n.start, 'Assigning to rvalue');\n          }\n        else l && this.checkPatternErrors(l, !0);\n        return n;\n      }),\n        (Nt.toAssignableList = function (n, o) {\n          for (var l = n.length, f = 0; f < l; f++) {\n            var m = n[f];\n            m && this.toAssignable(m, o);\n          }\n          if (l) {\n            var E = n[l - 1];\n            this.options.ecmaVersion === 6 &&\n              o &&\n              E &&\n              E.type === 'RestElement' &&\n              E.argument.type !== 'Identifier' &&\n              this.unexpected(E.argument.start);\n          }\n          return n;\n        }),\n        (Nt.parseSpread = function (n) {\n          var o = this.startNode();\n          return (\n            this.next(),\n            (o.argument = this.parseMaybeAssign(!1, n)),\n            this.finishNode(o, 'SpreadElement')\n          );\n        }),\n        (Nt.parseRestBinding = function () {\n          var n = this.startNode();\n          return (\n            this.next(),\n            this.options.ecmaVersion === 6 &&\n              this.type !== c.name &&\n              this.unexpected(),\n            (n.argument = this.parseBindingAtom()),\n            this.finishNode(n, 'RestElement')\n          );\n        }),\n        (Nt.parseBindingAtom = function () {\n          if (this.options.ecmaVersion >= 6)\n            switch (this.type) {\n              case c.bracketL:\n                var n = this.startNode();\n                return (\n                  this.next(),\n                  (n.elements = this.parseBindingList(c.bracketR, !0, !0)),\n                  this.finishNode(n, 'ArrayPattern')\n                );\n              case c.braceL:\n                return this.parseObj(!0);\n            }\n          return this.parseIdent();\n        }),\n        (Nt.parseBindingList = function (n, o, l, f) {\n          for (var m = [], E = !0; !this.eat(n); )\n            if (\n              (E ? (E = !1) : this.expect(c.comma), o && this.type === c.comma)\n            )\n              m.push(null);\n            else {\n              if (l && this.afterTrailingComma(n)) break;\n              if (this.type === c.ellipsis) {\n                var O = this.parseRestBinding();\n                this.parseBindingListItem(O),\n                  m.push(O),\n                  this.type === c.comma &&\n                    this.raiseRecoverable(\n                      this.start,\n                      'Comma is not permitted after the rest element'\n                    ),\n                  this.expect(n);\n                break;\n              } else m.push(this.parseAssignableListItem(f));\n            }\n          return m;\n        }),\n        (Nt.parseAssignableListItem = function (n) {\n          var o = this.parseMaybeDefault(this.start, this.startLoc);\n          return this.parseBindingListItem(o), o;\n        }),\n        (Nt.parseBindingListItem = function (n) {\n          return n;\n        }),\n        (Nt.parseMaybeDefault = function (n, o, l) {\n          if (\n            ((l = l || this.parseBindingAtom()),\n            this.options.ecmaVersion < 6 || !this.eat(c.eq))\n          )\n            return l;\n          var f = this.startNodeAt(n, o);\n          return (\n            (f.left = l),\n            (f.right = this.parseMaybeAssign()),\n            this.finishNode(f, 'AssignmentPattern')\n          );\n        }),\n        (Nt.checkLValSimple = function (n, o, l) {\n          o === void 0 && (o = pt);\n          var f = o !== pt;\n          switch (n.type) {\n            case 'Identifier':\n              this.strict &&\n                this.reservedWordsStrictBind.test(n.name) &&\n                this.raiseRecoverable(\n                  n.start,\n                  (f ? 'Binding ' : 'Assigning to ') +\n                    n.name +\n                    ' in strict mode'\n                ),\n                f &&\n                  (o === yt &&\n                    n.name === 'let' &&\n                    this.raiseRecoverable(\n                      n.start,\n                      'let is disallowed as a lexically bound name'\n                    ),\n                  l &&\n                    (mt(l, n.name) &&\n                      this.raiseRecoverable(n.start, 'Argument name clash'),\n                    (l[n.name] = !0)),\n                  o !== Dn && this.declareName(n.name, o, n.start));\n              break;\n            case 'ChainExpression':\n              this.raiseRecoverable(\n                n.start,\n                'Optional chaining cannot appear in left-hand side'\n              );\n              break;\n            case 'MemberExpression':\n              f && this.raiseRecoverable(n.start, 'Binding member expression');\n              break;\n            case 'ParenthesizedExpression':\n              return (\n                f &&\n                  this.raiseRecoverable(\n                    n.start,\n                    'Binding parenthesized expression'\n                  ),\n                this.checkLValSimple(n.expression, o, l)\n              );\n            default:\n              this.raise(n.start, (f ? 'Binding' : 'Assigning to') + ' rvalue');\n          }\n        }),\n        (Nt.checkLValPattern = function (n, o, l) {\n          switch ((o === void 0 && (o = pt), n.type)) {\n            case 'ObjectPattern':\n              for (var f = 0, m = n.properties; f < m.length; f += 1) {\n                var E = m[f];\n                this.checkLValInnerPattern(E, o, l);\n              }\n              break;\n            case 'ArrayPattern':\n              for (var O = 0, Y = n.elements; O < Y.length; O += 1) {\n                var Q = Y[O];\n                Q && this.checkLValInnerPattern(Q, o, l);\n              }\n              break;\n            default:\n              this.checkLValSimple(n, o, l);\n          }\n        }),\n        (Nt.checkLValInnerPattern = function (n, o, l) {\n          switch ((o === void 0 && (o = pt), n.type)) {\n            case 'Property':\n              this.checkLValInnerPattern(n.value, o, l);\n              break;\n            case 'AssignmentPattern':\n              this.checkLValPattern(n.left, o, l);\n              break;\n            case 'RestElement':\n              this.checkLValPattern(n.argument, o, l);\n              break;\n            default:\n              this.checkLValPattern(n, o, l);\n          }\n        });\n      var Rt = function (o, l, f, m, E) {\n          (this.token = o),\n            (this.isExpr = !!l),\n            (this.preserveSpace = !!f),\n            (this.override = m),\n            (this.generator = !!E);\n        },\n        Ue = {\n          b_stat: new Rt('{', !1),\n          b_expr: new Rt('{', !0),\n          b_tmpl: new Rt('${', !1),\n          p_stat: new Rt('(', !1),\n          p_expr: new Rt('(', !0),\n          q_tmpl: new Rt('`', !0, !0, function (n) {\n            return n.tryReadTemplateToken();\n          }),\n          f_stat: new Rt('function', !1),\n          f_expr: new Rt('function', !0),\n          f_expr_gen: new Rt('function', !0, !1, null, !0),\n          f_gen: new Rt('function', !1, !1, null, !0),\n        },\n        wn = Ge.prototype;\n      (wn.initialContext = function () {\n        return [Ue.b_stat];\n      }),\n        (wn.curContext = function () {\n          return this.context[this.context.length - 1];\n        }),\n        (wn.braceIsBlock = function (n) {\n          var o = this.curContext();\n          return o === Ue.f_expr || o === Ue.f_stat\n            ? !0\n            : n === c.colon && (o === Ue.b_stat || o === Ue.b_expr)\n            ? !o.isExpr\n            : n === c._return || (n === c.name && this.exprAllowed)\n            ? R.test(this.input.slice(this.lastTokEnd, this.start))\n            : n === c._else ||\n              n === c.semi ||\n              n === c.eof ||\n              n === c.parenR ||\n              n === c.arrow\n            ? !0\n            : n === c.braceL\n            ? o === Ue.b_stat\n            : n === c._var || n === c._const || n === c.name\n            ? !1\n            : !this.exprAllowed;\n        }),\n        (wn.inGeneratorContext = function () {\n          for (var n = this.context.length - 1; n >= 1; n--) {\n            var o = this.context[n];\n            if (o.token === 'function') return o.generator;\n          }\n          return !1;\n        }),\n        (wn.updateContext = function (n) {\n          var o,\n            l = this.type;\n          l.keyword && n === c.dot\n            ? (this.exprAllowed = !1)\n            : (o = l.updateContext)\n            ? o.call(this, n)\n            : (this.exprAllowed = l.beforeExpr);\n        }),\n        (wn.overrideContext = function (n) {\n          this.curContext() !== n &&\n            (this.context[this.context.length - 1] = n);\n        }),\n        (c.parenR.updateContext = c.braceR.updateContext =\n          function () {\n            if (this.context.length === 1) {\n              this.exprAllowed = !0;\n              return;\n            }\n            var n = this.context.pop();\n            n === Ue.b_stat &&\n              this.curContext().token === 'function' &&\n              (n = this.context.pop()),\n              (this.exprAllowed = !n.isExpr);\n          }),\n        (c.braceL.updateContext = function (n) {\n          this.context.push(this.braceIsBlock(n) ? Ue.b_stat : Ue.b_expr),\n            (this.exprAllowed = !0);\n        }),\n        (c.dollarBraceL.updateContext = function () {\n          this.context.push(Ue.b_tmpl), (this.exprAllowed = !0);\n        }),\n        (c.parenL.updateContext = function (n) {\n          var o =\n            n === c._if || n === c._for || n === c._with || n === c._while;\n          this.context.push(o ? Ue.p_stat : Ue.p_expr), (this.exprAllowed = !0);\n        }),\n        (c.incDec.updateContext = function () {}),\n        (c._function.updateContext = c._class.updateContext =\n          function (n) {\n            n.beforeExpr &&\n            n !== c._else &&\n            !(n === c.semi && this.curContext() !== Ue.p_stat) &&\n            !(\n              n === c._return &&\n              R.test(this.input.slice(this.lastTokEnd, this.start))\n            ) &&\n            !(\n              (n === c.colon || n === c.braceL) &&\n              this.curContext() === Ue.b_stat\n            )\n              ? this.context.push(Ue.f_expr)\n              : this.context.push(Ue.f_stat),\n              (this.exprAllowed = !1);\n          }),\n        (c.colon.updateContext = function () {\n          this.curContext().token === 'function' && this.context.pop(),\n            (this.exprAllowed = !0);\n        }),\n        (c.backQuote.updateContext = function () {\n          this.curContext() === Ue.q_tmpl\n            ? this.context.pop()\n            : this.context.push(Ue.q_tmpl),\n            (this.exprAllowed = !1);\n        }),\n        (c.star.updateContext = function (n) {\n          if (n === c._function) {\n            var o = this.context.length - 1;\n            this.context[o] === Ue.f_expr\n              ? (this.context[o] = Ue.f_expr_gen)\n              : (this.context[o] = Ue.f_gen);\n          }\n          this.exprAllowed = !0;\n        }),\n        (c.name.updateContext = function (n) {\n          var o = !1;\n          this.options.ecmaVersion >= 6 &&\n            n !== c.dot &&\n            ((this.value === 'of' && !this.exprAllowed) ||\n              (this.value === 'yield' && this.inGeneratorContext())) &&\n            (o = !0),\n            (this.exprAllowed = o);\n        });\n      var de = Ge.prototype;\n      (de.checkPropClash = function (n, o, l) {\n        if (\n          !(this.options.ecmaVersion >= 9 && n.type === 'SpreadElement') &&\n          !(\n            this.options.ecmaVersion >= 6 &&\n            (n.computed || n.method || n.shorthand)\n          )\n        ) {\n          var f = n.key,\n            m;\n          switch (f.type) {\n            case 'Identifier':\n              m = f.name;\n              break;\n            case 'Literal':\n              m = String(f.value);\n              break;\n            default:\n              return;\n          }\n          var E = n.kind;\n          if (this.options.ecmaVersion >= 6) {\n            m === '__proto__' &&\n              E === 'init' &&\n              (o.proto &&\n                (l\n                  ? l.doubleProto < 0 && (l.doubleProto = f.start)\n                  : this.raiseRecoverable(\n                      f.start,\n                      'Redefinition of __proto__ property'\n                    )),\n              (o.proto = !0));\n            return;\n          }\n          m = '$' + m;\n          var O = o[m];\n          if (O) {\n            var Y;\n            E === 'init'\n              ? (Y = (this.strict && O.init) || O.get || O.set)\n              : (Y = O.init || O[E]),\n              Y && this.raiseRecoverable(f.start, 'Redefinition of property');\n          } else O = o[m] = {init: !1, get: !1, set: !1};\n          O[E] = !0;\n        }\n      }),\n        (de.parseExpression = function (n, o) {\n          var l = this.start,\n            f = this.startLoc,\n            m = this.parseMaybeAssign(n, o);\n          if (this.type === c.comma) {\n            var E = this.startNodeAt(l, f);\n            for (E.expressions = [m]; this.eat(c.comma); )\n              E.expressions.push(this.parseMaybeAssign(n, o));\n            return this.finishNode(E, 'SequenceExpression');\n          }\n          return m;\n        }),\n        (de.parseMaybeAssign = function (n, o, l) {\n          if (this.isContextual('yield')) {\n            if (this.inGenerator) return this.parseYield(n);\n            this.exprAllowed = !1;\n          }\n          var f = !1,\n            m = -1,\n            E = -1,\n            O = -1;\n          o\n            ? ((m = o.parenthesizedAssign),\n              (E = o.trailingComma),\n              (O = o.doubleProto),\n              (o.parenthesizedAssign = o.trailingComma = -1))\n            : ((o = new Xt()), (f = !0));\n          var Y = this.start,\n            Q = this.startLoc;\n          (this.type === c.parenL || this.type === c.name) &&\n            ((this.potentialArrowAt = this.start),\n            (this.potentialArrowInForAwait = n === 'await'));\n          var Te = this.parseMaybeConditional(n, o);\n          if ((l && (Te = l.call(this, Te, Y, Q)), this.type.isAssign)) {\n            var xe = this.startNodeAt(Y, Q);\n            return (\n              (xe.operator = this.value),\n              this.type === c.eq && (Te = this.toAssignable(Te, !1, o)),\n              f ||\n                (o.parenthesizedAssign = o.trailingComma = o.doubleProto = -1),\n              o.shorthandAssign >= Te.start && (o.shorthandAssign = -1),\n              this.type === c.eq\n                ? this.checkLValPattern(Te)\n                : this.checkLValSimple(Te),\n              (xe.left = Te),\n              this.next(),\n              (xe.right = this.parseMaybeAssign(n)),\n              O > -1 && (o.doubleProto = O),\n              this.finishNode(xe, 'AssignmentExpression')\n            );\n          } else f && this.checkExpressionErrors(o, !0);\n          return (\n            m > -1 && (o.parenthesizedAssign = m),\n            E > -1 && (o.trailingComma = E),\n            Te\n          );\n        }),\n        (de.parseMaybeConditional = function (n, o) {\n          var l = this.start,\n            f = this.startLoc,\n            m = this.parseExprOps(n, o);\n          if (this.checkExpressionErrors(o)) return m;\n          if (this.eat(c.question)) {\n            var E = this.startNodeAt(l, f);\n            return (\n              (E.test = m),\n              (E.consequent = this.parseMaybeAssign()),\n              this.expect(c.colon),\n              (E.alternate = this.parseMaybeAssign(n)),\n              this.finishNode(E, 'ConditionalExpression')\n            );\n          }\n          return m;\n        }),\n        (de.parseExprOps = function (n, o) {\n          var l = this.start,\n            f = this.startLoc,\n            m = this.parseMaybeUnary(o, !1, !1, n);\n          return this.checkExpressionErrors(o) ||\n            (m.start === l && m.type === 'ArrowFunctionExpression')\n            ? m\n            : this.parseExprOp(m, l, f, -1, n);\n        }),\n        (de.parseExprOp = function (n, o, l, f, m) {\n          var E = this.type.binop;\n          if (E != null && (!m || this.type !== c._in) && E > f) {\n            var O = this.type === c.logicalOR || this.type === c.logicalAND,\n              Y = this.type === c.coalesce;\n            Y && (E = c.logicalAND.binop);\n            var Q = this.value;\n            this.next();\n            var Te = this.start,\n              xe = this.startLoc,\n              Ze = this.parseExprOp(\n                this.parseMaybeUnary(null, !1, !1, m),\n                Te,\n                xe,\n                E,\n                m\n              ),\n              Lt = this.buildBinary(o, l, n, Ze, Q, O || Y);\n            return (\n              ((O && this.type === c.coalesce) ||\n                (Y &&\n                  (this.type === c.logicalOR || this.type === c.logicalAND))) &&\n                this.raiseRecoverable(\n                  this.start,\n                  'Logical expressions and coalesce expressions cannot be mixed. Wrap either by parentheses'\n                ),\n              this.parseExprOp(Lt, o, l, f, m)\n            );\n          }\n          return n;\n        }),\n        (de.buildBinary = function (n, o, l, f, m, E) {\n          f.type === 'PrivateIdentifier' &&\n            this.raise(\n              f.start,\n              'Private identifier can only be left side of binary expression'\n            );\n          var O = this.startNodeAt(n, o);\n          return (\n            (O.left = l),\n            (O.operator = m),\n            (O.right = f),\n            this.finishNode(O, E ? 'LogicalExpression' : 'BinaryExpression')\n          );\n        }),\n        (de.parseMaybeUnary = function (n, o, l, f) {\n          var m = this.start,\n            E = this.startLoc,\n            O;\n          if (this.isContextual('await') && this.canAwait)\n            (O = this.parseAwait(f)), (o = !0);\n          else if (this.type.prefix) {\n            var Y = this.startNode(),\n              Q = this.type === c.incDec;\n            (Y.operator = this.value),\n              (Y.prefix = !0),\n              this.next(),\n              (Y.argument = this.parseMaybeUnary(null, !0, Q, f)),\n              this.checkExpressionErrors(n, !0),\n              Q\n                ? this.checkLValSimple(Y.argument)\n                : this.strict && Y.operator === 'delete' && Ms(Y.argument)\n                ? this.raiseRecoverable(\n                    Y.start,\n                    'Deleting local variable in strict mode'\n                  )\n                : Y.operator === 'delete' && gs(Y.argument)\n                ? this.raiseRecoverable(\n                    Y.start,\n                    'Private fields can not be deleted'\n                  )\n                : (o = !0),\n              (O = this.finishNode(\n                Y,\n                Q ? 'UpdateExpression' : 'UnaryExpression'\n              ));\n          } else if (!o && this.type === c.privateId)\n            (f || this.privateNameStack.length === 0) &&\n              this.options.checkPrivateFields &&\n              this.unexpected(),\n              (O = this.parsePrivateIdent()),\n              this.type !== c._in && this.unexpected();\n          else {\n            if (\n              ((O = this.parseExprSubscripts(n, f)),\n              this.checkExpressionErrors(n))\n            )\n              return O;\n            for (; this.type.postfix && !this.canInsertSemicolon(); ) {\n              var Te = this.startNodeAt(m, E);\n              (Te.operator = this.value),\n                (Te.prefix = !1),\n                (Te.argument = O),\n                this.checkLValSimple(O),\n                this.next(),\n                (O = this.finishNode(Te, 'UpdateExpression'));\n            }\n          }\n          if (!l && this.eat(c.starstar))\n            if (o) this.unexpected(this.lastTokStart);\n            else\n              return this.buildBinary(\n                m,\n                E,\n                O,\n                this.parseMaybeUnary(null, !1, !1, f),\n                '**',\n                !1\n              );\n          else return O;\n        });\n      function Ms(n) {\n        return (\n          n.type === 'Identifier' ||\n          (n.type === 'ParenthesizedExpression' && Ms(n.expression))\n        );\n      }\n      function gs(n) {\n        return (\n          (n.type === 'MemberExpression' &&\n            n.property.type === 'PrivateIdentifier') ||\n          (n.type === 'ChainExpression' && gs(n.expression)) ||\n          (n.type === 'ParenthesizedExpression' && gs(n.expression))\n        );\n      }\n      (de.parseExprSubscripts = function (n, o) {\n        var l = this.start,\n          f = this.startLoc,\n          m = this.parseExprAtom(n, o);\n        if (\n          m.type === 'ArrowFunctionExpression' &&\n          this.input.slice(this.lastTokStart, this.lastTokEnd) !== ')'\n        )\n          return m;\n        var E = this.parseSubscripts(m, l, f, !1, o);\n        return (\n          n &&\n            E.type === 'MemberExpression' &&\n            (n.parenthesizedAssign >= E.start && (n.parenthesizedAssign = -1),\n            n.parenthesizedBind >= E.start && (n.parenthesizedBind = -1),\n            n.trailingComma >= E.start && (n.trailingComma = -1)),\n          E\n        );\n      }),\n        (de.parseSubscripts = function (n, o, l, f, m) {\n          for (\n            var E =\n                this.options.ecmaVersion >= 8 &&\n                n.type === 'Identifier' &&\n                n.name === 'async' &&\n                this.lastTokEnd === n.end &&\n                !this.canInsertSemicolon() &&\n                n.end - n.start === 5 &&\n                this.potentialArrowAt === n.start,\n              O = !1;\n            ;\n\n          ) {\n            var Y = this.parseSubscript(n, o, l, f, E, O, m);\n            if (\n              (Y.optional && (O = !0),\n              Y === n || Y.type === 'ArrowFunctionExpression')\n            ) {\n              if (O) {\n                var Q = this.startNodeAt(o, l);\n                (Q.expression = Y), (Y = this.finishNode(Q, 'ChainExpression'));\n              }\n              return Y;\n            }\n            n = Y;\n          }\n        }),\n        (de.shouldParseAsyncArrow = function () {\n          return !this.canInsertSemicolon() && this.eat(c.arrow);\n        }),\n        (de.parseSubscriptAsyncArrow = function (n, o, l, f) {\n          return this.parseArrowExpression(this.startNodeAt(n, o), l, !0, f);\n        }),\n        (de.parseSubscript = function (n, o, l, f, m, E, O) {\n          var Y = this.options.ecmaVersion >= 11,\n            Q = Y && this.eat(c.questionDot);\n          f &&\n            Q &&\n            this.raise(\n              this.lastTokStart,\n              'Optional chaining cannot appear in the callee of new expressions'\n            );\n          var Te = this.eat(c.bracketL);\n          if (\n            Te ||\n            (Q && this.type !== c.parenL && this.type !== c.backQuote) ||\n            this.eat(c.dot)\n          ) {\n            var xe = this.startNodeAt(o, l);\n            (xe.object = n),\n              Te\n                ? ((xe.property = this.parseExpression()),\n                  this.expect(c.bracketR))\n                : this.type === c.privateId && n.type !== 'Super'\n                ? (xe.property = this.parsePrivateIdent())\n                : (xe.property = this.parseIdent(\n                    this.options.allowReserved !== 'never'\n                  )),\n              (xe.computed = !!Te),\n              Y && (xe.optional = Q),\n              (n = this.finishNode(xe, 'MemberExpression'));\n          } else if (!f && this.eat(c.parenL)) {\n            var Ze = new Xt(),\n              Lt = this.yieldPos,\n              Ri = this.awaitPos,\n              Ys = this.awaitIdentPos;\n            (this.yieldPos = 0), (this.awaitPos = 0), (this.awaitIdentPos = 0);\n            var gr = this.parseExprList(\n              c.parenR,\n              this.options.ecmaVersion >= 8,\n              !1,\n              Ze\n            );\n            if (m && !Q && this.shouldParseAsyncArrow())\n              return (\n                this.checkPatternErrors(Ze, !1),\n                this.checkYieldAwaitInDefaultParams(),\n                this.awaitIdentPos > 0 &&\n                  this.raise(\n                    this.awaitIdentPos,\n                    \"Cannot use 'await' as identifier inside an async function\"\n                  ),\n                (this.yieldPos = Lt),\n                (this.awaitPos = Ri),\n                (this.awaitIdentPos = Ys),\n                this.parseSubscriptAsyncArrow(o, l, gr, O)\n              );\n            this.checkExpressionErrors(Ze, !0),\n              (this.yieldPos = Lt || this.yieldPos),\n              (this.awaitPos = Ri || this.awaitPos),\n              (this.awaitIdentPos = Ys || this.awaitIdentPos);\n            var Js = this.startNodeAt(o, l);\n            (Js.callee = n),\n              (Js.arguments = gr),\n              Y && (Js.optional = Q),\n              (n = this.finishNode(Js, 'CallExpression'));\n          } else if (this.type === c.backQuote) {\n            (Q || E) &&\n              this.raise(\n                this.start,\n                'Optional chaining cannot appear in the tag of tagged template expressions'\n              );\n            var Qs = this.startNodeAt(o, l);\n            (Qs.tag = n),\n              (Qs.quasi = this.parseTemplate({isTagged: !0})),\n              (n = this.finishNode(Qs, 'TaggedTemplateExpression'));\n          }\n          return n;\n        }),\n        (de.parseExprAtom = function (n, o, l) {\n          this.type === c.slash && this.readRegexp();\n          var f,\n            m = this.potentialArrowAt === this.start;\n          switch (this.type) {\n            case c._super:\n              return (\n                this.allowSuper ||\n                  this.raise(this.start, \"'super' keyword outside a method\"),\n                (f = this.startNode()),\n                this.next(),\n                this.type === c.parenL &&\n                  !this.allowDirectSuper &&\n                  this.raise(\n                    f.start,\n                    'super() call outside constructor of a subclass'\n                  ),\n                this.type !== c.dot &&\n                  this.type !== c.bracketL &&\n                  this.type !== c.parenL &&\n                  this.unexpected(),\n                this.finishNode(f, 'Super')\n              );\n            case c._this:\n              return (\n                (f = this.startNode()),\n                this.next(),\n                this.finishNode(f, 'ThisExpression')\n              );\n            case c.name:\n              var E = this.start,\n                O = this.startLoc,\n                Y = this.containsEsc,\n                Q = this.parseIdent(!1);\n              if (\n                this.options.ecmaVersion >= 8 &&\n                !Y &&\n                Q.name === 'async' &&\n                !this.canInsertSemicolon() &&\n                this.eat(c._function)\n              )\n                return (\n                  this.overrideContext(Ue.f_expr),\n                  this.parseFunction(this.startNodeAt(E, O), 0, !1, !0, o)\n                );\n              if (m && !this.canInsertSemicolon()) {\n                if (this.eat(c.arrow))\n                  return this.parseArrowExpression(\n                    this.startNodeAt(E, O),\n                    [Q],\n                    !1,\n                    o\n                  );\n                if (\n                  this.options.ecmaVersion >= 8 &&\n                  Q.name === 'async' &&\n                  this.type === c.name &&\n                  !Y &&\n                  (!this.potentialArrowInForAwait ||\n                    this.value !== 'of' ||\n                    this.containsEsc)\n                )\n                  return (\n                    (Q = this.parseIdent(!1)),\n                    (this.canInsertSemicolon() || !this.eat(c.arrow)) &&\n                      this.unexpected(),\n                    this.parseArrowExpression(\n                      this.startNodeAt(E, O),\n                      [Q],\n                      !0,\n                      o\n                    )\n                  );\n              }\n              return Q;\n            case c.regexp:\n              var Te = this.value;\n              return (\n                (f = this.parseLiteral(Te.value)),\n                (f.regex = {pattern: Te.pattern, flags: Te.flags}),\n                f\n              );\n            case c.num:\n            case c.string:\n              return this.parseLiteral(this.value);\n            case c._null:\n            case c._true:\n            case c._false:\n              return (\n                (f = this.startNode()),\n                (f.value =\n                  this.type === c._null ? null : this.type === c._true),\n                (f.raw = this.type.keyword),\n                this.next(),\n                this.finishNode(f, 'Literal')\n              );\n            case c.parenL:\n              var xe = this.start,\n                Ze = this.parseParenAndDistinguishExpression(m, o);\n              return (\n                n &&\n                  (n.parenthesizedAssign < 0 &&\n                    !this.isSimpleAssignTarget(Ze) &&\n                    (n.parenthesizedAssign = xe),\n                  n.parenthesizedBind < 0 && (n.parenthesizedBind = xe)),\n                Ze\n              );\n            case c.bracketL:\n              return (\n                (f = this.startNode()),\n                this.next(),\n                (f.elements = this.parseExprList(c.bracketR, !0, !0, n)),\n                this.finishNode(f, 'ArrayExpression')\n              );\n            case c.braceL:\n              return this.overrideContext(Ue.b_expr), this.parseObj(!1, n);\n            case c._function:\n              return (\n                (f = this.startNode()), this.next(), this.parseFunction(f, 0)\n              );\n            case c._class:\n              return this.parseClass(this.startNode(), !1);\n            case c._new:\n              return this.parseNew();\n            case c.backQuote:\n              return this.parseTemplate();\n            case c._import:\n              return this.options.ecmaVersion >= 11\n                ? this.parseExprImport(l)\n                : this.unexpected();\n            default:\n              return this.parseExprAtomDefault();\n          }\n        }),\n        (de.parseExprAtomDefault = function () {\n          this.unexpected();\n        }),\n        (de.parseExprImport = function (n) {\n          var o = this.startNode();\n          if (\n            (this.containsEsc &&\n              this.raiseRecoverable(\n                this.start,\n                'Escape sequence in keyword import'\n              ),\n            this.next(),\n            this.type === c.parenL && !n)\n          )\n            return this.parseDynamicImport(o);\n          if (this.type === c.dot) {\n            var l = this.startNodeAt(o.start, o.loc && o.loc.start);\n            return (\n              (l.name = 'import'),\n              (o.meta = this.finishNode(l, 'Identifier')),\n              this.parseImportMeta(o)\n            );\n          } else this.unexpected();\n        }),\n        (de.parseDynamicImport = function (n) {\n          if (\n            (this.next(),\n            (n.source = this.parseMaybeAssign()),\n            this.options.ecmaVersion >= 16)\n          )\n            this.eat(c.parenR)\n              ? (n.options = null)\n              : (this.expect(c.comma),\n                this.afterTrailingComma(c.parenR)\n                  ? (n.options = null)\n                  : ((n.options = this.parseMaybeAssign()),\n                    this.eat(c.parenR) ||\n                      (this.expect(c.comma),\n                      this.afterTrailingComma(c.parenR) || this.unexpected())));\n          else if (!this.eat(c.parenR)) {\n            var o = this.start;\n            this.eat(c.comma) && this.eat(c.parenR)\n              ? this.raiseRecoverable(\n                  o,\n                  'Trailing comma is not allowed in import()'\n                )\n              : this.unexpected(o);\n          }\n          return this.finishNode(n, 'ImportExpression');\n        }),\n        (de.parseImportMeta = function (n) {\n          this.next();\n          var o = this.containsEsc;\n          return (\n            (n.property = this.parseIdent(!0)),\n            n.property.name !== 'meta' &&\n              this.raiseRecoverable(\n                n.property.start,\n                \"The only valid meta property for import is 'import.meta'\"\n              ),\n            o &&\n              this.raiseRecoverable(\n                n.start,\n                \"'import.meta' must not contain escaped characters\"\n              ),\n            this.options.sourceType !== 'module' &&\n              !this.options.allowImportExportEverywhere &&\n              this.raiseRecoverable(\n                n.start,\n                \"Cannot use 'import.meta' outside a module\"\n              ),\n            this.finishNode(n, 'MetaProperty')\n          );\n        }),\n        (de.parseLiteral = function (n) {\n          var o = this.startNode();\n          return (\n            (o.value = n),\n            (o.raw = this.input.slice(this.start, this.end)),\n            o.raw.charCodeAt(o.raw.length - 1) === 110 &&\n              (o.bigint =\n                o.value != null\n                  ? o.value.toString()\n                  : o.raw.slice(0, -1).replace(/_/g, '')),\n            this.next(),\n            this.finishNode(o, 'Literal')\n          );\n        }),\n        (de.parseParenExpression = function () {\n          this.expect(c.parenL);\n          var n = this.parseExpression();\n          return this.expect(c.parenR), n;\n        }),\n        (de.shouldParseArrow = function (n) {\n          return !this.canInsertSemicolon();\n        }),\n        (de.parseParenAndDistinguishExpression = function (n, o) {\n          var l = this.start,\n            f = this.startLoc,\n            m,\n            E = this.options.ecmaVersion >= 8;\n          if (this.options.ecmaVersion >= 6) {\n            this.next();\n            var O = this.start,\n              Y = this.startLoc,\n              Q = [],\n              Te = !0,\n              xe = !1,\n              Ze = new Xt(),\n              Lt = this.yieldPos,\n              Ri = this.awaitPos,\n              Ys;\n            for (this.yieldPos = 0, this.awaitPos = 0; this.type !== c.parenR; )\n              if (\n                (Te ? (Te = !1) : this.expect(c.comma),\n                E && this.afterTrailingComma(c.parenR, !0))\n              ) {\n                xe = !0;\n                break;\n              } else if (this.type === c.ellipsis) {\n                (Ys = this.start),\n                  Q.push(this.parseParenItem(this.parseRestBinding())),\n                  this.type === c.comma &&\n                    this.raiseRecoverable(\n                      this.start,\n                      'Comma is not permitted after the rest element'\n                    );\n                break;\n              } else Q.push(this.parseMaybeAssign(!1, Ze, this.parseParenItem));\n            var gr = this.lastTokEnd,\n              Js = this.lastTokEndLoc;\n            if (\n              (this.expect(c.parenR),\n              n && this.shouldParseArrow(Q) && this.eat(c.arrow))\n            )\n              return (\n                this.checkPatternErrors(Ze, !1),\n                this.checkYieldAwaitInDefaultParams(),\n                (this.yieldPos = Lt),\n                (this.awaitPos = Ri),\n                this.parseParenArrowList(l, f, Q, o)\n              );\n            (!Q.length || xe) && this.unexpected(this.lastTokStart),\n              Ys && this.unexpected(Ys),\n              this.checkExpressionErrors(Ze, !0),\n              (this.yieldPos = Lt || this.yieldPos),\n              (this.awaitPos = Ri || this.awaitPos),\n              Q.length > 1\n                ? ((m = this.startNodeAt(O, Y)),\n                  (m.expressions = Q),\n                  this.finishNodeAt(m, 'SequenceExpression', gr, Js))\n                : (m = Q[0]);\n          } else m = this.parseParenExpression();\n          if (this.options.preserveParens) {\n            var Qs = this.startNodeAt(l, f);\n            return (\n              (Qs.expression = m),\n              this.finishNode(Qs, 'ParenthesizedExpression')\n            );\n          } else return m;\n        }),\n        (de.parseParenItem = function (n) {\n          return n;\n        }),\n        (de.parseParenArrowList = function (n, o, l, f) {\n          return this.parseArrowExpression(this.startNodeAt(n, o), l, !1, f);\n        });\n      var Ci = [];\n      (de.parseNew = function () {\n        this.containsEsc &&\n          this.raiseRecoverable(this.start, 'Escape sequence in keyword new');\n        var n = this.startNode();\n        if (\n          (this.next(), this.options.ecmaVersion >= 6 && this.type === c.dot)\n        ) {\n          var o = this.startNodeAt(n.start, n.loc && n.loc.start);\n          (o.name = 'new'),\n            (n.meta = this.finishNode(o, 'Identifier')),\n            this.next();\n          var l = this.containsEsc;\n          return (\n            (n.property = this.parseIdent(!0)),\n            n.property.name !== 'target' &&\n              this.raiseRecoverable(\n                n.property.start,\n                \"The only valid meta property for new is 'new.target'\"\n              ),\n            l &&\n              this.raiseRecoverable(\n                n.start,\n                \"'new.target' must not contain escaped characters\"\n              ),\n            this.allowNewDotTarget ||\n              this.raiseRecoverable(\n                n.start,\n                \"'new.target' can only be used in functions and class static block\"\n              ),\n            this.finishNode(n, 'MetaProperty')\n          );\n        }\n        var f = this.start,\n          m = this.startLoc;\n        return (\n          (n.callee = this.parseSubscripts(\n            this.parseExprAtom(null, !1, !0),\n            f,\n            m,\n            !0,\n            !1\n          )),\n          this.eat(c.parenL)\n            ? (n.arguments = this.parseExprList(\n                c.parenR,\n                this.options.ecmaVersion >= 8,\n                !1\n              ))\n            : (n.arguments = Ci),\n          this.finishNode(n, 'NewExpression')\n        );\n      }),\n        (de.parseTemplateElement = function (n) {\n          var o = n.isTagged,\n            l = this.startNode();\n          return (\n            this.type === c.invalidTemplate\n              ? (o ||\n                  this.raiseRecoverable(\n                    this.start,\n                    'Bad escape sequence in untagged template literal'\n                  ),\n                (l.value = {\n                  raw: this.value.replace(\n                    /\\r\\n?/g,\n                    `\n`\n                  ),\n                  cooked: null,\n                }))\n              : (l.value = {\n                  raw: this.input.slice(this.start, this.end).replace(\n                    /\\r\\n?/g,\n                    `\n`\n                  ),\n                  cooked: this.value,\n                }),\n            this.next(),\n            (l.tail = this.type === c.backQuote),\n            this.finishNode(l, 'TemplateElement')\n          );\n        }),\n        (de.parseTemplate = function (n) {\n          n === void 0 && (n = {});\n          var o = n.isTagged;\n          o === void 0 && (o = !1);\n          var l = this.startNode();\n          this.next(), (l.expressions = []);\n          var f = this.parseTemplateElement({isTagged: o});\n          for (l.quasis = [f]; !f.tail; )\n            this.type === c.eof &&\n              this.raise(this.pos, 'Unterminated template literal'),\n              this.expect(c.dollarBraceL),\n              l.expressions.push(this.parseExpression()),\n              this.expect(c.braceR),\n              l.quasis.push((f = this.parseTemplateElement({isTagged: o})));\n          return this.next(), this.finishNode(l, 'TemplateLiteral');\n        }),\n        (de.isAsyncProp = function (n) {\n          return (\n            !n.computed &&\n            n.key.type === 'Identifier' &&\n            n.key.name === 'async' &&\n            (this.type === c.name ||\n              this.type === c.num ||\n              this.type === c.string ||\n              this.type === c.bracketL ||\n              this.type.keyword ||\n              (this.options.ecmaVersion >= 9 && this.type === c.star)) &&\n            !R.test(this.input.slice(this.lastTokEnd, this.start))\n          );\n        }),\n        (de.parseObj = function (n, o) {\n          var l = this.startNode(),\n            f = !0,\n            m = {};\n          for (l.properties = [], this.next(); !this.eat(c.braceR); ) {\n            if (f) f = !1;\n            else if (\n              (this.expect(c.comma),\n              this.options.ecmaVersion >= 5 &&\n                this.afterTrailingComma(c.braceR))\n            )\n              break;\n            var E = this.parseProperty(n, o);\n            n || this.checkPropClash(E, m, o), l.properties.push(E);\n          }\n          return this.finishNode(l, n ? 'ObjectPattern' : 'ObjectExpression');\n        }),\n        (de.parseProperty = function (n, o) {\n          var l = this.startNode(),\n            f,\n            m,\n            E,\n            O;\n          if (this.options.ecmaVersion >= 9 && this.eat(c.ellipsis))\n            return n\n              ? ((l.argument = this.parseIdent(!1)),\n                this.type === c.comma &&\n                  this.raiseRecoverable(\n                    this.start,\n                    'Comma is not permitted after the rest element'\n                  ),\n                this.finishNode(l, 'RestElement'))\n              : ((l.argument = this.parseMaybeAssign(!1, o)),\n                this.type === c.comma &&\n                  o &&\n                  o.trailingComma < 0 &&\n                  (o.trailingComma = this.start),\n                this.finishNode(l, 'SpreadElement'));\n          this.options.ecmaVersion >= 6 &&\n            ((l.method = !1),\n            (l.shorthand = !1),\n            (n || o) && ((E = this.start), (O = this.startLoc)),\n            n || (f = this.eat(c.star)));\n          var Y = this.containsEsc;\n          return (\n            this.parsePropertyName(l),\n            !n &&\n            !Y &&\n            this.options.ecmaVersion >= 8 &&\n            !f &&\n            this.isAsyncProp(l)\n              ? ((m = !0),\n                (f = this.options.ecmaVersion >= 9 && this.eat(c.star)),\n                this.parsePropertyName(l))\n              : (m = !1),\n            this.parsePropertyValue(l, n, f, m, E, O, o, Y),\n            this.finishNode(l, 'Property')\n          );\n        }),\n        (de.parseGetterSetter = function (n) {\n          var o = n.key.name;\n          this.parsePropertyName(n),\n            (n.value = this.parseMethod(!1)),\n            (n.kind = o);\n          var l = n.kind === 'get' ? 0 : 1;\n          if (n.value.params.length !== l) {\n            var f = n.value.start;\n            n.kind === 'get'\n              ? this.raiseRecoverable(f, 'getter should have no params')\n              : this.raiseRecoverable(\n                  f,\n                  'setter should have exactly one param'\n                );\n          } else\n            n.kind === 'set' &&\n              n.value.params[0].type === 'RestElement' &&\n              this.raiseRecoverable(\n                n.value.params[0].start,\n                'Setter cannot use rest params'\n              );\n        }),\n        (de.parsePropertyValue = function (n, o, l, f, m, E, O, Y) {\n          (l || f) && this.type === c.colon && this.unexpected(),\n            this.eat(c.colon)\n              ? ((n.value = o\n                  ? this.parseMaybeDefault(this.start, this.startLoc)\n                  : this.parseMaybeAssign(!1, O)),\n                (n.kind = 'init'))\n              : this.options.ecmaVersion >= 6 && this.type === c.parenL\n              ? (o && this.unexpected(),\n                (n.method = !0),\n                (n.value = this.parseMethod(l, f)),\n                (n.kind = 'init'))\n              : !o &&\n                !Y &&\n                this.options.ecmaVersion >= 5 &&\n                !n.computed &&\n                n.key.type === 'Identifier' &&\n                (n.key.name === 'get' || n.key.name === 'set') &&\n                this.type !== c.comma &&\n                this.type !== c.braceR &&\n                this.type !== c.eq\n              ? ((l || f) && this.unexpected(), this.parseGetterSetter(n))\n              : this.options.ecmaVersion >= 6 &&\n                !n.computed &&\n                n.key.type === 'Identifier'\n              ? ((l || f) && this.unexpected(),\n                this.checkUnreserved(n.key),\n                n.key.name === 'await' &&\n                  !this.awaitIdentPos &&\n                  (this.awaitIdentPos = m),\n                o\n                  ? (n.value = this.parseMaybeDefault(\n                      m,\n                      E,\n                      this.copyNode(n.key)\n                    ))\n                  : this.type === c.eq && O\n                  ? (O.shorthandAssign < 0 && (O.shorthandAssign = this.start),\n                    (n.value = this.parseMaybeDefault(\n                      m,\n                      E,\n                      this.copyNode(n.key)\n                    )))\n                  : (n.value = this.copyNode(n.key)),\n                (n.kind = 'init'),\n                (n.shorthand = !0))\n              : this.unexpected();\n        }),\n        (de.parsePropertyName = function (n) {\n          if (this.options.ecmaVersion >= 6) {\n            if (this.eat(c.bracketL))\n              return (\n                (n.computed = !0),\n                (n.key = this.parseMaybeAssign()),\n                this.expect(c.bracketR),\n                n.key\n              );\n            n.computed = !1;\n          }\n          return (n.key =\n            this.type === c.num || this.type === c.string\n              ? this.parseExprAtom()\n              : this.parseIdent(this.options.allowReserved !== 'never'));\n        }),\n        (de.initFunction = function (n) {\n          (n.id = null),\n            this.options.ecmaVersion >= 6 && (n.generator = n.expression = !1),\n            this.options.ecmaVersion >= 8 && (n.async = !1);\n        }),\n        (de.parseMethod = function (n, o, l) {\n          var f = this.startNode(),\n            m = this.yieldPos,\n            E = this.awaitPos,\n            O = this.awaitIdentPos;\n          return (\n            this.initFunction(f),\n            this.options.ecmaVersion >= 6 && (f.generator = n),\n            this.options.ecmaVersion >= 8 && (f.async = !!o),\n            (this.yieldPos = 0),\n            (this.awaitPos = 0),\n            (this.awaitIdentPos = 0),\n            this.enterScope(ut(o, f.generator) | Ee | (l ? Le : 0)),\n            this.expect(c.parenL),\n            (f.params = this.parseBindingList(\n              c.parenR,\n              !1,\n              this.options.ecmaVersion >= 8\n            )),\n            this.checkYieldAwaitInDefaultParams(),\n            this.parseFunctionBody(f, !1, !0, !1),\n            (this.yieldPos = m),\n            (this.awaitPos = E),\n            (this.awaitIdentPos = O),\n            this.finishNode(f, 'FunctionExpression')\n          );\n        }),\n        (de.parseArrowExpression = function (n, o, l, f) {\n          var m = this.yieldPos,\n            E = this.awaitPos,\n            O = this.awaitIdentPos;\n          return (\n            this.enterScope(ut(l, !1) | he),\n            this.initFunction(n),\n            this.options.ecmaVersion >= 8 && (n.async = !!l),\n            (this.yieldPos = 0),\n            (this.awaitPos = 0),\n            (this.awaitIdentPos = 0),\n            (n.params = this.toAssignableList(o, !0)),\n            this.parseFunctionBody(n, !0, !1, f),\n            (this.yieldPos = m),\n            (this.awaitPos = E),\n            (this.awaitIdentPos = O),\n            this.finishNode(n, 'ArrowFunctionExpression')\n          );\n        }),\n        (de.parseFunctionBody = function (n, o, l, f) {\n          var m = o && this.type !== c.braceL,\n            E = this.strict,\n            O = !1;\n          if (m)\n            (n.body = this.parseMaybeAssign(f)),\n              (n.expression = !0),\n              this.checkParams(n, !1);\n          else {\n            var Y =\n              this.options.ecmaVersion >= 7 &&\n              !this.isSimpleParamList(n.params);\n            (!E || Y) &&\n              ((O = this.strictDirective(this.end)),\n              O &&\n                Y &&\n                this.raiseRecoverable(\n                  n.start,\n                  \"Illegal 'use strict' directive in function with non-simple parameter list\"\n                ));\n            var Q = this.labels;\n            (this.labels = []),\n              O && (this.strict = !0),\n              this.checkParams(\n                n,\n                !E && !O && !o && !l && this.isSimpleParamList(n.params)\n              ),\n              this.strict && n.id && this.checkLValSimple(n.id, Dn),\n              (n.body = this.parseBlock(!1, void 0, O && !E)),\n              (n.expression = !1),\n              this.adaptDirectivePrologue(n.body.body),\n              (this.labels = Q);\n          }\n          this.exitScope();\n        }),\n        (de.isSimpleParamList = function (n) {\n          for (var o = 0, l = n; o < l.length; o += 1) {\n            var f = l[o];\n            if (f.type !== 'Identifier') return !1;\n          }\n          return !0;\n        }),\n        (de.checkParams = function (n, o) {\n          for (\n            var l = Object.create(null), f = 0, m = n.params;\n            f < m.length;\n            f += 1\n          ) {\n            var E = m[f];\n            this.checkLValInnerPattern(E, bt, o ? null : l);\n          }\n        }),\n        (de.parseExprList = function (n, o, l, f) {\n          for (var m = [], E = !0; !this.eat(n); ) {\n            if (E) E = !1;\n            else if ((this.expect(c.comma), o && this.afterTrailingComma(n)))\n              break;\n            var O = void 0;\n            l && this.type === c.comma\n              ? (O = null)\n              : this.type === c.ellipsis\n              ? ((O = this.parseSpread(f)),\n                f &&\n                  this.type === c.comma &&\n                  f.trailingComma < 0 &&\n                  (f.trailingComma = this.start))\n              : (O = this.parseMaybeAssign(!1, f)),\n              m.push(O);\n          }\n          return m;\n        }),\n        (de.checkUnreserved = function (n) {\n          var o = n.start,\n            l = n.end,\n            f = n.name;\n          if (\n            (this.inGenerator &&\n              f === 'yield' &&\n              this.raiseRecoverable(\n                o,\n                \"Cannot use 'yield' as identifier inside a generator\"\n              ),\n            this.inAsync &&\n              f === 'await' &&\n              this.raiseRecoverable(\n                o,\n                \"Cannot use 'await' as identifier inside an async function\"\n              ),\n            !(this.currentThisScope().flags & Ke) &&\n              f === 'arguments' &&\n              this.raiseRecoverable(\n                o,\n                \"Cannot use 'arguments' in class field initializer\"\n              ),\n            this.inClassStaticBlock &&\n              (f === 'arguments' || f === 'await') &&\n              this.raise(\n                o,\n                'Cannot use ' + f + ' in class static initialization block'\n              ),\n            this.keywords.test(f) &&\n              this.raise(o, \"Unexpected keyword '\" + f + \"'\"),\n            !(\n              this.options.ecmaVersion < 6 &&\n              this.input.slice(o, l).indexOf('\\\\') !== -1\n            ))\n          ) {\n            var m = this.strict ? this.reservedWordsStrict : this.reservedWords;\n            m.test(f) &&\n              (!this.inAsync &&\n                f === 'await' &&\n                this.raiseRecoverable(\n                  o,\n                  \"Cannot use keyword 'await' outside an async function\"\n                ),\n              this.raiseRecoverable(o, \"The keyword '\" + f + \"' is reserved\"));\n          }\n        }),\n        (de.parseIdent = function (n) {\n          var o = this.parseIdentNode();\n          return (\n            this.next(!!n),\n            this.finishNode(o, 'Identifier'),\n            n ||\n              (this.checkUnreserved(o),\n              o.name === 'await' &&\n                !this.awaitIdentPos &&\n                (this.awaitIdentPos = o.start)),\n            o\n          );\n        }),\n        (de.parseIdentNode = function () {\n          var n = this.startNode();\n          return (\n            this.type === c.name\n              ? (n.name = this.value)\n              : this.type.keyword\n              ? ((n.name = this.type.keyword),\n                (n.name === 'class' || n.name === 'function') &&\n                  (this.lastTokEnd !== this.lastTokStart + 1 ||\n                    this.input.charCodeAt(this.lastTokStart) !== 46) &&\n                  this.context.pop(),\n                (this.type = c.name))\n              : this.unexpected(),\n            n\n          );\n        }),\n        (de.parsePrivateIdent = function () {\n          var n = this.startNode();\n          return (\n            this.type === c.privateId\n              ? (n.name = this.value)\n              : this.unexpected(),\n            this.next(),\n            this.finishNode(n, 'PrivateIdentifier'),\n            this.options.checkPrivateFields &&\n              (this.privateNameStack.length === 0\n                ? this.raise(\n                    n.start,\n                    \"Private field '#\" +\n                      n.name +\n                      \"' must be declared in an enclosing class\"\n                  )\n                : this.privateNameStack[\n                    this.privateNameStack.length - 1\n                  ].used.push(n)),\n            n\n          );\n        }),\n        (de.parseYield = function (n) {\n          this.yieldPos || (this.yieldPos = this.start);\n          var o = this.startNode();\n          return (\n            this.next(),\n            this.type === c.semi ||\n            this.canInsertSemicolon() ||\n            (this.type !== c.star && !this.type.startsExpr)\n              ? ((o.delegate = !1), (o.argument = null))\n              : ((o.delegate = this.eat(c.star)),\n                (o.argument = this.parseMaybeAssign(n))),\n            this.finishNode(o, 'YieldExpression')\n          );\n        }),\n        (de.parseAwait = function (n) {\n          this.awaitPos || (this.awaitPos = this.start);\n          var o = this.startNode();\n          return (\n            this.next(),\n            (o.argument = this.parseMaybeUnary(null, !0, !1, n)),\n            this.finishNode(o, 'AwaitExpression')\n          );\n        });\n      var ts = Ge.prototype;\n      (ts.raise = function (n, o) {\n        var l = $t(this.input, n);\n        (o += ' (' + l.line + ':' + l.column + ')'),\n          this.sourceFile && (o += ' in ' + this.sourceFile);\n        var f = new SyntaxError(o);\n        throw ((f.pos = n), (f.loc = l), (f.raisedAt = this.pos), f);\n      }),\n        (ts.raiseRecoverable = ts.raise),\n        (ts.curPosition = function () {\n          if (this.options.locations)\n            return new ct(this.curLine, this.pos - this.lineStart);\n        });\n      var rn = Ge.prototype,\n        wi = function (o) {\n          (this.flags = o),\n            (this.var = []),\n            (this.lexical = []),\n            (this.functions = []);\n        };\n      (rn.enterScope = function (n) {\n        this.scopeStack.push(new wi(n));\n      }),\n        (rn.exitScope = function () {\n          this.scopeStack.pop();\n        }),\n        (rn.treatFunctionsAsVarInScope = function (n) {\n          return n.flags & J || (!this.inModule && n.flags & G);\n        }),\n        (rn.declareName = function (n, o, l) {\n          var f = !1;\n          if (o === yt) {\n            var m = this.currentScope();\n            (f =\n              m.lexical.indexOf(n) > -1 ||\n              m.functions.indexOf(n) > -1 ||\n              m.var.indexOf(n) > -1),\n              m.lexical.push(n),\n              this.inModule && m.flags & G && delete this.undefinedExports[n];\n          } else if (o === bn) {\n            var E = this.currentScope();\n            E.lexical.push(n);\n          } else if (o === vt) {\n            var O = this.currentScope();\n            this.treatFunctionsAsVar\n              ? (f = O.lexical.indexOf(n) > -1)\n              : (f = O.lexical.indexOf(n) > -1 || O.var.indexOf(n) > -1),\n              O.functions.push(n);\n          } else\n            for (var Y = this.scopeStack.length - 1; Y >= 0; --Y) {\n              var Q = this.scopeStack[Y];\n              if (\n                (Q.lexical.indexOf(n) > -1 &&\n                  !(Q.flags & Ie && Q.lexical[0] === n)) ||\n                (!this.treatFunctionsAsVarInScope(Q) &&\n                  Q.functions.indexOf(n) > -1)\n              ) {\n                f = !0;\n                break;\n              }\n              if (\n                (Q.var.push(n),\n                this.inModule && Q.flags & G && delete this.undefinedExports[n],\n                Q.flags & Ke)\n              )\n                break;\n            }\n          f &&\n            this.raiseRecoverable(\n              l,\n              \"Identifier '\" + n + \"' has already been declared\"\n            );\n        }),\n        (rn.checkLocalExport = function (n) {\n          this.scopeStack[0].lexical.indexOf(n.name) === -1 &&\n            this.scopeStack[0].var.indexOf(n.name) === -1 &&\n            (this.undefinedExports[n.name] = n);\n        }),\n        (rn.currentScope = function () {\n          return this.scopeStack[this.scopeStack.length - 1];\n        }),\n        (rn.currentVarScope = function () {\n          for (var n = this.scopeStack.length - 1; ; n--) {\n            var o = this.scopeStack[n];\n            if (o.flags & (Ke | We | Xe)) return o;\n          }\n        }),\n        (rn.currentThisScope = function () {\n          for (var n = this.scopeStack.length - 1; ; n--) {\n            var o = this.scopeStack[n];\n            if (o.flags & (Ke | We | Xe) && !(o.flags & he)) return o;\n          }\n        });\n      var Fn = function (o, l, f) {\n          (this.type = ''),\n            (this.start = l),\n            (this.end = 0),\n            o.options.locations && (this.loc = new wt(o, f)),\n            o.options.directSourceFile &&\n              (this.sourceFile = o.options.directSourceFile),\n            o.options.ranges && (this.range = [l, 0]);\n        },\n        Bn = Ge.prototype;\n      (Bn.startNode = function () {\n        return new Fn(this, this.start, this.startLoc);\n      }),\n        (Bn.startNodeAt = function (n, o) {\n          return new Fn(this, n, o);\n        });\n      function Fs(n, o, l, f) {\n        return (\n          (n.type = o),\n          (n.end = l),\n          this.options.locations && (n.loc.end = f),\n          this.options.ranges && (n.range[1] = l),\n          n\n        );\n      }\n      (Bn.finishNode = function (n, o) {\n        return Fs.call(this, n, o, this.lastTokEnd, this.lastTokEndLoc);\n      }),\n        (Bn.finishNodeAt = function (n, o, l, f) {\n          return Fs.call(this, n, o, l, f);\n        }),\n        (Bn.copyNode = function (n) {\n          var o = new Fn(this, n.start, this.startLoc);\n          for (var l in n) o[l] = n[l];\n          return o;\n        });\n      var Si =\n          'Gara Garay Gukh Gurung_Khema Hrkt Katakana_Or_Hiragana Kawi Kirat_Rai Krai Nag_Mundari Nagm Ol_Onal Onao Sunu Sunuwar Todhri Todr Tulu_Tigalari Tutg Unknown Zzzz',\n        Bs =\n          'ASCII ASCII_Hex_Digit AHex Alphabetic Alpha Any Assigned Bidi_Control Bidi_C Bidi_Mirrored Bidi_M Case_Ignorable CI Cased Changes_When_Casefolded CWCF Changes_When_Casemapped CWCM Changes_When_Lowercased CWL Changes_When_NFKC_Casefolded CWKCF Changes_When_Titlecased CWT Changes_When_Uppercased CWU Dash Default_Ignorable_Code_Point DI Deprecated Dep Diacritic Dia Emoji Emoji_Component Emoji_Modifier Emoji_Modifier_Base Emoji_Presentation Extender Ext Grapheme_Base Gr_Base Grapheme_Extend Gr_Ext Hex_Digit Hex IDS_Binary_Operator IDSB IDS_Trinary_Operator IDST ID_Continue IDC ID_Start IDS Ideographic Ideo Join_Control Join_C Logical_Order_Exception LOE Lowercase Lower Math Noncharacter_Code_Point NChar Pattern_Syntax Pat_Syn Pattern_White_Space Pat_WS Quotation_Mark QMark Radical Regional_Indicator RI Sentence_Terminal STerm Soft_Dotted SD Terminal_Punctuation Term Unified_Ideograph UIdeo Uppercase Upper Variation_Selector VS White_Space space XID_Continue XIDC XID_Start XIDS',\n        Vs = Bs + ' Extended_Pictographic',\n        js = Vs,\n        $s = js + ' EBase EComp EMod EPres ExtPict',\n        qs = $s,\n        Ii = qs,\n        Ei = {9: Bs, 10: Vs, 11: js, 12: $s, 13: qs, 14: Ii},\n        Ai =\n          'Basic_Emoji Emoji_Keycap_Sequence RGI_Emoji_Modifier_Sequence RGI_Emoji_Flag_Sequence RGI_Emoji_Tag_Sequence RGI_Emoji_ZWJ_Sequence RGI_Emoji',\n        Pi = {9: '', 10: '', 11: '', 12: '', 13: '', 14: Ai},\n        Ks =\n          'Cased_Letter LC Close_Punctuation Pe Connector_Punctuation Pc Control Cc cntrl Currency_Symbol Sc Dash_Punctuation Pd Decimal_Number Nd digit Enclosing_Mark Me Final_Punctuation Pf Format Cf Initial_Punctuation Pi Letter L Letter_Number Nl Line_Separator Zl Lowercase_Letter Ll Mark M Combining_Mark Math_Symbol Sm Modifier_Letter Lm Modifier_Symbol Sk Nonspacing_Mark Mn Number N Open_Punctuation Ps Other C Other_Letter Lo Other_Number No Other_Punctuation Po Other_Symbol So Paragraph_Separator Zp Private_Use Co Punctuation P punct Separator Z Space_Separator Zs Spacing_Mark Mc Surrogate Cs Symbol S Titlecase_Letter Lt Unassigned Cn Uppercase_Letter Lu',\n        Us =\n          'Adlam Adlm Ahom Anatolian_Hieroglyphs Hluw Arabic Arab Armenian Armn Avestan Avst Balinese Bali Bamum Bamu Bassa_Vah Bass Batak Batk Bengali Beng Bhaiksuki Bhks Bopomofo Bopo Brahmi Brah Braille Brai Buginese Bugi Buhid Buhd Canadian_Aboriginal Cans Carian Cari Caucasian_Albanian Aghb Chakma Cakm Cham Cham Cherokee Cher Common Zyyy Coptic Copt Qaac Cuneiform Xsux Cypriot Cprt Cyrillic Cyrl Deseret Dsrt Devanagari Deva Duployan Dupl Egyptian_Hieroglyphs Egyp Elbasan Elba Ethiopic Ethi Georgian Geor Glagolitic Glag Gothic Goth Grantha Gran Greek Grek Gujarati Gujr Gurmukhi Guru Han Hani Hangul Hang Hanunoo Hano Hatran Hatr Hebrew Hebr Hiragana Hira Imperial_Aramaic Armi Inherited Zinh Qaai Inscriptional_Pahlavi Phli Inscriptional_Parthian Prti Javanese Java Kaithi Kthi Kannada Knda Katakana Kana Kayah_Li Kali Kharoshthi Khar Khmer Khmr Khojki Khoj Khudawadi Sind Lao Laoo Latin Latn Lepcha Lepc Limbu Limb Linear_A Lina Linear_B Linb Lisu Lisu Lycian Lyci Lydian Lydi Mahajani Mahj Malayalam Mlym Mandaic Mand Manichaean Mani Marchen Marc Masaram_Gondi Gonm Meetei_Mayek Mtei Mende_Kikakui Mend Meroitic_Cursive Merc Meroitic_Hieroglyphs Mero Miao Plrd Modi Mongolian Mong Mro Mroo Multani Mult Myanmar Mymr Nabataean Nbat New_Tai_Lue Talu Newa Newa Nko Nkoo Nushu Nshu Ogham Ogam Ol_Chiki Olck Old_Hungarian Hung Old_Italic Ital Old_North_Arabian Narb Old_Permic Perm Old_Persian Xpeo Old_South_Arabian Sarb Old_Turkic Orkh Oriya Orya Osage Osge Osmanya Osma Pahawh_Hmong Hmng Palmyrene Palm Pau_Cin_Hau Pauc Phags_Pa Phag Phoenician Phnx Psalter_Pahlavi Phlp Rejang Rjng Runic Runr Samaritan Samr Saurashtra Saur Sharada Shrd Shavian Shaw Siddham Sidd SignWriting Sgnw Sinhala Sinh Sora_Sompeng Sora Soyombo Soyo Sundanese Sund Syloti_Nagri Sylo Syriac Syrc Tagalog Tglg Tagbanwa Tagb Tai_Le Tale Tai_Tham Lana Tai_Viet Tavt Takri Takr Tamil Taml Tangut Tang Telugu Telu Thaana Thaa Thai Thai Tibetan Tibt Tifinagh Tfng Tirhuta Tirh Ugaritic Ugar Vai Vaii Warang_Citi Wara Yi Yiii Zanabazar_Square Zanb',\n        Hs =\n          Us +\n          ' Dogra Dogr Gunjala_Gondi Gong Hanifi_Rohingya Rohg Makasar Maka Medefaidrin Medf Old_Sogdian Sogo Sogdian Sogd',\n        Ws =\n          Hs +\n          ' Elymaic Elym Nandinagari Nand Nyiakeng_Puachue_Hmong Hmnp Wancho Wcho',\n        Gs =\n          Ws +\n          ' Chorasmian Chrs Diak Dives_Akuru Khitan_Small_Script Kits Yezi Yezidi',\n        zs =\n          Gs +\n          ' Cypro_Minoan Cpmn Old_Uyghur Ougr Tangsa Tnsa Toto Vithkuqi Vith',\n        jo = zs + ' ' + Si,\n        $o = {9: Us, 10: Hs, 11: Ws, 12: Gs, 13: zs, 14: jo},\n        mr = {};\n      function qo(n) {\n        var o = (mr[n] = {\n          binary: tt(Ei[n] + ' ' + Ks),\n          binaryOfStrings: tt(Pi[n]),\n          nonBinary: {General_Category: tt(Ks), Script: tt($o[n])},\n        });\n        (o.nonBinary.Script_Extensions = o.nonBinary.Script),\n          (o.nonBinary.gc = o.nonBinary.General_Category),\n          (o.nonBinary.sc = o.nonBinary.Script),\n          (o.nonBinary.scx = o.nonBinary.Script_Extensions);\n      }\n      for (var Ni = 0, yr = [9, 10, 11, 12, 13, 14]; Ni < yr.length; Ni += 1) {\n        var Ko = yr[Ni];\n        qo(Ko);\n      }\n      var le = Ge.prototype,\n        Xs = function (o, l) {\n          (this.parent = o), (this.base = l || this);\n        };\n      (Xs.prototype.separatedFrom = function (o) {\n        for (var l = this; l; l = l.parent)\n          for (var f = o; f; f = f.parent)\n            if (l.base === f.base && l !== f) return !0;\n        return !1;\n      }),\n        (Xs.prototype.sibling = function () {\n          return new Xs(this.parent, this.base);\n        });\n      var on = function (o) {\n        (this.parser = o),\n          (this.validFlags =\n            'gim' +\n            (o.options.ecmaVersion >= 6 ? 'uy' : '') +\n            (o.options.ecmaVersion >= 9 ? 's' : '') +\n            (o.options.ecmaVersion >= 13 ? 'd' : '') +\n            (o.options.ecmaVersion >= 15 ? 'v' : '')),\n          (this.unicodeProperties =\n            mr[o.options.ecmaVersion >= 14 ? 14 : o.options.ecmaVersion]),\n          (this.source = ''),\n          (this.flags = ''),\n          (this.start = 0),\n          (this.switchU = !1),\n          (this.switchV = !1),\n          (this.switchN = !1),\n          (this.pos = 0),\n          (this.lastIntValue = 0),\n          (this.lastStringValue = ''),\n          (this.lastAssertionIsQuantifiable = !1),\n          (this.numCapturingParens = 0),\n          (this.maxBackReference = 0),\n          (this.groupNames = Object.create(null)),\n          (this.backReferenceNames = []),\n          (this.branchID = null);\n      };\n      (on.prototype.reset = function (o, l, f) {\n        var m = f.indexOf('v') !== -1,\n          E = f.indexOf('u') !== -1;\n        (this.start = o | 0),\n          (this.source = l + ''),\n          (this.flags = f),\n          m && this.parser.options.ecmaVersion >= 15\n            ? ((this.switchU = !0), (this.switchV = !0), (this.switchN = !0))\n            : ((this.switchU = E && this.parser.options.ecmaVersion >= 6),\n              (this.switchV = !1),\n              (this.switchN = E && this.parser.options.ecmaVersion >= 9));\n      }),\n        (on.prototype.raise = function (o) {\n          this.parser.raiseRecoverable(\n            this.start,\n            'Invalid regular expression: /' + this.source + '/: ' + o\n          );\n        }),\n        (on.prototype.at = function (o, l) {\n          l === void 0 && (l = !1);\n          var f = this.source,\n            m = f.length;\n          if (o >= m) return -1;\n          var E = f.charCodeAt(o);\n          if (!(l || this.switchU) || E <= 55295 || E >= 57344 || o + 1 >= m)\n            return E;\n          var O = f.charCodeAt(o + 1);\n          return O >= 56320 && O <= 57343 ? (E << 10) + O - 56613888 : E;\n        }),\n        (on.prototype.nextIndex = function (o, l) {\n          l === void 0 && (l = !1);\n          var f = this.source,\n            m = f.length;\n          if (o >= m) return m;\n          var E = f.charCodeAt(o),\n            O;\n          return !(l || this.switchU) ||\n            E <= 55295 ||\n            E >= 57344 ||\n            o + 1 >= m ||\n            (O = f.charCodeAt(o + 1)) < 56320 ||\n            O > 57343\n            ? o + 1\n            : o + 2;\n        }),\n        (on.prototype.current = function (o) {\n          return o === void 0 && (o = !1), this.at(this.pos, o);\n        }),\n        (on.prototype.lookahead = function (o) {\n          return (\n            o === void 0 && (o = !1), this.at(this.nextIndex(this.pos, o), o)\n          );\n        }),\n        (on.prototype.advance = function (o) {\n          o === void 0 && (o = !1), (this.pos = this.nextIndex(this.pos, o));\n        }),\n        (on.prototype.eat = function (o, l) {\n          return (\n            l === void 0 && (l = !1),\n            this.current(l) === o ? (this.advance(l), !0) : !1\n          );\n        }),\n        (on.prototype.eatChars = function (o, l) {\n          l === void 0 && (l = !1);\n          for (var f = this.pos, m = 0, E = o; m < E.length; m += 1) {\n            var O = E[m],\n              Y = this.at(f, l);\n            if (Y === -1 || Y !== O) return !1;\n            f = this.nextIndex(f, l);\n          }\n          return (this.pos = f), !0;\n        }),\n        (le.validateRegExpFlags = function (n) {\n          for (\n            var o = n.validFlags, l = n.flags, f = !1, m = !1, E = 0;\n            E < l.length;\n            E++\n          ) {\n            var O = l.charAt(E);\n            o.indexOf(O) === -1 &&\n              this.raise(n.start, 'Invalid regular expression flag'),\n              l.indexOf(O, E + 1) > -1 &&\n                this.raise(n.start, 'Duplicate regular expression flag'),\n              O === 'u' && (f = !0),\n              O === 'v' && (m = !0);\n          }\n          this.options.ecmaVersion >= 15 &&\n            f &&\n            m &&\n            this.raise(n.start, 'Invalid regular expression flag');\n        });\n      function Uo(n) {\n        for (var o in n) return !0;\n        return !1;\n      }\n      (le.validateRegExpPattern = function (n) {\n        this.regexp_pattern(n),\n          !n.switchN &&\n            this.options.ecmaVersion >= 9 &&\n            Uo(n.groupNames) &&\n            ((n.switchN = !0), this.regexp_pattern(n));\n      }),\n        (le.regexp_pattern = function (n) {\n          (n.pos = 0),\n            (n.lastIntValue = 0),\n            (n.lastStringValue = ''),\n            (n.lastAssertionIsQuantifiable = !1),\n            (n.numCapturingParens = 0),\n            (n.maxBackReference = 0),\n            (n.groupNames = Object.create(null)),\n            (n.backReferenceNames.length = 0),\n            (n.branchID = null),\n            this.regexp_disjunction(n),\n            n.pos !== n.source.length &&\n              (n.eat(41) && n.raise(\"Unmatched ')'\"),\n              (n.eat(93) || n.eat(125)) && n.raise('Lone quantifier brackets')),\n            n.maxBackReference > n.numCapturingParens &&\n              n.raise('Invalid escape');\n          for (var o = 0, l = n.backReferenceNames; o < l.length; o += 1) {\n            var f = l[o];\n            n.groupNames[f] || n.raise('Invalid named capture referenced');\n          }\n        }),\n        (le.regexp_disjunction = function (n) {\n          var o = this.options.ecmaVersion >= 16;\n          for (\n            o && (n.branchID = new Xs(n.branchID, null)),\n              this.regexp_alternative(n);\n            n.eat(124);\n\n          )\n            o && (n.branchID = n.branchID.sibling()),\n              this.regexp_alternative(n);\n          o && (n.branchID = n.branchID.parent),\n            this.regexp_eatQuantifier(n, !0) && n.raise('Nothing to repeat'),\n            n.eat(123) && n.raise('Lone quantifier brackets');\n        }),\n        (le.regexp_alternative = function (n) {\n          for (; n.pos < n.source.length && this.regexp_eatTerm(n); );\n        }),\n        (le.regexp_eatTerm = function (n) {\n          return this.regexp_eatAssertion(n)\n            ? (n.lastAssertionIsQuantifiable &&\n                this.regexp_eatQuantifier(n) &&\n                n.switchU &&\n                n.raise('Invalid quantifier'),\n              !0)\n            : (\n                n.switchU\n                  ? this.regexp_eatAtom(n)\n                  : this.regexp_eatExtendedAtom(n)\n              )\n            ? (this.regexp_eatQuantifier(n), !0)\n            : !1;\n        }),\n        (le.regexp_eatAssertion = function (n) {\n          var o = n.pos;\n          if (((n.lastAssertionIsQuantifiable = !1), n.eat(94) || n.eat(36)))\n            return !0;\n          if (n.eat(92)) {\n            if (n.eat(66) || n.eat(98)) return !0;\n            n.pos = o;\n          }\n          if (n.eat(40) && n.eat(63)) {\n            var l = !1;\n            if (\n              (this.options.ecmaVersion >= 9 && (l = n.eat(60)),\n              n.eat(61) || n.eat(33))\n            )\n              return (\n                this.regexp_disjunction(n),\n                n.eat(41) || n.raise('Unterminated group'),\n                (n.lastAssertionIsQuantifiable = !l),\n                !0\n              );\n          }\n          return (n.pos = o), !1;\n        }),\n        (le.regexp_eatQuantifier = function (n, o) {\n          return (\n            o === void 0 && (o = !1),\n            this.regexp_eatQuantifierPrefix(n, o) ? (n.eat(63), !0) : !1\n          );\n        }),\n        (le.regexp_eatQuantifierPrefix = function (n, o) {\n          return (\n            n.eat(42) ||\n            n.eat(43) ||\n            n.eat(63) ||\n            this.regexp_eatBracedQuantifier(n, o)\n          );\n        }),\n        (le.regexp_eatBracedQuantifier = function (n, o) {\n          var l = n.pos;\n          if (n.eat(123)) {\n            var f = 0,\n              m = -1;\n            if (\n              this.regexp_eatDecimalDigits(n) &&\n              ((f = n.lastIntValue),\n              n.eat(44) &&\n                this.regexp_eatDecimalDigits(n) &&\n                (m = n.lastIntValue),\n              n.eat(125))\n            )\n              return (\n                m !== -1 &&\n                  m < f &&\n                  !o &&\n                  n.raise('numbers out of order in {} quantifier'),\n                !0\n              );\n            n.switchU && !o && n.raise('Incomplete quantifier'), (n.pos = l);\n          }\n          return !1;\n        }),\n        (le.regexp_eatAtom = function (n) {\n          return (\n            this.regexp_eatPatternCharacters(n) ||\n            n.eat(46) ||\n            this.regexp_eatReverseSolidusAtomEscape(n) ||\n            this.regexp_eatCharacterClass(n) ||\n            this.regexp_eatUncapturingGroup(n) ||\n            this.regexp_eatCapturingGroup(n)\n          );\n        }),\n        (le.regexp_eatReverseSolidusAtomEscape = function (n) {\n          var o = n.pos;\n          if (n.eat(92)) {\n            if (this.regexp_eatAtomEscape(n)) return !0;\n            n.pos = o;\n          }\n          return !1;\n        }),\n        (le.regexp_eatUncapturingGroup = function (n) {\n          var o = n.pos;\n          if (n.eat(40)) {\n            if (n.eat(63)) {\n              if (this.options.ecmaVersion >= 16) {\n                var l = this.regexp_eatModifiers(n),\n                  f = n.eat(45);\n                if (l || f) {\n                  for (var m = 0; m < l.length; m++) {\n                    var E = l.charAt(m);\n                    l.indexOf(E, m + 1) > -1 &&\n                      n.raise('Duplicate regular expression modifiers');\n                  }\n                  if (f) {\n                    var O = this.regexp_eatModifiers(n);\n                    !l &&\n                      !O &&\n                      n.current() === 58 &&\n                      n.raise('Invalid regular expression modifiers');\n                    for (var Y = 0; Y < O.length; Y++) {\n                      var Q = O.charAt(Y);\n                      (O.indexOf(Q, Y + 1) > -1 || l.indexOf(Q) > -1) &&\n                        n.raise('Duplicate regular expression modifiers');\n                    }\n                  }\n                }\n              }\n              if (n.eat(58)) {\n                if ((this.regexp_disjunction(n), n.eat(41))) return !0;\n                n.raise('Unterminated group');\n              }\n            }\n            n.pos = o;\n          }\n          return !1;\n        }),\n        (le.regexp_eatCapturingGroup = function (n) {\n          if (n.eat(40)) {\n            if (\n              (this.options.ecmaVersion >= 9\n                ? this.regexp_groupSpecifier(n)\n                : n.current() === 63 && n.raise('Invalid group'),\n              this.regexp_disjunction(n),\n              n.eat(41))\n            )\n              return (n.numCapturingParens += 1), !0;\n            n.raise('Unterminated group');\n          }\n          return !1;\n        }),\n        (le.regexp_eatModifiers = function (n) {\n          for (var o = '', l = 0; (l = n.current()) !== -1 && Ho(l); )\n            (o += nt(l)), n.advance();\n          return o;\n        });\n      function Ho(n) {\n        return n === 105 || n === 109 || n === 115;\n      }\n      (le.regexp_eatExtendedAtom = function (n) {\n        return (\n          n.eat(46) ||\n          this.regexp_eatReverseSolidusAtomEscape(n) ||\n          this.regexp_eatCharacterClass(n) ||\n          this.regexp_eatUncapturingGroup(n) ||\n          this.regexp_eatCapturingGroup(n) ||\n          this.regexp_eatInvalidBracedQuantifier(n) ||\n          this.regexp_eatExtendedPatternCharacter(n)\n        );\n      }),\n        (le.regexp_eatInvalidBracedQuantifier = function (n) {\n          return (\n            this.regexp_eatBracedQuantifier(n, !0) &&\n              n.raise('Nothing to repeat'),\n            !1\n          );\n        }),\n        (le.regexp_eatSyntaxCharacter = function (n) {\n          var o = n.current();\n          return Tr(o) ? ((n.lastIntValue = o), n.advance(), !0) : !1;\n        });\n      function Tr(n) {\n        return (\n          n === 36 ||\n          (n >= 40 && n <= 43) ||\n          n === 46 ||\n          n === 63 ||\n          (n >= 91 && n <= 94) ||\n          (n >= 123 && n <= 125)\n        );\n      }\n      (le.regexp_eatPatternCharacters = function (n) {\n        for (var o = n.pos, l = 0; (l = n.current()) !== -1 && !Tr(l); )\n          n.advance();\n        return n.pos !== o;\n      }),\n        (le.regexp_eatExtendedPatternCharacter = function (n) {\n          var o = n.current();\n          return o !== -1 &&\n            o !== 36 &&\n            !(o >= 40 && o <= 43) &&\n            o !== 46 &&\n            o !== 63 &&\n            o !== 91 &&\n            o !== 94 &&\n            o !== 124\n            ? (n.advance(), !0)\n            : !1;\n        }),\n        (le.regexp_groupSpecifier = function (n) {\n          if (n.eat(63)) {\n            this.regexp_eatGroupName(n) || n.raise('Invalid group');\n            var o = this.options.ecmaVersion >= 16,\n              l = n.groupNames[n.lastStringValue];\n            if (l)\n              if (o)\n                for (var f = 0, m = l; f < m.length; f += 1) {\n                  var E = m[f];\n                  E.separatedFrom(n.branchID) ||\n                    n.raise('Duplicate capture group name');\n                }\n              else n.raise('Duplicate capture group name');\n            o\n              ? (l || (n.groupNames[n.lastStringValue] = [])).push(n.branchID)\n              : (n.groupNames[n.lastStringValue] = !0);\n          }\n        }),\n        (le.regexp_eatGroupName = function (n) {\n          if (((n.lastStringValue = ''), n.eat(60))) {\n            if (this.regexp_eatRegExpIdentifierName(n) && n.eat(62)) return !0;\n            n.raise('Invalid capture group name');\n          }\n          return !1;\n        }),\n        (le.regexp_eatRegExpIdentifierName = function (n) {\n          if (\n            ((n.lastStringValue = ''), this.regexp_eatRegExpIdentifierStart(n))\n          ) {\n            for (\n              n.lastStringValue += nt(n.lastIntValue);\n              this.regexp_eatRegExpIdentifierPart(n);\n\n            )\n              n.lastStringValue += nt(n.lastIntValue);\n            return !0;\n          }\n          return !1;\n        }),\n        (le.regexp_eatRegExpIdentifierStart = function (n) {\n          var o = n.pos,\n            l = this.options.ecmaVersion >= 11,\n            f = n.current(l);\n          return (\n            n.advance(l),\n            f === 92 &&\n              this.regexp_eatRegExpUnicodeEscapeSequence(n, l) &&\n              (f = n.lastIntValue),\n            Wo(f) ? ((n.lastIntValue = f), !0) : ((n.pos = o), !1)\n          );\n        });\n      function Wo(n) {\n        return h(n, !0) || n === 36 || n === 95;\n      }\n      le.regexp_eatRegExpIdentifierPart = function (n) {\n        var o = n.pos,\n          l = this.options.ecmaVersion >= 11,\n          f = n.current(l);\n        return (\n          n.advance(l),\n          f === 92 &&\n            this.regexp_eatRegExpUnicodeEscapeSequence(n, l) &&\n            (f = n.lastIntValue),\n          Go(f) ? ((n.lastIntValue = f), !0) : ((n.pos = o), !1)\n        );\n      };\n      function Go(n) {\n        return T(n, !0) || n === 36 || n === 95 || n === 8204 || n === 8205;\n      }\n      (le.regexp_eatAtomEscape = function (n) {\n        return this.regexp_eatBackReference(n) ||\n          this.regexp_eatCharacterClassEscape(n) ||\n          this.regexp_eatCharacterEscape(n) ||\n          (n.switchN && this.regexp_eatKGroupName(n))\n          ? !0\n          : (n.switchU &&\n              (n.current() === 99 && n.raise('Invalid unicode escape'),\n              n.raise('Invalid escape')),\n            !1);\n      }),\n        (le.regexp_eatBackReference = function (n) {\n          var o = n.pos;\n          if (this.regexp_eatDecimalEscape(n)) {\n            var l = n.lastIntValue;\n            if (n.switchU)\n              return l > n.maxBackReference && (n.maxBackReference = l), !0;\n            if (l <= n.numCapturingParens) return !0;\n            n.pos = o;\n          }\n          return !1;\n        }),\n        (le.regexp_eatKGroupName = function (n) {\n          if (n.eat(107)) {\n            if (this.regexp_eatGroupName(n))\n              return n.backReferenceNames.push(n.lastStringValue), !0;\n            n.raise('Invalid named reference');\n          }\n          return !1;\n        }),\n        (le.regexp_eatCharacterEscape = function (n) {\n          return (\n            this.regexp_eatControlEscape(n) ||\n            this.regexp_eatCControlLetter(n) ||\n            this.regexp_eatZero(n) ||\n            this.regexp_eatHexEscapeSequence(n) ||\n            this.regexp_eatRegExpUnicodeEscapeSequence(n, !1) ||\n            (!n.switchU && this.regexp_eatLegacyOctalEscapeSequence(n)) ||\n            this.regexp_eatIdentityEscape(n)\n          );\n        }),\n        (le.regexp_eatCControlLetter = function (n) {\n          var o = n.pos;\n          if (n.eat(99)) {\n            if (this.regexp_eatControlLetter(n)) return !0;\n            n.pos = o;\n          }\n          return !1;\n        }),\n        (le.regexp_eatZero = function (n) {\n          return n.current() === 48 && !vr(n.lookahead())\n            ? ((n.lastIntValue = 0), n.advance(), !0)\n            : !1;\n        }),\n        (le.regexp_eatControlEscape = function (n) {\n          var o = n.current();\n          return o === 116\n            ? ((n.lastIntValue = 9), n.advance(), !0)\n            : o === 110\n            ? ((n.lastIntValue = 10), n.advance(), !0)\n            : o === 118\n            ? ((n.lastIntValue = 11), n.advance(), !0)\n            : o === 102\n            ? ((n.lastIntValue = 12), n.advance(), !0)\n            : o === 114\n            ? ((n.lastIntValue = 13), n.advance(), !0)\n            : !1;\n        }),\n        (le.regexp_eatControlLetter = function (n) {\n          var o = n.current();\n          return kr(o) ? ((n.lastIntValue = o % 32), n.advance(), !0) : !1;\n        });\n      function kr(n) {\n        return (n >= 65 && n <= 90) || (n >= 97 && n <= 122);\n      }\n      le.regexp_eatRegExpUnicodeEscapeSequence = function (n, o) {\n        o === void 0 && (o = !1);\n        var l = n.pos,\n          f = o || n.switchU;\n        if (n.eat(117)) {\n          if (this.regexp_eatFixedHexDigits(n, 4)) {\n            var m = n.lastIntValue;\n            if (f && m >= 55296 && m <= 56319) {\n              var E = n.pos;\n              if (\n                n.eat(92) &&\n                n.eat(117) &&\n                this.regexp_eatFixedHexDigits(n, 4)\n              ) {\n                var O = n.lastIntValue;\n                if (O >= 56320 && O <= 57343)\n                  return (\n                    (n.lastIntValue = (m - 55296) * 1024 + (O - 56320) + 65536),\n                    !0\n                  );\n              }\n              (n.pos = E), (n.lastIntValue = m);\n            }\n            return !0;\n          }\n          if (\n            f &&\n            n.eat(123) &&\n            this.regexp_eatHexDigits(n) &&\n            n.eat(125) &&\n            vf(n.lastIntValue)\n          )\n            return !0;\n          f && n.raise('Invalid unicode escape'), (n.pos = l);\n        }\n        return !1;\n      };\n      function vf(n) {\n        return n >= 0 && n <= 1114111;\n      }\n      (le.regexp_eatIdentityEscape = function (n) {\n        if (n.switchU)\n          return this.regexp_eatSyntaxCharacter(n)\n            ? !0\n            : n.eat(47)\n            ? ((n.lastIntValue = 47), !0)\n            : !1;\n        var o = n.current();\n        return o !== 99 && (!n.switchN || o !== 107)\n          ? ((n.lastIntValue = o), n.advance(), !0)\n          : !1;\n      }),\n        (le.regexp_eatDecimalEscape = function (n) {\n          n.lastIntValue = 0;\n          var o = n.current();\n          if (o >= 49 && o <= 57) {\n            do (n.lastIntValue = 10 * n.lastIntValue + (o - 48)), n.advance();\n            while ((o = n.current()) >= 48 && o <= 57);\n            return !0;\n          }\n          return !1;\n        });\n      var Rc = 0,\n        Vn = 1,\n        an = 2;\n      le.regexp_eatCharacterClassEscape = function (n) {\n        var o = n.current();\n        if (xf(o)) return (n.lastIntValue = -1), n.advance(), Vn;\n        var l = !1;\n        if (\n          n.switchU &&\n          this.options.ecmaVersion >= 9 &&\n          ((l = o === 80) || o === 112)\n        ) {\n          (n.lastIntValue = -1), n.advance();\n          var f;\n          if (\n            n.eat(123) &&\n            (f = this.regexp_eatUnicodePropertyValueExpression(n)) &&\n            n.eat(125)\n          )\n            return l && f === an && n.raise('Invalid property name'), f;\n          n.raise('Invalid property name');\n        }\n        return Rc;\n      };\n      function xf(n) {\n        return (\n          n === 100 ||\n          n === 68 ||\n          n === 115 ||\n          n === 83 ||\n          n === 119 ||\n          n === 87\n        );\n      }\n      (le.regexp_eatUnicodePropertyValueExpression = function (n) {\n        var o = n.pos;\n        if (this.regexp_eatUnicodePropertyName(n) && n.eat(61)) {\n          var l = n.lastStringValue;\n          if (this.regexp_eatUnicodePropertyValue(n)) {\n            var f = n.lastStringValue;\n            return this.regexp_validateUnicodePropertyNameAndValue(n, l, f), Vn;\n          }\n        }\n        if (((n.pos = o), this.regexp_eatLoneUnicodePropertyNameOrValue(n))) {\n          var m = n.lastStringValue;\n          return this.regexp_validateUnicodePropertyNameOrValue(n, m);\n        }\n        return Rc;\n      }),\n        (le.regexp_validateUnicodePropertyNameAndValue = function (n, o, l) {\n          mt(n.unicodeProperties.nonBinary, o) ||\n            n.raise('Invalid property name'),\n            n.unicodeProperties.nonBinary[o].test(l) ||\n              n.raise('Invalid property value');\n        }),\n        (le.regexp_validateUnicodePropertyNameOrValue = function (n, o) {\n          if (n.unicodeProperties.binary.test(o)) return Vn;\n          if (n.switchV && n.unicodeProperties.binaryOfStrings.test(o))\n            return an;\n          n.raise('Invalid property name');\n        }),\n        (le.regexp_eatUnicodePropertyName = function (n) {\n          var o = 0;\n          for (n.lastStringValue = ''; Lc((o = n.current())); )\n            (n.lastStringValue += nt(o)), n.advance();\n          return n.lastStringValue !== '';\n        });\n      function Lc(n) {\n        return kr(n) || n === 95;\n      }\n      le.regexp_eatUnicodePropertyValue = function (n) {\n        var o = 0;\n        for (n.lastStringValue = ''; gf((o = n.current())); )\n          (n.lastStringValue += nt(o)), n.advance();\n        return n.lastStringValue !== '';\n      };\n      function gf(n) {\n        return Lc(n) || vr(n);\n      }\n      (le.regexp_eatLoneUnicodePropertyNameOrValue = function (n) {\n        return this.regexp_eatUnicodePropertyValue(n);\n      }),\n        (le.regexp_eatCharacterClass = function (n) {\n          if (n.eat(91)) {\n            var o = n.eat(94),\n              l = this.regexp_classContents(n);\n            return (\n              n.eat(93) || n.raise('Unterminated character class'),\n              o &&\n                l === an &&\n                n.raise('Negated character class may contain strings'),\n              !0\n            );\n          }\n          return !1;\n        }),\n        (le.regexp_classContents = function (n) {\n          return n.current() === 93\n            ? Vn\n            : n.switchV\n            ? this.regexp_classSetExpression(n)\n            : (this.regexp_nonEmptyClassRanges(n), Vn);\n        }),\n        (le.regexp_nonEmptyClassRanges = function (n) {\n          for (; this.regexp_eatClassAtom(n); ) {\n            var o = n.lastIntValue;\n            if (n.eat(45) && this.regexp_eatClassAtom(n)) {\n              var l = n.lastIntValue;\n              n.switchU &&\n                (o === -1 || l === -1) &&\n                n.raise('Invalid character class'),\n                o !== -1 &&\n                  l !== -1 &&\n                  o > l &&\n                  n.raise('Range out of order in character class');\n            }\n          }\n        }),\n        (le.regexp_eatClassAtom = function (n) {\n          var o = n.pos;\n          if (n.eat(92)) {\n            if (this.regexp_eatClassEscape(n)) return !0;\n            if (n.switchU) {\n              var l = n.current();\n              (l === 99 || Mc(l)) && n.raise('Invalid class escape'),\n                n.raise('Invalid escape');\n            }\n            n.pos = o;\n          }\n          var f = n.current();\n          return f !== 93 ? ((n.lastIntValue = f), n.advance(), !0) : !1;\n        }),\n        (le.regexp_eatClassEscape = function (n) {\n          var o = n.pos;\n          if (n.eat(98)) return (n.lastIntValue = 8), !0;\n          if (n.switchU && n.eat(45)) return (n.lastIntValue = 45), !0;\n          if (!n.switchU && n.eat(99)) {\n            if (this.regexp_eatClassControlLetter(n)) return !0;\n            n.pos = o;\n          }\n          return (\n            this.regexp_eatCharacterClassEscape(n) ||\n            this.regexp_eatCharacterEscape(n)\n          );\n        }),\n        (le.regexp_classSetExpression = function (n) {\n          var o = Vn,\n            l;\n          if (!this.regexp_eatClassSetRange(n))\n            if ((l = this.regexp_eatClassSetOperand(n))) {\n              l === an && (o = an);\n              for (var f = n.pos; n.eatChars([38, 38]); ) {\n                if (\n                  n.current() !== 38 &&\n                  (l = this.regexp_eatClassSetOperand(n))\n                ) {\n                  l !== an && (o = Vn);\n                  continue;\n                }\n                n.raise('Invalid character in character class');\n              }\n              if (f !== n.pos) return o;\n              for (; n.eatChars([45, 45]); )\n                this.regexp_eatClassSetOperand(n) ||\n                  n.raise('Invalid character in character class');\n              if (f !== n.pos) return o;\n            } else n.raise('Invalid character in character class');\n          for (;;)\n            if (!this.regexp_eatClassSetRange(n)) {\n              if (((l = this.regexp_eatClassSetOperand(n)), !l)) return o;\n              l === an && (o = an);\n            }\n        }),\n        (le.regexp_eatClassSetRange = function (n) {\n          var o = n.pos;\n          if (this.regexp_eatClassSetCharacter(n)) {\n            var l = n.lastIntValue;\n            if (n.eat(45) && this.regexp_eatClassSetCharacter(n)) {\n              var f = n.lastIntValue;\n              return (\n                l !== -1 &&\n                  f !== -1 &&\n                  l > f &&\n                  n.raise('Range out of order in character class'),\n                !0\n              );\n            }\n            n.pos = o;\n          }\n          return !1;\n        }),\n        (le.regexp_eatClassSetOperand = function (n) {\n          return this.regexp_eatClassSetCharacter(n)\n            ? Vn\n            : this.regexp_eatClassStringDisjunction(n) ||\n                this.regexp_eatNestedClass(n);\n        }),\n        (le.regexp_eatNestedClass = function (n) {\n          var o = n.pos;\n          if (n.eat(91)) {\n            var l = n.eat(94),\n              f = this.regexp_classContents(n);\n            if (n.eat(93))\n              return (\n                l &&\n                  f === an &&\n                  n.raise('Negated character class may contain strings'),\n                f\n              );\n            n.pos = o;\n          }\n          if (n.eat(92)) {\n            var m = this.regexp_eatCharacterClassEscape(n);\n            if (m) return m;\n            n.pos = o;\n          }\n          return null;\n        }),\n        (le.regexp_eatClassStringDisjunction = function (n) {\n          var o = n.pos;\n          if (n.eatChars([92, 113])) {\n            if (n.eat(123)) {\n              var l = this.regexp_classStringDisjunctionContents(n);\n              if (n.eat(125)) return l;\n            } else n.raise('Invalid escape');\n            n.pos = o;\n          }\n          return null;\n        }),\n        (le.regexp_classStringDisjunctionContents = function (n) {\n          for (var o = this.regexp_classString(n); n.eat(124); )\n            this.regexp_classString(n) === an && (o = an);\n          return o;\n        }),\n        (le.regexp_classString = function (n) {\n          for (var o = 0; this.regexp_eatClassSetCharacter(n); ) o++;\n          return o === 1 ? Vn : an;\n        }),\n        (le.regexp_eatClassSetCharacter = function (n) {\n          var o = n.pos;\n          if (n.eat(92))\n            return this.regexp_eatCharacterEscape(n) ||\n              this.regexp_eatClassSetReservedPunctuator(n)\n              ? !0\n              : n.eat(98)\n              ? ((n.lastIntValue = 8), !0)\n              : ((n.pos = o), !1);\n          var l = n.current();\n          return l < 0 || (l === n.lookahead() && _f(l)) || bf(l)\n            ? !1\n            : (n.advance(), (n.lastIntValue = l), !0);\n        });\n      function _f(n) {\n        return (\n          n === 33 ||\n          (n >= 35 && n <= 38) ||\n          (n >= 42 && n <= 44) ||\n          n === 46 ||\n          (n >= 58 && n <= 64) ||\n          n === 94 ||\n          n === 96 ||\n          n === 126\n        );\n      }\n      function bf(n) {\n        return (\n          n === 40 ||\n          n === 41 ||\n          n === 45 ||\n          n === 47 ||\n          (n >= 91 && n <= 93) ||\n          (n >= 123 && n <= 125)\n        );\n      }\n      le.regexp_eatClassSetReservedPunctuator = function (n) {\n        var o = n.current();\n        return Cf(o) ? ((n.lastIntValue = o), n.advance(), !0) : !1;\n      };\n      function Cf(n) {\n        return (\n          n === 33 ||\n          n === 35 ||\n          n === 37 ||\n          n === 38 ||\n          n === 44 ||\n          n === 45 ||\n          (n >= 58 && n <= 62) ||\n          n === 64 ||\n          n === 96 ||\n          n === 126\n        );\n      }\n      (le.regexp_eatClassControlLetter = function (n) {\n        var o = n.current();\n        return vr(o) || o === 95\n          ? ((n.lastIntValue = o % 32), n.advance(), !0)\n          : !1;\n      }),\n        (le.regexp_eatHexEscapeSequence = function (n) {\n          var o = n.pos;\n          if (n.eat(120)) {\n            if (this.regexp_eatFixedHexDigits(n, 2)) return !0;\n            n.switchU && n.raise('Invalid escape'), (n.pos = o);\n          }\n          return !1;\n        }),\n        (le.regexp_eatDecimalDigits = function (n) {\n          var o = n.pos,\n            l = 0;\n          for (n.lastIntValue = 0; vr((l = n.current())); )\n            (n.lastIntValue = 10 * n.lastIntValue + (l - 48)), n.advance();\n          return n.pos !== o;\n        });\n      function vr(n) {\n        return n >= 48 && n <= 57;\n      }\n      le.regexp_eatHexDigits = function (n) {\n        var o = n.pos,\n          l = 0;\n        for (n.lastIntValue = 0; Oc((l = n.current())); )\n          (n.lastIntValue = 16 * n.lastIntValue + Dc(l)), n.advance();\n        return n.pos !== o;\n      };\n      function Oc(n) {\n        return (\n          (n >= 48 && n <= 57) || (n >= 65 && n <= 70) || (n >= 97 && n <= 102)\n        );\n      }\n      function Dc(n) {\n        return n >= 65 && n <= 70\n          ? 10 + (n - 65)\n          : n >= 97 && n <= 102\n          ? 10 + (n - 97)\n          : n - 48;\n      }\n      (le.regexp_eatLegacyOctalEscapeSequence = function (n) {\n        if (this.regexp_eatOctalDigit(n)) {\n          var o = n.lastIntValue;\n          if (this.regexp_eatOctalDigit(n)) {\n            var l = n.lastIntValue;\n            o <= 3 && this.regexp_eatOctalDigit(n)\n              ? (n.lastIntValue = o * 64 + l * 8 + n.lastIntValue)\n              : (n.lastIntValue = o * 8 + l);\n          } else n.lastIntValue = o;\n          return !0;\n        }\n        return !1;\n      }),\n        (le.regexp_eatOctalDigit = function (n) {\n          var o = n.current();\n          return Mc(o)\n            ? ((n.lastIntValue = o - 48), n.advance(), !0)\n            : ((n.lastIntValue = 0), !1);\n        });\n      function Mc(n) {\n        return n >= 48 && n <= 55;\n      }\n      le.regexp_eatFixedHexDigits = function (n, o) {\n        var l = n.pos;\n        n.lastIntValue = 0;\n        for (var f = 0; f < o; ++f) {\n          var m = n.current();\n          if (!Oc(m)) return (n.pos = l), !1;\n          (n.lastIntValue = 16 * n.lastIntValue + Dc(m)), n.advance();\n        }\n        return !0;\n      };\n      var xr = function (o) {\n          (this.type = o.type),\n            (this.value = o.value),\n            (this.start = o.start),\n            (this.end = o.end),\n            o.options.locations && (this.loc = new wt(o, o.startLoc, o.endLoc)),\n            o.options.ranges && (this.range = [o.start, o.end]);\n        },\n        Ae = Ge.prototype;\n      (Ae.next = function (n) {\n        !n &&\n          this.type.keyword &&\n          this.containsEsc &&\n          this.raiseRecoverable(\n            this.start,\n            'Escape sequence in keyword ' + this.type.keyword\n          ),\n          this.options.onToken && this.options.onToken(new xr(this)),\n          (this.lastTokEnd = this.end),\n          (this.lastTokStart = this.start),\n          (this.lastTokEndLoc = this.endLoc),\n          (this.lastTokStartLoc = this.startLoc),\n          this.nextToken();\n      }),\n        (Ae.getToken = function () {\n          return this.next(), new xr(this);\n        }),\n        typeof Symbol < 'u' &&\n          (Ae[Symbol.iterator] = function () {\n            var n = this;\n            return {\n              next: function () {\n                var o = n.getToken();\n                return {done: o.type === c.eof, value: o};\n              },\n            };\n          }),\n        (Ae.nextToken = function () {\n          var n = this.curContext();\n          if (\n            ((!n || !n.preserveSpace) && this.skipSpace(),\n            (this.start = this.pos),\n            this.options.locations && (this.startLoc = this.curPosition()),\n            this.pos >= this.input.length)\n          )\n            return this.finishToken(c.eof);\n          if (n.override) return n.override(this);\n          this.readToken(this.fullCharCodeAtPos());\n        }),\n        (Ae.readToken = function (n) {\n          return h(n, this.options.ecmaVersion >= 6) || n === 92\n            ? this.readWord()\n            : this.getTokenFromCode(n);\n        }),\n        (Ae.fullCharCodeAtPos = function () {\n          var n = this.input.charCodeAt(this.pos);\n          if (n <= 55295 || n >= 56320) return n;\n          var o = this.input.charCodeAt(this.pos + 1);\n          return o <= 56319 || o >= 57344 ? n : (n << 10) + o - 56613888;\n        }),\n        (Ae.skipBlockComment = function () {\n          var n = this.options.onComment && this.curPosition(),\n            o = this.pos,\n            l = this.input.indexOf('*/', (this.pos += 2));\n          if (\n            (l === -1 && this.raise(this.pos - 2, 'Unterminated comment'),\n            (this.pos = l + 2),\n            this.options.locations)\n          )\n            for (\n              var f = void 0, m = o;\n              (f = ie(this.input, m, this.pos)) > -1;\n\n            )\n              ++this.curLine, (m = this.lineStart = f);\n          this.options.onComment &&\n            this.options.onComment(\n              !0,\n              this.input.slice(o + 2, l),\n              o,\n              this.pos,\n              n,\n              this.curPosition()\n            );\n        }),\n        (Ae.skipLineComment = function (n) {\n          for (\n            var o = this.pos,\n              l = this.options.onComment && this.curPosition(),\n              f = this.input.charCodeAt((this.pos += n));\n            this.pos < this.input.length && !X(f);\n\n          )\n            f = this.input.charCodeAt(++this.pos);\n          this.options.onComment &&\n            this.options.onComment(\n              !1,\n              this.input.slice(o + n, this.pos),\n              o,\n              this.pos,\n              l,\n              this.curPosition()\n            );\n        }),\n        (Ae.skipSpace = function () {\n          e: for (; this.pos < this.input.length; ) {\n            var n = this.input.charCodeAt(this.pos);\n            switch (n) {\n              case 32:\n              case 160:\n                ++this.pos;\n                break;\n              case 13:\n                this.input.charCodeAt(this.pos + 1) === 10 && ++this.pos;\n              case 10:\n              case 8232:\n              case 8233:\n                ++this.pos,\n                  this.options.locations &&\n                    (++this.curLine, (this.lineStart = this.pos));\n                break;\n              case 47:\n                switch (this.input.charCodeAt(this.pos + 1)) {\n                  case 42:\n                    this.skipBlockComment();\n                    break;\n                  case 47:\n                    this.skipLineComment(2);\n                    break;\n                  default:\n                    break e;\n                }\n                break;\n              default:\n                if (\n                  (n > 8 && n < 14) ||\n                  (n >= 5760 && pe.test(String.fromCharCode(n)))\n                )\n                  ++this.pos;\n                else break e;\n            }\n          }\n        }),\n        (Ae.finishToken = function (n, o) {\n          (this.end = this.pos),\n            this.options.locations && (this.endLoc = this.curPosition());\n          var l = this.type;\n          (this.type = n), (this.value = o), this.updateContext(l);\n        }),\n        (Ae.readToken_dot = function () {\n          var n = this.input.charCodeAt(this.pos + 1);\n          if (n >= 48 && n <= 57) return this.readNumber(!0);\n          var o = this.input.charCodeAt(this.pos + 2);\n          return this.options.ecmaVersion >= 6 && n === 46 && o === 46\n            ? ((this.pos += 3), this.finishToken(c.ellipsis))\n            : (++this.pos, this.finishToken(c.dot));\n        }),\n        (Ae.readToken_slash = function () {\n          var n = this.input.charCodeAt(this.pos + 1);\n          return this.exprAllowed\n            ? (++this.pos, this.readRegexp())\n            : n === 61\n            ? this.finishOp(c.assign, 2)\n            : this.finishOp(c.slash, 1);\n        }),\n        (Ae.readToken_mult_modulo_exp = function (n) {\n          var o = this.input.charCodeAt(this.pos + 1),\n            l = 1,\n            f = n === 42 ? c.star : c.modulo;\n          return (\n            this.options.ecmaVersion >= 7 &&\n              n === 42 &&\n              o === 42 &&\n              (++l,\n              (f = c.starstar),\n              (o = this.input.charCodeAt(this.pos + 2))),\n            o === 61 ? this.finishOp(c.assign, l + 1) : this.finishOp(f, l)\n          );\n        }),\n        (Ae.readToken_pipe_amp = function (n) {\n          var o = this.input.charCodeAt(this.pos + 1);\n          if (o === n) {\n            if (this.options.ecmaVersion >= 12) {\n              var l = this.input.charCodeAt(this.pos + 2);\n              if (l === 61) return this.finishOp(c.assign, 3);\n            }\n            return this.finishOp(n === 124 ? c.logicalOR : c.logicalAND, 2);\n          }\n          return o === 61\n            ? this.finishOp(c.assign, 2)\n            : this.finishOp(n === 124 ? c.bitwiseOR : c.bitwiseAND, 1);\n        }),\n        (Ae.readToken_caret = function () {\n          var n = this.input.charCodeAt(this.pos + 1);\n          return n === 61\n            ? this.finishOp(c.assign, 2)\n            : this.finishOp(c.bitwiseXOR, 1);\n        }),\n        (Ae.readToken_plus_min = function (n) {\n          var o = this.input.charCodeAt(this.pos + 1);\n          return o === n\n            ? o === 45 &&\n              !this.inModule &&\n              this.input.charCodeAt(this.pos + 2) === 62 &&\n              (this.lastTokEnd === 0 ||\n                R.test(this.input.slice(this.lastTokEnd, this.pos)))\n              ? (this.skipLineComment(3), this.skipSpace(), this.nextToken())\n              : this.finishOp(c.incDec, 2)\n            : o === 61\n            ? this.finishOp(c.assign, 2)\n            : this.finishOp(c.plusMin, 1);\n        }),\n        (Ae.readToken_lt_gt = function (n) {\n          var o = this.input.charCodeAt(this.pos + 1),\n            l = 1;\n          return o === n\n            ? ((l =\n                n === 62 && this.input.charCodeAt(this.pos + 2) === 62 ? 3 : 2),\n              this.input.charCodeAt(this.pos + l) === 61\n                ? this.finishOp(c.assign, l + 1)\n                : this.finishOp(c.bitShift, l))\n            : o === 33 &&\n              n === 60 &&\n              !this.inModule &&\n              this.input.charCodeAt(this.pos + 2) === 45 &&\n              this.input.charCodeAt(this.pos + 3) === 45\n            ? (this.skipLineComment(4), this.skipSpace(), this.nextToken())\n            : (o === 61 && (l = 2), this.finishOp(c.relational, l));\n        }),\n        (Ae.readToken_eq_excl = function (n) {\n          var o = this.input.charCodeAt(this.pos + 1);\n          return o === 61\n            ? this.finishOp(\n                c.equality,\n                this.input.charCodeAt(this.pos + 2) === 61 ? 3 : 2\n              )\n            : n === 61 && o === 62 && this.options.ecmaVersion >= 6\n            ? ((this.pos += 2), this.finishToken(c.arrow))\n            : this.finishOp(n === 61 ? c.eq : c.prefix, 1);\n        }),\n        (Ae.readToken_question = function () {\n          var n = this.options.ecmaVersion;\n          if (n >= 11) {\n            var o = this.input.charCodeAt(this.pos + 1);\n            if (o === 46) {\n              var l = this.input.charCodeAt(this.pos + 2);\n              if (l < 48 || l > 57) return this.finishOp(c.questionDot, 2);\n            }\n            if (o === 63) {\n              if (n >= 12) {\n                var f = this.input.charCodeAt(this.pos + 2);\n                if (f === 61) return this.finishOp(c.assign, 3);\n              }\n              return this.finishOp(c.coalesce, 2);\n            }\n          }\n          return this.finishOp(c.question, 1);\n        }),\n        (Ae.readToken_numberSign = function () {\n          var n = this.options.ecmaVersion,\n            o = 35;\n          if (\n            n >= 13 &&\n            (++this.pos, (o = this.fullCharCodeAtPos()), h(o, !0) || o === 92)\n          )\n            return this.finishToken(c.privateId, this.readWord1());\n          this.raise(this.pos, \"Unexpected character '\" + nt(o) + \"'\");\n        }),\n        (Ae.getTokenFromCode = function (n) {\n          switch (n) {\n            case 46:\n              return this.readToken_dot();\n            case 40:\n              return ++this.pos, this.finishToken(c.parenL);\n            case 41:\n              return ++this.pos, this.finishToken(c.parenR);\n            case 59:\n              return ++this.pos, this.finishToken(c.semi);\n            case 44:\n              return ++this.pos, this.finishToken(c.comma);\n            case 91:\n              return ++this.pos, this.finishToken(c.bracketL);\n            case 93:\n              return ++this.pos, this.finishToken(c.bracketR);\n            case 123:\n              return ++this.pos, this.finishToken(c.braceL);\n            case 125:\n              return ++this.pos, this.finishToken(c.braceR);\n            case 58:\n              return ++this.pos, this.finishToken(c.colon);\n            case 96:\n              if (this.options.ecmaVersion < 6) break;\n              return ++this.pos, this.finishToken(c.backQuote);\n            case 48:\n              var o = this.input.charCodeAt(this.pos + 1);\n              if (o === 120 || o === 88) return this.readRadixNumber(16);\n              if (this.options.ecmaVersion >= 6) {\n                if (o === 111 || o === 79) return this.readRadixNumber(8);\n                if (o === 98 || o === 66) return this.readRadixNumber(2);\n              }\n            case 49:\n            case 50:\n            case 51:\n            case 52:\n            case 53:\n            case 54:\n            case 55:\n            case 56:\n            case 57:\n              return this.readNumber(!1);\n            case 34:\n            case 39:\n              return this.readString(n);\n            case 47:\n              return this.readToken_slash();\n            case 37:\n            case 42:\n              return this.readToken_mult_modulo_exp(n);\n            case 124:\n            case 38:\n              return this.readToken_pipe_amp(n);\n            case 94:\n              return this.readToken_caret();\n            case 43:\n            case 45:\n              return this.readToken_plus_min(n);\n            case 60:\n            case 62:\n              return this.readToken_lt_gt(n);\n            case 61:\n            case 33:\n              return this.readToken_eq_excl(n);\n            case 63:\n              return this.readToken_question();\n            case 126:\n              return this.finishOp(c.prefix, 1);\n            case 35:\n              return this.readToken_numberSign();\n          }\n          this.raise(this.pos, \"Unexpected character '\" + nt(n) + \"'\");\n        }),\n        (Ae.finishOp = function (n, o) {\n          var l = this.input.slice(this.pos, this.pos + o);\n          return (this.pos += o), this.finishToken(n, l);\n        }),\n        (Ae.readRegexp = function () {\n          for (var n, o, l = this.pos; ; ) {\n            this.pos >= this.input.length &&\n              this.raise(l, 'Unterminated regular expression');\n            var f = this.input.charAt(this.pos);\n            if (\n              (R.test(f) && this.raise(l, 'Unterminated regular expression'), n)\n            )\n              n = !1;\n            else {\n              if (f === '[') o = !0;\n              else if (f === ']' && o) o = !1;\n              else if (f === '/' && !o) break;\n              n = f === '\\\\';\n            }\n            ++this.pos;\n          }\n          var m = this.input.slice(l, this.pos);\n          ++this.pos;\n          var E = this.pos,\n            O = this.readWord1();\n          this.containsEsc && this.unexpected(E);\n          var Y = this.regexpState || (this.regexpState = new on(this));\n          Y.reset(l, m, O),\n            this.validateRegExpFlags(Y),\n            this.validateRegExpPattern(Y);\n          var Q = null;\n          try {\n            Q = new RegExp(m, O);\n          } catch {}\n          return this.finishToken(c.regexp, {pattern: m, flags: O, value: Q});\n        }),\n        (Ae.readInt = function (n, o, l) {\n          for (\n            var f = this.options.ecmaVersion >= 12 && o === void 0,\n              m = l && this.input.charCodeAt(this.pos) === 48,\n              E = this.pos,\n              O = 0,\n              Y = 0,\n              Q = 0,\n              Te = o ?? 1 / 0;\n            Q < Te;\n            ++Q, ++this.pos\n          ) {\n            var xe = this.input.charCodeAt(this.pos),\n              Ze = void 0;\n            if (f && xe === 95) {\n              m &&\n                this.raiseRecoverable(\n                  this.pos,\n                  'Numeric separator is not allowed in legacy octal numeric literals'\n                ),\n                Y === 95 &&\n                  this.raiseRecoverable(\n                    this.pos,\n                    'Numeric separator must be exactly one underscore'\n                  ),\n                Q === 0 &&\n                  this.raiseRecoverable(\n                    this.pos,\n                    'Numeric separator is not allowed at the first of digits'\n                  ),\n                (Y = xe);\n              continue;\n            }\n            if (\n              (xe >= 97\n                ? (Ze = xe - 97 + 10)\n                : xe >= 65\n                ? (Ze = xe - 65 + 10)\n                : xe >= 48 && xe <= 57\n                ? (Ze = xe - 48)\n                : (Ze = 1 / 0),\n              Ze >= n)\n            )\n              break;\n            (Y = xe), (O = O * n + Ze);\n          }\n          return (\n            f &&\n              Y === 95 &&\n              this.raiseRecoverable(\n                this.pos - 1,\n                'Numeric separator is not allowed at the last of digits'\n              ),\n            this.pos === E || (o != null && this.pos - E !== o) ? null : O\n          );\n        });\n      function wf(n, o) {\n        return o ? parseInt(n, 8) : parseFloat(n.replace(/_/g, ''));\n      }\n      function Fc(n) {\n        return typeof BigInt != 'function' ? null : BigInt(n.replace(/_/g, ''));\n      }\n      (Ae.readRadixNumber = function (n) {\n        var o = this.pos;\n        this.pos += 2;\n        var l = this.readInt(n);\n        return (\n          l == null &&\n            this.raise(this.start + 2, 'Expected number in radix ' + n),\n          this.options.ecmaVersion >= 11 &&\n          this.input.charCodeAt(this.pos) === 110\n            ? ((l = Fc(this.input.slice(o, this.pos))), ++this.pos)\n            : h(this.fullCharCodeAtPos()) &&\n              this.raise(this.pos, 'Identifier directly after number'),\n          this.finishToken(c.num, l)\n        );\n      }),\n        (Ae.readNumber = function (n) {\n          var o = this.pos;\n          !n &&\n            this.readInt(10, void 0, !0) === null &&\n            this.raise(o, 'Invalid number');\n          var l = this.pos - o >= 2 && this.input.charCodeAt(o) === 48;\n          l && this.strict && this.raise(o, 'Invalid number');\n          var f = this.input.charCodeAt(this.pos);\n          if (!l && !n && this.options.ecmaVersion >= 11 && f === 110) {\n            var m = Fc(this.input.slice(o, this.pos));\n            return (\n              ++this.pos,\n              h(this.fullCharCodeAtPos()) &&\n                this.raise(this.pos, 'Identifier directly after number'),\n              this.finishToken(c.num, m)\n            );\n          }\n          l && /[89]/.test(this.input.slice(o, this.pos)) && (l = !1),\n            f === 46 &&\n              !l &&\n              (++this.pos,\n              this.readInt(10),\n              (f = this.input.charCodeAt(this.pos))),\n            (f === 69 || f === 101) &&\n              !l &&\n              ((f = this.input.charCodeAt(++this.pos)),\n              (f === 43 || f === 45) && ++this.pos,\n              this.readInt(10) === null && this.raise(o, 'Invalid number')),\n            h(this.fullCharCodeAtPos()) &&\n              this.raise(this.pos, 'Identifier directly after number');\n          var E = wf(this.input.slice(o, this.pos), l);\n          return this.finishToken(c.num, E);\n        }),\n        (Ae.readCodePoint = function () {\n          var n = this.input.charCodeAt(this.pos),\n            o;\n          if (n === 123) {\n            this.options.ecmaVersion < 6 && this.unexpected();\n            var l = ++this.pos;\n            (o = this.readHexChar(\n              this.input.indexOf('}', this.pos) - this.pos\n            )),\n              ++this.pos,\n              o > 1114111 &&\n                this.invalidStringToken(l, 'Code point out of bounds');\n          } else o = this.readHexChar(4);\n          return o;\n        }),\n        (Ae.readString = function (n) {\n          for (var o = '', l = ++this.pos; ; ) {\n            this.pos >= this.input.length &&\n              this.raise(this.start, 'Unterminated string constant');\n            var f = this.input.charCodeAt(this.pos);\n            if (f === n) break;\n            f === 92\n              ? ((o += this.input.slice(l, this.pos)),\n                (o += this.readEscapedChar(!1)),\n                (l = this.pos))\n              : f === 8232 || f === 8233\n              ? (this.options.ecmaVersion < 10 &&\n                  this.raise(this.start, 'Unterminated string constant'),\n                ++this.pos,\n                this.options.locations &&\n                  (this.curLine++, (this.lineStart = this.pos)))\n              : (X(f) && this.raise(this.start, 'Unterminated string constant'),\n                ++this.pos);\n          }\n          return (\n            (o += this.input.slice(l, this.pos++)),\n            this.finishToken(c.string, o)\n          );\n        });\n      var Bc = {};\n      (Ae.tryReadTemplateToken = function () {\n        this.inTemplateElement = !0;\n        try {\n          this.readTmplToken();\n        } catch (n) {\n          if (n === Bc) this.readInvalidTemplateToken();\n          else throw n;\n        }\n        this.inTemplateElement = !1;\n      }),\n        (Ae.invalidStringToken = function (n, o) {\n          if (this.inTemplateElement && this.options.ecmaVersion >= 9) throw Bc;\n          this.raise(n, o);\n        }),\n        (Ae.readTmplToken = function () {\n          for (var n = '', o = this.pos; ; ) {\n            this.pos >= this.input.length &&\n              this.raise(this.start, 'Unterminated template');\n            var l = this.input.charCodeAt(this.pos);\n            if (\n              l === 96 ||\n              (l === 36 && this.input.charCodeAt(this.pos + 1) === 123)\n            )\n              return this.pos === this.start &&\n                (this.type === c.template || this.type === c.invalidTemplate)\n                ? l === 36\n                  ? ((this.pos += 2), this.finishToken(c.dollarBraceL))\n                  : (++this.pos, this.finishToken(c.backQuote))\n                : ((n += this.input.slice(o, this.pos)),\n                  this.finishToken(c.template, n));\n            if (l === 92)\n              (n += this.input.slice(o, this.pos)),\n                (n += this.readEscapedChar(!0)),\n                (o = this.pos);\n            else if (X(l)) {\n              switch (((n += this.input.slice(o, this.pos)), ++this.pos, l)) {\n                case 13:\n                  this.input.charCodeAt(this.pos) === 10 && ++this.pos;\n                case 10:\n                  n += `\n`;\n                  break;\n                default:\n                  n += String.fromCharCode(l);\n                  break;\n              }\n              this.options.locations &&\n                (++this.curLine, (this.lineStart = this.pos)),\n                (o = this.pos);\n            } else ++this.pos;\n          }\n        }),\n        (Ae.readInvalidTemplateToken = function () {\n          for (; this.pos < this.input.length; this.pos++)\n            switch (this.input[this.pos]) {\n              case '\\\\':\n                ++this.pos;\n                break;\n              case '$':\n                if (this.input[this.pos + 1] !== '{') break;\n              case '`':\n                return this.finishToken(\n                  c.invalidTemplate,\n                  this.input.slice(this.start, this.pos)\n                );\n              case '\\r':\n                this.input[this.pos + 1] ===\n                  `\n` && ++this.pos;\n              case `\n`:\n              case '\\u2028':\n              case '\\u2029':\n                ++this.curLine, (this.lineStart = this.pos + 1);\n                break;\n            }\n          this.raise(this.start, 'Unterminated template');\n        }),\n        (Ae.readEscapedChar = function (n) {\n          var o = this.input.charCodeAt(++this.pos);\n          switch ((++this.pos, o)) {\n            case 110:\n              return `\n`;\n            case 114:\n              return '\\r';\n            case 120:\n              return String.fromCharCode(this.readHexChar(2));\n            case 117:\n              return nt(this.readCodePoint());\n            case 116:\n              return '\t';\n            case 98:\n              return '\\b';\n            case 118:\n              return '\\v';\n            case 102:\n              return '\\f';\n            case 13:\n              this.input.charCodeAt(this.pos) === 10 && ++this.pos;\n            case 10:\n              return (\n                this.options.locations &&\n                  ((this.lineStart = this.pos), ++this.curLine),\n                ''\n              );\n            case 56:\n            case 57:\n              if (\n                (this.strict &&\n                  this.invalidStringToken(\n                    this.pos - 1,\n                    'Invalid escape sequence'\n                  ),\n                n)\n              ) {\n                var l = this.pos - 1;\n                this.invalidStringToken(\n                  l,\n                  'Invalid escape sequence in template string'\n                );\n              }\n            default:\n              if (o >= 48 && o <= 55) {\n                var f = this.input.substr(this.pos - 1, 3).match(/^[0-7]+/)[0],\n                  m = parseInt(f, 8);\n                return (\n                  m > 255 && ((f = f.slice(0, -1)), (m = parseInt(f, 8))),\n                  (this.pos += f.length - 1),\n                  (o = this.input.charCodeAt(this.pos)),\n                  (f !== '0' || o === 56 || o === 57) &&\n                    (this.strict || n) &&\n                    this.invalidStringToken(\n                      this.pos - 1 - f.length,\n                      n\n                        ? 'Octal literal in template string'\n                        : 'Octal literal in strict mode'\n                    ),\n                  String.fromCharCode(m)\n                );\n              }\n              return X(o)\n                ? (this.options.locations &&\n                    ((this.lineStart = this.pos), ++this.curLine),\n                  '')\n                : String.fromCharCode(o);\n          }\n        }),\n        (Ae.readHexChar = function (n) {\n          var o = this.pos,\n            l = this.readInt(16, n);\n          return (\n            l === null &&\n              this.invalidStringToken(o, 'Bad character escape sequence'),\n            l\n          );\n        }),\n        (Ae.readWord1 = function () {\n          this.containsEsc = !1;\n          for (\n            var n = '', o = !0, l = this.pos, f = this.options.ecmaVersion >= 6;\n            this.pos < this.input.length;\n\n          ) {\n            var m = this.fullCharCodeAtPos();\n            if (T(m, f)) this.pos += m <= 65535 ? 1 : 2;\n            else if (m === 92) {\n              (this.containsEsc = !0), (n += this.input.slice(l, this.pos));\n              var E = this.pos;\n              this.input.charCodeAt(++this.pos) !== 117 &&\n                this.invalidStringToken(\n                  this.pos,\n                  'Expecting Unicode escape sequence \\\\uXXXX'\n                ),\n                ++this.pos;\n              var O = this.readCodePoint();\n              (o ? h : T)(O, f) ||\n                this.invalidStringToken(E, 'Invalid Unicode escape'),\n                (n += nt(O)),\n                (l = this.pos);\n            } else break;\n            o = !1;\n          }\n          return n + this.input.slice(l, this.pos);\n        }),\n        (Ae.readWord = function () {\n          var n = this.readWord1(),\n            o = c.name;\n          return this.keywords.test(n) && (o = U[n]), this.finishToken(o, n);\n        });\n      var Vc = '8.15.0';\n      Ge.acorn = {\n        Parser: Ge,\n        version: Vc,\n        defaultOptions: Pt,\n        Position: ct,\n        SourceLocation: wt,\n        getLineInfo: $t,\n        Node: Fn,\n        TokenType: x,\n        tokTypes: c,\n        keywordTypes: U,\n        TokContext: Rt,\n        tokContexts: Ue,\n        isIdentifierChar: T,\n        isIdentifierStart: h,\n        Token: xr,\n        isNewLine: X,\n        lineBreak: R,\n        lineBreakG: W,\n        nonASCIIwhitespace: pe,\n      };\n      function Sf(n, o) {\n        return Ge.parse(n, o);\n      }\n      function If(n, o, l) {\n        return Ge.parseExpressionAt(n, o, l);\n      }\n      function Ef(n, o) {\n        return Ge.tokenizer(n, o);\n      }\n      (e.Node = Fn),\n        (e.Parser = Ge),\n        (e.Position = ct),\n        (e.SourceLocation = wt),\n        (e.TokContext = Rt),\n        (e.Token = xr),\n        (e.TokenType = x),\n        (e.defaultOptions = Pt),\n        (e.getLineInfo = $t),\n        (e.isIdentifierChar = T),\n        (e.isIdentifierStart = h),\n        (e.isNewLine = X),\n        (e.keywordTypes = U),\n        (e.lineBreak = R),\n        (e.lineBreakG = W),\n        (e.nonASCIIwhitespace = pe),\n        (e.parse = Sf),\n        (e.parseExpressionAt = If),\n        (e.tokContexts = Ue),\n        (e.tokTypes = c),\n        (e.tokenizer = Ef),\n        (e.version = Vc);\n    });\n  });\n  var df = Z((Bo, ff) => {\n    (function (e, t) {\n      typeof Bo == 'object' && typeof ff < 'u'\n        ? t(Bo, hf())\n        : typeof define == 'function' && define.amd\n        ? define(['exports', 'acorn'], t)\n        : ((e = typeof globalThis < 'u' ? globalThis : e || self),\n          t(((e.acorn = e.acorn || {}), (e.acorn.loose = {})), e.acorn));\n    })(Bo, function (e, t) {\n      'use strict';\n      var s = '\\u2716';\n      function i(p) {\n        return p.name === s;\n      }\n      function r() {}\n      var a = function (h, T) {\n        if (\n          (T === void 0 && (T = {}),\n          (this.toks = this.constructor.BaseParser.tokenizer(h, T)),\n          (this.options = this.toks.options),\n          (this.input = this.toks.input),\n          (this.tok = this.last = {type: t.tokTypes.eof, start: 0, end: 0}),\n          (this.tok.validateRegExpFlags = r),\n          (this.tok.validateRegExpPattern = r),\n          this.options.locations)\n        ) {\n          var x = this.toks.curPosition();\n          this.tok.loc = new t.SourceLocation(this.toks, x, x);\n        }\n        (this.ahead = []),\n          (this.context = []),\n          (this.curIndent = 0),\n          (this.curLineStart = 0),\n          (this.nextLineStart = this.lineEnd(this.curLineStart) + 1),\n          (this.inAsync = !1),\n          (this.inGenerator = !1),\n          (this.inFunction = !1);\n      };\n      (a.prototype.startNode = function () {\n        return new t.Node(\n          this.toks,\n          this.tok.start,\n          this.options.locations ? this.tok.loc.start : null\n        );\n      }),\n        (a.prototype.storeCurrentPos = function () {\n          return this.options.locations\n            ? [this.tok.start, this.tok.loc.start]\n            : this.tok.start;\n        }),\n        (a.prototype.startNodeAt = function (h) {\n          return this.options.locations\n            ? new t.Node(this.toks, h[0], h[1])\n            : new t.Node(this.toks, h);\n        }),\n        (a.prototype.finishNode = function (h, T) {\n          return (\n            (h.type = T),\n            (h.end = this.last.end),\n            this.options.locations && (h.loc.end = this.last.loc.end),\n            this.options.ranges && (h.range[1] = this.last.end),\n            h\n          );\n        }),\n        (a.prototype.dummyNode = function (h) {\n          var T = this.startNode();\n          return (\n            (T.type = h),\n            (T.end = T.start),\n            this.options.locations && (T.loc.end = T.loc.start),\n            this.options.ranges && (T.range[1] = T.start),\n            (this.last = {\n              type: t.tokTypes.name,\n              start: T.start,\n              end: T.start,\n              loc: T.loc,\n            }),\n            T\n          );\n        }),\n        (a.prototype.dummyIdent = function () {\n          var h = this.dummyNode('Identifier');\n          return (h.name = s), h;\n        }),\n        (a.prototype.dummyString = function () {\n          var h = this.dummyNode('Literal');\n          return (h.value = h.raw = s), h;\n        }),\n        (a.prototype.eat = function (h) {\n          return this.tok.type === h ? (this.next(), !0) : !1;\n        }),\n        (a.prototype.isContextual = function (h) {\n          return this.tok.type === t.tokTypes.name && this.tok.value === h;\n        }),\n        (a.prototype.eatContextual = function (h) {\n          return this.tok.value === h && this.eat(t.tokTypes.name);\n        }),\n        (a.prototype.canInsertSemicolon = function () {\n          return (\n            this.tok.type === t.tokTypes.eof ||\n            this.tok.type === t.tokTypes.braceR ||\n            t.lineBreak.test(this.input.slice(this.last.end, this.tok.start))\n          );\n        }),\n        (a.prototype.semicolon = function () {\n          return this.eat(t.tokTypes.semi);\n        }),\n        (a.prototype.expect = function (h) {\n          if (this.eat(h)) return !0;\n          for (var T = 1; T <= 2; T++)\n            if (this.lookAhead(T).type === h) {\n              for (var x = 0; x < T; x++) this.next();\n              return !0;\n            }\n        }),\n        (a.prototype.pushCx = function () {\n          this.context.push(this.curIndent);\n        }),\n        (a.prototype.popCx = function () {\n          this.curIndent = this.context.pop();\n        }),\n        (a.prototype.lineEnd = function (h) {\n          for (\n            ;\n            h < this.input.length && !t.isNewLine(this.input.charCodeAt(h));\n\n          )\n            ++h;\n          return h;\n        }),\n        (a.prototype.indentationAfter = function (h) {\n          for (var T = 0; ; ++h) {\n            var x = this.input.charCodeAt(h);\n            if (x === 32) ++T;\n            else if (x === 9) T += this.options.tabSize;\n            else return T;\n          }\n        }),\n        (a.prototype.closes = function (h, T, x, w) {\n          return this.tok.type === h || this.tok.type === t.tokTypes.eof\n            ? !0\n            : x !== this.curLineStart &&\n                this.curIndent < T &&\n                this.tokenStartsLine() &&\n                (!w ||\n                  this.nextLineStart >= this.input.length ||\n                  this.indentationAfter(this.nextLineStart) < T);\n        }),\n        (a.prototype.tokenStartsLine = function () {\n          for (var h = this.tok.start - 1; h >= this.curLineStart; --h) {\n            var T = this.input.charCodeAt(h);\n            if (T !== 9 && T !== 32) return !1;\n          }\n          return !0;\n        }),\n        (a.prototype.extend = function (h, T) {\n          this[h] = T(this[h]);\n        }),\n        (a.prototype.parse = function () {\n          return this.next(), this.parseTopLevel();\n        }),\n        (a.extend = function () {\n          for (var h = [], T = arguments.length; T--; ) h[T] = arguments[T];\n          for (var x = this, w = 0; w < h.length; w++) x = h[w](x);\n          return x;\n        }),\n        (a.parse = function (h, T) {\n          return new this(h, T).parse();\n        }),\n        (a.BaseParser = t.Parser);\n      var u = a.prototype;\n      function d(p) {\n        return (p < 14 && p > 8) || p === 32 || p === 160 || t.isNewLine(p);\n      }\n      (u.next = function () {\n        if (\n          ((this.last = this.tok),\n          this.ahead.length\n            ? (this.tok = this.ahead.shift())\n            : (this.tok = this.readToken()),\n          this.tok.start >= this.nextLineStart)\n        ) {\n          for (; this.tok.start >= this.nextLineStart; )\n            (this.curLineStart = this.nextLineStart),\n              (this.nextLineStart = this.lineEnd(this.curLineStart) + 1);\n          this.curIndent = this.indentationAfter(this.curLineStart);\n        }\n      }),\n        (u.readToken = function () {\n          for (;;)\n            try {\n              return (\n                this.toks.next(),\n                this.toks.type === t.tokTypes.dot &&\n                  this.input.substr(this.toks.end, 1) === '.' &&\n                  this.options.ecmaVersion >= 6 &&\n                  (this.toks.end++, (this.toks.type = t.tokTypes.ellipsis)),\n                new t.Token(this.toks)\n              );\n            } catch (S) {\n              if (!(S instanceof SyntaxError)) throw S;\n              var p = S.message,\n                h = S.raisedAt,\n                T = !0;\n              if (/unterminated/i.test(p))\n                if (((h = this.lineEnd(S.pos + 1)), /string/.test(p)))\n                  T = {\n                    start: S.pos,\n                    end: h,\n                    type: t.tokTypes.string,\n                    value: this.input.slice(S.pos + 1, h),\n                  };\n                else if (/regular expr/i.test(p)) {\n                  var x = this.input.slice(S.pos, h);\n                  try {\n                    x = new RegExp(x);\n                  } catch {}\n                  T = {start: S.pos, end: h, type: t.tokTypes.regexp, value: x};\n                } else\n                  /template/.test(p)\n                    ? (T = {\n                        start: S.pos,\n                        end: h,\n                        type: t.tokTypes.template,\n                        value: this.input.slice(S.pos, h),\n                      })\n                    : (T = !1);\n              else if (\n                /invalid (unicode|regexp|number)|expecting unicode|octal literal|is reserved|directly after number|expected number in radix|numeric separator/i.test(\n                  p\n                )\n              )\n                for (; h < this.input.length && !d(this.input.charCodeAt(h)); )\n                  ++h;\n              else if (/character escape|expected hexadecimal/i.test(p))\n                for (; h < this.input.length; ) {\n                  var w = this.input.charCodeAt(h++);\n                  if (w === 34 || w === 39 || t.isNewLine(w)) break;\n                }\n              else if (/unexpected character/i.test(p)) h++, (T = !1);\n              else if (/regular expression/i.test(p)) T = !0;\n              else throw S;\n              if (\n                (this.resetTo(h),\n                T === !0 &&\n                  (T = {start: h, end: h, type: t.tokTypes.name, value: s}),\n                T)\n              )\n                return (\n                  this.options.locations &&\n                    (T.loc = new t.SourceLocation(\n                      this.toks,\n                      t.getLineInfo(this.input, T.start),\n                      t.getLineInfo(this.input, T.end)\n                    )),\n                  T\n                );\n            }\n        }),\n        (u.resetTo = function (p) {\n          (this.toks.pos = p), (this.toks.containsEsc = !1);\n          var h = this.input.charAt(p - 1);\n          if (\n            ((this.toks.exprAllowed =\n              !h ||\n              /[[{(,;:?/*=+\\-~!|&%^<>]/.test(h) ||\n              (/[enwfd]/.test(h) &&\n                /\\b(case|else|return|throw|new|in|(instance|type)?of|delete|void)$/.test(\n                  this.input.slice(p - 10, p)\n                ))),\n            this.options.locations)\n          ) {\n            (this.toks.curLine = 1),\n              (this.toks.lineStart = t.lineBreakG.lastIndex = 0);\n            for (var T; (T = t.lineBreakG.exec(this.input)) && T.index < p; )\n              ++this.toks.curLine,\n                (this.toks.lineStart = T.index + T[0].length);\n          }\n        }),\n        (u.lookAhead = function (p) {\n          for (; p > this.ahead.length; ) this.ahead.push(this.readToken());\n          return this.ahead[p - 1];\n        });\n      var y = a.prototype;\n      (y.parseTopLevel = function () {\n        var p = this.startNodeAt(\n          this.options.locations ? [0, t.getLineInfo(this.input, 0)] : 0\n        );\n        for (p.body = []; this.tok.type !== t.tokTypes.eof; )\n          p.body.push(this.parseStatement());\n        return (\n          this.toks.adaptDirectivePrologue(p.body),\n          (this.last = this.tok),\n          (p.sourceType =\n            this.options.sourceType === 'commonjs'\n              ? 'script'\n              : this.options.sourceType),\n          this.finishNode(p, 'Program')\n        );\n      }),\n        (y.parseStatement = function () {\n          var p = this.tok.type,\n            h = this.startNode(),\n            T;\n          switch (\n            (this.toks.isLet() && ((p = t.tokTypes._var), (T = 'let')), p)\n          ) {\n            case t.tokTypes._break:\n            case t.tokTypes._continue:\n              this.next();\n              var x = p === t.tokTypes._break;\n              return (\n                this.semicolon() || this.canInsertSemicolon()\n                  ? (h.label = null)\n                  : ((h.label =\n                      this.tok.type === t.tokTypes.name\n                        ? this.parseIdent()\n                        : null),\n                    this.semicolon()),\n                this.finishNode(h, x ? 'BreakStatement' : 'ContinueStatement')\n              );\n            case t.tokTypes._debugger:\n              return (\n                this.next(),\n                this.semicolon(),\n                this.finishNode(h, 'DebuggerStatement')\n              );\n            case t.tokTypes._do:\n              return (\n                this.next(),\n                (h.body = this.parseStatement()),\n                (h.test = this.eat(t.tokTypes._while)\n                  ? this.parseParenExpression()\n                  : this.dummyIdent()),\n                this.semicolon(),\n                this.finishNode(h, 'DoWhileStatement')\n              );\n            case t.tokTypes._for:\n              this.next();\n              var w =\n                this.options.ecmaVersion >= 9 && this.eatContextual('await');\n              if (\n                (this.pushCx(),\n                this.expect(t.tokTypes.parenL),\n                this.tok.type === t.tokTypes.semi)\n              )\n                return this.parseFor(h, null);\n              var S = this.toks.isLet(),\n                A = this.toks.isAwaitUsing(!0),\n                U = !A && this.toks.isUsing(!0);\n              if (\n                S ||\n                this.tok.type === t.tokTypes._var ||\n                this.tok.type === t.tokTypes._const ||\n                U ||\n                A\n              ) {\n                var M = S\n                    ? 'let'\n                    : U\n                    ? 'using'\n                    : A\n                    ? 'await using'\n                    : this.tok.value,\n                  c = this.startNode();\n                return (\n                  U || A\n                    ? (A && this.next(), this.parseVar(c, !0, M))\n                    : (c = this.parseVar(c, !0, M)),\n                  c.declarations.length === 1 &&\n                  (this.tok.type === t.tokTypes._in || this.isContextual('of'))\n                    ? (this.options.ecmaVersion >= 9 &&\n                        this.tok.type !== t.tokTypes._in &&\n                        (h.await = w),\n                      this.parseForIn(h, c))\n                    : this.parseFor(h, c)\n                );\n              }\n              var R = this.parseExpression(!0);\n              return this.tok.type === t.tokTypes._in || this.isContextual('of')\n                ? (this.options.ecmaVersion >= 9 &&\n                    this.tok.type !== t.tokTypes._in &&\n                    (h.await = w),\n                  this.parseForIn(h, this.toAssignable(R)))\n                : this.parseFor(h, R);\n            case t.tokTypes._function:\n              return this.next(), this.parseFunction(h, !0);\n            case t.tokTypes._if:\n              return (\n                this.next(),\n                (h.test = this.parseParenExpression()),\n                (h.consequent = this.parseStatement()),\n                (h.alternate = this.eat(t.tokTypes._else)\n                  ? this.parseStatement()\n                  : null),\n                this.finishNode(h, 'IfStatement')\n              );\n            case t.tokTypes._return:\n              return (\n                this.next(),\n                this.eat(t.tokTypes.semi) || this.canInsertSemicolon()\n                  ? (h.argument = null)\n                  : ((h.argument = this.parseExpression()), this.semicolon()),\n                this.finishNode(h, 'ReturnStatement')\n              );\n            case t.tokTypes._switch:\n              var W = this.curIndent,\n                X = this.curLineStart;\n              this.next(),\n                (h.discriminant = this.parseParenExpression()),\n                (h.cases = []),\n                this.pushCx(),\n                this.expect(t.tokTypes.braceL);\n              for (var ie; !this.closes(t.tokTypes.braceR, W, X, !0); )\n                if (\n                  this.tok.type === t.tokTypes._case ||\n                  this.tok.type === t.tokTypes._default\n                ) {\n                  var pe = this.tok.type === t.tokTypes._case;\n                  ie && this.finishNode(ie, 'SwitchCase'),\n                    h.cases.push((ie = this.startNode())),\n                    (ie.consequent = []),\n                    this.next(),\n                    pe ? (ie.test = this.parseExpression()) : (ie.test = null),\n                    this.expect(t.tokTypes.colon);\n                } else\n                  ie ||\n                    (h.cases.push((ie = this.startNode())),\n                    (ie.consequent = []),\n                    (ie.test = null)),\n                    ie.consequent.push(this.parseStatement());\n              return (\n                ie && this.finishNode(ie, 'SwitchCase'),\n                this.popCx(),\n                this.eat(t.tokTypes.braceR),\n                this.finishNode(h, 'SwitchStatement')\n              );\n            case t.tokTypes._throw:\n              return (\n                this.next(),\n                (h.argument = this.parseExpression()),\n                this.semicolon(),\n                this.finishNode(h, 'ThrowStatement')\n              );\n            case t.tokTypes._try:\n              if (\n                (this.next(),\n                (h.block = this.parseBlock()),\n                (h.handler = null),\n                this.tok.type === t.tokTypes._catch)\n              ) {\n                var ae = this.startNode();\n                this.next(),\n                  this.eat(t.tokTypes.parenL)\n                    ? ((ae.param = this.toAssignable(this.parseExprAtom(), !0)),\n                      this.expect(t.tokTypes.parenR))\n                    : (ae.param = null),\n                  (ae.body = this.parseBlock()),\n                  (h.handler = this.finishNode(ae, 'CatchClause'));\n              }\n              return (\n                (h.finalizer = this.eat(t.tokTypes._finally)\n                  ? this.parseBlock()\n                  : null),\n                !h.handler && !h.finalizer\n                  ? h.block\n                  : this.finishNode(h, 'TryStatement')\n              );\n            case t.tokTypes._var:\n            case t.tokTypes._const:\n              return this.parseVar(h, !1, T || this.tok.value);\n            case t.tokTypes._while:\n              return (\n                this.next(),\n                (h.test = this.parseParenExpression()),\n                (h.body = this.parseStatement()),\n                this.finishNode(h, 'WhileStatement')\n              );\n            case t.tokTypes._with:\n              return (\n                this.next(),\n                (h.object = this.parseParenExpression()),\n                (h.body = this.parseStatement()),\n                this.finishNode(h, 'WithStatement')\n              );\n            case t.tokTypes.braceL:\n              return this.parseBlock();\n            case t.tokTypes.semi:\n              return this.next(), this.finishNode(h, 'EmptyStatement');\n            case t.tokTypes._class:\n              return this.parseClass(!0);\n            case t.tokTypes._import:\n              if (this.options.ecmaVersion > 10) {\n                var He = this.lookAhead(1).type;\n                if (He === t.tokTypes.parenL || He === t.tokTypes.dot)\n                  return (\n                    (h.expression = this.parseExpression()),\n                    this.semicolon(),\n                    this.finishNode(h, 'ExpressionStatement')\n                  );\n              }\n              return this.parseImport();\n            case t.tokTypes._export:\n              return this.parseExport();\n            default:\n              if (this.toks.isAsyncFunction())\n                return this.next(), this.next(), this.parseFunction(h, !0, !0);\n              if (this.toks.isUsing(!1)) return this.parseVar(h, !1, 'using');\n              if (this.toks.isAwaitUsing(!1))\n                return this.next(), this.parseVar(h, !1, 'await using');\n              var qe = this.parseExpression();\n              return i(qe)\n                ? (this.next(),\n                  this.tok.type === t.tokTypes.eof\n                    ? this.finishNode(h, 'EmptyStatement')\n                    : this.parseStatement())\n                : p === t.tokTypes.name &&\n                  qe.type === 'Identifier' &&\n                  this.eat(t.tokTypes.colon)\n                ? ((h.body = this.parseStatement()),\n                  (h.label = qe),\n                  this.finishNode(h, 'LabeledStatement'))\n                : ((h.expression = qe),\n                  this.semicolon(),\n                  this.finishNode(h, 'ExpressionStatement'));\n          }\n        }),\n        (y.parseBlock = function () {\n          var p = this.startNode();\n          this.pushCx(), this.expect(t.tokTypes.braceL);\n          var h = this.curIndent,\n            T = this.curLineStart;\n          for (p.body = []; !this.closes(t.tokTypes.braceR, h, T, !0); )\n            p.body.push(this.parseStatement());\n          return (\n            this.popCx(),\n            this.eat(t.tokTypes.braceR),\n            this.finishNode(p, 'BlockStatement')\n          );\n        }),\n        (y.parseFor = function (p, h) {\n          return (\n            (p.init = h),\n            (p.test = p.update = null),\n            this.eat(t.tokTypes.semi) &&\n              this.tok.type !== t.tokTypes.semi &&\n              (p.test = this.parseExpression()),\n            this.eat(t.tokTypes.semi) &&\n              this.tok.type !== t.tokTypes.parenR &&\n              (p.update = this.parseExpression()),\n            this.popCx(),\n            this.expect(t.tokTypes.parenR),\n            (p.body = this.parseStatement()),\n            this.finishNode(p, 'ForStatement')\n          );\n        }),\n        (y.parseForIn = function (p, h) {\n          var T =\n            this.tok.type === t.tokTypes._in\n              ? 'ForInStatement'\n              : 'ForOfStatement';\n          return (\n            this.next(),\n            (p.left = h),\n            (p.right = this.parseExpression()),\n            this.popCx(),\n            this.expect(t.tokTypes.parenR),\n            (p.body = this.parseStatement()),\n            this.finishNode(p, T)\n          );\n        }),\n        (y.parseVar = function (p, h, T) {\n          (p.kind = T), this.next(), (p.declarations = []);\n          do {\n            var x = this.startNode();\n            (x.id =\n              this.options.ecmaVersion >= 6\n                ? this.toAssignable(this.parseExprAtom(), !0)\n                : this.parseIdent()),\n              (x.init = this.eat(t.tokTypes.eq)\n                ? this.parseMaybeAssign(h)\n                : null),\n              p.declarations.push(this.finishNode(x, 'VariableDeclarator'));\n          } while (this.eat(t.tokTypes.comma));\n          if (!p.declarations.length) {\n            var w = this.startNode();\n            (w.id = this.dummyIdent()),\n              p.declarations.push(this.finishNode(w, 'VariableDeclarator'));\n          }\n          return (\n            h || this.semicolon(), this.finishNode(p, 'VariableDeclaration')\n          );\n        }),\n        (y.parseClass = function (p) {\n          var h = this.startNode();\n          this.next(),\n            this.tok.type === t.tokTypes.name\n              ? (h.id = this.parseIdent())\n              : p === !0\n              ? (h.id = this.dummyIdent())\n              : (h.id = null),\n            (h.superClass = this.eat(t.tokTypes._extends)\n              ? this.parseExpression()\n              : null),\n            (h.body = this.startNode()),\n            (h.body.body = []),\n            this.pushCx();\n          var T = this.curIndent + 1,\n            x = this.curLineStart;\n          for (\n            this.eat(t.tokTypes.braceL),\n              this.curIndent + 1 < T &&\n                ((T = this.curIndent), (x = this.curLineStart));\n            !this.closes(t.tokTypes.braceR, T, x);\n\n          ) {\n            var w = this.parseClassElement();\n            w && h.body.body.push(w);\n          }\n          return (\n            this.popCx(),\n            this.eat(t.tokTypes.braceR) ||\n              ((this.last.end = this.tok.start),\n              this.options.locations &&\n                (this.last.loc.end = this.tok.loc.start)),\n            this.semicolon(),\n            this.finishNode(h.body, 'ClassBody'),\n            this.finishNode(h, p ? 'ClassDeclaration' : 'ClassExpression')\n          );\n        }),\n        (y.parseClassElement = function () {\n          if (this.eat(t.tokTypes.semi)) return null;\n          var p = this.options,\n            h = p.ecmaVersion,\n            T = p.locations,\n            x = this.curIndent,\n            w = this.curLineStart,\n            S = this.startNode(),\n            A = '',\n            U = !1,\n            M = !1,\n            c = 'method',\n            R = !1;\n          if (this.eatContextual('static')) {\n            if (h >= 13 && this.eat(t.tokTypes.braceL))\n              return this.parseClassStaticBlock(S), S;\n            this.isClassElementNameStart() || this.toks.type === t.tokTypes.star\n              ? (R = !0)\n              : (A = 'static');\n          }\n          if (\n            ((S.static = R),\n            !A &&\n              h >= 8 &&\n              this.eatContextual('async') &&\n              ((this.isClassElementNameStart() ||\n                this.toks.type === t.tokTypes.star) &&\n              !this.canInsertSemicolon()\n                ? (M = !0)\n                : (A = 'async')),\n            !A)\n          ) {\n            U = this.eat(t.tokTypes.star);\n            var W = this.toks.value;\n            (this.eatContextual('get') || this.eatContextual('set')) &&\n              (this.isClassElementNameStart() ? (c = W) : (A = W));\n          }\n          if (A)\n            (S.computed = !1),\n              (S.key = this.startNodeAt(\n                T\n                  ? [this.toks.lastTokStart, this.toks.lastTokStartLoc]\n                  : this.toks.lastTokStart\n              )),\n              (S.key.name = A),\n              this.finishNode(S.key, 'Identifier');\n          else if ((this.parseClassElementName(S), i(S.key)))\n            return (\n              i(this.parseMaybeAssign()) && this.next(),\n              this.eat(t.tokTypes.comma),\n              null\n            );\n          if (\n            h < 13 ||\n            this.toks.type === t.tokTypes.parenL ||\n            c !== 'method' ||\n            U ||\n            M\n          ) {\n            var X =\n              !S.computed &&\n              !S.static &&\n              !U &&\n              !M &&\n              c === 'method' &&\n              ((S.key.type === 'Identifier' && S.key.name === 'constructor') ||\n                (S.key.type === 'Literal' && S.key.value === 'constructor'));\n            (S.kind = X ? 'constructor' : c),\n              (S.value = this.parseMethod(U, M)),\n              this.finishNode(S, 'MethodDefinition');\n          } else {\n            if (this.eat(t.tokTypes.eq))\n              if (\n                this.curLineStart !== w &&\n                this.curIndent <= x &&\n                this.tokenStartsLine()\n              )\n                S.value = null;\n              else {\n                var ie = this.inAsync,\n                  pe = this.inGenerator;\n                (this.inAsync = !1),\n                  (this.inGenerator = !1),\n                  (S.value = this.parseMaybeAssign()),\n                  (this.inAsync = ie),\n                  (this.inGenerator = pe);\n              }\n            else S.value = null;\n            this.semicolon(), this.finishNode(S, 'PropertyDefinition');\n          }\n          return S;\n        }),\n        (y.parseClassStaticBlock = function (p) {\n          var h = this.curIndent,\n            T = this.curLineStart;\n          for (\n            p.body = [], this.pushCx();\n            !this.closes(t.tokTypes.braceR, h, T, !0);\n\n          )\n            p.body.push(this.parseStatement());\n          return (\n            this.popCx(),\n            this.eat(t.tokTypes.braceR),\n            this.finishNode(p, 'StaticBlock')\n          );\n        }),\n        (y.isClassElementNameStart = function () {\n          return this.toks.isClassElementNameStart();\n        }),\n        (y.parseClassElementName = function (p) {\n          this.toks.type === t.tokTypes.privateId\n            ? ((p.computed = !1), (p.key = this.parsePrivateIdent()))\n            : this.parsePropertyName(p);\n        }),\n        (y.parseFunction = function (p, h, T) {\n          var x = this.inAsync,\n            w = this.inGenerator,\n            S = this.inFunction;\n          return (\n            this.initFunction(p),\n            this.options.ecmaVersion >= 6 &&\n              (p.generator = this.eat(t.tokTypes.star)),\n            this.options.ecmaVersion >= 8 && (p.async = !!T),\n            this.tok.type === t.tokTypes.name\n              ? (p.id = this.parseIdent())\n              : h === !0 && (p.id = this.dummyIdent()),\n            (this.inAsync = p.async),\n            (this.inGenerator = p.generator),\n            (this.inFunction = !0),\n            (p.params = this.parseFunctionParams()),\n            (p.body = this.parseBlock()),\n            this.toks.adaptDirectivePrologue(p.body.body),\n            (this.inAsync = x),\n            (this.inGenerator = w),\n            (this.inFunction = S),\n            this.finishNode(p, h ? 'FunctionDeclaration' : 'FunctionExpression')\n          );\n        }),\n        (y.parseExport = function () {\n          var p = this.startNode();\n          if ((this.next(), this.eat(t.tokTypes.star)))\n            return (\n              this.options.ecmaVersion >= 11 &&\n                (this.eatContextual('as')\n                  ? (p.exported = this.parseExprAtom())\n                  : (p.exported = null)),\n              (p.source = this.eatContextual('from')\n                ? this.parseExprAtom()\n                : this.dummyString()),\n              this.options.ecmaVersion >= 16 &&\n                (p.attributes = this.parseWithClause()),\n              this.semicolon(),\n              this.finishNode(p, 'ExportAllDeclaration')\n            );\n          if (this.eat(t.tokTypes._default)) {\n            var h;\n            if (\n              this.tok.type === t.tokTypes._function ||\n              (h = this.toks.isAsyncFunction())\n            ) {\n              var T = this.startNode();\n              this.next(),\n                h && this.next(),\n                (p.declaration = this.parseFunction(T, 'nullableID', h));\n            } else\n              this.tok.type === t.tokTypes._class\n                ? (p.declaration = this.parseClass('nullableID'))\n                : ((p.declaration = this.parseMaybeAssign()), this.semicolon());\n            return this.finishNode(p, 'ExportDefaultDeclaration');\n          }\n          return (\n            this.tok.type.keyword ||\n            this.toks.isLet() ||\n            this.toks.isAsyncFunction()\n              ? ((p.declaration = this.parseStatement()),\n                (p.specifiers = []),\n                (p.source = null))\n              : ((p.declaration = null),\n                (p.specifiers = this.parseExportSpecifierList()),\n                (p.source = this.eatContextual('from')\n                  ? this.parseExprAtom()\n                  : null),\n                this.options.ecmaVersion >= 16 &&\n                  (p.attributes = this.parseWithClause()),\n                this.semicolon()),\n            this.finishNode(p, 'ExportNamedDeclaration')\n          );\n        }),\n        (y.parseImport = function () {\n          var p = this.startNode();\n          if ((this.next(), this.tok.type === t.tokTypes.string))\n            (p.specifiers = []), (p.source = this.parseExprAtom());\n          else {\n            var h;\n            this.tok.type === t.tokTypes.name &&\n              this.tok.value !== 'from' &&\n              ((h = this.startNode()),\n              (h.local = this.parseIdent()),\n              this.finishNode(h, 'ImportDefaultSpecifier'),\n              this.eat(t.tokTypes.comma)),\n              (p.specifiers = this.parseImportSpecifiers()),\n              (p.source =\n                this.eatContextual('from') &&\n                this.tok.type === t.tokTypes.string\n                  ? this.parseExprAtom()\n                  : this.dummyString()),\n              h && p.specifiers.unshift(h);\n          }\n          return (\n            this.options.ecmaVersion >= 16 &&\n              (p.attributes = this.parseWithClause()),\n            this.semicolon(),\n            this.finishNode(p, 'ImportDeclaration')\n          );\n        }),\n        (y.parseImportSpecifiers = function () {\n          var p = [];\n          if (this.tok.type === t.tokTypes.star) {\n            var h = this.startNode();\n            this.next(),\n              (h.local = this.eatContextual('as')\n                ? this.parseIdent()\n                : this.dummyIdent()),\n              p.push(this.finishNode(h, 'ImportNamespaceSpecifier'));\n          } else {\n            var T = this.curIndent,\n              x = this.curLineStart,\n              w = this.nextLineStart;\n            for (\n              this.pushCx(),\n                this.eat(t.tokTypes.braceL),\n                this.curLineStart > w && (w = this.curLineStart);\n              !this.closes(\n                t.tokTypes.braceR,\n                T + (this.curLineStart <= w ? 1 : 0),\n                x\n              );\n\n            ) {\n              var S = this.startNode();\n              if (this.eat(t.tokTypes.star))\n                (S.local = this.eatContextual('as')\n                  ? this.parseModuleExportName()\n                  : this.dummyIdent()),\n                  this.finishNode(S, 'ImportNamespaceSpecifier');\n              else {\n                if (\n                  this.isContextual('from') ||\n                  ((S.imported = this.parseModuleExportName()), i(S.imported))\n                )\n                  break;\n                (S.local = this.eatContextual('as')\n                  ? this.parseModuleExportName()\n                  : S.imported),\n                  this.finishNode(S, 'ImportSpecifier');\n              }\n              p.push(S), this.eat(t.tokTypes.comma);\n            }\n            this.eat(t.tokTypes.braceR), this.popCx();\n          }\n          return p;\n        }),\n        (y.parseWithClause = function () {\n          var p = [];\n          if (!this.eat(t.tokTypes._with)) return p;\n          var h = this.curIndent,\n            T = this.curLineStart,\n            x = this.nextLineStart;\n          for (\n            this.pushCx(),\n              this.eat(t.tokTypes.braceL),\n              this.curLineStart > x && (x = this.curLineStart);\n            !this.closes(\n              t.tokTypes.braceR,\n              h + (this.curLineStart <= x ? 1 : 0),\n              T\n            );\n\n          ) {\n            var w = this.startNode();\n            if (\n              ((w.key =\n                this.tok.type === t.tokTypes.string\n                  ? this.parseExprAtom()\n                  : this.parseIdent()),\n              this.eat(t.tokTypes.colon))\n            )\n              this.tok.type === t.tokTypes.string\n                ? (w.value = this.parseExprAtom())\n                : (w.value = this.dummyString());\n            else {\n              if (i(w.key)) break;\n              if (this.tok.type === t.tokTypes.string)\n                w.value = this.parseExprAtom();\n              else break;\n            }\n            p.push(this.finishNode(w, 'ImportAttribute')),\n              this.eat(t.tokTypes.comma);\n          }\n          return this.eat(t.tokTypes.braceR), this.popCx(), p;\n        }),\n        (y.parseExportSpecifierList = function () {\n          var p = [],\n            h = this.curIndent,\n            T = this.curLineStart,\n            x = this.nextLineStart;\n          for (\n            this.pushCx(),\n              this.eat(t.tokTypes.braceL),\n              this.curLineStart > x && (x = this.curLineStart);\n            !this.closes(\n              t.tokTypes.braceR,\n              h + (this.curLineStart <= x ? 1 : 0),\n              T\n            ) && !this.isContextual('from');\n\n          ) {\n            var w = this.startNode();\n            if (((w.local = this.parseModuleExportName()), i(w.local))) break;\n            (w.exported = this.eatContextual('as')\n              ? this.parseModuleExportName()\n              : w.local),\n              this.finishNode(w, 'ExportSpecifier'),\n              p.push(w),\n              this.eat(t.tokTypes.comma);\n          }\n          return this.eat(t.tokTypes.braceR), this.popCx(), p;\n        }),\n        (y.parseModuleExportName = function () {\n          return this.options.ecmaVersion >= 13 &&\n            this.tok.type === t.tokTypes.string\n            ? this.parseExprAtom()\n            : this.parseIdent();\n        });\n      var g = a.prototype;\n      (g.checkLVal = function (p) {\n        if (!p) return p;\n        switch (p.type) {\n          case 'Identifier':\n          case 'MemberExpression':\n            return p;\n          case 'ParenthesizedExpression':\n            return (p.expression = this.checkLVal(p.expression)), p;\n          default:\n            return this.dummyIdent();\n        }\n      }),\n        (g.parseExpression = function (p) {\n          var h = this.storeCurrentPos(),\n            T = this.parseMaybeAssign(p);\n          if (this.tok.type === t.tokTypes.comma) {\n            var x = this.startNodeAt(h);\n            for (x.expressions = [T]; this.eat(t.tokTypes.comma); )\n              x.expressions.push(this.parseMaybeAssign(p));\n            return this.finishNode(x, 'SequenceExpression');\n          }\n          return T;\n        }),\n        (g.parseParenExpression = function () {\n          this.pushCx(), this.expect(t.tokTypes.parenL);\n          var p = this.parseExpression();\n          return this.popCx(), this.expect(t.tokTypes.parenR), p;\n        }),\n        (g.parseMaybeAssign = function (p) {\n          if (this.inGenerator && this.toks.isContextual('yield')) {\n            var h = this.startNode();\n            return (\n              this.next(),\n              this.semicolon() ||\n              this.canInsertSemicolon() ||\n              (this.tok.type !== t.tokTypes.star && !this.tok.type.startsExpr)\n                ? ((h.delegate = !1), (h.argument = null))\n                : ((h.delegate = this.eat(t.tokTypes.star)),\n                  (h.argument = this.parseMaybeAssign())),\n              this.finishNode(h, 'YieldExpression')\n            );\n          }\n          var T = this.storeCurrentPos(),\n            x = this.parseMaybeConditional(p);\n          if (this.tok.type.isAssign) {\n            var w = this.startNodeAt(T);\n            return (\n              (w.operator = this.tok.value),\n              (w.left =\n                this.tok.type === t.tokTypes.eq\n                  ? this.toAssignable(x)\n                  : this.checkLVal(x)),\n              this.next(),\n              (w.right = this.parseMaybeAssign(p)),\n              this.finishNode(w, 'AssignmentExpression')\n            );\n          }\n          return x;\n        }),\n        (g.parseMaybeConditional = function (p) {\n          var h = this.storeCurrentPos(),\n            T = this.parseExprOps(p);\n          if (this.eat(t.tokTypes.question)) {\n            var x = this.startNodeAt(h);\n            return (\n              (x.test = T),\n              (x.consequent = this.parseMaybeAssign()),\n              (x.alternate = this.expect(t.tokTypes.colon)\n                ? this.parseMaybeAssign(p)\n                : this.dummyIdent()),\n              this.finishNode(x, 'ConditionalExpression')\n            );\n          }\n          return T;\n        }),\n        (g.parseExprOps = function (p) {\n          var h = this.storeCurrentPos(),\n            T = this.curIndent,\n            x = this.curLineStart;\n          return this.parseExprOp(this.parseMaybeUnary(!1), h, -1, p, T, x);\n        }),\n        (g.parseExprOp = function (p, h, T, x, w, S) {\n          if (\n            this.curLineStart !== S &&\n            this.curIndent < w &&\n            this.tokenStartsLine()\n          )\n            return p;\n          var A = this.tok.type.binop;\n          if (A != null && (!x || this.tok.type !== t.tokTypes._in) && A > T) {\n            var U = this.startNodeAt(h);\n            if (\n              ((U.left = p),\n              (U.operator = this.tok.value),\n              this.next(),\n              this.curLineStart !== S &&\n                this.curIndent < w &&\n                this.tokenStartsLine())\n            )\n              U.right = this.dummyIdent();\n            else {\n              var M = this.storeCurrentPos();\n              U.right = this.parseExprOp(\n                this.parseMaybeUnary(!1),\n                M,\n                A,\n                x,\n                w,\n                S\n              );\n            }\n            return (\n              this.finishNode(\n                U,\n                /&&|\\|\\||\\?\\?/.test(U.operator)\n                  ? 'LogicalExpression'\n                  : 'BinaryExpression'\n              ),\n              this.parseExprOp(U, h, T, x, w, S)\n            );\n          }\n          return p;\n        }),\n        (g.parseMaybeUnary = function (p) {\n          var h = this.storeCurrentPos(),\n            T;\n          if (\n            this.options.ecmaVersion >= 8 &&\n            this.toks.isContextual('await') &&\n            (this.inAsync ||\n              (this.toks.inModule && this.options.ecmaVersion >= 13) ||\n              (!this.inFunction && this.options.allowAwaitOutsideFunction))\n          )\n            (T = this.parseAwait()), (p = !0);\n          else if (this.tok.type.prefix) {\n            var x = this.startNode(),\n              w = this.tok.type === t.tokTypes.incDec;\n            w || (p = !0),\n              (x.operator = this.tok.value),\n              (x.prefix = !0),\n              this.next(),\n              (x.argument = this.parseMaybeUnary(!0)),\n              w && (x.argument = this.checkLVal(x.argument)),\n              (T = this.finishNode(\n                x,\n                w ? 'UpdateExpression' : 'UnaryExpression'\n              ));\n          } else if (this.tok.type === t.tokTypes.ellipsis) {\n            var S = this.startNode();\n            this.next(),\n              (S.argument = this.parseMaybeUnary(p)),\n              (T = this.finishNode(S, 'SpreadElement'));\n          } else if (!p && this.tok.type === t.tokTypes.privateId)\n            T = this.parsePrivateIdent();\n          else\n            for (\n              T = this.parseExprSubscripts();\n              this.tok.type.postfix && !this.canInsertSemicolon();\n\n            ) {\n              var A = this.startNodeAt(h);\n              (A.operator = this.tok.value),\n                (A.prefix = !1),\n                (A.argument = this.checkLVal(T)),\n                this.next(),\n                (T = this.finishNode(A, 'UpdateExpression'));\n            }\n          if (!p && this.eat(t.tokTypes.starstar)) {\n            var U = this.startNodeAt(h);\n            return (\n              (U.operator = '**'),\n              (U.left = T),\n              (U.right = this.parseMaybeUnary(!1)),\n              this.finishNode(U, 'BinaryExpression')\n            );\n          }\n          return T;\n        }),\n        (g.parseExprSubscripts = function () {\n          var p = this.storeCurrentPos();\n          return this.parseSubscripts(\n            this.parseExprAtom(),\n            p,\n            !1,\n            this.curIndent,\n            this.curLineStart\n          );\n        }),\n        (g.parseSubscripts = function (p, h, T, x, w) {\n          for (var S = this.options.ecmaVersion >= 11, A = !1; ; ) {\n            if (\n              this.curLineStart !== w &&\n              this.curIndent <= x &&\n              this.tokenStartsLine()\n            )\n              if (this.tok.type === t.tokTypes.dot && this.curIndent === x) --x;\n              else break;\n            var U =\n                p.type === 'Identifier' &&\n                p.name === 'async' &&\n                !this.canInsertSemicolon(),\n              M = S && this.eat(t.tokTypes.questionDot);\n            if (\n              (M && (A = !0),\n              (M &&\n                this.tok.type !== t.tokTypes.parenL &&\n                this.tok.type !== t.tokTypes.bracketL &&\n                this.tok.type !== t.tokTypes.backQuote) ||\n                this.eat(t.tokTypes.dot))\n            ) {\n              var c = this.startNodeAt(h);\n              (c.object = p),\n                this.curLineStart !== w &&\n                this.curIndent <= x &&\n                this.tokenStartsLine()\n                  ? (c.property = this.dummyIdent())\n                  : (c.property =\n                      this.parsePropertyAccessor() || this.dummyIdent()),\n                (c.computed = !1),\n                S && (c.optional = M),\n                (p = this.finishNode(c, 'MemberExpression'));\n            } else if (this.tok.type === t.tokTypes.bracketL) {\n              this.pushCx(), this.next();\n              var R = this.startNodeAt(h);\n              (R.object = p),\n                (R.property = this.parseExpression()),\n                (R.computed = !0),\n                S && (R.optional = M),\n                this.popCx(),\n                this.expect(t.tokTypes.bracketR),\n                (p = this.finishNode(R, 'MemberExpression'));\n            } else if (!T && this.tok.type === t.tokTypes.parenL) {\n              var W = this.parseExprList(t.tokTypes.parenR);\n              if (U && this.eat(t.tokTypes.arrow))\n                return this.parseArrowExpression(this.startNodeAt(h), W, !0);\n              var X = this.startNodeAt(h);\n              (X.callee = p),\n                (X.arguments = W),\n                S && (X.optional = M),\n                (p = this.finishNode(X, 'CallExpression'));\n            } else if (this.tok.type === t.tokTypes.backQuote) {\n              var ie = this.startNodeAt(h);\n              (ie.tag = p),\n                (ie.quasi = this.parseTemplate()),\n                (p = this.finishNode(ie, 'TaggedTemplateExpression'));\n            } else break;\n          }\n          if (A) {\n            var pe = this.startNodeAt(h);\n            (pe.expression = p), (p = this.finishNode(pe, 'ChainExpression'));\n          }\n          return p;\n        }),\n        (g.parseExprAtom = function () {\n          var p;\n          switch (this.tok.type) {\n            case t.tokTypes._this:\n            case t.tokTypes._super:\n              var h =\n                this.tok.type === t.tokTypes._this ? 'ThisExpression' : 'Super';\n              return (p = this.startNode()), this.next(), this.finishNode(p, h);\n            case t.tokTypes.name:\n              var T = this.storeCurrentPos(),\n                x = this.parseIdent(),\n                w = !1;\n              if (x.name === 'async' && !this.canInsertSemicolon()) {\n                if (this.eat(t.tokTypes._function))\n                  return (\n                    this.toks.overrideContext(t.tokContexts.f_expr),\n                    this.parseFunction(this.startNodeAt(T), !1, !0)\n                  );\n                this.tok.type === t.tokTypes.name &&\n                  ((x = this.parseIdent()), (w = !0));\n              }\n              return this.eat(t.tokTypes.arrow)\n                ? this.parseArrowExpression(this.startNodeAt(T), [x], w)\n                : x;\n            case t.tokTypes.regexp:\n              p = this.startNode();\n              var S = this.tok.value;\n              return (\n                (p.regex = {pattern: S.pattern, flags: S.flags}),\n                (p.value = S.value),\n                (p.raw = this.input.slice(this.tok.start, this.tok.end)),\n                this.next(),\n                this.finishNode(p, 'Literal')\n              );\n            case t.tokTypes.num:\n            case t.tokTypes.string:\n              return (\n                (p = this.startNode()),\n                (p.value = this.tok.value),\n                (p.raw = this.input.slice(this.tok.start, this.tok.end)),\n                this.tok.type === t.tokTypes.num &&\n                  p.raw.charCodeAt(p.raw.length - 1) === 110 &&\n                  (p.bigint =\n                    p.value != null\n                      ? p.value.toString()\n                      : p.raw.slice(0, -1).replace(/_/g, '')),\n                this.next(),\n                this.finishNode(p, 'Literal')\n              );\n            case t.tokTypes._null:\n            case t.tokTypes._true:\n            case t.tokTypes._false:\n              return (\n                (p = this.startNode()),\n                (p.value =\n                  this.tok.type === t.tokTypes._null\n                    ? null\n                    : this.tok.type === t.tokTypes._true),\n                (p.raw = this.tok.type.keyword),\n                this.next(),\n                this.finishNode(p, 'Literal')\n              );\n            case t.tokTypes.parenL:\n              var A = this.storeCurrentPos();\n              this.next();\n              var U = this.parseExpression();\n              if (\n                (this.expect(t.tokTypes.parenR), this.eat(t.tokTypes.arrow))\n              ) {\n                var M = U.expressions || [U];\n                return (\n                  M.length && i(M[M.length - 1]) && M.pop(),\n                  this.parseArrowExpression(this.startNodeAt(A), M)\n                );\n              }\n              if (this.options.preserveParens) {\n                var c = this.startNodeAt(A);\n                (c.expression = U),\n                  (U = this.finishNode(c, 'ParenthesizedExpression'));\n              }\n              return U;\n            case t.tokTypes.bracketL:\n              return (\n                (p = this.startNode()),\n                (p.elements = this.parseExprList(t.tokTypes.bracketR, !0)),\n                this.finishNode(p, 'ArrayExpression')\n              );\n            case t.tokTypes.braceL:\n              return (\n                this.toks.overrideContext(t.tokContexts.b_expr), this.parseObj()\n              );\n            case t.tokTypes._class:\n              return this.parseClass(!1);\n            case t.tokTypes._function:\n              return (\n                (p = this.startNode()), this.next(), this.parseFunction(p, !1)\n              );\n            case t.tokTypes._new:\n              return this.parseNew();\n            case t.tokTypes.backQuote:\n              return this.parseTemplate();\n            case t.tokTypes._import:\n              return this.options.ecmaVersion >= 11\n                ? this.parseExprImport()\n                : this.dummyIdent();\n            default:\n              return this.dummyIdent();\n          }\n        }),\n        (g.parseExprImport = function () {\n          var p = this.startNode(),\n            h = this.parseIdent(!0);\n          switch (this.tok.type) {\n            case t.tokTypes.parenL:\n              return this.parseDynamicImport(p);\n            case t.tokTypes.dot:\n              return (p.meta = h), this.parseImportMeta(p);\n            default:\n              return (p.name = 'import'), this.finishNode(p, 'Identifier');\n          }\n        }),\n        (g.parseDynamicImport = function (p) {\n          var h = this.parseExprList(t.tokTypes.parenR);\n          return (\n            (p.source = h[0] || this.dummyString()),\n            (p.options = h[1] || null),\n            this.finishNode(p, 'ImportExpression')\n          );\n        }),\n        (g.parseImportMeta = function (p) {\n          return (\n            this.next(),\n            (p.property = this.parseIdent(!0)),\n            this.finishNode(p, 'MetaProperty')\n          );\n        }),\n        (g.parseNew = function () {\n          var p = this.startNode(),\n            h = this.curIndent,\n            T = this.curLineStart,\n            x = this.parseIdent(!0);\n          if (this.options.ecmaVersion >= 6 && this.eat(t.tokTypes.dot))\n            return (\n              (p.meta = x),\n              (p.property = this.parseIdent(!0)),\n              this.finishNode(p, 'MetaProperty')\n            );\n          var w = this.storeCurrentPos();\n          return (\n            (p.callee = this.parseSubscripts(\n              this.parseExprAtom(),\n              w,\n              !0,\n              h,\n              T\n            )),\n            this.tok.type === t.tokTypes.parenL\n              ? (p.arguments = this.parseExprList(t.tokTypes.parenR))\n              : (p.arguments = []),\n            this.finishNode(p, 'NewExpression')\n          );\n        }),\n        (g.parseTemplateElement = function () {\n          var p = this.startNode();\n          return (\n            this.tok.type === t.tokTypes.invalidTemplate\n              ? (p.value = {raw: this.tok.value, cooked: null})\n              : (p.value = {\n                  raw: this.input.slice(this.tok.start, this.tok.end).replace(\n                    /\\r\\n?/g,\n                    `\n`\n                  ),\n                  cooked: this.tok.value,\n                }),\n            this.next(),\n            (p.tail = this.tok.type === t.tokTypes.backQuote),\n            this.finishNode(p, 'TemplateElement')\n          );\n        }),\n        (g.parseTemplate = function () {\n          var p = this.startNode();\n          this.next(), (p.expressions = []);\n          var h = this.parseTemplateElement();\n          for (p.quasis = [h]; !h.tail; )\n            this.next(),\n              p.expressions.push(this.parseExpression()),\n              this.expect(t.tokTypes.braceR)\n                ? (h = this.parseTemplateElement())\n                : ((h = this.startNode()),\n                  (h.value = {cooked: '', raw: ''}),\n                  (h.tail = !0),\n                  this.finishNode(h, 'TemplateElement')),\n              p.quasis.push(h);\n          return (\n            this.expect(t.tokTypes.backQuote),\n            this.finishNode(p, 'TemplateLiteral')\n          );\n        }),\n        (g.parseObj = function () {\n          var p = this.startNode();\n          (p.properties = []), this.pushCx();\n          var h = this.curIndent + 1,\n            T = this.curLineStart;\n          for (\n            this.eat(t.tokTypes.braceL),\n              this.curIndent + 1 < h &&\n                ((h = this.curIndent), (T = this.curLineStart));\n            !this.closes(t.tokTypes.braceR, h, T);\n\n          ) {\n            var x = this.startNode(),\n              w = void 0,\n              S = void 0,\n              A = void 0;\n            if (\n              this.options.ecmaVersion >= 9 &&\n              this.eat(t.tokTypes.ellipsis)\n            ) {\n              (x.argument = this.parseMaybeAssign()),\n                p.properties.push(this.finishNode(x, 'SpreadElement')),\n                this.eat(t.tokTypes.comma);\n              continue;\n            }\n            if (\n              (this.options.ecmaVersion >= 6 &&\n                ((A = this.storeCurrentPos()),\n                (x.method = !1),\n                (x.shorthand = !1),\n                (w = this.eat(t.tokTypes.star))),\n              this.parsePropertyName(x),\n              this.toks.isAsyncProp(x)\n                ? ((S = !0),\n                  (w =\n                    this.options.ecmaVersion >= 9 && this.eat(t.tokTypes.star)),\n                  this.parsePropertyName(x))\n                : (S = !1),\n              i(x.key))\n            ) {\n              i(this.parseMaybeAssign()) && this.next(),\n                this.eat(t.tokTypes.comma);\n              continue;\n            }\n            if (this.eat(t.tokTypes.colon))\n              (x.kind = 'init'), (x.value = this.parseMaybeAssign());\n            else if (\n              this.options.ecmaVersion >= 6 &&\n              (this.tok.type === t.tokTypes.parenL ||\n                this.tok.type === t.tokTypes.braceL)\n            )\n              (x.kind = 'init'),\n                (x.method = !0),\n                (x.value = this.parseMethod(w, S));\n            else if (\n              this.options.ecmaVersion >= 5 &&\n              x.key.type === 'Identifier' &&\n              !x.computed &&\n              (x.key.name === 'get' || x.key.name === 'set') &&\n              this.tok.type !== t.tokTypes.comma &&\n              this.tok.type !== t.tokTypes.braceR &&\n              this.tok.type !== t.tokTypes.eq\n            )\n              (x.kind = x.key.name),\n                this.parsePropertyName(x),\n                (x.value = this.parseMethod(!1));\n            else {\n              if (((x.kind = 'init'), this.options.ecmaVersion >= 6))\n                if (this.eat(t.tokTypes.eq)) {\n                  var U = this.startNodeAt(A);\n                  (U.operator = '='),\n                    (U.left = x.key),\n                    (U.right = this.parseMaybeAssign()),\n                    (x.value = this.finishNode(U, 'AssignmentExpression'));\n                } else x.value = x.key;\n              else x.value = this.dummyIdent();\n              x.shorthand = !0;\n            }\n            p.properties.push(this.finishNode(x, 'Property')),\n              this.eat(t.tokTypes.comma);\n          }\n          return (\n            this.popCx(),\n            this.eat(t.tokTypes.braceR) ||\n              ((this.last.end = this.tok.start),\n              this.options.locations &&\n                (this.last.loc.end = this.tok.loc.start)),\n            this.finishNode(p, 'ObjectExpression')\n          );\n        }),\n        (g.parsePropertyName = function (p) {\n          if (this.options.ecmaVersion >= 6)\n            if (this.eat(t.tokTypes.bracketL)) {\n              (p.computed = !0),\n                (p.key = this.parseExpression()),\n                this.expect(t.tokTypes.bracketR);\n              return;\n            } else p.computed = !1;\n          var h =\n            this.tok.type === t.tokTypes.num ||\n            this.tok.type === t.tokTypes.string\n              ? this.parseExprAtom()\n              : this.parseIdent();\n          p.key = h || this.dummyIdent();\n        }),\n        (g.parsePropertyAccessor = function () {\n          if (this.tok.type === t.tokTypes.name || this.tok.type.keyword)\n            return this.parseIdent();\n          if (this.tok.type === t.tokTypes.privateId)\n            return this.parsePrivateIdent();\n        }),\n        (g.parseIdent = function () {\n          var p =\n            this.tok.type === t.tokTypes.name\n              ? this.tok.value\n              : this.tok.type.keyword;\n          if (!p) return this.dummyIdent();\n          this.tok.type.keyword && (this.toks.type = t.tokTypes.name);\n          var h = this.startNode();\n          return this.next(), (h.name = p), this.finishNode(h, 'Identifier');\n        }),\n        (g.parsePrivateIdent = function () {\n          var p = this.startNode();\n          return (\n            (p.name = this.tok.value),\n            this.next(),\n            this.finishNode(p, 'PrivateIdentifier')\n          );\n        }),\n        (g.initFunction = function (p) {\n          (p.id = null),\n            (p.params = []),\n            this.options.ecmaVersion >= 6 &&\n              ((p.generator = !1), (p.expression = !1)),\n            this.options.ecmaVersion >= 8 && (p.async = !1);\n        }),\n        (g.toAssignable = function (p, h) {\n          if (\n            !(\n              !p ||\n              p.type === 'Identifier' ||\n              (p.type === 'MemberExpression' && !h)\n            )\n          )\n            if (p.type === 'ParenthesizedExpression')\n              this.toAssignable(p.expression, h);\n            else {\n              if (this.options.ecmaVersion < 6) return this.dummyIdent();\n              if (p.type === 'ObjectExpression') {\n                p.type = 'ObjectPattern';\n                for (var T = 0, x = p.properties; T < x.length; T += 1) {\n                  var w = x[T];\n                  this.toAssignable(w, h);\n                }\n              } else if (p.type === 'ArrayExpression')\n                (p.type = 'ArrayPattern'), this.toAssignableList(p.elements, h);\n              else if (p.type === 'Property') this.toAssignable(p.value, h);\n              else if (p.type === 'SpreadElement')\n                (p.type = 'RestElement'), this.toAssignable(p.argument, h);\n              else if (p.type === 'AssignmentExpression')\n                (p.type = 'AssignmentPattern'), delete p.operator;\n              else return this.dummyIdent();\n            }\n          return p;\n        }),\n        (g.toAssignableList = function (p, h) {\n          for (var T = 0, x = p; T < x.length; T += 1) {\n            var w = x[T];\n            this.toAssignable(w, h);\n          }\n          return p;\n        }),\n        (g.parseFunctionParams = function (p) {\n          return (\n            (p = this.parseExprList(t.tokTypes.parenR)),\n            this.toAssignableList(p, !0)\n          );\n        }),\n        (g.parseMethod = function (p, h) {\n          var T = this.startNode(),\n            x = this.inAsync,\n            w = this.inGenerator,\n            S = this.inFunction;\n          return (\n            this.initFunction(T),\n            this.options.ecmaVersion >= 6 && (T.generator = !!p),\n            this.options.ecmaVersion >= 8 && (T.async = !!h),\n            (this.inAsync = T.async),\n            (this.inGenerator = T.generator),\n            (this.inFunction = !0),\n            (T.params = this.parseFunctionParams()),\n            (T.body = this.parseBlock()),\n            this.toks.adaptDirectivePrologue(T.body.body),\n            (this.inAsync = x),\n            (this.inGenerator = w),\n            (this.inFunction = S),\n            this.finishNode(T, 'FunctionExpression')\n          );\n        }),\n        (g.parseArrowExpression = function (p, h, T) {\n          var x = this.inAsync,\n            w = this.inGenerator,\n            S = this.inFunction;\n          return (\n            this.initFunction(p),\n            this.options.ecmaVersion >= 8 && (p.async = !!T),\n            (this.inAsync = p.async),\n            (this.inGenerator = !1),\n            (this.inFunction = !0),\n            (p.params = this.toAssignableList(h, !0)),\n            (p.expression = this.tok.type !== t.tokTypes.braceL),\n            p.expression\n              ? (p.body = this.parseMaybeAssign())\n              : ((p.body = this.parseBlock()),\n                this.toks.adaptDirectivePrologue(p.body.body)),\n            (this.inAsync = x),\n            (this.inGenerator = w),\n            (this.inFunction = S),\n            this.finishNode(p, 'ArrowFunctionExpression')\n          );\n        }),\n        (g.parseExprList = function (p, h) {\n          this.pushCx();\n          var T = this.curIndent,\n            x = this.curLineStart,\n            w = [];\n          for (this.next(); !this.closes(p, T + 1, x); ) {\n            if (this.eat(t.tokTypes.comma)) {\n              w.push(h ? null : this.dummyIdent());\n              continue;\n            }\n            var S = this.parseMaybeAssign();\n            if (i(S)) {\n              if (this.closes(p, T, x)) break;\n              this.next();\n            } else w.push(S);\n            this.eat(t.tokTypes.comma);\n          }\n          return (\n            this.popCx(),\n            this.eat(p) ||\n              ((this.last.end = this.tok.start),\n              this.options.locations &&\n                (this.last.loc.end = this.tok.loc.start)),\n            w\n          );\n        }),\n        (g.parseAwait = function () {\n          var p = this.startNode();\n          return (\n            this.next(),\n            (p.argument = this.parseMaybeUnary()),\n            this.finishNode(p, 'AwaitExpression')\n          );\n        }),\n        (t.defaultOptions.tabSize = 4);\n      function L(p, h) {\n        return a.parse(p, h);\n      }\n      (e.LooseParser = a), (e.isDummy = i), (e.parse = L);\n    });\n  });\n  var Vo = Li(),\n    Ug = Jc(),\n    dr = e1(),\n    Hg = uf(),\n    Tf = df(),\n    Os = null;\n  function kf() {\n    return new Proxy(\n      {},\n      {\n        get: function (e, t) {\n          if (t in e) return e[t];\n          var s = String(t).split('#'),\n            i = s[0],\n            r = s[1] || 'default',\n            a = {id: i, chunks: [i], name: r, async: !0};\n          return (e[t] = a), a;\n        },\n      }\n    );\n  }\n  var Nc = {};\n  function mf(e, t, s) {\n    var i = dr.registerServerReference(e, t, s),\n      r = t + '#' + s;\n    return (Nc[r] = e), i;\n  }\n  function Wg(e) {\n    if (e.indexOf('use client') === -1 && e.indexOf('use server') === -1)\n      return null;\n    try {\n      var t = Tf.parse(e, {ecmaVersion: '2024', sourceType: 'source'}).body;\n    } catch {\n      return null;\n    }\n    for (var s = 0; s < t.length; s++) {\n      var i = t[s];\n      if (i.type !== 'ExpressionStatement' || !i.directive) break;\n      if (i.directive === 'use client') return 'use client';\n      if (i.directive === 'use server') return 'use server';\n    }\n    return null;\n  }\n  function Gg(e) {\n    if (e.indexOf('use server') === -1) return e;\n    var t;\n    try {\n      t = Tf.parse(e, {ecmaVersion: '2024', sourceType: 'source'});\n    } catch {\n      return e;\n    }\n    var s = [],\n      i = 0;\n    function r(T, x) {\n      if (!(!T || typeof T != 'object')) {\n        var w =\n          T.type === 'FunctionDeclaration' ||\n          T.type === 'FunctionExpression' ||\n          T.type === 'ArrowFunctionExpression';\n        if (w && x > 0 && T.body && T.body.type === 'BlockStatement')\n          for (var S = T.body.body, A = 0; A < S.length; A++) {\n            var U = S[A];\n            if (U.type !== 'ExpressionStatement') break;\n            if (U.directive === 'use server') {\n              s.push({\n                funcStart: T.start,\n                funcEnd: T.end,\n                dStart: U.start,\n                dEnd: U.end,\n                name: T.id ? T.id.name : 'action' + i,\n                isDecl: T.type === 'FunctionDeclaration',\n              }),\n                i++;\n              return;\n            }\n            if (!U.directive) break;\n          }\n        var M = w ? x + 1 : x;\n        for (var c in T)\n          if (!(c === 'start' || c === 'end' || c === 'type')) {\n            var R = T[c];\n            if (Array.isArray(R))\n              for (var W = 0; W < R.length; W++)\n                R[W] && typeof R[W].type == 'string' && r(R[W], M);\n            else R && typeof R.type == 'string' && r(R, M);\n          }\n      }\n    }\n    if (\n      (t.body.forEach(function (T) {\n        r(T, 0);\n      }),\n      s.length === 0)\n    )\n      return e;\n    s.sort(function (T, x) {\n      return x.funcStart - T.funcStart;\n    });\n    for (var a = e, u = 0; u < s.length; u++) {\n      for (\n        var d = s[u], y = d.dEnd, g = a.charAt(y);\n        y < a.length &&\n        (g === ' ' ||\n          g ===\n            `\n` ||\n          g === '\\r' ||\n          g === '\t');\n\n      )\n        y++, (g = a.charAt(y));\n      a = a.slice(0, d.dStart) + a.slice(y);\n      var L = y - d.dStart,\n        p = d.funcEnd - L,\n        h = a.slice(d.funcStart, p);\n      d.isDecl\n        ? (a =\n            a.slice(0, d.funcStart) +\n            'var ' +\n            d.name +\n            ' = __rsa(' +\n            h +\n            \", '\" +\n            d.name +\n            \"');\" +\n            a.slice(p))\n        : (a =\n            a.slice(0, d.funcStart) +\n            '__rsa(' +\n            h +\n            \", '\" +\n            d.name +\n            \"')\" +\n            a.slice(p));\n    }\n    return a;\n  }\n  function zg(e, t) {\n    if (!t.startsWith('.')) return t;\n    var s = e.split('/');\n    s.pop();\n    for (var i = t.split('/'), r = 0; r < i.length; r++)\n      if (i[r] !== '.') {\n        if (i[r] === '..') {\n          s.pop();\n          continue;\n        }\n        s.push(i[r]);\n      }\n    return s.join('/');\n  }\n  function Xg(e) {\n    Nc = {};\n    var t = {react: Vo, 'react/jsx-runtime': Ug},\n      s = {},\n      i = null;\n    if (\n      (Object.keys(e).forEach(function (h) {\n        if (!i)\n          try {\n            s[h] = Hg.transform(e[h], {\n              transforms: ['jsx', 'imports'],\n              jsxRuntime: 'automatic',\n              production: !0,\n            }).code;\n          } catch (T) {\n            i = h + ': ' + (T.message || String(T));\n          }\n      }),\n      i)\n    )\n      return {type: 'error', error: i};\n    function r(h, T) {\n      if (t[T]) return T;\n      if (T.startsWith('.')) {\n        var x = zg(h, T);\n        if (t[x] || s[x]) return x;\n        for (var w = ['.js', '.jsx', '.ts', '.tsx'], S = 0; S < w.length; S++) {\n          var A = x + w[S];\n          if (t[A] || s[A]) return A;\n        }\n      }\n      return T;\n    }\n    var a = {},\n      u = {};\n    function d(h) {\n      if (t[h]) return t[h];\n      if (!s[h]) throw new Error('Module \"' + h + '\" not found');\n      if (a[h]) return a[h].exports;\n      var T = Wg(e[h]);\n      if (T === 'use client')\n        return (t[h] = dr.createClientModuleProxy(h)), (u[h] = !0), t[h];\n      var x = {exports: {}};\n      a[h] = x;\n      var w = function (c) {\n          if (c.endsWith('.css')) return {};\n          var R = r(h, c);\n          return t[R] ? t[R] : d(R);\n        },\n        S = s[h];\n      if (\n        (T !== 'use server' && (S = Gg(S)),\n        new Function('module', 'exports', 'require', 'React', '__rsa', S)(\n          x,\n          x.exports,\n          w,\n          Vo,\n          function (c, R) {\n            return mf(c, h, R);\n          }\n        ),\n        (t[h] = x.exports),\n        T === 'use server')\n      )\n        for (var A = Object.keys(x.exports), U = 0; U < A.length; U++) {\n          var M = A[U];\n          typeof x.exports[M] == 'function' && mf(x.exports[M], h, M);\n        }\n      return delete a[h], x.exports;\n    }\n    var y = {exports: {}};\n    Object.keys(s).forEach(function (h) {\n      d(h),\n        (h === '/src/App.js' || h === './App.js' || h === './src/App.js') &&\n          (y.exports = t[h]);\n    }),\n      (Os = {module: y.exports});\n    var g = {};\n    function L(h) {\n      if (!g[h]) {\n        g[h] = !0;\n        var T = s[h];\n        if (T)\n          for (\n            var x = /require\\([\"']([^\"']+)[\"']\\)/g, w;\n            (w = x.exec(T)) !== null;\n\n          ) {\n            var S = w[1];\n            if (\n              !(\n                S === 'react' ||\n                S === 'react/jsx-runtime' ||\n                S === 'react/jsx-dev-runtime' ||\n                S.endsWith('.css')\n              )\n            ) {\n              var A = r(h, S);\n              s[A] && L(A);\n            }\n          }\n      }\n    }\n    Object.keys(u).forEach(function (h) {\n      L(h);\n    });\n    var p = {};\n    return (\n      Object.keys(g).forEach(function (h) {\n        p[h] = s[h];\n      }),\n      {type: 'deployed', compiledClients: p, clientEntries: u}\n    );\n  }\n  function Yg() {\n    if (!Os) throw new Error('No code deployed');\n    var e = Os.module.default || Os.module,\n      t = Vo.createElement(e);\n    return dr.renderToReadableStream(t, kf(), {\n      onError: function (s) {\n        return console.error('[RSC Server Error]', s), msg;\n      },\n    });\n  }\n  function Jg(e, t) {\n    if (!Os) throw new Error('No code deployed');\n    var s = Nc[e];\n    if (!s) throw new Error('Action \"' + e + '\" not found');\n    var i = t;\n    if (typeof t != 'string' && t && t.__formData) {\n      i = new FormData();\n      for (var r = 0; r < t.__formData.length; r++)\n        i.append(t.__formData[r][0], t.__formData[r][1]);\n    }\n    return Promise.resolve(dr.decodeReply(i)).then(function (a) {\n      var u = Promise.resolve(s.apply(null, a));\n      return u.then(function () {\n        var d = Os.module.default || Os.module;\n        return dr.renderToReadableStream(\n          {root: Vo.createElement(d), returnValue: u},\n          kf(),\n          {\n            onError: function (y) {\n              return console.error('[RSC Server Error]', y), msg;\n            },\n          }\n        );\n      });\n    });\n  }\n  function yf(e, t) {\n    var s = t.getReader();\n    function i() {\n      return s.read().then(function (r) {\n        if (r.done) {\n          self.postMessage({type: 'rsc-chunk', requestId: e, done: !0});\n          return;\n        }\n        return (\n          self.postMessage(\n            {type: 'rsc-chunk', requestId: e, done: !1, chunk: r.value},\n            [r.value.buffer]\n          ),\n          i()\n        );\n      });\n    }\n    i().catch(function (r) {\n      self.postMessage({type: 'rsc-error', requestId: e, error: String(r)});\n    });\n  }\n  self.onmessage = function (e) {\n    var t = e.data;\n    if (t.type === 'deploy')\n      try {\n        var s = Xg(t.files);\n        s && s.type === 'error'\n          ? self.postMessage({type: 'rsc-error', error: s.error})\n          : s && self.postMessage({type: 'deploy-result', result: s});\n      } catch (r) {\n        self.postMessage({type: 'rsc-error', error: String(r)});\n      }\n    else if (t.type === 'render')\n      try {\n        var i = Yg();\n        Promise.resolve(i)\n          .then(function (r) {\n            yf(t.requestId, r);\n          })\n          .catch(function (r) {\n            self.postMessage({\n              type: 'rsc-error',\n              requestId: t.requestId,\n              error: String(r),\n            });\n          });\n      } catch (r) {\n        self.postMessage({\n          type: 'rsc-error',\n          requestId: t.requestId,\n          error: String(r),\n        });\n      }\n    else if (t.type === 'callAction')\n      try {\n        Jg(t.actionId, t.encodedArgs)\n          .then(function (r) {\n            yf(t.requestId, r);\n          })\n          .catch(function (r) {\n            self.postMessage({\n              type: 'rsc-error',\n              requestId: t.requestId,\n              error: String(r),\n            });\n          });\n      } catch (r) {\n        self.postMessage({\n          type: 'rsc-error',\n          requestId: t.requestId,\n          error: String(r),\n        });\n      }\n  };\n  self.postMessage({type: 'ready'});\n})();\n/*! Bundled license information:\n\nreact/cjs/react.react-server.production.js:\n  (**\n   * @license React\n   * react.react-server.production.js\n   *\n   * Copyright (c) Meta Platforms, Inc. and affiliates.\n   *\n   * This source code is licensed under the MIT license found in the\n   * LICENSE file in the root directory of this source tree.\n   *)\n\nreact/cjs/react-jsx-runtime.react-server.production.js:\n  (**\n   * @license React\n   * react-jsx-runtime.react-server.production.js\n   *\n   * Copyright (c) Meta Platforms, Inc. and affiliates.\n   *\n   * This source code is licensed under the MIT license found in the\n   * LICENSE file in the root directory of this source tree.\n   *)\n\nreact-dom/cjs/react-dom.react-server.production.js:\n  (**\n   * @license React\n   * react-dom.react-server.production.js\n   *\n   * Copyright (c) Meta Platforms, Inc. and affiliates.\n   *\n   * This source code is licensed under the MIT license found in the\n   * LICENSE file in the root directory of this source tree.\n   *)\n\nreact-server-dom-webpack/cjs/react-server-dom-webpack-server.browser.production.js:\n  (**\n   * @license React\n   * react-server-dom-webpack-server.browser.production.js\n   *\n   * Copyright (c) Meta Platforms, Inc. and affiliates.\n   *\n   * This source code is licensed under the MIT license found in the\n   * LICENSE file in the root directory of this source tree.\n   *)\n*/\n"
  },
  {
    "path": "src/components/MDX/Sandpack/template.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nexport const template = {\n  '/src/index.js': {\n    hidden: true,\n    code: `import { StrictMode } from \"react\";\nimport { createRoot } from \"react-dom/client\";\nimport \"./styles.css\";\n\nimport App from \"./App\";\n\nconst root = createRoot(document.getElementById(\"root\"));\nroot.render(\n  <StrictMode>\n    <App />\n  </StrictMode>\n);`,\n  },\n  '/package.json': {\n    hidden: true,\n    code: JSON.stringify(\n      {\n        name: 'react.dev',\n        version: '0.0.0',\n        main: '/src/index.js',\n        scripts: {\n          start: 'react-scripts start',\n          build: 'react-scripts build',\n          test: 'react-scripts test --env=jsdom',\n          eject: 'react-scripts eject',\n        },\n        dependencies: {\n          react: '^19.2.1',\n          'react-dom': '^19.2.1',\n          'react-scripts': '^5.0.0',\n        },\n      },\n      null,\n      2\n    ),\n  },\n  '/public/index.html': {\n    hidden: true,\n    code: `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"UTF-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <title>Document</title>\n</head>\n<body>\n  <div id=\"root\"></div>\n</body>\n</html>`,\n  },\n};\n"
  },
  {
    "path": "src/components/MDX/Sandpack/templateRSC.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nimport type {SandpackFiles} from '@codesandbox/sandpack-react/unstyled';\n\nfunction hideFiles(files: SandpackFiles): SandpackFiles {\n  return Object.fromEntries(\n    Object.entries(files).map(([name, code]) => [\n      name,\n      typeof code === 'string' ? {code, hidden: true} : {...code, hidden: true},\n    ])\n  );\n}\n\n// --- Load RSC infrastructure files as raw strings via raw-loader ---\nconst RSC_SOURCE_FILES = {\n  'webpack-shim':\n    require('!raw-loader?esModule=false!./sandpack-rsc/sandbox-code/src/webpack-shim.js') as string,\n  'rsc-client':\n    require('!raw-loader?esModule=false!./sandpack-rsc/sandbox-code/src/rsc-client.js') as string,\n  'react-refresh-init':\n    require('!raw-loader?esModule=false!./sandpack-rsc/sandbox-code/src/__react_refresh_init__.js') as string,\n  'worker-bundle': `export default ${JSON.stringify(\n    require('!raw-loader?esModule=false!./sandpack-rsc/sandbox-code/src/worker-bundle.dist.js') as string\n  )};`,\n  'rsdw-client':\n    require('!raw-loader?esModule=false!../../../../node_modules/react-server-dom-webpack/cjs/react-server-dom-webpack-client.browser.production.js') as string,\n};\n\n// Load react-refresh runtime and strip the process.env.NODE_ENV guard\n// so it works in Sandpack's bundler which may not replace process.env.\nconst reactRefreshRaw =\n  require('!raw-loader?esModule=false!../../../../node_modules/next/dist/compiled/react-refresh/cjs/react-refresh-runtime.development.js') as string;\n\n// Wrap as a CJS module that Sandpack can require.\n// Strip the `if (process.env.NODE_ENV !== \"production\")` guard so the\n// runtime always executes inside the sandbox.\nconst reactRefreshModule = reactRefreshRaw.replace(\n  /if \\(process\\.env\\.NODE_ENV !== \"production\"\\) \\{/,\n  '{'\n);\n\n// Entry point that bootstraps the RSC client pipeline.\n// __react_refresh_init__ must be imported BEFORE rsc-client so the\n// DevTools hook stub exists before React's renderer loads.\nconst indexEntry = `\nimport './styles.css';\nimport './__react_refresh_init__';\nimport { initClient } from './rsc-client.js';\ninitClient();\n`.trim();\n\nconst indexHTML = `\n<!DOCTYPE html>\n  <html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <title>Document</title>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n  </body>\n</html>\n`.trim();\n\nexport const templateRSC: SandpackFiles = {\n  ...hideFiles({\n    '/public/index.html': indexHTML,\n    '/src/index.js': indexEntry,\n    '/src/__react_refresh_init__.js': RSC_SOURCE_FILES['react-refresh-init'],\n    '/src/rsc-client.js': RSC_SOURCE_FILES['rsc-client'],\n    '/src/rsc-server.js': RSC_SOURCE_FILES['worker-bundle'],\n    '/src/__webpack_shim__.js': RSC_SOURCE_FILES['webpack-shim'],\n    // RSDW client as a Sandpack local dependency (bypasses Babel bundler)\n    '/node_modules/react-server-dom-webpack/package.json':\n      '{\"name\":\"react-server-dom-webpack\",\"main\":\"index.js\"}',\n    '/node_modules/react-server-dom-webpack/client.browser.js':\n      RSC_SOURCE_FILES['rsdw-client'],\n    // react-refresh runtime as a Sandpack local dependency\n    '/node_modules/react-refresh/package.json':\n      '{\"name\":\"react-refresh\",\"main\":\"runtime.js\"}',\n    '/node_modules/react-refresh/runtime.js': reactRefreshModule,\n    '/package.json': JSON.stringify(\n      {\n        name: 'react.dev',\n        version: '0.0.0',\n        main: '/src/index.js',\n        dependencies: {\n          react: '19.2.4',\n          'react-dom': '19.2.4',\n        },\n      },\n      null,\n      2\n    ),\n  }),\n};\n"
  },
  {
    "path": "src/components/MDX/Sandpack/useSandpackLint.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\n// @ts-nocheck\n\nimport {useState, useEffect} from 'react';\nimport type {EditorView} from '@codemirror/view';\n\nexport type LintDiagnostic = {\n  line: number;\n  column: number;\n  severity: 'warning' | 'error';\n  message: string;\n}[];\n\nexport const useSandpackLint = () => {\n  const [lintErrors, setLintErrors] = useState<LintDiagnostic>([]);\n  const [lintExtensions, setLintExtensions] = useState<any>([]);\n  useEffect(() => {\n    const loadLinter = async () => {\n      const {linter} = await import('@codemirror/lint');\n      const onLint = linter(async (props: EditorView) => {\n        // This is intentionally delayed until CodeMirror calls it\n        // so that we don't take away bandwidth from things loading early.\n        const {runESLint} = await import('./runESLint');\n        const editorState = props.state.doc;\n        let {errors, codeMirrorErrors} = runESLint(editorState);\n        // Ignore parsing or internal linter errors.\n        const isReactRuleError = (error: any) => error.ruleId != null;\n        setLintErrors(errors.filter(isReactRuleError));\n        return codeMirrorErrors.filter(isReactRuleError);\n      });\n      setLintExtensions([onLint]);\n    };\n\n    loadLinter();\n  }, []);\n  return {lintErrors, lintExtensions};\n};\n"
  },
  {
    "path": "src/components/MDX/SandpackWithHTMLOutput.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nimport {Children, memo} from 'react';\nimport InlineCode from './InlineCode';\nimport {SandpackClient} from './Sandpack';\n\nconst ShowRenderedHTML = `\nimport { renderToStaticMarkup } from 'react-dom/server';\nimport formatHTML from './formatHTML.js';\n\nexport default function ShowRenderedHTML({children}) {\n  const markup = renderToStaticMarkup(\n    <html>\n      <head />\n      <body>{children}</body>\n    </html>\n  );\n  return (\n    <>\n      <h1>Rendered HTML:</h1>\n      <pre>\n        {formatHTML(markup)}\n      </pre>\n    </>\n  );\n}`;\n\nconst formatHTML = `\nimport format from 'html-format';\n\nexport default function formatHTML(markup) {\n  // Cheap tricks to format the HTML readably -- haven't been able to\n  // find a package that runs in browser and prettifies the HTML if it\n  // lacks line-breaks.\n  return format(markup\n    .replace('<html>', '<html>\\\\n')\n    .replace('<head>', '<head>\\\\n')\n    .replaceAll(/<\\\\/script>/g, '<\\\\/script>\\\\n')\n    .replaceAll(/<style([^>]*)\\\\/>/g, '<style $1/>\\\\n\\\\n')\n    .replaceAll(/<\\\\/style>/g, '\\\\n    <\\\\/style>\\\\n')\n    .replaceAll(/<link([^>]*)\\\\/>/g, '<link $1/>\\\\n')\n    .replaceAll(/<meta([^/]*)\\\\/>/g, '<meta $1/>\\\\n')\n    .replace('</head>', '</head>\\\\n')\n    .replace('<body>', '<body>\\\\n')\n    .replace('</body>', '\\\\n</body>\\\\n')\n    .replace('</h1>', '</h1>\\\\n')\n  );\n}\n`;\n\nconst packageJSON = `\n{\n  \"dependencies\": {\n    \"react\": \"^19.2.1\",\n    \"react-dom\": \"^19.2.1\",\n    \"react-scripts\": \"^5.0.0\",\n    \"html-format\": \"^1.1.2\"\n  },\n  \"main\": \"/index.js\",\n  \"devDependencies\": {}\n}\n`;\n\n// Intentionally not a React component because <Sandpack> will read\n// through its childrens' props. This imitates the output of ```\n// codeblocks in MDX.\nfunction createFile(meta: string, source: string) {\n  return (\n    <pre key={meta}>\n      <InlineCode meta={meta} className=\"language-js\">\n        {source}\n      </InlineCode>\n    </pre>\n  );\n}\n\nexport default memo(function SandpackWithHTMLOutput(\n  props: React.ComponentProps<typeof SandpackClient>\n) {\n  const children = [\n    ...Children.toArray(props.children),\n    createFile('src/ShowRenderedHTML.js', ShowRenderedHTML),\n    createFile('src/formatHTML.js hidden', formatHTML),\n    createFile('package.json hidden', packageJSON),\n  ];\n  return <SandpackClient {...props}>{children}</SandpackClient>;\n});\n"
  },
  {
    "path": "src/components/MDX/SimpleCallout.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport * as React from 'react';\nimport cn from 'classnames';\nimport {H3} from './Heading';\n\ninterface SimpleCalloutProps {\n  title: string;\n  children: React.ReactNode;\n  className?: string;\n}\nfunction SimpleCallout({title, children, className}: SimpleCalloutProps) {\n  return (\n    <div\n      className={cn(\n        'p-6 xl:p-8 pb-4 xl:pb-6 bg-card dark:bg-card-dark rounded-2xl shadow-inner-border dark:shadow-inner-border-dark text-base text-secondary dark:text-secondary-dark my-8',\n        className\n      )}>\n      <H3\n        className=\"text-primary dark:text-primary-dark mt-0 mb-3 leading-tight\"\n        isPageAnchor={false}>\n        {title}\n      </H3>\n      {children}\n    </div>\n  );\n}\n\nexport default SimpleCallout;\n"
  },
  {
    "path": "src/components/MDX/TeamMember.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport * as React from 'react';\nimport Image from 'next/legacy/image';\nimport {IconTwitter} from '../Icon/IconTwitter';\nimport {IconThreads} from '../Icon/IconThreads';\nimport {IconBsky} from '../Icon/IconBsky';\nimport {IconGitHub} from '../Icon/IconGitHub';\nimport {ExternalLink} from '../ExternalLink';\nimport {H3} from './Heading';\nimport {IconLink} from 'components/Icon/IconLink';\n\ninterface TeamMemberProps {\n  name: string;\n  title: string;\n  permalink: string;\n  children: React.ReactNode;\n  photo: string;\n  twitter?: string;\n  threads?: string;\n  bsky?: string;\n  github?: string;\n  personal?: string;\n}\n\n// TODO: good alt text for images/links\nexport function TeamMember({\n  name,\n  title,\n  permalink,\n  children,\n  photo,\n  github,\n  twitter,\n  threads,\n  bsky,\n  personal,\n}: TeamMemberProps) {\n  if (name == null || title == null || permalink == null || children == null) {\n    const identifier = name ?? title ?? permalink ?? 'unknown';\n    throw new Error(\n      `Expected name, title, permalink, and children for ${identifier}`\n    );\n  }\n  return (\n    <div className=\"pb-6 sm:pb-10\">\n      <div className=\"flex flex-col sm:flex-row height-auto\">\n        <div\n          className=\"hidden sm:block basis-2/5 rounded overflow-hidden relative\"\n          style={{width: 300, height: 250}}>\n          <Image src={photo} layout=\"fill\" objectFit=\"cover\" alt={name} />\n        </div>\n        <div\n          style={{minHeight: 300}}\n          className=\"block w-full sm:hidden flex-grow basis-2/5 rounded overflow-hidden relative\">\n          <Image src={photo} layout=\"fill\" objectFit=\"cover\" alt={name} />\n        </div>\n        <div className=\"ps-0 sm:ps-6 basis-3/5 items-start\">\n          <H3 className=\"mb-1 sm:my-0\" id={permalink}>\n            {name}\n          </H3>\n          {title && <div>{title}</div>}\n          {children}\n          <div className=\"sm:flex sm:flex-row flex-wrap text-secondary dark:text-secondary-dark\">\n            {twitter && (\n              <div className=\"me-4\">\n                <ExternalLink\n                  aria-label={`${name} on Twitter`}\n                  href={`https://twitter.com/${twitter}`}\n                  className=\"hover:text-primary hover:underline dark:text-primary-dark flex flex-row items-center\">\n                  <IconTwitter className=\"pe-1\" />\n                  {twitter}\n                </ExternalLink>\n              </div>\n            )}\n            {threads && (\n              <div className=\"me-4\">\n                <ExternalLink\n                  aria-label={`${name} on Threads`}\n                  href={`https://threads.net/${threads}`}\n                  className=\"hover:text-primary hover:underline dark:text-primary-dark flex flex-row items-center\">\n                  <IconThreads className=\"pe-1\" />\n                  {threads}\n                </ExternalLink>\n              </div>\n            )}\n            {bsky && (\n              <div className=\"me-4\">\n                <ExternalLink\n                  aria-label={`${name} on Bluesky`}\n                  href={`https://bsky.app/profile/${bsky}`}\n                  className=\"hover:text-primary hover:underline dark:text-primary-dark flex flex-row items-center\">\n                  <IconBsky className=\"pe-1\" />\n                  {bsky}\n                </ExternalLink>\n              </div>\n            )}\n            {github && (\n              <div className=\"me-4\">\n                <ExternalLink\n                  aria-label=\"GitHub Profile\"\n                  href={`https://github.com/${github}`}\n                  className=\"hover:text-primary hover:underline dark:text-primary-dark flex flex-row items-center\">\n                  <IconGitHub className=\"pe-1\" /> {github}\n                </ExternalLink>\n              </div>\n            )}\n            {personal && (\n              <ExternalLink\n                aria-label=\"Personal Site\"\n                href={`https://${personal}`}\n                className=\"hover:text-primary hover:underline dark:text-primary-dark flex flex-row items-center\">\n                <IconLink className=\"pe-1\" /> {personal}\n              </ExternalLink>\n            )}\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/components/MDX/TerminalBlock.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport {isValidElement, useState, useEffect} from 'react';\nimport * as React from 'react';\nimport {IconTerminal} from '../Icon/IconTerminal';\nimport {IconCopy} from 'components/Icon/IconCopy';\n\ntype LogLevel = 'info' | 'warning' | 'error';\n\ninterface TerminalBlockProps {\n  level?: LogLevel;\n  children: React.ReactNode;\n}\n\nfunction LevelText({type}: {type: LogLevel}) {\n  switch (type) {\n    case 'warning':\n      return <span className=\"text-yellow-50 bg-none me-1\">Warning: </span>;\n    case 'error':\n      return <span className=\"text-red-40 me-1\">Error: </span>;\n    default:\n      return null;\n  }\n}\n\nfunction TerminalBlock({level = 'info', children}: TerminalBlockProps) {\n  let message: string | undefined;\n  if (typeof children === 'string') {\n    message = children;\n  } else if (\n    isValidElement(children) &&\n    typeof (children as React.ReactElement<{children: string}>).props\n      .children === 'string'\n  ) {\n    message = (children as React.ReactElement<{children: string}>).props\n      .children;\n  } else {\n    throw Error('Expected TerminalBlock children to be a plain string.');\n  }\n\n  const [copied, setCopied] = useState(false);\n  useEffect(() => {\n    if (!copied) {\n      return;\n    } else {\n      const timer = setTimeout(() => {\n        setCopied(false);\n      }, 2000);\n      return () => clearTimeout(timer);\n    }\n  }, [copied]);\n\n  return (\n    <div className=\"rounded-lg bg-secondary dark:bg-gray-50 h-full\">\n      <div className=\"bg-gray-90 dark:bg-gray-60 w-full rounded-t-lg\">\n        <div className=\"text-primary-dark dark:text-primary-dark flex text-sm px-4 py-0.5 relative justify-between\">\n          <div>\n            <IconTerminal className=\"inline-flex me-2 self-center\" /> 터미널\n          </div>\n          <div>\n            <button\n              className=\"w-full text-start text-primary-dark dark:text-primary-dark \"\n              onClick={() => {\n                window.navigator.clipboard.writeText(message ?? '');\n                setCopied(true);\n              }}>\n              <IconCopy className=\"inline-flex me-2 self-center\" />{' '}\n              {copied ? '복사됨' : '복사'}\n            </button>\n          </div>\n        </div>\n      </div>\n      <pre\n        className=\"px-8 pt-4 pb-6 text-primary-dark dark:text-primary-dark font-mono text-code whitespace-pre overflow-x-auto\"\n        translate=\"no\"\n        dir=\"ltr\">\n        <code>\n          <LevelText type={level} />\n          {message}\n        </code>\n      </pre>\n    </div>\n  );\n}\n\nexport default TerminalBlock;\n"
  },
  {
    "path": "src/components/MDX/TocContext.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport {createContext} from 'react';\nimport type {ReactNode} from 'react';\n\nexport type TocItem = {\n  url: string;\n  text: ReactNode;\n  depth: number;\n};\nexport type Toc = Array<TocItem>;\n\nexport const TocContext = createContext<Toc>([]);\n"
  },
  {
    "path": "src/components/MDX/YouWillLearnCard.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport * as React from 'react';\nimport ButtonLink from 'components/ButtonLink';\nimport {IconNavArrow} from 'components/Icon/IconNavArrow';\n\ninterface YouWillLearnCardProps {\n  title: string;\n  path: string;\n  children: React.ReactNode;\n}\n\nfunction YouWillLearnCard({title, path, children}: YouWillLearnCardProps) {\n  return (\n    <div className=\"flex flex-col h-full bg-card dark:bg-card-dark shadow-inner justify-between rounded-lg pb-8 p-6 xl:p-8 mt-3\">\n      <div>\n        <h4 className=\"text-primary dark:text-primary-dark font-bold text-2xl leading-tight\">\n          {title}\n        </h4>\n        <div className=\"my-4\">{children}</div>\n      </div>\n      <div>\n        <ButtonLink\n          href={path}\n          className=\"mt-1\"\n          type=\"primary\"\n          size=\"md\"\n          label={title}>\n          Read More\n          <IconNavArrow displayDirection=\"end\" className=\"inline ms-1\" />\n        </ButtonLink>\n      </div>\n    </div>\n  );\n}\n\nexport default YouWillLearnCard;\n"
  },
  {
    "path": "src/components/PageHeading.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport Breadcrumbs from 'components/Breadcrumbs';\nimport Tag from 'components/Tag';\nimport {H1} from './MDX/Heading';\nimport type {RouteTag, RouteItem} from './Layout/getRouteMeta';\nimport * as React from 'react';\nimport {useState, useEffect} from 'react';\nimport {useRouter} from 'next/router';\nimport {IconCanary} from './Icon/IconCanary';\nimport {IconExperimental} from './Icon/IconExperimental';\nimport {IconCopy} from './Icon/IconCopy';\nimport {Button} from './Button';\n\ninterface PageHeadingProps {\n  title: string;\n  version?: 'experimental' | 'canary' | 'rc';\n  experimental?: boolean;\n  status?: string;\n  description?: string;\n  tags?: RouteTag[];\n  breadcrumbs: RouteItem[];\n}\n\nfunction CopyAsMarkdownButton() {\n  const {asPath} = useRouter();\n  const [copied, setCopied] = useState(false);\n\n  useEffect(() => {\n    if (!copied) return;\n    const timer = setTimeout(() => setCopied(false), 2000);\n    return () => clearTimeout(timer);\n  }, [copied]);\n\n  async function fetchPageBlob() {\n    const cleanPath = asPath.split(/[?#]/)[0];\n    const res = await fetch(cleanPath + '.md');\n    if (!res.ok) throw new Error('Failed to fetch');\n    const text = await res.text();\n    return new Blob([text], {type: 'text/plain'});\n  }\n\n  async function handleCopy() {\n    try {\n      await navigator.clipboard.write([\n        // Don't wait for the blob, or Safari will refuse clipboard access\n        new ClipboardItem({'text/plain': fetchPageBlob()}),\n      ]);\n      setCopied(true);\n    } catch {\n      // Silently fail\n    }\n  }\n\n  return (\n    <Button onClick={handleCopy} className=\"text-sm py-1 px-3\">\n      <IconCopy className=\"w-3.5 h-3.5 me-1.5\" />\n      {copied ? (\n        'Copied!'\n      ) : (\n        <>\n          <span className=\"hidden sm:inline\">Copy page</span>\n          <span className=\"sm:hidden\">Copy</span>\n        </>\n      )}\n    </Button>\n  );\n}\n\nfunction PageHeading({\n  title,\n  status,\n  version,\n  tags = [],\n  breadcrumbs,\n}: PageHeadingProps) {\n  return (\n    <div className=\"px-5 sm:px-12 pt-3.5\">\n      <div className=\"max-w-4xl ms-0 2xl:mx-auto\">\n        <div className=\"flex justify-between items-start\">\n          <div className=\"flex-1\">\n            {breadcrumbs ? <Breadcrumbs breadcrumbs={breadcrumbs} /> : null}\n          </div>\n          <CopyAsMarkdownButton />\n        </div>\n        <H1 className=\"mt-0 text-primary dark:text-primary-dark -mx-.5 break-words\">\n          {title}\n          {version === 'canary' && (\n            <IconCanary\n              title=\" - This feature is available in the latest Canary version of React\"\n              className=\"ms-4 mt-1 text-gray-50 dark:text-gray-40 inline-block w-6 h-6 align-[-1px]\"\n            />\n          )}\n          {version === 'rc' && (\n            <IconCanary\n              title=\" - This feature is available in the latest RC version\"\n              className=\"ms-4 mt-1 text-gray-50 dark:text-gray-40 inline-block w-6 h-6 align-[-1px]\"\n            />\n          )}\n          {version === 'experimental' && (\n            <IconExperimental\n              title=\" - This feature is available in the latest Experimental version of React\"\n              className=\"ms-4 mt-1 text-gray-50 dark:text-gray-40 inline-block w-6 h-6 align-[-1px]\"\n            />\n          )}\n          {status ? <em>—{status}</em> : ''}\n        </H1>\n        {tags?.length > 0 && (\n          <div className=\"mt-4\">\n            {tags.map((tag) => (\n              <Tag key={tag} variant={tag as RouteTag} />\n            ))}\n          </div>\n        )}\n      </div>\n    </div>\n  );\n}\n\nexport default PageHeading;\n"
  },
  {
    "path": "src/components/Search.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport Head from 'next/head';\nimport Link from 'next/link';\nimport Router from 'next/router';\nimport {lazy, useEffect} from 'react';\nimport * as React from 'react';\nimport {createPortal} from 'react-dom';\nimport {siteConfig} from 'siteConfig';\nimport type {ComponentType, PropsWithChildren} from 'react';\nimport type {DocSearchModalProps} from '@docsearch/react/modal';\n\nexport interface SearchProps {\n  appId?: string;\n  apiKey?: string;\n  indexName?: string;\n  searchParameters?: any;\n  isOpen: boolean;\n  onOpen: () => void;\n  onClose: () => void;\n}\n\nfunction Hit({hit, children}: any) {\n  return <Link href={hit.url.replace()}>{children}</Link>;\n}\n\n// Copy-pasted from @docsearch/react to avoid importing the whole bundle.\n// Slightly trimmed to features we use.\n// (c) Algolia, Inc.\nfunction isEditingContent(event: any) {\n  var element = event.target;\n  var tagName = element.tagName;\n  return (\n    element.isContentEditable ||\n    tagName === 'INPUT' ||\n    tagName === 'SELECT' ||\n    tagName === 'TEXTAREA'\n  );\n}\nfunction useDocSearchKeyboardEvents({\n  isOpen,\n  onOpen,\n  onClose,\n}: {\n  isOpen: boolean;\n  onOpen: () => void;\n  onClose: () => void;\n}) {\n  useEffect(() => {\n    function onKeyDown(event: any) {\n      function open() {\n        // We check that no other DocSearch modal is showing before opening\n        // another one.\n        if (!document.body.classList.contains('DocSearch--active')) {\n          onOpen();\n        }\n      }\n      if (\n        (event.keyCode === 27 && isOpen) ||\n        (event.key === 'k' && (event.metaKey || event.ctrlKey)) ||\n        (!isEditingContent(event) && event.key === '/' && !isOpen)\n      ) {\n        event.preventDefault();\n        if (isOpen) {\n          onClose();\n        } else if (!document.body.classList.contains('DocSearch--active')) {\n          open();\n        }\n      }\n    }\n\n    window.addEventListener('keydown', onKeyDown);\n    return function () {\n      window.removeEventListener('keydown', onKeyDown);\n    };\n  }, [isOpen, onOpen, onClose]);\n}\n\nconst options = {\n  appId: siteConfig.algolia.appId,\n  apiKey: siteConfig.algolia.apiKey,\n  indexName: siteConfig.algolia.indexName,\n};\n\nconst DocSearchModal: any = lazy(() =>\n  import('@docsearch/react/modal').then((mod) => ({\n    default: mod.DocSearchModal as ComponentType<\n      PropsWithChildren<DocSearchModalProps>\n    >,\n  }))\n);\n\nexport function Search({\n  isOpen,\n  onOpen,\n  onClose,\n  searchParameters = {\n    hitsPerPage: 30,\n    attributesToHighlight: [\n      'hierarchy.lvl0',\n      'hierarchy.lvl1',\n      'hierarchy.lvl2',\n      'hierarchy.lvl3',\n      'hierarchy.lvl4',\n      'hierarchy.lvl5',\n      'hierarchy.lvl6',\n      'content',\n    ],\n  },\n}: SearchProps) {\n  useDocSearchKeyboardEvents({isOpen, onOpen, onClose});\n  return (\n    <>\n      <Head>\n        <link\n          rel=\"preconnect\"\n          href={`https://${options.appId}-dsn.algolia.net`}\n        />\n      </Head>\n      {isOpen &&\n        createPortal(\n          <DocSearchModal\n            {...options}\n            searchParameters={searchParameters}\n            onClose={onClose}\n            navigator={{\n              navigate({itemUrl}: any) {\n                Router.push(itemUrl);\n              },\n            }}\n            transformItems={(items: any[]) => {\n              return items.map((item) => {\n                const url = new URL(item.url);\n                return {\n                  ...item,\n                  url: item.url.replace(url.origin, '').replace('#__next', ''),\n                };\n              });\n            }}\n            hitComponent={Hit}\n          />,\n          document.body\n        )}\n    </>\n  );\n}\n"
  },
  {
    "path": "src/components/Seo.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport * as React from 'react';\nimport Head from 'next/head';\nimport {withRouter, Router} from 'next/router';\nimport {siteConfig} from '../siteConfig';\nimport {finishedTranslations} from 'utils/finishedTranslations';\n\nexport interface SeoProps {\n  title: string;\n  titleForTitleTag: undefined | string;\n  description?: string;\n  image?: string;\n  // jsonld?: JsonLDType | Array<JsonLDType>;\n  children?: React.ReactNode;\n  isHomePage: boolean;\n  searchOrder?: number;\n}\n\n// If you are a maintainer of a language fork,\n// deployedTranslations has been moved to src/utils/finishedTranslations.ts.\n\nfunction getDomain(languageCode: string): string {\n  const subdomain = languageCode === 'en' ? '' : languageCode + '.';\n  return subdomain + 'react.dev';\n}\n\nexport const Seo = withRouter(\n  ({\n    title,\n    titleForTitleTag,\n    image = '/images/og-default.png',\n    router,\n    children,\n    isHomePage,\n    searchOrder,\n  }: SeoProps & {router: Router}) => {\n    const siteDomain = getDomain(siteConfig.languageCode);\n    const canonicalUrl = `https://${siteDomain}${\n      router.asPath.split(/[\\?\\#]/)[0]\n    }`;\n    // Allow setting a different title for Google results\n    const pageTitle =\n      (titleForTitleTag ?? title) + (isHomePage ? '' : ' – React');\n    // Twitter's meta parser is not very good.\n    const twitterTitle = pageTitle.replace(/[<>]/g, '');\n    let description = isHomePage\n      ? 'React is the library for web and native user interfaces. Build user interfaces out of individual pieces called components written in JavaScript. React is designed to let you seamlessly combine components written by independent people, teams, and organizations.'\n      : 'The library for web and native user interfaces';\n    return (\n      <Head>\n        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n        {title != null && <title key=\"title\">{pageTitle}</title>}\n        {isHomePage && (\n          // Let Google figure out a good description for each page.\n          <meta name=\"description\" key=\"description\" content={description} />\n        )}\n        <link rel=\"canonical\" href={canonicalUrl} />\n        <link\n          rel=\"alternate\"\n          href={canonicalUrl.replace(siteDomain, getDomain('en'))}\n          hrefLang=\"x-default\"\n        />\n        {finishedTranslations.map((languageCode) => (\n          <link\n            key={'alt-' + languageCode}\n            rel=\"alternate\"\n            hrefLang={languageCode}\n            href={canonicalUrl.replace(siteDomain, getDomain(languageCode))}\n          />\n        ))}\n        <meta property=\"fb:app_id\" content=\"623268441017527\" />\n        <meta property=\"og:type\" key=\"og:type\" content=\"website\" />\n        <meta property=\"og:url\" key=\"og:url\" content={canonicalUrl} />\n        {title != null && (\n          <meta property=\"og:title\" content={pageTitle} key=\"og:title\" />\n        )}\n        {description != null && (\n          <meta\n            property=\"og:description\"\n            key=\"og:description\"\n            content={description}\n          />\n        )}\n        <meta\n          property=\"og:image\"\n          key=\"og:image\"\n          content={`https://${siteDomain}${image}`}\n        />\n        <meta\n          name=\"twitter:card\"\n          key=\"twitter:card\"\n          content=\"summary_large_image\"\n        />\n        <meta name=\"twitter:site\" key=\"twitter:site\" content=\"@reactjs\" />\n        <meta name=\"twitter:creator\" key=\"twitter:creator\" content=\"@reactjs\" />\n        {title != null && (\n          <meta\n            name=\"twitter:title\"\n            key=\"twitter:title\"\n            content={twitterTitle}\n          />\n        )}\n        {description != null && (\n          <meta\n            name=\"twitter:description\"\n            key=\"twitter:description\"\n            content={description}\n          />\n        )}\n        <meta\n          name=\"twitter:image\"\n          key=\"twitter:image\"\n          content={`https://${siteDomain}${image}`}\n        />\n        <meta\n          name=\"google-site-verification\"\n          content=\"sIlAGs48RulR4DdP95YSWNKZIEtCqQmRjzn-Zq-CcD0\"\n        />\n        {searchOrder != null && (\n          <meta name=\"algolia-search-order\" content={'' + searchOrder} />\n        )}\n        <link\n          rel=\"preload\"\n          href=\"https://react.dev/fonts/Source-Code-Pro-Regular.woff2\"\n          as=\"font\"\n          type=\"font/woff2\"\n          crossOrigin=\"anonymous\"\n        />\n        <link\n          rel=\"preload\"\n          href=\"https://react.dev/fonts/Source-Code-Pro-Bold.woff2\"\n          as=\"font\"\n          type=\"font/woff2\"\n          crossOrigin=\"anonymous\"\n        />\n        <link\n          rel=\"preload\"\n          href=\"https://react.dev/fonts/Optimistic_Display_W_Md.woff2\"\n          as=\"font\"\n          type=\"font/woff2\"\n          crossOrigin=\"anonymous\"\n        />\n        <link\n          rel=\"preload\"\n          href=\"https://react.dev/fonts/Optimistic_Display_W_SBd.woff2\"\n          as=\"font\"\n          type=\"font/woff2\"\n          crossOrigin=\"anonymous\"\n        />\n        <link\n          rel=\"preload\"\n          href=\"https://react.dev/fonts/Optimistic_Display_W_Bd.woff2\"\n          as=\"font\"\n          type=\"font/woff2\"\n          crossOrigin=\"anonymous\"\n        />\n        <link\n          rel=\"preload\"\n          href=\"https://react.dev/fonts/Optimistic_Text_W_Md.woff2\"\n          as=\"font\"\n          type=\"font/woff2\"\n          crossOrigin=\"anonymous\"\n        />\n        <link\n          rel=\"preload\"\n          href=\"https://react.dev/fonts/Optimistic_Text_W_Bd.woff2\"\n          as=\"font\"\n          type=\"font/woff2\"\n          crossOrigin=\"anonymous\"\n        />\n        <link\n          rel=\"preload\"\n          href=\"https://react.dev/fonts/Optimistic_Text_W_Rg.woff2\"\n          as=\"font\"\n          type=\"font/woff2\"\n          crossOrigin=\"anonymous\"\n        />\n        <link\n          rel=\"preload\"\n          href=\"https://react.dev/fonts/Optimistic_Text_W_It.woff2\"\n          as=\"font\"\n          type=\"font/woff2\"\n          crossOrigin=\"anonymous\"\n        />\n        {children}\n      </Head>\n    );\n  }\n);\n"
  },
  {
    "path": "src/components/SocialBanner.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nimport {useRef, useEffect} from 'react';\nimport cn from 'classnames';\nimport {ExternalLink} from './ExternalLink';\n\nconst bannerText = '10월 7-8일 React Conf에 함께하세요.';\nconst bannerLink = 'https://conf.react.dev/';\nconst bannerLinkText = '더 알아보기.';\n\nexport default function SocialBanner() {\n  const ref = useRef<HTMLDivElement | null>(null);\n  useEffect(() => {\n    function patchedScrollTo(x: number, y: number) {\n      if (y === 0) {\n        // We're trying to reset scroll.\n        // If we already scrolled past the banner, consider it as y = 0.\n        const bannerHeight = ref.current?.offsetHeight ?? 0; // Could be zero (e.g. mobile)\n        y = Math.min(window.scrollY, bannerHeight);\n      }\n      return realScrollTo(x, y);\n    }\n    const realScrollTo = window.scrollTo;\n    (window as any).scrollTo = patchedScrollTo;\n    return () => {\n      (window as any).scrollTo = realScrollTo;\n    };\n  }, []);\n  return (\n    <div\n      ref={ref}\n      className={cn(\n        `h-[40px] hidden lg:flex w-full bg-gray-100 dark:bg-gray-700 text-base md:text-lg py-2 sm:py-0 items-center justify-center flex-col sm:flex-row z-[100]`\n      )}>\n      <div className=\"hidden sm:block\">{bannerText}</div>\n      <ExternalLink\n        className=\"ms-0 sm:ms-1 text-link dark:text-link-dark hover:underline\"\n        href={bannerLink}>\n        {bannerLinkText}\n      </ExternalLink>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/components/Tag.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport cn from 'classnames';\nimport type {RouteTag} from './Layout/getRouteMeta';\n\nconst variantMap = {\n  foundation: {\n    name: '기초',\n    classes: 'bg-yellow-50 text-white',\n  },\n  intermediate: {\n    name: '중급',\n    classes: 'bg-purple-40 text-white',\n  },\n  advanced: {\n    name: '심화',\n    classes: 'bg-green-40 text-white',\n  },\n  experimental: {\n    name: '실험적',\n    classes: 'bg-ui-orange text-white',\n  },\n  deprecated: {\n    name: '더 이상 사용되지 않음',\n    classes: 'bg-red-40 text-white',\n  },\n};\n\ninterface TagProps {\n  variant: RouteTag;\n  text?: string;\n  className?: string;\n}\n\nfunction Tag({text, variant, className}: TagProps) {\n  const {name, classes} = variantMap[variant];\n  return (\n    <span className={cn('me-2', className)}>\n      <span\n        className={cn(\n          'inline font-bold text-sm uppercase py-1 px-2 rounded',\n          classes\n        )}>\n        {text || name}\n      </span>\n    </span>\n  );\n}\n\nexport default Tag;\n"
  },
  {
    "path": "src/content/blog/2020/12/21/data-fetching-with-react-server-components.md",
    "content": "---\ntitle: \"제로 번들 사이즈 React 서버 컴포넌트를 소개합니다\"\nauthor: Dan Abramov, Lauren Tan, Joseph Savona, and Sebastian Markbage\ndate: 2020/12/21\ndescription: 2020년은 긴 한 해였습니다. 연말이 다가옴에 따라 제로 번들 사이즈의 React 서버 컴포넌트 연구에 대한 특별 연휴 업데이트를 공유하고자 합니다.\n---\n\n2020년 12월 21일, [Dan Abramov](https://twitter.com/dan_abramov), [Lauren Tan](https://twitter.com/potetotes), [Joseph Savona](https://twitter.com/en_JS), [Sebastian Markbåge](https://twitter.com/sebmarkbage)\n\n---\n\n<Intro>\n\n2020년은 긴 한 해였습니다. 연말이 다가옴에 따라 제로 번들 사이즈 **React 서버 컴포넌트** 연구에 대한 특별 연휴 업데이트를 공유하고자 합니다.\n\n</Intro>\n\n---\n\nReact 서버 컴포넌트를 소개하기 위해 강연과 데모를 준비했습니다. 이는 연휴 기간 또는 새해에 업무가 재개되는 시점에 확인할 수 있습니다.\n\n<YouTubeIframe src=\"https://www.youtube.com/embed/TQQPAU21ZUw\" />\n\n**React 서버 컴포넌트는 아직 연구 개발 단계입니다.** 투명성을 유지하고 React 커뮤니티로부터 초기 피드백을 받기 위해 작업을 공유하고 있습니다. 시간이 오래 걸릴 것이므로 **지금 당장 따라잡아야 한다고 느끼지 마세요!**\n\n이를 확인하기 위해 아래와 같은 순서를 따르는 것을 추천합니다.\n\n1. **강연**을 시청하여 React 서버 컴포넌트에 대해 학습하고 데모를 확인하세요.\n\n2. <strong>[데모를 클론](http://github.com/reactjs/server-components-demo)</strong>하여 컴퓨터에서 React 서버 컴포넌트를 사용해 보세요.\n\n3. 자세한 기술 분석과 피드백 제공을 위해 <strong>[RFC(FAQ가 마지막에 포함되어 있습니다)](https://github.com/reactjs/rfcs/pull/188)</strong>를 읽어보세요.\n\nRFC 또는 [@reactjs](https://twitter.com/reactjs) 트위터 계정에서 여러분의 의견을 기다리겠습니다. 즐거운 연말연시 안전하게 보내시고 내년에 뵙겠습니다!\n"
  },
  {
    "path": "src/content/blog/2021/06/08/the-plan-for-react-18.md",
    "content": "---\ntitle: \"React 18에 대한 계획\"\nauthor: Andrew Clark, Brian Vaughn, Christine Abernathy, Dan Abramov, Rachel Nabors, Rick Hanlon, Sebastian Markbage, and Seth Webster\ndate: 2021/06/08\ndescription: React 팀은 몇 가지 업데이트를 공유하게 되어 기쁩니다. 다음 주요 버전이 될 React 18 릴리즈에 대한 작업을 시작했습니다. 커뮤니티가 React 18의 새로운 기능을 점진적으로 채택할 수 있도록 준비하기 위해 워킹 그룹을 만들었습니다. 라이브러리 작성자가 사용해 보고 피드백을 제공할 수 있도록 React 18 Alpha를 게시했습니다...\n---\n\n2021년 6월 8일, [Andrew Clark](https://twitter.com/acdlite), [Brian Vaughn](https://github.com/bvaughn), [Christine Abernathy](https://twitter.com/abernathyca), [Dan Abramov](https://twitter.com/dan_abramov), [Rachel Nabors](https://twitter.com/rachelnabors), [Rick Hanlon](https://twitter.com/rickhanlonii), [Sebastian Markbåge](https://twitter.com/sebmarkbage), [Seth Webster](https://twitter.com/sethwebster)\n\n---\n\n<Intro>\n\nReact 팀은 몇 가지 업데이트를 공유하게 되어 기쁩니다.\n\n1. 다음번 주<sup>Major, 主</sup> 버전이 될 React 18 릴리즈에 대한 작업을 시작했습니다.\n2. 커뮤니티가 React 18의 새로운 기능을 점진적으로 채택할 수 있도록 준비하기 위해 워킹 그룹<sup>Working Group</sup>을 만들었습니다.\n3. 라이브러리 작성자가 사용해 보고 피드백을 제공할 수 있도록 React 18 Alpha를 게시했습니다.\n\n이번 업데이트는 주로 서드파티<sup>Third Party</sup> 라이브러리 관리자를 대상으로 합니다. React를 배우거나 가르치거나, 혹은 사용자 애플리케이션을 빌드하는 데 사용하는 경우 이 게시물을 무시해도 됩니다. 하지만 궁금한 점이 있으시다면 React 18 워킹 그룹의 토론에 참여하셔도 좋습니다!\n\n---\n\n</Intro>\n\n## React 18의 새로운 기능 {/*whats-coming-in-react-18*/}\n\nReact 18이 출시되면 [자동 일괄 처리<sup>Automatic Batching</sup>](https://github.com/reactwg/react-18/discussions/21)와 같은 기본 개선 사항 및 [`startTransition`](https://github.com/reactwg/react-18/discussions/41)과 같은 새로운 API, `React.lazy`를 기본적으로 지원하는 [새로운 스트리밍 서버 렌더러<sup>Streaming Server Renderer</sup>](https://github.com/reactwg/react-18/discussions/37)가 포함될 예정입니다.\n\n이러한 기능들은 React 18에 추가될 새로운 선택적<sup>Opt-In</sup> 메커니즘 덕분에 가능해졌습니다. 이를 \"동시성 렌더링<sup>Concurrent Rendering</sup>\"이라고 하며, 이 기능을 통해 React는 동시에 여러 버전의 UI를 준비할 수 있습니다. 이러한 변경 사항들은 대부분 직접 볼 수는 없지만, 앱의 실제 성능과 체감 성능을 모두 개선할 수 있는 새로운 가능성을 열어줍니다.\n\nReact의 미래에 대한 저희들의 연구를 계속 지켜보셨다면(물론, 그럴 필요는 없습니다!), \"동시성 모드<sup>Concurrent Mode</sup>\"라는 기능에 대해 들어보셨거나, 그것이 여러분들의 앱을 망칠 수 있다는 이야기를 들으셨을 수도 있습니다. 이러한 커뮤니티의 피드백을 반영하여 점진적인 도입을 위한 업그레이드 전략을 재설계했습니다. \"모드\"를 모두 사용하거나 사용하지 않는 대신, 동시성 렌더링<sup>Concurrent Rendering</sup>은 새로운 기능 중 하나에 의해 트리거되는 업데이트에 대해서만 활성화됩니다. 즉, **재작성 없이 React 18을 도입하고 자신의 속도에 맞춰 새로운 기능들을 사용해 볼 수 있습니다**.\n\n## 점진적인 도입 전략 {/*a-gradual-adoption-strategy*/}\n\nReact 18의 동시성은 선택적<sup>Opt-In</sup>이므로 컴포넌트 동작에 대한 중요 변경 사항<sup>Breaking Changes</sup>은 없습니다. **애플리케이션 코드를 거의 또는 전혀 변경하지 않고도, 일반적인 주요 React 릴리즈와 비슷한 수준의 노력으로 React 18로 업그레이드할 수 있습니다**. 여러 앱을 React 18로 전환한 경험에 비추어 볼 때, 많은 사용자가 반나절 안에 업그레이드할 수 있을 것으로 예상합니다.\n\n우리는 페이스북<sup>Facebook</sup>에서 수만 개의 컴포넌트에 동시성 기능들을 성공적으로 배포했으며, 경험상 대부분의 React 컴포넌트가 추가 변경 없이 \"바로 작동\"하였습니다. 커뮤니티 전체를 위한 원활한 업그레이드가 될 수 있도록 최선을 다하고 있으며, 오늘 React 18 워킹 그룹을 발표합니다.\n\n## 커뮤니티와의 협력 {/*working-with-the-community*/}\n\n이번 릴리즈에서는 새로운 시도를 하고 있습니다. React 커뮤니티의 전문가, 개발자, 라이브러리 작성자, 교육자들로 구성된 패널을 [React 18 워킹 그룹](https://github.com/reactwg/react-18)에 초대하여 피드백을 제공하고, 질문하고, 릴리즈에 대해 협업할 수 있도록 했습니다. 이번 소규모 그룹에 원하는 모든 분들을 초대할 수는 없었지만, 이번 실험이 성공하여 앞으로 더 많은 분을 초대할 수 있기를 바랍니다!\n\n**React 18 워킹 그룹의 목표는 기본 애플리케이션과 라이브러리가 React 18을 원활하고 점진적으로 채택할 수 있는 생태계를 준비하는 것입니다.** 워킹 그룹은 [깃허브 토론<sup>GitHub Discussions</sup>](https://github.com/reactwg/react-18/discussions)에서 호스팅되며, 일반인도 열람할 수 있습니다. 워킹 그룹의 구성원은 피드백을 남기고, 질문하고, 아이디어를 공유할 수 있습니다. 핵심 팀도 토론 저장소<sup>Repository</sup>를 사용하여 연구 결과를 공유할 것입니다. 안정적인 버전의 출시가 가까워지면, 중요 정보들을 블로그에 게시할 것입니다.\n\nReact 18로 업그레이드하는 방법이나, 릴리즈에 대한 추가적인 정보들은 [React 18 발표 게시물](https://github.com/reactwg/react-18/discussions/4)을 참고하세요.\n\n## React 18 워킹 그룹에 접근하기 {/*accessing-the-react-18-working-group*/}\n\n누구나 [React 18 워킹 그룹 저장소](https://github.com/reactwg/react-18)에서 토론 내용을 읽을 수 있습니다.\n\n워킹 그룹에 대한 초기 관심이 급증할 것으로 예상되므로, 초대받은 회원만 스레드를 만들거나 댓글을 달 수 있습니다. 그러나, 토론글은 모든 사람에게 완전히 공개되므로 모든 사람이 동일한 정보에 접근할 수 있습니다. 이는 워킹 그룹 구성원을 위한 생산적인 환경을 조성하는 동시에 더 많은 커뮤니티와의 투명성을 유지하는 좋은 절충안이라 생각합니다.\n\n언제나 그렇듯이 [이슈 트래커](https://github.com/facebook/react/issues)에 버그 보고서, 질문 및 일반적인 피드백을 제출할 수 있습니다.\n\n## 지금 React 18 Alpha를 사용하는 방법 {/*how-to-try-react-18-alpha-today*/}\n\n새로운 Alpha는 [정기적으로 `@alpha` 태그를 통해 npm에 배포됩니다](https://github.com/reactwg/react-18/discussions/9). 이러한 릴리즈는 메인 저장소<sup>Main Repo</sup>에 대한 가장 최근 커밋을 사용하여 빌드됩니다. 기능 혹은 버그 수정이 병합되면 그 다음주에 Alpha로 배포됩니다.\n\nAlpha 릴리즈 사이에는 중요한 동작 또는 API 변경이 있을 수 있습니다. **Alpha 릴리즈는 사용자를 대상으로 하는 프로덕션 애플리케이션에 권장하지 않는다는 점**을 기억하세요.\n\n## 예상 React 18 릴리즈 일정 {/*projected-react-18-release-timeline*/}\n\n구체적인 릴리즈 날짜는 예정되어 있지 않지만, 대부분의 프로덕션 애플리케이션에서 React 18을 사용할 수 있게 되려면 몇 달 동안 피드백과 반복 작업을 거쳐야 할 것으로 예상합니다.\n\n* 라이브러리 Alpha: 오늘 사용 가능\n* 공개 베타: 최소 몇 개월\n* 릴리즈 후보 (RC): Beta 출시 후 최소 몇 주 후\n* 일반 사용 가능: RC 이후 최소 몇 주 후\n\n예상 릴리즈 일정에 대한 자세한 내용은 [워킹 그룹에서 확인](https://github.com/reactwg/react-18/discussions/9)할 수 있습니다. 공개 릴리즈에 가까워지면 이 블로그에 업데이트를 게시하겠습니다.\n"
  },
  {
    "path": "src/content/blog/2021/12/17/react-conf-2021-recap.md",
    "content": "---\ntitle: \"React Conf 2021 요약\"\nauthor: Jesslyn Tannady and Rick Hanlon\ndate: 2021/12/17\ndescription: 지난주, 6번째 React Conf를 개최했습니다. 지난 몇 년 동안 React Conf 무대를 통해 React Native, React Hook과 같은 업계 변화를 알리는 발표를 했습니다. 올해는 React 18의 출시 및 동시성 기능의 점진적 도입을 시작으로 React의 멀티 플랫폼 비전을 공유했습니다.\n---\n\n2021년 12월 17일, [Jesslyn Tannady](https://twitter.com/jtannady), [Rick Hanlon](https://twitter.com/rickhanlonii)\n\n---\n\n<Intro>\n\n지난주, 6번째 React Conf를 개최했습니다. 지난 몇 년 동안 React Conf 무대를 통해 [_React Native_](https://engineering.fb.com/2015/03/26/android/react-native-bringing-modern-web-techniques-to-mobile/), [_React Hook_](https://reactjs.org/docs/hooks-intro.html)과 같은 업계 변화를 알리는 발표를 했습니다. 올해는 React 18의 출시 및 동시성 기능의 점진적 도입을 시작으로 React의 멀티 플랫폼 비전을 공유했습니다.\n\n</Intro>\n\n---\n\nReact Conf가 온라인으로 개최된 것은 이번이 처음이며, 8개 언어로 번역되어 무료로 스트리밍되었습니다. 전 세계의 참가자들은 다양한 시간대에서의 참여를 위해 컨퍼런스 Discord와 리플레이 이벤트를 이용했습니다. 50,000명 이상이 등록했으며, 19개 강연의 조회수는 60,000회를 넘었고, 두 이벤트에 걸쳐 5,000명이 Discord에 참여했습니다.\n\n모든 강연은 [온라인 스트리밍이 가능](https://www.youtube.com/watch?v=FZ0cG47msEk&list=PLNG_1j3cPCaZZ7etkzWA7JfdmKWT0pMsa)합니다.\n\n무대에서 공유된 내용을 요약하였습니다.\n\n## React 18 및 동시성 기능 {/*react-18-and-concurrent-features*/}\n\n기조연설에서 React 18을 시작으로 React의 미래에 대한 비전을 공유했습니다.\n\nReact 18은 오랫동안 기다려온 동시성 렌더러<sup>Concurrent Renderer</sup>를 추가하고 Suspense를 주<sup>Major, 主</sup><sub>(역자. Breaking Change를 일으키는)</sub> 변경 없이 업데이트했습니다. 앱은 React 18로 업그레이드하여 다른 주<sup>Major, 主</sup> 릴리즈와 동등한 수준의 노력으로 동시성 기능을 점진적으로 도입할 수 있습니다.\n\n**이는 동시성 모드가 없고 동시성 기능만 있음을 의미합니다.**\n\n기조연설에서는 Suspense, 서버 컴포넌트, 새로운 React 워킹 그룹에 대한 비전과 React Native에 대한 장기적인 멀티플랫폼 비전도 공유했습니다.\n\n[Andrew Clark](https://twitter.com/acdlite), [Juan Tejada](https://twitter.com/_jstejada), [Lauren Tan](https://twitter.com/potetotes), [Rick Hanlon](https://twitter.com/rickhanlonii)의 기조연설 전문을 아래에서 시청하세요.\n\n<YouTubeIframe src=\"https://www.youtube.com/embed/FZ0cG47msEk\" />\n\n## 애플리케이션 개발자를 위한 React 18 {/*react-18-for-application-developers*/}\n\n기조연설에서는 React 18 RC를 지금 바로 사용해볼 수 있다는 사실도 발표했습니다. 추가 피드백을 기다리는 중이며, 내년 초에 정식 버전으로 출시할 예정입니다.\n\nReact 18 RC를 사용해 보려면 의존성<sup>Dependencies</sup>을 업그레이드하세요.\n\n```bash\nnpm install react@rc react-dom@rc\n```\n\n그리고 새로운 `createRoot` API로 전환하세요.\n\n```js\n// before\nconst container = document.getElementById('root');\nReactDOM.render(<App />, container);\n\n// after\nconst container = document.getElementById('root');\nconst root = ReactDOM.createRoot(container);\nroot.render(<App/>);\n```\n\nReact 18 업그레이드 데모는 아래의 [Shruti Kapoor](https://twitter.com/shrutikapoor08)의 강연을 참조하세요.\n\n<YouTubeIframe src=\"https://www.youtube.com/embed/ytudH8je5ko\" />\n\n## Suspense가 있는 스트리밍 서버 렌더링 {/*streaming-server-rendering-with-suspense*/}\n\nReact 18에는 Suspense를 사용한 서버 측 렌더링<sup>SSR, Server Side Rendering</sup> 성능 개선 사항도 포함되어 있습니다.\n\n스트리밍 서버 렌더링을 사용하면, 서버의 React 컴포넌트에서 HTML을 생성하고 해당 HTML을 사용자에게 스트리밍할 수 있습니다. React 18에서는 `Suspense`를 사용하여 앱을 더 작은 독립 단위로 분해하여, 나머지 앱을 차단하지 않고 서로 독립적으로 스트리밍할 수 있습니다. 이는 사용자가 콘텐츠를 더 빨리 보고 훨씬 빠르게 상호작용할 수 있다는 것을 의미합니다.\n\n더 자세히 알아보려면 [Shaundai Person](https://twitter.com/shaundai)의 강연을 참조하세요:\n\n<YouTubeIframe src=\"https://www.youtube.com/embed/pj5N-Khihgc\" />\n\n## 첫 번째 React 워킹 그룹 {/*the-first-react-working-group*/}\n\nReact 18에서는 전문가, 개발자, 라이브러리 관리자, 교육자들로 구성된 패널과 협력하기 위한 첫 번째 워킹 그룹을 만들었습니다. 저희는 함께 점진적인 채택 전략을 세우고 `useId`, `useSyncExternalStore`, `useInsertionEffect`와 같은 새로운 API를 개선하기 위해 노력했습니다.\n\n해당 작업에 대한 개요는 [Aakansha' Doshi](https://twitter.com/aakansha1216)의 강연을 참조하세요.\n\n<YouTubeIframe src=\"https://www.youtube.com/embed/qn7gRClrC9U\" />\n\n## React 개발자 도구 {/*react-developer-tooling*/}\n\n이번 릴리즈의 새로운 기능을 지원하기 위해 새로 구성된 React 개발자 도구 팀과 개발자가 React 앱 디버깅에 도움이 되는 새로운 타임라인 프로파일러<sup>Timeline Profiler</sup>도 발표했습니다.\n\n새로운 개발자 도구 기능에 대한 자세한 내용과 데모는 [Brian Vaughn](https://twitter.com/brian_d_vaughn)의 강연을 참조하세요.\n\n<YouTubeIframe src=\"https://www.youtube.com/embed/oxDfrke8rZg\" />\n\n## memo 없는 React {/*react-without-memo*/}\n\n미래를 내다보며, [Xuan Huang(黄玄)](https://twitter.com/Huxpro)이 자동 메모화 컴파일러에 대한 React Labs 연구의 업데이트를 공유했습니다. 이 강연에서 자세한 정보와 컴파일러 프로토타입 데모를 확인하세요.\n\n<YouTubeIframe src=\"https://www.youtube.com/embed/lGEMwh32soc\" />\n\n## React 문서 기조연설 {/*react-docs-keynote*/}\n\n[Rachel Nabors](https://twitter.com/rachelnabors)가 React의 새로운 문서에 대한 투자와 관련한 기조연설로, React로 학습하고 디자인하는 방법에 대한 강연을 했습니다. ([현재 react.dev로 배포되었습니다.](/blog/2023/03/16/introducing-react-dev))\n\n<YouTubeIframe src=\"https://www.youtube.com/embed/mneDaMYOKP8\" />\n\n## 그리고... {/*and-more*/}\n\n**React로 학습하고 디자인하는 방법에 대한 강연**\n\n* Debbie O'Brien: [새로운 React 문서에서 배운 것들](https://youtu.be/-7odLW_hG7s).\n* Sarah Rainsberger: [브라우저에서 학습하기](https://youtu.be/5X-WEQflCL0).\n* Linton Ye: [React 디자인에서의 ROI](https://youtu.be/7cPWmID5XAk).\n* Delba de Oliveira: [React를 이용한 상호작용 놀이터](https://youtu.be/zL8cz2W0z34).\n\n**Relay, React Native, PyTorch 팀의 강연**\n\n* Robert Balicki: [Relay 재소개](https://youtu.be/lhVGdErZuN4).\n* Eric Rozell과 Steven Moyes: [React Native 데스크톱](https://youtu.be/9L4FFrvwJwY).\n* Roman Rädle: [React Native를 위한 온디바이스 머신러닝](https://youtu.be/NLj73vrc2I8).\n\n**접근성, 툴링 및 서버 컴포넌트에 대한 커뮤니티 강연**\n\n* Daishi Kato: [외부 스토어 라이브러리를 위한 React 18](https://youtu.be/oPfSC5bQPR8).\n* Diego Haz: [React 18에서 접근 가능한 컴포넌트 구축하기](https://youtu.be/dcm8fjBfro8).\n* Tafu Nakazaki: [React로 접근 가능한 일본어 폼 컴포넌트](https://youtu.be/S4a0QlsH0pU).\n* Lyle Troxell: [아티스트를 위한 UI 도구](https://youtu.be/b3l4WxipF).\n* Helen Lin: [Hydrogen + React 18](https://youtu.be/HS6vIYkSNks).\n\n## 감사드립니다 {/*thank-you*/}\n\n올해는 저희가 직접 컨퍼런스를 기획한 첫 해로, 많은 분들께 감사드리고 싶습니다.\n\n먼저, 모든 연사분들께 감사드립니다 [Aakansha Doshi](https://twitter.com/aakansha1216), [Andrew Clark](https://twitter.com/acdlite), [Brian Vaughn](https://twitter.com/brian_d_vaughn), [Daishi Kato](https://twitter.com/dai_shi), [Debbie O'Brien](https://twitter.com/debs_obrien), [Delba de Oliveira](https://twitter.com/delba_oliveira), [Diego Haz](https://twitter.com/diegohaz), [Eric Rozell](https://twitter.com/EricRozell), [Helen Lin](https://twitter.com/wizardlyhel), [Juan Tejada](https://twitter.com/_jstejada), [Lauren Tan](https://twitter.com/potetotes), [Linton Ye](https://twitter.com/lintonye), [Lyle Troxell](https://twitter.com/lyle), [Rachel Nabors](https://twitter.com/rachelnabors), [Rick Hanlon](https://twitter.com/rickhanlonii), [Robert Balicki](https://twitter.com/StatisticsFTW), [Roman Rädle](https://twitter.com/raedle), [Sarah Rainsberger](https://twitter.com/sarah11918), [Shaundai Person](https://twitter.com/shaundai), [Shruti Kapoor](https://twitter.com/shrutikapoor08), [Steven Moyes](https://twitter.com/moyessa), [Tafu Nakazaki](https://twitter.com/hawaiiman0), 그리고  [Xuan Huang (黄玄)](https://twitter.com/Huxpro).\n\n[Andrew Clark](https://twitter.com/acdlite), [Dan Abramov](https://twitter.com/dan_abramov), [Dave McCabe](https://twitter.com/mcc_abe), [Eli White](https://twitter.com/Eli_White), [Joe Savona](https://twitter.com/en_JS), [Lauren Tan](https://twitter.com/potetotes), [Rachel Nabors](https://twitter.com/rachelnabors), [Tim Yung](https://twitter.com/yungsters) 등 대담에 피드백을 제공해 주신 모든 분들께 감사드립니다.\n\n디스코드 컨퍼런스를 개설하고 디스코드 관리자로 활동해 주신 [Lauren Tan](https://twitter.com/potetotes)에게 감사드립니다.\n\n전반적인 방향에 대한 피드백을 제공하고 다양성과 포용성에 집중할 수 있도록 도와주신 [Seth Webster](https://twitter.com/sethwebster)에게 감사드립니다.\n\n사회를 진행하신 [Rachel Nabors](https://twitter.com/rachelnabors)와 사회 진행 가이드를 만들고, 사회 진행 팀을 이끌고, 번역가와 사회자를 교육하고, 두 이벤트의 사회 진행을 도와주신 [Aisha Blake](https://twitter.com/AishaBlake)께도 감사드립니다.\n\n사회자 [Jesslyn Tannady](https://twitter.com/jtannady), [Suzie Grange](https://twitter.com/missuze), [Becca Bailey](https://twitter.com/beccaliz), [Luna Wei](https://twitter.com/lunaleaps), [Joe Previte](https://twitter.com/jsjoeio), [Nicola Corti](https://twitter.com/Cortinico), [Gijs Weterings](https://twitter.com/gweterings), [Claudio Procida](https://twitter.com/claudiopro), Julia Neumann, Mengdi Chen, Jean Zhang, Ricky Li 및 [Xuan Huang (黄玄)](https://twitter.com/Huxpro)께 감사드립니다.\n\n리플레이 이벤트의 진행을 도와주시고 커뮤니티의 참여를 이끌어주신 [React India](https://www.reactindia.io/)의 [Manjula Dube](https://twitter.com/manjula_dube), [Sahil Mhapsekar](https://twitter.com/apheri0), [React China](https://twitter.com/ReactChina)의 [Jasmine Xie](https://twitter.com/jasmine_xby), [QiChang Li](https://twitter.com/QCL15), [YanLun Li](https://twitter.com/anneincoding)께도 감사의 말씀을 전합니다.\n\n컨퍼런스 웹사이트의 기반이 된 [가상 이벤트 스타터 키트](https://vercel.com/virtual-event-starter-kit)를 게시해주신 Vercel과 Next.js Conf 운영 경험을 공유해주신 [Lee Robinson](https://twitter.com/leeerob)과 [Delba de Oliveira](https://twitter.com/delba_oliveira)께 감사드립니다.\n\n컨퍼런스를 운영한 경험, [RustConf](https://rustconf.com/)를 운영하면서 얻은 교훈, [Event Driven](https://leanpub.com/eventdriven/)과 컨퍼런스를 운영하기 위한 조언을 공유해주신 [Leah Silber](https://twitter.com/wifelette)께 감사드립니다.\n\nWomen of React Conf를 운영한 경험을 공유해주신 [Kevin Lewis](https://twitter.com/_phzn)와 [Rachel Nabors](https://twitter.com/rachelnabors)께 감사드립니다.\n\n기획 전반에 걸쳐 조언과 아이디어를 제공해주신 [Aakansha Doshi](https://twitter.com/aakansha1216), [Laurie Barth](https://twitter.com/laurieontech), [Michael Chan](https://twitter.com/chantastic), [Shaundai Person](https://twitter.com/shaundai)께 감사드립니다.\n\n컨퍼런스 웹사이트와 티켓을 디자인하고 구축하는 데 도움을 주신 [Dan Lebowitz](https://twitter.com/lebo)께 감사드립니다.\n\n기조연설과 Meta 직원 대담의 동영상을 녹화해주신 Facebook 동영상 프로덕션 팀의 Laura Podolak Waddell, Desmond Osei-Acheampong, Mark Rossi, Josh Toberman 및 기타 직원들께도 감사드립니다.\n\n컨퍼런스를 구성하고, 스트림의 모든 동영상을 편집하고, 모든 강연을 번역하고, 여러 언어로 Discord를 진행하는 데 도움을 주신 파트너인 HitPlay께도 감사드립니다.\n\n마지막으로, 멋진 React 컨퍼런스를 만들어주신 모든 참가자 여러분께 감사드립니다!\n"
  },
  {
    "path": "src/content/blog/2022/03/08/react-18-upgrade-guide.md",
    "content": "---\ntitle: \"React 18로 업그레이드하는 방법\"\nauthor: Rick Hanlon\ndate: 2022/03/08\ndescription: React 18은 릴리스 노트에서 언급한 대로 새로운 동시성 렌더러를 도입하여 기존 애플리케이션에 점진적으로 적용할 계획입니다. 이 글에서는 React 18로 업그레이드하는 방법을 단계별로 소개하겠습니다.\n---\n\n2022년 3월 8일, [Rick Hanlon](https://twitter.com/rickhanlonii)\n\n---\n\n<Intro>\n\nReact 18은 [릴리스 노트](/blog/2022/03/29/react-v18)에서 언급한 대로 새로운 동시성 렌더러<sup>Concurrent Renderer</sup>를 도입하여 기존 애플리케이션에 점진적으로 적용할 계획입니다. 이 글에서는 React 18로 업그레이드하는 방법을 단계별로 소개하겠습니다.\n\nReact 18로 업그레이드하는 과정에서 발생하는 [문제를 알려주세요](https://github.com/facebook/react/issues/new/choose).\n\n</Intro>\n\n<Note>\n\nReact Native 사용자의 경우, React 18은 React Native의 향후 버전에 탑재될 것입니다. 이는 새로운 기능을 활용하기 위해 React 18이 이 글에서 소개되는 새로운 React Native 아키텍처에 의존하기 때문입니다. 자세한 정보는 [React Conf 기조연설](https://www.youtube.com/watch?v=FZ0cG47msEk&t=1530s)을 확인해주세요.\n\n</Note>\n\n---\n\n## 설치 {/*installing*/}\n\n최신 버전의 React를 설치하기 위해 다음 명령어를 입력하세요.\n\n```bash\nnpm install react react-dom\n```\n\n`yarn`을 사용한다면 다음 명령어를 입력하세요.\n\n```bash\nyarn add react react-dom\n```\n\n## 클라이언트 렌더링 API 업데이트 {/*updates-to-client-rendering-apis*/}\n\nReact 18을 처음 설치하면 콘솔 창에 다음과 같은 경고 메시지가 표시될 것입니다.\n\n<ConsoleBlock level=\"error\">\n\nReactDOM.render is no longer supported in React 18. Use createRoot instead. Until you switch to the new API, your app will behave as if it's running React 17. Learn more: https://reactjs.org/link/switch-to-createroot\n\n</ConsoleBlock>\n\nReact 18은 사용자들이 더 편리하게 Root 요소들을 관리할 수 있는 최신 Root API를 도입했습니다. 이에 더불어 최신 Root API는 새로운 동시성 렌더러를 동작시켜 동시성 기능<sup>Concurrent Features</sup>을 사용할 수 있게 합니다.\n\n```js\n// 변경 전\nimport { render } from 'react-dom';\nconst container = document.getElementById('app');\nrender(<App tab=\"home\" />, container);\n\n// 변경 후\nimport { createRoot } from 'react-dom/client';\nconst container = document.getElementById('app');\nconst root = createRoot(container); // 만약 TypeScript를 사용한다면 createRoot(container!)\nroot.render(<App tab=\"home\" />);\n```\n\n`unmountComponentAtNode`는 `root.unmount`로 변경되었습니다.\n\n```js\n// 변경 전\nunmountComponentAtNode(container);\n\n// 변경 후\nroot.unmount();\n```\n\nSuspense를 사용할 때 일반적으로 기대한 결과가 나오지 않기 때문에 콜백 함수를 렌더링 메서드에서 제거했습니다.\n\n```js\n// 변경 전\nconst container = document.getElementById('app');\nrender(<App tab=\"home\" />, container, () => {\n  console.log('rendered');\n});\n\n// 변경 후\nfunction AppWithCallbackAfterRender() {\n  useEffect(() => {\n    console.log('rendered');\n  });\n\n  return <App tab=\"home\" />\n}\n\nconst container = document.getElementById('app');\nconst root = createRoot(container);\nroot.render(<AppWithCallbackAfterRender />);\n```\n\n<Note>\n\n옛 렌더링 콜백 API에 정확히 대응하는 대안은 없으며, 사용자의 사용 방식에 따라 다를 수 있습니다. 자세한 정보는 현재 진행 중인 그룹 포스트인 [Replacing render with `createRoot`](https://github.com/reactwg/react-18/discussions/5)를 확인할 수 있습니다.\n\n</Note>\n\n마지막으로, Hydration으로 서버 측 렌더링<sup>SSR, Server Side Rendering</sup>을 이용할 경우에는 `hydrate`를 `hydrateRoot`로 업그레이드하세요.\n\n```js\n// 변경 전\nimport { hydrate } from 'react-dom';\nconst container = document.getElementById('app');\nhydrate(<App tab=\"home\" />, container);\n\n// 변경 후\nimport { hydrateRoot } from 'react-dom/client';\nconst container = document.getElementById('app');\nconst root = hydrateRoot(container, <App tab=\"home\" />);\n// createRoot와는 달리, root.render()를 별도로 호출할 필요가 없습니다.\n```\n\n자세한 정보는 [진행 중인 토론](https://github.com/reactwg/react-18/discussions/5)에서 확인할 수 있습니다.\n\n<Note>\n\n**업그레이드 이후에도 앱이 작동하지 않는다면 `<StrictMode>`로 감싸여 있지 않은지 확인해 보세요.** [React 18에서는 Strict 모드가 더 엄격해져서](#updates-to-strict-mode), 모든 컴포넌트가 개발 모드의 추가된 검사 항목을 만족하지 못할 수 있습니다. Strict 모드를 제거함으로써 문제를 해결할 수 있다면, 업그레이드 중에 제거한 다음, 통합 개발 환경이나 코드 편집기가 표시하는 오류들을 해결한 후에 (트리의 상단 또는 일부분에) 다시 추가할 수 있습니다.\n\n</Note>\n\n## Updates to Server Rendering APIs {/*updates-to-server-rendering-apis*/}\n\n이번 릴리즈에서는 서버와 스트리밍 SSR에서 Suspense를 최대한 지원하기 위해 `react-dom/server` API를 개편했습니다. 이번 개편을 통해, 서버에서 점진적인 Suspense 스트리밍을 지원하지 않는 구형 Node 스트리밍 API를 더 이상 사용하지 않게 되었습니다.\n\n이 API를 사용하면 다음과 같은 경고가 표시됩니다.\n\n* `renderToNodeStream`: **더 이상 사용되지 않음 ⛔️️**\n\n대신에 Node 환경에서 스트리밍하려면 다음 API를 사용하세요.\n* `renderToPipeableStream`: **새로 추가됨 ✨**\n\n더불어 Deno나 Cloudflare workers와 같은 최신 런타임 환경에서 Suspense를 활용하여 스트리밍 SSR를 지원하기 위해 새로운 API를 도입합니다.\n* `renderToReadableStream`: **새로 추가됨 ✨**\n\n다음 API들은 계속 동작하긴 하지만, Suspense 지원이 제한될 것입니다.\n* `renderToString`: **제한됨** ⚠️\n* `renderToStaticMarkup`: **제한됨** ⚠️\n\n마지막으로, 이 API는 앞으로도 이메일을 렌더링할 예정입니다.\n* `renderToStaticNodeStream`\n\n서버 렌더링 API 변경 사항과 관련된 자세한 정보는 작업 중인 그룹 포스트 [Upgrading to React 18 on the server](https://github.com/reactwg/react-18/discussions/22), [deep dive on the new Suspense SSR Architecture](https://github.com/reactwg/react-18/discussions/37), 그리고 React Conf 2021에서 [Shaundai Person](https://twitter.com/shaundai)이 발표한 [Streaming Server Rendering with Suspense](https://www.youtube.com/watch?v=pj5N-Khihgc)에서 확인할 수 있습니다.\n\n## TypeScript 사용 시 타입 작성에 대한 업데이트 {/*updates-to-typescript-definitions*/}\n\n프로젝트에 TypeScript를 사용하고 있다면, `@types/react`와 `@types/react-dom` 의존성을 최신 버전으로 업데이트해야 합니다. 새로운 타입들을 사용하면 더 안전하게 작업할 수 있으며, 기존의 타입 검사가 감지하지 못했던 이슈들을 찾아낼 수 있습니다. 가장 눈에 띄는 변화는 `children` 프로퍼티를 명확한 리스트로 정의해야 한다는 것입니다. 아래 예시를 확인해보세요.\n\n```typescript{3}\ninterface MyButtonProps {\n  color: string;\n  children?: React.ReactNode;\n}\n```\n\n[React 18 typings pull request](https://github.com/DefinitelyTyped/DefinitelyTyped/pull/56210)에서 타입 변경 내용을 전체적으로 확인할 수 있습니다. 이 링크는 라이브러리 타입 수정 내용을 담고 있어 코드를 어떻게 수정해야 하는지 확인할 수 있습니다. [automated migration script](https://github.com/eps1lon/types-react-codemod)를 사용하여 애플리케이션 코드에 최신이고 더 안전한 타입을 빠르게 적용할 수 있습니다.\n\n타이핑에 버그를 발견하면 DefinitelyTyped 저장소에 [이슈를 보내주세요](https://github.com/DefinitelyTyped/DefinitelyTyped/discussions/new?category=issues-with-a-types-package).\n\n## 자동 Batching {/*automatic-batching*/}\n\n\nReact 18은 자동으로 더 많은 Batching을 수행하여 놀라운 성능 향상을 이뤘습니다. Batching이란 더 나은 성능을 위해 여러 개의 상태 업데이트를 단 한 번의 리렌더링으로 처리하는 것을 말합니다. React 18 이전에는 React 이벤트 핸들러 내에서의 상태 업데이트만 Batching을 수행해 왔습니다. 그러나 프로미스, `setTimeout`, 네이티브 이벤트 핸들러 또는 다른 이벤트들은 React에서 기본적으로 Batching을 수행하지 않았죠. 다음 예시를 확인하세요.\n\n```js\n// React 18 이전에는 오직 React 이벤트들만 Batch되었습니다.\n\nfunction handleClick() {\n  setCount(c => c + 1);\n  setFlag(f => !f);\n  // React는 마지막에 단 한 번만 리렌더링합니다. (그게 Batching이죠!)\n}\n\nsetTimeout(() => {\n  setCount(c => c + 1);\n  setFlag(f => !f);\n  // React는 각 상태 업데이트마다 한 번씩, 총 두 번 렌더링합니다. (Batching 없음)\n}, 1000);\n```\n\nReact 18를 시작으로 `createRoot`를 사용하여 모든 업데이트가 자동으로 Batch될 것입니다. 이는 `setTimeout`, 프로미스, 네이티브 이벤트 핸들러 또는 다른 이벤트 내의 업데이트도 React 이벤트 내의 업데이트와 같이 Batch 될 것임을 의미합니다.\n\n```js\n// React 18 이후로 setTimeout, 프로미스,\n// 네이티브 이벤트 핸들러 또는 다른 이벤트 내의 업데이트도 Batch 됩니다.\n\nfunction handleClick() {\n  setCount(c => c + 1);\n  setFlag(f => !f);\n  // React는 마지막에 단 한 번만 리렌더링합니다. (그게 Batching이죠!)\n}\n\nsetTimeout(() => {\n  setCount(c => c + 1);\n  setFlag(f => !f);\n  // React는 마지막에 단 한 번만 리렌더링합니다. (그게 Batching이죠!)\n}, 1000);\n```\n\n이는 엄청난 변화이지만, 이를 통해 렌더링 작업에 수고를 줄일 수 있고, 애플리케이션의 성능도 향상될 것으로 예상합니다. 자동 Batching을 원하지 않는 경우, `flushSync`를 사용할 수도 있습니다.\n\n```js\nimport { flushSync } from 'react-dom';\n\nfunction handleClick() {\n  flushSync(() => {\n    setCounter(c => c + 1);\n  });\n  // 이 시점에서 React는 DOM을 업데이트합니다.\n  flushSync(() => {\n    setFlag(f => !f);\n  });\n  // 이 시점에서 React는 DOM을 업데이트합니다.\n}\n```\n\n자세한 정보는 [Automatic batching deep dive](https://github.com/reactwg/react-18/discussions/21)에서 확인할 수 있습니다.\n\n## 라이브러리를 위한 새로운 API {/*new-apis-for-libraries*/}\n\nReact 18 워킹 그룹은 스타일이나 외부 저장 장치와 같은 특정 목적을 위한 동시성 렌더링을 지원하기 위해 라이브러리 관리자들과 협력하여 새로운 API를 개발했습니다. React 18을 지원하기 위해 일부 라이브러리들은 다음 API 중 하나로 변경해야 할 수도 있습니다.\n\n* `useSyncExternalStore`는 외부 저장 장치의 업데이트를 실시간으로 반영하여 외부 저장 장치가 동시성 불러오기<sup>Concurrent Read</sup>를 지원할 수 있도록 하는 새로운 Hook입니다. 이 API는 React 외부의 상태를 포함하는 라이브러리에서 사용하는 것을 추천합니다. 자세한 정보는 [`useSyncExternalStore` overview post](https://github.com/reactwg/react-18/discussions/70)와 [`useSyncExternalStore` API details](https://github.com/reactwg/react-18/discussions/86)에서 확인할 수 있습니다.\n\n* `useInsertionEffect`는 CSS-in-JS 라이브러리가 렌더링 시 스타일 주입과 같은 성능 이슈를 개선하는 새로운 Hook입니다. 이미 CSS-in-JS 라이브러리를 만든 경우가 아니라면 이 Hook을 사용할 필요는 없을 것입니다. 이 Hook은 DOM이 변경된 후 실행되지만, 레이아웃 이펙트가 새로운 레이아웃을 읽어 들이기 전에 실행됩니다. 자세한 정보는 [Library Upgrade Guide for `<style>`](https://github.com/reactwg/react-18/discussions/110)에서 확인할 수 있습니다.\n\n또, React 18은 동시성 렌더링을 위해 `startTransition`, `useDeferredValue`, 그리고 `useId`와 같은 새로운 API를 도입했습니다. 더 자세한 내용은 [릴리즈 게시물](/blog/2022/03/29/react-v18)에서 확인할 수 있습니다.\n\n## Strict 모드 업데이트 {/*updates-to-strict-mode*/}\n\n차후에는 React에 새로운 기능을 추가하여 UI의 섹션을 추가하거나 제거할 수 있게 할 계획입니다. 예를 들면, 사용자가 뒤로 가기를 누르면 React가 즉시 이전 화면을 보여줄 수 있도록 하는 거죠. 이를 위해 React는 이전과 동일한 컴포넌트 상태를 사용하여 트리를 마운트 해제하고 다시 마운트할 것입니다.\n\n이 기능은 React의 성능을 대폭 향상할 것이지만, 컴포넌트들이 이펙트가 여러 번 마운트되었다가 사라지는 것에 아무런 영향을 받지 않아야 합니다. 대부분의 이펙트들은 변함없이 계속 작동하겠지만, 어떤 것들은 단 한 번만 마운트되고 사라질 수 있도록 설계되어 있습니다.\n\nReact 18은 이러한 문제를 시각적으로 보여주기 위해 Strict 모드에 개발 전용 검사를 새롭게 도입했습니다. 이 검사는 컴포넌트가 처음 마운트될 때 자동으로 마운트 해제한 다음, 이전 상태를 복원하면서 다시 마운트할 것입니다.\n\n이번 업데이트 이전에는 React가 다음과 같이 컴포넌트를 마운트하고 이펙트를 생성했습니다.\n\n```\n* React가 컴포넌트를 마운트합니다.\n    * 레이아웃 이펙트가 생성됩니다.\n    * Effect 이펙트가 생성됩니다.\n```\n\nReact 18의 Strict 모드에서는 개발 전용 모드에서 React가 컴포넌트를 마운트 해제하고 다시 마운트하는 것을 시뮬레이션합니다.\n\n```\n* React가 컴포넌트를 마운트합니다.\n    * 레이아웃 이펙트가 생성됩니다.\n    * Effect 이펙트가 생성됩니다.\n* React가 컴포넌트를 마운트 해제하는 것을 시뮬레이션합니다.\n    * 레이아웃 이펙트가 사라집니다.\n    * 이펙트가 사라집니다.\n* React는 컴포넌트를 이전 상태로 다시 마운트하는 것을 시뮬레이션합니다.\n    * 레이아웃 이펙트 셋업 코드가 실행됩니다.\n    * Effect 셋업 코드가 실행됩니다.\n```\n\n자세한 정보는 워킹 그룹 포스트인 [Adding Reusable State to StrictMode](https://github.com/reactwg/react-18/discussions/19)와 [How to support Reusable State in Effects](https://github.com/reactwg/react-18/discussions/18)에서 확인할 수 있습니다.\n\n## 테스트 환경 구축하기 {/*configuring-your-testing-environment*/}\n\n`createRoot`를 사용하기 위해 처음으로 테스트를 업데이트하면 테스트 콘솔 창에서 다음과 같은 경고 메시지가 표시될 수 있습니다.\n\n<ConsoleBlock level=\"error\">\n\nThe current testing environment is not configured to support act(...)\n\n</ConsoleBlock>\n\n이를 해결하려면 테스트를 실행하기 전에 `globalThis.IS_REACT_ACT_ENVIRONMENT`값을 `true`로 설정하세요.\n\n```js\n// 테스트 셋업 파일\nglobalThis.IS_REACT_ACT_ENVIRONMENT = true;\n```\n\n이 변수는 React에 유닛 테스트와 유사한 환경에서 실행 중임을 알리기 위함입니다. 업데이트된 사항을 `act`로 감싸지 않으면 React가 유용한 경고 메시지들을 표시합니다.\n\nReact에 `act`가 필요 없다고 알리려면 해당 변수를 `false`로 설정할 수 있습니다. 이렇게 하면 전체 브라우저 환경을 시뮬레이션하는 E2E<sup>End-to-End</sup> 테스트에 유용하게 사용할 수 있습니다.\n\n결국에는 테스트 라이브러리가 이를 자동으로 설정해줄 것으로 예상합니다. 예를 들어, [React Testing Library의 다음 버전은 추가적인 설정 없이도 React 18을 지원하는 기능이 내장되어 있습니다](https://github.com/testing-library/react-testing-library/issues/509#issuecomment-917989936).\n\n[`act` 테스트 API와 관련 변경 사항에 대한 추가 정보](https://github.com/reactwg/react-18/discussions/102)는 워킹 그룹에서 확인할 수 있습니다.\n\n## Internet Explorer 지원 중단 {/*dropping-support-for-internet-explorer*/}\n\n이번 배포에서 React는 [2022년 6월 15일에 지원이 종료되는](https://blogs.windows.com/windowsexperience/2021/05/19/the-future-of-internet-explorer-on-windows-10-is-in-microsoft-edge) Internet Explorer의 지원을 중단합니다.\n이러한 변경 사항을 적용하는 이유는 React 18에서 도입된 새로운 기능들이 IE에서 적절하게 폴리필 할 수 없는 마이크로 태스크와 같은 모던 브라우저 기능을 사용하기 때문입니다.\n\nInternet Explorer를 지원해야 하는 경우 React 17을 사용하는 것을 권장합니다.\n\n## 더 이상 사용되지 않습니다 {/*deprecations*/}\n\n* `react-dom`: `ReactDOM.render`가 더 이상 사용되지 않습니다. 사용하면 경고가 표시되고 앱이 React 17 모드로 실행됩니다.\n* `react-dom`: `ReactDOM.hydrate`가 더 이상 사용되지 않습니다. 사용하면 경고가 표시되고 앱이 React 17 모드로 실행됩니다.\n* `react-dom`: `ReactDOM.unmountComponentAtNode`가 더 이상 사용되지 않습니다.\n* `react-dom`: `ReactDOM.renderSubtreeIntoContainer`가 더 이상 사용되지 않습니다.\n* `react-dom/server`: `ReactDOMServer.renderToNodeStream`이 더 이상 사용되지 않습니다.\n\n## 이 밖의 주요한 변화<sup>Breaking Changes</sup> {/*other-breaking-changes*/}\n\n* **일관된 `useEffect` 타이밍**: 클릭이나 Keydown 이벤트와 같은 개별적인 사용자 입력 이벤트 중에 업데이트가 트리거된 경우 React는 항상 동기적으로 이펙트 함수를 플러시합니다. 이전에는 동작이 언제나 예측 가능하거나 일관적이지 않았습니다.\n* **Hydration 오류 강화**: 텍스트 콘텐츠가 누락되거나 추가되어 Hydration 불일치가 발생하는 경우, 이제 경고가 아닌 오류로 처리됩니다. React는 더 이상 서버 마크업과 일치시키기 위해 클라이언트에서 노드를 삽입하거나 삭제하여 개별 노드를 \"패치 업\" 하려고 시도하지 않으며, 트리에서 가장 가까운 `<Suspense>` 바운더리까지 클라이언트 렌더링으로 되돌립니다. 이렇게 하면 Hydration 트리의 일관성을 유지하고 Hydration 불일치로 인해 발생할 수 있는 잠재적인 개인정보 보호 및 보안 허점을 방지할 수 있습니다.\n* **항상 일관성을 유지하는 Suspense 트리**: 컴포넌트가 트리에 완전히 추가되기 전에 일시 중단되면 React는 불완전한 상태로 트리에 추가하거나 그 이펙트를 실행하지 않습니다. 대신 React는 새 트리를 완전히 버리고 비동기 작업이 완료될 때까지 기다린 다음 처음부터 다시 렌더링을 시도합니다. React는 브라우저를 블로킹하지 않고 다시 시도해 동시에 렌더링합니다.\n* **Suspense가 있는 레이아웃 효과**: 트리가 다시 일시 중단되고 Fallback으로 되돌아갈 때, React는 레이아웃 효과를 정리<sup>Clean Up</sup>한 후 경계 안의 콘텐츠가 다시 표시될 때 레이아웃 효과를 다시 생성합니다. 이는 Suspense와 함께 사용할 때 컴포넌트 라이브러리가 레이아웃을 올바르게 측정하지 못하던 문제를 해결합니다.\n* **새로운 JS 환경 요구 사항**: React는 이제 `Promise`, `Symbol`, `Object.assign`을 포함한 최신 브라우저 기능에 의존합니다. 최신 브라우저 기능을 기본적으로 제공하지 않거나 호환되지 않는 구현이 있는 Internet Explorer와 같은 구형 브라우저 및 기기를 지원하는 경우 번들된 애플리케이션에 전역 폴리필을 포함하는 것을 고려하세요.\n\n## 이 밖의 주목할 만한 변화 {/*other-notable-changes*/}\n\n### React {/*react*/}\n\n* **이제 컴포넌트들이 `undefined`를 렌더링할 수 있습니다.** React는 더 이상 컴포넌트에서 `undefined`를 반환할 때 경고하지 않습니다. 이렇게 함으로써 컴포넌트는 컴포넌트 트리의 중간에 허용된 값과 일관된 값을 반환하게 됩니다. JSX 앞에 `return` 문을 빼먹는 실수와 같은 문제를 방지하기 위해 린터<sup>Linter</sup>를 사용하는 것을 권장합니다.\n* **테스트에서 `act` 경고는 이제 선택 사항입니다.** E2E<sup>End-to-End</sup> 테스트를 실행하는 경우 `act` 경고 메시지는 불필요합니다. 이 경고 메시지가 쓸모 있는 [유닛 테스트에서만 활성화할 수 있도록](https://github.com/reactwg/react-18/discussions/102) 만들었습니다.\n* **이제 마운트가 해제된 컴포넌트에서 `setState` 관련 경고가 나타나지 않습니다.** 이전에는 `setState`를 마운트가 해제된 컴포넌트에서 호출할 때 메모리 누수에 대한 경고가 표시되었습니다. 이 경고는 구독을 위해 추가되었지만, 대부분의 경우 상태 설정에 문제가 없을 때 이 경고를 마주하게 되어 코드를 더 나쁘게 만드는 대안을 찾아야 했습니다. React 18에서는 이 경고를 [제거했습니다](https://github.com/facebook/react/pull/22114).\n* **더 이상 콘솔 로그를 억제하지 않습니다.** Strict 모드를 사용할 때 React는 예기치 않은 부작용을 감지하기 위해 각 컴포넌트를 두 번 렌더링합니다. React 17에서는 두 번째 렌더링의 콘솔 로그를 억제하여 로그를 더 쉽게 읽을 수 있게 했습니다. 그러나 [커뮤니티의 피드백](https://github.com/facebook/react/issues/21783)에 따라 이 억제를 제거했습니다. 이제 React DevTools가 설치되어 있다면 두 번째 로그의 렌더링이 회색으로 표시되며 완전히 억제하는 옵션도 사용할 수 있습니다. (기본적으로 꺼져 있음.)\n* **메모리 사용을 개선했습니다.** React는 이제 마운트가 해제될 때 더 많은 내부 필드를 정리하여 코드에 존재할 수 있는 수정되지 않은 메모리 누수의 영향을 줄입니다.\n\n### React DOM 서버 {/*react-dom-server*/}\n\n* <strong>`renderToString`</strong>은 이제 서버에서 Suspend할 때 더 이상 오류가 발생하지 않습니다. 대신 가장 가까운 `<Suspense>` 경계에 Fallback HTML을 출력하고, 그런 다음 클라이언트에서 동일한 콘텐츠를 다시 렌더링하도록 시도합니다. 여전히 `renderToPipeableStream` 또는 `renderToReadableStream`과 같은 스트리밍 API로 전환하는 것이 좋습니다.\n* <strong>`renderToStaticMarkup`</strong>은 이제 서버에서 Suspend할 때 더 이상 오류가 발생하지 않습니다. 대신 가장 가까운 `<Suspense>` 경계에 Fallback HTML을 출력합니다.\n\n## 변경 내역 {/*changelog*/}\n\n[여기서 전체 변경 내역](https://github.com/facebook/react/blob/main/CHANGELOG.md)을 확인할 수 있습니다.\n"
  },
  {
    "path": "src/content/blog/2022/03/29/react-v18.md",
    "content": "---\ntitle: \"React v18.0\"\nauthor: The React Team\ndate: 2022/03/08\ndescription: React 18 is now available on npm! In our last post, we shared step-by-step instructions for upgrading your app to React 18. In this post, we'll give an overview of what's new in React 18, and what it means for the future.\n---\n\n2022년 3월 29일, [The React Team](/community/team)\n\n---\n\n<Intro>\n\n이제 npm에서 React 18을 사용할 수 있습니다! 지난 포스팅에서는 앱을 [React 18로 업그레이드하는 방법](/blog/2022/03/08/react-18-upgrade-guide)을 단계별로 공유했습니다. 이번 포스팅에서는 React 18의 새로운 기능과 이것이 미래에 어떤 의미를 갖는지에 대해 설명하겠습니다.\n\n</Intro>\n\n---\n\n최신 주<sup>Major, 主</sup> 버전에는 자동 Batching, `startTransition`과 같은 새로운 API, Suspense를 지원하는 스트리밍 서버 측 렌더링<sup>SSR, Server Side Rendering</sup>과 같은 즉각적인 개선 사항이 포함되어 있습니다.\n\nReact 18의 많은 기능은 새로운 동시성 렌더러<sup>Concurrent Renderer</sup>를 기반으로 구축되었습니다. 동시성 렌더러는 선택 사항으로, 동시성 기능을 사용할 때만 활성화할 수 있지만 사람들이 애플리케이션을 빌드하는 방식에 큰 영향을 미칠 것으로 예상합니다.\n\n저희는 수년간 React의 동시성<sup>concurrency</sup> 지원을 연구하고 개발해 왔으며, 기존 사용자가 점진적으로 채택할 수 있도록 각별히 신경을 썼습니다. 지난 여름에는 커뮤니티의 전문가들로부터 피드백을 수집하고 전체 React 생태계를 위한 원활한 업그레이드 환경을 보장하기 위해 [React 18 워킹 그룹](/blog/2021/06/08/the-plan-for-react-18)을 구성했습니다.\n\n혹시 놓치셨다면, React Conf 2021에서 이 비전에 대해 자세히 공유했으니 참고해 보세요.\n\n* [이 연설](https://www.youtube.com/watch?v=FZ0cG47msEk&list=PLNG_1j3cPCaZZ7etkzWA7JfdmKWT0pMsa)에서는 개발자들이 훌륭한 사용자 경험을 쉽게 구축할 수 있도록 돕는 우리의 임무에 React 18이 어떻게 부합하는지 설명했습니다.\n* [Shruti Kapoor](https://twitter.com/shrutikapoor08)가 [React 18의 새로운 기능을 사용하는 방법을 시연](https://www.youtube.com/watch?v=ytudH8je5ko&list=PLNG_1j3cPCaZZ7etkzWA7JfdmKWT0pMsa&index=2)했습니다.\n* [Shaundai Person](https://twitter.com/shaundai)이 [Suspense를 사용한 스트리밍 서버 렌더링](https://www.youtube.com/watch?v=pj5N-Khihgc&list=PLNG_1j3cPCaZZ7etkzWA7JfdmKWT0pMsa&index=3)에 대한 개요를 설명했습니다.\n\n다음은 동시성 렌더링<sup>Concurrent Rendering</sup>부터 시작하여 이번 릴리스에서 기대할 수 있는 기능에 대한 전체 개요입니다.\n\n<Note>\n\nReact Native 사용자를 위해 React 18은 새로운 React Native 아키텍처와 함께 React Native로 제공될 예정입니다. 자세한 내용은 [React Conf 기조연설](https://www.youtube.com/watch?v=FZ0cG47msEk&t=1530s)를 확인하세요.\n\n</Note>\n\n## React의 동시성<sup>Concurrency</sup>이란? {/*what-is-concurrent-react*/}\n\nReact 18에서 가장 중요한 추가 사항은 여러분이 결코 생각할 필요가 없기를 바라는 것, 바로 동시성<sup>concurrency</sup>입니다. 라이브러리 관리자에게는 조금 더 복잡할 수 있지만 애플리케이션 개발자에게는 대부분 해당하는 이야기라 생각합니다.\n\n동시성 자체는 기능이 아닙니다. React가 동시에 여러 버전의 UI를 준비할 수 있게 해주는 새로운 내부 동작 방식입니다. 동시성은 구현의 세부 사항으로 생각할 수 있으며, 동시성이 제공하는 기능들 때문에 가치가 있습니다. React는 내부 구현에서 우선순위 큐 및 다중 버퍼링과 같은 정교한 기술들을 사용합니다. 하지만 우리의 공개 API에서는 이러한 개념을 찾아볼 수 없습니다.\n\nAPI를 설계할 때 우리는 개발자에게 구현 세부 사항을 숨기려고 노력합니다. React 개발자는 사용자 경험을 *어떤 모습*<sup>*What*</sup> 으로 만들 것인지에 집중하고, React는 그 경험을 *어떻게*<sup>*How*</sup> 전달할 것인지를 처리합니다. 따라서 React 개발자가 내부에서 동시성이 어떻게 작동하는지 알기를 기대하지 않습니다.\n\n그러나 React의 동시성은 일반적인 구현 세부 사항보다 더 중요합니다. 이것은 React의 핵심 렌더링 모델에 대한 근본적인 업데이트이기 때문입니다. 따라서 동시성이 어떻게 동작하는지 아는 것이 엄청 중요하지는 않지만, 고수준<sup>High Level</sup>에서 동시성이 무엇인지 아는 것은 가치가 있을 수 있습니다.\n\nReact의 동시성에서 핵심은 렌더링이 중단 가능하다는 것입니다. React 18로 처음 업그레이드할 때 동시성 기능을 추가하기 전 업데이트는 이전 버전의 React와 동일하게 중단되지 않는 단일 동기식 트랜잭션으로 렌더링 됩니다. 동기식 렌더링의 경우 업데이트가 렌더링을 시작하면 사용자가 화면에서 결과를 볼 수 있을 때까지 그 어떤 것도 렌더링을 방해할 수 없습니다.\n\n동시성 렌더링에서는 항상 그렇지는 않습니다. React는 업데이트된 렌더링을 시작하고 중간에 일시 중지했다가 나중에 계속할 수 있습니다. 심지어 진행 중인 렌더링을 완전히 중단할 수도 있습니다. React는 렌더링이 중단되더라도 UI가 일관되게 표시되도록 보장합니다. 이를 위해 트리 전체가 평가된 후 DOM 변형을 수행하기 위해 기다립니다. 이 기능을 통해 React는 메인 스레드를 차단하지 않고 백그라운드에서 새 화면을 준비할 수 있습니다. 즉, UI가 대규모 렌더링 작업 중에도 사용자 입력에 즉시 반응하여 유동적인 사용자 경험을 제공할 수 있습니다.\n\n또 다른 예시는 재사용 가능한 State입니다. React의 동시성은 화면에서 UI의 섹션을 제거했다가 나중에 다시 추가하면서 이전 State를 재사용할 수 있습니다. 예를 들어 사용자가 화면에서 벗어나 뒤로가기를 탭 할 때, React는 화면을 이전과 동일한 상태로 복원할 수 있어야 합니다. 곧 출시될 마이너 버전에서는 이 패턴을 구현하는 `<Offscreen>`이라는 새로운 컴포넌트를 추가할 계획입니다. 마찬가지로 `<Offscreen>`을 사용하여 사용자가 보기 전에 백그라운드에서 새 UI를 준비할 수 있게 될 것입니다.\n\n동시성 렌더링은 React의 강력한 새 도구이며 Suspense, Transitions, 스트리밍 서버 렌더링 등 대부분의 새로운 기능들이 이를 활용하기 위해 만들어졌습니다. 하지만 React 18은 이 새로운 기반 위에 구축하고자 하는 목표의 시작일 뿐입니다.\n\n## 점진적으로 동시성 기능 적용하기 {/*gradually-adopting-concurrent-features*/}\n\n기술적으로 동시성 렌더링은 획기적인 변화입니다. 동시성 렌더링은 중단이 가능하기 때문에 이 기능을 활성화하면 컴포넌트가 약간 다르게 동작합니다.\n\n저희는 테스트에서 수천 개의 컴포넌트를 React 18로 업그레이드했습니다. 그 결과 대부분의 기존 컴포넌트가 동시성 렌더링과 함께 변경 없이 \"그냥 작동\"한다는 사실을 발견했습니다. 하지만 일부 컴포넌트는 추가적인 마이그레이션 작업이 필요할 수 있습니다. 일반적으로 변경 사항은 크지 않지만, 사용자가 원하는 속도에 맞춰 변경할 수 있습니다. React 18의 새로운 렌더링 동작은 **여러분의 앱에서 새로운 기능을 사용하는 부분에서만 활성화됩니다**.\n\n전반적인 업그레이드 전략은 기존 코드를 훼손하지 않고 애플리케이션을 React 18에서 실행하는 것입니다. 그런 다음 자신의 속도에 맞춰 점진적으로 동시성 기능을 추가하기 시작할 수 있습니다. 개발 중에 동시성 관련 버그를 발견하는 데 도움이 되는 [`<StrictMode>`](/reference/react/StrictMode)를 사용할 수 있습니다. Strict 모드는 프로덕션 동작에는 영향을 미치지 않지만, 개발 중에 추가 경고를 기록하고 비활성화될 것으로 예상되는 함수를 이중 호출합니다. 모든 것을 잡아내지는 못하지만 가장 일반적인 유형의 실수를 방지하는 데 효과적입니다.\n\nReact 18로 업그레이드하면 즉시 동시성 기능을 사용할 수 있습니다. 예를 들어 `startTransition`을 사용하여 사용자 입력을 차단하지 않고 화면 사이를 탐색할 수 있습니다. 또는 비용이 많이 드는 리렌더링을 스로틀링하기 위해 `useDeferredValue`를 사용할 수 있습니다.\n\n하지만 장기적으로, 앱에 동시성을 추가하는 주된 방법은 동시성 지원 라이브러리 또는 프레임워크를 사용하는 것입니다. 대부분의 경우 동시성 API와 직접 상호 작용하지는 않을 것입니다. 예를 들어 개발자가 새 화면으로 이동할 때마다 `startTransition`을 호출하는 대신, 라우터 라이브러리가 자동으로 `startTransition`에 내비게이션을 래핑합니다.\n\n라이브러리가 동시성과 호환이 가능하도록 업그레이드하는 데 다소 시간이 걸릴 수 있습니다. 우리는 라이브러리에서 동시성 기능을 더 쉽게 활용할 수 있도록 새로운 API를 제공했습니다. 메인테이너들이 React 에코시스템을 점진적으로 마이그레이션하기 위해 노력하는 동안 인내심을 가져주세요.\n\n더 자세한 내용은 이전 게시물을 참조하세요. [React 18로 업그레이드하는 방법](/blog/2022/03/08/react-18-upgrade-guide).\n\n## 데이터 프레임워크의 Suspense {/*suspense-in-data-frameworks*/}\n\nReact 18에서는 Relay, Next.js, Hydrogen 또는 Remix와 같은 고유한 프레임워크에서 데이터를 가져오기<sup>Fetching</sup> 위해 [Suspense](/reference/react/Suspense)를 사용할 수 있습니다. Suspense를 사용한 Ad hoc 데이터 가져오기<sup>Fetching</sup>는 기술적으로 가능하지만, 일반적인 전략으로 권장되지는 않습니다.\n\n향후에는 이런 프레임워크를 사용하지 않고도 Suspense로 데이터에 더 쉽게 액세스할 수 있는 추가 기본 요소를 노출할 수 있습니다. 하지만 Suspense는 라우터, 데이터 레이어, 서버 렌더링 환경 등 애플리케이션의 아키텍처에 깊이 통합되어 있을 때 가장 잘 작동합니다. 따라서 장기적으로도 라이브러리와 프레임워크가 React 생태계에서 중요한 역할을 할 것으로 예상됩니다.\n\n이전 버전의 React에서와 마찬가지로, 클라이언트에서 코드 분할<sup>Code Splitting</sup>을 위해 Suspense를 `React.lazy`와 함께 사용할 수도 있습니다. 하지만 Suspense에 대한 비전은 항상 코드 로딩 그 이상이었습니다. 결국에는 Suspense에 대한 지원을 확장하여, 동일한 선언적 Suspense Fallback이 모든 비동기 작업(코드, 데이터, 이미지 등의 로딩)을 처리할 수 있도록 하는 것이 목표입니다.\n\n\n## 서버 컴포넌트는 아직 개발 중입니다. {/*server-components-is-still-in-development*/}\n\n[**서버 컴포넌트**](/blog/2020/12/21/data-fetching-with-react-server-components)는 개발자가 클라이언트 측 앱의 풍부한 상호작용과 기존 서버 렌더링의 향상된 성능을 결합하여 서버와 클라이언트를 아우르는 앱을 구축할 수 있게 해주는 곧 출시될 기능입니다. 서버 컴포넌트는 본질적으로 React의 동시성과 결합하여 있지는 않지만, Suspense 및 스트리밍 서버 렌더링과 같은 동시성 기능에 가장 잘 동작하도록 설계되었습니다.\n\n서버 컴포넌트는 아직 실험 단계이지만 18.x 마이너 릴리스에서 초기 버전을 출시할 예정입니다. 그 동안에는 이 제안을 발전시키고 광범위한 채택을 준비하기 위해 Next.js, Hydrogen, Remix와 같은 프레임워크와 협력하고 있습니다.\n\n## React 18의 새로운 기능 {/*whats-new-in-react-18*/}\n\n### 새로운 기능: 자동 Batching {/*new-feature-automatic-batching*/}\n\nBatching은 React가 성능 향상을 위해 여러 State 업데이트를 하나의 리렌더링으로 그룹화하는 것입니다. 자동 Batching이 없을 때, 우리는 React 이벤트 핸들러 내부의 업데이트만 Batch 했습니다. Promises, `setTimeout`, 네이티브 이벤트 핸들러 또는 기타 이벤트 내부의 업데이트는 기본적으로 React에서 Batch 되지 않았습니다. 자동 Batching을 사용하면 이러한 업데이트가 자동으로 Batch 됩니다.\n\n```js\n// 변경 전: 오직 React 이벤트만 Batch 됩니다.\nsetTimeout(() => {\n  setCount(c => c + 1);\n  setFlag(f => !f);\n  // React는 각 State 업데이트마다 한 번씩, 총 두 번 렌더링 됩니다. (Batching 없음)\n}, 1000);\n\n// 변경 후: setTimeout, Promises 내에서도 업데이트할 수 있습니다.\n// 네이티브 이벤트 핸들러 또는 다른 이벤트들이 Batch 됩니다.\nsetTimeout(() => {\n  setCount(c => c + 1);\n  setFlag(f => !f);\n  // React는 마지막에 한 번만 다시 렌더링 됩니다. (그게 Batching 이죠!)\n}, 1000);\n```\n\n더 자세한 내용은 [Automatic batching for fewer renders in React 18](https://github.com/reactwg/react-18/discussions/21)을 참조하세요.\n\n### 새로운 기능: Transitions {/*new-feature-transitions*/}\n\nTransition은 긴급한<sup>Urgent</sup> 업데이트와 긴급하지 않은<sup>Non-Urgent</sup> 업데이트를 구분하기 위한 React의 새로운 개념입니다.\n\n* **긴급한 업데이트**는 입력, 클릭, 누르기 등과 같은 직접적인 상호작용을 반영합니다.\n* **Transition 업데이트**는 UI를 한 화면에서 다른 화면으로 전환합니다.\n\n\n입력, 클릭, 누르기 등의 긴급한 업데이트는 실제 사물의 동작 방식에 대한 우리의 직관과 일치하도록 즉각적인 반응이 필요합니다. 그렇지 않으면 \"잘못됐다\"고 느끼기 때문입니다. 그러나 Transition은 사용자가 화면에 모든 중간값을 볼 것으로 기대하지 않기 때문에 다릅니다.\n\n예를 들어, 드롭다운에서 필터를 선택하면 클릭 즉시 필터 버튼 자체가 반응할 것으로 기대합니다. 그러나 실제 결과는 별도로 전환될 수 있습니다. 약간의 지연은 눈에 띄지 않고 종종 예상되는 일입니다. 또한 결과가 렌더링 되기 전에 필터를 다시 변경하면 오직 최신 결과만 볼 수 있습니다.\n\n일반적으로 최상의 사용자 경험을 위해서는 한 번의 사용자 입력으로 긴급한 업데이트와 긴급하지 않은 업데이트가 모두 이루어져야 합니다. 입력 이벤트 내에서 `startTransition` API를 사용하여 어떤 업데이트가 긴급한지, 어떤 업데이트가 \"Transition\"인지 React에 알릴 수 있습니다.\n\n```js\nimport { startTransition } from 'react';\n\n// 긴급(Urgent): 입력한 내용 표시\nsetInputValue(input);\n\n// 내부의 모든 State 업데이트를 Transition으로 표시\nstartTransition(() => {\n  // Transition: 결과 표시\n  setSearchQuery(input);\n});\n```\n\n`startTransition`으로 래핑된 업데이트는 긴급하지 않은 것으로 처리되며 클릭이나 키 누름과 같은 더 긴급한 업데이트가 들어올 경우 중단됩니다. 사용자가 여러 문자를 연속해서 입력하는 등의 이유로 Transition이 중단되면, React는 완료되지 않은 오래된 렌더링 작업을 버리고 최신 업데이트만 렌더링합니다.\n\n* `useTransition`: 보류 중인<sup>Pending</sup> State를 추적하는 값을 포함하여 Transition을 시작하는 Hook.\n* `startTransition`: Hook을 사용할 수 없을 때 Transition을 시작하는 메서드.\n\nTransition은 동시성 렌더링을 선택하여 업데이트를 중단할 수 있습니다. 콘텐츠가 다시 일시 중단되면, Transition은 백그라운드에서 Transition 콘텐츠를 렌더링하는 동안 현재 콘텐츠를 계속 표시하도록 React에 지시합니다. (자세한 내용은 [Suspense RFC](https://github.com/reactjs/rfcs/blob/main/text/0213-suspense-in-react-18.md)를 참고하세요.)\n\n[Transitions 참고 문서](/reference/react/useTransition).\n\n### 새로운 Suspense 기능 {/*new-suspense-features*/}\n\nSuspense를 사용하면 컴포넌트 트리의 일부가 아직 표시될 준비가 되지 않은 경우, 로딩 State를 선언적으로 지정할 수 있습니다.\n\n```js\n<Suspense fallback={<Spinner />}>\n  <Comments />\n</Suspense>\n```\n\nSuspense는 \"UI 로딩 상태\"를 React 프로그래밍 모델에서 1급 선언적 개념으로 만듭니다. 이를 통해 그 위에 고수준<sup>Higher-Level</sup>의 기능을 구축할 수 있습니다.\n\n저희는 몇 년 전에 Suspense의 제한된 버전을 도입했습니다. 하지만 지원되는 사용 사례는 `React.lazy`를 이용한 코드 분할뿐이었으며 서버에서 렌더링할 때는 전혀 지원되지 않았습니다.\n\nReact 18에서는 서버에서 Suspense에 대한 지원을 추가하고 동시성 렌더링 기능을 사용하여 기능을 확장했습니다.\n\nReact 18의 Suspense는 Transition API와 함께 사용할 때 가장 잘 동작합니다. Transition 도중 일시 중단하면, React는 이미 표시된 콘텐츠가 Fallback으로 대체되는 것을 방지합니다. 대신 React는 충분한 데이터가 로드될 때까지 렌더링을 지연시켜 로딩 상태가 나빠지는 것을 방지합니다.\n\n자세한 내용은 [Suspense in React 18](https://github.com/reactjs/rfcs/blob/main/text/0213-suspense-in-react-18.md)에 대한 RFC를 참고하세요.\n\n### 새로운 클라이언트 및 서버 렌더링 API {/*new-client-and-server-rendering-apis*/}\n\n이번 릴리스에서는 클라이언트와 서버에서 렌더링을 위해 노출하는 API를 재설계할 기회를 가졌습니다. 이러한 변경을 통해 사용자는 React 18의 새로운 API로 업그레이드하는 동안 React 17 모드에서 이전 API를 계속 사용할 수 있습니다.\n\n#### React DOM 클라이언트 {/*react-dom-client*/}\n\n새로운 API는 이제 `react-dom/client`에서 내보내집니다.\n\n* `createRoot`: `render` 또는 `unmount`에 Root를 생성하는 새로운 메서드. `ReactDOM.render` 대신 사용하세요. 이 함수가 없으면 React 18의 새로운 기능들이 동작하지 않습니다.\n* `hydrateRoot`: 서버 렌더링된 애플리케이션을 Hydrate하는 새로운 메서드. 새로운 React DOM 서버 API와 함께 `ReactDOM.hydrate` 대신 사용하세요. 이 메서드가 없으면 React 18의 새로운 기능들이 동작하지 않습니다.\n\n`createRoot`와 `hydrateRoot`는 모두 렌더링 중 오류가 발생하거나 로깅을 위한 Hydration 중 오류가 발생했을 때 알림을 받고자 하는 경우 `onRecoverableError`라는 새로운 옵션을 허용합니다. 기본적으로 React는 [`reportError`](https://developer.mozilla.org/en-US/docs/Web/API/reportError) 또는 오래된 브라우저에서는 `console.error`를 사용합니다.\n\n[React DOM 클라이언트 참고 문서](/reference/react-dom/client).\n\n#### React DOM 서버 {/*react-dom-server*/}\n\n이 새로운 API는 이제 `react-dom/server`에서 내보내지며 서버에서의 Suspense 스트리밍을 완벽하게 지원합니다.\n\n* `renderToPipeableStream`: Node 환경에서의 스트리밍용.\n* `renderToReadableStream`: 최신 엣지 런타임 환경(예: Deno 및 Cloudflare 워커)에서의 스트리밍용.\n\n기존의 `renderToString` 메서드는 계속 동작하지만 권장하지 않습니다.\n\n[React DOM 서버 참고 문서](/reference/react-dom/server).\n\n### 새로운 Strict 모드 동작 {/*new-strict-mode-behaviors*/}\n\n앞으로는 React가 State를 유지하면서 UI의 섹션을 추가하고 제거할 수 있는 기능을 추가하고 싶습니다. 예를 들어 사용자가 화면에서 벗어나 뒤로 탭 할 때 React는 이전 화면을 즉시 표시할 수 있어야 합니다. 이를 위해 React는 이전과 동일한 컴포넌트 State를 사용하여 트리를 마운트 해제하고 다시 마운트합니다.\n\n이 기능을 사용하면 React 앱의 성능이 즉시 향상되지만, 컴포넌트의 Effect가 여러 번 마운트 및 소멸하는 것에 대해 탄력적이어야 합니다. 대부분의 Effect는 변경 없이 동작하지만 일부 Effect는 한 번만 마운트되거나 소멸한다고 가정합니다.\n\n이러한 문제를 해결하기 위해 React 18은 Strict 모드에 새로운 개발 전용 검사를 도입했습니다. 이 새로운 검사는 컴포넌트가 처음 마운트될 때마다 모든 컴포넌트를 자동으로 마운트 해제하고 다시 마운트하여 두 번째 마운트 시 이전 State로 복원합니다.\n\n변경 전에는 React가 컴포넌트를 마운트하고 Effect를 생성했습니다.\n\n```\n* React가 컴포넌트를 마운트합니다.\n  * 레이아웃 이펙트가 생성됩니다.\n  * Effect 이펙트가 생성됩니다.\n```\n\nReact 18의 Strict 모드에서는 개발 모드에서 컴포넌트를 마운트 해제하고 다시 마운트하는 것을 시뮬레이션합니다.\n\n```\n* React가 컴포넌트를 마운트합니다.\n  * 레이아웃 이펙트가 생성됩니다.\n  * Effect 이펙트가 생성됩니다.\n* React가 컴포넌트를 마운트 해제하는 것을 시뮬레이션합니다.\n  * 레이아웃 이펙트가 사라집니다.\n  * 이펙트가 사라집니다.\n* React는 컴포넌트를 이전 상태로 다시 마운트하는 것을 시뮬레이션합니다.\n  * 레이아웃 이펙트 셋업 코드가 실행됩니다.\n  * Effect 셋업 코드가 실행됩니다.\n```\n\n[재사용 가능한 State를 보장하는 방법은 문서를 참고하세요](/reference/react/StrictMode#fixing-bugs-found-by-re-running-effects-in-development).\n\n### 새로운 Hook {/*new-hooks*/}\n\n#### `useId` {/*useid*/}\n\n`useId`는 Hydration 불일치를 방지하면서 클라이언트와 서버 모두에서 고유 ID를 생성하기 위한 새로운 Hook입니다. 주로 고유 ID가 필요한 접근성 API와 통합되는 컴포넌트 라이브러리에 유용합니다. 이는 React 17 이하에서 이미 존재하던 문제를 해결하지만, 새로운 스트리밍 서버 렌더러가 HTML을 순서대로 전달하지 않기 때문에 React 18에서는 더욱 중요합니다. [참고 문서](/reference/react/useId).\n\n> 주의\n>\n> `useId`는 [목록에서 Key](/learn/rendering-lists#where-to-get-your-key)를 생성하기 위한 것이 *아닙니다*. Key는 여러분의 데이터에서 생성해야 합니다.\n\n#### `useTransition` {/*usetransition*/}\n\n`useTransition`와 `startTransition`을 사용하면 일부 상태 업데이트를 긴급하지 않은 것으로 표시할 수 있습니다. 기본적으로 다른 상태 업데이트는 긴급한 것으로 간주합니다. React는 긴급하지 않은 상태 업데이트(예: 검색 결과 목록 렌더링)를 긴급한 상태 업데이트(예: 텍스트 입력 업데이트)로 중단할 수 있도록 허용합니다. [참고 문서](/reference/react/useTransition).\n\n#### `useDeferredValue` {/*usedeferredvalue*/}\n\n`useDeferredValue`를 사용하면 트리에서 긴급하지 않은 부분의 리렌더링을 지연<sup>Deferred</sup>시킬 수 있습니다. 디바운싱과 비슷하지만 디바운싱에 비해 몇 가지 장점이 있습니다. 고정된 시간 지연이 없기 때문에 React는 첫 번째 렌더링이 화면에 반영된 직후에 지연된<sup>Deferred</sup> 렌더링을 시도합니다. 지연된<sup>Deferred</sup> 렌더링은 중단할 수 있으며 사용자 입력을 차단하지 않습니다. [참고 문서](/reference/react/useDeferredValue).\n\n#### `useSyncExternalStore` {/*usesyncexternalstore*/}\n\n`useSyncExternalStore`는 Store에 대한 업데이트를 강제로 동기화하여 외부 Store가 동시 읽기를 지원할 수 있도록 하는 새로운 Hook입니다. 이 Hook은 외부 데이터에 대한 구독을 구현할 때 `useEffect`가 필요하지 않으며, React 외부 State와 통합하는 모든 라이브러리에 권장됩니다. [참고 문서](/reference/react/useSyncExternalStore).\n\n> 주의\n>\n> `useSyncExternalStore`는 애플리케이션 코드가 아닌 라이브러리에서 사용하기 위한 것입니다.\n\n#### `useInsertionEffect` {/*useinsertioneffect*/}\n\n`useInsertionEffect`는 CSS-in-JS 라이브러리가 렌더링에서 스타일을 삽입할 때 발생하는 성능 문제를 해결할 수 있는 새로운 Hook입니다. 이미 CSS-in-JS 라이브러리를 빌드한 경우가 아니라면 이 Hook을 사용할 일은 없을 것으로 예상됩니다. 이 Hook은 DOM이 변경된 후에 실행되지만, 레이아웃 Effect가 새 레이아웃을 읽기 전에 실행됩니다. 이는 React 17 이하에서 이미 존재하던 문제를 해결하지만, React 18에서는 동시성 렌더링 중에 브라우저가 레이아웃을 다시 계산할 기회를 제공하기 때문에 더욱 중요합니다. [참고 문서](/reference/react/useInsertionEffect).\n\n> 주의\n>\n> `useInsertionEffect` 는 애플리케이션 코드가 아닌 라이브러리에서 사용하기 위한 것입니다.\n\n## 업그레이드하는 방법 {/*how-to-upgrade*/}\n\n단계별 지침과 주요 주목할 만한 변경 사항 목록은 [React 18로 업그레이드하는 방법](/blog/2022/03/08/react-18-upgrade-guide)을 참고하세요.\n\n## Changelog {/*changelog*/}\n\n### React {/*react*/}\n\n* Add `useTransition` and `useDeferredValue` to separate urgent updates from transitions. ([#10426](https://github.com/facebook/react/pull/10426), [#10715](https://github.com/facebook/react/pull/10715), [#15593](https://github.com/facebook/react/pull/15593), [#15272](https://github.com/facebook/react/pull/15272), [#15578](https://github.com/facebook/react/pull/15578), [#15769](https://github.com/facebook/react/pull/15769), [#17058](https://github.com/facebook/react/pull/17058), [#18796](https://github.com/facebook/react/pull/18796), [#19121](https://github.com/facebook/react/pull/19121), [#19703](https://github.com/facebook/react/pull/19703), [#19719](https://github.com/facebook/react/pull/19719), [#19724](https://github.com/facebook/react/pull/19724), [#20672](https://github.com/facebook/react/pull/20672), [#20976](https://github.com/facebook/react/pull/20976) by [@acdlite](https://github.com/acdlite), [@lunaruan](https://github.com/lunaruan), [@rickhanlonii](https://github.com/rickhanlonii), and [@sebmarkbage](https://github.com/sebmarkbage))\n* Add `useId` for generating unique IDs. ([#17322](https://github.com/facebook/react/pull/17322), [#18576](https://github.com/facebook/react/pull/18576), [#22644](https://github.com/facebook/react/pull/22644), [#22672](https://github.com/facebook/react/pull/22672), [#21260](https://github.com/facebook/react/pull/21260) by [@acdlite](https://github.com/acdlite), [@lunaruan](https://github.com/lunaruan), and [@sebmarkbage](https://github.com/sebmarkbage))\n* Add `useSyncExternalStore` to help external store libraries integrate with React. ([#15022](https://github.com/facebook/react/pull/15022), [#18000](https://github.com/facebook/react/pull/18000), [#18771](https://github.com/facebook/react/pull/18771), [#22211](https://github.com/facebook/react/pull/22211), [#22292](https://github.com/facebook/react/pull/22292), [#22239](https://github.com/facebook/react/pull/22239), [#22347](https://github.com/facebook/react/pull/22347), [#23150](https://github.com/facebook/react/pull/23150) by [@acdlite](https://github.com/acdlite), [@bvaughn](https://github.com/bvaughn), and [@drarmstr](https://github.com/drarmstr))\n* Add `startTransition` as a version of `useTransition` without pending feedback. ([#19696](https://github.com/facebook/react/pull/19696)  by [@rickhanlonii](https://github.com/rickhanlonii))\n* Add `useInsertionEffect` for CSS-in-JS libraries. ([#21913](https://github.com/facebook/react/pull/21913)  by [@rickhanlonii](https://github.com/rickhanlonii))\n* Make Suspense remount layout effects when content reappears. ([#19322](https://github.com/facebook/react/pull/19322), [#19374](https://github.com/facebook/react/pull/19374), [#19523](https://github.com/facebook/react/pull/19523), [#20625](https://github.com/facebook/react/pull/20625), [#21079](https://github.com/facebook/react/pull/21079) by [@acdlite](https://github.com/acdlite), [@bvaughn](https://github.com/bvaughn), and [@lunaruan](https://github.com/lunaruan))\n* Make `<StrictMode>` re-run effects to check for restorable state. ([#19523](https://github.com/facebook/react/pull/19523) , [#21418](https://github.com/facebook/react/pull/21418)  by [@bvaughn](https://github.com/bvaughn) and [@lunaruan](https://github.com/lunaruan))\n* Assume Symbols are always available. ([#23348](https://github.com/facebook/react/pull/23348)  by [@sebmarkbage](https://github.com/sebmarkbage))\n* Remove `object-assign` polyfill. ([#23351](https://github.com/facebook/react/pull/23351)  by [@sebmarkbage](https://github.com/sebmarkbage))\n* Remove unsupported `unstable_changedBits` API. ([#20953](https://github.com/facebook/react/pull/20953)  by [@acdlite](https://github.com/acdlite))\n* Allow components to render undefined. ([#21869](https://github.com/facebook/react/pull/21869)  by [@rickhanlonii](https://github.com/rickhanlonii))\n* Flush `useEffect` resulting from discrete events like clicks synchronously. ([#21150](https://github.com/facebook/react/pull/21150)  by [@acdlite](https://github.com/acdlite))\n* Suspense `fallback={undefined}` now behaves the same as `null` and isn't ignored. ([#21854](https://github.com/facebook/react/pull/21854)  by [@rickhanlonii](https://github.com/rickhanlonii))\n* Consider all `lazy()` resolving to the same component equivalent. ([#20357](https://github.com/facebook/react/pull/20357)  by [@sebmarkbage](https://github.com/sebmarkbage))\n* Don't patch console during first render. ([#22308](https://github.com/facebook/react/pull/22308)  by [@lunaruan](https://github.com/lunaruan))\n* Improve memory usage. ([#21039](https://github.com/facebook/react/pull/21039)  by [@bgirard](https://github.com/bgirard))\n* Improve messages if string coercion throws (Temporal.*, Symbol, etc.) ([#22064](https://github.com/facebook/react/pull/22064)  by [@justingrant](https://github.com/justingrant))\n* Use `setImmediate` when available over `MessageChannel`. ([#20834](https://github.com/facebook/react/pull/20834)  by [@gaearon](https://github.com/gaearon))\n* Fix context failing to propagate inside suspended trees. ([#23095](https://github.com/facebook/react/pull/23095)  by [@gaearon](https://github.com/gaearon))\n* Fix `useReducer` observing incorrect props by removing the eager bailout mechanism. ([#22445](https://github.com/facebook/react/pull/22445)  by [@josephsavona](https://github.com/josephsavona))\n* Fix `setState` being ignored in Safari when appending iframes. ([#23111](https://github.com/facebook/react/pull/23111)  by [@gaearon](https://github.com/gaearon))\n* Fix a crash when rendering `ZonedDateTime` in the tree. ([#20617](https://github.com/facebook/react/pull/20617)  by [@dimaqq](https://github.com/dimaqq))\n* Fix a crash when document is set to `null` in tests. ([#22695](https://github.com/facebook/react/pull/22695)  by [@SimenB](https://github.com/SimenB))\n* Fix `onLoad` not triggering when concurrent features are on. ([#23316](https://github.com/facebook/react/pull/23316)  by [@gnoff](https://github.com/gnoff))\n* Fix a warning when a selector returns `NaN`. ([#23333](https://github.com/facebook/react/pull/23333)  by [@hachibeeDI](https://github.com/hachibeeDI))\n* Fix a crash when document is set to `null` in tests. ([#22695](https://github.com/facebook/react/pull/22695) by [@SimenB](https://github.com/SimenB))\n* Fix the generated license header. ([#23004](https://github.com/facebook/react/pull/23004)  by [@vitaliemiron](https://github.com/vitaliemiron))\n* Add `package.json` as one of the entry points. ([#22954](https://github.com/facebook/react/pull/22954)  by [@Jack](https://github.com/Jack-Works))\n* Allow suspending outside a Suspense boundary. ([#23267](https://github.com/facebook/react/pull/23267)  by [@acdlite](https://github.com/acdlite))\n* Log a recoverable error whenever hydration fails. ([#23319](https://github.com/facebook/react/pull/23319)  by [@acdlite](https://github.com/acdlite))\n\n### React DOM {/*react-dom*/}\n\n* Add `createRoot` and `hydrateRoot`. ([#10239](https://github.com/facebook/react/pull/10239), [#11225](https://github.com/facebook/react/pull/11225), [#12117](https://github.com/facebook/react/pull/12117), [#13732](https://github.com/facebook/react/pull/13732), [#15502](https://github.com/facebook/react/pull/15502), [#15532](https://github.com/facebook/react/pull/15532), [#17035](https://github.com/facebook/react/pull/17035), [#17165](https://github.com/facebook/react/pull/17165), [#20669](https://github.com/facebook/react/pull/20669), [#20748](https://github.com/facebook/react/pull/20748), [#20888](https://github.com/facebook/react/pull/20888), [#21072](https://github.com/facebook/react/pull/21072), [#21417](https://github.com/facebook/react/pull/21417), [#21652](https://github.com/facebook/react/pull/21652), [#21687](https://github.com/facebook/react/pull/21687), [#23207](https://github.com/facebook/react/pull/23207), [#23385](https://github.com/facebook/react/pull/23385) by [@acdlite](https://github.com/acdlite), [@bvaughn](https://github.com/bvaughn), [@gaearon](https://github.com/gaearon), [@lunaruan](https://github.com/lunaruan), [@rickhanlonii](https://github.com/rickhanlonii), [@trueadm](https://github.com/trueadm), and [@sebmarkbage](https://github.com/sebmarkbage))\n* Add selective hydration. ([#14717](https://github.com/facebook/react/pull/14717), [#14884](https://github.com/facebook/react/pull/14884), [#16725](https://github.com/facebook/react/pull/16725), [#16880](https://github.com/facebook/react/pull/16880), [#17004](https://github.com/facebook/react/pull/17004), [#22416](https://github.com/facebook/react/pull/22416), [#22629](https://github.com/facebook/react/pull/22629), [#22448](https://github.com/facebook/react/pull/22448), [#22856](https://github.com/facebook/react/pull/22856), [#23176](https://github.com/facebook/react/pull/23176) by [@acdlite](https://github.com/acdlite), [@gaearon](https://github.com/gaearon), [@salazarm](https://github.com/salazarm), and [@sebmarkbage](https://github.com/sebmarkbage))\n* Add `aria-description` to the list of known ARIA attributes. ([#22142](https://github.com/facebook/react/pull/22142)  by [@mahyareb](https://github.com/mahyareb))\n* Add `onResize` event to video elements. ([#21973](https://github.com/facebook/react/pull/21973)  by [@rileyjshaw](https://github.com/rileyjshaw))\n* Add `imageSizes` and `imageSrcSet` to known props. ([#22550](https://github.com/facebook/react/pull/22550)  by [@eps1lon](https://github.com/eps1lon))\n* Allow non-string `<option>` children if `value` is provided. ([#21431](https://github.com/facebook/react/pull/21431)  by [@sebmarkbage](https://github.com/sebmarkbage))\n* Fix `aspectRatio` style not being applied. ([#21100](https://github.com/facebook/react/pull/21100)  by [@gaearon](https://github.com/gaearon))\n* Warn if `renderSubtreeIntoContainer` is called. ([#23355](https://github.com/facebook/react/pull/23355)  by [@acdlite](https://github.com/acdlite))\n\n### React DOM Server {/*react-dom-server-1*/}\n\n* Add the new streaming renderer. ([#14144](https://github.com/facebook/react/pull/14144), [#20970](https://github.com/facebook/react/pull/20970), [#21056](https://github.com/facebook/react/pull/21056), [#21255](https://github.com/facebook/react/pull/21255), [#21200](https://github.com/facebook/react/pull/21200), [#21257](https://github.com/facebook/react/pull/21257), [#21276](https://github.com/facebook/react/pull/21276), [#22443](https://github.com/facebook/react/pull/22443), [#22450](https://github.com/facebook/react/pull/22450), [#23247](https://github.com/facebook/react/pull/23247), [#24025](https://github.com/facebook/react/pull/24025), [#24030](https://github.com/facebook/react/pull/24030) by [@sebmarkbage](https://github.com/sebmarkbage))\n* Fix context providers in SSR when handling multiple requests. ([#23171](https://github.com/facebook/react/pull/23171)  by [@frandiox](https://github.com/frandiox))\n* Revert to client render on text mismatch. ([#23354](https://github.com/facebook/react/pull/23354)  by [@acdlite](https://github.com/acdlite))\n* Deprecate `renderToNodeStream`. ([#23359](https://github.com/facebook/react/pull/23359)  by [@sebmarkbage](https://github.com/sebmarkbage))\n* Fix a spurious error log in the new server renderer. ([#24043](https://github.com/facebook/react/pull/24043)  by [@eps1lon](https://github.com/eps1lon))\n* Fix a bug in the new server renderer. ([#22617](https://github.com/facebook/react/pull/22617)  by [@shuding](https://github.com/shuding))\n* Ignore function and symbol values inside custom elements on the server. ([#21157](https://github.com/facebook/react/pull/21157)  by [@sebmarkbage](https://github.com/sebmarkbage))\n\n### React DOM Test Utils {/*react-dom-test-utils*/}\n\n* Throw when `act` is used in production. ([#21686](https://github.com/facebook/react/pull/21686)  by [@acdlite](https://github.com/acdlite))\n* Support disabling spurious act warnings with `global.IS_REACT_ACT_ENVIRONMENT`. ([#22561](https://github.com/facebook/react/pull/22561)  by [@acdlite](https://github.com/acdlite))\n* Expand act warning to cover all APIs that might schedule React work. ([#22607](https://github.com/facebook/react/pull/22607)  by [@acdlite](https://github.com/acdlite))\n* Make `act` batch updates. ([#21797](https://github.com/facebook/react/pull/21797)  by [@acdlite](https://github.com/acdlite))\n* Remove warning for dangling passive effects. ([#22609](https://github.com/facebook/react/pull/22609)  by [@acdlite](https://github.com/acdlite))\n\n### React Refresh {/*react-refresh*/}\n\n* Track late-mounted roots in Fast Refresh. ([#22740](https://github.com/facebook/react/pull/22740)  by [@anc95](https://github.com/anc95))\n* Add `exports` field to `package.json`. ([#23087](https://github.com/facebook/react/pull/23087)  by [@otakustay](https://github.com/otakustay))\n\n### Server Components (Experimental) {/*server-components-experimental*/}\n\n* Add Server Context support. ([#23244](https://github.com/facebook/react/pull/23244)  by [@salazarm](https://github.com/salazarm))\n* Add `lazy` support. ([#24068](https://github.com/facebook/react/pull/24068)  by [@gnoff](https://github.com/gnoff))\n* Update webpack plugin for webpack 5 ([#22739](https://github.com/facebook/react/pull/22739)  by [@michenly](https://github.com/michenly))\n* Fix a mistake in the Node loader. ([#22537](https://github.com/facebook/react/pull/22537)  by [@btea](https://github.com/btea))\n* Use `globalThis` instead of `window` for edge environments. ([#22777](https://github.com/facebook/react/pull/22777)  by [@huozhi](https://github.com/huozhi))\n"
  },
  {
    "path": "src/content/blog/2022/06/15/react-labs-what-we-have-been-working-on-june-2022.md",
    "content": "---\ntitle: \"React Labs: 그동안의 작업 – 2022년 6월\"\nauthor: Andrew Clark, Dan Abramov, Jan Kassens, Joseph Savona, Josh Story, Lauren Tan, Luna Ruan, Mengdi Chen, Rick Hanlon, Robert Zhang, Sathya Gunasekaran, Sebastian Markbage, and Xuan Huang\ndate: 2022/06/15\ndescription: React 18 은 수년간의 준비 끝에 탄생한 버전으로 React 팀에게 귀중한 교훈을 가져다주었습니다. 수년간의 연구와 다양한 경로를 모색한 끝에 출시된 제품입니다. 그 경로 중 일부는 성공적이었지만 더 많은 경로가 막다른 골목에서 새로운 인사이트로 이어졌습니다. 우리가 얻은 한 가지 교훈은 우리가 탐색하고 있는 경로에 대한 인사이트를 공유받지 못한 채 새로운 기능을 기다리는 것은 커뮤니티에 실망감을 준다는 것입니다.\n---\n\n2022년 6월 15일, [Andrew Clark](https://twitter.com/acdlite), [Dan Abramov](https://twitter.com/dan_abramov), [Jan Kassens](https://twitter.com/kassens), [Joseph Savona](https://twitter.com/en_JS), [Josh Story](https://twitter.com/joshcstory), [Lauren Tan](https://twitter.com/potetotes), [Luna Ruan](https://twitter.com/lunaruan), [Mengdi Chen](https://twitter.com/mengdi_en), [Rick Hanlon](https://twitter.com/rickhanlonii), [Robert Zhang](https://twitter.com/jiaxuanzhang01), [Sathya Gunasekaran](https://twitter.com/_gsathya), [Sebastian Markbåge](https://twitter.com/sebmarkbage), [Xuan Huang](https://twitter.com/Huxpro)\n\n---\n\n<Intro>\n\n[React 18](/blog/2022/03/29/react-v18)은 수년간의 준비 끝에 탄생한 버전으로 React 팀에게 귀중한 교훈을 가져다주었습니다. 수년간의 연구와 다양한 방법을 모색한 끝에 출시된 제품입니다. 그 방법 중 일부는 성공적이었지만 더 많은 방법들이 막다른 골목에서 새로운 통찰로 이어졌습니다. 우리가 얻은 한 가지 교훈은 우리가 탐색하고 있는 방법에 대한 통찰을 공유받지 못한 채 새로운 기능을 기다리는 것은 커뮤니티에 실망감을 준다는 것입니다.\n\n</Intro>\n\n---\n\n일반적으로 실험적인 프로젝트부터 명확하게 정의된 프로젝트까지 다양한 프로젝트가 수시로 진행 중입니다. 앞으로는 이러한 프로젝트에서 진행 중인 내용을 커뮤니티와 정기적으로 더 많이 공유하고자 합니다.\n\n기대치를 설정하기 위해 이 로드맵은 명확한 타임라인이 있는 로드맵이 아닙니다. 이러한 프로젝트 중 상당수는 현재 활발히 연구 중이며 구체적인 출시일을 정하기 어렵습니다. 연구 결과에 따라 현재 단계에서 출시되지 않을 수도 있습니다. 대신 저희가 적극적으로 고민하는 문제 영역과 지금까지 파악한 내용을 여러분과 공유하고자 합니다.\n\n## 서버 컴포넌트 {/*server-components*/}\n\n2020년 12월에 [React 서버 컴포넌트(RSC)의 실험적 데모](https://legacy.reactjs.org/blog/2020/12/21/data-fetching-with-react-server-components.html)를 발표했습니다. 그 이후로 React 18에서 종속성을 마무리하고 실험적 피드백에서 영감을 얻은 변경 작업을 진행했습니다.\n\n특히, 우리는 포크된 I/O 라이브러리(예: react-fetch)를 사용하는 아이디어를 포기하고 대신 호환성 향상을 위해 async/await 모델을 채택하고 있습니다. 데이터 불러오기에 라우터를 사용할 수도 있기 때문에 기술적으로 RSC의 릴리스에 지장을 주지는 않습니다. 또 다른 변화는 파일 확장자 방식에서 벗어나 [경계를 주석](https://github.com/reactjs/rfcs/pull/189#issuecomment-1116482278)으로 처리하는 방식을 채택하고 있다는 점입니다.\n\nVercel 및 Shopify와 협력하여 Webpack 및 Vite 모두에서 공유 시맨틱에 대한 번들러 지원을 통합하고 있습니다. 출시 전에 전체 React 생태계에서 RSC의 의미가 동일한지 확인하고자 합니다. 이것은 안정에 도달하는 데 주요 장애물입니다.\n\n## 자산<sup>Asset</sup> 로딩 {/*asset-loading*/}\n\n현재, 스크립트, 외부 스타일, 글꼴 및 이미지와 같은 리소스는 일반적으로 외부 시스템을 사용하여 사전 로드되고 로드됩니다. 이로 인해 스트리밍, 서버 컴포넌트 등과 같은 새로운 환경에서 조정하기 까다로울 수 있습니다.\n우리는 모든 React 환경에서 작동하는 React API를 통해 중복 제거된 외부 자산을 미리 로드하고 로드하기 위해 API를 추가하는 것을 고려하고 있습니다.\n\n우리는 또한 Suspense를 지원하여 로드될 때까지 표시를 차단하지만 스트리밍 및 동시성 렌더링을 차단하지 않는 이미지, CSS 및 글꼴을 가질 수 있도록 하는 것을 고려하고 있습니다. 이것은 시각적 요소가 튀어나오고 레이아웃이 바뀔 때 [\"popcorning\"](https://twitter.com/sebmarkbage/status/1516852731251724293)을 방지하는 데 도움이 될 수 있습니다.\n\n## 정적 서버 렌더링 최적화 {/*static-server-rendering-optimizations*/}\n\n정적 사이트 생성<sup>SSG</sup>과 증분 정적 재생성<sup>ISR</sup>은 캐시 가능한 페이지의 성능을 향상하는 좋은 방법이지만, 동적 서버 측 렌더링<sup>SSR</sup>의 성능을 개선할 수 있는 기능을 더 추가할 수 있다고 생각합니다. 특히, 대부분의 컨텐츠가 캐싱이 가능하지만 일부만 불가능한 경우를 위해서요. 컴파일 및 정적 경로를 활용하여 서버 렌더링을 최적화하는 방법을 모색하고 있습니다.\n\n## React 최적화 컴파일러 {/*react-compiler*/}\n\n우리는 React Conf 2021에서 React Forget을 [미리 선보였습니다](https://www.youtube.com/watch?v=lGEMwh32soc). 이 컴파일러는 React의 프로그래밍 모델을 유지하면서 재렌더링 비용을 최소화하기 위해 `useMemo` 와 `useCallback`에 상응하는 호출을 자동으로 생성하는 컴파일러입니다.\n\n최근 저희는 컴파일러의 안정성과 성능을 높이기 위해 컴파일러 재작업을 완료했습니다. 이 새로운 아키텍처를 통해 [로컬 변형 사용](/learn/keeping-components-pure#local-mutation-your-components-little-secret)과 같은 더 복잡한 패턴을 분석하고 메모화할 수 있게 되었으며, Memoization Hook과 동등한 수준 이상으로 많은 새로운 컴파일 시간 최적화 기회가 열리게 되었습니다.\n\n또한 컴파일러의 여러 측면을 탐색할 수 있는 플레이그라운드도 개발 중입니다. 플레이그라운드의 목표는 컴파일러를 더 쉽게 개발하는 것이지만, 컴파일러를 사용해보고 컴파일러가 하는 일에 대한 직관력을 키우는 데 도움이 될 것으로 생각합니다. 컴파일러가 내부에서 어떻게 작동하는지에 대한 다양한 통찰을 제공하며, 입력하는 대로 컴파일러의 출력을 실시간으로 렌더링합니다. 이 기능은 컴파일러가 출시될 때 함께 제공됩니다.\n\n## 오프스크린 {/*offscreen*/}\n\n이제 컴포넌트를 숨기거나 표시하려면 두 가지 옵션이 있습니다. 하나는 트리에서 완전히 추가하거나 제거하는 것입니다. 이 방법의 문제점은 마운트를 해제할 때마다 스크롤 위치와 같이 DOM에 저장된 State를 포함하여 UI의 State가 손실된다는 것입니다.\n\n다른 옵션은 컴포넌트를 마운트한 상태로 유지하고 CSS를 사용해 시각적으로 모양을 전환하는 것입니다. 이 방법은 UI의 State를 유지하지만, React가 새로운 업데이트를 받을 때마다 숨겨진 컴포넌트와 그 모든 자식들을 계속 렌더링해야 하므로 성능에 비용이 발생합니다.\n\n오프스크린은 UI를 시각적으로 숨기되 콘텐츠의 우선순위를 낮추는 세 번째 옵션을 도입합니다. 이 아이디어는 `content-visibility` CSS 프로퍼티와 비슷한 개념으로, 콘텐츠가 숨겨져 있을 때 나머지 UI와 동기화 상태를 유지할 필요가 없습니다. React는 앱의 나머지 부분이 유휴 상태가 될 때까지 또는 콘텐츠가 다시 표시될 때까지 렌더링 작업을 연기할 수 있습니다.\n\n오프스크린은 고수준<sup>High Level</sup>의 기능을 잠금 해제하는 저수준<sup>Low Level</sup>의 기능입니다. `startTransition` 과 같은 React의 다른 동시성 기능과 유사하게, 대부분의 경우 오프스크린 API와 직접 상호작용하지 않고 독단적인 프레임워크를 통해 다음과 같은 패턴을 구현합니다.\n\n* **즉각적인 전환.** 일부 라우팅 프레임워크는 링크 위로 마우스를 가져갈 때와 같이 후속 탐색 속도를 높이기 위해 이미 데이터를 미리 가져옵니다. 오프스크린을 사용하면 백그라운드에서 다음 화면을 미리 렌더링할 수도 있습니다.\n* **재사용 가능한 State.** 마찬가지로 경로나 탭 사이를 탐색할 때 오프스크린을 사용하여 이전 화면의 State를 보존하여 중단한 지점에서 다시 전환하고 선택할 수 있습니다.\n* **가상화된 목록 렌더링.** 많은 항목 목록을 표시할 때 가상화된 목록 프레임워크는 현재 표시되는 것보다 더 많은 행을 미리 렌더링합니다. 오프스크린을 사용하여 숨겨진 행을 목록에 보이는 항목보다 낮은 우선 순위로 미리 렌더링할 수 있습니다.\n* **배경 콘텐츠.** 또한 모달 오버레이를 표시할 때와 같이 콘텐츠를 숨기지 않고 백그라운드에서 우선 순위를 낮추는 관련 기능을 탐색하고 있습니다.\n\n## Transition 추적 {/*transition-tracing*/}\n\n현재 React에는 두 가지 프로파일링 도구가 있습니다. [기존의 프로파일러](https://legacy.reactjs.org/blog/2018/09/10/introducing-the-react-profiler.html)는 프로파일링 세션의 모든 커밋에 대한 개요를 보여줍니다. 각 커밋에 대해 렌더링된 모든 컴포넌트와 렌더링하는 데 걸린 시간도 표시됩니다. 또한 React 18에 도입된 [Timeline Profiler](https://github.com/reactwg/react-18/discussions/76)의 베타 버전이 있는데, 이 기능은 컴포넌트가 업데이트를 예약하는 시기와 React가 이러한 업데이트에서 작동하는 시기를 보여줍니다. 이 두 프로파일러는 개발자가 코드에서 성능 문제를 식별하는 데 도움이 됩니다.\n\n개발자가 각각의 느린 커밋 그 자체의 발생 여부나, context에서 벗어난 컴포넌트에 대해 아는 것은 그다지 유용하지 않다는 것을 깨달았습니다. 실제로 느린 커밋의 원인을 아는 것이 더 유용합니다. 그리고 개발자는 특정 상호 작용(예: 버튼 클릭, 초기 로드 또는 페이지 탐색)을 추적하여 성능을 회귀적으로 관찰하고 상호 작용이 느린 이유와 해결 방법을 이해할 수 있기를 원합니다.\n\n이전에는 [상호작용 추적 API](https://gist.github.com/bvaughn/8de925562903afd2e7a12554adcdda16)를 만들어 이 문제를 해결하려고 했지만, 이 API는 근본적인 설계 결함으로 인해 상호작용이 느린 이유를 추적하는 정확도가 떨어지고 때로는 상호작용이 끝나지 않는 경우가 있었습니다. 결국 이러한 문제로 인해 이 [API를 제거](https://github.com/facebook/react/pull/20037)하게 되었습니다.\n\n이러한 문제를 해결하는 새로운 버전의 상호작용 추적 API(`startTransition`을 통해 시작되므로 가칭 트랜지션 추적이라고 함)를 개발 중입니다.\n\n## 새로운 React 문서 {/*new-react-docs*/}\n\n작년에 새로운 React 문서 웹사이트의 베타 버전([훗날 react.dev로 출시](/blog/2023/03/16/introducing-react-dev))을 발표했습니다. 새로운 학습 자료는 Hook을 먼저 가르치고 새로운 다이어그램, 일러스트레이션, 많은 대화형 예시 및 과제를 포함합니다. 우리는 React 18 릴리스에 집중하기 위해 해당 작업에서 잠시 휴식을 취했지만 이제 React 18이 출시되었으므로 새 문서를 완료하고 제공하기 위해 적극적으로 노력하고 있습니다.\n\n새로운 혹은 숙련된 React 사용자 모두에게 어려운 주제 중 하나라고 들었기 때문에, 현재 Effect에 대한 자세한 섹션을 작성하고 있습니다. [Effect로 동기화하기](/learn/synchronizing-with-effects)는 시리즈의 첫 번째 페이지이며 다음 주에 더 많은 페이지가 추가될 예정입니다. Effect에 대한 자세한 섹션을 처음 작성하기 시작했을 때 React에 새로운 프리미티브를 추가하여 많은 일반적인 Effect 패턴을 단순화할 수 있다는 것을 깨달았습니다. [useEvent RFC](https://github.com/reactjs/rfcs/pull/220)에서 이에 대한 몇 가지 초기 생각을 공유했습니다. 현재 초기 연구 단계에 있으며 여전히 아이디어를 반복하고 있습니다. 지금까지 RFC에 대한 커뮤니티의 의견과 진행 중인 문서 재작성에 대한 [피드백](https://github.com/reactjs/react.dev/issues/3308) 및 기여에 감사드립니다. 새로운 웹 사이트 구현에 대한 많은 개선 사항을 제출하고 검토한 [Harish Kumar](https://github.com/harish-sethuraman)에게 특별히 감사드립니다.\n\n*이 블로그 게시물을 검토해 주신 [Sophie Alpert](https://twitter.com/sophiebits)에게 감사드립니다!*\n"
  },
  {
    "path": "src/content/blog/2023/03/16/introducing-react-dev.md",
    "content": "---\ntitle: \"react.dev를 소개합니다\"\nauthor: Dan Abramov and Rachel Nabors\ndate: 2023/03/16\ndescription: 오늘 React와 React 문서의 새로운 보금자리인 react.dev 를 출시하게 되어 기쁩니다. 이 글에서는 새로운 사이트에 대해 소개해 드리겠습니다.\n\n---\n\n2023년 3월 16일, [Dan Abramov](https://twitter.com/dan_abramov), [Rachel Nabors](https://twitter.com/rachelnabors)\n\n---\n\n<Intro>\n\n오늘 React와 React 문서의 새로운 보금자리인 [react.dev](https://react.dev)를 출시하게 되어 기쁩니다. 이 글에서는 새로운 사이트에 대해 소개해 드리겠습니다.\n\n</Intro>\n\n---\n\n## 요약 {/*tldr*/}\n\n* 새로운 React 사이트 ([react.dev](https://react.dev))는 함수 컴포넌트와 Hook을 사용한 현대적인 React를 가르칩니다.\n* 다이어그램, 삽화, 도전 과제, 그리고 600개 이상의 새로운 상호 작용 예시를 포함했습니다.\n* 예전 React 문서 사이트는 이제 [legacy.reactjs.org](https://legacy.reactjs.org)로 이전되었습니다.\n\n## 새로운 사이트, 새로운 도메인, 새로운 홈페이지 {/*new-site-new-domain-new-homepage*/}\n\n우선, 조금의 정리를 진행하겠습니다.\n\n새로운 문서의 출시를 축하하고, 더욱 중요한 것은 오래된 내용과 새로운 내용을 명확하게 구분하기 위해, 더 짧은 [react.dev](https://react.dev) 도메인으로 이전했습니다. 예전 [reactjs.org](https://reactjs.org) 도메인을 이제 이곳으로 리다이렉트할 것입니다.\n\n예전 React 문서는 이제 [legacy.reactjs.org](https://legacy.reactjs.org)에 보관되었습니다. 예전 내용으로 통하는 모든 기존 링크는 \"웹을 망가트리는 것\"을 방지하기 위해 자동으로 해당 위치로 리다이렉트할 것이지만, 레거시 사이트에는 더 이상 업데이트 받지 않을 것입니다.\n\n믿기 힘들겠지만, React는 곧 10살이 됩니다. 자바스크립트 시대에, 이건 마치 한 세기와 같습니다! 오늘날 React가 사용자 인터페이스를 만들기 위한 훌륭한 방법인 이유를 반영하기 위해 [React 홈페이지를 갱신하고](https://react.dev), 현대적인 React 기반 프레임워크를 더욱 명확하게 언급하기 위해 시작 가이드를 업데이트했습니다.\n\n아직 새로운 홈페이지를 보지 않았다면, 꼭 확인해 보세요!\n\n## Hook을 사용한 현대적인 React에 전념하기 {/*going-all-in-on-modern-react-with-hooks*/}\n\n2018년에 React Hook을 발표했을 때, Hook 문서는 클래스 컴포넌트에 익숙한 독자를 가정했습니다. 이는 커뮤니티가 Hook을 매우 빠르게 채택하는 데 도움이 되었지만, 시간이 지나면서 예전 문서는 새로운 독자에게 적합하지 않았습니다. 새로운 독자는 클래스 컴포넌트와 Hook을 사용한 것으로 React를 두 번 배워야만 했습니다.\n\n**새로운 문서는 Hook을 사용한 React를 처음부터 가르칩니다.** 문서는 두 가지 주요 섹션으로 나뉘어져 있습니다.\n\n* **[React 학습하기](/learn)** 는 React를 기초부터 스스로 학습할 수 있는 과정입니다.\n* **[API 참고서](/reference)** 는 모든 React API에 대한 세부 내용과 사용 예시를 제공합니다.\n\n각 섹션에서 무슨 내용을 알 수 있는지 자세히 살펴보겠습니다.\n\n<Note>\n\n아직 Hook 기반의 동등한 것이 없는 몇 가지 희귀한 클래스 컴포넌트 사용 사례가 여전히 있습니다. 클래스 컴포넌트는 그대로 지원되고, 새로운 사이트의 [Legacy API](/reference/react/legacy) 섹션에 문서화되어 있습니다.\n\n</Note>\n\n## 빠르게 시작하기 {/*quick-start*/}\n\n학습 섹션은 [빠르게 시작하기](/learn) 페이지로 시작합니다. 이는 React를 짧게 소개하는 여정입니다. 컴포넌트, Props, State 같은 개념에 대한 문법을 소개하지만, 그들을 어떻게 사용하는지에 대한 세부 내용을 다루진 않습니다.\n\n직접 해보며 배우고 싶다면, 다음으로 [자습서: 틱택토 게임](/learn/tutorial-tic-tac-toe)을 확인하는 것을 추천합니다. React로 작은 게임을 구현하는 것을 자세히 설명하면서, 동시에 일상적으로 사용할 기술을 가르칩니다. 여기에 구현하게 될 내용이 있습니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { useState } from 'react';\n\nfunction Square({ value, onSquareClick }) {\n  return (\n    <button className=\"square\" onClick={onSquareClick}>\n      {value}\n    </button>\n  );\n}\n\nfunction Board({ xIsNext, squares, onPlay }) {\n  function handleClick(i) {\n    if (calculateWinner(squares) || squares[i]) {\n      return;\n    }\n    const nextSquares = squares.slice();\n    if (xIsNext) {\n      nextSquares[i] = 'X';\n    } else {\n      nextSquares[i] = 'O';\n    }\n    onPlay(nextSquares);\n  }\n\n  const winner = calculateWinner(squares);\n  let status;\n  if (winner) {\n    status = 'Winner: ' + winner;\n  } else {\n    status = 'Next player: ' + (xIsNext ? 'X' : 'O');\n  }\n\n  return (\n    <>\n      <div className=\"status\">{status}</div>\n      <div className=\"board-row\">\n        <Square value={squares[0]} onSquareClick={() => handleClick(0)} />\n        <Square value={squares[1]} onSquareClick={() => handleClick(1)} />\n        <Square value={squares[2]} onSquareClick={() => handleClick(2)} />\n      </div>\n      <div className=\"board-row\">\n        <Square value={squares[3]} onSquareClick={() => handleClick(3)} />\n        <Square value={squares[4]} onSquareClick={() => handleClick(4)} />\n        <Square value={squares[5]} onSquareClick={() => handleClick(5)} />\n      </div>\n      <div className=\"board-row\">\n        <Square value={squares[6]} onSquareClick={() => handleClick(6)} />\n        <Square value={squares[7]} onSquareClick={() => handleClick(7)} />\n        <Square value={squares[8]} onSquareClick={() => handleClick(8)} />\n      </div>\n    </>\n  );\n}\n\nexport default function Game() {\n  const [history, setHistory] = useState([Array(9).fill(null)]);\n  const [currentMove, setCurrentMove] = useState(0);\n  const xIsNext = currentMove % 2 === 0;\n  const currentSquares = history[currentMove];\n\n  function handlePlay(nextSquares) {\n    const nextHistory = [...history.slice(0, currentMove + 1), nextSquares];\n    setHistory(nextHistory);\n    setCurrentMove(nextHistory.length - 1);\n  }\n\n  function jumpTo(nextMove) {\n    setCurrentMove(nextMove);\n  }\n\n  const moves = history.map((squares, move) => {\n    let description;\n    if (move > 0) {\n      description = 'Go to move #' + move;\n    } else {\n      description = 'Go to game start';\n    }\n    return (\n      <li key={move}>\n        <button onClick={() => jumpTo(move)}>{description}</button>\n      </li>\n    );\n  });\n\n  return (\n    <div className=\"game\">\n      <div className=\"game-board\">\n        <Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />\n      </div>\n      <div className=\"game-info\">\n        <ol>{moves}</ol>\n      </div>\n    </div>\n  );\n}\n\nfunction calculateWinner(squares) {\n  const lines = [\n    [0, 1, 2],\n    [3, 4, 5],\n    [6, 7, 8],\n    [0, 3, 6],\n    [1, 4, 7],\n    [2, 5, 8],\n    [0, 4, 8],\n    [2, 4, 6],\n  ];\n  for (let i = 0; i < lines.length; i++) {\n    const [a, b, c] = lines[i];\n    if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {\n      return squares[a];\n    }\n  }\n  return null;\n}\n```\n\n```css src/styles.css\n* {\n  box-sizing: border-box;\n}\n\nbody {\n  font-family: sans-serif;\n  margin: 20px;\n  padding: 0;\n}\n\n.square {\n  background: #fff;\n  border: 1px solid #999;\n  float: left;\n  font-size: 24px;\n  font-weight: bold;\n  line-height: 34px;\n  height: 34px;\n  margin-right: -1px;\n  margin-top: -1px;\n  padding: 0;\n  text-align: center;\n  width: 34px;\n}\n\n.board-row:after {\n  clear: both;\n  content: '';\n  display: table;\n}\n\n.status {\n  margin-bottom: 10px;\n}\n.game {\n  display: flex;\n  flex-direction: row;\n}\n\n.game-info {\n  margin-left: 20px;\n}\n```\n\n</Sandpack>\n\n또한 많은 사람에게 React를 \"완전히 이해시켜 준\" 자습서인 [React로 사고하기](/learn/thinking-in-react)도 강조하고 싶습니다. **두 가지 클래식 자습서 모두 함수 컴포넌트와 Hook을 사용하도록 업데이트했기에**, 새 자습서만큼 훌륭합니다.\n\n<Note>\n\n위쪽에 있는 예시는 *샌드박스*입니다. 우리는 600개 이상의 많은 샌드박스를 사이트 전체에 추가했습니다. 모든 샌드박스를 편집할 수 있고, 우측 상단에 있는 \"Fork\"를 눌러 별도의 탭에서 열 수 있습니다. 샌드박스는 React API를 빠르게 갖고 놀면서, 아이디어를 탐구하고, 이해를 확인하게 해줍니다.\n\n</Note>\n\n## 단계별로 React 배우기 {/*learn-react-step-by-step*/}\n\n세상에 있는 모든 사람이 React를 무료로 배울 동등한 기회를 가지길 바랍니다.\n\n이것이 학습 섹션이 여러 개의 장으로 구분된 자기 주도 학습 과정으로 구성된 이유입니다. 처음 두 장은 React의 기초에 관해서 설명합니다. React가 처음이거나 기억을 되살리고 싶다면, 여기서부터 시작하세요.\n\n- <strong>[UI 표현하기](/learn/describing-the-ui)</strong>에서는 컴포넌트로 어떻게 정보를 표시하는지 가르칩니다.\n- <strong>[상호작용 추가하기](/learn/adding-interactivity)</strong>에서는 사용자 입력에 대한 응답으로 화면을 어떻게 업데이트하는지 가르칩니다.\n\n다음 두 장은 더욱 고급 내용을 다루며, 더 복잡한 부분에 대해서 깊은 통찰을 줄 것입니다.\n\n- <strong>[State 관리하기](/learn/managing-state)</strong>에서는 앱의 복잡성이 증가함에 따라 어떻게 로직을 조직화하는지 가르칩니다.\n- <strong>[탈출구](/learn/escape-hatches)</strong>에서는 React \"외부로 탈출\"할 방법과, 이를 수행하기에 가장 적절한 시기를 가르칩니다.\n\n모든 장은 여러 개의 관련된 페이지로 구성되어 있습니다. 대부분의 페이지는 특정 기술이나 기법을 가르칩니다. 예를 들어, [JSX로 마크업 작성하기](/learn/writing-markup-with-jsx), [State에 있는 객체 업데이트하기](/learn/updating-objects-in-state)나 [컴포넌트 간 State 공유하기](/learn/sharing-state-between-components) 같은 것들이 있습니다. [렌더링 그리고 커밋](/learn/render-and-commit), [스냅샷으로서의 State](/learn/state-as-a-snapshot)와 같은 몇몇 페이지들은 아이디어를 설명하는 것에 집중합니다. 그리고 지난 몇 년 동안의 경험을 기반으로 제안을 공유하는 [Effect가 필요하지 않을 수도 있습니다](/learn/you-might-not-need-an-effect) 같은 페이지도 몇 개 있습니다.\n\n이러한 장들을 순서대로 읽을 필요는 없습니다. 누가 그런 시간이 있을까요?! 하지만 읽을 수도 있습니다. 학습 섹션에 있는 페이지는 오로지 이전 페이지에 소개된 개념에만 의존합니다. 책처럼 읽고 싶다면, 도전해 보세요!\n\n### 도전 과제로 이해를 확인하세요 {/*check-your-understanding-with-challenges*/}\n\n학습 섹션에 있는 대부분의 페이지는 이해를 확인하기 위한 몇 가지 도전 과제로 끝납니다. 예를 들어, 여기 [조건부 렌더링](/learn/conditional-rendering#challenges) 페이지에 있는 몇 가지 도전 과제가 있습니다.\n\n지금 당장 풀지 않아도 됩니다! *정말로* 원하지 않는다면 말입니다.\n\n<Challenges noTitle={true}>\n\n#### `? :`로 미완료 항목에 대한 아이콘을 보여주세요 {/*show-an-icon-for-incomplete-items-with--*/}\n\n`isPacked` 가 `true`가 아니라면 ❌를 렌더링하기 위해 조건 연산자 (`cond ? a : b`)를 사용하세요.\n\n<Sandpack>\n\n```js\nfunction Item({ name, isPacked }) {\n  return (\n    <li className=\"item\">\n      {name} {isPacked && '✅'}\n    </li>\n  );\n}\n\nexport default function PackingList() {\n  return (\n    <section>\n      <h1>Sally Ride's Packing List</h1>\n      <ul>\n        <Item\n          isPacked={true}\n          name=\"Space suit\"\n        />\n        <Item\n          isPacked={true}\n          name=\"Helmet with a golden leaf\"\n        />\n        <Item\n          isPacked={false}\n          name=\"Photo of Tam\"\n        />\n      </ul>\n    </section>\n  );\n}\n```\n\n</Sandpack>\n\n<Solution>\n\n<Sandpack>\n\n```js\nfunction Item({ name, isPacked }) {\n  return (\n    <li className=\"item\">\n      {name} {isPacked ? '✅' : '❌'}\n    </li>\n  );\n}\n\nexport default function PackingList() {\n  return (\n    <section>\n      <h1>Sally Ride's Packing List</h1>\n      <ul>\n        <Item\n          isPacked={true}\n          name=\"Space suit\"\n        />\n        <Item\n          isPacked={true}\n          name=\"Helmet with a golden leaf\"\n        />\n        <Item\n          isPacked={false}\n          name=\"Photo of Tam\"\n        />\n      </ul>\n    </section>\n  );\n}\n```\n\n</Sandpack>\n\n</Solution>\n\n#### `&&`로 항목의 중요성을 보여주세요 {/*show-the-item-importance-with-*/}\n\n이 예시에서는, 각 `Item`이 숫자로 된 `importance` prop을 받습니다. 오직 중요도가 `0`이 아닌 항목만 \"_(중요도: X)_\"을 이탤릭체로 렌더링하기 위해 `&&` 연산자를 사용하세요. 아이템 목록은 최종적으로 이렇게 되어야 합니다:\n\n* 우주복 _(중요도: 9)_\n* 금색 잎사귀가 달린 헬멧\n* Tam의 사진 _(중요도: 6)_\n\n두 라벨 사이에 공백을 넣는 것을 잊지 마세요!\n\n<Sandpack>\n\n```js\nfunction Item({ name, importance }) {\n  return (\n    <li className=\"item\">\n      {name}\n    </li>\n  );\n}\n\nexport default function PackingList() {\n  return (\n    <section>\n      <h1>Sally Ride's Packing List</h1>\n      <ul>\n        <Item\n          importance={9}\n          name=\"Space suit\"\n        />\n        <Item\n          importance={0}\n          name=\"Helmet with a golden leaf\"\n        />\n        <Item\n          importance={6}\n          name=\"Photo of Tam\"\n        />\n      </ul>\n    </section>\n  );\n}\n```\n\n</Sandpack>\n\n<Solution>\n\n이렇게 하면 해결할 수 있습니다.\n\n<Sandpack>\n\n```js\nfunction Item({ name, importance }) {\n  return (\n    <li className=\"item\">\n      {name}\n      {importance > 0 && ' '}\n      {importance > 0 &&\n        <i>(Importance: {importance})</i>\n      }\n    </li>\n  );\n}\n\nexport default function PackingList() {\n  return (\n    <section>\n      <h1>Sally Ride's Packing List</h1>\n      <ul>\n        <Item\n          importance={9}\n          name=\"Space suit\"\n        />\n        <Item\n          importance={0}\n          name=\"Helmet with a golden leaf\"\n        />\n        <Item\n          importance={6}\n          name=\"Photo of Tam\"\n        />\n      </ul>\n    </section>\n  );\n}\n```\n\n</Sandpack>\n\n만약 `importance`가 `0`일 경우, `0`이 결과로 렌더링 되지 않도록 `importance && ...`보다 `importance > 0 && ...`로 작성해야 한다는 것을 주의하세요!\n\n이 해결책에서는, 독립된 두 개의 조건이 name과 importance 라벨 사이에 공백을 넣기 위해 사용되었습니다. 그 대신에, `importance > 0 && <> <i>...</i></>` 같이 선행 공백에 있는 Fragment를 사용하거나 `importance > 0 && <i> ...</i>` 같이 `<i>`내부에 바로 공백을 추가할 수 있습니다.\n\n</Solution>\n\n</Challenges>\n\n좌측 하단에 있는 \"Show solution\" 버튼을 주목해 보세요. 스스로 확인하고 싶을 때 유용하게 사용할 수 있습니다!\n\n### 다이어그램과 삽화로 직관력을 높여보세요 {/*build-an-intuition-with-diagrams-and-illustrations*/}\n\n코드와 단어만으로 어떤 것을 설명하기 어려운 경우, 직관적으로 도움을 주는 다이어그램을 추가했습니다. 예를 들어, 여기 [State를 보존하고 초기화하기](/learn/preserving-and-resetting-state)에 있는 다이어그램 중 하나가 있습니다.\n\n<Diagram name=\"preserving_state_diff_same_pt1\" height={350} width={794} alt=\"다이어그램에는 세 개의 섹션이 있으며, 각 섹션 사이에 전환되는 화살표가 있습니다. 첫 번째 섹션에는 'div'로 레이블 된 React 컴포넌트가 있습니다. 이 컴포넌트의 단일 자식으로 'section'이라고 레이블 된 섹션이 있으며, 그 안에 'Counter'라고 레이블 된 컴포넌트가 있습니다. 이 컴포넌트 안에는 'count'라고 레이블 된 state 버블이 있으며 값은 3입니다. 중간 섹션에는 동일한 'div' 부모가 있지만, 이제는 하위 컴포넌트들이 삭제되었습니다. 이를 노란색 'proof' 이미지로 표시합니다. 세 번째 섹션에는 다시 동일한 'div' 부모가 있으며, 이번에는 'div'라고 레이블 된 새로운 하위 컴포넌트가 추가되었습니다. 이 컴포넌트 안에는 'Counter'라고 레이블 된 컴포넌트가 있으며, 그 안에 'count'라고 레이블 된 state 버블이 있습니다. 이번에는 값이 0으로 표시됩니다. 모든 부분이 노란색으로 강조되어 있습니다.\">\n\n`section`이 `div`로 변경될 때, `section`은 삭제되고 새로운 `div`가 추가됩니다.\n\n</Diagram>\n\n또한 문서 곳곳에서 몇몇 삽화를 보게 될 것입니다. 여기 [화면을 그리는 브라우저](/learn/render-and-commit#epilogue-browser-paint) 중 하나가 있습니다.\n\n<Illustration alt=\"'카드 요소가 있는 정물화'를 그리는 브라우저\" src=\"/images/docs/illustrations/i_browser-paint.png\" />\n\n브라우저 공급업체에게 이 표현이 100% 과학적으로 정확하다는 확인을 받았습니다.\n\n## 새로운, 상세한 API 참고서 {/*a-new-detailed-api-reference*/}\n\n[API 참고서](/reference/react)에서, 이제 모든 React API는 전용 페이지를 가집니다. 모든 종류의 API들이 포함됩니다.\n\n- [`useState`](/reference/react/useState) 같은 내장 Hook\n- [`<Suspense>`](/reference/react/Suspense) 같은 내장 컴포넌트\n- [`<input>`](/reference/react-dom/components/input) 같은 브라우저 내장 컴포넌트\n- [`renderToPipeableStream`](/reference/react-dom/server/renderToReadableStream) 같은 프레임워크 지향 API\n- [`memo`](/reference/react/memo) 같은 그 밖의 React API\nr\n모든 API 페이지가 *레퍼런스* 와 *사용법*을 포함하는 최소 두 개의 세그먼트로 나뉘어 있다는 것을 알 수 있습니다.\n\n[레퍼런스](/reference/react/useState#reference)는 인자와 반환 값을 나열하여 형식적인 API 서명을 설명합니다. 이는 간결하지만, 해당 API에 익숙하지 않다면 약간 추상적으로 느껴질 수 있습니다. 이것은 API를 어떻게 사용하는지가 아닌, API가 무엇을 하는지를 설명합니다.\n\n[사용법](/reference/react/useState#usage)은 동료나 친구가 설명하는 것처럼 실제로 API를 사용하는 이유와 방법을 보여줍니다. **이는 React 팀에서 각 API가 어떻게 사용되기를 의도한 것인지에 대한 표준적인 시나리오**를 보여줍니다. 색상 있는 코드 스니펫, 서로 다른 API들을 함께 사용하는 예시, 복사 및 붙여넣기 할 수 있는 레시피를 추가했습니다.\n\n<Recipes titleText=\"기본 useState 예시\" titleId=\"examples-basic\">\n\n#### 카운터 (숫자) {/*counter-number*/}\n\n이 예시에서 `count` State 변수는 숫자를 저장합니다. 버튼을 누르면 숫자가 증가합니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function Counter() {\n  const [count, setCount] = useState(0);\n\n  function handleClick() {\n    setCount(count + 1);\n  }\n\n  return (\n    <button onClick={handleClick}>\n      You pressed me {count} times\n    </button>\n  );\n}\n```\n\n</Sandpack>\n\n<Solution />\n\n#### 텍스트 필드 (문자열) {/*text-field-string*/}\n\n이 예시에서 `text` State 변수는 문자열을 저장합니다. 문자를 입력할 때, `handleChange`가 브라우저의 Input DOM 요소로부터 가장 최근에 입력된 값을 읽고 State를 업데이트하기 위해 `setText`를 호출합니다. 이에 따라 현재의 `text`를 아래에 표시할 수 있습니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function MyInput() {\n  const [text, setText] = useState('hello');\n\n  function handleChange(e) {\n    setText(e.target.value);\n  }\n\n  return (\n    <>\n      <input value={text} onChange={handleChange} />\n      <p>You typed: {text}</p>\n      <button onClick={() => setText('hello')}>\n        Reset\n      </button>\n    </>\n  );\n}\n```\n\n</Sandpack>\n\n<Solution />\n\n#### 체크박스 (불리언) {/*checkbox-boolean*/}\n\n이 예시에서 `liked` State 변수는 불리언을 저장합니다. Input 요소를 클릭할 때, `setLiked`가 브라우저 체크박스의 선택 여부에 따라 `liked` state 변수를 업데이트합니다. `liked`는 체크박스 아래에 있는 문구를 렌더링하는 데 사용됩니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function MyCheckbox() {\n  const [liked, setLiked] = useState(true);\n\n  function handleChange(e) {\n    setLiked(e.target.checked);\n  }\n\n  return (\n    <>\n      <label>\n        <input\n          type=\"checkbox\"\n          checked={liked}\n          onChange={handleChange}\n        />\n        I liked this\n      </label>\n      <p>You {liked ? 'liked' : 'did not like'} this.</p>\n    </>\n  );\n}\n```\n\n</Sandpack>\n\n<Solution />\n\n#### 양식 (두 가지 변수) {/*form-two-variables*/}\n\n하나의 컴포넌트에서 여러 개의 State 변수를 선언할 수 있습니다. 각각의 State 변수는 완전히 독립적입니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function Form() {\n  const [name, setName] = useState('Taylor');\n  const [age, setAge] = useState(42);\n\n  return (\n    <>\n      <input\n        value={name}\n        onChange={e => setName(e.target.value)}\n      />\n      <button onClick={() => setAge(age + 1)}>\n        Increment age\n      </button>\n      <p>Hello, {name}. You are {age}.</p>\n    </>\n  );\n}\n```\n\n```css\nbutton { display: block; margin-top: 10px; }\n```\n\n</Sandpack>\n\n<Solution />\n\n</Recipes>\n\n또한 몇몇 API 페이지는 (일반적인 문제에 대한) [트러블슈팅](/reference/react/useEffect#troubleshooting)과 (더 이상 사용하지 않는 API와 관련된) [대안](/reference/react-dom/findDOMNode#alternatives)을 포함하고 있습니다.\n\n이러한 접근 방식이 API 참고서를 단순히 인자를 찾는 용도뿐만 아니라, 각 API가 어떤 다양한 작업을 수행할 수 있는지, 어떻게 다른 API와 연결되어 있는지를 확인하는 데 유용하게 될 것을 기대합니다.\n\n## 다음은 무엇인가요? {/*whats-next*/}\n\n우리의 작은 여정을 마무리할 차례입니다! 새로운 웹 사이트를 둘러보며 마음에 드는 점과 안 드는 점을 찾아보고, [익명 설문조사](https://www.surveymonkey.co.uk/r/PYRPF3X)나 [이슈 트래커](https://github.com/reactjs/react.dev/issues)에 계속해서 피드백을 남겨주세요.\n\n이 프로젝트가 출시되기까지 오랜 시간이 걸렸다는 것을 알고 있습니다. React 커뮤니티에 걸맞은 높은 품질 기준을 유지하고자 했습니다. 이 문서를 작성하고 모든 예시를 만들면서 몇몇 기존 설명에서의 오류, React의 버그, 심지어 현재 해결하기 위해 노력하고 있는 React 디자인의 빈 곳까지 발견했습니다. 새로운 문서가 앞으로 React 자체를 더 높은 기준에 맞추도록 도와줄 것을 기대합니다.\n\n웹 사이트의 내용과 기능을 확장해달라는 많은 요청을 들었습니다. 예를 들어,\n\n- 모든 예시에 대한 TypeScript 버전을 제공하기\n- 업데이트된 성능, 테스트, 접근성 가이드 만들기\n- React 서버 컴포넌트를 지원하는 프레임워크로부터 독립적으로 문서화하기\n- 새로운 문서가 번역되도록 전 세계 커뮤니티와 협업하기\n- 새로운 웹 사이트에 놓친 기능 추가하기 (예를 들어, 이 블로그를 위한 RSS)\n\n이제 [react.dev](https://react.dev/)가 출시되었으니, 제 삼자 React 교육 자료를 \"따라잡는\" 데서 벗어나 새로운 정보를 추가하고 새 웹 사이트를 더욱 개선하는 데 집중할 수 있게 되었습니다.\n\nReact를 배우기에 가장 좋은 시기가 왔다고 생각합니다.\n\n## 누가 작업하고 있나요? {/*who-worked-on-this*/}\n\nReact 팀에서 [Rachel Nabors](https://twitter.com/rachelnabors/)는 프로젝트를 이끌고 (삽화도 제공했습니다), [Dan Abramov](https://twitter.com/dan_abramov)는 커리큘럼을 설계했습니다. 또한 두 사람은 대부분의 내용을 함께 저술했습니다.\n\n물론, 이렇게 큰 프로젝트는 혼자서 진행되는 것이 아닙니다. 감사할 분들이 많습니다!\n\n[Sylwia Vargas](https://twitter.com/SylwiaVargas)는 \"foo/bar/baz\"와 고양이만 있던 예시를 전 세계의 과학자, 예술가, 그리고 도시들을 소개하는 내용으로 개선했습니다. [Maggie Appleton](https://twitter.com/Mappletons)은 간단한 스케치를 명확한 다이어그램 시스템으로 변경했습니다.\n\n추가적인 글쓰기에 기여하신 [David McCabe](https://twitter.com/mcc_abe), [Sophie Alpert](https://twitter.com/sophiebits), [Rick Hanlon](https://twitter.com/rickhanlonii), [Andrew Clark](https://twitter.com/acdlite), [Matt Carroll](https://twitter.com/mattcarrollcode)에게 감사드립니다. 또한 아이디어와 피드백을 주신 [Natalia Tepluhina](https://twitter.com/n_tepluhina)와 [Sebastian Markbåge](https://twitter.com/sebmarkbage)에게 감사드립니다.\n\n웹 사이트 디자인을 해주신 [Dan Lebowitz](https://twitter.com/lebo)와 샌드박스 디자인을 해주신 [Razvan Gradinar](https://dribbble.com/GradinarRazvan)에게 감사드립니다.\n\n프론트엔드 개발에서는, 프로토타입 개발을 해주신 [Jared Palmer](https://twitter.com/jaredpalmer)에게 감사드립니다. UI 개발에 도움을 주신 [ThisDotLabs](https://www.thisdot.co/)의 [Dane Grant](https://twitter.com/danecando)와 [Dustin Goodman](https://twitter.com/dustinsgoodman)에게 감사드립니다. 샌드박스 통합 작업을 진행해 주신 [CodeSandbox](https://codesandbox.io/)의 [Ives van Hoorne](https://twitter.com/CompuIves), [Alex Moldovan](https://twitter.com/alexnmoldovan), [Jasper De Moor](https://twitter.com/JasperDeMoor), [Danilo Woznica](https://twitter.com/danilowoz)에게 감사드립니다. 세부 개발과 색상 및 미세한 세부 사항을 다듬는 디자인 작업을 해주신 [Rick Hanlon](https://twitter.com/rickhanlonii)에게 감사드립니다. 웹 사이트에 새로운 기능을 추가하고 유지하는 데 도움 주신 [Harish Kumar](https://www.strek.in/)와 [Luna Ruan](https://twitter.com/lunaruan)에게 감사드립니다.\n\n알파, 베타 테스트에 참여하기 위해 자발적으로 시간 내어 주신 분들께 큰 감사를 드립니다. 여러분의 열정과 소중한 피드백 덕분에 이 문서를 만들어 낼 수 있었습니다. 특별한 인사를 드리고 싶은 분은 React Conf 2021에서 React 문서를 이용했던 경험을 이야기해 주신 베타 테스터, [Debbie O'Brien](https://twitter.com/debs_obrien) 입니다.\n\n끝으로, 이 노력의 영감이 된 React 커뮤니티에 감사드립니다. 여러분은 우리가 이 일을 하는 이유이며, 새로운 문서가 여러분이 원하는 어떤 사용자 인터페이스든 React를 사용하여 구현하는 데 도움이 되길 바랍니다.\n"
  },
  {
    "path": "src/content/blog/2023/03/22/react-labs-what-we-have-been-working-on-march-2023.md",
    "content": "---\ntitle: \"React Labs: 그동안의 작업 – 2023년 3월\"\nauthor: Joseph Savona, Josh Story, Lauren Tan, Mengdi Chen, Samuel Susla, Sathya Gunasekaran, Sebastian Markbage, and Andrew Clark\ndate: 2023/03/22\ndescription: React Labs 게시글에는 활발히 연구 개발 중인 프로젝트에 대한 내용을 작성합니다. 우리는 지난 업데이트 이후 상당한 발전을 이루었고, 그 내용들을 공유하려고 합니다.\n---\n\n2023년 3월 22일, [Joseph Savona](https://twitter.com/en_JS), [Josh Story](https://twitter.com/joshcstory), [Lauren Tan](https://twitter.com/potetotes), [Mengdi Chen](https://twitter.com/mengdi_en), [Samuel Susla](https://twitter.com/SamuelSusla), [Sathya Gunasekaran](https://twitter.com/_gsathya), [Sebastian Markbåge](https://twitter.com/sebmarkbage), [Andrew Clark](https://twitter.com/acdlite)\n\n---\n\n<Intro>\n\nReact Labs 게시글에는 활발히 연구 개발 중인 프로젝트에 대한 내용을 작성합니다. 우리는 [지난 업데이트](/blog/2022/06/15/react-labs-what-we-have-been-working-on-june-2022) 이후 상당한 발전을 이루었고, 그 내용들을 공유하려고 합니다.\n\n</Intro>\n\n---\n\n## React 서버 컴포넌트 {/*react-server-components*/}\n\nReact 서버 컴포넌트<sup>React Server Components, RSC</sup>는 React 팀에서 설계한 새로운 애플리케이션 아키텍처입니다.\n\n우리는 먼저 [소개 발표](/blog/2020/12/21/data-fetching-with-react-server-components)와 [RFC](https://github.com/reactjs/rfcs/pull/188)에서 RSC에 대한 연구를 공유했습니다. 그 내용을 요약하면, 미리 실행하고 자바스크립트 번들에서 제외할 수 있는 새로운 종류의 컴포넌트인 서버 컴포넌트를 소개하고 있습니다. 서버 컴포넌트는 빌드 중에 실행되어 파일 시스템에서 읽거나 정적 콘텐츠를 가져올 수 있습니다. 또한 서버에서 실행할 수 있어 API를 빌드할 필요 없이 데이터 계층에 접근할 수 있습니다. Props를 통해 서버 컴포넌트에서 상호작용하는 브라우저의 클라이언트 컴포넌트로 데이터를 전달할 수 있습니다.\n\nRSC는 서버 중심의 멀티 페이지 애플리케이션의 간단한 \"요청/응답\" 멘탈 모델에 클라이언트 중심의 싱글 페이지 애플리케이션의 원활한 상호작용을 결합하여 양쪽의 장점을 모두 제공합니다.\n\n지난 업데이트 이후 우리는 제안을 승인하기 위해 [React 서버 컴포넌트 RFC](https://github.com/reactjs/rfcs/blob/main/text/0188-server-components.md)를 병합했습니다. [React 서버 모듈 컨벤션](https://github.com/reactjs/rfcs/blob/main/text/0227-server-module-conventions.md) 제안과 관련된 남아 있는 문제를 해결했고, 동료들과 `\"use client\"` 컨벤션을 따르기로 합의했습니다. 이러한 문서들은 RSC와 호환할 수 있는 구현 방식이 지원해야 하는 것에 대한 명세로도 사용됩니다.\n\n가장 큰 변경 점은 서버 컴포넌트에서 데이터를 가져오는 기본 방법으로 [`async` / `await`](https://github.com/reactjs/rfcs/pull/229)을 도입했다는 점입니다. 또한 Promise의 결과를 읽는 새로운 `use` Hook을 도입하여 클라이언트에서 데이터를 불러오는 것을 지원할 계획입니다. 비록 클라이언트 전용 애플리케이션의 임의의 컴포넌트에서 `async` / `await`을 지원할 수는 없지만, RSC 애플리케이션의 구조와 유사하게 클라이언트 전용 애플리케이션을 구성할 때를 위한 지원을 추가할 계획입니다.\n\n이제 데이터 가져오기<sup>Fetching</sup>가 어느 정도 잘 정리되었으므로 다른 방향을 살펴보고 있습니다. 바로 클라이언트에서 서버로 데이터를 전송하여 데이터베이스 변경을 실행하고 폼을 구현할 수 있도록 하는 것입니다. 서버와 클라이언트의 경계를 넘어 서버 액션 함수를 전달하면 클라이언트가 이를 호출하여 원활한 RPC를 제공할 수 있습니다. 서버 액션은 또한 자바스크립트를 불러오기 전에 점진적으로 향상된 폼을 제공합니다.\n\nReact 서버 컴포넌트는 [Next.js App 라우터](/learn/creating-a-react-app#nextjs-app-router)에 포함되어 있습니다. Next.js에서는 라우터와 깊은 결합을 통해 RSC를 기본 요소로 받아들이는 것을 보여줍니다. 그러나 이 방법이 RSC와 호환할 수 있는 라우터나 프레임워크를 구축하는 유일한 방법은 아닙니다. RSC 명세와 구현에서 제공하는 기능에는 명확한 구분이 있습니다. React 서버 컴포넌트는 호환할 수 있는 React 프레임워크에서 동작하는 컴포넌트에 대한 명세입니다.\n\n우리는 일반적으로 기존 프레임워크를 권장하지만, 직접 사용자 지정 프레임워크를 구축해야 하는 경우도 가능합니다. RSC와 호환할 수 있는 프레임워크를 직접 구축하는 것은 번들러와의 깊은 결합을 필요로하기 때문에 생각만큼 쉽지 않습니다. 현재 세대의 번들러는 클라이언트에서 사용하기에는 훌륭하지만, 서버와 클라이언트 간에 단일 모듈 그래프를 분할하는 것을 우선으로 지원하도록 설계되지 않았습니다. 이것이 지금 RSC를 내장하기 위한 기본 요소를 얻기 위해 번들러 개발자들과 직접 협력하는 이유입니다.\n\n## 에셋 불러오기 {/*asset-loading*/}\n\n[Suspense](/reference/react/Suspense)를 통해 컴포넌트의 데이터나 코드를 불러오는 동안 화면에 표시할 내용을 지정할 수 있습니다. 이를 통해 사용자는 페이지를 불러오는 동안 뿐만 아니라 더 많은 데이터와 코드를 불러오는 라우터 내비게이션 중에서도 점진적으로 더 많은 콘텐츠를 볼 수 있습니다. 그러나 사용자의 관점에서 새로운 콘텐츠가 준비되었는지를 고려할 때 데이터를 불러오고 렌더링하는 것이 모든 것을 알려주지는 않습니다. 기본적으로 브라우저는 스타일시트, 글꼴 및 이미지를 독립적으로 불러오기 때문에 UI 점프와 연속적인 레이아웃 이동이 발생할 수 있습니다.\n\n저희는 Suspense가 스타일시트, 글꼴 및 이미지를 불러오는 생명주기와 완전히 통합되도록 작업하고 있습니다. 이를 통해 React의 콘텐츠가 화면에 표시할 준비가 되었는지 판단할 수 있도록 노력하고 있습니다. 업데이트는 React 컴포넌트 작성 방식에 어떠한 변경도 없이, 더 일관되고 만족을 주는 방식으로 진행할 것입니다. 최적화를 위해 글꼴과 같은 에셋을 컴포넌트에서 직접 미리 불러오는 수동적인 방법도 제공할 것입니다.\n\n현재 이러한 기능들을 구현하고 있으며 곧 더 많은 정보를 공유하겠습니다.\n\n## 문서 메타데이터 {/*document-metadata*/}\n\n애플리케이션 속 여러 페이지와 화면에는 `<title>` 태그, 설명, 그리고 화면과 연관된 다른 `<meta>` 태그와 같은 여러 가지 메타데이터를 가질 수 있습니다. 유지보수의 관점에서 해당 정보를 그 페이지나 화면에 있는 React 컴포넌트에 가깝게 유지하는 것이 더 높은 확장성을 가지고 있습니다. 하지만 메타데이터를 위한 HTML 태그는 일반적으로 애플리케이션의 최상위를 나타내는 컴포넌트가 렌더링하는 문서의 `<head>` 부분에 있어야 합니다.\n\n현재 개발자는 두 가지 기술 중 하나의 방법으로 이 문제를 해결합니다.\n\n한 가지 방법은 `<title>`, `<meta>`, 그리고 그 안의 다른 태그들을 문서의 `<head>`로 이동시키는 특별한 서드파티 컴포넌트를 렌더링하는 방법입니다. 이 방법은 주요 브라우저에서는 작동하지만, Open Graph 파서<sup>Parser</sup>와 같이 클라이언트 측에서 자바스크립트를 실행하지 않는 클라이언트가 많기 때문에 보편적으로 적합하지 않습니다.\n\n또 다른 방법은 페이지를 두 부분으로 나누어 서버 렌더링하는 방법입니다. 먼저 주요 콘텐츠를 렌더링한 후 이러한 모든 태그가 수집됩니다. 그런 다음 수집한 태그를 이용하여 `<head>`를 렌더링합니다. 마지막으로 `<head>`와 주요 콘텐츠를 브라우저로 전송합니다. 이 접근법은 잘 작동하지만, `<head>`가 전송되기 전에 모든 콘텐츠가 렌더링 때까지 기다려야 하므로 [React 18의 Streaming Server Renderer](/reference/react-dom/server/renderToReadableStream)의 장점을 활용할 수 없습니다.\n\n이것이 바로 우리가 컴포넌트 트리 어디에서나 별도의 설정 없이 `<title>`, `<meta>`, 그리고 메타데이터 `<link>` 태그를 렌더링할 수 있는 내장 지원을 추가하는 이유입니다. 이는 완전한 클라이언트 측 코드와 SSR, 그리고 미래의 RSC를 포함한 모든 환경에서 동일한 방식으로 작동합니다. 우리는 곧 이에 대해 더 많은 세부 사항을 공유하겠습니다.\n\n## React 최적화 컴파일러 {/*react-optimizing-compiler*/}\n\n지난 업데이트 이후 우리는 React의 최적화 컴파일러인 [React Forget](/blog/2022/06/15/react-labs-what-we-have-been-working-on-june-2022#react-compiler)의 설계를 적극적으로 반복하며 작업하고 있습니다. 이전에 이를 \"자동 Memoizing 컴파일러\"라고 언급했고, 어떤 의미에서는 사실입니다. 그러나 컴파일러를 구축하면서 React의 프로그래밍 모델을 더 깊이 이해하는 데 도움이 되었습니다. React Forget을 이해하는 더 좋은 방법은 자동 *반응성* 컴파일러입니다.\n\nReact의 핵심 아이디어는 개발자가 현재 State의 함수로 UI를 정의하는 것입니다. 숫자, 문자열, 배열, 객체와 같은 순수한 자바스크립트의 값과 `if`/`else`, `for` 등 표준 자바스크립트트 관용구를 사용하여 컴포넌트 로직을 표현합니다. 멘탈 모델은 애플리케이션 State를 변경할 때마다 React가 다시 렌더링한다는 것입니다. 우리는 이런 간단한 멘탈 모델과 자바스크립트 의미론에 가깝게 유지하는 것이 React 프로그래밍 모델의 중요한 원칙이라고 생각합니다.\n\n문제는 React가 너무 자주 다시 렌더링하는 등 때때로 *지나치게* 반응적일 수 있습니다. 예시로 자바스크립트에서 두 개의 객체나 배열이 동등한지(동일한 key와 값을 가졌는지) 비교하는 저렴한 방법이 없으므로, 렌더링마다 새로운 객체나 배열을 생성하는 것은 React가 엄밀하게 필요한 것보다 더 많은 작업을 수행하게 될 수도 있습니다. 이는 개발자가 컴포넌트가 지나치게 반응적이지 않도록 명시적으로 컴포넌트를 메모해야 함을 의미합니다.\n\nReact Forget의 목표는 React 애플리케이션이 기본적으로 적당한 반응성을 갖도록 하는 것입니다. 즉, 오직 State 값을 *의미 있게* 변경할 때만 애플리케이션을 다시 렌더링 되도록 하는 것입니다. 구현의 관점에서 이는 자동으로 메모하는 것을 의미하지만, 반응성 프레이밍이 React와 Forget을 이해하는 더 좋은 방법이라고 생각합니다. 이에 대해 생각해 볼 수 있는 하나의 방법은 현재의 React가 객체 ID가 변경될 때 다시 렌더링한다는 것입니다. Forget을 사용하면 React는 의미상으로 값을 변경할 때 다시 렌더링하지만, 깊은 비교를 위한 런타임 비용을 발생시키지 않습니다.\n\n구체적인 진행 상황을 이야기하자면, 지난 업데이트 이후 컴파일러 설계를 자동 반응성 방식에 맞추고 내부적으로 컴파일러를 사용하며 얻은 피드백을 포함하기 위해 상당히 많은 반복 작업을 가졌습니다. 작년 말부터 시작한 컴파일러에 대한 몇 가지 중요한 리팩토링을 진행한 후, 이제 Meta에서 제품 환경의 제한된 부분에서 이를 사용하기 시작했습니다. 제품 환경에서 검증이 끝나면 오픈소스로 공개하려고 합니다.\n\n마지막으로, 많은 분들이 컴파일러가 어떻게 동작하는지에 대해 관심을 가져주셨습니다. 컴파일러를 검증하고 오픈소스로 공개할 때 더 많은 세부 사항을 공유할 수 있기를 기대합니다. 하지만 당장 공유할 수 있는 몇 가지가 있습니다.\n\n컴파일러의 핵심 부분을 Babel과 거의 분리했고, 핵심 컴파일러 API는 원본 위치 데이터를 유지하면서 (약간) 오래된 AST를 입력받아 새로운 AST를 반환합니다. 내부적으로는 저수준의 의미 분석을 수행하기 위해 맞춤형 코드 표현과 변환 파이프라인을 사용합니다. 그러나 컴파일러에 대한 기본 공개 인터페이스는 Babel 및 다른 빌드 시스템 플러그인을 통해 이루어집니다. 테스트 용이성을 위해 컴파일러를 호출하여 각 함수의 새로운 버전을 생성하고 교체하는 매우 얇은 래퍼인 Babel 플러그인을 가지고 있습니다.\n\n지난 몇 달 동안 컴파일러를 리팩토링하며 조건문, 반복문, 재할당, 변형과 같은 복잡성을 처리할 수 있는 핵심 컴파일 모델을 개선하는 데 집중하고 싶었습니다. 그러나 자바스크립트에는 if/else, 삼항 연산자, for, for-in, for-of 등 각각의 기능을 표현하는 다양한 방법이 있습니다. 처음부터 언어의 전체 기능을 지원하려고 하면 핵심 모델을 검증하는 시점이 지연되었을 것입니다. 대신, let/const, if/else, for 루프, 객체, 배열, 원시 값, 함수 호출 등 작지만 자바스크립트 언어를 대표하는 하위 집합부터 시작했습니다. 핵심 모델에 대한 자신감을 얻고 내부 추상화를 개선하면서 지원하는 언어의 하위 집합을 확장했습니다. 또한 아직 지원하지 않는 문법에 대해 명시적으로 로깅 진단 정보를 남기고, 지원되지 않는 입력에 대한 컴파일을 건너뛰고 있습니다. Meta의 코드베이스에서 컴파일러를 사용한 후 가장 많이 지원되지 않는 기능이 무엇인지 확인할 수 있는 유틸리티를 가지고 있습니다. 이를 통해 해당 기능들을 우선으로 작업할 수 있습니다. 우리는 전체 언어를 지원하도록 점진적으로 확장할 계획입니다.\n\nReact 컴포넌트의 순수한 자바스크립트를 반응형으로 만들기 위해, 코드가 정확하게 무엇을 원하는지 이해할 수 있도록 의미론적으로 깊은 이해를 하는 컴파일러가 필요합니다. 이러한 접근법을 채택함으로써, 우리는 자바스크립트 내에서 도메인 특화 언어에 국한되지 않고, 언어의 모든 표현 방법을 사용하여 어떠한 복잡도의 제품 코드라도 작성할 수 있는 반응성을 위한 시스템을 만들고 있습니다.\n\n## 오프스크린 렌더링 {/*offscreen-rendering*/}\n\n오프스크린 렌더링은 React에 추가될 추가적인 성능 부담 없이 백그라운드에서 화면을 렌더링하는 기능입니다. [`content-visibility` CSS 프로퍼티](https://developer.mozilla.org/en-US/docs/Web/CSS/content-visibility)를 DOM 엘리먼트뿐만 아니라 React 컴포넌트에서도 작동하는 버전으로 이해하시면 됩니다. 이번 연구 중에 아래와 같은 다양한 사용 사례를 발견했습니다.\n\n- 라우터는 백그라운드에서 화면을 사전 렌더링하여 사용자가 특정 화면으로 이동했을 때 즉시 사용하게 할 수 있습니다.\n- 탭 전환 컴포넌트는 숨겨진 탭의 state를 유지하여 사용자가 진행 상황을 잃지 않고 탭을 전환할 수 있습니다.\n- 가상화된 리스트 컴포넌트는 보이는 창의 위와 아래에 추가적인 행을 사전 렌더링할 수 있습니다.\n- 모달이나 팝업을 열 때 남은 애플리케이션을 \"백그라운드\" 상태로 전환하여 모달을 제외한 모든 항목에 대한 이벤트와 업데이트를 비활성화할 수 있습니다.\n\n대부분의 React 개발자는 React의 오프스크린 API와 직접 상호작용하지는 않을 것입니다. 대신, 오프스크린 렌더링은 라우터나 UI 라이브러리와 결합할 것이며, 해당 라이브러리를 사용하는 개발자는 추가적인 작업 없이 자연스럽게 이점을 누릴 수 있을 것입니다.\n\n핵심은 컴포넌트를 작성하는 방법이 변경되지 않으면서 어떠한 React 트리라도 오프스크린에서 렌더링할 수 있어야 한다는 점입니다. 컴포넌트를 오프스크린에서 렌더링할 때 컴포넌트가 보이기 전까지 실제로 *마운트*하지 않으며 Effect가 실행되지 않습니다. 예시로, 컴포넌트가 처음 나타날 때 `useEffect`를 사용하여 분석 로그를 남기는 경우, 사전 렌더링을 하는 것이 분석의 정확도를 손상하지 않을 것입니다. 마찬가지로 컴포넌트를 오프스크린으로 전환할 때 그 컴포넌트의 Effect 또한 마운트 해제됩니다. 오프스크린 렌더링의 핵심 기능은 컴포넌트의 가시성을 전환하면서도 그 State를 잃지 않는다는 점에 있습니다.\n\n지난 업데이트 이후 Meta 내부에서는 안드로이드와 iOS의 React Native 애플리케이션에서 실험적인 버전의 사전 렌더링을 테스트하였으며, 긍정적인 성능 결과를 얻었습니다. 또한 오프스크린 렌더링이 Suspense와 함께 작동하는 방식도 개선하여 오프스크린 트리 내부에서는 Suspense의 폴백이 발생하지 않도록 했습니다. 남아있는 작업은 라이브러리 개발자에게 제공할 기본 요소를 마무리하는 것입니다. 올해 말 테스트와 피드백을 위한 실험적인 API와 함께 RFC를 게시할 예정입니다.\n\n## 트랜지션 추적 {/*transition-tracing*/}\n\n트랜지션 추적 API를 통해 [React 트랜지션](/reference/react/useTransition)이 느려지는 시점을 감지하고 느려지는 이유를 조사할 수 있습니다. 지난 업데이트 이후 API의 초기 설계를 마무리하고 [RFC](https://github.com/reactjs/rfcs/pull/238)를 공개했습니다. 기본 기능도 함께 구현되었습니다. 이 프로젝트는 현재 보류 중입니다. RFC에 대한 피드백을 환영하며, 개발을 재개하여 React를 위한 더 나은 성능 측정 도구를 제공할 수 있기를 기대합니다. 이는 [Next.js App 라우터](/learn/creating-a-react-app#nextjs-app-router)와 같이 React 트랜지션 위에 구축된 라우터에서는 특히 더 유용할 것입니다.\n\n* * *\n이번 업데이트 외에도 최근 우리 팀은 커뮤니티 팟캐스트와 라이브스트림에 초청자로 출연하여 우리의 작업에 대해 더 많은 이야기를 나누고 질문에 답변했습니다.\n\n* [Dan Abramov](https://twitter.com/dan_abramov)와 [Joe Savona](https://twitter.com/en_JS)는 [Kent C. Dodds의 YouTube 채널](https://www.youtube.com/watch?v=h7tur48JSaw)에서 인터뷰를 통해 React 서버 컴포넌트를 둘러싼 우려 사항들을 논의했습니다.\n* [Dan Abramov](https://twitter.com/dan_abramov)와 [Joe Savona](https://twitter.com/en_JS)는 [JSParty 팟캐스트](https://jsparty.fm/267)의 초청자로서 React의 미래에 대한 생각을 공유했습니다.\n\n이 게시글을 검토해 준 [Andrew Clark](https://twitter.com/acdlite), [Dan Abramov](https://twitter.com/dan_abramov), [Dave McCabe](https://twitter.com/mcc_abe), [Luna Wei](https://twitter.com/lunaleaps), [Matt Carroll](https://twitter.com/mattcarrollcode), [Sean Keegan](https://twitter.com/DevRelSean), [Sebastian Silbermann](https://twitter.com/sebsilbermann), [Seth Webster](https://twitter.com/sethwebster), 그리고 [Sophie Alpert](https://twitter.com/sophiebits)에 감사를 전합니다.\n\n읽어주셔서 감사합니다. 다음 업데이트에서 만나요!\n"
  },
  {
    "path": "src/content/blog/2023/05/03/react-canaries.md",
    "content": "---\ntitle: \"React Canary: Meta 외부에서 점진적 기능 롤아웃 활성화하기\"\nauthor: Dan Abramov, Sophie Alpert, Rick Hanlon, Sebastian Markbage, and Andrew Clark\ndate: 2023/05/03\ndescription: Meta가 오랫동안 내부적으로 최첨단 버전의 React를 사용해 온 것과 유사하게, 새로운 기능이 안정된 버전으로 출시되기 전에 디자인이 거의 완성되는 즉시 개별적인 새로운 기능을 채택할 수 있는 옵션을 React 커뮤니티에 제공하고자 합니다. 공식적으로 지원하는 새로운 Canary 릴리즈 채널을 소개합니다. 프레임워크와 같이 엄선된 설정을 통해 개별 React 기능의 채택을 React 릴리즈 일정에서 분리할 수 있습니다.\n---\n\n2023년 5월 3일, [Dan Abramov](https://twitter.com/dan_abramov), [Sophie Alpert](https://twitter.com/sophiebits), [Rick Hanlon](https://twitter.com/rickhanlonii), [Sebastian Markbåge](https://twitter.com/sebmarkbage), [Andrew Clark](https://twitter.com/acdlite)\n\n---\n\n<Intro>\n\nMeta가 오랫동안 내부적으로 최첨단 버전의 React를 사용해 온 것과 유사하게, 새로운 기능이 안정된 버전으로 출시되기 전에 디자인이 거의 완성되는 즉시 개별적인 새로운 기능을 채택할 수 있는 옵션을 React 커뮤니티에 제공하고자 합니다. 공식적으로 지원하는 새로운 [Canary 릴리즈 채널](/community/versioning-policy#canary-channel)을 소개합니다. 프레임워크와 같이 엄선된 설정을 통해 개별 React 기능의 채택을 React 릴리즈 일정에서 분리할 수 있습니다.\n\n</Intro>\n\n---\n\n## 요약 {/*tldr*/}\n\n* 공식적으로 지원하는 React용 [Canary 릴리즈 채널](/community/versioning-policy#canary-channel)을 소개합니다. 공식적으로 지원하므로 회귀<sup>Regression</sup> 문제가 발생하면 안정된 릴리즈의 버그와 비슷한 수준으로 긴급하게 처리할 것입니다.\n* Canary를 사용하면 Semantic Versioning의 안정된 릴리즈에 적용되기 전에 개별적인 새로운 React 기능을 사용할 수 있습니다.\n* [실험적 채널](/community/versioning-policy#experimental-channel)과 달리, React Canary에는 채택할 준비가 되었다고 합리적으로 판단되는 기능만 포함됩니다. 프레임워크는 고정된 Canary React 릴리즈를 번들로 묶는 것을 고려할 것을 권장합니다.\n* 중요한 변경 사항과 새로운 기능이 Canary 릴리즈에 적용되면 블로그에 공지할 예정입니다.\n* **언제나 그렇듯이 React는 모든 안정된 릴리즈에 대해 Semantic Versioning을 따릅니다.**\n\n## React 기능은 보통 어떻게 개발되나요? {/*how-react-features-are-usually-developed*/}\n\n일반적으로 모든 React 기능은 동일한 단계를 거칩니다.\n\n1. 초기 버전을 개발하고 `experimental_` 또는 `unstable_`접두사를 붙입니다. 이 기능은 `experimental` 릴리즈 채널에서만 사용할 수 있습니다. 이 시점에서 이 기능은 크게 변경될 것으로 예상됩니다.\n2. 이 기능을 테스트하고 피드백을 제공하는 데 도움을 줄 Meta 팀을 찾습니다. 이 피드백을 바탕으로 기능이 변경될 것입니다. 기능이 안정화됨에 따라 Meta의 더 많은 팀과 협력하여 이 기능을 시험해 봅니다.\n3. 최종적으로 설계에 자신감이 생기면, API 이름에서 접두사를 제거하고 대부분의 Meta 제품이 사용하는 `main` 브랜치에서 이 기능을 기본적으로 사용할 수 있도록 합니다. 이제 Meta의 모든 팀이 이 기능을 사용할 수 있습니다.\n4. 방향에 대한 확신이 생기면 새 기능에 대한 RFC도 게시합니다. 이 설계가 광범위한 사례에 적합하다는 것을 이 시점에 알고 있지만, 마지막 순간에 몇 가지 조정을 할 수도 있습니다.\n5. 오픈 소스 릴리즈에 가까워지면 해당 기능에 대한 문서를 작성하고 최종적으로 안정된 React 릴리즈를 통해 기능을 출시합니다.\n\n이 플레이북은 지금까지 출시된 대부분의 기능에 대해 잘 작동합니다. 하지만 일반적으로 기능을 사용할 준비가 된 시점(3단계)과 오픈 소스로 공개되는 시점(5단계) 사이에는 상당한 차이가 있을 수 있습니다.\n\n**저희는 React 커뮤니티에 Meta와 동일한 접근 방식을 따르고 개별적인 새로운 기능을 더 일찍(사용 가능할 때) 채택할 수 있는 옵션을 제공하고자 합니다.**\n\n항상 그렇듯이 모든 React 기능은 결국 안정된 릴리즈에 포함될 것입니다.\n\n## 더 많은 마이너 릴리즈를 할 수 있나요? {/*can-we-just-do-more-minor-releases*/}\n\n일반적으로 새로운 기능을 소개할 때 마이너 릴리즈를 *사용*합니다.\n\n하지만 항상 가능한 것은 아닙니다. 새로운 기능이 아직 완전히 완성되지 않은 *다른* 새로운 기능과 상호 연결되어 있고 여전히 활발하게 반복 작업 중인 경우도 있습니다. 구현이 서로 연관되어 있기 때문에 별도로 릴리즈할 수 없습니다. 같은 패키지에 영향을 미치기 때문에(예시: `react` 및 `react-dom`) 별도로 버전을 배포할 수 없습니다. 또한 주요 버전 릴리즈가 쏟아져 나오지 않는 한 아직 준비되지 않은 부분에 대해 반복 작업을 수행할 수 있는 능력을 유지해야 하는데, 이를 위해서는 Semantic Versioning이 필요합니다.\n\nMeta에서는 `main` 브랜치에서 React를 빌드하고 매주 특정 고정 커밋에 수동으로 업데이트하는 방식으로 이 문제를 해결했습니다. 이는 지난 몇 년 동안 React Native 릴리즈가 따라왔던 접근 방식이기도 합니다. React Native의 모든 *안정된* 릴리즈는 React 저장소의 `main` 브랜치에서 특정 커밋에 고정됩니다. 이를 통해 React Native는 중요한 버그 수정을 포함하고 프레임워크 수준에서 글로벌 React 릴리즈 일정에 얽매이지 않고 새로운 React 기능을 점진적으로 채택할 수 있습니다.\n\n우리는 이 워크플로우를 다른 프레임워크와 선별된 설정에서 사용할 수 있도록 하고 싶습니다. 예를 들어, 이 플로우를 사용하면 React를 *기반으로 하는* 프레임워크가 안정된 React 릴리즈에 포함되기 *전에* React와 관련된 중요한 변경 사항을 포함할 수 있습니다. 일부 변경 사항은 프레임워크 통합에만 영향을 미치기 때문에 이 기능은 특히 유용합니다. 이를 통해 프레임워크는 Semantic Versioning을 중단시키지 않고 자체 마이너 버전에서 이러한 변경 사항을 릴리즈할 수 있습니다.\n\nCanary 채널을 통한 롤링 릴리즈를 통해 더욱 긴밀한 피드백 루프를 확보하고 새로운 기능이 커뮤니티에서 포괄적인 테스트를 거치도록 할 수 있습니다. 이 워크플로우는 자바스크립트 표준 위원회인 TC39가 [번호가 매겨진 단계로 변경 사항을 처리](https://tc39.es/process-document/)하는 방식에 가깝습니다. 새로운 자바스크립트 기능이 명세의 일부로 공식적으로 승인되기 전에 브라우저에서 제공하는 것처럼, 새로운 React 기능은 React 안정 릴리즈에 포함되기 전에 React를 기반으로 구축된 프레임워크에서 사용할 수 있습니다.\n\n## 실험적 릴리즈를 사용하지 않는 이유는 무엇인가요? {/*why-not-use-experimental-releases-instead*/}\n\n기술적으로는 [실험적 릴리즈](/community/versioning-policy#canary-channel)를 사용*할 수* 있지만, 실험적 API는 안정화 과정에서 중대한 변경이 있을 수 있으므로(또는 심지어 완전히 제거될 수도 있으므로) 프로덕션 환경에서 사용하지 않는 것이 좋습니다. Canary에도 실수가 있을 수 있지만(다른 릴리즈와 마찬가지로), 앞으로는 Canary에 중대한 변경 사항이 있으면 블로그에 공지할 계획입니다. Canary는 Meta가 내부적으로 실행하는 코드에 가장 가깝기 때문에 일반적으로 비교적 안정적일 것으로 기대할 수 있습니다. 하지만 버전을 고정*하고* 고정된 커밋 사이를 업데이트할 때는 GitHub 커밋 로그를 수동으로 스캔해야 합니다.\n\n**프레임워크와 같이 엄선된 환경 밖에서 React를 사용하는 대부분 사람은 Stable 릴리즈를 계속 사용하기를 원할 것으로 예상합니다.** 하지만 프레임워크를 구축하는 경우 특정 커밋에 고정된 React의 Canary 버전을 번들로 묶어 원하는 속도로 업데이트하는 것을 고려할 수 있습니다. 이 방법의 장점은 지난 몇 년 동안 React Native가 해왔던 방식과 유사하게, 완성된 개별 React 기능 및 버그 수정을 사용자에게 더 일찍, 자체 릴리즈 일정에 맞춰 제공할 수 있다는 것입니다. 단점은 어떤 React 커밋을 가져오는지 검토하고 릴리즈에 어떤 React 변경 사항이 포함되었는지 사용자에게 알리는 추가적인 책임을 져야 한다는 것입니다.\n\n프레임워크 작성자로서 이 접근 방식을 시도해 보고 싶다면, 저희에게 연락해 주세요.\n\n## 중요한 변경 사항 및 새로운 기능 조기 발표 {/*announcing-breaking-changes-and-new-features-early*/}\n\nCanary 릴리즈는 특정 시점에 다음 안정된 React 릴리즈에 포함될 내용에 대한 최선의 추측을 나타냅니다.\n\n기존에는 릴리즈 주기가 *끝*날 때(주요 릴리즈를 할 때)만 중요한 변경 사항을 발표했습니다. 이제 Canary 릴리즈가 공식적으로 지원하는 React의 사용 방식이 되었으므로, 중요한 변경 사항과 새로운 기능이 Canary에 *적용될 때* 발표하는 방식으로 전환할 계획입니다. 예를 들어, Canary에 출시될 중요한 변경 사항을 병합하는 경우, 필요 시 코드모드 및 마이그레이션 지침을 포함하여 React 블로그에 이에 대한 게시물을 작성할 것입니다. 그런 다음, 프레임워크 작성자가 해당 변경 사항을 포함하도록 고정된 React Canary를 업데이트하는 주요 릴리즈를 자르는 경우 릴리즈 노트에서 블로그 게시물로 링크할 수 있습니다. 마지막으로, 안정된 주요 버전의 React가 준비되면 이미 게시된 블로그 게시물에 링크하여 팀이 더 빠르게 진행하는 데 도움이 되기를 바랍니다.\n\n아직 Canary 외부에서 사용할 수 없는 API라도 Canary에 출시하는 대로 문서화할 계획입니다. Canary에서만 사용할 수 있는 API는 해당 페이지에 특별 메모로 표시될 것입니다. 여기에는 [`use`](https://github.com/reactjs/rfcs/pull/229)와 같은 API와 일부 다른 API(예시: `cache` 및 `createServerContext`)가 포함되며, 이에 대한 RFC를 보내드릴 예정입니다.\n\n## Canary는 반드시 고정해야 합니다. {/*canaries-must-be-pinned*/}\n\n앱이나 프레임워크에 Canary 워크플로우를 채택하기로 한 경우 항상 사용 중인 Canary의 *정확한* 버전을 고정해야 합니다. Canary는 사전 릴리즈이므로 여전히 변경 사항이 포함될 수 있습니다.\n\n## 예시: React 서버 컴포넌트 {/*example-react-server-components*/}\n\n지난 [3월에 발표했듯이](/blog/2023/03/22/react-labs-what-we-have-been-working-on-march-2023#react-server-components) React 서버 컴포넌트 컨벤션은 확정되었으며, 사용자 대면 API 계약과 관련된 중대한 변경 사항은 없을 것으로 예상합니다. 그러나 서로 얽혀 있는 여러 프레임워크 전용 기능([에셋 불러오기](/blog/2023/03/22/react-labs-what-we-have-been-working-on-march-2023#asset-loading)와 같은)에 대한 작업이 진행 중이며, 더 많은 변경 사항이 있을 것으로 예상되기 때문에 아직 안정된 버전의 React에서 React 서버 컴포넌트에 대한 지원을 릴리즈할 수는 없습니다.\n\n즉, React 서버 컴포넌트가 프레임워크에 채택될 준비가 되었다는 뜻입니다. 그러나 다음 주요 React 릴리즈가 나올 때까지 프레임워크가 이를 채택할 수 있는 유일한 방법은 고정된 Canary 버전의 React를 출시하는 것입니다. (두 개의 React 복사본이 번들로 제공되는 것을 피하고자, 이를 원하는 프레임워크는 프레임워크와 함께 제공하는 고정된 Canary에 `react` 및 `react-dom`의 해결 방법을 적용하고 사용자에게 이를 설명해야 합니다. 예를 들어, 이것이 Next.js App Router가 하는 일입니다.)\n\n## Stable 및 Canary 버전 모두에 대해 라이브러리 테스트하기 {/*testing-libraries-against-both-stable-and-canary-versions*/}\n\n라이브러리 작성자가 모든 Canary 릴리즈를 테스트하는 것은 엄청나게 어렵기 때문에 이를 기대하지는 않습니다. 하지만 [3년 전 다양한 React 사전 릴리즈 채널을 처음 도입했을 때](https://legacy.reactjs.org/blog/2019/10/22/react-release-channels.html)와 마찬가지로, 라이브러리 작성자는 최신 Stable 버전과 최신 Canary 버전 *모두*에 대해 테스트를 실행할 것을 권장합니다. 발표되지 않은 동작의 변화를 발견하는 경우, 진단에 도움이 될 수 있도록 React 저장소에 버그를 제출해 주세요. 이 관행이 널리 채택되면 라이브러리가 출시될 때 우발적인 회귀를 발견할 수 있기 때문에 라이브러리를 새로운 주요 버전의 React로 업그레이드하는 데 필요한 노력이 줄어들 것으로 기대합니다.\n\n<Note>\n\n엄밀히 말하면, Canary는 *새로운* 릴리즈 채널이 아니며 이전에는 Next라고 불렀습니다. 하지만 Next.js와의 혼동을 피하고자 이름을 변경하기로 했습니다. 공식적으로 지원하는 React 사용 방법인 Canary와 같은 새로운 기대치를 전달하기 위해 *새로운* 릴리즈 채널로 발표하게 되었습니다.\n\n</Note>\n\n## 안정된 릴리즈는 이전과 동일하게 작동합니다. {/*stable-releases-work-like-before*/}\n\nReact 릴리즈에는 어떠한 변경 사항도 도입하지 않습니다.\n"
  },
  {
    "path": "src/content/blog/2024/02/15/react-labs-what-we-have-been-working-on-february-2024.md",
    "content": "---\ntitle: \"React Labs: 그동안의 작업 - 2024년 2월\"\nauthor: Joseph Savona, Ricky Hanlon, Andrew Clark, Matt Carroll, and Dan Abramov\ndate: 2024/02/15\ndescription: React Labs 게시글에는 활발히 연구 개발 중인 프로젝트에 대한 내용을 작성합니다. 우리의 지난 업데이트 이후 상당한 발전을 이루었고, 이러한 진전 사항을 공유하려고 합니다.\n---\n\n2024년 2월 15일, [Joseph Savona](https://twitter.com/en_JS), [Ricky Hanlon](https://twitter.com/rickhanlonii), [Andrew Clark](https://twitter.com/acdlite), [Matt Carroll](https://twitter.com/mattcarrollcode), [Dan Abramov](https://twitter.com/dan_abramov)\n\n---\n\n<Intro>\n\nReact Labs 게시글에는 활발히 연구 개발 중인 프로젝트에 대한 내용을 작성합니다. 우리는 [지난 업데이트](/blog/2023/03/22/react-labs-what-we-have-been-working-on-march-2023) 이후 상당한 발전을 이루었고, 이러한 진전 사항을 공유하려고 합니다.\n\n</Intro>\n\n---\n\n## React 컴파일러 {/*react-compiler*/}\n\nReact 컴파일러는 더 이상 연구 프로젝트가 아닙니다. 컴파일러는 현재 instagram.com의 프로덕션 단계에서 작동하고 있으며, Meta의 더 많은 서비스에 컴파일러를 적용하고 첫 번째 오픈소스 배포를 준비하기 위해 노력하고 있습니다.\n\n[이전 게시글](/blog/2023/03/22/react-labs-what-we-have-been-working-on-march-2023#react-optimizing-compiler)에서 논의한 바와 같이, React는 State가 변경될 때 *이따금* 너무 자주 다시 렌더링 될 수 있습니다. React 초기부터 이런 경우에 대한 해답은 수동 메모이제이션<sup>Memoization</sup>이었습니다. 현재 API에서 이는 [`useMemo`](/reference/react/useMemo), [`useCallback`](/reference/react/useCallback), 그리고 [`memo`](/reference/react/memo) API를 적용하여 State 변경에 따른 React의 다시 렌더링되는 양을 수동으로 조정하는 것을 의미합니다. 그러나 수동 메모이제이션은 절충안입니다. 이는 우리의 코드를 복잡하게 만들고, 잘못 이해하기 쉬우며, 최신 State를 유지하기 위해 추가 작업이 필요합니다.\n\n수동 메모이제이션은 합리적인 절충안이지만, 만족하지 못했습니다. 우리의 비전은 State 변경 시 React가 *핵심적인 멘탈 모델을 손상하지 않으면서* UI의 정확한 부분만 *자동으로* 다시 렌더링하는 것입니다. 우리는 UI가 표준 자바스크립트 값과 패턴으로 이루어진 State의 단순한 함수라고 생각하는 React의 접근 방식이 바로 수많은 개발자가 React에 쉽게 접근할 수 있었던 핵심 이유라고 생각합니다. 이것이 우리가 React를 위한 최적화 컴파일러를 구축하기 위해 투자한 이유입니다.\n\n자바스크립트는 느슨한 규칙과 동적인 특징 때문에 최적화하기에 매우 까다로운 언어입니다. React 컴파일러는 자바스크립트 규칙과 *함께* \"React 규칙\"을 모두 모델링하여 코드를 안전하게 컴파일할 수 있습니다. 예를 들어, React 컴포넌트는 동일한 입력이 주어지면 동일한 값을 반환하는 멱등성을 만족해야 하며, Props나 State를 변경하지 못합니다. 이러한 규칙은 개발자가 작업할 수 있는 범위를 제한하고 컴파일러가 최적화할 수 있는 안전한 공간을 만들어 나가는 데 도움을 줍니다.\n\n물론, 개발자들이 가끔 규칙을 약간 비틀 수 있다는 것을 이해하고 있습니다. 우리의 목표는 React 컴파일러가 가능한 많은 코드에서 즉시 작동하도록 하는 것입니다. 컴파일러는 코드가 React 규칙을 엄격하게 따르고 있는지 탐지하려고 시도하며, 안전한 경우에는 컴파일하거나 안전하지 않은 경우에는 컴파일을 건너뛸 것입니다. 우리는 Meta의 크고 다양한 코드 베이스를 대상으로 테스트하며 이러한 접근법을 검증하는 데 도움을 주고 있습니다.\n\n자신의 코드가 React 규칙을 따르고 있는지 확인하고 싶은 개발자들에게, 우리는 [Strict Mode를 활성화](/reference/react/StrictMode)하고 [React의 ESLint 플러그인을 설정하는 것](/learn/editor-setup#linting)을 권장합니다. 이러한 도구들은 React 코드에서의 미묘한 오류를 잡고, 현재 애플리케이션의 품질을 향상하는 데 도움을 줄 수 있습니다. 또한, React 컴파일러와 같은 향후 다가올 기능들에 대비하여 애플리케이션을 준비하는 데 도움을 줄 수 있습니다. 우리는 또한 React 규칙에 대한 통합 문서와 ESLint 플러그인 업데이트를 작업하고 있으며, 이를 통해 팀에서 이러한 규칙들을 이해하고 적용하여 더욱 견고한 애플리케이션을 만들 수 있도록 도울 것입니다.\n\n컴파일러를 실제로 보고 싶다면, [지난 가을에 진행한 강연](https://www.youtube.com/watch?v=qOQClO3g8-Y)을 확인해 보세요. 강연 당시, 우리는 instagram.com의 한 페이지에 React 컴파일러를 시도한 초기 실험 데이터를 가지고 있었습니다. 그 이후로, 우리는 컴파일러를 instagram.com의 프로덕션 단계 전반에 걸쳐 적용했습니다. 또한 저희 팀을 확장하여 메타에서 출시할 추가적인 서비스와 오픈 소스의 출시를 가속했습니다. 저희는 앞으로의 길에 대해 큰 기대를 가지고 있고, 향후 몇 달 안에 더 많은 소식을 공유할 것입니다.\n\n## 액션 {/*actions*/}\n\n\n저희는 [이전에](/blog/2023/03/22/react-labs-what-we-have-been-working-on-march-2023#react-server-components) 서버 액션을 통한 클라이언트에서 서버로 데이터를 보내는 해결책을 탐색하고 있다고 공유했습니다. 이를 통해 데이터베이스 변경을 실행하고 폼을 구현할 수 있습니다. 서버 액션을 개발하는 동안, 이러한 API가 클라이언트 전용 애플리케이션에서도 데이터 처리를 지원하도록 확장했습니다.\n\n우리는 이러한 광범위한 기능 모음을 단순히 \"액션\"이라고 부릅니다. 액션을 사용하여 [`<form/>`](/reference/react-dom/components/form)과 같은 DOM 엘리먼트에 함수를 전달할 수 있습니다.\n\n```js\n<form action={search}>\n  <input name=\"query\" />\n  <button type=\"submit\">Search</button>\n</form>\n```\n\n`action` 함수는 동기적 또는 비동기적으로 실행할 수 있습니다. 클라이언트 측에서 표준 자바스크립트를 사용하여 정의하거나, [`'use server'`](/reference/rsc/use-server)라는 지시어를 사용하여 서버에서 정의할 수 있습니다. 액션을 사용할 때, React는 [`useFormStatus`](/reference/react-dom/hooks/useFormStatus), [`useActionState`](/reference/react/useActionState)와 같은 Hook을 제공하여 데이터 제출의 생명주기를 관리하고 현재 폼의 State와 응답에 접근할 수 있습니다.\n\n기본적으로 액션은 [트랜지션](/reference/react/useTransition) 내에서 제출되어 현재 페이지가 상호작용을 하는 동안 액션을 처리합니다. 액션은 비동기 함수를 지원하므로, 트랜지션 내에서 `async`/`await`을 사용할 수 있도록 추가했습니다. 이를 통해 `fetch`와 같은 비동기 요청이 시작되면 트랜지션의 `isPending` State를 사용하여 대기 중인 UI를 표시하고, 업데이트가 적용될 때까지 대기 중인 UI를 계속 보여줄 수 있습니다.\n\n액션과 함께, 낙관적 State 업데이트를 관리하기 위한 [`useOptimistic`](/reference/react/useOptimistic)이라는 기능을 소개합니다. 이 Hook을 사용하여 최종 State가 커밋되면 자동으로 되돌릴 수 있는 일시적인 업데이트를 적용할 수 있습니다. 액션의 경우, 제출이 성공적이라고 가정하고 클라이언트에서 데이터의 최종 State를 낙관적으로 업데이트할 수 있습니다. 그리고 서버에서 받은 데이터를 통해 값을 되돌릴 수 있습니다. 이는 일반적인 `async`/`await` 방식을 사용하여 작동하므로, 클라이언트에서 `fetch`를 사용하든 서버에서 서버 액션을 사용하든 동일하게 작동합니다.\n\n라이브러리 제작자는 각자의 컴포넌트에서 `useTransition`를 사용하여 사용자 지정 `action={fn}` Props를 구현할 수 있습니다. 우리의 의도는 라이브러리가 컴포넌트 API를 설계할 때 액션 패턴을 채택하여 React 개발자들에게 일관적인 경험을 제공하는 것입니다. 예를 들어, 라이브러리에서 `<Calendar onSelect={eventHandler}>`와 같은 컴포넌트를 제공하는 경우, `<Calendar selectAction={action}>`와 같은 API도 노출하는 것을 고려해 보세요.\n\n우리는 처음에 서버 액션을 클라이언트-서버 사이 데이터 전송에 중점을 두었지만, React의 철학은 모둔 플랫폼과 환경에서 동일한 프로그래밍 모델을 제공하는 것입니다. 가능하다면 클라이언트의 기능을 소개한 경우 해당 기능이 서버에서도 작동하도록 하고, 그 반대의 경우도 마찬가지입니다. 이 철학을 통해 애플리케이션이 실행되는 위치와 관계없이 작동하는 단일 API 모음을 생성하여, 이후 다른 환경으로 업그레이드 하기 쉽게 만들 수 있습니다.\n\n액션은 현재 실험적 채널에서 이용하실 수 있으며, React의 다음 배포에서 제공될 예정입니다.\n\n## React Canary의 새로운 기능 {/*new-features-in-react-canary*/}\n\n우리는 [React Canary](/blog/2023/05/03/react-canaries)에서 새로운 안정적 기능들이 최종 설계에 가까워질 때마다 각각을 선택적으로 채택할 수 있도록 소개했습니다. 그리고 이를 안정적인 시멘틱 버저닝으로 배포되기 이전에 사용할 수 있습니다.\n\nCanary는 React의 개발 방식을 변경하는 것입니다. 이전에는 기능들이 비공개로 Meta 내부에서 연구되고 개발되었기 때문에 사용자는 그 결과물이 안정적인 버전으로 배포되었을 때만 볼 수 있었습니다. 이제 Canary와 함께 커뮤니티의 도움을 받아 React Labs 블로그 시리즈에서 공유하고 있는 기능을 완성하고자 공개적으로 개발하고 있습니다. 이는 여러분들이 새로운 기능이 완성된 이후가 아닌 완료되는 과정에서 곧바로 알 수 있다는 것을 의미합니다.\n\nReact 서버 컴포넌트, 에셋 불러오기, 문서 메타데이터 및 액션 모두 React Canary에 도입되었으며, 이러한 기능에 대한 문서를 react.dev에 추가했습니다.\n\n- **지시어**: [`\"use client\"`](/reference/rsc/use-client)와 [`\"use server\"`](/reference/rsc/use-server)는 풀스택 React 프레임워크를 위해 설계한 번들러 기능입니다. 이들은 두 환경 사이의 \"분할점\"을 나타냅니다. `\"use client\"`는 [Astro Islands](https://docs.astro.build/en/concepts/islands/#creating-an-island)처럼 번들러에 `<script>` 태그를 생성하도록 지시합니다. 반면 `\"use server\"`는 [tRPC Mutations](https://trpc.io/docs/concepts)처럼 번들러에 POST 엔드포인트를 생성하도록 지시합니다. 두 지시어를 함께 사용하여 클라이이언트 측의 상호작용을 서버 측의 로직과 결합하는 재사용 가능한 컴포넌트를 작성할 수 있습니다.\n\n- **문서 메타데이터**: 우리는 컴포넌트 트리 어디에서든 [`<title>`](/reference/react-dom/components/title), [`<meta>`](/reference/react-dom/components/meta) 및 메타데이터 [`<link>`](/reference/react-dom/components/link) 태그를 렌더링하는 내장 지원을 추가했습니다. 이는 완전한 클라이언트 측 코드, SSR 및 RSC를 포함한 모든 환경에서 동일하게 작동합니다. 이는 [React Helmet](https://github.com/nfl/react-helmet)과 같은 라이브러리가 이미 제공하던 기능을 내장 지원으로 제공합니다.\n\n- **에셋 불러오기**: 우리는 Suspense를 스타일시트, 글꼴, 스크립트와 같은 리소스를 불러오는 생명주기와 통합했습니다. 이를 통해 React는 표시할 준비가 되었는지 결정하는데 [`<style>`](/reference/react-dom/components/style), [`<link>`](/reference/react-dom/components/link) 및 [`<script>`](/reference/react-dom/components/script)와 같은 엘리먼트 내부의 콘텐츠를 고려합니다. 또한 `preload`와 `preinit`과 같은 [새로운 리소스 불러오기 API](/reference/react-dom#resource-preloading-apis)를 추가하여 리소스를 언제 불러오고 초기화할지에 대한 더 많은 제어권을 제공했습니다.\n\n- **액션**: 앞서 언급한 대로, 클라이언트에서 서버로 데이터를 전송하는 것을 관리하기 위해 액션을 추가했습니다. [`<form/>`](/reference/react-dom/components/form)과 같은 엘리먼트에 `action`을 추가할 수 있으며, [`useFormStatus`](/reference/react-dom/hooks/useFormStatus)를 사용하여 진행 상황에 접근하고, [`useActionState`](/reference/react/useActionState)를 사용하여 결과를 처리하며, [`useOptimistic`](/reference/react/useOptimistic)를 사용하여 UI를 낙관적으로 업데이트할 수 있습니다.\n\n이러한 모든 기능은 함께 작동하기 때문에, 이들을 각각 안정적 채널에 배포하기는 어렵습니다. 폼 State에 접근하는 보조 Hook 없이 액션을 배포하는 것은 액션의 실제 유용성을 제한할 것입니다. 서버 액션을 통합하지 않으면서 React 서버 컴포넌트를 도입하면 서버에서 데이터를 수정하기에 복잡해질 것입니다.\n\n저희는 이러한 기능들을 안정적 채널에 배포하기 전에, 이들이 함께 작동하고 개발자가 프로덕션 단계에서 사용하기 위해 필요한 모든 것을 갖추었는지 보장해야 합니다. React Canary를 통해 개발자는 이런 기능을 개별적으로 개발하고, 전체 기능 목록이 완성되기 전까지 점진적으로 안정된 API를 배포할 수 있습니다.\n\n현재 React Canary에 포함된 기능들은 완성되어 배포할 준비가 되었습니다.\n\n## React의 다음 메이저 버전 {/*the-next-major-version-of-react*/}\n\n몇 년간의 반복 작업 끝에, `react@canary`가 이제 `react@latest`로의 출시 준비가 되었습니다. 위에서 언급한 새로운 기능들은 애플리케이션이 실행되는 모든 환경과 호환되며, 프로덕션 단계에서 사용되기 위해 필요한 모든 것을 제공합니다. 에셋 불러오기와 문서 메타데이터는 일부 애플리케이션에서 큰 변화일 수 있습니다. 따라서 React의 다음 버전은 주요 버전인 **React 19**가 될 것입니다.\n\n여전히 출시 준비를 마치기 위한 더 많은 작업이 있습니다. React 19에서는 Web 컴포넌트 지원과 같은 큰 변화가 필요한 오랫동안 요청된 개선 사항도 추가할 것입니다. 우리는 현재 이러한 변경 사항을 적용하고, 배포를 준비하며, 새로운 기능에 대한 문서 작업을 끝마치고, 무엇이 추가되었는지 공지를 발표하는 것을 중점으로 하고 있습니다.\n\n앞으로 몇 달 동안 React 19에 포함된 모든 내용과 새로운 클라이언트 기능을 채택하는 방법, 그리고 React 서버 컴포넌트를 지원하는 방법에 대한 많은 정보를 공유할 예정입니다.\n\n## 오프스크린 (Activity로 이름 변경) {/*offscreen-renamed-to-activity*/}\n\n지난 업데이트 이후, 우리는 연구 중인 특성의 이름을 \"오프스크린\"에서 \"Activity\"로 변경했습니다. \"오프스크린\" 이라는 이름은 애플리케이션의 보이지 않는 부분에 해당하는 의미만 암시했습니다. 하지만 해당 기능을 연구하는 동안, 모달 뒤편의 콘텐츠와 같이 애플리케이션의 일부가 가시적이지만 비활성화될 수 있다는 것을 알게 되었습니다. 새로운 이름은 애플리케이션의 특정 부분을 \"활성화\" 또는 \"비활성화\"로 표시하는 동작을 더 정확하게 반영합니다.\n\nActivity는 여전히 연구 중이며, 라이브러리 개발자에게 노출되는 기본 요소를 마무리하는 것이 남아 있습니다. 우리는 더욱 완성된 기능을 출시하는 데 중점을 두는 동안, 이러한 영역의 우선순위를 낮췄습니다.\n\n* * *\n\n이번 업데이트 외에도 우리 팀은 컨퍼런스에서 발표하고 팟캐스트 출연을 통해 우리의 작업에 관해 이야기를 나누고 질문에 답변했습니다.\n\n- [Sathya Gunasekaran](https://github.com/gsathya)은 [React India](https://www.youtube.com/watch?v=kjOacmVsLSE) 컨퍼런스에서 React 컴파일러에 관해 이야기했습니다.\n\n- [Dan Abramov](/community/team#dan-abramov)은 [RemixConf](https://www.youtube.com/watch?v=zMf_xeGPn6s)에서 \"다른 차원의 React\"를 주제로 강연했습니다. 이곳에서 React 서버 컴포넌트와 액션을 어떻게 만들었는지에 관한 대안적인 역사를 탐구했습니다.\n\n- [Dan Abramov](/community/team#dan-abramov)은 [Changelog의 JS Party 팟캐스트](https://changelog.com/jsparty/311)에서 React 서버 컴포넌트에 대한 인터뷰를 받았습니다.\n\n- [Matt Carroll](/community/team#matt-carroll)은 [Front-End Fire 팟캐스트](https://www.buzzsprout.com/2226499/14462424-interview-the-two-reacts-with-rachel-nabors-evan-bacon-and-matt-carroll)에서 인터뷰를 통해 [The Two Reacts](https://overreacted.io/the-two-reacts/)에 관해 이야기했습니다.\n\n이 게시물을 검토해 준 [Lauren Tan](https://twitter.com/potetotes), [Sophie Alpert](https://twitter.com/sophiebits), [Jason Bonta](https://threads.net/someextent), [Eli White](https://twitter.com/Eli_White) 및 [Sathya Gunasekaran](https://twitter.com/_gsathya)에게 감사드립니다.\n\n읽어주셔서 감사합니다. 그리고 [React Conf에서 만나요](https://conf.react.dev/)!\n\n"
  },
  {
    "path": "src/content/blog/2024/04/25/react-19-upgrade-guide.md",
    "content": "---\ntitle: \"React 19 업그레이드 가이드\"\nauthor: Ricky Hanlon\ndate: 2024/04/25\ndescription: React 19에 추가된 개선 사항들로 인해 일부 주요한 변경 사항이 있지만, 업그레이드를 가능한 원활하게 진행할 수 있도록 노력했으며 대부분의 앱에 큰 영향이 없을 것으로 예상합니다. 이 글에서는 앱과 라이브러리를 React 19로 업그레이드하는 단계를 안내합니다.\n---\n\n2024년 4월 25일, [Ricky Hanlon](https://twitter.com/rickhanlonii)\n\n---\n\n\n<Intro>\n\nReact 19에 추가된 개선 사항들로 인해 일부 주요한 변경 사항<sup>Breaking Changes</sup>이 있지만, 업그레이드를 가능한 한 원활하게 진행할 수 있도록 노력했으며 대부분의 앱에 큰 영향이 없을 것으로 예상합니다.\n\n</Intro>\n\n<Note>\n\n#### React 18.3도 함께 출시되었습니다 {/*react-18-3*/}\n\nReact 19으로의 업그레이드를 더 쉽게 돕기 위해 `react@18.3`을 출시했습니다. 이 버전은 18.2와 동일하지만, 더 이상 사용되지 않는 API 및 React 19에 필요한 변경 사항에 대한 경고를 추가했습니다.\n\nReact 19로 업그레이드하기 전에 먼저 React 18.3으로 업데이트하여 잠재적인 문제를 미리 파악하는 것을 권장합니다.\n\n18.3 버전의 변경 사항들은 [릴리스 노트](https://github.com/facebook/react/blob/main/CHANGELOG.md#1830-april-25-2024)에서 확인할 수 있습니다.\n\n</Note>\n\n이 글에서는 React 19로 업그레이드하는 방법을 단계별로 안내합니다.\n\n- [설치](#installing)\n- [Codemods](#codemods)\n- [주요한 변경 사항](#breaking-changes)\n- [사용 중단된 사항](#new-deprecations)\n- [주목할 만한 변경 사항](#notable-changes)\n- [TypeScript 변경 사항](#typescript-changes)\n- [변경 로그](#changelog)\n\nReact 19를 테스트해 보고 싶다면 해당 가이드에 나와 있는 단계를 따라주시고, 문제가 발생하면 [이슈를 제보해 주세요](https://github.com/facebook/react/issues/new?assignees=&labels=React+19&projects=&template=19.md&title=%5BReact+19%5D). React 19에 새롭게 추가된 기능 목록은 [React 19 릴리스 게시글](/blog/2024/12/05/react-19)에서 확인할 수 있습니다.\n\n---\n## 설치 {/*installing*/}\n\n<Note>\n\n#### 이제 새로운 JSX 변환 방식은 필수입니다 {/*new-jsx-transform-is-now-required*/}\n\n2020년에 [새로운 JSX 변환](https://legacy.reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html)을 도입하여 번들 크기를 줄이고 React를 import 하지 않고도 JSX를 사용할 수 있도록 했습니다. React 19에서는 Ref를 Prop으로 사용할 수 있는 기능이나 JSX 성능 향상과 같은 추가적인 개선 사항이 도입되며, 이러한 기능들은 새로운 변환<sup>New Transform</sup>이 필요합니다.\n\n새로운 변환이 활성화되지 않으면 다음과 같은 경고가 표시됩니다.\n\n<ConsoleBlockMulti>\n\n<ConsoleLogLine level=\"error\">\n\nYour app (or one of its dependencies) is using an outdated JSX transform. Update to the modern JSX transform for faster performance: https://react.dev/link/new-jsx-transform\n\n</ConsoleLogLine>\n\n</ConsoleBlockMulti>\n\n\n대부분의 환경에서는 이미 활성화되어 있기 때문에 대부분의 앱은 영향을 받지 않으리라고 예상됩니다. 수동으로 업그레이드하는 방법은 [해당 공지](https://legacy.reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html)를 참고하세요.\n\n</Note>\n\n\n최신 버전의 React 및 React DOM을 설치하기 위해 다음 명령어를 입력하세요.\n\n```bash\nnpm install --save-exact react@^19.0.0 react-dom@^19.0.0\n```\n\nYarn을 사용한다면 다음 명령어를 입력하세요.\n\n```bash\nyarn add --exact react@^19.0.0 react-dom@^19.0.0\n```\n\nTypeScript를 사용한다면 타입에 대한 업데이트도 필요합니다.\n```bash\nnpm install --save-exact @types/react@^19.0.0 @types/react-dom@^19.0.0\n```\n\nYarn을 사용한다면 다음 명령어를 입력하세요.\n```bash\nyarn add --exact @types/react@^19.0.0 @types/react-dom@^19.0.0\n```\n\n가장 흔한 교체 작업을 위한 Codemod도 포함되어 있습니다. 아래의 [TypeScript 변경 사항](#typescript-changes)을 참고하세요.\n\n## Codemods {/*codemods*/}\n\n업그레이드를 돕기 위해 [codemod.com](https://codemod.com) 팀과 협력하여 React 19의 새로운 API 및 패턴에 맞게 코드를 자동으로 업데이트해 주는 Codemod를 공개했습니다.\n\n모든 Codemod는 [`react-codemod` 저장소](https://github.com/reactjs/react-codemod)에 있으며, Codemod 팀도 유지보수 하는 데 함께하고 있습니다. Codemod를 실행할 때는 `react-codemod`보다 `codemod` 명령어 사용을 권장합니다. 왜냐하면 해당 명령어로 실행했을 때 더 빠르고, 더 복잡한 코드 마이그레이션을 처리하고, TypeScript에 대한 더 나은 지원을 제공합니다.\n\n\n<Note>\n\n#### React 19 codemod 전체 실행 {/*run-all-react-19-codemods*/}\n\n이 가이드에 나열된 모든 Codemod를 React 19의 `codemod` 레시피를 통해 한 번에 실행하려면 다음 명령어를 입력하세요.\n\n```bash\nnpx codemod@latest react/19/migration-recipe\n```\n\n이 명령어를 실행하면 `react-codemod`에서 아래 Codemod가 실행됩니다.\n- [`replace-reactdom-render`](https://github.com/reactjs/react-codemod?tab=readme-ov-file#replace-reactdom-render)\n- [`replace-string-ref`](https://github.com/reactjs/react-codemod?tab=readme-ov-file#replace-string-ref)\n- [`replace-act-import`](https://github.com/reactjs/react-codemod?tab=readme-ov-file#replace-act-import)\n- [`replace-use-form-state`](https://github.com/reactjs/react-codemod?tab=readme-ov-file#replace-use-form-state)\n- [`prop-types-typescript`](https://github.com/reactjs/react-codemod#react-proptypes-to-prop-types)\n\n이 명령어는 TypeScript 변경 사항은 포함하지 않습니다. 아래의 [TypeScript 변경 사항](#typescript-changes)을 참고하세요.\n\n</Note>\n\nCodemod가 포함된 변경 사항에는 아래와 같이 명령어가 함께 제공됩니다.\n\n사용할 수 있는 모든 Codemod 목록은 [`react-codemod` 저장소](https://github.com/reactjs/react-codemod)를 참고하세요.\n\n## 주요한 변경 사항<sup>Breaking Changes</sup> {/*breaking-changes*/}\n\n### 렌더링 중에 발생한 오류는 re-throw 하지 않음 {/*errors-in-render-are-not-re-thrown*/}\n\n이전 버전의 React에서는 렌더링 중에 발생한 오류를 잡아서 re-throw 했습니다. 개발 모드<sup>DEV</sup>에서는 `console.error`로도 로그를 출력하여 오류 로그가 중복되는 문제가 있었습니다.\n\nReact 19에서는 [오류 처리 방식을 개선하여](/blog/2024/04/25/react-19#error-handling) 더 이상 오류를 re-throw 하지 않음으로써 중복 로그를 줄였습니다.\n\n- **포착되지 않은 오류**: Error Boundary에서 잡히지 않은 오류는 `window.reportError`로 보고됩니다.\n- **포착된 오류**: Error Boundary에서 잡힌 오류는 `console.error`로 보고됩니다.\n\n이 변경은 대부분의 앱에 영향을 주지 않지만, 프로덕션 환경에서의 오류 보고가 re-throw에 의존하고 있다면 오류 처리 방식을 업데이트해야 할 수 있습니다. 이를 지원하기 위해 `createRoot` 및 `hydrateRoot`에 사용자 정의 오류 처리를 위한 새로운 메서드가 추가되었습니다.\n\n```js [[1, 2, \"onUncaughtError\"], [2, 5, \"onCaughtError\"]]\nconst root = createRoot(container, {\n  onUncaughtError: (error, errorInfo) => {\n    // ... log error report\n  },\n  onCaughtError: (error, errorInfo) => {\n    // ... log error report\n  }\n});\n```\n\n자세한 내용은 [`createRoot`](https://react.dev/reference/react-dom/client/createRoot) 및 [`hydrateRoot`](https://react.dev/reference/react-dom/client/hydrateRoot) 문서를 참고하세요.\n\n\n### React의 더 이상 사용되지 않는 API 제거 {/*removed-deprecated-react-apis*/}\n\n#### 제거됨: 함수형 컴포넌트에서의 `propTypes` 및 `defaultProps` {/*removed-proptypes-and-defaultprops*/}\n`PropTypes`는 [2017년 4월 (v15.5.0)](https://legacy.reactjs.org/blog/2017/04/07/react-v15.5.0.html#new-deprecation-warnings)부터 더 이상 권장하지 않습니다.\n\nReact 19에서는 `propType` 검사 기능이 React 패키지에서 제거되며 사용하더라도 아무 동작도 하지 않습니다. `propTypes`를 사용 중이라면 TypeScript나 다른 타입 검사 도구로 마이그레이션하는 것을 권장합니다.\n\n또한, 함수형 컴포넌트에서는 `defaultProps`가 제거되며, 대신 ES6의 기본 매개변수를 사용해야 합니다. 클래스형 컴포넌트에서는 ES6 대안이 없어서 `defaultProps`가 여전히 지원됩니다.\n\n```js\n// 변경 전\nimport PropTypes from 'prop-types';\n\nfunction Heading({text}) {\n  return <h1>{text}</h1>;\n}\nHeading.propTypes = {\n  text: PropTypes.string,\n};\nHeading.defaultProps = {\n  text: 'Hello, world!',\n};\n```\n```ts\n// 변경 후\ninterface Props {\n  text?: string;\n}\nfunction Heading({text = 'Hello, world!'}: Props) {\n  return <h1>{text}</h1>;\n}\n```\n\n<Note>\n\nCodemod를 사용해 `propTypes`를 TypeScript로 바꾸려면 다음 명령어를 입력하세요.\n\n```bash\nnpx codemod@latest react/prop-types-typescript\n```\n\n</Note>\n\n#### 제거됨: `contextTypes`와 `getChildContext`를 사용하는 레거시 콘텍스트 {/*removed-removing-legacy-context*/}\n\n레거시 콘텍스트는 [2018년 10월 (v16.6.0)](https://legacy.reactjs.org/blog/2018/10/23/react-v-16-6.html)부터 더 이상 권장하지 않습니다.\n\n레거시 콘텍스트는 클래스형 컴포넌트에서 `contextTypes`와 `getChildContext` API를 통해 사용할 수 있었지만, 미묘한 버그들로 인해 `contextType` API로 대체되었습니다. React 19에서는 React의 크기를 줄이고 성능을 향상하기 위해 레거시 콘텍스트가 제거됩니다.\n\n아직도 클래스형 컴포넌트에서 레거시 콘텍스트를 사용하고 있다면, 새로운 `contextType`API로 마이그레이션해야 합니다.\n\n```js {5-11,19-21}\n// 변경 전\nimport PropTypes from 'prop-types';\n\nclass Parent extends React.Component {\n  static childContextTypes = {\n    foo: PropTypes.string.isRequired,\n  };\n\n  getChildContext() {\n    return { foo: 'bar' };\n  }\n\n  render() {\n    return <Child />;\n  }\n}\n\nclass Child extends React.Component {\n  static contextTypes = {\n    foo: PropTypes.string.isRequired,\n  };\n\n  render() {\n    return <div>{this.context.foo}</div>;\n  }\n}\n```\n\n```js {2,7,9,15}\n// 변경 후\nconst FooContext = React.createContext();\n\nclass Parent extends React.Component {\n  render() {\n    return (\n      <FooContext value='bar'>\n        <Child />\n      </FooContext>\n    );\n  }\n}\n\nclass Child extends React.Component {\n  static contextType = FooContext;\n\n  render() {\n    return <div>{this.context}</div>;\n  }\n}\n```\n\n#### 제거됨: 문자열 Refs {/*removed-string-refs*/}\n문자열 Refs는 [2018년 3월 (v16.3.0)](https://legacy.reactjs.org/blog/2018/03/27/update-on-async-rendering.html)부터 더 이상 권장되지 않습니다.\n\n클래스형 컴포넌트에서는 문자열 Refs를 사용할 수 있었지만, [여러 단점](https://github.com/facebook/react/issues/1373)으로 인해 Ref 콜백 방식으로 대체되었습니다. React 19에서는 React를 더 간단하고 이해하기 쉽게 만들기 위해 문자열 Refs가 제거됩니다.\n\n클래스형 컴포넌트에서 아직 문자열 Refs를 사용하고 있다면, Ref 콜백으로 마이그레이션해야 합니다.\n\n```js {4,8}\n// 변경 전\nclass MyComponent extends React.Component {\n  componentDidMount() {\n    this.refs.input.focus();\n  }\n\n  render() {\n    return <input ref='input' />;\n  }\n}\n```\n\n```js {4,8}\n// 변경 후\nclass MyComponent extends React.Component {\n  componentDidMount() {\n    this.input.focus();\n  }\n\n  render() {\n    return <input ref={input => this.input = input} />;\n  }\n}\n```\n\n<Note>\n\nCodemod를 사용해 문자열 refs를 `ref` 콜백으로 바꾸려면 다음 명령어를 입력하세요.\n\n```bash\nnpx codemod@latest react/19/replace-string-ref\n```\n\n</Note>\n\n#### 제거됨: 모듈 패턴 팩토리<sup>Module Pattern Factories</sup> {/*removed-module-pattern-factories*/}\n모듈 패턴 팩토리는 [2019년 8월 (v16.9.0)](https://legacy.reactjs.org/blog/2019/08/08/react-v16.9.0.html#deprecating-module-pattern-factories)부터 더 이상 권장되지 않습니다.\n\n이 패턴은 거의 사용되지 않았으며, 이를 지원하는 것은 React를 불필요하게 더 크고 느리게 만들었습니다. React 19에서는 모듈 패턴 팩토리에 대한 지원이 제거되며 일반 함수로 마이그레이션해야 합니다.\n\n```js\n// 변경 전\nfunction FactoryComponent() {\n  return { render() { return <div />; } }\n}\n```\n\n```js\n// 변경 후\nfunction FactoryComponent() {\n  return <div />;\n}\n```\n\n#### 제거됨: `React.createFactory` {/*removed-createfactory*/}\n`createFactory`는 [2020년 2월 (v16.13.0)](https://legacy.reactjs.org/blog/2020/02/26/react-v16.13.0.html#deprecating-createfactory)부터 더 이상 권장하지 않습니다.\n\n`createFactory`는 JSX가 널리 사용되기 전에는 일반적이었지만 오늘날에는 거의 사용되지 않으며 JSX로 쉽게 대체할 수 있습니다. React 19에서는 `createFactory`가 제거되며 JSX로 마이그레이션해야 합니다.\n\n```js\n// 변경 전\nimport { createFactory } from 'react';\n\nconst button = createFactory('button');\n```\n\n```js\n// 변경 후\nconst button = <button />;\n```\n\n#### 제거됨: `react-test-renderer/shallow` {/*removed-react-test-renderer-shallow*/}\n\nReact 18에서는 `react-test-renderer/shallow`를 [react-shallow-renderer](https://github.com/enzymejs/react-shallow-renderer)로 다시 내보내도록 업데이트했습니다. React 19에서는 `react-test-render/shallow`가 완전히 제거되며 대신 해당 패키지를 직접 설치해야 합니다.\n\n```bash\nnpm install react-shallow-renderer --save-dev\n```\n```diff\n- import ShallowRenderer from 'react-test-renderer/shallow';\n+ import ShallowRenderer from 'react-shallow-renderer';\n```\n\n<Note>\n\n##### Shallow 렌더링 재고 권장 {/*please-reconsider-shallow-rendering*/}\n\nShallow 렌더링은 React 내부 구현에 의존하며 향후 React 업그레이드를 방해할 수 있습니다. 테스트를 [@testing-library/react](https://testing-library.com/docs/react-testing-library/intro/) 또는 [@testing-library/react-native](https://testing-library.com/docs/react-native-testing-library/intro)로 마이그레이션하는 것을 권장합니다.\n\n</Note>\n\n### 더 이상 사용되지 않는 React DOM API 제거 {/*removed-deprecated-react-dom-apis*/}\n\n#### 제거됨: `react-dom/test-utils` {/*removed-react-dom-test-utils*/}\n\n`act`는 이제 `react-dom/test-utils` 대신 `react` 패키지에서 가져와야 합니다.\n\n<ConsoleBlockMulti>\n\n<ConsoleLogLine level=\"error\">\n\n`ReactDOMTestUtils.act` is deprecated in favor of `React.act`. Import `act` from `react` instead of `react-dom/test-utils`. See https://react.dev/warnings/react-dom-test-utils for more info.\n\n</ConsoleLogLine>\n\n</ConsoleBlockMulti>\n\n이 경고를 해결하려면 `react`에서 `act`를 import 하세요.\n\n```diff\n- import {act} from 'react-dom/test-utils'\n+ import {act} from 'react';\n```\n\n기존의 다른 `test-utils` 함수들은 모두 제거되었습니다. 이러한 유틸리티는 흔히 사용되진 않았고 컴포넌트나 React의 내부 구현에 과하게 의존하게 만들 수 있었습니다. React 19에서 이 함수들을 호출하면 에러가 발생하며 다음 버전에서는 export도 완전히 제거될 예정입니다.\n\n대체 방법은 [경고 페이지](https://react.dev/warnings/react-dom-test-utils)를 참고하세요.\n\n<Note>\n\nCodemod를 사용해 `ReactDOMTestUtils.act`를 `React.act`로 바꾸려면 다음 명령어를 입력하세요.\n\n```bash\nnpx codemod@latest react/19/replace-act-import\n```\n\n</Note>\n\n#### 제거됨: `ReactDOM.render` {/*removed-reactdom-render*/}\n\n`ReactDOM.render`는 [2022년 3월 (v18.0.0)](/blog/2022/03/08/react-18-upgrade-guide)부터 더 이상 권장되지 않습니다. React 19에서는 `ReactDOM.render`가 제거되며 [`ReactDOM.createRoot`](/reference/react-dom/client/createRoot)를 사용해야 합니다.\n\n```js\n// 변경 전\nimport {render} from 'react-dom';\nrender(<App />, document.getElementById('root'));\n\n// 변경 후\nimport {createRoot} from 'react-dom/client';\nconst root = createRoot(document.getElementById('root'));\nroot.render(<App />);\n```\n\n<Note>\n\nCodemod를 사용해 `ReactDOM.render`를 `ReactDOMClient.createRoot`로 바꾸려면 다음 명령어를 입력하세요.\n\n```bash\nnpx codemod@latest react/19/replace-reactdom-render\n```\n\n</Note>\n\n#### 제거됨: `ReactDOM.hydrate` {/*removed-reactdom-hydrate*/}\n\n`ReactDOM.hydrate`는 [2022년 3월 (v18.0.0)](/blog/2022/03/08/react-18-upgrade-guide)부터 더 이상 권장되지 않습니다. React 19에서는 `ReactDOM.hydrate`가 제거되며 [`ReactDOM.hydrateRoot`](/reference/react-dom/client/hydrateRoot)로 마이그레이션 해야 합니다.\n\n```js\n// 변경 전\nimport {hydrate} from 'react-dom';\nhydrate(<App />, document.getElementById('root'));\n\n// 변경 후\nimport {hydrateRoot} from 'react-dom/client';\nhydrateRoot(document.getElementById('root'), <App />);\n```\n\n<Note>\n\nCodemod를 사용해 `ReactDOM.hydrate`를 `ReactDOMClient.hydrateRoot`로 바꾸려면 다음 명령어를 입력하세요.\n\n```bash\nnpx codemod@latest react/19/replace-reactdom-render\n```\n\n</Note>\n\n#### 제거됨: `unmountComponentAtNode` {/*removed-unmountcomponentatnode*/}\n\n`ReactDOM.unmountComponentAtNode`는 [2022년 3월 (v18.0.0)](https://react.dev/blog/2022/03/08/react-18-upgrade-guide)부터 더 이상 권장되지 않습니다. React 19부터는 `root.unmount()`를 사용해야 합니다.\n\n\n```js\n// 변경 전\nunmountComponentAtNode(document.getElementById('root'));\n\n// 변경 후\nroot.unmount();\n```\n\n자세한 내용은 [`createRoot`](https://react.dev/reference/react-dom/client/createRoot#root-unmount) 및 [`hydrateRoot`](https://react.dev/reference/react-dom/client/hydrateRoot#root-unmount)의 `root.unmount()`문서를 참고하세요.\n\n<Note>\n\nCodemod를 사용해 `unmountComponentAtNode`를 `root.unmount`로 바꾸려면 다음 명령어를 입력하세요.\n\n```bash\nnpx codemod@latest react/19/replace-reactdom-render\n```\n\n</Note>\n\n#### 제거됨: `ReactDOM.findDOMNode` {/*removed-reactdom-finddomnode*/}\n\n`ReactDOM.findDOMNode`는 [2018년 10월 (v16.6.0)](https://legacy.reactjs.org/blog/2018/10/23/react-v-16-6.html#deprecations-in-strictmode)부터 더 이상 권장되지 않습니다.\n\n`findDOMNode`는 레거시 코드의 해결책이었지만 실행 속도가 느리고 리팩토링에 취약하며 첫 번째 자식만 반환하는 등 많은 문제가 있어 제거됩니다 ([이곳](https://legacy.reactjs.org/docs/strict-mode.html#warning-about-deprecated-finddomnode-usage)에서 더 알아보기). `ReactDOM.findDOMNode` 대신 [DOM refs](/learn/manipulating-the-dom-with-refs)로 대체하여 사용할 수 있습니다.\n\n```js\n// 변경 전\nimport {findDOMNode} from 'react-dom';\n\nfunction AutoselectingInput() {\n  useEffect(() => {\n    const input = findDOMNode(this);\n    input.select()\n  }, []);\n\n  return <input defaultValue=\"Hello\" />;\n}\n```\n\n```js\n// 변경 후\nfunction AutoselectingInput() {\n  const ref = useRef(null);\n  useEffect(() => {\n    ref.current.select();\n  }, []);\n\n  return <input ref={ref} defaultValue=\"Hello\" />\n}\n```\n\n## 사용 중단된 사항 {/*new-deprecations*/}\n\n### 중단됨: `element.ref` {/*deprecated-element-ref*/}\n\nReact 19에서는 [`ref`를 일반 prop으로 사용하는 기능](/blog/2024/04/25/react-19#ref-as-a-prop)을 도입하여 기존의 `element.ref` 접근 방식은 사용 중단되고 대신 `element.props.ref`를 사용해야 합니다.\n\n`element.ref`에 접근하면 아래와 같은 경고가 표시됩니다.\n\n<ConsoleBlockMulti>\n\n<ConsoleLogLine level=\"error\">\n\nAccessing element.ref is no longer supported. ref is now a regular prop. It will be removed from the JSX Element type in a future release.\n\n</ConsoleLogLine>\n\n</ConsoleBlockMulti>\n\n### 중단됨: `react-test-renderer` {/*deprecated-react-test-renderer*/}\n\n`react-test-renderer`는 실제 사용 환경과 일치하지 않는 자체 렌더러 환경을 구현하고, 구현 세부 사항에 의존하는 테스트를 조장하며, React 내부 동작을 탐색하는 방식에 의존하기 때문에 사용 중단됩니다.\n\n이 테스트 렌더러는 [React Testing Library](https://testing-library.com)와 같은 더 나은 테스트 전략이 나오기 이전에 만들어졌으며, 이제는 더 현대적이고 지원 잘 되는 테스트 도구인 [@testing-library/react](https://testing-library.com/docs/react-testing-library/intro/) 또는 [@testing-library/react-native](https://testing-library.com/docs/react-native-testing-library/intro)를 사용하는 것을 권장합니다.\n\nReact 19부터는 `react-test-renderer`를 사용할 경우 사용 중단 경고가 로그로 출력되며 동시성 렌더링<sup>Concurrent Rendering</sup>을 사용하도록 변경되었습니다. 향후를 대비해 테스트를 React Testing Library 기반으로 이전하는 것을 추천합니다.\n\n## 주목할 만한 변경 사항 {/*notable-changes*/}\n\n### StrictMode 관련 변경 사항 {/*strict-mode-improvements*/}\n\nReact 19에는 Strict Mode 관련해 여러 수정 및 개선 사항이 포함되어 있습니다.\n\n개발 환경에서 Strict Mode가 이중 렌더링<sup>Double Rendering</sup>할 때, `useMemo`와 `useCallback`은 첫 번째 렌더링의 저장된 결과<sup>Memoized Results</sup>를 재사용합니다. 이미 Strict Mode와 호환되는 컴포넌트라면 동작상의 차이를 거의 느끼지 못할 것입니다.\n\n모든 Strict Mode 동작과 마찬가지로 이러한 기능은 개발 단계에서 컴포넌트의 잠재적 버그를 조기에 드러내고, 실제 배포 전에 수정할 수 있도록 돕기 위해 설계되었습니다. 예를 들어 개발 중에는 컴포넌트가 Suspense fallback으로 교체되는 상황을 시뮬레이션하기 위해 ref 콜백 함수가 초기 마운트 시 두 번 호출됩니다.\n\n### Suspense 관련 개선 사항 {/*improvements-to-suspense*/}\n\nReact 19에서는 컴포넌트가 일시 중단<sup>Suspend</sup> 될 때 React는 전체 형제 컴포넌트<sup>Entire Sibling Tree</sup>를 렌더링할 때까지 기다리지 않고 가장 가까운 Suspense 경계의 Fallback을 즉시 반영<sup>Commit</sup>합니다. Fallback이 반영된 후 React는 일시 중단된 형제 컴포넌트를 다시 렌더링 예약하여 트리의 나머지 부분에서 발생할 수 있는 lazy 요청을 사전 준비<sup>pre-warm</sup>하게 합니다.\n\n<Diagram name=\"prerender\" height={162} width={1270} alt=\"Diagram showing a tree of three components, one parent labeled Accordion and two children labeled Panel. Both Panel components contain isActive with value false.\">\n\n이전에는 컴포넌트가 일시 중단되면 형제 컴포넌트를 먼저 렌더링한 후 fallback이 반영되었습니다.\n\n</Diagram>\n\n<Diagram name=\"prewarm\" height={162} width={1270} alt=\"The same diagram as the previous, with the isActive of the first child Panel component highlighted indicating a click with the isActive value set to true. The second Panel component still contains value false.\" >\n\nReact 19에서는 컴포넌트가 일시 중단되면 fallback을 먼저 반영한 후 형제 컴포넌트들을 렌더링합니다.\n\n</Diagram>\n\n이 변경으로 인해 Suspense fallback은 더 빠르게 표시되며 동시에 lazy 요청에 대한 성능 최적화 효과도 유지됩니다.\n\n### UMD 빌드 제거됨 {/*umd-builds-removed*/}\n\n과거에는 빌드 과정 없이도 React를 불러올 수 있는 편리한 방법으로 UMD가 널리 사용되었습니다. 하지만 이제는 HTML 문서에서 스크립트로 모듈을 불러올 수 있는 더 현대적인 대안이 존재합니다. React 19부터는 테스트 및 릴리스 과정의 복잡성을 줄이기 위해 UMD 빌드를 더 이상 제공하지 않습니다.\n\nReact 19를 script 태그로 불러오려면 [esm.sh](https://esm.sh/)와 같은 ESM 기반 CDN을 사용할 것을 권장합니다.\n\n```html\n<script type=\"module\">\n  import React from \"https://esm.sh/react@19/?dev\"\n  import ReactDOMClient from \"https://esm.sh/react-dom@19/client?dev\"\n  ...\n</script>\n```\n\n### React 내부에 의존하는 라이브러리가 업그레이드를 막을 수도 있음 {/*libraries-depending-on-react-internals-may-block-upgrades*/}\n\n이번 릴리스에서는 React 내부 구현에 대한 변경이 포함되어 있으며 `SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED` 같은 내부 API를 사용하지 말라는 권고를 무시한 라이브러리에 영향을 줄 수 있습니다. 이러한 변경은 React 19의 개선 사항을 적용하기 데 필요하며 가이드라인을 따르는 라이브러리에는 문제가 발생하지 않습니다.\n\n[버전 관리 정책](https://react.dev/community/versioning-policy#what-counts-as-a-breaking-change)에 따라 이러한 업데이트는 중요 변경 사항으로 간주하지 않으며 어떻게 업그레이드해야 하는지에 대한 문서도 제공되지 않습니다. 권장 사항은 내부 구현에 의존하는 코드를 모두 제거하는 것입니다.\n\n내부 구현 사용의 영향을 명확히 보여주기 위해 `SECRET_INTERNALS` 접미사<sup>Suffix</sup>를 다음과 같이 변경했습니다.\n\n`_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE`\n\n앞으로는 React의 내부 구현에 접근하는 것을 더 강력하게 차단할 예정이며 이는 내부 API 사용을 억제하고 사용자가 React 업그레이드 과정에서 막히지 않도록 하기 위함입니다.\n\n## TypeScript 관련 변경 사항 {/*typescript-changes*/}\n\n### 제거된 TypeScript 타입 {/*removed-deprecated-typescript-types*/}\n\nReact 19에서 제거된 API에 따라 관련 TypeScript 타입이 정리되었습니다. 일부 제거된 타입은 더 적절한 패키지로 이동되었고 다른 일부는 이제 React의 동작을 설명하는 데 더 이상 필요하지 않기 때문에 제거되었습니다.\n\n<Note>\n대부분의 타입 관련 중요 변경 사항을 자동으로 마이그레이션하기 위한 [`types-react-codemod`](https://github.com/eps1lon/types-react-codemod/)를 공개했습니다.\n\n```bash\nnpx types-react-codemod@latest preset-19 ./path-to-app\n```\n\n`element.props`에 대해 안전하지 않은 접근<sup>Unsound Access</sup>이 많은 경우 아래 codemod를 추가로 실행할 수 있습니다.\n\n```bash\nnpx types-react-codemod@latest react-element-default-any-props ./path-to-your-react-ts-files\n```\n\n</Note>\n\n[`types-react-codemod`](https://github.com/eps1lon/types-react-codemod/) 문서를 확인하면 지원되는 교체 목록을 볼 수 있습니다. 만약 빠진 codemod가 있다면 [React 19 누락 codemod 목록](https://github.com/eps1lon/types-react-codemod/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3A%22React+19%22+label%3Aenhancement)에서 확인할 수 있습니다.\n\n\n### `ref` 정리<sup>Cleanup</sup> 필요 {/*ref-cleanup-required*/}\n\n_이 변경 사항은`react-19` codemod 프리셋에 포함된 [`no-implicit-ref-callback-return\n`](https://github.com/eps1lon/types-react-codemod/#no-implicit-ref-callback-return)항목에 해당합니다._\n\nref 정리 함수<sup>Cleanup Funtions</sup>가 도입됨에 따라 이제 ref 콜백에서 다른 값을 반환하는 경우 TypeScript에서 거부됩니다. 일반적인 해결 방법은 암시적 반환<sup>Implicit Return</sup>을 사용하지 않는 것입니다.\n\n```diff [[1, 1, \"(\"], [1, 1, \")\"], [2, 2, \"{\", 15], [2, 2, \"}\", 1]]\n- <div ref={current => (instance = current)} />\n+ <div ref={current => {instance = current}} />\n```\n\n예전에는 `HTMLDivElement` 인스턴스를 반환하는 코드가 있었는데 TypeScript는 그것이 정리 함수인지 단순 반환 값인지 구분할 수 없었습니다.\n\n### `useRef`는 인자가 필요함 {/*useref-requires-argument*/}\n\n_이 변경 사항은 `react-19` codemod 프리셋에 포함된 [`refobject-defaults`](https://github.com/eps1lon/types-react-codemod/#refobject-defaults) 항목에 해당합니다._\n\n오랫동안 제기되어 온 TypeScript와 React의 불편한 점 중 하나가 `useRef`였습니다. React 19에서는 타입 정의가 변경되어, 이제 `useRef`는 반드시 인자를 받아야 합니다. 이에 따라 타입 시그니처가 훨씬 단순해졌으며, 이제는 `createContext`와 더 유사하게 동작합니다.\n\n```ts\n// @ts-expect-error: Expected 1 argument but saw none\nuseRef();\n// Passes\nuseRef(undefined);\n// @ts-expect-error: Expected 1 argument but saw none\ncreateContext();\n// Passes\ncreateContext(undefined);\n```\n\n이제 모든 ref는 변경 가능<sup>Mutable</sup>합니다. 즉, `null`로 초기화했기 때문에 `ref.current`를 변경할 수 없었던 기존 문제를 더 이상 겪지 않아도 됩니다.\n\n```ts\nconst ref = useRef<number>(null);\n\n// 읽기 전용이라 'current'에 할당 불가\nref.current = 1;\n```\n\n`MutableRef`는 이제 사용 중단되었으며 `useRef`는 항상 단일 `RefObject` 타입을 반환합니다.\n\n```ts\ninterface RefObject<T> {\n  current: T\n}\n\ndeclare function useRef<T>: RefObject<T>\n```\n\n`useRef`는 여전히 `useRef<T>(null)`을 사용할 때 자동으로 `RefObject<T | null>`을 반환하는 오버로드를 편의상 제공합니다. `useRef`에 인자가 필요하도록 변경됨에 따라 마이그레이션을 쉽게 하려고 `useRef(undefined)`를 사용할 경우 자동으로 `RefObject<T | undefined>`를 반환하는 오버로드가 편의상 추가되었습니다.\n\n이 변경에 대한 이전 논의는 [[RFC] 모든 ref를 변경할 수 있게 만들기](https://github.com/DefinitelyTyped/DefinitelyTyped/pull/64772)에서 확인하실 수 있습니다.\n\n### `ReactElement` TypeScript 타입의 변경 사항 {/*changes-to-the-reactelement-typescript-type*/}\n\n_이 변경사항은 [`react-element-default-any-props`](https://github.com/eps1lon/types-react-codemod#react-element-default-any-props) codemod에 포함되어 있습니다._\n\n`ReactElement`로 타입이 지정된 요소의 `props` 기본 타입이 `any`에서 `unknown`으로 변경되었습니다. 단, `ReactElement`에 타입 인자를 명시적으로 제공할 때 영향을 받지 않습니다.\n\n```ts\ntype Example2 = ReactElement<{ id: string }>[\"props\"];\n//   ^? { id: string }\n```\n\n하지만 이전에 기본값을 사용하였으면 `unknown`을 처리해야 합니다.\n\n```ts\ntype Example = ReactElement[\"props\"];\n//   ^? Before, was 'any', now 'unknown'\n```\n\n이 변경은 주로 `element.props`에 대한 불안정한 접근을 많이 사용한 레거시 코드에 영향을 줍니다. 요소 내부 속성 접근<sup>Element Introspection</sup>은 예외적인 경우에만 사용되어야 하며, `any`를 명시적으로 사용해 타입 안정성이 없음을 드러내는 것이 좋습니다.\n\n### TypeScript의 JSX 네임스페이스 변경 {/*the-jsx-namespace-in-typescript*/}\n이 변경은 `react-19` codemod preset의 [`scoped-jsx`](https://github.com/eps1lon/types-react-codemod#scoped-jsx)항목에 포함되어 있습니다.\n\n오랫동안 요청된 기능 중 하나는 전역 `JSX` 네임스페이스를 제거하고 `React.JSX`로 대체하는 것이었습니다. 이 변경은 JSX를 사용하는 다양한 UI 라이브러리 간의 타입 충돌을 방지하기 위해 글로벌 타입 오염을 줄이는 데 도움이 됩니다.\n\n이제 JSX 네임스페이스 확장은 다음처럼 `declare module \"....\"을 통해 감싸야 합니다.\n\n```diff\n// global.d.ts\n+ declare module \"react\" {\n    namespace JSX {\n      interface IntrinsicElements {\n        \"my-element\": {\n          myElementProps: string;\n        };\n      }\n    }\n+ }\n```\n\n`tsconfig.json`의 `compilerOptions`에서 지정한 JSX 런타임 설정에 따라 정확한 모듈 명세자는 다음과 같이 달라집니다.\n\n- `\"jsx\": \"react-jsx\"` 인 경우 `react/jsx-runtime`.\n- `\"jsx\": \"react-jsxdev\"` 인 경우 `react/jsx-dev-runtime`.\n- `\"jsx\": \"react\"` 또는 `\"jsx\": \"preserve\"` 인 경우 `react`.\n\n### `useReducer` 타입 추론 개선 {/*better-usereducer-typings*/}\n\n[@mfp22](https://github.com/mfp22) 덕분에 이제 `useReducer`의 타입 추론이 개선되었습니다.\n\n하지만 이에 따라 호환성 깨짐이 발생했는데, 이제 `useReducer`는 전체 reducer 타입을 타입 인자로 받지 않고 아예 타입 인자를 생략하거나 state와 action 타입을 둘 다 지정해야 합니다.\n\n새로운 권장 방식은 타입 인자를 `useReducer`에 넘기지 _않는_ 것입니다.\n```diff\n- useReducer<React.Reducer<State, Action>>(reducer)\n+ useReducer(reducer)\n```\n하지만 특수한 경우에는 `Action`을 튜플로 전달하여 state와 action을 명시적으로 지정해야 할 수도 있습니다.\n```diff\n- useReducer<React.Reducer<State, Action>>(reducer)\n+ useReducer<State, [Action]>(reducer)\n```\nreducer를 인라인으로 정의한다면 함수 매개변수에 타입을 지정하는 방식을 권장합니다.\n```diff\n- useReducer<React.Reducer<State, Action>>((state, action) => state)\n+ useReducer((state: State, action: Action) => state)\n```\n이는 `useReducer` 호출문 밖으로 reducer를 분리할 때도 동일하게 적용됩니다.\n\n```ts\nconst reducer = (state: State, action: Action) => state;\n```\n\n## 변경 로그 {/*changelog*/}\n\n### 기타 주요한 변경 사항 {/*other-breaking-changes*/}\n\n- **react-dom**: `src` 및 `href` 속성에 JavaScript URL 사용 시 발생하던 오류 [#26507](https://github.com/facebook/react/pull/26507)\n- **react-dom**: `onRecoverableError`에서 `errorInfo.digest` 제거 [#28222](https://github.com/facebook/react/pull/28222)\n- **react-dom**: `unstable_flushControlled` 제거 [#26397](https://github.com/facebook/react/pull/26397)\n- **react-dom**: `unstable_createEventHandle` 제거 [#28271](https://github.com/facebook/react/pull/28271)\n- **react-dom**: `unstable_renderSubtreeIntoContainer` 제거 [#28271](https://github.com/facebook/react/pull/28271)\n- **react-dom**: `unstable_runWithPriority` 제거 [#28271](https://github.com/facebook/react/pull/28271)\n- **react-is**: `react-is`에서 사용 중단된 메서드 제거 [28224](https://github.com/facebook/react/pull/28224)\n\n### 기타 주목할 만한 변경 사항 {/*other-notable-changes*/}\n\n- **react**: 동기, 기본, 지속적 lane 처리 배치 적용 [#25700](https://github.com/facebook/react/pull/25700)\n- **react**: 중단된 컴포넌트의 형제 요소 선렌더링 방지 [#26380](https://github.com/facebook/react/pull/26380)\n- **react**: 렌더 단계에서의 업데이트로 인해 발생하는 무한 루프 감지 [#26625](https://github.com/facebook/react/pull/26625)\n- **react-dom**: popstate에서의 전환을 이제 동기적으로 처리 [#26025](https://github.com/facebook/react/pull/26025)\n- **react-dom**: SSR 중 layout effect 경고 제거 [#26395](https://github.com/facebook/react/pull/26395)\n- **react-dom**: src나 href에 빈 문자열 설정 시 경고 및 무시 (단, a 태그 제외) [#28124](https://github.com/facebook/react/pull/28124)\n\n전체 변경 사항은 [변경 로그 전체 보기](https://github.com/facebook/react/blob/main/CHANGELOG.md#1900-december-5-2024)를 참고하세요.\n\n---\n\n이 글을 작성하는 데에 도움을 준 모든 분들께 감사드립니다. [Andrew Clark](https://twitter.com/acdlite), [Eli White](https://twitter.com/Eli_White), [Jack Pope](https://github.com/jackpope), [Jan Kassens](https://github.com/kassens), [Josh Story](https://twitter.com/joshcstory), [Matt Carroll](https://twitter.com/mattcarrollcode), [Noah Lemen](https://twitter.com/noahlemen), [Sophie Alpert](https://twitter.com/sophiebits), [Sebastian Silbermann](https://twitter.com/sebsilbermann)\n"
  },
  {
    "path": "src/content/blog/2024/05/22/react-conf-2024-recap.md",
    "content": "---\ntitle: \"React Conf 2024 요약\"\nauthor: Ricky Hanlon\ndate: 2024/05/22\ndescription: 지난주 우리는 네바다주 헨더슨에서 React Conf 2024를 개최했습니다. 2일간의 콘퍼런스에서는 700명 이상의 참가자가 현장에서 모여 UI 엔지니어링 분야의 최신 동향을 논의했습니다. 이 글에서는 콘퍼런스에서 진행된 강연과 발표 내용을 요약했습니다.\n---\n\n2024년 5월 22일, [Ricky Hanlon](https://twitter.com/rickhanlonii)\n\n---\n\n<Intro>\n\n지난주 우리는 네바다주 헨더슨에서 React Conf 2024를 개최했습니다. 2일간의 콘퍼런스에서는 700명 이상의 참가자가 현장에서 모여 UI 엔지니어링 분야의 최신 동향을 논의했습니다. 이는 2019년 이후 처음 열린 오프라인 콘퍼런스였으며, 우리는 이 커뮤니티를 다시 한자리에 모을 수 있어 매우 기뻤습니다.\n\n</Intro>\n\n---\n\nReact Conf 2024에서는 [React 19 RC](/blog/2024/12/05/react-19), [React Native의 새로운 아키텍처 베타 버전](https://github.com/reactwg/react-native-new-architecture/discussions/189), 그리고 [React 컴파일러](/learn/react-compiler)의 실험 버전을 발표했습니다. 또한 커뮤니티에서도 [React Router v7](https://remix.run/blog/merging-remix-and-react-router), Expo Router의 [공용 서버 컴포넌트](https://www.youtube.com/watch?v=T8TZQ6k4SLE&t=20765s), [RedwoodJS](https://redwoodjs.com/blog/rsc-now-in-redwoodjs)의 React 서버 컴포넌트 등을 발표했습니다.\n\n[1일 차](https://www.youtube.com/watch?v=T8TZQ6k4SLE)와 [2일 차](https://www.youtube.com/watch?v=0ckOUBiuxVY)의 전체 스트리밍 영상은 온라인에서 시청하실 수 있습니다. 이 글에서는 콘퍼런스에서 진행된 강연과 발표 내용을 요약했습니다.\n\n## 1일 차 {/*day-1*/}\n\n_[1일 차 전체 스트리밍 시청하기.](https://www.youtube.com/watch?v=T8TZQ6k4SLE&t=973s)_\n\n첫째 날은 Meta CTO [Andrew \"Boz\" Bosworth](https://www.threads.net/@boztank)의 환영사와 Meta의 React 팀을 이끄는 [Seth Webster](https://twitter.com/sethwebster)와 사회자 [Ashley Narcisse](https://twitter.com/_darkfadr)의 소개로 시작되었습니다.\n\n첫째 날의 기조연설에서 [Joe Savona](https://twitter.com/en_JS)는 누구나 쉽게 뛰어난 사용자 경험을 구축할 수 있도록 하는 React의 목표와 비전을 공유했습니다. 이어서 [Lauren Tan](https://twitter.com/potetotes)은 React의 현황을 발표하며 2023년 React 다운로드 수가 10억 회를 넘었고, 신규 개발자의 37%가 React로 프로그래밍을 배운다는 사실을 공유했습니다. 마지막으로 그녀는 React 커뮤니티가 React를 React 답게 만들기 위해 한 일들을 강조했습니다.\n\n추가로 콘퍼런스에서 진행된 커뮤니티 강연도 확인하세요.\n\n- [Ryan Florence](https://twitter.com/ryanflorence): [바닐라 React](https://www.youtube.com/watch?v=T8TZQ6k4SLE&t=5542s)\n- [Lee Robinson](https://twitter.com/leeerob): [React 리듬 & 블루스](https://www.youtube.com/watch?v=0ckOUBiuxVY&t=12728s)\n- [Amy Dutton](https://twitter.com/selfteachme): [React 서버 컴포넌트를 포함한 RedwoodJS](https://www.youtube.com/watch?v=T8TZQ6k4SLE&t=26815s)\n- [Evan Bacon](https://twitter.com/Baconbrix): [Expo Router의 Universal React 서버 컴포넌트 소개](https://www.youtube.com/watch?v=T8TZQ6k4SLE&t=20765s)\n\n다음으로 기조연설에선 [Josh Story](https://twitter.com/joshcstory)와 [Andrew Clark](https://twitter.com/acdlite)가 React 19의 새로운 기능과 React 19 RC를 발표했습니다. [React 19 릴리스 포스트](/blog/2024/12/05/react-19)에서 모든 기능을 확인하고, 새로운 기능을 깊이 있게 다룬 다음 강연도 확인하세요.\n\n- [Lydia Hallie](https://twitter.com/lydiahallie): [React 19의 새로운 기능](https://www.youtube.com/watch?v=T8TZQ6k4SLE&t=8880s)\n- [Sam Selikoff](https://twitter.com/samselikoff): [React 파헤치기: React 19 로드맵](https://www.youtube.com/watch?v=T8TZQ6k4SLE&t=10112s)\n- [Josh Story](https://twitter.com/joshcstory): [React 19 심층 탐구: HTML 조정](https://www.youtube.com/watch?v=T8TZQ6k4SLE&t=24916s)\n- [Aurora Walberg Scharff](https://twitter.com/aurorascharff): [React 서버 컴포넌트로 폼 향상](https://www.youtube.com/watch?v=0ckOUBiuxVY&t=25280s)\n- [Dan Abramov](https://bsky.app/profile/danabra.mov): [두 대의 컴퓨터용 React](https://www.youtube.com/watch?v=T8TZQ6k4SLE&t=18825s)\n- [Kent C. Dodds](https://twitter.com/kentcdodds): [이제 React 서버 컴포넌트를 이해합니다](https://www.youtube.com/watch?v=0ckOUBiuxVY&t=11256s) \n\n마지막으로 [Joe Savona](https://twitter.com/en_JS), [Sathya Gunasekaran](https://twitter.com/_gsathya), [Mofei Zhang](https://twitter.com/zmofei)은 React 컴파일러가 [오픈소스](https://github.com/facebook/react/pull/29061)로 공개되었음을 알리고, 실험 버전을 공유했습니다.\n\n컴파일러 사용법과 동작 방식은 [관련 문서](/learn/react-compiler) 및 관련 강연을 확인하세요.\n\n- [Lauren Tan](https://twitter.com/potetotes): [Memo를 신경 쓰지 마세요](https://www.youtube.com/watch?v=T8TZQ6k4SLE&t=12020s)\n- [Sathya Gunasekaran](https://twitter.com/_gsathya) & [Mofei Zhang](https://twitter.com/zmofei): [React 컴파일러 심층 탐구](https://www.youtube.com/watch?v=0ckOUBiuxVY&t=9313s) \n\n1일 차 기조연설 전체 시청하기\n\n<YouTubeIframe src=\"https://www.youtube.com/embed/T8TZQ6k4SLE?t=973s\" />\n\n## 2일 차 {/*day-2*/}\n\n_[2일 차 전체 스트리밍 시청하기](https://www.youtube.com/watch?v=0ckOUBiuxVY&t=1720s)_\n\n둘째 날은 [Seth Webster](https://twitter.com/sethwebster)의 환영사와 [Eli White](https://x.com/Eli_White)의 감사 인사, 그리고 사회자 [Ashley Narcisse](https://twitter.com/_darkfadr)의 소개로 시작되었습니다.\n\n2일 차 기조연설에서 [Nicola Corti](https://twitter.com/cortinico)는 React Native의 현황을 발표하며 2023년 다운로드 수가 7,800만 건임을 공유했습니다. 또한 Meta에서 사용되는 2,000개 이상의 화면, 하루 20억 회 이상 방문 되는 Facebook 마켓플레이스의 제품 상세 페이지, Microsoft Windows의 시작 메뉴 일부와 대부분의 Microsoft Office 모바일/데스크톱 기능을 포함한 React Native 앱의 사례를 강조했습니다.\n\n또한 Nicola는 라이브러리, 프레임워크, 다양한 플랫폼을 포함해 React Native를 지원하기 위해 커뮤니티가 한 모든 활동도 강조했습니다. 더 자세한 내용은 커뮤니티 강연을 참고하세요.\n\n- [Chris Traganos](https://twitter.com/chris_trag) & [Anisha Malde](https://twitter.com/anisha_malde): [모바일 및 데스크톱 앱을 넘어선 React Native 확장](https://www.youtube.com/watch?v=0ckOUBiuxVY&t=5798s)\n- [Michał Pierzchała](https://twitter.com/thymikee): [React를 활용한 공간 컴퓨팅](https://www.youtube.com/watch?v=0ckOUBiuxVY&t=22525s)\n\n[Riccardo Cipolleschi](https://twitter.com/cipolleschir)는 React Native의 새로운 아키텍처가 베타 상태로 출시되어 앱에서 사용할 준비가 되었음을 발표하고, 새로운 기능 및 향후 로드맵을 공유했습니다. 더 자세한 내용은 아래 강연을 참고하세요.\n\n- [Olga Zinoveva](https://github.com/SlyCaptainFlint) & [Naman Goel](https://twitter.com/naman34): [크로스 플랫폼 React](https://www.youtube.com/watch?v=0ckOUBiuxVY&t=26569s)\n\n기조연설에서 Nicola는 React Native 신규 앱 개발 시 Expo와 같은 프레임워크 사용을 권장한다고 발표하고, 새로운 React Native 홈페이지와 시작 가이드를 공개했습니다. [React Native 문서](https://reactnative.dev/docs/next/environment-setup)에서 새 시작 가이드를 확인할 수 있습니다.\n\n마지막으로 [Kadi Kraman](https://twitter.com/kadikraman)이 Expo의 최신 기능과 개선 사항, 그리고 Expo를 통한 React Native 개발 시작 방법을 공유하며 기조연설을 마쳤습니다.\n\n2일 차 기조연설 전체 시청하기\n\n<YouTubeIframe src=\"https://www.youtube.com/embed/0ckOUBiuxVY?t=1720s\" />\n\n## Q&A {/*q-and-a*/}\n\nReact와 React Native 팀은 매일 Q&A 세션으로 하루를 마무리했습니다.\n\n- [Michael Chan](https://twitter.com/chantastic)이 진행한 [React Q&A](https://www.youtube.com/watch?v=T8TZQ6k4SLE&t=27518s) \n- [Jamon Holmgren](https://twitter.com/jamonholmgren)이 진행한 [React Native Q&A](https://www.youtube.com/watch?v=0ckOUBiuxVY&t=27935s)\n\n## 그리고... {/*and-more*/}\n\n접근성, 오류 보고, CSS 등 다양한 주제의 강연도 있었습니다.\n\n- [Kateryna Porshnieva](https://twitter.com/krambertech): [React 앱 접근성 해설](https://www.youtube.com/watch?v=0ckOUBiuxVY&t=20655s)\n- [Olivier Tassinari](https://twitter.com/olivtassinari): [Pigment CSS, 서버 컴포넌트 시대의 CSS](https://www.youtube.com/watch?v=0ckOUBiuxVY&t=21696s)\n- [Sunil Pai](https://twitter.com/threepointone): [실시간 React 서버 컴포넌트](https://www.youtube.com/watch?v=T8TZQ6k4SLE&t=24070s)\n- [Charlotte Isambert](https://twitter.com/c_isambert): [React의 규칙 깨기](https://www.youtube.com/watch?v=T8TZQ6k4SLE&t=25862s)\n- [Ryan Albrecht](https://github.com/ryan953): [오류 100% 해결하기](https://www.youtube.com/watch?v=0ckOUBiuxVY&t=19881s)\n\n## 감사드립니다 {/*thank-you*/}\n\nReact Conf 2024를 가능하게 해준 모든 스태프, 발표자, 참가자분들께 감사드립니다. 너무 많아 모두 나열할 수 없지만, 특별히 몇 분께 감사드리고 싶습니다.\n\n전체 행사 기획을 도와주신 [Barbara Markiewicz](https://twitter.com/barbara_markie), [Callstack 팀](https://www.callstack.com/), React Team Developer Advocate인 [Matt Carroll](https://twitter.com/mattcarrollcode)께 감사드립니다. 행사 운영을 도와주신 [Sunny Leggett](https://zeroslopeevents.com/about)와 [Zero Slope 팀](https://zeroslopeevents.com)께도 감사드립니다.\n\n사회를 맡아주신 Chief Vibes Officer [Ashley Narcisse](https://twitter.com/_darkfadr), Q&A 세션을 진행해 주신 [Michael Chan](https://twitter.com/chantastic)과 [Jamon Holmgren](https://twitter.com/jamonholmgren)께 감사드립니다.\n\n매일 환영사로 우리를 맞아주고 구조와 콘텐츠의 방향을 제시해 주신 [Seth Webster](https://twitter.com/sethwebster)와 [Eli White](https://x.com/Eli_White), 애프터 파티에서 특별한 메시지를 전해 주신 [Tom Occhino](https://twitter.com/tomocchino)께 감사드립니다.\n\n강연에 대한 세심한 피드백, 슬라이드 디자인, 그리고 전반적인 세부 사항을 신경 써 주신 [Ricky Hanlon](https://www.youtube.com/watch?v=FxTZL2U-uKg&t=1263s)께 감사드립니다.\n\n콘퍼런스 웹사이트를 제작해 주신 [Callstack](https://www.callstack.com/), 모바일 앱을 제작해 주신 [Kadi Kraman](https://twitter.com/kadikraman)과 [Expo 팀](https://expo.dev/)께 감사드립니다.\n\n행사를 가능하게 해 주신 후원자분들께 감사드립니다: [Remix](https://remix.run/), [Amazon](https://developer.amazon.com/apps-and-games?cmp=US_2024_05_3P_React-Conf-2024&ch=prtnr&chlast=prtnr&pub=ref&publast=ref&type=org&typelast=org), [MUI](https://mui.com/), [Sentry](https://sentry.io/for/react/?utm_source=sponsored-conf&utm_medium=sponsored-event&utm_campaign=frontend-fy25q2-evergreen&utm_content=logo-reactconf2024-learnmore), [Abbott](https://www.jobs.abbott/software), [Expo](https://expo.dev/), [RedwoodJS](https://redwoodjs.com/), [Vercel](https://vercel.com).\n\n시각, 무대, 그리고 음향을 담당해 주신 AV 팀과 행사를 개최해 주신 Westin Hotel에도 감사드립니다.\n\n지식과 커뮤니티에 관한 경험을 공유해 주신 모든 연사분께 감사드립니다.\n\n마지막으로 현장과 온라인에서 참석하여 무엇이 React를 React 답게 만드는지 보여주신 모든 분께 감사드립니다. React는 단순한 라이브러리를 넘어선 커뮤니티입니다. 모두가 한자리에 모여 함께 배우고 공유하는 모습이 큰 영감이 되었습니다.\n\n다음에 또 만나요!\n"
  },
  {
    "path": "src/content/blog/2024/10/21/react-compiler-beta-release.md",
    "content": "---\ntitle: \"React Compiler Beta Release\"\nauthor: Lauren Tan\ndate: 2024/10/21\ndescription: At React Conf 2024, we announced the experimental release of React Compiler, a build-time tool that optimizes your React app through automatic memoization. In this post, we want to share what's next for open source, and our progress on the compiler.\n\n---\n\nOctober 21, 2024 by [Lauren Tan](https://twitter.com/potetotes).\n\n---\n\n<Note>\n\n### React Compiler is now stable! {/*react-compiler-is-now-in-rc*/}\n\nPlease see the [stable release blog post](/blog/2025/10/07/react-compiler-1) for details.\n\n</Note>\n\n<Intro>\n\nThe React team is excited to share new updates:\n\n</Intro>\n\n1. We're publishing React Compiler Beta today, so that early adopters and library maintainers can try it and provide feedback.\n2. We're officially supporting React Compiler for apps on React 17+, through an optional `react-compiler-runtime` package.\n3. We're opening up public membership of the [React Compiler Working Group](https://github.com/reactwg/react-compiler) to prepare the community for gradual adoption of the compiler.\n\n---\n\nAt [React Conf 2024](/blog/2024/05/22/react-conf-2024-recap), we announced the experimental release of React Compiler, a build-time tool that optimizes your React app through automatic memoization. [You can find an introduction to React Compiler here](/learn/react-compiler).\n\nSince the first release, we've fixed numerous bugs reported by the React community, received several high quality bug fixes and contributions[^1] to the compiler, made the compiler more resilient to the broad diversity of JavaScript patterns, and have continued to roll out the compiler more widely at Meta.\n\nIn this post, we want to share what's next for React Compiler.\n\n## Try React Compiler Beta today {/*try-react-compiler-beta-today*/}\n\nAt [React India 2024](https://www.youtube.com/watch?v=qd5yk2gxbtg), we shared an update on React Compiler. Today, we are excited to announce a new Beta release of React Compiler and ESLint plugin. New betas are published to npm using the `@beta` tag.\n\nTo install React Compiler Beta:\n\n<TerminalBlock>\nnpm install -D babel-plugin-react-compiler@beta eslint-plugin-react-compiler@beta\n</TerminalBlock>\n\nOr, if you're using Yarn:\n\n<TerminalBlock>\nyarn add -D babel-plugin-react-compiler@beta eslint-plugin-react-compiler@beta\n</TerminalBlock>\n\nYou can watch [Sathya Gunasekaran's](https://twitter.com/_gsathya) talk at React India here:\n\n<YouTubeIframe src=\"https://www.youtube.com/embed/qd5yk2gxbtg\" />\n\n## We recommend everyone use the React Compiler linter today {/*we-recommend-everyone-use-the-react-compiler-linter-today*/}\n\nReact Compiler's ESLint plugin helps developers proactively identify and correct [Rules of React](/reference/rules) violations. **We strongly recommend everyone use the linter today**. The linter does not require that you have the compiler installed, so you can use it independently, even if you are not ready to try out the compiler.\n\nTo install the linter only:\n\n<TerminalBlock>\nnpm install -D eslint-plugin-react-compiler@beta\n</TerminalBlock>\n\nOr, if you're using Yarn:\n\n<TerminalBlock>\nyarn add -D eslint-plugin-react-compiler@beta\n</TerminalBlock>\n\nAfter installation you can enable the linter by [adding it to your ESLint config](/learn/react-compiler/installation#eslint-integration). Using the linter helps identify Rules of React breakages, making it easier to adopt the compiler when it's fully released.\n\n## Backwards Compatibility {/*backwards-compatibility*/}\n\nReact Compiler produces code that depends on runtime APIs added in React 19, but we've since added support for the compiler to also work with React 17 and 18. If you are not on React 19 yet, in the Beta release you can now try out React Compiler by specifying a minimum `target` in your compiler config, and adding `react-compiler-runtime` as a dependency. [You can find docs on this here](/reference/react-compiler/configuration#react-17-18).\n\n## Using React Compiler in libraries {/*using-react-compiler-in-libraries*/}\n\nOur initial release was focused on identifying major issues with using the compiler in applications. We've gotten great feedback and have substantially improved the compiler since then. We're now ready for broad feedback from the community, and for library authors to try out the compiler to improve performance and the developer experience of maintaining your library.\n\nReact Compiler can also be used to compile libraries. Because React Compiler needs to run on the original source code prior to any code transformations, it is not possible for an application's build pipeline to compile the libraries they use. Hence, our recommendation is for library maintainers to independently compile and test their libraries with the compiler, and ship compiled code to npm.\n\nBecause your code is pre-compiled, users of your library will not need to have the compiler enabled in order to benefit from the automatic memoization applied to your library. If your library targets apps not yet on React 19, specify a minimum `target` and add `react-compiler-runtime` as a direct dependency. The runtime package will use the correct implementation of APIs depending on the application's version, and polyfill the missing APIs if necessary.\n\n[You can find more docs on this here.](/reference/react-compiler/compiling-libraries)\n\n## Opening up React Compiler Working Group to everyone {/*opening-up-react-compiler-working-group-to-everyone*/}\n\nWe previously announced the invite-only [React Compiler Working Group](https://github.com/reactwg/react-compiler) at React Conf to provide feedback, ask questions, and collaborate on the compiler's experimental release.\n\nFrom today, together with the Beta release of React Compiler, we are opening up Working Group membership to everyone. The goal of the React Compiler Working Group is to prepare the ecosystem for a smooth, gradual adoption of React Compiler by existing applications and libraries. Please continue to file bug reports in the [React repo](https://github.com/facebook/react), but please leave feedback, ask questions, or share ideas in the [Working Group discussion forum](https://github.com/reactwg/react-compiler/discussions).\n\nThe core team will also use the discussions repo to share our research findings. As the Stable Release gets closer, any important information will also be posted on this forum.\n\n## React Compiler at Meta {/*react-compiler-at-meta*/}\n\nAt [React Conf](/blog/2024/05/22/react-conf-2024-recap), we shared that our rollout of the compiler on Quest Store and Instagram were successful. Since then, we've deployed React Compiler across several more major web apps at Meta, including [Facebook](https://www.facebook.com) and [Threads](https://www.threads.net). That means if you've used any of these apps recently, you may have had your experience powered by the compiler. We were able to onboard these apps onto the compiler with few code changes required, in a monorepo with more than 100,000 React components.\n\nWe've seen notable performance improvements across all of these apps. As we've rolled out, we're continuing to see results on the order of [the wins we shared previously at ReactConf](https://youtu.be/lyEKhv8-3n0?t=3223). These apps have already been heavily hand tuned and optimized by Meta engineers and React experts over the years, so even improvements on the order of a few percent are a huge win for us.\n\nWe also expected developer productivity wins from React Compiler. To measure this, we collaborated with our data science partners at Meta[^2] to conduct a thorough statistical analysis of the impact of manual memoization on productivity. Before rolling out the compiler at Meta, we discovered that only about 8% of React pull requests used manual memoization and that these pull requests took 31-46% longer to author[^3]. This confirmed our intuition that manual memoization introduces cognitive overhead, and we anticipate that React Compiler will lead to more efficient code authoring and review. Notably, React Compiler also ensures that *all* code is memoized by default, not just the (in our case) 8% where developers explicitly apply memoization.\n\n## Roadmap to Stable {/*roadmap-to-stable*/}\n\n*This is not a final roadmap, and is subject to change.*\n\nWe intend to ship a Release Candidate of the compiler in the near future following the Beta release, when the majority of apps and libraries that follow the Rules of React have been proven to work well with the compiler. After a period of final feedback from the community, we plan on a Stable Release for the compiler. The Stable Release will mark the beginning of a new foundation for React, and all apps and libraries will be strongly recommended to use the compiler and ESLint plugin.\n\n* ✅ Experimental: Released at React Conf 2024, primarily for feedback from early adopters.\n* ✅ Public Beta: Available today, for feedback from the wider community.\n* 🚧 Release Candidate (RC): React Compiler works for the majority of rule-following apps and libraries without issue.\n* 🚧 General Availability: After final feedback period from the community.\n\nThese releases also include the compiler's ESLint plugin, which surfaces diagnostics statically analyzed by the compiler. We plan to combine the existing eslint-plugin-react-hooks plugin with the compiler's ESLint plugin, so only one plugin needs to be installed.\n\nPost-Stable, we plan to add more compiler optimizations and improvements. This includes both continual improvements to automatic memoization, and new optimizations altogether, with minimal to no change of product code. Upgrading to each new release of the compiler is aimed to be straightforward, and each upgrade will continue to improve performance and add better handling of diverse JavaScript and React patterns.\n\nThroughout this process, we also plan to prototype an IDE extension for React. It is still very early in research, so we expect to be able to share more of our findings with you in a future React Labs blog post.\n\n---\n\nThanks to [Sathya Gunasekaran](https://twitter.com/_gsathya), [Joe Savona](https://twitter.com/en_JS), [Ricky Hanlon](https://twitter.com/rickhanlonii), [Alex Taylor](https://github.com/alexmckenley), [Jason Bonta](https://twitter.com/someextent), and [Eli White](https://twitter.com/Eli_White) for reviewing and editing this post.\n\n---\n\n[^1]: Thanks [@nikeee](https://github.com/facebook/react/pulls?q=is%3Apr+author%3Anikeee), [@henryqdineen](https://github.com/facebook/react/pulls?q=is%3Apr+author%3Ahenryqdineen), [@TrickyPi](https://github.com/facebook/react/pulls?q=is%3Apr+author%3ATrickyPi), and several others for their contributions to the compiler.\n\n[^2]: Thanks [Vaishali Garg](https://www.linkedin.com/in/vaishaligarg09) for leading this study on React Compiler at Meta, and for reviewing this post.\n\n[^3]: After controlling on author tenure, diff length/complexity, and other potential confounding factors.\n"
  },
  {
    "path": "src/content/blog/2024/12/05/react-19.md",
    "content": "---\ntitle: \"React v19\"\nauthor: React 팀\ndate: 2024/12/05\ndescription: React 19를 이제 npm에서 사용할 수 있습니다! 이 포스트에서 React 19의 새로운 기능들에 대한 개요와 도입하는 방법에 대해 설명합니다.\n---\n{/*<!-- eslint-disable md/no-double-space -->*/}\n2024년 12월 5일, [React 팀](/community/team)\n\n---\n<Note>\n\n### React 19는 이제 안정적입니다! {/*react-19-is-now-stable*/}\n\nReact 19 RC를 4월에 처음 공유한 이후 다음을 추가하였습니다.\n\n- **지연된 트리의 사전 워밍**: [Suspense 개선 사항](/blog/2024/04/25/react-19-upgrade-guide#improvements-to-suspense)을 참고하세요.\n- **React DOM 정적 API들**: [새로운 React DOM의 정적 API](#new-react-dom-static-apis)를 참고하세요.\n\n_이 게시물의 날짜는 안정된 버전의 릴리즈 날짜를 반영하도록 업데이트되었습니다._\n\n</Note>\n\n<Intro>\n\nReact v19를 이제 npm에서 사용할 수 있습니다!\n\n</Intro>\n\n[React 19 업그레이드 가이드](/blog/2024/04/25/react-19-upgrade-guide)에서 React 19로 앱을 업그레이드하는 단계별 지침을 공유했습니다. 이 포스트에서 React 19의 새로운 기능들과 이를 도입하는 방법을 제공합니다.\n\n- [React 19의 새로운 기능](#whats-new-in-react-19)\n- [React 19의 개선 사항](#improvements-in-react-19)\n- [업그레이드 방법](#how-to-upgrade)\n\n주요 변경 사항 목록은 [업그레이드 가이드](/blog/2024/04/25/react-19-upgrade-guide)를 참고하세요.\n\n---\n\n## React 19의 새로운 기능 {/*whats-new-in-react-19*/}\n\n### 액션 {/*actions*/}\n\nReact 앱에서 일반적인 사용 사례 중 하나는 데이터 변경을 수행한 뒤 응답에 따라 상태를 변경하는 것입니다. 예를 들어, 사용자가 이름을 변경하는 폼을 제출하면 API 요청을 보내고 그 응답을 처리해야 합니다. 이전에는 대기 상태, 에러, 낙관적 업데이트, 순차적 요청을 수동으로 처리해야 했습니다.\n\n예를 들어, `useState`로 대기, 에러 상태를 처리할 수 있었습니다.\n\n```js\n// 액션 이전\nfunction UpdateName({}) {\n  const [name, setName] = useState(\"\");\n  const [error, setError] = useState(null);\n  const [isPending, setIsPending] = useState(false);\n\n  const handleSubmit = async () => {\n    setIsPending(true);\n    const error = await updateName(name);\n    setIsPending(false);\n    if (error) {\n      setError(error);\n      return;\n    }\n    redirect(\"/path\");\n  };\n\n  return (\n    <div>\n      <input value={name} onChange={(event) => setName(event.target.value)} />\n      <button onClick={handleSubmit} disabled={isPending}>\n        Update\n      </button>\n      {error && <p>{error}</p>}\n    </div>\n  );\n}\n```\n\nReact 19에서는 비동기 함수를 사용하여 대기 상태, 에러, 폼, 낙관적 업데이트를 자동으로 처리할 수 있도록 지원을 추가했습니다.\n\n예를 들어, `useTransition`을 통해 대기 상태를 다룰 수 있습니다.\n\n```js\n// 액션을 통해 대기 상태를 활용\nfunction UpdateName({}) {\n  const [name, setName] = useState(\"\");\n  const [error, setError] = useState(null);\n  const [isPending, startTransition] = useTransition();\n\n  const handleSubmit = () => {\n    startTransition(async () => {\n      const error = await updateName(name);\n      if (error) {\n        setError(error);\n        return;\n      }\n      redirect(\"/path\");\n    })\n  };\n\n  return (\n    <div>\n      <input value={name} onChange={(event) => setName(event.target.value)} />\n      <button onClick={handleSubmit} disabled={isPending}>\n        Update\n      </button>\n      {error && <p>{error}</p>}\n    </div>\n  );\n}\n```\n\n비동기 전환은 즉시 `isPending` 상태를 `true`로 설정하고, 비동기 요청을 수행한 후, 모든 전환이 완료되면 `isPending`을 `false`로 변경합니다. 이를 통해 데이터가 변경되는 동안에도 현재 UI 반응성과 상호작용성을 유지할 수 있습니다.\n\n<Note>\n\n#### 관습에 따르면 비동기 전환을 사용하는 함수들을 \"액션\"이라 부릅니다. {/*by-convention-functions-that-use-async-transitions-are-called-actions*/}\n\n액션은 데이터 제출을 자동으로 관리합니다.\n\n- **대기 상태**: 액션은 요청 시작 시 대기 상태를 활성화하고 최종 상태가 커밋되었을때 자동으로 초기화합니다.\n- **낙관적 업데이트**: 액션은 새로운 [`useOptimistic`](#new-hook-optimistic-updates)훅을 통해 사용자가 요청을 제출하는 동안 즉각적인 피드백을 표시할 수 있습니다.\n- **에러 처리**: 액션은 요청 실패 시 Error Boundary를 보여주고 낙관적 업데이트를 원래 값으로, 자동으로 돌려놓습니다.\n- **폼**: `<form>` 엘리먼트는 `action` 및 `formAction` props에 함수를 전달하는 것을 지원합니다. `action` props에 함수가 전달되면 기본적으로 액션을 사용하며 제출 후 폼을 자동으로 초기화합니다.\n\n</Note>\n\n액션을 기반으로, React 19는 낙관적 업데이트를 관리하는 [`useOptimistic`](#new-hook-optimistic-updates)와 액션을 위한 일반적인 케이스를 처리하는 [`React.useActionState`](#new-hook-useactionstate) Hook을 도입했습니다. `react-dom`에서는 폼 처리를 자동화하는 [`<form>` 액션](#form-actions)과 폼 내의 공통 케이스를 지원하는 [`useFormStatus`](#new-hook-useformstatus)를 추가했습니다.\n\nReact 19에서 간단한 예시가 있습니다.\n\n```js\n// `<form>` 액션과 `useActionState`의 사용\nfunction ChangeName({ name, setName }) {\n  const [error, submitAction, isPending] = useActionState(\n    async (previousState, formData) => {\n      const error = await updateName(formData.get(\"name\"));\n      if (error) {\n        return error;\n      }\n      redirect(\"/path\");\n      return null;\n    },\n    null,\n  );\n\n  return (\n    <form action={submitAction}>\n      <input type=\"text\" name=\"name\" />\n      <button type=\"submit\" disabled={isPending}>Update</button>\n      {error && <p>{error}</p>}\n    </form>\n  );\n}\n```\n\n다음 섹션에서 React 19의 새로운 기능들을 분석해 보겠습니다.\n\n### 새로운 훅 `useActionState` {/*new-hook-useactionstate*/}\n\n액션의 일반적인 경우를 더 쉽게 처리하기 위해 `useActionState`라는 새로운 Hook을 추가했습니다.\n\n```js\nconst [error, submitAction, isPending] = useActionState(\n  async (previousState, newName) => {\n    const error = await updateName(newName);\n    if (error) {\n      // 액션에 대한 결과를 반환할 수 있습니다.\n      // 여기서 에러를 반환합니다.\n      return error;\n    }\n\n    // 정상 결과를 다룹니다.\n    return null;\n  },\n  null,\n);\n```\n\n`useActionState`는 함수(액션)를 받아서 이를 호출하는 래핑된 액션을 반환합니다. 이것이 작동하는 이유는, 액션들이 조합 가능하기 때문입니다. 래핑된 액션이 호출되면 `useActionState`는 액션의 마지막 결과를 `data`로 액션의 대기 상태를 `pending`으로 반환합니다.\n\n<Note>\n\n`React.useActionState` 는 Canary 릴리즈에서 `ReactDOM.useFormState`라 불렸지만 이름이 변경되었고 `useFormState`는 더 이상 사용되지 않습니다.\n\n더 많은 정보는 [#28491](https://github.com/facebook/react/pull/28491)을 참고하세요.\n\n</Note>\n\n더 많은 정보는 [`useActionState`](/reference/react/useActionState) 문서를 참고하세요.\n\n### React DOM: `<form>` 액션 {/*form-actions*/}\n\n액션은 또한 React 19의 새로운 `<form>`기능과 `react-dom`을 통합하였습니다. `<form>`, `<input>`, 그리고 `<button>` 엘리먼트의 `action`과 `formAction` 속성에 함수를 전달하여 액션으로 폼을 자동으로 제출할 수 있도록 지원을 추가하였습니다.\n\n```js [[1,1,\"actionFunction\"]]\n<form action={actionFunction}>\n```\n\n`<form>` 액션이 성공하면 React는 비제어 컴포넌트의 경우, 폼을 자동으로 재설정합니다. 만일 수동으로 `<form>`을 재설정해야 하는 경우, 새로운 React DOM API인 `requestFormReset`을 호출할 수 있습니다.\n\n더 많은 정보는 [`<form>`](/reference/react-dom/components/form), [`<input>`](/reference/react-dom/components/input) 그리고 `<button>`을 위한 `react-dom` 문서를 참고하세요.\n\n### React DOM: 새로운 Hook: `useFormStatus` {/*new-hook-useformstatus*/}\n\n디자인 시스템에서는 컴포넌트로 Props를 내려보내지 않고 `<form>` 내 정보에 접근해야 하는 디자인 컴포넌트를 작성하는 것이 일반적입니다. 이는 Context를 통해 수행할 수 있지만 일반적인 경우를 더 쉽게 만들기 위해 새로운 훅 `useFormStatus`을 추가했습니다.\n\n```js [[1, 4, \"pending\"], [1, 5, \"pending\"]]\nimport {useFormStatus} from 'react-dom';\n\nfunction DesignButton() {\n  const {pending} = useFormStatus();\n  return <button type=\"submit\" disabled={pending} />\n}\n```\n\n`useFormStatus`는 마치 폼이 Context Provider인 것처럼 부모 `<form>`의 상태를 읽습니다.\n\n더 많은 정보는 `react-dom`의 [`useFormStatus`](/reference/react-dom/hooks/useFormStatus) 문서를 참고하세요.\n\n### 새로운 Hook: `useOptimistic` {/*new-hook-optimistic-updates*/}\n\n데이터 변경을 수행할 때 또 다른 일반적인 UI 패턴은 비동기 요청이 진행되는 동안 최종 상태를 낙관적으로 보여주는 것입니다. React 19에서는 이를 더 쉽게 만들기 위해 새로운 훅 `useOptimistic`를 추가했습니다.\n\n```js {2,6,13,19}\nfunction ChangeName({currentName, onUpdateName}) {\n  const [optimisticName, setOptimisticName] = useOptimistic(currentName);\n\n  const submitAction = async formData => {\n    const newName = formData.get(\"name\");\n    setOptimisticName(newName);\n    const updatedName = await updateName(newName);\n    onUpdateName(updatedName);\n  };\n\n  return (\n    <form action={submitAction}>\n      <p>Your name is: {optimisticName}</p>\n      <p>\n        <label>Change Name:</label>\n        <input\n          type=\"text\"\n          name=\"name\"\n          disabled={currentName !== optimisticName}\n        />\n      </p>\n    </form>\n  );\n}\n```\n\n`useOptimistic` Hook은 `updateName` 요청이 진행 중일 때 `optimisticName`을 즉시 렌더링할 것입니다. 업데이트가 끝나거나 에러가 발생했을 때 React는 자동으로 `currentName` 값을 이전으로 되돌립니다.\n\n더 많은 정보는 [`useOptimistic`](/reference/react/useOptimistic)문서를 참고하세요.\n\n### 새로운 API: `use` {/*new-feature-use*/}\n\nReact 19에서 렌더링에서 Resource를 읽기 위해 새로운 API `use`를 발표했습니다.\n\n예를 들어 `use`를 통해 Promise를 읽을 수 있고 React는 Promise를 처리할 때까지 중단할 것입니다.\n\n```js {1,5}\nimport {use} from 'react';\n\nfunction Comments({commentsPromise}) {\n  // `use`는 promise가 처리될 때까지 중단될 것입니다.\n  const comments = use(commentsPromise);\n  return comments.map(comment => <p key={comment.id}>{comment}</p>);\n}\n\nfunction Page({commentsPromise}) {\n  // Comments 컴포넌트에서 `use`가 중단될 때\n  // Suspense Boundary가 보일 것 입니다.\n  return (\n    <Suspense fallback={<div>Loading...</div>}>\n      <Comments commentsPromise={commentsPromise} />\n    </Suspense>\n  )\n}\n```\n\n<Note>\n\n#### `use`는 더 이상 렌더링 중에 프로미스 생성을 지원하지 않습니다. {/*use-does-not-support-promises-created-in-render*/}\n\n만약 렌더링 중에 프로미스를 생성해 `use`에 전달하려고 하면 React는 경고를 표시할 것입니다.\n\n<ConsoleBlockMulti>\n\n<ConsoleLogLine level=\"error\">\n\nA component was suspended by an uncached promise. Creating promises inside a Client Component or hook is not yet supported, except via a Suspense-compatible library or framework.\n\n</ConsoleLogLine>\n\n</ConsoleBlockMulti>\n\n해결하려면 프로미스 캐싱을 위한 Suspense 기반 라이브러리나 프레임워크에서 프로미스를 전달해야 합니다. 앞으로 렌더링에서 프로미스를 더 쉽게 캐시할 수 있는 기능을 배포할 계획입니다.\n\n</Note>\n\n또한 `use`로 컨텍스트를 읽을 수 있으며 이를 통해 조기 반환 후와 같은 조건으로 컨텍스트를 읽을 수 있습니다.\n\n```js {1,11}\nimport {use} from 'react';\nimport ThemeContext from './ThemeContext'\n\nfunction Heading({children}) {\n  if (children == null) {\n    return null;\n  }\n\n  // 조기 반환으로 인하여,\n  // `useContext`는 동작하지 않습니다.\n  const theme = use(ThemeContext);\n  return (\n    <h1 style={{color: theme.color}}>\n      {children}\n    </h1>\n  );\n}\n```\n\n`use` API는 Hook과 유사하게 오직 렌더링 중일때만 호출됩니다. 훅과 달리 `use`는 조건적으로 호출됩니다. 앞으로 `use`를 사용하여 렌더링 중일 때 리소스들을 소비하도록 더 많은 방법을 지원할 계획입니다.\n\n더 많은 정보는 [`use`](/reference/react/use)문서를 참고하세요.\n\n## 새로운 React DOM의 정적 API {/*new-react-dom-static-apis*/}\n\n정적 사이트 생성을 위해 `react-dom/static`에 새로운 두 가지 API를 추가했습니다.\n- [`prerender`](/reference/react-dom/static/prerender)\n- [`prerenderToNodeStream`](/reference/react-dom/static/prerenderToNodeStream)\n\n이 새로운 API들은 `renderToString`보다 더 나아가서 정적 HTML 생성을 위해 데이터가 로드될 때까지 기다립니다. 이들은 Node.js Streams와 Web Streams와 같은 스트리밍 환경과 호환되도록 설계되었습니다. 예를 들어, Web Stream 환경에서는 `prerender`를 사용하여 React 트리를 정적 HTML로 미리 렌더링할 수 있습니다.\n\n```js\nimport { prerender } from 'react-dom/static';\n\nasync function handler(request) {\n  const {prelude} = await prerender(<App />, {\n    bootstrapScripts: ['/main.js']\n  });\n  return new Response(prelude, {\n    headers: { 'content-type': 'text/html' },\n  });\n}\n```\n\nPrerender API는 정적 HTML 스트림이 반환되기 전에 데이터가 로드되는 것을 기다립니다. Stream은 문자열로 변환이 가능하거나 스트리밍 응답으로 전송될 수 있습니다. 그러나 로드되는 콘텐츠를 스트리밍으로 지원하지 않습니다. 이는 기존 [React DOM server rendering APIs](/reference/react-dom/server)에서 지원됩니다.\n\n더 많은 정보는 [React DOM Static APIs](/reference/react-dom/static)를 참고하세요.\n\n## React 서버 컴포넌트 {/*react-server-components*/}\n\n### 서버 컴포넌트 {/*server-components*/}\n\n서버 컴포넌트는 번들링 전에 클라이언트 애플리케이션 또는 SSR 서버와 분리된 환경에서 컴포넌트를 미리 렌더링할 수 있는 새로운 옵션입니다. 이 별도의 환경이 React 서버 컴포넌트에서 \"서버\"입니다. 서버 컴포넌트는 CI 서버에서 빌드 시 한 번 실행하거나 웹 서버를 사용하여 각 요청에 대해 실행할 수 있습니다.\n\nReact 19는 Canary 채널에서 포함된 모든 React 서버 컴포넌트 기능을 포함하고 있습니다. 이는 서버 컴포넌트가 포함된 라이브러리들이 이제 [풀스택 React 아키텍처](/learn/creating-a-react-app#which-features-make-up-the-react-teams-full-stack-architecture-vision)를 지원하는 프레임워크에서 `react-server` [export 조건](https://github.com/reactjs/rfcs/blob/main/text/0227-server-module-conventions.md#react-server-conditional-exports)을 사용하여 React 19를 향한 상호 의존성<sup>Peer Dependencies</sup>으로 지정할 수 있음을 의미합니다.\n\n\n<Note>\n\n#### 서버 컴포넌트에 대한 지원을 어떻게 구축하나요? {/*how-do-i-build-support-for-server-components*/}\n\nReact 19에서 React 서버 컴포넌트는 안정적이며 마이너 버전 간에는 깨지지 않지만, React 서버 컴포넌트 번들러나 프레임워크를 구현하는 데 사용되는 기본 API는 semver를 따르지 않고 React 19.x의 마이너 버전 간에는 변경될 수 있습니다.\n\nReact 서버 컴포넌트를 지원하기 위해, 특정 React 버전에 고정하거나 Canary 릴리스를 사용하는 것을 권장합니다. 우리는 앞으로 번들러와 프레임워크와 협력하여 React 서버 컴포넌트를 구현하는 데 사용되는 API를 안정화할 계획입니다.\n\n</Note>\n\n\n더 많은 정보는 [React 서버 컴포넌트](/reference/rsc/server-components) 문서를 참고하세요.\n\n### 서버 액션 {/*server-actions*/}\n\n서버 액션은 서버에서 실행되는 비동기 함수를 클라이언트 컴포넌트에서 호출하는 것을 허용합니다.\n\n서버 액션이 `\"use server\"` 지시어로 정의될 때 프레임워크는 자동으로 서버 함수에 대한 참조를 생성하고 클라이언트 컴포넌트에 이를 전달합니다. 클라이언트에서 함수가 호출되면 React는 서버에 함수를 실행하라는 요청을 보내고 결과를 반환합니다.\n\n<Note>\n\n#### 서버 컴포넌트에 대한 지시어는 없습니다. {/*there-is-no-directive-for-server-components*/}\n\n흔한 오해는 서버 컴포넌트는 `\"use server\"`로 표시되지만 이에 대한 지시어는 존재하지 않습니다. `\"use server\"`지시어는 서버 액션을 위해 사용됩니다.\n\n더 많은 정보는 [지시어](/reference/rsc/directives)문서를 참고하세요.\n\n</Note>\n\n서버 액션은 서버 컴포넌트에서 생성되며 클라이언트 컴포넌트에 props를 전달되거나 클라이언트 컴포넌트에서 가져와 사용할 수 있습니다.\n\n더 많은 정보는 [React 서버 액션](/reference/rsc/server-actions)을 참고하세요.\n\n## React 19에서 개선 사항 {/*improvements-in-react-19*/}\n\n### Prop으로의 `ref` {/*ref-as-a-prop*/}\n\nReact 19부터 함수 컴포넌트의 Prop으로 `ref`에 접근할 수 있습니다.\n\n```js [[1, 1, \"ref\"], [1, 2, \"ref\", 45], [1, 6, \"ref\", 14]]\nfunction MyInput({placeholder, ref}) {\n  return <input placeholder={placeholder} ref={ref} />\n}\n\n//...\n<MyInput ref={ref} />\n```\n\n새로운 함수 컴포넌트에서는 더 이상 `forwardRef`이 필요하지 않으며, 새로운 `ref` Prop을 사용하도록 컴포넌트를 자동으로 업데이트하는 codemod를 배포할 예정입니다. 앞으로의 버전에서는 `forwardRef`를 사용하지 않도록 제거하고 더 이상 사용하지 않을 계획입니다.\n\n<Note>\n\n클래스에 전달된 `ref`는 컴포넌트 인스턴스를 참조하기 때문에 Props로 전달되지 않습니다.\n\n</Note>\n\n### 하이드레이션 에러에 대한 차이<sup>Diff</sup> {/*diffs-for-hydration-errors*/}\n\n예를 들어, 일치하지 않는 정보 없이 DEV 환경에서 여러 에러 로깅하는 대신 `react-dom`에서 하이드레이션 에러에 대한 오류 보고를 개선했습니다. \n\n<ConsoleBlockMulti>\n\n<ConsoleLogLine level=\"error\">\n\nWarning: Text content did not match. Server: \"Server\" Client: \"Client\"\n{'  '}at span\n{'  '}at App\n\n</ConsoleLogLine>\n\n<ConsoleLogLine level=\"error\">\n\nWarning: An error occurred during hydration. The server HTML was replaced with client content in \\<div\\>.\n\n</ConsoleLogLine>\n\n<ConsoleLogLine level=\"error\">\n\nWarning: Text content did not match. Server: \"Server\" Client: \"Client\"\n{'  '}at span\n{'  '}at App\n\n</ConsoleLogLine>\n\n<ConsoleLogLine level=\"error\">\n\nWarning: An error occurred during hydration. The server HTML was replaced with client content in \\<div\\>.\n\n</ConsoleLogLine>\n\n<ConsoleLogLine level=\"error\">\n\nUncaught Error: Text content does not match server-rendered HTML.\n{'  '}at checkForUnmatchedText\n{'  '}...\n\n</ConsoleLogLine>\n\n</ConsoleBlockMulti>\n\n이제 일치하지 않는 차이점을 보여주는 단일 메시지를 로깅합니다.\n\n\n<ConsoleBlockMulti>\n\n<ConsoleLogLine level=\"error\">\n\nUncaught Error: Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client. This can happen if an SSR-ed Client Component used:{'\\n'}\n\\- A server/client branch `if (typeof window !== 'undefined')`.\n\\- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.\n\\- Date formatting in a user's locale which doesn't match the server.\n\\- External changing data without sending a snapshot of it along with the HTML.\n\\- Invalid HTML tag nesting.{'\\n'}\nIt can also happen if the client has a browser extension installed which messes with the HTML before React loaded.{'\\n'}\nhttps://react.dev/link/hydration-mismatch {'\\n'}\n{'  '}\\<App\\>\n{'    '}\\<span\\>\n{'+    '}Client\n{'-    '}Server{'\\n'}\n{'  '}at throwOnHydrationMismatch\n{'  '}...\n\n</ConsoleLogLine>\n\n</ConsoleBlockMulti>\n\n### Provider로 사용하는 `<Context>` {/*context-as-a-provider*/}\n\nReact 19에서는 `<Context.Provider>` 대신에 `<Context>` Provider로 렌더링할 수 있습니다.\n\n\n```js {5,7}\nconst ThemeContext = createContext('');\n\nfunction App({children}) {\n  return (\n    <ThemeContext value=\"dark\">\n      {children}\n    </ThemeContext>\n  );\n}\n```\n\n새로운 Context 프로바이더는 `<Context>` 사용할 수 있고 기존 존재하는 프로바이더를 변환하기 위한 codemod를 배포할 예정입니다. 앞으로의 버전에서 `<Context.Provider>`를 더 이상 사용하지 않을 계획입니다.\n\n### Ref를 위한 클린업 함수 {/*cleanup-functions-for-refs*/}\n\n이제 `ref` 콜백에 클린업 함수를 반환하는 것을 지원합니다.\n\n```js {7-9}\n<input\n  ref={(ref) => {\n    // ref 생성\n\n    // NEW: 재설정을 위한 클린업 함수 반환\n    // DOM에서 엘리먼트가 제거될 때의 ref\n    return () => {\n      // ref 클린업\n    };\n  }}\n/>\n```\n\n컴포넌트가 마운트 해제될 때, React는 `ref` 콜백으로부터 클린업 함수를 호출할 것입니다. 이는 DOM Ref, 클래스 컴포넌트 Ref 그리고 `useImperativeHandle` 모두 해당합니다.\n\n<Note>\n\n이전에는 React가 컴포넌트를 마운트 해제할 때 `ref` 함수를 `null`과 함께 호출했습니다. 이제 만약 `ref`가 클린업 함수를 반환한다면, React는 이 단계를 건너뜁니다.\n\n앞으로의 버전에서는 컴포넌트를 마운트 해제할 때 `null`과 함께 `ref`를 호출하는 것을 더 이상 사용하지 않을 예정입니다.\n\n</Note>\n\n`ref` 클린업 함수의 도입으로 인해, TypeScript에서 `ref` 콜백에서 다른 값을 반환하는 것이 거부될 것입니다. 일반적으로 해결 방법은 암시적 반환을 사용하지 않도록 하는 것입니다. 예시는 아래와 같습니다.\n\n```diff [[1, 1, \"(\"], [1, 1, \")\"], [2, 2, \"{\", 15], [2, 2, \"}\", 1]]\n- <div ref={current => (instance = current)} />\n+ <div ref={current => {instance = current}} />\n```\n\n기존 코드는 `HTMLDivElement`의 인스턴스를 반환했고, TypeScript는 이것이 _클린업 함수인지_ 혹은 클린업 함수를 반환하지 않으려는 것인지 알지 못합니다.\n\n이 패턴은 [`no-implicit-ref-callback-return`](https://github.com/eps1lon/types-react-codemod/#no-implicit-ref-callback-return)을 사용하여 codemod로 변환할 수 있습니다.\n\n### `useDeferredValue` 초기값 {/*use-deferred-value-initial-value*/}\n\n`useDeferredValue`에 `initialValue` 옵션을 추가했습니다.\n\n```js [[1, 1, \"deferredValue\"], [1, 4, \"deferredValue\"], [2, 4, \"''\"]]\nfunction Search({deferredValue}) {\n  // 초기 렌더링 값은 '' 입니다.\n  // deferredValue로 리렌더링됩니다.\n  const value = useDeferredValue(deferredValue, '');\n\n  return (\n    <Results query={value} />\n  );\n}\n```\n\n<CodeStep step={2}>initialValue</CodeStep>이 제공되면, `useDeferredValue`는 컴포넌트의 초기 렌더링에서 이를 `value`로 반환하고 백그라운드에서 <CodeStep step={1}>deferredValue</CodeStep>으로 리렌더링을 예약합니다.\n\n더 많은 정보는 [`useDeferredValue`](/reference/react/useDeferredValue) 문서를 참고하세요.\n\n### 메타데이터 문서에 대한 지원 {/*support-for-metadata-tags*/}\n\nHTML에서는 `<title>`, `<link>`, `<meta>`와 같은 문서 메타 데이터 태그들이 문서의 `<head>` 섹션에 배치되어야 합니다. 그러나 React에서는 애플리케이션에 적합한 메타 데이터를 결정하는 컴포넌트가 `<head>`를 렌더링하는 곳과 아주 멀리 떨어져 있을 수 있거나, React 자체에서 `<head>`를 전혀 렌더링하지 않을 수도 있습니다. 과거에는 이러한 엘리먼트들을 이펙트를 사용하여 수동으로 삽입하거나 [`react-helmet`](https://github.com/nfl/react-helmet)과 같은 라이브러리를 사용하여 처리해야 했으며, React 애플리케이션을 서버에서 렌더링할 때 주의 깊게 처리해야 했습니다.\n\nReact 19에서는 컴포넌트에서 문서 메타 데이터 태그를 네이티브로 렌더링할 수 있는 지원을 추가하고 있습니다.\n\n```js {5-8}\nfunction BlogPost({post}) {\n  return (\n    <article>\n      <h1>{post.title}</h1>\n      <title>{post.title}</title>\n      <meta name=\"author\" content=\"Josh\" />\n      <link rel=\"author\" href=\"https://twitter.com/joshcstory/\" />\n      <meta name=\"keywords\" content={post.keywords} />\n      <p>\n        Eee equals em-see-squared...\n      </p>\n    </article>\n  );\n}\n```\n\nReact가 이 컴포넌트를 렌더링하면 `<title>` `<link>` 그리고 `<meta>`들이 보여지고 자동으로 문서의 `<head>` 섹션으로 호이스팅됩니다. 이러한 메타데이터 태그들이 네이티브로 지원하면 클라이언트 전용 앱, 스트리밍 SSR, 서버 컴포넌트와 동작되도록 보장할 수 있습니다.\n\n<Note>\n\n#### 여전히 메타데이터 라이브러리를 원한다면 {/*you-may-still-want-a-metadata-library*/}\n\n간단한 사용 사례의 경우에 문서 메타 데이터를 태그로 렌더링하는 것이 적합할 수 있지만, 라이브러리는 현재 경로에 따라 일반 메타 데이터를 구체적인 메타 데이터로 덮어쓰는 등 더 강력한 기능을 제공할 수 있습니다. 이러한 기능들은 메타 데이터 태그를 대체하는 것보다 [`react-helmet`](https://github.com/nfl/react-helmet)과 같은 프레임워크나 라이브러리를 더 쉽게 할 수 있도록 합니다.\n\n</Note>\n\n더 많은 내용은 [`<title>`](/reference/react-dom/components/title), [`<link>`](/reference/react-dom/components/link), [`<meta>`](/reference/react-dom/components/meta) 문서를 참고하세요.\n\n### 스타일시트 지원 {/*support-for-stylesheets*/}\n\n외부 링크 (`<link rel=\"stylesheet\" href=\"...\">`)와 인라인 (`<style>...</style>`) 스타일 시트는 스타일 우선순위 규칙으로 인해 DOM에서 안전한 위치를 요구합니다. 컴포넌트 내에서 합성 가능성을 허용하는 스타일시트 기능을 구축하는것은 어렵습니다. 그래서 사용자들은 종종 컴포넌트에서 멀리 떨어진 곳에서 모든 스타일을 로드하거나, 이러한 복잡성을 캡슐화하는 스타일 라이브러리를 사용하게 됩니다.\n\nReact 19에서는 이 복잡성에 대응하고, 클라이언트에서의 동시 렌더링과 서버에서의 스트리밍 렌더링에 대한 더 깊은 통합을 제공하며, 스타일시트에 대한 내장 지원을 제공합니다. 스타일시트의 `precedence`를 React에게 알리면, React는 스타일시트의 DOM 삽입 순서를 관리하고, 스타일 규칙에 의존하는 콘텐츠를 노출하기 전에 (외부의 경우) 스타일시트가 로드될 수 있도록 보장합니다.\n\n```js {4,5,17}\nfunction ComponentOne() {\n  return (\n    <Suspense fallback=\"loading...\">\n      <link rel=\"stylesheet\" href=\"foo\" precedence=\"default\" />\n      <link rel=\"stylesheet\" href=\"bar\" precedence=\"high\" />\n      <article class=\"foo-class bar-class\">\n        {...}\n      </article>\n    </Suspense>\n  )\n}\n\nfunction ComponentTwo() {\n  return (\n    <div>\n      <p>{...}</p>\n      <link rel=\"stylesheet\" href=\"baz\" precedence=\"default\" />  <-- foo 와 bar 사이에 삽입될 것\n    </div>\n  )\n}\n```\n\n서버 사이드 렌더링 중에 React는 스타일시트를 `<head>`에 포함합니다. 이는 브라우저가 스타일시트를 로드할 때까지 페인팅을 하지 않도록 보장합니다. 만약 스트리밍을 시작한 후 늦게 스타일시트가 발견된다면, React는 해당 스타일시트에 의존하는 Suspense 경계의 콘텐츠를 표시하기 전에 클라이언트에서 스타일시트가 `<head>`에 삽입되도록 보장합니다.\n\n클라이언트 사이드 렌더링 중에는 React가 렌더링을 커밋하기 전에 새로 렌더링된 스타일시트가 로드될 때까지 기다립니다. 애플리케이션의 여러 위치에서 이 컴포넌트를 렌더링하더라도 React는 문서에 스타일시트를 한 번만 포함합니다.\n\n```js {5}\nfunction App() {\n  return <>\n    <ComponentOne />\n    ...\n    <ComponentOne /> // DOM 내에서 스타일 시트 링크가 중복으로 이어지지 않습니다.\n  </>\n}\n```\n\n스타일시트를 수동으로 로드하는 데 익숙한 사용자들에게 이것은 의존하는 컴포넌트 옆에 스타일시트를 배치할 기회를 제공합니다. 이를 통해 더 나은 지역적 추론이 가능하고 실제로 의존하는 스타일시트만 로드하도록 보장하는 것이 더 쉬워집니다.\n\n스타일 라이브러리와 번들러의 통합도 이 새로운 기능을 채택할 수 있으므로, 직접 스타일시트를 렌더링하지 않더라도 도구가 이 기능을 사용하도록 업그레이드되면 여전히 혜택을 받을 수 있습니다.\n\n자세한 내용은 [`<link>`](/reference/react-dom/components/link)와 [`<style>`](/reference/react-dom/components/style)에 대한 문서를 참조하세요.\n\n### 비동기 스트립트 지원 {/*support-for-async-scripts*/}\n\nHTML 일반 스크립트 (`<script src=\"...\">`)와 지연 스크립트(`<script defer=\"\" src=\"...\">`)는 문서 순서대로 로드되어 컴포넌트 트리 깊숙한 곳에 이러한 종류의 스크립트를 렌더링하는 것을 어렵게 만듭니다. 그러나 비동기 스크립트 (`<script async=\"\" src=\"...\">`)는 임의의 순서로 로드됩니다.\n\nReact 19에서는 비동기 스크립트에 대한 더 나은 지원을 포함하여, 스크립트 인스턴스의 재배치와 중복 제거를 관리하지 않아도 실제로 스크립트에 의존하는 컴포넌트 트리내 어디든 렌더링할 수 있도록 허용합니다.\n\n```js {4,15}\nfunction MyComponent() {\n  return (\n    <div>\n      <script async={true} src=\"...\" />\n      Hello World\n    </div>\n  )\n}\n\nfunction App() {\n  <html>\n    <body>\n      <MyComponent>\n      ...\n      <MyComponent> // DOM 내에서 스크립트가 중복으로 이어지지 않습니다.\n    </body>\n  </html>\n}\n```\n\n모든 렌더링 환경에서, 비동기 스크립트는 중복으로 처리되어 React가 동일한 스크립트를 여러 다른 컴포넌트에서 렌더링하더라도 한 번만 로드하고 실행합니다.\n\n서버 사이드 렌더링에서는 비동기 스크립트가 `<head>`에 포함되며, 스타일시트, 폰트, 이미지 프리로드와 같이 페인트를 차단하는 더 중요한 리소스 뒤에 우선적으로 처리됩니다.\n\n더 자세한 내용은 [`<script>`](/reference/react-dom/components/script) 문서를 참조하세요.\n\n### 리소스 사전 로드 지원 {/*support-for-preloading-resources*/}\n\n문서 초기 로드 및 클라이언트 측 업데이트 중에 브라우저에 가능한 한 빨리 로드해야 할 리소스에 대해 알려주는 것이 페이지 성능에 중대한 영향을 미칠 수 있습니다.\n\nReact 19에는 비효율적인 리소스 로딩으로 인해 좋지 않은 경험을 제한받지 않도록, 브라우저 리소스를 로드하고 사전로드하기 위한 여러 새로운 API가 포함되어 있습니다. 이를 통해 우수한 사용자 경험을 구축하는 것이 가능하게 되었습니다.\n\n```js\nimport { prefetchDNS, preconnect, preload, preinit } from 'react-dom'\nfunction MyComponent() {\n  preinit('https://.../path/to/some/script.js', {as: 'script' }) // 스크립트 즉시 로드 실행\n  preload('https://.../path/to/font.woff', { as: 'font' }) // 폰트 사전로드\n  preload('https://.../path/to/stylesheet.css', { as: 'style' }) // 스타일시트 사전로드\n  prefetchDNS('https://...') // 실제로 이 호스트에서 아무것도 요청하지 않을때\n  preconnect('https://...') // 어떤 것을 요청할지 확신하지 못할 때\n}\n```\n```html\n<!-- 위 내용은 다음과 같은 DOM/HTML을 결과로 합니다. -->\n<html>\n  <head>\n    <!-- link/script는 호출순서에 따라 정렬되지 않고 초기 로딩의 유용성에 따라 우선순위 결정 -->\n    <link rel=\"prefetch-dns\" href=\"https://...\">\n    <link rel=\"preconnect\" href=\"https://...\">\n    <link rel=\"preload\" as=\"font\" href=\"https://.../path/to/font.woff\">\n    <link rel=\"preload\" as=\"style\" href=\"https://.../path/to/stylesheet.css\">\n    <script async=\"\" src=\"https://.../path/to/some/script.js\"></script>\n  </head>\n  <body>\n    ...\n  </body>\n</html>\n```\n\n이러한 API들은 초기 페이지 로드 최적화에 사용될 수 있으며, 스타일시트 로딩에서 폰트와 같은 추가 리소스의 발견을 완화시킬 수 있습니다. 또한, 예상된 네비게이션에서 사용되는 리소스 목록을 사전에 가져와 클릭 또는 호버 시 이러한 리소스를 즉시 사전로드하여 클라이언트 업데이트 속도를 높일 수 있습니다.\n\n더 자세한 내용은 [리소스 사전 로드 API](/reference/react-dom#resource-preloading-apis)를 참고하세요.\n\n### 서드파티 스크립트와 확장 프로그램의 호환성 {/*compatibility-with-third-party-scripts-and-extensions*/}\n\n저희는 서드파티 스크립트와 브라우저 확장 프로그램 호환성을 개선했습니다.\n\n화면 새로고침 시, 클라이언트에서 렌더링되는 엘리먼트가 서버에서 제공된 HTML과 일치하지 않으면 React는 컨텐츠를 수정하기 위해 클라이언트에서 강제 리렌더링합니다. 이전에는 서드파티 스크립트나 브라우저 확장 프로그램에 의해 삽입된 엘리먼트는 불일치 오류와 클라이언트 리렌더링을 유발했습니다.\n\nReact 19에서는 `<head>` 및 `<body>`에서 예상치 못한 태그가 발견되면 불일치 오류를 피하고자 이러한 태그들을 건너뜁니다. 또한, React가 관계없는 불일치로 인해 전체 문서를 리렌더링해야 할 경우, 서드파티 스크립트와 브라우저 확장 프로그램에 의해 삽입된 스타일시트는 그대로 남겨집니다.\n\n### 더 나은 에러 리포팅 {/*error-handling*/}\n\nReact 19에서는 오류 처리를 개선하여 중복을 줄이고 잡힌 오류와 잡히지 않은 오류를 처리할 수 있는 옵션을 제공했습니다. 예를 들어, 에러 바운더리에 의해 잡힌 렌더링 중 오류가 발생할 경우, 이전에는 React가 오류를 두 번 던졌습니다 (원래 오류와 자동 복구에 실패한 후에 다시). 그리고 `console.error`를 호출하여 오류가 발생한 위치에 대한 정보를 출력했습니다.\n\n이에 따라 잡힌 오류마다 세 개의 오류가 발생하는 문제가 있었습니다.\n\n<ConsoleBlockMulti>\n\n<ConsoleLogLine level=\"error\">\n\nUncaught Error: hit\n{'  '}at Throws\n{'  '}at renderWithHooks\n{'  '}...\n\n</ConsoleLogLine>\n\n<ConsoleLogLine level=\"error\">\n\nUncaught Error: hit<span className=\"ms-2 text-gray-30\">{'    <--'} Duplicate</span>\n{'  '}at Throws\n{'  '}at renderWithHooks\n{'  '}...\n\n</ConsoleLogLine>\n\n<ConsoleLogLine level=\"error\">\n\nThe above error occurred in the Throws component:\n{'  '}at Throws\n{'  '}at ErrorBoundary\n{'  '}at App{'\\n'}\nReact will try to recreate this component tree from scratch using the error boundary you provided, ErrorBoundary.\n\n</ConsoleLogLine>\n\n</ConsoleBlockMulti>\n\nReact 19에서는 모든 오류 정보를 하나의 오류 로그로 기록합니다.\n\n<ConsoleBlockMulti>\n\n<ConsoleLogLine level=\"error\">\n\nError: hit\n{'  '}at Throws\n{'  '}at renderWithHooks\n{'  '}...{'\\n'}\nThe above error occurred in the Throws component:\n{'  '}at Throws\n{'  '}at ErrorBoundary\n{'  '}at App{'\\n'}\nReact will try to recreate this component tree from scratch using the error boundary you provided, ErrorBoundary.\n{'  '}at ErrorBoundary\n{'  '}at App\n\n</ConsoleLogLine>\n\n</ConsoleBlockMulti>\n\n추가로, `onRecoverableError`을 보완하기 위해 두 새로운 루트 옵션을 추가했습니다.\n\n- `onCaughtError`: React가 에러 바운더리에서 오류를 잡을 때 호출됩니다.\n- `onUncaughtError`: 에러가 발생하고 에러 바운더리에 의해 잡히지 않을 때 호출됩니다.\n- `onRecoverableError`: 에러가 발생하고 자동으로 복구될 때 호출됩니다.\n\n더 자세한 내용과 예시는 [`createRoot`](/reference/react-dom/client/createRoot)와 [`hydrateRoot`](/reference/react-dom/client/hydrateRoot) 문서를 참고하세요.\n\n### 커스텀 엘리먼트 지원 {/*support-for-custom-elements*/}\n\nReact 19는 커스텀 엘리먼트에 대한 모든 지원을 추가하고 [Custom Elements Everywhere](https://custom-elements-everywhere.com/)의 모든 테스트를 통과했습니다.\n\n이전 버전에서는 React에서 인식되지 않는 props를 속성으로 처리하여 커스텀 엘리먼트 사용이 어려웠습니다. React 19에서는 클라이언트 및 SSR에서 속성을 지원하도록 아래와 같은 전략을 추가했습니다.\n\n- **서버 사이드 렌더링**: `string`, `number` 또는 한 값이 `true`인 원시 값일 경우 커스텀 엘리먼트에 전달된 props는 렌더링 될 것입니다. 비-원시 타입인 `object`, `symbol`, `function` 또는 값이 `false`인 props는 생략됩니다.\n- **클라이언트 사이드 렌더링**: 커스텀 엘리먼트 인스턴스의 속성과 일치하는 props는 프로퍼티로 할당됩니다. 그렇지 않은 경우에는 어트리뷰트로 할당됩니다.\n\nReact의 Custom 엘리먼트 지원의 설계 및 구현을 주도해 주신 [Joey Arhar](https://github.com/josepharhar)에게 감사드립니다.\n\n\n#### 업그레이드 방법 {/*how-to-upgrade*/}\n단계별 지침과 주요 변경 사항의 전체 목록은 [React 19 Upgrade Guide](/blog/2024/04/25/react-19-upgrade-guide)를 참고하세요.\n\n_참고: 이 게시물은 원래 2024년 4월 25일에 게시되었으며, 안정적인 릴리스와 함께 2024년 12월 5일로 업데이트되었습니다._\n"
  },
  {
    "path": "src/content/blog/2025/02/14/sunsetting-create-react-app.md",
    "content": "---\ntitle: \"Create React App 지원 종료\"\nauthor: Matt Carroll and Ricky Hanlon\ndate: 2025/02/14\ndescription: 새로운 앱에 대한 Create React App 사용을 중단하며, 기존 앱은 프레임워크나 Vite, Parcel, RSBuild 같은 빌드 도구로의 마이그레이션을 권장합니다. 또한 프레임워크가 프로젝트와 맞지 않거나, 자신만의 프레임워크를 구축하고 싶거나, 혹은 React가 어떻게 작동하는지 배우기 위해 React 앱을 처음부터 만들어 보고 싶은 사용자들을을 위한 문서를 제공합니다.\n---\n\n2025년 2월 14일, [Matt Carroll](https://twitter.com/mattcarrollcode), [Ricky Hanlon](https://bsky.app/profile/ricky.fm)\n\n---\n\n<Intro>\n\n새로운 앱에 대한 [Create React App](https://create-react-app.dev/) 사용을 중단하며, 기존 앱은 [프레임워크](#how-to-migrate-to-a-framework)나 Vite, Parcel, RSBuild 같은 빌드 도구로의 [마이그레이션](#how-to-migrate-to-a-build-tool)을 권장합니다.\n\n또한 프레임워크가 프로젝트와 맞지 않거나, 자신만의 프레임워크를 구축하고 싶거나, 혹은 React가 어떻게 작동하는지 배우기 위해 React 앱을 처음부터 만들어 보고 싶은 사용자들을 위한 [문서](https://react.dev/learn/build-a-react-app-from-scratch)를 제공합니다.\n\n</Intro>\n\n-----\n\n2016년 Create React App을 처음 출시하였을 때는 React 앱을 새로 구축할 명확한 방법이 없었습니다.\n\n당시 React 앱을 만들기 위해서는 JSX, Linting, Hot Reloading과 같은 기본 기능을 지원하는 여러 도구를 직접 설치하고 연결해야 했습니다. 이는 올바르게 수행하기 매우 까다로웠기 때문에, [커뮤니티](https://github.com/react-boilerplate/react-boilerplate)에서는 [자주 사용](https://github.com/gaearon/react-hot-boilerplate)하는 [설정](https://github.com/erikras/react-redux-universal-hot-example)에 대한 [보일러 플레이트](https://github.com/petehunt/react-boilerplate)를 [만들었습니다.](https://github.com/kriasoft/react-starter-kit) 하지만 보일러 플레이트는 업데이트하기 어렵고 조각화로 인해 React 팀이 새로운 기능을 배포하는 데 어려움이 많았습니다.\n\nCreate React App은 여러 도구를 하나의 권장 설정으로 통합하여 이러한 문제를 해결했습니다. 이로 인해 앱이 새로운 도구 기능으로 쉽게 업그레이드할 수 있게 되었으며, React 팀은 자명하지 않은 도구 변경 (Fast Refresh 지원, React Hooks Lint 규칙 등)을 가능한 많은 사용자에게 배포할 수 있었습니다.\n\n이 모델은 매우 인기를 끌었고, 오늘날 비슷한 방식으로 작동하는 도구들이 하나의 카테고리를 형성할 정도가 되었습니다.\n\n## Create React App의 지원 종료 {/*deprecating-create-react-app*/}\n\nCreate React App은 시작을 쉽게 해주지만, 고성능의 프로덕션 앱 구축을 어렵게 하는 [몇 가지 제한](#limitations-of-build-tools)이 있습니다. 원칙적으로는 이를 [프레임워크](#why-we-recommend-frameworks)로 발전시켜 해결할 수도 있습니다.\n\n하지만 현재 Create React App은 적극적으로 관리하는 담당자가 없고, 이미 많은 기존 프레임워크들이 이러한 문제를 잘 해결하고 있기 때문에 사용을 중단하기로 결정했습니다.\n\n오늘부터 새로운 앱 설치 시에는 지원 종료 경고 메시지가 표시됩니다.\n\n<ConsoleBlockMulti>\n<ConsoleLogLine level=\"error\">\n\ncreate-react-app is deprecated.\n{'\\n\\n'}\nYou can find a list of up-to-date React frameworks on react.dev\nFor more info see: react.dev/link/cra\n{'\\n\\n'}\nThis error message will only be shown once per install.\n\n</ConsoleLogLine>\n</ConsoleBlockMulti>\n\nCreate React App [웹사이트](https://create-react-app.dev/)와 [GitHub 저장소](https://github.com/facebook/create-react-app)에도 지원 종료 안내를 추가했습니다. Create React App은 유지 보수 모드로 계속 동작하며, React 19와 호환되는 새로운 버전을 배포했습니다.\n\n## 프레임워크로 마이그레이션하는 방법 {/*how-to-migrate-to-a-framework*/}\nReact 앱을 프레임워크로 [새로 만들기](https://react.dev/learn/creating-a-react-app)를 권장합니다. 추천하는 모든 프레임워크는 클라이언트 측 렌더링([CSR](https://developer.mozilla.org/en-US/docs/Glossary/CSR))과 단일 페이지 앱([SPA](https://developer.mozilla.org/ko/docs/Glossary/SPA))을 지원하며, CDN 또는 정적 호스팅 서비스에 서버 없이 배포 가능합니다.\n\n기존 앱의 경우 다음 안내서를 참고하여 클라이언트 전용 SPA로 마이그레이션할 수 있습니다.\n\n* [Next.js의 Create React App 마이그레이션 가이드](https://nextjs.org/docs/app/building-your-application/upgrading/from-create-react-app)\n* [React Router의 프레임워크 도입 가이드](https://reactrouter.com/upgrading/component-routes)\n* [Expo 웹팩에서 Expo Router로의 마이그레이션 가이드](https://docs.expo.dev/router/migrate/from-expo-webpack/)\n\n## 빌드 도구로 마이그레이션하는 방법 {/*how-to-migrate-to-a-build-tool*/}\n\n앱이 특수한 제약 조건을 가지고 있거나, 자신만의 프레임워크를 구축하여 문제를 해결하고 싶은 경우, 혹은 React가 처음부터 어떻게 동작하는지 배우고 싶은 경우에는 Vite, Parcel, RSBuild 등을 이용하여 커스텀 설정을 직접 구축할 수 있습니다.\n\n기존 앱의 경우 다음 안내서를 참고하여 빌드 도구로 마이그레이션할 수 있습니다.\n\n* [Vite의 Create React App 마이그레이션 가이드](https://www.robinwieruch.de/vite-create-react-app/)\n* [Parcel의 Create React App 마이그레이션 가이드](https://parceljs.org/migration/cra/)\n* [RSBuild의 Create React App 마이그레이션 가이드](https://rsbuild.dev/guide/migration/cra)\n\nVite, Parcel 또는 RSBuild로 시작하는 데 도움을 주기 위해 [React 앱 구축하기](/learn/build-a-react-app-from-scratch)에 대한 새로운 문서를 추가했습니다.\n\n<DeepDive>\n\n#### 프레임워크가 필요할까요? {/*do-i-need-a-framework*/}\n\n대부분의 앱은 프레임워크를 사용하는 것이 유리하지만, React 앱을 처음부터 직접 구축해야 하는 타당한 경우도 있습니다. 일반적인 기준으로, 만약 앱에서 라우팅이 필요하다면 프레임워크를 사용하는 것이 더 나을 가능성이 큽니다. \n\nSvelte에는 SvelteKit, Vue에는 Nuxt 그리고 Solid에는 SolidStart가 있듯이, React도 기본적으로 라우팅을 포함한 데이터 가져오기, 코드 분할 등의 기능을 통합한 [프레임워크 사용을 권장합니다.](#why-we-recommend-frameworks) 이렇게 하면 복잡한 설정을 직접 구성하거나, 사실상 자체 프레임워크를 만들어야 하는 부담을 피할 수 있습니다.\n\n하지만 여전히 Vite, Parcel, Rsbuild 같은 빌드 도구를 사용해 [React 앱을 처음부터 직접 구축하는 것](/learn/build-a-react-app-from-scratch)도 가능합니다.\n\n</DeepDive>\n\n[빌드 도구의 한계](#limitations-of-build-tools)와 [프레임워크를 권장하는 이유](#why-we-recommend-frameworks)에 대해 자세히 알아보려면 계속 읽어보세요.\n\n## 빌드 도구의 한계 {/*limitations-of-build-tools*/}\n\nCreate React App과 같은 빌드 도구는 React 앱을 시작하는 것을 쉽게 만듭니다. `npx create-react-app my-app`을 실행하면 개발 서버, Linting, 프로덕션 빌드가 완전히 설정된 React 앱을 얻을 수 있습니다.\n\n예를 들어, 내부 관리자 도구를 구축하는 경우 랜딩 페이지부터 시작할 수 있습니다.\n\n```js\nexport default function App() {\n  return (\n    <div>\n      <h1>Welcome to the Admin Tool!</h1>\n    </div>\n  )\n}\n```\n\n이를 통해 JSX, 기본 Lint 규칙, 개발 및 프로덕션에서 모두 실행할 번들러와 함께 바로 React 코딩을 시작할 수 있습니다. 그러나 이 설정에는 실제 프로덕션 앱을 구축하는 데 필요한 도구가 빠져 있습니다.\n\n대부분의 프로덕션 앱은 라우팅, 데이터 가져오기, 코드 분할과 같은 문제에 대한 해결책이 필요합니다.\n\n### 라우팅 {/*routing*/}\n\nCreate React App에는 특정 라우팅 솔루션이 포함되어 있지 않습니다. 처음 시작할 때는 `useState`를 사용하여 라우팅 간 전환을 할 수 있습니다. 하지만 이렇게 하면 모든 링크가 동일한 페이지로 이동하게 되며, 시간이 지남에 따라 앱 구조화가 어려워지면서 앱에 링크를 공유할 수 없습니다.\n\n```js\nimport {useState} from 'react';\n\nimport Home from './Home';\nimport Dashboard from './Dashboard';\n\nexport default function App() {\n  // ❌ 라우팅은 State 내에서 URL을 생성하지 않습니다.\n  const [route, setRoute] = useState('home');\n  return (\n    <div>\n      {route === 'home' && <Home />}\n      {route === 'dashboard' && <Dashboard />}\n    </div>\n  )\n}\n```\n\n이러한 이유로 Create React App을 사용하는 대부분의 앱은 [React Router](https://reactrouter.com/)나 [Tanstack Router](https://tanstack.com/router/latest)와 같은 라우팅 라이브러리를 추가로 사용합니다. 라우팅 라이브러리를 사용하면 앱에 추가적인 라우트를 정의할 수 있으며, 앱 구조에 대한 의견을 제공하며 라우트에 대한 링크를 공유할 수 있습니다. 예를 들어 React Router를 사용하면 다음과 같이 라우트를 정의할 수 있습니다.\n\n```js\nimport {RouterProvider, createBrowserRouter} from 'react-router';\n\nimport Home from './Home';\nimport Dashboard from './Dashboard';\n\n// ✅ 각각의 라우트는 자신만의 URL을 가지고 있습니다.\nconst router = createBrowserRouter([\n  {path: '/', element: <Home />},\n  {path: '/dashboard', element: <Dashboard />}\n]);\n\nexport default function App() {\n  return (\n    <RouterProvider value={router} />\n  )\n}\n```\n\n이 변경으로 인해 `/dashboard`로 링크를 공유할 수 있고, 앱이 대시보드 페이지로 이동합니다. 라우팅 라이브러리를 사용하면 중첩 라우트, 라우트 보호, 라우트 전환 등 추가 기능을 쉽게 구현할 수 있습니다.\n\n라우팅 라이브러리는 앱에 복잡성을 더해주지만, 앱 없이는 구현하기 어려운 기능도 추가하는 Trade-Off가 존재합니다.\n\n### 데이터 가져오기 {/*data-fetching*/}\n\nCreate React App의 또 다른 일반적인 문제는 데이터를 가져오는 것입니다. Create React App은 특정 데이터를 가져오는 솔루션을 포함하지 않습니다. 처음 시작한다면, 일반적인 방법은 데이터를 로드하기 위해 Effect 내에서 `fetch`를 사용하는 것입니다.\n\n하지만 이 방법을 사용하면 컴포넌트를 렌더링한 후에 데이터를 가져오므로, 네트워크 폭포수<sup>Network Waterfalls</sup> 현상이 발생할 수 있습니다. 네트워크 폭포수 현상은 코드를 다운로드하는 동안 병렬로 처리하는 대신, 앱을 렌더링할 때 데이터를 가져오면서 발생합니다.\n\n```js\nexport default function Dashboard() {\n  const [data, setData] = useState(null);\n\n  // ❌ 컴포넌트 내에서 데이터를 가져오면 네트워크 폭포수 현상을 일으킵니다.\n  useEffect(() => {\n    fetch('/api/data')\n      .then(response => response.json())\n      .then(data => setData(data));\n  }, []);\n\n  return (\n    <div>\n      {data.map(item => <div key={item.id}>{item.name}</div>)}\n    </div>\n  )\n}\n```\n\nEffect에서 데이터를 가져오는것은, 데이터를 더 일찍 가져올 수 있었음에도 불구하고, 사용자가 콘텐츠를 보기 위해 더 오래 기다려야 함을 의미합니다. 이 문제를 해결하기 위해 컴포넌트를 렌더링하기 전에 요청을 시작할 수 있도록 데이터 미리 가져오기 옵션을 제공하는 [React Query](https://react-query.tanstack.com/), [SWR](https://swr.vercel.app/ko), [Apollo](https://www.apollographql.com/docs/react) 또는 [Relay](https://relay.dev/)와 같은 라이브러리들을 사용할 수 있습니다. \n\n이러한 라이브러리들은 라우트 수준에서 데이터 의존성을 지정할 수 있는 라우팅 \"로더\" 패턴과 통합될 때 가장 효과적으로 작동하며, 이를 통해 라우터가 데이터 가져오기를 최적화할 수 있습니다.\n\n```js\nexport async function loader() {\n  const response = await fetch(`/api/data`);\n  const data = await response.json();\n  return data;\n}\n\n// ✅ 코드를 다운로드 할 동안 데이터를 병렬로 가져옵니다.\nexport default function Dashboard({loaderData}) {\n  return (\n    <div>\n      {loaderData.map(item => <div key={item.id}>{item.name}</div>)}\n    </div>\n  )\n}\n```\n\n초기 로드 시, 라우터는 라우트가 렌더링되기 전에 즉시 데이터를 가져올 수 있습니다. 사용자가 앱 내에서 이동할 때, 라우터는 데이터와 라우트를 동시에 병렬적으로 가져올 수 있습니다. 이는 화면에 콘텐츠가 표시되는 데 걸리는 시간을 줄이고 사용자 경험을 향상시킬 수 있습니다.\n\n그러나 이를 위해서는 앱에서 로더를 올바르게 구성해야 하며, 성능을 위해 복잡성을 감수해야 합니다.\n\n### 코드 분할 {/*code-splitting*/}\n\nCreate React App의 또 다른 일반적인 문제는 [코드 분할](https://www.patterns.dev/vanilla/bundle-splitting/)입니다. Create React App은 특정 코드 분할 솔루션을 포함하지 않습니다. 처음 시작한다면, 코드 분할을 전혀 고려하지 않을 수도 있습니다.\n\n이는 앱이 하나의 번들로 제공되는 것을 의미합니다.\n\n```txt\n- bundle.js    75kb\n```\n\n하지만 최적의 성능을 위해서는 코드를 개별 번들로 \"분할\"하여 사용자가 필요한 것만 다운로드하도록 해야 합니다. 이렇게 하면 사용자가 현재 보고 있는 페이지에 필요한 코드만 다운로드하므로 앱 로딩 시간을 줄일 수 있습니다.\n\n```txt\n- core.js      25kb\n- home.js      25kb\n- dashboard.js 25kb\n```\n\n코드 분할을 구현하는 한 가지 방법은 `React.lazy`를 사용하는 것입니다. 그러나 컴포넌트를 렌더링할 때까지 코드를 가져오지 못한다는 것을 의미하므로 네트워크 폭포수가 발생할 수 있습니다. 더 최적화된 해결책은 코드가 다운로드되는 동안 병렬로 코드를 가져오는 라우터 기능을 사용하는 것입니다. 예를 들어, React Router는 라우트를 코드 분할을 해야 하며 로드 시점을 최적화해야 함을 지정하는 `lazy` 옵션을 제공합니다.\n\n```js\nimport Home from './Home';\nimport Dashboard from './Dashboard';\n\n// ✅ 라우터는 렌더링되기 전에 다운로드 됩니다.\nconst router = createBrowserRouter([\n  {path: '/', lazy: () => import('./Home')},\n  {path: '/dashboard', lazy: () => import('Dashboard')}\n]);\n```\n\n최적화된 코드 분할은 올바르게 구현하기 까다롭고, 사용자가 필요 이상의 코드를 다운로드하게 만드는 실수를 쉽게 할 수 있습니다. 이는 캐싱을 최대화하고, 가져오기를 병렬화하며, [\"상호작용 시 가져오기\"](https://www.patterns.dev/vanilla/import-on-interaction) 패턴을 지원하기 위해 라우터 및 데이터 로딩 솔루션과 통합될 때 가장 효과적으로 작동합니다.\n\n### 그리고... {/*and-more*/}\n\n이것들은 Create React App의 몇 가지 제한 사항 예시에 불과합니다.\n\n라우팅, 데이터 가져오기, 코드 분할을 통합한 후에는 보류 중인 상태, 내비게이션 중단, 사용자에게 보내는 오류 메시지, 데이터 재검증도 고려해야 합니다. 사용자가 해결해야 할 문제의 전체 범주는 다음과 같습니다.\n\n<div style={{display: 'flex', width: '100%', justifyContent: 'space-around'}}>\n  <ul>\n    <li>접근성</li>\n    <li>자산 로딩</li>\n    <li>인증</li>\n    <li>캐싱</li>\n  </ul>\n  <ul>\n    <li>오류 처리</li>\n    <li>데이터 변경</li>\n    <li>탐색</li>\n    <li>낙관적 업데이트</li>\n  </ul>\n  <ul>\n    <li>점진적 향상</li>\n    <li>서버 사이드 렌더링</li>\n    <li>정적 사이트 생성</li>\n    <li>스트리밍</li>\n  </ul>\n</div>\n\n이 모든 것들이 함께 작동하여 가장 최적화된 [로딩 순서](https://www.patterns.dev/vanilla/loading-sequence/)를 만듭니다.\n\nCreate React App에서 이러한 문제들을 개별적으로 해결하는 것은 각 문제가 서로 연결되어 있고 사용자가 익숙하지 않은 문제 영역에 대한 깊은 전문 지식이 필요할 수 있기 때문에 어려울 수 있습니다. 이러한 문제들을 해결하기 위해 사용자들은 결국 Create React App 위에 자신만의 맞춤형 솔루션을 구축하게 되는데, 이는 Create React App이 원래 해결하려고 했던 문제입니다.\n\n## 프레임워크를 권장하는 이유 {/*why-we-recommend-frameworks*/}\n\nCreate React App, Vite, Parcel과 같은 빌드 도구에서 모든 요소를 직접 해결할 수 있지만, 이를 잘 수행 하기에는 어렵습니다. Create React App 자체가 여러 빌드 도구를 통합했던 것처럼, 이제는 모든 기능을 통합하여 사용자에게 최상의 경험을 제공할 수 있는 도구가 필요합니다.\n\n빌드 도구, 렌더링, 라우팅, 데이터 가져오기 및 코드 분할을 통합하는 이러한 종류의 도구들을 \"프레임워크\"라고 합니다. 또는 React 자체를 프레임워크라고 부르기도 하지만, 이들을 \"메타프레임워크\"라고 부를 수도 있습니다.\n\n프레임워크는 빌드 도구가 도구 사용을 쉽게 하기 위해 일부 의견을 강제하는 것과 같은 방식으로, 훨씬 더 나은 사용자 경험을 제공하기 위해 앱 구조화에 대한 일부 의견을 강제합니다. 이것이 우리가 새 프로젝트에 [Next.js](https://nextjs.org/), [React Router](https://reactrouter.com/) 및 [Expo](https://expo.dev/)와 같은 프레임워크를 권장하기 시작한 이유입니다.\n\n프레임워크는 Create React App과 동일한 시작 경험을 제공하지만, 사용자가 실제 프로덕션 앱에서 결국에는 해결해야만 하는 문제에 대한 해결책도 제공합니다.\n\n<DeepDive>\n\n#### 서버 렌더링은 선택적입니다 {/*server-rendering-is-optional*/}\n\n저희가 추천하는 프레임워크들은 모두 [클라이언트 사이드 렌더링(CSR)](https://developer.mozilla.org/en-US/docs/Glossary/CSR) 앱을 만들 수 있는 옵션을 제공합니다.\n\n경우에 따라 CSR이 페이지에 적합한 선택일 수 있지만, 대부분은 그렇지 않습니다. 앱의 대부분이 클라이언트 사이드라 하더라도, 이용약관 페이지나 문서와 같이 [정적 사이트 생성(SSG)](https://developer.mozilla.org/en-US/docs/Glossary/SSG) 또는 [서버 사이드 렌더링(SSR)](https://developer.mozilla.org/en-US/docs/Glossary/SSR)과 같은 서버 렌더링 기능의 혜택을 받을 수 있는 개별 페이지들이 많이 있습니다.\n\n서버 렌더링은 일반적으로 클라이언트에 더 적은 자바스크립트를 전송하고, 완전한 HTML 문서를 제공하여 [총 차단 시간(TBT)](https://web.dev/articles/tbt?hl=ko)을 줄임으로써 더 빠른 [최초 콘텐츠 페인트(FCP)](https://web.dev/articles/fcp?hl=ko)를 생성하며, 이는 [상호작용에서 다음 페인트까지(INP)](https://web.dev/articles/inp?hl=ko)도 낮출 수 있습니다. 이것이 Chrome 팀이 개발자들에게 최상의 성능을 달성하기 위해 완전한 클라이언트 사이드 접근 방식보다 정적 또는 서버 사이드 렌더링을 고려할 것을 [권장하는 이유](https://web.dev/articles/rendering-on-the-web?hl=ko)입니다.\n\n서버를 사용하는 데는 트레이드 오프가 있으며, 모든 페이지에 항상 최선의 선택인 것은 아닙니다. 서버에서 페이지를 생성하는 것은 추가 비용이 발생하고 생성하는 데 시간이 걸리므로 [최초 바이트까지의 시간(TTFB)](https://web.dev/articles/ttfb?hl=ko)이 증가할 수 있습니다. 가장 성능이 좋은 앱은 각 전략의 트레이드오프를 기반으로 페이지별로 적절한 렌더링 전략을 선택할 수 있습니다.\n\n프레임워크는 원하는 경우 모든 페이지에서 서버를 사용할 수 있는 옵션을 제공하지만, 서버 사용을 강제하지는 않습니다. 이를 통해 앱의 각 페이지에 맞는 렌더링 전략을 선택할 수 있습니다.\n\n#### 서버 컴포넌트는 어떤가요 {/*server-components*/}\n\n저희가 추천하는 프레임워크는 React 서버 컴포넌트도 지원합니다.\n\n서버 컴포넌트는 라우팅과 데이터 가져오기를 서버로 이동시키고, 렌더링되는 경로가 아닌 렌더링하는 데이터를 기반으로 클라이언트 컴포넌트에 대한 코드 분할이 가능하게 함으로써 이러한 문제를 해결하는 데 도움을 주며, 최상의 [로딩 시퀀스](https://www.patterns.dev/vanilla/loading-sequence)를 위해 전송되는 자바스크립트 양을 줄입니다.\n\n서버 컴포넌트는 서버를 필요로 하지 않습니다. CI 서버에서 빌드 시점에 실행하여 정적 사이트 생성(SSG) 앱을 만들거나, 웹 서버에서 런타임에 실행하여 서버 사이드 렌더링(SSR) 앱을 만들 수 있습니다.\n\n자세한 내용은 [제로 번들 사이즈 React 서버 컴포넌트 소개](/blog/2020/12/21/data-fetching-with-react-server-components) 및 [문서](/reference/rsc/server-components)를 참조하세요.\n\n</DeepDive>\n\n<Note>\n\n#### 서버 렌더링은 SEO만을 위한 것이 아닙니다 {/*server-rendering-is-not-just-for-seo*/}\n\n서버 렌더링이 [SEO](https://developer.mozilla.org/ko/docs/Glossary/SEO)만을 위한 것이라는 것은 흔한 오해입니다.\n\n서버 렌더링은 SEO를 개선할 수 있지만, 사용자가 화면에서 콘텐츠를 보기 전에 다운로드하고 파싱해야 하는 자바스크립트의 양을 줄임으로써 성능도 향상시킵니다.\n\n이것이 Chrome 팀이 개발자들에게 최상의 성능을 달성하기 위해 완전한 클라이언트 사이드 접근 방식보다 정적 또는 서버 사이드 렌더링을 고려할 것을 [권장하는 이유](https://web.dev/articles/rendering-on-the-web?hl=ko)입니다.\n\n</Note>\n\n---\n\n_[Dan Abramov](https://bsky.app/profile/danabra.mov)에게 Create React App을 만들어줘서 감사하며, [Joe Haddad](https://github.com/Timer), [Ian Schmitz](https://github.com/ianschmitz), [Brody McKee](https://github.com/mrmckeb), 그리고 [그 외 많은 분들](https://github.com/facebook/create-react-app/graphs/contributors)께 오랜 기간 Create React App을 유지보수해 주셔서 감사드립니다. 또한, [Brooks Lybrand](https://bsky.app/profile/brookslybrand.bsky.social), [Dan Abramov](https://bsky.app/profile/danabra.mov), [Devon Govett](https://bsky.app/profile/devongovett.bsky.social), [Eli White](https://x.com/Eli_White), [Jack Herrington](https://bsky.app/profile/jherr.dev), [Joe Savona](https://x.com/en_JS), [Lauren Tan](https://bsky.app/profile/no.lol), [Lee Robinson](https://x.com/leeerob), [Mark Erikson](https://bsky.app/profile/acemarke.dev), [Ryan Florence](https://x.com/ryanflorence), [Sophie Alpert](https://bsky.app/profile/sophiebits.com), [Tanner Linsley](https://bsky.app/profile/tannerlinsley.com), 그리고 [Theo Browne](https://x.com/theo)에게 이 글을 검토하고 피드백을 제공해 주셔서 감사드립니다._\n\n"
  },
  {
    "path": "src/content/blog/2025/04/21/react-compiler-rc.md",
    "content": "---\ntitle: 'React 컴파일러 RC'\nauthor: Lauren Tan and Mofei Zhang\ndate: 2025/04/21\ndescription: 컴파일러의 첫 번째 릴리즈 후보(Release Candidate, RC)를 공개합니다.\n---\n\n2025년 4월 21일, [Lauren Tan](https://x.com/potetotes), [Mofei Zhang](https://x.com/zmofei)\n\n---\n\n<Intro>\n\nReact 팀이 새로운 업데이트를 발표합니다.\n\n</Intro>\n\n1. 오늘 공개된 React 컴파일러 RC는 안정화 출시를 위한 준비 단계입니다.\n2. `eslint-plugin-react-compiler`를 `eslint-plugin-react-hooks` 에 통합했습니다.\n3. swc 지원을 추가했으며 Babel 없는 빌드를 지원하기 위해 oxc와도 협력 중입니다.\n\n---\n\n[React 컴파일러](https://react.dev/learn/react-compiler)는 자동 메모이제이션을 통해 React 앱을 최적화할 수 있도록 도와주는 빌드 툴입니다. 지난해, React 컴파일러의 [첫 번째 베타](https://react.dev/blog/2024/10/21/react-compiler-beta-release)를 공개했고 많은 피드백과 기여를 받았습니다. 실제로 컴파일러를 도입한 사례들 ([Sanity Studio](https://github.com/reactwg/react-compiler/discussions/33), [Wakelet](https://github.com/reactwg/react-compiler/discussions/52))에서도 성과를 확인했습니다. 그리고 이제 안정화 출시를 향해 나아가고 있습니다.\n\n오늘 컴파일러의 첫 번째 RC (Release Candidate)를 공개합니다. RC는 컴파일러의 안정적이고 최종 버전에 가까운 상태로 프로덕션 환경에서 시도해 볼 수 있습니다.\n\n## 오늘 바로 React 컴파일러 RC 사용해보기 {/*use-react-compiler-rc-today*/}\n\nRC 설치 방법은 다음과 같습니다.\n\nnpm\n\n<TerminalBlock>\n  {`npm install --save-dev --save-exact babel-plugin-react-compiler@rc`}\n</TerminalBlock>\n\npnpm\n\n<TerminalBlock>\n  {`pnpm add --save-dev --save-exact babel-plugin-react-compiler@rc`}\n</TerminalBlock>\n\nyarn\n\n<TerminalBlock>\n  {`yarn add --dev --exact babel-plugin-react-compiler@rc`}\n</TerminalBlock>\n\nRC에서는 React 컴파일러를 프로젝트에 더 쉽게 추가할 수 있도록 개선했고, 메모이제이션을 생성하는 방식을 최적화했습니다. 이제 옵셔널 체이닝과 배열 인덱스를 의존성으로 지원합니다. 동등성 검사나 문자열 보간 같은 더 다양한 의존성 추론 방법도 연구 중입니다. 이런 개선사항들은 궁극적으로 리렌더링을 줄이고 더 반응성 높은 UI를 만드는 데 기여합니다.\n\n커뮤니티 피드백 중 하나는 ref 검증 (ref-in-render validation)에서 가끔 거짓 양성 (false positive)이 발생한다는 것이었습니다. 우리는 컴파일러의 에러 메시지와 힌트를 전적으로 신뢰할 수 있어야 한다는 철학을 지향하므로 이번 RC에서는 해당 검증을 기본적으로 비활성화했습니다. 이 검증 방식을 개선하기 위해 작업할 것이며 후속 릴리즈에서 다시 활성화할 예정입니다.\n\n자세한 컴파일러 사용법은 [문서](https://react.dev/learn/react-compiler)에서 확인할 수 있습니다.\n\n## 피드백에 관해 {/*feedback*/}\n\nRC 기간 동안 React 사용자들이 컴파일러를 사용해보시고 React 레포지토리에 피드백을 제공해 주시길 바랍니다. 버그나 예상치 못한 동작을 발견하면 [이슈](https://github.com/facebook/react/issues)를 등록해 주세요. 일반적인 질문이나 제안이 있다면 [React Compiler Working Group](https://github.com/reactwg/react-compiler/discussions)에 남겨 주시면 됩니다.\n\n## 하위 호환성 {/*backwards-compatibility*/}\n\n베타 발표 때 언급했듯이 React 컴파일러는 React 17 이상에서 호환됩니다. 아직 React 19로 업데이트하지 않았다면 컴파일러 설정에서 최소 타겟을 지정하고 `react-compiler-runtime`을 의존성에 추가하면 React 컴파일러를 사용할 수 있습니다. 자세한 방법은 [문서](https://react.dev/learn/react-compiler#using-react-compiler-with-react-17-or-18)에서 확인할 수 있습니다.\n\n## `eslint-plugin-react-compiler`에서 `eslint-plugin-react-hooks`로 마이그레이션 {/*migrating-from-eslint-plugin-react-compiler-to-eslint-plugin-react-hooks*/}\n\n이미 `eslint-plugin-react-compiler`를 설치했다면 제거하고 `eslint-plugin-react-hooks@6.0.0-rc.1`를 사용해주세요. 이 개선에 기여한 [@michaelfaith](https://bsky.app/profile/michael.faith)에게 감사드립니다!\n\n설치 방법은 다음과 같습니다.\n\nnpm\n\n<TerminalBlock>\n  {`npm install --save-dev eslint-plugin-react-hooks@6.0.0-rc.1`}\n</TerminalBlock>\n\npnpm\n\n<TerminalBlock>\n  {`pnpm add --save-dev eslint-plugin-react-hooks@6.0.0-rc.1`}\n</TerminalBlock>\n\nyarn\n\n<TerminalBlock>\n  {`yarn add --dev eslint-plugin-react-hooks@6.0.0-rc.1`}\n</TerminalBlock>\n\n```js\n// eslint.config.js\nimport * as reactHooks from 'eslint-plugin-react-hooks';\n\nexport default [\n  // Flat Config (eslint 9+)\n  reactHooks.configs.recommended,\n\n  // Legacy Config\n  reactHooks.configs['recommended-latest'],\n];\n```\n\nReact 컴파일러 규칙을 활성화하려면 ESLint 설정에 `'react-hooks/react-compiler': 'error'`를 추가해주세요.\n\n린터는 컴파일러 설치 여부와 관계 없으므로 `eslint-plugin-react-hooks`를 업그레이드하는 것은 리스크가 없습니다. 바로 업그레이드하는 것을 권장합니다.\n\n## swc 지원 (실험적 기능) {/*swc-support-experimental*/}\n\nReact 컴파일러는 Babel, Vite, Rsbuild 등 [여러 빌드 도구](/learn/react-compiler#installation)에서 사용할 수 있습니다.\n\n추가로 [swc](https://swc.rs/) 팀의 강동윤([@kdy1dev](https://x.com/kdy1dev))님과 협력하여 swc 플러그인 지원을 추가 중입니다. 아직 완성되진 않았지만 [Next.js 앱에서 React 컴파일러를 활성화](https://nextjs.org/docs/app/api-reference/config/next-config-js/reactCompiler)하면 눈에 띄게 빌드 성능이 개선됩니다.\n\n최고의 빌드 성능을 위해 Next.js [15.3.1](https://github.com/vercel/next.js/releases/tag/v15.3.1) 이상 버전 사용을 권장합니다.\n\nVite 사용자는 여전히 [vite-plugin-react](https://github.com/vitejs/vite-plugin-react)를 [Babel 플러그인](https://react.dev/learn/react-compiler#usage-with-vite)에 적용해 컴파일러를 활성화할 수 있습니다. 또한, [oxc](https://oxc.rs/) 팀과도 협력해 [컴파일러 추가 지원](https://github.com/oxc-project/oxc/issues/10048) 예정입니다. [rolldown](https://github.com/rolldown/rolldown)이 공식 릴리즈되고 Vite와 oxc 지원이 완료되면 마이그레이션 방법을 문서에 추가할 계획입니다.\n\n## React 컴파일러 업그레이드 방법 {/*upgrading-react-compiler*/}\n\nReact 컴파일러는 자동 메모이제이션 기능이 성능 최적화에 집중될 때 가장 효과적입니다. 향후 버전에서는 메모이제이션 방식이 변경될 수 있습니다, 예를 들어 더 정교하고 세밀하게 말이죠.\n\n하지만 실제 제품 코드가 항상 정적으로 탐지 가능한 방식으로 작성되지 않습니다. JavaScript 특성상 코드가 [React의 규칙](https://react.dev/reference/rules)을 위반하는 경우도 있고 이가 빌드 시점에 드러나지 않을 수도 있습니다. 이런 이유로 메모이제이션 방식이 바뀌면 의도치 않은 결과가 발생할 수 있습니다.\n예를 들어 어떤 값이 컴파일러에 의해 메모이제이션된 상태로 사용되고 있었는데 해당 값이 컴포넌트 트리 어딘가에서 `useEffect` 의존성으로도 쓰이고 있다고 가정해봅시다. 이 때 그 값의 메모이제이션 방식이 달라지거나 더 이상 메모이제이션되지 않게 되면, `useEffect`가 과도하게 실행되거나 (over-fire) 혹은 필요한 상황에서 실행되지 않는 (under-fire) 문제가 생길 수 있습니다.\n기본적으로 [useEffect를 동기화 목적](https://react.dev/learn/synchronizing-with-effects)으로만 쓰길 권장하지만 실제 코드베이스에서는 특정 값이 변할 때만 실행되어야 하는 효과처럼 다른 용도로 `useEffect`가 사용되기도 합니다. 따라서 메모이제이션 변경이 이런 코드들에 영향을 줄 수 있습니다.\n\n메모이제이션 방식 변경은 드물지만 예기치 못한 동작을 유발할 수 있습니다. 따라서 React의 규칙을 지키고 지속적인 E2E 테스트를 수행하는 것이 중요합니다. 그래야 컴파일러를 안심하고 업그레이드할 수 있고 React 규칙 위반 문제를 발견할 수 있습니다.\n\n만약 충분한 테스트 커버리지가 없다면 컴파일러를 SemVer 범위(예: `^19.1.0`)로 지정하기보다 특정 버전(예: `19.1.0`)으로 고정하는 것을 권장합니다. npm이나 pnpm을 쓸 경우 `--save-exact`, yarn을 쓸 경우 `--exact` 플래그를 사용하면 됩니다. 이후 컴파일러 업그레이드를 수동으로 진행하면서 앱이 예상하는대로 동작하는지 반드시 확인하는 것이 좋습니다.\n\n## 안정화까지의 로드맵 {/*roadmap-to-stable*/}\n*이 로드맵은 최종 확정된 것이 아니며 변경될 수 있습니다.*\n\nRC에 대해 커뮤니티의 최종 피드백을 받은 뒤 컴파일러의 안정화 (Stable) 버전을 공개할 계획입니다.\n\n- ✅ 실험 단계 (Experimental): React Conf 2024에서 공개. 애플리케이션 개발자들의 피드백을 받기 위함.\n- ✅ 공개 베타 (Public Beta): 오늘부터 사용 가능. 주로 라이브러리 제작자들의 피드백을 받기 위함.\n- ✅ 릴리즈 후보 (RC): React 규칙을 따르는 대부분의 앱과 라이브러리에서 문제없이 동작.\n- 안정화 (General Availability): 커뮤니티의 최종 피드백 수집 후 공개 예정.\n\n안정화 이후에는 컴파일러 최적화와 개선을 이어갈 계획입니다. 여기에는 자동 메모이제이션의 점진적 개선 뿐만 아니라 제품 코드를 거의 수정하지 않고도 성능을 향상시킬 수 있는 새로운 최적화 기법들이 추가될 예정입니다. 각 업그레이드는 앱 성능을 꾸준히 개선하고, 더 다양한 JavaScript와 React 패턴을 다룰 수 있게 될 것입니다.\n\n---\n\n이 글을 검토하고 다듬어주신 [Joe Savona](https://x.com/en_JS), [Jason Bonta](https://x.com/someextent), [Jimmy Lai](https://x.com/feedthejim), and [강동윤](https://x.com/kdy1dev) (@kdy1dev)께 감사드립니다.\n"
  },
  {
    "path": "src/content/blog/2025/04/23/react-labs-view-transitions-activity-and-more.md",
    "content": "---\ntitle: \"React Labs: View Transitions, Activity 그리고 그 외\"\nauthor: Ricky Hanlon\ndate: 2025/04/23\ndescription: React Labs 게시글에는 활발히 연구 개발 중인 프로젝트에 대한 내용을 작성합니다. 이번 게시글에서는 오늘 바로 사용해볼 수 있는 두 가지 새로운 실험적 기능과 현재 작업 중인 다른 영역의 업데이트를 공유합니다.\n---\n\n2025년 4월 23일, [Ricky Hanlon](https://twitter.com/rickhanlonii)\n\n---\n\n<Intro>\n\nReact Labs 게시글에는 활발히 연구 개발 중인 프로젝트에 대한 내용을 작성합니다. 이번 게시글에서는 오늘 바로 사용해볼 수 있는 두 가지 새로운 실험적 기능과 현재 작업 중인 다른 영역의 업데이트를 공유합니다.\n\n</Intro>\n\n\n오늘 저희는 테스트할 준비가 완료된 두 가지 새로운 실험적 기능에 대한 문서를 공개하게 되어 기쁩니다.\n\n- [View Transitions](#view-transitions)\n- [Activity](#activity)\n\n또한 현재 개발 중인 새로운 기능들에 대한 업데이트도 공유합니다.\n- [React Performance Tracks](#react-performance-tracks)\n- [Compiler IDE Extension](#compiler-ide-extension)\n- [Automatic Effect Dependencies](#automatic-effect-dependencies)\n- [Fragment Refs](#fragment-refs)\n- [Concurrent Stores](#concurrent-stores)\n\n---\n\n# 새로운 실험적 기능 {/*new-experimental-features*/}\n\n<Note>\n\n`<Activity />`는 `react@19.2`에 포함되어 출시되었습니다.\n\n`<ViewTransition />`과 `addTransitionType`은 이제 `react@canary`에서 사용할 수 있습니다.\n\n</Note>\n\nView Transitions와 Activity는 이제 `react@experimental`에서 테스트할 준비가 되었습니다. 이러한 기능들은 프로덕션에서 테스트되었으며 안정적이지만, 피드백을 반영하는 과정에서 최종 API가 여전히 변경될 수 있습니다.\n\n가장 최신 실험적 버전으로 React 패키지를 업그레이드하여 사용해볼 수 있습니다.\n\n- `react@experimental`\n- `react-dom@experimental`\n\n앱에서 이러한 기능을 사용하는 방법을 알아보려면 계속 읽어보시거나, 새로 공개된 문서를 확인해보세요.\n\n- [`<ViewTransition>`](/reference/react/ViewTransition): Transition에 애니메이션을 활성화할 수 있는 컴포넌트입니다.\n- [`addTransitionType`](/reference/react/addTransitionType): Transition의 원인을 지정할 수 있는 함수입니다.\n- [`<Activity>`](/reference/react/Activity): UI의 일부를 숨기거나 보여줄 수 있는 컴포넌트입니다.\n\n## View Transitions {/*view-transitions*/}\n\nReact View Transitions는 앱의 UI 전환에 애니메이션을 더 쉽게 추가할 수 있게 해주는 새로운 실험적 기능입니다. 내부적으로, 이러한 애니메이션은 대부분의 최신 브라우저에서 사용할 수 있는 새로운 [`startViewTransition`](https://developer.mozilla.org/en-US/docs/Web/API/Document/startViewTransition) API를 사용합니다.\n\n엘리먼트의 애니메이션을 활성화하려면, 새로운 `<ViewTransition>` 컴포넌트로 감싸주세요.\n\n```js\n// 애니메이션할 \"대상\"\n<ViewTransition>\n  <div>animate me</div>\n</ViewTransition>\n```\n\n이 새로운 컴포넌트를 사용하면 애니메이션이 활성화될 때 무엇을 애니메이션할지 선언적으로 정의할 수 있습니다.\n\nView Transition에 대한 다음 세 가지 트리거 중 하나를 사용해서 \"언제\" 애니메이션할지 정의할 수 있습니다.\n\n```js\n// 애니메이션할 \"시점\"\n\n// Transitions\nstartTransition(() => setState(...));\n\n// Deferred Values\nconst deferred = useDeferredValue(value);\n\n// Suspense\n<Suspense fallback={<Fallback />}>\n  <div>Loading...</div>\n</Suspense>\n```\n\n기본적으로, 이러한 애니메이션은 [View Transitions의 기본 CSS 애니메이션](https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API/Using#customizing_your_animations)이 적용됩니다 (일반적으로 부드러운 크로스 페이드). [view transition 의사 선택자](https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API/Using#the_view_transition_pseudo-element_tree)를 사용해서 애니메이션이 \"어떻게\" 실행될지 정의할 수 있습니다. 예를 들어, `*`를 사용해서 모든 전환에 대한 기본 애니메이션을 변경할 수 있습니다.\n\n```\n// 애니메이션하는 \"방법\"\n::view-transition-old(*) {\n  animation: 300ms ease-out fade-out;\n}\n::view-transition-new(*) {\n  animation: 300ms ease-in fade-in;\n}\n```\n\n`startTransition`, `useDeferredValue`, 또는 `Suspense` 폴백이 콘텐츠로 전환되는 것과 같은 애니메이션 트리거로 인해 DOM이 업데이트되면, React는 [선언적 휴리스틱](/reference/react/ViewTransition#viewtransition)을 사용해서 애니메이션을 위해 활성화할 `<ViewTransition>` 컴포넌트를 자동으로 결정합니다. 그러면 브라우저가 CSS에서 정의된 애니메이션을 실행합니다.\n\n브라우저의 View Transition API에 익숙하고 React가 이를 어떻게 지원하는지 알고 싶다면 문서의 [`<ViewTransition>`이 어떻게 동작하는지](/reference/react/ViewTransition#how-does-viewtransition-work)를 참고하세요.\n\n이 글에서는 View Transition을 사용하는 몇 가지 예시를 살펴봅니다.\n\n다음과 같은 상호작용을 애니메이션하지 않는 앱부터 시작하겠습니다.\n- 비디오를 클릭해서 세부 정보를 봅니다.\n- \"back\"을 클릭해서 피드로 돌아갑니다.\n- 목록에서 타이핑해서 비디오를 필터링합니다.\n\n<Sandpack>\n\n```js src/App.js active\nimport TalkDetails from './Details'; import Home from './Home'; import {useRouter} from './router';\n\nexport default function App() {\n  const {url} = useRouter();\n\n  // 🚩 이 버전에는 아직 애니메이션이 포함되어 있지 않습니다.\n  return url === '/' ? <Home /> : <TalkDetails />;\n}\n```\n\n```js src/Details.js\nimport { fetchVideo, fetchVideoDetails } from \"./data\";\nimport { Thumbnail, VideoControls } from \"./Videos\";\nimport { useRouter } from \"./router\";\nimport Layout from \"./Layout\";\nimport { use, Suspense } from \"react\";\nimport { ChevronLeft } from \"./Icons\";\n\nfunction VideoInfo({ id }) {\n  const details = use(fetchVideoDetails(id));\n  return (\n    <>\n      <p className=\"info-title\">{details.title}</p>\n      <p className=\"info-description\">{details.description}</p>\n    </>\n  );\n}\n\nfunction VideoInfoFallback() {\n  return (\n    <>\n      <div className=\"fallback title\"></div>\n      <div className=\"fallback description\"></div>\n    </>\n  );\n}\n\nexport default function Details() {\n  const { url, navigateBack } = useRouter();\n  const videoId = url.split(\"/\").pop();\n  const video = use(fetchVideo(videoId));\n\n  return (\n    <Layout\n      heading={\n        <div\n          className=\"fit back\"\n          onClick={() => {\n            navigateBack(\"/\");\n          }}\n        >\n          <ChevronLeft /> Back\n        </div>\n      }\n    >\n      <div className=\"details\">\n        <Thumbnail video={video} large>\n          <VideoControls />\n        </Thumbnail>\n        <Suspense fallback={<VideoInfoFallback />}>\n          <VideoInfo id={video.id} />\n        </Suspense>\n      </div>\n    </Layout>\n  );\n}\n\n```\n\n```js src/Home.js\nimport { Video } from \"./Videos\";\nimport Layout from \"./Layout\";\nimport { fetchVideos } from \"./data\";\nimport { useId, useState, use } from \"react\";\nimport { IconSearch } from \"./Icons\";\n\nfunction SearchInput({ value, onChange }) {\n  const id = useId();\n  return (\n    <form className=\"search\" onSubmit={(e) => e.preventDefault()}>\n      <label htmlFor={id} className=\"sr-only\">\n        Search\n      </label>\n      <div className=\"search-input\">\n        <div className=\"search-icon\">\n          <IconSearch />\n        </div>\n        <input\n          type=\"text\"\n          id={id}\n          placeholder=\"Search\"\n          value={value}\n          onChange={(e) => onChange(e.target.value)}\n        />\n      </div>\n    </form>\n  );\n}\n\nfunction filterVideos(videos, query) {\n  const keywords = query\n    .toLowerCase()\n    .split(\" \")\n    .filter((s) => s !== \"\");\n  if (keywords.length === 0) {\n    return videos;\n  }\n  return videos.filter((video) => {\n    const words = (video.title + \" \" + video.description)\n      .toLowerCase()\n      .split(\" \");\n    return keywords.every((kw) => words.some((w) => w.includes(kw)));\n  });\n}\n\nexport default function Home() {\n  const videos = use(fetchVideos());\n  const count = videos.length;\n  const [searchText, setSearchText] = useState(\"\");\n  const foundVideos = filterVideos(videos, searchText);\n  return (\n    <Layout heading={<div className=\"fit\">{count} Videos</div>}>\n      <SearchInput value={searchText} onChange={setSearchText} />\n      <div className=\"video-list\">\n        {foundVideos.length === 0 && (\n          <div className=\"no-results\">No results</div>\n        )}\n        <div className=\"videos\">\n          {foundVideos.map((video) => (\n            <Video key={video.id} video={video} />\n          ))}\n        </div>\n      </div>\n    </Layout>\n  );\n}\n\n```\n\n```js src/Icons.js\nexport function ChevronLeft() {\n  return (\n    <svg\n      className=\"chevron-left\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width=\"20\"\n      height=\"20\"\n      viewBox=\"0 0 20 20\">\n      <g fill=\"none\" fillRule=\"evenodd\" transform=\"translate(-446 -398)\">\n        <path\n          fill=\"currentColor\"\n          fillRule=\"nonzero\"\n          d=\"M95.8838835,240.366117 C95.3957281,239.877961 94.6042719,239.877961 94.1161165,240.366117 C93.6279612,240.854272 93.6279612,241.645728 94.1161165,242.133883 L98.6161165,246.633883 C99.1042719,247.122039 99.8957281,247.122039 100.383883,246.633883 L104.883883,242.133883 C105.372039,241.645728 105.372039,240.854272 104.883883,240.366117 C104.395728,239.877961 103.604272,239.877961 103.116117,240.366117 L99.5,243.982233 L95.8838835,240.366117 Z\"\n          transform=\"translate(356.5 164.5)\"\n        />\n        <polygon points=\"446 418 466 418 466 398 446 398\" />\n      </g>\n    </svg>\n  );\n}\n\nexport function PauseIcon() {\n  return (\n    <svg\n      className=\"control-icon\"\n      style={{padding: '4px'}}\n      width=\"100\"\n      height=\"100\"\n      viewBox=\"0 0 512 512\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\">\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M256 0C114.617 0 0 114.615 0 256s114.617 256 256 256 256-114.615 256-256S397.383 0 256 0zm-32 320c0 8.836-7.164 16-16 16h-32c-8.836 0-16-7.164-16-16V192c0-8.836 7.164-16 16-16h32c8.836 0 16 7.164 16 16v128zm128 0c0 8.836-7.164 16-16 16h-32c-8.836 0-16-7.164-16-16V192c0-8.836 7.164-16 16-16h32c8.836 0 16 7.164 16 16v128z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  );\n}\n\nexport function PlayIcon() {\n  return (\n    <svg\n      className=\"control-icon\"\n      width=\"100\"\n      height=\"100\"\n      viewBox=\"0 0 72 72\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\">\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M36 69C54.2254 69 69 54.2254 69 36C69 17.7746 54.2254 3 36 3C17.7746 3 3 17.7746 3 36C3 54.2254 17.7746 69 36 69ZM52.1716 38.6337L28.4366 51.5801C26.4374 52.6705 24 51.2235 24 48.9464V23.0536C24 20.7764 26.4374 19.3295 28.4366 20.4199L52.1716 33.3663C54.2562 34.5034 54.2562 37.4966 52.1716 38.6337Z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  );\n}\nexport function Heart({liked, animate}) {\n  return (\n    <>\n      <svg\n        className=\"absolute overflow-visible\"\n        viewBox=\"0 0 24 24\"\n        fill=\"none\"\n        xmlns=\"http://www.w3.org/2000/svg\">\n        <circle\n          className={`circle ${liked ? 'liked' : ''} ${animate ? 'animate' : ''}`}\n          cx=\"12\"\n          cy=\"12\"\n          r=\"11.5\"\n          fill=\"transparent\"\n          strokeWidth=\"0\"\n          stroke=\"currentColor\"\n        />\n      </svg>\n\n      <svg\n        className={`heart ${liked ? 'liked' : ''} ${animate ? 'animate' : ''}`}\n        viewBox=\"0 0 24 24\"\n        fill=\"none\"\n        xmlns=\"http://www.w3.org/2000/svg\">\n        {liked ? (\n          <path\n            d=\"M12 23a.496.496 0 0 1-.26-.074C7.023 19.973 0 13.743 0 8.68c0-4.12 2.322-6.677 6.058-6.677 2.572 0 5.108 2.387 5.134 2.41l.808.771.808-.771C12.834 4.387 15.367 2 17.935 2 21.678 2 24 4.558 24 8.677c0 5.06-7.022 11.293-11.74 14.246a.496.496 0 0 1-.26.074V23z\"\n            fill=\"currentColor\"\n          />\n        ) : (\n          <path\n            fillRule=\"evenodd\"\n            clipRule=\"evenodd\"\n            d=\"m12 5.184-.808-.771-.004-.004C11.065 4.299 8.522 2.003 6 2.003c-3.736 0-6 2.558-6 6.677 0 4.47 5.471 9.848 10 13.079.602.43 1.187.82 1.74 1.167A.497.497 0 0 0 12 23v-.003c.09 0 .182-.026.26-.074C16.977 19.97 24 13.737 24 8.677 24 4.557 21.743 2 18 2c-2.569 0-5.166 2.387-5.192 2.413L12 5.184zm-.002 15.525c2.071-1.388 4.477-3.342 6.427-5.47C20.72 12.733 22 10.401 22 8.677c0-1.708-.466-2.855-1.087-3.55C20.316 4.459 19.392 4 18 4c-.726 0-1.63.364-2.5.9-.67.412-1.148.82-1.266.92-.03.025-.037.031-.019.014l-.013.013L12 7.949 9.832 5.88a10.08 10.08 0 0 0-1.33-.977C7.633 4.367 6.728 4.003 6 4.003c-1.388 0-2.312.459-2.91 1.128C2.466 5.826 2 6.974 2 8.68c0 1.726 1.28 4.058 3.575 6.563 1.948 2.127 4.352 4.078 6.423 5.466z\"\n            fill=\"currentColor\"\n          />\n        )}\n      </svg>\n    </>\n  );\n}\n\nexport function IconSearch(props) {\n  return (\n    <svg width=\"1em\" height=\"1em\" viewBox=\"0 0 20 20\">\n      <path\n        d=\"M14.386 14.386l4.0877 4.0877-4.0877-4.0877c-2.9418 2.9419-7.7115 2.9419-10.6533 0-2.9419-2.9418-2.9419-7.7115 0-10.6533 2.9418-2.9419 7.7115-2.9419 10.6533 0 2.9419 2.9418 2.9419 7.7115 0 10.6533z\"\n        stroke=\"currentColor\"\n        fill=\"none\"\n        strokeWidth=\"2\"\n        fillRule=\"evenodd\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"></path>\n    </svg>\n  );\n}\n```\n\n```js src/Layout.js\nimport { useIsNavPending } from \"./router\";\n\nexport default function Page({ heading, children }) {\n  const isPending = useIsNavPending();\n  return (\n    <div className=\"page\">\n      <div className=\"top\">\n        <div className=\"top-nav\">\n          {heading}\n          {isPending && <span className=\"loader\"></span>}\n        </div>\n      </div>\n\n      <div className=\"bottom\">\n        <div className=\"content\">{children}</div>\n      </div>\n    </div>\n  );\n}\n```\n\n```js src/LikeButton.js\nimport {useState} from 'react';\nimport {Heart} from './Icons';\n\n// A hack since we don't actually have a backend.\n// Unlike local state, this survives videos being filtered.\nconst likedVideos = new Set();\n\nexport default function LikeButton({video}) {\n  const [isLiked, setIsLiked] = useState(() => likedVideos.has(video.id));\n  const [animate, setAnimate] = useState(false);\n  return (\n    <button\n      className={`like-button ${isLiked && 'liked'}`}\n      aria-label={isLiked ? 'Unsave' : 'Save'}\n      onClick={() => {\n        const nextIsLiked = !isLiked;\n        if (nextIsLiked) {\n          likedVideos.add(video.id);\n        } else {\n          likedVideos.delete(video.id);\n        }\n        setAnimate(true);\n        setIsLiked(nextIsLiked);\n      }}>\n      <Heart liked={isLiked} animate={animate} />\n    </button>\n  );\n}\n```\n\n```js src/Videos.js\nimport { useState } from \"react\";\nimport LikeButton from \"./LikeButton\";\nimport { useRouter } from \"./router\";\nimport { PauseIcon, PlayIcon } from \"./Icons\";\nimport { startTransition } from \"react\";\n\nexport function VideoControls() {\n  const [isPlaying, setIsPlaying] = useState(false);\n\n  return (\n    <span\n      className=\"controls\"\n      onClick={() =>\n        startTransition(() => {\n          setIsPlaying((p) => !p);\n        })\n      }\n    >\n      {isPlaying ? <PauseIcon /> : <PlayIcon />}\n    </span>\n  );\n}\n\nexport function Thumbnail({ video, children }) {\n  return (\n    <div\n      aria-hidden=\"true\"\n      tabIndex={-1}\n      className={`thumbnail ${video.image}`}\n    >\n      {children}\n    </div>\n  );\n}\n\nexport function Video({ video }) {\n  const { navigate } = useRouter();\n\n  return (\n    <div className=\"video\">\n      <div\n        className=\"link\"\n        onClick={(e) => {\n          e.preventDefault();\n          navigate(`/video/${video.id}`);\n        }}\n      >\n        <Thumbnail video={video}></Thumbnail>\n\n        <div className=\"info\">\n          <div className=\"video-title\">{video.title}</div>\n          <div className=\"video-description\">{video.description}</div>\n        </div>\n      </div>\n      <LikeButton video={video} />\n    </div>\n  );\n}\n```\n\n\n```js src/data.js hidden\nconst videos = [\n  {\n    id: '1',\n    title: 'First video',\n    description: 'Video description',\n    image: 'blue',\n  },\n  {\n    id: '2',\n    title: 'Second video',\n    description: 'Video description',\n    image: 'red',\n  },\n  {\n    id: '3',\n    title: 'Third video',\n    description: 'Video description',\n    image: 'green',\n  },\n  {\n    id: '4',\n    title: 'Fourth video',\n    description: 'Video description',\n    image: 'purple',\n  },\n  {\n    id: '5',\n    title: 'Fifth video',\n    description: 'Video description',\n    image: 'yellow',\n  },\n  {\n    id: '6',\n    title: 'Sixth video',\n    description: 'Video description',\n    image: 'gray',\n  },\n];\n\nlet videosCache = new Map();\nlet videoCache = new Map();\nlet videoDetailsCache = new Map();\nconst VIDEO_DELAY = 1;\nconst VIDEO_DETAILS_DELAY = 1000;\nexport function fetchVideos() {\n  if (videosCache.has(0)) {\n    return videosCache.get(0);\n  }\n  const promise = new Promise((resolve) => {\n    setTimeout(() => {\n      resolve(videos);\n    }, VIDEO_DELAY);\n  });\n  videosCache.set(0, promise);\n  return promise;\n}\n\nexport function fetchVideo(id) {\n  if (videoCache.has(id)) {\n    return videoCache.get(id);\n  }\n  const promise = new Promise((resolve) => {\n    setTimeout(() => {\n      resolve(videos.find((video) => video.id === id));\n    }, VIDEO_DELAY);\n  });\n  videoCache.set(id, promise);\n  return promise;\n}\n\nexport function fetchVideoDetails(id) {\n  if (videoDetailsCache.has(id)) {\n    return videoDetailsCache.get(id);\n  }\n  const promise = new Promise((resolve) => {\n    setTimeout(() => {\n      resolve(videos.find((video) => video.id === id));\n    }, VIDEO_DETAILS_DELAY);\n  });\n  videoDetailsCache.set(id, promise);\n  return promise;\n}\n```\n\n```js src/router.js\nimport {\n  useState,\n  createContext,\n  use,\n  useTransition,\n  useLayoutEffect,\n  useEffect,\n} from \"react\";\n\nconst RouterContext = createContext({ url: \"/\", params: {} });\n\nexport function useRouter() {\n  return use(RouterContext);\n}\n\nexport function useIsNavPending() {\n  return use(RouterContext).isPending;\n}\n\nexport function Router({ children }) {\n  const [routerState, setRouterState] = useState({\n    pendingNav: () => {},\n    url: document.location.pathname,\n  });\n  const [isPending, startTransition] = useTransition();\n\n  function go(url) {\n    setRouterState({\n      url,\n      pendingNav() {\n        window.history.pushState({}, \"\", url);\n      },\n    });\n  }\n  function navigate(url) {\n    // Update router state in transition.\n    startTransition(() => {\n      go(url);\n    });\n  }\n\n  function navigateBack(url) {\n    // Update router state in transition.\n    startTransition(() => {\n      go(url);\n    });\n  }\n\n  useEffect(() => {\n    function handlePopState() {\n      // 복원은 동기적으로 이루어져야 하므로 애니메이션되면 안 됩니다.\n      // 트랜지션이더라도 마찬가지입니다.\n      startTransition(() => {\n        setRouterState({\n          url: document.location.pathname + document.location.search,\n          pendingNav() {\n            // 아무 작업도 하지 않습니다. URL은 이미 업데이트되었습니다.\n          },\n        });\n      });\n    }\n    window.addEventListener(\"popstate\", handlePopState);\n    return () => {\n      window.removeEventListener(\"popstate\", handlePopState);\n    };\n  }, []);\n  const pendingNav = routerState.pendingNav;\n  useLayoutEffect(() => {\n    pendingNav();\n  }, [pendingNav]);\n\n  return (\n    <RouterContext\n      value={{\n        url: routerState.url,\n        navigate,\n        navigateBack,\n        isPending,\n        params: {},\n      }}\n    >\n      {children}\n    </RouterContext>\n  );\n}\n```\n\n```css src/styles.css\n@font-face {\n  font-family: Optimistic Text;\n  src: url(https://react.dev/fonts/Optimistic_Text_W_Rg.woff2) format(\"woff2\");\n  font-weight: 400;\n  font-style: normal;\n  font-display: swap;\n}\n\n@font-face {\n  font-family: Optimistic Text;\n  src: url(https://react.dev/fonts/Optimistic_Text_W_Md.woff2) format(\"woff2\");\n  font-weight: 500;\n  font-style: normal;\n  font-display: swap;\n}\n\n@font-face {\n  font-family: Optimistic Text;\n  src: url(https://react.dev/fonts/Optimistic_Text_W_Bd.woff2) format(\"woff2\");\n  font-weight: 600;\n  font-style: normal;\n  font-display: swap;\n}\n\n@font-face {\n  font-family: Optimistic Text;\n  src: url(https://react.dev/fonts/Optimistic_Text_W_Bd.woff2) format(\"woff2\");\n  font-weight: 700;\n  font-style: normal;\n  font-display: swap;\n}\n\n* {\n  box-sizing: border-box;\n}\n\nhtml {\n  background-image: url(https://react.dev/images/meta-gradient-dark.png);\n  background-size: 100%;\n  background-position: -100%;\n  background-color: rgb(64 71 86);\n  background-repeat: no-repeat;\n  height: 100%;\n  width: 100%;\n}\n\nbody {\n  font-family: Optimistic Text, -apple-system, ui-sans-serif, system-ui, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;\n  padding: 10px 0 10px 0;\n  margin: 0;\n  display: flex;\n  justify-content: center;\n}\n\n#root {\n  flex: 1 1;\n  height: auto;\n  background-color: #fff;\n  border-radius: 10px;\n  max-width: 450px;\n  min-height: 600px;\n  padding-bottom: 10px;\n}\n\nh1 {\n  margin-top: 0;\n  font-size: 22px;\n}\n\nh2 {\n  margin-top: 0;\n  font-size: 20px;\n}\n\nh3 {\n  margin-top: 0;\n  font-size: 18px;\n}\n\nh4 {\n  margin-top: 0;\n  font-size: 16px;\n}\n\nh5 {\n  margin-top: 0;\n  font-size: 14px;\n}\n\nh6 {\n  margin-top: 0;\n  font-size: 12px;\n}\n\ncode {\n  font-size: 1.2em;\n}\n\nul {\n  padding-inline-start: 20px;\n}\n\n.sr-only {\n  position: absolute;\n  width: 1px;\n  height: 1px;\n  padding: 0;\n  margin: -1px;\n  overflow: hidden;\n  clip: rect(0, 0, 0, 0);\n  white-space: nowrap;\n  border-width: 0;\n}\n\n.absolute {\n  position: absolute;\n}\n\n.overflow-visible {\n  overflow: visible;\n}\n\n.visible {\n  overflow: visible;\n}\n\n.fit {\n  width: fit-content;\n}\n\n\n/* Layout */\n.page {\n  display: flex;\n  flex-direction: column;\n  height: 100%;\n}\n\n.top-hero {\n  height: 200px;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  background-image: conic-gradient(\n      from 90deg at -10% 100%,\n      #2b303b 0deg,\n      #2b303b 90deg,\n      #16181d 1turn\n  );\n}\n\n.bottom {\n  flex: 1;\n  overflow: auto;\n}\n\n.top-nav {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  margin-bottom: 0;\n  padding: 0 12px;\n  top: 0;\n  width: 100%;\n  height: 44px;\n  color: #23272f;\n  font-weight: 700;\n  font-size: 20px;\n  z-index: 100;\n  cursor: default;\n}\n\n.content {\n  padding: 0 12px;\n  margin-top: 4px;\n}\n\n\n.loader {\n  color: #23272f;\n  font-size: 3px;\n  width: 1em;\n  margin-right: 18px;\n  height: 1em;\n  border-radius: 50%;\n  position: relative;\n  text-indent: -9999em;\n  animation: loading-spinner 1.3s infinite linear;\n  animation-delay: 200ms;\n  transform: translateZ(0);\n}\n\n@keyframes loading-spinner {\n  0%,\n  100% {\n    box-shadow: 0 -3em 0 0.2em,\n    2em -2em 0 0em, 3em 0 0 -1em,\n    2em 2em 0 -1em, 0 3em 0 -1em,\n    -2em 2em 0 -1em, -3em 0 0 -1em,\n    -2em -2em 0 0;\n  }\n  12.5% {\n    box-shadow: 0 -3em 0 0, 2em -2em 0 0.2em,\n    3em 0 0 0, 2em 2em 0 -1em, 0 3em 0 -1em,\n    -2em 2em 0 -1em, -3em 0 0 -1em,\n    -2em -2em 0 -1em;\n  }\n  25% {\n    box-shadow: 0 -3em 0 -0.5em,\n    2em -2em 0 0, 3em 0 0 0.2em,\n    2em 2em 0 0, 0 3em 0 -1em,\n    -2em 2em 0 -1em, -3em 0 0 -1em,\n    -2em -2em 0 -1em;\n  }\n  37.5% {\n    box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em,\n    3em 0em 0 0, 2em 2em 0 0.2em, 0 3em 0 0em,\n    -2em 2em 0 -1em, -3em 0em 0 -1em, -2em -2em 0 -1em;\n  }\n  50% {\n    box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em,\n    3em 0 0 -1em, 2em 2em 0 0em, 0 3em 0 0.2em,\n    -2em 2em 0 0, -3em 0em 0 -1em, -2em -2em 0 -1em;\n  }\n  62.5% {\n    box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em,\n    3em 0 0 -1em, 2em 2em 0 -1em, 0 3em 0 0,\n    -2em 2em 0 0.2em, -3em 0 0 0, -2em -2em 0 -1em;\n  }\n  75% {\n    box-shadow: 0em -3em 0 -1em, 2em -2em 0 -1em,\n    3em 0em 0 -1em, 2em 2em 0 -1em, 0 3em 0 -1em,\n    -2em 2em 0 0, -3em 0em 0 0.2em, -2em -2em 0 0;\n  }\n  87.5% {\n    box-shadow: 0em -3em 0 0, 2em -2em 0 -1em,\n    3em 0 0 -1em, 2em 2em 0 -1em, 0 3em 0 -1em,\n    -2em 2em 0 0, -3em 0em 0 0, -2em -2em 0 0.2em;\n  }\n}\n\n/* LikeButton */\n.like-button {\n  outline-offset: 2px;\n  position: relative;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  width: 2.5rem;\n  height: 2.5rem;\n  cursor: pointer;\n  border-radius: 9999px;\n  border: none;\n  outline: none 2px;\n  color: #5e687e;\n  background: none;\n}\n\n.like-button:focus {\n  color: #a6423a;\n  background-color: rgba(166, 66, 58, .05);\n}\n\n.like-button:active {\n  color: #a6423a;\n  background-color: rgba(166, 66, 58, .05);\n  transform: scaleX(0.95) scaleY(0.95);\n}\n\n.like-button:hover {\n  background-color: #f6f7f9;\n}\n\n.like-button.liked {\n  color: #a6423a;\n}\n\n/* Icons */\n@keyframes circle {\n  0% {\n    transform: scale(0);\n    stroke-width: 16px;\n  }\n\n  50% {\n    transform: scale(.5);\n    stroke-width: 16px;\n  }\n\n  to {\n    transform: scale(1);\n    stroke-width: 0;\n  }\n}\n\n.circle {\n  color: rgba(166, 66, 58, .5);\n  transform-origin: center;\n  transition-property: all;\n  transition-duration: .15s;\n  transition-timing-function: cubic-bezier(.4,0,.2,1);\n}\n\n.circle.liked.animate {\n  animation: circle .3s forwards;\n}\n\n.heart {\n  width: 1.5rem;\n  height: 1.5rem;\n}\n\n.heart.liked {\n  transform-origin: center;\n  transition-property: all;\n  transition-duration: .15s;\n  transition-timing-function: cubic-bezier(.4, 0, .2, 1);\n}\n\n.heart.liked.animate {\n  animation: scale .35s ease-in-out forwards;\n}\n\n.control-icon {\n  color: hsla(0, 0%, 100%, .5);\n  filter:  drop-shadow(0 20px 13px rgba(0, 0, 0, .03)) drop-shadow(0 8px 5px rgba(0, 0, 0, .08));\n}\n\n.chevron-left {\n  margin-top: 2px;\n  rotate: 90deg;\n}\n\n\n/* Video */\n.thumbnail {\n  position: relative;\n  aspect-ratio: 16 / 9;\n  display: flex;\n  overflow: hidden;\n  flex-direction: column;\n  justify-content: center;\n  align-items: center;\n  border-radius: 0.5rem;\n  outline-offset: 2px;\n  width: 8rem;\n  vertical-align: middle;\n  background-color: #ffffff;\n  background-size: cover;\n  user-select: none;\n}\n\n.thumbnail.blue {\n  background-image: conic-gradient(at top right, #c76a15, #087ea4, #2b3491);\n}\n\n.thumbnail.red {\n  background-image: conic-gradient(at top right, #c76a15, #a6423a, #2b3491);\n}\n\n.thumbnail.green {\n  background-image: conic-gradient(at top right, #c76a15, #388f7f, #2b3491);\n}\n\n.thumbnail.purple {\n  background-image: conic-gradient(at top right, #c76a15, #575fb7, #2b3491);\n}\n\n.thumbnail.yellow {\n  background-image: conic-gradient(at top right, #c76a15, #FABD62, #2b3491);\n}\n\n.thumbnail.gray {\n  background-image: conic-gradient(at top right, #c76a15, #4E5769, #2b3491);\n}\n\n.video {\n  display: flex;\n  flex-direction: row;\n  gap: 0.75rem;\n  align-items: center;\n}\n\n.video .link {\n  display: flex;\n  flex-direction: row;\n  flex: 1 1 0;\n  gap: 0.125rem;\n  outline-offset: 4px;\n  cursor: pointer;\n}\n\n.video .info {\n  display: flex;\n  flex-direction: column;\n  justify-content: center;\n  margin-left: 8px;\n  gap: 0.125rem;\n}\n\n.video .info:hover {\n  text-decoration: underline;\n}\n\n.video-title {\n  font-size: 15px;\n  line-height: 1.25;\n  font-weight: 700;\n  color: #23272f;\n}\n\n.video-description {\n  color: #5e687e;\n  font-size: 13px;\n}\n\n/* Details */\n.details .thumbnail {\n  position: relative;\n  aspect-ratio: 16 / 9;\n  display: flex;\n  overflow: hidden;\n  flex-direction: column;\n  justify-content: center;\n  align-items: center;\n  border-radius: 0.5rem;\n  outline-offset: 2px;\n  width: 100%;\n  vertical-align: middle;\n  background-color: #ffffff;\n  background-size: cover;\n  user-select: none;\n}\n\n.video-details-title {\n  margin-top: 8px;\n}\n\n.video-details-speaker {\n  display: flex;\n  gap: 8px;\n  margin-top: 10px\n}\n\n.back {\n  display: flex;\n  align-items: center;\n  margin-left: -5px;\n  cursor: pointer;\n}\n\n.back:hover {\n  text-decoration: underline;\n}\n\n.info-title {\n  font-size: 1.5rem;\n  font-weight: 700;\n  line-height: 1.25;\n  margin: 8px 0 0 0 ;\n}\n\n.info-description {\n  margin: 8px 0 0 0;\n}\n\n.controls {\n  cursor: pointer;\n}\n\n.fallback {\n  background: #f6f7f8 linear-gradient(to right, #e6e6e6 5%, #cccccc 25%, #e6e6e6 35%) no-repeat;\n  background-size: 800px 104px;\n  display: block;\n  line-height: 1.25;\n  margin: 8px 0 0 0;\n  border-radius: 5px;\n  overflow: hidden;\n\n  animation: 1s linear 1s infinite shimmer;\n  animation-delay: 300ms;\n  animation-duration: 1s;\n  animation-fill-mode: forwards;\n  animation-iteration-count: infinite;\n  animation-name: shimmer;\n  animation-timing-function: linear;\n}\n\n\n.fallback.title {\n  width: 130px;\n  height: 30px;\n\n}\n\n.fallback.description {\n  width: 150px;\n  height: 21px;\n}\n\n@keyframes shimmer {\n  0% {\n    background-position: -468px 0;\n  }\n\n  100% {\n    background-position: 468px 0;\n  }\n}\n\n.search {\n  margin-bottom: 10px;\n}\n.search-input {\n  width: 100%;\n  position: relative;\n}\n\n.search-icon {\n  position: absolute;\n  top: 0;\n  bottom: 0;\n  inset-inline-start: 0;\n  display: flex;\n  align-items: center;\n  padding-inline-start: 1rem;\n  pointer-events: none;\n  color: #99a1b3;\n}\n\n.search-input input {\n  display: flex;\n  padding-inline-start: 2.75rem;\n  padding-top: 10px;\n  padding-bottom: 10px;\n  width: 100%;\n  text-align: start;\n  background-color: rgb(235 236 240);\n  outline: 2px solid transparent;\n  cursor: pointer;\n  border: none;\n  align-items: center;\n  color: rgb(35 39 47);\n  border-radius: 9999px;\n  vertical-align: middle;\n  font-size: 15px;\n}\n\n.search-input input:hover, .search-input input:active {\n  background-color: rgb(235 236 240/ 0.8);\n  color: rgb(35 39 47/ 0.8);\n}\n\n/* Home */\n.video-list {\n  position: relative;\n}\n\n.video-list .videos {\n  display: flex;\n  flex-direction: column;\n  gap: 1rem;\n  overflow-y: auto;\n  height: 100%;\n}\n```\n\n```js src/index.js hidden\nimport React, {StrictMode} from 'react';\nimport {createRoot} from 'react-dom/client';\nimport './styles.css';\n\nimport App from './App';\nimport {Router} from './router';\n\nconst root = createRoot(document.getElementById('root'));\nroot.render(\n  <StrictMode>\n    <Router>\n      <App />\n    </Router>\n  </StrictMode>\n);\n```\n\n```json package.json hidden\n{\n  \"dependencies\": {\n    \"react\": \"canary\",\n    \"react-dom\": \"canary\",\n    \"react-scripts\": \"latest\"\n  },\n  \"scripts\": {\n    \"start\": \"react-scripts start\",\n    \"build\": \"react-scripts build\",\n    \"test\": \"react-scripts test --env=jsdom\",\n    \"eject\": \"react-scripts eject\"\n  }\n}\n```\n\n</Sandpack>\n\n<Note>\n\n#### View Transitions는 CSS와 JS 기반 애니메이션을 대체하지 않습니다 {/*view-transitions-do-not-replace-css-and-js-driven-animations*/}\n\nView Transitions는 네비게이션, 확장, 열기, 재정렬과 같은 UI 전환에 사용하기 위한 것입니다. 앱의 모든 애니메이션을 대체하기 위한 것은 아닙니다.\n\n위 예시 앱에서 \"like\" 버튼을 클릭할 때와 Suspense 폴백 반짝임에 이미 애니메이션이 있는 것을 확인할 수 있습니다. 이들은 특정 엘리먼트를 애니메이션하기 때문에 CSS 애니메이션의 좋은 사용 사례입니다.\n\n</Note>\n\n### 네비게이션 애니메이션 {/*animating-navigations*/}\n\n저희 앱에는 Suspense가 활성화된 라우터가 포함되어 있으며, [페이지 전환이 이미 Transitions로 표시되어 있습니다](/reference/react/useTransition#building-a-suspense-enabled-router). 이는 네비게이션이 `startTransition`으로 수행된다는 의미입니다.\n\n```js\nfunction navigate(url) {\n  startTransition(() => {\n    go(url);\n  });\n}\n```\n\n`startTransition`은 View Transition 트리거이므로, 페이지 간 애니메이션을 위해 `<ViewTransition>`을 추가할 수 있습니다:\n\n```js\n// 애니메이션할 \"대상\"\n<ViewTransition key={url}>\n  {url === '/' ? <Home /> : <TalkDetails />}\n</ViewTransition>\n```\n\n`url`이 변경되면, `<ViewTransition>`과 새로운 라우트가 렌더링됩니다. `<ViewTransition>`이 `startTransition` 내부에서 업데이트되었으므로, `<ViewTransition>`이 애니메이션을 위해 활성화됩니다.\n\n\n기본적으로 View Transitions에는 브라우저의 기본 크로스 페이드 애니메이션이 포함되어 있습니다. 이를 예시에 적용하면, 페이지 간을 이동할 때마다 크로스 페이드 애니메이션이 실행됩니다.\n\n<Sandpack>\n\n```js src/App.js active\nimport {ViewTransition} from 'react'; import Details from './Details';\nimport Home from './Home'; import {useRouter} from './router';\n\nexport default function App() {\n  const {url} = useRouter();\n\n  // ViewTransition을 사용해 페이지 간 전환을 애니메이션합니다.\n  // 기본값으로는 추가 CSS가 필요하지 않습니다.\n  return (\n    <ViewTransition>\n      {url === '/' ? <Home /> : <Details />}\n    </ViewTransition>\n  );\n}\n```\n\n```js src/Details.js hidden\nimport { fetchVideo, fetchVideoDetails } from \"./data\";\nimport { Thumbnail, VideoControls } from \"./Videos\";\nimport { useRouter } from \"./router\";\nimport Layout from \"./Layout\";\nimport { use, Suspense } from \"react\";\nimport { ChevronLeft } from \"./Icons\";\n\nfunction VideoInfo({ id }) {\n  const details = use(fetchVideoDetails(id));\n  return (\n    <>\n      <p className=\"info-title\">{details.title}</p>\n      <p className=\"info-description\">{details.description}</p>\n    </>\n  );\n}\n\nfunction VideoInfoFallback() {\n  return (\n    <>\n      <div className=\"fallback title\"></div>\n      <div className=\"fallback description\"></div>\n    </>\n  );\n}\n\nexport default function Details() {\n  const { url, navigateBack } = useRouter();\n  const videoId = url.split(\"/\").pop();\n  const video = use(fetchVideo(videoId));\n\n  return (\n    <Layout\n      heading={\n        <div\n          className=\"fit back\"\n          onClick={() => {\n            navigateBack(\"/\");\n          }}\n        >\n          <ChevronLeft /> Back\n        </div>\n      }\n    >\n      <div className=\"details\">\n        <Thumbnail video={video} large>\n          <VideoControls />\n        </Thumbnail>\n        <Suspense fallback={<VideoInfoFallback />}>\n          <VideoInfo id={video.id} />\n        </Suspense>\n      </div>\n    </Layout>\n  );\n}\n\n```\n\n```js src/Home.js hidden\nimport { Video } from \"./Videos\";\nimport Layout from \"./Layout\";\nimport { fetchVideos } from \"./data\";\nimport { useId, useState, use } from \"react\";\nimport { IconSearch } from \"./Icons\";\n\nfunction SearchInput({ value, onChange }) {\n  const id = useId();\n  return (\n    <form className=\"search\" onSubmit={(e) => e.preventDefault()}>\n      <label htmlFor={id} className=\"sr-only\">\n        Search\n      </label>\n      <div className=\"search-input\">\n        <div className=\"search-icon\">\n          <IconSearch />\n        </div>\n        <input\n          type=\"text\"\n          id={id}\n          placeholder=\"Search\"\n          value={value}\n          onChange={(e) => onChange(e.target.value)}\n        />\n      </div>\n    </form>\n  );\n}\n\nfunction filterVideos(videos, query) {\n  const keywords = query\n    .toLowerCase()\n    .split(\" \")\n    .filter((s) => s !== \"\");\n  if (keywords.length === 0) {\n    return videos;\n  }\n  return videos.filter((video) => {\n    const words = (video.title + \" \" + video.description)\n      .toLowerCase()\n      .split(\" \");\n    return keywords.every((kw) => words.some((w) => w.includes(kw)));\n  });\n}\n\nexport default function Home() {\n  const videos = use(fetchVideos());\n  const count = videos.length;\n  const [searchText, setSearchText] = useState(\"\");\n  const foundVideos = filterVideos(videos, searchText);\n  return (\n    <Layout heading={<div className=\"fit\">{count} Videos</div>}>\n      <SearchInput value={searchText} onChange={setSearchText} />\n      <div className=\"video-list\">\n        {foundVideos.length === 0 && (\n          <div className=\"no-results\">No results</div>\n        )}\n        <div className=\"videos\">\n          {foundVideos.map((video) => (\n            <Video key={video.id} video={video} />\n          ))}\n        </div>\n      </div>\n    </Layout>\n  );\n}\n\n```\n\n```js src/Icons.js hidden\nexport function ChevronLeft() {\n  return (\n    <svg\n      className=\"chevron-left\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width=\"20\"\n      height=\"20\"\n      viewBox=\"0 0 20 20\">\n      <g fill=\"none\" fillRule=\"evenodd\" transform=\"translate(-446 -398)\">\n        <path\n          fill=\"currentColor\"\n          fillRule=\"nonzero\"\n          d=\"M95.8838835,240.366117 C95.3957281,239.877961 94.6042719,239.877961 94.1161165,240.366117 C93.6279612,240.854272 93.6279612,241.645728 94.1161165,242.133883 L98.6161165,246.633883 C99.1042719,247.122039 99.8957281,247.122039 100.383883,246.633883 L104.883883,242.133883 C105.372039,241.645728 105.372039,240.854272 104.883883,240.366117 C104.395728,239.877961 103.604272,239.877961 103.116117,240.366117 L99.5,243.982233 L95.8838835,240.366117 Z\"\n          transform=\"translate(356.5 164.5)\"\n        />\n        <polygon points=\"446 418 466 418 466 398 446 398\" />\n      </g>\n    </svg>\n  );\n}\n\nexport function PauseIcon() {\n  return (\n    <svg\n      className=\"control-icon\"\n      style={{padding: '4px'}}\n      width=\"100\"\n      height=\"100\"\n      viewBox=\"0 0 512 512\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\">\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M256 0C114.617 0 0 114.615 0 256s114.617 256 256 256 256-114.615 256-256S397.383 0 256 0zm-32 320c0 8.836-7.164 16-16 16h-32c-8.836 0-16-7.164-16-16V192c0-8.836 7.164-16 16-16h32c8.836 0 16 7.164 16 16v128zm128 0c0 8.836-7.164 16-16 16h-32c-8.836 0-16-7.164-16-16V192c0-8.836 7.164-16 16-16h32c8.836 0 16 7.164 16 16v128z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  );\n}\n\nexport function PlayIcon() {\n  return (\n    <svg\n      className=\"control-icon\"\n      width=\"100\"\n      height=\"100\"\n      viewBox=\"0 0 72 72\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\">\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M36 69C54.2254 69 69 54.2254 69 36C69 17.7746 54.2254 3 36 3C17.7746 3 3 17.7746 3 36C3 54.2254 17.7746 69 36 69ZM52.1716 38.6337L28.4366 51.5801C26.4374 52.6705 24 51.2235 24 48.9464V23.0536C24 20.7764 26.4374 19.3295 28.4366 20.4199L52.1716 33.3663C54.2562 34.5034 54.2562 37.4966 52.1716 38.6337Z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  );\n}\nexport function Heart({liked, animate}) {\n  return (\n    <>\n      <svg\n        className=\"absolute overflow-visible\"\n        viewBox=\"0 0 24 24\"\n        fill=\"none\"\n        xmlns=\"http://www.w3.org/2000/svg\">\n        <circle\n          className={`circle ${liked ? 'liked' : ''} ${animate ? 'animate' : ''}`}\n          cx=\"12\"\n          cy=\"12\"\n          r=\"11.5\"\n          fill=\"transparent\"\n          strokeWidth=\"0\"\n          stroke=\"currentColor\"\n        />\n      </svg>\n\n      <svg\n        className={`heart ${liked ? 'liked' : ''} ${animate ? 'animate' : ''}`}\n        viewBox=\"0 0 24 24\"\n        fill=\"none\"\n        xmlns=\"http://www.w3.org/2000/svg\">\n        {liked ? (\n          <path\n            d=\"M12 23a.496.496 0 0 1-.26-.074C7.023 19.973 0 13.743 0 8.68c0-4.12 2.322-6.677 6.058-6.677 2.572 0 5.108 2.387 5.134 2.41l.808.771.808-.771C12.834 4.387 15.367 2 17.935 2 21.678 2 24 4.558 24 8.677c0 5.06-7.022 11.293-11.74 14.246a.496.496 0 0 1-.26.074V23z\"\n            fill=\"currentColor\"\n          />\n        ) : (\n          <path\n            fillRule=\"evenodd\"\n            clipRule=\"evenodd\"\n            d=\"m12 5.184-.808-.771-.004-.004C11.065 4.299 8.522 2.003 6 2.003c-3.736 0-6 2.558-6 6.677 0 4.47 5.471 9.848 10 13.079.602.43 1.187.82 1.74 1.167A.497.497 0 0 0 12 23v-.003c.09 0 .182-.026.26-.074C16.977 19.97 24 13.737 24 8.677 24 4.557 21.743 2 18 2c-2.569 0-5.166 2.387-5.192 2.413L12 5.184zm-.002 15.525c2.071-1.388 4.477-3.342 6.427-5.47C20.72 12.733 22 10.401 22 8.677c0-1.708-.466-2.855-1.087-3.55C20.316 4.459 19.392 4 18 4c-.726 0-1.63.364-2.5.9-.67.412-1.148.82-1.266.92-.03.025-.037.031-.019.014l-.013.013L12 7.949 9.832 5.88a10.08 10.08 0 0 0-1.33-.977C7.633 4.367 6.728 4.003 6 4.003c-1.388 0-2.312.459-2.91 1.128C2.466 5.826 2 6.974 2 8.68c0 1.726 1.28 4.058 3.575 6.563 1.948 2.127 4.352 4.078 6.423 5.466z\"\n            fill=\"currentColor\"\n          />\n        )}\n      </svg>\n    </>\n  );\n}\n\nexport function IconSearch(props) {\n  return (\n    <svg width=\"1em\" height=\"1em\" viewBox=\"0 0 20 20\">\n      <path\n        d=\"M14.386 14.386l4.0877 4.0877-4.0877-4.0877c-2.9418 2.9419-7.7115 2.9419-10.6533 0-2.9419-2.9418-2.9419-7.7115 0-10.6533 2.9418-2.9419 7.7115-2.9419 10.6533 0 2.9419 2.9418 2.9419 7.7115 0 10.6533z\"\n        stroke=\"currentColor\"\n        fill=\"none\"\n        strokeWidth=\"2\"\n        fillRule=\"evenodd\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"></path>\n    </svg>\n  );\n}\n```\n\n```js src/Layout.js\nimport {ViewTransition} from 'react'; import { useIsNavPending } from \"./router\";\n\nexport default function Page({ heading, children }) {\n  const isPending = useIsNavPending();\n\n  return (\n    <div className=\"page\">\n      <div className=\"top\">\n        <div className=\"top-nav\">\n          {heading}\n          {isPending && <span className=\"loader\"></span>}\n        </div>\n      </div>\n      {/* 콘텐츠에 대해 ViewTransition을 사용하지 않습니다. */}\n      {/* 콘텐츠는 자체 ViewTransition을 정의할 수 있습니다. */}\n      <ViewTransition default=\"none\">\n        <div className=\"bottom\">\n          <div className=\"content\">{children}</div>\n        </div>\n      </ViewTransition>\n    </div>\n  );\n}\n```\n\n```js src/LikeButton.js hidden\nimport {useState} from 'react';\nimport {Heart} from './Icons';\n\n// A hack since we don't actually have a backend.\n// Unlike local state, this survives videos being filtered.\nconst likedVideos = new Set();\n\nexport default function LikeButton({video}) {\n  const [isLiked, setIsLiked] = useState(() => likedVideos.has(video.id));\n  const [animate, setAnimate] = useState(false);\n  return (\n    <button\n      className={`like-button ${isLiked && 'liked'}`}\n      aria-label={isLiked ? 'Unsave' : 'Save'}\n      onClick={() => {\n        const nextIsLiked = !isLiked;\n        if (nextIsLiked) {\n          likedVideos.add(video.id);\n        } else {\n          likedVideos.delete(video.id);\n        }\n        setAnimate(true);\n        setIsLiked(nextIsLiked);\n      }}>\n      <Heart liked={isLiked} animate={animate} />\n    </button>\n  );\n}\n```\n\n```js src/Videos.js hidden\nimport { useState } from \"react\";\nimport LikeButton from \"./LikeButton\";\nimport { useRouter } from \"./router\";\nimport { PauseIcon, PlayIcon } from \"./Icons\";\nimport { startTransition } from \"react\";\n\nexport function VideoControls() {\n  const [isPlaying, setIsPlaying] = useState(false);\n\n  return (\n    <span\n      className=\"controls\"\n      onClick={() =>\n        startTransition(() => {\n          setIsPlaying((p) => !p);\n        })\n      }\n    >\n      {isPlaying ? <PauseIcon /> : <PlayIcon />}\n    </span>\n  );\n}\n\nexport function Thumbnail({ video, children }) {\n  return (\n    <div\n      aria-hidden=\"true\"\n      tabIndex={-1}\n      className={`thumbnail ${video.image}`}\n    >\n      {children}\n    </div>\n  );\n}\n\nexport function Video({ video }) {\n  const { navigate } = useRouter();\n\n  return (\n    <div className=\"video\">\n      <div\n        className=\"link\"\n        onClick={(e) => {\n          e.preventDefault();\n          navigate(`/video/${video.id}`);\n        }}\n      >\n        <Thumbnail video={video}></Thumbnail>\n\n        <div className=\"info\">\n          <div className=\"video-title\">{video.title}</div>\n          <div className=\"video-description\">{video.description}</div>\n        </div>\n      </div>\n      <LikeButton video={video} />\n    </div>\n  );\n}\n```\n\n\n```js src/data.js hidden\nconst videos = [\n  {\n    id: '1',\n    title: 'First video',\n    description: 'Video description',\n    image: 'blue',\n  },\n  {\n    id: '2',\n    title: 'Second video',\n    description: 'Video description',\n    image: 'red',\n  },\n  {\n    id: '3',\n    title: 'Third video',\n    description: 'Video description',\n    image: 'green',\n  },\n  {\n    id: '4',\n    title: 'Fourth video',\n    description: 'Video description',\n    image: 'purple',\n  },\n  {\n    id: '5',\n    title: 'Fifth video',\n    description: 'Video description',\n    image: 'yellow',\n  },\n  {\n    id: '6',\n    title: 'Sixth video',\n    description: 'Video description',\n    image: 'gray',\n  },\n];\n\nlet videosCache = new Map();\nlet videoCache = new Map();\nlet videoDetailsCache = new Map();\nconst VIDEO_DELAY = 1;\nconst VIDEO_DETAILS_DELAY = 1000;\nexport function fetchVideos() {\n  if (videosCache.has(0)) {\n    return videosCache.get(0);\n  }\n  const promise = new Promise((resolve) => {\n    setTimeout(() => {\n      resolve(videos);\n    }, VIDEO_DELAY);\n  });\n  videosCache.set(0, promise);\n  return promise;\n}\n\nexport function fetchVideo(id) {\n  if (videoCache.has(id)) {\n    return videoCache.get(id);\n  }\n  const promise = new Promise((resolve) => {\n    setTimeout(() => {\n      resolve(videos.find((video) => video.id === id));\n    }, VIDEO_DELAY);\n  });\n  videoCache.set(id, promise);\n  return promise;\n}\n\nexport function fetchVideoDetails(id) {\n  if (videoDetailsCache.has(id)) {\n    return videoDetailsCache.get(id);\n  }\n  const promise = new Promise((resolve) => {\n    setTimeout(() => {\n      resolve(videos.find((video) => video.id === id));\n    }, VIDEO_DETAILS_DELAY);\n  });\n  videoDetailsCache.set(id, promise);\n  return promise;\n}\n```\n\n```js src/router.js\nimport {useState, createContext,use,useTransition,useLayoutEffect,useEffect} from \"react\";\n\nexport function Router({ children }) {\n  const [isPending, startTransition] = useTransition();\n\n  function navigate(url) {\n    // Update router state in transition.\n    startTransition(() => {\n      go(url);\n    });\n  }\n\n\n\n\n  const [routerState, setRouterState] = useState({\n    pendingNav: () => {},\n    url: document.location.pathname,\n  });\n\n\n  function go(url) {\n    setRouterState({\n      url,\n      pendingNav() {\n        window.history.pushState({}, \"\", url);\n      },\n    });\n  }\n\n\n  function navigateBack(url) {\n    startTransition(() => {\n      go(url);\n    });\n  }\n\n  useEffect(() => {\n    function handlePopState() {\n      // 복원은 동기적으로 이루어져야 하므로 애니메이션되면 안 됩니다.\n      // 트랜지션이더라도 마찬가지입니다.\n      startTransition(() => {\n        setRouterState({\n          url: document.location.pathname + document.location.search,\n          pendingNav() {\n            // 아무 작업도 하지 않습니다. URL은 이미 업데이트되었습니다.\n          },\n        });\n      });\n    }\n    window.addEventListener(\"popstate\", handlePopState);\n    return () => {\n      window.removeEventListener(\"popstate\", handlePopState);\n    };\n  }, []);\n  const pendingNav = routerState.pendingNav;\n  useLayoutEffect(() => {\n    pendingNav();\n  }, [pendingNav]);\n\n  return (\n    <RouterContext\n      value={{\n        url: routerState.url,\n        navigate,\n        navigateBack,\n        isPending,\n        params: {},\n      }}\n    >\n      {children}\n    </RouterContext>\n  );\n}\n\nconst RouterContext = createContext({ url: \"/\", params: {} });\n\nexport function useRouter() {\n  return use(RouterContext);\n}\n\nexport function useIsNavPending() {\n  return use(RouterContext).isPending;\n}\n```\n\n```css src/styles.css hidden\n@font-face {\n  font-family: Optimistic Text;\n  src: url(https://react.dev/fonts/Optimistic_Text_W_Rg.woff2) format(\"woff2\");\n  font-weight: 400;\n  font-style: normal;\n  font-display: swap;\n}\n\n@font-face {\n  font-family: Optimistic Text;\n  src: url(https://react.dev/fonts/Optimistic_Text_W_Md.woff2) format(\"woff2\");\n  font-weight: 500;\n  font-style: normal;\n  font-display: swap;\n}\n\n@font-face {\n  font-family: Optimistic Text;\n  src: url(https://react.dev/fonts/Optimistic_Text_W_Bd.woff2) format(\"woff2\");\n  font-weight: 600;\n  font-style: normal;\n  font-display: swap;\n}\n\n@font-face {\n  font-family: Optimistic Text;\n  src: url(https://react.dev/fonts/Optimistic_Text_W_Bd.woff2) format(\"woff2\");\n  font-weight: 700;\n  font-style: normal;\n  font-display: swap;\n}\n\n* {\n  box-sizing: border-box;\n}\n\nhtml {\n  background-image: url(https://react.dev/images/meta-gradient-dark.png);\n  background-size: 100%;\n  background-position: -100%;\n  background-color: rgb(64 71 86);\n  background-repeat: no-repeat;\n  height: 100%;\n  width: 100%;\n}\n\nbody {\n  font-family: Optimistic Text, -apple-system, ui-sans-serif, system-ui, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;\n  padding: 10px 0 10px 0;\n  margin: 0;\n  display: flex;\n  justify-content: center;\n}\n\n#root {\n  flex: 1 1;\n  height: auto;\n  background-color: #fff;\n  border-radius: 10px;\n  max-width: 450px;\n  min-height: 600px;\n  padding-bottom: 10px;\n}\n\nh1 {\n  margin-top: 0;\n  font-size: 22px;\n}\n\nh2 {\n  margin-top: 0;\n  font-size: 20px;\n}\n\nh3 {\n  margin-top: 0;\n  font-size: 18px;\n}\n\nh4 {\n  margin-top: 0;\n  font-size: 16px;\n}\n\nh5 {\n  margin-top: 0;\n  font-size: 14px;\n}\n\nh6 {\n  margin-top: 0;\n  font-size: 12px;\n}\n\ncode {\n  font-size: 1.2em;\n}\n\nul {\n  padding-inline-start: 20px;\n}\n\n.sr-only {\n  position: absolute;\n  width: 1px;\n  height: 1px;\n  padding: 0;\n  margin: -1px;\n  overflow: hidden;\n  clip: rect(0, 0, 0, 0);\n  white-space: nowrap;\n  border-width: 0;\n}\n\n.absolute {\n  position: absolute;\n}\n\n.overflow-visible {\n  overflow: visible;\n}\n\n.visible {\n  overflow: visible;\n}\n\n.fit {\n  width: fit-content;\n}\n\n\n/* Layout */\n.page {\n  display: flex;\n  flex-direction: column;\n  height: 100%;\n}\n\n.top-hero {\n  height: 200px;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  background-image: conic-gradient(\n      from 90deg at -10% 100%,\n      #2b303b 0deg,\n      #2b303b 90deg,\n      #16181d 1turn\n  );\n}\n\n.bottom {\n  flex: 1;\n  overflow: auto;\n}\n\n.top-nav {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  margin-bottom: 0;\n  padding: 0 12px;\n  top: 0;\n  width: 100%;\n  height: 44px;\n  color: #23272f;\n  font-weight: 700;\n  font-size: 20px;\n  z-index: 100;\n  cursor: default;\n}\n\n.content {\n  padding: 0 12px;\n  margin-top: 4px;\n}\n\n\n.loader {\n  color: #23272f;\n  font-size: 3px;\n  width: 1em;\n  margin-right: 18px;\n  height: 1em;\n  border-radius: 50%;\n  position: relative;\n  text-indent: -9999em;\n  animation: loading-spinner 1.3s infinite linear;\n  animation-delay: 200ms;\n  transform: translateZ(0);\n}\n\n@keyframes loading-spinner {\n  0%,\n  100% {\n    box-shadow: 0 -3em 0 0.2em,\n    2em -2em 0 0em, 3em 0 0 -1em,\n    2em 2em 0 -1em, 0 3em 0 -1em,\n    -2em 2em 0 -1em, -3em 0 0 -1em,\n    -2em -2em 0 0;\n  }\n  12.5% {\n    box-shadow: 0 -3em 0 0, 2em -2em 0 0.2em,\n    3em 0 0 0, 2em 2em 0 -1em, 0 3em 0 -1em,\n    -2em 2em 0 -1em, -3em 0 0 -1em,\n    -2em -2em 0 -1em;\n  }\n  25% {\n    box-shadow: 0 -3em 0 -0.5em,\n    2em -2em 0 0, 3em 0 0 0.2em,\n    2em 2em 0 0, 0 3em 0 -1em,\n    -2em 2em 0 -1em, -3em 0 0 -1em,\n    -2em -2em 0 -1em;\n  }\n  37.5% {\n    box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em,\n    3em 0em 0 0, 2em 2em 0 0.2em, 0 3em 0 0em,\n    -2em 2em 0 -1em, -3em 0em 0 -1em, -2em -2em 0 -1em;\n  }\n  50% {\n    box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em,\n    3em 0 0 -1em, 2em 2em 0 0em, 0 3em 0 0.2em,\n    -2em 2em 0 0, -3em 0em 0 -1em, -2em -2em 0 -1em;\n  }\n  62.5% {\n    box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em,\n    3em 0 0 -1em, 2em 2em 0 -1em, 0 3em 0 0,\n    -2em 2em 0 0.2em, -3em 0 0 0, -2em -2em 0 -1em;\n  }\n  75% {\n    box-shadow: 0em -3em 0 -1em, 2em -2em 0 -1em,\n    3em 0em 0 -1em, 2em 2em 0 -1em, 0 3em 0 -1em,\n    -2em 2em 0 0, -3em 0em 0 0.2em, -2em -2em 0 0;\n  }\n  87.5% {\n    box-shadow: 0em -3em 0 0, 2em -2em 0 -1em,\n    3em 0 0 -1em, 2em 2em 0 -1em, 0 3em 0 -1em,\n    -2em 2em 0 0, -3em 0em 0 0, -2em -2em 0 0.2em;\n  }\n}\n\n/* LikeButton */\n.like-button {\n  outline-offset: 2px;\n  position: relative;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  width: 2.5rem;\n  height: 2.5rem;\n  cursor: pointer;\n  border-radius: 9999px;\n  border: none;\n  outline: none 2px;\n  color: #5e687e;\n  background: none;\n}\n\n.like-button:focus {\n  color: #a6423a;\n  background-color: rgba(166, 66, 58, .05);\n}\n\n.like-button:active {\n  color: #a6423a;\n  background-color: rgba(166, 66, 58, .05);\n  transform: scaleX(0.95) scaleY(0.95);\n}\n\n.like-button:hover {\n  background-color: #f6f7f9;\n}\n\n.like-button.liked {\n  color: #a6423a;\n}\n\n/* Icons */\n@keyframes circle {\n  0% {\n    transform: scale(0);\n    stroke-width: 16px;\n  }\n\n  50% {\n    transform: scale(.5);\n    stroke-width: 16px;\n  }\n\n  to {\n    transform: scale(1);\n    stroke-width: 0;\n  }\n}\n\n.circle {\n  color: rgba(166, 66, 58, .5);\n  transform-origin: center;\n  transition-property: all;\n  transition-duration: .15s;\n  transition-timing-function: cubic-bezier(.4,0,.2,1);\n}\n\n.circle.liked.animate {\n  animation: circle .3s forwards;\n}\n\n.heart {\n  width: 1.5rem;\n  height: 1.5rem;\n}\n\n.heart.liked {\n  transform-origin: center;\n  transition-property: all;\n  transition-duration: .15s;\n  transition-timing-function: cubic-bezier(.4, 0, .2, 1);\n}\n\n.heart.liked.animate {\n  animation: scale .35s ease-in-out forwards;\n}\n\n.control-icon {\n  color: hsla(0, 0%, 100%, .5);\n  filter:  drop-shadow(0 20px 13px rgba(0, 0, 0, .03)) drop-shadow(0 8px 5px rgba(0, 0, 0, .08));\n}\n\n.chevron-left {\n  margin-top: 2px;\n  rotate: 90deg;\n}\n\n\n/* Video */\n.thumbnail {\n  position: relative;\n  aspect-ratio: 16 / 9;\n  display: flex;\n  overflow: hidden;\n  flex-direction: column;\n  justify-content: center;\n  align-items: center;\n  border-radius: 0.5rem;\n  outline-offset: 2px;\n  width: 8rem;\n  vertical-align: middle;\n  background-color: #ffffff;\n  background-size: cover;\n  user-select: none;\n}\n\n.thumbnail.blue {\n  background-image: conic-gradient(at top right, #c76a15, #087ea4, #2b3491);\n}\n\n.thumbnail.red {\n  background-image: conic-gradient(at top right, #c76a15, #a6423a, #2b3491);\n}\n\n.thumbnail.green {\n  background-image: conic-gradient(at top right, #c76a15, #388f7f, #2b3491);\n}\n\n.thumbnail.purple {\n  background-image: conic-gradient(at top right, #c76a15, #575fb7, #2b3491);\n}\n\n.thumbnail.yellow {\n  background-image: conic-gradient(at top right, #c76a15, #FABD62, #2b3491);\n}\n\n.thumbnail.gray {\n  background-image: conic-gradient(at top right, #c76a15, #4E5769, #2b3491);\n}\n\n.video {\n  display: flex;\n  flex-direction: row;\n  gap: 0.75rem;\n  align-items: center;\n}\n\n.video .link {\n  display: flex;\n  flex-direction: row;\n  flex: 1 1 0;\n  gap: 0.125rem;\n  outline-offset: 4px;\n  cursor: pointer;\n}\n\n.video .info {\n  display: flex;\n  flex-direction: column;\n  justify-content: center;\n  margin-left: 8px;\n  gap: 0.125rem;\n}\n\n.video .info:hover {\n  text-decoration: underline;\n}\n\n.video-title {\n  font-size: 15px;\n  line-height: 1.25;\n  font-weight: 700;\n  color: #23272f;\n}\n\n.video-description {\n  color: #5e687e;\n  font-size: 13px;\n}\n\n/* Details */\n.details .thumbnail {\n  position: relative;\n  aspect-ratio: 16 / 9;\n  display: flex;\n  overflow: hidden;\n  flex-direction: column;\n  justify-content: center;\n  align-items: center;\n  border-radius: 0.5rem;\n  outline-offset: 2px;\n  width: 100%;\n  vertical-align: middle;\n  background-color: #ffffff;\n  background-size: cover;\n  user-select: none;\n}\n\n.video-details-title {\n  margin-top: 8px;\n}\n\n.video-details-speaker {\n  display: flex;\n  gap: 8px;\n  margin-top: 10px\n}\n\n.back {\n  display: flex;\n  align-items: center;\n  margin-left: -5px;\n  cursor: pointer;\n}\n\n.back:hover {\n  text-decoration: underline;\n}\n\n.info-title {\n  font-size: 1.5rem;\n  font-weight: 700;\n  line-height: 1.25;\n  margin: 8px 0 0 0 ;\n}\n\n.info-description {\n  margin: 8px 0 0 0;\n}\n\n.controls {\n  cursor: pointer;\n}\n\n.fallback {\n  background: #f6f7f8 linear-gradient(to right, #e6e6e6 5%, #cccccc 25%, #e6e6e6 35%) no-repeat;\n  background-size: 800px 104px;\n  display: block;\n  line-height: 1.25;\n  margin: 8px 0 0 0;\n  border-radius: 5px;\n  overflow: hidden;\n\n  animation: 1s linear 1s infinite shimmer;\n  animation-delay: 300ms;\n  animation-duration: 1s;\n  animation-fill-mode: forwards;\n  animation-iteration-count: infinite;\n  animation-name: shimmer;\n  animation-timing-function: linear;\n}\n\n\n.fallback.title {\n  width: 130px;\n  height: 30px;\n\n}\n\n.fallback.description {\n  width: 150px;\n  height: 21px;\n}\n\n@keyframes shimmer {\n  0% {\n    background-position: -468px 0;\n  }\n\n  100% {\n    background-position: 468px 0;\n  }\n}\n\n.search {\n  margin-bottom: 10px;\n}\n.search-input {\n  width: 100%;\n  position: relative;\n}\n\n.search-icon {\n  position: absolute;\n  top: 0;\n  bottom: 0;\n  inset-inline-start: 0;\n  display: flex;\n  align-items: center;\n  padding-inline-start: 1rem;\n  pointer-events: none;\n  color: #99a1b3;\n}\n\n.search-input input {\n  display: flex;\n  padding-inline-start: 2.75rem;\n  padding-top: 10px;\n  padding-bottom: 10px;\n  width: 100%;\n  text-align: start;\n  background-color: rgb(235 236 240);\n  outline: 2px solid transparent;\n  cursor: pointer;\n  border: none;\n  align-items: center;\n  color: rgb(35 39 47);\n  border-radius: 9999px;\n  vertical-align: middle;\n  font-size: 15px;\n}\n\n.search-input input:hover, .search-input input:active {\n  background-color: rgb(235 236 240/ 0.8);\n  color: rgb(35 39 47/ 0.8);\n}\n\n/* Home */\n.video-list {\n  position: relative;\n}\n\n.video-list .videos {\n  display: flex;\n  flex-direction: column;\n  gap: 1rem;\n  overflow-y: auto;\n  height: 100%;\n}\n```\n\n```js src/index.js hidden\nimport React, {StrictMode} from 'react';\nimport {createRoot} from 'react-dom/client';\nimport './styles.css';\n\nimport App from './App';\nimport {Router} from './router';\n\nconst root = createRoot(document.getElementById('root'));\nroot.render(\n  <StrictMode>\n    <Router>\n      <App />\n    </Router>\n  </StrictMode>\n);\n```\n\n```json package.json hidden\n{\n  \"dependencies\": {\n    \"react\": \"canary\",\n    \"react-dom\": \"canary\",\n    \"react-scripts\": \"latest\"\n  },\n  \"scripts\": {\n    \"start\": \"react-scripts start\",\n    \"build\": \"react-scripts build\",\n    \"test\": \"react-scripts test --env=jsdom\",\n    \"eject\": \"react-scripts eject\"\n  }\n}\n```\n\n</Sandpack>\n\n라우터가 이미 `startTransition`으로 라우트를 업데이트하고 있으므로, `<ViewTransition>`을 추가하는 이 한 줄 변경만으로 기본 크로스 페이드 애니메이션이 활성화됩니다.\n\n어떻게 동작하는지 궁금하다면 [How does `<ViewTransition>` work?](/reference/react/ViewTransition#how-does-viewtransition-work) 문서를 참고하세요.\n\n<Note>\n\n#### `<ViewTransition>` 애니메이션 건너뛰기 {/*opting-out-of-viewtransition-animations*/}\n\n이 예시에서는 단순화를 위해 앱의 루트를 `<ViewTransition>`으로 감싸고 있습니다. 하지만 이렇게 하면 앱의 모든 트랜지션이 애니메이션되므로, 예상치 못한 애니메이션이 생길 수 있습니다.\n\n이를 해결하기 위해 각 페이지에서 자체적으로 애니메이션을 제어할 수 있도록 라우트 자식 요소를 `\"none\"`으로 감싸고 있습니다.\n\n```js\n// Layout.js\n<ViewTransition default=\"none\">\n  {children}\n</ViewTransition>\n```\n\n실제로는 네비게이션을 `\"enter\"`와 `\"exit\"` prop으로 처리하거나, 트랜지션 타입을 사용해야 합니다.\n\n</Note>\n\n### Customizing animations {/*customizing-animations*/}\n\n기본적으로 `<ViewTransition>`은 브라우저의 기본 크로스페이드를 포함합니다.\n\n애니메이션을 커스터마이징하려면, [어떻게 `<ViewTransition>`이 활성화되는지](/reference/react/ViewTransition#props)에 따라, 어떤 애니메이션을 사용할지 지정하는 Props를 `<ViewTransition>` 컴포넌트에 제공할 수 있습니다.\n\n예를 들어, `default` 크로스페이드 애니메이션을 느리게 만들 수 있습니다.\n\n```js\n<ViewTransition default=\"slow-fade\">\n  <Home />\n</ViewTransition>\n```\n\n그리고 [뷰 트랜지션 클래스](/reference/react/ViewTransition#view-transition-class)를 사용해서 CSS에서 `slow-fade`를 정의합니다.\n\n```css\n::view-transition-old(.slow-fade) {\n    animation-duration: 500ms;\n}\n\n::view-transition-new(.slow-fade) {\n    animation-duration: 500ms;\n}\n```\n\n이제 크로스페이드가 더 느려집니다.\n\n<Sandpack>\n\n```js src/App.js active\nimport { ViewTransition } from \"react\";\nimport Details from \"./Details\";\nimport Home from \"./Home\";\nimport { useRouter } from \"./router\";\n\nexport default function App() {\n  const { url } = useRouter();\n\n  // 기본 애니메이션으로 .slow-fade를 지정합니다.\n  // 애니메이션 정의는 animations.css를 참고하세요.\n  return (\n    <ViewTransition default=\"slow-fade\">\n      {url === '/' ? <Home /> : <Details />}\n    </ViewTransition>\n  );\n}\n```\n\n```js src/Details.js hidden\nimport { fetchVideo, fetchVideoDetails } from \"./data\";\nimport { Thumbnail, VideoControls } from \"./Videos\";\nimport { useRouter } from \"./router\";\nimport Layout from \"./Layout\";\nimport { use, Suspense } from \"react\";\nimport { ChevronLeft } from \"./Icons\";\n\nfunction VideoInfo({ id }) {\n  const details = use(fetchVideoDetails(id));\n  return (\n    <>\n      <p className=\"info-title\">{details.title}</p>\n      <p className=\"info-description\">{details.description}</p>\n    </>\n  );\n}\n\nfunction VideoInfoFallback() {\n  return (\n    <>\n      <div className=\"fallback title\"></div>\n      <div className=\"fallback description\"></div>\n    </>\n  );\n}\n\nexport default function Details() {\n  const { url, navigateBack } = useRouter();\n  const videoId = url.split(\"/\").pop();\n  const video = use(fetchVideo(videoId));\n\n  return (\n    <Layout\n      heading={\n        <div\n          className=\"fit back\"\n          onClick={() => {\n            navigateBack(\"/\");\n          }}\n        >\n          <ChevronLeft /> Back\n        </div>\n      }\n    >\n      <div className=\"details\">\n        <Thumbnail video={video} large>\n          <VideoControls />\n        </Thumbnail>\n        <Suspense fallback={<VideoInfoFallback />}>\n          <VideoInfo id={video.id} />\n        </Suspense>\n      </div>\n    </Layout>\n  );\n}\n\n```\n\n```js src/Home.js hidden\nimport { Video } from \"./Videos\";\nimport Layout from \"./Layout\";\nimport { fetchVideos } from \"./data\";\nimport { useId, useState, use } from \"react\";\nimport { IconSearch } from \"./Icons\";\n\nfunction SearchInput({ value, onChange }) {\n  const id = useId();\n  return (\n    <form className=\"search\" onSubmit={(e) => e.preventDefault()}>\n      <label htmlFor={id} className=\"sr-only\">\n        Search\n      </label>\n      <div className=\"search-input\">\n        <div className=\"search-icon\">\n          <IconSearch />\n        </div>\n        <input\n          type=\"text\"\n          id={id}\n          placeholder=\"Search\"\n          value={value}\n          onChange={(e) => onChange(e.target.value)}\n        />\n      </div>\n    </form>\n  );\n}\n\nfunction filterVideos(videos, query) {\n  const keywords = query\n    .toLowerCase()\n    .split(\" \")\n    .filter((s) => s !== \"\");\n  if (keywords.length === 0) {\n    return videos;\n  }\n  return videos.filter((video) => {\n    const words = (video.title + \" \" + video.description)\n      .toLowerCase()\n      .split(\" \");\n    return keywords.every((kw) => words.some((w) => w.includes(kw)));\n  });\n}\n\nexport default function Home() {\n  const videos = use(fetchVideos());\n  const count = videos.length;\n  const [searchText, setSearchText] = useState(\"\");\n  const foundVideos = filterVideos(videos, searchText);\n  return (\n    <Layout heading={<div className=\"fit\">{count} Videos</div>}>\n      <SearchInput value={searchText} onChange={setSearchText} />\n      <div className=\"video-list\">\n        {foundVideos.length === 0 && (\n          <div className=\"no-results\">No results</div>\n        )}\n        <div className=\"videos\">\n          {foundVideos.map((video) => (\n            <Video key={video.id} video={video} />\n          ))}\n        </div>\n      </div>\n    </Layout>\n  );\n}\n\n```\n\n```js src/Icons.js hidden\nexport function ChevronLeft() {\n  return (\n    <svg\n      className=\"chevron-left\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width=\"20\"\n      height=\"20\"\n      viewBox=\"0 0 20 20\">\n      <g fill=\"none\" fillRule=\"evenodd\" transform=\"translate(-446 -398)\">\n        <path\n          fill=\"currentColor\"\n          fillRule=\"nonzero\"\n          d=\"M95.8838835,240.366117 C95.3957281,239.877961 94.6042719,239.877961 94.1161165,240.366117 C93.6279612,240.854272 93.6279612,241.645728 94.1161165,242.133883 L98.6161165,246.633883 C99.1042719,247.122039 99.8957281,247.122039 100.383883,246.633883 L104.883883,242.133883 C105.372039,241.645728 105.372039,240.854272 104.883883,240.366117 C104.395728,239.877961 103.604272,239.877961 103.116117,240.366117 L99.5,243.982233 L95.8838835,240.366117 Z\"\n          transform=\"translate(356.5 164.5)\"\n        />\n        <polygon points=\"446 418 466 418 466 398 446 398\" />\n      </g>\n    </svg>\n  );\n}\n\nexport function PauseIcon() {\n  return (\n    <svg\n      className=\"control-icon\"\n      style={{padding: '4px'}}\n      width=\"100\"\n      height=\"100\"\n      viewBox=\"0 0 512 512\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\">\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M256 0C114.617 0 0 114.615 0 256s114.617 256 256 256 256-114.615 256-256S397.383 0 256 0zm-32 320c0 8.836-7.164 16-16 16h-32c-8.836 0-16-7.164-16-16V192c0-8.836 7.164-16 16-16h32c8.836 0 16 7.164 16 16v128zm128 0c0 8.836-7.164 16-16 16h-32c-8.836 0-16-7.164-16-16V192c0-8.836 7.164-16 16-16h32c8.836 0 16 7.164 16 16v128z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  );\n}\n\nexport function PlayIcon() {\n  return (\n    <svg\n      className=\"control-icon\"\n      width=\"100\"\n      height=\"100\"\n      viewBox=\"0 0 72 72\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\">\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M36 69C54.2254 69 69 54.2254 69 36C69 17.7746 54.2254 3 36 3C17.7746 3 3 17.7746 3 36C3 54.2254 17.7746 69 36 69ZM52.1716 38.6337L28.4366 51.5801C26.4374 52.6705 24 51.2235 24 48.9464V23.0536C24 20.7764 26.4374 19.3295 28.4366 20.4199L52.1716 33.3663C54.2562 34.5034 54.2562 37.4966 52.1716 38.6337Z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  );\n}\nexport function Heart({liked, animate}) {\n  return (\n    <>\n      <svg\n        className=\"absolute overflow-visible\"\n        viewBox=\"0 0 24 24\"\n        fill=\"none\"\n        xmlns=\"http://www.w3.org/2000/svg\">\n        <circle\n          className={`circle ${liked ? 'liked' : ''} ${animate ? 'animate' : ''}`}\n          cx=\"12\"\n          cy=\"12\"\n          r=\"11.5\"\n          fill=\"transparent\"\n          strokeWidth=\"0\"\n          stroke=\"currentColor\"\n        />\n      </svg>\n\n      <svg\n        className={`heart ${liked ? 'liked' : ''} ${animate ? 'animate' : ''}`}\n        viewBox=\"0 0 24 24\"\n        fill=\"none\"\n        xmlns=\"http://www.w3.org/2000/svg\">\n        {liked ? (\n          <path\n            d=\"M12 23a.496.496 0 0 1-.26-.074C7.023 19.973 0 13.743 0 8.68c0-4.12 2.322-6.677 6.058-6.677 2.572 0 5.108 2.387 5.134 2.41l.808.771.808-.771C12.834 4.387 15.367 2 17.935 2 21.678 2 24 4.558 24 8.677c0 5.06-7.022 11.293-11.74 14.246a.496.496 0 0 1-.26.074V23z\"\n            fill=\"currentColor\"\n          />\n        ) : (\n          <path\n            fillRule=\"evenodd\"\n            clipRule=\"evenodd\"\n            d=\"m12 5.184-.808-.771-.004-.004C11.065 4.299 8.522 2.003 6 2.003c-3.736 0-6 2.558-6 6.677 0 4.47 5.471 9.848 10 13.079.602.43 1.187.82 1.74 1.167A.497.497 0 0 0 12 23v-.003c.09 0 .182-.026.26-.074C16.977 19.97 24 13.737 24 8.677 24 4.557 21.743 2 18 2c-2.569 0-5.166 2.387-5.192 2.413L12 5.184zm-.002 15.525c2.071-1.388 4.477-3.342 6.427-5.47C20.72 12.733 22 10.401 22 8.677c0-1.708-.466-2.855-1.087-3.55C20.316 4.459 19.392 4 18 4c-.726 0-1.63.364-2.5.9-.67.412-1.148.82-1.266.92-.03.025-.037.031-.019.014l-.013.013L12 7.949 9.832 5.88a10.08 10.08 0 0 0-1.33-.977C7.633 4.367 6.728 4.003 6 4.003c-1.388 0-2.312.459-2.91 1.128C2.466 5.826 2 6.974 2 8.68c0 1.726 1.28 4.058 3.575 6.563 1.948 2.127 4.352 4.078 6.423 5.466z\"\n            fill=\"currentColor\"\n          />\n        )}\n      </svg>\n    </>\n  );\n}\n\nexport function IconSearch(props) {\n  return (\n    <svg width=\"1em\" height=\"1em\" viewBox=\"0 0 20 20\">\n      <path\n        d=\"M14.386 14.386l4.0877 4.0877-4.0877-4.0877c-2.9418 2.9419-7.7115 2.9419-10.6533 0-2.9419-2.9418-2.9419-7.7115 0-10.6533 2.9418-2.9419 7.7115-2.9419 10.6533 0 2.9419 2.9418 2.9419 7.7115 0 10.6533z\"\n        stroke=\"currentColor\"\n        fill=\"none\"\n        strokeWidth=\"2\"\n        fillRule=\"evenodd\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"></path>\n    </svg>\n  );\n}\n```\n\n```js src/Layout.js hidden\nimport {ViewTransition} from 'react'; import { useIsNavPending } from \"./router\";\n\nexport default function Page({ heading, children }) {\n  const isPending = useIsNavPending();\n\n  return (\n    <div className=\"page\">\n      <div className=\"top\">\n        <div className=\"top-nav\">\n          {heading}\n          {isPending && <span className=\"loader\"></span>}\n        </div>\n      </div>\n      {/* 콘텐츠는 ViewTransition을 적용하지 않습니다. */}\n      {/* 콘텐츠에서 자체적으로 ViewTransition을 정의할 수 있습니다. */}\n      <ViewTransition default=\"none\">\n        <div className=\"bottom\">\n          <div className=\"content\">{children}</div>\n        </div>\n      </ViewTransition>\n    </div>\n  );\n}\n```\n\n```js src/LikeButton.js hidden\nimport {useState} from 'react';\nimport {Heart} from './Icons';\n\n// 실제로는 백엔드가 없어서 쓰는 임시 처리입니다.\n// 로컬 상태와 달리, 비디오 목록을 필터링해도 이 값은 유지됩니다.\nconst likedVideos = new Set();\n\nexport default function LikeButton({video}) {\n  const [isLiked, setIsLiked] = useState(() => likedVideos.has(video.id));\n  const [animate, setAnimate] = useState(false);\n  return (\n    <button\n      className={`like-button ${isLiked && 'liked'}`}\n      aria-label={isLiked ? 'Unsave' : 'Save'}\n      onClick={() => {\n        const nextIsLiked = !isLiked;\n        if (nextIsLiked) {\n          likedVideos.add(video.id);\n        } else {\n          likedVideos.delete(video.id);\n        }\n        setAnimate(true);\n        setIsLiked(nextIsLiked);\n      }}>\n      <Heart liked={isLiked} animate={animate} />\n    </button>\n  );\n}\n```\n\n```js src/Videos.js hidden\nimport { useState } from \"react\";\nimport LikeButton from \"./LikeButton\";\nimport { useRouter } from \"./router\";\nimport { PauseIcon, PlayIcon } from \"./Icons\";\nimport { startTransition } from \"react\";\n\nexport function VideoControls() {\n  const [isPlaying, setIsPlaying] = useState(false);\n\n  return (\n    <span\n      className=\"controls\"\n      onClick={() =>\n        startTransition(() => {\n          setIsPlaying((p) => !p);\n        })\n      }\n    >\n      {isPlaying ? <PauseIcon /> : <PlayIcon />}\n    </span>\n  );\n}\n\nexport function Thumbnail({ video, children }) {\n  return (\n    <div\n      aria-hidden=\"true\"\n      tabIndex={-1}\n      className={`thumbnail ${video.image}`}\n    >\n      {children}\n    </div>\n  );\n}\n\nexport function Video({ video }) {\n  const { navigate } = useRouter();\n\n  return (\n    <div className=\"video\">\n      <div\n        className=\"link\"\n        onClick={(e) => {\n          e.preventDefault();\n          navigate(`/video/${video.id}`);\n        }}\n      >\n        <Thumbnail video={video}></Thumbnail>\n\n        <div className=\"info\">\n          <div className=\"video-title\">{video.title}</div>\n          <div className=\"video-description\">{video.description}</div>\n        </div>\n      </div>\n      <LikeButton video={video} />\n    </div>\n  );\n}\n```\n\n\n```js src/data.js hidden\nconst videos = [\n  {\n    id: '1',\n    title: 'First video',\n    description: 'Video description',\n    image: 'blue',\n  },\n  {\n    id: '2',\n    title: 'Second video',\n    description: 'Video description',\n    image: 'red',\n  },\n  {\n    id: '3',\n    title: 'Third video',\n    description: 'Video description',\n    image: 'green',\n  },\n  {\n    id: '4',\n    title: 'Fourth video',\n    description: 'Video description',\n    image: 'purple',\n  },\n  {\n    id: '5',\n    title: 'Fifth video',\n    description: 'Video description',\n    image: 'yellow',\n  },\n  {\n    id: '6',\n    title: 'Sixth video',\n    description: 'Video description',\n    image: 'gray',\n  },\n];\n\nlet videosCache = new Map();\nlet videoCache = new Map();\nlet videoDetailsCache = new Map();\nconst VIDEO_DELAY = 1;\nconst VIDEO_DETAILS_DELAY = 1000;\nexport function fetchVideos() {\n  if (videosCache.has(0)) {\n    return videosCache.get(0);\n  }\n  const promise = new Promise((resolve) => {\n    setTimeout(() => {\n      resolve(videos);\n    }, VIDEO_DELAY);\n  });\n  videosCache.set(0, promise);\n  return promise;\n}\n\nexport function fetchVideo(id) {\n  if (videoCache.has(id)) {\n    return videoCache.get(id);\n  }\n  const promise = new Promise((resolve) => {\n    setTimeout(() => {\n      resolve(videos.find((video) => video.id === id));\n    }, VIDEO_DELAY);\n  });\n  videoCache.set(id, promise);\n  return promise;\n}\n\nexport function fetchVideoDetails(id) {\n  if (videoDetailsCache.has(id)) {\n    return videoDetailsCache.get(id);\n  }\n  const promise = new Promise((resolve) => {\n    setTimeout(() => {\n      resolve(videos.find((video) => video.id === id));\n    }, VIDEO_DETAILS_DELAY);\n  });\n  videoDetailsCache.set(id, promise);\n  return promise;\n}\n```\n\n```js src/router.js hidden\nimport {\n  useState,\n  createContext,\n  use,\n  useTransition,\n  useLayoutEffect,\n  useEffect,\n} from \"react\";\n\nconst RouterContext = createContext({ url: \"/\", params: {} });\n\nexport function useRouter() {\n  return use(RouterContext);\n}\n\nexport function useIsNavPending() {\n  return use(RouterContext).isPending;\n}\n\nexport function Router({ children }) {\n  const [routerState, setRouterState] = useState({\n    pendingNav: () => {},\n    url: document.location.pathname,\n  });\n  const [isPending, startTransition] = useTransition();\n\n  function go(url) {\n    setRouterState({\n      url,\n      pendingNav() {\n        window.history.pushState({}, \"\", url);\n      },\n    });\n  }\n  function navigate(url) {\n    // 트랜지션 안에서 라우터 상태를 업데이트합니다.\n    startTransition(() => {\n      go(url);\n    });\n  }\n\n  function navigateBack(url) {\n    // 트랜지션 안에서 라우터 상태를 업데이트합니다.\n    startTransition(() => {\n      go(url);\n    });\n  }\n\n  useEffect(() => {\n    function handlePopState() {\n      // 복원은 동기적으로 처리되어야 하므로 여기서는 애니메이션되면 안 됩니다.\n      // 트랜지션이더라도 마찬가지입니다.\n      startTransition(() => {\n        setRouterState({\n          url: document.location.pathname + document.location.search,\n          pendingNav() {\n            // 아무 작업도 하지 않습니다. URL은 이미 업데이트되었습니다.\n          },\n        });\n      });\n    }\n    window.addEventListener(\"popstate\", handlePopState);\n    return () => {\n      window.removeEventListener(\"popstate\", handlePopState);\n    };\n  }, []);\n  const pendingNav = routerState.pendingNav;\n  useLayoutEffect(() => {\n    pendingNav();\n  }, [pendingNav]);\n\n  return (\n    <RouterContext\n      value={{\n        url: routerState.url,\n        navigate,\n        navigateBack,\n        isPending,\n        params: {},\n      }}\n    >\n      {children}\n    </RouterContext>\n  );\n}\n```\n\n```css src/styles.css hidden\n@font-face {\n  font-family: Optimistic Text;\n  src: url(https://react.dev/fonts/Optimistic_Text_W_Rg.woff2) format(\"woff2\");\n  font-weight: 400;\n  font-style: normal;\n  font-display: swap;\n}\n\n@font-face {\n  font-family: Optimistic Text;\n  src: url(https://react.dev/fonts/Optimistic_Text_W_Md.woff2) format(\"woff2\");\n  font-weight: 500;\n  font-style: normal;\n  font-display: swap;\n}\n\n@font-face {\n  font-family: Optimistic Text;\n  src: url(https://react.dev/fonts/Optimistic_Text_W_Bd.woff2) format(\"woff2\");\n  font-weight: 600;\n  font-style: normal;\n  font-display: swap;\n}\n\n@font-face {\n  font-family: Optimistic Text;\n  src: url(https://react.dev/fonts/Optimistic_Text_W_Bd.woff2) format(\"woff2\");\n  font-weight: 700;\n  font-style: normal;\n  font-display: swap;\n}\n\n* {\n  box-sizing: border-box;\n}\n\nhtml {\n  background-image: url(https://react.dev/images/meta-gradient-dark.png);\n  background-size: 100%;\n  background-position: -100%;\n  background-color: rgb(64 71 86);\n  background-repeat: no-repeat;\n  height: 100%;\n  width: 100%;\n}\n\nbody {\n  font-family: Optimistic Text, -apple-system, ui-sans-serif, system-ui, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;\n  padding: 10px 0 10px 0;\n  margin: 0;\n  display: flex;\n  justify-content: center;\n}\n\n#root {\n  flex: 1 1;\n  height: auto;\n  background-color: #fff;\n  border-radius: 10px;\n  max-width: 450px;\n  min-height: 600px;\n  padding-bottom: 10px;\n}\n\nh1 {\n  margin-top: 0;\n  font-size: 22px;\n}\n\nh2 {\n  margin-top: 0;\n  font-size: 20px;\n}\n\nh3 {\n  margin-top: 0;\n  font-size: 18px;\n}\n\nh4 {\n  margin-top: 0;\n  font-size: 16px;\n}\n\nh5 {\n  margin-top: 0;\n  font-size: 14px;\n}\n\nh6 {\n  margin-top: 0;\n  font-size: 12px;\n}\n\ncode {\n  font-size: 1.2em;\n}\n\nul {\n  padding-inline-start: 20px;\n}\n\n.sr-only {\n  position: absolute;\n  width: 1px;\n  height: 1px;\n  padding: 0;\n  margin: -1px;\n  overflow: hidden;\n  clip: rect(0, 0, 0, 0);\n  white-space: nowrap;\n  border-width: 0;\n}\n\n.absolute {\n  position: absolute;\n}\n\n.overflow-visible {\n  overflow: visible;\n}\n\n.visible {\n  overflow: visible;\n}\n\n.fit {\n  width: fit-content;\n}\n\n\n/* Layout */\n.page {\n  display: flex;\n  flex-direction: column;\n  height: 100%;\n}\n\n.top-hero {\n  height: 200px;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  background-image: conic-gradient(\n      from 90deg at -10% 100%,\n      #2b303b 0deg,\n      #2b303b 90deg,\n      #16181d 1turn\n  );\n}\n\n.bottom {\n  flex: 1;\n  overflow: auto;\n}\n\n.top-nav {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  margin-bottom: 0;\n  padding: 0 12px;\n  top: 0;\n  width: 100%;\n  height: 44px;\n  color: #23272f;\n  font-weight: 700;\n  font-size: 20px;\n  z-index: 100;\n  cursor: default;\n}\n\n.content {\n  padding: 0 12px;\n  margin-top: 4px;\n}\n\n\n.loader {\n  color: #23272f;\n  font-size: 3px;\n  width: 1em;\n  margin-right: 18px;\n  height: 1em;\n  border-radius: 50%;\n  position: relative;\n  text-indent: -9999em;\n  animation: loading-spinner 1.3s infinite linear;\n  animation-delay: 200ms;\n  transform: translateZ(0);\n}\n\n@keyframes loading-spinner {\n  0%,\n  100% {\n    box-shadow: 0 -3em 0 0.2em,\n    2em -2em 0 0em, 3em 0 0 -1em,\n    2em 2em 0 -1em, 0 3em 0 -1em,\n    -2em 2em 0 -1em, -3em 0 0 -1em,\n    -2em -2em 0 0;\n  }\n  12.5% {\n    box-shadow: 0 -3em 0 0, 2em -2em 0 0.2em,\n    3em 0 0 0, 2em 2em 0 -1em, 0 3em 0 -1em,\n    -2em 2em 0 -1em, -3em 0 0 -1em,\n    -2em -2em 0 -1em;\n  }\n  25% {\n    box-shadow: 0 -3em 0 -0.5em,\n    2em -2em 0 0, 3em 0 0 0.2em,\n    2em 2em 0 0, 0 3em 0 -1em,\n    -2em 2em 0 -1em, -3em 0 0 -1em,\n    -2em -2em 0 -1em;\n  }\n  37.5% {\n    box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em,\n    3em 0em 0 0, 2em 2em 0 0.2em, 0 3em 0 0em,\n    -2em 2em 0 -1em, -3em 0em 0 -1em, -2em -2em 0 -1em;\n  }\n  50% {\n    box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em,\n    3em 0 0 -1em, 2em 2em 0 0em, 0 3em 0 0.2em,\n    -2em 2em 0 0, -3em 0em 0 -1em, -2em -2em 0 -1em;\n  }\n  62.5% {\n    box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em,\n    3em 0 0 -1em, 2em 2em 0 -1em, 0 3em 0 0,\n    -2em 2em 0 0.2em, -3em 0 0 0, -2em -2em 0 -1em;\n  }\n  75% {\n    box-shadow: 0em -3em 0 -1em, 2em -2em 0 -1em,\n    3em 0em 0 -1em, 2em 2em 0 -1em, 0 3em 0 -1em,\n    -2em 2em 0 0, -3em 0em 0 0.2em, -2em -2em 0 0;\n  }\n  87.5% {\n    box-shadow: 0em -3em 0 0, 2em -2em 0 -1em,\n    3em 0 0 -1em, 2em 2em 0 -1em, 0 3em 0 -1em,\n    -2em 2em 0 0, -3em 0em 0 0, -2em -2em 0 0.2em;\n  }\n}\n\n/* LikeButton */\n.like-button {\n  outline-offset: 2px;\n  position: relative;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  width: 2.5rem;\n  height: 2.5rem;\n  cursor: pointer;\n  border-radius: 9999px;\n  border: none;\n  outline: none 2px;\n  color: #5e687e;\n  background: none;\n}\n\n.like-button:focus {\n  color: #a6423a;\n  background-color: rgba(166, 66, 58, .05);\n}\n\n.like-button:active {\n  color: #a6423a;\n  background-color: rgba(166, 66, 58, .05);\n  transform: scaleX(0.95) scaleY(0.95);\n}\n\n.like-button:hover {\n  background-color: #f6f7f9;\n}\n\n.like-button.liked {\n  color: #a6423a;\n}\n\n/* Icons */\n@keyframes circle {\n  0% {\n    transform: scale(0);\n    stroke-width: 16px;\n  }\n\n  50% {\n    transform: scale(.5);\n    stroke-width: 16px;\n  }\n\n  to {\n    transform: scale(1);\n    stroke-width: 0;\n  }\n}\n\n.circle {\n  color: rgba(166, 66, 58, .5);\n  transform-origin: center;\n  transition-property: all;\n  transition-duration: .15s;\n  transition-timing-function: cubic-bezier(.4,0,.2,1);\n}\n\n.circle.liked.animate {\n  animation: circle .3s forwards;\n}\n\n.heart {\n  width: 1.5rem;\n  height: 1.5rem;\n}\n\n.heart.liked {\n  transform-origin: center;\n  transition-property: all;\n  transition-duration: .15s;\n  transition-timing-function: cubic-bezier(.4, 0, .2, 1);\n}\n\n.heart.liked.animate {\n  animation: scale .35s ease-in-out forwards;\n}\n\n.control-icon {\n  color: hsla(0, 0%, 100%, .5);\n  filter:  drop-shadow(0 20px 13px rgba(0, 0, 0, .03)) drop-shadow(0 8px 5px rgba(0, 0, 0, .08));\n}\n\n.chevron-left {\n  margin-top: 2px;\n  rotate: 90deg;\n}\n\n\n/* Video */\n.thumbnail {\n  position: relative;\n  aspect-ratio: 16 / 9;\n  display: flex;\n  overflow: hidden;\n  flex-direction: column;\n  justify-content: center;\n  align-items: center;\n  border-radius: 0.5rem;\n  outline-offset: 2px;\n  width: 8rem;\n  vertical-align: middle;\n  background-color: #ffffff;\n  background-size: cover;\n  user-select: none;\n}\n\n.thumbnail.blue {\n  background-image: conic-gradient(at top right, #c76a15, #087ea4, #2b3491);\n}\n\n.thumbnail.red {\n  background-image: conic-gradient(at top right, #c76a15, #a6423a, #2b3491);\n}\n\n.thumbnail.green {\n  background-image: conic-gradient(at top right, #c76a15, #388f7f, #2b3491);\n}\n\n.thumbnail.purple {\n  background-image: conic-gradient(at top right, #c76a15, #575fb7, #2b3491);\n}\n\n.thumbnail.yellow {\n  background-image: conic-gradient(at top right, #c76a15, #FABD62, #2b3491);\n}\n\n.thumbnail.gray {\n  background-image: conic-gradient(at top right, #c76a15, #4E5769, #2b3491);\n}\n\n.video {\n  display: flex;\n  flex-direction: row;\n  gap: 0.75rem;\n  align-items: center;\n}\n\n.video .link {\n  display: flex;\n  flex-direction: row;\n  flex: 1 1 0;\n  gap: 0.125rem;\n  outline-offset: 4px;\n  cursor: pointer;\n}\n\n.video .info {\n  display: flex;\n  flex-direction: column;\n  justify-content: center;\n  margin-left: 8px;\n  gap: 0.125rem;\n}\n\n.video .info:hover {\n  text-decoration: underline;\n}\n\n.video-title {\n  font-size: 15px;\n  line-height: 1.25;\n  font-weight: 700;\n  color: #23272f;\n}\n\n.video-description {\n  color: #5e687e;\n  font-size: 13px;\n}\n\n/* Details */\n.details .thumbnail {\n  position: relative;\n  aspect-ratio: 16 / 9;\n  display: flex;\n  overflow: hidden;\n  flex-direction: column;\n  justify-content: center;\n  align-items: center;\n  border-radius: 0.5rem;\n  outline-offset: 2px;\n  width: 100%;\n  vertical-align: middle;\n  background-color: #ffffff;\n  background-size: cover;\n  user-select: none;\n}\n\n.video-details-title {\n  margin-top: 8px;\n}\n\n.video-details-speaker {\n  display: flex;\n  gap: 8px;\n  margin-top: 10px\n}\n\n.back {\n  display: flex;\n  align-items: center;\n  margin-left: -5px;\n  cursor: pointer;\n}\n\n.back:hover {\n  text-decoration: underline;\n}\n\n.info-title {\n  font-size: 1.5rem;\n  font-weight: 700;\n  line-height: 1.25;\n  margin: 8px 0 0 0 ;\n}\n\n.info-description {\n  margin: 8px 0 0 0;\n}\n\n.controls {\n  cursor: pointer;\n}\n\n.fallback {\n  background: #f6f7f8 linear-gradient(to right, #e6e6e6 5%, #cccccc 25%, #e6e6e6 35%) no-repeat;\n  background-size: 800px 104px;\n  display: block;\n  line-height: 1.25;\n  margin: 8px 0 0 0;\n  border-radius: 5px;\n  overflow: hidden;\n\n  animation: 1s linear 1s infinite shimmer;\n  animation-delay: 300ms;\n  animation-duration: 1s;\n  animation-fill-mode: forwards;\n  animation-iteration-count: infinite;\n  animation-name: shimmer;\n  animation-timing-function: linear;\n}\n\n\n.fallback.title {\n  width: 130px;\n  height: 30px;\n\n}\n\n.fallback.description {\n  width: 150px;\n  height: 21px;\n}\n\n@keyframes shimmer {\n  0% {\n    background-position: -468px 0;\n  }\n\n  100% {\n    background-position: 468px 0;\n  }\n}\n\n.search {\n  margin-bottom: 10px;\n}\n.search-input {\n  width: 100%;\n  position: relative;\n}\n\n.search-icon {\n  position: absolute;\n  top: 0;\n  bottom: 0;\n  inset-inline-start: 0;\n  display: flex;\n  align-items: center;\n  padding-inline-start: 1rem;\n  pointer-events: none;\n  color: #99a1b3;\n}\n\n.search-input input {\n  display: flex;\n  padding-inline-start: 2.75rem;\n  padding-top: 10px;\n  padding-bottom: 10px;\n  width: 100%;\n  text-align: start;\n  background-color: rgb(235 236 240);\n  outline: 2px solid transparent;\n  cursor: pointer;\n  border: none;\n  align-items: center;\n  color: rgb(35 39 47);\n  border-radius: 9999px;\n  vertical-align: middle;\n  font-size: 15px;\n}\n\n.search-input input:hover, .search-input input:active {\n  background-color: rgb(235 236 240/ 0.8);\n  color: rgb(35 39 47/ 0.8);\n}\n\n/* Home */\n.video-list {\n  position: relative;\n}\n\n.video-list .videos {\n  display: flex;\n  flex-direction: column;\n  gap: 1rem;\n  overflow-y: auto;\n  height: 100%;\n}\n```\n\n\n```css src/animations.css\n/* 뷰 트랜지션 클래스를 사용해서 .slow-fade를 정의합니다 */\n::view-transition-old(.slow-fade) {\n    animation-duration: 500ms;\n}\n\n::view-transition-new(.slow-fade) {\n    animation-duration: 500ms;\n}\n```\n\n```js src/index.js hidden\nimport React, {StrictMode} from 'react';\nimport {createRoot} from 'react-dom/client';\nimport './styles.css';\nimport './animations.css';\n\nimport App from './App';\nimport {Router} from './router';\n\nconst root = createRoot(document.getElementById('root'));\nroot.render(\n  <StrictMode>\n    <Router>\n      <App />\n    </Router>\n  </StrictMode>\n);\n```\n\n```json package.json hidden\n{\n  \"dependencies\": {\n    \"react\": \"canary\",\n    \"react-dom\": \"canary\",\n    \"react-scripts\": \"latest\"\n  },\n  \"scripts\": {\n    \"start\": \"react-scripts start\",\n    \"build\": \"react-scripts build\",\n    \"test\": \"react-scripts test --env=jsdom\",\n    \"eject\": \"react-scripts eject\"\n  }\n}\n```\n\n</Sandpack>\n\n`<ViewTransition>` 스타일링에 대한 전체 가이드는 [View Transition 스타일링](/reference/react/ViewTransition#styling-view-transitions)을 참조하세요.\n\n### Shared Element Transitions {/*shared-element-transitions*/}\n\n두 페이지에 같은 요소가 있을 때, 종종 한 페이지에서 다음 페이지로 이어지도록 애니메이션을 주고 싶을 때가 있습니다.\n\n이를 위해 `<ViewTransition>`에 고유한 `name` 속성을 추가할 수 있습니다.\n\n```js\n<ViewTransition name={`video-${video.id}`}>\n  <Thumbnail video={video} />\n</ViewTransition>\n```\n\n이제 비디오 썸네일이 두 페이지 사이에서 애니메이션으로 전환됩니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { ViewTransition } from \"react\";\nimport Details from \"./Details\";\nimport Home from \"./Home\";\nimport { useRouter } from \"./router\";\n\nexport default function App() {\n  const { url } = useRouter();\n\n  // 기본 slow-fade는 그대로 둡니다.\n  // 공유 요소 트랜지션에 포함되지 않는 콘텐츠가\n  // 크로스 페이드되도록 하기 위함입니다.\n  return (\n    <ViewTransition default=\"slow-fade\">\n      {url === \"/\" ? <Home /> : <Details />}\n    </ViewTransition>\n  );\n}\n```\n\n```js src/Details.js hidden\nimport { fetchVideo, fetchVideoDetails } from \"./data\";\nimport { Thumbnail, VideoControls } from \"./Videos\";\nimport { useRouter } from \"./router\";\nimport Layout from \"./Layout\";\nimport { use, Suspense } from \"react\";\nimport { ChevronLeft } from \"./Icons\";\n\nfunction VideoInfo({ id }) {\n  const details = use(fetchVideoDetails(id));\n  return (\n    <>\n      <p className=\"info-title\">{details.title}</p>\n      <p className=\"info-description\">{details.description}</p>\n    </>\n  );\n}\n\nfunction VideoInfoFallback() {\n  return (\n    <>\n      <div className=\"fallback title\"></div>\n      <div className=\"fallback description\"></div>\n    </>\n  );\n}\n\nexport default function Details() {\n  const { url, navigateBack } = useRouter();\n  const videoId = url.split(\"/\").pop();\n  const video = use(fetchVideo(videoId));\n\n  return (\n    <Layout\n      heading={\n        <div\n          className=\"fit back\"\n          onClick={() => {\n            navigateBack(\"/\");\n          }}\n        >\n          <ChevronLeft /> Back\n        </div>\n      }\n    >\n      <div className=\"details\">\n        <Thumbnail video={video} large>\n          <VideoControls />\n        </Thumbnail>\n        <Suspense fallback={<VideoInfoFallback />}>\n          <VideoInfo id={video.id} />\n        </Suspense>\n      </div>\n    </Layout>\n  );\n}\n\n```\n\n```js src/Home.js hidden\nimport { Video } from \"./Videos\";\nimport Layout from \"./Layout\";\nimport { fetchVideos } from \"./data\";\nimport { useId, useState, use } from \"react\";\nimport { IconSearch } from \"./Icons\";\n\nfunction SearchInput({ value, onChange }) {\n  const id = useId();\n  return (\n    <form className=\"search\" onSubmit={(e) => e.preventDefault()}>\n      <label htmlFor={id} className=\"sr-only\">\n        Search\n      </label>\n      <div className=\"search-input\">\n        <div className=\"search-icon\">\n          <IconSearch />\n        </div>\n        <input\n          type=\"text\"\n          id={id}\n          placeholder=\"Search\"\n          value={value}\n          onChange={(e) => onChange(e.target.value)}\n        />\n      </div>\n    </form>\n  );\n}\n\nfunction filterVideos(videos, query) {\n  const keywords = query\n    .toLowerCase()\n    .split(\" \")\n    .filter((s) => s !== \"\");\n  if (keywords.length === 0) {\n    return videos;\n  }\n  return videos.filter((video) => {\n    const words = (video.title + \" \" + video.description)\n      .toLowerCase()\n      .split(\" \");\n    return keywords.every((kw) => words.some((w) => w.includes(kw)));\n  });\n}\n\nexport default function Home() {\n  const videos = use(fetchVideos());\n  const count = videos.length;\n  const [searchText, setSearchText] = useState(\"\");\n  const foundVideos = filterVideos(videos, searchText);\n  return (\n    <Layout heading={<div className=\"fit\">{count} Videos</div>}>\n      <SearchInput value={searchText} onChange={setSearchText} />\n      <div className=\"video-list\">\n        {foundVideos.length === 0 && (\n          <div className=\"no-results\">No results</div>\n        )}\n        <div className=\"videos\">\n          {foundVideos.map((video) => (\n            <Video key={video.id} video={video} />\n          ))}\n        </div>\n      </div>\n    </Layout>\n  );\n}\n\n```\n\n```js src/Icons.js hidden\nexport function ChevronLeft() {\n  return (\n    <svg\n      className=\"chevron-left\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width=\"20\"\n      height=\"20\"\n      viewBox=\"0 0 20 20\">\n      <g fill=\"none\" fillRule=\"evenodd\" transform=\"translate(-446 -398)\">\n        <path\n          fill=\"currentColor\"\n          fillRule=\"nonzero\"\n          d=\"M95.8838835,240.366117 C95.3957281,239.877961 94.6042719,239.877961 94.1161165,240.366117 C93.6279612,240.854272 93.6279612,241.645728 94.1161165,242.133883 L98.6161165,246.633883 C99.1042719,247.122039 99.8957281,247.122039 100.383883,246.633883 L104.883883,242.133883 C105.372039,241.645728 105.372039,240.854272 104.883883,240.366117 C104.395728,239.877961 103.604272,239.877961 103.116117,240.366117 L99.5,243.982233 L95.8838835,240.366117 Z\"\n          transform=\"translate(356.5 164.5)\"\n        />\n        <polygon points=\"446 418 466 418 466 398 446 398\" />\n      </g>\n    </svg>\n  );\n}\n\nexport function PauseIcon() {\n  return (\n    <svg\n      className=\"control-icon\"\n      style={{padding: '4px'}}\n      width=\"100\"\n      height=\"100\"\n      viewBox=\"0 0 512 512\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\">\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M256 0C114.617 0 0 114.615 0 256s114.617 256 256 256 256-114.615 256-256S397.383 0 256 0zm-32 320c0 8.836-7.164 16-16 16h-32c-8.836 0-16-7.164-16-16V192c0-8.836 7.164-16 16-16h32c8.836 0 16 7.164 16 16v128zm128 0c0 8.836-7.164 16-16 16h-32c-8.836 0-16-7.164-16-16V192c0-8.836 7.164-16 16-16h32c8.836 0 16 7.164 16 16v128z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  );\n}\n\nexport function PlayIcon() {\n  return (\n    <svg\n      className=\"control-icon\"\n      width=\"100\"\n      height=\"100\"\n      viewBox=\"0 0 72 72\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\">\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M36 69C54.2254 69 69 54.2254 69 36C69 17.7746 54.2254 3 36 3C17.7746 3 3 17.7746 3 36C3 54.2254 17.7746 69 36 69ZM52.1716 38.6337L28.4366 51.5801C26.4374 52.6705 24 51.2235 24 48.9464V23.0536C24 20.7764 26.4374 19.3295 28.4366 20.4199L52.1716 33.3663C54.2562 34.5034 54.2562 37.4966 52.1716 38.6337Z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  );\n}\nexport function Heart({liked, animate}) {\n  return (\n    <>\n      <svg\n        className=\"absolute overflow-visible\"\n        viewBox=\"0 0 24 24\"\n        fill=\"none\"\n        xmlns=\"http://www.w3.org/2000/svg\">\n        <circle\n          className={`circle ${liked ? 'liked' : ''} ${animate ? 'animate' : ''}`}\n          cx=\"12\"\n          cy=\"12\"\n          r=\"11.5\"\n          fill=\"transparent\"\n          strokeWidth=\"0\"\n          stroke=\"currentColor\"\n        />\n      </svg>\n\n      <svg\n        className={`heart ${liked ? 'liked' : ''} ${animate ? 'animate' : ''}`}\n        viewBox=\"0 0 24 24\"\n        fill=\"none\"\n        xmlns=\"http://www.w3.org/2000/svg\">\n        {liked ? (\n          <path\n            d=\"M12 23a.496.496 0 0 1-.26-.074C7.023 19.973 0 13.743 0 8.68c0-4.12 2.322-6.677 6.058-6.677 2.572 0 5.108 2.387 5.134 2.41l.808.771.808-.771C12.834 4.387 15.367 2 17.935 2 21.678 2 24 4.558 24 8.677c0 5.06-7.022 11.293-11.74 14.246a.496.496 0 0 1-.26.074V23z\"\n            fill=\"currentColor\"\n          />\n        ) : (\n          <path\n            fillRule=\"evenodd\"\n            clipRule=\"evenodd\"\n            d=\"m12 5.184-.808-.771-.004-.004C11.065 4.299 8.522 2.003 6 2.003c-3.736 0-6 2.558-6 6.677 0 4.47 5.471 9.848 10 13.079.602.43 1.187.82 1.74 1.167A.497.497 0 0 0 12 23v-.003c.09 0 .182-.026.26-.074C16.977 19.97 24 13.737 24 8.677 24 4.557 21.743 2 18 2c-2.569 0-5.166 2.387-5.192 2.413L12 5.184zm-.002 15.525c2.071-1.388 4.477-3.342 6.427-5.47C20.72 12.733 22 10.401 22 8.677c0-1.708-.466-2.855-1.087-3.55C20.316 4.459 19.392 4 18 4c-.726 0-1.63.364-2.5.9-.67.412-1.148.82-1.266.92-.03.025-.037.031-.019.014l-.013.013L12 7.949 9.832 5.88a10.08 10.08 0 0 0-1.33-.977C7.633 4.367 6.728 4.003 6 4.003c-1.388 0-2.312.459-2.91 1.128C2.466 5.826 2 6.974 2 8.68c0 1.726 1.28 4.058 3.575 6.563 1.948 2.127 4.352 4.078 6.423 5.466z\"\n            fill=\"currentColor\"\n          />\n        )}\n      </svg>\n    </>\n  );\n}\n\nexport function IconSearch(props) {\n  return (\n    <svg width=\"1em\" height=\"1em\" viewBox=\"0 0 20 20\">\n      <path\n        d=\"M14.386 14.386l4.0877 4.0877-4.0877-4.0877c-2.9418 2.9419-7.7115 2.9419-10.6533 0-2.9419-2.9418-2.9419-7.7115 0-10.6533 2.9418-2.9419 7.7115-2.9419 10.6533 0 2.9419 2.9418 2.9419 7.7115 0 10.6533z\"\n        stroke=\"currentColor\"\n        fill=\"none\"\n        strokeWidth=\"2\"\n        fillRule=\"evenodd\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"></path>\n    </svg>\n  );\n}\n```\n\n```js src/Layout.js hidden\nimport {ViewTransition} from 'react'; import { useIsNavPending } from \"./router\";\n\nexport default function Page({ heading, children }) {\n  const isPending = useIsNavPending();\n\n  return (\n    <div className=\"page\">\n      <div className=\"top\">\n        <div className=\"top-nav\">\n          {heading}\n          {isPending && <span className=\"loader\"></span>}\n        </div>\n      </div>\n      {/* 콘텐츠에 대해 ViewTransition을 사용하지 않습니다. */}\n      {/* 콘텐츠는 자체 ViewTransition을 정의할 수 있습니다. */}\n      <ViewTransition default=\"none\">\n        <div className=\"bottom\">\n          <div className=\"content\">{children}</div>\n        </div>\n      </ViewTransition>\n    </div>\n  );\n}\n```\n\n```js src/LikeButton.js hidden\nimport {useState} from 'react';\nimport {Heart} from './Icons';\n\n// A hack since we don't actually have a backend.\n// Unlike local state, this survives videos being filtered.\nconst likedVideos = new Set();\n\nexport default function LikeButton({video}) {\n  const [isLiked, setIsLiked] = useState(() => likedVideos.has(video.id));\n  const [animate, setAnimate] = useState(false);\n  return (\n    <button\n      className={`like-button ${isLiked && 'liked'}`}\n      aria-label={isLiked ? 'Unsave' : 'Save'}\n      onClick={() => {\n        const nextIsLiked = !isLiked;\n        if (nextIsLiked) {\n          likedVideos.add(video.id);\n        } else {\n          likedVideos.delete(video.id);\n        }\n        setAnimate(true);\n        setIsLiked(nextIsLiked);\n      }}>\n      <Heart liked={isLiked} animate={animate} />\n    </button>\n  );\n}\n```\n\n```js src/Videos.js active\nimport { useState, ViewTransition } from \"react\"; import LikeButton from \"./LikeButton\"; import { useRouter } from \"./router\"; import { PauseIcon, PlayIcon } from \"./Icons\"; import { startTransition } from \"react\";\n\nexport function Thumbnail({ video, children }) {\n  // 공유 요소 트랜지션으로 애니메이션되도록 name을 추가합니다.\n  // 기본 애니메이션을 사용하므로 추가 CSS가 필요하지 않습니다.\n  return (\n    <ViewTransition name={`video-${video.id}`}>\n      <div\n        aria-hidden=\"true\"\n        tabIndex={-1}\n        className={`thumbnail ${video.image}`}\n      >\n        {children}\n      </div>\n    </ViewTransition>\n  );\n}\n\nexport function VideoControls() {\n  const [isPlaying, setIsPlaying] = useState(false);\n\n  return (\n    <span\n      className=\"controls\"\n      onClick={() =>\n        startTransition(() => {\n          setIsPlaying((p) => !p);\n        })\n      }\n    >\n      {isPlaying ? <PauseIcon /> : <PlayIcon />}\n    </span>\n  );\n}\n\nexport function Video({ video }) {\n  const { navigate } = useRouter();\n\n  return (\n    <div className=\"video\">\n      <div\n        className=\"link\"\n        onClick={(e) => {\n          e.preventDefault();\n          navigate(`/video/${video.id}`);\n        }}\n      >\n        <Thumbnail video={video}></Thumbnail>\n\n        <div className=\"info\">\n          <div className=\"video-title\">{video.title}</div>\n          <div className=\"video-description\">{video.description}</div>\n        </div>\n      </div>\n      <LikeButton video={video} />\n    </div>\n  );\n}\n```\n\n\n```js src/data.js hidden\nconst videos = [\n  {\n    id: '1',\n    title: 'First video',\n    description: 'Video description',\n    image: 'blue',\n  },\n  {\n    id: '2',\n    title: 'Second video',\n    description: 'Video description',\n    image: 'red',\n  },\n  {\n    id: '3',\n    title: 'Third video',\n    description: 'Video description',\n    image: 'green',\n  },\n  {\n    id: '4',\n    title: 'Fourth video',\n    description: 'Video description',\n    image: 'purple',\n  },\n  {\n    id: '5',\n    title: 'Fifth video',\n    description: 'Video description',\n    image: 'yellow',\n  },\n  {\n    id: '6',\n    title: 'Sixth video',\n    description: 'Video description',\n    image: 'gray',\n  },\n];\n\nlet videosCache = new Map();\nlet videoCache = new Map();\nlet videoDetailsCache = new Map();\nconst VIDEO_DELAY = 1;\nconst VIDEO_DETAILS_DELAY = 1000;\nexport function fetchVideos() {\n  if (videosCache.has(0)) {\n    return videosCache.get(0);\n  }\n  const promise = new Promise((resolve) => {\n    setTimeout(() => {\n      resolve(videos);\n    }, VIDEO_DELAY);\n  });\n  videosCache.set(0, promise);\n  return promise;\n}\n\nexport function fetchVideo(id) {\n  if (videoCache.has(id)) {\n    return videoCache.get(id);\n  }\n  const promise = new Promise((resolve) => {\n    setTimeout(() => {\n      resolve(videos.find((video) => video.id === id));\n    }, VIDEO_DELAY);\n  });\n  videoCache.set(id, promise);\n  return promise;\n}\n\nexport function fetchVideoDetails(id) {\n  if (videoDetailsCache.has(id)) {\n    return videoDetailsCache.get(id);\n  }\n  const promise = new Promise((resolve) => {\n    setTimeout(() => {\n      resolve(videos.find((video) => video.id === id));\n    }, VIDEO_DETAILS_DELAY);\n  });\n  videoDetailsCache.set(id, promise);\n  return promise;\n}\n```\n\n```js src/router.js hidden\nimport {\n  useState,\n  createContext,\n  use,\n  useTransition,\n  useLayoutEffect,\n  useEffect,\n} from \"react\";\n\nconst RouterContext = createContext({ url: \"/\", params: {} });\n\nexport function useRouter() {\n  return use(RouterContext);\n}\n\nexport function useIsNavPending() {\n  return use(RouterContext).isPending;\n}\n\nexport function Router({ children }) {\n  const [routerState, setRouterState] = useState({\n    pendingNav: () => {},\n    url: document.location.pathname,\n  });\n  const [isPending, startTransition] = useTransition();\n\n  function go(url) {\n    setRouterState({\n      url,\n      pendingNav() {\n        window.history.pushState({}, \"\", url);\n      },\n    });\n  }\n  function navigate(url) {\n    // Update router state in transition.\n    startTransition(() => {\n      go(url);\n    });\n  }\n\n  function navigateBack(url) {\n    // Update router state in transition.\n    startTransition(() => {\n      go(url);\n    });\n  }\n\n  useEffect(() => {\n    function handlePopState() {\n      // 복원은 동기적으로 이루어져야 하므로 애니메이션되면 안 됩니다.\n      // 트랜지션이더라도 마찬가지입니다.\n      startTransition(() => {\n        setRouterState({\n          url: document.location.pathname + document.location.search,\n          pendingNav() {\n            // 아무 작업도 하지 않습니다. URL은 이미 업데이트되었습니다.\n          },\n        });\n      });\n    }\n    window.addEventListener(\"popstate\", handlePopState);\n    return () => {\n      window.removeEventListener(\"popstate\", handlePopState);\n    };\n  }, []);\n  const pendingNav = routerState.pendingNav;\n  useLayoutEffect(() => {\n    pendingNav();\n  }, [pendingNav]);\n\n  return (\n    <RouterContext\n      value={{\n        url: routerState.url,\n        navigate,\n        navigateBack,\n        isPending,\n        params: {},\n      }}\n    >\n      {children}\n    </RouterContext>\n  );\n}\n```\n\n```css src/styles.css hidden\n@font-face {\n  font-family: Optimistic Text;\n  src: url(https://react.dev/fonts/Optimistic_Text_W_Rg.woff2) format(\"woff2\");\n  font-weight: 400;\n  font-style: normal;\n  font-display: swap;\n}\n\n@font-face {\n  font-family: Optimistic Text;\n  src: url(https://react.dev/fonts/Optimistic_Text_W_Md.woff2) format(\"woff2\");\n  font-weight: 500;\n  font-style: normal;\n  font-display: swap;\n}\n\n@font-face {\n  font-family: Optimistic Text;\n  src: url(https://react.dev/fonts/Optimistic_Text_W_Bd.woff2) format(\"woff2\");\n  font-weight: 600;\n  font-style: normal;\n  font-display: swap;\n}\n\n@font-face {\n  font-family: Optimistic Text;\n  src: url(https://react.dev/fonts/Optimistic_Text_W_Bd.woff2) format(\"woff2\");\n  font-weight: 700;\n  font-style: normal;\n  font-display: swap;\n}\n\n* {\n  box-sizing: border-box;\n}\n\nhtml {\n  background-image: url(https://react.dev/images/meta-gradient-dark.png);\n  background-size: 100%;\n  background-position: -100%;\n  background-color: rgb(64 71 86);\n  background-repeat: no-repeat;\n  height: 100%;\n  width: 100%;\n}\n\nbody {\n  font-family: Optimistic Text, -apple-system, ui-sans-serif, system-ui, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;\n  padding: 10px 0 10px 0;\n  margin: 0;\n  display: flex;\n  justify-content: center;\n}\n\n#root {\n  flex: 1 1;\n  height: auto;\n  background-color: #fff;\n  border-radius: 10px;\n  max-width: 450px;\n  min-height: 600px;\n  padding-bottom: 10px;\n}\n\nh1 {\n  margin-top: 0;\n  font-size: 22px;\n}\n\nh2 {\n  margin-top: 0;\n  font-size: 20px;\n}\n\nh3 {\n  margin-top: 0;\n  font-size: 18px;\n}\n\nh4 {\n  margin-top: 0;\n  font-size: 16px;\n}\n\nh5 {\n  margin-top: 0;\n  font-size: 14px;\n}\n\nh6 {\n  margin-top: 0;\n  font-size: 12px;\n}\n\ncode {\n  font-size: 1.2em;\n}\n\nul {\n  padding-inline-start: 20px;\n}\n\n.sr-only {\n  position: absolute;\n  width: 1px;\n  height: 1px;\n  padding: 0;\n  margin: -1px;\n  overflow: hidden;\n  clip: rect(0, 0, 0, 0);\n  white-space: nowrap;\n  border-width: 0;\n}\n\n.absolute {\n  position: absolute;\n}\n\n.overflow-visible {\n  overflow: visible;\n}\n\n.visible {\n  overflow: visible;\n}\n\n.fit {\n  width: fit-content;\n}\n\n\n/* Layout */\n.page {\n  display: flex;\n  flex-direction: column;\n  height: 100%;\n}\n\n.top-hero {\n  height: 200px;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  background-image: conic-gradient(\n      from 90deg at -10% 100%,\n      #2b303b 0deg,\n      #2b303b 90deg,\n      #16181d 1turn\n  );\n}\n\n.bottom {\n  flex: 1;\n  overflow: auto;\n}\n\n.top-nav {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  margin-bottom: 0;\n  padding: 0 12px;\n  top: 0;\n  width: 100%;\n  height: 44px;\n  color: #23272f;\n  font-weight: 700;\n  font-size: 20px;\n  z-index: 100;\n  cursor: default;\n}\n\n.content {\n  padding: 0 12px;\n  margin-top: 4px;\n}\n\n\n.loader {\n  color: #23272f;\n  font-size: 3px;\n  width: 1em;\n  margin-right: 18px;\n  height: 1em;\n  border-radius: 50%;\n  position: relative;\n  text-indent: -9999em;\n  animation: loading-spinner 1.3s infinite linear;\n  animation-delay: 200ms;\n  transform: translateZ(0);\n}\n\n@keyframes loading-spinner {\n  0%,\n  100% {\n    box-shadow: 0 -3em 0 0.2em,\n    2em -2em 0 0em, 3em 0 0 -1em,\n    2em 2em 0 -1em, 0 3em 0 -1em,\n    -2em 2em 0 -1em, -3em 0 0 -1em,\n    -2em -2em 0 0;\n  }\n  12.5% {\n    box-shadow: 0 -3em 0 0, 2em -2em 0 0.2em,\n    3em 0 0 0, 2em 2em 0 -1em, 0 3em 0 -1em,\n    -2em 2em 0 -1em, -3em 0 0 -1em,\n    -2em -2em 0 -1em;\n  }\n  25% {\n    box-shadow: 0 -3em 0 -0.5em,\n    2em -2em 0 0, 3em 0 0 0.2em,\n    2em 2em 0 0, 0 3em 0 -1em,\n    -2em 2em 0 -1em, -3em 0 0 -1em,\n    -2em -2em 0 -1em;\n  }\n  37.5% {\n    box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em,\n    3em 0em 0 0, 2em 2em 0 0.2em, 0 3em 0 0em,\n    -2em 2em 0 -1em, -3em 0em 0 -1em, -2em -2em 0 -1em;\n  }\n  50% {\n    box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em,\n    3em 0 0 -1em, 2em 2em 0 0em, 0 3em 0 0.2em,\n    -2em 2em 0 0, -3em 0em 0 -1em, -2em -2em 0 -1em;\n  }\n  62.5% {\n    box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em,\n    3em 0 0 -1em, 2em 2em 0 -1em, 0 3em 0 0,\n    -2em 2em 0 0.2em, -3em 0 0 0, -2em -2em 0 -1em;\n  }\n  75% {\n    box-shadow: 0em -3em 0 -1em, 2em -2em 0 -1em,\n    3em 0em 0 -1em, 2em 2em 0 -1em, 0 3em 0 -1em,\n    -2em 2em 0 0, -3em 0em 0 0.2em, -2em -2em 0 0;\n  }\n  87.5% {\n    box-shadow: 0em -3em 0 0, 2em -2em 0 -1em,\n    3em 0 0 -1em, 2em 2em 0 -1em, 0 3em 0 -1em,\n    -2em 2em 0 0, -3em 0em 0 0, -2em -2em 0 0.2em;\n  }\n}\n\n/* LikeButton */\n.like-button {\n  outline-offset: 2px;\n  position: relative;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  width: 2.5rem;\n  height: 2.5rem;\n  cursor: pointer;\n  border-radius: 9999px;\n  border: none;\n  outline: none 2px;\n  color: #5e687e;\n  background: none;\n}\n\n.like-button:focus {\n  color: #a6423a;\n  background-color: rgba(166, 66, 58, .05);\n}\n\n.like-button:active {\n  color: #a6423a;\n  background-color: rgba(166, 66, 58, .05);\n  transform: scaleX(0.95) scaleY(0.95);\n}\n\n.like-button:hover {\n  background-color: #f6f7f9;\n}\n\n.like-button.liked {\n  color: #a6423a;\n}\n\n/* Icons */\n@keyframes circle {\n  0% {\n    transform: scale(0);\n    stroke-width: 16px;\n  }\n\n  50% {\n    transform: scale(.5);\n    stroke-width: 16px;\n  }\n\n  to {\n    transform: scale(1);\n    stroke-width: 0;\n  }\n}\n\n.circle {\n  color: rgba(166, 66, 58, .5);\n  transform-origin: center;\n  transition-property: all;\n  transition-duration: .15s;\n  transition-timing-function: cubic-bezier(.4,0,.2,1);\n}\n\n.circle.liked.animate {\n  animation: circle .3s forwards;\n}\n\n.heart {\n  width: 1.5rem;\n  height: 1.5rem;\n}\n\n.heart.liked {\n  transform-origin: center;\n  transition-property: all;\n  transition-duration: .15s;\n  transition-timing-function: cubic-bezier(.4, 0, .2, 1);\n}\n\n.heart.liked.animate {\n  animation: scale .35s ease-in-out forwards;\n}\n\n.control-icon {\n  color: hsla(0, 0%, 100%, .5);\n  filter:  drop-shadow(0 20px 13px rgba(0, 0, 0, .03)) drop-shadow(0 8px 5px rgba(0, 0, 0, .08));\n}\n\n.chevron-left {\n  margin-top: 2px;\n  rotate: 90deg;\n}\n\n\n/* Video */\n.thumbnail {\n  position: relative;\n  aspect-ratio: 16 / 9;\n  display: flex;\n  overflow: hidden;\n  flex-direction: column;\n  justify-content: center;\n  align-items: center;\n  border-radius: 0.5rem;\n  outline-offset: 2px;\n  width: 8rem;\n  vertical-align: middle;\n  background-color: #ffffff;\n  background-size: cover;\n  user-select: none;\n}\n\n.thumbnail.blue {\n  background-image: conic-gradient(at top right, #c76a15, #087ea4, #2b3491);\n}\n\n.thumbnail.red {\n  background-image: conic-gradient(at top right, #c76a15, #a6423a, #2b3491);\n}\n\n.thumbnail.green {\n  background-image: conic-gradient(at top right, #c76a15, #388f7f, #2b3491);\n}\n\n.thumbnail.purple {\n  background-image: conic-gradient(at top right, #c76a15, #575fb7, #2b3491);\n}\n\n.thumbnail.yellow {\n  background-image: conic-gradient(at top right, #c76a15, #FABD62, #2b3491);\n}\n\n.thumbnail.gray {\n  background-image: conic-gradient(at top right, #c76a15, #4E5769, #2b3491);\n}\n\n.video {\n  display: flex;\n  flex-direction: row;\n  gap: 0.75rem;\n  align-items: center;\n}\n\n.video .link {\n  display: flex;\n  flex-direction: row;\n  flex: 1 1 0;\n  gap: 0.125rem;\n  outline-offset: 4px;\n  cursor: pointer;\n}\n\n.video .info {\n  display: flex;\n  flex-direction: column;\n  justify-content: center;\n  margin-left: 8px;\n  gap: 0.125rem;\n}\n\n.video .info:hover {\n  text-decoration: underline;\n}\n\n.video-title {\n  font-size: 15px;\n  line-height: 1.25;\n  font-weight: 700;\n  color: #23272f;\n}\n\n.video-description {\n  color: #5e687e;\n  font-size: 13px;\n}\n\n/* Details */\n.details .thumbnail {\n  position: relative;\n  aspect-ratio: 16 / 9;\n  display: flex;\n  overflow: hidden;\n  flex-direction: column;\n  justify-content: center;\n  align-items: center;\n  border-radius: 0.5rem;\n  outline-offset: 2px;\n  width: 100%;\n  vertical-align: middle;\n  background-color: #ffffff;\n  background-size: cover;\n  user-select: none;\n}\n\n.video-details-title {\n  margin-top: 8px;\n}\n\n.video-details-speaker {\n  display: flex;\n  gap: 8px;\n  margin-top: 10px\n}\n\n.back {\n  display: flex;\n  align-items: center;\n  margin-left: -5px;\n  cursor: pointer;\n}\n\n.back:hover {\n  text-decoration: underline;\n}\n\n.info-title {\n  font-size: 1.5rem;\n  font-weight: 700;\n  line-height: 1.25;\n  margin: 8px 0 0 0 ;\n}\n\n.info-description {\n  margin: 8px 0 0 0;\n}\n\n.controls {\n  cursor: pointer;\n}\n\n.fallback {\n  background: #f6f7f8 linear-gradient(to right, #e6e6e6 5%, #cccccc 25%, #e6e6e6 35%) no-repeat;\n  background-size: 800px 104px;\n  display: block;\n  line-height: 1.25;\n  margin: 8px 0 0 0;\n  border-radius: 5px;\n  overflow: hidden;\n\n  animation: 1s linear 1s infinite shimmer;\n  animation-delay: 300ms;\n  animation-duration: 1s;\n  animation-fill-mode: forwards;\n  animation-iteration-count: infinite;\n  animation-name: shimmer;\n  animation-timing-function: linear;\n}\n\n\n.fallback.title {\n  width: 130px;\n  height: 30px;\n\n}\n\n.fallback.description {\n  width: 150px;\n  height: 21px;\n}\n\n@keyframes shimmer {\n  0% {\n    background-position: -468px 0;\n  }\n\n  100% {\n    background-position: 468px 0;\n  }\n}\n\n.search {\n  margin-bottom: 10px;\n}\n.search-input {\n  width: 100%;\n  position: relative;\n}\n\n.search-icon {\n  position: absolute;\n  top: 0;\n  bottom: 0;\n  inset-inline-start: 0;\n  display: flex;\n  align-items: center;\n  padding-inline-start: 1rem;\n  pointer-events: none;\n  color: #99a1b3;\n}\n\n.search-input input {\n  display: flex;\n  padding-inline-start: 2.75rem;\n  padding-top: 10px;\n  padding-bottom: 10px;\n  width: 100%;\n  text-align: start;\n  background-color: rgb(235 236 240);\n  outline: 2px solid transparent;\n  cursor: pointer;\n  border: none;\n  align-items: center;\n  color: rgb(35 39 47);\n  border-radius: 9999px;\n  vertical-align: middle;\n  font-size: 15px;\n}\n\n.search-input input:hover, .search-input input:active {\n  background-color: rgb(235 236 240/ 0.8);\n  color: rgb(35 39 47/ 0.8);\n}\n\n/* Home */\n.video-list {\n  position: relative;\n}\n\n.video-list .videos {\n  display: flex;\n  flex-direction: column;\n  gap: 1rem;\n  overflow-y: auto;\n  height: 100%;\n}\n```\n\n\n```css src/animations.css\n/* 추가 애니메이션은 필요하지 않습니다 */\n\n\n\n\n\n\n\n\n\n/* 이전에 정의된 애니메이션은 아래에 있습니다 */\n\n\n\n\n\n::view-transition-old(.slow-fade) {\n    animation-duration: 500ms;\n}\n\n::view-transition-new(.slow-fade) {\n    animation-duration: 500ms;\n}\n```\n\n```js src/index.js hidden\nimport React, {StrictMode} from 'react';\nimport {createRoot} from 'react-dom/client';\nimport './styles.css';\nimport './animations.css';\n\nimport App from './App';\nimport {Router} from './router';\n\nconst root = createRoot(document.getElementById('root'));\nroot.render(\n  <StrictMode>\n    <Router>\n      <App />\n    </Router>\n  </StrictMode>\n);\n```\n\n```json package.json hidden\n{\n  \"dependencies\": {\n    \"react\": \"canary\",\n    \"react-dom\": \"canary\",\n    \"react-scripts\": \"latest\"\n  },\n  \"scripts\": {\n    \"start\": \"react-scripts start\",\n    \"build\": \"react-scripts build\",\n    \"test\": \"react-scripts test --env=jsdom\",\n    \"eject\": \"react-scripts eject\"\n  }\n}\n```\n\n</Sandpack>\n\n기본적으로 React는 Transition에 활성화된 각 요소에 대해 고유한 `name`을 자동으로 생성합니다. ([`<ViewTransition>`이 어떻게 동작하는지 참고하세요.](/reference/react/ViewTransition#how-does-viewtransition-work)) React가 어떤 Transition에서 특정 `name`을 가진 `<ViewTransition>`이 제거되고, 동일한 `name`을 가진 새로운 `<ViewTransition>`이 추가된 것을 감지하면 공유 요소 전환<sup>Shared Element Transition</sup>을 활성화합니다.\n\n자세한 내용은 [Animating a Shared Element](/reference/react/ViewTransition#animating-a-shared-element) 문서를 참고하세요.\n\n### 원인에 따라 애니메이션 적용하기 {/*animating-based-on-cause*/}\n\n때로는 트리거된 방식에 따라 요소의 애니메이션을 다르게 적용하고 싶을 때가 있습니다. 이 사용 사례의 경우 전환의 원인을 지정하기 위해 `addTransitionType`이라는 새로운 API를 추가했습니다.\n\n```js {4,11}\nfunction navigate(url) {\n  startTransition(() => {\n    // 전환 원인이 \"nav forward\"인 경우의 트랜지션 타입\n    addTransitionType('nav-forward');\n    go(url);\n  });\n}\nfunction navigateBack(url) {\n  startTransition(() => {\n    // 전환 원인이 \"nav backward\"인 경우의 트랜지션 타입\n    addTransitionType('nav-back');\n    go(url);\n  });\n}\n```\n\nTransition Types을 사용하면 `<ViewTransition>`에 Props를 통해 커스텀 애니메이션을 제공할 수 있습니다. \"6 Videos\" 와 \"Back\" 헤더에 공유 엘리먼트 Transition을 추가해 보겠습니다.\n\n```js {4,5}\n<ViewTransition\n  name=\"nav\"\n  share={{\n    'nav-forward': 'slide-forward',\n    'nav-back': 'slide-back',\n  }}>\n  {heading}\n</ViewTransition>\n```\n\n여기에서는 `share` Prop을 전달하여 Transition Type에 따라 어떻게 애니메이션을 적용할지 정의합니다. `nav-forward`로 인해 공통 Transition이 활성화되면, View Transition 클래스인 `slide-forward`가 적용됩니다. `nav-back`로 인해 활성화되면, `slide-back` 애니메이션이 활성화됩니다. CSS에서 이러한 애니메이션을 정의해 보겠습니다.\n\n```css\n::view-transition-old(.slide-forward) {\n    /* 앞으로 전환할 때 \"old\" 페이지는 왼쪽으로 슬라이드되어 나가야 합니다. */\n    animation: ...\n}\n\n::view-transition-new(.slide-forward) {\n    /* 앞으로 전환할 때 \"new\" 페이지는 오른쪽에서 슬라이드되어 들어와야 합니다. */\n    animation: ...\n}\n\n::view-transition-old(.slide-back) {\n    /* 뒤로 전환할 때 \"old\" 페이지는 오른쪽으로 슬라이드되어 나가야 합니다. */\n    animation: ...\n}\n\n::view-transition-new(.slide-back) {\n    /* 뒤로 전환할 때 \"new\" 페이지는 왼쪽에서 슬라이드되어 들어와야 합니다. */\n    animation: ...\n}\n```\n\n이제 Navigation Type에 따라 썸네일과 헤더에 애니메이션을 적용할 수 있습니다.\n\n<Sandpack>\n\n```js src/App.js hidden\nimport { ViewTransition } from \"react\";\nimport Details from \"./Details\";\nimport Home from \"./Home\";\nimport { useRouter } from \"./router\";\n\nexport default function App() {\n  const { url } = useRouter();\n\n  // Keeping our default slow-fade.\n  return (\n    <ViewTransition default=\"slow-fade\">\n      {url === \"/\" ? <Home /> : <Details />}\n    </ViewTransition>\n  );\n}\n```\n\n```js src/Details.js hidden\nimport { fetchVideo, fetchVideoDetails } from \"./data\";\nimport { Thumbnail, VideoControls } from \"./Videos\";\nimport { useRouter } from \"./router\";\nimport Layout from \"./Layout\";\nimport { use, Suspense } from \"react\";\nimport { ChevronLeft } from \"./Icons\";\n\nfunction VideoInfo({ id }) {\n  const details = use(fetchVideoDetails(id));\n  return (\n    <>\n      <p className=\"info-title\">{details.title}</p>\n      <p className=\"info-description\">{details.description}</p>\n    </>\n  );\n}\n\nfunction VideoInfoFallback() {\n  return (\n    <>\n      <div className=\"fallback title\"></div>\n      <div className=\"fallback description\"></div>\n    </>\n  );\n}\n\nexport default function Details() {\n  const { url, navigateBack } = useRouter();\n  const videoId = url.split(\"/\").pop();\n  const video = use(fetchVideo(videoId));\n\n  return (\n    <Layout\n      heading={\n        <div\n          className=\"fit back\"\n          onClick={() => {\n            navigateBack(\"/\");\n          }}\n        >\n          <ChevronLeft /> Back\n        </div>\n      }\n    >\n      <div className=\"details\">\n        <Thumbnail video={video} large>\n          <VideoControls />\n        </Thumbnail>\n        <Suspense fallback={<VideoInfoFallback />}>\n          <VideoInfo id={video.id} />\n        </Suspense>\n      </div>\n    </Layout>\n  );\n}\n\n```\n\n```js src/Home.js hidden\nimport { Video } from \"./Videos\";\nimport Layout from \"./Layout\";\nimport { fetchVideos } from \"./data\";\nimport { useId, useState, use } from \"react\";\nimport { IconSearch } from \"./Icons\";\n\nfunction SearchInput({ value, onChange }) {\n  const id = useId();\n  return (\n    <form className=\"search\" onSubmit={(e) => e.preventDefault()}>\n      <label htmlFor={id} className=\"sr-only\">\n        Search\n      </label>\n      <div className=\"search-input\">\n        <div className=\"search-icon\">\n          <IconSearch />\n        </div>\n        <input\n          type=\"text\"\n          id={id}\n          placeholder=\"Search\"\n          value={value}\n          onChange={(e) => onChange(e.target.value)}\n        />\n      </div>\n    </form>\n  );\n}\n\nfunction filterVideos(videos, query) {\n  const keywords = query\n    .toLowerCase()\n    .split(\" \")\n    .filter((s) => s !== \"\");\n  if (keywords.length === 0) {\n    return videos;\n  }\n  return videos.filter((video) => {\n    const words = (video.title + \" \" + video.description)\n      .toLowerCase()\n      .split(\" \");\n    return keywords.every((kw) => words.some((w) => w.includes(kw)));\n  });\n}\n\nexport default function Home() {\n  const videos = use(fetchVideos());\n  const count = videos.length;\n  const [searchText, setSearchText] = useState(\"\");\n  const foundVideos = filterVideos(videos, searchText);\n  return (\n    <Layout heading={<div className=\"fit\">{count} Videos</div>}>\n      <SearchInput value={searchText} onChange={setSearchText} />\n      <div className=\"video-list\">\n        {foundVideos.length === 0 && (\n          <div className=\"no-results\">No results</div>\n        )}\n        <div className=\"videos\">\n          {foundVideos.map((video) => (\n            <Video key={video.id} video={video} />\n          ))}\n        </div>\n      </div>\n    </Layout>\n  );\n}\n\n```\n\n```js src/Icons.js hidden\nexport function ChevronLeft() {\n  return (\n    <svg\n      className=\"chevron-left\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width=\"20\"\n      height=\"20\"\n      viewBox=\"0 0 20 20\">\n      <g fill=\"none\" fillRule=\"evenodd\" transform=\"translate(-446 -398)\">\n        <path\n          fill=\"currentColor\"\n          fillRule=\"nonzero\"\n          d=\"M95.8838835,240.366117 C95.3957281,239.877961 94.6042719,239.877961 94.1161165,240.366117 C93.6279612,240.854272 93.6279612,241.645728 94.1161165,242.133883 L98.6161165,246.633883 C99.1042719,247.122039 99.8957281,247.122039 100.383883,246.633883 L104.883883,242.133883 C105.372039,241.645728 105.372039,240.854272 104.883883,240.366117 C104.395728,239.877961 103.604272,239.877961 103.116117,240.366117 L99.5,243.982233 L95.8838835,240.366117 Z\"\n          transform=\"translate(356.5 164.5)\"\n        />\n        <polygon points=\"446 418 466 418 466 398 446 398\" />\n      </g>\n    </svg>\n  );\n}\n\nexport function PauseIcon() {\n  return (\n    <svg\n      className=\"control-icon\"\n      style={{padding: '4px'}}\n      width=\"100\"\n      height=\"100\"\n      viewBox=\"0 0 512 512\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\">\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M256 0C114.617 0 0 114.615 0 256s114.617 256 256 256 256-114.615 256-256S397.383 0 256 0zm-32 320c0 8.836-7.164 16-16 16h-32c-8.836 0-16-7.164-16-16V192c0-8.836 7.164-16 16-16h32c8.836 0 16 7.164 16 16v128zm128 0c0 8.836-7.164 16-16 16h-32c-8.836 0-16-7.164-16-16V192c0-8.836 7.164-16 16-16h32c8.836 0 16 7.164 16 16v128z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  );\n}\n\nexport function PlayIcon() {\n  return (\n    <svg\n      className=\"control-icon\"\n      width=\"100\"\n      height=\"100\"\n      viewBox=\"0 0 72 72\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\">\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M36 69C54.2254 69 69 54.2254 69 36C69 17.7746 54.2254 3 36 3C17.7746 3 3 17.7746 3 36C3 54.2254 17.7746 69 36 69ZM52.1716 38.6337L28.4366 51.5801C26.4374 52.6705 24 51.2235 24 48.9464V23.0536C24 20.7764 26.4374 19.3295 28.4366 20.4199L52.1716 33.3663C54.2562 34.5034 54.2562 37.4966 52.1716 38.6337Z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  );\n}\nexport function Heart({liked, animate}) {\n  return (\n    <>\n      <svg\n        className=\"absolute overflow-visible\"\n        viewBox=\"0 0 24 24\"\n        fill=\"none\"\n        xmlns=\"http://www.w3.org/2000/svg\">\n        <circle\n          className={`circle ${liked ? 'liked' : ''} ${animate ? 'animate' : ''}`}\n          cx=\"12\"\n          cy=\"12\"\n          r=\"11.5\"\n          fill=\"transparent\"\n          strokeWidth=\"0\"\n          stroke=\"currentColor\"\n        />\n      </svg>\n\n      <svg\n        className={`heart ${liked ? 'liked' : ''} ${animate ? 'animate' : ''}`}\n        viewBox=\"0 0 24 24\"\n        fill=\"none\"\n        xmlns=\"http://www.w3.org/2000/svg\">\n        {liked ? (\n          <path\n            d=\"M12 23a.496.496 0 0 1-.26-.074C7.023 19.973 0 13.743 0 8.68c0-4.12 2.322-6.677 6.058-6.677 2.572 0 5.108 2.387 5.134 2.41l.808.771.808-.771C12.834 4.387 15.367 2 17.935 2 21.678 2 24 4.558 24 8.677c0 5.06-7.022 11.293-11.74 14.246a.496.496 0 0 1-.26.074V23z\"\n            fill=\"currentColor\"\n          />\n        ) : (\n          <path\n            fillRule=\"evenodd\"\n            clipRule=\"evenodd\"\n            d=\"m12 5.184-.808-.771-.004-.004C11.065 4.299 8.522 2.003 6 2.003c-3.736 0-6 2.558-6 6.677 0 4.47 5.471 9.848 10 13.079.602.43 1.187.82 1.74 1.167A.497.497 0 0 0 12 23v-.003c.09 0 .182-.026.26-.074C16.977 19.97 24 13.737 24 8.677 24 4.557 21.743 2 18 2c-2.569 0-5.166 2.387-5.192 2.413L12 5.184zm-.002 15.525c2.071-1.388 4.477-3.342 6.427-5.47C20.72 12.733 22 10.401 22 8.677c0-1.708-.466-2.855-1.087-3.55C20.316 4.459 19.392 4 18 4c-.726 0-1.63.364-2.5.9-.67.412-1.148.82-1.266.92-.03.025-.037.031-.019.014l-.013.013L12 7.949 9.832 5.88a10.08 10.08 0 0 0-1.33-.977C7.633 4.367 6.728 4.003 6 4.003c-1.388 0-2.312.459-2.91 1.128C2.466 5.826 2 6.974 2 8.68c0 1.726 1.28 4.058 3.575 6.563 1.948 2.127 4.352 4.078 6.423 5.466z\"\n            fill=\"currentColor\"\n          />\n        )}\n      </svg>\n    </>\n  );\n}\n\nexport function IconSearch(props) {\n  return (\n    <svg width=\"1em\" height=\"1em\" viewBox=\"0 0 20 20\">\n      <path\n        d=\"M14.386 14.386l4.0877 4.0877-4.0877-4.0877c-2.9418 2.9419-7.7115 2.9419-10.6533 0-2.9419-2.9418-2.9419-7.7115 0-10.6533 2.9418-2.9419 7.7115-2.9419 10.6533 0 2.9419 2.9418 2.9419 7.7115 0 10.6533z\"\n        stroke=\"currentColor\"\n        fill=\"none\"\n        strokeWidth=\"2\"\n        fillRule=\"evenodd\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"></path>\n    </svg>\n  );\n}\n```\n\n```js src/Layout.js active\nimport {ViewTransition} from 'react'; import { useIsNavPending } from \"./router\";\n\nexport default function Page({ heading, children }) {\n  const isPending = useIsNavPending();\n  return (\n    <div className=\"page\">\n      <div className=\"top\">\n        <div className=\"top-nav\">\n          {/* 트랜지션 타입에 따라 커스텀 클래스를 적용합니다. */}\n          <ViewTransition\n            name=\"nav\"\n            share={{\n              'nav-forward': 'slide-forward',\n              'nav-back': 'slide-back',\n            }}>\n            {heading}\n          </ViewTransition>\n          {isPending && <span className=\"loader\"></span>}\n        </div>\n      </div>\n      {/* 콘텐츠에 대해 ViewTransition을 사용하지 않습니다. */}\n      {/* 콘텐츠는 자체 ViewTransition을 정의할 수 있습니다. */}\n      <ViewTransition default=\"none\">\n        <div className=\"bottom\">\n          <div className=\"content\">{children}</div>\n        </div>\n      </ViewTransition>\n    </div>\n  );\n}\n```\n\n```js src/LikeButton.js hidden\nimport {useState} from 'react';\nimport {Heart} from './Icons';\n\n// A hack since we don't actually have a backend.\n// Unlike local state, this survives videos being filtered.\nconst likedVideos = new Set();\n\nexport default function LikeButton({video}) {\n  const [isLiked, setIsLiked] = useState(() => likedVideos.has(video.id));\n  const [animate, setAnimate] = useState(false);\n  return (\n    <button\n      className={`like-button ${isLiked && 'liked'}`}\n      aria-label={isLiked ? 'Unsave' : 'Save'}\n      onClick={() => {\n        const nextIsLiked = !isLiked;\n        if (nextIsLiked) {\n          likedVideos.add(video.id);\n        } else {\n          likedVideos.delete(video.id);\n        }\n        setAnimate(true);\n        setIsLiked(nextIsLiked);\n      }}>\n      <Heart liked={isLiked} animate={animate} />\n    </button>\n  );\n}\n```\n\n```js src/Videos.js hidden\nimport { useState, ViewTransition } from \"react\";\nimport LikeButton from \"./LikeButton\";\nimport { useRouter } from \"./router\";\nimport { PauseIcon, PlayIcon } from \"./Icons\";\nimport { startTransition } from \"react\";\n\nexport function Thumbnail({ video, children }) {\n  // 공유 요소 트랜지션으로 애니메이션되도록 name을 추가합니다.\n  // 기본 애니메이션을 사용하므로 추가 CSS가 필요하지 않습니다.\n  return (\n    <ViewTransition name={`video-${video.id}`}>\n      <div\n        aria-hidden=\"true\"\n        tabIndex={-1}\n        className={`thumbnail ${video.image}`}\n      >\n        {children}\n      </div>\n    </ViewTransition>\n  );\n}\n\nexport function VideoControls() {\n  const [isPlaying, setIsPlaying] = useState(false);\n\n  return (\n    <span\n      className=\"controls\"\n      onClick={() =>\n        startTransition(() => {\n          setIsPlaying((p) => !p);\n        })\n      }\n    >\n      {isPlaying ? <PauseIcon /> : <PlayIcon />}\n    </span>\n  );\n}\n\nexport function Video({ video }) {\n  const { navigate } = useRouter();\n\n  return (\n    <div className=\"video\">\n      <div\n        className=\"link\"\n        onClick={(e) => {\n          e.preventDefault();\n          navigate(`/video/${video.id}`);\n        }}\n      >\n        <Thumbnail video={video}></Thumbnail>\n\n        <div className=\"info\">\n          <div className=\"video-title\">{video.title}</div>\n          <div className=\"video-description\">{video.description}</div>\n        </div>\n      </div>\n      <LikeButton video={video} />\n    </div>\n  );\n}\n```\n\n\n```js src/data.js hidden\nconst videos = [\n  {\n    id: '1',\n    title: 'First video',\n    description: 'Video description',\n    image: 'blue',\n  },\n  {\n    id: '2',\n    title: 'Second video',\n    description: 'Video description',\n    image: 'red',\n  },\n  {\n    id: '3',\n    title: 'Third video',\n    description: 'Video description',\n    image: 'green',\n  },\n  {\n    id: '4',\n    title: 'Fourth video',\n    description: 'Video description',\n    image: 'purple',\n  },\n  {\n    id: '5',\n    title: 'Fifth video',\n    description: 'Video description',\n    image: 'yellow',\n  },\n  {\n    id: '6',\n    title: 'Sixth video',\n    description: 'Video description',\n    image: 'gray',\n  },\n];\n\nlet videosCache = new Map();\nlet videoCache = new Map();\nlet videoDetailsCache = new Map();\nconst VIDEO_DELAY = 1;\nconst VIDEO_DETAILS_DELAY = 1000;\nexport function fetchVideos() {\n  if (videosCache.has(0)) {\n    return videosCache.get(0);\n  }\n  const promise = new Promise((resolve) => {\n    setTimeout(() => {\n      resolve(videos);\n    }, VIDEO_DELAY);\n  });\n  videosCache.set(0, promise);\n  return promise;\n}\n\nexport function fetchVideo(id) {\n  if (videoCache.has(id)) {\n    return videoCache.get(id);\n  }\n  const promise = new Promise((resolve) => {\n    setTimeout(() => {\n      resolve(videos.find((video) => video.id === id));\n    }, VIDEO_DELAY);\n  });\n  videoCache.set(id, promise);\n  return promise;\n}\n\nexport function fetchVideoDetails(id) {\n  if (videoDetailsCache.has(id)) {\n    return videoDetailsCache.get(id);\n  }\n  const promise = new Promise((resolve) => {\n    setTimeout(() => {\n      resolve(videos.find((video) => video.id === id));\n    }, VIDEO_DETAILS_DELAY);\n  });\n  videoDetailsCache.set(id, promise);\n  return promise;\n}\n```\n\n```js src/router.js\nimport {useState, createContext, use, useTransition, useLayoutEffect, useEffect, addTransitionType} from \"react\";\n\nexport function Router({ children }) {\n  const [isPending, startTransition] = useTransition();\n\n  function navigate(url) {\n    startTransition(() => {\n      // 전환 원인이 \"nav forward\"인 경우의 트랜지션 타입\n      addTransitionType('nav-forward');\n      go(url);\n    });\n  }\n  function navigateBack(url) {\n    startTransition(() => {\n      // 전환 원인이 \"nav backward\"인 경우의 트랜지션 타입\n      addTransitionType('nav-back');\n      go(url);\n    });\n  }\n\n\n  const [routerState, setRouterState] = useState({pendingNav: () => {}, url: document.location.pathname});\n\n  function go(url) {\n    setRouterState({\n      url,\n      pendingNav() {\n        window.history.pushState({}, \"\", url);\n      },\n    });\n  }\n\n  useEffect(() => {\n    function handlePopState() {\n      // 복원은 동기적으로 이루어져야 하므로 애니메이션되면 안 됩니다.\n      // 트랜지션이더라도 마찬가지입니다.\n      startTransition(() => {\n        setRouterState({\n          url: document.location.pathname + document.location.search,\n          pendingNav() {\n            // 아무 작업도 하지 않습니다. URL은 이미 업데이트되었습니다.\n          },\n        });\n      });\n    }\n    window.addEventListener(\"popstate\", handlePopState);\n    return () => {\n      window.removeEventListener(\"popstate\", handlePopState);\n    };\n  }, []);\n  const pendingNav = routerState.pendingNav;\n  useLayoutEffect(() => {\n    pendingNav();\n  }, [pendingNav]);\n\n  return (\n    <RouterContext\n      value={{\n        url: routerState.url,\n        navigate,\n        navigateBack,\n        isPending,\n        params: {},\n      }}\n    >\n      {children}\n    </RouterContext>\n  );\n}\n\nconst RouterContext = createContext({ url: \"/\", params: {} });\n\nexport function useRouter() {\n  return use(RouterContext);\n}\n\nexport function useIsNavPending() {\n  return use(RouterContext).isPending;\n}\n\n```\n\n```css src/styles.css hidden\n@font-face {\n  font-family: Optimistic Text;\n  src: url(https://react.dev/fonts/Optimistic_Text_W_Rg.woff2) format(\"woff2\");\n  font-weight: 400;\n  font-style: normal;\n  font-display: swap;\n}\n\n@font-face {\n  font-family: Optimistic Text;\n  src: url(https://react.dev/fonts/Optimistic_Text_W_Md.woff2) format(\"woff2\");\n  font-weight: 500;\n  font-style: normal;\n  font-display: swap;\n}\n\n@font-face {\n  font-family: Optimistic Text;\n  src: url(https://react.dev/fonts/Optimistic_Text_W_Bd.woff2) format(\"woff2\");\n  font-weight: 600;\n  font-style: normal;\n  font-display: swap;\n}\n\n@font-face {\n  font-family: Optimistic Text;\n  src: url(https://react.dev/fonts/Optimistic_Text_W_Bd.woff2) format(\"woff2\");\n  font-weight: 700;\n  font-style: normal;\n  font-display: swap;\n}\n\n* {\n  box-sizing: border-box;\n}\n\nhtml {\n  background-image: url(https://react.dev/images/meta-gradient-dark.png);\n  background-size: 100%;\n  background-position: -100%;\n  background-color: rgb(64 71 86);\n  background-repeat: no-repeat;\n  height: 100%;\n  width: 100%;\n}\n\nbody {\n  font-family: Optimistic Text, -apple-system, ui-sans-serif, system-ui, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;\n  padding: 10px 0 10px 0;\n  margin: 0;\n  display: flex;\n  justify-content: center;\n}\n\n#root {\n  flex: 1 1;\n  height: auto;\n  background-color: #fff;\n  border-radius: 10px;\n  max-width: 450px;\n  min-height: 600px;\n  padding-bottom: 10px;\n}\n\nh1 {\n  margin-top: 0;\n  font-size: 22px;\n}\n\nh2 {\n  margin-top: 0;\n  font-size: 20px;\n}\n\nh3 {\n  margin-top: 0;\n  font-size: 18px;\n}\n\nh4 {\n  margin-top: 0;\n  font-size: 16px;\n}\n\nh5 {\n  margin-top: 0;\n  font-size: 14px;\n}\n\nh6 {\n  margin-top: 0;\n  font-size: 12px;\n}\n\ncode {\n  font-size: 1.2em;\n}\n\nul {\n  padding-inline-start: 20px;\n}\n\n.sr-only {\n  position: absolute;\n  width: 1px;\n  height: 1px;\n  padding: 0;\n  margin: -1px;\n  overflow: hidden;\n  clip: rect(0, 0, 0, 0);\n  white-space: nowrap;\n  border-width: 0;\n}\n\n.absolute {\n  position: absolute;\n}\n\n.overflow-visible {\n  overflow: visible;\n}\n\n.visible {\n  overflow: visible;\n}\n\n.fit {\n  width: fit-content;\n}\n\n\n/* Layout */\n.page {\n  display: flex;\n  flex-direction: column;\n  height: 100%;\n}\n\n.top-hero {\n  height: 200px;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  background-image: conic-gradient(\n      from 90deg at -10% 100%,\n      #2b303b 0deg,\n      #2b303b 90deg,\n      #16181d 1turn\n  );\n}\n\n.bottom {\n  flex: 1;\n  overflow: auto;\n}\n\n.top-nav {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  margin-bottom: 0;\n  padding: 0 12px;\n  top: 0;\n  width: 100%;\n  height: 44px;\n  color: #23272f;\n  font-weight: 700;\n  font-size: 20px;\n  z-index: 100;\n  cursor: default;\n}\n\n.content {\n  padding: 0 12px;\n  margin-top: 4px;\n}\n\n\n.loader {\n  color: #23272f;\n  font-size: 3px;\n  width: 1em;\n  margin-right: 18px;\n  height: 1em;\n  border-radius: 50%;\n  position: relative;\n  text-indent: -9999em;\n  animation: loading-spinner 1.3s infinite linear;\n  animation-delay: 200ms;\n  transform: translateZ(0);\n}\n\n@keyframes loading-spinner {\n  0%,\n  100% {\n    box-shadow: 0 -3em 0 0.2em,\n    2em -2em 0 0em, 3em 0 0 -1em,\n    2em 2em 0 -1em, 0 3em 0 -1em,\n    -2em 2em 0 -1em, -3em 0 0 -1em,\n    -2em -2em 0 0;\n  }\n  12.5% {\n    box-shadow: 0 -3em 0 0, 2em -2em 0 0.2em,\n    3em 0 0 0, 2em 2em 0 -1em, 0 3em 0 -1em,\n    -2em 2em 0 -1em, -3em 0 0 -1em,\n    -2em -2em 0 -1em;\n  }\n  25% {\n    box-shadow: 0 -3em 0 -0.5em,\n    2em -2em 0 0, 3em 0 0 0.2em,\n    2em 2em 0 0, 0 3em 0 -1em,\n    -2em 2em 0 -1em, -3em 0 0 -1em,\n    -2em -2em 0 -1em;\n  }\n  37.5% {\n    box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em,\n    3em 0em 0 0, 2em 2em 0 0.2em, 0 3em 0 0em,\n    -2em 2em 0 -1em, -3em 0em 0 -1em, -2em -2em 0 -1em;\n  }\n  50% {\n    box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em,\n    3em 0 0 -1em, 2em 2em 0 0em, 0 3em 0 0.2em,\n    -2em 2em 0 0, -3em 0em 0 -1em, -2em -2em 0 -1em;\n  }\n  62.5% {\n    box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em,\n    3em 0 0 -1em, 2em 2em 0 -1em, 0 3em 0 0,\n    -2em 2em 0 0.2em, -3em 0 0 0, -2em -2em 0 -1em;\n  }\n  75% {\n    box-shadow: 0em -3em 0 -1em, 2em -2em 0 -1em,\n    3em 0em 0 -1em, 2em 2em 0 -1em, 0 3em 0 -1em,\n    -2em 2em 0 0, -3em 0em 0 0.2em, -2em -2em 0 0;\n  }\n  87.5% {\n    box-shadow: 0em -3em 0 0, 2em -2em 0 -1em,\n    3em 0 0 -1em, 2em 2em 0 -1em, 0 3em 0 -1em,\n    -2em 2em 0 0, -3em 0em 0 0, -2em -2em 0 0.2em;\n  }\n}\n\n/* LikeButton */\n.like-button {\n  outline-offset: 2px;\n  position: relative;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  width: 2.5rem;\n  height: 2.5rem;\n  cursor: pointer;\n  border-radius: 9999px;\n  border: none;\n  outline: none 2px;\n  color: #5e687e;\n  background: none;\n}\n\n.like-button:focus {\n  color: #a6423a;\n  background-color: rgba(166, 66, 58, .05);\n}\n\n.like-button:active {\n  color: #a6423a;\n  background-color: rgba(166, 66, 58, .05);\n  transform: scaleX(0.95) scaleY(0.95);\n}\n\n.like-button:hover {\n  background-color: #f6f7f9;\n}\n\n.like-button.liked {\n  color: #a6423a;\n}\n\n/* Icons */\n@keyframes circle {\n  0% {\n    transform: scale(0);\n    stroke-width: 16px;\n  }\n\n  50% {\n    transform: scale(.5);\n    stroke-width: 16px;\n  }\n\n  to {\n    transform: scale(1);\n    stroke-width: 0;\n  }\n}\n\n.circle {\n  color: rgba(166, 66, 58, .5);\n  transform-origin: center;\n  transition-property: all;\n  transition-duration: .15s;\n  transition-timing-function: cubic-bezier(.4,0,.2,1);\n}\n\n.circle.liked.animate {\n  animation: circle .3s forwards;\n}\n\n.heart {\n  width: 1.5rem;\n  height: 1.5rem;\n}\n\n.heart.liked {\n  transform-origin: center;\n  transition-property: all;\n  transition-duration: .15s;\n  transition-timing-function: cubic-bezier(.4, 0, .2, 1);\n}\n\n.heart.liked.animate {\n  animation: scale .35s ease-in-out forwards;\n}\n\n.control-icon {\n  color: hsla(0, 0%, 100%, .5);\n  filter:  drop-shadow(0 20px 13px rgba(0, 0, 0, .03)) drop-shadow(0 8px 5px rgba(0, 0, 0, .08));\n}\n\n.chevron-left {\n  margin-top: 2px;\n  rotate: 90deg;\n}\n\n\n/* Video */\n.thumbnail {\n  position: relative;\n  aspect-ratio: 16 / 9;\n  display: flex;\n  overflow: hidden;\n  flex-direction: column;\n  justify-content: center;\n  align-items: center;\n  border-radius: 0.5rem;\n  outline-offset: 2px;\n  width: 8rem;\n  vertical-align: middle;\n  background-color: #ffffff;\n  background-size: cover;\n  user-select: none;\n}\n\n.thumbnail.blue {\n  background-image: conic-gradient(at top right, #c76a15, #087ea4, #2b3491);\n}\n\n.thumbnail.red {\n  background-image: conic-gradient(at top right, #c76a15, #a6423a, #2b3491);\n}\n\n.thumbnail.green {\n  background-image: conic-gradient(at top right, #c76a15, #388f7f, #2b3491);\n}\n\n.thumbnail.purple {\n  background-image: conic-gradient(at top right, #c76a15, #575fb7, #2b3491);\n}\n\n.thumbnail.yellow {\n  background-image: conic-gradient(at top right, #c76a15, #FABD62, #2b3491);\n}\n\n.thumbnail.gray {\n  background-image: conic-gradient(at top right, #c76a15, #4E5769, #2b3491);\n}\n\n.video {\n  display: flex;\n  flex-direction: row;\n  gap: 0.75rem;\n  align-items: center;\n}\n\n.video .link {\n  display: flex;\n  flex-direction: row;\n  flex: 1 1 0;\n  gap: 0.125rem;\n  outline-offset: 4px;\n  cursor: pointer;\n}\n\n.video .info {\n  display: flex;\n  flex-direction: column;\n  justify-content: center;\n  margin-left: 8px;\n  gap: 0.125rem;\n}\n\n.video .info:hover {\n  text-decoration: underline;\n}\n\n.video-title {\n  font-size: 15px;\n  line-height: 1.25;\n  font-weight: 700;\n  color: #23272f;\n}\n\n.video-description {\n  color: #5e687e;\n  font-size: 13px;\n}\n\n/* Details */\n.details .thumbnail {\n  position: relative;\n  aspect-ratio: 16 / 9;\n  display: flex;\n  overflow: hidden;\n  flex-direction: column;\n  justify-content: center;\n  align-items: center;\n  border-radius: 0.5rem;\n  outline-offset: 2px;\n  width: 100%;\n  vertical-align: middle;\n  background-color: #ffffff;\n  background-size: cover;\n  user-select: none;\n}\n\n.video-details-title {\n  margin-top: 8px;\n}\n\n.video-details-speaker {\n  display: flex;\n  gap: 8px;\n  margin-top: 10px\n}\n\n.back {\n  display: flex;\n  align-items: center;\n  margin-left: -5px;\n  cursor: pointer;\n}\n\n.back:hover {\n  text-decoration: underline;\n}\n\n.info-title {\n  font-size: 1.5rem;\n  font-weight: 700;\n  line-height: 1.25;\n  margin: 8px 0 0 0 ;\n}\n\n.info-description {\n  margin: 8px 0 0 0;\n}\n\n.controls {\n  cursor: pointer;\n}\n\n.fallback {\n  background: #f6f7f8 linear-gradient(to right, #e6e6e6 5%, #cccccc 25%, #e6e6e6 35%) no-repeat;\n  background-size: 800px 104px;\n  display: block;\n  line-height: 1.25;\n  margin: 8px 0 0 0;\n  border-radius: 5px;\n  overflow: hidden;\n\n  animation: 1s linear 1s infinite shimmer;\n  animation-delay: 300ms;\n  animation-duration: 1s;\n  animation-fill-mode: forwards;\n  animation-iteration-count: infinite;\n  animation-name: shimmer;\n  animation-timing-function: linear;\n}\n\n\n.fallback.title {\n  width: 130px;\n  height: 30px;\n\n}\n\n.fallback.description {\n  width: 150px;\n  height: 21px;\n}\n\n@keyframes shimmer {\n  0% {\n    background-position: -468px 0;\n  }\n\n  100% {\n    background-position: 468px 0;\n  }\n}\n\n.search {\n  margin-bottom: 10px;\n}\n.search-input {\n  width: 100%;\n  position: relative;\n}\n\n.search-icon {\n  position: absolute;\n  top: 0;\n  bottom: 0;\n  inset-inline-start: 0;\n  display: flex;\n  align-items: center;\n  padding-inline-start: 1rem;\n  pointer-events: none;\n  color: #99a1b3;\n}\n\n.search-input input {\n  display: flex;\n  padding-inline-start: 2.75rem;\n  padding-top: 10px;\n  padding-bottom: 10px;\n  width: 100%;\n  text-align: start;\n  background-color: rgb(235 236 240);\n  outline: 2px solid transparent;\n  cursor: pointer;\n  border: none;\n  align-items: center;\n  color: rgb(35 39 47);\n  border-radius: 9999px;\n  vertical-align: middle;\n  font-size: 15px;\n}\n\n.search-input input:hover, .search-input input:active {\n  background-color: rgb(235 236 240/ 0.8);\n  color: rgb(35 39 47/ 0.8);\n}\n\n/* Home */\n.video-list {\n  position: relative;\n}\n\n.video-list .videos {\n  display: flex;\n  flex-direction: column;\n  gap: 1rem;\n  overflow-y: auto;\n  height: 100%;\n}\n```\n\n\n```css src/animations.css\n/* 트랜지션 타입에 의해 추가된 View Transition 클래스용 애니메이션 */\n::view-transition-old(.slide-forward) {\n    /* 앞으로 전환할 때 \"old\" 페이지는 왼쪽으로 슬라이드되어 나가야 합니다. */\n    animation: 150ms cubic-bezier(0.4, 0, 1, 1) both fade-out,\n    400ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;\n}\n\n::view-transition-new(.slide-forward) {\n    /* 앞으로 전환할 때 \"new\" 페이지는 오른쪽에서 슬라이드되어 들어와야 합니다. */\n    animation: 210ms cubic-bezier(0, 0, 0.2, 1) 150ms both fade-in,\n    400ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;\n}\n\n::view-transition-old(.slide-back) {\n    /* 뒤로 전환할 때 \"old\" 페이지는 오른쪽으로 슬라이드되어 나가야 합니다. */\n    animation: 150ms cubic-bezier(0.4, 0, 1, 1) both fade-out,\n    400ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-right;\n}\n\n::view-transition-new(.slide-back) {\n    /* 뒤로 전환할 때 \"new\" 페이지는 왼쪽에서 슬라이드되어 들어와야 합니다. */\n    animation: 210ms cubic-bezier(0, 0, 0.2, 1) 150ms both fade-in,\n    400ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-left;\n}\n\n/* New keyframes to support our animations above. */\n@keyframes fade-in {\n    from {\n        opacity: 0;\n    }\n}\n\n@keyframes fade-out {\n    to {\n        opacity: 0;\n    }\n}\n\n@keyframes slide-to-right {\n    to {\n        transform: translateX(50px);\n    }\n}\n\n@keyframes slide-from-right {\n    from {\n        transform: translateX(50px);\n    }\n    to {\n        transform: translateX(0);\n    }\n}\n\n@keyframes slide-to-left {\n    to {\n        transform: translateX(-50px);\n    }\n}\n\n@keyframes slide-from-left {\n    from {\n        transform: translateX(-50px);\n    }\n    to {\n        transform: translateX(0);\n    }\n}\n\n/* 이전에 정의된 애니메이션 */\n\n/* Default .slow-fade. */\n::view-transition-old(.slow-fade) {\n    animation-duration: 500ms;\n}\n\n::view-transition-new(.slow-fade) {\n    animation-duration: 500ms;\n}\n```\n\n```js src/index.js hidden\nimport React, {StrictMode} from 'react';\nimport {createRoot} from 'react-dom/client';\nimport './styles.css';\nimport './animations.css';\n\nimport App from './App';\nimport {Router} from './router';\n\nconst root = createRoot(document.getElementById('root'));\nroot.render(\n  <StrictMode>\n    <Router>\n      <App />\n    </Router>\n  </StrictMode>\n);\n```\n\n```json package.json hidden\n{\n  \"dependencies\": {\n    \"react\": \"canary\",\n    \"react-dom\": \"canary\",\n    \"react-scripts\": \"latest\"\n  },\n  \"scripts\": {\n    \"start\": \"react-scripts start\",\n    \"build\": \"react-scripts build\",\n    \"test\": \"react-scripts test --env=jsdom\",\n    \"eject\": \"react-scripts eject\"\n  }\n}\n```\n\n</Sandpack>\n\n### Suspense Boundaries 애니메이팅 {/*animating-suspense-boundaries*/}\n\nSuspense도 View Transitions를 활성화합니다.\n\n콘텐츠에 대한 폴백 애니메이션을 적용하려면 `Suspense`를 `<ViewTransition>`으로 래핑하면 됩니다.\n\n```js\n<ViewTransition>\n  <Suspense fallback={<VideoInfoFallback />}>\n    <VideoInfo />\n  </Suspense>\n</ViewTransition>\n```\n\n이를 추가하면 폴백이 콘텐츠에 크로스 페이드됩니다. 동영상을 클릭하면 동영상 정보에 애니메이션이 적용됩니다.\n\n<Sandpack>\n\n```js src/App.js hidden\nimport { ViewTransition } from \"react\";\nimport Details from \"./Details\";\nimport Home from \"./Home\";\nimport { useRouter } from \"./router\";\n\nexport default function App() {\n  const { url } = useRouter();\n\n  // Default slow-fade animation.\n  return (\n    <ViewTransition default=\"slow-fade\">\n      {url === \"/\" ? <Home /> : <Details />}\n    </ViewTransition>\n  );\n}\n```\n\n```js src/Details.js active\nimport { use, Suspense, ViewTransition } from \"react\"; import { fetchVideo, fetchVideoDetails } from \"./data\"; import { Thumbnail, VideoControls } from \"./Videos\"; import { useRouter } from \"./router\"; import Layout from \"./Layout\"; import { ChevronLeft } from \"./Icons\";\n\nfunction VideoDetails({ id }) {\n  // 폴백에서 콘텐츠로 크로스 페이드합니다.\n  return (\n    <ViewTransition default=\"slow-fade\">\n      <Suspense fallback={<VideoInfoFallback />}>\n          <VideoInfo id={id} />\n      </Suspense>\n    </ViewTransition>\n  );\n}\n\nfunction VideoInfoFallback() {\n  return (\n    <div>\n      <div className=\"fit fallback title\"></div>\n      <div className=\"fit fallback description\"></div>\n    </div>\n  );\n}\n\nexport default function Details() {\n  const { url, navigateBack } = useRouter();\n  const videoId = url.split(\"/\").pop();\n  const video = use(fetchVideo(videoId));\n\n  return (\n    <Layout\n      heading={\n        <div\n          className=\"fit back\"\n          onClick={() => {\n            navigateBack(\"/\");\n          }}\n        >\n          <ChevronLeft /> Back\n        </div>\n      }\n    >\n      <div className=\"details\">\n        <Thumbnail video={video} large>\n          <VideoControls />\n        </Thumbnail>\n        <VideoDetails id={video.id} />\n      </div>\n    </Layout>\n  );\n}\n\nfunction VideoInfo({ id }) {\n  const details = use(fetchVideoDetails(id));\n  return (\n    <div>\n      <p className=\"fit info-title\">{details.title}</p>\n      <p className=\"fit info-description\">{details.description}</p>\n    </div>\n  );\n}\n```\n\n```js src/Home.js hidden\nimport { Video } from \"./Videos\";\nimport Layout from \"./Layout\";\nimport { fetchVideos } from \"./data\";\nimport { useId, useState, use } from \"react\";\nimport { IconSearch } from \"./Icons\";\n\nfunction SearchInput({ value, onChange }) {\n  const id = useId();\n  return (\n    <form className=\"search\" onSubmit={(e) => e.preventDefault()}>\n      <label htmlFor={id} className=\"sr-only\">\n        Search\n      </label>\n      <div className=\"search-input\">\n        <div className=\"search-icon\">\n          <IconSearch />\n        </div>\n        <input\n          type=\"text\"\n          id={id}\n          placeholder=\"Search\"\n          value={value}\n          onChange={(e) => onChange(e.target.value)}\n        />\n      </div>\n    </form>\n  );\n}\n\nfunction filterVideos(videos, query) {\n  const keywords = query\n    .toLowerCase()\n    .split(\" \")\n    .filter((s) => s !== \"\");\n  if (keywords.length === 0) {\n    return videos;\n  }\n  return videos.filter((video) => {\n    const words = (video.title + \" \" + video.description)\n      .toLowerCase()\n      .split(\" \");\n    return keywords.every((kw) => words.some((w) => w.includes(kw)));\n  });\n}\n\nexport default function Home() {\n  const videos = use(fetchVideos());\n  const count = videos.length;\n  const [searchText, setSearchText] = useState(\"\");\n  const foundVideos = filterVideos(videos, searchText);\n  return (\n    <Layout heading={<div className=\"fit\">{count} Videos</div>}>\n      <SearchInput value={searchText} onChange={setSearchText} />\n      <div className=\"video-list\">\n        {foundVideos.length === 0 && (\n          <div className=\"no-results\">No results</div>\n        )}\n        <div className=\"videos\">\n          {foundVideos.map((video) => (\n            <Video key={video.id} video={video} />\n          ))}\n        </div>\n      </div>\n    </Layout>\n  );\n}\n\n```\n\n```js src/Icons.js hidden\nexport function ChevronLeft() {\n  return (\n    <svg\n      className=\"chevron-left\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width=\"20\"\n      height=\"20\"\n      viewBox=\"0 0 20 20\">\n      <g fill=\"none\" fillRule=\"evenodd\" transform=\"translate(-446 -398)\">\n        <path\n          fill=\"currentColor\"\n          fillRule=\"nonzero\"\n          d=\"M95.8838835,240.366117 C95.3957281,239.877961 94.6042719,239.877961 94.1161165,240.366117 C93.6279612,240.854272 93.6279612,241.645728 94.1161165,242.133883 L98.6161165,246.633883 C99.1042719,247.122039 99.8957281,247.122039 100.383883,246.633883 L104.883883,242.133883 C105.372039,241.645728 105.372039,240.854272 104.883883,240.366117 C104.395728,239.877961 103.604272,239.877961 103.116117,240.366117 L99.5,243.982233 L95.8838835,240.366117 Z\"\n          transform=\"translate(356.5 164.5)\"\n        />\n        <polygon points=\"446 418 466 418 466 398 446 398\" />\n      </g>\n    </svg>\n  );\n}\n\nexport function PauseIcon() {\n  return (\n    <svg\n      className=\"control-icon\"\n      style={{padding: '4px'}}\n      width=\"100\"\n      height=\"100\"\n      viewBox=\"0 0 512 512\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\">\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M256 0C114.617 0 0 114.615 0 256s114.617 256 256 256 256-114.615 256-256S397.383 0 256 0zm-32 320c0 8.836-7.164 16-16 16h-32c-8.836 0-16-7.164-16-16V192c0-8.836 7.164-16 16-16h32c8.836 0 16 7.164 16 16v128zm128 0c0 8.836-7.164 16-16 16h-32c-8.836 0-16-7.164-16-16V192c0-8.836 7.164-16 16-16h32c8.836 0 16 7.164 16 16v128z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  );\n}\n\nexport function PlayIcon() {\n  return (\n    <svg\n      className=\"control-icon\"\n      width=\"100\"\n      height=\"100\"\n      viewBox=\"0 0 72 72\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\">\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M36 69C54.2254 69 69 54.2254 69 36C69 17.7746 54.2254 3 36 3C17.7746 3 3 17.7746 3 36C3 54.2254 17.7746 69 36 69ZM52.1716 38.6337L28.4366 51.5801C26.4374 52.6705 24 51.2235 24 48.9464V23.0536C24 20.7764 26.4374 19.3295 28.4366 20.4199L52.1716 33.3663C54.2562 34.5034 54.2562 37.4966 52.1716 38.6337Z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  );\n}\nexport function Heart({liked, animate}) {\n  return (\n    <>\n      <svg\n        className=\"absolute overflow-visible\"\n        viewBox=\"0 0 24 24\"\n        fill=\"none\"\n        xmlns=\"http://www.w3.org/2000/svg\">\n        <circle\n          className={`circle ${liked ? 'liked' : ''} ${animate ? 'animate' : ''}`}\n          cx=\"12\"\n          cy=\"12\"\n          r=\"11.5\"\n          fill=\"transparent\"\n          strokeWidth=\"0\"\n          stroke=\"currentColor\"\n        />\n      </svg>\n\n      <svg\n        className={`heart ${liked ? 'liked' : ''} ${animate ? 'animate' : ''}`}\n        viewBox=\"0 0 24 24\"\n        fill=\"none\"\n        xmlns=\"http://www.w3.org/2000/svg\">\n        {liked ? (\n          <path\n            d=\"M12 23a.496.496 0 0 1-.26-.074C7.023 19.973 0 13.743 0 8.68c0-4.12 2.322-6.677 6.058-6.677 2.572 0 5.108 2.387 5.134 2.41l.808.771.808-.771C12.834 4.387 15.367 2 17.935 2 21.678 2 24 4.558 24 8.677c0 5.06-7.022 11.293-11.74 14.246a.496.496 0 0 1-.26.074V23z\"\n            fill=\"currentColor\"\n          />\n        ) : (\n          <path\n            fillRule=\"evenodd\"\n            clipRule=\"evenodd\"\n            d=\"m12 5.184-.808-.771-.004-.004C11.065 4.299 8.522 2.003 6 2.003c-3.736 0-6 2.558-6 6.677 0 4.47 5.471 9.848 10 13.079.602.43 1.187.82 1.74 1.167A.497.497 0 0 0 12 23v-.003c.09 0 .182-.026.26-.074C16.977 19.97 24 13.737 24 8.677 24 4.557 21.743 2 18 2c-2.569 0-5.166 2.387-5.192 2.413L12 5.184zm-.002 15.525c2.071-1.388 4.477-3.342 6.427-5.47C20.72 12.733 22 10.401 22 8.677c0-1.708-.466-2.855-1.087-3.55C20.316 4.459 19.392 4 18 4c-.726 0-1.63.364-2.5.9-.67.412-1.148.82-1.266.92-.03.025-.037.031-.019.014l-.013.013L12 7.949 9.832 5.88a10.08 10.08 0 0 0-1.33-.977C7.633 4.367 6.728 4.003 6 4.003c-1.388 0-2.312.459-2.91 1.128C2.466 5.826 2 6.974 2 8.68c0 1.726 1.28 4.058 3.575 6.563 1.948 2.127 4.352 4.078 6.423 5.466z\"\n            fill=\"currentColor\"\n          />\n        )}\n      </svg>\n    </>\n  );\n}\n\nexport function IconSearch(props) {\n  return (\n    <svg width=\"1em\" height=\"1em\" viewBox=\"0 0 20 20\">\n      <path\n        d=\"M14.386 14.386l4.0877 4.0877-4.0877-4.0877c-2.9418 2.9419-7.7115 2.9419-10.6533 0-2.9419-2.9418-2.9419-7.7115 0-10.6533 2.9418-2.9419 7.7115-2.9419 10.6533 0 2.9419 2.9418 2.9419 7.7115 0 10.6533z\"\n        stroke=\"currentColor\"\n        fill=\"none\"\n        strokeWidth=\"2\"\n        fillRule=\"evenodd\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"></path>\n    </svg>\n  );\n}\n```\n\n```js src/Layout.js hidden\nimport {ViewTransition} from 'react';\nimport { useIsNavPending } from \"./router\";\n\nexport default function Page({ heading, children }) {\n  const isPending = useIsNavPending();\n  return (\n    <div className=\"page\">\n      <div className=\"top\">\n        <div className=\"top-nav\">\n          {/* 트랜지션 타입에 따라 커스텀 클래스를 적용합니다. */}\n          <ViewTransition\n            name=\"nav\"\n            share={{\n              'nav-forward': 'slide-forward',\n              'nav-back': 'slide-back',\n            }}>\n            {heading}\n          </ViewTransition>\n          {isPending && <span className=\"loader\"></span>}\n        </div>\n      </div>\n      {/* 콘텐츠에 대해 ViewTransition을 사용하지 않습니다. */}\n      {/* 콘텐츠는 자체 ViewTransition을 정의할 수 있습니다. */}\n      <ViewTransition default=\"none\">\n        <div className=\"bottom\">\n          <div className=\"content\">{children}</div>\n        </div>\n      </ViewTransition>\n    </div>\n  );\n}\n```\n\n```js src/LikeButton.js hidden\nimport {useState} from 'react';\nimport {Heart} from './Icons';\n\n// A hack since we don't actually have a backend.\n// Unlike local state, this survives videos being filtered.\nconst likedVideos = new Set();\n\nexport default function LikeButton({video}) {\n  const [isLiked, setIsLiked] = useState(() => likedVideos.has(video.id));\n  const [animate, setAnimate] = useState(false);\n  return (\n    <button\n      className={`like-button ${isLiked && 'liked'}`}\n      aria-label={isLiked ? 'Unsave' : 'Save'}\n      onClick={() => {\n        const nextIsLiked = !isLiked;\n        if (nextIsLiked) {\n          likedVideos.add(video.id);\n        } else {\n          likedVideos.delete(video.id);\n        }\n        setAnimate(true);\n        setIsLiked(nextIsLiked);\n      }}>\n      <Heart liked={isLiked} animate={animate} />\n    </button>\n  );\n}\n```\n\n```js src/Videos.js hidden\nimport { useState, ViewTransition } from \"react\";\nimport LikeButton from \"./LikeButton\";\nimport { useRouter } from \"./router\";\nimport { PauseIcon, PlayIcon } from \"./Icons\";\nimport { startTransition } from \"react\";\n\nexport function Thumbnail({ video, children }) {\n  // 공유 요소 트랜지션으로 애니메이션되도록 name을 추가합니다.\n  // 기본 애니메이션을 사용하므로 추가 CSS가 필요하지 않습니다.\n  return (\n    <ViewTransition name={`video-${video.id}`}>\n      <div\n        aria-hidden=\"true\"\n        tabIndex={-1}\n        className={`thumbnail ${video.image}`}\n      >\n        {children}\n      </div>\n    </ViewTransition>\n  );\n}\n\nexport function VideoControls() {\n  const [isPlaying, setIsPlaying] = useState(false);\n\n  return (\n    <span\n      className=\"controls\"\n      onClick={() =>\n        startTransition(() => {\n          setIsPlaying((p) => !p);\n        })\n      }\n    >\n      {isPlaying ? <PauseIcon /> : <PlayIcon />}\n    </span>\n  );\n}\n\nexport function Video({ video }) {\n  const { navigate } = useRouter();\n\n  return (\n    <div className=\"video\">\n      <div\n        className=\"link\"\n        onClick={(e) => {\n          e.preventDefault();\n          navigate(`/video/${video.id}`);\n        }}\n      >\n        <Thumbnail video={video}></Thumbnail>\n\n        <div className=\"info\">\n          <div className=\"video-title\">{video.title}</div>\n          <div className=\"video-description\">{video.description}</div>\n        </div>\n      </div>\n      <LikeButton video={video} />\n    </div>\n  );\n}\n```\n\n\n```js src/data.js hidden\nconst videos = [\n  {\n    id: '1',\n    title: 'First video',\n    description: 'Video description',\n    image: 'blue',\n  },\n  {\n    id: '2',\n    title: 'Second video',\n    description: 'Video description',\n    image: 'red',\n  },\n  {\n    id: '3',\n    title: 'Third video',\n    description: 'Video description',\n    image: 'green',\n  },\n  {\n    id: '4',\n    title: 'Fourth video',\n    description: 'Video description',\n    image: 'purple',\n  },\n  {\n    id: '5',\n    title: 'Fifth video',\n    description: 'Video description',\n    image: 'yellow',\n  },\n  {\n    id: '6',\n    title: 'Sixth video',\n    description: 'Video description',\n    image: 'gray',\n  },\n];\n\nlet videosCache = new Map();\nlet videoCache = new Map();\nlet videoDetailsCache = new Map();\nconst VIDEO_DELAY = 1;\nconst VIDEO_DETAILS_DELAY = 1000;\nexport function fetchVideos() {\n  if (videosCache.has(0)) {\n    return videosCache.get(0);\n  }\n  const promise = new Promise((resolve) => {\n    setTimeout(() => {\n      resolve(videos);\n    }, VIDEO_DELAY);\n  });\n  videosCache.set(0, promise);\n  return promise;\n}\n\nexport function fetchVideo(id) {\n  if (videoCache.has(id)) {\n    return videoCache.get(id);\n  }\n  const promise = new Promise((resolve) => {\n    setTimeout(() => {\n      resolve(videos.find((video) => video.id === id));\n    }, VIDEO_DELAY);\n  });\n  videoCache.set(id, promise);\n  return promise;\n}\n\nexport function fetchVideoDetails(id) {\n  if (videoDetailsCache.has(id)) {\n    return videoDetailsCache.get(id);\n  }\n  const promise = new Promise((resolve) => {\n    setTimeout(() => {\n      resolve(videos.find((video) => video.id === id));\n    }, VIDEO_DETAILS_DELAY);\n  });\n  videoDetailsCache.set(id, promise);\n  return promise;\n}\n```\n\n```js src/router.js hidden\nimport {useState, createContext, use, useTransition, useLayoutEffect, useEffect, addTransitionType} from \"react\";\n\nexport function Router({ children }) {\n  const [isPending, startTransition] = useTransition();\n  const [routerState, setRouterState] = useState({pendingNav: () => {}, url: document.location.pathname});\n  function navigate(url) {\n    startTransition(() => {\n      // 전환 원인이 \"nav forward\"인 경우의 트랜지션 타입\n      addTransitionType('nav-forward');\n      go(url);\n    });\n  }\n  function navigateBack(url) {\n    startTransition(() => {\n      // 전환 원인이 \"nav backward\"인 경우의 트랜지션 타입\n      addTransitionType('nav-back');\n      go(url);\n    });\n  }\n\n  function go(url) {\n    setRouterState({\n      url,\n      pendingNav() {\n        window.history.pushState({}, \"\", url);\n      },\n    });\n  }\n\n  useEffect(() => {\n    function handlePopState() {\n      // 복원은 동기적으로 이루어져야 하므로 애니메이션되면 안 됩니다.\n      // 트랜지션이더라도 마찬가지입니다.\n      startTransition(() => {\n        setRouterState({\n          url: document.location.pathname + document.location.search,\n          pendingNav() {\n            // 아무 작업도 하지 않습니다. URL은 이미 업데이트되었습니다.\n          },\n        });\n      });\n    }\n    window.addEventListener(\"popstate\", handlePopState);\n    return () => {\n      window.removeEventListener(\"popstate\", handlePopState);\n    };\n  }, []);\n  const pendingNav = routerState.pendingNav;\n  useLayoutEffect(() => {\n    pendingNav();\n  }, [pendingNav]);\n\n  return (\n    <RouterContext\n      value={{\n        url: routerState.url,\n        navigate,\n        navigateBack,\n        isPending,\n        params: {},\n      }}\n    >\n      {children}\n    </RouterContext>\n  );\n}\n\nconst RouterContext = createContext({ url: \"/\", params: {} });\n\nexport function useRouter() {\n  return use(RouterContext);\n}\n\nexport function useIsNavPending() {\n  return use(RouterContext).isPending;\n}\n\n```\n\n```css src/styles.css hidden\n@font-face {\n  font-family: Optimistic Text;\n  src: url(https://react.dev/fonts/Optimistic_Text_W_Rg.woff2) format(\"woff2\");\n  font-weight: 400;\n  font-style: normal;\n  font-display: swap;\n}\n\n@font-face {\n  font-family: Optimistic Text;\n  src: url(https://react.dev/fonts/Optimistic_Text_W_Md.woff2) format(\"woff2\");\n  font-weight: 500;\n  font-style: normal;\n  font-display: swap;\n}\n\n@font-face {\n  font-family: Optimistic Text;\n  src: url(https://react.dev/fonts/Optimistic_Text_W_Bd.woff2) format(\"woff2\");\n  font-weight: 600;\n  font-style: normal;\n  font-display: swap;\n}\n\n@font-face {\n  font-family: Optimistic Text;\n  src: url(https://react.dev/fonts/Optimistic_Text_W_Bd.woff2) format(\"woff2\");\n  font-weight: 700;\n  font-style: normal;\n  font-display: swap;\n}\n\n* {\n  box-sizing: border-box;\n}\n\nhtml {\n  background-image: url(https://react.dev/images/meta-gradient-dark.png);\n  background-size: 100%;\n  background-position: -100%;\n  background-color: rgb(64 71 86);\n  background-repeat: no-repeat;\n  height: 100%;\n  width: 100%;\n}\n\nbody {\n  font-family: Optimistic Text, -apple-system, ui-sans-serif, system-ui, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;\n  padding: 10px 0 10px 0;\n  margin: 0;\n  display: flex;\n  justify-content: center;\n}\n\n#root {\n  flex: 1 1;\n  height: auto;\n  background-color: #fff;\n  border-radius: 10px;\n  max-width: 450px;\n  min-height: 600px;\n  padding-bottom: 10px;\n}\n\nh1 {\n  margin-top: 0;\n  font-size: 22px;\n}\n\nh2 {\n  margin-top: 0;\n  font-size: 20px;\n}\n\nh3 {\n  margin-top: 0;\n  font-size: 18px;\n}\n\nh4 {\n  margin-top: 0;\n  font-size: 16px;\n}\n\nh5 {\n  margin-top: 0;\n  font-size: 14px;\n}\n\nh6 {\n  margin-top: 0;\n  font-size: 12px;\n}\n\ncode {\n  font-size: 1.2em;\n}\n\nul {\n  padding-inline-start: 20px;\n}\n\n.sr-only {\n  position: absolute;\n  width: 1px;\n  height: 1px;\n  padding: 0;\n  margin: -1px;\n  overflow: hidden;\n  clip: rect(0, 0, 0, 0);\n  white-space: nowrap;\n  border-width: 0;\n}\n\n.absolute {\n  position: absolute;\n}\n\n.overflow-visible {\n  overflow: visible;\n}\n\n.visible {\n  overflow: visible;\n}\n\n.fit {\n  width: fit-content;\n}\n\n\n/* Layout */\n.page {\n  display: flex;\n  flex-direction: column;\n  height: 100%;\n}\n\n.top-hero {\n  height: 200px;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  background-image: conic-gradient(\n      from 90deg at -10% 100%,\n      #2b303b 0deg,\n      #2b303b 90deg,\n      #16181d 1turn\n  );\n}\n\n.bottom {\n  flex: 1;\n  overflow: auto;\n}\n\n.top-nav {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  margin-bottom: 0;\n  padding: 0 12px;\n  top: 0;\n  width: 100%;\n  height: 44px;\n  color: #23272f;\n  font-weight: 700;\n  font-size: 20px;\n  z-index: 100;\n  cursor: default;\n}\n\n.content {\n  padding: 0 12px;\n  margin-top: 4px;\n}\n\n\n.loader {\n  color: #23272f;\n  font-size: 3px;\n  width: 1em;\n  margin-right: 18px;\n  height: 1em;\n  border-radius: 50%;\n  position: relative;\n  text-indent: -9999em;\n  animation: loading-spinner 1.3s infinite linear;\n  animation-delay: 200ms;\n  transform: translateZ(0);\n}\n\n@keyframes loading-spinner {\n  0%,\n  100% {\n    box-shadow: 0 -3em 0 0.2em,\n    2em -2em 0 0em, 3em 0 0 -1em,\n    2em 2em 0 -1em, 0 3em 0 -1em,\n    -2em 2em 0 -1em, -3em 0 0 -1em,\n    -2em -2em 0 0;\n  }\n  12.5% {\n    box-shadow: 0 -3em 0 0, 2em -2em 0 0.2em,\n    3em 0 0 0, 2em 2em 0 -1em, 0 3em 0 -1em,\n    -2em 2em 0 -1em, -3em 0 0 -1em,\n    -2em -2em 0 -1em;\n  }\n  25% {\n    box-shadow: 0 -3em 0 -0.5em,\n    2em -2em 0 0, 3em 0 0 0.2em,\n    2em 2em 0 0, 0 3em 0 -1em,\n    -2em 2em 0 -1em, -3em 0 0 -1em,\n    -2em -2em 0 -1em;\n  }\n  37.5% {\n    box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em,\n    3em 0em 0 0, 2em 2em 0 0.2em, 0 3em 0 0em,\n    -2em 2em 0 -1em, -3em 0em 0 -1em, -2em -2em 0 -1em;\n  }\n  50% {\n    box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em,\n    3em 0 0 -1em, 2em 2em 0 0em, 0 3em 0 0.2em,\n    -2em 2em 0 0, -3em 0em 0 -1em, -2em -2em 0 -1em;\n  }\n  62.5% {\n    box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em,\n    3em 0 0 -1em, 2em 2em 0 -1em, 0 3em 0 0,\n    -2em 2em 0 0.2em, -3em 0 0 0, -2em -2em 0 -1em;\n  }\n  75% {\n    box-shadow: 0em -3em 0 -1em, 2em -2em 0 -1em,\n    3em 0em 0 -1em, 2em 2em 0 -1em, 0 3em 0 -1em,\n    -2em 2em 0 0, -3em 0em 0 0.2em, -2em -2em 0 0;\n  }\n  87.5% {\n    box-shadow: 0em -3em 0 0, 2em -2em 0 -1em,\n    3em 0 0 -1em, 2em 2em 0 -1em, 0 3em 0 -1em,\n    -2em 2em 0 0, -3em 0em 0 0, -2em -2em 0 0.2em;\n  }\n}\n\n/* LikeButton */\n.like-button {\n  outline-offset: 2px;\n  position: relative;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  width: 2.5rem;\n  height: 2.5rem;\n  cursor: pointer;\n  border-radius: 9999px;\n  border: none;\n  outline: none 2px;\n  color: #5e687e;\n  background: none;\n}\n\n.like-button:focus {\n  color: #a6423a;\n  background-color: rgba(166, 66, 58, .05);\n}\n\n.like-button:active {\n  color: #a6423a;\n  background-color: rgba(166, 66, 58, .05);\n  transform: scaleX(0.95) scaleY(0.95);\n}\n\n.like-button:hover {\n  background-color: #f6f7f9;\n}\n\n.like-button.liked {\n  color: #a6423a;\n}\n\n/* Icons */\n@keyframes circle {\n  0% {\n    transform: scale(0);\n    stroke-width: 16px;\n  }\n\n  50% {\n    transform: scale(.5);\n    stroke-width: 16px;\n  }\n\n  to {\n    transform: scale(1);\n    stroke-width: 0;\n  }\n}\n\n.circle {\n  color: rgba(166, 66, 58, .5);\n  transform-origin: center;\n  transition-property: all;\n  transition-duration: .15s;\n  transition-timing-function: cubic-bezier(.4,0,.2,1);\n}\n\n.circle.liked.animate {\n  animation: circle .3s forwards;\n}\n\n.heart {\n  width: 1.5rem;\n  height: 1.5rem;\n}\n\n.heart.liked {\n  transform-origin: center;\n  transition-property: all;\n  transition-duration: .15s;\n  transition-timing-function: cubic-bezier(.4, 0, .2, 1);\n}\n\n.heart.liked.animate {\n  animation: scale .35s ease-in-out forwards;\n}\n\n.control-icon {\n  color: hsla(0, 0%, 100%, .5);\n  filter:  drop-shadow(0 20px 13px rgba(0, 0, 0, .03)) drop-shadow(0 8px 5px rgba(0, 0, 0, .08));\n}\n\n.chevron-left {\n  margin-top: 2px;\n  rotate: 90deg;\n}\n\n\n/* Video */\n.thumbnail {\n  position: relative;\n  aspect-ratio: 16 / 9;\n  display: flex;\n  overflow: hidden;\n  flex-direction: column;\n  justify-content: center;\n  align-items: center;\n  border-radius: 0.5rem;\n  outline-offset: 2px;\n  width: 8rem;\n  vertical-align: middle;\n  background-color: #ffffff;\n  background-size: cover;\n  user-select: none;\n}\n\n.thumbnail.blue {\n  background-image: conic-gradient(at top right, #c76a15, #087ea4, #2b3491);\n}\n\n.thumbnail.red {\n  background-image: conic-gradient(at top right, #c76a15, #a6423a, #2b3491);\n}\n\n.thumbnail.green {\n  background-image: conic-gradient(at top right, #c76a15, #388f7f, #2b3491);\n}\n\n.thumbnail.purple {\n  background-image: conic-gradient(at top right, #c76a15, #575fb7, #2b3491);\n}\n\n.thumbnail.yellow {\n  background-image: conic-gradient(at top right, #c76a15, #FABD62, #2b3491);\n}\n\n.thumbnail.gray {\n  background-image: conic-gradient(at top right, #c76a15, #4E5769, #2b3491);\n}\n\n.video {\n  display: flex;\n  flex-direction: row;\n  gap: 0.75rem;\n  align-items: center;\n}\n\n.video .link {\n  display: flex;\n  flex-direction: row;\n  flex: 1 1 0;\n  gap: 0.125rem;\n  outline-offset: 4px;\n  cursor: pointer;\n}\n\n.video .info {\n  display: flex;\n  flex-direction: column;\n  justify-content: center;\n  margin-left: 8px;\n  gap: 0.125rem;\n}\n\n.video .info:hover {\n  text-decoration: underline;\n}\n\n.video-title {\n  font-size: 15px;\n  line-height: 1.25;\n  font-weight: 700;\n  color: #23272f;\n}\n\n.video-description {\n  color: #5e687e;\n  font-size: 13px;\n}\n\n/* Details */\n.details .thumbnail {\n  position: relative;\n  aspect-ratio: 16 / 9;\n  display: flex;\n  overflow: hidden;\n  flex-direction: column;\n  justify-content: center;\n  align-items: center;\n  border-radius: 0.5rem;\n  outline-offset: 2px;\n  width: 100%;\n  vertical-align: middle;\n  background-color: #ffffff;\n  background-size: cover;\n  user-select: none;\n}\n\n.video-details-title {\n  margin-top: 8px;\n}\n\n.video-details-speaker {\n  display: flex;\n  gap: 8px;\n  margin-top: 10px\n}\n\n.back {\n  display: flex;\n  align-items: center;\n  margin-left: -5px;\n  cursor: pointer;\n}\n\n.back:hover {\n  text-decoration: underline;\n}\n\n.info-title {\n  font-size: 1.5rem;\n  font-weight: 700;\n  line-height: 1.25;\n  margin: 8px 0 0 0 ;\n}\n\n.info-description {\n  margin: 8px 0 0 0;\n}\n\n.controls {\n  cursor: pointer;\n}\n\n.fallback {\n  background: #f6f7f8 linear-gradient(to right, #e6e6e6 5%, #cccccc 25%, #e6e6e6 35%) no-repeat;\n  background-size: 800px 104px;\n  display: block;\n  line-height: 1.25;\n  margin: 8px 0 0 0;\n  border-radius: 5px;\n  overflow: hidden;\n\n  animation: 1s linear 1s infinite shimmer;\n  animation-delay: 300ms;\n  animation-duration: 1s;\n  animation-fill-mode: forwards;\n  animation-iteration-count: infinite;\n  animation-name: shimmer;\n  animation-timing-function: linear;\n}\n\n\n.fallback.title {\n  width: 130px;\n  height: 30px;\n\n}\n\n.fallback.description {\n  width: 150px;\n  height: 21px;\n}\n\n@keyframes shimmer {\n  0% {\n    background-position: -468px 0;\n  }\n\n  100% {\n    background-position: 468px 0;\n  }\n}\n\n.search {\n  margin-bottom: 10px;\n}\n.search-input {\n  width: 100%;\n  position: relative;\n}\n\n.search-icon {\n  position: absolute;\n  top: 0;\n  bottom: 0;\n  inset-inline-start: 0;\n  display: flex;\n  align-items: center;\n  padding-inline-start: 1rem;\n  pointer-events: none;\n  color: #99a1b3;\n}\n\n.search-input input {\n  display: flex;\n  padding-inline-start: 2.75rem;\n  padding-top: 10px;\n  padding-bottom: 10px;\n  width: 100%;\n  text-align: start;\n  background-color: rgb(235 236 240);\n  outline: 2px solid transparent;\n  cursor: pointer;\n  border: none;\n  align-items: center;\n  color: rgb(35 39 47);\n  border-radius: 9999px;\n  vertical-align: middle;\n  font-size: 15px;\n}\n\n.search-input input:hover, .search-input input:active {\n  background-color: rgb(235 236 240/ 0.8);\n  color: rgb(35 39 47/ 0.8);\n}\n\n/* Home */\n.video-list {\n  position: relative;\n}\n\n.video-list .videos {\n  display: flex;\n  flex-direction: column;\n  gap: 1rem;\n  overflow-y: auto;\n  height: 100%;\n}\n```\n\n\n```css src/animations.css\n/* 폴백을 아래로 슬라이드합니다 */\n::view-transition-old(.slide-down) {\n    animation: 150ms ease-out both fade-out, 150ms ease-out both slide-down;\n}\n\n/* 콘텐츠를 위로 슬라이드합니다 */\n::view-transition-new(.slide-up) {\n    animation: 210ms ease-in 150ms both fade-in, 400ms ease-in both slide-up;\n}\n\n/* 새로운 키프레임을 정의합니다 */\n@keyframes slide-up {\n    from {\n        transform: translateY(10px);\n    }\n    to {\n        transform: translateY(0);\n    }\n}\n\n@keyframes slide-down {\n    from {\n        transform: translateY(0);\n    }\n    to {\n        transform: translateY(10px);\n    }\n}\n\n/* 이전에 정의된 애니메이션은 아래에 있습니다 */\n\n/* 트랜지션 타입에 의해 추가된 View Transition 클래스용 애니메이션 */\n::view-transition-old(.slide-forward) {\n   /* 앞으로 전환할 때 \"old\" 페이지는 왼쪽으로 슬라이드되어 나가야 합니다. */\n    animation: 150ms cubic-bezier(0.4, 0, 1, 1) both fade-out,\n    400ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;\n}\n\n::view-transition-new(.slide-forward) {\n    /* 앞으로 전환할 때 \"new\" 페이지는 오른쪽에서 슬라이드되어 들어와야 합니다. */\n    animation: 210ms cubic-bezier(0, 0, 0.2, 1) 150ms both fade-in,\n    400ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;\n}\n\n::view-transition-old(.slide-back) {\n    /* 뒤로 전환할 때 \"old\" 페이지는 오른쪽으로 슬라이드되어 나가야 합니다. */\n    animation: 150ms cubic-bezier(0.4, 0, 1, 1) both fade-out,\n    400ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-right;\n}\n\n::view-transition-new(.slide-back) {\n    /* 뒤로 전환할 때 \"new\" 페이지는 왼쪽에서 슬라이드되어 들어와야 합니다. */\n    animation: 210ms cubic-bezier(0, 0, 0.2, 1) 150ms both fade-in,\n    400ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-left;\n}\n\n/* 위 애니메이션을 지원하기 위한 키프레임 */\n@keyframes fade-in {\n    from {\n        opacity: 0;\n    }\n}\n\n@keyframes fade-out {\n    to {\n        opacity: 0;\n    }\n}\n\n@keyframes slide-to-right {\n    to {\n        transform: translateX(50px);\n    }\n}\n\n@keyframes slide-from-right {\n    from {\n        transform: translateX(50px);\n    }\n    to {\n        transform: translateX(0);\n    }\n}\n\n@keyframes slide-to-left {\n    to {\n        transform: translateX(-50px);\n    }\n}\n\n@keyframes slide-from-left {\n    from {\n        transform: translateX(-50px);\n    }\n    to {\n        transform: translateX(0);\n    }\n}\n\n/* Default .slow-fade. */\n::view-transition-old(.slow-fade) {\n    animation-duration: 500ms;\n}\n\n::view-transition-new(.slow-fade) {\n    animation-duration: 500ms;\n}\n```\n\n```js src/index.js hidden\nimport React, {StrictMode} from 'react';\nimport {createRoot} from 'react-dom/client';\nimport './styles.css';\nimport './animations.css';\n\nimport App from './App';\nimport {Router} from './router';\n\nconst root = createRoot(document.getElementById('root'));\nroot.render(\n  <StrictMode>\n    <Router>\n      <App />\n    </Router>\n  </StrictMode>\n);\n```\n\n```json package.json hidden\n{\n  \"dependencies\": {\n    \"react\": \"canary\",\n    \"react-dom\": \"canary\",\n    \"react-scripts\": \"latest\"\n  },\n  \"scripts\": {\n    \"start\": \"react-scripts start\",\n    \"build\": \"react-scripts build\",\n    \"test\": \"react-scripts test --env=jsdom\",\n    \"eject\": \"react-scripts eject\"\n  }\n}\n```\n\n</Sandpack>\n\n또한 폴백에 `exit`, 내부 콘텐츠에 `enter`를 사용하여 커스텀 애니메이션을 제공할 수도 있습니다.\n\n```js {3,8}\n<Suspense\n  fallback={\n    <ViewTransition exit=\"slide-down\">\n      <VideoInfoFallback />\n    </ViewTransition>\n  }\n>\n  <ViewTransition enter=\"slide-up\">\n    <VideoInfo id={id} />\n  </ViewTransition>\n</Suspense>\n```\n\nCSS로 `slide-down`과 `slide-up`을 정의하는 방법은 다음과 같습니다.\n\n```css {1, 6}\n::view-transition-old(.slide-down) {\n  /* 폴백을 아래로 슬라이드합니다 */\n  animation: ...;\n}\n\n::view-transition-new(.slide-up) {\n  /* 콘텐츠를 위로 슬라이드합니다 */\n  animation: ...;\n}\n```\n\n이제 Suspense 콘텐츠가 슬라이드 애니메이션으로 폴백을 대체합니다.\n\n<Sandpack>\n\n```js src/App.js hidden\nimport { ViewTransition } from \"react\";\nimport Details from \"./Details\";\nimport Home from \"./Home\";\nimport { useRouter } from \"./router\";\n\nexport default function App() {\n  const { url } = useRouter();\n\n  // Default slow-fade animation.\n  return (\n    <ViewTransition default=\"slow-fade\">\n      {url === \"/\" ? <Home /> : <Details />}\n    </ViewTransition>\n  );\n}\n```\n\n```js src/Details.js active\nimport { use, Suspense, ViewTransition } from \"react\"; import { fetchVideo, fetchVideoDetails } from \"./data\"; import { Thumbnail, VideoControls } from \"./Videos\"; import { useRouter } from \"./router\"; import Layout from \"./Layout\"; import { ChevronLeft } from \"./Icons\";\n\nfunction VideoDetails({ id }) {\n  return (\n    <Suspense\n      fallback={\n        // 폴백을 아래로 애니메이션합니다.\n        <ViewTransition exit=\"slide-down\">\n          <VideoInfoFallback />\n        </ViewTransition>\n      }\n    >\n      {/* 콘텐츠를 위로 애니메이션합니다 */}\n      <ViewTransition enter=\"slide-up\">\n        <VideoInfo id={id} />\n      </ViewTransition>\n    </Suspense>\n  );\n}\n\nfunction VideoInfoFallback() {\n  return (\n    <>\n      <div className=\"fallback title\"></div>\n      <div className=\"fallback description\"></div>\n    </>\n  );\n}\n\nexport default function Details() {\n  const { url, navigateBack } = useRouter();\n  const videoId = url.split(\"/\").pop();\n  const video = use(fetchVideo(videoId));\n\n  return (\n    <Layout\n      heading={\n        <div\n          className=\"fit back\"\n          onClick={() => {\n            navigateBack(\"/\");\n          }}\n        >\n          <ChevronLeft /> Back\n        </div>\n      }\n    >\n      <div className=\"details\">\n        <Thumbnail video={video} large>\n          <VideoControls />\n        </Thumbnail>\n        <VideoDetails id={video.id} />\n      </div>\n    </Layout>\n  );\n}\n\nfunction VideoInfo({ id }) {\n  const details = use(fetchVideoDetails(id));\n  return (\n    <>\n      <p className=\"info-title\">{details.title}</p>\n      <p className=\"info-description\">{details.description}</p>\n    </>\n  );\n}\n```\n\n```js src/Home.js hidden\nimport { Video } from \"./Videos\";\nimport Layout from \"./Layout\";\nimport { fetchVideos } from \"./data\";\nimport { useId, useState, use } from \"react\";\nimport { IconSearch } from \"./Icons\";\n\nfunction SearchInput({ value, onChange }) {\n  const id = useId();\n  return (\n    <form className=\"search\" onSubmit={(e) => e.preventDefault()}>\n      <label htmlFor={id} className=\"sr-only\">\n        Search\n      </label>\n      <div className=\"search-input\">\n        <div className=\"search-icon\">\n          <IconSearch />\n        </div>\n        <input\n          type=\"text\"\n          id={id}\n          placeholder=\"Search\"\n          value={value}\n          onChange={(e) => onChange(e.target.value)}\n        />\n      </div>\n    </form>\n  );\n}\n\nfunction filterVideos(videos, query) {\n  const keywords = query\n    .toLowerCase()\n    .split(\" \")\n    .filter((s) => s !== \"\");\n  if (keywords.length === 0) {\n    return videos;\n  }\n  return videos.filter((video) => {\n    const words = (video.title + \" \" + video.description)\n      .toLowerCase()\n      .split(\" \");\n    return keywords.every((kw) => words.some((w) => w.includes(kw)));\n  });\n}\n\nexport default function Home() {\n  const videos = use(fetchVideos());\n  const count = videos.length;\n  const [searchText, setSearchText] = useState(\"\");\n  const foundVideos = filterVideos(videos, searchText);\n  return (\n    <Layout heading={<div className=\"fit\">{count} Videos</div>}>\n      <SearchInput value={searchText} onChange={setSearchText} />\n      <div className=\"video-list\">\n        {foundVideos.length === 0 && (\n          <div className=\"no-results\">No results</div>\n        )}\n        <div className=\"videos\">\n          {foundVideos.map((video) => (\n            <Video key={video.id} video={video} />\n          ))}\n        </div>\n      </div>\n    </Layout>\n  );\n}\n\n```\n\n```js src/Icons.js hidden\nexport function ChevronLeft() {\n  return (\n    <svg\n      className=\"chevron-left\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width=\"20\"\n      height=\"20\"\n      viewBox=\"0 0 20 20\">\n      <g fill=\"none\" fillRule=\"evenodd\" transform=\"translate(-446 -398)\">\n        <path\n          fill=\"currentColor\"\n          fillRule=\"nonzero\"\n          d=\"M95.8838835,240.366117 C95.3957281,239.877961 94.6042719,239.877961 94.1161165,240.366117 C93.6279612,240.854272 93.6279612,241.645728 94.1161165,242.133883 L98.6161165,246.633883 C99.1042719,247.122039 99.8957281,247.122039 100.383883,246.633883 L104.883883,242.133883 C105.372039,241.645728 105.372039,240.854272 104.883883,240.366117 C104.395728,239.877961 103.604272,239.877961 103.116117,240.366117 L99.5,243.982233 L95.8838835,240.366117 Z\"\n          transform=\"translate(356.5 164.5)\"\n        />\n        <polygon points=\"446 418 466 418 466 398 446 398\" />\n      </g>\n    </svg>\n  );\n}\n\nexport function PauseIcon() {\n  return (\n    <svg\n      className=\"control-icon\"\n      style={{padding: '4px'}}\n      width=\"100\"\n      height=\"100\"\n      viewBox=\"0 0 512 512\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\">\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M256 0C114.617 0 0 114.615 0 256s114.617 256 256 256 256-114.615 256-256S397.383 0 256 0zm-32 320c0 8.836-7.164 16-16 16h-32c-8.836 0-16-7.164-16-16V192c0-8.836 7.164-16 16-16h32c8.836 0 16 7.164 16 16v128zm128 0c0 8.836-7.164 16-16 16h-32c-8.836 0-16-7.164-16-16V192c0-8.836 7.164-16 16-16h32c8.836 0 16 7.164 16 16v128z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  );\n}\n\nexport function PlayIcon() {\n  return (\n    <svg\n      className=\"control-icon\"\n      width=\"100\"\n      height=\"100\"\n      viewBox=\"0 0 72 72\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\">\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M36 69C54.2254 69 69 54.2254 69 36C69 17.7746 54.2254 3 36 3C17.7746 3 3 17.7746 3 36C3 54.2254 17.7746 69 36 69ZM52.1716 38.6337L28.4366 51.5801C26.4374 52.6705 24 51.2235 24 48.9464V23.0536C24 20.7764 26.4374 19.3295 28.4366 20.4199L52.1716 33.3663C54.2562 34.5034 54.2562 37.4966 52.1716 38.6337Z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  );\n}\nexport function Heart({liked, animate}) {\n  return (\n    <>\n      <svg\n        className=\"absolute overflow-visible\"\n        viewBox=\"0 0 24 24\"\n        fill=\"none\"\n        xmlns=\"http://www.w3.org/2000/svg\">\n        <circle\n          className={`circle ${liked ? 'liked' : ''} ${animate ? 'animate' : ''}`}\n          cx=\"12\"\n          cy=\"12\"\n          r=\"11.5\"\n          fill=\"transparent\"\n          strokeWidth=\"0\"\n          stroke=\"currentColor\"\n        />\n      </svg>\n\n      <svg\n        className={`heart ${liked ? 'liked' : ''} ${animate ? 'animate' : ''}`}\n        viewBox=\"0 0 24 24\"\n        fill=\"none\"\n        xmlns=\"http://www.w3.org/2000/svg\">\n        {liked ? (\n          <path\n            d=\"M12 23a.496.496 0 0 1-.26-.074C7.023 19.973 0 13.743 0 8.68c0-4.12 2.322-6.677 6.058-6.677 2.572 0 5.108 2.387 5.134 2.41l.808.771.808-.771C12.834 4.387 15.367 2 17.935 2 21.678 2 24 4.558 24 8.677c0 5.06-7.022 11.293-11.74 14.246a.496.496 0 0 1-.26.074V23z\"\n            fill=\"currentColor\"\n          />\n        ) : (\n          <path\n            fillRule=\"evenodd\"\n            clipRule=\"evenodd\"\n            d=\"m12 5.184-.808-.771-.004-.004C11.065 4.299 8.522 2.003 6 2.003c-3.736 0-6 2.558-6 6.677 0 4.47 5.471 9.848 10 13.079.602.43 1.187.82 1.74 1.167A.497.497 0 0 0 12 23v-.003c.09 0 .182-.026.26-.074C16.977 19.97 24 13.737 24 8.677 24 4.557 21.743 2 18 2c-2.569 0-5.166 2.387-5.192 2.413L12 5.184zm-.002 15.525c2.071-1.388 4.477-3.342 6.427-5.47C20.72 12.733 22 10.401 22 8.677c0-1.708-.466-2.855-1.087-3.55C20.316 4.459 19.392 4 18 4c-.726 0-1.63.364-2.5.9-.67.412-1.148.82-1.266.92-.03.025-.037.031-.019.014l-.013.013L12 7.949 9.832 5.88a10.08 10.08 0 0 0-1.33-.977C7.633 4.367 6.728 4.003 6 4.003c-1.388 0-2.312.459-2.91 1.128C2.466 5.826 2 6.974 2 8.68c0 1.726 1.28 4.058 3.575 6.563 1.948 2.127 4.352 4.078 6.423 5.466z\"\n            fill=\"currentColor\"\n          />\n        )}\n      </svg>\n    </>\n  );\n}\n\nexport function IconSearch(props) {\n  return (\n    <svg width=\"1em\" height=\"1em\" viewBox=\"0 0 20 20\">\n      <path\n        d=\"M14.386 14.386l4.0877 4.0877-4.0877-4.0877c-2.9418 2.9419-7.7115 2.9419-10.6533 0-2.9419-2.9418-2.9419-7.7115 0-10.6533 2.9418-2.9419 7.7115-2.9419 10.6533 0 2.9419 2.9418 2.9419 7.7115 0 10.6533z\"\n        stroke=\"currentColor\"\n        fill=\"none\"\n        strokeWidth=\"2\"\n        fillRule=\"evenodd\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"></path>\n    </svg>\n  );\n}\n```\n\n```js src/Layout.js hidden\nimport {ViewTransition} from 'react';\nimport { useIsNavPending } from \"./router\";\n\nexport default function Page({ heading, children }) {\n  const isPending = useIsNavPending();\n  return (\n    <div className=\"page\">\n      <div className=\"top\">\n        <div className=\"top-nav\">\n          {/* 트랜지션 타입에 따라 커스텀 클래스를 적용합니다. */}\n          <ViewTransition\n            name=\"nav\"\n            share={{\n              'nav-forward': 'slide-forward',\n              'nav-back': 'slide-back',\n            }}>\n            {heading}\n          </ViewTransition>\n          {isPending && <span className=\"loader\"></span>}\n        </div>\n      </div>\n      {/* 콘텐츠에 대해 ViewTransition을 사용하지 않습니다. */}\n      {/* 콘텐츠는 자체 ViewTransition을 정의할 수 있습니다. */}\n      <ViewTransition default=\"none\">\n        <div className=\"bottom\">\n          <div className=\"content\">{children}</div>\n        </div>\n      </ViewTransition>\n    </div>\n  );\n}\n```\n\n```js src/LikeButton.js hidden\nimport {useState} from 'react';\nimport {Heart} from './Icons';\n\n// A hack since we don't actually have a backend.\n// Unlike local state, this survives videos being filtered.\nconst likedVideos = new Set();\n\nexport default function LikeButton({video}) {\n  const [isLiked, setIsLiked] = useState(() => likedVideos.has(video.id));\n  const [animate, setAnimate] = useState(false);\n  return (\n    <button\n      className={`like-button ${isLiked && 'liked'}`}\n      aria-label={isLiked ? 'Unsave' : 'Save'}\n      onClick={() => {\n        const nextIsLiked = !isLiked;\n        if (nextIsLiked) {\n          likedVideos.add(video.id);\n        } else {\n          likedVideos.delete(video.id);\n        }\n        setAnimate(true);\n        setIsLiked(nextIsLiked);\n      }}>\n      <Heart liked={isLiked} animate={animate} />\n    </button>\n  );\n}\n```\n\n```js src/Videos.js hidden\nimport { useState, ViewTransition } from \"react\";\nimport LikeButton from \"./LikeButton\";\nimport { useRouter } from \"./router\";\nimport { PauseIcon, PlayIcon } from \"./Icons\";\nimport { startTransition } from \"react\";\n\nexport function Thumbnail({ video, children }) {\n  // 공유 요소 트랜지션으로 애니메이션되도록 name을 추가합니다.\n  // 기본 애니메이션을 사용하므로 추가 CSS가 필요하지 않습니다.\n  return (\n    <ViewTransition name={`video-${video.id}`}>\n      <div\n        aria-hidden=\"true\"\n        tabIndex={-1}\n        className={`thumbnail ${video.image}`}\n      >\n        {children}\n      </div>\n    </ViewTransition>\n  );\n}\n\nexport function VideoControls() {\n  const [isPlaying, setIsPlaying] = useState(false);\n\n  return (\n    <span\n      className=\"controls\"\n      onClick={() =>\n        startTransition(() => {\n          setIsPlaying((p) => !p);\n        })\n      }\n    >\n      {isPlaying ? <PauseIcon /> : <PlayIcon />}\n    </span>\n  );\n}\n\nexport function Video({ video }) {\n  const { navigate } = useRouter();\n\n  return (\n    <div className=\"video\">\n      <div\n        className=\"link\"\n        onClick={(e) => {\n          e.preventDefault();\n          navigate(`/video/${video.id}`);\n        }}\n      >\n        <Thumbnail video={video}></Thumbnail>\n\n        <div className=\"info\">\n          <div className=\"video-title\">{video.title}</div>\n          <div className=\"video-description\">{video.description}</div>\n        </div>\n      </div>\n      <LikeButton video={video} />\n    </div>\n  );\n}\n```\n\n\n```js src/data.js hidden\nconst videos = [\n  {\n    id: '1',\n    title: 'First video',\n    description: 'Video description',\n    image: 'blue',\n  },\n  {\n    id: '2',\n    title: 'Second video',\n    description: 'Video description',\n    image: 'red',\n  },\n  {\n    id: '3',\n    title: 'Third video',\n    description: 'Video description',\n    image: 'green',\n  },\n  {\n    id: '4',\n    title: 'Fourth video',\n    description: 'Video description',\n    image: 'purple',\n  },\n  {\n    id: '5',\n    title: 'Fifth video',\n    description: 'Video description',\n    image: 'yellow',\n  },\n  {\n    id: '6',\n    title: 'Sixth video',\n    description: 'Video description',\n    image: 'gray',\n  },\n];\n\nlet videosCache = new Map();\nlet videoCache = new Map();\nlet videoDetailsCache = new Map();\nconst VIDEO_DELAY = 1;\nconst VIDEO_DETAILS_DELAY = 1000;\nexport function fetchVideos() {\n  if (videosCache.has(0)) {\n    return videosCache.get(0);\n  }\n  const promise = new Promise((resolve) => {\n    setTimeout(() => {\n      resolve(videos);\n    }, VIDEO_DELAY);\n  });\n  videosCache.set(0, promise);\n  return promise;\n}\n\nexport function fetchVideo(id) {\n  if (videoCache.has(id)) {\n    return videoCache.get(id);\n  }\n  const promise = new Promise((resolve) => {\n    setTimeout(() => {\n      resolve(videos.find((video) => video.id === id));\n    }, VIDEO_DELAY);\n  });\n  videoCache.set(id, promise);\n  return promise;\n}\n\nexport function fetchVideoDetails(id) {\n  if (videoDetailsCache.has(id)) {\n    return videoDetailsCache.get(id);\n  }\n  const promise = new Promise((resolve) => {\n    setTimeout(() => {\n      resolve(videos.find((video) => video.id === id));\n    }, VIDEO_DETAILS_DELAY);\n  });\n  videoDetailsCache.set(id, promise);\n  return promise;\n}\n```\n\n```js src/router.js hidden\nimport {useState, createContext, use, useTransition, useLayoutEffect, useEffect, addTransitionType} from \"react\";\n\nexport function Router({ children }) {\n  const [isPending, startTransition] = useTransition();\n  const [routerState, setRouterState] = useState({pendingNav: () => {}, url: document.location.pathname});\n  function navigate(url) {\n    startTransition(() => {\n      // 전환 원인이 \"nav forward\"인 경우의 트랜지션 타입\n      addTransitionType('nav-forward');\n      go(url);\n    });\n  }\n  function navigateBack(url) {\n    startTransition(() => {\n      // 전환 원인이 \"nav backward\"인 경우의 트랜지션 타입\n      addTransitionType('nav-back');\n      go(url);\n    });\n  }\n\n  function go(url) {\n    setRouterState({\n      url,\n      pendingNav() {\n        window.history.pushState({}, \"\", url);\n      },\n    });\n  }\n\n  useEffect(() => {\n    function handlePopState() {\n      // 복원은 동기적으로 이루어져야 하므로 애니메이션되면 안 됩니다.\n      // 트랜지션이더라도 마찬가지입니다.\n      startTransition(() => {\n        setRouterState({\n          url: document.location.pathname + document.location.search,\n          pendingNav() {\n            // 아무 작업도 하지 않습니다. URL은 이미 업데이트되었습니다.\n          },\n        });\n      });\n    }\n    window.addEventListener(\"popstate\", handlePopState);\n    return () => {\n      window.removeEventListener(\"popstate\", handlePopState);\n    };\n  }, []);\n  const pendingNav = routerState.pendingNav;\n  useLayoutEffect(() => {\n    pendingNav();\n  }, [pendingNav]);\n\n  return (\n    <RouterContext\n      value={{\n        url: routerState.url,\n        navigate,\n        navigateBack,\n        isPending,\n        params: {},\n      }}\n    >\n      {children}\n    </RouterContext>\n  );\n}\n\nconst RouterContext = createContext({ url: \"/\", params: {} });\n\nexport function useRouter() {\n  return use(RouterContext);\n}\n\nexport function useIsNavPending() {\n  return use(RouterContext).isPending;\n}\n\n```\n\n```css src/styles.css hidden\n@font-face {\n  font-family: Optimistic Text;\n  src: url(https://react.dev/fonts/Optimistic_Text_W_Rg.woff2) format(\"woff2\");\n  font-weight: 400;\n  font-style: normal;\n  font-display: swap;\n}\n\n@font-face {\n  font-family: Optimistic Text;\n  src: url(https://react.dev/fonts/Optimistic_Text_W_Md.woff2) format(\"woff2\");\n  font-weight: 500;\n  font-style: normal;\n  font-display: swap;\n}\n\n@font-face {\n  font-family: Optimistic Text;\n  src: url(https://react.dev/fonts/Optimistic_Text_W_Bd.woff2) format(\"woff2\");\n  font-weight: 600;\n  font-style: normal;\n  font-display: swap;\n}\n\n@font-face {\n  font-family: Optimistic Text;\n  src: url(https://react.dev/fonts/Optimistic_Text_W_Bd.woff2) format(\"woff2\");\n  font-weight: 700;\n  font-style: normal;\n  font-display: swap;\n}\n\n* {\n  box-sizing: border-box;\n}\n\nhtml {\n  background-image: url(https://react.dev/images/meta-gradient-dark.png);\n  background-size: 100%;\n  background-position: -100%;\n  background-color: rgb(64 71 86);\n  background-repeat: no-repeat;\n  height: 100%;\n  width: 100%;\n}\n\nbody {\n  font-family: Optimistic Text, -apple-system, ui-sans-serif, system-ui, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;\n  padding: 10px 0 10px 0;\n  margin: 0;\n  display: flex;\n  justify-content: center;\n}\n\n#root {\n  flex: 1 1;\n  height: auto;\n  background-color: #fff;\n  border-radius: 10px;\n  max-width: 450px;\n  min-height: 600px;\n  padding-bottom: 10px;\n}\n\nh1 {\n  margin-top: 0;\n  font-size: 22px;\n}\n\nh2 {\n  margin-top: 0;\n  font-size: 20px;\n}\n\nh3 {\n  margin-top: 0;\n  font-size: 18px;\n}\n\nh4 {\n  margin-top: 0;\n  font-size: 16px;\n}\n\nh5 {\n  margin-top: 0;\n  font-size: 14px;\n}\n\nh6 {\n  margin-top: 0;\n  font-size: 12px;\n}\n\ncode {\n  font-size: 1.2em;\n}\n\nul {\n  padding-inline-start: 20px;\n}\n\n.sr-only {\n  position: absolute;\n  width: 1px;\n  height: 1px;\n  padding: 0;\n  margin: -1px;\n  overflow: hidden;\n  clip: rect(0, 0, 0, 0);\n  white-space: nowrap;\n  border-width: 0;\n}\n\n.absolute {\n  position: absolute;\n}\n\n.overflow-visible {\n  overflow: visible;\n}\n\n.visible {\n  overflow: visible;\n}\n\n.fit {\n  width: fit-content;\n}\n\n\n/* Layout */\n.page {\n  display: flex;\n  flex-direction: column;\n  height: 100%;\n}\n\n.top-hero {\n  height: 200px;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  background-image: conic-gradient(\n      from 90deg at -10% 100%,\n      #2b303b 0deg,\n      #2b303b 90deg,\n      #16181d 1turn\n  );\n}\n\n.bottom {\n  flex: 1;\n  overflow: auto;\n}\n\n.top-nav {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  margin-bottom: 0;\n  padding: 0 12px;\n  top: 0;\n  width: 100%;\n  height: 44px;\n  color: #23272f;\n  font-weight: 700;\n  font-size: 20px;\n  z-index: 100;\n  cursor: default;\n}\n\n.content {\n  padding: 0 12px;\n  margin-top: 4px;\n}\n\n\n.loader {\n  color: #23272f;\n  font-size: 3px;\n  width: 1em;\n  margin-right: 18px;\n  height: 1em;\n  border-radius: 50%;\n  position: relative;\n  text-indent: -9999em;\n  animation: loading-spinner 1.3s infinite linear;\n  animation-delay: 200ms;\n  transform: translateZ(0);\n}\n\n@keyframes loading-spinner {\n  0%,\n  100% {\n    box-shadow: 0 -3em 0 0.2em,\n    2em -2em 0 0em, 3em 0 0 -1em,\n    2em 2em 0 -1em, 0 3em 0 -1em,\n    -2em 2em 0 -1em, -3em 0 0 -1em,\n    -2em -2em 0 0;\n  }\n  12.5% {\n    box-shadow: 0 -3em 0 0, 2em -2em 0 0.2em,\n    3em 0 0 0, 2em 2em 0 -1em, 0 3em 0 -1em,\n    -2em 2em 0 -1em, -3em 0 0 -1em,\n    -2em -2em 0 -1em;\n  }\n  25% {\n    box-shadow: 0 -3em 0 -0.5em,\n    2em -2em 0 0, 3em 0 0 0.2em,\n    2em 2em 0 0, 0 3em 0 -1em,\n    -2em 2em 0 -1em, -3em 0 0 -1em,\n    -2em -2em 0 -1em;\n  }\n  37.5% {\n    box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em,\n    3em 0em 0 0, 2em 2em 0 0.2em, 0 3em 0 0em,\n    -2em 2em 0 -1em, -3em 0em 0 -1em, -2em -2em 0 -1em;\n  }\n  50% {\n    box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em,\n    3em 0 0 -1em, 2em 2em 0 0em, 0 3em 0 0.2em,\n    -2em 2em 0 0, -3em 0em 0 -1em, -2em -2em 0 -1em;\n  }\n  62.5% {\n    box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em,\n    3em 0 0 -1em, 2em 2em 0 -1em, 0 3em 0 0,\n    -2em 2em 0 0.2em, -3em 0 0 0, -2em -2em 0 -1em;\n  }\n  75% {\n    box-shadow: 0em -3em 0 -1em, 2em -2em 0 -1em,\n    3em 0em 0 -1em, 2em 2em 0 -1em, 0 3em 0 -1em,\n    -2em 2em 0 0, -3em 0em 0 0.2em, -2em -2em 0 0;\n  }\n  87.5% {\n    box-shadow: 0em -3em 0 0, 2em -2em 0 -1em,\n    3em 0 0 -1em, 2em 2em 0 -1em, 0 3em 0 -1em,\n    -2em 2em 0 0, -3em 0em 0 0, -2em -2em 0 0.2em;\n  }\n}\n\n/* LikeButton */\n.like-button {\n  outline-offset: 2px;\n  position: relative;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  width: 2.5rem;\n  height: 2.5rem;\n  cursor: pointer;\n  border-radius: 9999px;\n  border: none;\n  outline: none 2px;\n  color: #5e687e;\n  background: none;\n}\n\n.like-button:focus {\n  color: #a6423a;\n  background-color: rgba(166, 66, 58, .05);\n}\n\n.like-button:active {\n  color: #a6423a;\n  background-color: rgba(166, 66, 58, .05);\n  transform: scaleX(0.95) scaleY(0.95);\n}\n\n.like-button:hover {\n  background-color: #f6f7f9;\n}\n\n.like-button.liked {\n  color: #a6423a;\n}\n\n/* Icons */\n@keyframes circle {\n  0% {\n    transform: scale(0);\n    stroke-width: 16px;\n  }\n\n  50% {\n    transform: scale(.5);\n    stroke-width: 16px;\n  }\n\n  to {\n    transform: scale(1);\n    stroke-width: 0;\n  }\n}\n\n.circle {\n  color: rgba(166, 66, 58, .5);\n  transform-origin: center;\n  transition-property: all;\n  transition-duration: .15s;\n  transition-timing-function: cubic-bezier(.4,0,.2,1);\n}\n\n.circle.liked.animate {\n  animation: circle .3s forwards;\n}\n\n.heart {\n  width: 1.5rem;\n  height: 1.5rem;\n}\n\n.heart.liked {\n  transform-origin: center;\n  transition-property: all;\n  transition-duration: .15s;\n  transition-timing-function: cubic-bezier(.4, 0, .2, 1);\n}\n\n.heart.liked.animate {\n  animation: scale .35s ease-in-out forwards;\n}\n\n.control-icon {\n  color: hsla(0, 0%, 100%, .5);\n  filter:  drop-shadow(0 20px 13px rgba(0, 0, 0, .03)) drop-shadow(0 8px 5px rgba(0, 0, 0, .08));\n}\n\n.chevron-left {\n  margin-top: 2px;\n  rotate: 90deg;\n}\n\n\n/* Video */\n.thumbnail {\n  position: relative;\n  aspect-ratio: 16 / 9;\n  display: flex;\n  overflow: hidden;\n  flex-direction: column;\n  justify-content: center;\n  align-items: center;\n  border-radius: 0.5rem;\n  outline-offset: 2px;\n  width: 8rem;\n  vertical-align: middle;\n  background-color: #ffffff;\n  background-size: cover;\n  user-select: none;\n}\n\n.thumbnail.blue {\n  background-image: conic-gradient(at top right, #c76a15, #087ea4, #2b3491);\n}\n\n.thumbnail.red {\n  background-image: conic-gradient(at top right, #c76a15, #a6423a, #2b3491);\n}\n\n.thumbnail.green {\n  background-image: conic-gradient(at top right, #c76a15, #388f7f, #2b3491);\n}\n\n.thumbnail.purple {\n  background-image: conic-gradient(at top right, #c76a15, #575fb7, #2b3491);\n}\n\n.thumbnail.yellow {\n  background-image: conic-gradient(at top right, #c76a15, #FABD62, #2b3491);\n}\n\n.thumbnail.gray {\n  background-image: conic-gradient(at top right, #c76a15, #4E5769, #2b3491);\n}\n\n.video {\n  display: flex;\n  flex-direction: row;\n  gap: 0.75rem;\n  align-items: center;\n}\n\n.video .link {\n  display: flex;\n  flex-direction: row;\n  flex: 1 1 0;\n  gap: 0.125rem;\n  outline-offset: 4px;\n  cursor: pointer;\n}\n\n.video .info {\n  display: flex;\n  flex-direction: column;\n  justify-content: center;\n  margin-left: 8px;\n  gap: 0.125rem;\n}\n\n.video .info:hover {\n  text-decoration: underline;\n}\n\n.video-title {\n  font-size: 15px;\n  line-height: 1.25;\n  font-weight: 700;\n  color: #23272f;\n}\n\n.video-description {\n  color: #5e687e;\n  font-size: 13px;\n}\n\n/* Details */\n.details .thumbnail {\n  position: relative;\n  aspect-ratio: 16 / 9;\n  display: flex;\n  overflow: hidden;\n  flex-direction: column;\n  justify-content: center;\n  align-items: center;\n  border-radius: 0.5rem;\n  outline-offset: 2px;\n  width: 100%;\n  vertical-align: middle;\n  background-color: #ffffff;\n  background-size: cover;\n  user-select: none;\n}\n\n.video-details-title {\n  margin-top: 8px;\n}\n\n.video-details-speaker {\n  display: flex;\n  gap: 8px;\n  margin-top: 10px\n}\n\n.back {\n  display: flex;\n  align-items: center;\n  margin-left: -5px;\n  cursor: pointer;\n}\n\n.back:hover {\n  text-decoration: underline;\n}\n\n.info-title {\n  font-size: 1.5rem;\n  font-weight: 700;\n  line-height: 1.25;\n  margin: 8px 0 0 0 ;\n}\n\n.info-description {\n  margin: 8px 0 0 0;\n}\n\n.controls {\n  cursor: pointer;\n}\n\n.fallback {\n  background: #f6f7f8 linear-gradient(to right, #e6e6e6 5%, #cccccc 25%, #e6e6e6 35%) no-repeat;\n  background-size: 800px 104px;\n  display: block;\n  line-height: 1.25;\n  margin: 8px 0 0 0;\n  border-radius: 5px;\n  overflow: hidden;\n\n  animation: 1s linear 1s infinite shimmer;\n  animation-delay: 300ms;\n  animation-duration: 1s;\n  animation-fill-mode: forwards;\n  animation-iteration-count: infinite;\n  animation-name: shimmer;\n  animation-timing-function: linear;\n}\n\n\n.fallback.title {\n  width: 130px;\n  height: 30px;\n\n}\n\n.fallback.description {\n  width: 150px;\n  height: 21px;\n}\n\n@keyframes shimmer {\n  0% {\n    background-position: -468px 0;\n  }\n\n  100% {\n    background-position: 468px 0;\n  }\n}\n\n.search {\n  margin-bottom: 10px;\n}\n.search-input {\n  width: 100%;\n  position: relative;\n}\n\n.search-icon {\n  position: absolute;\n  top: 0;\n  bottom: 0;\n  inset-inline-start: 0;\n  display: flex;\n  align-items: center;\n  padding-inline-start: 1rem;\n  pointer-events: none;\n  color: #99a1b3;\n}\n\n.search-input input {\n  display: flex;\n  padding-inline-start: 2.75rem;\n  padding-top: 10px;\n  padding-bottom: 10px;\n  width: 100%;\n  text-align: start;\n  background-color: rgb(235 236 240);\n  outline: 2px solid transparent;\n  cursor: pointer;\n  border: none;\n  align-items: center;\n  color: rgb(35 39 47);\n  border-radius: 9999px;\n  vertical-align: middle;\n  font-size: 15px;\n}\n\n.search-input input:hover, .search-input input:active {\n  background-color: rgb(235 236 240/ 0.8);\n  color: rgb(35 39 47/ 0.8);\n}\n\n/* Home */\n.video-list {\n  position: relative;\n}\n\n.video-list .videos {\n  display: flex;\n  flex-direction: column;\n  gap: 1rem;\n  overflow-y: auto;\n  height: 100%;\n}\n```\n\n\n```css src/animations.css\n/* 폴백을 아래로 슬라이드합니다 */\n::view-transition-old(.slide-down) {\n    animation: 150ms ease-out both fade-out, 150ms ease-out both slide-down;\n}\n\n/* 콘텐츠를 위로 슬라이드합니다 */\n::view-transition-new(.slide-up) {\n    animation: 210ms ease-in 150ms both fade-in, 400ms ease-in both slide-up;\n}\n\n/* 새로운 키프레임을 정의합니다 */\n@keyframes slide-up {\n    from {\n        transform: translateY(10px);\n    }\n    to {\n        transform: translateY(0);\n    }\n}\n\n@keyframes slide-down {\n    from {\n        transform: translateY(0);\n    }\n    to {\n        transform: translateY(10px);\n    }\n}\n\n/* 이전에 정의된 애니메이션은 아래에 있습니다 */\n\n/* 트랜지션 타입에 의해 추가된 View Transition 클래스용 애니메이션 */\n::view-transition-old(.slide-forward) {\n    /* 앞으로 전환할 때 \"old\" 페이지는 왼쪽으로 슬라이드되어 나가야 합니다. */\n    animation: 150ms cubic-bezier(0.4, 0, 1, 1) both fade-out,\n    400ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;\n}\n\n::view-transition-new(.slide-forward) {\n    /* 앞으로 전환할 때 \"new\" 페이지는 오른쪽에서 슬라이드되어 들어와야 합니다. */\n    animation: 210ms cubic-bezier(0, 0, 0.2, 1) 150ms both fade-in,\n    400ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;\n}\n\n::view-transition-old(.slide-back) {\n    /* 뒤로 전환할 때 \"old\" 페이지는 오른쪽으로 슬라이드되어 나가야 합니다. */\n    animation: 150ms cubic-bezier(0.4, 0, 1, 1) both fade-out,\n    400ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-right;\n}\n\n::view-transition-new(.slide-back) {\n    /* 뒤로 전환할 때 \"new\" 페이지는 왼쪽에서 슬라이드되어 들어와야 합니다. */\n    animation: 210ms cubic-bezier(0, 0, 0.2, 1) 150ms both fade-in,\n    400ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-left;\n}\n\n/* 위 애니메이션을 지원하기 위한 키프레임 */\n@keyframes fade-in {\n    from {\n        opacity: 0;\n    }\n}\n\n@keyframes fade-out {\n    to {\n        opacity: 0;\n    }\n}\n\n@keyframes slide-to-right {\n    to {\n        transform: translateX(50px);\n    }\n}\n\n@keyframes slide-from-right {\n    from {\n        transform: translateX(50px);\n    }\n    to {\n        transform: translateX(0);\n    }\n}\n\n@keyframes slide-to-left {\n    to {\n        transform: translateX(-50px);\n    }\n}\n\n@keyframes slide-from-left {\n    from {\n        transform: translateX(-50px);\n    }\n    to {\n        transform: translateX(0);\n    }\n}\n\n/* Default .slow-fade. */\n::view-transition-old(.slow-fade) {\n    animation-duration: 500ms;\n}\n\n::view-transition-new(.slow-fade) {\n    animation-duration: 500ms;\n}\n```\n\n```js src/index.js hidden\nimport React, {StrictMode} from 'react';\nimport {createRoot} from 'react-dom/client';\nimport './styles.css';\nimport './animations.css';\n\nimport App from './App';\nimport {Router} from './router';\n\nconst root = createRoot(document.getElementById('root'));\nroot.render(\n  <StrictMode>\n    <Router>\n      <App />\n    </Router>\n  </StrictMode>\n);\n```\n\n```json package.json hidden\n{\n  \"dependencies\": {\n    \"react\": \"canary\",\n    \"react-dom\": \"canary\",\n    \"react-scripts\": \"latest\"\n  },\n  \"scripts\": {\n    \"start\": \"react-scripts start\",\n    \"build\": \"react-scripts build\",\n    \"test\": \"react-scripts test --env=jsdom\",\n    \"eject\": \"react-scripts eject\"\n  }\n}\n```\n\n</Sandpack>\n\n\n### 목록 애니메이팅 {/*animating-lists*/}\n\n검색 가능 항목 목록에서처럼 `<ViewTransition>`을 사용하여 항목 목록이 재정렬될 때 애니메이션을 적용할 수도 있습니다.\n\n```js {3,5}\n<div className=\"videos\">\n  {filteredVideos.map((video) => (\n    <ViewTransition key={video.id}>\n      <Video video={video} />\n    </ViewTransition>\n  ))}\n</div>\n```\n\nViewTransition을 활성화하려면 `useDeferredValue`를 사용할 수 있습니다.\n\n```js {2}\nconst [searchText, setSearchText] = useState('');\nconst deferredSearchText = useDeferredValue(searchText);\nconst filteredVideos = filterVideos(videos, deferredSearchText);\n```\n\n이제 검색창에 입력할 때 항목에 애니메이션이 적용됩니다.\n\n<Sandpack>\n\n```js src/App.js hidden\nimport { ViewTransition } from \"react\";\nimport Details from \"./Details\";\nimport Home from \"./Home\";\nimport { useRouter } from \"./router\";\n\nexport default function App() {\n  const { url } = useRouter();\n\n  // Default slow-fade animation.\n  return (\n    <ViewTransition default=\"slow-fade\">\n      {url === \"/\" ? <Home /> : <Details />}\n    </ViewTransition>\n  );\n}\n```\n\n```js src/Details.js hidden\nimport { use, Suspense, ViewTransition } from \"react\";\nimport { fetchVideo, fetchVideoDetails } from \"./data\";\nimport { Thumbnail, VideoControls } from \"./Videos\";\nimport { useRouter } from \"./router\";\nimport Layout from \"./Layout\";\nimport { ChevronLeft } from \"./Icons\";\n\nfunction VideoDetails({id}) {\n  // 페이지 간 교차 페이드 애니메이션을 적용합니다.\n  return (\n    <Suspense\n      fallback={\n        // 폴백을 아래로 애니메이션합니다.\n        <ViewTransition exit=\"slide-down\">\n          <VideoInfoFallback />\n        </ViewTransition>\n      }\n    >\n      {/* 콘텐츠를 위로 애니메이션합니다 */}\n      <ViewTransition enter=\"slide-up\">\n        <VideoInfo id={id} />\n      </ViewTransition>\n    </Suspense>\n  );\n}\n\nfunction VideoInfoFallback() {\n  return (\n    <>\n      <div className=\"fallback title\"></div>\n      <div className=\"fallback description\"></div>\n    </>\n  );\n}\n\nexport default function Details() {\n  const { url, navigateBack } = useRouter();\n  const videoId = url.split(\"/\").pop();\n  const video = use(fetchVideo(videoId));\n\n  return (\n    <Layout\n      heading={\n        <div\n          className=\"fit back\"\n          onClick={() => {\n            navigateBack(\"/\");\n          }}\n        >\n          <ChevronLeft /> Back\n        </div>\n      }\n    >\n      <div className=\"details\">\n        <Thumbnail video={video} large>\n          <VideoControls />\n        </Thumbnail>\n        <VideoDetails id={video.id} />\n      </div>\n    </Layout>\n  );\n}\n\nfunction VideoInfo({ id }) {\n  const details = use(fetchVideoDetails(id));\n  return (\n    <>\n      <p className=\"info-title\">{details.title}</p>\n      <p className=\"info-description\">{details.description}</p>\n    </>\n  );\n}\n```\n\n```js src/Home.js\nimport { useId, useState, use, useDeferredValue, ViewTransition } from \"react\";import { Video } from \"./Videos\";import Layout from \"./Layout\";import { fetchVideos } from \"./data\";import { IconSearch } from \"./Icons\";\n\nfunction SearchList({searchText, videos}) {\n  // useDeferredValue로 활성화합니다(\"언제\")\n  const deferredSearchText = useDeferredValue(searchText);\n  const filteredVideos = filterVideos(videos, deferredSearchText);\n  return (\n    <div className=\"video-list\">\n      <div className=\"videos\">\n        {filteredVideos.map((video) => (\n          // 리스트의 각 항목을 애니메이션합니다(\"무엇\")\n          <ViewTransition key={video.id}>\n            <Video video={video} />\n          </ViewTransition>\n        ))}\n      </div>\n      {filteredVideos.length === 0 && (\n        <div className=\"no-results\">No results</div>\n      )}\n    </div>\n  );\n}\n\nexport default function Home() {\n  const videos = use(fetchVideos());\n  const count = videos.length;\n  const [searchText, setSearchText] = useState('');\n\n  return (\n    <Layout heading={<div className=\"fit\">{count} Videos</div>}>\n      <SearchInput value={searchText} onChange={setSearchText} />\n      <SearchList videos={videos} searchText={searchText} />\n    </Layout>\n  );\n}\n\nfunction SearchInput({ value, onChange }) {\n  const id = useId();\n  return (\n    <form className=\"search\" onSubmit={(e) => e.preventDefault()}>\n      <label htmlFor={id} className=\"sr-only\">\n        Search\n      </label>\n      <div className=\"search-input\">\n        <div className=\"search-icon\">\n          <IconSearch />\n        </div>\n        <input\n          type=\"text\"\n          id={id}\n          placeholder=\"Search\"\n          value={value}\n          onChange={(e) => onChange(e.target.value)}\n        />\n      </div>\n    </form>\n  );\n}\n\nfunction filterVideos(videos, query) {\n  const keywords = query\n    .toLowerCase()\n    .split(\" \")\n    .filter((s) => s !== \"\");\n  if (keywords.length === 0) {\n    return videos;\n  }\n  return videos.filter((video) => {\n    const words = (video.title + \" \" + video.description)\n      .toLowerCase()\n      .split(\" \");\n    return keywords.every((kw) => words.some((w) => w.includes(kw)));\n  });\n}\n```\n\n```js src/Icons.js hidden\nexport function ChevronLeft() {\n  return (\n    <svg\n      className=\"chevron-left\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width=\"20\"\n      height=\"20\"\n      viewBox=\"0 0 20 20\">\n      <g fill=\"none\" fillRule=\"evenodd\" transform=\"translate(-446 -398)\">\n        <path\n          fill=\"currentColor\"\n          fillRule=\"nonzero\"\n          d=\"M95.8838835,240.366117 C95.3957281,239.877961 94.6042719,239.877961 94.1161165,240.366117 C93.6279612,240.854272 93.6279612,241.645728 94.1161165,242.133883 L98.6161165,246.633883 C99.1042719,247.122039 99.8957281,247.122039 100.383883,246.633883 L104.883883,242.133883 C105.372039,241.645728 105.372039,240.854272 104.883883,240.366117 C104.395728,239.877961 103.604272,239.877961 103.116117,240.366117 L99.5,243.982233 L95.8838835,240.366117 Z\"\n          transform=\"translate(356.5 164.5)\"\n        />\n        <polygon points=\"446 418 466 418 466 398 446 398\" />\n      </g>\n    </svg>\n  );\n}\n\nexport function PauseIcon() {\n  return (\n    <svg\n      className=\"control-icon\"\n      style={{padding: '4px'}}\n      width=\"100\"\n      height=\"100\"\n      viewBox=\"0 0 512 512\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\">\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M256 0C114.617 0 0 114.615 0 256s114.617 256 256 256 256-114.615 256-256S397.383 0 256 0zm-32 320c0 8.836-7.164 16-16 16h-32c-8.836 0-16-7.164-16-16V192c0-8.836 7.164-16 16-16h32c8.836 0 16 7.164 16 16v128zm128 0c0 8.836-7.164 16-16 16h-32c-8.836 0-16-7.164-16-16V192c0-8.836 7.164-16 16-16h32c8.836 0 16 7.164 16 16v128z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  );\n}\n\nexport function PlayIcon() {\n  return (\n    <svg\n      className=\"control-icon\"\n      width=\"100\"\n      height=\"100\"\n      viewBox=\"0 0 72 72\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\">\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M36 69C54.2254 69 69 54.2254 69 36C69 17.7746 54.2254 3 36 3C17.7746 3 3 17.7746 3 36C3 54.2254 17.7746 69 36 69ZM52.1716 38.6337L28.4366 51.5801C26.4374 52.6705 24 51.2235 24 48.9464V23.0536C24 20.7764 26.4374 19.3295 28.4366 20.4199L52.1716 33.3663C54.2562 34.5034 54.2562 37.4966 52.1716 38.6337Z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  );\n}\nexport function Heart({liked, animate}) {\n  return (\n    <>\n      <svg\n        className=\"absolute overflow-visible\"\n        viewBox=\"0 0 24 24\"\n        fill=\"none\"\n        xmlns=\"http://www.w3.org/2000/svg\">\n        <circle\n          className={`circle ${liked ? 'liked' : ''} ${animate ? 'animate' : ''}`}\n          cx=\"12\"\n          cy=\"12\"\n          r=\"11.5\"\n          fill=\"transparent\"\n          strokeWidth=\"0\"\n          stroke=\"currentColor\"\n        />\n      </svg>\n\n      <svg\n        className={`heart ${liked ? 'liked' : ''} ${animate ? 'animate' : ''}`}\n        viewBox=\"0 0 24 24\"\n        fill=\"none\"\n        xmlns=\"http://www.w3.org/2000/svg\">\n        {liked ? (\n          <path\n            d=\"M12 23a.496.496 0 0 1-.26-.074C7.023 19.973 0 13.743 0 8.68c0-4.12 2.322-6.677 6.058-6.677 2.572 0 5.108 2.387 5.134 2.41l.808.771.808-.771C12.834 4.387 15.367 2 17.935 2 21.678 2 24 4.558 24 8.677c0 5.06-7.022 11.293-11.74 14.246a.496.496 0 0 1-.26.074V23z\"\n            fill=\"currentColor\"\n          />\n        ) : (\n          <path\n            fillRule=\"evenodd\"\n            clipRule=\"evenodd\"\n            d=\"m12 5.184-.808-.771-.004-.004C11.065 4.299 8.522 2.003 6 2.003c-3.736 0-6 2.558-6 6.677 0 4.47 5.471 9.848 10 13.079.602.43 1.187.82 1.74 1.167A.497.497 0 0 0 12 23v-.003c.09 0 .182-.026.26-.074C16.977 19.97 24 13.737 24 8.677 24 4.557 21.743 2 18 2c-2.569 0-5.166 2.387-5.192 2.413L12 5.184zm-.002 15.525c2.071-1.388 4.477-3.342 6.427-5.47C20.72 12.733 22 10.401 22 8.677c0-1.708-.466-2.855-1.087-3.55C20.316 4.459 19.392 4 18 4c-.726 0-1.63.364-2.5.9-.67.412-1.148.82-1.266.92-.03.025-.037.031-.019.014l-.013.013L12 7.949 9.832 5.88a10.08 10.08 0 0 0-1.33-.977C7.633 4.367 6.728 4.003 6 4.003c-1.388 0-2.312.459-2.91 1.128C2.466 5.826 2 6.974 2 8.68c0 1.726 1.28 4.058 3.575 6.563 1.948 2.127 4.352 4.078 6.423 5.466z\"\n            fill=\"currentColor\"\n          />\n        )}\n      </svg>\n    </>\n  );\n}\n\nexport function IconSearch(props) {\n  return (\n    <svg width=\"1em\" height=\"1em\" viewBox=\"0 0 20 20\">\n      <path\n        d=\"M14.386 14.386l4.0877 4.0877-4.0877-4.0877c-2.9418 2.9419-7.7115 2.9419-10.6533 0-2.9419-2.9418-2.9419-7.7115 0-10.6533 2.9418-2.9419 7.7115-2.9419 10.6533 0 2.9419 2.9418 2.9419 7.7115 0 10.6533z\"\n        stroke=\"currentColor\"\n        fill=\"none\"\n        strokeWidth=\"2\"\n        fillRule=\"evenodd\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"></path>\n    </svg>\n  );\n}\n```\n\n```js src/Layout.js hidden\nimport {ViewTransition} from 'react';\nimport { useIsNavPending } from \"./router\";\n\nexport default function Page({ heading, children }) {\n  const isPending = useIsNavPending();\n  return (\n    <div className=\"page\">\n      <div className=\"top\">\n        <div className=\"top-nav\">\n          {/* 트랜지션 타입에 따라 커스텀 클래스를 적용합니다. */}\n          <ViewTransition\n            name=\"nav\"\n            share={{\n              'nav-forward': 'slide-forward',\n              'nav-back': 'slide-back',\n            }}>\n            {heading}\n          </ViewTransition>\n          {isPending && <span className=\"loader\"></span>}\n        </div>\n      </div>\n      {/* 콘텐츠에 대해 ViewTransition을 사용하지 않습니다. */}\n      {/* 콘텐츠는 자체 ViewTransition을 정의할 수 있습니다. */}\n      <ViewTransition default=\"none\">\n        <div className=\"bottom\">\n          <div className=\"content\">{children}</div>\n        </div>\n      </ViewTransition>\n    </div>\n  );\n}\n```\n\n```js src/LikeButton.js hidden\nimport {useState} from 'react';\nimport {Heart} from './Icons';\n\n// A hack since we don't actually have a backend.\n// Unlike local state, this survives videos being filtered.\nconst likedVideos = new Set();\n\nexport default function LikeButton({video}) {\n  const [isLiked, setIsLiked] = useState(() => likedVideos.has(video.id));\n  const [animate, setAnimate] = useState(false);\n  return (\n    <button\n      className={`like-button ${isLiked && 'liked'}`}\n      aria-label={isLiked ? 'Unsave' : 'Save'}\n      onClick={() => {\n        const nextIsLiked = !isLiked;\n        if (nextIsLiked) {\n          likedVideos.add(video.id);\n        } else {\n          likedVideos.delete(video.id);\n        }\n        setAnimate(true);\n        setIsLiked(nextIsLiked);\n      }}>\n      <Heart liked={isLiked} animate={animate} />\n    </button>\n  );\n}\n```\n\n```js src/Videos.js hidden\nimport { useState, ViewTransition } from \"react\";\nimport LikeButton from \"./LikeButton\";\nimport { useRouter } from \"./router\";\nimport { PauseIcon, PlayIcon } from \"./Icons\";\nimport { startTransition } from \"react\";\n\nexport function Thumbnail({ video, children }) {\n  // 공유 요소 트랜지션으로 애니메이션되도록 name을 추가합니다.\n  // 기본 애니메이션을 사용하므로 추가 CSS가 필요하지 않습니다.\n  return (\n    <ViewTransition name={`video-${video.id}`}>\n      <div\n        aria-hidden=\"true\"\n        tabIndex={-1}\n        className={`thumbnail ${video.image}`}\n      >\n        {children}\n      </div>\n    </ViewTransition>\n  );\n}\n\nexport function VideoControls() {\n  const [isPlaying, setIsPlaying] = useState(false);\n\n  return (\n    <span\n      className=\"controls\"\n      onClick={() =>\n        startTransition(() => {\n          setIsPlaying((p) => !p);\n        })\n      }\n    >\n      {isPlaying ? <PauseIcon /> : <PlayIcon />}\n    </span>\n  );\n}\n\nexport function Video({ video }) {\n  const { navigate } = useRouter();\n\n  return (\n    <div className=\"video\">\n      <div\n        className=\"link\"\n        onClick={(e) => {\n          e.preventDefault();\n          navigate(`/video/${video.id}`);\n        }}\n      >\n        <Thumbnail video={video}></Thumbnail>\n\n        <div className=\"info\">\n          <div className=\"video-title\">{video.title}</div>\n          <div className=\"video-description\">{video.description}</div>\n        </div>\n      </div>\n      <LikeButton video={video} />\n    </div>\n  );\n}\n```\n\n\n```js src/data.js hidden\nconst videos = [\n  {\n    id: '1',\n    title: 'First video',\n    description: 'Video description',\n    image: 'blue',\n  },\n  {\n    id: '2',\n    title: 'Second video',\n    description: 'Video description',\n    image: 'red',\n  },\n  {\n    id: '3',\n    title: 'Third video',\n    description: 'Video description',\n    image: 'green',\n  },\n  {\n    id: '4',\n    title: 'Fourth video',\n    description: 'Video description',\n    image: 'purple',\n  },\n  {\n    id: '5',\n    title: 'Fifth video',\n    description: 'Video description',\n    image: 'yellow',\n  },\n  {\n    id: '6',\n    title: 'Sixth video',\n    description: 'Video description',\n    image: 'gray',\n  },\n];\n\nlet videosCache = new Map();\nlet videoCache = new Map();\nlet videoDetailsCache = new Map();\nconst VIDEO_DELAY = 1;\nconst VIDEO_DETAILS_DELAY = 1000;\nexport function fetchVideos() {\n  if (videosCache.has(0)) {\n    return videosCache.get(0);\n  }\n  const promise = new Promise((resolve) => {\n    setTimeout(() => {\n      resolve(videos);\n    }, VIDEO_DELAY);\n  });\n  videosCache.set(0, promise);\n  return promise;\n}\n\nexport function fetchVideo(id) {\n  if (videoCache.has(id)) {\n    return videoCache.get(id);\n  }\n  const promise = new Promise((resolve) => {\n    setTimeout(() => {\n      resolve(videos.find((video) => video.id === id));\n    }, VIDEO_DELAY);\n  });\n  videoCache.set(id, promise);\n  return promise;\n}\n\nexport function fetchVideoDetails(id) {\n  if (videoDetailsCache.has(id)) {\n    return videoDetailsCache.get(id);\n  }\n  const promise = new Promise((resolve) => {\n    setTimeout(() => {\n      resolve(videos.find((video) => video.id === id));\n    }, VIDEO_DETAILS_DELAY);\n  });\n  videoDetailsCache.set(id, promise);\n  return promise;\n}\n```\n\n```js src/router.js hidden\nimport {useState, createContext, use, useTransition, useLayoutEffect, useEffect, addTransitionType} from \"react\";\n\nexport function Router({ children }) {\n  const [isPending, startTransition] = useTransition();\n  const [routerState, setRouterState] = useState({pendingNav: () => {}, url: document.location.pathname});\n  function navigate(url) {\n    startTransition(() => {\n      // 전환 원인이 \"nav forward\"인 경우의 트랜지션 타입\n      addTransitionType('nav-forward');\n      go(url);\n    });\n  }\n  function navigateBack(url) {\n    startTransition(() => {\n      // 전환 원인이 \"nav backward\"인 경우의 트랜지션 타입\n      addTransitionType('nav-back');\n      go(url);\n    });\n  }\n\n  function go(url) {\n    setRouterState({\n      url,\n      pendingNav() {\n        window.history.pushState({}, \"\", url);\n      },\n    });\n  }\n\n  useEffect(() => {\n    function handlePopState() {\n      // 복원은 동기적으로 이루어져야 하므로 애니메이션되면 안 됩니다.\n      // 트랜지션이더라도 마찬가지입니다.\n      startTransition(() => {\n        setRouterState({\n          url: document.location.pathname + document.location.search,\n          pendingNav() {\n            // 아무 작업도 하지 않습니다. URL은 이미 업데이트되었습니다.\n          },\n        });\n      });\n    }\n    window.addEventListener(\"popstate\", handlePopState);\n    return () => {\n      window.removeEventListener(\"popstate\", handlePopState);\n    };\n  }, []);\n  const pendingNav = routerState.pendingNav;\n  useLayoutEffect(() => {\n    pendingNav();\n  }, [pendingNav]);\n\n  return (\n    <RouterContext\n      value={{\n        url: routerState.url,\n        navigate,\n        navigateBack,\n        isPending,\n        params: {},\n      }}\n    >\n      {children}\n    </RouterContext>\n  );\n}\n\nconst RouterContext = createContext({ url: \"/\", params: {} });\n\nexport function useRouter() {\n  return use(RouterContext);\n}\n\nexport function useIsNavPending() {\n  return use(RouterContext).isPending;\n}\n\n```\n\n```css src/styles.css hidden\n@font-face {\n  font-family: Optimistic Text;\n  src: url(https://react.dev/fonts/Optimistic_Text_W_Rg.woff2) format(\"woff2\");\n  font-weight: 400;\n  font-style: normal;\n  font-display: swap;\n}\n\n@font-face {\n  font-family: Optimistic Text;\n  src: url(https://react.dev/fonts/Optimistic_Text_W_Md.woff2) format(\"woff2\");\n  font-weight: 500;\n  font-style: normal;\n  font-display: swap;\n}\n\n@font-face {\n  font-family: Optimistic Text;\n  src: url(https://react.dev/fonts/Optimistic_Text_W_Bd.woff2) format(\"woff2\");\n  font-weight: 600;\n  font-style: normal;\n  font-display: swap;\n}\n\n@font-face {\n  font-family: Optimistic Text;\n  src: url(https://react.dev/fonts/Optimistic_Text_W_Bd.woff2) format(\"woff2\");\n  font-weight: 700;\n  font-style: normal;\n  font-display: swap;\n}\n\n* {\n  box-sizing: border-box;\n}\n\nhtml {\n  background-image: url(https://react.dev/images/meta-gradient-dark.png);\n  background-size: 100%;\n  background-position: -100%;\n  background-color: rgb(64 71 86);\n  background-repeat: no-repeat;\n  height: 100%;\n  width: 100%;\n}\n\nbody {\n  font-family: Optimistic Text, -apple-system, ui-sans-serif, system-ui, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;\n  padding: 10px 0 10px 0;\n  margin: 0;\n  display: flex;\n  justify-content: center;\n}\n\n#root {\n  flex: 1 1;\n  height: auto;\n  background-color: #fff;\n  border-radius: 10px;\n  max-width: 450px;\n  min-height: 600px;\n  padding-bottom: 10px;\n}\n\nh1 {\n  margin-top: 0;\n  font-size: 22px;\n}\n\nh2 {\n  margin-top: 0;\n  font-size: 20px;\n}\n\nh3 {\n  margin-top: 0;\n  font-size: 18px;\n}\n\nh4 {\n  margin-top: 0;\n  font-size: 16px;\n}\n\nh5 {\n  margin-top: 0;\n  font-size: 14px;\n}\n\nh6 {\n  margin-top: 0;\n  font-size: 12px;\n}\n\ncode {\n  font-size: 1.2em;\n}\n\nul {\n  padding-inline-start: 20px;\n}\n\n.sr-only {\n  position: absolute;\n  width: 1px;\n  height: 1px;\n  padding: 0;\n  margin: -1px;\n  overflow: hidden;\n  clip: rect(0, 0, 0, 0);\n  white-space: nowrap;\n  border-width: 0;\n}\n\n.absolute {\n  position: absolute;\n}\n\n.overflow-visible {\n  overflow: visible;\n}\n\n.visible {\n  overflow: visible;\n}\n\n.fit {\n  width: fit-content;\n}\n\n\n/* Layout */\n.page {\n  display: flex;\n  flex-direction: column;\n  height: 100%;\n}\n\n.top-hero {\n  height: 200px;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  background-image: conic-gradient(\n      from 90deg at -10% 100%,\n      #2b303b 0deg,\n      #2b303b 90deg,\n      #16181d 1turn\n  );\n}\n\n.bottom {\n  flex: 1;\n  overflow: auto;\n}\n\n.top-nav {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  margin-bottom: 0;\n  padding: 0 12px;\n  top: 0;\n  width: 100%;\n  height: 44px;\n  color: #23272f;\n  font-weight: 700;\n  font-size: 20px;\n  z-index: 100;\n  cursor: default;\n}\n\n.content {\n  padding: 0 12px;\n  margin-top: 4px;\n}\n\n\n.loader {\n  color: #23272f;\n  font-size: 3px;\n  width: 1em;\n  margin-right: 18px;\n  height: 1em;\n  border-radius: 50%;\n  position: relative;\n  text-indent: -9999em;\n  animation: loading-spinner 1.3s infinite linear;\n  animation-delay: 200ms;\n  transform: translateZ(0);\n}\n\n@keyframes loading-spinner {\n  0%,\n  100% {\n    box-shadow: 0 -3em 0 0.2em,\n    2em -2em 0 0em, 3em 0 0 -1em,\n    2em 2em 0 -1em, 0 3em 0 -1em,\n    -2em 2em 0 -1em, -3em 0 0 -1em,\n    -2em -2em 0 0;\n  }\n  12.5% {\n    box-shadow: 0 -3em 0 0, 2em -2em 0 0.2em,\n    3em 0 0 0, 2em 2em 0 -1em, 0 3em 0 -1em,\n    -2em 2em 0 -1em, -3em 0 0 -1em,\n    -2em -2em 0 -1em;\n  }\n  25% {\n    box-shadow: 0 -3em 0 -0.5em,\n    2em -2em 0 0, 3em 0 0 0.2em,\n    2em 2em 0 0, 0 3em 0 -1em,\n    -2em 2em 0 -1em, -3em 0 0 -1em,\n    -2em -2em 0 -1em;\n  }\n  37.5% {\n    box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em,\n    3em 0em 0 0, 2em 2em 0 0.2em, 0 3em 0 0em,\n    -2em 2em 0 -1em, -3em 0em 0 -1em, -2em -2em 0 -1em;\n  }\n  50% {\n    box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em,\n    3em 0 0 -1em, 2em 2em 0 0em, 0 3em 0 0.2em,\n    -2em 2em 0 0, -3em 0em 0 -1em, -2em -2em 0 -1em;\n  }\n  62.5% {\n    box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em,\n    3em 0 0 -1em, 2em 2em 0 -1em, 0 3em 0 0,\n    -2em 2em 0 0.2em, -3em 0 0 0, -2em -2em 0 -1em;\n  }\n  75% {\n    box-shadow: 0em -3em 0 -1em, 2em -2em 0 -1em,\n    3em 0em 0 -1em, 2em 2em 0 -1em, 0 3em 0 -1em,\n    -2em 2em 0 0, -3em 0em 0 0.2em, -2em -2em 0 0;\n  }\n  87.5% {\n    box-shadow: 0em -3em 0 0, 2em -2em 0 -1em,\n    3em 0 0 -1em, 2em 2em 0 -1em, 0 3em 0 -1em,\n    -2em 2em 0 0, -3em 0em 0 0, -2em -2em 0 0.2em;\n  }\n}\n\n/* LikeButton */\n.like-button {\n  outline-offset: 2px;\n  position: relative;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  width: 2.5rem;\n  height: 2.5rem;\n  cursor: pointer;\n  border-radius: 9999px;\n  border: none;\n  outline: none 2px;\n  color: #5e687e;\n  background: none;\n}\n\n.like-button:focus {\n  color: #a6423a;\n  background-color: rgba(166, 66, 58, .05);\n}\n\n.like-button:active {\n  color: #a6423a;\n  background-color: rgba(166, 66, 58, .05);\n  transform: scaleX(0.95) scaleY(0.95);\n}\n\n.like-button:hover {\n  background-color: #f6f7f9;\n}\n\n.like-button.liked {\n  color: #a6423a;\n}\n\n/* Icons */\n@keyframes circle {\n  0% {\n    transform: scale(0);\n    stroke-width: 16px;\n  }\n\n  50% {\n    transform: scale(.5);\n    stroke-width: 16px;\n  }\n\n  to {\n    transform: scale(1);\n    stroke-width: 0;\n  }\n}\n\n.circle {\n  color: rgba(166, 66, 58, .5);\n  transform-origin: center;\n  transition-property: all;\n  transition-duration: .15s;\n  transition-timing-function: cubic-bezier(.4,0,.2,1);\n}\n\n.circle.liked.animate {\n  animation: circle .3s forwards;\n}\n\n.heart {\n  width: 1.5rem;\n  height: 1.5rem;\n}\n\n.heart.liked {\n  transform-origin: center;\n  transition-property: all;\n  transition-duration: .15s;\n  transition-timing-function: cubic-bezier(.4, 0, .2, 1);\n}\n\n.heart.liked.animate {\n  animation: scale .35s ease-in-out forwards;\n}\n\n.control-icon {\n  color: hsla(0, 0%, 100%, .5);\n  filter:  drop-shadow(0 20px 13px rgba(0, 0, 0, .03)) drop-shadow(0 8px 5px rgba(0, 0, 0, .08));\n}\n\n.chevron-left {\n  margin-top: 2px;\n  rotate: 90deg;\n}\n\n\n/* Video */\n.thumbnail {\n  position: relative;\n  aspect-ratio: 16 / 9;\n  display: flex;\n  overflow: hidden;\n  flex-direction: column;\n  justify-content: center;\n  align-items: center;\n  border-radius: 0.5rem;\n  outline-offset: 2px;\n  width: 8rem;\n  vertical-align: middle;\n  background-color: #ffffff;\n  background-size: cover;\n  user-select: none;\n}\n\n.thumbnail.blue {\n  background-image: conic-gradient(at top right, #c76a15, #087ea4, #2b3491);\n}\n\n.thumbnail.red {\n  background-image: conic-gradient(at top right, #c76a15, #a6423a, #2b3491);\n}\n\n.thumbnail.green {\n  background-image: conic-gradient(at top right, #c76a15, #388f7f, #2b3491);\n}\n\n.thumbnail.purple {\n  background-image: conic-gradient(at top right, #c76a15, #575fb7, #2b3491);\n}\n\n.thumbnail.yellow {\n  background-image: conic-gradient(at top right, #c76a15, #FABD62, #2b3491);\n}\n\n.thumbnail.gray {\n  background-image: conic-gradient(at top right, #c76a15, #4E5769, #2b3491);\n}\n\n.video {\n  display: flex;\n  flex-direction: row;\n  gap: 0.75rem;\n  align-items: center;\n}\n\n.video .link {\n  display: flex;\n  flex-direction: row;\n  flex: 1 1 0;\n  gap: 0.125rem;\n  outline-offset: 4px;\n  cursor: pointer;\n}\n\n.video .info {\n  display: flex;\n  flex-direction: column;\n  justify-content: center;\n  margin-left: 8px;\n  gap: 0.125rem;\n}\n\n.video .info:hover {\n  text-decoration: underline;\n}\n\n.video-title {\n  font-size: 15px;\n  line-height: 1.25;\n  font-weight: 700;\n  color: #23272f;\n}\n\n.video-description {\n  color: #5e687e;\n  font-size: 13px;\n}\n\n/* Details */\n.details .thumbnail {\n  position: relative;\n  aspect-ratio: 16 / 9;\n  display: flex;\n  overflow: hidden;\n  flex-direction: column;\n  justify-content: center;\n  align-items: center;\n  border-radius: 0.5rem;\n  outline-offset: 2px;\n  width: 100%;\n  vertical-align: middle;\n  background-color: #ffffff;\n  background-size: cover;\n  user-select: none;\n}\n\n.video-details-title {\n  margin-top: 8px;\n}\n\n.video-details-speaker {\n  display: flex;\n  gap: 8px;\n  margin-top: 10px\n}\n\n.back {\n  display: flex;\n  align-items: center;\n  margin-left: -5px;\n  cursor: pointer;\n}\n\n.back:hover {\n  text-decoration: underline;\n}\n\n.info-title {\n  font-size: 1.5rem;\n  font-weight: 700;\n  line-height: 1.25;\n  margin: 8px 0 0 0 ;\n}\n\n.info-description {\n  margin: 8px 0 0 0;\n}\n\n.controls {\n  cursor: pointer;\n}\n\n.fallback {\n  background: #f6f7f8 linear-gradient(to right, #e6e6e6 5%, #cccccc 25%, #e6e6e6 35%) no-repeat;\n  background-size: 800px 104px;\n  display: block;\n  line-height: 1.25;\n  margin: 8px 0 0 0;\n  border-radius: 5px;\n  overflow: hidden;\n\n  animation: 1s linear 1s infinite shimmer;\n  animation-delay: 300ms;\n  animation-duration: 1s;\n  animation-fill-mode: forwards;\n  animation-iteration-count: infinite;\n  animation-name: shimmer;\n  animation-timing-function: linear;\n}\n\n\n.fallback.title {\n  width: 130px;\n  height: 30px;\n\n}\n\n.fallback.description {\n  width: 150px;\n  height: 21px;\n}\n\n@keyframes shimmer {\n  0% {\n    background-position: -468px 0;\n  }\n\n  100% {\n    background-position: 468px 0;\n  }\n}\n\n.search {\n  margin-bottom: 10px;\n}\n.search-input {\n  width: 100%;\n  position: relative;\n}\n\n.search-icon {\n  position: absolute;\n  top: 0;\n  bottom: 0;\n  inset-inline-start: 0;\n  display: flex;\n  align-items: center;\n  padding-inline-start: 1rem;\n  pointer-events: none;\n  color: #99a1b3;\n}\n\n.search-input input {\n  display: flex;\n  padding-inline-start: 2.75rem;\n  padding-top: 10px;\n  padding-bottom: 10px;\n  width: 100%;\n  text-align: start;\n  background-color: rgb(235 236 240);\n  outline: 2px solid transparent;\n  cursor: pointer;\n  border: none;\n  align-items: center;\n  color: rgb(35 39 47);\n  border-radius: 9999px;\n  vertical-align: middle;\n  font-size: 15px;\n}\n\n.search-input input:hover, .search-input input:active {\n  background-color: rgb(235 236 240/ 0.8);\n  color: rgb(35 39 47/ 0.8);\n}\n\n/* Home */\n.video-list {\n  position: relative;\n}\n\n.video-list .videos {\n  display: flex;\n  flex-direction: column;\n  gap: 1rem;\n  overflow-y: auto;\n  height: 100%;\n}\n```\n\n\n```css src/animations.css\n/* 추가 애니메이션은 필요하지 않습니다 */\n\n\n\n\n\n\n\n\n\n/* 이전에 정의된 애니메이션은 아래에 있습니다 */\n\n\n\n\n\n\n/* Slide animation for Suspense */\n::view-transition-old(.slide-down) {\n    animation: 150ms ease-out both fade-out, 150ms ease-out both slide-down;\n}\n\n::view-transition-new(.slide-up) {\n    animation: 210ms ease-in 150ms both fade-in, 400ms ease-in both slide-up;\n}\n\n/* 트랜지션 타입에 의해 추가된 View Transition 클래스용 애니메이션 */\n::view-transition-old(.slide-forward) {\n    /* 앞으로 전환할 때 \"old\" 페이지는 왼쪽으로 슬라이드되어 나가야 합니다. */\n    animation: 150ms cubic-bezier(0.4, 0, 1, 1) both fade-out,\n    400ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;\n}\n\n::view-transition-new(.slide-forward) {\n    /* 앞으로 전환할 때 \"new\" 페이지는 오른쪽에서 슬라이드되어 들어와야 합니다. */\n    animation: 210ms cubic-bezier(0, 0, 0.2, 1) 150ms both fade-in,\n    400ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;\n}\n\n::view-transition-old(.slide-back) {\n    /* 뒤로 전환할 때 \"old\" 페이지는 오른쪽으로 슬라이드되어 나가야 합니다. */\n    animation: 150ms cubic-bezier(0.4, 0, 1, 1) both fade-out,\n    400ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-right;\n}\n\n::view-transition-new(.slide-back) {\n    /* 뒤로 전환할 때 \"new\" 페이지는 왼쪽에서 슬라이드되어 들어와야 합니다. */\n    animation: 210ms cubic-bezier(0, 0, 0.2, 1) 150ms both fade-in,\n    400ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-left;\n}\n\n/* 위 애니메이션을 지원하기 위한 키프레임 */\n@keyframes slide-up {\n    from {\n        transform: translateY(10px);\n    }\n    to {\n        transform: translateY(0);\n    }\n}\n\n@keyframes slide-down {\n    from {\n        transform: translateY(0);\n    }\n    to {\n        transform: translateY(10px);\n    }\n}\n\n@keyframes fade-in {\n    from {\n        opacity: 0;\n    }\n}\n\n@keyframes fade-out {\n    to {\n        opacity: 0;\n    }\n}\n\n@keyframes slide-to-right {\n    to {\n        transform: translateX(50px);\n    }\n}\n\n@keyframes slide-from-right {\n    from {\n        transform: translateX(50px);\n    }\n    to {\n        transform: translateX(0);\n    }\n}\n\n@keyframes slide-to-left {\n    to {\n        transform: translateX(-50px);\n    }\n}\n\n@keyframes slide-from-left {\n    from {\n        transform: translateX(-50px);\n    }\n    to {\n        transform: translateX(0);\n    }\n}\n\n\n/* Default .slow-fade. */\n::view-transition-old(.slow-fade) {\n    animation-duration: 500ms;\n}\n\n::view-transition-new(.slow-fade) {\n    animation-duration: 500ms;\n}\n```\n\n```js src/index.js hidden\nimport React, {StrictMode} from 'react';\nimport {createRoot} from 'react-dom/client';\nimport './styles.css';\nimport './animations.css';\n\nimport App from './App';\nimport {Router} from './router';\n\nconst root = createRoot(document.getElementById('root'));\nroot.render(\n  <StrictMode>\n    <Router>\n      <App />\n    </Router>\n  </StrictMode>\n);\n```\n\n```json package.json hidden\n{\n  \"dependencies\": {\n    \"react\": \"canary\",\n    \"react-dom\": \"canary\",\n    \"react-scripts\": \"latest\"\n  },\n  \"scripts\": {\n    \"start\": \"react-scripts start\",\n    \"build\": \"react-scripts build\",\n    \"test\": \"react-scripts test --env=jsdom\",\n    \"eject\": \"react-scripts eject\"\n  }\n}\n```\n\n</Sandpack>\n\n### 최종 결과물 {/*final-result*/}\n\n몇 개의 `<ViewTransition>` 컴포넌트와 몇 줄의 CSS를 추가하여 위의 모든 애니메이션을 최종 결과물에 추가할 수 있었습니다.\n\n저희는 View Transition이 여러분의 앱 제작 수준을 한 단계 높여줄 것으로 기대하고 있습니다. 오늘부터 React 릴리즈의 Experimental 채널에서 사용해 볼 수 있습니다.\n\n이제 느린 페이드 효과를 제거하고, 최종 결과물을 살펴봅시다.\n\n<Sandpack>\n\n```js src/App.js\nimport {ViewTransition} from 'react'; import Details from './Details'; import Home from './Home'; import {useRouter} from './router';\n\nexport default function App() {\n  const {url} = useRouter();\n\n  // 페이지 간을 크로스페이드로 애니메이션합니다.\n  return (\n    <ViewTransition key={url}>\n      {url === '/' ? <Home /> : <Details />}\n    </ViewTransition>\n  );\n}\n```\n\n```js src/Details.js\nimport { use, Suspense, ViewTransition } from \"react\"; import { fetchVideo, fetchVideoDetails } from \"./data\"; import { Thumbnail, VideoControls } from \"./Videos\"; import { useRouter } from \"./router\"; import Layout from \"./Layout\"; import { ChevronLeft } from \"./Icons\";\n\nfunction VideoDetails({id}) {\n  // Suspense 폴백에서 콘텐츠로 전환되는 애니메이션\n  return (\n    <Suspense\n      fallback={\n        // 폴백을 아래로 애니메이션합니다.\n        <ViewTransition exit=\"slide-down\">\n          <VideoInfoFallback />\n        </ViewTransition>\n      }\n    >\n      {/* 콘텐츠를 위로 애니메이션합니다 */}\n      <ViewTransition enter=\"slide-up\">\n        <VideoInfo id={id} />\n      </ViewTransition>\n    </Suspense>\n  );\n}\n\nfunction VideoInfoFallback() {\n  return (\n    <>\n      <div className=\"fallback title\"></div>\n      <div className=\"fallback description\"></div>\n    </>\n  );\n}\n\nexport default function Details() {\n  const { url, navigateBack } = useRouter();\n  const videoId = url.split(\"/\").pop();\n  const video = use(fetchVideo(videoId));\n\n  return (\n    <Layout\n      heading={\n        <div\n          className=\"fit back\"\n          onClick={() => {\n            navigateBack(\"/\");\n          }}\n        >\n          <ChevronLeft /> Back\n        </div>\n      }\n    >\n      <div className=\"details\">\n        <Thumbnail video={video} large>\n          <VideoControls />\n        </Thumbnail>\n        <VideoDetails id={video.id} />\n      </div>\n    </Layout>\n  );\n}\n\nfunction VideoInfo({ id }) {\n  const details = use(fetchVideoDetails(id));\n  return (\n    <>\n      <p className=\"info-title\">{details.title}</p>\n      <p className=\"info-description\">{details.description}</p>\n    </>\n  );\n}\n```\n\n```js src/Home.js\nimport { useId, useState, use, useDeferredValue, ViewTransition } from \"react\";import { Video } from \"./Videos\";import Layout from \"./Layout\";import { fetchVideos } from \"./data\";import { IconSearch } from \"./Icons\";\n\nfunction SearchList({searchText, videos}) {\n  // useDeferredValue로 활성화합니다(\"언제\")\n  const deferredSearchText = useDeferredValue(searchText);\n  const filteredVideos = filterVideos(videos, deferredSearchText);\n  return (\n    <div className=\"video-list\">\n      <div className=\"videos\">\n        {filteredVideos.map((video) => (\n          // 리스트의 각 항목을 애니메이션합니다(\"무엇\")\n          <ViewTransition key={video.id}>\n            <Video video={video} />\n          </ViewTransition>\n        ))}\n      </div>\n      {filteredVideos.length === 0 && (\n        <div className=\"no-results\">No results</div>\n      )}\n    </div>\n  );\n}\n\nexport default function Home() {\n  const videos = use(fetchVideos());\n  const count = videos.length;\n  const [searchText, setSearchText] = useState('');\n\n  return (\n    <Layout heading={<div className=\"fit\">{count} Videos</div>}>\n      <SearchInput value={searchText} onChange={setSearchText} />\n      <SearchList videos={videos} searchText={searchText} />\n    </Layout>\n  );\n}\n\nfunction SearchInput({ value, onChange }) {\n  const id = useId();\n  return (\n    <form className=\"search\" onSubmit={(e) => e.preventDefault()}>\n      <label htmlFor={id} className=\"sr-only\">\n        Search\n      </label>\n      <div className=\"search-input\">\n        <div className=\"search-icon\">\n          <IconSearch />\n        </div>\n        <input\n          type=\"text\"\n          id={id}\n          placeholder=\"Search\"\n          value={value}\n          onChange={(e) => onChange(e.target.value)}\n        />\n      </div>\n    </form>\n  );\n}\n\nfunction filterVideos(videos, query) {\n  const keywords = query\n    .toLowerCase()\n    .split(\" \")\n    .filter((s) => s !== \"\");\n  if (keywords.length === 0) {\n    return videos;\n  }\n  return videos.filter((video) => {\n    const words = (video.title + \" \" + video.description)\n      .toLowerCase()\n      .split(\" \");\n    return keywords.every((kw) => words.some((w) => w.includes(kw)));\n  });\n}\n```\n\n```js src/Icons.js hidden\nexport function ChevronLeft() {\n  return (\n    <svg\n      className=\"chevron-left\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width=\"20\"\n      height=\"20\"\n      viewBox=\"0 0 20 20\">\n      <g fill=\"none\" fillRule=\"evenodd\" transform=\"translate(-446 -398)\">\n        <path\n          fill=\"currentColor\"\n          fillRule=\"nonzero\"\n          d=\"M95.8838835,240.366117 C95.3957281,239.877961 94.6042719,239.877961 94.1161165,240.366117 C93.6279612,240.854272 93.6279612,241.645728 94.1161165,242.133883 L98.6161165,246.633883 C99.1042719,247.122039 99.8957281,247.122039 100.383883,246.633883 L104.883883,242.133883 C105.372039,241.645728 105.372039,240.854272 104.883883,240.366117 C104.395728,239.877961 103.604272,239.877961 103.116117,240.366117 L99.5,243.982233 L95.8838835,240.366117 Z\"\n          transform=\"translate(356.5 164.5)\"\n        />\n        <polygon points=\"446 418 466 418 466 398 446 398\" />\n      </g>\n    </svg>\n  );\n}\n\nexport function PauseIcon() {\n  return (\n    <svg\n      className=\"control-icon\"\n      style={{padding: '4px'}}\n      width=\"100\"\n      height=\"100\"\n      viewBox=\"0 0 512 512\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\">\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M256 0C114.617 0 0 114.615 0 256s114.617 256 256 256 256-114.615 256-256S397.383 0 256 0zm-32 320c0 8.836-7.164 16-16 16h-32c-8.836 0-16-7.164-16-16V192c0-8.836 7.164-16 16-16h32c8.836 0 16 7.164 16 16v128zm128 0c0 8.836-7.164 16-16 16h-32c-8.836 0-16-7.164-16-16V192c0-8.836 7.164-16 16-16h32c8.836 0 16 7.164 16 16v128z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  );\n}\n\nexport function PlayIcon() {\n  return (\n    <svg\n      className=\"control-icon\"\n      width=\"100\"\n      height=\"100\"\n      viewBox=\"0 0 72 72\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\">\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M36 69C54.2254 69 69 54.2254 69 36C69 17.7746 54.2254 3 36 3C17.7746 3 3 17.7746 3 36C3 54.2254 17.7746 69 36 69ZM52.1716 38.6337L28.4366 51.5801C26.4374 52.6705 24 51.2235 24 48.9464V23.0536C24 20.7764 26.4374 19.3295 28.4366 20.4199L52.1716 33.3663C54.2562 34.5034 54.2562 37.4966 52.1716 38.6337Z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  );\n}\nexport function Heart({liked, animate}) {\n  return (\n    <>\n      <svg\n        className=\"absolute overflow-visible\"\n        viewBox=\"0 0 24 24\"\n        fill=\"none\"\n        xmlns=\"http://www.w3.org/2000/svg\">\n        <circle\n          className={`circle ${liked ? 'liked' : ''} ${animate ? 'animate' : ''}`}\n          cx=\"12\"\n          cy=\"12\"\n          r=\"11.5\"\n          fill=\"transparent\"\n          strokeWidth=\"0\"\n          stroke=\"currentColor\"\n        />\n      </svg>\n\n      <svg\n        className={`heart ${liked ? 'liked' : ''} ${animate ? 'animate' : ''}`}\n        viewBox=\"0 0 24 24\"\n        fill=\"none\"\n        xmlns=\"http://www.w3.org/2000/svg\">\n        {liked ? (\n          <path\n            d=\"M12 23a.496.496 0 0 1-.26-.074C7.023 19.973 0 13.743 0 8.68c0-4.12 2.322-6.677 6.058-6.677 2.572 0 5.108 2.387 5.134 2.41l.808.771.808-.771C12.834 4.387 15.367 2 17.935 2 21.678 2 24 4.558 24 8.677c0 5.06-7.022 11.293-11.74 14.246a.496.496 0 0 1-.26.074V23z\"\n            fill=\"currentColor\"\n          />\n        ) : (\n          <path\n            fillRule=\"evenodd\"\n            clipRule=\"evenodd\"\n            d=\"m12 5.184-.808-.771-.004-.004C11.065 4.299 8.522 2.003 6 2.003c-3.736 0-6 2.558-6 6.677 0 4.47 5.471 9.848 10 13.079.602.43 1.187.82 1.74 1.167A.497.497 0 0 0 12 23v-.003c.09 0 .182-.026.26-.074C16.977 19.97 24 13.737 24 8.677 24 4.557 21.743 2 18 2c-2.569 0-5.166 2.387-5.192 2.413L12 5.184zm-.002 15.525c2.071-1.388 4.477-3.342 6.427-5.47C20.72 12.733 22 10.401 22 8.677c0-1.708-.466-2.855-1.087-3.55C20.316 4.459 19.392 4 18 4c-.726 0-1.63.364-2.5.9-.67.412-1.148.82-1.266.92-.03.025-.037.031-.019.014l-.013.013L12 7.949 9.832 5.88a10.08 10.08 0 0 0-1.33-.977C7.633 4.367 6.728 4.003 6 4.003c-1.388 0-2.312.459-2.91 1.128C2.466 5.826 2 6.974 2 8.68c0 1.726 1.28 4.058 3.575 6.563 1.948 2.127 4.352 4.078 6.423 5.466z\"\n            fill=\"currentColor\"\n          />\n        )}\n      </svg>\n    </>\n  );\n}\n\nexport function IconSearch(props) {\n  return (\n    <svg width=\"1em\" height=\"1em\" viewBox=\"0 0 20 20\">\n      <path\n        d=\"M14.386 14.386l4.0877 4.0877-4.0877-4.0877c-2.9418 2.9419-7.7115 2.9419-10.6533 0-2.9419-2.9418-2.9419-7.7115 0-10.6533 2.9418-2.9419 7.7115-2.9419 10.6533 0 2.9419 2.9418 2.9419 7.7115 0 10.6533z\"\n        stroke=\"currentColor\"\n        fill=\"none\"\n        strokeWidth=\"2\"\n        fillRule=\"evenodd\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"></path>\n    </svg>\n  );\n}\n```\n\n```js src/Layout.js\nimport {ViewTransition} from 'react'; import { useIsNavPending } from \"./router\";\n\nexport default function Page({ heading, children }) {\n  const isPending = useIsNavPending();\n  return (\n    <div className=\"page\">\n      <div className=\"top\">\n        <div className=\"top-nav\">\n          {/* 트랜지션 타입에 따라 커스텀 클래스를 적용합니다. */}\n          <ViewTransition\n            name=\"nav\"\n            share={{\n              'nav-forward': 'slide-forward',\n              'nav-back': 'slide-back',\n            }}>\n            {heading}\n          </ViewTransition>\n          {isPending && <span className=\"loader\"></span>}\n        </div>\n      </div>\n      {/* 콘텐츠에 대해 ViewTransition을 사용하지 않습니다. */}\n      {/* 콘텐츠는 자체 ViewTransition을 정의할 수 있습니다. */}\n      <ViewTransition default=\"none\">\n        <div className=\"bottom\">\n          <div className=\"content\">{children}</div>\n        </div>\n      </ViewTransition>\n    </div>\n  );\n}\n```\n\n```js src/LikeButton.js hidden\nimport {useState} from 'react';\nimport {Heart} from './Icons';\n\n// A hack since we don't actually have a backend.\n// Unlike local state, this survives videos being filtered.\nconst likedVideos = new Set();\n\nexport default function LikeButton({video}) {\n  const [isLiked, setIsLiked] = useState(() => likedVideos.has(video.id));\n  const [animate, setAnimate] = useState(false);\n  return (\n    <button\n      className={`like-button ${isLiked && 'liked'}`}\n      aria-label={isLiked ? 'Unsave' : 'Save'}\n      onClick={() => {\n        const nextIsLiked = !isLiked;\n        if (nextIsLiked) {\n          likedVideos.add(video.id);\n        } else {\n          likedVideos.delete(video.id);\n        }\n        setAnimate(true);\n        setIsLiked(nextIsLiked);\n      }}>\n      <Heart liked={isLiked} animate={animate} />\n    </button>\n  );\n}\n```\n\n```js src/Videos.js\nimport { useState, ViewTransition } from \"react\"; import LikeButton from \"./LikeButton\"; import { useRouter } from \"./router\"; import { PauseIcon, PlayIcon } from \"./Icons\"; import { startTransition } from \"react\";\n\nexport function Thumbnail({ video, children }) {\n  // 공유 요소 트랜지션으로 애니메이션되도록 name을 추가합니다.\n  return (\n    <ViewTransition name={`video-${video.id}`}>\n      <div\n        aria-hidden=\"true\"\n        tabIndex={-1}\n        className={`thumbnail ${video.image}`}\n      >\n        {children}\n      </div>\n    </ViewTransition>\n  );\n}\n\n\n\nexport function VideoControls() {\n  const [isPlaying, setIsPlaying] = useState(false);\n\n  return (\n    <span\n      className=\"controls\"\n      onClick={() =>\n        startTransition(() => {\n          setIsPlaying((p) => !p);\n        })\n      }\n    >\n      {isPlaying ? <PauseIcon /> : <PlayIcon />}\n    </span>\n  );\n}\n\nexport function Video({ video }) {\n  const { navigate } = useRouter();\n\n  return (\n    <div className=\"video\">\n      <div\n        className=\"link\"\n        onClick={(e) => {\n          e.preventDefault();\n          navigate(`/video/${video.id}`);\n        }}\n      >\n        <Thumbnail video={video}></Thumbnail>\n\n        <div className=\"info\">\n          <div className=\"video-title\">{video.title}</div>\n          <div className=\"video-description\">{video.description}</div>\n        </div>\n      </div>\n      <LikeButton video={video} />\n    </div>\n  );\n}\n```\n\n\n```js src/data.js hidden\nconst videos = [\n  {\n    id: '1',\n    title: 'First video',\n    description: 'Video description',\n    image: 'blue',\n  },\n  {\n    id: '2',\n    title: 'Second video',\n    description: 'Video description',\n    image: 'red',\n  },\n  {\n    id: '3',\n    title: 'Third video',\n    description: 'Video description',\n    image: 'green',\n  },\n  {\n    id: '4',\n    title: 'Fourth video',\n    description: 'Video description',\n    image: 'purple',\n  },\n  {\n    id: '5',\n    title: 'Fifth video',\n    description: 'Video description',\n    image: 'yellow',\n  },\n  {\n    id: '6',\n    title: 'Sixth video',\n    description: 'Video description',\n    image: 'gray',\n  },\n];\n\nlet videosCache = new Map();\nlet videoCache = new Map();\nlet videoDetailsCache = new Map();\nconst VIDEO_DELAY = 1;\nconst VIDEO_DETAILS_DELAY = 1000;\nexport function fetchVideos() {\n  if (videosCache.has(0)) {\n    return videosCache.get(0);\n  }\n  const promise = new Promise((resolve) => {\n    setTimeout(() => {\n      resolve(videos);\n    }, VIDEO_DELAY);\n  });\n  videosCache.set(0, promise);\n  return promise;\n}\n\nexport function fetchVideo(id) {\n  if (videoCache.has(id)) {\n    return videoCache.get(id);\n  }\n  const promise = new Promise((resolve) => {\n    setTimeout(() => {\n      resolve(videos.find((video) => video.id === id));\n    }, VIDEO_DELAY);\n  });\n  videoCache.set(id, promise);\n  return promise;\n}\n\nexport function fetchVideoDetails(id) {\n  if (videoDetailsCache.has(id)) {\n    return videoDetailsCache.get(id);\n  }\n  const promise = new Promise((resolve) => {\n    setTimeout(() => {\n      resolve(videos.find((video) => video.id === id));\n    }, VIDEO_DETAILS_DELAY);\n  });\n  videoDetailsCache.set(id, promise);\n  return promise;\n}\n```\n\n```js src/router.js\nimport {useState, createContext, use, useTransition, useLayoutEffect, useEffect, addTransitionType} from \"react\";\n\nexport function Router({ children }) {\n  const [isPending, startTransition] = useTransition();\n  function navigate(url) {\n    startTransition(() => {\n      // 전환 원인이 \"nav forward\"인 경우의 트랜지션 타입\n      addTransitionType('nav-forward');\n      go(url);\n    });\n  }\n  function navigateBack(url) {\n    startTransition(() => {\n      // 전환 원인이 \"nav backward\"인 경우의 트랜지션 타입\n      addTransitionType('nav-back');\n      go(url);\n    });\n  }\n\n  const [routerState, setRouterState] = useState({pendingNav: () => {}, url: document.location.pathname});\n\n  function go(url) {\n    setRouterState({\n      url,\n      pendingNav() {\n        window.history.pushState({}, \"\", url);\n      },\n    });\n  }\n\n  useEffect(() => {\n    function handlePopState() {\n      // 복원은 동기적으로 이루어져야 하므로 애니메이션되면 안 됩니다.\n      // 트랜지션이더라도 마찬가지입니다.\n      startTransition(() => {\n        setRouterState({\n          url: document.location.pathname + document.location.search,\n          pendingNav() {\n            // 아무 작업도 하지 않습니다. URL은 이미 업데이트되었습니다.\n          },\n        });\n      });\n    }\n    window.addEventListener(\"popstate\", handlePopState);\n    return () => {\n      window.removeEventListener(\"popstate\", handlePopState);\n    };\n  }, []);\n  const pendingNav = routerState.pendingNav;\n  useLayoutEffect(() => {\n    pendingNav();\n  }, [pendingNav]);\n\n  return (\n    <RouterContext\n      value={{\n        url: routerState.url,\n        navigate,\n        navigateBack,\n        isPending,\n        params: {},\n      }}\n    >\n      {children}\n    </RouterContext>\n  );\n}\n\nconst RouterContext = createContext({ url: \"/\", params: {} });\n\nexport function useRouter() {\n  return use(RouterContext);\n}\n\nexport function useIsNavPending() {\n  return use(RouterContext).isPending;\n}\n\n```\n\n```css src/styles.css hidden\n@font-face {\n  font-family: Optimistic Text;\n  src: url(https://react.dev/fonts/Optimistic_Text_W_Rg.woff2) format(\"woff2\");\n  font-weight: 400;\n  font-style: normal;\n  font-display: swap;\n}\n\n@font-face {\n  font-family: Optimistic Text;\n  src: url(https://react.dev/fonts/Optimistic_Text_W_Md.woff2) format(\"woff2\");\n  font-weight: 500;\n  font-style: normal;\n  font-display: swap;\n}\n\n@font-face {\n  font-family: Optimistic Text;\n  src: url(https://react.dev/fonts/Optimistic_Text_W_Bd.woff2) format(\"woff2\");\n  font-weight: 600;\n  font-style: normal;\n  font-display: swap;\n}\n\n@font-face {\n  font-family: Optimistic Text;\n  src: url(https://react.dev/fonts/Optimistic_Text_W_Bd.woff2) format(\"woff2\");\n  font-weight: 700;\n  font-style: normal;\n  font-display: swap;\n}\n\n* {\n  box-sizing: border-box;\n}\n\nhtml {\n  background-image: url(https://react.dev/images/meta-gradient-dark.png);\n  background-size: 100%;\n  background-position: -100%;\n  background-color: rgb(64 71 86);\n  background-repeat: no-repeat;\n  height: 100%;\n  width: 100%;\n}\n\nbody {\n  font-family: Optimistic Text, -apple-system, ui-sans-serif, system-ui, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;\n  padding: 10px 0 10px 0;\n  margin: 0;\n  display: flex;\n  justify-content: center;\n}\n\n#root {\n  flex: 1 1;\n  height: auto;\n  background-color: #fff;\n  border-radius: 10px;\n  max-width: 450px;\n  min-height: 600px;\n  padding-bottom: 10px;\n}\n\nh1 {\n  margin-top: 0;\n  font-size: 22px;\n}\n\nh2 {\n  margin-top: 0;\n  font-size: 20px;\n}\n\nh3 {\n  margin-top: 0;\n  font-size: 18px;\n}\n\nh4 {\n  margin-top: 0;\n  font-size: 16px;\n}\n\nh5 {\n  margin-top: 0;\n  font-size: 14px;\n}\n\nh6 {\n  margin-top: 0;\n  font-size: 12px;\n}\n\ncode {\n  font-size: 1.2em;\n}\n\nul {\n  padding-inline-start: 20px;\n}\n\n.sr-only {\n  position: absolute;\n  width: 1px;\n  height: 1px;\n  padding: 0;\n  margin: -1px;\n  overflow: hidden;\n  clip: rect(0, 0, 0, 0);\n  white-space: nowrap;\n  border-width: 0;\n}\n\n.absolute {\n  position: absolute;\n}\n\n.overflow-visible {\n  overflow: visible;\n}\n\n.visible {\n  overflow: visible;\n}\n\n.fit {\n  width: fit-content;\n}\n\n\n/* Layout */\n.page {\n  display: flex;\n  flex-direction: column;\n  height: 100%;\n}\n\n.top-hero {\n  height: 200px;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  background-image: conic-gradient(\n      from 90deg at -10% 100%,\n      #2b303b 0deg,\n      #2b303b 90deg,\n      #16181d 1turn\n  );\n}\n\n.bottom {\n  flex: 1;\n  overflow: auto;\n}\n\n.top-nav {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  margin-bottom: 0;\n  padding: 0 12px;\n  top: 0;\n  width: 100%;\n  height: 44px;\n  color: #23272f;\n  font-weight: 700;\n  font-size: 20px;\n  z-index: 100;\n  cursor: default;\n}\n\n.content {\n  padding: 0 12px;\n  margin-top: 4px;\n}\n\n\n.loader {\n  color: #23272f;\n  font-size: 3px;\n  width: 1em;\n  margin-right: 18px;\n  height: 1em;\n  border-radius: 50%;\n  position: relative;\n  text-indent: -9999em;\n  animation: loading-spinner 1.3s infinite linear;\n  animation-delay: 200ms;\n  transform: translateZ(0);\n}\n\n@keyframes loading-spinner {\n  0%,\n  100% {\n    box-shadow: 0 -3em 0 0.2em,\n    2em -2em 0 0em, 3em 0 0 -1em,\n    2em 2em 0 -1em, 0 3em 0 -1em,\n    -2em 2em 0 -1em, -3em 0 0 -1em,\n    -2em -2em 0 0;\n  }\n  12.5% {\n    box-shadow: 0 -3em 0 0, 2em -2em 0 0.2em,\n    3em 0 0 0, 2em 2em 0 -1em, 0 3em 0 -1em,\n    -2em 2em 0 -1em, -3em 0 0 -1em,\n    -2em -2em 0 -1em;\n  }\n  25% {\n    box-shadow: 0 -3em 0 -0.5em,\n    2em -2em 0 0, 3em 0 0 0.2em,\n    2em 2em 0 0, 0 3em 0 -1em,\n    -2em 2em 0 -1em, -3em 0 0 -1em,\n    -2em -2em 0 -1em;\n  }\n  37.5% {\n    box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em,\n    3em 0em 0 0, 2em 2em 0 0.2em, 0 3em 0 0em,\n    -2em 2em 0 -1em, -3em 0em 0 -1em, -2em -2em 0 -1em;\n  }\n  50% {\n    box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em,\n    3em 0 0 -1em, 2em 2em 0 0em, 0 3em 0 0.2em,\n    -2em 2em 0 0, -3em 0em 0 -1em, -2em -2em 0 -1em;\n  }\n  62.5% {\n    box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em,\n    3em 0 0 -1em, 2em 2em 0 -1em, 0 3em 0 0,\n    -2em 2em 0 0.2em, -3em 0 0 0, -2em -2em 0 -1em;\n  }\n  75% {\n    box-shadow: 0em -3em 0 -1em, 2em -2em 0 -1em,\n    3em 0em 0 -1em, 2em 2em 0 -1em, 0 3em 0 -1em,\n    -2em 2em 0 0, -3em 0em 0 0.2em, -2em -2em 0 0;\n  }\n  87.5% {\n    box-shadow: 0em -3em 0 0, 2em -2em 0 -1em,\n    3em 0 0 -1em, 2em 2em 0 -1em, 0 3em 0 -1em,\n    -2em 2em 0 0, -3em 0em 0 0, -2em -2em 0 0.2em;\n  }\n}\n\n/* LikeButton */\n.like-button {\n  outline-offset: 2px;\n  position: relative;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  width: 2.5rem;\n  height: 2.5rem;\n  cursor: pointer;\n  border-radius: 9999px;\n  border: none;\n  outline: none 2px;\n  color: #5e687e;\n  background: none;\n}\n\n.like-button:focus {\n  color: #a6423a;\n  background-color: rgba(166, 66, 58, .05);\n}\n\n.like-button:active {\n  color: #a6423a;\n  background-color: rgba(166, 66, 58, .05);\n  transform: scaleX(0.95) scaleY(0.95);\n}\n\n.like-button:hover {\n  background-color: #f6f7f9;\n}\n\n.like-button.liked {\n  color: #a6423a;\n}\n\n/* Icons */\n@keyframes circle {\n  0% {\n    transform: scale(0);\n    stroke-width: 16px;\n  }\n\n  50% {\n    transform: scale(.5);\n    stroke-width: 16px;\n  }\n\n  to {\n    transform: scale(1);\n    stroke-width: 0;\n  }\n}\n\n.circle {\n  color: rgba(166, 66, 58, .5);\n  transform-origin: center;\n  transition-property: all;\n  transition-duration: .15s;\n  transition-timing-function: cubic-bezier(.4,0,.2,1);\n}\n\n.circle.liked.animate {\n  animation: circle .3s forwards;\n}\n\n.heart {\n  width: 1.5rem;\n  height: 1.5rem;\n}\n\n.heart.liked {\n  transform-origin: center;\n  transition-property: all;\n  transition-duration: .15s;\n  transition-timing-function: cubic-bezier(.4, 0, .2, 1);\n}\n\n.heart.liked.animate {\n  animation: scale .35s ease-in-out forwards;\n}\n\n.control-icon {\n  color: hsla(0, 0%, 100%, .5);\n  filter:  drop-shadow(0 20px 13px rgba(0, 0, 0, .03)) drop-shadow(0 8px 5px rgba(0, 0, 0, .08));\n}\n\n.chevron-left {\n  margin-top: 2px;\n  rotate: 90deg;\n}\n\n\n/* Video */\n.thumbnail {\n  position: relative;\n  aspect-ratio: 16 / 9;\n  display: flex;\n  overflow: hidden;\n  flex-direction: column;\n  justify-content: center;\n  align-items: center;\n  border-radius: 0.5rem;\n  outline-offset: 2px;\n  width: 8rem;\n  vertical-align: middle;\n  background-color: #ffffff;\n  background-size: cover;\n  user-select: none;\n}\n\n.thumbnail.blue {\n  background-image: conic-gradient(at top right, #c76a15, #087ea4, #2b3491);\n}\n\n.thumbnail.red {\n  background-image: conic-gradient(at top right, #c76a15, #a6423a, #2b3491);\n}\n\n.thumbnail.green {\n  background-image: conic-gradient(at top right, #c76a15, #388f7f, #2b3491);\n}\n\n.thumbnail.purple {\n  background-image: conic-gradient(at top right, #c76a15, #575fb7, #2b3491);\n}\n\n.thumbnail.yellow {\n  background-image: conic-gradient(at top right, #c76a15, #FABD62, #2b3491);\n}\n\n.thumbnail.gray {\n  background-image: conic-gradient(at top right, #c76a15, #4E5769, #2b3491);\n}\n\n.video {\n  display: flex;\n  flex-direction: row;\n  gap: 0.75rem;\n  align-items: center;\n}\n\n.video .link {\n  display: flex;\n  flex-direction: row;\n  flex: 1 1 0;\n  gap: 0.125rem;\n  outline-offset: 4px;\n  cursor: pointer;\n}\n\n.video .info {\n  display: flex;\n  flex-direction: column;\n  justify-content: center;\n  margin-left: 8px;\n  gap: 0.125rem;\n}\n\n.video .info:hover {\n  text-decoration: underline;\n}\n\n.video-title {\n  font-size: 15px;\n  line-height: 1.25;\n  font-weight: 700;\n  color: #23272f;\n}\n\n.video-description {\n  color: #5e687e;\n  font-size: 13px;\n}\n\n/* Details */\n.details .thumbnail {\n  position: relative;\n  aspect-ratio: 16 / 9;\n  display: flex;\n  overflow: hidden;\n  flex-direction: column;\n  justify-content: center;\n  align-items: center;\n  border-radius: 0.5rem;\n  outline-offset: 2px;\n  width: 100%;\n  vertical-align: middle;\n  background-color: #ffffff;\n  background-size: cover;\n  user-select: none;\n}\n\n.video-details-title {\n  margin-top: 8px;\n}\n\n.video-details-speaker {\n  display: flex;\n  gap: 8px;\n  margin-top: 10px\n}\n\n.back {\n  display: flex;\n  align-items: center;\n  margin-left: -5px;\n  cursor: pointer;\n}\n\n.back:hover {\n  text-decoration: underline;\n}\n\n.info-title {\n  font-size: 1.5rem;\n  font-weight: 700;\n  line-height: 1.25;\n  margin: 8px 0 0 0 ;\n}\n\n.info-description {\n  margin: 8px 0 0 0;\n}\n\n.controls {\n  cursor: pointer;\n}\n\n.fallback {\n  background: #f6f7f8 linear-gradient(to right, #e6e6e6 5%, #cccccc 25%, #e6e6e6 35%) no-repeat;\n  background-size: 800px 104px;\n  display: block;\n  line-height: 1.25;\n  margin: 8px 0 0 0;\n  border-radius: 5px;\n  overflow: hidden;\n\n  animation: 1s linear 1s infinite shimmer;\n  animation-delay: 300ms;\n  animation-duration: 1s;\n  animation-fill-mode: forwards;\n  animation-iteration-count: infinite;\n  animation-name: shimmer;\n  animation-timing-function: linear;\n}\n\n\n.fallback.title {\n  width: 130px;\n  height: 30px;\n\n}\n\n.fallback.description {\n  width: 150px;\n  height: 21px;\n}\n\n@keyframes shimmer {\n  0% {\n    background-position: -468px 0;\n  }\n\n  100% {\n    background-position: 468px 0;\n  }\n}\n\n.search {\n  margin-bottom: 10px;\n}\n.search-input {\n  width: 100%;\n  position: relative;\n}\n\n.search-icon {\n  position: absolute;\n  top: 0;\n  bottom: 0;\n  inset-inline-start: 0;\n  display: flex;\n  align-items: center;\n  padding-inline-start: 1rem;\n  pointer-events: none;\n  color: #99a1b3;\n}\n\n.search-input input {\n  display: flex;\n  padding-inline-start: 2.75rem;\n  padding-top: 10px;\n  padding-bottom: 10px;\n  width: 100%;\n  text-align: start;\n  background-color: rgb(235 236 240);\n  outline: 2px solid transparent;\n  cursor: pointer;\n  border: none;\n  align-items: center;\n  color: rgb(35 39 47);\n  border-radius: 9999px;\n  vertical-align: middle;\n  font-size: 15px;\n}\n\n.search-input input:hover, .search-input input:active {\n  background-color: rgb(235 236 240/ 0.8);\n  color: rgb(35 39 47/ 0.8);\n}\n\n/* Home */\n.video-list {\n  position: relative;\n}\n\n.video-list .videos {\n  display: flex;\n  flex-direction: column;\n  gap: 1rem;\n  overflow-y: auto;\n  height: 100%;\n}\n```\n\n\n```css src/animations.css\n/* Suspense 폴백을 아래로 슬라이드하는 애니메이션 */\n::view-transition-old(.slide-down) {\n    animation: 150ms ease-out both fade-out, 150ms ease-out both slide-down;\n}\n\n::view-transition-new(.slide-up) {\n    animation: 210ms ease-in 150ms both fade-in, 400ms ease-in both slide-up;\n}\n\n/* 트랜지션 타입에 의해 추가된 View Transition 클래스용 애니메이션 */\n::view-transition-old(.slide-forward) {\n    /* 앞으로 전환할 때 \"old\" 페이지는 왼쪽으로 슬라이드되어 나가야 합니다. */\n    animation: 150ms cubic-bezier(0.4, 0, 1, 1) both fade-out,\n    400ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;\n}\n\n::view-transition-new(.slide-forward) {\n    /* 앞으로 전환할 때 \"new\" 페이지는 오른쪽에서 슬라이드되어 들어와야 합니다. */\n    animation: 210ms cubic-bezier(0, 0, 0.2, 1) 150ms both fade-in,\n    400ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;\n}\n\n::view-transition-old(.slide-back) {\n    /* 뒤로 전환할 때 \"old\" 페이지는 오른쪽으로 슬라이드되어 나가야 합니다. */\n    animation: 150ms cubic-bezier(0.4, 0, 1, 1) both fade-out,\n    400ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-right;\n}\n\n::view-transition-new(.slide-back) {\n    /* 뒤로 전환할 때 \"new\" 페이지는 왼쪽에서 슬라이드되어 들어와야 합니다. */\n    animation: 210ms cubic-bezier(0, 0, 0.2, 1) 150ms both fade-in,\n    400ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-left;\n}\n\n/* 위 애니메이션을 지원하기 위한 키프레임 */\n@keyframes slide-up {\n    from {\n        transform: translateY(10px);\n    }\n    to {\n        transform: translateY(0);\n    }\n}\n\n@keyframes slide-down {\n    from {\n        transform: translateY(0);\n    }\n    to {\n        transform: translateY(10px);\n    }\n}\n\n@keyframes fade-in {\n    from {\n        opacity: 0;\n    }\n}\n\n@keyframes fade-out {\n    to {\n        opacity: 0;\n    }\n}\n\n@keyframes slide-to-right {\n    to {\n        transform: translateX(50px);\n    }\n}\n\n@keyframes slide-from-right {\n    from {\n        transform: translateX(50px);\n    }\n    to {\n        transform: translateX(0);\n    }\n}\n\n@keyframes slide-to-left {\n    to {\n        transform: translateX(-50px);\n    }\n}\n\n@keyframes slide-from-left {\n    from {\n        transform: translateX(-50px);\n    }\n    to {\n        transform: translateX(0);\n    }\n}\n```\n\n```js src/index.js hidden\nimport React, {StrictMode} from 'react';\nimport {createRoot} from 'react-dom/client';\nimport './styles.css';\nimport './animations.css';\n\nimport App from './App';\nimport {Router} from './router';\n\nconst root = createRoot(document.getElementById('root'));\nroot.render(\n  <StrictMode>\n    <Router>\n      <App />\n    </Router>\n  </StrictMode>\n);\n```\n\n```json package.json hidden\n{\n  \"dependencies\": {\n    \"react\": \"canary\",\n    \"react-dom\": \"canary\",\n    \"react-scripts\": \"latest\"\n  },\n  \"scripts\": {\n    \"start\": \"react-scripts start\",\n    \"build\": \"react-scripts build\",\n    \"test\": \"react-scripts test --env=jsdom\",\n    \"eject\": \"react-scripts eject\"\n  }\n}\n```\n\n</Sandpack>\n\n작동 방식에 대해 자세히 알고 싶다면 문서에서 [`<ViewTransition>`의 작동 방식](/reference/react/ViewTransition#how-does-viewtransition-work)을 확인하세요.\n\n_View Transition을 구축한 배경에 대한 자세한 내용은 다음을 참조하세요. [#31975](https://github.com/facebook/react/pull/31975), [#32105](https://github.com/facebook/react/pull/32105), [#32041](https://github.com/facebook/react/pull/32041), [#32734](https://github.com/facebook/react/pull/32734), [#32797](https://github.com/facebook/react/pull/32797) [#31999](https://github.com/facebook/react/pull/31999), [#32031](https://github.com/facebook/react/pull/32031), [#32050](https://github.com/facebook/react/pull/32050), [#32820](https://github.com/facebook/react/pull/32820), [#32029](https://github.com/facebook/react/pull/32029), [#32028](https://github.com/facebook/react/pull/32028), and [#32038](https://github.com/facebook/react/pull/32038) by [@sebmarkbage](https://twitter.com/sebmarkbage) (Seb에게 감사합니다!)_\n\n---\n\n## Activity {/*activity*/}\n\n<Note>\n\n**`<Activity />`는 이제 React의 Canary 채널에서 사용할 수 있습니다.**\n\n[React의 릴리스 채널에 대해 더 알아보기](/community/versioning-policy#all-release-channels).\n\n</Note>\n\n[이전](/blog/2022/06/15/react-labs-what-we-have-been-working-on-june-2022#offscreen) [업데이트](/blog/2024/02/15/react-labs-what-we-have-been-working-on-february-2024#offscreen-renamed-to-activity)에서, 컴포넌트를 시각적으로 숨기고 우선순위를 낮출 수 있도록 하는 API를 연구하고 있다고 공유했습니다. 이 API는 컴포넌트를 마운트 해제하거나 CSS로 숨기는 방식에 비해 더 낮은 성능 비용으로 UI 상태를 유지하는 것을 목표로 합니다.\n\n이제 API와 그 작동 방식을 공유할 준비가 되었고, 실험적인 React 버전에서 테스트를 시작할 수 있습니다.\n\n`<Activity>`는 UI의 일부를 숨기고 표시하는 새로운 컴포넌트입니다.\n\n```js [[1, 1, \"'visible'\"], [2, 1, \"'hidden'\"]]\n<Activity mode={isVisible ? 'visible' : 'hidden'}>\n  <Page />\n</Activity>\n```\n\nActivity가 <CodeStep step={1}>visible</CodeStep>하면 정상적으로 렌더링됩니다. Activity가 <CodeStep step={2}>hidden</CodeStep>이면 마운트 해제되지만, 상태를 저장하고 화면에 표시되는 항목보다 낮은 우선 순위로 계속 렌더링됩니다.\n\n`Activity`를 사용하여 사용자가 사용하지 않는 UI 부분의 상태를 저장하거나 사용자가 다음에 사용할 가능성이 있는 부분을 미리 렌더링할 수 있습니다.\n\n위의 View Transition 예시를 개선한 몇 가지 예시를 살펴보겠습니다.\n\n<Note>\n\n**Activity가 `hidden`이면 Effect는 마운트되지 않습니다.**\n\n`<Activity>`가 `hidden`이면 Effect가 마운트 해제됩니다. 개념적으로는 컴포넌트가 마운트 해제되지만 React는 나중에 사용할 수 있도록 State를 저장합니다.\n\n실제로는 [Effect가 필요하지 않은 경우](/learn/you-might-not-need-an-effect) 가이드를 따랐다면 예상대로 작동합니다. 문제가 있는 Effect를 찾으려면, 예상치 못한 사이드 이펙트를 발견하기 위해 Activity 마운트 해제와 마운트를 수행하는 [`<StrictMode>`](/reference/react/StrictMode)를 추가하는 것이 좋습니다.\n\n</Note>\n\n### Activity로 상태 복원하기 {/*restoring-state-with-activity*/}\n\n사용자가 페이지에서 다른 페이지로 이동하면 이전 페이지의 렌더링을 중단하는 것이 일반적입니다.\n\n```js {6,7}\nfunction App() {\n  const { url } = useRouter();\n\n  return (\n    <>\n      {url === '/' && <Home />}\n      {url !== '/' && <Details />}\n    </>\n  );\n}\n```\n\n그러나 이는 사용자가 이전 페이지로 돌아갈 경우 이전 State는 모두 손실되는 것을 의미합니다. 예를 들어 `<Home />` 페이지에 `<input>` 필드가 있는 경우 사용자가 페이지를 나가면 `<input>`이 마운트 해제되고 입력했던 모든 텍스트가 손실됩니다.\n\nActivity를 사용하면 사용자가 페이지를 변경할 때 상태를 유지하여, 다시 돌아왔을 때 중단한 부분부터 다시 시작할 수 있습니다. 이 작업은 트리의 일부를 `<Activity>`로 감싸고 `mode`를 전환하면 됩니다.\n\n```js {6-8}\nfunction App() {\n  const { url } = useRouter();\n\n  return (\n    <>\n      <Activity mode={url === '/' ? 'visible' : 'hidden'}>\n        <Home />\n      </Activity>\n      {url !== '/' && <Details />}\n    </>\n  );\n}\n```\n\n이 변경으로 위의 View Transition 예시를 개선할 수 있습니다. 이전에는 동영상을 검색하고 선택한 후 돌아오면 검색 필터가 사라졌습니다. Activity를 사용하면 검색 필터가 복원되어 중단한 부분부터 다시 시작할 수 있습니다.\n\n동영상을 검색하고 선택한 후 \"back\"을 클릭해 보세요.\n\n<Sandpack>\n\n```js src/App.js\nimport { Activity, ViewTransition } from \"react\"; import Details from \"./Details\"; import Home from \"./Home\"; import { useRouter } from \"./router\";\n\nexport default function App() {\n  const { url } = useRouter();\n\n  return (\n    // View Transitions는 Activity를 인식합니다\n    <ViewTransition>\n      {/* 상태를 잃지 않도록 Home을 Activity로 렌더링합니다 */}\n      <Activity mode={url === '/' ? 'visible' : 'hidden'}>\n        <Home />\n      </Activity>\n      {url !== '/' && <Details />}\n    </ViewTransition>\n  );\n}\n```\n\n```js src/Details.js hidden\nimport { use, Suspense, ViewTransition } from \"react\";\nimport { fetchVideo, fetchVideoDetails } from \"./data\";\nimport { Thumbnail, VideoControls } from \"./Videos\";\nimport { useRouter } from \"./router\";\nimport Layout from \"./Layout\";\nimport { ChevronLeft } from \"./Icons\";\n\nfunction VideoDetails({id}) {\n  // Animate from Suspense fallback to content\n  return (\n    <Suspense\n      fallback={\n        // 폴백을 아래로 애니메이션합니다.\n        <ViewTransition exit=\"slide-down\">\n          <VideoInfoFallback />\n        </ViewTransition>\n      }\n    >\n      {/* 콘텐츠를 위로 애니메이션합니다 */}\n      <ViewTransition enter=\"slide-up\">\n        <VideoInfo id={id} />\n      </ViewTransition>\n    </Suspense>\n  );\n}\n\nfunction VideoInfoFallback() {\n  return (\n    <>\n      <div className=\"fallback title\"></div>\n      <div className=\"fallback description\"></div>\n    </>\n  );\n}\n\nexport default function Details() {\n  const { url, navigateBack } = useRouter();\n  const videoId = url.split(\"/\").pop();\n  const video = use(fetchVideo(videoId));\n\n  return (\n    <Layout\n      heading={\n        <div\n          className=\"fit back\"\n          onClick={() => {\n            navigateBack(\"/\");\n          }}\n        >\n          <ChevronLeft /> Back\n        </div>\n      }\n    >\n      <div className=\"details\">\n        <Thumbnail video={video} large>\n          <VideoControls />\n        </Thumbnail>\n        <VideoDetails id={video.id} />\n      </div>\n    </Layout>\n  );\n}\n\nfunction VideoInfo({ id }) {\n  const details = use(fetchVideoDetails(id));\n  return (\n    <>\n      <p className=\"info-title\">{details.title}</p>\n      <p className=\"info-description\">{details.description}</p>\n    </>\n  );\n}\n```\n\n```js src/Home.js hidden\nimport { useId, useState, use, useDeferredValue, ViewTransition } from \"react\";import { Video } from \"./Videos\";import Layout from \"./Layout\";import { fetchVideos } from \"./data\";import { IconSearch } from \"./Icons\";\n\nfunction SearchList({searchText, videos}) {\n  // useDeferredValue로 활성화합니다(\"언제\")\n  const deferredSearchText = useDeferredValue(searchText);\n  const filteredVideos = filterVideos(videos, deferredSearchText);\n  return (\n    <div className=\"video-list\">\n      {filteredVideos.length === 0 && (\n        <div className=\"no-results\">No results</div>\n      )}\n      <div className=\"videos\">\n        {filteredVideos.map((video) => (\n          // 리스트의 각 항목을 애니메이션합니다(\"무엇\")\n          <ViewTransition key={video.id}>\n            <Video video={video} />\n          </ViewTransition>\n        ))}\n      </div>\n    </div>\n  );\n}\n\nexport default function Home() {\n  const videos = use(fetchVideos());\n  const count = videos.length;\n  const [searchText, setSearchText] = useState('');\n\n  return (\n    <Layout heading={<div className=\"fit\">{count} Videos</div>}>\n      <SearchInput value={searchText} onChange={setSearchText} />\n      <SearchList videos={videos} searchText={searchText} />\n    </Layout>\n  );\n}\n\nfunction SearchInput({ value, onChange }) {\n  const id = useId();\n  return (\n    <form className=\"search\" onSubmit={(e) => e.preventDefault()}>\n      <label htmlFor={id} className=\"sr-only\">\n        Search\n      </label>\n      <div className=\"search-input\">\n        <div className=\"search-icon\">\n          <IconSearch />\n        </div>\n        <input\n          type=\"text\"\n          id={id}\n          placeholder=\"Search\"\n          value={value}\n          onChange={(e) => onChange(e.target.value)}\n        />\n      </div>\n    </form>\n  );\n}\n\nfunction filterVideos(videos, query) {\n  const keywords = query\n    .toLowerCase()\n    .split(\" \")\n    .filter((s) => s !== \"\");\n  if (keywords.length === 0) {\n    return videos;\n  }\n  return videos.filter((video) => {\n    const words = (video.title + \" \" + video.description)\n      .toLowerCase()\n      .split(\" \");\n    return keywords.every((kw) => words.some((w) => w.includes(kw)));\n  });\n}\n```\n\n```js src/Icons.js hidden\nexport function ChevronLeft() {\n  return (\n    <svg\n      className=\"chevron-left\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width=\"20\"\n      height=\"20\"\n      viewBox=\"0 0 20 20\">\n      <g fill=\"none\" fillRule=\"evenodd\" transform=\"translate(-446 -398)\">\n        <path\n          fill=\"currentColor\"\n          fillRule=\"nonzero\"\n          d=\"M95.8838835,240.366117 C95.3957281,239.877961 94.6042719,239.877961 94.1161165,240.366117 C93.6279612,240.854272 93.6279612,241.645728 94.1161165,242.133883 L98.6161165,246.633883 C99.1042719,247.122039 99.8957281,247.122039 100.383883,246.633883 L104.883883,242.133883 C105.372039,241.645728 105.372039,240.854272 104.883883,240.366117 C104.395728,239.877961 103.604272,239.877961 103.116117,240.366117 L99.5,243.982233 L95.8838835,240.366117 Z\"\n          transform=\"translate(356.5 164.5)\"\n        />\n        <polygon points=\"446 418 466 418 466 398 446 398\" />\n      </g>\n    </svg>\n  );\n}\n\nexport function PauseIcon() {\n  return (\n    <svg\n      className=\"control-icon\"\n      style={{padding: '4px'}}\n      width=\"100\"\n      height=\"100\"\n      viewBox=\"0 0 512 512\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\">\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M256 0C114.617 0 0 114.615 0 256s114.617 256 256 256 256-114.615 256-256S397.383 0 256 0zm-32 320c0 8.836-7.164 16-16 16h-32c-8.836 0-16-7.164-16-16V192c0-8.836 7.164-16 16-16h32c8.836 0 16 7.164 16 16v128zm128 0c0 8.836-7.164 16-16 16h-32c-8.836 0-16-7.164-16-16V192c0-8.836 7.164-16 16-16h32c8.836 0 16 7.164 16 16v128z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  );\n}\n\nexport function PlayIcon() {\n  return (\n    <svg\n      className=\"control-icon\"\n      width=\"100\"\n      height=\"100\"\n      viewBox=\"0 0 72 72\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\">\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M36 69C54.2254 69 69 54.2254 69 36C69 17.7746 54.2254 3 36 3C17.7746 3 3 17.7746 3 36C3 54.2254 17.7746 69 36 69ZM52.1716 38.6337L28.4366 51.5801C26.4374 52.6705 24 51.2235 24 48.9464V23.0536C24 20.7764 26.4374 19.3295 28.4366 20.4199L52.1716 33.3663C54.2562 34.5034 54.2562 37.4966 52.1716 38.6337Z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  );\n}\nexport function Heart({liked, animate}) {\n  return (\n    <>\n      <svg\n        className=\"absolute overflow-visible\"\n        viewBox=\"0 0 24 24\"\n        fill=\"none\"\n        xmlns=\"http://www.w3.org/2000/svg\">\n        <circle\n          className={`circle ${liked ? 'liked' : ''} ${animate ? 'animate' : ''}`}\n          cx=\"12\"\n          cy=\"12\"\n          r=\"11.5\"\n          fill=\"transparent\"\n          strokeWidth=\"0\"\n          stroke=\"currentColor\"\n        />\n      </svg>\n\n      <svg\n        className={`heart ${liked ? 'liked' : ''} ${animate ? 'animate' : ''}`}\n        viewBox=\"0 0 24 24\"\n        fill=\"none\"\n        xmlns=\"http://www.w3.org/2000/svg\">\n        {liked ? (\n          <path\n            d=\"M12 23a.496.496 0 0 1-.26-.074C7.023 19.973 0 13.743 0 8.68c0-4.12 2.322-6.677 6.058-6.677 2.572 0 5.108 2.387 5.134 2.41l.808.771.808-.771C12.834 4.387 15.367 2 17.935 2 21.678 2 24 4.558 24 8.677c0 5.06-7.022 11.293-11.74 14.246a.496.496 0 0 1-.26.074V23z\"\n            fill=\"currentColor\"\n          />\n        ) : (\n          <path\n            fillRule=\"evenodd\"\n            clipRule=\"evenodd\"\n            d=\"m12 5.184-.808-.771-.004-.004C11.065 4.299 8.522 2.003 6 2.003c-3.736 0-6 2.558-6 6.677 0 4.47 5.471 9.848 10 13.079.602.43 1.187.82 1.74 1.167A.497.497 0 0 0 12 23v-.003c.09 0 .182-.026.26-.074C16.977 19.97 24 13.737 24 8.677 24 4.557 21.743 2 18 2c-2.569 0-5.166 2.387-5.192 2.413L12 5.184zm-.002 15.525c2.071-1.388 4.477-3.342 6.427-5.47C20.72 12.733 22 10.401 22 8.677c0-1.708-.466-2.855-1.087-3.55C20.316 4.459 19.392 4 18 4c-.726 0-1.63.364-2.5.9-.67.412-1.148.82-1.266.92-.03.025-.037.031-.019.014l-.013.013L12 7.949 9.832 5.88a10.08 10.08 0 0 0-1.33-.977C7.633 4.367 6.728 4.003 6 4.003c-1.388 0-2.312.459-2.91 1.128C2.466 5.826 2 6.974 2 8.68c0 1.726 1.28 4.058 3.575 6.563 1.948 2.127 4.352 4.078 6.423 5.466z\"\n            fill=\"currentColor\"\n          />\n        )}\n      </svg>\n    </>\n  );\n}\n\nexport function IconSearch(props) {\n  return (\n    <svg width=\"1em\" height=\"1em\" viewBox=\"0 0 20 20\">\n      <path\n        d=\"M14.386 14.386l4.0877 4.0877-4.0877-4.0877c-2.9418 2.9419-7.7115 2.9419-10.6533 0-2.9419-2.9418-2.9419-7.7115 0-10.6533 2.9418-2.9419 7.7115-2.9419 10.6533 0 2.9419 2.9418 2.9419 7.7115 0 10.6533z\"\n        stroke=\"currentColor\"\n        fill=\"none\"\n        strokeWidth=\"2\"\n        fillRule=\"evenodd\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"></path>\n    </svg>\n  );\n}\n```\n\n```js src/Layout.js hidden\nimport {ViewTransition} from 'react'; import { useIsNavPending } from \"./router\";\n\nexport default function Page({ heading, children }) {\n  const isPending = useIsNavPending();\n  return (\n    <div className=\"page\">\n      <div className=\"top\">\n        <div className=\"top-nav\">\n          {/* 트랜지션 타입에 따라 커스텀 클래스를 적용합니다. */}\n          <ViewTransition\n            name=\"nav\"\n            share={{\n              'nav-forward': 'slide-forward',\n              'nav-back': 'slide-back',\n            }}>\n            {heading}\n          </ViewTransition>\n          {isPending && <span className=\"loader\"></span>}\n        </div>\n      </div>\n      {/* 콘텐츠에 대해 ViewTransition을 사용하지 않습니다. */}\n      {/* 콘텐츠는 자체 ViewTransition을 정의할 수 있습니다. */}\n      <ViewTransition default=\"none\">\n        <div className=\"bottom\">\n          <div className=\"content\">{children}</div>\n        </div>\n      </ViewTransition>\n    </div>\n  );\n}\n```\n\n```js src/LikeButton.js hidden\nimport {useState} from 'react';\nimport {Heart} from './Icons';\n\n// A hack since we don't actually have a backend.\n// Unlike local state, this survives videos being filtered.\nconst likedVideos = new Set();\n\nexport default function LikeButton({video}) {\n  const [isLiked, setIsLiked] = useState(() => likedVideos.has(video.id));\n  const [animate, setAnimate] = useState(false);\n  return (\n    <button\n      className={`like-button ${isLiked && 'liked'}`}\n      aria-label={isLiked ? 'Unsave' : 'Save'}\n      onClick={() => {\n        const nextIsLiked = !isLiked;\n        if (nextIsLiked) {\n          likedVideos.add(video.id);\n        } else {\n          likedVideos.delete(video.id);\n        }\n        setAnimate(true);\n        setIsLiked(nextIsLiked);\n      }}>\n      <Heart liked={isLiked} animate={animate} />\n    </button>\n  );\n}\n```\n\n```js src/Videos.js hidden\nimport { useState, ViewTransition } from \"react\";\nimport LikeButton from \"./LikeButton\";\nimport { useRouter } from \"./router\";\nimport { PauseIcon, PlayIcon } from \"./Icons\";\nimport { startTransition } from \"react\";\n\nexport function Thumbnail({ video, children }) {\n  // 공유 요소 트랜지션으로 애니메이션되도록 name을 추가합니다.\n  // 기본 애니메이션을 사용하므로 추가 CSS가 필요하지 않습니다.\n  return (\n    <ViewTransition name={`video-${video.id}`}>\n      <div\n        aria-hidden=\"true\"\n        tabIndex={-1}\n        className={`thumbnail ${video.image}`}\n      >\n        {children}\n      </div>\n    </ViewTransition>\n  );\n}\n\nexport function VideoControls() {\n  const [isPlaying, setIsPlaying] = useState(false);\n\n  return (\n    <span\n      className=\"controls\"\n      onClick={() =>\n        startTransition(() => {\n          setIsPlaying((p) => !p);\n        })\n      }\n    >\n      {isPlaying ? <PauseIcon /> : <PlayIcon />}\n    </span>\n  );\n}\n\nexport function Video({ video }) {\n  const { navigate } = useRouter();\n\n  return (\n    <div className=\"video\">\n      <div\n        className=\"link\"\n        onClick={(e) => {\n          e.preventDefault();\n          navigate(`/video/${video.id}`);\n        }}\n      >\n        <Thumbnail video={video}></Thumbnail>\n\n        <div className=\"info\">\n          <div className=\"video-title\">{video.title}</div>\n          <div className=\"video-description\">{video.description}</div>\n        </div>\n      </div>\n      <LikeButton video={video} />\n    </div>\n  );\n}\n```\n\n\n```js src/data.js hidden\nconst videos = [\n  {\n    id: '1',\n    title: 'First video',\n    description: 'Video description',\n    image: 'blue',\n  },\n  {\n    id: '2',\n    title: 'Second video',\n    description: 'Video description',\n    image: 'red',\n  },\n  {\n    id: '3',\n    title: 'Third video',\n    description: 'Video description',\n    image: 'green',\n  },\n  {\n    id: '4',\n    title: 'Fourth video',\n    description: 'Video description',\n    image: 'purple',\n  },\n  {\n    id: '5',\n    title: 'Fifth video',\n    description: 'Video description',\n    image: 'yellow',\n  },\n  {\n    id: '6',\n    title: 'Sixth video',\n    description: 'Video description',\n    image: 'gray',\n  },\n];\n\nlet videosCache = new Map();\nlet videoCache = new Map();\nlet videoDetailsCache = new Map();\nconst VIDEO_DELAY = 1;\nconst VIDEO_DETAILS_DELAY = 1000;\nexport function fetchVideos() {\n  if (videosCache.has(0)) {\n    return videosCache.get(0);\n  }\n  const promise = new Promise((resolve) => {\n    setTimeout(() => {\n      resolve(videos);\n    }, VIDEO_DELAY);\n  });\n  videosCache.set(0, promise);\n  return promise;\n}\n\nexport function fetchVideo(id) {\n  if (videoCache.has(id)) {\n    return videoCache.get(id);\n  }\n  const promise = new Promise((resolve) => {\n    setTimeout(() => {\n      resolve(videos.find((video) => video.id === id));\n    }, VIDEO_DELAY);\n  });\n  videoCache.set(id, promise);\n  return promise;\n}\n\nexport function fetchVideoDetails(id) {\n  if (videoDetailsCache.has(id)) {\n    return videoDetailsCache.get(id);\n  }\n  const promise = new Promise((resolve) => {\n    setTimeout(() => {\n      resolve(videos.find((video) => video.id === id));\n    }, VIDEO_DETAILS_DELAY);\n  });\n  videoDetailsCache.set(id, promise);\n  return promise;\n}\n```\n\n```js src/router.js hidden\nimport {useState, createContext, use, useTransition, useLayoutEffect, useEffect, addTransitionType} from \"react\";\n\nexport function Router({ children }) {\n  const [isPending, startTransition] = useTransition();\n  const [routerState, setRouterState] = useState({pendingNav: () => {}, url: document.location.pathname});\n  function navigate(url) {\n    startTransition(() => {\n      // 전환 원인이 \"nav forward\"인 경우의 트랜지션 타입\n      addTransitionType('nav-forward');\n      go(url);\n    });\n  }\n  function navigateBack(url) {\n    startTransition(() => {\n      // 전환 원인이 \"nav backward\"인 경우의 트랜지션 타입\n      addTransitionType('nav-back');\n      go(url);\n    });\n  }\n\n  function go(url) {\n    setRouterState({\n      url,\n      pendingNav() {\n        window.history.pushState({}, \"\", url);\n      },\n    });\n  }\n\n  useEffect(() => {\n    function handlePopState() {\n      // 복원은 동기적으로 이루어져야 하므로 애니메이션되면 안 됩니다.\n      // 트랜지션이더라도 마찬가지입니다.\n      startTransition(() => {\n        setRouterState({\n          url: document.location.pathname + document.location.search,\n          pendingNav() {\n            // 아무 작업도 하지 않습니다. URL은 이미 업데이트되었습니다.\n          },\n        });\n      });\n    }\n    window.addEventListener(\"popstate\", handlePopState);\n    return () => {\n      window.removeEventListener(\"popstate\", handlePopState);\n    };\n  }, []);\n  const pendingNav = routerState.pendingNav;\n  useLayoutEffect(() => {\n    pendingNav();\n  }, [pendingNav]);\n\n  return (\n    <RouterContext\n      value={{\n        url: routerState.url,\n        navigate,\n        navigateBack,\n        isPending,\n        params: {},\n      }}\n    >\n      {children}\n    </RouterContext>\n  );\n}\n\nconst RouterContext = createContext({ url: \"/\", params: {} });\n\nexport function useRouter() {\n  return use(RouterContext);\n}\n\nexport function useIsNavPending() {\n  return use(RouterContext).isPending;\n}\n\n```\n\n```css src/styles.css hidden\n@font-face {\n  font-family: Optimistic Text;\n  src: url(https://react.dev/fonts/Optimistic_Text_W_Rg.woff2) format(\"woff2\");\n  font-weight: 400;\n  font-style: normal;\n  font-display: swap;\n}\n\n@font-face {\n  font-family: Optimistic Text;\n  src: url(https://react.dev/fonts/Optimistic_Text_W_Md.woff2) format(\"woff2\");\n  font-weight: 500;\n  font-style: normal;\n  font-display: swap;\n}\n\n@font-face {\n  font-family: Optimistic Text;\n  src: url(https://react.dev/fonts/Optimistic_Text_W_Bd.woff2) format(\"woff2\");\n  font-weight: 600;\n  font-style: normal;\n  font-display: swap;\n}\n\n@font-face {\n  font-family: Optimistic Text;\n  src: url(https://react.dev/fonts/Optimistic_Text_W_Bd.woff2) format(\"woff2\");\n  font-weight: 700;\n  font-style: normal;\n  font-display: swap;\n}\n\n* {\n  box-sizing: border-box;\n}\n\nhtml {\n  background-image: url(https://react.dev/images/meta-gradient-dark.png);\n  background-size: 100%;\n  background-position: -100%;\n  background-color: rgb(64 71 86);\n  background-repeat: no-repeat;\n  height: 100%;\n  width: 100%;\n}\n\nbody {\n  font-family: Optimistic Text, -apple-system, ui-sans-serif, system-ui, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;\n  padding: 10px 0 10px 0;\n  margin: 0;\n  display: flex;\n  justify-content: center;\n}\n\n#root {\n  flex: 1 1;\n  height: auto;\n  background-color: #fff;\n  border-radius: 10px;\n  max-width: 450px;\n  min-height: 600px;\n  padding-bottom: 10px;\n}\n\nh1 {\n  margin-top: 0;\n  font-size: 22px;\n}\n\nh2 {\n  margin-top: 0;\n  font-size: 20px;\n}\n\nh3 {\n  margin-top: 0;\n  font-size: 18px;\n}\n\nh4 {\n  margin-top: 0;\n  font-size: 16px;\n}\n\nh5 {\n  margin-top: 0;\n  font-size: 14px;\n}\n\nh6 {\n  margin-top: 0;\n  font-size: 12px;\n}\n\ncode {\n  font-size: 1.2em;\n}\n\nul {\n  padding-inline-start: 20px;\n}\n\n.sr-only {\n  position: absolute;\n  width: 1px;\n  height: 1px;\n  padding: 0;\n  margin: -1px;\n  overflow: hidden;\n  clip: rect(0, 0, 0, 0);\n  white-space: nowrap;\n  border-width: 0;\n}\n\n.absolute {\n  position: absolute;\n}\n\n.overflow-visible {\n  overflow: visible;\n}\n\n.visible {\n  overflow: visible;\n}\n\n.fit {\n  width: fit-content;\n}\n\n\n/* Layout */\n.page {\n  display: flex;\n  flex-direction: column;\n  height: 100%;\n}\n\n.top-hero {\n  height: 200px;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  background-image: conic-gradient(\n      from 90deg at -10% 100%,\n      #2b303b 0deg,\n      #2b303b 90deg,\n      #16181d 1turn\n  );\n}\n\n.bottom {\n  flex: 1;\n  overflow: auto;\n}\n\n.top-nav {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  margin-bottom: 0;\n  padding: 0 12px;\n  top: 0;\n  width: 100%;\n  height: 44px;\n  color: #23272f;\n  font-weight: 700;\n  font-size: 20px;\n  z-index: 100;\n  cursor: default;\n}\n\n.content {\n  padding: 0 12px;\n  margin-top: 4px;\n}\n\n\n.loader {\n  color: #23272f;\n  font-size: 3px;\n  width: 1em;\n  margin-right: 18px;\n  height: 1em;\n  border-radius: 50%;\n  position: relative;\n  text-indent: -9999em;\n  animation: loading-spinner 1.3s infinite linear;\n  animation-delay: 200ms;\n  transform: translateZ(0);\n}\n\n@keyframes loading-spinner {\n  0%,\n  100% {\n    box-shadow: 0 -3em 0 0.2em,\n    2em -2em 0 0em, 3em 0 0 -1em,\n    2em 2em 0 -1em, 0 3em 0 -1em,\n    -2em 2em 0 -1em, -3em 0 0 -1em,\n    -2em -2em 0 0;\n  }\n  12.5% {\n    box-shadow: 0 -3em 0 0, 2em -2em 0 0.2em,\n    3em 0 0 0, 2em 2em 0 -1em, 0 3em 0 -1em,\n    -2em 2em 0 -1em, -3em 0 0 -1em,\n    -2em -2em 0 -1em;\n  }\n  25% {\n    box-shadow: 0 -3em 0 -0.5em,\n    2em -2em 0 0, 3em 0 0 0.2em,\n    2em 2em 0 0, 0 3em 0 -1em,\n    -2em 2em 0 -1em, -3em 0 0 -1em,\n    -2em -2em 0 -1em;\n  }\n  37.5% {\n    box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em,\n    3em 0em 0 0, 2em 2em 0 0.2em, 0 3em 0 0em,\n    -2em 2em 0 -1em, -3em 0em 0 -1em, -2em -2em 0 -1em;\n  }\n  50% {\n    box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em,\n    3em 0 0 -1em, 2em 2em 0 0em, 0 3em 0 0.2em,\n    -2em 2em 0 0, -3em 0em 0 -1em, -2em -2em 0 -1em;\n  }\n  62.5% {\n    box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em,\n    3em 0 0 -1em, 2em 2em 0 -1em, 0 3em 0 0,\n    -2em 2em 0 0.2em, -3em 0 0 0, -2em -2em 0 -1em;\n  }\n  75% {\n    box-shadow: 0em -3em 0 -1em, 2em -2em 0 -1em,\n    3em 0em 0 -1em, 2em 2em 0 -1em, 0 3em 0 -1em,\n    -2em 2em 0 0, -3em 0em 0 0.2em, -2em -2em 0 0;\n  }\n  87.5% {\n    box-shadow: 0em -3em 0 0, 2em -2em 0 -1em,\n    3em 0 0 -1em, 2em 2em 0 -1em, 0 3em 0 -1em,\n    -2em 2em 0 0, -3em 0em 0 0, -2em -2em 0 0.2em;\n  }\n}\n\n/* LikeButton */\n.like-button {\n  outline-offset: 2px;\n  position: relative;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  width: 2.5rem;\n  height: 2.5rem;\n  cursor: pointer;\n  border-radius: 9999px;\n  border: none;\n  outline: none 2px;\n  color: #5e687e;\n  background: none;\n}\n\n.like-button:focus {\n  color: #a6423a;\n  background-color: rgba(166, 66, 58, .05);\n}\n\n.like-button:active {\n  color: #a6423a;\n  background-color: rgba(166, 66, 58, .05);\n  transform: scaleX(0.95) scaleY(0.95);\n}\n\n.like-button:hover {\n  background-color: #f6f7f9;\n}\n\n.like-button.liked {\n  color: #a6423a;\n}\n\n/* Icons */\n@keyframes circle {\n  0% {\n    transform: scale(0);\n    stroke-width: 16px;\n  }\n\n  50% {\n    transform: scale(.5);\n    stroke-width: 16px;\n  }\n\n  to {\n    transform: scale(1);\n    stroke-width: 0;\n  }\n}\n\n.circle {\n  color: rgba(166, 66, 58, .5);\n  transform-origin: center;\n  transition-property: all;\n  transition-duration: .15s;\n  transition-timing-function: cubic-bezier(.4,0,.2,1);\n}\n\n.circle.liked.animate {\n  animation: circle .3s forwards;\n}\n\n.heart {\n  width: 1.5rem;\n  height: 1.5rem;\n}\n\n.heart.liked {\n  transform-origin: center;\n  transition-property: all;\n  transition-duration: .15s;\n  transition-timing-function: cubic-bezier(.4, 0, .2, 1);\n}\n\n.heart.liked.animate {\n  animation: scale .35s ease-in-out forwards;\n}\n\n.control-icon {\n  color: hsla(0, 0%, 100%, .5);\n  filter:  drop-shadow(0 20px 13px rgba(0, 0, 0, .03)) drop-shadow(0 8px 5px rgba(0, 0, 0, .08));\n}\n\n.chevron-left {\n  margin-top: 2px;\n  rotate: 90deg;\n}\n\n\n/* Video */\n.thumbnail {\n  position: relative;\n  aspect-ratio: 16 / 9;\n  display: flex;\n  overflow: hidden;\n  flex-direction: column;\n  justify-content: center;\n  align-items: center;\n  border-radius: 0.5rem;\n  outline-offset: 2px;\n  width: 8rem;\n  vertical-align: middle;\n  background-color: #ffffff;\n  background-size: cover;\n  user-select: none;\n}\n\n.thumbnail.blue {\n  background-image: conic-gradient(at top right, #c76a15, #087ea4, #2b3491);\n}\n\n.thumbnail.red {\n  background-image: conic-gradient(at top right, #c76a15, #a6423a, #2b3491);\n}\n\n.thumbnail.green {\n  background-image: conic-gradient(at top right, #c76a15, #388f7f, #2b3491);\n}\n\n.thumbnail.purple {\n  background-image: conic-gradient(at top right, #c76a15, #575fb7, #2b3491);\n}\n\n.thumbnail.yellow {\n  background-image: conic-gradient(at top right, #c76a15, #FABD62, #2b3491);\n}\n\n.thumbnail.gray {\n  background-image: conic-gradient(at top right, #c76a15, #4E5769, #2b3491);\n}\n\n.video {\n  display: flex;\n  flex-direction: row;\n  gap: 0.75rem;\n  align-items: center;\n}\n\n.video .link {\n  display: flex;\n  flex-direction: row;\n  flex: 1 1 0;\n  gap: 0.125rem;\n  outline-offset: 4px;\n  cursor: pointer;\n}\n\n.video .info {\n  display: flex;\n  flex-direction: column;\n  justify-content: center;\n  margin-left: 8px;\n  gap: 0.125rem;\n}\n\n.video .info:hover {\n  text-decoration: underline;\n}\n\n.video-title {\n  font-size: 15px;\n  line-height: 1.25;\n  font-weight: 700;\n  color: #23272f;\n}\n\n.video-description {\n  color: #5e687e;\n  font-size: 13px;\n}\n\n/* Details */\n.details .thumbnail {\n  position: relative;\n  aspect-ratio: 16 / 9;\n  display: flex;\n  overflow: hidden;\n  flex-direction: column;\n  justify-content: center;\n  align-items: center;\n  border-radius: 0.5rem;\n  outline-offset: 2px;\n  width: 100%;\n  vertical-align: middle;\n  background-color: #ffffff;\n  background-size: cover;\n  user-select: none;\n}\n\n.video-details-title {\n  margin-top: 8px;\n}\n\n.video-details-speaker {\n  display: flex;\n  gap: 8px;\n  margin-top: 10px\n}\n\n.back {\n  display: flex;\n  align-items: center;\n  margin-left: -5px;\n  cursor: pointer;\n}\n\n.back:hover {\n  text-decoration: underline;\n}\n\n.info-title {\n  font-size: 1.5rem;\n  font-weight: 700;\n  line-height: 1.25;\n  margin: 8px 0 0 0 ;\n}\n\n.info-description {\n  margin: 8px 0 0 0;\n}\n\n.controls {\n  cursor: pointer;\n}\n\n.fallback {\n  background: #f6f7f8 linear-gradient(to right, #e6e6e6 5%, #cccccc 25%, #e6e6e6 35%) no-repeat;\n  background-size: 800px 104px;\n  display: block;\n  line-height: 1.25;\n  margin: 8px 0 0 0;\n  border-radius: 5px;\n  overflow: hidden;\n\n  animation: 1s linear 1s infinite shimmer;\n  animation-delay: 300ms;\n  animation-duration: 1s;\n  animation-fill-mode: forwards;\n  animation-iteration-count: infinite;\n  animation-name: shimmer;\n  animation-timing-function: linear;\n}\n\n\n.fallback.title {\n  width: 130px;\n  height: 30px;\n\n}\n\n.fallback.description {\n  width: 150px;\n  height: 21px;\n}\n\n@keyframes shimmer {\n  0% {\n    background-position: -468px 0;\n  }\n\n  100% {\n    background-position: 468px 0;\n  }\n}\n\n.search {\n  margin-bottom: 10px;\n}\n.search-input {\n  width: 100%;\n  position: relative;\n}\n\n.search-icon {\n  position: absolute;\n  top: 0;\n  bottom: 0;\n  inset-inline-start: 0;\n  display: flex;\n  align-items: center;\n  padding-inline-start: 1rem;\n  pointer-events: none;\n  color: #99a1b3;\n}\n\n.search-input input {\n  display: flex;\n  padding-inline-start: 2.75rem;\n  padding-top: 10px;\n  padding-bottom: 10px;\n  width: 100%;\n  text-align: start;\n  background-color: rgb(235 236 240);\n  outline: 2px solid transparent;\n  cursor: pointer;\n  border: none;\n  align-items: center;\n  color: rgb(35 39 47);\n  border-radius: 9999px;\n  vertical-align: middle;\n  font-size: 15px;\n}\n\n.search-input input:hover, .search-input input:active {\n  background-color: rgb(235 236 240/ 0.8);\n  color: rgb(35 39 47/ 0.8);\n}\n\n/* Home */\n.video-list {\n  position: relative;\n}\n\n.video-list .videos {\n  display: flex;\n  flex-direction: column;\n  gap: 1rem;\n  overflow-y: auto;\n  height: 100%;\n}\n```\n\n\n```css src/animations.css\n/* 추가 애니메이션은 필요하지 않습니다 */\n\n\n\n\n\n\n\n\n\n/* 이전에 정의된 애니메이션은 아래에 있습니다 */\n\n\n\n\n\n\n/* Suspense 폴백을 아래로 슬라이드하는 애니메이션 */\n::view-transition-old(.slide-down) {\n    animation: 150ms ease-out both fade-out, 150ms ease-out both slide-down;\n}\n\n::view-transition-new(.slide-up) {\n    animation: 210ms ease-in 150ms both fade-in, 400ms ease-in both slide-up;\n}\n\n/* 트랜지션 타입에 의해 추가된 View Transition 클래스용 애니메이션 */\n::view-transition-old(.slide-forward) {\n    /* 앞으로 전환할 때 \"old\" 페이지는 왼쪽으로 슬라이드되어 나가야 합니다. */\n    animation: 150ms cubic-bezier(0.4, 0, 1, 1) both fade-out,\n    400ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;\n}\n\n::view-transition-new(.slide-forward) {\n    /* 앞으로 전환할 때 \"new\" 페이지는 오른쪽에서 슬라이드되어 들어와야 합니다. */\n    animation: 210ms cubic-bezier(0, 0, 0.2, 1) 150ms both fade-in,\n    400ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;\n}\n\n::view-transition-old(.slide-back) {\n    /* 뒤로 전환할 때 \"old\" 페이지는 오른쪽으로 슬라이드되어 나가야 합니다. */\n    animation: 150ms cubic-bezier(0.4, 0, 1, 1) both fade-out,\n    400ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-right;\n}\n\n::view-transition-new(.slide-back) {\n    /* 뒤로 전환할 때 \"new\" 페이지는 왼쪽에서 슬라이드되어 들어와야 합니다. */\n    animation: 210ms cubic-bezier(0, 0, 0.2, 1) 150ms both fade-in,\n    400ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-left;\n}\n\n/* 위 애니메이션을 지원하기 위한 키프레임 */\n@keyframes slide-up {\n    from {\n        transform: translateY(10px);\n    }\n    to {\n        transform: translateY(0);\n    }\n}\n\n@keyframes slide-down {\n    from {\n        transform: translateY(0);\n    }\n    to {\n        transform: translateY(10px);\n    }\n}\n\n@keyframes fade-in {\n    from {\n        opacity: 0;\n    }\n}\n\n@keyframes fade-out {\n    to {\n        opacity: 0;\n    }\n}\n\n@keyframes slide-to-right {\n    to {\n        transform: translateX(50px);\n    }\n}\n\n@keyframes slide-from-right {\n    from {\n        transform: translateX(50px);\n    }\n    to {\n        transform: translateX(0);\n    }\n}\n\n@keyframes slide-to-left {\n    to {\n        transform: translateX(-50px);\n    }\n}\n\n@keyframes slide-from-left {\n    from {\n        transform: translateX(-50px);\n    }\n    to {\n        transform: translateX(0);\n    }\n}\n\n/* Default .slow-fade. */\n::view-transition-old(.slow-fade) {\n    animation-duration: 500ms;\n}\n\n::view-transition-new(.slow-fade) {\n    animation-duration: 500ms;\n}\n```\n\n```js src/index.js hidden\nimport React, {StrictMode} from 'react';\nimport {createRoot} from 'react-dom/client';\nimport './styles.css';\nimport './animations.css';\n\nimport App from './App';\nimport {Router} from './router';\n\nconst root = createRoot(document.getElementById('root'));\nroot.render(\n  <StrictMode>\n    <Router>\n      <App />\n    </Router>\n  </StrictMode>\n);\n```\n\n```json package.json hidden\n{\n  \"dependencies\": {\n    \"react\": \"canary\",\n    \"react-dom\": \"canary\",\n    \"react-scripts\": \"latest\"\n  },\n  \"scripts\": {\n    \"start\": \"react-scripts start\",\n    \"build\": \"react-scripts build\",\n    \"test\": \"react-scripts test --env=jsdom\",\n    \"eject\": \"react-scripts eject\"\n  }\n}\n```\n\n</Sandpack>\n\n### Activity로 사전 렌더링하기 {/*prerender-with-activity*/}\n\n사용자가 사용할 가능성이 높은 UI의 다음 부분을 미리 준비하여 그들이 사용할 준비가 되었을 때 바로 사용할 수 있도록 하고 싶을 때가 있습니다. 사용자가 네비게이팅하기 전에 데이터를 이미 가져왔을 수 있으므로, 다음 라우트가 렌더링해야 하는 데이터를 일시 중단해야 하는 경우 특히 유용합니다.\n\n예를 들어, 현재 우리의 앱은 사용자가 동영상을 선택할 때 각 동영상에 대한 데이터를 로드하기 위해 일시 중단해야 합니다. 사용자가 탐색할 때까지 숨겨진 `<Activity>`에 있는 모든 페이지를 렌더링하여 이 문제를 개선할 수 있습니다.\n\n```js {2,5,8}\n<ViewTransition>\n  <Activity mode={url === '/' ? 'visible' : 'hidden'}>\n    <Home />\n  </Activity>\n  <Activity mode={url === '/details/1' ? 'visible' : 'hidden'}>\n    <Details id={id} />\n  </Activity>\n  <Activity mode={url === '/details/1' ? 'visible' : 'hidden'}>\n    <Details id={id} />\n  </Activity>\n<ViewTransition>\n```\n\n이 업데이트를 통해 다음 페이지의 콘텐츠가 미리 렌더링할 시간이 있는 경우 Suspense 폴백 없이 애니메이션이 적용됩니다. 동영상을 클릭하면 세부 정보 페이지의 동영상 제목과 설명이 폴백 없이 즉시 렌더링되는 것을 확인할 수 있습니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { Activity, ViewTransition, use } from \"react\"; import Details from \"./Details\"; import Home from \"./Home\"; import { useRouter } from \"./router\"; import {fetchVideos} from './data';\n\nexport default function App() {\n  const { url } = useRouter();\n  const videoId = url.split(\"/\").pop();\n  const videos = use(fetchVideos());\n\n  return (\n    <ViewTransition>\n      {/* 미리 렌더링하기 위해 Activity로 비디오를 렌더링합니다 */}\n      {videos.map(({id}) => (\n        <Activity key={id} mode={videoId === id ? 'visible' : 'hidden'}>\n          <Details id={id}/>\n        </Activity>\n      ))}\n      <Activity mode={url === '/' ? 'visible' : 'hidden'}>\n        <Home />\n      </Activity>\n    </ViewTransition>\n  );\n}\n```\n\n```js src/Details.js\nimport { use, Suspense, ViewTransition } from \"react\"; import { fetchVideo, fetchVideoDetails } from \"./data\"; import { Thumbnail, VideoControls } from \"./Videos\"; import { useRouter } from \"./router\"; import Layout from \"./Layout\"; import { ChevronLeft } from \"./Icons\";\n\nfunction VideoDetails({id}) {\n  // Suspense 폴백에서 콘텐츠로 애니메이션합니다.\n  // 미리 렌더링되어 있다면 폴백을 표시할 필요가 없습니다.\n\n  return (\n    <Suspense\n      fallback={\n        // 폴백을 아래로 애니메이션합니다.\n        <ViewTransition exit=\"slide-down\">\n          <VideoInfoFallback />\n        </ViewTransition>\n      }\n    >\n      {/* 콘텐츠를 위로 애니메이션합니다 */}\n      <ViewTransition enter=\"slide-up\">\n        <VideoInfo id={id} />\n      </ViewTransition>\n    </Suspense>\n  );\n}\n\nfunction VideoInfoFallback() {\n  return (\n    <>\n      <div className=\"fallback title\"></div>\n      <div className=\"fallback description\"></div>\n    </>\n  );\n}\n\nexport default function Details({id}) {\n  const { url, navigateBack } = useRouter();\n  const video = use(fetchVideo(id));\n\n  return (\n    <Layout\n      heading={\n        <div\n          className=\"fit back\"\n          onClick={() => {\n            navigateBack(\"/\");\n          }}\n        >\n          <ChevronLeft /> Back\n        </div>\n      }\n    >\n      <div className=\"details\">\n        <Thumbnail video={video} large>\n          <VideoControls />\n        </Thumbnail>\n        <VideoDetails id={video.id} />\n      </div>\n    </Layout>\n  );\n}\n\nfunction VideoInfo({ id }) {\n  const details = use(fetchVideoDetails(id));\n  return (\n    <>\n      <p className=\"info-title\">{details.title}</p>\n      <p className=\"info-description\">{details.description}</p>\n    </>\n  );\n}\n```\n\n```js src/Home.js hidden\nimport { useId, useState, use, useDeferredValue, ViewTransition } from \"react\";import { Video } from \"./Videos\";import Layout from \"./Layout\";import { fetchVideos } from \"./data\";import { IconSearch } from \"./Icons\";\n\nfunction SearchList({searchText, videos}) {\n  // useDeferredValue로 활성화합니다(\"언제\")\n  const deferredSearchText = useDeferredValue(searchText);\n  const filteredVideos = filterVideos(videos, deferredSearchText);\n  return (\n    <div className=\"video-list\">\n      {filteredVideos.length === 0 && (\n        <div className=\"no-results\">No results</div>\n      )}\n      <div className=\"videos\">\n        {filteredVideos.map((video) => (\n          // 리스트의 각 항목을 애니메이션합니다(\"무엇\")\n          <ViewTransition key={video.id}>\n            <Video video={video} />\n          </ViewTransition>\n        ))}\n      </div>\n    </div>\n  );\n}\n\nexport default function Home() {\n  const videos = use(fetchVideos());\n  const count = videos.length;\n  const [searchText, setSearchText] = useState('');\n\n  return (\n    <Layout heading={<div className=\"fit\">{count} Videos</div>}>\n      <SearchInput value={searchText} onChange={setSearchText} />\n      <SearchList videos={videos} searchText={searchText} />\n    </Layout>\n  );\n}\n\nfunction SearchInput({ value, onChange }) {\n  const id = useId();\n  return (\n    <form className=\"search\" onSubmit={(e) => e.preventDefault()}>\n      <label htmlFor={id} className=\"sr-only\">\n        Search\n      </label>\n      <div className=\"search-input\">\n        <div className=\"search-icon\">\n          <IconSearch />\n        </div>\n        <input\n          type=\"text\"\n          id={id}\n          placeholder=\"Search\"\n          value={value}\n          onChange={(e) => onChange(e.target.value)}\n        />\n      </div>\n    </form>\n  );\n}\n\nfunction filterVideos(videos, query) {\n  const keywords = query\n    .toLowerCase()\n    .split(\" \")\n    .filter((s) => s !== \"\");\n  if (keywords.length === 0) {\n    return videos;\n  }\n  return videos.filter((video) => {\n    const words = (video.title + \" \" + video.description)\n      .toLowerCase()\n      .split(\" \");\n    return keywords.every((kw) => words.some((w) => w.includes(kw)));\n  });\n}\n```\n\n```js src/Icons.js hidden\nexport function ChevronLeft() {\n  return (\n    <svg\n      className=\"chevron-left\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width=\"20\"\n      height=\"20\"\n      viewBox=\"0 0 20 20\">\n      <g fill=\"none\" fillRule=\"evenodd\" transform=\"translate(-446 -398)\">\n        <path\n          fill=\"currentColor\"\n          fillRule=\"nonzero\"\n          d=\"M95.8838835,240.366117 C95.3957281,239.877961 94.6042719,239.877961 94.1161165,240.366117 C93.6279612,240.854272 93.6279612,241.645728 94.1161165,242.133883 L98.6161165,246.633883 C99.1042719,247.122039 99.8957281,247.122039 100.383883,246.633883 L104.883883,242.133883 C105.372039,241.645728 105.372039,240.854272 104.883883,240.366117 C104.395728,239.877961 103.604272,239.877961 103.116117,240.366117 L99.5,243.982233 L95.8838835,240.366117 Z\"\n          transform=\"translate(356.5 164.5)\"\n        />\n        <polygon points=\"446 418 466 418 466 398 446 398\" />\n      </g>\n    </svg>\n  );\n}\n\nexport function PauseIcon() {\n  return (\n    <svg\n      className=\"control-icon\"\n      style={{padding: '4px'}}\n      width=\"100\"\n      height=\"100\"\n      viewBox=\"0 0 512 512\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\">\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M256 0C114.617 0 0 114.615 0 256s114.617 256 256 256 256-114.615 256-256S397.383 0 256 0zm-32 320c0 8.836-7.164 16-16 16h-32c-8.836 0-16-7.164-16-16V192c0-8.836 7.164-16 16-16h32c8.836 0 16 7.164 16 16v128zm128 0c0 8.836-7.164 16-16 16h-32c-8.836 0-16-7.164-16-16V192c0-8.836 7.164-16 16-16h32c8.836 0 16 7.164 16 16v128z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  );\n}\n\nexport function PlayIcon() {\n  return (\n    <svg\n      className=\"control-icon\"\n      width=\"100\"\n      height=\"100\"\n      viewBox=\"0 0 72 72\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\">\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M36 69C54.2254 69 69 54.2254 69 36C69 17.7746 54.2254 3 36 3C17.7746 3 3 17.7746 3 36C3 54.2254 17.7746 69 36 69ZM52.1716 38.6337L28.4366 51.5801C26.4374 52.6705 24 51.2235 24 48.9464V23.0536C24 20.7764 26.4374 19.3295 28.4366 20.4199L52.1716 33.3663C54.2562 34.5034 54.2562 37.4966 52.1716 38.6337Z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  );\n}\nexport function Heart({liked, animate}) {\n  return (\n    <>\n      <svg\n        className=\"absolute overflow-visible\"\n        viewBox=\"0 0 24 24\"\n        fill=\"none\"\n        xmlns=\"http://www.w3.org/2000/svg\">\n        <circle\n          className={`circle ${liked ? 'liked' : ''} ${animate ? 'animate' : ''}`}\n          cx=\"12\"\n          cy=\"12\"\n          r=\"11.5\"\n          fill=\"transparent\"\n          strokeWidth=\"0\"\n          stroke=\"currentColor\"\n        />\n      </svg>\n\n      <svg\n        className={`heart ${liked ? 'liked' : ''} ${animate ? 'animate' : ''}`}\n        viewBox=\"0 0 24 24\"\n        fill=\"none\"\n        xmlns=\"http://www.w3.org/2000/svg\">\n        {liked ? (\n          <path\n            d=\"M12 23a.496.496 0 0 1-.26-.074C7.023 19.973 0 13.743 0 8.68c0-4.12 2.322-6.677 6.058-6.677 2.572 0 5.108 2.387 5.134 2.41l.808.771.808-.771C12.834 4.387 15.367 2 17.935 2 21.678 2 24 4.558 24 8.677c0 5.06-7.022 11.293-11.74 14.246a.496.496 0 0 1-.26.074V23z\"\n            fill=\"currentColor\"\n          />\n        ) : (\n          <path\n            fillRule=\"evenodd\"\n            clipRule=\"evenodd\"\n            d=\"m12 5.184-.808-.771-.004-.004C11.065 4.299 8.522 2.003 6 2.003c-3.736 0-6 2.558-6 6.677 0 4.47 5.471 9.848 10 13.079.602.43 1.187.82 1.74 1.167A.497.497 0 0 0 12 23v-.003c.09 0 .182-.026.26-.074C16.977 19.97 24 13.737 24 8.677 24 4.557 21.743 2 18 2c-2.569 0-5.166 2.387-5.192 2.413L12 5.184zm-.002 15.525c2.071-1.388 4.477-3.342 6.427-5.47C20.72 12.733 22 10.401 22 8.677c0-1.708-.466-2.855-1.087-3.55C20.316 4.459 19.392 4 18 4c-.726 0-1.63.364-2.5.9-.67.412-1.148.82-1.266.92-.03.025-.037.031-.019.014l-.013.013L12 7.949 9.832 5.88a10.08 10.08 0 0 0-1.33-.977C7.633 4.367 6.728 4.003 6 4.003c-1.388 0-2.312.459-2.91 1.128C2.466 5.826 2 6.974 2 8.68c0 1.726 1.28 4.058 3.575 6.563 1.948 2.127 4.352 4.078 6.423 5.466z\"\n            fill=\"currentColor\"\n          />\n        )}\n      </svg>\n    </>\n  );\n}\n\nexport function IconSearch(props) {\n  return (\n    <svg width=\"1em\" height=\"1em\" viewBox=\"0 0 20 20\">\n      <path\n        d=\"M14.386 14.386l4.0877 4.0877-4.0877-4.0877c-2.9418 2.9419-7.7115 2.9419-10.6533 0-2.9419-2.9418-2.9419-7.7115 0-10.6533 2.9418-2.9419 7.7115-2.9419 10.6533 0 2.9419 2.9418 2.9419 7.7115 0 10.6533z\"\n        stroke=\"currentColor\"\n        fill=\"none\"\n        strokeWidth=\"2\"\n        fillRule=\"evenodd\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"></path>\n    </svg>\n  );\n}\n```\n\n```js src/Layout.js hidden\nimport {ViewTransition} from 'react'; import { useIsNavPending } from \"./router\";\n\nexport default function Page({ heading, children }) {\n  const isPending = useIsNavPending();\n  return (\n    <div className=\"page\">\n      <div className=\"top\">\n        <div className=\"top-nav\">\n          {/* 트랜지션 타입에 따라 커스텀 클래스를 적용합니다. */}\n          <ViewTransition\n            name=\"nav\"\n            share={{\n              'nav-forward': 'slide-forward',\n              'nav-back': 'slide-back',\n            }}>\n            {heading}\n          </ViewTransition>\n          {isPending && <span className=\"loader\"></span>}\n        </div>\n      </div>\n      {/* 콘텐츠에 대해 ViewTransition을 사용하지 않습니다. */}\n      {/* 콘텐츠는 자체 ViewTransition을 정의할 수 있습니다. */}\n      <ViewTransition default=\"none\">\n        <div className=\"bottom\">\n          <div className=\"content\">{children}</div>\n        </div>\n      </ViewTransition>\n    </div>\n  );\n}\n```\n\n```js src/LikeButton.js hidden\nimport {useState} from 'react';\nimport {Heart} from './Icons';\n\n// A hack since we don't actually have a backend.\n// Unlike local state, this survives videos being filtered.\nconst likedVideos = new Set();\n\nexport default function LikeButton({video}) {\n  const [isLiked, setIsLiked] = useState(() => likedVideos.has(video.id));\n  const [animate, setAnimate] = useState(false);\n  return (\n    <button\n      className={`like-button ${isLiked && 'liked'}`}\n      aria-label={isLiked ? 'Unsave' : 'Save'}\n      onClick={() => {\n        const nextIsLiked = !isLiked;\n        if (nextIsLiked) {\n          likedVideos.add(video.id);\n        } else {\n          likedVideos.delete(video.id);\n        }\n        setAnimate(true);\n        setIsLiked(nextIsLiked);\n      }}>\n      <Heart liked={isLiked} animate={animate} />\n    </button>\n  );\n}\n```\n\n```js src/Videos.js hidden\nimport { useState, ViewTransition } from \"react\";\nimport LikeButton from \"./LikeButton\";\nimport { useRouter } from \"./router\";\nimport { PauseIcon, PlayIcon } from \"./Icons\";\nimport { startTransition } from \"react\";\n\nexport function Thumbnail({ video, children }) {\n  // 공유 요소 트랜지션으로 애니메이션되도록 name을 추가합니다.\n  // 기본 애니메이션을 사용하므로 추가 CSS가 필요하지 않습니다.\n  return (\n    <ViewTransition name={`video-${video.id}`}>\n      <div\n        aria-hidden=\"true\"\n        tabIndex={-1}\n        className={`thumbnail ${video.image}`}\n      >\n        {children}\n      </div>\n    </ViewTransition>\n  );\n}\n\nexport function VideoControls() {\n  const [isPlaying, setIsPlaying] = useState(false);\n\n  return (\n    <span\n      className=\"controls\"\n      onClick={() =>\n        startTransition(() => {\n          setIsPlaying((p) => !p);\n        })\n      }\n    >\n      {isPlaying ? <PauseIcon /> : <PlayIcon />}\n    </span>\n  );\n}\n\nexport function Video({ video }) {\n  const { navigate } = useRouter();\n\n  return (\n    <div className=\"video\">\n      <div\n        className=\"link\"\n        onClick={(e) => {\n          e.preventDefault();\n          navigate(`/video/${video.id}`);\n        }}\n      >\n        <Thumbnail video={video}></Thumbnail>\n\n        <div className=\"info\">\n          <div className=\"video-title\">{video.title}</div>\n          <div className=\"video-description\">{video.description}</div>\n        </div>\n      </div>\n      <LikeButton video={video} />\n    </div>\n  );\n}\n```\n\n\n```js src/data.js hidden\nconst videos = [\n  {\n    id: '1',\n    title: 'First video',\n    description: 'Video description',\n    image: 'blue',\n  },\n  {\n    id: '2',\n    title: 'Second video',\n    description: 'Video description',\n    image: 'red',\n  },\n  {\n    id: '3',\n    title: 'Third video',\n    description: 'Video description',\n    image: 'green',\n  },\n  {\n    id: '4',\n    title: 'Fourth video',\n    description: 'Video description',\n    image: 'purple',\n  },\n  {\n    id: '5',\n    title: 'Fifth video',\n    description: 'Video description',\n    image: 'yellow',\n  },\n  {\n    id: '6',\n    title: 'Sixth video',\n    description: 'Video description',\n    image: 'gray',\n  },\n];\n\nlet videosCache = new Map();\nlet videoCache = new Map();\nlet videoDetailsCache = new Map();\nconst VIDEO_DELAY = 1;\nconst VIDEO_DETAILS_DELAY = 1000;\nexport function fetchVideos() {\n  if (videosCache.has(0)) {\n    return videosCache.get(0);\n  }\n  const promise = new Promise((resolve) => {\n    setTimeout(() => {\n      resolve(videos);\n    }, VIDEO_DELAY);\n  });\n  videosCache.set(0, promise);\n  return promise;\n}\n\nexport function fetchVideo(id) {\n  if (videoCache.has(id)) {\n    return videoCache.get(id);\n  }\n  const promise = new Promise((resolve) => {\n    setTimeout(() => {\n      resolve(videos.find((video) => video.id === id));\n    }, VIDEO_DELAY);\n  });\n  videoCache.set(id, promise);\n  return promise;\n}\n\nexport function fetchVideoDetails(id) {\n  if (videoDetailsCache.has(id)) {\n    return videoDetailsCache.get(id);\n  }\n  const promise = new Promise((resolve) => {\n    setTimeout(() => {\n      resolve(videos.find((video) => video.id === id));\n    }, VIDEO_DETAILS_DELAY);\n  });\n  videoDetailsCache.set(id, promise);\n  return promise;\n}\n```\n\n```js src/router.js hidden\nimport {useState, createContext, use, useTransition, useLayoutEffect, useEffect, addTransitionType} from \"react\";\n\nexport function Router({ children }) {\n  const [isPending, startTransition] = useTransition();\n  const [routerState, setRouterState] = useState({pendingNav: () => {}, url: document.location.pathname});\n  function navigate(url) {\n    startTransition(() => {\n      // 전환 원인이 \"nav forward\"인 경우의 트랜지션 타입\n      addTransitionType('nav-forward');\n      go(url);\n    });\n  }\n  function navigateBack(url) {\n    startTransition(() => {\n      // 전환 원인이 \"nav backward\"인 경우의 트랜지션 타입\n      addTransitionType('nav-back');\n      go(url);\n    });\n  }\n\n  function go(url) {\n    setRouterState({\n      url,\n      pendingNav() {\n        window.history.pushState({}, \"\", url);\n      },\n    });\n  }\n\n  useEffect(() => {\n    function handlePopState() {\n      // 복원은 동기적으로 이루어져야 하므로 애니메이션되면 안 됩니다.\n      // 트랜지션이더라도 마찬가지입니다.\n      startTransition(() => {\n        setRouterState({\n          url: document.location.pathname + document.location.search,\n          pendingNav() {\n            // 아무 작업도 하지 않습니다. URL은 이미 업데이트되었습니다.\n          },\n        });\n      });\n    }\n    window.addEventListener(\"popstate\", handlePopState);\n    return () => {\n      window.removeEventListener(\"popstate\", handlePopState);\n    };\n  }, []);\n  const pendingNav = routerState.pendingNav;\n  useLayoutEffect(() => {\n    pendingNav();\n  }, [pendingNav]);\n\n  return (\n    <RouterContext\n      value={{\n        url: routerState.url,\n        navigate,\n        navigateBack,\n        isPending,\n        params: {},\n      }}\n    >\n      {children}\n    </RouterContext>\n  );\n}\n\nconst RouterContext = createContext({ url: \"/\", params: {} });\n\nexport function useRouter() {\n  return use(RouterContext);\n}\n\nexport function useIsNavPending() {\n  return use(RouterContext).isPending;\n}\n\n```\n\n```css src/styles.css hidden\n@font-face {\n  font-family: Optimistic Text;\n  src: url(https://react.dev/fonts/Optimistic_Text_W_Rg.woff2) format(\"woff2\");\n  font-weight: 400;\n  font-style: normal;\n  font-display: swap;\n}\n\n@font-face {\n  font-family: Optimistic Text;\n  src: url(https://react.dev/fonts/Optimistic_Text_W_Md.woff2) format(\"woff2\");\n  font-weight: 500;\n  font-style: normal;\n  font-display: swap;\n}\n\n@font-face {\n  font-family: Optimistic Text;\n  src: url(https://react.dev/fonts/Optimistic_Text_W_Bd.woff2) format(\"woff2\");\n  font-weight: 600;\n  font-style: normal;\n  font-display: swap;\n}\n\n@font-face {\n  font-family: Optimistic Text;\n  src: url(https://react.dev/fonts/Optimistic_Text_W_Bd.woff2) format(\"woff2\");\n  font-weight: 700;\n  font-style: normal;\n  font-display: swap;\n}\n\n* {\n  box-sizing: border-box;\n}\n\nhtml {\n  background-image: url(https://react.dev/images/meta-gradient-dark.png);\n  background-size: 100%;\n  background-position: -100%;\n  background-color: rgb(64 71 86);\n  background-repeat: no-repeat;\n  height: 100%;\n  width: 100%;\n}\n\nbody {\n  font-family: Optimistic Text, -apple-system, ui-sans-serif, system-ui, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;\n  padding: 10px 0 10px 0;\n  margin: 0;\n  display: flex;\n  justify-content: center;\n}\n\n#root {\n  flex: 1 1;\n  height: auto;\n  background-color: #fff;\n  border-radius: 10px;\n  max-width: 450px;\n  min-height: 600px;\n  padding-bottom: 10px;\n}\n\nh1 {\n  margin-top: 0;\n  font-size: 22px;\n}\n\nh2 {\n  margin-top: 0;\n  font-size: 20px;\n}\n\nh3 {\n  margin-top: 0;\n  font-size: 18px;\n}\n\nh4 {\n  margin-top: 0;\n  font-size: 16px;\n}\n\nh5 {\n  margin-top: 0;\n  font-size: 14px;\n}\n\nh6 {\n  margin-top: 0;\n  font-size: 12px;\n}\n\ncode {\n  font-size: 1.2em;\n}\n\nul {\n  padding-inline-start: 20px;\n}\n\n.sr-only {\n  position: absolute;\n  width: 1px;\n  height: 1px;\n  padding: 0;\n  margin: -1px;\n  overflow: hidden;\n  clip: rect(0, 0, 0, 0);\n  white-space: nowrap;\n  border-width: 0;\n}\n\n.absolute {\n  position: absolute;\n}\n\n.overflow-visible {\n  overflow: visible;\n}\n\n.visible {\n  overflow: visible;\n}\n\n.fit {\n  width: fit-content;\n}\n\n\n/* Layout */\n.page {\n  display: flex;\n  flex-direction: column;\n  height: 100%;\n}\n\n.top-hero {\n  height: 200px;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  background-image: conic-gradient(\n      from 90deg at -10% 100%,\n      #2b303b 0deg,\n      #2b303b 90deg,\n      #16181d 1turn\n  );\n}\n\n.bottom {\n  flex: 1;\n  overflow: auto;\n}\n\n.top-nav {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  margin-bottom: 0;\n  padding: 0 12px;\n  top: 0;\n  width: 100%;\n  height: 44px;\n  color: #23272f;\n  font-weight: 700;\n  font-size: 20px;\n  z-index: 100;\n  cursor: default;\n}\n\n.content {\n  padding: 0 12px;\n  margin-top: 4px;\n}\n\n\n.loader {\n  color: #23272f;\n  font-size: 3px;\n  width: 1em;\n  margin-right: 18px;\n  height: 1em;\n  border-radius: 50%;\n  position: relative;\n  text-indent: -9999em;\n  animation: loading-spinner 1.3s infinite linear;\n  animation-delay: 200ms;\n  transform: translateZ(0);\n}\n\n@keyframes loading-spinner {\n  0%,\n  100% {\n    box-shadow: 0 -3em 0 0.2em,\n    2em -2em 0 0em, 3em 0 0 -1em,\n    2em 2em 0 -1em, 0 3em 0 -1em,\n    -2em 2em 0 -1em, -3em 0 0 -1em,\n    -2em -2em 0 0;\n  }\n  12.5% {\n    box-shadow: 0 -3em 0 0, 2em -2em 0 0.2em,\n    3em 0 0 0, 2em 2em 0 -1em, 0 3em 0 -1em,\n    -2em 2em 0 -1em, -3em 0 0 -1em,\n    -2em -2em 0 -1em;\n  }\n  25% {\n    box-shadow: 0 -3em 0 -0.5em,\n    2em -2em 0 0, 3em 0 0 0.2em,\n    2em 2em 0 0, 0 3em 0 -1em,\n    -2em 2em 0 -1em, -3em 0 0 -1em,\n    -2em -2em 0 -1em;\n  }\n  37.5% {\n    box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em,\n    3em 0em 0 0, 2em 2em 0 0.2em, 0 3em 0 0em,\n    -2em 2em 0 -1em, -3em 0em 0 -1em, -2em -2em 0 -1em;\n  }\n  50% {\n    box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em,\n    3em 0 0 -1em, 2em 2em 0 0em, 0 3em 0 0.2em,\n    -2em 2em 0 0, -3em 0em 0 -1em, -2em -2em 0 -1em;\n  }\n  62.5% {\n    box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em,\n    3em 0 0 -1em, 2em 2em 0 -1em, 0 3em 0 0,\n    -2em 2em 0 0.2em, -3em 0 0 0, -2em -2em 0 -1em;\n  }\n  75% {\n    box-shadow: 0em -3em 0 -1em, 2em -2em 0 -1em,\n    3em 0em 0 -1em, 2em 2em 0 -1em, 0 3em 0 -1em,\n    -2em 2em 0 0, -3em 0em 0 0.2em, -2em -2em 0 0;\n  }\n  87.5% {\n    box-shadow: 0em -3em 0 0, 2em -2em 0 -1em,\n    3em 0 0 -1em, 2em 2em 0 -1em, 0 3em 0 -1em,\n    -2em 2em 0 0, -3em 0em 0 0, -2em -2em 0 0.2em;\n  }\n}\n\n/* LikeButton */\n.like-button {\n  outline-offset: 2px;\n  position: relative;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  width: 2.5rem;\n  height: 2.5rem;\n  cursor: pointer;\n  border-radius: 9999px;\n  border: none;\n  outline: none 2px;\n  color: #5e687e;\n  background: none;\n}\n\n.like-button:focus {\n  color: #a6423a;\n  background-color: rgba(166, 66, 58, .05);\n}\n\n.like-button:active {\n  color: #a6423a;\n  background-color: rgba(166, 66, 58, .05);\n  transform: scaleX(0.95) scaleY(0.95);\n}\n\n.like-button:hover {\n  background-color: #f6f7f9;\n}\n\n.like-button.liked {\n  color: #a6423a;\n}\n\n/* Icons */\n@keyframes circle {\n  0% {\n    transform: scale(0);\n    stroke-width: 16px;\n  }\n\n  50% {\n    transform: scale(.5);\n    stroke-width: 16px;\n  }\n\n  to {\n    transform: scale(1);\n    stroke-width: 0;\n  }\n}\n\n.circle {\n  color: rgba(166, 66, 58, .5);\n  transform-origin: center;\n  transition-property: all;\n  transition-duration: .15s;\n  transition-timing-function: cubic-bezier(.4,0,.2,1);\n}\n\n.circle.liked.animate {\n  animation: circle .3s forwards;\n}\n\n.heart {\n  width: 1.5rem;\n  height: 1.5rem;\n}\n\n.heart.liked {\n  transform-origin: center;\n  transition-property: all;\n  transition-duration: .15s;\n  transition-timing-function: cubic-bezier(.4, 0, .2, 1);\n}\n\n.heart.liked.animate {\n  animation: scale .35s ease-in-out forwards;\n}\n\n.control-icon {\n  color: hsla(0, 0%, 100%, .5);\n  filter:  drop-shadow(0 20px 13px rgba(0, 0, 0, .03)) drop-shadow(0 8px 5px rgba(0, 0, 0, .08));\n}\n\n.chevron-left {\n  margin-top: 2px;\n  rotate: 90deg;\n}\n\n\n/* Video */\n.thumbnail {\n  position: relative;\n  aspect-ratio: 16 / 9;\n  display: flex;\n  overflow: hidden;\n  flex-direction: column;\n  justify-content: center;\n  align-items: center;\n  border-radius: 0.5rem;\n  outline-offset: 2px;\n  width: 8rem;\n  vertical-align: middle;\n  background-color: #ffffff;\n  background-size: cover;\n  user-select: none;\n}\n\n.thumbnail.blue {\n  background-image: conic-gradient(at top right, #c76a15, #087ea4, #2b3491);\n}\n\n.thumbnail.red {\n  background-image: conic-gradient(at top right, #c76a15, #a6423a, #2b3491);\n}\n\n.thumbnail.green {\n  background-image: conic-gradient(at top right, #c76a15, #388f7f, #2b3491);\n}\n\n.thumbnail.purple {\n  background-image: conic-gradient(at top right, #c76a15, #575fb7, #2b3491);\n}\n\n.thumbnail.yellow {\n  background-image: conic-gradient(at top right, #c76a15, #FABD62, #2b3491);\n}\n\n.thumbnail.gray {\n  background-image: conic-gradient(at top right, #c76a15, #4E5769, #2b3491);\n}\n\n.video {\n  display: flex;\n  flex-direction: row;\n  gap: 0.75rem;\n  align-items: center;\n}\n\n.video .link {\n  display: flex;\n  flex-direction: row;\n  flex: 1 1 0;\n  gap: 0.125rem;\n  outline-offset: 4px;\n  cursor: pointer;\n}\n\n.video .info {\n  display: flex;\n  flex-direction: column;\n  justify-content: center;\n  margin-left: 8px;\n  gap: 0.125rem;\n}\n\n.video .info:hover {\n  text-decoration: underline;\n}\n\n.video-title {\n  font-size: 15px;\n  line-height: 1.25;\n  font-weight: 700;\n  color: #23272f;\n}\n\n.video-description {\n  color: #5e687e;\n  font-size: 13px;\n}\n\n/* Details */\n.details .thumbnail {\n  position: relative;\n  aspect-ratio: 16 / 9;\n  display: flex;\n  overflow: hidden;\n  flex-direction: column;\n  justify-content: center;\n  align-items: center;\n  border-radius: 0.5rem;\n  outline-offset: 2px;\n  width: 100%;\n  vertical-align: middle;\n  background-color: #ffffff;\n  background-size: cover;\n  user-select: none;\n}\n\n.video-details-title {\n  margin-top: 8px;\n}\n\n.video-details-speaker {\n  display: flex;\n  gap: 8px;\n  margin-top: 10px\n}\n\n.back {\n  display: flex;\n  align-items: center;\n  margin-left: -5px;\n  cursor: pointer;\n}\n\n.back:hover {\n  text-decoration: underline;\n}\n\n.info-title {\n  font-size: 1.5rem;\n  font-weight: 700;\n  line-height: 1.25;\n  margin: 8px 0 0 0 ;\n}\n\n.info-description {\n  margin: 8px 0 0 0;\n}\n\n.controls {\n  cursor: pointer;\n}\n\n.fallback {\n  background: #f6f7f8 linear-gradient(to right, #e6e6e6 5%, #cccccc 25%, #e6e6e6 35%) no-repeat;\n  background-size: 800px 104px;\n  display: block;\n  line-height: 1.25;\n  margin: 8px 0 0 0;\n  border-radius: 5px;\n  overflow: hidden;\n\n  animation: 1s linear 1s infinite shimmer;\n  animation-delay: 300ms;\n  animation-duration: 1s;\n  animation-fill-mode: forwards;\n  animation-iteration-count: infinite;\n  animation-name: shimmer;\n  animation-timing-function: linear;\n}\n\n\n.fallback.title {\n  width: 130px;\n  height: 30px;\n\n}\n\n.fallback.description {\n  width: 150px;\n  height: 21px;\n}\n\n@keyframes shimmer {\n  0% {\n    background-position: -468px 0;\n  }\n\n  100% {\n    background-position: 468px 0;\n  }\n}\n\n.search {\n  margin-bottom: 10px;\n}\n.search-input {\n  width: 100%;\n  position: relative;\n}\n\n.search-icon {\n  position: absolute;\n  top: 0;\n  bottom: 0;\n  inset-inline-start: 0;\n  display: flex;\n  align-items: center;\n  padding-inline-start: 1rem;\n  pointer-events: none;\n  color: #99a1b3;\n}\n\n.search-input input {\n  display: flex;\n  padding-inline-start: 2.75rem;\n  padding-top: 10px;\n  padding-bottom: 10px;\n  width: 100%;\n  text-align: start;\n  background-color: rgb(235 236 240);\n  outline: 2px solid transparent;\n  cursor: pointer;\n  border: none;\n  align-items: center;\n  color: rgb(35 39 47);\n  border-radius: 9999px;\n  vertical-align: middle;\n  font-size: 15px;\n}\n\n.search-input input:hover, .search-input input:active {\n  background-color: rgb(235 236 240/ 0.8);\n  color: rgb(35 39 47/ 0.8);\n}\n\n/* Home */\n.video-list {\n  position: relative;\n}\n\n.video-list .videos {\n  display: flex;\n  flex-direction: column;\n  gap: 1rem;\n  overflow-y: auto;\n  height: 100%;\n}\n```\n\n\n```css src/animations.css\n/* 추가 애니메이션은 필요하지 않습니다 */\n\n\n\n\n\n\n\n\n\n/* 이전에 정의된 애니메이션은 아래에 있습니다 */\n\n\n\n\n\n\n/* Suspense 폴백을 아래로 슬라이드하는 애니메이션 */\n::view-transition-old(.slide-down) {\n    animation: 150ms ease-out both fade-out, 150ms ease-out both slide-down;\n}\n\n::view-transition-new(.slide-up) {\n    animation: 210ms ease-in 150ms both fade-in, 400ms ease-in both slide-up;\n}\n\n/* 트랜지션 타입에 의해 추가된 View Transition 클래스용 애니메이션 */\n::view-transition-old(.slide-forward) {\n    /* 앞으로 전환할 때 \"old\" 페이지는 왼쪽으로 슬라이드되어 나가야 합니다. */\n    animation: 150ms cubic-bezier(0.4, 0, 1, 1) both fade-out,\n    400ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;\n}\n\n::view-transition-new(.slide-forward) {\n    /* 앞으로 전환할 때 \"new\" 페이지는 오른쪽에서 슬라이드되어 들어와야 합니다. */\n    animation: 210ms cubic-bezier(0, 0, 0.2, 1) 150ms both fade-in,\n    400ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;\n}\n\n::view-transition-old(.slide-back) {\n    /* 뒤로 전환할 때 \"old\" 페이지는 오른쪽으로 슬라이드되어 나가야 합니다. */\n    animation: 150ms cubic-bezier(0.4, 0, 1, 1) both fade-out,\n    400ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-right;\n}\n\n::view-transition-new(.slide-back) {\n    /* 뒤로 전환할 때 \"new\" 페이지는 왼쪽에서 슬라이드되어 들어와야 합니다. */\n    animation: 210ms cubic-bezier(0, 0, 0.2, 1) 150ms both fade-in,\n    400ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-left;\n}\n\n/* 위 애니메이션을 지원하기 위한 키프레임 */\n@keyframes slide-up {\n    from {\n        transform: translateY(10px);\n    }\n    to {\n        transform: translateY(0);\n    }\n}\n\n@keyframes slide-down {\n    from {\n        transform: translateY(0);\n    }\n    to {\n        transform: translateY(10px);\n    }\n}\n\n@keyframes fade-in {\n    from {\n        opacity: 0;\n    }\n}\n\n@keyframes fade-out {\n    to {\n        opacity: 0;\n    }\n}\n\n@keyframes slide-to-right {\n    to {\n        transform: translateX(50px);\n    }\n}\n\n@keyframes slide-from-right {\n    from {\n        transform: translateX(50px);\n    }\n    to {\n        transform: translateX(0);\n    }\n}\n\n@keyframes slide-to-left {\n    to {\n        transform: translateX(-50px);\n    }\n}\n\n@keyframes slide-from-left {\n    from {\n        transform: translateX(-50px);\n    }\n    to {\n        transform: translateX(0);\n    }\n}\n\n/* Default .slow-fade. */\n::view-transition-old(.slow-fade) {\n    animation-duration: 500ms;\n}\n\n::view-transition-new(.slow-fade) {\n    animation-duration: 500ms;\n}\n```\n\n```js src/index.js hidden\nimport React, {StrictMode} from 'react';\nimport {createRoot} from 'react-dom/client';\nimport './styles.css';\nimport './animations.css';\n\nimport App from './App';\nimport {Router} from './router';\n\nconst root = createRoot(document.getElementById('root'));\nroot.render(\n  <StrictMode>\n    <Router>\n      <App />\n    </Router>\n  </StrictMode>\n);\n```\n\n```json package.json hidden\n{\n  \"dependencies\": {\n    \"react\": \"canary\",\n    \"react-dom\": \"canary\",\n    \"react-scripts\": \"latest\"\n  },\n  \"scripts\": {\n    \"start\": \"react-scripts start\",\n    \"build\": \"react-scripts build\",\n    \"test\": \"react-scripts test --env=jsdom\",\n    \"eject\": \"react-scripts eject\"\n  }\n}\n```\n\n</Sandpack>\n\n### Activity를 사용한 서버 사이드 렌더링 {/*server-side-rendering-with-activity*/}\n\n서버 사이드 렌더링(SSR)을 사용하는 페이지에서 Activity를 사용하는 경우 추가적인 최적화 과정이 있습니다.\n\n페이지의 일부가 `mode=\"hidden\"`으로 렌더링되는 경우, 해당 부분은 SSR 응답에 포함되지 않습니다. 대신, React는 페이지의 나머지 부분이 하이드레이션되는 동안 Activity 내부의 콘텐츠에 대한 클라이언트 렌더링을 예약하여 화면에 표시되는 콘텐츠의 우선순위를 정합니다.\n\n`mode=\"visible\"`으로 렌더링된 UI의 일부의 경우, React는 Suspense 콘텐츠가 낮은 우선순위로 하이드레이션되는 것과 유사하게 활동 내 콘텐츠의 하이드레이션 우선순위를 낮춥니다. 사용자가 페이지와 상호작용하는 경우, 필요한 경우 경계 내에서 하이드레이션의 우선순위를 지정합니다.\n\n이는 고급 사용 사례이지만 Activity에서 고려되는 추가적인 이점을 보여줍니다.\n\n### 향후 Activity의 모드 {/*future-modes-for-activity*/}\n\n향후 Activity에 더 많은 모드를 추가할 수 있습니다.\n\n예를 들어 일반적인 사용 사례는 \"활성화된\" 모달 뷰 뒤에 이전의 \"비활성된\" 페이지가 표시되는 모달을 렌더링하는 것입니다. 이 사용 사례에서는 \"hidden\" 모드가 표시되지 않고 SSR에 포함되지 않기 때문에 작동하지 않습니다.\n\n대신 콘텐츠를 계속 표시하고 &mdash;SSR에 포함하되&mdash; 마운트되지 않은 상태로 유지하고 업데이트 우선순위를 해제하는 새로운 모드를 고려하고 있습니다. 이 모드는 모달이 열려 있는 동안 백그라운드 콘텐츠가 업데이트되는 것을 보는 것이 방해가 될 수 있으므로, DOM 업데이트를 \"일시 중지\"해야 할 수도 있습니다.\n\nActivity에서 고려 중인 또 다른 모드는 메모리가 너무 많이 사용되는 경우 숨겨진 활동의 상태를 자동으로 삭제하는 기능입니다. 컴포넌트가 이미 마운트 해제된 상태이므로 너무 많은 리소스를 소모하기보다는 앱에서 가장 최근에 사용된 숨겨진 부분의 상태를 파기하는 것이 더 바람직할 수 있습니다.\n\n이 부분은 아직 연구 중인 부분이며, 진전이 있으면 더 많은 내용을 공유해드리겠습니다. 오늘 포함된 Activity에 대한 자세한 내용은 [문서를 참조하세요](/reference/react/Activity).\n\n---\n\n# 개발 중인 기능 {/*features-in-development*/}\n\n또한 아래와 같은 일반적인 문제를 해결하기 위한 기능들도 개발 중입니다.\n\n가능한 해결책을 반복적으로 검토하는 과정에서, 현재 병합 중인 PR을 통해 실험 중인 잠재적 API가 일부 공유될 수 있습니다. 다양한 아이디어를 시도하는 과정에서, 실험 결과에 따라 일부 해결책은 변경되거나 제거되기도 합니다.\n\n아직 충분히 검증되지 않은 해결책이 너무 이르게 공유되면, 커뮤니티에 혼란과 불필요한 변동을 초래할 수 있습니다. 투명성과 혼란 최소화 사이의 균형을 위해, 현재는 구체적인 해결책을 공개하지 않고 우리가 해결하려는 문제 자체만 공유하고 있습니다.\n\n이 기능들이 더 진전되면, 문서와 함께 블로그를 통해 공개하여 직접 사용해 볼 수 있도록 안내할 예정입니다.\n\n## React Performance Tracks {/*react-performance-tracks*/}\n\nReact 앱의 성능에 대한 더 많은 정보를 제공하기 위해 [커스텀 트랙 추가를 허용하는](https://developer.chrome.com/docs/devtools/performance/extension) 브라우저 API를 사용하여 성능 프로파일러에 새로운 커스텀 트랙 세트를 작업하고 있습니다.\n\n이 기능은 아직 진행 중이므로, 실험적 기능으로 완전히 출시하기 위한 문서를 발행할 준비가 되지 않았습니다. React의 실험적 버전을 사용하면 미리 보기를 할 수 있으며, 이는 자동으로 프로필에 성능 트랙을 추가합니다.\n\n<div style={{display: 'flex', justifyContent: 'center', marginBottom: '1rem'}}>\n  <picture >\n      <source srcset=\"/images/blog/react-labs-april-2025/perf_tracks.png\" />\n      <img className=\"w-full light-image\" src=\"/images/blog/react-labs-april-2025/perf_tracks.webp\" />\n  </picture>\n  <picture >\n      <source srcset=\"/images/blog/react-labs-april-2025/perf_tracks_dark.png\" />\n      <img className=\"w-full dark-image\" src=\"/images/blog/react-labs-april-2025/perf_tracks_dark.webp\" />\n  </picture>\n</div>\n\n성능과 스케줄러 트랙이 일시 중단된 트리에서 작업을 항상 \"연결\"하지 않는 등 해결할 계획인 몇 가지 알려진 문제들이 있어서, 아직 시도할 준비가 완전히 되지 않았습니다. 또한 트랙의 디자인과 사용성을 개선하기 위해 얼리 어답터들로부터 피드백을 계속 수집하고 있습니다.\n\n이러한 문제들을 해결하면, 실험적 문서를 발행하고 시도할 준비가 되었다고 공유하겠습니다.\n\n---\n\n## Automatic Effect Dependencies {/*automatic-effect-dependencies*/}\n\nhooks를 출시했을 때, 저희는 세 가지 동기가 있었습니다:\n\n- **컴포넌트 간 코드 공유**: hooks는 렌더링 props와 고차 컴포넌트 같은 패턴을 대체하여 컴포넌트 계층을 변경하지 않고도 상태가 있는 로직을 재사용할 수 있게 해주었습니다.\n- **생명주기가 아닌 함수의 관점에서 사고**: hooks는 생명주기 메서드를 기반으로 한 분할을 강제하는 것이 아니라 관련된 부분(구독 설정이나 데이터 가져오기 등)을 기반으로 하나의 컴포넌트를 더 작은 함수로 분할할 수 있게 해주었습니다.\n- **사전 컴파일 지원**: hooks는 생명주기 메서드로 인한 의도하지 않은 최적화 해제 문제와 클래스의 제약사항을 줄이면서 사전 컴파일을 지원하도록 설계되었습니다.\n\n출시 이후 hooks는 *컴포넌트 간 코드 공유* 측면에서 성공을 거두었습니다. 현재 hooks는 컴포넌트 간 로직을 공유하는 데 선호되는 방식이며, 렌더링 props와 고차 컴포넌트를 사용할 필요가 있는 경우도 줄어들었습니다. 또한 hooks는 클래스 컴포넌트로는 가능하지 않았던 Fast Refresh와 같은 기능을 지원하는 데에도 성공했습니다.\n\n### Effects는 어려울 수 있습니다 {/*effects-can-be-hard*/}\n\n안타깝게도, 일부 hooks는 여전히 생명주기 대신 함수의 관점에서 사고하기 어렵습니다. 특히 Effects는 여전히 이해하기 어렵고 개발자들로부터 듣는 가장 일반적인 고민거리입니다. 작년에 저희는 Effects가 어떻게 사용되는지, 그리고 이러한 사용 사례가 어떻게 단순화되고 이해하기 쉬워질 수 있는지에 대해 상당한 시간을 연구했습니다.\n\n종종 혼란은 필요하지 않을 때 Effect를 사용하는 데서 온다는 것을 발견했습니다. [You Might Not Need an Effect](/learn/you-might-not-need-an-effect) 가이드는 Effects가 올바른 솔루션이 아닌 경우들을 많이 다루고 있습니다. 하지만 Effect가 문제에 적합한 해결책일 때조차도, Effects는 클래스 컴포넌트 생명주기보다 여전히 이해하기 어려울 수 있습니다.\n\n혼란의 이유 중 하나는 개발자들이 _Effects_ 관점(Effect가 무엇을 하는지)이 아니라 _컴포넌트의_ 관점(생명주기 같은)에서 Effects를 생각하기 때문이라고 생각합니다.\n\n[문서의 예시](/learn/lifecycle-of-reactive-effects#thinking-from-the-effects-perspective)를 살펴보겠습니다:\n\n```js\nuseEffect(() => {\n  // roomId로 지정된 방에 연결된 effect...\n  const connection = createConnection(serverUrl, roomId);\n  connection.connect();\n  return () => {\n    // ...연결이 끊어질 때까지\n    connection.disconnect();\n  };\n}, [roomId]);\n```\n\n많은 사용자들은 이 코드를 \"마운트 시에 roomId에 연결하고, `roomId`가 변경될 때마다 이전 방으로부터 연결을 해제하고 새로운 연결을 생성한다\"고 읽을 것입니다. 하지만 이는 컴포넌트의 생명주기 관점에서 생각하는 것이며, 이는 Effect를 올바르게 작성하기 위해 모든 컴포넌트 생명주기 상태를 생각해야 한다는 의미입니다. 이는 어려울 수 있으므로, 컴포넌트 관점을 사용할 때 Effects가 클래스 생명주기보다 어려워 보이는 것이 이해할 만합니다.\n\n### 의존성 없는 Effects {/*effects-without-dependencies*/}\n\n대신 Effect의 관점에서 생각하는 것이 좋습니다. Effect는 컴포넌트 생명주기에 대해 알지 못합니다. 단지 동기화를 시작하는 방법과 중지하는 방법만 설명합니다. 사용자가 이런 식으로 Effects를 생각할 때, 그들의 Effects는 작성하기 더 쉬워지고, 필요한 만큼 여러 번 시작되고 중지되는 것에 더 탄력적이 됩니다.\n\nEffects가 컴포넌트 관점에서 생각되는 이유를 연구하는 데 시간을 보냈고, 그 이유 중 하나가 의존성 배열이라고 생각합니다. 작성해야 하므로, 바로 거기에 있고 여러분이 무엇에 \"반응\"하고 있는지를 상기시키며 '이 값들이 변경될 때 이것을 하라'는 멘탈 모델로 유도합니다.\n\nhooks를 출시할 때, 사전 컴파일로 사용하기 더 쉽게 만들 수 있다는 것을 알고 있었습니다. React 컴파일러를 사용하면, 이제 대부분의 경우 `useCallback`과 `useMemo`를 직접 작성하는 것을 피할 수 있습니다. Effects의 경우, 컴파일러가 의존성을 자동으로 삽입할 수 있습니다:\n\n```js\nuseEffect(() => {\n  const connection = createConnection(serverUrl, roomId);\n  connection.connect();\n  return () => {\n    connection.disconnect();\n  };\n}); // 컴파일러가 삽입한 의존성\n```\n\n이 코드에서는 React 컴파일러가 의존성을 자동으로 추론해 삽입하므로, 개발자가 직접 보거나 작성할 필요가 없습니다. [IDE 확장](#compiler-ide-extension)이나 [`useEffectEvent`](/reference/react/useEffectEvent) 같은 기능을 사용하면, 디버깅이 필요하거나 의존성을 제거해 최적화해야 할 때 컴파일러가 무엇을 삽입했는지 보여주는 CodeLens를 제공할 수 있습니다. 이는 컴포넌트나 Hook의 상태를 다른 무언가와 동기화하기 위해 언제든 실행될 수 있는 Effect를 작성할 때, 올바른 사고 모델을 강화하는 데 도움이 됩니다.\n\n저희의 바람은 의존성을 자동으로 삽입하는 방식이 작성하기 쉬울 뿐만 아니라, 컴포넌트 생명주기가 아니라 Effect가 무엇을 하는지에 집중하도록 강제함으로써 이해하기도 더 쉬워지는 것입니다.\n\n---\n\n## Compiler IDE Extension {/*compiler-ide-extension*/}\n\n2025년 말, 저희는 React 컴파일러의 첫 번째 안정화 릴리스를 [공유했으며](/blog/2025/10/07/react-compiler-1), 이후에도 더 많은 개선 사항을 제공하기 위해 지속적으로 투자하고 있습니다.\n\n또한 React 컴파일러를 사용해서 코드 이해와 디버깅을 향상시킬 수 있는 정보를 제공하는 방법을 탐구하기 시작했습니다. 저희가 탐구하기 시작한 아이디어 중 하나는 [Lauren Tan의 React Conf 발표](https://conf2024.react.dev/talks/5)에서 사용된 확장 프로그램과 유사한, React 컴파일러를 기반으로 하는 새로운 실험적 LSP 기반 React IDE 확장 프로그램입니다.\n\n저희의 아이디어는 컴파일러의 정적 분석을 사용해서 IDE에서 직접 더 많은 정보, 제안, 최적화 기회를 제공할 수 있다는 것입니다. 예를 들어, React의 규칙을 위반하는 코드에 대한 진단을 제공하거나, 컴포넌트와 hooks가 컴파일러에 의해 최적화되었는지 보여주는 호버, 또는 [자동으로 삽입된 Effect 의존성](#automatic-effect-dependencies)을 볼 수 있는 CodeLens를 제공할 수 있습니다.\n\nIDE 확장 프로그램은 아직 초기 탐구 단계이지만, 향후 업데이트에서 진행 상황을 공유하겠습니다.\n\n---\n\n## Fragment Refs {/*fragment-refs*/}\n\n이벤트 관리, 위치 지정, 포커스를 위한 DOM API들은 React로 작성할 때 구성하기 어렵습니다. 이는 종종 개발자들이 Effects에 의존하거나, 여러 Refs를 관리하거나, `findDOMNode`(React 19에서 제거됨)와 같은 API를 사용하게 만듭니다.\n\n저희는 단일 엘리먼트가 아닌 DOM 엘리먼트 그룹을 가리키는 Fragments에 refs를 추가하는 것을 탐구하고 있습니다. 저희의 희망은 이것이 여러 자식을 관리하는 것을 단순화하고 DOM API를 호출할 때 구성 가능한 React 코드를 작성하기 더 쉽게 만드는 것입니다.\n\nFragment refs는 아직 연구 중입니다. 최종 API가 완성에 가까워지면 더 많은 내용을 공유하겠습니다.\n\n---\n\n## Gesture Animations {/*gesture-animations*/}\n\n또한 메뉴를 스와이프로 열거나 사진 캐러셀을 스크롤하는 것과 같은 제스처 애니메이션을 지원하도록 View Transitions를 확장하는 방법도 연구하고 있습니다.\n\n제스처는 몇 가지 이유로 새로운 도전을 제시합니다:\n\n- **제스처는 연속적입니다**: 스와이프하는 동안 애니메이션은 트리거되어 끝까지 실행되는 것이 아니라, 손가락 위치와 시간에 직접적으로 연결됩니다.\n- **제스처는 항상 완료되지 않습니다**: 손가락을 놓았을 때, 제스처 애니메이션은 끝까지 실행될 수도 있고, 이동한 거리와 정도에 따라(예: 메뉴를 부분적으로만 연 경우) 원래 상태로 되돌아갈 수도 있습니다.\n- **제스처는 old와 new를 뒤집습니다**: 애니메이션이 진행되는 동안에는 출발 지점이 되는 페이지가 계속 \"살아 있는\" 상태로 상호작용 가능해야 합니다. 이는 \"old\" 상태가 스냅샷이고 \"new\" 상태가 실제 DOM인 브라우저의 View Transition 모델과는 반대입니다.\n\n저희는 잘 동작하는 접근 방식을 찾았다고 생각하며, 제스처 전환을 트리거하기 위한 새로운 API를 도입할 수도 있습니다. 다만 현재는 `<ViewTransition>`을 제공하는 데 집중하고 있으며, 제스처 지원은 그 이후에 다시 다룰 예정입니다.\n\n---\n\n## Concurrent Stores {/*concurrent-stores*/}\n\n동시 렌더링을 포함한 React 18을 출시하면서, React 상태나 context를 사용하지 않는 외부 스토어 라이브러리도 스토어 업데이트 시 동기 렌더링을 강제함으로써 [동시 렌더링을 지원](https://github.com/reactwg/react-18/discussions/70)할 수 있도록 `useSyncExternalStore`를 함께 출시했습니다.\n\n다만 `useSyncExternalStore`를 사용하면 비용이 따릅니다. 트랜지션과 같은 동시성 기능에서 빠져나오게 되고, 기존 콘텐츠에 Suspense 폴백이 표시되도록 강제하기 때문입니다.\n\n이제 React 19가 출시됨에 따라, `use` API로 동시 외부 스토어를 완전히 지원하기 위한 기본 요소를 만들기 위해 이 문제 영역을 다시 살펴보고 있습니다:\n\n```js\nconst value = use(store);\n```\n\n목표는 외부 상태를 렌더링 중에 찢김(tearing) 없이 읽을 수 있도록 하고, React가 제공하는 모든 동시성 기능과 자연스럽게 동작하게 하는 것입니다.\n\n이 연구는 아직 초기 단계입니다. 더 진행되면 새로운 API의 형태와 함께 추가 내용을 공유하겠습니다.\n\n---\n\n_이 게시물을 검토해 준 [Aurora Scharff](https://bsky.app/profile/aurorascharff.no), [Dan Abramov](https://bsky.app/profile/danabra.mov), [Eli White](https://twitter.com/Eli_White), [Lauren Tan](https://bsky.app/profile/no.lol), [Luna Wei](https://github.com/lunaleaps), [Matt Carroll](https://twitter.com/mattcarrollcode), [Jack Pope](https://jackpope.me), [Jason Bonta](https://threads.net/someextent), [Jordan Brown](https://github.com/jbrown215), [Jordan Eldredge](https://bsky.app/profile/capt.dev), [Mofei Zhang](https://threads.net/z_mofei), [Sebastien Lorber](https://bsky.app/profile/sebastienlorber.com), [Sebastian Markbåge](https://bsky.app/profile/sebmarkbage.calyptus.eu), 및 [Tim Yung](https://github.com/yungsters)에게 감사드립니다._\n"
  },
  {
    "path": "src/content/blog/2025/10/01/react-19-2.md",
    "content": "---\ntitle: \"React 19.2\"\nauthor: React 팀\ndate: 2025/10/01\ndescription: React 19.2에는 Activity, React 성능 트랙, useEffectEvent 등의 새로운 기능이 추가되었습니다.\n---\n\n2025년 10월 1일, React 팀\n\n---\n\n<Intro>\n\nReact 19.2가 npm에 공개되었습니다!\n\n</Intro>\n\n이번 릴리즈는 지난 12월 React 19, 6월 React 19.1에 이어 지난 한 해 동안 세 번째 릴리즈입니다. 이 글에서는 React 19.2의 새로운 기능 개요와 주목할 만한 변경 사항을 중점적으로 다룰 예정입니다.\n\n<InlineToc />\n\n---\n\n## 새로운 React 기능 {/*new-react-features*/}\n\n### `<Activity />` {/*activity*/}\n\n`<Activity>`를 사용하면 앱을 제어하고 우선순위를 지정할 수 있는 \"활동\"으로 나눌 수 있습니다.\n\nActivity는 앱의 특정 부분을 조건부로 렌더링하는 대안으로 사용할 수 있습니다.\n\n```js\n// Before\n{isVisible && <Page />}\n\n// After\n<Activity mode={isVisible ? 'visible' : 'hidden'}>\n  <Page />\n</Activity>\n```\n\nReact 19.2에서 Activity는 `visible`과 `hidden` 두 가지 모드를 지원합니다.\n\n- `hidden`: 자식 요소를 숨기고, 이펙트를 마운트 해제하며, React가 더 이상 작업할 것이 없을 때까지 모든 업데이트를 지연시킵니다.\n- `visible`: 자식 요소를 표시하고, 이펙트를 마운트하며, 업데이트가 정상적으로 처리되도록 허용합니다.\n\n이는 앱의 숨겨진 부분을 화면에 보이는 어떤 것의 성능에도 영향을 미치지 않으면서 미리 렌더링하고 계속 렌더링할 수 있음을 의미합니다.\n\nActivity를 사용하면 사용자가 다음에 탐색할 가능성이 있는 앱의 숨겨진 부분을 렌더링하거나, 사용자가 떠난 부분의 상태를 저장할 수 있습니다. 이는 백그라운드에서 데이터, CSS, 이미지를 로드하여 탐색을 더 빠르게 하고, 뒤로 가기 탐색 시 입력 필드와 같은 상태를 유지하는 데 도움이 됩니다.\n\n앞으로는 다양한 사용 사례를 위해 Activity에 더 많은 모드를 추가할 계획입니다.\n\nActivity 사용 방법에 대한 예시는 [Activity 문서](/reference/react/Activity)를 참조하세요.\n\n---\n\n### `useEffectEvent` {/*use-effect-event*/}\n\n`useEffect`의 일반적인 패턴 중 하나는 외부 시스템으로부터 어떤 종류의 \"이벤트\"에 대해 앱 코드에 알리는 것입니다. 예를 들어, 채팅방이 연결될 때 알림을 표시하고 싶을 수 있습니다.\n\n```js {5,11}\nfunction ChatRoom({ roomId, theme }) {\n  useEffect(() => {\n    const connection = createConnection(serverUrl, roomId);\n    connection.on('connected', () => {\n      showNotification('Connected!', theme);\n    });\n    connection.connect();\n    return () => {\n      connection.disconnect()\n    };\n  }, [roomId, theme]);\n  // ...\n```\n\n위 코드의 문제는 이러한 \"이벤트\" 내에서 사용되는 값의 변경이 주변 Effect를 다시 실행하게 만든다는 것입니다. 예를 들어, `theme`을 변경하면 채팅방이 다시 연결됩니다. 이는 `roomId`처럼 Effect 로직 자체와 관련된 값에는 타당하지만, `theme`에는 타당하지 않습니다.\n\n이 문제를 해결하기 위해 대부분의 사용자는 린트 규칙을 비활성화하고 의존성을 제외합니다. 그러나 나중에 Effect를 업데이트해야 할 때 린터가 더 이상 의존성을 최신 상태로 유지하는 데 도움을 줄 수 없으므로 버그가 발생할 수 있습니다.\n\n`useEffectEvent`를 사용하면 이 로직의 \"이벤트\" 부분을 이를 내보내는 Effect에서 분리할 수 있습니다.\n\n```js {2,3,4,9}\nfunction ChatRoom({ roomId, theme }) {\n  const onConnected = useEffectEvent(() => {\n    showNotification('Connected!', theme);\n  });\n\n  useEffect(() => {\n    const connection = createConnection(serverUrl, roomId);\n    connection.on('connected', () => {\n      onConnected();\n    });\n    connection.connect();\n    return () => connection.disconnect();\n  }, [roomId]); // ✅ 모든 의존성이 선언됨 (Effect Events는 의존성이 아님)\n  // ...\n```\n\nDOM 이벤트와 유사하게, Effect 이벤트는 항상 최신 props와 상태를 \"봅니다\".\n\n**Effect 이벤트는 의존성 배열에 선언해서는 안 됩니다**. 린터가 이를 의존성으로 삽입하려고 하지 않도록 `eslint-plugin-react-hooks@latest`로 업그레이드해야 합니다. Effect 이벤트는 \"자신\"의 Effect와 동일한 컴포넌트 또는 Hook 내에서만 선언할 수 있습니다. 이러한 제약은 린터에 의해 검증됩니다.\n\n<Note>\n\n#### `useEffectEvent`를 사용해야 하는 경우 {/*when-to-use-useeffectevent*/}\n\n`useEffectEvent`는 사용자 이벤트 대신 Effect에서 발생(발화)하는 개념적으로 \"이벤트\"인 함수에 사용해야 합니다 (이것이 \"Effect 이벤트\"인 이유입니다). 모든 것을 `useEffectEvent`로 감쌀 필요는 없으며, 린트 오류를 숨기기 위해서만 사용해서는 안 됩니다. 이는 버그로 이어질 수 있습니다.\n\n이벤트 Effect를 생각하는 방법에 대한 자세한 내용은 [이벤트와 Effect 분리하기](/learn/separating-events-from-effects#extracting-non-reactive-logic-out-of-effects)를 참조하세요.\n\n</Note>\n\n---\n\n### `cacheSignal` {/*cache-signal*/}\n\n<RSC>\n\n`cacheSignal`은 [리액트 서버 컴포넌트](/reference/rsc/server-components)에서만 사용됩니다.\n\n</RSC>\n\n`cacheSignal`을 사용하면 [`cache()`](/reference/react/cache)의 수명이 끝났을 때 알 수 있습니다.\n\n```\nimport {cache, cacheSignal} from 'react';\nconst dedupedFetch = cache(fetch);\n\nasync function Component() {\n  await dedupedFetch(url, { signal: cacheSignal() });\n}\n```\n\n이를 통해 결과가 더 이상 캐시에서 사용되지 않을 때 작업을 정리하거나 중단할 수 있습니다. 예를 들어:\n\n- React가 렌더링을 성공적으로 완료한 경우\n- 렌더링이 중단된 경우\n- 렌더링이 실패한 경우\n\n자세한 내용은 [`cacheSignal` 문서](/reference/react/cacheSignal)를 참조하세요.\n\n---\n\n### 성능 트랙 {/*performance-tracks*/}\n\nReact 19.2는 Chrome DevTools 성능 프로필에 새로운 [커스텀 트랙](https://developer.chrome.com/docs/devtools/performance/extension) 세트를 추가하여 React 앱의 성능에 대한 더 많은 정보를 제공합니다.\n\n<div style={{display: 'flex', justifyContent: 'center', marginBottom: '1rem'}}>\n  <picture >\n      <source srcset=\"/images/blog/react-labs-april-2025/perf_tracks.png\" />\n      <img className=\"w-full light-image\" src=\"/images/blog/react-labs-april-2025/perf_tracks.webp\" />\n  </picture>\n  <picture >\n      <source srcset=\"/images/blog/react-labs-april-2025/perf_tracks_dark.png\" />\n      <img className=\"w-full dark-image\" src=\"/images/blog/react-labs-april-2025/perf_tracks_dark.webp\" />\n  </picture>\n</div>\n\n[React 성능 트랙 문서](/reference/dev-tools/react-performance-tracks)에 트랙에 포함된 모든 내용이 설명되어 있지만, 여기서는 개략적인 개요를 제공합니다.\n\n#### Scheduler ⚛ {/*scheduler-*/}\n\n스케줄러 트랙은 사용자 상호작용을 위한 \"블로킹(blocking)\" 또는 `startTransition` 내부 업데이트를 위한 \"트랜지션(transition)\"과 같이 React가 다양한 우선순위에 대해 작업하는 내용을 보여줍니다. 각 트랙 내부에서는 업데이트를 예약한 이벤트 및 해당 업데이트의 렌더링이 발생한 시점과 같이 수행되는 작업 유형을 볼 수 있습니다.\n\n또한 업데이트가 다른 우선순위를 기다리면서 차단되는 시점 또는 React가 계속하기 전에 페인트를 기다리는 시점과 같은 정보도 표시합니다. 스케줄러 트랙은 React가 코드를 다양한 우선순위로 분할하고 작업을 완료한 순서를 이해하는 데 도움이 됩니다.\n\n포함된 모든 내용을 보려면 [스케쥴러 트랙](/reference/dev-tools/react-performance-tracks#scheduler) 문서를 참조하세요.\n\n#### Components ⚛ {/*components-*/}\n\n컴포넌트 트랙은 React가 렌더링하거나 이펙트를 실행하기 위해 작업하는 컴포넌트 트리를 보여줍니다. 내부에서는 자식 요소가 마운트되거나 이펙트가 마운트될 때 \"마운트(Mount)\" 또는 React 외부 작업에 양보하여 렌더링이 차단될 때 \"차단(Blocked)\"과 같은 레이블을 볼 수 있습니다.\n\n컴포넌트 트랙은 컴포넌트가 렌더링되거나 이펙트를 실행하는 시점과 해당 작업을 완료하는 데 걸리는 시간을 이해하여 성능 문제를 식별하는 데 도움이 됩니다.\n\n포함된 모든 내용을 보려면 [컴포넌트 트랙](/reference/dev-tools/react-performance-tracks#components) 문서를 참조하세요.\n\n---\n\n## 새로운 React DOM 기능 {/*new-react-dom-features*/}\n\n### 부분 사전 렌더링 {/*partial-pre-rendering*/}\n\n19.2에서는 앱의 일부를 미리 렌더링하고 나중에 렌더링을 재개할 수 있는 새로운 기능을 추가했습니다.\n\n이 기능은 \"부분 사전 렌더링(Partial Pre-rendering)\"이라고 불리며, 앱의 정적 부분을 미리 렌더링하여 CDN에서 제공한 다음 셸(shell) 렌더링을 재개하여 나중에 동적 콘텐츠로 채울 수 있도록 합니다.\n\n앱을 나중에 재개하기 위해 미리 렌더링하려면 먼저 `AbortController`와 함께 `prerender`를 호출합니다:\n\n```\nconst {prelude, postponed} = await prerender(<App />, {\n  signal: controller.signal,\n});\n\n// 나중에 사용하기 위해 지연된(postponed) 상태를 저장합니다.\nawait savePostponedState(postponed);\n\n// prelude를 클라이언트 또는 CDN으로 전송합니다.\n```\n\n그런 다음, `prelude` 셸을 클라이언트에 반환하고, 나중에 `resume`을 호출하여 SSR 스트림으로 \"재개\"할 수 있습니다:\n\n```\nconst postponed = await getPostponedState(request);\nconst resumeStream = await resume(<App />, postponed);\n\n// 스트림을 클라이언트로 전송합니다.\n```\n\n또는 `resumeAndPrerender`를 호출하여 SSG를 위한 정적 HTML을 얻기 위해 재개할 수 있습니다:\n\n```\nconst postponedState = await getPostponedState(request);\nconst { prelude } = await resumeAndPrerender(<App />, postponedState);\n\n// 완성된 HTML prelude를 CDN으로 전송합니다.\n```\n\n자세한 내용은 새로운 API 문서에서 확인할 수 있습니다:\n- `react-dom/server`\n  - [`resume`](/reference/react-dom/server/resume): 웹 스트림용.\n  - [`resumeToPipeableStream`](/reference/react-dom/server/resumeToPipeableStream): Node 스트림용.\n- `react-dom/static`\n  - [`resumeAndPrerender`](/reference/react-dom/static/resumeAndPrerender): 웹 스트림용.\n  - [`resumeAndPrerenderToNodeStream`](/reference/react-dom/static/resumeAndPrerenderToNodeStream): Node 스트림용.\n\n또한, `prerender` API는 이제 `resume` API로 전달할 `postpone` 상태를 반환합니다.\n\n---\n\n## 주목할 만한 변경 사항 {/*notable-changes*/}\n\n### SSR을 위한 Suspense Boundary 배치 {/*batching-suspense-boundaries-for-ssr*/}\n\n클라이언트에서 렌더링될 때와 서버 사이드 렌더링에서 스트리밍될 때 Suspense Boundary가 다르게 나타나는 동작 버그를 수정했습니다.\n\n19.2부터 React는 서버 렌더링된 Suspense Boundary의 노출을 짧은 시간 동안 배치(batch)하여 더 많은 콘텐츠가 함께 노출되고 클라이언트 렌더링 동작과 일치하도록 합니다.\n\n<Diagram name=\"19_2_batching_before\" height={162} width={1270} alt=\"Diagram with three sections, with an arrow transitioning each section in between. The first section contains a page rectangle showing a glimmer loading state with faded bars. The second panel shows the top half of the page revealed and highlighted in blue. The third panel shows the entire the page revealed and highlighted in blue.\">\n\n이전에는 스트리밍 서버 사이드 렌더링 동안 Suspense 콘텐츠가 즉시 폴백을 대체했습니다.\n\n</Diagram>\n\n<Diagram name=\"19_2_batching_after\" height={162} width={1270} alt=\"Diagram with three sections, with an arrow transitioning each section in between. The first section contains a page rectangle showing a glimmer loading state with faded bars. The second panel shows the same page. The third panel shows the entire the page revealed and highlighted in blue.\">\n\nReact 19.2에서는 Suspense Boundary가 짧은 시간 동안 배치되어 더 많은 콘텐츠를 함께 노출할 수 있습니다.\n\n</Diagram>\n\n이 수정은 또한 SSR 중 Suspense에 대한 `<ViewTransition>` 지원을 위한 앱을 준비합니다. 더 많은 콘텐츠를 함께 노출함으로써 애니메이션이 더 큰 콘텐츠 배치에서 실행될 수 있으며, 가깝게 스트리밍되는 콘텐츠의 체인 애니메이션을 피할 수 있습니다.\n\n<Note>\n\nReact는 스로틀링이 핵심 웹 바이탈 및 검색 순위에 영향을 미치지 않도록 휴리스틱을 사용합니다.\n\n예를 들어, 총 페이지 로드 시간이 2.5초([LCP](https://web.dev/articles/lcp)에 대해 \"좋음\"으로 간주되는 시간)에 가까워지면 React는 배치를 중단하고 즉시 콘텐츠를 노출하여 스로틀링이 측정 항목을 놓치는 이유가 되지 않도록 합니다.\n\n</Note>\n\n---\n\n### SSR: Node용 웹 스트림 지원 {/*ssr-web-streams-support-for-node*/}\n\nReact 19.2는 Node.js에서 스트리밍 SSR을 위한 웹 스트림(Web Streams) 지원을 추가합니다:\n- [`renderToReadableStream`](/reference/react-dom/server/renderToReadableStream)은 이제 Node.js에서 사용할 수 있습니다\n- [`prerender`](/reference/react-dom/static/prerender)는 이제 Node.js에서 사용할 수 있습니다\n\n새로운 `resume` API도 마찬가지입니다:\n- [`resume`](/reference/react-dom/server/resume)은 Node.js에서 사용할 수 있습니다.\n- [`resumeAndPrerender`](/reference/react-dom/static/resumeAndPrerender)는 Node.js에서 사용할 수 있습니다.\n\n\n<Pitfall>\n\n#### Node.js에서 서버 사이드 렌더링에는 Node 스트림을 선호하세요 {/*prefer-node-streams-for-server-side-rendering-in-nodejs*/}\n\nNode.js 환경에서는 여전히 Node 스트림 API 사용을 강력히 권장합니다:\n\n- [`renderToPipeableStream`](/reference/react-dom/server/renderToPipeableStream)\n- [`resumeToPipeableStream`](/reference/react-dom/server/resumeToPipeableStream)\n- [`prerenderToNodeStream`](/reference/react-dom/static/prerenderToNodeStream)\n- [`resumeAndPrerenderToNodeStream`](/reference/react-dom/static/resumeAndPrerenderToNodeStream)\n\n이는 Node에서 Node 스트림이 웹 스트림보다 훨씬 빠르며, 웹 스트림은 기본적으로 압축을 지원하지 않아 사용자가 의도치 않게 스트리밍의 이점을 놓칠 수 있기 때문입니다.\n\n</Pitfall>\n\n---\n\n### `eslint-plugin-react-hooks` v6 {/*eslint-plugin-react-hooks*/}\n\n또한 `eslint-plugin-react-hooks@latest`를 공개했으며, `recommended` 프리셋에 기본적으로 플랫 config를 포함하고 새로운 React 컴파일러 기반 규칙을 옵트인(opt-in)할 수 있도록 했습니다.\n\n레거시 config를 계속 사용하려면 `recommended-legacy`로 변경할 수 있습니다:\n\n```diff\n- extends: ['plugin:react-hooks/recommended']\n+ extends: ['plugin:react-hooks/recommended-legacy']\n```\n\n컴파일러 활성화 규칙의 전체 목록은 [린터 문서](/reference/eslint-plugin-react-hooks#recommended)를 참조하세요.\n\n전체 변경 사항 목록은 [`eslint-plugin-react-hooks` 변경 로그](https://github.com/facebook/react/blob/main/packages/eslint-plugin-react-hooks/CHANGELOG.md#610)를 참조하세요.\n\n---\n\n### 기본 `useId` 접두사 업데이트 {/*update-the-default-useid-prefix*/}\n\n19.2에서는 기본 `useId` 접두사를 `:r:`(19.0.0) 또는 `«r»`(19.1.0)에서 `_r_`로 업데이트하고 있습니다.\n\nCSS 선택자에 유효하지 않은 특수 문자를 사용하는 원래 의도는 사용자가 작성한 ID와 충돌할 가능성이 낮다는 것이었습니다. 그러나 View Transition을 지원하려면 `useId`에 의해 생성된 ID가 `view-transition-name` 및 XML 1.0 이름에 유효한지 확인해야 합니다.\n\n---\n\n## 변경 로그 {/*changelog*/}\n\n기타 주목할 만한 변경 사항\n- `react-dom`: 호이스팅 가능한 스타일에서 nonce 사용 허용 [#32461](https://github.com/facebook/react/pull/32461)\n- `react-dom`: React 소유 노드가 텍스트 콘텐츠도 포함하는 경우 컨테이너로 사용하는 것에 대한 경고 [#32774](https://github.com/facebook/react/pull/32774)\n\n주목할 만한 버그 수정\n- `react`: Context를 \"SomeContext.Provider\" 대신 \"SomeContext\"로 문자열화 [#33507](https://github.com/facebook/react/pull/33507)\n- `react`: popstate 이벤트에서 useDeferredValue의 무한 루프 수정 [#32821](https://github.com/facebook/react/pull/32821)\n- `react`: useDeferredValue에 초기 값이 전달될 때의 버그 수정 [#34376](https://github.com/facebook/react/pull/34376)\n- `react`: 클라이언트 액션(Client Actions)으로 양식을 제출할 때의 충돌 수정 [#33055](https://github.com/facebook/react/pull/33055)\n- `react`: 다시 중단되는(resuspend) 탈수된(dehydrated) Suspense Boundary의 콘텐츠 숨기기/숨김 해제 [#32900](https://github.com/facebook/react/pull/32900)\n- `react`: 핫 리로드(Hot Reload) 중 넓은 트리에서 스택 오버플로우 방지 [#34145](https://github.com/facebook/react/pull/34145)\n- `react`: 다양한 위치에서 컴포넌트 스택 개선 [#33629](https://github.com/facebook/react/pull/33629), [#33724](https://github.com/facebook/react/pull/33724), [#32735](https://github.com/facebook/react/pull/32735), [#33723](https://github.com/facebook/react/pull/33723)\n- `react`: React.lazy로 지연된 컴포넌트 내 React.use 사용 시 버그 수정 [#33941](https://github.com/facebook/react/pull/33941)\n- `react-dom`: ARIA 1.3 속성 사용 시 경고 중단 [#34264](https://github.com/facebook/react/pull/34264)\n- `react-dom`: 깊게 중첩된 Suspense 폴백 내 Suspense 버그 수정 [#33467](https://github.com/facebook/react/pull/33467)\n- `react-dom`: 렌더링 중 중단 후 중단 시 행잉(hanging) 방지 [#34192](https://github.com/facebook/react/pull/34192)\n\n전체 변경 사항 목록은 [변경 로그](https://github.com/facebook/react/blob/main/CHANGELOG.md)를 참조하세요.\n\n\n---\n\n이 글을 작성해주신 [Ricky Hanlon](https://bsky.app/profile/ricky.fm), 그리고 이 글을 검토해주신 [Dan Abramov](https://bsky.app/profile/danabra.mov), [Matt Carroll](https://twitter.com/mattcarrollcode), [Jack Pope](https://jackpope.me), [Joe Savona](https://x.com/en_JS)에게 감사드립니다.\n"
  },
  {
    "path": "src/content/blog/2025/10/07/introducing-the-react-foundation.md",
    "content": "---\ntitle: \"React Foundation 소개\"\nauthor: Seth Webster, Matt Carroll, Joe Savona\ndate: 2025/10/07\ndescription: 오늘 React Foundation의 설립과 새로운 기술 거버넌스 구조를 발표합니다\n---\n\n2025년 10월 7일, [Seth Webster](https://x.com/sethwebster), [Matt Carroll](https://x.com/mattcarrollcode), [Joe Savona](https://x.com/en_JS), [Sophie Alpert](https://x.com/sophiebits)\n\n---\n\n\n<div style={{display: 'flex', justifyContent: 'center', marginBottom: '1rem', marginLeft: '7rem', marginRight: '7rem' }}>\n  <picture >\n      <source srcset=\"/images/blog/react-foundation/react_foundation_logo.png\" />\n      <img className=\"w-full light-image\" src=\"/images/blog/react-foundation/react_foundation_logo.webp\" />\n  </picture>\n  <picture >\n      <source srcset=\"/images/blog/react-foundation/react_foundation_logo_dark.png\" />\n      <img className=\"w-full dark-image\" src=\"/images/blog/react-foundation/react_foundation_logo_dark.webp\" />\n  </picture>\n</div>\n\n<Intro>\n\n오늘 우리는 React Foundation의 설립과 새로운 기술 거버넌스 구조에 대한 계획을 발표합니다.\n\n</Intro>\n\n---\n\n우리는 10년 전 React를 오픈소스로 공개하여 개발자들이 훌륭한 사용자 경험을 구축하는 데 도움을 주고자 했습니다. 초기부터 React는 Meta 외부로부터 상당한 기여를 받았습니다. 시간이 지남에 따라 기여자 수와 기여 범위가 크게 증가했습니다. Meta를 위해 만들어진 도구로 시작했지만, 이제는 여러 회사가 참여하고 생태계 전반에서 정기적인 기여가 이루어지는 프로젝트로 성장했습니다. React는 이제 어느 한 회사의 범위를 넘어섰습니다.\n\nReact 커뮤니티를 더 잘 지원하기 위해, 우리는 React와 React Native를 Meta에서 새로운 React Foundation으로 이전하는 계획을 발표합니다. 이 변화의 하나로 새로운 독립적인 기술 거버넌스 구조도 도입할 예정입니다. 이러한 변화가 React 생태계 프로젝트에 더 많은 자원을 제공할 수 있다고 믿습니다.\n\n## React Foundation (재단) {/*the-react-foundation*/}\n\n우리는 React Foundation을 React, React Native 및 JSX와 같은 일부 부수적인 프로젝트의 새로운 보금자리로 만들 것입니다. React Foundation의 임무는 React 커뮤니티와 생태계를 지원하는 것입니다. React Foundation은 다음을 수행합니다.\n\n* GitHub, CI, 상표 등의 React의 인프라 유지\n* React Conf 개최\n* 생태계 프로젝트에 대한 재정적 지원, 보조금 지급, 프로그램 생성 등 React 생태계를 지원하기 위한 계획 생성\n\nReact Foundation은 이사회가 운영하며 Seth Webster가 상임 이사를 맡게 됩니다. 이사회는 React의 개발, 커뮤니티 및 생태계를 지원하기 위해 자금과 리소스를 지휘할 것입니다. 이것이 React Foundation이 회사 중립적이며 커뮤니티의 이익을 최우선으로 반영할 수 있도록 보장하는 최선의 구조라고 믿습니다.\n\nReact Foundation의 창립 기업 멤버는 Amazon, Callstack, Expo, Meta, Microsoft, Software Mansion, Vercel입니다. 이 회사들은 React 및 React Native 생태계에 큰 영향을 미쳤으며 그들의 지원에 감사드립니다. 앞으로 더 많은 회원들을 맞이할 수 있기를 기대합니다.\n\n<div style={{display: 'flex', justifyContent: 'center', margin: '2rem'}}>\n  <picture >\n      <source srcset=\"/images/blog/react-foundation/react_foundation_member_logos.png\" />\n      <img className=\"w-full light-image\" src=\"/images/blog/react-foundation/react_foundation_member_logos.webp\" />\n  </picture>\n  <picture >\n      <source srcset=\"/images/blog/react-foundation/react_foundation_member_logos_dark.png\" />\n      <img className=\"w-full dark-image\" src=\"/images/blog/react-foundation/react_foundation_member_logos_dark.webp\" />\n  </picture>\n</div>\n\n## React의 기술 거버넌스 {/*reacts-technical-governance*/}\n\n우리는 React의 기술적 방향은 React에 기여하고 유지 관리하는 사람들이 결정해야 한다고 믿습니다. React가 재단으로 이전함에 따라, 특정 회사나 조직이 과도하게 대표되지 않는 것이 중요합니다. 이를 달성하기 위해 우리는 React Foundation 으로부터 독립적인 React의 새로운 기술 거버넌스 구조를 설립할 계획입니다.\n\nReact의 새로운 기술 거버넌스 구조를 만드는 과정의 일환으로 커뮤니티의 피드백을 구할 것입니다. 구조가 확정되면 향후 게시글을 통해 세부 사항을 공유하겠습니다.\n\n## 감사 인사 {/*thank-you*/}\n\nReact를 만든 수천 명의 사람들, 회사들, 프로젝트들 덕분에 React가 성장할 수 있었습니다. React Foundation의 설립은 React 커뮤니티의 힘과 활기찬 생태계를 보여주는 증거입니다. React Foundation과 React의 새로운 기술 거버넌스는 앞으로 수년간 React의 안정적인 미래를 보장할 것입니다.\n"
  },
  {
    "path": "src/content/blog/2025/10/07/react-compiler-1.md",
    "content": "---\ntitle: \"React Compiler v1.0\"\nauthor: Lauren Tan, Joe Savona, and Mofei Zhang\ndate: 2025/10/07\ndescription: 오늘 컴파일러의 첫 번째 안정 버전을 출시합니다.\n\n---\n\n2025년 10월 7일, [Lauren Tan](https://x.com/potetotes), [Joe Savona](https://x.com/en_JS), [Mofei Zhang](https://x.com/zmofei) 작성.\n\n---\n\n<Intro>\n\nReact 팀이 새로운 소식을 전해드립니다.\n\n</Intro>\n\n1. React 컴파일러 1.0이 오늘 출시됩니다.\n2. 컴파일러 기반의 린트 규칙이 `eslint-plugin-react-hooks`의 `recommended` 및 `recommended-latest` 프리셋에 포함됩니다.\n3. 점진적 도입 가이드를 게시했으며, Expo, Vite, Next.js와 협력하여 새로운 앱이 컴파일러를 활성화한 상태로 시작할 수 있도록 했습니다.\n\n---\n\n오늘 컴파일러의 첫 번째 안정 버전을 출시합니다. React 컴파일러는 React와 React Native 모두에서 작동하며, 코드를 다시 작성할 필요 없이 컴포넌트와 훅을 자동으로 최적화합니다. 이 컴파일러는 Meta의 주요 앱에서 충분한 테스트를 거쳤으며 프로덕션 환경에서 사용할 준비가 되었습니다.\n\n[React 컴파일러](/learn/react-compiler)는 자동 메모이제이션을 통해 React 앱을 최적화하는 빌드 타임 도구입니다. 작년에 React 컴파일러의 [첫 베타 버전](/blog/2024/10/21/react-compiler-beta-release)을 공개하고 많은 좋은 피드백과 기여를 받았습니다. 컴파일러를 도입한 사용자들로부터 얻은 성과([Sanity Studio](https://github.com/reactwg/react-compiler/discussions/33) 및 [Wakelet](https://github.com/reactwg/react-compiler/discussions/52)의 사례 연구 참고)에 고무되었으며, React 커뮤니티의 더 많은 사용자에게 컴파일러를 제공하게 되어 기쁩니다.\n\n이번 릴리스는 거의 10년에 걸친 거대하고 복잡한 엔지니어링 노력의 정점입니다. React 팀의 컴파일러에 대한 첫 탐구는 2017년 [Prepack](https://github.com/facebookarchive/prepack)으로 시작되었습니다. 이 프로젝트는 결국 중단되었지만, 여기서 얻은 많은 교훈은 팀이 훅(Hook)을 설계하는 데 정보를 주었고, 훅은 미래의 컴파일러를 염두에 두고 설계되었습니다. 2021년, [Xuan Huang](https://x.com/Huxpro)은 React 컴파일러에 대한 새로운 접근 방식의 [첫 번째 버전](https://www.youtube.com/watch?v=lGEMwh32soc)을 시연했습니다.\n\n비록 이 새로운 React 컴파일러의 첫 버전은 결국 다시 작성되었지만, 첫 프로토타입은 이것이 다루기 쉬운 문제라는 확신을 주었고, 대안적인 컴파일러 아키텍처가 우리가 원했던 메모이제이션 특성을 정확하게 제공할 수 있다는 교훈을 주었습니다. [Joe Savona](https://x.com/en_JS), [Sathya Gunasekaran](https://x.com/_gsathya), [Mofei Zhang](https://x.com/zmofei), [Lauren Tan](https://x.com/potetotes)은 첫 번째 재작성을 통해 컴파일러의 아키텍처를 제어 흐름 그래프(CFG) 기반의 고수준 중간 표현(HIR)으로 전환했습니다. 이는 React 컴파일러 내에서 훨씬 더 정밀한 분석과 타입 추론까지 가능하게 하는 길을 열었습니다. 그 이후로 컴파일러의 많은 중요한 부분이 다시 작성되었으며, 각 재작성은 이전 시도에서 얻은 교훈을 바탕으로 이루어졌습니다. 그리고 그 과정에서 [React 팀](/community/team)의 많은 구성원으로부터 상당한 도움과 기여를 받았습니다.\n\n이번 안정 버전은 앞으로 나올 많은 릴리스 중 첫 번째입니다. 컴파일러는 계속해서 발전하고 개선될 것이며, 앞으로 10년 이상 React의 새로운 기반과 시대가 될 것으로 기대합니다.\n\n[빠른 시작](/learn/react-compiler)으로 바로 넘어가거나, React Conf 2025의 주요 내용을 계속 읽어볼 수 있습니다.\n\n<DeepDive>\n\n#### React 컴파일러는 어떻게 작동하나요? {/*how-does-react-compiler-work*/}\n\nReact 컴파일러는 자동 메모이제이션을 통해 컴포넌트와 훅을 최적화하는 최적화 컴파일러입니다. 현재는 Babel 플러그인으로 구현되어 있지만, 컴파일러는 Babel과 거의 분리되어 있으며 Babel이 제공하는 추상 구문 트리(AST)를 자체적인 새로운 HIR로 낮춥니다. 그리고 여러 컴파일러 패스를 통해 React 코드의 데이터 흐름과 가변성을 신중하게 이해합니다. 이를 통해 컴파일러는 렌더링에 사용되는 값을 세분화하여 메모이제이션할 수 있으며, 조건부 메모이제이션 기능도 포함하는데 이는 수동 메모이제이션으로는 불가능합니다.\n\n```js {8}\nimport { use } from 'react';\n\nexport default function ThemeProvider(props) {\n  if (!props.children) {\n    return null;\n  }\n  // 컴파일러는 조건부 반환 후에도 코드를 메모이제이션할 수 있습니다.\n  const theme = mergeTheme(props.theme, use(ThemeContext));\n  return (\n    <ThemeContext value={theme}>\n      {props.children}\n    </ThemeContext>\n  );\n}\n```\n_[React 컴파일러 Playground](https://playground.react.dev/#N4Igzg9grgTgxgUxALhASwLYAcIwC4AEwBUYCBAvgQGYwQYEDkMCAhnHowNwA6AdvwQAPHPgIATBNVZQANoWpQ+HNBD4EAKgAsEGBAAU6ANzSSYACix0sYAJRF+BAmmoFzAQisQbAOjha0WXEWPntgRycCFjxYdT45WV51Sgi4NTBCPB09AgBeAj0YAHMEbV0ES2swHyzygBoSMnMyvQBhNTxhPFtbJKdo2LcIpwAeFoR2vk6hQiNWWSgEXOBavQoAPmHI4C9ff0DghD4KLZGAenHJ6bxN5N7+ChA6kDS+ajQilHRsXEyATyw5GI+gWRTQfAA8lg8Ko+GBKDQ6AxGAAjVgohCyAC0WFB4KxLHYeCxaWwgQQMDO4jQGW4-H45nCyTOZ1JWECrBhagAshBJMgCDwQPNZEKHgQwJyae8EPCQVAwZDobC7FwnuAtBAAO4ASSmFL48zAKGksjIFCAA)에서 이 예시를 확인하세요._\n\n자동 메모이제이션 외에도 React 컴파일러는 React 코드에 대해 실행되는 유효성 검사 패스를 가지고 있습니다. 이 패스는 [React의 규칙](/reference/rules)을 인코딩하고, 컴파일러의 데이터 흐름 및 가변성에 대한 이해를 사용하여 React의 규칙이 깨진 부분에 대한 진단을 제공합니다. 이러한 진단은 종종 React 코드에 숨어있는 잠재적인 버그를 노출하며, 주로 `eslint-plugin-react-hooks`를 통해 표시됩니다.\n\n컴파일러가 코드를 최적화하는 방법에 대해 더 자세히 알아보려면 [Playground](https://playground.react.dev)를 방문하세요.\n\n</DeepDive>\n\n## 지금 React 컴파일러 사용하기 {/*use-react-compiler-today*/}\n컴파일러를 설치하려면 다음을 따르세요.\n\nnpm\n<TerminalBlock>\nnpm install --save-dev --save-exact babel-plugin-react-compiler@latest\n</TerminalBlock>\n\npnpm\n<TerminalBlock>\npnpm add --save-dev --save-exact babel-plugin-react-compiler@latest\n</TerminalBlock>\n\nyarn\n<TerminalBlock>\nyarn add --dev --exact babel-plugin-react-compiler@latest\n</TerminalBlock>\n\n안정 버전 출시의 일환으로, React 컴파일러를 프로젝트에 더 쉽게 추가할 수 있도록 하고 컴파일러가 메모이제이션을 생성하는 방식을 최적화했습니다. React 컴파일러는 이제 옵셔널 체이닝과 배열 인덱스를 의존성으로 지원합니다. 이러한 개선 사항은 궁극적으로 재렌더링을 줄이고 더 반응적인 UI를 만들면서도, 관용적인 선언적 코드를 계속 작성할 수 있게 해줍니다.\n\n컴파일러 사용에 대한 자세한 내용은 [문서](/learn/react-compiler)에서 확인할 수 있습니다.\n\n## 프로덕션 환경에서 확인된 결과 {/*react-compiler-at-meta*/}\n[컴파일러는 이미 Meta Quest Store와 같은 앱에 적용되었습니다](https://youtu.be/lyEKhv8-3n0?t=3002). 초기 로딩 및 페이지 간 탐색이 최대 12% 개선되었으며, 특정 상호작용은 2.5배 이상 빨라졌습니다. 이러한 성과에도 불구하고 메모리 사용량은 중립을 유지합니다. 결과는 다를 수 있지만, 비슷한 성능 향상을 확인하기 위해 앱에서 컴파일러를 실험해보는 것을 권장합니다.\n\n## 하위 호환성 {/*backwards-compatibility*/}\n베타 발표에서 언급했듯이, React 컴파일러는 React 17 이상과 호환됩니다. 아직 React 19를 사용하지 않는 경우, 컴파일러 설정에서 최소 타겟을 지정하고 `react-compiler-runtime`을 의존성으로 추가하여 React 컴파일러를 사용할 수 있습니다. 이에 대한 문서는 [여기](/reference/react-compiler/target#targeting-react-17-or-18)에서 찾을 수 있습니다.\n\n## 컴파일러 기반 린팅으로 React 규칙 강제하기 {/*migrating-from-eslint-plugin-react-compiler-to-eslint-plugin-react-hooks*/}\nReact 컴파일러에는 [React의 규칙](/reference/rules)을 위반하는 코드를 식별하는 데 도움이 되는 ESLint 규칙이 포함되어 있습니다. 린터는 컴파일러 설치를 요구하지 않으므로 `eslint-plugin-react-hooks`를 업그레이드하는 데 위험이 없습니다. 오늘 모든 사람이 업그레이드하는 것을 권장합니다.\n\n이미 `eslint-plugin-react-compiler`를 설치했다면, 이제 제거하고 `eslint-plugin-react-hooks@latest`를 사용할 수 있습니다. 이 개선에 기여해주신 [@michaelfaith](https://bsky.app/profile/michael.faith)님께 감사드립니다!\n\n설치 방법:\n\nnpm\n<TerminalBlock>\nnpm install --save-dev eslint-plugin-react-hooks@latest\n</TerminalBlock>\n\npnpm\n<TerminalBlock>\npnpm add --save-dev eslint-plugin-react-hooks@latest\n</TerminalBlock>\n\nyarn\n<TerminalBlock>\nyarn add --dev eslint-plugin-react-hooks@latest\n</TerminalBlock>\n\n```js {6}\n// eslint.config.js (Flat Config)\nimport reactHooks from 'eslint-plugin-react-hooks';\nimport { defineConfig } from 'eslint/config';\n\nexport default defineConfig([\n  reactHooks.configs.flat.recommended,\n]);\n```\n\n```js {3}\n// eslintrc.json (Legacy Config)\n{\n  \"extends\": [\"plugin:react-hooks/recommended\"],\n  // ...\n}\n```\n\nReact 컴파일러 규칙을 활성화하려면 `recommended` 프리셋을 사용하는 것을 권장합니다. 더 많은 지침은 [README](https://github.com/facebook/react/blob/main/packages/eslint-plugin-react-hooks/README.md)를 참조할 수 있습니다. 다음은 React Conf에서 소개된 몇 가지 예시입니다.\n\n- [`set-state-in-render`](/reference/eslint-plugin-react-hooks/lints/set-state-in-render)로 렌더링 루프를 유발하는 `setState` 패턴 포착.\n- [`set-state-in-effect`](/reference/eslint-plugin-react-hooks/lints/set-state-in-effect)를 통해 이펙트 내의 비용이 많이 드는 작업 플래그 지정.\n- [`refs`](/reference/eslint-plugin-react-hooks/lints/refs)로 렌더링 중 안전하지 않은 ref 접근 방지.\n\n## useMemo, useCallback, React.memo는 어떻게 해야 하나요? {/*what-should-i-do-about-usememo-usecallback-and-reactmemo*/}\n기본적으로 React 컴파일러는 분석과 휴리스틱을 기반으로 코드를 메모이제이션합니다. 대부분의 경우, 이 메모이제이션은 직접 작성한 것만큼 또는 그 이상으로 정밀할 것입니다. 그리고 위에서 언급했듯이, 컴파일러는 조기 반환 후와 같이 `useMemo`/`useCallback`을 사용할 수 없는 경우에도 메모이제이션할 수 있습니다.\n\n그러나 경우에 따라 개발자가 메모이제이션을 더 세밀하게 제어해야 할 수도 있습니다. `useMemo`와 `useCallback` 훅은 React 컴파일러와 함께 계속 사용하여 어떤 값을 메모이제이션할지 제어하는 탈출구로 사용할 수 있습니다. 이에 대한 흔한 사용 사례는 메모이제이션된 값이 이펙트 의존성으로 사용될 때, 의존성이 의미 있게 변경되지 않았음에도 이펙트가 반복적으로 실행되지 않도록 보장하는 것입니다.\n\n새로운 코드의 경우, 메모이제이션은 컴파일러에 의존하고 정밀한 제어가 필요한 경우에만 `useMemo`/`useCallback`을 사용하는 것을 권장합니다.\n\n기존 코드의 경우, 기존 메모이제이션을 그대로 두거나(제거하면 컴파일 출력이 변경될 수 있음) 메모이제이션을 제거하기 전에 신중하게 테스트하는 것을 권장합니다.\n\n## 새로운 앱은 React 컴파일러를 사용해야 합니다 {/*new-apps-should-use-react-compiler*/}\nExpo, Vite, Next.js 팀과 협력하여 새로운 앱 경험에 컴파일러를 추가했습니다.\n\n[Expo SDK 54](https://docs.expo.dev/guides/react-compiler/) 이상에서는 컴파일러가 기본적으로 활성화되어 있으므로, 새로운 앱은 처음부터 자동으로 컴파일러의 이점을 활용할 수 있습니다.\n\n<TerminalBlock>\nnpx create-expo-app@latest\n</TerminalBlock>\n\n[Vite](https://vite.dev/guide/) 및 [Next.js](https://nextjs.org/docs/app/api-reference/cli/create-next-app) 사용자는 `create-vite` 및 `create-next-app`에서 컴파일러가 활성화된 템플릿을 선택할 수 있습니다.\n\n<TerminalBlock>\nnpm create vite@latest\n</TerminalBlock>\n\n<br />\n\n<TerminalBlock>\nnpx create-next-app@latest\n</TerminalBlock>\n\n## React 컴파일러 점진적으로 도입하기 {/*adopt-react-compiler-incrementally*/}\n기존 애플리케이션을 유지보수하는 경우, 자신의 속도에 맞춰 컴파일러를 출시할 수 있습니다. 게이팅 전략, 호환성 검사, 출시 도구를 다루는 단계별 [점진적 도입 가이드](/learn/react-compiler/incremental-adoption)를 게시하여 안심하고 컴파일러를 활성화할 수 있도록 했습니다.\n\n## swc 지원 (실험적) {/*swc-support-experimental*/}\nReact 컴파일러는 Babel, Vite, Rsbuild와 같은 [여러 빌드 도구](/learn/react-compiler#installation)에 걸쳐 설치할 수 있습니다.\n\n이러한 도구 외에도, [swc](https://swc.rs/) 팀의 강동윤([@kdy1dev](https://x.com/kdy1dev))님과 협력하여 React 컴파일러를 swc 플러그인으로 추가 지원하는 작업을 진행하고 있습니다. 이 작업이 완료되지는 않았지만, [Next.js 앱에서 React 컴파일러를 활성화](https://nextjs.org/docs/app/api-reference/config/next-config-js/reactCompiler)하면 이제 Next.js 빌드 성능이 상당히 빨라질 것입니다.\n\n최상의 빌드 성능을 얻으려면 Next.js [15.3.1](https://github.com/vercel/next.js/releases/tag/v15.3.1) 이상을 사용하는 것을 권장합니다.\n\nVite 사용자는 계속해서 [vite-plugin-react](https://github.com/vitejs/vite-plugin-react)를 사용하여 컴파일러를 활성화할 수 있으며, 이를 [Babel 플러그인](/learn/react-compiler/installation#vite)으로 추가하면 됩니다. 또한 [oxc](https://oxc.rs/) 팀과 협력하여 [컴파일러 지원을 추가](https://github.com/oxc-project/oxc/issues/10048)하고 있습니다. [rolldown](https://github.com/rolldown/rolldown)이 공식적으로 출시되고 Vite에서 지원되며 React 컴파일러에 대한 oxc 지원이 추가되면, 마이그레이션 방법에 대한 정보로 문서를 업데이트할 것입니다.\n\n## React 컴파일러 업그레이드하기 {/*upgrading-react-compiler*/}\nReact 컴파일러는 적용된 자동 메모이제이션이 순전히 성능을 위한 것일 때 가장 잘 작동합니다. 향후 버전의 컴파일러는 메모이제이션이 적용되는 방식을 변경할 수 있으며, 예를 들어 더 세분화되고 정밀해질 수 있습니다.\n\n그러나 제품 코드는 때때로 자바스크립트에서 항상 정적으로 감지할 수 없는 방식으로 [React의 규칙](/reference/rules)을 위반할 수 있기 때문에, 메모이제이션을 변경하면 때때로 예기치 않은 결과가 발생할 수 있습니다. 예를 들어, 이전에 메모이제이션된 값이 컴포넌트 트리의 어딘가에서 `useEffect`의 의존성으로 사용될 수 있습니다. 이 값이 메모이제이션되는 방식이나 여부를 변경하면 해당 `useEffect`가 과도하게 또는 부족하게 실행될 수 있습니다. [동기화를 위해서만 useEffect를 사용](/learn/synchronizing-with-effects)하도록 권장하지만, 코드베이스에는 특정 값 변경에만 응답하여 실행되어야 하는 이펙트와 같은 다른 사용 사례를 다루는 `useEffect`가 있을 수 있습니다.\n\n즉, 메모이제이션을 변경하면 드문 경우에 예기치 않은 동작이 발생할 수 있습니다. 이러한 이유로, React의 규칙을 따르고 앱의 지속적인 엔드투엔드 테스트를 사용하여 안심하고 컴파일러를 업그레이드하고 문제를 일으킬 수 있는 React 규칙 위반을 식별하는 것을 권장합니다.\n\n테스트 커버리지가 좋지 않은 경우, 컴파일러를 SemVer 범위(예: `^1.0.0`)가 아닌 정확한 버전(예: `1.0.0`)으로 고정하는 것을 권장합니다. 컴파일러를 업그레이드할 때 `--save-exact`(npm/pnpm) 또는 `--exact` 플래그(yarn)를 전달하여 이를 수행할 수 있습니다. 그런 다음 컴파일러의 모든 업그레이드를 수동으로 수행하고 앱이 여전히 예상대로 작동하는지 주의 깊게 확인해야 합니다.\n\n---\n\n이 게시물을 검토하고 편집해주신 [Jason Bonta](https://x.com/someextent), [Jimmy Lai](https://x.com/feedthejim), [Kang Dongyoon](https://x.com/kdy1dev) (@kdy1dev), [Dan Abramov](https://bsky.app/profile/danabra.mov)님께 감사드립니다.\n"
  },
  {
    "path": "src/content/blog/2025/10/16/react-conf-2025-recap.md",
    "content": "---\ntitle: \"React Conf 2025 요약\"\nauthor: Matt Carroll, Ricky Hanlon\ndate: 2025/10/16\ndescription: 지난주 React Conf 2025를 개최했으며, 이 게시글에서 이벤트의 발표 내용과 공지 사항을 요약합니다.\n---\n\n2025년 10월 16일, [Matt Carroll](https://x.com/mattcarrollcode), [Ricky Hanlon](https://bsky.app/profile/ricky.fm)\n\n---\n\n<Intro>\n\n지난주 React Conf 2025를 개최했으며, 여기서 [React Foundation](/blog/2025/10/07/introducing-the-react-foundation) 설립을 발표하고 React 및 React Native에 도입될 새로운 기능을 선보였습니다.\n\n</Intro>\n\n---\n\nReact Conf 2025는 2025년 10월 7일부터 8일까지 네바다주 헨더슨에서 개최되었습니다.\n\n[첫째 날](https://www.youtube.com/watch?v=zyVRg2QR6LA&t=1067s)과 [둘째 날](https://www.youtube.com/watch?v=p9OcztRyDl0&t=2299s) 전체 스트리밍을 온라인에서 시청할 수 있으며, 이벤트 사진은 [여기](https://conf.react.dev/photos)에서 확인할 수 있습니다.\n\n이 게시글에서는 이벤트의 발표 내용과 공지 사항을 요약해 드립니다.\n\n\n## 1일 차 기조연설 {/*day-1-keynote*/}\n\n_1일 차 전체 스트리밍 시청하기 [링크](https://www.youtube.com/watch?v=zyVRg2QR6LA&t=1067s)_\n\n첫째 날 기조연설에서 Joe Savona는 지난 React Conf 이후 팀과 커뮤니티의 업데이트 사항, 그리고 React 19.0 및 19.1의 주요 기능을 공유했습니다.\n\nMofei Zhang은 React 19.2의 새로운 기능을 강조했으며, 여기에는 다음이 포함됩니다.\n* [`<Activity />`](https://react.dev/reference/react/Activity) — 가시성(visibility)을 관리하는 새로운 컴포넌트.\n* [`useEffectEvent`](https://react.dev/reference/react/useEffectEvent) — Effects 내에서 이벤트를 발생시키는 Hook.\n* [Performance Tracks](https://react.dev/reference/dev-tools/react-performance-tracks) — DevTools의 새로운 프로파일링 도구.\n* [Partial Pre-Rendering](https://react.dev/blog/2025/10/01/react-19-2#partial-pre-rendering) — 앱의 일부를 미리 렌더링하고 나중에 렌더링을 재개하는 기능.\n\nJack Pope는 Canary에 도입될 새로운 기능들을 발표했으며, 여기에는 다음이 포함됩니다.\n\n* [`<ViewTransition />`](https://react.dev/reference/react/ViewTransition) — 페이지 전환에 애니메이션을 적용하는 새로운 컴포넌트.\n* [Fragment Refs](https://react.dev/reference/react/Fragment#fragmentinstance) — Fragment로 감싸진 DOM 노드와 상호작용하는 새로운 방식.\n\nLauren Tan은 [React 컴파일러 v1.0](/blog/2025/10/07/react-compiler-1)을 발표하고, 다음과 같은 이점 때문에 모든 앱에서 React 컴파일러를 사용할 것을 권장했습니다.\n* React 코드를 이해하는 [자동 메모이제이션](/learn/react-compiler/introduction#what-does-react-compiler-do).\n* 모범 사례를 알려주는 React 컴파일러 기반의 [새로운 린트 규칙](/learn/react-compiler/installation#eslint-integration).\n* Vite, Next.js, Expo의 새로운 앱에 대한 [기본 지원](/learn/react-compiler/installation#basic-setup).\n* 기존 앱이 React 컴파일러로 마이그레이션하기 위한 [마이그레이션 가이드](/learn/react-compiler/incremental-adoption).\n\n마지막으로, Seth Webster는 React의 오픈소스 개발과 커뮤니티를 관리할 [React Foundation](/blog/2025/10/07/introducing-the-react-foundation) 설립을 발표했습니다.\n\n1일 차 시청하기\n\n<YouTubeIframe src=\"https://www.youtube.com/embed/zyVRg2QR6LA?si=z-8t_xCc12HwGJH_&t=1067s\" />\n\n## 2일 차 기조연설 {/*day-2-keynote*/}\n\n_2일 차 전체 스트리밍 시청하기 [링크](https://www.youtube.com/watch?v=p9OcztRyDl0&t=2299s)_\n\nJorge Cohen과 Nicola Corti는 React Native의 놀라운 성장을 강조하며 둘째 날을 시작했습니다. 주간 다운로드 4백만 건(전년 대비 100% 성장)을 기록했으며 Shopify, Zalando, HelloFresh의 주요 앱 마이그레이션, RISE, RUNNA, Partyful과 같은 수상 경력에 빛나는 앱 그리고 Mistral, Replit, v0의 AI 앱을 언급했습니다.\n\nRiccardo Cipolleschi는 React Native의 두 가지 주요 발표를 공유했습니다.\n- [React Native 0.82는 새로운 아키텍처만 지원합니다.](https://reactnative.dev/blog/2025/10/08/react-native-0.82#new-architecture-only)\n- [Experimental Hermes V1 지원](https://reactnative.dev/blog/2025/10/08/react-native-0.82#experimental-hermes-v1)\n\nRuben Norte와 Alex Hunt는 기조연설을 마무리하며 다음을 발표했습니다.\n- 웹의 React와의 호환성 향상을 위한 [새로운 웹 정렬 DOM API](https://reactnative.dev/blog/2025/10/08/react-native-0.82#dom-node-apis).\n- 새로운 네트워크 패널과 데스크탑 앱을 포함하는 [새로운 Performance API](https://reactnative.dev/blog/2025/10/08/react-native-0.82#web-performance-apis-canary).\n\n2일 차 시청하기\n\n<YouTubeIframe src=\"https://www.youtube.com/embed/p9OcztRyDl0?si=qPTHftsUE07cjZpS&t=2299s\" />\n\n\n## React 팀 발표 {/*react-team-talks*/}\n\n컨퍼런스 전반에 걸쳐 React 팀의 발표가 있었습니다. 여기에는 다음이 포함됩니다.\n* [Async React Part I](https://www.youtube.com/watch?v=zyVRg2QR6LA&t=10907s) 및 [Part II](https://www.youtube.com/watch?v=p9OcztRyDl0&t=29073s) [(Ricky Hanlon)](https://x.com/rickhanlonii)는 지난 10년간의 혁신을 통해 무엇이 가능한지 보여주었습니다.\n* [Exploring React Performance](https://www.youtube.com/watch?v=zyVRg2QR6LA&t=20274s) [(Joe Savona)](https://x.com/en_js)는 React 성능 연구 결과를 보여주었습니다.\n* [Reimagining Lists in React Native](https://www.youtube.com/watch?v=p9OcztRyDl0&t=10382s) [(Luna Wei)](https://x.com/lunaleaps)는 모드 기반 렌더링(hidden/pre-render/visible)으로 가시성을 관리하는 리스트용 새로운 기본 요소인 Virtual View를 소개했습니다.\n* [Profiling with React Performance tracks](https://www.youtube.com/watch?v=zyVRg2QR6LA&t=8276s) [(Ruslan Lesiutin)](https://x.com/ruslanlesiutin)은 새로운 React Performance Tracks를 사용하여 성능 문제를 디버깅하고 훌륭한 앱을 구축하는 방법을 보여주었습니다.\n* [React Strict DOM](https://www.youtube.com/watch?v=p9OcztRyDl0&t=9026s) [(Nicolas Gallagher)](https://nicolasgallagher.com/)는 Meta의 웹 코드를 네이티브에서 사용하는 접근 방식에 대해 이야기했습니다.\n* [View Transitions and Activity](https://www.youtube.com/watch?v=zyVRg2QR6LA&t=4870s) [(Chance Strickland)](https://x.com/chancethedev) — Chance는 React 팀과 협력하여 빠르고 네이티브 느낌의 애니메이션을 구축하기 위해 [`<Activity />`](https://react.dev/reference/react/Activity) 및 [`<ViewTransition />`](https://react.dev/reference/react/ViewTransition)를 사용하는 방법을 시연했습니다.\n* [In case you missed the memo](https://www.youtube.com/watch?v=zyVRg2QR6LA&t=9525s) [(Cody Olsen)](https://bsky.app/profile/codey.bsky.social) - Cody는 Sanity Studio에서 컴파일러를 채택하기 위해 React 팀과 협력했으며, 그 경험을 공유했습니다.\n## React 프레임워크 발표 {/*react-framework-talks*/}\n\n둘째 날 후반부에는 다음을 포함하여 React 프레임워크 팀의 연속 발표가 있었습니다.\n\n* [React Native, Amplified](https://www.youtube.com/watch?v=p9OcztRyDl0&t=5737s) (발표자: [Giovanni Laquidara](https://x.com/giolaq), [Eric Fahsl](https://x.com/efahsl))\n* [React Everywhere: Bringing React Into Native Apps](https://www.youtube.com/watch?v=p9OcztRyDl0&t=18213s) (발표자: [Mike Grabowski](https://x.com/grabbou))\n* [How Parcel Bundles React Server Components](https://www.youtube.com/watch?v=p9OcztRyDl0&t=19538s) (발표자: [Devon Govett](https://x.com/devonovett))\n* [Designing Page Transitions](https://www.youtube.com/watch?v=p9OcztRyDl0&t=20640s) (발표자: [Delba de Oliveira](https://x.com/delba_oliveira))\n* [Build Fast, Deploy Faster — Expo in 2025](https://www.youtube.com/watch?v=p9OcztRyDl0&t=21350s) (발표자: [Evan Bacon](https://x.com/baconbrix))\n* [The React Router's take on RSC](https://www.youtube.com/watch?v=p9OcztRyDl0&t=22367s) (발표자: [Kent C. Dodds](https://x.com/kentcdodds))\n* [RedwoodSDK: Web Standards Meet Full-Stack React](https://www.youtube.com/watch?v=p9OcztRyDl0&t=24992s) (발표자: [Peter Pistorius](https://x.com/appfactory) 및 [Aurora Scharff](https://x.com/aurorascharff))\n* [TanStack Start](https://www.youtube.com/watch?v=p9OcztRyDl0&t=26065s) (발표자: [Tanner Linsley](https://x.com/tannerlinsley))\n\n## Q&A {/*q-and-a*/}\n컨퍼런스 기간 동안 세 번의 Q&A 패널이 있었습니다.\n\n* [Meta의 React 팀 Q&A](https://www.youtube.com/watch?v=zyVRg2QR6LA&t=26304s) (호스트: [Shruti Kapoor](https://x.com/shrutikapoor08))\n* [React 프레임워크 Q&A](https://www.youtube.com/watch?v=p9OcztRyDl0&t=26812s) (호스트: [Jack Herrington](https://x.com/jherr))\n* [React와 AI 패널](https://www.youtube.com/watch?v=zyVRg2QR6LA&t=18741s) (호스트: [Lee Robinson](https://x.com/leerob))\n\n## 그리고... {/*and-more*/}\n\n다음과 같은 커뮤니티 발표도 들을 수 있었습니다.\n* [Building an MCP Server](https://www.youtube.com/watch?v=zyVRg2QR6LA&t=24204s) (발표자: [James Swinton](https://x.com/JamesSwintonDev) ([AG Grid](https://www.ag-grid.com/?utm_source=react-conf&utm_medium=react-conf-homepage&utm_campaign=react-conf-sponsorship-2025)))\n* [Modern Emails using React](https://www.youtube.com/watch?v=zyVRg2QR6LA&t=25521s) (발표자: [Zeno Rocha](https://x.com/zenorocha) ([Resend](https://resend.com/)))\n* [Why React Native Apps Make All the Money](https://www.youtube.com/watch?v=zyVRg2QR6LA&t=24917s) (발표자: [Perttu Lähteenlahti](https://x.com/plahteenlahti) ([RevenueCat](https://www.revenuecat.com/)))\n* [The invisible craft of great UX](https://www.youtube.com/watch?v=zyVRg2QR6LA&t=23400s) (발표자: [Michał Dudak](https://x.com/michaldudak) ([MUI](https://mui.com/)))\n\n## 감사드립니다 {/*thanks*/}\n\nReact Conf 2025를 가능하게 해준 모든 스태프, 연사, 참가자분들께 감사드립니다. 일일이 나열하기는 어렵지만, 특히 몇 분께 감사드립니다.\n\n이벤트 전체를 기획하고 컨퍼런스 웹사이트를 구축해 준 [Matt Carroll](https://x.com/mattcarrollcode)에게 감사드립니다.\n\n놀라운 헌신과 에너지로 React Conf의 MC를 맡아주시고, 사려 깊은 연사 소개, 재미있는 농담, 이벤트 전반에 걸친 진정한 열정을 보여준 [Michael Chan](https://x.com/chantastic)에게 감사드립니다. 라이브 스트리밍을 호스트하고, 각 연사를 인터뷰하며, 현장 React Conf 경험을 온라인으로 전달해 준 [Jorge Cohen](https://x.com/JorgeWritesCode)에게 감사드립니다.\n\nReact Conf를 공동 조직하고 디자인, 엔지니어링 및 마케팅 지원을 제공해 준 [Mateusz Kornacki](https://x.com/mat_kornacki), [Mike Grabowski](https://x.com/grabbou), [Kris Lis](https://www.linkedin.com/in/krzysztoflisakakris/) 및 [Callstack](https://www.callstack.com/) 팀에게 감사드립니다. 이벤트 조직을 도와준 [ZeroSlope 팀](https://zeroslopeevents.com/contact-us/)의 Sunny Leggett, Tracey Harrison, Tara Larish, Whitney Pogue, Brianne Smythia에게 감사드립니다.\n\nDiscord의 질문을 라이브 스트리밍으로 전달해 준 [Jorge Cabiedes Acosta](https://github.com/jorge-cab), [Gijs Weterings](https://x.com/gweterings), [Tim Yung](https://x.com/yungsters), [Jason Bonta](https://x.com/someextent)에게 감사드립니다. Discord 중재를 이끌어 준 [Lynn Yu](https://github.com/lynnshaoyu)에게 감사드립니다. 매일 우리를 환영해 준 [Seth Webster](https://x.com/sethwebster)에게 감사드립니다. 그리고 애프터 파티에서 특별 메시지를 전달해 준 [Christopher Chedeau](https://x.com/vjeux), [Kevin Gozali](https://x.com/fkgozali), [Pieter De Baets](https://x.com/Javache)에게 감사드립니다.\n\n컨퍼런스 모바일 앱을 구축해 준 [Kadi Kraman](https://x.com/kadikraman), [Beto](https://x.com/betomoedano), [Nicolas Solerieu](https://www.linkedin.com/in/nicolas-solerieu/)에게 감사드립니다. 컨퍼런스 웹사이트를 도와준 [Wojtek Szafraniec](https://x.com/wojteg1337)에게 감사드립니다. 시각 자료, 무대 및 사운드를 제공해 준 [Mustache](https://www.mustachepower.com/) 및 [Cornerstone](https://cornerstoneav.com/)에게, 그리고 호스팅을 맡아준 Westin Hotel에 감사드립니다.\n\n이벤트를 가능하게 해 준 모든 스폰서에게 감사드립니다. [Amazon](https://www.developer.amazon.com), [MUI](https://mui.com/), [Vercel](https://vercel.com/), [Expo](https://expo.dev/), [RedwoodSDK](https://rwsdk.com), [Ag Grid](https://www.ag-grid.com), [RevenueCat](https://www.revenuecat.com/), [Resend](https://resend.com), [Mux](https://www.mux.com/), [Old Mission](https://www.oldmissioncapital.com/), [Arcjet](https://arcjet.com), [Infinite Red](https://infinite.red/), [RenderATL](https://renderatl.com).\n\n지식과 경험을 커뮤니티와 공유해 준 모든 연사분들께 감사드립니다.\n\n마지막으로, React를 React답게 만드는 요소를 보여주기 위해 현장 및 온라인으로 참석해 준 모든 분들께 감사드립니다. React는 단순한 라이브러리 이상이며, 하나의 커뮤니티입니다. 모두가 함께 모여 지식을 공유하고 배우는 모습은 정말 고무적이었습니다.\n\n다음에 또 뵙겠습니다!\n"
  },
  {
    "path": "src/content/blog/2025/12/03/critical-security-vulnerability-in-react-server-components.md",
    "content": "---\ntitle: \"Critical Security Vulnerability in React Server Components\"\nauthor: The React Team\ndate: 2025/12/03\ndescription: There is an unauthenticated remote code execution vulnerability in React Server Components. A fix has been published in versions 19.0.1, 19.1.2, and 19.2.1. We recommend upgrading immediately.\n\n---\n\nDecember 3, 2025 by [The React Team](/community/team)\n\n---\n\n<Intro>\n\nThere is an unauthenticated remote code execution vulnerability in React Server Components.\n\nWe recommend upgrading immediately.\n\n</Intro>\n\n---\n\nOn November 29th, Lachlan Davidson reported a security vulnerability in React that allows unauthenticated remote code execution by exploiting a flaw in how React decodes payloads sent to React Server Function endpoints.\n\nEven if your app does not implement any React Server Function endpoints it may still be vulnerable if your app supports React Server Components.\n\nThis vulnerability was disclosed as [CVE-2025-55182](https://www.cve.org/CVERecord?id=CVE-2025-55182) and is rated CVSS 10.0.\n\nThe vulnerability is present in versions 19.0, 19.1.0, 19.1.1, and 19.2.0 of:\n\n* [react-server-dom-webpack](https://www.npmjs.com/package/react-server-dom-webpack)\n* [react-server-dom-parcel](https://www.npmjs.com/package/react-server-dom-parcel)\n* [react-server-dom-turbopack](https://www.npmjs.com/package/react-server-dom-turbopack?activeTab=readme)\n\n## Immediate Action Required {/*immediate-action-required*/}\n\nA fix was introduced in versions [19.0.1](https://github.com/facebook/react/releases/tag/v19.0.1), [19.1.2](https://github.com/facebook/react/releases/tag/v19.1.2), and [19.2.1](https://github.com/facebook/react/releases/tag/v19.2.1). If you are using any of the above packages please upgrade to any of the fixed versions immediately.\n\nIf your app’s React code does not use a server, your app is not affected by this vulnerability. If your app does not use a framework, bundler, or bundler plugin that supports React Server Components, your app is not affected by this vulnerability.\n\n### Affected frameworks and bundlers {/*affected-frameworks-and-bundlers*/}\n\nSome React frameworks and bundlers depended on, had peer dependencies for, or included the vulnerable React packages. The following React frameworks & bundlers are affected: [next](https://www.npmjs.com/package/next), [react-router](https://www.npmjs.com/package/react-router), [waku](https://www.npmjs.com/package/waku), [@parcel/rsc](https://www.npmjs.com/package/@parcel/rsc), [@vitejs/plugin-rsc](https://www.npmjs.com/package/@vitejs/plugin-rsc), and [rwsdk](https://www.npmjs.com/package/rwsdk).\n\nSee the [update instructions below](#update-instructions) for how to upgrade to these patches.\n\n### Hosting Provider Mitigations {/*hosting-provider-mitigations*/}\n\nWe have worked with a number of hosting providers to apply temporary mitigations.\n\nYou should not depend on these to secure your app, and still update immediately.\n\n### Vulnerability overview {/*vulnerability-overview*/}\n\n[React Server Functions](https://react.dev/reference/rsc/server-functions) allow a client to call a function on a server. React provides integration points and tools that frameworks and bundlers use to help React code run on both the client and the server. React translates requests on the client into HTTP requests which are forwarded to a server. On the server, React translates the HTTP request into a function call and returns the needed data to the client.\n\nAn unauthenticated attacker could craft a malicious HTTP request to any Server Function endpoint that, when deserialized by React, achieves remote code execution on the server. Further details of the vulnerability will be provided after the rollout of the fix is complete.\n\n## Update Instructions {/*update-instructions*/}\n\n<Note>\n\nThese instructions have been updated to include the new vulnerabilities:\n\n- **Denial of Service - High Severity**: [CVE-2025-55184](https://www.cve.org/CVERecord?id=CVE-2025-55184) and [CVE-2025-67779](https://www.cve.org/CVERecord?id=CVE-2025-67779) (CVSS 7.5)\n- **Source Code Exposure - Medium Severity**: [CVE-2025-55183](https://www.cve.org/CVERecord?id=CVE-2025-55183) (CVSS 5.3)\n- **Denial of Service - High Severity**: January 26, 2026 [CVE-2026-23864](https://www.cve.org/CVERecord?id=CVE-2026-23864) (CVSS 7.5)\n\nSee the [follow-up blog post](/blog/2025/12/11/denial-of-service-and-source-code-exposure-in-react-server-components) for more info.\n\n-----\n\n_Updated January 26, 2026._\n</Note>\n\n### Next.js {/*update-next-js*/}\n\nAll users should upgrade to the latest patched version in their release line:\n\n```bash\nnpm install next@14.2.35  // for 13.3.x, 13.4.x, 13.5.x, 14.x\nnpm install next@15.0.8   // for 15.0.x\nnpm install next@15.1.12  // for 15.1.x\nnpm install next@15.2.9   // for 15.2.x\nnpm install next@15.3.9   // for 15.3.x\nnpm install next@15.4.11  // for 15.4.x\nnpm install next@15.5.10  // for 15.5.x\nnpm install next@16.0.11  // for 16.0.x\nnpm install next@16.1.5   // for 16.1.x\n\nnpm install next@15.6.0-canary.60   // for 15.x canary releases\nnpm install next@16.1.0-canary.19   // for 16.x canary releases\n```\n\n15.0.8, 15.1.12, 15.2.9, 15.3.9, 15.4.10, 15.5.10, 15.6.0-canary.61, 16.0.11, 16.1.5\n\nIf you are on version `13.3` or later version of Next.js 13 (`13.3.x`, `13.4.x`, or `13.5.x`) please upgrade to version `14.2.35`.\n\nIf you are on `next@14.3.0-canary.77` or a later canary release, downgrade to the latest stable 14.x release:\n\n```bash\nnpm install next@14\n```\n\nSee the [Next.js blog](https://nextjs.org/blog/security-update-2025-12-11) for the latest update instructions and the [previous changelog](https://nextjs.org/blog/CVE-2025-66478) for more info.\n\n### React Router {/*update-react-router*/}\n\nIf you are using React Router's unstable RSC APIs, you should upgrade the following package.json dependencies if they exist:\n\n```bash\nnpm install react@latest\nnpm install react-dom@latest\nnpm install react-server-dom-parcel@latest\nnpm install react-server-dom-webpack@latest\nnpm install @vitejs/plugin-rsc@latest\n```\n\n### Expo {/*expo*/}\n\nTo learn more about mitigating, read the article on [expo.dev/changelog](https://expo.dev/changelog/mitigating-critical-security-vulnerability-in-react-server-components).\n\n### Redwood SDK {/*update-redwood-sdk*/}\n\nEnsure you are on rwsdk>=1.0.0-alpha.0\n\nFor the latest beta version:\n\n```bash\nnpm install rwsdk@latest\n```\n\nUpgrade to the latest `react-server-dom-webpack`:\n\n```bash\nnpm install react@latest react-dom@latest react-server-dom-webpack@latest\n```\n\nSee [Redwood docs](https://docs.rwsdk.com/migrating/) for more migration instructions.\n\n### Waku {/*update-waku*/}\n\nUpgrade to the latest `react-server-dom-webpack`:\n\n```bash\nnpm install react@latest react-dom@latest react-server-dom-webpack@latest waku@latest\n```\n\nSee [Waku announcement](https://github.com/wakujs/waku/discussions/1823) for more migration instructions.\n\n### `@vitejs/plugin-rsc` {/*vitejs-plugin-rsc*/}\n\nUpgrade to the latest RSC plugin:\n\n```bash\nnpm install react@latest react-dom@latest @vitejs/plugin-rsc@latest\n```\n\n### `react-server-dom-parcel` {/*update-react-server-dom-parcel*/}\n\nUpdate to the latest version:\n\n ```bash\n npm install react@latest react-dom@latest react-server-dom-parcel@latest\n ```\n\n### `react-server-dom-turbopack` {/*update-react-server-dom-turbopack*/}\n\nUpdate to the latest version:\n\n ```bash\n npm install react@latest react-dom@latest react-server-dom-turbopack@latest\n ```\n\n### `react-server-dom-webpack` {/*update-react-server-dom-webpack*/}\n\nUpdate to the latest version:\n\n ```bash\nnpm install react@latest react-dom@latest react-server-dom-webpack@latest\n ```\n\n\n### React Native {/*react-native*/}\n\nFor React Native users not using a monorepo or `react-dom`, your `react` version should be pinned in your `package.json`, and there are no additional steps needed.\n\nIf you are using React Native in a monorepo, you should update _only_ the impacted packages if they are installed:\n\n- `react-server-dom-webpack`\n- `react-server-dom-parcel`\n- `react-server-dom-turbopack`\n\nThis is required to mitigate the security advisory, but you do not need to update `react` and `react-dom` so this will not cause the version mismatch error in React Native.\n\nSee [this issue](https://github.com/facebook/react-native/issues/54772#issuecomment-3617929832) for more information.\n\n\n## Timeline {/*timeline*/}\n\n* **November 29th**: Lachlan Davidson reported the security vulnerability via [Meta Bug Bounty](https://bugbounty.meta.com/).\n* **November 30th**: Meta security researchers confirmed and began working with the React team on a fix.\n* **December 1st**: A fix was created and the React team began working with affected hosting providers and open source projects to validate the fix, implement mitigations and roll out the fix\n* **December 3rd**: The fix was published to npm and the publicly disclosed as CVE-2025-55182.\n\n## Attribution {/*attribution*/}\n\nThank you to [Lachlan Davidson](https://github.com/lachlan2k) for discovering, reporting, and working to help fix this vulnerability.\n"
  },
  {
    "path": "src/content/blog/2025/12/11/denial-of-service-and-source-code-exposure-in-react-server-components.md",
    "content": "---\ntitle: \"Denial of Service and Source Code Exposure in React Server Components\"\nauthor: The React Team\ndate: 2025/12/11\ndescription: Security researchers have found and disclosed two additional vulnerabilities in React Server Components while attempting to exploit the patches in last week’s critical vulnerability. High vulnerability Denial of Service (CVE-2025-55184), and medium vulnerability Source Code Exposure (CVE-2025-55183)\n\n\n---\n\nDecember 11, 2025 by [The React Team](/community/team)\n\n_Updated January 26, 2026._\n\n---\n\n<Intro>\n\nSecurity researchers have found and disclosed two additional vulnerabilities in React Server Components while attempting to exploit the patches in last week’s critical vulnerability.\n\n**These new vulnerabilities do not allow for Remote Code Execution.** The patch for React2Shell remains effective at mitigating the Remote Code Execution exploit.\n\n</Intro>\n\n---\n\nThe new vulnerabilities are disclosed as:\n\n- **Denial of Service - High Severity**: [CVE-2025-55184](https://www.cve.org/CVERecord?id=CVE-2025-55184), [CVE-2025-67779](https://www.cve.org/CVERecord?id=CVE-2025-67779), and [CVE-2026-23864](https://www.cve.org/CVERecord?id=CVE-2026-23864) (CVSS 7.5)\n- **Source Code Exposure - Medium Severity**: [CVE-2025-55183](https://www.cve.org/CVERecord?id=CVE-2025-55183) (CVSS 5.3)\n\nWe recommend upgrading immediately due to the severity of the newly disclosed vulnerabilities.\n\n<Note>\n\n#### The patches published earlier are vulnerable. {/*the-patches-published-earlier-are-vulnerable*/}\n\nIf you already updated for the previous vulnerabilities, you will need to update again.\n\nIf you updated to 19.0.3, 19.1.4, and 19.2.3, [these are incomplete](#additional-fix-published), and you will need to update again.\n\nPlease see [the instructions in the previous post](/blog/2025/12/03/critical-security-vulnerability-in-react-server-components#update-instructions) for upgrade steps.\n\n-----\n\n_Updated January 26, 2026._\n\n</Note>\n\nFurther details of these vulnerabilities will be provided after the rollout of the fixes are complete.\n\n## Immediate Action Required {/*immediate-action-required*/}\n\nThese vulnerabilities are present in the same packages and versions as [CVE-2025-55182](/blog/2025/12/03/critical-security-vulnerability-in-react-server-components).\n\nThis includes 19.0.0, 19.0.1, 19.0.2, 19.0.3, 19.1.0, 19.1.1, 19.1.2, 19.1.3, 19.2.0, 19.2.1, 19.2.2, and 19.2.3 of:\n\n* [react-server-dom-webpack](https://www.npmjs.com/package/react-server-dom-webpack)\n* [react-server-dom-parcel](https://www.npmjs.com/package/react-server-dom-parcel)\n* [react-server-dom-turbopack](https://www.npmjs.com/package/react-server-dom-turbopack?activeTab=readme)\n\nFixes were backported to versions 19.0.4, 19.1.5, and 19.2.4. If you are using any of the above packages please upgrade to any of the fixed versions immediately.\n\nAs before, if your app’s React code does not use a server, your app is not affected by these vulnerabilities. If your app does not use a framework, bundler, or bundler plugin that supports React Server Components, your app is not affected by these vulnerabilities.\n\n<Note>\n\n#### It’s common for critical CVEs to uncover follow‑up vulnerabilities. {/*its-common-for-critical-cves-to-uncover-followup-vulnerabilities*/}\n\nWhen a critical vulnerability is disclosed, researchers scrutinize adjacent code paths looking for variant exploit techniques to test whether the initial mitigation can be bypassed.\n\nThis pattern shows up across the industry, not just in JavaScript. For example, after [Log4Shell](https://nvd.nist.gov/vuln/detail/cve-2021-44228), additional CVEs ([1](https://nvd.nist.gov/vuln/detail/cve-2021-45046), [2](https://nvd.nist.gov/vuln/detail/cve-2021-45105)) were reported as the community probed the original fix.\n\nAdditional disclosures can be frustrating, but they are generally a sign of a healthy response cycle.\n\n</Note>\n\n### Affected frameworks and bundlers {/*affected-frameworks-and-bundlers*/}\n\nSome React frameworks and bundlers depended on, had peer dependencies for, or included the vulnerable React packages. The following React frameworks & bundlers are affected: [next](https://www.npmjs.com/package/next), [react-router](https://www.npmjs.com/package/react-router), [waku](https://www.npmjs.com/package/waku), [@parcel/rsc](https://www.npmjs.com/package/@parcel/rsc), [@vite/rsc-plugin](https://www.npmjs.com/package/@vitejs/plugin-rsc), and [rwsdk](https://www.npmjs.com/package/rwsdk).\n\nPlease see [the instructions in the previous post](/blog/2025/12/03/critical-security-vulnerability-in-react-server-components#update-instructions) for upgrade steps.\n\n### Hosting Provider Mitigations {/*hosting-provider-mitigations*/}\n\nAs before, we have worked with a number of hosting providers to apply temporary mitigations.\n\nYou should not depend on these to secure your app, and still update immediately.\n\n### React Native {/*react-native*/}\n\nFor React Native users not using a monorepo or `react-dom`, your `react` version should be pinned in your `package.json`, and there are no additional steps needed.\n\nIf you are using React Native in a monorepo, you should update _only_ the impacted packages if they are installed:\n\n- `react-server-dom-webpack`\n- `react-server-dom-parcel`\n- `react-server-dom-turbopack`\n\nThis is required to mitigate the security advisories, but you do not need to update `react` and `react-dom` so this will not cause the version mismatch error in React Native.\n\nSee [this issue](https://github.com/facebook/react-native/issues/54772#issuecomment-3617929832) for more information.\n\n---\n\n## High Severity: Multiple Denial of Service {/*high-severity-multiple-denial-of-service*/}\n\n**CVEs:** [CVE-2026-23864](https://www.cve.org/CVERecord?id=CVE-2026-23864)\n**Base Score:** 7.5 (High)\n**Date**: January 26, 2026\n\nSecurity researchers discovered additional DoS vulnerabilities still exist in React Server Components.\n\nThe vulnerabilities are triggered by sending specially crafted HTTP requests to Server Function endpoints, and could lead to server crashes, out-of-memory exceptions or excessive CPU usage; depending on the vulnerable code path being exercised, the application configuration and application code.\n\nThe patches published January 26th mitigate these DoS vulnerabilities.\n\n<Note>\n\n#### Additional fixes published {/*additional-fix-published*/}\n\nThe original fix addressing the DoS in [CVE-2025-55184](https://www.cve.org/CVERecord?id=CVE-2025-55184) was incomplete.\n\nThis left previous versions vulnerable. Versions 19.0.4, 19.1.5, 19.2.4 are safe.\n\n-----\n\n_Updated January 26, 2026._\n\n</Note>\n\n---\n\n## High Severity: Denial of Service {/*high-severity-denial-of-service*/}\n\n**CVEs:** [CVE-2025-55184](https://www.cve.org/CVERecord?id=CVE-2025-55184) and [CVE-2025-67779](https://www.cve.org/CVERecord?id=CVE-2025-67779)\n**Base Score:** 7.5 (High)\n\nSecurity researchers have discovered that a malicious HTTP request can be crafted and sent to any Server Functions endpoint that, when deserialized by React, can cause an infinite loop that hangs the server process and consumes CPU. Even if your app does not implement any React Server Function endpoints it may still be vulnerable if your app supports React Server Components.\n\nThis creates a vulnerability vector where an attacker may be able to deny users from accessing the product, and potentially have a performance impact on the server environment.\n\nThe patches published today mitigate by preventing the infinite loop.\n\n## Medium Severity: Source Code Exposure {/*low-severity-source-code-exposure*/}\n\n**CVE:** [CVE-2025-55183](https://www.cve.org/CVERecord?id=CVE-2025-55183)\n**Base Score**: 5.3 (Medium)\n\nA security researcher has discovered that a malicious HTTP request sent to a vulnerable Server Function may unsafely return the source code of any Server Function. Exploitation requires the existence of a Server Function which explicitly or implicitly exposes a stringified argument:\n\n```javascript\n'use server';\n\nexport async function serverFunction(name) {\n  const conn = db.createConnection('SECRET KEY');\n  const user = await conn.createUser(name); // implicitly stringified, leaked in db\n\n  return {\n   id: user.id,\n   message: `Hello, ${name}!` // explicitly stringified, leaked in reply\n  }}\n```\n\nAn attacker may be able to leak the following:\n\n```txt\n0:{\"a\":\"$@1\",\"f\":\"\",\"b\":\"Wy43RxUKdxmr5iuBzJ1pN\"}\n1:{\"id\":\"tva1sfodwq\",\"message\":\"Hello, async function(a){console.log(\\\"serverFunction\\\");let b=i.createConnection(\\\"SECRET KEY\\\");return{id:(await b.createUser(a)).id,message:`Hello, ${a}!`}}!\"}\n```\n\nThe patches published today prevent stringifying the Server Function source code.\n\n<Note>\n\n#### Only secrets in source code may be exposed. {/*only-secrets-in-source-code-may-be-exposed*/}\n\nSecrets hardcoded in source code may be exposed, but runtime secrets such as `process.env.SECRET` are not affected.\n\nThe scope of the exposed code is limited to the code inside the Server Function, which may include other functions depending on the amount of inlining your bundler provides. \n\nAlways verify against production bundles.\n\n</Note>\n\n---\n\n## Timeline {/*timeline*/}\n* **December 3rd**: Leak reported to Vercel and [Meta Bug Bounty](https://bugbounty.meta.com/) by [Andrew MacPherson](https://github.com/AndrewMohawk).\n* **December 4th**: Initial DoS reported to [Meta Bug Bounty](https://bugbounty.meta.com/) by [RyotaK](https://ryotak.net).\n* **December 6th**: Both issues confirmed by the React team, and the team began investigating.\n* **December 7th**: Initial fixes created and the React team began verifying and planning new patch.\n* **December 8th**: Affected hosting providers and open source projects notified.\n* **December 10th**: Hosting provider mitigations in place and patches verified.\n* **December 11th**: Additional DoS reported to [Meta Bug Bounty](https://bugbounty.meta.com/) by Shinsaku Nomura.\n* **December 11th**: Patches published and publicly disclosed as [CVE-2025-55183](https://www.cve.org/CVERecord?id=CVE-2025-55183) and [CVE-2025-55184](https://www.cve.org/CVERecord?id=CVE-2025-55184).\n* **December 11th**: Missing DoS case found internally, patched and publicly disclosed as [CVE-2025-67779](https://www.cve.org/CVERecord?id=CVE-2025-67779).\n* **January 26th**: Additional DoS cases found, patched, and publicly disclosed as [CVE-2026-23864](https://www.cve.org/CVERecord?id=CVE-2026-23864).\n---\n\n## Attribution {/*attribution*/}\n\nThank you to [Andrew MacPherson (AndrewMohawk)](https://github.com/AndrewMohawk) for reporting the Source Code Exposure, [RyotaK](https://ryotak.net) from GMO Flatt Security Inc and Shinsaku Nomura of Bitforest Co., Ltd. for reporting the Denial of Service vulnerabilities. Thank you to [Mufeed VH](https://x.com/mufeedvh) from [Winfunc Research](https://winfunc.com), [Joachim Viide](https://jviide.iki.fi), [RyotaK](https://ryotak.net) from [GMO Flatt Security Inc](https://flatt.tech/en/) and Xiangwei Zhang of Tencent Security YUNDING LAB for reporting the additional DoS vulnerabilities.\n"
  },
  {
    "path": "src/content/blog/2026/02/24/the-react-foundation.md",
    "content": "---\ntitle: \"The React Foundation: A New Home for React Hosted by the Linux Foundation\"\nauthor: Matt Carroll\ndate: 2026/02/24\ndescription: The React Foundation has officially launched, hosted by the Linux Foundation.\n---\n\nFebruary 24, 2026 by [Matt Carroll](https://x.com/mattcarrollcode)\n\n---\n\n<div style={{display: 'flex', justifyContent: 'center', marginBottom: '1rem', marginLeft: '7rem', marginRight: '7rem' }}>\n  <picture >\n      <source srcset=\"/images/blog/react-foundation/react_foundation_logo.png\" />\n      <img className=\"w-full light-image\" src=\"/images/blog/react-foundation/react_foundation_logo.webp\" />\n  </picture>\n  <picture >\n      <source srcset=\"/images/blog/react-foundation/react_foundation_logo_dark.png\" />\n      <img className=\"w-full dark-image\" src=\"/images/blog/react-foundation/react_foundation_logo_dark.webp\" />\n  </picture>\n</div>\n\n<Intro>\n\nThe React Foundation has officially launched, hosted by the Linux Foundation.\n\n</Intro>\n\n---\n\n[In October](/blog/2025/10/07/introducing-the-react-foundation), we announced our intent to form the React Foundation. Today, we're excited to share that the React Foundation has officially launched.\n\nReact, React Native, and supporting projects like JSX are no longer owned by Meta — they are now owned by the React Foundation, an independent foundation hosted by the Linux Foundation. You can read more in the [Linux Foundation's press release](https://www.linuxfoundation.org/press/linux-foundation-announces-the-formation-of-the-react-foundation).\n\n### Founding Members {/*founding-members*/}\n\nThe React Foundation has eight Platinum founding members: **Amazon**, **Callstack**, **Expo**, **Huawei**, **Meta**, **Microsoft**, **Software Mansion**, and **Vercel**. **Huawei** has joined since [our announcement in October](/blog/2025/10/07/introducing-the-react-foundation). The React Foundation will be governed by a board of directors composed of representatives from each member, with [Seth Webster](https://sethwebster.com/) serving as executive director.\n\n<div style={{display: 'flex', justifyContent: 'center', margin: '2rem'}}>\n  <picture >\n      <source srcset=\"/images/blog/react-foundation/react_foundation_member_logos_updated.png\" />\n      <img className=\"w-full light-image\" src=\"/images/blog/react-foundation/react_foundation_member_logos_updated.webp\" />\n  </picture>\n  <picture >\n      <source srcset=\"/images/blog/react-foundation/react_foundation_member_logos_dark_updated.png\" />\n      <img className=\"w-full dark-image\" src=\"/images/blog/react-foundation/react_foundation_member_logos_dark_updated.webp\" />\n  </picture>\n</div>\n\n### New Provisional Leadership Council {/*new-provisional-leadership-council*/}\n\nReact's technical governance will always be independent from the React Foundation board — React's technical direction will continue to be set by the people who contribute to and maintain React. We have formed a provisional leadership council to determine this structure. We will share an update in the coming months.\n\n### Next Steps {/*next-steps*/}\n\nThere is still work to do to complete the transition. In the coming months we will be:\n\n* Finalizing the technical governance structure for React\n* Transferring repositories, websites, and other infrastructure to the React Foundation\n* Exploring programs to support the React ecosystem\n* Kicking off planning for the next React Conf\n\nWe will share updates as this work progresses.\n\n### Thank You {/*thank-you*/}\n\nNone of this would be possible without the thousands of contributors who have shaped React over the past decade. Thank you to our founding members, to every contributor who has opened a pull request, filed an issue, or helped someone learn React, and to the millions of developers who build with React every day. The React Foundation exists because of this community, and we're looking forward to building its future together.\n"
  },
  {
    "path": "src/content/blog/index.md",
    "content": "---\ntitle: React 블로그\n---\n\n<Intro>\n\n이 블로그는 React 팀의 업데이트에 대한 공식 출처입니다. 릴리스 노트 및 더 이상 사용되지 않는 기능들에 대한 공지<sup>Deprecation Notice</sup>를 비롯한 중요 내용들을 이곳에 먼저 공유합니다. 블루스카이의 [@react.dev](https://bsky.app/profile/react.dev) 혹은 트위터의 [@reactjs](https://twitter.com/reactjs) 계정을 팔로우해도 좋지만, 이 블로그만으로도 모든 정보를 얻을 수 있습니다.\n\n</Intro>\n\n<div className=\"sm:-mx-5 flex flex-col gap-5 mt-12\">\n\n<BlogCard title=\"The React Foundation: A New Home for React Hosted by the Linux Foundation\" date=\"February 24, 2026\" url=\"/blog/2026/02/24/the-react-foundation\">\n\nThe React Foundation has officially launched under the Linux Foundation.\n\n</BlogCard>\n\n<BlogCard title=\"Denial of Service and Source Code Exposure in React Server Components\" date=\"December 11, 2025\" url=\"/blog/2025/12/11/denial-of-service-and-source-code-exposure-in-react-server-components\">\n\nSecurity researchers have found and disclosed two additional vulnerabilities in React Server Components while attempting to exploit the patches in last week’s critical vulnerability...\n\n</BlogCard>\n\n<BlogCard title=\"Critical Security Vulnerability in React Server Components\" date=\"December 3, 2025\" url=\"/blog/2025/12/03/critical-security-vulnerability-in-react-server-components\">\n\nThere is an unauthenticated remote code execution vulnerability in React Server Components. A fix has been published in versions 19.0.1, 19.1.2, and 19.2.1. We recommend upgrading immediately.\n\n</BlogCard>\n\n<BlogCard title=\"React Conf 2025 요약\" date=\"2025년 10월 16일\" url=\"/blog/2025/10/16/react-conf-2025-recap\">\n\n지난주에 React Conf 2025를 개최했습니다. 이 글에서는 행사에서 있었던 강연과 발표 내용을 정리합니다...\n\n</BlogCard>\n\n<BlogCard title=\"React 컴파일러 v1.0\" date=\"2025년 10월 7일\" url=\"/blog/2025/10/07/react-compiler-1\">\n\n오늘 컴파일러의 첫 릴리즈 버전을 출시하며, 도입을 더 쉽게 할 린팅 및 툴링 개선 사항도 함께 공개합니다.\n\n</BlogCard>\n\n<BlogCard title=\"React Foundation 소개\" date=\"2025년 10월 7일\" url=\"/blog/2025/10/07/introducing-the-react-foundation\">\n\n오늘 우리는 React Foundation의 설립과 새로운 기술 거버넌스 구조에 대한 계획을 발표합니다.\n\n</BlogCard>\n\n<BlogCard title=\"React 19.2\" date=\"2025년 10월 1일\" url=\"/blog/2025/10/01/react-19-2\">\n\nReact 19.2에는 Activity, React 성능 트랙, useEffectEvent 등의 새로운 기능이 추가되었습니다. 이 글에서는...\n\n</BlogCard>\n\n<BlogCard title=\"React Labs: View Transitions, Activity, and more\" date=\"April 23, 2025\" url=\"/blog/2025/04/23/react-labs-view-transitions-activity-and-more\">\n\nIn React Labs posts, we write about projects in active research and development. In this post, we're sharing two new experimental features that are ready to try today, and sharing other areas we're working on now ...\n\n</BlogCard>\n\n<BlogCard title=\"Create React App 지원 종료\" date=\"2025년 2월 14일\" url=\"/blog/2025/02/14/sunsetting-create-react-app\">\n\n새로운 앱에 대한 Create React App 사용을 중단하며, 기존 앱은 프레임워크나 Vite, Parcel, RSBuild 같은 빌드 도구로의 마이그레이션을 권장합니다. 또한 프레임워크가 프로젝트와 맞지 않거나, 자신만의 프레임워크를 구축하고 싶거나, 혹은 React가 어떻게 작동하는지 배우기 위해 React 앱을 처음부터 만들어 보고 싶은 사용자들을 위한 문서를 제공합니다.\n\n</BlogCard>\n\n<BlogCard title=\"React v19\" date=\"2024년 12월 5일\" url=\"/blog/2024/12/05/react-19\">\n\nReact 19 업그레이드 가이드에서 React 19로 앱을 업그레이드하는 단계별 지침을 공유했습니다. 이 포스트에서 React 19의 새로운 기능들과 이를 도입하는 방법을 제공합니다.\n\n</BlogCard>\n\n<BlogCard title=\"React Compiler Beta Release\" date=\"October 21, 2024\" url=\"/blog/2024/10/21/react-compiler-beta-release\">\n\nWe announced an experimental release of React Compiler at React Conf 2024. We've made a lot of progress since then, and in this post we want to share what's next for React Compiler ...\n\n</BlogCard>\n\n<BlogCard title=\"React Conf 2024 요약\" date=\"2024년 5월 22일\" url=\"/blog/2024/05/22/react-conf-2024-recap\">\n\n지난주 우리는 네바다주 헨더슨에서 2일간의 React Conf 2024를 개최했으며, 700명 이상의 참가자가 현장에서 모여 UI 엔지니어링 분야의 최신 동향을 논의했습니다. 이는 2019년 이후 처음 열린 오프라인 콘퍼런스였으며, 우리는 이 커뮤니티를 다시 한자리에 모을 수 있게 되어 매우 기뻤습니다.\n\n</BlogCard>\n\n<BlogCard title=\"React 19 업그레이드 가이드\" date=\"2024년 4월 25일\" url=\"/blog/2024/04/25/react-19-upgrade-guide\">\n\nReact 19에 추가된 개선 사항들로 인해 일부 주요한 변경 사항<sup>Breaking Changes</sup>이 있지만, 업그레이드를 가능한 원활하게 진행할 수 있도록 노력했으며 대부분의 앱에 큰 영향이 없을 것으로 예상합니다. 이 글에서는 앱과 라이브러리를 React 19로 업그레이드하는 단계를 안내합니다.\n\n</BlogCard>\n\n<BlogCard title=\"React Labs: 그동안의 작업 - 2024년 2월\" date=\"2024년 2월 15일\" url=\"/blog/2024/02/15/react-labs-what-we-have-been-working-on-february-2024\">\n\nReact Labs 게시글에서는 현재 연구 개발 중인 프로젝트에 대한 글을 작성합니다. 지난 업데이트 이후 React 컴파일러, 새로운 기능 및 React 19에서 상당한 진전이 있었으며, 그 내용을 공유하고자 합니다.\n\n</BlogCard>\n\n<BlogCard title=\"React Canaries: Meta 외부에서 점진적 기능 롤아웃 활성화하기\" date=\"2023년 5월 3일\" url=\"/blog/2023/05/03/react-canaries\">\n\n기존에는 새로운 React 기능이 Meta 에서만 먼저 제공되고 나중에 오픈 소스 릴리스에 적용되었습니다. 이제 Meta 에서 내부적으로 React 를 사용하는 방식과 유사하게 React 커뮤니티에 새로운 기능의 디자인이 거의 완성되는 즉시 개별 기능을 채택할 수 있는 옵션을 제공하고자 합니다. 공식적으로 지원되는 새로운 Canary release 채널을 소개합니다. 이를 통해 프레임워크와 같은 선별된 설정에서 개별 React 기능의 채택을 React 릴리스 일정에서 분리할 수 있습니다.\n\n</BlogCard>\n\n<BlogCard title=\"React Labs: 그동안의 작업 – 2023년 3월\" date=\"2023년 3월 22일\" url=\"/blog/2023/03/22/react-labs-what-we-have-been-working-on-march-2023\">\nReact Labs 게시글에는 활발히 연구 개발 중인 프로젝트에 대한 내용을 작성합니다. 우리는 지난 업데이트 이후 React 서버 컴포넌트, 에셋 로딩, 컴파일러 최적화, 오프스크린 랜더링, 트랜지션 추적에 대해 상당한 발전을 이루었고, 그 내용들을 공유하려고 합니다.\n</BlogCard>\n\n\n<BlogCard title=\"react.dev를 소개합니다\" date=\"2023년 3월 16일\" url=\"/blog/2023/03/16/introducing-react-dev\">\n오늘 React와 React 문서의 새로운 보금자리인 react.dev를 출시하게 되어 기쁩니다. 이 글에서는 새로운 사이트에 대해 소개해 드리겠습니다.\n</BlogCard>\n\n\n<BlogCard title=\"React Labs: 그동안의 작업 – 2022년 6월\" date=\"2022년 6월 15일\" url=\"/blog/2022/06/15/react-labs-what-we-have-been-working-on-june-2022\">\nReact 18은 수년간의 준비 끝에 탄생한 버전으로 React 팀에게 귀중한 교훈을 가져다주었습니다. 수년간의 연구와 다양한 경로를 모색한 끝에 출시된 제품입니다. 그 경로 중 일부는 성공적이었지만 더 많은 경로가 막다른 골목에서 새로운 인사이트로 이어졌습니다. 우리가 얻은 한 가지 교훈은 우리가 탐색하고 있는 경로에 대한 인사이트를 공유받지 못한 채 새로운 기능을 기다리는 것은 커뮤니티에 실망감을 준다는 것입니다.\n</BlogCard>\n\n<BlogCard title=\"React v18.0\" date=\"2022년 3월 29일\" url=\"/blog/2022/03/29/react-v18\">\n이제 npm에서 React 18을 사용할 수 있습니다! 지난 포스팅에서는 앱을 React 18로 업그레이드하는 방법을 단계별로 공유했습니다. 이번 포스팅에서는 React 18의 새로운 기능과 미래에 어떤 의미를 갖는지에 대해 설명하겠습니다.\n</BlogCard>\n\n<BlogCard title=\"React 18로 업그레이드하는 방법\" date=\"2022년 3월 8일\" url=\"/blog/2022/03/08/react-18-upgrade-guide\">\nReact 18은 릴리스 노트에서 언급한 대로, 새로운 동시성 렌더러를 도입하여 기존 애플리케이션에 점진적으로 적용할 계획입니다. 이 글에서는 React 18로 업그레이드하는 방법을 단계별로 소개하겠습니다.\n</BlogCard>\n\n<BlogCard title=\"React Conf 2021 요약\" date=\"2021년 12월 17일\" url=\"/blog/2021/12/17/react-conf-2021-recap\">\n지난주, 6번째 React Conf를 개최했습니다. 지난 몇 년 동안 React Conf 무대를 통해 React Native, React Hook과 같은 업계 변화를 알리는 발표를 했습니다. 올해는 React 18의 출시 및 동시성 기능의 점진적 도입을 시작으로 React의 멀티 플랫폼 비전을 공유했습니다.\n</BlogCard>\n\n<BlogCard title=\"React 18에 대한 계획\" date=\"2021년 6월 8일\" url=\"/blog/2021/06/08/the-plan-for-react-18\">\nReact 팀은 몇 가지 업데이트를 공유하게 되어 기쁩니다.\n\n- 다음 주요 버전이 될 React 18 릴리즈에 대한 작업을 시작했습니다.\n- 커뮤니티가 React 18의 새로운 기능을 점진적으로 채택할 수 있도록 준비하기 위해 워킹 그룹을 만들었습니다.\n- 라이브러리 작성자가 사용해 보고 피드백을 제공할 수 있도록 React 18 Alpha를 게시했습니다.\n</BlogCard>\n\n<BlogCard title=\"제로 번들 사이즈 React 서버 컴포넌트를 소개합니다\" date=\"2020년 12월 21일\" url=\"/blog/2020/12/21/data-fetching-with-react-server-components\">\n2020년은 긴 한 해였습니다. 연말이 다가옴에 따라 제로 번들 사이즈의 React 서버 컴포넌트 연구에 대한 특별 연휴 업데이트를 공유하고자 합니다. React 서버 컴포넌트를 소개하기 위해 강연과 데모를 준비했습니다. 이는 연휴 기간 또는 새해에 업무가 재개되는 시점에 확인할 수 있습니다.\n</BlogCard>\n\n</div>\n\n---\n\n### 모든 릴리스 노트 {/*all-release-notes*/}\n\nReact의 모든 릴리스 내용이 별도의 블로그 게시글로 작성되지는 않지만, 모든 릴리스에 대한 세부 변경 내역은 React 저장소의 [`CHANGELOG.md`](https://github.com/facebook/react/blob/main/CHANGELOG.md) 파일 또는 [Releases](https://github.com/facebook/react/releases) 페이지에서 확인할 수 있습니다.\n\n---\n\n### 옛 게시글 {/*older-posts*/}\n\n[옛 게시글](https://reactjs.org/blog/all.html)을 확인해 보세요.\n\n<div className=\"h-12\"></div>\n"
  },
  {
    "path": "src/content/community/acknowledgements.md",
    "content": "---\ntitle: 감사의 말\n---\n\n<Intro>\n\nReact는 원래 [Jordan Walke](https://github.com/jordwalke)에 의해 만들어졌습니다. 오늘날 React에는 [작업을 전담하는 전임 팀](/community/team)과 천여 명이 넘는 [오픈 소스 기여자들](https://github.com/facebook/react/graphs/contributors)이 있습니다.\n\n</Intro>\n\n## 과거 기여자 {/*past-contributors*/}\n\n과거에 React와 그 문서에 상당한 기여를 하고 수년간 이를 유지 관리하는 데 도움을 준 분들을 소개합니다.\n\n* [Almero Steyn](https://github.com/AlmeroSteyn)\n* [Andreas Svensson](https://github.com/syranide)\n* [Alex Krolick](https://github.com/alexkrolick)\n* [Alexey Pyltsyn](https://github.com/lex111)\n* [Andrey Lunyov](https://github.com/alunyov)\n* [Brandon Dail](https://github.com/aweary)\n* [Brian Vaughn](https://github.com/bvaughn)\n* [Caleb Meredith](https://github.com/calebmer)\n* [Chang Yan](https://github.com/cyan33)\n* [Cheng Lou](https://github.com/chenglou)\n* [Christoph Nakazawa](https://github.com/cpojer)\n* [Christopher Chedeau](https://github.com/vjeux)\n* [Clement Hoang](https://github.com/clemmy)\n* [Dave McCabe](https://github.com/davidmccabe)\n* [Dominic Gannaway](https://github.com/trueadm)\n* [Flarnie Marchan](https://github.com/flarnie)\n* [Jason Quense](https://github.com/jquense)\n* [Jesse Beach](https://github.com/jessebeach)\n* [Jessica Franco](https://github.com/Jessidhia)\n* [Jim Sproch](https://github.com/jimfb)\n* [Josh Duck](https://github.com/joshduck)\n* [Joe Critchley](https://github.com/joecritch)\n* [Jeff Morrison](https://github.com/jeffmo)\n* [LuMir](https://github.com/lumirlumir)\n* [Luna Ruan](https://github.com/lunaruan)\n* [Luna Wei](https://github.com/lunaleaps)\n* [Noah Lemen](https://github.com/noahlemen)\n* [Kathryn Middleton](https://github.com/kmiddleton14)\n* [Keyan Zhang](https://github.com/keyz)\n* [Marco Salazar](https://github.com/salazarm)\n* [Mengdi Chen](https://github.com/mondaychen)\n* [Nat Alison](https://github.com/tesseralis)\n* [Nathan Hunzaker](https://github.com/nhunzaker)\n* [Nicolas Gallagher](https://github.com/necolas)\n* [Paul O'Shannessy](https://github.com/zpao)\n* [Pete Hunt](https://github.com/petehunt)\n* [Philipp Spiess](https://github.com/philipp-spiess)\n* [Rachel Nabors](https://github.com/rachelnabors)\n* [Robert Zhang](https://github.com/robertzhidealx)\n* [Samuel Susla](https://github.com/sammy-SC)\n* [Sander Spies](https://github.com/sanderspies)\n* [Sasha Aickin](https://github.com/aickin)\n* [Sathya Gunasekaran](https://github.com/gsathya)\n* [Sophia Shoemaker](https://github.com/mrscobbler)\n* [Sunil Pai](https://github.com/threepointone)\n* [Tianyu Yao](https://github.com/)\n* [Tim Yung](https://github.com/yungsters)\n* [Xuan Huang](https://github.com/huxpro)\n\n이 목록이 전체 목록은 아닙니다.\n\n수년간 지도와 지원을 주신 [Tom Occhino](https://github.com/tomocchino)와 [Adam Wolff](https://github.com/wolffiex)에게 특별한 감사를 드립니다. [React를 다른 언어로 번역한](https://translations.react.dev/) [루밀(LuMir)](https://github.com/lumirlumir) 및 모든 자원봉사자에게도 감사드립니다.\n\n## 특별한 감사 인사 {/*additional-thanks*/}\n\n뿐만 아니라, 다음 분들도 감사드립니다.\n\n* npm에서 `react` package 이름을 사용하도록 허락한 [Jeff Barczewski](https://github.com/jeffbski)\n* reactjs.com 도메인명과 트위터의 [@reactjs](https://twitter.com/reactjs) 사용자 아이디를 양보한 [Christopher Aue](https://christopheraue.net/)\n* npm에서 [flux](https://www.npmjs.com/package/flux) package 이름을 사용하도록 한 [ProjectMoon](https://github.com/ProjectMoon)\n* GitHub의 [react](https://github.com/react) 조직을 사용하게 한 Shane Anderson\n"
  },
  {
    "path": "src/content/community/conferences.md",
    "content": "---\ntitle: React 컨퍼런스\n---\n\n<Intro>\nReact.js 관련 컨퍼런스를 알고 계신가요? 이곳에 추가해주세요! (목록은 날짜 순으로 정렬해주세요)\n\n</Intro>\n\n## 예정 컨퍼런스 {/*upcoming-conferences*/}\n\n### React Paris 2026 {/*react-paris-2026*/}\nMarch 26 - 27, 2026. In-person in Paris, France (hybrid event)\n\n[Website](https://react.paris/) - [Twitter](https://x.com/BeJS_)\n\n### CityJS London 2026 {/*cityjs-london-2026*/}\nApril 14-17, 2026. In-person in London\n\n[Website](https://india.cityjsconf.org/) - [Twitter](https://x.com/cityjsconf) - [Bluesky](https://bsky.app/profile/cityjsconf.bsky.social)\n\n### ZurichJS Conf 2026 {/*zurichjs-conf-2026*/}\nSeptember 10-11, 2026. In-person in Zurich, Switzerland\n\n[Website](https://conf.zurichjs.com?utm_campaign=ZurichJS_Conf&utm_source=referral&utm_content=reactjs_community_conferences) - [Twitter](https://x.com/zurichjs) - [LinkedIn](https://www.linkedin.com/company/zurichjs/)\n\n## Past Conferences {/*past-conferences*/}\n\n### CityJS New Delhi 2026 {/*cityjs-newdelhi-2026*/}\nFebruary 12-13, 2026. In-person in New Delhi, India\n\n[Website](https://india.cityjsconf.org/) - [Twitter](https://x.com/cityjsconf) - [Bluesky](https://bsky.app/profile/cityjsconf.bsky.social)\n\n### CityJS Singapore 2026 {/*cityjs-singapore-2026*/}\nFebruary 4-6, 2026. In-person in Singapore\n\n[Website](https://india.cityjsconf.org/) - [Twitter](https://x.com/cityjsconf) - [Bluesky](https://bsky.app/profile/cityjsconf.bsky.social)\n\n### React Advanced London 2025 {/*react-advanced-london-2025*/}\nNovember 28 & December 1, 2025. In-person in London, UK + online (hybrid event)\n\n[Website](https://reactadvanced.com/) - [Twitter](https://x.com/reactadvanced)\n\n### React Summit US 2025 {/*react-summit-us-2025*/}\nNovember 18 - 21, 2025. In-person in New York, USA + remote (hybrid event)\n\n[Website](https://reactsummit.us/) - [Twitter](https://x.com/reactsummit)\n\n### React India 2025 {/*react-india-2025*/}\nOctober 31 - November 01, 2025. In-person in Goa, India (hybrid event) + Oct 15 2025 - remote day\n\n[Website](https://www.reactindia.io) - [Twitter](https://twitter.com/react_india) - [Facebook](https://www.facebook.com/ReactJSIndia) - [Youtube](https://www.youtube.com/channel/UCaFbHCBkPvVv1bWs_jwYt3w)\n\n### React Conf 2025 {/*react-conf-2025*/}\nOctober 7-8, 2025. Henderson, Nevada, USA and free livestream\n\n[Website](https://conf.react.dev/) - [Twitter](https://x.com/reactjs) - [Bluesky](https://bsky.app/profile/react.dev)\n\n### RenderCon Kenya 2025 {/*rendercon-kenya-2025*/}\nOctober 04, 2025. Nairobi, Kenya\n\n[Website](https://rendercon.org/) - [Twitter](https://twitter.com/renderconke) - [LinkedIn](https://www.linkedin.com/company/renderconke/) - [YouTube](https://www.youtube.com/channel/UC0bCcG8gHUL4njDOpQGcMIA)\n\n### React Alicante 2025 {/*react-alicante-2025*/}\nOctober 2-4, 2025. Alicante, Spain.\n\n[Website](https://reactalicante.es/) - [Twitter](https://x.com/ReactAlicante) - [Bluesky](https://bsky.app/profile/reactalicante.es) - [YouTube](https://www.youtube.com/channel/UCaSdUaITU1Cz6PvC97A7e0w)\n\n### React Universe Conf 2025 {/*react-universe-conf-2025*/}\nSeptember 2-4, 2025. Wrocław, Poland.\n\n[Website](https://www.reactuniverseconf.com/) - [Twitter](https://twitter.com/react_native_eu) - [LinkedIn](https://www.linkedin.com/events/reactuniverseconf7163919537074118657/)\n\n### React Nexus 2025 {/*react-nexus-2025*/}\nJuly 03 - 05, 2025. In-person in Bangalore, India\n\n[Website](https://reactnexus.com/) - [Twitter](https://x.com/ReactNexus) - [Bluesky](https://bsky.app/profile/reactnexus.com) - [Linkedin](https://www.linkedin.com/company/react-nexus) - [YouTube](https://www.youtube.com/reactify_in)\n\n### React Summit 2025 {/*react-summit-2025*/}\nJune 13 - 17, 2025. In-person in Amsterdam, Netherlands + remote (hybrid event)\n\n[Website](https://reactsummit.com/) - [Twitter](https://x.com/reactsummit)\n\n### React Norway 2025 {/*react-norway-2025*/}\nJune 13, 2025. In-person in Oslo, Norway + remote (virtual event)\n\n[Website](https://reactnorway.com/) - [Twitter](https://x.com/ReactNorway)\n\n### CityJS Athens 2025 {/*cityjs-athens*/}\nMay 27 - 31, 2025. In-person in Athens, Greece\n\n[Website](https://athens.cityjsconf.org/) - [Twitter](https://x.com/cityjsconf) - [Bluesky](https://bsky.app/profile/cityjsconf.bsky.social)\n\n### App.js Conf 2025 {/*appjs-conf-2025*/}\nMay 28 - 30, 2025. In-person in Kraków, Poland + remote\n\n[Website](https://appjs.co) - [Twitter](https://twitter.com/appjsconf)\n\n### CityJS London 2025 {/*cityjs-london*/}\nApril 23 - 25, 2025. In-person in London, UK\n\n[Website](https://london.cityjsconf.org/) - [Twitter](https://x.com/cityjsconf) -  [Bluesky](https://bsky.app/profile/cityjsconf.bsky.social)\n\n### React Paris 2025 {/*react-paris-2025*/}\nMarch 20 - 21, 2025. In-person in Paris, France (hybrid event)\n\n[Website](https://react.paris/) - [Twitter](https://x.com/BeJS_) - [YouTube](https://www.youtube.com/playlist?list=PL53Z0yyYnpWitP8Zv01TSEQmKLvuRh_Dj)\n\n### React Native Connection 2025 {/*react-native-connection-2025*/}\nApril 3 (Reanimated Training) + April 4 (Conference), 2025. Paris, France.\n\n[Website](https://reactnativeconnection.io/) - [X](https://x.com/reactnativeconn) - [Bluesky](https://bsky.app/profile/reactnativeconnect.bsky.social)\n\n### React Day Berlin 2024 {/*react-day-berlin-2024*/}\nDecember 13 & 16, 2024. In-person in Berlin, Germany + remote (hybrid event)\n\n[Website](https://reactday.berlin/) - [Twitter](https://x.com/reactdayberlin)\n\n### React Africa 2024 {/*react-africa-2024*/}\nNovember 29, 2024. In-person in Casablanca, Morocco (hybrid event)\n\n[Website](https://react-africa.com/) - [Twitter](https://x.com/BeJS_)\n\n### React Summit US 2024 {/*react-summit-us-2024*/}\nNovember 19 & 22, 2024. In-person in New York, USA + online (hybrid event)\n\n[Website](https://reactsummit.us/) - [Twitter](https://twitter.com/reactsummit) - [Videos](https://portal.gitnation.org/)\n\n### React Native London Conf 2024 {/*react-native-london-2024*/}\nNovember 14 & 15, 2024. In-person in London, UK\n\n[Website](https://reactnativelondon.co.uk/) - [Twitter](https://x.com/RNLConf)\n\n### React Advanced London 2024 {/*react-advanced-london-2024*/}\nOctober 25 & 28, 2024. In-person in London, UK + online (hybrid event)\n\n[Website](https://reactadvanced.com/) - [Twitter](https://x.com/reactadvanced)\n\n### reactjsday 2024 {/*reactjsday-2024*/}\nOctober 25, 2024. In-person in Verona, Italy + online (hybrid event)\n\n[Website](https://2024.reactjsday.it/) - [Twitter](https://x.com/reactjsday) - [Facebook](https://www.facebook.com/GrUSP/) - [YouTube](https://www.youtube.com/c/grusp)\n\n### React Brussels 2024 {/*react-brussels-2024*/}\nOctober 18, 2024. In-person in Brussels, Belgium (hybrid event)\n\n[Website](https://www.react.brussels/) - [Twitter](https://x.com/BrusselsReact) - [YouTube](https://www.youtube.com/playlist?list=PL53Z0yyYnpWimQ0U75woee2zNUIFsiDC3)\n\n### React India 2024 {/*react-india-2024*/}\nOctober 17 - 19, 2024. In-person in Goa, India (hybrid event) + Oct 15 2024 - remote day\n\n[Website](https://www.reactindia.io) - [Twitter](https://twitter.com/react_india) - [Facebook](https://www.facebook.com/ReactJSIndia) - [Youtube](https://www.youtube.com/channel/UCaFbHCBkPvVv1bWs_jwYt3w)\n\n### RenderCon Kenya 2024 {/*rendercon-kenya-2024*/}\nOctober 04 - 05, 2024. Nairobi, Kenya\n\n[Website](https://rendercon.org/) - [Twitter](https://twitter.com/renderconke) - [LinkedIn](https://www.linkedin.com/company/renderconke/) - [YouTube](https://www.youtube.com/channel/UC0bCcG8gHUL4njDOpQGcMIA)\n\n### React Alicante 2024 {/*react-alicante-2024*/}\nSeptember 19-21, 2024. Alicante, Spain.\n\n[Website](https://reactalicante.es/) - [Twitter](https://twitter.com/ReactAlicante) - [YouTube](https://www.youtube.com/channel/UCaSdUaITU1Cz6PvC97A7e0w)\n\n### React Universe Conf 2024 {/*react-universe-conf-2024*/}\nSeptember 5-6, 2024. Wrocław, Poland.\n\n[Website](https://www.reactuniverseconf.com/) - [Twitter](https://twitter.com/react_native_eu) - [LinkedIn](https://www.linkedin.com/events/reactuniverseconf7163919537074118657/)\n\n\n### React Rally 2024 🐙 {/*react-rally-2024*/}\nAugust 12-13, 2024. Park City, UT, USA\n\n[Website](https://reactrally.com) - [Twitter](https://twitter.com/ReactRally) - [YouTube](https://www.youtube.com/channel/UCXBhQ05nu3L1abBUGeQ0ahw)\n\n### The Geek Conf 2024 {/*the-geek-conf-2024*/}\nJuly 25, 2024. In-person in Berlin, Germany + remote (hybrid event)\n\n[Website](https://thegeekconf.com) - [Twitter](https://twitter.com/thegeekconf)\n\n### Chain React 2024 {/*chain-react-2024*/}\nJuly 17-19, 2024. In-person in Portland, OR, USA\n\n[Website](https://chainreactconf.com) - [Twitter](https://twitter.com/ChainReactConf)\n\n### React Nexus 2024 {/*react-nexus-2024*/}\nJuly 04 & 05, 2024. Bangalore, India (In-person event)\n\n[Website](https://reactnexus.com/) - [Twitter](https://twitter.com/ReactNexus) - [Linkedin](https://www.linkedin.com/company/react-nexus) - [YouTube](https://www.youtube.com/reactify_in)\n\n### React Summit 2024 {/*react-summit-2024*/}\nJune 14 & 18, 2024. In-person in Amsterdam, Netherlands + remote (hybrid event)\n\n[Website](https://reactsummit.com/) - [Twitter](https://twitter.com/reactsummit) - [Videos](https://portal.gitnation.org/)\n\n### React Norway 2024 {/*react-norway-2024*/}\nJune 14, 2024. In-person at Farris Bad Hotel in Larvik, Norway and online (hybrid event).\n\n[Website](https://reactnorway.com/) - [Twitter](https://twitter.com/ReactNorway)\n\n### Render(ATL) 2024 🍑 {/*renderatl-2024-*/}\nJune 12 - June 14, 2024. Atlanta, GA, USA\n\n[Website](https://renderatl.com) - [Discord](https://www.renderatl.com/discord) - [Twitter](https://twitter.com/renderATL) - [Instagram](https://www.instagram.com/renderatl/) - [Facebook](https://www.facebook.com/renderatl/) - [LinkedIn](https://www.linkedin.com/company/renderatl) - [Podcast](https://www.renderatl.com/culture-and-code#/)\n\n### Frontend Nation 2024 {/*frontend-nation-2024*/}\nJune 4 - 7, 2024. Online\n\n[Website](https://frontendnation.com/) - [Twitter](https://twitter.com/frontendnation)\n\n### App.js Conf 2024 {/*appjs-conf-2024*/}\nMay 22 - 24, 2024. In-person in Kraków, Poland + remote\n\n[Website](https://appjs.co) - [Twitter](https://twitter.com/appjsconf)\n\n### React Conf 2024 {/*react-conf-2024*/}\nMay 15 - 16, 2024. In-person in Henderson, NV, USA + remote\n\n[Website](https://conf.react.dev) - [Twitter](https://twitter.com/reactjs)\n\n### React Native Connection 2024 {/*react-native-connection-2024*/}\nApril 23, 2024. In-person in Paris, France\n\n[Website](https://reactnativeconnection.io/) - [Twitter](https://twitter.com/ReactNativeConn)\n\n### React Miami 2024 {/*react-miami-2024*/}\nApril 19 - 20, 2024. In-person in Miami, FL, USA\n\n[Website](https://reactmiami.com/) - [Twitter](https://twitter.com/ReactMiamiConf)\n\n### Epic Web Conf 2024 {/*epic-web-2024*/}\nApril 10 - 11, 2024. In-person in Park City, UT, USA\n\n[Website](https://www.epicweb.dev/conf) - [YouTube](https://www.youtube.com/@EpicWebDev)\n\n### React Paris 2024 {/*react-paris-2024*/}\nMarch 22, 2024. In-person in Paris, France + Remote (hybrid)\n\n[Website](https://react.paris/) - [Twitter](https://twitter.com/BeJS_) - [LinkedIn](https://www.linkedin.com/events/7150816372074192900/comments/) - [Videos](https://www.youtube.com/playlist?list=PL53Z0yyYnpWhUzgvr2Nys3kZBBLcY0TA7)\n\n### React Day Berlin 2023 {/*react-day-berlin-2023*/}\nDecember 8 & 12, 2023. In-person in Berlin, Germany + remote first interactivity (hybrid event)\n\n[Website](https://reactday.berlin) - [Twitter](https://twitter.com/reactdayberlin) - [Facebook](https://www.facebook.com/reactdayberlin/) - [Videos](https://portal.gitnation.org/events/react-day-berlin-2023)\n\n### React Summit US 2023 {/*react-summit-us-2023*/}\nNovember 13 & 15, 2023. In-person in New York, US + remote first interactivity (hybrid event)\n\n[Website](https://reactsummit.us) - [Twitter](https://twitter.com/reactsummit) - [Facebook](https://www.facebook.com/reactamsterdam) - [Videos](https://portal.gitnation.org/events/react-summit-us-2023)\n\n### reactjsday 2023 {/*reactjsday-2023*/}\nOctober 27th 2023. In-person in Verona, Italy and online (hybrid event)\n\n[Website](https://2023.reactjsday.it/) - [Twitter](https://twitter.com/reactjsday) - [Facebook](https://www.facebook.com/GrUSP/) - [YouTube](https://www.youtube.com/c/grusp)\n\n### React Advanced 2023 {/*react-advanced-2023*/}\nOctober 20 & 23, 2023. In-person in London, UK + remote first interactivity (hybrid event)\n\n[Website](https://www.reactadvanced.com/) - [Twitter](https://twitter.com/ReactAdvanced) - [Facebook](https://www.facebook.com/ReactAdvanced) - [Videos](https://portal.gitnation.org/events/react-advanced-conference-2023)\n\n### React Brussels 2023 {/*react-brussels-2023*/}\nOctober 13th 2023. In-person in Brussels, Belgium + Remote (hybrid)\n\n[Website](https://www.react.brussels/) - [Twitter](https://twitter.com/BrusselsReact) - [Videos](https://www.youtube.com/playlist?list=PL53Z0yyYnpWh85KeMomUoVz8_brrmh_aC)\n\n### React India 2023 {/*react-india-2023*/}\nOctober 5 - 7, 2023. In-person in Goa, India (hybrid event) + Oct 3 2023 - remote day\n\n[Website](https://www.reactindia.io) - [Twitter](https://x.com/react_india) - [Facebook](https://www.facebook.com/ReactJSIndia) - [Youtube](https://www.youtube.com/channel/UCaFbHCBkPvVv1bWs_jwYt3w)\n\n### RenderCon Kenya 2023 {/*rendercon-kenya-2023*/}\nSeptember 29 - 30, 2023. Nairobi, Kenya\n\n[Website](https://rendercon.org/) - [Twitter](https://twitter.com/renderconke) - [LinkedIn](https://www.linkedin.com/company/renderconke/) - [YouTube](https://www.youtube.com/channel/UC0bCcG8gHUL4njDOpQGcMIA)\n\n### React Live 2023 {/*react-live-2023*/}\nSeptember 29, 2023. Amsterdam, Netherlands\n\n[Website](https://reactlive.nl/)\n\n### React Alicante 2023 {/*react-alicante-2023*/}\nSeptember 28 - 30, 2023. Alicante, Spain\n\n[Website](https://reactalicante.es/) - [Twitter](https://twitter.com/reactalicante)\n\n### RedwoodJS Conference 2023 {/*redwoodjs-conference-2023*/}\nSeptember 26 - 29, 2023. Grants Pass, Oregon + remote (hybrid event)\n\n[Website](https://www.redwoodjsconf.com/) - [Twitter](https://twitter.com/redwoodjs)\n\n### React Native EU 2023 {/*react-native-eu-2023*/}\nSeptember 7 & 8, 2023. Wrocław, Poland\n\n[Website](https://react-native.eu) - [Twitter](https://twitter.com/react_native_eu) - [Facebook](https://www.facebook.com/reactnativeeu)\n\n### React Rally 2023 🐙 {/*react-rally-2023*/}\nAugust 17 & 18, 2023. Salt Lake City, UT, USA\n\n[Website](https://www.reactrally.com/) - [Twitter](https://twitter.com/ReactRally) - [Instagram](https://www.instagram.com/reactrally/)\n\n### React Nexus 2023 {/*react-nexus-2023*/}\nJuly 07 & 08, 2023. Bangalore, India (In-person event)\n\n[Website](https://reactnexus.com/) - [Twitter](https://twitter.com/ReactNexus) - [Linkedin](https://www.linkedin.com/company/react-nexus) - [YouTube](https://www.youtube.com/reactify_in)\n\n### ReactNext 2023 {/*reactnext-2023*/}\nJune 27th, 2023. Tel Aviv, Israel\n\n[Website](https://www.react-next.com/) - [Facebook](https://www.facebook.com/ReactNextConf) - [Youtube](https://www.youtube.com/@ReactNext)\n\n### React Norway 2023 {/*react-norway-2023*/}\nJune 16th, 2023. Larvik, Norway\n\n[Website](https://reactnorway.com/) - [Twitter](https://twitter.com/ReactNorway/) - [Facebook](https://www.facebook.com/reactdaynorway/)\n\n### React Summit 2023 {/*react-summit-2023*/}\nJune 2 & 6, 2023. In-person in Amsterdam, Netherlands + remote first interactivity (hybrid event)\n\n[Website](https://reactsummit.com) - [Twitter](https://twitter.com/reactsummit) - [Facebook](https://www.facebook.com/reactamsterdam) - [Videos](https://portal.gitnation.org/events/react-summit-2023)\n\n### Render(ATL) 2023 🍑 {/*renderatl-2023-*/}\nMay 31 - June 2, 2023. Atlanta, GA, USA\n\n[Website](https://renderatl.com) - [Discord](https://www.renderatl.com/discord) - [Twitter](https://twitter.com/renderATL) - [Instagram](https://www.instagram.com/renderatl/) - [Facebook](https://www.facebook.com/renderatl/) - [LinkedIn](https://www.linkedin.com/company/renderatl) - [Podcast](https://www.renderatl.com/culture-and-code#/)\n\n### Chain React 2023 {/*chain-react-2023*/}\nMay 17 - 19, 2023. Portland, OR, USA\n\n[Website](https://chainreactconf.com/) - [Twitter](https://twitter.com/ChainReactConf) - [Facebook](https://www.facebook.com/ChainReactConf/) - [Youtube](https://www.youtube.com/channel/UCwpSzVt7QpLDbCnPXqR97-g/playlists)\n\n### App.js Conf 2023 {/*appjs-conf-2023*/}\nMay 10 - 12, 2023. In-person in Kraków, Poland + remote\n\n[Website](https://appjs.co) - [Twitter](https://twitter.com/appjsconf)\n\n### RemixConf 2023 {/*remixconf-2023*/}\nMay, 2023. Salt Lake City, UT\n\n[Website](https://remix.run/conf/2023) - [Twitter](https://twitter.com/remix_run)\n\n### Reactathon 2023 {/*reactathon-2023*/}\nMay 2 - 3, 2023. San Francisco, CA, USA\n\n[Website](https://reactathon.com) - [Twitter](https://twitter.com/reactathon) - [YouTube](https://www.youtube.com/realworldreact)\n\n### React Miami 2023 {/*react-miami-2023*/}\nApril 20 - 21, 2023. Miami, FL, USA\n\n[Website](https://www.reactmiami.com/) - [Twitter](https://twitter.com/ReactMiamiConf)\n\n### React Day Berlin 2022 {/*react-day-berlin-2022*/}\nDecember 2, 2022. In-person in Berlin, Germany + remote (hybrid event)\n\n[Website](https://reactday.berlin) - [Twitter](https://twitter.com/reactdayberlin) - [Facebook](https://www.facebook.com/reactdayberlin/) - [Videos](https://www.youtube.com/c/ReactConferences)\n\n### React Global Online Summit 22.2 by Geekle {/*react-global-online-summit-222-by-geekle*/}\nNovember 8 - 9, 2022 - Online Summit\n\n[Website](https://events.geekle.us/react3/) - [LinkedIn](https://www.linkedin.com/posts/geekle-us_event-react-reactjs-activity-6964904611207864320-gpDx?utm_source=share&utm_medium=member_desktop)\n\n### Remix Conf Europe 2022 {/*remix-conf-europe-2022*/}\nNovember 18, 2022, 7am PST / 10am EST / 4pm CET - remote event\n\n[Website](https://remixconf.eu/) - [Twitter](https://twitter.com/remixconfeu) - [Videos](https://portal.gitnation.org/events/remix-conf-europe-2022)\n\n### React Advanced 2022 {/*react-advanced-2022*/}\nOctober 21 & 25, 2022. In-person in London, UK + remote (hybrid event)\n\n[Website](https://www.reactadvanced.com/) - [Twitter](https://twitter.com/ReactAdvanced) - [Facebook](https://www.facebook.com/ReactAdvanced) - [Videos](https://portal.gitnation.org/events/react-advanced-conference-2022)\n\n### ReactJS Day 2022 {/*reactjs-day-2022*/}\nOctober 21, 2022 in Verona, Italy\n\n[Website](https://2022.reactjsday.it/) - [Twitter](https://twitter.com/reactjsday) - [LinkedIn](https://www.linkedin.com/company/grusp/) - [Facebook](https://www.facebook.com/reactjsday/) - [Videos](https://www.youtube.com/c/grusp)\n\n### React Brussels 2022 {/*react-brussels-2022*/}\nOctober 14, 2022. In-person in Brussels, Belgium + remote (hybrid event)\n\n[Website](https://www.react.brussels/) - [Twitter](https://twitter.com/BrusselsReact) - [LinkedIn](https://www.linkedin.com/events/6938421827153088512/) - [Facebook](https://www.facebook.com/events/1289080838167252/) - [Videos](https://www.youtube.com/channel/UCvES7lMpnx-t934qGxD4w4g)\n\n### React Alicante 2022 {/*react-alicante-2022*/}\nSeptember 29 - October 1, 2022. In-person in Alicante, Spain + remote (hybrid event)\n\n[Website](https://reactalicante.es/) - [Twitter](https://twitter.com/reactalicante) - [Facebook](https://www.facebook.com/ReactAlicante) - [Videos](https://www.youtube.com/channel/UCaSdUaITU1Cz6PvC97A7e0w)\n### React India 2022 {/*react-india-2022*/}\nSeptember 22 - 24, 2022. In-person in Goa, India + remote (hybrid event)\n\n[Website](https://www.reactindia.io) - [Twitter](https://twitter.com/react_india) - [Facebook](https://www.facebook.com/ReactJSIndia) - [Videos](https://www.youtube.com/channel/UCaFbHCBkPvVv1bWs_jwYt3w)\n\n### React Finland 2022 {/*react-finland-2022*/}\nSeptember 12 - 16, 2022. In-person in Helsinki, Finland\n\n[Website](https://react-finland.fi/) - [Twitter](https://twitter.com/ReactFinland) - [Schedule](https://react-finland.fi/schedule/) - [Speakers](https://react-finland.fi/speakers/)\n\n### React Native EU 2022: Powered by callstack {/*react-native-eu-2022-powered-by-callstack*/}\nSeptember 1-2, 2022 - Remote event\n\n[Website](https://www.react-native.eu/?utm_campaign=React_Native_EU&utm_source=referral&utm_content=reactjs_community_conferences) -\n[Twitter](https://twitter.com/react_native_eu) -\n[Linkedin](https://www.linkedin.com/showcase/react-native-eu) -\n[Facebook](https://www.facebook.com/reactnativeeu/) -\n[Instagram](https://www.instagram.com/reactnative_eu/)\n\n### ReactNext 2022 {/*reactnext-2022*/}\nJune 28, 2022. Tel-Aviv, Israel\n\n[Website](https://react-next.com) - [Twitter](https://twitter.com/ReactNext) - [Videos](https://www.youtube.com/c/ReactNext)\n\n### React Norway 2022 {/*react-norway-2022*/}\nJune 24, 2022. In-person at Farris Bad Hotel in Larvik, Norway and online (hybrid event).\n\n[Website](https://reactnorway.com/) - [Twitter](https://twitter.com/ReactNorway)\n\n### React Summit 2022 {/*react-summit-2022*/}\nJune 17 & 21, 2022. In-person in Amsterdam, Netherlands + remote first interactivity (hybrid event)\n\n[Website](https://reactsummit.com) - [Twitter](https://twitter.com/reactsummit) - [Facebook](https://www.facebook.com/reactamsterdam) - [Videos](https://portal.gitnation.org/events/react-summit-2022)\n\n### App.js Conf 2022 {/*appjs-conf-2022*/}\nJune 8 - 10, 2022. In-person in Kraków, Poland + remote\n\n[Website](https://appjs.co) - [Twitter](https://twitter.com/appjsconf)\n\n### React Day Bangalore 2022 {/*react-day-bangalore-2022*/}\nJune 8 - 9, 2022. Remote\n\n[Website](https://reactday.in/) - [Twitter](https://twitter.com/ReactDayIn) - [Linkedin](https://www.linkedin.com/company/react-day/) - [YouTube](https://www.youtube.com/reactify_in)\n\n### render(ATL) 2022 🍑 {/*renderatl-2022-*/}\nJune 1 - 4, 2022. Atlanta, GA, USA\n\n[Website](https://renderatl.com) - [Discord](https://www.renderatl.com/discord) - [Twitter](https://twitter.com/renderATL) - [Instagram](https://www.instagram.com/renderatl/) - [Facebook](https://www.facebook.com/renderatl/) - [LinkedIn](https://www.linkedin.com/company/renderatl) - [Podcast](https://www.renderatl.com/culture-and-code#/)\n\n### RemixConf 2022 {/*remixconf-2022*/}\nMay 24 - 25, 2022. Salt Lake City, UT\n\n[Website](https://remix.run/conf/2022) - [Twitter](https://twitter.com/remix_run) - [YouTube](https://www.youtube.com/playlist?list=PLXoynULbYuEC36XutMMWEuTu9uuh171wx)\n\n### Reactathon 2022 {/*reactathon-2022*/}\nMay 3 - 5, 2022. Berkeley, CA\n\n[Website](https://reactathon.com) - [Twitter](https://twitter.com/reactathon) -[YouTube](https://www.youtube.com/watch?v=-YG5cljNXIA)\n\n### React Global Online Summit 2022 by Geekle {/*react-global-online-summit-2022-by-geekle*/}\nApril 20 - 21, 2022 - Online Summit\n\n[Website](https://events.geekle.us/react2/) - [LinkedIn](https://www.linkedin.com/events/reactglobalonlinesummit-226887417664541614081/)\n\n### React Miami 2022 🌴 {/*react-miami-2022-*/}\nApril 18 - 19, 2022. Miami, Florida\n[Website](https://www.reactmiami.com/)\n\n### React Live 2022 {/*react-live-2022*/}\nApril 1, 2022. Amsterdam, The Netherlands\n\n[Website](https://www.reactlive.nl/) - [Twitter](https://twitter.com/reactlivenl)\n\n### AgentConf 2022 {/*agentconf-2022*/}\n\nJanuary 27 - 30, 2022. In-person in Dornbirn and Lech Austria\n\n[Website](https://agent.sh/) - [Twitter](https://twitter.com/AgentConf) - [Instagram](https://www.instagram.com/teamagent/)\n\n### React Conf 2021 {/*react-conf-2021*/}\nDecember 8, 2021 - remote event (replay event on December 9)\n\n[Website](https://conf.reactjs.org/)\n\n### ReactEurope 2021 {/*reacteurope-2021*/}\nDecember 9-10, 2021 - remote event\n\n[Videos](https://www.youtube.com/c/ReacteuropeOrgConf)\n\n### ReactNext 2021 {/*reactnext-2021*/}\nDecember 15, 2021. Tel-Aviv, Israel\n\n[Website](https://react-next.com) - [Twitter](https://twitter.com/ReactNext) - [Videos](https://www.youtube.com/channel/UC3BT8hh3yTTYxbLQy_wbk2w)\n\n### React India 2021 {/*react-india-2021*/}\nNovember 12-13, 2021 - remote event\n\n[Website](https://www.reactindia.io) - [Twitter](https://twitter.com/react_india) - [Facebook](https://www.facebook.com/ReactJSIndia/) - [LinkedIn](https://www.linkedin.com/showcase/14545585) - [YouTube](https://www.youtube.com/channel/UCaFbHCBkPvVv1bWs_jwYt3w/videos)\n\n### React Global by Geekle {/*react-global-by-geekle*/}\nNovember 3-4, 2021 - remote event\n\n[Website](https://geekle.us/react) - [LinkedIn](https://www.linkedin.com/events/javascriptglobalsummit6721691514176720896/) - [YouTube](https://www.youtube.com/watch?v=0HhWIvPhbu0)\n\n### React Advanced 2021 {/*react-advanced-2021*/}\nOctober 22-23, 2021. In-person in London, UK + remote (hybrid event)\n\n[Website](https://reactadvanced.com) - [Twitter](https://twitter.com/reactadvanced) - [Facebook](https://www.facebook.com/ReactAdvanced) - [Videos](https://youtube.com/c/ReactConferences)\n\n### React Conf Brasil 2021 {/*react-conf-brasil-2021*/}\nOctober 16, 2021 - remote event\n\n[Website](http://reactconf.com.br) - [Twitter](https://twitter.com/reactconfbr) - [Slack](https://react.now.sh) - [Facebook](https://facebook.com/reactconf) - [Instagram](https://instagram.com/reactconfbr) - [YouTube](https://www.youtube.com/channel/UCJL5eorStQfC0x1iiWhvqPA/videos)\n\n### React Brussels 2021 {/*react-brussels-2021*/}\nOctober 15, 2021 - remote event\n\n[Website](https://www.react.brussels/) - [Twitter](https://twitter.com/BrusselsReact) - [LinkedIn](https://www.linkedin.com/events/6805708233819336704/)\n\n### render(ATL) 2021 {/*renderatl-2021*/}\nSeptember 13-15, 2021. Atlanta, GA, USA\n\n[Website](https://renderatl.com) - [Twitter](https://twitter.com/renderATL) - [Instagram](https://www.instagram.com/renderatl/) - [Facebook](https://www.facebook.com/renderatl/) - [LinkedIn](https://www.linkedin.com/company/renderatl)\n\n### React Native EU 2021 {/*react-native-eu-2021*/}\nSeptember 1-2, 2021 - remote event\n\n[Website](https://www.react-native.eu/) - [Twitter](https://twitter.com/react_native_eu) - [Facebook](https://www.facebook.com/reactnativeeu/) - [Instagram](https://www.instagram.com/reactnative_eu/)\n\n### React Finland 2021 {/*react-finland-2021*/}\nAugust 30 - September 3, 2021 - remote event\n\n[Website](https://react-finland.fi/) - [Twitter](https://twitter.com/ReactFinland) - [LinkedIn](https://www.linkedin.com/company/react-finland/)\n\n### React Case Study Festival 2021 {/*react-case-study-festival-2021*/}\nApril 27-28, 2021 - remote event\n\n[Website](https://link.geekle.us/react/offsite) - [LinkedIn](https://www.linkedin.com/events/reactcasestudyfestival6721300943411015680/) - [Facebook](https://www.facebook.com/events/255715435820203)\n\n### React Summit - Remote Edition 2021 {/*react-summit---remote-edition-2021*/}\nApril 14-16, 2021, 7am PST / 10am EST / 4pm CEST - remote event\n\n[Website](https://remote.reactsummit.com) - [Twitter](https://twitter.com/reactsummit) - [Facebook](https://www.facebook.com/reactamsterdam) - [Videos](https://portal.gitnation.org/events/react-summit-remote-edition-2021)\n\n### React fwdays'21 {/*react-fwdays21*/}\nMarch 27, 2021 - remote event\n\n[Website](https://fwdays.com/en/event/react-fwdays-2021) - [Twitter](https://twitter.com/fwdays) - [Facebook](https://www.facebook.com/events/1133828147054286) - [LinkedIn](https://www.linkedin.com/events/reactfwdays-21onlineconference6758046347334582273) - [Meetup](https://www.meetup.com/ru-RU/Fwdays/events/275764431/)\n\n### React Next 2020 {/*react-next-2020*/}\nDecember 1-2, 2020 - remote event\n\n[Website](https://react-next.com/) - [Twitter](https://twitter.com/reactnext) - [Facebook](https://www.facebook.com/ReactNext2016/)\n\n### React Conf Brasil 2020 {/*react-conf-brasil-2020*/}\nNovember 21, 2020 - remote event\n\n[Website](https://reactconf.com.br/) - [Twitter](https://twitter.com/reactconfbr) - [Slack](https://react.now.sh/)\n\n### React Summit 2020 {/*react-summit-2020*/}\nOctober 15-16, 2020, 7am PST / 10am EST / 4pm CEST - remote event\n\n[Website](https://reactsummit.com) - [Twitter](https://twitter.com/reactsummit) - [Facebook](https://www.facebook.com/reactamsterdam) - [Videos](https://youtube.com/c/ReactConferences)\n\n### React Native EU 2020 {/*react-native-eu-2020*/}\nSeptember 3-4, 2020 - remote event\n\n[Website](https://www.react-native.eu/) - [Twitter](https://twitter.com/react_native_eu) - [Facebook](https://www.facebook.com/reactnativeeu/) - [YouTube](https://www.youtube.com/watch?v=m0GfmlGFh3E&list=PLZ3MwD-soTTHy9_88QPLF8DEJkvoB5Tl-) - [Instagram](https://www.instagram.com/reactnative_eu/)\n\n### ReactEurope 2020 {/*reacteurope-2020*/}\nMay 14-15, 2020 in Paris, France\n\n[Videos](https://www.youtube.com/c/ReacteuropeOrgConf)\n\n### Byteconf React 2020 {/*byteconf-react-2020*/}\nMay 1, 2020. Streamed online on YouTube.\n\n[Website](https://www.bytesized.xyz) - [Twitter](https://twitter.com/bytesizedcode) - [YouTube](https://www.youtube.com/channel/UC046lFvJZhiwSRWsoH8SFjg)\n\n### React Summit - Remote Edition 2020 {/*react-summit---remote-edition-2020*/}\n3pm CEST time, April 17, 2020 - remote event\n\n[Website](https://remote.reactsummit.com) - [Twitter](https://twitter.com/reactsummit) - [Facebook](https://www.facebook.com/reactamsterdam) - [Videos](https://youtube.com/c/ReactConferences)\n\n### Reactathon 2020 {/*reactathon-2020*/}\nMarch 30 - 31, 2020 in San Francisco, CA\n\n[Website](https://www.reactathon.com) - [Twitter](https://twitter.com/reactathon) - [Facebook](https://www.facebook.com/events/575942819854160/)\n\n### ReactConf AU 2020 {/*reactconf-au-2020*/}\nFebruary 27 & 28, 2020 in Sydney, Australia\n\n[Website](https://reactconfau.com/) - [Twitter](https://twitter.com/reactconfau) - [Facebook](https://www.facebook.com/reactconfau) - [Instagram](https://www.instagram.com/reactconfau/)\n\n### React Barcamp Cologne 2020 {/*react-barcamp-cologne-2020*/}\nFebruary 1-2, 2020 in Cologne, Germany\n\n[Website](https://react-barcamp.de/) - [Twitter](https://twitter.com/ReactBarcamp) - [Facebook](https://www.facebook.com/reactbarcamp)\n\n### React Day Berlin 2019 {/*react-day-berlin-2019*/}\nDecember 6, 2019 in Berlin, Germany\n\n[Website](https://reactday.berlin) - [Twitter](https://twitter.com/reactdayberlin) - [Facebook](https://www.facebook.com/reactdayberlin/) - [Videos](https://www.youtube.com/reactdayberlin)\n\n### React Summit 2019 {/*react-summit-2019*/}\nNovember 30, 2019 in Lagos, Nigeria\n\n[Website](https://reactsummit2019.splashthat.com) -[Twitter](https://twitter.com/react_summit)\n\n### React Conf Brasil 2019 {/*react-conf-brasil-2019*/}\nOctober 19, 2019 in São Paulo, BR\n\n[Website](https://reactconf.com.br/) - [Twitter](https://twitter.com/reactconfbr) - [Facebook](https://www.facebook.com/ReactAdvanced) - [Slack](https://react.now.sh/)\n\n### React Advanced 2019 {/*react-advanced-2019*/}\nOctober 25, 2019 in London, UK\n\n[Website](https://reactadvanced.com) - [Twitter](http://twitter.com/reactadvanced) - [Facebook](https://www.facebook.com/ReactAdvanced) - [Videos](https://youtube.com/c/ReactConferences)\n\n### React Conf 2019 {/*react-conf-2019*/}\nOctober 24-25, 2019 in Henderson, Nevada USA\n\n[Website](https://conf.reactjs.org/) - [Twitter](https://twitter.com/reactjs)\n\n### React Alicante 2019 {/*react-alicante-2019*/}\nSeptember 26-28, 2019 in Alicante, Spain\n\n[Website](http://reactalicante.es/) - [Twitter](https://twitter.com/reactalicante) - [Facebook](https://www.facebook.com/ReactAlicante)\n\n### React India 2019 {/*react-india-2019*/}\nSeptember 26-28, 2019 in Goa, India\n\n[Website](https://www.reactindia.io/) - [Twitter](https://twitter.com/react_india) - [Facebook](https://www.facebook.com/ReactJSIndia)\n\n### React Boston 2019 {/*react-boston-2019*/}\nSeptember 21-22, 2019 in Boston, Massachusetts USA\n\n[Website](https://www.reactboston.com/) - [Twitter](https://twitter.com/reactboston)\n\n### React Live 2019 {/*react-live-2019*/}\nSeptember 13th, 2019. Amsterdam, The Netherlands\n\n[Website](https://www.reactlive.nl/) - [Twitter](https://twitter.com/reactlivenl)\n\n### React New York 2019 {/*react-new-york-2019*/}\nSeptember 13th, 2019. New York, USA\n\n[Website](https://reactnewyork.com/) - [Twitter](https://twitter.com/reactnewyork)\n\n### ComponentsConf 2019 {/*componentsconf-2019*/}\nSeptember 6, 2019 in Melbourne, Australia\n\n[Website](https://www.componentsconf.com.au/) - [Twitter](https://twitter.com/componentsconf)\n\n### React Native EU 2019 {/*react-native-eu-2019*/}\nSeptember 5-6 in Wrocław, Poland\n\n[Website](https://react-native.eu) - [Twitter](https://twitter.com/react_native_eu) - [Facebook](https://www.facebook.com/reactnativeeu)\n\n### React Conf Iran 2019 {/*react-conf-iran-2019*/}\nAugust 29, 2019. Tehran, Iran.\n\n[Website](https://reactconf.ir/) - [Videos](https://www.youtube.com/playlist?list=PL-VNqZFI5Nf-Nsj0rD3CWXGPkH-DI_0VY) - [Highlights](https://github.com/ReactConf/react-conf-highlights)\n\n### React Rally 2019 {/*react-rally-2019*/}\nAugust 22-23, 2019. Salt Lake City, USA.\n\n[Website](https://www.reactrally.com/) - [Twitter](https://twitter.com/ReactRally) - [Instagram](https://www.instagram.com/reactrally/)\n\n### Chain React 2019 {/*chain-react-2019*/}\nJuly 11-12, 2019. Portland, OR, USA.\n\n[Website](https://infinite.red/ChainReactConf)\n\n### React Loop 2019 {/*react-loop-2019*/}\nJune 21, 2019 Chicago, Illinois USA\n\n[Website](https://reactloop.com) - [Twitter](https://twitter.com/ReactLoop)\n\n### React Norway 2019 {/*react-norway-2019*/}\nJune 12, 2019. Larvik, Norway\n\n[Website](https://reactnorway.com) - [Twitter](https://twitter.com/ReactNorway)\n\n### ReactNext 2019 {/*reactnext-2019*/}\nJune 11, 2019. Tel Aviv, Israel\n\n[Website](https://react-next.com) - [Twitter](https://twitter.com/ReactNext) - [Videos](https://www.youtube.com/channel/UC3BT8hh3yTTYxbLQy_wbk2w)\n\n### React Conf Armenia 2019 {/*react-conf-armenia-2019*/}\nMay 25, 2019 in Yerevan, Armenia\n\n[Website](https://reactconf.am/) - [Twitter](https://twitter.com/ReactConfAM) - [Facebook](https://www.facebook.com/reactconf.am/) - [YouTube](https://www.youtube.com/c/JavaScriptConferenceArmenia) - [CFP](http://bit.ly/speakReact)\n\n### ReactEurope 2019 {/*reacteurope-2019*/}\nMay 23-24, 2019 in Paris, France\n\n[Videos](https://www.youtube.com/c/ReacteuropeOrgConf)\n\n### React.NotAConf 2019 {/*reactnotaconf-2019*/}\nMay 11 in Sofia, Bulgaria\n\n[Website](http://react-not-a-conf.com/) - [Twitter](https://twitter.com/reactnotaconf) - [Facebook](https://www.facebook.com/events/780891358936156)\n\n### ReactJS Girls Conference {/*reactjs-girls-conference*/}\nMay 3, 2019 in London, UK\n\n[Website](https://reactjsgirls.com/) - [Twitter](https://twitter.com/reactjsgirls)\n\n### React Finland 2019 {/*react-finland-2019*/}\nApril 24-26 in Helsinki, Finland\n\n[Website](https://react-finland.fi/) - [Twitter](https://twitter.com/ReactFinland)\n\n### React Amsterdam 2019 {/*react-amsterdam-2019*/}\nApril 12, 2019 in Amsterdam, The Netherlands\n\n[Website](https://reactsummit.com) - [Twitter](https://twitter.com/reactsummit) - [Facebook](https://www.facebook.com/reactamsterdam) - [Videos](https://youtube.com/c/ReactConferences)\n\n### App.js Conf 2019 {/*appjs-conf-2019*/}\nApril 4-5, 2019 in Kraków, Poland\n\n[Website](https://appjs.co) - [Twitter](https://twitter.com/appjsconf)\n\n### Reactathon 2019 {/*reactathon-2019*/}\nMarch 30-31, 2019 in San Francisco, USA\n\n[Website](https://www.reactathon.com/) - [Twitter](https://twitter.com/reactathon)\n\n### React Iran 2019 {/*react-iran-2019*/}\nJanuary 31, 2019 in Tehran, Iran\n\n[Website](http://reactiran.com) - [Instagram](https://www.instagram.com/reactiran/)\n\n### React Day Berlin 2018 {/*react-day-berlin-2018*/}\nNovember 30, Berlin, Germany\n\n[Website](https://reactday.berlin) - [Twitter](https://twitter.com/reactdayberlin) - [Facebook](https://www.facebook.com/reactdayberlin/) - [Videos](https://www.youtube.com/channel/UC1EYHmQYBUJjkmL6OtK4rlw)\n\n### ReactNext 2018 {/*reactnext-2018*/}\nNovember 4 in Tel Aviv, Israel\n\n[Website](https://react-next.com) - [Twitter](https://twitter.com/ReactNext) - [Facebook](https://facebook.com/ReactNext2016)\n\n### React Conf 2018 {/*react-conf-2018*/}\nOctober 25-26 in Henderson, Nevada USA\n\n[Website](https://conf.reactjs.org/)\n\n### React Conf Brasil 2018 {/*react-conf-brasil-2018*/}\nOctober 20 in Sao Paulo, Brazil\n\n[Website](http://reactconfbr.com.br) - [Twitter](https://twitter.com/reactconfbr) - [Facebook](https://www.facebook.com/reactconf)\n\n### ReactJS Day 2018 {/*reactjs-day-2018*/}\nOctober 5 in Verona, Italy\n\n[Website](http://2018.reactjsday.it) - [Twitter](https://twitter.com/reactjsday)\n\n### React Boston 2018 {/*react-boston-2018*/}\nSeptember 29-30 in Boston, Massachusetts USA\n\n[Website](http://www.reactboston.com/) - [Twitter](https://twitter.com/ReactBoston)\n\n### React Alicante 2018 {/*react-alicante-2018*/}\nSeptember 13-15 in Alicante, Spain\n\n[Website](http://reactalicante.es) - [Twitter](https://twitter.com/ReactAlicante)\n\n### React Native EU 2018 {/*react-native-eu-2018*/}\nSeptember 5-6 in Wrocław, Poland\n\n[Website](https://react-native.eu) - [Twitter](https://twitter.com/react_native_eu) - [Facebook](https://www.facebook.com/reactnativeeu)\n\n### Byteconf React 2018 {/*byteconf-react-2018*/}\nAugust 31 streamed online, via Twitch\n\n[Website](https://byteconf.com) - [Twitch](https://twitch.tv/byteconf) - [Twitter](https://twitter.com/byteconf)\n\n### ReactFoo Delhi {/*reactfoo-delhi*/}\nAugust 18 in Delhi, India\n\n[Website](https://reactfoo.in/2018-delhi/) - [Twitter](https://twitter.com/reactfoo) - [Past talks](https://hasgeek.tv)\n\n### React DEV Conf China {/*react-dev-conf-china*/}\nAugust 18 in Guangzhou, China\n\n[Website](https://react.w3ctech.com)\n\n### React Rally 2018 {/*react-rally-2018*/}\nAugust 16-17 in Salt Lake City, Utah USA\n\n[Website](http://www.reactrally.com) - [Twitter](https://twitter.com/reactrally)\n\n### Chain React 2018 {/*chain-react-2018*/}\nJuly 11-13 in Portland, Oregon USA\n\n[Website](https://infinite.red/ChainReactConf) - [Twitter](https://twitter.com/chainreactconf)\n\n### ReactFoo Mumbai {/*reactfoo-mumbai*/}\nMay 26 in Mumbai, India\n\n[Website](https://reactfoo.in/2018-mumbai/) - [Twitter](https://twitter.com/reactfoo) - [Past talks](https://hasgeek.tv)\n\n\n### ReactEurope 2018 {/*reacteurope-2018*/}\nMay 17-18 in Paris, France\n\n[Videos](https://www.youtube.com/c/ReacteuropeOrgConf)\n\n### React.NotAConf 2018 {/*reactnotaconf-2018*/}\nApril 28 in Sofia, Bulgaria\n\n[Website](http://react-not-a-conf.com/) - [Twitter](https://twitter.com/reactnotaconf) - [Facebook](https://www.facebook.com/groups/1614950305478021/)\n\n### React Finland 2018 {/*react-finland-2018*/}\nApril 24-26 in Helsinki, Finland\n\n[Website](https://react-finland.fi/) - [Twitter](https://twitter.com/ReactFinland)\n\n### React Amsterdam 2018 {/*react-amsterdam-2018*/}\nApril 13 in Amsterdam, The Netherlands\n\n[Website](https://reactsummit.com) - [Twitter](https://twitter.com/reactsummit) - [Facebook](https://www.facebook.com/reactamsterdam)\n\n### React Native Camp UA 2018 {/*react-native-camp-ua-2018*/}\nMarch 31 in Kiev, Ukraine\n\n[Website](http://reactnative.com.ua/) - [Twitter](https://twitter.com/reactnativecamp) - [Facebook](https://www.facebook.com/reactnativecamp/)\n\n### Reactathon 2018 {/*reactathon-2018*/}\nMarch 20-22 in San Francisco, USA\n\n[Website](https://www.reactathon.com/) - [Twitter](https://twitter.com/reactathon) - [Videos (fundamentals)](https://www.youtube.com/watch?v=knn364bssQU&list=PLRvKvw42Rc7OWK5s-YGGFSmByDzzgC0HP), [Videos (advanced day1)](https://www.youtube.com/watch?v=57hmk4GvJpk&list=PLRvKvw42Rc7N0QpX2Rc5CdrqGuxzwD_0H), [Videos (advanced day2)](https://www.youtube.com/watch?v=1hvQ8p8q0a0&list=PLRvKvw42Rc7Ne46QAjWNWFo1Jf0mQdnIW)\n\n### ReactFest 2018 {/*reactfest-2018*/}\nMarch 8-9 in London, UK\n\n[Website](https://reactfest.uk/) - [Twitter](https://twitter.com/ReactFest) - [Videos](https://www.youtube.com/watch?v=YOCrJ5vRCnw&list=PLRgweB8YtNRt-Sf-A0y446wTJNUaAAmle)\n\n### AgentConf 2018 {/*agentconf-2018*/}\nJanuary 25-28 in Dornbirn, Austria\n\n[Website](http://agent.sh/)\n\n### ReactFoo Pune {/*reactfoo-pune*/}\nJanuary 19-20, Pune, India\n\n[Website](https://reactfoo.in/2018-pune/) - [Twitter](https://twitter.com/ReactFoo)\n\n### React Day Berlin 2017 {/*react-day-berlin-2017*/}\nDecember 2, Berlin, Germany\n\n[Website](https://reactday.berlin) - [Twitter](https://twitter.com/reactdayberlin) - [Facebook](https://www.facebook.com/reactdayberlin/) - [Videos](https://www.youtube.com/watch?v=UnNLJvHKfSY&list=PL-3BrJ5CiIx5GoXci54-VsrO6GwLhSHEK)\n\n### React Seoul 2017 {/*react-seoul-2017*/}\nNovember 4 in Seoul, South Korea\n\n[Website](http://seoul.reactjs.kr/en)\n\n### ReactiveConf 2017 {/*reactiveconf-2017*/}\nOctober 25–27, Bratislava, Slovakia\n\n[Website](https://reactiveconf.com) - [Videos](https://www.youtube.com/watch?v=BOKxSFB2hOE&list=PLa2ZZ09WYepMB-I7AiDjDYR8TjO8uoNjs)\n\n### React Summit 2017 {/*react-summit-2017*/}\nOctober 21 in Lagos, Nigeria\n\n[Website](https://reactsummit2017.splashthat.com/) - [Twitter](https://twitter.com/DevCircleLagos/) - [Facebook](https://www.facebook.com/groups/DevCLagos/)\n\n### State.js Conference 2017 {/*statejs-conference-2017*/}\nOctober 13 in Stockholm, Sweden\n\n[Website](https://statejs.com/)\n\n### React Conf Brasil 2017 {/*react-conf-brasil-2017*/}\nOctober 7 in Sao Paulo, Brazil\n\n[Website](http://reactconfbr.com.br) - [Twitter](https://twitter.com/reactconfbr) - [Facebook](https://www.facebook.com/reactconf/)\n\n### ReactJS Day 2017 {/*reactjs-day-2017*/}\nOctober 6 in Verona, Italy\n\n[Website](http://2017.reactjsday.it) - [Twitter](https://twitter.com/reactjsday) - [Videos](https://www.youtube.com/watch?v=bUqqJPIgjNU&list=PLWK9j6ps_unl293VhhN4RYMCISxye3xH9)\n\n### React Alicante 2017 {/*react-alicante-2017*/}\nSeptember 28-30 in Alicante, Spain\n\n[Website](http://reactalicante.es) - [Twitter](https://twitter.com/ReactAlicante) - [Videos](https://www.youtube.com/watch?v=UMZvRCWo6Dw&list=PLd7nkr8mN0sWvBH_s0foCE6eZTX8BmLUM)\n\n### React Boston 2017 {/*react-boston-2017*/}\nSeptember 23-24 in Boston, Massachusetts USA\n\n[Website](http://www.reactboston.com/) - [Twitter](https://twitter.com/ReactBoston) - [Videos](https://www.youtube.com/watch?v=2iPE5l3cl_s&list=PL-fCkV3wv4ub8zJMIhmrrLcQqSR5XPlIT)\n\n### ReactFoo 2017 {/*reactfoo-2017*/}\nSeptember 14 in Bangalore, India\n\n[Website](https://reactfoo.in/2017/) - [Videos](https://www.youtube.com/watch?v=3G6tMg29Wnw&list=PL279M8GbNsespKKm1L0NAzYLO6gU5LvfH)\n\n### ReactNext 2017 {/*reactnext-2017*/}\nSeptember 8-10 in Tel Aviv, Israel\n\n[Website](http://react-next.com/) - [Twitter](https://twitter.com/ReactNext) - [Videos (Hall A)](https://www.youtube.com/watch?v=eKXQw5kR86c&list=PLMYVq3z1QxSqq6D7jxVdqttOX7H_Brq8Z), [Videos (Hall B)](https://www.youtube.com/watch?v=1InokWxYGnE&list=PLMYVq3z1QxSqCZmaqgTXLsrcJ8mZmBF7T)\n\n### React Native EU 2017 {/*react-native-eu-2017*/}\nSeptember 6-7 in Wroclaw, Poland\n\n[Website](http://react-native.eu/) - [Videos](https://www.youtube.com/watch?v=453oKJAqfy0&list=PLzUKC1ci01h_hkn7_KoFA-Au0DXLAQZR7)\n\n### React Rally 2017 {/*react-rally-2017*/}\nAugust 24-25 in Salt Lake City, Utah USA\n\n[Website](http://www.reactrally.com) - [Twitter](https://twitter.com/reactrally) - [Videos](https://www.youtube.com/watch?v=f4KnHNCZcH4&list=PLUD4kD-wL_zZUhvAIHJjueJDPr6qHvkni)\n\n### Chain React 2017 {/*chain-react-2017*/}\nJuly 10-11 in Portland, Oregon USA\n\n[Website](https://infinite.red/ChainReactConf) - [Twitter](https://twitter.com/chainreactconf) - [Videos](https://www.youtube.com/watch?v=cz5BzwgATpc&list=PLFHvL21g9bk3RxJ1Ut5nR_uTZFVOxu522)\n\n### ReactEurope 2017 {/*reacteurope-2017*/}\nMay 18th & 19th in Paris, France\n\n[Videos](https://www.youtube.com/c/ReacteuropeOrgConf)\n\n### React Amsterdam 2017 {/*react-amsterdam-2017*/}\nApril 21st in Amsterdam, The Netherlands\n\n[Website](https://reactsummit.com) - [Twitter](https://twitter.com/reactsummit) - [Videos](https://youtube.com/c/ReactConferences)\n\n### React London 2017 {/*react-london-2017*/}\nMarch 28th at the [QEII Centre, London](http://qeiicentre.london/)\n\n[Website](http://react.london/) - [Videos](https://www.youtube.com/watch?v=2j9rSur_mnk&list=PLW6ORi0XZU0CFjdoYeC0f5QReBG-NeNKJ)\n\n### React Conf 2017 {/*react-conf-2017*/}\nMarch 13-14 in Santa Clara, CA\n\n[Website](http://conf.reactjs.org/) - [Videos](https://www.youtube.com/watch?v=7HSd1sk07uU&list=PLb0IAmt7-GS3fZ46IGFirdqKTIxlws7e0)\n\n### Agent Conference 2017 {/*agent-conference-2017*/}\nJanuary 20-21 in Dornbirn, Austria\n\n[Website](http://agent.sh/)\n\n### React Remote Conf 2016 {/*react-remote-conf-2016*/}\nOctober 26-28 online\n\n[Website](https://allremoteconfs.com/react-2016) - [Schedule](https://allremoteconfs.com/react-2016#schedule)\n\n### Reactive 2016 {/*reactive-2016*/}\nOctober 26-28 in Bratislava, Slovakia\n\n[Website](https://reactiveconf.com/)\n\n### ReactNL 2016 {/*reactnl-2016*/}\nOctober 13 in Amsterdam, The Netherlands\n\n[Website](http://reactnl.org/) - [Schedule](http://reactnl.org/#program)\n\n### ReactNext 2016 {/*reactnext-2016*/}\nSeptember 15 in Tel Aviv, Israel\n\n[Website](http://react-next.com/) - [Schedule](http://react-next.com/#schedule) - [Videos](https://www.youtube.com/channel/UC3BT8hh3yTTYxbLQy_wbk2w)\n\n### ReactRally 2016 {/*reactrally-2016*/}\nAugust 25-26 in Salt Lake City, UT\n\n[Website](http://www.reactrally.com/) - [Schedule](http://www.reactrally.com/#/schedule) - [Videos](https://www.youtube.com/playlist?list=PLUD4kD-wL_zYSfU3tIYsb4WqfFQzO_EjQ)\n\n### ReactEurope 2016 {/*reacteurope-2016*/}\nJune 2 & 3 in Paris, France\n\n[Videos](https://www.youtube.com/c/ReacteuropeOrgConf)\n\n### React Amsterdam 2016 {/*react-amsterdam-2016*/}\nApril 16 in Amsterdam, The Netherlands\n\n[Website](https://reactsummit.com) - [Twitter](https://twitter.com/reactsummit) - [Facebook](https://www.facebook.com/reactamsterdam) - [Videos](https://youtube.com/c/ReactConferences)\n\n### React.js Conf 2016 {/*reactjs-conf-2016*/}\nFebruary 22 & 23 in San Francisco, CA\n\n[Website](http://conf2016.reactjs.org/) - [Schedule](http://conf2016.reactjs.org/schedule.html) - [Videos](https://www.youtube.com/playlist?list=PLb0IAmt7-GS0M8Q95RIc2lOM6nc77q1IY)\n\n### Reactive 2015 {/*reactive-2015*/}\nNovember 2-4 in Bratislava, Slovakia\n\n[Website](https://reactive2015.com/) - [Schedule](https://reactive2015.com/schedule_speakers.html#schedule)\n\n### ReactEurope 2015 {/*reacteurope-2015*/}\nJuly 2 & 3 in Paris, France\n\n[Videos](https://www.youtube.com/c/ReacteuropeOrgConf)\n\n### React.js Conf 2015 {/*reactjs-conf-2015*/}\nJanuary 28 & 29 in Facebook HQ, CA\n\n[Website](http://conf2015.reactjs.org/) - [Schedule](http://conf2015.reactjs.org/schedule.html) - [Videos](https://www.youtube.com/playlist?list=PLb0IAmt7-GS1cbw4qonlQztYV1TAW0sCr)\n"
  },
  {
    "path": "src/content/community/docs-contributors.md",
    "content": "---\ntitle: 문서 기여자\n---\n\n<Intro>\n\nReact 문서는 [React 팀](/community/team)과 [외부 기여자](https://github.com/reactjs/react.dev/graphs/contributors)에 의해 작성되고 유지되고 있습니다. 우리는 이 페이지에서, 사이트에 상당한 기여를 한 사람들에게 감사를 전하고 싶습니다.\n\n</Intro>\n\n## 내용 {/*content*/}\n\n* [Rachel Nabors](https://twitter.com/RachelNabors): 편집, 글쓰기, 그림 그리기\n* [Dan Abramov](https://bsky.app/profile/danabra.mov): 글쓰기, 커리큘럼 설계\n* [Sylwia Vargas](https://twitter.com/SylwiaVargas): 예시 코드\n* [Rick Hanlon](https://twitter.com/rickhanlonii): 글쓰기\n* [David McCabe](https://twitter.com/mcc_abe): 글쓰기\n* [Sophie Alpert](https://twitter.com/sophiebits): 글쓰기\n* [Pete Hunt](https://twitter.com/floydophone): 글쓰기\n* [Andrew Clark](https://twitter.com/acdlite): 글쓰기\n* [Matt Carroll](https://twitter.com/mattcarrollcode): 편집, 글쓰기\n* [Natalia Tepluhina](https://twitter.com/n_tepluhina): 리뷰, 조언\n* [Sebastian Markbåge](https://twitter.com/sebmarkbage): 피드백\n\n## 디자인 {/*design*/}\n\n* [Dan Lebowitz](https://twitter.com/lebo): 사이트 디자인\n* [Razvan Gradinar](https://dribbble.com/GradinarRazvan): Sandbox 디자인\n* [Maggie Appleton](https://maggieappleton.com/): 도표 시스템\n* [Sophie Alpert](https://twitter.com/sophiebits): 색칠된 코드를 사용한 설명\n\n## 개발 {/*development*/}\n\n* [Jared Palmer](https://twitter.com/jaredpalmer): 사이트 개발\n* [ThisDotLabs](https://www.thisdot.co/) ([Dane Grant](https://twitter.com/danecando), [Dustin Goodman](https://twitter.com/dustinsgoodman)): 사이트 개발\n* [CodeSandbox](https://codesandbox.io/) ([Ives van Hoorne](https://twitter.com/CompuIves), [Alex Moldovan](https://twitter.com/alexnmoldovan), [Jasper De Moor](https://twitter.com/JasperDeMoor), [Danilo Woznica](https://twitter.com/danilowoz)): Sandbox 통합\n* [Dan Abramov](https://twitter.com/dan_abramov): 사이트 개발\n* [Rick Hanlon](https://twitter.com/rickhanlonii): 사이트 개발\n* [Harish Kumar](https://www.strek.in/): 개발과 유지보수\n* [Luna Ruan](https://twitter.com/lunaruan): Sandbox 개선\n\n## 한국어 번역 {/*korean-translations*/}\n\n* [루밀(LuMir)](https://github.com/lumirlumir): 사이트 개발, 유지보수, 번역 등\n\n저희에게 피드백을 제공한 수 많은 알파 테스터들과 커뮤니티 멤버들에게도 감사를 전하고 싶습니다.\n"
  },
  {
    "path": "src/content/community/index.md",
    "content": "---\ntitle: React 커뮤니티\n---\n\n<Intro>\n\nReact는 수백만의 개발자로 구성된 커뮤니티를 가지고 있습니다. 이 페이지는 누구나 참여할 수 있는 React 관련 커뮤니티 몇 가지를 소개합니다. 이 섹션의 다른 페이지에서 온라인 및 대면 학습 자료를 추가로 확인하세요.\n\n</Intro>\n\n## 행동 강령 {/*code-of-conduct*/}\n\nReact 커뮤니티에 참여하기 전에 [반드시 행동 강령을 읽어주세요](https://github.com/facebook/react/blob/main/CODE_OF_CONDUCT.md). 우리는 [기여자 서약](https://www.contributor-covenant.org/)을 채택했고, 모든 커뮤니티 구성원들이 지침을 준수할 것을 기대합니다.\n\n## Stack Overflow {/*stack-overflow*/}\n\nStack Overflow는 코드 관련 질문을 하거나 특정 오류에 대한 해결 방법을 찾고자 할 때 유용한 포럼입니다. **reactjs**로 태그된 [기존의 질문을](https://stackoverflow.com/questions/tagged/reactjs) 읽어보거나 [직접 질문하세요](https://stackoverflow.com/questions/ask?tags=reactjs)!\n\n## 인기 있는 토론 포럼 {/*popular-discussion-forums*/}\n\nReact의 베스트 프랙티스, 애플리케이션 아키텍처, 그리고 React의 미래를 주제로 토론할 수 있는 많은 온라인 포럼이 존재합니다. 코드 레벨에서 답할 수 있는 질문이 있다면, 대개 Stack Overflow가 더 적합합니다.\n\n각 커뮤니티는 수천 명의 React 사용자로 구성되어 있습니다.\n\n* [DEV의 React 커뮤니티](https://dev.to/t/react)\n* [Hashnode의 React 커뮤니티](https://hashnode.com/n/reactjs)\n* [Reactiflux 온라인 채팅](https://discord.gg/reactiflux)\n* [Reddit의 React 커뮤니티](https://www.reddit.com/r/reactjs/)\n\n## 뉴스 {/*news*/}\n\nReact에 대한 최신 뉴스는 [Twitter의 **@reactjs**](https://twitter.com/reactjs), [Bluesky의 **@react.dev**](https://bsky.app/profile/react.dev), 이 웹사이트의 [공식 React 블로그](/blog/)에서 확인하세요.\n"
  },
  {
    "path": "src/content/community/meetups.md",
    "content": "---\ntitle: React 모임\n---\n\n<Intro>\n\nReact.js 관련 모임이 있다면 이곳에 추가해주세요! (목록은 알파벳 순으로 유지해주세요)\n\n</Intro>\n\n## Albania {/*albania*/}\n* [Tirana](https://www.meetup.com/React-User-Group-Albania/)\n\n## Argentina {/*argentina*/}\n* [Buenos Aires](https://www.meetup.com/es/React-en-Buenos-Aires)\n* [Rosario](https://www.meetup.com/es/reactrosario)\n\n## Australia {/*australia*/}\n* [Brisbane](https://www.meetup.com/reactbris/)\n* [Melbourne](https://www.meetup.com/React-Melbourne/)\n* [Sydney](https://www.meetup.com/React-Sydney/)\n\n## Austria {/*austria*/}\n* [Vienna](https://www.meetup.com/Vienna-ReactJS-Meetup/)\n\n## Belgium {/*belgium*/}\n* [Belgium](https://www.meetup.com/ReactJS-Belgium/)\n\n## Brazil {/*brazil*/}\n* [Belo Horizonte](https://www.meetup.com/reactbh/)\n* [Curitiba](https://www.meetup.com/pt-br/ReactJS-CWB/)\n* [Florianópolis](https://www.meetup.com/pt-br/ReactJS-Floripa/)\n* [Joinville](https://www.meetup.com/pt-BR/React-Joinville/)\n* [São Paulo](https://www.meetup.com/pt-BR/ReactJS-SP/)\n\n## Bolivia {/*bolivia*/}\n* [Bolivia](https://www.meetup.com/ReactBolivia/)\n\n## Canada {/*canada*/}\n* [Halifax, NS](https://www.meetup.com/Halifax-ReactJS-Meetup/)\n* [Montreal, QC](https://guild.host/react-montreal/)\n* [Vancouver, BC](https://www.meetup.com/ReactJS-Vancouver-Meetup/)\n* [Ottawa, ON](https://www.meetup.com/Ottawa-ReactJS-Meetup/)\n* [Saskatoon, SK](https://www.meetup.com/saskatoon-react-meetup/)\n* [Toronto, ON](https://www.meetup.com/Toronto-React-Native/events/)\n\n## Colombia {/*colombia*/}\n* [Medellin](https://www.meetup.com/React-Medellin/)\n\n## Czechia {/*czechia*/}\n* [Prague](https://guild.host/react-prague/)\n\n## Denmark {/*denmark*/}\n* [Aalborg](https://www.meetup.com/Aalborg-React-React-Native-Meetup/)\n* [Aarhus](https://www.meetup.com/Aarhus-ReactJS-Meetup/)\n\n## England (UK) {/*england-uk*/}\n* [Manchester](https://www.meetup.com/Manchester-React-User-Group/)\n* [React.JS Girls London](https://www.meetup.com/ReactJS-Girls-London/)\n* [React Advanced London](https://guild.host/react-advanced-london)\n* [React Native Liverpool](https://www.meetup.com/react-native-liverpool/)\n* [React Native London](https://guild.host/RNLDN)\n\n## Finland {/*finland*/}\n* [Helsinki](https://www.meetabit.com/communities/react-helsinki)\n\n## France {/*france*/}\n* [Lille](https://www.meetup.com/ReactBeerLille/)\n* [Paris](https://www.meetup.com/ReactJS-Paris/)\n\n## Germany {/*germany*/}\n* [Cologne](https://www.meetup.com/React-Cologne/)\n* [Düsseldorf](https://www.meetup.com/de-DE/ReactJS-Meetup-Dusseldorf/)\n* [Hamburg](https://www.meetup.com/Hamburg-React-js-Meetup/)\n* [Karlsruhe](https://www.meetup.com/react_ka/)\n* [Kiel](https://www.meetup.com/Kiel-React-Native-Meetup/)\n* [Munich](https://www.meetup.com/ReactJS-Meetup-Munich/)\n* [React Berlin](https://guild.host/react-berlin)\n\n## Greece {/*greece*/}\n* [Athens](https://www.meetup.com/React-To-React-Athens-MeetUp/)\n* [Thessaloniki](https://www.meetup.com/Thessaloniki-ReactJS-Meetup/)\n\n## India {/*india*/}\n* [Ahmedabad](https://reactahmedabad.dev/)\n* [Bangalore (React)](https://www.meetup.com/ReactJS-Bangalore/)\n* [Bangalore (React Native)](https://www.meetup.com/React-Native-Bangalore-Meetup)\n* [Chennai](https://www.linkedin.com/company/chennaireact)\n* [Delhi NCR](https://www.meetup.com/React-Delhi-NCR/)\n* [Mumbai](https://reactmumbai.dev)\n* [Pune](https://www.meetup.com/ReactJS-and-Friends/)\n* [Rajasthan](https://reactrajasthan.com)\n\n## Indonesia {/*indonesia*/}\n* [Indonesia](https://www.meetup.com/reactindonesia/)\n\n## Ireland {/*ireland*/}\n* [Dublin](https://guild.host/reactjs-dublin)\n\n## Israel {/*israel*/}\n* [Tel Aviv](https://www.meetup.com/ReactJS-Israel/)\n\n## Italy {/*italy*/}\n* [Milan](https://www.meetup.com/React-JS-Milano/)\n\n## Japan {/*japan*/}\n* [Osaka](https://react-osaka.connpass.com/)\n\n## Kenya {/*kenya*/}\n* [Nairobi - Reactdevske](https://kommunity.com/reactjs-developer-community-kenya-reactdevske)\n\n## Malaysia {/*malaysia*/}\n* [Kuala Lumpur](https://www.kl-react.com/)\n* [Penang](https://www.facebook.com/groups/reactpenang/)\n\n## Netherlands {/*netherlands*/}\n* [Amsterdam](https://guild.host/react-amsterdam)\n\n## New Zealand {/*new-zealand*/}\n* [Wellington](https://www.meetup.com/React-Wellington/)\n\n## Norway {/*norway*/}\n* [Norway](https://reactjs-norway.webflow.io/)\n* [Oslo](https://www.meetup.com/ReactJS-Oslo-Meetup/)\n\n## Pakistan {/*pakistan*/}\n* [Karachi](https://www.facebook.com/groups/902678696597634/)\n* [Lahore](https://www.facebook.com/groups/ReactjsLahore/)\n\n## Philippines {/*philippines*/}\n* [Manila](https://www.meetup.com/reactjs-developers-manila/)\n* [Manila - ReactJS PH](https://www.meetup.com/ReactJS-Philippines/)\n\n## Poland {/*poland*/}\n* [Warsaw](https://www.meetup.com/React-js-Warsaw/)\n* [Wrocław](https://www.meetup.com/ReactJS-Wroclaw/)\n\n## Portugal {/*portugal*/}\n* [Lisbon](https://www.meetup.com/JavaScript-Lisbon/)\n\n## Scotland (UK) {/*scotland-uk*/}\n* [Edinburgh](https://www.meetup.com/react-edinburgh/)\n\n## Spain {/*spain*/}\n* [Barcelona](https://www.meetup.com/ReactJS-Barcelona/)\n\n## Sri Lanka {/*sri-lanka*/}\n* [Colombo](https://www.javascriptcolombo.com/)\n\n## Sweden {/*sweden*/}\n* [Goteborg](https://www.meetup.com/ReactJS-Goteborg/)\n* [Stockholm](https://www.meetup.com/Stockholm-ReactJS-Meetup/)\n\n## Switzerland {/*switzerland*/}\n* [Zurich](https://www.meetup.com/Zurich-ReactJS-Meetup/)\n\n## Turkey {/*turkey*/}\n* [Istanbul](https://kommunity.com/reactjs-istanbul)\n\n## Ukraine {/*ukraine*/}\n* [Kyiv](https://www.meetup.com/Kyiv-ReactJS-Meetup)\n\n## US {/*us*/}\n* [Atlanta, GA - ReactJS](https://www.meetup.com/React-ATL/)\n* [Austin, TX - ReactJS](https://www.meetup.com/ReactJS-Austin-Meetup/)\n* [Boston, MA - ReactJS](https://www.meetup.com/ReactJS-Boston/)\n* [Boston, MA - React Native](https://www.meetup.com/Boston-React-Native-Meetup/)\n* [Charlotte, NC - ReactJS](https://www.meetup.com/ReactJS-Charlotte/)\n* [Charlotte, NC - React Native](https://www.meetup.com/cltreactnative/)\n* [Chicago, IL - ReactJS](https://www.meetup.com/React-Chicago/)\n* [Cleveland, OH - ReactJS](https://www.meetup.com/Cleveland-React/)\n* [Columbus, OH - ReactJS](https://www.meetup.com/ReactJS-Columbus-meetup/)\n* [Dallas, TX - ReactJS](https://www.meetup.com/ReactDallas/)\n* [Denver, CO - React Denver](https://reactdenver.com/)\n* [Detroit, MI - Detroit React User Group](https://www.meetup.com/Detroit-React-User-Group/)\n* [Indianapolis, IN - React.Indy](https://www.meetup.com/React-Indy)\n* [Irvine, CA - ReactJS](https://www.meetup.com/ReactJS-OC/)\n* [Kansas City, MO - ReactJS](https://www.meetup.com/Kansas-City-React-Meetup/)\n* [Las Vegas, NV - ReactJS](https://www.meetup.com/ReactVegas/)\n* [Leesburg, VA - ReactJS](https://www.meetup.com/React-NOVA/)\n* [Los Angeles, CA - ReactJS](https://www.meetup.com/socal-react/)\n* [Los Angeles, CA - React Native](https://www.meetup.com/React-Native-Los-Angeles/)\n* [Miami, FL - ReactJS](https://www.meetup.com/React-Miami/)\n* [New York, NY - ReactJS](https://www.meetup.com/NYC-Javascript-React-Group/)\n* [New York, NY - React Ladies](https://www.meetup.com/React-Ladies/)\n* [New York, NY - React Native](https://www.meetup.com/React-Native-NYC/)\n* [New York, NY - useReactNYC](https://www.meetup.com/useReactNYC/)\n* [New York, NY - React.NYC](https://guild.host/react-nyc)\n* [Palo Alto, CA - React Native](https://www.meetup.com/React-Native-Silicon-Valley/)\n* [Phoenix, AZ - ReactJS](https://www.meetup.com/ReactJS-Phoenix/)\n* [Provo, UT - ReactJS](https://www.meetup.com/ReactJS-Utah/)\n* [San Diego, CA - San Diego JS](https://www.meetup.com/sandiegojs/)\n* [San Francisco - Real World React](https://www.meetup.com/Real-World-React)\n* [San Francisco - ReactJS](https://www.meetup.com/ReactJS-San-Francisco/)\n* [San Francisco, CA - React Native](https://www.meetup.com/React-Native-San-Francisco/)\n* [Santa Monica, CA - ReactJS](https://www.meetup.com/Los-Angeles-ReactJS-User-Group/)\n* [Seattle, WA - ReactJS](https://www.meetup.com/seattle-react-js/)\n* [Tampa, FL - ReactJS](https://www.meetup.com/ReactJS-Tampa-Bay/)\n* [Tucson, AZ - ReactJS](https://www.meetup.com/Tucson-ReactJS-Meetup/)\n* [Washington, DC - ReactJS](https://www.meetup.com/React-DC/)\n"
  },
  {
    "path": "src/content/community/team.md",
    "content": "---\ntitle: \"팀 소개\"\n---\n\n<Intro>\n\nReact 개발은 Meta의 전담 팀이 주도하며, 전 세계 개발자들이 기여하고 있습니다.\n\n</Intro>\n\n## React Core {/*react-core*/}\n\nReact Core 팀 구성원들은 React DOM과 React Native를 구동하는 엔진, 핵심 컴포넌트 API, React DevTools, 그리고 문서 웹사이트 개발을 전담합니다.\n\n현재 React 팀 멤버는 아래에 알파벳 순으로 나열되어 있습니다.\n\n<TeamMember name=\"Andrew Clark\" permalink=\"andrew-clark\" photo=\"/images/team/acdlite.jpg\" github=\"acdlite\" twitter=\"acdlite\" threads=\"acdlite\" title=\"Engineer at Vercel\">\n    Andrew는 WordPress로 사이트를 만들며 웹 개발을 시작했고, 어쩌다 보니 JavaScript까지 하게 되었습니다. 취미는 노래방에서 시간을 보내는 것이고, 날에 따라 디즈니 빌런 같기도, 디즈니 공주 같기도 합니다.\n</TeamMember>\n\n<TeamMember name=\"Dan Abramov\" permalink=\"dan-abramov\" photo=\"/images/team/gaearon.jpg\" github=\"gaearon\" bsky=\"danabra.mov\" title=\"Independent Engineer\">\n    Dan은 마이크로소프트 파워포인트에서 우연히 Visual Basic을 발견한 후 프로그래밍을 시작했습니다. 그는 [Sebastian](#sebastian-markbåge)의 트윗을 블로그 글로 옮기면서 자신의 천직을 찾았습니다. 가끔 포트나이트에서는 게임이 끝날 때까지 덤불 속에 숨어 있다가 승리하기도 합니다.\n</TeamMember>\n\n<TeamMember name=\"Eli White\" permalink=\"eli-white\" photo=\"/images/team/eli-white.jpg\" github=\"elicwhite\" twitter=\"Eli_White\" threads=\"elicwhite\" title=\"Engineering Manager at Meta\">\n    Eli는 중학교 시절 해킹으로 정학을 당한 후 프로그래밍을 시작했습니다. 2017년부터 React와 React Native 개발을 하고 있으며, 아이스크림과 애플파이 같은 간식을 특히 좋아합니다. 파쿠르, 실내 스카이다이빙, 공중 실크처럼 독특한 활동에 도전하는 모습도 볼 수 있습니다.\n</TeamMember>\n\n<TeamMember name=\"Hendrik Liebau\" permalink=\"hendrik-liebau\" photo=\"/images/team/hendrik.jpg\" github=\"unstubbable\" bsky=\"unstubbable.bsky.social\" twitter=\"unstubbable\" title=\"Engineer at Vercel\">\n    Hendrik은 90년대 말, Netscape Communicator로 첫 웹사이트를 만들며 기술 경력을 시작했습니다. 전산학 학위를 취득하고 디지털 에이전시에서 일한 뒤, React Server Components 번들러와 라이브러리를 개발했고, 이것이 Next.js 팀 합류의 발판이 되었습니다. 일 외에는 사이클링을 즐기며 작업실에서 이것저것 만드는 활동을 합니다.\n</TeamMember>\n\n<TeamMember name=\"Jack Pope\" permalink=\"jack-pope\" photo=\"/images/team/jack-pope.jpg\" github=\"jackpope\" personal=\"jackpope.me\" title=\"Engineer at Meta\">\n    AutoHotkey를 접한 직후, Jack은 생각나는 모든 작업을 자동화하는 스크립트를 작성했습니다. 이후 웹 앱 개발로 넘어와 현재까지도 웹 개발을 이어가고 있습니다. 최근에는 Instagram 웹 플랫폼에서 근무하다 React 팀에 합류했습니다. 가장 좋아하는 프로그래밍 언어는 JSX입니다.\n</TeamMember>\n\n<TeamMember name=\"Jason Bonta\" permalink=\"jason-bonta\" photo=\"/images/team/jasonbonta.jpg\" threads=\"someextent\" title=\"Engineering Manager at Meta\">\n    Jason은 임베디드 C를 과감히 포기하고 프론트엔드 개발자의 길을 걸었습니다. 해박한 CSS 지식과 아름다운 UI에 대한 열정을 무기로 2010년 페이스북에 합류했고, 그곳에서 자바스크립트 개발이 본격적으로 성장하는 것을 직접 보는 특권을 누리고 있습니다. for...of 루프의 작동 원리는 잘 모르지만, 훌륭한 사람들과 멋진 UX 프로젝트를 함께할 수 있다는 점을 즐깁니다.\n</TeamMember>\n\n<TeamMember name=\"Joe Savona\" permalink=\"joe-savona\" photo=\"/images/team/joe.jpg\" github=\"josephsavona\" twitter=\"en_JS\" threads=\"joesavona\" title=\"Engineer at Meta\">\n    Joe는 본래 수학과 철학을 전공하려 했지만, Matlab으로 물리 시뮬레이션을 작성하며 컴퓨터 과학에 빠져들게 되었습니다. React를 접하기 전에는 Relay, RSocket.js, 그리고 Skip 프로그래밍 언어 개발에 참여했습니다. 일을 하지 않을 때에는 달리기, 일본어 공부, 그리고 가족과 시간을 보내는 것을 즐깁니다.\n</TeamMember>\n\n<TeamMember name=\"Jordan Brown\" permalink=\"jordan-brown\" photo=\"/images/team/jordan.jpg\" github=\"jbrown215\" title=\"Engineer at Meta\">\n    Jordan은 iPhone 앱을 개발하며 코딩을 시작했습니다. 그 과정에서 for-loop가 무엇인지도 모르고 View Controller를 다루었습니다. 그는 개발자가 사랑하는 기술을 다루는 일을 즐기며 자연스럽게 React로 이어졌습니다. 업무 외에는 독서, 카이트보딩, 기타 연주를 즐깁니다.\n</TeamMember>\n\n<TeamMember name=\"Josh Story\" permalink=\"josh-story\" photo=\"/images/team/josh.jpg\" github=\"gnoff\" bsky=\"storyhb.com\" title=\"Engineer at Vercel\">\n    Josh는 대학에서 수학을 전공하며 프로그래밍을 알게 되었습니다. 첫 직장은 마이크로소프트 엑셀로 보험 요율을 계산하는 프로그램을 개발하는 일이었는데, 이것이 리액트 개발로 이어지는 계기가 되었습니다. 그 후 여러 스타트업을 거치며 개인 실무자부터 매니저, 임원까지 다양한 경험을 쌓았습니다. 업무 외에는 요리를 통해 새로운 것에 도전하는 것을 좋아합니다.\n</TeamMember>\n\n<TeamMember name=\"Lauren Tan (나은)\" permalink=\"lauren-tan\" photo=\"/images/team/lauren.jpg\" github=\"poteto\" twitter=\"potetotes\" threads=\"potetotes\" bsky=\"no.lol\" title=\"Engineer at Meta\">\n    나은은 `<marquee>`를 발견했을 때 개발의 전성기를 맞이했습니다. 그때부터 비슷한 느낌을 찾고 있습니다. 대학에서 컴퓨터과학 대신 재무금융을 전공하면서 엑셀로 코딩을 배웠습니다. 취미는 대화방에서 재미있는 밈을 공유하고, 남편과 함께 게임하고, 한국어를 배우고, 강아지 젤다와 노는 것입니다.\n</TeamMember>\n\n<TeamMember name=\"Matt Carroll\" permalink=\"matt-carroll\" photo=\"/images/team/matt-carroll.png\" github=\"mattcarrollcode\" twitter=\"mattcarrollcode\" threads=\"mattcarrollcode\" title=\"Developer Advocate at Meta\">\n    Matt는 어쩌다 보니 코딩의 세계에 발을 들였습니다. 그 후 혼자서는 만들 수 없는 것들을 커뮤니티와 함께 만드는 일에 매료되었죠. React를 다루기 전에는 YouTube, Google Assistant, Fuchsia, Google Cloud AI, 그리고 Evernote에서 일했습니다. 일을 하지 않을 때에는 등산, 재즈 감상, 가족과 시간을 보내는 것을 즐깁니다.\n</TeamMember>\n\n<TeamMember name=\"Mike Vitousek\" permalink=\"mike-vitousek\" photo=\"/images/team/mike.jpg\" github=\"mvitousek\" title=\"Engineer at Meta\">\n    Mike는 교수를 꿈꾸며 대학원에 들어갔지만, 연구 제안서를 작성하는 것보다 직접 무언가를 만드는 일이 더 적성에 맞는다는 것을 깨달았습니다. Meta에서 자바스크립트 인프라를 담당하며 경력을 쌓았고, 이것이 계기가 되어 React 컴파일러를 개발하게 되었습니다. 코딩을 하지 않는 시간에는 미국 북서부 지역에서 하이킹을 하거나 스키를 타며 시간을 보냅니다.\n</TeamMember>\n\n<TeamMember name=\"Mofei Zhang\" permalink=\"mofei-zhang\" photo=\"/images/team/mofei-zhang.png\" github=\"mofeiZ\" threads=\"z_mofei\" title=\"Engineer at Meta\">\n    Mofei는 비디오 게임에서 치트를 만들 수 있다는 사실을 깨닫고 프로그래밍을 시작했습니다. 학부와 대학원에서는 운영체제에 집중했지만, 지금은 React를 가지고 이것저것 만드는 것을 즐기고 있습니다. 업무 외에는 클라이밍 코스를 디버깅하거나 배낭여행을 계획하는 것을 좋아합니다.\n</TeamMember>\n\n<TeamMember name=\"Pieter Vanderwerff\" permalink=\"pieter-vanderwerff\" photo=\"/images/team/pieter.jpg\" github=\"pieterv\" threads=\"pietervanderwerff\" title=\"Engineer at Meta\">\n    Pieter는 건축공학을 공부했지만, 취업에 실패하자 직접 웹사이트를 만들었고, 그것이 그의 기술 커리어 시작점이 되었습니다. Meta에서 성능 및 언어 관련 업무를 담당했고, 지금은 React에 집중하고 있습니다. 일 외의 시간에는 산으로 오프로드 여행을 떠납니다.ㄴ\n</TeamMember>\n\n<TeamMember name=\"Rick Hanlon\" permalink=\"rick-hanlon\" photo=\"/images/team/rickhanlonii.jpg\" github=\"rickhanlonii\" twitter=\"rickhanlonii\" threads=\"rickhanlonii\" bsky=\"ricky.fm\" title=\"Engineer at Meta\">\n    Ricky는 이론 수학을 전공했지만, 어쩌다 보니 몇 년간 React Native 팀에서 일하다가 React 팀에 합류했습니다. 코딩을 하지 않을 때에는 스노보드, 자전거, 암벽 등반, 골프를 즐기거나 이슈 템플릿 양식에 맞지 않는 GitHub 이슈를 닫는 일을 합니다.\n</TeamMember>\n\n<TeamMember name=\"Ruslan Lesiutin\" permalink=\"ruslan-lesiutin\" photo=\"/images/team/lesiutin.jpg\" github=\"hoxyq\" twitter=\"ruslanlesiutin\" threads=\"lesiutin\" title=\"Engineer at Meta\">\n    Ruslan이 UI 프로그래밍에 처음 발을 들인 것은 어린 시절 자신만의 게임 포럼 HTML 템플릿을 손수 편집하면서였습니다. 어쩌다보니 결국 컴퓨터 과학을 전공하게 되었죠. 그는 음악, 게임, 그리고 밈을 좋아하는데, 주로 밈을 가장 즐겨 봅니다.\n</TeamMember>\n\n<TeamMember name=\"Sebastian Markbåge\" permalink=\"sebastian-markbåge\" photo=\"/images/team/sebmarkbage.jpg\" github=\"sebmarkbage\" twitter=\"sebmarkbage\" threads=\"sebmarkbage\" title=\"Engineer at Vercel\">\n    Sebastian은 심리학을 전공했습니다. 평소에는 말이 별로 없는데, 가끔 하는 말이 몇 달 후에야 비로소 의미를 이해할 수 있을 때도 있습니다. 성은 **“mark-boa-geh”**가 올바른 발음이지만, 현실적으로 “mark-beige”로 부르기로 타협했고, 그는 React를 사용하면서도 종종 타협하곤 합니다.\n</TeamMember>\n\n<TeamMember name=\"Sebastian Silbermann\" permalink=\"sebastian-silbermann\" photo=\"/images/team/sebsilbermann.jpg\" github=\"eps1lon\" twitter=\"sebsilbermann\" threads=\"sebsilbermann\" title=\"Engineer at Vercel\">\n    Sebastian은 수업 중에 하던 브라우저 게임을 더 재미있게 즐기려고 프로그래밍을 배웠습니다. 이것이 계기가 되어 가능한 한 많은 오픈소스 프로젝트에 기여하게 되었죠. 코딩 외에는 React 커뮤니티의 다른 Sebastian과 Zilberman과 혼동되지 않도록 자신을 알리는 데 힘쓰고 있습니다.\n</TeamMember>\n\n<TeamMember name=\"Seth Webster\" permalink=\"seth-webster\" photo=\"/images/team/seth.jpg\" github=\"sethwebster\" twitter=\"sethwebster\" threads=\"sethwebster\" personal=\"sethwebster.com\" title=\"Engineering Manager at Meta\">\n    Seth는 어릴 적 애리조나주 투손에서 자라며 프로그래밍을 시작했습니다. 학교를 마친 뒤 음악의 매력에 푹 빠져 약 10년간 투어 음악가로 활동했고, 이후 Intuit에서 일하며 직장으로 복귀했습니다. 여가 시간에는 [사진 촬영](https://www.sethwebster.com)을 하거나 미국 북동부에서 동물 구조를 위한 비행을 즐깁니다.\n</TeamMember>\n\n<TeamMember name=\"Sophie Alpert\" permalink=\"sophie-alpert\" photo=\"/images/team/sophiebits.jpg\" github=\"sophiebits\" twitter=\"sophiebits\" threads=\"sophiebits\" personal=\"sophiebits.com\" title=\"Independent Engineer\">\n    React가 출시되고 4일 뒤, Sophie는 진행하던 프로젝트 전체를 React로 다시 만들었는데, 지금 생각해보기엔 좀 무모했다고 합니다. 그렇게 프로젝트의 주요 기여자가 된 후 다른 사람들은 Facebook에서 급여를 받는데 왜 자신은 받지 못하는지 궁금해했고, 결국 공식적으로 팀에 합류해 React의 초기 성장을 이끌었습니다. 비록 몇 년 전에 회사를 그만두었지만, 여전히 팀의 그룹 채팅방에 남아 \"가치를 제공\"하고 있습니다.\n</TeamMember>\n\n<TeamMember name=\"Yuzhi Zheng\" permalink=\"yuzhi-zheng\" photo=\"/images/team/yuzhi.jpg\" github=\"yuzhi\" twitter=\"yuzhiz\" threads=\"yuzhiz\" title=\"Engineering Manager at Meta\">\n    Yuzhi는 학교에서 컴퓨터 과학을 전공했습니다. 실험실에 가지 않아도 코드가 바로 실행되는 즉각적인 만족감에 매료되었죠. 현재는 React 팀의 매니저로 일하고 있으며, 그전에는 Relay 데이터 페칭 프레임워크 개발에 참여했습니다. 여가 시간에는 정원을 가꾸거나 집안을 보수하며 삶을 최적화하는 것을 즐깁니다.\n</TeamMember>\n\n## Past contributors {/*past-contributors*/}\n\n과거 팀 멤버와 수년에 걸쳐 React에 크게 기여한 사람들은 [감사의 말](/community/acknowledgements) 페이지에서 확인할 수 있습니다.\n"
  },
  {
    "path": "src/content/community/translations.md",
    "content": "---\ntitle: 번역\n---\n\n<Intro>\n\nReact 문서는 전 세계의 글로벌 커뮤니티에 의해 다양한 언어로 번역되고 있습니다.\n\n</Intro>\n\n## 원본 사이트 {/*main-site*/}\n\n모든 번역은 공식 원본 문서를 기반으로 제공됩니다.\n\n- [English](https://react.dev/) &mdash; [기여하기](https://github.com/reactjs/react.dev/)\n\n## 완전한 번역 {/*full-translations*/}\n\n{/* 만약, 당신이 메인테이너이며 여기에 언어를 추가하고 싶다면, \"Core\" 번역을 완료하고 `src/utils` 아래의 `deployedTranslations`를 편집하세요. */}\n\n<LanguageList progress=\"complete\" />\n\n## 진행 중인 번역 {/*in-progress-translations*/}\n\n각 번역의 진행 상황은 [Is React Translated Yet?](https://translations.react.dev/)을 참조하세요.\n\n<LanguageList progress=\"in-progress\" />\n\n## 기여하는 방법 {/*how-to-contribute*/}\n\n번역 작업에 기여할 수 있습니다!\n\n커뮤니티는 \"react.dev\"의 각 언어별 포크<sup>Fork</sup>에서 React 문서의 번역 작업을 수행합니다. 일반적인 번역 작업은 Markdown 파일을 직접 번역하고 PR을 생성하는 것을 포함합니다. 위의 \"기여하기\" 링크를 클릭하여 해당 언어의 GitHub 저장소로 이동하고, 지침을 따라 번역 작업에 도움을 주실 수 있습니다. \n\n새로운 언어로 번역을 시작하고 싶다면 [translations.react.dev](https://github.com/reactjs/translations.react.dev)를 방문하세요.\n"
  },
  {
    "path": "src/content/community/versioning-policy.md",
    "content": "---\ntitle: 버전 관리 정책\n---\n\n<Intro>\nReact의 모든 안정적인 빌드는 높은 수준의 테스트를 거치고 유의적 버전<sup>Sementic Versioning, semver</sup>을 따릅니다. React는 또한 실험적인 기능에 대한 초기 피드백을 장려하기 위해 불안정한 릴리스 채널을 제공합니다. 이 페이지에서는 React 릴리스에서 기대할 수 있는 것에 대해 설명합니다.\n\n</Intro>\n\nThis versioning policy describes our approach to version numbers for packages such as `react` and `react-dom`. 지난 버전을 확인하려면, [React 버전](/versions) 페이지를 참고해주세요.\n\n## Stable releases {/*stable-releases*/}\n\nStable React releases (also known as \"Latest\" release channel) follow [semantic versioning (semver)](https://semver.org/lang/ko/) principles.\n\n버전 번호 **x.y.z**를 사용할 때 다음과 같습니다.\n\n* **치명적인 버그 수정**을 릴리즈할 때는 **패치 릴리즈**를 만들어 **z** 숫자를 변경합니다. (예: 15.6.2에서 15.6.3으로)\n* **새로운 기능**이나 **치명적이지 않은 버그 수정**을 릴리즈할 때는 **마이너 릴리즈**를 만들어 **y** 숫자를 변경합니다. (예: 15.6.2에서 15.7.0으로)\n* **Breaking Change**를 릴리즈할 때는 **메이저 릴리즈**를 만들어 **x** 숫자를 변경합니다. (예: 15.6.2에서 16.0.0으로)\n\n메이저 릴리즈는 새로운 기능을 포함할 수도 있고, 어떤 릴리즈든 버그 수정을 포함할 수도 있습니다.\n\n마이너 릴리즈는 릴리즈의 가장 흔한 유형입니다.\n\nWe know our users continue to use old versions of React in production. If we learn of a security vulnerability in React, we release a backported fix for all major versions that are affected by the vulnerability.\n\n### Breaking changes {/*breaking-changes*/}\n\nBreaking Changes는 모두에게 불편하기에 우리는 메이저 릴리즈의 수를 최소화하려고 노력합니다. 예를 들어, React 15는 2016년 4월에 릴리즈, React 16은 2017년 9월에 릴리즈, React 17은 2020년 10월에 릴리즈되었습니다.\n\n대신, 새로운 기능들을 마이너 버전으로 릴리즈합니다. 이는 마이너 릴리즈가 그 이름이 덜 주목받을지라도 종종 메이저 릴리즈보다 더 흥미로우며 매력적이라는 것을 의미합니다.\n\n### 안정성에 기여하기 {/*commitment-to-stability*/}\n\n시간이 지남에 따라 React를 변경할 때, 새로운 기능을 활용하는 데 필요한 노력을 최소화하려고 노력합니다. 가능한 경우, 오래된 API를 별개의 패키지에 넣는 한이 있더라도 작동하도록 합니다. 예를 들어, [믹스인<sup>Mixin</sup>은 몇 년 동안 권장되지 않았지만](https://legacy.reactjs.org/blog/2016/07/13/mixins-considered-harmful.html) [`create-react-class`를 통해](https://legacy.reactjs.org/docs/react-without-es6.html#mixins) 지금까지 지원하고 있으며, 많은 코드베이스가 이를 안정적인 레거시 코드로 계속 사용하고 있습니다.\n\n100만 명 이상의 개발자가 React를 사용하며 수백만 개의 컴포넌트를 일괄적으로 유지 관리합니다. 페이스북 코드베이스에만 5만개가 넘는 React 컴포넌트가 있습니다. 이는 우리가 새로운 React 버전으로 업그레이드하는 것을 가능한 한 쉽게 만들어야 한다는 것을 의미합니다. 만약 마이그레이션 과정 없이 큰 변화를 만든다면, 사람들은 오래된 버전에 갇히게 될 것입니다. 페이스북에서는 이러한 업그레이드 과정을 테스트합니다. 10명이 되지 않는 저희 팀이 5만 개가 넘는 컴포넌트를 업데이트할 수 있다면, React를 사용하는 사람이라면 누구나 업그레이드를 관리할 수 있을 것입니다. 대부분의 경우, 우리는 컴포넌트 문법을 업그레이드하기 위해 [자동화된 명령문](https://github.com/reactjs/react-codemod)을 작성하고, 이를 오픈소스 릴리즈에 포함해 모두가 사용할 수 있도록 합니다.\n\n### 경고를 활용한 점진적 업그레이드 {/*gradual-upgrades-via-warnings*/}\n\nReact의 개발 빌드에는 유용한 경고가 많이 포함되어 있습니다. 가능한 경우, 우리는 미래의 Breaking Change를 위해 경고를 추가합니다. 이 방법을 따르면, 최신 릴리스에서 경고가 없는 앱은 다음 메이저 릴리스와 호환됩니다. 이는 앱을 하나의 컴포넌트씩 업그레이드할 수 있도록 해줍니다.\n\n개발 빌드에서 나타나는 경고는 앱의 런타임 동작에 영향을 미치지 않습니다. 즉, 개발 빌드와 프로덕션 빌드 간 앱이 동일하게 동작할 것이라는 확신을 가질 수 있습니다. 유일한 차이점은 프로덕션 빌드에서 경고가 출력되지 않고 더 효율적이라는 것입니다. (만약 그렇지 않다면, 이슈를 제출해 주세요.)\n\n### 어떤 것들이 Breaking Change로 간주되나요? {/*what-counts-as-a-breaking-change*/}\n\n일반적으로, 다음 변경 사항들은 메이저 버전 번호를 변경하지 *않습니다*.\n* **개발 빌드 경고.** 프로덕션 동작에 영향을 미치지 않으므로, 우리는 메이저 버전 사이에 새로운 경고를 추가하거나 기존 경고를 수정할 수 있습니다. 이를 통해 앞으로 다가올 Breaking Change에 대해 안정적으로 경고할 수 있습니다.\n* **`unstable_`로 시작하는 API.** 이 API들은 아직 확정되지 않은 실험적 기능들로서 제공됩니다. `unstable_` 접두사를 사용하여 이들을 릴리즈함으로써, 더 빠르게 릴리즈를 반복하고 안정적인 API에 더 빠르게 도달할 수 있습니다.\n* **Alpha 버전과 Canary 버전의 React.** 이른 시일 내에 새로운 기능을 테스트하기 위해 Alpha 버전의 React를 제공하지만, Alpha 기간 동안 배운 것을 바탕으로 변경할 수 있는 유연성이 필요합니다. 이러한 버전을 사용하는 경우, 안정적인 릴리즈 이전에 API가 변경될 수 있음을 유의해야 합니다.\n* **문서화되지 않은 API와 내부 데이터 구조.** `__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED`나 `__reactInternalInstance$uk43rzhitjg`와 같은 내부 속성 이름에 접근하는 경우, 이에 대한 보장이 없습니다. 당신 스스로 해결해야 합니다.\n\n당신이 골머리를 앓는 것을 바라지 않기에, 이 정책은 실용적으로 설계되었습니다. 만약 이러한 변화를 모두 메이저 버전으로 릴리즈한다면, 더 많은 메이저 버전을 릴리즈하게 되고, 궁극적으로 버전 관리에 대해 커뮤니티에 더 많은 고통을 야기할 것입니다. 이는 우리가 원하는 만큼 React를 빠르게 개선할 수 없다는 것을 의미합니다.\n\n만약 목록에 있는 변경 사항이 커뮤니티에 광범위한 문제를 야기할 것으로 예상된다면, 우리는 점진적인 마이그레이션 과정을 제공하기 위해 최선을 다할 것입니다.\n\n### 마이너 릴리즈가 새로운 기능을 포함하지 않는다면, 왜 패치가 아닌가요? {/*if-a-minor-release-includes-no-new-features-why-isnt-it-a-patch*/}\n\n마이너 릴리즈가 새로운 기능을 포함하지 않을 수 있습니다. [시맨틱 버전에서 허용하듯,](https://semver.org/#spec-item-7) **\"[마이너 버전]은 비공개 코드 내에서 중요한 새로운 기능이나 개선 사항이 도입되었을 때 증가할 수 있습니다. 마이너 버전은 패치 레벨 변경을 포함할 수 있습니다.\"**\n\n그러나, 왜 이러한 릴리즈가 패치로 버전화되지 않는지 의문이 제기됩니다.\n\n이에 대한 해답은 React(또는 다른 소프트웨어)에 대한 어떠한 변경 사항도 얘기치 않은 방식으로 오류가 발생할 위험이 있다는 것입니다. 하나의 버그를 수정하는 패치 릴리즈가 다른 버그를 실수로 야기하는 경우를 상상해 보세요. 이는 개발자들에게만 방해가 되는 것이 아니라, 미래의 패치 릴리즈에 대한 신뢰도를 해칠 것입니다. 특히, 원래의 수정 사항이 실제로는 거의 접하지 못하는 버그를 위한 것이라면 더욱 유감입니다.\n\n우리는 React 릴리스를 버그 없이 유지하는 데에 있어 꽤 좋은 실적을 가지고 있으나, 패치 릴리스는 대부분의 개발자가 부작용 없이 적용할 수 있다고 가정하기 때문에 더 높은 신뢰도를 요구합니다.\n\n이러한 이유로 인해, 패치 릴리즈를 가장 중요한 버그와 보안 취약점에 대해서만 사용합니다.\n\n만약 릴리즈에 내부적인 리팩토링, 구현 세부 사항의 변경, 성능 개선, 작은 버그 수정과 같은 필수적이지 않은 변경 사항이 포함된다면, 새로운 기능이 없어도 마이너 버전을 증가시킵니다.\n\n## 모든 릴리즈 채널 {/*all-release-channels*/}\n\nReact는 버그 제출, 풀 리퀘스트 생성, [RFC 제출](https://github.com/reactjs/rfcs)을 위해 활발한 오픈소스 커뮤니티에 의존합니다. 피드백을 장려하기 위해 때때로 릴리즈되지 않은 기능을 포함하는 특별한 빌드를 공유합니다.\n\n<Note>\n\n이 섹션은 프레임워크, 라이브러리, 또는 개발자 도구를 활용해 작업하는 개발자와 가장 관련이 있습니다. 사용자 인터페이스를 구축하는 데에 주로 React를 사용하는 개발자는 릴리즈 채널에 대해 걱정할 필요가 없습니다.\n\n</Note>\n\n각각의 React 릴리즈 채널은 서로 다른 사용 사례를 위해 설계되었습니다.\n\n- [**최신 채널**](#latest-channel)은 안정적인 시맨틱 버전 React 릴리즈를 위한 채널입니다. npm에서 React를 설치할 때 확인할 수 있습니다. 최신 채널은 여러분들이 이미 사용하고 있는 채널입니다. **React를 직접 사용하는 사용자 인터페이스 애플리케이션은 이 채널을 사용합니다.**\n- [**카나리 채널**](#canary-channel)은 React 저장소의 메인 브랜치를 따릅니다. 다음 시맨틱 버전 릴리즈를 위한 릴리즈 후보로 간주할 수 있습니다. **[프레임워크나 엄선된 설정에서 고정 버전의 React와 함께 이 채널을 사용할 수 있습니다.](/blog/2023/05/03/react-canaries) 또한 React와 서드파티 프로젝트 간의 통합 테스트를 위해 카나리 채널을 사용할 수 있습니다.**\n- [**실험적 채널**](#experimental-channel)은 아직 안정된 릴리즈에 포함되지 않은 실험적인 API와 기능을 포함합니다. 추가적인 기능 플래그를 활성화한 채로 메인 브랜치를 따릅니다. 이 채널을 사용하여 릴리즈되기 전에 새로운 기능을 시도할 수 있습니다.\n\n모든 릴리즈는 npm에 공개되지만, 최신 채널만 시맨틱 버전을 사용합니다. 사전 릴리즈(카나리 채널과 실험적 채널)는 릴리즈의 내용과 커밋 날짜의 해시로부터 생성된 버전을 사용합니다. 예를 들어, 카나리 채널의 경우 `18.3.0-canary-388686f29-20230503`이고, 실험적 채널의 경우 `0.0.0-experimental-388686f29-20230503`입니다.\n\n**최신 채널과 카나리 채널 모두 사용자 인터페이스 애플리케이션을 공식적으로 지원하지만, 기대하는 바는 다릅니다.**\n\n* 최신 채널 릴리즈는 전통적인 시맨틱 버전 모델을 따릅니다.\n* 카나리 릴리즈는 [고정 버전](/blog/2023/05/03/react-canaries)이어야 하며, Breaking Changes를 포함할 수 있습니다. 자체적인 릴리즈 일정에 따라 새로운 React 기능과 버그 수정을 점진적으로 릴리즈하고자 하는 엄선된 설정(프레임워크와 같은)을 위해 존재합니다.\n\n실험적 릴리즈는 테스트 목적으로만 제공되며 릴리즈 간에 동작이 변경되지 않는다는 보장을 제공하지 않습니다. 최신 채널 릴리즈에 사용하는 시맨틱 버전 프로토콜을 따르지 않습니다.\n\n사전 릴리즈를 안정된 릴리즈에 사용하는 것과 동일한 레지스트리에 배포함으로써, npm workflow를 지원하는 [unpkg](https://unpkg.com)와 [CodeSandbox](https://codesandbox.io)와 같은 많은 도구를 활용할 수 있습니다.\n\n### 최신 채널 {/*latest-channel*/}\n\n최신 채널은 안정된 React 릴리즈를 위한 채널입니다. npm의 `latest` 태그에 해당합니다. 최신 채널은 실제 사용자들이 사용하게 되는 모든 React 앱에 대한 권장 채널입니다.\n\n**만약 어떤 채널을 사용해야 할지 불확실하다면, 최신 채널을 사용하세요.** 직접적으로 React를 사용하고 있다면, 이미 사용하고 있는 채널일 것입니다. 최신 채널의 업데이트는 매우 안정적이라고 기대할 수 있습니다. [안정적인 릴리즈](#stable-releases)에서 설명했듯, 버전은 시맨틱 버전 프로토콜을 따릅니다.\n\n### 카나리 채널 {/*canary-channel*/}\n\n카나리 채널은 React 저장소의 메인 브랜치를 따르는 사전 릴리즈 채널입니다. 카나리 채널의 릴리즈는 최신 채널의 릴리즈 후보로 간주할 수 있습니다. 카나리 채널은 더 자주 업데이트되는 최신 채널의 상위 집합이라고 생각할 수 있습니다.\n\n가장 최신의 카나리 릴리즈와 가장 최신의 최신 채널 릴리즈 사이의 변경 사항의 정도는 두 마이너 시멘틱 버전 릴리즈 사이의 정도와 비슷합니다. 그러나, **카나리 채널은 시맨틱 버전을 따르지 않습니다.** 카나리 채널의 연속적인 릴리즈 사이에는 때때로 변경 사항이 발생할 수 있습니다.\n\n**[카나리 워크플로우](/blog/2023/05/03/react-canaries)를 따르지 않는 한 사용자 인터페이스 애플리케이션에서 사전 릴리즈를 사용하지 마세요.**\n\n카나리 채널의 릴리즈는 npm에 'canary' 태그와 함께 게시됩니다. 버전은 `18.3.0-canary-388686f29-20230503`와 같이 빌드 내용과 커밋 날짜의 해시로부터 생성됩니다.\n\n#### 통합 테스트를 위해 카나리 채널을 사용하기 {/*using-the-canary-channel-for-integration-testing*/}\n\n카나리 채널은 React와 다른 프로젝트 간의 통합 테스트를 위해 사용할 수 있습니다.\n\nReact의 모든 변경 사항은 대중에게 공개되기 전에 광범위한 내부 테스트를 거칩니다. 그러나, React 생태계 전체에서 사용되는 수많은 환경과 설정이 있기 때문에, 모든 환경과 일정을 테스트할 수 없습니다.\n\n만약 당신이 서드파티 React 프레임워크, 라이브러리, 개발자 도구, 또는 유사한 인프라 유형 프로젝트의 저자라면, 최신 변경 사항에 대한 테스트 스위트를 주기적으로 실행함으로써 사용자와 전체 React 커뮤니티를 위해 React를 안정적으로 유지하는 데 도움을 줄 수 있습니다. 만약 관심이 있다면, 다음 단계를 따르세요.\n\n- 선호하는 통합 플랫폼을 사용하여 크론 작업을 설정하세요. 크론 작업은 [CircleCI](https://circleci.com/docs/2.0/triggers/#scheduled-builds)와 [Travis CI](https://docs.travis-ci.com/user/cron-jobs/)에서 모두 지원됩니다.\n- 크론 작업 내에서 npm의 `canary` 태그를 사용하여 React 패키지를 카나리 채널의 최신 React 릴리즈로 업데이트하세요. npm cli를 사용하면 다음과 같습니다.\n  ```console\n  npm update react@canary react-dom@canary\n  ```\n\n  또는 yarn을 사용하면 다음과 같습니다.\n\n  ```console\n  yarn upgrade react@canary react-dom@canary\n  ```\n- 업데이트된 패키지에 대해 테스트 스위트를 실행하세요.\n- 테스트가 모두 통과하면, 당신의 프로젝트는 다음 마이너 React 릴리즈와 함께 정상 작동할 것입니다.\n- 예상치 못한 문제가 발생하면, [이슈를 제출](https://github.com/facebook/react/issues)해 주세요.\n\nNext.js는 이 워크플로우를 사용하는 프로젝트입니다. 예시로 Next.js의 [CircleCI 설정](https://github.com/zeit/next.js/blob/c0a1c0f93966fe33edd93fb53e5fafb0dcd80a9e/.circleci/config.yml)을 참조할 수 있습니다.\n\n### 실험적 채널 {/*experimental-channel*/}\n\n카나리 채널과 마찬가지로 실험적 채널은 React 저장소의 메인 브랜치를 따르는 사전 릴리즈 채널입니다. 카나리 채널과 달리, 실험적 릴리즈에는 더 광범위한 릴리즈를 위해 준비되지 않은 추가 기능과 API가 포함됩니다.\n\n통상적으로, 카나리 채널의 업데이트는 상응하는 실험적 채널의 업데이트를 동반합니다. 같은 소스 버전을 기반으로 하지만, 다른 기능 플래그들을 사용하여 빌드합니다.\n\n실험적 릴리즈는 카나리 채널이나 최신 채널 릴리즈와 크게 다를 수 있습니다. **사용자 인터페이스 애플리케이션에서 실험적 릴리즈를 사용하지 마세요.** 실험적 채널의 릴리즈 사이에는 자주 Breaking Change가 발생할 수 있습니다.\n\n실험적 릴리즈는 npm에 `experimental` 태그와 함께 게시됩니다. 버전은 `0.0.0-experimental-68053d940-20210623`와 같이 빌드 내용과 커밋 날짜의 해시로부터 생성됩니다.\n\n#### 실험적 릴리즈에는 무엇이 포함되나요? {/*what-goes-into-an-experimental-release*/}\n\n실험적 기능은 더 많은 대중에게 공개될 준비가 되지 않은 기능이며, 최종적으로 공개되기 전에 크게 바뀔 수 있습니다. 일부 기능은 결국 공개되지 않을 수도 있습니다. 실험을 하는 이유는 제안된 변경 사항의 실현 가능성을 테스트하기 위해서입니다.\n\n만약 훅이 공개됐을 때 실험적 채널이 존재했다면, 우리는 훅을 최신 채널에 공개하기 전에 실험적 채널에 공개했을 것입니다.\n\n실험적 채널에 대해 통합 테스트를 실행하는 것이 중요하다고 생각할 수 있습니다. 이는 전적으로 당신에게 달려있습니다. 하지만 실험적 채널은 카나리 채널보다도 더 불안정할 수 있습니다. **실험적 릴리즈 간의 어떠한 안정성에 대해서도 보장하지 않습니다.**\n\n#### 실험적 기능에 대해 어떻게 더 알 수 있나요? {/*how-can-i-learn-more-about-experimental-features*/}\n\n실험적 기능은 문서화되지 않았을 수도 있습니다. 일반적으로, 실험적 기능들은 카나리 채널이나 최신 채널에 포함되기 전까지는 문서화되지 않습니다.\n\n문서화되지 않은 기능들은 [RFC](https://github.com/reactjs/rfcs)가 함께 제공될 수 있습니다.\n\n새로운 실험적 기능들이 준비되면 [React 블로그](/blog)에 게시될 것입니다. 그러나, 모든 실험적 기능들을 공개한다는 의미는 아닙니다.\n\n변경 사항에 대한 보다 자세한 내용은 깃허브 저장소의 [커밋 로그](https://github.com/facebook/react/commits/main)에서 확인할 수 있습니다.\n"
  },
  {
    "path": "src/content/community/videos.md",
    "content": "---\ntitle: React 영상\n---\n\n<Intro>\n\nReact와 React 생태계<sup>Ecosystem</sup>에 대한 토론 영상들입니다.\n\n</Intro>\n\n## React Conf 2024 {/*react-conf-2024*/}\n\nAt React Conf 2024, Meta CTO [Andrew \"Boz\" Bosworth](https://www.threads.net/@boztank) shared a welcome message to kick off the conference:\n\n<YouTubeIframe src=\"https://www.youtube.com/embed/T8TZQ6k4SLE?t=975s\" title=\"Boz and Seth Intro\" />\n\n### React 19 Keynote {/*react-19-keynote*/}\n\nIn the Day 1 keynote, we shared vision for React starting with React 19 and the React Compiler. Watch the full keynote from [Joe Savona](https://twitter.com/en_JS), [Lauren Tan](https://twitter.com/potetotes), [Andrew Clark](https://twitter.com/acdlite), [Josh Story](https://twitter.com/joshcstory), [Sathya Gunasekaran](https://twitter.com/_gsathya), and [Mofei Zhang](https://twitter.com/zmofei):\n\n\n<YouTubeIframe src=\"https://www.youtube.com/embed/lyEKhv8-3n0\" title=\"YouTube video player\" />\n\n### React Unpacked: A Roadmap to React 19 {/*react-unpacked-a-roadmap-to-react-19*/}\n\nReact 19 introduced new features including Actions, `use()`, `useOptimistic` and more. For a deep dive on using new features in React 19, see [Sam Selikoff's](https://twitter.com/samselikoff) talk:\n\n<YouTubeIframe src=\"https://www.youtube.com/embed/R0B2HsSM78s\" title=\"React Unpacked: A Roadmap to React 19\" />\n\n### What's New in React 19 {/*whats-new-in-react-19*/}\n\n[Lydia Hallie](https://twitter.com/lydiahallie) gave a visual deep dive of React 19's new features:\n\n<YouTubeIframe src=\"https://www.youtube.com/embed/AJOGzVygGcY\" title=\"What's New in React 19\" />\n\n### React 19 Deep Dive: Coordinating HTML {/*react-19-deep-dive-coordinating-html*/}\n\n[Josh Story](https://twitter.com/joshcstory) provided a deep dive on the document and resource streaming APIs in React 19:\n\n<YouTubeIframe src=\"https://www.youtube.com/embed/IBBN-s77YSI\" title=\"React 19 Deep Dive: Coordinating HTML\" />\n\n### React for Two Computers {/*react-for-two-computers*/}\n\n[Dan Abramov](https://bsky.app/profile/danabra.mov) imagined an alternate history where React started server-first:\n\n<YouTubeIframe src=\"https://www.youtube.com/embed/ozI4V_29fj4\" title=\"React for Two Computers\" />\n\n### Forget About Memo {/*forget-about-memo*/}\n\n[Lauren Tan](https://twitter.com/potetotes) gave a talk on using the React Compiler in practice:\n\n<YouTubeIframe src=\"https://www.youtube.com/embed/lvhPq5chokM\" title=\"Forget About Memo\" />\n\n### React Compiler Deep Dive {/*react-compiler-deep-dive*/}\n\n[Sathya Gunasekaran](https://twitter.com/_gsathya) and [Mofei Zhang](https://twitter.com/zmofei) provided a deep dive on how the React Compiler works:\n\n<YouTubeIframe src=\"https://www.youtube.com/embed/uA_PVyZP7AI\" title=\"React Compiler Deep Dive\" />\n\n### And more... {/*and-more-2024*/}\n\n**We also heard talks from the community on Server Components:**\n* [Enhancing Forms with React Server Components](https://www.youtube.com/embed/0ckOUBiuxVY&t=25280s) by [Aurora Walberg Scharff](https://twitter.com/aurorascharff)\n* [And Now You Understand React Server Components](https://www.youtube.com/embed/pOo7x8OiAec) by [Kent C. Dodds](https://twitter.com/kentcdodds)\n* [Real-time Server Components](https://www.youtube.com/embed/6sMANTHWtLM) by [Sunil Pai](https://twitter.com/threepointone)\n\n**Talks from React frameworks using new features:**\n\n* [Vanilla React](https://www.youtube.com/embed/ZcwA0xt8FlQ) by [Ryan Florence](https://twitter.com/ryanflorence)\n* [React Rhythm & Blues](https://www.youtube.com/embed/rs9X5MjvC4s) by [Lee Robinson](https://twitter.com/leeerob)\n* [RedwoodJS, now with React Server Components](https://www.youtube.com/embed/sjyY4MTECUU) by [Amy Dutton](https://twitter.com/selfteachme)\n* [Introducing Universal React Server Components in Expo Router](https://www.youtube.com/embed/djhEgxQf3Kw) by [Evan Bacon](https://twitter.com/Baconbrix)\n\n**And Q&As with the React and React Native teams:**\n- [React Q&A](https://www.youtube.com/embed/T8TZQ6k4SLE&t=27518s) hosted by [Michael Chan](https://twitter.com/chantastic)\n- [React Native Q&A](https://www.youtube.com/embed/0ckOUBiuxVY&t=27935s) hosted by [Jamon Holmgren](https://twitter.com/jamonholmgren)\n\nYou can watch all of the talks at React Conf 2024 at [conf2024.react.dev](https://conf2024.react.dev/talks).\n\n## React Conf 2021 {/*react-conf-2021*/}\n\n### React 18 기조연설 {/*react-18-keynote*/}\n\n기조연설에서 React 18을 시작으로 React의 미래에 대한 비전을 공유했습니다.\n\n[Andrew Clark](https://twitter.com/acdlite), [Juan Tejada](https://twitter.com/_jstejada), [Lauren Tan](https://twitter.com/potetotes), 그리고 [Rick Hanlon](https://twitter.com/rickhanlonii)의 전체 기조연설 보기.\n\n<YouTubeIframe src=\"https://www.youtube.com/embed/FZ0cG47msEk\" title=\"React 18 Keynote\" />\n\n### 애플리케이션 개발자를 위한 React 18 {/*react-18-for-application-developers*/}\n\nReact 18로 업그레이드하는 데모는 여기에서 [Shruti Kapoor](https://twitter.com/shrutikapoor08)의 강연을 참조하세요.\n\n<YouTubeIframe src=\"https://www.youtube.com/embed/ytudH8je5ko\" title=\"React 18 for Application Developers\" />\n\n### Suspense가 있는 스트리밍 서버 렌더링 {/*streaming-server-rendering-with-suspense*/}\n\nReact 18에는 Suspense를 사용한 서버 측 렌더링 성능 개선 사항도 포함되어 있습니다.\n\n스트리밍 서버 렌더링을 사용하면 서버의 React 컴포넌트에서 HTML을 생성하고 해당 HTML을 사용자에게 스트리밍할 수 있습니다. React 18에서는 'Suspense'를 사용하여 앱을 더 작은 독립 단위로 분해하여 나머지 앱을 차단하지 않고 서로 독립적으로 스트리밍할 수 있습니다. 이는 사용자가 콘텐츠를 더 빨리 보고 훨씬 빠르게 상호작용을 시작할 수 있다는 것을 의미합니다.\n\n더 자세히 알아보려면 [Shaundai Person](https://twitter.com/shaundai)의 강연을 참조하세요.\n\n<YouTubeIframe src=\"https://www.youtube.com/embed/pj5N-Khihgc\" title=\"Streaming Server Rendering with Suspense\" />\n\n### 첫 번째 React 워킹 그룹 {/*the-first-react-working-group*/}\n\nReact 18에서는 전문가, 개발자, 라이브러리 관리자, 교육자들로 구성된 패널과 협력하기 위해 첫 번째 워킹 그룹을 만들었습니다. 우리는 함께 점진적인 채택 전략을 세우고 `useId`, `useSyncExternalStore`, `useInsertionEffect`와 같은 새로운 API를 개선하기 위해 노력했습니다.\n\n이 작업에 대한 개요는 [Aakansha' Doshi](https://twitter.com/aakansha1216)의 강연을 참조하세요.\n\n<YouTubeIframe src=\"https://www.youtube.com/embed/qn7gRClrC9U\" title=\"The first React working group\" />\n\n### React 개발자 도구 {/*react-developer-tooling*/}\n\n이번 릴리즈의 새로운 기능을 지원하기 위해 새로 구성된 React 개발자 도구 팀과 개발자가 React 앱을 디버깅하는 데 도움이 되는 새로운 타임라인 프로파일러도 발표했습니다.\n\n새로운 개발자 도구 기능에 대한 자세한 내용과 데모는 [Brian Vaughn](https://twitter.com/brian_d_vaughn)의 강연을 참조하세요.\n\n<YouTubeIframe src=\"https://www.youtube.com/embed/oxDfrke8rZg\" title=\"React Developer Tooling\" />\n\n### memo 없는 React {/*react-without-memo*/}\n\n미래를 내다보며, [Xuan Huang(黄玄)](https://twitter.com/Huxpro)이 자동 메모화 컴파일러에 대한 React Labs 연구의 업데이트를 공유했습니다. 이 강연에서 자세한 정보와 컴파일러 프로토타입 데모를 확인하세요.\n\n<YouTubeIframe src=\"https://www.youtube.com/embed/lGEMwh32soc\" title=\"React without memo\" />\n\n### React 문서 기조연설 {/*react-docs-keynote*/}\n\n[Rachel Nabors](https://twitter.com/rachelnabors)가 React의 새 문서에 대한 투자와 관련한 기조연설로 React로 학습하고 디자인하는 방법에 대한 강연을 시작했습니다. ([현재 react.dev로 제공됨](/blog/2023/03/16/introducing-react-dev).)\n\n<YouTubeIframe src=\"https://www.youtube.com/embed/mneDaMYOKP8\" title=\"React docs keynote\" />\n\n### 그리고... {/*and-more*/}\n\n**React로 학습하고 설계하는 방법에 대한 강연.**\n\n* Debbie O'Brien: [새로운 React 문서에서 배운 것들](https://youtu.be/-7odLW_hG7s).\n* Sarah Rainsberger: [브라우저에서 학습하기](https://youtu.be/5X-WEQflCL0).\n* Linton Ye: [React로 디자인함에서의 ROI](https://youtu.be/7cPWmID5XAk).\n* Delba de Oliveira: [React를 이용한 상호작용 놀이터](https://youtu.be/zL8cz2W0z34).\n\n**Relay, React Native, PyTorch 팀의 강연.**\n\n* Robert Balicki: [Relay 다시 소개](https://youtu.be/lhVGdErZuN4).\n* Eric Rozell과 Steven Moyes: [React Native 데스크톱](https://youtu.be/9L4FFrvwJwY).\n* Roman Rädle: [React Native를 위한 온디바이스 머신러닝](https://youtu.be/NLj73vrc2I8).\n\n**접근성, 툴링 및 서버 컴포넌트에 대한 커뮤니티 강연.**\n\n* Daishi Kato: [외부 스토어 라이브러리를 위한 React 18](https://youtu.be/oPfSC5bQPR8).\n* Diego Haz: [React 18에서 접근 가능한 컴포넌트 구축하기](https://youtu.be/dcm8fjBfro8).\n* Tafu Nakazaki: [React로 접근 가능한 일본어 폼 컴포넌트](https://youtu.be/S4a0QlsH0pU).\n* Lyle Troxell: [아티스트를 위한 UI 도구](https://youtu.be/b3l4WxipFsE).\n* Helen Lin: [Hydrogen + React 18](https://youtu.be/HS6vIYkSNks).\n\n## 더 예전 영상들 {/*older-videos*/}\n\n### React Conf 2019 {/*react-conf-2019*/}\n\nReact Conf 2019의 영상 플레이리스트.\n<YouTubeIframe title=\"React Conf 2019\" src=\"https://www.youtube-nocookie.com/embed/playlist?list=PLPxbbTqCLbGHPxZpw4xj_Wwg8-fdNxJRh\" />\n\n### React Conf 2018 {/*react-conf-2018*/}\n\nReact Conf 2018의 영상 플레이리스트.\n<YouTubeIframe title=\"React Conf 2018\" src=\"https://www.youtube-nocookie.com/embed/playlist?list=PLPxbbTqCLbGE5AihOSExAa4wUM-P42EIJ\" />\n\n### React.js Conf 2017 {/*reactjs-conf-2017*/}\n\nReact.js Conf 2017의 영상 플레이리스트.\n<YouTubeIframe title=\"React.js Conf 2017\" src=\"https://www.youtube-nocookie.com/embed/playlist?list=PLb0IAmt7-GS3fZ46IGFirdqKTIxlws7e0\" />\n\n### React.js Conf 2016 {/*reactjs-conf-2016*/}\n\nReact.js Conf 2016의 영상 플레이리스트.\n<YouTubeIframe title=\"React.js Conf 2016\" src=\"https://www.youtube-nocookie.com/embed/playlist?list=PLb0IAmt7-GS0M8Q95RIc2lOM6nc77q1IY\" />\n\n### React.js Conf 2015 {/*reactjs-conf-2015*/}\n\nReact.js Conf 2015의 영상 플레이리스트.\n<YouTubeIframe title=\"React.js Conf 2015\" src=\"https://www.youtube-nocookie.com/embed/playlist?list=PLb0IAmt7-GS1cbw4qonlQztYV1TAW0sCr\" />\n\n### Best Practice 다시 생각해 보기 {/*rethinking-best-practices*/}\n\nJSConf EU 2013에서 Pete Hunt의 강연. 템플릿 개념을 버리고 자바스크립트를 사용하여 뷰<sup>View</sup>를 구축하는 방법, 데이터가 변경될 때 전체 애플리케이션을 리렌더링하는 방법, DOM 및 이벤트를 경량으로 구현하는 방법 등 세 가지 주제를 다루고 있습니다. (2013 - 0h30m).\n<YouTubeIframe title=\"Pete Hunt: React: Rethinking Best Practices - JSConf EU 2013\" src=\"https://www.youtube-nocookie.com/embed/x7cQ3mrcKaY\" />\n\n### React 소개 {/*introduction-to-react*/}\n\nFacebook Seattle에서 Tom Occhino와 Jordan Walke의 React 소개 (2013 - 1h20m).\n<YouTubeIframe title=\"Tom Occhino and Jordan Walke introduce React at Facebook Seattle\" src=\"https://www.youtube-nocookie.com/embed/XxVg_s8xAms\" />\n"
  },
  {
    "path": "src/content/errors/377.md",
    "content": "<Intro>\n\nIn the minified production build of React, we avoid sending down full error messages in order to reduce the number of bytes sent over the wire.\n\n</Intro>\n\nWe highly recommend using the development build locally when debugging your app since it tracks additional debug info and provides helpful warnings about potential problems in your apps, but if you encounter an exception while using the production build, this page will reassemble the original error message.\n\nThe full text of the error you just encountered is:\n\n<ErrorDecoder />\n\nThis error occurs when you pass a BigInt value from a Server Component to a Client Component.\n"
  },
  {
    "path": "src/content/errors/generic.md",
    "content": "<Intro>\n\nIn the minified production build of React, we avoid sending down full error messages in order to reduce the number of bytes sent over the wire.\n\n</Intro>\n\nWe highly recommend using the development build locally when debugging your app since it tracks additional debug info and provides helpful warnings about potential problems in your apps, but if you encounter an exception while using the production build, this page will reassemble the original error message.\n\nThe full text of the error you just encountered is:\n\n<ErrorDecoder />\n"
  },
  {
    "path": "src/content/errors/index.md",
    "content": "<Intro>\n\nIn the minified production build of React, we avoid sending down full error messages in order to reduce the number of bytes sent over the wire.\n\n</Intro>\n\n\nWe highly recommend using the development build locally when debugging your app since it tracks additional debug info and provides helpful warnings about potential problems in your apps, but if you encounter an exception while using the production build, the error message will include just a link to the docs for the error.\n\nFor an example, see: [https://react.dev/errors/149](/errors/149).\n"
  },
  {
    "path": "src/content/index.md",
    "content": "---\nid: home\ntitle: React\npermalink: index.html\n---\n\n{/* See HomeContent.js */}\n"
  },
  {
    "path": "src/content/learn/add-react-to-an-existing-project.md",
    "content": "---\ntitle: 기존 프로젝트에 React 추가하기\n---\n\n<Intro>\n\n기존 프로젝트에 상호작용 요소를 일부 추가하고 싶다면, React로 다시 작성할 필요가 없습니다. 기존 스택에 React를 추가하고 상호작용할 수 있는 React 컴포넌트를 어디에서나 렌더링하세요.\n\n</Intro>\n\n<Note>\n\n**로컬 개발 환경에 [Node.js](https://nodejs.org/en/)를 설치해야 합니다.** 온라인에서 [React](/learn/installation#try-react)를 시도하거나 간단한 HTML에서 React를 사용할 수도 있지만, 현실적인 개발을 위해 사용하는 대부분의 자바스크립트 도구에는 Node.js가 필요합니다.\n\n</Note>\n\n## 기존 웹사이트의 하위 경로 전체에 React 사용하기 {/*using-react-for-an-entire-subroute-of-your-existing-website*/}\n\n`example.com`이라는 또 다른 서버 기술(Rails와 같은)로 빌드한 기존 웹 앱이 있고, `example.com/some-app/`으로 시작하는 모든 경로를 React로 완전히 구현하고 싶다고 가정하겠습니다.\n\n다음과 같이 설정하는 것을 추천합니다.\n\n1. [React 기반 프레임워크](/learn/creating-a-react-app) 중 하나를 사용하여 **앱의 React 부분을 빌드하세요.**\n2. 사용하는 프레임워크 설정에서 **`/some-app` 을 *기본 경로*<sup>*Base Path*</sup>로 명시하세요**. (이때, [Next.js](https://nextjs.org/docs/app/api-reference/config/next-config-js/basePath), [Gatsby](https://www.gatsbyjs.com/docs/how-to/previews-deploys-hosting/path-prefix/)를 사용하세요!)\n3. **서버 또는 프록시를 구성**하여 `/some-app/` 하위의 모든 요청이 React 앱에서 처리되도록 하세요.\n\n이렇게 하면 앱의 React 부분이 해당 프레임워크에 [내장된 모범 사례](/learn/build-a-react-app-from-scratch#consider-using-a-framework)의 이점을 누릴 수 있습니다.\n\n많은 React 기반의 프레임워크는 풀스택이며 React 앱이 서버를 활용할 수 있도록 합니다. 그러나 서버에서 자바스크립트를 실행할 수 없거나 실행하고 싶지 않은 경우에도 동일한 접근방식을 사용할 수 있습니다. 이러한 경우에는 HTML/CSS/JS 내보내기(Next.js의 경우 [`next export` output](https://nextjs.org/docs/advanced-features/static-html-export), Gatsby의 경우 기본값)를 `/some-app/`에서 대신 제공하세요.\n\n## 기존 페이지의 일부분에 React 사용하기 {/*using-react-for-a-part-of-your-existing-page*/}\n\n이미 다른 기술(Rails와 같은 서버 기술 또는 Backbone과 같은 클라이언트 기술)로 빌드된 기존 페이지가 있고, 해당 페이지 어딘가에 상호작용할 수 있는 React 컴포넌트를 렌더링하고 싶다고 가정하겠습니다. 이는 React 컴포넌트를 통합하는 일반적인 방식입니다. 실제로 수년 동안 Meta에서 대부분의 React 사용을 이런 식으로 하였습니다!\n\n이 방식은 두 가지 단계로 수행할 수 있습니다.\n\n1. [JSX 구문](/learn/writing-markup-with-jsx)을 사용할 수 있게 **자바스크립트 환경을 설정**하고, [`import`](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Statements/import) / [`export`](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Statements/export) 구문을 통해 코드를 모듈로 분리한 다음, [npm](https://www.npmjs.com/) 패키지 레지스트리에서 패키지(예시: React)를 사용하세요.\n\n2. 해당 페이지에서 원하는 위치에 **React 컴포넌트를 렌더링하세요.**\n\n정확한 접근 방식은 기존 페이지의 설정에 따라 다르기 때문에, 몇 가지 세부 사항을 함께 살펴보겠습니다.\n\n### 1단계: 모듈 자바스크립트 환경 설정하기 {/*step-1-set-up-a-modular-javascript-environment*/}\n\n모듈 자바스크립트 환경은 모든 코드를 한 파일에 작성하는 것이 아닌, 각각의 React 컴포넌트를 개별 파일로 작성할 수 있게 합니다. 또한 (React 자체를 포함한) 다른 개발자들이 [npm](https://www.npmjs.com/) 레지스트리에 배포한 훌륭한 패키지들을 모두 사용할 수 있습니다. 이 작업을 수행하는 방법은 기존 설정에 따라 다릅니다.\n\n* **이미 애플리케이션이 `import` 문을 이용해 파일로 분리하고 있다면** 기존에 가지고 있는 설정을 이용해 보세요. JS 코드에서 `<div />`를 작성하면 문법 오류가 발생하는지 확인해 보세요. 문법 오류가 발생한다면 [Babel을 이용한 자바스크립트 코드 변환](https://babeljs.io/setup)이 필요할 수 있으며, JSX를 사용하려면 [Babel React 프리셋](https://babeljs.io/docs/babel-preset-react)을 활성화해야 할 수도 있습니다.\n\n* **애플리케이션이 자바스크립트 모듈을 컴파일하기 위한 기존 설정이 없다면,** [Vite](https://vite.dev/)를 이용하여 설정하세요. Vite 커뮤니티는 Rails, Django, Laravel을 포함한 [다양한 백엔드 프레임워크와의 통합](https://github.com/vitejs/awesome-vite#integrations-with-backends)을 지원하고 있습니다. 사용 중인 백엔드 프레임워크가 목록에 없다면 [가이드를 참고하여](https://vite.dev/guide/backend-integration.html) Vite 빌드를 백엔드와 수동으로 통합하세요.\n\n설정이 제대로 동작하는지 확인하려면 프로젝트 폴더에서 아래 명령어를 실행하세요.\n\n<TerminalBlock>\nnpm install react react-dom\n</TerminalBlock>\n\n그리고 메인 자바스크립트 파일(`index.js` 혹은 `main.js`라는 파일일 수 있습니다.)의 최상단에 다음 코드 라인을 추가하세요.\n\n<Sandpack>\n\n```html public/index.html hidden\n<!DOCTYPE html>\n<html>\n  <head><title>My app</title></head>\n  <body>\n    <!-- 기존 페이지 컨텐츠 (이 예시에서는 이 부분이 대체됩니다)-->\n    <div id=\"root\"></div>\n  </body>\n</html>\n```\n\n```js src/index.js active\nimport { createRoot } from 'react-dom/client';\n\n// 기존 HTML 컨텐츠를 지웁니다.\ndocument.body.innerHTML = '<div id=\"app\"></div>';\n\n// 대신에 여러분이 작성한 React 컴포넌트를 렌더링합니다.\nconst root = createRoot(document.getElementById('app'));\nroot.render(<h1>Hello, world</h1>);\n```\n\n</Sandpack>\n\n페이지의 전체 내용이 \"Hello, world!\"로 바뀌었다면 모든 것이 정상적으로 동작하고 있는 겁니다! 계속해서 읽어보세요.\n\n<Note>\n\n처음으로 기존 프로젝트에 모듈 자바스크립트 환경을 통합하기는 다소 어려워 보일 수 있으나, 그만한 가치가 있는 일입니다! 어려움을 겪는 부분이 있다면 [커뮤니티 리소스](/community)나 [Vite 채팅](https://chat.vite.dev/)을 이용해 보세요.\n\n</Note>\n\n\n### 2단계: 페이지 어디에서든 React 컴포넌트 렌더링하기 {/*step-2-render-react-components-anywhere-on-the-page*/}\n\n이전 단계에서는, 메인 파일 최상단에 아래 코드를 넣었습니다.\n\n```js\nimport { createRoot } from 'react-dom/client';\n\n// 기존 HTML 컨텐츠를 지웁니다.\ndocument.body.innerHTML = '<div id=\"app\"></div>';\n\n// 대신에 여러분이 작성한 React 컴포넌트를 렌더링합니다.\nconst root = createRoot(document.getElementById('app'));\nroot.render(<h1>Hello, world</h1>);\n```\n\n당연히 실제로는 기존 HTML 콘텐츠를 지우고 싶지 않을 겁니다!\n\n이 코드를 삭제하세요.\n\n대신 React 컴포넌트를 HTML의 특정 위치에 렌더링하고 싶을 것입니다. HTML 페이지를 열고(또는 이를 생성하는 서버 템플릿) HTML 태그에 고유한 [`id`](https://developer.mozilla.org/ko/docs/Web/HTML/Global_attributes/id) 어트리뷰트를 추가하세요.\n\n```html\n<!-- ... html의 어딘가 ... -->\n<nav id=\"navigation\"></nav>\n<!-- ... 더 많은 html ... -->\n```\n\n이렇게 하면 [`document.getElementById`](https://developer.mozilla.org/ko/docs/Web/API/Document/getElementById)로 HTML 엘리먼트를 찾아 [`createRoot`](/reference/react-dom/client/createRoot)에 전달함으로써 해당 요소 내부에 React 컴포넌트를 렌더링할 수 있습니다.\n\n<Sandpack>\n\n```html public/index.html\n<!DOCTYPE html>\n<html>\n  <head><title>My app</title></head>\n  <body>\n    <p>This paragraph is a part of HTML.</p>\n    <nav id=\"navigation\"></nav>\n    <p>This paragraph is also a part of HTML.</p>\n  </body>\n</html>\n```\n\n```js src/index.js active\nimport { createRoot } from 'react-dom/client';\n\nfunction NavigationBar() {\n  // TODO: 실제로 네비게이션 바를 구현합니다.\n  return <h1>Hello from React!</h1>;\n}\n\nconst domNode = document.getElementById('navigation');\nconst root = createRoot(domNode);\nroot.render(<NavigationBar />);\n```\n\n</Sandpack>\n\n기존에 존재하던 `index.html`의 원본 HTML 컨텐츠가 그대로 남아있는 것을 확인할 수 있습니다. 하지만 이제는 `<nav id=\"navigation\">` 안에 개발자가 직접 작성한 `NavigationBar` React 컴포넌트가 나타납니다. 기존 HTML 페이지에서 React 컴포넌트가 렌더링 되는 것에 대하여 더 알아보려면 [`createRoot` 사용법 문서](/reference/react-dom/client/createRoot#rendering-a-page-partially-built-with-react)를 읽어보세요.\n\n기존 프로젝트에서 React를 도입할 때, 일반적으로 작은 상호작용 컴포넌트(예시: 버튼)에서 시작하여 점진적으로 \"상위 구조로 확장하면서\" 결국에는 전체 페이지가 React로 빌드될 때까지 이 과정을 반복하게 됩니다. 이 지점에 도달한다면 React의 장점을 최대한 활용하기 위해 [React 프레임워크](/learn/creating-a-react-app)로 마이그레이션하는 것을 권장합니다.\n\n## 기존 네이티브 모바일 앱에서 React Native 사용하기 {/*using-react-native-in-an-existing-native-mobile-app*/}\n\n[React Native](https://reactnative.dev/) 역시 기존 네이티브 앱에 점진적으로 통합할 수 있습니다. 안드로이드(Java 또는 Kotlin)나 iOS(Objective-C 또는 Swift) 앱을 개발하고 있다면, [가이드를 참고하여](https://reactnative.dev/docs/integration-with-existing-apps) React Native 화면을 추가해보세요.\n"
  },
  {
    "path": "src/content/learn/adding-interactivity.md",
    "content": "---\ntitle: 상호작용 추가하기\n---\n\n<Intro>\n\n화면의 일부 요소는 사용자의 입력에 따라 업데이트됩니다. 예를 들어 이미지 갤러리에서 특정 이미지를 클릭하면 해당 이미지가 활성 상태가 됩니다. React에서는 시간에 따라 변화하는 데이터를 *state*라고 합니다. state는 어떠한 컴포넌트에든 추가할 수 있으며 필요에 따라 업데이트할 수도 있습니다. 이번 장에서는 상호작용을 다루는 컴포넌트를 작성하고 state를 업데이트하며, 시간에 따라 화면을 갱신하는 방법에 대해서 알아보겠습니다.\n\n</Intro>\n\n<YouWillLearn isChapter={true}>\n\n* [사용자 이벤트를 처리하는 방법](/learn/responding-to-events)\n* [컴포넌트가 state를 이용하여 정보를 \"기억\"하는 방법](/learn/state-a-components-memory)\n* [React가 UI를 업데이트하는 두 가지 단계](/learn/render-and-commit)\n* [state가 변경된 후 바로 업데이트되지 않는 이유](/learn/state-as-a-snapshot)\n* [여러 개의 state 업데이트를 대기열에 추가하는 방법](/learn/queueing-a-series-of-state-updates)\n* [state에서 객체를 업데이트하는 방법](/learn/updating-objects-in-state)\n* [state에서 배열을 업데이트하는 방법](/learn/updating-arrays-in-state)\n\n</YouWillLearn>\n\n## 이벤트에 대한 응답 {/*responding-to-events*/}\n\nReact에서는 JSX에 *이벤트 핸들러*를 추가할 수 있습니다. 이벤트 핸들러는 클릭, 마우스 호버, 폼 인풋 포커스 등 사용자 상호작용에 따라 유발되는 사용자 정의 함수입니다.\n\n`<button>`과 같은 내장 컴포넌트는 `onClick`과 같은 내장 브라우저 이벤트만 지원합니다. 반면 사용자 정의 컴포넌트를 생성하는 경우, 컴포넌트 이벤트 핸들러 props의 역할에 맞는 원하는 이름을 사용할 수 있습니다.\n\n<Sandpack>\n\n```js\nexport default function App() {\n  return (\n    <Toolbar\n      onPlayMovie={() => alert('Playing!')}\n      onUploadImage={() => alert('Uploading!')}\n    />\n  );\n}\n\nfunction Toolbar({ onPlayMovie, onUploadImage }) {\n  return (\n    <div>\n      <Button onClick={onPlayMovie}>\n        Play Movie\n      </Button>\n      <Button onClick={onUploadImage}>\n        Upload Image\n      </Button>\n    </div>\n  );\n}\n\nfunction Button({ onClick, children }) {\n  return (\n    <button onClick={onClick}>\n      {children}\n    </button>\n  );\n}\n```\n\n```css\nbutton { margin-right: 10px; }\n```\n\n</Sandpack>\n\n<LearnMore path=\"/learn/responding-to-events\">\n\n이벤트 핸들러를 추가하는 방법을 배우려면 **[사용자 이벤트를 처리하는 방법](/learn/responding-to-events)** 을 읽어보세요.\n\n</LearnMore>\n\n## State: 컴포넌트의 메모리 {/*state-a-components-memory*/}\n\n상호작용에 따라 컴포넌트는 종종 화면에 표시되는 내용을 변경해야 합니다. 폼에 타이핑하면 입력 필드를 업데이트해야 하고, 이미지 캐러셀에서 \"다음\"을 클릭하면 표시되는 이미지가 변경되어야 하며, \"구매\"를 클릭하면 제품이 장바구니에 추가되어야 합니다. 컴포넌트는 현재 입력값, 현재 이미지, 장바구니에 담긴 상품 등을 \"기억\"해야 합니다. React에서는 이러한 컴포넌트별 메모리를 *state*라고 부릅니다.\n\n[`useState`](/reference/react/useState) Hook을 사용하면 컴포넌트에 state를 추가할 수 있습니다. *Hooks*는 컴포넌트가 React의 주요 기능(state는 그중 하나입니다.)을 사용할 수 있도록 해주는 특별한 함수입니다. `useState` Hook을 사용하면 state 변수를 선언할 수 있습니다. `useState`는 초기 state를 인자로 받으며, 현재 상태와 상태를 업데이트할 수 있는 상태 설정 함수를 배열에 담아 반환합니다.\n\n```js\nconst [index, setIndex] = useState(0);\nconst [showMore, setShowMore] = useState(false);\n```\n\n다음은 이미지 갤러리가 클릭 이벤트에 따라 state를 사용하고 업데이트하는 방법입니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\nimport { sculptureList } from './data.js';\n\nexport default function Gallery() {\n  const [index, setIndex] = useState(0);\n  const [showMore, setShowMore] = useState(false);\n  const hasNext = index < sculptureList.length - 1;\n\n  function handleNextClick() {\n    if (hasNext) {\n      setIndex(index + 1);\n    } else {\n      setIndex(0);\n    }\n  }\n\n  function handleMoreClick() {\n    setShowMore(!showMore);\n  }\n\n  let sculpture = sculptureList[index];\n  return (\n    <>\n      <button onClick={handleNextClick}>\n        Next\n      </button>\n      <h2>\n        <i>{sculpture.name} </i>\n        by {sculpture.artist}\n      </h2>\n      <h3>\n        ({index + 1} of {sculptureList.length})\n      </h3>\n      <button onClick={handleMoreClick}>\n        {showMore ? 'Hide' : 'Show'} details\n      </button>\n      {showMore && <p>{sculpture.description}</p>}\n      <img\n        src={sculpture.url}\n        alt={sculpture.alt}\n      />\n    </>\n  );\n}\n```\n\n```js src/data.js\nexport const sculptureList = [{\n  name: 'Homenaje a la Neurocirugía',\n  artist: 'Marta Colvin Andrade',\n  description: 'Although Colvin is predominantly known for abstract themes that allude to pre-Hispanic symbols, this gigantic sculpture, an homage to neurosurgery, is one of her most recognizable public art pieces.',\n  url: 'https://i.imgur.com/Mx7dA2Y.jpg',\n  alt: 'A bronze statue of two crossed hands delicately holding a human brain in their fingertips.'\n}, {\n  name: 'Floralis Genérica',\n  artist: 'Eduardo Catalano',\n  description: 'This enormous (75 ft. or 23m) silver flower is located in Buenos Aires. It is designed to move, closing its petals in the evening or when strong winds blow and opening them in the morning.',\n  url: 'https://i.imgur.com/ZF6s192m.jpg',\n  alt: 'A gigantic metallic flower sculpture with reflective mirror-like petals and strong stamens.'\n}, {\n  name: 'Eternal Presence',\n  artist: 'John Woodrow Wilson',\n  description: 'Wilson was known for his preoccupation with equality, social justice, as well as the essential and spiritual qualities of humankind. This massive (7ft. or 2,13m) bronze represents what he described as \"a symbolic Black presence infused with a sense of universal humanity.\"',\n  url: 'https://i.imgur.com/aTtVpES.jpg',\n  alt: 'The sculpture depicting a human head seems ever-present and solemn. It radiates calm and serenity.'\n}, {\n  name: 'Moai',\n  artist: 'Unknown Artist',\n  description: 'Located on the Easter Island, there are 1,000 moai, or extant monumental statues, created by the early Rapa Nui people, which some believe represented deified ancestors.',\n  url: 'https://i.imgur.com/RCwLEoQm.jpg',\n  alt: 'Three monumental stone busts with the heads that are disproportionately large with somber faces.'\n}, {\n  name: 'Blue Nana',\n  artist: 'Niki de Saint Phalle',\n  description: 'The Nanas are triumphant creatures, symbols of femininity and maternity. Initially, Saint Phalle used fabric and found objects for the Nanas, and later on introduced polyester to achieve a more vibrant effect.',\n  url: 'https://i.imgur.com/Sd1AgUOm.jpg',\n  alt: 'A large mosaic sculpture of a whimsical dancing female figure in a colorful costume emanating joy.'\n}, {\n  name: 'Ultimate Form',\n  artist: 'Barbara Hepworth',\n  description: 'This abstract bronze sculpture is a part of The Family of Man series located at Yorkshire Sculpture Park. Hepworth chose not to create literal representations of the world but developed abstract forms inspired by people and landscapes.',\n  url: 'https://i.imgur.com/2heNQDcm.jpg',\n  alt: 'A tall sculpture made of three elements stacked on each other reminding of a human figure.'\n}, {\n  name: 'Cavaliere',\n  artist: 'Lamidi Olonade Fakeye',\n  description: \"Descended from four generations of woodcarvers, Fakeye's work blended traditional and contemporary Yoruba themes.\",\n  url: 'https://i.imgur.com/wIdGuZwm.png',\n  alt: 'An intricate wood sculpture of a warrior with a focused face on a horse adorned with patterns.'\n}, {\n  name: 'Big Bellies',\n  artist: 'Alina Szapocznikow',\n  description: \"Szapocznikow is known for her sculptures of the fragmented body as a metaphor for the fragility and impermanence of youth and beauty. This sculpture depicts two very realistic large bellies stacked on top of each other, each around five feet (1,5m) tall.\",\n  url: 'https://i.imgur.com/AlHTAdDm.jpg',\n  alt: 'The sculpture reminds a cascade of folds, quite different from bellies in classical sculptures.'\n}, {\n  name: 'Terracotta Army',\n  artist: 'Unknown Artist',\n  description: 'The Terracotta Army is a collection of terracotta sculptures depicting the armies of Qin Shi Huang, the first Emperor of China. The army consisted of more than 8,000 soldiers, 130 chariots with 520 horses, and 150 cavalry horses.',\n  url: 'https://i.imgur.com/HMFmH6m.jpg',\n  alt: '12 terracotta sculptures of solemn warriors, each with a unique facial expression and armor.'\n}, {\n  name: 'Lunar Landscape',\n  artist: 'Louise Nevelson',\n  description: 'Nevelson was known for scavenging objects from New York City debris, which she would later assemble into monumental constructions. In this one, she used disparate parts like a bedpost, juggling pin, and seat fragment, nailing and gluing them into boxes that reflect the influence of Cubism’s geometric abstraction of space and form.',\n  url: 'https://i.imgur.com/rN7hY6om.jpg',\n  alt: 'A black matte sculpture where the individual elements are initially indistinguishable.'\n}, {\n  name: 'Aureole',\n  artist: 'Ranjani Shettar',\n  description: 'Shettar merges the traditional and the modern, the natural and the industrial. Her art focuses on the relationship between man and nature. Her work was described as compelling both abstractly and figuratively, gravity defying, and a \"fine synthesis of unlikely materials.\"',\n  url: 'https://i.imgur.com/okTpbHhm.jpg',\n  alt: 'A pale wire-like sculpture mounted on concrete wall and descending on the floor. It appears light.'\n}, {\n  name: 'Hippos',\n  artist: 'Taipei Zoo',\n  description: 'The Taipei Zoo commissioned a Hippo Square featuring submerged hippos at play.',\n  url: 'https://i.imgur.com/6o5Vuyu.jpg',\n  alt: 'A group of bronze hippo sculptures emerging from the sett sidewalk as if they were swimming.'\n}];\n```\n\n```css\nh2 { margin-top: 10px; margin-bottom: 0; }\nh3 {\n margin-top: 5px;\n font-weight: normal;\n font-size: 100%;\n}\nimg { width: 120px; height: 120px; }\nbutton {\n  display: block;\n  margin-top: 10px;\n  margin-bottom: 10px;\n}\n```\n\n</Sandpack>\n\n<LearnMore path=\"/learn/state-a-components-memory\">\n\n값을 기억하고 상호 작용에 따라 업데이트하는 방법을 배우려면 **[컴포넌트가 state를 이용하여 정보를 \"기억\"하는 방법](/learn/state-a-components-memory)** 을 읽어보세요.\n\n</LearnMore>\n\n## 렌더링과 반영 {/*render-and-commit*/}\n\n컴포넌트는 화면에 표시되기 전에 React에 의해 렌더링 되어야 합니다. 이 과정을 이해하면 코드가 어떻게 실행되는지 파악하고 코드의 동작을 설명하는 데 도움이 될 것입니다.\n\n컴포넌트가 주방에서 재료들을 사용해 맛있는 요리를 조리하는 요리사라고 생각해 봅시다. 이 시나리오에서 React는 손님들로부터 주문받고 요리사에게 주문을 가져다주는 웨이터입니다. 이때, UI를 주문하고 서빙하는 과정은 세 단계로 이루어집니다.\n\n1. 렌더링 **유발** (주방에 식사 주문을 전달하기)\n2. 컴포넌트 **렌더링** (주방에서 주문을 준비하기)\n3. DOM에 **반영** (주문을 테이블에 서빙하기)\n\n<IllustrationBlock sequential>\n  <Illustration caption=\"Trigger\" alt=\"React as a server in a restaurant, fetching orders from the users and delivering them to the Component Kitchen.\" src=\"/images/docs/illustrations/i_render-and-commit1.png\" />\n  <Illustration caption=\"Render\" alt=\"The Card Chef gives React a fresh Card component.\" src=\"/images/docs/illustrations/i_render-and-commit2.png\" />\n  <Illustration caption=\"Commit\" alt=\"React delivers the Card to the user at their table.\" src=\"/images/docs/illustrations/i_render-and-commit3.png\" />\n</IllustrationBlock>\n\n<LearnMore path=\"/learn/render-and-commit\">\n\nUI 업데이트의 생명주기를 배우려면 **[React가 UI를 업데이트하는 두 가지 단계](/learn/render-and-commit)** 를 읽어보세요.\n\n</LearnMore>\n\n## snapshot으로서의 state {/*state-as-a-snapshot*/}\n\n일반적인 JavaScript 변수와 달리, React의 state는 snapshot과 유사하게 동작합니다. 상태를 갱신하면 이미 있는 state 변수 자체를 변경하는 것이 아니라, 리렌더링을 유발합니다. 이는 처음에는 놀라울 수 있습니다!\n\n```js\nconsole.log(count);  // 0\nsetCount(count + 1); // Request a re-render with 1\nconsole.log(count);  // Still 0!\n```\n\n이 동작은 미묘한 버그를 피하는 데 도움이 됩니다. 간단한 채팅 앱을 예시로 들겠습니다. \"Send\"를 먼저 누른 *다음* 수신자를 Bob으로 변경하면 어떻게 될지 추측해 보세요. 5초 후에 `alert`에 어떤 이름이 나타날까요?\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function Form() {\n  const [to, setTo] = useState('Alice');\n  const [message, setMessage] = useState('Hello');\n\n  function handleSubmit(e) {\n    e.preventDefault();\n    setTimeout(() => {\n      alert(`You said ${message} to ${to}`);\n    }, 5000);\n  }\n\n  return (\n    <form onSubmit={handleSubmit}>\n      <label>\n        To:{' '}\n        <select\n          value={to}\n          onChange={e => setTo(e.target.value)}>\n          <option value=\"Alice\">Alice</option>\n          <option value=\"Bob\">Bob</option>\n        </select>\n      </label>\n      <textarea\n        placeholder=\"Message\"\n        value={message}\n        onChange={e => setMessage(e.target.value)}\n      />\n      <button type=\"submit\">Send</button>\n    </form>\n  );\n}\n```\n\n```css\nlabel, textarea { margin-bottom: 10px; display: block; }\n```\n\n</Sandpack>\n\n\n<LearnMore path=\"/learn/state-as-a-snapshot\">\n\n이벤트 핸들러 내에서 state가 \"고정되어\" 변하지 않는 것처럼 보이는 이유에 대하여 배우려면 **[state가 변경된 후 바로 업데이트되지 않는 이유](/learn/state-as-a-snapshot)** 를 읽어보세요.\n\n</LearnMore>\n\n## state 업데이트를 연속으로 대기열에 추가하기 {/*queueing-a-series-of-state-updates*/}\n\n이 컴포넌트는 버그가 있습니다. \"+3\"을 클릭하면 점수가 한 번만 증가합니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function Counter() {\n  const [score, setScore] = useState(0);\n\n  function increment() {\n    setScore(score + 1);\n  }\n\n  return (\n    <>\n      <button onClick={() => increment()}>+1</button>\n      <button onClick={() => {\n        increment();\n        increment();\n        increment();\n      }}>+3</button>\n      <h1>Score: {score}</h1>\n    </>\n  )\n}\n```\n\n```css\nbutton { display: inline-block; margin: 10px; font-size: 20px; }\n```\n\n</Sandpack>\n\n[state가 변경된 후 바로 업데이트되지 않는 이유](/learn/state-as-a-snapshot)는 이런 현상이 발생하는 이유를 설명해 줍니다. state를 설정하면 리렌더링이 유발되지만, 이미 실행 중인 코드에서는 변경되지 않습니다. 따라서 `setScore(score + 1)`를 호출한 직후에도 `score`는 여전히 `0`으로 유지됩니다.\n\n```js\nconsole.log(score);  // 0\nsetScore(score + 1); // setScore(0 + 1);\nconsole.log(score);  // 0\nsetScore(score + 1); // setScore(0 + 1);\nconsole.log(score);  // 0\nsetScore(score + 1); // setScore(0 + 1);\nconsole.log(score);  // 0\n```\n\n이 문제는 state를 설정할 때 *updater function*을 전달하는 방식을 통해 해결할 수 있습니다. `setScore(score + 1)`를 `setScore(s => s + 1)`로 대체함으로써 \"+3\" 버튼이 수정되는 것을 확인할 수 있습니다. 이러한 방법으로 여러 개의 state 업데이트를 대기열에 추가할 수 있습니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function Counter() {\n  const [score, setScore] = useState(0);\n\n  function increment() {\n    setScore(s => s + 1);\n  }\n\n  return (\n    <>\n      <button onClick={() => increment()}>+1</button>\n      <button onClick={() => {\n        increment();\n        increment();\n        increment();\n      }}>+3</button>\n      <h1>Score: {score}</h1>\n    </>\n  )\n}\n```\n\n```css\nbutton { display: inline-block; margin: 10px; font-size: 20px; }\n```\n\n</Sandpack>\n\n<LearnMore path=\"/learn/queueing-a-series-of-state-updates\">\n\nstate 업데이트를 연속적으로 대기열에 추가하는 방법을 배우려면 **[여러 개의 state 업데이트를 대기열에 추가하는 방법](/learn/queueing-a-series-of-state-updates)** 을 읽어보세요.\n\n</LearnMore>\n\n## state 내 객체 업데이트 {/*updating-objects-in-state*/}\n\nState는 객체를 포함하여 모든 종류의 JavaScript 타입을 관리할 수 있습니다. 그러나 React state에 있는 객체와 배열을 직접 변경해서는 안 됩니다. 대신 객체나 배열을 업데이트할 때는 새로운 객체를 생성하거나 기존 객체의 복사본을 만들어서 상태를 업데이트해야 합니다.\n\n일반적으로 변경하려는 객체나 배열을 복사하기 위해 `...` 전개 구문을 사용합니다. 예를 들어 중첩된 객체의 업데이트는 다음과 같이 처리할 수 있습니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function Form() {\n  const [person, setPerson] = useState({\n    name: 'Niki de Saint Phalle',\n    artwork: {\n      title: 'Blue Nana',\n      city: 'Hamburg',\n      image: 'https://i.imgur.com/Sd1AgUOm.jpg',\n    }\n  });\n\n  function handleNameChange(e) {\n    setPerson({\n      ...person,\n      name: e.target.value\n    });\n  }\n\n  function handleTitleChange(e) {\n    setPerson({\n      ...person,\n      artwork: {\n        ...person.artwork,\n        title: e.target.value\n      }\n    });\n  }\n\n  function handleCityChange(e) {\n    setPerson({\n      ...person,\n      artwork: {\n        ...person.artwork,\n        city: e.target.value\n      }\n    });\n  }\n\n  function handleImageChange(e) {\n    setPerson({\n      ...person,\n      artwork: {\n        ...person.artwork,\n        image: e.target.value\n      }\n    });\n  }\n\n  return (\n    <>\n      <label>\n        Name:\n        <input\n          value={person.name}\n          onChange={handleNameChange}\n        />\n      </label>\n      <label>\n        Title:\n        <input\n          value={person.artwork.title}\n          onChange={handleTitleChange}\n        />\n      </label>\n      <label>\n        City:\n        <input\n          value={person.artwork.city}\n          onChange={handleCityChange}\n        />\n      </label>\n      <label>\n        Image:\n        <input\n          value={person.artwork.image}\n          onChange={handleImageChange}\n        />\n      </label>\n      <p>\n        <i>{person.artwork.title}</i>\n        {' by '}\n        {person.name}\n        <br />\n        (located in {person.artwork.city})\n      </p>\n      <img\n        src={person.artwork.image}\n        alt={person.artwork.title}\n      />\n    </>\n  );\n}\n```\n\n```css\nlabel { display: block; }\ninput { margin-left: 5px; margin-bottom: 5px; }\nimg { width: 200px; height: 200px; }\n```\n\n</Sandpack>\n\n객체를 복사하는 작업이 번거롭다면 [Immer](https://github.com/immerjs/use-immer)와 같은 라이브러리를 사용하여 반복적인 코드를 줄일 수 있습니다.\n\n<Sandpack>\n\n```js\nimport { useImmer } from 'use-immer';\n\nexport default function Form() {\n  const [person, updatePerson] = useImmer({\n    name: 'Niki de Saint Phalle',\n    artwork: {\n      title: 'Blue Nana',\n      city: 'Hamburg',\n      image: 'https://i.imgur.com/Sd1AgUOm.jpg',\n    }\n  });\n\n  function handleNameChange(e) {\n    updatePerson(draft => {\n      draft.name = e.target.value;\n    });\n  }\n\n  function handleTitleChange(e) {\n    updatePerson(draft => {\n      draft.artwork.title = e.target.value;\n    });\n  }\n\n  function handleCityChange(e) {\n    updatePerson(draft => {\n      draft.artwork.city = e.target.value;\n    });\n  }\n\n  function handleImageChange(e) {\n    updatePerson(draft => {\n      draft.artwork.image = e.target.value;\n    });\n  }\n\n  return (\n    <>\n      <label>\n        Name:\n        <input\n          value={person.name}\n          onChange={handleNameChange}\n        />\n      </label>\n      <label>\n        Title:\n        <input\n          value={person.artwork.title}\n          onChange={handleTitleChange}\n        />\n      </label>\n      <label>\n        City:\n        <input\n          value={person.artwork.city}\n          onChange={handleCityChange}\n        />\n      </label>\n      <label>\n        Image:\n        <input\n          value={person.artwork.image}\n          onChange={handleImageChange}\n        />\n      </label>\n      <p>\n        <i>{person.artwork.title}</i>\n        {' by '}\n        {person.name}\n        <br />\n        (located in {person.artwork.city})\n      </p>\n      <img\n        src={person.artwork.image}\n        alt={person.artwork.title}\n      />\n    </>\n  );\n}\n```\n\n```json package.json\n{\n  \"dependencies\": {\n    \"immer\": \"1.7.3\",\n    \"react\": \"latest\",\n    \"react-dom\": \"latest\",\n    \"react-scripts\": \"latest\",\n    \"use-immer\": \"0.5.1\"\n  },\n  \"scripts\": {\n    \"start\": \"react-scripts start\",\n    \"build\": \"react-scripts build\",\n    \"test\": \"react-scripts test --env=jsdom\",\n    \"eject\": \"react-scripts eject\"\n  }\n}\n```\n\n```css\nlabel { display: block; }\ninput { margin-left: 5px; margin-bottom: 5px; }\nimg { width: 200px; height: 200px; }\n```\n\n</Sandpack>\n\n<LearnMore path=\"/learn/updating-objects-in-state\">\n\n객체를 올바르게 업데이트하는 방법을 배우려면 **[state에서 객체를 업데이트하는 방법](/learn/updating-objects-in-state)** 을 읽어보세요.\n\n</LearnMore>\n\n## state 내 배열 업데이트 {/*updating-arrays-in-state*/}\n\n배열 또한 state에 저장될 때 읽기 전용으로 다루어야 하는 가변 JavaScript 객체입니다. 객체와 마찬가지로 상태에 저장된 배열을 업데이트하려면 새로운 배열을 생성하거나 기존 배열의 복사본을 만들어서 상태를 업데이트한 후, 새로운 배열을 상태에 설정해야 합니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nconst initialList = [\n  { id: 0, title: 'Big Bellies', seen: false },\n  { id: 1, title: 'Lunar Landscape', seen: false },\n  { id: 2, title: 'Terracotta Army', seen: true },\n];\n\nexport default function BucketList() {\n  const [list, setList] = useState(\n    initialList\n  );\n\n  function handleToggle(artworkId, nextSeen) {\n    setList(list.map(artwork => {\n      if (artwork.id === artworkId) {\n        return { ...artwork, seen: nextSeen };\n      } else {\n        return artwork;\n      }\n    }));\n  }\n\n  return (\n    <>\n      <h1>Art Bucket List</h1>\n      <h2>My list of art to see:</h2>\n      <ItemList\n        artworks={list}\n        onToggle={handleToggle} />\n    </>\n  );\n}\n\nfunction ItemList({ artworks, onToggle }) {\n  return (\n    <ul>\n      {artworks.map(artwork => (\n        <li key={artwork.id}>\n          <label>\n            <input\n              type=\"checkbox\"\n              checked={artwork.seen}\n              onChange={e => {\n                onToggle(\n                  artwork.id,\n                  e.target.checked\n                );\n              }}\n            />\n            {artwork.title}\n          </label>\n        </li>\n      ))}\n    </ul>\n  );\n}\n```\n\n</Sandpack>\n\n배열을 복사하는 작업이 번거롭다면 [Immer](https://github.com/immerjs/use-immer)와 같은 라이브러리를 사용하여 반복적인 코드를 줄일 수 있습니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\nimport { useImmer } from 'use-immer';\n\nconst initialList = [\n  { id: 0, title: 'Big Bellies', seen: false },\n  { id: 1, title: 'Lunar Landscape', seen: false },\n  { id: 2, title: 'Terracotta Army', seen: true },\n];\n\nexport default function BucketList() {\n  const [list, updateList] = useImmer(initialList);\n\n  function handleToggle(artworkId, nextSeen) {\n    updateList(draft => {\n      const artwork = draft.find(a =>\n        a.id === artworkId\n      );\n      artwork.seen = nextSeen;\n    });\n  }\n\n  return (\n    <>\n      <h1>Art Bucket List</h1>\n      <h2>My list of art to see:</h2>\n      <ItemList\n        artworks={list}\n        onToggle={handleToggle} />\n    </>\n  );\n}\n\nfunction ItemList({ artworks, onToggle }) {\n  return (\n    <ul>\n      {artworks.map(artwork => (\n        <li key={artwork.id}>\n          <label>\n            <input\n              type=\"checkbox\"\n              checked={artwork.seen}\n              onChange={e => {\n                onToggle(\n                  artwork.id,\n                  e.target.checked\n                );\n              }}\n            />\n            {artwork.title}\n          </label>\n        </li>\n      ))}\n    </ul>\n  );\n}\n```\n\n```json package.json\n{\n  \"dependencies\": {\n    \"immer\": \"1.7.3\",\n    \"react\": \"latest\",\n    \"react-dom\": \"latest\",\n    \"react-scripts\": \"latest\",\n    \"use-immer\": \"0.5.1\"\n  },\n  \"scripts\": {\n    \"start\": \"react-scripts start\",\n    \"build\": \"react-scripts build\",\n    \"test\": \"react-scripts test --env=jsdom\",\n    \"eject\": \"react-scripts eject\"\n  }\n}\n```\n\n</Sandpack>\n\n<LearnMore path=\"/learn/updating-arrays-in-state\">\n\n배열을 올바르게 업데이트하는 방법을 배우려면 **[state에서 배열을 업데이트하는 방법](/learn/updating-arrays-in-state)** 을 읽어보세요.\n\n</LearnMore>\n\n## What's next? {/*whats-next*/}\n\n이 장을 페이지별로 읽으려면 [사용자 이벤트를 처리하는 방법](/learn/responding-to-events)으로 이동하세요!\n\n이미 이러한 주제에 익숙하시다면 [State 다루기](/learn/managing-state)에 대해 읽어보시는 것도 좋습니다.\n"
  },
  {
    "path": "src/content/learn/build-a-react-app-from-scratch.md",
    "content": "---\ntitle: 처음부터 React 앱 만들기\n---\n\n<Intro>\n\n앱에 기존 프레임워크에서 잘 제공되지 않는 제약 조건이 있거나, 자체 프레임워크를 구축하는 것을 선호하거나, React 앱의 기본 사항만 배우려는 경우 React 앱을 처음부터 빌드할 수 있습니다.\n\n</Intro>\n\n<DeepDive>\n\n#### 프레임워크 사용을 고려해 보세요 {/*consider-using-a-framework*/}\n\nReact로 처음부터 시작하는 것은 React를 처음 사용하기에는 쉬운 방법이지만, 이 방식이 종종 자신만의 임시 프레임워크를 만드는 것과 다름없다는 점을 알아야 합니다. 요구사항이 발전함에 따라, 저희가 추천하는 프레임워크들이 이미 잘 개발하고 해결한 문제들을 직접 해결해야 할 수도 있습니다.\n\n예를 들어, 나중에 앱이 서버 사이드 렌더링(SSR), 정적 사이트 생성(SSG), 또는 React 서버 컴포넌트<sup>RSC</sup>를 지원해야 한다면, 이 모든 것을 직접 구현해야 할 것입니다. 마찬가지로, 미래의 React 기능 중 프레임워크 수준의 통합이 필요한 기능이 있다면, 사용하고 싶을 때 직접 구현해야 합니다.\n\n저희가 추천하는 프레임워크들은 더 나은 성능의 앱을 구축하는 데도 도움을 줍니다. 예를 들어, 네트워크 요청에서 워터폴<sup>Waterfall</sup> 현상을 줄이거나 제거하면 사용자 경험이 향상됩니다. 토이 프로젝트를 만들 때는 이것이 높은 우선순위가 아닐 수 있지만, 앱의 사용자가 늘어난다면 성능 개선을 원하게 될 것입니다.\n\n이 방법을 택하면 상황에 따라 라우팅, 데이터 가져오기, 기타 기능을 개발하는 방식이 달라지기 때문에 지원을 받기가 더 어려워집니다. 이 옵션은 이러한 문제를 스스로 해결하는 데 익숙하거나, 이러한 기능들이 전혀 필요 없을 것이라고 확신하는 경우에만 선택해야 합니다.\n\n추천 프레임워크 목록은 [새로운 React 앱 만들기](/learn/creating-a-react-app)를 확인해 보세요.\n\n</DeepDive>\n\n\n## Step 1: 빌드 툴 설치하기 {/*step-1-install-a-build-tool*/}\n\n첫 번째 단계는 `vite`, `parcel` 또는 `rsbuild` 같은 빌드 툴을 설치하는 것입니다. 빌드 툴은 소스 코드를 패키징하고 실행하는 기능을 제공하며, 로컬 개발을 위한 개발 서버와 앱을 프로덕션 서버에 배포하기 위한 빌드 명령어를 제공합니다.\n\n### Vite {/*vite*/}\n\n[Vite](https://vite.dev/)는 모던 웹 프로젝트에서 빠르고 간결한 개발 환경을 제공하는 것을 목표로 하는 빌드 도구입니다.\n\n<TerminalBlock>\nnpm create vite@latest my-app -- --template react-ts\n</TerminalBlock>\n\nVite는 명확한 특성을 보이며, 별도의 설정 없이도 합리적인 기본값을 제공합니다. Vite는 빠른 새로고침, JSX, Babel/SWC 등과 같은 일반적인 기능을 지원하는 풍부한 플러그인 생태계를 가지고 있습니다. 시작하려면 Vite의 [React 플러그인](https://ko.vite.dev/plugins/#vitejs-plugin-react) 또는 [React SWC 플러그인](https://ko.vite.dev/plugins/#vitejs-plugin-react-swc), 그리고 [React 서버 사이드 렌더링(SSR) 예시 프로젝트](https://ko.vite.dev/guide/ssr.html#example-projects)를 참고하세요.\n\nVite는 저희가 [추천하는 프레임워크](/learn/creating-a-react-app)인 [React Router](https://reactrouter.com/start/framework/installation)에서도 이미 빌드 툴로 사용하고 있습니다.\n\n### Parcel {/*parcel*/}\n\n[Parcel](https://parceljs.org/)은 뛰어난 기본 개발 경험과 함께, 프로젝트를 이제 막 시작하는 단계부터 대규모 프로덕션 애플리케이션까지 확장할 수 있는 아키텍처를 결합한 빌드 툴입니다.\n\n<TerminalBlock>\nnpm install --save-dev parcel\n</TerminalBlock>\n\nParcel은 별다른 설정 없이도 빠른 새로고침<sup>Fast Refresh</sup>, JSX, TypeScript, Flow 그리고 스타일링 기능을 지원합니다. 시작하려면 [Parcel에서 React 시작하기](https://parceljs.org/recipes/react/#getting-started)를 참고하세요.\n\n### Rsbuild {/*rsbuild*/}\n\n[Rsbuild](https://rsbuild.dev/)는 React 애플리케이션에 원활한 개발 경험을 제공하는 Rspack 기반의 빌드 툴입니다. 즉시 사용할 수 있도록 신중하게 조정된 기본 설정과 성능 최적화가 적용되어 있습니다.\n\n<TerminalBlock>\nnpx create-rsbuild --template react\n</TerminalBlock>\n\nRsbuild는 빠른 새로고침, JSX, TypeScript, 그리고 스타일링과 같은 React 기능을 기본적으로 지원합니다. 시작하려면 [Rsbuild의 React 가이드](https://rsbuild.dev/guide/framework/react)를 참고하세요.\n\n<Note>\n\n#### React Native를 위한 Metro {/*react-native*/}\n\nReact Native로 처음부터 시작한다면, React Native용 JavaScript 번들러인 [Metro](https://metrobundler.dev/)를 사용해야 합니다. Metro는 iOS 및 Android 같은 플랫폼을 위한 번들링을 지원하지만, 여기에 언급된 다른 툴들과 비교했을 때 많은 기능이 부족합니다. 따라서 프로젝트에 React Native 지원이 필요한 것이 아니라면, Vite, Parcel, 또는 Rsbuild로 시작하는 것을 추천합니다.\n\n</Note>\n\n## Step 2: 일반적인 애플리케이션 패턴 구축 {/*step-2-build-common-application-patterns*/}\n\n위에서 언급한 빌드 도구들은 클라이언트 전용의 단일 페이지 앱(SPA)으로 시작하지만, 라우팅, 데이터 가져오기, 스타일링과 같은 일반적인 기능에 대한 추가적인 솔루션은 포함하지 않습니다.\n\nReact 생태계에는 이러한 문제들을 해결하기 위한 많은 도구가 있습니다. 저희는 널리 사용되는 몇 가지 도구를 출발점으로 제시했지만, 본인에게 더 적합한 다른 도구들을 자유롭게 선택해도 좋습니다.\n\n### 라우팅 {/*routing*/}\n\n라우팅은 사용자가 특정 URL에 접속했을 때 어떤 콘텐츠나 페이지를 보여줄지 결정합니다. URL을 앱의 다양한 부분과 대응하기 위해 라우터를 설정해야 합니다. 또한 중첩 라우터, 경로 매개변수, 쿼리 매개변수도 처리해야 합니다. 라우터는 코드 내에서 구성하거나, 컴포넌트 폴더 및 파일 구조를 기반으로 정의할 수 있습니다.\n\n라우터는 최신 애플리케이션의 핵심 부분이며, 일반적으로 데이터 가져오기(더 빠른 로딩을 위한 전체 페이지 데이터 미리 가져오기 포함), (클라이언트 번들 크기 최소화를 위한) 코드 분할, (각 페이지가 어떻게 생성되는지 결정하는) 페이지 렌더링 방식과 통합됩니다.\n\n다음을 사용하는 것을 제안합니다.\n\n- [React Router](https://reactrouter.com/start/data/custom)\n- [Tanstack Router](https://tanstack.com/router/latest)\n\n\n### 데이터 가져오기 {/*data-fetching*/}\n\n서버나 다른 데이터 소스에서 데이터를 가져오는 것은 대부분의 애플리케이션에서 핵심적인 부분입니다. 이를 올바르게 수행하려면 로딩 상태, 오류 상태, 가져온 데이터 캐싱을 처리해야 하는데, 이는 복잡할 수 있습니다.\n\n목적에 맞게 제작된 데이터 가져오기 라이브러리는 데이터를 가져오고 캐싱하는 어려운 작업을 대신 해주므로, 개발자는 앱에 필요한 데이터가 무엇인지, 그리고 어떻게 표시할지에 집중할 수 있습니다. 이러한 라이브러리는 일반적으로 컴포넌트에서 직접 사용되지만, 더 빠른 미리 가져오기<sup>Pre-Fetching</sup>와 더 나은 성능을 위해 라우팅 로더에 통합될 수도 있고, 서버 렌더링에서도 사용될 수 있습니다.\n\n컴포넌트에서 직접 데이터를 가져오면 네트워크 요청 폭포<sup>Network Request Waterfall</sup> 현상으로 인해 로딩 시간이 느려질 수 있다는 사실을 알아두세요. 그래서 저희는 라우터 로더나 서버에서 최대한 데이터를 미리 가져오는 것을 권장합니다! 이렇게 하면 페이지를 표시할 때 페이지의 데이터를 한꺼번에 가져올 수 있습니다.\n\n대부분의 백엔드나 REST 스타일 API에서 데이터를 가져온다면 다음을 사용할 것을 제안합니다.\n\n- [React Query](https://tanstack.com/query/latest)\n- [SWR](https://swr.vercel.app/)\n- [RTK Query](https://redux-toolkit.js.org/rtk-query/overview)\n\nGraphQL API에서 데이터를 가져온다면 다음을 사용할 것을 제안합니다.\n\n- [Apollo](https://www.apollographql.com/docs/react)\n- [Relay](https://relay.dev/)\n\n\n### 코드 분할 {/*code-splitting*/}\n\n코드 분할은 앱을 더 작은 번들로 나누어 필요할 때만 로드할 수 있도록 하는 과정입니다. 앱의 코드 크기는 새로운 기능과 추가적인 의존성이 생길 때마다 증가합니다. 앱 전체의 코드는 사용되기 전에 모두 전송되어야 하므로 로딩 속도가 느려질 수 있습니다. 캐싱, 기능/의존성 감소, 일부 코드가 서버에서 실행되도록 코드를 이동하는 방법이 느린 로딩을 완화하는 데 도움이 될 수 있지만, 과도하게 사용하면 기능적으로 손해를 볼 수 있는 불완전한 해결책입니다.\n\n마찬가지로, 프레임워크를 사용하는 앱이 코드 분할을 처리하도록 의존한다면, 오히려 코드 분할을 전혀 하지 않았을 때보다 로딩이 느려지는 상황을 겪을 수도 있습니다. 예를 들어, 차트를 [지연 로딩](/reference/react/lazy)하면 차트를 렌더링하는 데 필요한 코드 전송이 지연되어 차트 코드가 앱의 나머지 부분과 분리됩니다. [Parcel은 `React.lazy`를 이용한 코드 분할](https://parceljs.org/recipes/react/#code-splitting)을 지원합니다. 하지만 차트가 초기 렌더링 된 후에 데이터를 로드한다면, 두 번 기다려야 합니다. 이것이 바로 폭포 현상입니다. 차트 데이터를 가져오고 렌더링 코드를 동시에 보내는 것보다 각 단계가 순서대로 완료되는 것을 더 기다려야 합니다.\n\n번들링 및 데이터 가져오기와 통합할 때 라우트별로 코드를 나누면, 앱의 초기 로드 시간과 가장 큰 시각적 콘텐츠가 렌더링 되는 시간([Largest Contentful Paint](https://web.dev/articles/lcp))을 줄일 수 있습니다.\n\n코드 분할 지침은 빌드 도구 문서를 참조하세요.\n- [Vite 빌드 최적화](https://vite.dev/guide/features.html#build-optimizations)\n- [Parcel 코드 분할](https://parceljs.org/features/code-splitting/)\n- [Rsbuild 코드 분할](https://rsbuild.dev/guide/optimization/code-splitting)\n\n### 애플리케이션 성능 향상 {/*improving-application-performance*/}\n\n선택한 빌드 도구는 단일 페이지 앱(SPA)만 지원하므로, 서버 사이드 렌더링(SSR), 정적 사이트 생성(SSG), 또는 React 서버 컴포넌트(RSC)와 같은 다른 [렌더링 패턴](https://www.patterns.dev/vanilla/rendering-patterns)을 직접 구현해야 합니다. 처음에는 이러한 기능이 필요하지 않더라도, 나중에는 SSR, SSG 또는 RSC가 유리한 라우트가 생길 수 있습니다.\n\n* **단일 페이지 앱 (SPA)** 은 단일 HTML 페이지를 로드하고 사용자가 앱과 상호작용을 할 때 페이지를 동적으로 업데이트합니다. SPA는 시작하기는 더 쉽지만, 초기 로드 시간이 느릴 수 있습니다. SPA는 대부분의 빌드 도구에서 기본 아키텍처입니다.\n\n* **스트리밍 서버 측 렌더링(SSR)** 은 서버에서 페이지를 렌더링하고 완전히 렌더링 된 페이지를 클라이언트로 보냅니다. SSR은 성능을 향상할 수 있지만, 단일 페이지 앱보다 설정하고 유지 관리하는 것이 더 복잡할 수 있습니다. 스트리밍 기능이 추가되면서 SSR은 설정 및 유지 관리가 매우 복잡해질 수 있습니다. [Vite의 SSR 가이드](https://vite.dev/guide/ssr.html)를 참조하세요.\n\n* **정적 사이트 생성(SSG)** 은 빌드 시점에 앱에 대한 정적 HTML 파일을 생성합니다. SSG는 성능을 향상할 수 있지만, 서버 측 렌더링보다 설정하고 유지 관리하는 것이 더 복잡할 수 있습니다. [Vite의 SSG 가이드](https://vite.dev/guide/ssr.html#pre-rendering-ssg)를 참조하세요.\n\n* **React 서버 컴포넌트(RSC)** 를 사용하면 빌드 타임, 서버 전용, 대화형 컴포넌트를 단일 React 트리에서 혼합할 수 있습니다. RSC는 성능을 향상할 수 있지만, 현재는 설정하고 유지 관리하는 데 깊은 전문 지식이 필요합니다. [Parcel의 RSC 예시](https://github.com/parcel-bundler/rsc-examples)를 참조하세요.\n\n프레임워크로 만들어진 앱이 라우트별로 렌더링 전략을 선택할 수 있도록, 렌더링 전략은 라우터와 통합되어야 합니다. 이렇게 하면 전체 앱을 다시 작성할 필요 없이 다양한 렌더링 전략을 사용할 수 있습니다. 예를 들어, 앱의 랜딩 페이지는 정적으로 생성되는 것(SSG)이 유리할 수 있지만, 콘텐츠 피드가 있는 페이지는 서버 측 렌더링이 가장 잘 작동할 수 있습니다.\n\n올바른 라우트에 올바른 렌더링 전략을 사용하면 콘텐츠의 첫 바이트가 로드되는 시간([Time to First Byte](https://web.dev/articles/ttfb)), 첫 번째 콘텐츠가 렌더링 되는 시간([First Contentful Paint](https://web.dev/articles/fcp)), 그리고 앱의 가장 큰 시각적 콘텐츠가 렌더링되는 시간([Largest Contentful Paint](https://web.dev/articles/lcp))을 줄일 수 있습니다.\n\n### 그리고 더... {/*and-more*/}\n\n이것들은 새로운 앱을 처음부터 구축할 때 고려해야 할 기능들의 몇 가지 예시에 불과합니다. 맞닥뜨리게 될 많은 제약은 각 문제가 서로 얽혀 있고 익숙하지 않은 문제 영역에 대한 깊은 전문 지식을 요구할 수 있기 때문에 해결하기 어려울 수 있습니다.\n\n이러한 문제들을 직접 해결하고 싶지 않다면, 이러한 기능을 바로 제공하는 [프레임워크로 시작](/learn/creating-a-react-app)할 수 있습니다.\n"
  },
  {
    "path": "src/content/learn/choosing-the-state-structure.md",
    "content": "---\ntitle: State 구조 선택하기\n---\n\n<Intro>\n\nState를 잘 구조화하면 수정과 디버깅이 즐거운 컴포넌트와 지속적인 버그의 원인이 되는 컴포넌트의 차이를 만들 수 있습니다. 다음은 state를 구조화할 때 고려해야 할 몇 가지 팁입니다.\n\n</Intro>\n\n<YouWillLearn>\n\n* 단일 vs 다중 state 변수를 사용하는 경우\n* State를 구성할 때 피해야 할 사항\n* 상태 구조의 일반적인 문제를 해결하는 방법\n\n</YouWillLearn>\n\n## State 구조화 원칙 {/*principles-for-structuring-state*/}\n\n상태를 갖는 구성요소를 작성할 때, 사용할 state 변수의 수와 데이터의 형태를 선택해야 합니다. 최적이 아닌 state 구조에서도 올바른 프로그램을 작성할 수 있지만, 더 나은 선택을 할 수 있는 몇 가지 원칙이 있습니다.\n\n1. **연관된 state 그룹화하기.** 두 개 이상의 state 변수를 항상 동시에 업데이트한다면, 단일 state 변수로 병합하는 것을 고려하세요.\n2. **State의 모순 피하기.** 여러 state 조각이 서로 모순되고 \"불일치\"할 수 있는 방식으로 state를 구성하는 것은 실수가 발생할 여지를 만듭니다. 이를 피하세요.\n3. **불필요한 state 피하기.** 렌더링 중에 컴포넌트의 props나 기존 state 변수에서 일부 정보를 계산할 수 있다면, 컴포넌트의 state에 해당 정보를 넣지 않아야 합니다.\n4. **State의 중복 피하기.** 여러 상태 변수 간 또는 중첩된 객체 내에서 동일한 데이터가 중복될 경우 동기화를 유지하기가 어렵습니다. 가능하다면 중복을 줄이세요.\n5. **깊게 중첩된 state 피하기.** 깊게 계층화된 state는 업데이트하기 쉽지 않습니다. 가능하면 state를 평탄한 방식으로 구성하는 것이 좋습니다.\n\n이러한 원칙 뒤에 있는 목표는 *오류 없이 상태를 쉽게 업데이트하는 것* 입니다. State에서 불필요하고 중복된 데이터를 제거하면 모든 데이터 조각이 동기화 상태를 유지하는 데 도움이 됩니다. 이는 데이터베이스 엔지니어가 [데이터베이스 구조를 \"정규화\"](https://learn.microsoft.com/ko-kr/office/troubleshoot/access/database-normalization-description)하여 버그 발생 가능성을 줄이는 것과 유사합니다. 알베르트 아인슈타인의 말을 빌리자면, **\"당신의 state를 가능한 한 단순하게 만들어야 한다, 더 단순하게 가 아니라.\"**\n\n이제 이 원칙들이 실제로 어떻게 적용되는지 살펴보겠습니다.\n\n\n## 연관된 state 그룹화하기 {/*group-related-state*/}\n\n단일 state 변수와 다중 state 변수 사이에서 무엇을 사용할지 불확실한 경우가 있습니다.\n\n이렇게 해야 할까요?\n\n```js\nconst [x, setX] = useState(0);\nconst [y, setY] = useState(0);\n```\n\n아니면 이렇게?\n\n```js\nconst [position, setPosition] = useState({ x: 0, y: 0 });\n```\n\n기술적으로 이 두 가지 접근 방식 모두 사용할 수 있습니다. 하지만 **두 개의 state 변수가 항상 함께 변경된다면, 단일 state 변수로 통합하는 것이 좋습니다.** 그러면 마우스 커서를 움직이면 빨간 점의 두 좌표가 모두 업데이트되는 이 예시처럼 항상 동기화를 유지하는 것을 잊지 않을 것입니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function MovingDot() {\n  const [position, setPosition] = useState({\n    x: 0,\n    y: 0\n  });\n  return (\n    <div\n      onPointerMove={e => {\n        setPosition({\n          x: e.clientX,\n          y: e.clientY\n        });\n      }}\n      style={{\n        position: 'relative',\n        width: '100vw',\n        height: '100vh',\n      }}>\n      <div style={{\n        position: 'absolute',\n        backgroundColor: 'red',\n        borderRadius: '50%',\n        transform: `translate(${position.x}px, ${position.y}px)`,\n        left: -10,\n        top: -10,\n        width: 20,\n        height: 20,\n      }} />\n    </div>\n  )\n}\n```\n\n```css\nbody { margin: 0; padding: 0; height: 250px; }\n```\n\n</Sandpack>\n\n데이터를 객체나 배열로 그룹화하는 또 다른 경우는 필요한 state의 조각 수를 모를 때입니다. 예를 들어, 사용자가 커스텀 필드를 추가할 수 있는 양식이 있는 경우에 유용합니다.\n\n<Pitfall>\n\nState 변수가 객체인 경우에는 다른 필드를 명시적으로 복사하지 않고 [하나의 필드만 업데이트할 수 없다](/learn/updating-objects-in-state)는 것을 기억하세요. 예를 들어 위의 예시에서 `setPosition({ x: 100 })`은 `y` 속성이 존재하지 않기 때문에 사용할 수 없습니다! 대신, `x`만 설정하려면 `setPosition({ ...position, x: 100 })`을 하거나 두 개의 state 변수로 나누고 `setX(100)`을 해야 합니다.\n\n</Pitfall>\n\n## State의 모순 피하기 {/*avoid-contradictions-in-state*/}\n\n다음은 `isSending`과 `isSent` state 변수가 있는 호텔 피드백 양식입니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function FeedbackForm() {\n  const [text, setText] = useState('');\n  const [isSending, setIsSending] = useState(false);\n  const [isSent, setIsSent] = useState(false);\n\n  async function handleSubmit(e) {\n    e.preventDefault();\n    setIsSending(true);\n    await sendMessage(text);\n    setIsSending(false);\n    setIsSent(true);\n  }\n\n  if (isSent) {\n    return <h1>Thanks for feedback!</h1>\n  }\n\n  return (\n    <form onSubmit={handleSubmit}>\n      <p>How was your stay at The Prancing Pony?</p>\n      <textarea\n        disabled={isSending}\n        value={text}\n        onChange={e => setText(e.target.value)}\n      />\n      <br />\n      <button\n        disabled={isSending}\n        type=\"submit\"\n      >\n        Send\n      </button>\n      {isSending && <p>Sending...</p>}\n    </form>\n  );\n}\n\n// Pretend to send a message.\nfunction sendMessage(text) {\n  return new Promise(resolve => {\n    setTimeout(resolve, 2000);\n  });\n}\n```\n\n</Sandpack>\n\n이 코드는 작동하긴 하지만, \"불가능한\" state를 허용합니다. 예를 들어 `setIsSent`와 `setIsSending`을 함께 호출하는 것을 잊어버린 경우, `isSending`과 `isSent`가 동시에 `true`인 상황에 처할 수 있습니다. 컴포넌트가 복잡할수록 무슨 일이 일어났는지 이해하기가 어렵습니다.\n\n**`isSending`과 `isSent`는 동시에 `true`가 되어서는 안되기 때문에, 이 두 변수를** `'typing'`(초깃값), `'sending'`, `'sent'` **세 가지 유효한 상태 중 하나를 가질 수 있는 `status` state 변수로 대체하는 것이 좋습니다.**\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function FeedbackForm() {\n  const [text, setText] = useState('');\n  const [status, setStatus] = useState('typing');\n\n  async function handleSubmit(e) {\n    e.preventDefault();\n    setStatus('sending');\n    await sendMessage(text);\n    setStatus('sent');\n  }\n\n  const isSending = status === 'sending';\n  const isSent = status === 'sent';\n\n  if (isSent) {\n    return <h1>Thanks for feedback!</h1>\n  }\n\n  return (\n    <form onSubmit={handleSubmit}>\n      <p>How was your stay at The Prancing Pony?</p>\n      <textarea\n        disabled={isSending}\n        value={text}\n        onChange={e => setText(e.target.value)}\n      />\n      <br />\n      <button\n        disabled={isSending}\n        type=\"submit\"\n      >\n        Send\n      </button>\n      {isSending && <p>Sending...</p>}\n    </form>\n  );\n}\n\n// Pretend to send a message.\nfunction sendMessage(text) {\n  return new Promise(resolve => {\n    setTimeout(resolve, 2000);\n  });\n}\n```\n\n</Sandpack>\n\n가독성을 위해 몇 가지 상수를 선언할 수도 있습니다.\n\n```js\nconst isSending = status === 'sending';\nconst isSent = status === 'sent';\n```\n\n이들은 state 변수가 아니기 때문에 서로 동기화되지 않을 우려는 없습니다.\n\n## 불필요한 state 피하기 {/*avoid-redundant-state*/}\n\n렌더링 중에 컴포넌트의 props나 기존 state 변수에서 일부 정보를 계산할 수 있다면, 컴포넌트의 state에 해당 정보를 넣지 **않아야 합니다.**\n\n예를 들어, 이 양식을 사용해 보세요. 작동은 하지만, 불필요한 state가 있지 않나요?\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function Form() {\n  const [firstName, setFirstName] = useState('');\n  const [lastName, setLastName] = useState('');\n  const [fullName, setFullName] = useState('');\n\n  function handleFirstNameChange(e) {\n    setFirstName(e.target.value);\n    setFullName(e.target.value + ' ' + lastName);\n  }\n\n  function handleLastNameChange(e) {\n    setLastName(e.target.value);\n    setFullName(firstName + ' ' + e.target.value);\n  }\n\n  return (\n    <>\n      <h2>Let’s check you in</h2>\n      <label>\n        First name:{' '}\n        <input\n          value={firstName}\n          onChange={handleFirstNameChange}\n        />\n      </label>\n      <label>\n        Last name:{' '}\n        <input\n          value={lastName}\n          onChange={handleLastNameChange}\n        />\n      </label>\n      <p>\n        Your ticket will be issued to: <b>{fullName}</b>\n      </p>\n    </>\n  );\n}\n```\n\n```css\nlabel { display: block; margin-bottom: 5px; }\n```\n\n</Sandpack>\n\n이 양식에는 `firstName`, `lastName`, `fullName`의 세 가지 state 변수가 있습니다. 그러나 `fullName`은 불필요합니다. **렌더링 중에 항상 `firstName`과 `lastName`에서 `fullName`을 계산할 수 있기 때문에 state에서 제거하세요.**\n\n이렇게 하면 됩니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function Form() {\n  const [firstName, setFirstName] = useState('');\n  const [lastName, setLastName] = useState('');\n\n  const fullName = firstName + ' ' + lastName;\n\n  function handleFirstNameChange(e) {\n    setFirstName(e.target.value);\n  }\n\n  function handleLastNameChange(e) {\n    setLastName(e.target.value);\n  }\n\n  return (\n    <>\n      <h2>Let’s check you in</h2>\n      <label>\n        First name:{' '}\n        <input\n          value={firstName}\n          onChange={handleFirstNameChange}\n        />\n      </label>\n      <label>\n        Last name:{' '}\n        <input\n          value={lastName}\n          onChange={handleLastNameChange}\n        />\n      </label>\n      <p>\n        Your ticket will be issued to: <b>{fullName}</b>\n      </p>\n    </>\n  );\n}\n```\n\n```css\nlabel { display: block; margin-bottom: 5px; }\n```\n\n</Sandpack>\n\n여기에서, `fullName`은 state 변수가 *아닙니다.* 대신 렌더링 중에 계산됩니다.\n\n```js\nconst fullName = firstName + ' ' + lastName;\n```\n\n따라서 변경 핸들러는 이를 업데이트하기 위해 특별한 작업을 수행할 필요가 없습니다. `setFirstName` 또는 `setLastName`을 호출하면, 다시 렌더링하는 것을 유발하여, 다음 `fullName`이 새 데이터로 계산됩니다.\n\n<DeepDive>\n\n#### Props를 state에 미러링하지 마세요. {/*don-t-mirror-props-in-state*/}\n\n다음 코드는 불필요한 state의 일반적인 예입니다.\n\n```js\nfunction Message({ messageColor }) {\n  const [color, setColor] = useState(messageColor);\n```\n\n여기서 `color` state 변수는 `messageColor` prop로 초기화됩니다. 문제는 **부모 컴포넌트가 나중에 다른 값의 `messageColor`를 전달한다면 (예를 들어, `'blue'` 대신 `'red'`), `color` *state 변수* 가 업데이트되지 않습니다!** State는 첫 번째 렌더링 중에만 초기화됩니다.\n\n그 때문에 state 변수의 일부 prop를 \"미러링\"하면 혼란이 발생할 수 있습니다. 대신 코드에 `messageColor` prop를 직접 사용하세요. 더 짧은 이름을 지정하려면 상수를 사용하세요.\n\n```js\nfunction Message({ messageColor }) {\n  const color = messageColor;\n```\n\n이렇게 하면 부모 컴포넌트에서 전달된 prop와 동기화를 잃지 않습니다.\n\nProps를 상태로 \"미러링\"하는 것은 특정 prop에 대한 모든 업데이트를 무시하기를 *원할* 때에만 의미가 있습니다. 관례에 따라 prop의 이름을 `initial` 또는 `default`로 시작하여 새로운 값이 무시됨을 명확히 하세요.\n\n```js\nfunction Message({ initialColor }) {\n  // The `color` state variable holds the *first* value of `initialColor`.\n  // Further changes to the `initialColor` prop are ignored.\n  const [color, setColor] = useState(initialColor);\n```\n\n</DeepDive>\n\n## State의 중복 피하기 {/*avoid-duplication-in-state*/}\n\n이 메뉴 목록 컴포넌트로 여러 가지 중 하나의 여행 간식을 선택할 수 있습니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nconst initialItems = [\n  { title: 'pretzels', id: 0 },\n  { title: 'crispy seaweed', id: 1 },\n  { title: 'granola bar', id: 2 },\n];\n\nexport default function Menu() {\n  const [items, setItems] = useState(initialItems);\n  const [selectedItem, setSelectedItem] = useState(\n    items[0]\n  );\n\n  return (\n    <>\n      <h2>What's your travel snack?</h2>\n      <ul>\n        {items.map(item => (\n          <li key={item.id}>\n            {item.title}\n            {' '}\n            <button onClick={() => {\n              setSelectedItem(item);\n            }}>Choose</button>\n          </li>\n        ))}\n      </ul>\n      <p>You picked {selectedItem.title}.</p>\n    </>\n  );\n}\n```\n\n```css\nbutton { margin-top: 10px; }\n```\n\n</Sandpack>\n\n현재는 선택된 항목을 `selectedItem` state 변수에 객체로 저장합니다. 그러나 이는 좋지 않습니다. **`selectedItem`의 내용이 `items` 목록 내의 항목 중 하나와 동일한 객체입니다.** 이는 항목 자체에 대한 정보가 두 곳에서 중복되는 것입니다.\n\n이것은 왜 문제일까요? 각 항목을 편집할 수 있도록 만들어 보겠습니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nconst initialItems = [\n  { title: 'pretzels', id: 0 },\n  { title: 'crispy seaweed', id: 1 },\n  { title: 'granola bar', id: 2 },\n];\n\nexport default function Menu() {\n  const [items, setItems] = useState(initialItems);\n  const [selectedItem, setSelectedItem] = useState(\n    items[0]\n  );\n\n  function handleItemChange(id, e) {\n    setItems(items.map(item => {\n      if (item.id === id) {\n        return {\n          ...item,\n          title: e.target.value,\n        };\n      } else {\n        return item;\n      }\n    }));\n  }\n\n  return (\n    <>\n      <h2>What's your travel snack?</h2>\n      <ul>\n        {items.map((item, index) => (\n          <li key={item.id}>\n            <input\n              value={item.title}\n              onChange={e => {\n                handleItemChange(item.id, e)\n              }}\n            />\n            {' '}\n            <button onClick={() => {\n              setSelectedItem(item);\n            }}>Choose</button>\n          </li>\n        ))}\n      </ul>\n      <p>You picked {selectedItem.title}.</p>\n    </>\n  );\n}\n```\n\n```css\nbutton { margin-top: 10px; }\n```\n\n</Sandpack>\n\n먼저 항목에서 \"Choose\"를 클릭한 *후* 이를 편집할 경우, **입력이 업데이트되지만, 하단의 라벨에는 편집 내용이 반영되지 않습니다.** 이는 state가 중복되었으며 `selectedItem`을 업데이트하는 것을 잊어버렸기 때문입니다.\n\n`selectedItem`도 업데이트할 수 있지만 더 쉬운 수정 방법은 중복을 제거하는 것입니다. 이 예에서는 `selectedItem` 객체(`items` 내부의 객체와 중복을 생성하는) 대신 `selectedId`를 state로 유지하고, *그다음* `items` 배열에서 해당 ID의 항목을 검색하여 `selectedItem`을 가져옵니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nconst initialItems = [\n  { title: 'pretzels', id: 0 },\n  { title: 'crispy seaweed', id: 1 },\n  { title: 'granola bar', id: 2 },\n];\n\nexport default function Menu() {\n  const [items, setItems] = useState(initialItems);\n  const [selectedId, setSelectedId] = useState(0);\n\n  const selectedItem = items.find(item =>\n    item.id === selectedId\n  );\n\n  function handleItemChange(id, e) {\n    setItems(items.map(item => {\n      if (item.id === id) {\n        return {\n          ...item,\n          title: e.target.value,\n        };\n      } else {\n        return item;\n      }\n    }));\n  }\n\n  return (\n    <>\n      <h2>What's your travel snack?</h2>\n      <ul>\n        {items.map((item, index) => (\n          <li key={item.id}>\n            <input\n              value={item.title}\n              onChange={e => {\n                handleItemChange(item.id, e)\n              }}\n            />\n            {' '}\n            <button onClick={() => {\n              setSelectedId(item.id);\n            }}>Choose</button>\n          </li>\n        ))}\n      </ul>\n      <p>You picked {selectedItem.title}.</p>\n    </>\n  );\n}\n```\n\n```css\nbutton { margin-top: 10px; }\n```\n\n</Sandpack>\n\nState는 다음과 같이 중복되었습니다.\n\n* `items = [{ id: 0, title: 'pretzels'}, ...]`\n* `selectedItem = {id: 0, title: 'pretzels'}`\n\n하지만 변경 후에는 다음과 같습니다.\n\n* `items = [{ id: 0, title: 'pretzels'}, ...]`\n* `selectedId = 0`\n\n중복은 사라지고 필수적인 state만 유지됩니다!\n\n이제 *선택한* 항목을 편집하면 아래 메시지가 즉시 업데이트됩니다. 이는 `setItems`가 다시 렌더링하도록 유발하고, `items.find(...)`가 업데이트된 제목의 항목을 찾을 것이기 때문입니다. *선택한 ID*만 필수이므로 *선택한 항목*을 state로 유지할 필요가 없습니다. 나머지는 렌더링하는 동안 계산할 수 있습니다.\n\n## 깊게 중첩된 state 피하기 {/*avoid-deeply-nested-state*/}\n\n행성, 대륙, 국가로 구성된 여행 계획을 상상해 보세요. 이 예시처럼 중첩된 객체와 배열을 사용하여 여행 계획의 state를 구성하고 싶을 수 있습니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\nimport { initialTravelPlan } from './places.js';\n\nfunction PlaceTree({ place }) {\n  const childPlaces = place.childPlaces;\n  return (\n    <li>\n      {place.title}\n      {childPlaces.length > 0 && (\n        <ol>\n          {childPlaces.map(place => (\n            <PlaceTree key={place.id} place={place} />\n          ))}\n        </ol>\n      )}\n    </li>\n  );\n}\n\nexport default function TravelPlan() {\n  const [plan, setPlan] = useState(initialTravelPlan);\n  const planets = plan.childPlaces;\n  return (\n    <>\n      <h2>Places to visit</h2>\n      <ol>\n        {planets.map(place => (\n          <PlaceTree key={place.id} place={place} />\n        ))}\n      </ol>\n    </>\n  );\n}\n```\n\n```js src/places.js active\nexport const initialTravelPlan = {\n  id: 0,\n  title: '(Root)',\n  childPlaces: [{\n    id: 1,\n    title: 'Earth',\n    childPlaces: [{\n      id: 2,\n      title: 'Africa',\n      childPlaces: [{\n        id: 3,\n        title: 'Botswana',\n        childPlaces: []\n      }, {\n        id: 4,\n        title: 'Egypt',\n        childPlaces: []\n      }, {\n        id: 5,\n        title: 'Kenya',\n        childPlaces: []\n      }, {\n        id: 6,\n        title: 'Madagascar',\n        childPlaces: []\n      }, {\n        id: 7,\n        title: 'Morocco',\n        childPlaces: []\n      }, {\n        id: 8,\n        title: 'Nigeria',\n        childPlaces: []\n      }, {\n        id: 9,\n        title: 'South Africa',\n        childPlaces: []\n      }]\n    }, {\n      id: 10,\n      title: 'Americas',\n      childPlaces: [{\n        id: 11,\n        title: 'Argentina',\n        childPlaces: []\n      }, {\n        id: 12,\n        title: 'Brazil',\n        childPlaces: []\n      }, {\n        id: 13,\n        title: 'Barbados',\n        childPlaces: []\n      }, {\n        id: 14,\n        title: 'Canada',\n        childPlaces: []\n      }, {\n        id: 15,\n        title: 'Jamaica',\n        childPlaces: []\n      }, {\n        id: 16,\n        title: 'Mexico',\n        childPlaces: []\n      }, {\n        id: 17,\n        title: 'Trinidad and Tobago',\n        childPlaces: []\n      }, {\n        id: 18,\n        title: 'Venezuela',\n        childPlaces: []\n      }]\n    }, {\n      id: 19,\n      title: 'Asia',\n      childPlaces: [{\n        id: 20,\n        title: 'China',\n        childPlaces: []\n      }, {\n        id: 21,\n        title: 'India',\n        childPlaces: []\n      }, {\n        id: 22,\n        title: 'Singapore',\n        childPlaces: []\n      }, {\n        id: 23,\n        title: 'South Korea',\n        childPlaces: []\n      }, {\n        id: 24,\n        title: 'Thailand',\n        childPlaces: []\n      }, {\n        id: 25,\n        title: 'Vietnam',\n        childPlaces: []\n      }]\n    }, {\n      id: 26,\n      title: 'Europe',\n      childPlaces: [{\n        id: 27,\n        title: 'Croatia',\n        childPlaces: [],\n      }, {\n        id: 28,\n        title: 'France',\n        childPlaces: [],\n      }, {\n        id: 29,\n        title: 'Germany',\n        childPlaces: [],\n      }, {\n        id: 30,\n        title: 'Italy',\n        childPlaces: [],\n      }, {\n        id: 31,\n        title: 'Portugal',\n        childPlaces: [],\n      }, {\n        id: 32,\n        title: 'Spain',\n        childPlaces: [],\n      }, {\n        id: 33,\n        title: 'Turkey',\n        childPlaces: [],\n      }]\n    }, {\n      id: 34,\n      title: 'Oceania',\n      childPlaces: [{\n        id: 35,\n        title: 'Australia',\n        childPlaces: [],\n      }, {\n        id: 36,\n        title: 'Bora Bora (French Polynesia)',\n        childPlaces: [],\n      }, {\n        id: 37,\n        title: 'Easter Island (Chile)',\n        childPlaces: [],\n      }, {\n        id: 38,\n        title: 'Fiji',\n        childPlaces: [],\n      }, {\n        id: 39,\n        title: 'Hawaii (the USA)',\n        childPlaces: [],\n      }, {\n        id: 40,\n        title: 'New Zealand',\n        childPlaces: [],\n      }, {\n        id: 41,\n        title: 'Vanuatu',\n        childPlaces: [],\n      }]\n    }]\n  }, {\n    id: 42,\n    title: 'Moon',\n    childPlaces: [{\n      id: 43,\n      title: 'Rheita',\n      childPlaces: []\n    }, {\n      id: 44,\n      title: 'Piccolomini',\n      childPlaces: []\n    }, {\n      id: 45,\n      title: 'Tycho',\n      childPlaces: []\n    }]\n  }, {\n    id: 46,\n    title: 'Mars',\n    childPlaces: [{\n      id: 47,\n      title: 'Corn Town',\n      childPlaces: []\n    }, {\n      id: 48,\n      title: 'Green Hill',\n      childPlaces: []\n    }]\n  }]\n};\n```\n\n</Sandpack>\n\n이제 방문한 장소를 삭제하는 버튼을 추가하고 싶습니다. 어떻게 해야 할까요? [중첩된 state를 업데이트하는 것](/learn/updating-objects-in-state#updating-a-nested-object)은 변경된 부분부터 모든 객체의 복사본을 만드는 것을 의미합니다. 깊게 중첩된 장소를 삭제하는 것은 전체 부모 장소 체인을 복사하는 것을 의미합니다. 이러한 코드는 매우 장황할 수 있습니다.\n\n**만일 state가 쉽게 업데이트하기에 너무 중첩되어 있다면, \"평탄\"하게 만드는 것을 고려하세요.** 여기 데이터를 다시 구조화하는 한 가지 방법이 있습니다. 각 `place`가 *자식 장소*의 배열을 가지는 트리 구조 대신, 각 장소가 *자식 장소 ID*의 배열을 가지도록 할 수 있습니다. 그런 다음 각 장소 ID와 해당 장소에 대한 매핑을 저장하세요.\n\n이 데이터 재구성은 데이터베이스 테이블을 떠올리게 할 수 있습니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\nimport { initialTravelPlan } from './places.js';\n\nfunction PlaceTree({ id, placesById }) {\n  const place = placesById[id];\n  const childIds = place.childIds;\n  return (\n    <li>\n      {place.title}\n      {childIds.length > 0 && (\n        <ol>\n          {childIds.map(childId => (\n            <PlaceTree\n              key={childId}\n              id={childId}\n              placesById={placesById}\n            />\n          ))}\n        </ol>\n      )}\n    </li>\n  );\n}\n\nexport default function TravelPlan() {\n  const [plan, setPlan] = useState(initialTravelPlan);\n  const root = plan[0];\n  const planetIds = root.childIds;\n  return (\n    <>\n      <h2>Places to visit</h2>\n      <ol>\n        {planetIds.map(id => (\n          <PlaceTree\n            key={id}\n            id={id}\n            placesById={plan}\n          />\n        ))}\n      </ol>\n    </>\n  );\n}\n```\n\n```js src/places.js active\nexport const initialTravelPlan = {\n  0: {\n    id: 0,\n    title: '(Root)',\n    childIds: [1, 42, 46],\n  },\n  1: {\n    id: 1,\n    title: 'Earth',\n    childIds: [2, 10, 19, 26, 34]\n  },\n  2: {\n    id: 2,\n    title: 'Africa',\n    childIds: [3, 4, 5, 6 , 7, 8, 9]\n  },\n  3: {\n    id: 3,\n    title: 'Botswana',\n    childIds: []\n  },\n  4: {\n    id: 4,\n    title: 'Egypt',\n    childIds: []\n  },\n  5: {\n    id: 5,\n    title: 'Kenya',\n    childIds: []\n  },\n  6: {\n    id: 6,\n    title: 'Madagascar',\n    childIds: []\n  },\n  7: {\n    id: 7,\n    title: 'Morocco',\n    childIds: []\n  },\n  8: {\n    id: 8,\n    title: 'Nigeria',\n    childIds: []\n  },\n  9: {\n    id: 9,\n    title: 'South Africa',\n    childIds: []\n  },\n  10: {\n    id: 10,\n    title: 'Americas',\n    childIds: [11, 12, 13, 14, 15, 16, 17, 18],\n  },\n  11: {\n    id: 11,\n    title: 'Argentina',\n    childIds: []\n  },\n  12: {\n    id: 12,\n    title: 'Brazil',\n    childIds: []\n  },\n  13: {\n    id: 13,\n    title: 'Barbados',\n    childIds: []\n  },\n  14: {\n    id: 14,\n    title: 'Canada',\n    childIds: []\n  },\n  15: {\n    id: 15,\n    title: 'Jamaica',\n    childIds: []\n  },\n  16: {\n    id: 16,\n    title: 'Mexico',\n    childIds: []\n  },\n  17: {\n    id: 17,\n    title: 'Trinidad and Tobago',\n    childIds: []\n  },\n  18: {\n    id: 18,\n    title: 'Venezuela',\n    childIds: []\n  },\n  19: {\n    id: 19,\n    title: 'Asia',\n    childIds: [20, 21, 22, 23, 24, 25],\n  },\n  20: {\n    id: 20,\n    title: 'China',\n    childIds: []\n  },\n  21: {\n    id: 21,\n    title: 'India',\n    childIds: []\n  },\n  22: {\n    id: 22,\n    title: 'Singapore',\n    childIds: []\n  },\n  23: {\n    id: 23,\n    title: 'South Korea',\n    childIds: []\n  },\n  24: {\n    id: 24,\n    title: 'Thailand',\n    childIds: []\n  },\n  25: {\n    id: 25,\n    title: 'Vietnam',\n    childIds: []\n  },\n  26: {\n    id: 26,\n    title: 'Europe',\n    childIds: [27, 28, 29, 30, 31, 32, 33],\n  },\n  27: {\n    id: 27,\n    title: 'Croatia',\n    childIds: []\n  },\n  28: {\n    id: 28,\n    title: 'France',\n    childIds: []\n  },\n  29: {\n    id: 29,\n    title: 'Germany',\n    childIds: []\n  },\n  30: {\n    id: 30,\n    title: 'Italy',\n    childIds: []\n  },\n  31: {\n    id: 31,\n    title: 'Portugal',\n    childIds: []\n  },\n  32: {\n    id: 32,\n    title: 'Spain',\n    childIds: []\n  },\n  33: {\n    id: 33,\n    title: 'Turkey',\n    childIds: []\n  },\n  34: {\n    id: 34,\n    title: 'Oceania',\n    childIds: [35, 36, 37, 38, 39, 40, 41],\n  },\n  35: {\n    id: 35,\n    title: 'Australia',\n    childIds: []\n  },\n  36: {\n    id: 36,\n    title: 'Bora Bora (French Polynesia)',\n    childIds: []\n  },\n  37: {\n    id: 37,\n    title: 'Easter Island (Chile)',\n    childIds: []\n  },\n  38: {\n    id: 38,\n    title: 'Fiji',\n    childIds: []\n  },\n  39: {\n    id: 39,\n    title: 'Hawaii (the USA)',\n    childIds: []\n  },\n  40: {\n    id: 40,\n    title: 'New Zealand',\n    childIds: []\n  },\n  41: {\n    id: 41,\n    title: 'Vanuatu',\n    childIds: []\n  },\n  42: {\n    id: 42,\n    title: 'Moon',\n    childIds: [43, 44, 45]\n  },\n  43: {\n    id: 43,\n    title: 'Rheita',\n    childIds: []\n  },\n  44: {\n    id: 44,\n    title: 'Piccolomini',\n    childIds: []\n  },\n  45: {\n    id: 45,\n    title: 'Tycho',\n    childIds: []\n  },\n  46: {\n    id: 46,\n    title: 'Mars',\n    childIds: [47, 48]\n  },\n  47: {\n    id: 47,\n    title: 'Corn Town',\n    childIds: []\n  },\n  48: {\n    id: 48,\n    title: 'Green Hill',\n    childIds: []\n  }\n};\n```\n\n</Sandpack>\n\n**이제 state가 \"평탄\"(\"정규화\"라고도 함)하므로 중첩된 항목을 업데이트하는 것이 더 쉬워졌습니다.**\n\n이제 장소를 제거하기 위해, state의 두 단계만 업데이트하면 됩니다.\n\n- 업데이트된 버전의 *부모* 장소는 `childIds` 배열에서 제거된 ID를 제외해야 합니다.\n- 업데이트된 버전의 루트 \"테이블\" 객체는 부모 장소의 업데이트된 버전을 포함해야 합니다.\n\n다음은 이를 수행하는 방법의 예입니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\nimport { initialTravelPlan } from './places.js';\n\nexport default function TravelPlan() {\n  const [plan, setPlan] = useState(initialTravelPlan);\n\n  function handleComplete(parentId, childId) {\n    const parent = plan[parentId];\n    // Create a new version of the parent place\n    // that doesn't include this child ID.\n    const nextParent = {\n      ...parent,\n      childIds: parent.childIds\n        .filter(id => id !== childId)\n    };\n    // Update the root state object...\n    setPlan({\n      ...plan,\n      // ...so that it has the updated parent.\n      [parentId]: nextParent\n    });\n  }\n\n  const root = plan[0];\n  const planetIds = root.childIds;\n  return (\n    <>\n      <h2>Places to visit</h2>\n      <ol>\n        {planetIds.map(id => (\n          <PlaceTree\n            key={id}\n            id={id}\n            parentId={0}\n            placesById={plan}\n            onComplete={handleComplete}\n          />\n        ))}\n      </ol>\n    </>\n  );\n}\n\nfunction PlaceTree({ id, parentId, placesById, onComplete }) {\n  const place = placesById[id];\n  const childIds = place.childIds;\n  return (\n    <li>\n      {place.title}\n      <button onClick={() => {\n        onComplete(parentId, id);\n      }}>\n        Complete\n      </button>\n      {childIds.length > 0 &&\n        <ol>\n          {childIds.map(childId => (\n            <PlaceTree\n              key={childId}\n              id={childId}\n              parentId={id}\n              placesById={placesById}\n              onComplete={onComplete}\n            />\n          ))}\n        </ol>\n      }\n    </li>\n  );\n}\n```\n\n```js src/places.js\nexport const initialTravelPlan = {\n  0: {\n    id: 0,\n    title: '(Root)',\n    childIds: [1, 42, 46],\n  },\n  1: {\n    id: 1,\n    title: 'Earth',\n    childIds: [2, 10, 19, 26, 34]\n  },\n  2: {\n    id: 2,\n    title: 'Africa',\n    childIds: [3, 4, 5, 6 , 7, 8, 9]\n  },\n  3: {\n    id: 3,\n    title: 'Botswana',\n    childIds: []\n  },\n  4: {\n    id: 4,\n    title: 'Egypt',\n    childIds: []\n  },\n  5: {\n    id: 5,\n    title: 'Kenya',\n    childIds: []\n  },\n  6: {\n    id: 6,\n    title: 'Madagascar',\n    childIds: []\n  },\n  7: {\n    id: 7,\n    title: 'Morocco',\n    childIds: []\n  },\n  8: {\n    id: 8,\n    title: 'Nigeria',\n    childIds: []\n  },\n  9: {\n    id: 9,\n    title: 'South Africa',\n    childIds: []\n  },\n  10: {\n    id: 10,\n    title: 'Americas',\n    childIds: [11, 12, 13, 14, 15, 16, 17, 18],\n  },\n  11: {\n    id: 11,\n    title: 'Argentina',\n    childIds: []\n  },\n  12: {\n    id: 12,\n    title: 'Brazil',\n    childIds: []\n  },\n  13: {\n    id: 13,\n    title: 'Barbados',\n    childIds: []\n  },\n  14: {\n    id: 14,\n    title: 'Canada',\n    childIds: []\n  },\n  15: {\n    id: 15,\n    title: 'Jamaica',\n    childIds: []\n  },\n  16: {\n    id: 16,\n    title: 'Mexico',\n    childIds: []\n  },\n  17: {\n    id: 17,\n    title: 'Trinidad and Tobago',\n    childIds: []\n  },\n  18: {\n    id: 18,\n    title: 'Venezuela',\n    childIds: []\n  },\n  19: {\n    id: 19,\n    title: 'Asia',\n    childIds: [20, 21, 22, 23, 24, 25],\n  },\n  20: {\n    id: 20,\n    title: 'China',\n    childIds: []\n  },\n  21: {\n    id: 21,\n    title: 'India',\n    childIds: []\n  },\n  22: {\n    id: 22,\n    title: 'Singapore',\n    childIds: []\n  },\n  23: {\n    id: 23,\n    title: 'South Korea',\n    childIds: []\n  },\n  24: {\n    id: 24,\n    title: 'Thailand',\n    childIds: []\n  },\n  25: {\n    id: 25,\n    title: 'Vietnam',\n    childIds: []\n  },\n  26: {\n    id: 26,\n    title: 'Europe',\n    childIds: [27, 28, 29, 30, 31, 32, 33],\n  },\n  27: {\n    id: 27,\n    title: 'Croatia',\n    childIds: []\n  },\n  28: {\n    id: 28,\n    title: 'France',\n    childIds: []\n  },\n  29: {\n    id: 29,\n    title: 'Germany',\n    childIds: []\n  },\n  30: {\n    id: 30,\n    title: 'Italy',\n    childIds: []\n  },\n  31: {\n    id: 31,\n    title: 'Portugal',\n    childIds: []\n  },\n  32: {\n    id: 32,\n    title: 'Spain',\n    childIds: []\n  },\n  33: {\n    id: 33,\n    title: 'Turkey',\n    childIds: []\n  },\n  34: {\n    id: 34,\n    title: 'Oceania',\n    childIds: [35, 36, 37, 38, 39, 40, 41],\n  },\n  35: {\n    id: 35,\n    title: 'Australia',\n    childIds: []\n  },\n  36: {\n    id: 36,\n    title: 'Bora Bora (French Polynesia)',\n    childIds: []\n  },\n  37: {\n    id: 37,\n    title: 'Easter Island (Chile)',\n    childIds: []\n  },\n  38: {\n    id: 38,\n    title: 'Fiji',\n    childIds: []\n  },\n  39: {\n    id: 39,\n    title: 'Hawaii (the USA)',\n    childIds: []\n  },\n  40: {\n    id: 40,\n    title: 'New Zealand',\n    childIds: []\n  },\n  41: {\n    id: 41,\n    title: 'Vanuatu',\n    childIds: []\n  },\n  42: {\n    id: 42,\n    title: 'Moon',\n    childIds: [43, 44, 45]\n  },\n  43: {\n    id: 43,\n    title: 'Rheita',\n    childIds: []\n  },\n  44: {\n    id: 44,\n    title: 'Piccolomini',\n    childIds: []\n  },\n  45: {\n    id: 45,\n    title: 'Tycho',\n    childIds: []\n  },\n  46: {\n    id: 46,\n    title: 'Mars',\n    childIds: [47, 48]\n  },\n  47: {\n    id: 47,\n    title: 'Corn Town',\n    childIds: []\n  },\n  48: {\n    id: 48,\n    title: 'Green Hill',\n    childIds: []\n  }\n};\n```\n\n```css\nbutton { margin: 10px; }\n```\n\n</Sandpack>\n\nState를 원하는 만큼 중첩할 수 있지만, \"평탄\"하게 만드는 것은 많은 문제를 해결할 수 있습니다. State를 업데이트하기 쉽게 만들고 중첩된 객체의 다른 부분에 중복이 없도록 도와줍니다.\n\n<DeepDive>\n\n#### 메모리 사용량 개선하기 {/*improving-memory-usage*/}\n\n이상적으로 메모리 사용량을 개선하기 위해서는 삭제된 항목(그리고 그들의 자식들!)을 \"테이블\" 객체에서 제거해야 합니다. 이 버전은 그렇게 합니다. 또한 업데이트 로직을 더 간결하게 만들기 위해 [Immer를 사용](/learn/updating-objects-in-state#write-concise-update-logic-with-immer)합니다.\n\n<Sandpack>\n\n```js\nimport { useImmer } from 'use-immer';\nimport { initialTravelPlan } from './places.js';\n\nexport default function TravelPlan() {\n  const [plan, updatePlan] = useImmer(initialTravelPlan);\n\n  function handleComplete(parentId, childId) {\n    updatePlan(draft => {\n      // Remove from the parent place's child IDs.\n      const parent = draft[parentId];\n      parent.childIds = parent.childIds\n        .filter(id => id !== childId);\n\n      // Forget this place and all its subtree.\n      deleteAllChildren(childId);\n      function deleteAllChildren(id) {\n        const place = draft[id];\n        place.childIds.forEach(deleteAllChildren);\n        delete draft[id];\n      }\n    });\n  }\n\n  const root = plan[0];\n  const planetIds = root.childIds;\n  return (\n    <>\n      <h2>Places to visit</h2>\n      <ol>\n        {planetIds.map(id => (\n          <PlaceTree\n            key={id}\n            id={id}\n            parentId={0}\n            placesById={plan}\n            onComplete={handleComplete}\n          />\n        ))}\n      </ol>\n    </>\n  );\n}\n\nfunction PlaceTree({ id, parentId, placesById, onComplete }) {\n  const place = placesById[id];\n  const childIds = place.childIds;\n  return (\n    <li>\n      {place.title}\n      <button onClick={() => {\n        onComplete(parentId, id);\n      }}>\n        Complete\n      </button>\n      {childIds.length > 0 &&\n        <ol>\n          {childIds.map(childId => (\n            <PlaceTree\n              key={childId}\n              id={childId}\n              parentId={id}\n              placesById={placesById}\n              onComplete={onComplete}\n            />\n          ))}\n        </ol>\n      }\n    </li>\n  );\n}\n```\n\n```js src/places.js\nexport const initialTravelPlan = {\n  0: {\n    id: 0,\n    title: '(Root)',\n    childIds: [1, 42, 46],\n  },\n  1: {\n    id: 1,\n    title: 'Earth',\n    childIds: [2, 10, 19, 26, 34]\n  },\n  2: {\n    id: 2,\n    title: 'Africa',\n    childIds: [3, 4, 5, 6 , 7, 8, 9]\n  },\n  3: {\n    id: 3,\n    title: 'Botswana',\n    childIds: []\n  },\n  4: {\n    id: 4,\n    title: 'Egypt',\n    childIds: []\n  },\n  5: {\n    id: 5,\n    title: 'Kenya',\n    childIds: []\n  },\n  6: {\n    id: 6,\n    title: 'Madagascar',\n    childIds: []\n  },\n  7: {\n    id: 7,\n    title: 'Morocco',\n    childIds: []\n  },\n  8: {\n    id: 8,\n    title: 'Nigeria',\n    childIds: []\n  },\n  9: {\n    id: 9,\n    title: 'South Africa',\n    childIds: []\n  },\n  10: {\n    id: 10,\n    title: 'Americas',\n    childIds: [11, 12, 13, 14, 15, 16, 17, 18],\n  },\n  11: {\n    id: 11,\n    title: 'Argentina',\n    childIds: []\n  },\n  12: {\n    id: 12,\n    title: 'Brazil',\n    childIds: []\n  },\n  13: {\n    id: 13,\n    title: 'Barbados',\n    childIds: []\n  },\n  14: {\n    id: 14,\n    title: 'Canada',\n    childIds: []\n  },\n  15: {\n    id: 15,\n    title: 'Jamaica',\n    childIds: []\n  },\n  16: {\n    id: 16,\n    title: 'Mexico',\n    childIds: []\n  },\n  17: {\n    id: 17,\n    title: 'Trinidad and Tobago',\n    childIds: []\n  },\n  18: {\n    id: 18,\n    title: 'Venezuela',\n    childIds: []\n  },\n  19: {\n    id: 19,\n    title: 'Asia',\n    childIds: [20, 21, 22, 23, 24, 25,],\n  },\n  20: {\n    id: 20,\n    title: 'China',\n    childIds: []\n  },\n  21: {\n    id: 21,\n    title: 'India',\n    childIds: []\n  },\n  22: {\n    id: 22,\n    title: 'Singapore',\n    childIds: []\n  },\n  23: {\n    id: 23,\n    title: 'South Korea',\n    childIds: []\n  },\n  24: {\n    id: 24,\n    title: 'Thailand',\n    childIds: []\n  },\n  25: {\n    id: 25,\n    title: 'Vietnam',\n    childIds: []\n  },\n  26: {\n    id: 26,\n    title: 'Europe',\n    childIds: [27, 28, 29, 30, 31, 32, 33],\n  },\n  27: {\n    id: 27,\n    title: 'Croatia',\n    childIds: []\n  },\n  28: {\n    id: 28,\n    title: 'France',\n    childIds: []\n  },\n  29: {\n    id: 29,\n    title: 'Germany',\n    childIds: []\n  },\n  30: {\n    id: 30,\n    title: 'Italy',\n    childIds: []\n  },\n  31: {\n    id: 31,\n    title: 'Portugal',\n    childIds: []\n  },\n  32: {\n    id: 32,\n    title: 'Spain',\n    childIds: []\n  },\n  33: {\n    id: 33,\n    title: 'Turkey',\n    childIds: []\n  },\n  34: {\n    id: 34,\n    title: 'Oceania',\n    childIds: [35, 36, 37, 38, 39, 40, 41],\n  },\n  35: {\n    id: 35,\n    title: 'Australia',\n    childIds: []\n  },\n  36: {\n    id: 36,\n    title: 'Bora Bora (French Polynesia)',\n    childIds: []\n  },\n  37: {\n    id: 37,\n    title: 'Easter Island (Chile)',\n    childIds: []\n  },\n  38: {\n    id: 38,\n    title: 'Fiji',\n    childIds: []\n  },\n  39: {\n    id: 39,\n    title: 'Hawaii (the USA)',\n    childIds: []\n  },\n  40: {\n    id: 40,\n    title: 'New Zealand',\n    childIds: []\n  },\n  41: {\n    id: 41,\n    title: 'Vanuatu',\n    childIds: []\n  },\n  42: {\n    id: 42,\n    title: 'Moon',\n    childIds: [43, 44, 45]\n  },\n  43: {\n    id: 43,\n    title: 'Rheita',\n    childIds: []\n  },\n  44: {\n    id: 44,\n    title: 'Piccolomini',\n    childIds: []\n  },\n  45: {\n    id: 45,\n    title: 'Tycho',\n    childIds: []\n  },\n  46: {\n    id: 46,\n    title: 'Mars',\n    childIds: [47, 48]\n  },\n  47: {\n    id: 47,\n    title: 'Corn Town',\n    childIds: []\n  },\n  48: {\n    id: 48,\n    title: 'Green Hill',\n    childIds: []\n  }\n};\n```\n\n```css\nbutton { margin: 10px; }\n```\n\n```json package.json\n{\n  \"dependencies\": {\n    \"immer\": \"1.7.3\",\n    \"react\": \"latest\",\n    \"react-dom\": \"latest\",\n    \"react-scripts\": \"latest\",\n    \"use-immer\": \"0.5.1\"\n  },\n  \"scripts\": {\n    \"start\": \"react-scripts start\",\n    \"build\": \"react-scripts build\",\n    \"test\": \"react-scripts test --env=jsdom\",\n    \"eject\": \"react-scripts eject\"\n  }\n}\n```\n\n</Sandpack>\n\n</DeepDive>\n\n때로는 중첩된 state를 자식 컴포넌트로 이동시켜 state 중첩을 줄일 수도 있습니다. 이는 항목이 호버되었는가와 같이 저장할 필요가 없는 임시의 UI state에 대해 잘 작동합니다.\n\n<Recap>\n\n* 만약 두 state 변수가 항상 함께 업데이트된다면, 하나로 합치는 것을 고려해 보세요.\n* State 변수를 신중하게 선택하여 \"불가능한\" state를 만들지 않도록 하세요.\n* State를 업데이트할 때 실수할 가능성을 줄이도록 state를 구조화하세요.\n* 동기화를 유지하지 않아도 되도록 불필요하고 중복된 state를 피하세요.\n* 특별히 업데이트를 방지하려는 경우를 제외하고는 props를 state에 *넣지* 마세요.\n* 선택과 같은 UI 패턴의 경우, 객체 자체가 아닌 ID 또는 인덱스를 state에 유지하세요.\n* 깊게 중첩된 state를 업데이트하는 것이 복잡한 경우, 평탄하게 만들어 보세요.\n\n</Recap>\n\n<Challenges>\n\n#### 업데이트되지 않는 컴포넌트 수정하기 {/*fix-a-component-thats-not-updating*/}\n\n이 `Clock` 컴포넌트는 `color`와 `time` 두 가지 props를 받습니다. 선택 창에서 다른 색상을 선택하면 `Clock` 컴포넌트는 부모 컴포넌트에서 다른 `color` prop을 받습니다. 그러나 어떤 이유에서인지 표시된 색상이 업데이트되지 않습니다. 왜 그럴까요? 문제를 해결하세요.\n\n<Sandpack>\n\n```js src/Clock.js active\nimport { useState } from 'react';\n\nexport default function Clock(props) {\n  const [color, setColor] = useState(props.color);\n  return (\n    <h1 style={{ color: color }}>\n      {props.time}\n    </h1>\n  );\n}\n```\n\n```js src/App.js hidden\nimport { useState, useEffect } from 'react';\nimport Clock from './Clock.js';\n\nfunction useTime() {\n  const [time, setTime] = useState(() => new Date());\n  useEffect(() => {\n    const id = setInterval(() => {\n      setTime(new Date());\n    }, 1000);\n    return () => clearInterval(id);\n  }, []);\n  return time;\n}\n\nexport default function App() {\n  const time = useTime();\n  const [color, setColor] = useState('lightcoral');\n  return (\n    <div>\n      <p>\n        Pick a color:{' '}\n        <select value={color} onChange={e => setColor(e.target.value)}>\n          <option value=\"lightcoral\">lightcoral</option>\n          <option value=\"midnightblue\">midnightblue</option>\n          <option value=\"rebeccapurple\">rebeccapurple</option>\n        </select>\n      </p>\n      <Clock color={color} time={time.toLocaleTimeString()} />\n    </div>\n  );\n}\n```\n\n</Sandpack>\n\n<Solution>\n\n문제는 이 컴포넌트가 `color` prop로 초기화한 `color` state를 갖는 것입니다. 그러나 `color` prop가 변경되면 이는 state 변수에 영향을 주지 않습니다! 그래서 그들은 동기화되지 않습니다. 이 문제를 해결하기 위해, state 변수를 완전히 제거하고 `color` prop를 직접 사용하세요.\n\n<Sandpack>\n\n```js src/Clock.js active\nimport { useState } from 'react';\n\nexport default function Clock(props) {\n  return (\n    <h1 style={{ color: props.color }}>\n      {props.time}\n    </h1>\n  );\n}\n```\n\n```js src/App.js hidden\nimport { useState, useEffect } from 'react';\nimport Clock from './Clock.js';\n\nfunction useTime() {\n  const [time, setTime] = useState(() => new Date());\n  useEffect(() => {\n    const id = setInterval(() => {\n      setTime(new Date());\n    }, 1000);\n    return () => clearInterval(id);\n  }, []);\n  return time;\n}\n\nexport default function App() {\n  const time = useTime();\n  const [color, setColor] = useState('lightcoral');\n  return (\n    <div>\n      <p>\n        Pick a color:{' '}\n        <select value={color} onChange={e => setColor(e.target.value)}>\n          <option value=\"lightcoral\">lightcoral</option>\n          <option value=\"midnightblue\">midnightblue</option>\n          <option value=\"rebeccapurple\">rebeccapurple</option>\n        </select>\n      </p>\n      <Clock color={color} time={time.toLocaleTimeString()} />\n    </div>\n  );\n}\n```\n\n</Sandpack>\n\n또는 구조 분해 구문을 사용하세요.\n\n<Sandpack>\n\n```js src/Clock.js active\nimport { useState } from 'react';\n\nexport default function Clock({ color, time }) {\n  return (\n    <h1 style={{ color: color }}>\n      {time}\n    </h1>\n  );\n}\n```\n\n```js src/App.js hidden\nimport { useState, useEffect } from 'react';\nimport Clock from './Clock.js';\n\nfunction useTime() {\n  const [time, setTime] = useState(() => new Date());\n  useEffect(() => {\n    const id = setInterval(() => {\n      setTime(new Date());\n    }, 1000);\n    return () => clearInterval(id);\n  }, []);\n  return time;\n}\n\nexport default function App() {\n  const time = useTime();\n  const [color, setColor] = useState('lightcoral');\n  return (\n    <div>\n      <p>\n        Pick a color:{' '}\n        <select value={color} onChange={e => setColor(e.target.value)}>\n          <option value=\"lightcoral\">lightcoral</option>\n          <option value=\"midnightblue\">midnightblue</option>\n          <option value=\"rebeccapurple\">rebeccapurple</option>\n        </select>\n      </p>\n      <Clock color={color} time={time.toLocaleTimeString()} />\n    </div>\n  );\n}\n```\n\n</Sandpack>\n\n</Solution>\n\n#### 깨진 포장 목록 수정하기 {/*fix-a-broken-packing-list*/}\n\n이 포장 목록에는 몇 개의 항목이 포장되었는지와 전체 항목 수를 보여주는 푸터가 있습니다. 처음에는 작동하는 것처럼 보이지만 버그가 있습니다. 예를 들어, 항목을 포장했다고 표시했다가 삭제하면 카운터가 올바르게 업데이트되지 않습니다. 항상 올바르게 작동하도록 카운터를 수정하세요.\n\n<Hint>\n\n이 예시에 불필요한 state가 있나요?\n\n</Hint>\n\n<Sandpack>\n\n```js src/App.js\nimport { useState } from 'react';\nimport AddItem from './AddItem.js';\nimport PackingList from './PackingList.js';\n\nlet nextId = 3;\nconst initialItems = [\n  { id: 0, title: 'Warm socks', packed: true },\n  { id: 1, title: 'Travel journal', packed: false },\n  { id: 2, title: 'Watercolors', packed: false },\n];\n\nexport default function TravelPlan() {\n  const [items, setItems] = useState(initialItems);\n  const [total, setTotal] = useState(3);\n  const [packed, setPacked] = useState(1);\n\n  function handleAddItem(title) {\n    setTotal(total + 1);\n    setItems([\n      ...items,\n      {\n        id: nextId++,\n        title: title,\n        packed: false\n      }\n    ]);\n  }\n\n  function handleChangeItem(nextItem) {\n    if (nextItem.packed) {\n      setPacked(packed + 1);\n    } else {\n      setPacked(packed - 1);\n    }\n    setItems(items.map(item => {\n      if (item.id === nextItem.id) {\n        return nextItem;\n      } else {\n        return item;\n      }\n    }));\n  }\n\n  function handleDeleteItem(itemId) {\n    setTotal(total - 1);\n    setItems(\n      items.filter(item => item.id !== itemId)\n    );\n  }\n\n  return (\n    <>\n      <AddItem\n        onAddItem={handleAddItem}\n      />\n      <PackingList\n        items={items}\n        onChangeItem={handleChangeItem}\n        onDeleteItem={handleDeleteItem}\n      />\n      <hr />\n      <b>{packed} out of {total} packed!</b>\n    </>\n  );\n}\n```\n\n```js src/AddItem.js hidden\nimport { useState } from 'react';\n\nexport default function AddItem({ onAddItem }) {\n  const [title, setTitle] = useState('');\n  return (\n    <>\n      <input\n        placeholder=\"Add item\"\n        value={title}\n        onChange={e => setTitle(e.target.value)}\n      />\n      <button onClick={() => {\n        setTitle('');\n        onAddItem(title);\n      }}>Add</button>\n    </>\n  )\n}\n```\n\n```js src/PackingList.js hidden\nimport { useState } from 'react';\n\nexport default function PackingList({\n  items,\n  onChangeItem,\n  onDeleteItem\n}) {\n  return (\n    <ul>\n      {items.map(item => (\n        <li key={item.id}>\n          <label>\n            <input\n              type=\"checkbox\"\n              checked={item.packed}\n              onChange={e => {\n                onChangeItem({\n                  ...item,\n                  packed: e.target.checked\n                });\n              }}\n            />\n            {' '}\n            {item.title}\n          </label>\n          <button onClick={() => onDeleteItem(item.id)}>\n            Delete\n          </button>\n        </li>\n      ))}\n    </ul>\n  );\n}\n```\n\n```css\nbutton { margin: 5px; }\nli { list-style-type: none; }\nul, li { margin: 0; padding: 0; }\n```\n\n</Sandpack>\n\n<Solution>\n\n`total`과 `packed` 카운터를 올바르게 업데이트하도록 각 이벤트 핸들러를 신중하게 변경할 수 있지만, 근본적인 문제는 이 state 변수들이 존재한다는 것입니다. `items` 배열 자체에서 항목 수(포장된 항목 또는 전체)를 항상 계산할 수 있기 때문에 이들은 불필요합니다. 불필요한 state를 제거하여 버그를 수정하세요.\n\n<Sandpack>\n\n```js src/App.js\nimport { useState } from 'react';\nimport AddItem from './AddItem.js';\nimport PackingList from './PackingList.js';\n\nlet nextId = 3;\nconst initialItems = [\n  { id: 0, title: 'Warm socks', packed: true },\n  { id: 1, title: 'Travel journal', packed: false },\n  { id: 2, title: 'Watercolors', packed: false },\n];\n\nexport default function TravelPlan() {\n  const [items, setItems] = useState(initialItems);\n\n  const total = items.length;\n  const packed = items\n    .filter(item => item.packed)\n    .length;\n\n  function handleAddItem(title) {\n    setItems([\n      ...items,\n      {\n        id: nextId++,\n        title: title,\n        packed: false\n      }\n    ]);\n  }\n\n  function handleChangeItem(nextItem) {\n    setItems(items.map(item => {\n      if (item.id === nextItem.id) {\n        return nextItem;\n      } else {\n        return item;\n      }\n    }));\n  }\n\n  function handleDeleteItem(itemId) {\n    setItems(\n      items.filter(item => item.id !== itemId)\n    );\n  }\n\n  return (\n    <>\n      <AddItem\n        onAddItem={handleAddItem}\n      />\n      <PackingList\n        items={items}\n        onChangeItem={handleChangeItem}\n        onDeleteItem={handleDeleteItem}\n      />\n      <hr />\n      <b>{packed} out of {total} packed!</b>\n    </>\n  );\n}\n```\n\n```js src/AddItem.js hidden\nimport { useState } from 'react';\n\nexport default function AddItem({ onAddItem }) {\n  const [title, setTitle] = useState('');\n  return (\n    <>\n      <input\n        placeholder=\"Add item\"\n        value={title}\n        onChange={e => setTitle(e.target.value)}\n      />\n      <button onClick={() => {\n        setTitle('');\n        onAddItem(title);\n      }}>Add</button>\n    </>\n  )\n}\n```\n\n```js src/PackingList.js hidden\nimport { useState } from 'react';\n\nexport default function PackingList({\n  items,\n  onChangeItem,\n  onDeleteItem\n}) {\n  return (\n    <ul>\n      {items.map(item => (\n        <li key={item.id}>\n          <label>\n            <input\n              type=\"checkbox\"\n              checked={item.packed}\n              onChange={e => {\n                onChangeItem({\n                  ...item,\n                  packed: e.target.checked\n                });\n              }}\n            />\n            {' '}\n            {item.title}\n          </label>\n          <button onClick={() => onDeleteItem(item.id)}>\n            Delete\n          </button>\n        </li>\n      ))}\n    </ul>\n  );\n}\n```\n\n```css\nbutton { margin: 5px; }\nli { list-style-type: none; }\nul, li { margin: 0; padding: 0; }\n```\n\n</Sandpack>\n\n이벤트 핸들러가 이 변경 후에 `setItems`를 호출하는 것에만 관심이 있다는 것을 주목하세요. 항목 수는 이제 `items`에서 다음 렌더링하는 동안 계산되므로 항상 최신 상태입니다.\n\n</Solution>\n\n#### 선택 사라짐 수정하기 {/*fix-the-disappearing-selection*/}\n\nState에 `letters` 목록이 있습니다. 특정 문자에 호버 또는 포커스하면 하이라이트 됩니다. 현재 하이라이트 된 문자는 `highlightedLetter` state 변수에 저장됩니다. 각각의 문자에 \"별표\"와 \"별표 해제\"를 할 수 있으며, 이는 state의 `letters` 배열을 업데이트합니다.\n\n이 코드는 작동하지만, 작은 UI 버그가 있습니다. \"별표\" 또는 \"별표 해제\"를 누르면 하이라이트가 잠시 사라집니다. 그러나 포인터를 움직이거나 키보드로 다른 문자로 전환하면 바로 다시 나타납니다. 왜 이런 일이 발생할까요? 버튼 클릭 후 하이라이트가 사라지지 않도록 수정하세요.\n\n<Sandpack>\n\n```js src/App.js\nimport { useState } from 'react';\nimport { initialLetters } from './data.js';\nimport Letter from './Letter.js';\n\nexport default function MailClient() {\n  const [letters, setLetters] = useState(initialLetters);\n  const [highlightedLetter, setHighlightedLetter] = useState(null);\n\n  function handleHover(letter) {\n    setHighlightedLetter(letter);\n  }\n\n  function handleStar(starred) {\n    setLetters(letters.map(letter => {\n      if (letter.id === starred.id) {\n        return {\n          ...letter,\n          isStarred: !letter.isStarred\n        };\n      } else {\n        return letter;\n      }\n    }));\n  }\n\n  return (\n    <>\n      <h2>Inbox</h2>\n      <ul>\n        {letters.map(letter => (\n          <Letter\n            key={letter.id}\n            letter={letter}\n            isHighlighted={\n              letter === highlightedLetter\n            }\n            onHover={handleHover}\n            onToggleStar={handleStar}\n          />\n        ))}\n      </ul>\n    </>\n  );\n}\n```\n\n```js src/Letter.js\nexport default function Letter({\n  letter,\n  isHighlighted,\n  onHover,\n  onToggleStar,\n}) {\n  return (\n    <li\n      className={\n        isHighlighted ? 'highlighted' : ''\n      }\n      onFocus={() => {\n        onHover(letter);\n      }}\n      onPointerMove={() => {\n        onHover(letter);\n      }}\n    >\n      <button onClick={() => {\n        onToggleStar(letter);\n      }}>\n        {letter.isStarred ? 'Unstar' : 'Star'}\n      </button>\n      {letter.subject}\n    </li>\n  )\n}\n```\n\n```js src/data.js\nexport const initialLetters = [{\n  id: 0,\n  subject: 'Ready for adventure?',\n  isStarred: true,\n}, {\n  id: 1,\n  subject: 'Time to check in!',\n  isStarred: false,\n}, {\n  id: 2,\n  subject: 'Festival Begins in Just SEVEN Days!',\n  isStarred: false,\n}];\n```\n\n```css\nbutton { margin: 5px; }\nli { border-radius: 5px; }\n.highlighted { background: #d2eaff; }\n```\n\n</Sandpack>\n\n<Solution>\n\n문제는 `highlightedLetter`에 문자 객체를 보관하고 있다는 것입니다. 그러나 `letters` 배열에서도 동일한 정보를 보관하고 있습니다. 그래서 state에 중복이 있습니다! 버튼 클릭 후 `letters` 배열을 업데이트하면 `highlightedLetter`와 다른 새 문자 객체가 생성됩니다. 이것이 `highlightedLetter === letter` 검사가 `false`가 되고 하이라이트가 사라지는 이유입니다. 포인터가 움직일 때 `setHighlightedLetter`를 호출하면 다시 나타납니다.\n\n문제를 해결하기 위해 state에서 중복을 제거하세요. 두 곳에 *문자 자체* 를 저장하는 대신 `highlightedId`를 저장하세요. 그런 다음 `letter.id === highlightedId`로 각 문자에 대해 `isHighlighted`를 확인할 수 있으며, 이는 마지막 렌더링 이후 `letter` 객체가 변경되었더라도 작동합니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { useState } from 'react';\nimport { initialLetters } from './data.js';\nimport Letter from './Letter.js';\n\nexport default function MailClient() {\n  const [letters, setLetters] = useState(initialLetters);\n  const [highlightedId, setHighlightedId ] = useState(null);\n\n  function handleHover(letterId) {\n    setHighlightedId(letterId);\n  }\n\n  function handleStar(starredId) {\n    setLetters(letters.map(letter => {\n      if (letter.id === starredId) {\n        return {\n          ...letter,\n          isStarred: !letter.isStarred\n        };\n      } else {\n        return letter;\n      }\n    }));\n  }\n\n  return (\n    <>\n      <h2>Inbox</h2>\n      <ul>\n        {letters.map(letter => (\n          <Letter\n            key={letter.id}\n            letter={letter}\n            isHighlighted={\n              letter.id === highlightedId\n            }\n            onHover={handleHover}\n            onToggleStar={handleStar}\n          />\n        ))}\n      </ul>\n    </>\n  );\n}\n```\n\n```js src/Letter.js\nexport default function Letter({\n  letter,\n  isHighlighted,\n  onHover,\n  onToggleStar,\n}) {\n  return (\n    <li\n      className={\n        isHighlighted ? 'highlighted' : ''\n      }\n      onFocus={() => {\n        onHover(letter.id);\n      }}\n      onPointerMove={() => {\n        onHover(letter.id);\n      }}\n    >\n      <button onClick={() => {\n        onToggleStar(letter.id);\n      }}>\n        {letter.isStarred ? 'Unstar' : 'Star'}\n      </button>\n      {letter.subject}\n    </li>\n  )\n}\n```\n\n```js src/data.js\nexport const initialLetters = [{\n  id: 0,\n  subject: 'Ready for adventure?',\n  isStarred: true,\n}, {\n  id: 1,\n  subject: 'Time to check in!',\n  isStarred: false,\n}, {\n  id: 2,\n  subject: 'Festival Begins in Just SEVEN Days!',\n  isStarred: false,\n}];\n```\n\n```css\nbutton { margin: 5px; }\nli { border-radius: 5px; }\n.highlighted { background: #d2eaff; }\n```\n\n</Sandpack>\n\n</Solution>\n\n#### 다중 선택 구현 {/*implement-multiple-selection*/}\n\n이 예시에서 각 `Letter`는 `isSelected` prop와 선택된 것으로 표시하는 `onToggle` 핸들러를 갖고 있습니다. 이는 작동하지만 state는 `selectedId` (`null` 또는 ID)로 저장되므로 한 번에 하나의 문자만 선택할 수 있습니다.\n\n다중 선택을 지원하도록 state 구조를 변경하세요. (어떻게 구조화할까요? 코드를 작성하기 전에 이에 대해 생각해 보세요.) 각 체크박스는 다른 체크박스와 독립적이어야 합니다. 선택된 문자를 클릭하면 선택이 해제되어야 합니다. 마지막으로, 푸터는 선택된 항목의 올바른 수를 보여야 합니다.\n\n<Hint>\n\n하나의 선택된 ID 대신 선택된 ID의 배열 또는 [Set](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set)을 state에 보관할 수 있습니다.\n\n</Hint>\n\n<Sandpack>\n\n```js src/App.js\nimport { useState } from 'react';\nimport { letters } from './data.js';\nimport Letter from './Letter.js';\n\nexport default function MailClient() {\n  const [selectedId, setSelectedId] = useState(null);\n\n  // TODO: allow multiple selection\n  const selectedCount = 1;\n\n  function handleToggle(toggledId) {\n    // TODO: allow multiple selection\n    setSelectedId(toggledId);\n  }\n\n  return (\n    <>\n      <h2>Inbox</h2>\n      <ul>\n        {letters.map(letter => (\n          <Letter\n            key={letter.id}\n            letter={letter}\n            isSelected={\n              // TODO: allow multiple selection\n              letter.id === selectedId\n            }\n            onToggle={handleToggle}\n          />\n        ))}\n        <hr />\n        <p>\n          <b>\n            You selected {selectedCount} letters\n          </b>\n        </p>\n      </ul>\n    </>\n  );\n}\n```\n\n```js src/Letter.js\nexport default function Letter({\n  letter,\n  onToggle,\n  isSelected,\n}) {\n  return (\n    <li className={\n      isSelected ? 'selected' : ''\n    }>\n      <label>\n        <input\n          type=\"checkbox\"\n          checked={isSelected}\n          onChange={() => {\n            onToggle(letter.id);\n          }}\n        />\n        {letter.subject}\n      </label>\n    </li>\n  )\n}\n```\n\n```js src/data.js\nexport const letters = [{\n  id: 0,\n  subject: 'Ready for adventure?',\n  isStarred: true,\n}, {\n  id: 1,\n  subject: 'Time to check in!',\n  isStarred: false,\n}, {\n  id: 2,\n  subject: 'Festival Begins in Just SEVEN Days!',\n  isStarred: false,\n}];\n```\n\n```css\ninput { margin: 5px; }\nli { border-radius: 5px; }\nlabel { width: 100%; padding: 5px; display: inline-block; }\n.selected { background: #d2eaff; }\n```\n\n</Sandpack>\n\n<Solution>\n\n단일 `selectedId` 대신 `selectedIds` *배열* 을 state에 유지하세요. 예를 들어, 첫 번째와 마지막 문자를 선택하면 `[0, 2]`를 포함합니다. 아무것도 선택되지 않은 경우 빈 `[]` 배열이 됩니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { useState } from 'react';\nimport { letters } from './data.js';\nimport Letter from './Letter.js';\n\nexport default function MailClient() {\n  const [selectedIds, setSelectedIds] = useState([]);\n\n  const selectedCount = selectedIds.length;\n\n  function handleToggle(toggledId) {\n    // Was it previously selected?\n    if (selectedIds.includes(toggledId)) {\n      // Then remove this ID from the array.\n      setSelectedIds(selectedIds.filter(id =>\n        id !== toggledId\n      ));\n    } else {\n      // Otherwise, add this ID to the array.\n      setSelectedIds([\n        ...selectedIds,\n        toggledId\n      ]);\n    }\n  }\n\n  return (\n    <>\n      <h2>Inbox</h2>\n      <ul>\n        {letters.map(letter => (\n          <Letter\n            key={letter.id}\n            letter={letter}\n            isSelected={\n              selectedIds.includes(letter.id)\n            }\n            onToggle={handleToggle}\n          />\n        ))}\n        <hr />\n        <p>\n          <b>\n            You selected {selectedCount} letters\n          </b>\n        </p>\n      </ul>\n    </>\n  );\n}\n```\n\n```js src/Letter.js\nexport default function Letter({\n  letter,\n  onToggle,\n  isSelected,\n}) {\n  return (\n    <li className={\n      isSelected ? 'selected' : ''\n    }>\n      <label>\n        <input\n          type=\"checkbox\"\n          checked={isSelected}\n          onChange={() => {\n            onToggle(letter.id);\n          }}\n        />\n        {letter.subject}\n      </label>\n    </li>\n  )\n}\n```\n\n```js src/data.js\nexport const letters = [{\n  id: 0,\n  subject: 'Ready for adventure?',\n  isStarred: true,\n}, {\n  id: 1,\n  subject: 'Time to check in!',\n  isStarred: false,\n}, {\n  id: 2,\n  subject: 'Festival Begins in Just SEVEN Days!',\n  isStarred: false,\n}];\n```\n\n```css\ninput { margin: 5px; }\nli { border-radius: 5px; }\nlabel { width: 100%; padding: 5px; display: inline-block; }\n.selected { background: #d2eaff; }\n```\n\n</Sandpack>\n\n배열을 사용했을 때 사소한 단점은 각 항목에 대해 `selectedIds.includes(letter.id)`를 호출하여 선택 여부를 확인한다는 것입니다. 배열이 매우 큰 경우 [`includes()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/includes)를 사용한 배열 검색은 선형 시간이 걸리고, 개별 항목마다 검색을 수행하기 때문에 성능상 문제가 될 수 있습니다.\n\n이를 해결하기 위해, state에 빠른 [`has()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/has) 연산을 제공하는 [Set](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set)을 대신 보관할 수 있습니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { useState } from 'react';\nimport { letters } from './data.js';\nimport Letter from './Letter.js';\n\nexport default function MailClient() {\n  const [selectedIds, setSelectedIds] = useState(\n    new Set()\n  );\n\n  const selectedCount = selectedIds.size;\n\n  function handleToggle(toggledId) {\n    // Create a copy (to avoid mutation).\n    const nextIds = new Set(selectedIds);\n    if (nextIds.has(toggledId)) {\n      nextIds.delete(toggledId);\n    } else {\n      nextIds.add(toggledId);\n    }\n    setSelectedIds(nextIds);\n  }\n\n  return (\n    <>\n      <h2>Inbox</h2>\n      <ul>\n        {letters.map(letter => (\n          <Letter\n            key={letter.id}\n            letter={letter}\n            isSelected={\n              selectedIds.has(letter.id)\n            }\n            onToggle={handleToggle}\n          />\n        ))}\n        <hr />\n        <p>\n          <b>\n            You selected {selectedCount} letters\n          </b>\n        </p>\n      </ul>\n    </>\n  );\n}\n```\n\n```js src/Letter.js\nexport default function Letter({\n  letter,\n  onToggle,\n  isSelected,\n}) {\n  return (\n    <li className={\n      isSelected ? 'selected' : ''\n    }>\n      <label>\n        <input\n          type=\"checkbox\"\n          checked={isSelected}\n          onChange={() => {\n            onToggle(letter.id);\n          }}\n        />\n        {letter.subject}\n      </label>\n    </li>\n  )\n}\n```\n\n```js src/data.js\nexport const letters = [{\n  id: 0,\n  subject: 'Ready for adventure?',\n  isStarred: true,\n}, {\n  id: 1,\n  subject: 'Time to check in!',\n  isStarred: false,\n}, {\n  id: 2,\n  subject: 'Festival Begins in Just SEVEN Days!',\n  isStarred: false,\n}];\n```\n\n```css\ninput { margin: 5px; }\nli { border-radius: 5px; }\nlabel { width: 100%; padding: 5px; display: inline-block; }\n.selected { background: #d2eaff; }\n```\n\n</Sandpack>\n\n이제 각 항목은 매우 빠른 `selectedIds.has(letter.id)` 검사를 수행합니다.\n\n[State의 객체를 변경해서는 안 되며,](/learn/updating-objects-in-state) Set도 마찬가지입니다. 이것이 `handleToggle` 함수가 먼저 Set의 *복사본* 을 만들고 그 복사본을 업데이트하는 이유입니다.\n\n</Solution>\n\n</Challenges>\n"
  },
  {
    "path": "src/content/learn/conditional-rendering.md",
    "content": "---\ntitle: 조건부 렌더링\n---\n\n<Intro>\n\n컴포넌트는 조건에 따라 다른 항목을 표시해야 하는 경우가 많습니다. React는 `if` 문, `&&` 및 `? :` 연산자와 같은 자바스크립트 문법을 사용하여 조건부로 JSX를 렌더링할 수 있습니다.\n\n</Intro>\n\n<YouWillLearn>\n\n* 조건에 따라 다른 JSX를 반환하는 방법\n* JSX 조각을 조건부로 포함하거나 제외하는 방법\n* React 코드에서 흔히 볼 수 있는 조건부 문법\n\n</YouWillLearn>\n\n## 조건부로 JSX 반환하기 {/*conditionally-returning-jsx*/}\n\n짐을 챙겼는지 안 챙겼는지 표시할 수 있는 여러 개의 `Item`을 렌더링하는 `PackingList` 컴포넌트가 있다고 가정해봅시다.\n\n<Sandpack>\n\n```js\nfunction Item({ name, isPacked }) {\n  return <li className=\"item\">{name}</li>;\n}\n\nexport default function PackingList() {\n  return (\n    <section>\n      <h1>Sally Ride's Packing List</h1>\n      <ul>\n        <Item\n          isPacked={true}\n          name=\"Space suit\"\n        />\n        <Item\n          isPacked={true}\n          name=\"Helmet with a golden leaf\"\n        />\n        <Item\n          isPacked={false}\n          name=\"Photo of Tam\"\n        />\n      </ul>\n    </section>\n  );\n}\n```\n\n</Sandpack>\n\n`Item` 컴포넌트 중 일부는 `isPacked` prop이 `false`가 아닌 `true`로 설정되어 있습니다. `isPacked={true}`인 경우 짐을 챙긴 항목에 체크 표시(✅)를 추가하려고 합니다.\n\n다음과 같이 [`if`/`else` 문](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Statements/if...else)으로 작성할 수 있습니다.\n\n```js\nif (isPacked) {\n  return <li className=\"item\">{name} ✅</li>;\n}\nreturn <li className=\"item\">{name}</li>;\n```\n\n`isPacked` prop이 `true`이면 이 코드는 **다른 JSX 트리를 반환합니다.** 이로 인해 일부 항목은 끝에 체크 표시가 있습니다.\n\n<Sandpack>\n\n```js\nfunction Item({ name, isPacked }) {\n  if (isPacked) {\n    return <li className=\"item\">{name} ✅</li>;\n  }\n  return <li className=\"item\">{name}</li>;\n}\n\nexport default function PackingList() {\n  return (\n    <section>\n      <h1>Sally Ride's Packing List</h1>\n      <ul>\n        <Item\n          isPacked={true}\n          name=\"Space suit\"\n        />\n        <Item\n          isPacked={true}\n          name=\"Helmet with a golden leaf\"\n        />\n        <Item\n          isPacked={false}\n          name=\"Photo of Tam\"\n        />\n      </ul>\n    </section>\n  );\n}\n```\n\n</Sandpack>\n\n각각의 경우를 수정해보고 반환 결과가 어떻게 달라지는지 확인해 보세요!\n\nJavaScript의 `if`와 `return` 문으로 분기 로직을 만드는 방법을 살펴보세요. React에서 제어 흐름(예: 조건문)은 JavaScript로 처리합니다.\n\n### 조건부로 `null`을 사용하여 아무것도 반환하지 않기 {/*conditionally-returning-nothing-with-null*/}\n\n어떤 경우에는 아무것도 렌더링하고 싶지 않을 수 있습니다. 예를 들어, 짐을 챙긴 항목을 전혀 보여주지 않는다고 가정해보세요. 컴포넌트는 반드시 무언가를 반환해야 하는데 이 경우에 `null`을 반환할 수 있습니다. 다음과 같이 말이죠.\n\n```js\nif (isPacked) {\n  return null;\n}\nreturn <li className=\"item\">{name}</li>;\n```\n\n`isPacked`가 `true`라면 컴포넌트는 아무것도 반환하지 않지만, `false`라면 JSX가 반환될 것입니다.\n\n<Sandpack>\n\n```js\nfunction Item({ name, isPacked }) {\n  if (isPacked) {\n    return null;\n  }\n  return <li className=\"item\">{name}</li>;\n}\n\nexport default function PackingList() {\n  return (\n    <section>\n      <h1>Sally Ride's Packing List</h1>\n      <ul>\n        <Item\n          isPacked={true}\n          name=\"Space suit\"\n        />\n        <Item\n          isPacked={true}\n          name=\"Helmet with a golden leaf\"\n        />\n        <Item\n          isPacked={false}\n          name=\"Photo of Tam\"\n        />\n      </ul>\n    </section>\n  );\n}\n```\n\n</Sandpack>\n\n실제로 컴포넌트에서 `null`을 반환하는 것은 개발자가 렌더링하려고 할 때 놀랄 수 있기 때문에 흔한 경우는 아닙니다. 더 자주, 부모 컴포넌트 JSX에 컴포넌트를 조건부로 포함하거나 제외할 수 있습니다. 다음과 같이 해보세요!\n\n## 조건부로 JSX 포함시키기 {/*conditionally-including-jsx*/}\n\n이전 예시에서는 어떤 항목(있는 경우)을 제어했습니다. 컴포넌트에 의해 JSX 트리가 반환되었습니다. 렌더링된 출력 결과에서 이미 일부 중복이 발견되었을 수 있습니다.\n\n```js\n<li className=\"item\">{name} ✅</li>\n```\n\n이것은 아래와 매우 비슷합니다.\n\n```js\n<li className=\"item\">{name}</li>\n```\n\n두 조건부 분기가 모두 `<li className=\"item\">...</li>`를 반환합니다.\n\n```js\nif (isPacked) {\n  return <li className=\"item\">{name} ✅</li>;\n}\nreturn <li className=\"item\">{name}</li>;\n```\n\n이 중복코드가 나쁘지는 않지만, 코드를 유지 보수하기 더 어렵게 만들 수 있습니다. `className`을 바꾸고 싶다면 어떻게 해야 할까요? 코드상 두 군데를 수정해야 합니다! 이러한 상황에서 조건부로 약간의 JSX를 포함해 코드를 더 [DRY(덜 반복적이게)](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself) 하게 만들 수 있습니다.\n\n### 삼항 조건 연산자 (`? :`) {/*conditional-ternary-operator--*/}\n\nJavaScript는 [조건 연산자](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Operators/Conditional_Operator) 또는 \"삼항 조건 연산자\"라는 조건식을 작성하기 위한 간단한 문법이 있습니다.\n\n이 코드 대신,\n\n```js\nif (isPacked) {\n  return <li className=\"item\">{name} ✅</li>;\n}\nreturn <li className=\"item\">{name}</li>;\n```\n\n다음과 같이 작성할 수 있습니다.\n\n```js\nreturn (\n  <li className=\"item\">\n    {isPacked ? name + ' ✅' : name}\n  </li>\n);\n```\n\n*\"`isPacked`가 참이면 (`?`) `name + ' ✔'`을 렌더링하고, 그렇지 않으면 (`:`) `name`을 렌더링한다.\"* 라고 읽을 수 있습니다.\n\n<DeepDive>\n\n#### 두 예시는 완전히 동일할까요? {/*are-these-two-examples-fully-equivalent*/}\n\n`<li>`의 두 가지 다른 \"인스턴스\"를 만들 수 있기 때문에 객체 지향 프로그래밍에서는 위의 두 예가 미묘하게 다르다고 생각할 수 있습니다. 그러나 JSX 엘리먼트는 내부 상태를 보유하지 않으며 실제 DOM 노드가 아니기 때문에 \"인스턴스\"가 아닙니다. 이것은 청사진처럼 간단한 설명입니다. 따라서 위의 두 가지 예시 코드는 실제로 완전히 *동일합니다*. [상태를 보존하고 초기화하기](/learn/preserving-and-resetting-state)에서는 이 기능이 어떻게 작동하는지 자세히 설명합니다.\n\n</DeepDive>\n\n이제 완성된 항목의 텍스트를 `<del>`과 같은 다른 HTML 태그로 줄 바꿈 하여 삭제하려고 합니다. 더 많은 JSX를 중첩하기 쉽도록 새로운 줄과 괄호를 추가할 수 있습니다.\n\n<Sandpack>\n\n```js\nfunction Item({ name, isPacked }) {\n  return (\n    <li className=\"item\">\n      {isPacked ? (\n        <del>\n          {name + ' ✅'}\n        </del>\n      ) : (\n        name\n      )}\n    </li>\n  );\n}\n\nexport default function PackingList() {\n  return (\n    <section>\n      <h1>Sally Ride's Packing List</h1>\n      <ul>\n        <Item\n          isPacked={true}\n          name=\"Space suit\"\n        />\n        <Item\n          isPacked={true}\n          name=\"Helmet with a golden leaf\"\n        />\n        <Item\n          isPacked={false}\n          name=\"Photo of Tam\"\n        />\n      </ul>\n    </section>\n  );\n}\n```\n\n</Sandpack>\n\n이 스타일은 간단한 조건에 잘 어울리지만, 적당히 사용하는 게 좋습니다. 중첩된 조건부 마크업이 너무 많아 컴포넌트가 지저분해질 경우 자식 컴포넌트를 추출하여 정리하세요. React에서 마크업은 코드의 일부이므로 변수 및 함수와 같은 도구를 사용하여 복잡한 식을 정리할 수 있습니다.\n\n### 논리 AND 연산자 (`&&`) {/*logical-and-operator-*/}\n\n또 다른 일반적인 손쉬운 방법은 [JavaScript 논리 AND ('&&') 연산자](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Logical_AND#:~:text=The%20logical%20AND%20(%20%26%26%20)%20operator,it%20returns%20a%20Boolean%20value.)입니다. React 컴포넌트에서는 조건이 참일 때 일부 JSX를 렌더링하거나 **그렇지 않으면 아무것도 렌더링하지 않을 때** 를 나타내는 경우가 많습니다. 다음과 같이 `&&`를 사용하면 `isPacked`가 `true`인 경우에만 조건부로 체크 표시를 렌더링할 수 있습니다.\n\n```js\nreturn (\n  <li className=\"item\">\n    {name} {isPacked && '✅'}\n  </li>\n);\n```\n\n이것을 \"`isPacked`이면 (`&&`) 체크 표시를 렌더링하고, 그렇지 않으면 아무것도 렌더링하지 않습니다.\"라고 읽을 수 있습니다.\n\n자, 잘 작동합니다.\n\n<Sandpack>\n\n```js\nfunction Item({ name, isPacked }) {\n  return (\n    <li className=\"item\">\n      {name} {isPacked && '✅'}\n    </li>\n  );\n}\n\nexport default function PackingList() {\n  return (\n    <section>\n      <h1>Sally Ride's Packing List</h1>\n      <ul>\n        <Item\n          isPacked={true}\n          name=\"Space suit\"\n        />\n        <Item\n          isPacked={true}\n          name=\"Helmet with a golden leaf\"\n        />\n        <Item\n          isPacked={false}\n          name=\"Photo of Tam\"\n        />\n      </ul>\n    </section>\n  );\n}\n```\n\n</Sandpack>\n\n[JavaScript && 표현식](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Logical_AND)은 왼쪽(조건)이 `true`이면 오른쪽(체크 표시)의 값을 반환합니다. 그러나 조건이 `false`이면 전체 표현 식이 `false`가 됩니다. React는 `false`를 `null` 또는 `undefined`처럼 JSX 트리의 \"구멍\"으로 간주하고 그 자리에 아무것도 렌더링하지 않습니다.\n\n\n<Pitfall>\n\n**`&&`의 왼쪽에 숫자를 두지 마세요.**\n\n조건을 테스트하기 위해 JavaScript는 자동으로 왼쪽을 부울로 변환합니다. 그러나 왼쪽이 `0`이면 전체 식이 (`0`)을 얻게 되고, React는 아무것도 아닌 `0`을 렌더링할 것입니다.\n\n예를 들어, 흔하게 하는 실수로 `messageCount && <p>New messages</p>`와 같은 코드를 작성하는 것입니다. 메시지 카운트가 `0`일 때 아무것도 렌더링하지 않는다고 쉽게 추측할 수 있지만, 실제로는 `0` 자체를 렌더링합니다!\n\n이 문제를 해결하려면 `messageCount > 0 && <p>New messages</p>` 처럼 왼쪽을 부울로 만드세요.\n\n</Pitfall>\n\n### 변수에 조건부로 JSX를 할당하기 {/*conditionally-assigning-jsx-to-a-variable*/}\n\n위와 같은 방법이 일반 코드를 작성하는 데 방해가 되면 `if` 문과 변수를 사용하세요. [`let`](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Statements/let)으로 정의된 변수는 재할당할 수 있으므로 표시할 기본 내용인 이름을 먼저 대입하세요.\n\n```js\nlet itemContent = name;\n```\n\n`if` 문을 사용하여 `isPacked`가 `true`인 경우 JSX 표현식을 `itemContent`에 다시 할당합니다.\n\n```js\nif (isPacked) {\n  itemContent = name + \" ✅\";\n}\n```\n\n[중괄호는 \"JavaScript로 들어가는 창\"을 엽니다.](/learn/javascript-in-jsx-with-curly-braces#using-curly-braces-a-window-into-the-javascript-world) 반환된 JSX 트리에 중괄호를 사용하고 이전에 계산된 식을 JSX 내부에 중첩하여 변수를 포함합니다.\n\n```js\n<li className=\"item\">\n  {itemContent}\n</li>\n```\n\n이 스타일은 가장 장황하면서도 가장 유연합니다. 코드가 잘 작동 중입니다.\n\n<Sandpack>\n\n```js\nfunction Item({ name, isPacked }) {\n  let itemContent = name;\n  if (isPacked) {\n    itemContent = name + \" ✅\";\n  }\n  return (\n    <li className=\"item\">\n      {itemContent}\n    </li>\n  );\n}\n\nexport default function PackingList() {\n  return (\n    <section>\n      <h1>Sally Ride's Packing List</h1>\n      <ul>\n        <Item\n          isPacked={true}\n          name=\"Space suit\"\n        />\n        <Item\n          isPacked={true}\n          name=\"Helmet with a golden leaf\"\n        />\n        <Item\n          isPacked={false}\n          name=\"Photo of Tam\"\n        />\n      </ul>\n    </section>\n  );\n}\n```\n\n</Sandpack>\n\n이전과 같이 텍스트뿐만 아니라 임의의 JSX에도 작동합니다.\n\n<Sandpack>\n\n```js\nfunction Item({ name, isPacked }) {\n  let itemContent = name;\n  if (isPacked) {\n    itemContent = (\n      <del>\n        {name + \" ✅\"}\n      </del>\n    );\n  }\n  return (\n    <li className=\"item\">\n      {itemContent}\n    </li>\n  );\n}\n\nexport default function PackingList() {\n  return (\n    <section>\n      <h1>Sally Ride's Packing List</h1>\n      <ul>\n        <Item\n          isPacked={true}\n          name=\"Space suit\"\n        />\n        <Item\n          isPacked={true}\n          name=\"Helmet with a golden leaf\"\n        />\n        <Item\n          isPacked={false}\n          name=\"Photo of Tam\"\n        />\n      </ul>\n    </section>\n  );\n}\n```\n\n</Sandpack>\n\nJavaScript가 익숙하지 않다면, 처음에는 이런 다양한 코드 스타일이 낯설게 보일 수 있습니다. 그러나 이러한 코드를 학습한다면 React 컴포넌트뿐만 아니라 어떤 JavaScript 코드도 읽고 쓸 수 있습니다! 처음에 가장 선호하는 것을 선택해보고, 만약 다른 코드들이 어떻게 작동하는지를 잊어버린다면 이 문서를 다시 참고하세요.\n\n<Recap>\n\n* React에서 JavaScript로 분기 로직을 제어합니다.\n* 조건부로 `if` 문과 함께 JSX 식을 반환할 수 있습니다.\n* 조건부로 일부 JSX를 변수에 저장한 다음 중괄호를 사용하여 다른 JSX에 포함할 수 있습니다.\n* JSX에서 `{cond ? <A /> : <B />}`는 *\"`cond`이면 `<A />`를 렌더링하고, 그렇지 않으면 `<B />`를 렌더링합니다.\"* 를 의미합니다.\n* JSX에서 `{cond && <A />}`는 *\"`cond`이면, `<A />`를 렌더링하되, 그렇지 않으면 아무것도 렌더링하지 않습니다.\"* 를 의미합니다.\n* 위 예시는 흔한 방법이지만, `if`를 선호한다면 사용하지 않아도 됩니다.\n\n</Recap>\n\n\n\n<Challenges>\n\n#### `? :`를 사용하여 완료되지 않은 항목의 아이콘을 표시합니다. {/*show-an-icon-for-incomplete-items-with--*/}\n\n`isPacked`가 `true`가 아닌 경우 조건부 연산자(`cond ? a : b`)를 사용하여 ❌를 렌더링합니다.\n\n<Sandpack>\n\n```js\nfunction Item({ name, isPacked }) {\n  return (\n    <li className=\"item\">\n      {name} {isPacked && '✅'}\n    </li>\n  );\n}\n\nexport default function PackingList() {\n  return (\n    <section>\n      <h1>Sally Ride's Packing List</h1>\n      <ul>\n        <Item\n          isPacked={true}\n          name=\"Space suit\"\n        />\n        <Item\n          isPacked={true}\n          name=\"Helmet with a golden leaf\"\n        />\n        <Item\n          isPacked={false}\n          name=\"Photo of Tam\"\n        />\n      </ul>\n    </section>\n  );\n}\n```\n\n</Sandpack>\n\n<Solution>\n\n<Sandpack>\n\n```js\nfunction Item({ name, isPacked }) {\n  return (\n    <li className=\"item\">\n      {name} {isPacked ? '✅' : '❌'}\n    </li>\n  );\n}\n\nexport default function PackingList() {\n  return (\n    <section>\n      <h1>Sally Ride's Packing List</h1>\n      <ul>\n        <Item\n          isPacked={true}\n          name=\"Space suit\"\n        />\n        <Item\n          isPacked={true}\n          name=\"Helmet with a golden leaf\"\n        />\n        <Item\n          isPacked={false}\n          name=\"Photo of Tam\"\n        />\n      </ul>\n    </section>\n  );\n}\n```\n\n</Sandpack>\n\n</Solution>\n\n#### 항목의 중요한 정도를 `&&`로 표시합니다. {/*show-the-item-importance-with-*/}\n\n이 예시에서 각 `Item`은 숫자 타입인 `importance`를 props로 받습니다. `&&` 연산자를 사용하여 \"_(Importance: X)_\"를 이탤릭체로 렌더링하되 난이도가 0이 아닌 항목만 렌더링합니다. 항목 목록은 다음과 같이 표시합니다.\n\n* Space suit _(Importance: 9)_\n* Helmet with a golden leaf\n* Photo of Tam _(Importance: 6)_\n\n두 레이블 사이에 공백을 추가하는 것을 잊지 마세요!\n\n<Sandpack>\n\n```js\nfunction Item({ name, importance }) {\n  return (\n    <li className=\"item\">\n      {name}\n    </li>\n  );\n}\n\nexport default function PackingList() {\n  return (\n    <section>\n      <h1>Sally Ride's Packing List</h1>\n      <ul>\n        <Item\n          importance={9}\n          name=\"Space suit\"\n        />\n        <Item\n          importance={0}\n          name=\"Helmet with a golden leaf\"\n        />\n        <Item\n          importance={6}\n          name=\"Photo of Tam\"\n        />\n      </ul>\n    </section>\n  );\n}\n```\n\n</Sandpack>\n\n<Solution>\n\n이렇게 하는 건 트릭이지만 효과는 있을 것입니다.\n\n<Sandpack>\n\n```js\nfunction Item({ name, importance }) {\n  return (\n    <li className=\"item\">\n      {name}\n      {importance > 0 && ' '}\n      {importance > 0 &&\n        <i>(Importance: {importance})</i>\n      }\n    </li>\n  );\n}\n\nexport default function PackingList() {\n  return (\n    <section>\n      <h1>Sally Ride's Packing List</h1>\n      <ul>\n        <Item\n          importance={9}\n          name=\"Space suit\"\n        />\n        <Item\n          importance={0}\n          name=\"Helmet with a golden leaf\"\n        />\n        <Item\n          importance={6}\n          name=\"Photo of Tam\"\n        />\n      </ul>\n    </section>\n  );\n}\n```\n\n</Sandpack>\n\n`importance && ...` 보다는 `importance > 0 && ...`로 작성해야 합니다. `importance`가 `0`이면 결과로 `0`이 렌더링 되지 않습니다!\n\n이 솔루션에서는 이름과 중요도 레이블 사이에 공백을 삽입하는 데 두 가지 개별 조건이 사용됩니다. `importance > 0 && <><i>...</i></>` 처럼 공백이 있는 Fragment 를 앞에 사용할 수 있습니다. 또는 `importance > 0 && <i> ...</i>` 처럼 `<i>` 안에 바로 공백을 넣으세요.\n\n</Solution>\n\n#### 변수와 일련의 `? :`를 `if`로 리팩토링합니다. {/*refactor-a-series-of---to-if-and-variables*/}\n\n`Drink` 컴포넌트는 일련의 `? :` 조건을 사용하여 `name` props가 `tea`인지 `coffee`인지에 따라 다른 정보를 표시합니다. 문제는 각 음료에 대한 정보가 여러 가지 조건에 걸쳐 퍼져 있다는 것입니다. 세 가지 `? :` 조건 대신 하나의 `if` 문을 사용하도록 이 코드를 리팩토링하세요.\n\n<Sandpack>\n\n```js\nfunction Drink({ name }) {\n  return (\n    <section>\n      <h1>{name}</h1>\n      <dl>\n        <dt>Part of plant</dt>\n        <dd>{name === 'tea' ? 'leaf' : 'bean'}</dd>\n        <dt>Caffeine content</dt>\n        <dd>{name === 'tea' ? '15–70 mg/cup' : '80–185 mg/cup'}</dd>\n        <dt>Age</dt>\n        <dd>{name === 'tea' ? '4,000+ years' : '1,000+ years'}</dd>\n      </dl>\n    </section>\n  );\n}\n\nexport default function DrinkList() {\n  return (\n    <div>\n      <Drink name=\"tea\" />\n      <Drink name=\"coffee\" />\n    </div>\n  );\n}\n```\n\n</Sandpack>\n\n`if`를 사용하도록 코드를 리팩토링한 후 이를 단순화하는 방법이 있습니까?\n\n<Solution>\n\n이 문제를 해결할 방법은 여러 가지가 있지만, 여기에 한 가지 시작점이 있습니다.\n\n<Sandpack>\n\n```js\nfunction Drink({ name }) {\n  let part, caffeine, age;\n  if (name === 'tea') {\n    part = 'leaf';\n    caffeine = '15–70 mg/cup';\n    age = '4,000+ years';\n  } else if (name === 'coffee') {\n    part = 'bean';\n    caffeine = '80–185 mg/cup';\n    age = '1,000+ years';\n  }\n  return (\n    <section>\n      <h1>{name}</h1>\n      <dl>\n        <dt>Part of plant</dt>\n        <dd>{part}</dd>\n        <dt>Caffeine content</dt>\n        <dd>{caffeine}</dd>\n        <dt>Age</dt>\n        <dd>{age}</dd>\n      </dl>\n    </section>\n  );\n}\n\nexport default function DrinkList() {\n  return (\n    <div>\n      <Drink name=\"tea\" />\n      <Drink name=\"coffee\" />\n    </div>\n  );\n}\n```\n\n</Sandpack>\n\n여기서는 각 음료에 대한 정보가 여러 조건에 분산되지 않고 함께 그룹화됩니다. 그러면 다음에 더 많은 음료를 쉽게 추가할 수 있습니다.\n\n또 다른 해결책은 정보를 객체로 이동하여 조건을 완전히 제거하는 것입니다.\n\n<Sandpack>\n\n```js\nconst drinks = {\n  tea: {\n    part: 'leaf',\n    caffeine: '15–70 mg/cup',\n    age: '4,000+ years'\n  },\n  coffee: {\n    part: 'bean',\n    caffeine: '80–185 mg/cup',\n    age: '1,000+ years'\n  }\n};\n\nfunction Drink({ name }) {\n  const info = drinks[name];\n  return (\n    <section>\n      <h1>{name}</h1>\n      <dl>\n        <dt>Part of plant</dt>\n        <dd>{info.part}</dd>\n        <dt>Caffeine content</dt>\n        <dd>{info.caffeine}</dd>\n        <dt>Age</dt>\n        <dd>{info.age}</dd>\n      </dl>\n    </section>\n  );\n}\n\nexport default function DrinkList() {\n  return (\n    <div>\n      <Drink name=\"tea\" />\n      <Drink name=\"coffee\" />\n    </div>\n  );\n}\n```\n\n</Sandpack>\n\n</Solution>\n\n</Challenges>\n"
  },
  {
    "path": "src/content/learn/creating-a-react-app.md",
    "content": "---\ntitle: 새로운 React 앱 만들기\n---\n\n<Intro>\n\nReact로 새로운 앱이나 웹사이트를 구축하려면 프레임워크부터 시작하는 것이 좋습니다.\n\n</Intro>\n\n앱에 기존 프레임워크에서 잘 제공되지 않는 제약 조건이 있거나, 자체 프레임워크를 빌드하는 것을 선호하거나, React 앱의 기본 사항만 배우려는 경우 [React 앱을 처음부터 빌드할 수 있습니다](/learn/build-a-react-app-from-scratch).\n\n## 풀스택 프레임워크 {/*full-stack-frameworks*/}\n\n이러한 권장 프레임워크는 프로덕션에서 앱을 배포하고 확장하는 데 필요한 모든 기능을 지원합니다. 그들은 최신 React 기능을 통합하고 React의 아키텍처를 활용합니다.\n\n<Note>\n\n#### 풀스택 프레임워크에는 서버가 필요하지 않습니다 {/*react-frameworks-do-not-require-a-server*/}\n\n이 페이지의 모든 프레임워크는 클라이언트 측 렌더링([CSR](https://developer.mozilla.org/en-US/docs/Glossary/CSR)), 단일 페이지 앱([SPA](https://developer.mozilla.org/en-US/docs/Glossary/SPA)), 정적 사이트 생성([SSG](https://developer.mozilla.org/en-US/docs/Glossary/SSG))을 지원합니다. 이러한 앱은 서버 없이 [CDN](https://developer.mozilla.org/en-US/docs/Glossary/CDN) 또는 정적 호스팅 서비스에 배포할 수 있습니다. 또한 이러한 프레임워크를 사용하면 사용 사례에 적합한 경우 경로별로 서버 측 렌더링을 추가할 수 있습니다.\n\n이렇게 하면 클라이언트 전용 앱으로 시작할 수 있으며, 나중에 요구 사항이 변경되는 경우 앱을 다시 작성하지 않고도 개별 경로에서 서버 기능을 사용하도록 선택할 수 있습니다. 렌더링 전략을 구성하는 방법에 대한 프레임워크 설명서를 참조하세요.\n\n</Note>\n\n### Next.js (앱 라우터) {/*nextjs-app-router*/}\n\n**[Next.js의 앱 라우터](https://nextjs.org/docs)는 React의 아키텍처를 최대한 활용하여 풀 스택 React 앱을 활성화하는 React 프레임워크입니다.**\n\n<TerminalBlock>\nnpx create-next-app@latest\n</TerminalBlock>\n\nNext.js는 [Vercel](https://vercel.com/)에서 유지 관리합니다. [Next.js 앱을 빌드](https://nextjs.org/docs/app/building-your-application/deploying)해서 Node.js, 도커 컨테이너, 서버리스 호스팅, 혹은 자체 서버에 배포할 수 있습니다. Next.js는 또한 서버가 필요없는 [정적 내보내기](https://nextjs.org/docs/app/building-your-application/deploying/static-exports)도 지원합니다.\n\n### React Router (v7) {/*react-router-v7*/}\n\n**[React Router](https://reactrouter.com/start/framework/installation)는 React에서 가장 인기 있는 라우팅 라이브러리이며 Vite와 함께 사용하면 풀스택 React 프레임워크를 만들 수 있습니다**. 표준 Web API를 강조하고 다양한 자바스크립트 런타임과 플랫폼을 위한 [준비된 배포 템플릿](https://github.com/remix-run/react-router-templates)이 있습니다.\n\n새로운 React Router 프레임워크를 생성하려면 다음 명령을 사용하세요.\n\n<TerminalBlock>\nnpx create-react-router@latest\n</TerminalBlock>\n\nReact Router는 [Shopify](https://www.shopify.com)에서 유지 관리합니다.\n\n### Expo (네이티브 앱용) {/*expo*/}\n\n**[Expo](https://expo.dev/)는 네이티브 UI를 사용하여 안드로이드, iOS, 웹을 위한 범용앱을 만들 수 있는 React 프레임워크입니다.** 네이티브 부분을 쉽게 사용할 수 있게 해주는 [React Native SDK](https://reactnative.dev/)를 제공합니다. 새로운 Expo 프로젝트를 생성하려면 다음 명령을 사용하세요.\n\n<TerminalBlock>\nnpx create-expo-app@latest\n</TerminalBlock>\n\nExpo를 처음 사용하는 경우, [Expo 자습서](https://docs.expo.dev/tutorial/introduction/)를 참조하세요.\n\nExpo는 [Expo (the company)](https://expo.dev/about)에서 유지 관리합니다. Expo로 앱을 빌드하는 것은 무료이고 구글이나 애플 스토어에 제한없이 제출할 수 있습니다. Expo는 추가적으로 옵트인 유료 클라우드 서비스를 제공합니다.\n\n\n## 다른 프레임워크 {/*other-frameworks*/}\n\n풀스택 React 비전을 향해 나아가고 있는 또 다른 떠오르는 프레임워크가 있습니다.\n\n- [TanStack Start (Beta)](https://tanstack.com/): TanStack Start는 TanStack Router를 기반으로 하는 풀스택 React 프레임워크입니다. Nitro나 Vite와 같이 전체 문서 SSR, 스트리밍, 서버 함수, 번들링과 많은 유용한 도구를 제공합니다.\n- [RedwoodJS](https://redwoodjs.com/): Redwood는 쉽게 풀스택 웹 애플리케이션을 만들 수 있도록 사전탑재된 패키지와 구성을 가진 풀스택 React 프레임워크입니다.\n\n<DeepDive>\n\n#### React 팀의 풀스택 아키텍처 비전을 구성하는 기능은 무엇인가요? {/*which-features-make-up-the-react-teams-full-stack-architecture-vision*/}\n\nNext.js의 App Router 번들러는 공식 [React Server Components 명세](https://github.com/reactjs/rfcs/blob/main/text/0188-server-components.md)를 모두 구현합니다. 이를 통해 빌드 시간, 서버 전용 및 대화형 구성 요소를 단일 React 트리에 혼합할 수 있습니다.\n\n예를 들어, 서버 전용 React 컴포넌트를 데이터베이스나 파일을 읽는 `비동기` 함수로 작성할 수 있습니다. 그런 다음 데이터를 대화형 컴포넌트로 전달할 수 있습니다.\n\n```js\n// 이 컴포넌트는 *오직* 서버에서만(혹은 빌드되는 동안만) 실행됩니다.\nasync function Talks({ confId }) {\n  // 1. 서버에서라면 데이터 레이어와 대화할 수 있습니다. API 엔드포인트는 필요하지 않습니다.\n  const talks = await db.Talks.findAll({ confId });\n\n  // 2. 렌더링 로직이 추가되더라고도 자바스크립트 번들 크기를 크게 만들지 않습니다. \n  const videos = talks.map(talk => talk.video);\n\n  // 3. 브라우저에서 실행될 컴포넌트에 데이터를 전달합니다.\n  return <SearchableVideoList videos={videos} />;\n}\n```\n\nNext.js의 App Router는 [Suspense와 데이터 조회](/blog/2022/03/29/react-v18#suspense-in-data-frameworks)를 통합합니다. React tree에서 서로다른 사용자 인터페이스를 직접적으로 로딩 상태(예: 스켈레톤 플레이스홀더)로 지정할 수 있게 해줍니다.\n\n```js\n<Suspense fallback={<TalksLoading />}>\n  <Talks confId={conf.id} />\n</Suspense>\n```\n\n서버 컴포넌트와 Suspense는 Next.js 기능이 아닌 React 기능입니다. 그러나 프레임워크 수준에서 이를 채택하려면 참여와 사소하지 않은 구현 작업이 필요합니다. 현재 Next.js App Router는 가장 완벽한 구현입니다. React 팀은 차세대 프레임워크에서 이러한 기능을 더 쉽게 구현할 수 있도록 번들러 개발자와 협력하고 있습니다.\n\n</DeepDive>\n\n## 처음부터 시작하기 {/*start-from-scratch*/}\n\n앱에 기존 프레임워크에서 잘 제공되지 않는 제약 조건이 있거나, 자체 프레임워크를 구축하는 것을 선호하거나, React 앱의 기본 사항을 배우려는 경우 React 프로젝트를 처음부터 시작하는 데 사용할 수 있는 다른 옵션이 있습니다.\n\n처음부터 시작하면 더 많은 유연성을 얻을 수 있지만 라우팅, 데이터 가져오기 및 기타 일반적인 사용 패턴에 사용할 도구를 선택해야 합니다. 이미 존재하는 프레임워크를 사용하는 대신 자신만의 프레임워크를 구축하는 것과 비슷합니다. 저희가 [권장하는 프레임워크](#full-stack-frameworks)에는 이러한 문제에 대한 기본 제공 솔루션이 있습니다.\n\n자신만의 솔루션을 구축하려면, [Vite](https://vite.dev/), [Parcel](https://parceljs.org/) 또는 [RSbuild](https://rsbuild.dev/)와 같은 빌드 도구로 시작할 수 있도록 하는 [처음부터 React 앱 만들기](/learn/build-a-react-app-from-scratch) 가이드를 참조하세요.\n\n-----\n\n_만약 이 페이지에 포함되는데 관심있는 프레임워크 작성자라면, [저희에게 알려주세요](https://github.com/reactjs/react.dev/issues/new?assignees=&labels=type%3A+framework&projects=&template=3-framework.yml&title=%5BFramework%5D%3A+)._\n"
  },
  {
    "path": "src/content/learn/describing-the-ui.md",
    "content": "---\ntitle: UI 표현하기\n---\n\n<Intro>\n\nReact는 사용자 인터페이스(UI)를 렌더링하기 위한 JavaScript 라이브러리입니다. UI는 버튼, 텍스트, 이미지와 같은 작은 요소로 구성됩니다. React를 통해 작은 요소들을 재사용 가능하고 중첩할 수 있는 *컴포넌트*로 조합할 수 있습니다. 웹 사이트에서 휴대전화 앱에 이르기까지 화면에 있는 모든 것을 컴포넌트로 나눌 수 있습니다. 이 장에서는 React 컴포넌트를 만들고, 사용자화하며, 조건부로 표시하는 방법에 대해서 알아봅시다.\n\n</Intro>\n\n<YouWillLearn isChapter={true}>\n\n* [첫 React 컴포넌트를 작성하는 방법](/learn/your-first-component)\n* [다중 컴포넌트 파일을 만드는 경우와 방법](/learn/importing-and-exporting-components)\n* [JSX로 JavaScript에 마크업을 추가하는 방법](/learn/writing-markup-with-jsx)\n* [컴포넌트에서 JavaScript 기능을 이용하기 위해 JSX에 중괄호를 사용하는 방법](/learn/javascript-in-jsx-with-curly-braces)\n* [Props를 사용하여 컴포넌트를 구성하는 방법](/learn/passing-props-to-a-component)\n* [조건부 렌더링을 하는 방법](/learn/conditional-rendering)\n* [여러 개의 컴포넌트를 한 번에 렌더링하는 방법](/learn/rendering-lists)\n* [컴포넌트를 순수하게 유지하여 혼란스러운 버그를 피하는 방법](/learn/keeping-components-pure)\n* [트리로서 UI를 이해하는 것이 유용한 이유](/learn/understanding-your-ui-as-a-tree)\n\n</YouWillLearn>\n\n## 첫 컴포넌트 {/*your-first-component*/}\n\nReact 애플리케이션은 *컴포넌트*라고 불리는 독립된 UI 조각들로 이루어집니다. React 컴포넌트는 마크업을 얹을 수 있는 JavaScript 함수입니다. 컴포넌트는 버튼과 같이 작을 수도 있고 전체 페이지와 같이 큰 경우도 있습니다. 다음의 `Gallery` 컴포넌트는 세 개의 `Profile` 컴포넌트를 렌더링하고 있습니다.\n\n<Sandpack>\n\n```js\nfunction Profile() {\n  return (\n    <img\n      src=\"https://i.imgur.com/MK3eW3As.jpg\"\n      alt=\"Katherine Johnson\"\n    />\n  );\n}\n\nexport default function Gallery() {\n  return (\n    <section>\n      <h1>Amazing scientists</h1>\n      <Profile />\n      <Profile />\n      <Profile />\n    </section>\n  );\n}\n```\n\n```css\nimg { margin: 0 10px 10px 0; height: 90px; }\n```\n\n</Sandpack>\n\n<LearnMore path=\"/learn/your-first-component\">\n\nReact 컴포넌트를 선언하고 사용하는 방법을 배우려면 **[첫 컴포넌트](/learn/your-first-component)** 를 읽어보세요.\n\n</LearnMore>\n\n## 컴포넌트 Import 및 Export 하기 {/*importing-and-exporting-components*/}\n\n하나의 파일에 많은 컴포넌트를 선언할 수 있지만, 파일이 커지면 탐색하기 어려워집니다. 이를 해결하기 위해 컴포넌트를 별도의 파일로 만들어 *export*하고 다른 파일에서 해당 컴포넌트를 *import*할 수 있습니다.\n\n<Sandpack>\n\n```js src/App.js hidden\nimport Gallery from './Gallery.js';\n\nexport default function App() {\n  return (\n    <Gallery />\n  );\n}\n```\n\n```js src/Gallery.js active\nimport Profile from './Profile.js';\n\nexport default function Gallery() {\n  return (\n    <section>\n      <h1>Amazing scientists</h1>\n      <Profile />\n      <Profile />\n      <Profile />\n    </section>\n  );\n}\n```\n\n```js src/Profile.js\nexport default function Profile() {\n  return (\n    <img\n      src=\"https://i.imgur.com/QIrZWGIs.jpg\"\n      alt=\"Alan L. Hart\"\n    />\n  );\n}\n```\n\n```css\nimg { margin: 0 10px 10px 0; }\n```\n\n</Sandpack>\n\n<LearnMore path=\"/learn/importing-and-exporting-components\">\n\n컴포넌트를 개별 파일로 분리하는 방법을 배우려면 **[컴포넌트 Import 및 Export 하기](/learn/importing-and-exporting-components)** 를 읽어보세요.\n\n</LearnMore>\n\n## JSX로 마크업 작성하기 {/*writing-markup-with-jsx*/}\n\nReact 컴포넌트는 React가 브라우저에 렌더링하는 마크업을 포함할 수 있는 JavaScript 함수입니다. React 컴포넌트는 그 마크업을 표현하기 위해 JSX라는 확장된 문법을 사용합니다. JSX는 HTML과 매우 유사하지만 조금 더 엄격하며 동적인 정보를 표시할 수 있습니다.\n\n기존의 HTML 마크업을 React 컴포넌트에 그대로 붙여넣으면 동작하지 않을 수도 있습니다.\n\n<Sandpack>\n\n```js\nexport default function TodoList() {\n  return (\n    // This doesn't quite work!\n    <h1>Hedy Lamarr's Todos</h1>\n    <img\n      src=\"https://i.imgur.com/yXOvdOSs.jpg\"\n      alt=\"Hedy Lamarr\"\n      class=\"photo\"\n    >\n    <ul>\n      <li>Invent new traffic lights\n      <li>Rehearse a movie scene\n      <li>Improve spectrum technology\n    </ul>\n  );\n}\n```\n\n```css\nimg { height: 90px; }\n```\n\n</Sandpack>\n\n만약 이미 만들어진 HTML 마크업이 있다면 [converter](https://transform.tools/html-to-jsx)를 사용하여 변환할 수 있습니다.\n\n<Sandpack>\n\n```js\nexport default function TodoList() {\n  return (\n    <>\n      <h1>Hedy Lamarr's Todos</h1>\n      <img\n        src=\"https://i.imgur.com/yXOvdOSs.jpg\"\n        alt=\"Hedy Lamarr\"\n        className=\"photo\"\n      />\n      <ul>\n        <li>Invent new traffic lights</li>\n        <li>Rehearse a movie scene</li>\n        <li>Improve spectrum technology</li>\n      </ul>\n    </>\n  );\n}\n```\n\n```css\nimg { height: 90px; }\n```\n\n</Sandpack>\n\n<LearnMore path=\"/learn/writing-markup-with-jsx\">\n\n올바르게 JSX를 작성하는 방법을 배우려면 **[JSX로 마크업 작성하기](/learn/writing-markup-with-jsx)** 를 읽어보세요.\n\n</LearnMore>\n\n## JSX에서 중괄호를 이용하여 JavaScript 사용하기 {/*javascript-in-jsx-with-curly-braces*/}\n\nJSX를 사용하면 JavaScript 파일에 HTML과 비슷한 마크업을 작성할 수 있어 렌더링 로직과 콘텐츠를 같은 곳에 둘 수 있습니다. 때로는 그 마크업 내부에 JavaScript 로직을 추가하거나 동적인 프로퍼티를 참조해야 하는 경우가 있습니다. 그럴 때 JSX에서 중괄호를 사용하여 JavaScript와 연결된 \"창문을 열 수\" 있습니다.\n\n<Sandpack>\n\n```js\nconst person = {\n  name: 'Gregorio Y. Zara',\n  theme: {\n    backgroundColor: 'black',\n    color: 'pink'\n  }\n};\n\nexport default function TodoList() {\n  return (\n    <div style={person.theme}>\n      <h1>{person.name}'s Todos</h1>\n      <img\n        className=\"avatar\"\n        src=\"https://i.imgur.com/7vQD0fPs.jpg\"\n        alt=\"Gregorio Y. Zara\"\n      />\n      <ul>\n        <li>Improve the videophone</li>\n        <li>Prepare aeronautics lectures</li>\n        <li>Work on the alcohol-fuelled engine</li>\n      </ul>\n    </div>\n  );\n}\n```\n\n```css\nbody { padding: 0; margin: 0 }\nbody > div > div { padding: 20px; }\n.avatar { border-radius: 50%; height: 90px; }\n```\n\n</Sandpack>\n\n<LearnMore path=\"/learn/javascript-in-jsx-with-curly-braces\">\n\nJSX에서 중괄호를 사용하여 JavaScript 데이터에 접근하는 방법을 배우려면 **[JSX에서 중괄호를 이용하여 JavaScript 사용하기](/learn/javascript-in-jsx-with-curly-braces)** 를 읽어보세요.\n\n</LearnMore>\n\n## 컴포넌트에 Props 전달하기 {/*passing-props-to-a-component*/}\n\nReact 컴포넌트는 서로 통신하기 위해 *props*를 사용합니다. 모든 부모 컴포넌트는 자식 컴포넌트에 props를 제공하여 정보를 전달할 수 있습니다. Props는 HTML 어트리뷰트와 유사해 보이지만 객체, 배열, 함수를 포함한 모든 JavaScript 값이 전달될 수 있습니다. 심지어 JSX도 가능합니다!\n\n<Sandpack>\n\n```js\nimport { getImageUrl } from './utils.js'\n\nexport default function Profile() {\n  return (\n    <Card>\n      <Avatar\n        size={100}\n        person={{\n          name: 'Katsuko Saruhashi',\n          imageId: 'YfeOqp2'\n        }}\n      />\n    </Card>\n  );\n}\n\nfunction Avatar({ person, size }) {\n  return (\n    <img\n      className=\"avatar\"\n      src={getImageUrl(person)}\n      alt={person.name}\n      width={size}\n      height={size}\n    />\n  );\n}\n\nfunction Card({ children }) {\n  return (\n    <div className=\"card\">\n      {children}\n    </div>\n  );\n}\n\n```\n\n```js src/utils.js\nexport function getImageUrl(person, size = 's') {\n  return (\n    'https://i.imgur.com/' +\n    person.imageId +\n    size +\n    '.jpg'\n  );\n}\n```\n\n```css\n.card {\n  width: fit-content;\n  margin: 5px;\n  padding: 5px;\n  font-size: 20px;\n  text-align: center;\n  border: 1px solid #aaa;\n  border-radius: 20px;\n  background: #fff;\n}\n.avatar {\n  margin: 20px;\n  border-radius: 50%;\n}\n```\n\n</Sandpack>\n\n<LearnMore path=\"/learn/passing-props-to-a-component\">\n\nProps를 전달하고 활용하는 방법을 배우려면 **[컴포넌트에 Props 전달하기](/learn/passing-props-to-a-component)** 를 읽어보세요.\n\n</LearnMore>\n\n## 조건부 렌더링 {/*conditional-rendering*/}\n\n컴포넌트는 조건에 따라 다른 항목을 표시해야 하는 경우가 많습니다. React는 `if` 문, `&&` 및 `? :` 연산자와 같은 자바스크립트 문법을 사용하여 JSX를 조건부로 렌더링할 수 있습니다.\n\n이 예시에서는 JavaScript `&&` 연산자를 사용하여 체크 표시를 조건부로 렌더링합니다.\n\n<Sandpack>\n\n```js\nfunction Item({ name, isPacked }) {\n  return (\n    <li className=\"item\">\n      {name} {isPacked && '✅'}\n    </li>\n  );\n}\n\nexport default function PackingList() {\n  return (\n    <section>\n      <h1>Sally Ride's Packing List</h1>\n      <ul>\n        <Item\n          isPacked={true}\n          name=\"Space suit\"\n        />\n        <Item\n          isPacked={true}\n          name=\"Helmet with a golden leaf\"\n        />\n        <Item\n          isPacked={false}\n          name=\"Photo of Tam\"\n        />\n      </ul>\n    </section>\n  );\n}\n```\n\n</Sandpack>\n\n<LearnMore path=\"/learn/conditional-rendering\">\n\n**[조건부 렌더링](/learn/conditional-rendering)** 을 통해 콘텐츠를 조건부로 렌더링하는 다양한 방법을 배울 수 있습니다.\n\n</LearnMore>\n\n## 리스트 렌더링 {/*rendering-lists*/}\n\n데이터 모음으로부터 유사한 컴포넌트를 여러 개 표시하고 싶을 때가 종종 있습니다. React와 JavaScript의 `filter()`와 `map()`을 함께 사용하면 데이터 배열을 필터링하고 컴포넌트 배열로 변환할 수 있습니다.\n\n각 배열 항목마다 `key`를 지정해야 합니다. 일반적으로 데이터베이스에서 가져온 ID를 `key`로 사용하게 될 것입니다. Key를 사용하면 리스트가 변경되더라도 React가 각 항목의 위치를 추적할 수 있습니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { people } from './data.js';\nimport { getImageUrl } from './utils.js';\n\nexport default function List() {\n  const listItems = people.map(person =>\n    <li key={person.id}>\n      <img\n        src={getImageUrl(person)}\n        alt={person.name}\n      />\n      <p>\n        <b>{person.name}:</b>\n        {' ' + person.profession + ' '}\n        known for {person.accomplishment}\n      </p>\n    </li>\n  );\n  return (\n    <article>\n      <h1>Scientists</h1>\n      <ul>{listItems}</ul>\n    </article>\n  );\n}\n```\n\n```js src/data.js\nexport const people = [{\n  id: 0,\n  name: 'Creola Katherine Johnson',\n  profession: 'mathematician',\n  accomplishment: 'spaceflight calculations',\n  imageId: 'MK3eW3A'\n}, {\n  id: 1,\n  name: 'Mario José Molina-Pasquel Henríquez',\n  profession: 'chemist',\n  accomplishment: 'discovery of Arctic ozone hole',\n  imageId: 'mynHUSa'\n}, {\n  id: 2,\n  name: 'Mohammad Abdus Salam',\n  profession: 'physicist',\n  accomplishment: 'electromagnetism theory',\n  imageId: 'bE7W1ji'\n}, {\n  id: 3,\n  name: 'Percy Lavon Julian',\n  profession: 'chemist',\n  accomplishment: 'pioneering cortisone drugs, steroids and birth control pills',\n  imageId: 'IOjWm71'\n}, {\n  id: 4,\n  name: 'Subrahmanyan Chandrasekhar',\n  profession: 'astrophysicist',\n  accomplishment: 'white dwarf star mass calculations',\n  imageId: 'lrWQx8l'\n}];\n```\n\n```js src/utils.js\nexport function getImageUrl(person) {\n  return (\n    'https://i.imgur.com/' +\n    person.imageId +\n    's.jpg'\n  );\n}\n```\n\n```css\nul { list-style-type: none; padding: 0px 10px; }\nli {\n  margin-bottom: 10px;\n  display: grid;\n  grid-template-columns: 1fr 1fr;\n  align-items: center;\n}\nimg { width: 100px; height: 100px; border-radius: 50%; }\nh1 { font-size: 22px; }\nh2 { font-size: 20px; }\n```\n\n</Sandpack>\n\n<LearnMore path=\"/learn/rendering-lists\">\n\n컴포넌트 목록을 렌더링하는 방법과 어떻게 key를 선택하는지에 대해 배우려면 **[리스트 렌더링](/learn/rendering-lists)** 을 읽어보세요.\n\n</LearnMore>\n\n## 컴포넌트 순수하게 유지하기 {/*keeping-components-pure*/}\n\n어떤 JavaScript 함수는 *순수*합니다. 순수 함수는 다음과 같은 특징이 있습니다.\n\n* **자신의 일만 처리합니다.** 호출되기 전에 존재했던 어떤 객체나 변수도 변경하지 않습니다.\n* **입력이 같으면 출력도 같습니다.** 순수 함수는 같은 입력을 받으면 언제나 같은 결과를 반환해야 합니다.\n\n컴포넌트를 엄격하게 순수 함수로만 작성하면 코드 베이스가 커져도 이해하기 어려운 버그와 예측할 수 없는 동작을 피할 수 있습니다. 다음은 순수하지 않은 컴포넌트의 예시입니다.\n\n<Sandpack>\n\n```js\nlet guest = 0;\n\nfunction Cup() {\n  // Bad: changing a preexisting variable!\n  guest = guest + 1;\n  return <h2>Tea cup for guest #{guest}</h2>;\n}\n\nexport default function TeaSet() {\n  return (\n    <>\n      <Cup />\n      <Cup />\n      <Cup />\n    </>\n  );\n}\n```\n\n</Sandpack>\n\n기존 변수를 수정하는 대신 prop을 전달하여 컴포넌트를 순수하게 만들 수 있습니다.\n\n<Sandpack>\n\n```js\nfunction Cup({ guest }) {\n  return <h2>Tea cup for guest #{guest}</h2>;\n}\n\nexport default function TeaSet() {\n  return (\n    <>\n      <Cup guest={1} />\n      <Cup guest={2} />\n      <Cup guest={3} />\n    </>\n  );\n}\n```\n\n</Sandpack>\n\n<LearnMore path=\"/learn/keeping-components-pure\">\n\n컴포넌트를 순수하고 예측 가능한 함수로 작성하는 방법을 배우려면 **[컴포넌트 순수하게 유지하기](/learn/keeping-components-pure)** 를 읽어보세요.\n\n</LearnMore>\n\n## 트리로서의 UI {/*your-ui-as-a-tree*/}\n\nReact는 컴포넌트와 모듈 간의 관계를 모델링하기 위해 트리를 사용합니다.\n\nReact 렌더 트리는 컴포넌트 간의 부모-자식 관계를 나타냅니다.\n\n\n<Diagram name=\"generic_render_tree\" height={250} width={500} alt=\"A tree graph with five nodes, with each node representing a component. The root node is located at the top the tree graph and is labelled 'Root Component'. It has two arrows extending down to two nodes labelled 'Component A' and 'Component C'. Each of the arrows is labelled with 'renders'. 'Component A' has a single 'renders' arrow to a node labelled 'Component B'. 'Component C' has a single 'renders' arrow to a node labelled 'Component D'.\">\n\nReact 렌더 트리 예시\n\n</Diagram>\n\n트리의 상단에 위치한 컴포넌트와 루트 컴포넌트 근처의 컴포넌트를 최상위 컴포넌트라고 합니다. 자식 컴포넌트가 없는 컴포넌트를 리프 컴포넌트라고 합니다. 이 컴포넌트 분류는 앱의 데이터 흐름과 성능을 이해하는 데 유용합니다.\n\n자바스크립트 모듈 간의 관계를 모델링하는 것은 앱을 이해하는데 유용한 또 다른 방법입니다. 이를 모듈 의존성 트리라고 정의합니다.\n\n<Diagram name=\"generic_dependency_tree\" height={250} width={500} alt=\"A tree graph with five nodes. Each node represents a JavaScript module. The top-most node is labelled 'RootModule.js'. It has three arrows extending to the nodes: 'ModuleA.js', 'ModuleB.js', and 'ModuleC.js'. Each arrow is labelled as 'imports'. 'ModuleC.js' node has a single 'imports' arrow that points to a node labelled 'ModuleD.js'.\">\n\n모듈 의존성 트리 예시\n\n</Diagram>\n\n의존성 트리는 종종 빌드 도구에 의해 클라이언트가 다운로드하고 렌더링하는 데 필요한 모든 관련 자바스크립트 코드를 번들하는 데에 사용됩니다. 큰 번들 크기는 React 앱의 사용자 경험을 저하합니다. 모듈 의존성 트리를 이해하는 것은 이러한 문제를 디버깅하는 데 도움이 됩니다.\n\n<LearnMore path=\"/learn/understanding-your-ui-as-a-tree\">\n\n React 앱을 위한 렌더, 모듈 의존성 트리를 생성하는 법과 사용자 경험과 성능을 향상하기 위한 유용한 사고방식을 알고 싶은 경우 <strong>[트리로서의 UI](/learn/understanding-your-ui-as-a-tree)</strong>를 읽어보세요.\n\n</LearnMore>\n\n\n## What's next? {/*whats-next*/}\n\n[첫 컴포넌트](/learn/your-first-component) 페이지로 이동하여 이 장을 페이지별로 읽어보세요!\n\n이미 이러한 주제에 대해 알고 있다면 [상호작용 추가하기](/learn/adding-interactivity)를 읽어보는 것은 어떨까요?\n"
  },
  {
    "path": "src/content/learn/editor-setup.md",
    "content": "---\ntitle: 에디터 설정하기\n---\n\n<Intro>\n\n적절한 개발 환경은 코드의 가독성 및 개발 속도를 높여줍니다. 심지어 코드를 작성하는 과정에서 버그를 찾아줄 수도 있습니다. 코드 에디터를 설치하는 것이 이번이 처음이거나, 현재 사용하는 에디터의 설정을 개선하고 싶으시다면, 몇 가지를 추천드립니다.\n\n</Intro>\n\n<YouWillLearn>\n\n* 어떤 에디터가 가장 유명한지\n* 어떻게 자동으로 코드를 포맷팅하는지\n\n</YouWillLearn>\n\n## 에디터 {/*your-editor*/}\n\n[VS Code](https://code.visualstudio.com/)는 현재 가장 많이 사용하는 에디터 중 하나입니다. VS Code에 설치할 수 있는 확장<sup>Extension</sup>의 종류는 무수히 많으며, [깃허브<sup>GitHub</sup>](https://github.com)와 같은 외부 서비스와의 연동도 지원합니다. 아래에 나열한 기능들은 대부분 확장<sup>Extension</sup>으로 존재하기 때문에 VS Code의 설정을 다양한 방식으로 쉽게 변경할 수 있습니다.\n\n이 외에도 React 커뮤니티에서는 다음과 같은 에디터들을 자주 사용합니다.\n\n* [WebStorm](https://www.jetbrains.com/ko-kr/webstorm/)은 자바스크립트<sup>JavaScript</sup>에 특화되어 설계된 통합 개발 환경입니다.\n* [Sublime Text](https://www.sublimetext.com/)는 JSX와 타입스크립트<sup>TypeScript</sup>를 지원하며 [문법 강조](https://stackoverflow.com/a/70960574/458193) 및 자동 완성 기능이 내장되어 있습니다.\n* [Vim](https://www.vim.org/)은 모든 종류의 텍스트를 매우 효율적으로 생성하고 변경할 수 있도록 설계된 텍스트 편집기입니다. 대부분의 UNIX 시스템과 macOS에 \"vi\"로 포함되어 있습니다.\n\n## 에디터 기능 추천 {/*recommended-text-editor-features*/}\n\n이러한 기능들이 기본으로 설정된 에디터들도 있지만, 별도의 확장<sup>Extensions</sup> 추가가 필요한 경우도 존재합니다. 현재 사용 중인 에디터에서 어떠한 기능들을 지원하는지 한번 확인해 보세요!\n\n### 린팅<sup>Linting</sup> {/*linting*/}\n\n코드 린터<sup>Linter</sup>는 코드를 작성하는 동안 실시간으로 문제를 찾아, 빠른 문제 해결을 도와줍니다. 자바스크립트를 위한 오픈 소스 린터<sup>Linter</sup>인 [ESLint](https://eslint.org)를 가장 많이 사용합니다!\n\n* [React를 위한 추천 설정과 함께 ESLint 설치하기](https://www.npmjs.com/package/eslint-config-react-app) (사전에 [Node.js](https://nodejs.org/ko/download/current/)를 설치해야 합니다.)\n* [VS Code의 ESLint를 공식 익스텐션과 통합하기](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint)\n\n**프로젝트의 모든 [`eslint-plugin-react-hooks`](https://www.npmjs.com/package/eslint-plugin-react-hooks) 규칙을 활성화했는지 확인하세요.** 이 규칙은 필수적이며 가장 심각한 버그를 조기에 발견합니다. 권장되는 [`eslint-config-react-app`](https://www.npmjs.com/package/eslint-config-react-app) 프리셋에는 이미 이 규칙이 포함되어 있습니다.\n\n### 포맷팅<sup>Formatting</sup> {/*formatting*/}\n\n다른 개발자들과 협업할 때 가장 피하고 싶은 것은 [탭 vs 공백](https://www.google.com/search?q=tabs+vs+spaces)에 대한 논쟁일 것입니다. 다행히 [Prettier](https://prettier.io/)를 사용하면 직접 지정해 놓은 규칙들에 부합하도록 코드의 형식을 깔끔하게 정리할 수 있습니다. Prettier를 실행하면 모든 탭은 공백으로 전환될 뿐만 아니라 들여쓰기, 따옴표 형식과 같은 요소들이 전부 설정에 부합하도록 수정될 것입니다. 파일을 저장할 때마다 Prettier가 자동 실행되어 이러한 작업들을 수행해 주는 것이 가장 이상적인 설정입니다.\n\n다음과 같은 단계를 통해 [VS Code의 Prettier 익스텐션](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode)을 설치할 수 있습니다.\n\n1. VS Code 실행하기\n2. 퀵오픈 사용하기 (Ctrl/Cmd+P 누르기)\n3. `ext install esbenp.prettier-vscode`라고 입력하기\n4. 엔터 누르기\n\n#### 저장 시점에 포맷팅하기 {/*formatting-on-save*/}\n\n저장할 때마다 코드가 포맷팅 되는 것이 가장 이상적일 것입니다. 이러한 설정은 VS Code에 자체적으로 내장되어 있습니다!\n\n1. VS Code에서 `CTRL/CMD + SHIFT + P` 누르기\n2. \"settings\"라고 입력하기\n3. 엔터 누르기\n4. 검색 창에서 \"format on save\"라고 입력하기\n5. \"format on save\" 옵션이 제대로 체크되었는지 확인하세요!\n\n> 만약 ESLint 프리셋에 포맷팅 규칙이 있는 경우 Prettier와 충돌을 일으킬 수도 있습니다. ESLint가 *오직* 논리적 실수를 잡는 데만 사용되도록 [`eslint-config-prettier`](https://github.com/prettier/eslint-config-prettier)를 사용하여 ESLint 프리셋의 모든 포맷팅 규칙을 비활성화하는 것을 권장합니다. Pull Request가 병합<sup>Merge</sup>되기 전에 파일 형식이 지정되도록 하려면 지속적인 통합을 위해 [`prettier --check`](https://prettier.io/docs/en/cli.html#--check)를 사용하세요.\n"
  },
  {
    "path": "src/content/learn/escape-hatches.md",
    "content": "---\ntitle: 탈출구\n---\n\n<Intro>\n\n일부 컴포넌트는 React 외부의 시스템을 제어하고 동기화해야 할 수 있습니다. 예를 들어 브라우저 API를 사용해 input에 초점을 맞추거나, React 없이 구현된 비디오 플레이어를 재생 및 일시 정지하거나, 원격 서버에 연결해서 메시지를 수신해야 할 수 있습니다. 이 장에서는 React의 \"외부\"로 나가서 외부 시스템에 연결할 수 있는 탈출구를 배웁니다. 대부분의 애플리케이션 로직과 데이터 흐름은 이러한 기능에 의존해서는 안 됩니다.\n\n</Intro>\n\n<YouWillLearn isChapter={true}>\n\n* [다시 렌더링하지 않고 정보를 \"기억\"하는 방법](/learn/referencing-values-with-refs)\n* [React가 관리하는 DOM 엘리먼트에 접근하는 방법](/learn/manipulating-the-dom-with-refs)\n* [컴포넌트를 외부 시스템과 동기화하는 방법](/learn/synchronizing-with-effects)\n* [컴포넌트에서 불필요한 Effect를 제거하는 방법](/learn/you-might-not-need-an-effect)\n* [Effect의 생명주기가 컴포넌트와 어떻게 다른지](/learn/lifecycle-of-reactive-effects)\n* [일부 값이 Effect를 다시 발생시키는 것을 막는 방법](/learn/separating-events-from-effects)\n* [Effect 재실행을 줄이는 방법](/learn/removing-effect-dependencies)\n* [컴포넌트 간 로직을 공유하는 방법](/learn/reusing-logic-with-custom-hooks)\n\n</YouWillLearn>\n\n## Ref로 값 참조하기 {/*referencing-values-with-refs*/}\n\n컴포넌트가 일부 정보를 \"기억\"하고 싶지만, 해당 정보가 [렌더링을 유발](/learn/render-and-commit)하지 않도록 하려면 Ref를 사용하세요.\n\n```js\nconst ref = useRef(0);\n```\n\nState처럼 Ref는 다시 렌더링하는 사이에 React에 의해 유지됩니다. 다만 State의 설정은 컴포넌트를 다시 렌더링 하지만, Ref의 변경은 그렇지 않습니다! `ref.current` 프로퍼티를 통해 해당 Ref의 현재 값에 접근할 수 있습니다.\n\n<Sandpack>\n\n```js\nimport { useRef } from 'react';\n\nexport default function Counter() {\n  let ref = useRef(0);\n\n  function handleClick() {\n    ref.current = ref.current + 1;\n    alert('You clicked ' + ref.current + ' times!');\n  }\n\n  return (\n    <button onClick={handleClick}>\n      Click me!\n    </button>\n  );\n}\n```\n\n</Sandpack>\n\nRef는 React가 추적하지 않는 컴포넌트의 비밀 주머니와 같습니다. 예를 들어 Ref를 사용하여 컴포넌트의 렌더링 출력에 영향을 주지 않는 [Timeout ID](https://developer.mozilla.org/en-US/docs/Web/API/setTimeout#return_value), [DOM 엘리먼트](https://developer.mozilla.org/en-US/docs/Web/API/Element) 및 기타 객체를 저장할 수 있습니다.\n\n<LearnMore path=\"/learn/referencing-values-with-refs\">\n\nRef를 사용하여 정보를 기억하는 방법을 배우려면 <strong>[Ref로 값 참조하기](/learn/referencing-values-with-refs)</strong>를 읽어보세요.\n\n</LearnMore>\n\n## Ref로 DOM 조작하기 {/*manipulating-the-dom-with-refs*/}\n\nReact는 렌더링 결과물에 맞춰 DOM 변경을 자동으로 처리하기 때문에 컴포넌트에서 자주 DOM을 조작해야 할 필요는 없습니다. 하지만 가끔 특정 노드에 포커스를 옮기거나, 스크롤 위치를 옮기거나, 위치와 크기를 측정하기 위해서 React가 관리하는 DOM 요소에 접근해야 할 때가 있습니다. React는 이런 작업을 수행하는 내장 방법을 제공하지 않기 때문에 DOM 노드에 접근하기 위한 Ref가 필요할 것입니다. 예를 들어 버튼을 클릭하면 Ref를 사용해 input에 포커스를 옮길 것입니다.\n\n<Sandpack>\n\n```js\nimport { useRef } from 'react';\n\nexport default function Form() {\n  const inputRef = useRef(null);\n\n  function handleClick() {\n    inputRef.current.focus();\n  }\n\n  return (\n    <>\n      <input ref={inputRef} />\n      <button onClick={handleClick}>\n        Focus the input\n      </button>\n    </>\n  );\n}\n```\n\n</Sandpack>\n\n<LearnMore path=\"/learn/manipulating-the-dom-with-refs\">\n\nReact가 관리하는 DOM 엘리먼트에 접근하는 방법을 배우려면 <strong>[Ref로 DOM 조작하기](/learn/manipulating-the-dom-with-refs)</strong>를 읽어보세요.\n\n</LearnMore>\n\n## Effect로 값 동기화하기 {/*synchronizing-with-effects*/}\n\n일부 컴포넌트는 외부 시스템과 동기화해야 합니다. 예를 들어 React State에 따라 React가 아닌 컴포넌트를 제어하거나, 채팅 서버에 대한 연결을 설정하거나, 컴포넌트가 화면에 나타났을 때 분석 로그를 보낼 수 있습니다. 특정 이벤트를 처리하는 이벤트 핸들러와 달리 Effect는 렌더링 후 일부 코드를 실행합니다. 컴포넌트를 React 외부 시스템과 동기화할 때 이를 사용하세요.\n\nPlay/Pause를 몇 번 누르고 비디오 플레이어가 `isPlaying` Prop 값을 어떻게 동기화하는지 확인하세요.\n\n<Sandpack>\n\n```js\nimport { useState, useRef, useEffect } from 'react';\n\nfunction VideoPlayer({ src, isPlaying }) {\n  const ref = useRef(null);\n\n  useEffect(() => {\n    if (isPlaying) {\n      ref.current.play();\n    } else {\n      ref.current.pause();\n    }\n  }, [isPlaying]);\n\n  return <video ref={ref} src={src} loop playsInline />;\n}\n\nexport default function App() {\n  const [isPlaying, setIsPlaying] = useState(false);\n  return (\n    <>\n      <button onClick={() => setIsPlaying(!isPlaying)}>\n        {isPlaying ? 'Pause' : 'Play'}\n      </button>\n      <VideoPlayer\n        isPlaying={isPlaying}\n        src=\"https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4\"\n      />\n    </>\n  );\n}\n```\n\n```css\nbutton { display: block; margin-bottom: 20px; }\nvideo { width: 250px; }\n```\n\n</Sandpack>\n\n많은 Effect는 스스로 \"클린업\"하기도 합니다. 예를 들어 채팅 서버에 대한 연결을 설정하는 Effect는 해당 서버에서 컴포넌트의 연결을 끊는 방법을 React에 알려주는 <em>클린업 함수</em>를 반환해야 합니다.\n\n<Sandpack>\n\n```js\nimport { useState, useEffect } from 'react';\nimport { createConnection } from './chat.js';\n\nexport default function ChatRoom() {\n  useEffect(() => {\n    const connection = createConnection();\n    connection.connect();\n    return () => connection.disconnect();\n  }, []);\n  return <h1>Welcome to the chat!</h1>;\n}\n```\n\n```js src/chat.js\nexport function createConnection() {\n  // A real implementation would actually connect to the server\n  return {\n    connect() {\n      console.log('✅ Connecting...');\n    },\n    disconnect() {\n      console.log('❌ Disconnected.');\n    }\n  };\n}\n```\n\n```css\ninput { display: block; margin-bottom: 20px; }\n```\n\n</Sandpack>\n\n개발 모드에서 React는 즉시 실행되고 Effect를 한 번 더 클린업합니다. 그래서 `\"✅ Connecting...\"`을 두 번 출력하는 것입니다. 이렇게 하여 클린업 함수를 구현하는 것을 잊지 않도록 합니다.\n\n<LearnMore path=\"/learn/synchronizing-with-effects\">\n\n컴포넌트를 외부 시스템과 동기화하는 방법을 배우려면 <strong>[Effect로 동기화하기](/learn/synchronizing-with-effects)</strong>를 읽어보세요.\n\n</LearnMore>\n\n## Effect가 필요하지 않은 경우 {/*you-might-not-need-an-effect*/}\n\nEffect는 React 패러다임에서 벗어날 수 있는 탈출구입니다. Effect를 사용하면 React를 \"벗어나\" 컴포넌트를 외부 시스템과 동기화할 수 있습니다. 외부 시스템이 관여하지 않는 경우 (예를 들어 일부 Props 또는 State가 변경될 때 컴포넌트의 State를 업데이트하려는 경우) Effect가 필요하지 않습니다. 불필요한 Effect를 제거하면 코드를 더 쉽게 따라갈 수 있고, 실행 속도가 빨라지며, 에러 발생 가능성이 줄어듭니다.\n\nEffect가 필요하지 않은 두 가지 일반적인 경우가 있습니다.\n- **렌더링을 위해 데이터를 변환하는 데 Effect가 필요하지 않습니다.**\n- **사용자 이벤트를 처리하는 데 Effect가 필요하지 않습니다.**\n\n예를 들어, 다른 State에 따라 일부 State를 조정하는 데는 Effect가 필요하지 않습니다.\n\n```js {5-9}\nfunction Form() {\n  const [firstName, setFirstName] = useState('Taylor');\n  const [lastName, setLastName] = useState('Swift');\n\n  // 🔴 Avoid: redundant state and unnecessary Effect\n  const [fullName, setFullName] = useState('');\n  useEffect(() => {\n    setFullName(firstName + ' ' + lastName);\n  }, [firstName, lastName]);\n  // ...\n}\n```\n\n대신에 렌더링하는 동안 가능한 한 많이 계산하세요.\n\n```js {4-5}\nfunction Form() {\n  const [firstName, setFirstName] = useState('Taylor');\n  const [lastName, setLastName] = useState('Swift');\n  // ✅ Good: calculated during rendering\n  const fullName = firstName + ' ' + lastName;\n  // ...\n}\n```\n\n그러나 외부 시스템과 동기화하려면 Effect가 <em>필요</em>합니다.\n\n<LearnMore path=\"/learn/you-might-not-need-an-effect\">\n\n불필요한 Effect를 제거하는 방법을 배우려면 <strong>[Effect가 필요하지 않은 경우](/learn/you-might-not-need-an-effect)</strong>를 읽어보세요.\n\n</LearnMore>\n\n## React Effect의 생명주기 {/*lifecycle-of-reactive-effects*/}\n\nEffect는 컴포넌트와 다른 생명주기를 가집니다. 컴포넌트는 마운트, 업데이트 또는 마운트 해제할 수 있습니다. 반면 Effect는 동기화를 시작하거나 후에 동기화를 중지하는 두 가지 작업만 할 수 있습니다. Effect가 시간에 따라 변하는 Props와 State에 의존하는 경우 이 주기는 여러 번 발생할 수 있습니다.\n\n다음 Effect는 `roomId` Prop의 값에 의존합니다. Props는 다시 렌더링할 때 변할 수 있는 *반응형 값* 입니다. `roomId`가 변경되면 Effect가 *다시 동기화* (및 서버에 다시 연결)합니다.\n\n<Sandpack>\n\n```js\nimport { useState, useEffect } from 'react';\nimport { createConnection } from './chat.js';\n\nconst serverUrl = 'https://localhost:1234';\n\nfunction ChatRoom({ roomId }) {\n  useEffect(() => {\n    const connection = createConnection(serverUrl, roomId);\n    connection.connect();\n    return () => connection.disconnect();\n  }, [roomId]);\n\n  return <h1>Welcome to the {roomId} room!</h1>;\n}\n\nexport default function App() {\n  const [roomId, setRoomId] = useState('general');\n  return (\n    <>\n      <label>\n        Choose the chat room:{' '}\n        <select\n          value={roomId}\n          onChange={e => setRoomId(e.target.value)}\n        >\n          <option value=\"general\">general</option>\n          <option value=\"travel\">travel</option>\n          <option value=\"music\">music</option>\n        </select>\n      </label>\n      <hr />\n      <ChatRoom roomId={roomId} />\n    </>\n  );\n}\n```\n\n```js src/chat.js\nexport function createConnection(serverUrl, roomId) {\n  // A real implementation would actually connect to the server\n  return {\n    connect() {\n      console.log('✅ Connecting to \"' + roomId + '\" room at ' + serverUrl + '...');\n    },\n    disconnect() {\n      console.log('❌ Disconnected from \"' + roomId + '\" room at ' + serverUrl);\n    }\n  };\n}\n```\n\n```css\ninput { display: block; margin-bottom: 20px; }\nbutton { margin-left: 10px; }\n```\n\n</Sandpack>\n\nReact는 Effect의 의존성을 올바르게 명시했는지 확인하는 린터 규칙을 제공합니다. 위의 예시에서 의존성 목록에 `roomId`를 명시하는 것을 잊어버렸다면, 린터가 해당 버그를 자동으로 찾아낼 것입니다.\n\n<LearnMore path=\"/learn/lifecycle-of-reactive-effects\">\n\nEffect의 생명주기가 컴포넌트와 어떻게 다른지를 배우려면 <strong>[React Effect의 생명주기](/learn/lifecycle-of-reactive-effects)</strong>를 읽어보세요.\n\n</LearnMore>\n\n## Effect에서 이벤트 분리하기 {/*separating-events-from-effects*/}\n\n이벤트 핸들러는 같은 상호작용을 다시 수행할 때만 다시 실행됩니다. 이벤트 핸들러와 달리, Effect는 props나 state와 같이 읽은 값이 마지막 렌더링 때와 다르면 다시 동기화됩니다. 때로는 두 가지 동작을 혼합하여, 일부 값에만 반응하고 다른 값에는 반응하지 않는 Effect를 원할 수도 있습니다.\n\nEffect 내의 모든 코드는 <em>반응형</em>이며, 읽은 반응형 값이 다시 렌더링되는 것으로 인해 변경되면 다시 실행됩니다. 예를 들어 다음의 Effect는 `roomId` 또는 `theme`이 변경되면 채팅에 다시 연결됩니다.\n\n<Sandpack>\n\n```json package.json hidden\n{\n  \"dependencies\": {\n    \"react\": \"latest\",\n    \"react-dom\": \"latest\",\n    \"react-scripts\": \"latest\",\n    \"toastify-js\": \"1.12.0\"\n  },\n  \"scripts\": {\n    \"start\": \"react-scripts start\",\n    \"build\": \"react-scripts build\",\n    \"test\": \"react-scripts test --env=jsdom\",\n    \"eject\": \"react-scripts eject\"\n  }\n}\n```\n\n```js\nimport { useState, useEffect } from 'react';\nimport { createConnection, sendMessage } from './chat.js';\nimport { showNotification } from './notifications.js';\n\nconst serverUrl = 'https://localhost:1234';\n\nfunction ChatRoom({ roomId, theme }) {\n  useEffect(() => {\n    const connection = createConnection(serverUrl, roomId);\n    connection.on('connected', () => {\n      showNotification('Connected!', theme);\n    });\n    connection.connect();\n    return () => connection.disconnect();\n  }, [roomId, theme]);\n\n  return <h1>Welcome to the {roomId} room!</h1>\n}\n\nexport default function App() {\n  const [roomId, setRoomId] = useState('general');\n  const [isDark, setIsDark] = useState(false);\n  return (\n    <>\n      <label>\n        Choose the chat room:{' '}\n        <select\n          value={roomId}\n          onChange={e => setRoomId(e.target.value)}\n        >\n          <option value=\"general\">general</option>\n          <option value=\"travel\">travel</option>\n          <option value=\"music\">music</option>\n        </select>\n      </label>\n      <label>\n        <input\n          type=\"checkbox\"\n          checked={isDark}\n          onChange={e => setIsDark(e.target.checked)}\n        />\n        Use dark theme\n      </label>\n      <hr />\n      <ChatRoom\n        roomId={roomId}\n        theme={isDark ? 'dark' : 'light'}\n      />\n    </>\n  );\n}\n```\n\n```js src/chat.js\nexport function createConnection(serverUrl, roomId) {\n  // A real implementation would actually connect to the server\n  let connectedCallback;\n  let timeout;\n  return {\n    connect() {\n      timeout = setTimeout(() => {\n        if (connectedCallback) {\n          connectedCallback();\n        }\n      }, 100);\n    },\n    on(event, callback) {\n      if (connectedCallback) {\n        throw Error('Cannot add the handler twice.');\n      }\n      if (event !== 'connected') {\n        throw Error('Only \"connected\" event is supported.');\n      }\n      connectedCallback = callback;\n    },\n    disconnect() {\n      clearTimeout(timeout);\n    }\n  };\n}\n```\n\n```js src/notifications.js\nimport Toastify from 'toastify-js';\nimport 'toastify-js/src/toastify.css';\n\nexport function showNotification(message, theme) {\n  Toastify({\n    text: message,\n    duration: 2000,\n    gravity: 'top',\n    position: 'right',\n    style: {\n      background: theme === 'dark' ? 'black' : 'white',\n      color: theme === 'dark' ? 'white' : 'black',\n    },\n  }).showToast();\n}\n```\n\n```css\nlabel { display: block; margin-top: 10px; }\n```\n\n</Sandpack>\n\n이것은 이상적이지 않습니다. `roomId`가 변경된 경우에만 채팅에 다시 연결하고 싶습니다. `theme`을 전환해도 채팅에 다시 연결되지 않아야 합니다! `theme`를 읽는 코드를 Effect에서 *Effect Event*로 옮기세요.\n\n<Sandpack>\n\n```json package.json hidden\n{\n  \"dependencies\": {\n    \"react\": \"latest\",\n    \"react-dom\": \"latest\",\n    \"react-scripts\": \"latest\",\n    \"toastify-js\": \"1.12.0\"\n  },\n  \"scripts\": {\n    \"start\": \"react-scripts start\",\n    \"build\": \"react-scripts build\",\n    \"test\": \"react-scripts test --env=jsdom\",\n    \"eject\": \"react-scripts eject\"\n  }\n}\n```\n\n```js\nimport { useState, useEffect } from 'react';\nimport { useEffectEvent } from 'react';\nimport { createConnection, sendMessage } from './chat.js';\nimport { showNotification } from './notifications.js';\n\nconst serverUrl = 'https://localhost:1234';\n\nfunction ChatRoom({ roomId, theme }) {\n  const onConnected = useEffectEvent(() => {\n    showNotification('Connected!', theme);\n  });\n\n  useEffect(() => {\n    const connection = createConnection(serverUrl, roomId);\n    connection.on('connected', () => {\n      onConnected();\n    });\n    connection.connect();\n    return () => connection.disconnect();\n  }, [roomId]);\n\n  return <h1>Welcome to the {roomId} room!</h1>\n}\n\nexport default function App() {\n  const [roomId, setRoomId] = useState('general');\n  const [isDark, setIsDark] = useState(false);\n  return (\n    <>\n      <label>\n        Choose the chat room:{' '}\n        <select\n          value={roomId}\n          onChange={e => setRoomId(e.target.value)}\n        >\n          <option value=\"general\">general</option>\n          <option value=\"travel\">travel</option>\n          <option value=\"music\">music</option>\n        </select>\n      </label>\n      <label>\n        <input\n          type=\"checkbox\"\n          checked={isDark}\n          onChange={e => setIsDark(e.target.checked)}\n        />\n        Use dark theme\n      </label>\n      <hr />\n      <ChatRoom\n        roomId={roomId}\n        theme={isDark ? 'dark' : 'light'}\n      />\n    </>\n  );\n}\n```\n\n```js src/chat.js\nexport function createConnection(serverUrl, roomId) {\n  // A real implementation would actually connect to the server\n  let connectedCallback;\n  let timeout;\n  return {\n    connect() {\n      timeout = setTimeout(() => {\n        if (connectedCallback) {\n          connectedCallback();\n        }\n      }, 100);\n    },\n    on(event, callback) {\n      if (connectedCallback) {\n        throw Error('Cannot add the handler twice.');\n      }\n      if (event !== 'connected') {\n        throw Error('Only \"connected\" event is supported.');\n      }\n      connectedCallback = callback;\n    },\n    disconnect() {\n      clearTimeout(timeout);\n    }\n  };\n}\n```\n\n```js src/notifications.js hidden\nimport Toastify from 'toastify-js';\nimport 'toastify-js/src/toastify.css';\n\nexport function showNotification(message, theme) {\n  Toastify({\n    text: message,\n    duration: 2000,\n    gravity: 'top',\n    position: 'right',\n    style: {\n      background: theme === 'dark' ? 'black' : 'white',\n      color: theme === 'dark' ? 'white' : 'black',\n    },\n  }).showToast();\n}\n```\n\n```css\nlabel { display: block; margin-top: 10px; }\n```\n\n</Sandpack>\n\nEffect 이벤트 내부의 코드는 반응형이 아니므로 `theme`를 변경해도 더 이상 Effect를 다시 연결하지 않습니다.\n\n<LearnMore path=\"/learn/separating-events-from-effects\">\n\n일부 값이 Effect를 다시 발생시키는 것을 막는 방법을 배우려면 <strong>[Effect에서 이벤트 분리하기](/learn/separating-events-from-effects)</strong>를 읽어보세요.\n\n</LearnMore>\n\n## Effect의 의존성 제거하기 {/*removing-effect-dependencies*/}\n\nEffect를 작성하면 린터는 Effect의 의존성 목록에 Effect가 읽는 모든 반응형 값(예를 들어 Props 및 State)을 포함했는지 확인합니다. 이렇게 하면 Effect가 컴포넌트의 최신 Props 및 State와 동기화 상태를 유지할 수 있습니다. 불필요한 의존성으로 인해 Effect가 너무 자주 실행되거나 무한 루프를 생성할 수도 있습니다. 이 가이드를 따라 Effect에서 불필요한 의존성을 검토하고 제거하세요.\n\n예를 들어 다음 Effect는 사용자가 Input을 편집할 때마다 다시 생성되는 `options` 객체에 의존합니다.\n\n<Sandpack>\n\n```js\nimport { useState, useEffect } from 'react';\nimport { createConnection } from './chat.js';\n\nconst serverUrl = 'https://localhost:1234';\n\nfunction ChatRoom({ roomId }) {\n  const [message, setMessage] = useState('');\n\n  const options = {\n    serverUrl: serverUrl,\n    roomId: roomId\n  };\n\n  useEffect(() => {\n    const connection = createConnection(options);\n    connection.connect();\n    return () => connection.disconnect();\n  }, [options]);\n\n  return (\n    <>\n      <h1>Welcome to the {roomId} room!</h1>\n      <input value={message} onChange={e => setMessage(e.target.value)} />\n    </>\n  );\n}\n\nexport default function App() {\n  const [roomId, setRoomId] = useState('general');\n  return (\n    <>\n      <label>\n        Choose the chat room:{' '}\n        <select\n          value={roomId}\n          onChange={e => setRoomId(e.target.value)}\n        >\n          <option value=\"general\">general</option>\n          <option value=\"travel\">travel</option>\n          <option value=\"music\">music</option>\n        </select>\n      </label>\n      <hr />\n      <ChatRoom roomId={roomId} />\n    </>\n  );\n}\n```\n\n```js src/chat.js\nexport function createConnection({ serverUrl, roomId }) {\n  // A real implementation would actually connect to the server\n  return {\n    connect() {\n      console.log('✅ Connecting to \"' + roomId + '\" room at ' + serverUrl + '...');\n    },\n    disconnect() {\n      console.log('❌ Disconnected from \"' + roomId + '\" room at ' + serverUrl);\n    }\n  };\n}\n```\n\n```css\ninput { display: block; margin-bottom: 20px; }\nbutton { margin-left: 10px; }\n```\n\n</Sandpack>\n\n해당 채팅에 메시지를 입력할 때마다 채팅이 다시 연결되는 것을 원치 않을 것입니다. 이 문제를 해결하려면 Effect 내에서 `options` 객체를 생성하여 Effect가 `roomId` 문자열에만 의존하도록 하세요.\n\n<Sandpack>\n\n```js\nimport { useState, useEffect } from 'react';\nimport { createConnection } from './chat.js';\n\nconst serverUrl = 'https://localhost:1234';\n\nfunction ChatRoom({ roomId }) {\n  const [message, setMessage] = useState('');\n\n  useEffect(() => {\n    const options = {\n      serverUrl: serverUrl,\n      roomId: roomId\n    };\n    const connection = createConnection(options);\n    connection.connect();\n    return () => connection.disconnect();\n  }, [roomId]);\n\n  return (\n    <>\n      <h1>Welcome to the {roomId} room!</h1>\n      <input value={message} onChange={e => setMessage(e.target.value)} />\n    </>\n  );\n}\n\nexport default function App() {\n  const [roomId, setRoomId] = useState('general');\n  return (\n    <>\n      <label>\n        Choose the chat room:{' '}\n        <select\n          value={roomId}\n          onChange={e => setRoomId(e.target.value)}\n        >\n          <option value=\"general\">general</option>\n          <option value=\"travel\">travel</option>\n          <option value=\"music\">music</option>\n        </select>\n      </label>\n      <hr />\n      <ChatRoom roomId={roomId} />\n    </>\n  );\n}\n```\n\n```js src/chat.js\nexport function createConnection({ serverUrl, roomId }) {\n  // A real implementation would actually connect to the server\n  return {\n    connect() {\n      console.log('✅ Connecting to \"' + roomId + '\" room at ' + serverUrl + '...');\n    },\n    disconnect() {\n      console.log('❌ Disconnected from \"' + roomId + '\" room at ' + serverUrl);\n    }\n  };\n}\n```\n\n```css\ninput { display: block; margin-bottom: 20px; }\nbutton { margin-left: 10px; }\n```\n\n</Sandpack>\n\n의존성 목록을 편집하여 `options` 의존성을 제거하지 않았음을 알 수 있습니다. 그것은 잘못된 방법일 것입니다. 대신 주변 코드를 변경함으로써 의존성을 *불필요*하게 만들었습니다. 의존성 목록을 Effect의 코드에서 사용하는 모든 반응형 값의 목록으로 생각하세요. 이 목록에 무엇을 넣을 것인지 의도적으로 선택하는 것이 아닙니다. 이 목록은 당신의 코드를 설명합니다. 의존성 목록을 변경하려면, 코드를 변경하세요.\n\n<LearnMore path=\"/learn/removing-effect-dependencies\">\n\nEffect 재실행을 줄이는 방법을 배우려면 <strong>[Effect의 의존성 제거하기](/learn/removing-effect-dependencies)</strong>를 읽어보세요.\n\n</LearnMore>\n\n## 커스텀 Hook으로 로직 재사용하기 {/*reusing-logic-with-custom-hooks*/}\n\nReact는 `useState`, `useContext`, 그리고 `useEffect`같은 Hook들이 내장되어 있습니다. 때로는 데이터를 가져오거나 사용자가 온라인 상태인지 여부를 추적하거나 대화방에 연결하는 등 조금 더 구체적인 목적을 가진 Hook이 존재하길 바랄 수도 있습니다. 이를 위해 애플리케이션의 필요에 따라 자신만의 Hook을 만들 수 있습니다.\n\n이번 예시에서는 `usePointerPosition` 커스텀 Hook은 커서 위치를 추적하는 반면 `useDelayedValue` 커스텀 Hook은 전달한 값보다 특정 밀리초만큼 \"지연\"된 값을 반환합니다. 샌드박스 미리보기 영역 위로 커서를 이동하면 커서를 따라 움직이는 점의 흔적을 확인할 수 있습니다.\n\n<Sandpack>\n\n```js\nimport { usePointerPosition } from './usePointerPosition.js';\nimport { useDelayedValue } from './useDelayedValue.js';\n\nexport default function Canvas() {\n  const pos1 = usePointerPosition();\n  const pos2 = useDelayedValue(pos1, 100);\n  const pos3 = useDelayedValue(pos2, 200);\n  const pos4 = useDelayedValue(pos3, 100);\n  const pos5 = useDelayedValue(pos4, 50);\n  return (\n    <>\n      <Dot position={pos1} opacity={1} />\n      <Dot position={pos2} opacity={0.8} />\n      <Dot position={pos3} opacity={0.6} />\n      <Dot position={pos4} opacity={0.4} />\n      <Dot position={pos5} opacity={0.2} />\n    </>\n  );\n}\n\nfunction Dot({ position, opacity }) {\n  return (\n    <div style={{\n      position: 'absolute',\n      backgroundColor: 'pink',\n      borderRadius: '50%',\n      opacity,\n      transform: `translate(${position.x}px, ${position.y}px)`,\n      pointerEvents: 'none',\n      left: -20,\n      top: -20,\n      width: 40,\n      height: 40,\n    }} />\n  );\n}\n```\n\n```js src/usePointerPosition.js\nimport { useState, useEffect } from 'react';\n\nexport function usePointerPosition() {\n  const [position, setPosition] = useState({ x: 0, y: 0 });\n  useEffect(() => {\n    function handleMove(e) {\n      setPosition({ x: e.clientX, y: e.clientY });\n    }\n    window.addEventListener('pointermove', handleMove);\n    return () => window.removeEventListener('pointermove', handleMove);\n  }, []);\n  return position;\n}\n```\n\n```js src/useDelayedValue.js\nimport { useState, useEffect } from 'react';\n\nexport function useDelayedValue(value, delay) {\n  const [delayedValue, setDelayedValue] = useState(value);\n\n  useEffect(() => {\n    setTimeout(() => {\n      setDelayedValue(value);\n    }, delay);\n  }, [value, delay]);\n\n  return delayedValue;\n}\n```\n\n```css\nbody { min-height: 300px; }\n```\n\n</Sandpack>\n\n커스텀 Hook을 생성하고, 함께 구성하고, 서로 데이터를 전달하고, 컴포넌트 사이에서 재사용할 수 있습니다. 앱이 성장함에 따라 이미 작성한 커스텀 Hook을 재사용할 수 있으므로 직접 작성하는 Effect의 수가 줄어들 것입니다. 또한 React 커뮤니티에서 관리하는 훌륭한 커스텀 Hook이 많습니다.\n\n<LearnMore path=\"/learn/reusing-logic-with-custom-hooks\">\n\n컴포넌트 간 로직을 공유하는 방법을 배우려면 <strong>[커스텀 Hook으로 로직 재사용하기](/learn/reusing-logic-with-custom-hooks)</strong>를 읽어보세요.\n\n</LearnMore>\n\n## 다음은 무엇인가요? {/*whats-next*/}\n\n이 장을 한 페이지씩 읽어보려면 [Ref로 값 참조하기](/learn/referencing-values-with-refs)로 이동하세요!\n"
  },
  {
    "path": "src/content/learn/extracting-state-logic-into-a-reducer.md",
    "content": "---\ntitle: State 로직을 Reducer로 작성하기\n---\n\n<Intro>\n\n한 컴포넌트에서 State 업데이트가 여러 이벤트 핸들러로 분산되는 경우가 있습니다. 이 경우 컴포넌트를 관리하기 어려워집니다. 따라서, 문제 해결을 위해 State를 업데이트하는 모든 로직을 *Reducer*를 사용해 컴포넌트 외부의 단일 함수로 통합해 관리할 수 있습니다.\n\n</Intro>\n\n<YouWillLearn>\n\n- Reducer 함수란 무엇인가\n- `useState`에서 `useReducer`로 리팩토링 하는 방법\n- Reducer를 언제 사용할 수 있는지\n- Reducer를 잘 작성하는 방법\n\n</YouWillLearn>\n\n## Reducer를 사용하여 State 로직 통합하기 {/*consolidate-state-logic-with-a-reducer*/}\n\n컴포넌트가 복잡해지면 컴포넌트의 State가 업데이트되는 다양한 경우를 한눈에 파악하기 어려워질 수 있습니다. 예를 들어, 아래의 `TaskApp` 컴포넌트는 State에 `tasks` 배열을 보유하고 있으며, 세 가지의 이벤트 핸들러를 사용하여 `task`를 추가, 제거 및 수정합니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { useState } from 'react';\nimport AddTask from './AddTask.js';\nimport TaskList from './TaskList.js';\n\nexport default function TaskApp() {\n  const [tasks, setTasks] = useState(initialTasks);\n\n  function handleAddTask(text) {\n    setTasks([...tasks, {\n      id: nextId++,\n      text: text,\n      done: false\n    }]);\n  }\n\n  function handleChangeTask(task) {\n    setTasks(tasks.map(t => {\n      if (t.id === task.id) {\n        return task;\n      } else {\n        return t;\n      }\n    }));\n  }\n\n  function handleDeleteTask(taskId) {\n    setTasks(\n      tasks.filter(t => t.id !== taskId)\n    );\n  }\n\n  return (\n    <>\n      <h1>Prague itinerary</h1>\n      <AddTask\n        onAddTask={handleAddTask}\n      />\n      <TaskList\n        tasks={tasks}\n        onChangeTask={handleChangeTask}\n        onDeleteTask={handleDeleteTask}\n      />\n    </>\n  );\n}\n\nlet nextId = 3;\nconst initialTasks = [\n  { id: 0, text: 'Visit Kafka Museum', done: true },\n  { id: 1, text: 'Watch a puppet show', done: false },\n  { id: 2, text: 'Lennon Wall pic', done: false },\n];\n```\n\n```js src/AddTask.js hidden\nimport { useState } from 'react';\n\nexport default function AddTask({ onAddTask }) {\n  const [text, setText] = useState('');\n  return (\n    <>\n      <input\n        placeholder=\"Add task\"\n        value={text}\n        onChange={e => setText(e.target.value)}\n      />\n      <button onClick={() => {\n        setText('');\n        onAddTask(text);\n      }}>Add</button>\n    </>\n  )\n}\n```\n\n```js src/TaskList.js hidden\nimport { useState } from 'react';\n\nexport default function TaskList({\n  tasks,\n  onChangeTask,\n  onDeleteTask\n}) {\n  return (\n    <ul>\n      {tasks.map(task => (\n        <li key={task.id}>\n          <Task\n            task={task}\n            onChange={onChangeTask}\n            onDelete={onDeleteTask}\n          />\n        </li>\n      ))}\n    </ul>\n  );\n}\n\nfunction Task({ task, onChange, onDelete }) {\n  const [isEditing, setIsEditing] = useState(false);\n  let taskContent;\n  if (isEditing) {\n    taskContent = (\n      <>\n        <input\n          value={task.text}\n          onChange={e => {\n            onChange({\n              ...task,\n              text: e.target.value\n            });\n          }} />\n        <button onClick={() => setIsEditing(false)}>\n          Save\n        </button>\n      </>\n    );\n  } else {\n    taskContent = (\n      <>\n        {task.text}\n        <button onClick={() => setIsEditing(true)}>\n          Edit\n        </button>\n      </>\n    );\n  }\n  return (\n    <label>\n      <input\n        type=\"checkbox\"\n        checked={task.done}\n        onChange={e => {\n          onChange({\n            ...task,\n            done: e.target.checked\n          });\n        }}\n      />\n      {taskContent}\n      <button onClick={() => onDelete(task.id)}>\n        Delete\n      </button>\n    </label>\n  );\n}\n```\n\n```css\nbutton { margin: 5px; }\nli { list-style-type: none; }\nul, li { margin: 0; padding: 0; }\n```\n\n</Sandpack>\n\n각 이벤트 핸들러는 State를 업데이트하기 위해 `setTasks`를 호출합니다. 컴포넌트가 커질수록 그 안에서 State를 다루는 로직의 양도 늘어나게 됩니다. 복잡성은 줄이고 접근성을 높이기 위해서, 컴포넌트 내부에 있는 State 로직을 컴포넌트 외부의 **\"Reducer\"라고 하는** 단일 함수로 옮길 수 있습니다.\n\nReducer는 State를 다루는 다른 방법입니다. 다음과 같은 세가지 단계에 걸쳐 `useState`에서 `useReducer`로 바꿀 수 있습니다.\n\n1. State를 설정하는 방식에서 Action을 Dispatch하는 방식으로 **전환하기**.\n2. Reducer 함수 **작성하기**.\n3. 컴포넌트에서 Reducer **사용하기**.\n\n### 1단계: State를 설정하는 방식에서 Action을 Dispatch하는 방식으로 전환하기 {/*step-1-move-from-setting-state-to-dispatching-actions*/}\n\n현재 이벤트 핸들러는 State를 설정함으로써 *무엇을 할 것인지*를 명시합니다.\n\n```js\nfunction handleAddTask(text) {\n  setTasks([...tasks, {\n    id: nextId++,\n    text: text,\n    done: false\n  }]);\n}\n\nfunction handleChangeTask(task) {\n  setTasks(tasks.map(t => {\n    if (t.id === task.id) {\n      return task;\n    } else {\n      return t;\n    }\n  }));\n}\n\nfunction handleDeleteTask(taskId) {\n  setTasks(\n    tasks.filter(t => t.id !== taskId)\n  );\n}\n```\n\n위 코드에서 State 설정 관련 로직을 전부 지워보세요. 다음과 같이 세가지 이벤트 핸들러가 남습니다.\n\n- 사용자가 \"Add\"를 눌렀을 때 호출되는 `handleAddTask(text)`.\n- 사용자가 `task`를 토글하거나 \"Save\"를 누르면 호출되는 `handleChangeTask(task)`.\n- 사용자가 \"Delete\"를 누르면 호출되는 `handleDeleteTask(taskId)`.\n\nReducer를 사용한 State 관리는 State를 직접 설정하는 것과 약간 다릅니다. State를 설정하여 React에게 \"무엇을 할 지\"를 지시하는 대신, 이벤트 핸들러에서 \"Action\"을 전달하여 \"사용자가 방금 한 일\"을 지정합니다. (State 업데이트 로직은 다른 곳에 있습니다!) 즉, 이벤트 핸들러를 통해 \"`tasks`를 설정\"하는 대신 \"`task`를 추가/변경/삭제\"하는 Action을 전달하는 것입니다. 이러한 방식이 사용자의 의도를 더 명확하게 설명합니다.\n\n```js\nfunction handleAddTask(text) {\n  dispatch({\n    type: 'added',\n    id: nextId++,\n    text: text,\n  });\n}\n\nfunction handleChangeTask(task) {\n  dispatch({\n    type: 'changed',\n    task: task\n  });\n}\n\nfunction handleDeleteTask(taskId) {\n  dispatch({\n    type: 'deleted',\n    id: taskId\n  });\n}\n```\n\n`dispatch` 함수에 넣어준 객체를 \"Action\" 이라고 합니다.\n\n```js {3-7}\nfunction handleDeleteTask(taskId) {\n  dispatch(\n    // \"Action\" 객체:\n    {\n      type: 'deleted',\n      id: taskId\n    }\n  );\n}\n```\n\n이 객체는 일반적인 자바스크립트 객체입니다. 이 안에 어떤 것이든 자유롭게 넣을 수 있지만, 일반적으로 *어떤 상황이 발생하는지*에 대한 최소한의 정보를 담고 있어야합니다. (`dispatch` 함수 자체에 대한 부분은 이후 단계에서 다룰 예정입니다.)\n\n<Note>\n\nAction 객체는 어떤 형태든 될 수 있습니다. 그렇지만 발생한 일을 설명하는 문자열 `type` 을 넘겨주고 이외의 정보는 다른 필드에 담아서 전달하도록 작성하는 게 일반적입니다. `type`은 컴포넌트에 따라 값이 다르며 이 예시의 경우 `'added'` 또는 `'added_task'` 둘 다 괜찮습니다. 무슨 일이 일어나는지를 설명할 수 있는 이름을 넣어주면 됩니다.\n\n```js\ndispatch({\n  // 컴포넌트마다 다른 값\n  type: 'what_happened',\n  // 다른 필드는 이곳에\n});\n```\n\n</Note>\n\n### 2단계: Reducer 함수 작성하기 {/*step-2-write-a-reducer-function*/}\n\nReducer 함수는 State에 대한 로직을 넣는 곳입니다. 이 함수는 현재의 State 값과 Action 객체, 이렇게 두 개의 인자를 받고 다음 State 값을 반환합니다.\n\n```js\nfunction yourReducer(state, action) {\n  // React가 설정하게될 다음 State 값을 반환합니다.\n}\n```\n\nReact는 Reducer에서 반환한 값을 State에 설정합니다.\n\n이 예시에서 이벤트 핸들러에 구현 되어있는 State 설정과 관련 로직을 Reducer 함수로 옮기기 위해서 다음과 같이 해보겠습니다.\n\n1. 첫 번째 인자에 현재 State (`tasks`) 선언하기.\n2. 두 번째 인자에 `action` 객체 선언하기.\n3. Reducer에서 *다음* State 반환하기 (React가 State에 설정하게 될 값).\n\n다음은 State 설정과 관련 모든 로직을 Reducer 함수로 마이그레이션한 코드입니다.\n\n```js\nfunction tasksReducer(tasks, action) {\n  if (action.type === 'added') {\n    return [...tasks, {\n      id: action.id,\n      text: action.text,\n      done: false\n    }];\n  } else if (action.type === 'changed') {\n    return tasks.map(t => {\n      if (t.id === action.task.id) {\n        return action.task;\n      } else {\n        return t;\n      }\n    });\n  } else if (action.type === 'deleted') {\n    return tasks.filter(t => t.id !== action.id);\n  } else {\n    throw Error('Unknown action: ' + action.type);\n  }\n}\n```\n\nReducer 함수는 State(`tasks`)를 인자로 받고 있기 때문에, 이를 **컴포넌트 외부에서 선언**할 수 있습니다. 이렇게 하면 들여쓰기 수준이 줄어들고 코드를 더 쉽게 읽을 수 있습니다.\n\n<Note>\n\n위 코드에서 `if`/`else` 문을 사용하고 있지만 Reducer 함수 안에서는 [`switch` 문](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Statements/switch)을 사용하는 게 규칙입니다. 물론 결과는 같지만, `switch` 문으로 작성하는 것이 한눈에 읽기 더 쉬울 수 있습니다. 이제부터 이 문서에서 다룰 예시는 아래처럼 `switch` 문을 사용합니다.\n\n```js\nfunction tasksReducer(tasks, action) {\n  switch (action.type) {\n    case 'added': {\n      return [...tasks, {\n        id: action.id,\n        text: action.text,\n        done: false\n      }];\n    }\n    case 'changed': {\n      return tasks.map(t => {\n        if (t.id === action.task.id) {\n          return action.task;\n        } else {\n          return t;\n        }\n      });\n    }\n    case 'deleted': {\n      return tasks.filter(t => t.id !== action.id);\n    }\n    default: {\n      throw Error('Unknown action: ' + action.type);\n    }\n  }\n}\n```\n\n각자 다른 `case` 속에서 선언된 변수들이 서로 충돌하지 않도록 `case` 블록을 중괄호인 `{`와 `}`로 감싸는 걸 추천합니다. 또 `case`는 일반적인 경우라면 `return`으로 끝나야합니다. `return` 하는 것을 잊으면 코드가 다음 `case`로 \"떨어져\" 실수할 수 있습니다!\n\n아직 `switch` 문에 익숙하지 않다면, `if`/`else` 문을 사용해도 괜찮습니다.\n\n</Note>\n\n<DeepDive>\n\n#### 왜 Reducer라고 부르게 되었을까요? {/*why-are-reducers-called-this-way*/}\n\nReducer를 사용하면 컴포넌트 내부의 코드 양을 \"줄일 수\" 있지만, 실제로는 배열에서 사용하는 [`reduce()`](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce) 연산의 이름에서 따 명명되었습니다.\n\n`reduce()`는 배열의 여러 값을 단일 값으로 \"누적\"하는 연산을 수행합니다.\n\n```js\nconst arr = [1, 2, 3, 4, 5];\nconst sum = arr.reduce(\n  (result, number) => result + number\n); // 1 + 2 + 3 + 4 + 5\n```\n\n`reduce`로 전달하는 함수는 \"Reducer\"로 알려져 있습니다. 이 함수는 지금까지의 결과(`result`)와 현재 아이템(`number`)을 인자로 받고 다음 결과를 반환합니다. 비슷한 아이디어의 예로 React의 Reducer는 지금까지의 State와 Action을 인자로 받고 다음 State를 반환합니다. 이 과정에서 여러 Action을 누적하여 State로 반환합니다.\n\n`initialState`와 Reducer 함수를 넘겨 받아 최종적인 State 값으로 계산하기 위한 `action` 배열을 인자로 받는 `reduce()` 메서드를 사용할 수도 있습니다.\n\n<Sandpack>\n\n```js src/index.js active\nimport tasksReducer from './tasksReducer.js';\n\nlet initialState = [];\nlet actions = [\n  { type: 'added', id: 1, text: 'Visit Kafka Museum' },\n  { type: 'added', id: 2, text: 'Watch a puppet show' },\n  { type: 'deleted', id: 1 },\n  { type: 'added', id: 3, text: 'Lennon Wall pic' },\n];\n\nlet finalState = actions.reduce(\n  tasksReducer,\n  initialState\n);\n\nconst output = document.getElementById('output');\noutput.textContent = JSON.stringify(\n  finalState,\n  null,\n  2\n);\n```\n\n```js src/tasksReducer.js\nexport default function tasksReducer(tasks, action) {\n  switch (action.type) {\n    case 'added': {\n      return [...tasks, {\n        id: action.id,\n        text: action.text,\n        done: false\n      }];\n    }\n    case 'changed': {\n      return tasks.map(t => {\n        if (t.id === action.task.id) {\n          return action.task;\n        } else {\n          return t;\n        }\n      });\n    }\n    case 'deleted': {\n      return tasks.filter(t => t.id !== action.id);\n    }\n    default: {\n      throw Error('Unknown action: ' + action.type);\n    }\n  }\n}\n```\n\n```html public/index.html\n<pre id=\"output\"></pre>\n```\n\n</Sandpack>\n\n여러분이 직접 구현 할 일은 거의 없지만 위 예시는 React에 구현되어 있는 것과 비슷합니다!\n\n</DeepDive>\n\n### 3단계: 컴포넌트에서 Reducer 사용하기 {/*step-3-use-the-reducer-from-your-component*/}\n\n마지막으로 컴포넌트에 `tasksReducer`를 연결할 차례입니다. React에서 `useReducer` Hook을 불러와주세요.\n\n```js\nimport { useReducer } from 'react';\n```\n\n그런 다음, `useState`를\n\n```js\nconst [tasks, setTasks] = useState(initialTasks);\n```\n\n아래 처럼 `useReducer`로 바꿔주세요.\n\n```js\nconst [tasks, dispatch] = useReducer(tasksReducer, initialTasks);\n```\n\n`useReducer` Hook은 초기 State 값을 입력받아 유상태<sup>Stateful</sup> 값을 반환한다는 점과 State를 설정하는 함수(`useReducer`의 경우는 Dispatch 함수를 의미)의 원리를 보면 `useState`와 비슷합니다. 하지만 조금 다른 점이 있습니다.\n\n`useReducer` Hook은 두 개의 인자를 넘겨받습니다.\n\n1. Reducer 함수\n2. 초기 State 값\n\n그리고 아래와 같이 반환합니다.\n\n1. State를 담을 수 있는 값\n2. Dispatch 함수 (사용자의 Action을 Reducer 함수에게 \"전달하게 될\")\n\n이제 준비가 다 되었습니다! 아래 예시의 컴포넌트 파일 아래에는 Reducer가 선언되어 있습니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { useReducer } from 'react';\nimport AddTask from './AddTask.js';\nimport TaskList from './TaskList.js';\n\nexport default function TaskApp() {\n  const [tasks, dispatch] = useReducer(\n    tasksReducer,\n    initialTasks\n  );\n\n  function handleAddTask(text) {\n    dispatch({\n      type: 'added',\n      id: nextId++,\n      text: text,\n    });\n  }\n\n  function handleChangeTask(task) {\n    dispatch({\n      type: 'changed',\n      task: task\n    });\n  }\n\n  function handleDeleteTask(taskId) {\n    dispatch({\n      type: 'deleted',\n      id: taskId\n    });\n  }\n\n  return (\n    <>\n      <h1>Prague itinerary</h1>\n      <AddTask\n        onAddTask={handleAddTask}\n      />\n      <TaskList\n        tasks={tasks}\n        onChangeTask={handleChangeTask}\n        onDeleteTask={handleDeleteTask}\n      />\n    </>\n  );\n}\n\nfunction tasksReducer(tasks, action) {\n  switch (action.type) {\n    case 'added': {\n      return [...tasks, {\n        id: action.id,\n        text: action.text,\n        done: false\n      }];\n    }\n    case 'changed': {\n      return tasks.map(t => {\n        if (t.id === action.task.id) {\n          return action.task;\n        } else {\n          return t;\n        }\n      });\n    }\n    case 'deleted': {\n      return tasks.filter(t => t.id !== action.id);\n    }\n    default: {\n      throw Error('Unknown action: ' + action.type);\n    }\n  }\n}\n\nlet nextId = 3;\nconst initialTasks = [\n  { id: 0, text: 'Visit Kafka Museum', done: true },\n  { id: 1, text: 'Watch a puppet show', done: false },\n  { id: 2, text: 'Lennon Wall pic', done: false }\n];\n```\n\n```js src/AddTask.js hidden\nimport { useState } from 'react';\n\nexport default function AddTask({ onAddTask }) {\n  const [text, setText] = useState('');\n  return (\n    <>\n      <input\n        placeholder=\"Add task\"\n        value={text}\n        onChange={e => setText(e.target.value)}\n      />\n      <button onClick={() => {\n        setText('');\n        onAddTask(text);\n      }}>Add</button>\n    </>\n  )\n}\n```\n\n```js src/TaskList.js hidden\nimport { useState } from 'react';\n\nexport default function TaskList({\n  tasks,\n  onChangeTask,\n  onDeleteTask\n}) {\n  return (\n    <ul>\n      {tasks.map(task => (\n        <li key={task.id}>\n          <Task\n            task={task}\n            onChange={onChangeTask}\n            onDelete={onDeleteTask}\n          />\n        </li>\n      ))}\n    </ul>\n  );\n}\n\nfunction Task({ task, onChange, onDelete }) {\n  const [isEditing, setIsEditing] = useState(false);\n  let taskContent;\n  if (isEditing) {\n    taskContent = (\n      <>\n        <input\n          value={task.text}\n          onChange={e => {\n            onChange({\n              ...task,\n              text: e.target.value\n            });\n          }} />\n        <button onClick={() => setIsEditing(false)}>\n          Save\n        </button>\n      </>\n    );\n  } else {\n    taskContent = (\n      <>\n        {task.text}\n        <button onClick={() => setIsEditing(true)}>\n          Edit\n        </button>\n      </>\n    );\n  }\n  return (\n    <label>\n      <input\n        type=\"checkbox\"\n        checked={task.done}\n        onChange={e => {\n          onChange({\n            ...task,\n            done: e.target.checked\n          });\n        }}\n      />\n      {taskContent}\n      <button onClick={() => onDelete(task.id)}>\n        Delete\n      </button>\n    </label>\n  );\n}\n```\n\n```css\nbutton { margin: 5px; }\nli { list-style-type: none; }\nul, li { margin: 0; padding: 0; }\n```\n\n</Sandpack>\n\n아래처럼 Reducer를 다른 파일로 분리하는 것도 가능합니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { useReducer } from 'react';\nimport AddTask from './AddTask.js';\nimport TaskList from './TaskList.js';\nimport tasksReducer from './tasksReducer.js';\n\nexport default function TaskApp() {\n  const [tasks, dispatch] = useReducer(\n    tasksReducer,\n    initialTasks\n  );\n\n  function handleAddTask(text) {\n    dispatch({\n      type: 'added',\n      id: nextId++,\n      text: text,\n    });\n  }\n\n  function handleChangeTask(task) {\n    dispatch({\n      type: 'changed',\n      task: task\n    });\n  }\n\n  function handleDeleteTask(taskId) {\n    dispatch({\n      type: 'deleted',\n      id: taskId\n    });\n  }\n\n  return (\n    <>\n      <h1>Prague itinerary</h1>\n      <AddTask\n        onAddTask={handleAddTask}\n      />\n      <TaskList\n        tasks={tasks}\n        onChangeTask={handleChangeTask}\n        onDeleteTask={handleDeleteTask}\n      />\n    </>\n  );\n}\n\nlet nextId = 3;\nconst initialTasks = [\n  { id: 0, text: 'Visit Kafka Museum', done: true },\n  { id: 1, text: 'Watch a puppet show', done: false },\n  { id: 2, text: 'Lennon Wall pic', done: false },\n];\n```\n\n```js src/tasksReducer.js\nexport default function tasksReducer(tasks, action) {\n  switch (action.type) {\n    case 'added': {\n      return [...tasks, {\n        id: action.id,\n        text: action.text,\n        done: false\n      }];\n    }\n    case 'changed': {\n      return tasks.map(t => {\n        if (t.id === action.task.id) {\n          return action.task;\n        } else {\n          return t;\n        }\n      });\n    }\n    case 'deleted': {\n      return tasks.filter(t => t.id !== action.id);\n    }\n    default: {\n      throw Error('Unknown action: ' + action.type);\n    }\n  }\n}\n```\n\n```js src/AddTask.js hidden\nimport { useState } from 'react';\n\nexport default function AddTask({ onAddTask }) {\n  const [text, setText] = useState('');\n  return (\n    <>\n      <input\n        placeholder=\"Add task\"\n        value={text}\n        onChange={e => setText(e.target.value)}\n      />\n      <button onClick={() => {\n        setText('');\n        onAddTask(text);\n      }}>Add</button>\n    </>\n  )\n}\n```\n\n```js src/TaskList.js hidden\nimport { useState } from 'react';\n\nexport default function TaskList({\n  tasks,\n  onChangeTask,\n  onDeleteTask\n}) {\n  return (\n    <ul>\n      {tasks.map(task => (\n        <li key={task.id}>\n          <Task\n            task={task}\n            onChange={onChangeTask}\n            onDelete={onDeleteTask}\n          />\n        </li>\n      ))}\n    </ul>\n  );\n}\n\nfunction Task({ task, onChange, onDelete }) {\n  const [isEditing, setIsEditing] = useState(false);\n  let taskContent;\n  if (isEditing) {\n    taskContent = (\n      <>\n        <input\n          value={task.text}\n          onChange={e => {\n            onChange({\n              ...task,\n              text: e.target.value\n            });\n          }} />\n        <button onClick={() => setIsEditing(false)}>\n          Save\n        </button>\n      </>\n    );\n  } else {\n    taskContent = (\n      <>\n        {task.text}\n        <button onClick={() => setIsEditing(true)}>\n          Edit\n        </button>\n      </>\n    );\n  }\n  return (\n    <label>\n      <input\n        type=\"checkbox\"\n        checked={task.done}\n        onChange={e => {\n          onChange({\n            ...task,\n            done: e.target.checked\n          });\n        }}\n      />\n      {taskContent}\n      <button onClick={() => onDelete(task.id)}>\n        Delete\n      </button>\n    </label>\n  );\n}\n```\n\n```css\nbutton { margin: 5px; }\nli { list-style-type: none; }\nul, li { margin: 0; padding: 0; }\n```\n\n</Sandpack>\n\n관심사를 분리하면 컴포넌트의 로직은 읽기 더 쉬워질 수 있습니다. 이렇게 하면 이벤트 핸들러는 Action을 전달해줘서 *무슨 일이 일어났는지*에 관련한 것만 명시하면 되고 Reducer 함수는 이에 대한 응답으로 *State가 어떤 값으로 업데이트 될지*를 결정하기만 하면 됩니다.\n\n## `useState`와 `useReducer` 비교하기 {/*comparing-usestate-and-usereducer*/}\n\nReducer가 좋은 점만 있는 것은 아닙니다! 아래에서 `useState`와 `useReducer`를 비교할 수 있는 몇 가지 방법을 소개하겠습니다.\n\n- **코드 크기:** 일반적으로 `useState`를 사용하면, 미리 작성해야 하는 코드가 줄어듭니다. `useReducer`를 사용하면 Reducer 함수 *그리고* Action을 전달하는 부분 둘 다 작성해야 합니다. 하지만 여러 이벤트 핸들러에서 비슷한 방식으로 State를 업데이트하는 경우, `useReducer`를 사용하면 코드의 양을 줄이는 데 도움이 될 수 있습니다.\n\n- **가독성:** `useState`로 간단한 State를 업데이트하는 경우 가독성이 좋은 편입니다. 그렇지만 더 복잡한 구조의 State를 다루게 되면 컴포넌트의 코드 양이 더 많아져 한눈에 읽기 어려워질 수 있습니다. 이 경우 `useReducer`를 사용하면 업데이트 로직이 *어떻게 동작하는지*와 이벤트 핸들러를 통해서 *무엇이 발생했는지* 구현한 부분을 명확하게 구분할 수 있습니다.\n\n- **디버깅:** `useState`를 사용하며 버그를 발견했을 때, *왜*, *어디서* State가 잘못 설정됐는지 찾기 어려울 수 있습니다. `useReducer`를 사용하면, 콘솔 로그를 Reducer에 추가하여 State가 업데이트되는 모든 부분과 *왜* 해당 버그가 발생했는지(어떤 `Action`으로 인한 것인지)를 확인할 수 있습니다. 각 `Action`이 올바르게 작성되어 있다면, 버그를 발생시킨 부분이 Reducer 로직 자체에 있다는 것을 알 수 있을 것입니다. 그렇지만 `useState`를 사용하는 경우보다 더 많은 코드를 단계별로 실행해서 디버깅 해야 하는 점이 있기도 합니다.\n\n- **테스팅:** Reducer는 컴포넌트에 의존하지 않는 순수 함수입니다. 이는 Reducer를 독립적으로 분리해서 내보내거나 테스트할 수 있다는 것을 의미합니다. 일반적으로 더 현실적인 환경에서 컴포넌트를 테스트하는 것이 좋지만, 복잡한 State를 업데이트하는 로직의 경우 Reducer가 특정 초기 State 및 Action에 대해 특정 State를 반환한다고 생각하고 테스트하는 것이 유용할 수 있습니다.\n\n- **개인적인 취향:** Reducer를 좋아하는 사람도 있지만, 그렇지 않는 사람들도 있습니다. 괜찮습니다. 이건 선호도의 문제이니까요. `useState`와 `useReducer`는 동일한 방식이기 때문에 언제나 마음대로 바꿔서 사용해도 무방합니다.\n\n만약 일부 컴포넌트에서 잘못된 방식으로 State를 업데이트하는 것으로 인한 버그가 자주 발생하거나 해당 코드에 더 많은 구조를 도입하고 싶다면 Reducer 사용을 권장합니다. 이때 모든 부분에 Reducer를 적용하지 않아도 됩니다. `useState`와 `useReducer`를 마음대로 섞고 매치하세요! 이 둘은 심지어 같은 컴포넌트 안에서도 사용할 수 있습니다.\n\n## Reducer 잘 작성하기 {/*writing-reducers-well*/}\n\nReducer를 작성할 때, 다음과 같은 두 가지 팁을 명심하세요.\n\n- **Reducer는 반드시 순수해야 합니다.** [State 업데이트 함수](/learn/queueing-a-series-of-state-updates)와 비슷하게, Reducer는 렌더링 중에 실행됩니다! (Action은 다음 렌더링까지 대기합니다.) 이것은 Reducer는 [반드시 순수](/learn/keeping-components-pure)해야한다는 걸 의미합니다. 즉, 입력 값이 같다면 결과 값도 항상 같아야 합니다. 요청을 보내거나 timeout을 스케쥴링하거나 사이드 이펙트(컴포넌트 외부에 영향을 미치는 작업)를 수행해서는 안 됩니다. Reducer는 [객체](/learn/updating-objects-in-state)와 [배열](/learn/updating-arrays-in-state)을 변경하지 않고 업데이트해야 합니다.\n\n- **각 Action은 데이터 안에서 여러 변경들이 있더라도 하나의 사용자 상호작용을 설명해야 합니다.** 예를 들어, 사용자가 Reducer가 관리하는 5개의 필드가 있는 양식에서 '재설정'을 누른 경우, 5개의 개별 `set_field` Action보다는 하나의 `reset_form` Action을 전송하는 것이 더 합리적입니다. 모든 Action을 Reducer에 기록하면 어떤 상호작용이나 응답이 어떤 순서로 일어났는지 재구성할 수 있을 만큼 로그가 명확해야 합니다. 이는 디버깅에 도움이 됩니다!\n\n## Immer로 간결한 Reducer 작성하기 {/*writing-concise-reducers-with-immer*/}\n\n일반적인 State에서 [객체](/learn/updating-objects-in-state#write-concise-update-logic-with-immer)와 [배열](/learn/updating-arrays-in-state#write-concise-update-logic-with-immer)을 업데이트 하는 것처럼, Immer 라이브러리를 사용하면 Reducer를 더 간결하게 작성할 수 있습니다. 이 라이브러리에서 제공하는 [`useImmerReducer`](https://github.com/immerjs/use-immer#useimmerreducer)를 사용하여 `push` 또는 `arr[i] =` 로 값을 할당하므로써 State를 변경해보겠습니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { useImmerReducer } from 'use-immer';\nimport AddTask from './AddTask.js';\nimport TaskList from './TaskList.js';\n\nfunction tasksReducer(draft, action) {\n  switch (action.type) {\n    case 'added': {\n      draft.push({\n        id: action.id,\n        text: action.text,\n        done: false\n      });\n      break;\n    }\n    case 'changed': {\n      const index = draft.findIndex(t =>\n        t.id === action.task.id\n      );\n      draft[index] = action.task;\n      break;\n    }\n    case 'deleted': {\n      return draft.filter(t => t.id !== action.id);\n    }\n    default: {\n      throw Error('Unknown action: ' + action.type);\n    }\n  }\n}\n\nexport default function TaskApp() {\n  const [tasks, dispatch] = useImmerReducer(\n    tasksReducer,\n    initialTasks\n  );\n\n  function handleAddTask(text) {\n    dispatch({\n      type: 'added',\n      id: nextId++,\n      text: text,\n    });\n  }\n\n  function handleChangeTask(task) {\n    dispatch({\n      type: 'changed',\n      task: task\n    });\n  }\n\n  function handleDeleteTask(taskId) {\n    dispatch({\n      type: 'deleted',\n      id: taskId\n    });\n  }\n\n  return (\n    <>\n      <h1>Prague itinerary</h1>\n      <AddTask\n        onAddTask={handleAddTask}\n      />\n      <TaskList\n        tasks={tasks}\n        onChangeTask={handleChangeTask}\n        onDeleteTask={handleDeleteTask}\n      />\n    </>\n  );\n}\n\nlet nextId = 3;\nconst initialTasks = [\n  { id: 0, text: 'Visit Kafka Museum', done: true },\n  { id: 1, text: 'Watch a puppet show', done: false },\n  { id: 2, text: 'Lennon Wall pic', done: false },\n];\n```\n\n```js src/AddTask.js hidden\nimport { useState } from 'react';\n\nexport default function AddTask({ onAddTask }) {\n  const [text, setText] = useState('');\n  return (\n    <>\n      <input\n        placeholder=\"Add task\"\n        value={text}\n        onChange={e => setText(e.target.value)}\n      />\n      <button onClick={() => {\n        setText('');\n        onAddTask(text);\n      }}>Add</button>\n    </>\n  )\n}\n```\n\n```js src/TaskList.js hidden\nimport { useState } from 'react';\n\nexport default function TaskList({\n  tasks,\n  onChangeTask,\n  onDeleteTask\n}) {\n  return (\n    <ul>\n      {tasks.map(task => (\n        <li key={task.id}>\n          <Task\n            task={task}\n            onChange={onChangeTask}\n            onDelete={onDeleteTask}\n          />\n        </li>\n      ))}\n    </ul>\n  );\n}\n\nfunction Task({ task, onChange, onDelete }) {\n  const [isEditing, setIsEditing] = useState(false);\n  let taskContent;\n  if (isEditing) {\n    taskContent = (\n      <>\n        <input\n          value={task.text}\n          onChange={e => {\n            onChange({\n              ...task,\n              text: e.target.value\n            });\n          }} />\n        <button onClick={() => setIsEditing(false)}>\n          Save\n        </button>\n      </>\n    );\n  } else {\n    taskContent = (\n      <>\n        {task.text}\n        <button onClick={() => setIsEditing(true)}>\n          Edit\n        </button>\n      </>\n    );\n  }\n  return (\n    <label>\n      <input\n        type=\"checkbox\"\n        checked={task.done}\n        onChange={e => {\n          onChange({\n            ...task,\n            done: e.target.checked\n          });\n        }}\n      />\n      {taskContent}\n      <button onClick={() => onDelete(task.id)}>\n        Delete\n      </button>\n    </label>\n  );\n}\n```\n\n```css\nbutton { margin: 5px; }\nli { list-style-type: none; }\nul, li { margin: 0; padding: 0; }\n```\n\n```json package.json\n{\n  \"dependencies\": {\n    \"immer\": \"1.7.3\",\n    \"react\": \"latest\",\n    \"react-dom\": \"latest\",\n    \"react-scripts\": \"latest\",\n    \"use-immer\": \"0.5.1\"\n  },\n  \"scripts\": {\n    \"start\": \"react-scripts start\",\n    \"build\": \"react-scripts build\",\n    \"test\": \"react-scripts test --env=jsdom\",\n    \"eject\": \"react-scripts eject\"\n  }\n}\n```\n\n</Sandpack>\n\nReducer는 순수해야 하기 때문에, 이 안에서는 State를 변경할 수 없습니다. 그러나, Immer에서 제공하는 특별한 `draft` 객체를 사용하면 안전하게 State를 변경할 수 있습니다. 내부적으로, Immer는 변경 사항이 반영된 `draft`로 State의 복사본을 생성합니다. 이것이 `useImmerReducer`가 관리하는 Reducer가 첫 번째 인수인 State를 변형할 수 있고 새로운 State 값을 반환할 필요가 없는 이유입니다.\n\n## 요약 {/*요약*/}\n\n- `useState`에서 `useReducer`로 변환하려면\n  1. 이벤트 핸들러에서 Action을 전달합니다.\n  2. 주어진 State와 Action에 대해 다음 State를 반환하는 Reducer 함수를 작성합니다.\n  3. `useState`를 `useReducer`로 바꿉니다.\n- Reducer를 사용하면 코드를 조금 더 작성해야 하지만 디버깅과 테스트에 도움이 됩니다.\n- Reducer는 반드시 순수해야 합니다.\n- 각 Action은 단일 사용자 상호작용을 설명해야 합니다.\n- 객체와 배열을 변경하는 스타일로 Reducer를 작성하려면 Immer 라이브러리를 사용하세요.\n\n<Challenges>\n\n#### 이벤트 핸들러에서 Action 전달하기 {/*dispatch-actions-from-event-handlers*/}\n\n현재 `ContactList.js`와 `Chat.js`의 이벤트 핸들러 안에는 `// TODO` 주석이 있습니다. 이 때문에 input에 값을 입력해도 동작하지 않고 탭 버튼을 클릭해도 선택된 수신인을 변경할 수 없습니다.\n\n`// TODO` 주석이 있는 부분을 지우고 상황에 맞는 Action을 `dispatch`하는 코드를 작성해보세요. Action에 대한 힌트를 얻고 싶다면 `messengerReducer.js`에 구현된 reducer를 확인해보세요. 이 reducer는 이미 작성되어있기 때문에 변경할 필요가 없습니다. 여러분은 `ContactList.js`와 `Chat.js`에 Action을 담아 전달하는 코드를 작성하기만 하면 됩니다.\n\n<Hint>\n\n`dispatch` 함수는 컴포넌트의 prop으로 전달되기 때문에 이미 두 컴포넌트 모두에서 사용할 수 있습니다. 따라서 알맞은 action 객체를 담아 `dispatch`를 호출하면 됩니다.\n\naction 객체를 어떻게 작성해야하는지 확인하고 싶다면, reducer를 보고 어떤 `action` 필드가 들어갈지 유추할 수 있습니다. reducer에 정의된 `changed_selection`의 경우를 예를 들어 보겠습니다.\n\n```js\ncase 'changed_selection': {\n  return {\n    ...state,\n    selectedId: action.contactId\n  };\n}\n```\n\naction 객체가 `type: 'changed_selection'`을 갖고 있어야 한다는 것을 의미합니다. 또, `action.contactId`가 사용되고 있는 것으로 보아, action 객체에 프로퍼티로 `contactId`를 포함시켜야 한다는 것을 알 수 있습니다.\n\n</Hint>\n\n<Sandpack>\n\n```js src/App.js\nimport { useReducer } from 'react';\nimport Chat from './Chat.js';\nimport ContactList from './ContactList.js';\nimport {\n  initialState,\n  messengerReducer\n} from './messengerReducer';\n\nexport default function Messenger() {\n  const [state, dispatch] = useReducer(\n    messengerReducer,\n    initialState\n  );\n  const message = state.message;\n  const contact = contacts.find(c =>\n    c.id === state.selectedId\n  );\n  return (\n    <div>\n      <ContactList\n        contacts={contacts}\n        selectedId={state.selectedId}\n        dispatch={dispatch}\n      />\n      <Chat\n        key={contact.id}\n        message={message}\n        contact={contact}\n        dispatch={dispatch}\n      />\n    </div>\n  );\n}\n\nconst contacts = [\n  { id: 0, name: 'Taylor', email: 'taylor@mail.com' },\n  { id: 1, name: 'Alice', email: 'alice@mail.com' },\n  { id: 2, name: 'Bob', email: 'bob@mail.com' }\n];\n```\n\n```js src/messengerReducer.js\nexport const initialState = {\n  selectedId: 0,\n  message: 'Hello'\n};\n\nexport function messengerReducer(\n  state,\n  action\n) {\n  switch (action.type) {\n    case 'changed_selection': {\n      return {\n        ...state,\n        selectedId: action.contactId,\n        message: ''\n      };\n    }\n    case 'edited_message': {\n      return {\n        ...state,\n        message: action.message\n      };\n    }\n    default: {\n      throw Error('Unknown action: ' + action.type);\n    }\n  }\n}\n```\n\n```js src/ContactList.js\nexport default function ContactList({contacts, selectedId, dispatch}) {\n  return (\n    <section className=\"contact-list\">\n      <ul>\n        {contacts.map(contact =>\n          <li key={contact.id}>\n            <button onClick={() => {\n              // TODO: dispatch changed_selection\n            }}>\n              {selectedId === contact.id ? (\n                <b>{contact.name}</b>\n              ) : (\n                contact.name\n              )}\n            </button>\n          </li>\n        )}\n      </ul>\n    </section>\n  );\n}\n```\n\n```js src/Chat.js\nimport { useState } from 'react';\n\nexport default function Chat({\n  contact,\n  message,\n  dispatch\n}) {\n  return (\n    <section className=\"chat\">\n      <textarea\n        value={message}\n        placeholder={'Chat to ' + contact.name}\n        onChange={e => {\n          // TODO: dispatch edited_message\n          // (Read the input value from e.target.value)\n        }}\n      />\n      <br />\n      <button>Send to {contact.email}</button>\n    </section>\n  );\n}\n```\n\n```css\n.chat, .contact-list {\n  float: left;\n  margin-bottom: 20px;\n}\nul, li {\n  list-style: none;\n  margin: 0;\n  padding: 0;\n}\nli button {\n  width: 100px;\n  padding: 10px;\n  margin-right: 10px;\n}\ntextarea {\n  height: 150px;\n}\n```\n\n</Sandpack>\n\n<Solution>\n\nreducer의 코드를 보고 action에 필요한 것을 다음과 같이 유추해볼 수 있습니다.\n\n```js\n// 사용자가 \"Alice\"를 눌렀을 때\ndispatch({\n  type: 'changed_selection',\n  contactId: 1\n});\n\n// 사용자가 \"Hello!\"를 입력했을 때\ndispatch({\n  type: 'edited_message',\n  message: 'Hello!'\n});\n```\n\n아래 코드는 알맞은 메시지를 전달하도록 수정한 코드입니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { useReducer } from 'react';\nimport Chat from './Chat.js';\nimport ContactList from './ContactList.js';\nimport {\n  initialState,\n  messengerReducer\n} from './messengerReducer';\n\nexport default function Messenger() {\n  const [state, dispatch] = useReducer(\n    messengerReducer,\n    initialState\n  );\n  const message = state.message;\n  const contact = contacts.find(c =>\n    c.id === state.selectedId\n  );\n  return (\n    <div>\n      <ContactList\n        contacts={contacts}\n        selectedId={state.selectedId}\n        dispatch={dispatch}\n      />\n      <Chat\n        key={contact.id}\n        message={message}\n        contact={contact}\n        dispatch={dispatch}\n      />\n    </div>\n  );\n}\n\nconst contacts = [\n  { id: 0, name: 'Taylor', email: 'taylor@mail.com' },\n  { id: 1, name: 'Alice', email: 'alice@mail.com' },\n  { id: 2, name: 'Bob', email: 'bob@mail.com' }\n];\n```\n\n```js src/messengerReducer.js\nexport const initialState = {\n  selectedId: 0,\n  message: 'Hello'\n};\n\nexport function messengerReducer(\n  state,\n  action\n) {\n  switch (action.type) {\n    case 'changed_selection': {\n      return {\n        ...state,\n        selectedId: action.contactId,\n        message: ''\n      };\n    }\n    case 'edited_message': {\n      return {\n        ...state,\n        message: action.message\n      };\n    }\n    default: {\n      throw Error('Unknown action: ' + action.type);\n    }\n  }\n}\n```\n\n```js src/ContactList.js\nexport default function ContactList({contacts, selectedId, dispatch}) {\n  return (\n    <section className=\"contact-list\">\n      <ul>\n        {contacts.map(contact =>\n          <li key={contact.id}>\n            <button onClick={() => {\n              dispatch({\n                type: 'changed_selection',\n                contactId: contact.id\n              });\n            }}>\n              {selectedId === contact.id ? (\n                <b>{contact.name}</b>\n              ) : (\n                contact.name\n              )}\n            </button>\n          </li>\n        )}\n      </ul>\n    </section>\n  );\n}\n```\n\n```js src/Chat.js\nimport { useState } from 'react';\n\nexport default function Chat({\n  contact,\n  message,\n  dispatch\n}) {\n  return (\n    <section className=\"chat\">\n      <textarea\n        value={message}\n        placeholder={'Chat to ' + contact.name}\n        onChange={e => {\n          dispatch({\n            type: 'edited_message',\n            message: e.target.value\n          });\n        }}\n      />\n      <br />\n      <button>Send to {contact.email}</button>\n    </section>\n  );\n}\n```\n\n```css\n.chat, .contact-list {\n  float: left;\n  margin-bottom: 20px;\n}\nul, li {\n  list-style: none;\n  margin: 0;\n  padding: 0;\n}\nli button {\n  width: 100px;\n  padding: 10px;\n  margin-right: 10px;\n}\ntextarea {\n  height: 150px;\n}\n```\n\n</Sandpack>\n\n</Solution>\n\n#### message 전송 시, input 입력 값 지우기 {/*clear-the-input-on-sending-a-message*/}\n\n현재까지의 예시 코드를 실행 했을 때는 \"Send\"를 눌러도 아무런 일이 일어나지 않습니다. \"Send\" 버튼에 이벤트 핸들러를 추가하기 위해서 아래처럼 코드를 작성해봅시다.\n\n1. 수신자의 email과 message를 담은 `경고창(alert)` 표시하기.\n2. input의 message 값 지우기\n\n<Sandpack>\n\n```js src/App.js\nimport { useReducer } from 'react';\nimport Chat from './Chat.js';\nimport ContactList from './ContactList.js';\nimport {\n  initialState,\n  messengerReducer\n} from './messengerReducer';\n\nexport default function Messenger() {\n  const [state, dispatch] = useReducer(\n    messengerReducer,\n    initialState\n  );\n  const message = state.message;\n  const contact = contacts.find(c =>\n    c.id === state.selectedId\n  );\n  return (\n    <div>\n      <ContactList\n        contacts={contacts}\n        selectedId={state.selectedId}\n        dispatch={dispatch}\n      />\n      <Chat\n        key={contact.id}\n        message={message}\n        contact={contact}\n        dispatch={dispatch}\n      />\n    </div>\n  );\n}\n\nconst contacts = [\n  { id: 0, name: 'Taylor', email: 'taylor@mail.com' },\n  { id: 1, name: 'Alice', email: 'alice@mail.com' },\n  { id: 2, name: 'Bob', email: 'bob@mail.com' }\n];\n```\n\n```js src/messengerReducer.js\nexport const initialState = {\n  selectedId: 0,\n  message: 'Hello'\n};\n\nexport function messengerReducer(\n  state,\n  action\n) {\n  switch (action.type) {\n    case 'changed_selection': {\n      return {\n        ...state,\n        selectedId: action.contactId,\n        message: ''\n      };\n    }\n    case 'edited_message': {\n      return {\n        ...state,\n        message: action.message\n      };\n    }\n    default: {\n      throw Error('Unknown action: ' + action.type);\n    }\n  }\n}\n```\n\n```js src/ContactList.js\nexport default function ContactList({contacts, selectedId, dispatch}) {\n  return (\n    <section className=\"contact-list\">\n      <ul>\n        {contacts.map(contact =>\n          <li key={contact.id}>\n            <button onClick={() => {\n              dispatch({\n                type: 'changed_selection',\n                contactId: contact.id\n              });\n            }}>\n              {selectedId === contact.id ? (\n                <b>{contact.name}</b>\n              ) : (\n                contact.name\n              )}\n            </button>\n          </li>\n        )}\n      </ul>\n    </section>\n  );\n}\n```\n\n```js src/Chat.js active\nimport { useState } from 'react';\n\nexport default function Chat({\n  contact,\n  message,\n  dispatch\n}) {\n  return (\n    <section className=\"chat\">\n      <textarea\n        value={message}\n        placeholder={'Chat to ' + contact.name}\n        onChange={e => {\n          dispatch({\n            type: 'edited_message',\n            message: e.target.value\n          });\n        }}\n      />\n      <br />\n      <button>Send to {contact.email}</button>\n    </section>\n  );\n}\n```\n\n```css\n.chat, .contact-list {\n  float: left;\n  margin-bottom: 20px;\n}\nul, li {\n  list-style: none;\n  margin: 0;\n  padding: 0;\n}\nli button {\n  width: 100px;\n  padding: 10px;\n  margin-right: 10px;\n}\ntextarea {\n  height: 150px;\n}\n```\n\n</Sandpack>\n\n<Solution>\n\n\"Send\" 버튼의 이벤트 핸들러에서 이를 구현할 수 있는 몇가지 방법이 있습니다. 그중 한 가지 방법은 경고창(alert)을 표시한 다음, `message`를 빈값으로 설정하기 위해 `edited_message` action을 전달하는 것입니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { useReducer } from 'react';\nimport Chat from './Chat.js';\nimport ContactList from './ContactList.js';\nimport {\n  initialState,\n  messengerReducer\n} from './messengerReducer';\n\nexport default function Messenger() {\n  const [state, dispatch] = useReducer(\n    messengerReducer,\n    initialState\n  );\n  const message = state.message;\n  const contact = contacts.find(c =>\n    c.id === state.selectedId\n  );\n  return (\n    <div>\n      <ContactList\n        contacts={contacts}\n        selectedId={state.selectedId}\n        dispatch={dispatch}\n      />\n      <Chat\n        key={contact.id}\n        message={message}\n        contact={contact}\n        dispatch={dispatch}\n      />\n    </div>\n  );\n}\n\nconst contacts = [\n  { id: 0, name: 'Taylor', email: 'taylor@mail.com' },\n  { id: 1, name: 'Alice', email: 'alice@mail.com' },\n  { id: 2, name: 'Bob', email: 'bob@mail.com' }\n];\n```\n\n```js src/messengerReducer.js\nexport const initialState = {\n  selectedId: 0,\n  message: 'Hello'\n};\n\nexport function messengerReducer(\n  state,\n  action\n) {\n  switch (action.type) {\n    case 'changed_selection': {\n      return {\n        ...state,\n        selectedId: action.contactId,\n        message: ''\n      };\n    }\n    case 'edited_message': {\n      return {\n        ...state,\n        message: action.message\n      };\n    }\n    default: {\n      throw Error('Unknown action: ' + action.type);\n    }\n  }\n}\n```\n\n```js src/ContactList.js\nexport default function ContactList({contacts, selectedId, dispatch}) {\n  return (\n    <section className=\"contact-list\">\n      <ul>\n        {contacts.map(contact =>\n          <li key={contact.id}>\n            <button onClick={() => {\n              dispatch({\n                type: 'changed_selection',\n                contactId: contact.id\n              });\n            }}>\n              {selectedId === contact.id ? (\n                <b>{contact.name}</b>\n              ) : (\n                contact.name\n              )}\n            </button>\n          </li>\n        )}\n      </ul>\n    </section>\n  );\n}\n```\n\n```js src/Chat.js active\nimport { useState } from 'react';\n\nexport default function Chat({\n  contact,\n  message,\n  dispatch\n}) {\n  return (\n    <section className=\"chat\">\n      <textarea\n        value={message}\n        placeholder={'Chat to ' + contact.name}\n        onChange={e => {\n          dispatch({\n            type: 'edited_message',\n            message: e.target.value\n          });\n        }}\n      />\n      <br />\n      <button onClick={() => {\n        alert(`Sending \"${message}\" to ${contact.email}`);\n        dispatch({\n          type: 'edited_message',\n          message: '',\n        });\n      }}>Send to {contact.email}</button>\n    </section>\n  );\n}\n```\n\n```css\n.chat, .contact-list {\n  float: left;\n  margin-bottom: 20px;\n}\nul, li {\n  list-style: none;\n  margin: 0;\n  padding: 0;\n}\nli button {\n  width: 100px;\n  padding: 10px;\n  margin-right: 10px;\n}\ntextarea {\n  height: 150px;\n}\n```\n\n</Sandpack>\n\n잘 동작하기 때문에 \"Send\" 버튼을 누르면 input의 입력값이 잘 지워질 것입니다.\n\n하지만 *사용자의 관점에서 봤을 때*, message를 전송하는 것과 input 필드에 텍스트를 입력하는 것은 다른 행위입니다. 이를 반영하기 위해 `sent_message`라는 *새로운* action을 만들어서 reducer에서 별도로 분리하여 작성해보겠습니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { useReducer } from 'react';\nimport Chat from './Chat.js';\nimport ContactList from './ContactList.js';\nimport {\n  initialState,\n  messengerReducer\n} from './messengerReducer';\n\nexport default function Messenger() {\n  const [state, dispatch] = useReducer(\n    messengerReducer,\n    initialState\n  );\n  const message = state.message;\n  const contact = contacts.find(c =>\n    c.id === state.selectedId\n  );\n  return (\n    <div>\n      <ContactList\n        contacts={contacts}\n        selectedId={state.selectedId}\n        dispatch={dispatch}\n      />\n      <Chat\n        key={contact.id}\n        message={message}\n        contact={contact}\n        dispatch={dispatch}\n      />\n    </div>\n  );\n}\n\nconst contacts = [\n  { id: 0, name: 'Taylor', email: 'taylor@mail.com' },\n  { id: 1, name: 'Alice', email: 'alice@mail.com' },\n  { id: 2, name: 'Bob', email: 'bob@mail.com' }\n];\n```\n\n```js src/messengerReducer.js active\nexport const initialState = {\n  selectedId: 0,\n  message: 'Hello'\n};\n\nexport function messengerReducer(\n  state,\n  action\n) {\n  switch (action.type) {\n    case 'changed_selection': {\n      return {\n        ...state,\n        selectedId: action.contactId,\n        message: ''\n      };\n    }\n    case 'edited_message': {\n      return {\n        ...state,\n        message: action.message\n      };\n    }\n    case 'sent_message': {\n      return {\n        ...state,\n        message: ''\n      };\n    }\n    default: {\n      throw Error('Unknown action: ' + action.type);\n    }\n  }\n}\n```\n\n```js src/ContactList.js\nexport default function ContactList({contacts, selectedId, dispatch}) {\n  return (\n    <section className=\"contact-list\">\n      <ul>\n        {contacts.map(contact =>\n          <li key={contact.id}>\n            <button onClick={() => {\n              dispatch({\n                type: 'changed_selection',\n                contactId: contact.id\n              });\n            }}>\n              {selectedId === contact.id ? (\n                <b>{contact.name}</b>\n              ) : (\n                contact.name\n              )}\n            </button>\n          </li>\n        )}\n      </ul>\n    </section>\n  );\n}\n```\n\n```js src/Chat.js active\nimport { useState } from 'react';\n\nexport default function Chat({\n  contact,\n  message,\n  dispatch\n}) {\n  return (\n    <section className=\"chat\">\n      <textarea\n        value={message}\n        placeholder={'Chat to ' + contact.name}\n        onChange={e => {\n          dispatch({\n            type: 'edited_message',\n            message: e.target.value\n          });\n        }}\n      />\n      <br />\n      <button onClick={() => {\n        alert(`Sending \"${message}\" to ${contact.email}`);\n        dispatch({\n          type: 'sent_message',\n        });\n      }}>Send to {contact.email}</button>\n    </section>\n  );\n}\n```\n\n```css\n.chat, .contact-list {\n  float: left;\n  margin-bottom: 20px;\n}\nul, li {\n  list-style: none;\n  margin: 0;\n  padding: 0;\n}\nli button {\n  width: 100px;\n  padding: 10px;\n  margin-right: 10px;\n}\ntextarea {\n  height: 150px;\n}\n```\n\n</Sandpack>\n\n결과적으로 두 방법은 동일하게 동작합니다. 그렇지만 action 객체의 type은 \"state가 어떻게 변경되길 원하는지\"보다, \"사용자가 무엇을 했는지\"를 이상적으로 설명해야 한다는 점을 명심하세요. 이렇게 하면 이후에 기능을 추가하기 훨씬 더 수월해집니다.\n\n두 해결책 모두 reducer 안에서 `alert`를 **작성하지 않는** 것이 중요합니다. reducer는 순수함수이어야 하므로, 이 안에서는 오직 다음 state 값을 계산하기 위한 작업만 해야 합니다. 사용자에게 message를 보여주는 것을 포함한 다른 어떤 것도 \"수행하지 않아야\" 합니다. 이런 부분은 이벤트 핸들러 안에서 수행해야 합니다. (이 같은 실수를 방지하기 위해 React는 Strict 모드에서 reducer를 여러 번 호출합니다. 이 부분이 바로 reducer 안에 alert를 넣으면 두 번 실행되는 이유입니다.)\n\n</Solution>\n\n#### 탭 전환 시, input 입력 값 복원하기 {/*restore-input-values-when-switching-between-tabs*/}\n\n이 예시에서 선택된 수신자를 바꾸기 위해 탭 버튼을 누르면 message를 입력받는 input 필드의 텍스트 값이 항상 지워지도록 되어있습니다.\n\n```js\ncase 'changed_selection': {\n  return {\n    ...state,\n    selectedId: action.contactId,\n    message: '' // input 입력 값을 지우는 부분\n  };\n```\n\n이렇게 하는 이유는 각 수신자 사이에서 한개의 message 입력 값을 공유하고 싶지 않기 때문입니다. 그런데 이런 방식보다, 앱이 각 연락처에 대한 message 입력 값을 별도로 \"기억\"하여 선택된 연락처가 전환할 때마다 기억 했던 값을 복원하도록 하는 것이 더 나을 것입니다.\n\n여러분이 할 일은 *각 연락처 마다* 별도로 message의 초기 값을 기억할 수 있도록 state의 구조를 바꾸는 것입니다. 이 때, reducer, 초기 state 값 그리고 컴포넌트를 조금씩 변경해야할 것입니다.\n\n<Hint>\n\nstate를 다음과 같이 구조화할 수 있습니다.\n\n```js\nexport const initialState = {\n  selectedId: 0,\n  messages: {\n    0: 'Hello, Taylor', // contactId = 0의 message 초기 값\n    1: 'Hello, Alice' // contactId = 1의 message 초기 값\n  },\n};\n```\n\n`[key]: value` [계산된 프로퍼티명](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Operators/Object_initializer#계산된_프로퍼티명) 문법은 `messages` 객체를 업데이트하는데 도움이 됩니다.\n\n```js\n{\n  ...state.messages,\n  [id]: message\n}\n```\n\n</Hint>\n\n<Sandpack>\n\n```js src/App.js\nimport { useReducer } from 'react';\nimport Chat from './Chat.js';\nimport ContactList from './ContactList.js';\nimport {\n  initialState,\n  messengerReducer\n} from './messengerReducer';\n\nexport default function Messenger() {\n  const [state, dispatch] = useReducer(\n    messengerReducer,\n    initialState\n  );\n  const message = state.message;\n  const contact = contacts.find(c =>\n    c.id === state.selectedId\n  );\n  return (\n    <div>\n      <ContactList\n        contacts={contacts}\n        selectedId={state.selectedId}\n        dispatch={dispatch}\n      />\n      <Chat\n        key={contact.id}\n        message={message}\n        contact={contact}\n        dispatch={dispatch}\n      />\n    </div>\n  );\n}\n\nconst contacts = [\n  { id: 0, name: 'Taylor', email: 'taylor@mail.com' },\n  { id: 1, name: 'Alice', email: 'alice@mail.com' },\n  { id: 2, name: 'Bob', email: 'bob@mail.com' }\n];\n```\n\n```js src/messengerReducer.js\nexport const initialState = {\n  selectedId: 0,\n  message: 'Hello'\n};\n\nexport function messengerReducer(\n  state,\n  action\n) {\n  switch (action.type) {\n    case 'changed_selection': {\n      return {\n        ...state,\n        selectedId: action.contactId,\n        message: ''\n      };\n    }\n    case 'edited_message': {\n      return {\n        ...state,\n        message: action.message\n      };\n    }\n    case 'sent_message': {\n      return {\n        ...state,\n        message: ''\n      };\n    }\n    default: {\n      throw Error('Unknown action: ' + action.type);\n    }\n  }\n}\n```\n\n```js src/ContactList.js\nexport default function ContactList({contacts, selectedId, dispatch}) {\n  return (\n    <section className=\"contact-list\">\n      <ul>\n        {contacts.map(contact =>\n          <li key={contact.id}>\n            <button onClick={() => {\n              dispatch({\n                type: 'changed_selection',\n                contactId: contact.id\n              });\n            }}>\n              {selectedId === contact.id ? (\n                <b>{contact.name}</b>\n              ) : (\n                contact.name\n              )}\n            </button>\n          </li>\n        )}\n      </ul>\n    </section>\n  );\n}\n```\n\n```js src/Chat.js\nimport { useState } from 'react';\n\nexport default function Chat({\n  contact,\n  message,\n  dispatch\n}) {\n  return (\n    <section className=\"chat\">\n      <textarea\n        value={message}\n        placeholder={'Chat to ' + contact.name}\n        onChange={e => {\n          dispatch({\n            type: 'edited_message',\n            message: e.target.value\n          });\n        }}\n      />\n      <br />\n      <button onClick={() => {\n        alert(`Sending \"${message}\" to ${contact.email}`);\n        dispatch({\n          type: 'sent_message',\n        });\n      }}>Send to {contact.email}</button>\n    </section>\n  );\n}\n```\n\n```css\n.chat, .contact-list {\n  float: left;\n  margin-bottom: 20px;\n}\nul, li {\n  list-style: none;\n  margin: 0;\n  padding: 0;\n}\nli button {\n  width: 100px;\n  padding: 10px;\n  margin-right: 10px;\n}\ntextarea {\n  height: 150px;\n}\n```\n\n</Sandpack>\n\n<Solution>\n\nreducer는 각 연락처마다 별도의 message 초기 값을 저장하고 업데이트할 수 있도록 바꿔야합니다.\n\n```js\n// input 텍스트 값이 수정될 때\ncase 'edited_message': {\n  return {\n    // selectedId와 같은 다른 state 값은 유지합니다.\n    ...state,\n    messages: {\n      // 다른 연락처의 message 값들은 유지 시키지만,\n      ...state.messages,\n      // 선택된 연락처의 message는 바꿉니다.\n      [state.selectedId]: action.message\n    }\n  };\n}\n```\n\n현재 선택된 연락처의 message를 읽기 위해서 `Messenger` 컴포넌트의 코드 또한 수정해야 합니다.\n\n```js\nconst message = state.messages[state.selectedId];\n```\n\n완성된 코드는 다음과 같습니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { useReducer } from 'react';\nimport Chat from './Chat.js';\nimport ContactList from './ContactList.js';\nimport {\n  initialState,\n  messengerReducer\n} from './messengerReducer';\n\nexport default function Messenger() {\n  const [state, dispatch] = useReducer(\n    messengerReducer,\n    initialState\n  );\n  const message = state.messages[state.selectedId];\n  const contact = contacts.find(c =>\n    c.id === state.selectedId\n  );\n  return (\n    <div>\n      <ContactList\n        contacts={contacts}\n        selectedId={state.selectedId}\n        dispatch={dispatch}\n      />\n      <Chat\n        key={contact.id}\n        message={message}\n        contact={contact}\n        dispatch={dispatch}\n      />\n    </div>\n  );\n}\n\nconst contacts = [\n  { id: 0, name: 'Taylor', email: 'taylor@mail.com' },\n  { id: 1, name: 'Alice', email: 'alice@mail.com' },\n  { id: 2, name: 'Bob', email: 'bob@mail.com' }\n];\n```\n\n```js src/messengerReducer.js\nexport const initialState = {\n  selectedId: 0,\n  messages: {\n    0: 'Hello, Taylor',\n    1: 'Hello, Alice',\n    2: 'Hello, Bob'\n  }\n};\n\nexport function messengerReducer(\n  state,\n  action\n) {\n  switch (action.type) {\n    case 'changed_selection': {\n      return {\n        ...state,\n        selectedId: action.contactId,\n      };\n    }\n    case 'edited_message': {\n      return {\n        ...state,\n        messages: {\n          ...state.messages,\n          [state.selectedId]: action.message\n        }\n      };\n    }\n    case 'sent_message': {\n      return {\n        ...state,\n        messages: {\n          ...state.messages,\n          [state.selectedId]: ''\n        }\n      };\n    }\n    default: {\n      throw Error('Unknown action: ' + action.type);\n    }\n  }\n}\n```\n\n```js src/ContactList.js\nexport default function ContactList({contacts, selectedId, dispatch}) {\n  return (\n    <section className=\"contact-list\">\n      <ul>\n        {contacts.map(contact =>\n          <li key={contact.id}>\n            <button onClick={() => {\n              dispatch({\n                type: 'changed_selection',\n                contactId: contact.id\n              });\n            }}>\n              {selectedId === contact.id ? (\n                <b>{contact.name}</b>\n              ) : (\n                contact.name\n              )}\n            </button>\n          </li>\n        )}\n      </ul>\n    </section>\n  );\n}\n```\n\n```js src/Chat.js\nimport { useState } from 'react';\n\nexport default function Chat({\n  contact,\n  message,\n  dispatch\n}) {\n  return (\n    <section className=\"chat\">\n      <textarea\n        value={message}\n        placeholder={'Chat to ' + contact.name}\n        onChange={e => {\n          dispatch({\n            type: 'edited_message',\n            message: e.target.value\n          });\n        }}\n      />\n      <br />\n      <button onClick={() => {\n        alert(`Sending \"${message}\" to ${contact.email}`);\n        dispatch({\n          type: 'sent_message',\n        });\n      }}>Send to {contact.email}</button>\n    </section>\n  );\n}\n```\n\n```css\n.chat, .contact-list {\n  float: left;\n  margin-bottom: 20px;\n}\nul, li {\n  list-style: none;\n  margin: 0;\n  padding: 0;\n}\nli button {\n  width: 100px;\n  padding: 10px;\n  margin-right: 10px;\n}\ntextarea {\n  height: 150px;\n}\n```\n\n</Sandpack>\n\n특히 다른 동작을 구현하기 위해 이벤트 핸들러를 변경할 필요가 없습니다. reducer를 사용하지 않았더라면, state를 업데이트하는 모든 이벤트 핸들러를 변경해야 했을 것입니다.\n\n</Solution>\n\n#### 처음부터 `useReducer` 구현해보기 {/*implement-usereducer-from-scratch*/}\n\n앞선 예시들에서는, `useReducer` hook을 React에서 불러와 사용했습니다. 이번에는 *`useReducer` 훅 자체*를 직접 구현해 볼 것입니다! 다음은 시작을 위한 스탭입니다. 10줄 이상의 코드를 작성할 필요가 없습니다.\n\n변경 사항을 테스트하려면 input에 텍스트를 입력하거나 연락처를 선택해보세요.\n\n<Hint>\n\n다음은 구현에 대한 더 자세한 밑그림입니다.\n\n```js\nexport function useReducer(reducer, initialState) {\n  const [state, setState] = useState(initialState);\n\n  function dispatch(action) {\n    // ???\n  }\n\n  return [state, dispatch];\n}\n```\n\nreducer 함수는 두 개의 인수인 현재 state와 action 객체를 입력받고 다음 state를 반환한다는 것을 떠올려보세요. `dispatch` 를 구현하려면 무엇을 해야 할까요?\n\n</Hint>\n\n<Sandpack>\n\n```js src/App.js\nimport { useReducer } from './MyReact.js';\nimport Chat from './Chat.js';\nimport ContactList from './ContactList.js';\nimport {\n  initialState,\n  messengerReducer\n} from './messengerReducer';\n\nexport default function Messenger() {\n  const [state, dispatch] = useReducer(\n    messengerReducer,\n    initialState\n  );\n  const message = state.messages[state.selectedId];\n  const contact = contacts.find(c =>\n    c.id === state.selectedId\n  );\n  return (\n    <div>\n      <ContactList\n        contacts={contacts}\n        selectedId={state.selectedId}\n        dispatch={dispatch}\n      />\n      <Chat\n        key={contact.id}\n        message={message}\n        contact={contact}\n        dispatch={dispatch}\n      />\n    </div>\n  );\n}\n\nconst contacts = [\n  { id: 0, name: 'Taylor', email: 'taylor@mail.com' },\n  { id: 1, name: 'Alice', email: 'alice@mail.com' },\n  { id: 2, name: 'Bob', email: 'bob@mail.com' }\n];\n```\n\n```js src/messengerReducer.js\nexport const initialState = {\n  selectedId: 0,\n  messages: {\n    0: 'Hello, Taylor',\n    1: 'Hello, Alice',\n    2: 'Hello, Bob'\n  }\n};\n\nexport function messengerReducer(\n  state,\n  action\n) {\n  switch (action.type) {\n    case 'changed_selection': {\n      return {\n        ...state,\n        selectedId: action.contactId,\n      };\n    }\n    case 'edited_message': {\n      return {\n        ...state,\n        messages: {\n          ...state.messages,\n          [state.selectedId]: action.message\n        }\n      };\n    }\n    case 'sent_message': {\n      return {\n        ...state,\n        messages: {\n          ...state.messages,\n          [state.selectedId]: ''\n        }\n      };\n    }\n    default: {\n      throw Error('Unknown action: ' + action.type);\n    }\n  }\n}\n```\n\n```js src/MyReact.js active\nimport { useState } from 'react';\n\nexport function useReducer(reducer, initialState) {\n  const [state, setState] = useState(initialState);\n\n  // ???\n\n  return [state, dispatch];\n}\n```\n\n```js src/ContactList.js hidden\nexport default function ContactList({contacts, selectedId, dispatch}) {\n  return (\n    <section className=\"contact-list\">\n      <ul>\n        {contacts.map(contact =>\n          <li key={contact.id}>\n            <button onClick={() => {\n              dispatch({\n                type: 'changed_selection',\n                contactId: contact.id\n              });\n            }}>\n              {selectedId === contact.id ? (\n                <b>{contact.name}</b>\n              ) : (\n                contact.name\n              )}\n            </button>\n          </li>\n        )}\n      </ul>\n    </section>\n  );\n}\n```\n\n```js src/Chat.js hidden\nimport { useState } from 'react';\n\nexport default function Chat({\n  contact,\n  message,\n  dispatch\n}) {\n  return (\n    <section className=\"chat\">\n      <textarea\n        value={message}\n        placeholder={'Chat to ' + contact.name}\n        onChange={e => {\n          dispatch({\n            type: 'edited_message',\n            message: e.target.value\n          });\n        }}\n      />\n      <br />\n      <button onClick={() => {\n        alert(`Sending \"${message}\" to ${contact.email}`);\n        dispatch({\n          type: 'sent_message',\n        });\n      }}>Send to {contact.email}</button>\n    </section>\n  );\n}\n```\n\n```css\n.chat, .contact-list {\n  float: left;\n  margin-bottom: 20px;\n}\nul, li {\n  list-style: none;\n  margin: 0;\n  padding: 0;\n}\nli button {\n  width: 100px;\n  padding: 10px;\n  margin-right: 10px;\n}\ntextarea {\n  height: 150px;\n}\n```\n\n</Sandpack>\n\n<Solution>\n\ndispatch 함수에 action을 담아 전달하면 현재 state와 action과 함께 reducer를 호출하고 반환된 결과를 다음 state로 저장합니다. 이를 구현한 코드는 다음과 같습니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { useReducer } from './MyReact.js';\nimport Chat from './Chat.js';\nimport ContactList from './ContactList.js';\nimport {\n  initialState,\n  messengerReducer\n} from './messengerReducer';\n\nexport default function Messenger() {\n  const [state, dispatch] = useReducer(\n    messengerReducer,\n    initialState\n  );\n  const message = state.messages[state.selectedId];\n  const contact = contacts.find(c =>\n    c.id === state.selectedId\n  );\n  return (\n    <div>\n      <ContactList\n        contacts={contacts}\n        selectedId={state.selectedId}\n        dispatch={dispatch}\n      />\n      <Chat\n        key={contact.id}\n        message={message}\n        contact={contact}\n        dispatch={dispatch}\n      />\n    </div>\n  );\n}\n\nconst contacts = [\n  { id: 0, name: 'Taylor', email: 'taylor@mail.com' },\n  { id: 1, name: 'Alice', email: 'alice@mail.com' },\n  { id: 2, name: 'Bob', email: 'bob@mail.com' }\n];\n```\n\n```js src/messengerReducer.js\nexport const initialState = {\n  selectedId: 0,\n  messages: {\n    0: 'Hello, Taylor',\n    1: 'Hello, Alice',\n    2: 'Hello, Bob'\n  }\n};\n\nexport function messengerReducer(\n  state,\n  action\n) {\n  switch (action.type) {\n    case 'changed_selection': {\n      return {\n        ...state,\n        selectedId: action.contactId,\n      };\n    }\n    case 'edited_message': {\n      return {\n        ...state,\n        messages: {\n          ...state.messages,\n          [state.selectedId]: action.message\n        }\n      };\n    }\n    case 'sent_message': {\n      return {\n        ...state,\n        messages: {\n          ...state.messages,\n          [state.selectedId]: ''\n        }\n      };\n    }\n    default: {\n      throw Error('Unknown action: ' + action.type);\n    }\n  }\n}\n```\n\n```js src/MyReact.js active\nimport { useState } from 'react';\n\nexport function useReducer(reducer, initialState) {\n  const [state, setState] = useState(initialState);\n\n  function dispatch(action) {\n    const nextState = reducer(state, action);\n    setState(nextState);\n  }\n\n  return [state, dispatch];\n}\n```\n\n```js src/ContactList.js hidden\nexport default function ContactList({contacts, selectedId, dispatch}) {\n  return (\n    <section className=\"contact-list\">\n      <ul>\n        {contacts.map(contact =>\n          <li key={contact.id}>\n            <button onClick={() => {\n              dispatch({\n                type: 'changed_selection',\n                contactId: contact.id\n              });\n            }}>\n              {selectedId === contact.id ? (\n                <b>{contact.name}</b>\n              ) : (\n                contact.name\n              )}\n            </button>\n          </li>\n        )}\n      </ul>\n    </section>\n  );\n}\n```\n\n```js src/Chat.js hidden\nimport { useState } from 'react';\n\nexport default function Chat({\n  contact,\n  message,\n  dispatch\n}) {\n  return (\n    <section className=\"chat\">\n      <textarea\n        value={message}\n        placeholder={'Chat to ' + contact.name}\n        onChange={e => {\n          dispatch({\n            type: 'edited_message',\n            message: e.target.value\n          });\n        }}\n      />\n      <br />\n      <button onClick={() => {\n        alert(`Sending \"${message}\" to ${contact.email}`);\n        dispatch({\n          type: 'sent_message',\n        });\n      }}>Send to {contact.email}</button>\n    </section>\n  );\n}\n```\n\n```css\n.chat, .contact-list {\n  float: left;\n  margin-bottom: 20px;\n}\nul, li {\n  list-style: none;\n  margin: 0;\n  padding: 0;\n}\nli button {\n  width: 100px;\n  padding: 10px;\n  margin-right: 10px;\n}\ntextarea {\n  height: 150px;\n}\n```\n\n</Sandpack>\n\n대부분의 경우에서는 중요하지 않지만, 좀 더 정확한 구현은 아래와 같습니다.\n\n```js\nfunction dispatch(action) {\n  setState(s => reducer(s, action));\n}\n```\n\n[업데이터 함수와 비슷하게](/learn/queueing-a-series-of-state-updates) 전달된 action이 다음 렌더링이 있을 때까지 큐에 쌓이기 때문에 이렇게 작성하는 것이 더 좋습니다.\n\n</Solution>\n\n</Challenges>\n"
  },
  {
    "path": "src/content/learn/importing-and-exporting-components.md",
    "content": "---\ntitle: 컴포넌트 import 및 export 하기\n---\n\n<Intro>\n\n컴포넌트의 가장 큰 장점은 재사용성으로 컴포넌트를 조합해 또 다른 컴포넌트를 만들 수 있다는 것입니다. 컴포넌트를 여러 번 중첩하게 되면 다른 파일로 분리해야 하는 시점이 생깁니다. 이렇게 분리하면 나중에 파일을 찾기 더 쉽고 재사용하기 편리해집니다.\n\n</Intro>\n\n<YouWillLearn>\n\n* Root 컴포넌트란\n* 컴포넌트를 import 하거나 export 하는 방법\n* 언제 default 또는 named imports와 exports를 사용할지\n* 한 파일에서 여러 컴포넌트를 import 하거나 export 하는 방법\n* 여러 컴포넌트를 여러 파일로 분리하는 방법\n\n</YouWillLearn>\n\n## Root 컴포넌트란 {/*the-root-component-file*/}\n\n[첫 컴포넌트](/learn/your-first-component)에서 만든 `Profile` 컴포넌트와 `Gallery` 컴포넌트는 아래와 같이 렌더링 됩니다.\n\n<Sandpack>\n\n```js\nfunction Profile() {\n  return (\n    <img\n      src=\"https://i.imgur.com/MK3eW3As.jpg\"\n      alt=\"Katherine Johnson\"\n    />\n  );\n}\n\nexport default function Gallery() {\n  return (\n    <section>\n      <h1>Amazing scientists</h1>\n      <Profile />\n      <Profile />\n      <Profile />\n    </section>\n  );\n}\n```\n\n```css\nimg { margin: 0 10px 10px 0; height: 90px; }\n```\n\n</Sandpack>\n\n이 예시의 컴포넌트들은 모두 `App.js`라는 **root 컴포넌트 파일**에 존재합니다. 설정에 따라 root 컴포넌트가 다른 파일에 위치할 수도 있습니다. Next.js처럼 파일 기반으로 라우팅하는 프레임워크일 경우 페이지별로 root 컴포넌트가 다를 수 있습니다.\n\n## 컴포넌트를 import 하거나 export 하는 방법 {/*exporting-and-importing-a-component*/}\n\n랜딩 화면을 변경하게 되어 과학자들이 아니라 과학책으로 변경되거나 프로필 사진을 다른 곳에서 사용하게 된다면 `Gallery` 컴포넌트와 `Profile` 컴포넌트를 root 컴포넌트가 아닌 다른 파일로 옮기는 게 좋습니다. 그렇게 변경하면 재사용성이 높아져 컴포넌트를 모듈로 사용할 수 있습니다. 컴포넌트를 다른 파일로 이동하려면 세 가지 단계가 있습니다.\n\n1. 컴포넌트를 추가할 JS 파일을 **생성**합니다.\n2. 새로 만든 파일에서 함수 컴포넌트를 **export** 합니다. ([default](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/export#using_the_default_export) 또는 [named](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/export#using_named_exports) export 방식을 사용합니다)\n3. 컴포넌트를 사용할 파일에서 **import** 합니다. (적절한 방식을 선택해서 [default](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/import#importing_defaults) 또는 [named](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/import#import_a_single_export_from_a_module)로 import 합니다)\n\n아래 예시를 보면 `App.js` 파일에서 `Profile`과 `Gallery` 컴포넌트를 빼서 새로운 `Gallery.js` 파일로 옮겼습니다. 이제 `Gallery`는 `Gallery.js`에서 import 해서 사용할 수 있습니다.\n\n\n<Sandpack>\n\n```js src/App.js\nimport Gallery from './Gallery.js';\n\nexport default function App() {\n  return (\n    <Gallery />\n  );\n}\n```\n\n```js src/Gallery.js\nfunction Profile() {\n  return (\n    <img\n      src=\"https://i.imgur.com/QIrZWGIs.jpg\"\n      alt=\"Alan L. Hart\"\n    />\n  );\n}\n\nexport default function Gallery() {\n  return (\n    <section>\n      <h1>Amazing scientists</h1>\n      <Profile />\n      <Profile />\n      <Profile />\n    </section>\n  );\n}\n```\n\n```css\nimg { margin: 0 10px 10px 0; height: 90px; }\n```\n\n</Sandpack>\n\n이제 이 예시에서는 컴포넌트들이 두 파일로 나뉘게 되었습니다.\n\n1. `Gallery.js`:\n     - `Profile` 컴포넌트를 정의하고 해당 파일에서만 사용되기 때문에 export 되지 않습니다.\n     - **Default** 방식으로 `Gallery` 컴포넌트를 export 합니다.\n2. `App.js`:\n     - **Default** 방식으로 `Gallery`를 `Gallery.js`로부터 **import** 합니다.\n     - Root `App` 컴포넌트를 **default** 방식으로 **export** 합니다.\n\n\n<Note>\n\n가끔 `.js`와 같은 파일 확장자가 없을 때도 있습니다.\n\n```js\nimport Gallery from './Gallery';\n```\n\nReact에서는 `'./Gallery.js'` 또는 `'./Gallery'` 둘 다 사용할 수 있지만 전자의 경우가 [native ES Modules](https://developer.mozilla.org/docs/Web/JavaScript/Guide/Modules) 사용 방법에 더 가깝습니다.\n\n</Note>\n\n<DeepDive>\n\n#### Default와 Named Exports {/*default-vs-named-exports*/}\n\n보통 JavaScript에서는 default와 named export라는 두 가지 방법으로 값을 export 합니다. 지금까지의 예시에서는 default export만 사용했지만 두 방법 다 한 파일에서 사용할 수도 있습니다. **다만 한 파일에서는 하나의 _default_ export만 존재할 수 있고 _named_ export는 여러 개가 존재할 수 있습니다.**\n\n![Default and named exports](/images/docs/illustrations/i_import-export.svg)\n\nExport 하는 방식에 따라 import 하는 방식이 정해져 있습니다. Default export로 한 값을 named import로 가져오려고 하려면 에러가 발생합니다. 아래 표에는 각각의 경우의 문법이 정리되어 있습니다.\n\n| Syntax           | Export 구문                           | Import 구문                          |\n| -----------      | -----------                                | -----------                               |\n| Default  | `export default function Button() {}` | `import Button from './button.js';`     |\n| Named    | `export function Button() {}`         | `import { Button } from './button.js';` |\n\n_Default_ import를 사용하는 경우 원한다면 `import` 단어 후에 다른 이름으로 값을 가져올 수 있습니다. 예를 들어 `import Banana from './button.js'` 라고 쓰더라도 같은 default export 값을 가져오게 됩니다. 반대로 named import를 사용할 때는 양쪽 파일에서 사용하고자 하는 값의 이름이 같아야 하기 때문에 _named_ import라고 불립니다.\n\n**보편적으로 한 파일에서 하나의 컴포넌트만 export 할 때 default export 방식을 사용하고 여러 컴포넌트를 export 할 경우엔 named export 방식을 사용합니다.** 어떤 방식을 사용하든 컴포넌트와 파일의 이름을 의미 있게 명명하는 것은 중요합니다. `export default () => {}` 처럼 이름 없는 컴포넌트는 나중에 디버깅하기 어렵기 때문에 권장하지 않습니다.\n\n</DeepDive>\n\n## 한 파일에서 여러 컴포넌트를 import 하거나 export 하는 방법 {/*exporting-and-importing-multiple-components-from-the-same-file*/}\n\n전체 갤러리가 아니라 하나의 `Profile`만 사용하고 싶을 때 `Profile` 컴포넌트만 export 하면 됩니다. 하지만 `Gallery.js` 파일에는 이미 하나의 *default* export가 존재하기 때문의 _두 개의_ default export를 정의할 수 없습니다. 이런 경우 새로운 파일 하나를 더 생성해서 default export를 사용하거나 *named* export로 `Profile` 컴포넌트를 export 할 수 있습니다. **한 파일에서는 단 하나의 default export만 사용할 수 있지만 named export는 여러 번 사용할 수 있습니다.**\n\n\n먼저 named export 방식을 사용해서 `Gallery.js` 파일에서 `Profile` 컴포넌트를 **export** 합니다. (`default` 키워드를 사용하지 않습니다)\n\n```js\nexport function Profile() {\n  // ...\n}\n```\n\n그 다음엔 named import 방식으로 `Gallery.js` 파일에서 `Profile` 컴포넌트를 `App.js` 파일로 **import** 합니다. (중괄호를 사용합니다)\n\n```js\nimport { Profile } from './Gallery.js';\n```\n\n마지막으로 `<Profile />`을 `App` 컴포넌트에서 **렌더링**합니다.\n\n```js\nexport default function App() {\n  return <Profile />;\n}\n```\n\n이제 `Gallery.js`에는 default `Gallery` export랑 named `Profile` export라는 두 가지의 export가 존재합니다. `App.js`에서는 두 컴포넌트를 import 해서 사용합니다. 아래의 예시에서 `<Profile />`과 `<Gallery />`를 교차해서 사용해 보세요.\n\n<Sandpack>\n\n```js src/App.js\nimport Gallery from './Gallery.js';\nimport { Profile } from './Gallery.js';\n\nexport default function App() {\n  return (\n    <Profile />\n  );\n}\n```\n\n```js src/Gallery.js\nexport function Profile() {\n  return (\n    <img\n      src=\"https://i.imgur.com/QIrZWGIs.jpg\"\n      alt=\"Alan L. Hart\"\n    />\n  );\n}\n\nexport default function Gallery() {\n  return (\n    <section>\n      <h1>Amazing scientists</h1>\n      <Profile />\n      <Profile />\n      <Profile />\n    </section>\n  );\n}\n```\n\n```css\nimg { margin: 0 10px 10px 0; height: 90px; }\n```\n\n</Sandpack>\n\n이제 default와 named export 방식 둘 다 사용할 수 있게 됐습니다.\n\n* `Gallery.js`:\n  - **Named export** 방식으로 `Profile`이라는 이름의 컴포넌트를 export 합니다.\n  - **Default export** 방식으로 `Gallery` 컴포넌트를 export 합니다.\n* `App.js`:\n  - `Gallery.js`에서 **named import** 방식으로 `Profile` 컴포넌트를 import 합니다.\n  - `Gallery.js`에서 **default import** 방식으로 `Gallery` 컴포넌트를 import 합니다.\n  - **Default export** 방식으로 `App` 컴포넌트를 export 합니다.\n\n<Recap>\n\n이 페이지에서 배우게 된 것들입니다.\n\n* Root 컴포넌트란 무엇인지\n* 컴포넌트를 import 하거나 export 하는 방법\n* 언제 default 또는 named imports와 exports를 사용할지\n* 한 파일에서 여러 컴포넌트를 export 하는 방법\n\n</Recap>\n\n\n\n<Challenges>\n\n#### 컴포넌트를 한 단계 더 분리하기 {/*split-the-components-further*/}\n\n현재 `Gallery.js` 파일이 `Profile`과 `Gallery`를 둘 다 export 해서 헷갈리게 할 수 있습니다.\n\n`Profile.js` 파일을 생성해서 `Profile` 컴포넌트를 해당 파일로 옮기고 `App` 컴포넌트에서는 `<Profile />`과 `<Gallery />`를 각각 렌더링하도록 변경합니다.\n\nDefault 또는 named export를 사용해서 `Profile`을 export 할 수 있습니다. 다만 주의할 점은 사용한 export 방식에 맞는 import 문법을 사용해야 한다는 점입니다. 아래 문법 표는 위 deep dive에서 인용했습니다.\n\n| Syntax           | Export 구문                           | Import 구문                          |\n| -----------      | -----------                                | -----------                               |\n| Default  | `export default function Button() {}` | `import Button from './button.js';`     |\n| Named    | `export function Button() {}`         | `import { Button } from './button.js';` |\n\n<Hint>\n\n컴포넌트를 사용하는 모든 파일에서 import 해야 합니다. `Gallery`에서도 `Profile`을 사용합니다.\n\n</Hint>\n\n<Sandpack>\n\n```js src/App.js\nimport Gallery from './Gallery.js';\nimport { Profile } from './Gallery.js';\n\nexport default function App() {\n  return (\n    <div>\n      <Profile />\n    </div>\n  );\n}\n```\n\n```js src/Gallery.js active\n// Move me to Profile.js!\nexport function Profile() {\n  return (\n    <img\n      src=\"https://i.imgur.com/QIrZWGIs.jpg\"\n      alt=\"Alan L. Hart\"\n    />\n  );\n}\n\nexport default function Gallery() {\n  return (\n    <section>\n      <h1>Amazing scientists</h1>\n      <Profile />\n      <Profile />\n      <Profile />\n    </section>\n  );\n}\n```\n\n```js src/Profile.js\n```\n\n```css\nimg { margin: 0 10px 10px 0; height: 90px; }\n```\n\n</Sandpack>\n\nExport 방식 중 하나를 사용했으면 다른 방식으로도 동작하게 시도해 보세요.\n\n<Solution>\n\n다음은 named export를 사용했을 경우의 해법입니다.\n\n<Sandpack>\n\n```js src/App.js\nimport Gallery from './Gallery.js';\nimport { Profile } from './Profile.js';\n\nexport default function App() {\n  return (\n    <div>\n      <Profile />\n      <Gallery />\n    </div>\n  );\n}\n```\n\n```js src/Gallery.js\nimport { Profile } from './Profile.js';\n\nexport default function Gallery() {\n  return (\n    <section>\n      <h1>Amazing scientists</h1>\n      <Profile />\n      <Profile />\n      <Profile />\n    </section>\n  );\n}\n```\n\n```js src/Profile.js\nexport function Profile() {\n  return (\n    <img\n      src=\"https://i.imgur.com/QIrZWGIs.jpg\"\n      alt=\"Alan L. Hart\"\n    />\n  );\n}\n```\n\n```css\nimg { margin: 0 10px 10px 0; height: 90px; }\n```\n\n</Sandpack>\n\n다음은 default export를 사용했을 경우의 해법입니다.\n\n<Sandpack>\n\n```js src/App.js\nimport Gallery from './Gallery.js';\nimport Profile from './Profile.js';\n\nexport default function App() {\n  return (\n    <div>\n      <Profile />\n      <Gallery />\n    </div>\n  );\n}\n```\n\n```js src/Gallery.js\nimport Profile from './Profile.js';\n\nexport default function Gallery() {\n  return (\n    <section>\n      <h1>Amazing scientists</h1>\n      <Profile />\n      <Profile />\n      <Profile />\n    </section>\n  );\n}\n```\n\n```js src/Profile.js\nexport default function Profile() {\n  return (\n    <img\n      src=\"https://i.imgur.com/QIrZWGIs.jpg\"\n      alt=\"Alan L. Hart\"\n    />\n  );\n}\n```\n\n```css\nimg { margin: 0 10px 10px 0; height: 90px; }\n```\n\n</Sandpack>\n\n</Solution>\n\n</Challenges>\n"
  },
  {
    "path": "src/content/learn/index.md",
    "content": "---\ntitle: 빠르게 시작하기\n---\n\n<Intro>\n\nReact 문서에 오신 것을 환영합니다! 이 페이지에서는 여러분이 매일 사용하게 될 React 개념의 80%를 소개합니다.\n\n</Intro>\n\n<YouWillLearn>\n\n- 컴포넌트를 만들고 중첩하는 방법\n- 마크업과 스타일을 추가하는 방법\n- 데이터를 표시하는 방법\n- 조건과 리스트를 렌더링하는 방법\n- 이벤트에 응답하고 화면을 업데이트하는 방법\n- 컴포넌트 간에 데이터를 공유하는 방법\n\n</YouWillLearn>\n\n## 컴포넌트 생성 및 중첩하기 {/*components*/}\n\nReact 앱은 *컴포넌트*로 구성됩니다. 컴포넌트는 고유한 로직과 모양을 가진 사용자 인터페이스<sup>UI</sup>의 일부입니다. 컴포넌트는 버튼만큼 작을 수도 있고 전체 페이지만큼 클 수도 있습니다.\n\nReact 컴포넌트는 마크업을 반환하는 자바스크립트 함수입니다.\n\n```js\nfunction MyButton() {\n  return (\n    <button>I'm a button</button>\n  );\n}\n```\n\n이제 `MyButton`을 선언했으므로 다른 컴포넌트 안에 중첩할 수 있습니다.\n\n```js {5}\nexport default function MyApp() {\n  return (\n    <div>\n      <h1>Welcome to my app</h1>\n      <MyButton />\n    </div>\n  );\n}\n```\n\n`<MyButton />`이 대문자로 시작하는 것을 주목해 주세요. 이것이 바로 React 컴포넌트임을 알 수 있는 방법입니다. React 컴포넌트의 이름은 항상 대문자로 시작해야 하고 HTML 태그는 소문자로 시작해야 합니다.\n\n결과를 확인해 보세요.\n\n<Sandpack>\n\n```js\nfunction MyButton() {\n  return (\n    <button>\n      I'm a button\n    </button>\n  );\n}\n\nexport default function MyApp() {\n  return (\n    <div>\n      <h1>Welcome to my app</h1>\n      <MyButton />\n    </div>\n  );\n}\n```\n\n</Sandpack>\n\n`export default` 키워드는 파일의 기본 컴포넌트를 지정합니다. 자바스크립트 문법에 익숙하지 않다면 [MDN](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Statements/export)과 [javascript.info](https://ko.javascript.info/import-export)를 참고해 주세요.\n\n## JSX로 마크업 작성하기 {/*writing-markup-with-jsx*/}\n\n위에서 본 마크업 문법을 *JSX*라고 합니다. 이것은 선택 사항이지만 대부분의 React 프로젝트는 편의성을 위해 JSX를 사용합니다. [로컬 개발에 권장하는 모든 도구](/learn/installation)는 JSX를 기본적으로 지원합니다.\n\nJSX는 HTML보다 엄격합니다. JSX에서는 `<br />`같이 태그를 닫아야 합니다. 또한 컴포넌트는 여러 개의 JSX 태그를 반환할 수 없습니다. `<div>...</div>` 또는 빈 `<>...</>` 래퍼와 같이 공유되는 부모로 감싸야 합니다.\n\n```js {3,6}\nfunction AboutPage() {\n  return (\n    <>\n      <h1>About</h1>\n      <p>Hello there.<br />How do you do?</p>\n    </>\n  );\n}\n```\n\nJSX로 변환할 HTML이 많은 경우 [온라인 변환기](https://transform.tools/html-to-jsx)를 사용할 수 있습니다.\n\n## 스타일 추가하기 {/*adding-styles*/}\n\nReact에서는 `className`으로 CSS 클래스를 지정합니다. 이것은 HTML의 [`class`](https://developer.mozilla.org/ko/docs/Web/HTML/Global_attributes/class) 어트리뷰트와 동일한 방식으로 동작합니다.\n\n```js\n<img className=\"avatar\" />\n```\n\n그 다음 별도의 CSS 파일에 해당 CSS 규칙을 작성합니다.\n\n```css\n/* In your CSS */\n.avatar {\n  border-radius: 50%;\n}\n```\n\nReact는 CSS 파일을 추가하는 방법을 규정하지 않습니다. 가장 간단한 방법은 HTML에 [`<link>`](https://developer.mozilla.org/ko-KR/docs/Web/HTML/Element/link) 태그를 추가하는 것입니다. 빌드 도구나 프레임워크를 사용하는 경우 해당 문서를 참고하여 프로젝트에 CSS 파일을 추가하는 방법을 알아보세요.\n\n## 데이터 표시하기 {/*displaying-data*/}\n\nJSX를 사용하면 자바스크립트에 마크업을 넣을 수 있습니다. 중괄호를 사용하면 코드에서 일부 변수를 삽입하여 사용자에게 표시할 수 있도록 자바스크립트로 \"이스케이프 백<sup>Escape Back</sup>\" 할 수 있습니다. 아래의 예시는 `user.name`을 표시합니다.\n\n```js {3}\nreturn (\n  <h1>\n    {user.name}\n  </h1>\n);\n```\n\nJSX 어트리뷰트에서 따옴표 *대신* 중괄호를 사용하여 \"자바스크립트로 이스케이프<sup>Escape Into JavaScript</sup>\" 할 수도 있습니다. 예를 들어 `className=\"avatar\"`는 `\"avatar\"` 문자열을 CSS로 전달하지만 `src={user.imageUrl}`는 자바스크립트 `user.imageUrl` 변수 값을 읽은 다음 해당 값을 `src` 어트리뷰트로 전달합니다.\n\n```js {3,4}\nreturn (\n  <img\n    className=\"avatar\"\n    src={user.imageUrl}\n  />\n);\n```\n\nJSX 중괄호 안에 [문자열 연결](https://ko.javascript.info/operators#string-concatenation-with-binary)과 같이 더 복잡한 표현식을 넣을 수도 있습니다.\n\n<Sandpack>\n\n```js\nconst user = {\n  name: 'Hedy Lamarr',\n  imageUrl: 'https://i.imgur.com/yXOvdOSs.jpg',\n  imageSize: 90,\n};\n\nexport default function Profile() {\n  return (\n    <>\n      <h1>{user.name}</h1>\n      <img\n        className=\"avatar\"\n        src={user.imageUrl}\n        alt={'Photo of ' + user.name}\n        style={{\n          width: user.imageSize,\n          height: user.imageSize\n        }}\n      />\n    </>\n  );\n}\n```\n\n```css\n.avatar {\n  border-radius: 50%;\n}\n\n.large {\n  border: 4px solid gold;\n}\n```\n\n</Sandpack>\n\n위의 예시에서 `style={{}}`은 특별한 문법이 아니라 `style={ }` JSX 중괄호 안에 있는 일반 `{}` 객체입니다. 스타일이 자바스크립트 변수에 의존하는 경우 `style` 어트리뷰트를 사용할 수 있습니다.\n\n## 조건부 렌더링 {/*conditional-rendering*/}\n\nReact에서 조건문을 작성하는 데에는 특별한 문법이 필요 없습니다. 일반적인 자바스크립트 코드를 작성할 때 사용하는 것과 동일한 방법을 사용합니다. 예를 들어 [`if`](https://developer.mozilla.org/ko-KR/docs/Web/JavaScript/Reference/Statements/if...else) 문을 사용하여 조건부로 JSX를 포함할 수 있습니다.\n\n```js\nlet content;\nif (isLoggedIn) {\n  content = <AdminPanel />;\n} else {\n  content = <LoginForm />;\n}\nreturn (\n  <div>\n    {content}\n  </div>\n);\n```\n\n더욱 간결한 코드를 원한다면 [조건부 삼항 연산자](https://developer.mozilla.org/ko-KR/docs/Web/JavaScript/Reference/Operators/Conditional_Operator)를 사용할 수 있습니다. 이것은 `if` 문과 달리 JSX 내부에서 동작합니다.\n\n```js\n<div>\n  {isLoggedIn ? (\n    <AdminPanel />\n  ) : (\n    <LoginForm />\n  )}\n</div>\n```\n\n`else` 분기가 필요하지 않으면 더 짧은 [`&& 연산자`](https://developer.mozilla.org/ko-KR/docs/Web/JavaScript/Reference/Operators/Logical_AND#short-circuit_evaluation)를 사용할 수도 있습니다.\n\n```js\n<div>\n  {isLoggedIn && <AdminPanel />}\n</div>\n```\n\n이러한 접근 방식은 어트리뷰트를 조건부로 지정할 때도 동작합니다. 이러한 자바스크립트 문법에 익숙하지 않다면 항상 `if...else`를 사용하는 것으로 시작할 수 있습니다.\n\n## 리스트 렌더링하기 {/*rendering-lists*/}\n\n컴포넌트 리스트를 렌더링하기 위해서는 [`for` 문](https://developer.mozilla.org/ko-KR/docs/Web/JavaScript/Reference/Statements/for) 및 [`map()` 함수](https://developer.mozilla.org/ko-KR/docs/Web/JavaScript/Reference/Global_Objects/Array/map)와 같은 자바스크립트 기능을 사용합니다.\n\n예를 들어 여러 제품이 있다고 가정하겠습니다.\n\n```js\nconst products = [\n  { title: 'Cabbage', id: 1 },\n  { title: 'Garlic', id: 2 },\n  { title: 'Apple', id: 3 },\n];\n```\n\n컴포넌트 내에서 `map()` 함수를 사용하여 제품 배열을 `<li>` 항목 배열로 변환합니다.\n```js\nconst listItems = products.map(product =>\n  <li key={product.id}>\n    {product.title}\n  </li>\n);\n\nreturn (\n  <ul>{listItems}</ul>\n);\n```\n\n`<li>`에 `key` 어트리뷰트가 있는 것을 주목하세요. 목록의 각 항목에 대해, 형제 항목 사이에서 해당 항목을 고유하게 식별하는 문자열 또는 숫자를 전달해야 합니다. React는 나중에 항목을 삽입, 삭제 또는 재정렬할 때 어떤 일이 일어났는지 알기 위해 `key`를 사용합니다.\n\n<Sandpack>\n\n```js\nconst products = [\n  { title: 'Cabbage', isFruit: false, id: 1 },\n  { title: 'Garlic', isFruit: false, id: 2 },\n  { title: 'Apple', isFruit: true, id: 3 },\n];\n\nexport default function ShoppingList() {\n  const listItems = products.map(product =>\n    <li\n      key={product.id}\n      style={{\n        color: product.isFruit ? 'magenta' : 'darkgreen'\n      }}\n    >\n      {product.title}\n    </li>\n  );\n\n  return (\n    <ul>{listItems}</ul>\n  );\n}\n```\n\n</Sandpack>\n\n## 이벤트에 응답하기 {/*responding-to-events*/}\n\n컴포넌트 내부에 *이벤트 핸들러* 함수를 선언하여 이벤트에 응답할 수 있습니다.\n\n```js {2-4,7}\nfunction MyButton() {\n  function handleClick() {\n    alert('You clicked me!');\n  }\n\n  return (\n    <button onClick={handleClick}>\n      Click me\n    </button>\n  );\n}\n```\n\n`onClick={handleClick}`의 끝에 소괄호(`()`)가 없는 것을 주목하세요! 이벤트 핸들러 함수를 *호출*하지 않고 *전달*만 하면 됩니다. React는 사용자가 버튼을 클릭할 때 이벤트 핸들러를 호출합니다.\n\n## 화면 업데이트하기 {/*updating-the-screen*/}\n\n컴포넌트가 특정 정보를 \"기억\"하여 표시하기를 원하는 경우가 종종 있습니다. 예를 들어 버튼이 클릭된 횟수를 세고 싶을 수 있습니다. 이렇게 하려면 컴포넌트에 *State*를 추가하면 됩니다.\n\n먼저, React에서 [`useState`](/reference/react/useState)를 가져옵니다.\n\n```js\nimport { useState } from 'react';\n```\n\n이제 컴포넌트 내부에 *State 변수*를 선언할 수 있습니다.\n\n```js\nfunction MyButton() {\n  const [count, setCount] = useState(0);\n  // ...\n```\n\n`useState`로부터 현재 State (`count`)와 이를 업데이트할 수 있는 함수 (`setCount`)를 얻을 수 있습니다. 이들을 어떤 이름으로도 지정할 수 있지만 `[something, setSomething]`으로 작성하는 것이 일반적입니다.\n\n버튼이 처음 표시될 때는 `useState()`에 `0`을 전달했기 때문에 `count`가 `0`이 됩니다. State를 변경하고 싶다면 `setCount()`를 실행하고 새 값을 전달하세요. 이 버튼을 클릭하면 카운터가 증가합니다.\n\n```js {5}\nfunction MyButton() {\n  const [count, setCount] = useState(0);\n\n  function handleClick() {\n    setCount(count + 1);\n  }\n\n  return (\n    <button onClick={handleClick}>\n      Clicked {count} times\n    </button>\n  );\n}\n```\n\nReact가 컴포넌트 함수를 다시 호출합니다. 이번에는 `count`가 `1`이 되고, 그 다음에는 `2`가 될 것입니다. 이런 방식입니다.\n\n같은 컴포넌트를 여러 번 렌더링하면 각각의 컴포넌트는 고유한 State를 얻게 됩니다. 각 버튼을 개별적으로 클릭해 보세요.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function MyApp() {\n  return (\n    <div>\n      <h1>Counters that update separately</h1>\n      <MyButton />\n      <MyButton />\n    </div>\n  );\n}\n\nfunction MyButton() {\n  const [count, setCount] = useState(0);\n\n  function handleClick() {\n    setCount(count + 1);\n  }\n\n  return (\n    <button onClick={handleClick}>\n      Clicked {count} times\n    </button>\n  );\n}\n```\n\n```css\nbutton {\n  display: block;\n  margin-bottom: 5px;\n}\n```\n\n</Sandpack>\n\n각 버튼이 고유한 `count` State를 \"기억\"하고 다른 버튼에 영향을 주지 않는 방식에 주목해 주세요.\n\n## Hook 사용하기 {/*using-hooks*/}\n\n`use`로 시작하는 함수를 *Hook*이라고 합니다. `useState`는 React에서 제공하는 내장 Hook입니다. 다른 내장 Hook은 [API 참고서](/reference/react)에서 찾아볼 수 있습니다. 또한 기존의 것들을 조합하여 자신만의 Hook을 작성할 수도 있습니다.\n\nHook은 다른 함수보다 더 제한적입니다. 컴포넌트(또는 다른 Hook)의 *상단*에서만 Hook을 호출할 수 있습니다. 조건이나 반복에서 `useState`를 사용하고 싶다면 새 컴포넌트를 추출하여 그곳에 넣으세요.\n\n## 컴포넌트 간에 데이터 공유하기 {/*sharing-data-between-components*/}\n\n이전 예시에서는 각각의 `MyButton`에 독립적인 `count`가 있었고, 각 버튼을 클릭하면 클릭한 버튼의 `count`만 변경되었습니다.\n\n<DiagramGroup>\n\n<Diagram name=\"sharing_data_child\" height={367} width={407} alt=\"Diagram showing a tree of three components, one parent labeled MyApp and two children labeled MyButton. Both MyButton components contain a count with value zero.\">\n\n처음에 각 `MyButton`의 `count` State는 `0`입니다.\n\n</Diagram>\n\n<Diagram name=\"sharing_data_child_clicked\" height={367} width={407} alt=\"The same diagram as the previous, with the count of the first child MyButton component highlighted indicating a click with the count value incremented to one. The second MyButton component still contains value zero.\" >\n\n첫 번째 `MyButton`이 `count`를 `1`로 업데이트합니다.\n\n</Diagram>\n\n</DiagramGroup>\n\n하지만 *데이터를 공유하고 항상 함께 업데이트하기* 위한 컴포넌트가 필요한 경우가 많습니다.\n\n두 `MyButton` 컴포넌트가 동일한 `count`를 표시하고 함께 업데이트하려면, State를 개별 버튼에서 모든 버튼이 포함된 가장 가까운 컴포넌트로 \"위쪽\"으로 이동해야 합니다.\n\n이 예시에서는 `MyApp`입니다.\n\n<DiagramGroup>\n\n<Diagram name=\"sharing_data_parent\" height={385} width={410} alt=\"Diagram showing a tree of three components, one parent labeled MyApp and two children labeled MyButton. MyApp contains a count value of zero which is passed down to both of the MyButton components, which also show value zero.\" >\n\n처음에 `MyApp`의 `count` State는 `0`이며 두 자식에게 모두 전달합니다.\n\n</Diagram>\n\n<Diagram name=\"sharing_data_parent_clicked\" height={385} width={410} alt=\"The same diagram as the previous, with the count of the parent MyApp component highlighted indicating a click with the value incremented to one. The flow to both of the children MyButton components is also highlighted, and the count value in each child is set to one indicating the value was passed down.\" >\n\n클릭 시 `MyApp`은 `count` State를 `1`로 업데이트하고 두 자식에게 전달합니다.\n\n</Diagram>\n\n</DiagramGroup>\n\n이제 두 버튼 중 하나를 클릭하면 `MyApp`의 `count`가 변경되어 `MyButton`의 카운트가 모두 변경됩니다. 이를 코드로 표현하는 방법은 다음과 같습니다.\n\n먼저 `MyButton`에서 `MyApp`으로 *State를 위로 이동*합니다.\n\n```js {2-6,18}\nexport default function MyApp() {\n  const [count, setCount] = useState(0);\n\n  function handleClick() {\n    setCount(count + 1);\n  }\n\n  return (\n    <div>\n      <h1>Counters that update separately</h1>\n      <MyButton />\n      <MyButton />\n    </div>\n  );\n}\n\nfunction MyButton() {\n  // ... we're moving code from here ...\n}\n\n```\n\n그 다음 공유된 클릭 핸들러와 함께 `MyApp`에서 각 `MyButton`으로 *State를 전달합니다*. 이전에 `<img>`와 같은 기본 제공 태그를 사용했던 것처럼 JSX 중괄호를 사용하여 `MyButton`에 정보를 전달할 수 있습니다.\n\n```js {11-12}\nexport default function MyApp() {\n  const [count, setCount] = useState(0);\n\n  function handleClick() {\n    setCount(count + 1);\n  }\n\n  return (\n    <div>\n      <h1>Counters that update together</h1>\n      <MyButton count={count} onClick={handleClick} />\n      <MyButton count={count} onClick={handleClick} />\n    </div>\n  );\n}\n```\n\n이렇게 전달한 정보를 *Props*라고 합니다. 이제 `MyApp` 컴포넌트는 `count` State와 `handleClick` 이벤트 핸들러를 포함하며, *이 두 가지를 각 버튼에 Props로 전달합니다*.\n\n마지막으로 부모 컴포넌트에서 전달한 Props를 *읽도록* `MyButton`을 변경합니다.\n\n\n```js {1,3}\nfunction MyButton({ count, onClick }) {\n  return (\n    <button onClick={onClick}>\n      Clicked {count} times\n    </button>\n  );\n}\n```\n\n버튼을 클릭하면 `onClick` 핸들러가 실행됩니다. 각 버튼의 `onClick` Prop는 `MyApp` 내부의 `handleClick` 함수로 설정되었으므로 그 안에 있는 코드가 실행됩니다. 이 코드는 `setCount(count + 1)`를 실행하여 `count` State 변수를 증가시킵니다. 새로운 `count` 값은 각 버튼에 Prop로 전달되므로 모든 버튼에는 새로운 값이 표시됩니다. 이를 \"State 끌어올리기\"라고 합니다. State를 위로 이동함으로써 컴포넌트 간에 State를 공유하게 됩니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function MyApp() {\n  const [count, setCount] = useState(0);\n\n  function handleClick() {\n    setCount(count + 1);\n  }\n\n  return (\n    <div>\n      <h1>Counters that update together</h1>\n      <MyButton count={count} onClick={handleClick} />\n      <MyButton count={count} onClick={handleClick} />\n    </div>\n  );\n}\n\nfunction MyButton({ count, onClick }) {\n  return (\n    <button onClick={onClick}>\n      Clicked {count} times\n    </button>\n  );\n}\n```\n\n```css\nbutton {\n  display: block;\n  margin-bottom: 5px;\n}\n```\n\n</Sandpack>\n\n## 다음 단계 {/*next-steps*/}\n\n이제 React 코드를 작성하는 기본적인 방법을 알았습니다!\n\n[자습서](/learn/tutorial-tic-tac-toe)를 확인하여 이를 실습하고 React로 첫 번째 미니 앱을 만들어보세요.\n"
  },
  {
    "path": "src/content/learn/installation.md",
    "content": "---\ntitle: 설치하기\n---\n\n<Intro>\n\nReact는 처음부터 점진적으로 적용할 수 있도록 설계되었으며 필요한 만큼 React를 사용할 수 있습니다. React를 맛보기로 접해보려거나, 간단한 HTML 페이지에 약간의 상호작용을 추가하거나, 복잡한 React 기반의 앱을 시작하고자 하는 경우, 이 섹션을 참고하세요.\n\n</Intro>\n\n<YouWillLearn isChapter={true}>\n\n* [새로운 React 프로젝트를 시작하는 방법](/learn/creating-a-react-app)\n* [기존 프로젝트에 React를 추가하는 방법](/learn/add-react-to-an-existing-project)\n* [에디터를 설정하는 방법](/learn/editor-setup)\n* [React 개발자 도구를 설치하는 방법](/learn/react-developer-tools)\n\n</YouWillLearn>\n\n## React 시도하기 {/*try-react*/}\n\n단순히 React를 사용해 보고 싶다면, 아무것도 설치할 필요 없습니다. 이 샌드박스를 통해 사용해 보세요!\n\n<Sandpack>\n\n```js\nfunction Greeting({ name }) {\n  return <h1>Hello, {name}</h1>;\n}\n\nexport default function App() {\n  return <Greeting name=\"world\" />\n}\n```\n\n</Sandpack>\n\n직접 편집하거나 오른쪽 상단의 \"Fork\" 버튼을 눌러 새 탭에서 열 수 있습니다.\n\nReact 문서의 대부분 페이지에는 이와 같은 샌드박스가 있습니다. React 문서 외에도 [CodeSandbox](https://codesandbox.io/s/new), [StackBlitz](https://stackblitz.com/fork/react), [CodePen](https://codepen.io/pen?template=QWYVwWN) 등의 온라인 샌드박스에서 React를 지원합니다.\n\n### 로컬 환경에서 React 시도하기 {/*try-react-locally*/}\n\n컴퓨터의 로컬 환경에서 React를 사용해 보고 싶다면, 이 [HTML 페이지를 다운로드](https://gist.githubusercontent.com/gaearon/0275b1e1518599bbeafcde4722e79ed1/raw/db72dcbf3384ee1708c4a07d3be79860db04bff0/example.html)하고 에디터와 브라우저에서 열어보세요!\n\n## 새로운 React 프로젝트 시작하기 {/*start-a-new-react-project*/}\n\nReact로 완전히 앱이나 웹사이트를 구축하고 싶다면, [새로운 React 프로젝트를 시작](/learn/creating-a-react-app)하세요.\n\n컴퓨터의 로컬 환경에서 React를 사용해 보고 싶다면, 이 [HTML 페이지를 다운로드](https://gist.githubusercontent.com/gaearon/0275b1e1518599bbeafcde4722e79ed1/raw/db72dcbf3384ee1708c4a07d3be79860db04bff0/example.html)하고 에디터와 브라우저에서 열어보세요!\n\n## 새로운 React 앱 만들기 {/*creating-a-react-app*/}\n\n새로운 React 앱을 만들고 싶다면, [새로운 React 앱 만들기](/learn/creating-a-react-app)에서 권장하는 프레임워크를 사용하여 만들 수 있습니다.\n\n## 처음부터 React 앱 만들기 {/*build-a-react-app-from-scratch*/}\n\n프레임워크가 프로젝트에 맞지 않거나, 자신만의 프레임워크를 구축하고 싶거나, React 앱의 기본을 배우고 싶다면 [처음부터 React 앱 만들기](/learn/build-a-react-app-from-scratch)에서 확인할 수 있습니다.\n\n## 기존 프로젝트에 React 추가하기 {/*add-react-to-an-existing-project*/}\n\n기존 앱이나 웹사이트에 React를 적용하고 싶다면, [기존 프로젝트에 React를 추가](/learn/add-react-to-an-existing-project)하세요.\n\n<Note>\n\n#### Create React App을 사용해야 하나요? {/*should-i-use-create-react-app*/}\n\n아니요. Create React App은 더 이상 사용되지 않습니다. 자세한 정보는 [Create React App 지원 종료](/blog/2025/02/14/sunsetting-create-react-app)에서 확인할 수 있습니다.\n\n</Note>\n\n## 다음 단계 {/*next-steps*/}\n\nReact의 개념 중 가장 중요한 개념들을 살펴보기 위해 [빠르게 시작하기](/learn)로 이동하세요.\n"
  },
  {
    "path": "src/content/learn/javascript-in-jsx-with-curly-braces.md",
    "content": "---\ntitle: 중괄호가 있는 JSX 안에서 자바스크립트 사용하기\n---\n\n<Intro>\n\nJSX를 사용하면 JavaScript 파일에 HTML과 비슷한 마크업을 작성하여 렌더링 로직과 콘텐츠를 같은 곳에 놓을 수 있습니다. 때로는 JavaScript 로직을 추가하거나 해당 마크업 내부의 동적인 프로퍼티를 참조하고 싶을 수 있습니다. 이 상황에서는 JSX에서 중괄호를 사용하여 JavaScript를 사용할 수 있습니다.\n\n</Intro>\n\n<YouWillLearn>\n\n* 따옴표로 문자열을 전달하는 방법\n* 중괄호가 있는 JSX 안에서 JavaScript 변수를 참조하는 방법\n* 중괄호가 있는 JSX 안에서 JavaScript 함수를 호출하는 방법\n* 중괄호가 있는 JSX 안에서 JavaScript 객체를 사용하는 방법\n\n</YouWillLearn>\n\n## 따옴표로 문자열 전달하기 {/*passing-strings-with-quotes*/}\n\n문자열 어트리뷰트를 JSX에 전달하려면 작은따옴표나 큰따옴표로 묶어야 합니다.\n\n<Sandpack>\n\n```js\nexport default function Avatar() {\n  return (\n    <img\n      className=\"avatar\"\n      src=\"https://i.imgur.com/7vQD0fPs.jpg\"\n      alt=\"Gregorio Y. Zara\"\n    />\n  );\n}\n```\n\n```css\n.avatar { border-radius: 50%; height: 90px; }\n```\n\n</Sandpack>\n\n여기에서는 `\"https://i.imgur.com/7vQD0fPs.jpg\"`와 `\"Gregorio Y. Zara\"`가 문자열로 전달되고 있습니다.\n\n그러나 `src` 또는 `alt`를 동적으로 지정하려면 어떻게 해야 할까요? **`\"`와`\"`를 `{`와`}`로 바꿔 JavaScript의 값을 사용할 수 있습니다**.\n\n<Sandpack>\n\n```js\nexport default function Avatar() {\n  const avatar = 'https://i.imgur.com/7vQD0fPs.jpg';\n  const description = 'Gregorio Y. Zara';\n  return (\n    <img\n      className=\"avatar\"\n      src={avatar}\n      alt={description}\n    />\n  );\n}\n```\n\n```css\n.avatar { border-radius: 50%; height: 90px; }\n```\n\n</Sandpack>\n\n이미지를 둥글게 만드는 `\"avatar\"` CSS 클래스 이름을 지정하는 `className=\"avatar\"`와 `avatar`라는 JavaScript 변수의 값을 읽는 `src={avatar}`의 차이점에 주목해야 합니다. 중괄호를 사용하면 마크업에서 바로 JavaScript를 사용할 수 있기 때문입니다.\n\n## 중괄호 사용하기: JavaScript 세계로 연결하는 창 {/*using-curly-braces-a-window-into-the-javascript-world*/}\n\nJSX는 JavaScript를 작성하는 특별한 방법입니다. 중괄호 `{ }` 사이에서 JavaScript를 사용할 수 있습니다. 아래 예시는 `name`을 선언한 다음 `<h1>` 내부에 중괄호로 포함합니다.\n\n<Sandpack>\n\n```js\nexport default function TodoList() {\n  const name = 'Gregorio Y. Zara';\n  return (\n    <h1>{name}'s To Do List</h1>\n  );\n}\n```\n\n</Sandpack>\n\n`name` 값을 `'Gregorio Y. Zara'`에서 `'Hedy Lamarr'`로 변경해 To Do List 제목이 어떻게 변경되는지 확인해봅시다.\n\n`formatDate()`와 같은 함수 호출을 포함해 모든 JavaScript 표현식은 중괄호 사이에서 작동합니다.\n\n<Sandpack>\n\n```js\nconst today = new Date();\n\nfunction formatDate(date) {\n  return new Intl.DateTimeFormat(\n    'en-US',\n    { weekday: 'long' }\n  ).format(date);\n}\n\nexport default function TodoList() {\n  return (\n    <h1>To Do List for {formatDate(today)}</h1>\n  );\n}\n```\n\n</Sandpack>\n\n### 중괄호를 사용하는 곳 {/*where-to-use-curly-braces*/}\n\nJSX 안에서 중괄호는 두 가지 방법으로만 사용할 수 있습니다.\n\n1. JSX 태그 안의 **문자**: `<h1>{name}'s To Do List</h1>`는 작동하지만, `<{tag}>Gregorio Y. Zara's To Do List</{tag}>`는 작동하지 않습니다.\n2. `=` 바로 뒤에 오는 **어트리뷰트**: `src={avatar}`는 `avatar` 변수를 읽지만 `src=\"{avatar}\"`는 `\"{avatar}\"` 문자열을 전달합니다.\n\n## \"이중 중괄호\" 사용하기: JSX의 CSS와 다른 객체 {/*using-double-curlies-css-and-other-objects-in-jsx*/}\n\nJSX에는 문자열, 숫자 및 기타 JavaScript 표현식뿐만 아니라 객체를 전달할 수도 있습니다. 또한 객체는 `{ name: \"Hedy Lamarr\", inventions: 5 }`처럼 중괄호로 표시됩니다. 따라서 JSX에서 객체를 전달하려면 `person={{ name: \"Hedy Lamarr\", inventions: 5 }}`와 같이 다른 중괄호 쌍으로 객체를 감싸야 합니다.\n\nJSX의 인라인 CSS 스타일에서도 볼 수 있습니다. React에서 인라인 스타일을 사용할 필요가 없습니다(CSS class는 대부분 잘 작동합니다). 그러나 인라인 스타일이 필요할 때 `style` 어트리뷰트에 객체를 전달해야 합니다.\n\n<Sandpack>\n\n```js\nexport default function TodoList() {\n  return (\n    <ul style={{\n      backgroundColor: 'black',\n      color: 'pink'\n    }}>\n      <li>Improve the videophone</li>\n      <li>Prepare aeronautics lectures</li>\n      <li>Work on the alcohol-fuelled engine</li>\n    </ul>\n  );\n}\n```\n\n```css\nbody { padding: 0; margin: 0 }\nul { padding: 20px 20px 20px 40px; margin: 0; }\n```\n\n</Sandpack>\n\n`backgroundColor`와 `color` 값을 변경해 보세요.\n\n아래와 같이 작성할 때 중괄호 안에 JavaScript 객체를 볼 수 있습니다.\n\n```js {2-5}\n<ul style={\n  {\n    backgroundColor: 'black',\n    color: 'pink'\n  }\n}>\n```\n\nJSX에서 `{{` 와 `}}` 를 본다면 JSX 중괄호 안의 객체에 불과하다는 것을 알아야 합니다.\n\n<Pitfall>\n\n인라인 `style` 프로퍼티는 캐멀 케이스로 작성됩니다. 예를 들어 HTML에서의 `<ul style=\"background-color: black\">`은 컴포넌트에서 `<ul style={{ backgroundColor: 'black' }}>`로 작성됩니다.\n\n</Pitfall>\n\n## JavaScript 객체와 중괄호에 대해서 더 알아보기 {/*more-fun-with-javascript-objects-and-curly-braces*/}\n\n여러 표현식을 하나의 객체로 옮기고 중괄호 안의 JSX에서 참조할 수 있습니다.\n\n<Sandpack>\n\n```js\nconst person = {\n  name: 'Gregorio Y. Zara',\n  theme: {\n    backgroundColor: 'black',\n    color: 'pink'\n  }\n};\n\nexport default function TodoList() {\n  return (\n    <div style={person.theme}>\n      <h1>{person.name}'s Todos</h1>\n      <img\n        className=\"avatar\"\n        src=\"https://i.imgur.com/7vQD0fPs.jpg\"\n        alt=\"Gregorio Y. Zara\"\n      />\n      <ul>\n        <li>Improve the videophone</li>\n        <li>Prepare aeronautics lectures</li>\n        <li>Work on the alcohol-fuelled engine</li>\n      </ul>\n    </div>\n  );\n}\n```\n\n```css\nbody { padding: 0; margin: 0 }\nbody > div > div { padding: 20px; }\n.avatar { border-radius: 50%; height: 90px; }\n```\n\n</Sandpack>\n\n이 예시에서 `person` 객체는 `name` 문자열과 `theme` 객체를 포함합니다.\n\n```js\nconst person = {\n  name: 'Gregorio Y. Zara',\n  theme: {\n    backgroundColor: 'black',\n    color: 'pink'\n  }\n};\n```\n\n컴포넌트는 `person` 값을 아래와 같이 사용할 수 있습니다.\n\n```js\n<div style={person.theme}>\n  <h1>{person.name}'s Todos</h1>\n```\n\nJSX는 JavaScript를 사용하여 데이터와 논리를 구성할 수 있는 매우 작은 템플릿 언어입니다.\n\n<Recap>\n\n이제 JSX에 대한 거의 모든 것을 알게 되었습니다.\n\n* 따옴표 안의 JSX 어트리뷰트는 문자열로 전달됩니다.\n* 중괄호를 사용하면 JavaScript 논리와 변수를 마크업으로 가져올 수 있습니다.\n* JSX 태그 내부 또는 어트리뷰트의 `=` 뒤에서 작동합니다.\n* `{{` 및 `}}` 는 특별한 문법이 아닙니다. JSX 중괄호 안에 들어 있는 JavaScript 객체입니다.\n\n</Recap>\n\n<Challenges>\n\n#### 실수 고치기 {/*fix-the-mistake*/}\n\n아래 코드는 `Objects are not valid as a React child`라는 오류가 발생합니다.\n\n<Sandpack>\n\n```js\nconst person = {\n  name: 'Gregorio Y. Zara',\n  theme: {\n    backgroundColor: 'black',\n    color: 'pink'\n  }\n};\n\nexport default function TodoList() {\n  return (\n    <div style={person.theme}>\n      <h1>{person}'s Todos</h1>\n      <img\n        className=\"avatar\"\n        src=\"https://i.imgur.com/7vQD0fPs.jpg\"\n        alt=\"Gregorio Y. Zara\"\n      />\n      <ul>\n        <li>Improve the videophone</li>\n        <li>Prepare aeronautics lectures</li>\n        <li>Work on the alcohol-fuelled engine</li>\n      </ul>\n    </div>\n  );\n}\n```\n\n```css\nbody { padding: 0; margin: 0 }\nbody > div > div { padding: 20px; }\n.avatar { border-radius: 50%; height: 90px; }\n```\n\n</Sandpack>\n\n문제를 찾았나요?\n\n<Hint>중괄호 안에 무엇이 있는지 찾아봅시다. 중괄호 안에 올바른 것을 넣고 있나요?</Hint>\n\n<Solution>\n\n이것은 예시가 *객체 자체*를 문자열이 아닌 마크업으로 렌더링하기 때문에 발생합니다. `<h1>{person}'s Todos</h1>`는 `person` 객체 전체를 렌더링하려고 합니다. 원시 객체를 텍스트 콘텐츠로 포함하면 React가 어떻게 표시할지 모르기 때문에 오류가 발생합니다.\n\n문제를 해결하려면 `<h1>{person}'s Todos</h1>`를 `<h1>{person.name}'s Todos</h1>`로 바꾸어야 합니다.\n\n<Sandpack>\n\n```js\nconst person = {\n  name: 'Gregorio Y. Zara',\n  theme: {\n    backgroundColor: 'black',\n    color: 'pink'\n  }\n};\n\nexport default function TodoList() {\n  return (\n    <div style={person.theme}>\n      <h1>{person.name}'s Todos</h1>\n      <img\n        className=\"avatar\"\n        src=\"https://i.imgur.com/7vQD0fPs.jpg\"\n        alt=\"Gregorio Y. Zara\"\n      />\n      <ul>\n        <li>Improve the videophone</li>\n        <li>Prepare aeronautics lectures</li>\n        <li>Work on the alcohol-fuelled engine</li>\n      </ul>\n    </div>\n  );\n}\n```\n\n```css\nbody { padding: 0; margin: 0 }\nbody > div > div { padding: 20px; }\n.avatar { border-radius: 50%; height: 90px; }\n```\n\n</Sandpack>\n\n</Solution>\n\n#### 정보를 객체로 추출하기 {/*extract-information-into-an-object*/}\n\n이미지 URL을 `person` 객체로 추출해 봅시다.\n\n<Sandpack>\n\n```js\nconst person = {\n  name: 'Gregorio Y. Zara',\n  theme: {\n    backgroundColor: 'black',\n    color: 'pink'\n  }\n};\n\nexport default function TodoList() {\n  return (\n    <div style={person.theme}>\n      <h1>{person.name}'s Todos</h1>\n      <img\n        className=\"avatar\"\n        src=\"https://i.imgur.com/7vQD0fPs.jpg\"\n        alt=\"Gregorio Y. Zara\"\n      />\n      <ul>\n        <li>Improve the videophone</li>\n        <li>Prepare aeronautics lectures</li>\n        <li>Work on the alcohol-fuelled engine</li>\n      </ul>\n    </div>\n  );\n}\n```\n\n```css\nbody { padding: 0; margin: 0 }\nbody > div > div { padding: 20px; }\n.avatar { border-radius: 50%; height: 90px; }\n```\n\n</Sandpack>\n\n<Solution>\n\n이미지 URL을 `person.imageUrl`이라는 프로퍼티로 이동하고 중괄호를 사용하여 `<img>` 태그에서 읽습니다.\n\n<Sandpack>\n\n```js\nconst person = {\n  name: 'Gregorio Y. Zara',\n  imageUrl: \"https://i.imgur.com/7vQD0fPs.jpg\",\n  theme: {\n    backgroundColor: 'black',\n    color: 'pink'\n  }\n};\n\nexport default function TodoList() {\n  return (\n    <div style={person.theme}>\n      <h1>{person.name}'s Todos</h1>\n      <img\n        className=\"avatar\"\n        src={person.imageUrl}\n        alt=\"Gregorio Y. Zara\"\n      />\n      <ul>\n        <li>Improve the videophone</li>\n        <li>Prepare aeronautics lectures</li>\n        <li>Work on the alcohol-fuelled engine</li>\n      </ul>\n    </div>\n  );\n}\n```\n\n```css\nbody { padding: 0; margin: 0 }\nbody > div > div { padding: 20px; }\n.avatar { border-radius: 50%; height: 90px; }\n```\n\n</Sandpack>\n\n</Solution>\n\n#### JSX 중괄호 안에 표현식 작성하기 {/*write-an-expression-inside-jsx-curly-braces*/}\n\n아래 객체에서 전체 이미지 URL은 기본 URL, `imageId`, `imageSize` 및 파일 확장자 네 부분으로 나누어져 있습니다.\n\n이미지 URL은 기본 URL (항상 `'https://i.imgur.com/'`), `imageId` (`'7vQD0fP'`), `imageSize` (`'s'`) 및 파일 확장자 (항상 `'.jpg'`)와 같은 어트리뷰트를 결합합니다. 그러나 `<img>` 태그가 `src`를 지정하는 방식에 문제가 있습니다.\n\n어떻게 고칠 수 있을까요?\n\n<Sandpack>\n\n```js\n\nconst baseUrl = 'https://i.imgur.com/';\nconst person = {\n  name: 'Gregorio Y. Zara',\n  imageId: '7vQD0fP',\n  imageSize: 's',\n  theme: {\n    backgroundColor: 'black',\n    color: 'pink'\n  }\n};\n\nexport default function TodoList() {\n  return (\n    <div style={person.theme}>\n      <h1>{person.name}'s Todos</h1>\n      <img\n        className=\"avatar\"\n        src=\"{baseUrl}{person.imageId}{person.imageSize}.jpg\"\n        alt={person.name}\n      />\n      <ul>\n        <li>Improve the videophone</li>\n        <li>Prepare aeronautics lectures</li>\n        <li>Work on the alcohol-fuelled engine</li>\n      </ul>\n    </div>\n  );\n}\n```\n\n```css\nbody { padding: 0; margin: 0 }\nbody > div > div { padding: 20px; }\n.avatar { border-radius: 50%; height: 90px; }\n```\n\n</Sandpack>\n\n수정 사항이 제대로 작동하는지 확인하려면 `imageSize` 값을 `'b'`로 변경해 보세요. 수정 후에 이미지의 크기가 조정되어야 합니다.\n\n<Solution>\n\n`src={baseUrl + person.imageId + person.imageSize + '.jpg'}`같이 작성할 수 있습니다.\n\n1. `{` 는 JavaScript 표현식을 엽니다.\n2. `baseUrl + person.imageId + person.imageSize + '.jpg'` 는 올바른 URL 문자열을 생성합니다.\n3. `}` 는 JavaScript 표현식을 닫습니다.\n\n<Sandpack>\n\n```js\nconst baseUrl = 'https://i.imgur.com/';\nconst person = {\n  name: 'Gregorio Y. Zara',\n  imageId: '7vQD0fP',\n  imageSize: 's',\n  theme: {\n    backgroundColor: 'black',\n    color: 'pink'\n  }\n};\n\nexport default function TodoList() {\n  return (\n    <div style={person.theme}>\n      <h1>{person.name}'s Todos</h1>\n      <img\n        className=\"avatar\"\n        src={baseUrl + person.imageId + person.imageSize + '.jpg'}\n        alt={person.name}\n      />\n      <ul>\n        <li>Improve the videophone</li>\n        <li>Prepare aeronautics lectures</li>\n        <li>Work on the alcohol-fuelled engine</li>\n      </ul>\n    </div>\n  );\n}\n```\n\n```css\nbody { padding: 0; margin: 0 }\nbody > div > div { padding: 20px; }\n.avatar { border-radius: 50%; height: 90px; }\n```\n\n</Sandpack>\n\n이 표현식을 아래의 `getImageUrl`과 같은 별도의 함수로 옮길 수도 있습니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { getImageUrl } from './utils.js'\n\nconst person = {\n  name: 'Gregorio Y. Zara',\n  imageId: '7vQD0fP',\n  imageSize: 's',\n  theme: {\n    backgroundColor: 'black',\n    color: 'pink'\n  }\n};\n\nexport default function TodoList() {\n  return (\n    <div style={person.theme}>\n      <h1>{person.name}'s Todos</h1>\n      <img\n        className=\"avatar\"\n        src={getImageUrl(person)}\n        alt={person.name}\n      />\n      <ul>\n        <li>Improve the videophone</li>\n        <li>Prepare aeronautics lectures</li>\n        <li>Work on the alcohol-fuelled engine</li>\n      </ul>\n    </div>\n  );\n}\n```\n\n```js src/utils.js\nexport function getImageUrl(person) {\n  return (\n    'https://i.imgur.com/' +\n    person.imageId +\n    person.imageSize +\n    '.jpg'\n  );\n}\n```\n\n```css\nbody { padding: 0; margin: 0 }\nbody > div > div { padding: 20px; }\n.avatar { border-radius: 50%; height: 90px; }\n```\n\n</Sandpack>\n\n변수와 함수는 마크업을 단순하게 유지하는 데 도움이 될 수 있습니다!\n\n</Solution>\n\n</Challenges>\n"
  },
  {
    "path": "src/content/learn/keeping-components-pure.md",
    "content": "---\ntitle: 컴포넌트를 순수하게 유지하기\n---\n\n<Intro>\n\n일부 자바스크립트 함수는 *순수*합니다. 순수 함수는 오직 연산만을 수행합니다. 컴포넌트를 엄격하게 순수 함수로만 작성하면 코드베이스의 규모가 점점 커지더라도 예상밖의 동작이나 당황케하는 버그를 피할 수 있습니다. 이런 효과를 얻기 위해서는 몇 가지 규칙을 따라야 합니다.\n\n</Intro>\n\n<YouWillLearn>\n\n* 순수성이란 무엇인지 그리고, 순수성을 통해 버그를 피하는 방법\n* 렌더링 단계에서 변화를 막아 컴포넌트의 순수성을 유지하는 방법\n* 엄격 모드(Strict Mode)를 활용해 컴포넌트 속 실수를 발견하는 방법\n\n</YouWillLearn>\n\n## 순수성: 수식으로서의 컴포넌트 {/*purity-components-as-formulas*/}\n\n컴퓨터 과학에서(특히 함수형 프로그래밍의 세계에서는) [순수 함수](https://wikipedia.org/wiki/Pure_function)는 다음과 같은 특징을 지니고 있습니다.\n\n* **자신의 일에만 집중합니다.** 함수가 호출되기 전에 존재했던 어떤 객체나 변수도 변경하지 않습니다.\n* **같은 입력, 같은 출력.** 같은 입력이 주어졌다면 순수 함수는 항상 같은 결과를 반환합니다.\n\n여러분들에게 이미 익숙할 수학 수식은 순수 함수의 예시 중 하나입니다.\n\n이 수학 공식을 생각해보세요. <Math><MathI>y</MathI> = 2<MathI>x</MathI></Math>.\n\n만약 <Math><MathI>x</MathI> = 2</Math>이라면 항상 <Math><MathI>y</MathI> = 4</Math>입니다.\n\n만약 <Math><MathI>x</MathI> = 3</Math>이라면 항상 <Math><MathI>y</MathI> = 6</Math>입니다.\n\n만약 <Math><MathI>x</MathI> = 3</Math>이라면, 그날의 시간이나 주식 시장의 상태에 따라 <MathI>y</MathI>가 갑자기 <Math>9</Math>가 되거나 <Math>–1</Math>이 되거나 <Math>2.5</Math>가 되는 일은 일어나지 않습니다.\n\n만약 <Math><MathI>y</MathI> = 2<MathI>x</MathI></Math> 그리고 <Math><MathI>x</MathI> = 3</Math>이라면, <MathI>y</MathI>는 _항상_ <Math>6</Math>이 될 것입니다.\n\n위 내용들을 자바스크립트 함수로 만든다면 아래와 같습니다.\n\n```js\nfunction double(number) {\n  return 2 * number;\n}\n```\n\n위 예시에서, `double`은 **순수 함수**입니다. 인자로 `3`을 넘긴다면, 항상 `6`을 반환할 것입니다.\n\nReact는 이러한 개념을 기반으로 설계되었습니다. **React는 여러분이 작성하는 모든 컴포넌트가 순수 함수라고 가정합니다.** 이러한 가정은 React 컴포넌트에 같은 입력이 주어진다면 늘 같은 JSX를 반환한다는 것을 의미합니다.\n\n<Sandpack>\n\n```js src/App.js\nfunction Recipe({ drinkers }) {\n  return (\n    <ol>\n      <li>Boil {drinkers} cups of water.</li>\n      <li>Add {drinkers} spoons of tea and {0.5 * drinkers} spoons of spice.</li>\n      <li>Add {0.5 * drinkers} cups of milk to boil and sugar to taste.</li>\n    </ol>\n  );\n}\n\nexport default function App() {\n  return (\n    <section>\n      <h1>Spiced Chai Recipe</h1>\n      <h2>For two</h2>\n      <Recipe drinkers={2} />\n      <h2>For a gathering</h2>\n      <Recipe drinkers={4} />\n    </section>\n  );\n}\n```\n\n</Sandpack>\n\n`Recipe`에 `drinkers={2}`를 넘기면 항상 `2 cups of water`를 포함한 JSX 반환합니다.\n\n`drinkers={4}`를 넘기면 항상 `4 cups of water`를 포함한 JSX를 반환합니다.\n\n수학 공식처럼 말입니다.\n\n컴포넌트를 마치 레시피라고 생각할 수도 있습니다. 레시피를 그대로 따르고 요리 중 새로운 재료를 추가하지 않으면 매번 동일한 요리를 만들 수 있습니다. 여기서 \"요리\"는 컴포넌트가 React에 전달하여 [렌더링](/learn/render-and-commit)하도록 하는 JSX입니다.\n\n<Illustration src=\"/images/docs/illustrations/i_puritea-recipe.png\" alt=\"A tea recipe for x people: take x cups of water, add x spoons of tea and 0.5x spoons of spices, and 0.5x cups of milk\" />\n\n## 사이드 이펙트: 의도하지(않은) 결과 {/*side-effects-unintended-consequences*/}\n\nReact의 렌더링 과정은 항상 순수해야 합니다. 컴포넌트는 JSX만 반환해야 하며, 렌더링 이전에 존재했던 어떤 객체나 변수도 변경해서는 안 됩니다. 그것은 컴포넌트를 순수하지 않게 만듭니다!\n\n아래는 이러한 규칙을 위반하는 컴포넌트입니다.\n\n<Sandpack>\n\n```js\nlet guest = 0;\n\nfunction Cup() {\n  // 나쁜 지점: 이미 존재했던 변수를 변경하고 있습니다!\n  guest = guest + 1;\n  return <h2>Tea cup for guest #{guest}</h2>;\n}\n\nexport default function TeaSet() {\n  return (\n    <>\n      <Cup />\n      <Cup />\n      <Cup />\n    </>\n  );\n}\n```\n\n</Sandpack>\n\n이 컴포넌트는 컴포넌트 외부에 선언된 `guest`라는 변수를 읽고 수정하고 있습니다. 이는 **해당 컴포넌트를 여러 번 호출할 때마다 서로 다른 JSX를 생성한다**는 것을 의미합니다! 게다가 _다른_ 컴포넌트들이 `guest`를 읽는다면, 각각 언제 렌더링 되었는지에 따라 서로 다른 JSX를 생성할 것입니다! 이것은 예측 불가능한 동작입니다.\n\n<Math><MathI>y</MathI> = 2<MathI>x</MathI></Math>라는 수식으로 돌아가서, 이제 우리는 <Math><MathI>x</MathI> = 2</Math>라 하더라도 항상 <Math><MathI>y</MathI> = 4</Math>일 것이라고 확신할 수 없습니다. 우리의 테스트가 실패하거나, 사용자는 불편을 겪으며, 심지어 비행기까지 추락하게 만들 수도 있습니다. 순수하지 못한 코드가 얼마나 혼란스러운 버그로 이어질 수 있는지 아시겠나요?\n\n대신, [`guest` 변수를 Prop으로 넘겨](/learn/passing-props-to-a-component)이 컴포넌트를 고칠 수 있습니다.\n\n<Sandpack>\n\n```js\nfunction Cup({ guest }) {\n  return <h2>Tea cup for guest #{guest}</h2>;\n}\n\nexport default function TeaSet() {\n  return (\n    <>\n      <Cup guest={1} />\n      <Cup guest={2} />\n      <Cup guest={3} />\n    </>\n  );\n}\n```\n\n</Sandpack>\n\n이제 컴포넌트가 `guest` prop에만 의존해 JSX를 반환하므로 순수해졌습니다.\n\n일반적으로 컴포넌트가 특정 순서대로 렌더링될 것이라고 기대하면 안됩니다. <Math><MathI>y</MathI> = 2<MathI>x</MathI></Math>를 <Math><MathI>y</MathI> = 5<MathI>x</MathI></Math>보다 먼저 계산하든 나중에 계산하든 상관없습니다. 두 수식은 서로 독립적으로 결과를 도출하기 때문입니다. 마찬가지로 각 컴포넌트는 \"자기 자신만 생각\"해야 합니다. 렌더링 도중에 다른 컴포넌트와 영향을 주고받거나, 의존해서는 안됩니다. 렌더링은 마치 학교 시험과 같습니다. 각 컴포넌트는 자신의 JSX를 직접 계산해야 합니다!\n\n<DeepDive>\n\n#### 엄격 모드(Strict Mode)로 순수하지 않은 연산을 찾아내기 {/*detecting-impure-calculations-with-strict-mode*/}\n\n아직 전부 사용해본 적은 없을 수 있지만, React에서는 렌더링하는 동안 읽을 수 있는 세 가지 종류의 입력 요소가 있습니다. [Props](/learn/passing-props-to-a-component), [State](/learn/state-a-components-memory), 그리고 [Context](/learn/passing-data-deeply-with-context). 이러한 입력 요소는 항상 읽기전용으로 취급해야 합니다.\n\n사용자의 입력에 따라 무언가를 _변경_ 하려는 경우, 변수 값을 직접 수정하는 대신 [State](/learn/state-a-components-memory)를 설정(set)해야 합니다. 컴포넌트가 렌더링되는 동안엔 기존 변수나 객체를 변경하면 안됩니다.\n\nReact는 개발 중에 각 컴포넌트의 함수를 두 번 호출하는 \"엄격 모드\"를 제공합니다. **컴포넌트 함수를 두 번 호출함으로써, 엄격 모드는 이러한 규칙을 위반하는 컴포넌트를 찾는데 도움을 줍니다.**\n\n원래 예시에서 \"Guest #1\", \"Guest #2\", \"Guest #3\" 대신 \"Guest #2\", \"Guest #4\", \"Guest #6\"이 어떻게 표시되었는지 확인해보세요. 기존 함수가 순수하지 않았기에 엄격 모드로 인해 두 번 호출되는 과정에서 로직이 깨져버렸습니다. 그러나 수정된 순수 버전의 함수는 두 번씩 호출되더라도 동작합니다. **순수 함수는 오직 계산만 수행하므로 두 번 호출되더라도 아무것도 변하지 않습니다.** `double(2)`를 두 번 호출해도 반환값은 변하지 않는 것과 <Math><MathI>y</MathI> = 2<MathI>x</MathI></Math>을 두 번 푼다고 해도 <MathI>y</MathI>값이 바뀌지는 않는 것처럼, 항상 같은 입력이면 같은 출력을 내보냅니다.\n\n엄격 모드는 프로덕션에 영향을 주지 않기 때문에 사용자의 앱 속도가 느려지지 않습니다. 엄격 모드를 적용하려면 최상단 컴포넌트를 `<React.StrictMode>`로 감싸면 됩니다. 몇몇 프레임워크에는 이것이 기본적으로 설정되어 있습니다.\n\n</DeepDive>\n\n### 지역 변경: 컴포넌트의 작은 비밀 {/*local-mutation-your-components-little-secret*/}\n\n위 예시에서의 문제는 컴포넌트가 기존 변수를 렌더링 중에 변경했다는 것입니다. 이것은 \"**변경<sup>Mutation</sup>**\"으로 불려 더 무시무시하게 들립니다. 순수 함수는 함수 스코프 밖의 변수나 호출 전에 생성된 객체를 변경하지 않습니다.\n\n그러나, **렌더링하는 동안 _방금_ 생성한 변수와 객체를 변경하는 것은 전혀 문제가 없습니다.** 이번 예시에서는, `[]` 배열을 만들고, `cups` 변수에 할당하고, 컵 한 묶음을 `push` 할 것입니다.\n\n<Sandpack>\n\n```js\nfunction Cup({ guest }) {\n  return <h2>Tea cup for guest #{guest}</h2>;\n}\n\nexport default function TeaGathering() {\n  const cups = [];\n  for (let i = 1; i <= 12; i++) {\n    cups.push(<Cup key={i} guest={i} />);\n  }\n  return cups;\n}\n```\n\n</Sandpack>\n\n만약 `cups` 변수나 `[]` 배열이 `TeaGathering`의 바깥에서 생성되었다면 정말 큰 문제가 될 겁니다! 배열에 요소들을 `push`하면 _이미 존재하던_ 객체가 직접 변경되기 때문입니다.\n\n하지만 위 예시에서는 동일한 렌더링 과정 중에 `TeaGathering` 내부에서 변수와 배열이 생성되었기 때문에 괜찮습니다. `TeaGathering` 외부에 있는 코드들은 이런 일이 생겼는지도 모릅니다. 이를 **\"지역 변경\"** 이라 하며, 이것은 컴포넌트의 작은 비밀과도 같습니다.\n\n## 사이드 이펙트를 _일으킬 수 있는_ 지점 {/*where-you-_can_-cause-side-effects*/}\n\n함수형 프로그래밍은 순수성에 크게 의존하지만, 결국 어느 시점에 어디선가 _무언가는_ 바뀌어야 합니다. 그것이 프로그래밍의 요점입니다! 화면을 업데이트하고, 애니메이션을 시작하고, 데이터를 변경하는 이러한 변화들을 **사이드 이펙트**라고 합니다. 렌더링 중에 발생하는 것이 아니라 _\"사이드에서\"_ 발생하는 현상입니다.\n\nReact에서, **사이드 이펙트는 보통 [이벤트 핸들러](/learn/responding-to-events) 내부에 위치합니다. **이벤트 핸들러는 React가 일부 작업을 수행할 때 반응하는 함수들입니다. 예를 들면 버튼을 클릭할 때처럼 말이죠. 이벤트 핸들러가 컴포넌트 _내부에_ 정의되었다 하더라도 렌더링 _중에는_ 실행되지 않습니다! **그래서 이벤트 핸들러는 순수할 필요가 없습니다.**\n\n다른 옵션을 모두 사용했음에도 사이드 이펙트를 처리할 적합한 이벤트 핸들러를 찾지 못했다면, 컴포넌트에서 [`useEffect`](/reference/react/useEffect)를 호출해 반환된 JSX에 해당 사이드 이펙트를 연결할 수 있습니다. 이렇게 하면 React가 렌더링을 마치고 사이드 이펙트가 허용된 시점에 그것을 실행하도록 만듭니다. **그러나 이 방식은 최후의 수단이 되어야 합니다.**\n\n가능하면 렌더링만으로 로직을 표현해 보세요. 이것이 당신을 얼마나 더 나아가게 할 수 있는지 알면 놀라게 될 것입니다!\n\n<DeepDive>\n\n#### React는 왜 순수성을 신경쓸까요? {/*why-does-react-care-about-purity*/}\n\n순수 함수를 작성하려면 약간의 습관과 훈련이 필요합니다. 그러나 이건 또한 놀라운 기회를 열어줍니다.\n\n* 컴포넌트는 다른 환경에서도 실행될 수 있습니다. 예를 들면 서버에서 말이죠! 동일한 입력에 대해 동일한 결과를 반환하기 때문에 하나의 컴포넌트는 많은 사용자 요청을 처리할 수 있습니다.\n* 입력이 변경되지 않은 컴포넌트들은 [렌더링을 건너뛰어](/reference/react/memo) 성능을 향상시킬 수 있습니다. 순수 함수는 항상 동일한 결과를 반환하므로 캐싱하기에 안전합니다.\n* 깊은 컴포넌트 트리를 렌더링하는 도중에 일부 데이터가 변경되는 경우, React는 무의미해진 렌더링을 끝내는 데 시간을 낭비하지 않고 렌더링을 아예 다시 시작할 수 있습니다. 순수성은 언제든지 연산을 중단하는 것을 안전하게 합니다.\n\n우리가 구축하고 있는 모든 새로운 React 기능은 순수성을 활용합니다. 데이터 가져오기에서부터 애니메이션, 성능에 이르기까지 컴포넌트를 순수하게 유지하면 React 패러다임의 힘이 발휘됩니다.\n\n</DeepDive>\n\n<Recap>\n\n* 컴포넌트는 순수해야만 합니다. 이것은 두 가지를 의미합니다.\n  * **자신의 일에만 집중합니다.** 렌더링 전에 존재했던 객체나 변수를 변경하지 않아야 합니다.\n  * **같은 입력, 같은 출력.** 입력이 같을 경우, 컴포넌트는 항상 같은 JSX를 반환해야 합니다.\n* 렌더링은 언제든지 발생할 수 있으므로 컴포넌트는 서로의 렌더링 순서에 의존하지 않아야 합니다.\n* 컴포넌트가 렌더링을 위해 사용하는 입력을 변경해서는 안됩니다. 여기에는 Props, State, Context가 포함됩니다. 화면을 업데이트하려면 기존 객체를 변경하는 대신 [State를 \"set\"](/learn/state-a-components-memory)하세요.\n* 반환하는 JSX에서 컴포넌트의 로직을 표현하기 위해 노력하세요. \"무언가를 변경\"해야 할 경우 일반적으로 이벤트 핸들러에서 변경하고 싶을 것입니다. 최후의 수단으로 `useEffect`를 사용할 수 있습니다.\n* 순수 함수를 작성하는 것은 약간의 연습이 필요하지만, React 패러다임의 힘을 발휘하게 합니다.\n\n</Recap>\n\n<Challenges>\n\n#### 고장난 시계 고치기 {/*fix-a-broken-clock*/}\n\n이 컴포넌트는 자정부터 아침 6시까지의 시간에는 `<h1>`의 CSS 클래스를 `\"night\"`로 설정하고 그 외에 시간에는 `\"day\"`로 설정하려고 합니다. 하지만 이건 동작하지 않습니다. 이 컴포넌트를 고칠 수 있나요?\n\n컴퓨터의 시간대를 일시적으로 변경하여 정답이 동작하는지 확인할 수 있습니다. 현재 시간이 자정에서 아침 6시 사이이면 시계의 색상이 반전되어야 합니다!\n\n<Hint>\n\n렌더링은 *연산*일 뿐이며, 무언가를 직접 \"수행\"하려고 해서는 안 됩니다. 같은 생각을 다르게 표현할 수 있나요?\n\n</Hint>\n\n<Sandpack>\n\n```js src/Clock.js active\nexport default function Clock({ time }) {\n  const hours = time.getHours();\n  if (hours >= 0 && hours <= 6) {\n    document.getElementById('time').className = 'night';\n  } else {\n    document.getElementById('time').className = 'day';\n  }\n  return (\n    <h1 id=\"time\">\n      {time.toLocaleTimeString()}\n    </h1>\n  );\n}\n```\n\n```js src/App.js hidden\nimport { useState, useEffect } from 'react';\nimport Clock from './Clock.js';\n\nfunction useTime() {\n  const [time, setTime] = useState(() => new Date());\n  useEffect(() => {\n    const id = setInterval(() => {\n      setTime(new Date());\n    }, 1000);\n    return () => clearInterval(id);\n  }, []);\n  return time;\n}\n\nexport default function App() {\n  const time = useTime();\n  return (\n    <Clock time={time} />\n  );\n}\n```\n\n```css\nbody > * {\n  width: 100%;\n  height: 100%;\n}\n.day {\n  background: #fff;\n  color: #222;\n}\n.night {\n  background: #222;\n  color: #fff;\n}\n```\n\n</Sandpack>\n\n<Solution>\n\n`className`을 연산하고 렌더링 출력에 포함해서 이 컴포넌트를 고칠 수 있습니다.\n\n<Sandpack>\n\n```js src/Clock.js active\nexport default function Clock({ time }) {\n  const hours = time.getHours();\n  let className;\n  if (hours >= 0 && hours <= 6) {\n    className = 'night';\n  } else {\n    className = 'day';\n  }\n  return (\n    <h1 className={className}>\n      {time.toLocaleTimeString()}\n    </h1>\n  );\n}\n```\n\n```js src/App.js hidden\nimport { useState, useEffect } from 'react';\nimport Clock from './Clock.js';\n\nfunction useTime() {\n  const [time, setTime] = useState(() => new Date());\n  useEffect(() => {\n    const id = setInterval(() => {\n      setTime(new Date());\n    }, 1000);\n    return () => clearInterval(id);\n  }, []);\n  return time;\n}\n\nexport default function App() {\n  const time = useTime();\n  return (\n    <Clock time={time} />\n  );\n}\n```\n\n```css\nbody > * {\n  width: 100%;\n  height: 100%;\n}\n.day {\n  background: #fff;\n  color: #222;\n}\n.night {\n  background: #222;\n  color: #fff;\n}\n```\n\n</Sandpack>\n\n이 예시에서는 사이드 이펙트(DOM 수정)가 전혀 필요하지 않았습니다. JSX만 반환하면 됩니다.\n\n</Solution>\n\n#### 망가진 프로필 고치기 {/*fix-a-broken-profile*/}\n\n두 개의 `Profile` 컴포넌트는 각각 다른 데이터로 나란히 렌더링됩니다. 첫 번째 프로필에서 \"Collapse\"를 누른 다음 \"Expand\"를 누릅니다. 이제 두 프로필에 동일한 사람이 표시될 것입니다. 이것은 버그입니다.\n\n버그의 원인을 찾아서 고쳐보세요.\n\n<Hint>\n\n버그가 있는 코드는 `Profile.js`에 있습니다. 처음부터 끝까지 읽으세요!\n\n</Hint>\n\n<Sandpack>\n\n```js src/Profile.js\nimport Panel from './Panel.js';\nimport { getImageUrl } from './utils.js';\n\nlet currentPerson;\n\nexport default function Profile({ person }) {\n  currentPerson = person;\n  return (\n    <Panel>\n      <Header />\n      <Avatar />\n    </Panel>\n  )\n}\n\nfunction Header() {\n  return <h1>{currentPerson.name}</h1>;\n}\n\nfunction Avatar() {\n  return (\n    <img\n      className=\"avatar\"\n      src={getImageUrl(currentPerson)}\n      alt={currentPerson.name}\n      width={50}\n      height={50}\n    />\n  );\n}\n```\n\n```js src/Panel.js hidden\nimport { useState } from 'react';\n\nexport default function Panel({ children }) {\n  const [open, setOpen] = useState(true);\n  return (\n    <section className=\"panel\">\n      <button onClick={() => setOpen(!open)}>\n        {open ? 'Collapse' : 'Expand'}\n      </button>\n      {open && children}\n    </section>\n  );\n}\n```\n\n```js src/App.js\nimport Profile from './Profile.js';\n\nexport default function App() {\n  return (\n    <>\n      <Profile person={{\n        imageId: 'lrWQx8l',\n        name: 'Subrahmanyan Chandrasekhar',\n      }} />\n      <Profile person={{\n        imageId: 'MK3eW3A',\n        name: 'Creola Katherine Johnson',\n      }} />\n    </>\n  )\n}\n```\n\n```js src/utils.js hidden\nexport function getImageUrl(person, size = 's') {\n  return (\n    'https://i.imgur.com/' +\n    person.imageId +\n    size +\n    '.jpg'\n  );\n}\n```\n\n```css\n.avatar { margin: 5px; border-radius: 50%; }\n.panel {\n  border: 1px solid #aaa;\n  border-radius: 6px;\n  margin-top: 20px;\n  padding: 10px;\n  width: 200px;\n}\nh1 { margin: 5px; font-size: 18px; }\n```\n\n</Sandpack>\n\n<Solution>\n\n문제는 `Profile` 컴포넌트가 기존 변수인 `currentPerson`를 수정하고 `Header` 및 `Avatar` 컴포넌트가 이 변수를 읽는다는 점입니다. 이것은 *세 가지 모두*를 순수하지 않게 만들고 예측하기 어렵게 만듭니다.\n\n버그를 수정하려면 `currentPerson` 변수를 제거하세요. 대신, Props를 통해 `Profile`의 모든 정보를 `Header` 및 `Avatar`로 전달하세요. 두 컴포넌트에 `person` 프로퍼티를 추가하여 모든 하위 컴포넌트로 전달해야 합니다.\n\n<Sandpack>\n\n```js src/Profile.js active\nimport Panel from './Panel.js';\nimport { getImageUrl } from './utils.js';\n\nexport default function Profile({ person }) {\n  return (\n    <Panel>\n      <Header person={person} />\n      <Avatar person={person} />\n    </Panel>\n  )\n}\n\nfunction Header({ person }) {\n  return <h1>{person.name}</h1>;\n}\n\nfunction Avatar({ person }) {\n  return (\n    <img\n      className=\"avatar\"\n      src={getImageUrl(person)}\n      alt={person.name}\n      width={50}\n      height={50}\n    />\n  );\n}\n```\n\n```js src/Panel.js hidden\nimport { useState } from 'react';\n\nexport default function Panel({ children }) {\n  const [open, setOpen] = useState(true);\n  return (\n    <section className=\"panel\">\n      <button onClick={() => setOpen(!open)}>\n        {open ? 'Collapse' : 'Expand'}\n      </button>\n      {open && children}\n    </section>\n  );\n}\n```\n\n```js src/App.js\nimport Profile from './Profile.js';\n\nexport default function App() {\n  return (\n    <>\n      <Profile person={{\n        imageId: 'lrWQx8l',\n        name: 'Subrahmanyan Chandrasekhar',\n      }} />\n      <Profile person={{\n        imageId: 'MK3eW3A',\n        name: 'Creola Katherine Johnson',\n      }} />\n    </>\n  );\n}\n```\n\n```js src/utils.js hidden\nexport function getImageUrl(person, size = 's') {\n  return (\n    'https://i.imgur.com/' +\n    person.imageId +\n    size +\n    '.jpg'\n  );\n}\n```\n\n```css\n.avatar { margin: 5px; border-radius: 50%; }\n.panel {\n  border: 1px solid #aaa;\n  border-radius: 6px;\n  margin-top: 20px;\n  padding: 10px;\n  width: 200px;\n}\nh1 { margin: 5px; font-size: 18px; }\n```\n\n</Sandpack>\n\nReact는 컴포넌트 함수가 특정 순서로 실행된다는 것을 보장하지 않기 때문에, 컴포넌트 함수 간에 변수로 소통할 수 없습니다. 모든 소통은 프로퍼티를 통해 이루어져야 합니다.\n\n</Solution>\n\n#### 깨진 StoryTray 수리하기 {/*fix-a-broken-story-tray*/}\n\n회사의 CEO가 온라인 시계 앱에 \"stories\"를 추가해 달라고 요청했는데 거절할 수 없는 상황입니다. \"Create Story\" 플레이스홀더 뒤에 `stories` 목록을 받는 `StoryTray`컴포넌트를 작성했습니다.\n\n프로퍼티로 받는 `stories` 배열 마지막에 가짜 story를 하나 더 추가해서 \"Create Story\" 플레이스홀더를 구현했습니다. 하지만 어떤 이유에서인지 \"Create Story\"는 한 번 이상 등장합니다. 이 문제를 해결해보세요.\n\n<Sandpack>\n\n```js src/StoryTray.js active\nexport default function StoryTray({ stories }) {\n  stories.push({\n    id: 'create',\n    label: 'Create Story'\n  });\n\n  return (\n    <ul>\n      {stories.map(story => (\n        <li key={story.id}>\n          {story.label}\n        </li>\n      ))}\n    </ul>\n  );\n}\n```\n\n```js src/App.js hidden\nimport { useState, useEffect } from 'react';\nimport StoryTray from './StoryTray.js';\n\nconst initialStories = [\n  {id: 0, label: \"Ankit's Story\" },\n  {id: 1, label: \"Taylor's Story\" },\n];\n\nexport default function App() {\n  const [stories, setStories] = useState([...initialStories])\n  const time = useTime();\n\n  // HACK: Prevent the memory from growing forever while you read docs.\n  // We're breaking our own rules here.\n  if (stories.length > 100) {\n    stories.length = 100;\n  }\n\n  return (\n    <div\n      style={{\n        width: '100%',\n        height: '100%',\n        textAlign: 'center',\n      }}\n    >\n      <h2>It is {time.toLocaleTimeString()} now.</h2>\n      <StoryTray stories={stories} />\n    </div>\n  );\n}\n\nfunction useTime() {\n  const [time, setTime] = useState(() => new Date());\n  useEffect(() => {\n    const id = setInterval(() => {\n      setTime(new Date());\n    }, 1000);\n    return () => clearInterval(id);\n  }, []);\n  return time;\n}\n```\n\n```css\nul {\n  margin: 0;\n  list-style-type: none;\n}\n\nli {\n  border: 1px solid #aaa;\n  border-radius: 6px;\n  float: left;\n  margin: 5px;\n  margin-bottom: 20px;\n  padding: 5px;\n  width: 70px;\n  height: 100px;\n}\n```\n\n```js sandbox.config.json hidden\n{\n  \"hardReloadOnChange\": true\n}\n```\n\n</Sandpack>\n\n<Solution>\n\n시계가 업데이트될 때마다 \"Create story\"가 _두 번_ 추가됩니다. 이는 렌더링 중에 변경이 있음을 암시합니다. 엄격 모드는 컴포넌트를 두 번 호출하여 이러한 문제를 더 눈에 띄게 만들도록 해줍니다.\n\n`StoryTray` 함수는 순수하지 않습니다. 전달된 `stories` 배열(Prop)에서 `push`를 호출하면 `StroyTray`가 렌더링을 시작하기 _전에_ 생성되었던 객체를 직접 변경합니다. 이로 인해 버그가 발생하고 결과를 예측하기가 매우 어렵게 되었습니다.\n\n가장 간단한 해결 방법은 배열을 전혀 건드리지 않고 \"Create Story\"를 별도로 렌더링하는 것입니다.\n\n<Sandpack>\n\n```js src/StoryTray.js active\nexport default function StoryTray({ stories }) {\n  return (\n    <ul>\n      {stories.map(story => (\n        <li key={story.id}>\n          {story.label}\n        </li>\n      ))}\n      <li>Create Story</li>\n    </ul>\n  );\n}\n```\n\n```js src/App.js hidden\nimport { useState, useEffect } from 'react';\nimport StoryTray from './StoryTray.js';\n\nconst initialStories = [\n  {id: 0, label: \"Ankit's Story\" },\n  {id: 1, label: \"Taylor's Story\" },\n];\n\nexport default function App() {\n  const [stories, setStories] = useState([...initialStories])\n  const time = useTime();\n\n  // HACK: Prevent the memory from growing forever while you read docs.\n  // We're breaking our own rules here.\n  if (stories.length > 100) {\n    stories.length = 100;\n  }\n\n  return (\n    <div\n      style={{\n        width: '100%',\n        height: '100%',\n        textAlign: 'center',\n      }}\n    >\n      <h2>It is {time.toLocaleTimeString()} now.</h2>\n      <StoryTray stories={stories} />\n    </div>\n  );\n}\n\nfunction useTime() {\n  const [time, setTime] = useState(() => new Date());\n  useEffect(() => {\n    const id = setInterval(() => {\n      setTime(new Date());\n    }, 1000);\n    return () => clearInterval(id);\n  }, []);\n  return time;\n}\n```\n\n```css\nul {\n  margin: 0;\n  list-style-type: none;\n}\n\nli {\n  border: 1px solid #aaa;\n  border-radius: 6px;\n  float: left;\n  margin: 5px;\n  margin-bottom: 20px;\n  padding: 5px;\n  width: 70px;\n  height: 100px;\n}\n```\n\n</Sandpack>\n\n또는 항목을 추가하기 전에 _새로운_ 배열(기존 배열을 복사해서)을 생성할 수 있습니다.\n\n<Sandpack>\n\n```js src/StoryTray.js active\nexport default function StoryTray({ stories }) {\n  // Copy the array!\n  const storiesToDisplay = stories.slice();\n\n  // Does not affect the original array:\n  storiesToDisplay.push({\n    id: 'create',\n    label: 'Create Story'\n  });\n\n  return (\n    <ul>\n      {storiesToDisplay.map(story => (\n        <li key={story.id}>\n          {story.label}\n        </li>\n      ))}\n    </ul>\n  );\n}\n```\n\n```js src/App.js hidden\nimport { useState, useEffect } from 'react';\nimport StoryTray from './StoryTray.js';\n\nconst initialStories = [\n  {id: 0, label: \"Ankit's Story\" },\n  {id: 1, label: \"Taylor's Story\" },\n];\n\nexport default function App() {\n  const [stories, setStories] = useState([...initialStories])\n  const time = useTime();\n\n  // HACK: Prevent the memory from growing forever while you read docs.\n  // We're breaking our own rules here.\n  if (stories.length > 100) {\n    stories.length = 100;\n  }\n\n  return (\n    <div\n      style={{\n        width: '100%',\n        height: '100%',\n        textAlign: 'center',\n      }}\n    >\n      <h2>It is {time.toLocaleTimeString()} now.</h2>\n      <StoryTray stories={stories} />\n    </div>\n  );\n}\n\nfunction useTime() {\n  const [time, setTime] = useState(() => new Date());\n  useEffect(() => {\n    const id = setInterval(() => {\n      setTime(new Date());\n    }, 1000);\n    return () => clearInterval(id);\n  }, []);\n  return time;\n}\n```\n\n```css\nul {\n  margin: 0;\n  list-style-type: none;\n}\n\nli {\n  border: 1px solid #aaa;\n  border-radius: 6px;\n  float: left;\n  margin: 5px;\n  margin-bottom: 20px;\n  padding: 5px;\n  width: 70px;\n  height: 100px;\n}\n```\n\n</Sandpack>\n\n이 코드는 지역 변경으로 유지하고 렌더링 함수를 순수하게 만듭니다. 그러나 여전히 조심해야 합니다. 예를 들어 배열의 기존 항목을 하나라도 변경하려고 한다면, 해당 항목 자체를 복제해야 합니다.\n\n배열에서 어떤 연산이 변경을 일으키는지, 어떤 작업이 그렇지 않은지를 기억하는 것이 좋습니다. 예를 들어 `push`, `pop`, `reverse`, `sort`는 기존 배열을 변경하지만 `slice`, `filter`, `map`은 새로운 배열을 만듭니다.\n\n</Solution>\n\n</Challenges>\n"
  },
  {
    "path": "src/content/learn/lifecycle-of-reactive-effects.md",
    "content": "---\ntitle: 'React Effect의 생명주기'\n---\n\n<Intro>\n\neffects는 컴포넌트와 다른 생명주기를 가집니다. 컴포넌트는 마운트, 업데이트 또는 마운트 해제할 수 있습니다. effect는 동기화를 시작하고 나중에 동기화를 중지하는 두 가지 작업만 할 수 있습니다. 이 사이클은 시간이 지남에 따라 변하는 props와 state에 의존하는 effect의 경우 여러 번 발생할 수 있습니다. React는 effect의 의존성을 올바르게 지정했는지 확인하는 린터 규칙을 제공합니다. 이렇게 하면 effect가 최신 props와 state에 동기화됩니다.\n\n</Intro>\n\n<YouWillLearn>\n\n- effect의 생명주기가 컴포넌트의 생명주기와 다른 점\n- 각 effect를 개별적으로 생각하는 방법\n- effect를 다시 동기화해야 하는 시기와 그 이유\n- effect의 의존성이 결정되는 방법\n- 값이 유동적이라는 것의 의미\n- 빈 의존성 배열이 의미하는 것\n- React가 린터로 의존성이 올바른지 확인하는 방법\n- 린터에 동의하지 않을 때 해야 할 일\n\n</YouWillLearn>\n\n## effect의 생명주기 {/*the-lifecycle-of-an-effect*/}\n\n모든 React 컴포넌트는 동일한 생명주기를 거칩니다.\n\n- 컴포넌트는 화면에 추가될 때 _마운트_ 됩니다.\n- 컴포넌트는 일반적으로 상호작용에 대한 응답으로 새로운 props나 state를 수신하면 _업데이트_ 됩니다.\n- 컴포넌트가 화면에서 제거되면 _마운트가 해제_ 됩니다.\n\n**컴포넌트에 대해 생각하기에는 좋지만 effects에 대해서는 생각하지 _않는_ 것이 좋습니다.** 대신 컴포넌트의 생명주기와 독립적으로 각 effect를 생각해 보세요. effect는 [외부 시스템을 현재 props 및 state와 동기화](/learn/synchronizing-with-effects)하는 방법을 설명합니다. 코드가 변경되면 동기화를 더 자주 또는 덜 자주 수행해야 합니다.\n\n이 점을 설명하기 위해 컴포넌트를 채팅 서버에 연결하는 effect를 예로 들어보겠습니다.\n\n```js\nconst serverUrl = 'https://localhost:1234';\n\nfunction ChatRoom({ roomId }) {\n  useEffect(() => {\n    const connection = createConnection(serverUrl, roomId);\n    connection.connect();\n    return () => {\n      connection.disconnect();\n    };\n  }, [roomId]);\n  // ...\n}\n```\n\neffect의 본문에는 **동기화 시작** 방법이 명시되어 있습니다.\n\n```js {2-3}\n    // ...\n    const connection = createConnection(serverUrl, roomId);\n    connection.connect();\n    return () => {\n      connection.disconnect();\n    };\n    // ...\n```\n\neffect에서 반환되는 cleanup 함수는 **동기화를 중지**하는 방법을 지정합니다.\n\n```js {5}\n    // ...\n    const connection = createConnection(serverUrl, roomId);\n    connection.connect();\n    return () => {\n      connection.disconnect();\n    };\n    // ...\n```\n\n직관적으로 React는 컴포넌트가 마운트될 때 **동기화를 시작**하고 컴포넌트가 마운트 해제될 때 **동기화를 중지**할 것이라고 생각할 수 있습니다. 하지만 이것이 이야기의 끝이 아닙니다! 때로는 컴포넌트가 마운트된 상태에서 **동기화를 여러 번 시작하고 중지**해야 할 수도 있습니다.\n\n이러한 동작이 필요한 _이유_ 와 _발생 시기_, 그리고 이러한 동작을 제어할 수 있는 _방법_ 을 살펴보겠습니다.\n\n<Note>\n\n일부 effects는 cleanup 함수를 전혀 반환하지 않습니다. [대부분의 경우](/learn/synchronizing-with-effects#how-to-handle-the-effect-firing-twice-in-development) 함수를 반환하고 싶겠지만, 그렇지 않은 경우 React는 빈 cleanup 함수를 반환한 것처럼 동작합니다.\n\n</Note>\n\n### 동기화가 두 번 이상 수행되어야 하는 이유 {/*why-synchronization-may-need-to-happen-more-than-once*/}\n\n이 `ChatRoom` 컴포넌트가 사용자가 드롭다운에서 선택한 `roomId` prop을 받는다고 가정해 보겠습니다. 처음에 사용자가 `\"general\"` 대화방을 `roomId`로 선택했다고 가정해 봅시다. 앱에 `\"general\"` 채팅방이 표시됩니다.\n\n```js {3}\nconst serverUrl = 'https://localhost:1234';\n\nfunction ChatRoom({ roomId /* \"general\" */ }) {\n  // ...\n  return <h1>Welcome to the {roomId} room!</h1>;\n}\n```\n\nUI가 표시되면 React가 effect를 실행하여 **동기화를 시작**합니다. `\"general\"` 방에 연결됩니다.\n\n```js {3,4}\nfunction ChatRoom({ roomId /* \"general\" */ }) {\n  useEffect(() => {\n    const connection = createConnection(serverUrl, roomId); // \"general\" 방에 연결\n    connection.connect();\n    return () => {\n      connection.disconnect(); // \"general\" 방에서 연결 해제\n    };\n  }, [roomId]);\n  // ...\n```\n\n지금까지는 괜찮습니다.\n\n나중에 사용자가 드롭다운에서 다른 방(예: `\"travel\"`)을 선택합니다. 먼저 React가 UI를 업데이트합니다.\n\n```js {1}\nfunction ChatRoom({ roomId /* \"travel\" */ }) {\n  // ...\n  return <h1>Welcome to the {roomId} room!</h1>;\n}\n```\n\n다음에 어떤 일이 일어날지 생각해 보세요. 사용자는 UI에서 `\"travel\"`이 선택된 대화방임을 알 수 있습니다. 그러나 지난번에 실행된 effect는 여전히 `\"general\"` 대화방에 연결되어 있습니다. **`roomId` prop이 변경되었기 때문에 그때 effect가 수행한 작업(`\"general\"` 방에 연결)이 더 이상 UI와 일치하지 않습니다.**\n\n이 시점에서 React가 두 가지 작업을 수행하기를 원합니다.\n\n1. 이전 roomId와의 동기화를 중지합니다(`\"general\"` 방에서 연결 끊기).\n2. 새 roomId와 동기화 시작(`\"travel\"` 방에 연결)\n\n**다행히도, 여러분은 이미 이 두 가지를 수행하는 방법을 React에 가르쳤습니다!** effect의 본문에는 동기화를 시작하는 방법이 명시되어 있고, cleanup 함수에는 동기화를 중지하는 방법이 명시되어 있습니다. 이제 React가 해야 할 일은 올바른 순서로 올바른 props와 state로 호출하기만 하면 됩니다. 정확히 어떻게 일어나는지 살펴보겠습니다.\n\n### React가 effect를 재동기화하는 방법 {/*how-react-re-synchronizes-your-effect*/}\n\n`ChatRoom` 컴포넌트가 `roomId` prop에 새로운 값을 받았다는 것을 기억하세요. 이전에는 `\"general\"`이었지만 이제는 `\"travel\"`입니다. 다른 방에 다시 연결하려면 React가 effect를 다시 동기화해야 합니다.\n\n**동기화를 중지**하기 위해 React는 `\"general\"` 방에 연결한 후 effect가 반환한 cleanup 함수를 호출합니다. `roomId`가 `\"general\"`이었기 때문에, cleanup 함수는 `\"general\"` 방에서 연결을 끊습니다.\n\n```js {6}\nfunction ChatRoom({ roomId /* \"general\" */ }) {\n  useEffect(() => {\n    const connection = createConnection(serverUrl, roomId); // \"general\" 방에 연결\n    connection.connect();\n    return () => {\n      connection.disconnect(); // \"general\" 방에서 연결 해제\n    };\n    // ...\n```\n\n그러면 React는 이 렌더링 중에 여러분이 제공한 effect를 실행합니다. 이번에는 `roomId`가 `\"travel\"`이므로 `\"travel\"` 채팅방과 **동기화되기 시작**합니다(결국 cleanup 함수도 호출될 때까지).\n\n```js {3,4}\nfunction ChatRoom({ roomId /* \"travel\" */ }) {\n  useEffect(() => {\n    const connection = createConnection(serverUrl, roomId); // \"travel\" 방에 연결\n    connection.connect();\n    // ...\n```\n\n덕분에 이제 사용자가 UI에서 선택한 방과 동일한 방에 연결됩니다. 재앙을 피했습니다!\n\n컴포넌트가 다른 `roomId`로 다시 렌더링할 때마다 effect가 다시 동기화됩니다. 예를 들어 사용자가 `roomId`를 `\"travel\"`에서 `\"music\"`으로 변경한다고 가정해 봅시다. React는 다시 cleanup 함수를 호출하여 effect **동기화를 중지**합니다(`\"travel\"` 방에서 연결을 끊습니다). 그런 다음 새 `roomId` prop로 본문을 실행하여 **동기화를 다시 시작**합니다(`\"music\"` 방에 연결).\n\n마지막으로 사용자가 다른 화면으로 이동하면 `ChatRoom`이 마운트를 해제합니다. 이제 연결 상태를 유지할 필요가 전혀 없습니다. React는 마지막으로 effect **동기화를 중지**하고 `\"music\"` 채팅방에서 연결을 끊습니다.\n\n### effect의 관점에서 생각하기 {/*thinking-from-the-effects-perspective*/}\n\n`ChatRoom` 컴포넌트의 관점에서 일어난 모든 일을 요약해 보겠습니다.\n\n1. `roomId`가 `\"general\"`으로 설정되어 마운트된 `ChatRoom`\n2. `roomId`가 `\"travel\"`으로 설정되어 업데이트된 `ChatRoom`\n3. `roomId`가 `\"music\"`으로 설정되어 업데이트된 `ChatRoom`\n3. 마운트 해제된 `ChatRoom`\n\n컴포넌트의 생명주기에서 이러한 각 시점에서 effect는 다른 작업을 수행했습니다.\n\n1. effect가 `\"general\"` 대화방에 연결됨\n2. `\"general\"` 방에서 연결이 끊어지고 `\"travel\"` 방에 연결된 effect\n3. `\"travel\"` 방에서 연결이 끊어지고 `\"music\"` 방에 연결된 effect\n4. `\"music\"` 방에서 연결이 끊어진 effect\n\n이제 effect 자체의 관점에서 무슨 일이 일어났는지 생각해 봅시다.\n\n```js\n  useEffect(() => {\n    // roomId로 지정된 방에 연결된 effect...\n    const connection = createConnection(serverUrl, roomId);\n    connection.connect();\n    return () => {\n      // ...연결이 끊어질 때까지\n      connection.disconnect();\n    };\n  }, [roomId]);\n```\n\n이 코드의 구조는 어떤 일이 일어났는지 겹치지 않는 시간의 연속으로 보는 데 영감을 줄 수 있습니다.\n\n1. `\"general\"` 방에 연결된 effect (연결이 끊어질 때까지)\n2. `\"travel\"` 방에 연결된 effect (연결이 끊어질 때까지)\n3. `\"music\"` 방에 연결된 effect (연결이 끊어질 때까지)\n\n이전에는 컴포넌트의 관점에서 생각했습니다. 컴포넌트의 관점에서 보면 effect를 '렌더링 후' 또는 '마운트 해제 전'과 같은 특정 시점에 실행되는 '콜백' 또는 '생명주기 이벤트'로 생각하기 쉬웠습니다. 이러한 사고방식은 매우 빠르게 복잡해지므로 피하는 것이 가장 좋습니다.\n\n**대신 항상 한 번에 하나의 시작/중지 사이클에만 집중하세요. 컴포넌트를 마운트, 업데이트 또는 마운트 해제하는 것은 중요하지 않습니다. 동기화를 시작하는 방법과 중지하는 방법만 설명하면 됩니다. 이 작업을 잘 수행하면 필요한 횟수만큼 effect를 시작하고 중지할 수 있는 탄력성을 확보할 수 있습니다.**\n\n이렇게 하면 JSX를 생성하는 렌더링 로직을 작성할 때 컴포넌트가 마운트되는지 업데이트되는지 생각하지 않는 방법을 떠올릴 수 있습니다. 화면에 무엇이 표시되어야 하는지 설명하면 [나머지는 React가 알아서 처리합니다.](/learn/reacting-to-input-with-state)\n\n### React가 effect를 다시 동기화될 수 있는지 확인하는 방법 {/*how-react-verifies-that-your-effect-can-re-synchronize*/}\n\n다음은 여러분이 플레이할 수 있는 라이브 예시입니다. \"채팅 열기\"를 눌러 `ChatRoom` 컴포넌트를 마운트합니다.\n\n<Sandpack>\n\n```js\nimport { useState, useEffect } from 'react';\nimport { createConnection } from './chat.js';\n\nconst serverUrl = 'https://localhost:1234';\n\nfunction ChatRoom({ roomId }) {\n  useEffect(() => {\n    const connection = createConnection(serverUrl, roomId);\n    connection.connect();\n    return () => connection.disconnect();\n  }, [roomId]);\n  return <h1>Welcome to the {roomId} room!</h1>;\n}\n\nexport default function App() {\n  const [roomId, setRoomId] = useState('general');\n  const [show, setShow] = useState(false);\n  return (\n    <>\n      <label>\n        Choose the chat room:{' '}\n        <select\n          value={roomId}\n          onChange={e => setRoomId(e.target.value)}\n        >\n          <option value=\"general\">general</option>\n          <option value=\"travel\">travel</option>\n          <option value=\"music\">music</option>\n        </select>\n      </label>\n      <button onClick={() => setShow(!show)}>\n        {show ? 'Close chat' : 'Open chat'}\n      </button>\n      {show && <hr />}\n      {show && <ChatRoom roomId={roomId} />}\n    </>\n  );\n}\n```\n\n```js src/chat.js\nexport function createConnection(serverUrl, roomId) {\n  // 실제 구현은 실제로 서버에 연결됩니다.\n  return {\n    connect() {\n      console.log('✅ Connecting to \"' + roomId + '\" room at ' + serverUrl + '...');\n    },\n    disconnect() {\n      console.log('❌ Disconnected from \"' + roomId + '\" room at ' + serverUrl);\n    }\n  };\n}\n```\n\n```css\ninput { display: block; margin-bottom: 20px; }\nbutton { margin-left: 10px; }\n```\n\n</Sandpack>\n\n컴포넌트가 처음 마운트될 때 3개의 로그가 표시됩니다.\n\n1. `✅ https://localhost:1234... 에서 \"general\" 방에 연결 중입니다.` *(개발 전용)*\n2. `❌ https://localhost:1234에서 \"일반\" 방에서 연결 해제되었습니다.` *(개발 전용)*\n3. `✅ https://localhost:1234... 에서 \"general\" 방에 연결 중입니다.`\n\n처음 두 개의 로그는 개발 전용입니다. 개발 시 React는 항상 각 컴포넌트를 한 번씩 다시 마운트합니다.\n\n**React는 개발 단계에서 즉시 강제로 동기화를 수행하여 effect가 다시 동기화할 수 있는지 확인합니다.** 이는 도어록이 작동하는지 확인하기 위해 문을 열었다가 다시 닫는 것을 떠올리게 할 수 있습니다. React는 개발 과정에서 effect를 한 번 더 시작하고 중지하여 [정리를 잘 구현했는지](/learn/synchronizing-with-effects#how-to-handle-the-effect-firing-twice-in-development) 확인합니다.\n\n실제로 effect가 다시 동기화되는 주된 이유는 effect가 사용하는 일부 데이터가 변경된 경우입니다. 위의 sandbox에서 선택한 채팅방을 변경합니다. `roomId`가 변경되면 effect가 다시 동기화되는 것을 확인할 수 있습니다.\n\n그러나 다시 동기화해야 하는 더 특이한 경우도 있습니다. 예를 들어, 채팅이 열려 있는 상태에서 위의 sandbox에서 `serverUrl`을 편집해 보세요. 코드 편집에 대한 응답으로 effect가 어떻게 다시 동기화되는지 주목하세요. 앞으로 React는 재동기화에 의존하는 더 많은 기능을 추가할 수 있습니다.\n\n### React가 effect를 다시 동기화해야 한다는 것을 인식하는 방법 {/*how-react-knows-that-it-needs-to-re-synchronize-the-effect*/}\n\n`roomId`가 변경된 후 effect를 다시 동기화해야 한다는 것을 어떻게 React가 알았는지 궁금할 것입니다. 그것은 여러분이 [종속성 목록](/learn/synchronizing-with-effects#step-2-specify-the-effect-dependencies)에 `roomId`를 포함함으로써 해당 코드가 `roomId`에 종속되어 있다고 React에 알렸기 때문입니다.\n\n```js {1,3,8}\nfunction ChatRoom({ roomId }) { // roomId prop은 시간이 지남에 따라 변경될 수 있습니다.\n  useEffect(() => {\n    const connection = createConnection(serverUrl, roomId); // 이 effect는 roomId를 읽습니다.\n    connection.connect();\n    return () => {\n      connection.disconnect();\n    };\n\n  }, [roomId]); // 따라서 React에 이 effect가 roomId에 \"의존\"한다고 알려줍니다.\n  // ...\n```\n\n작동 방식은 다음과 같습니다.\n\n1. `roomId`가 `prop`이므로 시간이 지남에 따라 변경될 수 있다는 것을 알고 있습니다.\n2. effect가 `roomId`를 읽는다는 것을 알았습니다.(따라서 로직이 나중에 변경될 수 있는 값에 따라 달라집니다.)\n3. 그렇기 때문에 `roomId`를 effect의 종속성으로 지정한 것입니다 (`roomId` 가 변경되면 다시 동기화되도록).\n\n컴포넌트가 다시 렌더링 될 때마다 React는 전달한 의존성 배열을 살펴봅니다. 배열의 값 중 하나라도 이전 렌더링 중에 전달한 동일한 지점의 값과 다르면 React는 effect를 다시 동기화합니다.\n\n예를 들어, 초기 렌더링 중에 `[\"general\"]`을 전달했는데 나중에 다음 렌더링 중에 `[\"travel\"]`을 전달한 경우, React는 `\"general\"`과 `\"travel\"`을 비교합니다. 이 값들은 ([`Object.is`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is)로 비교) 다른 값이기 때문에 React는 effect를 다시 동기화합니다. 반면에 컴포넌트가 다시 렌더링 되지만 `roomId`가 변경되지 않은 경우, effect는 동일한 방에 연결된 상태로 유지됩니다.\n\n### 각 effect는 별도의 동기화 프로세스를 나타냅니다. {/*each-effect-represents-a-separate-synchronization-process*/}\n\n이 로직은 이미 작성한 effect와 동시에 실행되어야 하므로 관련 없는 로직을 effect에 추가하지 마세요. 예를 들어 사용자가 회의실을 방문할 때 분석 이벤트를 전송하고 싶다고 가정해 보겠습니다. 이미 `roomId`에 의존하는 effect가 있으므로 거기에 분석 호출을 추가하고 싶을 수 있습니다.\n\n```js {3}\nfunction ChatRoom({ roomId }) {\n  useEffect(() => {\n    logVisit(roomId);\n    const connection = createConnection(serverUrl, roomId);\n    connection.connect();\n    return () => {\n      connection.disconnect();\n    };\n  }, [roomId]);\n  // ...\n}\n```\n\n하지만 나중에 이 effect에 연결을 다시 설정해야 하는 다른 종속성을 추가한다고 가정해 보겠습니다. 이 effect가 다시 동기화되면 의도하지 않은 동일한 방에 대해 `logVisit(roomId)`도 호출합니다. 방문을 기록하는 것은 연결과는 **별개의 프로세스**입니다. 두 개의 개별 effect로 작성하세요.\n\n```js {2-4}\nfunction ChatRoom({ roomId }) {\n  useEffect(() => {\n    logVisit(roomId);\n  }, [roomId]);\n\n  useEffect(() => {\n    const connection = createConnection(serverUrl, roomId);\n    // ...\n  }, [roomId]);\n  // ...\n}\n```\n\n**코드의 각 effect는 별도의 독립적인 동기화 프로세스를 나타내야 합니다.**\n\n위의 예시에서는 한 effect를 삭제해도 다른 effect의 로직이 깨지지 않습니다. 이는 서로 다른 것을 동기화하므로 분리하는 것이 합리적이라는 것을 나타냅니다. 반면에 일관된 로직을 별도의 effect로 분리하면 코드가 \"더 깔끔해\" 보일 수 있지만 [유지 보수가 더 어려워집니다.](/learn/you-might-not-need-an-effect#chains-of-computations) 따라서 코드가 더 깔끔해 보이는지 여부가 아니라 프로세스가 동일하거나 분리되어 있는지를 고려해야 합니다.\n\n## 반응형 값에 \"반응\"하는 effect {/*effects-react-to-reactive-values*/}\n\neffect에서 두 개의 변수(`serverUrl` 및 `roomId`)를 읽지만 종속성으로 `roomId`만 지정했습니다.\n\n```js {5,10}\nconst serverUrl = 'https://localhost:1234';\n\nfunction ChatRoom({ roomId }) {\n  useEffect(() => {\n    const connection = createConnection(serverUrl, roomId);\n    connection.connect();\n    return () => {\n      connection.disconnect();\n    };\n  }, [roomId]);\n  // ...\n}\n```\n\n`serverUrl`이 종속성이 될 필요가 없는 이유는 무엇인가요?\n\n이는 재 렌더링으로 인해 `serverUrl`이 변경되지 않기 때문입니다. 컴포넌트가 몇 번이나 다시 렌더링하든, 그 이유와 상관없이 항상 동일합니다. `serverUrl`은 절대 변경되지 않으므로 종속성으로 지정하는 것은 의미가 없습니다. 결국 종속성은 시간이 지남에 따라 변경될 때만 무언가를 수행합니다!\n\n반면에 `roomId`는 다시 렌더링할 때 달라질 수 있습니다. 컴포넌트 내부에서 선언된 **Props, state 및 기타값은 렌더링 중에 계산되고 React 데이터 흐름에 참여하기 때문에 _반응형_ 입니다.**\n\n`serverUrl`이 state 변수라면 반응형일 것입니다. 반응형 값은 종속성에 포함되어야 합니다.\n\n```js {2,5,10}\nfunction ChatRoom({ roomId }) { // Props는 시간이 지남에 따라 변화합니다.\n  const [serverUrl, setServerUrl] = useState('https://localhost:1234'); // State는 시간이 지남에 따라 변화합니다.\n\n  useEffect(() => {\n    const connection = createConnection(serverUrl, roomId); // effect는 Props와 state를 읽습니다.\n    connection.connect();\n    return () => {\n      connection.disconnect();\n    };\n  }, [roomId, serverUrl]); // 따라서 이 effect는 props와 state에 \"의존\"한다고 React에 알려줍니다.\n  // ...\n}\n```\n\n`serverUrl`을 종속성으로 포함하면 effect가 변경된 후 다시 동기화되도록 할 수 있습니다.\n\n이 sandbox에서 선택한 대화방을 변경하거나 서버 URL을 수정해 보세요.\n\n<Sandpack>\n\n```js\nimport { useState, useEffect } from 'react';\nimport { createConnection } from './chat.js';\n\nfunction ChatRoom({ roomId }) {\n  const [serverUrl, setServerUrl] = useState('https://localhost:1234');\n\n  useEffect(() => {\n    const connection = createConnection(serverUrl, roomId);\n    connection.connect();\n    return () => connection.disconnect();\n  }, [roomId, serverUrl]);\n\n  return (\n    <>\n      <label>\n        Server URL:{' '}\n        <input\n          value={serverUrl}\n          onChange={e => setServerUrl(e.target.value)}\n        />\n      </label>\n      <h1>Welcome to the {roomId} room!</h1>\n    </>\n  );\n}\n\nexport default function App() {\n  const [roomId, setRoomId] = useState('general');\n  return (\n    <>\n      <label>\n        Choose the chat room:{' '}\n        <select\n          value={roomId}\n          onChange={e => setRoomId(e.target.value)}\n        >\n          <option value=\"general\">general</option>\n          <option value=\"travel\">travel</option>\n          <option value=\"music\">music</option>\n        </select>\n      </label>\n      <hr />\n      <ChatRoom roomId={roomId} />\n    </>\n  );\n}\n```\n\n```js src/chat.js\nexport function createConnection(serverUrl, roomId) {\n  // 실제 구현은 실제로 서버에 연결됩니다.\n  return {\n    connect() {\n      console.log('✅ Connecting to \"' + roomId + '\" room at ' + serverUrl + '...');\n    },\n    disconnect() {\n      console.log('❌ Disconnected from \"' + roomId + '\" room at ' + serverUrl);\n    }\n  };\n}\n```\n\n```css\ninput { display: block; margin-bottom: 20px; }\nbutton { margin-left: 10px; }\n```\n\n</Sandpack>\n\n`roomId` 또는 `serverUrl`과 같은 반응형 값을 변경할 때마다 effect가 채팅 서버에 다시 연결합니다.\n\n### 빈 종속성이 있는 effect의 의미 {/*what-an-effect-with-empty-dependencies-means*/}\n\n`serverUrl`과 `roomId`를 모두 컴포넌트 외부로 이동하면 어떻게 되나요?\n\n```js {1,2}\nconst serverUrl = 'https://localhost:1234';\nconst roomId = 'general';\n\nfunction ChatRoom() {\n  useEffect(() => {\n    const connection = createConnection(serverUrl, roomId);\n    connection.connect();\n    return () => {\n      connection.disconnect();\n    };\n  }, []); // ✅ 선언된 모든 종속성\n  // ...\n}\n```\n\n이제 effect의 코드는 *어떤* 반응형 값도 사용하지 않으므로 종속성이 비어 있을 수 있습니다(`[]`).\n\n컴포넌트의 관점에서 생각해 보면, 빈 `[]` 의존성 배열은 이 effect가 컴포넌트가 마운트될 때만 채팅방에 연결되고 컴포넌트가 마운트 해제될 때만 연결이 끊어진다는 것을 의미합니다. (React는 로직을 스트레스 테스트하기 위해 개발 단계에서 [한 번 더 동기화](#how-react-verifies-that-your-effect-can-re-synchronize)한다는 점을 기억하세요.)\n\n\n<Sandpack>\n\n```js\nimport { useState, useEffect } from 'react';\nimport { createConnection } from './chat.js';\n\nconst serverUrl = 'https://localhost:1234';\nconst roomId = 'general';\n\nfunction ChatRoom() {\n  useEffect(() => {\n    const connection = createConnection(serverUrl, roomId);\n    connection.connect();\n    return () => connection.disconnect();\n  }, []);\n  return <h1>Welcome to the {roomId} room!</h1>;\n}\n\nexport default function App() {\n  const [show, setShow] = useState(false);\n  return (\n    <>\n      <button onClick={() => setShow(!show)}>\n        {show ? 'Close chat' : 'Open chat'}\n      </button>\n      {show && <hr />}\n      {show && <ChatRoom />}\n    </>\n  );\n}\n```\n\n```js src/chat.js\nexport function createConnection(serverUrl, roomId) {\n  // 실제 구현은 실제로 서버에 연결됩니다.\n  return {\n    connect() {\n      console.log('✅ Connecting to \"' + roomId + '\" room at ' + serverUrl + '...');\n    },\n    disconnect() {\n      console.log('❌ Disconnected from \"' + roomId + '\" room at ' + serverUrl);\n    }\n  };\n}\n```\n\n```css\ninput { display: block; margin-bottom: 20px; }\nbutton { margin-left: 10px; }\n```\n\n</Sandpack>\n\n하지만 [effect의 관점에서 생각](#thinking-from-the-effects-perspective)하면 마운트 및 마운트 해제에 대해 전혀 생각할 필요가 없습니다. 중요한 것은 effect가 동기화를 시작하고 중지하는 작업을 지정한 것입니다. 현재는 반응형 종속성이 없습니다. 하지만 사용자가 시간이 지남에 따라 `roomId` 또는 `serverUrl`을 변경하려는 경우(그리고 반응형이 되는 경우) effect의 코드는 변경되지 않습니다. 종속성에 추가하기만 하면 됩니다.\n\n### 컴포넌트 본문에서 선언된 모든 변수는 반응형입니다. {/*all-variables-declared-in-the-component-body-are-reactive*/}\n\nprops와 state만 반응형 값인 것은 아닙니다. 이들로부터 계산하는 값도 반응형입니다. props나 state가 변경되면 컴포넌트가 다시 렌더링 되고 그로부터 계산된 값도 변경됩니다. 이 때문에 effect에서 사용하는 컴포넌트 본문의 모든 변수는 effect 종속성 목록에 있어야 합니다.\n\n사용자가 드롭다운에서 채팅 서버를 선택할 수 있지만 설정에서 기본 서버를 구성할 수도 있다고 가정해 봅시다. 이미 settings state를 [context](/learn/scaling-up-with-reducer-and-context)에 넣어서 해당 context에서 `settings`를 읽었다고 가정해 보겠습니다. 이제 props에서 선택한 서버와 기본 서버를 기준으로 `serverUrl`을 계산합니다.\n\n```js {3,5,10}\nfunction ChatRoom({ roomId, selectedServerUrl }) { // roomId는 반응형입니다.\n  const settings = useContext(SettingsContext); // settings는 반응형입니다.\n  const serverUrl = selectedServerUrl ?? settings.defaultServerUrl; // serverUrl는 반응형입니다.\n  useEffect(() => {\n    const connection = createConnection(serverUrl, roomId); // effect는 roomId 와 serverUrl를 읽습니다.\n    connection.connect();\n    return () => {\n      connection.disconnect();\n    };\n  }, [roomId, serverUrl]); // 따라서 둘 중 하나가 변경되면 다시 동기화해야 합니다!\n  // ...\n}\n```\n\n이 예시에서 `serverUrl`은 prop이나 state 변수가 아닙니다. 렌더링 중에 계산하는 일반 변수입니다. 하지만 렌더링 중에 계산되므로 재 렌더링으로 인해 변경될 수 있습니다. 이것이 바로 반응형인 이유입니다.\n\n**컴포넌트 내부의 모든 값(컴포넌트 본문의 props, state, 변수 포함)은 반응형입니다. 모든 반응형 값은 다시 렌더링할 때 변경될 수 있으므로 반응형 값을 effect의 종속 요소로 포함해야 합니다.**\n\n즉, effect는 컴포넌트 본문의 모든 값에 \"반응\"합니다.\n\n<DeepDive>\n\n#### 전역 또는 변경할 수 있는 값이 종속성이 될 수 있나요? {/*can-global-or-mutable-values-be-dependencies*/}\n\n변경할 수 있는 값(전역 변수 포함)은 반응하지 않습니다.\n\n**[`location.pathname`](https://developer.mozilla.org/en-US/docs/Web/API/Location/pathname)과 같은 변경 가능한 값은 종속성이 될 수 없습니다.** 이 값은 변경할 수 있으므로 React 렌더링 데이터 흐름 외부에서 언제든지 변경할 수 있습니다. 이 값을 변경해도 컴포넌트가 다시 렌더링 되지는 않습니다. 따라서 종속성에서 지정했더라도 React는 effect가 변경될 때 effect를 다시 동기화할지 *알 수 없습니다.* 또한 렌더링 도중(의존성을 계산할 때) 변경할 수 있는 데이터를 읽는 것은 [렌더링의 순수성](/learn/keeping-components-pure)을 깨뜨리기 때문에 React의 규칙을 위반합니다. 대신, [`useSyncExternalStore`](/learn/you-might-not-need-an-effect#subscribing-to-an-external-store)를 사용하여 외부 변경할 수 있는 값을 읽고 구독해야 합니다.\n\n**[`ref.current`](/reference/react/useRef#reference)와 같이 변경 가능한 값이나 이 값에서 읽은 것 역시 종속성이 될 수 없습니다.** `useRef`가 반환하는 `ref` 객체 자체는 종속성이 될 수 있지만 `current` prop는 의도적으로 변경할 수 있습니다. 이를 통해 [재 렌더링을 트리거하지 않고도 무언가를 추적할 수 있습니다.](/learn/referencing-values-with-refs) 하지만 변경해도 다시 렌더링이 트리거되지 않기 때문에 반응형 값이 아니며, React는 이 값이 변경될 때 effect를 다시 실행할지 알지 못합니다.\n\n이 페이지에서 아래에서 배우게 되겠지만, 린터는 이러한 문제를 자동으로 확인합니다.\n\n</DeepDive>\n\n### React는 모든 반응형 값을 종속성으로 지정했는지 확인합니다. {/*react-verifies-that-you-specified-every-reactive-value-as-a-dependency*/}\n\n린터가 [React에 대해 구성](/learn/editor-setup#linting)된 경우, effect의 코드에서 사용되는 모든 반응형 값이 종속성으로 선언되었는지 확인합니다. 예를 들어, `roomId`와 `serverUrl`이 모두 반응형이기 때문에 이것은 린트 오류입니다.\n\n<Sandpack>\n\n```js\nimport { useState, useEffect } from 'react';\nimport { createConnection } from './chat.js';\n\nfunction ChatRoom({ roomId }) { // roomId는 반응형입니다.\n  const [serverUrl, setServerUrl] = useState('https://localhost:1234'); // serverUrl는 반응형입니다.\n\n  useEffect(() => {\n    const connection = createConnection(serverUrl, roomId);\n    connection.connect();\n    return () => connection.disconnect();\n  }, []); // <-- 여기 무언가 잘못되었습니다!\n\n  return (\n    <>\n      <label>\n        Server URL:{' '}\n        <input\n          value={serverUrl}\n          onChange={e => setServerUrl(e.target.value)}\n        />\n      </label>\n      <h1>Welcome to the {roomId} room!</h1>\n    </>\n  );\n}\n\nexport default function App() {\n  const [roomId, setRoomId] = useState('general');\n  return (\n    <>\n      <label>\n        Choose the chat room:{' '}\n        <select\n          value={roomId}\n          onChange={e => setRoomId(e.target.value)}\n        >\n          <option value=\"general\">general</option>\n          <option value=\"travel\">travel</option>\n          <option value=\"music\">music</option>\n        </select>\n      </label>\n      <hr />\n      <ChatRoom roomId={roomId} />\n    </>\n  );\n}\n```\n\n```js src/chat.js\nexport function createConnection(serverUrl, roomId) {\n  // 실제 구현은 실제로 서버에 연결됩니다.\n  return {\n    connect() {\n      console.log('✅ Connecting to \"' + roomId + '\" room at ' + serverUrl + '...');\n    },\n    disconnect() {\n      console.log('❌ Disconnected from \"' + roomId + '\" room at ' + serverUrl);\n    }\n  };\n}\n```\n\n```css\ninput { display: block; margin-bottom: 20px; }\nbutton { margin-left: 10px; }\n```\n\n</Sandpack>\n\n이것은 React 오류처럼 보일 수 있지만 실제로는 코드의 버그를 지적하는 것입니다. `roomId`와 `serverUrl`은 시간이 지남에 따라 변경될 수 있지만, 변경 시 effect를 다시 동기화하는 것을 잊어버리고 있습니다. 사용자가 UI에서 다른 값을 선택한 후에도 초기 `roomId`와 `serverUrl`에 연결된 상태로 유지됩니다.\n\n버그를 수정하려면 린터의 제안에 따라 effect의 종속 요소로 `roomId` 및 `serverUrl`을 지정하세요.\n\n```js {9}\nfunction ChatRoom({ roomId }) { // roomId는 반응형입니다.\n  const [serverUrl, setServerUrl] = useState('https://localhost:1234'); // serverUrl는 반응형입니다.\n  useEffect(() => {\n    const connection = createConnection(serverUrl, roomId);\n    connection.connect();\n    return () => {\n      connection.disconnect();\n    };\n  }, [serverUrl, roomId]); // ✅ 선언된 모든 종속성\n  // ...\n}\n```\n\n위의 sandbox에서 이 수정 방법을 시도해 보세요. 린터 오류가 사라지고 필요할 때 채팅이 다시 연결되는지 확인합니다.\n\n<Note>\n\n어떤 경우에는 컴포넌트 내부에서 값이 선언되더라도 절대 변하지 않는다는 것을 React가 *알고 있습니다.* 예를 들어, `useState`에서 반환되는 [`set` 함수](/reference/react/useState#setstate)와 [`useRef`](/reference/react/useRef)에서 반환되는 ref 객체는 *안정적*이며, 다시 렌더링해도 변경되지 않도록 보장됩니다. 안정된 값은 반응하지 않으므로 목록에서 생략할 수 있습니다. 이러한 값은 변경되지 않으므로 포함해도 상관없습니다.\n\n</Note>\n\n### 다시 동기화하지 않으려는 경우 어떻게 해야 하나요? {/*what-to-do-when-you-dont-want-to-re-synchronize*/}\n\n이전 예시에서는 `roomId`와 `serverUrl`을 종속성으로 나열하여 린트 오류를 수정했습니다.\n\n**그러나 대신 이러한 값이 반응형 값이 아니라는 것, 즉 재 렌더링의 결과로 변경*될 수 없다*는 것을 린터에 \"증명\"할 수 있습니다.** 예를 들어 `serverUrl`과 `roomId`가 렌더링에 의존하지 않고 항상 같은 값을 갖는다면 컴포넌트 외부로 옮길 수 있습니다. 이제 종속성이 될 필요가 없습니다.\n\n```js {1,2,11}\nconst serverUrl = 'https://localhost:1234'; // serverUrl는 반응형이 아닙니다.\nconst roomId = 'general'; // roomId는 반응형이 아닙니다.\n\nfunction ChatRoom() {\n  useEffect(() => {\n    const connection = createConnection(serverUrl, roomId);\n    connection.connect();\n    return () => {\n      connection.disconnect();\n    };\n  }, []); // ✅ 선언된 모든 종속성\n  // ...\n}\n```\n\n*effect 내부*로 이동할 수도 있습니다. 렌더링 중에 계산되지 않으므로 반응하지 않습니다.\n\n```js {3,4,10}\nfunction ChatRoom() {\n  useEffect(() => {\n    const serverUrl = 'https://localhost:1234'; // serverUrl는 반응형이 아닙니다.\n    const roomId = 'general'; // roomId는 반응형이 아닙니다.\n    const connection = createConnection(serverUrl, roomId);\n    connection.connect();\n    return () => {\n      connection.disconnect();\n    };\n  }, []); // ✅ 선언된 모든 종속성\n  // ...\n}\n```\n\n**effect는 반응형 코드 블록입니다.** 내부에서 읽은 값이 변경되면 다시 동기화됩니다. 상호작용당 한 번만 실행되는 이벤트 핸들러와 달리 effect는 동기화가 필요할 때마다 실행됩니다.\n\n**종속성을 \"선택\"할 수 없습니다.** 종속성에는 effect에서 읽은 모든 [반응형 값](#all-variables-declared-in-the-component-body-are-reactive)이 포함되어야 합니다. 린터가 이를 강제합니다. 때때로 이에 따라 무한 루프와 같은 문제가 발생하거나 effect가 너무 자주 다시 동기화될 수 있습니다. 린터를 억제하여 이러한 문제를 해결하지 마세요! 대신 시도할 방법은 다음과 같습니다.\n\n* **effect가 독립적인 동기화 프로세스를 나타내는지 확인하세요.** effect가 아무것도 동기화하지 않는다면 [불필요한 것일 수 있습니다.](/learn/you-might-not-need-an-effect) 여러 개의 독립적인 것을 동기화하는 경우 [분할](#each-effect-represents-a-separate-synchronization-process)하세요.\n\n* **props나 state에 \"반응\"하지 않고 effect를 다시 동기화하지 않고 최신 값을 읽으려면** effect를 반응하는 부분(effect에 유지할 것)과 반응하지 않는 부분(_effect 이벤트_ 라고 하는 것으로 추출할 수 있는 것)으로 분리하면 됩니다. [이벤트와 effect를 분리하는 방법을 읽어보세요.](/learn/separating-events-from-effects)\n\n* **객체와 함수를 종속성으로 사용하지 마세요.** 렌더링 중에 오브젝트와 함수를 생성한 다음 effect에서 읽으면 렌더링할 때마다 오브젝트와 함수가 달라집니다. 그러면 매번 effect를 다시 동기화해야 합니다. [effect에서 불필요한 종속성을 제거하는 방법에 대해 자세히 읽어보세요.](/learn/removing-effect-dependencies)\n\n<Pitfall>\n\n린터는 여러분의 친구이지만 그 힘은 제한되어 있습니다. 린터는 종속성이 *잘못*되었을 때만 알 수 있습니다. 각 사례를 해결하는 *최선*의 방법은 알지 못합니다. 만약 린터가 종속성을 제안하지만 이를 추가하면 루프가 발생한다고 해서 린터를 무시해야 한다는 의미는 아닙니다. 해당 값이 반응적이지 않고 종속성이 될 *필요*가 없도록 effect 내부(또는 외부)의 코드를 변경해야 합니다.\n\n기존 코드베이스가 있는 경우 이처럼 린터를 억제하는 effect가 있을 수 있습니다.\n\n```js {3-4}\nuseEffect(() => {\n  // ...\n  // 🔴 이런 식으로 린트를 억누르지 마세요.\n  // eslint-ignore-next-line react-hooks/exhaustive-deps\n}, []);\n```\n\n[다음](/learn/separating-events-from-effects) [페이지](/learn/removing-effect-dependencies)에서는 규칙을 위반하지 않고 이 코드를 수정하는 방법을 알아보세요. 언제나 고칠 가치가 있습니다!\n\n</Pitfall>\n\n<Recap>\n\n- 컴포넌트는 마운트, 업데이트, 마운트 해제할 수 있습니다.\n- 각 effect는 주변 컴포넌트와 별도의 생명주기를 가집니다.\n- 각 effect는 시작 및 중지할 수 있는 별도의 동기화 프로세스를 설명합니다.\n- effect를 작성하고 읽을 때는 컴포넌트의 관점(마운트, 업데이트 또는 마운트 해제 방법)이 아닌 개별 effect의 관점(동기화 *시작* 및 *중지* 방법)에서 생각하세요.\n- 컴포넌트 본문 내부에 선언된 값은 \"반응형\"입니다.\n- 반응형 값은 시간이 지남에 따라 변경될 수 있으므로 effect를 다시 동기화해야 합니다.\n- 린터는 effect 내부에서 사용된 모든 반응형 값이 종속성으로 지정되었는지 확인합니다.\n- 린터에 의해 플래그가 지정된 모든 오류는 합법적인 오류입니다. 규칙을 위반하지 않도록 코드를 수정할 방법은 항상 있습니다.\n\n</Recap>\n\n<Challenges>\n\n#### 모든 키 입력 시 재연결 문제 수정 {/*fix-reconnecting-on-every-keystroke*/}\n\n이 예시에서 `ChatRoom` 컴포넌트는 컴포넌트가 마운트될 때 채팅방에 연결되고, 마운트가 해제되면 연결이 끊어지며, 다른 채팅방을 선택하면 다시 연결됩니다. 이 동작은 올바른 것이므로 계속 작동하도록 해야 합니다.\n\n하지만 문제가 있습니다. 하단의 메시지 상자 입력란에 입력할 때마다 `ChatRoom`*도* 채팅에 다시 연결됩니다. (콘솔을 지우고 입력란에 입력하면 이 문제를 확인할 수 있습니다) 이런 일이 발생하지 않도록 문제를 해결하세요.\n\n<Hint>\n\n이 effect에 대한 종속성 배열을 추가해야 할 수도 있습니다. 어떤 종속성이 있어야 할까요?\n\n</Hint>\n\n<Sandpack>\n\n```js\nimport { useState, useEffect } from 'react';\nimport { createConnection } from './chat.js';\n\nconst serverUrl = 'https://localhost:1234';\n\nfunction ChatRoom({ roomId }) {\n  const [message, setMessage] = useState('');\n\n  useEffect(() => {\n    const connection = createConnection(serverUrl, roomId);\n    connection.connect();\n    return () => connection.disconnect();\n  });\n\n  return (\n    <>\n      <h1>Welcome to the {roomId} room!</h1>\n      <input\n        value={message}\n        onChange={e => setMessage(e.target.value)}\n      />\n    </>\n  );\n}\n\nexport default function App() {\n  const [roomId, setRoomId] = useState('general');\n  return (\n    <>\n      <label>\n        Choose the chat room:{' '}\n        <select\n          value={roomId}\n          onChange={e => setRoomId(e.target.value)}\n        >\n          <option value=\"general\">general</option>\n          <option value=\"travel\">travel</option>\n          <option value=\"music\">music</option>\n        </select>\n      </label>\n      <hr />\n      <ChatRoom roomId={roomId} />\n    </>\n  );\n}\n```\n\n```js src/chat.js\nexport function createConnection(serverUrl, roomId) {\n  // 실제 구현은 실제로 서버에 연결됩니다.\n  return {\n    connect() {\n      console.log('✅ Connecting to \"' + roomId + '\" room at ' + serverUrl + '...');\n    },\n    disconnect() {\n      console.log('❌ Disconnected from \"' + roomId + '\" room at ' + serverUrl);\n    }\n  };\n}\n```\n\n```css\ninput { display: block; margin-bottom: 20px; }\nbutton { margin-left: 10px; }\n```\n\n</Sandpack>\n\n<Solution>\n\n이 effect에는 종속성 배열이 전혀 없었기 때문에 렌더링할 때마다 다시 동기화되었습니다. 먼저 종속성 배열을 추가합니다. 그런 다음 effect에서 사용하는 모든 반응형 값이 배열에 지정되어 있는지 확인합니다. 예를 들어 `roomId`는 반응형이므로(props이므로) 배열에 포함되어야 합니다. 이렇게 하면 사용자가 다른 방을 선택해도 채팅이 다시 연결됩니다. 반면 `serverUrl`은 컴포넌트 외부에 정의됩니다. 그렇기 때문에 배열에 포함할 필요가 없습니다.\n\n<Sandpack>\n\n```js\nimport { useState, useEffect } from 'react';\nimport { createConnection } from './chat.js';\n\nconst serverUrl = 'https://localhost:1234';\n\nfunction ChatRoom({ roomId }) {\n  const [message, setMessage] = useState('');\n\n  useEffect(() => {\n    const connection = createConnection(serverUrl, roomId);\n    connection.connect();\n    return () => connection.disconnect();\n  }, [roomId]);\n\n  return (\n    <>\n      <h1>Welcome to the {roomId} room!</h1>\n      <input\n        value={message}\n        onChange={e => setMessage(e.target.value)}\n      />\n    </>\n  );\n}\n\nexport default function App() {\n  const [roomId, setRoomId] = useState('general');\n  return (\n    <>\n      <label>\n        Choose the chat room:{' '}\n        <select\n          value={roomId}\n          onChange={e => setRoomId(e.target.value)}\n        >\n          <option value=\"general\">general</option>\n          <option value=\"travel\">travel</option>\n          <option value=\"music\">music</option>\n        </select>\n      </label>\n      <hr />\n      <ChatRoom roomId={roomId} />\n    </>\n  );\n}\n```\n\n```js src/chat.js\nexport function createConnection(serverUrl, roomId) {\n  // 실제 구현은 실제로 서버에 연결됩니다.\n  return {\n    connect() {\n      console.log('✅ Connecting to \"' + roomId + '\" room at ' + serverUrl + '...');\n    },\n    disconnect() {\n      console.log('❌ Disconnected from \"' + roomId + '\" room at ' + serverUrl);\n    }\n  };\n}\n```\n\n```css\ninput { display: block; margin-bottom: 20px; }\nbutton { margin-left: 10px; }\n```\n\n</Sandpack>\n\n</Solution>\n\n#### 동기화 켜기 및 끄기 {/*switch-synchronization-on-and-off*/}\n\n이 예시에서 effect는 창 [`pointermove`](https://developer.mozilla.org/en-US/docs/Web/API/Element/pointermove_event) 이벤트를 구독하여 화면에서 분홍색 점을 이동합니다. 미리 보기 영역 위로 마우스를 가져가서(또는 모바일 장치에서 화면을 터치하여) 분홍색 점이 어떻게 움직이는지 확인해 보세요.\n\n체크박스도 있습니다. 체크 박스를 선택하면 `canMove` state 변수가 토글되지만 이 state 변수는 코드의 어느 곳에서도 사용되지 않습니다. 여러분의 임무는 `canMove`가 `false`일 때(체크박스가 체크된 상태) 점의 이동이 중지되도록 코드를 변경하는 것입니다. 체크 박스를 다시 켜고 `canMove`를 `true`로 설정하면 상자가 다시 움직여야 합니다. 즉, 점이 움직일 수 있는지는 체크 박스의 체크 여부와 동기화 state를 유지해야 합니다.\n\n<Hint>\n\neffect는 조건부로 선언할 수 없습니다. 하지만 effect 내부의 코드는 조건을 사용할 수 있습니다!\n\n</Hint>\n\n<Sandpack>\n\n```js\nimport { useState, useEffect } from 'react';\n\nexport default function App() {\n  const [position, setPosition] = useState({ x: 0, y: 0 });\n  const [canMove, setCanMove] = useState(true);\n\n  useEffect(() => {\n    function handleMove(e) {\n      setPosition({ x: e.clientX, y: e.clientY });\n    }\n    window.addEventListener('pointermove', handleMove);\n    return () => window.removeEventListener('pointermove', handleMove);\n  }, []);\n\n  return (\n    <>\n      <label>\n        <input type=\"checkbox\"\n          checked={canMove}\n          onChange={e => setCanMove(e.target.checked)}\n        />\n        The dot is allowed to move\n      </label>\n      <hr />\n      <div style={{\n        position: 'absolute',\n        backgroundColor: 'pink',\n        borderRadius: '50%',\n        opacity: 0.6,\n        transform: `translate(${position.x}px, ${position.y}px)`,\n        pointerEvents: 'none',\n        left: -20,\n        top: -20,\n        width: 40,\n        height: 40,\n      }} />\n    </>\n  );\n}\n```\n\n```css\nbody {\n  height: 200px;\n}\n```\n\n</Sandpack>\n\n<Solution>\n\n한 가지 해결책은 `setPosition` 호출을 `if (canMove) { ... }` 조건으로 감싸는 것입니다.\n\n<Sandpack>\n\n```js\nimport { useState, useEffect } from 'react';\n\nexport default function App() {\n  const [position, setPosition] = useState({ x: 0, y: 0 });\n  const [canMove, setCanMove] = useState(true);\n\n  useEffect(() => {\n    function handleMove(e) {\n      if (canMove) {\n        setPosition({ x: e.clientX, y: e.clientY });\n      }\n    }\n    window.addEventListener('pointermove', handleMove);\n    return () => window.removeEventListener('pointermove', handleMove);\n  }, [canMove]);\n\n  return (\n    <>\n      <label>\n        <input type=\"checkbox\"\n          checked={canMove}\n          onChange={e => setCanMove(e.target.checked)}\n        />\n        The dot is allowed to move\n      </label>\n      <hr />\n      <div style={{\n        position: 'absolute',\n        backgroundColor: 'pink',\n        borderRadius: '50%',\n        opacity: 0.6,\n        transform: `translate(${position.x}px, ${position.y}px)`,\n        pointerEvents: 'none',\n        left: -20,\n        top: -20,\n        width: 40,\n        height: 40,\n      }} />\n    </>\n  );\n}\n```\n\n```css\nbody {\n  height: 200px;\n}\n```\n\n</Sandpack>\n\n또는 *이벤트 구독 로직*을 `if (canMove) { ... }` 조건으로 래핑할 수 있습니다.\n\n<Sandpack>\n\n```js\nimport { useState, useEffect } from 'react';\n\nexport default function App() {\n  const [position, setPosition] = useState({ x: 0, y: 0 });\n  const [canMove, setCanMove] = useState(true);\n\n  useEffect(() => {\n    function handleMove(e) {\n      setPosition({ x: e.clientX, y: e.clientY });\n    }\n    if (canMove) {\n      window.addEventListener('pointermove', handleMove);\n      return () => window.removeEventListener('pointermove', handleMove);\n    }\n  }, [canMove]);\n\n  return (\n    <>\n      <label>\n        <input type=\"checkbox\"\n          checked={canMove}\n          onChange={e => setCanMove(e.target.checked)}\n        />\n        The dot is allowed to move\n      </label>\n      <hr />\n      <div style={{\n        position: 'absolute',\n        backgroundColor: 'pink',\n        borderRadius: '50%',\n        opacity: 0.6,\n        transform: `translate(${position.x}px, ${position.y}px)`,\n        pointerEvents: 'none',\n        left: -20,\n        top: -20,\n        width: 40,\n        height: 40,\n      }} />\n    </>\n  );\n}\n```\n\n```css\nbody {\n  height: 200px;\n}\n```\n\n</Sandpack>\n\n이 두 경우 모두 `canMove`는 effect 내부에서 읽는 반응형 변수입니다. 그렇기 때문에 effect 종속성 목록에 지정해야 합니다. 이렇게 하면 값이 변경될 때마다 effect가 다시 동기화됩니다.\n\n</Solution>\n\n#### 오래된 값 버그 조사하기 {/*investigate-a-stale-value-bug*/}\n\n이 예시에서는 체크박스가 켜져 있으면 분홍색 점이 움직여야 하고 체크박스가 꺼져 있으면 움직임을 멈춰야 합니다. 이를 위한 로직은 이미 구현되어 있습니다. `handleMove` 이벤트 핸들러는 `canMove` state 변수를 확인합니다.\n\n그러나 어떤 이유에서인지 `handleMove` 내부의 `canMove` state 변수는 체크박스를 체크한 후에도 항상 `true`인 \"오래된\" state인 것처럼 보입니다. 어떻게 이런 일이 가능할까요? 코드에서 실수를 찾아서 수정하세요.\n\n<Hint>\n\n린터 규칙이 억압되는 것이 보이면 억압을 제거하세요! 보통 이 부분에서 실수가 발생합니다.\n\n</Hint>\n\n<Sandpack>\n\n```js\nimport { useState, useEffect } from 'react';\n\nexport default function App() {\n  const [position, setPosition] = useState({ x: 0, y: 0 });\n  const [canMove, setCanMove] = useState(true);\n\n  function handleMove(e) {\n    if (canMove) {\n      setPosition({ x: e.clientX, y: e.clientY });\n    }\n  }\n\n  useEffect(() => {\n    window.addEventListener('pointermove', handleMove);\n    return () => window.removeEventListener('pointermove', handleMove);\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, []);\n\n  return (\n    <>\n      <label>\n        <input type=\"checkbox\"\n          checked={canMove}\n          onChange={e => setCanMove(e.target.checked)}\n        />\n        The dot is allowed to move\n      </label>\n      <hr />\n      <div style={{\n        position: 'absolute',\n        backgroundColor: 'pink',\n        borderRadius: '50%',\n        opacity: 0.6,\n        transform: `translate(${position.x}px, ${position.y}px)`,\n        pointerEvents: 'none',\n        left: -20,\n        top: -20,\n        width: 40,\n        height: 40,\n      }} />\n    </>\n  );\n}\n```\n\n```css\nbody {\n  height: 200px;\n}\n```\n\n</Sandpack>\n\n<Solution>\n\n원래 코드의 문제는 의존성 린터를 억제하는 것이었습니다. 억제를 제거하면 이 effect가 `handleMove` 함수에 의존한다는 것을 알 수 있습니다. `handleMove`는 컴포넌트 본문 내부에서 선언되어 반응형 값이 되기 때문입니다. 모든 반응형 값은 종속성으로 지정해야 하며, 그렇지 않으면 시간이 지나면 낡아질 수 있습니다!\n\n원본 코드 작성자는 effect가 어떤 반응형 값에도 의존(`[]`)하지 않는다고 말함으로써 React에 \"거짓말\"을 했습니다. 이것이 바로 React가 `canMove`가 변경된 후 effect를 다시 동기화하지 않은 이유입니다(그리고 `handleMove`도 함께). React가 effect를 재동기화하지 않았기 때문에 리스너로 첨부된 `handleMove`는 초기 렌더링 중에 생성된 `handleMove` 함수입니다. 초기 렌더링하는 동안 `canMove`는 `true`이었기 때문에 초기 렌더링의 `handleMove`는 영원히 그 값을 보게 됩니다.\n\n**린터를 억제하지 않으면 오래된 값으로 인한 문제가 발생하지 않습니다.** 이 버그를 해결하는 방법에는 몇 가지가 있지만 항상 린터 억제를 제거하는 것부터 시작해야 합니다. 그런 다음 코드를 변경하여 린트 오류를 수정하세요.\n\neffect 종속성을 `[handleMove]`로 변경할 수 있지만, 렌더링할 때마다 새로 정의되는 함수가 *될 것*이므로 종속성 배열을 모두 제거하는 것이 좋습니다. 그러면 렌더링할 때마다 effect가 다시 동기화됩니다.\n\n<Sandpack>\n\n```js\nimport { useState, useEffect } from 'react';\n\nexport default function App() {\n  const [position, setPosition] = useState({ x: 0, y: 0 });\n  const [canMove, setCanMove] = useState(true);\n\n  function handleMove(e) {\n    if (canMove) {\n      setPosition({ x: e.clientX, y: e.clientY });\n    }\n  }\n\n  useEffect(() => {\n    window.addEventListener('pointermove', handleMove);\n    return () => window.removeEventListener('pointermove', handleMove);\n  });\n\n  return (\n    <>\n      <label>\n        <input type=\"checkbox\"\n          checked={canMove}\n          onChange={e => setCanMove(e.target.checked)}\n        />\n        The dot is allowed to move\n      </label>\n      <hr />\n      <div style={{\n        position: 'absolute',\n        backgroundColor: 'pink',\n        borderRadius: '50%',\n        opacity: 0.6,\n        transform: `translate(${position.x}px, ${position.y}px)`,\n        pointerEvents: 'none',\n        left: -20,\n        top: -20,\n        width: 40,\n        height: 40,\n      }} />\n    </>\n  );\n}\n```\n\n```css\nbody {\n  height: 200px;\n}\n```\n\n</Sandpack>\n\n이 솔루션은 작동하지만 이상적이지는 않습니다. effect 안에 `console.log('Resubscribing')`를 넣으면 렌더링할 때마다 재구독하는 것을 확인할 수 있습니다. 재구독은 빠르지만 너무 자주 하는 것은 피하는 것이 좋습니다.\n\n더 나은 해결책은 `handleMove` 함수를 effect *내부*로 옮기는 것입니다. 그러면 `handleMove`는 반응형 값이 아니므로 effect가 함수에 종속되지 않습니다. 대신, 이제 코드가 effect 내부에서 읽는 `canMove`에 의존해야 합니다. 이제 effect가 `canMove`의 값과 동기화 state를 유지하므로 원하는 동작과 일치합니다.\n\n<Sandpack>\n\n```js\nimport { useState, useEffect } from 'react';\n\nexport default function App() {\n  const [position, setPosition] = useState({ x: 0, y: 0 });\n  const [canMove, setCanMove] = useState(true);\n\n  useEffect(() => {\n    function handleMove(e) {\n      if (canMove) {\n        setPosition({ x: e.clientX, y: e.clientY });\n      }\n    }\n\n    window.addEventListener('pointermove', handleMove);\n    return () => window.removeEventListener('pointermove', handleMove);\n  }, [canMove]);\n\n  return (\n    <>\n      <label>\n        <input type=\"checkbox\"\n          checked={canMove}\n          onChange={e => setCanMove(e.target.checked)}\n        />\n        The dot is allowed to move\n      </label>\n      <hr />\n      <div style={{\n        position: 'absolute',\n        backgroundColor: 'pink',\n        borderRadius: '50%',\n        opacity: 0.6,\n        transform: `translate(${position.x}px, ${position.y}px)`,\n        pointerEvents: 'none',\n        left: -20,\n        top: -20,\n        width: 40,\n        height: 40,\n      }} />\n    </>\n  );\n}\n```\n\n```css\nbody {\n  height: 200px;\n}\n```\n\n</Sandpack>\n\neffect 본문에 `console.log('Resubscribing')`를 추가하면 이제 체크 박스를 토글 하거나(`canMove` 변경 사항) 코드를 편집할 때만 다시 구독하는 것을 확인할 수 있습니다. 이렇게 하면 항상 재구독하던 이전 접근 방식보다 개선되었습니다.\n\n[이벤트와 effect 분리하기](/learn/separating-events-from-effects)에서 이러한 유형의 문제에 대한 보다 일반적인 접근 방식을 배우게 됩니다.\n\n</Solution>\n\n#### 연결 스위치 수정 {/*fix-a-connection-switch*/}\n\n이 예시에서 `chat.js`의 채팅 서비스는 `createEncryptedConnection`과 `createUnencryptedConnection`이라는 두 개의 서로 다른 API를 노출합니다. 루트 `App` 컴포넌트는 사용자가 암호화 사용 여부를 선택할 수 있도록 한 다음, 해당 API 메서드를 하위 `ChatRoom` 컴포넌트에 `createConnection` prop으로 전달합니다.\n\n처음에는 콘솔 로그에 연결이 암호화되지 않았다고 표시됩니다. 체크 박스를 켜면 아무 일도 일어나지 않습니다. 그러나 그 후에 선택한 대화방을 변경하면 채팅이 다시 연결*되고* 콘솔 메시지에서 볼 수 있듯이 암호화가 활성화됩니다. 이것은 버그입니다. 체크 박스를 토글해*도* 채팅이 다시 연결되도록 버그를 수정했습니다.\n\n<Hint>\n\n린터를 억제하는 것은 항상 의심스러운 일입니다. 버그일까요?\n\n</Hint>\n\n<Sandpack>\n\n```js src/App.js\nimport { useState } from 'react';\nimport ChatRoom from './ChatRoom.js';\nimport {\n  createEncryptedConnection,\n  createUnencryptedConnection,\n} from './chat.js';\n\nexport default function App() {\n  const [roomId, setRoomId] = useState('general');\n  const [isEncrypted, setIsEncrypted] = useState(false);\n  return (\n    <>\n      <label>\n        Choose the chat room:{' '}\n        <select\n          value={roomId}\n          onChange={e => setRoomId(e.target.value)}\n        >\n          <option value=\"general\">general</option>\n          <option value=\"travel\">travel</option>\n          <option value=\"music\">music</option>\n        </select>\n      </label>\n      <label>\n        <input\n          type=\"checkbox\"\n          checked={isEncrypted}\n          onChange={e => setIsEncrypted(e.target.checked)}\n        />\n        Enable encryption\n      </label>\n      <hr />\n      <ChatRoom\n        roomId={roomId}\n        createConnection={isEncrypted ?\n          createEncryptedConnection :\n          createUnencryptedConnection\n        }\n      />\n    </>\n  );\n}\n```\n\n```js src/ChatRoom.js active\nimport { useState, useEffect } from 'react';\n\nexport default function ChatRoom({ roomId, createConnection }) {\n  useEffect(() => {\n    const connection = createConnection(roomId);\n    connection.connect();\n    return () => connection.disconnect();\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [roomId]);\n\n  return <h1>Welcome to the {roomId} room!</h1>;\n}\n```\n\n```js src/chat.js\nexport function createEncryptedConnection(roomId) {\n  // 실제 구현은 실제로 서버에 연결됩니다.\n  return {\n    connect() {\n      console.log('✅ 🔐 Connecting to \"' + roomId + '... (encrypted)');\n    },\n    disconnect() {\n      console.log('❌ 🔐 Disconnected from \"' + roomId + '\" room (encrypted)');\n    }\n  };\n}\n\nexport function createUnencryptedConnection(roomId) {\n  // 실제 구현은 실제로 서버에 연결됩니다.\n  return {\n    connect() {\n      console.log('✅ Connecting to \"' + roomId + '... (unencrypted)');\n    },\n    disconnect() {\n      console.log('❌ Disconnected from \"' + roomId + '\" room (unencrypted)');\n    }\n  };\n}\n```\n\n```css\nlabel { display: block; margin-bottom: 10px; }\n```\n\n</Sandpack>\n\n<Solution>\n\n린터 억제를 제거하면 린트 오류가 표시됩니다. 문제는 `createConnection`이 props이기 때문에 반응형 값이라는 것입니다. 시간이 지남에 따라 변경될 수 있습니다! (실제로 사용자가 체크박스를 선택하면 부모 컴포넌트가 다른 값의 `createConnection` prop을 전달합니다) 실제로 그래야 합니다. 이것이 바로 종속성이 되어야 하는 이유입니다. 목록에 포함해 버그를 수정하세요.\n\n<Sandpack>\n\n```js src/App.js\nimport { useState } from 'react';\nimport ChatRoom from './ChatRoom.js';\nimport {\n  createEncryptedConnection,\n  createUnencryptedConnection,\n} from './chat.js';\n\nexport default function App() {\n  const [roomId, setRoomId] = useState('general');\n  const [isEncrypted, setIsEncrypted] = useState(false);\n  return (\n    <>\n      <label>\n        Choose the chat room:{' '}\n        <select\n          value={roomId}\n          onChange={e => setRoomId(e.target.value)}\n        >\n          <option value=\"general\">general</option>\n          <option value=\"travel\">travel</option>\n          <option value=\"music\">music</option>\n        </select>\n      </label>\n      <label>\n        <input\n          type=\"checkbox\"\n          checked={isEncrypted}\n          onChange={e => setIsEncrypted(e.target.checked)}\n        />\n        Enable encryption\n      </label>\n      <hr />\n      <ChatRoom\n        roomId={roomId}\n        createConnection={isEncrypted ?\n          createEncryptedConnection :\n          createUnencryptedConnection\n        }\n      />\n    </>\n  );\n}\n```\n\n```js src/ChatRoom.js active\nimport { useState, useEffect } from 'react';\n\nexport default function ChatRoom({ roomId, createConnection }) {\n  useEffect(() => {\n    const connection = createConnection(roomId);\n    connection.connect();\n    return () => connection.disconnect();\n  }, [roomId, createConnection]);\n\n  return <h1>Welcome to the {roomId} room!</h1>;\n}\n```\n\n```js src/chat.js\nexport function createEncryptedConnection(roomId) {\n  // 실제 구현은 실제로 서버에 연결됩니다.\n  return {\n    connect() {\n      console.log('✅ 🔐 Connecting to \"' + roomId + '... (encrypted)');\n    },\n    disconnect() {\n      console.log('❌ 🔐 Disconnected from \"' + roomId + '\" room (encrypted)');\n    }\n  };\n}\n\nexport function createUnencryptedConnection(roomId) {\n  // 실제 구현은 실제로 서버에 연결됩니다.\n  return {\n    connect() {\n      console.log('✅ Connecting to \"' + roomId + '... (unencrypted)');\n    },\n    disconnect() {\n      console.log('❌ Disconnected from \"' + roomId + '\" room (unencrypted)');\n    }\n  };\n}\n```\n\n```css\nlabel { display: block; margin-bottom: 10px; }\n```\n\n</Sandpack>\n\n`createConnection`이 종속성이라는 것은 맞습니다. 하지만 누군가 이 프로퍼티의 값으로 인라인 함수를 전달하도록 `App` 컴포넌트를 편집할 수 있기 때문에 이 코드는 약간 취약합니다. 이 경우 `App` 컴포넌트가 다시 렌더링할 때마다 값이 달라지므로 effect가 너무 자주 다시 동기화될 수 있습니다. 이를 방지하려면 대신 `isEncrypted`를 전달할 수 있습니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { useState } from 'react';\nimport ChatRoom from './ChatRoom.js';\n\nexport default function App() {\n  const [roomId, setRoomId] = useState('general');\n  const [isEncrypted, setIsEncrypted] = useState(false);\n  return (\n    <>\n      <label>\n        Choose the chat room:{' '}\n        <select\n          value={roomId}\n          onChange={e => setRoomId(e.target.value)}\n        >\n          <option value=\"general\">general</option>\n          <option value=\"travel\">travel</option>\n          <option value=\"music\">music</option>\n        </select>\n      </label>\n      <label>\n        <input\n          type=\"checkbox\"\n          checked={isEncrypted}\n          onChange={e => setIsEncrypted(e.target.checked)}\n        />\n        Enable encryption\n      </label>\n      <hr />\n      <ChatRoom\n        roomId={roomId}\n        isEncrypted={isEncrypted}\n      />\n    </>\n  );\n}\n```\n\n```js src/ChatRoom.js active\nimport { useState, useEffect } from 'react';\nimport {\n  createEncryptedConnection,\n  createUnencryptedConnection,\n} from './chat.js';\n\nexport default function ChatRoom({ roomId, isEncrypted }) {\n  useEffect(() => {\n    const createConnection = isEncrypted ?\n      createEncryptedConnection :\n      createUnencryptedConnection;\n    const connection = createConnection(roomId);\n    connection.connect();\n    return () => connection.disconnect();\n  }, [roomId, isEncrypted]);\n\n  return <h1>Welcome to the {roomId} room!</h1>;\n}\n```\n\n```js src/chat.js\nexport function createEncryptedConnection(roomId) {\n  // 실제 구현은 실제로 서버에 연결됩니다.\n  return {\n    connect() {\n      console.log('✅ 🔐 Connecting to \"' + roomId + '... (encrypted)');\n    },\n    disconnect() {\n      console.log('❌ 🔐 Disconnected from \"' + roomId + '\" room (encrypted)');\n    }\n  };\n}\n\nexport function createUnencryptedConnection(roomId) {\n  // 실제 구현은 실제로 서버에 연결됩니다.\n  return {\n    connect() {\n      console.log('✅ Connecting to \"' + roomId + '... (unencrypted)');\n    },\n    disconnect() {\n      console.log('❌ Disconnected from \"' + roomId + '\" room (unencrypted)');\n    }\n  };\n}\n```\n\n```css\nlabel { display: block; margin-bottom: 10px; }\n```\n\n</Sandpack>\n\n이 버전에서는 `App` 컴포넌트가 함수 대신 boolean prop을 전달합니다. effect 내에서 어떤 함수를 사용할지 결정합니다. `createEncryptedConnection`과 `createUnencryptedConnection`은 모두 컴포넌트 외부에서 선언되므로 반응형이 아니므로 종속성이 될 필요가 없습니다. 이에 대한 자세한 내용은 [effect 종속성 제거하기](/learn/removing-effect-dependencies)에서 확인할 수 있습니다.\n\n</Solution>\n\n#### select box 체인 채우기 {/*populate-a-chain-of-select-boxes*/}\n\n이 예시에는 두 개의 select box가 있습니다. 하나의 select box에서 사용자는 행성을 선택할 수 있습니다. 다른 select box는 사용자가 해당 *행성의* 장소를 선택할 수 있도록 합니다. 두 번째 상자는 아직 작동하지 않습니다. 여러분의 임무는 선택한 행성의 장소를 표시하도록 만드는 것입니다.\n\n첫 번째 select box의 작동 방식을 살펴보겠습니다. 이 상자는 `\"/planets\"` API 호출의 결과로 `planetList` state를 채웁니다. 현재 선택된 행성의 ID는 `planetId` state 변수에 보관됩니다. `placeList` state 변수가 `\"/planets/\" + planetId + \"/places\"` API 호출의 결과로 채워지도록 몇 가지 추가 코드를 추가할 위치를 찾아야 합니다.\n\n이 코드를 올바르게 구현하면 행성을 선택하면 장소 목록이 채워져야 합니다. 행성을 변경하면 장소 목록이 변경되어야 합니다.\n\n<Hint>\n\n두 개의 독립적인 동기화 프로세스가 있는 경우 두 개의 개별 effect를 작성해야 합니다.\n\n</Hint>\n\n<Sandpack>\n\n```js src/App.js\nimport { useState, useEffect } from 'react';\nimport { fetchData } from './api.js';\n\nexport default function Page() {\n  const [planetList, setPlanetList] = useState([])\n  const [planetId, setPlanetId] = useState('');\n\n  const [placeList, setPlaceList] = useState([]);\n  const [placeId, setPlaceId] = useState('');\n\n  useEffect(() => {\n    let ignore = false;\n    fetchData('/planets').then(result => {\n      if (!ignore) {\n        console.log('Fetched a list of planets.');\n        setPlanetList(result);\n        setPlanetId(result[0].id); // 첫 번째 행성을 선택합니다.\n      }\n    });\n    return () => {\n      ignore = true;\n    }\n  }, []);\n\n  return (\n    <>\n      <label>\n        Pick a planet:{' '}\n        <select value={planetId} onChange={e => {\n          setPlanetId(e.target.value);\n        }}>\n          {planetList.map(planet =>\n            <option key={planet.id} value={planet.id}>{planet.name}</option>\n          )}\n        </select>\n      </label>\n      <label>\n        Pick a place:{' '}\n        <select value={placeId} onChange={e => {\n          setPlaceId(e.target.value);\n        }}>\n          {placeList.map(place =>\n            <option key={place.id} value={place.id}>{place.name}</option>\n          )}\n        </select>\n      </label>\n      <hr />\n      <p>You are going to: {placeId || '???'} on {planetId || '???'} </p>\n    </>\n  );\n}\n```\n\n```js src/api.js hidden\nexport function fetchData(url) {\n  if (url === '/planets') {\n    return fetchPlanets();\n  } else if (url.startsWith('/planets/')) {\n    const match = url.match(/^\\/planets\\/([\\w-]+)\\/places(\\/)?$/);\n    if (!match || !match[1] || !match[1].length) {\n      throw Error('Expected URL like \"/planets/earth/places\". Received: \"' + url + '\".');\n    }\n    return fetchPlaces(match[1]);\n  } else throw Error('Expected URL like \"/planets\" or \"/planets/earth/places\". Received: \"' + url + '\".');\n}\n\nasync function fetchPlanets() {\n  return new Promise(resolve => {\n    setTimeout(() => {\n      resolve([{\n        id: 'earth',\n        name: 'Earth'\n      }, {\n        id: 'venus',\n        name: 'Venus'\n      }, {\n        id: 'mars',\n        name: 'Mars'\n      }]);\n    }, 1000);\n  });\n}\n\nasync function fetchPlaces(planetId) {\n  if (typeof planetId !== 'string') {\n    throw Error(\n      'fetchPlaces(planetId) expects a string argument. ' +\n      'Instead received: ' + planetId + '.'\n    );\n  }\n  return new Promise(resolve => {\n    setTimeout(() => {\n      if (planetId === 'earth') {\n        resolve([{\n          id: 'laos',\n          name: 'Laos'\n        }, {\n          id: 'spain',\n          name: 'Spain'\n        }, {\n          id: 'vietnam',\n          name: 'Vietnam'\n        }]);\n      } else if (planetId === 'venus') {\n        resolve([{\n          id: 'aurelia',\n          name: 'Aurelia'\n        }, {\n          id: 'diana-chasma',\n          name: 'Diana Chasma'\n        }, {\n          id: 'kumsong-vallis',\n          name: 'Kŭmsŏng Vallis'\n        }]);\n      } else if (planetId === 'mars') {\n        resolve([{\n          id: 'aluminum-city',\n          name: 'Aluminum City'\n        }, {\n          id: 'new-new-york',\n          name: 'New New York'\n        }, {\n          id: 'vishniac',\n          name: 'Vishniac'\n        }]);\n      } else throw Error('Unknown planet ID: ' + planetId);\n    }, 1000);\n  });\n}\n```\n\n```css\nlabel { display: block; margin-bottom: 10px; }\n```\n\n</Sandpack>\n\n<Solution>\n\n두 가지 독립적인 동기화 프로세스가 있습니다.\n\n- 첫 번째 선택 상자는 원격 행성 목록에 동기화됩니다.\n- 두 번째 선택 상자는 현재 `planetId`에 대한 원격 장소 목록과 동기화됩니다.\n\n그렇기 때문에 두 개의 개별 effect로 설명하는 것이 합리적입니다. 다음은 이를 수행하는 방법에 대한 예시입니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { useState, useEffect } from 'react';\nimport { fetchData } from './api.js';\n\nexport default function Page() {\n  const [planetList, setPlanetList] = useState([])\n  const [planetId, setPlanetId] = useState('');\n\n  const [placeList, setPlaceList] = useState([]);\n  const [placeId, setPlaceId] = useState('');\n\n  useEffect(() => {\n    let ignore = false;\n    fetchData('/planets').then(result => {\n      if (!ignore) {\n        console.log('Fetched a list of planets.');\n        setPlanetList(result);\n        setPlanetId(result[0].id); // 첫 번째 행성을 선택합니다.\n      }\n    });\n    return () => {\n      ignore = true;\n    }\n  }, []);\n\n  useEffect(() => {\n    if (planetId === '') {\n      // 첫 번째 상자에서는 아직 아무것도 선택되지 않았습니다.\n      return;\n    }\n\n    let ignore = false;\n    fetchData('/planets/' + planetId + '/places').then(result => {\n      if (!ignore) {\n        console.log('Fetched a list of places on \"' + planetId + '\".');\n        setPlaceList(result);\n        setPlaceId(result[0].id); // 첫 번째 장소를 선택합니다.\n      }\n    });\n    return () => {\n      ignore = true;\n    }\n  }, [planetId]);\n\n  return (\n    <>\n      <label>\n        Pick a planet:{' '}\n        <select value={planetId} onChange={e => {\n          setPlanetId(e.target.value);\n        }}>\n          {planetList.map(planet =>\n            <option key={planet.id} value={planet.id}>{planet.name}</option>\n          )}\n        </select>\n      </label>\n      <label>\n        Pick a place:{' '}\n        <select value={placeId} onChange={e => {\n          setPlaceId(e.target.value);\n        }}>\n          {placeList.map(place =>\n            <option key={place.id} value={place.id}>{place.name}</option>\n          )}\n        </select>\n      </label>\n      <hr />\n      <p>You are going to: {placeId || '???'} on {planetId || '???'} </p>\n    </>\n  );\n}\n```\n\n```js src/api.js hidden\nexport function fetchData(url) {\n  if (url === '/planets') {\n    return fetchPlanets();\n  } else if (url.startsWith('/planets/')) {\n    const match = url.match(/^\\/planets\\/([\\w-]+)\\/places(\\/)?$/);\n    if (!match || !match[1] || !match[1].length) {\n      throw Error('Expected URL like \"/planets/earth/places\". Received: \"' + url + '\".');\n    }\n    return fetchPlaces(match[1]);\n  } else throw Error('Expected URL like \"/planets\" or \"/planets/earth/places\". Received: \"' + url + '\".');\n}\n\nasync function fetchPlanets() {\n  return new Promise(resolve => {\n    setTimeout(() => {\n      resolve([{\n        id: 'earth',\n        name: 'Earth'\n      }, {\n        id: 'venus',\n        name: 'Venus'\n      }, {\n        id: 'mars',\n        name: 'Mars'\n      }]);\n    }, 1000);\n  });\n}\n\nasync function fetchPlaces(planetId) {\n  if (typeof planetId !== 'string') {\n    throw Error(\n      'fetchPlaces(planetId) expects a string argument. ' +\n      'Instead received: ' + planetId + '.'\n    );\n  }\n  return new Promise(resolve => {\n    setTimeout(() => {\n      if (planetId === 'earth') {\n        resolve([{\n          id: 'laos',\n          name: 'Laos'\n        }, {\n          id: 'spain',\n          name: 'Spain'\n        }, {\n          id: 'vietnam',\n          name: 'Vietnam'\n        }]);\n      } else if (planetId === 'venus') {\n        resolve([{\n          id: 'aurelia',\n          name: 'Aurelia'\n        }, {\n          id: 'diana-chasma',\n          name: 'Diana Chasma'\n        }, {\n          id: 'kumsong-vallis',\n          name: 'Kŭmsŏng Vallis'\n        }]);\n      } else if (planetId === 'mars') {\n        resolve([{\n          id: 'aluminum-city',\n          name: 'Aluminum City'\n        }, {\n          id: 'new-new-york',\n          name: 'New New York'\n        }, {\n          id: 'vishniac',\n          name: 'Vishniac'\n        }]);\n      } else throw Error('Unknown planet ID: ' + planetId);\n    }, 1000);\n  });\n}\n```\n\n```css\nlabel { display: block; margin-bottom: 10px; }\n```\n\n</Sandpack>\n\n이 코드는 약간 반복적입니다. 하지만 그렇다고 해서 이를 하나의 effect로 결합해야 하는 이유는 없습니다! 이렇게 하면 두 effect의 종속성을 하나의 목록으로 결합한 다음 행성을 변경하면 모든 행성 목록을 다시 가져와야 합니다. effect는 코드 재사용을 위한 도구가 아닙니다.\n\n대신 반복을 줄이기 위해 아래의 `useSelectOptions`와 같은 커스텀 Hook에 일부 로직을 추출할 수 있습니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { useState } from 'react';\nimport { useSelectOptions } from './useSelectOptions.js';\n\nexport default function Page() {\n  const [\n    planetList,\n    planetId,\n    setPlanetId\n  ] = useSelectOptions('/planets');\n\n  const [\n    placeList,\n    placeId,\n    setPlaceId\n  ] = useSelectOptions(planetId ? `/planets/${planetId}/places` : null);\n\n  return (\n    <>\n      <label>\n        Pick a planet:{' '}\n        <select value={planetId} onChange={e => {\n          setPlanetId(e.target.value);\n        }}>\n          {planetList?.map(planet =>\n            <option key={planet.id} value={planet.id}>{planet.name}</option>\n          )}\n        </select>\n      </label>\n      <label>\n        Pick a place:{' '}\n        <select value={placeId} onChange={e => {\n          setPlaceId(e.target.value);\n        }}>\n          {placeList?.map(place =>\n            <option key={place.id} value={place.id}>{place.name}</option>\n          )}\n        </select>\n      </label>\n      <hr />\n      <p>You are going to: {placeId || '...'} on {planetId || '...'} </p>\n    </>\n  );\n}\n```\n\n```js src/useSelectOptions.js\nimport { useState, useEffect } from 'react';\nimport { fetchData } from './api.js';\n\nexport function useSelectOptions(url) {\n  const [list, setList] = useState(null);\n  const [selectedId, setSelectedId] = useState('');\n  useEffect(() => {\n    if (url === null) {\n      return;\n    }\n\n    let ignore = false;\n    fetchData(url).then(result => {\n      if (!ignore) {\n        setList(result);\n        setSelectedId(result[0].id);\n      }\n    });\n    return () => {\n      ignore = true;\n    }\n  }, [url]);\n  return [list, selectedId, setSelectedId];\n}\n```\n\n```js src/api.js hidden\nexport function fetchData(url) {\n  if (url === '/planets') {\n    return fetchPlanets();\n  } else if (url.startsWith('/planets/')) {\n    const match = url.match(/^\\/planets\\/([\\w-]+)\\/places(\\/)?$/);\n    if (!match || !match[1] || !match[1].length) {\n      throw Error('Expected URL like \"/planets/earth/places\". Received: \"' + url + '\".');\n    }\n    return fetchPlaces(match[1]);\n  } else throw Error('Expected URL like \"/planets\" or \"/planets/earth/places\". Received: \"' + url + '\".');\n}\n\nasync function fetchPlanets() {\n  return new Promise(resolve => {\n    setTimeout(() => {\n      resolve([{\n        id: 'earth',\n        name: 'Earth'\n      }, {\n        id: 'venus',\n        name: 'Venus'\n      }, {\n        id: 'mars',\n        name: 'Mars'\n      }]);\n    }, 1000);\n  });\n}\n\nasync function fetchPlaces(planetId) {\n  if (typeof planetId !== 'string') {\n    throw Error(\n      'fetchPlaces(planetId) expects a string argument. ' +\n      'Instead received: ' + planetId + '.'\n    );\n  }\n  return new Promise(resolve => {\n    setTimeout(() => {\n      if (planetId === 'earth') {\n        resolve([{\n          id: 'laos',\n          name: 'Laos'\n        }, {\n          id: 'spain',\n          name: 'Spain'\n        }, {\n          id: 'vietnam',\n          name: 'Vietnam'\n        }]);\n      } else if (planetId === 'venus') {\n        resolve([{\n          id: 'aurelia',\n          name: 'Aurelia'\n        }, {\n          id: 'diana-chasma',\n          name: 'Diana Chasma'\n        }, {\n          id: 'kumsong-vallis',\n          name: 'Kŭmsŏng Vallis'\n        }]);\n      } else if (planetId === 'mars') {\n        resolve([{\n          id: 'aluminum-city',\n          name: 'Aluminum City'\n        }, {\n          id: 'new-new-york',\n          name: 'New New York'\n        }, {\n          id: 'vishniac',\n          name: 'Vishniac'\n        }]);\n      } else throw Error('Unknown planet ID: ' + planetId);\n    }, 1000);\n  });\n}\n```\n\n```css\nlabel { display: block; margin-bottom: 10px; }\n```\n\n</Sandpack>\n\nsandbox에서 `useSelectOptions.js` 탭을 확인하여 작동 방식을 확인하세요. 이상적으로는 애플리케이션의 대부분 effect는 사용자가 직접 작성했든 커뮤니티에서 작성했든 결국 커스텀 hook으로 대체되어야 합니다. 커스텀 hook은 동기화 로직을 숨기므로 호출 컴포넌트는 effect에 대해 알지 못합니다. 앱을 계속 개발하다 보면 선택할 수 있는 Hook 팔레트를 개발하게 될 것이고, 결국에는 컴포넌트에 effect를 자주 작성할 필요가 없게 될 것입니다.\n\n</Solution>\n\n</Challenges>\n"
  },
  {
    "path": "src/content/learn/managing-state.md",
    "content": "---\ntitle: State 관리하기\n---\n\n<Intro>\n\n애플리케이션이 커짐에 따라, state가 어떻게 구성되는지 그리고 데이터가 컴포넌트 간에 어떻게 흐르는지에 대해 의식적으로 파악하면 도움이 됩니다. 불필요하거나 중복된 state는 버그의 흔한 원인입니다. 이 장에서는 state를 잘 구성하는 방법, state 업데이트 로직을 유지 보수 가능하게 관리하는 방법, 그리고 멀리 있는 컴포넌트 간에 state를 공유하는 방법에 대해 알아봅니다.\n\n</Intro>\n\n<YouWillLearn isChapter={true}>\n\n* [UI 변경을 state 변경으로 생각하는 방법](/learn/reacting-to-input-with-state)\n* [State를 잘 구조화하는 방법](/learn/choosing-the-state-structure)\n* [\"State를 끌어올려\" 컴포넌트 간에 공유하는 방법](/learn/sharing-state-between-components)\n* [State가 보존될지 초기화될지 컨트롤하는 방법](/learn/preserving-and-resetting-state)\n* [복잡한 State 로직을 함수로 통합하는 방법](/learn/extracting-state-logic-into-a-reducer)\n* [\"Prop drilling\" 없이 정보를 전달하는 방법](/learn/passing-data-deeply-with-context)\n* [앱이 커짐에 따라 state 관리를 확장하는 방법](/learn/scaling-up-with-reducer-and-context)\n\n</YouWillLearn>\n\n## State를 사용해 input 다루기 {/*reacting-to-input-with-state*/}\n\nReact를 사용하면 코드에서 직접 UI를 수정하지 않습니다. 예를 들어 \"버튼 비활성화\", \"버튼 활성화\", \"성공 메시지 표시\" 등의 명령을 작성하지 않습니다. 대신 컴포넌트의 여러 시각적 상태(\"초기 상태\", \"입력 상태\", \"성공 상태\")에 대해 보고 싶은 UI를 설명하고, 사용자 입력에 따라 state 변경을 유발합니다. 이는 디자이너가 UI를 바라보는 방식과 비슷합니다.\n\n여기 React로 구현된 퀴즈 폼이 있습니다. `status` state 변수를 사용해 제출 버튼을 활성화 혹은 비활성화할지, 또는 성공 메시지를 대신 표지할지 여부를 결정하는 것에 주목해 주세요.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function Form() {\n  const [answer, setAnswer] = useState('');\n  const [error, setError] = useState(null);\n  const [status, setStatus] = useState('typing');\n\n  if (status === 'success') {\n    return <h1>That's right!</h1>\n  }\n\n  async function handleSubmit(e) {\n    e.preventDefault();\n    setStatus('submitting');\n    try {\n      await submitForm(answer);\n      setStatus('success');\n    } catch (err) {\n      setStatus('typing');\n      setError(err);\n    }\n  }\n\n  function handleTextareaChange(e) {\n    setAnswer(e.target.value);\n  }\n\n  return (\n    <>\n      <h2>City quiz</h2>\n      <p>\n        In which city is there a billboard that turns air into drinkable water?\n      </p>\n      <form onSubmit={handleSubmit}>\n        <textarea\n          value={answer}\n          onChange={handleTextareaChange}\n          disabled={status === 'submitting'}\n        />\n        <br />\n        <button disabled={\n          answer.length === 0 ||\n          status === 'submitting'\n        }>\n          Submit\n        </button>\n        {error !== null &&\n          <p className=\"Error\">\n            {error.message}\n          </p>\n        }\n      </form>\n    </>\n  );\n}\n\nfunction submitForm(answer) {\n  // Pretend it's hitting the network.\n  return new Promise((resolve, reject) => {\n    setTimeout(() => {\n      let shouldError = answer.toLowerCase() !== 'lima'\n      if (shouldError) {\n        reject(new Error('Good guess but a wrong answer. Try again!'));\n      } else {\n        resolve();\n      }\n    }, 1500);\n  });\n}\n```\n\n```css\n.Error { color: red; }\n```\n\n</Sandpack>\n\n<LearnMore path=\"/learn/reacting-to-input-with-state\">\n\nState-driven 사고방식으로 상호작용에 접근하는 법을 배우려면 **[State를 사용해 Input 다루기](/learn/reacting-to-input-with-state)** 를 읽어보세요.\n\n</LearnMore>\n\n## State 구조 선택하기 {/*choosing-the-state-structure*/}\n\nState를 잘 구조화한다면 지속적인 버그의 원인이 되는 컴포넌트가 아닌, 수정과 디버깅이 용이한 컴포넌트를 만들 수 있습니다. 가장 중요한 원칙은 state가 중복되거나 불필요한 정보를 포함하지 않는 것입니다. 불필요한 state가 있다면 업데이트하는 것을 잊어버려 버그가 발생하기 쉽습니다!\n\n예를 들어 아래 폼에는 **중복된** `fullName` state 변수가 있습니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function Form() {\n  const [firstName, setFirstName] = useState('');\n  const [lastName, setLastName] = useState('');\n  const [fullName, setFullName] = useState('');\n\n  function handleFirstNameChange(e) {\n    setFirstName(e.target.value);\n    setFullName(e.target.value + ' ' + lastName);\n  }\n\n  function handleLastNameChange(e) {\n    setLastName(e.target.value);\n    setFullName(firstName + ' ' + e.target.value);\n  }\n\n  return (\n    <>\n      <h2>Let’s check you in</h2>\n      <label>\n        First name:{' '}\n        <input\n          value={firstName}\n          onChange={handleFirstNameChange}\n        />\n      </label>\n      <label>\n        Last name:{' '}\n        <input\n          value={lastName}\n          onChange={handleLastNameChange}\n        />\n      </label>\n      <p>\n        Your ticket will be issued to: <b>{fullName}</b>\n      </p>\n    </>\n  );\n}\n```\n\n```css\nlabel { display: block; margin-bottom: 5px; }\n```\n\n</Sandpack>\n\n컴포넌트가 렌더링 되는 동안 `fullName` 을 계산해 이를 제거하고 코드를 단순화할 수 있습니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function Form() {\n  const [firstName, setFirstName] = useState('');\n  const [lastName, setLastName] = useState('');\n\n  const fullName = firstName + ' ' + lastName;\n\n  function handleFirstNameChange(e) {\n    setFirstName(e.target.value);\n  }\n\n  function handleLastNameChange(e) {\n    setLastName(e.target.value);\n  }\n\n  return (\n    <>\n      <h2>Let’s check you in</h2>\n      <label>\n        First name:{' '}\n        <input\n          value={firstName}\n          onChange={handleFirstNameChange}\n        />\n      </label>\n      <label>\n        Last name:{' '}\n        <input\n          value={lastName}\n          onChange={handleLastNameChange}\n        />\n      </label>\n      <p>\n        Your ticket will be issued to: <b>{fullName}</b>\n      </p>\n    </>\n  );\n}\n```\n\n```css\nlabel { display: block; margin-bottom: 5px; }\n```\n\n</Sandpack>\n\n이 변경이 사소해 보일 수 있지만, React 앱의 많은 버그가 이러한 방식으로 수정됩니다.\n\n<LearnMore path=\"/learn/choosing-the-state-structure\">\n\n버그 방지를 위해 state 구조를 설계하는 방법을 배우려면 **[State 구조 선택하기](/learn/choosing-the-state-structure)** 를 읽어보세요.\n\n</LearnMore>\n\n## 컴포넌트 간 State 공유하기 {/*sharing-state-between-components*/}\n\n때때로 두 컴포넌트의 state가 항상 함께 변경되기를 원할 수 있습니다. 이를 위해서는 각 컴포넌트에서 state를 제거하고 가장 가까운 공통 부모 컴포넌트로 옮긴 후 props로 자식들에게 전달해야 합니다. 이 방법을 \"state 끌어올리기\"라고 하며, React 코드를 작성할 때 가장 흔히 하는 일 중 하나입니다.\n\n아래 예시에서는 한 번에 하나의 패널만 활성화되어야 합니다. 이를 위해 개별 패널 내에서 활성 state를 유지하는 대신, 부모 컴포넌트에서 state를 관리하고 자식들의 props를 지정합니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function Accordion() {\n  const [activeIndex, setActiveIndex] = useState(0);\n  return (\n    <>\n      <h2>Almaty, Kazakhstan</h2>\n      <Panel\n        title=\"About\"\n        isActive={activeIndex === 0}\n        onShow={() => setActiveIndex(0)}\n      >\n        With a population of about 2 million, Almaty is Kazakhstan's largest city. From 1929 to 1997, it was its capital city.\n      </Panel>\n      <Panel\n        title=\"Etymology\"\n        isActive={activeIndex === 1}\n        onShow={() => setActiveIndex(1)}\n      >\n        The name comes from <span lang=\"kk-KZ\">алма</span>, the Kazakh word for \"apple\" and is often translated as \"full of apples\". In fact, the region surrounding Almaty is thought to be the ancestral home of the apple, and the wild <i lang=\"la\">Malus sieversii</i> is considered a likely candidate for the ancestor of the modern domestic apple.\n      </Panel>\n    </>\n  );\n}\n\nfunction Panel({\n  title,\n  children,\n  isActive,\n  onShow\n}) {\n  return (\n    <section className=\"panel\">\n      <h3>{title}</h3>\n      {isActive ? (\n        <p>{children}</p>\n      ) : (\n        <button onClick={onShow}>\n          Show\n        </button>\n      )}\n    </section>\n  );\n}\n```\n\n```css\nh3, p { margin: 5px 0px; }\n.panel {\n  padding: 10px;\n  border: 1px solid #aaa;\n}\n```\n\n</Sandpack>\n\n<LearnMore path=\"/learn/sharing-state-between-components\">\n\nState를 끌어올려 컴포넌트들을 동기화하는 방법을 배우려면 **[컴포넌트 간 State 공유하기](/learn/sharing-state-between-components)** 를 읽어보세요.\n\n</LearnMore>\n\n## State를 보존하고 초기화하기 {/*preserving-and-resetting-state*/}\n\n컴포넌트가 리렌더링 될 때, React는 트리에서 유지(및 업데이트) 할 부분과, 버리거나 다시 생성할 부분을 결정해야 합니다. 대부분의 경우 React의 자동화된 동작이 충분히 잘 작동합니다. 기본적으로 React는 기존에 렌더링 된 컴포넌트 트리와 \"일치하는\" 트리 부분을 보존합니다.\n\n하지만 때로는 이것이 바람직한 동작이 아닐 수 있습니다. 아래 채팅 앱에서는 메시지를 입력한 후에 수신자를 변경하더라도 입력이 초기화되지 않습니다. 따라서 사용자가 실수로 잘못된 사람에게 메시지를 보낼 수도 있습니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { useState } from 'react';\nimport Chat from './Chat.js';\nimport ContactList from './ContactList.js';\n\nexport default function Messenger() {\n  const [to, setTo] = useState(contacts[0]);\n  return (\n    <div>\n      <ContactList\n        contacts={contacts}\n        selectedContact={to}\n        onSelect={contact => setTo(contact)}\n      />\n      <Chat contact={to} />\n    </div>\n  )\n}\n\nconst contacts = [\n  { name: 'Taylor', email: 'taylor@mail.com' },\n  { name: 'Alice', email: 'alice@mail.com' },\n  { name: 'Bob', email: 'bob@mail.com' }\n];\n```\n\n```js src/ContactList.js\nexport default function ContactList({\n  selectedContact,\n  contacts,\n  onSelect\n}) {\n  return (\n    <section className=\"contact-list\">\n      <ul>\n        {contacts.map(contact =>\n          <li key={contact.email}>\n            <button onClick={() => {\n              onSelect(contact);\n            }}>\n              {contact.name}\n            </button>\n          </li>\n        )}\n      </ul>\n    </section>\n  );\n}\n```\n\n```js src/Chat.js\nimport { useState } from 'react';\n\nexport default function Chat({ contact }) {\n  const [text, setText] = useState('');\n  return (\n    <section className=\"chat\">\n      <textarea\n        value={text}\n        placeholder={'Chat to ' + contact.name}\n        onChange={e => setText(e.target.value)}\n      />\n      <br />\n      <button>Send to {contact.email}</button>\n    </section>\n  );\n}\n```\n\n```css\n.chat, .contact-list {\n  float: left;\n  margin-bottom: 20px;\n}\nul, li {\n  list-style: none;\n  margin: 0;\n  padding: 0;\n}\nli button {\n  width: 100px;\n  padding: 10px;\n  margin-right: 10px;\n}\ntextarea {\n  height: 150px;\n}\n```\n\n</Sandpack>\n\n`<Chat key={email} />` 처럼 다른 `key`를 전달함으로써 React의 기본 동작을 무시하고 *강제로* 컴포넌트의 상태를 초기화할 수 있습니다. 이를 통해 수신자가 다르다면 새로운 데이터(및 input과 같은 UI)로 처음부터 다시 생성해야 하는 **별개의** Chat 컴포넌트로 간주해야 한다는 것을 React에 알려줍니다. 이제 수신자를 변경하면 같은 컴포넌트를 렌더링하더라도 input 필드가 초기화됩니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { useState } from 'react';\nimport Chat from './Chat.js';\nimport ContactList from './ContactList.js';\n\nexport default function Messenger() {\n  const [to, setTo] = useState(contacts[0]);\n  return (\n    <div>\n      <ContactList\n        contacts={contacts}\n        selectedContact={to}\n        onSelect={contact => setTo(contact)}\n      />\n      <Chat key={to.email} contact={to} />\n    </div>\n  )\n}\n\nconst contacts = [\n  { name: 'Taylor', email: 'taylor@mail.com' },\n  { name: 'Alice', email: 'alice@mail.com' },\n  { name: 'Bob', email: 'bob@mail.com' }\n];\n```\n\n```js src/ContactList.js\nexport default function ContactList({\n  selectedContact,\n  contacts,\n  onSelect\n}) {\n  return (\n    <section className=\"contact-list\">\n      <ul>\n        {contacts.map(contact =>\n          <li key={contact.email}>\n            <button onClick={() => {\n              onSelect(contact);\n            }}>\n              {contact.name}\n            </button>\n          </li>\n        )}\n      </ul>\n    </section>\n  );\n}\n```\n\n```js src/Chat.js\nimport { useState } from 'react';\n\nexport default function Chat({ contact }) {\n  const [text, setText] = useState('');\n  return (\n    <section className=\"chat\">\n      <textarea\n        value={text}\n        placeholder={'Chat to ' + contact.name}\n        onChange={e => setText(e.target.value)}\n      />\n      <br />\n      <button>Send to {contact.email}</button>\n    </section>\n  );\n}\n```\n\n```css\n.chat, .contact-list {\n  float: left;\n  margin-bottom: 20px;\n}\nul, li {\n  list-style: none;\n  margin: 0;\n  padding: 0;\n}\nli button {\n  width: 100px;\n  padding: 10px;\n  margin-right: 10px;\n}\ntextarea {\n  height: 150px;\n}\n```\n\n</Sandpack>\n\n<LearnMore path=\"/learn/preserving-and-resetting-state\">\n\nState의 생명주기, 그리고 생명주기를 컨트롤하는 방법을 배우려면 **[State를 보존하고 초기화하기](/learn/preserving-and-resetting-state)** 를 읽어보세요.\n\n</LearnMore>\n\n## State 로직을 reducer로 작성하기 {/*extracting-state-logic-into-a-reducer*/}\n\n여러 이벤트 핸들러를 통해 많은 state 업데이트가 이루어지는 컴포넌트는 감당하기 힘들 수 있습니다. 이 때 컴포넌트 외부에서 \"reducer\"라는 단일 함수를 사용하여 모든 state 업데이트 로직을 통합할 수 있습니다. 이벤트 핸들러는 오로지 사용자의 \"action\"만을 명시하므로 간결해집니다. 각 action에 대한 state 업데이트 방법은 파일 맨 마지막 부분의 reducer 함수에 명시되어 있습니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { useReducer } from 'react';\nimport AddTask from './AddTask.js';\nimport TaskList from './TaskList.js';\n\nexport default function TaskApp() {\n  const [tasks, dispatch] = useReducer(\n    tasksReducer,\n    initialTasks\n  );\n\n  function handleAddTask(text) {\n    dispatch({\n      type: 'added',\n      id: nextId++,\n      text: text,\n    });\n  }\n\n  function handleChangeTask(task) {\n    dispatch({\n      type: 'changed',\n      task: task\n    });\n  }\n\n  function handleDeleteTask(taskId) {\n    dispatch({\n      type: 'deleted',\n      id: taskId\n    });\n  }\n\n  return (\n    <>\n      <h1>Prague itinerary</h1>\n      <AddTask\n        onAddTask={handleAddTask}\n      />\n      <TaskList\n        tasks={tasks}\n        onChangeTask={handleChangeTask}\n        onDeleteTask={handleDeleteTask}\n      />\n    </>\n  );\n}\n\nfunction tasksReducer(tasks, action) {\n  switch (action.type) {\n    case 'added': {\n      return [...tasks, {\n        id: action.id,\n        text: action.text,\n        done: false\n      }];\n    }\n    case 'changed': {\n      return tasks.map(t => {\n        if (t.id === action.task.id) {\n          return action.task;\n        } else {\n          return t;\n        }\n      });\n    }\n    case 'deleted': {\n      return tasks.filter(t => t.id !== action.id);\n    }\n    default: {\n      throw Error('Unknown action: ' + action.type);\n    }\n  }\n}\n\nlet nextId = 3;\nconst initialTasks = [\n  { id: 0, text: 'Visit Kafka Museum', done: true },\n  { id: 1, text: 'Watch a puppet show', done: false },\n  { id: 2, text: 'Lennon Wall pic', done: false }\n];\n```\n\n```js src/AddTask.js hidden\nimport { useState } from 'react';\n\nexport default function AddTask({ onAddTask }) {\n  const [text, setText] = useState('');\n  return (\n    <>\n      <input\n        placeholder=\"Add task\"\n        value={text}\n        onChange={e => setText(e.target.value)}\n      />\n      <button onClick={() => {\n        setText('');\n        onAddTask(text);\n      }}>Add</button>\n    </>\n  )\n}\n```\n\n```js src/TaskList.js hidden\nimport { useState } from 'react';\n\nexport default function TaskList({\n  tasks,\n  onChangeTask,\n  onDeleteTask\n}) {\n  return (\n    <ul>\n      {tasks.map(task => (\n        <li key={task.id}>\n          <Task\n            task={task}\n            onChange={onChangeTask}\n            onDelete={onDeleteTask}\n          />\n        </li>\n      ))}\n    </ul>\n  );\n}\n\nfunction Task({ task, onChange, onDelete }) {\n  const [isEditing, setIsEditing] = useState(false);\n  let taskContent;\n  if (isEditing) {\n    taskContent = (\n      <>\n        <input\n          value={task.text}\n          onChange={e => {\n            onChange({\n              ...task,\n              text: e.target.value\n            });\n          }} />\n        <button onClick={() => setIsEditing(false)}>\n          Save\n        </button>\n      </>\n    );\n  } else {\n    taskContent = (\n      <>\n        {task.text}\n        <button onClick={() => setIsEditing(true)}>\n          Edit\n        </button>\n      </>\n    );\n  }\n  return (\n    <label>\n      <input\n        type=\"checkbox\"\n        checked={task.done}\n        onChange={e => {\n          onChange({\n            ...task,\n            done: e.target.checked\n          });\n        }}\n      />\n      {taskContent}\n      <button onClick={() => onDelete(task.id)}>\n        Delete\n      </button>\n    </label>\n  );\n}\n```\n\n```css\nbutton { margin: 5px; }\nli { list-style-type: none; }\nul, li { margin: 0; padding: 0; }\n```\n\n</Sandpack>\n\n<LearnMore path=\"/learn/extracting-state-logic-into-a-reducer\">\n\nReducer 함수에 로직을 통합하는 방법을 배우려면 **[state 로직을 reducer로 작성하기](/learn/extracting-state-logic-into-a-reducer)** 를 읽어보세요.\n\n</LearnMore>\n\n## Context를 사용해 데이터를 깊게 전달하기 {/*passing-data-deeply-with-context*/}\n\n일반적으로는 props를 통해 부모 컴포넌트에서 자식 컴포넌트로 정보를 전달합니다. 그러나 중간에 많은 컴포넌트를 거쳐야 하거나, 애플리케이션의 많은 컴포넌트에서 동일한 정보가 필요한 경우에는 props를 전달하는 것이 번거롭고 불편할 수 있습니다. 이때 Context를 사용하면 부모 컴포넌트가 props를 통해 명시적으로 정보를 전달하지 않아도, 트리에 있는 모든 자식 컴포넌트가 (얼마나 깊게 있든지 간에) 정보를 사용할 수 있도록 할 수 있습니다.\n\n아래 예시에서 `Heading` 컴포넌트는 가장 가까운 `Section`에 \"물어봄으로써\" 자신의 heading 레벨을 결정합니다. 각 `Section`은 부모 `Section`에 레벨을 물어보고 거기에 1을 더해 자신의 레벨을 트래킹합니다. 각 `Section`은 props를 전달하지 않고도 모든 하위 컴포넌트에 정보를 제공하며, 이는 Context를 통해 수행됩니다.\n\n<Sandpack>\n\n```js\nimport Heading from './Heading.js';\nimport Section from './Section.js';\n\nexport default function Page() {\n  return (\n    <Section>\n      <Heading>Title</Heading>\n      <Section>\n        <Heading>Heading</Heading>\n        <Heading>Heading</Heading>\n        <Heading>Heading</Heading>\n        <Section>\n          <Heading>Sub-heading</Heading>\n          <Heading>Sub-heading</Heading>\n          <Heading>Sub-heading</Heading>\n          <Section>\n            <Heading>Sub-sub-heading</Heading>\n            <Heading>Sub-sub-heading</Heading>\n            <Heading>Sub-sub-heading</Heading>\n          </Section>\n        </Section>\n      </Section>\n    </Section>\n  );\n}\n```\n\n```js src/Section.js\nimport { useContext } from 'react';\nimport { LevelContext } from './LevelContext.js';\n\nexport default function Section({ children }) {\n  const level = useContext(LevelContext);\n  return (\n    <section className=\"section\">\n      <LevelContext value={level + 1}>\n        {children}\n      </LevelContext>\n    </section>\n  );\n}\n```\n\n```js src/Heading.js\nimport { useContext } from 'react';\nimport { LevelContext } from './LevelContext.js';\n\nexport default function Heading({ children }) {\n  const level = useContext(LevelContext);\n  switch (level) {\n    case 0:\n      throw Error('Heading must be inside a Section!');\n    case 1:\n      return <h1>{children}</h1>;\n    case 2:\n      return <h2>{children}</h2>;\n    case 3:\n      return <h3>{children}</h3>;\n    case 4:\n      return <h4>{children}</h4>;\n    case 5:\n      return <h5>{children}</h5>;\n    case 6:\n      return <h6>{children}</h6>;\n    default:\n      throw Error('Unknown level: ' + level);\n  }\n}\n```\n\n```js src/LevelContext.js\nimport { createContext } from 'react';\n\nexport const LevelContext = createContext(0);\n```\n\n```css\n.section {\n  padding: 10px;\n  margin: 5px;\n  border-radius: 5px;\n  border: 1px solid #aaa;\n}\n```\n\n</Sandpack>\n\n<LearnMore path=\"/learn/passing-data-deeply-with-context\">\n\nProps 전달하기의 대안으로 context를 사용하는 방법을 배우려면 **[Context를 사용해 데이터를 깊게 전달하기](/learn/passing-data-deeply-with-context)** 를 읽어보세요.\n\n</LearnMore>\n\n## Reducer와 Context로 앱 확장하기 {/*scaling-up-with-reducer-and-context*/}\n\nReducer를 사용하면 컴포넌트의 state 업데이트 로직을 통합할 수 있습니다. Context를 사용하면 다른 컴포넌트에 정보를 깊숙이 전달할 수 있습니다. Reducer와 Context를 함께 사용하여 복잡한 화면의 state를 관리할 수 있습니다.\n\n이 접근 방식을 사용하면 상위 컴포넌트가 Reducer로 복잡한 state를 관리합니다. 트리 깊은 곳에 있는 다른 컴포넌트는 Context를 통해 상위 컴포넌트의 state를 읽을 수 있습니다. 또한 해당 state를 업데이트하기 위해 action을 dispatch 할 수도 있습니다.\n\n<Sandpack>\n\n```js src/App.js\nimport AddTask from './AddTask.js';\nimport TaskList from './TaskList.js';\nimport { TasksProvider } from './TasksContext.js';\n\nexport default function TaskApp() {\n  return (\n    <TasksProvider>\n      <h1>Day off in Kyoto</h1>\n      <AddTask />\n      <TaskList />\n    </TasksProvider>\n  );\n}\n```\n\n```js src/TasksContext.js\nimport { createContext, useContext, useReducer } from 'react';\n\nconst TasksContext = createContext(null);\nconst TasksDispatchContext = createContext(null);\n\nexport function TasksProvider({ children }) {\n  const [tasks, dispatch] = useReducer(\n    tasksReducer,\n    initialTasks\n  );\n\n  return (\n    <TasksContext value={tasks}>\n      <TasksDispatchContext value={dispatch}>\n        {children}\n      </TasksDispatchContext>\n    </TasksContext>\n  );\n}\n\nexport function useTasks() {\n  return useContext(TasksContext);\n}\n\nexport function useTasksDispatch() {\n  return useContext(TasksDispatchContext);\n}\n\nfunction tasksReducer(tasks, action) {\n  switch (action.type) {\n    case 'added': {\n      return [...tasks, {\n        id: action.id,\n        text: action.text,\n        done: false\n      }];\n    }\n    case 'changed': {\n      return tasks.map(t => {\n        if (t.id === action.task.id) {\n          return action.task;\n        } else {\n          return t;\n        }\n      });\n    }\n    case 'deleted': {\n      return tasks.filter(t => t.id !== action.id);\n    }\n    default: {\n      throw Error('Unknown action: ' + action.type);\n    }\n  }\n}\n\nconst initialTasks = [\n  { id: 0, text: 'Philosopher’s Path', done: true },\n  { id: 1, text: 'Visit the temple', done: false },\n  { id: 2, text: 'Drink matcha', done: false }\n];\n```\n\n```js src/AddTask.js\nimport { useState, useContext } from 'react';\nimport { useTasksDispatch } from './TasksContext.js';\n\nexport default function AddTask({ onAddTask }) {\n  const [text, setText] = useState('');\n  const dispatch = useTasksDispatch();\n  return (\n    <>\n      <input\n        placeholder=\"Add task\"\n        value={text}\n        onChange={e => setText(e.target.value)}\n      />\n      <button onClick={() => {\n        setText('');\n        dispatch({\n          type: 'added',\n          id: nextId++,\n          text: text,\n        });\n      }}>Add</button>\n    </>\n  );\n}\n\nlet nextId = 3;\n```\n\n```js src/TaskList.js\nimport { useState, useContext } from 'react';\nimport { useTasks, useTasksDispatch } from './TasksContext.js';\n\nexport default function TaskList() {\n  const tasks = useTasks();\n  return (\n    <ul>\n      {tasks.map(task => (\n        <li key={task.id}>\n          <Task task={task} />\n        </li>\n      ))}\n    </ul>\n  );\n}\n\nfunction Task({ task }) {\n  const [isEditing, setIsEditing] = useState(false);\n  const dispatch = useTasksDispatch();\n  let taskContent;\n  if (isEditing) {\n    taskContent = (\n      <>\n        <input\n          value={task.text}\n          onChange={e => {\n            dispatch({\n              type: 'changed',\n              task: {\n                ...task,\n                text: e.target.value\n              }\n            });\n          }} />\n        <button onClick={() => setIsEditing(false)}>\n          Save\n        </button>\n      </>\n    );\n  } else {\n    taskContent = (\n      <>\n        {task.text}\n        <button onClick={() => setIsEditing(true)}>\n          Edit\n        </button>\n      </>\n    );\n  }\n  return (\n    <label>\n      <input\n        type=\"checkbox\"\n        checked={task.done}\n        onChange={e => {\n          dispatch({\n            type: 'changed',\n            task: {\n              ...task,\n              done: e.target.checked\n            }\n          });\n        }}\n      />\n      {taskContent}\n      <button onClick={() => {\n        dispatch({\n          type: 'deleted',\n          id: task.id\n        });\n      }}>\n        Delete\n      </button>\n    </label>\n  );\n}\n```\n\n```css\nbutton { margin: 5px; }\nli { list-style-type: none; }\nul, li { margin: 0; padding: 0; }\n```\n\n</Sandpack>\n\n<LearnMore path=\"/learn/scaling-up-with-reducer-and-context\">\n\n커져가는 앱에서 state 관리가 어떻게 확장되는지 알아보려면  **[Scaling Up with Reducer and Context](/learn/scaling-up-with-reducer-and-context)** 를 읽어보세요.\n\n</LearnMore>\n\n## 다음은 무엇인가요? {/*whats-next*/}\n\n이 장을 한 페이지씩 읽어보려면 [State를 사용해 Input 다루기](/learn/reacting-to-input-with-state)로 이동하세요!\n\n이 주제에 이미 익숙하다면 [탈출구](/learn/escape-hatches)에 대해서 읽어보는 것은 어떨까요?\n"
  },
  {
    "path": "src/content/learn/manipulating-the-dom-with-refs.md",
    "content": "---\ntitle: 'Ref로 DOM 조작하기'\n---\n\n<Intro>\n\nReact는 렌더링 결과물에 맞춰 [DOM](https://developer.mozilla.org/docs/Web/API/Document_Object_Model/Introduction) 변경을 자동으로 처리하기 때문에 컴포넌트에서 자주 DOM을 조작해야 할 필요는 없습니다. 하지만 가끔 특정 노드에 포커스를 옮기거나, 스크롤 위치를 옮기거나, 위치와 크기를 측정하기 위해서 React가 관리하는 DOM 요소에 접근해야 할 때가 있습니다. React는 이런 작업을 수행하는 내장 방법을 제공하지 않기 때문에 DOM 노드에 접근하기 위한 *ref*가 필요할 것입니다.\n\n</Intro>\n\n<YouWillLearn>\n\n- `ref` 어트리뷰트로 React가 관리하는 DOM 노드에 접근하는 법\n- `ref` JSX 어트리뷰트와 `useRef` Hook의 관련성\n- 다른 컴포넌트의 DOM 노드에 접근하는 법\n- React가 관리하는 DOM을 수정해도 안전한 경우\n\n</YouWillLearn>\n\n## ref로 노드 가져오기 {/*getting-a-ref-to-the-node*/}\n\n먼저 React가 관리하는 DOM 노드에 접근하기 위해 `useRef` Hook을 가져옵니다.\n\n```js\nimport { useRef } from 'react';\n```\n\n컴포넌트 안에서 ref를 선언하기 위해 방금 가져온 Hook을 사용합니다.\n\n```js\nconst myRef = useRef(null);\n```\n\n마지막으로 ref를 DOM 노드를 가져와야하는 JSX tag 에 `ref` 어트리뷰트로 전달합니다.\n\n```js\n<div ref={myRef}>\n```\n\n`useRef` Hook은 `current`라는 단일 속성을 가진 객체를 반환합니다. 초기에는 'myRef.current'가 'null'이 됩니다. React가 이 `<div>`에 대한 DOM 노드를 생성할 때, React는 이 노드에 대한 참조를 `myRef.current`에 넣습니다. 그리고 이 DOM 노드를 [이벤트 핸들러](/learn/responding-to-events)에서 접근하거나 노드에 정의된 내장 [브라우저 API](https://developer.mozilla.org/docs/Web/API/Element)를 사용할 수 있습니다.\n\n```js\n// 예를 들어 이렇게 브라우저 API를 사용할 수 있습니다\nmyRef.current.scrollIntoView();\n```\n\n### 예시: 텍스트 입력에 포커스 이동하기 {/*example-focusing-a-text-input*/}\n\n이 예시에서 버튼을 클릭하면 input 요소로 포커스를 이동합니다.\n\n<Sandpack>\n\n```js\nimport { useRef } from 'react';\n\nexport default function Form() {\n  const inputRef = useRef(null);\n\n  function handleClick() {\n    inputRef.current.focus();\n  }\n\n  return (\n    <>\n      <input ref={inputRef} />\n      <button onClick={handleClick}>\n        Focus the input\n      </button>\n    </>\n  );\n}\n```\n\n</Sandpack>\n\n위 예시를 구현하기 위해서\n\n1. `useRef` Hook을 사용하여 `inputRef`를 선언합니다.\n2. 선언한 `inputRef`를 `<input ref={inputRef}>`처럼 전달합니다. 이 행위는 **React에 이 `<input>`의 DOM 노드를 `inputRef.current`에 넣어줘** 라고 하는 것입니다.\n3. `handleClick` 함수에서 `inputRef.current`에서 input DOM 노드를 읽고 `inputRef.current.focus()`로 [`focus()`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus)를 호출합니다.\n4. `<button>`의 `onClick`으로 `handleClick` 이벤트 핸들러를 전달합니다.\n\nDOM 조작이 ref를 사용하는 가장 일반적인 사용처지만 `useRef` Hook은 setTimeout Timer ID 같은 React 외부 요소를 저장하는 용도로도 사용할 수 있습니다. state와 비슷하게 ref는 렌더링 사이에도 유지됩니다. ref를 설정하더라도 컴포넌트의 렌더링을 다시 유발하지 않는 state와 유사합니다. [Ref와 값 참조](/learn/referencing-values-with-refs)에서 ref에 대해 자세히 배울 수 있습니다.\n\n### 예시: 한 요소로 스크롤을 이동하기 {/*example-scrolling-to-an-element*/}\n\n한 컴포넌트에서 하나 이상의 ref를 가질 수 있습니다. 이 예시에서는 이미지 3개가 있는 캐러셀이 있습니다. 각 버튼은 브라우저 [`scrollIntoView()`](https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView) 메서드를 해당 DOM 노드로 호출하여 이미지를 중앙에 배치합니다.\n\n<Sandpack>\n\n```js\nimport { useRef } from 'react';\n\nexport default function CatFriends() {\n  const firstCatRef = useRef(null);\n  const secondCatRef = useRef(null);\n  const thirdCatRef = useRef(null);\n\n  function handleScrollToFirstCat() {\n    firstCatRef.current.scrollIntoView({\n      behavior: 'smooth',\n      block: 'nearest',\n      inline: 'center'\n    });\n  }\n\n  function handleScrollToSecondCat() {\n    secondCatRef.current.scrollIntoView({\n      behavior: 'smooth',\n      block: 'nearest',\n      inline: 'center'\n    });\n  }\n\n  function handleScrollToThirdCat() {\n    thirdCatRef.current.scrollIntoView({\n      behavior: 'smooth',\n      block: 'nearest',\n      inline: 'center'\n    });\n  }\n\n  return (\n    <>\n      <nav>\n        <button onClick={handleScrollToFirstCat}>\n          Neo\n        </button>\n        <button onClick={handleScrollToSecondCat}>\n          Millie\n        </button>\n        <button onClick={handleScrollToThirdCat}>\n          Bella\n        </button>\n      </nav>\n      <div>\n        <ul>\n          <li>\n            <img\n              src=\"https://placecats.com/neo/300/200\"\n              alt=\"Neo\"\n              ref={firstCatRef}\n            />\n          </li>\n          <li>\n            <img\n              src=\"https://placecats.com/millie/200/200\"\n              alt=\"Millie\"\n              ref={secondCatRef}\n            />\n          </li>\n          <li>\n            <img\n              src=\"https://placecats.com/bella/199/200\"\n              alt=\"Bella\"\n              ref={thirdCatRef}\n            />\n          </li>\n        </ul>\n      </div>\n    </>\n  );\n}\n```\n\n```css\ndiv {\n  width: 100%;\n  overflow: hidden;\n}\n\nnav {\n  text-align: center;\n}\n\nbutton {\n  margin: .25rem;\n}\n\nul,\nli {\n  list-style: none;\n  white-space: nowrap;\n}\n\nli {\n  display: inline;\n  padding: 0.5rem;\n}\n```\n\n</Sandpack>\n\n<DeepDive>\n\n#### ref 콜백을 사용하여 ref 리스트 관리하기 {/*how-to-manage-a-list-of-refs-using-a-ref-callback*/}\n\n위 예시에서는 미리 정해진 숫자만큼 ref가 있었습니다. 하지만 때때로 목록의 아이템마다 ref가 필요할 수도 있고, 얼마나 많은 ref가 필요할지 예측할 수 없는 경우도 있습니다. 그럴 때 아래 코드는 **작동하지 않습니다**.\n\n```js\n<ul>\n  {items.map((item) => {\n    // 작동하지 않습니다!\n    const ref = useRef(null);\n    return <li ref={ref} />;\n  })}\n</ul>\n```\n\n왜냐하면 **Hook은 컴포넌트의 최상단에서만 호출되어야 하기 때문입니다**. `useRef`를 반복문, 조건문 혹은 `map()` 안쪽에서 호출할 수 없습니다.\n\n이 문제를 해결하는 한 방법은 부모 요소에서 단일 ref를 얻고, [`querySelectorAll`](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelectorAll)과 같은 DOM 조작 메서드를 사용하여 그 안에서 개별 자식 노드를 \"찾는\" 것입니다. 하지만 이는 다루기가 힘들며 DOM 구조가 바뀌는 경우 작동하지 않을 수 있습니다.\n\n또 다른 해결책은 **`ref` 어트리뷰트에 함수를 전달하는 것입니다.** 이것을 [`ref` 콜백](/reference/react-dom/components/common#ref-callback)이라 부릅니다. React는 ref를 설정할 때 DOM 노드와 함께 ref 콜백을 호출하고, ref를 지울 때는 콜백에서 반환된 정리 함수를 호출합니다. 이를 통해 배열이나 [Map](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Map)을 직접 관리하고 인덱스나 특정 ID로 ref에 접근할 수 있습니다.\n\n아래 예시는 긴 목록에서 특정 노드에 스크롤 하기 위해 앞에서 말한 접근법을 사용합니다.\n\n<Sandpack>\n\n```js\nimport { useRef, useState } from \"react\";\n\nexport default function CatFriends() {\n  const itemsRef = useRef(null);\n  const [catList, setCatList] = useState(setupCatList);\n\n  function scrollToCat(cat) {\n    const map = getMap();\n    const node = map.get(cat);\n    node.scrollIntoView({\n      behavior: \"smooth\",\n      block: \"nearest\",\n      inline: \"center\",\n    });\n  }\n\n  function getMap() {\n    if (!itemsRef.current) {\n      // 처음 사용하는 경우, Map을 초기화합니다.\n      itemsRef.current = new Map();\n    }\n    return itemsRef.current;\n  }\n\n  return (\n    <>\n      <nav>\n        <button onClick={() => scrollToCat(catList[0])}>Neo</button>\n        <button onClick={() => scrollToCat(catList[5])}>Millie</button>\n        <button onClick={() => scrollToCat(catList[8])}>Bella</button>\n      </nav>\n      <div>\n        <ul>\n          {catList.map((cat) => (\n            <li\n              key={cat.id}\n              ref={(node) => {\n                const map = getMap();\n                map.set(cat, node);\n\n                return () => {\n                  map.delete(cat);\n                };\n              }}\n            >\n              <img src={cat.imageUrl} />\n            </li>\n          ))}\n        </ul>\n      </div>\n    </>\n  );\n}\n\nfunction setupCatList() {\n  const catCount = 10;\n  const catList = new Array(catCount)\n  for (let i = 0; i < catCount; i++) {\n    let imageUrl = '';\n    if (i < 5) {\n      imageUrl = \"https://placecats.com/neo/320/240\";\n    } else if (i < 8) {\n      imageUrl = \"https://placecats.com/millie/320/240\";\n    } else {\n      imageUrl = \"https://placecats.com/bella/320/240\";\n    }\n    catList[i] = {\n      id: i,\n      imageUrl,\n    };\n  }\n  return catList;\n}\n\n```\n\n```css\ndiv {\n  width: 100%;\n  overflow: hidden;\n}\n\nnav {\n  text-align: center;\n}\n\nbutton {\n  margin: .25rem;\n}\n\nul,\nli {\n  list-style: none;\n  white-space: nowrap;\n}\n\nli {\n  display: inline;\n  padding: 0.5rem;\n}\n```\n\n</Sandpack>\n\n이 예시에서 `itemsRef`는 하나의 DOM 노드를 가지고 있지 않습니다. 대신에 식별자와 DOM 노드로 연결된 [Map](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Map)을 가지고 있습니다. ([Ref는 어떤 값이든 가질 수 있습니다!](/learn/referencing-values-with-refs)) 모든 리스트 아이템에 있는 [`ref` 콜백](/reference/react-dom/components/common#ref-callback)은 Map 변경을 처리합니다.\n\n```js\n<li\n  key={cat.id}\n  ref={node => {\n    const map = getMap();\n    // Add to the Map\n    map.set(cat, node);\n\n    return () => {\n      // Remove from the Map\n      map.delete(cat);\n    };\n  }}\n>\n```\n\n이렇게 하면 나중에 Map에서 개별 DOM 노드를 읽을 수 있습니다.\n\n<Note>\n\nStrict Mode가 활성화되어 있다면 개발 모드에서 ref 콜백이 두 번 실행됩니다.\n\nref 콜백에서 [이 방식이 버그를 찾는데 어떻게 도움이 되는지](/reference/react/StrictMode#fixing-bugs-found-by-re-running-ref-callbacks-in-development) 자세히 알아보세요.\n\n</Note>\n\n</DeepDive>\n\n## 다른 컴포넌트의 DOM 노드 접근하기 {/*accessing-another-components-dom-nodes*/}\n\n`<input />`같은 브라우저 요소를 출력하는 내장 컴포넌트에 ref를 주입할 때 React는 ref의 `current` 프로퍼티를 그에 해당하는 (브라우저의 실제 `<input />` 같은) DOM 노드로 설정합니다.\n\n하지만 `<MyInput />` 같이 **직접 만든** 컴포넌트에 ref를 주입할 때는 `null`이 기본적으로 주어집니다. 여기 앞서 말한 내용을 설명하는 예시가 있습니다. 버튼을 클릭할 때 input 요소에 포커스 **되지 않는 것을** 주목하세요.\n\n<Pitfall>\nRef는 일종의 탈출구입니다. 다른 컴포넌트의 DOM 노드를 수동으로 조작하면 코드를 불안정하게 만들 수 있습니다.\n</Pitfall>\n\n부모 컴포넌트에서 자식 컴포넌트로 ref를 [prop 처럼](/learn/passing-props-to-a-component) 전달할 수 있습니다.\n\n```js {3-4,9}\nimport { useRef } from 'react';\n\nfunction MyInput({ ref }) {\n  return <input ref={ref} />;\n}\n\nfunction MyForm() {\n  const inputRef = useRef(null);\n  return <MyInput ref={inputRef} />\n}\n```\n\n위 예시에서, 부모 컴포넌트인 `MyForm`에서 ref를 생성하고, 이를 자식 컴포넌트인 `MyInput`으로 전달합니다. 그리고 `MyInput`은 그 ref를 `<input>`에 넘겨줍니다. `<input>`은 [내장 컴포넌트](/reference/react-dom/components/common)이므로, React는 해당 ref의 `.current` 속성을 `<input>` DOM 요소로 설정합니다.\n\n`MyForm`에서 생성된 `inputRef`는 이제 `MyInput`이 반환한 `<input>` DOM 요소를 가리킵니다. 그리고 `MyForm`에서 생성한 클릭 핸들러는 `inputRef`에 접근하여 `focus()`를 호출함으로써 `<input>`에 포커스를 설정할 수 있습니다.\n\n\n<Sandpack>\n\n```js\nimport { useRef } from 'react';\n\nfunction MyInput({ ref }) {\n  return <input ref={ref} />;\n}\n\nexport default function MyForm() {\n  const inputRef = useRef(null);\n\n  function handleClick() {\n    inputRef.current.focus();\n  }\n\n  return (\n    <>\n      <MyInput ref={inputRef} />\n      <button onClick={handleClick}>\n        Focus the input\n      </button>\n    </>\n  );\n}\n```\n\n</Sandpack>\n\n<DeepDive>\n\n#### 명령형 처리방식으로 하위 API 노출하기 {/*exposing-a-subset-of-the-api-with-an-imperative-handle*/}\n\n위 예시에서 `MyInput`에 전달된 ref는 DOM 입력 요소로 전달됩니다. 그리고 부모 컴포넌트에서 DOM 노드의 `focus()`를 호출할 수 있게 되었습니다. 하지만 이에 따라 부모 컴포넌트에서 DOM 노드의 CSS 스타일을 직접 변경하는 등의 예상치 못 한 작업을 할 수 있습니다. 몇몇 상황에서는 노출된 기능을 제한하고 싶을 수 있는데, 이 때  [`useImperativeHandle`](/reference/react/useImperativeHandle)을 사용합니다.\n\n<Sandpack>\n\n```js\nimport { useRef, useImperativeHandle } from \"react\";\n\nfunction MyInput({ ref }) {\n  const realInputRef = useRef(null);\n  useImperativeHandle(ref, () => ({\n    // 오직 focus만 노출합니다.\n    focus() {\n      realInputRef.current.focus();\n    },\n  }));\n  return <input ref={realInputRef} />;\n};\n\nexport default function Form() {\n  const inputRef = useRef(null);\n\n  function handleClick() {\n    inputRef.current.focus();\n  }\n\n  return (\n    <>\n      <MyInput ref={inputRef} />\n      <button onClick={handleClick}>Focus the input</button>\n    </>\n  );\n}\n```\n\n</Sandpack>\n\n여기 `MyInput` 내부의 `realInputRef`는 실제 input DOM 노드를 가지고 있습니다. 하지만 [`useImperativeHandle`](/reference/react/useImperativeHandle)을 사용하여 React가 ref를 참조하는 부모 컴포넌트에 직접 구성한 객체를 전달하도록 지시합니다. 따라서 `Form` 컴포넌트 안쪽의 `inputRef.current`는 `focus` 메서드만 가지고 있습니다. 이 경우 ref는 DOM 노드가 아니라 [`useImperativeHandle`](/reference/react/useImperativeHandle) 호출에서 직접 구성한 객체가 됩니다.\n\n</DeepDive>\n\n## React가 ref를 부여할 때 {/*when-react-attaches-the-refs*/}\n\nReact의 모든 갱신은 [두 단계](/learn/render-and-commit#step-3-react-commits-changes-to-the-dom)로 나눌 수 있습니다.\n\n* **렌더링** 단계에서 React는 화면에 무엇을 그려야 하는지 알아내도록 컴포넌트를 호출합니다.\n* **커밋** 단계에서 React는 변경사항을 DOM에 적용합니다.\n\n일반적으로 렌더링하는 중 ref에 접근하는 것을 [원하지 않습니다](/learn/referencing-values-with-refs#best-practices-for-refs). DOM 노드를 보유하는 ref도 마찬가지입니다. 첫 렌더링에서 DOM 노드는 아직 생성되지 않아서 `ref.current`는 `null`인 상태입니다. 그리고 갱신에 의한 렌더링에서 DOM 노드는 아직 업데이트되지 않은 상태입니다. 두 상황 모두 ref를 읽기에 너무 이른 상황입니다.\n\nReact는 `ref.current`를 커밋 단계에서 설정합니다. DOM을 변경하기 전에 React는 관련된 `ref.current` 값을 미리 `null`로 설정합니다. 그리고 DOM을 변경한 후 React는 즉시 대응하는 DOM 노드로 다시 설정합니다.\n\n**대부분 `ref` 접근은 이벤트 핸들러 안에서 일어납니다.** ref를 사용하여 뭔가를 하고 싶지만, 그것을 시행할 특정 이벤트가 없을 때 Effect 가 필요할 수도 있습니다. Effects 에 대해서 다음 페이지에서 이야기해 보겠습니다.\n\n<DeepDive>\n\n#### flushSync로 state 변경을 동적으로 플러시하기 {/*flushing-state-updates-synchronously-with-flush-sync*/}\n\n새로운 할 일을 추가하고 할 일 목록의 마지막으로 화면 스크롤을 내리는 아래 코드를 봅시다. 어떤 이유에 의해 마지막으로 추가된 할 일의 직전으로 항상 스크롤 되는 것을 관찰하세요.\n\n<Sandpack>\n\n```js\nimport { useState, useRef } from 'react';\n\nexport default function TodoList() {\n  const listRef = useRef(null);\n  const [text, setText] = useState('');\n  const [todos, setTodos] = useState(\n    initialTodos\n  );\n\n  function handleAdd() {\n    const newTodo = { id: nextId++, text: text };\n    setText('');\n    setTodos([ ...todos, newTodo]);\n    listRef.current.lastChild.scrollIntoView({\n      behavior: 'smooth',\n      block: 'nearest'\n    });\n  }\n\n  return (\n    <>\n      <button onClick={handleAdd}>\n        Add\n      </button>\n      <input\n        value={text}\n        onChange={e => setText(e.target.value)}\n      />\n      <ul ref={listRef}>\n        {todos.map(todo => (\n          <li key={todo.id}>{todo.text}</li>\n        ))}\n      </ul>\n    </>\n  );\n}\n\nlet nextId = 0;\nlet initialTodos = [];\nfor (let i = 0; i < 20; i++) {\n  initialTodos.push({\n    id: nextId++,\n    text: 'Todo #' + (i + 1)\n  });\n}\n```\n\n</Sandpack>\n\n문제는 다음 두 줄에 있습니다.\n\n```js\nsetTodos([ ...todos, newTodo]);\nlistRef.current.lastChild.scrollIntoView();\n```\n\nReact에서 [state 갱신은 큐에 쌓여 비동기적으로 처리됩니다](/learn/queueing-a-series-of-state-updates). 이렇게 동작하는 것은 일반적으로 기대하는 방향입니다. 하지만 여기에선 `setTodos`가 DOM을 바로 업데이트하지 않기 때문에 문제가 발생합니다. 그래서 할 일 목록의 마지막 노드로 스크롤 할 때, DOM에 아직 새로운 할 일이 추가되지 않은 상태입니다. 위 예시에서 스크롤이 계속 한 항목에 뒤처지는 이유입니다.\n\n이 문제를 고치기 위해 React가 DOM 변경을 동기적으로 수행하도록 할 수 있습니다. 이를 위해 `react-dom` 패키지의 `flushSync`를 가져오고 state 업데이트를 `flushSync` 호출로 감싸면 됩니다.\n\n```js\nflushSync(() => {\n  setTodos([ ...todos, newTodo]);\n});\nlistRef.current.lastChild.scrollIntoView();\n```\n\n위의 내용은 `flushSync`로 감싼 코드가 실행된 직후 React가 동기적으로 DOM을 변경하도록 지시합니다. 결과적으로 마지막 할 일은 스크롤 하기 전에 항상 DOM에 추가되어 있을 것입니다.\n\n<Sandpack>\n\n```js\nimport { useState, useRef } from 'react';\nimport { flushSync } from 'react-dom';\n\nexport default function TodoList() {\n  const listRef = useRef(null);\n  const [text, setText] = useState('');\n  const [todos, setTodos] = useState(\n    initialTodos\n  );\n\n  function handleAdd() {\n    const newTodo = { id: nextId++, text: text };\n    flushSync(() => {\n      setText('');\n      setTodos([ ...todos, newTodo]);\n    });\n    listRef.current.lastChild.scrollIntoView({\n      behavior: 'smooth',\n      block: 'nearest'\n    });\n  }\n\n  return (\n    <>\n      <button onClick={handleAdd}>\n        Add\n      </button>\n      <input\n        value={text}\n        onChange={e => setText(e.target.value)}\n      />\n      <ul ref={listRef}>\n        {todos.map(todo => (\n          <li key={todo.id}>{todo.text}</li>\n        ))}\n      </ul>\n    </>\n  );\n}\n\nlet nextId = 0;\nlet initialTodos = [];\nfor (let i = 0; i < 20; i++) {\n  initialTodos.push({\n    id: nextId++,\n    text: 'Todo #' + (i + 1)\n  });\n}\n```\n\n</Sandpack>\n\n</DeepDive>\n\n## ref로 DOM을 조작하는 모범 사례 {/*best-practices-for-dom-manipulation-with-refs*/}\n\nRef는 탈출구입니다. \"React에서 벗어나야 할 때\"만 사용해야 합니다. 포커스 혹은 스크롤 위치를 관리하거나, React가 노출하지 않는 브라우저 API를 호출하는 등의 작업이 이에 포함됩니다.\n\n포커스 및 스크롤 관리 같은 비 파괴적인 행동을 고수한다면 어떤 문제도 마주치지 않을 것입니다. 하지만 DOM을 직접 수정하는 시도를 한다면 React가 만들어 내는 변경 사항과 충돌을 발생시킬 위험을 감수해야 합니다.\n\n이 문제를 이해하기 위해 이번 예시에서는 환영 문구와 두 버튼을 포함하고 있습니다. 첫 버튼은 일반적인 React [조건부 렌더링](/learn/conditional-rendering)과 [state](/learn/state-a-components-memory)를 사용하여 노드 존재 여부를 토글 합니다. 두 번째 버튼은 [DOM API의 `remove()`](https://developer.mozilla.org/en-US/docs/Web/API/Element/remove)를 사용하여 React의 제어 밖에서 노드를 강제적으로 삭제합니다.\n\n\"Toggle with setState\"를 몇 차례 눌러보세요. 메시지가 반복적으로 나타나거나 사라질 것입니다. 이후 \"Remove from the DOM\"을 눌러보세요. 이것은 강제적으로 노드를 삭제합니다. 마지막으로 \"Toggle with setState\"를 다시 눌러보세요.\n\n<Sandpack>\n\n```js\nimport {useState, useRef} from 'react';\n\nexport default function Counter() {\n  const [show, setShow] = useState(true);\n  const ref = useRef(null);\n\n  return (\n    <div>\n      <button\n        onClick={() => {\n          setShow(!show);\n        }}>\n        Toggle with setState\n      </button>\n      <button\n        onClick={() => {\n          ref.current.remove();\n        }}>\n        Remove from the DOM\n      </button>\n      {show && <p ref={ref}>Hello world</p>}\n    </div>\n  );\n}\n```\n\n```css\np,\nbutton {\n  display: block;\n  margin: 10px;\n}\n```\n\n</Sandpack>\n\nDOM 요소를 직접 삭제한 뒤 `setState`를 사용하여 다시 DOM 노드를 노출하는 것은 충돌을 발생시킵니다. DOM을 직접 변경했을 때 React는 DOM 노드를 올바르게 계속 관리할 방법을 모르기 때문입니다.\n\n**React가 관리하는 DOM 노드를 직접 바꾸려 하지 마세요.** React가 관리하는 DOM 요소에 대한 수정, 자식 추가 혹은 자식 삭제는 비일관적인 시각적 결과 혹은 위 예시처럼 충돌로 이어집니다.\n\n하지만 항상 이것을 할 수 없다는 의미는 아닙니다. 주의 깊게 사용해야 합니다. **안전하게 React가 업데이트할 이유가 없는 DOM 노드 일부를 수정할 수 있습니다.** 예를 들어 몇몇 `<div>`가 항상 빈 채로 JSX에 있다면, React는 해당 노드의 자식 요소를 건드릴 이유가 없습니다. 따라서 빈 노드에서 엘리먼트를 추가하거나 삭제하는 것은 안전합니다.\n\n<Recap>\n\n- Ref는 일반적인 개념이지만, 대부분의 경우 DOM 요소를 저장하는 데 사용합니다.\n- React에 `<div ref={myRef}>`와 같이 작성하면, 해당 DOM 노드를 `myRef.current`에 넣도록 지시하는 것입니다.\n- 대부분의 경우, Ref는 DOM 요소에 포커스를 주거나, 스크롤하거나, 치수를 측정하는 등 DOM을 직접 변경하지 않고 상태를 유지하는 작업에 사용합니다.\n- 컴포넌트는 기본적으로 자신의 DOM 노드를 외부에 노출하지 않습니다. `ref` Prop을 사용하여 DOM 노드를 선택적으로 노출할 수 있습니다.\n- React가 관리하는 DOM 노드를 직접 변경하는 것은 피하세요.\n- 불가피하게 React가 관리하는 DOM 노드를 수정해야 한다면, React가 업데이트할 이유가 없는 부분만 수정해야 합니다.\n\n</Recap>\n\n<Challenges>\n\n#### 비디오 재생과 멈춤 {/*play-and-pause-the-video*/}\n\n이 예시에서 버튼은 재생과 멈춤 상태를 토글 합니다. 하지만 실제로 비디오가 재생되거나 멈추기 위해서는 state를 변경하는 것으로 충분하지 않습니다. `<video>` DOM 요소의 [`play()`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/play)와 [`pause()`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/pause)를 호출해야 합니다. ref를 추가하고 버튼이 작동하게 만들어보세요.\n\n<Sandpack>\n\n```js\nimport { useState, useRef } from 'react';\n\nexport default function VideoPlayer() {\n  const [isPlaying, setIsPlaying] = useState(false);\n\n  function handleClick() {\n    const nextIsPlaying = !isPlaying;\n    setIsPlaying(nextIsPlaying);\n  }\n\n  return (\n    <>\n      <button onClick={handleClick}>\n        {isPlaying ? 'Pause' : 'Play'}\n      </button>\n      <video width=\"250\">\n        <source\n          src=\"https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4\"\n          type=\"video/mp4\"\n        />\n      </video>\n    </>\n  )\n}\n```\n\n```css\nbutton { display: block; margin-bottom: 20px; }\n```\n\n</Sandpack>\n\n추가적인 도전을 하고자 한다면, 비디오를 마우스 오른쪽 버튼으로 클릭하고 내장된 브라우저 미디어 컨트롤을 사용하여 재생할 때도 \"재생\" 버튼이 비디오의 재생 여부와 동기화될 수 있도록 하세요. 이를 위해 비디오의 `onPlay`와 `onPause` 이벤트를 청취할 수 있습니다.\n\n<Solution>\n\nref를 선언하고 `<video>` 요소에 추가해 보세요. 그리고 다음 상태에 종속된 이벤트 핸들러에서 `ref.current.play()`와 `ref.current.pause()`를 호출하세요.\n\n<Sandpack>\n\n```js\nimport { useState, useRef } from 'react';\n\nexport default function VideoPlayer() {\n  const [isPlaying, setIsPlaying] = useState(false);\n  const ref = useRef(null);\n\n  function handleClick() {\n    const nextIsPlaying = !isPlaying;\n    setIsPlaying(nextIsPlaying);\n\n    if (nextIsPlaying) {\n      ref.current.play();\n    } else {\n      ref.current.pause();\n    }\n  }\n\n  return (\n    <>\n      <button onClick={handleClick}>\n        {isPlaying ? 'Pause' : 'Play'}\n      </button>\n      <video\n        width=\"250\"\n        ref={ref}\n        onPlay={() => setIsPlaying(true)}\n        onPause={() => setIsPlaying(false)}\n      >\n        <source\n          src=\"https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4\"\n          type=\"video/mp4\"\n        />\n      </video>\n    </>\n  )\n}\n```\n\n```css\nbutton { display: block; margin-bottom: 20px; }\n```\n\n</Sandpack>\n\n내장된 브라우저 컨트롤을 처리하기 위해, `<video>` 요소에 `onPlay`와 `onPause` 핸들러를 추가하고 해당 핸들러에서 `setIsPlaying`을 호출할 수 있습니다. 이 방식을 통해 사용자가 브라우저 컨트롤을 사용하여 비디오를 재생할 경우, state가 이에 맞게 조정됩니다.\n\n</Solution>\n\n#### 검색 필드에 포커스하기 {/*focus-the-search-field*/}\n\n\"Search\" 버튼을 클릭하면 입력 필드에 포커스가 이동하도록 만들어 보세요.\n\n<Sandpack>\n\n```js\nexport default function Page() {\n  return (\n    <>\n      <nav>\n        <button>Search</button>\n      </nav>\n      <input\n        placeholder=\"Looking for something?\"\n      />\n    </>\n  );\n}\n```\n\n```css\nbutton { display: block; margin-bottom: 10px; }\n```\n\n</Sandpack>\n\n<Solution>\n\ninput에 ref를 추가하고 `focus()`를 호출하여 포커스를 이동하세요.\n\n<Sandpack>\n\n```js\nimport { useRef } from 'react';\n\nexport default function Page() {\n  const inputRef = useRef(null);\n  return (\n    <>\n      <nav>\n        <button onClick={() => {\n          inputRef.current.focus();\n        }}>\n          Search\n        </button>\n      </nav>\n      <input\n        ref={inputRef}\n        placeholder=\"Looking for something?\"\n      />\n    </>\n  );\n}\n```\n\n```css\nbutton { display: block; margin-bottom: 10px; }\n```\n\n</Sandpack>\n\n</Solution>\n\n#### 이미지 캐러셀 스크롤링 {/*scrolling-an-image-carousel*/}\n\n이 이미지 캐러셀은 활성화된 이미지를 전환하는 \"Next\" 버튼이 있습니다. 클릭할 때 갤러리가 활성화된 이미지로 수평 스크롤 되도록 만들어 봅시다. 활성화된 이미지의 DOM 노드에서 [`scrollIntoView()`](https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView) 호출이 필요할 수 있습니다.\n\n```js\nnode.scrollIntoView({\n  behavior: 'smooth',\n  block: 'nearest',\n  inline: 'center'\n});\n```\n\n<Hint>\n\n이 활동을 위해 모든 이미지에 `ref`를 부여할 필요는 없습니다. 현재 활성화된 이미지 혹은 리스트 자체와 연결되는 ref 하나면 충분합니다. `flushSync`를 사용해서 스크롤 하기 전에 DOM이 변경되도록 하세요.\n\n</Hint>\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function CatFriends() {\n  const [index, setIndex] = useState(0);\n  return (\n    <>\n      <nav>\n        <button onClick={() => {\n          if (index < catList.length - 1) {\n            setIndex(index + 1);\n          } else {\n            setIndex(0);\n          }\n        }}>\n          Next\n        </button>\n      </nav>\n      <div>\n        <ul>\n          {catList.map((cat, i) => (\n            <li key={cat.id}>\n              <img\n                className={\n                  index === i ?\n                    'active' :\n                    ''\n                }\n                src={cat.imageUrl}\n                alt={'Cat #' + cat.id}\n              />\n            </li>\n          ))}\n        </ul>\n      </div>\n    </>\n  );\n}\n\nconst catCount = 10;\nconst catList = new Array(catCount);\nfor (let i = 0; i < catCount; i++) {\n  const bucket = Math.floor(Math.random() * catCount) % 2;\n  let imageUrl = '';\n  switch (bucket) {\n    case 0: {\n      imageUrl = \"https://placecats.com/neo/250/200\";\n      break;\n    }\n    case 1: {\n      imageUrl = \"https://placecats.com/millie/250/200\";\n      break;\n    }\n    case 2:\n    default: {\n      imageUrl = \"https://placecats.com/bella/250/200\";\n      break;\n    }\n  }\n  catList[i] = {\n    id: i,\n    imageUrl,\n  };\n}\n\n```\n\n```css\ndiv {\n  width: 100%;\n  overflow: hidden;\n}\n\nnav {\n  text-align: center;\n}\n\nbutton {\n  margin: .25rem;\n}\n\nul,\nli {\n  list-style: none;\n  white-space: nowrap;\n}\n\nli {\n  display: inline;\n  padding: 0.5rem;\n}\n\nimg {\n  padding: 10px;\n  margin: -10px;\n  transition: background 0.2s linear;\n}\n\n.active {\n  background: rgba(0, 100, 150, 0.4);\n}\n```\n\n</Sandpack>\n\n<Solution>\n\n`selectedRef`를 선언하고 조건적으로 현재 활성화된 이미지에 전달할 수 있습니다.\n\n```js\n<li ref={index === i ? selectedRef : null}>\n```\n\n`index === i` 조건이 만족할 때 이 이미지가 선택된 이미지라는 뜻이고 그 `<li>`은 `selectedRef`를 받을 것입니다. React는 `selectedRef.current`가 현재 선택된 올바른 DOM 노드를 올바르게 가리키도록 합니다.\n\n스크롤 하기 전에 React가 DOM 변경을 끝내기 위해 `flushSync` 호출이 필요하다는 것을 주의하세요. 그렇지 않다면 `selectedRef.current`는 항상 이전에 선택된 아이템을 가리키고 있을 것입니다.\n\n<Sandpack>\n\n```js\nimport { useRef, useState } from 'react';\nimport { flushSync } from 'react-dom';\n\nexport default function CatFriends() {\n  const selectedRef = useRef(null);\n  const [index, setIndex] = useState(0);\n\n  return (\n    <>\n      <nav>\n        <button onClick={() => {\n          flushSync(() => {\n            if (index < catList.length - 1) {\n              setIndex(index + 1);\n            } else {\n              setIndex(0);\n            }\n          });\n          selectedRef.current.scrollIntoView({\n            behavior: 'smooth',\n            block: 'nearest',\n            inline: 'center'\n          });\n        }}>\n          Next\n        </button>\n      </nav>\n      <div>\n        <ul>\n          {catList.map((cat, i) => (\n            <li\n              key={cat.id}\n              ref={index === i ?\n                selectedRef :\n                null\n              }\n            >\n              <img\n                className={\n                  index === i ?\n                    'active'\n                    : ''\n                }\n                src={cat.imageUrl}\n                alt={'Cat #' + cat.id}\n              />\n            </li>\n          ))}\n        </ul>\n      </div>\n    </>\n  );\n}\n\nconst catCount = 10;\nconst catList = new Array(catCount);\nfor (let i = 0; i < catCount; i++) {\n  const bucket = Math.floor(Math.random() * catCount) % 2;\n  let imageUrl = '';\n  switch (bucket) {\n    case 0: {\n      imageUrl = \"https://placecats.com/neo/250/200\";\n      break;\n    }\n    case 1: {\n      imageUrl = \"https://placecats.com/millie/250/200\";\n      break;\n    }\n    case 2:\n    default: {\n      imageUrl = \"https://placecats.com/bella/250/200\";\n      break;\n    }\n  }\n  catList[i] = {\n    id: i,\n    imageUrl,\n  };\n}\n\n```\n\n```css\ndiv {\n  width: 100%;\n  overflow: hidden;\n}\n\nnav {\n  text-align: center;\n}\n\nbutton {\n  margin: .25rem;\n}\n\nul,\nli {\n  list-style: none;\n  white-space: nowrap;\n}\n\nli {\n  display: inline;\n  padding: 0.5rem;\n}\n\nimg {\n  padding: 10px;\n  margin: -10px;\n  transition: background 0.2s linear;\n}\n\n.active {\n  background: rgba(0, 100, 150, 0.4);\n}\n```\n\n</Sandpack>\n\n</Solution>\n\n#### 별개의 컴포넌트에서 검색 필드에 포커스 이동하기 {/*focus-the-search-field-with-separate-components*/}\n\n\"Search\" 버튼을 클릭하면 포커스가 필드에 놓이도록 해보세요. 각 컴포넌트는 별개의 파일에 정의되어 있고 코드의 위치를 옮겨서는 안 된다는 점을 명심하세요. 별개의 컴포넌트들을 어떻게 연결할 수 있을까요?\n\n<Hint>\n\n`SearchInput` 같은 컴포넌트에서 DOM 노드를 노출하려면 `ref`를 Prop처럼 전달해야 합니다.\n\n</Hint>\n\n<Sandpack>\n\n```js src/App.js\nimport SearchButton from './SearchButton.js';\nimport SearchInput from './SearchInput.js';\n\nexport default function Page() {\n  return (\n    <>\n      <nav>\n        <SearchButton />\n      </nav>\n      <SearchInput />\n    </>\n  );\n}\n```\n\n```js src/SearchButton.js\nexport default function SearchButton() {\n  return (\n    <button>\n      Search\n    </button>\n  );\n}\n```\n\n```js src/SearchInput.js\nexport default function SearchInput() {\n  return (\n    <input\n      placeholder=\"Looking for something?\"\n    />\n  );\n}\n```\n\n```css\nbutton { display: block; margin-bottom: 10px; }\n```\n\n</Sandpack>\n\n<Solution>\n\n`SearchButton`에 `onClick` prop을 추가하고 `SearchButton`은 `onClick`을 브라우저의 `<button>`에 전달하도록 만드세요. 또 `<SearchInput>`에 ref를 사용하고 실제 `<input>`이 연결되도록 해야 합니다. 마지막으로 클릭 핸들러에서 ref에 저장된 DOM 노드 내부의 `focus`를 호출하세요.\n\n<Sandpack>\n\n```js src/App.js\nimport { useRef } from 'react';\nimport SearchButton from './SearchButton.js';\nimport SearchInput from './SearchInput.js';\n\nexport default function Page() {\n  const inputRef = useRef(null);\n  return (\n    <>\n      <nav>\n        <SearchButton onClick={() => {\n          inputRef.current.focus();\n        }} />\n      </nav>\n      <SearchInput ref={inputRef} />\n    </>\n  );\n}\n```\n\n```js src/SearchButton.js\nexport default function SearchButton({ onClick }) {\n  return (\n    <button onClick={onClick}>\n      Search\n    </button>\n  );\n}\n```\n\n```js src/SearchInput.js\nexport default function SearchInput({ ref }) {\n  return (\n    <input\n      ref={ref}\n      placeholder=\"Looking for something?\"\n    />\n  );\n}\n```\n\n```css\nbutton { display: block; margin-bottom: 10px; }\n```\n\n</Sandpack>\n\n</Solution>\n\n</Challenges>\n"
  },
  {
    "path": "src/content/learn/passing-data-deeply-with-context.md",
    "content": "---\ntitle: Context를 사용해 데이터를 깊게 전달하기\n---\n\n<Intro>\n\n보통의 경우 부모 컴포넌트에서 자식 컴포넌트로 props를 통해 정보를 전달합니다. 그러나 중간에 많은 컴포넌트를 거쳐야 하거나, 애플리케이션의 많은 컴포넌트에서 동일한 정보가 필요한 경우에는 props를 전달하는 것이 번거롭고 불편할 수 있습니다. *Context*는 부모 컴포넌트가 트리 아래에 있는 모든 컴포넌트에 깊이에 상관없이 정보를 명시적으로 props를 통해 전달하지 않고도 사용할 수 있게 해줍니다.\n\n</Intro>\n\n<YouWillLearn>\n\n- \"Prop drilling\" 이란?\n- Context로 반복적인 prop 전달 대체하기\n- Context의 일반적인 사용 사례\n- Context의 일반적인 대안\n\n</YouWillLearn>\n\n## Props 전달하기의 문제점 {/*the-problem-with-passing-props*/}\n\n[Props 전달하기](/learn/passing-props-to-a-component)는 UI 트리를 통해 해당 데이터를 사용하는 컴포넌트에 명시적으로 데이터를 전달하는 훌륭한 방법입니다.\n\n하지만 이 방식은 어떤 prop을 트리를 통해 깊이 전해줘야 하거나, 많은 컴포넌트에서 같은 prop이 필요한 경우에 장황하고 불편할 수 있습니다. 데이터가 필요한 여러 컴포넌트의 가장 가까운 공통 조상은 트리 상 높이 위치할 수 있고 그렇게 높게까지 [state를 끌어올리는 것](/learn/sharing-state-between-components)은 \"Prop drilling\"이라는 상황을 초래할 수 있습니다.\n\n<DiagramGroup>\n\n<Diagram name=\"passing_data_lifting_state\" height={160} width={608} captionPosition=\"top\" alt=\"Diagram with a tree of three components. The parent contains a bubble representing a value highlighted in purple. The value flows down to each of the two children, both highlighted in purple.\" >\n\nState 끌어올리기\n\n</Diagram>\n<Diagram name=\"passing_data_prop_drilling\" height={430} width={608} captionPosition=\"top\" alt=\"Diagram with a tree of ten nodes, each node with two children or less. The root node contains a bubble representing a value highlighted in purple. The value flows down through the two children, each of which pass the value but do not contain it. The left child passes the value down to two children which are both highlighted purple. The right child of the root passes the value through to one of its two children - the right one, which is highlighted purple. That child passed the value through its single child, which passes it down to both of its two children, which are highlighted purple.\">\n\nProp drilling\n\n</Diagram>\n\n</DiagramGroup>\n\n데이터를 사용할 트리의 내부 컴포넌트에 props를 전달하는 대신 \"순간이동\"시킬 방법이 있다면 좋지 않을까요? React의 context를 사용하면 됩니다!\n\n## Context: Props 전달하기의 대안 {/*context-an-alternative-to-passing-props*/}\n\nContext는 부모 컴포넌트가 그 아래의 트리 전체에 데이터를 전달할 수 있도록 해줍니다. Context에는 많은 용도가 있습니다. 하나의 예시로 다음의 크기 조정을 위해 `level`을 받는  `Heading` 컴포넌트를 보세요.\n\n<Sandpack>\n\n```js\nimport Heading from './Heading.js';\nimport Section from './Section.js';\n\nexport default function Page() {\n  return (\n    <Section>\n      <Heading level={1}>Title</Heading>\n      <Heading level={2}>Heading</Heading>\n      <Heading level={3}>Sub-heading</Heading>\n      <Heading level={4}>Sub-sub-heading</Heading>\n      <Heading level={5}>Sub-sub-sub-heading</Heading>\n      <Heading level={6}>Sub-sub-sub-sub-heading</Heading>\n    </Section>\n  );\n}\n```\n\n```js src/Section.js\nexport default function Section({ children }) {\n  return (\n    <section className=\"section\">\n      {children}\n    </section>\n  );\n}\n```\n\n```js src/Heading.js\nexport default function Heading({ level, children }) {\n  switch (level) {\n    case 1:\n      return <h1>{children}</h1>;\n    case 2:\n      return <h2>{children}</h2>;\n    case 3:\n      return <h3>{children}</h3>;\n    case 4:\n      return <h4>{children}</h4>;\n    case 5:\n      return <h5>{children}</h5>;\n    case 6:\n      return <h6>{children}</h6>;\n    default:\n      throw Error('Unknown level: ' + level);\n  }\n}\n```\n\n```css\n.section {\n  padding: 10px;\n  margin: 5px;\n  border-radius: 5px;\n  border: 1px solid #aaa;\n}\n```\n\n</Sandpack>\n\n같은 `Section` 내의 여러 제목이 항상 동일한 크기를 가져야 한다고 가정해 봅시다.\n\n<Sandpack>\n\n```js\nimport Heading from './Heading.js';\nimport Section from './Section.js';\n\nexport default function Page() {\n  return (\n    <Section>\n      <Heading level={1}>Title</Heading>\n      <Section>\n        <Heading level={2}>Heading</Heading>\n        <Heading level={2}>Heading</Heading>\n        <Heading level={2}>Heading</Heading>\n        <Section>\n          <Heading level={3}>Sub-heading</Heading>\n          <Heading level={3}>Sub-heading</Heading>\n          <Heading level={3}>Sub-heading</Heading>\n          <Section>\n            <Heading level={4}>Sub-sub-heading</Heading>\n            <Heading level={4}>Sub-sub-heading</Heading>\n            <Heading level={4}>Sub-sub-heading</Heading>\n          </Section>\n        </Section>\n      </Section>\n    </Section>\n  );\n}\n```\n\n```js src/Section.js\nexport default function Section({ children }) {\n  return (\n    <section className=\"section\">\n      {children}\n    </section>\n  );\n}\n```\n\n```js src/Heading.js\nexport default function Heading({ level, children }) {\n  switch (level) {\n    case 1:\n      return <h1>{children}</h1>;\n    case 2:\n      return <h2>{children}</h2>;\n    case 3:\n      return <h3>{children}</h3>;\n    case 4:\n      return <h4>{children}</h4>;\n    case 5:\n      return <h5>{children}</h5>;\n    case 6:\n      return <h6>{children}</h6>;\n    default:\n      throw Error('Unknown level: ' + level);\n  }\n}\n```\n\n```css\n.section {\n  padding: 10px;\n  margin: 5px;\n  border-radius: 5px;\n  border: 1px solid #aaa;\n}\n```\n\n</Sandpack>\n\n지금은 각각의 `<Heading>`에 `level` prop을 전달하고 있습니다.\n\n```js\n<Section>\n  <Heading level={3}>About</Heading>\n  <Heading level={3}>Photos</Heading>\n  <Heading level={3}>Videos</Heading>\n</Section>\n```\n\n`<Section>` 컴포넌트에 `level` prop을 전달해 이를 `<Heading>`에서 제거할 수 있으면 좋겠네요. 이렇게 하면 같은 섹션의 모든 제목이 동일한 크기를 갖도록 지정할 수 있습니다.\n\n```js\n<Section level={3}>\n  <Heading>About</Heading>\n  <Heading>Photos</Heading>\n  <Heading>Videos</Heading>\n</Section>\n```\n\n하지만 어떻게 `<Heading>` 컴포넌트가 가장 가까운 `<Section>`의 레벨을 알 수 있을까요? **그렇게 하려면 자식에게 트리 위 어딘가에 있는 데이터를 \"요청하는\" 방법이 필요합니다.**\n\nProps만으로는 불가능합니다. 여기서부터 context가 활약하기 시작합니다. 다음의 세 단계로 진행됩니다.\n\n1. Context를 **생성하세요**. (제목 레벨을 위한 것이므로 `LevelContext`라고 이름 지어봅시다.)\n2. 데이터가 필요한 컴포넌트에서 context를 **사용하세요**. (`Heading`에서는 `LevelContext`를 사용합니다.)\n3. 데이터를 지정하는 컴포넌트에서 context를 **제공하세요**. (`Section`에서는 `LevelContext`를 제공합니다.)\n\nContext는 부모가 트리 내부 전체에, 심지어 멀리 떨어진 컴포넌트에조차 어떤 데이터를 제공할 수 있도록 합니다.\n\n<DiagramGroup>\n\n<Diagram name=\"passing_data_context_close\" height={160} width={608} captionPosition=\"top\" alt=\"Diagram with a tree of three components. The parent contains a bubble representing a value highlighted in orange which projects down to the two children, each highlighted in orange.\" >\n\nContext를 가까운 자식 컴포넌트에서 사용하기\n\n</Diagram>\n\n<Diagram name=\"passing_data_context_far\" height={430} width={608} captionPosition=\"top\" alt=\"Diagram with a tree of ten nodes, each node with two children or less. The root parent node contains a bubble representing a value highlighted in orange. The value projects down directly to four leaves and one intermediate component in the tree, which are all highlighted in orange. None of the other intermediate components are highlighted.\">\n\nContext를 먼 자식 컴포넌트에서 사용하기\n\n</Diagram>\n\n</DiagramGroup>\n\n### 1단계: Context 생성하기 {/*step-1-create-the-context*/}\n\n먼저 context를 만들어야 합니다. 컴포넌트에서 사용할 수 있도록 **파일에서 내보내야 합니다.**\n\n<Sandpack>\n\n```js\nimport Heading from './Heading.js';\nimport Section from './Section.js';\n\nexport default function Page() {\n  return (\n    <Section>\n      <Heading level={1}>Title</Heading>\n      <Section>\n        <Heading level={2}>Heading</Heading>\n        <Heading level={2}>Heading</Heading>\n        <Heading level={2}>Heading</Heading>\n        <Section>\n          <Heading level={3}>Sub-heading</Heading>\n          <Heading level={3}>Sub-heading</Heading>\n          <Heading level={3}>Sub-heading</Heading>\n          <Section>\n            <Heading level={4}>Sub-sub-heading</Heading>\n            <Heading level={4}>Sub-sub-heading</Heading>\n            <Heading level={4}>Sub-sub-heading</Heading>\n          </Section>\n        </Section>\n      </Section>\n    </Section>\n  );\n}\n```\n\n```js src/Section.js\nexport default function Section({ children }) {\n  return (\n    <section className=\"section\">\n      {children}\n    </section>\n  );\n}\n```\n\n```js src/Heading.js\nexport default function Heading({ level, children }) {\n  switch (level) {\n    case 1:\n      return <h1>{children}</h1>;\n    case 2:\n      return <h2>{children}</h2>;\n    case 3:\n      return <h3>{children}</h3>;\n    case 4:\n      return <h4>{children}</h4>;\n    case 5:\n      return <h5>{children}</h5>;\n    case 6:\n      return <h6>{children}</h6>;\n    default:\n      throw Error('Unknown level: ' + level);\n  }\n}\n```\n\n```js src/LevelContext.js active\nimport { createContext } from 'react';\n\nexport const LevelContext = createContext(1);\n```\n\n```css\n.section {\n  padding: 10px;\n  margin: 5px;\n  border-radius: 5px;\n  border: 1px solid #aaa;\n}\n```\n\n</Sandpack>\n\n`createContext`의 유일한 인자는 기본값입니다. 여기서 `1`은 가장 큰 제목 레벨을 나타내지만 모든 종류의 값을(객체까지) 전달할 수 있습니다. 다음 단계에서 기본값이 얼마나 중요한지 알게 됩니다.\n\n### 2단계: Context 사용하기 {/*step-2-use-the-context*/}\n\nReact에서 `useContext` Hook과 생성한 Context를 가져옵니다.\n\n```js\nimport { useContext } from 'react';\nimport { LevelContext } from './LevelContext.js';\n```\n\n지금은 `Heading` 컴포넌트가 `level`을 props에서 읽어옵니다.\n\n```js\nexport default function Heading({ level, children }) {\n  // ...\n}\n```\n\n`level`prop을 제거하고 대신 위에서 가져온 context인 `LevelContext`에서 값을 읽도록 합니다.\n\n```js {2}\nexport default function Heading({ children }) {\n  const level = useContext(LevelContext);\n  // ...\n}\n```\n\n`useContext`는 Hook입니다. `useState`, `useReducer`와 같이 Hook은 React 컴포넌트의 바로 안에서만 호출할 수 있습니다. (반복문이나 조건문 내부가 아닙니다.) **`useContext`는 React에게 `Heading` 컴포넌트가 `LevelContext`를 읽으려 한다고 알려줍니다.**\n\n이제 `Heading` 컴포넌트는 `level` prop을 갖지 않습니다. 이제는 JSX에서 다음과 같이 `Heading`에 `level` prop을 전달할 필요가 없습니다.\n\n```js\n<Section>\n  <Heading level={4}>Sub-sub-heading</Heading>\n  <Heading level={4}>Sub-sub-heading</Heading>\n  <Heading level={4}>Sub-sub-heading</Heading>\n</Section>\n```\n\n`Section`이 대신 `level`을 받도록 JSX를 업데이트합니다.\n\n```jsx\n<Section level={4}>\n  <Heading>Sub-sub-heading</Heading>\n  <Heading>Sub-sub-heading</Heading>\n  <Heading>Sub-sub-heading</Heading>\n</Section>\n```\n\n다시 한번 알려드리자면, 동작하도록 만들려던 마크업은 다음과 같습니다.\n\n<Sandpack>\n\n```js\nimport Heading from './Heading.js';\nimport Section from './Section.js';\n\nexport default function Page() {\n  return (\n    <Section level={1}>\n      <Heading>Title</Heading>\n      <Section level={2}>\n        <Heading>Heading</Heading>\n        <Heading>Heading</Heading>\n        <Heading>Heading</Heading>\n        <Section level={3}>\n          <Heading>Sub-heading</Heading>\n          <Heading>Sub-heading</Heading>\n          <Heading>Sub-heading</Heading>\n          <Section level={4}>\n            <Heading>Sub-sub-heading</Heading>\n            <Heading>Sub-sub-heading</Heading>\n            <Heading>Sub-sub-heading</Heading>\n          </Section>\n        </Section>\n      </Section>\n    </Section>\n  );\n}\n```\n\n```js src/Section.js\nexport default function Section({ children }) {\n  return (\n    <section className=\"section\">\n      {children}\n    </section>\n  );\n}\n```\n\n```js src/Heading.js\nimport { useContext } from 'react';\nimport { LevelContext } from './LevelContext.js';\n\nexport default function Heading({ children }) {\n  const level = useContext(LevelContext);\n  switch (level) {\n    case 1:\n      return <h1>{children}</h1>;\n    case 2:\n      return <h2>{children}</h2>;\n    case 3:\n      return <h3>{children}</h3>;\n    case 4:\n      return <h4>{children}</h4>;\n    case 5:\n      return <h5>{children}</h5>;\n    case 6:\n      return <h6>{children}</h6>;\n    default:\n      throw Error('Unknown level: ' + level);\n  }\n}\n```\n\n```js src/LevelContext.js\nimport { createContext } from 'react';\n\nexport const LevelContext = createContext(1);\n```\n\n```css\n.section {\n  padding: 10px;\n  margin: 5px;\n  border-radius: 5px;\n  border: 1px solid #aaa;\n}\n```\n\n</Sandpack>\n\n이 예시는 아직 잘 작동하지 않습니다! **Context를 *사용하고* 있지만, 아직 *제공하지* 않았기 때문에** 모든 제목의 크기가 동일합니다. React가 어디서 값을 가져와야 할지 모르기 때문이죠.\n\nContext를 제공하지 않으면 React는 이전 단계에서 지정한 기본값을 사용합니다. 이 예시에서는 `1`을 `createContext`의 인수로 지정했으므로 `useContext(LevelContext)`가 `1`을 반환하고 모든 제목을 `<h1>`로 설정합니다. 이 문제를 각각의 `Section`이 고유한 context를 제공하도록 하여 해결합시다.\n\n### 3단계: Context 제공하기 {/*step-3-provide-the-context*/}\n\n`Section` 컴포넌트는 자식들을 렌더링하고 있습니다.\n\n```js\nexport default function Section({ children }) {\n  return (\n    <section className=\"section\">\n      {children}\n    </section>\n  );\n}\n```\n\n`LevelContext`를 자식들에게 제공하기 위해 **context provider로 감싸줍니다.**\n\n```js {1,6,8}\nimport { LevelContext } from './LevelContext.js';\n\nexport default function Section({ level, children }) {\n  return (\n    <section className=\"section\">\n      <LevelContext value={level}>\n        {children}\n      </LevelContext>\n    </section>\n  );\n}\n```\n\n이것은 React에게 `<Section>` 내의 어떤 컴포넌트가 `LevelContext`를 요구하면 `level`을 주라고 알려줍니다. 컴포넌트는 그 위에 있는 UI 트리에서 가장 가까운 `<LevelContext>`의 값을 사용합니다.\n\n<Sandpack>\n\n```js\nimport Heading from './Heading.js';\nimport Section from './Section.js';\n\nexport default function Page() {\n  return (\n    <Section level={1}>\n      <Heading>Title</Heading>\n      <Section level={2}>\n        <Heading>Heading</Heading>\n        <Heading>Heading</Heading>\n        <Heading>Heading</Heading>\n        <Section level={3}>\n          <Heading>Sub-heading</Heading>\n          <Heading>Sub-heading</Heading>\n          <Heading>Sub-heading</Heading>\n          <Section level={4}>\n            <Heading>Sub-sub-heading</Heading>\n            <Heading>Sub-sub-heading</Heading>\n            <Heading>Sub-sub-heading</Heading>\n          </Section>\n        </Section>\n      </Section>\n    </Section>\n  );\n}\n```\n\n```js src/Section.js\nimport { LevelContext } from './LevelContext.js';\n\nexport default function Section({ level, children }) {\n  return (\n    <section className=\"section\">\n      <LevelContext value={level}>\n        {children}\n      </LevelContext>\n    </section>\n  );\n}\n```\n\n```js src/Heading.js\nimport { useContext } from 'react';\nimport { LevelContext } from './LevelContext.js';\n\nexport default function Heading({ children }) {\n  const level = useContext(LevelContext);\n  switch (level) {\n    case 1:\n      return <h1>{children}</h1>;\n    case 2:\n      return <h2>{children}</h2>;\n    case 3:\n      return <h3>{children}</h3>;\n    case 4:\n      return <h4>{children}</h4>;\n    case 5:\n      return <h5>{children}</h5>;\n    case 6:\n      return <h6>{children}</h6>;\n    default:\n      throw Error('Unknown level: ' + level);\n  }\n}\n```\n\n```js src/LevelContext.js\nimport { createContext } from 'react';\n\nexport const LevelContext = createContext(1);\n```\n\n```css\n.section {\n  padding: 10px;\n  margin: 5px;\n  border-radius: 5px;\n  border: 1px solid #aaa;\n}\n```\n\n</Sandpack>\n\n기존 코드와 동일한 결과이지만 `level` prop을 각 `Heading` 컴포넌트에 전달할 필요는 없습니다! 대신 위의 가장 가까운 `Section`에게 제목 레벨을 \"확인\"합니다.\n\n1. `level` prop 을 `<Section>`에 전달합니다.\n2. `Section`은 자식을 `<LevelContext value={level}>`로 감싸줍니다.\n3. `Heading`은 `useContext(LevelContext)`를 사용해 가장 근처의 `LevelContext`의 값을 요청합니다.\n\n## 같은 컴포넌트에서 context를 사용하며 제공하기 {/*using-and-providing-context-from-the-same-component*/}\n\n지금은 각각의 섹션에 `level`을 수동으로 지정해야 합니다.\n\n```js\nexport default function Page() {\n  return (\n    <Section level={1}>\n      ...\n      <Section level={2}>\n        ...\n        <Section level={3}>\n          ...\n```\n\nContext를 통해 위의 컴포넌트에서 정보를 읽을 수 있으므로 각 `Section`은 위의 `Section`에서 `level`을 읽고 자동으로 `level + 1`을 아래로 전달할 수 있습니다. 방법은 다음과 같습니다.\n\n```js src/Section.js {5,8}\nimport { useContext } from 'react';\nimport { LevelContext } from './LevelContext.js';\n\nexport default function Section({ children }) {\n  const level = useContext(LevelContext);\n  return (\n    <section className=\"section\">\n      <LevelContext value={level + 1}>\n        {children}\n      </LevelContext>\n    </section>\n  );\n}\n```\n\n이렇게 바꾸면 `<Section>`과 `<Heading>` *둘 모두에* `level`을 전달할 필요가 없습니다.\n\n<Sandpack>\n\n```js\nimport Heading from './Heading.js';\nimport Section from './Section.js';\n\nexport default function Page() {\n  return (\n    <Section>\n      <Heading>Title</Heading>\n      <Section>\n        <Heading>Heading</Heading>\n        <Heading>Heading</Heading>\n        <Heading>Heading</Heading>\n        <Section>\n          <Heading>Sub-heading</Heading>\n          <Heading>Sub-heading</Heading>\n          <Heading>Sub-heading</Heading>\n          <Section>\n            <Heading>Sub-sub-heading</Heading>\n            <Heading>Sub-sub-heading</Heading>\n            <Heading>Sub-sub-heading</Heading>\n          </Section>\n        </Section>\n      </Section>\n    </Section>\n  );\n}\n```\n\n```js src/Section.js\nimport { useContext } from 'react';\nimport { LevelContext } from './LevelContext.js';\n\nexport default function Section({ children }) {\n  const level = useContext(LevelContext);\n  return (\n    <section className=\"section\">\n      <LevelContext value={level + 1}>\n        {children}\n      </LevelContext>\n    </section>\n  );\n}\n```\n\n```js src/Heading.js\nimport { useContext } from 'react';\nimport { LevelContext } from './LevelContext.js';\n\nexport default function Heading({ children }) {\n  const level = useContext(LevelContext);\n  switch (level) {\n    case 0:\n      throw Error('Heading must be inside a Section!');\n    case 1:\n      return <h1>{children}</h1>;\n    case 2:\n      return <h2>{children}</h2>;\n    case 3:\n      return <h3>{children}</h3>;\n    case 4:\n      return <h4>{children}</h4>;\n    case 5:\n      return <h5>{children}</h5>;\n    case 6:\n      return <h6>{children}</h6>;\n    default:\n      throw Error('Unknown level: ' + level);\n  }\n}\n```\n\n```js src/LevelContext.js\nimport { createContext } from 'react';\n\nexport const LevelContext = createContext(0);\n```\n\n```css\n.section {\n  padding: 10px;\n  margin: 5px;\n  border-radius: 5px;\n  border: 1px solid #aaa;\n}\n```\n\n</Sandpack>\n\n이제 `Heading`과 `Section` 모두 자신들이 얼마나 \"깊이\" 있는지 확인하기 위해 `LevelContext`를 읽습니다. 그리고 `Section`은 그 안에 있는 어떤 것이든 \"더 깊은\" 레벨이라는 것을 명시하기 위해 자식들을 `LevelContext`로 감싸고 있습니다.\n\n<Note>\n\n이 예시에서는 하위 컴포넌트가 context를 오버라이드 할 수 있는 방법을 시각적으로 보여주기 때문에 제목 레벨을 사용합니다. 하지만 context는 다른 많은 상황에서도 유용합니다. 현재 색상 테마, 현재 로그인된 사용자 등 전체 하위 트리에 필요한 정보를 전달할 수 있습니다.\n\n</Note>\n\n## Context로 중간 컴포넌트 지나치기 {/*context-passes-through-intermediate-components*/}\n\nContext를 제공하는 컴포넌트와 context를 사용하는 컴포넌트 사이에 원하는 만큼의 컴포넌트를 삽입할 수 있습니다. 여기에는 `<div>`와 같은 기본 컴포넌트와 직접 만들 수 있는 컴포넌트가 모두 포함됩니다.\n\n이 예시에서는 점선 테두리를 가진 동일한 `Post` 컴포넌트가 두 가지 다른 중첩 레벨로 렌더링 됩니다. 내부의 `<Heading>`이 가장 가까운 `<Section>`에서 자동으로 레벨을 가져오는 것에 주목하세요.\n\n<Sandpack>\n\n```js\nimport Heading from './Heading.js';\nimport Section from './Section.js';\n\nexport default function ProfilePage() {\n  return (\n    <Section>\n      <Heading>My Profile</Heading>\n      <Post\n        title=\"Hello traveller!\"\n        body=\"Read about my adventures.\"\n      />\n      <AllPosts />\n    </Section>\n  );\n}\n\nfunction AllPosts() {\n  return (\n    <Section>\n      <Heading>Posts</Heading>\n      <RecentPosts />\n    </Section>\n  );\n}\n\nfunction RecentPosts() {\n  return (\n    <Section>\n      <Heading>Recent Posts</Heading>\n      <Post\n        title=\"Flavors of Lisbon\"\n        body=\"...those pastéis de nata!\"\n      />\n      <Post\n        title=\"Buenos Aires in the rhythm of tango\"\n        body=\"I loved it!\"\n      />\n    </Section>\n  );\n}\n\nfunction Post({ title, body }) {\n  return (\n    <Section isFancy={true}>\n      <Heading>\n        {title}\n      </Heading>\n      <p><i>{body}</i></p>\n    </Section>\n  );\n}\n```\n\n```js src/Section.js\nimport { useContext } from 'react';\nimport { LevelContext } from './LevelContext.js';\n\nexport default function Section({ children, isFancy }) {\n  const level = useContext(LevelContext);\n  return (\n    <section className={\n      'section ' +\n      (isFancy ? 'fancy' : '')\n    }>\n      <LevelContext value={level + 1}>\n        {children}\n      </LevelContext>\n    </section>\n  );\n}\n```\n\n```js src/Heading.js\nimport { useContext } from 'react';\nimport { LevelContext } from './LevelContext.js';\n\nexport default function Heading({ children }) {\n  const level = useContext(LevelContext);\n  switch (level) {\n    case 0:\n      throw Error('Heading must be inside a Section!');\n    case 1:\n      return <h1>{children}</h1>;\n    case 2:\n      return <h2>{children}</h2>;\n    case 3:\n      return <h3>{children}</h3>;\n    case 4:\n      return <h4>{children}</h4>;\n    case 5:\n      return <h5>{children}</h5>;\n    case 6:\n      return <h6>{children}</h6>;\n    default:\n      throw Error('Unknown level: ' + level);\n  }\n}\n```\n\n```js src/LevelContext.js\nimport { createContext } from 'react';\n\nexport const LevelContext = createContext(0);\n```\n\n```css\n.section {\n  padding: 10px;\n  margin: 5px;\n  border-radius: 5px;\n  border: 1px solid #aaa;\n}\n\n.fancy {\n  border: 4px dashed pink;\n}\n```\n\n</Sandpack>\n\n이 작업을 위해 아무것도 하지 않아도 됩니다. `Section`은 트리에 대한 context를 지정하므로 아무 곳에나 `<Heading>`을 삽입할 수 있으며 알맞은 크기를 가집니다. 위의 샌드박스에서 한 번 시도해보세요!\n\n**Context를 사용하면 \"주변에 적응\"하고 렌더링 되는 *위치*(또는 *어떤 context*)에 따라 자신을 다르게 표시하는 컴포넌트를 작성할 수 있습니다.**\n\nContext의 작동 방식은 [CSS 속성 상속](https://developer.mozilla.org/ko/docs/Web/CSS/inheritance)을 연상시킵니다. CSS에서 `<div>`에 대해 `color: blue`를 지정할 수 있으며, 중간에 있는 다른 DOM 노드가 `color: green`으로 재정의하지 않는 한 그 안의 모든 DOM 노드가 그 색상을 상속합니다. 마찬가지로, React에서 위에서 가져온 어떤 context를 재정의하는 유일한 방법은 자식들을 다른 값을 가진 context provider로 래핑하는 것입니다.\n\nCSS에서 `color`와 `background-color` 같이 다른 속성들은 서로 영향을 주지 않습니다. `<div>`의 모든 `color`를 `background-color`에 영향을 미치지 않고 빨간색으로 지정할 수 있죠. 이처럼 **서로 다른 React context는 영향을 주지 않습니다.** `createContext()`로 만든 각각의 context는 완벽히 분리되어 있고 *특정 context*를 사용 및 제공하는 컴포넌트끼리 묶여 있습니다. 하나의 컴포넌트는 문제없이 많은 다른 context를 사용하거나 제공할 수 있습니다.\n\n## Context를 사용하기 전에 고려할 것 {/*before-you-use-context*/}\n\nContext는 사용하기에 꽤 유혹적입니다. 그러나 이는 또한 남용하기 쉽다는 의미이기도 합니다. **어떤 props를 여러 레벨 깊이로 전달해야 한다고 해서 해당 정보를 context에 넣어야 하는 것은 아닙니다.**\n\n다음은 context를 사용하기 전 고려해볼 몇 가지 대안들입니다.\n\n1. **[Props 전달하기](/learn/passing-props-to-a-component)로 시작하기.** 사소한 컴포넌트들이 아니라면 여러 개의 props가 여러 컴포넌트를 거쳐 가는 것은 그리 이상한 일이 아닙니다. 힘든 일처럼 느껴질 수 있지만 어떤 컴포넌트가 어떤 데이터를 사용하는지 매우 명확히 해줍니다. 데이터의 흐름이 props를 통해 분명해져 코드를 유지보수 하기에도 좋습니다.\n2. **컴포넌트를 추출하고 [JSX를 `children`으로 전달하기.](/learn/passing-props-to-a-component#passing-jsx-as-children)** 데이터를 사용하지 않는 많은 중간 컴포넌트 층을 통해 어떤 데이터를 전달하는 (더 아래로 보내기만 하는) 경우에는 컴포넌트를 추출하는 것을 잊은 경우가 많습니다. 예를 들어 `posts`처럼 직접 사용하지 않는 props를 `<Layout posts={posts} />`와 같이 전달할 수 있습니다. 대신 `Layout`은 `children`을 prop으로 받고 `<Layout><Posts posts={posts} /><Layout>`을 렌더링하세요. 이렇게 하면 데이터를 지정하는 컴포넌트와 데이터가 필요한 컴포넌트 사이의 층수가 줄어듭니다.\n\n만약 이 접근 방법들이 잘 맞지 않는다면 context를 고려해보세요.\n\n## Context 사용 예시 {/*use-cases-for-context*/}\n\n- **테마 지정하기:** 사용자가 모양을 변경할 수 있는 애플리케이션의 경우에 (e.g. 다크 모드) context provider를 앱 최상단에 두고 시각적으로 조정이 필요한 컴포넌트에서 context를 사용할 수 있습니다.\n- **현재 계정:** 로그인한 사용자를 알아야 하는 컴포넌트가 많을 수 있습니다. Context에 놓으면 트리 어디에서나 편하게 알아낼 수 있습니다. 일부 애플리케이션에서는 동시에 여러 계정을 운영할 수도 있습니다. (e.g. 다른 사용자로 댓글을 남기는 경우.) 이런 경우에는 UI의 일부를 서로 다른 현재 계정 값을 가진 provider로 감싸 주는 것이 편리합니다.\n- **라우팅:** 대부분의 라우팅 솔루션은 현재 경로를 유지하기 위해 내부적으로 context를 사용합니다. 이것이 모든 링크의 활성화 여부를 \"알 수 있는\" 방법입니다. 라우터를 만든다면 같은 방식으로 하고 싶을 것입니다.\n- **상태 관리:** 애플리케이션이 커지면 결국 앱 상단에 수많은 state가 생기게 됩니다. 아래 멀리 떨어진 많은 컴포넌트가 그 값을 변경하고 싶어할 수 있습니다. 흔히 [reducer를 context와 함께 사용하는 것](/learn/scaling-up-with-reducer-and-context)은 복잡한 state를 관리하고 번거로운 작업 없이 멀리 있는 컴포넌트까지 값을 전달하는 방법입니다.\n\nContext는 정적인 값으로 제한되지 않습니다. 다음 렌더링 시 다른 값을 준다면 React는 아래의 모든 컴포넌트에서 그 값을 갱신합니다. 이것은 context와 state가 자주 조합되는 이유입니다.\n\n일반적으로 트리의 다른 부분에서 멀리 떨어져 있는 컴포넌트들이 같은 정보가 필요하다는 것은 context를 사용하기 좋다는 징조입니다.\n\n<Recap>\n\n- Context는 컴포넌트가 트리 상 아래에 위치한 모든 곳에 데이터를 제공하도록 합니다.\n- Context를 전달하려면 다음과 같습니다\n  1. `export const MyContext = createContext(defaultValue)`로 context를 생성하고 내보내세요.\n  2. `useContext(MyContext)` Hook에 전달해 얼마나 깊이 있든 자식 컴포넌트가 읽을 수 있도록 합니다.\n  3. 자식을 `<MyContext value={...}>`로 감싸 부모로부터 context를 받도록 합니다.\n- Context는 중간의 어떤 컴포넌트도 지나갈 수 있습니다.\n- Context를 활용해 \"주변에 적응하는\" 컴포넌트를 작성할 수 있습니다.\n- Context를 사용하기 전에 props를 전달하거나 JSX를 `children`으로 전달하는 것을 먼저 시도해보세요.\n\n</Recap>\n\n<Challenges>\n\n#### Context로 prop drilling 대체하기 {/*replace-prop-drilling-with-context*/}\n\n다음의 예시에서 체크 박스를 토글하는 것은 각각의 `<PlaceImage>`에 전달된 `imageSize` prop을 변경합니다. 체크 박스의 state는 `App` 컴포넌트의 최상단에서 가지고 있지만 `<PlaceImage>`에서 그 값을 알아야 합니다.\n\n현재는 `App`이 `imageSize`를 `List`에 전달하면 또 `Place`에 전달하고 다시 `PlaceImage`에 전달하고 있습니다.`imageSize` prop을 제거하고 `App` 컴포넌트가 직접 `PlaceImage`에 값을 전달하도록 해봅시다.\n\nContext를 `Context.js`에 선언합니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { useState } from 'react';\nimport { places } from './data.js';\nimport { getImageUrl } from './utils.js';\n\nexport default function App() {\n  const [isLarge, setIsLarge] = useState(false);\n  const imageSize = isLarge ? 150 : 100;\n  return (\n    <>\n      <label>\n        <input\n          type=\"checkbox\"\n          checked={isLarge}\n          onChange={e => {\n            setIsLarge(e.target.checked);\n          }}\n        />\n        Use large images\n      </label>\n      <hr />\n      <List imageSize={imageSize} />\n    </>\n  )\n}\n\nfunction List({ imageSize }) {\n  const listItems = places.map(place =>\n    <li key={place.id}>\n      <Place\n        place={place}\n        imageSize={imageSize}\n      />\n    </li>\n  );\n  return <ul>{listItems}</ul>;\n}\n\nfunction Place({ place, imageSize }) {\n  return (\n    <>\n      <PlaceImage\n        place={place}\n        imageSize={imageSize}\n      />\n      <p>\n        <b>{place.name}</b>\n        {': ' + place.description}\n      </p>\n    </>\n  );\n}\n\nfunction PlaceImage({ place, imageSize }) {\n  return (\n    <img\n      src={getImageUrl(place)}\n      alt={place.name}\n      width={imageSize}\n      height={imageSize}\n    />\n  );\n}\n```\n\n```js src/Context.js\n\n```\n\n```js src/data.js\nexport const places = [{\n  id: 0,\n  name: 'Bo-Kaap in Cape Town, South Africa',\n  description: 'The tradition of choosing bright colors for houses began in the late 20th century.',\n  imageId: 'K9HVAGH'\n}, {\n  id: 1,\n  name: 'Rainbow Village in Taichung, Taiwan',\n  description: 'To save the houses from demolition, Huang Yung-Fu, a local resident, painted all 1,200 of them in 1924.',\n  imageId: '9EAYZrt'\n}, {\n  id: 2,\n  name: 'Macromural de Pachuca, Mexico',\n  description: 'One of the largest murals in the world covering homes in a hillside neighborhood.',\n  imageId: 'DgXHVwu'\n}, {\n  id: 3,\n  name: 'Selarón Staircase in Rio de Janeiro, Brazil',\n  description: 'This landmark was created by Jorge Selarón, a Chilean-born artist, as a \"tribute to the Brazilian people.\"',\n  imageId: 'aeO3rpI'\n}, {\n  id: 4,\n  name: 'Burano, Italy',\n  description: 'The houses are painted following a specific color system dating back to 16th century.',\n  imageId: 'kxsph5C'\n}, {\n  id: 5,\n  name: 'Chefchaouen, Marocco',\n  description: 'There are a few theories on why the houses are painted blue, including that the color repels mosquitos or that it symbolizes sky and heaven.',\n  imageId: 'rTqKo46'\n}, {\n  id: 6,\n  name: 'Gamcheon Culture Village in Busan, South Korea',\n  description: 'In 2009, the village was converted into a cultural hub by painting the houses and featuring exhibitions and art installations.',\n  imageId: 'ZfQOOzf'\n}];\n```\n\n```js src/utils.js\nexport function getImageUrl(place) {\n  return (\n    'https://i.imgur.com/' +\n    place.imageId +\n    'l.jpg'\n  );\n}\n```\n\n```css\nul { list-style-type: none; padding: 0px 10px; }\nli {\n  margin-bottom: 10px;\n  display: grid;\n  grid-template-columns: auto 1fr;\n  gap: 20px;\n  align-items: center;\n}\n```\n\n</Sandpack>\n\n<Solution>\n\n`imageSize` prop을 모든 컴포넌트에서 제거합니다.\n\n`Context.js`에 `ImageSizeContext`를 생성하고 내보냅니다. 리스트를 `<ImageSizeContext value={imageSize}>`로 감싸 값을 아래로 전달하고 `useContext(ImageSizeContext)`로 `PlaceImage`에서 그것을 읽습니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { useState, useContext } from 'react';\nimport { places } from './data.js';\nimport { getImageUrl } from './utils.js';\nimport { ImageSizeContext } from './Context.js';\n\nexport default function App() {\n  const [isLarge, setIsLarge] = useState(false);\n  const imageSize = isLarge ? 150 : 100;\n  return (\n    <ImageSizeContext\n      value={imageSize}\n    >\n      <label>\n        <input\n          type=\"checkbox\"\n          checked={isLarge}\n          onChange={e => {\n            setIsLarge(e.target.checked);\n          }}\n        />\n        Use large images\n      </label>\n      <hr />\n      <List />\n    </ImageSizeContext>\n  )\n}\n\nfunction List() {\n  const listItems = places.map(place =>\n    <li key={place.id}>\n      <Place place={place} />\n    </li>\n  );\n  return <ul>{listItems}</ul>;\n}\n\nfunction Place({ place }) {\n  return (\n    <>\n      <PlaceImage place={place} />\n      <p>\n        <b>{place.name}</b>\n        {': ' + place.description}\n      </p>\n    </>\n  );\n}\n\nfunction PlaceImage({ place }) {\n  const imageSize = useContext(ImageSizeContext);\n  return (\n    <img\n      src={getImageUrl(place)}\n      alt={place.name}\n      width={imageSize}\n      height={imageSize}\n    />\n  );\n}\n```\n\n```js src/Context.js\nimport { createContext } from 'react';\n\nexport const ImageSizeContext = createContext(500);\n```\n\n```js src/data.js\nexport const places = [{\n  id: 0,\n  name: 'Bo-Kaap in Cape Town, South Africa',\n  description: 'The tradition of choosing bright colors for houses began in the late 20th century.',\n  imageId: 'K9HVAGH'\n}, {\n  id: 1,\n  name: 'Rainbow Village in Taichung, Taiwan',\n  description: 'To save the houses from demolition, Huang Yung-Fu, a local resident, painted all 1,200 of them in 1924.',\n  imageId: '9EAYZrt'\n}, {\n  id: 2,\n  name: 'Macromural de Pachuca, Mexico',\n  description: 'One of the largest murals in the world covering homes in a hillside neighborhood.',\n  imageId: 'DgXHVwu'\n}, {\n  id: 3,\n  name: 'Selarón Staircase in Rio de Janeiro, Brazil',\n  description: 'This landmark was created by Jorge Selarón, a Chilean-born artist, as a \"tribute to the Brazilian people\".',\n  imageId: 'aeO3rpI'\n}, {\n  id: 4,\n  name: 'Burano, Italy',\n  description: 'The houses are painted following a specific color system dating back to 16th century.',\n  imageId: 'kxsph5C'\n}, {\n  id: 5,\n  name: 'Chefchaouen, Marocco',\n  description: 'There are a few theories on why the houses are painted blue, including that the color repels mosquitos or that it symbolizes sky and heaven.',\n  imageId: 'rTqKo46'\n}, {\n  id: 6,\n  name: 'Gamcheon Culture Village in Busan, South Korea',\n  description: 'In 2009, the village was converted into a cultural hub by painting the houses and featuring exhibitions and art installations.',\n  imageId: 'ZfQOOzf'\n}];\n```\n\n```js src/utils.js\nexport function getImageUrl(place) {\n  return (\n    'https://i.imgur.com/' +\n    place.imageId +\n    'l.jpg'\n  );\n}\n```\n\n```css\nul { list-style-type: none; padding: 0px 10px; }\nli {\n  margin-bottom: 10px;\n  display: grid;\n  grid-template-columns: auto 1fr;\n  gap: 20px;\n  align-items: center;\n}\n```\n\n</Sandpack>\n\n어떻게 중간의 컴포넌트가 `imageSize`를 더는 전달할 필요가 없어졌는지 주목하세요.\n\n</Solution>\n\n</Challenges>\n"
  },
  {
    "path": "src/content/learn/passing-props-to-a-component.md",
    "content": "---\ntitle: 컴포넌트에 props 전달하기\n---\n\n<Intro>\n\nReact 컴포넌트는 props를 이용해 서로 통신합니다. 모든 부모 컴포넌트는 props를 줌으로써 몇몇의 정보를 자식 컴포넌트에게 전달할 수 있습니다. props는 HTML 어트리뷰트를 생각나게 할 수도 있지만, 객체, 배열, 함수를 포함한 모든 JavaScript 값을 전달할 수 있습니다.\n\n</Intro>\n\n<YouWillLearn>\n\n* 컴포넌트에 props를 전달하는 방법\n* 컴포넌트에서 props를 읽는 방법\n* props의 기본값을 지정하는 방법\n* 컴포넌트에 JSX를 전달하는 방법\n* 시간에 따라 props가 변하는 방식\n\n</YouWillLearn>\n\n## 친숙한 props {/*familiar-props*/}\n\nprops는 JSX 태그에 전달하는 정보입니다. 예를 들어, `className`, `src`, `alt`, `width`, `height`는 `<img>` 태그에 전달할 수 있습니다.\n\n<Sandpack>\n\n```js\nfunction Avatar() {\n  return (\n    <img\n      className=\"avatar\"\n      src=\"https://i.imgur.com/1bX5QH6.jpg\"\n      alt=\"Lin Lanying\"\n      width={100}\n      height={100}\n    />\n  );\n}\n\nexport default function Profile() {\n  return (\n    <Avatar />\n  );\n}\n```\n\n```css\nbody { min-height: 120px; }\n.avatar { margin: 20px; border-radius: 50%; }\n```\n\n</Sandpack>\n\n`<img>` 태그에 전달할 수 있는 props는 미리 정의되어 있습니다. (ReactDOM은 [HTML 표준](https://www.w3.org/TR/html52/semantics-embedded-content.html#the-img-element)을 준수합니다.) 자신이 생성한 `<Avatar>`와 같은 어떤 컴포넌트든 props를 전달할 수 있습니다. 방법은 다음과 같습니다.\n\n## 컴포넌트에 props 전달하기 {/*passing-props-to-a-component*/}\n\n아래 코드에서 `Profile` 컴포넌트는 자식 컴포넌트인 `Avatar`에 어떠한 props도 전달하지 않습니다.\n\n```js\nexport default function Profile() {\n  return (\n    <Avatar />\n  );\n}\n```\n\n다음 두 단계에 걸쳐 `Avatar`에 props를 전달할 수 있습니다.\n\n### 1단계: 자식 컴포넌트에 props 전달하기 {/*step-1-pass-props-to-the-child-component*/}\n\n먼저, `Avatar`에 몇몇 props를 전달합니다. 예를 들어 `person` (객체)와 `size` (숫자)를 전달해 보겠습니다.\n\n```js\nexport default function Profile() {\n  return (\n    <Avatar\n      person={{ name: 'Lin Lanying', imageId: '1bX5QH6' }}\n      size={100}\n    />\n  );\n}\n```\n\n<Note>\n\n`person=` 뒤에 있는 이중 괄호가 혼란스럽다면, [JSX 중괄호 안의 객체](/learn/javascript-in-jsx-with-curly-braces#using-double-curlies-css-and-other-objects-in-jsx)라고 기억하시면 됩니다.\n\n</Note>\n\n이제 `Avatar` 컴포넌트 내 props를 읽을 수 있습니다.\n\n### 2단계: 자식 컴포넌트 내부에서 props 읽기 {/*step-2-read-props-inside-the-child-component*/}\n\n이러한 props는 `function Avatar` 바로 뒤에 있는 `({`와 `})` 안에 그들의 이름인 `person, size` 등을 쉼표로 구분함으로써 읽을 수 있습니다. 이렇게 하면 `Avatar` 코드 내에서 변수를 사용하는 것처럼 사용할 수 있습니다.\n\n```js\nfunction Avatar({ person, size }) {\n  // person과 size는 이곳에서 사용가능합니다.\n}\n```\n\n`Avatar`에 렌더링을 위해 `person` 과 `size` props를 사용하는 로직을 추가하면 완료됩니다.\n\n이제 `Avatar`를 다른 props를 이용해 다양한 방식으로 렌더링하도록 구성할 수 있습니다. 값을 조정해 보세요!\n\n<Sandpack>\n\n```js src/App.js\nimport { getImageUrl } from './utils.js';\n\nfunction Avatar({ person, size }) {\n  return (\n    <img\n      className=\"avatar\"\n      src={getImageUrl(person)}\n      alt={person.name}\n      width={size}\n      height={size}\n    />\n  );\n}\n\nexport default function Profile() {\n  return (\n    <div>\n      <Avatar\n        size={100}\n        person={{\n          name: 'Katsuko Saruhashi',\n          imageId: 'YfeOqp2'\n        }}\n      />\n      <Avatar\n        size={80}\n        person={{\n          name: 'Aklilu Lemma',\n          imageId: 'OKS67lh'\n        }}\n      />\n      <Avatar\n        size={50}\n        person={{\n          name: 'Lin Lanying',\n          imageId: '1bX5QH6'\n        }}\n      />\n    </div>\n  );\n}\n```\n\n```js src/utils.js\nexport function getImageUrl(person, size = 's') {\n  return (\n    'https://i.imgur.com/' +\n    person.imageId +\n    size +\n    '.jpg'\n  );\n}\n```\n\n```css\nbody { min-height: 120px; }\n.avatar { margin: 10px; border-radius: 50%; }\n```\n\n</Sandpack>\n\nprops를 사용하면 부모 컴포넌트와 자식 컴포넌트를 독립적으로 생각할 수 있습니다. 예를 들어, `Avatar` 가 props를 어떻게 사용하는지 생각할 필요 없이  `Profile`의 `person` 또는 `size` props를 수정할 수 있습니다. 마찬가지로 `Profile`을 보지 않고도 `Avatar`가 props를 사용하는 방식을 바꿀 수 있습니다.\n\nprops는 조절할 수 있는 손잡이라고 생각하면 됩니다. props는 함수의 인수와 동일한 역할을 합니다. 사실 props는 컴포넌트에 대한 유일한 인자입니다! React 컴포넌트 함수는 하나의 인자, 즉 `props` 객체를 받습니다.\n\n```js\nfunction Avatar(props) {\n  let person = props.person;\n  let size = props.size;\n  // ...\n}\n```\n\n보통은 전체 props 자체를 필요로 하지는 않기에, 개별 props로 구조 분해 할당합니다.\n\n<Pitfall>\n\nprops를 선언할 때 `(` 및 `)` 안에  **`{` 및 `}` 쌍을 놓치지 마세요**\n\n```js\nfunction Avatar({ person, size }) {\n  // ...\n}\n```\n\n이 문법을 [\"구조 분해 할당\"](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment#Unpacking_fields_from_objects_passed_as_a_function_parameter)이라고 부르며 함수 매개변수의 속성과 동등합니다.\n\n```js\nfunction Avatar(props) {\n  let person = props.person;\n  let size = props.size;\n  // ...\n}\n```\n\n</Pitfall>\n\n## prop의 기본값 지정하기 {/*specifying-a-default-value-for-a-prop*/}\n\n값이 지정되지 않았을 때, prop에 기본값을 주길 원한다면, 변수 바로 뒤에 `=` 과 함께 기본값을 넣어 구조 분해 할당을 해줄 수 있습니다.\n\n```js\nfunction Avatar({ person, size = 100 }) {\n  // ...\n}\n```\n\n이제 `<Avatar person={...} />`가 `size` prop이 없이 렌더링 된다면, `size`는 `100`으로 설정됩니다.\n\n이 [기본값](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Functions/Default_parameters)은 size prop이 없거나 `size={undefined}`로 전달될 때 사용됩니다. 그러나 `size={null}`  또는 `size={0}`으로 전달된다면, 기본값은 사용되지 **않습니다**.\n\n## JSX spread 문법으로 props 전달하기 {/*forwarding-props-with-the-jsx-spread-syntax*/}\n\n때때로 전달되는 props는 반복적입니다.\n\n```js\nfunction Profile({ person, size, isSepia, thickBorder }) {\n  return (\n    <div className=\"card\">\n      <Avatar\n        person={person}\n        size={size}\n        isSepia={isSepia}\n        thickBorder={thickBorder}\n      />\n    </div>\n  );\n}\n```\n\n반복적인 코드는 가독성을 높일 수 있다는 점에서 잘못된 것은 아닙니다. 하지만 때로는 간결함이 중요할 때도 있습니다. `Profile`이 `Avatar`에서 하는 것처럼, 일부 컴포넌트는 그들의 모든 props를 자식 컴포넌트에 전달합니다.\nprops를 직접 사용하지 않기 때문에 보다 간결한 \"spread\" 문법을 사용하는 것이 합리적일 수 있습니다.\n\n\n```js\nfunction Profile(props) {\n  return (\n    <div className=\"card\">\n      <Avatar {...props} />\n    </div>\n  );\n}\n```\n\n이렇게 하면 `Profile`의 모든 props를 각각의 이름을 나열하지 않고 `Avatar`로 전달합니다.\n\n**spread 문법은 제한적으로 사용하세요**. 다른 모든 컴포넌트에 이 구문을 사용한다면 문제가 있는 것입니다. 이는 종종 컴포넌트들을 분할하여 자식을 JSX로 전달해야 함을 나타냅니다. 더 자세히 알아봅시다!\n\n## 자식을 JSX로 전달하기 {/*passing-jsx-as-children*/}\n\n내장된 브라우저 태그는 중첩하는 것이 일반적입니다.\n\n```js\n<div>\n  <img />\n</div>\n```\n\n때로는 같은 방식으로 자체 컴포넌트를 중첩하고 싶을 때가 있습니다.\n\n\n```js\n<Card>\n  <Avatar />\n</Card>\n```\n\nJSX 태그 내에 콘텐츠를 중첩하면, 부모 컴포넌트는 해당 콘텐츠를 `children`이라는 prop으로 받을 것입니다. 예를 들어, 아래의 `Card` 컴포넌트는 `<Avatar />`로 설정된 `children` prop을 받아 이를 래퍼 div에 렌더링 할 것입니다.\n\n<Sandpack>\n\n```js src/App.js\nimport Avatar from './Avatar.js';\n\nfunction Card({ children }) {\n  return (\n    <div className=\"card\">\n      {children}\n    </div>\n  );\n}\n\nexport default function Profile() {\n  return (\n    <Card>\n      <Avatar\n        size={100}\n        person={{\n          name: 'Katsuko Saruhashi',\n          imageId: 'YfeOqp2'\n        }}\n      />\n    </Card>\n  );\n}\n```\n\n```js src/Avatar.js\nimport { getImageUrl } from './utils.js';\n\nexport default function Avatar({ person, size }) {\n  return (\n    <img\n      className=\"avatar\"\n      src={getImageUrl(person)}\n      alt={person.name}\n      width={size}\n      height={size}\n    />\n  );\n}\n```\n\n```js src/utils.js\nexport function getImageUrl(person, size = 's') {\n  return (\n    'https://i.imgur.com/' +\n    person.imageId +\n    size +\n    '.jpg'\n  );\n}\n```\n\n```css\n.card {\n  width: fit-content;\n  margin: 5px;\n  padding: 5px;\n  font-size: 20px;\n  text-align: center;\n  border: 1px solid #aaa;\n  border-radius: 20px;\n  background: #fff;\n}\n.avatar {\n  margin: 20px;\n  border-radius: 50%;\n}\n```\n\n</Sandpack>\n\n`<Card>` 내부의 `<Avatar>`를 텍스트로 바꾸어 `<Card>` 컴포넌트가 중첩된 콘텐츠를 어떻게 감싸는지 확인해 보세요. 그 내부에서 무엇이 렌더링 되는지 \"알\" 필요는 없습니다. 이 유연한 패턴은 많은 곳에서 볼 수 있습니다.\n\n`children` prop을 가지고 있는 컴포넌트는 부모 컴포넌트가 임의의 JSX로 \"채울\" 수 있는 \"구멍\"이 있는 것으로 생각할 수 있습니다. 패널, 그리드 등의 시각적 래퍼에 종종 `children` prop를 사용합니다.\n\n<Illustration src=\"/images/docs/illustrations/i_children-prop.png\" alt='A puzzle-like Card tile with a slot for \"children\" pieces like text and Avatar' />\n\n## 시간에 따라 props가 변하는 방식 {/*how-props-change-over-time*/}\n\n아래의 `Clock` 컴포넌트는 부모 컴포넌트로부터 `color`와 `time`이라는 두 가지 props를 받습니다. (부모 컴포넌트의 코드는 아직 자세히 다루지 않을 [state](/learn/state-a-components-memory)를 사용하기 때문에 생략합니다.)\n\n\n아래 select box의 색상을 바꿔보세요.\n\n<Sandpack>\n\n```js src/Clock.js active\nexport default function Clock({ color, time }) {\n  return (\n    <h1 style={{ color: color }}>\n      {time}\n    </h1>\n  );\n}\n```\n\n```js src/App.js hidden\nimport { useState, useEffect } from 'react';\nimport Clock from './Clock.js';\n\nfunction useTime() {\n  const [time, setTime] = useState(() => new Date());\n  useEffect(() => {\n    const id = setInterval(() => {\n      setTime(new Date());\n    }, 1000);\n    return () => clearInterval(id);\n  }, []);\n  return time;\n}\n\nexport default function App() {\n  const time = useTime();\n  const [color, setColor] = useState('lightcoral');\n  return (\n    <div>\n      <p>\n        Pick a color:{' '}\n        <select value={color} onChange={e => setColor(e.target.value)}>\n          <option value=\"lightcoral\">lightcoral</option>\n          <option value=\"midnightblue\">midnightblue</option>\n          <option value=\"rebeccapurple\">rebeccapurple</option>\n        </select>\n      </p>\n      <Clock color={color} time={time.toLocaleTimeString()} />\n    </div>\n  );\n}\n```\n\n</Sandpack>\n\n이 예시는 **컴포넌트가 시간에 따라 다른 props를 받을 수 있음을 보여줍니다.** Props는 항상 고정되어 있지 않습니다! 여기서 `time` prop은 매초 변경되고, `color` prop은 다른 색상을 선택하면 변경됩니다. Props는 컴포넌트의 데이터를 처음에만 반영하는 것이 아니라 모든 시점에 반영합니다.\n\n그러나 props는 컴퓨터 과학에서 \"변경할 수 없다\"라는 의미의 [불변성](https://en.wikipedia.org/wiki/Immutable_object)을 가지고 있습니다. 컴포넌트가 props를 변경해야 하는 경우(예: 사용자의 상호작용이나 새로운 데이터에 대한 응답), 부모 컴포넌트에 *다른 props*, 즉 새로운 객체를 전달하도록 \"요청\"해야 합니다! 그러면 이전의 props는 버려지고, 결국 자바스크립트 엔진은 기존 props가 차지했던 메모리를 회수하게 됩니다.\n\n\n**\"props 변경\"을 시도하지 마세요.** 선택한 색을 변경하는 등 사용자 입력에 반응해야 하는 경우에는 [State: 컴포넌트의 메모리](/learn/state-a-components-memory)에서 배울 \"set state\"가 필요할 것입니다.\n\n<Recap>\n\n* Props를 전달하려면 HTML 어트리뷰트를 사용할 때와 마찬가지로 JSX에 props를 추가합니다.\n* Props를 읽으려면 `function Avatar({ person, size })` 구조 분해 할당 문법을 사용합니다.\n* `size = 100` 과 같은 기본값을 지정할 수 있으며, 이는 누락되거나 `undefined` 인 props에 사용됩니다.\n* 모든 props를 `<Avatar {...props} />`로 전달할 수 있습니다. JSX spread 문법을 사용할 수 있지만 과도하게 사용하지 마세요!\n* `<Card><Avatar /></Card>`와 같이 중첩된 JSX는 `Card`컴포넌트의 자식 컴포넌트로 나타납니다.\n* Props는 읽기 전용 스냅샷으로, 렌더링 할 때마다 새로운 버전의 props를 받습니다.\n* Props는 변경할 수 없습니다. 상호작용이 필요한 경우 state를 설정해야 합니다.\n\n</Recap>\n\n\n\n<Challenges>\n\n#### 컴포넌트 추출하기 {/*extract-a-component*/}\n\n이  `Gallery` 컴포넌트에는 두 가지 프로필에 대한 몇 가지 비슷한 마크업이 포함되어 있습니다. 중복을 줄이기 위해 `Profile` 컴포넌트를 추출해 보세요. 어떤 props를 전달할지 골라야 할 수 있습니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { getImageUrl } from './utils.js';\n\nexport default function Gallery() {\n  return (\n    <div>\n      <h1>Notable Scientists</h1>\n      <section className=\"profile\">\n        <h2>Maria Skłodowska-Curie</h2>\n        <img\n          className=\"avatar\"\n          src={getImageUrl('szV5sdG')}\n          alt=\"Maria Skłodowska-Curie\"\n          width={70}\n          height={70}\n        />\n        <ul>\n          <li>\n            <b>Profession: </b>\n            physicist and chemist\n          </li>\n          <li>\n            <b>Awards: 4 </b>\n            (Nobel Prize in Physics, Nobel Prize in Chemistry, Davy Medal, Matteucci Medal)\n          </li>\n          <li>\n            <b>Discovered: </b>\n            polonium (chemical element)\n          </li>\n        </ul>\n      </section>\n      <section className=\"profile\">\n        <h2>Katsuko Saruhashi</h2>\n        <img\n          className=\"avatar\"\n          src={getImageUrl('YfeOqp2')}\n          alt=\"Katsuko Saruhashi\"\n          width={70}\n          height={70}\n        />\n        <ul>\n          <li>\n            <b>Profession: </b>\n            geochemist\n          </li>\n          <li>\n            <b>Awards: 2 </b>\n            (Miyake Prize for geochemistry, Tanaka Prize)\n          </li>\n          <li>\n            <b>Discovered: </b>\n            a method for measuring carbon dioxide in seawater\n          </li>\n        </ul>\n      </section>\n    </div>\n  );\n}\n```\n\n```js src/utils.js\nexport function getImageUrl(imageId, size = 's') {\n  return (\n    'https://i.imgur.com/' +\n    imageId +\n    size +\n    '.jpg'\n  );\n}\n```\n\n```css\n.avatar { margin: 5px; border-radius: 50%; min-height: 70px; }\n.profile {\n  border: 1px solid #aaa;\n  border-radius: 6px;\n  margin-top: 20px;\n  padding: 10px;\n}\nh1, h2 { margin: 5px; }\nh1 { margin-bottom: 10px; }\nul { padding: 0px 10px 0px 20px; }\nli { margin: 5px; }\n```\n\n</Sandpack>\n\n<Hint>\n\n과학자 중 한 명에 대한 마크업을 추출하는 것으로 시작하세요. 그런 다음 두 번째 예시에서 일치하지 않는 부분을 찾아 props로 구성할 수 있게 만듭니다.\n\n</Hint>\n\n<Solution>\n\n이 솔루션에서 `Profile` 컴포넌트는 `imageId` (문자열), `name` (문자열), `profession` (문자열), `awards` (문자열 배열), `discovery` (문자열), `imageSize` (숫자) 등 여러 props를 허용합니다.\n\n`imageSize` prop에는 기본값이 있으므로, 컴포넌트에 전달하지 않았습니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { getImageUrl } from './utils.js';\n\nfunction Profile({\n  imageId,\n  name,\n  profession,\n  awards,\n  discovery,\n  imageSize = 70\n}) {\n  return (\n    <section className=\"profile\">\n      <h2>{name}</h2>\n      <img\n        className=\"avatar\"\n        src={getImageUrl(imageId)}\n        alt={name}\n        width={imageSize}\n        height={imageSize}\n      />\n      <ul>\n        <li><b>Profession:</b> {profession}</li>\n        <li>\n          <b>Awards: {awards.length} </b>\n          ({awards.join(', ')})\n        </li>\n        <li>\n          <b>Discovered: </b>\n          {discovery}\n        </li>\n      </ul>\n    </section>\n  );\n}\n\nexport default function Gallery() {\n  return (\n    <div>\n      <h1>Notable Scientists</h1>\n      <Profile\n        imageId=\"szV5sdG\"\n        name=\"Maria Skłodowska-Curie\"\n        profession=\"physicist and chemist\"\n        discovery=\"polonium (chemical element)\"\n        awards={[\n          'Nobel Prize in Physics',\n          'Nobel Prize in Chemistry',\n          'Davy Medal',\n          'Matteucci Medal'\n        ]}\n      />\n      <Profile\n        imageId='YfeOqp2'\n        name='Katsuko Saruhashi'\n        profession='geochemist'\n        discovery=\"a method for measuring carbon dioxide in seawater\"\n        awards={[\n          'Miyake Prize for geochemistry',\n          'Tanaka Prize'\n        ]}\n      />\n    </div>\n  );\n}\n```\n\n```js src/utils.js\nexport function getImageUrl(imageId, size = 's') {\n  return (\n    'https://i.imgur.com/' +\n    imageId +\n    size +\n    '.jpg'\n  );\n}\n```\n\n```css\n.avatar { margin: 5px; border-radius: 50%; min-height: 70px; }\n.profile {\n  border: 1px solid #aaa;\n  border-radius: 6px;\n  margin-top: 20px;\n  padding: 10px;\n}\nh1, h2 { margin: 5px; }\nh1 { margin-bottom: 10px; }\nul { padding: 0px 10px 0px 20px; }\nli { margin: 5px; }\n```\n\n</Sandpack>\n\n`awards`가 배열인 경우 별도의 `awardCount` prop이 필요하지 않다는 점에 주의하세요. `awards.length`를 사용하여 awards 개수를 파악할 수 있습니다. props에는 어떤 값도 사용할 수 있으며, 배열도 포함된다는 점을 기억하세요!\n\n이 페이지의 앞선 예시와 더 유사한 또 다른 해결책은, 사람에 대한 모든 정보를 하나의 객체로 그룹화하고, 해당 객체를 하나의 prop으로 전달하는 것입니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { getImageUrl } from './utils.js';\n\nfunction Profile({ person, imageSize = 70 }) {\n  const imageSrc = getImageUrl(person)\n\n  return (\n    <section className=\"profile\">\n      <h2>{person.name}</h2>\n      <img\n        className=\"avatar\"\n        src={imageSrc}\n        alt={person.name}\n        width={imageSize}\n        height={imageSize}\n      />\n      <ul>\n        <li>\n          <b>Profession:</b> {person.profession}\n        </li>\n        <li>\n          <b>Awards: {person.awards.length} </b>\n          ({person.awards.join(', ')})\n        </li>\n        <li>\n          <b>Discovered: </b>\n          {person.discovery}\n        </li>\n      </ul>\n    </section>\n  )\n}\n\nexport default function Gallery() {\n  return (\n    <div>\n      <h1>Notable Scientists</h1>\n      <Profile person={{\n        imageId: 'szV5sdG',\n        name: 'Maria Skłodowska-Curie',\n        profession: 'physicist and chemist',\n        discovery: 'polonium (chemical element)',\n        awards: [\n          'Nobel Prize in Physics',\n          'Nobel Prize in Chemistry',\n          'Davy Medal',\n          'Matteucci Medal'\n        ],\n      }} />\n      <Profile person={{\n        imageId: 'YfeOqp2',\n        name: 'Katsuko Saruhashi',\n        profession: 'geochemist',\n        discovery: 'a method for measuring carbon dioxide in seawater',\n        awards: [\n          'Miyake Prize for geochemistry',\n          'Tanaka Prize'\n        ],\n      }} />\n    </div>\n  );\n}\n```\n\n```js src/utils.js\nexport function getImageUrl(person, size = 's') {\n  return (\n    'https://i.imgur.com/' +\n    person.imageId +\n    size +\n    '.jpg'\n  );\n}\n```\n\n```css\n.avatar { margin: 5px; border-radius: 50%; min-height: 70px; }\n.profile {\n  border: 1px solid #aaa;\n  border-radius: 6px;\n  margin-top: 20px;\n  padding: 10px;\n}\nh1, h2 { margin: 5px; }\nh1 { margin-bottom: 10px; }\nul { padding: 0px 10px 0px 20px; }\nli { margin: 5px; }\n```\n\n</Sandpack>\n\nJSX 어트리뷰트의 컬렉션이 아닌 JavaScript 객체의 속성으로 구성되어 있어서 문법이 약간 달라 보이지만, 이 예시는 대부분 동일하며 두 가지 접근 방식 중 어느 쪽을 선택해도 괜찮습니다.\n\n</Solution>\n\n#### prop에 따라 이미지 크기 조정하기 {/*adjust-the-image-size-based-on-a-prop*/}\n\n이번 예시에서는 `Avatar` 가 `<img>`의 넓이와 높이를 결정하는 숫자 `size` prop를 받습니다. `size` prop은 `40`으로 설정되어 있습니다. 그러나 새 탭에서 이미지를 열면, 이미지가 `160픽셀`로 커져 있을 것입니다. 실제 이미지 크기는 요청하는 썸네일 크기에 따라 결정됩니다.\n\n`size` prop에 따라 가장 가까운 이미지 크기를 요청하도록 `Avatar` 컴포넌트를 변경하세요. 특히 `size` 가 `90`보다 작으면 `'s'`(\"small\")을, 아니면 `'b'`(\"big\")을 `getImageUrl` 함수에 전달하세요. `size` prop를 다른 값들을 전달해 보고, 아바타를 렌더링 하는지, 새 탭에서 이미지를 열어 변경사항이 제대로 반영되는지 확인해 보세요.\n<Sandpack>\n\n```js src/App.js\nimport { getImageUrl } from './utils.js';\n\nfunction Avatar({ person, size }) {\n  return (\n    <img\n      className=\"avatar\"\n      src={getImageUrl(person, 'b')}\n      alt={person.name}\n      width={size}\n      height={size}\n    />\n  );\n}\n\nexport default function Profile() {\n  return (\n    <Avatar\n      size={40}\n      person={{\n        name: 'Gregorio Y. Zara',\n        imageId: '7vQD0fP'\n      }}\n    />\n  );\n}\n```\n\n```js src/utils.js\nexport function getImageUrl(person, size) {\n  return (\n    'https://i.imgur.com/' +\n    person.imageId +\n    size +\n    '.jpg'\n  );\n}\n```\n\n```css\n.avatar { margin: 20px; border-radius: 50%; }\n```\n\n</Sandpack>\n\n<Solution>\n\n방법은 다음과 같습니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { getImageUrl } from './utils.js';\n\nfunction Avatar({ person, size }) {\n  let thumbnailSize = 's';\n  if (size > 90) {\n    thumbnailSize = 'b';\n  }\n  return (\n    <img\n      className=\"avatar\"\n      src={getImageUrl(person, thumbnailSize)}\n      alt={person.name}\n      width={size}\n      height={size}\n    />\n  );\n}\n\nexport default function Profile() {\n  return (\n    <>\n      <Avatar\n        size={40}\n        person={{\n          name: 'Gregorio Y. Zara',\n          imageId: '7vQD0fP'\n        }}\n      />\n      <Avatar\n        size={120}\n        person={{\n          name: 'Gregorio Y. Zara',\n          imageId: '7vQD0fP'\n        }}\n      />\n    </>\n  );\n}\n```\n\n```js src/utils.js\nexport function getImageUrl(person, size) {\n  return (\n    'https://i.imgur.com/' +\n    person.imageId +\n    size +\n    '.jpg'\n  );\n}\n```\n\n```css\n.avatar { margin: 20px; border-radius: 50%; }\n```\n\n</Sandpack>\n\n또한 [`window.devicePixelRatio`](https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio)를 고려하여 높은 DPI 화면에서 더 선명한 이미지를 표시할 수도 있습니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { getImageUrl } from './utils.js';\n\nconst ratio = window.devicePixelRatio;\n\nfunction Avatar({ person, size }) {\n  let thumbnailSize = 's';\n  if (size * ratio > 90) {\n    thumbnailSize = 'b';\n  }\n  return (\n    <img\n      className=\"avatar\"\n      src={getImageUrl(person, thumbnailSize)}\n      alt={person.name}\n      width={size}\n      height={size}\n    />\n  );\n}\n\nexport default function Profile() {\n  return (\n    <>\n      <Avatar\n        size={40}\n        person={{\n          name: 'Gregorio Y. Zara',\n          imageId: '7vQD0fP'\n        }}\n      />\n      <Avatar\n        size={70}\n        person={{\n          name: 'Gregorio Y. Zara',\n          imageId: '7vQD0fP'\n        }}\n      />\n      <Avatar\n        size={120}\n        person={{\n          name: 'Gregorio Y. Zara',\n          imageId: '7vQD0fP'\n        }}\n      />\n    </>\n  );\n}\n```\n\n```js src/utils.js\nexport function getImageUrl(person, size) {\n  return (\n    'https://i.imgur.com/' +\n    person.imageId +\n    size +\n    '.jpg'\n  );\n}\n```\n\n```css\n.avatar { margin: 20px; border-radius: 50%; }\n```\n\n</Sandpack>\n\nprops를 사용하면 `<Avatar>` 컴포넌트 내부에 이와 같은 로직을 캡슐화할 수 있으므로(필요하면 나중에 변경할 수 있습니다), 누구나 이미지가 요청되고 크기가 조정되는 방식에 대해 생각하지 않고 `<Avatar>` 컴포넌트를 사용할 수 있습니다.\n\n</Solution>\n\n#### `children` prop에 JSX 전달하기 {/*passing-jsx-in-a-children-prop*/}\n\n아래 마크업에서 `Card` 컴포넌트를 추출하고, `children` prop을 사용하여 다른 JSX를 전달하세요.\n\n<Sandpack>\n\n```js\nexport default function Profile() {\n  return (\n    <div>\n      <div className=\"card\">\n        <div className=\"card-content\">\n          <h1>Photo</h1>\n          <img\n            className=\"avatar\"\n            src=\"https://i.imgur.com/OKS67lhm.jpg\"\n            alt=\"Aklilu Lemma\"\n            width={70}\n            height={70}\n          />\n        </div>\n      </div>\n      <div className=\"card\">\n        <div className=\"card-content\">\n          <h1>About</h1>\n          <p>Aklilu Lemma was a distinguished Ethiopian scientist who discovered a natural treatment to schistosomiasis.</p>\n        </div>\n      </div>\n    </div>\n  );\n}\n```\n\n```css\n.card {\n  width: fit-content;\n  margin: 20px;\n  padding: 20px;\n  border: 1px solid #aaa;\n  border-radius: 20px;\n  background: #fff;\n}\n.card-content {\n  text-align: center;\n}\n.avatar {\n  margin: 10px;\n  border-radius: 50%;\n}\nh1 {\n  margin: 5px;\n  padding: 0;\n  font-size: 24px;\n}\n```\n\n</Sandpack>\n\n<Hint>\n\n컴포넌트의 태그 안에 넣는 모든 JSX는 해당 컴포넌트에 `children` prop로 전달됩니다.\n\n</Hint>\n\n<Solution>\n\n두 곳에서 모두 `Card` 컴포넌트를 사용할 수 있는 방법입니다.\n\n<Sandpack>\n\n```js\nfunction Card({ children }) {\n  return (\n    <div className=\"card\">\n      <div className=\"card-content\">\n        {children}\n      </div>\n    </div>\n  );\n}\n\nexport default function Profile() {\n  return (\n    <div>\n      <Card>\n        <h1>Photo</h1>\n        <img\n          className=\"avatar\"\n          src=\"https://i.imgur.com/OKS67lhm.jpg\"\n          alt=\"Aklilu Lemma\"\n          width={100}\n          height={100}\n        />\n      </Card>\n      <Card>\n        <h1>About</h1>\n        <p>Aklilu Lemma was a distinguished Ethiopian scientist who discovered a natural treatment to schistosomiasis.</p>\n      </Card>\n    </div>\n  );\n}\n```\n\n```css\n.card {\n  width: fit-content;\n  margin: 20px;\n  padding: 20px;\n  border: 1px solid #aaa;\n  border-radius: 20px;\n  background: #fff;\n}\n.card-content {\n  text-align: center;\n}\n.avatar {\n  margin: 10px;\n  border-radius: 50%;\n}\nh1 {\n  margin: 5px;\n  padding: 0;\n  font-size: 24px;\n}\n```\n\n</Sandpack>\n\n모든 `Card`에 항상 제목을 붙이고 싶다면 `title`을 별도의 prop으로 만들 수도 있습니다.\n\n<Sandpack>\n\n```js\nfunction Card({ children, title }) {\n  return (\n    <div className=\"card\">\n      <div className=\"card-content\">\n        <h1>{title}</h1>\n        {children}\n      </div>\n    </div>\n  );\n}\n\nexport default function Profile() {\n  return (\n    <div>\n      <Card title=\"Photo\">\n        <img\n          className=\"avatar\"\n          src=\"https://i.imgur.com/OKS67lhm.jpg\"\n          alt=\"Aklilu Lemma\"\n          width={100}\n          height={100}\n        />\n      </Card>\n      <Card title=\"About\">\n        <p>Aklilu Lemma was a distinguished Ethiopian scientist who discovered a natural treatment to schistosomiasis.</p>\n      </Card>\n    </div>\n  );\n}\n```\n\n```css\n.card {\n  width: fit-content;\n  margin: 20px;\n  padding: 20px;\n  border: 1px solid #aaa;\n  border-radius: 20px;\n  background: #fff;\n}\n.card-content {\n  text-align: center;\n}\n.avatar {\n  margin: 10px;\n  border-radius: 50%;\n}\nh1 {\n  margin: 5px;\n  padding: 0;\n  font-size: 24px;\n}\n```\n\n</Sandpack>\n\n</Solution>\n\n</Challenges>\n"
  },
  {
    "path": "src/content/learn/preserving-and-resetting-state.md",
    "content": "---\ntitle: State를 보존하고 초기화하기\n---\n\n<Intro>\n\n각 컴포넌트는 독립된 state를 가집니다. React는 UI 트리에서의 위치를 통해 각 state가 어떤 컴포넌트에 속하는지 추적합니다. 리렌더링마다 언제 state를 보존하고 또 state를 초기화할지 컨트롤할 수 있습니다.\n\n</Intro>\n\n<YouWillLearn>\n\n* React가 언제 state를 보존하고 언제 초기화하는지\n* 어떻게 React가 컴포넌트의 state를 초기화하도록 강제할 수 있는지\n* key와 타입이 state 보존에 어떻게 영향을 주는지\n\n</YouWillLearn>\n\n## State 는 렌더트리의 위치에 연결 됩니다. {/*state-is-tied-to-a-position-in-the-tree*/}\n\nReact 는 UI 안에 있는 컴포넌트 구조로 [렌더 트리](learn/understanding-your-ui-as-a-tree#the-render-tree)를 만듭니다.\n\n컴포넌트에 state를 줄 때 state가 컴포넌트 안에 \"살고\" 있다고 생각할 수도 있습니다. 하지만 사실 state는 React 안에 있습니다. React는 컴포넌트가 UI 트리에 있는 위치를 이용해 React가 가지고 있는 각 state를 알맞은 컴포넌트와 연결합니다.\n\n여기 동일한 `<Counter />` JSX 태그가 다른 두 군데에서 렌더링되고 있습니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function App() {\n  const counter = <Counter />;\n  return (\n    <div>\n      {counter}\n      {counter}\n    </div>\n  );\n}\n\nfunction Counter() {\n  const [score, setScore] = useState(0);\n  const [hover, setHover] = useState(false);\n\n  let className = 'counter';\n  if (hover) {\n    className += ' hover';\n  }\n\n  return (\n    <div\n      className={className}\n      onPointerEnter={() => setHover(true)}\n      onPointerLeave={() => setHover(false)}\n    >\n      <h1>{score}</h1>\n      <button onClick={() => setScore(score + 1)}>\n        Add one\n      </button>\n    </div>\n  );\n}\n```\n\n```css\nlabel {\n  display: block;\n  clear: both;\n}\n\n.counter {\n  width: 100px;\n  text-align: center;\n  border: 1px solid gray;\n  border-radius: 4px;\n  padding: 20px;\n  margin: 0 20px 20px 0;\n  float: left;\n}\n\n.hover {\n  background: #ffffd8;\n}\n```\n\n</Sandpack>\n\n카운터가 다음과 같이 트리 구조로 보입니다.\n\n<DiagramGroup>\n\n<Diagram name=\"preserving_state_tree\" height={248} width={395} alt=\"Diagram of a tree of React components. The root node is labeled 'div' and has two children. Each of the children are labeled 'Counter' and both contain a state bubble labeled 'count' with value 0.\">\n\nReact tree\n\n</Diagram>\n\n</DiagramGroup>\n\n**이 둘은 각각 트리에서 자기 고유의 위치에 렌더링되어 있으므로 분리되어있는 카운터입니다.** 일반적으로 React를 사용할 때 위치에 대해 생각할 필요는 없지만 React가 어떻게 작동하는지 이해할 때 유용합니다.\n\nReact에서 화면의 각 컴포넌트는 완전히 분리된 state를 가집니다. 예를 들어, 두 `Counter` 컴포넌트를 나란히 렌더링하면 그들은 각각 자신만의 독립된 `score`과 `hover` state를 가지게 됩니다.\n\n두 카운터를 클릭해보고 서로 영향을 끼치지 않는 것을 확인해보세요.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function App() {\n  return (\n    <div>\n      <Counter />\n      <Counter />\n    </div>\n  );\n}\n\nfunction Counter() {\n  const [score, setScore] = useState(0);\n  const [hover, setHover] = useState(false);\n\n  let className = 'counter';\n  if (hover) {\n    className += ' hover';\n  }\n\n  return (\n    <div\n      className={className}\n      onPointerEnter={() => setHover(true)}\n      onPointerLeave={() => setHover(false)}\n    >\n      <h1>{score}</h1>\n      <button onClick={() => setScore(score + 1)}>\n        Add one\n      </button>\n    </div>\n  );\n}\n```\n\n```css\n.counter {\n  width: 100px;\n  text-align: center;\n  border: 1px solid gray;\n  border-radius: 4px;\n  padding: 20px;\n  margin: 0 20px 20px 0;\n  float: left;\n}\n\n.hover {\n  background: #ffffd8;\n}\n```\n\n</Sandpack>\n\n특정 카운터가 갱신되면, 해당 컴포넌트의 상태만 갱신되는 것을 확인할 수 있습니다.\n\n\n<DiagramGroup>\n\n<Diagram name=\"preserving_state_increment\" height={248} width={441} alt=\"Diagram of a tree of React components. The root node is labeled 'div' and has two children. The left child is labeled 'Counter' and contains a state bubble labeled 'count' with value 0. The right child is labeled 'Counter' and contains a state bubble labeled 'count' with value 1. The state bubble of the right child is highlighted in yellow to indicate its value has updated.\">\n\nUpdating state\n\n</Diagram>\n\n</DiagramGroup>\n\nReact는 트리의 동일한 컴포넌트를 동일한 위치에 렌더링하는 동안 상태를 유지합니다. 이를 확인하려면, 두 Counter를 모두 증가시키고, \"Render the second counter\" 체크박스의 체크를 해제하여 두 번째 컴포넌트를 제거해보세요. 그리고, 다시 체크박스를 눌러 추가해보세요.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function App() {\n  const [showB, setShowB] = useState(true);\n  return (\n    <div>\n      <Counter />\n      {showB && <Counter />}\n      <label>\n        <input\n          type=\"checkbox\"\n          checked={showB}\n          onChange={e => {\n            setShowB(e.target.checked)\n          }}\n        />\n        Render the second counter\n      </label>\n    </div>\n  );\n}\n\nfunction Counter() {\n  const [score, setScore] = useState(0);\n  const [hover, setHover] = useState(false);\n\n  let className = 'counter';\n  if (hover) {\n    className += ' hover';\n  }\n\n  return (\n    <div\n      className={className}\n      onPointerEnter={() => setHover(true)}\n      onPointerLeave={() => setHover(false)}\n    >\n      <h1>{score}</h1>\n      <button onClick={() => setScore(score + 1)}>\n        Add one\n      </button>\n    </div>\n  );\n}\n```\n\n```css\nlabel {\n  display: block;\n  clear: both;\n}\n\n.counter {\n  width: 100px;\n  text-align: center;\n  border: 1px solid gray;\n  border-radius: 4px;\n  padding: 20px;\n  margin: 0 20px 20px 0;\n  float: left;\n}\n\n.hover {\n  background: #ffffd8;\n}\n```\n\n</Sandpack>\n\n두 번째 카운터를 렌더링하지 않을 때 그 state가 완전히 사라지는 것을 확인해보세요. 이는 React가 컴포넌트를 제거할 때 그 state도 같이 제거하기 때문입니다.\n\n<DiagramGroup>\n\n<Diagram name=\"preserving_state_remove_component\" height={253} width={422} alt=\"Diagram of a tree of React components. The root node is labeled 'div' and has two children. The left child is labeled 'Counter' and contains a state bubble labeled 'count' with value 0. The right child is missing, and in its place is a yellow 'poof' image, highlighting the component being deleted from the tree.\">\n\nDeleting a component\n\n</Diagram>\n\n</DiagramGroup>\n\n\"Render the second counter\"를 누를 때 두 번째 `Counter`와 그 state는 처음부터 초기화되고(`score = 0`) DOM에 추가됩니다.\n\n<DiagramGroup>\n\n<Diagram name=\"preserving_state_add_component\" height={258} width={500} alt=\"Diagram of a tree of React components. The root node is labeled 'div' and has two children. The left child is labeled 'Counter' and contains a state bubble labeled 'count' with value 0. The right child is labeled 'Counter' and contains a state bubble labeled 'count' with value 0. The entire right child node is highlighted in yellow, indicating that it was just added to the tree.\">\n\nAdding a component\n\n</Diagram>\n\n</DiagramGroup>\n\n**React는 컴포넌트가 UI 트리에서 그 자리에 렌더링되는 한 state를 유지합니다.** 만약 그것을 제거하거나 같은 자리에 다른 컴포넌트가 렌더링되면 React는 그 state를 버립니다.\n\n## 같은 자리의 같은 컴포넌트는 state를 보존합니다 {/*same-component-at-the-same-position-preserves-state*/}\n\n다음 예시에는 서로 다른 두 `<Counter />` 태그가 있습니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function App() {\n  const [isFancy, setIsFancy] = useState(false);\n  return (\n    <div>\n      {isFancy ? (\n        <Counter isFancy={true} />\n      ) : (\n        <Counter isFancy={false} />\n      )}\n      <label>\n        <input\n          type=\"checkbox\"\n          checked={isFancy}\n          onChange={e => {\n            setIsFancy(e.target.checked)\n          }}\n        />\n        Use fancy styling\n      </label>\n    </div>\n  );\n}\n\nfunction Counter({ isFancy }) {\n  const [score, setScore] = useState(0);\n  const [hover, setHover] = useState(false);\n\n  let className = 'counter';\n  if (hover) {\n    className += ' hover';\n  }\n  if (isFancy) {\n    className += ' fancy';\n  }\n\n  return (\n    <div\n      className={className}\n      onPointerEnter={() => setHover(true)}\n      onPointerLeave={() => setHover(false)}\n    >\n      <h1>{score}</h1>\n      <button onClick={() => setScore(score + 1)}>\n        Add one\n      </button>\n    </div>\n  );\n}\n```\n\n```css\nlabel {\n  display: block;\n  clear: both;\n}\n\n.counter {\n  width: 100px;\n  text-align: center;\n  border: 1px solid gray;\n  border-radius: 4px;\n  padding: 20px;\n  margin: 0 20px 20px 0;\n  float: left;\n}\n\n.fancy {\n  border: 5px solid gold;\n  color: #ff6767;\n}\n\n.hover {\n  background: #ffffd8;\n}\n```\n\n</Sandpack>\n\n체크 박스를 선택하거나 선택 해제할 때 카운터 state는 초기화되지 않습니다. `isFancy`가 `true`이든 `false`이든 `<Counter />`는 같은 자리에 있습니다. root `App` 컴포넌트가 반환한 `div`의 첫 번째 자식으로 있죠.\n\n<DiagramGroup>\n\n<Diagram name=\"preserving_state_same_component\" height={461} width={600} alt=\"Diagram with two sections separated by an arrow transitioning between them. Each section contains a layout of components with a parent labeled 'App' containing a state bubble labeled isFancy. This component has one child labeled 'div', which leads to a prop bubble containing isFancy (highlighted in purple) passed down to the only child. The last child is labeled 'Counter' and contains a state bubble with label 'count' and value 3 in both diagrams. In the left section of the diagram, nothing is highlighted and the isFancy parent state value is false. In the right section of the diagram, the isFancy parent state value has changed to true and it is highlighted in yellow, and so is the props bubble below, which has also changed its isFancy value to true.\">\n\n`Counter`는 같은 자리에 있기 때문에 `App` 상태의 갱신은 `Counter`를 초기화시키지 않습니다\n\n</Diagram>\n\n</DiagramGroup>\n\n\n첫 번째 자리의 같은 컴포넌트이기 때문에 React의 관점에서는 같은 카운터입니다.\n\n<Pitfall>\n\n**React는 JSX 마크업에서가 아닌 UI 트리에서의 위치에 관심이 있다는 것을** 기억하세요! 이 컴포넌트는 `if` 안과 밖에 다른 `<Counter />`를 가진 `return` 문을 두 개 가지고 있습니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function App() {\n  const [isFancy, setIsFancy] = useState(false);\n  if (isFancy) {\n    return (\n      <div>\n        <Counter isFancy={true} />\n        <label>\n          <input\n            type=\"checkbox\"\n            checked={isFancy}\n            onChange={e => {\n              setIsFancy(e.target.checked)\n            }}\n          />\n          Use fancy styling\n        </label>\n      </div>\n    );\n  }\n  return (\n    <div>\n      <Counter isFancy={false} />\n      <label>\n        <input\n          type=\"checkbox\"\n          checked={isFancy}\n          onChange={e => {\n            setIsFancy(e.target.checked)\n          }}\n        />\n        Use fancy styling\n      </label>\n    </div>\n  );\n}\n\nfunction Counter({ isFancy }) {\n  const [score, setScore] = useState(0);\n  const [hover, setHover] = useState(false);\n\n  let className = 'counter';\n  if (hover) {\n    className += ' hover';\n  }\n  if (isFancy) {\n    className += ' fancy';\n  }\n\n  return (\n    <div\n      className={className}\n      onPointerEnter={() => setHover(true)}\n      onPointerLeave={() => setHover(false)}\n    >\n      <h1>{score}</h1>\n      <button onClick={() => setScore(score + 1)}>\n        Add one\n      </button>\n    </div>\n  );\n}\n```\n\n```css\nlabel {\n  display: block;\n  clear: both;\n}\n\n.counter {\n  width: 100px;\n  text-align: center;\n  border: 1px solid gray;\n  border-radius: 4px;\n  padding: 20px;\n  margin: 0 20px 20px 0;\n  float: left;\n}\n\n.fancy {\n  border: 5px solid gold;\n  color: #ff6767;\n}\n\n.hover {\n  background: #ffffd8;\n}\n```\n\n</Sandpack>\n\n체크 박스를 선택할 때 state가 초기화될 거라고 생각했을 수도 있지만 그렇지 않습니다! **두 `<Counter />` 태그가 같은 위치에 렌더링되기** 때문이죠. React는 함수 안 어디에 조건문이 있는지 모릅니다. React는 당신이 반환하는 트리만 \"봅니다\". 두 상황에서 `App` 컴포넌트는 `<Counter />`를 첫 번째 자식으로 가진 `<div>`를 반환합니다. 이것이 React가 두 `<Counter />`를 _같은_ 것으로 보는 이유입니다.\n\n그들이 같은 \"주소\"를 갖는다고 생각할 수도 있습니다. root의 첫 번째 자식의 첫 번째 자식으로요. 이것이 당신이 어떻게 로직을 만들었는지와 상관없이 React가 이전과 다음 렌더링 사이에 컴포넌트를 맞추는 방법입니다.\n\n</Pitfall>\n\n## 같은 위치의 다른 컴포넌트는 state를 초기화합니다 {/*different-components-at-the-same-position-reset-state*/}\n\n다음 예시에서 체크 박스를 선택하면 `<Counter>`가 `<p>`로 교체됩니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function App() {\n  const [isPaused, setIsPaused] = useState(false);\n  return (\n    <div>\n      {isPaused ? (\n        <p>See you later!</p>\n      ) : (\n        <Counter />\n      )}\n      <label>\n        <input\n          type=\"checkbox\"\n          checked={isPaused}\n          onChange={e => {\n            setIsPaused(e.target.checked)\n          }}\n        />\n        Take a break\n      </label>\n    </div>\n  );\n}\n\nfunction Counter() {\n  const [score, setScore] = useState(0);\n  const [hover, setHover] = useState(false);\n\n  let className = 'counter';\n  if (hover) {\n    className += ' hover';\n  }\n\n  return (\n    <div\n      className={className}\n      onPointerEnter={() => setHover(true)}\n      onPointerLeave={() => setHover(false)}\n    >\n      <h1>{score}</h1>\n      <button onClick={() => setScore(score + 1)}>\n        Add one\n      </button>\n    </div>\n  );\n}\n```\n\n```css\nlabel {\n  display: block;\n  clear: both;\n}\n\n.counter {\n  width: 100px;\n  text-align: center;\n  border: 1px solid gray;\n  border-radius: 4px;\n  padding: 20px;\n  margin: 0 20px 20px 0;\n  float: left;\n}\n\n.hover {\n  background: #ffffd8;\n}\n```\n\n</Sandpack>\n\n여기서 당신은 같은 자리의 _다른_ 컴포넌트 타입으로 바꿉니다. 처음에는 `<div>`가 `Counter`를 갖고 있습니다. 하지만 `p`로 바꾸면 React는 UI 트리에서 `Counter`와 그 state를 제거합니다.\n\n<DiagramGroup>\n\n<Diagram name=\"preserving_state_diff_pt1\" height={290} width={753} alt=\"Diagram with three sections, with an arrow transitioning each section in between. The first section contains a React component labeled 'div' with a single child labeled 'Counter' containing a state bubble labeled 'count' with value 3. The middle section has the same 'div' parent, but the child component has now been deleted, indicated by a yellow 'proof' image. The third section has the same 'div' parent again, now with a new child labeled 'p', highlighted in yellow.\">\n\n`Counter` 가 `p` 로 바뀌면, `Counter` 는 삭제되고 `p` 가 추가됩니다\n\n</Diagram>\n\n</DiagramGroup>\n\n<DiagramGroup>\n\n<Diagram name=\"preserving_state_diff_pt2\" height={290} width={753} alt=\"Diagram with three sections, with an arrow transitioning each section in between. The first section contains a React component labeled 'p'. The middle section has the same 'div' parent, but the child component has now been deleted, indicated by a yellow 'proof' image. The third section has the same 'div' parent again, now with a new child labeled 'Counter' containing a state bubble labeled 'count' with value 0, highlighted in yellow.\">\n\n다시 되돌리면, `p` 는 삭제되고 `Counter` 가 추가됩니다.\n\n</Diagram>\n\n</DiagramGroup>\n\n또한 **같은 위치에 다른 컴포넌트를 렌더링할 때 컴포넌트는 그의 전체 서브 트리의 state를 초기화합니다.** 이것이 어떻게 작동하는지 보기 위해 카운터를 증가시키고 체크 박스를 체크해보세요.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function App() {\n  const [isFancy, setIsFancy] = useState(false);\n  return (\n    <div>\n      {isFancy ? (\n        <div>\n          <Counter isFancy={true} />\n        </div>\n      ) : (\n        <section>\n          <Counter isFancy={false} />\n        </section>\n      )}\n      <label>\n        <input\n          type=\"checkbox\"\n          checked={isFancy}\n          onChange={e => {\n            setIsFancy(e.target.checked)\n          }}\n        />\n        Use fancy styling\n      </label>\n    </div>\n  );\n}\n\nfunction Counter({ isFancy }) {\n  const [score, setScore] = useState(0);\n  const [hover, setHover] = useState(false);\n\n  let className = 'counter';\n  if (hover) {\n    className += ' hover';\n  }\n  if (isFancy) {\n    className += ' fancy';\n  }\n\n  return (\n    <div\n      className={className}\n      onPointerEnter={() => setHover(true)}\n      onPointerLeave={() => setHover(false)}\n    >\n      <h1>{score}</h1>\n      <button onClick={() => setScore(score + 1)}>\n        Add one\n      </button>\n    </div>\n  );\n}\n```\n\n```css\nlabel {\n  display: block;\n  clear: both;\n}\n\n.counter {\n  width: 100px;\n  text-align: center;\n  border: 1px solid gray;\n  border-radius: 4px;\n  padding: 20px;\n  margin: 0 20px 20px 0;\n  float: left;\n}\n\n.fancy {\n  border: 5px solid gold;\n  color: #ff6767;\n}\n\n.hover {\n  background: #ffffd8;\n}\n```\n\n</Sandpack>\n\n`Counter`의 State는 체크 박스를 선택할 때 초기화됩니다. 비록 `Counter`를 렌더링하지만, `div`의 첫 번째 자식이 `section`에서 `div`로 바뀝니다. 자식 `section`이 DOM에서 제거되었을 때, 그 아래 전체 트리(`Counter`와 그 State를 포함하여)도 함께 제거됩니다.\n\n<DiagramGroup>\n\n<Diagram name=\"preserving_state_diff_same_pt1\" height={350} width={794} alt=\"Diagram with three sections, with an arrow transitioning each section in between. The first section contains a React component labeled 'div' with a single child labeled 'section', which has a single child labeled 'Counter' containing a state bubble labeled 'count' with value 3. The middle section has the same 'div' parent, but the child components have now been deleted, indicated by a yellow 'proof' image. The third section has the same 'div' parent again, now with a new child labeled 'div', highlighted in yellow, also with a new child labeled 'Counter' containing a state bubble labeled 'count' with value 0, all highlighted in yellow.\">\n\n`section`이 `div`로 바뀌면, `section`은 삭제되고 새로운 `div`가 추가됩니다.\n\n</Diagram>\n\n</DiagramGroup>\n\n<DiagramGroup>\n\n<Diagram name=\"preserving_state_diff_same_pt2\" height={350} width={794} alt=\"Diagram with three sections, with an arrow transitioning each section in between. The first section contains a React component labeled 'div' with a single child labeled 'div', which has a single child labeled 'Counter' containing a state bubble labeled 'count' with value 0. The middle section has the same 'div' parent, but the child components have now been deleted, indicated by a yellow 'proof' image. The third section has the same 'div' parent again, now with a new child labeled 'section', highlighted in yellow, also with a new child labeled 'Counter' containing a state bubble labeled 'count' with value 0, all highlighted in yellow.\">\n\n다시 되돌리면, `div`는 삭제되고 새로운 `section`이 추가됩니다.\n\n</Diagram>\n\n</DiagramGroup>\n\n경험상<sup>Rule of Thumb</sup> **리렌더링할 때 State를 유지하고 싶다면, 트리 구조가 \"같아야\" 합니다.** 만약 구조가 다르다면 React가 트리에서 컴포넌트를 지울 때 State로 지우기 때문에 State가 유지되지 않습니다.\n\n<Pitfall>\n\n이것이 컴포넌트 함수를 중첩해서 정의하면 안되는 이유입니다.\n\n여기, `MyComponent` 안에서 `MyTextField` 컴포넌트 함수를 정의하고 있습니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function MyComponent() {\n  const [counter, setCounter] = useState(0);\n\n  function MyTextField() {\n    const [text, setText] = useState('');\n\n    return (\n      <input\n        value={text}\n        onChange={e => setText(e.target.value)}\n      />\n    );\n  }\n\n  return (\n    <>\n      <MyTextField />\n      <button onClick={() => {\n        setCounter(counter + 1)\n      }}>Clicked {counter} times</button>\n    </>\n  );\n}\n```\n\n</Sandpack>\n\n\n버튼을 누를 때마다 입력 State가 사라집니다! 이것은 `MyComponent`를 렌더링할 때마다 *다른* `MyTextField` 함수가 만들어지기 때문입니다. 따라서 같은 함수에서 *다른* 컴포넌트를 렌더링할 때마다 React는 그 아래의 모든 state를 초기화합니다. 이런 문제를 피하려면 **항상 컴포넌트를 중첩해서 정의하지 않고 최상위 범위에서 정의해야 합니다.**\n\n</Pitfall>\n\n## 같은 위치에서 state를 초기화하기 {/*resetting-state-at-the-same-position*/}\n\n기본적으로 React는 컴포넌트가 같은 위치를 유지하면 state를 유지합니다. 보통 이것이 당신이 원하는 행동이기 때문에 기본 동작으로서 타당합니다. 그러나 가끔 컴포넌트 State를 초기화하고 싶을 때가 있습니다. 두 선수가 턴마다 자신의 점수를 추적하는 앱을 한번 봅시다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function Scoreboard() {\n  const [isPlayerA, setIsPlayerA] = useState(true);\n  return (\n    <div>\n      {isPlayerA ? (\n        <Counter person=\"Taylor\" />\n      ) : (\n        <Counter person=\"Sarah\" />\n      )}\n      <button onClick={() => {\n        setIsPlayerA(!isPlayerA);\n      }}>\n        Next player!\n      </button>\n    </div>\n  );\n}\n\nfunction Counter({ person }) {\n  const [score, setScore] = useState(0);\n  const [hover, setHover] = useState(false);\n\n  let className = 'counter';\n  if (hover) {\n    className += ' hover';\n  }\n\n  return (\n    <div\n      className={className}\n      onPointerEnter={() => setHover(true)}\n      onPointerLeave={() => setHover(false)}\n    >\n      <h1>{person}'s score: {score}</h1>\n      <button onClick={() => setScore(score + 1)}>\n        Add one\n      </button>\n    </div>\n  );\n}\n```\n\n```css\nh1 {\n  font-size: 18px;\n}\n\n.counter {\n  width: 100px;\n  text-align: center;\n  border: 1px solid gray;\n  border-radius: 4px;\n  padding: 20px;\n  margin: 0 20px 20px 0;\n}\n\n.hover {\n  background: #ffffd8;\n}\n```\n\n</Sandpack>\n\n지금은 선수를 바꿀 때 점수가 유지됩니다. 두 `Counter`가 같은 위치에 나타나기 때문에 React는 그들을 `person` props가 변경된 *같은* `Counter`로 봅니다.\n\n하지만, 개념적으로 `app` 에는 두 개의 분리된 카운터가 있어야 합니다. 그들은 UI에 같은 위치에 나타나지만, 하나는 Taylor의 카운터이고, 다른 하나는 Sarah의 카운터입니다.\n\n이 둘을 바꿀 때 state를 초기화하기 위한 두 가지 방법이 있습니다.\n\n1. 다른 위치에 컴포넌트를 렌더링하기\n2. 각 컴포넌트에 `key`로 명시적인 식별자를 제공하기\n\n\n### 선택지 1: 다른 위치에 컴포넌트를 렌더링하기 {/*option-1-rendering-a-component-in-different-positions*/}\n\n두 `Counter`가 독립적이기를 원한다면 둘을 다른 위치에 렌더링할 수 있습니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function Scoreboard() {\n  const [isPlayerA, setIsPlayerA] = useState(true);\n  return (\n    <div>\n      {isPlayerA &&\n        <Counter person=\"Taylor\" />\n      }\n      {!isPlayerA &&\n        <Counter person=\"Sarah\" />\n      }\n      <button onClick={() => {\n        setIsPlayerA(!isPlayerA);\n      }}>\n        Next player!\n      </button>\n    </div>\n  );\n}\n\nfunction Counter({ person }) {\n  const [score, setScore] = useState(0);\n  const [hover, setHover] = useState(false);\n\n  let className = 'counter';\n  if (hover) {\n    className += ' hover';\n  }\n\n  return (\n    <div\n      className={className}\n      onPointerEnter={() => setHover(true)}\n      onPointerLeave={() => setHover(false)}\n    >\n      <h1>{person}'s score: {score}</h1>\n      <button onClick={() => setScore(score + 1)}>\n        Add one\n      </button>\n    </div>\n  );\n}\n```\n\n```css\nh1 {\n  font-size: 18px;\n}\n\n.counter {\n  width: 100px;\n  text-align: center;\n  border: 1px solid gray;\n  border-radius: 4px;\n  padding: 20px;\n  margin: 0 20px 20px 0;\n}\n\n.hover {\n  background: #ffffd8;\n}\n```\n\n</Sandpack>\n\n* 처음에는 `isPlayerA`가 `true`입니다. 따라서 첫 번째 자리에 `Counter`가 있고 두 번째 자리는 비어있습니다.\n* \"Next player\"를 클릭하면 첫 번째 자리는 비워지고 두 번째 자리에 `Counter`가 옵니다.\n\n<DiagramGroup>\n\n<Diagram name=\"preserving_state_diff_position_p1\" height={375} width={504} alt=\"Diagram with a tree of React components. The parent is labeled 'Scoreboard' with a state bubble labeled isPlayerA with value 'true'. The only child, arranged to the left, is labeled Counter with a state bubble labeled 'count' and value 0. All of the left child is highlighted in yellow, indicating it was added.\">\n\nInitial state\n\n</Diagram>\n\n<Diagram name=\"preserving_state_diff_position_p2\" height={375} width={504} alt=\"Diagram with a tree of React components. The parent is labeled 'Scoreboard' with a state bubble labeled isPlayerA with value 'false'. The state bubble is highlighted in yellow, indicating that it has changed. The left child is replaced with a yellow 'poof' image indicating that it has been deleted and there is a new child on the right, highlighted in yellow indicating that it was added. The new child is labeled 'Counter' and contains a state bubble labeled 'count' with value 0.\">\n\nClicking \"next\"\n\n</Diagram>\n\n<Diagram name=\"preserving_state_diff_position_p3\" height={375} width={504} alt=\"Diagram with a tree of React components. The parent is labeled 'Scoreboard' with a state bubble labeled isPlayerA with value 'true'. The state bubble is highlighted in yellow, indicating that it has changed. There is a new child on the left, highlighted in yellow indicating that it was added. The new child is labeled 'Counter' and contains a state bubble labeled 'count' with value 0. The right child is replaced with a yellow 'poof' image indicating that it has been deleted.\">\n\nClicking \"next\" again\n\n</Diagram>\n\n</DiagramGroup>\n\n> 각 `Counter`의 state는 DOM에서 지워질 때마다 제거됩니다. 이것이 버튼을 누를 때마다 초기화되는 이유입니다.\n\n이 방법은 같은 자리에 적은 수의 독립된 컴포넌트만을 가지고 있을 때 편리합니다. 이 예시에서는 두 개밖에 없기 때문에 JSX에서 각각 렌더링하기 번거롭지 않습니다.\n\n### 선택지 2: key를 이용해 state를 초기화하기 {/*option-2-resetting-state-with-a-key*/}\n\nState를 초기화하는 좀 더 일반적인 방법이 또 있습니다.\n\n[배열을 렌더링할 때](/learn/rendering-lists#keeping-list-items-in-order-with-key) `key`를 봤을 것입니다. key는 배열을 위한 것만은 아닙니다! React가 컴포넌트를 구별할 수 있도록 key를 사용할 수도 있습니다. 기본적으로 React는 컴포넌트를 구별하기 위해 부모 안에서의 순서(\"첫 번째 카운터\", \"두 번째 카운터\")를 이용합니다. 그러나 key를 이용하면 React에게 단지 *첫 번째* 카운터나 *두 번째* 카운터가 아니라 특정한 카운터라고 말해줄 수 있습니다. 예를 들면 *Taylor의* 카운터처럼요. 이렇게 트리 어디에서 나타나든 React는 *Taylor의* 카운터라는 것을 알 수 있습니다.\n\n다음 예시에서 두 `<Counter />`는 JSX에서 같은 위치에 나타나지만, state를 공유하지는 않습니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function Scoreboard() {\n  const [isPlayerA, setIsPlayerA] = useState(true);\n  return (\n    <div>\n      {isPlayerA ? (\n        <Counter key=\"Taylor\" person=\"Taylor\" />\n      ) : (\n        <Counter key=\"Sarah\" person=\"Sarah\" />\n      )}\n      <button onClick={() => {\n        setIsPlayerA(!isPlayerA);\n      }}>\n        Next player!\n      </button>\n    </div>\n  );\n}\n\nfunction Counter({ person }) {\n  const [score, setScore] = useState(0);\n  const [hover, setHover] = useState(false);\n\n  let className = 'counter';\n  if (hover) {\n    className += ' hover';\n  }\n\n  return (\n    <div\n      className={className}\n      onPointerEnter={() => setHover(true)}\n      onPointerLeave={() => setHover(false)}\n    >\n      <h1>{person}'s score: {score}</h1>\n      <button onClick={() => setScore(score + 1)}>\n        Add one\n      </button>\n    </div>\n  );\n}\n```\n\n```css\nh1 {\n  font-size: 18px;\n}\n\n.counter {\n  width: 100px;\n  text-align: center;\n  border: 1px solid gray;\n  border-radius: 4px;\n  padding: 20px;\n  margin: 0 20px 20px 0;\n}\n\n.hover {\n  background: #ffffd8;\n}\n```\n\n</Sandpack>\n\nTaylor와 Sarah를 바꾸지만, state를 유지하지는 않습니다. **당신이 다른 `key`를 주었기** 때문이죠.\n\n```js\n{isPlayerA ? (\n  <Counter key=\"Taylor\" person=\"Taylor\" />\n) : (\n  <Counter key=\"Sarah\" person=\"Sarah\" />\n)}\n```\n\n`key`를 명시하면 React는 부모 내에서의 순서 대신에 `key` 자체를 위치의 일부로 사용합니다. 이것이 컴포넌트를 JSX에서 같은 자리에 렌더링하지만 React 관점에서는 다른 카운터인 이유입니다. 결과적으로 그들은 절대 state를 공유하지 않습니다. 카운터가 화면에 나타날 때마다 state가 새로 만들어집니다. 그리고 카운터가 제거될 때 마다 state도 제거됩니다. 그들을 토글할 때마다 state가 계속 초기화됩니다.\n\n> key가 전역적으로 유일하지 않다는 것을 기억해야 합니다. key는 오직 부모 안에서만 자리를 명시합니다.\n\n### key를 이용해 폼을 초기화하기 {/*resetting-a-form-with-a-key*/}\n\nkey로 state를 초기화하는 것은 특히 폼을 다룰 때 유용합니다.\n\n다음 채팅 앱에서 `<Chat>` 컴포넌트는 문자열 입력 state를 가지고 있습니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { useState } from 'react';\nimport Chat from './Chat.js';\nimport ContactList from './ContactList.js';\n\nexport default function Messenger() {\n  const [to, setTo] = useState(contacts[0]);\n  return (\n    <div>\n      <ContactList\n        contacts={contacts}\n        selectedContact={to}\n        onSelect={contact => setTo(contact)}\n      />\n      <Chat contact={to} />\n    </div>\n  )\n}\n\nconst contacts = [\n  { id: 0, name: 'Taylor', email: 'taylor@mail.com' },\n  { id: 1, name: 'Alice', email: 'alice@mail.com' },\n  { id: 2, name: 'Bob', email: 'bob@mail.com' }\n];\n```\n\n```js src/ContactList.js\nexport default function ContactList({\n  selectedContact,\n  contacts,\n  onSelect\n}) {\n  return (\n    <section className=\"contact-list\">\n      <ul>\n        {contacts.map(contact =>\n          <li key={contact.id}>\n            <button onClick={() => {\n              onSelect(contact);\n            }}>\n              {contact.name}\n            </button>\n          </li>\n        )}\n      </ul>\n    </section>\n  );\n}\n```\n\n```js src/Chat.js\nimport { useState } from 'react';\n\nexport default function Chat({ contact }) {\n  const [text, setText] = useState('');\n  return (\n    <section className=\"chat\">\n      <textarea\n        value={text}\n        placeholder={'Chat to ' + contact.name}\n        onChange={e => setText(e.target.value)}\n      />\n      <br />\n      <button>Send to {contact.email}</button>\n    </section>\n  );\n}\n```\n\n```css\n.chat, .contact-list {\n  float: left;\n  margin-bottom: 20px;\n}\nul, li {\n  list-style: none;\n  margin: 0;\n  padding: 0;\n}\nli button {\n  width: 100px;\n  padding: 10px;\n  margin-right: 10px;\n}\ntextarea {\n  height: 150px;\n}\n```\n\n</Sandpack>\n\n입력란에 타이핑한 후에 \"Alice\"나 \"Bob\"을 눌러 다른 수신자를 선택해보세요. `<Chat>`이 트리의 같은 곳에서 렌더링되기 때문에 입력값이 유지되는 것을 볼 수 있습니다.\n\n**많은 앱에서 이런 동작을 원하겠지만 채팅 앱에서는 아닙니다!** 사용자가 실수로 클릭해서 이미 입력한 내용을 잘못된 사람에게 보내는 것은 바람직하지 않습니다. 이것을 고치기 위해 `key`를 추가해봅시다.\n\n```js\n<Chat key={to.id} contact={to} />\n```\n\n이것은 다른 수신자를 선택할 때 `Chat` 컴포넌트가 그 트리에 있는 모든 state를 포함해서 처음부터 다시 생성되는 것을 보장해줍니다. React는 DOM 엘리먼트도 다시 사용하는 대신 새로 만들 것입니다.\n\n이제 수신자를 바꿀 때마다 입력란이 비워지게 됩니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { useState } from 'react';\nimport Chat from './Chat.js';\nimport ContactList from './ContactList.js';\n\nexport default function Messenger() {\n  const [to, setTo] = useState(contacts[0]);\n  return (\n    <div>\n      <ContactList\n        contacts={contacts}\n        selectedContact={to}\n        onSelect={contact => setTo(contact)}\n      />\n      <Chat key={to.id} contact={to} />\n    </div>\n  )\n}\n\nconst contacts = [\n  { id: 0, name: 'Taylor', email: 'taylor@mail.com' },\n  { id: 1, name: 'Alice', email: 'alice@mail.com' },\n  { id: 2, name: 'Bob', email: 'bob@mail.com' }\n];\n```\n\n```js src/ContactList.js\nexport default function ContactList({\n  selectedContact,\n  contacts,\n  onSelect\n}) {\n  return (\n    <section className=\"contact-list\">\n      <ul>\n        {contacts.map(contact =>\n          <li key={contact.id}>\n            <button onClick={() => {\n              onSelect(contact);\n            }}>\n              {contact.name}\n            </button>\n          </li>\n        )}\n      </ul>\n    </section>\n  );\n}\n```\n\n```js src/Chat.js\nimport { useState } from 'react';\n\nexport default function Chat({ contact }) {\n  const [text, setText] = useState('');\n  return (\n    <section className=\"chat\">\n      <textarea\n        value={text}\n        placeholder={'Chat to ' + contact.name}\n        onChange={e => setText(e.target.value)}\n      />\n      <br />\n      <button>Send to {contact.email}</button>\n    </section>\n  );\n}\n```\n\n```css\n.chat, .contact-list {\n  float: left;\n  margin-bottom: 20px;\n}\nul, li {\n  list-style: none;\n  margin: 0;\n  padding: 0;\n}\nli button {\n  width: 100px;\n  padding: 10px;\n  margin-right: 10px;\n}\ntextarea {\n  height: 150px;\n}\n```\n\n</Sandpack>\n\n<DeepDive>\n\n#### 제거된 컴포넌트의 state를 보존하기 {/*preserving-state-for-removed-components*/}\n\n실제 채팅 앱에서는 이전의 수신자를 선택했을 때 입력값이 복구되는 것을 원할 것입니다. 보이지 않는 컴포넌트의 state를 \"살아 있게\"하는 몇 가지 방법이 있습니다.\n\n- 현재 채팅만 렌더링하는 대신 _모든_ 채팅을 렌더링하고 CSS로 안 보이게 할 수 있습니다. 채팅은 트리에서 사라지지 않을 것이고 따라서 그들의 state는 유지됩니다. 이 방법은 간단한 UI에서 잘 작동합니다. 하지만 숨겨진 트리가 크고 많은 DOM 노드를 가지고 있다면 매우 느려질 것입니다.\n- [state를 상위로 올리고](/learn/sharing-state-between-components) 각 수신자의 임시 메시지를 부모 컴포넌트에 가지고 있을 수 있습니다. 이 방법에서 부모가 중요한 정보를 가지고 있기 때문에 자식 컴포넌트가 제거되어도 상관이 없습니다. 이것이 가장 일반적인 해법입니다.\n- React state 이외의 다른 저장소를 이용할 수도 있습니다. 예를 들어 사용자가 페이지를 실수로 닫아도 메시지를 유지하고 싶을 수도 있습니다. 이때 [`localStorage`](https://developer.mozilla.org/ko/docs/Web/API/Window/localStorage)에 메시지를 저장하고 이를 이용해 `Chat` 컴포넌트를 초기화할 수 있습니다.\n\n어떤 방법을 선택하더라도 _Alice와의_ 채팅은 _Bob과의_ 채팅과 개념상 구별되기 때문에 현재 수신자를 기반으로 `<Chat>` 트리에 `key`를 주는 것이 타당합니다.\n\n</DeepDive>\n\n<Recap>\n\n- React는 같은 컴포넌트가 같은 자리에 렌더링되는 한 state를 유지합니다.\n- state는 JSX 태그에 저장되지 않습니다. state는 JSX으로 만든 트리 위치와 연관됩니다.\n- 컴포넌트에 다른 key를 주어서 그 하위 트리를 초기화하도록 강제할 수 있습니다.\n- 중첩해서 컴포넌트를 정의하면 원치 않게 state가 초기화될 수 있기 때문에 그렇게 하지 마세요.\n\n</Recap>\n\n\n\n<Challenges>\n\n#### 입력 문자열이 사라지는 것 고치기 {/*fix-disappearing-input-text*/}\n\n이 예시에서 버튼을 누르면 메시지를 보여줍니다. 하지만 버튼을 누르는 것은 또한 원치 않게 state를 초기화합니다. 왜 이런 현상이 일어날까요? 버튼을 눌러도 입력 문자열이 초기화되지 않도록 고쳐보세요.\n\n<Sandpack>\n\n```js src/App.js\nimport { useState } from 'react';\n\nexport default function App() {\n  const [showHint, setShowHint] = useState(false);\n  if (showHint) {\n    return (\n      <div>\n        <p><i>Hint: Your favorite city?</i></p>\n        <Form />\n        <button onClick={() => {\n          setShowHint(false);\n        }}>Hide hint</button>\n      </div>\n    );\n  }\n  return (\n    <div>\n      <Form />\n      <button onClick={() => {\n        setShowHint(true);\n      }}>Show hint</button>\n    </div>\n  );\n}\n\nfunction Form() {\n  const [text, setText] = useState('');\n  return (\n    <textarea\n      value={text}\n      onChange={e => setText(e.target.value)}\n    />\n  );\n}\n```\n\n```css\ntextarea { display: block; margin: 10px 0; }\n```\n\n</Sandpack>\n\n<Solution>\n\n`Form`이 다른 위치에 렌더링되기 때문에 문제가 생깁니다. `if` 문에서는 `<div>`의 두 번째 자식이지만 `else` 문에서는 첫 번째 자식입니다. 따라서 각 위치에서 컴포넌트 타입이 바뀌게 됩니다. 첫 번째 위치는 `p`와 `Form` 사이를 바뀌고 두 번째 위치에서는 `Form`과 `button` 사이에서 바뀝니다. React는 컴포넌트 타입이 변할 때마다 state를 초기화합니다.\n\n분기를 합쳐서 `Form`를 항상 같은 자리에서 렌더링하는 것이 가장 쉬운 해결 방법입니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { useState } from 'react';\n\nexport default function App() {\n  const [showHint, setShowHint] = useState(false);\n  return (\n    <div>\n      {showHint &&\n        <p><i>Hint: Your favorite city?</i></p>\n      }\n      <Form />\n      {showHint ? (\n        <button onClick={() => {\n          setShowHint(false);\n        }}>Hide hint</button>\n      ) : (\n        <button onClick={() => {\n          setShowHint(true);\n        }}>Show hint</button>\n      )}\n    </div>\n  );\n}\n\nfunction Form() {\n  const [text, setText] = useState('');\n  return (\n    <textarea\n      value={text}\n      onChange={e => setText(e.target.value)}\n    />\n  );\n}\n```\n\n```css\ntextarea { display: block; margin: 10px 0; }\n```\n\n</Sandpack>\n\n\n엄밀히 말하면 `else` 문의 `<Form />` 앞에 `null`을 추가해서 `if` 문에서와 트리 구조를 일치시켜도 됩니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { useState } from 'react';\n\nexport default function App() {\n  const [showHint, setShowHint] = useState(false);\n  if (showHint) {\n    return (\n      <div>\n        <p><i>Hint: Your favorite city?</i></p>\n        <Form />\n        <button onClick={() => {\n          setShowHint(false);\n        }}>Hide hint</button>\n      </div>\n    );\n  }\n  return (\n    <div>\n      {null}\n      <Form />\n      <button onClick={() => {\n        setShowHint(true);\n      }}>Show hint</button>\n    </div>\n  );\n}\n\nfunction Form() {\n  const [text, setText] = useState('');\n  return (\n    <textarea\n      value={text}\n      onChange={e => setText(e.target.value)}\n    />\n  );\n}\n```\n\n```css\ntextarea { display: block; margin: 10px 0; }\n```\n\n</Sandpack>\n\n이 방법에서 `Form`은 항상 두 번째 자식이기 때문에 같은 위치를 유지하고 state를 유지합니다. 하지만 이 접근 방식은 훨씬 애매하고 다른 사람이 `null`을 지워버릴 리스크를 남깁니다.\n\n</Solution>\n\n#### 두 필드를 맞바꾸기 {/*swap-two-form-fields*/}\n\n다음 폼은 first name과 last name을 입력받습니다. 또한 어떤 필드가 앞에 가는지를 조절하는 체크 박스로 있습니다. 체크 박스를 선택하면 \"Last name\" 필드가 \"First name\" 필드 앞에 나타납니다.\n\n거의 모든 작업에는 버그가 있습니다. \"First name\"에 입력을 하고 체크 박스를 선택해도 문자열은 \"Last name\"이 된 첫 번째 인풋에 그대로 있습니다. 순서를 바꿀 때 입력 문자열도 *이동*하도록 수정해보세요.\n\n<Hint>\n\n이 필드들에게 부모 내에서의 위치는 충분하지 않은 것 같습니다. 리렌더링될 때 React에게 state를 연결하는 방법을 알려줄 수 있을까요?\n\n</Hint>\n\n<Sandpack>\n\n```js src/App.js\nimport { useState } from 'react';\n\nexport default function App() {\n  const [reverse, setReverse] = useState(false);\n  let checkbox = (\n    <label>\n      <input\n        type=\"checkbox\"\n        checked={reverse}\n        onChange={e => setReverse(e.target.checked)}\n      />\n      Reverse order\n    </label>\n  );\n  if (reverse) {\n    return (\n      <>\n        <Field label=\"Last name\" />\n        <Field label=\"First name\" />\n        {checkbox}\n      </>\n    );\n  } else {\n    return (\n      <>\n        <Field label=\"First name\" />\n        <Field label=\"Last name\" />\n        {checkbox}\n      </>\n    );\n  }\n}\n\nfunction Field({ label }) {\n  const [text, setText] = useState('');\n  return (\n    <label>\n      {label}:{' '}\n      <input\n        type=\"text\"\n        value={text}\n        placeholder={label}\n        onChange={e => setText(e.target.value)}\n      />\n    </label>\n  );\n}\n```\n\n```css\nlabel { display: block; margin: 10px 0; }\n```\n\n</Sandpack>\n\n<Solution>\n\n`if`와 `else`문 안의 두 `<Field>` 컴포넌트에게 `key`를 주십시오. 이로써 부모 안에서의 순서가 바뀌더라도 React에게 각 `<Field>`의 올바른 state를 어떻게 \"맞출지\" 알려줄 수 있습니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { useState } from 'react';\n\nexport default function App() {\n  const [reverse, setReverse] = useState(false);\n  let checkbox = (\n    <label>\n      <input\n        type=\"checkbox\"\n        checked={reverse}\n        onChange={e => setReverse(e.target.checked)}\n      />\n      Reverse order\n    </label>\n  );\n  if (reverse) {\n    return (\n      <>\n        <Field key=\"lastName\" label=\"Last name\" />\n        <Field key=\"firstName\" label=\"First name\" />\n        {checkbox}\n      </>\n    );\n  } else {\n    return (\n      <>\n        <Field key=\"firstName\" label=\"First name\" />\n        <Field key=\"lastName\" label=\"Last name\" />\n        {checkbox}\n      </>\n    );\n  }\n}\n\nfunction Field({ label }) {\n  const [text, setText] = useState('');\n  return (\n    <label>\n      {label}:{' '}\n      <input\n        type=\"text\"\n        value={text}\n        placeholder={label}\n        onChange={e => setText(e.target.value)}\n      />\n    </label>\n  );\n}\n```\n\n```css\nlabel { display: block; margin: 10px 0; }\n```\n\n</Sandpack>\n\n</Solution>\n\n#### 폼 세부내용을 초기화하기 {/*reset-a-detail-form*/}\n\n여기 수정 가능한 연락처 목록이 있습니다. 연락처 상세 정보를 수정하고 \"Save\"를 눌러 갱신하거나 \"Reset\"을 눌러 수정한 것을 되돌릴 수 있습니다.\n\nAlice와 같이 다른 연락처를 선택했을 때 state는 갱신되지만, 폼은 여전히 이전 내용을 보여줍니다. 다른 연락처를 선택했을 때 폼이 초기화되도록 수정해보세요.\n\n<Sandpack>\n\n```js src/App.js\nimport { useState } from 'react';\nimport ContactList from './ContactList.js';\nimport EditContact from './EditContact.js';\n\nexport default function ContactManager() {\n  const [\n    contacts,\n    setContacts\n  ] = useState(initialContacts);\n  const [\n    selectedId,\n    setSelectedId\n  ] = useState(0);\n  const selectedContact = contacts.find(c =>\n    c.id === selectedId\n  );\n\n  function handleSave(updatedData) {\n    const nextContacts = contacts.map(c => {\n      if (c.id === updatedData.id) {\n        return updatedData;\n      } else {\n        return c;\n      }\n    });\n    setContacts(nextContacts);\n  }\n\n  return (\n    <div>\n      <ContactList\n        contacts={contacts}\n        selectedId={selectedId}\n        onSelect={id => setSelectedId(id)}\n      />\n      <hr />\n      <EditContact\n        initialData={selectedContact}\n        onSave={handleSave}\n      />\n    </div>\n  )\n}\n\nconst initialContacts = [\n  { id: 0, name: 'Taylor', email: 'taylor@mail.com' },\n  { id: 1, name: 'Alice', email: 'alice@mail.com' },\n  { id: 2, name: 'Bob', email: 'bob@mail.com' }\n];\n```\n\n```js src/ContactList.js\nexport default function ContactList({\n  contacts,\n  selectedId,\n  onSelect\n}) {\n  return (\n    <section>\n      <ul>\n        {contacts.map(contact =>\n          <li key={contact.id}>\n            <button onClick={() => {\n              onSelect(contact.id);\n            }}>\n              {contact.id === selectedId ?\n                <b>{contact.name}</b> :\n                contact.name\n              }\n            </button>\n          </li>\n        )}\n      </ul>\n    </section>\n  );\n}\n```\n\n```js src/EditContact.js\nimport { useState } from 'react';\n\nexport default function EditContact({ initialData, onSave }) {\n  const [name, setName] = useState(initialData.name);\n  const [email, setEmail] = useState(initialData.email);\n  return (\n    <section>\n      <label>\n        Name:{' '}\n        <input\n          type=\"text\"\n          value={name}\n          onChange={e => setName(e.target.value)}\n        />\n      </label>\n      <label>\n        Email:{' '}\n        <input\n          type=\"email\"\n          value={email}\n          onChange={e => setEmail(e.target.value)}\n        />\n      </label>\n      <button onClick={() => {\n        const updatedData = {\n          id: initialData.id,\n          name: name,\n          email: email\n        };\n        onSave(updatedData);\n      }}>\n        Save\n      </button>\n      <button onClick={() => {\n        setName(initialData.name);\n        setEmail(initialData.email);\n      }}>\n        Reset\n      </button>\n    </section>\n  );\n}\n```\n\n```css\nul, li {\n  list-style: none;\n  margin: 0;\n  padding: 0;\n}\nli { display: inline-block; }\nli button {\n  padding: 10px;\n}\nlabel {\n  display: block;\n  margin: 10px 0;\n}\nbutton {\n  margin-right: 10px;\n  margin-bottom: 10px;\n}\n```\n\n</Sandpack>\n\n<Solution>\n\n`EditContact` 컴포넌트에 `key={selectedId}`를 주세요. 이로써 다른 연락처를 선택하면 폼이 초기화됩니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { useState } from 'react';\nimport ContactList from './ContactList.js';\nimport EditContact from './EditContact.js';\n\nexport default function ContactManager() {\n  const [\n    contacts,\n    setContacts\n  ] = useState(initialContacts);\n  const [\n    selectedId,\n    setSelectedId\n  ] = useState(0);\n  const selectedContact = contacts.find(c =>\n    c.id === selectedId\n  );\n\n  function handleSave(updatedData) {\n    const nextContacts = contacts.map(c => {\n      if (c.id === updatedData.id) {\n        return updatedData;\n      } else {\n        return c;\n      }\n    });\n    setContacts(nextContacts);\n  }\n\n  return (\n    <div>\n      <ContactList\n        contacts={contacts}\n        selectedId={selectedId}\n        onSelect={id => setSelectedId(id)}\n      />\n      <hr />\n      <EditContact\n        key={selectedId}\n        initialData={selectedContact}\n        onSave={handleSave}\n      />\n    </div>\n  )\n}\n\nconst initialContacts = [\n  { id: 0, name: 'Taylor', email: 'taylor@mail.com' },\n  { id: 1, name: 'Alice', email: 'alice@mail.com' },\n  { id: 2, name: 'Bob', email: 'bob@mail.com' }\n];\n```\n\n```js src/ContactList.js\nexport default function ContactList({\n  contacts,\n  selectedId,\n  onSelect\n}) {\n  return (\n    <section>\n      <ul>\n        {contacts.map(contact =>\n          <li key={contact.id}>\n            <button onClick={() => {\n              onSelect(contact.id);\n            }}>\n              {contact.id === selectedId ?\n                <b>{contact.name}</b> :\n                contact.name\n              }\n            </button>\n          </li>\n        )}\n      </ul>\n    </section>\n  );\n}\n```\n\n```js src/EditContact.js\nimport { useState } from 'react';\n\nexport default function EditContact({ initialData, onSave }) {\n  const [name, setName] = useState(initialData.name);\n  const [email, setEmail] = useState(initialData.email);\n  return (\n    <section>\n      <label>\n        Name:{' '}\n        <input\n          type=\"text\"\n          value={name}\n          onChange={e => setName(e.target.value)}\n        />\n      </label>\n      <label>\n        Email:{' '}\n        <input\n          type=\"email\"\n          value={email}\n          onChange={e => setEmail(e.target.value)}\n        />\n      </label>\n      <button onClick={() => {\n        const updatedData = {\n          id: initialData.id,\n          name: name,\n          email: email\n        };\n        onSave(updatedData);\n      }}>\n        Save\n      </button>\n      <button onClick={() => {\n        setName(initialData.name);\n        setEmail(initialData.email);\n      }}>\n        Reset\n      </button>\n    </section>\n  );\n}\n```\n\n```css\nul, li {\n  list-style: none;\n  margin: 0;\n  padding: 0;\n}\nli { display: inline-block; }\nli button {\n  padding: 10px;\n}\nlabel {\n  display: block;\n  margin: 10px 0;\n}\nbutton {\n  margin-right: 10px;\n  margin-bottom: 10px;\n}\n```\n\n</Sandpack>\n\n</Solution>\n\n#### 이미지가 로딩될 동안 이미지가 안 보이게 하기 {/*clear-an-image-while-its-loading*/}\n\n\"Next\"를 누르면 브라우저는 다음 이미지를 불러오기 시작합니다. 하지만 같은 `<img>` 태그에서 보이기 때문에 기본적으로 다음 이미지를 불러오기 전까지 기존 이미지가 나옵니다. 만약 설명과 이미지가 항상 일치해야 한다면 이것은 바람직하지 않은 동작입니다. \"Next\"를 누르는 순간 이전 이미지가 안 보이도록 바꾸어보세요.\n\n<Hint>\n\nReact가 DOM을 재사용하지 않고 새로 만들게 하는 방법이 있을까요?\n\n</Hint>\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function Gallery() {\n  const [index, setIndex] = useState(0);\n  const hasNext = index < images.length - 1;\n\n  function handleClick() {\n    if (hasNext) {\n      setIndex(index + 1);\n    } else {\n      setIndex(0);\n    }\n  }\n\n  let image = images[index];\n  return (\n    <>\n      <button onClick={handleClick}>\n        Next\n      </button>\n      <h3>\n        Image {index + 1} of {images.length}\n      </h3>\n      <img src={image.src} />\n      <p>\n        {image.place}\n      </p>\n    </>\n  );\n}\n\nlet images = [{\n  place: 'Penang, Malaysia',\n  src: 'https://i.imgur.com/FJeJR8M.jpg'\n}, {\n  place: 'Lisbon, Portugal',\n  src: 'https://i.imgur.com/dB2LRbj.jpg'\n}, {\n  place: 'Bilbao, Spain',\n  src: 'https://i.imgur.com/z08o2TS.jpg'\n}, {\n  place: 'Valparaíso, Chile',\n  src: 'https://i.imgur.com/Y3utgTi.jpg'\n}, {\n  place: 'Schwyz, Switzerland',\n  src: 'https://i.imgur.com/JBbMpWY.jpg'\n}, {\n  place: 'Prague, Czechia',\n  src: 'https://i.imgur.com/QwUKKmF.jpg'\n}, {\n  place: 'Ljubljana, Slovenia',\n  src: 'https://i.imgur.com/3aIiwfm.jpg'\n}];\n```\n\n```css\nimg { width: 150px; height: 150px; }\n```\n\n</Sandpack>\n\n<Solution>\n\n`<img>` 태그에 `key`를 줄 수 있습니다. `key`가 바뀌면 React는 `<img>` DOM 노드를 새로 다시 만듭니다. 이미지를 불러오는 동안 이미지가 잠깐 깜박이는데 앱의 모든 이미지가 이처럼 작동하기를 원하지는 않을 것입니다. 하지만 이미지와 설명이 항상 일치하도록 할 때는 일리가 있습니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function Gallery() {\n  const [index, setIndex] = useState(0);\n  const hasNext = index < images.length - 1;\n\n  function handleClick() {\n    if (hasNext) {\n      setIndex(index + 1);\n    } else {\n      setIndex(0);\n    }\n  }\n\n  let image = images[index];\n  return (\n    <>\n      <button onClick={handleClick}>\n        Next\n      </button>\n      <h3>\n        Image {index + 1} of {images.length}\n      </h3>\n      <img key={image.src} src={image.src} />\n      <p>\n        {image.place}\n      </p>\n    </>\n  );\n}\n\nlet images = [{\n  place: 'Penang, Malaysia',\n  src: 'https://i.imgur.com/FJeJR8M.jpg'\n}, {\n  place: 'Lisbon, Portugal',\n  src: 'https://i.imgur.com/dB2LRbj.jpg'\n}, {\n  place: 'Bilbao, Spain',\n  src: 'https://i.imgur.com/z08o2TS.jpg'\n}, {\n  place: 'Valparaíso, Chile',\n  src: 'https://i.imgur.com/Y3utgTi.jpg'\n}, {\n  place: 'Schwyz, Switzerland',\n  src: 'https://i.imgur.com/JBbMpWY.jpg'\n}, {\n  place: 'Prague, Czechia',\n  src: 'https://i.imgur.com/QwUKKmF.jpg'\n}, {\n  place: 'Ljubljana, Slovenia',\n  src: 'https://i.imgur.com/3aIiwfm.jpg'\n}];\n```\n\n```css\nimg { width: 150px; height: 150px; }\n```\n\n</Sandpack>\n\n</Solution>\n\n#### 배열에서 잘못 지정된 state 고치기 {/*fix-misplaced-state-in-the-list*/}\n\n다음 예시에서 배열의 각 `Contact`는 \"Show email\"이 눌렸는지에 대한 state를 갖고 있습니다. Alice의 \"Show email\"을 누르고 \"Show in reverse order\" 체크 박스를 선택해보세요. 아래쪽으로 내려간 Alice의 이메일은 닫혀있고 대신 _Taylor_의 이메일이 열려있는 것을 볼 수 있습니다.\n\n순서와 관계없이 확장 state가 각 연락처와 연관되도록 고쳐보세요.\n\n<Sandpack>\n\n```js src/App.js\nimport { useState } from 'react';\nimport Contact from './Contact.js';\n\nexport default function ContactList() {\n  const [reverse, setReverse] = useState(false);\n\n  const displayedContacts = [...contacts];\n  if (reverse) {\n    displayedContacts.reverse();\n  }\n\n  return (\n    <>\n      <label>\n        <input\n          type=\"checkbox\"\n          checked={reverse}\n          onChange={e => {\n            setReverse(e.target.checked)\n          }}\n        />{' '}\n        Show in reverse order\n      </label>\n      <ul>\n        {displayedContacts.map((contact, i) =>\n          <li key={i}>\n            <Contact contact={contact} />\n          </li>\n        )}\n      </ul>\n    </>\n  );\n}\n\nconst contacts = [\n  { id: 0, name: 'Alice', email: 'alice@mail.com' },\n  { id: 1, name: 'Bob', email: 'bob@mail.com' },\n  { id: 2, name: 'Taylor', email: 'taylor@mail.com' }\n];\n```\n\n```js src/Contact.js\nimport { useState } from 'react';\n\nexport default function Contact({ contact }) {\n  const [expanded, setExpanded] = useState(false);\n  return (\n    <>\n      <p><b>{contact.name}</b></p>\n      {expanded &&\n        <p><i>{contact.email}</i></p>\n      }\n      <button onClick={() => {\n        setExpanded(!expanded);\n      }}>\n        {expanded ? 'Hide' : 'Show'} email\n      </button>\n    </>\n  );\n}\n```\n\n```css\nul, li {\n  list-style: none;\n  margin: 0;\n  padding: 0;\n}\nli {\n  margin-bottom: 20px;\n}\nlabel {\n  display: block;\n  margin: 10px 0;\n}\nbutton {\n  margin-right: 10px;\n  margin-bottom: 10px;\n}\n```\n\n</Sandpack>\n\n<Solution>\n\n이 예시에서는 배열의 인덱스를 `key`로 사용해서 문제가 발생합니다.\n\n```js\n{displayedContacts.map((contact, i) =>\n  <li key={i}>\n```\n\n하지만 우리는 state가 _각 특정 연락처_와 연관되기를 바랍니다.\n\n대신 연락처 ID를 `key`로 사용해서 문제를 해결할 수 있습니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { useState } from 'react';\nimport Contact from './Contact.js';\n\nexport default function ContactList() {\n  const [reverse, setReverse] = useState(false);\n\n  const displayedContacts = [...contacts];\n  if (reverse) {\n    displayedContacts.reverse();\n  }\n\n  return (\n    <>\n      <label>\n        <input\n          type=\"checkbox\"\n          checked={reverse}\n          onChange={e => {\n            setReverse(e.target.checked)\n          }}\n        />{' '}\n        Show in reverse order\n      </label>\n      <ul>\n        {displayedContacts.map(contact =>\n          <li key={contact.id}>\n            <Contact contact={contact} />\n          </li>\n        )}\n      </ul>\n    </>\n  );\n}\n\nconst contacts = [\n  { id: 0, name: 'Alice', email: 'alice@mail.com' },\n  { id: 1, name: 'Bob', email: 'bob@mail.com' },\n  { id: 2, name: 'Taylor', email: 'taylor@mail.com' }\n];\n```\n\n```js src/Contact.js\nimport { useState } from 'react';\n\nexport default function Contact({ contact }) {\n  const [expanded, setExpanded] = useState(false);\n  return (\n    <>\n      <p><b>{contact.name}</b></p>\n      {expanded &&\n        <p><i>{contact.email}</i></p>\n      }\n      <button onClick={() => {\n        setExpanded(!expanded);\n      }}>\n        {expanded ? 'Hide' : 'Show'} email\n      </button>\n    </>\n  );\n}\n```\n\n```css\nul, li {\n  list-style: none;\n  margin: 0;\n  padding: 0;\n}\nli {\n  margin-bottom: 20px;\n}\nlabel {\n  display: block;\n  margin: 10px 0;\n}\nbutton {\n  margin-right: 10px;\n  margin-bottom: 10px;\n}\n```\n\n</Sandpack>\n\nState는 트리의 위치와 연관됩니다. `key`는 위치에 순서 대신 이름을 줄 수 있게 해 줍니다.\n\n</Solution>\n\n</Challenges>\n"
  },
  {
    "path": "src/content/learn/queueing-a-series-of-state-updates.md",
    "content": "---\ntitle: state 업데이트 큐\n---\n\n<Intro>\n\nstate 변수를 설정하면 다음 렌더링이 큐에 들어갑니다. 그러나 때에 따라 다음 렌더링을 큐에 넣기 전에, 값에 대해 여러 작업을 수행하고 싶을 때도 있습니다. 이를 위해서는 React가 state 업데이트를 어떻게 배치하면 좋을지 이해하는 것이 도움이 됩니다.\n\n</Intro>\n\n<YouWillLearn>\n\n* \"batching\"이란 무엇이며 React가 여러 state 업데이트를 처리하는 방법\n* 동일한 state 변수에서 여러 업데이트를 연속으로 적용하는 방법\n\n</YouWillLearn>\n\n## React state batches 업데이트 {/*react-batches-state-updates*/}\n\n`setNumber(number + 1)`를 세 번 호출하므로 \"+3\" 버튼을 클릭하면 세 번 증가할 것으로 예상할 수 있습니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function Counter() {\n  const [number, setNumber] = useState(0);\n\n  return (\n    <>\n      <h1>{number}</h1>\n      <button onClick={() => {\n        setNumber(number + 1);\n        setNumber(number + 1);\n        setNumber(number + 1);\n      }}>+3</button>\n    </>\n  )\n}\n```\n\n```css\nbutton { display: inline-block; margin: 10px; font-size: 20px; }\nh1 { display: inline-block; margin: 10px; width: 30px; text-align: center; }\n```\n\n</Sandpack>\n\n이전 세션에서 기억할 수 있듯이 [각 렌더링의 state 값은 고정되어 있으므로](/learn/state-as-a-snapshot#rendering-takes-a-snapshot-in-time), 첫 번째 렌더링의 이벤트 핸들러의 `number` 값은 `setNumber(1)`을 몇 번 호출하든 항상 0입니다.\n\n```js\nsetNumber(0 + 1);\nsetNumber(0 + 1);\nsetNumber(0 + 1);\n```\n\n하지만 여기에는 한가지 요인이 더 있습니다. **React는 state 업데이트를 하기 전에 이벤트 핸들러의 모든 코드가 실행될 때까지 기다립니다.** 이 때문에 리렌더링은 모든 `setNumber()` 호출이 완료된 이후에만 일어납니다.\n\n이는 음식점에서 주문받는 웨이터를 생각해 볼 수 있습니다. 웨이터는 첫 번째 요리를 말하자마자 주방으로 달려가지 않습니다! 대신 주문이 끝날 때까지 기다렸다가 주문을 변경하고, 심지어 테이블에 있는 다른 사람의 주문도 받습니다.\n\n<Illustration src=\"/images/docs/illustrations/i_react-batching.png\"  alt=\"An elegant cursor at a restaurant places and order multiple times with React, playing the part of the waiter. After she calls setState() multiple times, the waiter writes down the last one she requested as her final order.\" />\n\n이렇게 하면 너무 많은 [리렌더링](/learn/render-and-commit#re-renders-when-state-updates)이 발생하지 않고도 여러 컴포넌트에서 나온 다수의 state 변수를 업데이트할 수 있습니다. 하지만 이는 이벤트 핸들러와 그 안에 있는 코드가 완료될 때까지 UI가 업데이트되지 않는다는 의미이기도 합니다. **batching**라고도 하는 이 동작은 React 앱을 훨씬 빠르게 실행할 수 있게 해줍니다. 또한 일부 변수만 업데이트된 \"반쯤 완성된\" 혼란스러운 렌더링을 처리하지 않아도 됩니다.\n\n**React는 클릭과 같은 여러 의도적인 이벤트에 대해 batch를 수행하지 않으며,** 각 클릭은 개별적으로 처리됩니다. React는 안전한 경우에만 batch를 수행하니 안심하세요. 예를 들어 첫 번째 버튼 클릭으로 양식이 비활성화되면 두 번째 클릭으로 양식이 다시 제출되지 않도록 보장합니다.\n\n## 다음 렌더링 전에 동일한 state 변수를 여러 번 업데이트하기 {/*updating-the-same-state-multiple-times-before-the-next-render*/}\n\n흔한 사례는 아니지만, 다음 렌더링 전에 동일한 state 변수를 여러 번 업데이트 하고 싶다면 `setNumber(number + 1)` 와 같은 다음 state 값을 전달하는 대신, `setNumber(n => n + 1)` 와 같이 이전 큐의 state를 기반으로 다음 state를 계산하는 함수를 전달할 수 있습니다. 이는 단순히 state 값을 대체하는 것이 아니라 React에 \"state 값으로 무언가를 하라\"고 지시하는 방법입니다.\n\n이제 카운터를 증가시켜 보세요.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function Counter() {\n  const [number, setNumber] = useState(0);\n\n  return (\n    <>\n      <h1>{number}</h1>\n      <button onClick={() => {\n        setNumber(n => n + 1);\n        setNumber(n => n + 1);\n        setNumber(n => n + 1);\n      }}>+3</button>\n    </>\n  )\n}\n```\n\n```css\nbutton { display: inline-block; margin: 10px; font-size: 20px; }\nh1 { display: inline-block; margin: 10px; width: 30px; text-align: center; }\n```\n\n</Sandpack>\n\n여기서 `n => n + 1` 은 업데이터 함수(updater function)라고 부릅니다. 이를 state 설정자 함수에 전달 할 때,\n\n1. React는 이벤트 핸들러의 다른 코드가 모두 실행된 후에 이 함수가 처리되도록 큐에 넣습니다.\n2. 다음 렌더링 중에 React는 큐를 순회하여 최종 업데이트된 state를 제공합니다.\n\n```js\nsetNumber(n => n + 1);\nsetNumber(n => n + 1);\nsetNumber(n => n + 1);\n```\n\nReact가 이벤트 핸들러를 수행하는 동안 여러 코드를 통해 작동하는 방식은 다음과 같습니다.\n\n1. `setNumber(n => n + 1)`: `n => n + 1` 함수를 큐에 추가합니다.\n2. `setNumber(n => n + 1)`: `n => n + 1` 함수를 큐에 추가합니다.\n3. `setNumber(n => n + 1)`: `n => n + 1` 함수를 큐에 추가합니다.\n\n다음 렌더링 중에 `useState` 를 호출하면 React는 큐를 순회합니다. 이전 `number` state는 `0`이었으므로 React는 이를 첫 번째 업데이터 함수에 `n` 인수로 전달합니다. 그런 다음 React는 이전 업데이터 함수의 반환 값을 가져와서 다음 업데이터 함수에 `n`으로 전달하는 식으로 반복합니다.\n\n|  queued update | `n` | returns |\n|--------------|---------|-----|\n| `n => n + 1` | `0` | `0 + 1 = 1` |\n| `n => n + 1` | `1` | `1 + 1 = 2` |\n| `n => n + 1` | `2` | `2 + 1 = 3` |\n\nReact는 `3`을 최종 결과로 저장하고 `useState`에서 반환합니다.\n\n이것이 위 예시 \"+3\"을 클릭하면 값이 3씩 올바르게 증가하는 이유입니다.\n\n### state를 교체한 후 업데이트하면 어떻게 되나요? {/*what-happens-if-you-update-state-after-replacing-it*/}\n\n이 이벤트 핸들러는 어떨까요? 다음 렌더링에서 `number`가 어떻게 될까요?\n\n```js\n<button onClick={() => {\n  setNumber(number + 5);\n  setNumber(n => n + 1);\n}}>\n```\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function Counter() {\n  const [number, setNumber] = useState(0);\n\n  return (\n    <>\n      <h1>{number}</h1>\n      <button onClick={() => {\n        setNumber(number + 5);\n        setNumber(n => n + 1);\n      }}>Increase the number</button>\n    </>\n  )\n}\n```\n\n```css\nbutton { display: inline-block; margin: 10px; font-size: 20px; }\nh1 { display: inline-block; margin: 10px; width: 30px; text-align: center; }\n```\n\n</Sandpack>\n\n이 이벤트 핸들러가 React에 지시하는 작업은 다음과 같습니다.\n\n1. `setNumber(number + 5)` : `number`는 `0`이므로 `setNumber(0 + 5)`입니다. React는 큐에 *\"`5`로 바꾸기\"* 를 추가합니다.\n2. `setNumber(n => n + 1)` : `n => n + 1`는 업데이터 함수입니다. React는 *해당 함수*를 큐에 추가합니다.\n\n다음 렌더링하는 동안 React는 state 큐를 순회합니다.\n\n|   queued update       | `n` | returns |\n|--------------|---------|-----|\n| \"replace with `5`\" | `0` (unused) | `5` |\n| `n => n + 1` | `5` | `5 + 1 = 6` |\n\nReact는 `6`을 최종 결과로 저장하고 `useState`에서 반환합니다.\n\n<Note>\n\n`setState(5)`가 실제로는 `setState(n => 5)` 처럼 동작하지만 `n`이 사용되지 않는다는 것을 눈치채셨을 것입니다!\n\n</Note>\n\n### 업데이트 후 state를 바꾸면 어떻게 되나요? {/*what-happens-if-you-replace-state-after-updating-it*/}\n\n한 가지 예를 더 들어보겠습니다. 다음 렌더링에서 `number`가 어떻게 될까요?\n\n```js\n<button onClick={() => {\n  setNumber(number + 5);\n  setNumber(n => n + 1);\n  setNumber(42);\n}}>\n```\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function Counter() {\n  const [number, setNumber] = useState(0);\n\n  return (\n    <>\n      <h1>{number}</h1>\n      <button onClick={() => {\n        setNumber(number + 5);\n        setNumber(n => n + 1);\n        setNumber(42);\n      }}>Increase the number</button>\n    </>\n  )\n}\n```\n\n```css\nbutton { display: inline-block; margin: 10px; font-size: 20px; }\nh1 { display: inline-block; margin: 10px; width: 30px; text-align: center; }\n```\n\n</Sandpack>\n\n이 이벤트 핸들러를 실행하는 동안 React가 이 코드를 통해 작동하는 방식은 다음과 같습니다.\n\n1. `setNumber(number + 5)`: `number` 는 `0` 이므로 `setNumber(0 + 5)`입니다. React는 *\"`5`로 바꾸기\"* 를 큐에 추가합니다.\n2. `setNumber(n => n + 1)`: `n => n + 1` 는 업데이터 함수입니다. React는 *이 함수*를 큐에 추가합니다.\n3. `setNumber(42)`: React는 *\"`42`로 바꾸기\"* 를 큐에 추가합니다.\n\n다음 렌더링하는 동안, React는 state 큐를 순회합니다.\n\n|   queued update       | `n` | returns |\n|--------------|---------|-----|\n| \"replace with `5`\" | `0` (unused) | `5` |\n| `n => n + 1` | `5` | `5 + 1 = 6` |\n| \"replace with `42`\" | `6` (unused) | `42` |\n\n그런 다음 React는 `42`를 최종 결과로 저장하고 `useState`에서 반환합니다.\n\n요약하자면, `setNumber` state 설정자 함수에 전달할 내용은 다음과 같이 생각할 수 있습니다:\n\n* **업데이터 함수** (예. `n => n + 1`) 가 큐에 추가됩니다.\n* **다른 값** (예. 숫자 `5`) 은 큐에 \"`5`로 바꾸기\"를 추가하며, 이미 큐에 대기 중인 항목은 무시합니다.\n\n이벤트 핸들러가 완료되면 React는 리렌더링을 실행합니다. 리렌더링하는 동안 React는 큐를 처리합니다. 업데이터 함수는 렌더링 중에 실행되므로, **업데이터 함수는 [순수](/learn/keeping-components-pure)해야 하며** 결과만 *반환* 해야 합니다. 업데이터 함수 내부에서 state를 변경하거나 다른 사이드 이팩트를 실행하려고 하지 마세요. Strict 모드에서 React는 각 업데이터 함수를 두 번 실행(두 번째 결과는 버림)하여 실수를 찾을 수 있도록 도와줍니다.\n\n### 명명 규칙 {/*naming-conventions*/}\n\n업데이터 함수 인수의 이름은 해당 state 변수의 첫 글자로 지정하는 것이 일반적입니다.\n\n```js\nsetEnabled(e => !e);\nsetLastName(ln => ln.toUpperCase());\nsetFriendCount(fc => fc * 2);\n```\n\n좀 더 자세한 코드를 선호하는 경우 `setEnabled(enabled => !enabled)`와 같이 전체 state 변수 이름을 반복하거나, `setEnabled(prevEnabled => !prevEnabled)`와 같은 접두사를 사용하는 것이 널리 사용되는 규칙입니다.\n\n<Recap>\n\n* state를 설정하더라도 기존 렌더링의 변수는 변경되지 않으며, 대신 새로운 렌더링을 요청합니다.\n* React는 이벤트 핸들러가 실행을 마친 후 state 업데이트를 처리합니다. 이를 batching 이라고 합니다.\n* 하나의 이벤트에서 일부 state를 여러 번 업데이트하려면 `setNumber(n => n + 1)` 업데이터 함수를 사용할 수 있습니다.\n\n</Recap>\n\n\n\n<Challenges>\n\n#### 요청 카운터를 고쳐보세요. {/*fix-a-request-counter*/}\n\n사용자가 동시에 여러 개의 미술품을 주문할 수 있는 예술 쇼핑몰 앱에서 작업하고 있습니다. 사용자가 \"Buy\" 버튼을 누를 때마다 \"Pending\" 카운터가 1씩 증가해야 합니다. 3초 후에는 \"Pending\" 카운터가 감소하고 \"Completed\" 카운터가 증가해야 합니다.\n\n그런데 \"Pending\" 카운터가 의도대로 작동하지 않고 있습니다. \"Buy\"를 누르면  `-1`로 감소합니다(그럴 수 없습니다!). 그리고 빠르게 두 번 클릭하면 두 카운터가 모두 예측할 수 없게 작동하는 것 같습니다.\n\n왜 이런 일이 발생할까요? 두 카운터를 모두 수정하세요.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function RequestTracker() {\n  const [pending, setPending] = useState(0);\n  const [completed, setCompleted] = useState(0);\n\n  async function handleClick() {\n    setPending(pending + 1);\n    await delay(3000);\n    setPending(pending - 1);\n    setCompleted(completed + 1);\n  }\n\n  return (\n    <>\n      <h3>\n        Pending: {pending}\n      </h3>\n      <h3>\n        Completed: {completed}\n      </h3>\n      <button onClick={handleClick}>\n        Buy\n      </button>\n    </>\n  );\n}\n\nfunction delay(ms) {\n  return new Promise(resolve => {\n    setTimeout(resolve, ms);\n  });\n}\n```\n\n</Sandpack>\n\n<Solution>\n\n`handleClick` 이벤트 핸들러 내부에서 `Pending`과 `Completed`의 값은 클릭 이벤트 당시의 값과 일치합니다. 첫 번째 렌더링의 경우, `Pending`이 `0`이었으므로 `setPending(pending - 1)`는 `setPending(-1)` 되는데, 이는 잘못된 것입니다. 클릭 중에 결정된 구체적인 값으로 카운터를 설정하는 대신 카운터를 *증가* 또는 *감소*하고 싶으므로 대신 업데이터 함수를 전달할 수 있습니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function RequestTracker() {\n  const [pending, setPending] = useState(0);\n  const [completed, setCompleted] = useState(0);\n\n  async function handleClick() {\n    setPending(p => p + 1);\n    await delay(3000);\n    setPending(p => p - 1);\n    setCompleted(c => c + 1);\n  }\n\n  return (\n    <>\n      <h3>\n        Pending: {pending}\n      </h3>\n      <h3>\n        Completed: {completed}\n      </h3>\n      <button onClick={handleClick}>\n        Buy\n      </button>\n    </>\n  );\n}\n\nfunction delay(ms) {\n  return new Promise(resolve => {\n    setTimeout(resolve, ms);\n  });\n}\n```\n\n</Sandpack>\n\n이렇게 하면 카운터를 늘리거나 줄일 때 클릭 당시의 state가 아니라 *최신* state와 관련하여 카운터를 늘리거나 줄일 수 있습니다.\n\n</Solution>\n\n#### state 큐를 직접 구현해 보세요. {/*implement-the-state-queue-yourself*/}\n\n이번 도전과제에서는 React의 작은 부분을 처음부터 다시 구현하게 됩니다! 생각보다 어렵지 않습니다.\n\n샌드박스 미리보기를 스크롤 하세요. **4개의 테스트 케이스**가 표시되는 것을 확인하세요. 이 페이지의 앞부분에서 보았던 예시와 일치합니다. 여러분의 임무는 각 케이스에 대해 올바른 결과를 반환하도록 `getFinalState` 함수를 구현하는 것입니다. 올바르게 구현하면 네 가지 테스트를 모두 통과할 것입니다.\n\n두 개의 인수를 받게 됩니다. `baseState`는 초기 state(예: `0`)이고, `queue`는 숫자(예: `5`)와 업데이터 함수(예: `n => n + 1`)가 추가된, 순서대로 섞여 있는 배열입니다.\n\n여러분의 임무는 이 페이지의 표에 표시된 것처럼 최종 state를 반환하는 것입니다!\n\n<Hint>\n\n막막한 느낌이 든다면 다음 코드 구조로 시작해 보세요.\n\n```js\nexport function getFinalState(baseState, queue) {\n  let finalState = baseState;\n\n  for (let update of queue) {\n    if (typeof update === 'function') {\n      // TODO: apply the updater function\n    } else {\n      // TODO: replace the state\n    }\n  }\n\n  return finalState;\n}\n```\n\n빈칸을 채워주세요!\n\n</Hint>\n\n<Sandpack>\n\n```js src/processQueue.js active\nexport function getFinalState(baseState, queue) {\n  let finalState = baseState;\n\n  // TODO: do something with the queue...\n\n  return finalState;\n}\n```\n\n```js src/App.js\nimport { getFinalState } from './processQueue.js';\n\nfunction increment(n) {\n  return n + 1;\n}\nincrement.toString = () => 'n => n+1';\n\nexport default function App() {\n  return (\n    <>\n      <TestCase\n        baseState={0}\n        queue={[1, 1, 1]}\n        expected={1}\n      />\n      <hr />\n      <TestCase\n        baseState={0}\n        queue={[\n          increment,\n          increment,\n          increment\n        ]}\n        expected={3}\n      />\n      <hr />\n      <TestCase\n        baseState={0}\n        queue={[\n          5,\n          increment,\n        ]}\n        expected={6}\n      />\n      <hr />\n      <TestCase\n        baseState={0}\n        queue={[\n          5,\n          increment,\n          42,\n        ]}\n        expected={42}\n      />\n    </>\n  );\n}\n\nfunction TestCase({\n  baseState,\n  queue,\n  expected\n}) {\n  const actual = getFinalState(baseState, queue);\n  return (\n    <>\n      <p>Base state: <b>{baseState}</b></p>\n      <p>Queue: <b>[{queue.join(', ')}]</b></p>\n      <p>Expected result: <b>{expected}</b></p>\n      <p style={{\n        color: actual === expected ?\n          'green' :\n          'red'\n      }}>\n        Your result: <b>{actual}</b>\n        {' '}\n        ({actual === expected ?\n          'correct' :\n          'wrong'\n        })\n      </p>\n    </>\n  );\n}\n```\n\n</Sandpack>\n\n<Solution>\n\n이 페이지에 설명된 바로 그 알고리즘이 React가 최종 state를 계산하는 데 사용하는 알고리즘입니다.\n\n<Sandpack>\n\n```js src/processQueue.js active\nexport function getFinalState(baseState, queue) {\n  let finalState = baseState;\n\n  for (let update of queue) {\n    if (typeof update === 'function') {\n      // Apply the updater function.\n      finalState = update(finalState);\n    } else {\n      // Replace the next state.\n      finalState = update;\n    }\n  }\n\n  return finalState;\n}\n```\n\n```js src/App.js\nimport { getFinalState } from './processQueue.js';\n\nfunction increment(n) {\n  return n + 1;\n}\nincrement.toString = () => 'n => n+1';\n\nexport default function App() {\n  return (\n    <>\n      <TestCase\n        baseState={0}\n        queue={[1, 1, 1]}\n        expected={1}\n      />\n      <hr />\n      <TestCase\n        baseState={0}\n        queue={[\n          increment,\n          increment,\n          increment\n        ]}\n        expected={3}\n      />\n      <hr />\n      <TestCase\n        baseState={0}\n        queue={[\n          5,\n          increment,\n        ]}\n        expected={6}\n      />\n      <hr />\n      <TestCase\n        baseState={0}\n        queue={[\n          5,\n          increment,\n          42,\n        ]}\n        expected={42}\n      />\n    </>\n  );\n}\n\nfunction TestCase({\n  baseState,\n  queue,\n  expected\n}) {\n  const actual = getFinalState(baseState, queue);\n  return (\n    <>\n      <p>Base state: <b>{baseState}</b></p>\n      <p>Queue: <b>[{queue.join(', ')}]</b></p>\n      <p>Expected result: <b>{expected}</b></p>\n      <p style={{\n        color: actual === expected ?\n          'green' :\n          'red'\n      }}>\n        Your result: <b>{actual}</b>\n        {' '}\n        ({actual === expected ?\n          'correct' :\n          'wrong'\n        })\n      </p>\n    </>\n  );\n}\n```\n\n</Sandpack>\n\n이제 React의 이 부분이 어떻게 작동하는지 알 수 있습니다!\n\n</Solution>\n\n</Challenges>\n"
  },
  {
    "path": "src/content/learn/react-compiler/debugging.md",
    "content": "---\ntitle: 디버깅 및 문제 해결\n---\n\n<Intro>\n이 가이드에서는 React 컴파일러 사용 시 발생하는 문제를 식별하고 해결하는 방법을 알아봅니다. 컴파일 문제를 디버깅하고 일반적인 문제를 해결하는 방법을 배웁니다.\n</Intro>\n\n<YouWillLearn>\n\n* 컴파일러 오류와 런타임 문제의 차이\n* 컴파일을 방해하는 일반적인 패턴\n* 단계별 디버깅 워크플로우\n\n</YouWillLearn>\n\n## 컴파일러 동작 이해하기 {/*understanding-compiler-behavior*/}\n\nReact 컴파일러는 [React의 규칙](/reference/rules)을 따르는 코드를 처리하도록 설계되었습니다. 이러한 규칙을 위반할 수 있는 코드를 만나면 앱의 동작을 변경하는 위험을 감수하기보다 안전하게 최적화를 건너뜁니다.\n\n### 컴파일러 오류 vs 런타임 문제 {/*compiler-errors-vs-runtime-issues*/}\n\n**컴파일러 오류**는 빌드 시점에 발생하며 코드가 컴파일되지 않게 합니다. 컴파일러는 문제가 있는 코드를 실패시키기보다 건너뛰도록 설계되어 있기 때문에 이러한 오류는 자주 발생하지 않습니다.\n\n**런타임 문제**는 컴파일된 코드가 예상과 다르게 동작할 때 발생합니다. React 컴파일러 관련 문제가 발생하면 대부분 런타임 문제입니다. 이는 일반적으로 컴파일러가 감지할 수 없는 미묘한 방식으로 코드가 React의 규칙을 위반하고, 컴파일러가 건너뛰어야 했던 컴포넌트를 실수로 컴파일했을 때 발생합니다.\n\n런타임 문제를 디버깅할 때는 ESLint 규칙이 감지하지 못한 영향받는 컴포넌트의 React 규칙 위반을 찾는 데 집중하세요. 컴파일러는 코드가 이러한 규칙을 따른다고 가정하며, 감지할 수 없는 방식으로 규칙이 위반되면 런타임 문제가 발생합니다.\n\n\n## 컴파일을 방해하는 일반적인 패턴 {/*common-breaking-patterns*/}\n\nReact 컴파일러가 앱을 망가뜨릴 수 있는 주요 원인 중 하나는 코드가 정확성을 위해 메모이제이션에 의존하도록 작성된 경우입니다. 이는 앱이 제대로 작동하기 위해 특정 값이 메모이제이션되는 것에 의존한다는 의미입니다. 컴파일러가 수동 방식과 다르게 메모이제이션할 수 있으므로, Effect가 과도하게 실행되거나 무한 루프가 발생하거나 업데이트가 누락되는 등의 예상치 못한 동작이 발생할 수 있습니다.\n\n이런 상황이 발생하는 일반적인 시나리오는 다음과 같습니다.\n\n- **참조 동등성에 의존하는 Effect** - Effect가 렌더링 간에 동일한 참조를 유지하는 객체나 배열에 의존하는 경우\n- **안정적인 참조가 필요한 의존성 배열** - 불안정한 의존성이 Effect를 너무 자주 실행하거나 무한 루프를 생성하는 경우\n- **참조 검사 기반 조건부 로직** - 코드가 캐싱이나 최적화를 위해 참조 동등성 검사를 사용하는 경우\n\n## 디버깅 워크플로우 {/*debugging-workflow*/}\n\n문제가 발생하면 다음 단계를 따르세요.\n\n### 컴파일러 빌드 오류 {/*compiler-build-errors*/}\n\n빌드를 예상치 않게 중단시키는 컴파일러 오류가 발생하면 컴파일러의 버그일 가능성이 높습니다. 다음 정보와 함께 [facebook/react](https://github.com/facebook/react/issues) 저장소에 보고해 주세요.\n- 오류 메시지\n- 오류를 발생시킨 코드\n- React 및 컴파일러 버전\n\n### 런타임 문제 {/*runtime-issues*/}\n\n런타임 동작 문제의 경우 다음을 확인하세요.\n\n### 1. 일시적으로 컴파일 비활성화 {/*temporarily-disable-compilation*/}\n\n`\"use no memo\"`를 사용하여 문제가 컴파일러와 관련이 있는지 확인합니다.\n\n```js\nfunction ProblematicComponent() {\n  \"use no memo\"; // 이 컴포넌트의 컴파일 건너뛰기\n  // ... 나머지 컴포넌트\n}\n```\n\n문제가 사라지면 React 규칙 위반과 관련이 있을 가능성이 높습니다.\n\n문제가 있는 컴포넌트에서 수동 메모이제이션(`useMemo`, `useCallback`, `memo`)을 제거하여 메모이제이션 없이도 앱이 올바르게 작동하는지 확인해 볼 수도 있습니다. 모든 메모이제이션을 제거해도 버그가 계속 발생하면 수정해야 할 React 규칙 위반이 있는 것입니다.\n\n### 2. 단계별로 문제 해결 {/*fix-issues-step-by-step*/}\n\n1. 근본 원인 식별 (주로 정확성을 위한 메모이제이션)\n2. 각 수정 후 테스트\n3. 수정 완료 후 `\"use no memo\"` 제거\n4. React DevTools에서 컴포넌트에 ✨ 배지가 표시되는지 확인\n\n## 컴파일러 버그 보고 {/*reporting-compiler-bugs*/}\n\n컴파일러 버그를 발견했다고 생각되면 다음을 확인하세요.\n\n1. **React 규칙 위반이 아닌지 확인** - ESLint로 검사\n2. **최소 재현 사례 생성** - 작은 예시로 문제 격리\n3. **컴파일러 없이 테스트** - 컴파일 시에만 문제가 발생하는지 확인\n4. **[이슈](https://github.com/facebook/react/issues/new?template=compiler_bug_report.yml) 등록**:\n   - React 및 컴파일러 버전\n   - 최소 재현 코드\n   - 예상 동작 vs 실제 동작\n   - 오류 메시지\n\n## 다음 단계 {/*next-steps*/}\n\n- 문제 예방을 위해 [React의 규칙](/reference/rules) 검토\n- 점진적 배포 전략을 위한 [점진적 도입 가이드](/learn/react-compiler/incremental-adoption) 확인\n"
  },
  {
    "path": "src/content/learn/react-compiler/incremental-adoption.md",
    "content": "---\ntitle: 점진적 도입\n---\n\n<Intro>\nReact 컴파일러는 점진적으로 도입할 수 있으며, 코드베이스의 특정 부분에서 먼저 시도해 볼 수 있습니다. 이 가이드에서는 기존 프로젝트에서 컴파일러를 단계적으로 배포하는 방법을 알아봅니다.\n</Intro>\n\n<YouWillLearn>\n\n* 점진적 도입이 권장되는 이유\n* 디렉터리 기반 도입을 위한 Babel overrides 사용\n* 선택적 컴파일을 위한 `\"use memo\"` 지시어 사용\n* 컴포넌트 제외를 위한 `\"use no memo\"` 지시어 사용\n* 게이팅을 통한 런타임 기능 플래그\n* 도입 진행 상황 모니터링\n\n</YouWillLearn>\n\n## 점진적 도입이 필요한 이유 {/*why-incremental-adoption*/}\n\nReact 컴파일러는 전체 코드베이스를 자동으로 최적화하도록 설계되었지만, 한 번에 모두 도입할 필요는 없습니다. 점진적 도입은 배포 과정을 제어할 수 있게 해주어 앱의 작은 부분에서 컴파일러를 테스트한 후 나머지 부분으로 확장할 수 있습니다.\n\n작은 부분부터 시작하면 컴파일러의 최적화에 대한 신뢰를 쌓을 수 있습니다. 컴파일된 코드로 앱이 올바르게 동작하는지 확인하고, 성능 개선을 측정하고, 코드베이스에 특정한 엣지 케이스를 식별할 수 있습니다. 이 접근 방식은 안정성이 중요한 프로덕션 애플리케이션에 특히 유용합니다.\n\n점진적 도입은 컴파일러가 발견할 수 있는 React 규칙 위반을 해결하기도 더 쉽게 만듭니다. 전체 코드베이스의 위반을 한 번에 수정하는 대신 컴파일러 적용 범위를 확장하면서 체계적으로 해결할 수 있습니다. 이를 통해 마이그레이션을 관리하기 쉽게 유지하고 버그 도입 위험을 줄일 수 있습니다.\n\n컴파일되는 코드 부분을 제어함으로써 A/B 테스트를 실행하여 컴파일러 최적화의 실제 영향을 측정할 수도 있습니다. 이 데이터는 전체 도입에 대한 정보에 기반한 결정을 내릴 수 있어 팀에게 가치를 입증하는데 도움이 됩니다.\n\n## 점진적 도입 방법 {/*approaches-to-incremental-adoption*/}\n\nReact 컴파일러를 점진적으로 도입하는 세 가지 주요 방법이 있습니다.\n\n1. **Babel overrides** - 특정 디렉터리에 컴파일러 적용\n2. **`\"use memo\"`로 선택적 적용** - 명시적으로 선택한 컴포넌트만 컴파일\n3. **런타임 게이팅** - 기능 플래그로 컴파일 제어\n\n모든 방법은 전체 배포 전에 애플리케이션의 특정 부분에서 컴파일러를 테스트할 수 있게 해줍니다.\n\n## Babel Overrides를 사용한 디렉터리 기반 도입 {/*directory-based-adoption*/}\n\nBabel의 `overrides` 옵션을 사용하면 코드베이스의 여러 부분에 서로 다른 플러그인을 적용할 수 있습니다. 디렉터리별로 React 컴파일러를 점진적으로 도입하는 데 적합합니다.\n\n### 기본 설정 {/*basic-configuration*/}\n\n특정 디렉터리에 컴파일러를 적용하는 것부터 시작합니다.\n\n```js\n// babel.config.js\nmodule.exports = {\n  plugins: [\n    // 모든 파일에 적용되는 전역 플러그인\n  ],\n  overrides: [\n    {\n      test: './src/modern/**/*.{js,jsx,ts,tsx}',\n      plugins: [\n        'babel-plugin-react-compiler'\n      ]\n    }\n  ]\n};\n```\n\n### 적용 범위 확장 {/*expanding-coverage*/}\n\n신뢰가 쌓이면 더 많은 디렉터리를 추가합니다.\n\n```js\n// babel.config.js\nmodule.exports = {\n  plugins: [\n    // 전역 플러그인\n  ],\n  overrides: [\n    {\n      test: ['./src/modern/**/*.{js,jsx,ts,tsx}', './src/features/**/*.{js,jsx,ts,tsx}'],\n      plugins: [\n        'babel-plugin-react-compiler'\n      ]\n    },\n    {\n      test: './src/legacy/**/*.{js,jsx,ts,tsx}',\n      plugins: [\n        // 레거시 코드용 다른 플러그인\n      ]\n    }\n  ]\n};\n```\n\n### 컴파일러 옵션과 함께 사용 {/*with-compiler-options*/}\n\noverride별로 컴파일러 옵션을 설정할 수도 있습니다.\n\n```js\n// babel.config.js\nmodule.exports = {\n  plugins: [],\n  overrides: [\n    {\n      test: './src/experimental/**/*.{js,jsx,ts,tsx}',\n      plugins: [\n        ['babel-plugin-react-compiler', {\n          // 옵션 ...\n        }]\n      ]\n    },\n    {\n      test: './src/production/**/*.{js,jsx,ts,tsx}',\n      plugins: [\n        ['babel-plugin-react-compiler', {\n          // 옵션 ...\n        }]\n      ]\n    }\n  ]\n};\n```\n\n\n## `\"use memo\"`를 사용한 선택적 모드 {/*opt-in-mode-with-use-memo*/}\n\n최대한의 제어를 위해 `compilationMode: 'annotation'`을 사용하여 `\"use memo\"` 지시어를 통해 명시적으로 선택한 컴포넌트와 Hook만 컴파일할 수 있습니다.\n\n<Note>\n이 방법은 개별 컴포넌트와 Hook에 대한 세밀한 제어를 제공합니다. 전체 디렉터리에 영향을 주지 않고 특정 컴포넌트에서 컴파일러를 테스트하고 싶을 때 유용합니다.\n</Note>\n\n### 어노테이션 모드 설정 {/*annotation-mode-configuration*/}\n\n```js\n// babel.config.js\nmodule.exports = {\n  plugins: [\n    ['babel-plugin-react-compiler', {\n      compilationMode: 'annotation',\n    }],\n  ],\n};\n```\n\n### 지시어 사용 {/*using-the-directive*/}\n\n컴파일하려는 함수의 시작 부분에 `\"use memo\"`를 추가합니다.\n\n```js\nfunction TodoList({ todos }) {\n  \"use memo\"; // 이 컴포넌트를 컴파일 대상으로 선택\n\n  const sortedTodos = todos.slice().sort();\n\n  return (\n    <ul>\n      {sortedTodos.map(todo => (\n        <TodoItem key={todo.id} todo={todo} />\n      ))}\n    </ul>\n  );\n}\n\nfunction useSortedData(data) {\n  \"use memo\"; // 이 Hook을 컴파일 대상으로 선택\n\n  return data.slice().sort();\n}\n```\n\n`compilationMode: 'annotation'`을 사용하면 다음을 해야 합니다.\n- 최적화하려는 모든 컴포넌트에 `\"use memo\"`를 추가합니다.\n- 모든 커스텀 Hook에 `\"use memo\"`를 추가합니다.\n- 이후에 새로 작성하는 컴포넌트에도 추가하는 것을 잊지 마세요.\n\n이를 통해 컴파일러의 영향을 평가하는 동안 어떤 컴포넌트가 컴파일되는지 정밀하게 제어할 수 있습니다.\n\n## 게이팅을 통한 런타임 기능 플래그 {/*runtime-feature-flags-with-gating*/}\n\n`gating` 옵션을 사용하면 기능 플래그를 사용하여 런타임에 컴파일을 제어할 수 있습니다. A/B 테스트를 실행하거나 사용자 세그먼트에 따라 컴파일러를 점진적으로 배포하는 데 유용합니다.\n\n### 게이팅 작동 방식 {/*how-gating-works*/}\n\n컴파일러는 최적화된 코드를 런타임 검사로 감쌉니다. 게이트가 `true`를 반환하면 최적화된 버전이 실행됩니다. 그렇지 않으면 원본 코드가 실행됩니다.\n\n### 게이팅 설정 {/*gating-configuration*/}\n\n```js\n// babel.config.js\nmodule.exports = {\n  plugins: [\n    ['babel-plugin-react-compiler', {\n      gating: {\n        source: 'ReactCompilerFeatureFlags',\n        importSpecifierName: 'isCompilerEnabled',\n      },\n    }],\n  ],\n};\n```\n\n### 기능 플래그 구현 {/*implementing-the-feature-flag*/}\n\n게이팅 함수를 내보내는 모듈을 생성합니다.\n\n```js\n// ReactCompilerFeatureFlags.js\nexport function isCompilerEnabled() {\n  // 기능 플래그 시스템 사용\n  return getFeatureFlag('react-compiler-enabled');\n}\n```\n\n## 도입 문제 해결 {/*troubleshooting-adoption*/}\n\n도입 중 문제가 발생하면 다음을 시도해 보세요.\n\n1. `\"use no memo\"`를 사용하여 문제가 있는 컴포넌트를 일시적으로 제외\n2. [디버깅 가이드](/learn/react-compiler/debugging)에서 일반적인 문제 확인\n3. ESLint 플러그인이 식별한 React 규칙 위반 수정\n4. 더 점진적인 도입을 위해 `compilationMode: 'annotation'` 사용 고려\n\n## 다음 단계 {/*next-steps*/}\n\n- 더 많은 옵션은 [설정 가이드](/reference/react-compiler/configuration) 참고\n- [디버깅 기법](/learn/react-compiler/debugging) 알아보기\n- 모든 컴파일러 옵션은 [API 레퍼런스](/reference/react-compiler/configuration) 확인\n"
  },
  {
    "path": "src/content/learn/react-compiler/index.md",
    "content": "---\ntitle: React 컴파일러\n---\n\n## 소개 {/*introduction*/}\n\n[React 컴파일러가 하는 일](/learn/react-compiler/introduction)과 메모이제이션을 자동으로 처리하여 `useMemo`, `useCallback`, `React.memo`를 수동으로 사용할 필요 없이 React 애플리케이션을 최적화하는 방법을 알아보세요.\n\n## 설치 {/*installation*/}\n\n[React 컴파일러 설치](/learn/react-compiler/installation)를 통해 시작하고 빌드 도구와 함께 구성하는 방법을 알아보세요.\n\n\n## 점진적 도입 {/*incremental-adoption*/}\n\n아직 모든 곳에서 활성화할 준비가 되지 않았다면, 기존 코드베이스에서 [React 컴파일러를 점진적으로 도입하는 전략](/learn/react-compiler/incremental-adoption)을 알아보세요.\n\n## 디버깅 및 문제 해결 {/*debugging-and-troubleshooting*/}\n\n예상대로 작동하지 않을 때, [디버깅 가이드](/learn/react-compiler/debugging)를 사용하여 컴파일러 오류와 런타임 문제의 차이를 이해하고, 일반적인 문제 패턴을 식별하며, 체계적인 디버깅 워크플로우를 따라가 보세요.\n\n## 설정 및 레퍼런스 {/*configuration-and-reference*/}\n\n자세한 설정 옵션과 API 레퍼런스는 다음을 참고하세요.\n\n- [설정 옵션](/reference/react-compiler/configuration) - React 버전 호환성을 포함한 모든 컴파일러 설정 옵션\n- [지시어](/reference/react-compiler/directives) - 함수 수준의 컴파일 제어\n- [라이브러리 컴파일](/reference/react-compiler/compiling-libraries) - 사전 컴파일된 라이브러리 배포\n\n## 추가 자료 {/*additional-resources*/}\n\n이 문서 외에도, 컴파일러에 대한 추가 정보와 논의를 위해 [React Compiler Working Group](https://github.com/reactwg/react-compiler)을 확인하는 것을 권장합니다.\n\n"
  },
  {
    "path": "src/content/learn/react-compiler/installation.md",
    "content": "---\ntitle: 설치\n---\n\n<Intro>\n이 가이드에서는 React 애플리케이션에 React 컴파일러를 설치하고 설정하는 방법을 알아봅니다.\n</Intro>\n\n<YouWillLearn>\n\n* React 컴파일러 설치 방법\n* 다양한 빌드 도구를 위한 기본 설정\n* 설정이 올바르게 작동하는지 확인하는 방법\n\n</YouWillLearn>\n\n## 필수 조건 {/*prerequisites*/}\n\nReact 컴파일러는 React 19에서 가장 잘 작동하도록 설계되었지만, React 17과 18도 지원합니다. [React 버전 호환성](/reference/react-compiler/target)에서 자세히 알아보세요.\n\n## 설치 {/*installation*/}\n\nReact 컴파일러를 `devDependency`로 설치합니다.\n\n<TerminalBlock>\nnpm install -D babel-plugin-react-compiler@latest\n</TerminalBlock>\n\n또는 Yarn을 사용하는 경우\n\n<TerminalBlock>\nyarn add -D babel-plugin-react-compiler@latest\n</TerminalBlock>\n\n또는 pnpm을 사용하는 경우\n\n<TerminalBlock>\npnpm install -D babel-plugin-react-compiler@latest\n</TerminalBlock>\n\n## 기본 설정 {/*basic-setup*/}\n\nReact 컴파일러는 기본적으로 별도의 설정 없이 작동하도록 설계되었습니다. 하지만 특수한 상황에서 설정이 필요한 경우(예를 들어 React 19 미만 버전을 타깃으로 하는 경우) [컴파일러 옵션 레퍼런스](/reference/react-compiler/configuration)를 참고하세요.\n\n설정 과정은 빌드 도구에 따라 다릅니다. React 컴파일러는 빌드 파이프라인과 통합되는 Babel 플러그인을 포함하고 있습니다.\n\n<Pitfall>\nReact 컴파일러는 Babel 플러그인 파이프라인에서 **가장 먼저** 실행되어야 합니다. 컴파일러는 적절한 분석을 위해 원본 소스 정보가 필요하므로, 다른 변환보다 먼저 코드를 처리해야 합니다.\n</Pitfall>\n\n### Babel {/*babel*/}\n\n`babel.config.js`를 생성하거나 업데이트합니다.\n\n```js {3}\nmodule.exports = {\n  plugins: [\n    'babel-plugin-react-compiler', // 가장 먼저 실행되어야 합니다!\n    // ... 다른 플러그인\n  ],\n  // ... 다른 설정\n};\n```\n\n### Vite {/*vite*/}\n\nVite를 사용하는 경우 `vite-plugin-react`에 플러그인을 추가할 수 있습니다.\n\n```js {3,9}\n// vite.config.js\nimport { defineConfig } from 'vite';\nimport react from '@vitejs/plugin-react';\n\nexport default defineConfig({\n  plugins: [\n    react({\n      babel: {\n        plugins: ['babel-plugin-react-compiler'],\n      },\n    }),\n  ],\n});\n```\n\n또는 Vite용 별도의 Babel 플러그인을 선호하는 경우\n\n<TerminalBlock>\nnpm install -D vite-plugin-babel\n</TerminalBlock>\n\n```js {2,11}\n// vite.config.js\nimport babel from 'vite-plugin-babel';\nimport { defineConfig } from 'vite';\nimport react from '@vitejs/plugin-react';\n\nexport default defineConfig({\n  plugins: [\n    react(),\n    babel({\n      babelConfig: {\n        plugins: ['babel-plugin-react-compiler'],\n      },\n    }),\n  ],\n});\n```\n\n### Next.js {/*usage-with-nextjs*/}\n\n자세한 내용은 [Next.js 문서](https://nextjs.org/docs/app/api-reference/next-config-js/reactCompiler)를 참고하세요.\n\n### React Router {/*usage-with-react-router*/}\n`vite-plugin-babel`을 설치하고, 컴파일러의 Babel 플러그인을 추가합니다.\n\n<TerminalBlock>\nnpm install vite-plugin-babel\n</TerminalBlock>\n\n```js {3-4,16}\n// vite.config.js\nimport { defineConfig } from \"vite\";\nimport babel from \"vite-plugin-babel\";\nimport { reactRouter } from \"@react-router/dev/vite\";\n\nconst ReactCompilerConfig = { /* ... */ };\n\nexport default defineConfig({\n  plugins: [\n    reactRouter(),\n    babel({\n      filter: /\\.[jt]sx?$/,\n      babelConfig: {\n        presets: [\"@babel/preset-typescript\"], // TypeScript를 사용하는 경우\n        plugins: [\n          [\"babel-plugin-react-compiler\", ReactCompilerConfig],\n        ],\n      },\n    }),\n  ],\n});\n```\n\n### Webpack {/*usage-with-webpack*/}\n\n커뮤니티에서 제공하는 webpack loader는 [여기에서 확인할 수 있습니다](https://github.com/SukkaW/react-compiler-webpack).\n\n### Expo {/*usage-with-expo*/}\n\nExpo 앱에서 React 컴파일러를 활성화하고 사용하는 방법은 [Expo 문서](https://docs.expo.dev/guides/react-compiler/)를 참고하세요.\n\n### Metro (React Native) {/*usage-with-react-native-metro*/}\n\nReact Native는 Metro를 통해 Babel을 사용하므로, 설치 방법은 [Babel 사용법](#babel) 섹션을 참고하세요.\n\n### Rspack {/*usage-with-rspack*/}\n\nRspack 앱에서 React 컴파일러를 활성화하고 사용하는 방법은 [Rspack 문서](https://rspack.dev/guide/tech/react#react-compiler)를 참고하세요.\n\n### Rsbuild {/*usage-with-rsbuild*/}\n\nRsbuild 앱에서 React 컴파일러를 활성화하고 사용하는 방법은 [Rsbuild 문서](https://rsbuild.dev/guide/framework/react#react-compiler)를 참고하세요.\n\n\n## ESLint 연동 {/*eslint-integration*/}\n\nReact 컴파일러는 최적화할 수 없는 코드를 식별하는 데 도움이 되는 ESLint 규칙을 포함합니다. ESLint 규칙이 오류를 보고하면 컴파일러가 해당 특정 컴포넌트나 Hook의 최적화를 건너뜁니다. 이는 안전합니다. 컴파일러는 코드베이스의 다른 부분을 계속 최적화합니다. 모든 위반 사항을 즉시 수정할 필요는 없습니다. 원하는 속도로 해결하면서 최적화되는 컴포넌트의 수를 점진적으로 늘려가세요.\n\nESLint 플러그인을 설치합니다.\n\n<TerminalBlock>\nnpm install -D eslint-plugin-react-hooks@latest\n</TerminalBlock>\n\n`eslint-plugin-react-hooks`를 아직 설정하지 않았다면 [readme의 설치 지침](https://github.com/facebook/react/blob/main/packages/eslint-plugin-react-hooks/README.md#installation)을 따르세요. 컴파일러 규칙은 `recommended-latest` 프리셋에서 사용할 수 있습니다.\n\nESLint 규칙은 다음과 같은 역할을 합니다.\n- [React 규칙](/reference/rules) 위반 식별\n- 최적화할 수 없는 컴포넌트 표시\n- 문제 해결에 도움이 되는 오류 메시지 제공\n\n## 설정 확인 {/*verify-your-setup*/}\n\n설치 후 React 컴파일러가 올바르게 작동하는지 확인합니다.\n\n### React DevTools 확인 {/*check-react-devtools*/}\n\nReact 컴파일러에 의해 최적화된 컴포넌트는 React DevTools에서 \"Memo ✨\" 배지가 표시됩니다.\n\n1. [React Developer Tools](/learn/react-developer-tools) 브라우저 확장 프로그램을 설치합니다.\n2. 개발 모드에서 앱을 엽니다.\n3. React DevTools를 엽니다.\n4. 컴포넌트 이름 옆에 ✨ 이모지가 있는지 확인합니다.\n\n컴파일러가 작동하는 경우\n- 컴포넌트에 \"Memo ✨\" 배지가 React DevTools에 표시됩니다.\n- 비용이 큰 계산이 자동으로 메모이제이션됩니다.\n- 수동으로 `useMemo`를 사용할 필요가 없습니다.\n\n### 빌드 출력 확인 {/*check-build-output*/}\n\n빌드 출력을 확인하여 컴파일러가 실행되고 있는지 확인할 수도 있습니다. 컴파일된 코드에는 컴파일러가 자동으로 추가하는 자동 메모이제이션 로직이 포함됩니다.\n\n```js\nimport { c as _c } from \"react/compiler-runtime\";\nexport default function MyApp() {\n  const $ = _c(1);\n  let t0;\n  if ($[0] === Symbol.for(\"react.memo_cache_sentinel\")) {\n    t0 = <div>Hello World</div>;\n    $[0] = t0;\n  } else {\n    t0 = $[0];\n  }\n  return t0;\n}\n\n```\n\n## 문제 해결 {/*troubleshooting*/}\n\n### 특정 컴포넌트 제외하기 {/*opting-out-specific-components*/}\n\n컴파일 후 특정 컴포넌트에서 문제가 발생하면 `\"use no memo\"` 지시어를 사용하여 일시적으로 해당 컴포넌트를 제외할 수 있습니다.\n\n```js\nfunction ProblematicComponent() {\n  \"use no memo\";\n  // 컴포넌트 코드\n}\n```\n\n이렇게 하면 컴파일러에게 이 특정 컴포넌트의 최적화를 건너뛰도록 지시합니다. 근본적인 문제를 해결한 후 지시어를 제거해야 합니다.\n\n더 많은 문제 해결 도움말은 [디버깅 가이드](/learn/react-compiler/debugging)를 참고하세요.\n\n## 다음 단계 {/*next-steps*/}\n\nReact 컴파일러를 설치했으니, 다음 내용을 자세히 알아보세요.\n\n- React 17과 18을 위한 [React 버전 호환성](/reference/react-compiler/target)\n- 컴파일러를 사용자 정의하기 위한 [설정 옵션](/reference/react-compiler/configuration)\n- 기존 코드베이스를 위한 [점진적 도입 전략](/learn/react-compiler/incremental-adoption)\n- 문제 해결을 위한 [디버깅 기법](/learn/react-compiler/debugging)\n- React 라이브러리 컴파일을 위한 [라이브러리 컴파일 가이드](/reference/react-compiler/compiling-libraries)\n"
  },
  {
    "path": "src/content/learn/react-compiler/introduction.md",
    "content": "---\ntitle: 소개\n---\n\n<Intro>\nReact 컴파일러는 React 앱을 자동으로 최적화하는 새로운 빌드 타임 도구입니다. 일반 JavaScript와 함께 작동하며 [React의 규칙](/reference/rules)을 이해하므로 코드를 다시 작성할 필요 없이 사용할 수 있습니다.\n</Intro>\n\n<YouWillLearn>\n\n* React 컴파일러가 하는 일\n* 컴파일러 시작하기\n* 점진적 도입 전략\n* 문제가 발생했을 때 디버깅 및 문제 해결\n* React 라이브러리에서 컴파일러 사용하기\n\n</YouWillLearn>\n\n## React 컴파일러는 무엇을 하나요? {/*what-does-react-compiler-do*/}\n\nReact 컴파일러는 빌드 시점에 React 애플리케이션을 자동으로 최적화합니다. React는 최적화 없이도 충분히 빠른 경우가 많지만, 때로는 앱의 반응성을 유지하기 위해 컴포넌트와 값을 수동으로 메모이제이션해야 할 때가 있습니다. 이러한 수동 메모이제이션은 지루하고 실수하기 쉬우며 유지보수해야 할 추가 코드가 생깁니다. React 컴파일러는 이 최적화를 자동으로 수행하여 정신적 부담을 덜어주므로 기능 구현에 집중할 수 있습니다.\n\n### React 컴파일러 이전 {/*before-react-compiler*/}\n\n컴파일러 없이는 리렌더링을 최적화하기 위해 컴포넌트와 값을 수동으로 메모이제이션해야 합니다.\n\n```js\nimport { useMemo, useCallback, memo } from 'react';\n\nconst ExpensiveComponent = memo(function ExpensiveComponent({ data, onClick }) {\n  const processedData = useMemo(() => {\n    return expensiveProcessing(data);\n  }, [data]);\n\n  const handleClick = useCallback((item) => {\n    onClick(item.id);\n  }, [onClick]);\n\n  return (\n    <div>\n      {processedData.map(item => (\n        <Item key={item.id} onClick={() => handleClick(item)} />\n      ))}\n    </div>\n  );\n});\n```\n\n\n<Note>\n\n이 수동 메모이제이션에는 메모이제이션을 깨뜨리는 미묘한 버그가 있습니다.\n\n```js [[2, 1, \"() => handleClick(item)\"]]\n<Item key={item.id} onClick={() => handleClick(item)} />\n```\n\n`handleClick`이 `useCallback`으로 감싸져 있더라도, 화살표 함수 `() => handleClick(item)`은 컴포넌트가 렌더링될 때마다 새 함수를 생성합니다. 이는 `Item`이 항상 새로운 `onClick` prop을 받게 되어 메모이제이션이 깨진다는 것을 의미합니다.\n\nReact 컴파일러는 화살표 함수 유무와 관계없이 이를 올바르게 최적화하여 `props.onClick`이 변경될 때만 `Item`이 리렌더링되도록 합니다.\n\n</Note>\n\n### React 컴파일러 이후 {/*after-react-compiler*/}\n\nReact 컴파일러를 사용하면 수동 메모이제이션 없이 동일한 코드를 작성할 수 있습니다.\n\n```js\nfunction ExpensiveComponent({ data, onClick }) {\n  const processedData = expensiveProcessing(data);\n\n  const handleClick = (item) => {\n    onClick(item.id);\n  };\n\n  return (\n    <div>\n      {processedData.map(item => (\n        <Item key={item.id} onClick={() => handleClick(item)} />\n      ))}\n    </div>\n  );\n}\n```\n\n_[React 컴파일러 Playground에서 이 예시 보기](https://playground.react.dev/#N4Igzg9grgTgxgUxALhAMygOzgFwJYSYAEAogB4AOCmYeAbggMIQC2Fh1OAFMEQCYBDHAIA0RQowA2eOAGsiAXwCURYAB1iROITA4iFGBERgwCPgBEhAogF4iCStVoMACoeO1MAcy6DhSgG4NDSItHT0ACwFMPkkmaTlbIi48HAQWFRsAPlUQ0PFMKRlZFLSWADo8PkC8hSDMPJgEHFhiLjzQgB4+eiyO-OADIwQTM0thcpYBClL02xz2zXz8zoBJMqJZBABPG2BU9Mq+BQKiuT2uTJyomLizkoOMk4B6PqX8pSUFfs7nnro3qEapgFCAFEA)_\n\nReact 컴파일러는 자동으로 최적의 메모이제이션을 적용하여 앱이 필요할 때만 리렌더링되도록 합니다.\n\n<DeepDive>\n#### React 컴파일러는 어떤 종류의 메모이제이션을 추가하나요? {/*what-kind-of-memoization-does-react-compiler-add*/}\n\nReact 컴파일러의 자동 메모이제이션은 주로 **업데이트 성능 개선**(기존 컴포넌트의 리렌더링)에 초점을 맞추고 있으므로, 다음 두 가지 사용 사례에 집중합니다.\n\n1. **컴포넌트의 연쇄적인 리렌더링 건너뛰기**\n    * `<Parent />`를 리렌더링하면 `<Parent />`만 변경되었더라도 컴포넌트 트리의 많은 컴포넌트가 리렌더링됩니다.\n1. **React 외부의 비용이 많이 드는 계산 건너뛰기**\n    * 예를 들어, 해당 데이터가 필요한 컴포넌트나 Hook 내부에서 `expensivelyProcessAReallyLargeArrayOfObjects()`를 호출하는 경우\n\n#### 리렌더링 최적화 {/*optimizing-re-renders*/}\n\nReact는 UI를 현재 state의 함수로 표현할 수 있게 해줍니다 (더 구체적으로는 props, state, context). 현재 구현에서 컴포넌트의 state가 변경되면 React는 `useMemo()`, `useCallback()`, `React.memo()`를 사용한 수동 메모이제이션을 적용하지 않는 한 해당 컴포넌트 <em>및 모든 자식 컴포넌트</em>를 리렌더링합니다. 예를 들어, 다음 예시에서 `<MessageButton>`은 `<FriendList>`의 state가 변경될 때마다 리렌더링됩니다.\n\n```javascript\nfunction FriendList({ friends }) {\n  const onlineCount = useFriendOnlineCount();\n  if (friends.length === 0) {\n    return <NoFriends />;\n  }\n  return (\n    <div>\n      <span>{onlineCount} online</span>\n      {friends.map((friend) => (\n        <FriendListCard key={friend.id} friend={friend} />\n      ))}\n      <MessageButton />\n    </div>\n  );\n}\n```\n[_React 컴파일러 Playground에서 이 예시 보기_](https://playground.react.dev/#N4Igzg9grgTgxgUxALhAMygOzgFwJYSYAEAYjHgpgCYAyeYOAFMEWuZVWEQL4CURwADrEicQgyKEANnkwIAwtEw4iAXiJQwCMhWoB5TDLmKsTXgG5hRInjRFGbXZwB0UygHMcACzWr1ABn4hEWsYBBxYYgAeADkIHQ4uAHoAPksRbisiMIiYYkYs6yiqPAA3FMLrIiiwAAcAQ0wU4GlZBSUcbklDNqikusaKkKrgR0TnAFt62sYHdmp+VRT7SqrqhOo6Bnl6mCoiAGsEAE9VUfmqZzwqLrHqM7ubolTVol5eTOGigFkEMDB6u4EAAhKA4HCEZ5DNZ9ErlLIWYTcEDcIA)\n\nReact 컴파일러는 수동 메모이제이션과 동등한 것을 자동으로 적용하여 state가 변경될 때 앱의 관련 부분만 리렌더링되도록 합니다. 이를 때때로 \"세밀한 반응성\"이라고 합니다. 위의 예시에서 React 컴파일러는 `friends`가 변경되더라도 `<FriendListCard />`의 반환 값을 재사용할 수 있다고 판단하고, 이 JSX를 다시 생성하지 않으며 count가 변경될 때 `<MessageButton>`의 리렌더링을 피할 수 있습니다.\n\n#### 비용이 많이 드는 계산도 메모이제이션됩니다 {/*expensive-calculations-also-get-memoized*/}\n\nReact 컴파일러는 렌더링 중에 사용되는 비용이 많이 드는 계산도 자동으로 메모이제이션할 수 있습니다.\n\n```js\n// 컴포넌트나 Hook이 아니므로 React 컴파일러가 메모이제이션하지 **않음**\nfunction expensivelyProcessAReallyLargeArrayOfObjects() { /* ... */ }\n\n// 컴포넌트이므로 React 컴파일러가 메모이제이션함\nfunction TableContainer({ items }) {\n  // 이 함수 호출이 메모이제이션됩니다.\n  const data = expensivelyProcessAReallyLargeArrayOfObjects(items);\n  // ...\n}\n```\n[_React 컴파일러 Playground에서 이 예시 보기_](https://playground.react.dev/#N4Igzg9grgTgxgUxALhAejQAgFTYHIQAuumAtgqRAJYBeCAJpgEYCemASggIZyGYDCEUgAcqAGwQwANJjBUAdokyEAFlTCZ1meUUxdMcIcIjyE8vhBiYVECAGsAOvIBmURYSonMCAB7CzcgBuCGIsAAowEIhgYACCnFxioQAyXDAA5gixMDBcLADyzvlMAFYIvGAAFACUmMCYaNiYAHStOFgAvk5OGJgAshTUdIysHNy8AkbikrIKSqpaWvqGIiZmhE6u7p7ymAAqXEwSguZcCpKV9VSEFBodtcBOmAYmYHz0XIT6ALzefgFUYKhCJRBAxeLcJIsVIZLI5PKFYplCqVa63aoAbm6u0wMAQhFguwAPPRAQA+YAfL4dIloUmBMlODogDpAA)\n\n그러나 `expensivelyProcessAReallyLargeArrayOfObjects`가 정말로 비용이 많이 드는 함수라면, 다음과 같은 이유로 React 외부에서 자체 메모이제이션을 구현하는 것을 고려해야 할 수 있습니다.\n\n- React 컴파일러는 모든 함수가 아닌 React 컴포넌트와 Hook만 메모이제이션합니다.\n- React 컴파일러의 메모이제이션은 여러 컴포넌트나 Hook 간에 공유되지 않습니다.\n\n따라서 `expensivelyProcessAReallyLargeArrayOfObjects`가 여러 다른 컴포넌트에서 사용되는 경우, 정확히 동일한 `items`가 전달되더라도 비용이 많이 드는 계산이 반복적으로 실행됩니다. 코드를 더 복잡하게 만들기 전에 먼저 [프로파일링](reference/react/useMemo#how-to-tell-if-a-calculation-is-expensive)하여 정말로 비용이 많이 드는지 확인하는 것을 권장합니다.\n</DeepDive>\n\n## 컴파일러를 사용해 봐야 하나요? {/*should-i-try-out-the-compiler*/}\n\nReact 컴파일러를 사용해 보길 권장합니다. 현재 컴파일러는 React에 대한 선택적 추가 기능이지만, 미래에는 일부 기능이 완전히 작동하기 위해 컴파일러가 필요할 수 있습니다.\n\n### 사용해도 안전한가요? {/*is-it-safe-to-use*/}\n\nReact 컴파일러는 이제 안정적이며 프로덕션에서 광범위하게 테스트되었습니다. Meta와 같은 회사에서 프로덕션에 사용되었지만, 앱의 프로덕션에 컴파일러를 배포하는 것은 코드베이스의 건강 상태와 [React의 규칙](/reference/rules)을 얼마나 잘 따랐는지에 따라 달라집니다.\n\n## 어떤 빌드 도구가 지원되나요? {/*what-build-tools-are-supported*/}\n\nReact 컴파일러는 Babel, Vite, Metro, Rsbuild와 같은 [여러 빌드 도구](/learn/react-compiler/installation)에 설치할 수 있습니다.\n\nReact 컴파일러는 주로 핵심 컴파일러를 감싸는 가벼운 Babel 플러그인 래퍼로, Babel 자체와 분리되도록 설계되었습니다. 컴파일러의 초기 안정 버전은 주로 Babel 플러그인으로 유지되지만, swc 및 [oxc](https://github.com/oxc-project/oxc/issues/10048) 팀과 협력하여 React 컴파일러에 대한 일급 지원을 구축하고 있어 향후 빌드 파이프라인에 Babel을 다시 추가할 필요가 없을 것입니다.\n\nNext.js 사용자는 [v15.3.1](https://github.com/vercel/next.js/releases/tag/v15.3.1) 이상을 사용하여 swc로 호출되는 React 컴파일러를 활성화할 수 있습니다.\n\n## useMemo, useCallback, React.memo는 어떻게 해야 하나요? {/*what-should-i-do-about-usememo-usecallback-and-reactmemo*/}\n\n기본적으로 React 컴파일러는 분석과 휴리스틱을 기반으로 코드를 메모이제이션합니다. 대부분의 경우 이 메모이제이션은 직접 작성한 것만큼 정확하거나 더 정확할 것입니다.\n\n그러나 일부 경우에는 개발자가 메모이제이션에 대해 더 많은 제어가 필요할 수 있습니다. `useMemo`와 `useCallback` Hook은 어떤 값이 메모이제이션되는지에 대한 제어를 제공하는 탈출구로 React 컴파일러와 함께 계속 사용할 수 있습니다. 일반적인 사용 사례는 메모이제이션된 값이 Effect 의존성으로 사용되어 의존성이 의미 있게 변경되지 않더라도 Effect가 반복적으로 실행되지 않도록 하는 경우입니다.\n\n새 코드의 경우, 메모이제이션은 컴파일러에 의존하고 정밀한 제어가 필요한 곳에서 `useMemo`/`useCallback`을 사용하는 것을 권장합니다.\n\n기존 코드의 경우, 기존 메모이제이션을 그대로 두거나(제거하면 컴파일 출력이 변경될 수 있음) 메모이제이션을 제거하기 전에 신중하게 테스트하는 것을 권장합니다.\n\n## React 컴파일러 사용해 보기 {/*try-react-compiler*/}\n\n이 섹션은 React 컴파일러를 시작하고 프로젝트에서 효과적으로 사용하는 방법을 이해하는 데 도움이 됩니다.\n\n* **[설치](/learn/react-compiler/installation)** - React 컴파일러를 설치하고 빌드 도구에 맞게 구성하기\n* **[React 버전 호환성](/reference/react-compiler/target)** - React 17, 18, 19 지원\n* **[설정](/reference/react-compiler/configuration)** - 특정 요구 사항에 맞게 컴파일러 커스텀하기\n* **[점진적 도입](/learn/react-compiler/incremental-adoption)** - 기존 코드베이스에서 컴파일러를 점진적으로 배포하기 위한 전략\n* **[디버깅 및 문제 해결](/learn/react-compiler/debugging)** - 컴파일러 사용 시 문제 식별 및 해결\n* **[라이브러리 컴파일](/reference/react-compiler/compiling-libraries)** - 컴파일된 코드 배포를 위한 모범 사례\n* **[API 레퍼런스](/reference/react-compiler/configuration)** - 모든 설정 옵션에 대한 자세한 문서\n\n## 추가 리소스 {/*additional-resources*/}\n\n이 문서 외에도 컴파일러에 대한 추가 정보와 논의를 위해 [React Compiler Working Group](https://github.com/reactwg/react-compiler)을 확인하는 것을 권장합니다.\n\n"
  },
  {
    "path": "src/content/learn/react-developer-tools.md",
    "content": "---\ntitle: React 개발자 도구\n---\n\n<Intro>\n\nReact 개발자 도구를 사용하여 React [컴포넌트](/learn/your-first-component)를 검사하고 [Props](/learn/passing-props-to-a-component)와 [State](/learn/state-a-components-memory)를 편집할 수 있으며 성능 문제를 식별할 수 있습니다.\n\n</Intro>\n\n<YouWillLearn>\n\n* React 개발자 도구 설치 방법\n\n</YouWillLearn>\n\n## 브라우저 확장 프로그램 {/*browser-extension*/}\n\nReact로 빌드된 웹 사이트를 디버깅하는 가장 쉬운 방법은 React 개발자 도구 브라우저 확장 프로그램을 설치하는 것입니다. 널리 사용되는 여러 브라우저에서 사용할 수 있습니다.\n\n* [**Chrome**용으로 설치](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi?hl=en)\n* [**Firefox**용으로 설치](https://addons.mozilla.org/en-US/firefox/addon/react-devtools/)\n* [**Edge**용으로 설치](https://microsoftedge.microsoft.com/addons/detail/react-developer-tools/gpphkfbcpidddadnkolkpfckpihlkkil)\n\n설치가 완료된 후 **React로 빌드된** 사이트에 방문하면 _Components_ and _Profiler_ 패널이 표시됩니다.\n\n![React 개발자 도구 확장 프로그램](/images/docs/react-devtools-extension.png)\n\n### Safari 및 기타 브라우저 {/*safari-and-other-browsers*/}\n다른 브라우저(예: Safari)의 경우, [`react-devtools`](https://www.npmjs.com/package/react-devtools)를 npm 패키지로 설치해야 합니다.\n```bash\n# Yarn\nyarn global add react-devtools\n\n# npm\nnpm install -g react-devtools\n```\n\n다음으로 터미널에서 개발자 도구를 엽니다.\n```bash\nreact-devtools\n```\n\n다음으로 웹 사이트의 `<head>`의 `<script>` 태그를 통해 웹 사이트를 연결합니다.\n```html {3}\n<html>\n  <head>\n    <script src=\"http://localhost:8097\"></script>\n```\n\n브라우저를 새로고침하면 개발자 도구를 확인할 수 있습니다.\n\n![React Developer Tools standalone](/images/docs/react-devtools-standalone.png)\n\n## 모바일 (React Native) {/*mobile-react-native*/}\n\n[React Native](https://reactnative.dev/)로 만든 앱은 React Developer Tools가 내장된 [React Native DevTools](https://reactnative.dev/docs/react-native-devtools)를 통해 디버깅할 수 있습니다. 네이티브 요소를 강조하거나 선택하는 기능을 포함한 브라우저 확장 프로그램에서 사용하던 모든 기능을 동일하게 사용할 수 있습니다.\n\n[React Native에서 디버깅하는 방법 더 알아보기](https://reactnative.dev/docs/debugging).\n\n> 참고: React Native 0.76 이전 버전을 사용하는 경우, 위의 [Safari 및 기타 브라우저](#safari-and-other-browsers) 가이드를 참고하여 독립형 React DevTools를 사용하세요.\n"
  },
  {
    "path": "src/content/learn/reacting-to-input-with-state.md",
    "content": "---\ntitle: State를 사용해 Input 다루기\n---\n\n<Intro>\n\nReact는 선언적인 방식으로 UI를 조작합니다. 개별적인 UI를 직접 조작하는 것 대신에 컴포넌트 내부에 여러 state를 묘사하고 사용자의 입력에 따라 state를 변경합니다. 이는 디자이너가 UI를 바라보는 방식과 비슷합니다.\n\n</Intro>\n\n<YouWillLearn>\n\n* 명령형 UI 프로그래밍이 아닌 선언형 UI 프로그래밍을 하는 방법\n* 컴포넌트에 들어갈 수 있는 다양한 시각적 state를 열거하는 방법\n* 코드에서 다른 시각적 state 간의 변경을 트리거 하는 방법\n\n</YouWillLearn>\n\n## 선언형 UI와 명령형 UI 비교 {/*how-declarative-ui-compares-to-imperative*/}\n\nUI 상호작용을 디자인할 때 사용자가 액션을 하면 어떻게 UI를 *변경* 해야 할지 고민해본 적이 있을 것입니다. 사용자가 폼을 제출한다고 생각해봅시다.\n\n* 폼에 무언가를 입력하면 \"제출\" 버튼이 **활성화될** 것입니다.\n* \"제출\" 버튼을 누르면 폼과 버튼이 **비활성화되고** 스피너가 **나타날** 것입니다.\n* 네트워크 요청이 성공하면 폼은 **숨겨질** 것이고 \"감사합니다.\" 메시지가 **나타날** 것입니다.\n* 네트워크 요청이 실패하면 오류 메시지가 **보일** 것이고 폼은 다시 **활성화될** 것입니다.\n\n위 내용은 **명령형 프로그래밍**에서 상호작용을 구현하는 방법입니다. UI를 조작하기 위해서는 발생한 상황에 따라 정확한 지침을 작성해야만 합니다. 다른 방법을 한번 생각해봅시다. 옆에 누군가를 태우고 차례대로 어디를 가야 할지 말해준다고 상상해보세요.\n\n<Illustration src=\"/images/docs/illustrations/i_imperative-ui-programming.png\"  alt=\"In a car driven by an anxious-looking person representing JavaScript, a passenger orders the driver to execute a sequence of complicated turn by turn navigations.\" />\n\n옆에 탄 운전기사는 당신이 어디를 가고싶어 하는지 모릅니다. 그저 명령에 따를 뿐이죠. (잘못된 지시를 하면 잘못된 곳에 도착하고 말겁니다.) 우리가 이것을 *명령형*이라 부르는 이유입니다. 왜냐하면 컴퓨터에게 스피너부터 버튼까지 각각의 요소에 UI를 *어떻게* 업데이트 해야할지 \"명령\"을 내려야하기 때문이죠.\n\n아래의 명령형 UI 프로그래밍 예시는 React *없이* 만들어진 폼입니다. 브라우저에 내장된 [DOM](https://developer.mozilla.org/ko/docs/Web/API/Document_Object_Model)을 사용했습니다.\n\n<Sandpack>\n\n```js src/index.js active\nasync function handleFormSubmit(e) {\n  e.preventDefault();\n  disable(textarea);\n  disable(button);\n  show(loadingMessage);\n  hide(errorMessage);\n  try {\n    await submitForm(textarea.value);\n    show(successMessage);\n    hide(form);\n  } catch (err) {\n    show(errorMessage);\n    errorMessage.textContent = err.message;\n  } finally {\n    hide(loadingMessage);\n    enable(textarea);\n    enable(button);\n  }\n}\n\nfunction handleTextareaChange() {\n  if (textarea.value.length === 0) {\n    disable(button);\n  } else {\n    enable(button);\n  }\n}\n\nfunction hide(el) {\n  el.style.display = 'none';\n}\n\nfunction show(el) {\n  el.style.display = '';\n}\n\nfunction enable(el) {\n  el.disabled = false;\n}\n\nfunction disable(el) {\n  el.disabled = true;\n}\n\nfunction submitForm(answer) {\n  // 네트워크에 접속한다고 가정해봅시다.\n  return new Promise((resolve, reject) => {\n    setTimeout(() => {\n      if (answer.toLowerCase() === 'istanbul') {\n        resolve();\n      } else {\n        reject(new Error('Good guess but a wrong answer. Try again!'));\n      }\n    }, 1500);\n  });\n}\n\nlet form = document.getElementById('form');\nlet textarea = document.getElementById('textarea');\nlet button = document.getElementById('button');\nlet loadingMessage = document.getElementById('loading');\nlet errorMessage = document.getElementById('error');\nlet successMessage = document.getElementById('success');\nform.onsubmit = handleFormSubmit;\ntextarea.oninput = handleTextareaChange;\n```\n\n```js sandbox.config.json hidden\n{\n  \"hardReloadOnChange\": true\n}\n```\n\n```html public/index.html\n<form id=\"form\">\n  <h2>City quiz</h2>\n  <p>\n    What city is located on two continents?\n  </p>\n  <textarea id=\"textarea\"></textarea>\n  <br />\n  <button id=\"button\" disabled>Submit</button>\n  <p id=\"loading\" style=\"display: none\">Loading...</p>\n  <p id=\"error\" style=\"display: none; color: red;\"></p>\n</form>\n<h1 id=\"success\" style=\"display: none\">That's right!</h1>\n\n<style>\n* { box-sizing: border-box; }\nbody { font-family: sans-serif; margin: 20px; padding: 0; }\n</style>\n```\n\n</Sandpack>\n\n위의 예시에서는 문제가 없겠지만 위와 같이 UI를 조작하면 더 복잡한 시스템에서는 난이도가 기하급수적으로 올라갑니다. 여러 다른 폼으로 가득 찬 페이지를 업데이트해야 한다고 생각해보세요. 새로운 UI 요소나 새로운 상호작용을 추가하려면 버그의 발생을 막기 위해 기존의 모든 코드를 주의 깊게 살펴봐야만 할 겁니다. (예를 들면 어떤 것을 보여주거나 숨기거나 하는 것을 잊을지도 모릅니다).\n\nReact는 이러한 문제점을 해결하기 위해 만들어졌습니다.\n\nReact에서는 직접 UI를 조작할 필요가 없습니다. 컴포넌트를 직접 활성화하거나 비활성화하거나 보여주거나 숨길 필요가 없습니다. 대신에 **무엇을 보여주고 싶은지 선언**하기만 하면 됩니다. 그러면 React는 어떻게 UI를 업데이트 해야 할지 이해할 것입니다. 택시를 탄다고 생각해 봅시다. 운전기사에게 어디서 꺾어야 할지 알려주는게 아니라 가고 싶은 곳을 말한다고 생각해 보세요. 당신을 거기까지 데려다주는 것은 운전기사의 일이고 운전기사는 어쩌면 당신이 몰랐던 지름길을 알고 있을지도 모릅니다!\n\n<Illustration src=\"/images/docs/illustrations/i_declarative-ui-programming.png\" alt=\"In a car driven by React, a passenger asks to be taken to a specific place on the map. React figures out how to do that.\" />\n\n## UI를 선언적인 방식으로 생각하기 {/*thinking-about-ui-declaratively*/}\n\n지금까지 폼을 선언적인 방식으로 구현하는 방법을 살펴보았습니다. React처럼 생각하는 방법을 더 잘 이해하기 위해 UI를 React에서 다시 구현하는 과정을 아래에서 살펴봅시다.\n\n1. 컴포넌트의 다양한 시각적 state를 **확인하세요.**\n2. 무엇이 state 변화를 트리거하는지 **알아내세요.**\n3. `useState`를 사용해서 메모리의 state를 **표현하세요.**\n4. 불필요한 state 변수를 **제거하세요.**\n5. state 설정을 위해 이벤트 핸들러를 **연결하세요.**\n\n## 첫 번째: 컴포넌트의 다양한 시각적 state 확인하기 {/*step-1-identify-your-components-different-visual-states*/}\n\n여러가지 \"state\"를 가지고 있는 [\"상태 기계\"](https://ko.wikipedia.org/wiki/유한_상태_기계)라는 것을 컴퓨터 과학에서 들어본 적이 있을 것입니다. 그리고 디자이너와 일한다면 다양한 \"시각적 state\"에 관한 모형을 본 적이 있을 것입니다. React는 디자인과 컴퓨터 과학의 사이에 있기 때문에 두 아이디어 모두에서 영감을 받았습니다.\n\n먼저 사용자가 볼 수 있는 UI의 모든 \"state\"를 시각화해야 합니다.\n\n* **Empty**: 폼은 비활성화된 \"제출\" 버튼을 가지고 있다.\n* **Typing**: 폼은 활성화된 \"제출\" 버튼을 가지고 있다.\n* **Submitting**: 폼은 완전히 비활성화되고 스피너가 보인다.\n* **Success**: 폼 대신에 \"감사합니다\" 메시지가 보인다.\n* **Error**: \"Typing\" state와 동일하지만 오류 메시지가 보인다.\n\n디자이너처럼 로직을 추가하기 전에 여러 state를 \"목업\" 하거나 \"모형\"을 만들고 싶을 것입니다. 여기에 폼의 생김새와 관련된 모형이 있다고 생각해봅시다. 이 모형은 기본값이 `'empty'`인 `status` prop에 의해 컨트롤됩니다.\n\n<Sandpack>\n\n```js\nexport default function Form({\n  status = 'empty'\n}) {\n  if (status === 'success') {\n    return <h1>That's right!</h1>\n  }\n  return (\n    <>\n      <h2>City quiz</h2>\n      <p>\n        In which city is there a billboard that turns air into drinkable water?\n      </p>\n      <form>\n        <textarea />\n        <br />\n        <button>\n          Submit\n        </button>\n      </form>\n    </>\n  )\n}\n```\n\n</Sandpack>\n\n여기서 prop을 네이밍하는 것은 중요하지 않습니다. 원하는 이름을 붙이면 됩니다. `status = 'empty'`를 `status = 'success'`로 변경하고 성공 메시지가 나타나는 것을 보세요. 모형을 만듦으로써 로직을 연결하기 전에 UI에서 빠르게 테스트해볼 수 있습니다. 다음은 `status` prop에 의해 \"컨트롤\"되는 조금 더 구체적인 프로토타입입니다.\n\n<Sandpack>\n\n```js\nexport default function Form({\n  // 'submitting', 'error', 'success'로 한 번 변경해보세요:\n  status = 'empty'\n}) {\n  if (status === 'success') {\n    return <h1>That's right!</h1>\n  }\n  return (\n    <>\n      <h2>City quiz</h2>\n      <p>\n        In which city is there a billboard that turns air into drinkable water?\n      </p>\n      <form>\n        <textarea disabled={\n          status === 'submitting'\n        } />\n        <br />\n        <button disabled={\n          status === 'empty' ||\n          status === 'submitting'\n        }>\n          Submit\n        </button>\n        {status === 'error' &&\n          <p className=\"Error\">\n            Good guess but a wrong answer. Try again!\n          </p>\n        }\n      </form>\n      </>\n  );\n}\n```\n\n```css\n.Error { color: red; }\n```\n\n</Sandpack>\n\n<DeepDive>\n\n#### 많은 시각적 state를 한 번에 보여주기 {/*displaying-many-visual-states-at-once*/}\n\n컴포넌트가 많은 시각적 state를 가지고 있다면 한 페이지에서 모두 보여주는 것도 편하게 할 수 있습니다.\n\n<Sandpack>\n\n```js src/App.js active\nimport Form from './Form.js';\n\nlet statuses = [\n  'empty',\n  'typing',\n  'submitting',\n  'success',\n  'error',\n];\n\nexport default function App() {\n  return (\n    <>\n      {statuses.map(status => (\n        <section key={status}>\n          <h4>Form ({status}):</h4>\n          <Form status={status} />\n        </section>\n      ))}\n    </>\n  );\n}\n```\n\n```js src/Form.js\nexport default function Form({ status }) {\n  if (status === 'success') {\n    return <h1>That's right!</h1>\n  }\n  return (\n    <form>\n      <textarea disabled={\n        status === 'submitting'\n      } />\n      <br />\n      <button disabled={\n        status === 'empty' ||\n        status === 'submitting'\n      }>\n        Submit\n      </button>\n      {status === 'error' &&\n        <p className=\"Error\">\n          Good guess but a wrong answer. Try again!\n        </p>\n      }\n    </form>\n  );\n}\n```\n\n```css\nsection { border-bottom: 1px solid #aaa; padding: 20px; }\nh4 { color: #222; }\nbody { margin: 0; }\n.Error { color: red; }\n```\n\n</Sandpack>\n\n이런 페이지를 보통 \"살아있는 스타일가이드(living styleguides)\" 또는 \"스토리북(storybooks)\"이라고 부릅니다.\n\n</DeepDive>\n\n### 두 번째: 무엇이 state 변화를 트리거하는지 알아내기 {/*step-2-determine-what-triggers-those-state-changes*/}\n\n<IllustrationBlock title=\"Types of inputs\">\n  <Illustration caption=\"Human inputs\" alt=\"A finger.\" src=\"/images/docs/illustrations/i_inputs1.png\" />\n  <Illustration caption=\"Computer inputs\" alt=\"Ones and zeroes.\" src=\"/images/docs/illustrations/i_inputs2.png\" />\n</IllustrationBlock>\n\n두 종류의 인풋 유형으로 state 변경을 트리거할 수 있습니다.\n\n* 버튼을 누르거나, 필드를 입력하거나, 링크를 이동하는 것 등의 **휴먼 인풋**\n* 네트워크 응답이 오거나, 타임아웃이 되거나, 이미지를 로딩하거나 하는 등의 **컴퓨터 인풋**\n\n두 가지 경우 모두 **UI를 업데이트하기 위해서는 [state 변수](/learn/state-a-components-memory#anatomy-of-usestate)를 설정해야 합니다**. 지금 만들고 있는 폼의 경우 몇 가지 입력에 따라 state를 변경해야 합니다.\n\n* **텍스트 인풋을 변경하면** (휴먼) 텍스트 상자가 비어있는지 여부에 따라 state를 *Empty*에서 *Typing* 으로 또는 그 반대로 변경해야 합니다.\n* **제출 버튼을 클릭하면** (휴먼) *Submitting* state를 변경해야 합니다.\n* **네트워크 응답이 성공적으로 오면** (컴퓨터) *Success* state를 변경해야 합니다.\n* **네트워크 요청이 실패하면** (컴퓨터) 해당하는 오류 메시지와 함께 *Error* state를 변경해야 합니다.\n\n> 휴먼 인풋은 종종 [이벤트 핸들러](/learn/responding-to-events)가 필요할 수도 있다는 것도 기억하세요!\n\n이와 같은 흐름은 종이에 그려 시각화할 수 있습니다. 종이에 각각의 state를 라벨링 된 원으로 그리고 각각의 state 변화를 화살표로 이어보세요. 이러한 과정을 통해 state 변화의 흐름을 파악할 수 있을 뿐 아니라 구현 전에 버그를 찾을 수도 있습니다.\n\n<DiagramGroup>\n\n<Diagram name=\"responding_to_input_flow\" height={350} width={688} alt=\"Flow chart moving left to right with 5 nodes. The first node labeled 'empty' has one edge labeled 'start typing' connected to a node labeled 'typing'. That node has one edge labeled 'press submit' connected to a node labeled 'submitting', which has two edges. The left edge is labeled 'network error' connecting to a node labeled 'error'. The right edge is labeled 'network success' connecting to a node labeled 'success'.\">\n\nForm states\n\n</Diagram>\n\n</DiagramGroup>\n\n### 세 번째: 메모리의 state를 `useState`로 표현하기 {/*step-3-represent-the-state-in-memory-with-usestate*/}\n\n다음으로 [`useState`](/reference/react/useState)를 사용하여 컴포넌트의 시각적 state를 표현해야 합니다. 이 과정은 단순함이 핵심입니다. 각각의 state는 \"움직이는 조각\"입니다. 그리고 **\"움직이는 조각\"은 적을수록 좋습니다**. 복잡한 건 버그를 일으키기 마련입니다!\n\n먼저 *반드시 필요한* state를 가지고 시작해봅시다. 예를 들면 인풋의 `answer`은 반드시 저장해야 할 것입니다. 그리고 (존재한다면) 가장 최근에 발생한 `error`도 저장해야 할 겁니다.\n\n```js\nconst [answer, setAnswer] = useState('');\nconst [error, setError] = useState(null);\n```\n\n그리고 나서는 앞서 필요하다고 나열했던 나머지 시각적 state도 살펴봅시다. 보통은 어떤 state 변수를 사용할지에 대한 방법이 여러 가지기 때문에 이것저것 실험해볼 필요가 있습니다.\n\n좋은 방법이 곧바로 떠오르지 않는다면 가능한 모든 시각적 state를 커버할 수 있는 *확실한* 것을 먼저 추가하는 방식으로 시작하세요.\n\n```js\nconst [isEmpty, setIsEmpty] = useState(true);\nconst [isTyping, setIsTyping] = useState(false);\nconst [isSubmitting, setIsSubmitting] = useState(false);\nconst [isSuccess, setIsSuccess] = useState(false);\nconst [isError, setIsError] = useState(false);\n```\n\n처음으로 떠올린 생각이 최고의 방법은 아닐 수도 있습니다. 하지만 괜찮습니다. state를 리팩토링을 하는 것도 과정 중 하나니까요!\n\n### 네 번째: 불필요한 state 변수를 제거하기 {/*step-4-remove-any-non-essential-state-variables*/}\n\nstate의 중복은 피하고 필수적인 state만 남겨두고 싶을 겁니다. state 구조를 리팩토링하는 데 시간을 조금만 투자하면 컴포넌트는 더 이해하기 쉬워질 것이고 불필요한 중복은 줄어들 것이며 의도하지 않은 의미를 피할 수도 있을 것입니다. 리팩토링의 목표는 **state가 사용자에게 유효한 UI를 보여주지 않는 경우를 방지하는 것입니다.** (예를 들면 오류 메시지가 나타났는데 인풋이 비활성화 돼 있어 유저가 오류를 수정할 수 없는 상황은 원하지 않을 겁니다!)\n\n여기에 state 변수에 관한 몇 가지 질문이 있습니다.\n\n* **state가 역설을 일으키지는 않나요?** 예를 들면 `isTyping`과 `isSubmitting`이 동시에 `true`일 수는 없습니다. 이러한 역설은 보통 state가 충분히 제한되지 않았음을 의미합니다. 여기에는 두 boolean에 대한 네 가지 조합이 있지만 오직 유효한 state는 세 개뿐입니다. 이러한 \"불가능한\" state를 제거하기 위해 세 가지 값 `'typing'`, `'submitting'`, `'success'`을 하나의 `status`로 합칠 수 있습니다.\n* **다른 state 변수에 이미 같은 정보가 담겨있진 않나요?** `isEmpty`와 `isTyping`은 동시에 `true`가 될 수 없습니다. 이를 각각의 state 변수로 분리하면 싱크가 맞지 않거나 버그가 발생할 위험이 있습니다. 이 경우에는 운이 좋게도 `isEmpty`를 지우고 `answer.length === 0`으로 체크할 수 있습니다.\n* **다른 변수를 뒤집었을 때 같은 정보를 얻을 수 있진 않나요?** `isError`는 `error !== null`로도 대신 확인할 수 있기 때문에 필요하지 않습니다.\n\n이러한 정리 과정을 거친 후에는 세 가지 (일곱 개에서 줄어든!) *필수* 변수만 남게 됩니다.\n\n```js\nconst [answer, setAnswer] = useState('');\nconst [error, setError] = useState(null);\nconst [status, setStatus] = useState('typing'); // 'typing', 'submitting', or 'success'\n```\n\n어느 하나를 지웠을 때 정상적으로 작동하지 않는다는 점에서 이것들이 모두 필수라는 것을 알 수 있습니다.\n\n<DeepDive>\n\n#### Reducer를 사용하여 \"불가능한\" State 제거 {/*eliminating-impossible-states-with-a-reducer*/}\n\n여기 폼의 State를 나타내는데 충분한 세 가지 변수가 있습니다. 하지만 세 변수는 여전히 말이 안 되는 일부 중간 State를 가지고 있습니다. 예를 들면 `error`가 `null`이 아닌데 `status`가 `success`인 것은 말이 되지 않습니다. State를 조금 더 정확하게 모델링하기 위해서는 [Reducer로 분리](/learn/extracting-state-logic-into-a-reducer)하는 방법도 있습니다. Reducer를 사용하면 여러 State 변수를 하나의 객체로 통합하고 관련된 모든 로직도 합칠 수 있습니다!\n\n</DeepDive>\n\n### 다섯 번째: state 설정을 위해 이벤트 핸들러를 연결하기 {/*step-5-connect-the-event-handlers-to-set-state*/}\n\n마지막으로 state 변수를 설정하기 위해 이벤트 핸들러를 연결하세요. 아래는 이벤트 핸들러까지 연결된 최종 폼입니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function Form() {\n  const [answer, setAnswer] = useState('');\n  const [error, setError] = useState(null);\n  const [status, setStatus] = useState('typing');\n\n  if (status === 'success') {\n    return <h1>That's right!</h1>\n  }\n\n  async function handleSubmit(e) {\n    e.preventDefault();\n    setStatus('submitting');\n    try {\n      await submitForm(answer);\n      setStatus('success');\n    } catch (err) {\n      setStatus('typing');\n      setError(err);\n    }\n  }\n\n  function handleTextareaChange(e) {\n    setAnswer(e.target.value);\n  }\n\n  return (\n    <>\n      <h2>City quiz</h2>\n      <p>\n        In which city is there a billboard that turns air into drinkable water?\n      </p>\n      <form onSubmit={handleSubmit}>\n        <textarea\n          value={answer}\n          onChange={handleTextareaChange}\n          disabled={status === 'submitting'}\n        />\n        <br />\n        <button disabled={\n          answer.length === 0 ||\n          status === 'submitting'\n        }>\n          Submit\n        </button>\n        {error !== null &&\n          <p className=\"Error\">\n            {error.message}\n          </p>\n        }\n      </form>\n    </>\n  );\n}\n\nfunction submitForm(answer) {\n  // 네트워크에 접속한다고 가정해봅시다.\n  return new Promise((resolve, reject) => {\n    setTimeout(() => {\n      let shouldError = answer.toLowerCase() !== 'lima'\n      if (shouldError) {\n        reject(new Error('Good guess but a wrong answer. Try again!'));\n      } else {\n        resolve();\n      }\n    }, 1500);\n  });\n}\n```\n\n```css\n.Error { color: red; }\n```\n\n</Sandpack>\n\n이러한 코드가 기존의 명령형 프로그래밍 예시보다는 길지만 그래도 조금 더 견고합니다. 모든 상호작용을 state로 표현하게 되면 이후에 새로운 시각적 state가 추가되더라도 기존의 로직이 손상되는 것을 막을 수 있습니다. 또한 상호작용 자체의 로직을 변경하지 않고도 각각의 state에 표시되는 항목을 변경할 수 있습니다.\n\n<Recap>\n\n* 선언형 프로그래밍은 UI를 세밀하게 직접 조작하는 것(명령형)이 아니라 각각의 시각적 state로 UI를 묘사하는 것을 의미합니다.\n* 컴포넌트를 개발할 때\n  1. 모든 시각적 state를 확인하세요.\n  2. 휴먼이나 컴퓨터가 state 변화를 어떻게 트리거 하는지 알아내세요.\n  3. `useState`로 state를 모델링하세요.\n  4. 버그와 모순을 피하려면 불필요한 state를 제거하세요.\n  5. state 설정을 위해 이벤트 핸들러를 연결하세요.\n\n</Recap>\n\n\n\n<Challenges>\n\n#### CSS 클래스를 추가하고 제거하기 {/*add-and-remove-a-css-class*/}\n\n사진을 클릭하면 바깥에 있는 `<div>`의 `background--active` CSS 클래스를 *제거*하고 `<img>`에 `picture--active` 클래스를 추가합니다. 그리고 배경을 다시 클릭하면 원래 CSS 클래스로 돌아와야 합니다.\n\n화면상으로는 사진을 클릭하면 보라색 배경은 제거되고 사진의 테두리는 강조 표시됩니다. 사진 외부를 클릭하면 배경이 강조 표시되고 사진의 테두리 강조 표시는 제거됩니다.\n\n<Sandpack>\n\n```js\nexport default function Picture() {\n  return (\n    <div className=\"background background--active\">\n      <img\n        className=\"picture\"\n        alt=\"Rainbow houses in Kampung Pelangi, Indonesia\"\n        src=\"https://i.imgur.com/5qwVYb1.jpeg\"\n      />\n    </div>\n  );\n}\n```\n\n```css\nbody { margin: 0; padding: 0; height: 250px; }\n\n.background {\n  width: 100vw;\n  height: 100vh;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  background: #eee;\n}\n\n.background--active {\n  background: #a6b5ff;\n}\n\n.picture {\n  width: 200px;\n  height: 200px;\n  border-radius: 10px;\n  border: 5px solid transparent;\n}\n\n.picture--active {\n  border: 5px solid #a6b5ff;\n}\n```\n\n</Sandpack>\n\n<Solution>\n\n이 컴포넌트는 두 가지 시각적 state를 가지고 있습니다. 이미지가 활성화되었을 때와 비활성화되었을 때입니다.\n\n* 이미지가 활성화되었을 때 CSS 클래스는 `background`와 `picture picture--active`가 됩니다.\n* 이미지가 비활성화되었을 때 CSS 클래스는 `background background--active`와 `picture`가 됩니다.\n\n이미지의 활성화 state를 기억하기 위해서는 하나의 boolean state 변수로 충분합니다. 원래 하려고 했던 것은 CSS 클래스를 제거하거나 추가하는 것이었습니다. 하지만 React에서는 UI 요소를 *조작하는 것* 보다는 무엇을 보여주길 원하는지 *묘사하는 것*이 필요합니다. 그렇기 때문에 현재 state를 기반으로 두 CSS 클래스 모두를 계산해야 합니다. 그리고 이미지를 클릭했을 때 배경이 클릭되지 않도록 이벤트의 [전파를 막을](/learn/responding-to-events#stopping-propagation) 필요가 있습니다.\n\n이미지를 클릭한 다음 이미지 외부도 클릭해 잘 작동하는지 확인해봅시다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function Picture() {\n  const [isActive, setIsActive] = useState(false);\n\n  let backgroundClassName = 'background';\n  let pictureClassName = 'picture';\n  if (isActive) {\n    pictureClassName += ' picture--active';\n  } else {\n    backgroundClassName += ' background--active';\n  }\n\n  return (\n    <div\n      className={backgroundClassName}\n      onClick={() => setIsActive(false)}\n    >\n      <img\n        onClick={e => {\n          e.stopPropagation();\n          setIsActive(true);\n        }}\n        className={pictureClassName}\n        alt=\"Rainbow houses in Kampung Pelangi, Indonesia\"\n        src=\"https://i.imgur.com/5qwVYb1.jpeg\"\n      />\n    </div>\n  );\n}\n```\n\n```css\nbody { margin: 0; padding: 0; height: 250px; }\n\n.background {\n  width: 100vw;\n  height: 100vh;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  background: #eee;\n}\n\n.background--active {\n  background: #a6b5ff;\n}\n\n.picture {\n  width: 200px;\n  height: 200px;\n  border-radius: 10px;\n  border: 5px solid transparent;\n}\n\n.picture--active {\n  border: 5px solid #a6b5ff;\n}\n```\n\n</Sandpack>\n\n아래와 같이 두 JSX 덩어리로 나눌 수도 있습니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function Picture() {\n  const [isActive, setIsActive] = useState(false);\n  if (isActive) {\n    return (\n      <div\n        className=\"background\"\n        onClick={() => setIsActive(false)}\n      >\n        <img\n          className=\"picture picture--active\"\n          alt=\"Rainbow houses in Kampung Pelangi, Indonesia\"\n          src=\"https://i.imgur.com/5qwVYb1.jpeg\"\n          onClick={e => e.stopPropagation()}\n        />\n      </div>\n    );\n  }\n  return (\n    <div className=\"background background--active\">\n      <img\n        className=\"picture\"\n        alt=\"Rainbow houses in Kampung Pelangi, Indonesia\"\n        src=\"https://i.imgur.com/5qwVYb1.jpeg\"\n        onClick={() => setIsActive(true)}\n      />\n    </div>\n  );\n}\n```\n\n```css\nbody { margin: 0; padding: 0; height: 250px; }\n\n.background {\n  width: 100vw;\n  height: 100vh;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  background: #eee;\n}\n\n.background--active {\n  background: #a6b5ff;\n}\n\n.picture {\n  width: 200px;\n  height: 200px;\n  border-radius: 10px;\n  border: 5px solid transparent;\n}\n\n.picture--active {\n  border: 5px solid #a6b5ff;\n}\n```\n\n</Sandpack>\n\n두 개의 다른 JSX 덩어리가 동일한 트리를 표현할 경우 중첩된 요소들(첫 번째 `<div>` → 첫 번째 `<img>`)이 순서대로 작성되어야 한다는 것을 기억하세요. 그렇지 않으면 `isActive`는 하위 트리 전체를 다시 만들고 [state는 초기화](/learn/preserving-and-resetting-state)될 것입니다. 그렇기 때문에 유사한 JSX 트리가 반환될 경우에는 이를 하나로 만들어 사용하는 것이 좋습니다.\n\n</Solution>\n\n#### 프로필 편집기 {/*profile-editor*/}\n\n아래는 순수 자바스크립트만으로 작성된 짧은 코드입니다. 동작을 이해하기 위해 살펴봅시다.\n\n<Sandpack>\n\n```js src/index.js active\nfunction handleFormSubmit(e) {\n  e.preventDefault();\n  if (button.textContent === 'Edit Profile') {\n    button.textContent = 'Save Profile';\n    hide(firstNameText);\n    hide(lastNameText);\n    show(firstNameInput);\n    show(lastNameInput);\n  } else {\n    button.textContent = 'Edit Profile';\n    hide(firstNameInput);\n    hide(lastNameInput);\n    show(firstNameText);\n    show(lastNameText);\n  }\n}\n\nfunction handleFirstNameChange() {\n  firstNameText.textContent = firstNameInput.value;\n  helloText.textContent = (\n    'Hello ' +\n    firstNameInput.value + ' ' +\n    lastNameInput.value + '!'\n  );\n}\n\nfunction handleLastNameChange() {\n  lastNameText.textContent = lastNameInput.value;\n  helloText.textContent = (\n    'Hello ' +\n    firstNameInput.value + ' ' +\n    lastNameInput.value + '!'\n  );\n}\n\nfunction hide(el) {\n  el.style.display = 'none';\n}\n\nfunction show(el) {\n  el.style.display = '';\n}\n\nlet form = document.getElementById('form');\nlet profile = document.getElementById('profile');\nlet editButton = document.getElementById('editButton');\nlet firstNameInput = document.getElementById('firstNameInput');\nlet firstNameText = document.getElementById('firstNameText');\nlet lastNameInput = document.getElementById('lastNameInput');\nlet helloText = document.getElementById('helloText');\nform.onsubmit = handleFormSubmit;\nfirstNameInput.oninput = handleFirstNameChange;\nlastNameInput.oninput = handleLastNameChange;\n```\n\n```js sandbox.config.json hidden\n{\n  \"hardReloadOnChange\": true\n}\n```\n\n```html public/index.html\n<form id=\"form\">\n  <label>\n    First name:\n    <b id=\"firstNameText\">Jane</b>\n    <input\n      id=\"firstNameInput\"\n      value=\"Jane\"\n      style=\"display: none\">\n  </label>\n  <label>\n    Last name:\n    <b id=\"lastNameText\">Jacobs</b>\n    <input\n      id=\"lastNameInput\"\n      value=\"Jacobs\"\n      style=\"display: none\">\n  </label>\n  <button type=\"submit\" id=\"button\">Edit Profile</button>\n  <p><i id=\"helloText\">Hello, Jane Jacobs!</i></p>\n</form>\n\n<style>\n* { box-sizing: border-box; }\nbody { font-family: sans-serif; margin: 20px; padding: 0; }\nlabel { display: block; margin-bottom: 20px; }\n</style>\n```\n\n</Sandpack>\n\n이 폼은 두 가지 모드를 가지고 있습니다. 하나는 편집 모드이고 이때 인풋들을 볼 수 있습니다. 또 다른 하나는 보기 모드이고 이때는 오직 결과만 볼 수 있습니다. 버튼의 라벨은 속한 모드에 따라 \"Edit\"과 \"Save\"로 변경됩니다. 또한 인풋들의 내용을 변경할 때 환영 메시지를 실시간으로 확인할 수 있습니다.\n\n당신의 목적은 아래 샌드박스에서 React로 다시 구현하는 것입니다. 편의를 위해 마크업은 이미 JSX로 변환되어 있습니다. 하지만 원래 구현돼있던 것처럼 인풋들을 보여주고 숨기는 것은 직접 구현해야 합니다.\n\n마찬가지로 아래에 있는 텍스트도 업데이트시켜야 합니다!\n\n<Sandpack>\n\n```js\nexport default function EditProfile() {\n  return (\n    <form>\n      <label>\n        First name:{' '}\n        <b>Jane</b>\n        <input />\n      </label>\n      <label>\n        Last name:{' '}\n        <b>Jacobs</b>\n        <input />\n      </label>\n      <button type=\"submit\">\n        Edit Profile\n      </button>\n      <p><i>Hello, Jane Jacobs!</i></p>\n    </form>\n  );\n}\n```\n\n```css\nlabel { display: block; margin-bottom: 20px; }\n```\n\n</Sandpack>\n\n<Solution>\n\n인풋의 값을 저장하기 위해서는 `firstName`과 `lastName` 두 가지 변수가 필요합니다. 또한 인풋들을 보여줄지 말지를 결정하는 `isEditing` 변수도 필요합니다. 하지만 `fullName` 변수는 필요하지 _않습니다._ 왜냐하면 전체 이름은 `firstName`과 `lastName` 변수를 합쳐 만들 수 있기 때문입니다.\n\n마지막으로 `isEditing`에 따라 인풋들을 보여주고 숨기기 위해 [조건부 렌더링](/learn/conditional-rendering)을 해야 합니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function EditProfile() {\n  const [isEditing, setIsEditing] = useState(false);\n  const [firstName, setFirstName] = useState('Jane');\n  const [lastName, setLastName] = useState('Jacobs');\n\n  return (\n    <form onSubmit={e => {\n      e.preventDefault();\n      setIsEditing(!isEditing);\n    }}>\n      <label>\n        First name:{' '}\n        {isEditing ? (\n          <input\n            value={firstName}\n            onChange={e => {\n              setFirstName(e.target.value)\n            }}\n          />\n        ) : (\n          <b>{firstName}</b>\n        )}\n      </label>\n      <label>\n        Last name:{' '}\n        {isEditing ? (\n          <input\n            value={lastName}\n            onChange={e => {\n              setLastName(e.target.value)\n            }}\n          />\n        ) : (\n          <b>{lastName}</b>\n        )}\n      </label>\n      <button type=\"submit\">\n        {isEditing ? 'Save' : 'Edit'} Profile\n      </button>\n      <p><i>Hello, {firstName} {lastName}!</i></p>\n    </form>\n  );\n}\n```\n\n```css\nlabel { display: block; margin-bottom: 20px; }\n```\n\n</Sandpack>\n\n위 코드와 원래의 명령형 코드를 비교해보세요. 차이점을 알겠나요?\n\n</Solution>\n\n#### 명령형 코드를 React 없이 리팩토링하기 {/*refactor-the-imperative-solution-without-react*/}\n\n여기 React 없이 명령형으로 작성된 챌린지 이전의 코드가 있습니다.\n\n<Sandpack>\n\n```js src/index.js active\nfunction handleFormSubmit(e) {\n  e.preventDefault();\n  if (button.textContent === 'Edit Profile') {\n    button.textContent = 'Save Profile';\n    hide(firstNameText);\n    hide(lastNameText);\n    show(firstNameInput);\n    show(lastNameInput);\n  } else {\n    button.textContent = 'Edit Profile';\n    hide(firstNameInput);\n    hide(lastNameInput);\n    show(firstNameText);\n    show(lastNameText);\n  }\n}\n\nfunction handleFirstNameChange() {\n  firstNameText.textContent = firstNameInput.value;\n  helloText.textContent = (\n    'Hello ' +\n    firstNameInput.value + ' ' +\n    lastNameInput.value + '!'\n  );\n}\n\nfunction handleLastNameChange() {\n  lastNameText.textContent = lastNameInput.value;\n  helloText.textContent = (\n    'Hello ' +\n    firstNameInput.value + ' ' +\n    lastNameInput.value + '!'\n  );\n}\n\nfunction hide(el) {\n  el.style.display = 'none';\n}\n\nfunction show(el) {\n  el.style.display = '';\n}\n\nlet form = document.getElementById('form');\nlet profile = document.getElementById('profile');\nlet editButton = document.getElementById('editButton');\nlet firstNameInput = document.getElementById('firstNameInput');\nlet firstNameText = document.getElementById('firstNameText');\nlet lastNameInput = document.getElementById('lastNameInput');\nlet helloText = document.getElementById('helloText');\nform.onsubmit = handleFormSubmit;\nfirstNameInput.oninput = handleFirstNameChange;\nlastNameInput.oninput = handleLastNameChange;\n```\n\n```js sandbox.config.json hidden\n{\n  \"hardReloadOnChange\": true\n}\n```\n\n```html public/index.html\n<form id=\"form\">\n  <label>\n    First name:\n    <b id=\"firstNameText\">Jane</b>\n    <input\n      id=\"firstNameInput\"\n      value=\"Jane\"\n      style=\"display: none\">\n  </label>\n  <label>\n    Last name:\n    <b id=\"lastNameText\">Jacobs</b>\n    <input\n      id=\"lastNameInput\"\n      value=\"Jacobs\"\n      style=\"display: none\">\n  </label>\n  <button type=\"submit\" id=\"button\">Edit Profile</button>\n  <p><i id=\"helloText\">Hello, Jane Jacobs!</i></p>\n</form>\n\n<style>\n* { box-sizing: border-box; }\nbody { font-family: sans-serif; margin: 20px; padding: 0; }\nlabel { display: block; margin-bottom: 20px; }\n</style>\n```\n\n</Sandpack>\n\nReact가 없다고 상상해보세요. 이 로직을 조금 더 견고하고 React와 비슷하게 리팩토링할 수 있을까요? React에서처럼 state를 명시적으로 표현하면 어떻게 보일까요?\n\n어디서부터 시작해야할지 고민 중이라면 아래에 구조를 미리 만들어 두었습니다. 아래 구조에서 시작한다면 비어있는 `updateDOM` 함수 로직을 작성해보세요. (필요에 따라 원래 코드를 참조하시면 됩니다.)\n\n<Sandpack>\n\n```js src/index.js active\nlet firstName = 'Jane';\nlet lastName = 'Jacobs';\nlet isEditing = false;\n\nfunction handleFormSubmit(e) {\n  e.preventDefault();\n  setIsEditing(!isEditing);\n}\n\nfunction handleFirstNameChange(e) {\n  setFirstName(e.target.value);\n}\n\nfunction handleLastNameChange(e) {\n  setLastName(e.target.value);\n}\n\nfunction setFirstName(value) {\n  firstName = value;\n  updateDOM();\n}\n\nfunction setLastName(value) {\n  lastName = value;\n  updateDOM();\n}\n\nfunction setIsEditing(value) {\n  isEditing = value;\n  updateDOM();\n}\n\nfunction updateDOM() {\n  if (isEditing) {\n    button.textContent = 'Save Profile';\n    // TODO: 인풋을 보여주고 텍스트는 숨깁니다.\n  } else {\n    button.textContent = 'Edit Profile';\n    // TODO: 인풋을 숨기고 텍스트를 보여줍니다.\n  }\n  // TODO: 텍스트 라벨을 업데이트합니다.\n}\n\nfunction hide(el) {\n  el.style.display = 'none';\n}\n\nfunction show(el) {\n  el.style.display = '';\n}\n\nlet form = document.getElementById('form');\nlet profile = document.getElementById('profile');\nlet editButton = document.getElementById('editButton');\nlet firstNameInput = document.getElementById('firstNameInput');\nlet firstNameText = document.getElementById('firstNameText');\nlet lastNameInput = document.getElementById('lastNameInput');\nlet helloText = document.getElementById('helloText');\nform.onsubmit = handleFormSubmit;\nfirstNameInput.oninput = handleFirstNameChange;\nlastNameInput.oninput = handleLastNameChange;\n```\n\n```js sandbox.config.json hidden\n{\n  \"hardReloadOnChange\": true\n}\n```\n\n```html public/index.html\n<form id=\"form\">\n  <label>\n    First name:\n    <b id=\"firstNameText\">Jane</b>\n    <input\n      id=\"firstNameInput\"\n      value=\"Jane\"\n      style=\"display: none\">\n  </label>\n  <label>\n    Last name:\n    <b id=\"lastNameText\">Jacobs</b>\n    <input\n      id=\"lastNameInput\"\n      value=\"Jacobs\"\n      style=\"display: none\">\n  </label>\n  <button type=\"submit\" id=\"button\">Edit Profile</button>\n  <p><i id=\"helloText\">Hello, Jane Jacobs!</i></p>\n</form>\n\n<style>\n* { box-sizing: border-box; }\nbody { font-family: sans-serif; margin: 20px; padding: 0; }\nlabel { display: block; margin-bottom: 20px; }\n</style>\n```\n\n</Sandpack>\n\n<Solution>\n\n누락된 로직에는 인풋과 텍스트의 표시 여부 토글 및 라벨 업데이트가 포함되어 있습니다.\n\n<Sandpack>\n\n```js src/index.js active\nlet firstName = 'Jane';\nlet lastName = 'Jacobs';\nlet isEditing = false;\n\nfunction handleFormSubmit(e) {\n  e.preventDefault();\n  setIsEditing(!isEditing);\n}\n\nfunction handleFirstNameChange(e) {\n  setFirstName(e.target.value);\n}\n\nfunction handleLastNameChange(e) {\n  setLastName(e.target.value);\n}\n\nfunction setFirstName(value) {\n  firstName = value;\n  updateDOM();\n}\n\nfunction setLastName(value) {\n  lastName = value;\n  updateDOM();\n}\n\nfunction setIsEditing(value) {\n  isEditing = value;\n  updateDOM();\n}\n\nfunction updateDOM() {\n  if (isEditing) {\n    button.textContent = 'Save Profile';\n    hide(firstNameText);\n    hide(lastNameText);\n    show(firstNameInput);\n    show(lastNameInput);\n  } else {\n    button.textContent = 'Edit Profile';\n    hide(firstNameInput);\n    hide(lastNameInput);\n    show(firstNameText);\n    show(lastNameText);\n  }\n  firstNameText.textContent = firstName;\n  lastNameText.textContent = lastName;\n  helloText.textContent = (\n    'Hello ' +\n    firstName + ' ' +\n    lastName + '!'\n  );\n}\n\nfunction hide(el) {\n  el.style.display = 'none';\n}\n\nfunction show(el) {\n  el.style.display = '';\n}\n\nlet form = document.getElementById('form');\nlet profile = document.getElementById('profile');\nlet editButton = document.getElementById('editButton');\nlet firstNameInput = document.getElementById('firstNameInput');\nlet firstNameText = document.getElementById('firstNameText');\nlet lastNameInput = document.getElementById('lastNameInput');\nlet helloText = document.getElementById('helloText');\nform.onsubmit = handleFormSubmit;\nfirstNameInput.oninput = handleFirstNameChange;\nlastNameInput.oninput = handleLastNameChange;\n```\n\n```js sandbox.config.json hidden\n{\n  \"hardReloadOnChange\": true\n}\n```\n\n```html public/index.html\n<form id=\"form\">\n  <label>\n    First name:\n    <b id=\"firstNameText\">Jane</b>\n    <input\n      id=\"firstNameInput\"\n      value=\"Jane\"\n      style=\"display: none\">\n  </label>\n  <label>\n    Last name:\n    <b id=\"lastNameText\">Jacobs</b>\n    <input\n      id=\"lastNameInput\"\n      value=\"Jacobs\"\n      style=\"display: none\">\n  </label>\n  <button type=\"submit\" id=\"button\">Edit Profile</button>\n  <p><i id=\"helloText\">Hello, Jane Jacobs!</i></p>\n</form>\n\n<style>\n* { box-sizing: border-box; }\nbody { font-family: sans-serif; margin: 20px; padding: 0; }\nlabel { display: block; margin-bottom: 20px; }\n</style>\n```\n\n</Sandpack>\n\n작성된 `updateDOM` 함수는 React가 state를 설정할 때 어떤 작업을 수행하는지 보여줍니다. (하지만 React는 마지막으로 state가 설정된 이후 변경되지 않은 속성에 대해서는 DOM을 건드리지 않습니다.)\n\n</Solution>\n\n</Challenges>\n"
  },
  {
    "path": "src/content/learn/referencing-values-with-refs.md",
    "content": "---\ntitle: 'Ref로 값 참조하기'\n---\n\n<Intro>\n\n컴포넌트가 일부 정보를 \"기억\"하고 싶지만, 해당 정보가 [렌더링을 유발](/learn/render-and-commit)하지 않도록 하려면 *Ref*를 사용하세요.\n\n</Intro>\n\n<YouWillLearn>\n\n- 컴포넌트 Ref를 어떻게 추가하는가\n- Ref의 값이 어떻게 업데이트되는가\n- Ref가 State와 어떻게 다른가\n- Ref를 어떻게 안전하게 사용할까\n\n</YouWillLearn>\n\n## 컴포넌트에 Ref를 추가하기 {/*adding-a-ref-to-your-component*/}\n\nReact에서 `useRef` Hook을 가져와 컴포넌트에 Ref를 추가할 수 있습니다.\n\n```js\nimport { useRef } from 'react';\n```\n\n컴포넌트 내에서 `useRef` Hook을 호출하고 참조할 초깃값을 유일한 인자로 전달합니다. 예를 들어 다음은 값 `0`에 대한 Ref입니다.\n\n```js\nconst ref = useRef(0);\n```\n\n`useRef`는 다음과 같은 객체를 반환합니다.\n\n```js\n{\n  current: 0 // useRef에 전달한 값\n}\n```\n\n<Illustration src=\"/images/docs/illustrations/i_ref.png\" alt=\"An arrow with 'current' written on it stuffed into a pocket with 'ref' written on it.\" />\n\n`ref.current` 프로퍼티를 통해 해당 Ref의 `current` 값에 접근할 수 있습니다. 이 값은 의도적으로 변경할 수 있으므로 읽고 쓸 수 있습니다. React가 추적하지 않는 구성 요소의 비밀 주머니라 할 수 있습니다. (이것이 바로 React의 단방향 데이터 흐름에서 \"탈출구\"가 되는 것입니다. 아래에서 자세히 설명하고 있습니다!)\n\n여기서 버튼은 클릭할 때마다 `ref.current`를 증가시킵니다.\n\n<Sandpack>\n\n```js\nimport { useRef } from 'react';\n\nexport default function Counter() {\n  let ref = useRef(0);\n\n  function handleClick() {\n    ref.current = ref.current + 1;\n    alert('You clicked ' + ref.current + ' times!');\n  }\n\n  return (\n    <button onClick={handleClick}>\n      Click me!\n    </button>\n  );\n}\n```\n\n</Sandpack>\n\nRef는 숫자를 가리키지만, [State](/learn/state-a-components-memory)처럼 문자열, 객체, 심지어 함수 등 모든 것을 가리킬 수 있습니다. State와 달리 Ref는 읽고 수정할 수 있는 `current` 프로퍼티를 가진 일반 자바스크립트 객체입니다.\n\n**컴포넌트는 모든 증가에 대하여 다시 렌더링 되지 않습니다.** State와 마찬가지로 Ref도 React에 리렌더에 의해 유지됩니다. 그러나, State를 설정하면 컴포넌트가 다시 렌더링 됩니다. Ref를 변경하면 다시 렌더링 되지 않습니다!\n\n## 예시: 스톱워치 작성하기 {/*example-building-a-stopwatch*/}\n\nRef와 State를 단일 컴포넌트로 결합할 수 있습니다. 예를 들어 사용자가 버튼을 눌러 시작하거나 중지할 수 있는 스톱워치를 만들어봅시다. 사용자가 \"시작\"을 누른 후 시간이 얼마나 지났는지 표시하려면 시작 버튼을 누른 시기와 현재 시각을 추적해야 합니다. **이 정보는 렌더링에 사용되므로 State를 유지합니다.**\n\n```js\nconst [startTime, setStartTime] = useState(null);\nconst [now, setNow] = useState(null);\n```\n\n사용자가 \"시작\"을 누르면 [`setInterval`](https://developer.mozilla.org/docs/Web/API/setInterval)을 사용하여 10밀리초마다 시간을 업데이트합니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function Stopwatch() {\n  const [startTime, setStartTime] = useState(null);\n  const [now, setNow] = useState(null);\n\n  function handleStart() {\n    // 카운팅을 시작합니다.\n    setStartTime(Date.now());\n    setNow(Date.now());\n\n    setInterval(() => {\n      // 10ms 마다 현재 시간을 업데이트 합니다.\n      setNow(Date.now());\n    }, 10);\n  }\n\n  let secondsPassed = 0;\n  if (startTime != null && now != null) {\n    secondsPassed = (now - startTime) / 1000;\n  }\n\n  return (\n    <>\n      <h1>Time passed: {secondsPassed.toFixed(3)}</h1>\n      <button onClick={handleStart}>\n        Start\n      </button>\n    </>\n  );\n}\n```\n\n</Sandpack>\n\n\"Stop\" 버튼을 누르면 `now` State 변수의 업데이트를 중지하기 위해 기존 Interval을 취소해야 합니다. 이를 위해 [`clearInterval`](https://developer.mozilla.org/en-US/docs/Web/API/clearInterval)을 호출하면 됩니다. 그러나 이전에 사용자가 시작을 눌렀을 때 `setInterval` 호출로 반환된 interval ID를 제공해야 합니다. Interval ID는 어딘가에 보관해야 합니다. **Interval ID는 렌더링에 사용하지 않으므로 Ref에 저장할 수 있습니다.**\n\n<Sandpack>\n\n```js\nimport { useState, useRef } from 'react';\n\nexport default function Stopwatch() {\n  const [startTime, setStartTime] = useState(null);\n  const [now, setNow] = useState(null);\n  const intervalRef = useRef(null);\n\n  function handleStart() {\n    setStartTime(Date.now());\n    setNow(Date.now());\n\n    clearInterval(intervalRef.current);\n    intervalRef.current = setInterval(() => {\n      setNow(Date.now());\n    }, 10);\n  }\n\n  function handleStop() {\n    clearInterval(intervalRef.current);\n  }\n\n  let secondsPassed = 0;\n  if (startTime != null && now != null) {\n    secondsPassed = (now - startTime) / 1000;\n  }\n\n  return (\n    <>\n      <h1>Time passed: {secondsPassed.toFixed(3)}</h1>\n      <button onClick={handleStart}>\n        Start\n      </button>\n      <button onClick={handleStop}>\n        Stop\n      </button>\n    </>\n  );\n}\n```\n\n</Sandpack>\n\n렌더링에 정보를 사용할 때 해당 정보를 State로 유지합니다. 이벤트 핸들러에게만 필요한 정보이고 변경이 일어날 때 리렌더링이 필요하지 않다면, Ref를 사용하는 것이 더 효율적일 수 있습니다.\n\n## Ref와 State의 차이 {/*differences-between-refs-and-state*/}\n\nRef가 State보다 덜 \"엄격한\" 것으로 생각될 수 있습니다. 예를 들어, 항상 State 설정 함수를 사용하지 않고 변경할 수 있습니다. 하지만 대부분은 State를 사용하고 싶을 것입니다. Ref는 자주 필요하지 않은 \"탈출구\"입니다. State와 Ref를 비교한 것은 다음과 같습니다.\n\n| Ref                                                                            | State                                                                                              |\n|--------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------|\n| `useRef(initialValue)` 는 `{ current: initialValue }`를 반환합니다.            | `useState(initialValue)`는 State 변수의 현재 값과 Setter 함수 `[value, setValue]`를 반환합니다.     |\n| `current` 값을 바꿔도 리렌더링 하지 않습니다.                                   | State를 바꾸면 리렌더링 합니다.                                                                     |\n| Mutable: 렌더링 프로세스 외부에서 `current` 값을 수정 및 업데이트할 수 있습니다. | Immutable: State를 수정하기 위해서는 State 설정 함수를 반드시 사용하여 리렌더링 대기열에 넣어야 합니다. |\n| 렌더링 중에는 `current` 값을 읽거나 쓰면 안 됩니다. | 언제든지 State를 읽을 수 있습니다. 그러나 각 렌더링마다 변경되지 않는 자체적인 State의 [Snapshot](/learn/state-as-a-snapshot)이 있습니다. |\n\n다음은 State와 함께 구현한 카운터 버튼입니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function Counter() {\n  const [count, setCount] = useState(0);\n\n  function handleClick() {\n    setCount(count + 1);\n  }\n\n  return (\n    <button onClick={handleClick}>\n      You clicked {count} times\n    </button>\n  );\n}\n```\n\n</Sandpack>\n\n`count` 값을 표시하므로 State 값을 사용하는 것이 타당합니다. 카운터의 값을 `setCount()`로 설정하면 React는 컴포넌트를 다시 렌더링하고 새 카운트를 반영하도록 화면을 업데이트합니다.\n\n이를 Ref와 함께 구현하려고 하면 React는 컴포넌트를 다시 렌더링하지 않으므로 카운트가 변경되는 것을 볼 수 없습니다! 이 버튼을 클릭해도 **텍스트를 업데이트하지 않는** 방법을 확인해봅시다.\n\n<Sandpack>\n\n```js\nimport { useRef } from 'react';\n\nexport default function Counter() {\n  let countRef = useRef(0);\n\n  function handleClick() {\n    // 이것은 컴포넌트의 리렌더를 일으키지 않습니다!\n    countRef.current = countRef.current + 1;\n  }\n\n  return (\n    <button onClick={handleClick}>\n      You clicked {countRef.current} times\n    </button>\n  );\n}\n```\n\n</Sandpack>\n\n이것이 렌더링 중에 `ref.current`를 출력하면 신뢰할 수 없는 코드가 나오는 이유입니다. 이 부분이 필요하면 State를 대신 사용해야 합니다.\n\n<DeepDive>\n\n#### `useRef`는 내부적으로 어떻게 동작하나요? {/*how-does-useref-work-inside*/}\n\nReact가 `useState`와 `useRef`를 모두 제공하지만, 원칙적으로 `useRef`는 `useState` 위에 구현할 수 있습니다. React 내부에서 `useRef`를 이렇게 구현하는 것을 상상할 수 있습니다.\n\n```js\n// Inside of React\nfunction useRef(initialValue) {\n  const [ref, unused] = useState({ current: initialValue });\n  return ref;\n}\n```\n\n첫 번째 렌더링 중에 `useRef`는 `{ current: initialValue }`를 반환합니다. 이 객체는 React에 의해 저장되므로 다음 렌더링 중에 같은 객체를 반환합니다. 이 예시에서는 State Setter를 사용하지 않는 점에 주목하세요. `useRef`는 항상 동일한 객체를 반환해야 하므로 필요하지 않습니다!\n\nReact는 `useRef`가 실제로 충분히 일반적이기 때문에 built-in 버전을 제공합니다. setter가 없는 일반적인 state 변수라고 생각할 수 있습니다. 객체 지향 프로그래밍에 익숙하다면 Ref는 인스턴스 필드를 상기시킬 수 있습니다. 하지만 `this.something` 대신에 `somethingRef.current` 처럼 써야합니다.\n\n</DeepDive>\n\n## Ref를 사용할 시기 {/*when-to-use-refs*/}\n\n일반적으로 컴포넌트가 React를 \"외부\"와 외부 API--컴포넌트의 형태에 영향을 미치지 않는 브라우저 API 와 통신해야 할 때 Ref를 사용합니다. 다음은 몇 가지 특별한 상황입니다.\n\n- [Timeout ID](https://developer.mozilla.org/ko/docs/Web/API/setTimeout)를 저장\n- [다음 페이지](/learn/manipulating-the-dom-with-refs)에서 다루는 [DOM 엘리먼트](https://developer.mozilla.org/ko/docs/Web/API/Element) 저장 및 조작\n- JSX를 계산하는 데 필요하지 않은 다른 객체 저장\n\n컴포넌트가 일부 값을 저장해야 하지만 렌더링 로직에 영향을 미치지 않는 경우, Ref를 선택합니다.\n\n## Ref의 좋은 예시 {/*best-practices-for-refs*/}\n\n다음 원칙을 따르면 컴포넌트를 보다 쉽게 예측할 수 있습니다.\n\n- **Ref를 탈출구로 간주합니다.** Ref는 외부 시스템이나 브라우저 API로 작업할 때 유용합니다. 애플리케이션 로직과 데이터 흐름의 상당 부분이 Ref에 의존한다면 접근 방식을 재고해 보는 것이 좋습니다.\n- **렌더링 중에 `ref.current`를 읽거나 쓰지 마세요.** 렌더링 중에 일부 정보가 필요한 경우 [State](/learn/state-a-components-memory)를 대신 사용하세요. `ref.current`가 언제 변하는지 React는 모르기 때문에 렌더링할 때 읽어도 컴포넌트의 동작을 예측하기 어렵습니다. (`if (!ref.current) ref.current = new Thing()`과 같은 코드는 첫 번째 렌더링 중에 Ref를 한 번만 설정하는 경우라 예외입니다.)\n\nReact State의 제한은 Ref에 적용되지 않습니다. 예를 들어 State는 [모든 렌더링에 대한 Snapshot](/learn/state-as-a-snapshot) 및 [동기적으로 업데이트되지 않는 것](/learn/queueing-a-series-of-state-updates)과 같이 작동합니다. 그러나 Ref의 `current` 값을 변조하면 다음과 같이 즉시 변경됩니다.\n\n```js\nref.current = 5;\nconsole.log(ref.current); // 5\n```\n\n그 이유는 **Ref 자체가 일반 자바스크립트 객체**처럼 동작하기 때문입니다.\n\n또한 Ref로 작업할 때 [Mutation 방지](/learn/updating-objects-in-state)에 대해 걱정할 필요가 없습니다. 변형하는 객체를 렌더링에 사용하지 않는 한, React는 Ref 혹은 해당 콘텐츠를 어떻게 처리하든 신경 쓰지 않습니다.\n\n## Ref와 DOM {/*refs-and-the-dom*/}\n\n임의의 값을 Ref로 지정할 수 있습니다. 그러나 Ref의 가장 일반적인 사용 사례는 DOM 엘리먼트에 접근하는 것입니다. 예를 들어 프로그래밍 방식으로 입력창에 초점을 맞추려는 경우 유용합니다. `<div ref={myRef}>`와 같은 JSX의 `ref` 어트리뷰트에 Ref를 전달하면 React는 해당 DOM 엘리먼트를 `myRef.current`에 넣습니다. 만약 엘리먼트가 DOM 에서 사라지면, React 는 `myRef.current` 값을 `null` 로 업데이트 합니다. 이에 대한 자세한 내용은 [Ref로 DOM 조작하기](/learn/manipulating-the-dom-with-refs)에서 확인할 수 있습니다.\n\n<Recap>\n\n- Ref는 렌더링에 사용되지 않는 값을 고정하기 위한 탈출구이며, 자주 필요하지는 않습니다.\n- Ref는 읽거나 설정할 수 있는 `current`라는 프로퍼티를 호출할 수 있는 자바스크립트 순수객체입니다.\n- `useRef` Hook을 호출해 Ref를 달라고 React에 요청할 수 있습니다.\n- State와 마찬가지로 Ref는 컴포넌트의 렌더링 간에 정보를 유지할 수 있습니다.\n- State와 달리 Ref의 `current` 값을 설정하면 리렌더링을 트리거하지 않습니다.\n- 렌더링 중에 `ref.current`를 읽거나 쓰지 마세요. 컴포넌트를 예측하기 어렵게 만듭니다.\n\n</Recap>\n\n<Challenges>\n\n#### 정상적으로 동작하지 않는 채팅 입력창 수정 {/*fix-a-broken-chat-input*/}\n\n메시지를 입력하고 \"Send\"를 클릭합니다. \"Sent!\" 경고창(alert)이 나타나기 전에 3초 정도 지연된다는 것을 알 수 있습니다. 이 지연된 시간 동안 \"Undo\" 버튼을 볼 수 있습니다. 누르세요. 이 \"Undo\" 버튼은 \"Sent!\" 메시지가 나타나지 않도록 합니다. `handleSend` 중 저장된 Timeout ID에 대해 [`clearTimeout`](https://developer.mozilla.org/en-US/docs/Web/API/clearTimeout)을 호출하면 됩니다. 그러나 \"Undo\"를 클릭한 후에도 \"Sent!\" 메시지가 계속 나타납니다. 왜 작동하지 않는지 찾아서 고쳐봅시다.\n\n<Hint>\n\n모든 렌더에서 컴포넌트를 처음부터 실행(및 변수를 초기화)하기 때문에 `let timeoutID`와 같은 regular 변수는 렌더와 리렌더 사이에 \"존재\"하지 않습니다. timeout ID는 다른 곳에 보관해야 할까요?\n\n</Hint>\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function Chat() {\n  const [text, setText] = useState('');\n  const [isSending, setIsSending] = useState(false);\n  let timeoutID = null;\n\n  function handleSend() {\n    setIsSending(true);\n    timeoutID = setTimeout(() => {\n      alert('Sent!');\n      setIsSending(false);\n    }, 3000);\n  }\n\n  function handleUndo() {\n    setIsSending(false);\n    clearTimeout(timeoutID);\n  }\n\n  return (\n    <>\n      <input\n        disabled={isSending}\n        value={text}\n        onChange={e => setText(e.target.value)}\n      />\n      <button\n        disabled={isSending}\n        onClick={handleSend}>\n        {isSending ? 'Sending...' : 'Send'}\n      </button>\n      {isSending &&\n        <button onClick={handleUndo}>\n          Undo\n        </button>\n      }\n    </>\n  );\n}\n```\n\n</Sandpack>\n\n<Solution>\n\n컴포넌트가 리렌더 때마다(예를 들면 state를 설정하는 경우) 모든 지역 변수가 처음부터 초기화됩니다. 따라서 timeout ID를 `timeoutID`와 같은 로컬 변수에 저장한 다음 나중에 다른 이벤트 핸들러가 이를 \"볼\" 수 없습니다. 대신 ref에 저장하면 이 ref는 렌더 사이에 React에 보존됩니다.\n\n<Sandpack>\n\n```js\nimport { useState, useRef } from 'react';\n\nexport default function Chat() {\n  const [text, setText] = useState('');\n  const [isSending, setIsSending] = useState(false);\n  const timeoutRef = useRef(null);\n\n  function handleSend() {\n    setIsSending(true);\n    timeoutRef.current = setTimeout(() => {\n      alert('Sent!');\n      setIsSending(false);\n    }, 3000);\n  }\n\n  function handleUndo() {\n    setIsSending(false);\n    clearTimeout(timeoutRef.current);\n  }\n\n  return (\n    <>\n      <input\n        disabled={isSending}\n        value={text}\n        onChange={e => setText(e.target.value)}\n      />\n      <button\n        disabled={isSending}\n        onClick={handleSend}>\n        {isSending ? 'Sending...' : 'Send'}\n      </button>\n      {isSending &&\n        <button onClick={handleUndo}>\n          Undo\n        </button>\n      }\n    </>\n  );\n}\n```\n\n</Sandpack>\n\n</Solution>\n\n\n#### 리렌더링을 못하는 컴포넌트 수정 {/*fix-a-component-failing-to-re-render*/}\n\n이 버튼은 \"On\"과 \"Off\"를 표시하게 되어 있습니다. 그러나 항상 \"Off\"를 표시합니다. 코드가 뭐가 잘못됐나요? 고쳐봅시다.\n\n<Sandpack>\n\n```js\nimport { useRef } from 'react';\n\nexport default function Toggle() {\n  const isOnRef = useRef(false);\n\n  return (\n    <button onClick={() => {\n      isOnRef.current = !isOnRef.current;\n    }}>\n      {isOnRef.current ? 'On' : 'Off'}\n    </button>\n  );\n}\n```\n\n</Sandpack>\n\n<Solution>\n\n예시에서 ref의 current 값은 렌더링 출력에 사용됩니다. `{isOnRef.current ? 'On' : 'Off'}`. 이것은 이 정보가 ref에 있어서는 안 되며, 대신 state여야 한다는 표시입니다. 이 문제를 해결하려면 ref를 제거하고 state를 대신 사용합니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function Toggle() {\n  const [isOn, setIsOn] = useState(false);\n\n  return (\n    <button onClick={() => {\n      setIsOn(!isOn);\n    }}>\n      {isOn ? 'On' : 'Off'}\n    </button>\n  );\n}\n```\n\n</Sandpack>\n\n</Solution>\n\n#### debouncing 수정 {/*fix-debouncing*/}\n\n예시에서 모든 버튼 클릭 핸들러는 [\"debounced\"](https://redd.one/blog/debounce-vs-throttle)됩니다. 어떤 의미인지 보려면 버튼 중 하나를 누르세요. 1초 후에 메시지가 어떻게 표시되는지 확인해볼게요. 메시지 대기 중에 버튼을 누르면 타이머가 리셋됩니다. 따라서 같은 버튼을 여러 번 빠르게 클릭하면 *다음* 클릭이 멈출 때까지 메시지가 나타나지 않습니다. Debouncing을 사용하면 사용자가 \"작업이 중지될\" 때까지 일부 작업을 지연시킬 수 있습니다.\n\n예시는 작동하지만, 의도한 대로 작동하지 않습니다. 버튼은 독립적이지 않습니다. 문제를 보려면 버튼 중 하나를 클릭한 다음 즉시 다른 버튼을 클릭합니다. 지연된 후에 양쪽 버튼의 메시지를 볼 수 있을 것이라고 예상할 것입니다. 그러나 마지막 버튼의 메시지만 표시됩니다. 첫 번째 버튼의 메시지가 사라집니다.\n\n왜 두 버튼이 서로 간섭하는 것일까요? 문제를 찾아 해결해 봅시다.\n\n<Hint>\n\n마지막 timeout ID 변수는 모든 `DebouncedButton` 컴포넌트 간에 공유됩니다. 따라서 한 버튼을 클릭하면 다른 버튼의 시간 초과가 재설정됩니다. 버튼별로 timeout ID를 따로 저장할 수 있을까요?\n\n</Hint>\n\n<Sandpack>\n\n```js\nlet timeoutID;\n\nfunction DebouncedButton({ onClick, children }) {\n    return (\n        <button onClick={() => {\n            clearTimeout(timeoutID);\n            timeoutID = setTimeout(() => {\n                onClick();\n            }, 1000);\n        }}>\n            {children}\n        </button>\n    );\n}\n\nexport default function Dashboard() {\n    return (\n        <>\n            <DebouncedButton\n                onClick={() => alert('Spaceship launched!')}\n            >\n                Launch the spaceship\n            </DebouncedButton>\n            <DebouncedButton\n                onClick={() => alert('Soup boiled!')}\n            >\n                Boil the soup\n            </DebouncedButton>\n            <DebouncedButton\n                onClick={() => alert('Lullaby sung!')}\n            >\n                Sing a lullaby\n            </DebouncedButton>\n        </>\n    )\n}\n```\n\n```css\nbutton { display: block; margin: 10px; }\n```\n\n</Sandpack>\n\n<Solution>\n\n`timeoutID`와 같은 변수는 모든 컴포넌트 간에 공유됩니다. 그러므로 두 번째 버튼을 클릭하면 첫 번째 버튼의 처리시간 초과가 재설정됩니다. 이 문제를 해결하기 위해 ref에서 시간 초과를 유지할 수 있습니다. 각 버튼은 자체 ref를 갖게 되므로 서로 충돌하지 않습니다. 두 개의 버튼을 빠르게 클릭하면 두 개의 메시지가 모두 표시됩니다.\n\n<Sandpack>\n\n```js\nimport { useState, useRef } from 'react';\n\nfunction DebouncedButton({ onClick, children }) {\n    const timeoutRef = useRef(null);\n    return (\n        <button onClick={() => {\n            clearTimeout(timeoutRef.current);\n            timeoutRef.current = setTimeout(() => {\n                onClick();\n            }, 1000);\n        }}>\n            {children}\n        </button>\n    );\n}\n\nexport default function Dashboard() {\n    return (\n        <>\n            <DebouncedButton\n                onClick={() => alert('Spaceship launched!')}\n            >\n                Launch the spaceship\n            </DebouncedButton>\n            <DebouncedButton\n                onClick={() => alert('Soup boiled!')}\n            >\n                Boil the soup\n            </DebouncedButton>\n            <DebouncedButton\n                onClick={() => alert('Lullaby sung!')}\n            >\n                Sing a lullaby\n            </DebouncedButton>\n        </>\n    )\n}\n```\n\n```css\nbutton { display: block; margin: 10px; }\n```\n\n</Sandpack>\n\n</Solution>\n\n#### 최신 state 읽기 {/*read-the-latest-state*/}\n\n이 예시에서는 \"보내기\"를 누른 후 메시지가 표시되기 전에 약간의 지연이 발생합니다. \"hello\"를 입력하고 보내기를 누른 다음 입력을 빠르게 다시 편집합니다. 편집한 내용에도 불구하고 경고창(alert)에는 여전히 \"hello\"([그 당시](/learn/state-as-a-snapshot#state-over-time) state 값 버튼이 클릭 됨)가 표시됩니다.\n\n보통 이런 행동은 앱에서 원하는 것입니다. 그러나 일부 비동기 코드가 일부 state의 *최신* 버전을 읽기를 원하는 경우가 있습니다. 클릭 당시가 아니라 *현재* 입력 텍스트를 경고창(alert)에 표시하도록 할 수 있는 방법이 있을까요?\n\n<Sandpack>\n\n```js\nimport { useState, useRef } from 'react';\n\nexport default function Chat() {\n    const [text, setText] = useState('');\n\n    function handleSend() {\n        setTimeout(() => {\n            alert('Sending: ' + text);\n        }, 3000);\n    }\n\n    return (\n        <>\n            <input\n                value={text}\n                onChange={e => setText(e.target.value)}\n            />\n            <button\n                onClick={handleSend}>\n                Send\n            </button>\n        </>\n    );\n}\n```\n\n</Sandpack>\n\n<Solution>\n\nstate는 [snapshot 처럼](/learn/state-as-a-snapshot) 작동하므로 타임아웃과 같은 비동기 작업에서 최신 state를 읽을 수 없습니다. 그러나 최신 입력 텍스트를 ref에 유지할 수 있습니다. ref는 변경할 수 있으므로 언제든지 `current` 프로퍼티를 읽을 수 있습니다. current 텍스트는 렌더링에도 사용되므로, 이 예에서는 state 변수(렌더링을 위한)와 ref(시간 초과 시 읽음) *둘 다* 필요합니다. current ref를 수동으로 업데이트해야 합니다.\n\n<Sandpack>\n\n```js\nimport { useState, useRef } from 'react';\n\nexport default function Chat() {\n    const [text, setText] = useState('');\n    const textRef = useRef(text);\n\n    function handleChange(e) {\n        setText(e.target.value);\n        textRef.current = e.target.value;\n    }\n\n    function handleSend() {\n        setTimeout(() => {\n            alert('Sending: ' + textRef.current);\n        }, 3000);\n    }\n\n    return (\n        <>\n            <input\n                value={text}\n                onChange={handleChange}\n            />\n            <button\n                onClick={handleSend}>\n                Send\n            </button>\n        </>\n    );\n}\n```\n\n</Sandpack>\n\n</Solution>\n\n</Challenges>\n"
  },
  {
    "path": "src/content/learn/removing-effect-dependencies.md",
    "content": "---\ntitle: Effect의 의존성 제거하기\n---\n\n<Intro>\n\nEffect를 작성하면 린터는 Effect의 의존성 목록에 Effect가 읽는 모든 반응형 값(예를 들어 props 및 State)을 포함했는지 확인합니다. 이렇게 하면 Effect가 컴포넌트의 최신 props 및 State와 동기화 상태를 유지할 수 있습니다. 불필요한 의존성으로 인해 Effect가 너무 자주 실행되거나 무한 루프를 생성할 수도 있습니다. 이 가이드를 따라 Effect에서 불필요한 의존성을 검토하고 제거하세요.\n\n</Intro>\n\n<YouWillLearn>\n\n* Effect 의존성 무한 루프를 수정하는 방법\n* 의존성을 제거하고자 할 때 해야 할 일\n* Effect에 \"반응\"하지 않고 Effect에서 값을 읽는 방법\n* 객체와 함수 의존성을 피하는 방법과 이유\n* 의존성 린터를 억제하는 것이 위험한 이유와 대신 할 수 있는 일\n\n</YouWillLearn>\n\n## 의존성은 코드와 일치해야 합니다. {/*dependencies-should-match-the-code*/}\n\nEffect를 작성할 때는 먼저 Effect가 수행하기를 원하는 작업을 [시작하고 중지](/learn/lifecycle-of-reactive-effects#the-lifecycle-of-an-effect)하는 방법을 지정합니다.\n\n```js {5-7}\nconst serverUrl = 'https://localhost:1234';\n\nfunction ChatRoom({ roomId }) {\n  useEffect(() => {\n    const connection = createConnection(serverUrl, roomId);\n    connection.connect();\n    return () => connection.disconnect();\n    // ...\n}\n```\n\n그런 다음 Effect 의존성을 비워두면(`[]`) 린터가 올바른 의존성을 제안합니다.\n\n<Sandpack>\n\n```js\nimport { useState, useEffect } from 'react';\nimport { createConnection } from './chat.js';\n\nconst serverUrl = 'https://localhost:1234';\n\nfunction ChatRoom({ roomId }) {\n  useEffect(() => {\n    const connection = createConnection(serverUrl, roomId);\n    connection.connect();\n    return () => connection.disconnect();\n  }, []); // <-- 여기서 실수를 수정하세요!\n  return <h1>Welcome to the {roomId} room!</h1>;\n}\n\nexport default function App() {\n  const [roomId, setRoomId] = useState('general');\n  return (\n    <>\n      <label>\n        Choose the chat room:{' '}\n        <select\n          value={roomId}\n          onChange={e => setRoomId(e.target.value)}\n        >\n          <option value=\"general\">general</option>\n          <option value=\"travel\">travel</option>\n          <option value=\"music\">music</option>\n        </select>\n      </label>\n      <hr />\n      <ChatRoom roomId={roomId} />\n    </>\n  );\n}\n```\n\n```js src/chat.js\nexport function createConnection(serverUrl, roomId) {\n  // 실제 구현에서는 서버에 연결됩니다\n  return {\n    connect() {\n      console.log('✅ Connecting to \"' + roomId + '\" room at ' + serverUrl + '...');\n    },\n    disconnect() {\n      console.log('❌ Disconnected from \"' + roomId + '\" room at ' + serverUrl);\n    }\n  };\n}\n```\n\n```css\ninput { display: block; margin-bottom: 20px; }\nbutton { margin-left: 10px; }\n```\n\n</Sandpack>\n\n린터에 표시된 내용에 따라 채우세요:\n\n```js {6}\nfunction ChatRoom({ roomId }) {\n  useEffect(() => {\n    const connection = createConnection(serverUrl, roomId);\n    connection.connect();\n    return () => connection.disconnect();\n  }, [roomId]); // ✅ 모든 의존성 선언됨\n  // ...\n}\n```\n\n[Effect는 반응형 값에 \"반응\"합니다.](/learn/lifecycle-of-reactive-effects#effects-react-to-reactive-values) `roomId`는 반응형 값이므로(재렌더링으로 인해 변경될 수 있음), 린터는 이를 의존성으로 지정했는지 확인합니다. `roomId`가 다른 값을 받으면 React는 Effect를 다시 동기화합니다. 이렇게 하면 채팅이 선택된 방에 연결된 상태를 유지하고 드롭다운에 '반응'합니다.\n\n<Sandpack>\n\n```js\nimport { useState, useEffect } from 'react';\nimport { createConnection } from './chat.js';\n\nconst serverUrl = 'https://localhost:1234';\n\nfunction ChatRoom({ roomId }) {\n  useEffect(() => {\n    const connection = createConnection(serverUrl, roomId);\n    connection.connect();\n    return () => connection.disconnect();\n  }, [roomId]);\n  return <h1>Welcome to the {roomId} room!</h1>;\n}\n\nexport default function App() {\n  const [roomId, setRoomId] = useState('general');\n  return (\n    <>\n      <label>\n        Choose the chat room:{' '}\n        <select\n          value={roomId}\n          onChange={e => setRoomId(e.target.value)}\n        >\n          <option value=\"general\">general</option>\n          <option value=\"travel\">travel</option>\n          <option value=\"music\">music</option>\n        </select>\n      </label>\n      <hr />\n      <ChatRoom roomId={roomId} />\n    </>\n  );\n}\n```\n\n```js src/chat.js\nexport function createConnection(serverUrl, roomId) {\n  // 실제 구현에서는 서버에 연결됩니다\n  return {\n    connect() {\n      console.log('✅ Connecting to \"' + roomId + '\" room at ' + serverUrl + '...');\n    },\n    disconnect() {\n      console.log('❌ Disconnected from \"' + roomId + '\" room at ' + serverUrl);\n    }\n  };\n}\n```\n\n```css\ninput { display: block; margin-bottom: 20px; }\nbutton { margin-left: 10px; }\n```\n\n</Sandpack>\n\n### 의존성을 제거하려면 의존성이 아님을 증명하세요 {/*to-remove-a-dependency-prove-that-its-not-a-dependency*/}\n\nEffect의 의존성을 \"선택\"할 수 없다는 점에 유의하세요. Effect의 코드에서 사용되는 모든 <CodeStep step={2}>반응형 값</CodeStep>은 의존성 목록에 선언되어야 합니다. 의존성 목록은 주변 코드에 의해 결정됩니다.\n\n```js [[2, 3, \"roomId\"], [2, 5, \"roomId\"], [2, 8, \"roomId\"]]\nconst serverUrl = 'https://localhost:1234';\n\nfunction ChatRoom({ roomId }) { // 이것은 반응형 값입니다\n  useEffect(() => {\n    const connection = createConnection(serverUrl, roomId); // 이 Effect는 해당 반응형 값을 읽습니다\n    connection.connect();\n    return () => connection.disconnect();\n  }, [roomId]); // ✅ 따라서 해당 반응형 값을 Effect의 의존성으로 지정해야 합니다\n  // ...\n}\n```\n\n[반응형 값](/learn/lifecycle-of-reactive-effects#all-variables-declared-in-the-component-body-are-reactive)에는 props와 컴포넌트 내부에서 직접 선언된 모든 변수 및 함수가 포함됩니다. `roomId`는 반응형 값이므로 의존성 목록에서 제거할 수 없습니다. 린터가 허용하지 않습니다.\n\n```js {8}\nconst serverUrl = 'https://localhost:1234';\n\nfunction ChatRoom({ roomId }) {\n  useEffect(() => {\n    const connection = createConnection(serverUrl, roomId);\n    connection.connect();\n    return () => connection.disconnect();\n  }, []); // 🔴 React Hook useEffect의 의존성 'roomId'가 누락되었습니다.\n  // ...\n}\n```\n\n그리고 린터가 맞을 것입니다! `roomId`는 시간이 지남에 따라 변경될 수 있으므로 코드에 버그가 발생할 수 있습니다.\n\n**의존성을 제거하려면 해당 컴포넌트가 의존성이 될 *필요가 없다는 것*을 린터에 \"증명\"하세요.** 예를 들어 `roomId`를 컴포넌트 밖으로 이동시켜서 반응형값이 아니고 재렌더링 시에도 변경되지 않음을 증명할 수 있습니다.\n\n```js {2,9}\nconst serverUrl = 'https://localhost:1234';\nconst roomId = 'music'; // 더 이상 반응형 값이 아닙니다\n\nfunction ChatRoom() {\n  useEffect(() => {\n    const connection = createConnection(serverUrl, roomId);\n    connection.connect();\n    return () => connection.disconnect();\n  }, []); // ✅ 모든 의존성 선언됨\n  // ...\n}\n```\n\n이제 `roomId`는 반응형 값이 아니므로(재렌더링할 때 변경할 수 없으므로) 의존성이 될 필요가 없습니다.\n\n<Sandpack>\n\n```js\nimport { useState, useEffect } from 'react';\nimport { createConnection } from './chat.js';\n\nconst serverUrl = 'https://localhost:1234';\nconst roomId = 'music';\n\nexport default function ChatRoom() {\n  useEffect(() => {\n    const connection = createConnection(serverUrl, roomId);\n    connection.connect();\n    return () => connection.disconnect();\n  }, []);\n  return <h1>Welcome to the {roomId} room!</h1>;\n}\n```\n\n```js src/chat.js\nexport function createConnection(serverUrl, roomId) {\n  // 실제 구현에서는 서버에 연결됩니다\n  return {\n    connect() {\n      console.log('✅ Connecting to \"' + roomId + '\" room at ' + serverUrl + '...');\n    },\n    disconnect() {\n      console.log('❌ Disconnected from \"' + roomId + '\" room at ' + serverUrl);\n    }\n  };\n}\n```\n\n```css\ninput { display: block; margin-bottom: 20px; }\nbutton { margin-left: 10px; }\n```\n\n</Sandpack>\n\n이것이 [빈(`[]`) 의존성 목록](/learn/lifecycle-of-reactive-effects#what-an-effect-with-empty-dependencies-means)을 지정할 수 있는 이유입니다. Effect는 더 이상 반응형 값에 의존하지 않으므로 컴포넌트의 props나 State가 변경될 때 Effect를 다시 실행할 필요가 없습니다.\n\n### 의존성을 변경하려면 코드를 변경하세요. {/*to-change-the-dependencies-change-the-code*/}\n\n작업 흐름에서 패턴을 발견했을 수도 있습니다.\n\n1. 먼저 Effect의 코드 또는 반응형 값 선언 방식을 **변경**합니다.\n2. 그런 다음, **변경한 코드에 맞게** 의존성을 조정합니다.\n3. 의존성 목록이 마음에 들지 않으면 **첫 번째 단계로 돌아갑니다.** (그리고 코드를 다시 변경합니다.)\n\n마지막 부분이 중요합니다. 의존성을 변경하려면 먼저 주변 코드를 변경하세요. 의존성 목록은 [Effect의 코드에서 사용하는 모든 반응형 값의 목록](/learn/lifecycle-of-reactive-effects#react-verifies-that-you-specified-every-reactive-value-as-a-dependency)이라고 생각하면 됩니다. 이 목록에 무엇을 넣을지는 사용자가 선택하지 않습니다. 이 목록은 코드를 설명합니다. 의존성 목록을 변경하려면 코드를 변경하세요.\n\n이것은 방정식을 푸는 것처럼 느껴질 수 있습니다. 예를 들어 의존성 제거와 같은 목표를 설정하고 그 목표에 맞는 코드를 \"찾아야\" 합니다. 모든 사람이 방정식을 푸는 것을 재미있어하는 것은 아니며, Effect를 작성할 때도 마찬가지입니다! 다행히도 아래에 시도해 볼 수 있는 일반적인 레시피 목록이 있습니다.\n\n<Pitfall>\n\n기존 코드베이스가 있는 경우 이와 같이 린터를 억제하는 Effect가 있을 수 있습니다.\n\n```js {3-4}\nuseEffect(() => {\n  // ...\n  // 🔴 이렇게 린터를 억제하지 마세요:\n  // eslint-disable-next-line react-hooks/exhaustive-deps\n}, []);\n```\n\n**의존성이 코드와 일치하지 않으면 버그가 발생할 위험이 매우 높습니다.** 린터를 억제하면 Effect가 의존하는 값에 대해 React에 \"거짓말\"을 하게 됩니다.\n\n대신 다음에 소개할 기술을 사용하세요.\n\n</Pitfall>\n\n<DeepDive>\n\n#### 의존성 린터를 억제하는 것이 왜 위험한가요? {/*why-is-suppressing-the-dependency-linter-so-dangerous*/}\n\n린터를 억제하면 매우 직관적이지 않은 버그가 발생하여 찾아서 수정하기가 어렵습니다. 한 가지 예시를 들어보겠습니다.\n\n<Sandpack>\n\n```js\nimport { useState, useEffect } from 'react';\n\nexport default function Timer() {\n  const [count, setCount] = useState(0);\n  const [increment, setIncrement] = useState(1);\n\n  function onTick() {\n    setCount(count + increment);\n  }\n\n  useEffect(() => {\n    const id = setInterval(onTick, 1000);\n    return () => clearInterval(id);\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, []);\n\n  return (\n    <>\n      <h1>\n        Counter: {count}\n        <button onClick={() => setCount(0)}>Reset</button>\n      </h1>\n      <hr />\n      <p>\n        Every second, increment by:\n        <button disabled={increment === 0} onClick={() => {\n          setIncrement(i => i - 1);\n        }}>–</button>\n        <b>{increment}</b>\n        <button onClick={() => {\n          setIncrement(i => i + 1);\n        }}>+</button>\n      </p>\n    </>\n  );\n}\n```\n\n```css\nbutton { margin: 10px; }\n```\n\n</Sandpack>\n\n\"마운트할 때만\" Effect를 실행하고 싶다고 가정해 봅시다. [빈(`[]`) 의존성](/learn/lifecycle-of-reactive-effects#what-an-effect-with-empty-dependencies-means)이 그렇게 한다는 것을 읽었으므로 린터를 무시하고 `[]` 의존성을 강제로 지정하기로 결정했습니다.\n\n이 카운터는 두 개의 버튼으로 설정할 수 있는 양만큼 매초마다 증가해야 합니다. 하지만 이 Effect가 아무 것도 의존하지 않는다고 React에 \"거짓말\"을 했기 때문에, React는 초기 렌더링에서 계속 `onTick` 함수를 사용합니다. [이 렌더링에서](/learn/state-as-a-snapshot#rendering-takes-a-snapshot-in-time) `count`는 `0`이었고 `increment`는 `1`이었습니다. 그래서 이 렌더링의 `onTick`은 항상 매초마다 `setCount(0 + 1)`를 호출하고 항상 `1`이 표시됩니다. 이와 같은 버그는 여러 컴포넌트에 분산되어 있을 때 수정하기가 더 어렵습니다.\n\n린터를 무시하는 것보다 더 좋은 해결책은 항상 있습니다! 이 코드를 수정하려면 의존성 목록에 `onTick`을 추가해야 합니다. (interval을 한 번만 설정하려면 [`onTick`을 Effect 이벤트로 만드세요.](/learn/separating-events-from-effects#reading-latest-props-and-state-with-effect-events))\n\n**의존성 린트 오류는 컴파일 오류로 처리하는 것이 좋습니다. 이를 억제하지 않으면 이와 같은 버그가 발생하지 않습니다.** 이 페이지의 나머지 부분에서는 이 경우와 다른 경우에 대한 대안을 설명합니다.\n\n</DeepDive>\n\n## 불필요한 의존성 제거하기 {/*removing-unnecessary-dependencies*/}\n\n코드를 반영하기 위해 Effect의 의존성을 조정할 때마다 의존성 목록을 살펴보십시오. 이러한 의존성 중 하나라도 변경되면 Effect가 다시 실행되는 것이 합리적일까요? 가끔 대답은 \"아니오\"입니다.\n\n* 다른 조건에서 Effect의 *다른 부분*을 다시 실행하고 싶을 수도 있습니다.\n* 일부 의존성의 변경에 \"반응\"하지 않고 \"최신 값\"만 읽고 싶을 수도 있습니다.\n* 의존성은 객체나 함수이기 때문에 *의도치 않게* 너무 자주 변경될 수 있습니다.\n\n올바른 해결책을 찾으려면 Effect에 대한 몇 가지 질문에 답해야 합니다. 몇 가지 질문을 살펴봅시다.\n\n### 이 코드를 이벤트 핸들러로 옮겨야 하나요? {/*should-this-code-move-to-an-event-handler*/}\n\n가장 먼저 고려해야 할 것은 이 코드가 Effect 되어야 하는지 여부입니다.\n\n폼을 상상해 봅시다. 제출할 때 `submitted` State 변수를 `true`로 설정합니다. POST 요청을 보내고 알림을 표시해야 합니다. 이 로직은 `submitted`가 `true`가 될 때 \"반응\"하는 Effect 안에 넣었습니다.\n\n```js {6-8}\nfunction Form() {\n  const [submitted, setSubmitted] = useState(false);\n\n  useEffect(() => {\n    if (submitted) {\n      // 🔴 피하세요: Effect 내부에 이벤트별 로직\n      post('/api/register');\n      showNotification('Successfully registered!');\n    }\n  }, [submitted]);\n\n  function handleSubmit() {\n    setSubmitted(true);\n  }\n\n  // ...\n}\n```\n\n나중에 현재 테마에 따라 알림 메시지의 스타일을 지정하고 싶으므로 현재 테마를 읽습니다. `theme`는 컴포넌트 본문에서 선언되었기때문에 이는 반응형 값이므로 의존성으로 추가합니다.\n\n```js {3,9,11}\nfunction Form() {\n  const [submitted, setSubmitted] = useState(false);\n  const theme = useContext(ThemeContext);\n\n  useEffect(() => {\n    if (submitted) {\n      // 🔴 피하세요: Effect 내부에 이벤트별 로직\n      post('/api/register');\n      showNotification('Successfully registered!', theme);\n    }\n  }, [submitted, theme]); // ✅ 모든 의존성 선언됨\n\n  function handleSubmit() {\n    setSubmitted(true);\n  }\n\n  // ...\n}\n```\n\n이렇게 하면 버그가 발생하게 됩니다. 먼저 폼을 제출한 다음 어두운 테마와 밝은 테마 간 전환한다고 가정해 보겠습니다. `theme`이 변경되고 Effect가 다시 실행되어 동일한 알림이 다시 표시됩니다!\n\n**여기서 문제는 이것이 애초에 Effect가 아니어야 한다는 점입니다.** 이 POST 요청을 보내고 특정 상호작용인 폼 제출에 대한 응답으로 알림을 표시하고 싶다는 것입니다. 특정 상호작용에 대한 응답으로 일부 코드를 실행하려면 해당 로직을 해당 이벤트 핸들러에 직접 넣어야 합니다.\n\n```js {6-7}\nfunction Form() {\n  const theme = useContext(ThemeContext);\n\n  function handleSubmit() {\n    // ✅ 좋습니다: 이벤트별 로직은 이벤트 핸들러에서 호출됩니다\n    post('/api/register');\n    showNotification('Successfully registered!', theme);\n  }\n\n  // ...\n}\n```\n\n이제 코드가 이벤트 핸들러에 있고, 이는 반응형 코드가 아니므로 사용자가 폼을 제출할 때만 실행됩니다. [이벤트 핸들러와 Effect 중에서 선택하는 방법](/learn/separating-events-from-effects#reactive-values-and-reactive-logic)과 [불필요한 Effect를 삭제하는 방법](/learn/you-might-not-need-an-effect)에 대해 자세히 알아보세요.\n\n### Effect 가 관련 없는 여러 가지 작업을 수행하나요? {/*is-your-effect-doing-several-unrelated-things*/}\n\n다음으로 스스로에게 물어봐야 할 질문은 Effect가 서로 관련이 없는 여러 가지 작업을 수행하고 있는지 여부입니다.\n\n사용자가 도시와 지역을 선택해야 하는 배송 폼을 만든다고 가정해 보겠습니다. 선택한 `country`에 따라 서버에서 `cities` 목록을 가져와 드롭다운에 표시합니다.\n\n```js\nfunction ShippingForm({ country }) {\n  const [cities, setCities] = useState(null);\n  const [city, setCity] = useState(null);\n\n  useEffect(() => {\n    let ignore = false;\n    fetch(`/api/cities?country=${country}`)\n      .then(response => response.json())\n      .then(json => {\n        if (!ignore) {\n          setCities(json);\n        }\n      });\n    return () => {\n      ignore = true;\n    };\n  }, [country]); // ✅ 모든 의존성 선언됨\n\n  // ...\n```\n\n[Effect에서 데이터를 페칭하는](/learn/you-might-not-need-an-effect#fetching-data) 좋은 예시입니다. `country` props에 따라 `cities` State를 네트워크와 동기화하고 있습니다. `ShippingForm`이 표시되는 즉시 그리고 `country`가 변경될 때마다 (어떤 상호작용이 원인이든 상관없이) 데이터를 가져와야 하므로 이벤트 핸들러에서는 이 작업을 수행할 수 없습니다.\n\n이제 도시 지역에 대한 두 번째 셀렉트박스를 추가하여 현재 선택된 `city`의 `areas`을 가져온다고 가정해 보겠습니다. 동일한 Effect 내에 지역 목록에 대한 두 번째 `fetch` 호출을 추가하는 것으로 시작할 수 있습니다.\n\n```js {15-24,28}\nfunction ShippingForm({ country }) {\n  const [cities, setCities] = useState(null);\n  const [city, setCity] = useState(null);\n  const [areas, setAreas] = useState(null);\n\n  useEffect(() => {\n    let ignore = false;\n    fetch(`/api/cities?country=${country}`)\n      .then(response => response.json())\n      .then(json => {\n        if (!ignore) {\n          setCities(json);\n        }\n      });\n    // 🔴 피하세요: 단일 Effect가 두 개의 독립적인 프로세스를 동기화함\n    if (city) {\n      fetch(`/api/areas?city=${city}`)\n        .then(response => response.json())\n        .then(json => {\n          if (!ignore) {\n            setAreas(json);\n          }\n        });\n    }\n    return () => {\n      ignore = true;\n    };\n  }, [country, city]); // ✅ 모든 의존성 선언됨\n\n  // ...\n```\n\n하지만 이제 Effect가 `city` State 변수를 사용하므로 의존성 목록에 `city`를 추가해야 했습니다. 이로 인해 사용자가 다른 도시를 선택하면 Effect가 다시 실행되어 `fetchCities(country)`를 호출하는 문제가 발생했습니다. 결과적으로 불필요하게 도시 목록을 여러 번 다시 가져오게 됩니다.\n\n**이 코드의 문제점은 서로 관련이 없는 두 가지를 동기화하고 있다는 것입니다.**\n\n1. `country` props를 기반으로 `cities` State를 네트워크에 동기화하려고 합니다.\n2. `city` State를 기반으로 `areas` State를 네트워크에 동기화하려고 합니다.\n\n로직을 두 개의 Effect로 분할하고, 각 Effect는 동기화해야 하는 props에 반응합니다.\n\n\n```js {19-33}\nfunction ShippingForm({ country }) {\n  const [cities, setCities] = useState(null);\n  useEffect(() => {\n    let ignore = false;\n    fetch(`/api/cities?country=${country}`)\n      .then(response => response.json())\n      .then(json => {\n        if (!ignore) {\n          setCities(json);\n        }\n      });\n    return () => {\n      ignore = true;\n    };\n  }, [country]); // ✅ 모든 의존성 선언됨\n\n  const [city, setCity] = useState(null);\n  const [areas, setAreas] = useState(null);\n  useEffect(() => {\n    if (city) {\n      let ignore = false;\n      fetch(`/api/areas?city=${city}`)\n        .then(response => response.json())\n        .then(json => {\n          if (!ignore) {\n            setAreas(json);\n          }\n        });\n      return () => {\n        ignore = true;\n      };\n    }\n  }, [city]); // ✅ 모든 의존성 선언됨\n\n  // ...\n```\n\n이제 첫 번째 Effect는 `country`가 변경될 때만 다시 실행되고, 두 번째 Effect는 `city`가 변경될 때 다시 실행됩니다. 목적에 따라 분리했으니, 서로 다른 두 가지가 두 개의 개별 Effect에 의해 동기화됩니다. 두 개의 개별 Effect에는 두 개의 개별 의존성 목록이 있으므로 의도치 않게 서로를 트리거하지 않습니다.\n\n최종 코드는 원본보다 길지만 Effect를 분할하는 것이 여전히 정확합니다. [각 Effect는 독립적인 동기화 프로세스를 나타내야 합니다.](/learn/lifecycle-of-reactive-effects#each-effect-represents-a-separate-synchronization-process) 이 예시에서는 한 Effect를 삭제해도 다른 Effect의 로직이 깨지지 않습니다. 즉, *서로 다른 것을 동기화*하므로 분할하는 것이 좋습니다. [중복이 걱정된다면 반복되는 로직을 커스텀 훅으로 추출](/learn/reusing-logic-with-custom-hooks#when-to-use-custom-hooks)하여 이 코드를 개선할 수 있습니다.\n\n### 다음 State를 계산하기 위해 어떤 State를 읽고 있나요? {/*are-you-reading-some-state-to-calculate-the-next-state*/}\n\n이 Effect는 새 메시지가 도착할 때마다 새로 생성된 배열로 `messages` State 변수를 업데이트합니다.\n\n```js {2,6-8}\nfunction ChatRoom({ roomId }) {\n  const [messages, setMessages] = useState([]);\n  useEffect(() => {\n    const connection = createConnection();\n    connection.connect();\n    connection.on('message', (receivedMessage) => {\n      setMessages([...messages, receivedMessage]);\n    });\n    // ...\n```\n\n`messages` 변수를 사용하여 모든 기존 메시지로 시작하는 [새 배열을 생성](/learn/updating-arrays-in-state)하고 마지막에 새 메시지를 추가합니다. 하지만 `messages`는 Effect에서 읽는 반응형 값이므로 의존성이어야 합니다.\n\n```js {7,10}\nfunction ChatRoom({ roomId }) {\n  const [messages, setMessages] = useState([]);\n  useEffect(() => {\n    const connection = createConnection();\n    connection.connect();\n    connection.on('message', (receivedMessage) => {\n      setMessages([...messages, receivedMessage]);\n    });\n    return () => connection.disconnect();\n  }, [roomId, messages]); // ✅ 모든 의존성 선언됨\n  // ...\n```\n\n그리고 `messages`를 의존성으로 만들면 문제가 발생합니다.\n\n메시지를 수신할 때마다 `setMessages()`는 컴포넌트가 수신된 메시지를 포함하는 새 `messages` 배열로 재렌더링하도록 합니다. 하지만 이 Effect는 이제 `messages`에 따라 달라지므로 Effect도 다시 동기화됩니다. 따라서 새 메시지가 올 때마다 채팅이 다시 연결됩니다. 사용자가 원하지 않을 것입니다!\n\n이 문제를 해결하려면 Effect 내에서 `messages`를 읽지 마세요. 대신 [업데이터 함수](/reference/react/useState#updating-state-based-on-the-previous-state)를 `setMessages`에 전달하세요:\n\n```js {7,10}\nfunction ChatRoom({ roomId }) {\n  const [messages, setMessages] = useState([]);\n  useEffect(() => {\n    const connection = createConnection();\n    connection.connect();\n    connection.on('message', (receivedMessage) => {\n      setMessages(msgs => [...msgs, receivedMessage]);\n    });\n    return () => connection.disconnect();\n  }, [roomId]); // ✅ 모든 의존성 선언됨\n  // ...\n```\n\n**이제 Effect가 `messages` 변수를 전혀 읽지 않는 것을 알 수 있습니다.** `msgs => [...msgs, receivedMessage]`와 같은 업데이터 함수만 전달하면 됩니다. React는 [업데이터 함수를 대기열에 넣고](/learn/queueing-a-series-of-state-updates) 다음 렌더링 중에 `msgs` 인수를 제공합니다. 이 때문에 Effect 자체는 더 이상 `messages`에 의존할 필요가 없습니다. 이 수정으로 인해 채팅 메시지를 수신해도 더 이상 채팅이 다시 연결되지 않습니다.\n\n### 값의 변경에 '반응'하지 않고 값을 읽고 싶으신가요? {/*do-you-want-to-read-a-value-without-reacting-to-its-changes*/}\n\n사용자가 새 메시지를 받을 때 `isMuted`가 `true`가 아니면 소리를 재생하고 싶다고 가정해 보세요.\n\n```js {3,10-12}\nfunction ChatRoom({ roomId }) {\n  const [messages, setMessages] = useState([]);\n  const [isMuted, setIsMuted] = useState(false);\n\n  useEffect(() => {\n    const connection = createConnection();\n    connection.connect();\n    connection.on('message', (receivedMessage) => {\n      setMessages(msgs => [...msgs, receivedMessage]);\n      if (!isMuted) {\n        playSound();\n      }\n    });\n    // ...\n```\n\n이제 Effect의 코드에서 `isMuted`를 사용하므로 의존성에 추가해야 합니다.\n\n```js {10,15}\nfunction ChatRoom({ roomId }) {\n  const [messages, setMessages] = useState([]);\n  const [isMuted, setIsMuted] = useState(false);\n\n  useEffect(() => {\n    const connection = createConnection();\n    connection.connect();\n    connection.on('message', (receivedMessage) => {\n      setMessages(msgs => [...msgs, receivedMessage]);\n      if (!isMuted) {\n        playSound();\n      }\n    });\n    return () => connection.disconnect();\n  }, [roomId, isMuted]); // ✅ 모든 의존성 선언됨\n  // ...\n```\n\n문제는 (사용자가 `isMuted` 토글을 누르는 등) `isMuted`가 변경될 때마다 Effect가 다시 동기화되고 채팅에 다시 연결된다는 점입니다. 이는 바람직한 사용자 경험이 아닙니다! (이 예시에서는 린터를 비활성화해도 작동하지 않습니다. 그렇게 하면 `isMuted`가 이전 값으로 '고착'됩니다.)\n\n이 문제를 해결하려면 Effect에서 반응해서는 안 되는 로직을 추출해야 합니다. 이 Effect가 `isMuted`의 변경에 \"반응\"하지 않기를 원합니다. [이 비반응 로직을 Effect 이벤트로 옮기면 됩니다](/learn/separating-events-from-effects#declaring-an-effect-event):\n\n```js {1,7-12,18,21}\nimport { useState, useEffect, useEffectEvent } from 'react';\n\nfunction ChatRoom({ roomId }) {\n  const [messages, setMessages] = useState([]);\n  const [isMuted, setIsMuted] = useState(false);\n\n  const onMessage = useEffectEvent(receivedMessage => {\n    setMessages(msgs => [...msgs, receivedMessage]);\n    if (!isMuted) {\n      playSound();\n    }\n  });\n\n  useEffect(() => {\n    const connection = createConnection();\n    connection.connect();\n    connection.on('message', (receivedMessage) => {\n      onMessage(receivedMessage);\n    });\n    return () => connection.disconnect();\n  }, [roomId]); // ✅ 모든 의존성 선언됨\n  // ...\n```\n\nEffect 이벤트를 사용하면 Effect를 반응형 부분(`roomId`와 같은 반응형 값과 그 변경에 \"반응\"해야 하는)과 비반응형 부분(`onMessage`가 `isMuted`를 읽는 것처럼 최신 값만 읽는)으로 나눌 수 있습니다. **이제 Effect 이벤트 내에서 `isMuted`를 읽었으므로 Effect의 의존성이 될 필요가 없습니다.** 그 결과, \"Muted\" 설정을 켜고 끌 때 채팅이 다시 연결되지 않아 원래 문제가 해결되었습니다!\n\n#### props를 이벤트 핸들러로 감싸기 {/*wrapping-an-event-handler-from-the-props*/}\n\n컴포넌트가 이벤트 핸들러를 props로 받을 때 비슷한 문제가 발생할 수 있습니다.\n\n```js {1,8,11}\nfunction ChatRoom({ roomId, onReceiveMessage }) {\n  const [messages, setMessages] = useState([]);\n\n  useEffect(() => {\n    const connection = createConnection();\n    connection.connect();\n    connection.on('message', (receivedMessage) => {\n      onReceiveMessage(receivedMessage);\n    });\n    return () => connection.disconnect();\n  }, [roomId, onReceiveMessage]); // ✅ 모든 의존성 선언됨\n  // ...\n```\n\n부모 컴포넌트가 렌더링할 때마다 *다른* `onReceiveMessage` 함수를 전달한다고 가정해 보겠습니다.\n\n```js {3-5}\n<ChatRoom\n  roomId={roomId}\n  onReceiveMessage={receivedMessage => {\n    // ...\n  }}\n/>\n```\n\n`onReceiveMessage`는 의존성이므로 부모가 재렌더링할 때마다 Effect가 다시 동기화됩니다. 그러면 채팅에 다시 연결됩니다. 이 문제를 해결하려면 호출을 Effect 이벤트로 감싸세요:\n\n```js {4-6,12,15}\nfunction ChatRoom({ roomId, onReceiveMessage }) {\n  const [messages, setMessages] = useState([]);\n\n  const onMessage = useEffectEvent(receivedMessage => {\n    onReceiveMessage(receivedMessage);\n  });\n\n  useEffect(() => {\n    const connection = createConnection();\n    connection.connect();\n    connection.on('message', (receivedMessage) => {\n      onMessage(receivedMessage);\n    });\n    return () => connection.disconnect();\n  }, [roomId]); // ✅ 모든 의존성 선언됨\n  // ...\n```\n\nEffect 이벤트는 반응하지 않으므로 의존성으로 지정할 필요가 없습니다. 그 결과, 부모 컴포넌트가 재렌더링할 때마다 다른 함수를 전달하더라도 채팅이 더 이상 다시 연결되지 않습니다.\n\n#### 반응형 코드와 비반응형 코드 분리 {/*separating-reactive-and-non-reactive-code*/}\n\n이 예시에서는 `roomId`가 변경될 때마다 방문을 기록하려고 합니다. 모든 로그에 현재 `notificationCount`를 포함하고 싶지만 `notificationCount` 변경으로 로그 이벤트가 촉발하는 것은 원하지 않습니다.\n\n해결책은 다시 비반응형 코드를 Effect 이벤트로 분리하는 것입니다.\n\n```js {2-4,7}\nfunction Chat({ roomId, notificationCount }) {\n  const onVisit = useEffectEvent(visitedRoomId => {\n    logVisit(visitedRoomId, notificationCount);\n  });\n\n  useEffect(() => {\n    onVisit(roomId);\n  }, [roomId]); // ✅ 모든 의존성 선언됨\n  // ...\n}\n```\n\n로직이 `roomId`와 관련하여 반응하기를 원하므로 Effect 내부에서 `roomId`를 읽습니다. 그러나 `notificationCount`를 변경하여 추가 방문을 기록하는 것은 원하지 않으므로 Effect 이벤트 내부에서 `notificationCount`를 읽습니다. [Effect 이벤트를 사용하여 Effect에서 최신 props와 State를 읽는 방법에 대해 자세히 알아보세요.](/learn/separating-events-from-effects#reading-latest-props-and-state-with-effect-events)\n\n### 일부 반응형 값이 의도치 않게 변경되나요? {/*does-some-reactive-value-change-unintentionally*/}\n\nEffect가 특정 값에 '반응'하기를 원하지만, 그 값이 원하는 것보다 더 자주 변경되어 사용자의 관점에서 실제 변경 사항을 반영하지 못할 수도 있습니다. 예를 들어 컴포넌트 본문에 `options` 객체를 생성한 다음 Effect 내부에서 해당 객체를 읽는다고 가정해 보겠습니다.\n\n```js {3-6,9}\nfunction ChatRoom({ roomId }) {\n  // ...\n  const options = {\n    serverUrl: serverUrl,\n    roomId: roomId\n  };\n\n  useEffect(() => {\n    const connection = createConnection(options);\n    connection.connect();\n    // ...\n```\n\n이 객체는 컴포넌트 본문에서 선언되므로 [반응형 값](/learn/lifecycle-of-reactive-effects#effects-react-to-reactive-values)입니다. Effect 내에서 이와 같은 반응형 값을 읽으면 의존성으로 선언합니다. 이렇게 하면 Effect가 변경 사항에 \"반응\"하게 됩니다.\n\n```js {3,6}\n  // ...\n  useEffect(() => {\n    const connection = createConnection(options);\n    connection.connect();\n    return () => connection.disconnect();\n  }, [options]); // ✅ 모든 의존성 선언됨\n  // ...\n```\n\n의존성으로 선언하는 것이 중요합니다! 이렇게 하면 예를 들어 `roomId`가 변경되면 Effect가 새 `options`으로 채팅에 다시 연결됩니다. 하지만 위 코드에도 문제가 있습니다. 이를 확인하려면 아래 샌드박스의 인풋에 타이핑하고 콘솔에서 어떤 일이 발생하는지 살펴보세요:\n\n<Sandpack>\n\n```js\nimport { useState, useEffect } from 'react';\nimport { createConnection } from './chat.js';\n\nconst serverUrl = 'https://localhost:1234';\n\nfunction ChatRoom({ roomId }) {\n  const [message, setMessage] = useState('');\n\n  // 문제를 보여주기 위해 린터를 일시적으로 비활성화합니다.\n  // eslint-disable-next-line react-hooks/exhaustive-deps\n  const options = {\n    serverUrl: serverUrl,\n    roomId: roomId\n  };\n\n  useEffect(() => {\n    const connection = createConnection(options);\n    connection.connect();\n    return () => connection.disconnect();\n  }, [options]);\n\n  return (\n    <>\n      <h1>Welcome to the {roomId} room!</h1>\n      <input value={message} onChange={e => setMessage(e.target.value)} />\n    </>\n  );\n}\n\nexport default function App() {\n  const [roomId, setRoomId] = useState('general');\n  return (\n    <>\n      <label>\n        Choose the chat room:{' '}\n        <select\n          value={roomId}\n          onChange={e => setRoomId(e.target.value)}\n        >\n          <option value=\"general\">general</option>\n          <option value=\"travel\">travel</option>\n          <option value=\"music\">music</option>\n        </select>\n      </label>\n      <hr />\n      <ChatRoom roomId={roomId} />\n    </>\n  );\n}\n```\n\n```js src/chat.js\nexport function createConnection({ serverUrl, roomId }) {\n  // 실제 구현에서는 서버에 연결됩니다\n  return {\n    connect() {\n      console.log('✅ Connecting to \"' + roomId + '\" room at ' + serverUrl + '...');\n    },\n    disconnect() {\n      console.log('❌ Disconnected from \"' + roomId + '\" room at ' + serverUrl);\n    }\n  };\n}\n```\n\n```css\ninput { display: block; margin-bottom: 20px; }\nbutton { margin-left: 10px; }\n```\n\n</Sandpack>\n\n위의 샌드박스에서 입력은 `message` State 변수만 업데이트합니다. 사용자 입장에서는 이것이 채팅 연결에 영향을 미치지 않아야 합니다. 하지만 `message`를 업데이트할 때마다 컴포넌트가 재렌더링됩니다. 컴포넌트가 재렌더링되면 그 안에 있는 코드가 처음부터 다시 실행됩니다.\n\n`ChatRoom` 컴포넌트를 재렌더링할 때마다 새로운 `options` 객체가 처음부터 새로 생성됩니다. React는 `options` 객체가 마지막 렌더링 중에 생성된 `options` 객체와 *다른 객체*임을 인식합니다. 그렇기 때문에 (`options`에 따라 달라지는) Effect를 다시 동기화하고 사용자가 입력할 때 채팅이 다시 연결됩니다.\n\n**이 문제는 객체와 함수에만 영향을 줍니다. 자바스크립트에서는 새로 생성된 객체와 함수가 다른 모든 객체와 구별되는 것으로 간주됩니다. 그 안의 내용이 동일할 수 있다는 것은 중요하지 않습니다!**\n\n```js {7-8}\n// 첫 번째 렌더링 중\nconst options1 = { serverUrl: 'https://localhost:1234', roomId: 'music' };\n\n// 다음 렌더링 중\nconst options2 = { serverUrl: 'https://localhost:1234', roomId: 'music' };\n\n// 이 두 객체는 서로 다릅니다!\nconsole.log(Object.is(options1, options2)); // false\n```\n\n**객체 및 함수 의존성으로 인해 Effect가 필요 이상으로 자주 재동기화될 수 있습니다.**\n\n그렇기 때문에 가능하면 객체와 함수를 Effect의 의존성으로 사용하지 않는 것이 좋습니다. 대신 컴포넌트 외부나 Effect 내부로 이동하거나 원시 값을 추출해 보세요.\n\n#### 정적 객체와 함수를 컴포넌트 외부로 이동 {/*move-static-objects-and-functions-outside-your-component*/}\n\n객체가 props 및 State에 의존하지 않는 경우 해당 객체를 컴포넌트 외부로 이동할 수 있습니다.\n\n```js {1-4,13}\nconst options = {\n  serverUrl: 'https://localhost:1234',\n  roomId: 'music'\n};\n\nfunction ChatRoom() {\n  const [message, setMessage] = useState('');\n\n  useEffect(() => {\n    const connection = createConnection(options);\n    connection.connect();\n    return () => connection.disconnect();\n  }, []); // ✅ 모든 의존성 선언됨\n  // ...\n```\n\n이렇게 하면 린터가 반응하지 않는다는 것을 증명할 수 있습니다. 재렌더링의 결과로 변경될 수 없으므로 의존성이 될 필요가 없습니다. 이제 `ChatRoom`을 재렌더링해도 Effect가 다시 동기화되지 않습니다.\n\n이는 함수에도 적용됩니다.\n\n```js {1-6,12}\nfunction createOptions() {\n  return {\n    serverUrl: 'https://localhost:1234',\n    roomId: 'music'\n  };\n}\n\nfunction ChatRoom() {\n  const [message, setMessage] = useState('');\n\n  useEffect(() => {\n    const options = createOptions();\n    const connection = createConnection(options);\n    connection.connect();\n    return () => connection.disconnect();\n  }, []); // ✅ 모든 의존성 선언됨\n  // ...\n```\n\n`createOptions`는 컴포넌트 외부에서 선언되므로 반응형 값이 아닙니다. 그렇기 때문에 Effect의 의존성에 지정할 필요가 없으며, Effect가 다시 동기화되지 않는 이유이기도 합니다.\n\n#### Effect 내에서 동적 객체 및 함수 이동 {/*move-dynamic-objects-and-functions-inside-your-effect*/}\n\n객체가 `roomId` props처럼 재렌더링의 결과로 변경될 수 있는 반응형 값에 의존하는 경우, 컴포넌트 외부로 끌어낼 수 없습니다. 하지만 Effect의 코드 *내부*로 이동시킬 수는 있습니다.\n\n```js {7-10,11,14}\nconst serverUrl = 'https://localhost:1234';\n\nfunction ChatRoom({ roomId }) {\n  const [message, setMessage] = useState('');\n\n  useEffect(() => {\n    const options = {\n      serverUrl: serverUrl,\n      roomId: roomId\n    };\n    const connection = createConnection(options);\n    connection.connect();\n    return () => connection.disconnect();\n  }, [roomId]); // ✅ 모든 의존성 선언됨\n  // ...\n```\n\n이제 `options`이 Effect 내부에서 선언되었으므로 더 이상 Effect의 의존성이 아닙니다. 대신 Effect에서 사용하는 유일한 반응형 값은 `roomId`입니다. `roomId`는 객체나 함수가 아니기 때문에 의도치 않게 달라지지 않을 것이라고 확신할 수 있습니다. 자바스크립트에서 숫자와 문자열은 그 내용에 따라 비교됩니다.\n\n```js {7-8}\n// 첫 번째 렌더링 중\nconst roomId1 = 'music';\n\n// 다음 렌더링 중\nconst roomId2 = 'music';\n\n// 이 두 문자열은 동일합니다!\nconsole.log(Object.is(roomId1, roomId2)); // true\n```\n\n이 수정 덕분에 입력을 수정해도 더 이상 채팅이 다시 연결되지 않습니다.\n\n<Sandpack>\n\n```js\nimport { useState, useEffect } from 'react';\nimport { createConnection } from './chat.js';\n\nconst serverUrl = 'https://localhost:1234';\n\nfunction ChatRoom({ roomId }) {\n  const [message, setMessage] = useState('');\n\n  useEffect(() => {\n    const options = {\n      serverUrl: serverUrl,\n      roomId: roomId\n    };\n    const connection = createConnection(options);\n    connection.connect();\n    return () => connection.disconnect();\n  }, [roomId]);\n\n  return (\n    <>\n      <h1>Welcome to the {roomId} room!</h1>\n      <input value={message} onChange={e => setMessage(e.target.value)} />\n    </>\n  );\n}\n\nexport default function App() {\n  const [roomId, setRoomId] = useState('general');\n  return (\n    <>\n      <label>\n        Choose the chat room:{' '}\n        <select\n          value={roomId}\n          onChange={e => setRoomId(e.target.value)}\n        >\n          <option value=\"general\">general</option>\n          <option value=\"travel\">travel</option>\n          <option value=\"music\">music</option>\n        </select>\n      </label>\n      <hr />\n      <ChatRoom roomId={roomId} />\n    </>\n  );\n}\n```\n\n```js src/chat.js\nexport function createConnection({ serverUrl, roomId }) {\n  // 실제 구현에서는 서버에 연결됩니다\n  return {\n    connect() {\n      console.log('✅ Connecting to \"' + roomId + '\" room at ' + serverUrl + '...');\n    },\n    disconnect() {\n      console.log('❌ Disconnected from \"' + roomId + '\" room at ' + serverUrl);\n    }\n  };\n}\n```\n\n```css\ninput { display: block; margin-bottom: 20px; }\nbutton { margin-left: 10px; }\n```\n\n</Sandpack>\n\n그러나 예상대로 `roomId` 드롭다운을 변경하면 다시 연결됩니다.\n\n이는 함수에서도 마찬가지입니다.\n\n```js {7-12,14}\nconst serverUrl = 'https://localhost:1234';\n\nfunction ChatRoom({ roomId }) {\n  const [message, setMessage] = useState('');\n\n  useEffect(() => {\n    function createOptions() {\n      return {\n        serverUrl: serverUrl,\n        roomId: roomId\n      };\n    }\n\n    const options = createOptions();\n    const connection = createConnection(options);\n    connection.connect();\n    return () => connection.disconnect();\n  }, [roomId]); // ✅ 모든 의존성 선언됨\n  // ...\n```\n\nEffect 내에서 로직을 그룹화하기 위해 자신만의 함수를 작성할 수 있습니다. Effect 내부에서 선언하는 한, 반응형 값이 아니므로 Effect의 의존성이 될 필요가 없습니다.\n\n#### 객체에서 원시 값 읽기 {/*read-primitive-values-from-objects*/}\n\n가끔 props에서 객체를 받을 수도 있습니다.\n\n```js {1,5,8}\nfunction ChatRoom({ options }) {\n  const [message, setMessage] = useState('');\n\n  useEffect(() => {\n    const connection = createConnection(options);\n    connection.connect();\n    return () => connection.disconnect();\n  }, [options]); // ✅ 모든 의존성 선언됨\n  // ...\n```\n\n렌더링 중에 부모 컴포넌트가 객체를 생성한다는 점이 위험합니다.\n\n```js {3-6}\n<ChatRoom\n  roomId={roomId}\n  options={{\n    serverUrl: serverUrl,\n    roomId: roomId\n  }}\n/>\n```\n\n이렇게 하면 부모 컴포넌트가 재렌더링할 때마다 Effect가 다시 연결됩니다. 이 문제를 해결하려면 Effect 외부의 객체에서 정보를 읽고 객체 및 함수 의존성을 피하십시오:\n\n```js {4,7-8,12}\nfunction ChatRoom({ options }) {\n  const [message, setMessage] = useState('');\n\n  const { roomId, serverUrl } = options;\n  useEffect(() => {\n    const connection = createConnection({\n      roomId: roomId,\n      serverUrl: serverUrl\n    });\n    connection.connect();\n    return () => connection.disconnect();\n  }, [roomId, serverUrl]); // ✅ 모든 의존성 선언됨\n  // ...\n```\n\n로직은 약간 반복적입니다 (Effect 외부의 객체에서 일부 값을 읽은 다음 Effect 내부에 동일한 값을 가진 객체를 만듭니다). 하지만 Effect가 실제로 어떤 정보에 의존하는지 매우 명확하게 알 수 있습니다. 부모 컴포넌트에 의해 의도치 않게 객체가 다시 생성된 경우 채팅이 다시 연결되지 않습니다. 하지만 `options.roomId` 또는 `options.serverUrl`이 실제로 다른 경우 채팅이 다시 연결됩니다.\n\n#### 함수에서 원시값 계산 {/*calculate-primitive-values-from-functions*/}\n\n함수에 대해서도 동일한 접근 방식을 사용할 수 있습니다. 예를 들어 부모 컴포넌트가 함수를 전달한다고 가정해 보겠습니다.\n\n```js {3-8}\n<ChatRoom\n  roomId={roomId}\n  getOptions={() => {\n    return {\n      serverUrl: serverUrl,\n      roomId: roomId\n    };\n  }}\n/>\n```\n\n의존성을 만들지 않으려면 (그리고 재렌더링할 때 다시 연결되는 것을 방지하려면) Effect 외부에서 호출하세요. 이렇게 하면 객체가 아니며 Effect 내부에서 읽을 수 있는 `roomId` 및 `serverUrl` 값을 얻을 수 있습니다.\n\n```js {1,4}\nfunction ChatRoom({ getOptions }) {\n  const [message, setMessage] = useState('');\n\n  const { roomId, serverUrl } = getOptions();\n  useEffect(() => {\n    const connection = createConnection({\n      roomId: roomId,\n      serverUrl: serverUrl\n    });\n    connection.connect();\n    return () => connection.disconnect();\n  }, [roomId, serverUrl]); // ✅ 모든 의존성 선언됨\n  // ...\n```\n\n이는 렌더링 중에 호출해도 안전하므로 [순수](/learn/keeping-components-pure) 함수에서만 작동합니다. 함수가 이벤트 핸들러이지만 변경 사항으로 인해 Effect가 다시 동기화되는 것을 원하지 않는 경우, [대신 Effect 이벤트로 함수를 감싸세요.](#do-you-want-to-read-a-value-without-reacting-to-its-changes)\n\n<Recap>\n\n- 의존성은 항상 코드와 일치해야 합니다.\n- 의존성이 마음에 들지 않으면 코드를 수정해야 합니다.\n- 린터를 억제하면 매우 혼란스러운 버그가 발생하므로 항상 피해야 합니다.\n- 의존성을 제거하려면 해당 의존성이 필요하지 않다는 것을 린터에게 \"증명\"해야 합니다.\n- 특정 상호작용에 대한 응답으로 일부 코드가 실행되어야 하는 경우 해당 코드를 이벤트 핸들러로 이동하세요.\n- Effect의 다른 부분이 다른 이유로 다시 실행되어야 하는 경우 여러 개의 Effect로 분할하세요.\n- 이전 State를 기반으로 일부 State를 업데이트하려면 업데이터 함수를 전달하세요.\n- \"반응\"하지 않고 최신 값을 읽으려면 Effect에서 Effect 이벤트를 추출하세요.\n- 자바스크립트에서 객체와 함수는 서로 다른 시간에 생성된 경우 서로 다른 것으로 간주됩니다.\n- 객체와 함수의 의존성을 피하세요. 컴포넌트 외부나 Effect 내부로 이동하세요.\n\n</Recap>\n\n<Challenges>\n\n#### 인터벌 초기화 수정하기 {/*fix-a-resetting-interval*/}\n\n이 Effect는 매초마다 증가되는 인터벌을 설정합니다. 이상한 일이 발생하는 것을 발견했습니다. 인터벌이 증가될 때마다 인터벌이 파괴되고 다시 생성되는 것 같습니다. 인터벌이 계속 다시 생성되지 않도록 코드를 수정하세요.\n\n<Hint>\n\n이 Effect 코드가 `count`에 의존하는 것 같습니다. 이 의존성이 필요하지 않은 방법이 있을까요? 해당 값에 의존성을 추가하지 않고 이전 값을 기반으로 `count` State를 업데이트하는 방법이 있을 것입니다.\n\n</Hint>\n\n<Sandpack>\n\n```js\nimport { useState, useEffect } from 'react';\n\nexport default function Timer() {\n  const [count, setCount] = useState(0);\n\n  useEffect(() => {\n    console.log('✅ Creating an interval');\n    const id = setInterval(() => {\n      console.log('⏰ Interval tick');\n      setCount(count + 1);\n    }, 1000);\n    return () => {\n      console.log('❌ Clearing an interval');\n      clearInterval(id);\n    };\n  }, [count]);\n\n  return <h1>Counter: {count}</h1>\n}\n```\n\n</Sandpack>\n\n<Solution>\n\nEffect 내부에서 `count` State를 `count + 1`로 업데이트하고 싶습니다. 그러나 이렇게 하면 Effect가 틱할 때마다 변경되는 `count`에 의존하게되므로 매 틱마다 인터벌이 다시 만들어집니다.\n\n이 문제를 해결하려면 [업데이터 함수](/reference/react/useState#updating-state-based-on-the-previous-state)를 사용하여 `setCount(count + 1)` 대신 `setCount(c => c + 1)`를 작성합니다.\n\n<Sandpack>\n\n```js\nimport { useState, useEffect } from 'react';\n\nexport default function Timer() {\n  const [count, setCount] = useState(0);\n\n  useEffect(() => {\n    console.log('✅ Creating an interval');\n    const id = setInterval(() => {\n      console.log('⏰ Interval tick');\n      setCount(c => c + 1);\n    }, 1000);\n    return () => {\n      console.log('❌ Clearing an interval');\n      clearInterval(id);\n    };\n  }, []);\n\n  return <h1>Counter: {count}</h1>\n}\n```\n\n</Sandpack>\n\nEffect 내부에서 `count`를 읽는 대신 `c => c + 1` 명령어(\"이 숫자를 증가시켜라!\")를 React에 전달합니다. React는 다음 렌더링에 이를 적용합니다. 그리고 Effect 내부에서 `count` 값을 더 이상 읽을 필요가 없으므로 Effect의 의존성을 비워둘 수 있습니다(`[]`). 이렇게 하면 매 틱마다 Effect가 인터벌을 다시 생성하지 않아도 됩니다.\n\n</Solution>\n\n#### 애니메이션을 다시 촉발하는 현상 고치기 {/*fix-a-retriggering-animation*/}\n\n이 예시에서는 \"Show\"를 누르면 환영 메시지가 페이드인 합니다. 애니메이션은 1초 정도 걸립니다.\"Remove\"를 누르면 환영 메시지가 즉시 사라집니다. 페이드인 애니메이션의 로직은 `animation.js` 파일에서 일반 자바스크립트 애니메이션 루프로 구현됩니다. 이 로직을 변경할 필요는 없습니다. 서드파티 라이브러리로 처리하면 됩니다. Effect는 DOM 노드에 대한 `FadeInAnimation` 인스턴스를 생성한 다음 `start(duration)` 또는 `stop()`을 호출하여 애니메이션을 제어합니다. `duration`은 슬라이더로 제어합니다. 슬라이더를 조정하여 애니메이션이 어떻게 변하는지 확인하세요.\n\n이 코드는 이미 작동하지만 변경하고 싶은 부분이 있습니다. 현재 `duration` State 변수를 제어하는 슬라이더를 움직이면 애니메이션이 다시 촉발됩니다. Effect가 `duration` 변수에 \"반응\"하지 않도록 동작을 변경하세요. \"Show\"를 누르면 Effect는 슬라이더의 현재 `duration`을 사용해야 합니다. 그러나 슬라이더를 움직이는 것만으로 애니메이션이 다시 촉발되어서는 안 됩니다.\n\n<Hint>\n\nEffect 안에 반응성이 없어야 하는 코드가 있나요? 비반응형 코드를 Effect 밖으로 옮기려면 어떻게 해야 하나요?\n\n</Hint>\n\n<Sandpack>\n\n```js\nimport { useState, useEffect, useRef } from 'react';\nimport { useEffectEvent } from 'react';\nimport { FadeInAnimation } from './animation.js';\n\nfunction Welcome({ duration }) {\n  const ref = useRef(null);\n\n  useEffect(() => {\n    const animation = new FadeInAnimation(ref.current);\n    animation.start(duration);\n    return () => {\n      animation.stop();\n    };\n  }, [duration]);\n\n  return (\n    <h1\n      ref={ref}\n      style={{\n        opacity: 0,\n        color: 'white',\n        padding: 50,\n        textAlign: 'center',\n        fontSize: 50,\n        backgroundImage: 'radial-gradient(circle, rgba(63,94,251,1) 0%, rgba(252,70,107,1) 100%)'\n      }}\n    >\n      Welcome\n    </h1>\n  );\n}\n\nexport default function App() {\n  const [duration, setDuration] = useState(1000);\n  const [show, setShow] = useState(false);\n\n  return (\n    <>\n      <label>\n        <input\n          type=\"range\"\n          min=\"100\"\n          max=\"3000\"\n          value={duration}\n          onChange={e => setDuration(Number(e.target.value))}\n        />\n        <br />\n        Fade in duration: {duration} ms\n      </label>\n      <button onClick={() => setShow(!show)}>\n        {show ? 'Remove' : 'Show'}\n      </button>\n      <hr />\n      {show && <Welcome duration={duration} />}\n    </>\n  );\n}\n```\n\n```js src/animation.js\nexport class FadeInAnimation {\n  constructor(node) {\n    this.node = node;\n  }\n  start(duration) {\n    this.duration = duration;\n    if (this.duration === 0) {\n      // 즉시 끝으로 이동\n      this.onProgress(1);\n    } else {\n      this.onProgress(0);\n      // 애니메이션 시작\n      this.startTime = performance.now();\n      this.frameId = requestAnimationFrame(() => this.onFrame());\n    }\n  }\n  onFrame() {\n    const timePassed = performance.now() - this.startTime;\n    const progress = Math.min(timePassed / this.duration, 1);\n    this.onProgress(progress);\n    if (progress < 1) {\n      // 아직 더 그릴 프레임이 있습니다\n      this.frameId = requestAnimationFrame(() => this.onFrame());\n    }\n  }\n  onProgress(progress) {\n    this.node.style.opacity = progress;\n  }\n  stop() {\n    cancelAnimationFrame(this.frameId);\n    this.startTime = null;\n    this.frameId = null;\n    this.duration = 0;\n  }\n}\n```\n\n```css\nlabel, button { display: block; margin-bottom: 20px; }\nhtml, body { min-height: 300px; }\n```\n\n</Sandpack>\n\n<Solution>\n\nEffect는 `duration`의 최신 값을 읽어야 하지만, `duration`의 변화에 \"반응\"하지 않기를 원합니다. 애니메이션을 시작하기 위해 `duration`을 사용하지만 애니메이션이 시작해도 반응하지 않습니다. 반응하지 않는 코드를 추출하고 Effect에서 해당 함수를 호출합니다.\n\n<Sandpack>\n\n```js\nimport { useState, useEffect, useRef } from 'react';\nimport { FadeInAnimation } from './animation.js';\nimport { useEffectEvent } from 'react';\n\nfunction Welcome({ duration }) {\n  const ref = useRef(null);\n\n  const onAppear = useEffectEvent(animation => {\n    animation.start(duration);\n  });\n\n  useEffect(() => {\n    const animation = new FadeInAnimation(ref.current);\n    onAppear(animation);\n    return () => {\n      animation.stop();\n    };\n  }, []);\n\n  return (\n    <h1\n      ref={ref}\n      style={{\n        opacity: 0,\n        color: 'white',\n        padding: 50,\n        textAlign: 'center',\n        fontSize: 50,\n        backgroundImage: 'radial-gradient(circle, rgba(63,94,251,1) 0%, rgba(252,70,107,1) 100%)'\n      }}\n    >\n      Welcome\n    </h1>\n  );\n}\n\nexport default function App() {\n  const [duration, setDuration] = useState(1000);\n  const [show, setShow] = useState(false);\n\n  return (\n    <>\n      <label>\n        <input\n          type=\"range\"\n          min=\"100\"\n          max=\"3000\"\n          value={duration}\n          onChange={e => setDuration(Number(e.target.value))}\n        />\n        <br />\n        Fade in duration: {duration} ms\n      </label>\n      <button onClick={() => setShow(!show)}>\n        {show ? 'Remove' : 'Show'}\n      </button>\n      <hr />\n      {show && <Welcome duration={duration} />}\n    </>\n  );\n}\n```\n\n```js src/animation.js\nexport class FadeInAnimation {\n  constructor(node) {\n    this.node = node;\n  }\n  start(duration) {\n    this.duration = duration;\n    this.onProgress(0);\n    this.startTime = performance.now();\n    this.frameId = requestAnimationFrame(() => this.onFrame());\n  }\n  onFrame() {\n    const timePassed = performance.now() - this.startTime;\n    const progress = Math.min(timePassed / this.duration, 1);\n    this.onProgress(progress);\n    if (progress < 1) {\n      // 아직 더 그릴 프레임이 있습니다\n      this.frameId = requestAnimationFrame(() => this.onFrame());\n    }\n  }\n  onProgress(progress) {\n    this.node.style.opacity = progress;\n  }\n  stop() {\n    cancelAnimationFrame(this.frameId);\n    this.startTime = null;\n    this.frameId = null;\n    this.duration = 0;\n  }\n}\n```\n\n```css\nlabel, button { display: block; margin-bottom: 20px; }\nhtml, body { min-height: 300px; }\n```\n\n</Sandpack>\n\n`onAppear`와 같은 Effect 이벤트는 반응형 이벤트가 아니므로 애니메이션을 다시 촉발시키지 않고도 내부의 `duration`을 읽을 수 있습니다.\n\n</Solution>\n\n#### 채팅 재연결 문제 해결하기 {/*fix-a-reconnecting-chat*/}\n\n이 예시에서는 'Toggle theme'을 누를 때마다 채팅이 다시 연결됩니다. 왜 이런 일이 발생하나요? 서버 URL을 편집하거나 다른 채팅방을 선택할 때만 채팅이 다시 연결되도록 실수를 수정하세요.\n\n`chat.js`를 외부 서드파티 라이브러리로 취급하여 API를 확인하기 위해 참조할 수는 있지만 편집해서는 안됩니다.\n\n<Hint>\n\n이 문제를 해결하는 방법은 여러 가지가 있지만 궁극적으로 객체를 의존성으로 사용하지 않으려는 것입니다.\n\n</Hint>\n\n<Sandpack>\n\n```js src/App.js\nimport { useState } from 'react';\nimport ChatRoom from './ChatRoom.js';\n\nexport default function App() {\n  const [isDark, setIsDark] = useState(false);\n  const [roomId, setRoomId] = useState('general');\n  const [serverUrl, setServerUrl] = useState('https://localhost:1234');\n\n  const options = {\n    serverUrl: serverUrl,\n    roomId: roomId\n  };\n\n  return (\n    <div className={isDark ? 'dark' : 'light'}>\n      <button onClick={() => setIsDark(!isDark)}>\n        Toggle theme\n      </button>\n      <label>\n        Server URL:{' '}\n        <input\n          value={serverUrl}\n          onChange={e => setServerUrl(e.target.value)}\n        />\n      </label>\n      <label>\n        Choose the chat room:{' '}\n        <select\n          value={roomId}\n          onChange={e => setRoomId(e.target.value)}\n        >\n          <option value=\"general\">general</option>\n          <option value=\"travel\">travel</option>\n          <option value=\"music\">music</option>\n        </select>\n      </label>\n      <hr />\n      <ChatRoom options={options} />\n    </div>\n  );\n}\n```\n\n```js src/ChatRoom.js active\nimport { useEffect } from 'react';\nimport { createConnection } from './chat.js';\n\nexport default function ChatRoom({ options }) {\n  useEffect(() => {\n    const connection = createConnection(options);\n    connection.connect();\n    return () => connection.disconnect();\n  }, [options]);\n\n  return <h1>Welcome to the {options.roomId} room!</h1>;\n}\n```\n\n```js src/chat.js\nexport function createConnection({ serverUrl, roomId }) {\n  // 실제 구현에서는 서버에 연결됩니다\n  if (typeof serverUrl !== 'string') {\n    throw Error('Expected serverUrl to be a string. Received: ' + serverUrl);\n  }\n  if (typeof roomId !== 'string') {\n    throw Error('Expected roomId to be a string. Received: ' + roomId);\n  }\n  return {\n    connect() {\n      console.log('✅ Connecting to \"' + roomId + '\" room at ' + serverUrl + '...');\n    },\n    disconnect() {\n      console.log('❌ Disconnected from \"' + roomId + '\" room at ' + serverUrl);\n    }\n  };\n}\n```\n\n```css\nlabel, button { display: block; margin-bottom: 5px; }\n.dark { background: #222; color: #eee; }\n```\n\n</Sandpack>\n\n<Solution>\n\nEffect가 `options` 객체에 의존하기 때문에 다시 실행되고 있습니다. 객체는 의도치 않게 다시 생성될 수 있으므로 가능하면 Effect의 의존성 요소로 지정하지 않도록 해야 합니다.\n\n가장 덜 공격적인 수정 방법은 Effect 외부에서 `roomId`와 `serverUrl`을 읽은 다음 Effect가 이러한 기본 값에 의존하도록 만드는 것입니다(의도치 않게 변경할 수 없음). Effect 내부에서, 객체를 생성하고 이를 `createConnection`에 전달합니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { useState } from 'react';\nimport ChatRoom from './ChatRoom.js';\n\nexport default function App() {\n  const [isDark, setIsDark] = useState(false);\n  const [roomId, setRoomId] = useState('general');\n  const [serverUrl, setServerUrl] = useState('https://localhost:1234');\n\n  const options = {\n    serverUrl: serverUrl,\n    roomId: roomId\n  };\n\n  return (\n    <div className={isDark ? 'dark' : 'light'}>\n      <button onClick={() => setIsDark(!isDark)}>\n        Toggle theme\n      </button>\n      <label>\n        Server URL:{' '}\n        <input\n          value={serverUrl}\n          onChange={e => setServerUrl(e.target.value)}\n        />\n      </label>\n      <label>\n        Choose the chat room:{' '}\n        <select\n          value={roomId}\n          onChange={e => setRoomId(e.target.value)}\n        >\n          <option value=\"general\">general</option>\n          <option value=\"travel\">travel</option>\n          <option value=\"music\">music</option>\n        </select>\n      </label>\n      <hr />\n      <ChatRoom options={options} />\n    </div>\n  );\n}\n```\n\n```js src/ChatRoom.js active\nimport { useEffect } from 'react';\nimport { createConnection } from './chat.js';\n\nexport default function ChatRoom({ options }) {\n  const { roomId, serverUrl } = options;\n  useEffect(() => {\n    const connection = createConnection({\n      roomId: roomId,\n      serverUrl: serverUrl\n    });\n    connection.connect();\n    return () => connection.disconnect();\n  }, [roomId, serverUrl]);\n\n  return <h1>Welcome to the {options.roomId} room!</h1>;\n}\n```\n\n```js src/chat.js\nexport function createConnection({ serverUrl, roomId }) {\n  // 실제 구현에서는 서버에 연결됩니다\n  if (typeof serverUrl !== 'string') {\n    throw Error('Expected serverUrl to be a string. Received: ' + serverUrl);\n  }\n  if (typeof roomId !== 'string') {\n    throw Error('Expected roomId to be a string. Received: ' + roomId);\n  }\n  return {\n    connect() {\n      console.log('✅ Connecting to \"' + roomId + '\" room at ' + serverUrl + '...');\n    },\n    disconnect() {\n      console.log('❌ Disconnected from \"' + roomId + '\" room at ' + serverUrl);\n    }\n  };\n}\n```\n\n```css\nlabel, button { display: block; margin-bottom: 5px; }\n.dark { background: #222; color: #eee; }\n```\n\n</Sandpack>\n\n객체의 `options` props를 보다 구체적인 `roomId` 및 `serverUrl` props로 대체하는 것이 더 좋을 것입니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { useState } from 'react';\nimport ChatRoom from './ChatRoom.js';\n\nexport default function App() {\n  const [isDark, setIsDark] = useState(false);\n  const [roomId, setRoomId] = useState('general');\n  const [serverUrl, setServerUrl] = useState('https://localhost:1234');\n\n  return (\n    <div className={isDark ? 'dark' : 'light'}>\n      <button onClick={() => setIsDark(!isDark)}>\n        Toggle theme\n      </button>\n      <label>\n        Server URL:{' '}\n        <input\n          value={serverUrl}\n          onChange={e => setServerUrl(e.target.value)}\n        />\n      </label>\n      <label>\n        Choose the chat room:{' '}\n        <select\n          value={roomId}\n          onChange={e => setRoomId(e.target.value)}\n        >\n          <option value=\"general\">general</option>\n          <option value=\"travel\">travel</option>\n          <option value=\"music\">music</option>\n        </select>\n      </label>\n      <hr />\n      <ChatRoom\n        roomId={roomId}\n        serverUrl={serverUrl}\n      />\n    </div>\n  );\n}\n```\n\n```js src/ChatRoom.js active\nimport { useState, useEffect } from 'react';\nimport { createConnection } from './chat.js';\n\nexport default function ChatRoom({ roomId, serverUrl }) {\n  useEffect(() => {\n    const connection = createConnection({\n      roomId: roomId,\n      serverUrl: serverUrl\n    });\n    connection.connect();\n    return () => connection.disconnect();\n  }, [roomId, serverUrl]);\n\n  return <h1>Welcome to the {roomId} room!</h1>;\n}\n```\n\n```js src/chat.js\nexport function createConnection({ serverUrl, roomId }) {\n  // 실제 구현에서는 서버에 연결됩니다\n  if (typeof serverUrl !== 'string') {\n    throw Error('Expected serverUrl to be a string. Received: ' + serverUrl);\n  }\n  if (typeof roomId !== 'string') {\n    throw Error('Expected roomId to be a string. Received: ' + roomId);\n  }\n  return {\n    connect() {\n      console.log('✅ Connecting to \"' + roomId + '\" room at ' + serverUrl + '...');\n    },\n    disconnect() {\n      console.log('❌ Disconnected from \"' + roomId + '\" room at ' + serverUrl);\n    }\n  };\n}\n```\n\n```css\nlabel, button { display: block; margin-bottom: 5px; }\n.dark { background: #222; color: #eee; }\n```\n\n</Sandpack>\n\n가능하면 원시 props를 사용하면 나중에 컴포넌트를 더 쉽게 최적화할 수 있습니다.\n\n</Solution>\n\n#### 다시, 채팅 재연결 문제 수정하기 {/*fix-a-reconnecting-chat-again*/}\n\n이 예시는 암호화 여부와 상관없이 채팅에 연결됩니다. 체크박스를 토글하면 암호화가 켜져 있을 때와 꺼져 있을 때 콘솔에 표시되는 메시지가 달라지는 것을 확인할 수 있습니다. 채팅방을 변경해 보세요. 그런 다음 테마를 토글해 보세요. 채팅방에 연결되면 몇 초마다 새 메시지를 받게 됩니다. 메시지의 색상이 선택한 테마와 일치하는지 확인하세요.\n\n이 예시에서는 테마를 변경하려고 할 때마다 채팅이 다시 연결됩니다. 이 문제를 수정하세요. 수정 후 테마를 변경해도 채팅이 다시 연결되지 않지만, 암호화 설정을 토글하거나 채팅방을 변경하면 다시 연결됩니다.\n\n`chat.js`의 코드를 변경하지 마세요. 그 외에는 동일한 동작을 초래하는 한 어떤 코드든 변경할 수 있습니다. 예를 들어 어떤 props가 전달되는지를 확인하고 변경하는 것이 도움이 될 수 있습니다.\n\n<Hint>\n\n두 개의 함수를 전달하고 있습니다. `onMessage`와 `createConnection`입니다. 이 두 함수는 `App`이 다시 렌더링할 때마다 처음부터 새로 생성됩니다. 매번 새로운 값으로 간주되기 때문에 Effect를 다시 촉발시킵니다.\n\n이러한 함수 중 하나가 이벤트 핸들러입니다. 이벤트 핸들러 함수의 새 값에 '반응'하지 않고 이벤트 핸들러를 Effect로 호출하는 방법을 알고 계신가요? 유용할 것 같습니다!\n\n이러한 함수 중 다른 함수는 가져온 API 메서드에 일부 State를 전달하기 위해서만 존재합니다. 이 함수가 정말 필요한가요? 전달되는 필수 정보는 무엇인가요? 일부 import를 `App.js`에서 `ChatRoom.js`로 옮겨야 할 수도 있습니다.\n\n</Hint>\n\n<Sandpack>\n\n```json package.json hidden\n{\n  \"dependencies\": {\n    \"react\": \"latest\",\n    \"react-dom\": \"latest\",\n    \"react-scripts\": \"latest\",\n    \"toastify-js\": \"1.12.0\"\n  },\n  \"scripts\": {\n    \"start\": \"react-scripts start\",\n    \"build\": \"react-scripts build\",\n    \"test\": \"react-scripts test --env=jsdom\",\n    \"eject\": \"react-scripts eject\"\n  }\n}\n```\n\n```js src/App.js\nimport { useState } from 'react';\nimport ChatRoom from './ChatRoom.js';\nimport {\n  createEncryptedConnection,\n  createUnencryptedConnection,\n} from './chat.js';\nimport { showNotification } from './notifications.js';\n\nexport default function App() {\n  const [isDark, setIsDark] = useState(false);\n  const [roomId, setRoomId] = useState('general');\n  const [isEncrypted, setIsEncrypted] = useState(false);\n\n  return (\n    <>\n      <label>\n        <input\n          type=\"checkbox\"\n          checked={isDark}\n          onChange={e => setIsDark(e.target.checked)}\n        />\n        Use dark theme\n      </label>\n      <label>\n        <input\n          type=\"checkbox\"\n          checked={isEncrypted}\n          onChange={e => setIsEncrypted(e.target.checked)}\n        />\n        Enable encryption\n      </label>\n      <label>\n        Choose the chat room:{' '}\n        <select\n          value={roomId}\n          onChange={e => setRoomId(e.target.value)}\n        >\n          <option value=\"general\">general</option>\n          <option value=\"travel\">travel</option>\n          <option value=\"music\">music</option>\n        </select>\n      </label>\n      <hr />\n      <ChatRoom\n        roomId={roomId}\n        onMessage={msg => {\n          showNotification('New message: ' + msg, isDark ? 'dark' : 'light');\n        }}\n        createConnection={() => {\n          const options = {\n            serverUrl: 'https://localhost:1234',\n            roomId: roomId\n          };\n          if (isEncrypted) {\n            return createEncryptedConnection(options);\n          } else {\n            return createUnencryptedConnection(options);\n          }\n        }}\n      />\n    </>\n  );\n}\n```\n\n```js src/ChatRoom.js active\nimport { useState, useEffect } from 'react';\nimport { useEffectEvent } from 'react';\n\nexport default function ChatRoom({ roomId, createConnection, onMessage }) {\n  useEffect(() => {\n    const connection = createConnection();\n    connection.on('message', (msg) => onMessage(msg));\n    connection.connect();\n    return () => connection.disconnect();\n  }, [createConnection, onMessage]);\n\n  return <h1>Welcome to the {roomId} room!</h1>;\n}\n```\n\n```js src/chat.js\nexport function createEncryptedConnection({ serverUrl, roomId }) {\n  // 실제 구현에서는 서버에 연결됩니다\n  if (typeof serverUrl !== 'string') {\n    throw Error('Expected serverUrl to be a string. Received: ' + serverUrl);\n  }\n  if (typeof roomId !== 'string') {\n    throw Error('Expected roomId to be a string. Received: ' + roomId);\n  }\n  let intervalId;\n  let messageCallback;\n  return {\n    connect() {\n      console.log('✅ 🔐 Connecting to \"' + roomId + '\" room... (encrypted)');\n      clearInterval(intervalId);\n      intervalId = setInterval(() => {\n        if (messageCallback) {\n          if (Math.random() > 0.5) {\n            messageCallback('hey')\n          } else {\n            messageCallback('lol');\n          }\n        }\n      }, 3000);\n    },\n    disconnect() {\n      clearInterval(intervalId);\n      messageCallback = null;\n      console.log('❌ 🔐 Disconnected from \"' + roomId + '\" room (encrypted)');\n    },\n    on(event, callback) {\n      if (messageCallback) {\n        throw Error('Cannot add the handler twice.');\n      }\n      if (event !== 'message') {\n        throw Error('Only \"message\" event is supported.');\n      }\n      messageCallback = callback;\n    },\n  };\n}\n\nexport function createUnencryptedConnection({ serverUrl, roomId }) {\n  // 실제 구현에서는 서버에 연결됩니다\n  if (typeof serverUrl !== 'string') {\n    throw Error('Expected serverUrl to be a string. Received: ' + serverUrl);\n  }\n  if (typeof roomId !== 'string') {\n    throw Error('Expected roomId to be a string. Received: ' + roomId);\n  }\n  let intervalId;\n  let messageCallback;\n  return {\n    connect() {\n      console.log('✅ Connecting to \"' + roomId + '\" room (unencrypted)...');\n      clearInterval(intervalId);\n      intervalId = setInterval(() => {\n        if (messageCallback) {\n          if (Math.random() > 0.5) {\n            messageCallback('hey')\n          } else {\n            messageCallback('lol');\n          }\n        }\n      }, 3000);\n    },\n    disconnect() {\n      clearInterval(intervalId);\n      messageCallback = null;\n      console.log('❌ Disconnected from \"' + roomId + '\" room (unencrypted)');\n    },\n    on(event, callback) {\n      if (messageCallback) {\n        throw Error('Cannot add the handler twice.');\n      }\n      if (event !== 'message') {\n        throw Error('Only \"message\" event is supported.');\n      }\n      messageCallback = callback;\n    },\n  };\n}\n```\n\n```js src/notifications.js\nimport Toastify from 'toastify-js';\nimport 'toastify-js/src/toastify.css';\n\nexport function showNotification(message, theme) {\n  Toastify({\n    text: message,\n    duration: 2000,\n    gravity: 'top',\n    position: 'right',\n    style: {\n      background: theme === 'dark' ? 'black' : 'white',\n      color: theme === 'dark' ? 'white' : 'black',\n    },\n  }).showToast();\n}\n```\n\n```css\nlabel, button { display: block; margin-bottom: 5px; }\n```\n\n</Sandpack>\n\n<Solution>\n\n이 문제를 해결하는 올바른 방법은 여러 가지가 있는데, 그 중 한 가지 해결책을 소개합니다.\n\n원래 예시에서는 테마를 변경하면 다른 `onMessage` 및 `createConnection` 함수가 생성되어 전달되었습니다. Effect가 이러한 함수에 의존했기 때문에 테마를 전환할 때마다 채팅이 다시 연결되었습니다.\n\n`message`의 문제를 해결하려면 `onMessage`를 Effect 이벤트로 감싸야 했습니다.\n\n```js {1,2,6}\nexport default function ChatRoom({ roomId, createConnection, onMessage }) {\n  const onReceiveMessage = useEffectEvent(onMessage);\n\n  useEffect(() => {\n    const connection = createConnection();\n    connection.on('message', (msg) => onReceiveMessage(msg));\n    // ...\n```\n\n`onMessage` props와 달리 `onReceiveMessage` Effect 이벤트는 반응하지 않습니다. 그렇기 때문에 Effect의 의존성이 될 필요가 없습니다. 따라서 `onMessage`를 변경해도 채팅이 다시 연결되지 않습니다.\n\n반응형이어야 하기 때문에 `createConnection`으로는 동일한 작업을 수행할 수 없습니다. 사용자가 암호화 연결과 비암호화 연결 사이를 전환하거나 사용자가 현재 방을 전환하면 Effect가 다시 촉발되기를 원합니다. 하지만 `createConnection`은 함수이기 때문에 이 함수가 읽는 정보가 실제로 변경되었는지 여부를 확인할 수 없습니다. 이 문제를 해결하려면 `App` 컴포넌트에서 `createConnection`을 전달하는 대신 원시값인 `roomId` 및 `isEncrypted`를 전달하세요:\n\n```js {2-3}\n      <ChatRoom\n        roomId={roomId}\n        isEncrypted={isEncrypted}\n        onMessage={msg => {\n          showNotification('New message: ' + msg, isDark ? 'dark' : 'light');\n        }}\n      />\n```\n\n이제 `App`에서 전달하지 않고 Effect 내부로 `createConnection` 함수를 옮길 수 있습니다.\n\n```js {1-4,6,10-20}\nimport {\n  createEncryptedConnection,\n  createUnencryptedConnection,\n} from './chat.js';\n\nexport default function ChatRoom({ roomId, isEncrypted, onMessage }) {\n  const onReceiveMessage = useEffectEvent(onMessage);\n\n  useEffect(() => {\n    function createConnection() {\n      const options = {\n        serverUrl: 'https://localhost:1234',\n        roomId: roomId\n      };\n      if (isEncrypted) {\n        return createEncryptedConnection(options);\n      } else {\n        return createUnencryptedConnection(options);\n      }\n    }\n    // ...\n```\n\n이 두 가지 변경 사항 이후에는 Effect가 더 이상 함수 값에 의존하지 않습니다.\n\n```js {1,8,10,21}\nexport default function ChatRoom({ roomId, isEncrypted, onMessage }) { // 반응형 값\n  const onReceiveMessage = useEffectEvent(onMessage); // 비반응형\n\n  useEffect(() => {\n    function createConnection() {\n      const options = {\n        serverUrl: 'https://localhost:1234',\n        roomId: roomId // 반응형 값 읽기\n      };\n      if (isEncrypted) { // 반응형 값 읽기\n        return createEncryptedConnection(options);\n      } else {\n        return createUnencryptedConnection(options);\n      }\n    }\n\n    const connection = createConnection();\n    connection.on('message', (msg) => onReceiveMessage(msg));\n    connection.connect();\n    return () => connection.disconnect();\n  }, [roomId, isEncrypted]); // ✅ 모든 의존성 선언됨\n```\n\n그 결과, 의미 있는 정보(`roomId` 또는 `isEncrypted`)가 변경될 때만 채팅이 다시 연결됩니다.\n\n<Sandpack>\n\n```json package.json hidden\n{\n  \"dependencies\": {\n    \"react\": \"latest\",\n    \"react-dom\": \"latest\",\n    \"react-scripts\": \"latest\",\n    \"toastify-js\": \"1.12.0\"\n  },\n  \"scripts\": {\n    \"start\": \"react-scripts start\",\n    \"build\": \"react-scripts build\",\n    \"test\": \"react-scripts test --env=jsdom\",\n    \"eject\": \"react-scripts eject\"\n  }\n}\n```\n\n```js src/App.js\nimport { useState } from 'react';\nimport ChatRoom from './ChatRoom.js';\n\nimport { showNotification } from './notifications.js';\n\nexport default function App() {\n  const [isDark, setIsDark] = useState(false);\n  const [roomId, setRoomId] = useState('general');\n  const [isEncrypted, setIsEncrypted] = useState(false);\n\n  return (\n    <>\n      <label>\n        <input\n          type=\"checkbox\"\n          checked={isDark}\n          onChange={e => setIsDark(e.target.checked)}\n        />\n        Use dark theme\n      </label>\n      <label>\n        <input\n          type=\"checkbox\"\n          checked={isEncrypted}\n          onChange={e => setIsEncrypted(e.target.checked)}\n        />\n        Enable encryption\n      </label>\n      <label>\n        Choose the chat room:{' '}\n        <select\n          value={roomId}\n          onChange={e => setRoomId(e.target.value)}\n        >\n          <option value=\"general\">general</option>\n          <option value=\"travel\">travel</option>\n          <option value=\"music\">music</option>\n        </select>\n      </label>\n      <hr />\n      <ChatRoom\n        roomId={roomId}\n        isEncrypted={isEncrypted}\n        onMessage={msg => {\n          showNotification('New message: ' + msg, isDark ? 'dark' : 'light');\n        }}\n      />\n    </>\n  );\n}\n```\n\n```js src/ChatRoom.js active\nimport { useState, useEffect } from 'react';\nimport { useEffectEvent } from 'react';\nimport {\n  createEncryptedConnection,\n  createUnencryptedConnection,\n} from './chat.js';\n\nexport default function ChatRoom({ roomId, isEncrypted, onMessage }) {\n  const onReceiveMessage = useEffectEvent(onMessage);\n\n  useEffect(() => {\n    function createConnection() {\n      const options = {\n        serverUrl: 'https://localhost:1234',\n        roomId: roomId\n      };\n      if (isEncrypted) {\n        return createEncryptedConnection(options);\n      } else {\n        return createUnencryptedConnection(options);\n      }\n    }\n\n    const connection = createConnection();\n    connection.on('message', (msg) => onReceiveMessage(msg));\n    connection.connect();\n    return () => connection.disconnect();\n  }, [roomId, isEncrypted]);\n\n  return <h1>Welcome to the {roomId} room!</h1>;\n}\n```\n\n```js src/chat.js\nexport function createEncryptedConnection({ serverUrl, roomId }) {\n  // 실제 구현에서는 서버에 연결됩니다\n  if (typeof serverUrl !== 'string') {\n    throw Error('Expected serverUrl to be a string. Received: ' + serverUrl);\n  }\n  if (typeof roomId !== 'string') {\n    throw Error('Expected roomId to be a string. Received: ' + roomId);\n  }\n  let intervalId;\n  let messageCallback;\n  return {\n    connect() {\n      console.log('✅ 🔐 Connecting to \"' + roomId + '\" room... (encrypted)');\n      clearInterval(intervalId);\n      intervalId = setInterval(() => {\n        if (messageCallback) {\n          if (Math.random() > 0.5) {\n            messageCallback('hey')\n          } else {\n            messageCallback('lol');\n          }\n        }\n      }, 3000);\n    },\n    disconnect() {\n      clearInterval(intervalId);\n      messageCallback = null;\n      console.log('❌ 🔐 Disconnected from \"' + roomId + '\" room (encrypted)');\n    },\n    on(event, callback) {\n      if (messageCallback) {\n        throw Error('Cannot add the handler twice.');\n      }\n      if (event !== 'message') {\n        throw Error('Only \"message\" event is supported.');\n      }\n      messageCallback = callback;\n    },\n  };\n}\n\nexport function createUnencryptedConnection({ serverUrl, roomId }) {\n  // 실제 구현에서는 서버에 연결됩니다\n  if (typeof serverUrl !== 'string') {\n    throw Error('Expected serverUrl to be a string. Received: ' + serverUrl);\n  }\n  if (typeof roomId !== 'string') {\n    throw Error('Expected roomId to be a string. Received: ' + roomId);\n  }\n  let intervalId;\n  let messageCallback;\n  return {\n    connect() {\n      console.log('✅ Connecting to \"' + roomId + '\" room (unencrypted)...');\n      clearInterval(intervalId);\n      intervalId = setInterval(() => {\n        if (messageCallback) {\n          if (Math.random() > 0.5) {\n            messageCallback('hey')\n          } else {\n            messageCallback('lol');\n          }\n        }\n      }, 3000);\n    },\n    disconnect() {\n      clearInterval(intervalId);\n      messageCallback = null;\n      console.log('❌ Disconnected from \"' + roomId + '\" room (unencrypted)');\n    },\n    on(event, callback) {\n      if (messageCallback) {\n        throw Error('Cannot add the handler twice.');\n      }\n      if (event !== 'message') {\n        throw Error('Only \"message\" event is supported.');\n      }\n      messageCallback = callback;\n    },\n  };\n}\n```\n\n```js src/notifications.js\nimport Toastify from 'toastify-js';\nimport 'toastify-js/src/toastify.css';\n\nexport function showNotification(message, theme) {\n  Toastify({\n    text: message,\n    duration: 2000,\n    gravity: 'top',\n    position: 'right',\n    style: {\n      background: theme === 'dark' ? 'black' : 'white',\n      color: theme === 'dark' ? 'white' : 'black',\n    },\n  }).showToast();\n}\n```\n\n```css\nlabel, button { display: block; margin-bottom: 5px; }\n```\n\n</Sandpack>\n\n</Solution>\n\n</Challenges>\n"
  },
  {
    "path": "src/content/learn/render-and-commit.md",
    "content": "---\ntitle: 렌더링 그리고 커밋\n---\n\n<Intro>\n\n컴포넌트를 화면에 표시하기 이전에 React에서 렌더링을 해야 합니다. 해당 과정의 단계를 이해하면 코드가 어떻게 실행되는지 이해할 수 있고 React 렌더링 동작에 관해 설명하는데 도움이 됩니다.\n\n</Intro>\n\n<YouWillLearn>\n\n* React에서 렌더링의 의미\n* React가 컴포넌트를 언제, 왜 렌더링 하는지\n* 화면에 컴포넌트를 표시하는 단계\n* 렌더링이 항상 DOM 업데이트를 하지 않는 이유\n\n</YouWillLearn>\n\n주방에서 요리사가 컴포넌트를 재료로 맛있는 요리를 한다고 상상해보세요. 이 시나리오에서 React는 고객들의 요청을 받고 주문을 가져오는 웨이터입니다. 이 과정에는 UI를 요청하고 제공하는 세 가지 단계가 있습니다.\n\n1. 렌더링 **트리거** (손님의 주문을 주방으로 전달)\n2. 컴포넌트 **렌더링** (주방에서 주문 준비하기)\n3. DOM에 **커밋** (테이블에 주문한 요리 내놓기)\n\n<IllustrationBlock sequential>\n  <Illustration caption=\"Trigger\" alt=\"React as a server in a restaurant, fetching orders from the users and delivering them to the Component Kitchen.\" src=\"/images/docs/illustrations/i_render-and-commit1.png\" />\n  <Illustration caption=\"Render\" alt=\"The Card Chef gives React a fresh Card component.\" src=\"/images/docs/illustrations/i_render-and-commit2.png\" />\n  <Illustration caption=\"Commit\" alt=\"React delivers the Card to the user at their table.\" src=\"/images/docs/illustrations/i_render-and-commit3.png\" />\n</IllustrationBlock>\n\n## 1단계: 렌더링 트리거 {/*step-1-trigger-a-render*/}\n\n컴포넌트 렌더링이 일어나는 데에는 두 가지 이유가 있습니다.\n\n1. 컴포넌트의 **초기 렌더링인 경우**\n2. 컴포넌트의 **state가 업데이트된 경우**\n\n### 초기 렌더링 {/*initial-render*/}\n\n앱을 시작할 때 초기 렌더링을 트리거해야 합니다. 프레임워크와 샌드박스는 때때로 이 코드를 숨기곤 하지만, 대상 DOM 노드와 함께 [`createRoot`](/reference/react-dom/client/createRoot)를 호출한 다음 해당 컴포넌트로 `render` 메서드를 호출하면 이 작업이 완료됩니다.\n\n<Sandpack>\n\n```js src/index.js active\nimport Image from './Image.js';\nimport { createRoot } from 'react-dom/client';\n\nconst root = createRoot(document.getElementById('root'))\nroot.render(<Image />);\n```\n\n```js src/Image.js\nexport default function Image() {\n  return (\n    <img\n      src=\"https://i.imgur.com/ZF6s192.jpg\"\n      alt=\"'Floralis Genérica' by Eduardo Catalano: a gigantic metallic flower sculpture with reflective petals\"\n    />\n  );\n}\n```\n\n</Sandpack>\n\n`root.render()` 호출을 주석 처리하고 컴포넌트가 사라지는 것을 확인하세요!\n\n### State 업데이트 시 리렌더링 {/*re-renders-when-state-updates*/}\n\n컴포넌트가 처음으로 렌더링 된 후에는 [`set` 함수](/reference/react/useState#setstate)를 통해 상태를 업데이트하여 추가적인 렌더링을 트리거할 수 있습니다. 컴포넌트의 상태를 업데이트하면 자동으로 렌더링 대기열에 추가됩니다. (이것은 레스토랑의 손님이 첫 주문 이후에 갈증이나 배고픔의 상태에 따라 차, 디저트 등의 메뉴를 주문하는 것으로 상상해 볼 수 있습니다.)\n\n<IllustrationBlock sequential>\n  <Illustration caption=\"State update...\" alt=\"React as a server in a restaurant, serving a Card UI to the user, represented as a patron with a cursor for their head. The patron expresses they want a pink card, not a black one!\" src=\"/images/docs/illustrations/i_rerender1.png\" />\n  <Illustration caption=\"...triggers...\" alt=\"React returns to the Component Kitchen and tells the Card Chef they need a pink Card.\" src=\"/images/docs/illustrations/i_rerender2.png\" />\n  <Illustration caption=\"...render!\" alt=\"The Card Chef gives React the pink Card.\" src=\"/images/docs/illustrations/i_rerender3.png\" />\n</IllustrationBlock>\n\n## 2단계: React 컴포넌트 렌더링 {/*step-2-react-renders-your-components*/}\n\n렌더링을 트리거한 후 React는 컴포넌트를 호출하여 화면에 표시할 내용을 파악합니다. **\"렌더링\"은 React에서 컴포넌트를 호출하는 것입니다.**\n\n* **초기 렌더링에서** React는 루트 컴포넌트를 호출합니다.\n* **이후 렌더링에서** React는 state 업데이트가 일어나 렌더링을 트리거한 컴포넌트를 호출합니다.\n\n재귀적 단계: 업데이트된 컴포넌트가 다른 컴포넌트를 반환하면 React는 다음으로 _해당_  컴포넌트를 렌더링하고 해당 컴포넌트도 컴포넌트를 반환하면 _반환된_  컴포넌트를 다음에 렌더링하는 방식입니다. 중첩된 컴포넌트가 더 이상 없고 React가 화면에 표시되어야 하는 내용을 정확히 알 때까지 이 단계는 계속됩니다.\n\n다음 예시에서 React는 `Gallery()`와 `Image()`를 여러 번 호출합니다.\n\n<Sandpack>\n\n```js src/Gallery.js active\nexport default function Gallery() {\n  return (\n    <section>\n      <h1>Inspiring Sculptures</h1>\n      <Image />\n      <Image />\n      <Image />\n    </section>\n  );\n}\n\nfunction Image() {\n  return (\n    <img\n      src=\"https://i.imgur.com/ZF6s192.jpg\"\n      alt=\"'Floralis Genérica' by Eduardo Catalano: a gigantic metallic flower sculpture with reflective petals\"\n    />\n  );\n}\n```\n\n```js src/index.js\nimport Gallery from './Gallery.js';\nimport { createRoot } from 'react-dom/client';\n\nconst root = createRoot(document.getElementById('root'))\nroot.render(<Gallery />);\n```\n\n```css\nimg { margin: 0 10px 10px 0; }\n```\n\n</Sandpack>\n\n* **초기 렌더링 하는 동안** React는 `<section>`, `<h1>` 그리고 3개의 `<img>` 태그에 대한 [DOM 노드를 생성](https://developer.mozilla.org/docs/Web/API/Document/createElement)합니다.\n* **리렌더링하는 동안** React는 이전 렌더링 이후 변경된 속성을 계산합니다. 다음 단계인 커밋 단계까지는 해당 정보로 아무런 작업도 수행하지 않습니다.\n\n<Pitfall>\n\n렌더링은 항상 [순수한 계산](/learn/keeping-components-pure):\n\n* **동일한 입력에는 동일한 출력을 해야합니다.** 동일한 입력이 주어지면 컴포넌트는 항상 동일한 JSX를 반환해야 합니다. (누군가 토마토 샐러드를 주문하면 그들은 양파가 있는 샐러드를 받으면 안 됩니다!)\n* **이전의 state를 변경해서는 안됩니다.** 렌더링 전에 존재했던 객체나 변수를 변경해서는 안 됩니다. (누군가의 주문이 다른 사람의 주문을 변경해서는 안 됩니다.)\n\n그렇지 않으면 코드베이스가 복잡해짐에 따라 혼란스러운 버그와 예측할 수 없는 동작이 발생할 수 있습니다. \"Strict Mode\"에서 개발할 때 React는 각 컴포넌트의 함수를 두 번 호출하여 순수하지 않은 함수로 인한 실수를 표면화하는데 도움을 받을 수 있습니다.\n\n</Pitfall>\n\n<DeepDive>\n\n#### 성능 최적화 {/*optimizing-performance*/}\n\n업데이트된 컴포넌트 내에 중첩된 모든 컴포넌트를 렌더링하는 기본 동작은 업데이트된 컴포넌트가 트리에서 매우 높은 곳에 있는 경우 성능 최적화되지 않습니다. 성능 문제가 발생하는 경우 [성능](https://ko.legacy.reactjs.org/docs/optimizing-performance.html) 섹션에 설명된 몇 가지 옵트인 방식으로 문제를 해결 할 수 있습니다. **성급하게 최적화하지 마세요!**\n\n</DeepDive>\n\n## 3단계: React가 DOM에 변경사항을 커밋 {/*step-3-react-commits-changes-to-the-dom*/}\n\n컴포넌트를 렌더링(호출)한 후 React는 DOM을 수정합니다.\n\n* **초기 렌더링의 경우** React는 [`appendChild()`](https://developer.mozilla.org/docs/Web/API/Node/appendChild) DOM API를 사용하여 생성한 모든 DOM 노드를 화면에 표시합니다.\n* **리렌더링의 경우** React는 필요한 최소한의 작업(렌더링하는 동안 계산된 것!)을 적용하여 DOM이 최신 렌더링 출력과 일치하도록 합니다.\n\n**React는 렌더링 간에 차이가 있는 경우에만 DOM 노드를 변경합니다.** 예를 들어 매초 부모로부터 전달된 다른 props로 다시 렌더링하는 컴포넌트가 있습니다. `<input>`에 텍스트를 입력하여 `value`를 업데이트 하지만 컴포넌트가 리렌더링될 때 텍스트가 사라지지 않습니다.\n\n<Sandpack>\n\n```js src/Clock.js active\nexport default function Clock({ time }) {\n  return (\n    <>\n      <h1>{time}</h1>\n      <input />\n    </>\n  );\n}\n```\n\n```js src/App.js hidden\nimport { useState, useEffect } from 'react';\nimport Clock from './Clock.js';\n\nfunction useTime() {\n  const [time, setTime] = useState(() => new Date());\n  useEffect(() => {\n    const id = setInterval(() => {\n      setTime(new Date());\n    }, 1000);\n    return () => clearInterval(id);\n  }, []);\n  return time;\n}\n\nexport default function App() {\n  const time = useTime();\n  return (\n    <Clock time={time.toLocaleTimeString()} />\n  );\n}\n```\n\n</Sandpack>\n\n마지막 단계에서 React가 `<h1>`의 내용만 새로운 `time`으로 업데이트하기 때문입니다. `<input>`이 JSX에서 이전과 같은 위치로 확인되므로 React는 `<input>` 또는 `value`를 건드리지 않습니다!\n## 에필로그: 브라우저 페인트 {/*epilogue-browser-paint*/}\n\n렌더링이 완료되고 React가 DOM을 업데이트한 후 브라우저는 화면을 다시 그립니다. 이 단계를 \"브라우저 렌더링\"이라고 하지만 이 문서의 나머지 부분에서 혼동을 피하고자 \"페인팅\"이라고 부를 것입니다.\n\n<Illustration alt=\"A browser painting 'still life with card element'.\" src=\"/images/docs/illustrations/i_browser-paint.png\" />\n\n<Recap>\n\n* React 앱의 모든 화면 업데이트는 세 단계로 이루어집니다.\n  1. 트리거\n  2. 렌더링\n  3. 커밋\n* Strict Mode를 사용하여 컴포넌트에서 실수를 찾을 수 있습니다.\n* 렌더링 결과가 이전과 같으면 React는 DOM을 건드리지 않습니다.\n\n</Recap>\n"
  },
  {
    "path": "src/content/learn/rendering-lists.md",
    "content": "---\ntitle: 리스트 렌더링\n---\n\n<Intro>\n\n데이터 모음에서 유사한 컴포넌트를 여러 개 표시하고 싶을 때가 종종 있습니다. [JavaScript 배열 메서드](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array#)를 사용하여 데이터 배열을 조작할 수 있습니다. 이 페이지에서는 React에서 [`filter()`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/filter)와 [`map()`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/map)을 사용해 데이터 배열을 필터링하고 컴포넌트 배열로 변환해보겠습니다.\n\n</Intro>\n\n<YouWillLearn>\n\n* JavaScript의 `map()`을 사용하여 배열을 컴포넌트로 렌더링하는 방법\n* JavaScript의 `filter()`를 사용하여 특정 컴포넌트만 렌더링하는 방법\n* React에서 Key가 필요한 때와 이유\n\n</YouWillLearn>\n\n## 배열을 데이터로 렌더링하기 {/*rendering-data-from-arrays*/}\n\n내용이 있는 리스트가 있다고 가정해 봅시다.\n\n```js\n<ul>\n  <li>Creola Katherine Johnson: mathematician</li>\n  <li>Mario José Molina-Pasquel Henríquez: chemist</li>\n  <li>Mohammad Abdus Salam: physicist</li>\n  <li>Percy Lavon Julian: chemist</li>\n  <li>Subrahmanyan Chandrasekhar: astrophysicist</li>\n</ul>\n```\n\n이러한 리스트 항목의 유일한 차이점은 콘텐츠, 즉 데이터입니다. 댓글 목록에서 프로필 이미지 갤러리에 이르기까지 인터페이스를 구축할 때 서로 다른 데이터를 사용하여 동일한 컴포넌트의 여러 인스턴스를 표시해야 하는 경우가 종종 있습니다. 이러한 상황에서 해당 데이터를 JavaScript 객체와 배열에 저장하고 [`map()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map)과 [`filter()`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/filter) 같은 메서드를 사용하여 해당 객체에서 컴포넌트 리스트를 렌더링할 수 있습니다.\n\n다음은 배열에서 항목 리스트를 생성하는 방법에 대한 간단한 예시입니다.\n\n1. 데이터를 배열로 **이동**합니다.\n\n```js\nconst people = [\n  'Creola Katherine Johnson: mathematician',\n  'Mario José Molina-Pasquel Henríquez: chemist',\n  'Mohammad Abdus Salam: physicist',\n  'Percy Lavon Julian: chemist',\n  'Subrahmanyan Chandrasekhar: astrophysicist'\n];\n```\n\n2. `people`의 요소를 새로운 JSX 노드 배열인 `listItems`에 **매핑**합니다.\n\n```js\nconst listItems = people.map(person => <li>{person}</li>);\n```\n\n3. `<ul>`로 래핑된 컴포넌트의 `listItems`를 **반환**합니다.\n\n```js\nreturn <ul>{listItems}</ul>;\n```\n\n결과는 다음과 같습니다.\n\n<Sandpack>\n\n```js\nconst people = [\n  'Creola Katherine Johnson: mathematician',\n  'Mario José Molina-Pasquel Henríquez: chemist',\n  'Mohammad Abdus Salam: physicist',\n  'Percy Lavon Julian: chemist',\n  'Subrahmanyan Chandrasekhar: astrophysicist'\n];\n\nexport default function List() {\n  const listItems = people.map(person =>\n    <li>{person}</li>\n  );\n  return <ul>{listItems}</ul>;\n}\n```\n\n```css\nli { margin-bottom: 10px; }\n```\n\n</Sandpack>\n\n위의 샌드박스에 콘솔 에러가 표시된다는 점에 주의하세요.\n\n<ConsoleBlock level=\"error\">\n\nWarning: Each child in a list should have a unique \"key\" prop.\n\n</ConsoleBlock>\n\n이 에러를 수정하는 방법은 이 페이지의 뒷부분에서 알아보겠습니다. 그 전에 데이터에 몇 가지 구조를 추가해 보겠습니다.\n\n## 배열의 항목들을 필터링하기 {/*filtering-arrays-of-items*/}\n\n이 데이터는 훨씬 더 구조화될 수 있습니다.\n\n```js\nconst people = [{\n  id: 0,\n  name: 'Creola Katherine Johnson',\n  profession: 'mathematician',\n}, {\n  id: 1,\n  name: 'Mario José Molina-Pasquel Henríquez',\n  profession: 'chemist',\n}, {\n  id: 2,\n  name: 'Mohammad Abdus Salam',\n  profession: 'physicist',\n}, {\n  id: 3,\n  name: 'Percy Lavon Julian',\n  profession: 'chemist',\n}, {\n  id: 4,\n  name: 'Subrahmanyan Chandrasekhar',\n  profession: 'astrophysicist',\n}];\n```\n\n직업이 `'chemist'`인 사람들만 표시하고 싶다고 가정해 봅시다. JavaScript의 `filter()` 메서드를 사용하여 해당하는 사람만 반환할 수 있습니다. 이 메서드는 항목 배열을 받아 \"test\"(`true` 혹은 `false`를 반환하는 함수)를 통과한 후 테스트에 통과된 항목(`true`가 반환된 항목)만 있는 새로운 배열을 반환합니다.\n\n`직업`이 `'chemist'`인 항목만 필요합니다. 이를 위한 \"test\" 함수는 `(person) => person.profession === 'chemist'`와 같습니다. 이를 적용하는 과정은 다음과 같습니다.\n\n1. `people`에서 `filter()`를 호출해 `person.profession === 'chemist'`로 필터링해서 \"chemist\"로만 구성된 새로운 배열 `chemists`를 **생성**합니다.\n\n```js\nconst chemists = people.filter(person =>\n  person.profession === 'chemist'\n);\n```\n\n2. 이제 `chemists`를 **매핑**합니다.\n\n```js {1,13}\nconst listItems = chemists.map(person =>\n  <li>\n     <img\n       src={getImageUrl(person)}\n       alt={person.name}\n     />\n     <p>\n       <b>{person.name}:</b>\n       {' ' + person.profession + ' '}\n       known for {person.accomplishment}\n     </p>\n  </li>\n);\n```\n\n3. 마지막으로 컴포넌트에서 `listItems`를 **반환**합니다.\n\n```js\nreturn <ul>{listItems}</ul>;\n```\n\n<Sandpack>\n\n```js src/App.js\nimport { people } from './data.js';\nimport { getImageUrl } from './utils.js';\n\nexport default function List() {\n  const chemists = people.filter(person =>\n    person.profession === 'chemist'\n  );\n  const listItems = chemists.map(person =>\n    <li>\n      <img\n        src={getImageUrl(person)}\n        alt={person.name}\n      />\n      <p>\n        <b>{person.name}:</b>\n        {' ' + person.profession + ' '}\n        known for {person.accomplishment}\n      </p>\n    </li>\n  );\n  return <ul>{listItems}</ul>;\n}\n```\n\n```js src/data.js\nexport const people = [{\n  id: 0,\n  name: 'Creola Katherine Johnson',\n  profession: 'mathematician',\n  accomplishment: 'spaceflight calculations',\n  imageId: 'MK3eW3A'\n}, {\n  id: 1,\n  name: 'Mario José Molina-Pasquel Henríquez',\n  profession: 'chemist',\n  accomplishment: 'discovery of Arctic ozone hole',\n  imageId: 'mynHUSa'\n}, {\n  id: 2,\n  name: 'Mohammad Abdus Salam',\n  profession: 'physicist',\n  accomplishment: 'electromagnetism theory',\n  imageId: 'bE7W1ji'\n}, {\n  id: 3,\n  name: 'Percy Lavon Julian',\n  profession: 'chemist',\n  accomplishment: 'pioneering cortisone drugs, steroids and birth control pills',\n  imageId: 'IOjWm71'\n}, {\n  id: 4,\n  name: 'Subrahmanyan Chandrasekhar',\n  profession: 'astrophysicist',\n  accomplishment: 'white dwarf star mass calculations',\n  imageId: 'lrWQx8l'\n}];\n```\n\n```js src/utils.js\nexport function getImageUrl(person) {\n  return (\n    'https://i.imgur.com/' +\n    person.imageId +\n    's.jpg'\n  );\n}\n```\n\n```css\nul { list-style-type: none; padding: 0px 10px; }\nli {\n  margin-bottom: 10px;\n  display: grid;\n  grid-template-columns: auto 1fr;\n  gap: 20px;\n  align-items: center;\n}\nimg { width: 100px; height: 100px; border-radius: 50%; }\n```\n\n</Sandpack>\n\n<Pitfall>\n\n화살표 함수는 암시적으로 `=>` 바로 뒤에 식을 반환하기 때문에 `return` 문이 필요하지 않습니다.\n\n```js\nconst listItems = chemists.map(person =>\n  <li>...</li> // 암시적 반환!\n);\n```\n\n하지만 **`=>` 뒤에 `{` 중괄호가 오는 경우 `return`을 명시적으로 작성해야 합니다!**\n\n```js\nconst listItems = chemists.map(person => { // 중괄호\n  return <li>...</li>;\n});\n```\n\n`=> {` 를 표현하는 화살표 함수를 [\"block body\"](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions#function_body)를 가지고 있다고 말합니다. 이 함수를 사용하면 한 줄 이상의 코드를 작성할 수 있지만 `return` 문을 *반드시* 작성해야 합니다. 그렇지 않으면 아무것도 반환되지 않습니다!\n\n</Pitfall>\n\n## `key`를 사용해서 리스트 항목을 순서대로 유지하기 {/*keeping-list-items-in-order-with-key*/}\n\n위의 모든 샌드박스의 콘솔에 에러가 표시되는 것을 확인할 수 있습니다.\n\n<ConsoleBlock level=\"error\">\n\nWarning: Each child in a list should have a unique \"key\" prop.\n\n</ConsoleBlock>\n\n각 배열 항목에 다른 항목 중에서 고유하게 식별할 수 있는 문자열 또는 숫자를 `key`로 지정해야 합니다.\n\n```js\n<li key={person.id}>...</li>\n```\n\n<Note>\n\n`map()` 호출 내부의 JSX 엘리먼트에는 항상 key가 필요합니다!\n\n</Note>\n\nKey는 각 컴포넌트가 어떤 배열 항목에 해당하는지 React에 알려주어 나중에 일치시킬 수 있도록 합니다. 이는 배열 항목이 정렬 등으로 인해 이동하거나 삽입되거나 삭제될 수 있는 경우 중요해집니다. `key`를 잘 선택하면 React가 정확히 무슨 일이 일어났는지 추론하고 DOM 트리에 올바르게 업데이트 하는데 도움이 됩니다.\n\n즉석에서 key를 생성하는 대신 데이터 안에 key를 포함해야 합니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { people } from './data.js';\nimport { getImageUrl } from './utils.js';\n\nexport default function List() {\n  const listItems = people.map(person =>\n    <li key={person.id}>\n      <img\n        src={getImageUrl(person)}\n        alt={person.name}\n      />\n      <p>\n        <b>{person.name}</b>\n          {' ' + person.profession + ' '}\n          known for {person.accomplishment}\n      </p>\n    </li>\n  );\n  return <ul>{listItems}</ul>;\n}\n```\n\n```js src/data.js active\nexport const people = [{\n  id: 0, // JSX에서 key로 사용됩니다.\n  name: 'Creola Katherine Johnson',\n  profession: 'mathematician',\n  accomplishment: 'spaceflight calculations',\n  imageId: 'MK3eW3A'\n}, {\n  id: 1, // JSX에서 key로 사용됩니다.\n  name: 'Mario José Molina-Pasquel Henríquez',\n  profession: 'chemist',\n  accomplishment: 'discovery of Arctic ozone hole',\n  imageId: 'mynHUSa'\n}, {\n  id: 2, // JSX에서 key로 사용됩니다.\n  name: 'Mohammad Abdus Salam',\n  profession: 'physicist',\n  accomplishment: 'electromagnetism theory',\n  imageId: 'bE7W1ji'\n}, {\n  id: 3, // JSX에서 key로 사용됩니다.\n  name: 'Percy Lavon Julian',\n  profession: 'chemist',\n  accomplishment: 'pioneering cortisone drugs, steroids and birth control pills',\n  imageId: 'IOjWm71'\n}, {\n  id: 4, // JSX에서 key로 사용됩니다.\n  name: 'Subrahmanyan Chandrasekhar',\n  profession: 'astrophysicist',\n  accomplishment: 'white dwarf star mass calculations',\n  imageId: 'lrWQx8l'\n}];\n```\n\n```js src/utils.js\nexport function getImageUrl(person) {\n  return (\n    'https://i.imgur.com/' +\n    person.imageId +\n    's.jpg'\n  );\n}\n```\n\n```css\nul { list-style-type: none; padding: 0px 10px; }\nli {\n  margin-bottom: 10px;\n  display: grid;\n  grid-template-columns: auto 1fr;\n  gap: 20px;\n  align-items: center;\n}\nimg { width: 100px; height: 100px; border-radius: 50%; }\n```\n\n</Sandpack>\n\n<DeepDive>\n\n#### 각 리스트 항목에 대해 여러 DOM 노드 표시하기 {/*displaying-several-dom-nodes-for-each-list-item*/}\n\n각 항목이 하나가 아닌 여러 개의 DOM 노드를 렌더링해야 하는 경우에는 어떻게 해야 할까요?\n\n짧은 `<> </>` fragment 구문으로는 key를 전달할 수 없으므로 key를 단일 `<div>`로 그룹화하거나 약간 더 길고 명시적인 `<Fragment>` 문법을 사용해야 합니다.\n\n```js\nimport { Fragment } from 'react';\n\n// ...\n\nconst listItems = people.map(person =>\n  <Fragment key={person.id}>\n    <h1>{person.name}</h1>\n    <p>{person.bio}</p>\n  </Fragment>\n);\n```\n\nFragment는 DOM에서 사라지므로 `<h1>`, `<p>`, `<h1>`, `<p>` 등의 평평한 리스트가 생성됩니다.\n\n</DeepDive>\n\n### `key`를 가져오는 곳 {/*where-to-get-your-key*/}\n\n데이터 소스마다 다른 key 소스를 제공합니다\n\n* **데이터베이스의 데이터:** 데이터베이스에서 데이터를 가져오는 경우 본질적으로 고유한 데이터베이스 key/ID를 사용할 수 있습니다.\n* **로컬에서 생성된 데이터:** 데이터가 로컬에서 생성되고 유지되는 경우(예: 메모 작성 앱의 노트), 항목을 만들 때 증분 일련번호나 [`crypto.randomUUID()`](https://developer.mozilla.org/en-US/docs/Web/API/Crypto/randomUUID) 또는 [`uuid`](https://www.npmjs.com/package/uuid) 같은 패키지를 사용하세요.\n\n### key 규칙 {/*rules-of-keys*/}\n\n* **key는 형제 간에 고유해야 합니다.** 하지만 같은 key를 _다른_ 배열의 JSX 노드에 동일한 key로 사용해도 괜찮습니다.\n* **key는 변경되어서는 안 되며** 그렇게 되면 key는 목적에 어긋납니다! 렌더링 중에는 key를 생성하지 마세요.\n\n### React에 key가 필요한 이유는 무엇인가요? {/*why-does-react-need-keys*/}\n\n데스크탑의 파일에 이름이 없다고 상상해 보세요. 대신 첫 번째 파일, 두 번째 파일 등 순서대로 파일을 참조할 것입니다. 익숙해질 수도 있지만, 파일을 삭제한다면 혼란스러워질 수도 있습니다. 두 번째 파일이 첫 번째 파일이 되고 세 번째 파일이 두 번째 파일이 되는 식으로 말이죠.\n\n폴더의 파일 이름과 배열의 JSX key는 비슷한 용도로 사용됩니다. 이를 통해 형제 항목 간에 항목을 고유하게 식별할 수 있습니다. 잘 선택된 key는 배열 내 위치보다 더 많은 정보를 제공합니다. 재정렬로 인해 *위치*가 변경되더라도 `key`는 React가 생명주기 내내 해당 항목을 식별할 수 있게 해줍니다.\n\n<Pitfall>\n\n배열에서 항목의 인덱스를 key로 사용하고 싶을 수도 있습니다. 실제로 `key`를 전혀 지정하지 않으면 React는 인덱스를 사용합니다. 하지만 항목이 삽입되거나 삭제하거나 배열의 순서가 바뀌면 시간이 지남에 따라 항목을 렌더링하는 순서가 변경됩니다. 인덱스를 key로 사용하면 종종 미묘하고 혼란스러운 버그가 발생합니다.\n\n마찬가지로 `key={Math.random()}`처럼 즉석에서 key를 생성하지 마세요. 이렇게 하면 렌더링 간에 key가 일치하지 않아 모든 컴포넌트와 DOM이 매번 다시 생성될 수 있습니다. 속도가 느려질 뿐만 아니라 리스트 항목 내부의 모든 사용자의 입력도 손실됩니다. 대신 데이터 기반의 안정적인 ID를 사용하세요.\n\n컴포넌트가 `key`를 prop으로 받지 않는다는 점에 유의하세요. key는 React 자체에서 힌트로만 사용됩니다. 컴포넌트에 ID가 필요하다면 `<Profile key={id} userId={id} />`와 같이 별도의 prop으로 전달해야 합니다.\n\n</Pitfall>\n\n<Recap>\n\n이 페이지에서 학습한 내용\n\n* 컴포넌트에서 배열이나 객체와 같은 데이터 구조로 데이터를 이동하는 방법\n* JavaScript의 `map()`을 사용하여 유사한 컴포넌트 집합을 생성하는 방법\n* JavaScript의 `filter()`를 사용하여 필터링된 항목의 배열을 생성하는 방법\n* 컬렉션에서 각 컴포넌트에 `key`를 설정하여 위치나 데이터가 변경되더라도 React가 각 컴포넌트를 추적할 수 있도록 하는 이유와 방법\n\n</Recap>\n\n\n\n<Challenges>\n\n#### 리스트를 둘로 나누기 {/*splitting-a-list-in-two*/}\n\n예시는 모든 사람의 리스트를 보여줍니다.\n\n두 개의 개별 리스트 **Chemists**와 **Everyone Else**을 차례로 표시하도록 변경하세요. 이전과 마찬가지로 `person.profession === 'chemist'`를 확인하여 어떤 사람이 chemist인지 확인할 수 있습니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { people } from './data.js';\nimport { getImageUrl } from './utils.js';\n\nexport default function List() {\n  const listItems = people.map(person =>\n    <li key={person.id}>\n      <img\n        src={getImageUrl(person)}\n        alt={person.name}\n      />\n      <p>\n        <b>{person.name}:</b>\n        {' ' + person.profession + ' '}\n        known for {person.accomplishment}\n      </p>\n    </li>\n  );\n  return (\n    <article>\n      <h1>Scientists</h1>\n      <ul>{listItems}</ul>\n    </article>\n  );\n}\n```\n\n```js src/data.js\nexport const people = [{\n  id: 0,\n  name: 'Creola Katherine Johnson',\n  profession: 'mathematician',\n  accomplishment: 'spaceflight calculations',\n  imageId: 'MK3eW3A'\n}, {\n  id: 1,\n  name: 'Mario José Molina-Pasquel Henríquez',\n  profession: 'chemist',\n  accomplishment: 'discovery of Arctic ozone hole',\n  imageId: 'mynHUSa'\n}, {\n  id: 2,\n  name: 'Mohammad Abdus Salam',\n  profession: 'physicist',\n  accomplishment: 'electromagnetism theory',\n  imageId: 'bE7W1ji'\n}, {\n  id: 3,\n  name: 'Percy Lavon Julian',\n  profession: 'chemist',\n  accomplishment: 'pioneering cortisone drugs, steroids and birth control pills',\n  imageId: 'IOjWm71'\n}, {\n  id: 4,\n  name: 'Subrahmanyan Chandrasekhar',\n  profession: 'astrophysicist',\n  accomplishment: 'white dwarf star mass calculations',\n  imageId: 'lrWQx8l'\n}];\n```\n\n```js src/utils.js\nexport function getImageUrl(person) {\n  return (\n    'https://i.imgur.com/' +\n    person.imageId +\n    's.jpg'\n  );\n}\n```\n\n```css\nul { list-style-type: none; padding: 0px 10px; }\nli {\n  margin-bottom: 10px;\n  display: grid;\n  grid-template-columns: auto 1fr;\n  gap: 20px;\n  align-items: center;\n}\nimg { width: 100px; height: 100px; border-radius: 50%; }\n```\n\n</Sandpack>\n\n<Solution>\n\n`filter()`를 두 번 사용해서 두 개의 개별 배열을 만든 다음 두 배열을 모두 `매핑`할 수 있습니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { people } from './data.js';\nimport { getImageUrl } from './utils.js';\n\nexport default function List() {\n  const chemists = people.filter(person =>\n    person.profession === 'chemist'\n  );\n  const everyoneElse = people.filter(person =>\n    person.profession !== 'chemist'\n  );\n  return (\n    <article>\n      <h1>Scientists</h1>\n      <h2>Chemists</h2>\n      <ul>\n        {chemists.map(person =>\n          <li key={person.id}>\n            <img\n              src={getImageUrl(person)}\n              alt={person.name}\n            />\n            <p>\n              <b>{person.name}:</b>\n              {' ' + person.profession + ' '}\n              known for {person.accomplishment}\n            </p>\n          </li>\n        )}\n      </ul>\n      <h2>Everyone Else</h2>\n      <ul>\n        {everyoneElse.map(person =>\n          <li key={person.id}>\n            <img\n              src={getImageUrl(person)}\n              alt={person.name}\n            />\n            <p>\n              <b>{person.name}:</b>\n              {' ' + person.profession + ' '}\n              known for {person.accomplishment}\n            </p>\n          </li>\n        )}\n      </ul>\n    </article>\n  );\n}\n```\n\n```js src/data.js\nexport const people = [{\n  id: 0,\n  name: 'Creola Katherine Johnson',\n  profession: 'mathematician',\n  accomplishment: 'spaceflight calculations',\n  imageId: 'MK3eW3A'\n}, {\n  id: 1,\n  name: 'Mario José Molina-Pasquel Henríquez',\n  profession: 'chemist',\n  accomplishment: 'discovery of Arctic ozone hole',\n  imageId: 'mynHUSa'\n}, {\n  id: 2,\n  name: 'Mohammad Abdus Salam',\n  profession: 'physicist',\n  accomplishment: 'electromagnetism theory',\n  imageId: 'bE7W1ji'\n}, {\n  id: 3,\n  name: 'Percy Lavon Julian',\n  profession: 'chemist',\n  accomplishment: 'pioneering cortisone drugs, steroids and birth control pills',\n  imageId: 'IOjWm71'\n}, {\n  id: 4,\n  name: 'Subrahmanyan Chandrasekhar',\n  profession: 'astrophysicist',\n  accomplishment: 'white dwarf star mass calculations',\n  imageId: 'lrWQx8l'\n}];\n```\n\n```js src/utils.js\nexport function getImageUrl(person) {\n  return (\n    'https://i.imgur.com/' +\n    person.imageId +\n    's.jpg'\n  );\n}\n```\n\n```css\nul { list-style-type: none; padding: 0px 10px; }\nli {\n  margin-bottom: 10px;\n  display: grid;\n  grid-template-columns: auto 1fr;\n  gap: 20px;\n  align-items: center;\n}\nimg { width: 100px; height: 100px; border-radius: 50%; }\n```\n\n</Sandpack>\n\n이 솔루션에서는 `map` 호출이 상위 `<ul>` 요소 안에 인라인으로 직접 배치되지만, 가독성을 위해 변수를 도입할 수도 있습니다.\n\n렌더링 된 리스트 간에 여전히 약간의 중복이 있습니다. 여기서 더 나아가 반복적인 부분을 `<ListSection>` 컴포넌트로 추출할 수 있습니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { people } from './data.js';\nimport { getImageUrl } from './utils.js';\n\nfunction ListSection({ title, people }) {\n  return (\n    <>\n      <h2>{title}</h2>\n      <ul>\n        {people.map(person =>\n          <li key={person.id}>\n            <img\n              src={getImageUrl(person)}\n              alt={person.name}\n            />\n            <p>\n              <b>{person.name}:</b>\n              {' ' + person.profession + ' '}\n              known for {person.accomplishment}\n            </p>\n          </li>\n        )}\n      </ul>\n    </>\n  );\n}\n\nexport default function List() {\n  const chemists = people.filter(person =>\n    person.profession === 'chemist'\n  );\n  const everyoneElse = people.filter(person =>\n    person.profession !== 'chemist'\n  );\n  return (\n    <article>\n      <h1>Scientists</h1>\n      <ListSection\n        title=\"Chemists\"\n        people={chemists}\n      />\n      <ListSection\n        title=\"Everyone Else\"\n        people={everyoneElse}\n      />\n    </article>\n  );\n}\n```\n\n```js src/data.js\nexport const people = [{\n  id: 0,\n  name: 'Creola Katherine Johnson',\n  profession: 'mathematician',\n  accomplishment: 'spaceflight calculations',\n  imageId: 'MK3eW3A'\n}, {\n  id: 1,\n  name: 'Mario José Molina-Pasquel Henríquez',\n  profession: 'chemist',\n  accomplishment: 'discovery of Arctic ozone hole',\n  imageId: 'mynHUSa'\n}, {\n  id: 2,\n  name: 'Mohammad Abdus Salam',\n  profession: 'physicist',\n  accomplishment: 'electromagnetism theory',\n  imageId: 'bE7W1ji'\n}, {\n  id: 3,\n  name: 'Percy Lavon Julian',\n  profession: 'chemist',\n  accomplishment: 'pioneering cortisone drugs, steroids and birth control pills',\n  imageId: 'IOjWm71'\n}, {\n  id: 4,\n  name: 'Subrahmanyan Chandrasekhar',\n  profession: 'astrophysicist',\n  accomplishment: 'white dwarf star mass calculations',\n  imageId: 'lrWQx8l'\n}];\n```\n\n```js src/utils.js\nexport function getImageUrl(person) {\n  return (\n    'https://i.imgur.com/' +\n    person.imageId +\n    's.jpg'\n  );\n}\n```\n\n```css\nul { list-style-type: none; padding: 0px 10px; }\nli {\n  margin-bottom: 10px;\n  display: grid;\n  grid-template-columns: auto 1fr;\n  gap: 20px;\n  align-items: center;\n}\nimg { width: 100px; height: 100px; border-radius: 50%; }\n```\n\n</Sandpack>\n\n세심한 독자라면 두 번의 `filter` 호출을 통해 각 사람의 직업을 두 번 확인하고 있다는 것을 알아차릴 수 있습니다. 속성을 확인하는 속도가 매우 빠르기 때문에 이 예시에서는 문제가 없습니다. 하지만 로직이 더 복잡하다면 `filter` 호출을 수동으로 배열을 구성하고 각 사람을 한 번씩 확인하는 반복문으로 대체할 수 있습니다.\n\n실제로 `people`이 바뀌지 않는다면 이 코드를 컴포넌트 외부로 옮길 수도 있습니다. React의 관점에서 중요한 것은 결국 JSX 노드 배열을 제공한다는 것입니다. 그 배열을 생성하는 방법은 중요하지 않습니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { people } from './data.js';\nimport { getImageUrl } from './utils.js';\n\nlet chemists = [];\nlet everyoneElse = [];\npeople.forEach(person => {\n  if (person.profession === 'chemist') {\n    chemists.push(person);\n  } else {\n    everyoneElse.push(person);\n  }\n});\n\nfunction ListSection({ title, people }) {\n  return (\n    <>\n      <h2>{title}</h2>\n      <ul>\n        {people.map(person =>\n          <li key={person.id}>\n            <img\n              src={getImageUrl(person)}\n              alt={person.name}\n            />\n            <p>\n              <b>{person.name}:</b>\n              {' ' + person.profession + ' '}\n              known for {person.accomplishment}\n            </p>\n          </li>\n        )}\n      </ul>\n    </>\n  );\n}\n\nexport default function List() {\n  return (\n    <article>\n      <h1>Scientists</h1>\n      <ListSection\n        title=\"Chemists\"\n        people={chemists}\n      />\n      <ListSection\n        title=\"Everyone Else\"\n        people={everyoneElse}\n      />\n    </article>\n  );\n}\n```\n\n```js src/data.js\nexport const people = [{\n  id: 0,\n  name: 'Creola Katherine Johnson',\n  profession: 'mathematician',\n  accomplishment: 'spaceflight calculations',\n  imageId: 'MK3eW3A'\n}, {\n  id: 1,\n  name: 'Mario José Molina-Pasquel Henríquez',\n  profession: 'chemist',\n  accomplishment: 'discovery of Arctic ozone hole',\n  imageId: 'mynHUSa'\n}, {\n  id: 2,\n  name: 'Mohammad Abdus Salam',\n  profession: 'physicist',\n  accomplishment: 'electromagnetism theory',\n  imageId: 'bE7W1ji'\n}, {\n  id: 3,\n  name: 'Percy Lavon Julian',\n  profession: 'chemist',\n  accomplishment: 'pioneering cortisone drugs, steroids and birth control pills',\n  imageId: 'IOjWm71'\n}, {\n  id: 4,\n  name: 'Subrahmanyan Chandrasekhar',\n  profession: 'astrophysicist',\n  accomplishment: 'white dwarf star mass calculations',\n  imageId: 'lrWQx8l'\n}];\n```\n\n```js src/utils.js\nexport function getImageUrl(person) {\n  return (\n    'https://i.imgur.com/' +\n    person.imageId +\n    's.jpg'\n  );\n}\n```\n\n```css\nul { list-style-type: none; padding: 0px 10px; }\nli {\n  margin-bottom: 10px;\n  display: grid;\n  grid-template-columns: auto 1fr;\n  gap: 20px;\n  align-items: center;\n}\nimg { width: 100px; height: 100px; border-radius: 50%; }\n```\n\n</Sandpack>\n\n</Solution>\n\n#### 하나의 컴포넌트에 중첩된 리스트 {/*nested-lists-in-one-component*/}\n\n이 배열에서 레시피 리스트를 만들어 보세요! 배열의 각 레시피에 대해 이름을 `<h2>`로 표시하고 재료를 `<ul>`에 나열합니다.\n\n<Hint>\n\n이렇게 하려면 서로 다른 두 개의 `map` 호출을 중첩해야 합니다.\n\n</Hint>\n\n<Sandpack>\n\n```js src/App.js\nimport { recipes } from './data.js';\n\nexport default function RecipeList() {\n  return (\n    <div>\n      <h1>Recipes</h1>\n    </div>\n  );\n}\n```\n\n```js src/data.js\nexport const recipes = [{\n  id: 'greek-salad',\n  name: 'Greek Salad',\n  ingredients: ['tomatoes', 'cucumber', 'onion', 'olives', 'feta']\n}, {\n  id: 'hawaiian-pizza',\n  name: 'Hawaiian Pizza',\n  ingredients: ['pizza crust', 'pizza sauce', 'mozzarella', 'ham', 'pineapple']\n}, {\n  id: 'hummus',\n  name: 'Hummus',\n  ingredients: ['chickpeas', 'olive oil', 'garlic cloves', 'lemon', 'tahini']\n}];\n```\n\n</Sandpack>\n\n<Solution>\n\n한 가지 방법을 소개합니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { recipes } from './data.js';\n\nexport default function RecipeList() {\n  return (\n    <div>\n      <h1>Recipes</h1>\n      {recipes.map(recipe =>\n        <div key={recipe.id}>\n          <h2>{recipe.name}</h2>\n          <ul>\n            {recipe.ingredients.map(ingredient =>\n              <li key={ingredient}>\n                {ingredient}\n              </li>\n            )}\n          </ul>\n        </div>\n      )}\n    </div>\n  );\n}\n```\n\n```js src/data.js\nexport const recipes = [{\n  id: 'greek-salad',\n  name: 'Greek Salad',\n  ingredients: ['tomatoes', 'cucumber', 'onion', 'olives', 'feta']\n}, {\n  id: 'hawaiian-pizza',\n  name: 'Hawaiian Pizza',\n  ingredients: ['pizza crust', 'pizza sauce', 'mozzarella', 'ham', 'pineapple']\n}, {\n  id: 'hummus',\n  name: 'Hummus',\n  ingredients: ['chickpeas', 'olive oil', 'garlic cloves', 'lemon', 'tahini']\n}];\n```\n\n</Sandpack>\n\n각 `recipes`에는 이미 `id` 필드가 포함되어 있으므로 외부 반복문은 이 필드를 `key`로 사용합니다. 재료를 순회할 때 사용할 수 있는 ID는 없습니다. 하지만 같은 레시피에서 같은 재료가 두 번씩 나열되지 않는다고 가정하면 재료의 이름을 `key`로 사용할 수 있습니다. 또는 데이터 구조를 변경하여 ID를 추가하거나 인덱스를 `key`로 사용할 수 있습니다. (단, 재료 순서를 안전하게 바꿀 수 없다는 점에 유의하세요.)\n\n</Solution>\n\n#### 리스트 항목 컴포넌트 추출하기 {/*extracting-a-list-item-component*/}\n\n`RecipeList` 컴포넌트에는 두 개의 중첩된 `map` 호출이 포함되어 있습니다. 이를 단순화하기 위해 `id`, `name`, `ingredients` props를 허용하는 `Recipe` 컴포넌트를 추출합니다. 외부 `key`를 어디에 위치하고 그 이유는 무엇일까요?\n\n<Sandpack>\n\n```js src/App.js\nimport { recipes } from './data.js';\n\nexport default function RecipeList() {\n  return (\n    <div>\n      <h1>Recipes</h1>\n      {recipes.map(recipe =>\n        <div key={recipe.id}>\n          <h2>{recipe.name}</h2>\n          <ul>\n            {recipe.ingredients.map(ingredient =>\n              <li key={ingredient}>\n                {ingredient}\n              </li>\n            )}\n          </ul>\n        </div>\n      )}\n    </div>\n  );\n}\n```\n\n```js src/data.js\nexport const recipes = [{\n  id: 'greek-salad',\n  name: 'Greek Salad',\n  ingredients: ['tomatoes', 'cucumber', 'onion', 'olives', 'feta']\n}, {\n  id: 'hawaiian-pizza',\n  name: 'Hawaiian Pizza',\n  ingredients: ['pizza crust', 'pizza sauce', 'mozzarella', 'ham', 'pineapple']\n}, {\n  id: 'hummus',\n  name: 'Hummus',\n  ingredients: ['chickpeas', 'olive oil', 'garlic cloves', 'lemon', 'tahini']\n}];\n```\n\n</Sandpack>\n\n<Solution>\n\n외부 `map`의 JSX를 새 `Recipe` 컴포넌트에 복사하여 붙여넣고 해당 JSX를 반환할 수 있습니다. 그런 다음 `recipe.name`을 `name`으로, `recipe.id`를 `id`로 변경하는 등의 작업을 수행하여 `Recipe`에 props로 전달할 수 있습니다.\n\n<Sandpack>\n\n```js\nimport { recipes } from './data.js';\n\nfunction Recipe({ id, name, ingredients }) {\n  return (\n    <div>\n      <h2>{name}</h2>\n      <ul>\n        {ingredients.map(ingredient =>\n          <li key={ingredient}>\n            {ingredient}\n          </li>\n        )}\n      </ul>\n    </div>\n  );\n}\n\nexport default function RecipeList() {\n  return (\n    <div>\n      <h1>Recipes</h1>\n      {recipes.map(recipe =>\n        <Recipe {...recipe} key={recipe.id} />\n      )}\n    </div>\n  );\n}\n```\n\n```js src/data.js\nexport const recipes = [{\n  id: 'greek-salad',\n  name: 'Greek Salad',\n  ingredients: ['tomatoes', 'cucumber', 'onion', 'olives', 'feta']\n}, {\n  id: 'hawaiian-pizza',\n  name: 'Hawaiian Pizza',\n  ingredients: ['pizza crust', 'pizza sauce', 'mozzarella', 'ham', 'pineapple']\n}, {\n  id: 'hummus',\n  name: 'Hummus',\n  ingredients: ['chickpeas', 'olive oil', 'garlic cloves', 'lemon', 'tahini']\n}];\n```\n\n</Sandpack>\n\n여기서 `<Recipe {...recipe} key={recipe.id} />`는 \"`recipe` 객체의 모든 속성을 props로 `Recipe` 컴포넌트로 전달\"하는 손쉬운 구문입니다. `<Recipe id={recipe.id} name={recipe.name} ingredients={recipe.ingredients} key={recipe.id} />` 처럼 각 prop을 명시적으로 작성할 수도 있습니다.\n\n**`key`는 `Recipe`에서 반환된 루트 `<div>`가 아니라 `<Recipe>` 자체에 지정된다는 점에 유의하세요.** 이는 이 `key`가 주변 배열의 context 내에서 직접 필요하기 때문입니다. 이전에는 `<div>` 배열이 있었기 때문에 각 배열에 `key`가 필요했지만, 지금은 `<Recipe>` 배열이 있습니다. 즉, 컴포넌트를 추출할 때 복사하여 붙여넣은 JSX 외부에 `key`를 남겨두는 것을 잊지 마세요.\n\n</Solution>\n\n#### 구분 기호가 있는 리스트 {/*list-with-a-separator*/}\n\n이 예시는 Tachibana Hokushi 의 유명한 하이쿠(일본의 정형시)를 렌더링하며, 각 행은 `<p>` 태그로 래핑되어 있습니다. 여러분이 해야 할 일은 각 단락 사이에 `<hr />` 구분 기호를 삽입하는 것입니다. 결과 구조는 다음과 같아야 합니다.\n\n```js\n<article>\n  <p>I write, erase, rewrite</p>\n  <hr />\n  <p>Erase again, and then</p>\n  <hr />\n  <p>A poppy blooms.</p>\n</article>\n```\n\n하이쿠는 세 줄로만 구성되어 있지만 솔루션 코드는 몇 줄이든 상관없이 동작합니다. `<hr />` 요소는 `<p>`요소 *사이*에만 표시되며 시작이나 끝에는 표시되지 않는다는 점에 유의하세요!\n\n<Sandpack>\n\n```js\nconst poem = {\n  lines: [\n    'I write, erase, rewrite',\n    'Erase again, and then',\n    'A poppy blooms.'\n  ]\n};\n\nexport default function Poem() {\n  return (\n    <article>\n      {poem.lines.map((line, index) =>\n        <p key={index}>\n          {line}\n        </p>\n      )}\n    </article>\n  );\n}\n```\n\n```css\nbody {\n  text-align: center;\n}\np {\n  font-family: Georgia, serif;\n  font-size: 20px;\n  font-style: italic;\n}\nhr {\n  margin: 0 120px 0 120px;\n  border: 1px dashed #45c3d8;\n}\n```\n\n</Sandpack>\n\n(시의 행은 절대로 순서가 바뀌지 않으므로 인덱스를 key로 사용할 수 있는 드문 예입니다.)\n\n<Hint>\n\n`map`을 반복문으로 변환하거나 Fragment 를 사용해야 합니다.\n\n</Hint>\n\n<Solution>\n\n다음과 같이 반복문을 작성하여  `<hr />` 과 `<p>...</p>`를 출력 배열에 삽입할 수 있습니다.\n\n<Sandpack>\n\n```js\nconst poem = {\n  lines: [\n    'I write, erase, rewrite',\n    'Erase again, and then',\n    'A poppy blooms.'\n  ]\n};\n\nexport default function Poem() {\n  let output = [];\n\n  // 출력할 배열을 작성합니다.\n  poem.lines.forEach((line, i) => {\n    output.push(\n      <hr key={i + '-separator'} />\n    );\n    output.push(\n      <p key={i + '-text'}>\n        {line}\n      </p>\n    );\n  });\n  // 첫 번째 <hr />을 삭제합니다.\n  output.shift();\n\n  return (\n    <article>\n      {output}\n    </article>\n  );\n}\n```\n\n```css\nbody {\n  text-align: center;\n}\np {\n  font-family: Georgia, serif;\n  font-size: 20px;\n  font-style: italic;\n}\nhr {\n  margin: 0 120px 0 120px;\n  border: 1px dashed #45c3d8;\n}\n```\n\n</Sandpack>\n\n각 구분 기호와 단락이 동일한 배열에 있기 때문에 원래 행의 인덱스를 `key`로 사용하면 더는 작동하지 않습니다. 하지만  `key={i + '-text'}` 처럼 접미사를 사용해서 각각에 고유한 key를 부여할 수 있습니다.\n\n또는 `<hr />` 과 `<p>...</p>` 를 포함한 Fragment 모음을 렌더링할 수 있습니다. 하지만 `<> </>` 이렇게 쓰는 손쉬운 문법은 key를 전달해주지 않기 때문에 `<Fragment>` 를 명시적으로 작성해야 합니다.\n\n<Sandpack>\n\n```js\nimport React, { Fragment } from 'react';\n\nconst poem = {\n  lines: [\n    'I write, erase, rewrite',\n    'Erase again, and then',\n    'A poppy blooms.'\n  ]\n};\n\nexport default function Poem() {\n  return (\n    <article>\n      {poem.lines.map((line, i) =>\n        <Fragment key={i}>\n          {i > 0 && <hr />}\n          <p>{line}</p>\n        </Fragment>\n      )}\n    </article>\n  );\n}\n```\n\n```css\nbody {\n  text-align: center;\n}\np {\n  font-family: Georgia, serif;\n  font-size: 20px;\n  font-style: italic;\n}\nhr {\n  margin: 0 120px 0 120px;\n  border: 1px dashed #45c3d8;\n}\n```\n\n</Sandpack>\n\n종종 `<> </>` 이렇게 쓰이는 Fragment 는 부가적인 `<div>`를 추가하지 않고도 JSX 노드를 그룹화할 수 있다는 것을 기억하세요!\n\n</Solution>\n\n</Challenges>\n"
  },
  {
    "path": "src/content/learn/responding-to-events.md",
    "content": "---\ntitle: 이벤트에 응답하기\n---\n\n<Intro>\n\nReact에서는 JSX에 *이벤트 핸들러*를 추가할 수 있습니다. 이벤트 핸들러는 클릭, 마우스 호버, 폼 인풋 포커스 등 사용자 상호작용에 따라 유발되는 사용자 정의 함수입니다.\n\n</Intro>\n\n<YouWillLearn>\n\n* 이벤트 핸들러를 작성하는 여러 가지 방법\n* 이벤트 처리 로직을 부모 컴포넌트에서 전달하는 방법\n* 이벤트가 전파되는 방식과 이를 멈추는 방법\n\n</YouWillLearn>\n\n## 이벤트 핸들러 추가하기 {/*adding-event-handlers*/}\n\n이벤트 핸들러 추가를 위해서는 먼저 함수를 정의하고 이를 적절한 JSX 태그에 [prop 형태로 전달](/learn/passing-props-to-a-component)해야 합니다. 아래 예시는 아직 아무런 동작도 수행하지 않는 버튼입니다.\n\n<Sandpack>\n\n```js\nexport default function Button() {\n  return (\n    <button>\n      I don't do anything\n    </button>\n  );\n}\n```\n\n</Sandpack>\n\n이제 다음 3단계 과정을 거쳐 사용자가 버튼을 클릭할 경우 메시지를 보여주도록 만들어 보겠습니다.\n\n1. `Button` 컴포넌트 *내부에* `handleClick` 함수를 선언합니다.\n2. 해당 함수 내부 로직을 구현합니다. 이번에는 메시지를 표시하기 위해 `alert`를 사용합니다.\n3. `<button>` JSX에 `onClick={handleClick}`을 추가합니다.\n\n<Sandpack>\n\n```js\nexport default function Button() {\n  function handleClick() {\n    alert('You clicked me!');\n  }\n\n  return (\n    <button onClick={handleClick}>\n      Click me\n    </button>\n  );\n}\n```\n\n```css\nbutton { margin-right: 10px; }\n```\n\n</Sandpack>\n\n`handleClick` 함수를 정의하였고 이를 `<button>`에 [prop 형태로 전달](/learn/passing-props-to-a-component)하였습니다. 여기서 `handleClick`은 **이벤트 핸들러**입니다. 이벤트 핸들러 함수는 다음 특징을 가집니다.\n\n* 주로 컴포넌트 *내부*에서 정의됩니다.\n* `handle`로 시작하고 그 뒤에 이벤트명을 붙인 함수명을 가집니다.\n\n관습적으로 `handle`로 시작하며 이벤트명을 이어 붙인 이벤트 핸들러 명명법이 일반적입니다. `onClick={handleClick}`, `onMouseEnter={handleMouseEnter}`와 같은 경우를 자주 볼 수 있을 것입니다.\n\n다른 방법으로 이벤트 핸들러를 JSX 내에서 인라인으로 정의할 수 있습니다.\n\n```jsx\n<button onClick={function handleClick() {\n  alert('You clicked me!');\n}}>\n```\n\n또는 화살표 함수를 사용하여 보다 간결하게 정의할 수도 있습니다.\n\n```jsx\n<button onClick={() => {\n  alert('You clicked me!');\n}}>\n```\n\n이러한 스타일은 모두 동일한 결과를 보여줍니다. 특히 인라인 이벤트 핸들러는 짧은 함수들을 정의할 때 편리합니다.\n\n<Pitfall>\n\n이벤트 핸들러로 전달한 함수들은 호출이 아닌 전달되어야 합니다. 아래는 예시입니다.\n\n| 함수를 전달하기 (올바른 예시)      | 함수를 호출하기 (잘못된 예시)        |\n| -------------------------------- | ---------------------------------- |\n| `<button onClick={handleClick}>` | `<button onClick={handleClick()}>` |\n\n이 차이는 미묘합니다. 첫 번째 예시에서 `handleClick` 함수는 `onClick` 이벤트 핸들러에 전달되었습니다. 이후 React는 이 내용을 기억하고 오직 사용자가 버튼을 클릭하였을 때만 함수를 호출하도록 합니다.\n\n두 번째 예시에서는 `handleClick()` 끝의 `()`가 [렌더링](/learn/render-and-commit) 과정 중 클릭이 없었음에도 불구하고 *즉시* 함수를 실행하도록 만듭니다. 이는 [JSX `{` 와 `}`](/learn/javascript-in-jsx-with-curly-braces) 내의 자바스크립트가 즉시 실행되기 때문입니다.\n\n인라인으로 코드를 작성할 때에도 동일한 함정이 다른 형태로 나타납니다.\n\n| 함수를 전달하기 (올바른 예시)             | 함수를 호출하기 (잘못된 예시)       |\n| --------------------------------------- | --------------------------------- |\n| `<button onClick={() => alert('...')}>` | `<button onClick={alert('...')}>` |\n\n\n다음과 같이 인라인 함수를 전달하면 버튼을 클릭할 때마다 실행되는 것이 아니라 컴포넌트가 렌더링 될 때마다 실행될 것입니다.\n\n```jsx\n// 이 alert는 클릭 시 실행되지 않고 컴포넌트가 렌더링 된 시점에 실행됩니다!\n<button onClick={alert('You clicked me!')}>\n```\n\n만약 이벤트 핸들러를 인라인으로 정의하고자 한다면, 아래와 같이 익명 함수로 감싸면 됩니다.\n\n```jsx\n<button onClick={() => alert('You clicked me!')}>\n```\n\n이러한 방법으로 매 렌더링마다 내부 코드를 실행하지 않고 함수를 생성하여 추후 이벤트에 의해 호출되게 합니다.\n\n두 가지 경우 모두, 전달하는 것은 함수입니다.\n\n* `<button onClick={handleClick}>`은 `handleClick` 함수를 전달합니다.\n* `<button onClick={() => alert('...')}>`은 `() => alert('...')` 함수를 전달합니다.\n\n[화살표 함수에 대해 더 알아보세요.](https://ko.javascript.info/arrow-functions-basics)\n\n</Pitfall>\n\n### 이벤트 핸들러 내에서 Prop 읽기 {/*reading-props-in-event-handlers*/}\n\n이벤트 핸들러는 컴포넌트 내부에서 선언되기에 이들은 해당 컴포넌트의 prop에 접근할 수 있습니다. 아래에서 클릭시 `message` prop의 내용을 포함한 alert를 표시하는 버튼을 볼 수 있습니다.\n\n<Sandpack>\n\n```js\nfunction AlertButton({ message, children }) {\n  return (\n    <button onClick={() => alert(message)}>\n      {children}\n    </button>\n  );\n}\n\nexport default function Toolbar() {\n  return (\n    <div>\n      <AlertButton message=\"Playing!\">\n        Play Movie\n      </AlertButton>\n      <AlertButton message=\"Uploading!\">\n        Upload Image\n      </AlertButton>\n    </div>\n  );\n}\n```\n\n```css\nbutton { margin-right: 10px; }\n```\n\n</Sandpack>\n\n위와 같이 두 개의 버튼이 서로 다른 메시지를 표시할 수 있습니다. 전달되는 메시지를 변경해보세요.\n\n### 이벤트 핸들러를 Prop으로 전달하기 {/*passing-event-handlers-as-props*/}\n\n종종 부모 컴포넌트로 자식의 이벤트 핸들러를 지정하기를 원할 수 있습니다. 버튼의 경우를 고려해봅시다. `Button` 컴포넌트를 사용하는 위치에 따라 다른 기능을 수행하도록 만들고자 할 때가 있을 것입니다. 한 버튼은 영화를 재생하고 다른 버튼은 이미지를 업로드하도록 말이죠.\n\n이러한 기능을 위해서 컴포넌트가 그 부모 컴포넌트로부터 받은 prop을 이벤트 핸들러로 다음과 같이 전달합니다.\n\n<Sandpack>\n\n```js\nfunction Button({ onClick, children }) {\n  return (\n    <button onClick={onClick}>\n      {children}\n    </button>\n  );\n}\n\nfunction PlayButton({ movieName }) {\n  function handlePlayClick() {\n    alert(`Playing ${movieName}!`);\n  }\n\n  return (\n    <Button onClick={handlePlayClick}>\n      Play \"{movieName}\"\n    </Button>\n  );\n}\n\nfunction UploadButton() {\n  return (\n    <Button onClick={() => alert('Uploading!')}>\n      Upload Image\n    </Button>\n  );\n}\n\nexport default function Toolbar() {\n  return (\n    <div>\n      <PlayButton movieName=\"Kiki's Delivery Service\" />\n      <UploadButton />\n    </div>\n  );\n}\n```\n\n```css\nbutton { margin-right: 10px; }\n```\n\n</Sandpack>\n\n위 코드에서는 `Toolbar` 컴포넌트가 `PlayButton`과 `UploadButton`을 렌더링합니다.\n\n- `PlayButton`은 `handlePlayClick`을 `Button` 내 `onClick` prop으로 전달합니다.\n- `UploadButton`은 `() => alert('Uploading!')`을 `Button` 내 `onClick` prop으로 전달합니다.\n\n최종적으로, `Button` 컴포넌트는 `onClick` prop을 받습니다. 이후 받은 prop을 브라우저 빌트인 `<button>`의 `onClick={onClick}`으로 직접 전달합니다. 이를 통해 React가 전달받은 함수를 클릭 시점에 호출함을 알 수 있습니다.\n\n만약 [디자인 시스템](https://uxdesign.cc/everything-you-need-to-know-about-design-systems-54b109851969)을 적용한다면 버튼과 같은 컴포넌트는 동작을 지정하지 않고 스타일만 지정하는 것이 일반적입니다. 그 대신, `PlayButton`과 `UploadButton` 같은 컴포넌트가 이벤트 핸들러를 전달하도록 합니다.\n\n### 이벤트 핸들러 Prop 명명하기 {/*naming-event-handler-props*/}\n\n`<button>`과 `<div>` 같은 빌트인 컴포넌트는 `onClick`과 같은 [브라우저 이벤트 이름](/reference/react-dom/components/common#common-props) 만을 지원합니다. 그러나 사용자 정의 컴포넌트에서는 이벤트 핸들러 prop의 이름을 원하는 대로 명명할 수 있습니다.\n\n관습적으로 이벤트 핸들러 prop의 이름은 `on`으로 시작하여 대문자 영문으로 이어집니다.\n\n그 예시로, `Button` 컴포넌트의 `onClick` prop은 `onSmash`라는 이름으로 호출할 수도 있습니다.\n\n<Sandpack>\n\n```js\nfunction Button({ onSmash, children }) {\n  return (\n    <button onClick={onSmash}>\n      {children}\n    </button>\n  );\n}\n\nexport default function App() {\n  return (\n    <div>\n      <Button onSmash={() => alert('Playing!')}>\n        Play Movie\n      </Button>\n      <Button onSmash={() => alert('Uploading!')}>\n        Upload Image\n      </Button>\n    </div>\n  );\n}\n```\n\n```css\nbutton { margin-right: 10px; }\n```\n\n</Sandpack>\n\n이 예시에서 `<button onClick={onSmash}>`은 브라우저의 `<button>`(소문자)이 여전히 `onClick` prop을 필요로 하고 있음을 보여줍니다. 그러나 여러분이 직접 정의한 `Button` 컴포넌트가 받게 될 prop의 이름은 여러분이 원하는 대로 명명할 수 있습니다.\n\n컴포넌트가 여러 상호작용을 지원한다면 이벤트 핸들러 prop을 애플리케이션에 특화시켜 명명할 수 있습니다. 예시에서는 `Toolbar` 컴포넌트가 `onPlayMovie`와 `onUploadImage` 이벤트 핸들러를 받습니다.\n\n<Sandpack>\n\n```js\nexport default function App() {\n  return (\n    <Toolbar\n      onPlayMovie={() => alert('Playing!')}\n      onUploadImage={() => alert('Uploading!')}\n    />\n  );\n}\n\nfunction Toolbar({ onPlayMovie, onUploadImage }) {\n  return (\n    <div>\n      <Button onClick={onPlayMovie}>\n        Play Movie\n      </Button>\n      <Button onClick={onUploadImage}>\n        Upload Image\n      </Button>\n    </div>\n  );\n}\n\nfunction Button({ onClick, children }) {\n  return (\n    <button onClick={onClick}>\n      {children}\n    </button>\n  );\n}\n```\n\n```css\nbutton { margin-right: 10px; }\n```\n\n</Sandpack>\n\n`App` 컴포넌트는 `Toolbar`가 `onPlayMovie` 또는 `onUploadImage`를 가지고 *무엇*을 할 것인지 알 필요가 없음에 유의하세요. 이 `Toolbar` 구현의 특별한 부분입니다. 지금 `Toolbar`는 위 요소들을 `Button`의 `onClick` 핸들러 요소로 내려보내지만, 추후에는 키보드 바로가기 키 입력을 통해 이들을 활성화할 수도 있을 것입니다. `onPlayMovie`와 같이 prop 이름을 애플리케이션별 상호작용에 기반하여 명명한다면 나중에 어떻게 이를 이용하게 될지에 대한 유연성을 제공할 것입니다.\n\n<Note>\n\n이벤트 핸들러에 적절한 HTML 태그를 사용하고 있는지 확인하세요. 예를 들어 클릭을 처리하기 위해서는 `<div onClick={handleClick}>` 대신 [`<button onClick={handleClick}>`](https://developer.mozilla.org/ko/docs/Web/HTML/Element/button)을 사용하세요. 실제 브라우저에서 `<button>`은 키보드 내비게이션과 같은 빌트인 브라우저 동작을 활성화 합니다. 만일 버튼의 기본 브라우저 스타일링이 싫어서 링크나 다른 UI 요소처럼 보이도록 하고 싶다면 CSS를 통해 그 목적을 이룰 수 있습니다. [접근성을 위한 마크업 작성법에 대해 더 알아보세요.](https://developer.mozilla.org/ko/docs/Learn/Accessibility/HTML)\n\n</Note>\n\n## 이벤트 전파 {/*event-propagation*/}\n\n이벤트 핸들러는 해당 컴포넌트가 가진 어떤 자식 컴포넌트의 이벤트를 수신할 수도 있습니다. 이를 이벤트가 트리를 따라 \"bubble\" 되거나 \"전파된다\"고 표현합니다. 이때 이벤트는 발생한 지점에서 시작하여 트리를 따라 위로 전달됩니다.\n\n아래 `<div>`는 두 개의 버튼을 포함하고 있습니다. `<div>` *그리고* 각각의 버튼은 각자의 `onClick` 핸들러를 가지고 있습니다. 버튼을 클릭한다면 어느 핸들러가 작동하게 될까요?\n\n<Sandpack>\n\n```js\nexport default function Toolbar() {\n  return (\n    <div className=\"Toolbar\" onClick={() => {\n      alert('You clicked on the toolbar!');\n    }}>\n      <button onClick={() => alert('Playing!')}>\n        Play Movie\n      </button>\n      <button onClick={() => alert('Uploading!')}>\n        Upload Image\n      </button>\n    </div>\n  );\n}\n```\n\n```css\n.Toolbar {\n  background: #aaa;\n  padding: 5px;\n}\nbutton { margin: 5px; }\n```\n\n</Sandpack>\n\n둘 중의 어느 버튼을 클릭하더라도 해당 버튼의 `onClick`이 먼저 실행될 것이며 이후 부모인 `<div>`의 `onClick`이 뒤이어 실행될 것입니다. 따라서 두 개의 메시지가 표시될 것입니다. 만약 툴바 자체를 클릭한다면 오직 부모인 `<div>`의 `onClick` 만이 실행될 것입니다.\n\n<Pitfall>\n\n부여된 JSX 태그 내에서만 실행되는 `onScroll`을 제외한 React 내의 모든 이벤트는 전파됩니다.\n\n</Pitfall>\n\n### 전파 멈추기 {/*stopping-propagation*/}\n\n이벤트 핸들러는 **이벤트 오브젝트**를 유일한 매개변수로 받습니다. 관습을 따르자면 \"event\"를 의미하는 `e`로 호출되는 것이 일반적입니다. 이 오브젝트를 이벤트의 정보를 읽어들이는데 사용할 수 있습니다.\n\n이러한 이벤트 오브젝트는 전파를 멈출 수 있게 해줍니다. 이벤트가 부모 컴포넌트에 닿지 못하도록 막으려면 아래 `Button` 컴포넌트와 같이 `e.stopPropagation()`를 호출합니다.\n\n<Sandpack>\n\n```js\nfunction Button({ onClick, children }) {\n  return (\n    <button onClick={e => {\n      e.stopPropagation();\n      onClick();\n    }}>\n      {children}\n    </button>\n  );\n}\n\nexport default function Toolbar() {\n  return (\n    <div className=\"Toolbar\" onClick={() => {\n      alert('You clicked on the toolbar!');\n    }}>\n      <Button onClick={() => alert('Playing!')}>\n        Play Movie\n      </Button>\n      <Button onClick={() => alert('Uploading!')}>\n        Upload Image\n      </Button>\n    </div>\n  );\n}\n```\n\n```css\n.Toolbar {\n  background: #aaa;\n  padding: 5px;\n}\nbutton { margin: 5px; }\n```\n\n</Sandpack>\n\n버튼을 클릭하면 다음과 같은 절차가 진행됩니다.\n\n1. React가 `<button>`에 전달된 `onClick` 핸들러를 호출합니다.\n2. `Button`에 정의된 해당 핸들러는 다음을 수행합니다.\n   * `e.stopPropagation()`을 호출하여 이벤트가 더 이상 bubbling 되지 않도록 방지합니다.\n   * `Toolbar` 컴포넌트가 전달해 준 `onClick` 함수를 호출합니다.\n3. `Toolbar` 컴포넌트에서 정의된 위 함수가 버튼의 alert를 표시합니다.\n4. 전파가 중단되었으므로 부모인 `<div>`의 `onClick`은 *실행되지 않습니다*.\n\n`e.stopPropagation()`의 결과, 버튼을 클릭하는 것은 이제 `<button>`과 그 부모인 툴바의 `<div>`가 보내는 두 개의 alert를 표시하지 않고 단 하나의 `<button>` alert 만을 표시합니다. 버튼을 클릭하는 것은 그 주변의 툴바 부분을 클릭하는 것과 같지 않기에 이 UI 상에서는 전파를 멈추는 것이 합리적일 것입니다.\n\n<DeepDive>\n\n#### 단계별 이벤트 캡처 {/*capture-phase-events*/}\n\n드물게 *전파가 중단된 상황일지라도* 자식 컴포넌트의 모든 이벤트를 캡처해 확인해야 할 수 있습니다. 일례로, 분석을 위해 전파 로직에 상관 없이 모든 클릭 이벤트를 기록하고 싶을 수 있습니다. 이를 위해서는 이벤트명 마지막에 `Capture`를 추가하면 됩니다.\n\n```js\n<div onClickCapture={() => { /* 가장 먼저 실행됩니다 */ }}>\n  <button onClick={e => e.stopPropagation()} />\n  <button onClick={e => e.stopPropagation()} />\n</div>\n```\n\n각각의 이벤트는 세 단계를 거쳐 전파됩니다.\n\n1. 아래로 전달되면서 만나는 모든 `onClickCapture` 핸들러를 호출합니다.\n2. 클릭된 요소의 `onClick` 핸들러를 실행합니다.\n3. 위로 전달되면서 만나는 모든 `onClick` 핸들러를 호출합니다.\n\n이벤트 캡처는 라우터나 분석을 위한 코드에 유용할 수 있지만 일반 애플리케이션 코드에서는 사용하지 않을 가능성이 높습니다.\n\n</DeepDive>\n\n### 전파의 대안으로 핸들러를 전달하기 {/*passing-handlers-as-alternative-to-propagation*/}\n\n아래 클릭 핸들러가 어떤 코드 라인을 실행시킨 _이후에_ 부모로부터 전달받은 `onClick` prop을 호출하는지 확인해보세요.\n\n```js {4,5}\nfunction Button({ onClick, children }) {\n  return (\n    <button onClick={e => {\n      e.stopPropagation();\n      onClick();\n    }}>\n      {children}\n    </button>\n  );\n}\n```\n\n이 핸들러 내에서 부모의 `onClick` 이벤트 핸들러를 호출하는 부분 앞에 코드를 더 추가할 수도 있습니다. 이러한 패턴은 전파의 대안을 제공합니다. 부모 컴포넌트가 일부 추가적인 동작에 특화되도록 하면서 자식 컴포넌트가 이벤트를 처리할 수 있도록 합니다. 전파와는 다르게 자동으로 동작하지 않습니다. 이 패턴의 장점은 일부 이벤트의 결과로 실행되는 전체 코드 체인을 명확히 좇을 수 있게 해줍니다.\n\n전파를 활용하고 있지만 어떤 핸들러가 왜 실행되는 지 추적하는데 어려움을 겪고 있다면 이러한 접근법을 시도해 보시기 바랍니다.\n\n### 기본 동작 방지하기 {/*preventing-default-behavior*/}\n\n일부 브라우저 이벤트는 그와 관련된 기본 브라우저 동작을 가집니다. 일례로 `<form>`의 제출 이벤트는 그 내부의 버튼을 클릭 시 페이지 전체를 리로드하는 것이 기본 동작입니다.\n\n<Sandpack>\n\n```js\nexport default function Signup() {\n  return (\n    <form onSubmit={() => alert('Submitting!')}>\n      <input />\n      <button>Send</button>\n    </form>\n  );\n}\n```\n\n```css\nbutton { margin-left: 5px; }\n```\n\n</Sandpack>\n\n이러한 일이 발생하지 않도록 막기 위해 `e.preventDefault()`를 이벤트 오브젝트에서 호출할 수 있습니다.\n\n<Sandpack>\n\n```js\nexport default function Signup() {\n  return (\n    <form onSubmit={e => {\n      e.preventDefault();\n      alert('Submitting!');\n    }}>\n      <input />\n      <button>Send</button>\n    </form>\n  );\n}\n```\n\n```css\nbutton { margin-left: 5px; }\n```\n\n</Sandpack>\n\n`e.stopPropagation()`와 `e.preventDefault()`를 혼동하지 마세요. 둘 다 유용하지만, 서로 전혀 관련 없는 기능입니다.\n\n* [`e.stopPropagation()`](https://developer.mozilla.org/docs/Web/API/Event/stopPropagation)은 이벤트 핸들러가 상위 태그에서 실행되지 않도록 멈춥니다.\n* [`e.preventDefault()` ](https://developer.mozilla.org/docs/Web/API/Event/preventDefault)는 기본 브라우저 동작을 가진 일부 이벤트가 해당 기본 동작을 실행하지 않도록 방지합니다.\n\n## 이벤트 핸들러가 사이드 이펙트를 가질 수도 있나요? {/*can-event-handlers-have-side-effects*/}\n\n가능합니다! 이벤트 핸들러는 사이드 이펙트를 위한 최고의 위치입니다.\n\n함수를 렌더링하는 것과 다르게 이벤트 핸들러는 [순수할](/learn/keeping-components-pure) 필요가 없기에 무언가를 *변경*하는데 최적의 위치입니다. 예를 들어 타이핑에 반응해 입력 값을 수정하거나, 버튼 입력에 따라 리스트를 변경할 때 적절합니다. 그러나 일부 정보를 수정하기 위해서는 먼저 그 정보를 저장하기 위한 수단이 필요합니다. React에서는 [컴포넌트의 기억 역할을 하는 state](/learn/state-a-components-memory)를 이용할 수 있습니다. 해당 기능의 모든 것에 대해 다음 페이지에서 배울 것입니다.\n\n<Recap>\n\n* `<button>`과 같은 요소에 함수를 prop으로 전달하여 이벤트를 처리할 수 있습니다.\n* 이벤트 핸들러는 **호출이 아니라** 전달만 가능합니다! `onClick={handleClick()}`이 아니라 `onClick={handleClick}`입니다.\n* 이벤트 핸들러 함수는 별개의 함수 혹은 인라인 형태로 정의할 수 있습니다.\n* 이벤트 핸들러는 컴포넌트 내부에서 정의되기에 다른 prop에 접근할 수 있습니다.\n* 이벤트 핸들러는 부모에서 선언하여 자식에게 prop으로 전달할 수 있습니다.\n* 사용자 정의 이벤트 핸들러의 이름을 애플리케이션에 특화된 이름으로 명명할 수 있습니다.\n* 이벤트는 위쪽으로 전파됩니다. 첫 번째 매개변수로 `e.stopPropagation()`를 호출하여 방지할 수 있습니다.\n* 이벤트는 의도치 않은 기본 브라우저 동작을 유발할 수 있습니다. `e.preventDefault()`를 호출하여 방지할 수 있습니다.\n* 명시적으로 이벤트 핸들러 prop을 자식 핸들러에서 호출하는 것은 전파에 대한 좋은 대안이 될 수 있습니다.\n\n</Recap>\n\n\n\n<Challenges>\n\n#### 이벤트 핸들러 고치기 {/*fix-an-event-handler*/}\n\n이 버튼을 클릭하면 페이지 배경이 흰색과 검은색으로 교체되도록 하려 합니다. 그러나 지금은 클릭 시 아무 일도 일어나지 않습니다. 문제를 해결해보세요. (`handleClick` 내의 로직에 대해선 걱정 마세요. 해당 부분은 정상입니다.)\n\n<Sandpack>\n\n```js\nexport default function LightSwitch() {\n  function handleClick() {\n    let bodyStyle = document.body.style;\n    if (bodyStyle.backgroundColor === 'black') {\n      bodyStyle.backgroundColor = 'white';\n    } else {\n      bodyStyle.backgroundColor = 'black';\n    }\n  }\n\n  return (\n    <button onClick={handleClick()}>\n      Toggle the lights\n    </button>\n  );\n}\n```\n\n</Sandpack>\n\n<Solution>\n\n문제는 `<button onClick={handleClick()}>`이 `handleClick` 함수를 전달하는 것이 아닌 렌더링 중 _호출한다는_ 것입니다. `()` 호출을 삭제하여 `<button onClick={handleClick}>`로 수정하면 문제를 해결할 수 있습니다.\n\n<Sandpack>\n\n```js\nexport default function LightSwitch() {\n  function handleClick() {\n    let bodyStyle = document.body.style;\n    if (bodyStyle.backgroundColor === 'black') {\n      bodyStyle.backgroundColor = 'white';\n    } else {\n      bodyStyle.backgroundColor = 'black';\n    }\n  }\n\n  return (\n    <button onClick={handleClick}>\n      Toggle the lights\n    </button>\n  );\n}\n```\n\n</Sandpack>\n\n또 다른 방법으로 `<button onClick={() => handleClick()}>`와 같이 해당 호출을 다른 함수로 감쌀 수 있습니다.\n\n<Sandpack>\n\n```js\nexport default function LightSwitch() {\n  function handleClick() {\n    let bodyStyle = document.body.style;\n    if (bodyStyle.backgroundColor === 'black') {\n      bodyStyle.backgroundColor = 'white';\n    } else {\n      bodyStyle.backgroundColor = 'black';\n    }\n  }\n\n  return (\n    <button onClick={() => handleClick()}>\n      Toggle the lights\n    </button>\n  );\n}\n```\n\n</Sandpack>\n\n</Solution>\n\n#### 이벤트들을 연결하기 {/*wire-up-the-events*/}\n\n아래 `ColorSwitch` 컴포넌트는 버튼을 렌더링합니다. 이 버튼을 통해 페이지 색을 변경하고자 합니다. 버튼을 클릭하면 색이 변화할 수 있도록 부모로부터 받은 `onChangeColor` 이벤트 핸들러 prop에 연결하세요.\n\n위 작업을 마치면 해당 버튼을 클릭하는 것이 페이지 클릭 카운터의 수치 또한 증가시킴을 확인할 수 있습니다. 부모 컴포넌트를 작성한 여러분의 동료는 `onChangeColor`가 어떠한 카운터 수치도 증가시키지 않는다고 주장하네요. 왜 이런 일이 발생한 걸까요? 버튼을 클릭하면 색상만 변경되고, 카운터 수치는 증가하지 않도록 수정하세요.\n\n<Sandpack>\n\n```js src/ColorSwitch.js active\nexport default function ColorSwitch({\n  onChangeColor\n}) {\n  return (\n    <button>\n      Change color\n    </button>\n  );\n}\n```\n\n```js src/App.js hidden\nimport { useState } from 'react';\nimport ColorSwitch from './ColorSwitch.js';\n\nexport default function App() {\n  const [clicks, setClicks] = useState(0);\n\n  function handleClickOutside() {\n    setClicks(c => c + 1);\n  }\n\n  function getRandomLightColor() {\n    let r = 150 + Math.round(100 * Math.random());\n    let g = 150 + Math.round(100 * Math.random());\n    let b = 150 + Math.round(100 * Math.random());\n    return `rgb(${r}, ${g}, ${b})`;\n  }\n\n  function handleChangeColor() {\n    let bodyStyle = document.body.style;\n    bodyStyle.backgroundColor = getRandomLightColor();\n  }\n\n  return (\n    <div style={{ width: '100%', height: '100%' }} onClick={handleClickOutside}>\n      <ColorSwitch onChangeColor={handleChangeColor} />\n      <br />\n      <br />\n      <h2>Clicks on the page: {clicks}</h2>\n    </div>\n  );\n}\n```\n\n</Sandpack>\n\n<Solution>\n\n먼저, `<button onClick={onChangeColor}>`와 같이 이벤트 핸들러를 추가해야 합니다.\n\n그러나 이는 카운터 수치 증가라는 문제로 이어집니다. 여러분의 동료가 주장했듯 `onChangeColor`가 카운터 수치 증가에 영향을 끼칠 이유가 없다면 문제의 원인은 이벤트가 전파되어 다른 핸들러가 카운터에 영향을 끼쳤기 때문일 것입니다. 이 문제를 해결하려면 전파를 멈추어야 합니다. 또한 여전히 `onChangeColor`를 호출해야 한다는 것을 잊지 마세요.\n\n<Sandpack>\n\n```js src/ColorSwitch.js active\nexport default function ColorSwitch({\n  onChangeColor\n}) {\n  return (\n    <button onClick={e => {\n      e.stopPropagation();\n      onChangeColor();\n    }}>\n      Change color\n    </button>\n  );\n}\n```\n\n```js src/App.js hidden\nimport { useState } from 'react';\nimport ColorSwitch from './ColorSwitch.js';\n\nexport default function App() {\n  const [clicks, setClicks] = useState(0);\n\n  function handleClickOutside() {\n    setClicks(c => c + 1);\n  }\n\n  function getRandomLightColor() {\n    let r = 150 + Math.round(100 * Math.random());\n    let g = 150 + Math.round(100 * Math.random());\n    let b = 150 + Math.round(100 * Math.random());\n    return `rgb(${r}, ${g}, ${b})`;\n  }\n\n  function handleChangeColor() {\n    let bodyStyle = document.body.style;\n    bodyStyle.backgroundColor = getRandomLightColor();\n  }\n\n  return (\n    <div style={{ width: '100%', height: '100%' }} onClick={handleClickOutside}>\n      <ColorSwitch onChangeColor={handleChangeColor} />\n      <br />\n      <br />\n      <h2>Clicks on the page: {clicks}</h2>\n    </div>\n  );\n}\n```\n\n</Sandpack>\n\n</Solution>\n\n</Challenges>\n"
  },
  {
    "path": "src/content/learn/reusing-logic-with-custom-hooks.md",
    "content": "---\ntitle: 커스텀 Hook으로 로직 재사용하기\n---\n\n<Intro>\n\nReact는 `useState`, `useContext`, 그리고 `useEffect`같이 몇몇 내재하고 있는 Hook이 존재합니다. 가끔 조금 더 구체적인 목적을 가진 Hook이 존재하길 바랄 때도 있을 겁니다. 예를 들어, 데이터를 가져온다든가, 사용자가 온라인 상태인지 계속 확인한다든가, 혹은 채팅방에 연결하기 위한 목적들처럼요. React에서 다음과 같은 Hook들을 찾기는 어려울 것입니다. 하지만 애플리케이션의 필요에 알맞은 본인만의 Hook을 만들 수 있습니다.\n\n</Intro>\n\n<YouWillLearn>\n\n- 커스텀 Hook이 무엇이고, 어떻게 본인만의 Hook을 작성하는 지\n- 컴포넌트 간 로직을 재사용하는 방법\n- 나만의 커스텀 Hook 이름 짓기와 구조 잡기\n- 언제 그리고 왜 커스텀 Hook을 추출해야 하는지\n\n</YouWillLearn>\n\n## 커스텀 Hook: 컴포넌트간 로직 공유하기 {/*custom-hooks-sharing-logic-between-components*/}\n\n네트워크에 크게 의존하는 앱 (대부분의 앱이 그렇듯)을 개발 중이라고 생각해 보세요. 사용자가 앱을 사용하는 동안 네트워크가 갑자기 사라진다면, 사용자에게 경고하고 싶을 겁니다. 이런 경우 어떻게 하실 건가요? 컴포넌트에는 다음 두 가지가 필요할 것입니다.\n\n1. 네트워크가 온라인 상태인지 아닌지 추적하는 하나의 state\n2. 전역 [`online (온라인)`](https://developer.mozilla.org/en-US/docs/Web/API/Window/online_event), [`offline (오프라인)`](https://developer.mozilla.org/en-US/docs/Web/API/Window/offline_event) 이벤트를 구독하고, 이에 맞춰 state를 업데이트하는 Effect\n\n두 가지 요소는 컴포넌트가 네트워크 상태와 [동기화](/learn/synchronizing-with-effects) 되도록 합니다. 다음과 같이 구현할 수 있습니다.\n\n<Sandpack>\n\n```js\nimport { useState, useEffect } from 'react';\n\nexport default function StatusBar() {\n  const [isOnline, setIsOnline] = useState(true);\n  useEffect(() => {\n    function handleOnline() {\n      setIsOnline(true);\n    }\n    function handleOffline() {\n      setIsOnline(false);\n    }\n    window.addEventListener('online', handleOnline);\n    window.addEventListener('offline', handleOffline);\n    return () => {\n      window.removeEventListener('online', handleOnline);\n      window.removeEventListener('offline', handleOffline);\n    };\n  }, []);\n\n  return <h1>{isOnline ? '✅ 온라인' : '❌ 연결 안 됨'}</h1>;\n}\n```\n\n</Sandpack>\n\n네트워크를 껐다 켰다 해보세요. 그리고 `StatusBar` 가 어떻게 업데이트되는지 확인해 보세요.\n\n이제 다른 컴포넌트에서 같은 로직을 *또* 사용한다고 상상해 보세요. 네트워크가 꺼졌을 때, \"저장\" 대신 \"재연결 중...\"을 보여주는 비활성화된 저장 버튼을 구현하고 싶다고 가정해 봅시다.\n\n구현하기 위해, 앞서 사용한 `isOnline` state과 Effect를 `SaveButton` 안에 복사 붙여넣기 할 수 있습니다.\n\n<Sandpack>\n\n```js\nimport { useState, useEffect } from 'react';\n\nexport default function SaveButton() {\n  const [isOnline, setIsOnline] = useState(true);\n  useEffect(() => {\n    function handleOnline() {\n      setIsOnline(true);\n    }\n    function handleOffline() {\n      setIsOnline(false);\n    }\n    window.addEventListener('online', handleOnline);\n    window.addEventListener('offline', handleOffline);\n    return () => {\n      window.removeEventListener('online', handleOnline);\n      window.removeEventListener('offline', handleOffline);\n    };\n  }, []);\n\n  function handleSaveClick() {\n    console.log('✅ 진행사항 저장됨');\n  }\n\n  return (\n    <button disabled={!isOnline} onClick={handleSaveClick}>\n      {isOnline ? '진행사항 저장' : '재연결 중...'}\n    </button>\n  );\n}\n```\n\n</Sandpack>\n\n네트워크를 껐을 때, 버튼의 모양이 바뀌는지 확인해 봅시다.\n\n위의 두 컴포넌트는 잘 동작합니다. 하지만 둘 사이의 로직이 중복되는 점은 아쉽습니다. 두 컴포넌트가 다른 *시각적 모양*을 갖고 있다고 해도, 둘 사이의 로직을 재사용하길 원합니다.\n\n### 컴포넌트로부터 커스텀 Hook 추출하기 {/*extracting-your-own-custom-hook-from-a-component*/}\n\n[`useState`](/reference/react/useState) 그리고 [`useEffect`](/reference/react/useEffect)와 비슷한 내장된 `useOnlineStatus` Hook이 있다고 상상해 봅시다. 그럼 두 컴포넌트를 단순화할 수 있고, 둘 간의 중복을 제거할 수 있게 됩니다.\n\n```js {2,7}\nfunction StatusBar() {\n  const isOnline = useOnlineStatus();\n  return <h1>{isOnline ? '✅ 온라인' : '❌ 연결 안 됨'}</h1>;\n}\n\nfunction SaveButton() {\n  const isOnline = useOnlineStatus();\n\n  function handleSaveClick() {\n    console.log('✅ 진행사항 저장됨');\n  }\n\n  return (\n    <button disabled={!isOnline} onClick={handleSaveClick}>\n      {isOnline ? '진행사항 저장' : '재연결 중...'}\n    </button>\n  );\n}\n```\n\n내장된 Hook이 없다고 해도, 스스로 만들어 낼 수 있습니다. `useOnlineStatus` 함수를 정의하고, 앞서 작성한 컴포넌트들의 중복되는 코드를 바꿔보세요.\n\n```js {2-16}\nfunction useOnlineStatus() {\n  const [isOnline, setIsOnline] = useState(true);\n  useEffect(() => {\n    function handleOnline() {\n      setIsOnline(true);\n    }\n    function handleOffline() {\n      setIsOnline(false);\n    }\n    window.addEventListener('online', handleOnline);\n    window.addEventListener('offline', handleOffline);\n    return () => {\n      window.removeEventListener('online', handleOnline);\n      window.removeEventListener('offline', handleOffline);\n    };\n  }, []);\n  return isOnline;\n}\n```\n\n함수의 마지막에 `isOnline`을 반환하면, 컴포넌트가 그 값을 읽을 수 있게 해줍니다.\n\n<Sandpack>\n\n```js\nimport { useOnlineStatus } from './useOnlineStatus.js';\n\nfunction StatusBar() {\n  const isOnline = useOnlineStatus();\n  return <h1>{isOnline ? '✅ 온라인' : '❌ 연결 안 됨'}</h1>;\n}\n\nfunction SaveButton() {\n  const isOnline = useOnlineStatus();\n\n  function handleSaveClick() {\n    console.log('✅ 진행사항 저장됨');\n  }\n\n  return (\n    <button disabled={!isOnline} onClick={handleSaveClick}>\n      {isOnline ? '진행사항 저장' : '재연결 중...'}\n    </button>\n  );\n}\n\nexport default function App() {\n  return (\n    <>\n      <SaveButton />\n      <StatusBar />\n    </>\n  );\n}\n```\n\n```js src/useOnlineStatus.js\nimport { useState, useEffect } from 'react';\n\nexport function useOnlineStatus() {\n  const [isOnline, setIsOnline] = useState(true);\n  useEffect(() => {\n    function handleOnline() {\n      setIsOnline(true);\n    }\n    function handleOffline() {\n      setIsOnline(false);\n    }\n    window.addEventListener('online', handleOnline);\n    window.addEventListener('offline', handleOffline);\n    return () => {\n      window.removeEventListener('online', handleOnline);\n      window.removeEventListener('offline', handleOffline);\n    };\n  }, []);\n  return isOnline;\n}\n```\n\n</Sandpack>\n\n네트워크에 따라 두 컴포넌트가 업데이트되는지 확인해 봅시다.\n\n이제 컴포넌트는 반복되는 로직이 많지 않게 되었습니다. **중요한 건, 두 컴포넌트 내부 코드가 *어떻게 그것을 하는지* (브라우저 이벤트 구독하기) 보다 *그들이 무엇을 하려는지* (온라인 state 사용하기)에 대해 설명하고 있다는 점입니다.**\n\n커스텀 Hook을 만들어 낼 때, 브라우저 API나 외부 시스템과 소통하는 방법과 같은 불필요한 세부 사항을 숨길 수 있습니다. 컴포넌트의 코드는 목적만을 나타낼 뿐 실행 방법에 대해선 나타내지 않습니다.\n\n### Hook의 이름은 항상 `use`로 시작해야 합니다. {/*hook-names-always-start-with-use*/}\n\nReact 애플리케이션은 여러 컴포넌트로 만들어집니다. 컴포넌트들은 내장되거나 직접 작성한 Hook으로 만들어집니다. 종종 다른 사람들에 의해 만들어진 Hook을 사용했을 것입니다. 하지만 때에 따라 본인만의 Hook을 만들어야 할 때도 있습니다.\n\n이때, 다음의 작명 규칙을 준수해야 합니다.\n\n1. **React 컴포넌트의 이름은 항상 대문자로 시작해야 합니다.** (예시 : `StatusBar`, `SaveButton`) 또한 React 컴포넌트는 JSX처럼 어떻게 보이는지 React가 알 수 있는 무언가를 반환해야 합니다.\n2. **Hook의 이름은 `use` 뒤에 대문자로 시작해야 합니다.** (예시 : [`useState`](/reference/react/useState) (내장된 Hook) or `useOnlineStatus` (앞서 작성한 커스텀 Hook)) Hook들은 어떤 값이든 반환할 수 있습니다.\n\n이런 규칙들은 컴포넌트를 볼 때, 어디에 state, Effect 및 다른 React 기능들이 \"숨어\" 있는지 알 수 있게 해줍니다. 예를 들어, 만약 컴포넌트 안에 `getColor()`라는 함수를 보았다면, 해당 함수의 이름이 `use`로 시작하지 않으므로 함수 안에 React state가 있을 수 없다는 것을 확신할 수 있습니다. 반대로 `useOnlineStatus()` 함수의 경우 높은 확률로 내부에 다른 Hook을 사용하고 있을 수 있습니다!\n\n<Note>\n\nlinter가 [React에 맞춰있다면](/learn/editor-setup#linting), 작명 규칙을 지키게합니다. 위의 코드로 다시 올라가 `useOnlineStatus`를 `getOnlineStatus`로 바꿔보세요. linter가 내부에서 `useState`나 `useEffect`를 사용하는 것을 더 이상 허용하지 않을 겁니다. 오로지 Hook과 컴포넌트만 다른 Hook을 사용할 수 있습니다!\n\n</Note>\n\n<DeepDive>\n\n#### 렌더링 중에 호출되는 모든 함수는 use 접두사로 시작해야 하나요? {/*should-all-functions-called-during-rendering-start-with-the-use-prefix*/}\n\n아닙니다. Hook을 *호출*하지 않는 함수는 *Hook일* 필요가 없습니다.\n\n함수가 어떤 Hook도 호출하지 않는다면, `use`를 이름 앞에 작성하는 것을 피하세요. 대신, `use` 없이 일반적인 함수로 작성하세요. 예를 들어 `useSorted`가 Hook을 호출하지 않는다면 `getSorted`로 변경할 수 있습니다.\n\n```js\n// 🔴 안 좋은 예시 : Hook을 사용하고 있지 않는 Hook.\nfunction useSorted(items) {\n  return items.slice().sort();\n}\n\n// ✅ 좋은 예시 : Hook을 사용하지 않는 일반 함수.\nfunction getSorted(items) {\n  return items.slice().sort();\n}\n```\n\n다음의 예시는 조건문 뿐만 아니라 어디든 일반 함수를 사용할 수 있다는 것을 보여줍니다.\n\n```js\nfunction List({ items, shouldSort }) {\n  let displayedItems = items;\n  if (shouldSort) {\n    // ✅ getSorted()가 Hook이 아니기 때문에 조건에 따라 호출할 수 있습니다.\n    displayedItems = getSorted(items);\n  }\n  // ...\n}\n```\n\n적어도 하나의 Hook을 내부에서 사용한다면 반드시 함수 앞에 `use`를 작성해야 합니다. (그리고 이 자체로 Hook이 됩니다.)\n\n```js\n// ✅ 좋은 예시 : Hook을 사용하는 Hook\nfunction useAuth() {\n  return useContext(Auth);\n}\n```\n\n기술적으로 이건 React에 의해 강요되진 않습니다. 원칙적으로 다른 Hook을 사용하지 않는 Hook을 만들 수 있습니다. 이건 가끔 혼란스럽고 제한되기 때문에 해당 방식을 피하는 것이 가장 좋습니다. 하지만, 매우 드물게 이런 방식이 도움이 될 때도 있습니다. 예를 들어 지금 당장은 함수에서 어떤 Hook도 사용하지 않지만, 미래에 Hook을 호출할 계획이 있다면 `use`를 앞에 붙여 이름 짓는 것이 가능합니다.\n\n```js {3-4}\n// ✅ 좋은 예시 : 추후에 다른 Hook을 사용할 가능성이 있는 Hook\nfunction useAuth() {\n  // TODO: 인증이 수행될 때 해당 코드를 useContext(Auth)를 반환하는 코드로 바꾸기\n  return TEST_USER;\n}\n```\n\n그럼, 컴포넌트는 조건에 따라 호출할 수 없게 됩니다. 이건 실제로 Hook을 내부에 추가해 호출할 때 매우 중요합니다. 지금이든 나중이든 Hook을 내부에서 사용할 계획이 없다면, Hook으로 만들지 마세요.\n\n</DeepDive>\n\n### 커스텀 Hook은 state 그 자체를 공유하는게 아닌 state 저장 로직을 공유하도록 합니다. {/*custom-hooks-let-you-share-stateful-logic-not-state-itself*/}\n\n앞선 예시에서, 우리가 네트워크를 껐다 켰을 때 양쪽 컴포넌트가 함께 업데이트되었습니다. 그렇다고 해서 `isOnline` state 변수가 두 컴포넌트 간 공유되었다고 생각하면 안 됩니다. 다음의 코드를 확인해 보세요.\n\n```js {2,7}\nfunction StatusBar() {\n  const isOnline = useOnlineStatus();\n  // ...\n}\n\nfunction SaveButton() {\n  const isOnline = useOnlineStatus();\n  // ...\n}\n```\n\n우리가 중복된 부분을 걷어내기 전에도 동일하게 동작합니다.\n\n```js {2-5,10-13}\nfunction StatusBar() {\n  const [isOnline, setIsOnline] = useState(true);\n  useEffect(() => {\n    // ...\n  }, []);\n  // ...\n}\n\nfunction SaveButton() {\n  const [isOnline, setIsOnline] = useState(true);\n  useEffect(() => {\n    // ...\n  }, []);\n  // ...\n}\n```\n\n완전히 독립적인 두 state 변수와 Effect가 있음을 확인할 수 있습니다. 그들은 우리가 동일한 외부 변수(네트워크의 연결 state)를 동기화했기 때문에 같은 시간에 같은 값을 가지고 있을 뿐입니다.\n\n이걸 더 잘 표현하기 위해 다른 예시가 필요할 겁니다. 다음의 `Form` 컴포넌트를 살펴보세요.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function Form() {\n  const [firstName, setFirstName] = useState('Mary');\n  const [lastName, setLastName] = useState('Poppins');\n\n  function handleFirstNameChange(e) {\n    setFirstName(e.target.value);\n  }\n\n  function handleLastNameChange(e) {\n    setLastName(e.target.value);\n  }\n\n  return (\n    <>\n      <label>\n        First name:\n        <input value={firstName} onChange={handleFirstNameChange} />\n      </label>\n      <label>\n        Last name:\n        <input value={lastName} onChange={handleLastNameChange} />\n      </label>\n      <p><b>Good morning, {firstName} {lastName}.</b></p>\n    </>\n  );\n}\n```\n\n```css\nlabel { display: block; }\ninput { margin-left: 10px; }\n```\n\n</Sandpack>\n\n각각의 폼 입력에 반복되는 로직이 있습니다.\n\n1. state가 존재합니다. (`firstName`와 `lastName`)\n2. 변화를 다루는 함수가 존재합니다. (`handleFirstNameChange`와 `handleLastNameChange`).\n3. 해당 입력에 대한 `value`와 `onChange`의 속성을 지정하는 JSX가 존재합니다.\n\n`useFormInput` 커스텀 Hook을 통해 반복되는 로직을 추출할 수 있습니다.\n\n<Sandpack>\n\n```js\nimport { useFormInput } from './useFormInput.js';\n\nexport default function Form() {\n  const firstNameProps = useFormInput('Mary');\n  const lastNameProps = useFormInput('Poppins');\n\n  return (\n    <>\n      <label>\n        First name:\n        <input {...firstNameProps} />\n      </label>\n      <label>\n        Last name:\n        <input {...lastNameProps} />\n      </label>\n      <p><b>Good morning, {firstNameProps.value} {lastNameProps.value}.</b></p>\n    </>\n  );\n}\n```\n\n```js src/useFormInput.js active\nimport { useState } from 'react';\n\nexport function useFormInput(initialValue) {\n  const [value, setValue] = useState(initialValue);\n\n  function handleChange(e) {\n    setValue(e.target.value);\n  }\n\n  const inputProps = {\n    value: value,\n    onChange: handleChange\n  };\n\n  return inputProps;\n}\n```\n\n```css\nlabel { display: block; }\ninput { margin-left: 10px; }\n```\n\n</Sandpack>\n\n`value`라고 불리는 state 변수가 *한 번만* 정의된다는 것을 기억하세요.\n\n이와 달리, `Form` 컴포넌트는 `useFormInput`을 **두 번** 호출합니다.\n\n```js\nfunction Form() {\n  const firstNameProps = useFormInput('Mary');\n  const lastNameProps = useFormInput('Poppins');\n  // ...\n```\n\n위의 예시는 왜 두 개의 다른 state 변수를 정의하는 식으로 동작하는지 보여줍니다.\n\n**커스텀 Hook은 우리가 *state 그 자체*가 아닌 *state 저장 로직*을 공유하도록 해줍니다. 같은 Hook을 호출하더라도 각각의 Hook 호출은 완전히 독립되어 있습니다.** 이것이 위의 두 코드가 완전히 같은 이유입니다. 원한다면 위로 돌아가 비교해 보세요. 커스텀 Hook을 추출하기 전과 후가 동일합니다.\n\n대신 여러 컴포넌트 간 state 자체를 공유할 필요가 있다면, [state를 위로 올려 전달하세요](/learn/sharing-state-between-components).\n\n## Hook 사이에 상호작용하는 값 전달하기 {/*passing-reactive-values-between-hooks*/}\n\n커스텀 Hook 안의 코드는 컴포넌트가 재렌더링될 때마다 다시 돌아갈 겁니다. 이게 바로 커스텀 Hook이 (컴포넌트처럼) [순수해야하는 이유](/learn/keeping-components-pure) 입니다. 커스텀 Hook을 컴포넌트 본체의 한 부분이라고 생각하세요!\n\n커스텀 Hook이 컴포넌트와 함께 재렌더링된다면, 항상 가장 최신의 props와 state를 전달받을 것입니다. 이게 무슨 말인지 살펴보기 위해 아래의 채팅방 예시를 확인해 보세요. 서버 URL이나 채팅방을 바꾼다고 생각해봅시다.\n\n<Sandpack>\n\n```js src/App.js\nimport { useState } from 'react';\nimport ChatRoom from './ChatRoom.js';\n\nexport default function App() {\n  const [roomId, setRoomId] = useState('general');\n  return (\n    <>\n      <label>\n        Choose the chat room:{' '}\n        <select\n          value={roomId}\n          onChange={e => setRoomId(e.target.value)}\n        >\n          <option value=\"general\">general</option>\n          <option value=\"travel\">travel</option>\n          <option value=\"music\">music</option>\n        </select>\n      </label>\n      <hr />\n      <ChatRoom\n        roomId={roomId}\n      />\n    </>\n  );\n}\n```\n\n```js src/ChatRoom.js active\nimport { useState, useEffect } from 'react';\nimport { createConnection } from './chat.js';\nimport { showNotification } from './notifications.js';\n\nexport default function ChatRoom({ roomId }) {\n  const [serverUrl, setServerUrl] = useState('https://localhost:1234');\n\n  useEffect(() => {\n    const options = {\n      serverUrl: serverUrl,\n      roomId: roomId\n    };\n    const connection = createConnection(options);\n    connection.on('message', (msg) => {\n      showNotification('New message: ' + msg);\n    });\n    connection.connect();\n    return () => connection.disconnect();\n  }, [roomId, serverUrl]);\n\n  return (\n    <>\n      <label>\n        Server URL:\n        <input value={serverUrl} onChange={e => setServerUrl(e.target.value)} />\n      </label>\n      <h1>Welcome to the {roomId} room!</h1>\n    </>\n  );\n}\n```\n\n```js src/chat.js\nexport function createConnection({ serverUrl, roomId }) {\n  // 실제 구현은 실제로 서버에 연결됩니다.\n  if (typeof serverUrl !== 'string') {\n    throw Error('Expected serverUrl to be a string. Received: ' + serverUrl);\n  }\n  if (typeof roomId !== 'string') {\n    throw Error('Expected roomId to be a string. Received: ' + roomId);\n  }\n  let intervalId;\n  let messageCallback;\n  return {\n    connect() {\n      console.log('✅ Connecting to \"' + roomId + '\" room at ' + serverUrl + '...');\n      clearInterval(intervalId);\n      intervalId = setInterval(() => {\n        if (messageCallback) {\n          if (Math.random() > 0.5) {\n            messageCallback('hey')\n          } else {\n            messageCallback('lol');\n          }\n        }\n      }, 3000);\n    },\n    disconnect() {\n      clearInterval(intervalId);\n      messageCallback = null;\n      console.log('❌ Disconnected from \"' + roomId + '\" room at ' + serverUrl + '');\n    },\n    on(event, callback) {\n      if (messageCallback) {\n        throw Error('Cannot add the handler twice.');\n      }\n      if (event !== 'message') {\n        throw Error('Only \"message\" event is supported.');\n      }\n      messageCallback = callback;\n    },\n  };\n}\n```\n\n```js src/notifications.js\nimport Toastify from 'toastify-js';\nimport 'toastify-js/src/toastify.css';\n\nexport function showNotification(message, theme = 'dark') {\n  Toastify({\n    text: message,\n    duration: 2000,\n    gravity: 'top',\n    position: 'right',\n    style: {\n      background: theme === 'dark' ? 'black' : 'white',\n      color: theme === 'dark' ? 'white' : 'black',\n    },\n  }).showToast();\n}\n```\n\n```json package.json hidden\n{\n  \"dependencies\": {\n    \"react\": \"latest\",\n    \"react-dom\": \"latest\",\n    \"react-scripts\": \"latest\",\n    \"toastify-js\": \"1.12.0\"\n  },\n  \"scripts\": {\n    \"start\": \"react-scripts start\",\n    \"build\": \"react-scripts build\",\n    \"test\": \"react-scripts test --env=jsdom\",\n    \"eject\": \"react-scripts eject\"\n  }\n}\n```\n\n```css\ninput { display: block; margin-bottom: 20px; }\nbutton { margin-left: 10px; }\n```\n\n</Sandpack>\n\n`serverUrl`나 `roomId`를 변경할 때, Effect는 [변화에 \"반응\"](/learn/lifecycle-of-reactive-effects#effects-react-to-reactive-values)하며 재동기화합니다. Effect의 의존성이 변경될 때마다 채팅방을 재연결하는 콘솔 메시지를 보낼 수 있습니다.\n\n이제 Effect 코드를 커스텀 Hook 안에 넣어봅시다.\n\n```js {2-13}\nexport function useChatRoom({ serverUrl, roomId }) {\n  useEffect(() => {\n    const options = {\n      serverUrl: serverUrl,\n      roomId: roomId\n    };\n    const connection = createConnection(options);\n    connection.connect();\n    connection.on('message', (msg) => {\n      showNotification('New message: ' + msg);\n    });\n    return () => connection.disconnect();\n  }, [roomId, serverUrl]);\n}\n```\n\n`ChatRoom` 컴포넌트가 내부 동작이 어떻게 동작하는지 걱정할 필요 없이 커스텀 Hook을 호출할 수 있게 해줍니다.\n\n```js {4-7}\nexport default function ChatRoom({ roomId }) {\n  const [serverUrl, setServerUrl] = useState('https://localhost:1234');\n\n  useChatRoom({\n    roomId: roomId,\n    serverUrl: serverUrl\n  });\n\n  return (\n    <>\n      <label>\n        Server URL:\n        <input value={serverUrl} onChange={e => setServerUrl(e.target.value)} />\n      </label>\n      <h1>Welcome to the {roomId} room!</h1>\n    </>\n  );\n}\n```\n\n매우 간단해졌습니다! (그런데도 똑같이 동작합니다)\n\n로직이 props와 state 변화에 따라 *여전히 응답*하는지 확인해 봅시다. 서버 URL이나 방을 변경해 보세요.\n\n<Sandpack>\n\n```js src/App.js\nimport { useState } from 'react';\nimport ChatRoom from './ChatRoom.js';\n\nexport default function App() {\n  const [roomId, setRoomId] = useState('general');\n  return (\n    <>\n      <label>\n        Choose the chat room:{' '}\n        <select\n          value={roomId}\n          onChange={e => setRoomId(e.target.value)}\n        >\n          <option value=\"general\">general</option>\n          <option value=\"travel\">travel</option>\n          <option value=\"music\">music</option>\n        </select>\n      </label>\n      <hr />\n      <ChatRoom\n        roomId={roomId}\n      />\n    </>\n  );\n}\n```\n\n```js src/ChatRoom.js active\nimport { useState } from 'react';\nimport { useChatRoom } from './useChatRoom.js';\n\nexport default function ChatRoom({ roomId }) {\n  const [serverUrl, setServerUrl] = useState('https://localhost:1234');\n\n  useChatRoom({\n    roomId: roomId,\n    serverUrl: serverUrl\n  });\n\n  return (\n    <>\n      <label>\n        Server URL:\n        <input value={serverUrl} onChange={e => setServerUrl(e.target.value)} />\n      </label>\n      <h1>Welcome to the {roomId} room!</h1>\n    </>\n  );\n}\n```\n\n```js src/useChatRoom.js\nimport { useEffect } from 'react';\nimport { createConnection } from './chat.js';\nimport { showNotification } from './notifications.js';\n\nexport function useChatRoom({ serverUrl, roomId }) {\n  useEffect(() => {\n    const options = {\n      serverUrl: serverUrl,\n      roomId: roomId\n    };\n    const connection = createConnection(options);\n    connection.connect();\n    connection.on('message', (msg) => {\n      showNotification('New message: ' + msg);\n    });\n    return () => connection.disconnect();\n  }, [roomId, serverUrl]);\n}\n```\n\n```js src/chat.js\nexport function createConnection({ serverUrl, roomId }) {\n  // 실제 구현은 실제로 서버에 연결됩니다.\n  if (typeof serverUrl !== 'string') {\n    throw Error('Expected serverUrl to be a string. Received: ' + serverUrl);\n  }\n  if (typeof roomId !== 'string') {\n    throw Error('Expected roomId to be a string. Received: ' + roomId);\n  }\n  let intervalId;\n  let messageCallback;\n  return {\n    connect() {\n      console.log('✅ Connecting to \"' + roomId + '\" room at ' + serverUrl + '...');\n      clearInterval(intervalId);\n      intervalId = setInterval(() => {\n        if (messageCallback) {\n          if (Math.random() > 0.5) {\n            messageCallback('hey')\n          } else {\n            messageCallback('lol');\n          }\n        }\n      }, 3000);\n    },\n    disconnect() {\n      clearInterval(intervalId);\n      messageCallback = null;\n      console.log('❌ Disconnected from \"' + roomId + '\" room at ' + serverUrl + '');\n    },\n    on(event, callback) {\n      if (messageCallback) {\n        throw Error('Cannot add the handler twice.');\n      }\n      if (event !== 'message') {\n        throw Error('Only \"message\" event is supported.');\n      }\n      messageCallback = callback;\n    },\n  };\n}\n```\n\n```js src/notifications.js\nimport Toastify from 'toastify-js';\nimport 'toastify-js/src/toastify.css';\n\nexport function showNotification(message, theme = 'dark') {\n  Toastify({\n    text: message,\n    duration: 2000,\n    gravity: 'top',\n    position: 'right',\n    style: {\n      background: theme === 'dark' ? 'black' : 'white',\n      color: theme === 'dark' ? 'white' : 'black',\n    },\n  }).showToast();\n}\n```\n\n```json package.json hidden\n{\n  \"dependencies\": {\n    \"react\": \"latest\",\n    \"react-dom\": \"latest\",\n    \"react-scripts\": \"latest\",\n    \"toastify-js\": \"1.12.0\"\n  },\n  \"scripts\": {\n    \"start\": \"react-scripts start\",\n    \"build\": \"react-scripts build\",\n    \"test\": \"react-scripts test --env=jsdom\",\n    \"eject\": \"react-scripts eject\"\n  }\n}\n```\n\n```css\ninput { display: block; margin-bottom: 20px; }\nbutton { margin-left: 10px; }\n```\n\n</Sandpack>\n\n어떻게 Hook의 반환 값을 가져올 수 있는지 확인해 보세요.\n\n```js {2}\nexport default function ChatRoom({ roomId }) {\n  const [serverUrl, setServerUrl] = useState('https://localhost:1234');\n\n  useChatRoom({\n    roomId: roomId,\n    serverUrl: serverUrl\n  });\n  // ...\n```\n\n그리고 다른 Hook에 입력으로 전달하세요.\n\n```js {6}\nexport default function ChatRoom({ roomId }) {\n  const [serverUrl, setServerUrl] = useState('https://localhost:1234');\n\n  useChatRoom({\n    roomId: roomId,\n    serverUrl: serverUrl\n  });\n  // ...\n```\n\n매번 `ChatRoom`가 재렌더링될 때마다, Hook에 최신 `roomId`와 `serverUrl` 값을 넘겨줍니다. 이것이 바로 Effect가 다시 렌더링된 후 값이 다를 때마다 채팅에 다시 연결되는 이유입니다. (만약 오디오 또는 비디오 처리 소프트웨어를 작업해 본 적이 있다면, 이처럼 Hook을 연결하는 것이 시각적 혹은 청각적 효과를 연결하는 것을 떠오르게 할 겁니다. 이게 바로 `useState`의 결과를 `useChatRoom`의 입력으로 \"넣어주는 것\"과 같습니다.)\n\n### 커스텀 Hook에 이벤트 핸들러 넘겨주기 {/*passing-event-handlers-to-custom-hooks*/}\n\n더 많은 컴포넌트에서 `useChatRoom`을 사용하기 시작하면, 컴포넌트가 그 동작을 커스텀할 수 있도록 하고 싶을 수 있습니다. 예를 들어, 현재 메시지가 도착했을 때 무엇을 할지에 대한 로직은 Hook 내부에 하드 코딩되어 있습니다.\n\n```js {9-11}\nexport function useChatRoom({ serverUrl, roomId }) {\n  useEffect(() => {\n    const options = {\n      serverUrl: serverUrl,\n      roomId: roomId\n    };\n    const connection = createConnection(options);\n    connection.connect();\n    connection.on('message', (msg) => {\n      showNotification('New message: ' + msg);\n    });\n    return () => connection.disconnect();\n  }, [roomId, serverUrl]);\n}\n```\n\n이 로직을 컴포넌트에 되돌려 놓고 싶다고 해봅시다.\n\n```js {7-9}\nexport default function ChatRoom({ roomId }) {\n  const [serverUrl, setServerUrl] = useState('https://localhost:1234');\n\n  useChatRoom({\n    roomId: roomId,\n    serverUrl: serverUrl,\n    onReceiveMessage(msg) {\n      showNotification('New message: ' + msg);\n    }\n  });\n  // ...\n```\n\n이게 동작하게 하기 위해, 커스텀 Hook을 정의된 옵션 중 하나인 `onReceiveMessage`를 갖도록 해봅시다.\n\n```js {1,10,13}\nexport function useChatRoom({ serverUrl, roomId, onReceiveMessage }) {\n  useEffect(() => {\n    const options = {\n      serverUrl: serverUrl,\n      roomId: roomId\n    };\n    const connection = createConnection(options);\n    connection.connect();\n    connection.on('message', (msg) => {\n      onReceiveMessage(msg);\n    });\n    return () => connection.disconnect();\n  }, [roomId, serverUrl, onReceiveMessage]); // ✅ 모든 의존성이 정의됨.\n}\n```\n\n이대로도 동작하지만, 커스텀 Hook이 이벤트 핸들러를 허용할 때 하나 더 개선할 수 있는 부분이 있습니다.\n\n컴포넌트가 재렌더링될 때마다 채팅방을 재연결하는 원인이 되기 때문에, 의존성에 `onReceiveMessage`를 추가하는 것은 이상적이지 않습니다. [이 이벤트 핸들러를 의존성에서 제거하기 위해 Effect 이벤트로 감싸주세요.](/learn/removing-effect-dependencies#wrapping-an-event-handler-from-the-props)\n\n```js {1,4,5,15,18}\nimport { useEffect, useEffectEvent } from 'react';\n// ...\n\nexport function useChatRoom({ serverUrl, roomId, onReceiveMessage }) {\n  const onMessage = useEffectEvent(onReceiveMessage);\n\n  useEffect(() => {\n    const options = {\n      serverUrl: serverUrl,\n      roomId: roomId\n    };\n    const connection = createConnection(options);\n    connection.connect();\n    connection.on('message', (msg) => {\n      onMessage(msg);\n    });\n    return () => connection.disconnect();\n  }, [roomId, serverUrl]); // ✅ 모든 의존성이 정의됨.\n}\n```\n\n이제 `ChatRoom`가 재렌더링될 때마다 채팅방이 재연결되지 않습니다. 여기 커스텀 Hook에 이벤트 핸들러를 넘겨주는 직접 다뤄볼 수 있는 제대로 동작하는 예시가 있습니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { useState } from 'react';\nimport ChatRoom from './ChatRoom.js';\n\nexport default function App() {\n  const [roomId, setRoomId] = useState('general');\n  return (\n    <>\n      <label>\n        Choose the chat room:{' '}\n        <select\n          value={roomId}\n          onChange={e => setRoomId(e.target.value)}\n        >\n          <option value=\"general\">general</option>\n          <option value=\"travel\">travel</option>\n          <option value=\"music\">music</option>\n        </select>\n      </label>\n      <hr />\n      <ChatRoom\n        roomId={roomId}\n      />\n    </>\n  );\n}\n```\n\n```js src/ChatRoom.js active\nimport { useState } from 'react';\nimport { useChatRoom } from './useChatRoom.js';\nimport { showNotification } from './notifications.js';\n\nexport default function ChatRoom({ roomId }) {\n  const [serverUrl, setServerUrl] = useState('https://localhost:1234');\n\n  useChatRoom({\n    roomId: roomId,\n    serverUrl: serverUrl,\n    onReceiveMessage(msg) {\n      showNotification('New message: ' + msg);\n    }\n  });\n\n  return (\n    <>\n      <label>\n        Server URL:\n        <input value={serverUrl} onChange={e => setServerUrl(e.target.value)} />\n      </label>\n      <h1>Welcome to the {roomId} room!</h1>\n    </>\n  );\n}\n```\n\n```js src/useChatRoom.js\nimport { useEffect } from 'react';\nimport { useEffectEvent } from 'react';\nimport { createConnection } from './chat.js';\n\nexport function useChatRoom({ serverUrl, roomId, onReceiveMessage }) {\n  const onMessage = useEffectEvent(onReceiveMessage);\n\n  useEffect(() => {\n    const options = {\n      serverUrl: serverUrl,\n      roomId: roomId\n    };\n    const connection = createConnection(options);\n    connection.connect();\n    connection.on('message', (msg) => {\n      onMessage(msg);\n    });\n    return () => connection.disconnect();\n  }, [roomId, serverUrl]);\n}\n```\n\n```js src/chat.js\nexport function createConnection({ serverUrl, roomId }) {\n  // 실제 구현에서는 서버에 연결됩니다\n  if (typeof serverUrl !== 'string') {\n    throw Error('Expected serverUrl to be a string. Received: ' + serverUrl);\n  }\n  if (typeof roomId !== 'string') {\n    throw Error('Expected roomId to be a string. Received: ' + roomId);\n  }\n  let intervalId;\n  let messageCallback;\n  return {\n    connect() {\n      console.log('✅ Connecting to \"' + roomId + '\" room at ' + serverUrl + '...');\n      clearInterval(intervalId);\n      intervalId = setInterval(() => {\n        if (messageCallback) {\n          if (Math.random() > 0.5) {\n            messageCallback('hey')\n          } else {\n            messageCallback('lol');\n          }\n        }\n      }, 3000);\n    },\n    disconnect() {\n      clearInterval(intervalId);\n      messageCallback = null;\n      console.log('❌ Disconnected from \"' + roomId + '\" room at ' + serverUrl + '');\n    },\n    on(event, callback) {\n      if (messageCallback) {\n        throw Error('Cannot add the handler twice.');\n      }\n      if (event !== 'message') {\n        throw Error('Only \"message\" event is supported.');\n      }\n      messageCallback = callback;\n    },\n  };\n}\n```\n\n```js src/notifications.js\nimport Toastify from 'toastify-js';\nimport 'toastify-js/src/toastify.css';\n\nexport function showNotification(message, theme = 'dark') {\n  Toastify({\n    text: message,\n    duration: 2000,\n    gravity: 'top',\n    position: 'right',\n    style: {\n      background: theme === 'dark' ? 'black' : 'white',\n      color: theme === 'dark' ? 'white' : 'black',\n    },\n  }).showToast();\n}\n```\n\n```json package.json hidden\n{\n  \"dependencies\": {\n    \"react\": \"latest\",\n    \"react-dom\": \"latest\",\n    \"react-scripts\": \"latest\",\n    \"toastify-js\": \"1.12.0\"\n  },\n  \"scripts\": {\n    \"start\": \"react-scripts start\",\n    \"build\": \"react-scripts build\",\n    \"test\": \"react-scripts test --env=jsdom\",\n    \"eject\": \"react-scripts eject\"\n  }\n}\n```\n\n```css\ninput { display: block; margin-bottom: 20px; }\nbutton { margin-left: 10px; }\n```\n\n</Sandpack>\n\n이제 `useChatRoom`을 사용하기 위해 `useChatRoom`이 *어떻게* 동작하는지 알 필요가 없습니다. 다른 컴포넌트에 추가하거나, 다른 옵션을 전달해도 똑같이 동작할 겁니다. 이게 바로 커스텀 Hook의 힘입니다.\n\n## 언제 커스텀 Hook을 사용해야 하는지 {/*when-to-use-custom-hooks*/}\n\n모든 자잘한 중복되는 코드들까지 커스텀 Hook으로 분리할 필요가 없습니다. 어떤 중복된 코드는 괜찮습니다. 예를 들어, 앞선 예시처럼 하나의 `useState`를 감싸기 위한 `useFormInput`을 분리하는 것은 불필요합니다.\n\n하지만 Effect를 사용하든 사용하지 않든, 커스텀 Hook 안에 그것을 감싸는 게 좋은지 아닌지 고려하세요. [Effect를 자주 쓸 필요가 없을지 모릅니다.](/learn/you-might-not-need-an-effect) 만약 Effect를 사용한다면, 그건 외부 시스템과 동기화한다든가 React가 내장하지 않은 API를 위해 무언가를 하는 등 \"React에서 벗어나기\" 위함일 겁니다. 커스텀 Hook으로 감싸는 것은 목적을 정확하게 전달하고 어떻게 데이터가 그것을 통해 흐르는지 알 수 있게 해줍니다.\n\n예를 들어 두 가지 목록을 보여주는 `ShippingForm` 컴포넌트를 살펴봅시다. 하나는 도시의 목록을 보여주고, 다른 하나는 선택된 도시의 구역 목록을 보여줍니다. 아마 코드를 다음과 같이 작성하기 시작할 겁니다.\n\n```js {3-16,20-35}\nfunction ShippingForm({ country }) {\n  const [cities, setCities] = useState(null);\n  // 이 Effect는 나라별 도시를 불러옵니다.\n  useEffect(() => {\n    let ignore = false;\n    fetch(`/api/cities?country=${country}`)\n      .then(response => response.json())\n      .then(json => {\n        if (!ignore) {\n          setCities(json);\n        }\n      });\n    return () => {\n      ignore = true;\n    };\n  }, [country]);\n\n  const [city, setCity] = useState(null);\n  const [areas, setAreas] = useState(null);\n  // 이 Effect 선택된 도시의 구역을 불러옵니다.\n  useEffect(() => {\n    if (city) {\n      let ignore = false;\n      fetch(`/api/areas?city=${city}`)\n        .then(response => response.json())\n        .then(json => {\n          if (!ignore) {\n            setAreas(json);\n          }\n        });\n      return () => {\n        ignore = true;\n      };\n    }\n  }, [city]);\n\n  // ...\n```\n\n이 코드들이 반복됨에도 불구하고, [Effect들을 따로 분리하는 것이 옳습니다.](/learn/removing-effect-dependencies#is-your-effect-doing-several-unrelated-things) 그들은 다른 두 가지(도시, 구역)를 동기화합니다. 따라서 하나의 Effect로 통합시킬 필요가 없습니다. 대신 `ShippingForm` 컴포넌트를 `useData`라는 커스텀 Hook을 통해 공통된 로직을 추출할 수 있습니다.\n\n```js {2-18}\nfunction useData(url) {\n  const [data, setData] = useState(null);\n  useEffect(() => {\n    if (url) {\n      let ignore = false;\n      fetch(url)\n        .then(response => response.json())\n        .then(json => {\n          if (!ignore) {\n            setData(json);\n          }\n        });\n      return () => {\n        ignore = true;\n      };\n    }\n  }, [url]);\n  return data;\n}\n```\n\n이제 `ShippingForm` 컴포넌트 내부의 Effect들을 `useData`로 교체할 수 있습니다.\n\n```js {2,4}\nfunction ShippingForm({ country }) {\n  const cities = useData(`/api/cities?country=${country}`);\n  const [city, setCity] = useState(null);\n  const areas = useData(city ? `/api/areas?city=${city}` : null);\n  // ...\n```\n\n커스텀 Hook을 추출하는 것은 데이터의 흐름을 명확하게 해줍니다. `url`을 입력하고 `data`를 받습니다. `useData`안의 Effect를 \"숨김으로써\" 다른 사람이 `ShippingForm` 컴포넌트에 [불필요한 의존성](/learn/removing-effect-dependencies)을 추가하는 것을 막을 수 있습니다. 시간이 지나면 앱의 대부분 Effect들은 커스텀 Hook 안에 있을 겁니다.\n\n<DeepDive>\n\n#### 커스텀 Hook이 구체적인 고급 사용 사례에 집중하도록 하기 {/*keep-your-custom-hooks-focused-on-concrete-high-level-use-cases*/}\n\n커스텀 Hook의 이름을 고르는 것부터 시작해 봅시다. 만약 명확한 이름을 고르기 위해 고군분투한다면, 그건 아마 사용하는 Effect가 컴포넌트 로직의 일부분에 너무 결합하여 있다는 의미일 겁니다. 그리고 아직 분리될 준비가 안 됐다는 뜻입니다.\n\n이상적으로 커스텀 Hook의 이름은 코드를 자주 작성하는 사람이 아니더라도 커스텀 Hook이 무슨 일을 하고, 무엇을 props로 받고, 무엇을 반환하는지 알 수 있도록 아주 명확해야 합니다.\n\n* ✅ `useData(url)`\n* ✅ `useImpressionLog(eventName, extraData)`\n* ✅ `useChatRoom(options)`\n\n외부 시스템과 동기화할 때, 커스텀 Hook의 이름은 좀 더 기술적이고 해당 시스템을 특정하는 용어를 사용하는 것이 좋습니다. 해당 시스템에 친숙한 사람에게도 명확한 이름이라면 좋습니다.\n\n* ✅ `useMediaQuery(query)`\n* ✅ `useSocket(url)`\n* ✅ `useIntersectionObserver(ref, options)`\n\n**커스텀 Hook이 구체적인 고급 사용 사례에 집중할 수 있도록 하세요.** `useEffect` API 그 자체를 위한 대책이나 편리하게 감싸는 용도로 동작하는 커스텀 \"생명 주기\" Hook을 생성하거나 사용하는 것을 피하세요.\n\n* 🔴 `useMount(fn)`\n* 🔴 `useEffectOnce(fn)`\n* 🔴 `useUpdateEffect(fn)`\n\n예를 들어, 이 `useMount` Hook은 코드가 \"마운트 시\"에만 동작하는 것을 확인하기 위해 만들어졌습니다.\n\n```js {4-5,14-15}\nfunction ChatRoom({ roomId }) {\n  const [serverUrl, setServerUrl] = useState('https://localhost:1234');\n\n  // 🔴 안 좋은 예 : 커스텀 \"생명 주기\" Hook을 사용\n  useMount(() => {\n    const connection = createConnection({ roomId, serverUrl });\n    connection.connect();\n\n    post('/analytics/event', { eventName: 'visit_chat' });\n  });\n  // ...\n}\n\n// 🔴 안 좋은 예 : 커스텀 \"생명 주기\" Hook을 생성\nfunction useMount(fn) {\n  useEffect(() => {\n    fn();\n  }, []); // 🔴 React Hook useEffect은 'fn'의 의존성을 갖고 있지 않음.\n}\n```\n\n**`useMount`과 같은 커스텀 \"생명 주기\" Hook은 전형적인 React와 맞지 않습니다.** 예를 들어 이 코드 예시는 문제가 있지만(`roomId`나 `serverUrl`의 변화에 반응하지 않음.), 린터는 오직 직접적인 `useEffect` 호출만 체크하기 때문에 경고하지 않습니다. 린터는 Hook에 대해 모르고 있습니다.\n\nEffect를 작성할 때, React API를 직접적으로 사용하세요.\n\n```js\nfunction ChatRoom({ roomId }) {\n  const [serverUrl, setServerUrl] = useState('https://localhost:1234');\n\n  // ✅ 좋은 예시 : 두 Effect는 목적에 따라 나뉘어 있습니다.\n\n  useEffect(() => {\n    const connection = createConnection({ serverUrl, roomId });\n    connection.connect();\n    return () => connection.disconnect();\n  }, [serverUrl, roomId]);\n\n  useEffect(() => {\n    post('/analytics/event', { eventName: 'visit_chat', roomId });\n  }, [roomId]);\n\n  // ...\n}\n```\n\n그렇게 되면 (그럴 필요는 없지만) 커스텀 Hook을 서로 다른 고급 사용 예시에 따라 분리할 수 있습니다.\n\n```js\nfunction ChatRoom({ roomId }) {\n  const [serverUrl, setServerUrl] = useState('https://localhost:1234');\n\n  // ✅ 좋은 예시: 목적에 따라 이름이 지어진 커스텀 Hook\n  useChatRoom({ serverUrl, roomId });\n  useImpressionLog('visit_chat', { roomId });\n  // ...\n}\n```\n\n**좋은 커스텀 Hook은 호출 코드가 하는 일을 제한하면서 좀 더 선언적으로 만들 수 있습니다.** 예를 들어, `useChatRoom(options)`은 오직 채팅방과 연결할 수 있지만, `useImpressionLog(eventName, extraData)`은 애널리틱스에만 노출된 기록(Impression log)을 보낼 수 있습니다. 커스텀 Hook API가 사용 사례를 제한하지 않고 너무 추상적이라면, 장기적으로는 그것이 해결할 수 있는 것보다 더 많은 문제를 만들 가능성이 높습니다.\n\n</DeepDive>\n\n### 커스텀 Hook은 더 나은 패턴으로 변경할 수 있도록 도와줍니다. {/*custom-hooks-help-you-migrate-to-better-patterns*/}\n\nEffect는 [탈출구](/learn/escape-hatches) 입니다. \"React에서 벗어나\"는 것이 필요할 때나 사용 시에 괜찮은 내장된 해결 방법이 없는 경우, 사용합니다. React 팀의 목표는 더 구체적인 문제에 더 구체적인 해결 방법을 제공해 앱에 있는 Effect의 숫자를 점차 최소한으로 줄이는 것입니다. 커스텀 Hook으로 Effect를 감싸는 것은 이런 해결 방법들이 가능해질 때 코드를 쉽게 업그레이드할 수 있게 해줍니다.\n\n예시로 돌아가 봅시다.\n\n<Sandpack>\n\n```js\nimport { useOnlineStatus } from './useOnlineStatus.js';\n\nfunction StatusBar() {\n  const isOnline = useOnlineStatus();\n  return <h1>{isOnline ? '✅ Online' : '❌ Disconnected'}</h1>;\n}\n\nfunction SaveButton() {\n  const isOnline = useOnlineStatus();\n\n  function handleSaveClick() {\n    console.log('✅ Progress saved');\n  }\n\n  return (\n    <button disabled={!isOnline} onClick={handleSaveClick}>\n      {isOnline ? 'Save progress' : 'Reconnecting...'}\n    </button>\n  );\n}\n\nexport default function App() {\n  return (\n    <>\n      <SaveButton />\n      <StatusBar />\n    </>\n  );\n}\n```\n\n```js src/useOnlineStatus.js active\nimport { useState, useEffect } from 'react';\n\nexport function useOnlineStatus() {\n  const [isOnline, setIsOnline] = useState(true);\n  useEffect(() => {\n    function handleOnline() {\n      setIsOnline(true);\n    }\n    function handleOffline() {\n      setIsOnline(false);\n    }\n    window.addEventListener('online', handleOnline);\n    window.addEventListener('offline', handleOffline);\n    return () => {\n      window.removeEventListener('online', handleOnline);\n      window.removeEventListener('offline', handleOffline);\n    };\n  }, []);\n  return isOnline;\n}\n```\n\n</Sandpack>\n\n위의 예시에서 `useOnlineStatus`는 한 쌍의 [`useState`](/reference/react/useState)와 [`useEffect`](/reference/react/useEffect)와 함께 실행됩니다. 하지만 이건 가장 좋은 해결 방법은 아닙니다. 이 해결 방법이 고려하지 못한 수많은 예외 상황이 존재합니다. 예를 들어, 이건 컴포넌트가 마운트됐을 때, `isOnline`이 이미 `true`라고 가정합니다. 하지만 이것은 네트워크가 이미 꺼졌을 때 틀린 가정이 됩니다. 이런 상황을 확인하기 위해 브라우저 [`navigator.onLine`](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/onLine) API를 사용할 수도 있습니다. 하지만 이걸 직접적으로 사용하게 되면 초기 HTML을 생성하기 위한 서버에선 동작하지 않습니다. 짧게 말하면 코드는 보완되어야 합니다.\n\nReact는 이런 모든 문제를 신경 써주는 [`useSyncExternalStore`](/reference/react/useSyncExternalStore)라고 불리는 섬세한 API를 포함합니다. 여기 새 API의 장점을 가지고 다시 쓰인 `useOnlineStatus`이 있습니다.\n\n<Sandpack>\n\n```js\nimport { useOnlineStatus } from './useOnlineStatus.js';\n\nfunction StatusBar() {\n  const isOnline = useOnlineStatus();\n  return <h1>{isOnline ? '✅ Online' : '❌ Disconnected'}</h1>;\n}\n\nfunction SaveButton() {\n  const isOnline = useOnlineStatus();\n\n  function handleSaveClick() {\n    console.log('✅ Progress saved');\n  }\n\n  return (\n    <button disabled={!isOnline} onClick={handleSaveClick}>\n      {isOnline ? 'Save progress' : 'Reconnecting...'}\n    </button>\n  );\n}\n\nexport default function App() {\n  return (\n    <>\n      <SaveButton />\n      <StatusBar />\n    </>\n  );\n}\n```\n\n```js src/useOnlineStatus.js active\nimport { useSyncExternalStore } from 'react';\n\nfunction subscribe(callback) {\n  window.addEventListener('online', callback);\n  window.addEventListener('offline', callback);\n  return () => {\n    window.removeEventListener('online', callback);\n    window.removeEventListener('offline', callback);\n  };\n}\n\nexport function useOnlineStatus() {\n  return useSyncExternalStore(\n    subscribe,\n    () => navigator.onLine, // 클라이언트의 값을 받아오는 방법\n    () => true // 서버의 값을 받아오는 방법\n  );\n}\n\n```\n\n</Sandpack>\n\n어떻게 이 변경을 하기 위해 **다른 컴포넌트들을 변경하지 않아**도 되는지 알아봅시다.\n\n```js {2,7}\nfunction StatusBar() {\n  const isOnline = useOnlineStatus();\n  // ...\n}\n\nfunction SaveButton() {\n  const isOnline = useOnlineStatus();\n  // ...\n}\n```\n\n커스텀 Hook으로 Effect를 감싸는 것이 종종 유용한 이유는 다음과 같습니다.\n\n1. 매우 명확하게 Effect로 주고받는 데이터 흐름을 만들 때\n2. 컴포넌트가 Effect의 정확한 실행보다 목적에 집중하도록 할 때\n3. React가 새 기능을 추가할 때, 다른 컴포넌트의 변경 없이 이 Effect를 삭제할 수 있을 때\n\n[디자인 시스템](https://uxdesign.cc/everything-you-need-to-know-about-design-systems-54b109851969)과 과 마찬가지로, 앱의 컴포넌트에서 일반적인 관용구를 추출하여 커스텀 Hook으로 만드는 것이 도움이 될 수 있습니다. 이렇게 하면 컴포넌트의 코드가 의도에 집중할 수 있고, Effect를 자주 작성하지 않아도 됩니다. React 커뮤니티에서 많은 훌륭한 커스텀 Hook을 관리하고 있습니다.\n\n<DeepDive>\n\n#### React가 데이터 패칭을 위한 내부 해결책을 제공할까요? {/*will-react-provide-any-built-in-solution-for-data-fetching*/}\n\n현재는 [`use`](/reference/react/use#streaming-data-from-server-to-client) API를 사용해, 렌더링 단계에서 [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)를 `use`에 넘겨 데이터를 읽을 수 있습니다.\n\n```js {1,4,11}\nimport { use, Suspense } from \"react\";\n\nfunction Message({ messagePromise }) {\n  const messageContent = use(messagePromise);\n  return <p>Here is the message: {messageContent}</p>;\n}\n\nexport function MessageContainer({ messagePromise }) {\n  return (\n    <Suspense fallback={<p>⌛Downloading message...</p>}>\n      <Message messagePromise={messagePromise} />\n    </Suspense>\n  );\n}\n```\n\n아직 세부 사항을 조정 중이지만, 나중에는 데이터 패칭을 다음과 같이 작성할 것입니다.\n\n```js {1,4,6}\nimport { use } from 'react';\n\nfunction ShippingForm({ country }) {\n  const cities = use(fetch(`/api/cities?country=${country}`));\n  const [city, setCity] = useState(null);\n  const areas = city ? use(fetch(`/api/areas?city=${city}`)) : null;\n  // ...\n```\n\n앱에 `useData`과 같은 커스텀 Hook을 사용한다면, 모든 컴포넌트에 수동으로 Effect를 작성하는 것보다 최종적으로 권장되는 접근 방식으로 변경하는 것이 더 적은 변경이 요구됩니다. 그러나 이전의 접근 방식도 충분히 잘 동작하기 때문에 Effect 사용을 즐긴다면 그렇게 사용해도 됩니다.\n\n</DeepDive>\n\n### 여러 방법이 존재합니다. {/*there-is-more-than-one-way-to-do-it*/}\n\n브라우저의 [`requestAnimationFrame`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame) API를 이용해 **처음부터** 페이드 인 애니메이션을 구현한다고 생각해 봅시다. 아마 애니메이션을 반복시키기 위해 Effect부터 작성할 겁니다. 각각의 애니메이션 프레임 동안 [참조해 둔 ref](/learn/manipulating-the-dom-with-refs) DOM 노드의 투명도를 `1`에 도달할 때까지 변경할 수 있습니다. 코드는 다음과 같이 작성될 겁니다.\n\n<Sandpack>\n\n```js\nimport { useState, useEffect, useRef } from 'react';\n\nfunction Welcome() {\n  const ref = useRef(null);\n\n  useEffect(() => {\n    const duration = 1000;\n    const node = ref.current;\n\n    let startTime = performance.now();\n    let frameId = null;\n\n    function onFrame(now) {\n      const timePassed = now - startTime;\n      const progress = Math.min(timePassed / duration, 1);\n      onProgress(progress);\n      if (progress < 1) {\n        // 아직 그려야 할 프레임이 많습니다.\n        frameId = requestAnimationFrame(onFrame);\n      }\n    }\n\n    function onProgress(progress) {\n      node.style.opacity = progress;\n    }\n\n    function start() {\n      onProgress(0);\n      startTime = performance.now();\n      frameId = requestAnimationFrame(onFrame);\n    }\n\n    function stop() {\n      cancelAnimationFrame(frameId);\n      startTime = null;\n      frameId = null;\n    }\n\n    start();\n    return () => stop();\n  }, []);\n\n  return (\n    <h1 className=\"welcome\" ref={ref}>\n      Welcome\n    </h1>\n  );\n}\n\nexport default function App() {\n  const [show, setShow] = useState(false);\n  return (\n    <>\n      <button onClick={() => setShow(!show)}>\n        {show ? 'Remove' : 'Show'}\n      </button>\n      <hr />\n      {show && <Welcome />}\n    </>\n  );\n}\n```\n\n```css\nlabel, button { display: block; margin-bottom: 20px; }\nhtml, body { min-height: 300px; }\n.welcome {\n  opacity: 0;\n  color: white;\n  padding: 50px;\n  text-align: center;\n  font-size: 50px;\n  background-image: radial-gradient(circle, rgba(63,94,251,1) 0%, rgba(252,70,107,1) 100%);\n}\n```\n\n</Sandpack>\n\n이 컴포넌트의 가독성을 위해 로직을 추출해 `useFadeIn` 커스텀 Hook을 만들어 봅시다.\n\n<Sandpack>\n\n```js\nimport { useState, useEffect, useRef } from 'react';\nimport { useFadeIn } from './useFadeIn.js';\n\nfunction Welcome() {\n  const ref = useRef(null);\n\n  useFadeIn(ref, 1000);\n\n  return (\n    <h1 className=\"welcome\" ref={ref}>\n      Welcome\n    </h1>\n  );\n}\n\nexport default function App() {\n  const [show, setShow] = useState(false);\n  return (\n    <>\n      <button onClick={() => setShow(!show)}>\n        {show ? 'Remove' : 'Show'}\n      </button>\n      <hr />\n      {show && <Welcome />}\n    </>\n  );\n}\n```\n\n```js src/useFadeIn.js\nimport { useEffect } from 'react';\n\nexport function useFadeIn(ref, duration) {\n  useEffect(() => {\n    const node = ref.current;\n\n    let startTime = performance.now();\n    let frameId = null;\n\n    function onFrame(now) {\n      const timePassed = now - startTime;\n      const progress = Math.min(timePassed / duration, 1);\n      onProgress(progress);\n      if (progress < 1) {\n        // 아직 그려야 할 프레임이 많습니다.\n        frameId = requestAnimationFrame(onFrame);\n      }\n    }\n\n    function onProgress(progress) {\n      node.style.opacity = progress;\n    }\n\n    function start() {\n      onProgress(0);\n      startTime = performance.now();\n      frameId = requestAnimationFrame(onFrame);\n    }\n\n    function stop() {\n      cancelAnimationFrame(frameId);\n      startTime = null;\n      frameId = null;\n    }\n\n    start();\n    return () => stop();\n  }, [ref, duration]);\n}\n```\n\n```css\nlabel, button { display: block; margin-bottom: 20px; }\nhtml, body { min-height: 300px; }\n.welcome {\n  opacity: 0;\n  color: white;\n  padding: 50px;\n  text-align: center;\n  font-size: 50px;\n  background-image: radial-gradient(circle, rgba(63,94,251,1) 0%, rgba(252,70,107,1) 100%);\n}\n```\n\n</Sandpack>\n\n`useFadeIn` 코드를 유지할 수도 있지만 더 리팩토링할 수도 있습니다. 예를 들어 `useFadeIn` 밖으로 애니메이션 반복 설정 로직을 빼내 `useAnimationLoop` 커스텀 Hook으로 만들 수 있습니다.\n\n<Sandpack>\n\n```js\nimport { useState, useEffect, useRef } from 'react';\nimport { useFadeIn } from './useFadeIn.js';\n\nfunction Welcome() {\n  const ref = useRef(null);\n\n  useFadeIn(ref, 1000);\n\n  return (\n    <h1 className=\"welcome\" ref={ref}>\n      Welcome\n    </h1>\n  );\n}\n\nexport default function App() {\n  const [show, setShow] = useState(false);\n  return (\n    <>\n      <button onClick={() => setShow(!show)}>\n        {show ? 'Remove' : 'Show'}\n      </button>\n      <hr />\n      {show && <Welcome />}\n    </>\n  );\n}\n```\n\n```js src/useFadeIn.js active\nimport { useState, useEffect } from 'react';\nimport { useEffectEvent } from 'react';\n\nexport function useFadeIn(ref, duration) {\n  const [isRunning, setIsRunning] = useState(true);\n\n  useAnimationLoop(isRunning, (timePassed) => {\n    const progress = Math.min(timePassed / duration, 1);\n    ref.current.style.opacity = progress;\n    if (progress === 1) {\n      setIsRunning(false);\n    }\n  });\n}\n\nfunction useAnimationLoop(isRunning, drawFrame) {\n  const onFrame = useEffectEvent(drawFrame);\n\n  useEffect(() => {\n    if (!isRunning) {\n      return;\n    }\n\n    const startTime = performance.now();\n    let frameId = null;\n\n    function tick(now) {\n      const timePassed = now - startTime;\n      onFrame(timePassed);\n      frameId = requestAnimationFrame(tick);\n    }\n\n    tick();\n    return () => cancelAnimationFrame(frameId);\n  }, [isRunning]);\n}\n```\n\n```css\nlabel, button { display: block; margin-bottom: 20px; }\nhtml, body { min-height: 300px; }\n.welcome {\n  opacity: 0;\n  color: white;\n  padding: 50px;\n  text-align: center;\n  font-size: 50px;\n  background-image: radial-gradient(circle, rgba(63,94,251,1) 0%, rgba(252,70,107,1) 100%);\n}\n```\n\n</Sandpack>\n\n하지만, *반드시* 이처럼 작성할 필요는 없습니다. 일반 함수와 마찬가지로 궁극적으로 코드의 여러 부분 사이의 경계를 어디에 그릴지 결정해야 합니다. 매우 다르게 접근할 수도 있습니다. Effect 내부의 로직을 유지하는 대신, 대부분의 중요한 로직을 자바스크립트의 [Class](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Classes) 내부로 이동시킬 수 있습니다.\n\n<Sandpack>\n\n```js\nimport { useState, useEffect, useRef } from 'react';\nimport { useFadeIn } from './useFadeIn.js';\n\nfunction Welcome() {\n  const ref = useRef(null);\n\n  useFadeIn(ref, 1000);\n\n  return (\n    <h1 className=\"welcome\" ref={ref}>\n      Welcome\n    </h1>\n  );\n}\n\nexport default function App() {\n  const [show, setShow] = useState(false);\n  return (\n    <>\n      <button onClick={() => setShow(!show)}>\n        {show ? 'Remove' : 'Show'}\n      </button>\n      <hr />\n      {show && <Welcome />}\n    </>\n  );\n}\n```\n\n```js src/useFadeIn.js active\nimport { useState, useEffect } from 'react';\nimport { FadeInAnimation } from './animation.js';\n\nexport function useFadeIn(ref, duration) {\n  useEffect(() => {\n    const animation = new FadeInAnimation(ref.current);\n    animation.start(duration);\n    return () => {\n      animation.stop();\n    };\n  }, [ref, duration]);\n}\n```\n\n```js src/animation.js\nexport class FadeInAnimation {\n  constructor(node) {\n    this.node = node;\n  }\n  start(duration) {\n    this.duration = duration;\n    this.onProgress(0);\n    this.startTime = performance.now();\n    this.frameId = requestAnimationFrame(() => this.onFrame());\n  }\n  onFrame() {\n    const timePassed = performance.now() - this.startTime;\n    const progress = Math.min(timePassed / this.duration, 1);\n    this.onProgress(progress);\n    if (progress === 1) {\n      this.stop();\n    } else {\n      // 아직 더 그릴 프레임이 있습니다\n      this.frameId = requestAnimationFrame(() => this.onFrame());\n    }\n  }\n  onProgress(progress) {\n    this.node.style.opacity = progress;\n  }\n  stop() {\n    cancelAnimationFrame(this.frameId);\n    this.startTime = null;\n    this.frameId = null;\n    this.duration = 0;\n  }\n}\n```\n\n```css\nlabel, button { display: block; margin-bottom: 20px; }\nhtml, body { min-height: 300px; }\n.welcome {\n  opacity: 0;\n  color: white;\n  padding: 50px;\n  text-align: center;\n  font-size: 50px;\n  background-image: radial-gradient(circle, rgba(63,94,251,1) 0%, rgba(252,70,107,1) 100%);\n}\n```\n\n</Sandpack>\n\nEffect는 외부 시스템과 React를 연결할 수 있게 해줍니다. 예를 들어 여러 애니메이션을 연결하는 것처럼 Effects 간의 조정이 더 많이 필요할수록, 위의 코드 예시처럼 Effect와 Hook 밖으로 로직을 *완전히* 분리하는 것이 합리적입니다. 그렇게 분리한 코드는 \"외부 시스템\"이 *될 것입니다* Effect는 React 밖으로 내보낸 시스템에 메시지만 보내면 되기 때문에 이런 방식은 Effect가 심플한 상태를 유지하도록 합니다.\n\n위의 예시는 페이드인 로직이 자바스크립트로 작성되어야 하는 경우라고 가정합니다. 하지만 이런 특정 페이드인 애니메이션은 일반 [CSS 애니메이션](https://developer.mozilla.org/ko/docs/Web/CSS/CSS_Animations/Using_CSS_animations)으로 구현하는 것이 더 간단하고 훨씬 효율적입니다.\n\n<Sandpack>\n\n```js\nimport { useState, useEffect, useRef } from 'react';\nimport './welcome.css';\n\nfunction Welcome() {\n  return (\n    <h1 className=\"welcome\">\n      Welcome\n    </h1>\n  );\n}\n\nexport default function App() {\n  const [show, setShow] = useState(false);\n  return (\n    <>\n      <button onClick={() => setShow(!show)}>\n        {show ? 'Remove' : 'Show'}\n      </button>\n      <hr />\n      {show && <Welcome />}\n    </>\n  );\n}\n```\n\n```css src/styles.css\nlabel, button { display: block; margin-bottom: 20px; }\nhtml, body { min-height: 300px; }\n```\n\n```css src/welcome.css active\n.welcome {\n  color: white;\n  padding: 50px;\n  text-align: center;\n  font-size: 50px;\n  background-image: radial-gradient(circle, rgba(63,94,251,1) 0%, rgba(252,70,107,1) 100%);\n\n  animation: fadeIn 1000ms;\n}\n\n@keyframes fadeIn {\n  0% { opacity: 0; }\n  100% { opacity: 1; }\n}\n\n```\n\n</Sandpack>\n\n가끔 Hook이 필요하지 않을 수 있습니다!\n\n<Recap>\n\n- 커스텀 Hook을 사용하면 컴포넌트 간 로직을 공유할 수 있습니다.\n- 커스텀 Hook의 이름은 `use` 뒤에 대문자로 시작되어야 합니다.\n- 커스텀 Hook은 state 자체가 아닌 state 저장 로직만 공유합니다.\n- 하나의 Hook에서 다른 Hook으로 반응형 값을 전달할 수 있고, 값은 최신 상태로 유지됩니다.\n- 모든 Hook은 컴포넌트가 재렌더링될 때 마다 재실행됩니다.\n- 커스텀 Hook의 코드는 컴포넌트 코드처럼 순수해야 합니다.\n- 커스텀 Hook을 통해 받는 이벤트 핸들러는 Effect로 감싸야 합니다.\n- `useMount`같은 커스텀 Hook을 생성하면 안 됩니다. 용도를 명확히 하세요.\n- 코드의 경계를 선택하는 방법과 위치는 여러분이 결정할 수 있습니다.\n\n</Recap>\n\n<Challenges>\n\n#### `useCounter` Hook 추출하기 {/*extract-a-usecounter-hook*/}\n\n이 컴포넌트는 매초 증가하는 숫자를 보여주기 위해 state 변수와 Effect를 사용합니다. `useCounter`라는 커스텀 Hook으로 이 로직을 분리해 봅시다. 우리의 목표는 정확히 다음과 같이 동작하는 `Counter`를 만드는 것입니다.\n\n```js\nexport default function Counter() {\n  const count = useCounter();\n  return <h1>Seconds passed: {count}</h1>;\n}\n```\n\n`useCounter.js` 에 커스텀 Hook을 작성하고 `App.js` 파일에 가져와야 합니다.\n\n<Sandpack>\n\n```js\nimport { useState, useEffect } from 'react';\n\nexport default function Counter() {\n  const [count, setCount] = useState(0);\n  useEffect(() => {\n    const id = setInterval(() => {\n      setCount(c => c + 1);\n    }, 1000);\n    return () => clearInterval(id);\n  }, []);\n  return <h1>Seconds passed: {count}</h1>;\n}\n```\n\n```js src/useCounter.js\n// 이 파일에 커스텀 Hook을 작성하세요!\n```\n\n</Sandpack>\n\n<Solution>\n\n코드가 다음과 같아야 합니다.\n\n<Sandpack>\n\n```js\nimport { useCounter } from './useCounter.js';\n\nexport default function Counter() {\n  const count = useCounter();\n  return <h1>Seconds passed: {count}</h1>;\n}\n```\n\n```js src/useCounter.js\nimport { useState, useEffect } from 'react';\n\nexport function useCounter() {\n  const [count, setCount] = useState(0);\n  useEffect(() => {\n    const id = setInterval(() => {\n      setCount(c => c + 1);\n    }, 1000);\n    return () => clearInterval(id);\n  }, []);\n  return count;\n}\n```\n\n</Sandpack>\n\n`App.js`가 더 이상 `useState`와 `useEffect`를 가져오지 않아도 된다는 것을 기억하세요.\n\n</Solution>\n\n#### 카운터의 지연을 수정 가능하게 하기 {/*make-the-counter-delay-configurable*/}\n\n이 예시에는 슬라이더를 통해 조작되는 `delay`라는 state 변수가 있지만 사용되고 있지 않습니다. `useCounter` 커스텀 Hook에 `delay` 값을 전달해, 하드 코딩된 `1000` ms이 아닌 전달된 `delay` 값을 사용하도록 해봅시다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\nimport { useCounter } from './useCounter.js';\n\nexport default function Counter() {\n  const [delay, setDelay] = useState(1000);\n  const count = useCounter();\n  return (\n    <>\n      <label>\n        Tick duration: {delay} ms\n        <br />\n        <input\n          type=\"range\"\n          value={delay}\n          min=\"10\"\n          max=\"2000\"\n          onChange={e => setDelay(Number(e.target.value))}\n        />\n      </label>\n      <hr />\n      <h1>Ticks: {count}</h1>\n    </>\n  );\n}\n```\n\n```js src/useCounter.js\nimport { useState, useEffect } from 'react';\n\nexport function useCounter() {\n  const [count, setCount] = useState(0);\n  useEffect(() => {\n    const id = setInterval(() => {\n      setCount(c => c + 1);\n    }, 1000);\n    return () => clearInterval(id);\n  }, []);\n  return count;\n}\n```\n\n</Sandpack>\n\n<Solution>\n\n`useCounter(delay)`에 `delay` 값을 넘겨준 뒤, Hook 내부에서 하드 코딩된 `1000` 값 대신 `delay`를 사용해 봅시다. Effect의 의존성에 `delay`를 추가해야 합니다. 이렇게 되면 `delay`가 변경되면 간격이 재설정됩니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\nimport { useCounter } from './useCounter.js';\n\nexport default function Counter() {\n  const [delay, setDelay] = useState(1000);\n  const count = useCounter(delay);\n  return (\n    <>\n      <label>\n        Tick duration: {delay} ms\n        <br />\n        <input\n          type=\"range\"\n          value={delay}\n          min=\"10\"\n          max=\"2000\"\n          onChange={e => setDelay(Number(e.target.value))}\n        />\n      </label>\n      <hr />\n      <h1>Ticks: {count}</h1>\n    </>\n  );\n}\n```\n\n```js src/useCounter.js\nimport { useState, useEffect } from 'react';\n\nexport function useCounter(delay) {\n  const [count, setCount] = useState(0);\n  useEffect(() => {\n    const id = setInterval(() => {\n      setCount(c => c + 1);\n    }, delay);\n    return () => clearInterval(id);\n  }, [delay]);\n  return count;\n}\n```\n\n</Sandpack>\n\n</Solution>\n\n#### `useCounter`에서 `useInterval` 분리하기 {/*extract-useinterval-out-of-usecounter*/}\n\n이제 `useCounter`는 두 가지 일을 합니다. 간격을 설정하고, 간격마다 state 변수를 증가시킵니다. 간격을 설정하는 로직을 `useInterval`라는 이름의 다른 Hook으로 분리해 봅시다. 이 Hook은 `onTick` 콜백과 `delay`, 두 가지 props가 필요합니다. 이렇게 변경하면 `useCounter`은 다음과 같이 보일 것입니다.\n\n```js\nexport function useCounter(delay) {\n  const [count, setCount] = useState(0);\n  useInterval(() => {\n    setCount(c => c + 1);\n  }, delay);\n  return count;\n}\n```\n\n`useInterval.js` 파일에 `useInterval`을 작성하고 `useCounter.js` 파일에 가져오세요.\n\n\n<Sandpack>\n\n```js\nimport { useCounter } from './useCounter.js';\n\nexport default function Counter() {\n  const count = useCounter(1000);\n  return <h1>Seconds passed: {count}</h1>;\n}\n```\n\n```js src/useCounter.js\nimport { useState, useEffect } from 'react';\n\nexport function useCounter(delay) {\n  const [count, setCount] = useState(0);\n  useEffect(() => {\n    const id = setInterval(() => {\n      setCount(c => c + 1);\n    }, delay);\n    return () => clearInterval(id);\n  }, [delay]);\n  return count;\n}\n```\n\n```js src/useInterval.js\n// 이 파일에 커스텀 Hook을 작성하세요!\n```\n\n</Sandpack>\n\n<Solution>\n\n`useInterval` 내부의 로직은 간격을 설정하고 초기화해야 합니다. 그 외에 다른 것은 필요하지 않습니다.\n\n<Sandpack>\n\n```js\nimport { useCounter } from './useCounter.js';\n\nexport default function Counter() {\n  const count = useCounter(1000);\n  return <h1>Seconds passed: {count}</h1>;\n}\n```\n\n```js src/useCounter.js\nimport { useState } from 'react';\nimport { useInterval } from './useInterval.js';\n\nexport function useCounter(delay) {\n  const [count, setCount] = useState(0);\n  useInterval(() => {\n    setCount(c => c + 1);\n  }, delay);\n  return count;\n}\n```\n\n```js src/useInterval.js active\nimport { useEffect } from 'react';\n\nexport function useInterval(onTick, delay) {\n  useEffect(() => {\n    const id = setInterval(onTick, delay);\n    return () => clearInterval(id);\n  }, [onTick, delay]);\n}\n```\n\n</Sandpack>\n\n이 해결 방법은 다음에 해결해야할 약간의 도전 과제가 남아 있습니다.\n\n</Solution>\n\n#### 간격 재설정 고치기 {/*fix-a-resetting-interval*/}\n\n이 예시에서 *두 개의* 별개의 간격이 존재합니다.\n\n`useCounter`를 호출하는 `App` 컴포넌트는 카운터를 매초 업데이트하기 위해 `useInterval`를 호출합니다. 그러나 `App` 는 `useInterval`를 2초에 한 번씩 랜덤하게 배경색을 변경하기 위해 `useInterval`를 *또* 호출합니다.\n\n이런 이유로 배경을 업데이트하는 콜백은 절대 실행되지 않습니다. `useInterval` 내부에 로그를 남겨보세요.\n\n```js {2,5}\n  useEffect(() => {\n    console.log('✅ Setting up an interval with delay ', delay)\n    const id = setInterval(onTick, delay);\n    return () => {\n      console.log('❌ Clearing an interval with delay ', delay)\n      clearInterval(id);\n    };\n  }, [onTick, delay]);\n```\n\n로그가 생각했던 대로 잘 동작하나요? 어떤 Effect가 불필요하게 재동기화한다면, 어떤 의존성이 원인이 되었는지 예측할 수 있나요? 해당 Effect에서 [그 의존성을 제거하는](/learn/removing-effect-dependencies) 방법이 있나요?\n\n이 문제를 해결한 뒤, 배경 화면이 2초마다 바뀔 수 있다고 예상합니다.\n\n<Hint>\n\n`useInterval`가 이벤트 리스너를 하나의 prop로 받는 것처럼 보입니다. 이 이벤트 리스너를 감싸 Effect의 의존성이 될 필요가 없도록 만드는 방법을 생각해 낼 수 있나요?\n\n</Hint>\n\n<Sandpack>\n\n```js\nimport { useCounter } from './useCounter.js';\nimport { useInterval } from './useInterval.js';\n\nexport default function Counter() {\n  const count = useCounter(1000);\n\n  useInterval(() => {\n    const randomColor = `hsla(${Math.random() * 360}, 100%, 50%, 0.2)`;\n    document.body.style.backgroundColor = randomColor;\n  }, 2000);\n\n  return <h1>Seconds passed: {count}</h1>;\n}\n```\n\n```js src/useCounter.js\nimport { useState } from 'react';\nimport { useInterval } from './useInterval.js';\n\nexport function useCounter(delay) {\n  const [count, setCount] = useState(0);\n  useInterval(() => {\n    setCount(c => c + 1);\n  }, delay);\n  return count;\n}\n```\n\n```js src/useInterval.js\nimport { useEffect } from 'react';\nimport { useEffectEvent } from 'react';\n\nexport function useInterval(onTick, delay) {\n  useEffect(() => {\n    const id = setInterval(onTick, delay);\n    return () => {\n      clearInterval(id);\n    };\n  }, [onTick, delay]);\n}\n```\n\n</Sandpack>\n\n<Solution>\n\n[앞서 그랬던 것처럼](/learn/reusing-logic-with-custom-hooks#passing-event-handlers-to-custom-hooks) `useInterval` 내부에서 콜백을 Effect 이벤트로 감싸주세요.\n\n이 방법은 `onTick`을 Effect의 의존성에서 빼낼 수 있도록 합니다. Effect는 컴포넌트가 재렌더링될 때마다 재동기화하지 않을 것이고 배경색을 변경 간격 역시 변경되는 기회가 오기 전에 매초 초기화되는 일은 없게 됩니다.\n\n이제 각 간격은 원하는 대로 동작하고 서로를 방해하지 않습니다.\n\n\n<Sandpack>\n\n\n```js\nimport { useCounter } from './useCounter.js';\nimport { useInterval } from './useInterval.js';\n\nexport default function Counter() {\n  const count = useCounter(1000);\n\n  useInterval(() => {\n    const randomColor = `hsla(${Math.random() * 360}, 100%, 50%, 0.2)`;\n    document.body.style.backgroundColor = randomColor;\n  }, 2000);\n\n  return <h1>Seconds passed: {count}</h1>;\n}\n```\n\n```js src/useCounter.js\nimport { useState } from 'react';\nimport { useInterval } from './useInterval.js';\n\nexport function useCounter(delay) {\n  const [count, setCount] = useState(0);\n  useInterval(() => {\n    setCount(c => c + 1);\n  }, delay);\n  return count;\n}\n```\n\n```js src/useInterval.js active\nimport { useEffect } from 'react';\nimport { useEffectEvent } from 'react';\n\nexport function useInterval(callback, delay) {\n  const onTick = useEffectEvent(callback);\n  useEffect(() => {\n    const id = setInterval(onTick, delay);\n    return () => clearInterval(id);\n  }, [delay]);\n}\n```\n\n</Sandpack>\n\n</Solution>\n\n#### 엇갈린 움직임 구현하기 {/*implement-a-staggering-movement*/}\n\n이 예시에선 `usePointerPosition()` Hook이 최근 포인터의 위치를 추적합니다. 커서나 손을 미리보기 화면 위로 이동하면 빨간 점이 움직임을 따라가는 것을 확인할 수 있습니다. 이 위치는 `pos1` 변수에 저장됩니다.\n\n실제로는 다섯 개의 다른 점이 렌더링되고 있습니다. 모든 점이 같은 위치에 나타나기 때문에 보이지 않습니다. 이 부분을 수정해야 합니다. 대신 구현해야 하는 것은 \"엇갈린\" 움직임입니다. 각 점이 이전 점의 경로를 \"따라야\" 합니다. 예를 들어 커서를 빠르게 움직이면 첫 번째 점이 빠르게 뒤쫓고, 두 번째 점이 첫 번째 점을 약간의 지연을 두고 따라가고, 세 번째 점이 두 번째 점을 따라가는 방식으로 움직여야 합니다.\n\n`useDelayedValue` 커스텀 Hook을 구현해야 합니다. 현재 구현은 제공된 `value`를 반환하지만, 대신 밀리초 이전의 `delay`를 받으려고 합니다. 이를 위해선 state와 Effect가 필요할 수 있습니다.\n\n`useDelayedValue`를 값을 구현하고 나면 점들이 서로 따라 움직이는 것을 볼 수 있을 것입니다.\n\n<Hint>\n\n`delayedValue`을 커스텀 Hook 안에 state 변수로 저장해야 합니다. `value`가 변경되면 Effect를 실행하고 싶을 것입니다. 이 Effect는 `delay`만큼의 시간이 지난 후 `delayedValue`을 업데이트해야 합니다. `setTimeout`을 호출하는 것이 도움이 될 수 있습니다.\n\n이 Effect를 정리해야 하나요? 왜 또는 왜 안 되나요?\n\n</Hint>\n\n<Sandpack>\n\n```js\nimport { usePointerPosition } from './usePointerPosition.js';\n\nfunction useDelayedValue(value, delay) {\n  // TODO: 이 Hook 실행하기\n  return value;\n}\n\nexport default function Canvas() {\n  const pos1 = usePointerPosition();\n  const pos2 = useDelayedValue(pos1, 100);\n  const pos3 = useDelayedValue(pos2, 200);\n  const pos4 = useDelayedValue(pos3, 100);\n  const pos5 = useDelayedValue(pos3, 50);\n  return (\n    <>\n      <Dot position={pos1} opacity={1} />\n      <Dot position={pos2} opacity={0.8} />\n      <Dot position={pos3} opacity={0.6} />\n      <Dot position={pos4} opacity={0.4} />\n      <Dot position={pos5} opacity={0.2} />\n    </>\n  );\n}\n\nfunction Dot({ position, opacity }) {\n  return (\n    <div style={{\n      position: 'absolute',\n      backgroundColor: 'pink',\n      borderRadius: '50%',\n      opacity,\n      transform: `translate(${position.x}px, ${position.y}px)`,\n      pointerEvents: 'none',\n      left: -20,\n      top: -20,\n      width: 40,\n      height: 40,\n    }} />\n  );\n}\n```\n\n```js src/usePointerPosition.js\nimport { useState, useEffect } from 'react';\n\nexport function usePointerPosition() {\n  const [position, setPosition] = useState({ x: 0, y: 0 });\n  useEffect(() => {\n    function handleMove(e) {\n      setPosition({ x: e.clientX, y: e.clientY });\n    }\n    window.addEventListener('pointermove', handleMove);\n    return () => window.removeEventListener('pointermove', handleMove);\n  }, []);\n  return position;\n}\n```\n\n```css\nbody { min-height: 300px; }\n```\n\n</Sandpack>\n\n<Solution>\n\n다음은 동작하는 버전입니다. state 변수로 `delayedValue`를 유지합니다. `value`가 업데이트되면, Effect는 `delayedValue`를 업데이트하기 위한 타임아웃을 예약해 둡니다. 이게 바로 `delayedValue`가 항상 진짜 `value`보다 \"뒤처지는\" 이유입니다.\n\n<Sandpack>\n\n```js\nimport { useState, useEffect } from 'react';\nimport { usePointerPosition } from './usePointerPosition.js';\n\nfunction useDelayedValue(value, delay) {\n  const [delayedValue, setDelayedValue] = useState(value);\n\n  useEffect(() => {\n    setTimeout(() => {\n      setDelayedValue(value);\n    }, delay);\n  }, [value, delay]);\n\n  return delayedValue;\n}\n\nexport default function Canvas() {\n  const pos1 = usePointerPosition();\n  const pos2 = useDelayedValue(pos1, 100);\n  const pos3 = useDelayedValue(pos2, 200);\n  const pos4 = useDelayedValue(pos3, 100);\n  const pos5 = useDelayedValue(pos3, 50);\n  return (\n    <>\n      <Dot position={pos1} opacity={1} />\n      <Dot position={pos2} opacity={0.8} />\n      <Dot position={pos3} opacity={0.6} />\n      <Dot position={pos4} opacity={0.4} />\n      <Dot position={pos5} opacity={0.2} />\n    </>\n  );\n}\n\nfunction Dot({ position, opacity }) {\n  return (\n    <div style={{\n      position: 'absolute',\n      backgroundColor: 'pink',\n      borderRadius: '50%',\n      opacity,\n      transform: `translate(${position.x}px, ${position.y}px)`,\n      pointerEvents: 'none',\n      left: -20,\n      top: -20,\n      width: 40,\n      height: 40,\n    }} />\n  );\n}\n```\n\n```js src/usePointerPosition.js\nimport { useState, useEffect } from 'react';\n\nexport function usePointerPosition() {\n  const [position, setPosition] = useState({ x: 0, y: 0 });\n  useEffect(() => {\n    function handleMove(e) {\n      setPosition({ x: e.clientX, y: e.clientY });\n    }\n    window.addEventListener('pointermove', handleMove);\n    return () => window.removeEventListener('pointermove', handleMove);\n  }, []);\n  return position;\n}\n```\n\n```css\nbody { min-height: 300px; }\n```\n\n</Sandpack>\n\n이 Effect는 정리할 필요가 \"없다\"는 걸 기억하세요. 정리 기능에 `clearTimeout`를 호출했다면 매번 `value`는 변경되고, 이미 예정된 타임아웃을 리셋합니다. 동작이 계속 유지되도록 하기 위해 모든 타임아웃이 동작하길 바랄 겁니다.\n\n</Solution>\n\n</Challenges>\n"
  },
  {
    "path": "src/content/learn/rsc-sandbox-test.md",
    "content": "---\ntitle: RSC Sandbox Test\n---\n\n## Basic Server Component {/*basic-server-component*/}\n\n<SandpackRSC>\n\n```js src/App.js\nexport default function App() {\n  return <h1>Hello from a Server Component!</h1>;\n}\n```\n\n</SandpackRSC>\n\n## Server + Client Components {/*server-client*/}\n\n<SandpackRSC>\n\n```js src/App.js\nimport Counter from './Counter';\n\nexport default function App() {\n  return (\n    <div>\n      <h1>Server Component</h1>\n      <p>This text is rendered on the server.</p>\n      <Counter />\n    </div>\n  );\n}\n```\n\n```js src/Counter.js\n'use client';\nimport { useState } from 'react';\n\nexport default function Counter() {\n  const [count, setCount] = useState(0);\n  return (\n    <button onClick={() => setCount(count + 1)}>\n      Count: {count}\n    </button>\n  );\n}\n```\n\n</SandpackRSC>\n\n## Async Server Component with Suspense {/*async-suspense*/}\n\n<SandpackRSC>\n\n```js src/App.js\nimport { Suspense } from 'react';\nimport Albums from './Albums';\n\nexport default function App() {\n  return (\n    <div>\n      <h1>Music</h1>\n      <Suspense fallback={<p>Loading albums...</p>}>\n        <Albums />\n      </Suspense>\n    </div>\n  );\n}\n```\n\n```js src/Albums.js\nasync function fetchAlbums() {\n  await new Promise(resolve => setTimeout(resolve, 1000));\n  return ['Abbey Road', 'Let It Be', 'Revolver'];\n}\n\nexport default async function Albums() {\n  const albums = await fetchAlbums();\n  return (\n    <ul>\n      {albums.map(album => (\n        <li key={album}>{album}</li>\n      ))}\n    </ul>\n  );\n}\n```\n\n</SandpackRSC>\n\n## Streaming Proof {/*streaming-proof*/}\n\nThis demo proves streaming is incremental. The shell renders instantly with a `<Suspense>` fallback. After 2 seconds the async component streams in and replaces it — without re-rendering the outer content. The timestamps show the gap.\n\n<SandpackRSC>\n\n```js src/App.js\nimport { Suspense } from 'react';\nimport SlowData from './SlowData';\nimport Timestamp from './Timestamp';\n\nexport default function App() {\n  return (\n    <div>\n      <h1>Streaming Proof</h1>\n      <p>Shell rendered at: <Timestamp /></p>\n      <Suspense fallback={<p>⏳ Waiting for data to stream in...</p>}>\n        <SlowData />\n      </Suspense>\n    </div>\n  );\n}\n```\n\n```js src/SlowData.js\nimport Timestamp from './Timestamp';\n\nasync function fetchData() {\n  await new Promise(resolve => setTimeout(resolve, 2000));\n  return ['Chunk A', 'Chunk B', 'Chunk C'];\n}\n\nexport default async function SlowData() {\n  const items = await fetchData();\n  return (\n    <div>\n      <p>Data streamed in at: <Timestamp /></p>\n      <ul>\n        {items.map(item => (\n          <li key={item}>{item}</li>\n        ))}\n      </ul>\n    </div>\n  );\n}\n```\n\n```js src/Timestamp.js\n'use client';\n\nexport default function Timestamp() {\n  return <strong>{new Date().toLocaleTimeString()}</strong>;\n}\n```\n\n</SandpackRSC>\n\n## Flight Data Types {/*flight-data-types*/}\n\nThis demo passes Map, Set, Date, and BigInt from a server component through the Flight stream to a client component, proving the full Flight protocol type system works end-to-end.\n\n<SandpackRSC>\n\n```js src/App.js\nimport DataViewer from './DataViewer';\n\nexport default function App() {\n  const map = new Map([\n    ['alice', 100],\n    ['bob', 200],\n  ]);\n  const set = new Set(['react', 'next', 'remix']);\n  const date = new Date('2025-06-15T12:00:00Z');\n  const big = 9007199254740993n;\n\n  return (\n    <div>\n      <h1>Flight Data Types</h1>\n      <DataViewer map={map} set={set} date={date} big={big} />\n    </div>\n  );\n}\n```\n\n```js src/DataViewer.js\n'use client';\n\nexport default function DataViewer({ map, set, date, big }) {\n  const checks = [\n    ['Map', map instanceof Map, () => (\n      <ul>{[...map.entries()].map(([k, v]) => <li key={k}>{k}: {v}</li>)}</ul>\n    )],\n    ['Set', set instanceof Set, () => (\n      <ul>{[...set].map(v => <li key={v}>{v}</li>)}</ul>\n    )],\n    ['Date', date instanceof Date, () => (\n      <p>{date.toISOString()}</p>\n    )],\n    ['BigInt', typeof big === 'bigint', () => (\n      <p>{big.toString()}</p>\n    )],\n  ];\n\n  return (\n    <div>\n      {checks.map(([label, passed, render]) => (\n        <div key={label} style={{ marginBottom: 12 }}>\n          <strong>{label}: {passed ? 'pass' : 'FAIL'}</strong>\n          {render()}\n        </div>\n      ))}\n    </div>\n  );\n}\n```\n\n</SandpackRSC>\n\n## Promise Streaming with use() {/*promise-streaming-use*/}\n\nThe server creates a promise (resolves in 2s) and passes it as a prop through a parent async component that suspends for 3s. When the parent reveals at ~3s, the promise is already resolved — so `use()` returns instantly with no inner fallback. The elapsed time should be ~3000ms (the parent's delay), not ~5000ms (which would mean the promise restarted on the client).\n\n<SandpackRSC>\n\n```js src/App.js\nimport { Suspense } from 'react';\nimport SlowParent from './SlowParent';\nimport UserCard from './UserCard';\n\nasync function fetchUser() {\n  await new Promise(resolve => setTimeout(resolve, 2000));\n  return { name: 'Alice', role: 'Engineer' };\n}\n\nfunction now() {\n  return Date.now();\n}\n\nexport default function App() {\n  const serverTime = now();\n  const userPromise = fetchUser();\n  return (\n    <div>\n      <h1>Promise Streaming</h1>\n      <p>Promise resolves in 2s. Parent suspends for 3s.</p>\n      <Suspense fallback={<p>Outer: waiting for parent (3s)...</p>}>\n        <SlowParent>\n          <Suspense fallback={<p>Inner: waiting for data (should not appear!)</p>}>\n            <UserCard userPromise={userPromise} serverTime={serverTime} />\n          </Suspense>\n        </SlowParent>\n      </Suspense>\n    </div>\n  );\n}\n```\n\n```js src/SlowParent.js\nexport default async function SlowParent({ children }) {\n  await new Promise(resolve => setTimeout(resolve, 3000));\n  return <div>{children}</div>;\n}\n```\n\n```js src/UserCard.js\n'use client';\nimport { use } from 'react';\n\nfunction now() {\n  return Date.now();\n}\nexport default function UserCard({ userPromise, serverTime }) {\n  const user = use(userPromise);\n  const elapsed = now() - serverTime;\n  return (\n    <div style={{\n      border: '1px solid #ccc',\n      borderRadius: 8,\n      padding: 16,\n    }}>\n      <strong>{user.name}</strong>\n      <p>{user.role}</p>\n      <p style={{ fontSize: 13 }}>\n        Rendered {elapsed}ms after server created the promise.\n      </p>\n      <p style={{ color: '#666', fontSize: 12 }}>\n        ~3000ms = promise already resolved, waited only for parent.\n        ~5000ms would mean the promise restarted on the client.\n      </p>\n    </div>\n  );\n}\n```\n\n</SandpackRSC>\n\n## Flight Data Types in Server Actions {/*flight-data-types-actions*/}\n\nThis demo sends Map, Set, Date, and BigInt from a client component *to* a server action via `encodeReply`/`decodeReply`, then verifies the types survived the round trip.\n\n<SandpackRSC>\n\n```js src/App.js\nimport { testTypes, getResults } from './actions';\nimport TestButton from './TestButton';\n\nexport default async function App() {\n  const results = await getResults();\n  return (\n    <div>\n      <h1>Flight Types in Server Actions</h1>\n      <TestButton testTypes={testTypes} />\n      {results ? (\n        <div>\n          {results.map(r => (\n            <div key={r.label} style={{ marginBottom: 12 }}>\n              <strong>{r.label}: {r.ok ? 'pass' : 'FAIL'}</strong>\n              <p>{r.detail}</p>\n            </div>\n          ))}\n        </div>\n      ) : (\n        <p>Click the button to send typed data to the server action.</p>\n      )}\n    </div>\n  );\n}\n```\n\n```js src/actions.js\n'use server';\n\nlet results = null;\n\nexport async function testTypes(map, set, date, big) {\n  results = [\n    {\n      label: 'Map',\n      ok: map instanceof Map,\n      detail: map instanceof Map\n        ? 'entries: ' + JSON.stringify([...map.entries()])\n        : 'received: ' + typeof map,\n    },\n    {\n      label: 'Set',\n      ok: set instanceof Set,\n      detail: set instanceof Set\n        ? 'values: ' + JSON.stringify([...set])\n        : 'received: ' + typeof set,\n    },\n    {\n      label: 'Date',\n      ok: date instanceof Date,\n      detail: date instanceof Date\n        ? date.toISOString()\n        : 'received: ' + typeof date,\n    },\n    {\n      label: 'BigInt',\n      ok: typeof big === 'bigint',\n      detail: typeof big === 'bigint'\n        ? big.toString()\n        : 'received: ' + typeof big,\n    },\n  ];\n}\n\nexport async function getResults() {\n  return results;\n}\n```\n\n```js src/TestButton.js\n'use client';\nimport { useTransition } from 'react';\n\nexport default function TestButton({ testTypes }) {\n  const [pending, startTransition] = useTransition();\n\n  function handleClick() {\n    startTransition(async () => {\n      await testTypes(\n        new Map([['alice', 100], ['bob', 200]]),\n        new Set(['react', 'next', 'remix']),\n        new Date('2025-06-15T12:00:00Z'),\n        9007199254740993n\n      );\n    });\n  }\n\n  return (\n    <button onClick={handleClick} disabled={pending}>\n      {pending ? 'Sending...' : 'Send typed data to server'}\n    </button>\n  );\n}\n```\n\n</SandpackRSC>\n\n## Server Action Mutation + Re-render {/*action-mutation-rerender*/}\n\nThe server action mutates server-side data and returns a confirmation string. The updated list is only visible because the framework automatically re-renders the entire server component tree after the action completes — the server component re-reads the data and streams the new UI to the client.\n\n<SandpackRSC>\n\n```js src/App.js\nimport { getTodos } from './db';\nimport { createTodo } from './actions';\nimport AddTodo from './AddTodo';\n\nexport default function App() {\n  const todos = getTodos();\n  return (\n    <div>\n      <h1>Todo List</h1>\n      <p style={{ color: '#666', fontSize: 13 }}>\n        This list is rendered by a server component\n        reading server-side data. It only updates because\n        the server re-renders after each action.\n      </p>\n      <ul>\n        {todos.map((todo, i) => (\n          <li key={i}>{todo}</li>\n        ))}\n      </ul>\n      <AddTodo createTodo={createTodo} />\n    </div>\n  );\n}\n```\n\n```js src/db.js\nlet todos = ['Buy groceries'];\n\nexport function getTodos() {\n  return [...todos];\n}\n\nexport function addTodo(text) {\n  todos.push(text);\n}\n```\n\n```js src/actions.js\n'use server';\nimport { addTodo } from './db';\n\nexport async function createTodo(text) {\n  if (!text) return 'Please enter a todo.';\n  addTodo(text);\n  return 'Added: ' + text;\n}\n```\n\n```js src/AddTodo.js\n'use client';\nimport { useState, useTransition } from 'react';\n\nexport default function AddTodo({ createTodo }) {\n  const [text, setText] = useState('');\n  const [message, setMessage] = useState('');\n  const [pending, startTransition] = useTransition();\n\n  function handleSubmit(e) {\n    e.preventDefault();\n    startTransition(async () => {\n      const result = await createTodo(text);\n      setMessage(result);\n      setText('');\n    });\n  }\n\n  return (\n    <div>\n      <form onSubmit={handleSubmit}>\n        <input\n          value={text}\n          onChange={e => setText(e.target.value)}\n          placeholder=\"New todo\"\n        />\n        <button disabled={pending}>\n          {pending ? 'Adding...' : 'Add'}\n        </button>\n      </form>\n      {message && (\n        <p style={{ color: '#666', fontSize: 13 }}>\n          Action returned: \"{message}\"\n        </p>\n      )}\n    </div>\n  );\n}\n```\n\n</SandpackRSC>\n\n## Inline Server Actions {/*inline-server-actions*/}\n\nServer actions defined inline inside a server component with `'use server'` on the function body. The action closes over module-level state and is passed as a prop — no separate `actions.js` file needed.\n\n<SandpackRSC>\n\n```js src/App.js\nimport LikeButton from './LikeButton';\n\nlet count = 0;\n\nexport default function App() {\n  async function addLike() {\n    'use server';\n    count++;\n  }\n\n  return (\n    <div>\n      <h1>Inline Server Actions</h1>\n      <p>Likes: {count}</p>\n      <LikeButton addLike={addLike} />\n    </div>\n  );\n}\n```\n\n```js src/LikeButton.js\n'use client';\n\nexport default function LikeButton({ addLike }) {\n  return (\n    <form action={addLike}>\n      <button type=\"submit\">Like</button>\n    </form>\n  );\n}\n```\n\n</SandpackRSC>\n\n## Server Functions {/*server-functions*/}\n\n<SandpackRSC>\n\n```js src/App.js\nimport { addLike, getLikeCount } from './actions';\nimport LikeButton from './LikeButton';\n\nexport default async function App() {\n  const count = await getLikeCount();\n  return (\n    <div>\n      <h1>Server Functions</h1>\n      <p>Likes: {count}</p>\n      <LikeButton addLike={addLike} />\n    </div>\n  );\n}\n```\n\n```js src/actions.js\n'use server';\n\nlet count = 0;\n\nexport async function addLike() {\n  count++;\n}\n\nexport async function getLikeCount() {\n  return count;\n}\n```\n\n```js src/LikeButton.js\n'use client';\n\nexport default function LikeButton({ addLike }) {\n  return (\n    <form action={addLike}>\n      <button type=\"submit\">Like</button>\n    </form>\n  );\n}\n```\n\n</SandpackRSC>\n"
  },
  {
    "path": "src/content/learn/scaling-up-with-reducer-and-context.md",
    "content": "---\ntitle: Reducer와 Context로 앱 확장하기\n---\n\n<Intro>\n\nReducer를 사용하면 컴포넌트의 state 업데이트 로직을 통합할 수 있습니다. Context를 사용하면 다른 컴포넌트들에 정보를 전달할 수 있습니다. Reducer와 context를 함께 사용하여 복잡한 화면의 state를 관리할 수 있습니다.\n\n</Intro>\n\n<YouWillLearn>\n\n* reducer와 context를 결합하는 방법\n* state와 dispatch 함수를 prop으로 전달하지 않는 방법\n* context와 state 로직을 별도의 파일에서 관리하는 방법\n\n</YouWillLearn>\n\n## Reducer와 context를 결합하기 {/*combining-a-reducer-with-context*/}\n\n[Reducer의 개요](/learn/extracting-state-logic-into-a-reducer)의 예시에서 reducer로 state를 관리하는 방법을 알아보았습니다. 해당 예시에서 state 업데이트 로직을 모두 포함하는 reducer 함수를 `App.js` 파일의 맨 아래에 선언했습니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { useReducer } from 'react';\nimport AddTask from './AddTask.js';\nimport TaskList from './TaskList.js';\n\nexport default function TaskApp() {\n  const [tasks, dispatch] = useReducer(\n    tasksReducer,\n    initialTasks\n  );\n\n  function handleAddTask(text) {\n    dispatch({\n      type: 'added',\n      id: nextId++,\n      text: text,\n    });\n  }\n\n  function handleChangeTask(task) {\n    dispatch({\n      type: 'changed',\n      task: task\n    });\n  }\n\n  function handleDeleteTask(taskId) {\n    dispatch({\n      type: 'deleted',\n      id: taskId\n    });\n  }\n\n  return (\n    <>\n      <h1>Day off in Kyoto</h1>\n      <AddTask\n        onAddTask={handleAddTask}\n      />\n      <TaskList\n        tasks={tasks}\n        onChangeTask={handleChangeTask}\n        onDeleteTask={handleDeleteTask}\n      />\n    </>\n  );\n}\n\nfunction tasksReducer(tasks, action) {\n  switch (action.type) {\n    case 'added': {\n      return [...tasks, {\n        id: action.id,\n        text: action.text,\n        done: false\n      }];\n    }\n    case 'changed': {\n      return tasks.map(t => {\n        if (t.id === action.task.id) {\n          return action.task;\n        } else {\n          return t;\n        }\n      });\n    }\n    case 'deleted': {\n      return tasks.filter(t => t.id !== action.id);\n    }\n    default: {\n      throw Error('Unknown action: ' + action.type);\n    }\n  }\n}\n\nlet nextId = 3;\nconst initialTasks = [\n  { id: 0, text: 'Philosopher’s Path', done: true },\n  { id: 1, text: 'Visit the temple', done: false },\n  { id: 2, text: 'Drink matcha', done: false }\n];\n```\n\n```js src/AddTask.js\nimport { useState } from 'react';\n\nexport default function AddTask({ onAddTask }) {\n  const [text, setText] = useState('');\n  return (\n    <>\n      <input\n        placeholder=\"Add task\"\n        value={text}\n        onChange={e => setText(e.target.value)}\n      />\n      <button onClick={() => {\n        setText('');\n        onAddTask(text);\n      }}>Add</button>\n    </>\n  )\n}\n```\n\n```js src/TaskList.js\nimport { useState } from 'react';\n\nexport default function TaskList({\n  tasks,\n  onChangeTask,\n  onDeleteTask\n}) {\n  return (\n    <ul>\n      {tasks.map(task => (\n        <li key={task.id}>\n          <Task\n            task={task}\n            onChange={onChangeTask}\n            onDelete={onDeleteTask}\n          />\n        </li>\n      ))}\n    </ul>\n  );\n}\n\nfunction Task({ task, onChange, onDelete }) {\n  const [isEditing, setIsEditing] = useState(false);\n  let taskContent;\n  if (isEditing) {\n    taskContent = (\n      <>\n        <input\n          value={task.text}\n          onChange={e => {\n            onChange({\n              ...task,\n              text: e.target.value\n            });\n          }} />\n        <button onClick={() => setIsEditing(false)}>\n          Save\n        </button>\n      </>\n    );\n  } else {\n    taskContent = (\n      <>\n        {task.text}\n        <button onClick={() => setIsEditing(true)}>\n          Edit\n        </button>\n      </>\n    );\n  }\n  return (\n    <label>\n      <input\n        type=\"checkbox\"\n        checked={task.done}\n        onChange={e => {\n          onChange({\n            ...task,\n            done: e.target.checked\n          });\n        }}\n      />\n      {taskContent}\n      <button onClick={() => onDelete(task.id)}>\n        Delete\n      </button>\n    </label>\n  );\n}\n```\n\n```css\nbutton { margin: 5px; }\nli { list-style-type: none; }\nul, li { margin: 0; padding: 0; }\n```\n\n</Sandpack>\n\nReducer는 이벤트 핸들러를 짧고 간결하게 유지하는 데 도움이 됩니다. 그러나 앱이 커지면 다른 어려움에 부딪힐지도 모릅니다. **현재 `tasks` state 및 `dispatch` 함수는 최상위 `TaskApp` 컴포넌트에서만 사용할 수 있습니다.** 다른 컴포넌트가 작업 목록을 읽거나 변경하려면 현재 state와 해당 state를 변경하는 이벤트 핸들러를 명시적으로 [props로 전달](/learn/passing-props-to-a-component)해야 합니다.\n\n예를 들어, `TaskApp`은 `tasks` 리스트와 이벤트 핸들러를 `TaskList`에 전달합니다.\n\n```js\n<TaskList\n  tasks={tasks}\n  onChangeTask={handleChangeTask}\n  onDeleteTask={handleDeleteTask}\n/>\n```\n\n그리고 `TaskList` 컴포넌트에서 `Task` 컴포넌트로 이벤트 핸들러를 전달합니다.\n\n```js\n<Task\n  task={task}\n  onChange={onChangeTask}\n  onDelete={onDeleteTask}\n/>\n```\n\n지금처럼 간단한 예시에서는 잘 동작하지만, 수십 수백개의 컴포넌트를 거쳐 state나 함수를 전달하기는 쉽지 않습니다.\n\n이것이 props를 통한 전달 대신 `tasks` state와 `dispatch` 함수를 모두 [context에 넣고](/learn/passing-data-deeply-with-context) 싶은 이유입니다. **이렇게 하면 트리에서 `TaskApp` 아래에 있는 모든 컴포넌트가 \"prop drilling\"이라는 반복적인 작업 없이 tasks와 dispatch actions를 읽을 수 있습니다.**\n\nReducer와 context를 결합하는 방법은 아래와 같습니다.\n\n1. Context를 **생성합니다**.\n2. State와 dispatch 함수를 context에 **넣습니다**.\n3. 트리 안에서 context를 **사용합니다**.\n\n### 1단계: Context 생성 {/*step-1-create-the-context*/}\n\n`useReducer` Hook은 현재 `tasks`와 업데이트할 수 있는 `dispatch` 함수를 반환합니다.\n\n```js\nconst [tasks, dispatch] = useReducer(tasksReducer, initialTasks);\n```\n\n트리를 통해 전달하려면, 두 개의 별개의 context를 [생성](/learn/passing-data-deeply-with-context#step-2-use-the-context)해야 합니다.\n\n- `TasksContext`는 현재 `tasks` 리스트를 제공합니다.\n- `TasksDispatchContext`는 컴포넌트에서 action을 dispatch 하는 함수를 제공합니다.\n\n두 context는 나중에 다른 파일에서 가져올 수 있도록 별도의 파일에서 내보냅니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { useReducer } from 'react';\nimport AddTask from './AddTask.js';\nimport TaskList from './TaskList.js';\n\nexport default function TaskApp() {\n  const [tasks, dispatch] = useReducer(\n    tasksReducer,\n    initialTasks\n  );\n\n  function handleAddTask(text) {\n    dispatch({\n      type: 'added',\n      id: nextId++,\n      text: text,\n    });\n  }\n\n  function handleChangeTask(task) {\n    dispatch({\n      type: 'changed',\n      task: task\n    });\n  }\n\n  function handleDeleteTask(taskId) {\n    dispatch({\n      type: 'deleted',\n      id: taskId\n    });\n  }\n\n  return (\n    <>\n      <h1>Day off in Kyoto</h1>\n      <AddTask\n        onAddTask={handleAddTask}\n      />\n      <TaskList\n        tasks={tasks}\n        onChangeTask={handleChangeTask}\n        onDeleteTask={handleDeleteTask}\n      />\n    </>\n  );\n}\n\nfunction tasksReducer(tasks, action) {\n  switch (action.type) {\n    case 'added': {\n      return [...tasks, {\n        id: action.id,\n        text: action.text,\n        done: false\n      }];\n    }\n    case 'changed': {\n      return tasks.map(t => {\n        if (t.id === action.task.id) {\n          return action.task;\n        } else {\n          return t;\n        }\n      });\n    }\n    case 'deleted': {\n      return tasks.filter(t => t.id !== action.id);\n    }\n    default: {\n      throw Error('Unknown action: ' + action.type);\n    }\n  }\n}\n\nlet nextId = 3;\nconst initialTasks = [\n  { id: 0, text: 'Philosopher’s Path', done: true },\n  { id: 1, text: 'Visit the temple', done: false },\n  { id: 2, text: 'Drink matcha', done: false }\n];\n```\n\n```js src/TasksContext.js active\nimport { createContext } from 'react';\n\nexport const TasksContext = createContext(null);\nexport const TasksDispatchContext = createContext(null);\n```\n\n```js src/AddTask.js\nimport { useState } from 'react';\n\nexport default function AddTask({ onAddTask }) {\n  const [text, setText] = useState('');\n  return (\n    <>\n      <input\n        placeholder=\"Add task\"\n        value={text}\n        onChange={e => setText(e.target.value)}\n      />\n      <button onClick={() => {\n        setText('');\n        onAddTask(text);\n      }}>Add</button>\n    </>\n  )\n}\n```\n\n```js src/TaskList.js\nimport { useState } from 'react';\n\nexport default function TaskList({\n  tasks,\n  onChangeTask,\n  onDeleteTask\n}) {\n  return (\n    <ul>\n      {tasks.map(task => (\n        <li key={task.id}>\n          <Task\n            task={task}\n            onChange={onChangeTask}\n            onDelete={onDeleteTask}\n          />\n        </li>\n      ))}\n    </ul>\n  );\n}\n\nfunction Task({ task, onChange, onDelete }) {\n  const [isEditing, setIsEditing] = useState(false);\n  let taskContent;\n  if (isEditing) {\n    taskContent = (\n      <>\n        <input\n          value={task.text}\n          onChange={e => {\n            onChange({\n              ...task,\n              text: e.target.value\n            });\n          }} />\n        <button onClick={() => setIsEditing(false)}>\n          Save\n        </button>\n      </>\n    );\n  } else {\n    taskContent = (\n      <>\n        {task.text}\n        <button onClick={() => setIsEditing(true)}>\n          Edit\n        </button>\n      </>\n    );\n  }\n  return (\n    <label>\n      <input\n        type=\"checkbox\"\n        checked={task.done}\n        onChange={e => {\n          onChange({\n            ...task,\n            done: e.target.checked\n          });\n        }}\n      />\n      {taskContent}\n      <button onClick={() => onDelete(task.id)}>\n        Delete\n      </button>\n    </label>\n  );\n}\n```\n\n```css\nbutton { margin: 5px; }\nli { list-style-type: none; }\nul, li { margin: 0; padding: 0; }\n```\n\n</Sandpack>\n\n여기서는 두 context에 모두 기본값으로 `null`을 전달하고 있습니다. 실제 값은 `TaskApp` 컴포넌트에서 제공될 것입니다.\n\n### 2단계: State와 dispatch 함수를 context에 넣기 {/*step-2-put-state-and-dispatch-into-context*/}\n\n이제 `TaskApp` 컴포넌트에서 두 context를 모두 가져올 수 있습니다. `useReducer()`에서 반환된 `tasks` 및 `dispatch`를 가져와 아래 트리 전체에 [제공](/learn/passing-data-deeply-with-context#step-3-provide-the-context)하세요.\n\n```js {4,7-8}\nimport { TasksContext, TasksDispatchContext } from './TasksContext.js';\n\nexport default function TaskApp() {\n  const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);\n  // ...\n  return (\n    <TasksContext value={tasks}>\n      <TasksDispatchContext value={dispatch}>\n        ...\n      </TasksDispatchContext>\n    </TasksContext>\n  );\n}\n```\n\n지금은 props와 context를 모두 이용하여 정보를 전달하고 있습니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { useReducer } from 'react';\nimport AddTask from './AddTask.js';\nimport TaskList from './TaskList.js';\nimport { TasksContext, TasksDispatchContext } from './TasksContext.js';\n\nexport default function TaskApp() {\n  const [tasks, dispatch] = useReducer(\n    tasksReducer,\n    initialTasks\n  );\n\n  function handleAddTask(text) {\n    dispatch({\n      type: 'added',\n      id: nextId++,\n      text: text,\n    });\n  }\n\n  function handleChangeTask(task) {\n    dispatch({\n      type: 'changed',\n      task: task\n    });\n  }\n\n  function handleDeleteTask(taskId) {\n    dispatch({\n      type: 'deleted',\n      id: taskId\n    });\n  }\n\n  return (\n    <TasksContext value={tasks}>\n      <TasksDispatchContext value={dispatch}>\n        <h1>Day off in Kyoto</h1>\n        <AddTask\n          onAddTask={handleAddTask}\n        />\n        <TaskList\n          tasks={tasks}\n          onChangeTask={handleChangeTask}\n          onDeleteTask={handleDeleteTask}\n        />\n      </TasksDispatchContext>\n    </TasksContext>\n  );\n}\n\nfunction tasksReducer(tasks, action) {\n  switch (action.type) {\n    case 'added': {\n      return [...tasks, {\n        id: action.id,\n        text: action.text,\n        done: false\n      }];\n    }\n    case 'changed': {\n      return tasks.map(t => {\n        if (t.id === action.task.id) {\n          return action.task;\n        } else {\n          return t;\n        }\n      });\n    }\n    case 'deleted': {\n      return tasks.filter(t => t.id !== action.id);\n    }\n    default: {\n      throw Error('Unknown action: ' + action.type);\n    }\n  }\n}\n\nlet nextId = 3;\nconst initialTasks = [\n  { id: 0, text: 'Philosopher’s Path', done: true },\n  { id: 1, text: 'Visit the temple', done: false },\n  { id: 2, text: 'Drink matcha', done: false }\n];\n```\n\n```js src/TasksContext.js\nimport { createContext } from 'react';\n\nexport const TasksContext = createContext(null);\nexport const TasksDispatchContext = createContext(null);\n```\n\n```js src/AddTask.js\nimport { useState } from 'react';\n\nexport default function AddTask({ onAddTask }) {\n  const [text, setText] = useState('');\n  return (\n    <>\n      <input\n        placeholder=\"Add task\"\n        value={text}\n        onChange={e => setText(e.target.value)}\n      />\n      <button onClick={() => {\n        setText('');\n        onAddTask(text);\n      }}>Add</button>\n    </>\n  )\n}\n```\n\n```js src/TaskList.js\nimport { useState } from 'react';\n\nexport default function TaskList({\n  tasks,\n  onChangeTask,\n  onDeleteTask\n}) {\n  return (\n    <ul>\n      {tasks.map(task => (\n        <li key={task.id}>\n          <Task\n            task={task}\n            onChange={onChangeTask}\n            onDelete={onDeleteTask}\n          />\n        </li>\n      ))}\n    </ul>\n  );\n}\n\nfunction Task({ task, onChange, onDelete }) {\n  const [isEditing, setIsEditing] = useState(false);\n  let taskContent;\n  if (isEditing) {\n    taskContent = (\n      <>\n        <input\n          value={task.text}\n          onChange={e => {\n            onChange({\n              ...task,\n              text: e.target.value\n            });\n          }} />\n        <button onClick={() => setIsEditing(false)}>\n          Save\n        </button>\n      </>\n    );\n  } else {\n    taskContent = (\n      <>\n        {task.text}\n        <button onClick={() => setIsEditing(true)}>\n          Edit\n        </button>\n      </>\n    );\n  }\n  return (\n    <label>\n      <input\n        type=\"checkbox\"\n        checked={task.done}\n        onChange={e => {\n          onChange({\n            ...task,\n            done: e.target.checked\n          });\n        }}\n      />\n      {taskContent}\n      <button onClick={() => onDelete(task.id)}>\n        Delete\n      </button>\n    </label>\n  );\n}\n```\n\n```css\nbutton { margin: 5px; }\nli { list-style-type: none; }\nul, li { margin: 0; padding: 0; }\n```\n\n</Sandpack>\n\n다음 단계에서 이제 prop을 통한 전달을 제거합니다.\n\n### 3단계: 트리 안에서 context 사용하기 {/*step-3-use-context-anywhere-in-the-tree*/}\n\n이제 `tasks` 리스트나 이벤트 핸들러를 트리 아래로 전달할 필요가 없습니다.\n\n```js {4-5}\n<TasksContext value={tasks}>\n  <TasksDispatchContext value={dispatch}>\n    <h1>Day off in Kyoto</h1>\n    <AddTask />\n    <TaskList />\n  </TasksDispatchContext>\n</TasksContext>\n```\n\n대신 필요한 컴포넌트에서는 `TaskContext`에서 `tasks` 리스트를 읽을 수 있습니다.\n\n```js {2}\nexport default function TaskList() {\n  const tasks = useContext(TasksContext);\n  // ...\n```\n\n`tasks` 리스트를 업데이트하기 위해서 컴포넌트에서 context의 `dispatch` 함수를 읽고 호출할 수 있습니다.\n\n```js {3,9-13}\nexport default function AddTask({ onAddTask }) {\n  const [text, setText] = useState('');\n  const dispatch = useContext(TasksDispatchContext);\n  // ...\n  return (\n    // ...\n    <button onClick={() => {\n      setText('');\n      dispatch({\n        type: 'added',\n        id: nextId++,\n        text: text,\n      });\n    }}>Add</button>\n    // ...\n```\n\n**`TaskApp` 컴포넌트는 어떤 이벤트 핸들러도 아래로 전달하지 않으며, `TaskList`도 `Task` 컴포넌트로 이벤트 핸들러를 전달하지 않습니다.** 각 컴포넌트는 필요한 context를 읽습니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { useReducer } from 'react';\nimport AddTask from './AddTask.js';\nimport TaskList from './TaskList.js';\nimport { TasksContext, TasksDispatchContext } from './TasksContext.js';\n\nexport default function TaskApp() {\n  const [tasks, dispatch] = useReducer(\n    tasksReducer,\n    initialTasks\n  );\n\n  return (\n    <TasksContext value={tasks}>\n      <TasksDispatchContext value={dispatch}>\n        <h1>Day off in Kyoto</h1>\n        <AddTask />\n        <TaskList />\n      </TasksDispatchContext>\n    </TasksContext>\n  );\n}\n\nfunction tasksReducer(tasks, action) {\n  switch (action.type) {\n    case 'added': {\n      return [...tasks, {\n        id: action.id,\n        text: action.text,\n        done: false\n      }];\n    }\n    case 'changed': {\n      return tasks.map(t => {\n        if (t.id === action.task.id) {\n          return action.task;\n        } else {\n          return t;\n        }\n      });\n    }\n    case 'deleted': {\n      return tasks.filter(t => t.id !== action.id);\n    }\n    default: {\n      throw Error('Unknown action: ' + action.type);\n    }\n  }\n}\n\nconst initialTasks = [\n  { id: 0, text: 'Philosopher’s Path', done: true },\n  { id: 1, text: 'Visit the temple', done: false },\n  { id: 2, text: 'Drink matcha', done: false }\n];\n```\n\n```js src/TasksContext.js\nimport { createContext } from 'react';\n\nexport const TasksContext = createContext(null);\nexport const TasksDispatchContext = createContext(null);\n```\n\n```js src/AddTask.js\nimport { useState, useContext } from 'react';\nimport { TasksDispatchContext } from './TasksContext.js';\n\nexport default function AddTask({ onAddTask }) {\n  const [text, setText] = useState('');\n  const dispatch = useContext(TasksDispatchContext);\n  return (\n    <>\n      <input\n        placeholder=\"Add task\"\n        value={text}\n        onChange={e => setText(e.target.value)}\n      />\n      <button onClick={() => {\n        setText('');\n        dispatch({\n          type: 'added',\n          id: nextId++,\n          text: text,\n        });\n      }}>Add</button>\n    </>\n  );\n}\n\nlet nextId = 3;\n```\n\n```js src/TaskList.js active\nimport { useState, useContext } from 'react';\nimport { TasksContext, TasksDispatchContext } from './TasksContext.js';\n\nexport default function TaskList() {\n  const tasks = useContext(TasksContext);\n  return (\n    <ul>\n      {tasks.map(task => (\n        <li key={task.id}>\n          <Task task={task} />\n        </li>\n      ))}\n    </ul>\n  );\n}\n\nfunction Task({ task }) {\n  const [isEditing, setIsEditing] = useState(false);\n  const dispatch = useContext(TasksDispatchContext);\n  let taskContent;\n  if (isEditing) {\n    taskContent = (\n      <>\n        <input\n          value={task.text}\n          onChange={e => {\n            dispatch({\n              type: 'changed',\n              task: {\n                ...task,\n                text: e.target.value\n              }\n            });\n          }} />\n        <button onClick={() => setIsEditing(false)}>\n          Save\n        </button>\n      </>\n    );\n  } else {\n    taskContent = (\n      <>\n        {task.text}\n        <button onClick={() => setIsEditing(true)}>\n          Edit\n        </button>\n      </>\n    );\n  }\n  return (\n    <label>\n      <input\n        type=\"checkbox\"\n        checked={task.done}\n        onChange={e => {\n          dispatch({\n            type: 'changed',\n            task: {\n              ...task,\n              done: e.target.checked\n            }\n          });\n        }}\n      />\n      {taskContent}\n      <button onClick={() => {\n        dispatch({\n          type: 'deleted',\n          id: task.id\n        });\n      }}>\n        Delete\n      </button>\n    </label>\n  );\n}\n```\n\n```css\nbutton { margin: 5px; }\nli { list-style-type: none; }\nul, li { margin: 0; padding: 0; }\n```\n\n</Sandpack>\n\n**State는 여전히 최상위 `TaskApp` 컴포넌트에서 `useReducer`로 관리되고 있습니다.** 그러나 이제 context를 가져와 트리 아래의 모든 컴포넌트에서 해당 `tasks` 및 `dispatch`를 사용할 수 있습니다.\n\n## 하나의 파일로 합치기 {/*moving-all-wiring-into-a-single-file*/}\n\n반드시 이런 방식으로 작성하지 않아도 되지만, reducer와 context를 모두 하나의 파일에 작성하면 컴포넌트들을 조금 더 정리할 수 있습니다. 현재, `TasksContext.js`는 두 개의 context만을 선언하고 있습니다.\n\n```js\nimport { createContext } from 'react';\n\nexport const TasksContext = createContext(null);\nexport const TasksDispatchContext = createContext(null);\n```\n\n이제 이 파일이 좀 더 복잡해질 예정입니다. Reducer를 같은 파일로 옮기고 `TasksProvider` 컴포넌트를 새로 선언합니다. 이 컴포넌트는 모든 것을 하나로 묶는 역할을 하게 됩니다.\n\n1. Reducer로 state를 관리합니다.\n2. 두 context를 모두 하위 컴포넌트에 제공합니다.\n3. [`children`을 prop으로](/learn/passing-props-to-a-component#passing-jsx-as-children) 받기 때문에 JSX를 전달할 수 있습니다.\n\n```js\nexport function TasksProvider({ children }) {\n  const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);\n\n  return (\n    <TasksContext value={tasks}>\n      <TasksDispatchContext value={dispatch}>\n        {children}\n      </TasksDispatchContext>\n    </TasksContext>\n  );\n}\n```\n\n**이렇게 하면 `TaskApp` 컴포넌트의 복잡성과 연결이 모두 제거됩니다.**\n\n<Sandpack>\n\n```js src/App.js\nimport AddTask from './AddTask.js';\nimport TaskList from './TaskList.js';\nimport { TasksProvider } from './TasksContext.js';\n\nexport default function TaskApp() {\n  return (\n    <TasksProvider>\n      <h1>Day off in Kyoto</h1>\n      <AddTask />\n      <TaskList />\n    </TasksProvider>\n  );\n}\n```\n\n```js src/TasksContext.js\nimport { createContext, useReducer } from 'react';\n\nexport const TasksContext = createContext(null);\nexport const TasksDispatchContext = createContext(null);\n\nexport function TasksProvider({ children }) {\n  const [tasks, dispatch] = useReducer(\n    tasksReducer,\n    initialTasks\n  );\n\n  return (\n    <TasksContext value={tasks}>\n      <TasksDispatchContext value={dispatch}>\n        {children}\n      </TasksDispatchContext>\n    </TasksContext>\n  );\n}\n\nfunction tasksReducer(tasks, action) {\n  switch (action.type) {\n    case 'added': {\n      return [...tasks, {\n        id: action.id,\n        text: action.text,\n        done: false\n      }];\n    }\n    case 'changed': {\n      return tasks.map(t => {\n        if (t.id === action.task.id) {\n          return action.task;\n        } else {\n          return t;\n        }\n      });\n    }\n    case 'deleted': {\n      return tasks.filter(t => t.id !== action.id);\n    }\n    default: {\n      throw Error('Unknown action: ' + action.type);\n    }\n  }\n}\n\nconst initialTasks = [\n  { id: 0, text: 'Philosopher’s Path', done: true },\n  { id: 1, text: 'Visit the temple', done: false },\n  { id: 2, text: 'Drink matcha', done: false }\n];\n```\n\n```js src/AddTask.js\nimport { useState, useContext } from 'react';\nimport { TasksDispatchContext } from './TasksContext.js';\n\nexport default function AddTask({ onAddTask }) {\n  const [text, setText] = useState('');\n  const dispatch = useContext(TasksDispatchContext);\n  return (\n    <>\n      <input\n        placeholder=\"Add task\"\n        value={text}\n        onChange={e => setText(e.target.value)}\n      />\n      <button onClick={() => {\n        setText('');\n        dispatch({\n          type: 'added',\n          id: nextId++,\n          text: text,\n        });\n      }}>Add</button>\n    </>\n  );\n}\n\nlet nextId = 3;\n```\n\n```js src/TaskList.js\nimport { useState, useContext } from 'react';\nimport { TasksContext, TasksDispatchContext } from './TasksContext.js';\n\nexport default function TaskList() {\n  const tasks = useContext(TasksContext);\n  return (\n    <ul>\n      {tasks.map(task => (\n        <li key={task.id}>\n          <Task task={task} />\n        </li>\n      ))}\n    </ul>\n  );\n}\n\nfunction Task({ task }) {\n  const [isEditing, setIsEditing] = useState(false);\n  const dispatch = useContext(TasksDispatchContext);\n  let taskContent;\n  if (isEditing) {\n    taskContent = (\n      <>\n        <input\n          value={task.text}\n          onChange={e => {\n            dispatch({\n              type: 'changed',\n              task: {\n                ...task,\n                text: e.target.value\n              }\n            });\n          }} />\n        <button onClick={() => setIsEditing(false)}>\n          Save\n        </button>\n      </>\n    );\n  } else {\n    taskContent = (\n      <>\n        {task.text}\n        <button onClick={() => setIsEditing(true)}>\n          Edit\n        </button>\n      </>\n    );\n  }\n  return (\n    <label>\n      <input\n        type=\"checkbox\"\n        checked={task.done}\n        onChange={e => {\n          dispatch({\n            type: 'changed',\n            task: {\n              ...task,\n              done: e.target.checked\n            }\n          });\n        }}\n      />\n      {taskContent}\n      <button onClick={() => {\n        dispatch({\n          type: 'deleted',\n          id: task.id\n        });\n      }}>\n        Delete\n      </button>\n    </label>\n  );\n}\n```\n\n```css\nbutton { margin: 5px; }\nli { list-style-type: none; }\nul, li { margin: 0; padding: 0; }\n```\n\n</Sandpack>\n\n`TasksContext.js`에서 context를 사용하기 위한 _use_ 함수들도 내보낼 수 있습니다.\n\n```js\nexport function useTasks() {\n  return useContext(TasksContext);\n}\n\nexport function useTasksDispatch() {\n  return useContext(TasksDispatchContext);\n}\n```\n\n이 함수를 사용하여 컴포넌트에서 context를 읽을 수 있습니다.\n\n```js {5-7}\nconst tasks = useTasks();\nconst dispatch = useTasksDispatch();\n```\n\n이렇게 하면 동작이 바뀌는 건 아니지만, 다음에 context를 더 분리하거나 함수들에 로직을 추가하기 쉬워집니다. **이제 모든 context와 reducer는 `TasksContext.js`에 있습니다. 이렇게 컴포넌트들이 데이터를 어디서 가져오는지가 아닌 무엇을 보여줄 것인지에 집중할 수 있도록 깨끗하게 정리할 수 있습니다.**\n\n<Sandpack>\n\n```js src/App.js\nimport AddTask from './AddTask.js';\nimport TaskList from './TaskList.js';\nimport { TasksProvider } from './TasksContext.js';\n\nexport default function TaskApp() {\n  return (\n    <TasksProvider>\n      <h1>Day off in Kyoto</h1>\n      <AddTask />\n      <TaskList />\n    </TasksProvider>\n  );\n}\n```\n\n```js src/TasksContext.js\nimport { createContext, useContext, useReducer } from 'react';\n\nconst TasksContext = createContext(null);\n\nconst TasksDispatchContext = createContext(null);\n\nexport function TasksProvider({ children }) {\n  const [tasks, dispatch] = useReducer(\n    tasksReducer,\n    initialTasks\n  );\n\n  return (\n    <TasksContext value={tasks}>\n      <TasksDispatchContext value={dispatch}>\n        {children}\n      </TasksDispatchContext>\n    </TasksContext>\n  );\n}\n\nexport function useTasks() {\n  return useContext(TasksContext);\n}\n\nexport function useTasksDispatch() {\n  return useContext(TasksDispatchContext);\n}\n\nfunction tasksReducer(tasks, action) {\n  switch (action.type) {\n    case 'added': {\n      return [...tasks, {\n        id: action.id,\n        text: action.text,\n        done: false\n      }];\n    }\n    case 'changed': {\n      return tasks.map(t => {\n        if (t.id === action.task.id) {\n          return action.task;\n        } else {\n          return t;\n        }\n      });\n    }\n    case 'deleted': {\n      return tasks.filter(t => t.id !== action.id);\n    }\n    default: {\n      throw Error('Unknown action: ' + action.type);\n    }\n  }\n}\n\nconst initialTasks = [\n  { id: 0, text: 'Philosopher’s Path', done: true },\n  { id: 1, text: 'Visit the temple', done: false },\n  { id: 2, text: 'Drink matcha', done: false }\n];\n```\n\n```js src/AddTask.js\nimport { useState } from 'react';\nimport { useTasksDispatch } from './TasksContext.js';\n\nexport default function AddTask({ onAddTask }) {\n  const [text, setText] = useState('');\n  const dispatch = useTasksDispatch();\n  return (\n    <>\n      <input\n        placeholder=\"Add task\"\n        value={text}\n        onChange={e => setText(e.target.value)}\n      />\n      <button onClick={() => {\n        setText('');\n        dispatch({\n          type: 'added',\n          id: nextId++,\n          text: text,\n        });\n      }}>Add</button>\n    </>\n  );\n}\n\nlet nextId = 3;\n```\n\n```js src/TaskList.js active\nimport { useState } from 'react';\nimport { useTasks, useTasksDispatch } from './TasksContext.js';\n\nexport default function TaskList() {\n  const tasks = useTasks();\n  return (\n    <ul>\n      {tasks.map(task => (\n        <li key={task.id}>\n          <Task task={task} />\n        </li>\n      ))}\n    </ul>\n  );\n}\n\nfunction Task({ task }) {\n  const [isEditing, setIsEditing] = useState(false);\n  const dispatch = useTasksDispatch();\n  let taskContent;\n  if (isEditing) {\n    taskContent = (\n      <>\n        <input\n          value={task.text}\n          onChange={e => {\n            dispatch({\n              type: 'changed',\n              task: {\n                ...task,\n                text: e.target.value\n              }\n            });\n          }} />\n        <button onClick={() => setIsEditing(false)}>\n          Save\n        </button>\n      </>\n    );\n  } else {\n    taskContent = (\n      <>\n        {task.text}\n        <button onClick={() => setIsEditing(true)}>\n          Edit\n        </button>\n      </>\n    );\n  }\n  return (\n    <label>\n      <input\n        type=\"checkbox\"\n        checked={task.done}\n        onChange={e => {\n          dispatch({\n            type: 'changed',\n            task: {\n              ...task,\n              done: e.target.checked\n            }\n          });\n        }}\n      />\n      {taskContent}\n      <button onClick={() => {\n        dispatch({\n          type: 'deleted',\n          id: task.id\n        });\n      }}>\n        Delete\n      </button>\n    </label>\n  );\n}\n```\n\n```css\nbutton { margin: 5px; }\nli { list-style-type: none; }\nul, li { margin: 0; padding: 0; }\n```\n\n</Sandpack>\n\n`TasksProvider`는 tasks를 화면의 한 부분으로 tasks를 관리합니다. `useTasks`로 tasks를 읽을 수 있고, `useTasksDispatch`로 컴포넌트들에서 tasks를 업데이트 할 수 있습니다.\n\n> `useTasks`와 `useTasksDispatch` 같은 함수들을 **[사용자 정의 Hook](/learn/reusing-logic-with-custom-hooks)이라고 합니다.** 이름이 `use`로 시작되는 함수들은 사용자 정의 Hook입니다. 사용자 정의 Hook 안에서도 `useContext` 등 다른 Hook을 사용할 수 있습니다.\n\n앱이 커질수록 context-reducer 조합이 더 많아질 겁니다. 앱을 확장하고 큰 노력 없이 트리 아래에서 데이터에 접근할 수 있도록 [state를 끌어올리기](/learn/sharing-state-between-components) 위한 강력한 방법이기 때문입니다.\n\n<Recap>\n\n- Reducer와 context를 결합해서 컴포넌트가 상위 state를 읽고 수정할 수 있도록 할 수 있습니다.\n- State와 dispatch 함수를 하위 컴포넌트들에 제공하는 방법\n  1. 두 개의 context를 만듭니다(각각 state와 dispatch 함수를 위한 것).\n  2. Reducer를 사용하는 컴포넌트에 두 context를 모두 제공합니다.\n  3. 하위 컴포넌트들에서 필요한 context를 사용합니다.\n- 더 나아가 하나의 파일로 합쳐서 컴포넌트들을 정리할 수 있습니다.\n  - Context를 제공하는 `TasksProvider` 같은 컴포넌트를 내보낼 수 있습니다.\n  - 바로 사용할 수 있도록 `useTasks`와 `useTasksDispatch` 같은 사용자 Hook을 내보낼 수 있습니다.\n- context-reducer 조합을 앱에 여러 개 만들 수 있습니다.\n\n</Recap>\n"
  },
  {
    "path": "src/content/learn/separating-events-from-effects.md",
    "content": "---\ntitle: 'Effect에서 이벤트 분리하기'\n---\n\n<Intro>\n\n이벤트 핸들러는 같은 상호작용을 반복하는 경우에만 재실행됩니다. Effect는 이벤트 핸들러와 달리 prop이나 state 변수 등 읽은 값이 마지막 렌더링 때와 다르면 다시 동기화합니다. 때로는 두 동작이 섞여서 어떤 값에는 반응해 재실행되지만, 다른 값에는 그러지 않는 Effect를 원할 때도 있습니다. 이 페이지에서 그 방법을 알려드리겠습니다.\n\n</Intro>\n\n<YouWillLearn>\n\n- 이벤트 핸들러와 Effect 중에 선택하는 방법\n- Effect는 반응형이고 이벤트 핸들러는 아닌 이유\n- Effect의 코드 일부가 반응형이 아니길 원한다면 해야 할 것\n- Effect 이벤트의 정의와 Effect에서 추출하는 방법\n- Effect 이벤트를 사용해 Effect에서 최근의 props와 state를 읽는 방법\n\n</YouWillLearn>\n\n## 이벤트 핸들러와 Effect 중에 선택하기 {/*choosing-between-event-handlers-and-effects*/}\n\n먼저 이벤트 핸들러와 Effect의 차이점에 대해 간단히 알아보겠습니다.\n\n채팅방 컴포넌트를 구현한다고 상상해 보세요. 요구사항은 아래와 같습니다.\n\n1. 채팅방 컴포넌트는 선택된 채팅방에 자동으로 연결해야 합니다.\n2. \"전송\" 버튼을 클릭하면 채팅에 메시지를 전송해야 합니다.\n\n코드를 이미 구현했다고 하겠습니다. 그런데 그 코드를 어디에 넣어야 할지 확실하지 않습니다. 이벤트 핸들러와 Effect 중에 무엇을 사용해야 할까요? 이 질문에 답해야 할 때마다 [해당 코드가 실행되어야 하는 *이유*](/learn/synchronizing-with-effects#what-are-effects-and-how-are-they-different-from-events)를 고려해 보세요.\n\n### 이벤트 핸들러는 특정 상호작용에 대한 응답으로 실행된다 {/*event-handlers-run-in-response-to-specific-interactions*/}\n\n사용자 관점에서 메시지는 \"전송\" 버튼이 클릭 되었기 *때문에* 전송되어야 합니다. 다른 때나 다른 이유로 메시지가 전송되면 사용자는 꽤 당황할 것입니다. 그러므로 메시지를 전송하는 건 이벤트 핸들러가 되어야 합니다. 이벤트 핸들러는 특정 상호작용을 처리하게 해줍니다.\n\n```js {4-6}\nfunction ChatRoom({ roomId }) {\n  const [message, setMessage] = useState('');\n  // ...\n  function handleSendClick() {\n    sendMessage(message);\n  }\n  // ...\n  return (\n    <>\n      <input value={message} onChange={e => setMessage(e.target.value)} />\n      <button onClick={handleSendClick}>전송</button>\n    </>\n  );\n}\n```\n\n이벤트 핸들러를 사용하면 사용자가 버튼을 누를 *때만* `sendMessage(message)`가 실행될 것이라고 확신할 수 있습니다.\n\n### Effect는 동기화가 필요할 때마다 실행된다 {/*effects-run-whenever-synchronization-is-needed*/}\n\n채팅방 컴포넌트는 채팅방과의 연결을 유지해야 한다는 요구사항도 떠올려 보세요. 이 코드는 어디에 넣어야 할까요?\n\n이 코드를 실행하는 *이유*는 어떠한 특정 상호작용이 아닙니다. 사용자가 채팅방 화면으로 이동한 이유나 방법은 상관없습니다. 사용자가 현재 채팅방 화면을 보고 상호작용할 수 있으므로 컴포넌트는 선택된 채팅 서버에 계속 연결되어 있어야 합니다. 채팅방 컴포넌트가 앱의 첫 화면이고 사용자가 아무런 상호작용을 하지 않은 경우라 해도 *여전히* 연결되어 있어야 합니다. 그러므로 이 코드는 Effect입니다.\n\n```js {3-9}\nfunction ChatRoom({ roomId }) {\n  // ...\n  useEffect(() => {\n    const connection = createConnection(serverUrl, roomId);\n    connection.connect();\n    return () => {\n      connection.disconnect();\n    };\n  }, [roomId]);\n  // ...\n}\n```\n\n이렇게 코드를 작성하면 사용자가 수행하는 특정 상호작용에 *상관없이* 현재 선택된 채팅 서버와 항상 연결된 상태임을 확신할 수 있습니다. 사용자가 앱을 열기만 했든 다른 방을 선택했든 다른 화면으로 이동했다가 다시 돌아왔든, 컴포넌트가 현재 선택된 방과 *동기화된 상태를 유지*할 것이고 [필요할 때마다 다시 연결](/learn/lifecycle-of-reactive-effects#why-synchronization-may-need-to-happen-more-than-once)할 것을 Effect가 보장합니다.\n\n<Sandpack>\n\n```js\nimport { useState, useEffect } from 'react';\nimport { createConnection, sendMessage } from './chat.js';\n\nconst serverUrl = 'https://localhost:1234';\n\nfunction ChatRoom({ roomId }) {\n  const [message, setMessage] = useState('');\n\n  useEffect(() => {\n    const connection = createConnection(serverUrl, roomId);\n    connection.connect();\n    return () => connection.disconnect();\n  }, [roomId]);\n\n  function handleSendClick() {\n    sendMessage(message);\n  }\n\n  return (\n    <>\n      <h1>{roomId} 방에 오신 것을 환영합니다!</h1>\n      <input value={message} onChange={e => setMessage(e.target.value)} />\n      <button onClick={handleSendClick}>전송</button>\n    </>\n  );\n}\n\nexport default function App() {\n  const [roomId, setRoomId] = useState('general');\n  const [show, setShow] = useState(false);\n  return (\n    <>\n      <label>\n        채팅방 선택:{' '}\n        <select\n          value={roomId}\n          onChange={e => setRoomId(e.target.value)}\n        >\n          <option value=\"general\">general</option>\n          <option value=\"travel\">travel</option>\n          <option value=\"music\">music</option>\n        </select>\n      </label>\n      <button onClick={() => setShow(!show)}>\n        {show ? '채팅 닫기' : '채팅 열기'}\n      </button>\n      {show && <hr />}\n      {show && <ChatRoom roomId={roomId} />}\n    </>\n  );\n}\n```\n\n```js src/chat.js\nexport function sendMessage(message) {\n  console.log('🔵 전송한 메시지: ' + message);\n}\n\nexport function createConnection(serverUrl, roomId) {\n  // 실제 구현은 실제로 서버에 연결했을 것입니다.\n  return {\n    connect() {\n      console.log('✅ ' + serverUrl + '의 \"' + roomId + '\" 방에 연결 중...');\n    },\n    disconnect() {\n      console.log('❌ ' + serverUrl + '의 \"' + roomId + '\" 방과 연결 해제');\n    }\n  };\n}\n```\n\n```css\ninput, select { margin-right: 20px; }\n```\n\n</Sandpack>\n\n## 반응형 값과 반응형 로직 {/*reactive-values-and-reactive-logic*/}\n\n이벤트 핸들러는 버튼 클릭과 같이 항상 \"수동으로\" 트리거 되지만, Effect는 동기화 유지에 필요한 만큼 자주 실행 및 재실행되기 때문에 \"자동으로\" 트리거 된다고 직감적으로 말할 수도 있습니다.\n\n이에 대해 더 정확하게 생각하는 방법이 있습니다.\n\n컴포넌트 본문 내부에 선언된 props, state, 변수를 <CodeStep step={2}>반응형 값</CodeStep>이라고 합니다. 이 예시에서 `serverUrl`은 반응형 값이 아니지만 `roomId`와 `message`는 반응형 값입니다. 반응형 값은 데이터 렌더링 과정에 관여합니다.\n\n```js [[2, 3, \"roomId\"], [2, 4, \"message\"]]\nconst serverUrl = 'https://localhost:1234';\n\nfunction ChatRoom({ roomId }) {\n  const [message, setMessage] = useState('');\n\n  // ...\n}\n```\n\n이러한 반응형 값은 리렌더링으로 인해 변경될 수 있습니다. 예를 들어 사용자가 `message`를 편집하거나 드롭다운에서 다른 `roomId`를 선택하는 경우가 있습니다. 이벤트 핸들러와 Effect는 변화에 다르게 반응합니다.\n\n- **이벤트 핸들러 내부의 로직은 *반응형*이 아닙니다**. 사용자가 같은 상호작용(예: 클릭)을 반복하지 않는 한 재실행되지 않습니다. 이벤트 핸들러는 변화에 \"반응\"하지 않으면서 반응형 값을 읽을 수 있습니다.\n- **Effect 내부의 로직은 *반응형*입니다.** Effect에서 반응형 값을 읽는 경우 [그 값을 의존성으로 지정해야 합니다.](/learn/lifecycle-of-reactive-effects#effects-react-to-reactive-values) 그렇게 하면 리렌더링이 그 값을 바꾸는 경우 React가 새로운 값으로 Effect의 로직을 다시 실행합니다.\n\n이 차이를 설명하기 위해 이전 예시를 다시 보겠습니다.\n\n### 이벤트 핸들러 내부의 로직은 반응형이 아니다 {/*logic-inside-event-handlers-is-not-reactive*/}\n\n아래의 코드 라인을 보세요. 이 로직이 반응형이어야 할까요, 아닐까요?\n\n```js [[2, 2, \"message\"]]\n    // ...\n    sendMessage(message);\n    // ...\n```\n\n사용자 관점에서 **`message`를 바꾸는 것이 메시지를 전송하고 싶다는 의미는 _아닙니다._** 사용자가 입력 중이라는 의미일 뿐입니다. 즉 메시지를 전송하는 로직은 반응형이어서는 안 됩니다. <CodeStep step={2}>반응형 값</CodeStep>이 변경되었다는 이유만으로 로직이 재실행되어서는 안 됩니다. 그러므로 이 로직은 이벤트 핸들러에 속합니다.\n\n```js {2}\n  function handleSendClick() {\n    sendMessage(message);\n  }\n```\n\n이벤트 핸들러는 반응형이 아니므로 `sendMessage(message)`는 사용자가 전송 버튼을 클릭할 때만 실행될 것입니다.\n\n### Effect 내부의 로직은 반응형이다 {/*logic-inside-effects-is-reactive*/}\n\n이제 아래의 코드 라인으로 돌아가 봅시다.\n\n```js [[2, 2, \"roomId\"]]\n    // ...\n    const connection = createConnection(serverUrl, roomId);\n    connection.connect();\n    // ...\n```\n\n사용자 관점에서 **`roomId`를 바꾸는 것은 다른 방에 연결하고 싶다는 의미입니다.** 즉 방에 연결하기 위한 로직은 반응형이어야 합니다. 우리는 이 코드가 <CodeStep step={2}>반응형 값</CodeStep>을 \"따라가고\" 그 값이 바뀌면 다시 실행되기를 원합니다. 그러므로 이 로직은 Effect에 속합니다.\n\n```js {2-3}\n  useEffect(() => {\n    const connection = createConnection(serverUrl, roomId);\n    connection.connect();\n    return () => {\n      connection.disconnect()\n    };\n  }, [roomId]);\n```\n\nEffect는 반응형이므로 `createConnection(serverUrl, roomId)`와 `connection.connect()`는 구별되는 모든 `roomId` 값에 대해 실행될 겁니다. Effect는 채팅 연결과 현재 선택된 방의 동기화를 유지해 줍니다.\n\n## Effect에서 비반응형 로직 추출하기 {/*extracting-non-reactive-logic-out-of-effects*/}\n\n반응형 로직과 비반응형 로직을 섞으려 한다면 더 까다로워집니다.\n\n예를 들어 사용자가 채팅에 연결할 때 알림을 보여주는 상황을 상상해 보세요. 올바른 색상의 알림을 보여주기 위해 props로부터 현재 테마(dark 또는 light)를 읽습니다.\n\n```js {1,4-6}\nfunction ChatRoom({ roomId, theme }) {\n  useEffect(() => {\n    const connection = createConnection(serverUrl, roomId);\n    connection.on('connected', () => {\n      showNotification('연결됨!', theme);\n    });\n    connection.connect();\n    // ...\n```\n\n그러나 `theme`은 (리렌더링으로 변경될 수 있는) 반응형 값이고 [Effect가 읽는 모든 반응형 값은 의존성으로 선언되어야 합니다.](/learn/lifecycle-of-reactive-effects#react-verifies-that-you-specified-every-reactive-value-as-a-dependency) 그러므로 `theme`을 Effect의 의존성으로 지정해야 합니다.\n\n```js {5,11}\nfunction ChatRoom({ roomId, theme }) {\n  useEffect(() => {\n    const connection = createConnection(serverUrl, roomId);\n    connection.on('connected', () => {\n      showNotification('연결됨!', theme);\n    });\n    connection.connect();\n    return () => {\n      connection.disconnect()\n    };\n  }, [roomId, theme]); // ✅ 모든 의존성 선언됨\n  // ...\n```\n\n이 예시로 이것저것 해보면서 사용자 경험상의 문제를 발견할 수 있을지 확인해 보세요.\n\n<Sandpack>\n\n```json package.json hidden\n{\n  \"dependencies\": {\n    \"react\": \"latest\",\n    \"react-dom\": \"latest\",\n    \"react-scripts\": \"latest\",\n    \"toastify-js\": \"1.12.0\"\n  },\n  \"scripts\": {\n    \"start\": \"react-scripts start\",\n    \"build\": \"react-scripts build\",\n    \"test\": \"react-scripts test --env=jsdom\",\n    \"eject\": \"react-scripts eject\"\n  }\n}\n```\n\n```js\nimport { useState, useEffect } from 'react';\nimport { createConnection, sendMessage } from './chat.js';\nimport { showNotification } from './notifications.js';\n\nconst serverUrl = 'https://localhost:1234';\n\nfunction ChatRoom({ roomId, theme }) {\n  useEffect(() => {\n    const connection = createConnection(serverUrl, roomId);\n    connection.on('connected', () => {\n      showNotification('연결됨!', theme);\n    });\n    connection.connect();\n    return () => connection.disconnect();\n  }, [roomId, theme]);\n\n  return <h1>{roomId} 방에 오신 것을 환영합니다!</h1>\n}\n\nexport default function App() {\n  const [roomId, setRoomId] = useState('general');\n  const [isDark, setIsDark] = useState(false);\n  return (\n    <>\n      <label>\n        채팅방 선택:{' '}\n        <select\n          value={roomId}\n          onChange={e => setRoomId(e.target.value)}\n        >\n          <option value=\"general\">general</option>\n          <option value=\"travel\">travel</option>\n          <option value=\"music\">music</option>\n        </select>\n      </label>\n      <label>\n        <input\n          type=\"checkbox\"\n          checked={isDark}\n          onChange={e => setIsDark(e.target.checked)}\n        />\n        어두운 테마 사용\n      </label>\n      <hr />\n      <ChatRoom\n        roomId={roomId}\n        theme={isDark ? 'dark' : 'light'}\n      />\n    </>\n  );\n}\n```\n\n```js src/chat.js\nexport function createConnection(serverUrl, roomId) {\n  // 실제 구현은 실제로 서버에 연결했을 것입니다.\n  let connectedCallback;\n  let timeout;\n  return {\n    connect() {\n      timeout = setTimeout(() => {\n        if (connectedCallback) {\n          connectedCallback();\n        }\n      }, 100);\n    },\n    on(event, callback) {\n      if (connectedCallback) {\n        throw Error('핸들러는 두 번 추가할 수 없습니다.');\n      }\n      if (event !== 'connected') {\n        throw Error('\"connected\" 이벤트만 지원됩니다.');\n      }\n      connectedCallback = callback;\n    },\n    disconnect() {\n      clearTimeout(timeout);\n    }\n  };\n}\n```\n\n```js src/notifications.js\nimport Toastify from 'toastify-js';\nimport 'toastify-js/src/toastify.css';\n\nexport function showNotification(message, theme) {\n  Toastify({\n    text: message,\n    duration: 2000,\n    gravity: 'top',\n    position: 'right',\n    style: {\n      background: theme === 'dark' ? 'black' : 'white',\n      color: theme === 'dark' ? 'white' : 'black',\n    },\n  }).showToast();\n}\n```\n\n```css\nlabel { display: block; margin-top: 10px; }\n```\n\n</Sandpack>\n\n`roomId`가 변경되면 채팅은 예상대로 다시 연결됩니다. 하지만 `theme`도 의존성이므로 dark 테마와 light 테마 사이를 전환할 때마다 채팅도 다시 연결됩니다. 좋지 않습니다!\n\n다시 말해 아래의 코드 라인이 비록 (반응형인) Effect 내부에 있지만 반응형이 *아니길* 바랍니다.\n\n```js\n      // ...\n      showNotification('연결됨!', theme);\n      // ...\n```\n\n이 비반응형 로직을 주변의 반응형 Effect로부터 분리할 방법이 필요합니다.\n\n### Effect 이벤트 선언하기 {/*declaring-an-effect-event*/}\n\n[`useEffectEvent`](/reference/react/useEffectEvent)라는 특별한 Hook을 사용하여 Effect에서 비반응형 로직을 추출하세요.\n\n```js {1,4-6}\nimport { useEffect, useEffectEvent } from 'react';\n\nfunction ChatRoom({ roomId, theme }) {\n  const onConnected = useEffectEvent(() => {\n    showNotification('연결됨!', theme);\n  });\n  // ...\n```\n\n여기서 `onConnected`를 *Effect 이벤트*라고 합니다. Effect 로직의 일부이지만 이벤트 핸들러와 훨씬 비슷하게 동작합니다. 내부의 로직은 반응형이 아니며 항상 props와 state의 최근 값을 \"바라봅니다\".\n\n이제 Effect 내부에서 Effect 이벤트인 `onConnected`를 호출할 수 있습니다.\n\n```js {2-4,9,13}\nfunction ChatRoom({ roomId, theme }) {\n  const onConnected = useEffectEvent(() => {\n    showNotification('연결됨!', theme);\n  });\n\n  useEffect(() => {\n    const connection = createConnection(serverUrl, roomId);\n    connection.on('connected', () => {\n      onConnected();\n    });\n    connection.connect();\n    return () => connection.disconnect();\n  }, [roomId]); // ✅ 모든 의존성이 선언됨\n  // ...\n```\n\n이렇게 하면 문제가 해결됩니다. Effect에서 더 이상 사용하지 않으므로, Effect의 의존성 목록에서 `theme`을 *제거*해야 한다는 점에 유의하세요. 또한 `onConnected`를 *추가*할 필요도 없습니다. **Effect 이벤트는 반응형이 아니므로 의존성에서 제외되어야 합니다.**\n\n새로운 동작이 예상대로 작동하는지 확인해 보세요.\n\n<Sandpack>\n\n```json package.json hidden\n{\n  \"dependencies\": {\n    \"react\": \"latest\",\n    \"react-dom\": \"latest\",\n    \"react-scripts\": \"latest\",\n    \"toastify-js\": \"1.12.0\"\n  },\n  \"scripts\": {\n    \"start\": \"react-scripts start\",\n    \"build\": \"react-scripts build\",\n    \"test\": \"react-scripts test --env=jsdom\",\n    \"eject\": \"react-scripts eject\"\n  }\n}\n```\n\n```js\nimport { useState, useEffect } from 'react';\nimport { useEffectEvent } from 'react';\nimport { createConnection, sendMessage } from './chat.js';\nimport { showNotification } from './notifications.js';\n\nconst serverUrl = 'https://localhost:1234';\n\nfunction ChatRoom({ roomId, theme }) {\n  const onConnected = useEffectEvent(() => {\n    showNotification('연결됨!', theme);\n  });\n\n  useEffect(() => {\n    const connection = createConnection(serverUrl, roomId);\n    connection.on('connected', () => {\n      onConnected();\n    });\n    connection.connect();\n    return () => connection.disconnect();\n  }, [roomId]);\n\n  return <h1>{roomId} 방에 오신 것을 환영합니다!</h1>\n}\n\nexport default function App() {\n  const [roomId, setRoomId] = useState('general');\n  const [isDark, setIsDark] = useState(false);\n  return (\n    <>\n      <label>\n        채팅방 선택:{' '}\n        <select\n          value={roomId}\n          onChange={e => setRoomId(e.target.value)}\n        >\n          <option value=\"general\">general</option>\n          <option value=\"travel\">travel</option>\n          <option value=\"music\">music</option>\n        </select>\n      </label>\n      <label>\n        <input\n          type=\"checkbox\"\n          checked={isDark}\n          onChange={e => setIsDark(e.target.checked)}\n        />\n        어두운 테마 사용\n      </label>\n      <hr />\n      <ChatRoom\n        roomId={roomId}\n        theme={isDark ? 'dark' : 'light'}\n      />\n    </>\n  );\n}\n```\n\n```js src/chat.js\nexport function createConnection(serverUrl, roomId) {\n  // 실제 구현은 실제로 서버에 연결했을 것입니다.\n  let connectedCallback;\n  let timeout;\n  return {\n    connect() {\n      timeout = setTimeout(() => {\n        if (connectedCallback) {\n          connectedCallback();\n        }\n      }, 100);\n    },\n    on(event, callback) {\n      if (connectedCallback) {\n        throw Error('핸들러는 두 번 추가할 수 없습니다.');\n      }\n      if (event !== 'connected') {\n        throw Error('\"connected\" 이벤트만 지원됩니다.');\n      }\n      connectedCallback = callback;\n    },\n    disconnect() {\n      clearTimeout(timeout);\n    }\n  };\n}\n```\n\n```js src/notifications.js hidden\nimport Toastify from 'toastify-js';\nimport 'toastify-js/src/toastify.css';\n\nexport function showNotification(message, theme) {\n  Toastify({\n    text: message,\n    duration: 2000,\n    gravity: 'top',\n    position: 'right',\n    style: {\n      background: theme === 'dark' ? 'black' : 'white',\n      color: theme === 'dark' ? 'white' : 'black',\n    },\n  }).showToast();\n}\n```\n\n```css\nlabel { display: block; margin-top: 10px; }\n```\n\n</Sandpack>\n\nEffect 이벤트가 이벤트 핸들러와 아주 비슷하다고 생각할 수 있습니다. 이벤트 핸들러는 사용자의 상호작용에 대한 응답으로 실행되는 반면에 Effect 이벤트는 Effect에서 직접 트리거 된다는 것이 주요한 차이점입니다. Effect 이벤트를 사용하면 Effect의 반응성과 반응형이어서는 안 되는 코드 사이의 \"연결을 끊어줍니다\".\n\n### Effect 이벤트로 최근 props와 state 읽기 {/*reading-latest-props-and-state-with-effect-events*/}\n\nEffect 이벤트는 의존성 린터를 억제하고 싶은 충동이 드는 많은 패턴을 해결할 수 있게 해줍니다.\n\n예를 들어 페이지 방문을 기록하기 위한 Effect가 있다고 해보겠습니다.\n\n```js\nfunction Page() {\n  useEffect(() => {\n    logVisit();\n  }, []);\n  // ...\n}\n```\n\n이후 사이트에 여러 경로가 추가되고 이제 `Page` 컴포넌트는 현재 경로가 담긴 `url`을 prop으로 받습니다. `logVisit`에 `url`을 전달하여 호출하려는데 의존성 린터가 불평합니다.\n\n```js {1,3}\nfunction Page({ url }) {\n  useEffect(() => {\n    logVisit(url);\n  }, []); // 🔴 React Hook useEffect의 의존성 'url'이 누락되었습니다.\n  // ...\n}\n```\n\n이 코드로 무엇을 하려는 것인지 생각해 보세요. 각 URL은 서로 다른 페이지를 나타내므로 각 URL에 대한 방문을 *따로 기록하려 합니다*. 즉 이 `logVisit` 호출은 `url`에 반응형*이어야 합니다*. 그러므로 이런 경우에는 의존성 린터의 말을 따라 `url`을 의존성으로 추가하는 것이 합리적입니다.\n\n```js {4}\nfunction Page({ url }) {\n  useEffect(() => {\n    logVisit(url);\n  }, [url]); // ✅ 모든 의존성이 선언됨\n  // ...\n}\n```\n\n이제 모든 페이지 방문기록에 장바구니의 물건 개수도 포함하려 한다고 해보겠습니다.\n\n```js {2-3,6}\nfunction Page({ url }) {\n  const { items } = useContext(ShoppingCartContext);\n  const numberOfItems = items.length;\n\n  useEffect(() => {\n    logVisit(url, numberOfItems);\n  }, [url]); // 🔴 React Hook useEffect의 의존성 'numberOfItems'가 누락되었습니다.\n  // ...\n}\n```\n\nEffect 내부에서 `numberOfItems`를 사용했으므로 린터는 이를 의존성에 추가해달라고 부탁합니다. 하지만 `logVisit` 호출이 `numberOfItems`에 반응하지 *않길* 원합니다. 사용자가 장바구니에 무언가를 넣어 `numberOfItems`가 변경되는 것이 사용자가 페이지를 다시 방문했음을 *의미하지는 않습니다*. 즉 *페이지 방문*은 어떤 의미에서 \"이벤트\"입니다. 이 이벤트는 특정한 시점에 발생합니다.\n\n코드를 두 부분으로 나눠보세요.\n\n```js {5-7,10}\nfunction Page({ url }) {\n  const { items } = useContext(ShoppingCartContext);\n  const numberOfItems = items.length;\n\n  const onVisit = useEffectEvent(visitedUrl => {\n    logVisit(visitedUrl, numberOfItems);\n  });\n\n  useEffect(() => {\n    onVisit(url);\n  }, [url]); // ✅ 모든 의존성 선언됨\n  // ...\n}\n```\n\n여기서 `onVisit`은 Effect 이벤트입니다. 그 내부의 코드는 반응형이 아닙니다. 그러므로 `numberOfItems` (또는 다른 반응형 값!)의 변경이 주변 코드를 재실행시킬 걱정 없이 사용할 수 있습니다.\n\n반면에 Effect 자체는 여전히 반응형입니다. Effect 내부의 코드는 prop인 `url`을 사용하므로 다른 `url`로 리렌더링 될 때마다 Effect가 재실행됩니다. 그로 인해 Effect 이벤트인 `onVisit`가 호출될 것입니다.\n\n결과적으로 prop인 `url` 변경될 때마다 `logVisit`을 호출할 것이고 항상 최근의 `numberOfItems`를 읽을 것입니다. 하지만 `numberOfItems` 혼자만 변경되면 어떠한 코드도 재실행되지 않습니다.\n\n<Note>\n\n인수 없이 `onVisit()`을 호출하고 그 내부에서 `url`을 읽을 수 있는지 궁금할 수도 있습니다.\n\n```js {2,6}\n  const onVisit = useEffectEvent(() => {\n    logVisit(url, numberOfItems);\n  });\n\n  useEffect(() => {\n    onVisit();\n  }, [url]);\n```\n\n이렇게 해도 읽을 수 있지만 `url`을 Effect 이벤트에 명시적으로 전달하는 것이 좋습니다. **`url`을 Effect 이벤트에 인수로 전달함으로써 다른 `url`로 페이지를 방문하는 것이 사용자 관점에서는 별도의 \"이벤트\"임을 나타내는 것입니다.** `visitedUrl`은 발생한 \"이벤트\"의 *일부분*입니다.\n\n```js {1-2,6}\n  const onVisit = useEffectEvent(visitedUrl => {\n    logVisit(visitedUrl, numberOfItems);\n  });\n\n  useEffect(() => {\n    onVisit(url);\n  }, [url]);\n```\n\nEffect 이벤트가 `visitedUrl`을 명시적으로 \"요구\"하므로 `url`을 Effect의 의존성에서 실수로 제거하는 일은 이제 있을 수 없습니다. 의존성에서 `url`을 제거하면 (별개의 페이지 방문을 하나로 취급하게 되는데) 린터가 경고할 것입니다. `onVisit`이 `url`에 반응하기를 원하므로 `url`을 (반응형이 아닌) `onVisit` 내부에서 읽지 말고 Effect에서 전달해 줍니다.\n\n이것은 Effect 내부에 비동기 로직이 있는 경우에 특히 중요해집니다.\n\n```js {6,8}\n  const onVisit = useEffectEvent(visitedUrl => {\n    logVisit(visitedUrl, numberOfItems);\n  });\n\n  useEffect(() => {\n    setTimeout(() => {\n      onVisit(url);\n    }, 5000); // 방문 기록을 지연시킴\n  }, [url]);\n```\n\n여기서 `onVisit` 내부의 `url`은 (이미 변경되었을 수 있는) *최근의* `url`에 해당하지만 `visitedUrl`은 최초에 이 Effect (및 `onVisit` 호출)을 실행하게 만든 `url`에 해당합니다.\n\n</Note>\n\n<DeepDive>\n\n#### 대안으로 의존성 린터를 억제하는 것은 괜찮은가요? {/*is-it-okay-to-suppress-the-dependency-linter-instead*/}\n\n기존 코드베이스에서는 아래와 같이 린트 규칙이 억제된 것을 가끔 볼 수 있습니다.\n\n```js {7-9}\nfunction Page({ url }) {\n  const { items } = useContext(ShoppingCartContext);\n  const numberOfItems = items.length;\n\n  useEffect(() => {\n    logVisit(url, numberOfItems);\n    // 🔴 이런 식으로 린터를 억제하는 것은 피하세요.\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [url]);\n  // ...\n}\n```\n\n린터를 **절대로 억제하지 않는 것**을 권장합니다.\n\n규칙을 억제하는 것의 첫 번째 단점은 코드에 추가한 새로운 반응형 의존성에 Effect가 \"반응\"해야 할 때 React가 더 이상 경고하지 않는다는 것입니다. 이전 예시에서는 React가 의존성에 `url`을 추가하라고 상기시켜 주었기 *때문에* 그렇게 했습니다. 린터를 억제하면 해당 Effect에 대한 향후 편집에 대해 이러한 알림을 더 이상 받지 않게 됩니다. 이는 버그로 이어집니다.\n\n다음은 린터를 억제하여 발생하는 혼란스러운 버그의 예시입니다. 이 예시에서 `handleMove` 함수는 점이 커서를 따라가야 하는지를 결정하기 위해 state 변수 `canMove`의 현재 값을 읽어야 합니다. 그러나 `handleMove` 내부에서 `canMove`는 항상 `true`입니다.\n\n왜 그런지 알겠나요?\n\n<Sandpack>\n\n```js\nimport { useState, useEffect } from 'react';\n\nexport default function App() {\n  const [position, setPosition] = useState({ x: 0, y: 0 });\n  const [canMove, setCanMove] = useState(true);\n\n  function handleMove(e) {\n    if (canMove) {\n      setPosition({ x: e.clientX, y: e.clientY });\n    }\n  }\n\n  useEffect(() => {\n    window.addEventListener('pointermove', handleMove);\n    return () => window.removeEventListener('pointermove', handleMove);\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, []);\n\n  return (\n    <>\n      <label>\n        <input type=\"checkbox\"\n          checked={canMove}\n          onChange={e => setCanMove(e.target.checked)}\n        />\n        점 움직이게 하기\n      </label>\n      <hr />\n      <div style={{\n        position: 'absolute',\n        backgroundColor: 'pink',\n        borderRadius: '50%',\n        opacity: 0.6,\n        transform: `translate(${position.x}px, ${position.y}px)`,\n        pointerEvents: 'none',\n        left: -20,\n        top: -20,\n        width: 40,\n        height: 40,\n      }} />\n    </>\n  );\n}\n```\n\n```css\nbody {\n  height: 200px;\n}\n```\n\n</Sandpack>\n\n\n이 코드의 문제는 린터를 억제한다는 것입니다. 억제하는 것을 제거하면 이 Effect가 `handleMove` 함수에 의존해야 함을 알게 될 것입니다. `handleMove`는 컴포넌트 본문 내부에서 선언되어서 반응형 값이기 때문입니다. 모든 반응형 값은 의존성으로 지정되어야 하며 그렇지 않으면 시간이 지남에 따라 오래되어 최근 값과 달라질 가능성이 있습니다!\n\n기존 코드의 작성자는 Effect가 반응형 값에 의존하지 않는다고(`[]`) React에 \"거짓말\"을 했습니다. 그러므로 React는 `canMove`가 (`handleMove`와 함께) 변경된 후에 Effect를 다시 동기화하지 않았습니다. React가 Effect를 다시 동기화하지 않았기 때문에 리스너로 부착된 `handleMove`는 초기 렌더링 과정에서 생성된 `handleMove` 함수입니다. 초기 렌더링 과정에서 `canMove`가 `true`였으므로 초기 렌더링 과정에서 생성된 `handleMove`는 영원히 `true`를 바라보게 됩니다.\n\n**린터를 억제하지 않으면 오래된 값으로 인한 문제가 절대 발생하지 않습니다.**\n\n`useEffectEvent`를 사용하면 린터에 \"거짓말\"을 할 필요가 없으며 코드는 기대한 대로 동작합니다.\n\n<Sandpack>\n\n```js\nimport { useState, useEffect } from 'react';\nimport { useEffectEvent } from 'react';\n\nexport default function App() {\n  const [position, setPosition] = useState({ x: 0, y: 0 });\n  const [canMove, setCanMove] = useState(true);\n\n  const onMove = useEffectEvent(e => {\n    if (canMove) {\n      setPosition({ x: e.clientX, y: e.clientY });\n    }\n  });\n\n  useEffect(() => {\n    window.addEventListener('pointermove', onMove);\n    return () => window.removeEventListener('pointermove', onMove);\n  }, []);\n\n  return (\n    <>\n      <label>\n        <input type=\"checkbox\"\n          checked={canMove}\n          onChange={e => setCanMove(e.target.checked)}\n        />\n        점 움직이게 하기\n      </label>\n      <hr />\n      <div style={{\n        position: 'absolute',\n        backgroundColor: 'pink',\n        borderRadius: '50%',\n        opacity: 0.6,\n        transform: `translate(${position.x}px, ${position.y}px)`,\n        pointerEvents: 'none',\n        left: -20,\n        top: -20,\n        width: 40,\n        height: 40,\n      }} />\n    </>\n  );\n}\n```\n\n```css\nbody {\n  height: 200px;\n}\n```\n\n</Sandpack>\n\n`useEffectEvent`가 *항상* 올바른 해결책이라는 의미는 아닙니다. `useEffectEvent`는 반응형이 아니길 원하는 코드 라인에만 적용해야 합니다. 위의 샌드박스에서는 Effect의 코드가 `canMove`에 반응하길 원하지 않았습니다. 그러므로 Effect 이벤트로 추출하는 것이 합리적이었습니다.\n\n린터 억제의 다른 올바른 대안에 대해서는 [Effect 의존성 제거하기](/learn/removing-effect-dependencies)를 읽어보세요.\n\n</DeepDive>\n\n### Effect 이벤트의 한계 {/*limitations-of-effect-events*/}\n\nEffect 이벤트는 사용 방법에 매우 제한적입니다.\n\n* **Effect 내부에서만 호출하세요.**\n* **절대로 다른 컴포넌트나 Hook에 전달하지 마세요.**\n\n예를 들어 아래와 같이 Effect 이벤트를 선언하고 전달하지 마세요.\n\n```js {4-6,8}\nfunction Timer() {\n  const [count, setCount] = useState(0);\n\n  const onTick = useEffectEvent(() => {\n    setCount(count + 1);\n  });\n\n  useTimer(onTick, 1000); // 🔴 금지: Effect 이벤트 전달하기\n\n  return <h1>{count}</h1>\n}\n\nfunction useTimer(callback, delay) {\n  useEffect(() => {\n    const id = setInterval(() => {\n      callback();\n    }, delay);\n    return () => {\n      clearInterval(id);\n    };\n  }, [delay, callback]); // 의존성에 \"callback\"을 지정해야 함\n}\n```\n\n그 대신 Effect 이벤트는 항상 자신을 사용하는 Effect의 바로 근처에 선언하세요.\n\n```js {10-12,16,21}\nfunction Timer() {\n  const [count, setCount] = useState(0);\n  useTimer(() => {\n    setCount(count + 1);\n  }, 1000);\n  return <h1>{count}</h1>\n}\n\nfunction useTimer(callback, delay) {\n  const onTick = useEffectEvent(() => {\n    callback();\n  });\n\n  useEffect(() => {\n    const id = setInterval(() => {\n      onTick(); // ✅ 바람직함: Effect 내부에서 지역적으로만 호출됨\n    }, delay);\n    return () => {\n      clearInterval(id);\n    };\n  }, [delay]); // \"onTick\"(Effect 이벤트)를 의존성으로 지정할 필요 없음\n}\n```\n\nEffect 이벤트는 Effect의 코드 중 비반응형인 \"부분\"입니다. Effect 이벤트는 자신을 사용하는 Effect 근처에 있어야 합니다.\n\n<Recap>\n\n- 이벤트 핸들러는 특정 상호작용에 대한 응답으로 실행됩니다.\n- Effect는 동기화가 필요할 때마다 실행됩니다.\n- 이벤트 핸들러 내부의 로직은 반응형이 아닙니다.\n- Effect 내부의 로직은 반응형입니다.\n- Effect의 비반응형 로직은 Effect 이벤트로 옮길 수 있습니다.\n- Effect 이벤트는 Effect 내부에서만 호출하세요.\n- Effect 이벤트를 다른 컴포넌트나 Hook에 전달하지 마세요.\n\n</Recap>\n\n<Challenges>\n\n#### 업데이트되지 않는 변수 고치기 {/*fix-a-variable-that-doesnt-update*/}\n\n아래의 `Timer` 컴포넌트에는 매초 증가하는 state 변수 `count`가 있습니다. 증가량은 state 변수 `increment`에 저장됩니다. 변수 `increment`는 더하기와 빼기 버튼으로 제어할 수 있습니다.\n\n하지만 더하기 버튼을 아무리 많이 클릭해도 카운터는 여전히 매초 1씩 증가합니다. 이 코드는 무엇이 잘못되었을까요? Effect의 코드 내부에서 `increment`는 왜 항상 `1`일까요? 실수를 찾아 고쳐보세요.\n\n<Hint>\n\n이 코드를 고치려면 규칙을 따르는 것으로 충분합니다.\n\n</Hint>\n\n<Sandpack>\n\n```js\nimport { useState, useEffect } from 'react';\n\nexport default function Timer() {\n  const [count, setCount] = useState(0);\n  const [increment, setIncrement] = useState(1);\n\n  useEffect(() => {\n    const id = setInterval(() => {\n      setCount(c => c + increment);\n    }, 1000);\n    return () => {\n      clearInterval(id);\n    };\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, []);\n\n  return (\n    <>\n      <h1>\n        카운터: {count}\n        <button onClick={() => setCount(0)}>재설정</button>\n      </h1>\n      <hr />\n      <p>\n        초당 증가량:\n        <button disabled={increment === 0} onClick={() => {\n          setIncrement(i => i - 1);\n        }}>–</button>\n        <b>{increment}</b>\n        <button onClick={() => {\n          setIncrement(i => i + 1);\n        }}>+</button>\n      </p>\n    </>\n  );\n}\n```\n\n```css\nbutton { margin: 10px; }\n```\n\n</Sandpack>\n\n<Solution>\n\nEffect의 버그를 찾을 때는 늘 그렇듯 억제된 린터 규칙이 있는지 찾는 것부터 시작하세요.\n\n린터를 억제하는 주석을 제거하면 React는 이 Effect의 코드가 `increment`에 의존한다고 알려줄 것입니다. 하지만 여러분은 이 Effect가 어떠한 반응형 값에도 의존하지 않는다고(`[]`) 함으로써 React에 \"거짓말\"을 했습니다. 의존성 배열에 `increment`를 추가하세요.\n\n<Sandpack>\n\n```js\nimport { useState, useEffect } from 'react';\n\nexport default function Timer() {\n  const [count, setCount] = useState(0);\n  const [increment, setIncrement] = useState(1);\n\n  useEffect(() => {\n    const id = setInterval(() => {\n      setCount(c => c + increment);\n    }, 1000);\n    return () => {\n      clearInterval(id);\n    };\n  }, [increment]);\n\n  return (\n    <>\n      <h1>\n        카운터: {count}\n        <button onClick={() => setCount(0)}>재설정</button>\n      </h1>\n      <hr />\n      <p>\n        초당 증가량:\n        <button disabled={increment === 0} onClick={() => {\n          setIncrement(i => i - 1);\n        }}>–</button>\n        <b>{increment}</b>\n        <button onClick={() => {\n          setIncrement(i => i + 1);\n        }}>+</button>\n      </p>\n    </>\n  );\n}\n```\n\n```css\nbutton { margin: 10px; }\n```\n\n</Sandpack>\n\n이제 `increment`가 변경되면 React는 Effect를 다시 동기화시킬 것이고 그로 인해 interval은 재시작될 것입니다.\n\n</Solution>\n\n#### 멈추는 카운터 고치기 {/*fix-a-freezing-counter*/}\n\n아래의 `Timer` 컴포넌트에는 매초 증가하는 state 변수 `count`가 있습니다. 증가량은 state 변수 `increment`에 저장되며 더하기와 빼기 버튼으로 제어할 수 있습니다. 예를 들어 더하기 버튼을 9번 누르면 `count`가 이제 매초 1이 아닌 10씩 증가하는 것을 확인할 수 있습니다.\n\n이 사용자 인터페이스에는 작은 문제가 있습니다. 더하기 또는 빼기 버튼을 초당 한 번보다 빠르게 계속 누르면 타이머 자체가 잠시 멈춘 것처럼 보입니다. 타이머는 마지막으로 버튼을 누른 후 1초가 지나야 다시 시작됩니다. 타이머가 중단되지 않고 *매초* tick 하도록 이 현상의 원인을 찾고 문제를 해결하세요.\n\n<Hint>\n\n타이머를 설정하는 Effect가 `increment` 값에 \"반응\"하는 것으로 보입니다. `setCount`를 호출하려고 현재의 `increment` 값을 사용하는 코드 라인이 정말 반응형이어야 할까요?\n\n</Hint>\n\n<Sandpack>\n\n```js\nimport { useState, useEffect } from 'react';\nimport { useEffectEvent } from 'react';\n\nexport default function Timer() {\n  const [count, setCount] = useState(0);\n  const [increment, setIncrement] = useState(1);\n\n  useEffect(() => {\n    const id = setInterval(() => {\n      setCount(c => c + increment);\n    }, 1000);\n    return () => {\n      clearInterval(id);\n    };\n  }, [increment]);\n\n  return (\n    <>\n      <h1>\n        카운터: {count}\n        <button onClick={() => setCount(0)}>재설정</button>\n      </h1>\n      <hr />\n      <p>\n        초당 증가량:\n        <button disabled={increment === 0} onClick={() => {\n          setIncrement(i => i - 1);\n        }}>–</button>\n        <b>{increment}</b>\n        <button onClick={() => {\n          setIncrement(i => i + 1);\n        }}>+</button>\n      </p>\n    </>\n  );\n}\n```\n\n```css\nbutton { margin: 10px; }\n```\n\n</Sandpack>\n\n<Solution>\n\nEffect 내부의 코드가 state 변수 `increment`를 사용하는 것이 문제입니다. Effect가 `increment`에 의존하므로 `increment`가 변경될 때마다 Effect가 다시 동기화되고 그로 인해 interval이 clear 됩니다. 타이머가 시작되려고 할 때마다 매번 interval을 clear 하면 타이머가 멈춘 것처럼 보일 것입니다.\n\n이 문제를 해결하려면 Effect에서 Effect 이벤트를 `onTick`으로 추출하세요.\n\n<Sandpack>\n\n```js\nimport { useState, useEffect } from 'react';\nimport { useEffectEvent } from 'react';\n\nexport default function Timer() {\n  const [count, setCount] = useState(0);\n  const [increment, setIncrement] = useState(1);\n\n  const onTick = useEffectEvent(() => {\n    setCount(c => c + increment);\n  });\n\n  useEffect(() => {\n    const id = setInterval(() => {\n      onTick();\n    }, 1000);\n    return () => {\n      clearInterval(id);\n    };\n  }, []);\n\n  return (\n    <>\n      <h1>\n        카운터: {count}\n        <button onClick={() => setCount(0)}>재설정</button>\n      </h1>\n      <hr />\n      <p>\n        초당 증가량:\n        <button disabled={increment === 0} onClick={() => {\n          setIncrement(i => i - 1);\n        }}>–</button>\n        <b>{increment}</b>\n        <button onClick={() => {\n          setIncrement(i => i + 1);\n        }}>+</button>\n      </p>\n    </>\n  );\n}\n```\n\n\n```css\nbutton { margin: 10px; }\n```\n\n</Sandpack>\n\n`onTick`은 Effect 이벤트이므로 내부의 코드는 반응형이 아닙니다. `increment`가 변해도 Effect를 트리거 하지 않습니다.\n\n</Solution>\n\n#### 조정할 수 없는 딜레이 고치기 {/*fix-a-non-adjustable-delay*/}\n\n이 예시에서는 지연 시간인 interval을 사용자화할 수 있습니다. interval은 state 변수 `delay`에 저장되어 있고 두 개의 버튼으로 업데이트됩니다. 그러나 `delay`가 1000밀리초(즉 1초)가 될 때까지 \"+100 ms\" 버튼을 눌러도 타이머가 여전히 매우 빠르게(100밀리초마다) 증가하는 것을 알 수 있습니다. 마치 `delay`의 변화가 무시되는 것 같습니다. 버그를 찾아 고치세요.\n\n<Hint>\n\nEffect 이벤트 내부의 코드는 반응형이 아닙니다. `setInterval` 호출이 재실행되길 _원할_ 경우가 있을까요?\n\n</Hint>\n\n<Sandpack>\n\n```js\nimport { useState, useEffect } from 'react';\nimport { useEffectEvent } from 'react';\n\nexport default function Timer() {\n  const [count, setCount] = useState(0);\n  const [increment, setIncrement] = useState(1);\n  const [delay, setDelay] = useState(100);\n\n  const onTick = useEffectEvent(() => {\n    setCount(c => c + increment);\n  });\n\n  const onMount = useEffectEvent(() => {\n    return setInterval(() => {\n      onTick();\n    }, delay);\n  });\n\n  useEffect(() => {\n    const id = onMount();\n    return () => {\n      clearInterval(id);\n    }\n  }, []);\n\n  return (\n    <>\n      <h1>\n        카운터: {count}\n        <button onClick={() => setCount(0)}>재설정</button>\n      </h1>\n      <hr />\n      <p>\n        증가량:\n        <button disabled={increment === 0} onClick={() => {\n          setIncrement(i => i - 1);\n        }}>–</button>\n        <b>{increment}</b>\n        <button onClick={() => {\n          setIncrement(i => i + 1);\n        }}>+</button>\n      </p>\n      <p>\n        증가 지연 시간:\n        <button disabled={delay === 100} onClick={() => {\n          setDelay(d => d - 100);\n        }}>–100 ms</button>\n        <b>{delay} ms</b>\n        <button onClick={() => {\n          setDelay(d => d + 100);\n        }}>+100 ms</button>\n      </p>\n    </>\n  );\n}\n```\n\n\n```css\nbutton { margin: 10px; }\n```\n\n</Sandpack>\n\n<Solution>\n\n위 예시의 문제는 코드가 실제로 해야 하는 일을 고려하지 않고 `onMount`라는 Effect 이벤트로 추출했다는 것입니다. Effect 이벤트는 코드 일부를 비반응형으로 만들고 싶다는 특정한 이유가 있을 때만 추출해야 합니다. 하지만 `setInterval` 호출은 state 변수 `delay`에 *반응해야 합니다*. `delay`가 변경되면 interval이 다시 설정되기를 원하는 겁니다! 이 코드를 고치려면 모든 반응형 코드를 Effect 내부로 다시 가져오세요.\n\n<Sandpack>\n\n```js\nimport { useState, useEffect } from 'react';\nimport { useEffectEvent } from 'react';\n\nexport default function Timer() {\n  const [count, setCount] = useState(0);\n  const [increment, setIncrement] = useState(1);\n  const [delay, setDelay] = useState(100);\n\n  const onTick = useEffectEvent(() => {\n    setCount(c => c + increment);\n  });\n\n  useEffect(() => {\n    const id = setInterval(() => {\n      onTick();\n    }, delay);\n    return () => {\n      clearInterval(id);\n    }\n  }, [delay]);\n\n  return (\n    <>\n      <h1>\n        카운터: {count}\n        <button onClick={() => setCount(0)}>재설정</button>\n      </h1>\n      <hr />\n      <p>\n        증가량:\n        <button disabled={increment === 0} onClick={() => {\n          setIncrement(i => i - 1);\n        }}>–</button>\n        <b>{increment}</b>\n        <button onClick={() => {\n          setIncrement(i => i + 1);\n        }}>+</button>\n      </p>\n      <p>\n        증가 지연 시간:\n        <button disabled={delay === 100} onClick={() => {\n          setDelay(d => d - 100);\n        }}>–100 ms</button>\n        <b>{delay} ms</b>\n        <button onClick={() => {\n          setDelay(d => d + 100);\n        }}>+100 ms</button>\n      </p>\n    </>\n  );\n}\n```\n\n```css\nbutton { margin: 10px; }\n```\n\n</Sandpack>\n\n코드의 *목적*보다는 *타이밍*에 초점을 두는 `onMount` 같은 함수는 보통 의심해 봐야 합니다. 언뜻 보기에 \"더 잘 설명한다\"라고 느낄 수 있지만 의도를 모호하게 합니다. 경험상 Effect 이벤트는 *사용자* 관점에서 일어나는 일에 부합해야 합니다. 예를 들어 `onMessage`, `onTick`, `onVisit` 또는 `onConnected`는 Effect 이벤트의 이름으로 좋습니다. 내부의 코드는 반응형일 필요가 없을 가능성이 높습니다. 반면에 `onMount`, `onUpdate`, `onUnmount` 또는 `onAfterRender`는 너무 일반적이어서 *반응형이어야 하는* 코드를 실수로 넣기 쉽습니다. 그러므로 Effect 이벤트의 이름은 코드가 실행된 시점이 아니라 *사용자가 일어났다고 생각하는 일*을 따서 지어야 합니다.\n\n</Solution>\n\n#### 지연된 알림 고치기 {/*fix-a-delayed-notification*/}\n\n이 컴포넌트는 채팅방에 참여하면 알림을 보여줍니다. 하지만 알림을 바로 보여주지는 않습니다. 대신 의도적으로 2초 정도 지연시켜서 사용자가 UI를 둘러볼 수 있도록 합니다.\n\n대부분 동작하지만, 버그가 있습니다. 드롭다운을 \"general\"에서 \"travel\"로 변경한 다음 \"music\"으로 아주 빠르게 변경해 보세요. 2초 안에 변경하면 (기대한 대로!) 두 개의 알림이 보이지만 *둘 다* \"music에 오신 것을 환영합니다\"라고 합니다.\n\n\"general\"에서 \"travel\"로 전환한 다음 \"music\"으로 매우 빠르게 전환할 때 첫 번째 알림은 \"travel에 오신 것을 환영합니다\"이고 두 번째 알림은 \"music에 오신 것을 환영합니다\"가 되도록 고쳐보세요. (추가 도전으로 *이미* 알림이 올바른 방을 보여주도록 만들었다면 나중의 알림만 보여주도록 코드를 바꿔보세요.)\n\n<Hint>\n\nEffect는 자신이 어느 방에 연결했는지 알고 있습니다. Effect 이벤트에 전달하고 싶을 만한 정보는 없나요?\n\n</Hint>\n\n<Sandpack>\n\n```json package.json hidden\n{\n  \"dependencies\": {\n    \"react\": \"latest\",\n    \"react-dom\": \"latest\",\n    \"react-scripts\": \"latest\",\n    \"toastify-js\": \"1.12.0\"\n  },\n  \"scripts\": {\n    \"start\": \"react-scripts start\",\n    \"build\": \"react-scripts build\",\n    \"test\": \"react-scripts test --env=jsdom\",\n    \"eject\": \"react-scripts eject\"\n  }\n}\n```\n\n```js\nimport { useState, useEffect } from 'react';\nimport { useEffectEvent } from 'react';\nimport { createConnection, sendMessage } from './chat.js';\nimport { showNotification } from './notifications.js';\n\nconst serverUrl = 'https://localhost:1234';\n\nfunction ChatRoom({ roomId, theme }) {\n  const onConnected = useEffectEvent(() => {\n    showNotification(roomId + '에 오신 것을 환영합니다', theme);\n  });\n\n  useEffect(() => {\n    const connection = createConnection(serverUrl, roomId);\n    connection.on('connected', () => {\n      setTimeout(() => {\n        onConnected();\n      }, 2000);\n    });\n    connection.connect();\n    return () => connection.disconnect();\n  }, [roomId]);\n\n  return <h1>{roomId} 방에 오신 것을 환영합니다!</h1>\n}\n\nexport default function App() {\n  const [roomId, setRoomId] = useState('general');\n  const [isDark, setIsDark] = useState(false);\n  return (\n    <>\n      <label>\n        채팅방 선택:{' '}\n        <select\n          value={roomId}\n          onChange={e => setRoomId(e.target.value)}\n        >\n          <option value=\"general\">general</option>\n          <option value=\"travel\">travel</option>\n          <option value=\"music\">music</option>\n        </select>\n      </label>\n      <label>\n        <input\n          type=\"checkbox\"\n          checked={isDark}\n          onChange={e => setIsDark(e.target.checked)}\n        />\n        어두운 테마 사용\n      </label>\n      <hr />\n      <ChatRoom\n        roomId={roomId}\n        theme={isDark ? 'dark' : 'light'}\n      />\n    </>\n  );\n}\n```\n\n```js src/chat.js\nexport function createConnection(serverUrl, roomId) {\n  // 실제 구현은 실제로 서버에 연결했을 것입니다.\n  let connectedCallback;\n  let timeout;\n  return {\n    connect() {\n      timeout = setTimeout(() => {\n        if (connectedCallback) {\n          connectedCallback();\n        }\n      }, 100);\n    },\n    on(event, callback) {\n      if (connectedCallback) {\n        throw Error('핸들러는 두 번 추가할 수 없습니다.');\n      }\n      if (event !== 'connected') {\n        throw Error('\"connected\" 이벤트만 지원됩니다.');\n      }\n      connectedCallback = callback;\n    },\n    disconnect() {\n      clearTimeout(timeout);\n    }\n  };\n}\n```\n\n```js src/notifications.js hidden\nimport Toastify from 'toastify-js';\nimport 'toastify-js/src/toastify.css';\n\nexport function showNotification(message, theme) {\n  Toastify({\n    text: message,\n    duration: 2000,\n    gravity: 'top',\n    position: 'right',\n    style: {\n      background: theme === 'dark' ? 'black' : 'white',\n      color: theme === 'dark' ? 'white' : 'black',\n    },\n  }).showToast();\n}\n```\n\n```css\nlabel { display: block; margin-top: 10px; }\n```\n\n</Sandpack>\n\n<Solution>\n\nEffect 이벤트 내부의 `roomId`는 *Effect 이벤트가 호출되는 시점*의 값입니다.\n\nEffect 이벤트는 2초의 지연 후에 호출됩니다. travel 방에서 music 방으로 빠르게 전환하는 경우 travel 방의 알림을 보여줄 때쯤이면 `roomId`는 이미 `\"music\"`입니다. 그러므로 두 알림 모두 \"music에 오신 것을 환영합니다\"를 보여줍니다.\n\n이 문제를 고치려면 Effect 이벤트 내부에서 *최근의* `roomId`를 읽는 게 아니라 아래의 `connectedRoomId`처럼 Effect 이벤트의 매개변수로 만드세요. 그다음 Effect에서 `onConnected(roomId)`로 호출해서 `roomId`를 전달하세요.\n\n<Sandpack>\n\n```json package.json hidden\n{\n  \"dependencies\": {\n    \"react\": \"latest\",\n    \"react-dom\": \"latest\",\n    \"react-scripts\": \"latest\",\n    \"toastify-js\": \"1.12.0\"\n  },\n  \"scripts\": {\n    \"start\": \"react-scripts start\",\n    \"build\": \"react-scripts build\",\n    \"test\": \"react-scripts test --env=jsdom\",\n    \"eject\": \"react-scripts eject\"\n  }\n}\n```\n\n```js\nimport { useState, useEffect } from 'react';\nimport { useEffectEvent } from 'react';\nimport { createConnection, sendMessage } from './chat.js';\nimport { showNotification } from './notifications.js';\n\nconst serverUrl = 'https://localhost:1234';\n\nfunction ChatRoom({ roomId, theme }) {\n  const onConnected = useEffectEvent(connectedRoomId => {\n    showNotification(connectedRoomId + '에 오신 것을 환영합니다', theme);\n  });\n\n  useEffect(() => {\n    const connection = createConnection(serverUrl, roomId);\n    connection.on('connected', () => {\n      setTimeout(() => {\n        onConnected(roomId);\n      }, 2000);\n    });\n    connection.connect();\n    return () => connection.disconnect();\n  }, [roomId]);\n\n  return <h1>{roomId} 방에 오신 것을 환영합니다!</h1>\n}\n\nexport default function App() {\n  const [roomId, setRoomId] = useState('general');\n  const [isDark, setIsDark] = useState(false);\n  return (\n    <>\n      <label>\n        채팅방 선택:{' '}\n        <select\n          value={roomId}\n          onChange={e => setRoomId(e.target.value)}\n        >\n          <option value=\"general\">general</option>\n          <option value=\"travel\">travel</option>\n          <option value=\"music\">music</option>\n        </select>\n      </label>\n      <label>\n        <input\n          type=\"checkbox\"\n          checked={isDark}\n          onChange={e => setIsDark(e.target.checked)}\n        />\n        어두운 테마 사용\n      </label>\n      <hr />\n      <ChatRoom\n        roomId={roomId}\n        theme={isDark ? 'dark' : 'light'}\n      />\n    </>\n  );\n}\n```\n\n```js src/chat.js\nexport function createConnection(serverUrl, roomId) {\n  // 실제 구현은 실제로 서버에 연결했을 것입니다.\n  let connectedCallback;\n  let timeout;\n  return {\n    connect() {\n      timeout = setTimeout(() => {\n        if (connectedCallback) {\n          connectedCallback();\n        }\n      }, 100);\n    },\n    on(event, callback) {\n      if (connectedCallback) {\n        throw Error('핸들러는 두 번 추가할 수 없습니다.');\n      }\n      if (event !== 'connected') {\n        throw Error('\"connected\" 이벤트만 지원됩니다.');\n      }\n      connectedCallback = callback;\n    },\n    disconnect() {\n      clearTimeout(timeout);\n    }\n  };\n}\n```\n\n```js src/notifications.js hidden\nimport Toastify from 'toastify-js';\nimport 'toastify-js/src/toastify.css';\n\nexport function showNotification(message, theme) {\n  Toastify({\n    text: message,\n    duration: 2000,\n    gravity: 'top',\n    position: 'right',\n    style: {\n      background: theme === 'dark' ? 'black' : 'white',\n      color: theme === 'dark' ? 'white' : 'black',\n    },\n  }).showToast();\n}\n```\n\n```css\nlabel { display: block; margin-top: 10px; }\n```\n\n</Sandpack>\n\n`roomId`가 `\"travel\"`로 설정된 (그래서 `\"travel\"` 방에 연결된) Effect는 `\"travel\"`에 대한 알림을 보여줄 것입니다. `roomId`가 `\"music\"`으로 설정된 (그래서 `\"music\"` 방에 연결된) Effect는 `\"music\"`에 대한 알림을 보여줄 것입니다. 다시 말해 `theme`은 항상 최근 값을 사용하는 반면에 `connectedRoomId`는 (반응형인) Effect에서 비롯됩니다.\n\n추가 도전을 해결하려면 알림의 timeout ID를 저장하고 Effect의 클린업 함수에서 해제하면 됩니다.\n\n<Sandpack>\n\n```json package.json hidden\n{\n  \"dependencies\": {\n    \"react\": \"latest\",\n    \"react-dom\": \"latest\",\n    \"react-scripts\": \"latest\",\n    \"toastify-js\": \"1.12.0\"\n  },\n  \"scripts\": {\n    \"start\": \"react-scripts start\",\n    \"build\": \"react-scripts build\",\n    \"test\": \"react-scripts test --env=jsdom\",\n    \"eject\": \"react-scripts eject\"\n  }\n}\n```\n\n```js\nimport { useState, useEffect } from 'react';\nimport { useEffectEvent } from 'react';\nimport { createConnection, sendMessage } from './chat.js';\nimport { showNotification } from './notifications.js';\n\nconst serverUrl = 'https://localhost:1234';\n\nfunction ChatRoom({ roomId, theme }) {\n  const onConnected = useEffectEvent(connectedRoomId => {\n    showNotification(connectedRoomId + '에 오신 것을 환영합니다', theme);\n  });\n\n  useEffect(() => {\n    const connection = createConnection(serverUrl, roomId);\n    let notificationTimeoutId;\n    connection.on('connected', () => {\n      notificationTimeoutId = setTimeout(() => {\n        onConnected(roomId);\n      }, 2000);\n    });\n    connection.connect();\n    return () => {\n      connection.disconnect();\n      if (notificationTimeoutId !== undefined) {\n        clearTimeout(notificationTimeoutId);\n      }\n    };\n  }, [roomId]);\n\n  return <h1>{roomId} 방에 오신 것을 환영합니다!</h1>\n}\n\nexport default function App() {\n  const [roomId, setRoomId] = useState('general');\n  const [isDark, setIsDark] = useState(false);\n  return (\n    <>\n      <label>\n        채팅방 선택:{' '}\n        <select\n          value={roomId}\n          onChange={e => setRoomId(e.target.value)}\n        >\n          <option value=\"general\">general</option>\n          <option value=\"travel\">travel</option>\n          <option value=\"music\">music</option>\n        </select>\n      </label>\n      <label>\n        <input\n          type=\"checkbox\"\n          checked={isDark}\n          onChange={e => setIsDark(e.target.checked)}\n        />\n        어두운 테마 사용\n      </label>\n      <hr />\n      <ChatRoom\n        roomId={roomId}\n        theme={isDark ? 'dark' : 'light'}\n      />\n    </>\n  );\n}\n```\n\n```js src/chat.js\nexport function createConnection(serverUrl, roomId) {\n  // 실제 구현은 실제 서버에 연결했을 것입니다.\n  let connectedCallback;\n  let timeout;\n  return {\n    connect() {\n      timeout = setTimeout(() => {\n        if (connectedCallback) {\n          connectedCallback();\n        }\n      }, 100);\n    },\n    on(event, callback) {\n      if (connectedCallback) {\n        throw Error('핸들러는 두 번 추가할 수 없습니다.');\n      }\n      if (event !== 'connected') {\n        throw Error('\"connected\" 이벤트만 지원됩니다.');\n      }\n      connectedCallback = callback;\n    },\n    disconnect() {\n      clearTimeout(timeout);\n    }\n  };\n}\n```\n\n```js src/notifications.js hidden\nimport Toastify from 'toastify-js';\nimport 'toastify-js/src/toastify.css';\n\nexport function showNotification(message, theme) {\n  Toastify({\n    text: message,\n    duration: 2000,\n    gravity: 'top',\n    position: 'right',\n    style: {\n      background: theme === 'dark' ? 'black' : 'white',\n      color: theme === 'dark' ? 'white' : 'black',\n    },\n  }).showToast();\n}\n```\n\n```css\nlabel { display: block; margin-top: 10px; }\n```\n\n</Sandpack>\n\n이것으로 이미 예약된 (하지만 아직 표시되지 않은) 알림은 방을 바꿀 때 취소되는 것이 보장됩니다.\n\n</Solution>\n\n</Challenges>\n"
  },
  {
    "path": "src/content/learn/setup.md",
    "content": "---\ntitle: 설정하기\n---\n<Intro>\n\nReact는 에디터, 타입스크립트, 브라우저 확장 프로그램, 컴파일러와 같은 도구와 연동됩니다. 이 섹션은 환경을 설정하는 데 도움이 될 것입니다.\n\n</Intro>\n\n## 에디터 설정하기 {/*editor-setup*/}\n\n[추천 에디터](/learn/editor-setup)를 살펴보고 React와 함께 작동하도록 설정하는 방법을 알아보세요.\n\n## 타입스크립트 사용하기 {/*using-typescript*/}\n\n타입스크립트는 자바스크립트 코드베이스에 타입 정의를 추가하는 인기 있는 방법입니다. [React 프로젝트에 타입스크립트를 설정하는 방법을 알아보세요](/learn/typescript).\n\n## React 개발자 도구 {/*react-developer-tools*/}\n\nReact 개발자 도구는 React 컴포넌트를 검사하고, Props와 State를 편집하고, 성능 문제를 식별할 수 있는 브라우저 확장 프로그램입니다. [여기](learn/react-developer-tools)에서 설치하는 방법을 확인해보세요.\n\n## React 컴파일러 {/*react-compiler*/}\n\nReact 컴파일러는 React 앱을 자동으로 최적화하는 도구입니다. [자세히 알아보세요](/learn/react-compiler).\n\n## 다음 단계 {/*next-steps*/}\n\n[빠르게 시작하기](/learn)에서 자주 접하게 될 가장 중요한 React 개념들을 둘러보세요.\n"
  },
  {
    "path": "src/content/learn/sharing-state-between-components.md",
    "content": "---\ntitle: 컴포넌트 간 State 공유하기\n---\n\n<Intro>\n\n때때로 두 컴포넌트의 state가 항상 함께 변경되기를 원할 수 있습니다. 그렇게 하려면 각 컴포넌트에서 state를 제거하고 가장 가까운 공통 부모 컴포넌트로 옮긴 후 props로 전달해야 합니다. 이 방법을 *State 끌어올리기*라고 하며 React 코드를 작성할 때 가장 흔히 하는 일 중 하나입니다.\n\n</Intro>\n\n<YouWillLearn>\n\n- State 끌어올리기를 통해 컴포넌트 간 state를 공유하는 방법\n- 제어 컴포넌트와 비제어 컴포넌트\n\n</YouWillLearn>\n\n## State 끌어올리기 예시 {/*lifting-state-up-by-example*/}\n\n예시에서는 부모 컴포넌트인 `Accordion`이 두 개의 `Panel`을 렌더링합니다.\n\n* `Accordion`\n  - `Panel`\n  - `Panel`\n\n각 `Panel` 컴포넌트는 콘텐츠 표시 여부를 결정하는 Boolean형 `isActive` 상태를 가집니다.\n\n각 패널의 Show 버튼을 눌러봅시다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nfunction Panel({ title, children }) {\n  const [isActive, setIsActive] = useState(false);\n  return (\n    <section className=\"panel\">\n      <h3>{title}</h3>\n      {isActive ? (\n        <p>{children}</p>\n      ) : (\n        <button onClick={() => setIsActive(true)}>\n          Show\n        </button>\n      )}\n    </section>\n  );\n}\n\nexport default function Accordion() {\n  return (\n    <>\n      <h2>Almaty, Kazakhstan</h2>\n      <Panel title=\"About\">\n        With a population of about 2 million, Almaty is Kazakhstan's largest city. From 1929 to 1997, it was its capital city.\n      </Panel>\n      <Panel title=\"Etymology\">\n        The name comes from <span lang=\"kk-KZ\">алма</span>, the Kazakh word for \"apple\" and is often translated as \"full of apples\". In fact, the region surrounding Almaty is thought to be the ancestral home of the apple, and the wild <i lang=\"la\">Malus sieversii</i> is considered a likely candidate for the ancestor of the modern domestic apple.\n      </Panel>\n    </>\n  );\n}\n```\n\n```css\nh3, p { margin: 5px 0px; }\n.panel {\n  padding: 10px;\n  border: 1px solid #aaa;\n}\n```\n\n</Sandpack>\n\n한 패널의 버튼을 눌러도 다른 패널에는 영향을 미치지 않고 독립적입니다.\n\n<DiagramGroup>\n\n<Diagram name=\"sharing_state_child\" height={367} width={477} alt=\"Accordion이라는 이름의 하나의 부모와 Panel이라는 이름의 두 자식으로 구성된 세 컴포넌트 트리를 나타내는 다이어그램입니다. 두 Panel 컴포넌트는 값이 false인 isActive를 가집니다.\">\n\n처음에는 각 `Panel`의 `isActive` state가 `false`이기 때문에 두 컴포넌트 모두 닫힌 상태로 보입니다.\n\n</Diagram>\n\n<Diagram name=\"sharing_state_child_clicked\" height={367} width={480} alt=\"이전과 동일한 그림에서 자식 중 첫 번째 Panel 컴포넌트의 강조 표시된 isActive가 값이 true로 변경된 클릭을 나타냅니다. 두 번째 Panel 컴포넌트는 여전히 false 값을 가집니다.\" >\n\n두 `Panel`의 버튼 중 어느 것을 클릭하더라도 클릭한 해당 `Panel`의 `isActive` state만 변경됩니다.\n\n</Diagram>\n\n</DiagramGroup>\n\n**그러나 이제 한 번에 하나의 패널만 열리도록 변경하려고 합니다.** 두 번째 패널을 열기 위해선 첫 번째 패널을 닫아야 합니다. 어떻게 해야 할까요?\n\n두 패널을 조정하려면 다음 세 단계를 통해 부모 컴포넌트로 패널의 \"State 끌어올리기\"가 필요합니다.\n\n1. 자식 컴포넌트의 state를 **제거**합니다.\n2. 하드 코딩된 값을 공통 부모로부터 **전달**합니다.\n3. 공통 부모에 state를 **추가**하고 이벤트 핸들러와 함께 전달합니다.\n\n이 방법으로 `Accordion` 컴포넌트가 두 `Panel`을 조정하고 한 번에 하나만 열리도록 할 수 있습니다.\n\n### Step 1: 자식 컴포넌트에서 state 제거하기 {/*step-1-remove-state-from-the-child-components*/}\n\n`Panel`의 `isActive`에 대한 제어권을 부모 컴포넌트에 줄 수 있습니다. 즉 부모 컴포넌트는 `isActive`를 `Panel`에 prop으로 전달합니다. 다음 줄을 `Panel` 컴포넌트에서 제거하는 것으로 시작해봅시다.\n\n```js\nconst [isActive, setIsActive] = useState(false);\n```\n\n대신 `Panel`의 prop 목록에 `isActive`를 추가합니다.\n\n```js\nfunction Panel({ title, children, isActive }) {\n```\n\n이제 `Panel`의 부모 컴포넌트는 [props 내리꽂기](/learn/passing-props-to-a-component)를 통해 `isActive`를 제어할 수 있습니다. 반대로 `Panel` 컴포넌트는 `isActive`를 제어할 수 없습니다. 이제 부모 컴포넌트에 달려 있습니다.\n\n### Step 2: 하드 코딩된 데이터를 부모 컴포넌트로 전달하기 {/*step-2-pass-hardcoded-data-from-the-common-parent*/}\n\nstate를 올리려면, 조정하려는 *두* 자식 컴포넌트의 가장 가까운 공통 부모 컴포넌트에 두어야 합니다.\n\n* `Accordion` *(가장 가까운 공통 부모)*\n  - `Panel`\n  - `Panel`\n\n예시에서는 `Accordion` 컴포넌트입니다. 이 컴포넌트는 두 패널의 상위에 있고 props를 제어할 수 있기 때문에 현재 어느 패널이 활성화되었는지에 대한 \"진리의 원천(source of truth)\"이 됩니다. `Accordion` 컴포넌트가 하드 코딩된 값을 가지는 `isActive`를 (예를 들면 `true`) 두 패널에 전달하도록 만듭니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function Accordion() {\n  return (\n    <>\n      <h2>Almaty, Kazakhstan</h2>\n      <Panel title=\"About\" isActive={true}>\n        With a population of about 2 million, Almaty is Kazakhstan's largest city. From 1929 to 1997, it was its capital city.\n      </Panel>\n      <Panel title=\"Etymology\" isActive={true}>\n        The name comes from <span lang=\"kk-KZ\">алма</span>, the Kazakh word for \"apple\" and is often translated as \"full of apples\". In fact, the region surrounding Almaty is thought to be the ancestral home of the apple, and the wild <i lang=\"la\">Malus sieversii</i> is considered a likely candidate for the ancestor of the modern domestic apple.\n      </Panel>\n    </>\n  );\n}\n\nfunction Panel({ title, children, isActive }) {\n  return (\n    <section className=\"panel\">\n      <h3>{title}</h3>\n      {isActive ? (\n        <p>{children}</p>\n      ) : (\n        <button onClick={() => setIsActive(true)}>\n          Show\n        </button>\n      )}\n    </section>\n  );\n}\n```\n\n```css\nh3, p { margin: 5px 0px; }\n.panel {\n  padding: 10px;\n  border: 1px solid #aaa;\n}\n```\n\n</Sandpack>\n\n`Accordion` 컴포넌트에서 하드 코딩된 `isActive` 값을 변경하고 화면에 표시되는 결과를 확인해보세요.\n\n### Step 3: 공통 부모에 state 추가하기 {/*step-3-add-state-to-the-common-parent*/}\n\n상태 끌어올리기는 종종 state로 저장하고 있는 것의 특성을 바꿉니다.\n\n이 케이스에서는, 한 번에 하나의 패널만 활성화되어야 합니다. 이를 위해 공통 부모 컴포넌트인 `Accordion`은 *어떤* 패널이 활성화된 패널인지 추적하고 있어야 합니다. state 변수에 `boolean` 값을 사용하는 대신, 활성화되어있는 `Panel`의 인덱스 숫자를 사용할 수 있습니다.\n\n```js\nconst [activeIndex, setActiveIndex] = useState(0);\n```\n\n`activeIndex`가 `0`이면 첫 번째 패널이 활성화된 것이고, `1`이면 두 번째 패널이 활성화된 것입니다.\n\n각 `Panel`에서 \"Show\" 버튼을 클릭하면 `Accordion`의 활성화된 인덱스를 변경해야 합니다. `activeIndex` state는 `Accordion` 내에서 정의되었기 때문에 `Panel`은 값을 직접 설정할 수 없습니다. `Accordion` 컴포넌트는 `Panel` 컴포넌트가 state를 변경할 수 있음을 [이벤트 핸들러를 prop으로 전달하기](/learn/responding-to-events#passing-event-handlers-as-props)를 통해 *명시적으로 허용*해야 합니다.\n\n```js\n<>\n  <Panel\n    isActive={activeIndex === 0}\n    onShow={() => setActiveIndex(0)}\n  >\n    ...\n  </Panel>\n  <Panel\n    isActive={activeIndex === 1}\n    onShow={() => setActiveIndex(1)}\n  >\n    ...\n  </Panel>\n</>\n```\n\n이제 `Panel` 내의 `<button>`은 `onShow` prop을 클릭 이벤트 핸들러로 사용할 수 있습니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function Accordion() {\n  const [activeIndex, setActiveIndex] = useState(0);\n  return (\n    <>\n      <h2>Almaty, Kazakhstan</h2>\n      <Panel\n        title=\"About\"\n        isActive={activeIndex === 0}\n        onShow={() => setActiveIndex(0)}\n      >\n        With a population of about 2 million, Almaty is Kazakhstan's largest city. From 1929 to 1997, it was its capital city.\n      </Panel>\n      <Panel\n        title=\"Etymology\"\n        isActive={activeIndex === 1}\n        onShow={() => setActiveIndex(1)}\n      >\n        The name comes from <span lang=\"kk-KZ\">алма</span>, the Kazakh word for \"apple\" and is often translated as \"full of apples\". In fact, the region surrounding Almaty is thought to be the ancestral home of the apple, and the wild <i lang=\"la\">Malus sieversii</i> is considered a likely candidate for the ancestor of the modern domestic apple.\n      </Panel>\n    </>\n  );\n}\n\nfunction Panel({\n  title,\n  children,\n  isActive,\n  onShow\n}) {\n  return (\n    <section className=\"panel\">\n      <h3>{title}</h3>\n      {isActive ? (\n        <p>{children}</p>\n      ) : (\n        <button onClick={onShow}>\n          Show\n        </button>\n      )}\n    </section>\n  );\n}\n```\n\n```css\nh3, p { margin: 5px 0px; }\n.panel {\n  padding: 10px;\n  border: 1px solid #aaa;\n}\n```\n\n</Sandpack>\n\n이렇게 상태 끌어올리기가 완성됩니다! state를 공통 부모 컴포넌트로 옮기는 것은 두 패널을 조정할 수 있게 합니다. 두 개의 \"보임\" 플래그 대신 활성화된 인덱스를 사용하는 것은 한 번에 하나의 패널만 활성화됨을 보장합니다. 그리고 자식 컴포넌트로 이벤트 핸들러를 전달하는 것은 자식 컴포넌트에서 부모의 상태를 변경할 수 있게 합니다.\n\n<DiagramGroup>\n\n<Diagram name=\"sharing_state_parent\" height={385} width={487} alt=\"Accordion이라는 이름의 하나의 부모와 Panel이라는 이름의 두 자식으로 구성된 세 컴포넌트 트리를 나타내는 다이어그램입니다. Accordion은 값이 0인 activeIndex를 가지며, 첫 번째 패널의 isActive에 true를, 두 번째 패널의 isActive에 false를 반환합니다.\" >\n\n처음에 `Accordion`의 `activeIndex`는 `0`이므로 첫 번째 `Panel`은 `isActive = true`를 받습니다.\n\n</Diagram>\n\n<Diagram name=\"sharing_state_parent_clicked\" height={385} width={521} alt=\"이전과 동일한 그림에서 부모 컴포넌트 Accordion의 강조 표시된 activeIndex가 값이 1로 변경된 클릭을 나타냅니다. 두 하위 Panel 컴포넌트에 대한 전달 흐름도 강조 표시되어 isActive 값이 반대로 변경됨을 나타냅니다: 첫 번째 패널은 false, 두 번째 패널은 true\" >\n\n`Accordion`의 `activeIndex` state가 `1`로 변경되면 두 번째 `Panel`은 `isActive = true`를 받게 됩니다.\n\n</Diagram>\n\n</DiagramGroup>\n\n<DeepDive>\n\n#### 제어와 비제어 컴포넌트 {/*controlled-and-uncontrolled-components*/}\n\n\"제어되지 않은\" 몇몇 지역 state를 갖는 컴포넌트를 사용하는 것은 흔한 일입니다. 예를 들어 `isActive` state를 갖는 원래의 `Panel` 컴포넌트는 해당 컴포넌트의 부모에서 패널의 활성화 여부에 영향을 줄 수 없기 때문에 제어되지 않습니다.\n\n반대로 컴포넌트의 중요한 정보가 자체 지역 state 대신 props에 의해 만들어지는 경우 컴포넌트가 \"제어된다\"고 합니다. 이를 통해 부모 컴포넌트가 동작을 완전히 지정할 수 있습니다. `isActive` prop을 갖는 최종 `Panel` 컴포넌트는 `Accordion` 컴포넌트에 의해 제어됩니다.\n\n비제어 컴포넌트는 설정할 것이 적어 부모 컴포넌트에서 사용하기 더 쉽습니다. 하지만 여러 컴포넌트를 함께 조정하려고 할 때 비제어 컴포넌트는 덜 유연합니다. 제어 컴포넌트는 최대한으로 유연하지만, 부모 컴포넌트에서 props로 충분히 설정해주어야 합니다.\n\n실제로 \"제어\"와 \"비제어\"는 엄격한 기술 용어가 아니며 일반적으로 컴포넌트는 지역 state와 props를 혼합해서 사용합니다. 그러나 이런 구분은 컴포넌트의 설계와 제공하는 기능에 관해 설명하는데 유용한 방법입니다.\n\n컴포넌트를 작성할 때 어떤 정보가 (props를 통해) 제어되어야 하고 어떤 정보가 (state를 통해) 제어되지 않아야 하는지 고려하세요. 그렇지만 언제든 마음이 바뀔 수 있고 나중에 리팩토링 할 수 있습니다.\n\n</DeepDive>\n\n## 각 state의 단일 진리의 원천 {/*a-single-source-of-truth-for-each-state*/}\n\nReact 애플리케이션에서 많은 컴포넌트는 자체 state를 가집니다. 일부 상태는 입력처럼 리프 컴포넌트(트리 맨 아래에 있는 컴포넌트)와 가깝게 \"생존\"합니다. 다른 상태는 앱의 상단에 더 가깝게 \"생존\"할 수 있습니다. 예를 들면 클라이언트 측 라우팅 라이브러리도 현재 경로를 React state로 저장하고 props로 전달하도록 구현되어 있습니다!\n\n**각각의 고유한 state에 대해 어떤 컴포넌트가 \"소유\"할지 고를 수 있습니다.** 이 원칙은 또한 [\"단일 진리의 원천\"](https://en.wikipedia.org/wiki/Single_source_of_truth) 을 갖는 것으로 알려져 있습니다. 이는 모든 state가 한 곳에 존재한다는 의미가 아니라 그 정보를 가지고 있는 _특정_ 컴포넌트가 있다는 것을 말합니다. 컴포넌트 간의 공유된 state를 중복하는 대신 그들의 공통 부모로 *끌어올리고* 필요한 자식에게 *전달*하세요.\n\n작업이 진행되면서 애플리케이션은 계속 변합니다. 각 state가 어디에 \"생존\"해야 할지 고민하는 동안 state를 아래로 이동하거나 다시 올리는 것은 흔히 있는 일입니다. 이건 모두 과정의 일부입니다!\n\n컴포넌트를 몇 개 더 사용하여 어떤 느낌인지 알아보려면 [React로 사고하기](/learn/thinking-in-react)를 읽어보세요.\n\n<Recap>\n\n* 두 컴포넌트를 조정하고 싶을 때 state를 그들의 공통 부모로 이동합니다.\n* 그리고 공통 부모로부터 props를 통해 정보를 전달합니다.\n* 마지막으로 이벤트 핸들러를 전달해 자식에서 부모의 state를 변경할 수 있도록 합니다.\n* 컴포넌트를 (props로부터) \"제어\"할지 (state로부터) \"비제어\" 할지 고려하면 유용합니다.\n\n</Recap>\n\n<Challenges>\n\n#### 동기화된 입력 {/*synced-inputs*/}\n\n아래 두 입력은 독립적입니다. 두 입력의 동기화 상태를 유지하세요. 한 입력을 수정하면 다른 입력도 같은 문구로 변경되어야 하며 반대 경우도 동일합니다.\n\n<Hint>\n\nstate를 부모 컴포넌트로 끌어올려야 합니다.\n\n</Hint>\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function SyncedInputs() {\n  return (\n    <>\n      <Input label=\"First input\" />\n      <Input label=\"Second input\" />\n    </>\n  );\n}\n\nfunction Input({ label }) {\n  const [text, setText] = useState('');\n\n  function handleChange(e) {\n    setText(e.target.value);\n  }\n\n  return (\n    <label>\n      {label}\n      {' '}\n      <input\n        value={text}\n        onChange={handleChange}\n      />\n    </label>\n  );\n}\n```\n\n```css\ninput { margin: 5px; }\nlabel { display: block; }\n```\n\n</Sandpack>\n\n<Solution>\n\n`text` state를 `handleChange` 핸들러와 함께 부모 컴포넌트로 옮기고 두 `Input` 컴포넌트에 props로 전달하세요. 이는 두 컴포넌트의 입력이 동기화를 유지하게 합니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function SyncedInputs() {\n  const [text, setText] = useState('');\n\n  function handleChange(e) {\n    setText(e.target.value);\n  }\n\n  return (\n    <>\n      <Input\n        label=\"First input\"\n        value={text}\n        onChange={handleChange}\n      />\n      <Input\n        label=\"Second input\"\n        value={text}\n        onChange={handleChange}\n      />\n    </>\n  );\n}\n\nfunction Input({ label, value, onChange }) {\n  return (\n    <label>\n      {label}\n      {' '}\n      <input\n        value={value}\n        onChange={onChange}\n      />\n    </label>\n  );\n}\n```\n\n```css\ninput { margin: 5px; }\nlabel { display: block; }\n```\n\n</Sandpack>\n\n</Solution>\n\n#### 목록 필터링하기 {/*filtering-a-list*/}\n\n예시에서 `SearchBar`는 텍스트 입력을 제어하는 자체 `query` state를 가집니다. 부모 컴포넌트 `FilterableList`는 `List`의 목록을 표시하지만 검색 질의를 고려하지 않습니다.\n\n검색 질의에 따라 목록을 필터링하도록 `filterItems(foods, query)` 함수를 사용하세요. 수정한 것을 테스트하려면 검색창에 \"s\"를 입력했을 때 \"Sushi\", \"Shish kebab\", \"Dim sum\"이 목록에 표시되는지 확인하세요.\n\n`filterItems`은 이미 구현 및 가져오기가 되었으므로 직접 작성할 필요가 없습니다!\n\n<Hint>\n\n`query` state와 `handleChange` 핸들러를 `SearchBar`에서 제거한 후 `FilterableList`로 이동해야 합니다. 그런 다음 `SearchBar`에 `query`와 `onChange` props로 전달합니다.\n\n</Hint>\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\nimport { foods, filterItems } from './data.js';\n\nexport default function FilterableList() {\n  return (\n    <>\n      <SearchBar />\n      <hr />\n      <List items={foods} />\n    </>\n  );\n}\n\nfunction SearchBar() {\n  const [query, setQuery] = useState('');\n\n  function handleChange(e) {\n    setQuery(e.target.value);\n  }\n\n  return (\n    <label>\n      Search:{' '}\n      <input\n        value={query}\n        onChange={handleChange}\n      />\n    </label>\n  );\n}\n\nfunction List({ items }) {\n  return (\n    <table>\n      {items.map(food => (\n        <tr key={food.id}>\n          <td>{food.name}</td>\n          <td>{food.description}</td>\n        </tr>\n      ))}\n    </table>\n  );\n}\n```\n\n```js src/data.js\nexport function filterItems(items, query) {\n  query = query.toLowerCase();\n  return items.filter(item =>\n    item.name.split(' ').some(word =>\n      word.toLowerCase().startsWith(query)\n    )\n  );\n}\n\nexport const foods = [{\n  id: 0,\n  name: 'Sushi',\n  description: 'Sushi is a traditional Japanese dish of prepared vinegared rice'\n}, {\n  id: 1,\n  name: 'Dal',\n  description: 'The most common way of preparing dal is in the form of a soup to which onions, tomatoes and various spices may be added'\n}, {\n  id: 2,\n  name: 'Pierogi',\n  description: 'Pierogi are filled dumplings made by wrapping unleavened dough around a savoury or sweet filling and cooking in boiling water'\n}, {\n  id: 3,\n  name: 'Shish kebab',\n  description: 'Shish kebab is a popular meal of skewered and grilled cubes of meat.'\n}, {\n  id: 4,\n  name: 'Dim sum',\n  description: 'Dim sum is a large range of small dishes that Cantonese people traditionally enjoy in restaurants for breakfast and lunch'\n}];\n```\n\n</Sandpack>\n\n<Solution>\n\n`query` state를 `FilterableList` 컴포넌트로 끌어올리세요. 필터링 된 목록을 얻기 위해 `filterItems(foods, query)` 를 호출하고 그 값을 `List`로 전달하세요. 이제 질의 입력값은 목록에 반영됩니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\nimport { foods, filterItems } from './data.js';\n\nexport default function FilterableList() {\n  const [query, setQuery] = useState('');\n  const results = filterItems(foods, query);\n\n  function handleChange(e) {\n    setQuery(e.target.value);\n  }\n\n  return (\n    <>\n      <SearchBar\n        query={query}\n        onChange={handleChange}\n      />\n      <hr />\n      <List items={results} />\n    </>\n  );\n}\n\nfunction SearchBar({ query, onChange }) {\n  return (\n    <label>\n      Search:{' '}\n      <input\n        value={query}\n        onChange={onChange}\n      />\n    </label>\n  );\n}\n\nfunction List({ items }) {\n  return (\n    <table>\n      {items.map(food => (\n        <tr key={food.id}>\n          <td>{food.name}</td>\n          <td>{food.description}</td>\n        </tr>\n      ))}\n    </table>\n  );\n}\n```\n\n```js src/data.js\nexport function filterItems(items, query) {\n  query = query.toLowerCase();\n  return items.filter(item =>\n    item.name.split(' ').some(word =>\n      word.toLowerCase().startsWith(query)\n    )\n  );\n}\n\nexport const foods = [{\n  id: 0,\n  name: 'Sushi',\n  description: 'Sushi is a traditional Japanese dish of prepared vinegared rice'\n}, {\n  id: 1,\n  name: 'Dal',\n  description: 'The most common way of preparing dal is in the form of a soup to which onions, tomatoes and various spices may be added'\n}, {\n  id: 2,\n  name: 'Pierogi',\n  description: 'Pierogi are filled dumplings made by wrapping unleavened dough around a savoury or sweet filling and cooking in boiling water'\n}, {\n  id: 3,\n  name: 'Shish kebab',\n  description: 'Shish kebab is a popular meal of skewered and grilled cubes of meat.'\n}, {\n  id: 4,\n  name: 'Dim sum',\n  description: 'Dim sum is a large range of small dishes that Cantonese people traditionally enjoy in restaurants for breakfast and lunch'\n}];\n```\n\n</Sandpack>\n\n</Solution>\n\n</Challenges>\n"
  },
  {
    "path": "src/content/learn/start-a-new-react-project.md",
    "content": "---\ntitle: 새로운 React 프로젝트 시작하기\n---\n\n<Intro>\n\nReact로 새로운 앱이나 새로운 웹사이트를 완전히 작성하고 싶다면, 커뮤니티에서 React 기반 프레임워크 중 하나를 사용하는 것이 좋습니다.\n\n</Intro>\n\n프레임워크 없이 React를 사용할 수 있습니다. 그러나 대부분의 애플리케이션이나 사이트들이 결국에는 코드 분할<sup>Code-Splitting</sup>, 라우팅<sup>Routing</sup>, 데이터 가져오기<sup>Data Fetching</sup>, 그리고 HTML 생성에 대한 해결책을 찾고 있다는 것을 발견했습니다. 이러한 문제들은 UI 라이브러리들의 공통적인 문제이며 React만의 문제는 아닙니다.\n\n프레임워크로 시작하면 React를 빠르게 시작할 수 있고, 나중에 자체적인 프레임워크를 구축하는 것을 피할 수 있습니다.\n\n<DeepDive>\n\n#### 프레임워크 없이 React 를 사용할 수 있나요? {/*can-i-use-react-without-a-framework*/}\n\n물론 React를 프레임워크 없이 사용할 수 있습니다. [기존 페이지의 일부분에 React 사용하기](/learn/add-react-to-an-existing-project#using-react-for-a-part-of-your-existing-page)를 살펴보세요. **하지만, 새로운 애플리케이션이나 사이트 전체를 React로 구축하는 경우에는 프레임워크의 사용을 권장합니다.**\n\n이유는 아래와 같습니다.\n\n라우팅<sup>Routing</sup>이나 데이터 가져오기<sup>Data Fetching</sup>와 같은 기능이 처음엔 필요하지 않더라도, 이를 위한 라이브러리의 사용은 필요할 수도 있습니다. 자바스크립트<sup>JavaScript</sup> 번들은 날마다 새로운 기능들이 더해지고, 각각의 라우팅 코드를 어떻게 분할해야 할지 고민해야 하는 순간이 오게 됩니다. 데이터 가져오기<sup>Data Fetching</sup>는 날로 복잡해지고, 아마 서버-클라이언트 네트워크의 워터폴<sup>Waterfall</sup>이 애플리케이션의 속도를 느리게 하는 순간도 직면하게 될 겁니다. 성능이 좋지 않은 네트워크 환경이나 저사양의 단말기를 이용하는 사용자들이 늘어남에 따라, 서버에서 혹은 빌드 시간에 컴포넌트에서 HTML을 생성하여 내용<sup>Content</sup>을 빠르게 표시해야할 필요가 생길 수도 있습니다. 일부 코드의 설정을 변경하여 서버에서 혹은 빌드되는 동안 실행시키는 것은 매우 까다로운 작업이 될 수 있습니다.\n\n**이러한 문제가 React에만 국한된 것은 아닙니다. 이것이 바로 Svelte에 SvelteKit이 존재하고, Vue에는 Nuxt가 있는 이유입니다.** 자체 서비스에서 이러한 문제를 해결하기 위해서는 라우터<sup>Router</sup>와 데이터 가져오기<sup>Data Fetching</sup> 라이브러리를 번들러와 통합할 필요가 있습니다. 초기 세팅을 구동하는 건 그다지 어렵지 않지만, 시간이 지나면서 앱이 커져도 빠르게 로드되는 앱을 만드는 데에는 많은 세부 사항들이 포함됩니다. 앱 코드의 최소한의 양만 전송하되, 페이지에 필요한 데이터를 병렬로 처리하면서 클라이언트-서버 간의 단일 왕복<sup>Single Roundtrip</sup>으로 이를 처리하고 싶을 수도 있습니다. 자바스크립트 코드가 실행되기도 전에 페이지가 상호작용 할 수 있도록 점진적인 개선을 하고 싶을 수도 있습니다. 마케팅 목적의 완전히 정적인 HTML 파일들이 속한 폴더를 생성하여 자바스크립트가 비활성화된 환경에서도 이를 작동하게 하고 싶을 수도 있습니다. 이러한 기능들을 구현하는 것은 실제로 많은 작업을 요합니다.\n\n**현재 페이지의 React 프레임워크들은 추가 작업 없이도 기본적으로 이러한 문제들을 해결합니다.** 이들은 매우 간소화된 상태로 시작할 수 있고 애플리케이션의 필요에 따라 확장이 가능합니다. 각각의 React 프레임워크들은 커뮤니티가 있어 질문에 대한 답을 얻고 도구를 업그레이드하는 것이 더 쉬워집니다. 또한 프레임워크들은 코드에 구조를 제공하며, 다른 프로젝트들간의 맥락과 스킬을 유지하는 데에 도움이 됩니다. 반대로, 맞춤 설정을 사용하면 지원되지 않는 의존성<sup>Dependency</sup> 버전에 빠질 수 있으며, 결국엔 커뮤니티나 업그레이드 경로가 없는 자체 프레임워크를 만들게 될 수도 있습니다. (그리고 만약 이전에 우리가 만든 것들과 비슷하다면, 더 엉성하게 설계된 것일 수 있습니다.)\n\n애플리케이션이 이러한 프레임워크들의 지원을 잘 받지 못하는 특수한 제약에 놓여 있거나, 스스로 이러한 문제들을 해결하고 싶다면 React를 사용하여 자체 맞춤 설정을 적용할 수 있습니다. npm에서 `react` 와 `react-dom`을 설치하고, [Vite](https://vitejs.dev/) 나 [Parcel](https://parceljs.org/) 같은 번들러를 활용하여 맞춤 빌드 프로세스를 정립한 다음, 라우팅<sup>Routing</sup>, 정적 생성<sup>Static Generation</sup> 혹은 서버 사이드 렌더링<sup>SSR, Server Side Rendering</sup> 등 필요에 따라 다른 도구들을 추가할 수 있습니다.\n\n</DeepDive>\n\n## 프로덕션 수준의 React 프레임워크 {/*production-grade-react-frameworks*/}\n\n아래 프레임워크들은 프로덕션에서 애플리케이션을 배포하고 확장하는 데에 필요한 모든 기능을 지원하며 [풀스택 아키텍처 비전](#which-features-make-up-the-react-teams-full-stack-architecture-vision)을 지원하는 방향으로 발전하고 있습니다. 모든 프레임워크들은 활발한 커뮤니티의 지원을 받는 오픈 소스이며, 자체 서버나 호스팅 제공자에 배포할 수 있습니다. 만일 이 목록에 포함되길 원하는 프레임워크의 저자가 있다면, [여기에서 알려주세요.](https://github.com/reactjs/react.dev/issues/new?assignees=&labels=type%3A+framework&projects=&template=3-framework.yml&title=%5BFramework%5D%3A+)\n\n### Next.js {/*nextjs-pages-router*/}\n\n**[Next.js의 Pages Router](https://nextjs.org/)는 풀스택 React 프레임워크입니다.** 다재다능한 도구이며, 정적인 블로그부터 복잡한 동적 애플리케이션까지 다양한 크기의 React 애플리케이션을 만들 수 있습니다. 새로운 Next.js 프로젝트를 작성하려면 터미널에서 다음을 실행하세요.\n\n<TerminalBlock>\nnpx create-next-app@latest\n</TerminalBlock>\n\nNext.js를 처음 사용하는 분이라면 [Next.js 배우기 코스](https://nextjs.org/learn)를 읽어보세요.\n\nNext.js는 [Vercel](https://vercel.com/)이 관리합니다. 어떤 Node.js 서버, 서버리스 호스팅 또는 직접 소유한 서버 어느 곳에서라도 [Next.js 애플리케이션을 배포](https://nextjs.org/docs/app/building-your-application/deploying)할 수 있습니다. Next.js는 서버가 필요없는 [정적 내보내기<sup>Static Exports</sup>](https://nextjs.org/docs/pages/building-your-application/deploying/static-exports) 도 제공합니다.\n\n### Remix {/*remix*/}\n\n**[Remix](https://remix.run/)는 중첩 라우팅이 가능한 풀스택 React 프레임워크입니다.** 애플리케이션을 중첩되는 하위 파트로 나눌 수 있으며, 각 파트는 병렬로 데이터를 읽어 들일 수 있고 사용자의 행동에 반응하여 다시 그려질 수 있습니다. 새로운 Remix 프로젝트를 작성하려면 다음을 실행하세요.\n\n<TerminalBlock>\nnpx create-remix\n</TerminalBlock>\n\nRemix를 처음 사용하는 분이라면 Remix [블로그 자습서](https://remix.run/docs/en/main/tutorials/blog) (짧은 문서)와 [애플리케이션 자습서](https://remix.run/docs/en/main/tutorials/jokes) (긴 문서)를 참고하세요.\n\nRemix는 [Shopify](https://www.shopify.com/)가 관리합니다. Remix 프로젝트를 작성할 때는 [배포할 대상을 선택](https://remix.run/docs/en/main/guides/deployment)해야 합니다. Remix 애플리케이션은 어떤 Node.js 서버나 서버리스 호스팅에라도 [어댑터<sup>Adapter</sup>](https://remix.run/docs/en/main/other-api/adapter)를 사용하거나 직접 작성하여 배포할 수 있습니다.\n\n### Gatsby {/*gatsby*/}\n\n**[Gatsby](https://www.gatsbyjs.com/)는 CMS를 활용한 빠른 웹 사이트를 작성하는 React 프레임워크입니다.** 풍부한 플러그인 생태계와 GraphQL 데이터 레이어가 콘텐츠, API, 서비스와 어우러져 하나의 웹 사이트를 이룹니다. 새로운 Gatsby 프로젝트를 작성하려면 다음을 실행하세요.\n\n<TerminalBlock>\nnpx create-gatsby\n</TerminalBlock>\n\nGatsby를 처음 사용하는 분이라면 [Gatsby 자습서](https://www.gatsbyjs.com/docs/tutorial/)를 읽어보세요.\n\nGatsby는 [Netlify](https://www.netlify.com/)가 관리합니다. 어느 정적인 호스팅에라도 [완전히 정적인 Gatsby 사이트를 배포](https://www.gatsbyjs.com/docs/how-to/previews-deploys-hosting)할 수 있습니다. 서버 전용 기능을 사용한다면 사용하려는 호스팅 제공업체가 Gatsby를 지원하는지 먼저 확인하세요.\n\n### Expo (네이티브 앱) {/*expo*/}\n\n**[Expo](https://expo.dev/)는 진짜 네이티브 UI를 갖춘 유니버설 안드로이드, iOS, 웹을 작성할 수 있는 React 프레임워크입니다.** [React Native](https://reactnative.dev/)용 SDK를 제공하여 네이티브 부분을 더 쉽게 사용할 수 있습니다. 새로운 Expo 프로젝트를 작성하려면 다음을 실행하세요.\n\n<TerminalBlock>\nnpx create-expo-app\n</TerminalBlock>\n\nExpo를 처음 사용하는 분이라면 [Expo 자습서](https://docs.expo.dev/tutorial/introduction/)를 참고하세요.\n\nExpo는 [Expo (기업)](https://expo.dev/about)이 관리합니다. Expo를 사용하여 애플리케이션을 작성하는 것은 무료이며 작성된 앱을 구글과 애플 앱 스토어에 올리는 데에도 제약이 없습니다. Expo는 추가로 사용할 수 있는 클라우드 서비스를 유료로 제공하고 있습니다.\n\n## Bleeding-edge React frameworks {/*bleeding-edge-react-frameworks*/}\n\nReact를 지속적으로 개선할 방법을 찾아가는 과정에서, 우리는 React를 프레임워크(특히 라우팅, 번들링, 서버 기술)와 더 밀접하게 통합하는 것이 React 사용자에게 더 나은 앱을 만드는 데 도움을 줄 수 있는 가장 큰 기회라는 것을 깨달았습니다. Next.js 팀은 [React 서버 컴포넌트](/blog/2023/03/22/react-labs-what-we-have-been-working-on-march-2023#react-server-components)와 같은 가장 최신의 React 기능을 프레임워크에 구애받지 않는 형태로 연구, 개발, 통합, 테스트하는 데에 협력하기로 합의했습니다.\n\n이러한 기능들은 매일 프로덕션 수준에 근접하고 있으며, 다른 번들러 및 프레임워크 개발자들과 이를 통합하기 위해 협의 중입니다. 바라건데 1, 2년 후에 이 페이지에 나열된 모든 프레임워크가 이러한 기능을 지원했으면 합니다. (이러한 기능을 실험해 보기 위해 우리와 협력하고 싶은 프레임워크 개발자가 있다면 알려주세요!)\n\n### Next.js (App Router) {/*nextjs-app-router*/}\n\n**[Next.js의 App Router](https://nextjs.org/docs)는 React 팀의 풀스택 아키텍처 비전을 구현하기 위해 재설계된 Next.js API입니다.** 이를 통해 서버에서 또는 빌드 중에 실행되는 비동기 컴포넌트에서 데이터를 가져올 수 있습니다.\n\n\nNext.js는 [Vercel](https://vercel.com/)이 관리합니다. 어떤 Node.js 서버, 서버리스 호스팅 또는 직접 소유한 서버 어느 곳에라도 [Next.js 애플리케이션을 배포](https://nextjs.org/docs/app/building-your-application/deploying)할 수 있습니다. Next.js 는 서버가 필요없는 [정적 내보내기<sup>Static Exports</sup>](https://nextjs.org/docs/pages/building-your-application/deploying/static-exports)도 제공합니다.\n\n<DeepDive>\n\n#### React 팀의 풀스택 아키텍처 비전을 구현한 기능은 무엇인가요? {/*which-features-make-up-the-react-teams-full-stack-architecture-vision*/}\n\nNext.js의 App Router 번들러는 공식 [React 서버 컴포넌트 명세](https://github.com/reactjs/rfcs/blob/main/text/0188-server-components.md) 전체를 구현했습니다. 이를 통해 빌드 시간<sup>Build-Time</sup>, 서버 전용<sup>Server-Only</sup>, 그리고 대화형<sup>Interactive</sup> 컴포넌트를 하나의 React 트리<sup>Tree</sup>에서 혼합할 수 있습니다.\n\n예를 들어, 데이터베이스나 파일을 읽는 `async` 함수로 서버 전용 React 컴포넌트를 작성할 수 있습니다. 그런 다음 이를 통해 데이터를 대화형 컴포넌트로 전달할 수 있습니다.\n\n```js\n// 이 컴포넌트는 *서버(또는 빌드 중)에서만* 실행됩니다.\nasync function Talks({ confId }) {\n  // 1. 서버에 있으므로 데이터 계층과 통신할 수 있습니다. API 엔드포인트가 필요하지 않습니다.\n  const talks = await db.Talks.findAll({ confId });\n\n  // 2. 렌더링 로직을 얼마든지 추가할 수 있습니다. 자바스크립트 번들 크기가 커지지 않습니다.\n  const videos = talks.map(talk => talk.video);\n\n  // 3. 브라우저에서 실행될 컴포넌트에 데이터를 전달합니다.\n  return <SearchableVideoList videos={videos} />;\n}\n```\n\nNext.js의 App Router는 또한 Suspense를 사용하는 데이터 통신과도 잘 어울립니다. 이를 통해 React 트리에서 사용자 인터페이스의 다른 부분에 대한 로딩 상태(스켈레톤<sup>Skeleton</sup> 플레이스홀더<sup>Placeholder</sup>와 같은)를 직접 지정할 수 있습니다.\n\n```js\n<Suspense fallback={<TalksLoading />}>\n  <Talks confId={conf.id} />\n</Suspense>\n```\n\n서버 컴포넌트와 Suspense는 Next.js의 기능이 아닌 React의 기능입니다. 하지만 프레임워크 수준에서 이를 채택하려면 많은 노력과 비교적 복잡한 구현 작업이 필요합니다. 현재로서는 Next.js의 App Router가 가장 완벽한 구현입니다. React 팀은 차세대 프레임워크에서는 이러한 기능을 구현하기 쉽도록 번들러 개발자와 함께 노력하고 있습니다.\n\n</DeepDive>\n"
  },
  {
    "path": "src/content/learn/state-a-components-memory.md",
    "content": "---\ntitle: \"State: 컴포넌트의 기억 저장소\"\n---\n\n<Intro>\n\n컴포넌트는 상호 작용의 결과로 화면의 내용을 변경해야 하는 경우가 많습니다. 폼에 입력하면 입력 필드가 업데이트되어야 하고, 이미지 캐러셀에서 \"다음\"을 클릭할 때 표시되는 이미지가 변경되어야 하고, \"구매\"를 클릭하면 상품이 장바구니에 담겨야 합니다. 컴포넌트는 현재 입력값, 현재 이미지, 장바구니와 같은 것들을 \"기억\"해야 합니다. React는 이런 종류의 컴포넌트별 메모리를 *state*라고 부릅니다.\n\n</Intro>\n\n<YouWillLearn>\n\n* [`useState`](/reference/react/useState) 훅으로 state 변수를 추가하는 방법\n* `useState` 훅이 반환하는 한 쌍의 값\n* 둘 이상의 state 변수를 추가하는 방법\n* state를 지역적이라고 하는 이유\n\n</YouWillLearn>\n\n## 일반 변수로 충분하지 않은 경우 {/*when-a-regular-variable-isnt-enough*/}\n\n다음은 조각상 이미지를 렌더링하는 컴포넌트입니다. \"Next\" 버튼을 클릭하면 `index`를 `1`, `2`로 변경하여 다음 조각상을 표시해야 합니다. 그러나 이것은 **작동하지 않습니다** (시도해 보세요!)\n\n<Sandpack>\n\n```js\nimport { sculptureList } from './data.js';\n\nexport default function Gallery() {\n  let index = 0;\n\n  function handleClick() {\n    index = index + 1;\n  }\n\n  let sculpture = sculptureList[index];\n  return (\n    <>\n      <button onClick={handleClick}>\n        Next\n      </button>\n      <h2>\n        <i>{sculpture.name} </i>\n        by {sculpture.artist}\n      </h2>\n      <h3>\n        ({index + 1} of {sculptureList.length})\n      </h3>\n      <img\n        src={sculpture.url}\n        alt={sculpture.alt}\n      />\n      <p>\n        {sculpture.description}\n      </p>\n    </>\n  );\n}\n```\n\n```js src/data.js\nexport const sculptureList = [{\n  name: 'Homenaje a la Neurocirugía',\n  artist: 'Marta Colvin Andrade',\n  description: 'Although Colvin is predominantly known for abstract themes that allude to pre-Hispanic symbols, this gigantic sculpture, an homage to neurosurgery, is one of her most recognizable public art pieces.',\n  url: 'https://i.imgur.com/Mx7dA2Y.jpg',\n  alt: 'A bronze statue of two crossed hands delicately holding a human brain in their fingertips.'\n}, {\n  name: 'Floralis Genérica',\n  artist: 'Eduardo Catalano',\n  description: 'This enormous (75 ft. or 23m) silver flower is located in Buenos Aires. It is designed to move, closing its petals in the evening or when strong winds blow and opening them in the morning.',\n  url: 'https://i.imgur.com/ZF6s192m.jpg',\n  alt: 'A gigantic metallic flower sculpture with reflective mirror-like petals and strong stamens.'\n}, {\n  name: 'Eternal Presence',\n  artist: 'John Woodrow Wilson',\n  description: 'Wilson was known for his preoccupation with equality, social justice, as well as the essential and spiritual qualities of humankind. This massive (7ft. or 2,13m) bronze represents what he described as \"a symbolic Black presence infused with a sense of universal humanity.\"',\n  url: 'https://i.imgur.com/aTtVpES.jpg',\n  alt: 'The sculpture depicting a human head seems ever-present and solemn. It radiates calm and serenity.'\n}, {\n  name: 'Moai',\n  artist: 'Unknown Artist',\n  description: 'Located on the Easter Island, there are 1,000 moai, or extant monumental statues, created by the early Rapa Nui people, which some believe represented deified ancestors.',\n  url: 'https://i.imgur.com/RCwLEoQm.jpg',\n  alt: 'Three monumental stone busts with the heads that are disproportionately large with somber faces.'\n}, {\n  name: 'Blue Nana',\n  artist: 'Niki de Saint Phalle',\n  description: 'The Nanas are triumphant creatures, symbols of femininity and maternity. Initially, Saint Phalle used fabric and found objects for the Nanas, and later on introduced polyester to achieve a more vibrant effect.',\n  url: 'https://i.imgur.com/Sd1AgUOm.jpg',\n  alt: 'A large mosaic sculpture of a whimsical dancing female figure in a colorful costume emanating joy.'\n}, {\n  name: 'Ultimate Form',\n  artist: 'Barbara Hepworth',\n  description: 'This abstract bronze sculpture is a part of The Family of Man series located at Yorkshire Sculpture Park. Hepworth chose not to create literal representations of the world but developed abstract forms inspired by people and landscapes.',\n  url: 'https://i.imgur.com/2heNQDcm.jpg',\n  alt: 'A tall sculpture made of three elements stacked on each other reminding of a human figure.'\n}, {\n  name: 'Cavaliere',\n  artist: 'Lamidi Olonade Fakeye',\n  description: \"Descended from four generations of woodcarvers, Fakeye's work blended traditional and contemporary Yoruba themes.\",\n  url: 'https://i.imgur.com/wIdGuZwm.png',\n  alt: 'An intricate wood sculpture of a warrior with a focused face on a horse adorned with patterns.'\n}, {\n  name: 'Big Bellies',\n  artist: 'Alina Szapocznikow',\n  description: \"Szapocznikow is known for her sculptures of the fragmented body as a metaphor for the fragility and impermanence of youth and beauty. This sculpture depicts two very realistic large bellies stacked on top of each other, each around five feet (1,5m) tall.\",\n  url: 'https://i.imgur.com/AlHTAdDm.jpg',\n  alt: 'The sculpture reminds a cascade of folds, quite different from bellies in classical sculptures.'\n}, {\n  name: 'Terracotta Army',\n  artist: 'Unknown Artist',\n  description: 'The Terracotta Army is a collection of terracotta sculptures depicting the armies of Qin Shi Huang, the first Emperor of China. The army consisted of more than 8,000 soldiers, 130 chariots with 520 horses, and 150 cavalry horses.',\n  url: 'https://i.imgur.com/HMFmH6m.jpg',\n  alt: '12 terracotta sculptures of solemn warriors, each with a unique facial expression and armor.'\n}, {\n  name: 'Lunar Landscape',\n  artist: 'Louise Nevelson',\n  description: 'Nevelson was known for scavenging objects from New York City debris, which she would later assemble into monumental constructions. In this one, she used disparate parts like a bedpost, juggling pin, and seat fragment, nailing and gluing them into boxes that reflect the influence of Cubism’s geometric abstraction of space and form.',\n  url: 'https://i.imgur.com/rN7hY6om.jpg',\n  alt: 'A black matte sculpture where the individual elements are initially indistinguishable.'\n}, {\n  name: 'Aureole',\n  artist: 'Ranjani Shettar',\n  description: 'Shettar merges the traditional and the modern, the natural and the industrial. Her art focuses on the relationship between man and nature. Her work was described as compelling both abstractly and figuratively, gravity defying, and a \"fine synthesis of unlikely materials.\"',\n  url: 'https://i.imgur.com/okTpbHhm.jpg',\n  alt: 'A pale wire-like sculpture mounted on concrete wall and descending on the floor. It appears light.'\n}, {\n  name: 'Hippos',\n  artist: 'Taipei Zoo',\n  description: 'The Taipei Zoo commissioned a Hippo Square featuring submerged hippos at play.',\n  url: 'https://i.imgur.com/6o5Vuyu.jpg',\n  alt: 'A group of bronze hippo sculptures emerging from the sett sidewalk as if they were swimming.'\n}];\n```\n\n```css\nh2 { margin-top: 10px; margin-bottom: 0; }\nh3 {\n  margin-top: 5px;\n  font-weight: normal;\n  font-size: 100%;\n}\nimg { width: 120px; height: 120px; }\nbutton {\n  display: block;\n  margin-top: 10px;\n  margin-bottom: 10px;\n}\n```\n\n</Sandpack>\n\n`handleClick` 이벤트 핸들러는 지역 변수 `index`를 업데이트하고 있습니다. 하지만 이러한 변화를 보이지 않게 하는 두 가지 이유가 있습니다.\n\n1. **지역 변수는 렌더링 간에 유지되지 않습니다.** React는 이 컴포넌트를 두 번째로 렌더링할 때 지역 변수에 대한 변경 사항은 고려하지 않고 처음부터 렌더링 합니다.\n2. **지역 변수를 변경해도 렌더링을 일으키지 않습니다.** React는 새로운 데이터로 컴포넌트를 다시 렌더링해야 한다는 것을 인식하지 못합니다.\n\n컴포넌트를 새로운 데이터로 업데이트하기 위해선 다음 두 가지가 필요합니다.\n\n1. 렌더링 사이에 데이터를 **유지**합니다.\n2. React가 새로운 데이터로 컴포넌트를 렌더링하도록 **유발**합니다.\n\n[`useState`](/reference/react/useState) 훅은 이 두 가지를 제공합니다.\n\n1. 렌더링 간에 데이터를 유지하기 위한 **state 변수**.\n2. 변수를 업데이트하고 React가 컴포넌트를 다시 렌더링하도록 유발하는 **state setter 함수**\n\n## state 변수 추가하기 {/*adding-a-state-variable*/}\n\nstate 변수를 추가하려면 파일 상단의 React에서 `useState`를 가져옵니다.\n\n```js\nimport { useState } from 'react';\n```\n\n그런 다음 이 줄을\n\n```js\nlet index = 0;\n```\n\n다음과 같이 바꿉니다.\n\n```js\nconst [index, setIndex] = useState(0);\n```\n\n`index`는 state 변수이고 `setIndex` 는 setter 함수입니다.\n\n> 여기서 `[`와 `]` 문법을 [배열 구조 분해](https://ko.javascript.info/destructuring-assignment)라고 하며, 배열로부터 값을 읽을 수 있게 해줍니다. `useState`가 반환하는 배열에는 항상 두 개의 항목이 있습니다.\n\n이것이 `handleClick`에서 함께 작동하는 방식입니다\n\n```js\nfunction handleClick() {\n  setIndex(index + 1);\n}\n```\n\n이제 \"Next\" 버튼을 클릭하면 현재 조각상을 전환합니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\nimport { sculptureList } from './data.js';\n\nexport default function Gallery() {\n  const [index, setIndex] = useState(0);\n\n  function handleClick() {\n    setIndex(index + 1);\n  }\n\n  let sculpture = sculptureList[index];\n  return (\n    <>\n      <button onClick={handleClick}>\n        Next\n      </button>\n      <h2>\n        <i>{sculpture.name} </i>\n        by {sculpture.artist}\n      </h2>\n      <h3>\n        ({index + 1} of {sculptureList.length})\n      </h3>\n      <img\n        src={sculpture.url}\n        alt={sculpture.alt}\n      />\n      <p>\n        {sculpture.description}\n      </p>\n    </>\n  );\n}\n```\n\n```js src/data.js\nexport const sculptureList = [{\n  name: 'Homenaje a la Neurocirugía',\n  artist: 'Marta Colvin Andrade',\n  description: 'Although Colvin is predominantly known for abstract themes that allude to pre-Hispanic symbols, this gigantic sculpture, an homage to neurosurgery, is one of her most recognizable public art pieces.',\n  url: 'https://i.imgur.com/Mx7dA2Y.jpg',\n  alt: 'A bronze statue of two crossed hands delicately holding a human brain in their fingertips.'\n}, {\n  name: 'Floralis Genérica',\n  artist: 'Eduardo Catalano',\n  description: 'This enormous (75 ft. or 23m) silver flower is located in Buenos Aires. It is designed to move, closing its petals in the evening or when strong winds blow and opening them in the morning.',\n  url: 'https://i.imgur.com/ZF6s192m.jpg',\n  alt: 'A gigantic metallic flower sculpture with reflective mirror-like petals and strong stamens.'\n}, {\n  name: 'Eternal Presence',\n  artist: 'John Woodrow Wilson',\n  description: 'Wilson was known for his preoccupation with equality, social justice, as well as the essential and spiritual qualities of humankind. This massive (7ft. or 2,13m) bronze represents what he described as \"a symbolic Black presence infused with a sense of universal humanity.\"',\n  url: 'https://i.imgur.com/aTtVpES.jpg',\n  alt: 'The sculpture depicting a human head seems ever-present and solemn. It radiates calm and serenity.'\n}, {\n  name: 'Moai',\n  artist: 'Unknown Artist',\n  description: 'Located on the Easter Island, there are 1,000 moai, or extant monumental statues, created by the early Rapa Nui people, which some believe represented deified ancestors.',\n  url: 'https://i.imgur.com/RCwLEoQm.jpg',\n  alt: 'Three monumental stone busts with the heads that are disproportionately large with somber faces.'\n}, {\n  name: 'Blue Nana',\n  artist: 'Niki de Saint Phalle',\n  description: 'The Nanas are triumphant creatures, symbols of femininity and maternity. Initially, Saint Phalle used fabric and found objects for the Nanas, and later on introduced polyester to achieve a more vibrant effect.',\n  url: 'https://i.imgur.com/Sd1AgUOm.jpg',\n  alt: 'A large mosaic sculpture of a whimsical dancing female figure in a colorful costume emanating joy.'\n}, {\n  name: 'Ultimate Form',\n  artist: 'Barbara Hepworth',\n  description: 'This abstract bronze sculpture is a part of The Family of Man series located at Yorkshire Sculpture Park. Hepworth chose not to create literal representations of the world but developed abstract forms inspired by people and landscapes.',\n  url: 'https://i.imgur.com/2heNQDcm.jpg',\n  alt: 'A tall sculpture made of three elements stacked on each other reminding of a human figure.'\n}, {\n  name: 'Cavaliere',\n  artist: 'Lamidi Olonade Fakeye',\n  description: \"Descended from four generations of woodcarvers, Fakeye's work blended traditional and contemporary Yoruba themes.\",\n  url: 'https://i.imgur.com/wIdGuZwm.png',\n  alt: 'An intricate wood sculpture of a warrior with a focused face on a horse adorned with patterns.'\n}, {\n  name: 'Big Bellies',\n  artist: 'Alina Szapocznikow',\n  description: \"Szapocznikow is known for her sculptures of the fragmented body as a metaphor for the fragility and impermanence of youth and beauty. This sculpture depicts two very realistic large bellies stacked on top of each other, each around five feet (1,5m) tall.\",\n  url: 'https://i.imgur.com/AlHTAdDm.jpg',\n  alt: 'The sculpture reminds a cascade of folds, quite different from bellies in classical sculptures.'\n}, {\n  name: 'Terracotta Army',\n  artist: 'Unknown Artist',\n  description: 'The Terracotta Army is a collection of terracotta sculptures depicting the armies of Qin Shi Huang, the first Emperor of China. The army consisted of more than 8,000 soldiers, 130 chariots with 520 horses, and 150 cavalry horses.',\n  url: 'https://i.imgur.com/HMFmH6m.jpg',\n  alt: '12 terracotta sculptures of solemn warriors, each with a unique facial expression and armor.'\n}, {\n  name: 'Lunar Landscape',\n  artist: 'Louise Nevelson',\n  description: 'Nevelson was known for scavenging objects from New York City debris, which she would later assemble into monumental constructions. In this one, she used disparate parts like a bedpost, juggling pin, and seat fragment, nailing and gluing them into boxes that reflect the influence of Cubism’s geometric abstraction of space and form.',\n  url: 'https://i.imgur.com/rN7hY6om.jpg',\n  alt: 'A black matte sculpture where the individual elements are initially indistinguishable.'\n}, {\n  name: 'Aureole',\n  artist: 'Ranjani Shettar',\n  description: 'Shettar merges the traditional and the modern, the natural and the industrial. Her art focuses on the relationship between man and nature. Her work was described as compelling both abstractly and figuratively, gravity defying, and a \"fine synthesis of unlikely materials.\"',\n  url: 'https://i.imgur.com/okTpbHhm.jpg',\n  alt: 'A pale wire-like sculpture mounted on concrete wall and descending on the floor. It appears light.'\n}, {\n  name: 'Hippos',\n  artist: 'Taipei Zoo',\n  description: 'The Taipei Zoo commissioned a Hippo Square featuring submerged hippos at play.',\n  url: 'https://i.imgur.com/6o5Vuyu.jpg',\n  alt: 'A group of bronze hippo sculptures emerging from the sett sidewalk as if they were swimming.'\n}];\n```\n\n```css\nh2 { margin-top: 10px; margin-bottom: 0; }\nh3 {\n margin-top: 5px;\n font-weight: normal;\n font-size: 100%;\n}\nimg { width: 120px; height: 120px; }\nbutton {\n  display: block;\n  margin-top: 10px;\n  margin-bottom: 10px;\n}\n```\n\n</Sandpack>\n\n### 첫 번째 훅 만나기 {/*meet-your-first-hook*/}\n\nReact에서 `useState`와 같이 \"`use`\"로 시작하는 다른 모든 함수를 훅이라고 합니다.\n\n*훅*은 React가 오직 [렌더링](/learn/render-and-commit#step-1-trigger-a-render) 중일 때만 사용할 수 있는 특별한 함수입니다. (이에 대해서는 다음 페이지에서 자세히 알아보겠습니다) 이를 통해 다양한 React 기능을 \"연결\"할 수 있습니다.\n\nState는 이러한 기능 중 하나일 뿐이며, 나중에 다른 훅들을 만나게 됩니다.\n\n<Pitfall>\n\n**훅(`use`로 시작하는 함수들)은 컴포넌트의 최상위 수준 또는 [커스텀 훅](/learn/reusing-logic-with-custom-hooks)에서만 호출할 수 있습니다.** 조건문, 반복문 또는 기타 중첩 함수 내부에서는 훅을 호출할 수 없습니다. 훅은 함수이지만 컴포넌트의 필요에 대한 무조건적인 선언으로 생각하면 도움이 됩니다. 파일 상단에서 모듈을 \"import\"하는 것과 유사하게 컴포넌트 상단에서 React 기능을 \"사용\"합니다.\n\n</Pitfall>\n\n### `useState` 해부하기 {/*anatomy-of-usestate*/}\n\n[`useState`](/reference/react/useState)를 호출하는 것은, React에 이 컴포넌트가 무언가를 기억하기를 원한다고 말하는 것입니다.\n\n```js\nconst [index, setIndex] = useState(0);\n```\n\n이 경우 React가 `index`를 기억하기를 원합니다.\n\n<Note>\n\n이 쌍의 이름은 `const [something, setSomething]`과 같이 지정하는 것이 규칙입니다. 원하는 대로 이름을 지을 수 있지만, 규칙을 사용하면 프로젝트 전반에 걸쳐 상황을 더 쉽게 이해할 수 있습니다.\n\n</Note>\n\n`useState`의 유일한 인수는 state 변수의 **초깃값**입니다. 이 예시에서 `index`의 초깃값은 `useState(0)`에 의해 `0`으로 설정됩니다.\n\n컴포넌트가 렌더링될 때마다, `useState`는 다음 두 개의 값을 포함하는 배열을 제공합니다.\n\n1. 저장한 값을 가진 **state 변수** (`index`).\n2. state 변수를 업데이트하고 React에 컴포넌트를 다시 렌더링하도록 유발하는 **state setter 함수** (`setIndex`).\n\n실제 작동 방식은 다음과 같습니다.\n\n```js\nconst [index, setIndex] = useState(0);\n```\n\n1. **컴포넌트가 처음 렌더링 됩니다.** `index`의 초깃값으로 `useState`를 사용해 `0`을 전달했으므로 `[0, setIndex]`를 반환합니다. React는 `0`을 최신 state 값으로 기억합니다.\n2. **state를 업데이트합니다.** 사용자가 버튼을 클릭하면 `setIndex(index + 1)`를 호출합니다. `index`는 `0`이므로 `setIndex(1)`입니다. 이는 React에 `index`는 `1`임을 기억하게 하고 또 다른 렌더링을 유발합니다.\n3. **컴포넌트가 두 번째로 렌더링 됩니다.** React는 여전히 `useState(0)`를 보지만, `index`를 `1`로 설정한 것을 기억하고 있기 때문에, 이번에는 `[1, setIndex]`를 반환합니다.\n4. 이런 식으로 계속됩니다!\n\n## 컴포넌트에 여러 state 변수 지정하기 {/*giving-a-component-multiple-state-variables*/}\n\n하나의 컴포넌트에 원하는 만큼 많은 타입의 state 변수를 가질 수 있습니다. 이 컴포넌트는 숫자 타입 `index`와 \"Show details\"를 클릭했을 때 토글 되는 불리언 타입인 `showMore`라는 두 개의 state 변수를 가지고 있습니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\nimport { sculptureList } from './data.js';\n\nexport default function Gallery() {\n  const [index, setIndex] = useState(0);\n  const [showMore, setShowMore] = useState(false);\n\n  function handleNextClick() {\n    setIndex(index + 1);\n  }\n\n  function handleMoreClick() {\n    setShowMore(!showMore);\n  }\n\n  let sculpture = sculptureList[index];\n  return (\n    <>\n      <button onClick={handleNextClick}>\n        Next\n      </button>\n      <h2>\n        <i>{sculpture.name} </i>\n        by {sculpture.artist}\n      </h2>\n      <h3>\n        ({index + 1} of {sculptureList.length})\n      </h3>\n      <button onClick={handleMoreClick}>\n        {showMore ? 'Hide' : 'Show'} details\n      </button>\n      {showMore && <p>{sculpture.description}</p>}\n      <img\n        src={sculpture.url}\n        alt={sculpture.alt}\n      />\n    </>\n  );\n}\n```\n\n```js src/data.js\nexport const sculptureList = [{\n  name: 'Homenaje a la Neurocirugía',\n  artist: 'Marta Colvin Andrade',\n  description: 'Although Colvin is predominantly known for abstract themes that allude to pre-Hispanic symbols, this gigantic sculpture, an homage to neurosurgery, is one of her most recognizable public art pieces.',\n  url: 'https://i.imgur.com/Mx7dA2Y.jpg',\n  alt: 'A bronze statue of two crossed hands delicately holding a human brain in their fingertips.'\n}, {\n  name: 'Floralis Genérica',\n  artist: 'Eduardo Catalano',\n  description: 'This enormous (75 ft. or 23m) silver flower is located in Buenos Aires. It is designed to move, closing its petals in the evening or when strong winds blow and opening them in the morning.',\n  url: 'https://i.imgur.com/ZF6s192m.jpg',\n  alt: 'A gigantic metallic flower sculpture with reflective mirror-like petals and strong stamens.'\n}, {\n  name: 'Eternal Presence',\n  artist: 'John Woodrow Wilson',\n  description: 'Wilson was known for his preoccupation with equality, social justice, as well as the essential and spiritual qualities of humankind. This massive (7ft. or 2,13m) bronze represents what he described as \"a symbolic Black presence infused with a sense of universal humanity.\"',\n  url: 'https://i.imgur.com/aTtVpES.jpg',\n  alt: 'The sculpture depicting a human head seems ever-present and solemn. It radiates calm and serenity.'\n}, {\n  name: 'Moai',\n  artist: 'Unknown Artist',\n  description: 'Located on the Easter Island, there are 1,000 moai, or extant monumental statues, created by the early Rapa Nui people, which some believe represented deified ancestors.',\n  url: 'https://i.imgur.com/RCwLEoQm.jpg',\n  alt: 'Three monumental stone busts with the heads that are disproportionately large with somber faces.'\n}, {\n  name: 'Blue Nana',\n  artist: 'Niki de Saint Phalle',\n  description: 'The Nanas are triumphant creatures, symbols of femininity and maternity. Initially, Saint Phalle used fabric and found objects for the Nanas, and later on introduced polyester to achieve a more vibrant effect.',\n  url: 'https://i.imgur.com/Sd1AgUOm.jpg',\n  alt: 'A large mosaic sculpture of a whimsical dancing female figure in a colorful costume emanating joy.'\n}, {\n  name: 'Ultimate Form',\n  artist: 'Barbara Hepworth',\n  description: 'This abstract bronze sculpture is a part of The Family of Man series located at Yorkshire Sculpture Park. Hepworth chose not to create literal representations of the world but developed abstract forms inspired by people and landscapes.',\n  url: 'https://i.imgur.com/2heNQDcm.jpg',\n  alt: 'A tall sculpture made of three elements stacked on each other reminding of a human figure.'\n}, {\n  name: 'Cavaliere',\n  artist: 'Lamidi Olonade Fakeye',\n  description: \"Descended from four generations of woodcarvers, Fakeye's work blended traditional and contemporary Yoruba themes.\",\n  url: 'https://i.imgur.com/wIdGuZwm.png',\n  alt: 'An intricate wood sculpture of a warrior with a focused face on a horse adorned with patterns.'\n}, {\n  name: 'Big Bellies',\n  artist: 'Alina Szapocznikow',\n  description: \"Szapocznikow is known for her sculptures of the fragmented body as a metaphor for the fragility and impermanence of youth and beauty. This sculpture depicts two very realistic large bellies stacked on top of each other, each around five feet (1,5m) tall.\",\n  url: 'https://i.imgur.com/AlHTAdDm.jpg',\n  alt: 'The sculpture reminds a cascade of folds, quite different from bellies in classical sculptures.'\n}, {\n  name: 'Terracotta Army',\n  artist: 'Unknown Artist',\n  description: 'The Terracotta Army is a collection of terracotta sculptures depicting the armies of Qin Shi Huang, the first Emperor of China. The army consisted of more than 8,000 soldiers, 130 chariots with 520 horses, and 150 cavalry horses.',\n  url: 'https://i.imgur.com/HMFmH6m.jpg',\n  alt: '12 terracotta sculptures of solemn warriors, each with a unique facial expression and armor.'\n}, {\n  name: 'Lunar Landscape',\n  artist: 'Louise Nevelson',\n  description: 'Nevelson was known for scavenging objects from New York City debris, which she would later assemble into monumental constructions. In this one, she used disparate parts like a bedpost, juggling pin, and seat fragment, nailing and gluing them into boxes that reflect the influence of Cubism’s geometric abstraction of space and form.',\n  url: 'https://i.imgur.com/rN7hY6om.jpg',\n  alt: 'A black matte sculpture where the individual elements are initially indistinguishable.'\n}, {\n  name: 'Aureole',\n  artist: 'Ranjani Shettar',\n  description: 'Shettar merges the traditional and the modern, the natural and the industrial. Her art focuses on the relationship between man and nature. Her work was described as compelling both abstractly and figuratively, gravity defying, and a \"fine synthesis of unlikely materials.\"',\n  url: 'https://i.imgur.com/okTpbHhm.jpg',\n  alt: 'A pale wire-like sculpture mounted on concrete wall and descending on the floor. It appears light.'\n}, {\n  name: 'Hippos',\n  artist: 'Taipei Zoo',\n  description: 'The Taipei Zoo commissioned a Hippo Square featuring submerged hippos at play.',\n  url: 'https://i.imgur.com/6o5Vuyu.jpg',\n  alt: 'A group of bronze hippo sculptures emerging from the sett sidewalk as if they were swimming.'\n}];\n```\n\n```css\nh2 { margin-top: 10px; margin-bottom: 0; }\nh3 {\n margin-top: 5px;\n font-weight: normal;\n font-size: 100%;\n}\nimg { width: 120px; height: 120px; }\nbutton {\n  display: block;\n  margin-top: 10px;\n  margin-bottom: 10px;\n}\n```\n\n</Sandpack>\n\n이 예시에서 `index`와 `showMore`처럼 서로 연관이 없는 경우 여러 개의 state 변수를 가지는 것이 좋습니다. 하지만 두 state 변수를 자주 함께 변경하는 경우에는 두 변수를 하나로 합치는 것이 더 좋을 수 있습니다. 예를 들어, 필드가 많은 폼의 경우 필드별로 state 변수를 사용하는 것보다 하나의 객체 state 변수를 사용하는 것이 더 편리합니다. 더 많은 팁은 [state 구조 선택](/learn/choosing-the-state-structure)에서 확인할 수 있습니다.\n\n<DeepDive>\n\n#### React는 어떤 state를 반환할지 어떻게 알 수 있을까요? {/*how-does-react-know-which-state-to-return*/}\n\n`useState` 호출이 *어떤* state 변수를 참조하는지에 대한 정보를 받지 못한다는 것을 눈치채셨을 것입니다. `useState`에 전달되는 \"식별자\"가 없는데 어떤 변수를 반환할지 어떻게 알 수 있을까요? 함수를 파싱하는 것과 같은 마법에 의존할까요? 대답은 '아니오' 입니다.\n\n대신 간결한 구문을 구현하기 위해 훅은 **동일한 컴포넌트의 모든 렌더링에서 안정적인 호출 순서에 의존합니다.** 위의 규칙(\"최상위 수준에서만 훅 호출\")을 따르면, 훅은 항상 같은 순서로 호출되기 때문에 실제로 잘 작동합니다. 또한, [린터 플러그인](https://www.npmjs.com/package/eslint-plugin-react-hooks)은 대부분의 실수를 잡아줍니다.\n\n내부적으로 React는 모든 컴포넌트에 대해 한 쌍의 state 배열을 가집니다. 또한 렌더링 전에 `0`으로 설정된 현재 인덱스 쌍을 유지합니다. `useState`를 호출할 때마다, React는 다음 state 쌍을 제공하고 인덱스를 증가시킵니다. 이 메커니즘에 대한 자세한 내용은 [React 훅: 마법이 아닌 배열](https://medium.com/@ryardley/react-hooks-not-magic-just-arrays-cd4f1857236e)에서 확인할 수 있습니다.\n\n이 예시에서는 **React를 사용하지 않지만,** 내부적으로 `useState`가 어떻게 작동하는지에 대한 아이디어를 제공합니다.\n\n<Sandpack>\n\n```js src/index.js active\nlet componentHooks = [];\nlet currentHookIndex = 0;\n\n// React 내부에서 useState가 작동하는 방식 (단순화됨).\nfunction useState(initialState) {\n  let pair = componentHooks[currentHookIndex];\n  if (pair) {\n    // 첫 번째 렌더링이 아니므로\n    // state 쌍이 이미 존재합니다.\n    // 이를 반환하고 다음 Hook 호출을 준비합니다.\n    currentHookIndex++;\n    return pair;\n  }\n\n  // 처음 렌더링하는 것이므로\n  // state 쌍을 생성하고 저장합니다.\n  pair = [initialState, setState];\n\n  function setState(nextState) {\n    // 사용자가 state 변경을 요청하면\n    // 새 값을 쌍에 넣습니다.\n    pair[0] = nextState;\n    updateDOM();\n  }\n\n  // 이후 렌더링을 위해 쌍을 저장하고\n  // 다음 Hook 호출을 준비합니다.\n  componentHooks[currentHookIndex] = pair;\n  currentHookIndex++;\n  return pair;\n}\n\nfunction Gallery() {\n  // 각 useState() 호출은 다음 쌍을 가져옵니다.\n  const [index, setIndex] = useState(0);\n  const [showMore, setShowMore] = useState(false);\n\n  function handleNextClick() {\n    setIndex(index + 1);\n  }\n\n  function handleMoreClick() {\n    setShowMore(!showMore);\n  }\n\n  let sculpture = sculptureList[index];\n  // 이 예시는 React를 사용하지 않으므로\n  // JSX 대신 출력 객체를 반환합니다.\n  return {\n    onNextClick: handleNextClick,\n    onMoreClick: handleMoreClick,\n    header: `${sculpture.name} by ${sculpture.artist}`,\n    counter: `${index + 1} of ${sculptureList.length}`,\n    more: `${showMore ? 'Hide' : 'Show'} details`,\n    description: showMore ? sculpture.description : null,\n    imageSrc: sculpture.url,\n    imageAlt: sculpture.alt\n  };\n}\n\nfunction updateDOM() {\n  // 컴포넌트를 렌더링하기 전에\n  // 현재 Hook 인덱스를 초기화합니다.\n  currentHookIndex = 0;\n  let output = Gallery();\n\n  // 출력과 일치하도록 DOM을 업데이트합니다.\n  // 이 부분은 React가 대신 해줍니다.\n  nextButton.onclick = output.onNextClick;\n  header.textContent = output.header;\n  moreButton.onclick = output.onMoreClick;\n  moreButton.textContent = output.more;\n  image.src = output.imageSrc;\n  image.alt = output.imageAlt;\n  if (output.description !== null) {\n    description.textContent = output.description;\n    description.style.display = '';\n  } else {\n    description.style.display = 'none';\n  }\n}\n\nlet nextButton = document.getElementById('nextButton');\nlet header = document.getElementById('header');\nlet moreButton = document.getElementById('moreButton');\nlet description = document.getElementById('description');\nlet image = document.getElementById('image');\nlet sculptureList = [{\n  name: 'Homenaje a la Neurocirugía',\n  artist: 'Marta Colvin Andrade',\n  description: 'Although Colvin is predominantly known for abstract themes that allude to pre-Hispanic symbols, this gigantic sculpture, an homage to neurosurgery, is one of her most recognizable public art pieces.',\n  url: 'https://i.imgur.com/Mx7dA2Y.jpg',\n  alt: 'A bronze statue of two crossed hands delicately holding a human brain in their fingertips.'\n}, {\n  name: 'Floralis Genérica',\n  artist: 'Eduardo Catalano',\n  description: 'This enormous (75 ft. or 23m) silver flower is located in Buenos Aires. It is designed to move, closing its petals in the evening or when strong winds blow and opening them in the morning.',\n  url: 'https://i.imgur.com/ZF6s192m.jpg',\n  alt: 'A gigantic metallic flower sculpture with reflective mirror-like petals and strong stamens.'\n}, {\n  name: 'Eternal Presence',\n  artist: 'John Woodrow Wilson',\n  description: 'Wilson was known for his preoccupation with equality, social justice, as well as the essential and spiritual qualities of humankind. This massive (7ft. or 2,13m) bronze represents what he described as \"a symbolic Black presence infused with a sense of universal humanity.\"',\n  url: 'https://i.imgur.com/aTtVpES.jpg',\n  alt: 'The sculpture depicting a human head seems ever-present and solemn. It radiates calm and serenity.'\n}, {\n  name: 'Moai',\n  artist: 'Unknown Artist',\n  description: 'Located on the Easter Island, there are 1,000 moai, or extant monumental statues, created by the early Rapa Nui people, which some believe represented deified ancestors.',\n  url: 'https://i.imgur.com/RCwLEoQm.jpg',\n  alt: 'Three monumental stone busts with the heads that are disproportionately large with somber faces.'\n}, {\n  name: 'Blue Nana',\n  artist: 'Niki de Saint Phalle',\n  description: 'The Nanas are triumphant creatures, symbols of femininity and maternity. Initially, Saint Phalle used fabric and found objects for the Nanas, and later on introduced polyester to achieve a more vibrant effect.',\n  url: 'https://i.imgur.com/Sd1AgUOm.jpg',\n  alt: 'A large mosaic sculpture of a whimsical dancing female figure in a colorful costume emanating joy.'\n}, {\n  name: 'Ultimate Form',\n  artist: 'Barbara Hepworth',\n  description: 'This abstract bronze sculpture is a part of The Family of Man series located at Yorkshire Sculpture Park. Hepworth chose not to create literal representations of the world but developed abstract forms inspired by people and landscapes.',\n  url: 'https://i.imgur.com/2heNQDcm.jpg',\n  alt: 'A tall sculpture made of three elements stacked on each other reminding of a human figure.'\n}, {\n  name: 'Cavaliere',\n  artist: 'Lamidi Olonade Fakeye',\n  description: \"Descended from four generations of woodcarvers, Fakeye's work blended traditional and contemporary Yoruba themes.\",\n  url: 'https://i.imgur.com/wIdGuZwm.png',\n  alt: 'An intricate wood sculpture of a warrior with a focused face on a horse adorned with patterns.'\n}, {\n  name: 'Big Bellies',\n  artist: 'Alina Szapocznikow',\n  description: \"Szapocznikow is known for her sculptures of the fragmented body as a metaphor for the fragility and impermanence of youth and beauty. This sculpture depicts two very realistic large bellies stacked on top of each other, each around five feet (1,5m) tall.\",\n  url: 'https://i.imgur.com/AlHTAdDm.jpg',\n  alt: 'The sculpture reminds a cascade of folds, quite different from bellies in classical sculptures.'\n}, {\n  name: 'Terracotta Army',\n  artist: 'Unknown Artist',\n  description: 'The Terracotta Army is a collection of terracotta sculptures depicting the armies of Qin Shi Huang, the first Emperor of China. The army consisted of more than 8,000 soldiers, 130 chariots with 520 horses, and 150 cavalry horses.',\n  url: 'https://i.imgur.com/HMFmH6m.jpg',\n  alt: '12 terracotta sculptures of solemn warriors, each with a unique facial expression and armor.'\n}, {\n  name: 'Lunar Landscape',\n  artist: 'Louise Nevelson',\n  description: 'Nevelson was known for scavenging objects from New York City debris, which she would later assemble into monumental constructions. In this one, she used disparate parts like a bedpost, juggling pin, and seat fragment, nailing and gluing them into boxes that reflect the influence of Cubism’s geometric abstraction of space and form.',\n  url: 'https://i.imgur.com/rN7hY6om.jpg',\n  alt: 'A black matte sculpture where the individual elements are initially indistinguishable.'\n}, {\n  name: 'Aureole',\n  artist: 'Ranjani Shettar',\n  description: 'Shettar merges the traditional and the modern, the natural and the industrial. Her art focuses on the relationship between man and nature. Her work was described as compelling both abstractly and figuratively, gravity defying, and a \"fine synthesis of unlikely materials.\"',\n  url: 'https://i.imgur.com/okTpbHhm.jpg',\n  alt: 'A pale wire-like sculpture mounted on concrete wall and descending on the floor. It appears light.'\n}, {\n  name: 'Hippos',\n  artist: 'Taipei Zoo',\n  description: 'The Taipei Zoo commissioned a Hippo Square featuring submerged hippos at play.',\n  url: 'https://i.imgur.com/6o5Vuyu.jpg',\n  alt: 'A group of bronze hippo sculptures emerging from the sett sidewalk as if they were swimming.'\n}];\n\n// UI를 초기 state와 일치시킵니다.\nupdateDOM();\n```\n\n```html public/index.html\n<button id=\"nextButton\">\n  Next\n</button>\n<h3 id=\"header\"></h3>\n<button id=\"moreButton\"></button>\n<p id=\"description\"></p>\n<img id=\"image\">\n\n<style>\n* { box-sizing: border-box; }\nbody { font-family: sans-serif; margin: 20px; padding: 0; }\nbutton { display: block; margin-bottom: 10px; }\n</style>\n```\n\n```css\nbutton { display: block; margin-bottom: 10px; }\n```\n\n</Sandpack>\n\n이해하지 않아도 React를 사용하는 데 문제는 없지만, 도움이 되는 사고방식으로서 유용할 수 있을 것입니다.\n\n</DeepDive>\n\n## State는 격리되고 비공개로 유지됩니다 {/*state-is-isolated-and-private*/}\n\nState는 화면에서 컴포넌트 인스턴스에 지역적입니다. 다시 말해, **동일한 컴포넌트를 두 번 렌더링한다면 각 복사본은 완전히 격리된 state를 가집니다!** 그중 하나를 변경해도 다른 하나에는 영향을 미치지 않습니다.\n\n이 예시에서 이전에 나왔던 `Gallery` 컴포넌트가 로직 변경 없이 두 번 렌더링되었습니다. 각각의 갤러리 내부 버튼을 클릭해 보세요. 그들의 state가 서로 독립적임을 주목하세요.\n<Sandpack>\n\n```js\nimport Gallery from './Gallery.js';\n\nexport default function Page() {\n  return (\n    <div className=\"Page\">\n      <Gallery />\n      <Gallery />\n    </div>\n  );\n}\n\n```\n\n```js src/Gallery.js\nimport { useState } from 'react';\nimport { sculptureList } from './data.js';\n\nexport default function Gallery() {\n  const [index, setIndex] = useState(0);\n  const [showMore, setShowMore] = useState(false);\n\n  function handleNextClick() {\n    setIndex(index + 1);\n  }\n\n  function handleMoreClick() {\n    setShowMore(!showMore);\n  }\n\n  let sculpture = sculptureList[index];\n  return (\n    <section>\n      <button onClick={handleNextClick}>\n        Next\n      </button>\n      <h2>\n        <i>{sculpture.name} </i>\n        by {sculpture.artist}\n      </h2>\n      <h3>\n        ({index + 1} of {sculptureList.length})\n      </h3>\n      <button onClick={handleMoreClick}>\n        {showMore ? 'Hide' : 'Show'} details\n      </button>\n      {showMore && <p>{sculpture.description}</p>}\n      <img\n        src={sculpture.url}\n        alt={sculpture.alt}\n      />\n    </section>\n  );\n}\n```\n\n```js src/data.js\nexport const sculptureList = [{\n  name: 'Homenaje a la Neurocirugía',\n  artist: 'Marta Colvin Andrade',\n  description: 'Although Colvin is predominantly known for abstract themes that allude to pre-Hispanic symbols, this gigantic sculpture, an homage to neurosurgery, is one of her most recognizable public art pieces.',\n  url: 'https://i.imgur.com/Mx7dA2Y.jpg',\n  alt: 'A bronze statue of two crossed hands delicately holding a human brain in their fingertips.'\n}, {\n  name: 'Floralis Genérica',\n  artist: 'Eduardo Catalano',\n  description: 'This enormous (75 ft. or 23m) silver flower is located in Buenos Aires. It is designed to move, closing its petals in the evening or when strong winds blow and opening them in the morning.',\n  url: 'https://i.imgur.com/ZF6s192m.jpg',\n  alt: 'A gigantic metallic flower sculpture with reflective mirror-like petals and strong stamens.'\n}, {\n  name: 'Eternal Presence',\n  artist: 'John Woodrow Wilson',\n  description: 'Wilson was known for his preoccupation with equality, social justice, as well as the essential and spiritual qualities of humankind. This massive (7ft. or 2,13m) bronze represents what he described as \"a symbolic Black presence infused with a sense of universal humanity.\"',\n  url: 'https://i.imgur.com/aTtVpES.jpg',\n  alt: 'The sculpture depicting a human head seems ever-present and solemn. It radiates calm and serenity.'\n}, {\n  name: 'Moai',\n  artist: 'Unknown Artist',\n  description: 'Located on the Easter Island, there are 1,000 moai, or extant monumental statues, created by the early Rapa Nui people, which some believe represented deified ancestors.',\n  url: 'https://i.imgur.com/RCwLEoQm.jpg',\n  alt: 'Three monumental stone busts with the heads that are disproportionately large with somber faces.'\n}, {\n  name: 'Blue Nana',\n  artist: 'Niki de Saint Phalle',\n  description: 'The Nanas are triumphant creatures, symbols of femininity and maternity. Initially, Saint Phalle used fabric and found objects for the Nanas, and later on introduced polyester to achieve a more vibrant effect.',\n  url: 'https://i.imgur.com/Sd1AgUOm.jpg',\n  alt: 'A large mosaic sculpture of a whimsical dancing female figure in a colorful costume emanating joy.'\n}, {\n  name: 'Ultimate Form',\n  artist: 'Barbara Hepworth',\n  description: 'This abstract bronze sculpture is a part of The Family of Man series located at Yorkshire Sculpture Park. Hepworth chose not to create literal representations of the world but developed abstract forms inspired by people and landscapes.',\n  url: 'https://i.imgur.com/2heNQDcm.jpg',\n  alt: 'A tall sculpture made of three elements stacked on each other reminding of a human figure.'\n}, {\n  name: 'Cavaliere',\n  artist: 'Lamidi Olonade Fakeye',\n  description: \"Descended from four generations of woodcarvers, Fakeye's work blended traditional and contemporary Yoruba themes.\",\n  url: 'https://i.imgur.com/wIdGuZwm.png',\n  alt: 'An intricate wood sculpture of a warrior with a focused face on a horse adorned with patterns.'\n}, {\n  name: 'Big Bellies',\n  artist: 'Alina Szapocznikow',\n  description: \"Szapocznikow is known for her sculptures of the fragmented body as a metaphor for the fragility and impermanence of youth and beauty. This sculpture depicts two very realistic large bellies stacked on top of each other, each around five feet (1,5m) tall.\",\n  url: 'https://i.imgur.com/AlHTAdDm.jpg',\n  alt: 'The sculpture reminds a cascade of folds, quite different from bellies in classical sculptures.'\n}, {\n  name: 'Terracotta Army',\n  artist: 'Unknown Artist',\n  description: 'The Terracotta Army is a collection of terracotta sculptures depicting the armies of Qin Shi Huang, the first Emperor of China. The army consisted of more than 8,000 soldiers, 130 chariots with 520 horses, and 150 cavalry horses.',\n  url: 'https://i.imgur.com/HMFmH6m.jpg',\n  alt: '12 terracotta sculptures of solemn warriors, each with a unique facial expression and armor.'\n}, {\n  name: 'Lunar Landscape',\n  artist: 'Louise Nevelson',\n  description: 'Nevelson was known for scavenging objects from New York City debris, which she would later assemble into monumental constructions. In this one, she used disparate parts like a bedpost, juggling pin, and seat fragment, nailing and gluing them into boxes that reflect the influence of Cubism’s geometric abstraction of space and form.',\n  url: 'https://i.imgur.com/rN7hY6om.jpg',\n  alt: 'A black matte sculpture where the individual elements are initially indistinguishable.'\n}, {\n  name: 'Aureole',\n  artist: 'Ranjani Shettar',\n  description: 'Shettar merges the traditional and the modern, the natural and the industrial. Her art focuses on the relationship between man and nature. Her work was described as compelling both abstractly and figuratively, gravity defying, and a \"fine synthesis of unlikely materials.\"',\n  url: 'https://i.imgur.com/okTpbHhm.jpg',\n  alt: 'A pale wire-like sculpture mounted on concrete wall and descending on the floor. It appears light.'\n}, {\n  name: 'Hippos',\n  artist: 'Taipei Zoo',\n  description: 'The Taipei Zoo commissioned a Hippo Square featuring submerged hippos at play.',\n  url: 'https://i.imgur.com/6o5Vuyu.jpg',\n  alt: 'A group of bronze hippo sculptures emerging from the sett sidewalk as if they were swimming.'\n}];\n```\n\n```css\nbutton { display: block; margin-bottom: 10px; }\n.Page > * {\n  float: left;\n  width: 50%;\n  padding: 10px;\n}\nh2 { margin-top: 10px; margin-bottom: 0; }\nh3 {\n  margin-top: 5px;\n  font-weight: normal;\n  font-size: 100%;\n}\nimg { width: 120px; height: 120px; }\nbutton {\n  display: block;\n  margin-top: 10px;\n  margin-bottom: 10px;\n}\n```\n\n</Sandpack>\n\n이것이 state를 일반적인 모듈 상단에 선언할 수 있는 보통의 변수와 구별하는 요소입니다. State는 특정 함수 호출이나 코드 내의 특정 위치와 관련이 없습니다. 대신, 화면의 특정 위치에 \"지역적\"입니다. `<Gallery />` 컴포넌트를 두 번 렌더링했으므로 그들의 state는 별도로 저장됩니다.\n\n또한 `Page` 컴포넌트가 `Gallery`의 state에 대해 아무것도 \"알지\" 않는다는 점과 심지어 그것이 있는지도 모른다는 것에 주목하세요. Props와 달리, **state는 선언한 컴포넌트에 완전히 비공개입니다.** 부모 컴포넌트는 이를 변경할 수 없습니다. 이로써 다른 컴포넌트에 영향을 미치지 않고 어떤 컴포넌트에든 state를 추가하거나 제거할 수 있게 됩니다.\n\n만약 두 개의 갤러리가 state를 동기화하길 원한다면, React에서 올바른 방법은 자식 컴포넌트에서 state를 *제거*하고 가장 가까운 공통 부모 컴포넌트에 추가하는 것입니다. 다음 몇 페이지는 단일 컴포넌트의 state 구성에 중점을 두겠지만, 이 주제는 [컴포넌트 간 state 공유](/learn/sharing-state-between-components)에서 다시 다룰 것입니다.\n\n<Recap>\n\n* 컴포넌트가 렌더링 간에 어떤 정보를 \"기억\"해야 할 때 state 변수를 사용합니다.\n* state 변수는 `useState` 훅을 호출하여 선언합니다.\n* 훅은 use로 시작하는 특별한 함수들입니다. 이들은 state와 같은 React 기능에 \"연결\"할 수 있도록 해줍니다.\n* 훅은 import와 마찬가지로 반드시 호출되어야 합니다. `useState`를 포함한 훅을 호출하는 것은 컴포넌트나 다른 훅의 최상위 수준에서만 유효합니다.\n* `useState` 훅은 현재 state와 이를 업데이트할 함수로 이루어진 한 쌍을 반환합니다.\n* 여러 개의 state 변수를 가질 수 있습니다. React 내부에서는 그들을 순서대로 매칭합니다.\n* state는 컴포넌트에 비공개입니다. 두 곳에서 렌더링하더라도 각각의 복사본은 고유한 state를 가집니다.\n\n</Recap>\n\n\n\n<Challenges>\n\n#### 갤러리 완성하기 {/*complete-the-gallery*/}\n\n마지막 조각상에서 \"Next\"를 누르면 코드가 충돌합니다. 로직을 수정하여 이를 해결하세요. 이벤트 핸들러에 추가로 로직을 추가하거나 동작이 불가능할 때 버튼을 비활성화하여 이를 처리할 수 있습니다.\n\n충돌을 수정한 후, 이전 조각상을 표시하는 \"Previous\" 버튼을 추가하세요. 첫 번째 조각상에서는 충돌이 발생하지 않아야 합니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\nimport { sculptureList } from './data.js';\n\nexport default function Gallery() {\n  const [index, setIndex] = useState(0);\n  const [showMore, setShowMore] = useState(false);\n\n  function handleNextClick() {\n    setIndex(index + 1);\n  }\n\n  function handleMoreClick() {\n    setShowMore(!showMore);\n  }\n\n  let sculpture = sculptureList[index];\n  return (\n    <>\n      <button onClick={handleNextClick}>\n        Next\n      </button>\n      <h2>\n        <i>{sculpture.name} </i>\n        by {sculpture.artist}\n      </h2>\n      <h3>\n        ({index + 1} of {sculptureList.length})\n      </h3>\n      <button onClick={handleMoreClick}>\n        {showMore ? 'Hide' : 'Show'} details\n      </button>\n      {showMore && <p>{sculpture.description}</p>}\n      <img\n        src={sculpture.url}\n        alt={sculpture.alt}\n      />\n    </>\n  );\n}\n```\n\n```js src/data.js\nexport const sculptureList = [{\n  name: 'Homenaje a la Neurocirugía',\n  artist: 'Marta Colvin Andrade',\n  description: 'Although Colvin is predominantly known for abstract themes that allude to pre-Hispanic symbols, this gigantic sculpture, an homage to neurosurgery, is one of her most recognizable public art pieces.',\n  url: 'https://i.imgur.com/Mx7dA2Y.jpg',\n  alt: 'A bronze statue of two crossed hands delicately holding a human brain in their fingertips.'\n}, {\n  name: 'Floralis Genérica',\n  artist: 'Eduardo Catalano',\n  description: 'This enormous (75 ft. or 23m) silver flower is located in Buenos Aires. It is designed to move, closing its petals in the evening or when strong winds blow and opening them in the morning.',\n  url: 'https://i.imgur.com/ZF6s192m.jpg',\n  alt: 'A gigantic metallic flower sculpture with reflective mirror-like petals and strong stamens.'\n}, {\n  name: 'Eternal Presence',\n  artist: 'John Woodrow Wilson',\n  description: 'Wilson was known for his preoccupation with equality, social justice, as well as the essential and spiritual qualities of humankind. This massive (7ft. or 2,13m) bronze represents what he described as \"a symbolic Black presence infused with a sense of universal humanity.\"',\n  url: 'https://i.imgur.com/aTtVpES.jpg',\n  alt: 'The sculpture depicting a human head seems ever-present and solemn. It radiates calm and serenity.'\n}, {\n  name: 'Moai',\n  artist: 'Unknown Artist',\n  description: 'Located on the Easter Island, there are 1,000 moai, or extant monumental statues, created by the early Rapa Nui people, which some believe represented deified ancestors.',\n  url: 'https://i.imgur.com/RCwLEoQm.jpg',\n  alt: 'Three monumental stone busts with the heads that are disproportionately large with somber faces.'\n}, {\n  name: 'Blue Nana',\n  artist: 'Niki de Saint Phalle',\n  description: 'The Nanas are triumphant creatures, symbols of femininity and maternity. Initially, Saint Phalle used fabric and found objects for the Nanas, and later on introduced polyester to achieve a more vibrant effect.',\n  url: 'https://i.imgur.com/Sd1AgUOm.jpg',\n  alt: 'A large mosaic sculpture of a whimsical dancing female figure in a colorful costume emanating joy.'\n}, {\n  name: 'Ultimate Form',\n  artist: 'Barbara Hepworth',\n  description: 'This abstract bronze sculpture is a part of The Family of Man series located at Yorkshire Sculpture Park. Hepworth chose not to create literal representations of the world but developed abstract forms inspired by people and landscapes.',\n  url: 'https://i.imgur.com/2heNQDcm.jpg',\n  alt: 'A tall sculpture made of three elements stacked on each other reminding of a human figure.'\n}, {\n  name: 'Cavaliere',\n  artist: 'Lamidi Olonade Fakeye',\n  description: \"Descended from four generations of woodcarvers, Fakeye's work blended traditional and contemporary Yoruba themes.\",\n  url: 'https://i.imgur.com/wIdGuZwm.png',\n  alt: 'An intricate wood sculpture of a warrior with a focused face on a horse adorned with patterns.'\n}, {\n  name: 'Big Bellies',\n  artist: 'Alina Szapocznikow',\n  description: \"Szapocznikow is known for her sculptures of the fragmented body as a metaphor for the fragility and impermanence of youth and beauty. This sculpture depicts two very realistic large bellies stacked on top of each other, each around five feet (1,5m) tall.\",\n  url: 'https://i.imgur.com/AlHTAdDm.jpg',\n  alt: 'The sculpture reminds a cascade of folds, quite different from bellies in classical sculptures.'\n}, {\n  name: 'Terracotta Army',\n  artist: 'Unknown Artist',\n  description: 'The Terracotta Army is a collection of terracotta sculptures depicting the armies of Qin Shi Huang, the first Emperor of China. The army consisted of more than 8,000 soldiers, 130 chariots with 520 horses, and 150 cavalry horses.',\n  url: 'https://i.imgur.com/HMFmH6m.jpg',\n  alt: '12 terracotta sculptures of solemn warriors, each with a unique facial expression and armor.'\n}, {\n  name: 'Lunar Landscape',\n  artist: 'Louise Nevelson',\n  description: 'Nevelson was known for scavenging objects from New York City debris, which she would later assemble into monumental constructions. In this one, she used disparate parts like a bedpost, juggling pin, and seat fragment, nailing and gluing them into boxes that reflect the influence of Cubism’s geometric abstraction of space and form.',\n  url: 'https://i.imgur.com/rN7hY6om.jpg',\n  alt: 'A black matte sculpture where the individual elements are initially indistinguishable.'\n}, {\n  name: 'Aureole',\n  artist: 'Ranjani Shettar',\n  description: 'Shettar merges the traditional and the modern, the natural and the industrial. Her art focuses on the relationship between man and nature. Her work was described as compelling both abstractly and figuratively, gravity defying, and a \"fine synthesis of unlikely materials.\"',\n  url: 'https://i.imgur.com/okTpbHhm.jpg',\n  alt: 'A pale wire-like sculpture mounted on concrete wall and descending on the floor. It appears light.'\n}, {\n  name: 'Hippos',\n  artist: 'Taipei Zoo',\n  description: 'The Taipei Zoo commissioned a Hippo Square featuring submerged hippos at play.',\n  url: 'https://i.imgur.com/6o5Vuyu.jpg',\n  alt: 'A group of bronze hippo sculptures emerging from the sett sidewalk as if they were swimming.'\n}];\n```\n\n```css\nbutton { display: block; margin-bottom: 10px; }\n.Page > * {\n  float: left;\n  width: 50%;\n  padding: 10px;\n}\nh2 { margin-top: 10px; margin-bottom: 0; }\nh3 {\n  margin-top: 5px;\n  font-weight: normal;\n  font-size: 100%;\n}\nimg { width: 120px; height: 120px; }\n```\n\n</Sandpack>\n\n<Solution>\n\n이것은 각 이벤트 핸들러 내에 방어 조건을 추가하고 필요할 때 버튼을 비활성화하는 방식으로 구현합니다:\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\nimport { sculptureList } from './data.js';\n\nexport default function Gallery() {\n  const [index, setIndex] = useState(0);\n  const [showMore, setShowMore] = useState(false);\n\n  let hasPrev = index > 0;\n  let hasNext = index < sculptureList.length - 1;\n\n  function handlePrevClick() {\n    if (hasPrev) {\n      setIndex(index - 1);\n    }\n  }\n\n  function handleNextClick() {\n    if (hasNext) {\n      setIndex(index + 1);\n    }\n  }\n\n  function handleMoreClick() {\n    setShowMore(!showMore);\n  }\n\n  let sculpture = sculptureList[index];\n  return (\n    <>\n      <button\n        onClick={handlePrevClick}\n        disabled={!hasPrev}\n      >\n        Previous\n      </button>\n      <button\n        onClick={handleNextClick}\n        disabled={!hasNext}\n      >\n        Next\n      </button>\n      <h2>\n        <i>{sculpture.name} </i>\n        by {sculpture.artist}\n      </h2>\n      <h3>\n        ({index + 1} of {sculptureList.length})\n      </h3>\n      <button onClick={handleMoreClick}>\n        {showMore ? 'Hide' : 'Show'} details\n      </button>\n      {showMore && <p>{sculpture.description}</p>}\n      <img\n        src={sculpture.url}\n        alt={sculpture.alt}\n      />\n    </>\n  );\n}\n```\n\n```js src/data.js hidden\nexport const sculptureList = [{\n  name: 'Homenaje a la Neurocirugía',\n  artist: 'Marta Colvin Andrade',\n  description: 'Although Colvin is predominantly known for abstract themes that allude to pre-Hispanic symbols, this gigantic sculpture, an homage to neurosurgery, is one of her most recognizable public art pieces.',\n  url: 'https://i.imgur.com/Mx7dA2Y.jpg',\n  alt: 'A bronze statue of two crossed hands delicately holding a human brain in their fingertips.'\n}, {\n  name: 'Floralis Genérica',\n  artist: 'Eduardo Catalano',\n  description: 'This enormous (75 ft. or 23m) silver flower is located in Buenos Aires. It is designed to move, closing its petals in the evening or when strong winds blow and opening them in the morning.',\n  url: 'https://i.imgur.com/ZF6s192m.jpg',\n  alt: 'A gigantic metallic flower sculpture with reflective mirror-like petals and strong stamens.'\n}, {\n  name: 'Eternal Presence',\n  artist: 'John Woodrow Wilson',\n  description: 'Wilson was known for his preoccupation with equality, social justice, as well as the essential and spiritual qualities of humankind. This massive (7ft. or 2,13m) bronze represents what he described as \"a symbolic Black presence infused with a sense of universal humanity.\"',\n  url: 'https://i.imgur.com/aTtVpES.jpg',\n  alt: 'The sculpture depicting a human head seems ever-present and solemn. It radiates calm and serenity.'\n}, {\n  name: 'Moai',\n  artist: 'Unknown Artist',\n  description: 'Located on the Easter Island, there are 1,000 moai, or extant monumental statues, created by the early Rapa Nui people, which some believe represented deified ancestors.',\n  url: 'https://i.imgur.com/RCwLEoQm.jpg',\n  alt: 'Three monumental stone busts with the heads that are disproportionately large with somber faces.'\n}, {\n  name: 'Blue Nana',\n  artist: 'Niki de Saint Phalle',\n  description: 'The Nanas are triumphant creatures, symbols of femininity and maternity. Initially, Saint Phalle used fabric and found objects for the Nanas, and later on introduced polyester to achieve a more vibrant effect.',\n  url: 'https://i.imgur.com/Sd1AgUOm.jpg',\n  alt: 'A large mosaic sculpture of a whimsical dancing female figure in a colorful costume emanating joy.'\n}, {\n  name: 'Ultimate Form',\n  artist: 'Barbara Hepworth',\n  description: 'This abstract bronze sculpture is a part of The Family of Man series located at Yorkshire Sculpture Park. Hepworth chose not to create literal representations of the world but developed abstract forms inspired by people and landscapes.',\n  url: 'https://i.imgur.com/2heNQDcm.jpg',\n  alt: 'A tall sculpture made of three elements stacked on each other reminding of a human figure.'\n}, {\n  name: 'Cavaliere',\n  artist: 'Lamidi Olonade Fakeye',\n  description: \"Descended from four generations of woodcarvers, Fakeye's work blended traditional and contemporary Yoruba themes.\",\n  url: 'https://i.imgur.com/wIdGuZwm.png',\n  alt: 'An intricate wood sculpture of a warrior with a focused face on a horse adorned with patterns.'\n}, {\n  name: 'Big Bellies',\n  artist: 'Alina Szapocznikow',\n  description: \"Szapocznikow is known for her sculptures of the fragmented body as a metaphor for the fragility and impermanence of youth and beauty. This sculpture depicts two very realistic large bellies stacked on top of each other, each around five feet (1,5m) tall.\",\n  url: 'https://i.imgur.com/AlHTAdDm.jpg',\n  alt: 'The sculpture reminds a cascade of folds, quite different from bellies in classical sculptures.'\n}, {\n  name: 'Terracotta Army',\n  artist: 'Unknown Artist',\n  description: 'The Terracotta Army is a collection of terracotta sculptures depicting the armies of Qin Shi Huang, the first Emperor of China. The army consisted of more than 8,000 soldiers, 130 chariots with 520 horses, and 150 cavalry horses.',\n  url: 'https://i.imgur.com/HMFmH6m.jpg',\n  alt: '12 terracotta sculptures of solemn warriors, each with a unique facial expression and armor.'\n}, {\n  name: 'Lunar Landscape',\n  artist: 'Louise Nevelson',\n  description: 'Nevelson was known for scavenging objects from New York City debris, which she would later assemble into monumental constructions. In this one, she used disparate parts like a bedpost, juggling pin, and seat fragment, nailing and gluing them into boxes that reflect the influence of Cubism’s geometric abstraction of space and form.',\n  url: 'https://i.imgur.com/rN7hY6om.jpg',\n  alt: 'A black matte sculpture where the individual elements are initially indistinguishable.'\n}, {\n  name: 'Aureole',\n  artist: 'Ranjani Shettar',\n  description: 'Shettar merges the traditional and the modern, the natural and the industrial. Her art focuses on the relationship between man and nature. Her work was described as compelling both abstractly and figuratively, gravity defying, and a \"fine synthesis of unlikely materials.\"',\n  url: 'https://i.imgur.com/okTpbHhm.jpg',\n  alt: 'A pale wire-like sculpture mounted on concrete wall and descending on the floor. It appears light.'\n}, {\n  name: 'Hippos',\n  artist: 'Taipei Zoo',\n  description: 'The Taipei Zoo commissioned a Hippo Square featuring submerged hippos at play.',\n  url: 'https://i.imgur.com/6o5Vuyu.jpg',\n  alt: 'A group of bronze hippo sculptures emerging from the sett sidewalk as if they were swimming.'\n}];\n```\n\n```css\nbutton { display: block; margin-bottom: 10px; }\n.Page > * {\n  float: left;\n  width: 50%;\n  padding: 10px;\n}\nh2 { margin-top: 10px; margin-bottom: 0; }\nh3 {\n  margin-top: 5px;\n  font-weight: normal;\n  font-size: 100%;\n}\nimg { width: 120px; height: 120px; }\n```\n\n</Sandpack>\n\n반환된 JSX와 이벤트 핸들러 내부에서 `hasPrev`와 `hasNext`가 어떻게 사용되는지 주목하세요! 이 편리한 패턴은 이벤트 핸들러 함수가 렌더링하는 동안 선언된 모든 변수를 [\"클로저로 참조\"](https://developer.mozilla.org/ko/docs/Web/JavaScript/Closures)하기 때문에 작동합니다.\n\n</Solution>\n\n#### 폼 입력 불가 문제 고치기 {/*fix-stuck-form-inputs*/}\n\n입력 필드에 입력하면 아무것도 나타나지 않습니다. 마치 입력값이 빈 문자열로 \"고정\"된 것처럼 보입니다. 첫 번째 `<input>`의 `value`는 항상 `firstName` 변수와 일치하도록 설정되어 있으며, 두 번째 `<input>`의 `value`는 항상 `lastName` 변수와 일치하도록 설정되어 있습니다. 이 부분은 맞습니다. 두 입력 모두 onChange 이벤트 핸들러를 가지고 있으며, 최신 사용자 입력(`e.target.value`)을 기반으로 변수를 업데이트하려고 시도합니다. 그러나 변수들은 다시 렌더링 되는 동안 값을 \"기억\"하지 않는 것처럼 보입니다. state 변수를 사용하여 이 문제를 해결하세요.\n\n<Sandpack>\n\n```js\nexport default function Form() {\n  let firstName = '';\n  let lastName = '';\n\n  function handleFirstNameChange(e) {\n    firstName = e.target.value;\n  }\n\n  function handleLastNameChange(e) {\n    lastName = e.target.value;\n  }\n\n  function handleReset() {\n    firstName = '';\n    lastName = '';\n  }\n\n  return (\n    <form onSubmit={e => e.preventDefault()}>\n      <input\n        placeholder=\"First name\"\n        value={firstName}\n        onChange={handleFirstNameChange}\n      />\n      <input\n        placeholder=\"Last name\"\n        value={lastName}\n        onChange={handleLastNameChange}\n      />\n      <h1>Hi, {firstName} {lastName}</h1>\n      <button onClick={handleReset}>Reset</button>\n    </form>\n  );\n}\n```\n\n```css\nh1 { margin-top: 10px; }\n```\n\n</Sandpack>\n\n<Solution>\n\n먼저, React에서 `useState`를 가져와야 합니다. 그런 다음 `useState`를 호출하여 선언된 state 변수로 `firstName`과 `lastName`을 대체합니다. 마지막으로 `firstName = ...` 할당을 `setFirstName(...)`로 대체하고, `lastName`에 대해서도 동일하게 수행합니다. 재설정 버튼이 작동하도록 `handleReset`을 업데이트하는 것을 잊지 마세요.\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function Form() {\n  const [firstName, setFirstName] = useState('');\n  const [lastName, setLastName] = useState('');\n\n  function handleFirstNameChange(e) {\n    setFirstName(e.target.value);\n  }\n\n  function handleLastNameChange(e) {\n    setLastName(e.target.value);\n  }\n\n  function handleReset() {\n    setFirstName('');\n    setLastName('');\n  }\n\n  return (\n    <form onSubmit={e => e.preventDefault()}>\n      <input\n        placeholder=\"First name\"\n        value={firstName}\n        onChange={handleFirstNameChange}\n      />\n      <input\n        placeholder=\"Last name\"\n        value={lastName}\n        onChange={handleLastNameChange}\n      />\n      <h1>Hi, {firstName} {lastName}</h1>\n      <button onClick={handleReset}>Reset</button>\n    </form>\n  );\n}\n```\n\n```css\nh1 { margin-top: 10px; }\n```\n\n</Sandpack>\n\n</Solution>\n\n#### 충돌 고치기 {/*fix-a-crash*/}\n\n사용자가 피드백을 남길 수 있는 간단한 폼이 있는데, 피드백을 제출하면 감사 메시지가 표시되어야 합니다. 그러나 \"예상보다 적은 훅을 렌더링했습니다\"라는 오류 메시지와 함께 충돌이 발생합니다. 실수를 발견하고 고칠 수 있나요?\n\n<Hint>\n\n훅을 호출할 수 있는 *위치*에 제한이 있나요? 이 컴포넌트가 규칙을 어겼나요? 린터 검사를 비활성화하는 주석이 있는지 확인해 보세요. 버그가 자주 숨어 있는 곳입니다!\n\n</Hint>\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function FeedbackForm() {\n  const [isSent, setIsSent] = useState(false);\n  if (isSent) {\n    return <h1>Thank you!</h1>;\n  } else {\n    // eslint-disable-next-line\n    const [message, setMessage] = useState('');\n    return (\n      <form onSubmit={e => {\n        e.preventDefault();\n        alert(`Sending: \"${message}\"`);\n        setIsSent(true);\n      }}>\n        <textarea\n          placeholder=\"Message\"\n          value={message}\n          onChange={e => setMessage(e.target.value)}\n        />\n        <br />\n        <button type=\"submit\">Send</button>\n      </form>\n    );\n  }\n}\n```\n\n</Sandpack>\n\n<Solution>\n\n훅은 컴포넌트 함수의 최상위 수준에서만 호출할 수 있습니다. 여기서 첫 번째 `isSent` 정의는 이 규칙을 따르지만 `message` 정의는 조건문 내에 중첩되어 있습니다.\n\n문제를 해결하려면 조건문 밖으로 옮기세요.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function FeedbackForm() {\n  const [isSent, setIsSent] = useState(false);\n  const [message, setMessage] = useState('');\n\n  if (isSent) {\n    return <h1>Thank you!</h1>;\n  } else {\n    return (\n      <form onSubmit={e => {\n        e.preventDefault();\n        alert(`Sending: \"${message}\"`);\n        setIsSent(true);\n      }}>\n        <textarea\n          placeholder=\"Message\"\n          value={message}\n          onChange={e => setMessage(e.target.value)}\n        />\n        <br />\n        <button type=\"submit\">Send</button>\n      </form>\n    );\n  }\n}\n```\n\n</Sandpack>\n\n훅은 무조건 항상 동일한 순서로 호출되어야 한다는 것을 기억하세요!\n\n또한 중첩을 줄이기 위해 불필요한 `else` 분기를 제거할 수도 있습니다. 그러나 여전히 모든 훅 호출이 첫 번째 `return` *이전*에 발생하는 것이 중요합니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function FeedbackForm() {\n  const [isSent, setIsSent] = useState(false);\n  const [message, setMessage] = useState('');\n\n  if (isSent) {\n    return <h1>Thank you!</h1>;\n  }\n\n  return (\n    <form onSubmit={e => {\n      e.preventDefault();\n      alert(`Sending: \"${message}\"`);\n      setIsSent(true);\n    }}>\n      <textarea\n        placeholder=\"Message\"\n        value={message}\n        onChange={e => setMessage(e.target.value)}\n      />\n      <br />\n      <button type=\"submit\">Send</button>\n    </form>\n  );\n}\n```\n\n</Sandpack>\n\n두 번째 `useState` 호출을 `if` 조건문 뒤로 이동해 보세요. 그러면 어떻게 오류가 발생하는지 확인할 수 있을 것입니다.\n\n만약 [React 용으로 설정된](/learn/editor-setup#linting) 린터를 사용 중이라면 이와 같은 실수를 할 때 린트 오류가 표시될 것입니다. 로컬에서 잘못된 코드를 시도할 때 오류가 보이지 않는다면 프로젝트에 린터를 설정해야 합니다.\n\n</Solution>\n\n#### 불필요한 state 제거하기 {/*remove-unnecessary-state*/}\n\n이 예시에서 버튼을 클릭하면 사용자의 이름을 요청한 후 그 이름으로 환영 메시지를 표시해야 합니다. 이름을 유지하기 위해 state를 사용하려고 했지만, 첫 번째에는 항상 \"Hello, !\"라고 표시되고 그다음부터는 항상 \"Hello, [이전 입력값]!\"이 나옵니다.\n\n이 코드를 수정하려면 불필요한 state 변수를 제거하세요. ([왜 이것이 작동하지 않는지](/learn/state-as-a-snapshot)에 대해서는 나중에 설명하겠습니다.)\n\n이 state 변수가 불필요한 이유를 설명할 수 있을까요?\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function FeedbackForm() {\n  const [name, setName] = useState('');\n\n  function handleClick() {\n    setName(prompt('What is your name?'));\n    alert(`Hello, ${name}!`);\n  }\n\n  return (\n    <button onClick={handleClick}>\n      Greet\n    </button>\n  );\n}\n```\n\n</Sandpack>\n\n<Solution>\n\n다음은 필요한 곳에서 선언된 일반적인 `name` 변수를 사용하는 수정된 버전입니다.\n\n<Sandpack>\n\n```js\nexport default function FeedbackForm() {\n  function handleClick() {\n    const name = prompt('What is your name?');\n    alert(`Hello, ${name}!`);\n  }\n\n  return (\n    <button onClick={handleClick}>\n      Greet\n    </button>\n  );\n}\n```\n\n</Sandpack>\n\n컴포넌트가 다시 렌더링 될 때만 정보를 유지하기 위해 state 변수가 필요합니다. 단일 이벤트 핸들러 내에서는 일반 변수가 잘 작동합니다. 일반 변수가 잘 동작할 때 state 변수를 도입하지 마세요.\n\n</Solution>\n\n</Challenges>\n"
  },
  {
    "path": "src/content/learn/state-as-a-snapshot.md",
    "content": "---\ntitle: 스냅샷으로서의 State\n---\n\n<Intro>\n\nState 변수는 읽고 쓸 수 있는 일반 자바스크립트 변수처럼 보일 수 있습니다. 하지만 state는 스냅샷처럼 동작합니다. state 변수를 설정하여도 이미 가지고 있는 state 변수는 변경되지 않고, 대신 리렌더링이 발동됩니다.\n\n</Intro>\n\n<YouWillLearn>\n\n* state 설정으로 리렌더링이 동작하는 방식\n* state 업데이트 시기 및 방법\n* state를 설정한 직후에 state가 업데이트되지 않는 이유\n* 이벤트 핸들러가 state의 \"스냅샷\"에 접근하는 방법\n\n</YouWillLearn>\n\n## state를 설정하면 렌더링이 동작합니다 {/*setting-state-triggers-renders*/}\n\n클릭과 같은 사용자 이벤트에 반응하여 사용자 인터페이스가 직접 변경된다고 생각할 수 있습니다. React에서는 이 멘탈 모델과는 조금 다르게 작동합니다. 이전 페이지에서 [state를 설정하면 React에 리렌더링을 요청](/learn/render-and-commit#step-1-trigger-a-render)하는 것을 보았습니다. 즉, 인터페이스가 이벤트에 반응하려면 state를 업데이트해야 합니다.\n\n\n이 예시에서는 \"send\"를 누르면 `setIsSent(true)`는 React에 UI를 다시 렌더링하도록 지시합니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function Form() {\n  const [isSent, setIsSent] = useState(false);\n  const [message, setMessage] = useState('Hi!');\n  if (isSent) {\n    return <h1>Your message is on its way!</h1>\n  }\n  return (\n    <form onSubmit={(e) => {\n      e.preventDefault();\n      setIsSent(true);\n      sendMessage(message);\n    }}>\n      <textarea\n        placeholder=\"Message\"\n        value={message}\n        onChange={e => setMessage(e.target.value)}\n      />\n      <button type=\"submit\">Send</button>\n    </form>\n  );\n}\n\nfunction sendMessage(message) {\n  // ...\n}\n```\n\n```css\nlabel, textarea { margin-bottom: 10px; display: block; }\n```\n\n</Sandpack>\n\n버튼을 클릭하면 다음과 같은 일이 발생합니다.\n\n1. `onSubmit` 이벤트 핸들러가 실행됩니다.\n2. `setIsSent(true)`가 `isSent`를 `true`로 설정하고 새로운 렌더링을 큐에 넣습니다.\n3. React는 새로운 `isSent`값에 따라 컴포넌트를 다시 렌더링합니다.\n\nstate와 렌더링의 관계를 자세히 살펴보겠습니다.\n\n## 렌더링은 그 시점의 스냅샷을 찍습니다. {/*rendering-takes-a-snapshot-in-time*/}\n\n[\"렌더링\"](/learn/render-and-commit#step-2-react-renders-your-components)이란 React가 컴포넌트, 즉 함수를 호출한다는 뜻입니다. 해당 함수에서 반환하는 JSX는 시간상 UI의 스냅샷과 같습니다. prop, 이벤트 핸들러, 로컬 변수는 모두 **렌더링 당시의 state를 사용해** 계산됩니다.\n\n사진이나 동영상 프레임과 달리 반환하는 UI \"스냅샷\"은 대화형입니다. 여기에는 입력에 대한 응답으로 어떤 일이 일어날지 지정하는 이벤트 핸들러와 같은 로직이 포함됩니다. 그러면 React는 이 스냅샷과 일치하도록 화면을 업데이트하고 이벤트 핸들러를 연결합니다. 결과적으로 버튼을 누르면 JSX의 클릭 핸들러가 발동됩니다.\n\nReact가 컴포넌트를 다시 렌더링할 때.\n\n1. React가 함수를 다시 호출합니다.\n2. 함수가 새로운 JSX 스냅샷을 반환합니다.\n3. 그러면 React가 함수가 반환한 스냅샷과 일치하도록 화면을 업데이트합니다.\n\n<IllustrationBlock title=\"다시 렌더링\" sequential>\n    <Illustration caption=\"React가 함수를 호출합니다\" src=\"/images/docs/illustrations/i_render1.png\" />\n    <Illustration caption=\"스냅샷을 계산합니다\" src=\"/images/docs/illustrations/i_render2.png\" />\n    <Illustration caption=\"DOM tree를 업데이트 합니다\" src=\"/images/docs/illustrations/i_render3.png\" />\n</IllustrationBlock>\n\n컴포넌트의 메모리로써 state는 함수가 반환된 후 사라지는 일반 변수와 다릅니다. state는 실제로 함수 외부에 마치 선반에 있는 것처럼 React 자체에 \"존재\"합니다. React가 컴포넌트를 호출하면 특정 렌더링에 대한 state의 스냅샷을 제공합니다. 컴포넌트는 **해당 렌더링의 state 값을 사용해** 계산된 새로운 props 세트와 이벤트 핸들러가 포함된 UI의 스냅샷을 JSX에 반환합니다!\n\n<IllustrationBlock sequential>\n  <Illustration caption=\"React에 state를 업데이트하라고 명령합니다\" src=\"/images/docs/illustrations/i_state-snapshot1.png\" />\n  <Illustration caption=\"React가 state 값을 업데이트 합니다\" src=\"/images/docs/illustrations/i_state-snapshot2.png\" />\n  <Illustration caption=\"React는 상태 값의 스냅샷을 컴포넌트에 전달합니다\" src=\"/images/docs/illustrations/i_state-snapshot3.png\" />\n</IllustrationBlock>\n\n다음은 이것이 어떻게 작동하는지 보여주는 간단한 실험입니다. 이 예시에서는 '+3' 버튼을 클릭하면 `setNumber(number + 1)`를 세 번 호출하므로 카운터가 세 번 증가할 것으로 예상할 수 있습니다.\n\n\"+3\" 버튼을 클릭하면 어떻게 되는지 확인해 봅시다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function Counter() {\n  const [number, setNumber] = useState(0);\n\n  return (\n    <>\n      <h1>{number}</h1>\n      <button onClick={() => {\n        setNumber(number + 1);\n        setNumber(number + 1);\n        setNumber(number + 1);\n      }}>+3</button>\n    </>\n  )\n}\n```\n\n```css\nbutton { display: inline-block; margin: 10px; font-size: 20px; }\nh1 { display: inline-block; margin: 10px; width: 30px; text-align: center; }\n```\n\n</Sandpack>\n\n이 `number`는 클릭당 한 번만 증가한다는 점에 유의하세요!\n\n**state를 설정하면 다음 렌더링에 대해서만 변경됩니다.** 첫 번째 렌더링에서 `number`는 `0`이었습니다. 따라서 해당 렌더링의 `onClick` 핸들러에서 `setNumber(number + 1)`가 호출된 후에도 `number`의 값은 여전히 `0`입니다.\n\n```js\n<button onClick={() => {\n  setNumber(number + 1);\n  setNumber(number + 1);\n  setNumber(number + 1);\n}}>+3</button>\n```\n\n이 버튼의 클릭 핸들러가 React에 지시하는 작업은 다음과 같습니다.\n\n1. `setNumber(number + 1)`: `number`는 `0`이므로 `setNumber(0 + 1)`입니다.\n    - React는 다음 렌더링에서 `number`를 `1`로 변경할 준비를 합니다.\n2. `setNumber(number + 1)`: `number`는 `0`이므로 `setNumber(0 + 1)`입니다.\n    - React는 다음 렌더링에서 `number`를 `1`로 변경할 준비를 합니다.\n3. `setNumber(number + 1)`: `number`는 `0`이므로 `setNumber(0 + 1)`입니다.\n    - React는 다음 렌더링에서 `number`를 `1`로 변경할 준비를 합니다.\n\n`setNumber(number + 1)`를 세 번 호출했지만, 이 렌더링에서 이벤트 핸들러에서 `number`는 항상 `0`이므로 state를 `1`로 세 번 설정합니다. 이것이 이벤트 핸들러가 완료된 후 React가 컴포넌트 안의 `number` 를 `3`이 아닌 `1`로 다시 렌더링하는 이유입니다.\n\n코드에서 state 변수를 해당 값으로 대입하여 이를 시각화할 수도 있습니다. 이 렌더링에서 `number` state 변수는 `0`이므로 이벤트 핸들러는 다음과 같습니다.\n\n```js\n<button onClick={() => {\n  setNumber(0 + 1);\n  setNumber(0 + 1);\n  setNumber(0 + 1);\n}}>+3</button>\n```\n\n다음 렌더링에서는 `number`가 `1`이므로 렌더링의 클릭 핸들러는 다음과 같이 표시됩니다.\n\n```js\n<button onClick={() => {\n  setNumber(1 + 1);\n  setNumber(1 + 1);\n  setNumber(1 + 1);\n}}>+3</button>\n```\n\n그렇기 때문에 버튼을 다시 클릭하면 카운터가 `2`로 설정되고, 다음 클릭 시에는 `3`으로 설정되는 방식입니다.\n\n## 시간 경과에 따른 State {/*state-over-time*/}\n\n재미있네요. 이 버튼을 클릭하면 어떤 경고창이 표시되는지 맞혀보세요.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function Counter() {\n  const [number, setNumber] = useState(0);\n\n  return (\n    <>\n      <h1>{number}</h1>\n      <button onClick={() => {\n        setNumber(number + 5);\n        alert(number);\n      }}>+5</button>\n    </>\n  )\n}\n```\n\n```css\nbutton { display: inline-block; margin: 10px; font-size: 20px; }\nh1 { display: inline-block; margin: 10px; width: 30px; text-align: center; }\n```\n\n</Sandpack>\n\n이전의 대체 메서드를 사용하면 경고창이 \"0\"을 표시한다고 추측할 수 있습니다.\n\n```js\nsetNumber(0 + 5);\nalert(0);\n```\n\n하지만 경고창에 타이머를 설정하여 컴포넌트가 다시 렌더링 된 후에만 발동하도록 하면 어떨까요? \"0\" 또는 \"5\"라고 표시될까요? 맞춰보세요!\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function Counter() {\n  const [number, setNumber] = useState(0);\n\n  return (\n    <>\n      <h1>{number}</h1>\n      <button onClick={() => {\n        setNumber(number + 5);\n        setTimeout(() => {\n          alert(number);\n        }, 3000);\n      }}>+5</button>\n    </>\n  )\n}\n```\n\n```css\nbutton { display: inline-block; margin: 10px; font-size: 20px; }\nh1 { display: inline-block; margin: 10px; width: 30px; text-align: center; }\n```\n\n</Sandpack>\n\n놀라셨나요? 대체 메서드를 사용하면 경고에 전달된 state의 \"스냅샷\"을 볼 수 있습니다.\n\n```js\nsetNumber(0 + 5);\nsetTimeout(() => {\n  alert(0);\n}, 3000);\n```\n\nReact에 저장된 state는 경고창이 실행될 때 변경되었을 수 있지만 사용자가 상호작용한 시점에 state 스냅샷을 사용하는 건 이미 예약되어 있던 것입니다!\n\n**state 변수의 값은** 이벤트 핸들러의 코드가 비동기적이더라도 **렌더링 내에서 절대 변경되지 않습니다.** 해당 렌더링의 `onClick` 내에서, `setNumber(number + 5)`가 호출된 후에도 `number`의 값은 계속 `0`입니다. 이 값은 컴포넌트를 호출해 React가 UI의 \"스냅샷을 찍을\" 때 \"고정\"된 값입니다.\n\n다음은 이벤트 핸들러가 타이밍 실수를 줄이는 방법을 보여주는 예입니다. 아래는 5초 지연된 메시지를 보내는 양식입니다. 이 시나리오를 상상해 보세요.\n\n1. \"Send\" 버튼을 누르면 Alice에게 \"Hello\"가 전송됩니다.\n2. 5초 지연이 끝나기 전에 \"To\" 필드의 값을 \"Bob\"으로 변경합니다.\n\n`alert`에 어떤 내용이 표시되기를 기대하나요? \"앨리스에게 인사했습니다\"라고 표시될까요, 아니면 \"당신은 밥에게 인사했습니다\"라고 표시될까요? 알고 있는 내용을 바탕으로 추측해보고, 다음의 코드를 실행해 보세요.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function Form() {\n  const [to, setTo] = useState('Alice');\n  const [message, setMessage] = useState('Hello');\n\n  function handleSubmit(e) {\n    e.preventDefault();\n    setTimeout(() => {\n      alert(`You said ${message} to ${to}`);\n    }, 5000);\n  }\n\n  return (\n    <form onSubmit={handleSubmit}>\n      <label>\n        To:{' '}\n        <select\n          value={to}\n          onChange={e => setTo(e.target.value)}>\n          <option value=\"Alice\">Alice</option>\n          <option value=\"Bob\">Bob</option>\n        </select>\n      </label>\n      <textarea\n        placeholder=\"Message\"\n        value={message}\n        onChange={e => setMessage(e.target.value)}\n      />\n      <button type=\"submit\">Send</button>\n    </form>\n  );\n}\n```\n\n```css\nlabel, textarea { margin-bottom: 10px; display: block; }\n```\n\n</Sandpack>\n\n**React는 렌더링의 이벤트 핸들러 내에서 state 값을 \"고정\"으로 유지합니다.** 코드가 실행되는 동안 state가 변경되었는지를 걱정할 필요가 없습니다.\n\n하지만 다시 렌더링하기 전에 최신 state를 읽고 싶다면 어떻게 해야 할까요? 다음 페이지에서 설명하는 [state 갱신 함수](/learn/queueing-a-series-of-state-updates)를 사용하면 됩니다!\n\n<Recap>\n\n* state를 설정하면 새 렌더링을 요청합니다.\n* React는 컴포넌트 외부에 마치 선반에 보관하듯 state를 저장합니다.\n* `useState`를 호출하면 React는 해당 렌더링에 대한 state의 스냅샷을 제공합니다.\n* 변수와 이벤트 핸들러는 다시 렌더링해도 \"살아남지\" 않습니다. 모든 렌더링에는 고유한 이벤트 핸들러가 있습니다.\n* 모든 렌더링(및 그 안의 함수)은 항상 React가 그 렌더링에 제공한 state의 스냅샷을 \"보게\" 됩니다.\n* 렌더링 된 JSX에 대해 생각하는 방식과 유사하게 이벤트 핸들러에서 state를 대체할 수 있습니다.\n* 과거에 생성된 이벤트 핸들러는 그것이 생성된 렌더링 시점의 state 값을 갖습니다.\n\n</Recap>\n\n\n\n<Challenges>\n\n#### 신호등 구현하기 {/*implement-a-traffic-light*/}\n\n다음은 버튼을 누르면 토글되는 신호등 컴포넌트입니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function TrafficLight() {\n  const [walk, setWalk] = useState(true);\n\n  function handleClick() {\n    setWalk(!walk);\n  }\n\n  return (\n    <>\n      <button onClick={handleClick}>\n        Change to {walk ? 'Stop' : 'Walk'}\n      </button>\n      <h1 style={{\n        color: walk ? 'darkgreen' : 'darkred'\n      }}>\n        {walk ? 'Walk' : 'Stop'}\n      </h1>\n    </>\n  );\n}\n```\n\n```css\nh1 { margin-top: 20px; }\n```\n\n</Sandpack>\n\n클릭 핸들러에 `alert`를 추가하세요. 신호등이 녹색이고 \"걷기\"라고 표시되면 버튼을 클릭하면 \"다음은 정지입니다\"라고 표시되어야 합니다. 신호등이 빨간색이고 \"중지\"라고 표시되면 버튼을 클릭하면 \"다음은 걷기입니다\"라고 표시되어야 합니다.\n\n`alert`를 `setWalk` 호출 전이나 후에 넣는 것이 차이가 있을까요?\n\n<Solution>\n\n`alert`는 다음과 같이 작성해야 합니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function TrafficLight() {\n  const [walk, setWalk] = useState(true);\n\n  function handleClick() {\n    setWalk(!walk);\n    alert(walk ? 'Stop is next' : 'Walk is next');\n  }\n\n  return (\n    <>\n      <button onClick={handleClick}>\n        Change to {walk ? 'Stop' : 'Walk'}\n      </button>\n      <h1 style={{\n        color: walk ? 'darkgreen' : 'darkred'\n      }}>\n        {walk ? 'Walk' : 'Stop'}\n      </h1>\n    </>\n  );\n}\n```\n\n```css\nh1 { margin-top: 20px; }\n```\n\n</Sandpack>\n\n`setWalk` 호출 앞에 넣든, 뒤에 넣든 아무런 차이가 없습니다. 해당 렌더링의 `walk` 값은 고정되어 있습니다. `setWalk`를 호출하면 다음 렌더링에 대해서만 변경되고, 이전 렌더링의 이벤트 핸들러에는 영향을 미치지 않습니다.\n\n이 라인은 처음에는 직관적이지 않게 보일 수 있습니다.\n\n```js\nalert(walk ? 'Stop is next' : 'Walk is next');\n```\n\n하지만 이렇게 읽으면 이해가 될 것입니다. \"신호등에 'Walk now'가 표시되면, 메시지에 'Stop is next.'라고, 표시되어야 합니다.\" 이벤트 핸들러 내부의 `walk` 변수는 해당 렌더링의 값인 `walk`와 일치하며 변경되지 않습니다.\n\n대체 메서드를 적용하여 이것이 올바른지 확인할 수 있습니다. `walk`가 `true`이면 다음과 같은 결과를 얻습니다.\n\n```js\n<button onClick={() => {\n  setWalk(false);\n  alert('Stop is next');\n}}>\n  Change to Stop\n</button>\n<h1 style={{color: 'darkgreen'}}>\n  Walk\n</h1>\n```\n\n따라서 \"Change to Stop\"을 클릭하면 `walk`가 `false`로 설정된 렌더링이 대기열에 추가되고 \"Stop is next\"라는 경고가 표시됩니다.\n\n</Solution>\n\n</Challenges>\n"
  },
  {
    "path": "src/content/learn/synchronizing-with-effects.md",
    "content": "---\ntitle: 'Effect로 동기화하기'\n---\n\n<Intro>\n\n일부 컴포넌트에서는 외부 시스템과 동기화해야 할 수 있습니다. 예를 들어 React의 state을 기준으로 React와 상관없는 구성 요소를 제어하거나, 서버 연결을 설정하거나, 구성 요소가 화면에 나타날 때 분석 목적의 로그를 전송할 수도 있습니다. *Effect*를 사용하면 렌더링 후 특정 코드를 실행하여 React 외부의 시스템과 컴포넌트를 동기화할 수 있습니다.\n\n</Intro>\n\n<YouWillLearn>\n\n- Effect가 무엇인지\n- Effect가 이벤트와 다른 점\n- 컴포넌트에서 Effect를 선언하는 방법\n- 불필요한 Effect 재실행을 건너뛰는 방법\n- 개발 중에 Effect가 두 번 실행되는 이유와 해결 방법\n\n</YouWillLearn>\n\n## Effect란 무엇이고 이벤트와는 어떻게 다른가요? {/*what-are-effects-and-how-are-they-different-from-events*/}\n\nEffect에 대해 자세히 알아보기 전에, 컴포넌트 내부의 2가지 로직 유형에 대해 알아야 합니다.\n\n- **렌더링 코드**([UI 표현하기](/learn/describing-the-ui)에 소개됨)를 주관하는 로직은 컴포넌트의 최상단에 위치하며, props와 state를 적절히 변형해 결과적으로 JSX를 반환합니다. [렌더링 코드 로직은 순수해야 합니다.](/learn/keeping-components-pure) 수학 공식처럼 결과만 계산해야 하고, 그 외에는 아무것도 하지 말아야 합니다.\n\n- **이벤트 핸들러**([상호작용 더하기](/learn/adding-interactivity)에 소개됨)는 단순한 계산 용도가 아닌 무언가를 *하는* 컴포넌트 내부의 중첩 함수입니다. 이벤트 핸들러는 입력 필드를 업데이트하거나, 제품을 구입하기 위해 HTTP POST 요청을 보내거나, 사용자를 다른 화면으로 이동시킬 수 있습니다. 이벤트 핸들러에는 특정 사용자 작업(예: 버튼 클릭 또는 입력)으로 인해 발생하는 [\"부수 효과\"](https://en.wikipedia.org/wiki/Side_effect_(computer_science))(이러한 부수 효과가 프로그램 상태를 변경합니다.)를 포함합니다.\n\n가끔은 이것으로 충분하지 않습니다. 화면에 보일 때마다 채팅 서버에 접속해야 하는 `ChatRoom` 컴포넌트를 생각해 보세요. 서버에 접속하는 것은 순수한 계산이 아니고 부수 효과를 발생시키기 때문에 렌더링 중에는 할 수 없습니다. 하지만 클릭 한 번으로 `ChatRoom`이 표시되는 특정 이벤트는 하나도 없습니다.\n\n**Effect**는 렌더링 자체에 의해 발생하는 부수 효과를 특정하는 것으로, 특정 이벤트가 아닌 렌더링에 의해 직접 발생합니다. 채팅에서 메시지를 보내는 것은 *이벤트*입니다. 왜냐하면 이것은 사용자가 특정 버튼을 클릭함에 따라 직접적으로 발생합니다. 그러나 서버 연결 설정은 *Effect*입니다. 왜냐하면 이것은 컴포넌트의 표시를 주관하는 어떤 상호 작용과도 상관없이 발생해야 합니다. Effect는 [커밋](/learn/render-and-commit)이 끝난 후에 화면 업데이트가 이루어지고 나서 실행됩니다. 이 시점이 React 컴포넌트를 외부 시스템(네트워크 또는 써드파티 라이브러리와 같은)과 동기화하기 좋은 타이밍입니다.\n\n<Note>\n\n이 텍스트에서의 대문자 \"Effect\"는 위에서 언급한 React에 특화된 정의를 나타내며, 곧 렌더링에 의한 부수 효과를 의미합니다. 보다 일반적인 프로그래밍 개념을 언급할 때에는 \"부수 효과\"라고 말하겠습니다.\n\n</Note>\n\n\n## Effect가 필요 없을지도 모릅니다 {/*you-might-not-need-an-effect*/}\n\n**컴포넌트에 Effect를 무작정 추가하지 마세요.** Effect는 주로 React 코드를 벗어난 특정 *외부* 시스템과 동기화하기 위해 사용됩니다. 이는 브라우저 API, 서드 파티 위젯, 네트워크 등을 포함합니다. 만약 당신의 Effect가 단순히 다른 상태에 기반하여 일부 상태를 조정하는 경우에는 [Effect가 필요하지 않을 수 있습니다.](/learn/you-might-not-need-an-effect)\n\n## Effect를 작성하는 법 {/*how-to-write-an-effect*/}\n\nEffect를 작성하기 위해서는 다음 세 단계를 따릅니다.\n\n1. **Effect 선언.** 기본적으로 Effect는 모든 [commit](/learn/render-and-commit) 이후에 실행됩니다.\n2. **Effect 의존성 지정.** 대부분의 Effect는 모든 렌더링 후가 아닌 *필요할 때*만 다시 실행되어야 합니다. 예를 들어, 페이드 인 애니메이션은 컴포넌트가 나타날 때에만 트리거 되어야 합니다. 채팅 방에 연결, 연결 해제하는 것은 컴포넌트가 나타나거나 사라질 때 또는 채팅 방이 변경될 때만 발생해야 합니다. *의존성*을 지정하여 이를 제어하는 방법을 배우게 될 것입니다.\n3. **필요한 경우 클린업 함수 추가.** 일부 Effect는 수행 중이던 작업을 중지, 취소 또는 정리하는 방법을 지정해야 할 수 있습니다. 예를 들어, \"연결\"은 \"연결 해제\"가 필요하며, \"구독\"은 \"구독 취소\"가 필요하고, \"불러오기(fetch)\"는 \"취소\" 또는 \"무시\"가 필요합니다. 이런 경우에 Effect에서 *클린업 함수(cleanup function)*를 반환하여 어떻게 수행하는지 배우게 될 것입니다.\n\n각 단계를 자세히 살펴보겠습니다.\n\n### 1단계: Effect 선언하기 {/*step-1-declare-an-effect*/}\n\n컴포넌트 내에서 Effect를 선언하려면, React에서 [`useEffect` 훅](/reference/react/useEffect)을 import 하세요.\n\n```js\nimport { useEffect } from 'react';\n```\n\n그런 다음, 컴포넌트의 최상위 레벨에서 호출하고 Effect 내부에 코드를 넣으세요.\n\n```js {2-4}\nfunction MyComponent() {\n  useEffect(() => {\n    // 이곳의 코드는 *모든* 렌더링 후에 실행됩니다\n  });\n  return <div />;\n}\n```\n\n컴포넌트가 렌더링 될 때마다 React는 화면을 업데이트한 다음 `useEffect` 내부의 코드를 실행합니다. 다시 말해, **`useEffect`는 화면에 렌더링이 반영될 때까지 코드 실행을 \"지연\"시킵니다.**\n\n이제 외부 시스템과 동기화하기 위해 어떻게 Effect를 사용할 수 있는지 알아보겠습니다. `<VideoPlayer>`라는 React 컴포넌트를 살펴보겠습니다. 이 컴포넌트를 `isPlaying`이라는 props를 통해 재생 중인지 일시 정지 상태인지 제어하는 것이 좋아 보이네요.\n\n```js\n<VideoPlayer isPlaying={isPlaying} />;\n```\n\n커스텀 `VideoPlayer` 컴포넌트는 내장 브라우저 [`<video>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/video) 태그를 렌더링 합니다.\n\n```js\nfunction VideoPlayer({ src, isPlaying }) {\n  // TODO: isPlaying을 활용하여 무언가 수행하기\n  return <video src={src} />;\n}\n```\n\n그러나 `<video>` 태그에는 `isPlaying` prop이 없습니다. 이를 제어하는 유일한 방법은 DOM 요소에서 수동으로 [`play()`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/play) 및 [`pause()`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/pause) 메서드를 호출하는 것입니다. **`isPlaying` prop의 값(현재 비디오가 재생 중인지 여부)을 `play()` 및 `pause()`와 같은 호출과 동기화해야 합니다.**\n\n먼저 `<video>` DOM 노드의 [ref를 가져와야](/learn/manipulating-the-dom-with-refs) 합니다.\n\n`play()` 또는 `pause()`를 렌더링 중에 호출하려고 시도할 수 있겠지만, 이는 올바른 접근이 아닙니다.\n\n<Sandpack>\n\n```js\nimport { useState, useRef, useEffect } from 'react';\n\nfunction VideoPlayer({ src, isPlaying }) {\n  const ref = useRef(null);\n\n  if (isPlaying) {\n    ref.current.play();  // 렌더링 중에 이를 호출하는 것이 허용되지 않습니다.\n  } else {\n    ref.current.pause(); // 역시 이렇게 호출하면 바로 위의 호출과 충돌이 발생합니다.\n  }\n\n  return <video ref={ref} src={src} loop playsInline />;\n}\n\nexport default function App() {\n  const [isPlaying, setIsPlaying] = useState(false);\n  return (\n    <>\n      <button onClick={() => setIsPlaying(!isPlaying)}>\n        {isPlaying ? '일시정지' : '재생'}\n      </button>\n      <VideoPlayer\n        isPlaying={isPlaying}\n        src=\"https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4\"\n      />\n    </>\n  );\n}\n```\n\n```css\nbutton { display: block; margin-bottom: 20px; }\nvideo { width: 250px; }\n```\n\n</Sandpack>\n\n이 코드가 올바르지 않은 이유는 렌더링 중에 DOM 노드를 조작하려고 시도하기 때문입니다. React에서는 [렌더링이 JSX의 순수한 계산](/learn/keeping-components-pure)이어야 하며, DOM 수정과 같은 부수 효과를 포함해서는 안됩니다.\n\n게다가, 처음으로 `VideoPlayer`가 호출될 때 해당 DOM이 아직 존재하지 않습니다! React는 컴포넌트가 JSX를 반환할 때까지 어떤 DOM을 생성할지 모르기 때문에 `play()` 또는 `pause()`를 호출할 DOM 노드가 아직 없습니다.\n\n해결책은 **부수 효과를 렌더링 연산에서 분리하기 위해 `useEffect`로 감싸는 것입니다.**\n\n```js {6,12}\nimport { useEffect, useRef } from 'react';\n\nfunction VideoPlayer({ src, isPlaying }) {\n  const ref = useRef(null);\n\n  useEffect(() => {\n    if (isPlaying) {\n      ref.current.play();\n    } else {\n      ref.current.pause();\n    }\n  });\n\n  return <video ref={ref} src={src} loop playsInline />;\n}\n```\n\nDOM 업데이트를 Effect로 감싸면 React가 화면을 업데이트한 다음에 Effect가 실행됩니다.\n\n`VideoPlayer` 컴포넌트가 렌더링 될 때(처음 호출하거나 다시 렌더링 할 때) 몇 가지 일이 발생합니다. 먼저 React는 화면을 업데이트하여 `<video>` 태그가 올바른 속성과 함께 DOM에 있는지 확인합니다. 그런 다음 React는 Effect를 실행합니다. 마지막으로 Effect에서는 `isPlaying` 값에 따라 `play()` 또는 `pause()`를 호출합니다.\n\n\"재생\" 또는 \"일시 정지\"를 여러 번 눌러보고 비디오 플레이어가 `isPlaying` 값과 동기화되는지 확인해 보세요.\n\n<Sandpack>\n\n```js\nimport { useState, useRef, useEffect } from 'react';\n\nfunction VideoPlayer({ src, isPlaying }) {\n  const ref = useRef(null);\n\n  useEffect(() => {\n    if (isPlaying) {\n      ref.current.play();\n    } else {\n      ref.current.pause();\n    }\n  });\n\n  return <video ref={ref} src={src} loop playsInline />;\n}\n\nexport default function App() {\n  const [isPlaying, setIsPlaying] = useState(false);\n  return (\n    <>\n      <button onClick={() => setIsPlaying(!isPlaying)}>\n        {isPlaying ? '일시 정지' : '재생'}\n      </button>\n      <VideoPlayer\n        isPlaying={isPlaying}\n        src=\"https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4\"\n      />\n    </>\n  );\n}\n```\n\n```css\nbutton { display: block; margin-bottom: 20px; }\nvideo { width: 250px; }\n```\n\n</Sandpack>\n\n이 예시에서 React 상태와 동기화된 \"외부 시스템\"은 브라우저 미디어 API였습니다. 이와 비슷한 접근 방식으로 React가 아닌 레거시 코드(예: jQuery 플러그인)를 선언적인 React 컴포넌트로 감싸는 데에도 사용할 수 있습니다.\n\n실제로 비디오 플레이어를 제어하는 것은 훨씬 복잡합니다. `play()`를 호출하는 것이 실패할 수 있으며, 사용자는 컴포넌트의 UI가 아닌 브라우저 내장 컨트롤을 사용하여 동영상을 재생 또는 일시 정지할 수 있습니다. 이 예시는 매우 단순화되었고 불완전한 것임을 유의해주세요.\n\n<Pitfall>\n\n기본적으로, Effect는 *모든* 렌더링 후에 실행됩니다. 이러한 이유로 다음과 같은 코드는 **무한 루프를 만들어낼** 것입니다.\n\n```js\nconst [count, setCount] = useState(0);\nuseEffect(() => {\n  setCount(count + 1);\n});\n```\n\nEffect는 렌더링의 *결과*로 실행됩니다. state를 설정하면 렌더링이 *트리거*됩니다. Effect 안에서 즉시 상태를 설정하는 것은 기계의 전원 플러그를 기계 그 자체에 연결하는 것과 비슷합니다. Effect가 실행되고 상태가 설정되면 재렌더링이 발생하고, Effect가 다시 실행되고 상태가 설정되면 또 다른 재렌더링이 발생하며, 이런 식으로 계속됩니다.\n\nEffect는 일반적으로 컴포넌트를 *외부* 시스템과 동기화하는 데 사용됩니다. 외부 시스템이 없고 다른 상태에 기반하여 상태를 조정하려는 경우에는 [Effect가 필요하지 않을 수 있습니다.](/learn/you-might-not-need-an-effect)\n\n</Pitfall>\n\n### 2단계: Effect의 의존성 지정하기 {/*step-2-specify-the-effect-dependencies*/}\n\n기본적으로, Effect는 *모든* 렌더링 후에 실행됩니다. 이는 종종 **원하는 동작이 아닐 수 있습니다:**\n\n- 때때로 느릴 수 있습니다. 외부 시스템과 동기화하는 것이 항상 즉시 이루어지지 않기 때문에 필요하지 않을 경우에는 실행을 건너뛰고 싶을 수 있습니다. 예를 들어, 모든 키 입력마다 채팅 서버에 다시 연결하길 원하지 않을 것입니다.\n- 때때로 잘못될 수 있습니다. 예를 들어, 모든 키 입력마다 컴포넌트 fade-in 애니메이션을 트리거하길 원하지 않을 것입니다. 애니메이션은 컴포넌트가 처음 나타날 때에만 한 번 실행되어야 합니다.\n\n이 문제를 설명하기 위해 이전 예시에 몇 가지 `console.log` 호출과 부모 컴포넌트의 상태를 업데이트하는 텍스트 입력을 추가한 예시를 살펴보겠습니다. 입력할 때 Effect가 다시 실행되는 것을 주목하세요.\n\n<Sandpack>\n\n```js\nimport { useState, useRef, useEffect } from 'react';\n\nfunction VideoPlayer({ src, isPlaying }) {\n  const ref = useRef(null);\n\n  useEffect(() => {\n    if (isPlaying) {\n      console.log('video.play() 호출');\n      ref.current.play();\n    } else {\n      console.log('video.pause() 호출');\n      ref.current.pause();\n    }\n  });\n\n  return <video ref={ref} src={src} loop playsInline />;\n}\n\nexport default function App() {\n  const [isPlaying, setIsPlaying] = useState(false);\n  const [text, setText] = useState('');\n  return (\n    <>\n      <input value={text} onChange={e => setText(e.target.value)} />\n      <button onClick={() => setIsPlaying(!isPlaying)}>\n        {isPlaying ? '일시 정지' : '재생'}\n      </button>\n      <VideoPlayer\n        isPlaying={isPlaying}\n        src=\"https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4\"\n      />\n    </>\n  );\n}\n```\n\n```css\ninput, button { display: block; margin-bottom: 20px; }\nvideo { width: 250px; }\n```\n\n</Sandpack>\n\nReact에게 Effect를 **불필요하게 다시 실행하지 않도록 지시**하려면 `useEffect` 호출의 두 번째 인자로 *의존성(dependencies)* 배열을 지정하세요. 먼저 위의 예시에 빈 `[]` 배열을 14번째 줄에 추가하면 됩니다.\n\n```js {3}\n  useEffect(() => {\n    // ...\n  }, []);\n```\n\n`'isPlaying'`에 대한 의존성이 누락되었다는 오류가 표시될 것입니다.\n\n<Sandpack>\n\n```js\nimport { useState, useRef, useEffect } from 'react';\n\nfunction VideoPlayer({ src, isPlaying }) {\n  const ref = useRef(null);\n\n  useEffect(() => {\n    if (isPlaying) {\n      console.log('video.play() 호출');\n      ref.current.play();\n    } else {\n      console.log('video.pause() 호출');\n      ref.current.pause();\n    }\n  }, []); // 이 코드는 에러를 유발합니다\n\n  return <video ref={ref} src={src} loop playsInline />;\n}\n\nexport default function App() {\n  const [isPlaying, setIsPlaying] = useState(false);\n  const [text, setText] = useState('');\n  return (\n    <>\n      <input value={text} onChange={e => setText(e.target.value)} />\n      <button onClick={() => setIsPlaying(!isPlaying)}>\n        {isPlaying ? '일시 정지' : '재생'}\n      </button>\n      <VideoPlayer\n        isPlaying={isPlaying}\n        src=\"https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4\"\n      />\n    </>\n  );\n}\n```\n\n```css\ninput, button { display: block; margin-bottom: 20px; }\nvideo { width: 250px; }\n```\n\n</Sandpack>\n\n문제는 Effect 내부의 코드가 어떤 작업을 수행할지 결정하기 위해 `isPlaying` prop에 *의존*하지만 이 의존성이 명시적으로 선언되지 않았다는 것입니다. 이 문제를 해결하려면 의존성 배열에 `isPlaying`을 추가하세요.\n\n```js {2,7}\n  useEffect(() => {\n    if (isPlaying) { // 여기서 사용하니까...\n      // ...\n    } else {\n      // ...\n    }\n  }, [isPlaying]); // ...여기에 선언되어야겠네!\n```\n\n이제 모든 의존성이 의존성 배열 안에 선언되어 오류가 없을 것입니다. 의존성 배열로 `[isPlaying]`을 지정하면 React에게 이전 렌더링 중에 `isPlaying`이 이전과 동일하다면 Effect를 다시 실행하지 않도록 해야 한다고 알려줍니다. 이 변경으로 입력란에 입력을 입력하면 Effect가 다시 실행되지 않고, 재생/일시 정지 버튼을 누르면 Effect가 실행됩니다.\n\n<Sandpack>\n\n```js\nimport { useState, useRef, useEffect } from 'react';\n\nfunction VideoPlayer({ src, isPlaying }) {\n  const ref = useRef(null);\n\n  useEffect(() => {\n    if (isPlaying) {\n      console.log('video.play() 호출');\n      ref.current.play();\n    } else {\n      console.log('video.pause() 호출');\n      ref.current.pause();\n    }\n  }, [isPlaying]);\n\n  return <video ref={ref} src={src} loop playsInline />;\n}\n\nexport default function App() {\n  const [isPlaying, setIsPlaying] = useState(false);\n  const [text, setText] = useState('');\n  return (\n    <>\n      <input value={text} onChange={e => setText(e.target.value)} />\n      <button onClick={() => setIsPlaying(!isPlaying)}>\n        {isPlaying ? '일시 정지' : '재생'}\n      </button>\n      <VideoPlayer\n        isPlaying={isPlaying}\n        src=\"https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4\"\n      />\n    </>\n  );\n}\n```\n\n```css\ninput, button { display: block; margin-bottom: 20px; }\nvideo { width: 250px; }\n```\n\n</Sandpack>\n\n의존성 배열에는 여러 개의 종속성을 포함할 수 있습니다. React는 지정한 모든 종속성이 이전 렌더링의 그것과 정확히 동일한 값을 가진 경우에만 Effect를 다시 실행하지 않습니다. React는 [`Object.is`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is) 비교를 사용하여 종속성 값을 비교합니다. 자세한 내용은 [`useEffect` 참조 문서](/reference/react/useEffect#reference)를 참조하세요.\n\n**의존성을 \"선택\"할 수 없다는 점에 유의하세요.** 의존성 배열에 지정한 종속성이 Effect 내부의 코드를 기반으로 React가 기대하는 것과 일치하지 않으면 린트 에러가 발생합니다. 이를 통해 코드 내의 많은 버그를 잡을 수 있습니다. 코드가 다시 실행되길 원하지 않는 경우, [*Effect 내부를 수정하여* 그 종속성이 \"필요\"하지 않도록 만드세요.](/learn/lifecycle-of-reactive-effects#what-to-do-when-you-dont-want-to-re-synchronize)\n\n<Pitfall>\n\n의존성 배열이 없는 경우와 *빈* `[]` 의존성 배열이 있는 경우의 동작이 다릅니다.\n\n```js {3,7,11}\nuseEffect(() => {\n  // 모든 렌더링 후에 실행됩니다\n});\n\nuseEffect(() => {\n  // 마운트될 때만 실행됩니다 (컴포넌트가 나타날 때)\n}, []);\n\nuseEffect(() => {\n // 마운트될 때 실행되며, *또한* 렌더링 이후에 a 또는 b 중 하나라도 변경된 경우에도 실행됩니다\n}, [a, b]);\n```\n\n다음 단계에서 \"마운트(mount)\"가 무엇을 의미하는지 자세히 살펴보겠습니다.\n\n</Pitfall>\n\n<DeepDive>\n\n#### 왜 ref는 의존성 배열에서 생략해도 되나요? {/*why-was-the-ref-omitted-from-the-dependency-array*/}\n\n이 Effect는 `ref`와 `isPlaying`을 _모두_ 사용하지만, 의존성 배열 안에 선언된 것은 `isPlaying` 뿐입니다.\n\n```js {9}\nfunction VideoPlayer({ src, isPlaying }) {\n  const ref = useRef(null);\n  useEffect(() => {\n    if (isPlaying) {\n      ref.current.play();\n    } else {\n      ref.current.pause();\n    }\n  }, [isPlaying]);\n```\n\n이것은 `ref` 객체가 <em>안정된 식별성(stable identity)</em>을 가지기 때문입니다. React는 동일한 `useRef` 호출에서 항상 [같은 객체를 얻을 수 있음을](/reference/react/useRef#returns) 보장합니다. 이 객체는 절대 변경되지 않기 때문에 자체적으로 Effect를 다시 실행시키지 않습니다. 따라서 `ref`는 의존성 배열에 포함하든 포함하지 않든 상관없습니다. 포함해도 문제없습니다.\n\n```js {9}\nfunction VideoPlayer({ src, isPlaying }) {\n  const ref = useRef(null);\n  useEffect(() => {\n    if (isPlaying) {\n      ref.current.play();\n    } else {\n      ref.current.pause();\n    }\n  }, [isPlaying, ref]);\n```\n\n[`useState`](/reference/react/useState#setstate)로 반환되는 `set` 함수들도 안정된 식별성을 가지기 때문에, 종종 이러한 함수들도 의존성에서 생략되는 것을 볼 수 있습니다. 린터가 의존성을 생략해도 오류를 표시하지 않는다면 그렇게 해도 안전합니다.\n\n안정된 식별성을 가진 의존성을 생략하는 것은 린터가 해당 객체가 안정적임을 \"알 수\" 있는 경우에만 작동합니다. 예를 들어, `ref`가 부모 컴포넌트에서 전달되었다면, 의존성 배열에 명시해야 합니다. 이것은 좋은 접근 방식입니다. 왜냐하면 부모 컴포넌트가 항상 동일한 ref를 전달하는지 또는 여러 ref 중 하나를 조건부로 전달하는지 알 수 없기 때문입니다. 따라서 당신의 Effect는 전달되는 ref에 따라 달라질 것입니다.\n\n</DeepDive>\n\n### 3단계: 필요하다면 클린업을 추가하세요 {/*step-3-add-cleanup-if-needed*/}\n\n다른 예시를 고려해 보겠습니다. 사용자에게 표시될 때 채팅 서버에 연결해야 하는 `ChatRoom` 컴포넌트를 작성 중입니다. `createConnection()` API가 주어지며, 이 API는 `connect()` 및 `disconnect()` 메서드를 가진 객체를 반환합니다. 사용자에게 표시되는 동안 컴포넌트가 채팅 서버와의 연결을 유지하려면 어떻게 해야 할까요?\n\n먼저 Effect를 작성해 보겠습니다.\n\n```js\nuseEffect(() => {\n  const connection = createConnection();\n  connection.connect();\n});\n```\n\n매번 재렌더링 후에 채팅 서버에 연결하는 것은 느리므로 의존성 배열을 추가합니다.\n\n```js {4}\nuseEffect(() => {\n  const connection = createConnection();\n  connection.connect();\n}, []);\n```\n\n**Effect 내부의 코드는 어떠한 props나 상태도 사용하지 않으므로, 의존성 배열은 `[]` (빈 배열)입니다. 이는 React에게 이 코드를 컴포넌트가 \"마운트\"될 때만 실행하도록 알려줍니다. 즉, 화면에 처음으로 나타날 때에만 실행되게 됩니다.**\n\n이 코드를 실행해 보겠습니다.\n\n<Sandpack>\n\n```js\nimport { useEffect } from 'react';\nimport { createConnection } from './chat.js';\n\nexport default function ChatRoom() {\n  useEffect(() => {\n    const connection = createConnection();\n    connection.connect();\n  }, []);\n  return <h1>채팅에 오신걸 환영합니다!</h1>;\n}\n```\n\n```js src/chat.js\nexport function createConnection() {\n  // 실제 구현은 정말로 채팅 서버에 연결하는 것이 되어야 합니다.\n  return {\n    connect() {\n      console.log('✅ 연결 중...');\n    },\n    disconnect() {\n      console.log('❌ 연결이 끊겼습니다.');\n    }\n  };\n}\n```\n\n```css\ninput { display: block; margin-bottom: 20px; }\n```\n\n</Sandpack>\n\n이 Effect는 마운트될 때만 실행되므로 콘솔에 \"✅ 연결 중...\"이 한 번 출력될 것으로 예상할 수 있습니다. 그러나 콘솔을 확인해 보면 \"✅ 연결 중...\"이 두 번 출력됩니다. 왜 그럴까요?\n\nChatRoom 컴포넌트가 여러 화면으로 구성된 큰 앱의 일부라고 가정해 보겠습니다. 사용자가 ChatRoom 페이지에서 여정을 시작합니다. 컴포넌트가 마운트되고 `connection.connect()`를 호출합니다. 그런 다음 사용자가 다른 화면으로 이동한다고 상상해보세요. 예를 들어, 설정 페이지로 이동할 수 있습니다. ChatRoom 컴포넌트가 마운트 해제됩니다. 마지막으로 사용자가 뒤로 가기 버튼을 클릭하고 ChatRoom이 다시 마운트됩니다. 이렇게 되면 두 번째 연결이 설정되지만 첫 번째 연결은 종료되지 않았습니다! 사용자가 앱을 탐색하는 동안 연결은 종료되지 않고 계속 쌓일 것입니다.\n\n이와 같은 버그는 앱의 이곳저곳을 수동으로 테스트해보지 않으면 놓치기 쉽습니다. 이러한 문제를 빠르게 파악할 수 있도록 React는 개발 모드에서 초기 마운트 후 모든 컴포넌트를 한 번 다시 마운트합니다.\n\n\"✅ 연결 중...\" 로그가 두 번 출력되는 것을 보면 결국 무엇이 문제인지 알 수 있습니다. 컴포넌트가 마운트 해제될 때 연결을 닫지 않는 문제가 바로 그것이죠.\n\n이 문제를 해결하려면 Effect에서 클린업 함수를 반환하면 됩니다.\n\n```js {4-6}\n  useEffect(() => {\n    const connection = createConnection();\n    connection.connect();\n    return () => {\n      connection.disconnect();\n    };\n  }, []);\n```\n\nReact는 Effect가 다시 실행되기 전마다 클린업 함수를 호출하고, 컴포넌트가 마운트 해제(제거)될 때에도 마지막으로 호출합니다. 클린업 함수가 구현된 경우 어떤 일이 일어나는지 살펴보겠습니다.\n\n<Sandpack>\n\n```js\nimport { useState, useEffect } from 'react';\nimport { createConnection } from './chat.js';\n\nexport default function ChatRoom() {\n  useEffect(() => {\n    const connection = createConnection();\n    connection.connect();\n    return () => connection.disconnect();\n  }, []);\n  return <h1>채팅에 오신걸 환영합니다!</h1>;\n}\n```\n\n```js src/chat.js\nexport function createConnection() {\n  // 실제 구현은 정말로 채팅 서버에 연결하는 것이 되어야 합니다.\n  return {\n    connect() {\n      console.log('✅ 연결 중...');\n    },\n    disconnect() {\n      console.log('❌ 연결 해제됨');\n    }\n  };\n}\n```\n\n```css\ninput { display: block; margin-bottom: 20px; }\n```\n\n</Sandpack>\n\n이제 개발 모드에서 세 개의 콘솔 로그를 확인할 수 있습니다:\n\n1. `\"✅ 연결 중...\"`\n2. `\"❌ 연결 해제됨\"`\n3. `\"✅ 연결 중...\"`\n\n**이것이 개발 모드에서 올바른 동작입니다.** 컴포넌트를 다시 마운트함으로써 React는 사용자가 다른 부분을 탐색하고 다시 돌아와도 코드가 깨지지 않을 것임을 확인합니다. 연결을 해제하고 다시 연결하는 것이 바로 일어나는 일입니다! 클린업을 잘 구현하면 Effect를 한 번 실행하는 것과 실행, 클린업, 이후 다시 실행하는 것 사이에 사용자에게 보이는 차이가 없어야 합니다. 개발 중에는 연결/해제 호출이 하나 더 있는데, 이는 React가 개발 중에 코드를 검사하여 버그를 찾는 것입니다. 이것은 정상적인 동작입니다 - 이것을 없애려고 하지 마세요!\n\n**배포 환경에서는 `\"✅ 연결 중...\"`이 한 번만 출력됩니다.** 컴포넌트를 다시 마운트하는 것은 개발 중에만 발생하며 클린업이 필요한 Effect를 찾아주는 데 도움을 줍니다. 개발 동작에서 벗어나려면 [Strict Mode](/reference/react/StrictMode)를 끄는 것도 가능하지만, 켜둘 것을 권장합니다. 이렇게 하면 위와 같은 많은 버그를 찾을 수 있습니다.\n\n## 개발 중에 Effect가 두 번 실행되는 경우를 다루는 방법 {/*how-to-handle-the-effect-firing-twice-in-development*/}\n\nReact는 마지막 예시와 같은 버그를 찾기 위해 개발 중에 컴포넌트를 명시적으로 다시 마운트합니다. **\"Effect를 한 번 실행하는 방법\"이 아니라 \"어떻게 Effect가 다시 마운트된 후에도 작동하도록 고칠 것인가\"라는 것이 옳은 질문입니다.**\n\n일반적으로 정답은 클린업 함수를 구현하는 것입니다. 클린업 함수는 Effect가 수행하던 작업을 중단하거나 되돌리는 역할을 합니다. 기본 원칙은 사용자가 Effect가 한 번 실행되는 것(배포 환경과 같이)과 _설정 → 클린업 → 설정_ 순서(개발 중에 볼 수 있는 것) 간에 차이를 느끼지 못해야 합니다.\n\n작성할 대부분의 Effect는 아래의 일반적인 패턴 중 하나에 해당될 것입니다.\n\n<Pitfall>\n\n#### Effect가 두 번 실행되는 것을 막기위해 ref를 사용하지 마세요 {/*dont-use-refs-to-prevent-effects-from-firing*/}\n\nEffect가 개발 모드에서 두 번 실행되는 것을 막으려다 흔히 빠지는 함정은 `ref`를 사용해 Effect가 한 번만 실행되도록 하는 것입니다. 예를 들어 위의 버그를 `useRef`를 사용하여 \"수정\"하려고 할 수도 있습니다:\n\n```js {1,3-4}\n  const connectionRef = useRef(null);\n  useEffect(() => {\n    // 🚩 버그를 수정하지 않습니다!!!\n    if (!connectionRef.current) {\n      connectionRef.current = createConnection();\n      connectionRef.current.connect();\n    }\n  }, []);\n```\n\n이렇게 하면 개발 모드에서 `\"✅ 연결 중...\"`이 한 번만 보이지만 버그가 수정된 건 아닙니다.\n\n사용자가 다른 페이지로 이동해도 연결은 여전히 닫히지 않고, 다시 돌아오면 새 연결이 생성됩니다. 사용자가 앱을 탐색할수록 연결이 계속 쌓이게 되는데, 이는 \"수정\" 전과 동일합니다.\n\n버그를 수정하기 위해선 Effect를 단순히 한 번만 실행되도록 만드는 것으로는 부족합니다. Effect는 위에 있는 예시가 연결을 클린업 한것처럼 다시 마운트된 이후에도 제대로 동작해야 합니다.\n\n아래에 있는 일반적인 패턴을 다루는 예시를 살펴보세요.\n\n</Pitfall>\n\n### React로 작성되지 않은 위젯 제어하기 {/*controlling-non-react-widgets*/}\n\n가끔씩 React로 작성되지 않은 UI 위젯을 추가해야 할 때가 있습니다. 예를 들어, 페이지에 지도 컴포넌트를 추가한다고 가정해 보겠습니다. 이 지도 컴포넌트에는 `setZoomLevel()` 메서드가 있으며, `zoomLevel` state 변수와 동기화하려고 할 것입니다. Effect는 다음과 비슷할 것입니다.\n\n```js\nuseEffect(() => {\n  const map = mapRef.current;\n  map.setZoomLevel(zoomLevel);\n}, [zoomLevel]);\n```\n\n이 경우에는 클린업이 필요하지 않음을 유의하세요. 개발 모드에서 React는 Effect를 두 번 호출하지만, 동일한 값을 가지고 `setZoomLevel`을 두 번 호출하는 것은 아무런 문제가 되지 않습니다. 약간 느릴 수 있지만, 이것은 제품 환경에서 불필요하게 다시 마운트되지 않기 때문에 문제가 되지 않습니다.\n\n일부 API는 연속해서 두 번 호출하는 것을 허용하지 않을 수도 있습니다. 예를 들어 내장된 [`<dialog>`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement) 요소의 [`showModal`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/showModal) 메서드는 두 번 호출하면 예외를 던집니다. 클린업 함수를 구현하고 이 함수에서 대화 상자를 닫도록 만들어보세요.\n\n```js {4}\nuseEffect(() => {\n  const dialog = dialogRef.current;\n  dialog.showModal();\n  return () => dialog.close();\n}, []);\n```\n\n개발 중에는 Effect가 `showModal()`을 호출한 다음 즉시 `close()`를 호출하고 다시 `showModal()`을 호출합니다. 이것은 사용자가 확인할 수 있는 동작이며 제품 환경에서 볼 수 있는 것과 동일합니다.\n\n### 이벤트 구독하기 {/*subscribing-to-events*/}\n\n만약 Effect가 어떤 것을 구독한다면, 클린업 함수에서 구독을 해지해야 합니다.\n\n```js {6}\nuseEffect(() => {\n  function handleScroll(e) {\n    console.log(window.scrollX, window.scrollY);\n  }\n  window.addEventListener('scroll', handleScroll);\n  return () => window.removeEventListener('scroll', handleScroll);\n}, []);\n```\n\n개발 중에는 Effect가 `addEventListener()`를 호출한 다음 즉시 `removeEventListener()`를 호출하고, 그다음 동일한 핸들러로 `addEventListener()`를 호출합니다. 따라서 한 번에 하나의 활성 구독만 있게 됩니다. 이것은 제품 환경에서 한 번 `addEventListener()`를 호출하는 것과 동일한 동작을 가집니다.\n\n### 애니메이션 트리거 {/*triggering-animations*/}\n\nEffect가 어떤 요소를 애니메이션으로 표시하는 경우, 클린업 함수에서 애니메이션을 초기 값으로 재설정해야 합니다.\n\n```js {4-6}\nuseEffect(() => {\n  const node = ref.current;\n  node.style.opacity = 1; // Trigger the animation\n  return () => {\n    node.style.opacity = 0; // Reset to the initial value\n  };\n}, []);\n```\n\n개발 중에는 불투명도가 `1`로 설정되고, 그런 다음 `0`으로 설정되고, 다시 `1`로 설정됩니다. 이것은 제품 환경에서 `1`로 직접 설정하는 것과 동일한 동작을 가집니다. tweening을 지원하는 서드파티 애니메이션 라이브러리를 사용하는 경우 클린업 함수에서 타임라인을 초기 상태로 재설정해야 합니다.\n\n### 데이터 페칭 {/*fetching-data*/}\n\n만약 Effect가 어떤 데이터를 가져온다면, 클린업 함수에서는 [fetch를 중단](https://developer.mozilla.org/en-US/docs/Web/API/AbortController)하거나 결과를 무시해야 합니다.\n\n```js {2,6,13-15}\nuseEffect(() => {\n  let ignore = false;\n\n  async function startFetching() {\n    const json = await fetchTodos(userId);\n    if (!ignore) {\n      setTodos(json);\n    }\n  }\n\n  startFetching();\n\n  return () => {\n    ignore = true;\n  };\n}, [userId]);\n```\n\n이미 발생한 네트워크 요청을 \"실행 취소\"할 수는 없지만, 클린업 함수는 더 이상 관련이 없는 페치가 애플리케이션에 계속 영향을 미치지 않도록 보장해야 합니다. `userId`가 `'Alice'`에서 `'Bob'`으로 변경되면 클린업은 `'Bob'`이후에 도착하더라도 `'Alice'` 응답을 무시하도록 보장합니다.\n\n**개발 중에는 네트워크 탭에서 두 개의 페치가 표시됩니다.** 이는 문제가 없습니다. 위의 접근 방식을 사용하면 첫 번째 Effect는 즉시 클린업되어 `ignore` 변수의 복사본이 `true`로 설정됩니다. 따라서 추가 요청이 있더라도 `if (!ignore)` 검사 덕분에 state에 영향을 미치지 않습니다.\n\n**제품 환경에서는 하나의 요청만 있을 것입니다.** 개발 중에 두 번째 요청이 문제라면, 가장 좋은 방법은 중복 요청을 제거하고 컴포넌트 간에 응답을 캐시하는 솔루션을 사용하는 것입니다:\n\n```js\nfunction TodoList() {\n  const todos = useSomeDataLibrary(`/api/user/${userId}/todos`);\n  // ...\n```\n\n이렇게 하면 개발 환경을 개선하는데 도움이 될 뿐만 아니라 애플리케이션의 반응 속도도 향상됩니다. 예를 들어 사용자가 뒤로 가기 버튼을 눌렀을 때 데이터를 다시 로드하는 것을 기다릴 필요가 없습니다. 데이터가 캐시되기 때문입니다. 이러한 캐시를 직접 구축하거나 비슷한 효과를 누릴 수 있는 여러 대안 중 하나를 사용할 수 있습니다.\n\n<DeepDive>\n\n#### Effect에서 데이터를 가져오는 좋은 대안은 무엇인가요? {/*what-are-good-alternatives-to-data-fetching-in-effects*/}\n\nEffect 안에서 `fetch` 호출을 작성하는 것은 [데이터를 가져오는 인기 있는 방법](https://www.robinwieruch.de/react-hooks-fetch-data/)입니다, 특히 완전히 클라이언트 측 앱에서는요. 하지만 이는 매우 수동적인 접근 방식이며 중요한 단점이 있습니다.\n\n- **Effect는 서버에서 실행되지 않습니다.** 따라서 초기 서버 렌더링된 HTML은 데이터가 없는 로딩 상태만 포함하게 됩니다. 클라이언트 컴퓨터는 모든 JavaScript를 다운로드하고 앱을 렌더링해야만 데이터를 로드해야 한다는 것을 알게 될 것입니다. 이는 효율적이지 않습니다.\n- **Effect 안에서 직접 가져오면 \"네트워크 폭포\"를 쉽게 만들 수 있습니다.** 부모 컴포넌트를 렌더링하면 일부 데이터를 가져오고 자식 컴포넌트를 렌더링한 다음 그들이 데이터를 가져오기 시작합니다. 네트워크가 빠르지 않으면 이는 모든 데이터를 병렬로 가져오는 것보다 훨씬 느립니다.\n- **Effect 안에서 직접 가져오는 것은 일반적으로 데이터를 미리 로드하거나 캐시하지 않음을 의미합니다.** 예를 들어 컴포넌트가 마운트 해제되고 다시 마운트되면 데이터를 다시 가져와야 합니다.\n- **그리 편리하지 않습니다.** `fetch` 호출을 작성할 때 [경쟁 상태](https://maxrozen.com/race-conditions-fetching-data-react-with-useeffect)와 같은 버그에 영향을 받지 않는 방식으로 작성하는 데 꽤 많은 보일러플레이트 코드가 필요합니다.\n\n이 단점 목록은 React에만 해당되는 것은 아닙니다. 어떤 라이브러리에서든 마운트 시에 데이터를 가져온다면 비슷한 단점이 존재합니다. 마운트 시에 데이터를 페칭하는 것도 라우팅과 마찬가지로 잘 수행하기 어려운 작업이므로 다음 접근 방식을 권장합니다.\n\n- **[프레임워크](/learn/creating-a-react-app#full-stack-frameworks)를 사용하고 있다면, 그 프레임워크가 제공하는 내장 데이터 패칭 기능을 사용하세요.** 최신 React 프레임워크는 효율적인 데이터 패칭 메커니즘을 내장하고 있으며, 앞서 언급한 단점을 겪지 않습니다.\n- **사용하지 않는다면, 클라이언트 사이드 캐시를 사용하거나 직접 구축하는 것을 고려하세요.** 인기있는 오픈소스 솔루션으로는 [React Query](https://tanstack.com/query/latest), [useSWR](https://swr.vercel.app/), [React Router 6.4+](https://beta.reactrouter.com/en/main/start/overview)가 있습니다. 직접 구현할 수도 있는데, 이 경우에는 Effects를 사용하되, 요청 중복 제거, 응답 캐싱, 네트워크 워터폴 방지를 위한 로직을 추가해야 합니다(데이터를 미리 로드하거나, 필요한 데이터를 상위 라우트로 호이스팅하는 방식으로).\n\n이러한 접근 방식 중 어느 것도 적합하지 않은 경우, Effect 내에서 데이터를 직접 가져오는 것을 계속하셔도 됩니다.\n\n</DeepDive>\n\n### 분석 보내기 {/*sending-analytics*/}\n\n페이지 방문 시 분석 이벤트를 보내는 다음 코드를 고려해보세요.\n\n```js\nuseEffect(() => {\n  logVisit(url); // POST 요청을 보냄\n}, [url]);\n```\n\n개발 환경에서는 `logVisit`가 각 URL에 대해 두 번 호출될 것입니다. 그래서 이를 수정하고 싶을 수 있습니다. **우리는 이 코드를 그대로 유지하는 것을 권장합니다.** 이전 예시와 마찬가지로 한 번 실행하거나 두 번 실행하는 것 사이에서 *사용자가 볼 수 있는* 동작 차이가 없습니다. 실제로 개발 환경에서는 `logVisit`가 아무 작업도 수행하지 않아야 합니다. 왜냐하면 개발 환경의 로그가 제품 지표를 왜곡시키지 않도록 하기 위함입니다. 컴포넌트는 파일을 저장할 때마다 재마운트되므로 개발 환경에서는 추가적인 방문 기록을 로그에 남기게 됩니다.\n\n**제품 환경에서는 중복된 방문 로그가 없을 것입니다.**\n\n보내는 분석 이벤트를 디버깅하려면 앱을 스테이징 환경(제품 모드로 실행)에 배포하거나 [Strict Mode](/reference/react/StrictMode)를 일시적으로 사용 중지하여 개발 환경 전용의 재마운팅 검사를 수행할 수 있습니다. 또한 Effect 대신 라우트 변경 이벤트 핸들러에서 분석을 보낼 수도 있습니다. 더 정밀한 분석을 위해 [Intersection Observer](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API)를 사용하여 어떤 컴포넌트가 뷰포트에 있는지와 얼마나 오래 보이는지 추적하는 데 도움이 될 수 있습니다.\n\n### Effect가 아닌 경우: 애플리케이션 초기화 {/*not-an-effect-initializing-the-application*/}\n\n일부 로직은 애플리케이션 시작 시에 한 번만 실행되어야 합니다. 이러한 로직은 컴포넌트 외부에 배치할 수 있습니다.\n\n```js {2-3}\nif (typeof window !== 'undefined') { // 브라우저에서 실행 중인지 확인합니다.\n  checkAuthToken();\n  loadDataFromLocalStorage();\n}\n\nfunction App() {\n  // ...\n}\n```\n\n위와 같이 컴포넌트 외부에서 해당 로직을 실행하면, 해당 로직은 브라우저가 페이지를 로드한 후 한 번만 실행됨이 보장됩니다.\n\n### Effect가 아닌 경우: 제품 구입하기 {/*not-an-effect-buying-a-product*/}\n\n가끔은 클린업 함수를 작성하더라도 Effect가 두 번 실행되는 것에 대해 사용자가 확인할 수 있는 결과를 방지할 방법이 없을 수 있습니다. 예를 들어, 아래와 같이 제품을 구매하는 POST 요청을 보내는 Effect가 있다고 가정해 보겠습니다.\n\n```js {2-3}\nuseEffect(() => {\n  // 🔴 잘못된 방법: 이 Effect는 개발 환경에서 두 번 실행되며 코드에 문제가 드러납니다.\n  fetch('/api/buy', { method: 'POST' });\n}, []);\n```\n\n사용자는 제품을 두 번 구매하고 싶지 않을 것입니다. 그러나 이것은 이러한 로직을 Effect에 넣지 않아야 하는 이유입니다. 사용자가 다른 페이지로 이동한 다음 뒤로 가기 버튼을 누르는 경우 어떻게 될까요? Effect가 다시 실행됩니다. 사용자가 페이지를 방문할 때 제품을 구매하려고 하지 않으며, 사용자가 \"구매\" 버튼을 클릭할 때 제품을 구매하고 싶은 것입니다.\n\n구매는 렌더링에 의해 발생하는 것이 아니라 특정 상호 작용에 의해 발생합니다. 사용자가 버튼을 누를 때만 실행되어야 합니다. **Effect를 삭제하고 `/api/buy` 요청을 Buy 버튼의 이벤트 핸들러로 이동하세요.**\n\n```js {2-3}\n  function handleClick() {\n    // ✅ 구매는 특정 상호 작용에 의해 발생하는 이벤트입니다.\n    fetch('/api/buy', { method: 'POST' });\n  }\n```\n\n**만약 컴포넌트를 다시 마운트했을 때 애플리케이션의 로직이 깨진다면, 기존에 존재하던 버그가 드러난 것입니다.** 사용자의 관점에서 페이지를 방문하는 것과 페이지를 방문하고, 링크를 클릭한 다음, 뒤로 가기 버튼을 눌러서 다시 페이지로 돌아온것 과 차이가 없어야 합니다. React는 개발 환경에서 컴포넌트를 한 번 다시 마운트하여 이 원칙을 준수하는지 확인합니다.\n\n## 위에서 설명한 모든 것들 적용해보기 {/*putting-it-all-together*/}\n\n이 플레이그라운드를 살펴보면 실제로 Effect가 어떻게 작동하는지에 대한 \"느낌을 얻을\" 수 있습니다.\n\n이 예시는 [`setTimeout`](https://developer.mozilla.org/en-US/docs/Web/API/setTimeout)을 사용하여 Effect가 실행된 후 3초 후에 입력 텍스트와 함께 콘솔 로그가 표시되도록 합니다. 클린업 함수는 실행을 기다리는 타임아웃을 취소합니다. \"컴포넌트 마운트\" 버튼을 눌러 시작하세요.\n\n<Sandpack>\n\n```js\nimport { useState, useEffect } from 'react';\n\nfunction Playground() {\n  const [text, setText] = useState('a');\n\n  useEffect(() => {\n    function onTimeout() {\n      console.log('⏰ ' + text);\n    }\n\n    console.log('🔵 스케줄 로그 \"' + text);\n    const timeoutId = setTimeout(onTimeout, 3000);\n\n    return () => {\n      console.log('🟡 취소 로그 \"' + text);\n      clearTimeout(timeoutId);\n    };\n  }, [text]);\n\n  return (\n    <>\n      <label>\n        What to log:{' '}\n        <input\n          value={text}\n          onChange={e => setText(e.target.value)}\n        />\n      </label>\n      <h1>{text}</h1>\n    </>\n  );\n}\n\nexport default function App() {\n  const [show, setShow] = useState(false);\n  return (\n    <>\n      <button onClick={() => setShow(!show)}>\n        컴포넌트 {show ? '마운트 해제' : '마운트'}\n      </button>\n      {show && <hr />}\n      {show && <Playground />}\n    </>\n  );\n}\n```\n\n</Sandpack>\n\n처음에는 `Schedule \"a\" log`, `Cancel \"a\" log`, 그리고 다시 `Schedule \"a\" log` 라는 세 가지 로그를 볼 수 있을 것입니다. 몇 초 후에는 `a`라는 로그가 나타날 것입니다. 이전에 배운 내용처럼 추가된 스케줄/취소 쌍은 React가 컴포넌트를 개발 중에 한 번 다시 마운트하여 정리를 제대로 구현했는지 확인하기 때문입니다.\n\n이제 입력란을 `abc`로 수정해 보세요. 충분히 빠르게 입력하면 `Schedule \"ab\" log` 바로 뒤에 `Cancel \"ab\" log`와 `Schedule \"abc\" log`가 나타날 것입니다. **React는 항상 이전 렌더의 Effect를 다음 렌더의 Effect보다 먼저 정리합니다.** 따라서 빠르게 입력하더라도 한 번에 최대 하나의 타임아웃만 예약되는 것을 볼 수 있습니다. 입력을 몇 번 해보면서 Effect가 어떻게 정리되는지 느껴보세요.\n\n입력란에 무언가를 입력한 다음 \"컴포넌트 마운트 해제\"를 눌러보세요. 마운트 해제가 마지막 렌더의 Effect를 정리함을 주목하세요. 여기서는 타임아웃이 실행되기 전에 마지막 타임아웃이 취소됩니다.\n\n마지막으로 위 컴포넌트를 수정하고 정리 함수를 주석 처리하여 타임아웃이 취소되지 않도록 해보세요. `abcde`를 빠르게 입력해 보세요. 몇 초 후에 무엇이 기대되는지 생각해 보세요. 타임아웃 내부의 `console.log(text)`가 가장 최근의 `text`를 출력하고 다섯 번의 `abcde` 로그가 생성될까요? 직접 시도하여 확인해 보세요!\n\n수 초 후에 `a`, `ab`, `abc`, `abcd`, 그리고 `abcde`라는 일련의 로그를 볼 수 있을 것입니다. **각 Effect는 해당 렌더의 `text` 값을 \"캡처\"합니다.** `text` 상태가 변경되었는지 여부는 중요하지 않습니다. `text = 'ab'` 렌더의 Effect에서는 항상 `'ab'`를 볼 것입니다. 다시 말해, 각 렌더의 Effect는 서로 격리되어 있습니다. 이 작동 방식에 대해서 궁금하다면 [클로저](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures)에 대해 읽어볼 수 있습니다.\n\n<DeepDive>\n\n#### 각각의 렌더링은 각각의 고유한 Effect를 갖습니다. {/*each-render-has-its-own-effects*/}\n\n`useEffect`를 렌더링 결과물에 \"부착\"하는 것으로 생각할 수 있습니다. 다음과 같은 Effect를 고려해 보세요.\n\n```js\nexport default function ChatRoom({ roomId }) {\n  useEffect(() => {\n    const connection = createConnection(roomId);\n    connection.connect();\n    return () => connection.disconnect();\n  }, [roomId]);\n\n  return <h1>Welcome to {roomId}!</h1>;\n}\n```\n\n이제 사용자가 앱을 탐색하는 동안 정확히 어떤 일이 일어나는지 알아보겠습니다.\n\n#### 초기 렌더링 {/*initial-render*/}\n\n사용자가 `<ChatRoom roomId=\"general\" />`을 방문합니다. 이때, `roomId`를 `'general'`로 [멘탈모델 위에서 대체](/learn/state-as-a-snapshot#rendering-takes-a-snapshot-in-time)해보겠습니다.\n\n```js\n  // 첫 번째 렌더링에 대한 JSX (roomId = \"general\")\n  return <h1>Welcome to general!</h1>;\n```\n\n**Effect 또한 렌더링 결과물의 일부입니다.** 첫 번째 렌더링의 Effect는 다음과 같습니다.\n\n```js\n  // 첫 번째 렌더링에 대한 이펙트 (roomId = \"general\")\n  () => {\n    const connection = createConnection('general');\n    connection.connect();\n    return () => connection.disconnect();\n  },\n  // 첫 번째 렌더링의 의존성 (roomId = \"general\")\n  ['general']\n```\n\nReact는 이 Effect를 실행하며, `'general'` 채팅방에 연결합니다.\n\n#### 같은 의존성 사이에서의 재렌더링 {/*re-render-with-same-dependencies*/}\n\n`<ChatRoom roomId=\"general\" />`가 다시 렌더링된다고 가정해봅시다. JSX 결과물은 동일합니다.\n\n```js\n  // 두 번째 렌더링에 대한 JSX (roomId = \"general\")\n  return <h1>Welcome to general!</h1>;\n```\n\nReact는 렌더링 출력이 변경되지 않았기 때문에 DOM을 업데이트하지 않습니다.\n\n두 번째 렌더링에서의 Effect는 다음과 같습니다.\n\n```js\n  // 두 번째 렌더링에 대한 Effect (roomId = \"general\")\n  () => {\n    const connection = createConnection('general');\n    connection.connect();\n    return () => connection.disconnect();\n  },\n  // 두 번째 렌더링에 대한 의존성 (roomId = \"general\")\n  ['general']\n```\n\nReact는 두 번째 렌더링에서의 `['general']`를 첫 번째 렌더링에서의 `['general']`와 비교합니다. **모든 의존성이 동일하므로 React는 두 번째 렌더링에서의 Effect를 *무시*합니다.** 해당 Effect는 호출되지 않습니다.\n\n#### 다른 의존성으로 재렌더링 {/*re-render-with-different-dependencies*/}\n\n그럼, 사용자가 `<ChatRoom roomId=\"travel\" />`을 탐색합니다. 이번에는 컴포넌트가 다른 JSX를 반환합니다.\n\n```js\n  // 세 번째 렌더링에 대한 JSX (roomId = \"travel\")\n  return <h1>Welcome to travel!</h1>;\n```\n\nReact는 DOM을 업데이트하여 `\"Welcome to general\"`을 `\"Welcome to travel\"`로 변경합니다.\n\n세 번째 렌더링에서의 Effect는 다음과 같습니다:\n\n```js\n  // 세 번째 렌더링에 대한 Effect (roomId = \"travel\")\n  () => {\n    const connection = createConnection('travel');\n    connection.connect();\n    return () => connection.disconnect();\n  },\n  // 세 번째 렌더링에 대한 의존성 (roomId = \"travel\")\n  ['travel']\n```\n\nReact는 세 번째 렌더링에서의 `['travel']`와 두 번째 렌더링에서의 `['general']`를 비교합니다. 하나의 의존성이 다릅니다: `Object.is('travel', 'general')`은 `false`입니다. Effect는 건너뛸 수 없습니다.\n\n**React는 세 번째 렌더링의 Effect를 적용하기 전에 먼저 실행된 Effect를 정리해야 합니다.** 두 번째 렌더링의 Effect가 건너뛰어졌기 때문에, React는 첫 번째 렌더링의 Effect를 정리해야 합니다. 처음 렌더링되었을 때 스크롤하면, `createConnection('general')`로 생성된 연결에 대해 `disconnect()`를 호출하는 것을 볼 수 있습니다. 이로써 앱은 `'general'` 채팅방과의 연결이 해제됩니다.\n\n그 후에 React는 세 번째 렌더링의 Effect를 실행합니다. `'travel'` 채팅방에 연결합니다.\n\n#### 마운트 해제 {/*unmount*/}\n\n마지막으로, 사용자가 다른 페이지로 이동하게 되어 `ChatRoom` 컴포넌트가 마운트 해제됩니다. React는 마지막 Effect의 클린업 함수를 실행합니다. 마지막 Effect는 세 번째 렌더링에서 온 것입니다. 세 번째 렌더링의 클린업은 `createConnection('travel')` 연결을 종료합니다. 그래서 앱은 `'travel'` 채팅방과의 연결을 해제하게 됩니다.\n\n#### 개발 환경에서만의 동작 {/*development-only-behaviors*/}\n\n[Strict Mode](/reference/react/StrictMode)가 활성화된 경우, React는 모든 컴포넌트를 한 번 마운트한 후에 다시 마운트합니다(state와 DOM은 보존됩니다). 이는 [클린업이 필요한 Effect를 찾는 데 도움이 되며](#step-3-add-cleanup-if-needed) 경쟁 조건과 같은 버그를 초기에 드러날 수 있게 합니다. 게다가 React는 개발 중 파일을 저장할 때마다 Effect를 다시 마운트합니다. 이러한 두 가지 동작은 개발 환경에서만 적용됩니다.\n\n</DeepDive>\n\n<Recap>\n\n- 이벤트와 달리 Effect는 특정 상호작용이 아닌 렌더링 자체에 의해 발생합니다.\n- Effect를 사용하면 컴포넌트를 외부 시스템(타사 API, 네트워크 등)과 동기화할 수 있습니다.\n- 기본적으로 Effect는 모든 렌더링(초기 렌더링 포함) 후에 실행됩니다.\n- React는 모든 의존성이 마지막 렌더링과 동일한 값을 가지면 Effect를 건너뜁니다.\n- 의존성을 \"선택\"할 수 없습니다. 의존성은 Effect 내부의 코드에 의해 결정됩니다.\n- 빈 의존성 배열(`[]`)은 컴포넌트 \"마운팅\"(화면에 추가됨)을 의미합니다.\n- Strict Mode에서 React는 컴포넌트를 두 번 마운트합니다(개발 환경에서만!) 이는 Effect의 스트레스 테스트를 위한 것입니다.\n- Effect가 다시 마운트로 인해 중단된 경우 클린업 함수를 구현해야 합니다.\n- React는 Effect가 다음에 실행되기 전에 정리 함수를 호출하며, 마운트 해제 중에도 호출합니다.\n\n</Recap>\n\n<Challenges>\n\n#### 마운트시 input 필드에 포커스하기 {/*focus-a-field-on-mount*/}\n\n이 예시에서는 폼이 `<MyInput />` 컴포넌트를 렌더링합니다.\n\n화면에 나타날 때 `MyInput`이 자동으로 포커스되도록 입력의 [`focus()`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus) 메서드를 사용하세요. 이미 주석 처리된 구현이 있지만 제대로 작동하지 않습니다. 왜 작동하지 않는지 확인하고 수정해 보세요. (`autoFocus` 속성은 존재하지 않는 것으로 가정하세요. 우리는 처음부터 동일한 기능을 다시 구현하고 있습니다.)\n\n<Sandpack>\n\n```js src/MyInput.js active\nimport { useEffect, useRef } from 'react';\n\nexport default function MyInput({ value, onChange }) {\n  const ref = useRef(null);\n\n  // TODO: This doesn't quite work. Fix it.\n  // ref.current.focus()\n\n  return (\n    <input\n      ref={ref}\n      value={value}\n      onChange={onChange}\n    />\n  );\n}\n```\n\n```js src/App.js hidden\nimport { useState } from 'react';\nimport MyInput from './MyInput.js';\n\nexport default function Form() {\n  const [show, setShow] = useState(false);\n  const [name, setName] = useState('Taylor');\n  const [upper, setUpper] = useState(false);\n  return (\n    <>\n      <button onClick={() => setShow(s => !s)}>form {show ? '숨기기' : '보기'}</button>\n      <br />\n      <hr />\n      {show && (\n        <>\n          <label>\n            이름을 입력하세요:\n            <MyInput\n              value={name}\n              onChange={e => setName(e.target.value)}\n            />\n          </label>\n          <label>\n            <input\n              type=\"checkbox\"\n              checked={upper}\n              onChange={e => setUpper(e.target.checked)}\n            />\n            대문자로 만들기\n          </label>\n          <p>안녕하세요, <b>{upper ? name.toUpperCase() : name}님</b></p>\n        </>\n      )}\n    </>\n  );\n}\n```\n\n```css\nlabel {\n  display: block;\n  margin-top: 20px;\n  margin-bottom: 20px;\n}\n\nbody {\n  min-height: 150px;\n}\n```\n\n</Sandpack>\n\n\n솔루션이 제대로 작동하는지 확인하려면 \"form 보기\"를 누르고 입력란이 포커스되는지 확인하세요.(강조 표시, 커서가 내부에 배치됨). \"form 숨기기\"를 누르고 다시 \"form 보기\"를 눌러 입력란이 다시 강조 표시되는지 확인하세요.\n\n`MyInput`은 렌더링 후 매번 포커스되는 것이 아니라 _마운트 시에만_ 포커스되어야 합니다. 이 동작이 올바른지 확인하려면 \"form 보기\"를 누른 다음 \"대문자로 만들기\" 체크박스를 반복해서 클릭하세요. 체크박스를 클릭해도 상단의 입력란은 포커스가 _되지 않아야_ 합니다.\n\n<Solution>\n\n렌더링 중에 `ref.current.focus()`를 호출하는 것은 적절하지 않습니다. *부수 효과*이기 때문입니다. 부수 효과는 이벤트 핸들러 내부에 배치하거나 `useEffect`로 선언해야 합니다. 이 경우에는 부작용이 특정 상호작용이 아니라 컴포넌트가 나타나는 것에 의해 _발생되기_ 때문에 Effect 내부에 넣는 것이 맞습니다.\n\n실수를 고치려면 `ref.current.focus()` 호출을 Effect 선언으로 감싸세요. 그런 다음, 이 Effect가 렌더링 후 매번 실행되는 것이 아니라 마운트 시에만 실행되도록 하려면 빈 `[]` 의존성을 추가하세요.\n\n<Sandpack>\n\n```js src/MyInput.js active\nimport { useEffect, useRef } from 'react';\n\nexport default function MyInput({ value, onChange }) {\n  const ref = useRef(null);\n\n  useEffect(() => {\n    ref.current.focus();\n  }, []);\n\n  return (\n    <input\n      ref={ref}\n      value={value}\n      onChange={onChange}\n    />\n  );\n}\n```\n\n```js src/App.js hidden\nimport { useState } from 'react';\nimport MyInput from './MyInput.js';\n\nexport default function Form() {\n  const [show, setShow] = useState(false);\n  const [name, setName] = useState('Taylor');\n  const [upper, setUpper] = useState(false);\n  return (\n    <>\n      <button onClick={() => setShow(s => !s)}>form {show ? '숨기기' : '보기'} form</button>\n      <br />\n      <hr />\n      {show && (\n        <>\n          <label>\n            이름을 입력하세요:\n            <MyInput\n              value={name}\n              onChange={e => setName(e.target.value)}\n            />\n          </label>\n          <label>\n            <input\n              type=\"checkbox\"\n              checked={upper}\n              onChange={e => setUpper(e.target.checked)}\n            />\n            대문자로 만들기\n          </label>\n          <p>안녕하세요, <b>{upper ? name.toUpperCase() : name}</b>님</p>\n        </>\n      )}\n    </>\n  );\n}\n```\n\n```css\nlabel {\n  display: block;\n  margin-top: 20px;\n  margin-bottom: 20px;\n}\n\nbody {\n  min-height: 150px;\n}\n```\n\n</Sandpack>\n\n</Solution>\n\n#### 조건부로 input 필드에 포커스하기 {/*focus-a-field-conditionally*/}\n\n이 폼은 두 개의 `<MyInput />` 컴포넌트를 렌더링합니다.\n\n\"form 보기\"를 누르면 두 번째 필드가 자동으로 포커스됩니다. 이는 두 `<MyInput />` 컴포넌트 모두 내부의 필드에 포커스를 주려고 하기 때문입니다. 두 개의 입력 필드에 연속해서 `focus()`를 호출하면 마지막 호출이 항상 \"승리하게\" 됩니다.\n\n이제 첫 번째 필드에 포커스를 주려면 첫 번째 `MyInput` 컴포넌트가 `true`로 설정된 `shouldFocus` prop을 받도록 변경해야 합니다. 변경된 로직에 따라 `MyInput`이 받은 `shouldFocus` prop이 `true`일 때에만 `focus()`가 호출되도록 변경해 보세요.\n\n<Sandpack>\n\n```js src/MyInput.js active\nimport { useEffect, useRef } from 'react';\n\nexport default function MyInput({ shouldFocus, value, onChange }) {\n  const ref = useRef(null);\n\n  // TODO: shouldFocus가 true일때만 호출되도록\n  useEffect(() => {\n    ref.current.focus();\n  }, []);\n\n  return (\n    <input\n      ref={ref}\n      value={value}\n      onChange={onChange}\n    />\n  );\n}\n```\n\n```js src/App.js hidden\nimport { useState } from 'react';\nimport MyInput from './MyInput.js';\n\nexport default function Form() {\n  const [show, setShow] = useState(false);\n  const [firstName, setFirstName] = useState('Taylor');\n  const [lastName, setLastName] = useState('Swift');\n  const [upper, setUpper] = useState(false);\n  const name = firstName + ' ' + lastName;\n  return (\n    <>\n      <button onClick={() => setShow(s => !s)}>form {show ? '숨기기' : '보기'}</button>\n      <br />\n      <hr />\n      {show && (\n        <>\n          <label>\n            이름을 입력하세요:\n            <MyInput\n              value={firstName}\n              onChange={e => setFirstName(e.target.value)}\n              shouldFocus={true}\n            />\n          </label>\n          <label>\n            성을 입력하세요:\n            <MyInput\n              value={lastName}\n              onChange={e => setLastName(e.target.value)}\n              shouldFocus={false}\n            />\n          </label>\n          <p>안녕하세요, <b>{upper ? name.toUpperCase() : name}</b>님</p>\n        </>\n      )}\n    </>\n  );\n}\n```\n\n```css\nlabel {\n  display: block;\n  margin-top: 20px;\n  margin-bottom: 20px;\n}\n\nbody {\n  min-height: 150px;\n}\n```\n\n</Sandpack>\n\n해당 코드를 실행하고 주어진 검증 방법을 따라 진행해 봅시다. \"form 보기\" 버튼을 반복적으로 누르고 \"form 숨기기\" 버튼을 클릭하여 결과를 확인할 수 있습니다. 폼이 나타날 때, *첫 번째* 입력 필드에만 포커스가 설정됩니다. 부모 컴포넌트가 첫 번째 입력 필드를 `shouldFocus={true}`로 렌더링하고 두 번째 입력 필드를 `shouldFocus={false}`로 렌더링하기 때문입니다. 또한 두 입력 필드 모두 정상적으로 작동하며, 둘 다 텍스트를 입력할 수 있습니다.\n\n<Hint>\n\n조건부로 `useEffect`를 선언할 수는 없지만, `useEffect` 내부에 조건부 로직을 포함시켜 원하는 동작을 구현할 수 있습니다.\n\n</Hint>\n\n<Solution>\n\n조건부 로직을 Effect 내부로 넣어주세요. `shouldFocus`를 Effect 내에서 사용하므로 이를 의존성으로 명시해야 합니다. (만약 어떤 input의 `shouldFocus`가 `false`에서 `true`로 변경된다면, 마운트 후에 포커스가 될 것입니다.)\n\n<Sandpack>\n\n```js src/MyInput.js active\nimport { useEffect, useRef } from 'react';\n\nexport default function MyInput({ shouldFocus, value, onChange }) {\n  const ref = useRef(null);\n\n  useEffect(() => {\n    if (shouldFocus) {\n      ref.current.focus();\n    }\n  }, [shouldFocus]);\n\n  return (\n    <input\n      ref={ref}\n      value={value}\n      onChange={onChange}\n    />\n  );\n}\n```\n\n```js src/App.js hidden\nimport { useState } from 'react';\nimport MyInput from './MyInput.js';\n\nexport default function Form() {\n  const [show, setShow] = useState(false);\n  const [firstName, setFirstName] = useState('Taylor');\n  const [lastName, setLastName] = useState('Swift');\n  const [upper, setUpper] = useState(false);\n  const name = firstName + ' ' + lastName;\n  return (\n    <>\n      <button onClick={() => setShow(s => !s)}>form {show ? '숨기기' : '보기'}</button>\n      <br />\n      <hr />\n      {show && (\n        <>\n          <label>\n            이름을 입력하세요:\n            <MyInput\n              value={firstName}\n              onChange={e => setFirstName(e.target.value)}\n              shouldFocus={true}\n            />\n          </label>\n          <label>\n            성을 입력하세요:\n            <MyInput\n              value={lastName}\n              onChange={e => setLastName(e.target.value)}\n              shouldFocus={false}\n            />\n          </label>\n          <p>안녕하세요, <b>{upper ? name.toUpperCase() : name}님</b></p>\n        </>\n      )}\n    </>\n  );\n}\n```\n\n```css\nlabel {\n  display: block;\n  margin-top: 20px;\n  margin-bottom: 20px;\n}\n\nbody {\n  min-height: 150px;\n}\n```\n\n</Sandpack>\n\n</Solution>\n\n#### 두 번 실행되는 interval 고치기 {/*fix-an-interval-that-fires-twice*/}\n\n아래 `Counter` 컴포넌트는 매 초마다 증가하는 카운터를 나타냅니다. 컴포넌트가 마운트될 때 [`setInterval`](https://developer.mozilla.org/en-US/docs/Web/API/setInterval)을 호출합니다. 이로 인해 `onTick` 함수가 매 초마다 실행됩니다. `onTick` 함수는 카운터를 증가시킵니다.\n\n하지만 1초마다 한 번씩 증가하는 대신 두 번씩 증가합니다. 왜 그럴까요? 버그의 원인을 찾아 수정하세요.\n\n<Hint>\n\n`setInterval`은 interval ID를 반환하는데, 이를 [`clearInterval`](https://developer.mozilla.org/en-US/docs/Web/API/clearInterval) 함수에 전달하여 interval을 중지할 수 있습니다.\n\n</Hint>\n\n<Sandpack>\n\n```js src/Counter.js active\nimport { useState, useEffect } from 'react';\n\nexport default function Counter() {\n  const [count, setCount] = useState(0);\n\n  useEffect(() => {\n    function onTick() {\n      setCount(c => c + 1);\n    }\n\n    setInterval(onTick, 1000);\n  }, []);\n\n  return <h1>{count}</h1>;\n}\n```\n\n```js src/App.js hidden\nimport { useState } from 'react';\nimport Counter from './Counter.js';\n\nexport default function Form() {\n  const [show, setShow] = useState(false);\n  return (\n    <>\n      <button onClick={() => setShow(s => !s)}>카운터 {show ? '숨기기' : '보기'}</button>\n      <br />\n      <hr />\n      {show && <Counter />}\n    </>\n  );\n}\n```\n\n```css\nlabel {\n  display: block;\n  margin-top: 20px;\n  margin-bottom: 20px;\n}\n\nbody {\n  min-height: 150px;\n}\n```\n\n</Sandpack>\n\n<Solution>\n\n[Strict Mode](/reference/react/StrictMode)가 활성화된 경우 (이 사이트의 코드 예시 샌드박스처럼), React는 개발 중에 각 컴포넌트를 한 번씩 리마운트합니다. 이로 인해 간격이 두 번 설정되어 매 초마다 카운터가 두 번 증가합니다.\n\n그러나 React의 동작이 버그의 *원인*은 아닙니다. 버그는 코드에 있습니다. React의 동작은 버그를 더 눈에 띄게 만듭니다. 실제 문제는 이 Effect가 프로세스를 시작한 후에 클린업할 수 있는 방법을 제공하지 않는 것입니다.\n\n이 코드를 수정하려면 `setInterval`에 의해 반환된 interval ID를 저장하고, [`clearInterval`](https://developer.mozilla.org/en-US/docs/Web/API/clearInterval)을 사용하여 클린업 함수를 구현하세요.\n\n<Sandpack>\n\n```js src/Counter.js active\nimport { useState, useEffect } from 'react';\n\nexport default function Counter() {\n  const [count, setCount] = useState(0);\n\n  useEffect(() => {\n    function onTick() {\n      setCount(c => c + 1);\n    }\n\n    const intervalId = setInterval(onTick, 1000);\n    return () => clearInterval(intervalId);\n  }, []);\n\n  return <h1>{count}</h1>;\n}\n```\n\n```js src/App.js hidden\nimport { useState } from 'react';\nimport Counter from './Counter.js';\n\nexport default function App() {\n  const [show, setShow] = useState(false);\n  return (\n    <>\n      <button onClick={() => setShow(s => !s)}>카운터 {show ? '숨기기' : '보기'}</button>\n      <br />\n      <hr />\n      {show && <Counter />}\n    </>\n  );\n}\n```\n\n```css\nlabel {\n  display: block;\n  margin-top: 20px;\n  margin-bottom: 20px;\n}\n\nbody {\n  min-height: 150px;\n}\n```\n\n</Sandpack>\n\n개발 중에 React는 여전히 컴포넌트를 한 번 리마운트하여 클린업이 잘 구현되었는지 확인합니다. 따라서 최초의 `setInterval` 호출 이후에, 바로 다음 `clearInterval`, 그리고 다시 `setInterval` 호출이 발생합니다. 프로덕션(운영 환경)에서는 `setInterval` 호출이 한 번만 있을 것입니다. 개발 환경과 운영 환경 모두 사용자가 볼 수 있는 동작은 동일합니다. 카운터가 1초마다 한 번씩 증가하는 것이죠.\n\n</Solution>\n\n#### Effect 내부에서의 잘못된 데이터 페칭 고치기 {/*fix-fetching-inside-an-effect*/}\n\n이 컴포넌트는 select 태그로 선택한 사람의 일대기를 보여줍니다. 이 컴포넌트는 선택된 `person`이 변경될 때마다, 또한 마운트될 때마다 비동기 함수 `fetchBio(person)`를 호출하여 일대기를 불러옵니다. 이 비동기 함수는 [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)를 반환하며, 이 Promise는 결국 문자열로 resolve됩니다. 불러오기가 완료되면 `setBio`를 호출하여 해당 문자열을 select의 option으로 표시합니다.\n\n<Sandpack>\n\n{/* not the most efficient, but this validation is enabled in the linter only, so it's fine to ignore it here since we know what we're doing */}\n```js src/App.js\nimport { useState, useEffect } from 'react';\nimport { fetchBio } from './api.js';\n\nexport default function Page() {\n  const [person, setPerson] = useState('Alice');\n  const [bio, setBio] = useState(null);\n\n  useEffect(() => {\n    setBio(null);\n    fetchBio(person).then(result => {\n      setBio(result);\n    });\n  }, [person]);\n\n  return (\n    <>\n      <select value={person} onChange={e => {\n        setPerson(e.target.value);\n      }}>\n        <option value=\"Alice\">Alice</option>\n        <option value=\"Bob\">Bob</option>\n        <option value=\"Taylor\">Taylor</option>\n      </select>\n      <hr />\n      <p><i>{bio ?? 'Loading...'}</i></p>\n    </>\n  );\n}\n```\n\n```js src/api.js hidden\nexport async function fetchBio(person) {\n  const delay = person === 'Bob' ? 2000 : 200;\n  return new Promise(resolve => {\n    setTimeout(() => {\n      resolve('이것은 ' + person + '의 일대기입니다.');\n    }, delay);\n  })\n}\n\n```\n\n</Sandpack>\n\n\n이 코드에 버그가 있습니다. \"Alice\"를 선택한 다음 \"Bob\"을 선택한 다음 바로 \"Taylor\"을 선택하면 버그가 발생합니다. 충분히 빠르게 이 작업을 수행하면 버그를 확인할 수 있습니다. Taylor가 선택되었지만 아래 단락에는 \"이것은 Bob의 일대기입니다.\"라고 표시됩니다.\n\n이러한 현상이 발생하는 이유는 무엇일까요? 이 Effect 내부의 버그를 수정하세요.\n\n<Hint>\n\nEffect가 비동기로 무언가를 가져오는 경우 일반적으로 클린업이 필요합니다.\n\n</Hint>\n\n<Solution>\n\n버그를 트리거하려면 다음 순서대로 진행되어야 합니다:\n\n- `'Bob'`을 선택하면 `fetchBio('Bob')`가 트리거됩니다.\n- `'Taylor'`을 선택하면 `fetchBio('Taylor')`가 트리거됩니다.\n- **`'Taylor'`의 일대기를 가져오는 작업이 `'Bob'`의 일대기를 가져오는 작업보다 *먼저* 완료됩니다.**\n- `'Taylor'` 렌더링의 Effect가 `setBio('This is Taylor’s bio')`를 호출합니다.\n- `'Bob'`의 일대기를 가져오는 작업이 완료됩니다.\n- `'Bob'` 렌더링의 Effect가 `setBio('This is Bob’s bio')`를 호출합니다.\n\n이렇게 하면 Taylor가 선택되었음에도 불구하고 Bob의 일대기가 표시됩니다. 이와 같은 버그는 두 개의 비동기 작업이 \"경쟁(race)\"하며 작업 완료의 순서를 예상할 수 없는 [경쟁 조건(race condition)](https://en.wikipedia.org/wiki/Race_condition)이라고 합니다.\n\n이 경쟁 조건을 해결하려면 클린업 함수를 추가하세요.\n\n<Sandpack>\n\n{/* not the most efficient, but this validation is enabled in the linter only, so it's fine to ignore it here since we know what we're doing */}\n```js src/App.js\nimport { useState, useEffect } from 'react';\nimport { fetchBio } from './api.js';\n\nexport default function Page() {\n  const [person, setPerson] = useState('Alice');\n  const [bio, setBio] = useState(null);\n  useEffect(() => {\n    let ignore = false;\n    setBio(null);\n    fetchBio(person).then(result => {\n      if (!ignore) {\n        setBio(result);\n      }\n    });\n    return () => {\n      ignore = true;\n    }\n  }, [person]);\n\n  return (\n    <>\n      <select value={person} onChange={e => {\n        setPerson(e.target.value);\n      }}>\n        <option value=\"Alice\">Alice</option>\n        <option value=\"Bob\">Bob</option>\n        <option value=\"Taylor\">Taylor</option>\n      </select>\n      <hr />\n      <p><i>{bio ?? 'Loading...'}</i></p>\n    </>\n  );\n}\n```\n\n```js src/api.js hidden\nexport async function fetchBio(person) {\n  const delay = person === 'Bob' ? 2000 : 200;\n  return new Promise(resolve => {\n    setTimeout(() => {\n      resolve('이것은 ' + person + '의 일대기입니다.');\n    }, delay);\n  })\n}\n\n```\n\n</Sandpack>\n\n각 렌더링의 Effect는 자체 `ignore` 변수를 가지고 있습니다. 처음에 `ignore` 변수는 `false`로 설정됩니다. 그러나 Effect가 클린업되면(예: 다른 사람을 선택할 때), 해당 Effect의 `ignore` 변수는 `true`로 설정됩니다. 이제 어떤 순서로 요청이 완료되는지는 중요하지 않습니다. 마지막 사람의 Effect만 `ignore`가 `false`로 설정되므로 `setBio(result)`를 호출합니다. 이전 Effect는 정리되었으므로 `if (!ignore)` 검사가 `setBio` 호출을 방지합니다:\n\n- `'Bob'`을 선택하면 `fetchBio('Bob')`가 트리거됩니다.\n- `'Taylor'`을 선택하면 `fetchBio('Taylor')`가 트리거되며 이전(Bob의) Effect가 **정리(cleaned up)**됩니다.\n- `'Taylor'`의 일대기를 가져오는 작업이 `'Bob'`의 일대기를 가져오는 작업보다 *먼저* 완료됩니다.\n- `'Taylor'` 렌더링의 Effect가 `setBio('This is Taylor’s bio')`를 호출합니다.\n- `'Bob'`의 일대기를 가져오는 작업이 완료됩니다.\n- `'Bob'` 렌더링의 Effect는 **`ignore` 플래그가 `true`로 설정되었기 때문에 아무 일도 수행하지 않습니다.**\n\n오래된 API 호출의 결과를 무시하는 것 외에도 더 이상 필요하지 않은 요청을 취소하기 위해 [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController)를 사용할 수도 있습니다. 그러나 이것만으로는 경쟁 조건에 대한 충분한 보호가 이뤄지지 않습니다. 피치 못할 상황에서는 추가적인 비동기 작업이 후행할 수 있으므로 `ignore`와 같은 명시적 플래그를 사용하는 것이 이러한 종류의 문제를 가장 안전하게 해결하는 가장 신뢰할 수 있는 방법입니다.\n\n</Solution>\n\n</Challenges>\n"
  },
  {
    "path": "src/content/learn/thinking-in-react.md",
    "content": "---\ntitle: React로 사고하기\n---\n\n<Intro>\n\nReact를 사용하면 우리가 고려하고 있는 디자인이나 만들 앱에 대한 생각을 바꿀 수 있습니다. React로 사용자 인터페이스를 빌드할 때, 먼저 이를 컴포넌트라는 조각으로 나눕니다. 그리고 각 컴포넌트의 다양한 시각적 상태들을 정의합니다. 마지막으로 컴포넌트들을 연결하여 데이터가 그 사이를 흘러가게 합니다. 이 자습서에서는 React로 검색할 수 있는 상품 테이블을 만드는 과정을 체계적으로 안내해 드리겠습니다.\n\n</Intro>\n\n## 모의 시안과 함께 시작하기 {/*start-with-the-mockup*/}\n\n이미 JSON API와 디자이너로부터 제공받은 모의 시안이 있다고 생각해 봅시다.\nJSON API는 아래와 같은 형태의 데이터를 반환합니다.\n\n```json\n[\n  { category: \"Fruits\", price: \"$1\", stocked: true, name: \"Apple\" },\n  { category: \"Fruits\", price: \"$1\", stocked: true, name: \"Dragonfruit\" },\n  { category: \"Fruits\", price: \"$2\", stocked: false, name: \"Passionfruit\" },\n  { category: \"Vegetables\", price: \"$2\", stocked: true, name: \"Spinach\" },\n  { category: \"Vegetables\", price: \"$4\", stocked: false, name: \"Pumpkin\" },\n  { category: \"Vegetables\", price: \"$1\", stocked: true, name: \"Peas\" }\n]\n```\n\n모의 시안은 다음과 같이 생겼습니다.\n\n<img src=\"/images/docs/s_thinking-in-react_ui.png\" width=\"300\" style={{margin: '0 auto'}} />\n\nReact로 UI를 구현하기 위해서 일반적으로 다섯 가지 단계를 거칩니다.\n\n## Step 1: UI를 컴포넌트 계층으로 쪼개기 {/*step-1-break-the-ui-into-a-component-hierarchy*/}\n\n먼저 모의 시안에 있는 모든 컴포넌트와 하위 컴포넌트 주변에 박스를 그리고 그들에게 이름을 붙이면서 시작해 보세요. 디자이너와 함께 일한다면 그들이 이미 디자인 툴을 통하여 이 컴포넌트들에 이름을 정해두었을 수도 있습니다. 한번 여쭤보세요!\n\n어떤 배경을 가지고 있냐에 따라, 디자인을 컴포넌트로 나누는 방법에 대한 관점이 달라질 수 있습니다.\n\n* **Programming**--새로운 함수나 객체를 만드는 방식으로 해봅시다. 이 중 [단일 책임 원칙](https://ko.wikipedia.org/wiki/%EB%8B%A8%EC%9D%BC_%EC%B1%85%EC%9E%84_%EC%9B%90%EC%B9%99)을 반영하고자 한다면 컴포넌트는 이상적으로는 한 번에 한 가지 일만 해야 합니다. 만약 컴포넌트가 점점 커진다면 작은 하위 컴포넌트로 쪼개져야 하겠죠.\n* **CSS**--클래스 선택자를 무엇으로 만들지 생각해 봅시다. (실제 컴포넌트들은 약간 더 세분되어 있습니다.)\n* **Design**--디자인 계층을 어떤 식으로 구성할 지 생각해 봅시다.\n\nJSON이 잘 구조화되어 있다면, 종종 이것이 UI의 컴포넌트 구조가 자연스럽게 데이터 모델에 대응된다는 것을 발견할 수 있습니다. 이는 UI와 데이터 모델은 보통 같은 정보 아키텍처, 즉 같은 구조를 가지기 때문입니다. UI를 컴포넌트로 분리하고, 각 컴포넌트가 데이터 모델에 매칭될 수 있도록 하세요.\n\n여기 다섯 개의 컴포넌트가 있습니다.\n\n<FullWidth>\n\n<CodeDiagram flip>\n\n<img src=\"/images/docs/s_thinking-in-react_ui_outline.png\" width=\"500\" style={{margin: '0 auto'}} />\n\n1. `FilterableProductTable`(회색): 예시 전체를 포괄합니다.\n2. `SearchBar`(파란색): 사용자의 입력을 받습니다.\n3. `ProductTable`(라벤더색): 데이터 리스트를 보여주고, 사용자의 입력을 기반으로 필터링합니다.\n4. `ProductCategoryRow`(초록색): 각 카테고리의 헤더를 보여줍니다.\n5. `ProductRow`(노란색): 각각의 제품에 해당하는 행을 보여줍니다.\n\n</CodeDiagram>\n\n</FullWidth>\n\n`ProductTable`을 보면 \"Name\"과 \"Price\" 레이블을 포함한 테이블 헤더 기능만을 가진 컴포넌트는 없습니다. 독립된 컴포넌트를 따로 생성할 지 생성하지 않을지는 당신의 선택입니다. 이 예시에서는 `ProductTable`의 위의 단순한 헤더들이 `ProductTable`의 일부이기 때문에 위 레이블들을 컴포넌트로 만들지 않고 그냥 남겨두었습니다. 그러나 이 헤더가 복잡해지면 (즉 정렬을 위한 기능을 추가하는 등) `ProductTableHeader` 컴포넌트를 만드는 것이 더 합리적일 것입니다.\n\n이제 모의 시안 내의 컴포넌트들을 확인했으니, 이들을 계층 구조로 정리해 봅시다. 모의 시안에서 한 컴포넌트 내에 있는 다른 컴포넌트는 계층 구조에서 자식으로 표현됩니다.\n\n* `FilterableProductTable`\n  * `SearchBar`\n  * `ProductTable`\n    * `ProductCategoryRow`\n    * `ProductRow`\n\n## Step 2: React로 정적인 버전 구현하기 {/*step-2-build-a-static-version-in-react*/}\n\n이제 컴포넌트 계층구조를 만들었으니, 앱을 실제로 구현해 볼 시간입니다. 가장 쉬운 접근 방법은 상호작용 기능은 아직 추가하지 않고 데이터 모델로부터 UI를 렌더링하는 버전을 만드는 것입니다. 대체로 먼저 정적인 버전을 만들고 상호작용 기능을 추가하는 게 더 쉽습니다. 정적 버전을 만드는 것은 많은 타이핑이 필요하지만, 생각할 것은 적으며, 반대로 상호작용 기능을 추가하는 것은 많은 생각이 필요하지만, 타이핑은 그리 많이 필요하지 않습니다.\n\n데이터 모델을 렌더링하는 앱의 정적인 버전을 만들기 위해 다른 컴포넌트를 재사용하고 [Props](/learn/passing-props-to-a-component)를 이용하여 데이터를 넘겨주는 [컴포넌트](/learn/your-first-component)를 구현할 수 있습니다. Props는 부모가 자식에게 데이터를 넘겨줄 때 사용할 수 있는 방법입니다. (혹시 [State](/learn/state-a-components-memory) 개념에 익숙하다고 해도 정적인 버전을 만드는 데는 State를 쓰지 마세요! State는 오직 상호작용을 위해, 즉 시간이 지남에 따라 데이터가 바뀌는 것에 사용합니다. 우리는 앱의 정적 버전을 만들고 있기 때문에 지금은 필요하지 않습니다.)\n\n앱을 만들 때 계층 구조에 따라 상층부에 있는 컴포넌트 (즉, `FilterableProductTable`부터 시작하는 것)부터 하향식<sup>Top-Down</sup>으로 만들거나 혹은 하층부에 있는 컴포넌트 (`ProductRow`)부터 상향식<sup>Bottom-Up</sup>으로 만들 수 있습니다. 간단한 예시에서는 보통 하향식으로 만드는 게 쉽지만, 프로젝트가 커지면 상향식으로 만들고 테스트를 작성하면서 개발하는 것이 더 쉽습니다.\n\n<Sandpack>\n\n```jsx src/App.js\nfunction ProductCategoryRow({ category }) {\n  return (\n    <tr>\n      <th colSpan=\"2\">\n        {category}\n      </th>\n    </tr>\n  );\n}\n\nfunction ProductRow({ product }) {\n  const name = product.stocked ? product.name :\n    <span style={{ color: 'red' }}>\n      {product.name}\n    </span>;\n\n  return (\n    <tr>\n      <td>{name}</td>\n      <td>{product.price}</td>\n    </tr>\n  );\n}\n\nfunction ProductTable({ products }) {\n  const rows = [];\n  let lastCategory = null;\n\n  products.forEach((product) => {\n    if (product.category !== lastCategory) {\n      rows.push(\n        <ProductCategoryRow\n          category={product.category}\n          key={product.category} />\n      );\n    }\n    rows.push(\n      <ProductRow\n        product={product}\n        key={product.name} />\n    );\n    lastCategory = product.category;\n  });\n\n  return (\n    <table>\n      <thead>\n        <tr>\n          <th>Name</th>\n          <th>Price</th>\n        </tr>\n      </thead>\n      <tbody>{rows}</tbody>\n    </table>\n  );\n}\n\nfunction SearchBar() {\n  return (\n    <form>\n      <input type=\"text\" placeholder=\"Search...\" />\n      <label>\n        <input type=\"checkbox\" />\n        {' '}\n        Only show products in stock\n      </label>\n    </form>\n  );\n}\n\nfunction FilterableProductTable({ products }) {\n  return (\n    <div>\n      <SearchBar />\n      <ProductTable products={products} />\n    </div>\n  );\n}\n\nconst PRODUCTS = [\n  {category: \"Fruits\", price: \"$1\", stocked: true, name: \"Apple\"},\n  {category: \"Fruits\", price: \"$1\", stocked: true, name: \"Dragonfruit\"},\n  {category: \"Fruits\", price: \"$2\", stocked: false, name: \"Passionfruit\"},\n  {category: \"Vegetables\", price: \"$2\", stocked: true, name: \"Spinach\"},\n  {category: \"Vegetables\", price: \"$4\", stocked: false, name: \"Pumpkin\"},\n  {category: \"Vegetables\", price: \"$1\", stocked: true, name: \"Peas\"}\n];\n\nexport default function App() {\n  return <FilterableProductTable products={PRODUCTS} />;\n}\n```\n\n```css\nbody {\n  padding: 5px;\n}\nlabel {\n  display: block;\n  margin-top: 5px;\n  margin-bottom: 5px;\n}\nth {\n  padding-top: 10px;\n}\ntd {\n  padding: 2px;\n  padding-right: 40px;\n}\n```\n\n</Sandpack>\n\n(위 코드가 어렵게 느껴진다면, [빠르게 시작하기](/learn)를 먼저 참고하세요!)\n\n이 단계가 끝나면 데이터 렌더링을 위해 만들어진 재사용 가능한 컴포넌트들의 라이브러리를 가지게 됩니다. 현재는 앱의 정적 버전이기 때문에 컴포넌트는 단순히 JSX만 리턴합니다. 계층구조의 최상단 컴포넌트 `FilterableProductTable`은 Prop으로 데이터 모델을 받습니다. 이는 데이터가 최상단 컴포넌트부터 트리의 맨 아래까지 흘러가기 때문에 <em>단방향 데이터 흐름</em>이라고 부릅니다.\n\n<Pitfall>\n\n여기까지는 아직 State 값을 쓰지 마세요. 다음 단계에서 사용할 겁니다!\n\n</Pitfall>\n\n## Step 3: 최소한의 데이터만 이용해서 완벽하게 UI State 표현하기 {/*step-3-find-the-minimal-but-complete-representation-of-ui-state*/}\n\nUI를 상호작용<sup>Interactive</sup>하게 만들려면 사용자가 기반 데이터 모델을 변경할 수 있게 해야 합니다. React는 *State*를 통해 기반 데이터 모델을 변경할 수 있게 합니다.\n\nState는 앱이 기억해야 하는, 변경할 수 있는 데이터의 최소 집합이라고 생각하세요. State를 구조화하는 데 가장 중요한 원칙은 [중복 배제 원칙<sup>Don't Repeat Yourself</sup>](https://ko.wikipedia.org/wiki/%EC%A4%91%EB%B3%B5%EB%B0%B0%EC%A0%9C)입니다. 애플리케이션이 필요로 하는 가장 최소한의 State를 파악하고 나머지 모든 것들은 필요에 따라 실시간으로 계산하세요. 예를 들어, 쇼핑 리스트를 만든다고 하면 당신은 배열에 상품 아이템들을 넣을 겁니다. UI에 상품 아이템의 개수를 노출하고 싶다고 하면 상품 아이템 개수를 따로 State 값으로 가지는 게 아니라 단순하게 배열의 길이만 쓰면 됩니다.\n\n예시 애플리케이션 내 데이터들을 생각해 봅시다. 애플리케이션은 다음과 같은 데이터를 가지고 있습니다.\n\n1. 제품의 원본 목록\n2. 사용자가 입력한 검색어\n3. 체크박스의 값\n4. 필터링된 제품 목록\n\n이 중 어떤 게 State가 되어야 할까요? 아래의 세 가지 질문을 통해 결정할 수 있습니다.\n\n- **시간이 지나도 변하지 않나요?** 그러면 확실히 State가 아닙니다.\n- **부모로부터 Props를 통해 전달됩니까?** 그러면 확실히 State가 아닙니다.\n- 컴포넌트 안의 다른 State나 Props를 가지고 **계산 가능한가요?** 그렇다면 *절대로* State가 아닙니다!\n\n그 외 남는 건 아마 State일 겁니다.\n\n위 데이터들을 다시 한번 순서대로 살펴봅시다.\n\n1. 제품의 원본 목록은 **Props로 전달되었기 때문에 State가 아닙니다**.\n2. 사용자가 입력한 검색어는 시간에 따라 변하고, 다른 요소로부터 계산할 수 없기 때문에 State로 볼 수 있습니다.\n3. 체크박스의 값은 시간에 따라 바뀌고 다른 요소로부터 계산할 수 없기 때문에 State로 볼 수 있습니다.\n4. 필터링된 제품 목록은 원본 제품 목록을 받아서 검색어와 체크박스의 값에 따라 **계산할 수 있으므로, 이는 State가 아닙니다.**\n\n따라서, 검색어와 체크박스의 값만이 State입니다! 잘하셨습니다!\n\n<DeepDive>\n\n#### Props vs State {/*props-vs-state*/}\n\nReact는 Props와 State라는 두 개의 데이터 \"모델\"이 존재합니다. 둘의 성격은 매우 다릅니다.\n\n- [**Props**는 함수를 통해 전달되는 인자 같은 성격을 가집니다.](/learn/passing-props-to-a-component) Props는 부모 컴포넌트로부터 자식 컴포넌트로 데이터를 넘겨서 외관을 커스터마이징하게 해줍니다. 예를 들어, `Form`은 `color`라는 Prop을 `Button`으로 보내서 `Button`을 내가 원하는 형태로 커스터마이징할 수 있습니다.\n- [**State**는 컴포넌트의 메모리 같은 성격을 가집니다.](/learn/state-a-components-memory) State는 컴포넌트가 몇몇 정보를 계속 따라갈 수 있게 해주고 변화하면서 상호작용<sup>Interaction</sup>을 만들어 냅니다. 예를 들어, `Button`은 `isHovered`라는 State를 따라갈 것입니다.\n\nProps와 State는 다르지만, 함께 동작합니다. State는 보통 부모 컴포넌트에 저장합니다. (그래서 부모 컴포넌트는 그 State를 변경할 수 있습니다.) 그리고 부모 컴포넌트는 State를 자식 컴포넌트에 Props로서 전달합니다. 처음 봤을 때 둘의 차이를 잘 알기 어려워도 괜찮습니다. 약간 연습이 필요할 거예요!\n\n</DeepDive>\n\n## Step 4: State가 어디에 있어야 할 지 정하기 {/*step-4-identify-where-your-state-should-live*/}\n\n이제 앱에서 필요한 최소한의 State를 결정했습니다. 다음으로는 어떤 컴포넌트가 이 State를 소유하고, 변경할 책임을 지게 할 지 정해야 합니다. React는 항상 컴포넌트 계층구조를 따라 부모에서 자식으로 데이터를 전달하는 단방향 데이터 흐름을 사용하는 것을 기억하세요! 앱을 구현하면서 어떤 컴포넌트가 State를 가져야 하는 지 명확하지 않을 수 있습니다. 이 개념이 처음이라면 더 어려울 수 있습니다. 그러나 아래의 과정을 따라가면 해결할 수 있습니다.\n\n애플리케이션의 각 State에 대해서,\n\n1. 해당 State를 기반으로 렌더링하는 모든 컴포넌트를 찾으세요.\n2. 그들의 가장 가까운 공통되는 부모 컴포넌트를 찾으세요. 계층에서 모두를 포괄하는 상위 컴포넌트입니다.\n3. State가 어디에 위치해야 하는지 결정합니다.\n   1. 대개, 공통 부모에 State를 그냥 두면 됩니다.\n   2. 혹은, 공통 부모 상위 컴포넌트에 둬도 됩니다.\n   3. State를 소유할 적절한 컴포넌트를 찾지 못했다면, State를 소유하는 컴포넌트를 하나 만들어서 상위 계층에 추가하세요.\n\n이전 단계에서, 이 애플리케이션의 두 가지 State인 사용자의 검색어 입력과 체크 박스의 값을 발견하였습니다. 이 예시에서 그들은 항상 함께 나타나기 때문에 같은 위치에 두는 것이 합리적입니다.\n\n이제 이 전략을 애플리케이션에 적용해 봅시다.\n\n1. **State를 쓰는 컴포넌트를 찾아봅시다**:\n   - `ProductTable`은 State에 기반한 상품 리스트를 필터링해야 합니다. (검색어와 체크 박스의 값)\n   - `SearchBar`는 State를 표시해 주어야 합니다. (검색어와 체크 박스의 값)\n2. **공통 부모를 찾아봅시다**: 둘 모두가 공유하는 첫 번째 부모는 `FilterableProductTable`입니다.\n3. **어디에 State가 존재해야 할지 정해봅시다**: 우리는`FilterableProductTable`에 검색어와 체크 박스 값을 State로 둘 겁니다.\n\n이제 State 값은 `FilterableProductTable` 안에 있습니다.\n\n[`useState()` Hook](/reference/react/useState)을 이용해서 State를 컴포넌트에 추가하세요. Hook은 React 기능에 \"연결할 수<sup>Hook Into</sup>\" 있게 해주는 특별한 함수입니다. `FilterableProductTable`의 상단에 두 개의 State 변수를 추가해서 초깃값을 명확하게 보여주세요.\n\n```js\nfunction FilterableProductTable({ products }) {\n  const [filterText, setFilterText] = useState('');\n  const [inStockOnly, setInStockOnly] = useState(false);\n```\n\n다음으로, `filterText`와 `inStockOnly`를 `ProductTable`와 `SearchBar`에게 Props로 전달하세요.\n\n```js\n<div>\n  <SearchBar\n    filterText={filterText}\n    inStockOnly={inStockOnly} />\n  <ProductTable\n    products={products}\n    filterText={filterText}\n    inStockOnly={inStockOnly} />\n</div>\n```\n\n이제 애플리케이션이 어떻게 동작하는지 알 수 있습니다. `filterText`의 초깃값을 `useState('')`에서 `useState('fruit')`로 수정해 보세요. 검색창과 데이터 테이블이 모두 업데이트됨을 확인할 수 있습니다.\n\n<Sandpack>\n\n```jsx src/App.js\nimport { useState } from 'react';\n\nfunction FilterableProductTable({ products }) {\n  const [filterText, setFilterText] = useState('');\n  const [inStockOnly, setInStockOnly] = useState(false);\n  return (\n    <div>\n      <SearchBar\n        filterText={filterText}\n        inStockOnly={inStockOnly} />\n      <ProductTable\n        products={products}\n        filterText={filterText}\n        inStockOnly={inStockOnly} />\n    </div>\n  );\n}\nfunction ProductCategoryRow({ category }) {\n  return (\n    <tr>\n      <th colSpan=\"2\">\n        {category}\n      </th>\n    </tr>\n  );\n}\nfunction ProductRow({ product }) {\n  const name = product.stocked ? product.name :\n    <span style={{ color: 'red' }}>\n      {product.name}\n    </span>;\n\n  return (\n    <tr>\n      <td>{name}</td>\n      <td>{product.price}</td>\n    </tr>\n  );\n}\n\nfunction ProductTable({ products, filterText, inStockOnly }) {\n  const rows = [];\n  let lastCategory = null;\n\n  products.forEach((product) => {\n    if (\n      product.name.toLowerCase().indexOf(\n        filterText.toLowerCase()\n      ) === -1\n    ) {\n      return;\n    }\n    if (inStockOnly && !product.stocked) {\n      return;\n    }\n    if (product.category !== lastCategory) {\n      rows.push(\n        <ProductCategoryRow\n          category={product.category}\n          key={product.category} />\n      );\n    }\n    rows.push(\n      <ProductRow\n        product={product}\n        key={product.name} />\n    );\n    lastCategory = product.category;\n  });\n\n  return (\n    <table>\n      <thead>\n        <tr>\n          <th>Name</th>\n          <th>Price</th>\n        </tr>\n      </thead>\n      <tbody>{rows}</tbody>\n    </table>\n  );\n}\n\nfunction SearchBar({ filterText, inStockOnly }) {\n  return (\n    <form>\n      <input\n        type=\"text\"\n        value={filterText}\n        placeholder=\"Search...\"/>\n      <label>\n        <input\n          type=\"checkbox\"\n          checked={inStockOnly} />\n        {' '}\n        Only show products in stock\n      </label>\n    </form>\n  );\n}\n\nconst PRODUCTS = [\n  {category: \"Fruits\", price: \"$1\", stocked: true, name: \"Apple\"},\n  {category: \"Fruits\", price: \"$1\", stocked: true, name: \"Dragonfruit\"},\n  {category: \"Fruits\", price: \"$2\", stocked: false, name: \"Passionfruit\"},\n  {category: \"Vegetables\", price: \"$2\", stocked: true, name: \"Spinach\"},\n  {category: \"Vegetables\", price: \"$4\", stocked: false, name: \"Pumpkin\"},\n  {category: \"Vegetables\", price: \"$1\", stocked: true, name: \"Peas\"}\n];\n\nexport default function App() {\n  return <FilterableProductTable products={PRODUCTS} />;\n}\n```\n\n```css\nbody {\n  padding: 5px;\n}\nlabel {\n  display: block;\n  margin-top: 5px;\n  margin-bottom: 5px;\n}\nth {\n  padding-top: 5px;\n}\ntd {\n  padding: 2px;\n}\n```\n\n</Sandpack>\n\n아직 폼을 수정하는 작업이 작동하지 않음에 유의하세요. 위 샌드박스에서 콘솔 에러가 발생하며, 그 이유를 설명하겠습니다.\n\n<ConsoleBlock level=\"error\">\n\nYou provided a \\`value\\` prop to a form field without an \\`onChange\\` handler. This will render a read-only field.\n\n</ConsoleBlock>\n\n위에 있는 샌드박스를 보면, `ProductTable`과 `SearchBar`가 `filterText`와 `inStockOnly` Props를 표<sup>Table</sup>, 입력창<sup>Input</sup>, 체크 박스를 렌더링하기 위해서 읽고 있습니다. 예를 들면, `SearchBar` 입력창의 `value`를 아래와 같이 채우고 있습니다.\n\n```js {1,6}\nfunction SearchBar({ filterText, inStockOnly }) {\n  return (\n    <form>\n      <input\n        type=\"text\"\n        value={filterText}\n        placeholder=\"Search...\"/>\n```\n\n그러나 아직 사용자의 키보드 입력과 같은 행동에 반응하는 코드는 작성하지 않았습니다. 이 과정은 마지막 단계에서 진행할 예정입니다.\n\n## Step 5: 역 데이터 흐름 추가하기 {/*step-5-add-inverse-data-flow*/}\n\n지금까지 우리는 계층 구조 아래로 흐르는 Props와 State의 함수로써 앱을 만들었습니다. 이제 사용자 입력에 따라 State를 변경하려면 반대 방향의 데이터 흐름을 만들어야 합니다. 이를 위해서는 계층 구조의 하단에 있는 컴포넌트에서 `FilterableProductTable`의 State를 업데이트할 수 있어야 합니다.\n\nReact는 데이터 흐름을 명시적으로 보이게 만들어 줍니다. 그러나 이는 전통적인 양방향 데이터 바인딩보다 조금 더 많은 타이핑이 필요합니다.\n\n4단계의 예시에서 체크하거나 키보드를 타이핑할 경우 UI의 변화가 없고 입력을 무시하는 것을 확인할 수 있습니다. 이건 의도적으로 `<input value={filterText} />`로 코드를 쓰면서 `value`라는 Prop이 항상`FilterableProductTable`의 `filterText`라는 State를 통해서 데이터를 받도록 정했기 때문입니다. `filterText`라는 State가 변경되는 게 아니기 때문에 input의 `value`는 변하지 않고 화면도 바뀌는 게 없습니다.\n\n우리는 사용자가 input을 변경할 때마다 사용자의 입력을 반영할 수 있도록 State를 업데이트하기를 원합니다. State는 `FilterableProductTable`이 가지고 있고 State 변경을 위해서는 `setFilterText`와 `setInStockOnly`를 호출을 하면 됩니다. `SearchBar`가 `FilterableProductTable`의 State를 업데이트할 수 있도록 하려면, 이 함수들을 `SearchBar`로 전달해야 합니다.\n\n```js {2,3,10,11}\nfunction FilterableProductTable({ products }) {\n  const [filterText, setFilterText] = useState('');\n  const [inStockOnly, setInStockOnly] = useState(false);\n\n  return (\n    <div>\n      <SearchBar\n        filterText={filterText}\n        inStockOnly={inStockOnly}\n        onFilterTextChange={setFilterText}\n        onInStockOnlyChange={setInStockOnly} />\n```\n\n`SearchBar`에서 `onChange` 이벤트 핸들러를 추가하여 부모 State를 변경할 수 있도록 구현할 수 있습니다.\n\n```js {4,5,13,19}\nfunction SearchBar({\n  filterText,\n  inStockOnly,\n  onFilterTextChange,\n  onInStockOnlyChange\n}) {\n  return (\n    <form>\n      <input\n        type=\"text\"\n        value={filterText}\n        placeholder=\"Search...\"\n        onChange={(e) => onFilterTextChange(e.target.value)}\n      />\n      <label>\n        <input\n          type=\"checkbox\"\n          checked={inStockOnly}\n          onChange={(e) => onInStockOnlyChange(e.target.checked)}\n```\n\n이제 애플리케이션이 완전히 동작합니다!\n\n<Sandpack>\n\n```jsx src/App.js\nimport { useState } from 'react';\n\nfunction FilterableProductTable({ products }) {\n  const [filterText, setFilterText] = useState('');\n  const [inStockOnly, setInStockOnly] = useState(false);\n\n  return (\n    <div>\n      <SearchBar\n        filterText={filterText}\n        inStockOnly={inStockOnly}\n        onFilterTextChange={setFilterText}\n        onInStockOnlyChange={setInStockOnly} />\n      <ProductTable\n        products={products}\n        filterText={filterText}\n        inStockOnly={inStockOnly} />\n    </div>\n  );\n}\n\nfunction ProductCategoryRow({ category }) {\n  return (\n    <tr>\n      <th colSpan=\"2\">\n        {category}\n      </th>\n    </tr>\n  );\n}\n\nfunction ProductRow({ product }) {\n  const name = product.stocked ? product.name :\n    <span style={{ color: 'red' }}>\n      {product.name}\n    </span>;\n\n  return (\n    <tr>\n      <td>{name}</td>\n      <td>{product.price}</td>\n    </tr>\n  );\n}\n\nfunction ProductTable({ products, filterText, inStockOnly }) {\n  const rows = [];\n  let lastCategory = null;\n\n  products.forEach((product) => {\n    if (\n      product.name.toLowerCase().indexOf(\n        filterText.toLowerCase()\n      ) === -1\n    ) {\n      return;\n    }\n    if (inStockOnly && !product.stocked) {\n      return;\n    }\n    if (product.category !== lastCategory) {\n      rows.push(\n        <ProductCategoryRow\n          category={product.category}\n          key={product.category} />\n      );\n    }\n    rows.push(\n      <ProductRow\n        product={product}\n        key={product.name} />\n    );\n    lastCategory = product.category;\n  });\n\n  return (\n    <table>\n      <thead>\n        <tr>\n          <th>Name</th>\n          <th>Price</th>\n        </tr>\n      </thead>\n      <tbody>{rows}</tbody>\n    </table>\n  );\n}\n\nfunction SearchBar({\n  filterText,\n  inStockOnly,\n  onFilterTextChange,\n  onInStockOnlyChange\n}) {\n  return (\n    <form>\n      <input\n        type=\"text\"\n        value={filterText} placeholder=\"Search...\"\n        onChange={(e) => onFilterTextChange(e.target.value)} />\n      <label>\n         <input\n          type=\"checkbox\"\n          checked={inStockOnly}\n          onChange={(e) => onInStockOnlyChange(e.target.checked)} />\n        {' '}\n        Only show products in stock\n      </label>\n    </form>\n  );\n}\n\nconst PRODUCTS = [\n  {category: \"Fruits\", price: \"$1\", stocked: true, name: \"Apple\"},\n  {category: \"Fruits\", price: \"$1\", stocked: true, name: \"Dragonfruit\"},\n  {category: \"Fruits\", price: \"$2\", stocked: false, name: \"Passionfruit\"},\n  {category: \"Vegetables\", price: \"$2\", stocked: true, name: \"Spinach\"},\n  {category: \"Vegetables\", price: \"$4\", stocked: false, name: \"Pumpkin\"},\n  {category: \"Vegetables\", price: \"$1\", stocked: true, name: \"Peas\"}\n];\n\nexport default function App() {\n  return <FilterableProductTable products={PRODUCTS} />;\n}\n```\n\n```css\nbody {\n  padding: 5px;\n}\nlabel {\n  display: block;\n  margin-top: 5px;\n  margin-bottom: 5px;\n}\nth {\n  padding: 4px;\n}\ntd {\n  padding: 2px;\n}\n```\n\n</Sandpack>\n\n[상호작용 추가하기](/learn/adding-interactivity) 섹션에서 State를 변경하고 이벤트를 다루는 것에 대해 더 깊이있게 배울 수 있습니다.\n\n## 더 나아가기 {/*where-to-go-from-here*/}\n\n지금까지는 React를 이용해서 컴포넌트와 앱을 만들려고 할 때 어떻게 사고할지에 대해 간단히 소개했습니다. [당장 React로 프로젝트를 시작](/learn/installation)해도 좋고 다음 단계로 넘어가서 이 [자습서를 이용해서 좀 더 심화](/learn/describing-the-ui) 학습해도 좋습니다.\n"
  },
  {
    "path": "src/content/learn/tutorial-tic-tac-toe.md",
    "content": "---\ntitle: '자습서: 틱택토 게임'\n---\n\n<Intro>\n\n이 자습서에서는 작은 틱택토 게임을 만들어 볼 것입니다. 이 자습서는 현재 사용되는 React 지식을 전제로 하지 않습니다. 이 자습서에서 배우게 될 기술은 모든 React 앱을 만드는데 기본이 되는 기술이며 이 기술을 완전히 이해하면 React에 대해 깊게 이해할 수 있습니다.\n\n</Intro>\n\n<Note>\n\n이 자습서는 **직접 해보면서 배우는 것**을 선호하고, 빠르게 무언가를 만들어 보고 싶은 분들을 위해 설계되었습니다.\n각 개념을 단계별로 학습하는 것을 선호한다면 [UI 표현하기](/learn/describing-the-ui)부터 시작하세요.\n\n</Note>\n\n자습서는 아래와 같이 몇 가지 부문으로 나뉩니다.\n\n- [자습서 환경설정](#setup-for-the-tutorial)은 자습서를 따를 수 있는 **시작점**을 제공합니다.\n- [개요](#overview)에서는 React의 **핵심**(컴포넌트, Props, State)을 배울 수 있습니다.\n- [게임 완료하기](#completing-the-game)에서는 React 개발에서 **가장 흔히 쓰이는 기술**을 배울 수 있습니다.\n- [시간여행 추가하기](#adding-time-travel)에서는 React의 고유한 강점에 대해 **더 깊은 통찰력**을 얻을 수 있습니다.\n\n### 무엇을 만들까요? {/*what-are-you-building*/}\n\n이 자습서에서는 React로 상호작용하는 틱택토 게임을 만들어 볼 것입니다.\n\n완성하면 어떤 모습인지 아래에서 확인해 보세요.\n\n<Sandpack>\n\n```js src/App.js\nimport { useState } from 'react';\n\nfunction Square({ value, onSquareClick }) {\n  return (\n    <button className=\"square\" onClick={onSquareClick}>\n      {value}\n    </button>\n  );\n}\n\nfunction Board({ xIsNext, squares, onPlay }) {\n  function handleClick(i) {\n    if (calculateWinner(squares) || squares[i]) {\n      return;\n    }\n    const nextSquares = squares.slice();\n    if (xIsNext) {\n      nextSquares[i] = 'X';\n    } else {\n      nextSquares[i] = 'O';\n    }\n    onPlay(nextSquares);\n  }\n\n  const winner = calculateWinner(squares);\n  let status;\n  if (winner) {\n    status = 'Winner: ' + winner;\n  } else {\n    status = 'Next player: ' + (xIsNext ? 'X' : 'O');\n  }\n\n  return (\n    <>\n      <div className=\"status\">{status}</div>\n      <div className=\"board-row\">\n        <Square value={squares[0]} onSquareClick={() => handleClick(0)} />\n        <Square value={squares[1]} onSquareClick={() => handleClick(1)} />\n        <Square value={squares[2]} onSquareClick={() => handleClick(2)} />\n      </div>\n      <div className=\"board-row\">\n        <Square value={squares[3]} onSquareClick={() => handleClick(3)} />\n        <Square value={squares[4]} onSquareClick={() => handleClick(4)} />\n        <Square value={squares[5]} onSquareClick={() => handleClick(5)} />\n      </div>\n      <div className=\"board-row\">\n        <Square value={squares[6]} onSquareClick={() => handleClick(6)} />\n        <Square value={squares[7]} onSquareClick={() => handleClick(7)} />\n        <Square value={squares[8]} onSquareClick={() => handleClick(8)} />\n      </div>\n    </>\n  );\n}\n\nexport default function Game() {\n  const [history, setHistory] = useState(() => [Array(9).fill(null)]);\n  const [currentMove, setCurrentMove] = useState(0);\n  const xIsNext = currentMove % 2 === 0;\n  const currentSquares = history[currentMove];\n\n  function handlePlay(nextSquares) {\n    const nextHistory = [...history.slice(0, currentMove + 1), nextSquares];\n    setHistory(nextHistory);\n    setCurrentMove(nextHistory.length - 1);\n  }\n\n  function jumpTo(nextMove) {\n    if (currentMove === nextMove) {\n      return;\n    }\n\n    setCurrentMove(nextMove);\n  }\n\n  const moves = history.map((squares, move) => {\n    let description;\n    if (move > 0) {\n      description = 'Go to move #' + move;\n    } else {\n      description = 'Go to game start';\n    }\n    return (\n      <li key={move}>\n        <button onClick={() => jumpTo(move)}>{description}</button>\n      </li>\n    );\n  });\n\n  return (\n    <div className=\"game\">\n      <div className=\"game-board\">\n        <Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />\n      </div>\n      <div className=\"game-info\">\n        <ol>{moves}</ol>\n      </div>\n    </div>\n  );\n}\n\nfunction calculateWinner(squares) {\n  const lines = [\n    [0, 1, 2],\n    [3, 4, 5],\n    [6, 7, 8],\n    [0, 3, 6],\n    [1, 4, 7],\n    [2, 5, 8],\n    [0, 4, 8],\n    [2, 4, 6],\n  ];\n  for (let i = 0; i < lines.length; i++) {\n    const [a, b, c] = lines[i];\n    if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {\n      return squares[a];\n    }\n  }\n  return null;\n}\n```\n\n```css src/styles.css\n* {\n  box-sizing: border-box;\n}\n\nbody {\n  font-family: sans-serif;\n  margin: 20px;\n  padding: 0;\n}\n\n.square {\n  background: #fff;\n  border: 1px solid #999;\n  float: left;\n  font-size: 24px;\n  font-weight: bold;\n  line-height: 34px;\n  height: 34px;\n  margin-right: -1px;\n  margin-top: -1px;\n  padding: 0;\n  text-align: center;\n  width: 34px;\n}\n\n.board-row:after {\n  clear: both;\n  content: '';\n  display: table;\n}\n\n.status {\n  margin-bottom: 10px;\n}\n.game {\n  display: flex;\n  flex-direction: row;\n}\n\n.game-info {\n  margin-left: 20px;\n}\n```\n\n</Sandpack>\n\n코드가 아직 이해되지 않거나 문법이 익숙하지 않더라도 걱정하지 마세요! 이 자습서의 목표는 React와 그 문법을 이해하는 데 도움을 주는 것입니다.\n\n자습서를 계속하기 전에 위의 틱택토 게임을 확인하세요. 눈에 띄는 기능 중 하나는 보드 오른쪽의 번호가 있는 목록입니다. 이 목록은 게임에서 발생한 모든 움직임의 기록을 제공하며 게임이 진행됨에 따라 업데이트됩니다.\n\n완성된 틱택토 게임을 플레이해 보셨다면 계속 진행하세요. 자습서는 더 간단한 템플릿에서 시작할 것입니다. 다음 단계는 게임 만들기를 시작하기 위한 설정을 다룹니다.\n\n## 자습서 환경설정 {/*setup-for-the-tutorial*/}\n\n아래의 실시간 코드 편집기에서 오른쪽 위의 **포크<sup>Fork</sup>** 버튼을 클릭하여 새 탭에서 CodeSandBox 편집기를 열어주세요. CodeSandBox를 사용하면 브라우저에서 코드를 작성할 수 있으며 사용자가 만든 앱이 어떻게 보이는지 즉시 확인할 수 있습니다. 새 탭에는 텅 빈 사각형과 이 자습서의 시작 코드가 표시되어야 합니다.\n\n<Sandpack>\n\n```js src/App.js\nexport default function Square() {\n  return <button className=\"square\">X</button>;\n}\n```\n\n```css src/styles.css\n* {\n  box-sizing: border-box;\n}\n\nbody {\n  font-family: sans-serif;\n  margin: 20px;\n  padding: 0;\n}\n\n.square {\n  background: #fff;\n  border: 1px solid #999;\n  float: left;\n  font-size: 24px;\n  font-weight: bold;\n  line-height: 34px;\n  height: 34px;\n  margin-right: -1px;\n  margin-top: -1px;\n  padding: 0;\n  text-align: center;\n  width: 34px;\n}\n\n.board-row:after {\n  clear: both;\n  content: '';\n  display: table;\n}\n\n.status {\n  margin-bottom: 10px;\n}\n.game {\n  display: flex;\n  flex-direction: row;\n}\n\n.game-info {\n  margin-left: 20px;\n}\n```\n\n</Sandpack>\n\n<Note>\n\n로컬 개발 환경을 사용하여 이 자습서를 진행할 수도 있습니다. 로컬 환경에서 진행을 원한다면 아래와 같이 수행하세요.\n\n1. [Node.js](https://nodejs.org/ko/)를 설치하세요.\n1. 우측 상단의 **다운로드** 버튼, 또는 앞서 열었던 CodeSandBox 탭에서 왼쪽 위 모서리의 버튼을 누르고 메뉴를 열어 **Download Sandbox**(혹은 **File -> Export to ZIP**)를 선택하여 보관된 파일을 로컬로 내려받으세요.\n1. 파일의 압축을 풀고, 터미널을 열어 `cd` 명령어로 압축을 푼 디렉터리로 이동하세요.\n1. `npm install` 명령어를 실행하여 의존성을 설치하세요.\n1. `npm start` 명령어를 실행하여 로컬 서버를 시작하고 프롬프트에 따라 브라우저에서 실행 중인 코드를 확인하세요.\n\n문제가 생기더라도 좌절하지 마세요! 로컬 환경 대신 온라인에서 자습서를 진행하시고 로컬 설정은 나중에 다시 시도하세요.\n\n</Note>\n\n## 개요 {/*overview*/}\n\n이제 환경 설정이 완료되었으니, React의 개요를 살펴보겠습니다!\n\n### 초기 코드 살펴보기 {/*inspecting-the-starter-code*/}\n\nCodeSandBox에는 세 가지 주요 구역이 있습니다.\n\n![CodeSandBox의 초기 코드](../images/tutorial/react-starter-code-codesandbox.png)\n\n1. `App.js`, `index.js`, `style.css` 와 같은 파일 목록과 `public` 폴더가 있는 _파일_ 구역\n1. 선택한 파일의 소스 코드를 볼 수 있는 _코드 편집기_\n1. 작성한 코드가 어떻게 보이는지 확인할 수 있는 _브라우저_ 구역\n\n_파일_ 구역에서 `App.js` 파일을 선택하세요. _코드 편집기_ 에서 해당 파일의 내용이 있어야 합니다.\n\n```jsx\nexport default function Square() {\n  return <button className=\"square\">X</button>;\n}\n```\n\n_브라우저_ 구역에 아래와 같이 X가 있는 사각형이 표시되어야 합니다.\n\n![x가 채워진 사각형](../images/tutorial/x-filled-square.png)\n\n이제 초기 코드의 파일을 살펴보겠습니다.\n\n#### `App.js` {/*appjs*/}\n\n`App.js`의 코드는 <em>컴포넌트</em>를 생성합니다. React에서 컴포넌트는 사용자 인터페이스 일부를 표시하는 재사용 가능한 코드 조각입니다. 컴포넌트는 애플리케이션의 UI 엘리먼트를 렌더링, 관리, 업데이트할 때 사용합니다. 컴포넌트를 한 줄씩 살펴보면서 무슨 일이 일어나는지 알아보겠습니다.\n\n```js {1}\nexport default function Square() {\n  return <button className=\"square\">X</button>;\n}\n```\n\n첫 번째 줄은 `Square` 함수를 정의합니다. 자바스크립트의 `export` 키워드는 이 함수를 파일 외부에서 접근할 수 있도록 만들어 줍니다. `default` 키워드는 코드를 사용하는 다른 파일에서 이 함수가 파일의 주요 함수임을 알려줍니다.\n\n```js {2}\nexport default function Square() {\n  return <button className=\"square\">X</button>;\n}\n```\n\n두 번째 줄은 버튼을 반환합니다. 자바스크립트의 `return` 키워드는 해당 키워드 뒤에 오는 모든 것이 함수 호출자에게 값으로 반환됨을 의미합니다. `<button>`은 *JSX 엘리먼트*입니다. JSX 엘리먼트는 자바스크립트 코드와 HTML 태그의 조합으로 표시할 내용을 설명합니다. `className=\"square\"`는 버튼 *Prop* 또는 프로퍼티로, CSS에 버튼의 스타일을 지정하는 방법을 알려줍니다. `X`는 버튼 내부에 표시되는 텍스트이며, `</button>`은 JSX 엘리먼트를 닫아 버튼 내부에 다음 콘텐츠를 배치해서는 안 됨을 나타냅니다.\n#### `styles.css` {/*stylescss*/}\n\nCodeSandBox의 _파일_ 구역에서 `styles.css` 파일을 여세요. 이 파일은 React 앱의 스타일을 정의합니다. 처음 두 개의 <em>CSS 선택자</em>인 `*`와 `body`는 앱 대부분의 스타일을 정의하고, `.square` 선택자는 `className` 프로퍼티가 `square`로 설정된 모든 컴포넌트의 스타일을 정의합니다. 초기 코드에서는 `App.js` 파일의 `Square` 컴포넌트의 버튼과 매치됩니다.\n\n#### `index.js` {/*indexjs*/}\n\nCodeSandBox의 _파일_ 구역에서 `index.js` 파일을 여세요. 자습서를 진행하는 중에는 이 파일을 편집하지 않지만, 이 파일은 `App.js` 파일에서 만든 컴포넌트와 웹 브라우저 사이의 다리 역할을 합니다.\n\n```jsx\nimport { StrictMode } from 'react';\nimport { createRoot } from 'react-dom/client';\nimport './styles.css';\n\nimport App from './App';\n```\n\n1~5줄은 아래에 있는 필요한 모든 코드를 한 곳으로 가져옵니다.\n\n* React\n* 웹 브라우저와 상호작용하는 React의 라이브러리 (React DOM)\n* 컴포넌트의 스타일\n* `App.js`에서 만든 컴포넌트\n\n파일의 나머지 코드는 모든 코드를 한데 모아 최종 결과물을 `public` 폴더의 `index.html`에 주입합니다.\n\n### 보드 만들기 {/*building-the-board*/}\n\n`App.js`로 돌아가서 자습서의 나머지 부분을 진행하겠습니다.\n\n현재 보드에는 사각형이 하나뿐이지만 게임을 진행하려면 9개가 필요합니다. 간단하게 사각형을 복사해서 붙여 넣어 보면 아래처럼 두 개의 사각형을 만들 수 있습니다.\n\n```js {2}\nexport default function Square() {\n  return <button className=\"square\">X</button><button className=\"square\">X</button>;\n}\n```\n\n하지만 다음과 같은 오류가 발생합니다.\n\n<ConsoleBlock level=\"error\">\n\n/src/App.js: Adjacent JSX elements must be wrapped in an enclosing tag. Did you want a JSX Fragment `<>...</>`?\n\n</ConsoleBlock>\n\nReact 컴포넌트는 두 개의 버튼처럼 인접한 여러 개의 JSX 엘리먼트가 아닌 단일 JSX 엘리먼트를 반환해야 합니다. 이 오류는 *Fragment*(`<>` 와 `</>`)를 사용하여 다음과 같이 여러 개의 인접한 JSX 엘리먼트를 감싸 해결할 수 있습니다.\n\n```js {3-6}\nexport default function Square() {\n  return (\n    <>\n      <button className=\"square\">X</button>\n      <button className=\"square\">X</button>\n    </>\n  );\n}\n```\n\n이제 사각형이 두 개가 되었습니다.\n\n![두개의 X가 채워진 사각형](../images/tutorial/two-x-filled-squares.png)\n\n훌륭합니다! 이제 간단히 복사-붙여넣기 몇 번만 하면 9개의 사각형을 만들 수 있습니다.\n\n![한 줄에 X가 채워진 9개의 사각형](../images/tutorial/nine-x-filled-squares.png)\n\n이런! 사각형이 보드에 필요한 격자 모양이 아니라 한 줄로 되어있습니다. 이 문제를 해결하려면 `div`를 사용하여 사각형을 행으로 그룹화하고 몇 가지 CSS 클래스를 추가해야 합니다. 이 과정에서 각 사각형에 번호를 부여하여 표시되는 위치를 알 수 있게 하겠습니다.\n\n`App.js` 파일에서 `Square` 컴포넌트를 다음과 같이 업데이트하세요.\n\n```js {3-19}\nexport default function Square() {\n  return (\n    <>\n      <div className=\"board-row\">\n        <button className=\"square\">1</button>\n        <button className=\"square\">2</button>\n        <button className=\"square\">3</button>\n      </div>\n      <div className=\"board-row\">\n        <button className=\"square\">4</button>\n        <button className=\"square\">5</button>\n        <button className=\"square\">6</button>\n      </div>\n      <div className=\"board-row\">\n        <button className=\"square\">7</button>\n        <button className=\"square\">8</button>\n        <button className=\"square\">9</button>\n      </div>\n    </>\n  );\n}\n```\n\n`styles.css` 에 정의된 CSS는 `board-row`라는 `className`으로 지정된 `div`를 스타일 합니다. 이제 스타일된 `div`를 사용하여 컴포넌트를 행으로 그룹화하여 틱택토 보드를 완성하겠습니다.\n\n![1부터 9까지의 숫자가 채워진 틱택토 보드](../images/tutorial/number-filled-board.png)\n\n하지만 문제가 있습니다. `Square`로 이름 지어진 컴포넌트가 더 이상 하나의 사각형이 아닙니다. 이 문제를 수정하기 위해 `Board`로 이름을 변경하겠습니다.\n\n```js {1}\nexport default function Board() {\n  //...\n}\n```\n\n이 시점에서 코드는 다음과 같아야 합니다.\n\n<Sandpack>\n\n```js\nexport default function Board() {\n  return (\n    <>\n      <div className=\"board-row\">\n        <button className=\"square\">1</button>\n        <button className=\"square\">2</button>\n        <button className=\"square\">3</button>\n      </div>\n      <div className=\"board-row\">\n        <button className=\"square\">4</button>\n        <button className=\"square\">5</button>\n        <button className=\"square\">6</button>\n      </div>\n      <div className=\"board-row\">\n        <button className=\"square\">7</button>\n        <button className=\"square\">8</button>\n        <button className=\"square\">9</button>\n      </div>\n    </>\n  );\n}\n```\n\n```css src/styles.css\n* {\n  box-sizing: border-box;\n}\n\nbody {\n  font-family: sans-serif;\n  margin: 20px;\n  padding: 0;\n}\n\n.square {\n  background: #fff;\n  border: 1px solid #999;\n  float: left;\n  font-size: 24px;\n  font-weight: bold;\n  line-height: 34px;\n  height: 34px;\n  margin-right: -1px;\n  margin-top: -1px;\n  padding: 0;\n  text-align: center;\n  width: 34px;\n}\n\n.board-row:after {\n  clear: both;\n  content: '';\n  display: table;\n}\n\n.status {\n  margin-bottom: 10px;\n}\n.game {\n  display: flex;\n  flex-direction: row;\n}\n\n.game-info {\n  margin-left: 20px;\n}\n```\n\n</Sandpack>\n\n<Note>\n\n어휴… 입력할 내용이 많네요! 이 페이지에서 코드를 복사하여 붙여 넣어도 괜찮습니다. 하지만 조금 더 도전해 보고 싶다면 코드를 스스로 작성하고, 스스로 작성한 코드만 복사하는 것을 권장합니다.\n\n</Note>\n\n### Props를 통해 데이터 전달하기 {/*passing-data-through-props*/}\n\n다음으로 사용자가 사각형을 클릭할 때 사각형의 값을 비어있는 상태에서 \"X\"로 변경해야 합니다. 조금 전 보드를 만들었던 방법으로는 사각형을 변경하는 코드를 9번 (각 사각형당 한번) 복사해서 붙여 넣어야 합니다! 복사-붙여넣기 대신 React의 컴포넌트 아키텍처를 사용하면 재사용할 수 있는 컴포넌트를 만들어서 지저분하고 중복된 코드를 피할 수 있습니다.\n\n먼저 `Board` 컴포넌트에서 첫 번째 사각형을 정의하는 줄(`<button className=\"square\">1</button>`)을 새 `Square` 컴포넌트로 복사하세요.\n\n```js {1-3}\nfunction Square() {\n  return <button className=\"square\">1</button>;\n}\n\nexport default function Board() {\n  // ...\n}\n```\n\n다음으로, `Board` 컴포넌트를 JSX 문법을 사용하여 해당 `Square` 컴포넌트를 렌더링하도록 수정하세요.\n\n```js {5-19}\n// ...\nexport default function Board() {\n  return (\n    <>\n      <div className=\"board-row\">\n        <Square />\n        <Square />\n        <Square />\n      </div>\n      <div className=\"board-row\">\n        <Square />\n        <Square />\n        <Square />\n      </div>\n      <div className=\"board-row\">\n        <Square />\n        <Square />\n        <Square />\n      </div>\n    </>\n  );\n}\n```\n\n브라우저의 `div`와 달리, 직접 만든 `Board`와 `Square` 컴포넌트는 반드시 대문자로 시작해야 한다는 점에 유의하세요.\n\n보드를 살펴보겠습니다.\n\n![1로 채워진 보드](../images/tutorial/board-filled-with-ones.png)\n\n이런! 이전에 가지고 있던 번호가 채워진 사각형이 사라졌습니다. 이제 각 사각형은 \"1\"로 표시됩니다. 이 문제를 해결하기 위해 *Props*를 사용하여 각 사각형이 가져야 할 값을 부모 컴포넌트(`Board`)에서 자식 컴포넌트(`Square`)로 전달하겠습니다.\n\n`Square` 컴포넌트를 `Board`에서 전달할 Prop `value`를 읽도록 수정하세요.\n\n```js {1}\nfunction Square({ value }) {\n  return <button className=\"square\">1</button>;\n}\n```\n\n`function Square({ value })`는 사각형 컴포넌트에 `value` Prop를 전달할 수 있음을 나타냅니다.\n\n이제 모든 사각형에 `1` 대신 `value`를 표시하겠습니다. 아래와 같이 해보세요.\n\n```js {2}\nfunction Square({ value }) {\n  return <button className=\"square\">value</button>;\n}\n```\n\n이런, 원하던 것과는 다른 결과입니다.\n\n![value로 채워진 보드](../images/tutorial/board-filled-with-value.png)\n\n컴포넌트에서 단어 \"value\"가 아닌 자바스크립트 변수 `value`가 렌더링 되어야 합니다. JSX에서 \"자바스크립트로 탈출\"하려면, 중괄호가 필요합니다. JSX에서 `value` 주위에 중괄호를 다음과 같이 추가하세요.\n\n```js {2}\nfunction Square({ value }) {\n  return <button className=\"square\">{value}</button>;\n}\n```\n\n지금은 빈 보드가 표시되어야 합니다.\n\n![비어있는 보드](../images/tutorial/empty-board.png)\n\n보드가 비어있는 이유는 `Board` 컴포넌트가 렌더링하는 각 `Square` 컴포넌트에 아직 `value` Prop를 전달하지 않았기 때문입니다. 이 문제를 해결하기 위해 `Board` 컴포넌트가 렌더링하는 각 `Square` 컴포넌트에 `value` Prop를 추가하겠습니다.\n\n```js {5-7,10-12,15-17}\nexport default function Board() {\n  return (\n    <>\n      <div className=\"board-row\">\n        <Square value=\"1\" />\n        <Square value=\"2\" />\n        <Square value=\"3\" />\n      </div>\n      <div className=\"board-row\">\n        <Square value=\"4\" />\n        <Square value=\"5\" />\n        <Square value=\"6\" />\n      </div>\n      <div className=\"board-row\">\n        <Square value=\"7\" />\n        <Square value=\"8\" />\n        <Square value=\"9\" />\n      </div>\n    </>\n  );\n}\n```\n\n이제 숫자가 있는 보드가 다시 표시됩니다.\n\n![1부터 9까지의 숫자로 채워진 틱택토 보드](../images/tutorial/number-filled-board.png)\n\n수정된 코드는 다음과 같습니다.\n\n<Sandpack>\n\n```js src/App.js\nfunction Square({ value }) {\n  return <button className=\"square\">{value}</button>;\n}\n\nexport default function Board() {\n  return (\n    <>\n      <div className=\"board-row\">\n        <Square value=\"1\" />\n        <Square value=\"2\" />\n        <Square value=\"3\" />\n      </div>\n      <div className=\"board-row\">\n        <Square value=\"4\" />\n        <Square value=\"5\" />\n        <Square value=\"6\" />\n      </div>\n      <div className=\"board-row\">\n        <Square value=\"7\" />\n        <Square value=\"8\" />\n        <Square value=\"9\" />\n      </div>\n    </>\n  );\n}\n```\n\n```css src/styles.css\n* {\n  box-sizing: border-box;\n}\n\nbody {\n  font-family: sans-serif;\n  margin: 20px;\n  padding: 0;\n}\n\n.square {\n  background: #fff;\n  border: 1px solid #999;\n  float: left;\n  font-size: 24px;\n  font-weight: bold;\n  line-height: 34px;\n  height: 34px;\n  margin-right: -1px;\n  margin-top: -1px;\n  padding: 0;\n  text-align: center;\n  width: 34px;\n}\n\n.board-row:after {\n  clear: both;\n  content: '';\n  display: table;\n}\n\n.status {\n  margin-bottom: 10px;\n}\n.game {\n  display: flex;\n  flex-direction: row;\n}\n\n.game-info {\n  margin-left: 20px;\n}\n```\n\n</Sandpack>\n\n### 사용자와 상호작용하는 컴포넌트 만들기 {/*making-an-interactive-component*/}\n\n이제 `Square` 컴포넌트를 클릭하면 `X`로 채워보겠습니다. `Square` 컴포넌트 내부에 `handleClick` 함수를 선언하세요. 그런 다음 `Square` 컴포넌트에서 반환<sup>`return`</sup>된 JSX 버튼<sup>`<button>`</sup> 요소에 `onClick` Props를 추가하세요.\n\n```js {2-4,9}\nfunction Square({ value }) {\n  function handleClick() {\n    console.log('clicked!');\n  }\n\n  return (\n    <button\n      className=\"square\"\n      onClick={handleClick}\n    >\n      {value}\n    </button>\n  );\n}\n```\n\n이제 사각형을 클릭하면, CodeSandBox의 _브라우저_ 구역에 있는 _콘솔_ 탭에 `\"clicked!\"` 라는 로그가 표시됩니다. 사각형을 한 번 더 클릭하면 `\"clicked!\"` 라는 로그가 다시 생성됩니다. 같은 메시지가 포함된 콘솔 로그를 반복해도 콘솔에 더 많은 줄이 생기지 않습니다. 대신 첫 번째 `\"clicked!\"` 로그 옆의 숫자가 증가하는 것을 볼 수 있습니다.\n\n<Note>\n\n만약 로컬 개발 환경을 사용하여 이 자습서를 진행한다면, 브라우저의 콘솔을 열어야 합니다. 예를 들어, Chrome 브라우저를 사용하는 경우, 키보드 단축키 **Shift + Ctrl + J** (Windows/Linux 환경) 또는 **Option + ⌘ + J** (macOS 환경)를 사용하여 콘솔을 볼 수 있습니다.\n\n</Note>\n\n다음으로 사각형 컴포넌트가 클릭된 것을 \"기억\"하고 \"X\" 표시로 채워보겠습니다. 컴포넌트는 무언가 \"기억\"하기 위해 *State*를 사용합니다.\n\nReact는 컴포넌트에서 호출하여 무언가를 \"기억\"할 수 있는 `useState`라는 특별한 함수를 제공합니다. `Square`의 현재 값을 State에 저장하고 `Square`를 클릭하면 값을 변경하도록 하겠습니다.\n\n파일 상단에서 `useState`를 불러오세요. `Square` 컴포넌트에서 `value` Prop을 제거하는 대신, `Square` 컴포넌트의 시작 부분에 `useState`를 호출하는 새 줄을 추가하고 `value`라는 이름의 State 변수를 반환하도록 하세요.\n\n```js {1,3,4}\nimport { useState } from 'react';\n\nfunction Square() {\n  const [value, setValue] = useState(null);\n\n  function handleClick() {\n    //...\n```\n\n`value`는 값을 저장하고 `setValue`는 값을 변경하는 데 사용하는 함수입니다. `useState`에 전달된 `null`은 이 State 변수의 초깃값으로 사용되므로 현재 `value`는 `null`과 같습니다.\n\n`Square` 컴포넌트는 더 이상 Props를 허용하지 않으므로 `Board` 컴포넌트가 생성한 9개의 사각형 컴포넌트에서 `value` Prop를 제거하세요.\n\n```js {6-8,11-13,16-18}\n// ...\nexport default function Board() {\n  return (\n    <>\n      <div className=\"board-row\">\n        <Square />\n        <Square />\n        <Square />\n      </div>\n      <div className=\"board-row\">\n        <Square />\n        <Square />\n        <Square />\n      </div>\n      <div className=\"board-row\">\n        <Square />\n        <Square />\n        <Square />\n      </div>\n    </>\n  );\n}\n```\n\n이제 `Square`를 클릭하였을 때 \"X\"를 표시하도록 변경하겠습니다. `console.log(\"clicked!\");` 이벤트 핸들러를 `setValue('X');`로 변경하세요. 이제 `Square` 컴포넌트는 다음과 같습니다.\n\n```js {5}\nfunction Square() {\n  const [value, setValue] = useState(null);\n\n  function handleClick() {\n    setValue('X');\n  }\n\n  return (\n    <button\n      className=\"square\"\n      onClick={handleClick}\n    >\n      {value}\n    </button>\n  );\n}\n```\n\n`onClick` 핸들러에서 `set` 함수를 호출함으로써 React에 `<button>`을 클릭 할 때마다 `Square`를 다시 렌더링하도록 했습니다. 업데이트 후 `Square`의 `value`는 `'X'`가 되므로, 앞으로 보드에서 \"X\"를 볼 수 있습니다. 사각형을 클릭하면 \"X\"가 표시됩니다.\n\n![보드에 x를 추가하기](../images/tutorial/tictac-adding-x-s.gif)\n\n각 사각형에는 고유한 State가 있습니다. 각 사각형에 저장된 `value`는 다른 사각형과 완전히 독립적입니다. 컴포넌트에서 `set` 함수를 호출하면 React는 그 안에 있는 자식 컴포넌트도 자동으로 업데이트합니다.\n\n위의 변경 사항을 적용한 코드는 다음과 같습니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { useState } from 'react';\n\nfunction Square() {\n  const [value, setValue] = useState(null);\n\n  function handleClick() {\n    setValue('X');\n  }\n\n  return (\n    <button\n      className=\"square\"\n      onClick={handleClick}\n    >\n      {value}\n    </button>\n  );\n}\n\nexport default function Board() {\n  return (\n    <>\n      <div className=\"board-row\">\n        <Square />\n        <Square />\n        <Square />\n      </div>\n      <div className=\"board-row\">\n        <Square />\n        <Square />\n        <Square />\n      </div>\n      <div className=\"board-row\">\n        <Square />\n        <Square />\n        <Square />\n      </div>\n    </>\n  );\n}\n```\n\n```css src/styles.css\n* {\n  box-sizing: border-box;\n}\n\nbody {\n  font-family: sans-serif;\n  margin: 20px;\n  padding: 0;\n}\n\n.square {\n  background: #fff;\n  border: 1px solid #999;\n  float: left;\n  font-size: 24px;\n  font-weight: bold;\n  line-height: 34px;\n  height: 34px;\n  margin-right: -1px;\n  margin-top: -1px;\n  padding: 0;\n  text-align: center;\n  width: 34px;\n}\n\n.board-row:after {\n  clear: both;\n  content: '';\n  display: table;\n}\n\n.status {\n  margin-bottom: 10px;\n}\n.game {\n  display: flex;\n  flex-direction: row;\n}\n\n.game-info {\n  margin-left: 20px;\n}\n```\n\n</Sandpack>\n\n### React 개발자 도구 {/*react-developer-tools*/}\n\nReact 개발자 도구를 사용하면 React 컴포넌트의 Props와 State를 확인할 수 있습니다. CodeSandBox의 _브라우저_ 구역 하단에서 React 개발자 도구 탭을 찾을 수 있습니다.\n\n![CodeSandbox의 React 개발자 도구](../images/tutorial/codesandbox-devtools.png)\n\n화면에서 특정 컴포넌트를 검사하려면 React 개발자 도구의 왼쪽 위 모서리에 있는 버튼을 사용하세요.\n\n![React 개발자 도구로 페이지의 컴포넌트 선택하기](../images/tutorial/devtools-select.gif)\n\n<Note>\n\n로컬 환경에서 개발하는 경우, React 개발자 도구는 [Chrome](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi?hl=en), [Firefox](https://addons.mozilla.org/en-US/firefox/addon/react-devtools/), 그리고 [Edge](https://microsoftedge.microsoft.com/addons/detail/react-developer-tools/gpphkfbcpidddadnkolkpfckpihlkkil) 브라우저의 확장 프로그램으로 사용할 수 있습니다. 설치 후 브라우저 개발자 도구에 React를 사용하는 사이트를 위한 *Components* 탭이 나타납니다.\n\n</Note>\n\n## 게임 완료하기 {/*completing-the-game*/}\n\n이제 틱택토 게임을 위한 기본적인 구성 요소는 모두 갖추었습니다. 게임을 완성하기 위해서는 보드에 \"X\"와 \"O\"를 번갈아 배치해야 하며, 승자를 결정할 방법이 필요합니다.\n\n### State 끌어올리기 {/*lifting-state-up*/}\n\n현재 각 `Square` 컴포넌트는 게임 State의 일부를 유지합니다. 틱택토 게임에서 승자를 확인하려면 `Board`가 9개의 `Square` 컴포넌트 각각의 State를 어떻게든 알고 있어야 합니다.\n\n어떻게 접근하는 것이 좋을까요? `Board`가 각각의 `Square`에 해당 `Square`의 State를 \"요청\"해야 한다고 생각해 보겠습니다. 이 접근 방식은 React에서 기술적으로는 가능하지만, 코드가 이해하기 어렵고 버그에 취약하며 리팩토링하기 어렵기 때문에 권장하지 않습니다. 가장 좋은 접근 방식은 게임의 State를 각 `Square`가 아닌 부모 `Board` 컴포넌트에 저장하는 것입니다. `Board` 컴포넌트는 각 사각형에 숫자를 전달했을 때와 같이 Prop를 전달하여 각 `Square`에 표시할 내용을 정할 수 있습니다.\n\n**여러 자식 컴포넌트에서 데이터를 수집하거나 두 자식 컴포넌트가 서로 통신하도록 하려면, 부모 컴포넌트에서 공유 State를 선언하세요. 부모 컴포넌트는 Props를 통해 해당 State를 자식 컴포넌트에 전달할 수 있습니다. 이렇게 하면 자식 컴포넌트가 서로 동기화되고 부모 컴포넌트와도 동기화되도록 유지할 수 있습니다.**\n\nReact 컴포넌트를 리팩토링할 때 부모 컴포넌트로 State를 끌어올리는 것은 흔히 쓰이는 방법입니다.\n\n이번 기회에 직접 사용해 보도록 하겠습니다. `Board` 컴포넌트를 편집하여 9개 사각형에 해당하는 9개의 `null`의 배열을 기본값으로 하는 State 변수 `squares`를 선언하세요.\n\n```js {3}\n// ...\nexport default function Board() {\n  const [squares, setSquares] = useState(Array(9).fill(null));\n  return (\n    // ...\n  );\n}\n```\n\n`Array(9).fill(null)`은 9개의 엘리먼트로 배열을 생성하고 각 엘리먼트를 `null`로 설정합니다. 그 주위에 있는 `useState()` 호출은 처음에 해당 배열로 설정된 State 변수 `squares`를 선언합니다. 배열의 각 항목은 사각형의 값에 해당합니다. 나중에 보드를 채우면, `squares` 배열은 다음과 같은 모양이 됩니다.\n\n```jsx\n['O', null, 'X', 'X', 'X', 'O', 'O', null, null]\n```\n\n이제 `Board` 컴포넌트는 렌더링하는 각 `Square` 컴포넌트에 `value` Prop를 전달해야 합니다.\n\n```js {6-8,11-13,16-18}\nexport default function Board() {\n  const [squares, setSquares] = useState(Array(9).fill(null));\n  return (\n    <>\n      <div className=\"board-row\">\n        <Square value={squares[0]} />\n        <Square value={squares[1]} />\n        <Square value={squares[2]} />\n      </div>\n      <div className=\"board-row\">\n        <Square value={squares[3]} />\n        <Square value={squares[4]} />\n        <Square value={squares[5]} />\n      </div>\n      <div className=\"board-row\">\n        <Square value={squares[6]} />\n        <Square value={squares[7]} />\n        <Square value={squares[8]} />\n      </div>\n    </>\n  );\n}\n```\n\n다음으로 보드 컴포넌트에서 각 `value` Prop를 받을 수 있도록 `Square` 컴포넌트를 수정하겠습니다. 이를 위해 사각형 컴포넌트에서 `value`의 상태 추적과 버튼의 `onClick` Prop를 제거해야 합니다.\n\n```js {1,2}\nfunction Square({value}) {\n  return <button className=\"square\">{value}</button>;\n}\n```\n\n이때의 보드는 텅 비어있습니다.\n\n![텅 빈 보드](../images/tutorial/empty-board.png)\n\n코드는 다음과 같습니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { useState } from 'react';\n\nfunction Square({ value }) {\n  return <button className=\"square\">{value}</button>;\n}\n\nexport default function Board() {\n  const [squares, setSquares] = useState(Array(9).fill(null));\n  return (\n    <>\n      <div className=\"board-row\">\n        <Square value={squares[0]} />\n        <Square value={squares[1]} />\n        <Square value={squares[2]} />\n      </div>\n      <div className=\"board-row\">\n        <Square value={squares[3]} />\n        <Square value={squares[4]} />\n        <Square value={squares[5]} />\n      </div>\n      <div className=\"board-row\">\n        <Square value={squares[6]} />\n        <Square value={squares[7]} />\n        <Square value={squares[8]} />\n      </div>\n    </>\n  );\n}\n```\n\n```css src/styles.css\n* {\n  box-sizing: border-box;\n}\n\nbody {\n  font-family: sans-serif;\n  margin: 20px;\n  padding: 0;\n}\n\n.square {\n  background: #fff;\n  border: 1px solid #999;\n  float: left;\n  font-size: 24px;\n  font-weight: bold;\n  line-height: 34px;\n  height: 34px;\n  margin-right: -1px;\n  margin-top: -1px;\n  padding: 0;\n  text-align: center;\n  width: 34px;\n}\n\n.board-row:after {\n  clear: both;\n  content: '';\n  display: table;\n}\n\n.status {\n  margin-bottom: 10px;\n}\n.game {\n  display: flex;\n  flex-direction: row;\n}\n\n.game-info {\n  margin-left: 20px;\n}\n```\n\n</Sandpack>\n\n이제 각 사각형은 `'X'` , `'O'` , 또는 빈 사각형의 경우 `null`이 되는 `value` Prop를 받습니다.\n\n다음으로 `Square`를 클릭하였을 때 발생하는 동작을 변경하겠습니다. 이제 `Board` 컴포넌트가 어떤 사각형이 채워졌는지를 관리하므로 `Square`가 `Board`의 State를 업데이트할 방법을 만들어야 합니다. 컴포넌트는 자신이 정의한 State에만 접근할 수 있으므로 `Square`에서 `Board`의 State를 직접 변경할 수 없습니다.\n\n대신에 `Board` 컴포넌트에서 `Square` 컴포넌트로 함수를 전달하고 사각형을 클릭할 때 `Square`가 해당 함수를 호출하도록 할 수 있습니다. `Square` 컴포넌트를 클릭할 때 호출할 함수부터 시작하겠습니다. `onSquareClick`으로 해당 함수를 호출하세요.\n\n```js {3}\nfunction Square({ value }) {\n  return (\n    <button className=\"square\" onClick={onSquareClick}>\n      {value}\n    </button>\n  );\n}\n```\n\n다음으로, `Square` 컴포넌트의 Props에 `onSquareClick` 함수를 추가하세요.\n\n```js {1}\nfunction Square({ value, onSquareClick }) {\n  return (\n    <button className=\"square\" onClick={onSquareClick}>\n      {value}\n    </button>\n  );\n}\n```\n\n이제 `onSquareClick` Prop을 `Board` 컴포넌트의 `handleClick` 함수와 연결하세요. `onSquareClick` 함수를 `handleClick`과 연결하려면 첫 번째 `Square` 컴포넌트의 `onSquareClick` Prop에 해당 함수를 전달하면 됩니다.\n\n```js {7}\nexport default function Board() {\n  const [squares, setSquares] = useState(Array(9).fill(null));\n\n  return (\n    <>\n      <div className=\"board-row\">\n        <Square value={squares[0]} onSquareClick={handleClick} />\n        //...\n  );\n}\n```\n\n마지막으로 보드 컴포넌트 내부에 `handleClick` 함수를 정의하여 보드의 State를 담고 있는 `squares` 배열을 업데이트하세요.\n\n```js {4-8}\nexport default function Board() {\n  const [squares, setSquares] = useState(Array(9).fill(null));\n\n  function handleClick() {\n    const nextSquares = squares.slice();\n    nextSquares[0] = \"X\";\n    setSquares(nextSquares);\n  }\n\n  return (\n    // ...\n  )\n}\n```\n\n`handleClick` 함수는 자바스크립트의 `slice()` 배열 메서드를 사용하여 `squares` 배열의 사본 `nextSquares`를 생성합니다. 그런 다음 `handleClick` 함수는 `nextSquares` 배열의 첫 번째 사각형(인덱스 `[0]`)에 `X`를 추가하여 업데이트합니다.\n\n`setSquares` 함수를 호출하면 React는 컴포넌트의 State가 변경되었음을 알 수 있습니다. 그러면 `squares`의 State를 사용하는 컴포넌트(`Board`)와 그 하위 컴포넌트(보드를 구성하는 `Square` 컴포넌트)가 다시 렌더링 됩니다.\n\n<Note>\n\n자바스크립트는 [클로저](https://developer.mozilla.org/ko/docs/Web/JavaScript/Closures)를 지원하므로 내부 함수가(예: `handleClick`) 외부 함수(예: `Board`)에 정의된 변수 및 함수에 접근할 수 있습니다. `handleClick` 함수는 `squares`의 State를 읽고 `setSquares` 메서드를 호출할 수 있는데, 이 두 함수는 `Board` 함수 내부에 정의되어 있기 때문입니다.\n\n</Note>\n\n이제 보드에 X를 추가할 수 있게 되었지만 가능한 건 오직 왼쪽 위 사각형뿐입니다. `handleClick` 함수는 왼쪽 위 사각형(인덱스 `0`)만 업데이트하도록 하드 코딩되어 있습니다. 모든 사각형을 업데이트할 수 있도록 `handleClick` 함수를 수정하겠습니다. `handleClick` 함수에 업데이트할 사각형의 인덱스를 나타내는 인수 `i`를 추가하세요.\n\n```js {4,6}\nexport default function Board() {\n  const [squares, setSquares] = useState(Array(9).fill(null));\n\n  function handleClick(i) {\n    const nextSquares = squares.slice();\n    nextSquares[i] = \"X\";\n    setSquares(nextSquares);\n  }\n\n  return (\n    // ...\n  )\n}\n```\n\n다음으로 인수 `i`를 `handleClick`에 전달해야 합니다. 사각형의 `onSquareClick` Prop를 아래와 같이 JSX에서 직접 `handleClick(0)`으로 설정할 수도 있지만 이 방법은 작동하지 않습니다.\n\n```jsx\n<Square value={squares[0]} onSquareClick={handleClick(0)} />\n```\n\n이유는 다음과 같습니다. `handleClick(0)` 호출은 보드 컴포넌트 렌더링의 일부가 됩니다. `handleClick(0)`은 `setSquares`를 호출하여 보드 컴포넌트의 State를 변경하기 때문에 보드 컴포넌트 전체가 다시 렌더링 됩니다. 하지만 이 과정에서 `handleClick(0)`은 다시 실행되기 때문에 무한 루프에 빠지게 됩니다.\n\n<ConsoleBlock level=\"error\">\n\nToo many re-renders. React limits the number of renders to prevent an infinite loop.\n\n</ConsoleBlock>\n\n왜 이러한 문제가 더 일찍 발생하지 않았을까요?\n\n이전에 `onSquareClick={handleClick}`을 전달할 땐 함수를 *호출*한 것이 아니라 `handleClick` 함수를 Prop로 전달했기 때문입니다. 하지만 지금은 `handleClick(0)`의 괄호를 보면 알 수 있듯이 해당 함수를 호출하고 있으므로 해당 함수가 너무 일찍 실행됩니다. 사용자가 클릭하기 전까지 `handleClick` 함수를 호출하면 *안 됩*니다!\n\n이 문제를 해결하려면 `handleClick(0)`을 호출하는 `handleFirstSquareClick` 함수를 만들고, `handleClick(1)`을 호출하는 `handleSecondSquareClick`을 만들고… 계속해서 만들면 됩니다. 그리고 아까와 같이 호출하는 대신 `onSquareClick={handleFirstSquareClick}`와 같은 함수를 Prop로 전달 해 주면 됩니다. 이렇게 하면 무한 루프를 해결할 수 있습니다.\n\n하지만 9개의 서로 다른 함수를 정의하고 각각에 이름을 붙이는 것은 너무 장황합니다. 대신 이렇게 해보겠습니다.\n\n```js {6}\nexport default function Board() {\n  // ...\n  return (\n    <>\n      <div className=\"board-row\">\n        <Square value={squares[0]} onSquareClick={() => handleClick(0)} />\n        // ...\n  );\n}\n```\n\n새로운 문법 `() =>`에 주목하세요. 여기서 `() => handleClick(0)`은 *화살표 함수*로, 함수를 짧게 정의하는 방법입니다. 사각형을 클릭하면 `=>` \"화살표\" 뒤의 코드가 실행되어 `handleClick(0)`을 호출합니다.\n\n이제 전달한 화살표 함수에서 `handleClick`을 호출하도록 나머지 8개의 사각형 컴포넌트를 수정해야 합니다. `handleClick`을 호출할 때 인수가 올바른 사각형의 인덱스에 해당하는지 확인하세요.\n\n```js {6-8,11-13,16-18}\nexport default function Board() {\n  // ...\n  return (\n    <>\n      <div className=\"board-row\">\n        <Square value={squares[0]} onSquareClick={() => handleClick(0)} />\n        <Square value={squares[1]} onSquareClick={() => handleClick(1)} />\n        <Square value={squares[2]} onSquareClick={() => handleClick(2)} />\n      </div>\n      <div className=\"board-row\">\n        <Square value={squares[3]} onSquareClick={() => handleClick(3)} />\n        <Square value={squares[4]} onSquareClick={() => handleClick(4)} />\n        <Square value={squares[5]} onSquareClick={() => handleClick(5)} />\n      </div>\n      <div className=\"board-row\">\n        <Square value={squares[6]} onSquareClick={() => handleClick(6)} />\n        <Square value={squares[7]} onSquareClick={() => handleClick(7)} />\n        <Square value={squares[8]} onSquareClick={() => handleClick(8)} />\n      </div>\n    </>\n  );\n};\n```\n\n이제 보드의 사각형을 클릭하여 X를 다시 추가할 수 있습니다.\n\n![보드를 X로 채우기](../images/tutorial/tictac-adding-x-s.gif)\n\n하지만 이번에는 모든 State 관리를 사각형이 아닌 `Board` 컴포넌트에서 처리합니다!\n\n코드는 다음과 같습니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { useState } from 'react';\n\nfunction Square({ value, onSquareClick }) {\n  return (\n    <button className=\"square\" onClick={onSquareClick}>\n      {value}\n    </button>\n  );\n}\n\nexport default function Board() {\n  const [squares, setSquares] = useState(Array(9).fill(null));\n\n  function handleClick(i) {\n    const nextSquares = squares.slice();\n    nextSquares[i] = 'X';\n    setSquares(nextSquares);\n  }\n\n  return (\n    <>\n      <div className=\"board-row\">\n        <Square value={squares[0]} onSquareClick={() => handleClick(0)} />\n        <Square value={squares[1]} onSquareClick={() => handleClick(1)} />\n        <Square value={squares[2]} onSquareClick={() => handleClick(2)} />\n      </div>\n      <div className=\"board-row\">\n        <Square value={squares[3]} onSquareClick={() => handleClick(3)} />\n        <Square value={squares[4]} onSquareClick={() => handleClick(4)} />\n        <Square value={squares[5]} onSquareClick={() => handleClick(5)} />\n      </div>\n      <div className=\"board-row\">\n        <Square value={squares[6]} onSquareClick={() => handleClick(6)} />\n        <Square value={squares[7]} onSquareClick={() => handleClick(7)} />\n        <Square value={squares[8]} onSquareClick={() => handleClick(8)} />\n      </div>\n    </>\n  );\n}\n```\n\n```css src/styles.css\n* {\n  box-sizing: border-box;\n}\n\nbody {\n  font-family: sans-serif;\n  margin: 20px;\n  padding: 0;\n}\n\n.square {\n  background: #fff;\n  border: 1px solid #999;\n  float: left;\n  font-size: 24px;\n  font-weight: bold;\n  line-height: 34px;\n  height: 34px;\n  margin-right: -1px;\n  margin-top: -1px;\n  padding: 0;\n  text-align: center;\n  width: 34px;\n}\n\n.board-row:after {\n  clear: both;\n  content: '';\n  display: table;\n}\n\n.status {\n  margin-bottom: 10px;\n}\n.game {\n  display: flex;\n  flex-direction: row;\n}\n\n.game-info {\n  margin-left: 20px;\n}\n```\n\n</Sandpack>\n\n이제 `Board`가 모든 State를 관리하므로 부모 `Board` 컴포넌트는 자식 `Square` 컴포넌트가 올바르게 표시될 수 있도록 Props를 전달합니다. `Square`를 클릭하면 자식 `Square` 컴포넌트가 부모 `Board` 컴포넌트에 보드의 State를 업데이트하도록 요청합니다. `Board`의 State가 변경되면 `Board` 컴포넌트와 모든 자식 `Square` 컴포넌트가 자동으로 다시 렌더링 됩니다. `Board` 컴포넌트에 속한 모든 사각형의 State를 유지하면 나중에 승자를 결정할 수 있습니다.\n\n사용자가 보드의 왼쪽 위 사각형을 클릭하여 `X`를 추가하면 어떤 일이 발생하는지 다시 한번 정리해 보겠습니다.\n\n1. 왼쪽 위 사각형을 클릭하면 `button`이 `Square`로부터 `onClick` Prop로 받은 함수가 실행됩니다. `Square` 컴포넌트는 보드에서 해당 함수를 `onSquareClick` Props로 받았습니다. `Board` 컴포넌트는 JSX에서 해당 함수를 직접 정의했습니다. 이 함수는 `0`을 인수로 `handleClick`을 호출합니다.\n1. `handleClick`은 인수 `0`을 사용하여 `squares` 배열의 첫 번째 엘리먼트를 `null`에서 `X`로 업데이트합니다.\n1. `Board` 컴포넌트의 `squares` State가 업데이트되어 `Board`와 그 모든 자식이 다시 렌더링 됩니다. 이에 따라 인덱스가 `0`인 `Square` 컴포넌트의 `value` Prop가 `null`에서 `X`로 변경됩니다.\n\n최종적으로 사용자는 왼쪽 위 사각형을 클릭한 후 비어있는 사각형이 `X`로 변경된 것을 확인할 수 있습니다.\n\n<Note>\n\nDOM `<button>` 엘리먼트의 `onClick` 어트리뷰트는 빌트인 컴포넌트이기 때문에 React에서 특별한 의미를 갖습니다. 사용자 정의 컴포넌트, 예를 들어 사각형의 경우 이름은 사용자가 원하는 대로 지을 수 있습니다. `Square`의 `onSquareClick` Prop나 `Board`의 `handleClick` 함수에 어떠한 이름을 붙여도 코드는 동일하게 작동합니다. React에서는 주로 이벤트를 나타내는 Prop에는 `onSomething`과 같은 이름을 사용하고, 이벤트를 처리하는 함수를 정의 할 때는 `handleSomething`과 같은 이름을 사용합니다.\n\n</Note>\n\n### 불변성이 왜 중요할까요 {/*why-immutability-is-important*/}\n\n`handleClick`에서 기존 배열을 수정하는 대신 `.slice()`를 호출하여 `squares` 배열의 사본을 생성하는 방식에 주목해주세요. 그 이유를 설명하기 위해 불변성과 불변성을 배우는 것이 중요한 이유에 대해 논의해 보겠습니다.\n\n일반적으로 데이터를 변경하는 방법에는 두 가지가 있습니다. 첫 번째 방법은 데이터의 값을 직접 변경하여 데이터를 <em>변형</em>하는 것입니다. 두 번째 방법은 원하는 변경 사항이 있는 새 복사본으로 데이터를 대체하는 것입니다. 다음은 `squares` 배열을 변형한 경우의 모습입니다.\n\n```jsx\nconst squares = [null, null, null, null, null, null, null, null, null];\nsquares[0] = 'X';\n// 이제 `squares`는 `[\"X\", null, null, null, null, null, null, null, null]`입니다.\n```\n\n그리고 아래는 `squares` 배열을 변형하지 않고 데이터를 변경한 경우의 모습입니다.\n\n```jsx\nconst squares = [null, null, null, null, null, null, null, null, null];\nconst nextSquares = ['X', null, null, null, null, null, null, null, null];\n// 이제 `squares`는 변경되지 않았지만 `nextSquares`의 첫 번째 요소는 `null`이 아닌 `'X'`입니다.\n```\n\n최종 결과는 같지만, 원본 데이터를 직접 변형하지 않음으로써 몇 가지 이점을 얻을 수 있습니다.\n\n불변성을 사용하면 복잡한 기능을 훨씬 쉽게 구현할 수 있습니다. 우리는 이 자습서의 뒷부분에서 게임의 진행 과정을 검토하고 과거 움직임으로 \"돌아가기\"를 할 수 있는 \"시간 여행\" 기능을 구현할 예정입니다. 특정 작업을 실행 취소하고 다시 실행하는 기능은 이 게임에만 국한된 것이 아닌 앱의 일반적인 요구사항입니다. 직접적인 데이터 변경을 피하면 이전 버전의 데이터를 그대로 유지하여 나중에 재사용(또는 초기화)할 수 있습니다.\n\n불변성을 사용하는 것의 또 다른 장점이 있습니다. 기본적으로 부모 컴포넌트의 State가 변경되면 모든 자식 컴포넌트가 자동으로 다시 렌더링 됩니다. 여기에는 변경 사항이 없는 자식 컴포넌트도 포함됩니다. 리렌더링 자체가 사용자에게 보이는 것은 아니지만 성능상의 이유로 트리의 영향을 받지 않는 부분의 리렌더링을 피하는 것이 좋습니다. 불변성을 사용하면 컴포넌트가 데이터의 변경 여부를 저렴한 비용으로 판단할 수 있습니다. [`memo` API 참고서](/reference/react/memo)에서 React가 컴포넌트를 다시 렌더링할 시점을 선택하는 방법에 대해 살펴볼 수 있습니다.\n\n### 순서 정하기 {/*taking-turns*/}\n\n이제 이 틱택토 게임에서 가장 큰 결함인 \"O\"를 보드에 표시할 수 없다는 문제를 수정할 차례입니다.\n\n기본적으로 첫 번째 이동을 \"X\"로 설정합니다. 이제 보드 컴포넌트에 또 다른 State를 추가하여 추적해 보겠습니다.\n\n```js {2}\nfunction Board() {\n  const [xIsNext, setXIsNext] = useState(true);\n  const [squares, setSquares] = useState(Array(9).fill(null));\n\n  // ...\n}\n```\n\n플레이어가 움직일 때마다 다음 플레이어를 결정하기 위해 불리언 값인 `xIsNext`가 반전되고 게임의 State가 저장됩니다. `Board`의 `handleClick` 함수를 업데이트하여 `xIsNext`의 값을 반전시키세요.\n\n```js {7,8,9,10,11,13}\nexport default function Board() {\n  const [xIsNext, setXIsNext] = useState(true);\n  const [squares, setSquares] = useState(Array(9).fill(null));\n\n  function handleClick(i) {\n    const nextSquares = squares.slice();\n    if (xIsNext) {\n      nextSquares[i] = \"X\";\n    } else {\n      nextSquares[i] = \"O\";\n    }\n    setSquares(nextSquares);\n    setXIsNext(!xIsNext);\n  }\n\n  return (\n    //...\n  );\n}\n```\n\n이제 다른 사각형을 클릭하면 정상적으로 `X`와 `O`가 번갈아 표시됩니다!\n\n하지만 다른 문제가 발생했습니다. 같은 사각형을 여러 번 클릭해 보세요.\n\n![O가 X를 덮어씁니다.](../images/tutorial/o-replaces-x.gif)\n\n`O`가 `X`를 덮어씌웁니다! 이렇게 하면 게임이 좀 더 흥미로워질 수 있지만 지금은 원래의 규칙을 유지하겠습니다.\n\n지금은 `X`와 `O`로 사각형을 표시할 때 먼저 해당 사각형에 이미 `X` 또는 `O`값이 있는지 확인하고 있지 않습니다. *일찍이 돌아와서* 이 문제를 해결하기 위해 사각형에 이미 `X`와 `O`가 있는지 확인하겠습니다. 사각형이 이미 채워져 있는 경우 보드의 State를 업데이트하기 전에 `handleClick` 함수에서 조기에 `return` 하겠습니다.\n\n```js {2,3,4}\nfunction handleClick(i) {\n  if (squares[i]) {\n    return;\n  }\n  const nextSquares = squares.slice();\n  //...\n}\n```\n\n이제 빈 사각형에 `X` 또는 `O`만 추가할 수 있습니다! 코드는 다음과 같습니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { useState } from 'react';\n\nfunction Square({value, onSquareClick}) {\n  return (\n    <button className=\"square\" onClick={onSquareClick}>\n      {value}\n    </button>\n  );\n}\n\nexport default function Board() {\n  const [xIsNext, setXIsNext] = useState(true);\n  const [squares, setSquares] = useState(Array(9).fill(null));\n\n  function handleClick(i) {\n    if (squares[i]) {\n      return;\n    }\n    const nextSquares = squares.slice();\n    if (xIsNext) {\n      nextSquares[i] = 'X';\n    } else {\n      nextSquares[i] = 'O';\n    }\n    setSquares(nextSquares);\n    setXIsNext(!xIsNext);\n  }\n\n  return (\n    <>\n      <div className=\"board-row\">\n        <Square value={squares[0]} onSquareClick={() => handleClick(0)} />\n        <Square value={squares[1]} onSquareClick={() => handleClick(1)} />\n        <Square value={squares[2]} onSquareClick={() => handleClick(2)} />\n      </div>\n      <div className=\"board-row\">\n        <Square value={squares[3]} onSquareClick={() => handleClick(3)} />\n        <Square value={squares[4]} onSquareClick={() => handleClick(4)} />\n        <Square value={squares[5]} onSquareClick={() => handleClick(5)} />\n      </div>\n      <div className=\"board-row\">\n        <Square value={squares[6]} onSquareClick={() => handleClick(6)} />\n        <Square value={squares[7]} onSquareClick={() => handleClick(7)} />\n        <Square value={squares[8]} onSquareClick={() => handleClick(8)} />\n      </div>\n    </>\n  );\n}\n```\n\n```css src/styles.css\n* {\n  box-sizing: border-box;\n}\n\nbody {\n  font-family: sans-serif;\n  margin: 20px;\n  padding: 0;\n}\n\n.square {\n  background: #fff;\n  border: 1px solid #999;\n  float: left;\n  font-size: 24px;\n  font-weight: bold;\n  line-height: 34px;\n  height: 34px;\n  margin-right: -1px;\n  margin-top: -1px;\n  padding: 0;\n  text-align: center;\n  width: 34px;\n}\n\n.board-row:after {\n  clear: both;\n  content: '';\n  display: table;\n}\n\n.status {\n  margin-bottom: 10px;\n}\n.game {\n  display: flex;\n  flex-direction: row;\n}\n\n.game-info {\n  margin-left: 20px;\n}\n```\n\n</Sandpack>\n\n### 승자 결정하기 {/*declaring-a-winner*/}\n\n이제 어느 플레이어의 다음 차례인지 표시했으니, 게임의 승자가 결정되어 더 이상 차례를 만들 필요가 없을 때도 표시해야 합니다. 이를 위해 9개의 사각형 배열을 가져와서 승자를 확인하고 적절하게 `'X'` , `'O'` , 또는 `null`을 반환하는 도우미 함수 `calculateWinner`를 추가하겠습니다. `calculateWinner` 함수에 대해 너무 걱정하지 마세요. 이 함수는 React에서만 국한되는 함수가 아닙니다.\n\n```js src/App.js\nexport default function Board() {\n  //...\n}\n\nfunction calculateWinner(squares) {\n  const lines = [\n    [0, 1, 2],\n    [3, 4, 5],\n    [6, 7, 8],\n    [0, 3, 6],\n    [1, 4, 7],\n    [2, 5, 8],\n    [0, 4, 8],\n    [2, 4, 6]\n  ];\n  for (let i = 0; i < lines.length; i++) {\n    const [a, b, c] = lines[i];\n    if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {\n      return squares[a];\n    }\n  }\n  return null;\n}\n```\n\n<Note>\n\n`calculateWinner` 함수를 `Board`의 앞에 정의하든 뒤에 정의하든 상관없습니다. 여기에선 컴포넌트를 편집할 때마다 편집기 상에서 스크롤 할 필요가 없도록 마지막에 배치하겠습니다.\n\n</Note>\n\n`Board` 컴포넌트의 `handleClick` 함수에서 `calculateWinner(squares)`를 호출하여 플레이어가 이겼는지 확인하세요. 이 검사는 사용자가 이미 `X` 또는 `O`가 있는 사각형을 클릭했는지를 확인하는 것과 동시에 수행할 수 있습니다. 두 경우 모두 함수를 조기 반환하겠습니다.\n\n```js {2}\nfunction handleClick(i) {\n  if (squares[i] || calculateWinner(squares)) {\n    return;\n  }\n  const nextSquares = squares.slice();\n  //...\n}\n```\n\n게임이 끝났을 때 플레이어에게 알리기 위해 \"Winner: X\" 또는 \"Winner: O\"라고 표시하겠습니다. 이렇게 하려면 `Board` 컴포넌트에 `status` 구역을 추가하면 됩니다. 게임이 끝나면 `status`는 승자를 표시하고 게임이 진행 중인 경우 다음 플레이어의 차례를 표시합니다.\n\n```js {3-9,13}\nexport default function Board() {\n  // ...\n  const winner = calculateWinner(squares);\n  let status;\n  if (winner) {\n    status = \"Winner: \" + winner;\n  } else {\n    status = \"Next player: \" + (xIsNext ? \"X\" : \"O\");\n  }\n\n  return (\n    <>\n      <div className=\"status\">{status}</div>\n      <div className=\"board-row\">\n        // ...\n  )\n}\n```\n\n축하합니다! 이제 제대로 작동하는 틱택토 게임을 만들었습니다. 그리고 방금 React의 기본도 배웠습니다. 그러니 여기서 진정한 승자는 바로 <em>여러분</em>입니다. 코드는 다음과 같습니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { useState } from 'react';\n\nfunction Square({value, onSquareClick}) {\n  return (\n    <button className=\"square\" onClick={onSquareClick}>\n      {value}\n    </button>\n  );\n}\n\nexport default function Board() {\n  const [xIsNext, setXIsNext] = useState(true);\n  const [squares, setSquares] = useState(Array(9).fill(null));\n\n  function handleClick(i) {\n    if (calculateWinner(squares) || squares[i]) {\n      return;\n    }\n    const nextSquares = squares.slice();\n    if (xIsNext) {\n      nextSquares[i] = 'X';\n    } else {\n      nextSquares[i] = 'O';\n    }\n    setSquares(nextSquares);\n    setXIsNext(!xIsNext);\n  }\n\n  const winner = calculateWinner(squares);\n  let status;\n  if (winner) {\n    status = 'Winner: ' + winner;\n  } else {\n    status = 'Next player: ' + (xIsNext ? 'X' : 'O');\n  }\n\n  return (\n    <>\n      <div className=\"status\">{status}</div>\n      <div className=\"board-row\">\n        <Square value={squares[0]} onSquareClick={() => handleClick(0)} />\n        <Square value={squares[1]} onSquareClick={() => handleClick(1)} />\n        <Square value={squares[2]} onSquareClick={() => handleClick(2)} />\n      </div>\n      <div className=\"board-row\">\n        <Square value={squares[3]} onSquareClick={() => handleClick(3)} />\n        <Square value={squares[4]} onSquareClick={() => handleClick(4)} />\n        <Square value={squares[5]} onSquareClick={() => handleClick(5)} />\n      </div>\n      <div className=\"board-row\">\n        <Square value={squares[6]} onSquareClick={() => handleClick(6)} />\n        <Square value={squares[7]} onSquareClick={() => handleClick(7)} />\n        <Square value={squares[8]} onSquareClick={() => handleClick(8)} />\n      </div>\n    </>\n  );\n}\n\nfunction calculateWinner(squares) {\n  const lines = [\n    [0, 1, 2],\n    [3, 4, 5],\n    [6, 7, 8],\n    [0, 3, 6],\n    [1, 4, 7],\n    [2, 5, 8],\n    [0, 4, 8],\n    [2, 4, 6],\n  ];\n  for (let i = 0; i < lines.length; i++) {\n    const [a, b, c] = lines[i];\n    if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {\n      return squares[a];\n    }\n  }\n  return null;\n}\n```\n\n```css src/styles.css\n* {\n  box-sizing: border-box;\n}\n\nbody {\n  font-family: sans-serif;\n  margin: 20px;\n  padding: 0;\n}\n\n.square {\n  background: #fff;\n  border: 1px solid #999;\n  float: left;\n  font-size: 24px;\n  font-weight: bold;\n  line-height: 34px;\n  height: 34px;\n  margin-right: -1px;\n  margin-top: -1px;\n  padding: 0;\n  text-align: center;\n  width: 34px;\n}\n\n.board-row:after {\n  clear: both;\n  content: '';\n  display: table;\n}\n\n.status {\n  margin-bottom: 10px;\n}\n.game {\n  display: flex;\n  flex-direction: row;\n}\n\n.game-info {\n  margin-left: 20px;\n}\n```\n\n</Sandpack>\n\n## 시간여행 추가하기 {/*adding-time-travel*/}\n\n마지막 연습으로 게임의 이전 동작으로 \"시간을 거슬러 올라가는\" 기능을 만들어 보겠습니다.\n\n### 이동 히스토리 저장하기 {/*storing-a-history-of-moves*/}\n\n`squares` 배열을 변형하면 시간 여행을 구현하기는 매우 어려울 것입니다.\n\n하지만 우리는 `slice()`를 사용하여 매번 이동할 때마다 `squares` 배열의 새 복사본을 만들고 이를 불변으로 처리했습니다. 덕분에 `squares` 배열의 모든 과거 버전을 저장할 수 있고 이미 발생한 턴 사이를 탐색할 수 있습니다.\n\n과거의 `squares` 배열을 `history`라는 다른 배열에 저장하고 이 배열을 새로운 State 변수로 저장하겠습니다. `history` 배열은 첫 번째 이동부터 마지막 이동까지 모든 보드 State를 나타내며 다음과 같은 모양을 갖습니다.\n\n```jsx\n[\n  // 첫 번째 이동 전\n  [null, null, null, null, null, null, null, null, null],\n  // 첫 번째 이동 후\n  [null, null, null, null, 'X', null, null, null, null],\n  // 두 번째 이동 후\n  [null, null, null, null, 'X', null, null, null, 'O'],\n  // ...\n]\n```\n\n### 한 번 더 State 끌어올리기 {/*lifting-state-up-again*/}\n\n이제 과거 이동 목록을 표시하기 위해 새로운 최상위 컴포넌트 `Game`을 작성하세요. 여기에 전체 게임 기록을 포함하는 `history` State를 배치하겠습니다.\n\n`history` State를 `Game` 컴포넌트에 배치하면 자식 `Board` 컴포넌트에서 `squares` State를 제거할 수 있습니다. `Square` 컴포넌트에서 `Board` 컴포넌트로 State를 \"끌어올렸던\" 것처럼, 이제 `Board` 컴포넌트에서 최상위 `Game` 컴포넌트로 State를 끌어올릴 수 있습니다. 이렇게 하면 `Game` 컴포넌트가 `Board` 컴포넌트의 데이터를 완전히 제어하고 `Board`의 `history`에서 이전 순서를 렌더링하도록 지시할 수 있습니다.\n\n먼저 `export default`가 있는 `Game` 컴포넌트를 추가하세요. 일부 마크업 안에 `Board` 컴포넌트를 렌더링하도록 하세요.\n\n```js {1,5-16}\nfunction Board() {\n  // ...\n}\n\nexport default function Game() {\n  return (\n    <div className=\"game\">\n      <div className=\"game-board\">\n        <Board />\n      </div>\n      <div className=\"game-info\">\n        <ol>{/*TODO*/}</ol>\n      </div>\n    </div>\n  );\n}\n```\n\n`export default` 키워드를 `function Board() {` 선언 앞에서 제거하고 `function Game() {` 선언 앞에 추가한 것에 유의하세요. 이것은 `index.js` 파일에서 `Board` 컴포넌트 대신 `Game` 컴포넌트를 최상위 컴포넌트로 사용하도록 지시합니다. `Game` 컴포넌트가 반환하는 내용에 추가한 `div`는 나중에 보드에 추가할 게임 정보를 위한 공간을 확보합니다.\n\n다음 플레이어와 이동 기록을 추적하기 위해 `Game` 컴포넌트에 몇개의 State를 추가하세요.\n\n```js {2-3}\nexport default function Game() {\n  const [xIsNext, setXIsNext] = useState(true);\n  const [history, setHistory] = useState([Array(9).fill(null)]);\n  // ...\n```\n\n`[Array(9).fill(null)]`은 단일 항목배열로 그 자체가 9개의 `null`의 배열이라는 점에 유의하세요.\n\n현재 이동에 대한 사각형을 렌더링하려면 `history`에서 마지막 사각형의 배열을 읽어야 합니다. 렌더링 중에 계산할 수 있는 충분한 정보가 이미 있으므로 `useState`는 필요하지 않습니다.\n\n```js {4}\nexport default function Game() {\n  const [xIsNext, setXIsNext] = useState(true);\n  const [history, setHistory] = useState([Array(9).fill(null)]);\n  const currentSquares = history[history.length - 1];\n  // ...\n```\n\n다음으로 `Game` 컴포넌트 안에 `Board` 컴포넌트가 게임을 업데이트할 때 호출할 `handlePlay` 함수를 만드세요. `xIsNext` , `currentSquares` , `handlePlay`를 `Board` 컴포넌트에 Props로 전달하세요.\n\n```js {6-8,13}\nexport default function Game() {\n  const [xIsNext, setXIsNext] = useState(true);\n  const [history, setHistory] = useState([Array(9).fill(null)]);\n  const currentSquares = history[history.length - 1];\n\n  function handlePlay(nextSquares) {\n    // TODO\n  }\n\n  return (\n    <div className=\"game\">\n      <div className=\"game-board\">\n        <Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />\n        //...\n  )\n}\n```\n\n`Board` 컴포넌트가 Props에 의해 완전히 제어되도록 만들겠습니다. `Board` 컴포넌트를 `xIsNext`, `squares`, 그리고 플레이어가 움직일 때마다 `Board`가 업데이트된 사각형을 배열로 호출할 수 있는 새로운 `onPlay` 함수를 Props로 받도록 변경하세요. 다음으로 `Board` 함수에서 `useState`를 호출하는 처음 두 줄을 제거하세요.\n\n```js {1}\nfunction Board({ xIsNext, squares, onPlay }) {\n  function handleClick(i) {\n    //...\n  }\n  // ...\n}\n```\n\n이제 `Board` 컴포넌트의 `handleClick`에 있는 `setSquares` 및 `setXIsNext` 호출을 새로운 `onPlay` 함수에 대한 단일 호출로 대체함으로써 사용자가 사각형을 클릭할 때 `Game` 컴포넌트가 `Board`를 업데이트할 수 있습니다.\n\n```js {12}\nfunction Board({ xIsNext, squares, onPlay }) {\n  function handleClick(i) {\n    if (calculateWinner(squares) || squares[i]) {\n      return;\n    }\n    const nextSquares = squares.slice();\n    if (xIsNext) {\n      nextSquares[i] = \"X\";\n    } else {\n      nextSquares[i] = \"O\";\n    }\n    onPlay(nextSquares);\n  }\n  //...\n}\n```\n\n`Board` 컴포넌트는 `Game` 컴포넌트가 전달한 Props에 의해 완전히 제어됩니다. 게임이 다시 작동하게 하려면 `Game` 컴포넌트에서 `handlePlay` 함수를 구현해야 합니다.\n\n`handlePlay`가 호출되면 무엇을 해야 할까요? 이전의 보드는 업데이트된 `setSquares`를 호출했지만, 이제는 업데이트된 `squares` 배열을 `onPlay`로 전달한다는 걸 기억하세요.\n\n`handlePlay` 함수는 리렌더링을 트리거하기 위해 `Game`의 State를 업데이트해야 하지만, 더 이상 호출할 수 있는 `setSquares` 함수가 없으며 대신 이 정보를 저장하기 위해 `history` State 변수를 사용하고 있습니다. 업데이트된 `squares` 배열을 새 히스토리 항목으로 추가하여 `history`를 업데이트해야 하고, `Board`에서 했던 것처럼 `xIsNext` 값을 반전시켜야 합니다.\n\n```js {4-5}\nexport default function Game() {\n  //...\n  function handlePlay(nextSquares) {\n    setHistory([...history, nextSquares]);\n    setXIsNext(!xIsNext);\n  }\n  //...\n}\n```\n\n위에서 `[...history, nextSquares]`는 `history`에 있는 모든 항목을 포함하는 새 배열을 만들고 그 뒤에 `nextSquares`를 만듭니다. (`...history` [*전개 구문*](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Operators/Spread_syntax)을 \"`history` 의 모든 항목 열거\"로 읽을 수 있습니다.)\n\n예를 들어, `history`가 `[[null,null,null], [\"X\",null,null]]`이고 `nextSquares`가 `[\"X\",null,\"O\"]`라면 새로운 `[...history, nextSquares]` 배열은 `[[null,null,null], [\"X\",null,null], [\"X\",null,\"O\"]]`가 될 것입니다.\n\n이 시점에서 State를 `Game` 컴포넌트로 옮겼으므로 리팩토링 전과 마찬가지로 UI가 완전히 작동해야 합니다. 이 시점에서 코드의 모습은 다음과 같습니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { useState } from 'react';\n\nfunction Square({ value, onSquareClick }) {\n  return (\n    <button className=\"square\" onClick={onSquareClick}>\n      {value}\n    </button>\n  );\n}\n\nfunction Board({ xIsNext, squares, onPlay }) {\n  function handleClick(i) {\n    if (calculateWinner(squares) || squares[i]) {\n      return;\n    }\n    const nextSquares = squares.slice();\n    if (xIsNext) {\n      nextSquares[i] = 'X';\n    } else {\n      nextSquares[i] = 'O';\n    }\n    onPlay(nextSquares);\n  }\n\n  const winner = calculateWinner(squares);\n  let status;\n  if (winner) {\n    status = 'Winner: ' + winner;\n  } else {\n    status = 'Next player: ' + (xIsNext ? 'X' : 'O');\n  }\n\n  return (\n    <>\n      <div className=\"status\">{status}</div>\n      <div className=\"board-row\">\n        <Square value={squares[0]} onSquareClick={() => handleClick(0)} />\n        <Square value={squares[1]} onSquareClick={() => handleClick(1)} />\n        <Square value={squares[2]} onSquareClick={() => handleClick(2)} />\n      </div>\n      <div className=\"board-row\">\n        <Square value={squares[3]} onSquareClick={() => handleClick(3)} />\n        <Square value={squares[4]} onSquareClick={() => handleClick(4)} />\n        <Square value={squares[5]} onSquareClick={() => handleClick(5)} />\n      </div>\n      <div className=\"board-row\">\n        <Square value={squares[6]} onSquareClick={() => handleClick(6)} />\n        <Square value={squares[7]} onSquareClick={() => handleClick(7)} />\n        <Square value={squares[8]} onSquareClick={() => handleClick(8)} />\n      </div>\n    </>\n  );\n}\n\nexport default function Game() {\n  const [xIsNext, setXIsNext] = useState(true);\n  const [history, setHistory] = useState([Array(9).fill(null)]);\n  const currentSquares = history[history.length - 1];\n\n  function handlePlay(nextSquares) {\n    setHistory([...history, nextSquares]);\n    setXIsNext(!xIsNext);\n  }\n\n  return (\n    <div className=\"game\">\n      <div className=\"game-board\">\n        <Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />\n      </div>\n      <div className=\"game-info\">\n        <ol>{/*TODO*/}</ol>\n      </div>\n    </div>\n  );\n}\n\nfunction calculateWinner(squares) {\n  const lines = [\n    [0, 1, 2],\n    [3, 4, 5],\n    [6, 7, 8],\n    [0, 3, 6],\n    [1, 4, 7],\n    [2, 5, 8],\n    [0, 4, 8],\n    [2, 4, 6],\n  ];\n  for (let i = 0; i < lines.length; i++) {\n    const [a, b, c] = lines[i];\n    if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {\n      return squares[a];\n    }\n  }\n  return null;\n}\n```\n\n```css src/styles.css\n* {\n  box-sizing: border-box;\n}\n\nbody {\n  font-family: sans-serif;\n  margin: 20px;\n  padding: 0;\n}\n\n.square {\n  background: #fff;\n  border: 1px solid #999;\n  float: left;\n  font-size: 24px;\n  font-weight: bold;\n  line-height: 34px;\n  height: 34px;\n  margin-right: -1px;\n  margin-top: -1px;\n  padding: 0;\n  text-align: center;\n  width: 34px;\n}\n\n.board-row:after {\n  clear: both;\n  content: '';\n  display: table;\n}\n\n.status {\n  margin-bottom: 10px;\n}\n.game {\n  display: flex;\n  flex-direction: row;\n}\n\n.game-info {\n  margin-left: 20px;\n}\n```\n\n</Sandpack>\n\n### 과거 움직임 보여주기 {/*showing-the-past-moves*/}\n\n이제 틱택토 게임의 히스토리를 기록하므로, 플레이어에게 과거 이동 목록을 보여줄 수 있습니다.\n\n`<button>`과 같은 React 엘리먼트는 일반 자바스크립트 객체이므로 애플리케이션에서 전달할 수 있습니다. React에서 여러 엘리먼트를 렌더링하려면 React 엘리먼트 배열을 사용할 수 있습니다.\n\n이미 State에 이동 `history` 배열이 있으므로 이를 React 엘리먼트 배열로 변환해야 합니다. 자바스크립트에서 한 배열을 다른 배열로 변환하려면 [배열 `map` 메서드](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array/map)를 사용하면 됩니다.\n\n```jsx\n[1, 2, 3].map((x) => x * 2) // [2, 4, 6]\n```\n\n`map`을 사용해 이동의 `history`를 화면의 버튼을 나타내는 React 엘리먼트로 변환하고, 과거의 이동으로 \"점프\"할 수 있는 버튼 목록을 표시하세요. `Game` 컴포넌트에서 `history`를 `map` 해보겠습니다.\n\n```js {11-13,15-27,35}\nexport default function Game() {\n  const [xIsNext, setXIsNext] = useState(true);\n  const [history, setHistory] = useState([Array(9).fill(null)]);\n  const currentSquares = history[history.length - 1];\n\n  function handlePlay(nextSquares) {\n    setHistory([...history, nextSquares]);\n    setXIsNext(!xIsNext);\n  }\n\n  function jumpTo(nextMove) {\n    // TODO\n  }\n\n  const moves = history.map((squares, move) => {\n    let description;\n    if (move > 0) {\n      description = 'Go to move #' + move;\n    } else {\n      description = 'Go to game start';\n    }\n    return (\n      <li>\n        <button onClick={() => jumpTo(move)}>{description}</button>\n      </li>\n    );\n  });\n\n  return (\n    <div className=\"game\">\n      <div className=\"game-board\">\n        <Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />\n      </div>\n      <div className=\"game-info\">\n        <ol>{moves}</ol>\n      </div>\n    </div>\n  );\n}\n```\n\n아래에서 코드가 어떻게 보이는지 확인할 수 있습니다. 개발자 도구 콘솔에 다음과 같은 오류 메시지가 표시되어야 합니다:\n\n<ConsoleBlock level=\"warning\">\n경고: 배열 또는 반복자의 각 자식 요소는 고유한 \"key\" 속성을 가져야 합니다. &#96;Game&#96;의 렌더 메서드를 확인하세요.\n</ConsoleBlock>\n\n다음 부문에서 이 오류를 수정하겠습니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { useState } from 'react';\n\nfunction Square({ value, onSquareClick }) {\n  return (\n    <button className=\"square\" onClick={onSquareClick}>\n      {value}\n    </button>\n  );\n}\n\nfunction Board({ xIsNext, squares, onPlay }) {\n  function handleClick(i) {\n    if (calculateWinner(squares) || squares[i]) {\n      return;\n    }\n    const nextSquares = squares.slice();\n    if (xIsNext) {\n      nextSquares[i] = 'X';\n    } else {\n      nextSquares[i] = 'O';\n    }\n    onPlay(nextSquares);\n  }\n\n  const winner = calculateWinner(squares);\n  let status;\n  if (winner) {\n    status = 'Winner: ' + winner;\n  } else {\n    status = 'Next player: ' + (xIsNext ? 'X' : 'O');\n  }\n\n  return (\n    <>\n      <div className=\"status\">{status}</div>\n      <div className=\"board-row\">\n        <Square value={squares[0]} onSquareClick={() => handleClick(0)} />\n        <Square value={squares[1]} onSquareClick={() => handleClick(1)} />\n        <Square value={squares[2]} onSquareClick={() => handleClick(2)} />\n      </div>\n      <div className=\"board-row\">\n        <Square value={squares[3]} onSquareClick={() => handleClick(3)} />\n        <Square value={squares[4]} onSquareClick={() => handleClick(4)} />\n        <Square value={squares[5]} onSquareClick={() => handleClick(5)} />\n      </div>\n      <div className=\"board-row\">\n        <Square value={squares[6]} onSquareClick={() => handleClick(6)} />\n        <Square value={squares[7]} onSquareClick={() => handleClick(7)} />\n        <Square value={squares[8]} onSquareClick={() => handleClick(8)} />\n      </div>\n    </>\n  );\n}\n\nexport default function Game() {\n  const [xIsNext, setXIsNext] = useState(true);\n  const [history, setHistory] = useState([Array(9).fill(null)]);\n  const currentSquares = history[history.length - 1];\n\n  function handlePlay(nextSquares) {\n    setHistory([...history, nextSquares]);\n    setXIsNext(!xIsNext);\n  }\n\n  function jumpTo(nextMove) {\n    // TODO\n  }\n\n  const moves = history.map((squares, move) => {\n    let description;\n    if (move > 0) {\n      description = 'Go to move #' + move;\n    } else {\n      description = 'Go to game start';\n    }\n    return (\n      <li>\n        <button onClick={() => jumpTo(move)}>{description}</button>\n      </li>\n    );\n  });\n\n  return (\n    <div className=\"game\">\n      <div className=\"game-board\">\n        <Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />\n      </div>\n      <div className=\"game-info\">\n        <ol>{moves}</ol>\n      </div>\n    </div>\n  );\n}\n\nfunction calculateWinner(squares) {\n  const lines = [\n    [0, 1, 2],\n    [3, 4, 5],\n    [6, 7, 8],\n    [0, 3, 6],\n    [1, 4, 7],\n    [2, 5, 8],\n    [0, 4, 8],\n    [2, 4, 6],\n  ];\n  for (let i = 0; i < lines.length; i++) {\n    const [a, b, c] = lines[i];\n    if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {\n      return squares[a];\n    }\n  }\n  return null;\n}\n```\n\n```css src/styles.css\n* {\n  box-sizing: border-box;\n}\n\nbody {\n  font-family: sans-serif;\n  margin: 20px;\n  padding: 0;\n}\n\n.square {\n  background: #fff;\n  border: 1px solid #999;\n  float: left;\n  font-size: 24px;\n  font-weight: bold;\n  line-height: 34px;\n  height: 34px;\n  margin-right: -1px;\n  margin-top: -1px;\n  padding: 0;\n  text-align: center;\n  width: 34px;\n}\n\n.board-row:after {\n  clear: both;\n  content: '';\n  display: table;\n}\n\n.status {\n  margin-bottom: 10px;\n}\n\n.game {\n  display: flex;\n  flex-direction: row;\n}\n\n.game-info {\n  margin-left: 20px;\n}\n```\n\n</Sandpack>\n\n`map`으로 `history` 배열을 반복할 때 전달한 함수 내에서 `squares` 인수는 `history`의 각 엘리먼트를 통과하고, `move` 인수는 각 배열 인덱스를 통과합니다: `0`, `1`, `2`, … (대부분은 실제 배열 엘리먼트가 필요하지만, 이 경우에는 이동 목록을 렌더링하기 위해 인덱스만 있어도 됩니다.)\n\n틱택토 게임 `history`의 각 이동에 대해 버튼 `<button>`이 포함된 목록 항목 `<li>`를 생성하세요. 버튼에는 (아직 구현하지 않은) `jumpTo`라는 함수를 호출하는 `onClick` 핸들러가 있습니다.\n\n현재로서는 개발자 도구 콘솔에 게임의 발생한 동작 목록과 오류가 표시되어야 합니다. \"key\" 오류가 무엇을 의미하는지 알아보겠습니다.\n\n### Key 선택하기 {/*picking-a-key*/}\n\n리스트를 렌더링할 때 React는 렌더링된 각 리스트 항목에 대한 몇 가지 정보를 저장합니다. 리스트를 업데이트할 때 React는 무엇이 변경되었는지 확인해야 합니다. 리스트의 항목은 추가, 제거, 재정렬 또는 업데이트될 수 있습니다.\n\n아래의 리스트가\n\n```html\n<li>Alexa: 7 tasks left</li>\n<li>Ben: 5 tasks left</li>\n```\n\n다음과 같이 변한다고 상상해 보세요.\n\n```html\n<li>Ben: 9 tasks left</li>\n<li>Claudia: 8 tasks left</li>\n<li>Alexa: 5 tasks left</li>\n```\n\n아마 task의 개수가 업데이트 되었을 뿐만 아니라 Alexa와 Ben의 순서가 바뀌고 Claudia가 두 사람 사이에 추가되었다고 생각할 것입니다. 그러나 React는 컴퓨터 프로그램이므로 우리가 의도한 바가 무엇인지 알지 못합니다. 그러므로 리스트의 항목에 _`key`_ 프로퍼티를 지정하여 각 리스트의 항목이 다른 항목과 다르다는 것을 구별해 주어야 합니다. 만약 데이터베이스에서 데이터를 불러와서 사용한다면 Alexa, Ben, Claudia의 데이터베이스 ID를 `key`로 사용할 수 있습니다.\n\n```js {1}\n<li key={user.id}>\n  {user.name}: {user.taskCount} tasks left\n</li>\n```\n\n리스트가 다시 렌더링 되면 React는 각 리스트 항목의 `key`를 가져와서 이전 리스트의 항목에서 일치하는 `key`를 탐색합니다. 현재 리스트에서 이전에 존재하지 않았던 `key`가 있으면 React는 컴포넌트를 생성합니다. 만약 현재 리스트에 이전 리스트에 존재했던 `key`를 가지고 있지 않다면 React는 그 `key`를 가진 컴포넌트를 제거합니다. 두 `key`가 일치한다면 해당 컴포넌트는 이동합니다.\n\n`key`는 React가 각 컴포넌트를 구별할 수 있도록 하여 컴포넌트가 다시 렌더링 될 때 React가 해당 컴포넌트의 State를 유지할 수 있게 합니다. 컴포넌트의 `key`가 변하면 컴포넌트는 제거되고 새로운 State와 함께 다시 생성됩니다.\n\n`key`는 React에서 특별하고 미리 지정된 프로퍼티입니다. 엘리먼트가 생성되면 React는 `key` 프로퍼티를 추출하여 반환되는 엘리먼트에 직접 `key`를 저장합니다. `key`가 Props로 전달되는 것처럼 보일 수 있지만, React는 자동으로 `key`를 사용해 업데이트할 컴포넌트를 결정합니다. 부모가 지정한 `key`가 무엇인지 컴포넌트는 알 수 없습니다.\n\n**동적인 리스트를 만들 때마다 적절한 `key`를 할당하는 것을 강력하게 추천합니다.** 적절한 `key`가 없는 경우 데이터를 재구성하는 것을 고려해 보세요.\n\n`key`가 지정되지 않은 경우, React는 경고를 표시하며 배열의 인덱스를 기본 `key`로 사용합니다. 배열 인덱스를 `key`로 사용하면 리스트 항목의 순서를 바꾸거나 항목을 추가/제거할 때 문제가 발생합니다. 명시적으로 `key={i}`를 전달하면 경고는 사라지지만 배열의 인덱스를 사용할 때와 같은 문제가 발생하므로 대부분은 추천하지 않습니다.\n\n`key`는 전역적으로 고유할 필요는 없으며 컴포넌트와 해당 컴포넌트의 형제 컴포넌트 사이에서만 고유하면 됩니다.\n\n### 시간여행 구현하기 {/*implementing-time-travel*/}\n\n틱택토 게임의 기록에서 과거의 각 이동에는 해당 이동의 일련번호인 고유 ID가 있습니다. 이동은 중간에 순서를 바꾸거나 삭제하거나 삽입할 수 없으므로 이동 인덱스를 `key`로 사용하는 것이 안전합니다.\n\n`Game` 함수에서 `<li key={move}>`로 `key`를 추가할 수 있으며 렌더링된 게임을 다시 로드하면 React의 \"key\" 에러가 사라질 것입니다.\n\n```js {4}\nconst moves = history.map((squares, move) => {\n  //...\n  return (\n    <li key={move}>\n      <button onClick={() => jumpTo(move)}>{description}</button>\n    </li>\n  );\n});\n```\n\n<Sandpack>\n\n```js src/App.js\nimport { useState } from 'react';\n\nfunction Square({ value, onSquareClick }) {\n  return (\n    <button className=\"square\" onClick={onSquareClick}>\n      {value}\n    </button>\n  );\n}\n\nfunction Board({ xIsNext, squares, onPlay }) {\n  function handleClick(i) {\n    if (calculateWinner(squares) || squares[i]) {\n      return;\n    }\n    const nextSquares = squares.slice();\n    if (xIsNext) {\n      nextSquares[i] = 'X';\n    } else {\n      nextSquares[i] = 'O';\n    }\n    onPlay(nextSquares);\n  }\n\n  const winner = calculateWinner(squares);\n  let status;\n  if (winner) {\n    status = 'Winner: ' + winner;\n  } else {\n    status = 'Next player: ' + (xIsNext ? 'X' : 'O');\n  }\n\n  return (\n    <>\n      <div className=\"status\">{status}</div>\n      <div className=\"board-row\">\n        <Square value={squares[0]} onSquareClick={() => handleClick(0)} />\n        <Square value={squares[1]} onSquareClick={() => handleClick(1)} />\n        <Square value={squares[2]} onSquareClick={() => handleClick(2)} />\n      </div>\n      <div className=\"board-row\">\n        <Square value={squares[3]} onSquareClick={() => handleClick(3)} />\n        <Square value={squares[4]} onSquareClick={() => handleClick(4)} />\n        <Square value={squares[5]} onSquareClick={() => handleClick(5)} />\n      </div>\n      <div className=\"board-row\">\n        <Square value={squares[6]} onSquareClick={() => handleClick(6)} />\n        <Square value={squares[7]} onSquareClick={() => handleClick(7)} />\n        <Square value={squares[8]} onSquareClick={() => handleClick(8)} />\n      </div>\n    </>\n  );\n}\n\nexport default function Game() {\n  const [xIsNext, setXIsNext] = useState(true);\n  const [history, setHistory] = useState([Array(9).fill(null)]);\n  const currentSquares = history[history.length - 1];\n\n  function handlePlay(nextSquares) {\n    setHistory([...history, nextSquares]);\n    setXIsNext(!xIsNext);\n  }\n\n  function jumpTo(nextMove) {\n    // TODO\n  }\n\n  const moves = history.map((squares, move) => {\n    let description;\n    if (move > 0) {\n      description = 'Go to move #' + move;\n    } else {\n      description = 'Go to game start';\n    }\n    return (\n      <li key={move}>\n        <button onClick={() => jumpTo(move)}>{description}</button>\n      </li>\n    );\n  });\n\n  return (\n    <div className=\"game\">\n      <div className=\"game-board\">\n        <Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />\n      </div>\n      <div className=\"game-info\">\n        <ol>{moves}</ol>\n      </div>\n    </div>\n  );\n}\n\nfunction calculateWinner(squares) {\n  const lines = [\n    [0, 1, 2],\n    [3, 4, 5],\n    [6, 7, 8],\n    [0, 3, 6],\n    [1, 4, 7],\n    [2, 5, 8],\n    [0, 4, 8],\n    [2, 4, 6],\n  ];\n  for (let i = 0; i < lines.length; i++) {\n    const [a, b, c] = lines[i];\n    if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {\n      return squares[a];\n    }\n  }\n  return null;\n}\n\n```\n\n```css src/styles.css\n* {\n  box-sizing: border-box;\n}\n\nbody {\n  font-family: sans-serif;\n  margin: 20px;\n  padding: 0;\n}\n\n.square {\n  background: #fff;\n  border: 1px solid #999;\n  float: left;\n  font-size: 24px;\n  font-weight: bold;\n  line-height: 34px;\n  height: 34px;\n  margin-right: -1px;\n  margin-top: -1px;\n  padding: 0;\n  text-align: center;\n  width: 34px;\n}\n\n.board-row:after {\n  clear: both;\n  content: '';\n  display: table;\n}\n\n.status {\n  margin-bottom: 10px;\n}\n\n.game {\n  display: flex;\n  flex-direction: row;\n}\n\n.game-info {\n  margin-left: 20px;\n}\n```\n\n</Sandpack>\n\n`jumpTo`를 구현하기 전에 사용자가 현재 어떤 단계를 보고 있는지를 추적할 수 있는 `Game` 컴포넌트가 필요합니다. 이를 위해 기본값이 `0`인 `currentMove` 라는 새 State 변수를 정의하세요.\n\n```js {4}\nexport default function Game() {\n  const [xIsNext, setXIsNext] = useState(true);\n  const [history, setHistory] = useState([Array(9).fill(null)]);\n  const [currentMove, setCurrentMove] = useState(0);\n  const currentSquares = history[history.length - 1];\n  //...\n}\n```\n\n다음으로 `Game` 내부의 `jumpTo` 함수를 업데이트하여 해당 `currentMove`를 업데이트하세요. 또한 `currentMove`를 변경하는 숫자가 짝수면 `xIsNext`를 `true`로 설정하세요.\n\n```js {4-5}\nexport default function Game() {\n  // ...\n  function jumpTo(nextMove) {\n    setCurrentMove(nextMove);\n    setXIsNext(nextMove % 2 === 0);\n  }\n  //...\n}\n```\n\n이제 사각형을 클릭할 때 호출되는 `Game`의 `handlePlay` 함수 내용을 두 가지 변경하겠습니다.\n\n- \"시간을 거슬러 올라가서\" 그 시점에서 새로운 이동을 하는 경우 해당 시점까지의 히스토리만 유지해야 합니다. `history`의 모든 항목(`...` 전개 구문) 뒤에 `nextSquares`를 추가하는 대신 `history.slice(0, currentMove + 1)`의 모든 항목 뒤에 추가하여 이전 히스토리의 해당 부분만 유지하도록 하겠습니다.\n- 이동할 때마다 최신 히스토리 항목을 가리키도록 `currentMove`를 업데이트하세요.\n\n```js {2-4}\nfunction handlePlay(nextSquares) {\n  const nextHistory = [...history.slice(0, currentMove + 1), nextSquares];\n  setHistory(nextHistory);\n  setCurrentMove(nextHistory.length - 1);\n  setXIsNext(!xIsNext);\n}\n```\n\n마지막으로 항상 마지막 동작을 렌더링하는 대신 현재 선택한 동작을 렌더링하도록 `Game` 컴포넌트를 수정하겠습니다.\n\n```js {5}\nexport default function Game() {\n  const [xIsNext, setXIsNext] = useState(true);\n  const [history, setHistory] = useState([Array(9).fill(null)]);\n  const [currentMove, setCurrentMove] = useState(0);\n  const currentSquares = history[currentMove];\n\n  // ...\n}\n```\n\n게임 히스토리의 특정 단계를 클릭하면 틱택토 보드가 즉시 업데이트되어 해당 단계가 발생한 시점의 보드 모양이 표시됩니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { useState } from 'react';\n\nfunction Square({value, onSquareClick}) {\n  return (\n    <button className=\"square\" onClick={onSquareClick}>\n      {value}\n    </button>\n  );\n}\n\nfunction Board({ xIsNext, squares, onPlay }) {\n  function handleClick(i) {\n    if (calculateWinner(squares) || squares[i]) {\n      return;\n    }\n    const nextSquares = squares.slice();\n    if (xIsNext) {\n      nextSquares[i] = 'X';\n    } else {\n      nextSquares[i] = 'O';\n    }\n    onPlay(nextSquares);\n  }\n\n  const winner = calculateWinner(squares);\n  let status;\n  if (winner) {\n    status = 'Winner: ' + winner;\n  } else {\n    status = 'Next player: ' + (xIsNext ? 'X' : 'O');\n  }\n\n  return (\n    <>\n      <div className=\"status\">{status}</div>\n      <div className=\"board-row\">\n        <Square value={squares[0]} onSquareClick={() => handleClick(0)} />\n        <Square value={squares[1]} onSquareClick={() => handleClick(1)} />\n        <Square value={squares[2]} onSquareClick={() => handleClick(2)} />\n      </div>\n      <div className=\"board-row\">\n        <Square value={squares[3]} onSquareClick={() => handleClick(3)} />\n        <Square value={squares[4]} onSquareClick={() => handleClick(4)} />\n        <Square value={squares[5]} onSquareClick={() => handleClick(5)} />\n      </div>\n      <div className=\"board-row\">\n        <Square value={squares[6]} onSquareClick={() => handleClick(6)} />\n        <Square value={squares[7]} onSquareClick={() => handleClick(7)} />\n        <Square value={squares[8]} onSquareClick={() => handleClick(8)} />\n      </div>\n    </>\n  );\n}\n\nexport default function Game() {\n  const [xIsNext, setXIsNext] = useState(true);\n  const [history, setHistory] = useState([Array(9).fill(null)]);\n  const [currentMove, setCurrentMove] = useState(0);\n  const currentSquares = history[currentMove];\n\n  function handlePlay(nextSquares) {\n    const nextHistory = [...history.slice(0, currentMove + 1), nextSquares];\n    setHistory(nextHistory);\n    setCurrentMove(nextHistory.length - 1);\n    setXIsNext(!xIsNext);\n  }\n\n  function jumpTo(nextMove) {\n    setCurrentMove(nextMove);\n    setXIsNext(nextMove % 2 === 0);\n  }\n\n  const moves = history.map((squares, move) => {\n    let description;\n    if (move > 0) {\n      description = 'Go to move #' + move;\n    } else {\n      description = 'Go to game start';\n    }\n    return (\n      <li key={move}>\n        <button onClick={() => jumpTo(move)}>{description}</button>\n      </li>\n    );\n  });\n\n  return (\n    <div className=\"game\">\n      <div className=\"game-board\">\n        <Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />\n      </div>\n      <div className=\"game-info\">\n        <ol>{moves}</ol>\n      </div>\n    </div>\n  );\n}\n\nfunction calculateWinner(squares) {\n  const lines = [\n    [0, 1, 2],\n    [3, 4, 5],\n    [6, 7, 8],\n    [0, 3, 6],\n    [1, 4, 7],\n    [2, 5, 8],\n    [0, 4, 8],\n    [2, 4, 6],\n  ];\n  for (let i = 0; i < lines.length; i++) {\n    const [a, b, c] = lines[i];\n    if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {\n      return squares[a];\n    }\n  }\n  return null;\n}\n```\n\n```css src/styles.css\n* {\n  box-sizing: border-box;\n}\n\nbody {\n  font-family: sans-serif;\n  margin: 20px;\n  padding: 0;\n}\n\n.square {\n  background: #fff;\n  border: 1px solid #999;\n  float: left;\n  font-size: 24px;\n  font-weight: bold;\n  line-height: 34px;\n  height: 34px;\n  margin-right: -1px;\n  margin-top: -1px;\n  padding: 0;\n  text-align: center;\n  width: 34px;\n}\n\n.board-row:after {\n  clear: both;\n  content: '';\n  display: table;\n}\n\n.status {\n  margin-bottom: 10px;\n}\n.game {\n  display: flex;\n  flex-direction: row;\n}\n\n.game-info {\n  margin-left: 20px;\n}\n```\n\n</Sandpack>\n\n### 최종 정리 {/*final-cleanup*/}\n\n코드를 자세히 살펴보면 `currentMove`가 짝수일 때는 `xIsNext === true`가 되고, `currentMove`가 홀수일 때는 `xIsNext === false`가 되는 것을 알 수 있습니다. 즉, `currentMove`의 값을 알고 있다면 언제나 `xIsNext`가 무엇인지 알아낼 수 있습니다.\n\n이 두 가지 State를 모두 저장할 이유가 없습니다. 항상 중복되는 State는 피하세요. State에 저장하는 것을 단순화하면 버그를 줄이고 코드를 더 쉽게 이해할 수 있습니다. `Game`을 변경하여 더 이상 `xIsNext`를 별도의 State 변수로 저장하지 않고 `currentMove`를 기반으로 알아내도록 수정하겠습니다.\n\n```js {4,10,14}\nexport default function Game() {\n  const [history, setHistory] = useState([Array(9).fill(null)]);\n  const [currentMove, setCurrentMove] = useState(0);\n  const xIsNext = currentMove % 2 === 0;\n  const currentSquares = history[currentMove];\n\n  function handlePlay(nextSquares) {\n    const nextHistory = [...history.slice(0, currentMove + 1), nextSquares];\n    setHistory(nextHistory);\n    setCurrentMove(nextHistory.length - 1);\n  }\n\n  function jumpTo(nextMove) {\n    setCurrentMove(nextMove);\n  }\n  // ...\n}\n```\n\n더 이상 `xIsNext` State 선언이나 `setXIsNext` 호출이 필요하지 않습니다. 이제 컴포넌트를 코딩하는 동안 실수를 하더라도 `xIsNext`가 `currentMove`와 동기화되지 않을 가능성이 없습니다.\n\n### 마무리 {/*wrapping-up*/}\n\n축하합니다! 여러분은 틱택토 게임을 만들었습니다.\n\n- 틱택토를 플레이할 수 있습니다.\n- 플레이어가 게임에서 이겼을 때를 표시합니다.\n- 게임이 진행됨에 따라 히스토리를 저장합니다.\n- 플레이어가 게임 히스토리를 검토하고 게임 보드의 이전 버전을 볼 수 있습니다.\n\n수고하셨습니다! 이제 React가 어떻게 작동하는지 어느 정도 이해하셨기를 바랍니다.\n\n최종 결과물을 아래에서 확인하세요.\n\n<Sandpack>\n\n```js src/App.js\nimport { useState } from 'react';\n\nfunction Square({ value, onSquareClick }) {\n  return (\n    <button className=\"square\" onClick={onSquareClick}>\n      {value}\n    </button>\n  );\n}\n\nfunction Board({ xIsNext, squares, onPlay }) {\n  function handleClick(i) {\n    if (calculateWinner(squares) || squares[i]) {\n      return;\n    }\n    const nextSquares = squares.slice();\n    if (xIsNext) {\n      nextSquares[i] = 'X';\n    } else {\n      nextSquares[i] = 'O';\n    }\n    onPlay(nextSquares);\n  }\n\n  const winner = calculateWinner(squares);\n  let status;\n  if (winner) {\n    status = 'Winner: ' + winner;\n  } else {\n    status = 'Next player: ' + (xIsNext ? 'X' : 'O');\n  }\n\n  return (\n    <>\n      <div className=\"status\">{status}</div>\n      <div className=\"board-row\">\n        <Square value={squares[0]} onSquareClick={() => handleClick(0)} />\n        <Square value={squares[1]} onSquareClick={() => handleClick(1)} />\n        <Square value={squares[2]} onSquareClick={() => handleClick(2)} />\n      </div>\n      <div className=\"board-row\">\n        <Square value={squares[3]} onSquareClick={() => handleClick(3)} />\n        <Square value={squares[4]} onSquareClick={() => handleClick(4)} />\n        <Square value={squares[5]} onSquareClick={() => handleClick(5)} />\n      </div>\n      <div className=\"board-row\">\n        <Square value={squares[6]} onSquareClick={() => handleClick(6)} />\n        <Square value={squares[7]} onSquareClick={() => handleClick(7)} />\n        <Square value={squares[8]} onSquareClick={() => handleClick(8)} />\n      </div>\n    </>\n  );\n}\n\nexport default function Game() {\n  const [history, setHistory] = useState([Array(9).fill(null)]);\n  const [currentMove, setCurrentMove] = useState(0);\n  const xIsNext = currentMove % 2 === 0;\n  const currentSquares = history[currentMove];\n\n  function handlePlay(nextSquares) {\n    const nextHistory = [...history.slice(0, currentMove + 1), nextSquares];\n    setHistory(nextHistory);\n    setCurrentMove(nextHistory.length - 1);\n  }\n\n  function jumpTo(nextMove) {\n    setCurrentMove(nextMove);\n  }\n\n  const moves = history.map((squares, move) => {\n    let description;\n    if (move > 0) {\n      description = 'Go to move #' + move;\n    } else {\n      description = 'Go to game start';\n    }\n    return (\n      <li key={move}>\n        <button onClick={() => jumpTo(move)}>{description}</button>\n      </li>\n    );\n  });\n\n  return (\n    <div className=\"game\">\n      <div className=\"game-board\">\n        <Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />\n      </div>\n      <div className=\"game-info\">\n        <ol>{moves}</ol>\n      </div>\n    </div>\n  );\n}\n\nfunction calculateWinner(squares) {\n  const lines = [\n    [0, 1, 2],\n    [3, 4, 5],\n    [6, 7, 8],\n    [0, 3, 6],\n    [1, 4, 7],\n    [2, 5, 8],\n    [0, 4, 8],\n    [2, 4, 6],\n  ];\n  for (let i = 0; i < lines.length; i++) {\n    const [a, b, c] = lines[i];\n    if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {\n      return squares[a];\n    }\n  }\n  return null;\n}\n```\n\n```css src/styles.css\n* {\n  box-sizing: border-box;\n}\n\nbody {\n  font-family: sans-serif;\n  margin: 20px;\n  padding: 0;\n}\n\n.square {\n  background: #fff;\n  border: 1px solid #999;\n  float: left;\n  font-size: 24px;\n  font-weight: bold;\n  line-height: 34px;\n  height: 34px;\n  margin-right: -1px;\n  margin-top: -1px;\n  padding: 0;\n  text-align: center;\n  width: 34px;\n}\n\n.board-row:after {\n  clear: both;\n  content: '';\n  display: table;\n}\n\n.status {\n  margin-bottom: 10px;\n}\n.game {\n  display: flex;\n  flex-direction: row;\n}\n\n.game-info {\n  margin-left: 20px;\n}\n```\n\n</Sandpack>\n\n시간이 남거나 새로운 React 기술을 연습하고 싶다면 아래에 틱택토 게임을 개선할 수 있는 몇 가지 아이디어가 있습니다. 아이디어는 난이도가 낮은 순으로 정렬되어 있습니다.\n\n1. 현재 이동에 대해서만 버튼 대신 \"당신은 #번째 순서에 있습니다…\"를 표시해 보세요.\n1. `Board`를 하드 코딩 하는 대신 두 개의 루프를 사용하여 사각형을 만들도록 다시 작성해 보세요.\n1. 동작을 오름차순 또는 내림차순으로 정렬할 수 있는 토글 버튼을 추가해 보세요.\n1. 누군가 승리하면 승리의 원인이 된 세 개의 사각형을 강조 표시해 보세요. (아무도 승리하지 않으면 무승부라는 메시지를 표시하세요. )\n1. 이동 히스토리 목록에서 각 이동의 위치를 형식(열, 행)으로 표시해 보세요.\n\n이 자습서를 통해 엘리먼트, 컴포넌트, Props, State를 포함한 React의 개념에 대해 살펴봤습니다. 이제 이러한 개념이 게임을 만들 때 어떻게 작동하는지 보았으니, [React로 사고하기](/learn/thinking-in-react)를 통해 앱의 UI를 만들 때 동일한 React 개념이 어떻게 작동하는지 확인해 보세요.\n"
  },
  {
    "path": "src/content/learn/typescript.md",
    "content": "---\ntitle: TypeScript 사용하기\nre: https://github.com/reactjs/react.dev/issues/5960\n---\n\n<Intro>\n\nTypeScript는 JavaScript 코드 베이스에 타입 정의를 추가하기 위해 널리 사용하는 방법입니다. 기본적으로 TypeScript는 [JSX를 지원](/learn/writing-markup-with-jsx)하며, [`@types/react`](https://www.npmjs.com/package/@types/react) 및 [`@types/react-dom`](https://www.npmjs.com/package/@types/react-dom)을 추가하여 완전한 React Web 지원을 받을 수 있습니다.\n\n</Intro>\n\n<YouWillLearn>\n\n* [React 컴포넌트가 있는 TypeScript](/learn/typescript#typescript-with-react-components)\n* [Hook 타입 정의 예시](/learn/typescript#example-hooks)\n* [`@types/react`의 일반적인 타입](/learn/typescript#useful-types)\n* [추가 학습](/learn/typescript#further-learning)\n\n</YouWillLearn>\n\n## 설치 {/*installation*/}\n\n모든 [프로덕션 수준의 React 프레임워크](/learn/creating-a-react-app#full-stack-frameworks)는 TypeScript 사용을 지원합니다. 프레임워크별 설치 가이드를 따르세요.\n\n- [Next.js](https://nextjs.org/docs/app/building-your-application/configuring/typescript)\n- [Remix](https://remix.run/docs/en/1.19.2/guides/typescript)\n- [Gatsby](https://www.gatsbyjs.com/docs/how-to/custom-configuration/typescript/)\n- [Expo](https://docs.expo.dev/guides/typescript/)\n\n### 기존 React 프로젝트에 TypeScript 추가하기 {/*adding-typescript-to-an-existing-react-project*/}\n\n최신 버전의 React 타입 정의를 설치합니다.\n\n<TerminalBlock>\nnpm install --save-dev @types/react @types/react-dom\n</TerminalBlock>\n\n다음 컴파일러 옵션들을 `tsconfig.json`에 설정해야 합니다.\n\n1. `dom`은 [`lib`](https://www.typescriptlang.org/ko/tsconfig/#lib)에 포함되어야 합니다. (주의: `lib` 옵션을 지정하지 않으면, 기본적으로 `dom`을 포함합니다.)\n1. [`jsx`](https://www.typescriptlang.org/ko/tsconfig/#jsx)를 유효한 옵션 중 하나로 설정해야 합니다. 대부분의 애플리케이션에서는 `preserve`로 충분합니다.\n  라이브러리를 게시하는 경우, 어떤 값을 선택해야 하는지 [`jsx` 설명서](https://www.typescriptlang.org/ko/tsconfig/#jsx)를 참조하세요.\n\n## React 컴포넌트가 있는 TypeScript {/*typescript-with-react-components*/}\n\n<Note>\n\nJSX를 포함하고 있는 모든 파일들은 `.tsx` 파일 확장자를 사용해야 합니다. 이는 해당 파일이 JSX를 포함하고 있음을 TypeScript에 알려주는 TypeScript 전용 확장자입니다.\n\n</Note>\n\nReact와 함께 TypeScript를 작성하는 것은 React와 함께 JavaScript를 작성하는 것과 매우 유사합니다. 컴포넌트로 작업할 때 가장 중요한 차이점은 컴포넌트의 Props에 타입을 제공할 수 있다는 점입니다. 이러한 타입은 에디터에서 정확성을 검사하고 인라인 문서를 제공하는 데 사용할 수 있습니다.\n\n[빠르게 시작하기](/learn) 가이드에서 가져온 [`MyButton` 컴포넌트](/learn#components)를 예로 들어 버튼의 `title`을 설명하는 타입을 추가할 수 있습니다.\n\n<Sandpack>\n\n```tsx src/App.tsx active\nfunction MyButton({ title }: { title: string }) {\n  return (\n    <button>{title}</button>\n  );\n}\n\nexport default function MyApp() {\n  return (\n    <div>\n      <h1>Welcome to my app</h1>\n      <MyButton title=\"I'm a button\" />\n    </div>\n  );\n}\n```\n\n```js src/App.js hidden\nimport AppTSX from \"./App.tsx\";\nexport default App = AppTSX;\n```\n</Sandpack>\n\n<Note>\n\n이 문서에 있는 샌드박스들은 TypeScript 코드를 다룰 수는 있지만 타입을 검사하지는 않습니다. 즉, TypeScript 샌드박스를 수정하여 학습할 수는 있지만, 타입 오류나 경고는 발생하지 않습니다. 타입 검사를 받으려면, [TypeScript Playground](https://www.typescriptlang.org/ko/play)를 사용하거나 더 완전한 기능을 갖춘 온라인 샌드박스를 사용할 수 있습니다.\n\n</Note>\n\n이 인라인 문법은 컴포넌트에 타입을 제공하는 가장 간단한 방법이지만, 설명할 필드가 많아지기 시작하면 다루기 어려워질 수 있습니다. 대신, `interface`나 `type`을 사용하여 컴포넌트의 Props를 설명할 수 있습니다.\n\n<Sandpack>\n\n```tsx src/App.tsx active\ninterface MyButtonProps {\n  /** 버튼 안에 보여질 텍스트 */\n  title: string;\n  /** 버튼이 상호작용할 수 있는지 여부 */\n  disabled: boolean;\n}\n\nfunction MyButton({ title, disabled }: MyButtonProps) {\n  return (\n    <button disabled={disabled}>{title}</button>\n  );\n}\n\nexport default function MyApp() {\n  return (\n    <div>\n      <h1>Welcome to my app</h1>\n      <MyButton title=\"I'm a disabled button\" disabled={true}/>\n    </div>\n  );\n}\n```\n\n```js src/App.js hidden\nimport AppTSX from \"./App.tsx\";\nexport default App = AppTSX;\n```\n\n</Sandpack>\n\n컴포넌트의 Props를 설명하는 타입은 원하는 만큼 단순하거나 복잡할 수 있지만, `type` 또는 `interface`로 설명되는 객체 타입이어야 합니다. TypeScript가 객체를 설명하는 방법에 대해 [객체 타입](https://www.typescriptlang.org/docs/handbook/2/objects.html)에서 배울 수 있습니다만, [유니언 타입](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#union-types)을 사용하여 몇 가지 타입 중 하나가 될 수 있는 Prop을 설명하는 것과 더 고급 사용 예시에 대한 [타입에서 타입 만들기](https://www.typescriptlang.org/docs/handbook/2/types-from-types.html) 가이드 역시 흥미로울 것입니다.\n\n## Hook 타입 정의 예시 {/*example-hooks*/}\n\n`@types/react`의 타입 정의는 내장 Hook에 대한 타입을 포함하고 있으므로 추가 설정 없이 컴포넌트에 사용할 수 있습니다. 컴포넌트에 작성한 코드를 고려하도록 만들었기 때문에 대부분의 경우 [추론된 타입](https://www.typescriptlang.org/ko/docs/handbook/type-inference.html)을 얻을 수 있으며, 이상적으로는 타입을 제공하는 사소한 작업을 처리할 필요가 없습니다.\n\n하지만, Hook에 타입을 제공하는 방법의 몇 가지 예시를 볼 수 있습니다.\n\n### `useState` {/*typing-usestate*/}\n\n[`useState` Hook](/reference/react/useState)은 초기 State로 전달된 값을 재사용하여 값의 타입을 결정합니다. 예를 들어\n\n```ts\n// 타입을 \"boolean\"으로 추론합니다.\nconst [enabled, setEnabled] = useState(false);\n```\n\n`boolean` 타입이 `enabled`에 할당되고, `setEnabled`는 `boolean` 인수나 `boolean`을 반환하는 함수를 받는 함수가 됩니다. State에 대한 타입을 명시적으로 제공하려면 `useState` 호출에 타입 인수를 제공하면 됩니다.\n\n```ts\n// 명시적으로 타입을 \"boolean\"으로 설정합니다.\nconst [enabled, setEnabled] = useState<boolean>(false);\n```\n\n이 경우에는 그다지 유용하지 않지만, 타입 제공을 원하는 일반적인 경우는 유니언<sup>Union</sup> 타입이 있는 경우입니다. 예를 들어, 여기서 `Status`는 몇 가지 다른 문자열 중 하나일 수 있습니다.\n\n```ts\ntype Status = \"idle\" | \"loading\" | \"success\" | \"error\";\n\nconst [status, setStatus] = useState<Status>(\"idle\");\n```\n\n또는 [State 구조화 원칙](/learn/choosing-the-state-structure#principles-for-structuring-state)에서 권장하는 대로, 관련 State를 객체로 그룹화하고 객체 타입을 통해 다른 가능성을 설명할 수 있습니다.\n\n```ts\ntype RequestState =\n  | { status: 'idle' }\n  | { status: 'loading' }\n  | { status: 'success', data: any }\n  | { status: 'error', error: Error };\n\nconst [requestState, setRequestState] = useState<RequestState>({ status: 'idle' });\n```\n\n### `useReducer` {/*typing-usereducer*/}\n\n[`useReducer` Hook](/reference/react/useReducer)은 Reducer 함수와 초기 State를 취하는 더 복잡한 Hook입니다. Reducer 함수의 타입은 초기 State에서 추론됩니다. State에 대한 타입을 제공하기 위해 `useReducer` 호출에 타입 인수를 선택적으로 제공할 수 있지만, 대신 초기 State에서 타입을 설정하는 것이 더 좋은 경우가 많습니다.\n\n<Sandpack>\n\n```tsx src/App.tsx active\nimport {useReducer} from 'react';\n\ninterface State {\n   count: number\n};\n\ntype CounterAction =\n  | { type: \"reset\" }\n  | { type: \"setCount\"; value: State[\"count\"] }\n\nconst initialState: State = { count: 0 };\n\nfunction stateReducer(state: State, action: CounterAction): State {\n  switch (action.type) {\n    case \"reset\":\n      return initialState;\n    case \"setCount\":\n      return { ...state, count: action.value };\n    default:\n      throw new Error(\"Unknown action\");\n  }\n}\n\nexport default function App() {\n  const [state, dispatch] = useReducer(stateReducer, initialState);\n\n  const addFive = () => dispatch({ type: \"setCount\", value: state.count + 5 });\n  const reset = () => dispatch({ type: \"reset\" });\n\n  return (\n    <div>\n      <h1>Welcome to my counter</h1>\n\n      <p>Count: {state.count}</p>\n      <button onClick={addFive}>Add 5</button>\n      <button onClick={reset}>Reset</button>\n    </div>\n  );\n}\n\n```\n\n```js src/App.js hidden\nimport AppTSX from \"./App.tsx\";\nexport default App = AppTSX;\n```\n\n</Sandpack>\n\n\n몇 가지 주요 위치에서 TypeScript를 사용하고 있습니다.\n\n - `interface State`는 Reducer State의 모양을 설명합니다.\n - `type CounterAction`은 Reducer에 Dispatch 할 수 있는 다양한 액션을 설명합니다.\n - `const initialState: State`는 초기 State의 타입을 제공하고, 기본적으로 `useReducer`에서 사용하는 타입도 제공합니다.\n - `stateReducer(state: State, action: CounterAction): State`는 Reducer 함수의 인수와 반환 값의 타입을 설정합니다.\n\n`initialState`에 타입을 설정하는 것보다 더 명시적인 대안은 `useReducer`에 타입 인수를 제공하는 것입니다.\n\n```ts\nimport { stateReducer, State } from './your-reducer-implementation';\n\nconst initialState = { count: 0 };\n\nexport default function App() {\n  const [state, dispatch] = useReducer<State>(stateReducer, initialState);\n}\n```\n\n### `useContext` {/*typing-usecontext*/}\n\n[`useContext` Hook](/reference/react/useContext)은 컴포넌트를 통해 Props를 전달할 필요 없이 컴포넌트 트리를 따라 데이터를 전달하는 기술입니다. Provider 컴포넌트를 생성할 때 사용되며, 종종 자식 컴포넌트에서 값을 소비하는 Hook을 생성할 때 사용됩니다.\n\nContext에서 제공한 값의 타입은 `createContext` 호출에 전달된 값에서 추론됩니다.\n\n<Sandpack>\n\n```tsx src/App.tsx active\nimport { createContext, useContext, useState } from 'react';\n\ntype Theme = \"light\" | \"dark\" | \"system\";\nconst ThemeContext = createContext<Theme>(\"system\");\n\nconst useGetTheme = () => useContext(ThemeContext);\n\nexport default function MyApp() {\n  const [theme, setTheme] = useState<Theme>('light');\n\n  return (\n    <ThemeContext value={theme}>\n      <MyComponent />\n    </ThemeContext>\n  )\n}\n\nfunction MyComponent() {\n  const theme = useGetTheme();\n\n  return (\n    <div>\n      <p>Current theme: {theme}</p>\n    </div>\n  )\n}\n```\n\n```js src/App.js hidden\nimport AppTSX from \"./App.tsx\";\nexport default App = AppTSX;\n```\n\n</Sandpack>\n\n이 기술은 합리적인 기본값이 있을 때 효과적이지만 그렇지 않은 경우도 간혹 있으며, 그러한 경우 `null`이 기본값으로 합리적이라고 느낄 수 있습니다. 그러나, 타입 시스템이 코드를 이해할 수 있도록 하려면 `createContext`에서 `ContextShape | null`을 명시적으로 설정해야 합니다.\n\n이에 따라 Context 소비자에 대한 타입에서 `| null`을 제거해야 하는 문제가 발생합니다. 권장 사항은 Hook이 런타임에 존재 여부를 검사하고 존재하지 않을 경우 에러를 던지는 것입니다.\n\n```js {5, 16-20}\nimport { createContext, useContext, useState, useMemo } from 'react';\n\n// 이것은 더 간단한 예시이지만, 더 복잡한 객체를 상상할 수 있습니다.\ntype ComplexObject = {\n  kind: string\n};\n\n// context는 기본값을 정확하게 반영하기 위해 타입에 `| null`을 사용하여 만들어집니다.\nconst Context = createContext<ComplexObject | null>(null);\n\n// Hook의 검사를 통해 `| null`을 제거합니다.\nconst useGetComplexObject = () => {\n  const object = useContext(Context);\n  if (!object) { throw new Error(\"useGetComplexObject must be used within a Provider\") }\n  return object;\n}\n\nexport default function MyApp() {\n  const object = useMemo(() => ({ kind: \"complex\" }), []);\n\n  return (\n    <Context value={object}>\n      <MyComponent />\n    </Context>\n  )\n}\n\nfunction MyComponent() {\n  const object = useGetComplexObject();\n\n  return (\n    <div>\n      <p>Current object: {object.kind}</p>\n    </div>\n  )\n}\n```\n\n### `useMemo` {/*typing-usememo*/}\n\n<Note>\n\n[React Compiler](/learn/react-compiler) automatically memoizes values and functions, reducing the need for manual `useMemo` calls. You can use the compiler to handle memoization automatically.\n\n</Note>\n\n[`useMemo`](/reference/react/useMemo) Hook은 함수 호출로부터 Memorized 된 값을 생성/재접근하여, 두 번째 매개변수로 전달된 종속성이 변경될 때만 함수를 다시 실행합니다. Hook을 호출한 결과는 첫 번째 매개변수에 있는 함수의 반환 값에서 추론됩니다. Hook에 타입 인수를 제공하여 더욱더 명확하게 할 수 있습니다.\n\n```ts\n// visibleTodos의 타입은 filterTodos의 반환 값에서 추론됩니다.\nconst visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);\n```\n\n\n### `useCallback` {/*typing-usecallback*/}\n\n<Note>\n\n[React Compiler](/learn/react-compiler) automatically memoizes values and functions, reducing the need for manual `useCallback` calls. You can use the compiler to handle memoization automatically.\n\n</Note>\n\n[`useCallback`](/reference/react/useCallback)은 두 번째 매개변수로 전달되는 종속성이 같다면 함수에 대한 안정적인 참조를 제공합니다. `useMemo`와 마찬가지로, 함수의 타입은 첫 번째 매개변수에 있는 함수의 반환 값에서 추론되며, Hook에 타입 인수를 제공하여 더욱더 명확하게 할 수 있습니다.\n\n\n```ts\nconst handleClick = useCallback(() => {\n  // ...\n}, [todos]);\n```\n\nTypeScript strict mode에서 작업할 때 `useCallback`을 사용하려면 콜백에 매개변수를 위한 타입을 추가해야 합니다. 콜백의 타입은 함수의 반환 값에서 추론되고, 매개변수 없이는 타입을 완전히 이해할 수 없기 때문입니다.\n\n코드 스타일 선호도에 따라, 콜백을 정의하는 동시에 이벤트 핸들러의 타입을 제공하기 위해 React 타입의 `*EventHandler` 함수를 사용할 수 있습니다.\n\n```ts\nimport { useState, useCallback } from 'react';\n\nexport default function Form() {\n  const [value, setValue] = useState(\"Change me\");\n\n  const handleChange = useCallback<React.ChangeEventHandler<HTMLInputElement>>((event) => {\n    setValue(event.currentTarget.value);\n  }, [setValue])\n\n  return (\n    <>\n      <input value={value} onChange={handleChange} />\n      <p>Value: {value}</p>\n    </>\n  );\n}\n```\n\n## 유용한 타입들 {/*useful-types*/}\n\n`@types/react` 패키지<sup>Package</sup>에는 상당히 광범위한 타입 집합이 있으며, React와 TypeScript가 상호작용하는 방식에 익숙하다면 읽어볼 가치가 있습니다. [DefinitelyTyped에 있는 React 폴더에서](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/react/index.d.ts) 찾을 수 있습니다. 여기에서는 좀 더 일반적인 타입 몇 가지를 다루겠습니다.\n\n### DOM 이벤트 {/*typing-dom-events*/}\n\nReact에서 DOM 이벤트로 작업할 때, 종종 이벤트 핸들러로부터 이벤트의 타입을 추론할 수 있습니다. 하지만, 이벤트 핸들러에 전달할 함수를 추출하고 싶을 때는 이벤트 타입을 명시적으로 설정해야 합니다.\n\n<Sandpack>\n\n```tsx src/App.tsx active\nimport { useState } from 'react';\n\nexport default function Form() {\n  const [value, setValue] = useState(\"Change me\");\n\n  function handleChange(event: React.ChangeEvent<HTMLInputElement>) {\n    setValue(event.currentTarget.value);\n  }\n\n  return (\n    <>\n      <input value={value} onChange={handleChange} />\n      <p>Value: {value}</p>\n    </>\n  );\n}\n```\n\n```js src/App.js hidden\nimport AppTSX from \"./App.tsx\";\nexport default App = AppTSX;\n```\n\n</Sandpack>\n\nReact 타입에는 많은 이벤트 타입이 있으며, 전체 목록은 [DOM에서 가장 많이 사용되는 이벤트](https://developer.mozilla.org/en-US/docs/Web/Events)를 기반으로 한 [여기](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/b580df54c0819ec9df62b0835a315dd48b8594a9/types/react/index.d.ts#L1247C1-L1373)에서 확인할 수 있습니다.\n\n찾고 있는 타입을 결정할 때 먼저 사용 중인 이벤트 핸들러의 호버<sup>Hover</sup> 정보를 확인하면, 이벤트의 타입이 표시됩니다.\n\n목록에 포함되지 않은 이벤트를 사용해야 한다면, 모든 이벤트의 기본 타입인 `React.SyntheticEvent` 타입을 사용할 수 있습니다.\n\n### Children {/*typing-children*/}\n\n컴포넌트의 자식을 설명하는 데는 두 가지 일반적인 경로가 있습니다. 첫 번째는 JSX에서 자식으로 전달할 수 있는 모든 가능한 타입의 조합(union)인 `React.ReactNode` 타입을 사용하는 것입니다.\n\n```ts\ninterface ModalRendererProps {\n  title: string;\n  children: React.ReactNode;\n}\n```\n\n이것은 자식에 대해 매우 광범위한 정의입니다. 두 번째는 `string`이나 `number` 같은 JavaScript 원시 값<sup>Primitive</sup>이 아닌 JSX 엘리먼트만 있는 `React.ReactElement` 타입을 사용하는 것입니다.\n\n```ts\ninterface ModalRendererProps {\n  title: string;\n  children: React.ReactElement;\n}\n```\n\n자식이 특정 JSX 엘리먼트 타입이라고 설명하기 위해 TypeScript를 사용할 수 없으므로, `<li>` 자식만 허용하는 컴포넌트를 설명하기 위해 타입 시스템을 사용할 수 없다는 점에 주의하세요.\n\n[TypeScript 플레이그라운드](https://www.typescriptlang.org/ko/play?#code/JYWwDg9gTgLgBAJQKYEMDG8BmUIjgIilQ3wChSB6CxYmAOmXRgDkIATJOdNJMGAZzgwAFpxAR+8YADswAVwGkZMJFEzpOjDKw4AFHGEEBvUnDhphwADZsi0gFw0mDWjqQBuUgF9yaCNMlENzgAXjgACjADfkctFnYkfQhDAEpQgD44AB42YAA3dKMo5P46C2tbJGkvLIpcgt9-QLi3AEEwMFCItJDMrPTTbIQ3dKywdIB5aU4kKyQQKpha8drhhIGzLLWODbNs3b3s8YAxKBQAcwXpAThMaGWDvbH0gFloGbmrgQfBzYpd1YjQZbEYARkB6zMwO2SHSAAlZlYIBCdtCRkZpHIrFYahQYQD8UYYFA5EhcfjyGYqHAXnJAsIUHlOOUbHYhMIIHJzsI0Qk4P9SLUBuRqXEXEwAKKfRZcNA8PiCfxWACecAAUgBlAAacFm80W-CU11U6h4TgwUv11yShjgJjMLMqDnN9Dilq+nh8pD8AXgCHdMrCkWisVoAet0R6fXqhWKhjKllZVVxMcavpd4Zg7U6Qaj+2hmdG4zeRF10uu-Aeq0LBfLMEe-V+T2L7zLVu+FBWLdLeq+lc7DYFf39deFVOotMCACNOCh1dq219a+30uC8YWoZsRyuEdjkevR8uvoVMdjyTWt4WiSSydXD4NqZP4AymeZE072ZzuUeZQKheQgA)에서 타입 체커를 사용하여 `React.ReactNode`와 `React.ReactElement`의 모든 예시를 확인할 수 있습니다.\n\n### Style Props {/*typing-style-props*/}\n\nReact의 인라인 스타일을 사용할 때, `React.CSSProperties`를 사용하여 `style` Prop에 전달된 객체를 설명할 수 있습니다. 이 타입은 가능한 모든 CSS 프로퍼티의 조합이고, `style` Prop에 유효한 CSS 프로퍼티를 전달하고 있는지 확인하며, 에디터에서 자동 완성 기능을 사용할 수 있는 좋은 방법입니다.\n\n```ts\ninterface MyComponentProps {\n  style: React.CSSProperties;\n}\n```\n\n## 추가 학습 {/*further-learning*/}\n\n이 가이드는 React에서 TypeScript를 사용하는 기본 사항을 다루었지만, 배울 것이 더 많습니다.\n문서의 개별 API 페이지에는 TypeScript와 함께 사용하는 방법에 대한 자세한 설명이 포함되어 있을 수 있습니다.\n\n다음 리소스를 추천합니다.\n\n - [TypeScript 핸드북](https://www.typescriptlang.org/ko/docs/handbook/)은 TypeScript에 대한 공식 문서로, 대부분 주요 언어 기능을 다루고 있습니다.\n\n - [TypeScript 릴리즈 노트](https://devblogs.microsoft.com/typescript/)에서는 각각의 새로운 기능에 대해 자세히 설명합니다.\n\n - [React TypeScript 치트시트](https://react-typescript-cheatsheet.netlify.app/)는 React와 함께 TypeScript를 사용하기 위해 커뮤니티에서 관리하는 치트시트로, 유용한 엣지 케이스를 많이 다루고 이 문서보다 더 폭넓은 정보를 제공합니다.\n\n - [TypeScript 커뮤니티 디스코드](https://discord.com/invite/typescript)는 TypeScript 및 React 문제에 대해 질문하고 도움을 받을 수 있는 좋은 곳입니다.\n"
  },
  {
    "path": "src/content/learn/understanding-your-ui-as-a-tree.md",
    "content": "---\ntitle: 트리로서 UI 이해하기\n---\n\n<Intro>\n\nReact 앱은 서로 중첩된 많은 컴포넌트로 구성되어 있습니다. React는 어떻게 앱의 컴포넌트 구조를 추적할까요?\n\nReact와 많은 다른 UI 라이브러리는 UI를 트리로 모델링합니다. 애플리케이션을 트리로 생각하면 컴포넌트 간의 관계를 이해하는 데 도움이 됩니다. 이러한 이해는 성능과 상태 관리와 같이 앞으로 배울 개념을 디버깅하는 데 도움이 될 것입니다.\n\n</Intro>\n\n<YouWillLearn>\n\n* React가 컴포넌트 구조를 \"이해하는\" 방법\n* 렌더 트리가 무엇이고 어떤 용도로 사용되는지\n* 모듈 의존성 트리가 무엇이고 어떤 용도로 사용되는지\n\n</YouWillLearn>\n\n## 트리로서의 UI {/*your-ui-as-a-tree*/}\n\n트리는 요소 사이의 관계 모델이며 UI는 종종 트리 구조를 사용하여 표현됩니다. 예를 들어, 브라우저는 HTML ([DOM](https://developer.mozilla.org/docs/Web/API/Document_Object_Model/Introduction))과 CSS ([CSSOM](https://developer.mozilla.org/docs/Web/API/CSS_Object_Model))을 모델링하기 위해 트리 구조를 사용합니다. 모바일 플랫폼도 뷰 계층 구조를 나타내는 데 트리를 사용합니다.\n\n<Diagram name=\"preserving_state_dom_tree\" height={193} width={864} alt=\"가로로 배열된 세 부분으로 구성된 다이어그램입니다. 첫 번째 부분에는 '컴포넌트 A', '컴포넌트 B', '컴포넌트 C'라는 레이블이 붙은 세 개의 직사각형이 수직으로 쌓여 있습니다. 다음 패널로 넘어가는 화살표는 위에 React 로고가 있고 'React'라고 레이블이 붙어 있습니다. 중간 섹션에는 'A'라고 레이블이 붙은 루트와 'B', 'C'라고 레이블이 붙은 두 자식이 있는 컴포넌트 트리가 포함되어 있습니다. 다음 섹션은 다시 위에 React 로고가 있는 화살표를 사용하여 'React DOM'이라는 레이블과 함께 전환됩니다. 세 번째이자 마지막 섹션은 브라우저의 와이어프레임으로, 8개의 노드가 있는 트리를 포함하고 있으며, 그 중 일부분만 강조되어 있습니다(중간 섹션에서 파생된 서브트리를 나타냅니다).\">\n\nReact는 컴포넌트로부터 UI 트리를 생성합니다. 이 예에서 UI 트리는 DOM을 렌더링하는 데 사용됩니다.\n</Diagram>\n\n브라우저와 모바일 플랫폼처럼 React도 React 앱의 컴포넌트 간의 관계를 관리하고 모델링하기 위해 트리 구조를 사용합니다. 트리는 React 앱에서 데이터가 흐르는 방식과 렌더링 및 앱 크기를 최적화하는 방법을 이해하는 데 유용한 도구입니다.\n\n## 렌더 트리 {/*the-render-tree*/}\n\n컴포넌트의 주요 특징은 다른 컴포넌트의 컴포넌트를 구성하는 것입니다. [컴포넌트를 중첩](/learn/your-first-component#nesting-and-organizing-components)하면 부모 컴포넌트와 자식 컴포넌트의 개념이 생기며, 각 부모 컴포넌트는 다른 컴포넌트의 자식이 될 수 있습니다.\n\nReact 앱을 렌더링할 때, 이 관계를 렌더 트리라고 알려진 트리로 모델링할 수 있습니다.\n\n아래는 명언을 렌더링하는 React 앱입니다.\n\n<Sandpack>\n\n```js src/App.js\nimport FancyText from './FancyText';\nimport InspirationGenerator from './InspirationGenerator';\nimport Copyright from './Copyright';\n\nexport default function App() {\n  return (\n    <>\n      <FancyText title text=\"Get Inspired App\" />\n      <InspirationGenerator>\n        <Copyright year={2004} />\n      </InspirationGenerator>\n    </>\n  );\n}\n\n```\n\n```js src/FancyText.js\nexport default function FancyText({title, text}) {\n  return title\n    ? <h1 className='fancy title'>{text}</h1>\n    : <h3 className='fancy cursive'>{text}</h3>\n}\n```\n\n```js src/InspirationGenerator.js\nimport * as React from 'react';\nimport quotes from './quotes';\nimport FancyText from './FancyText';\n\nexport default function InspirationGenerator({children}) {\n  const [index, setIndex] = React.useState(0);\n  const quote = quotes[index];\n  const next = () => setIndex((index + 1) % quotes.length);\n\n  return (\n    <>\n      <p>Your inspirational quote is:</p>\n      <FancyText text={quote} />\n      <button onClick={next}>Inspire me again</button>\n      {children}\n    </>\n  );\n}\n```\n\n```js src/Copyright.js\nexport default function Copyright({year}) {\n  return <p className='small'>©️ {year}</p>;\n}\n```\n\n```js src/quotes.js\nexport default [\n  \"Don’t let yesterday take up too much of today.” — Will Rogers\",\n  \"Ambition is putting a ladder against the sky.\",\n  \"A joy that's shared is a joy made double.\",\n  ];\n```\n\n```css\n.fancy {\n  font-family: 'Georgia';\n}\n.title {\n  color: #007AA3;\n  text-decoration: underline;\n}\n.cursive {\n  font-style: italic;\n}\n.small {\n  font-size: 10px;\n}\n```\n\n</Sandpack>\n\n<Diagram name=\"render_tree\" height={250} width={500} alt=\"다섯 개의 노드가 있는 트리 그래프입니다. 각 노드는 컴포넌트를 나타냅니다. 트리의 루트는 앱이며, 두 개의 화살표가 여기에서 'InspirationGenerator'와 'FancyText'로 확장됩니다. 화살표에는 'renders'라는 레이블이 표시됩니다. 'InspirationGenerator' 노드에는 'FancyText'와 'Copyright' 노드를 가리키는 두 개의 화살표가 있습니다.\">\n\nReact는 렌더링된 컴포넌트로 구성된 UI 트리인 *렌더 트리*를 생성합니다.\n\n</Diagram>\n\n예시 앱에서, 우리는 위의 렌더 트리를 구성할 수 있습니다.\n\n트리는 노드로 구성되어 있으며, 각 노드는 컴포넌트를 나타냅니다. `App`, `FancyText`, `Copyright` 등은 모두 트리의 노드입니다.\n\nReact 렌더 트리에서 루트 노드는 앱의 [Root 컴포넌트](/learn/importing-and-exporting-components#the-root-component-file)입니다. 이 경우 루트 컴포넌트는 `App`이며 React가 렌더링하는 첫 번째 컴포넌트입니다. 트리의 각 화살표는 부모 컴포넌트에서 자식 컴포넌트를 가리킵니다.\n\n<DeepDive>\n\n#### 렌더 트리에 HTML 태그는 어디에 있나요? {/*where-are-the-html-elements-in-the-render-tree*/}\n\n위의 렌더 트리에서 각 컴포넌트가 렌더링하는 HTML 태그에 대한 언급이 없음을 알 수 있습니다. 이는 렌더 트리가 React [컴포넌트](learn/your-first-component#components-ui-building-blocks)로만 구성되어 있기 때문입니다.\n\nUI 프레임워크로서 React는 플랫폼에 독립적입니다. react.dev에서는 HTML 마크업을 UI 기본 요소로 사용하는 웹을 렌더링하는 예시를 보여줍니다. 하지만 React 앱은 모바일이나 데스크톱 플랫폼에 렌더링 될 수 있으며, 이러한 플랫폼은 [UIView](https://developer.apple.com/documentation/uikit/uiview)나 [FrameworkElement](https://learn.microsoft.com/en-us/dotnet/api/system.windows.frameworkelement?view=windowsdesktop-7.0)와 같은 다른 UI 기본 요소를 사용할 수 있습니다.\n\n이러한 플랫폼 UI 기본 요소는 React의 일부가 아닙니다. React 렌더 트리는 앱이 렌더링되는 플랫폼에 관계없이 React 앱에 대한 통찰력을 제공할 수 있습니다.\n\n</DeepDive>\n\n렌더 트리는 React 앱의 단일 렌더링을 나타냅니다. [조건부 렌더링](/learn/conditional-rendering)을 사용하면 부모 컴포넌트가 전달된 데이터에 따라 다른 자식을 렌더링할 수 있습니다.\n\n우리는 앱을 업데이트하여 명언이나 색상을 조건부로 렌더링할 수 있습니다.\n\n<Sandpack>\n\n```js src/App.js\nimport FancyText from './FancyText';\nimport InspirationGenerator from './InspirationGenerator';\nimport Copyright from './Copyright';\n\nexport default function App() {\n  return (\n    <>\n      <FancyText title text=\"Get Inspired App\" />\n      <InspirationGenerator>\n        <Copyright year={2004} />\n      </InspirationGenerator>\n    </>\n  );\n}\n\n```\n\n```js src/FancyText.js\nexport default function FancyText({title, text}) {\n  return title\n    ? <h1 className='fancy title'>{text}</h1>\n    : <h3 className='fancy cursive'>{text}</h3>\n}\n```\n\n```js src/Color.js\nexport default function Color({value}) {\n  return <div className=\"colorbox\" style={{backgroundColor: value}} />\n}\n```\n\n```js src/InspirationGenerator.js\nimport * as React from 'react';\nimport inspirations from './inspirations';\nimport FancyText from './FancyText';\nimport Color from './Color';\n\nexport default function InspirationGenerator({children}) {\n  const [index, setIndex] = React.useState(0);\n  const inspiration = inspirations[index];\n  const next = () => setIndex((index + 1) % inspirations.length);\n\n  return (\n    <>\n      <p>Your inspirational {inspiration.type} is:</p>\n      {inspiration.type === 'quote'\n      ? <FancyText text={inspiration.value} />\n      : <Color value={inspiration.value} />}\n\n      <button onClick={next}>Inspire me again</button>\n      {children}\n    </>\n  );\n}\n```\n\n```js src/Copyright.js\nexport default function Copyright({year}) {\n  return <p className='small'>©️ {year}</p>;\n}\n```\n\n```js src/inspirations.js\nexport default [\n  {type: 'quote', value: \"Don’t let yesterday take up too much of today.” — Will Rogers\"},\n  {type: 'color', value: \"#B73636\"},\n  {type: 'quote', value: \"Ambition is putting a ladder against the sky.\"},\n  {type: 'color', value: \"#256266\"},\n  {type: 'quote', value: \"A joy that's shared is a joy made double.\"},\n  {type: 'color', value: \"#F9F2B4\"},\n];\n```\n\n```css\n.fancy {\n  font-family: 'Georgia';\n}\n.title {\n  color: #007AA3;\n  text-decoration: underline;\n}\n.cursive {\n  font-style: italic;\n}\n.small {\n  font-size: 10px;\n}\n.colorbox {\n  height: 100px;\n  width: 100px;\n  margin: 8px;\n}\n```\n</Sandpack>\n\n<Diagram name=\"conditional_render_tree\" height={250} width={561} alt=\"6개의 노드가 있는 트리 그래프. 트리의 맨 위 노드에는 'App'이라는 이름이 붙고, 두 개의 화살표가 'InspirationGenerator'와 'FancyText'라는 이름의 노드로 확장됩니다. 화살표는 실선이며 'renders'라고 표시됩니다. 'InspirationGenerator' 노드에도 세 개의 화살표가 있습니다. 'FancyText'와 'Color' 노드의 화살표는 점선으로 표시되고 'renders?'로 표시됩니다. 마지막 화살표는 'Copyright'라는 이름의 노드를 가리키고, 실선으로 표시되고 'renders'로 표시됩니다.\">\n\n조건부 렌더링을 사용하면, 서로 다른 렌더링에서 렌더 트리가 다른 컴포넌트를 렌더링할 수 있습니다.\n\n</Diagram>\n\n이 예시에서, `inspiration.type`이 무엇이냐에 따라 `<FancyText>` 또는 `<Color>`를 렌더링할 수 있습니다. 렌더 트리는 각 렌더링마다 다를 수 있습니다.\n\n렌더 트리가 렌더링 단계마다 다를 수 있지만, 이 트리는 React 앱에서 최상위 컴포넌트와 리프 컴포넌트가 무엇인지를 식별하는 데 도움이 됩니다. 최상위 컴포넌트는 루트 컴포넌트에 가장 가까운 컴포넌트이며, 그 아래의 모든 컴포넌트의 렌더링 성능에 영향을 미치며, 가장 복잡성이 높습니다. 리프 컴포넌트는 트리의 맨 아래에 있으며 자식 컴포넌트가 없으며 자주 다시 렌더링 됩니다.\n\n이 컴포넌트 카테고리를 식별하는 것은 앱의 데이터 흐름과 성능을 이해하는 데 유용합니다.\n\n## 모듈 의존성 트리 {/*the-module-dependency-tree*/}\n\n트리로 모델링 할 수 있는 React 앱의 다른 관계는 앱의 모듈 의존성입니다. [컴포넌트를 분리](/learn/importing-and-exporting-components#exporting-and-importing-a-component)하고 로직을 별도의 파일로 분리하면 컴포넌트, 함수 또는 상수를 내보내는 [JS 모듈](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules)을 만들 수 있습니다.\n\n모듈 의존성 트리의 각 노드는 모듈이며, 각 가지는 해당 모듈의 `import` 문을 나타냅니다.\n\n이전의 영감 앱을 사용하면 모듈 의존성 트리 또는 줄여서 의존성 트리를 구축할 수 있습니다.\n\n<Diagram name=\"module_dependency_tree\" height={250} width={658} alt=\"7개의 노드가 있는 트리 그래프. 각 노드는 모듈 이름으로 레이블됩니다. 트리의 최상위 노드는 'App.js'로 레이블이 표시됩니다. 모듈 'InspirationGenerator.js', 'FancyText.js' 및 'Copyright.js'를 가리키는 세 개의 화살표가 있고 화살표는 'imports'로 레이블이 표시됩니다. InspirationGenerator.js' 노드에서 'FancyText.js', 'Color.js' 및 'inspirations.js'의 세 개의 모듈로 확장되는 세 개의 화살표가 있습니다. 화살표는 'imports'로 레이블이 표시됩니다.\">\n\n영감 앱의 모듈 의존성 트리입니다.\n\n</Diagram>\n\n트리의 루트 노드는 루트 모듈이며, 엔트리 포인트 파일이라고도 합니다. 일반적으로 루트 컴포넌트를 포함하는 모듈입니다.\n\n동일한 앱의 렌더 트리와 비교하면 유사한 구조가 있지만 몇 가지 차이점이 있습니다.\n\n* 트리를 구성하는 노드는 컴포넌트가 아닌 모듈을 나타냅니다.\n* `inspirations.js`와 같은 컴포넌트가 아닌 모듈도 이 트리에 나타납니다. 렌더 트리는 컴포넌트만 캡슐화합니다.\n* `Copyright.js`가 `App.js` 아래에 나타나지만, 렌더 트리에서 `Copyright` 컴포넌트는 `InspirationGenerator`의 자식으로 나타납니다. 이는 `InspirationGenerator`가 [자식 props](/learn/passing-props-to-a-component#passing-jsx-as-children)로 JSX를 허용하기 때문에, `Copyright`를 자식 컴포넌트로 렌더링하지만 모듈을 가져오지는 않기 때문입니다.\n\n의존성 트리는 React 앱을 실행하는 데 필요한 모듈을 결정하는 데 유용합니다. React 앱을 프로덕션용으로 빌드할 때, 일반적으로 클라이언트에 제공할 모든 필요 JavaScript를 번들로 묶는 빌드 단계가 있습니다. 이 작업을 담당하는 도구를 [번들러](https://developer.mozilla.org/en-US/docs/Learn/Tools_and_testing/Understanding_client-side_tools/Overview#the_modern_tooling_ecosystem)라고 하며, 번들러는 의존성 트리를 사용하여 포함해야 할 모듈을 결정합니다.\n\n앱이 커짐에 따라 번들 크기도 커집니다. 번들 크기가 커지면 클라이언트가 다운로드하고 실행하는 데 드는 비용도 커집니다. 또한 UI가 그려지는 데 시간이 지체될 수 있습니다. 앱의 의존성 트리를 파악하면 이러한 문제를 디버깅하는 데 도움이 될 수 있습니다.\n\n[comment]: <> (perhaps we should also deep dive on conditional imports)\n\n<Recap>\n\n* 트리는 요소 간의 관계를 나타내는 일반적인 방법입니다. UI를 모델링하는 데 자주 사용됩니다.\n* 렌더 트리는 단일 렌더링에서 React 컴포넌트 간의 중첩 관계를 나타냅니다.\n* 조건부 렌더링을 사용하면 렌더 트리가 다른 렌더링에서 변경될 수 있습니다. 다른 prop 값으로 인해 컴포넌트가 다른 자식 컴포넌트를 렌더링할 수 있습니다.\n* 렌더 트리는 최상위 컴포넌트와 리프 컴포넌트를 식별하는 데 도움이 됩니다. 최상위 컴포넌트는 그 아래의 모든 컴포넌트의 렌더링 성능에 영향을 미치며, 리프 컴포넌트는 자주 다시 렌더링됩니다. 이러한 컴포넌트를 식별하는 것은 렌더링 성능을 이해하고 디버깅하는 데 유용합니다.\n* 의존성 트리는 React 앱의 모듈 의존성을 나타냅니다.\n* 의존성 트리는 앱을 배포하기 위해 필요한 코드를 번들로 묶는 데 빌드 도구에서 사용됩니다.\n* 의존성 트리는 느리게 페인트되는 큰 번들 크기를 디버깅하는 데 유용하며, 어떤 코드를 번들로 묶을지 최적화할 기회를 제공합니다.\n\n</Recap>\n\n[TODO]: <> (Add challenges)\n"
  },
  {
    "path": "src/content/learn/updating-arrays-in-state.md",
    "content": "---\ntitle: 배열 State 업데이트하기\n---\n\n<Intro>\n\n배열은 JavaScript에서는 변경이 가능하지만, state로 저장할 때에는 변경할 수 없도록 처리해야 합니다. 객체와 마찬가지로, state에 저장된 배열을 업데이트하고 싶을 때에는, 새 배열을 생성(혹은 기존 배열의 복사본을 생성)한 뒤, 이 새 배열을 state로 두어 업데이트해야 합니다.\n\n</Intro>\n\n<YouWillLearn>\n\n- React state에서 배열의 항목을 추가, 삭제 또는 변경하는 방법\n- 배열 내부의 객체를 업데이트하는 방법\n- Immer로 덜 반복해서 배열을 복사하는 방법\n\n</YouWillLearn>\n\n## 변경하지 않고 배열 업데이트하기 {/*updating-arrays-without-mutation*/}\n\nJavaScript에서 배열은 다른 종류의 객체입니다. [객체와 마찬가지로](/learn/updating-objects-in-state) React state에서 배열은 읽기 전용으로 처리해야 합니다. 즉 `arr[0] = 'bird'`처럼 배열 내부의 항목을 재할당해서는 안 되며 `push()`나 `pop()`같은 함수로 배열을 변경해서는 안됩니다.\n\n대신 배열을 업데이트할 때마다 *새* 배열을 state 설정 함수에 전달해야 합니다. 이를 위해 state의 원본 배열을 변경시키지 않는 `filter()`와 `map()` 같은 함수를 사용하여 원본 배열로부터 새 배열을 만들 수 있습니다. 이후 이 새 배열들을 state에 설정합니다.\n\n다음은 일반적인 배열 연산에 대한 참조 표입니다. React state 내에서 배열을 다룰 땐, 왼쪽 열에 있는 함수들의 사용을 피하는 대신, 오른쪽 열에 있는 함수들을 선호해야 합니다.\n\n|         | 비선호 (배열을 변경) | 선호 (새 배열을 반환)                                           |\n|---------|----------------|---------------------------------------------------------|\n| 추가 | `push`, `unshift` | `concat`, `[...arr]` 전개 연산자 ([예시](#adding-to-an-array)) |\n| 제거 | `pop`, `shift`, `splice` | `filter`, `slice` ([예시](#removing-from-an-array))       |\n| 교체 | `splice`, `arr[i] = ...` 할당 | `map` ([예시](#replacing-items-in-an-array))              |\n| 정렬 | `reverse`, `sort` | 배열을 복사한 이후 처리 ([예시](#making-other-changes-to-an-array)) |\n\n또는 두 열의 함수를 모두 사용할 수 있도록 하는 [Immer](#write-concise-update-logic-with-immer)를 사용할 수 있습니다.\n\n<Pitfall>\n\n안타깝지만, [`slice`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice)와 [`splice`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice) 함수는 이름이 비슷하지만 몹시 다릅니다.\n\n* `slice`를 사용하면 배열 또는 그 일부를 복사할 수 있습니다.\n* `splice`는 배열을 **변경**합니다. (항목을 추가하거나 제거합니다.)\n\nReact에서는, state의 객체나 배열을 변경하지 않는 게 좋기 때문에 `slice` (`p`가 없습니다!)를 훨씬 더 자주 사용하게 될 것입니다. [객체 업데이트](/learn/updating-objects-in-state)에서 변경이 무엇이고 왜 state에 권장되지 않는지에 대해 이유를 설명합니다.\n\n</Pitfall>\n\n### 배열에 항목 추가하기 {/*adding-to-an-array*/}\n\n`push()`는 배열을 변경합니다. (원치 않는 방식)\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nlet nextId = 0;\n\nexport default function List() {\n  const [name, setName] = useState('');\n  const [artists, setArtists] = useState([]);\n\n  return (\n    <>\n      <h1>Inspiring sculptors:</h1>\n      <input\n        value={name}\n        onChange={e => setName(e.target.value)}\n      />\n      <button onClick={() => {\n        artists.push({\n          id: nextId++,\n          name: name,\n        });\n      }}>Add</button>\n      <ul>\n        {artists.map(artist => (\n          <li key={artist.id}>{artist.name}</li>\n        ))}\n      </ul>\n    </>\n  );\n}\n```\n\n```css\nbutton { margin-left: 5px; }\n```\n\n</Sandpack>\n\n대신 기존에 존재하던 항목들 뒤에 새 항목을 포함하는 *새로운* 배열을 만드세요. 이를 위한 방법은 여러 가지가 있지만 가장 쉬운 방법은 `...` [배열 전개 구문](a-javascript-refresher#array-spread)을 사용하는 것입니다.\n\n```js\nsetArtists( // 아래의 새로운 배열로 state를 변경합니다.\n  [\n    ...artists, // 기존 배열의 모든 항목에,\n    { id: nextId++, name: name } // 마지막에 새 항목을 추가합니다.\n  ]\n);\n```\n\n이제 올바르게 작동합니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nlet nextId = 0;\n\nexport default function List() {\n  const [name, setName] = useState('');\n  const [artists, setArtists] = useState([]);\n\n  return (\n    <>\n      <h1>Inspiring sculptors:</h1>\n      <input\n        value={name}\n        onChange={e => setName(e.target.value)}\n      />\n      <button onClick={() => {\n        setArtists([\n          ...artists,\n          { id: nextId++, name: name }\n        ]);\n      }}>Add</button>\n      <ul>\n        {artists.map(artist => (\n          <li key={artist.id}>{artist.name}</li>\n        ))}\n      </ul>\n    </>\n  );\n}\n```\n\n```css\nbutton { margin-left: 5px; }\n```\n\n</Sandpack>\n\n배열 전개 구문을 사용하여 기존 배열인 `...artists`의 *앞*에 항목을 배치하여 추가할 수도 있습니다.\n\n```js\nsetArtists([\n  { id: nextId++, name: name }, // 추가할 항목을 앞에 배치하고,\n  ...artists // 기존 배열의 항목들을 뒤에 배치합니다.\n]);\n```\n\n이런 식으로 전개 구문은 배열의 가장 뒤에 추가하는 `push()`와, 배열의 가장 앞에 추가하는 `unshift()`의 두 기능 모두 수행할 수 있습니다. 위의 샌드박스에서 사용해보세요!\n\n### 배열에서 항목 제거하기 {/*removing-from-an-array*/}\n\n배열에서 항목을 제거하는 가장 쉬운 방법은 *필터링*하는 것입니다. 다시 말해서 해당 항목을 포함하지 않는 새 배열을 제공하는 것입니다. 이렇게 하려면 `filter` 함수를 사용하면 됩니다. 예를 들면 아래와 같습니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nlet initialArtists = [\n  { id: 0, name: 'Marta Colvin Andrade' },\n  { id: 1, name: 'Lamidi Olonade Fakeye'},\n  { id: 2, name: 'Louise Nevelson'},\n];\n\nexport default function List() {\n  const [artists, setArtists] = useState(\n    initialArtists\n  );\n\n  return (\n    <>\n      <h1>Inspiring sculptors:</h1>\n      <ul>\n        {artists.map(artist => (\n          <li key={artist.id}>\n            {artist.name}{' '}\n            <button onClick={() => {\n              setArtists(\n                artists.filter(a =>\n                  a.id !== artist.id\n                )\n              );\n            }}>\n              Delete\n            </button>\n          </li>\n        ))}\n      </ul>\n    </>\n  );\n}\n```\n\n</Sandpack>\n\n\"Delete\" 버튼을 몇 번 클릭하고, 클릭 이벤트 핸들러를 확인해보세요.\n\n```js\nsetArtists(\n  artists.filter(a => a.id !== artist.id)\n);\n```\n\n여기서 `artists.filter(s => s.id !== artist.id)`는 \"`artist.id`와 ID가 다른 `artists`로 구성된 배열을 생성한다\"는 의미입니다. 즉, 각 artist의 \"Delete\" 버튼은 해당 artist를 배열에서 필터링한 다음, 반환된 배열로 리렌더링을 요청합니다. `filter`가 원본 배열을 수정하지 않는다는 점에 주의하세요.\n\n### 배열 변환하기 {/*transforming-an-array*/}\n\n배열의 일부 또는 전체 항목을 변경하고자 한다면, `map()`을 사용해 **새로운** 배열을 만들 수 있습니다. `map`에 전달할 함수는 데이터나 인덱스(또는 둘 다)를 기반으로 각 항목을 어떻게 처리할지 결정할 수 있습니다.\n\n이 예시에서 배열은 두 개의 원과 하나의 정사각형 좌표를 가집니다. 버튼을 누르면, 원들은 50픽셀 아래로 이동합니다. `map()`으로 새 데이터 배열을 생성하여 이를 처리합니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nlet initialShapes = [\n  { id: 0, type: 'circle', x: 50, y: 100 },\n  { id: 1, type: 'square', x: 150, y: 100 },\n  { id: 2, type: 'circle', x: 250, y: 100 },\n];\n\nexport default function ShapeEditor() {\n  const [shapes, setShapes] = useState(\n    initialShapes\n  );\n\n  function handleClick() {\n    const nextShapes = shapes.map(shape => {\n      if (shape.type === 'square') {\n        // 변경시키지 않고 반환합니다.\n        return shape;\n      } else {\n        // 50px 아래로 이동한 새로운 원을 반환합니다.\n        return {\n          ...shape,\n          y: shape.y + 50,\n        };\n      }\n    });\n    // 새로운 배열로 리렌더링합니다.\n    setShapes(nextShapes);\n  }\n\n  return (\n    <>\n      <button onClick={handleClick}>\n        Move circles down!\n      </button>\n      {shapes.map(shape => (\n        <div style={{\n          background: 'purple',\n          position: 'absolute',\n          left: shape.x,\n          top: shape.y,\n          borderRadius:\n            shape.type === 'circle'\n              ? '50%' : '',\n          width: 20,\n          height: 20,\n        }} />\n      ))}\n    </>\n  );\n}\n```\n\n```css\nbody { height: 300px; }\n```\n\n</Sandpack>\n\n### 배열 내 항목 교체하기 {/*replacing-items-in-an-array*/}\n\n배열에서 하나 이상의 항목을 교체하는 경우가 특히 흔합니다. `arr[0] = 'bird'`와 같은 할당은 원본 배열을 변경시키므로, 이 경우에도 `map`을 사용하는 편이 좋습니다.\n\n항목을 교체하기 위해 `map`을 이용해서 새로운 배열을 만듭니다. `map`을 호출할 때 두 번째 인수로 항목의 인덱스를 받을 수 있습니다. 인덱스는 원래 항목(첫 번째 인수)을 반환할지 다른 항목을 반환할지를 결정할 때 사용합니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nlet initialCounters = [\n  0, 0, 0\n];\n\nexport default function CounterList() {\n  const [counters, setCounters] = useState(\n    initialCounters\n  );\n\n  function handleIncrementClick(index) {\n    const nextCounters = counters.map((c, i) => {\n      if (i === index) {\n        // 클릭된 counter를 증가시킵니다.\n        return c + 1;\n      } else {\n        // 변경되지 않은 나머지를 반환합니다.\n        return c;\n      }\n    });\n    setCounters(nextCounters);\n  }\n\n  return (\n    <ul>\n      {counters.map((counter, i) => (\n        <li key={i}>\n          {counter}\n          <button onClick={() => {\n            handleIncrementClick(i);\n          }}>+1</button>\n        </li>\n      ))}\n    </ul>\n  );\n}\n```\n\n```css\nbutton { margin: 5px; }\n```\n\n</Sandpack>\n\n### 배열에 항목 삽입하기 {/*inserting-into-an-array*/}\n\n가끔은 시작도, 끝도 아닌 위치에 항목을 삽입하고 싶을 수 있습니다. 이를 위해, `...` 배열 전개 구문과 `slice()` 함수를 함께 사용할 수 있습니다. `slice()` 함수를 사용하면 배열의 \"일부분\"을 잘라낼 수 있습니다. 항목을 삽입하려면 삽입 지점 *앞에* 자른 배열을 전개하고, 새 항목과 원본 배열의 나머지 부분을 전개하는 배열을 만듭니다.\n\n이 예시에서 삽입 버튼은 항상 인덱스 `1`에 삽입됩니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nlet nextId = 3;\nconst initialArtists = [\n  { id: 0, name: 'Marta Colvin Andrade' },\n  { id: 1, name: 'Lamidi Olonade Fakeye'},\n  { id: 2, name: 'Louise Nevelson'},\n];\n\nexport default function List() {\n  const [name, setName] = useState('');\n  const [artists, setArtists] = useState(\n    initialArtists\n  );\n\n  function handleClick() {\n    const insertAt = 1; // 모든 인덱스가 될 수 있습니다.\n    const nextArtists = [\n      // 삽입 지점 이전 항목\n      ...artists.slice(0, insertAt),\n      // 새 항목\n      { id: nextId++, name: name },\n      // 삽입 지점 이후 항목\n      ...artists.slice(insertAt)\n    ];\n    setArtists(nextArtists);\n    setName('');\n  }\n\n  return (\n    <>\n      <h1>Inspiring sculptors:</h1>\n      <input\n        value={name}\n        onChange={e => setName(e.target.value)}\n      />\n      <button onClick={handleClick}>\n        Insert\n      </button>\n      <ul>\n        {artists.map(artist => (\n          <li key={artist.id}>{artist.name}</li>\n        ))}\n      </ul>\n    </>\n  );\n}\n```\n\n```css\nbutton { margin-left: 5px; }\n```\n\n</Sandpack>\n\n### 배열에 기타 변경 적용하기 {/*making-other-changes-to-an-array*/}\n\n전개 구문과 `map()`, `filter()` 같은 비-변경 함수들로만으로는 할 수 없는 일이 몇 가지 있습니다. 예를 들어 배열을 뒤집거나 정렬하고 싶을 수 있습니다. JavaScript의 `reverse()` 및 `sort()` 함수는 원본 배열을 변경시키므로 직접 사용할 수 없습니다.\n\n**대신, 먼저 배열을 복사한 뒤 변경할 수 있습니다.**\n\n예를 들어서 아래와 같습니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nconst initialList = [\n  { id: 0, title: 'Big Bellies' },\n  { id: 1, title: 'Lunar Landscape' },\n  { id: 2, title: 'Terracotta Army' },\n];\n\nexport default function List() {\n  const [list, setList] = useState(initialList);\n\n  function handleClick() {\n    const nextList = [...list];\n    nextList.reverse();\n    setList(nextList);\n  }\n\n  return (\n    <>\n      <button onClick={handleClick}>\n        Reverse\n      </button>\n      <ul>\n        {list.map(artwork => (\n          <li key={artwork.id}>{artwork.title}</li>\n        ))}\n      </ul>\n    </>\n  );\n}\n```\n\n</Sandpack>\n\n여기서는 먼저 `[...list]` 전개 구문을 사용해 원본 배열의 복사본을 만듭니다. 이제 복사본이 있으므로 `nextList.reverse()` 또는 `nextList.sort()`와 같은 변경 함수를 사용하거나 `nextList[0] = \"something\"`과 같이 개별 항목을 할당할 수도 있습니다.\n\n그러나, **배열을 복사하더라도 배열 *내부* 에 기존 항목을 직접 변경해서는 안됩니다**. 이는 얕은 복사이기 때문에 복사한 새 배열에는 원본 배열과 동일한 항목이 포함됩니다. 따라서 복사된 배열 내부의 객체를 수정하면 기존 state가 변경됩니다. 예를 들면, 아래와 같은 코드가 문제가 됩니다.\n\n```js\nconst nextList = [...list];\nnextList[0].seen = true; // 문제: list[0]을 변경시킵니다.\nsetList(nextList);\n```\n\n`nextList`와 `list`는 서로 다른 배열이지만, **`nextList[0]`과 `list[0]`은 동일한 객체를 가리킵니다**. 따라서 `nextList[0].seen`을 변경하면 `list[0].seen`도 변경됩니다. 이것은 state 변경이므로 피해야 합니다. [중첩된 JavaScript 객체 업데이트](/learn/updating-objects-in-state#updating-a-nested-object)와 유사한 방식으로 이 문제를 해결할 수 있습니다. 변경하려는 개별 항목을 변경하는 대신 복사합니다. 방법은 다음과 같습니다.\n\n## 배열 내부의 객체 업데이트하기 {/*updating-objects-inside-arrays*/}\n\n객체는 *실제로* 배열 \"내부\"에 위치하지 않습니다. 코드에서 \"내부\"로 나타낼 수 있지만 배열의 각 객체는 배열이 \"가리키는\" 별도의 값입니다. 이것이 `list[0]`처럼 중첩된 필드를 변경할 때 주의해야 하는 이유입니다. 다른 사람의 artwork 목록이 배열의 동일한 요소를 가리킬 수 있습니다!\n\n**중첩된 state를 업데이트할 때, 업데이트하려는 지점부터 최상위 레벨까지의 복사본을 만들어야 합니다.** 어떻게 작동하는지 살펴봅시다.\n\n아래 예시에서 두 개의 개별 artwork 목록들은 초기 state가 서로 같습니다. 두 리스트는 분리되어야 하지만 변경으로 인해 두 목록의 state가 실수로 공유되고 한 목록의 체크박스를 선택하면 다른 목록에도 영향을 미칩니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nlet nextId = 3;\nconst initialList = [\n  { id: 0, title: 'Big Bellies', seen: false },\n  { id: 1, title: 'Lunar Landscape', seen: false },\n  { id: 2, title: 'Terracotta Army', seen: true },\n];\n\nexport default function BucketList() {\n  const [myList, setMyList] = useState(initialList);\n  const [yourList, setYourList] = useState(\n    initialList\n  );\n\n  function handleToggleMyList(artworkId, nextSeen) {\n    const myNextList = [...myList];\n    const artwork = myNextList.find(\n      a => a.id === artworkId\n    );\n    artwork.seen = nextSeen;\n    setMyList(myNextList);\n  }\n\n  function handleToggleYourList(artworkId, nextSeen) {\n    const yourNextList = [...yourList];\n    const artwork = yourNextList.find(\n      a => a.id === artworkId\n    );\n    artwork.seen = nextSeen;\n    setYourList(yourNextList);\n  }\n\n  return (\n    <>\n      <h1>Art Bucket List</h1>\n      <h2>My list of art to see:</h2>\n      <ItemList\n        artworks={myList}\n        onToggle={handleToggleMyList} />\n      <h2>Your list of art to see:</h2>\n      <ItemList\n        artworks={yourList}\n        onToggle={handleToggleYourList} />\n    </>\n  );\n}\n\nfunction ItemList({ artworks, onToggle }) {\n  return (\n    <ul>\n      {artworks.map(artwork => (\n        <li key={artwork.id}>\n          <label>\n            <input\n              type=\"checkbox\"\n              checked={artwork.seen}\n              onChange={e => {\n                onToggle(\n                  artwork.id,\n                  e.target.checked\n                );\n              }}\n            />\n            {artwork.title}\n          </label>\n        </li>\n      ))}\n    </ul>\n  );\n}\n```\n\n</Sandpack>\n\n문제는 아래와 같은 코드에 있습니다.\n\n```js\nconst myNextList = [...myList];\nconst artwork = myNextList.find(a => a.id === artworkId);\nartwork.seen = nextSeen; // 문제: 기존 항목을 변경시킵니다.\nsetMyList(myNextList);\n```\n\n`myNextList` 배열 자체는 새로운 배열이지만, *항목 자체*는 `myList` 원본 배열과 동일합니다. 따라서 `artwork.seen`을 변경하면 *원본* artwork 항목이 변경됩니다. 해당 artwork 항목은 `yourArtWorks`에도 존재하므로 버그가 발생합니다. 이런 버그는 생각하기 어려울 수 있지만 다행히도 state 변경을 피하면 해결할 수 있습니다.\n\n**`map`을 사용하면 이전 항목의 변경 없이 업데이트된 버전으로 대체할 수 있습니다.**\n\n```js\nsetMyList(myList.map(artwork => {\n  if (artwork.id === artworkId) {\n    // 변경된 *새* 객체를 만들어 반환합니다.\n    return { ...artwork, seen: nextSeen };\n  } else {\n    // 변경시키지 않고 반환합니다.\n    return artwork;\n  }\n}));\n```\n\n여기서 `...`는 [객체의 복사본 생성](/learn/updating-objects-in-state#copying-objects-with-the-spread-syntax)에 사용되는 객체 전개 구문입니다.\n\n이 접근 방식을 사용하면, 기존 state 항목이 변경되지 않고, 버그가 수정됩니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nlet nextId = 3;\nconst initialList = [\n  { id: 0, title: 'Big Bellies', seen: false },\n  { id: 1, title: 'Lunar Landscape', seen: false },\n  { id: 2, title: 'Terracotta Army', seen: true },\n];\n\nexport default function BucketList() {\n  const [myList, setMyList] = useState(initialList);\n  const [yourList, setYourList] = useState(\n    initialList\n  );\n\n  function handleToggleMyList(artworkId, nextSeen) {\n    setMyList(myList.map(artwork => {\n      if (artwork.id === artworkId) {\n        // 변경된 *새* 객체를 만들어 반환합니다.\n        return { ...artwork, seen: nextSeen };\n      } else {\n        // 변경시키지 않고 반환합니다.\n        return artwork;\n      }\n    }));\n  }\n\n  function handleToggleYourList(artworkId, nextSeen) {\n    setYourList(yourList.map(artwork => {\n      if (artwork.id === artworkId) {\n        // 변경된 *새* 객체를 만들어 반환합니다.\n        return { ...artwork, seen: nextSeen };\n      } else {\n        // 변경시키지 않고 반환합니다.\n        return artwork;\n      }\n    }));\n  }\n\n  return (\n    <>\n      <h1>Art Bucket List</h1>\n      <h2>My list of art to see:</h2>\n      <ItemList\n        artworks={myList}\n        onToggle={handleToggleMyList} />\n      <h2>Your list of art to see:</h2>\n      <ItemList\n        artworks={yourList}\n        onToggle={handleToggleYourList} />\n    </>\n  );\n}\n\nfunction ItemList({ artworks, onToggle }) {\n  return (\n    <ul>\n      {artworks.map(artwork => (\n        <li key={artwork.id}>\n          <label>\n            <input\n              type=\"checkbox\"\n              checked={artwork.seen}\n              onChange={e => {\n                onToggle(\n                  artwork.id,\n                  e.target.checked\n                );\n              }}\n            />\n            {artwork.title}\n          </label>\n        </li>\n      ))}\n    </ul>\n  );\n}\n```\n\n</Sandpack>\n\n일반적으로 **방금 생성한 객체만 변경해야 합니다.** *새* artwork를 삽입하는 경우 변경이 가능하지만, 이미 state에 존재하는 것을 처리하려면 복사본을 만들어야 합니다.\n\n### Immer로 간결한 업데이트 로직 작성하기 {/*write-concise-update-logic-with-immer*/}\n\n변경 없이 중첩된 배열을 업데이트하는 것은 [객체와 마찬가지로](/learn/updating-objects-in-state#write-concise-update-logic-with-immer) 약간 반복적일 수 있습니다.\n\n- 일반적으로 깊은 레벨까지의 state를 업데이트할 필요는 없습니다. state 객체가 매우 깊다면 [다르게 재구성](/learn/choosing-the-state-structure#avoid-deeply-nested-state)하여 평평하게 만들 수 있습니다.\n- state 구조를 변경하고 싶지 않다면, [Immer](https://github.com/immerjs/use-immer) 사용할 수 있습니다. 손쉽게 변경 문법을 사용하여 작성할 수 있고 복사본을 생성하여 처리할 수 있습니다.\n\n다음은 Immer로 다시 작성한 Art Bucket List 예시입니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\nimport { useImmer } from 'use-immer';\n\nlet nextId = 3;\nconst initialList = [\n  { id: 0, title: 'Big Bellies', seen: false },\n  { id: 1, title: 'Lunar Landscape', seen: false },\n  { id: 2, title: 'Terracotta Army', seen: true },\n];\n\nexport default function BucketList() {\n  const [myList, updateMyList] = useImmer(\n    initialList\n  );\n  const [yourArtworks, updateYourList] = useImmer(\n    initialList\n  );\n\n  function handleToggleMyList(id, nextSeen) {\n    updateMyList(draft => {\n      const artwork = draft.find(a =>\n        a.id === id\n      );\n      artwork.seen = nextSeen;\n    });\n  }\n\n  function handleToggleYourList(artworkId, nextSeen) {\n    updateYourList(draft => {\n      const artwork = draft.find(a =>\n        a.id === artworkId\n      );\n      artwork.seen = nextSeen;\n    });\n  }\n\n  return (\n    <>\n      <h1>Art Bucket List</h1>\n      <h2>My list of art to see:</h2>\n      <ItemList\n        artworks={myList}\n        onToggle={handleToggleMyList} />\n      <h2>Your list of art to see:</h2>\n      <ItemList\n        artworks={yourArtworks}\n        onToggle={handleToggleYourList} />\n    </>\n  );\n}\n\nfunction ItemList({ artworks, onToggle }) {\n  return (\n    <ul>\n      {artworks.map(artwork => (\n        <li key={artwork.id}>\n          <label>\n            <input\n              type=\"checkbox\"\n              checked={artwork.seen}\n              onChange={e => {\n                onToggle(\n                  artwork.id,\n                  e.target.checked\n                );\n              }}\n            />\n            {artwork.title}\n          </label>\n        </li>\n      ))}\n    </ul>\n  );\n}\n```\n\n```json package.json\n{\n  \"dependencies\": {\n    \"immer\": \"1.7.3\",\n    \"react\": \"latest\",\n    \"react-dom\": \"latest\",\n    \"react-scripts\": \"latest\",\n    \"use-immer\": \"0.5.1\"\n  },\n  \"scripts\": {\n    \"start\": \"react-scripts start\",\n    \"build\": \"react-scripts build\",\n    \"test\": \"react-scripts test --env=jsdom\",\n    \"eject\": \"react-scripts eject\"\n  }\n}\n```\n\n</Sandpack>\n\nImmer를 사용하면 **`artwork.seen = nextSeen`과 같이 변경해도 괜찮다는 것에 유의하세요.**\n\n```js\nupdateMyTodos(draft => {\n  const artwork = draft.find(a => a.id === artworkId);\n  artwork.seen = nextSeen;\n});\n```\n\n이는 *원본* state를 변경하는 것이 아니라, Immer에서 제공하는 특수 `draft` 객체를 변경하기 때문입니다. 마찬가지로 `push()`와 `pop()`같은 변경 함수들도 `draft`의 컨텐츠에 적용할 수 있습니다.\n\n내부적으로 Immer는 항상 `draft`에서 수행한 변경 사항에 따라 처음부터 다음 state를 구성합니다. 이렇게 하면 state를 변경하지 않고도 이벤트 핸들러를 매우 간결하게 유지할 수 있습니다.\n\n<Recap>\n\n- 배열을 state로 만들 수 있지만 변경하면 안됩니다.\n- 배열을 변경하는 대신 배열의 *새로운* 버전을 만들고, state를 업데이트 해야합니다.\n- `[...arr, newItem]` 배열 전개 구문을 사용하여 새 항목을 포함한 배열을 생성할 수 있습니다.\n- `filter()`와 `map()`을 사용하여 필터링된 항목들이나 변환된 항목들을 가진 배열을 만들 수 있습니다.\n- Immer를 사용하여 코드 간결성을 유지할 수 있습니다.\n\n</Recap>\n\n\n\n<Challenges>\n\n#### 장바구니의 항목 업데이트하기 {/*update-an-item-in-the-shopping-cart*/}\n\n\"+\" 버튼을 누르면 해당 숫자가 증가하도록 `handleIncreaseClick` 로직을 채워보세요.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nconst initialProducts = [{\n  id: 0,\n  name: 'Baklava',\n  count: 1,\n}, {\n  id: 1,\n  name: 'Cheese',\n  count: 5,\n}, {\n  id: 2,\n  name: 'Spaghetti',\n  count: 2,\n}];\n\nexport default function ShoppingCart() {\n  const [\n    products,\n    setProducts\n  ] = useState(initialProducts)\n\n  function handleIncreaseClick(productId) {\n\n  }\n\n  return (\n    <ul>\n      {products.map(product => (\n        <li key={product.id}>\n          {product.name}\n          {' '}\n          (<b>{product.count}</b>)\n          <button onClick={() => {\n            handleIncreaseClick(product.id);\n          }}>\n            +\n          </button>\n        </li>\n      ))}\n    </ul>\n  );\n}\n```\n\n```css\nbutton { margin: 5px; }\n```\n\n</Sandpack>\n\n<Solution>\n\n`map` 함수를 사용하여 새 배열을 생성하고 `...` 객체 전개 구문을 사용하여 새 배열에 넣을 변경된 객체의 복사본을 만들 수 있습니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nconst initialProducts = [{\n  id: 0,\n  name: 'Baklava',\n  count: 1,\n}, {\n  id: 1,\n  name: 'Cheese',\n  count: 5,\n}, {\n  id: 2,\n  name: 'Spaghetti',\n  count: 2,\n}];\n\nexport default function ShoppingCart() {\n  const [\n    products,\n    setProducts\n  ] = useState(initialProducts)\n\n  function handleIncreaseClick(productId) {\n    setProducts(products.map(product => {\n      if (product.id === productId) {\n        return {\n          ...product,\n          count: product.count + 1\n        };\n      } else {\n        return product;\n      }\n    }))\n  }\n\n  return (\n    <ul>\n      {products.map(product => (\n        <li key={product.id}>\n          {product.name}\n          {' '}\n          (<b>{product.count}</b>)\n          <button onClick={() => {\n            handleIncreaseClick(product.id);\n          }}>\n            +\n          </button>\n        </li>\n      ))}\n    </ul>\n  );\n}\n```\n\n```css\nbutton { margin: 5px; }\n```\n\n</Sandpack>\n\n</Solution>\n\n#### 장바구니에서 항목 제거하기 {/*remove-an-item-from-the-shopping-cart*/}\n\n이 장바구니에는 작동하는 \"+\" 버튼이 있지만 \"-\" 버튼은 아무 기능도 하지 않습니다. 이 \"-\" 버튼에 해당 상품의 `count`가 감소하도록 하는 이벤트 핸들러를 추가해야 합니다. count가 1일 때 \"-\"를 누르면 상품이 장바구니에서 자동으로 제거됩니다. 0이 표시되지 않도록 합니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nconst initialProducts = [{\n  id: 0,\n  name: 'Baklava',\n  count: 1,\n}, {\n  id: 1,\n  name: 'Cheese',\n  count: 5,\n}, {\n  id: 2,\n  name: 'Spaghetti',\n  count: 2,\n}];\n\nexport default function ShoppingCart() {\n  const [\n    products,\n    setProducts\n  ] = useState(initialProducts)\n\n  function handleIncreaseClick(productId) {\n    setProducts(products.map(product => {\n      if (product.id === productId) {\n        return {\n          ...product,\n          count: product.count + 1\n        };\n      } else {\n        return product;\n      }\n    }))\n  }\n\n  return (\n    <ul>\n      {products.map(product => (\n        <li key={product.id}>\n          {product.name}\n          {' '}\n          (<b>{product.count}</b>)\n          <button onClick={() => {\n            handleIncreaseClick(product.id);\n          }}>\n            +\n          </button>\n          <button>\n            –\n          </button>\n        </li>\n      ))}\n    </ul>\n  );\n}\n```\n\n```css\nbutton { margin: 5px; }\n```\n\n</Sandpack>\n\n<Solution>\n\n먼저 `map`을 사용하여 새 배열을 만들고 `filter`로 `count`가 `0`인 상품들을 제거할 수 있습니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nconst initialProducts = [{\n  id: 0,\n  name: 'Baklava',\n  count: 1,\n}, {\n  id: 1,\n  name: 'Cheese',\n  count: 5,\n}, {\n  id: 2,\n  name: 'Spaghetti',\n  count: 2,\n}];\n\nexport default function ShoppingCart() {\n  const [\n    products,\n    setProducts\n  ] = useState(initialProducts)\n\n  function handleIncreaseClick(productId) {\n    setProducts(products.map(product => {\n      if (product.id === productId) {\n        return {\n          ...product,\n          count: product.count + 1\n        };\n      } else {\n        return product;\n      }\n    }))\n  }\n\n  function handleDecreaseClick(productId) {\n    let nextProducts = products.map(product => {\n      if (product.id === productId) {\n        return {\n          ...product,\n          count: product.count - 1\n        };\n      } else {\n        return product;\n      }\n    });\n    nextProducts = nextProducts.filter(p =>\n      p.count > 0\n    );\n    setProducts(nextProducts)\n  }\n\n  return (\n    <ul>\n      {products.map(product => (\n        <li key={product.id}>\n          {product.name}\n          {' '}\n          (<b>{product.count}</b>)\n          <button onClick={() => {\n            handleIncreaseClick(product.id);\n          }}>\n            +\n          </button>\n          <button onClick={() => {\n            handleDecreaseClick(product.id);\n          }}>\n            –\n          </button>\n        </li>\n      ))}\n    </ul>\n  );\n}\n```\n\n```css\nbutton { margin: 5px; }\n```\n\n</Sandpack>\n\n</Solution>\n\n#### 비변경 함수를 사용하여 변경 수정하기 {/*fix-the-mutations-using-non-mutative-methods*/}\n\n이 예시에서 `App.js`의 모든 이벤트 핸들러는 변경을 사용합니다. 결과적으로 todos를 편집하거나 삭제하는 기능이 동작하지 않습니다. 비변경 함수를 사용하도록 `handleAddTodo`, `handleChangeTodo` 그리고 `handleDeleteTodo`를 다시 작성해보세요.\n\n<Sandpack>\n\n```js src/App.js\nimport { useState } from 'react';\nimport AddTodo from './AddTodo.js';\nimport TaskList from './TaskList.js';\n\nlet nextId = 3;\nconst initialTodos = [\n  { id: 0, title: 'Buy milk', done: true },\n  { id: 1, title: 'Eat tacos', done: false },\n  { id: 2, title: 'Brew tea', done: false },\n];\n\nexport default function TaskApp() {\n  const [todos, setTodos] = useState(\n    initialTodos\n  );\n\n  function handleAddTodo(title) {\n    todos.push({\n      id: nextId++,\n      title: title,\n      done: false\n    });\n  }\n\n  function handleChangeTodo(nextTodo) {\n    const todo = todos.find(t =>\n      t.id === nextTodo.id\n    );\n    todo.title = nextTodo.title;\n    todo.done = nextTodo.done;\n  }\n\n  function handleDeleteTodo(todoId) {\n    const index = todos.findIndex(t =>\n      t.id === todoId\n    );\n    todos.splice(index, 1);\n  }\n\n  return (\n    <>\n      <AddTodo\n        onAddTodo={handleAddTodo}\n      />\n      <TaskList\n        todos={todos}\n        onChangeTodo={handleChangeTodo}\n        onDeleteTodo={handleDeleteTodo}\n      />\n    </>\n  );\n}\n```\n\n```js src/AddTodo.js\nimport { useState } from 'react';\n\nexport default function AddTodo({ onAddTodo }) {\n  const [title, setTitle] = useState('');\n  return (\n    <>\n      <input\n        placeholder=\"Add todo\"\n        value={title}\n        onChange={e => setTitle(e.target.value)}\n      />\n      <button onClick={() => {\n        setTitle('');\n        onAddTodo(title);\n      }}>Add</button>\n    </>\n  )\n}\n```\n\n```js src/TaskList.js\nimport { useState } from 'react';\n\nexport default function TaskList({\n  todos,\n  onChangeTodo,\n  onDeleteTodo\n}) {\n  return (\n    <ul>\n      {todos.map(todo => (\n        <li key={todo.id}>\n          <Task\n            todo={todo}\n            onChange={onChangeTodo}\n            onDelete={onDeleteTodo}\n          />\n        </li>\n      ))}\n    </ul>\n  );\n}\n\nfunction Task({ todo, onChange, onDelete }) {\n  const [isEditing, setIsEditing] = useState(false);\n  let todoContent;\n  if (isEditing) {\n    todoContent = (\n      <>\n        <input\n          value={todo.title}\n          onChange={e => {\n            onChange({\n              ...todo,\n              title: e.target.value\n            });\n          }} />\n        <button onClick={() => setIsEditing(false)}>\n          Save\n        </button>\n      </>\n    );\n  } else {\n    todoContent = (\n      <>\n        {todo.title}\n        <button onClick={() => setIsEditing(true)}>\n          Edit\n        </button>\n      </>\n    );\n  }\n  return (\n    <label>\n      <input\n        type=\"checkbox\"\n        checked={todo.done}\n        onChange={e => {\n          onChange({\n            ...todo,\n            done: e.target.checked\n          });\n        }}\n      />\n      {todoContent}\n      <button onClick={() => onDelete(todo.id)}>\n        Delete\n      </button>\n    </label>\n  );\n}\n```\n\n```css\nbutton { margin: 5px; }\nli { list-style-type: none; }\nul, li { margin: 0; padding: 0; }\n```\n\n</Sandpack>\n\n<Solution>\n\n`handleAddTodo`에서는 배열 전개 구문을 사용할 수 있습니다. `handleChanageTodo`에서는 `map`을 사용하여 새 배열을 만들 수 있습니다. `handleDeleteTodo`에서는 `filter`를 사용해 새 배열을 만들 수 있습니다. 이제 목록이 정상적으로 동작합니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { useState } from 'react';\nimport AddTodo from './AddTodo.js';\nimport TaskList from './TaskList.js';\n\nlet nextId = 3;\nconst initialTodos = [\n  { id: 0, title: 'Buy milk', done: true },\n  { id: 1, title: 'Eat tacos', done: false },\n  { id: 2, title: 'Brew tea', done: false },\n];\n\nexport default function TaskApp() {\n  const [todos, setTodos] = useState(\n    initialTodos\n  );\n\n  function handleAddTodo(title) {\n    setTodos([\n      ...todos,\n      {\n        id: nextId++,\n        title: title,\n        done: false\n      }\n    ]);\n  }\n\n  function handleChangeTodo(nextTodo) {\n    setTodos(todos.map(t => {\n      if (t.id === nextTodo.id) {\n        return nextTodo;\n      } else {\n        return t;\n      }\n    }));\n  }\n\n  function handleDeleteTodo(todoId) {\n    setTodos(\n      todos.filter(t => t.id !== todoId)\n    );\n  }\n\n  return (\n    <>\n      <AddTodo\n        onAddTodo={handleAddTodo}\n      />\n      <TaskList\n        todos={todos}\n        onChangeTodo={handleChangeTodo}\n        onDeleteTodo={handleDeleteTodo}\n      />\n    </>\n  );\n}\n```\n\n```js src/AddTodo.js\nimport { useState } from 'react';\n\nexport default function AddTodo({ onAddTodo }) {\n  const [title, setTitle] = useState('');\n  return (\n    <>\n      <input\n        placeholder=\"Add todo\"\n        value={title}\n        onChange={e => setTitle(e.target.value)}\n      />\n      <button onClick={() => {\n        setTitle('');\n        onAddTodo(title);\n      }}>Add</button>\n    </>\n  )\n}\n```\n\n```js src/TaskList.js\nimport { useState } from 'react';\n\nexport default function TaskList({\n  todos,\n  onChangeTodo,\n  onDeleteTodo\n}) {\n  return (\n    <ul>\n      {todos.map(todo => (\n        <li key={todo.id}>\n          <Task\n            todo={todo}\n            onChange={onChangeTodo}\n            onDelete={onDeleteTodo}\n          />\n        </li>\n      ))}\n    </ul>\n  );\n}\n\nfunction Task({ todo, onChange, onDelete }) {\n  const [isEditing, setIsEditing] = useState(false);\n  let todoContent;\n  if (isEditing) {\n    todoContent = (\n      <>\n        <input\n          value={todo.title}\n          onChange={e => {\n            onChange({\n              ...todo,\n              title: e.target.value\n            });\n          }} />\n        <button onClick={() => setIsEditing(false)}>\n          Save\n        </button>\n      </>\n    );\n  } else {\n    todoContent = (\n      <>\n        {todo.title}\n        <button onClick={() => setIsEditing(true)}>\n          Edit\n        </button>\n      </>\n    );\n  }\n  return (\n    <label>\n      <input\n        type=\"checkbox\"\n        checked={todo.done}\n        onChange={e => {\n          onChange({\n            ...todo,\n            done: e.target.checked\n          });\n        }}\n      />\n      {todoContent}\n      <button onClick={() => onDelete(todo.id)}>\n        Delete\n      </button>\n    </label>\n  );\n}\n```\n\n```css\nbutton { margin: 5px; }\nli { list-style-type: none; }\nul, li { margin: 0; padding: 0; }\n```\n\n</Sandpack>\n\n</Solution>\n\n\n#### Immer를 사용해서 변경 수정하기 {/*fix-the-mutations-using-immer*/}\n\n이 예시는 이전 예시와 동일한 예시입니다. 이번에는 Immer를 사용하여 변경을 수정합니다. 편의를 위해 `useImmer`는 이미 import되어 있으므로 `todos` state 변수를 사용하도록 수정해야 합니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { useState } from 'react';\nimport { useImmer } from 'use-immer';\nimport AddTodo from './AddTodo.js';\nimport TaskList from './TaskList.js';\n\nlet nextId = 3;\nconst initialTodos = [\n  { id: 0, title: 'Buy milk', done: true },\n  { id: 1, title: 'Eat tacos', done: false },\n  { id: 2, title: 'Brew tea', done: false },\n];\n\nexport default function TaskApp() {\n  const [todos, setTodos] = useState(\n    initialTodos\n  );\n\n  function handleAddTodo(title) {\n    todos.push({\n      id: nextId++,\n      title: title,\n      done: false\n    });\n  }\n\n  function handleChangeTodo(nextTodo) {\n    const todo = todos.find(t =>\n      t.id === nextTodo.id\n    );\n    todo.title = nextTodo.title;\n    todo.done = nextTodo.done;\n  }\n\n  function handleDeleteTodo(todoId) {\n    const index = todos.findIndex(t =>\n      t.id === todoId\n    );\n    todos.splice(index, 1);\n  }\n\n  return (\n    <>\n      <AddTodo\n        onAddTodo={handleAddTodo}\n      />\n      <TaskList\n        todos={todos}\n        onChangeTodo={handleChangeTodo}\n        onDeleteTodo={handleDeleteTodo}\n      />\n    </>\n  );\n}\n```\n\n```js src/AddTodo.js\nimport { useState } from 'react';\n\nexport default function AddTodo({ onAddTodo }) {\n  const [title, setTitle] = useState('');\n  return (\n    <>\n      <input\n        placeholder=\"Add todo\"\n        value={title}\n        onChange={e => setTitle(e.target.value)}\n      />\n      <button onClick={() => {\n        setTitle('');\n        onAddTodo(title);\n      }}>Add</button>\n    </>\n  )\n}\n```\n\n```js src/TaskList.js\nimport { useState } from 'react';\n\nexport default function TaskList({\n  todos,\n  onChangeTodo,\n  onDeleteTodo\n}) {\n  return (\n    <ul>\n      {todos.map(todo => (\n        <li key={todo.id}>\n          <Task\n            todo={todo}\n            onChange={onChangeTodo}\n            onDelete={onDeleteTodo}\n          />\n        </li>\n      ))}\n    </ul>\n  );\n}\n\nfunction Task({ todo, onChange, onDelete }) {\n  const [isEditing, setIsEditing] = useState(false);\n  let todoContent;\n  if (isEditing) {\n    todoContent = (\n      <>\n        <input\n          value={todo.title}\n          onChange={e => {\n            onChange({\n              ...todo,\n              title: e.target.value\n            });\n          }} />\n        <button onClick={() => setIsEditing(false)}>\n          Save\n        </button>\n      </>\n    );\n  } else {\n    todoContent = (\n      <>\n        {todo.title}\n        <button onClick={() => setIsEditing(true)}>\n          Edit\n        </button>\n      </>\n    );\n  }\n  return (\n    <label>\n      <input\n        type=\"checkbox\"\n        checked={todo.done}\n        onChange={e => {\n          onChange({\n            ...todo,\n            done: e.target.checked\n          });\n        }}\n      />\n      {todoContent}\n      <button onClick={() => onDelete(todo.id)}>\n        Delete\n      </button>\n    </label>\n  );\n}\n```\n\n```css\nbutton { margin: 5px; }\nli { list-style-type: none; }\nul, li { margin: 0; padding: 0; }\n```\n\n```json package.json\n{\n  \"dependencies\": {\n    \"immer\": \"1.7.3\",\n    \"react\": \"latest\",\n    \"react-dom\": \"latest\",\n    \"react-scripts\": \"latest\",\n    \"use-immer\": \"0.5.1\"\n  },\n  \"scripts\": {\n    \"start\": \"react-scripts start\",\n    \"build\": \"react-scripts build\",\n    \"test\": \"react-scripts test --env=jsdom\",\n    \"eject\": \"react-scripts eject\"\n  }\n}\n```\n\n</Sandpack>\n\n<Solution>\n\nImmer를 사용하면, Immer가 제공하는 `draft`의 일부만 변경하는 방식으로 코드를 작성할 수 있습니다. 여기에서 모든 변경은 `draft`에서 수행되므로 코드가 잘 동작합니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { useState } from 'react';\nimport { useImmer } from 'use-immer';\nimport AddTodo from './AddTodo.js';\nimport TaskList from './TaskList.js';\n\nlet nextId = 3;\nconst initialTodos = [\n  { id: 0, title: 'Buy milk', done: true },\n  { id: 1, title: 'Eat tacos', done: false },\n  { id: 2, title: 'Brew tea', done: false },\n];\n\nexport default function TaskApp() {\n  const [todos, updateTodos] = useImmer(\n    initialTodos\n  );\n\n  function handleAddTodo(title) {\n    updateTodos(draft => {\n      draft.push({\n        id: nextId++,\n        title: title,\n        done: false\n      });\n    });\n  }\n\n  function handleChangeTodo(nextTodo) {\n    updateTodos(draft => {\n      const todo = draft.find(t =>\n        t.id === nextTodo.id\n      );\n      todo.title = nextTodo.title;\n      todo.done = nextTodo.done;\n    });\n  }\n\n  function handleDeleteTodo(todoId) {\n    updateTodos(draft => {\n      const index = draft.findIndex(t =>\n        t.id === todoId\n      );\n      draft.splice(index, 1);\n    });\n  }\n\n  return (\n    <>\n      <AddTodo\n        onAddTodo={handleAddTodo}\n      />\n      <TaskList\n        todos={todos}\n        onChangeTodo={handleChangeTodo}\n        onDeleteTodo={handleDeleteTodo}\n      />\n    </>\n  );\n}\n```\n\n```js src/AddTodo.js\nimport { useState } from 'react';\n\nexport default function AddTodo({ onAddTodo }) {\n  const [title, setTitle] = useState('');\n  return (\n    <>\n      <input\n        placeholder=\"Add todo\"\n        value={title}\n        onChange={e => setTitle(e.target.value)}\n      />\n      <button onClick={() => {\n        setTitle('');\n        onAddTodo(title);\n      }}>Add</button>\n    </>\n  )\n}\n```\n\n```js src/TaskList.js\nimport { useState } from 'react';\n\nexport default function TaskList({\n  todos,\n  onChangeTodo,\n  onDeleteTodo\n}) {\n  return (\n    <ul>\n      {todos.map(todo => (\n        <li key={todo.id}>\n          <Task\n            todo={todo}\n            onChange={onChangeTodo}\n            onDelete={onDeleteTodo}\n          />\n        </li>\n      ))}\n    </ul>\n  );\n}\n\nfunction Task({ todo, onChange, onDelete }) {\n  const [isEditing, setIsEditing] = useState(false);\n  let todoContent;\n  if (isEditing) {\n    todoContent = (\n      <>\n        <input\n          value={todo.title}\n          onChange={e => {\n            onChange({\n              ...todo,\n              title: e.target.value\n            });\n          }} />\n        <button onClick={() => setIsEditing(false)}>\n          Save\n        </button>\n      </>\n    );\n  } else {\n    todoContent = (\n      <>\n        {todo.title}\n        <button onClick={() => setIsEditing(true)}>\n          Edit\n        </button>\n      </>\n    );\n  }\n  return (\n    <label>\n      <input\n        type=\"checkbox\"\n        checked={todo.done}\n        onChange={e => {\n          onChange({\n            ...todo,\n            done: e.target.checked\n          });\n        }}\n      />\n      {todoContent}\n      <button onClick={() => onDelete(todo.id)}>\n        Delete\n      </button>\n    </label>\n  );\n}\n```\n\n```css\nbutton { margin: 5px; }\nli { list-style-type: none; }\nul, li { margin: 0; padding: 0; }\n```\n\n```json package.json\n{\n  \"dependencies\": {\n    \"immer\": \"1.7.3\",\n    \"react\": \"latest\",\n    \"react-dom\": \"latest\",\n    \"react-scripts\": \"latest\",\n    \"use-immer\": \"0.5.1\"\n  },\n  \"scripts\": {\n    \"start\": \"react-scripts start\",\n    \"build\": \"react-scripts build\",\n    \"test\": \"react-scripts test --env=jsdom\",\n    \"eject\": \"react-scripts eject\"\n  }\n}\n```\n\n</Sandpack>\n\n또한 Immer를 사용하여 변경 및 비변경 접근 방식을 함께 사용할 수 있습니다.\n\n예를 들어, 이 버전에서 `handleAddTodo`는 Immer의 `draft`를 변경하여 구현되는 반면, `handleChangeTodo`와 `handleDeleteTodo`는 비변경 함수인 `map`과 `filter` 함수를 사용합니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { useState } from 'react';\nimport { useImmer } from 'use-immer';\nimport AddTodo from './AddTodo.js';\nimport TaskList from './TaskList.js';\n\nlet nextId = 3;\nconst initialTodos = [\n  { id: 0, title: 'Buy milk', done: true },\n  { id: 1, title: 'Eat tacos', done: false },\n  { id: 2, title: 'Brew tea', done: false },\n];\n\nexport default function TaskApp() {\n  const [todos, updateTodos] = useImmer(\n    initialTodos\n  );\n\n  function handleAddTodo(title) {\n    updateTodos(draft => {\n      draft.push({\n        id: nextId++,\n        title: title,\n        done: false\n      });\n    });\n  }\n\n  function handleChangeTodo(nextTodo) {\n    updateTodos(todos.map(todo => {\n      if (todo.id === nextTodo.id) {\n        return nextTodo;\n      } else {\n        return todo;\n      }\n    }));\n  }\n\n  function handleDeleteTodo(todoId) {\n    updateTodos(\n      todos.filter(t => t.id !== todoId)\n    );\n  }\n\n  return (\n    <>\n      <AddTodo\n        onAddTodo={handleAddTodo}\n      />\n      <TaskList\n        todos={todos}\n        onChangeTodo={handleChangeTodo}\n        onDeleteTodo={handleDeleteTodo}\n      />\n    </>\n  );\n}\n```\n\n```js src/AddTodo.js\nimport { useState } from 'react';\n\nexport default function AddTodo({ onAddTodo }) {\n  const [title, setTitle] = useState('');\n  return (\n    <>\n      <input\n        placeholder=\"Add todo\"\n        value={title}\n        onChange={e => setTitle(e.target.value)}\n      />\n      <button onClick={() => {\n        setTitle('');\n        onAddTodo(title);\n      }}>Add</button>\n    </>\n  )\n}\n```\n\n```js src/TaskList.js\nimport { useState } from 'react';\n\nexport default function TaskList({\n  todos,\n  onChangeTodo,\n  onDeleteTodo\n}) {\n  return (\n    <ul>\n      {todos.map(todo => (\n        <li key={todo.id}>\n          <Task\n            todo={todo}\n            onChange={onChangeTodo}\n            onDelete={onDeleteTodo}\n          />\n        </li>\n      ))}\n    </ul>\n  );\n}\n\nfunction Task({ todo, onChange, onDelete }) {\n  const [isEditing, setIsEditing] = useState(false);\n  let todoContent;\n  if (isEditing) {\n    todoContent = (\n      <>\n        <input\n          value={todo.title}\n          onChange={e => {\n            onChange({\n              ...todo,\n              title: e.target.value\n            });\n          }} />\n        <button onClick={() => setIsEditing(false)}>\n          Save\n        </button>\n      </>\n    );\n  } else {\n    todoContent = (\n      <>\n        {todo.title}\n        <button onClick={() => setIsEditing(true)}>\n          Edit\n        </button>\n      </>\n    );\n  }\n  return (\n    <label>\n      <input\n        type=\"checkbox\"\n        checked={todo.done}\n        onChange={e => {\n          onChange({\n            ...todo,\n            done: e.target.checked\n          });\n        }}\n      />\n      {todoContent}\n      <button onClick={() => onDelete(todo.id)}>\n        Delete\n      </button>\n    </label>\n  );\n}\n```\n\n```css\nbutton { margin: 5px; }\nli { list-style-type: none; }\nul, li { margin: 0; padding: 0; }\n```\n\n```json package.json\n{\n  \"dependencies\": {\n    \"immer\": \"1.7.3\",\n    \"react\": \"latest\",\n    \"react-dom\": \"latest\",\n    \"react-scripts\": \"latest\",\n    \"use-immer\": \"0.5.1\"\n  },\n  \"scripts\": {\n    \"start\": \"react-scripts start\",\n    \"build\": \"react-scripts build\",\n    \"test\": \"react-scripts test --env=jsdom\",\n    \"eject\": \"react-scripts eject\"\n  }\n}\n```\n\n</Sandpack>\n\nImmer를 사용하면 각각의 케이스에서 가장 자연스러운 방식을 선택할 수 있습니다.\n\n</Solution>\n\n</Challenges>\n"
  },
  {
    "path": "src/content/learn/updating-objects-in-state.md",
    "content": "---\ntitle: 객체 State 업데이트하기\n---\n\n<Intro>\n\nState는 객체를 포함한 모든 종류의 자바스크립트 값을 가질 수 있습니다. 하지만 React state가 가진 객체를 직접 변경해서는 안 됩니다. 객체를 업데이트하고 싶을 때는 새로운 객체를 생성하여 (또는 기존 객체의 복사본을 만들어), state가 복사본을 사용하도록 하세요.\n\n</Intro>\n\n<YouWillLearn>\n\n- React state에서 객체를 올바르게 업데이트하는 방법\n- 중첩된 객체를 변경하지 않고 업데이트하는 방법\n- 불변성이란 무엇인지, 그리고 불변성을 지키는 방법\n- Immer로 반복을 줄여 객체를 복사하는 방법\n\n</YouWillLearn>\n\n## 변경이란? {/*whats-a-mutation*/}\n\nState에는 모든 종류의 자바스크립트 값을 저장할 수 있습니다.\n\n```js\nconst [x, setX] = useState(0);\n```\n\n지금까지 숫자, 문자열, 불리언을 다루었습니다. 이러한 자바스크립트 값들은 변경할 수 없거나 \"읽기 전용\"을 의미하는 \"불변성\"을 가집니다. 값을 _교체_ 하기 위해서는 리렌더링이 필요합니다.\n\n```js\nsetX(5);\n```\n\n`x` state는 `0`에서 `5`로 바뀌었지만, _숫자 `0` 자체_ 는 바뀌지 않았습니다. 숫자, 문자열, 불리언과 같이 자바스크립트에 정의되어 있는 원시 값들은 변경할 수 없습니다.\n\nstate에 있는 이러한 객체를 생각해보세요.\n\n```js\nconst [position, setPosition] = useState({ x: 0, y: 0 });\n```\n\n기술적으로 _객체 자체_ 의 내용은 바꿀 수 있습니다. **이것을 변경(mutation)이라고 합니다.**\n\n```js\nposition.x = 5;\n```\n\n하지만 React state의 객체들이 기술적으로 변경 가능할지라도, 숫자, 불리언, 문자열과 같이 불변성을 가진 것처럼 다루어야 합니다. 객체를 변경하는 대신 교체해야 합니다.\n\n## State를 읽기 전용인 것처럼 다루세요 {/*treat-state-as-read-only*/}\n\n다시 말하면, **state에 저장한 자바스크립트 객체는 어떤 것이라도 읽기 전용인 것처럼** 다루어야 합니다.\n\n아래 예시에서 state의 object는 현재 포인터 위치를 나타냅니다. 프리뷰 영역을 누르거나 커서를 움직일 때 빨간 점이 이동해야 합니다. 하지만 점은 초기 위치에 머무릅니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function MovingDot() {\n  const [position, setPosition] = useState({\n    x: 0,\n    y: 0\n  });\n  return (\n    <div\n      onPointerMove={e => {\n        position.x = e.clientX;\n        position.y = e.clientY;\n      }}\n      style={{\n        position: 'relative',\n        width: '100vw',\n        height: '100vh',\n      }}>\n      <div style={{\n        position: 'absolute',\n        backgroundColor: 'red',\n        borderRadius: '50%',\n        transform: `translate(${position.x}px, ${position.y}px)`,\n        left: -10,\n        top: -10,\n        width: 20,\n        height: 20,\n      }} />\n    </div>\n  )\n}\n```\n\n```css\nbody { margin: 0; padding: 0; height: 250px; }\n```\n\n</Sandpack>\n\n문제는 이 코드입니다.\n\n```js\nonPointerMove={e => {\n  position.x = e.clientX;\n  position.y = e.clientY;\n}}\n```\n\n이 코드는 `position`에 할당된 객체를 [이전 렌더링](/learn/state-as-a-snapshot#rendering-takes-a-snapshot-in-time)에서 수정합니다. 그러나 React는 state 설정 함수가 없으면 객체가 변경되었는지 알 수 없습니다. 따라서 React는 아무것도 하지 않습니다. 이는 식사를 한 뒤에 주문을 바꾸려는 것과 같습니다. state를 변경하는 것이 어떤 경우에는 동작할 수 있지만, 권장하지 않습니다. 렌더링 시에 접근하려는 state 값은 읽기 전용처럼 다루어야 합니다.\n\n이러한 경우에 [리렌더링을 발생시키려면](/learn/state-as-a-snapshot#setting-state-triggers-renders), ***새* 객체를 생성하여 state 설정 함수로 전달하세요**\n\n```js\nonPointerMove={e => {\n  setPosition({\n    x: e.clientX,\n    y: e.clientY\n  });\n}}\n```\n\n`setPosition`은 React에게 다음과 같이 요청합니다.\n\n* `position`을 이 새로운 객체로 교체하라\n* 그리고 이 컴포넌트를 다시 렌더링하라\n\n이제 프리뷰 영역을 누르거나 hover 시에 빨간 점이 포인터를 따라오는 것을 볼 수 있습니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function MovingDot() {\n  const [position, setPosition] = useState({\n    x: 0,\n    y: 0\n  });\n  return (\n    <div\n      onPointerMove={e => {\n        setPosition({\n          x: e.clientX,\n          y: e.clientY\n        });\n      }}\n      style={{\n        position: 'relative',\n        width: '100vw',\n        height: '100vh',\n      }}>\n      <div style={{\n        position: 'absolute',\n        backgroundColor: 'red',\n        borderRadius: '50%',\n        transform: `translate(${position.x}px, ${position.y}px)`,\n        left: -10,\n        top: -10,\n        width: 20,\n        height: 20,\n      }} />\n    </div>\n  )\n}\n```\n\n```css\nbody { margin: 0; padding: 0; height: 250px; }\n```\n\n</Sandpack>\n\n<DeepDive>\n\n#### 지역 변경은 괜찮습니다 {/*local-mutation-is-fine*/}\n\n이 코드는 state에 *존재하는* 객체를 변경하기에 문제가 됩니다.\n\n```js\nposition.x = e.clientX;\nposition.y = e.clientY;\n```\n\n하지만 이 코드는 *방금 생성한* 새로운 객체를 변경하기 때문에 **적절합니다**.\n\n```js\nconst nextPosition = {};\nnextPosition.x = e.clientX;\nnextPosition.y = e.clientY;\nsetPosition(nextPosition);\n```\n\n위 코드는 아래처럼 작성할 수 있습니다.\n\n```js\nsetPosition({\n  x: e.clientX,\n  y: e.clientY\n});\n```\n\n변경은 이미 state에 *존재하는* 객체를 변경할 때만 문제가 됩니다. 방금 만든 객체를 수정하는 것은 *아직 다른 코드가 해당 객체를 참조하지 않기 때문에* 괜찮습니다. 그 객체를 변경하는 것은 해당 객체에 의존하는 무언가에 우연히 영향을 주지 않습니다. 이것은 \"지역 변경 local mutation\" 이라고 합니다. [렌더링하는 동안](/learn/keeping-components-pure#local-mutation-your-components-little-secret) 지역 변경을 할 수도 있으며, 이는 아주 편리합니다!\n\n</DeepDive>\n\n## 전개 문법으로 객체 복사하기 {/*copying-objects-with-the-spread-syntax*/}\n\n이전 예시에서 `position` 객체는 현재 커서 위치에서 항상 새롭게 생성됩니다. 하지만 종종 새로 생성하는 객체에 *존재하는* 데이터를 포함하고 싶을 수 있습니다. 예를 들어 폼에서 *단 한 개*의 필드만 수정하고, 나머지 모든 필드는 이전 값을 유지하고 싶을 수 있습니다.\n\n이 input 필드는 `onChange` 핸들러가 state를 변경하기 때문에 동작하지 않습니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function Form() {\n  const [person, setPerson] = useState({\n    firstName: 'Barbara',\n    lastName: 'Hepworth',\n    email: 'bhepworth@sculpture.com'\n  });\n\n  function handleFirstNameChange(e) {\n    person.firstName = e.target.value;\n  }\n\n  function handleLastNameChange(e) {\n    person.lastName = e.target.value;\n  }\n\n  function handleEmailChange(e) {\n    person.email = e.target.value;\n  }\n\n  return (\n    <>\n      <label>\n        First name:\n        <input\n          value={person.firstName}\n          onChange={handleFirstNameChange}\n        />\n      </label>\n      <label>\n        Last name:\n        <input\n          value={person.lastName}\n          onChange={handleLastNameChange}\n        />\n      </label>\n      <label>\n        Email:\n        <input\n          value={person.email}\n          onChange={handleEmailChange}\n        />\n      </label>\n      <p>\n        {person.firstName}{' '}\n        {person.lastName}{' '}\n        ({person.email})\n      </p>\n    </>\n  );\n}\n```\n\n```css\nlabel { display: block; }\ninput { margin-left: 5px; margin-bottom: 5px; }\n```\n\n</Sandpack>\n\n예를 들어, 이 코드는 이전 렌더의 state를 변경합니다.\n\n```js\nperson.firstName = e.target.value;\n```\n\n원하는 동작을 정확히 얻기 위해서는 새로운 객체를 생성하여 `setPerson`으로 전달해야 합니다. 하지만, 단 하나의 필드가 바뀌었기 때문에 **기존에 존재하는 다른 데이터를 복사**해야 합니다.\n\n```js\nsetPerson({\n  firstName: e.target.value, // input의 새로운 first name\n  lastName: person.lastName,\n  email: person.email\n});\n```\n\n`...` [객체 전개](a-javascript-refresher#object-spread) 구문을 사용하면 모든 프로퍼티를 각각 복사하지 않아도 됩니다.\n\n```js\nsetPerson({\n  ...person, // 이전 필드를 복사\n  firstName: e.target.value // 새로운 부분은 덮어쓰기\n});\n```\n\n이제 폼이 동작합니다!\n\n각 input 필드에 대해 분리된 state를 선언하지 않았음을 기억하세요. 큰 폼들은 올바르게 업데이트한다면, 한 객체에 모든 데이터를 그룹화하여 저장하는 것이 편리합니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function Form() {\n  const [person, setPerson] = useState({\n    firstName: 'Barbara',\n    lastName: 'Hepworth',\n    email: 'bhepworth@sculpture.com'\n  });\n\n  function handleFirstNameChange(e) {\n    setPerson({\n      ...person,\n      firstName: e.target.value\n    });\n  }\n\n  function handleLastNameChange(e) {\n    setPerson({\n      ...person,\n      lastName: e.target.value\n    });\n  }\n\n  function handleEmailChange(e) {\n    setPerson({\n      ...person,\n      email: e.target.value\n    });\n  }\n\n  return (\n    <>\n      <label>\n        First name:\n        <input\n          value={person.firstName}\n          onChange={handleFirstNameChange}\n        />\n      </label>\n      <label>\n        Last name:\n        <input\n          value={person.lastName}\n          onChange={handleLastNameChange}\n        />\n      </label>\n      <label>\n        Email:\n        <input\n          value={person.email}\n          onChange={handleEmailChange}\n        />\n      </label>\n      <p>\n        {person.firstName}{' '}\n        {person.lastName}{' '}\n        ({person.email})\n      </p>\n    </>\n  );\n}\n```\n\n```css\nlabel { display: block; }\ninput { margin-left: 5px; margin-bottom: 5px; }\n```\n\n</Sandpack>\n\n`...` 전개 문법은 \"얕다\"는 점을 알아두세요. 이것은 한 레벨 깊이의 내용만 복사합니다. 빠르지만, 중첩된 프로퍼티를 업데이트하고 싶다면 한 번 이상 사용해야 한다는 뜻이기도 합니다.\n\n<DeepDive>\n\n#### 여러 필드에 단일 이벤트 핸들러 사용하기 {/*using-a-single-event-handler-for-multiple-fields*/}\n\n`[` 와 `]` 괄호를 객체 정의 안에 사용하여 동적 이름을 가진 프로퍼티를 명시할 수 있습니다. 아래에는 이전 예시와 같지만, 세 개의 다른 이벤트 핸들러 대신 하나의 이벤트 핸들러를 사용하는 예시가 있습니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function Form() {\n  const [person, setPerson] = useState({\n    firstName: 'Barbara',\n    lastName: 'Hepworth',\n    email: 'bhepworth@sculpture.com'\n  });\n\n  function handleChange(e) {\n    setPerson({\n      ...person,\n      [e.target.name]: e.target.value\n    });\n  }\n\n  return (\n    <>\n      <label>\n        First name:\n        <input\n          name=\"firstName\"\n          value={person.firstName}\n          onChange={handleChange}\n        />\n      </label>\n      <label>\n        Last name:\n        <input\n          name=\"lastName\"\n          value={person.lastName}\n          onChange={handleChange}\n        />\n      </label>\n      <label>\n        Email:\n        <input\n          name=\"email\"\n          value={person.email}\n          onChange={handleChange}\n        />\n      </label>\n      <p>\n        {person.firstName}{' '}\n        {person.lastName}{' '}\n        ({person.email})\n      </p>\n    </>\n  );\n}\n```\n\n```css\nlabel { display: block; }\ninput { margin-left: 5px; margin-bottom: 5px; }\n```\n\n</Sandpack>\n\n`e.target.name`은 `<input>` DOM 엘리먼트의 `name` 프로퍼티를 나타냅니다.\n\n</DeepDive>\n\n## 중첩된 객체 갱신하기 {/*updating-a-nested-object*/}\n\n아래와 같이 중첩된 객체 구조를 생각해 보세요.\n\n```js\nconst [person, setPerson] = useState({\n  name: 'Niki de Saint Phalle',\n  artwork: {\n    title: 'Blue Nana',\n    city: 'Hamburg',\n    image: 'https://i.imgur.com/Sd1AgUOm.jpg',\n  }\n});\n```\n\n`person.artwork.city`를 업데이트하고 싶다면, 변경하는 방법은 명백합니다.\n\n```js\nperson.artwork.city = 'New Delhi';\n```\n\n하지만 React에서는 state를 변경할 수 없는 것으로 다루어야 합니다! `city`를 바꾸기 위해서는 먼저 (이전 객체의 데이터로 생성된) 새로운 `artwork` 객체를 생성한 뒤, 그것을 가리키는 새로운 `person` 객체를 만들어야 합니다.\n\n```js\nconst nextArtwork = { ...person.artwork, city: 'New Delhi' };\nconst nextPerson = { ...person, artwork: nextArtwork };\nsetPerson(nextPerson);\n```\n\n또는 단순하게 함수를 호출할 수 있습니다.\n\n```js\nsetPerson({\n  ...person, // 다른 필드 복사\n  artwork: { // artwork 교체\n    ...person.artwork, // 동일한 값 사용\n    city: 'New Delhi' // 하지만 New Delhi!\n  }\n});\n```\n\n이 방법은 코드가 길어질 수 있지만 많은 경우에 정상적으로 동작합니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function Form() {\n  const [person, setPerson] = useState({\n    name: 'Niki de Saint Phalle',\n    artwork: {\n      title: 'Blue Nana',\n      city: 'Hamburg',\n      image: 'https://i.imgur.com/Sd1AgUOm.jpg',\n    }\n  });\n\n  function handleNameChange(e) {\n    setPerson({\n      ...person,\n      name: e.target.value\n    });\n  }\n\n  function handleTitleChange(e) {\n    setPerson({\n      ...person,\n      artwork: {\n        ...person.artwork,\n        title: e.target.value\n      }\n    });\n  }\n\n  function handleCityChange(e) {\n    setPerson({\n      ...person,\n      artwork: {\n        ...person.artwork,\n        city: e.target.value\n      }\n    });\n  }\n\n  function handleImageChange(e) {\n    setPerson({\n      ...person,\n      artwork: {\n        ...person.artwork,\n        image: e.target.value\n      }\n    });\n  }\n\n  return (\n    <>\n      <label>\n        Name:\n        <input\n          value={person.name}\n          onChange={handleNameChange}\n        />\n      </label>\n      <label>\n        Title:\n        <input\n          value={person.artwork.title}\n          onChange={handleTitleChange}\n        />\n      </label>\n      <label>\n        City:\n        <input\n          value={person.artwork.city}\n          onChange={handleCityChange}\n        />\n      </label>\n      <label>\n        Image:\n        <input\n          value={person.artwork.image}\n          onChange={handleImageChange}\n        />\n      </label>\n      <p>\n        <i>{person.artwork.title}</i>\n        {' by '}\n        {person.name}\n        <br />\n        (located in {person.artwork.city})\n      </p>\n      <img\n        src={person.artwork.image}\n        alt={person.artwork.title}\n      />\n    </>\n  );\n}\n```\n\n```css\nlabel { display: block; }\ninput { margin-left: 5px; margin-bottom: 5px; }\nimg { width: 200px; height: 200px; }\n```\n\n</Sandpack>\n\n<DeepDive>\n\n#### 객체들은 사실 중첩되어 있지 않습니다 {/*objects-are-not-really-nested*/}\n\n이러한 객체는 코드에서 \"중첩되어\" 나타납니다.\n\n```js\nlet obj = {\n  name: 'Niki de Saint Phalle',\n  artwork: {\n    title: 'Blue Nana',\n    city: 'Hamburg',\n    image: 'https://i.imgur.com/Sd1AgUOm.jpg',\n  }\n};\n```\n\n\"중첩\"은 객체의 동작에 대해 생각하는 부정확한 방법입니다. 코드가 실행될 때, \"중첩된\" 객체라는 것은 없습니다. 실제로 당신은 두 개의 다른 객체를 보는 것입니다.\n\n```js\nlet obj1 = {\n  title: 'Blue Nana',\n  city: 'Hamburg',\n  image: 'https://i.imgur.com/Sd1AgUOm.jpg',\n};\n\nlet obj2 = {\n  name: 'Niki de Saint Phalle',\n  artwork: obj1\n};\n```\n\n`obj1` 객체는 `obj2` \"안\"에 없습니다. `obj3` 또한 `obj1`을 \"가리킬\" 수 있기 때문입니다.\n\n```js\nlet obj1 = {\n  title: 'Blue Nana',\n  city: 'Hamburg',\n  image: 'https://i.imgur.com/Sd1AgUOm.jpg',\n};\n\nlet obj2 = {\n  name: 'Niki de Saint Phalle',\n  artwork: obj1\n};\n\nlet obj3 = {\n  name: 'Copycat',\n  artwork: obj1\n};\n```\n\n`obj3.artwork.city`을 변경하려 했다면, `obj2.artwork.city`와 `obj1.city` 둘 다에 영향을 미칠 것입니다. 이는 `obj3.artwork`, `obj2.artwork`와 `obj1`이 같은 객체이기 때문입니다. 객체를 \"중첩된\" 것으로 생각하면 이해하기 어려울 수 있습니다. 그것들은 프로퍼티를 통해 서로를 \"가리키는\" 각각의 객체들입니다.\n\n</DeepDive>\n\n### Immer로 간결한 갱신 로직 작성하기 {/*write-concise-update-logic-with-immer*/}\n\nstate가 깊이 중첩되어있다면 [평탄화](/learn/choosing-the-state-structure#avoid-deeply-nested-state)를 고려해보세요. 만약 state 구조를 바꾸고 싶지 않다면, 중첩 전개할 수 있는 더 간편한 방법이 있습니다. [Immer](https://github.com/immerjs/use-immer)는 편리하고, 변경 구문을 사용할 수 있게 해주며 복사본 생성을 도와주는 인기 있는 라이브러리입니다. Immer를 사용하면 작성한 코드는 \"법칙을 깨고\" 객체를 변경하는 것처럼 보일 수 있습니다.\n\n```js\nupdatePerson(draft => {\n  draft.artwork.city = 'Lagos';\n});\n```\n\n하지만 일반적인 변경과는 다르게 이것은 이전 state를 덮어쓰지 않습니다!\n\n<DeepDive>\n\n#### Immer는 어떻게 작동할까요? {/*how-does-immer-work*/}\n\nImmer가 제공하는 `draft`는 [Proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy)라고 하는 아주 특별한 객체 타입으로, 당신이 하는 일을 \"기록\" 합니다. 객체를 원하는 만큼 자유롭게 변경할 수 있는 이유죠! Immer는 내부적으로 `draft`의 어느 부분이 변경되었는지 알아내어, 변경사항을 포함한 완전히 새로운 객체를 생성합니다.\n\n</DeepDive>\n\nImmer를 사용하기 위해서는,\n\n1. `package.json`에 `dependencies`로 `use-immer`를 추가하세요\n2. `npm install`을 실행하세요\n3. `import { useState } from 'react'`를 `import { useImmer } from 'use-immer'`로 교체하세요.\n\n위의 예시를 Immer로 바꾼 코드입니다.\n\n<Sandpack>\n\n```js\nimport { useImmer } from 'use-immer';\n\nexport default function Form() {\n  const [person, updatePerson] = useImmer({\n    name: 'Niki de Saint Phalle',\n    artwork: {\n      title: 'Blue Nana',\n      city: 'Hamburg',\n      image: 'https://i.imgur.com/Sd1AgUOm.jpg',\n    }\n  });\n\n  function handleNameChange(e) {\n    updatePerson(draft => {\n      draft.name = e.target.value;\n    });\n  }\n\n  function handleTitleChange(e) {\n    updatePerson(draft => {\n      draft.artwork.title = e.target.value;\n    });\n  }\n\n  function handleCityChange(e) {\n    updatePerson(draft => {\n      draft.artwork.city = e.target.value;\n    });\n  }\n\n  function handleImageChange(e) {\n    updatePerson(draft => {\n      draft.artwork.image = e.target.value;\n    });\n  }\n\n  return (\n    <>\n      <label>\n        Name:\n        <input\n          value={person.name}\n          onChange={handleNameChange}\n        />\n      </label>\n      <label>\n        Title:\n        <input\n          value={person.artwork.title}\n          onChange={handleTitleChange}\n        />\n      </label>\n      <label>\n        City:\n        <input\n          value={person.artwork.city}\n          onChange={handleCityChange}\n        />\n      </label>\n      <label>\n        Image:\n        <input\n          value={person.artwork.image}\n          onChange={handleImageChange}\n        />\n      </label>\n      <p>\n        <i>{person.artwork.title}</i>\n        {' by '}\n        {person.name}\n        <br />\n        (located in {person.artwork.city})\n      </p>\n      <img\n        src={person.artwork.image}\n        alt={person.artwork.title}\n      />\n    </>\n  );\n}\n```\n\n```json package.json\n{\n  \"dependencies\": {\n    \"immer\": \"1.7.3\",\n    \"react\": \"latest\",\n    \"react-dom\": \"latest\",\n    \"react-scripts\": \"latest\",\n    \"use-immer\": \"0.5.1\"\n  },\n  \"scripts\": {\n    \"start\": \"react-scripts start\",\n    \"build\": \"react-scripts build\",\n    \"test\": \"react-scripts test --env=jsdom\",\n    \"eject\": \"react-scripts eject\"\n  }\n}\n```\n\n```css\nlabel { display: block; }\ninput { margin-left: 5px; margin-bottom: 5px; }\nimg { width: 200px; height: 200px; }\n```\n\n</Sandpack>\n\n이벤트 핸들러가 얼마나 간결해졌는지 보세요. 하나의 컴포넌트 안에서 원하는 만큼 `useState`와 `useImmer`를 섞어 사용할 수 있습니다. Immer는 업데이트 핸들러를 간결하게 관리할 수 있는 좋은 방법이며, 특히 state가 중첩되어 있고 객체를 복사하는 것이 중복되는 코드를 만들 때 유용합니다.\n\n<DeepDive>\n\n#### 왜 React에서 state 변경은 권장되지 않나요? {/*why-is-mutating-state-not-recommended-in-react*/}\n\n몇 가지 이유가 있습니다.\n\n* **디버깅:** 만약 `console.log`를 사용하고 state를 변경하지 않는다면, 과거 로그들은 가장 최근 state 변경 사항들에 의해 지워지지 않습니다. 따라서 state가 렌더링 사이에 어떻게 바뀌었는지 명확하게 알 수 있습니다.\n* **최적화:** 보편적인 React [최적화 전략](/reference/react/memo)은 이전 props 또는 state가 다음 것과 동일할 때 일을 건너뛰는 것에 의존합니다. state를 절대 변경하지 않는다면 변경사항이 있었는지 확인하는 작업이 매우 빨라집니다. `prevObj === obj`를 통해 내부적으로 아무것도 바뀌지 않았음을 확인할 수 있습니다.\n* **새로운 기능:** 우리가 만드는 새로운 React 기능들은 [스냅샷처럼 다루어지는 것](/learn/state-as-a-snapshot)에 의존합니다. 만약 state의 과거 버전을 변경한다면, 새로운 기능을 사용하지 못할 수 있습니다.\n* **요구사항 변화:** 취소/복원 구현, 변화 내역 조회, 사용자가 이전 값으로 폼을 재설정하기 등의 기능은 아무것도 변경되지 않았을 때 더 쉽습니다. 왜냐하면 당신은 메모리에 state의 이전 복사본을 저장하여 적절한 상황에 다시 사용할 수 있기 때문입니다. 변경하는 것으로 시작하게 되면 이러한 기능들은 나중에 추가하기 어려울 수 있습니다.\n* **더 간단한 구현:** React는 변경에 의존하지 않기 때문에 객체로 뭔가 특별한 것을 할 필요가 없습니다. 프로퍼티를 가져오거나, 항상 프록시로 감싸거나, 다른 많은 \"반응형\" 솔루션이 그러듯 초기화 시에 다른 작업을 하지 않아도 됩니다. 이것은 React가 state에 --얼마나 크던-- 추가적인 성능 또는 정확성 함정 없이 아무 객체나 넣을 수 있게 해주는 이유이기도 합니다.\n\n실제로, React에서 state를 변경하는 것으로 \"도망\"쳐버릴수도 있지만, 우리는 그렇게 하지 않기를 강하게 권장함으로써 당신이 이러한 접근법을 바탕으로 개발된 새로운 React 기능들을 사용할 수 있기를 바랍니다. 미래의 기여자들과 어쩌면 미래의 당신 스스로까지 고마워할 것입니다!\n\n</DeepDive>\n\n<Recap>\n\n* React의 모든 state를 불변한 것으로 대하세요.\n* state에 객체를 저장할 때, 객체를 변경하는 것은 렌더링을 발생시키지 않으며 이전 렌더 \"스냅샷\"의 state를 바꿀 것입니다.\n* 객체를 변경하는 대신 *새로운* 객체를 생성하여 state를 설정함으로써 리렌더링을 일으키세요.\n* 객체의 복사본을 만들기 위해 `{...obj, something: 'newValue'}` 객체 전개 구문을 사용할 수 있습니다.\n* 전개 구문은 얕습니다. 그것은 한 레벨 깊이만 복사합니다.\n* 중첩된 객체를 업데이트하기 위해서는 변경하는 부분에서부터 시작하여 객체의 모든 항목의 복사본을 만들어야 합니다.\n* 반복적인 복사 코드를 줄이기 위해서 Immer를 사용하세요.\n\n</Recap>\n\n\n\n<Challenges>\n\n#### 잘못된 state 업데이트 고치기 {/*fix-incorrect-state-updates*/}\n\n이 폼은 몇 가지 문제가 있습니다. 스코어를 올리는 버튼을 몇 번 클릭해 보세요. 스코어가 올라가지 않는 것을 확인하세요. 그리고 first name을 수정하여, 스코어가 갑자기 당신의 수정 사항을 \"따라잡은\" 것을 확인하세요. 마지막으로 last name을 수정하여, 스코어가 완전하게 사라진 것을 확인하세요.\n\n이 모든 버그를 올바르게 수정하는 것이 당신의 일입니다. 고칠 때마다 각각의 문제가 왜 발생하는지 설명해 보세요.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function Scoreboard() {\n  const [player, setPlayer] = useState({\n    firstName: 'Ranjani',\n    lastName: 'Shettar',\n    score: 10,\n  });\n\n  function handlePlusClick() {\n    player.score++;\n  }\n\n  function handleFirstNameChange(e) {\n    setPlayer({\n      ...player,\n      firstName: e.target.value,\n    });\n  }\n\n  function handleLastNameChange(e) {\n    setPlayer({\n      lastName: e.target.value\n    });\n  }\n\n  return (\n    <>\n      <label>\n        Score: <b>{player.score}</b>\n        {' '}\n        <button onClick={handlePlusClick}>\n          +1\n        </button>\n      </label>\n      <label>\n        First name:\n        <input\n          value={player.firstName}\n          onChange={handleFirstNameChange}\n        />\n      </label>\n      <label>\n        Last name:\n        <input\n          value={player.lastName}\n          onChange={handleLastNameChange}\n        />\n      </label>\n    </>\n  );\n}\n```\n\n```css\nlabel { display: block; margin-bottom: 10px; }\ninput { margin-left: 5px; margin-bottom: 5px; }\n```\n\n</Sandpack>\n\n<Solution>\n\n두가지 문제 모두가 고쳐진 버전입니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function Scoreboard() {\n  const [player, setPlayer] = useState({\n    firstName: 'Ranjani',\n    lastName: 'Shettar',\n    score: 10,\n  });\n\n  function handlePlusClick() {\n    setPlayer({\n      ...player,\n      score: player.score + 1,\n    });\n  }\n\n  function handleFirstNameChange(e) {\n    setPlayer({\n      ...player,\n      firstName: e.target.value,\n    });\n  }\n\n  function handleLastNameChange(e) {\n    setPlayer({\n      ...player,\n      lastName: e.target.value\n    });\n  }\n\n  return (\n    <>\n      <label>\n        Score: <b>{player.score}</b>\n        {' '}\n        <button onClick={handlePlusClick}>\n          +1\n        </button>\n      </label>\n      <label>\n        First name:\n        <input\n          value={player.firstName}\n          onChange={handleFirstNameChange}\n        />\n      </label>\n      <label>\n        Last name:\n        <input\n          value={player.lastName}\n          onChange={handleLastNameChange}\n        />\n      </label>\n    </>\n  );\n}\n```\n\n```css\nlabel { display: block; }\ninput { margin-left: 5px; margin-bottom: 5px; }\n```\n\n</Sandpack>\n\n`handlePlusClick`의 문제는 `player` 객체를 변경했다는 점입니다. 결과적으로 React는 리렌더링을 할 필요성을 몰랐으며, 스코어를 업데이트하지 않았습니다. 이것이 first name을 변경했을 때 state가 업데이트되었으며, 리렌더링을 야기하여 스코어 _또한_ 업데이트된 이유입니다.\n\n`handleLastNameChange`의 문제는 그것이 이미 존재하는 `...player` 필드를 새 객체로 복사하지 않았다는 점입니다. 이것이 last name을 수정한 후에 스코어가 없어진 이유입니다.\n\n</Solution>\n\n#### 변경 사항을 찾아 고치세요 {/*find-and-fix-the-mutation*/}\n\n정적인 배경 위에 드래그할 수 있는 박스가 있습니다. select input을 사용해 박스의 색상을 바꿀 수 있습니다.\n\n하지만 문제가 있습니다. 만약 박스를 먼저 옮긴 뒤 색상을 바꾸면, (움직여서는 안되는!) 배경이 박스 위치로 \"점프\"할 것입니다. 하지만 이것은 발생해선 안 되는 문제입니다. `Background`의 `position` prop은 `{ x: 0, y: 0 }`인 `initialPosition`으로 설정되어 있습니다. 왜 색상이 바뀐 후에 배경이 움직일까요?\n\n문제를 찾아 고쳐 보세요.\n\n<Hint>\n\n예상하지 못한 것이 바뀐다면, 그것은 변경 때문입니다. `App.js`의 변경 사항을 찾아 고쳐 보세요.\n\n</Hint>\n\n<Sandpack>\n\n```js src/App.js\nimport { useState } from 'react';\nimport Background from './Background.js';\nimport Box from './Box.js';\n\nconst initialPosition = {\n  x: 0,\n  y: 0\n};\n\nexport default function Canvas() {\n  const [shape, setShape] = useState({\n    color: 'orange',\n    position: initialPosition\n  });\n\n  function handleMove(dx, dy) {\n    shape.position.x += dx;\n    shape.position.y += dy;\n  }\n\n  function handleColorChange(e) {\n    setShape({\n      ...shape,\n      color: e.target.value\n    });\n  }\n\n  return (\n    <>\n      <select\n        value={shape.color}\n        onChange={handleColorChange}\n      >\n        <option value=\"orange\">orange</option>\n        <option value=\"lightpink\">lightpink</option>\n        <option value=\"aliceblue\">aliceblue</option>\n      </select>\n      <Background\n        position={initialPosition}\n      />\n      <Box\n        color={shape.color}\n        position={shape.position}\n        onMove={handleMove}\n      >\n        Drag me!\n      </Box>\n    </>\n  );\n}\n```\n\n```js src/Box.js\nimport { useState } from 'react';\n\nexport default function Box({\n  children,\n  color,\n  position,\n  onMove\n}) {\n  const [\n    lastCoordinates,\n    setLastCoordinates\n  ] = useState(null);\n\n  function handlePointerDown(e) {\n    e.target.setPointerCapture(e.pointerId);\n    setLastCoordinates({\n      x: e.clientX,\n      y: e.clientY,\n    });\n  }\n\n  function handlePointerMove(e) {\n    if (lastCoordinates) {\n      setLastCoordinates({\n        x: e.clientX,\n        y: e.clientY,\n      });\n      const dx = e.clientX - lastCoordinates.x;\n      const dy = e.clientY - lastCoordinates.y;\n      onMove(dx, dy);\n    }\n  }\n\n  function handlePointerUp(e) {\n    setLastCoordinates(null);\n  }\n\n  return (\n    <div\n      onPointerDown={handlePointerDown}\n      onPointerMove={handlePointerMove}\n      onPointerUp={handlePointerUp}\n      style={{\n        width: 100,\n        height: 100,\n        cursor: 'grab',\n        backgroundColor: color,\n        position: 'absolute',\n        border: '1px solid black',\n        display: 'flex',\n        justifyContent: 'center',\n        alignItems: 'center',\n        transform: `translate(\n          ${position.x}px,\n          ${position.y}px\n        )`,\n      }}\n    >{children}</div>\n  );\n}\n```\n\n```js src/Background.js\nexport default function Background({\n  position\n}) {\n  return (\n    <div style={{\n      position: 'absolute',\n      transform: `translate(\n        ${position.x}px,\n        ${position.y}px\n      )`,\n      width: 250,\n      height: 250,\n      backgroundColor: 'rgba(200, 200, 0, 0.2)',\n    }} />\n  );\n};\n```\n\n```css\nbody { height: 280px; }\nselect { margin-bottom: 10px; }\n```\n\n</Sandpack>\n\n<Solution>\n\n문제는 `handleMove` 내부의 변경입니다. 핸들러는 `shape.position`을 변경했지만, 그것은 `initialPosition`가 가리키는 객체와 동일합니다. 이것이 모양과 배경이 둘 다 움직인 이유입니다. (이것은 변경이기에, 색상 수정처럼 관련 없는 업데이트가 리렌더링을 발생시킬 때까지 화면에 반영되지 않습니다.)\n\n`handleMove`에서 변경을 제거하고, 모양을 복사하기 위해 전개 연산자를 사용함으로써 문제를 해결할 수 있습니다. `+=`는 변경이기에, 일반 `+` 연산자로 작성해야 한다는 것을 알아두세요.\n\n<Sandpack>\n\n```js src/App.js\nimport { useState } from 'react';\nimport Background from './Background.js';\nimport Box from './Box.js';\n\nconst initialPosition = {\n  x: 0,\n  y: 0\n};\n\nexport default function Canvas() {\n  const [shape, setShape] = useState({\n    color: 'orange',\n    position: initialPosition\n  });\n\n  function handleMove(dx, dy) {\n    setShape({\n      ...shape,\n      position: {\n        x: shape.position.x + dx,\n        y: shape.position.y + dy,\n      }\n    });\n  }\n\n  function handleColorChange(e) {\n    setShape({\n      ...shape,\n      color: e.target.value\n    });\n  }\n\n  return (\n    <>\n      <select\n        value={shape.color}\n        onChange={handleColorChange}\n      >\n        <option value=\"orange\">orange</option>\n        <option value=\"lightpink\">lightpink</option>\n        <option value=\"aliceblue\">aliceblue</option>\n      </select>\n      <Background\n        position={initialPosition}\n      />\n      <Box\n        color={shape.color}\n        position={shape.position}\n        onMove={handleMove}\n      >\n        Drag me!\n      </Box>\n    </>\n  );\n}\n```\n\n```js src/Box.js\nimport { useState } from 'react';\n\nexport default function Box({\n  children,\n  color,\n  position,\n  onMove\n}) {\n  const [\n    lastCoordinates,\n    setLastCoordinates\n  ] = useState(null);\n\n  function handlePointerDown(e) {\n    e.target.setPointerCapture(e.pointerId);\n    setLastCoordinates({\n      x: e.clientX,\n      y: e.clientY,\n    });\n  }\n\n  function handlePointerMove(e) {\n    if (lastCoordinates) {\n      setLastCoordinates({\n        x: e.clientX,\n        y: e.clientY,\n      });\n      const dx = e.clientX - lastCoordinates.x;\n      const dy = e.clientY - lastCoordinates.y;\n      onMove(dx, dy);\n    }\n  }\n\n  function handlePointerUp(e) {\n    setLastCoordinates(null);\n  }\n\n  return (\n    <div\n      onPointerDown={handlePointerDown}\n      onPointerMove={handlePointerMove}\n      onPointerUp={handlePointerUp}\n      style={{\n        width: 100,\n        height: 100,\n        cursor: 'grab',\n        backgroundColor: color,\n        position: 'absolute',\n        border: '1px solid black',\n        display: 'flex',\n        justifyContent: 'center',\n        alignItems: 'center',\n        transform: `translate(\n          ${position.x}px,\n          ${position.y}px\n        )`,\n      }}\n    >{children}</div>\n  );\n}\n```\n\n```js src/Background.js\nexport default function Background({\n  position\n}) {\n  return (\n    <div style={{\n      position: 'absolute',\n      transform: `translate(\n        ${position.x}px,\n        ${position.y}px\n      )`,\n      width: 250,\n      height: 250,\n      backgroundColor: 'rgba(200, 200, 0, 0.2)',\n    }} />\n  );\n};\n```\n\n```css\nbody { height: 280px; }\nselect { margin-bottom: 10px; }\n```\n\n</Sandpack>\n\n</Solution>\n\n#### Immer로 객체 업데이트하기 {/*update-an-object-with-immer*/}\n\n이것은 이전 챌린지와 비슷한, 버그가 있는 예시입니다. 이번에는 Immer를 사용해서 변경을 고쳐 보세요. 편의를 위해 `useImmer`는 이미 포함되어 있으므로 사용하기 위해서는 `shape` state 변수를 바꿔야 합니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { useState } from 'react';\nimport { useImmer } from 'use-immer';\nimport Background from './Background.js';\nimport Box from './Box.js';\n\nconst initialPosition = {\n  x: 0,\n  y: 0\n};\n\nexport default function Canvas() {\n  const [shape, setShape] = useState({\n    color: 'orange',\n    position: initialPosition\n  });\n\n  function handleMove(dx, dy) {\n    shape.position.x += dx;\n    shape.position.y += dy;\n  }\n\n  function handleColorChange(e) {\n    setShape({\n      ...shape,\n      color: e.target.value\n    });\n  }\n\n  return (\n    <>\n      <select\n        value={shape.color}\n        onChange={handleColorChange}\n      >\n        <option value=\"orange\">orange</option>\n        <option value=\"lightpink\">lightpink</option>\n        <option value=\"aliceblue\">aliceblue</option>\n      </select>\n      <Background\n        position={initialPosition}\n      />\n      <Box\n        color={shape.color}\n        position={shape.position}\n        onMove={handleMove}\n      >\n        Drag me!\n      </Box>\n    </>\n  );\n}\n```\n\n```js src/Box.js\nimport { useState } from 'react';\n\nexport default function Box({\n  children,\n  color,\n  position,\n  onMove\n}) {\n  const [\n    lastCoordinates,\n    setLastCoordinates\n  ] = useState(null);\n\n  function handlePointerDown(e) {\n    e.target.setPointerCapture(e.pointerId);\n    setLastCoordinates({\n      x: e.clientX,\n      y: e.clientY,\n    });\n  }\n\n  function handlePointerMove(e) {\n    if (lastCoordinates) {\n      setLastCoordinates({\n        x: e.clientX,\n        y: e.clientY,\n      });\n      const dx = e.clientX - lastCoordinates.x;\n      const dy = e.clientY - lastCoordinates.y;\n      onMove(dx, dy);\n    }\n  }\n\n  function handlePointerUp(e) {\n    setLastCoordinates(null);\n  }\n\n  return (\n    <div\n      onPointerDown={handlePointerDown}\n      onPointerMove={handlePointerMove}\n      onPointerUp={handlePointerUp}\n      style={{\n        width: 100,\n        height: 100,\n        cursor: 'grab',\n        backgroundColor: color,\n        position: 'absolute',\n        border: '1px solid black',\n        display: 'flex',\n        justifyContent: 'center',\n        alignItems: 'center',\n        transform: `translate(\n          ${position.x}px,\n          ${position.y}px\n        )`,\n      }}\n    >{children}</div>\n  );\n}\n```\n\n```js src/Background.js\nexport default function Background({\n  position\n}) {\n  return (\n    <div style={{\n      position: 'absolute',\n      transform: `translate(\n        ${position.x}px,\n        ${position.y}px\n      )`,\n      width: 250,\n      height: 250,\n      backgroundColor: 'rgba(200, 200, 0, 0.2)',\n    }} />\n  );\n};\n```\n\n```css\nbody { height: 280px; }\nselect { margin-bottom: 10px; }\n```\n\n```json package.json\n{\n  \"dependencies\": {\n    \"immer\": \"1.7.3\",\n    \"react\": \"latest\",\n    \"react-dom\": \"latest\",\n    \"react-scripts\": \"latest\",\n    \"use-immer\": \"0.5.1\"\n  },\n  \"scripts\": {\n    \"start\": \"react-scripts start\",\n    \"build\": \"react-scripts build\",\n    \"test\": \"react-scripts test --env=jsdom\",\n    \"eject\": \"react-scripts eject\"\n  }\n}\n```\n\n</Sandpack>\n\n<Solution>\n\n이것은 Immer로 다시 작성된 해결 방법입니다. 이벤트 핸들러가 변경하는 방식이 어떻게 작성되어있는지 확인해보세요. 하지만 문제는 발생하지 않습니다. 내부적으로 Immer는 존재하는 객체를 절대 변경하지 않기 때문입니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { useImmer } from 'use-immer';\nimport Background from './Background.js';\nimport Box from './Box.js';\n\nconst initialPosition = {\n  x: 0,\n  y: 0\n};\n\nexport default function Canvas() {\n  const [shape, updateShape] = useImmer({\n    color: 'orange',\n    position: initialPosition\n  });\n\n  function handleMove(dx, dy) {\n    updateShape(draft => {\n      draft.position.x += dx;\n      draft.position.y += dy;\n    });\n  }\n\n  function handleColorChange(e) {\n    updateShape(draft => {\n      draft.color = e.target.value;\n    });\n  }\n\n  return (\n    <>\n      <select\n        value={shape.color}\n        onChange={handleColorChange}\n      >\n        <option value=\"orange\">orange</option>\n        <option value=\"lightpink\">lightpink</option>\n        <option value=\"aliceblue\">aliceblue</option>\n      </select>\n      <Background\n        position={initialPosition}\n      />\n      <Box\n        color={shape.color}\n        position={shape.position}\n        onMove={handleMove}\n      >\n        Drag me!\n      </Box>\n    </>\n  );\n}\n```\n\n```js src/Box.js\nimport { useState } from 'react';\n\nexport default function Box({\n  children,\n  color,\n  position,\n  onMove\n}) {\n  const [\n    lastCoordinates,\n    setLastCoordinates\n  ] = useState(null);\n\n  function handlePointerDown(e) {\n    e.target.setPointerCapture(e.pointerId);\n    setLastCoordinates({\n      x: e.clientX,\n      y: e.clientY,\n    });\n  }\n\n  function handlePointerMove(e) {\n    if (lastCoordinates) {\n      setLastCoordinates({\n        x: e.clientX,\n        y: e.clientY,\n      });\n      const dx = e.clientX - lastCoordinates.x;\n      const dy = e.clientY - lastCoordinates.y;\n      onMove(dx, dy);\n    }\n  }\n\n  function handlePointerUp(e) {\n    setLastCoordinates(null);\n  }\n\n  return (\n    <div\n      onPointerDown={handlePointerDown}\n      onPointerMove={handlePointerMove}\n      onPointerUp={handlePointerUp}\n      style={{\n        width: 100,\n        height: 100,\n        cursor: 'grab',\n        backgroundColor: color,\n        position: 'absolute',\n        border: '1px solid black',\n        display: 'flex',\n        justifyContent: 'center',\n        alignItems: 'center',\n        transform: `translate(\n          ${position.x}px,\n          ${position.y}px\n        )`,\n      }}\n    >{children}</div>\n  );\n}\n```\n\n```js src/Background.js\nexport default function Background({\n  position\n}) {\n  return (\n    <div style={{\n      position: 'absolute',\n      transform: `translate(\n        ${position.x}px,\n        ${position.y}px\n      )`,\n      width: 250,\n      height: 250,\n      backgroundColor: 'rgba(200, 200, 0, 0.2)',\n    }} />\n  );\n};\n```\n\n```css\nbody { height: 280px; }\nselect { margin-bottom: 10px; }\n```\n\n```json package.json\n{\n  \"dependencies\": {\n    \"immer\": \"1.7.3\",\n    \"react\": \"latest\",\n    \"react-dom\": \"latest\",\n    \"react-scripts\": \"latest\",\n    \"use-immer\": \"0.5.1\"\n  },\n  \"scripts\": {\n    \"start\": \"react-scripts start\",\n    \"build\": \"react-scripts build\",\n    \"test\": \"react-scripts test --env=jsdom\",\n    \"eject\": \"react-scripts eject\"\n  }\n}\n```\n\n</Sandpack>\n\n</Solution>\n\n</Challenges>\n"
  },
  {
    "path": "src/content/learn/writing-markup-with-jsx.md",
    "content": "---\ntitle: JSX로 마크업 작성하기\n---\n\n<Intro>\n\n*JSX*는 JavaScript를 확장한 문법으로, JavaScript 파일을 HTML과 비슷하게 마크업을 작성할 수 있도록 해줍니다. 컴포넌트를 작성하는 다른 방법도 있지만, 대부분의 React 개발자는 JSX의 간결함을 선호하며 대부분의 코드 베이스에서 JSX를 사용합니다.\n\n</Intro>\n\n<YouWillLearn>\n\n* React에서 마크업과 렌더링 로직을 같이 사용하는 이유\n* JSX와 HTML의 차이점\n* JSX로 정보를 보여주는 방법\n\n</YouWillLearn>\n\n## JSX: JavaScript에 마크업 넣기 {/*jsx-putting-markup-into-javascript*/}\n\nWeb은 HTML, CSS, JavaScript를 기반으로 만들어져 왔습니다. 수년 동안 웹 개발자는 HTML로 내용을, CSS로 디자인을, JavaScript로 로직을 작성해 왔습니다. 보통은 HTML, CSS, JavaScript를 분리된 파일로 관리합니다! 페이지의 로직이 JavaScript 안에서 분리되어 동작하는 동안, HTML 안에서는 내용이 마크업 되었습니다.\n\n<DiagramGroup>\n\n<Diagram name=\"writing_jsx_html\" height={237} width={325} alt=\"HTML markup with purple background and a div with two child tags: p and form. \">\n\nHTML\n\n</Diagram>\n\n<Diagram name=\"writing_jsx_js\" height={237} width={325} alt=\"Three JavaScript handlers with yellow background: onSubmit, onLogin, and onClick.\">\n\nJavaScript\n\n</Diagram>\n\n</DiagramGroup>\n\n그러나 Web이 더욱 인터랙티브해지면서, 로직이 내용을 결정하는 경우가 많아졌습니다. 그래서 JavaScript가 HTML을 담당하게 되었죠! 이것이 바로 **React에서 렌더링 로직과 마크업이 같은 위치에 함께 있게 된 이유입니다. 즉, 컴포넌트에서 말이죠.**\n\n<DiagramGroup>\n\n<Diagram name=\"writing_jsx_sidebar\" height={330} width={325} alt=\"React component with HTML and JavaScript from previous examples mixed. Function name is Sidebar which calls the function isLoggedIn, highlighted in yellow. Nested inside the function highlighted in purple is the p tag from before, and a Form tag referencing the component shown in the next diagram.\">\n\n`Sidebar.js` React component\n\n</Diagram>\n\n<Diagram name=\"writing_jsx_form\" height={330} width={325} alt=\"React component with HTML and JavaScript from previous examples mixed. Function name is Form containing two handlers onClick and onSubmit highlighted in yellow. Following the handlers is HTML highlighted in purple. The HTML contains a form element with a nested input element, each with an onClick prop.\">\n\n`Form.js` React component\n\n</Diagram>\n\n</DiagramGroup>\n\n버튼의 렌더링 로직과 버튼의 마크업이 함께 있으면, 매번 변화가 생길 때마다 서로 동기화 상태를 유지할 수 있습니다. 반대로 버튼의 마크업과 사이드바의 마크업처럼 서로 관련이 없는 항목들은 서로 분리되어 있으므로, 각각 개별적으로 변경하는 것이 더 안전합니다.\n\n각 React 컴포넌트는 React가 브라우저에 마크업을 렌더링할 수 있는 JavaScript 함수입니다. React 컴포넌트는 JSX라는 확장된 문법을 사용하여 마크업을 나타냅니다. JSX는 HTML과 비슷해 보이지만, 조금 더 엄격하며 동적으로 정보를 표시할 수 있습니다. JSX를 이해하는 가장 좋은 방법은 HTML 마크업을 JSX 마크업으로 변환해 보는 것입니다.\n\n<Note>\n\nJSX와 React는 서로 다른 별개의 개념입니다. 종종 함께 사용되기도 하지만 [독립적으로](https://ko.legacy.reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html#whats-a-jsx-transform) 사용할 수도 있습니다. JSX는 확장된 문법이고, React는 JavaScript 라이브러리입니다.\n\n</Note>\n\n## HTML을 JSX로 변환하기 {/*converting-html-to-jsx*/}\n\n다음과 같은 HTML이 있다고 가정해 봅시다. (완벽한 코드라고 가정합시다.)\n\n```html\n<h1>Hedy Lamarr's Todos</h1>\n<img\n  src=\"https://i.imgur.com/yXOvdOSs.jpg\"\n  alt=\"Hedy Lamarr\"\n  class=\"photo\"\n>\n<ul>\n    <li>Invent new traffic lights\n    <li>Rehearse a movie scene\n    <li>Improve the spectrum technology\n</ul>\n```\n\n이제 이것을 컴포넌트로 만들어 볼 겁니다.\n\n```js\nexport default function TodoList() {\n  return (\n    // ???\n  )\n}\n```\n\n이 코드를 그대로 복사하여 붙여 넣는다면 동작하지 않을 겁니다.\n\n\n<Sandpack>\n\n```js\nexport default function TodoList() {\n  return (\n    // 이것은 동작하지 않습니다!\n    <h1>Hedy Lamarr's Todos</h1>\n    <img\n      src=\"https://i.imgur.com/yXOvdOSs.jpg\"\n      alt=\"Hedy Lamarr\"\n      class=\"photo\"\n    >\n    <ul>\n      <li>Invent new traffic lights\n      <li>Rehearse a movie scene\n      <li>Improve the spectrum technology\n    </ul>\n  );\n}\n```\n\n```css\nimg { height: 90px }\n```\n\n</Sandpack>\n\n왜냐하면 JSX는 HTML보다 더 엄격하고 몇 가지 규칙이 더 있기 때문입니다! 위의 오류 메시지를 읽으면 마크업을 수정하도록 안내하고 있습니다. 또는 아래의 가이드를 따를 수 있습니다.\n\n<Note>\n\n대부분의 경우 React의 화면 오류 메시지는 문제가 있는 곳을 찾는 데 도움이 됩니다. 막히면 읽어주세요!\n\n</Note>\n\n## JSX의 규칙 {/*the-rules-of-jsx*/}\n\n### 1. 하나의 루트 엘리먼트로 반환하기 {/*1-return-a-single-root-element*/}\n\n한 컴포넌트에서 여러 엘리먼트를 반환하려면, **하나의 부모 태그로 감싸주세요.**\n\n예를 들면, 다음과 같이 `<div>`를 사용할 수 있습니다.\n\n```js {1,11}\n<div>\n  <h1>Hedy Lamarr's Todos</h1>\n  <img\n    src=\"https://i.imgur.com/yXOvdOSs.jpg\"\n    alt=\"Hedy Lamarr\"\n    class=\"photo\"\n  >\n  <ul>\n    ...\n  </ul>\n</div>\n```\n\n\n마크업에 `<div>`를 추가하고 싶지 않다면, `<>`와 `</>`로 대체할 수 있습니다.\n\n```js {1,11}\n<>\n  <h1>Hedy Lamarr's Todos</h1>\n  <img\n    src=\"https://i.imgur.com/yXOvdOSs.jpg\"\n    alt=\"Hedy Lamarr\"\n    class=\"photo\"\n  >\n  <ul>\n    ...\n  </ul>\n</>\n```\n\n이 빈 태그를 [*Fragment*](/reference/react/Fragment)라고 합니다. Fragments는 브라우저상의 HTML 트리 구조에서 흔적을 남기지 않고 그룹화해 줍니다.\n\n<DeepDive>\n\n#### 왜 여러 JSX 태그를 하나로 감싸줘야 할까요? {/*why-do-multiple-jsx-tags-need-to-be-wrapped*/}\n\nJSX는 HTML처럼 보이지만 내부적으로는 일반 JavaScript 객체로 변환됩니다. 하나의 배열로 감싸지 않은 하나의 함수에서는 두 개의 객체를 반환할 수 없습니다. 따라서 또 다른 태그나 Fragment로 감싸지 않으면 두 개의 JSX 태그를 반환할 수 없습니다.\n\n</DeepDive>\n\n### 2. 모든 태그는 닫아주기 {/*2-close-all-the-tags*/}\n\nJSX에서는 태그를 명시적으로 닫아야 합니다. `<img>`처럼 자체적으로 닫아주는 태그는 반드시 `<img />` 형태로 작성해야 하며, `<li>oranges`와 같은 래핑 태그도 `<li>oranges</li>` 형태로 작성해야 합니다.\n\n다음과 같이 Hedy Lamarr의 이미지와 리스트의 항목들을 닫아줍니다.\n\n```js {2-6,8-10}\n<>\n  <img\n    src=\"https://i.imgur.com/yXOvdOSs.jpg\"\n    alt=\"Hedy Lamarr\"\n    class=\"photo\"\n   />\n  <ul>\n    <li>Invent new traffic lights</li>\n    <li>Rehearse a movie scene</li>\n    <li>Improve the spectrum technology</li>\n  </ul>\n</>\n```\n\n### 3. <s>거의</s> 대부분 캐멀 케이스로! {/*3-camelcase-almost-all-the-things*/}\n\nJSX는 JavaScript로 바뀌고 JSX에서 작성된 어트리뷰트는 JavaScript 객체의 키가 됩니다. 컴포넌트에서는 종종 어트리뷰트를 변수로 읽고 싶은 경우가 있습니다. 그러나 JavaScript는 변수명에 제한이 있습니다. 예를 들면, 변수명에 대시를 포함하거나 `class`처럼 예약어를 사용할 수 없습니다.\n\n이것이 바로 React에서 HTML과 SVG의 어트리뷰트 대부분이 캐멀 케이스로 작성되는 이유입니다. 예를 들면, `stroke-width` 대신 `strokeWidth`로 사용합니다. `class`는 예약어이기 때문에, React에서는 [DOM의 프로퍼티](https://developer.mozilla.org/en-US/docs/Web/API/Element/className)의 이름을 따서 `className`으로 대신 작성합니다.\n\n```js {4}\n<img\n  src=\"https://i.imgur.com/yXOvdOSs.jpg\"\n  alt=\"Hedy Lamarr\"\n  className=\"photo\"\n/>\n```\n\n[이러한 모든 어트리뷰트는 React DOM 엘리먼트에서 찾을 수 있습니다.](/reference/react-dom/components/common) 틀려도 걱정하지 마세요. React는 [브라우저 콘솔](https://developer.mozilla.org/docs/Tools/Browser_Console)에서 수정 가능한 부분을 메시지로 알려줍니다.\n\n<Pitfall>\n\n역사적인 이유로, [`aria-*`](https://developer.mozilla.org/docs/Web/Accessibility/ARIA)와 [`data-*`](https://developer.mozilla.org/docs/Learn/HTML/Howto/Use_data_attributes)의 어트리뷰트는 HTML에서와 동일하게 대시를 사용하여 작성합니다.\n\n</Pitfall>\n\n### 추천-팁: JSX 변환기 사용하기 {/*pro-tip-use-a-jsx-converter*/}\n\n기존 마크업에서 모든 어트리뷰트를 변환하는 것은 지루할 수 있습니다. [변환기](https://transform.tools/html-to-jsx)를 사용하여 기존 HTML과 SVG를 JSX로 변환하는 것을 추천합니다. 변환기는 매우 유용하지만 그래도 JSX를 혼자서 편안하게 작성할 수 있도록 어트리뷰트를 어떻게 쓰는지 이해하는 것도 중요합니다.\n\n최종 코드는 다음과 같습니다.\n\n<Sandpack>\n\n```js\nexport default function TodoList() {\n  return (\n    <>\n      <h1>Hedy Lamarr's Todos</h1>\n      <img\n        src=\"https://i.imgur.com/yXOvdOSs.jpg\"\n        alt=\"Hedy Lamarr\"\n        className=\"photo\"\n      />\n      <ul>\n        <li>Invent new traffic lights</li>\n        <li>Rehearse a movie scene</li>\n        <li>Improve the spectrum technology</li>\n      </ul>\n    </>\n  );\n}\n```\n\n```css\nimg { height: 90px }\n```\n\n</Sandpack>\n\n<Recap>\n\n지금까지 JSX가 존재하는 이유와 컴포넌트에서 JSX를 쓰는 방법에 대해 알아보았습니다.\n\n* React 컴포넌트는 서로 관련이 있는 마크업과 렌더링 로직을 함께 그룹화합니다.\n* JSX는 HTML과 비슷하지만 몇 가지 차이점이 있습니다. 필요한 경우 [변환기](https://transform.tools/html-to-jsx)를 사용할 수 있습니다.\n* 오류 메시지는 종종 마크업을 수정할 수 있도록 올바른 방향을 알려줍니다.\n\n</Recap>\n\n\n\n<Challenges>\n\n#### HTML을 JSX로 변환해보기 {/*convert-some-html-to-jsx*/}\n\n다음은 컴포넌트에 HTML을 붙여 넣었지만, 올바른 JSX가 아닙니다. 수정해 보세요.\n\n<Sandpack>\n\n```js\nexport default function Bio() {\n  return (\n    <div class=\"intro\">\n      <h1>Welcome to my website!</h1>\n    </div>\n    <p class=\"summary\">\n      You can find my thoughts here.\n      <br><br>\n      <b>And <i>pictures</b></i> of scientists!\n    </p>\n  );\n}\n```\n\n```css\n.intro {\n  background-image: linear-gradient(to left, violet, indigo, blue, green, yellow, orange, red);\n  background-clip: text;\n  color: transparent;\n  -webkit-background-clip: text;\n  -webkit-text-fill-color: transparent;\n}\n\n.summary {\n  padding: 20px;\n  border: 10px solid gold;\n}\n```\n\n</Sandpack>\n\n직접 수정할지 변환기를 사용할지는 여러분에게 달려 있습니다!\n\n<Solution>\n\n<Sandpack>\n\n```js\nexport default function Bio() {\n  return (\n    <div>\n      <div className=\"intro\">\n        <h1>Welcome to my website!</h1>\n      </div>\n      <p className=\"summary\">\n        You can find my thoughts here.\n        <br /><br />\n        <b>And <i>pictures</i></b> of scientists!\n      </p>\n    </div>\n  );\n}\n```\n\n```css\n.intro {\n  background-image: linear-gradient(to left, violet, indigo, blue, green, yellow, orange, red);\n  background-clip: text;\n  color: transparent;\n  -webkit-background-clip: text;\n  -webkit-text-fill-color: transparent;\n}\n\n.summary {\n  padding: 20px;\n  border: 10px solid gold;\n}\n```\n\n</Sandpack>\n\n</Solution>\n\n</Challenges>\n"
  },
  {
    "path": "src/content/learn/you-might-not-need-an-effect.md",
    "content": "---\ntitle: 'Effect가 필요하지 않은 경우'\n---\n\n<Intro>\n\nEffect는 React 패러다임에서 벗어날 수 있는 탈출구입니다. Effect를 사용하면 React를 \"벗어나\" 컴포넌트를 React가 아닌 위젯, 네트워크, 또는 브라우저 DOM과 같은 외부 시스템과 동기화할 수 있습니다. 외부 시스템이 관여하지 않는 경우 (예를 들어 일부 props 또는 state가 변경될 때 컴포넌트의 state를 업데이트하려는 경우), Effect가 필요하지 않습니다. 불필요한 Effect를 제거하면 코드를 더 쉽게 따라갈 수 있고, 실행 속도가 빨라지며, 에러 발생 가능성이 줄어듭니다.\n\n</Intro>\n\n<YouWillLearn>\n\n* 컴포넌트에서 불필요한 Effect를 제거하는 이유와 방법\n* Effect 없이 값비싼 계산을 캐시하는 방법\n* Effect 없이 컴포넌트 state를 초기화하고 조정하는 방법\n* 이벤트 핸들러 간에 로직을 공유하는 방법\n* 이벤트 핸들러로 이동해야 하는 로직\n* 부모 컴포넌트에 변경 사항을 알리는 방법\n\n</YouWillLearn>\n\n## 불필요한 Effect를 제거하는 방법 {/*how-to-remove-unnecessary-effects*/}\n\nEffect가 필요하지 않은 두 가지 일반적인 경우가 있습니다.\n\n* **렌더링을 위해 데이터를 변환하는 데 Effect가 필요하지 않습니다.** 예를 들어 리스트를 표시하기 전에 필터링하고 싶다고 가정해 보겠습니다. 리스트가 변경될 때 state 변수를 업데이트하는 Effect를 작성하고 싶을 수 있습니다. 하지만 이는 비효율적입니다. state를 업데이트할 때 React는 먼저 컴포넌트 함수를 호출해 화면에 표시될 내용을 계산합니다. 그런 다음 React는 이러한 변경 사항을 DOM에 [\"commit\"](/learn/render-and-commit)하여 화면을 업데이트합니다. 그리고 나서 React가 Effect를 실행합니다. 만약 Effect도 *즉시* state를 업데이트한다면 전체 프로세스가 처음부터 다시 시작됩니다! 불필요한 렌더링 패스를 피하려면, 컴포넌트의 최상위 레벨에서 모든 데이터를 변환하세요. 그러면 props나 state가 변경될 때마다 해당 코드가 자동으로 다시 실행됩니다.\n* **사용자 이벤트를 처리하는 데 Effect가 필요하지 않습니다.** 예를 들어 사용자가 제품을 구매할 때 `/api/buy` POST 요청을 전송하고 알림을 표시하고 싶다고 가정해 보겠습니다. 구매 버튼 클릭 이벤트 핸들러에서는 정확히 어떤 일이 일어났는지 알 수 있습니다. Effect가 실행될 때까지 사용자가 무엇을 했는지 (예: 어떤 버튼을 클릭 했는지) 알 수 없습니다. 그렇기 때문에 일반적으로 해당되는 이벤트 핸들러에서 사용자 이벤트를 처리합니다.\n\n외부 시스템과 [동기화](/learn/synchronizing-with-effects#what-are-effects-and-how-are-they-different-from-events)하기 위해서는 Effect가 *필요합니다*. 예를 들어, jQuery 위젯을 React state와 동기화된 상태로 유지하는 Effect를 작성할 수 있습니다. 또한 Effect로 데이터를 가져올 수도 있습니다. 예를 들어, 검색 결과를 현재 검색어와 동기화할 수 있습니다. 다만, 최신 [프레임워크](/learn/creating-a-react-app#full-stack-frameworks)는 컴포넌트에서 직접 Effect를 작성하는 것보다 더 효율적인 내장 데이터 페칭 메커니즘을 제공한다는 점을 기억하세요.\n\n올바른 직관을 얻기 위해, 몇 가지 일반적이고 구체적인 예를 살펴봅시다!\n\n### props 또는 state에 따라 state 업데이트하기 {/*updating-state-based-on-props-or-state*/}\n\n`firstName`과 `lastName`이라는 두 개의 state 변수가 있다고 가정해 봅시다. 두 변수를 연결해서 `fullName`을 계산하고 싶습니다. 또한 `firstName`이나 `lastName`이 변경될 때마다 `fullName`이 업데이트되기를 바랍니다. 가장 먼저 `fullName` State 변수를 추가하고 Effect에서 업데이트하고 싶을 것입니다.\n\n```js {5-9}\nfunction Form() {\n  const [firstName, setFirstName] = useState('Taylor');\n  const [lastName, setLastName] = useState('Swift');\n\n  // 🔴 피하세요: 중복된 state 및 불필요한 Effect\n  const [fullName, setFullName] = useState('');\n  useEffect(() => {\n    setFullName(firstName + ' ' + lastName);\n  }, [firstName, lastName]);\n  // ...\n}\n```\n\n이는 필요 이상으로 복잡합니다. 또한 `fullName`에 대한 오래된 값으로 전체 렌더링 패스를 수행한 다음, 업데이트된 값으로 즉시 다시 렌더링하기 때문에 비효율적입니다. state 변수와 Effect를 제거하세요.\n\n```js {4-5}\nfunction Form() {\n  const [firstName, setFirstName] = useState('Taylor');\n  const [lastName, setLastName] = useState('Swift');\n  // ✅ 좋습니다: 렌더링 중에 계산됨\n  const fullName = firstName + ' ' + lastName;\n  // ...\n}\n```\n\n**기존 props나 state에서 계산할 수 있는 것이 있으면 , [그것을 state에 넣지 마세요.](/learn/choosing-the-state-structure#avoid-redundant-state) 대신, 렌더링 중에 계산하게 하세요.** 이렇게 하면 코드가 더 빨라지고 (추가적인 \"연속적인\" 업데이트를 피할 수 있으며), 더 간단해지고 (일부 코드를 제거할 수 있으며), 에러가 덜 발생합니다(서로 다른 state 변수가 서로 동기화되지 않아 발생하는 버그를 피할 수 있습니다). 이 접근 방식이 생소하게 느껴진다면, [React로 사고하기](/learn/thinking-in-react#step-3-find-the-minimal-but-complete-representation-of-ui-state)에서 무엇이 state에 들어가야 하는지 설명해 줄 것입니다.\n\n### 비용이 많이 드는 계산 캐싱하기 {/*caching-expensive-calculations*/}\n\n이 컴포넌트는 props로 받은 `todos`를 `filter` prop에 따라 필터링하여 `visibleTodos`를 계산합니다. 결과를 state에 저장하고 Effect에서 업데이트하고 싶을 수 있습니다.\n\n```js {4-8}\nfunction TodoList({ todos, filter }) {\n  const [newTodo, setNewTodo] = useState('');\n\n  // 🔴 피하세요: 중복된 state 및 불필요한 효과\n  const [visibleTodos, setVisibleTodos] = useState([]);\n  useEffect(() => {\n    setVisibleTodos(getFilteredTodos(todos, filter));\n  }, [todos, filter]);\n\n  // ...\n}\n```\n\n앞의 예시와 마찬가지로, 이것은 불필요하고 비효율적입니다. 먼저, state와 Effect를 제거합니다.\n\n```js {3-4}\nfunction TodoList({ todos, filter }) {\n  const [newTodo, setNewTodo] = useState('');\n  // ✅ getFilteredTodos()가 느리지 않다면 괜찮습니다.\n  const visibleTodos = getFilteredTodos(todos, filter);\n  // ...\n}\n```\n\n보통, 이 코드는 괜찮습니다! 하지만 `getFilteredTodos()`가 느리거나 `todos`가 많을 수도 있습니다. 이 경우 `newTodo`와 같이 관련 없는 state 변수가 변경된 경우 `getFilteredTodos()`를 다시 계산하고 싶지 않을 수 있습니다.\n\n[`useMemo`](/reference/react/useMemo) Hook으로 래핑해서 값비싼 계산을 캐시(또는 [\"메모이제이션\"](https://ko.wikipedia.org/wiki/메모이제이션)) 할 수 있습니다.\n\n<Note>\n\n[React 컴파일러](/learn/react-compiler)는 비용이 많이 드는 계산을 자동으로 메모이제이션할 수 있어, 많은 경우 수동으로 `useMemo`를 사용할 필요가 없습니다.\n\n</Note>\n\n```js {5-8}\nimport { useMemo, useState } from 'react';\n\nfunction TodoList({ todos, filter }) {\n  const [newTodo, setNewTodo] = useState('');\n  const visibleTodos = useMemo(() => {\n    // ✅ todos 또는 filter가 변경되지 않는 한 다시 실행되지 않습니다.\n    return getFilteredTodos(todos, filter);\n  }, [todos, filter]);\n  // ...\n}\n```\n\n혹은 한 줄로 작성할 수도 있습니다.\n\n```js {5-6}\nimport { useMemo, useState } from 'react';\n\nfunction TodoList({ todos, filter }) {\n  const [newTodo, setNewTodo] = useState('');\n  // ✅ todos나 filter가 변경되지 않는 한 getFilteredTodos()를 다시 실행하지 않습니다.\n  const visibleTodos = useMemo(() => getFilteredTodos(todos, filter), [todos, filter]);\n  // ...\n}\n```\n\n**이렇게 하면 `todos`나 `filter`가 변경되지 않는 한 내부 함수가 다시 실행되지 않기를 원한다는 것을 React에게 알립니다.** React는 초기 렌더링 중에 `getFilteredTodos()`의 반환값을 기억합니다. 다음 렌더링 중에 `todos`나 `filter`가 다른지 확인합니다. 지난번과 동일하다면, `useMemo`는 마지막으로 저장한 결과를 반환합니다. 만약 다르다면, React는 내부 함수를 다시 호출하고 그 결과를 저장합니다.\n\n[`useMemo`](/reference/react/useMemo)로 감싸는 함수는 렌더링 중에 실행되므로, [순수한 계산](/learn/keeping-components-pure)에만 작동합니다.\n\n<DeepDive>\n\n#### 계산이 비싼지 어떻게 알 수 있나요? {/*how-to-tell-if-a-calculation-is-expensive*/}\n\n일반적으로 수천 개의 객체를 만들거나 반복하는 경우가 아니라면 비용이 많이 들지 않을 것입니다. 좀 더 확신을 얻고 싶다면 console log를 추가하여 코드에 소요된 시간을 측정할 수 있습니다.\n\n```js {1,3}\nconsole.time('filter array');\nconst visibleTodos = getFilteredTodos(todos, filter);\nconsole.timeEnd('filter array');\n```\n\n측정하려는 상호작용을 수행합니다(예: input에 입력하기). 그러면 `filter array: 0.15ms`와 같은 로그가 console에 표시됩니다. 전체적으로 기록된 시간이 상당한 양(예: 1ms 이상)으로 합산되면 해당 계산을 메모이제이션하는 것이 좋습니다. 그런 다음 실험적으로 해당 계산을 `useMemo`로 감싸서 해당 상호작용에 대해 총 로깅 시간이 감소했는지를 확인할 수 있습니다.\n\n```js\nconsole.time('filter array');\nconst visibleTodos = useMemo(() => {\n  return getFilteredTodos(todos, filter); // todos와 filter가 변경되지 않은 경우 건너뜁니다\n}, [todos, filter]);\nconsole.timeEnd('filter array');\n```\n\n`useMemo`는 *첫 번째* 렌더링을 더 빠르게 만들지 않습니다. 업데이트 시 불필요한 작업을 건너뛰는 데만 도움이 됩니다.\n\n당신의 컴퓨터가 사용자의 컴퓨터보다 빠를 수 있으므로 인위적인 속도 저하로 성능을 테스트하는 것이 좋습니다. 예를 들어 Chrome은 이를 위해 [CPU 스로틀링](https://developer.chrome.com/blog/new-in-devtools-61/#throttling) 옵션을 제공합니다.\n\n또한 개발 중에 성능을 측정하는 것은 가장 정확한 결과를 제공하지 않는다는 점에 유의하세요. (예를 들어 [Strict Mode](/reference/react/StrictMode)를 켜면 각 컴포넌트가 한 번이 아닌 두 번 렌더링 되는 것을 볼 수 있습니다.) 가장 정확한 시간을 얻으려면 프로덕션용 앱을 빌드하고 사용자가 사용하는 것과 같은 기기에서 테스트하세요.\n\n</DeepDive>\n\n### prop 변경 시 모든 state 초기화 {/*resetting-all-state-when-a-prop-changes*/}\n\n이 `ProfilePage` 컴포넌트는 `userId` prop을 받습니다. 페이지는 댓글 입력을 포함하며 `comment` state 변수를 사용해 해당 값을 보관합니다. 어느 날 한 프로필에서 다른 프로필로 이동할 때 `comment` state가 재설정되지 않는 문제를 발견했습니다. 그 결과 실수로 잘못된 사용자의 프로필에 댓글을 게시하기 쉽습니다. 이 문제를 해결하기 위해 `userId`가 변경될 때마다 `comment` state 변수를 비우려고 합니다.\n\n```js {4-7}\nexport default function ProfilePage({ userId }) {\n  const [comment, setComment] = useState('');\n\n  // 🔴 피하세요: Effect에서 prop 변경 시 state 초기화\n  useEffect(() => {\n    setComment('');\n  }, [userId]);\n  // ...\n}\n```\n\n이는 비효율적인데 `ProfilePage`와 그 자식이 오래된 값으로 처음 렌더링 한 다음 다시 렌더링 하기 때문입니다. 또한 `ProfilePage` 내부에 어떤 state가 있는 *모든* 컴포넌트에서 이 작업을 수행해야 하므로 복잡합니다. 예를 들어 댓글 UI가 중첩된 경우 중첩된 댓글 state도 비워야 합니다.\n\n대신 명시적인 key를 전달하여 각 사용자의 프로필이 개념적으로 _다른_ 프로필임을 React에 알릴 수 있습니다. 컴포넌트를 둘로 분할하고 외부 컴포넌트에서 내부 컴포넌트로 `key` 어트리뷰트를 전달하세요.\n\n```js {5,11-12}\nexport default function ProfilePage({ userId }) {\n  return (\n    <Profile\n      userId={userId}\n      key={userId}\n    />\n  );\n}\n\nfunction Profile({ userId }) {\n  // ✅ 이 state 및 아래의 다른 state는 key 변경 시 자동으로 재설정됩니다.\n  const [comment, setComment] = useState('');\n  // ...\n}\n```\n\n일반적으로 React는 동일한 컴포넌트가 같은 위치에 렌더링 될 때 state를 보존합니다. **`Profile` 컴포넌트에 `userId`를 `key`로 전달하면 React가 `userId`가 다른 두 개의 `Profile` 컴포넌트를 state를 공유해서는 안 되는 두 개의 다른 컴포넌트로 취급하도록 요청하는 것입니다.** `userId`로 설정한 key가 변경될 때마다 React는 DOM을 다시 생성하고 `Profile` 컴포넌트와 그 모든 자식의 [state를 재설정](/learn/preserving-and-resetting-state#option-2-resetting-state-with-a-key)합니다. 이제 프로필 사이를 탐색할 때 `comment` 필드가 자동으로 비워집니다.\n\n이 예시에서는 외부 `ProfilePage` 컴포넌트만 내보내 프로젝트의 다른 파일에 표시된다는 점에 유의하세요. `ProfilePage`를 렌더링하는 컴포넌트는 key를 전달할 필요 없이 일반적인 prop로 `userId`를 전달합니다. `ProfilePage`가 이를 내부 `Profile` 컴포넌트에 `key`로 전달한다는 사실은 구현 세부 사항입니다.\n\n### prop이 변경될 때 일부 state 조정하기 {/*adjusting-some-state-when-a-prop-changes*/}\n\nprop이 변경될 때 전체가 아닌 일부 state만 재설정하거나 조정하고 싶을 때가 있습니다.\n\n이 `List` 컴포넌트는 `items` 목록을 prop으로 받고 `selection` state 변수에 선택된 item을 유지합니다. `items` prop이 다른 배열을 받을 때마다 `selection`을 `null`로 재설정하고 싶습니다.\n\n```js {5-8}\nfunction List({ items }) {\n  const [isReverse, setIsReverse] = useState(false);\n  const [selection, setSelection] = useState(null);\n\n  // 🔴 피하세요: Effect에서 prop 변경 시 state 조정하기\n  useEffect(() => {\n    setSelection(null);\n  }, [items]);\n  // ...\n}\n```\n\n이것 역시 이상적이지 않습니다. `items`가 변경될 때마다 `List`와 그 자식 컴포넌트들은 처음에는 오래된 `selection` 값으로 렌더링됩니다. 그런 다음 React는 DOM을 업데이트하고 Effect를 실행합니다. 마지막으로 `setSelection(null)` 호출은 `List`와 그 자식 컴포넌트들을 다시 렌더링하여 이 전체 프로세스를 다시 시작하게 됩니다.\n\nEffect를 삭제하는 것으로 시작하세요. 대신 렌더링 중에 직접 state를 조정하세요.\n\n```js {5-11}\nfunction List({ items }) {\n  const [isReverse, setIsReverse] = useState(false);\n  const [selection, setSelection] = useState(null);\n\n  // 더 좋습니다: 렌더링 중 state 조정\n  const [prevItems, setPrevItems] = useState(items);\n  if (items !== prevItems) {\n    setPrevItems(items);\n    setSelection(null);\n  }\n  // ...\n}\n```\n\n이렇게 [이전 렌더링의 정보를 저장하는 것](/reference/react/useState#storing-information-from-previous-renders)은 이해하기 어려울 수 있지만 Effect에서 동일한 state를 업데이트하는 것보다 낫습니다. 위 예시에서는 렌더링 도중 `setSelection`이 직접 호출됩니다. React는 `return` 문으로 종료된 후 *즉시* `List`를 다시 렌더링 합니다. React는 아직 `List` 자식을 렌더링 하거나 DOM을 업데이트하지 않았기 때문에 오래된 `selection` 값의 렌더링을 건너뛸 수 있습니다.\n\n렌더링 도중 컴포넌트를 업데이트하면 React는 반환된 JSX를 버리고 즉시 렌더링을 다시 시도합니다. 매우 느린 연속적 재시도를 피하기 위해 React는 렌더링 중에 *동일한* 컴포넌트의 state만 업데이트할 수 있도록 합니다. 렌더링 도중 다른 컴포넌트의 state를 업데이트하면 에러가 발생합니다. 반복을 피하려면 `items !== prevItems`와 같은 조건이 필요합니다. 이런 식으로 state를 조정할 수는 있지만 [컴포넌트를 순수하게 유지](/learn/keeping-components-pure)하기 위해 DOM 변경이나 타임아웃 설정과 같은 다른 사이드 이펙트들은 이벤트 핸들러나 Effect에 남겨둬야 합니다.\n\n**이 패턴이 Effect보다 더 효율적이지만 대부분의 컴포넌트에는 이 패턴이 필요하지 않습니다.** 어떻게 하든 props나 다른 state에 따라 state를 조정하면 데이터 흐름을 이해하고 디버깅하기가 더 어려워집니다. 대신 [key를 사용하여 모든 state를 초기화](#resetting-all-state-when-a-prop-changes)하거나 [렌더링 중에 모든 state를 계산](#updating-state-based-on-props-or-state)할 수 있는지 항상 확인하세요. 예를 들어 선택한 *item*을 저장(및 초기화)하는 대신 선택한 *item ID*를 저장할 수 있습니다.\n\n```js {3-5}\nfunction List({ items }) {\n  const [isReverse, setIsReverse] = useState(false);\n  const [selectedId, setSelectedId] = useState(null);\n  // ✅ 최고예요: 렌더링 중에 모든 것을 계산\n  const selection = items.find(item => item.id === selectedId) ?? null;\n  // ...\n}\n```\n\n이제 state를 \"조정\"할 필요가 전혀 없습니다. 선택한 ID를 가진 item이 목록에 있으면 선택된 state로 유지됩니다. 그렇지 않은 경우 일치하는 item을 찾을 수 없으므로 렌더링 중에 계산된 `selection`은 `null`이 됩니다. 이 동작은 다르지만 대부분의 `items` 변경이 selection을 보존하므로 더 나은 방법이라고 할 수 있습니다.\n\n### 이벤트 핸들러 간 로직 공유 {/*sharing-logic-between-event-handlers*/}\n\n제품을 구매할 수 있는 두 개의 버튼(구매 및 결제)이 있는 제품 페이지가 있다고 가정해 보겠습니다. 사용자가 제품을 장바구니에 넣을 때마다 알림을 표시하고 싶습니다. 두 버튼의 클릭 핸들러에서 모두 `showNotification()`을 호출하는 것은 반복적으로 느껴지므로 이 로직을 Effect에 배치하고 싶을 수 있습니다.\n\n```js {2-7}\nfunction ProductPage({ product, addToCart }) {\n  // 🔴 피하세요: Effect 내부의 이벤트별 로직\n  useEffect(() => {\n    if (product.isInCart) {\n      showNotification(`Added ${product.name} to the shopping cart!`);\n    }\n  }, [product]);\n\n  function handleBuyClick() {\n    addToCart(product);\n  }\n\n  function handleCheckoutClick() {\n    addToCart(product);\n    navigateTo('/checkout');\n  }\n  // ...\n}\n```\n\n이 Effect는 불필요합니다. 또한 버그를 유발할 가능성이 높습니다. 예를 들어 페이지가 리로드 될 때마다 앱이 장바구니를 \"기억\"한다고 가정해 보겠습니다. 카트에 제품을 한 번 추가하고 페이지를 새로 고치면 알림이 다시 표시됩니다. 해당 제품 페이지를 새로 고칠 때마다 알림이 계속 표시됩니다. 이는 페이지 로드 시 `product.isInCart`가 이미 `true`이므로 위의 Effect는 `showNotification()`을 호출하기 때문입니다.\n\n**어떤 코드가 Effect에 있어야 하는지 이벤트 핸들러에 있어야 하는지 확실하지 않은 경우 이 코드가 실행되어야 하는 *이유*를 자문해 보세요. 컴포넌트가 사용자에게 표시되었기 *때문에* 실행되어야 하는 코드에만 Effect를 사용하세요.** 이 예시에서는 페이지가 표시되었기 때문이 아니라 사용자가 *버튼을 눌렀기* 때문에 알림이 표시되어야 합니다! Effect를 삭제하고 공유 로직을 두 이벤트 핸들러에서 호출되는 함수에 넣으세요.\n\n```js {2-6,9,13}\nfunction ProductPage({ product, addToCart }) {\n  // ✅ 좋습니다: 이벤트 핸들러에서 이벤트별 로직이 호출됩니다.\n  function buyProduct() {\n    addToCart(product);\n    showNotification(`Added ${product.name} to the shopping cart!`);\n  }\n\n  function handleBuyClick() {\n    buyProduct();\n  }\n\n  function handleCheckoutClick() {\n    buyProduct();\n    navigateTo('/checkout');\n  }\n  // ...\n}\n```\n\n이렇게 하면 불필요한 Effect가 제거되고 버그가 수정됩니다.\n\n### POST 요청 보내기 {/*sending-a-post-request*/}\n\n이 `Form` 컴포넌트는 두 가지 종류의 POST 요청을 전송합니다. 마운트 될 때 analytics 이벤트를 보냅니다. 폼을 작성하고 Submit 버튼을 클릭하면 `/api/register` 엔드포인트로 POST 요청을 보냅니다.\n\n```js {5-8,10-16}\nfunction Form() {\n  const [firstName, setFirstName] = useState('');\n  const [lastName, setLastName] = useState('');\n\n  // ✅ 좋습니다: 컴포넌트가 표시되었으므로 이 로직이 실행되어야 합니다.\n  useEffect(() => {\n    post('/analytics/event', { eventName: 'visit_form' });\n  }, []);\n\n  // 🔴 피하세요: Effect 내부의 이벤트별 로직\n  const [jsonToSubmit, setJsonToSubmit] = useState(null);\n  useEffect(() => {\n    if (jsonToSubmit !== null) {\n      post('/api/register', jsonToSubmit);\n    }\n  }, [jsonToSubmit]);\n\n  function handleSubmit(e) {\n    e.preventDefault();\n    setJsonToSubmit({ firstName, lastName });\n  }\n  // ...\n}\n```\n\n앞의 예와 동일한 기준을 적용해 보겠습니다.\n\nanalytics POST 요청은 Effect에 남아 있어야 합니다. analytics 이벤트를 전송하는 <em>이유</em>는 폼이 표시되었기 때문입니다. (개발 중에는 두 번 실행되지만 이를 처리하는 방법은 [여기](/learn/synchronizing-with-effects#sending-analytics)를 참조하세요.)\n\n그러나 `/api/register` POST 요청은 _표시되는_ 폼으로 인해 발생하는 것이 아닙니다. 사용자가 버튼을 누를 때라는 특정 시점에만 요청을 보내려고 합니다. 이 요청은 해당 _특정 상호작용에서만_ 발생해야 합니다. 두 번째 Effect를 삭제하고 해당 POST 요청을 이벤트 핸들러로 이동합니다:\n\n```js {12-13}\nfunction Form() {\n  const [firstName, setFirstName] = useState('');\n  const [lastName, setLastName] = useState('');\n\n  // ✅ 좋습니다: 컴포넌트가 표시되었으므로 이 로직이 실행됩니다.\n  useEffect(() => {\n    post('/analytics/event', { eventName: 'visit_form' });\n  }, []);\n\n  function handleSubmit(e) {\n    e.preventDefault();\n    // ✅ 좋습니다: 이벤트별 로직은 이벤트 핸들러에 있습니다.\n    post('/api/register', { firstName, lastName });\n  }\n  // ...\n}\n```\n\n어떤 로직을 이벤트 핸들러에 넣을지 Effect에 넣을지 선택할 때 사용자 관점에서 <em>어떤 종류의 로직인지</em>에 대한 답을 찾아야 합니다. 이 로직이 특정 상호작용으로 인해 발생하는 것이라면 이벤트 핸들러에 두세요. 사용자가 화면에서 컴포넌트를 <em>보는 것</em>이 원인이라면 Effect에 두세요.\n\n### 연쇄 계산 {/*chains-of-computations*/}\n\n때때로 다른 state에 따라 각각 state를 조정하는 Effect를 체이닝하고 싶을 때가 있습니다.\n\n```js {7-29}\nfunction Game() {\n  const [card, setCard] = useState(null);\n  const [goldCardCount, setGoldCardCount] = useState(0);\n  const [round, setRound] = useState(1);\n  const [isGameOver, setIsGameOver] = useState(false);\n\n  // 🔴 피하세요: 서로를 트리거하기 위해서만 state를 조정하는 Effect 체인\n  useEffect(() => {\n    if (card !== null && card.gold) {\n      setGoldCardCount(c => c + 1);\n    }\n  }, [card]);\n\n  useEffect(() => {\n    if (goldCardCount > 3) {\n      setRound(r => r + 1)\n      setGoldCardCount(0);\n    }\n  }, [goldCardCount]);\n\n  useEffect(() => {\n    if (round > 5) {\n      setIsGameOver(true);\n    }\n  }, [round]);\n\n  useEffect(() => {\n    alert('Good game!');\n  }, [isGameOver]);\n\n  function handlePlaceCard(nextCard) {\n    if (isGameOver) {\n      throw Error('Game already ended.');\n    } else {\n      setCard(nextCard);\n    }\n  }\n\n  // ...\n```\n\n이 코드에는 두 가지 문제가 있습니다.\n\n첫 번째 문제는 매우 비효율적이라는 점입니다. 컴포넌트(및 그 자식)는 체인의 각 `set` 호출 사이에 다시 렌더링해야 합니다. 위의 예시에서 최악의 경우(`setCard` → 렌더링 → `setGoldCardCount` → 렌더링 → `setRound` → 렌더링 → `setIsGameOver` → 렌더링)에는 아래 트리의 불필요한 리렌더링이 세 번 발생합니다.\n\n두 번째 문제는 속도가 느리지 않더라도 코드가 발전함에 따라 작성한 \"체인\"이 새로운 요구 사항에 맞지 않는 경우가 발생할 수 있다는 점입니다. 게임 이동의 기록을 단계별로 살펴볼 수 있는 방법을 추가한다고 가정해 보겠습니다. 각 state 변수를 과거의 값으로 업데이트하여 이를 수행할 수 있습니다. 하지만 `card` state를 과거의 값으로 설정하면 Effect 체인이 다시 트리거되고 표시되는 데이터가 변경됩니다. 이러한 코드는 융통성이 없고 취약한 경우가 많습니다.\n\n이 경우 렌더링 중에 가능한 것을 계산하고 이벤트 핸들러에서 state를 조정하는 것이 좋습니다.\n\n```js {6-7,14-26}\nfunction Game() {\n  const [card, setCard] = useState(null);\n  const [goldCardCount, setGoldCardCount] = useState(0);\n  const [round, setRound] = useState(1);\n\n  // ✅ 렌더링 중에 가능한 것을 계산합니다.\n  const isGameOver = round > 5;\n\n  function handlePlaceCard(nextCard) {\n    if (isGameOver) {\n      throw Error('Game already ended.');\n    }\n\n    // ✅ 이벤트 핸들러에서 다음 state를 모두 계산합니다.\n    setCard(nextCard);\n    if (nextCard.gold) {\n      if (goldCardCount < 3) {\n        setGoldCardCount(goldCardCount + 1);\n      } else {\n        setGoldCardCount(0);\n        setRound(round + 1);\n        if (round === 5) {\n          alert('Good game!');\n        }\n      }\n    }\n  }\n\n  // ...\n```\n\n훨씬 더 효율적입니다. 또한 게임 기록을 볼 수 있는 방법을 구현하면 이제 다른 모든 값을 조정하는 Effect 체인을 트리거 하지 않고도 각 state 변수를 과거의 행동으로 설정할 수 있습니다. 여러 이벤트 핸들러 간에 로직을 재사용해야 하는 경우 [함수를 추출](#sharing-logic-between-event-handlers)하여 해당 핸들러에서 호출할 수 있습니다.\n\n이벤트 핸들러 내부에서 [state는 스냅샷처럼 동작한다](/learn/state-as-a-snapshot)는 점을 기억하세요. 예를 들어 `setRound(round + 1)`를 호출한 후에도 `round` 변수는 사용자가 버튼을 클릭한 시점의 값을 반영합니다. 계산에 다음 값을 사용해야 하는 경우 `const nextRound = round + 1`처럼 수동으로 정의하세요.\n\n이벤트 핸들러에서 직접 다음 state를 계산할 수 **없는** 경우도 있습니다. 예를 들어 여러 개의 드롭 다운이 있는 폼에서 다음 드롭 다운의 옵션이 이전 드롭 다운의 선택된 값에 따라 달라진다고 가정해 보겠습니다. 이 경우 네트워크와 동기화하기 때문에 Effect 체인이 적절합니다.\n\n### 애플리케이션 초기화 {/*initializing-the-application*/}\n\n일부 로직은 앱이 로드될 때 한 번만 실행되어야 합니다.\n\n그것을 최상위 컴포넌트의 Effect에 배치하고 싶을 수도 있습니다.\n\n```js {2-6}\nfunction App() {\n  // 🔴 피하세요: 한 번만 실행되어야 하는 로직이 포함된 Effect\n  useEffect(() => {\n    loadDataFromLocalStorage();\n    checkAuthToken();\n  }, []);\n  // ...\n}\n```\n\n하지만 이 함수가 [개발 중에 두 번 실행된다](/learn/synchronizing-with-effects#how-to-handle-the-effect-firing-twice-in-development)는 사실을 금방 알게 될 것입니다. 함수가 두 번 호출되도록 설계되지 않았기 때문에 인증 토큰이 무효화되는 등의 문제가 발생할 수 있습니다. 일반적으로 컴포넌트는 다시 마운트 할 때 탄력이 있어야 합니다. 여기에는 최상위 `App` 컴포넌트가 포함됩니다.\n\n프로덕션 환경에서 실제로 다시 마운트되지 않을 수도 있지만 모든 컴포넌트에서 동일한 제약 조건을 따르면 코드를 이동하고 재사용하기가 더 쉬워집니다. 일부 로직이 *컴포넌트 마운트당 한 번*이 아니라 *앱 로드당 한 번* 실행되어야 하는 경우 최상위 변수를 추가하여 이미 실행되었는지를 추적하세요.\n\n```js {1,5-6,10}\nlet didInit = false;\n\nfunction App() {\n  useEffect(() => {\n    if (!didInit) {\n      didInit = true;\n      // ✅ 앱 로드당 한 번만 실행\n      loadDataFromLocalStorage();\n      checkAuthToken();\n    }\n  }, []);\n  // ...\n}\n```\n\n모듈 초기화 중이나 앱이 렌더링 되기 전에 실행할 수도 있습니다.\n\n```js {1,5}\nif (typeof window !== 'undefined') { // 브라우저에서 실행 중인지 확인합니다.\n   // ✅ 앱 로드당 한 번만 실행\n  checkAuthToken();\n  loadDataFromLocalStorage();\n}\n\nfunction App() {\n  // ...\n}\n```\n\n컴포넌트를 import 할 때 최상위 레벨의 코드는 렌더링 되지 않더라도 한 번 실행됩니다. 임의의 컴포넌트를 import 할 때 속도 저하나 예상치 못한 동작을 방지하려면 이 패턴을 과도하게 사용하지 마세요. app 전체 초기화 로직은 `App.js`와 같은 루트 컴포넌트 모듈이나 애플리케이션의 엔트리 포인트에 두세요.\n\n\n### state 변경을 부모 컴포넌트에게 알리기 {/*notifying-parent-components-about-state-changes*/}\n\n`true` 또는 `false`가 될 수 있는 내부 `isOn` state를 가진 `Toggle` 컴포넌트를 작성하고 있다고 가정해 보겠습니다. 클릭 또는 드래그를 통해 토글하는 방법에는 몇 가지가 있습니다. `Toggle` 내부 state가 변경될 때마다 부모 컴포넌트에 알리고 싶을 때 `onChange` 이벤트를 노출하고 Effect에서 호출합니다.\n\n```js {4-7}\nfunction Toggle({ onChange }) {\n  const [isOn, setIsOn] = useState(false);\n\n  // 🔴 피하세요: onChange 핸들러가 너무 늦게 실행됨\n  useEffect(() => {\n    onChange(isOn);\n  }, [isOn, onChange])\n\n  function handleClick() {\n    setIsOn(!isOn);\n  }\n\n  function handleDragEnd(e) {\n    if (isCloserToRightEdge(e)) {\n      setIsOn(true);\n    } else {\n      setIsOn(false);\n    }\n  }\n\n  // ...\n}\n```\n\n앞서와 마찬가지로 이것은 이상적이지 않습니다. `Toggle`이 먼저 state를 업데이트하고 React가 화면을 업데이트합니다. 그런 다음 React는 Effect를 실행하고 부모 컴포넌트에서 전달된 `onChange` 함수를 호출합니다. 이제 부모 컴포넌트는 자신의 state를 업데이트하고 다른 렌더링 패스를 시작합니다. 모든 것을 한 번의 패스로 처리하는 것이 좋습니다.\n\nEffect를 삭제하고 대신 동일한 이벤트 핸들러 내에서 *두* 컴포넌트의 state를 업데이트합니다.\n\n```js {5-7,11,16,18}\nfunction Toggle({ onChange }) {\n  const [isOn, setIsOn] = useState(false);\n\n  function updateToggle(nextIsOn) {\n    // ✅ 좋습니다: 업데이트를 유발한 이벤트가 발생한 동안 모든 업데이트를 수행합니다.\n    setIsOn(nextIsOn);\n    onChange(nextIsOn);\n  }\n\n  function handleClick() {\n    updateToggle(!isOn);\n  }\n\n  function handleDragEnd(e) {\n    if (isCloserToRightEdge(e)) {\n      updateToggle(true);\n    } else {\n      updateToggle(false);\n    }\n  }\n\n  // ...\n}\n```\n\n이 접근 방식을 사용하면 `Toggle` 컴포넌트와 그 부모 컴포넌트 모두 이벤트가 진행되는 동안 state를 업데이트 합니다. React는 서로 다른 컴포넌트의 [업데이트를 일괄 처리](/learn/queueing-a-series-of-state-updates)하므로 렌더링 패스는 한 번만 발생합니다.\n\nstate를 완전히 제거하고 대신 부모 컴포넌트로부터 `isOn`을 수신할 수도 있습니다.\n\n```js {1,2}\n// ✅ 이것도 좋습니다: 컴포넌트는 부모에 의해 완전히 제어됩니다.\nfunction Toggle({ isOn, onChange }) {\n  function handleClick() {\n    onChange(!isOn);\n  }\n\n  function handleDragEnd(e) {\n    if (isCloserToRightEdge(e)) {\n      onChange(true);\n    } else {\n      onChange(false);\n    }\n  }\n\n  // ...\n}\n```\n\n[\"state 끌어올리기\"](/learn/sharing-state-between-components)는 부모 컴포넌트가 부모 자체의 state를 토글 하여 `Toggle`을 완전히 제어할 수 있게 해줍니다. 즉, 부모 컴포넌트에 더 많은 로직을 포함해야 하지만 전체적으로 걱정해야 할 state는 줄어듭니다. 두 개의 서로 다른 state 변수를 동기화하려고 할 때마다 대신 state 끌어올리기를 사용해 보세요!\n\n### 부모에게 데이터 전달하기 {/*passing-data-to-the-parent*/}\n\n이 `Child` 컴포넌트는 일부 데이터를 가져온 다음 Effect에서 `Parent` 컴포넌트에 전달합니다.\n\n```js {9-14}\nfunction Parent() {\n  const [data, setData] = useState(null);\n  // ...\n  return <Child onFetched={setData} />;\n}\n\nfunction Child({ onFetched }) {\n  const data = useSomeAPI();\n  // 🔴 피하세요: Effect에서 부모에게 데이터 전달하기\n  useEffect(() => {\n    if (data) {\n      onFetched(data);\n    }\n  }, [onFetched, data]);\n  // ...\n}\n```\n\nReact에서 데이터는 부모 컴포넌트에서 자식 컴포넌트로 흐릅니다. 화면에 뭔가 잘못된 것이 보이면 컴포넌트 체인을 따라 올라가서 어떤 컴포넌트가 잘못된 prop을 전달하거나 잘못된 state를 가지고 있는지 찾아내면 정보의 출처를 추적할 수 있습니다. 자식 컴포넌트가 Effect에서 부모 컴포넌트의 state를 업데이트하면 데이터 흐름을 추적하기가 매우 어려워집니다. 자식과 부모 모두 동일한 데이터가 필요하므로 부모 컴포넌트가 해당 데이터를 가져와서 자식에게 대신 *내려주도록* 하세요.\n\n```js {4-5}\nfunction Parent() {\n  const data = useSomeAPI();\n  // ...\n  // ✅ 좋습니다: 자식에게 데이터 전달하기\n  return <Child data={data} />;\n}\n\nfunction Child({ data }) {\n  // ...\n}\n```\n\n이렇게 하면 데이터가 부모에서 자식으로 내려오기 때문에 데이터 흐름이 더 간단하고 예측 가능하게 유지됩니다.\n\n### 외부 저장소 구독하기 {/*subscribing-to-an-external-store*/}\n\n때로는 컴포넌트가 React state 외부의 일부 데이터를 구독해야 할 수도 있습니다. 이 데이터는 서드파티 라이브러리 또는 내장 브라우저 API에서 가져올 수 있습니다. 이 데이터는 React가 모르는 사이에 변경될 수 있으므로 컴포넌트를 수동으로 구독해야 합니다. 이 작업은 종종 Effect를 통해 수행됩니다. 다음은 예시입니다.\n\n```js {2-17}\nfunction useOnlineStatus() {\n  // 이상적이지 않습니다: Effect에서 저장소를 수동으로 구독\n  const [isOnline, setIsOnline] = useState(true);\n  useEffect(() => {\n    function updateState() {\n      setIsOnline(navigator.onLine);\n    }\n\n    updateState();\n\n    window.addEventListener('online', updateState);\n    window.addEventListener('offline', updateState);\n    return () => {\n      window.removeEventListener('online', updateState);\n      window.removeEventListener('offline', updateState);\n    };\n  }, []);\n  return isOnline;\n}\n\nfunction ChatIndicator() {\n  const isOnline = useOnlineStatus();\n  // ...\n}\n```\n\n여기서 컴포넌트는 외부 데이터 저장소(이 경우 브라우저 `navigator.onLine` API)를 구독합니다. 이 API는 서버에 존재하지 않으므로(초기 HTML에 사용할 수 없으므로) 처음 state는 `true`로 설정됩니다. 브라우저에서 해당 데이터 저장소의 값이 변경될 때마다 컴포넌트는 해당 state를 업데이트합니다.\n\n이를 위해 Effect를 사용하는 것이 일반적이지만 React에는 외부 저장소를 구독하기 위해 특별히 제작된 Hook이 있습니다. Effect를 삭제하고 [`useSyncExternalStore`](/reference/react/useSyncExternalStore)에 대한 호출로 대체합니다.\n\n```js {11-16}\nfunction subscribe(callback) {\n  window.addEventListener('online', callback);\n  window.addEventListener('offline', callback);\n  return () => {\n    window.removeEventListener('online', callback);\n    window.removeEventListener('offline', callback);\n  };\n}\n\nfunction useOnlineStatus() {\n  // ✅ 좋습니다: 내장 Hook으로 외부 스토어 구독하기\n  return useSyncExternalStore(\n    subscribe, // 동일한 함수를 전달하는 한 React는 다시 구독하지 않습니다.\n    () => navigator.onLine, // 클라이언트에서 값을 얻는 방법\n    () => true // 서버에서 값을 얻는 방법\n  );\n}\n\nfunction ChatIndicator() {\n  const isOnline = useOnlineStatus();\n  // ...\n}\n```\n\n이 접근 방식은 변경 가능한 데이터를 Effect를 사용해 React state에 수동으로 동기화하는 것보다 에러가 덜 발생합니다. 일반적으로 위의 `useOnlineStatus()`와 같은 사용자 정의 Hook을 작성하여 개별 컴포넌트에서 이 코드를 반복할 필요가 없도록 합니다. [React 컴포넌트에서 외부 store를 구독하는 방법에 대해 자세히 읽어보세요.](/reference/react/useSyncExternalStore)\n\n### 데이터 가져오기 {/*fetching-data*/}\n\n많은 앱이 데이터 가져오기를 시작하기 위해 Effect를 사용합니다. 이와 같은 데이터를 가져오는 Effect를 작성하는 것은 매우 일반적입니다.\n\n```js {5-10}\nfunction SearchResults({ query }) {\n  const [results, setResults] = useState([]);\n  const [page, setPage] = useState(1);\n\n  useEffect(() => {\n    // 🔴 피하세요: 정리 로직 없이 가져오기\n    fetchResults(query, page).then(json => {\n      setResults(json);\n    });\n  }, [query, page]);\n\n  function handleNextPageClick() {\n    setPage(page + 1);\n  }\n  // ...\n}\n```\n\n데이터 가져오기를 이벤트 핸들러로 옮길 필요는 *없습니다*.\n\n이벤트 핸들러에 로직을 넣어야 했던 앞선 예시와 모순되는 것처럼 보일 수 있습니다! 하지만 데이터 가져오기를 해야 하는 주된 이유가 *입력 이벤트*가 아니라는 점을 고려하세요. 검색 입력은 URL에서 미리 채워지는 경우가 많으며 사용자는 입력을 건드리지 않고 뒤로 앞으로 탐색할 수도 있습니다.\n\n`page`와 `query`의 출처가 어디인지는 중요하지 않습니다. 이 컴포넌트가 표시되는 동안에는 현재 `page` 및 `query`에 대한 네트워크의 데이터와 `results`를 [동기화](/learn/synchronizing-with-effects)하고 싶을 것입니다. 이것이 바로 Effect의 이유입니다.\n\n하지만 위의 코드에는 버그가 있습니다. `\"hello\"`를 빠르게 입력한다고 가정해 봅시다. 그러면 `query`가 `\"h\"`에서 `\"he\"`, `\"hel\"`, `\"hell\"`, `\"hello\"`로 바뀝니다. 이렇게 하면 별도의 데이터 가져오기가 시작되지만 응답이 어떤 순서로 도착할지는 보장할 수 없습니다. 예를 들어 `\"hello\"` 응답 *후에* `\"hell\"` 응답이 도착할 수 있습니다. `setResults()`를 마지막으로 호출하므로 잘못된 검색 결과가 표시될 수 있습니다. 이를 [\"경쟁 조건\"](https://ko.wikipedia.org/wiki/경쟁_상태)이라고 하는데, 서로 다른 두 요청이 서로 \"경쟁\"하여 예상과 다른 순서로 도착하는 것을 말합니다.\n\n**경쟁 조건을 수정하려면 오래된 응답을 무시하는 [정리 함수를 추가](/learn/synchronizing-with-effects#fetching-data)해야 합니다.**\n\n```js {5,7,9,11-13}\nfunction SearchResults({ query }) {\n  const [results, setResults] = useState([]);\n  const [page, setPage] = useState(1);\n  useEffect(() => {\n    let ignore = false;\n    fetchResults(query, page).then(json => {\n      if (!ignore) {\n        setResults(json);\n      }\n    });\n    return () => {\n      ignore = true;\n    };\n  }, [query, page]);\n\n  function handleNextPageClick() {\n    setPage(page + 1);\n  }\n  // ...\n}\n```\n\n이렇게 하면 Effect가 데이터를 가져올 때 마지막으로 요청된 응답을 제외한 모든 응답이 무시됩니다.\n\n데이터 가져오기를 구현할 때 경쟁 조건을 처리하는 것만이 어려운 것은 아닙니다. 응답 캐싱(사용자가 뒤로가기 버튼을 클릭하여 이전 화면을 즉시 볼 수 있도록), 서버에서 데이터를 가져오는 방법(초기 서버 렌더링 HTML에 스피너 대신 가져온 콘텐츠가 포함되도록), 네트워크 워터폴을 피하는 방법(자식이 모든 부모를 기다리지 않고 데이터를 가져올 수 있도록)도 고려해야 합니다.\n\n**이러한 문제는 React뿐만 아니라 모든 UI 라이브러리에 적용됩니다. 이를 해결하는 것은 간단하지 않으며, 그렇기 때문에 최신 [프레임워크](/learn/creating-a-react-app#full-stack-frameworks)는 Effect에서 데이터를 가져오는 것보다 더 효율적인 내장 데이터 페칭 메커니즘을 제공합니다.**\n\n프레임워크를 사용하지 않고(그리고 직접 빌드하고 싶지 않고) Effect에서 데이터를 보다 인체공학적으로 가져오고 싶다면 이 예시처럼 가져오기 로직을 사용자 정의 Hook으로 추출하는 것을 고려하세요.\n\n```js {4}\nfunction SearchResults({ query }) {\n  const [page, setPage] = useState(1);\n  const params = new URLSearchParams({ query, page });\n  const results = useData(`/api/search?${params}`);\n\n  function handleNextPageClick() {\n    setPage(page + 1);\n  }\n  // ...\n}\n\nfunction useData(url) {\n  const [data, setData] = useState(null);\n  useEffect(() => {\n    let ignore = false;\n    fetch(url)\n      .then(response => response.json())\n      .then(json => {\n        if (!ignore) {\n          setData(json);\n        }\n      });\n    return () => {\n      ignore = true;\n    };\n  }, [url]);\n  return data;\n}\n```\n\n또한 에러 처리와 콘텐츠 로딩 여부를 추적하기 위한 로직을 추가하고 싶을 것입니다. 이와 같은 Hook을 직접 빌드하거나 React 에코시스템에서 이미 사용 가능한 많은 솔루션 중 하나를 사용할 수 있습니다. **이 방법만으로는 프레임워크에 내장된 데이터 가져오기 메커니즘을 사용하는 것만큼 효율적이지는 않지만, 데이터 가져오기 로직을 사용자 정의 Hook으로 옮기면 나중에 효율적인 데이터 가져오기 전략을 취하기가 더 쉬워집니다.**\n\n일반적으로 Effect를 작성해야 할 때마다 위의 `useData`와 같이 보다 선언적이고 목적에 맞게 구축된 API를 사용하여 일부 기능을 커스텀 Hook으로 추출할 수 있는 경우를 주시하세요. 컴포넌트에서 원시 `useEffect` 호출이 적을수록 애플리케이션을 유지 관리하기가 더 쉬워집니다.\n\n<Recap>\n\n- 렌더링 중에 무언가를 계산할 수 있다면 Effect가 필요하지 않습니다.\n- 비용이 많이 드는 계산을 캐시하려면 `useEffect` 대신 `useMemo`를 추가하세요.\n- 전체 컴포넌트 트리의 state를 초기화하려면 다른 `key`를 전달하세요.\n- prop 변경에 대한 응답으로 특정 state bit를 초기화하려면 렌더링 중에 설정하세요.\n- 컴포넌트가 *표시되어* 실행되는 코드는 Effect에 있어야 하고 나머지는 이벤트에 있어야 합니다.\n- 여러 컴포넌트의 state를 업데이트해야 하는 경우 단일 이벤트 중에 수행하는 것이 좋습니다.\n- 다른 컴포넌트의 state 변수를 동기화하려고 할 때마다 state 끌어올리기를 고려하세요.\n- Effect로 데이터를 가져올 수 있지만 경쟁 조건을 피하기 위해 정리를 구현해야 합니다.\n\n</Recap>\n\n<Challenges>\n\n#### Effect 없이 데이터 변환하기 {/*transform-data-without-effects*/}\n\n아래의 todos 목록에 `TodoList`가 표시됩니다. \"Show only active todos\" 체크박스를 선택하면 완료된 todos는 목록에 표시되지 않습니다. 표시되는 todos와 관계없이 footer에는 아직 완료되지 않은 todos의 수가 표시됩니다.\n\n불필요한 state와 Effect를 모두 제거하여 이 컴포넌트를 단순화하세요.\n\n<Sandpack>\n\n```js\nimport { useState, useEffect } from 'react';\nimport { initialTodos, createTodo } from './todos.js';\n\nexport default function TodoList() {\n  const [todos, setTodos] = useState(initialTodos);\n  const [showActive, setShowActive] = useState(false);\n  const [activeTodos, setActiveTodos] = useState([]);\n  const [visibleTodos, setVisibleTodos] = useState([]);\n  const [footer, setFooter] = useState(null);\n\n  useEffect(() => {\n    setActiveTodos(todos.filter(todo => !todo.completed));\n  }, [todos]);\n\n  useEffect(() => {\n    setVisibleTodos(showActive ? activeTodos : todos);\n  }, [showActive, todos, activeTodos]);\n\n  useEffect(() => {\n    setFooter(\n      <footer>\n        {activeTodos.length} todos left\n      </footer>\n    );\n  }, [activeTodos]);\n\n  return (\n    <>\n      <label>\n        <input\n          type=\"checkbox\"\n          checked={showActive}\n          onChange={e => setShowActive(e.target.checked)}\n        />\n        Show only active todos\n      </label>\n      <NewTodo onAdd={newTodo => setTodos([...todos, newTodo])} />\n      <ul>\n        {visibleTodos.map(todo => (\n          <li key={todo.id}>\n            {todo.completed ? <s>{todo.text}</s> : todo.text}\n          </li>\n        ))}\n      </ul>\n      {footer}\n    </>\n  );\n}\n\nfunction NewTodo({ onAdd }) {\n  const [text, setText] = useState('');\n\n  function handleAddClick() {\n    setText('');\n    onAdd(createTodo(text));\n  }\n\n  return (\n    <>\n      <input value={text} onChange={e => setText(e.target.value)} />\n      <button onClick={handleAddClick}>\n        Add\n      </button>\n    </>\n  );\n}\n```\n\n```js src/todos.js\nlet nextId = 0;\n\nexport function createTodo(text, completed = false) {\n  return {\n    id: nextId++,\n    text,\n    completed\n  };\n}\n\nexport const initialTodos = [\n  createTodo('Get apples', true),\n  createTodo('Get oranges', true),\n  createTodo('Get carrots'),\n];\n```\n\n```css\nlabel { display: block; }\ninput { margin-top: 10px; }\n```\n\n</Sandpack>\n\n<Hint>\n\n렌더링 중에 무언가를 계산할 수 있다면 state나 이를 업데이트하는 Effect가 필요하지 않습니다.\n\n</Hint>\n\n<Solution>\n\n이 예시에서는 `todos` 목록과 체크박스의 체크 여부를 나타내는 `showActive` state 변수의 두 가지 필수 state만 있습니다. 다른 모든 state 변수는 [불필요하며](/learn/choosing-the-state-structure#avoid-redundant-state) 렌더링 중에 대신 계산할 수 있습니다. 여기에는 주변 JSX로 바로 이동할 수 있는 `footer`가 포함됩니다.\n\n결과는 다음과 같아야 합니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\nimport { initialTodos, createTodo } from './todos.js';\n\nexport default function TodoList() {\n  const [todos, setTodos] = useState(initialTodos);\n  const [showActive, setShowActive] = useState(false);\n  const activeTodos = todos.filter(todo => !todo.completed);\n  const visibleTodos = showActive ? activeTodos : todos;\n\n  return (\n    <>\n      <label>\n        <input\n          type=\"checkbox\"\n          checked={showActive}\n          onChange={e => setShowActive(e.target.checked)}\n        />\n        Show only active todos\n      </label>\n      <NewTodo onAdd={newTodo => setTodos([...todos, newTodo])} />\n      <ul>\n        {visibleTodos.map(todo => (\n          <li key={todo.id}>\n            {todo.completed ? <s>{todo.text}</s> : todo.text}\n          </li>\n        ))}\n      </ul>\n      <footer>\n        {activeTodos.length} todos left\n      </footer>\n    </>\n  );\n}\n\nfunction NewTodo({ onAdd }) {\n  const [text, setText] = useState('');\n\n  function handleAddClick() {\n    setText('');\n    onAdd(createTodo(text));\n  }\n\n  return (\n    <>\n      <input value={text} onChange={e => setText(e.target.value)} />\n      <button onClick={handleAddClick}>\n        Add\n      </button>\n    </>\n  );\n}\n```\n\n```js src/todos.js\nlet nextId = 0;\n\nexport function createTodo(text, completed = false) {\n  return {\n    id: nextId++,\n    text,\n    completed\n  };\n}\n\nexport const initialTodos = [\n  createTodo('Get apples', true),\n  createTodo('Get oranges', true),\n  createTodo('Get carrots'),\n];\n```\n\n```css\nlabel { display: block; }\ninput { margin-top: 10px; }\n```\n\n</Sandpack>\n\n</Solution>\n\n#### Effect 없이 계산 캐시하기 {/*cache-a-calculation-without-effects*/}\n\n이 예시에서는 todos 필터링이 `getVisibleTodos()`라는 별도의 함수로 추출되었습니다. 이 함수 안에는 언제 호출되는지 알 수 있도록 `console.log()` 호출이 포함되어 있습니다. \"Show only active todos\"를 토글하면 `getVisibleTodos()`가 다시 실행되는 것을 확인할 수 있습니다. 이는 표시할 todos를 토글하면 표시되는 todos가 변경되기 때문에 예상되는 현상입니다.\n\n여러분이 해야 할 일은 `TodoList` 컴포넌트에서 `visibleTodos` 목록을 다시 계산하는 Effect를 제거하는 것입니다. 그러나 input에 입력할 때 `getVisibleTodos()`가 다시 실행되지 않도록(따라서 로그를 출력하지 *않도록*) 해야 합니다.\n\n<Hint>\n\n한 가지 해결책은 `useMemo` 호출을 추가하여 표시되는 todos를 캐시하는 것입니다. 덜 분명한 또 다른 해결책도 있습니다.\n\n</Hint>\n\n<Sandpack>\n\n```js\nimport { useState, useEffect } from 'react';\nimport { initialTodos, createTodo, getVisibleTodos } from './todos.js';\n\nexport default function TodoList() {\n  const [todos, setTodos] = useState(initialTodos);\n  const [showActive, setShowActive] = useState(false);\n  const [text, setText] = useState('');\n  const [visibleTodos, setVisibleTodos] = useState([]);\n\n  useEffect(() => {\n    setVisibleTodos(getVisibleTodos(todos, showActive));\n  }, [todos, showActive]);\n\n  function handleAddClick() {\n    setText('');\n    setTodos([...todos, createTodo(text)]);\n  }\n\n  return (\n    <>\n      <label>\n        <input\n          type=\"checkbox\"\n          checked={showActive}\n          onChange={e => setShowActive(e.target.checked)}\n        />\n        Show only active todos\n      </label>\n      <input value={text} onChange={e => setText(e.target.value)} />\n      <button onClick={handleAddClick}>\n        Add\n      </button>\n      <ul>\n        {visibleTodos.map(todo => (\n          <li key={todo.id}>\n            {todo.completed ? <s>{todo.text}</s> : todo.text}\n          </li>\n        ))}\n      </ul>\n    </>\n  );\n}\n```\n\n```js src/todos.js\nlet nextId = 0;\nlet calls = 0;\n\nexport function getVisibleTodos(todos, showActive) {\n  console.log(`getVisibleTodos() was called ${++calls} times`);\n  const activeTodos = todos.filter(todo => !todo.completed);\n  const visibleTodos = showActive ? activeTodos : todos;\n  return visibleTodos;\n}\n\nexport function createTodo(text, completed = false) {\n  return {\n    id: nextId++,\n    text,\n    completed\n  };\n}\n\nexport const initialTodos = [\n  createTodo('Get apples', true),\n  createTodo('Get oranges', true),\n  createTodo('Get carrots'),\n];\n```\n\n```css\nlabel { display: block; }\ninput { margin-top: 10px; }\n```\n\n</Sandpack>\n\n<Solution>\n\nstate 변수와 Effect를 제거하고 대신 `getVisibleTodos()`를 호출한 결과를 캐시하는 `useMemo` 호출을 추가합니다.\n\n<Sandpack>\n\n```js\nimport { useState, useMemo } from 'react';\nimport { initialTodos, createTodo, getVisibleTodos } from './todos.js';\n\nexport default function TodoList() {\n  const [todos, setTodos] = useState(initialTodos);\n  const [showActive, setShowActive] = useState(false);\n  const [text, setText] = useState('');\n  const visibleTodos = useMemo(\n    () => getVisibleTodos(todos, showActive),\n    [todos, showActive]\n  );\n\n  function handleAddClick() {\n    setText('');\n    setTodos([...todos, createTodo(text)]);\n  }\n\n  return (\n    <>\n      <label>\n        <input\n          type=\"checkbox\"\n          checked={showActive}\n          onChange={e => setShowActive(e.target.checked)}\n        />\n        Show only active todos\n      </label>\n      <input value={text} onChange={e => setText(e.target.value)} />\n      <button onClick={handleAddClick}>\n        Add\n      </button>\n      <ul>\n        {visibleTodos.map(todo => (\n          <li key={todo.id}>\n            {todo.completed ? <s>{todo.text}</s> : todo.text}\n          </li>\n        ))}\n      </ul>\n    </>\n  );\n}\n```\n\n```js src/todos.js\nlet nextId = 0;\nlet calls = 0;\n\nexport function getVisibleTodos(todos, showActive) {\n  console.log(`getVisibleTodos() was called ${++calls} times`);\n  const activeTodos = todos.filter(todo => !todo.completed);\n  const visibleTodos = showActive ? activeTodos : todos;\n  return visibleTodos;\n}\n\nexport function createTodo(text, completed = false) {\n  return {\n    id: nextId++,\n    text,\n    completed\n  };\n}\n\nexport const initialTodos = [\n  createTodo('Get apples', true),\n  createTodo('Get oranges', true),\n  createTodo('Get carrots'),\n];\n```\n\n```css\nlabel { display: block; }\ninput { margin-top: 10px; }\n```\n\n</Sandpack>\n\n이렇게 변경하면 `todos` 또는 `showActive`가 변경되는 경우에만 `getVisibleTodos()`가 호출됩니다. input에 입력하면 `text` state 변수만 변경되므로 `getVisibleTodos()` 호출이 트리거 되지 않습니다.\n\n`useMemo`가 필요 없는 다른 해결책도 있습니다. `text` state 변수가 todos 목록에 영향을 줄 수 없으므로 `NewTodo` 폼을 별도의 컴포넌트로 추출하고 `text` state 변수를 그 안에 옮길 수 있습니다.\n\n<Sandpack>\n\n```js\nimport { useState, useMemo } from 'react';\nimport { initialTodos, createTodo, getVisibleTodos } from './todos.js';\n\nexport default function TodoList() {\n  const [todos, setTodos] = useState(initialTodos);\n  const [showActive, setShowActive] = useState(false);\n  const visibleTodos = getVisibleTodos(todos, showActive);\n\n  return (\n    <>\n      <label>\n        <input\n          type=\"checkbox\"\n          checked={showActive}\n          onChange={e => setShowActive(e.target.checked)}\n        />\n        Show only active todos\n      </label>\n      <NewTodo onAdd={newTodo => setTodos([...todos, newTodo])} />\n      <ul>\n        {visibleTodos.map(todo => (\n          <li key={todo.id}>\n            {todo.completed ? <s>{todo.text}</s> : todo.text}\n          </li>\n        ))}\n      </ul>\n    </>\n  );\n}\n\nfunction NewTodo({ onAdd }) {\n  const [text, setText] = useState('');\n\n  function handleAddClick() {\n    setText('');\n    onAdd(createTodo(text));\n  }\n\n  return (\n    <>\n      <input value={text} onChange={e => setText(e.target.value)} />\n      <button onClick={handleAddClick}>\n        Add\n      </button>\n    </>\n  );\n}\n```\n\n```js src/todos.js\nlet nextId = 0;\nlet calls = 0;\n\nexport function getVisibleTodos(todos, showActive) {\n  console.log(`getVisibleTodos() was called ${++calls} times`);\n  const activeTodos = todos.filter(todo => !todo.completed);\n  const visibleTodos = showActive ? activeTodos : todos;\n  return visibleTodos;\n}\n\nexport function createTodo(text, completed = false) {\n  return {\n    id: nextId++,\n    text,\n    completed\n  };\n}\n\nexport const initialTodos = [\n  createTodo('Get apples', true),\n  createTodo('Get oranges', true),\n  createTodo('Get carrots'),\n];\n```\n\n```css\nlabel { display: block; }\ninput { margin-top: 10px; }\n```\n\n</Sandpack>\n\n이 접근 방식도 요구 사항을 충족합니다. input에 입력하면 `text` state 변수만 업데이트됩니다. `text` state 변수가 하위 `NewTodo` 컴포넌트에 있기 때문에 상위 `TodoList` 컴포넌트는 다시 렌더링 되지 않습니다. 이것이 사용자가 입력할 때 `getVisibleTodos()`가 호출되지 않는 이유입니다. (다른 이유로 `TodoList`가 다시 렌더링 되는 경우에도 여전히 호출됩니다.)\n\n</Solution>\n\n#### Effect 없이 state 초기화하기 {/*reset-state-without-effects*/}\n\n이 `EditContact` 컴포넌트는 `{ id, name, email }` 모양의 연락처 객체를 `savedContact` prop으로 받습니다. name과 email input 필드를 편집해 보세요. Save을 누르면 폼 위의 연락처 버튼이 편집된 name으로 업데이트됩니다. Reset을 누르면 폼의 보류 중인 변경 사항이 모두 삭제됩니다. 이 UI를 사용해 보면서 사용법을 익혀보세요.\n\n상단의 버튼으로 연락처를 선택하면 해당 연락처의 세부 정보를 반영하도록 폼이 초기화됩니다. 이 작업은 `EditContact.js` 내의 Effect로 수행됩니다. 이 Effect를 제거합니다. `savedContact.id`가 변경될 때 폼을 초기화하는 다른 방법을 찾아보세요.\n\n<Sandpack>\n\n```js src/App.js hidden\nimport { useState } from 'react';\nimport ContactList from './ContactList.js';\nimport EditContact from './EditContact.js';\n\nexport default function ContactManager() {\n  const [\n    contacts,\n    setContacts\n  ] = useState(initialContacts);\n  const [\n    selectedId,\n    setSelectedId\n  ] = useState(0);\n  const selectedContact = contacts.find(c =>\n    c.id === selectedId\n  );\n\n  function handleSave(updatedData) {\n    const nextContacts = contacts.map(c => {\n      if (c.id === updatedData.id) {\n        return updatedData;\n      } else {\n        return c;\n      }\n    });\n    setContacts(nextContacts);\n  }\n\n  return (\n    <div>\n      <ContactList\n        contacts={contacts}\n        selectedId={selectedId}\n        onSelect={id => setSelectedId(id)}\n      />\n      <hr />\n      <EditContact\n        savedContact={selectedContact}\n        onSave={handleSave}\n      />\n    </div>\n  )\n}\n\nconst initialContacts = [\n  { id: 0, name: 'Taylor', email: 'taylor@mail.com' },\n  { id: 1, name: 'Alice', email: 'alice@mail.com' },\n  { id: 2, name: 'Bob', email: 'bob@mail.com' }\n];\n```\n\n```js src/ContactList.js hidden\nexport default function ContactList({\n  contacts,\n  selectedId,\n  onSelect\n}) {\n  return (\n    <section>\n      <ul>\n        {contacts.map(contact =>\n          <li key={contact.id}>\n            <button onClick={() => {\n              onSelect(contact.id);\n            }}>\n              {contact.id === selectedId ?\n                <b>{contact.name}</b> :\n                contact.name\n              }\n            </button>\n          </li>\n        )}\n      </ul>\n    </section>\n  );\n}\n```\n\n```js src/EditContact.js active\nimport { useState, useEffect } from 'react';\n\nexport default function EditContact({ savedContact, onSave }) {\n  const [name, setName] = useState(savedContact.name);\n  const [email, setEmail] = useState(savedContact.email);\n\n  useEffect(() => {\n    setName(savedContact.name);\n    setEmail(savedContact.email);\n  }, [savedContact]);\n\n  return (\n    <section>\n      <label>\n        Name:{' '}\n        <input\n          type=\"text\"\n          value={name}\n          onChange={e => setName(e.target.value)}\n        />\n      </label>\n      <label>\n        Email:{' '}\n        <input\n          type=\"email\"\n          value={email}\n          onChange={e => setEmail(e.target.value)}\n        />\n      </label>\n      <button onClick={() => {\n        const updatedData = {\n          id: savedContact.id,\n          name: name,\n          email: email\n        };\n        onSave(updatedData);\n      }}>\n        Save\n      </button>\n      <button onClick={() => {\n        setName(savedContact.name);\n        setEmail(savedContact.email);\n      }}>\n        Reset\n      </button>\n    </section>\n  );\n}\n```\n\n```css\nul, li {\n  list-style: none;\n  margin: 0;\n  padding: 0;\n}\nli { display: inline-block; }\nli button {\n  padding: 10px;\n}\nlabel {\n  display: block;\n  margin: 10px 0;\n}\nbutton {\n  margin-right: 10px;\n  margin-bottom: 10px;\n}\n```\n\n</Sandpack>\n\n<Hint>\n\n`savedContact.id`가 다른 경우 `EditContact` 폼은 개념적으로 _다른 연락처의 폼_이며 state를 보존해서는 안 된다는 것을 React에 알리는 방법이 있다면 좋을 것 같습니다. 그런 방법을 기억하시나요?\n\n</Hint>\n\n<Solution>\n\n`EditContact` 컴포넌트를 둘로 분할합니다. 모든 폼 state를 내부 `EditForm` 컴포넌트로 이동합니다. 외부 `EditContact` 컴포넌트를 내보내고 내부 `EditForm` 컴포넌트에 `savedContact.id`를 `key`로 전달하도록 합니다. 그 결과 내부 `EditForm` 컴포넌트는 다른 연락처를 선택할 때마다 모든 폼 state를 초기화하고 DOM을 다시 생성합니다.\n\n<Sandpack>\n\n```js src/App.js hidden\nimport { useState } from 'react';\nimport ContactList from './ContactList.js';\nimport EditContact from './EditContact.js';\n\nexport default function ContactManager() {\n  const [\n    contacts,\n    setContacts\n  ] = useState(initialContacts);\n  const [\n    selectedId,\n    setSelectedId\n  ] = useState(0);\n  const selectedContact = contacts.find(c =>\n    c.id === selectedId\n  );\n\n  function handleSave(updatedData) {\n    const nextContacts = contacts.map(c => {\n      if (c.id === updatedData.id) {\n        return updatedData;\n      } else {\n        return c;\n      }\n    });\n    setContacts(nextContacts);\n  }\n\n  return (\n    <div>\n      <ContactList\n        contacts={contacts}\n        selectedId={selectedId}\n        onSelect={id => setSelectedId(id)}\n      />\n      <hr />\n      <EditContact\n        savedContact={selectedContact}\n        onSave={handleSave}\n      />\n    </div>\n  )\n}\n\nconst initialContacts = [\n  { id: 0, name: 'Taylor', email: 'taylor@mail.com' },\n  { id: 1, name: 'Alice', email: 'alice@mail.com' },\n  { id: 2, name: 'Bob', email: 'bob@mail.com' }\n];\n```\n\n```js src/ContactList.js hidden\nexport default function ContactList({\n  contacts,\n  selectedId,\n  onSelect\n}) {\n  return (\n    <section>\n      <ul>\n        {contacts.map(contact =>\n          <li key={contact.id}>\n            <button onClick={() => {\n              onSelect(contact.id);\n            }}>\n              {contact.id === selectedId ?\n                <b>{contact.name}</b> :\n                contact.name\n              }\n            </button>\n          </li>\n        )}\n      </ul>\n    </section>\n  );\n}\n```\n\n```js src/EditContact.js active\nimport { useState } from 'react';\n\nexport default function EditContact(props) {\n  return (\n    <EditForm\n      {...props}\n      key={props.savedContact.id}\n    />\n  );\n}\n\nfunction EditForm({ savedContact, onSave }) {\n  const [name, setName] = useState(savedContact.name);\n  const [email, setEmail] = useState(savedContact.email);\n\n  return (\n    <section>\n      <label>\n        Name:{' '}\n        <input\n          type=\"text\"\n          value={name}\n          onChange={e => setName(e.target.value)}\n        />\n      </label>\n      <label>\n        Email:{' '}\n        <input\n          type=\"email\"\n          value={email}\n          onChange={e => setEmail(e.target.value)}\n        />\n      </label>\n      <button onClick={() => {\n        const updatedData = {\n          id: savedContact.id,\n          name: name,\n          email: email\n        };\n        onSave(updatedData);\n      }}>\n        Save\n      </button>\n      <button onClick={() => {\n        setName(savedContact.name);\n        setEmail(savedContact.email);\n      }}>\n        Reset\n      </button>\n    </section>\n  );\n}\n```\n\n```css\nul, li {\n  list-style: none;\n  margin: 0;\n  padding: 0;\n}\nli { display: inline-block; }\nli button {\n  padding: 10px;\n}\nlabel {\n  display: block;\n  margin: 10px 0;\n}\nbutton {\n  margin-right: 10px;\n  margin-bottom: 10px;\n}\n```\n\n</Sandpack>\n\n</Solution>\n\n#### Effect 없이 폼 제출하기 {/*submit-a-form-without-effects*/}\n\n이 `Form` 컴포넌트를 사용하면 친구에게 메시지를 보낼 수 있습니다. 폼을 제출하면 `showForm` state 변수가 `false`로 설정됩니다. 그러면 `sendMessage(message)`를 호출하는 Effect가 트리거되어 메시지가 전송됩니다(콘솔에서 확인할 수 있음). 메시지가 전송되면 \"Open chat\" 버튼이 있는 \"Thank you\" 대화 상자가 표시되어 폼으로 돌아갈 수 있습니다.\n\n앱 사용자가 너무 많은 메시지를 보내고 있습니다. 채팅을 조금 더 어렵게 만들기 위해 양식 대신 \"Thank you\" 대화 상자를 *먼저* 표시하기로 결정했습니다. `showForm` state 변수를 `true`가 아닌 `false`로 초기화하도록 변경합니다. 이렇게 변경하자마자 콘솔에 빈 메시지가 전송된 것으로 표시됩니다. 이 로직의 뭔가가 잘못되었습니다!\n\n이 문제의 근본 원인은 무엇인가요? 그리고 어떻게 해결할 수 있을까요?\n\n<Hint>\n\n사용자가 \"Thank you\" 대화 상자를 보았기 _때문에_ 메시지를 보내야 하나요? 아니면 그 반대일까요?\n\n</Hint>\n\n<Sandpack>\n\n```js\nimport { useState, useEffect } from 'react';\n\nexport default function Form() {\n  const [showForm, setShowForm] = useState(true);\n  const [message, setMessage] = useState('');\n\n  useEffect(() => {\n    if (!showForm) {\n      sendMessage(message);\n    }\n  }, [showForm, message]);\n\n  function handleSubmit(e) {\n    e.preventDefault();\n    setShowForm(false);\n  }\n\n  if (!showForm) {\n    return (\n      <>\n        <h1>Thanks for using our services!</h1>\n        <button onClick={() => {\n          setMessage('');\n          setShowForm(true);\n        }}>\n          Open chat\n        </button>\n      </>\n    );\n  }\n\n  return (\n    <form onSubmit={handleSubmit}>\n      <textarea\n        placeholder=\"Message\"\n        value={message}\n        onChange={e => setMessage(e.target.value)}\n      />\n      <button type=\"submit\" disabled={message === ''}>\n        Send\n      </button>\n    </form>\n  );\n}\n\nfunction sendMessage(message) {\n  console.log('Sending message: ' + message);\n}\n```\n\n```css\nlabel, textarea { margin-bottom: 10px; display: block; }\n```\n\n</Sandpack>\n\n<Solution>\n\n`showForm` state 변수는 폼을 표시할지 아니면 \"Thank you\" 대화 상자를 표시할지를 결정합니다. 그러나 \"Thank you\" 대화 상자가 _표시되었기_ 때문에 메시지를 보내지 않습니다. 사용자가 _폼을 제출했기_ 때문에 메시지를 보내려고 합니다. 오해의 소지가 있는 Effect를 삭제하고 `handleSubmit` 이벤트 핸들러 내부로 `sendMessage` 호출을 이동합니다.\n\n<Sandpack>\n\n```js\nimport { useState, useEffect } from 'react';\n\nexport default function Form() {\n  const [showForm, setShowForm] = useState(true);\n  const [message, setMessage] = useState('');\n\n  function handleSubmit(e) {\n    e.preventDefault();\n    setShowForm(false);\n    sendMessage(message);\n  }\n\n  if (!showForm) {\n    return (\n      <>\n        <h1>Thanks for using our services!</h1>\n        <button onClick={() => {\n          setMessage('');\n          setShowForm(true);\n        }}>\n          Open chat\n        </button>\n      </>\n    );\n  }\n\n  return (\n    <form onSubmit={handleSubmit}>\n      <textarea\n        placeholder=\"Message\"\n        value={message}\n        onChange={e => setMessage(e.target.value)}\n      />\n      <button type=\"submit\" disabled={message === ''}>\n        Send\n      </button>\n    </form>\n  );\n}\n\nfunction sendMessage(message) {\n  console.log('Sending message: ' + message);\n}\n```\n\n```css\nlabel, textarea { margin-bottom: 10px; display: block; }\n```\n\n</Sandpack>\n\n이 버전에서는 이벤트인 _폼을 제출하는 것_만으로 메시지가 전송되는 것을 확인할 수 있습니다. 이 기능은 `showForm`이 처음에 `true`으로 설정되었는지 `false`로 설정되었는지에 관계없이 동일하게 잘 작동합니다. (`false`로 설정하면 추가 콘솔 메시지가 표시되지 않습니다.)\n\n</Solution>\n\n</Challenges>\n"
  },
  {
    "path": "src/content/learn/your-first-component.md",
    "content": "---\ntitle: 첫 번째 컴포넌트\n---\n\n<Intro>\n\n*컴포넌트*는 React의 핵심 개념 중 하나입니다. 컴포넌트는 사용자 인터페이스(UI)를 구축하는 기반이 되므로 React 여정을 시작하기에 완벽한 곳입니다!\n\n</Intro>\n\n<YouWillLearn>\n\n* 컴포넌트가 무엇일까\n* React 애플리케이션에서 컴포넌트의 역할\n* 첫 번째 React 컴포넌트를 작성하는 방법\n\n</YouWillLearn>\n\n## 컴포넌트: UI 구성 요소 {/*components-ui-building-blocks*/}\n\n웹에서는 HTML을 통해 `<h1>`, `<li>`와 같은 태그를 사용하여 풍부한 구조의 문서를 만들 수 있습니다.\n\n```html\n<article>\n  <h1>My First Component</h1>\n  <ol>\n    <li>Components: UI Building Blocks</li>\n    <li>Defining a Component</li>\n    <li>Using a Component</li>\n  </ol>\n</article>\n```\n\n이 마크업은 `<article>`, 제목 `<h1>`, (축약된) 목차를 정렬된 목록 `<ol>`로 나타냅니다. 이와 같은 마크업은 스타일을 위한 CSS, 상호작용을 위한 JavaScript와 결합하여 웹에서 볼 수 있는 모든 사이드바, 아바타, 모달, 드롭다운 등 모든 UI의 기반이 됩니다.\n\nReact를 사용하면 마크업, CSS, JavaScript를 **앱의 재사용 가능한 UI 요소인** 사용자 정의 \"컴포넌트\"로 결합할 수 있습니다. 위에서 본 목차 코드는 모든 페이지에 렌더링할 수 있는 `<TableOfContents />` 컴포넌트로 전환될 수 있습니다. 내부적으로는 여전히 `<article>`, `<h1>` 등과 같은 동일한 HTML태그를 사용합니다.\n\nHTML 태그와 마찬가지로 컴포넌트를 작성, 순서 지정 및 중첩하여 전체 페이지를 디자인할 수 있습니다. 예를 들어, 여러분이 읽고 있는 문서 페이지는 React 컴포넌트로 구성되어 있습니다.\n\n```js\n<PageLayout>\n  <NavigationHeader>\n    <SearchBar />\n    <Link to=\"/docs\">Docs</Link>\n  </NavigationHeader>\n  <Sidebar />\n  <PageContent>\n    <TableOfContents />\n    <DocumentationText />\n  </PageContent>\n</PageLayout>\n```\n\n프로젝트가 성장함에 따라 이미 작성한 컴포넌트를 재사용하여 많은 디자인을 구성할 수 있으므로 개발 속도가 빨라집니다. 위의 목차는 `<TableOfContents />`를 사용하여 어떤 화면에도 추가할 수 있습니다! [Chakra UI](https://chakra-ui.com/), [Material UI.](https://material-ui.com/)와 같은 React 오픈 소스 커뮤니티에서 공유되는 수천 개의 컴포넌트로 프로젝트를 빠르게 시작할 수도 있습니다.\n\n## 컴포넌트 정의하기 {/*defining-a-component*/}\n\n기존에는 웹 페이지를 만들 때 웹 개발자가 컨텐츠를 마크업한 다음 JavaScript를 뿌려 상호작용을 추가했습니다. 이는 웹에서 상호작용이 중요했던 시절에 효과적이였습니다. 이제는 많은 사이트와 모든 앱에서 상호작용을 기대합니다. React는 동일한 기술을 사용하면서도 상호작용을 우선시합니다. **React 컴포넌트는 *마크업으로 뿌릴 수 있는 JavaScript* 함수입니다.** 그 모습은 다음과 같습니다.(아래 예시는 편집할 수 있습니다.)\n\n<Sandpack>\n\n```js\nexport default function Profile() {\n  return (\n    <img\n      src=\"https://i.imgur.com/MK3eW3Am.jpg\"\n      alt=\"Katherine Johnson\"\n    />\n  )\n}\n```\n\n```css\nimg { height: 200px; }\n```\n\n</Sandpack>\n\n컴포넌트를 빌드하는 방법은 다음과 같습니다.\n\n### 1단계: 컴포넌트 내보내기 {/*step-1-export-the-component*/}\n\n`export default` 접두사는 [표준 JavaScript 구문](https://developer.mozilla.org/docs/web/javascript/reference/statements/export)입니다(React에만 해당되지 않습니다). 이 접두사를 사용하면 나중에 다른 파일에서 가져올 수 있도록 파일에 주요 기능을 표시할 수 있습니다. (더 자세한 내용은 [컴포넌트 Importing 및 Exporting](/learn/importing-and-exporting-components)을 참고하세요!)\n\n### 2단계: 함수 정의하기 {/*step-2-define-the-function*/}\n\n`function Profile() { }`을 사용하면 `Profile`이라는 이름의 JavaScript함수를 정의할 수 있습니다.\n\n<Pitfall>\n\nReact 컴포넌트는 일반 JavaScript 함수이지만, **이름은 대문자로 시작해야 하며** 그렇지 않으면 작동하지 않습니다!\n\n</Pitfall>\n\n### 3단계: 마크업 추가하기 {/*step-3-add-markup*/}\n\n이 컴포넌트는 `src` 및 `alt` 속성을 가진 `<img />` 태그를 반환합니다. `<img />`는 HTML처럼 작성되었지만 실제로는 JavaScript입니다! 이 구문을 [JSX](/learn/writing-markup-with-jsx)라고 하며, JavaScript 안에 마크업을 삽입할 수 있습니다.\n\n반환문은 이 컴포넌트에서처럼 한 줄에 모두 작성할 수 있습니다.\n\n```js\nreturn <img src=\"https://i.imgur.com/MK3eW3As.jpg\" alt=\"Katherine Johnson\" />;\n```\n\n그러나 마크업이 모두 `return` 키워드와 같은 라인에 있지 않은 경우에는 다음과 같이 괄호로 묶어야 합니다.\n\n```js\nreturn (\n  <div>\n    <img src=\"https://i.imgur.com/MK3eW3As.jpg\" alt=\"Katherine Johnson\" />\n  </div>\n);\n```\n\n<Pitfall>\n\n괄호가 없으면 `return` 뒷 라인에 있는 모든 코드가 [무시됩니다](https://stackoverflow.com/questions/2846283/what-are-the-rules-for-javascripts-automatic-semicolon-insertion-asi)!\n\n</Pitfall>\n\n## 컴포넌트 사용하기 {/*using-a-component*/}\n\n이제 `Profile` 컴포넌트를 정의했으므로 다른 컴포넌트 안에 중첩할 수 있습니다. 예를 들어 여러 `Profile` 컴포넌트를 사용하는 `Gallery` 컴포넌트를 내보낼 수 있습니다.\n\n<Sandpack>\n\n```js\nfunction Profile() {\n  return (\n    <img\n      src=\"https://i.imgur.com/MK3eW3As.jpg\"\n      alt=\"Katherine Johnson\"\n    />\n  );\n}\n\nexport default function Gallery() {\n  return (\n    <section>\n      <h1>Amazing scientists</h1>\n      <Profile />\n      <Profile />\n      <Profile />\n    </section>\n  );\n}\n```\n\n```css\nimg { margin: 0 10px 10px 0; height: 90px; }\n```\n\n</Sandpack>\n\n### 브라우저에 표시되는 내용 {/*what-the-browser-sees*/}\n\n대소문자의 차이에 주목하세요.\n\n* `<section>`은 소문자이므로 React는 HTML태그를 가리킨다고 이해합니다.\n* `<Profile />`은 대문자 `p`로 시작하므로 React는 `Profile`이라는 컴포넌트를 사용하고자 한다고 이해합니다.\n\n그리고 `Profile`은 더 많은 `<img />`가 포함되어 있습니다. 결국 브라우저에 표시되는 내용은 다음과 같습니다.\n\n```html\n<section>\n  <h1>Amazing scientists</h1>\n  <img src=\"https://i.imgur.com/MK3eW3As.jpg\" alt=\"Katherine Johnson\" />\n  <img src=\"https://i.imgur.com/MK3eW3As.jpg\" alt=\"Katherine Johnson\" />\n  <img src=\"https://i.imgur.com/MK3eW3As.jpg\" alt=\"Katherine Johnson\" />\n</section>\n```\n\n### 컴포넌트 중첩 및 구성 {/*nesting-and-organizing-components*/}\n\n컴포넌트는 일반 JavaScript함수이므로 같은 파일에 여러 컴포넌트를 포함할 수 있습니다. 컴포넌트가 상대적으로 작거나 서로 밀접하게 관련되어 있을 때 편리합니다. 이 파일이 복잡해지면 언제든지 `Profile`을 별도의 파일로 옮길 수 있습니다. 이 방법은 바로 다음 장인 [컴포넌트의 importing과 exporting](/learn/importing-and-exporting-components) 페이지에서 확인할 수 있습니다.\n\n`Profile` 컴포넌트는 `Gallery`안에서 렌더링되기 때문에(심지어 여러번 렌더링됩니다!), `Gallery`는 각 `Profile`을 \"자식\"으로 렌더링하는 **부모 컴포넌트**라고 말할 수 있습니다. 컴포넌트를 한 번 정의한 다음 원하는 곳에서 원하는 만큼 여러 번 사용할 수 있다는 점이 바로 React의 마법입니다.\n\n<Pitfall>\n\n컴포넌트는 다른 컴포넌트를 렌더링할 수 있지만, **그 정의를 중첩해서는 안 됩니다.**\n\n```js {2-5}\nexport default function Gallery() {\n  // 🔴 절대 컴포넌트 안에 다른 컴포넌트를 정의하면 안 됩니다!\n  function Profile() {\n    // ...\n  }\n  // ...\n}\n```\n\n위 스니펫은 [매우 느리고 버그를 촉발합니다.](/learn/preserving-and-resetting-state#different-components-at-the-same-position-reset-state) 대신 최상위 레벨에서 컴포넌트를 정의하세요.\n\n```js {5-8}\nexport default function Gallery() {\n  // ...\n}\n\n// ✅ 최상위 레벨에서 컴포넌트를 선언합니다\nfunction Profile() {\n  // ...\n}\n```\n\n자식 컴포넌트에 부모 컴포넌트의 일부 데이터가 필요한 경우, 정의를 중첩하는 대신 [props로 전달](/learn/passing-props-to-a-component)하세요.\n\n</Pitfall>\n\n<DeepDive>\n\n#### 컴포넌트의 모든 것 {/*components-all-the-way-down*/}\n\nReact 애플리케이션은 \"root\"컴포넌트에서 시작됩니다. 보통 새 프로젝트를 시작할 때 자동으로 생성됩니다. 예를 들어, [CodeSandbox](https://codesandbox.io/) 또는 [Next.js](https://nextjs.org/)를 사용하는 경우, root 컴포넌트는 `pages/index.js`에 정의됩니다. 이 예시에서는 root 컴포넌트를 내보내고 있습니다.\n\n대부분의 React 앱은 모든 부분에서 컴포넌트를 사용합니다. 즉, 버튼과 같이 재사용 가능한 부분뿐만 아니라 사이드바, 목록, 그리고 궁극적으로 전체 페이지와 같은 큰 부분에도 컴포넌트를 사용하게 됩니다! 컴포넌트는 한 번만 사용되더라도 UI 코드와 마크업을 정리하는 편리한 방법입니다.\n\n[React 기반 프레임워크들](/learn/creating-a-react-app)은 이를 한 단계 더 발전시킵니다. 빈 HTML파일을 사용하고 React가 JavaScript로 페이지 관리를 \"다룰 수 있게\" 하도록 하는 대신, React 컴포넌트에서 HTML을 자동으로 생성하기도합니다. 이를 통해 JavaScript 코드가 로드되기 전에 앱에서 일부 컨텐츠를 표시할 수 있습니다.\n\n그렇지만 여전히 많은 웹사이트들은 React를 [약간의 상호작용을 추가하는 용도로만](/learn/add-react-to-an-existing-project#using-react-for-a-part-of-your-existing-page) 사용합니다. 이러한 웹사이트에는 전체 페이지에 하나의 root 컴포넌트가 아닌 여러 개의 root 컴포넌트가 있습니다. 필요한 만큼 React를 많이 또는 적게 사용할 수 있습니다.\n\n</DeepDive>\n\n<Recap>\n\n이제 막 React를 처음 사용해 보았습니다! 몇 가지 핵심 사항을 요약해 보겠습니다.\n\n* React를 사용하면 앱의 **재사용 가능한 UI 요소**인 컴포넌트를 만들 수 있습니다.\n* React 앱에서 모든 UI는 컴포넌트입니다.\n* React 컴포넌트는 다음 몇 가지를 제외하고는 일반적인 JavaScript 함수입니다.\n\n  1. 컴포넌트의 이름은 항상 대문자로 시작합니다.\n  2. JSX 마크업을 반환합니다.\n\n</Recap>\n\n\n\n<Challenges>\n\n#### 컴포넌트 내보내기 {/*export-the-component*/}\n\nroot 컴포넌트를 내보내지 않았기 때문에 이 샌드박스는 작동하지 않습니다.\n\n<Sandpack>\n\n```js\nfunction Profile() {\n  return (\n    <img\n      src=\"https://i.imgur.com/lICfvbD.jpg\"\n      alt=\"Aklilu Lemma\"\n    />\n  );\n}\n```\n\n```css\nimg { height: 181px; }\n```\n\n</Sandpack>\n\n정답을 확인하기 전에 직접 해결해 보세요\n\n<Solution>\n\n함수 정의 앞에 `export default`를 추가하세요.\n\n<Sandpack>\n\n```js\nexport default function Profile() {\n  return (\n    <img\n      src=\"https://i.imgur.com/lICfvbD.jpg\"\n      alt=\"Aklilu Lemma\"\n    />\n  );\n}\n```\n\n```css\nimg { height: 181px; }\n```\n\n</Sandpack>\n\n이 예시에서 왜 'export'만으로 해결되지 않는지 궁금할 것입니다. [컴포넌트의 importing과 exporting](/learn/importing-and-exporting-components)에서 'export'와 'export default'의 차이점을 배울 수 있습니다.\n\n</Solution>\n\n#### return문을 고치세요 {/*fix-the-return-statement*/}\n\n이 `return`문에는 문제가 있습니다. 고칠 수 있나요?\n\n<Hint>\n\n이 문제를 해결하려고 시도하는 동안 \"예기치 않은 토큰\" 오류가 발생할 수 있습니다. 이 경우 세미콜론이 닫는 괄호 *뒤에* 나타나는지 확인하세요. `return ( )` 안에 세미콜론을 남겨두면 오류가 발생합니다\n\n</Hint>\n\n\n<Sandpack>\n\n```js\nexport default function Profile() {\n  return\n    <img src=\"https://i.imgur.com/jA8hHMpm.jpg\" alt=\"Katsuko Saruhashi\" />;\n}\n```\n\n```css\nimg { height: 180px; }\n```\n\n</Sandpack>\n\n<Solution>\n\n다음과 같이 return문을 한 줄로 이동하여 이 컴포넌트를 수정할 수 있습니다.\n\n<Sandpack>\n\n```js\nexport default function Profile() {\n  return <img src=\"https://i.imgur.com/jA8hHMpm.jpg\" alt=\"Katsuko Saruhashi\" />;\n}\n```\n\n```css\nimg { height: 180px; }\n```\n\n</Sandpack>\n\n또는 반환된 JSX 마크업을 괄호 안에 감싸서 `return` 바로 뒤에 열 수 있습니다.\n\n<Sandpack>\n\n```js\nexport default function Profile() {\n  return (\n    <img\n      src=\"https://i.imgur.com/jA8hHMpm.jpg\"\n      alt=\"Katsuko Saruhashi\"\n    />\n  );\n}\n```\n\n```css\nimg { height: 180px; }\n```\n\n</Sandpack>\n\n</Solution>\n\n#### 실수를 찾아내세요 {/*spot-the-mistake*/}\n\n`Profile` 컴포넌트가 선언되고 사용되는 방식에 문제가 있습니다. 실수를 발견할 수 있을까요? (React가 컴포넌트를 일반 HTML 태그와 어떻게 구분하는지 기억해 보세요!)\n\n<Sandpack>\n\n```js\nfunction profile() {\n  return (\n    <img\n      src=\"https://i.imgur.com/QIrZWGIs.jpg\"\n      alt=\"Alan L. Hart\"\n    />\n  );\n}\n\nexport default function Gallery() {\n  return (\n    <section>\n      <h1>Amazing scientists</h1>\n      <profile />\n      <profile />\n      <profile />\n    </section>\n  );\n}\n```\n\n```css\nimg { margin: 0 10px 10px 0; height: 90px; }\n```\n\n</Sandpack>\n\n<Solution>\n\nReact 컴포넌트 이름은 대문자로 시작해야 합니다.\n\n`function profile()`을 `function Profile()`로 변경한 다음 모든 `<profile />`을 `<Profile />`로 변경합니다.\n\n<Sandpack>\n\n```js\nfunction Profile() {\n  return (\n    <img\n      src=\"https://i.imgur.com/QIrZWGIs.jpg\"\n      alt=\"Alan L. Hart\"\n    />\n  );\n}\n\nexport default function Gallery() {\n  return (\n    <section>\n      <h1>Amazing scientists</h1>\n      <Profile />\n      <Profile />\n      <Profile />\n    </section>\n  );\n}\n```\n\n```css\nimg { margin: 0 10px 10px 0; }\n```\n\n</Sandpack>\n\n</Solution>\n\n#### 컴포넌트를 새로 작성해 보세요. {/*your-own-component*/}\n\n컴포넌트를 처음부터 작성해 보세요. 유효한 이름을 지정하고 마크업을 반환할 수 있습니다. 아이디어가 떠오르지 않는다면 `<h1>Good job!</h1>` 라고 표시하는 `Congratulations` 컴포넌트를 작성할 수 있습니다. export를 잊지 마세요!\n\n<Sandpack>\n\n```js\n// 아래에 컴포넌트를 작성해 보세요!\n\n```\n\n</Sandpack>\n\n<Solution>\n\n<Sandpack>\n\n```js\nexport default function Congratulations() {\n  return (\n    <h1>Good job!</h1>\n  );\n}\n```\n\n</Sandpack>\n\n</Solution>\n\n</Challenges>\n"
  },
  {
    "path": "src/content/reference/dev-tools/react-performance-tracks.md",
    "content": "---\ntitle: React Performance 트랙\n---\n\n<Intro>\n\nReact Performance(성능) 트랙은 브라우저 개발자 도구의 Performance 패널 타임라인에 표시되는 특수한 맞춤형 항목입니다.\n\n</Intro>\n\n이 트랙은 개발자에게 React 애플리케이션의 성능에 대한 포괄적인 인사이트를 제공하도록 설계되었습니다. 네트워크 요청, 자바스크립트 실행, 이벤트 루프 활동과 같은 주요 데이터 소스와 함께 React 전용 이벤트와 메트릭을 시각화하며, 애플리케이션 동작을 완전히 이해할 수 있도록 Performance(성능) 패널 내의 통합된 타임라인에서 모든 정보가 동기화되어 표시됩니다.\n\n<div style={{display: 'flex', justifyContent: 'center', marginBottom: '1rem'}}>\n  <img className=\"w-full light-image\" src=\"/images/docs/performance-tracks/overview.png\" alt=\"React Performance 트랙\" />\n  <img className=\"w-full dark-image\" src=\"/images/docs/performance-tracks/overview.dark.png\" alt=\"React Performance 트랙\" />\n</div>\n\n<InlineToc />\n\n---\n\n## 사용법 {/*usage*/}\n\nReact Performance 트랙은 개발 및 프로파일링 빌드에서만 사용할 수 있습니다.\n\n- **개발**: 기본적으로 활성화되어 있습니다.\n- **프로파일링**: 기본적으로 Scheduler 트랙만 활성화되어 있습니다. 컴포넌트 트랙은 [`<Profiler>`](/reference/react/Profiler)로 감싼 서브트리 내의 컴포넌트만 나열합니다. [React 개발자 도구 확장 프로그램](/learn/react-developer-tools)을 활성화했다면, `<Profiler>`로 감싸지 않았더라도 모든 컴포넌트가 컴포넌트 트랙에 포함됩니다. 서버 트랙은 프로파일링 빌드에서 사용할 수 없습니다.\n\n활성화된 경우 [확장성 API](https://developer.chrome.com/docs/devtools/performance/extension)를 제공하는 브라우저의 Performance(성능) 패널로 기록한 트레이스에서 트랙이 자동으로 표시됩니다.\n\n<Pitfall>\n\nReact Performance 트랙을 구동하는 프로파일링 도구는 추가적인 오버헤드를 발생시키므로, 프로덕션 빌드에서는 기본적으로 비활성화되어 있습니다.\n서버 컴포넌트 및 서버 요청(Server Requests) 트랙은 개발 빌드에서만 사용할 수 있습니다.\n\n</Pitfall>\n\n### 프로파일링 빌드 사용하기 {/*using-profiling-builds*/}\n\n프로덕션 및 개발 빌드 외에도 React는 특수한 프로파일링 빌드를 제공합니다.\n프로파일링 빌드를 사용하려면 `react-dom/client` 대신 `react-dom/profiling`을 사용해야 합니다. 모든 `react-dom/client` 임포트(import) 구문을 수동으로 수정하는 대신 **번들러 별칭(alias)** 을 설정하여 빌드 시점에 \n`react-dom/client` 가 `react-dom/profiling` 을 가리키도록 하는 것을 권장합니다.\n사용 중인 프레임워크에 React 프로파일링 빌드를 활성화하는 기능이 내장되어 있을 수 있습니다.\n\n---\n\n## 트랙 {/*tracks*/}\n\n### Scheduler {/*scheduler*/}\n\nScheduler는 우선순위가 다른 작업을 관리하는 데 사용되는 React의 내부 개념입니다. 이 트랙은 특정 우선순위의 작업을 나타내는 네 개의 서브트랙으로 구성됩니다.\n\n- **Blocking** - 사용자 상호작용에 의해 시작될 수 있는 동기 업데이트입니다.\n- **Transition** - 백그라운드에서 발생하는 논블로킹 작업으로 주로 [`startTransition`](/reference/react/startTransition)을 통해 시작됩니다.\n- **Suspense** - 폴백(fallback) 표시나 콘텐츠 노출 등 Suspense 경계와 관련된 작업입니다.\n- **Idle** - 우선순위가 더 높은 다른 작업이 없을 때 수행되는 가장 낮은 우선순위의 작업입니다.\n\n<div style={{display: 'flex', justifyContent: 'center', marginBottom: '1rem'}}>\n  <img className=\"w-full light-image\" src=\"/images/docs/performance-tracks/scheduler.png\" alt=\"Scheduler 트랙\" />\n  <img className=\"w-full dark-image\" src=\"/images/docs/performance-tracks/scheduler.dark.png\" alt=\"Scheduler 트랙\" />\n</div>\n\n#### 렌더링 {/*renders*/}\n\n모든 렌더링 과정(render pass)은 타임라인에서 확인할 수 있는 여러 단계로 구성됩니다.\n\n- **Update** - 새로운 렌더링 과정을 발생시킨 원인입니다.\n- **Render** - React가 컴포넌트의 렌더링 함수를 호출하여 업데이트된 서브트리를 렌더링하는 단계입니다. 렌더링된 컴포넌트 서브트리는 동일한 색 구성표를 따르는 [컴포넌트 트랙](#components)에서 확인할 수 있습니다.\n- **Commit** - 컴포넌트 렌더링 후, React가 DOM에 변경 사항을 반영하고 [`useLayoutEffect`](/reference/react/useLayoutEffect)와 같은 레이아웃 Effect를 실행하는 단계입니다.\n- **Remaining Effects** - React가 렌더링된 서브트리의 패시브(passive) Effect를 실행하는 단계입니다. 이는 주로 브라우저가 화면을 그린(paint) 후에 발생하며, 이 시점에 [`useEffect`](/reference/react/useEffect)와 같은 Hook이 실행됩니다. 클릭과 같은 사용자 상호작용이나 다른 불연속 이벤트(discrete events)는 예외적으로 페인트 이전에 실행될 수 있습니다.\n\n<div style={{display: 'flex', justifyContent: 'center', marginBottom: '1rem'}}>\n  <img className=\"w-full light-image\" src=\"/images/docs/performance-tracks/scheduler-update.png\" alt=\"Scheduler 트랙: 업데이트\" />\n  <img className=\"w-full dark-image\" src=\"/images/docs/performance-tracks/scheduler-update.dark.png\" alt=\"Scheduler 트랙: 업데이트\" />\n</div>\n\n[렌더링과 커밋에 대해 더 알아보기](/learn/render-and-commit).\n\n#### 연쇄 업데이트 {/*cascading-updates*/}\n\n연쇄 업데이트(Cascading updates)는 성능 저하를 유발하는 대표적인 패턴 중 하나입니다. 렌더링 과정 중에 업데이트가 예약되면, React는 이미 완료된 작업을 폐기하고 새로운 과정을 다시 시작할 수 있습니다.\n\n개발 빌드에서는 어떤 컴포넌트가 새로운 업데이트를 예약했는지 확인할 수 있습니다. 여기에는 일반적인 업데이트와 연쇄적으로 발생하는 업데이트가 모두 포함됩니다. \"Cascading update\" 항목을 클릭하면 업데이트를 예약한 메서드 이름이 포함된 상세한 스택 트레이스를 확인할 수 있습니다.\n\n<div style={{display: 'flex', justifyContent: 'center', marginBottom: '1rem'}}>\n  <img className=\"w-full light-image\" src=\"/images/docs/performance-tracks/scheduler-cascading-update.png\" alt=\"Scheduler 트랙: 연쇄 업데이트\" />\n  <img className=\"w-full dark-image\" src=\"/images/docs/performance-tracks/scheduler-cascading-update.dark.png\" alt=\"Scheduler 트랙: 연쇄 업데이트\" />\n</div>\n\n[Effect에 대해 더 알아보기](/learn/you-might-not-need-an-effect).\n\n### 컴포넌트 {/*components*/}\n\n컴포넌트 트랙은 React 컴포넌트의 지속 시간을 시각화합니다. 각 항목은 해당 컴포넌트와 그 **모든 하위 컴포넌트**의 렌더링 지속 시간을 나타내는 플레임그래프로 표시됩니다.\n\n<div style={{display: 'flex', justifyContent: 'center', marginBottom: '1rem'}}>\n  <img className=\"w-full light-image\" src=\"/images/docs/performance-tracks/components-render.png\" alt=\"컴포넌트 트랙: 렌더링 지속 시간\" />\n  <img className=\"w-full dark-image\" src=\"/images/docs/performance-tracks/components-render.dark.png\" alt=\"컴포넌트 트랙: 렌더링 지속 시간\" />\n</div>\n\n렌더링 지속 시간과 유사하게 Effect 지속 시간도 플레임그래프로 표현되지만 Scheduler 트랙의 해당 단계와 일치하는 다른 색 구성표를 사용합니다.\n\n<div style={{display: 'flex', justifyContent: 'center', marginBottom: '1rem'}}>\n  <img className=\"w-full light-image\" src=\"/images/docs/performance-tracks/components-effects.png\" alt=\"컴포넌트 트랙: effect 지속 시간\" />\n  <img className=\"w-full dark-image\" src=\"/images/docs/performance-tracks/components-effects.dark.png\" alt=\"컴포넌트 트랙: effect 지속 시간\" />\n</div>\n\n<Note>\n\n렌더링과 달리 모든 Effect가 기본적으로 컴포넌트 트랙에 표시되는 것은 아닙니다.\n\n성능을 유지하고 **UI가 복잡해지는 것을 방지하기 위해** React는 0.05ms 이상의 지속 시간을 가지거나 업데이트를 트리거한 Effect만 표시합니다.\n\n</Note>\n\n렌더링 및 Effect 단계 중에 추가 이벤트가 표시될 수 있습니다.\n\n- <span style={{padding: '0.125rem 0.25rem', backgroundColor: '#facc15', color: '#1f1f1fff'}}>마운트</span> - 컴포넌트 렌더링 또는 Effect에 해당하는 서브트리가 마운트되었습니다.\n- <span style={{padding: '0.125rem 0.25rem', backgroundColor: '#facc15', color: '#1f1f1fff'}}>마운트 해제</span> - 컴포넌트 렌더링 또는 Effect에 해당하는 서브트리가 마운트 해제되었습니다.\n- <span style={{padding: '0.125rem 0.25rem', backgroundColor: '#facc15', color: '#1f1f1fff'}}>Reconnect</span> - 마운트와 유사하지만 [`<Activity>`](/reference/react/Activity)를 사용하는 경우에만 발생합니다.\n- <span style={{padding: '0.125rem 0.25rem', backgroundColor: '#facc15', color: '#1f1f1fff'}}>Disconnect</span> - 마운트 해제와 유사하지만 [`<Activity>`](/reference/react/Activity)를 사용하는 경우에만 발생합니다.\n\n#### 변경된 props {/*changed-props*/}\n\n개발 빌드에서 컴포넌트 렌더링 항목을 클릭하면 props의 잠재적인 변경 사항을 확인할 수 있습니다. 이 정보를 사용하여 불필요한 렌더링을 식별할 수 있습니다.\n\n<div style={{display: 'flex', justifyContent: 'center', marginBottom: '1rem'}}>\n  <img className=\"w-full light-image\" src=\"/images/docs/performance-tracks/changed-props.png\" alt=\"컴포넌트 트랙: 변경된 props\" />\n  <img className=\"w-full dark-image\" src=\"/images/docs/performance-tracks/changed-props.dark.png\" alt=\"컴포넌트 트랙: 변경된 props\" />\n</div>\n\n### 서버 {/*server*/}\n\n<div style={{display: 'flex', justifyContent: 'center', marginBottom: '1rem'}}>\n  <img className=\"w-full light-image\" src=\"/images/docs/performance-tracks/server-overview.png\" alt=\"React 서버 성능 트랙\" />\n  <img className=\"w-full dark-image\" src=\"/images/docs/performance-tracks/server-overview.dark.png\" alt=\"React 서버 성능 트랙\" />\n</div>\n\n#### 서버 요청 {/*server-requests*/}\n\n서버 요청 트랙은 최종적으로 React 서버 컴포넌트로 귀결되는 모든 Promise를 시각화합니다. 여기에는 `fetch` 호출이나 비동기 Node.js 파일 작업과 같은 모든 비동기(async) 작업이 포함됩니다.\n\nReact는 서드 파티 코드 내부에서 시작된 Promise를 **퍼스트 파티(1st party) 코드**를 차단하는 전체 작업의 지속 시간을 나타내는 단일 스팬으로 결합하려고 시도합니다.\n예를 들어, 내부적으로 `fetch`를 여러 번 호출하는 서드 파티 라이브러리 메서드 `getUser`는 여러 `fetch` 스팬을 표시하는 대신 `getUser`라는 단일 스팬으로 표현됩니다.\n\n스팬을 클릭하면 Promise가 생성된 위치의 스택 트레이스와 함께, 가능한 경우 **Promise가 해결(resolve)된 값**의 뷰가 표시됩니다.\n\n거부된(Rejected) Promise는 거부된 값과 함께 빨간색으로 표시됩니다.\n\n#### 서버 컴포넌트 {/*server-components*/}\n\n서버 컴포넌트 트랙은 React 서버 컴포넌트의 렌더링 지속 시간과 해당 컴포넌트가 **대기한(awaited) Promise**를 시각화합니다. 시간 정보는 플레임그래프 형태로 표시되며, 각 항목은 해당 컴포넌트와 모든 하위 컴포넌트의 렌더링 지속 시간을 나타냅니다.\n\nPromise를 await하면 React가 해당 Promise의 지속 시간을 **확인할 수 있습니다.** 모든 I/O 작업을 확인하려면 서버 요청 트랙을 사용하세요.\n\n렌더링 지속 시간에 따라 다른 색상으로 표시됩니다. 색이 어두울수록 지속 시간이 더 길다는 것을 의미합니다.\n\n서버 컴포넌트 트랙 그룹에는 항상 \"Primary\" 트랙이 포함됩니다. React가 서버 컴포넌트를 **동시(concurrently)에** 렌더링할 수 있는 경우 추가적인 \"Parallel\" 트랙이 표시됩니다.\n8개 이상의 서버 컴포넌트가 동시에 렌더링되면 React는 트랙을 계속 추가하는 대신 마지막 \"Parallel\" 트랙에 연결하여 표시합니다.\n"
  },
  {
    "path": "src/content/reference/eslint-plugin-react-hooks/index.md",
    "content": "---\ntitle: eslint-plugin-react-hooks\nversion: rc\n---\n\n<Intro>\n\n`eslint-plugin-react-hooks`는 [React의 규칙](/reference/rules)을 적용하기 위한 ESLint 규칙을 제공합니다.\n\n</Intro>\n\n이 플러그인은 빌드 시간에 React 규칙 위반을 감지하여 컴포넌트와 Hook이 정확성과 성능을 위한 React 규칙을 따르도록 도와줍니다. 린트는 기본적인 React 패턴(`exhaustive-deps` 및 `rules-of-hooks`)과 React 컴파일러가 표시하는 문제를 모두 다룹니다. React 컴파일러 진단은 ESLint 플러그인에 의해 자동으로 표시되며, 앱이 아직 컴파일러를 도입하지 않았더라도 사용할 수 있습니다.\n\n<Note>\n컴파일러가 진단을 보고하면 컴파일러가 지원되지 않거나 React 규칙을 위반하는 패턴을 정적으로 감지했다는 것을 의미합니다. 이를 감지하면 해당 컴포넌트와 Hook을 **자동으로** 건너뛰고 나머지 앱은 계속 컴파일합니다. 이렇게 하면 앱을 손상시키지 않는 안전한 최적화의 최적 적용 범위를 보장합니다.\n\n린트에서 이것이 의미하는 바는 모든 위반을 즉시 수정할 필요가 없다는 것입니다. 자신의 속도에 맞춰 해결하여 점진적으로 최적화된 컴포넌트 수를 늘리세요.\n</Note>\n\n## 권장<sup>Recommended</sup> 규칙 {/*recommended*/}\n\n아래 규칙들은 `eslint-plugin-react-hooks`의 `recommended` 프리셋에 포함되어 있습니다.\n\n* [`exhaustive-deps`](/reference/eslint-plugin-react-hooks/lints/exhaustive-deps) - React Hook의 의존성 배열에 필요한 모든 의존성이 포함되어 있는지 검증합니다.\n* [`rules-of-hooks`](/reference/eslint-plugin-react-hooks/lints/rules-of-hooks) - 컴포넌트와 Hook이 Hook의 규칙을 따르는지 검증합니다.\n* [`component-hook-factories`](/reference/eslint-plugin-react-hooks/lints/component-hook-factories) - 중첩된 컴포넌트나 Hook을 정의하는 고차 함수를 검증합니다.\n* [`config`](/reference/eslint-plugin-react-hooks/lints/config) - 컴파일러 설정 옵션을 검증합니다.\n* [`error-boundaries`](/reference/eslint-plugin-react-hooks/lints/error-boundaries) - 자식 오류에 대해 try/catch 대신 Error Boundary 사용을 검증합니다.\n* [`gating`](/reference/eslint-plugin-react-hooks/lints/gating) - 게이팅 모드 설정을 검증합니다.\n* [`globals`](/reference/eslint-plugin-react-hooks/lints/globals) - 렌더링 중 전역 변수의 할당/변이를 검증합니다.\n* [`immutability`](/reference/eslint-plugin-react-hooks/lints/immutability) - props, state 및 기타 불변 값의 변이를 검증합니다.\n* [`incompatible-library`](/reference/eslint-plugin-react-hooks/lints/incompatible-library) - 메모이제이션과 호환되지 않는 라이브러리 사용을 검증합니다.\n* [`preserve-manual-memoization`](/reference/eslint-plugin-react-hooks/lints/preserve-manual-memoization) - 기존의 수동 메모이제이션이 컴파일러에 의해 유지되는지 검증합니다.\n* [`purity`](/reference/eslint-plugin-react-hooks/lints/purity) - 알려진 불순 함수를 확인하여 컴포넌트/Hook이 순수한지 검증합니다.\n* [`refs`](/reference/eslint-plugin-react-hooks/lints/refs) - 렌더링 중 읽기/쓰기가 아닌 ref의 올바른 사용을 검증합니다.\n* [`set-state-in-effect`](/reference/eslint-plugin-react-hooks/lints/set-state-in-effect) - Effect에서 `setState`를 동기적으로 호출하는 것을 검증합니다.\n* [`set-state-in-render`](/reference/eslint-plugin-react-hooks/lints/set-state-in-render) - 렌더링 중 state 설정을 검증합니다.\n* [`static-components`](/reference/eslint-plugin-react-hooks/lints/static-components) - 컴포넌트가 매 렌더링마다 재생성되지 않고 정적인지 검증합니다.\n* [`unsupported-syntax`](/reference/eslint-plugin-react-hooks/lints/unsupported-syntax) - React 컴파일러가 지원하지 않는 문법을 검증합니다.\n* [`use-memo`](/reference/eslint-plugin-react-hooks/lints/use-memo) - 반환값 없이 `useMemo` Hook을 사용하는 것을 검증합니다.\n"
  },
  {
    "path": "src/content/reference/eslint-plugin-react-hooks/lints/component-hook-factories.md",
    "content": "---\ntitle: component-hook-factories\n---\n\n<Intro>\n\n중첩된 컴포넌트나 Hook을 정의하는 고차 함수를 검증합니다. 컴포넌트와 Hook은 모듈 레벨에서 정의해야 합니다.\n\n</Intro>\n\n## 규칙 세부 사항 {/*rule-details*/}\n\n다른 함수 내부에서 컴포넌트나 Hook을 정의하면 호출할 때마다 새로운 인스턴스가 생성됩니다. React는 각각을 완전히 다른 컴포넌트로 취급하여 전체 컴포넌트 트리를 파괴하고 다시 생성하며, 모든 state를 잃고 성능 문제를 일으킵니다.\n\n### 잘못된 예시 {/*invalid*/}\n\n이 규칙에 대한 잘못된 코드 예시입니다.\n\n```js\n// ❌ 컴포넌트를 생성하는 팩토리 함수\nfunction createComponent(defaultValue) {\n  return function Component() {\n    // ...\n  };\n}\n\n// ❌ 컴포넌트 내부에서 정의된 컴포넌트\nfunction Parent() {\n  function Child() {\n    // ...\n  }\n\n  return <Child />;\n}\n\n// ❌ Hook 팩토리 함수\nfunction createCustomHook(endpoint) {\n  return function useData() {\n    // ...\n  };\n}\n```\n\n### 올바른 예시 {/*valid*/}\n\n이 규칙에 대한 올바른 코드 예시입니다.\n\n```js\n// ✅ 모듈 레벨에서 정의된 컴포넌트\nfunction Component({ defaultValue }) {\n  // ...\n}\n\n// ✅ 모듈 레벨에서 정의된 커스텀 Hook\nfunction useData(endpoint) {\n  // ...\n}\n```\n\n## 문제 해결 {/*troubleshooting*/}\n\n### 동적 컴포넌트 동작이 필요한 경우 {/*dynamic-behavior*/}\n\n커스터마이즈된 컴포넌트를 만들기 위해 팩토리가 필요하다고 생각할 수 있습니다.\n\n```js\n// ❌ 잘못된 예: 팩토리 패턴\nfunction makeButton(color) {\n  return function Button({children}) {\n    return (\n      <button style={{backgroundColor: color}}>\n        {children}\n      </button>\n    );\n  };\n}\n\nconst RedButton = makeButton('red');\nconst BlueButton = makeButton('blue');\n```\n\n대신 [자식을 JSX로 전달](/learn/passing-props-to-a-component#passing-jsx-as-children)하세요.\n\n```js\n// ✅ 더 나은 방법: JSX를 자식으로 전달\nfunction Button({color, children}) {\n  return (\n    <button style={{backgroundColor: color}}>\n      {children}\n    </button>\n  );\n}\n\nfunction App() {\n  return (\n    <>\n      <Button color=\"red\">Red</Button>\n      <Button color=\"blue\">Blue</Button>\n    </>\n  );\n}\n```\n"
  },
  {
    "path": "src/content/reference/eslint-plugin-react-hooks/lints/config.md",
    "content": "---\ntitle: config\n---\n\n<Intro>\n\n컴파일러 [설정 옵션](/reference/react-compiler/configuration)을 검증합니다.\n\n</Intro>\n\n## 규칙 세부 사항 {/*rule-details*/}\n\nReact 컴파일러는 동작을 제어하기 위해 다양한 [설정 옵션](/reference/react-compiler/configuration)을 받습니다. 이 규칙은 설정이 올바른 옵션 이름과 값 타입을 사용하는지 검증하여 오타나 잘못된 설정으로 인한 무시되는 오류를 방지합니다.\n\n### 잘못된 예시 {/*invalid*/}\n\n이 규칙에 대한 잘못된 코드 예시입니다.\n\n```js\n// ❌ 알 수 없는 옵션 이름\nmodule.exports = {\n  plugins: [\n    ['babel-plugin-react-compiler', {\n      compileMode: 'all' // 오타: compilationMode여야 함\n    }]\n  ]\n};\n\n// ❌ 유효하지 않은 옵션 값\nmodule.exports = {\n  plugins: [\n    ['babel-plugin-react-compiler', {\n      compilationMode: 'everything' // 유효하지 않음: 'all' 또는 'infer'를 사용하세요\n    }]\n  ]\n};\n```\n\n### 올바른 예시 {/*valid*/}\n\n이 규칙에 대한 올바른 코드 예시입니다.\n\n```js\n// ✅ 유효한 컴파일러 설정\nmodule.exports = {\n  plugins: [\n    ['babel-plugin-react-compiler', {\n      compilationMode: 'infer',\n      panicThreshold: 'critical_errors'\n    }]\n  ]\n};\n```\n\n## 문제 해결 {/*troubleshooting*/}\n\n### 설정이 예상대로 작동하지 않는 경우 {/*config-not-working*/}\n\n컴파일러 설정에 오타나 잘못된 값이 있을 수 있습니다.\n\n```js\n// ❌ 잘못된 예: 일반적인 설정 실수\nmodule.exports = {\n  plugins: [\n    ['babel-plugin-react-compiler', {\n      // 옵션 이름 오타\n      compilationMod: 'all',\n      // 잘못된 값 타입\n      panicThreshold: true,\n      // 알 수 없는 옵션\n      optimizationLevel: 'max'\n    }]\n  ]\n};\n```\n\n유효한 옵션은 [설정 문서](/reference/react-compiler/configuration)를 확인하세요.\n\n```js\n// ✅ 더 나은 방법: 유효한 설정\nmodule.exports = {\n  plugins: [\n    ['babel-plugin-react-compiler', {\n      compilationMode: 'all', // 또는 'infer'\n      panicThreshold: 'none', // 또는 'critical_errors', 'all_errors'\n      // 문서화된 옵션만 사용하세요\n    }]\n  ]\n};\n```\n"
  },
  {
    "path": "src/content/reference/eslint-plugin-react-hooks/lints/error-boundaries.md",
    "content": "---\ntitle: error-boundaries\n---\n\n<Intro>\n\n자식 컴포넌트의 오류에 대해 `try`/`catch` 대신 Error Boundary 사용을 검증합니다.\n\n</Intro>\n\n## 규칙 세부 사항 {/*rule-details*/}\n\n`try`/`catch` 블록은 React의 렌더링 과정에서 발생하는 오류를 잡을 수 없습니다. 렌더링 메서드나 Hook에서 발생한 오류는 컴포넌트 트리를 타고 위로 전파됩니다. 오직 [Error Boundary](/reference/react/Component#catching-rendering-errors-with-an-error-boundary)만이 이러한 오류를 잡을 수 있습니다.\n\n### 잘못된 예시 {/*invalid*/}\n\n이 규칙에 대한 잘못된 코드 예시입니다.\n\n```js\n// ❌ `try`/`catch`는 렌더링 오류를 잡을 수 없음\nfunction Parent() {\n  try {\n    return <ChildComponent />; // 여기서 오류가 발생하면 catch가 도움이 되지 않음\n  } catch (error) {\n    return <div>Error occurred</div>;\n  }\n}\n```\n\n### 올바른 예시 {/*valid*/}\n\n이 규칙에 대한 올바른 코드 예시입니다.\n\n```js\n// ✅ Error Boundary 사용\nfunction Parent() {\n  return (\n    <ErrorBoundary>\n      <ChildComponent />\n    </ErrorBoundary>\n  );\n}\n```\n\n## 문제 해결 {/*troubleshooting*/}\n\n### 린터가 `use`를 `try`/`catch`로 감싸지 말라고 하는 이유는 무엇인가요? {/*why-is-the-linter-telling-me-not-to-wrap-use-in-trycatch*/}\n\n`use` Hook은 전통적인 의미에서 오류를 던지지 않고 컴포넌트 실행을 일시 중단합니다. `use`가 대기 중인 Promise를 만나면 컴포넌트를 일시 중단하고 React가 폴백을 표시하도록 합니다. Suspense와 Error Boundary만이 이러한 경우를 처리할 수 있습니다. 린터는 `catch` 블록이 절대 실행되지 않으므로 혼란을 방지하기 위해 `use` 주위의 `try`/`catch`에 대해 경고합니다.\n\n```js\n// ❌ `use` Hook 주위의 try/catch\nfunction Component({promise}) {\n  try {\n    const data = use(promise); // 잡을 수 없음 - `use`는 던지지 않고 일시 중단함\n    return <div>{data}</div>;\n  } catch (error) {\n    return <div>Failed to load</div>; // 도달 불가\n  }\n}\n\n// ✅ Error Boundary가 `use` 오류를 잡음\nfunction App() {\n  return (\n    <ErrorBoundary fallback={<div>Failed to load</div>}>\n      <Suspense fallback={<div>Loading...</div>}>\n        <DataComponent promise={fetchData()} />\n      </Suspense>\n    </ErrorBoundary>\n  );\n}\n\n```\n"
  },
  {
    "path": "src/content/reference/eslint-plugin-react-hooks/lints/exhaustive-deps.md",
    "content": "---\ntitle: exhaustive-deps\n---\n\n<Intro>\n\nReact Hook의 의존성 배열에 필요한 모든 의존성이 포함되어 있는지 검증합니다.\n\n</Intro>\n\n## 규칙 세부 사항 {/*rule-details*/}\n\n`useEffect`, `useMemo`, `useCallback`과 같은 React Hook은 의존성 배열을 받습니다. 이 Hook들 안에서 참조한 값이 의존성 배열에 포함되지 않으면, 그 값이 바뀌어도 React가 Effect를 다시 실행하거나 값을 다시 계산하지 않습니다. 그 결과 Hook이 예전 값을 계속 붙잡고 사용하는 오래된 클로저<sup>Stale Closure</sup> 문제가 생겨 최신 값이 아닌 상태로 동작하게 됩니다.\n\n## 일반적인 위반 사례 {/*common-violations*/}\n\n이 오류는 Effect 실행 시점을 조절하기 위해 의존성을 의도적으로 누락할 때 자주 발생합니다. Effect는 컴포넌트를 외부 시스템과 동기화하기 위한 용도여야 합니다. 의존성 배열은 Effect가 어떤 값을 사용하고 있는지 React에게 알려주며, React는 이를 바탕으로 언제 다시 동기화해야 하는지 판단합니다.\n\n린터와 싸우고 있는 자신을 발견한다면, 아마도 코드 구조를 다시 구성해야 할 가능성이 큽니다. 자세한 방법은 [Effect의 의존성 제거하기](/learn/removing-effect-dependencies) 문서를 참고하세요.\n\n### 잘못된 예시 {/*invalid*/}\n\n이 규칙에 대한 잘못된 코드 예시입니다.\n\n```js\n// ❌ Missing dependency\nuseEffect(() => {\n  console.log(count);\n}, []); // Missing 'count'\n\n// ❌ Missing prop\nuseEffect(() => {\n  fetchUser(userId);\n}, []); // Missing 'userId'\n\n// ❌ Incomplete dependencies\nuseMemo(() => {\n  return items.sort(sortOrder);\n}, [items]); // Missing 'sortOrder'\n```\n\n### 올바른 예시 {/*valid*/}\n\n이 규칙에 대한 올바른 예시입니다.\n\n```js\n// ✅ All dependencies included\nuseEffect(() => {\n  console.log(count);\n}, [count]);\n\n// ✅ All dependencies included\nuseEffect(() => {\n  fetchUser(userId);\n}, [userId]);\n```\n\n## 문제 해결 {/*troubleshooting*/}\n\n### 함수를 의존성으로 추가하면 무한 루프가 발생할 수 있습니다 {/*function-dependency-loops*/}\n\nEffect를 사용하고 있지만, 렌더링이 일어날 때마다 새로운 함수를 매번 생성하고 있습니다.\n\n```js\n// ❌ Causes infinite loop\nconst logItems = () => {\n  console.log(items);\n};\n\nuseEffect(() => {\n  logItems();\n}, [logItems]); // Infinite loop!\n```\n\n대부분의 경우 Effect는 필요하지 않습니다. 대신 그 동작이 실제로 발생하는 지점에서 함수를 호출하세요.\n\n```js\n// ✅ Call it from the event handler\nconst logItems = () => {\n  console.log(items);\n};\n\nreturn <button onClick={logItems}>Log</button>;\n\n// ✅ Or derive during render if there's no side effect\nitems.forEach(item => {\n  console.log(item);\n});\n```\n\n정말로 Effect가 필요한 경우(예: 외부 무언가를 구독해야 하는 상황)에는, 의존성이 안정적이도록 만드세요.\n\n```js\n// ✅ useCallback keeps the function reference stable\nconst logItems = useCallback(() => {\n  console.log(items);\n}, [items]);\n\nuseEffect(() => {\n  logItems();\n}, [logItems]);\n\n// ✅ Or move the logic straight into the effect\nuseEffect(() => {\n  console.log(items);\n}, [items]);\n```\n\n### Effect를 한 번만 실행하기 {/*effect-on-mount*/}\n\n마운트 시점에 Effect를 한 번만 실행하고 싶지만, 린터가 누락된 의존성에 대해 경고합니다.\n\n```js\n// ❌ Missing dependency\nuseEffect(() => {\n  sendAnalytics(userId);\n}, []); // Missing 'userId'\n```\n\n의존성을 포함하는 것을 권장하며, 정말로 한 번만 실행해야 한다면 Ref를 사용하세요.\n\n```js\n// ✅ Include dependency\nuseEffect(() => {\n  sendAnalytics(userId);\n}, [userId]);\n\n// ✅ Or use a ref guard inside an effect\nconst sent = useRef(false);\n\nuseEffect(() => {\n  if (sent.current) {\n    return;\n  }\n\n  sent.current = true;\n  sendAnalytics(userId);\n}, [userId]);\n```\n\n## 옵션 {/*options*/}\n\n공유 ESLint 설정을 사용해 커스텀 Effect Hook을 설정할 수 있습니다. (`eslint-plugin-react-hooks` 6.1.1 이상에서 지원.)\n\n```js\n{\n  \"settings\": {\n    \"react-hooks\": {\n      \"additionalEffectHooks\": \"(useMyEffect|useCustomEffect)\"\n    }\n  }\n}\n```\n\n- `additionalEffectHooks`: Exhaustive Deps 검사를 적용해야 하는 커스텀 Hook을 정규식 패턴으로 지정합니다. 이 설정은 모든 `react-hooks` 규칙에서 공통으로 사용됩니다.\n\n하위 호환성을 위해 이 규칙은 규칙 단위<sup>Rule Level</sup> 옵션도 함께 지원합니다.\n\n```js\n{\n  \"rules\": {\n    \"react-hooks/exhaustive-deps\": [\"warn\", {\n      \"additionalHooks\": \"(useMyCustomHook|useAnotherHook)\"\n    }]\n  }\n}\n```\n\n- `additionalHooks`: Exhaustive Deps 검사를 적용해야 하는 Hook을 정규식으로 지정합니다. **참고:** 이 규칙 단위 옵션을 지정하면 공유 `settings` 설정보다 우선 적용됩니다.\n"
  },
  {
    "path": "src/content/reference/eslint-plugin-react-hooks/lints/gating.md",
    "content": "---\ntitle: gating\n---\n\n<Intro>\n\n[게이팅 모드](/reference/react-compiler/gating)의 설정을 검증합니다.\n\n</Intro>\n\n## 규칙 세부 사항 {/*rule-details*/}\n\n게이팅 모드는 특정 컴포넌트를 최적화 대상으로 표시하여 React 컴파일러를 점진적으로 도입할 수 있게 해줍니다. 이 규칙은 컴파일러가 어떤 컴포넌트를 처리할지 알 수 있도록 게이팅 설정이 유효한지 확인합니다.\n\n### 잘못된 예시 {/*invalid*/}\n\n이 규칙에 대한 잘못된 코드 예시입니다.\n\n```js\n// ❌ 필수 필드 누락\nmodule.exports = {\n  plugins: [\n    ['babel-plugin-react-compiler', {\n      gating: {\n        importSpecifierName: '__experimental_useCompiler'\n        // 'source' 필드 누락\n      }\n    }]\n  ]\n};\n\n// ❌ 유효하지 않은 게이팅 타입\nmodule.exports = {\n  plugins: [\n    ['babel-plugin-react-compiler', {\n      gating: '__experimental_useCompiler' // 객체여야 함\n    }]\n  ]\n};\n```\n\n### 올바른 예시 {/*valid*/}\n\n이 규칙에 대한 올바른 코드 예시입니다.\n\n```js\n// ✅ 완전한 게이팅 설정\nmodule.exports = {\n  plugins: [\n    ['babel-plugin-react-compiler', {\n      gating: {\n        importSpecifierName: 'isCompilerEnabled', // 내보낸 함수 이름\n        source: 'featureFlags' // 모듈 이름\n      }\n    }]\n  ]\n};\n\n// featureFlags.js\nexport function isCompilerEnabled() {\n  // ...\n}\n\n// ✅ 게이팅 없음 (모든 것을 컴파일)\nmodule.exports = {\n  plugins: [\n    ['babel-plugin-react-compiler', {\n      // gating 필드 없음 - 모든 컴포넌트를 컴파일\n    }]\n  ]\n};\n```\n"
  },
  {
    "path": "src/content/reference/eslint-plugin-react-hooks/lints/globals.md",
    "content": "---\ntitle: globals\n---\n\n<Intro>\n\n렌더링 중 전역 변수의 할당/변이를 검증합니다. 이는 [사이드 이펙트는 렌더링 외부에서 실행되어야 한다는](/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render) 규칙을 보완합니다.\n\n</Intro>\n\n## 규칙 세부 사항 {/*rule-details*/}\n\n전역 변수는 React의 제어 범위 밖에 존재합니다. 렌더링 중에 전역 변수를 수정하면 렌더링이 순수하다는 React의 가정을 깨뜨립니다. 이로 인해 컴포넌트가 개발 환경과 프로덕션 환경에서 다르게 동작하거나, Fast Refresh가 중단되거나, React 컴파일러 같은 기능으로 앱을 최적화할 수 없게 됩니다.\n\n### 잘못된 예시 {/*invalid*/}\n\n이 규칙에 대한 잘못된 코드 예시입니다.\n\n```js\n// ❌ 전역 카운터\nlet renderCount = 0;\nfunction Component() {\n  renderCount++; // 전역 변수 변이\n  return <div>Count: {renderCount}</div>;\n}\n\n// ❌ window 프로퍼티 수정\nfunction Component({userId}) {\n  window.currentUser = userId; // 전역 변이\n  return <div>User: {userId}</div>;\n}\n\n// ❌ 전역 배열 push\nconst events = [];\nfunction Component({event}) {\n  events.push(event); // 전역 배열 변이\n  return <div>Events: {events.length}</div>;\n}\n\n// ❌ 캐시 조작\nconst cache = {};\nfunction Component({id}) {\n  if (!cache[id]) {\n    cache[id] = fetchData(id); // 렌더링 중 캐시 수정\n  }\n  return <div>{cache[id]}</div>;\n}\n```\n\n### 올바른 예시 {/*valid*/}\n\n이 규칙에 대한 올바른 코드 예시입니다.\n\n```js\n// ✅ 카운터에는 state 사용\nfunction Component() {\n  const [clickCount, setClickCount] = useState(0);\n\n  const handleClick = () => {\n    setClickCount(c => c + 1);\n  };\n\n  return (\n    <button onClick={handleClick}>\n      Clicked: {clickCount} times\n    </button>\n  );\n}\n\n// ✅ 전역 값에는 context 사용\nfunction Component() {\n  const user = useContext(UserContext);\n  return <div>User: {user.id}</div>;\n}\n\n// ✅ 외부 state를 React와 동기화\nfunction Component({title}) {\n  useEffect(() => {\n    document.title = title; // Effect 내에서는 OK\n  }, [title]);\n\n  return <div>Page: {title}</div>;\n}\n```\n"
  },
  {
    "path": "src/content/reference/eslint-plugin-react-hooks/lints/immutability.md",
    "content": "---\ntitle: immutability\n---\n\n<Intro>\n\n[불변인](/reference/rules/components-and-hooks-must-be-pure#props-and-state-are-immutable) props, state 및 기타 값을 변이하는 것을 검증합니다.\n\n</Intro>\n\n## 규칙 세부 사항 {/*rule-details*/}\n\n컴포넌트의 props와 state는 불변 스냅샷입니다. 절대 직접 변이하지 마세요. 대신 새로운 props를 전달하고, `useState`의 setter 함수를 사용하세요.\n\n## 일반적인 위반 사례 {/*common-violations*/}\n\n### 잘못된 예시 {/*invalid*/}\n\n```js\n// ❌ 배열 push 변이\nfunction Component() {\n  const [items, setItems] = useState([1, 2, 3]);\n\n  const addItem = () => {\n    items.push(4); // 변이!\n    setItems(items); // 같은 참조, 리렌더링 안 됨\n  };\n}\n\n// ❌ 객체 프로퍼티 할당\nfunction Component() {\n  const [user, setUser] = useState({name: 'Alice'});\n\n  const updateName = () => {\n    user.name = 'Bob'; // 변이!\n    setUser(user); // 같은 참조\n  };\n}\n\n// ❌ 스프레드 없이 정렬\nfunction Component() {\n  const [items, setItems] = useState([3, 1, 2]);\n\n  const sortItems = () => {\n    setItems(items.sort()); // sort는 변이함!\n  };\n}\n```\n\n### 올바른 예시 {/*valid*/}\n\n```js\n// ✅ 새 배열 생성\nfunction Component() {\n  const [items, setItems] = useState([1, 2, 3]);\n\n  const addItem = () => {\n    setItems([...items, 4]); // 새 배열\n  };\n}\n\n// ✅ 새 객체 생성\nfunction Component() {\n  const [user, setUser] = useState({name: 'Alice'});\n\n  const updateName = () => {\n    setUser({...user, name: 'Bob'}); // 새 객체\n  };\n}\n```\n\n## 문제 해결 {/*troubleshooting*/}\n\n### 배열에 항목을 추가해야 하는 경우 {/*add-items-array*/}\n\n`push()` 같은 메서드로 배열을 변이하면 리렌더링이 트리거되지 않습니다.\n\n```js\n// ❌ 잘못된 예: 배열 변이\nfunction TodoList() {\n  const [todos, setTodos] = useState([]);\n\n  const addTodo = (id, text) => {\n    todos.push({id, text});\n    setTodos(todos); // 같은 배열 참조!\n  };\n\n  return (\n    <ul>\n      {todos.map(todo => <li key={todo.id}>{todo.text}</li>)}\n    </ul>\n  );\n}\n```\n\n대신 새 배열을 생성하세요.\n\n```js\n// ✅ 더 나은 방법: 새 배열 생성\nfunction TodoList() {\n  const [todos, setTodos] = useState([]);\n\n  const addTodo = (id, text) => {\n    setTodos([...todos, {id, text}]);\n    // 또는: setTodos(todos => [...todos, {id: Date.now(), text}])\n  };\n\n  return (\n    <ul>\n      {todos.map(todo => <li key={todo.id}>{todo.text}</li>)}\n    </ul>\n  );\n}\n```\n\n### 중첩된 객체를 업데이트해야 하는 경우 {/*update-nested-objects*/}\n\n중첩된 프로퍼티를 변이하면 리렌더링이 트리거되지 않습니다.\n\n```js\n// ❌ 잘못된 예: 중첩된 객체 변이\nfunction UserProfile() {\n  const [user, setUser] = useState({\n    name: 'Alice',\n    settings: {\n      theme: 'light',\n      notifications: true\n    }\n  });\n\n  const toggleTheme = () => {\n    user.settings.theme = 'dark'; // 변이!\n    setUser(user); // 같은 객체 참조\n  };\n}\n```\n\n업데이트가 필요한 각 레벨에서 스프레드하세요.\n\n```js\n// ✅ 더 나은 방법: 각 레벨에서 새 객체 생성\nfunction UserProfile() {\n  const [user, setUser] = useState({\n    name: 'Alice',\n    settings: {\n      theme: 'light',\n      notifications: true\n    }\n  });\n\n  const toggleTheme = () => {\n    setUser({\n      ...user,\n      settings: {\n        ...user.settings,\n        theme: 'dark'\n      }\n    });\n  };\n}\n\n```\n"
  },
  {
    "path": "src/content/reference/eslint-plugin-react-hooks/lints/incompatible-library.md",
    "content": "---\ntitle: incompatible-library\n---\n\n<Intro>\n\n메모이제이션(수동 또는 자동)과 호환되지 않는 라이브러리 사용에 대해 검증합니다.\n\n</Intro>\n\n<Note>\n\n이러한 라이브러리는 React의 메모이제이션 규칙이 완전히 문서화되기 전에 설계되었습니다. 당시에는 앱 상태가 변경될 때 컴포넌트가 적절한 반응성을 유지하도록 인체공학적인 방법을 최적화하기 위해 올바른 선택을 했습니다. 이러한 레거시 패턴은 작동했지만, 이후 React의 프로그래밍 모델과 호환되지 않는다는 것을 발견했습니다. React 규칙을 따르는 패턴을 사용하도록 이러한 라이브러리를 마이그레이션하기 위해 라이브러리 작성자와 계속 협력하고 있습니다.\n\n</Note>\n\n## 규칙 세부 사항 {/*rule-details*/}\n\n일부 라이브러리는 React에서 지원하지 않는 패턴을 사용합니다. 린터가 [알려진 목록](https://github.com/facebook/react/blob/main/compiler/packages/babel-plugin-react-compiler/src/HIR/DefaultModuleTypeProvider.ts)에서 이러한 API의 사용을 감지하면 이 규칙에 따라 플래그를 지정합니다. 이는 React 컴파일러가 앱을 손상시키지 않기 위해 이러한 호환되지 않는 API를 사용하는 컴포넌트를 자동으로 건너뛸 수 있음을 의미합니다.\n\n```js\n// 이러한 라이브러리로 메모이제이션이 깨지는 예시\nfunction Form() {\n  const { watch } = useForm();\n\n  // ❌ 'name' 필드가 변경되어도 이 값은 절대 업데이트되지 않습니다\n  const name = useMemo(() => watch('name'), [watch]);\n\n  return <div>Name: {name}</div>; // UI가 \"얼어붙은\" 것처럼 보입니다\n}\n```\n\nReact 컴파일러는 React 규칙을 따라 값을 자동으로 메모이제이션합니다. 수동 `useMemo`로 문제가 발생하면 컴파일러의 자동 최적화도 깨집니다. 이 규칙은 이러한 문제가 있는 패턴을 식별하는 데 도움이 됩니다.\n\n<DeepDive>\n\n#### React 규칙을 따르는 API 설계하기 {/*designing-apis-that-follow-the-rules-of-react*/}\n\n라이브러리 API나 Hook을 설계할 때 고려해야 할 질문 중 하나는 API 호출을 `useMemo`로 안전하게 메모이제이션할 수 있는지 여부입니다. 그렇지 않다면 수동 메모이제이션과 React 컴파일러 메모이제이션 모두 사용자의 코드를 손상시킬 것입니다.\n\n예를 들어, 이러한 호환되지 않는 패턴 중 하나는 \"내부 가변성\"입니다. 내부 가변성은 객체나 함수가 참조는 동일하게 유지되지만 시간이 지남에 따라 변경되는 자체 숨겨진 상태를 유지하는 것을 말합니다. 외부에서는 동일해 보이지만 내용물을 은밀하게 재배치하는 상자라고 생각하면 됩니다. React는 다른 상자를 받았는지만 확인하고 안에 무엇이 들어 있는지는 확인하지 않기 때문에 변경 사항을 알 수 없습니다. 이는 메모이제이션을 깨뜨리는데, React는 값의 일부가 변경된 경우 외부 객체(또는 함수)가 변경되는 것에 의존하기 때문입니다.\n\nReact API를 설계할 때의 경험 법칙으로, `useMemo`가 이를 깨뜨릴지 생각해보세요.\n\n```js\nfunction Component() {\n  const { someFunction } = useLibrary();\n  // 이와 같은 함수를 메모이제이션하는 것은 항상 안전해야 합니다\n  const result = useMemo(() => someFunction(), [someFunction]);\n}\n```\n\n대신, 불변 상태를 반환하고 명시적인 업데이트 함수를 사용하는 API를 설계하세요.\n\n```js\n// ✅ 좋은 예시: 업데이트될 때 참조가 변경되는 불변 상태를 반환\nfunction Component() {\n  const { field, updateField } = useLibrary();\n  // 이것은 항상 메모이제이션하기에 안전합니다\n  const greeting = useMemo(() => `Hello, ${field.name}!`, [field.name]);\n\n  return (\n    <div>\n      <input\n        value={field.name}\n        onChange={(e) => updateField('name', e.target.value)}\n      />\n      <p>{greeting}</p>\n    </div>\n  );\n}\n```\n\n</DeepDive>\n\n### 잘못된 예시 {/*invalid*/}\n\n이 규칙에 대한 잘못된 코드 예시입니다.\n\n```js\n// ❌ react-hook-form `watch`\nfunction Component() {\n  const {watch} = useForm();\n  const value = watch('field'); // 내부 가변성\n  return <div>{value}</div>;\n}\n\n// ❌ TanStack Table `useReactTable`\nfunction Component({data}) {\n  const table = useReactTable({\n    data,\n    columns,\n    getCoreRowModel: getCoreRowModel(),\n  });\n  // table 인스턴스가 내부 가변성을 사용합니다\n  return <Table table={table} />;\n}\n```\n\n<Pitfall>\n\n#### MobX {/*mobx*/}\n\n`observer`와 같은 MobX 패턴도 메모이제이션 가정을 깨뜨리지만, 린터는 아직 이를 감지하지 못합니다. MobX에 의존하고 있고 React 컴파일러에서 앱이 작동하지 않는다면 `\"use no memo\"` 지시어를 사용해야 할 수 있습니다.\n\n```js\n// ❌ MobX `observer`\nconst Component = observer(() => {\n  const [timer] = useState(() => new Timer());\n  return <span>경과된 시간: {timer.secondsPassed}</span>;\n});\n```\n\n</Pitfall>\n\n### 올바른 예시 {/*valid*/}\n\n이 규칙에 대한 올바른 코드 예시입니다.\n\n```js\n// ✅ react-hook-form의 경우 `useWatch`를 사용하세요\nfunction Component() {\n  const {register, control} = useForm();\n  const watchedValue = useWatch({\n    control,\n    name: 'field'\n  });\n\n  return (\n    <>\n      <input {...register('field')} />\n      <div>현재 값: {watchedValue}</div>\n    </>\n  );\n}\n```\n\n일부 다른 라이브러리는 아직 React의 메모이제이션 모델과 호환되는 대체 API가 없습니다. 린터가 이러한 API를 호출하는 컴포넌트나 Hook을 자동으로 건너뛰지 않는다면 [이슈를 제출](https://github.com/facebook/react/issues)하여 린터에 추가할 수 있도록 해주세요.\n"
  },
  {
    "path": "src/content/reference/eslint-plugin-react-hooks/lints/preserve-manual-memoization.md",
    "content": "---\ntitle: preserve-manual-memoization\n---\n\n<Intro>\n\n컴파일러가 기존 수동 메모이제이션을 보존하는지 검증합니다. React 컴파일러는 추론이 [기존 수동 메모이제이션과 일치하거나 이를 초과하는 경우](/learn/react-compiler/introduction#what-should-i-do-about-usememo-usecallback-and-reactmemo)에만 컴포넌트와 Hook을 컴파일합니다.\n\n</Intro>\n\n## 규칙 세부 사항 {/*rule-details*/}\n\nReact 컴파일러는 기존의 `useMemo`, `useCallback` 및 `React.memo` 호출을 보존합니다. 수동으로 메모이제이션한 경우 컴파일러는 타당한 이유가 있다고 가정하고 제거하지 않습니다. 그러나 불완전한 의존성은 컴파일러가 코드의 데이터 흐름을 이해하고 추가 최적화를 적용하는 것을 방해합니다.\n\n### 잘못된 예시 {/*invalid*/}\n\n이 규칙에 대한 잘못된 코드 예시입니다.\n\n```js\n// ❌ useMemo에 의존성 누락\nfunction Component({ data, filter }) {\n  const filtered = useMemo(\n    () => data.filter(filter),\n    [data] // 'filter' 의존성 누락\n  );\n\n  return <List items={filtered} />;\n}\n\n// ❌ useCallback에 의존성 누락\nfunction Component({ onUpdate, value }) {\n  const handleClick = useCallback(() => {\n    onUpdate(value);\n  }, [onUpdate]); // 'value' 누락\n\n  return <button onClick={handleClick}>Update</button>;\n}\n```\n\n### 올바른 예시 {/*valid*/}\n\n이 규칙에 대한 올바른 코드 예시입니다.\n\n```js\n// ✅ 완전한 의존성\nfunction Component({ data, filter }) {\n  const filtered = useMemo(\n    () => data.filter(filter),\n    [data, filter] // 모든 의존성 포함\n  );\n\n  return <List items={filtered} />;\n}\n\n// ✅ 또는 컴파일러가 처리하도록 함\nfunction Component({ data, filter }) {\n  // 수동 메모이제이션 불필요\n  const filtered = data.filter(filter);\n  return <List items={filtered} />;\n}\n```\n\n## 문제 해결 {/*troubleshooting*/}\n\n### 수동 메모이제이션을 제거해야 하나요? {/*remove-manual-memoization*/}\n\nReact 컴파일러가 수동 메모이제이션을 불필요하게 만드는지 궁금할 수 있습니다.\n\n```js\n// 이게 여전히 필요한가요?\nfunction Component({items, sortBy}) {\n  const sorted = useMemo(() => {\n    return [...items].sort((a, b) => {\n      return a[sortBy] - b[sortBy];\n    });\n  }, [items, sortBy]);\n\n  return <List items={sorted} />;\n}\n```\n\nReact 컴파일러를 사용하는 경우 안전하게 제거할 수 있습니다.\n\n```js\n// ✅ 더 나은 방법: 컴파일러가 최적화하도록 함\nfunction Component({items, sortBy}) {\n  const sorted = [...items].sort((a, b) => {\n    return a[sortBy] - b[sortBy];\n  });\n\n  return <List items={sorted} />;\n}\n```\n"
  },
  {
    "path": "src/content/reference/eslint-plugin-react-hooks/lints/purity.md",
    "content": "---\ntitle: purity\n---\n\n<Intro>\n\n알려진 순수하지 않은 함수를 호출하지 않는지 확인하여 [컴포넌트와 Hook이 순수한지](/reference/rules/components-and-hooks-must-be-pure) 검증합니다.\n\n</Intro>\n\n## 규칙 세부 사항 {/*rule-details*/}\n\nReact 컴포넌트는 순수 함수여야 합니다. 동일한 props가 주어지면 항상 동일한 JSX를 반환해야 합니다. 컴포넌트가 렌더링 중에 `Math.random()`이나 `Date.now()`와 같은 함수를 사용하면 매번 다른 출력을 생성하여 React의 가정을 깨뜨리고 하이드레이션 불일치, 잘못된 메모이제이션, 예측할 수 없는 동작과 같은 버그를 발생시킵니다.\n\n## 일반적인 위반 사례 {/*common-violations*/}\n\n일반적으로 동일한 입력에 대해 다른 값을 반환하는 API는 이 규칙을 위반합니다. 일반적인 예시는 다음과 같습니다.\n\n- `Math.random()`\n- `Date.now()` / `new Date()`\n- `crypto.randomUUID()`\n- `performance.now()`\n\n### 잘못된 예시 {/*invalid*/}\n\n이 규칙에 대한 잘못된 코드 예시입니다.\n\n```js\n// ❌ 렌더링 중 Math.random() 사용\nfunction Component() {\n  const id = Math.random(); // 렌더링할 때마다 다름\n  return <div key={id}>Content</div>;\n}\n\n// ❌ 값으로 Date.now() 사용\nfunction Component() {\n  const timestamp = Date.now(); // 렌더링할 때마다 변경됨\n  return <div>생성 시각: {timestamp}</div>;\n}\n```\n\n### 올바른 예시 {/*valid*/}\n\n이 규칙에 대한 올바른 코드 예시입니다.\n\n```js\n// ✅ 초기 상태에서 안정적인 ID 생성\nfunction Component() {\n  const [id] = useState(() => crypto.randomUUID());\n  return <div key={id}>Content</div>;\n}\n```\n\n## 문제 해결 {/*troubleshooting*/}\n\n### 현재 시간을 표시해야 합니다 {/*current-time*/}\n\n렌더링 중에 `Date.now()`를 호출하면 컴포넌트가 순수하지 않게 됩니다.\n\n```js\n// ❌ 잘못된 예시: 렌더링할 때마다 시간이 변경됨\nfunction Clock() {\n  return <div>현재 시각: {Date.now()}</div>;\n}\n```\n\n대신 [순수하지 않은 함수를 렌더링 외부로 이동하세요](/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent).\n\n```js\nfunction Clock() {\n  const [time, setTime] = useState(() => Date.now());\n\n  useEffect(() => {\n    const interval = setInterval(() => {\n      setTime(Date.now());\n    }, 1000);\n\n    return () => clearInterval(interval);\n  }, []);\n\n  return <div>현재 시각: {time}</div>;\n}\n```\n"
  },
  {
    "path": "src/content/reference/eslint-plugin-react-hooks/lints/refs.md",
    "content": "---\ntitle: refs\n---\n\n<Intro>\n\n렌더링 중에 읽기/쓰기를 하지 않는 ref의 올바른 사용법을 검증합니다. [`useRef()` 사용법](/reference/react/useRef#usage)의 \"주의하세요!\" 섹션을 참고하세요.\n\n</Intro>\n\n## 규칙 세부 사항 {/*rule-details*/}\n\nRef는 렌더링에 사용되지 않는 값을 보유합니다. State와 달리 ref를 변경해도 재렌더링이 트리거되지 않습니다. 렌더링 중에 `ref.current`를 읽거나 쓰는 것은 React의 예상을 깨뜨립니다. Ref는 읽으려고 할 때 초기화되지 않았을 수 있으며, 그 값은 오래되었거나 일관되지 않을 수 있습니다.\n\n## Ref 감지 방법 {/*how-it-detects-refs*/}\n\n린트는 ref로 알고 있는 값에만 이러한 규칙을 적용합니다. 값은 컴파일러가 다음 패턴 중 하나를 발견하면 ref로 추론됩니다.\n\n- `useRef()` 또는 `React.createRef()`에서 반환된 값\n\n  ```js\n  const scrollRef = useRef(null);\n  ```\n\n- `ref`로 명명되거나 `Ref`로 끝나는 식별자가 `.current`를 읽거나 쓰는 경우\n\n  ```js\n  buttonRef.current = node;\n  ```\n\n- JSX `ref` prop을 통해 전달된 경우 (예: `<div ref={someRef} />`)\n\n  ```jsx\n  <input ref={inputRef} />\n  ```\n\n무언가가 ref로 표시되면 그 추론은 할당, 구조 분해 또는 헬퍼 호출을 통해 값을 따라갑니다. 이를 통해 ref가 인수로 전달된 다른 함수 내부에서 `ref.current`에 액세스하는 경우에도 린트가 위반 사항을 찾아낼 수 있습니다.\n\n## 일반적인 위반 사례 {/*common-violations*/}\n\n- 렌더링 중에 `ref.current` 읽기\n- 렌더링 중에 `refs` 업데이트\n- State여야 하는 값에 `refs` 사용\n\n### 잘못된 예시 {/*invalid*/}\n\n이 규칙에 대한 잘못된 코드 예시입니다.\n\n```js\n// ❌ 렌더링 중에 ref 읽기\nfunction Component() {\n  const ref = useRef(0);\n  const value = ref.current; // 렌더링 중에 읽지 마세요\n  return <div>{value}</div>;\n}\n\n// ❌ 렌더링 중에 ref 수정\nfunction Component({value}) {\n  const ref = useRef(null);\n  ref.current = value; // 렌더링 중에 수정하지 마세요\n  return <div />;\n}\n```\n\n### 올바른 예시 {/*valid*/}\n\n이 규칙에 대한 올바른 코드 예시입니다.\n\n```js\n// ✅ Effect/핸들러에서 ref 읽기\nfunction Component() {\n  const ref = useRef(null);\n\n  useEffect(() => {\n    if (ref.current) {\n      console.log(ref.current.offsetWidth); // Effect에서는 OK\n    }\n  });\n\n  return <div ref={ref} />;\n}\n\n// ✅ UI 값에는 state 사용\nfunction Component() {\n  const [count, setCount] = useState(0);\n\n  return (\n    <button onClick={() => setCount(count + 1)}>\n      {count}\n    </button>\n  );\n}\n\n// ✅ ref 값의 지연 초기화\nfunction Component() {\n  const ref = useRef(null);\n\n  // 첫 사용 시 한 번만 초기화\n  if (ref.current === null) {\n    ref.current = expensiveComputation(); // OK - 지연 초기화\n  }\n\n  const handleClick = () => {\n    console.log(ref.current); // 초기화된 값 사용\n  };\n\n  return <button onClick={handleClick}>Click</button>;\n}\n```\n\n## 문제 해결 {/*troubleshooting*/}\n\n### 린트가 `.current`가 있는 일반 객체를 플래그 지정했습니다 {/*plain-object-current*/}\n\n이름 휴리스틱은 의도적으로 `ref.current`와 `fooRef.current`를 실제 ref로 취급합니다. 커스텀 컨테이너 객체를 모델링하는 경우 다른 이름(예: `box`)을 선택하거나 가변 값을 state로 이동하세요. 이름을 변경하면 컴파일러가 ref로 추론하지 않기 때문에 린트를 피할 수 있습니다.\n"
  },
  {
    "path": "src/content/reference/eslint-plugin-react-hooks/lints/rules-of-hooks.md",
    "content": "---\ntitle: rules-of-hooks\n---\n\n<Intro>\n\n컴포넌트와 Hook이 [Hook의 규칙](/reference/rules/rules-of-hooks)을 따르는지 검증합니다.\n\n</Intro>\n\n## 규칙 세부 사항 {/*rule-details*/}\n\nReact는 Hook이 호출되는 순서에 의존하여 렌더링 간에 state를 올바르게 보존합니다. 컴포넌트가 렌더링될 때마다 React는 정확히 같은 Hook이 정확히 같은 순서로 호출되기를 기대합니다. Hook이 조건부로 호출되거나 루프에서 호출되면 React는 어떤 state가 어떤 Hook 호출에 해당하는지 추적할 수 없게 되어 state 불일치와 \"Rendered fewer/more hooks than expected\" 오류 같은 버그가 발생합니다.\n\n## 일반적인 위반 사례 {/*common-violations*/}\n\n다음 패턴들은 Hook의 규칙을 위반합니다.\n\n- **조건문의 Hook** (`if`/`else`, 삼항 연산자, `&&`/`||`)\n- **루프의 Hook** (`for`, `while`, `do-while`)\n- **조기 return 이후의 Hook**\n- **콜백/이벤트 핸들러의 Hook**\n- **async 함수의 Hook**\n- **클래스 메서드의 Hook**\n- **모듈 레벨의 Hook**\n\n<Note>\n\n### `use` Hook {/*use-hook*/}\n\n`use` Hook은 다른 React Hook과 다릅니다. 조건부로 호출하거나 루프에서 호출할 수 있습니다.\n\n```js\n// ✅ `use`는 조건문에서 호출 가능\nif (shouldFetch) {\n  const data = use(fetchPromise);\n}\n\n// ✅ `use`는 루프에서 호출 가능\nfor (const promise of promises) {\n  results.push(use(promise));\n}\n```\n\n하지만 `use`에는 여전히 제약이 있습니다.\n- `try`/`catch`로 감쌀 수 없습니다.\n- 컴포넌트나 Hook 내부에서 호출해야 합니다.\n\n더 알아보기: [`use` API 레퍼런스](/reference/react/use)\n\n</Note>\n\n### 잘못된 예시 {/*invalid*/}\n\n이 규칙에 대한 잘못된 코드 예시입니다.\n\n```js\n// ❌ 조건문의 Hook\nif (isLoggedIn) {\n  const [user, setUser] = useState(null);\n}\n\n// ❌ 조기 return 이후의 Hook\nif (!data) return <Loading />;\nconst [processed, setProcessed] = useState(data);\n\n// ❌ 콜백의 Hook\n<button onClick={() => {\n  const [clicked, setClicked] = useState(false);\n}}/>\n\n// ❌ try/catch의 `use`\ntry {\n  const data = use(promise);\n} catch (e) {\n  // 오류 처리\n}\n\n// ❌ 모듈 레벨의 Hook\nconst globalState = useState(0); // 컴포넌트 외부\n```\n\n### 올바른 예시 {/*valid*/}\n\n이 규칙에 대한 올바른 코드 예시입니다.\n\n```js\nfunction Component({ isSpecial, shouldFetch, fetchPromise }) {\n  // ✅ 최상위 레벨의 Hook\n  const [count, setCount] = useState(0);\n  const [name, setName] = useState('');\n\n  if (!isSpecial) {\n    return null;\n  }\n\n  if (shouldFetch) {\n    // ✅ `use`는 조건문에서 호출 가능\n    const data = use(fetchPromise);\n    return <div>{data}</div>;\n  }\n\n  return <div>{name}: {count}</div>;\n}\n```\n\n## 문제 해결 {/*troubleshooting*/}\n\n### 조건에 따라 데이터를 가져오고 싶은 경우 {/*conditional-data-fetching*/}\n\n`useEffect`를 조건부로 호출하려고 합니다.\n\n```js\n// ❌ 조건부 Hook\nif (isLoggedIn) {\n  useEffect(() => {\n    fetchUserData();\n  }, []);\n}\n```\n\nHook을 무조건 호출하고 내부에서 조건을 확인하세요.\n\n```js\n// ✅ Hook 내부의 조건\nuseEffect(() => {\n  if (isLoggedIn) {\n    fetchUserData();\n  }\n}, [isLoggedIn]);\n```\n\n<Note>\n\n`useEffect`에서 데이터를 가져오는<sup>Fetch</sup> 것보다 더 나은 방법이 있습니다. 데이터 가져오기에는 React Query, useSWR 또는 React Router 6.4+를 사용하는 것을 고려하세요. 이러한 솔루션은 요청 중복 제거, 응답 캐싱, 네트워크 워터폴 방지를 처리합니다.\n\n더 알아보기: [데이터 가져오기](/learn/synchronizing-with-effects#fetching-data)\n\n</Note>\n\n### 다른 시나리오에 따라 다른 state가 필요한 경우 {/*conditional-state-initialization*/}\n\nstate를 조건부로 초기화하려고 합니다.\n\n```js\n// ❌ 조건부 state\nif (userType === 'admin') {\n  const [permissions, setPermissions] = useState(adminPerms);\n} else {\n  const [permissions, setPermissions] = useState(userPerms);\n}\n```\n\n항상 `useState`를 호출하고 초기값을 조건부로 설정하세요.\n\n```js\n// ✅ 조건부 초기값\nconst [permissions, setPermissions] = useState(\n  userType === 'admin' ? adminPerms : userPerms\n);\n```\n\n## 옵션 {/*options*/}\n\n공유 ESLint 설정을 사용하여 커스텀 Effect Hook을 설정할 수 있습니다 (`eslint-plugin-react-hooks` 6.1.1 이상에서 사용 가능).\n\n```js\n{\n  \"settings\": {\n    \"react-hooks\": {\n      \"additionalEffectHooks\": \"(useMyEffect|useCustomEffect)\"\n    }\n  }\n}\n```\n\n- `additionalEffectHooks`: Effect로 취급되어야 하는 커스텀 Hook을 일치시키는 정규식 패턴입니다. 이를 통해 `useEffectEvent` 및 유사한 이벤트 함수를 커스텀 Effect Hook에서 호출할 수 있습니다.\n\n이 공유 설정은 `rules-of-hooks`와 `exhaustive-deps` 규칙 모두에서 사용되어 모든 Hook 관련 린트에서 일관된 동작을 보장합니다.\n"
  },
  {
    "path": "src/content/reference/eslint-plugin-react-hooks/lints/set-state-in-effect.md",
    "content": "---\ntitle: set-state-in-effect\n---\n\n<Intro>\n\nEffect에서 `setState`를 동기적으로 호출하는 것에 대해 검증합니다. 이는 성능을 저하시키는 재렌더링으로 이어질 수 있습니다.\n\n</Intro>\n\n## 규칙 세부 사항 {/*rule-details*/}\n\nEffect 내부에서 즉시 state를 설정하면 React가 전체 렌더링 사이클을 다시 시작해야 합니다. Effect에서 state를 업데이트하면 React는 컴포넌트를 다시 렌더링하고, DOM에 변경 사항을 적용한 다음, Effect를 다시 실행해야 합니다. 이는 렌더링 중에 직접 데이터를 변환하거나 props에서 state를 파생시켜 피할 수 있었던 추가 렌더링 패스를 생성합니다. 대신 컴포넌트의 최상위 레벨에서 데이터를 변환하세요. 이 코드는 추가 렌더링 사이클을 트리거하지 않고 props나 state가 변경될 때 자연스럽게 다시 실행됩니다.\n\nEffect에서 동기적으로 `setState`를 호출하면 브라우저가 페인트하기 전에 즉시 재렌더링이 트리거되어 성능 문제와 시각적 끊김이 발생합니다. React는 두 번 렌더링해야 합니다. 한 번은 state 업데이트를 적용하고, 또 한 번은 Effect가 실행된 후입니다. 단일 렌더링으로 동일한 결과를 얻을 수 있을 때 이러한 이중 렌더링은 낭비입니다.\n\n많은 경우 Effect가 전혀 필요하지 않을 수도 있습니다. 자세한 내용은 [Effect가 필요하지 않은 경우](/learn/you-might-not-need-an-effect)를 참고하세요.\n\n## 일반적인 위반 사례 {/*common-violations*/}\n\n이 규칙은 동기적으로 `setState`가 불필요하게 사용되는 여러 패턴을 감지합니다.\n\n- 로딩 상태를 동기적으로 설정\n- Effect에서 props로부터 state 파생\n- 렌더링 대신 Effect에서 데이터 변환\n\n### 잘못된 예시 {/*invalid*/}\n\n이 규칙에 대한 잘못된 코드 예시입니다.\n\n```js\n// ❌ Effect에서 동기적으로 setState\nfunction Component({data}) {\n  const [items, setItems] = useState([]);\n\n  useEffect(() => {\n    setItems(data); // 추가 렌더링, 대신 초기 상태를 사용하세요\n  }, [data]);\n}\n\n// ❌ 로딩 상태를 동기적으로 설정\nfunction Component() {\n  const [loading, setLoading] = useState(false);\n\n  useEffect(() => {\n    setLoading(true); // 동기적, 추가 렌더링 발생\n    fetchData().then(() => setLoading(false));\n  }, []);\n}\n\n// ❌ Effect에서 데이터 변환\nfunction Component({rawData}) {\n  const [processed, setProcessed] = useState([]);\n\n  useEffect(() => {\n    setProcessed(rawData.map(transform)); // 렌더링 중에 생성해야 함\n  }, [rawData]);\n}\n\n// ❌ props로부터 state 파생\nfunction Component({selectedId, items}) {\n  const [selected, setSelected] = useState(null);\n\n  useEffect(() => {\n    setSelected(items.find(i => i.id === selectedId));\n  }, [selectedId, items]);\n}\n```\n\n### 올바른 예시 {/*valid*/}\n\n이 규칙에 대한 올바른 코드 예시입니다.\n\n```js\n// ✅ 값이 ref에서 오는 경우 Effect에서 setState는 괜찮습니다\nfunction Tooltip() {\n  const ref = useRef(null);\n  const [tooltipHeight, setTooltipHeight] = useState(0);\n\n  useLayoutEffect(() => {\n    const { height } = ref.current.getBoundingClientRect();\n    setTooltipHeight(height);\n  }, []);\n}\n\n// ✅ 렌더링 중에 계산\nfunction Component({selectedId, items}) {\n  const selected = items.find(i => i.id === selectedId);\n  return <div>{selected?.name}</div>;\n}\n```\n\n**기존 props나 state로부터 계산할 수 있는 경우 state에 넣지 마세요.** 대신 렌더링 중에 계산하세요. 이렇게 하면 코드가 더 빠르고 간단하며 오류가 덜 발생합니다. 자세한 내용은 [Effect가 필요하지 않은 경우](/learn/you-might-not-need-an-effect)를 참고하세요.\n"
  },
  {
    "path": "src/content/reference/eslint-plugin-react-hooks/lints/set-state-in-render.md",
    "content": "---\ntitle: set-state-in-render\n---\n\n<Intro>\n\n렌더링 중에 무조건 state를 설정하는 것에 대해 검증합니다. 이는 추가 렌더링과 잠재적인 무한 렌더링 루프를 트리거할 수 있습니다.\n\n</Intro>\n\n## 규칙 세부 사항 {/*rule-details*/}\n\n렌더링 중에 무조건 `setState`를 호출하면 현재 렌더링이 완료되기 전에 다른 렌더링이 트리거됩니다. 이는 앱을 충돌시키는 무한 루프를 생성합니다.\n\n## 일반적인 위반 사례 {/*common-violations*/}\n\n### 잘못된 예시 {/*invalid*/}\n\n```js\n// ❌ 렌더링 중에 직접 무조건 setState\nfunction Component({value}) {\n  const [count, setCount] = useState(0);\n  setCount(value); // 무한 루프!\n  return <div>{count}</div>;\n}\n```\n\n### 올바른 예시 {/*valid*/}\n\n```js\n// ✅ 렌더링 중에 파생\nfunction Component({items}) {\n  const sorted = [...items].sort(); // 렌더링 중에 계산\n  return <ul>{sorted.map(/*...*/)}</ul>;\n}\n\n// ✅ 이벤트 핸들러에서 state 설정\nfunction Component() {\n  const [count, setCount] = useState(0);\n  return (\n    <button onClick={() => setCount(count + 1)}>\n      {count}\n    </button>\n  );\n}\n\n// ✅ state를 설정하는 대신 props에서 파생\nfunction Component({user}) {\n  const name = user?.name || '';\n  const email = user?.email || '';\n  return <div>{name}</div>;\n}\n\n// ✅ 이전 렌더링의 props와 state로부터 조건부로 state 파생\nfunction Component({ items }) {\n  const [isReverse, setIsReverse] = useState(false);\n  const [selection, setSelection] = useState(null);\n\n  const [prevItems, setPrevItems] = useState(items);\n  if (items !== prevItems) { // 이 조건이 유효하게 만듭니다\n    setPrevItems(items);\n    setSelection(null);\n  }\n  // ...\n}\n```\n\n## 문제 해결 {/*troubleshooting*/}\n\n### state를 prop과 동기화하고 싶습니다 {/*clamp-state-to-prop*/}\n\n일반적인 문제는 렌더링 후 state를 \"수정\"하려고 시도하는 것입니다. 카운터가 `max` prop을 초과하지 않도록 유지하고 싶다고 가정해봅시다.\n\n```js\n// ❌ 잘못된 예시: 렌더링 중에 제한\nfunction Counter({max}) {\n  const [count, setCount] = useState(0);\n\n  if (count > max) {\n    setCount(max);\n  }\n\n  return (\n    <button onClick={() => setCount(count + 1)}>\n      {count}\n    </button>\n  );\n}\n```\n\n`count`가 `max`를 초과하자마자 무한 루프가 트리거됩니다.\n\n대신 이 로직을 이벤트(state가 처음 설정되는 곳)로 이동하는 것이 더 좋습니다. 예를 들어 state를 업데이트하는 순간에 최댓값을 적용할 수 있습니다.\n\n```js\n// ✅ 업데이트할 때 제한\nfunction Counter({max}) {\n  const [count, setCount] = useState(0);\n\n  const increment = () => {\n    setCount(current => Math.min(current + 1, max));\n  };\n\n  return <button onClick={increment}>{count}</button>;\n}\n```\n\n이제 setter는 클릭에 대한 응답으로만 실행되고, React는 정상적으로 렌더링을 완료하며, `count`는 절대 `max`를 넘지 않습니다.\n\n드문 경우지만 이전 렌더링의 정보를 기반으로 state를 조정해야 할 수 있습니다. 그런 경우 조건부로 state를 설정하는 [이 패턴](/reference/react/useState#storing-information-from-previous-renders)을 따르세요.\n"
  },
  {
    "path": "src/content/reference/eslint-plugin-react-hooks/lints/static-components.md",
    "content": "---\ntitle: static-components\n---\n\n<Intro>\n\n컴포넌트가 정적이며 렌더링할 때마다 다시 생성되지 않는지 검증합니다. 동적으로 다시 생성되는 컴포넌트는 state를 초기화하고 과도한 재렌더링을 트리거할 수 있습니다.\n\n</Intro>\n\n## 규칙 세부 사항 {/*rule-details*/}\n\n다른 컴포넌트 내부에 정의된 컴포넌트는 렌더링할 때마다 다시 생성됩니다. React는 각각을 완전히 새로운 컴포넌트 타입으로 간주하여 이전 컴포넌트를 마운트 해제하고 새 컴포넌트를 마운트하며, 그 과정에서 모든 state와 DOM 노드를 파괴합니다.\n\n### 잘못된 예시 {/*invalid*/}\n\n이 규칙에 대한 잘못된 코드 예시입니다.\n\n```js\n// ❌ 컴포넌트 내부에 컴포넌트 정의\nfunction Parent() {\n  const ChildComponent = () => { // 렌더링할 때마다 새 컴포넌트!\n    const [count, setCount] = useState(0);\n    return <button onClick={() => setCount(count + 1)}>{count}</button>;\n  };\n\n  return <ChildComponent />; // 렌더링할 때마다 state 재설정\n}\n\n// ❌ 동적 컴포넌트 생성\nfunction Parent({type}) {\n  const Component = type === 'button'\n    ? () => <button>Click</button>\n    : () => <div>Text</div>;\n\n  return <Component />;\n}\n```\n\n### 올바른 예시 {/*valid*/}\n\n이 규칙에 대한 올바른 코드 예시입니다.\n\n```js\n// ✅ 모듈 레벨의 컴포넌트\nconst ButtonComponent = () => <button>Click</button>;\nconst TextComponent = () => <div>Text</div>;\n\nfunction Parent({type}) {\n  const Component = type === 'button'\n    ? ButtonComponent  // 기존 컴포넌트 참조\n    : TextComponent;\n\n  return <Component />;\n}\n```\n\n## 문제 해결 {/*troubleshooting*/}\n\n### 조건부로 다른 컴포넌트를 렌더링해야 합니다 {/*conditional-components*/}\n\n로컬 state에 액세스하기 위해 내부에 컴포넌트를 정의할 수 있습니다.\n\n```js\n// ❌ 잘못된 예시: 부모 state에 액세스하기 위한 내부 컴포넌트\nfunction Parent() {\n  const [theme, setTheme] = useState('light');\n\n  function ThemedButton() { // 렌더링할 때마다 재생성!\n    return (\n      <button className={theme}>\n        Click me\n      </button>\n    );\n  }\n\n  return <ThemedButton />;\n}\n```\n\n대신 데이터를 props로 전달하세요.\n\n```js\n// ✅ 더 나은 방법: 정적 컴포넌트에 props 전달\nfunction ThemedButton({theme}) {\n  return (\n    <button className={theme}>\n      Click me\n    </button>\n  );\n}\n\nfunction Parent() {\n  const [theme, setTheme] = useState('light');\n  return <ThemedButton theme={theme} />;\n}\n```\n\n<Note>\n\n로컬 변수에 액세스하기 위해 다른 컴포넌트 내부에 컴포넌트를 정의하고 싶다면, 대신 props를 전달해야 한다는 신호입니다. 이렇게 하면 컴포넌트를 더 재사용 가능하고 테스트하기 쉽게 만들 수 있습니다.\n\n</Note>\n"
  },
  {
    "path": "src/content/reference/eslint-plugin-react-hooks/lints/unsupported-syntax.md",
    "content": "---\ntitle: unsupported-syntax\n---\n\n<Intro>\n\nReact 컴파일러가 지원하지 않는 문법에 대해 검증합니다. 필요한 경우 독립적인 유틸리티 함수와 같이 React 외부에서 이 문법을 계속 사용할 수 있습니다.\n\n</Intro>\n\n## 규칙 세부 사항 {/*rule-details*/}\n\nReact 컴파일러는 최적화를 적용하기 위해 코드를 정적으로 분석합니다. `eval` 및 `with`와 같은 기능은 컴파일 타임에 코드가 무엇을 하는지 정적으로 이해하는 것을 불가능하게 만들기 때문에 컴파일러는 이를 사용하는 컴포넌트를 최적화할 수 없습니다.\n\n### 잘못된 예시 {/*invalid*/}\n\n이 규칙에 대한 잘못된 코드 예시입니다.\n\n```js\n// ❌ 컴포넌트에서 eval 사용\nfunction Component({ code }) {\n  const result = eval(code); // 분석할 수 없음\n  return <div>{result}</div>;\n}\n\n// ❌ with 문 사용\nfunction Component() {\n  with (Math) { // 동적으로 스코프 변경\n    return <div>{sin(PI / 2)}</div>;\n  }\n}\n\n// ❌ eval을 사용한 동적 프로퍼티 액세스\nfunction Component({propName}) {\n  const value = eval(`props.${propName}`);\n  return <div>{value}</div>;\n}\n```\n\n### 올바른 예시 {/*valid*/}\n\n이 규칙에 대한 올바른 코드 예시입니다.\n\n```js\n// ✅ 일반적인 프로퍼티 액세스 사용\nfunction Component({propName, props}) {\n  const value = props[propName]; // 분석 가능\n  return <div>{value}</div>;\n}\n\n// ✅ 표준 Math 메서드 사용\nfunction Component() {\n  return <div>{Math.sin(Math.PI / 2)}</div>;\n}\n```\n\n## 문제 해결 {/*troubleshooting*/}\n\n### 동적 코드를 평가해야 합니다 {/*evaluate-dynamic-code*/}\n\n사용자가 제공한 코드를 평가해야 할 수 있습니다.\n\n```js\n// ❌ 잘못된 예시: 컴포넌트에서 eval\nfunction Calculator({expression}) {\n  const result = eval(expression); // 안전하지 않고 최적화 불가능\n  return <div>결과: {result}</div>;\n}\n```\n\n대신 안전한 표현식 파서를 사용하세요.\n\n```js\n// ✅ 더 나은 방법: 안전한 파서 사용\nimport {evaluate} from 'mathjs'; // 또는 유사한 라이브러리\n\nfunction Calculator({expression}) {\n  const [result, setResult] = useState(null);\n\n  const calculate = () => {\n    try {\n      // 안전한 수학적 표현식 평가\n      setResult(evaluate(expression));\n    } catch (error) {\n      setResult('잘못된 표현식');\n    }\n  };\n\n  return (\n    <div>\n      <button onClick={calculate}>계산</button>\n      {result && <div>결과: {result}</div>}\n    </div>\n  );\n}\n```\n\n<Note>\n\n사용자 입력과 함께 `eval`을 절대 사용하지 마세요. 보안 위험이 있습니다. 수학적 표현식, JSON 파싱 또는 템플릿 평가와 같은 특정 사용 사례에는 전용 파싱 라이브러리를 사용하세요.\n\n</Note>\n"
  },
  {
    "path": "src/content/reference/eslint-plugin-react-hooks/lints/use-memo.md",
    "content": "---\ntitle: use-memo\n---\n\n<Intro>\n\n`useMemo` Hook이 반환값과 함께 사용되는지 검증합니다. 자세한 내용은 [`useMemo` 문서](/reference/react/useMemo)를 참고하세요.\n\n</Intro>\n\n## 규칙 세부 사항 {/*rule-details*/}\n\n`useMemo`는 비용이 많이 드는 값을 계산하고 캐싱하기 위한 것이지 부수 효과<sup>Side Effect</sup>를 위한 것이 아닙니다. 반환값이 없으면 `useMemo`는 `undefined`를 반환하여 목적을 달성하지 못하며, 잘못된 Hook을 사용하고 있음을 나타낼 가능성이 높습니다.\n\n### 잘못된 예시 {/*invalid*/}\n\n이 규칙에 대한 잘못된 코드 예시입니다.\n\n```js\n// ❌ 반환값 없음\nfunction Component({ data }) {\n  const processed = useMemo(() => {\n    data.forEach(item => console.log(item));\n    // return 누락!\n  }, [data]);\n\n  return <div>{processed}</div>; // 항상 undefined\n}\n```\n\n### 올바른 예시 {/*valid*/}\n\n이 규칙에 대한 올바른 코드 예시입니다.\n\n```js\n// ✅ 계산된 값 반환\nfunction Component({ data }) {\n  const processed = useMemo(() => {\n    return data.map(item => item * 2);\n  }, [data]);\n\n  return <div>{processed}</div>;\n}\n```\n\n## 문제 해결 {/*troubleshooting*/}\n\n### 의존성이 변경될 때 부수 효과를 실행해야 합니다 {/*side-effects*/}\n\n부수 효과<sup>Side Effect</sup>에 `useMemo`를 사용하려고 할 수 있습니다.\n\n{/* TODO(@poteto) fix compiler validation to check for unassigned useMemos */}\n```js\n// ❌ 잘못된 예시: useMemo에서 부수 효과\nfunction Component({user}) {\n  // 반환값 없음, 부수 효과만\n  useMemo(() => {\n    analytics.track('UserViewed', {userId: user.id});\n  }, [user.id]);\n\n  // 변수에 할당되지 않음\n  useMemo(() => {\n    return analytics.track('UserViewed', {userId: user.id});\n  }, [user.id]);\n}\n```\n\n부수 효과가 사용자 상호작용에 대한 응답으로 발생해야 하는 경우 부수 효과를 이벤트와 함께 배치하는 것이 가장 좋습니다.\n\n```js\n// ✅ 좋은 예시: 이벤트 핸들러에서 부수 효과\nfunction Component({user}) {\n  const handleClick = () => {\n    analytics.track('ButtonClicked', {userId: user.id});\n    // 기타 클릭 로직...\n  };\n\n  return <button onClick={handleClick}>Click me</button>;\n}\n```\n\n부수 효과가 React state를 외부 state와 동기화하는 경우(또는 그 반대) `useEffect`를 사용하세요.\n\n```js\n// ✅ 좋은 예시: useEffect에서 동기화\nfunction Component({theme}) {\n  useEffect(() => {\n    localStorage.setItem('preferredTheme', theme);\n    document.body.className = theme;\n  }, [theme]);\n\n  return <div>현재 테마: {theme}</div>;\n}\n```\n"
  },
  {
    "path": "src/content/reference/react/Activity.md",
    "content": "---\ntitle: <Activity>\n---\n\n<Intro>\n\n`<Activity>`를 사용하면 자식 컴포넌트의 UI와 내부 상태를 숨기고 복원할 수 있습니다.\n\n```js\n<Activity mode={visibility}>\n  <Sidebar />\n</Activity>\n```\n\n</Intro>\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `<Activity>` {/*activity*/}\n\nActivity를 사용하여 애플리케이션의 일부를 숨길 수 있습니다.\n\n```js [[1, 1, \"\\\\\"hidden\\\\\"\"], [2, 2, \"<Sidebar />\"], [3, 1, \"\\\\\"visible\\\\\"\"]]\n<Activity mode={isShowingSidebar ? \"visible\" : \"hidden\"}>\n  <Sidebar />\n</Activity>\n```\n\nActivity 경계가 <CodeStep step={1}>숨겨지면</CodeStep>, React는 `display: \"none\"` CSS 프로퍼티를 사용해 <CodeStep step={2}>자식 컴포넌트</CodeStep>를 시각적으로 숨깁니다. 또한 Effect를 클린업하고 활성 구독을 모두 해제합니다.\n\n숨겨진 상태에서도 자식 컴포넌트는 새로운 props에 반응하여 리렌더링되지만, 나머지 콘텐츠보다 낮은 우선순위로 처리됩니다.\n\n경계가 다시 <CodeStep step={3}>보이게 되면</CodeStep>, React는 이전 상태를 복원한 상태로 자식 컴포넌트를 표시하고 Effect를 다시 생성합니다.\n\n이러한 방식으로 Activity는 \"백그라운드 작업\"을 렌더링하는 메커니즘으로 생각할 수 있습니다. 다시 표시될 가능성이 있는 콘텐츠를 완전히 삭제하는 대신, Activity를 사용하면 해당 콘텐츠의 UI와 내부 상태를 유지하고 복원할 수 있으며, 동시에 숨겨진 콘텐츠가 원치 않는 부작용을 일으키지 않도록 보장합니다.\n\n[아래에서 더 많은 예시를 확인하세요.](#usage)\n\n#### Props {/*props*/}\n\n* `children`: 표시하거나 숨길 UI입니다.\n* `mode`: `'visible'` 또는 `'hidden'` 중 하나의 문자열 값입니다. 생략하면 기본값은 `'visible'`입니다. \n\n#### 주의 사항 {/*caveats*/}\n\n- Activity가 [ViewTransition](/reference/react/ViewTransition) 내부에서 렌더링되고, [startTransition](/reference/react/startTransition)으로 인한 업데이트의 결과로 보이게 되면 ViewTransition의 `enter` 애니메이션이 활성화됩니다. 숨겨지면 `exit` 애니메이션이 활성화됩니다.\n- 텍스트만 렌더링하는 Activity는 아무것도 렌더링하지 않습니다. 가시성 변경을 적용할 대응하는 DOM 엘리먼트가 없기 때문입니다. 예를 들어 `const ComponentThatJustReturnsText = () => \"Hello, World!\"`인 경우, `<Activity mode=\"hidden\"><ComponentThatJustReturnsText /></Activity>`는 DOM에 아무런 출력도 생성하지 않습니다.\n\n---\n\n## 사용법 {/*usage*/}\n\n### 숨겨진 컴포넌트의 상태 복원하기 {/*restoring-the-state-of-hidden-components*/}\n\nReact에서 컴포넌트를 조건부로 표시하거나 숨기려면 일반적으로 해당 조건에 따라 마운트하거나 마운트 해제합니다.\n\n```jsx\n{isShowingSidebar && (\n  <Sidebar />\n)}\n```\n\n하지만 컴포넌트를 마운트 해제하면 내부 상태가 사라지는데, 이것이 항상 원하는 동작은 아닙니다.\n\nActivity 경계를 사용해 컴포넌트를 숨기면 React는 나중을 위해 상태를 \"저장\"합니다.\n\n```jsx\n<Activity mode={isShowingSidebar ? \"visible\" : \"hidden\"}>\n  <Sidebar />\n</Activity>\n```\n\n이렇게 하면 컴포넌트를 숨긴 후 나중에 이전 상태 그대로 복원할 수 있습니다.\n\n다음 예시에는 펼칠 수 있는 섹션이 있는 사이드바가 있습니다. \"Overview\"를 누르면 아래에 세 개의 하위 항목이 표시됩니다. 메인 앱 영역에는 사이드바를 숨기고 표시하는 버튼도 있습니다.\n\nOverview 섹션을 펼친 다음 사이드바를 닫았다가 다시 열어보세요.\n\n<Sandpack>\n\n```js src/App.js active\nimport { useState } from 'react';\nimport Sidebar from './Sidebar.js';\n\nexport default function App() {\n  const [isShowingSidebar, setIsShowingSidebar] = useState(true);\n\n  return (\n    <>\n      {isShowingSidebar && (\n        <Sidebar />\n      )}\n\n      <main>\n        <button onClick={() => setIsShowingSidebar(!isShowingSidebar)}>\n          Toggle sidebar\n        </button>\n        <h1>Main content</h1>\n      </main>\n    </>\n  );\n}\n```\n\n```js src/Sidebar.js\nimport { useState } from 'react';\n\nexport default function Sidebar() {\n  const [isExpanded, setIsExpanded] = useState(false)\n  \n  return (\n    <nav>\n      <button onClick={() => setIsExpanded(!isExpanded)}>\n        Overview\n        <span className={`indicator ${isExpanded ? 'down' : 'right'}`}>\n          &#9650;\n        </span>\n      </button>\n\n      {isExpanded && (\n        <ul>\n          <li>Section 1</li>\n          <li>Section 2</li>\n          <li>Section 3</li>\n        </ul>\n      )}\n    </nav>\n  );\n}\n```\n\n```css\nbody { height: 275px; margin: 0; }\n#root {\n  display: flex;\n  gap: 10px;\n  height: 100%;\n}\nnav {\n  padding: 10px;\n  background: #eee;\n  font-size: 14px;\n  height: 100%;\n}\nmain {\n  padding: 10px;\n}\np {\n  margin: 0;\n}\nh1 {\n  margin-top: 10px;\n}\n.indicator {\n  margin-left: 4px;\n  display: inline-block;\n  rotate: 90deg;\n}\n.indicator.down {\n  rotate: 180deg;\n}\n```\n\n</Sandpack>\n\nOverview 섹션은 항상 접힌 상태로 시작합니다. `isShowingSidebar`가 `false`로 바뀌면서 사이드바를 마운트 해제하기 때문에 모든 내부 상태가 손실됩니다.\n\n이것이 바로 Activity를 사용하기 완벽한 사례입니다. 시각적으로 숨기면서도 사이드바의 내부 상태를 보존할 수 있습니다.\n\n사이드바의 조건부 렌더링을 Activity 경계로 교체해보겠습니다.\n\n```jsx {7,9}\n// Before\n{isShowingSidebar && (\n  <Sidebar />\n)}\n\n// After\n<Activity mode={isShowingSidebar ? 'visible' : 'hidden'}>\n  <Sidebar />\n</Activity>\n```\n\n새로운 동작을 확인해보세요.\n\n<Sandpack>\n\n```js src/App.js active\nimport { Activity, useState } from 'react';\n\nimport Sidebar from './Sidebar.js';\n\nexport default function App() {\n  const [isShowingSidebar, setIsShowingSidebar] = useState(true);\n\n  return (\n    <>\n      <Activity mode={isShowingSidebar ? 'visible' : 'hidden'}>\n        <Sidebar />\n      </Activity>\n\n      <main>\n        <button onClick={() => setIsShowingSidebar(!isShowingSidebar)}>\n          Toggle sidebar\n        </button>\n        <h1>Main content</h1>\n      </main>\n    </>\n  );\n}\n```\n\n```js src/Sidebar.js\nimport { useState } from 'react';\n\nexport default function Sidebar() {\n  const [isExpanded, setIsExpanded] = useState(false)\n  \n  return (\n    <nav>\n      <button onClick={() => setIsExpanded(!isExpanded)}>\n        Overview\n        <span className={`indicator ${isExpanded ? 'down' : 'right'}`}>\n          &#9650;\n        </span>\n      </button>\n\n      {isExpanded && (\n        <ul>\n          <li>Section 1</li>\n          <li>Section 2</li>\n          <li>Section 3</li>\n        </ul>\n      )}\n    </nav>\n  );\n}\n```\n\n```css\nbody { height: 275px; margin: 0; }\n#root {\n  display: flex;\n  gap: 10px;\n  height: 100%;\n}\nnav {\n  padding: 10px;\n  background: #eee;\n  font-size: 14px;\n  height: 100%;\n}\nmain {\n  padding: 10px;\n}\np {\n  margin: 0;\n}\nh1 {\n  margin-top: 10px;\n}\n.indicator {\n  margin-left: 4px;\n  display: inline-block;\n  rotate: 90deg;\n}\n.indicator.down {\n  rotate: 180deg;\n}\n```\n\n</Sandpack>\n\n이제 사이드바의 내부 상태가 구현을 변경하지 않고도 복원됩니다.\n\n---\n\n### 숨겨진 컴포넌트의 DOM 복원하기 {/*restoring-the-dom-of-hidden-components*/}\n\nActivity 경계는 `display: none`을 사용해 자식 컴포넌트를 숨기기 때문에, 숨겨진 상태에서도 자식의 DOM이 보존됩니다. 이는 사용자가 다시 상호작용할 가능성이 있는 UI 부분의 임시 상태를 유지하는 데 유용합니다.\n\n이 예시에서 Contact 탭에는 사용자가 메시지를 입력할 수 있는 `<textarea>`가 있습니다. 텍스트를 입력한 후 Home 탭으로 변경했다가 다시 Contact 탭으로 돌아오면 입력한 메시지가 사라집니다.\n\n<Sandpack>\n\n```js src/App.js \nimport { useState } from 'react';\nimport TabButton from './TabButton.js';\nimport Home from './Home.js';\nimport Contact from './Contact.js';\n\nexport default function App() {\n  const [activeTab, setActiveTab] = useState('contact');\n\n  return (\n    <>\n      <TabButton\n        isActive={activeTab === 'home'}\n        onClick={() => setActiveTab('home')}\n      >\n        Home\n      </TabButton>\n      <TabButton\n        isActive={activeTab === 'contact'}\n        onClick={() => setActiveTab('contact')}\n      >\n        Contact\n      </TabButton>\n\n      <hr />\n\n      {activeTab === 'home' && <Home />}\n      {activeTab === 'contact' && <Contact />}\n    </>\n  );\n}\n```\n\n```js src/TabButton.js\nexport default function TabButton({ onClick, children, isActive }) {\n  if (isActive) {\n    return <b>{children}</b>\n  }\n\n  return (\n    <button onClick={onClick}>\n      {children}\n    </button>\n  );\n}\n```\n\n```js src/Home.js\nexport default function Home() {\n  return (\n    <p>Welcome to my profile!</p>\n  );\n}\n```\n\n```js src/Contact.js active\nexport default function Contact() {\n  return (\n    <div>\n      <p>Send me a message!</p>\n\n      <textarea />\n\n      <p>You can find me online here:</p>\n      <ul>\n        <li>admin@mysite.com</li>\n        <li>+123456789</li>\n      </ul>\n    </div>\n  );\n}\n```\n\n```css\nbody { height: 275px; }\nbutton { margin-right: 10px }\nb { display: inline-block; margin-right: 10px; }\n.pending { color: #777; }\n```\n\n</Sandpack>\n\n`App`에서 `Contact`를 완전히 마운트 해제하기 때문입니다. Contact 탭이 마운트 해제되면 `<textarea>` 엘리먼트의 내부 DOM 상태가 손실됩니다.\n\nActivity 경계를 사용해 활성 탭을 표시하고 숨기도록 전환하면 각 탭의 DOM 상태를 보존할 수 있습니다. 텍스트를 입력하고 다시 탭을 전환해보면 입력한 메시지가 더 이상 초기화되지 않는 것을 확인할 수 있습니다.\n\n<Sandpack>\n\n```js src/App.js active\nimport { Activity, useState } from 'react';\nimport TabButton from './TabButton.js';\nimport Home from './Home.js';\nimport Contact from './Contact.js';\n\nexport default function App() {\n  const [activeTab, setActiveTab] = useState('contact');\n\n  return (\n    <>\n      <TabButton\n        isActive={activeTab === 'home'}\n        onClick={() => setActiveTab('home')}\n      >\n        Home\n      </TabButton>\n      <TabButton\n        isActive={activeTab === 'contact'}\n        onClick={() => setActiveTab('contact')}\n      >\n        Contact\n      </TabButton>\n\n      <hr />\n\n      <Activity mode={activeTab === 'home' ? 'visible' : 'hidden'}>\n        <Home />\n      </Activity>\n      <Activity mode={activeTab === 'contact' ? 'visible' : 'hidden'}>\n        <Contact />\n      </Activity>\n    </>\n  );\n}\n```\n\n```js src/TabButton.js\nexport default function TabButton({ onClick, children, isActive }) {\n  if (isActive) {\n    return <b>{children}</b>\n  }\n\n  return (\n    <button onClick={onClick}>\n      {children}\n    </button>\n  );\n}\n```\n\n```js src/Home.js\nexport default function Home() {\n  return (\n    <p>Welcome to my profile!</p>\n  );\n}\n```\n\n```js src/Contact.js \nexport default function Contact() {\n  return (\n    <div>\n      <p>Send me a message!</p>\n\n      <textarea />\n\n      <p>You can find me online here:</p>\n      <ul>\n        <li>admin@mysite.com</li>\n        <li>+123456789</li>\n      </ul>\n    </div>\n  );\n}\n```\n\n```css\nbody { height: 275px; }\nbutton { margin-right: 10px }\nb { display: inline-block; margin-right: 10px; }\n.pending { color: #777; }\n```\n\n</Sandpack>\n\n다시 한번, Activity 경계를 통해 Contact 탭의 내부 상태를 구현 변경 없이 보존할 수 있었습니다.\n\n---\n\n### 표시될 가능성이 있는 콘텐츠 사전 렌더링하기 {/*pre-rendering-content-thats-likely-to-become-visible*/}\n\n지금까지 Activity를 사용해 사용자가 상호작용한 콘텐츠를 임시 상태를 삭제하지 않고 숨기는 방법을 살펴봤습니다.\n\n하지만 Activity 경계는 사용자가 아직 처음 보지 못한 콘텐츠를 _준비_ 하는 데도 사용할 수 있습니다.\n\n```jsx [[1, 1, \"\\\\\"hidden\\\\\"\"]]\n<Activity mode=\"hidden\">\n  <SlowComponent />\n</Activity>\n```\n\nActivity 경계가 초기 렌더링 중에 <CodeStep step={1}>숨겨진</CodeStep> 상태라면, 자식 컴포넌트는 페이지에 보이지 않지만 _여전히 렌더링_ 됩니다. 다만 보이는 콘텐츠보다 낮은 우선순위로 렌더링되며, Effect는 마운트되지 않습니다.\n\n이러한 _사전 렌더링_ 을 통해 자식 컴포넌트가 필요한 코드나 데이터를 미리 로드할 수 있으므로, 나중에 Activity 경계가 보이게 될 때 로딩 시간이 줄어들어 더 빠르게 표시할 수 있습니다.\n\n예시를 살펴보겠습니다.\n\n이 데모에서 Posts 탭은 일부 데이터를 로드합니다. 탭을 누르면 데이터를 가져오는 동안 Suspense 폴백이 표시됩니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { useState, Suspense } from 'react';\nimport TabButton from './TabButton.js';\nimport Home from './Home.js';\nimport Posts from './Posts.js';\n\nexport default function App() {\n  const [activeTab, setActiveTab] = useState('home');\n\n  return (\n    <>\n      <TabButton\n        isActive={activeTab === 'home'}\n        onClick={() => setActiveTab('home')}\n      >\n        Home\n      </TabButton>\n      <TabButton\n        isActive={activeTab === 'posts'}\n        onClick={() => setActiveTab('posts')}\n      >\n        Posts\n      </TabButton>\n\n      <hr />\n\n      <Suspense fallback={<h1>🌀 Loading...</h1>}>\n        {activeTab === 'home' && <Home />}\n        {activeTab === 'posts' && <Posts />}\n      </Suspense>\n    </>\n  );\n}\n```\n\n```js src/TabButton.js hidden\nexport default function TabButton({ onClick, children, isActive }) {\n  if (isActive) {\n    return <b>{children}</b>\n  }\n\n  return (\n    <button onClick={onClick}>\n      {children}\n    </button>\n  );\n}\n```\n\n```js src/Home.js\nexport default function Home() {\n  return (\n    <p>Welcome to my profile!</p>\n  );\n}\n```\n\n```js src/Posts.js\nimport { use } from 'react';\nimport { fetchData } from './data.js';\n\nexport default function Posts() {\n  const posts = use(fetchData('/posts'));\n\n  return (\n    <ul className=\"items\">\n      {posts.map(post =>\n        <li className=\"item\" key={post.id}>\n          {post.title}\n        </li>\n      )}\n    </ul>\n  );\n}\n```\n\n```js src/data.js hidden\n// Note: 데이터 페칭 방법은\n// Suspense와 함께 사용하는 프레임워크에 따라 달라집니다.\n// 일반적으로 캐싱 로직은 프레임워크 내부에 있습니다.\n\nlet cache = new Map();\n\nexport function fetchData(url) {\n  if (!cache.has(url)) {\n    cache.set(url, getData(url));\n  }\n  return cache.get(url);\n}\n\nasync function getData(url) {\n  if (url.startsWith('/posts')) {\n    return await getPosts();\n  } else {\n    throw Error('Not implemented');\n  }\n}\n\nasync function getPosts() {\n  // 대기 시간을 눈에 띄게 만들기 위해 가짜 지연을 추가합니다.\n  await new Promise(resolve => {\n    setTimeout(resolve, 1000);\n  });\n  let posts = [];\n  for (let i = 0; i < 10; i++) {\n    posts.push({\n      id: i,\n      title: 'Post #' + (i + 1)\n    });\n  }\n  return posts;\n}\n```\n\n```css\nbody { height: 275px; }\nbutton { margin-right: 10px }\nb { display: inline-block; margin-right: 10px; }\n.pending { color: #777; }\nvideo { width: 300px; margin-top: 10px; aspect-ratio: 16/9; }\n```\n\n</Sandpack>\n\n`App`이 탭이 활성화될 때까지 `Posts`를 마운트하지 않기 때문입니다.\n\n`App`을 수정하여 Activity 경계로 활성 탭을 표시하고 숨기도록 하면, 앱이 처음 로드될 때 `Posts`가 사전 렌더링되어 보이기 전에 데이터를 가져올 수 있습니다.\n\n이제 Posts 탭을 클릭해보세요.\n\n<Sandpack>\n\n```js src/App.js\nimport { Activity, useState, Suspense } from 'react';\nimport TabButton from './TabButton.js';\nimport Home from './Home.js';\nimport Posts from './Posts.js';\n\nexport default function App() {\n  const [activeTab, setActiveTab] = useState('home');\n\n  return (\n    <>\n      <TabButton\n        isActive={activeTab === 'home'}\n        onClick={() => setActiveTab('home')}\n      >\n        Home\n      </TabButton>\n      <TabButton\n        isActive={activeTab === 'posts'}\n        onClick={() => setActiveTab('posts')}\n      >\n        Posts\n      </TabButton>\n\n      <hr />\n\n      <Suspense fallback={<h1>🌀 Loading...</h1>}>\n        <Activity mode={activeTab === 'home' ? 'visible' : 'hidden'}>\n          <Home />\n        </Activity>\n        <Activity mode={activeTab === 'posts' ? 'visible' : 'hidden'}>\n          <Posts />\n        </Activity>\n      </Suspense>\n    </>\n  );\n}\n```\n\n```js src/TabButton.js hidden\nexport default function TabButton({ onClick, children, isActive }) {\n  if (isActive) {\n    return <b>{children}</b>\n  }\n\n  return (\n    <button onClick={onClick}>\n      {children}\n    </button>\n  );\n}\n```\n\n```js src/Home.js\nexport default function Home() {\n  return (\n    <p>Welcome to my profile!</p>\n  );\n}\n```\n\n```js src/Posts.js\nimport { use } from 'react';\nimport { fetchData } from './data.js';\n\nexport default function Posts() {\n  const posts = use(fetchData('/posts'));\n\n  return (\n    <ul className=\"items\">\n      {posts.map(post =>\n        <li className=\"item\" key={post.id}>\n          {post.title}\n        </li>\n      )}\n    </ul>\n  );\n}\n```\n\n```js src/data.js hidden\n// Note: 데이터 페칭 방법은\n// Suspense와 함께 사용하는 프레임워크에 따라 달라집니다.\n// 일반적으로 캐싱 로직은 프레임워크 내부에 있습니다.\n\nlet cache = new Map();\n\nexport function fetchData(url) {\n  if (!cache.has(url)) {\n    cache.set(url, getData(url));\n  }\n  return cache.get(url);\n}\n\nasync function getData(url) {\n  if (url.startsWith('/posts')) {\n    return await getPosts();\n  } else {\n    throw Error('Not implemented');\n  }\n}\n\nasync function getPosts() {\n  // 대기 시간을 눈에 띄게 만들기 위해 가짜 지연을 추가합니다.\n  await new Promise(resolve => {\n    setTimeout(resolve, 1000);\n  });\n  let posts = [];\n  for (let i = 0; i < 10; i++) {\n    posts.push({\n      id: i,\n      title: 'Post #' + (i + 1)\n    });\n  }\n  return posts;\n}\n```\n\n```css\nbody { height: 275px; }\nbutton { margin-right: 10px }\nb { display: inline-block; margin-right: 10px; }\n.pending { color: #777; }\nvideo { width: 300px; margin-top: 10px; aspect-ratio: 16/9; }\n```\n\n</Sandpack>\n\n숨겨진 Activity 경계 덕분에 `Posts`가 더 빠른 렌더링을 준비할 수 있었습니다.\n\n---\n\n숨겨진 Activity 경계로 컴포넌트를 사전 렌더링하는 것은 사용자가 다음에 상호작용할 가능성이 있는 UI 부분의 로딩 시간을 줄이는 강력한 방법입니다.\n\n<Note>\n\n**사전 렌더링 중에는 Suspense가 가능한 데이터만 가져옵니다.** 여기에는 다음이 포함됩니다.\n\n- [Relay](https://relay.dev/docs/guided-tour/rendering/loading-states/)와 [Next.js](https://nextjs.org/docs/app/building-your-application/routing/loading-ui-and-streaming#streaming-with-suspense) 같이 Suspense가 가능한 프레임워크를 사용한 데이터 가져오기.\n- [`lazy`](/reference/react/lazy)를 활용한 지연 로딩 컴포넌트.\n- [`use`](/reference/react/use)를 사용해서 캐시된 Promise 값 읽기.\n\nActivity는 Effect 내부에서 가져온 데이터를 감지하지 **않습니다.**\n\n위의 `Posts` 컴포넌트에서 데이터를 로드하는 정확한 방법은 프레임워크에 따라 다릅니다. Suspense가 가능한 프레임워크를 사용하는 경우, 프레임워크의 데이터 불러오기 관련 문서에서 자세한 내용을 확인할 수 있습니다.\n\n독자적인 프레임워크를 사용하지 않는 Suspense가 가능한 데이터 가져오기 기능은 아직 지원되지 않습니다. Suspense 지원 데이터 소스를 구현하기 위한 요구 사항은 불안정하고 문서화되지 않았습니다. 데이터 소스를 Suspense와 통합하기 위한 공식 API는 향후 React 버전에서 출시될 예정입니다.\n\n</Note>\n\n---\n\n\n### 페이지 로드 중 상호작용 속도 높이기 {/*speeding-up-interactions-during-page-load*/}\n\nReact에는 선택적 하이드레이션이라는 내부 성능 최적화가 포함되어 있습니다. 이는 앱의 초기 HTML을 _청크 단위_ 로 하이드레이션하여, 페이지의 다른 컴포넌트가 코드나 데이터를 아직 로드하지 않았더라도 일부 컴포넌트를 상호작용 가능하게 만듭니다.\n\nSuspense 경계는 자연스럽게 컴포넌트 트리를 서로 독립적인 단위로 나누기 때문에 선택적 하이드레이션에 참여합니다.\n\n```jsx\nfunction Page() {\n  return (\n    <>\n      <MessageComposer />\n\n      <Suspense fallback=\"Loading chats...\">\n        <Chats />\n      </Suspense>\n    </>\n  )\n}\n```\n\n여기서 `MessageComposer`는 `Chats`가 마운트되어 데이터를 가져오기 시작하기 전에도 페이지의 초기 렌더링 중에 완전히 하이드레이션될 수 있습니다.\n\n컴포넌트 트리를 개별 단위로 나누면 React가 앱의 서버 렌더링된 HTML을 청크 단위로 하이드레이션할 수 있어, 앱의 일부가 가능한 한 빠르게 상호작용 가능해집니다.\n\n그렇다면 Suspense를 사용하지 않는 페이지는 어떻게 될까요?\n\n다음 탭 예시를 보겠습니다.\n\n```jsx\nfunction Page() {\n  const [activeTab, setActiveTab] = useState('home');\n\n  return (\n    <>\n      <TabButton onClick={() => setActiveTab('home')}>\n        Home\n      </TabButton>\n      <TabButton onClick={() => setActiveTab('video')}>\n        Video\n      </TabButton>\n\n      {activeTab === 'home' && (\n        <Home />\n      )}\n      {activeTab === 'video' && (\n        <Video />\n      )}\n    </>\n  )\n}\n```\n\n여기서 React는 전체 페이지를 한 번에 하이드레이션해야 합니다. `Home`이나 `Video`가 렌더링이 느리다면 하이드레이션 중에 탭 버튼이 반응하지 않는 것처럼 느껴질 수 있습니다.\n\n활성 탭 주위에 Suspense를 추가하면 이 문제를 해결할 수 있습니다.\n\n```jsx {13,20}\nfunction Page() {\n  const [activeTab, setActiveTab] = useState('home');\n\n  return (\n    <>\n      <TabButton onClick={() => setActiveTab('home')}>\n        Home\n      </TabButton>\n      <TabButton onClick={() => setActiveTab('video')}>\n        Video\n      </TabButton>\n\n      <Suspense fallback={<Placeholder />}>\n        {activeTab === 'home' && (\n          <Home />\n        )}\n        {activeTab === 'video' && (\n          <Video />\n        )}\n      </Suspense>\n    </>\n  )\n}\n```\n\n...하지만 이렇게 하면 초기 렌더링에서 `Placeholder` 폴백이 표시되기 때문에 UI가 변경됩니다.\n\n대신 Activity를 사용할 수 있습니다. Activity 경계는 자식을 표시하고 숨기기 때문에 이미 자연스럽게 컴포넌트 트리를 독립적인 단위로 나눕니다. 그리고 Suspense처럼 이 기능을 통해 선택적 하이드레이션에 참여할 수 있습니다.\n\n예시를 수정하여 활성 탭 주위에 Activity 경계를 사용해보겠습니다.\n\n```jsx {13-18}\nfunction Page() {\n  const [activeTab, setActiveTab] = useState('home');\n\n  return (\n    <>\n      <TabButton onClick={() => setActiveTab('home')}>\n        Home\n      </TabButton>\n      <TabButton onClick={() => setActiveTab('video')}>\n        Video\n      </TabButton>\n\n      <Activity mode={activeTab === \"home\" ? \"visible\" : \"hidden\"}>\n        <Home />\n      </Activity>\n      <Activity mode={activeTab === \"video\" ? \"visible\" : \"hidden\"}>\n        <Video />\n      </Activity>\n    </>\n  )\n}\n```\n\n이제 초기 서버 렌더링된 HTML은 원래 버전과 동일하게 보이지만, Activity 덕분에 React는 `Home`이나 `Video`를 마운트하기도 전에 탭 버튼을 먼저 하이드레이션할 수 있습니다.\n\n---\n\n따라서 콘텐츠를 숨기고 표시하는 것 외에도, Activity 경계는 페이지의 어느 부분이 독립적으로 상호작용 가능해질 수 있는지 React에 알려줌으로써 하이드레이션 중 앱의 성능을 향상시킵니다.\n\n페이지가 콘텐츠의 일부를 숨기지 않더라도, 하이드레이션 성능을 향상시키기 위해 항상 보이는 Activity 경계를 추가할 수 있습니다.\n\n```jsx\nfunction Page() {\n  return (\n    <>\n      <Post />\n\n      <Activity>\n        <Comments />\n      </Activity>\n    </>\n  );\n} \n```\n\n---\n\n## 문제 해결 {/*troubleshooting*/}\n\n### 숨겨진 컴포넌트에 원치 않는 부작용이 있습니다 {/*my-hidden-components-have-unwanted-side-effects*/}\n\nActivity 경계는 자식에 `display: none`을 설정하고 Effect를 클린업하여 콘텐츠를 숨깁니다. 따라서 부작용을 적절히 클린업하는 대부분의 잘 작성된 React 컴포넌트는 이미 Activity에 의해 숨겨지는 것에 대해 견고합니다.\n\n하지만 숨겨진 컴포넌트가 마운트 해제된 컴포넌트와 다르게 동작하는 _몇 가지_ 상황이 있습니다. 특히 숨겨진 컴포넌트의 DOM은 제거되지 않기 때문에, 해당 DOM의 부작용은 컴포넌트가 숨겨진 후에도 지속됩니다.\n\n예를 들어 `<video>` 태그를 생각해보세요. 일반적으로 클린업이 필요하지 않습니다. 비디오를 재생 중이더라도 태그를 마운트 해제하면 브라우저에서 비디오와 오디오 재생이 중지되기 때문입니다. 비디오를 재생한 후 이 데모에서 Home을 눌러보세요.\n\n<Sandpack>\n\n```js src/App.js active\nimport { useState } from 'react';\nimport TabButton from './TabButton.js';\nimport Home from './Home.js';\nimport Video from './Video.js';\n\nexport default function App() {\n  const [activeTab, setActiveTab] = useState('video');\n\n  return (\n    <>\n      <TabButton\n        isActive={activeTab === 'home'}\n        onClick={() => setActiveTab('home')}\n      >\n        Home\n      </TabButton>\n      <TabButton\n        isActive={activeTab === 'video'}\n        onClick={() => setActiveTab('video')}\n      >\n        Video\n      </TabButton>\n\n      <hr />\n\n      {activeTab === 'home' && <Home />}\n      {activeTab === 'video' && <Video />}\n    </>\n  );\n}\n```\n\n```js src/TabButton.js hidden\nexport default function TabButton({ onClick, children, isActive }) {\n  if (isActive) {\n    return <b>{children}</b>\n  }\n\n  return (\n    <button onClick={onClick}>\n      {children}\n    </button>\n  );\n}\n```\n\n```js src/Home.js\nexport default function Home() {\n  return (\n    <p>Welcome to my profile!</p>\n  );\n}\n```\n\n```js src/Video.js \nexport default function Video() {\n  return (\n    <video\n      // 'Big Buck Bunny' licensed under CC 3.0 by the Blender foundation. Hosted by archive.org\n      src=\"https://archive.org/download/BigBuckBunny_124/Content/big_buck_bunny_720p_surround.mp4\"\n      controls\n      playsInline\n    />\n\n  );\n}\n```\n\n```css\nbody { height: 275px; }\nbutton { margin-right: 10px }\nb { display: inline-block; margin-right: 10px; }\n.pending { color: #777; }\nvideo { width: 300px; margin-top: 10px; aspect-ratio: 16/9; }\n```\n\n</Sandpack>\n\n비디오가 예상대로 재생을 멈춥니다.\n\n이제 사용자가 마지막으로 시청한 타임코드를 보존하여 비디오 탭으로 돌아왔을 때 처음부터 다시 시작하지 않도록 하고 싶다고 가정해봅시다.\n\n이것은 Activity를 사용하기 완벽한 사례입니다!\n\n`App`을 수정하여 비활성 탭을 마운트 해제하는 대신 숨겨진 Activity 경계로 숨기고, 이번에는 데모가 어떻게 동작하는지 확인해보세요.\n\n<Sandpack>\n\n```js src/App.js active\nimport { Activity, useState } from 'react';\nimport TabButton from './TabButton.js';\nimport Home from './Home.js';\nimport Video from './Video.js';\n\nexport default function App() {\n  const [activeTab, setActiveTab] = useState('video');\n\n  return (\n    <>\n      <TabButton\n        isActive={activeTab === 'home'}\n        onClick={() => setActiveTab('home')}\n      >\n        Home\n      </TabButton>\n      <TabButton\n        isActive={activeTab === 'video'}\n        onClick={() => setActiveTab('video')}\n      >\n        Video\n      </TabButton>\n\n      <hr />\n\n      <Activity mode={activeTab === 'home' ? 'visible' : 'hidden'}>\n        <Home />\n      </Activity>\n      <Activity mode={activeTab === 'video' ? 'visible' : 'hidden'}>\n        <Video />\n      </Activity>\n    </>\n  );\n}\n```\n\n```js src/TabButton.js hidden\nexport default function TabButton({ onClick, children, isActive }) {\n  if (isActive) {\n    return <b>{children}</b>\n  }\n\n  return (\n    <button onClick={onClick}>\n      {children}\n    </button>\n  );\n}\n```\n\n```js src/Home.js\nexport default function Home() {\n  return (\n    <p>Welcome to my profile!</p>\n  );\n}\n```\n\n```js src/Video.js \nexport default function Video() {\n  return (\n    <video\n      controls\n      playsInline\n      // 'Big Buck Bunny' licensed under CC 3.0 by the Blender foundation. Hosted by archive.org\n      src=\"https://archive.org/download/BigBuckBunny_124/Content/big_buck_bunny_720p_surround.mp4\"\n    />\n\n  );\n}\n```\n\n```css\nbody { height: 275px; }\nbutton { margin-right: 10px }\nb { display: inline-block; margin-right: 10px; }\n.pending { color: #777; }\nvideo { width: 300px; margin-top: 10px; aspect-ratio: 16/9; }\n```\n\n</Sandpack>\n\n이런! 비디오가 숨겨진 후에도 비디오와 오디오가 계속 재생됩니다. 탭의 `<video>` 엘리먼트가 여전히 DOM에 있기 때문입니다.\n\n이를 해결하기 위해 비디오를 일시정지하는 클린업 함수가 있는 Effect를 추가할 수 있습니다.\n\n```jsx {2,4-10,14}\nexport default function VideoTab() {\n  const ref = useRef();\n\n  useLayoutEffect(() => {\n    const videoRef = ref.current;\n\n    return () => {\n      videoRef.pause()\n    }\n  }, []);\n\n  return (\n    <video\n      ref={ref}\n      controls\n      playsInline\n      src=\"...\"\n    />\n\n  );\n}\n```\n\n개념적으로 클린업 코드가 컴포넌트의 UI가 시각적으로 숨겨지는 것과 연결되어 있기 때문에 `useEffect` 대신 `useLayoutEffect`를 호출합니다. 일반 effect를 사용하면 (예를 들어) 다시 suspend되는 Suspense 경계나 View Transition에 의해 코드가 지연될 수 있습니다.\n\n새로운 동작을 확인해보세요. 비디오를 재생하고 Home 탭으로 전환한 다음 다시 Video 탭으로 돌아와보세요.\n\n<Sandpack>\n\n```js src/App.js active\nimport { Activity, useState } from 'react';\nimport TabButton from './TabButton.js';\nimport Home from './Home.js';\nimport Video from './Video.js';\n\nexport default function App() {\n  const [activeTab, setActiveTab] = useState('video');\n\n  return (\n    <>\n      <TabButton\n        isActive={activeTab === 'home'}\n        onClick={() => setActiveTab('home')}\n      >\n        Home\n      </TabButton>\n      <TabButton\n        isActive={activeTab === 'video'}\n        onClick={() => setActiveTab('video')}\n      >\n        Video\n      </TabButton>\n\n      <hr />\n\n      <Activity mode={activeTab === 'home' ? 'visible' : 'hidden'}>\n        <Home />\n      </Activity>\n      <Activity mode={activeTab === 'video' ? 'visible' : 'hidden'}>\n        <Video />\n      </Activity>\n    </>\n  );\n}\n```\n\n```js src/TabButton.js hidden\nexport default function TabButton({ onClick, children, isActive }) {\n  if (isActive) {\n    return <b>{children}</b>\n  }\n\n  return (\n    <button onClick={onClick}>\n      {children}\n    </button>\n  );\n}\n```\n\n```js src/Home.js\nexport default function Home() {\n  return (\n    <p>Welcome to my profile!</p>\n  );\n}\n```\n\n```js src/Video.js \nimport { useRef, useLayoutEffect } from 'react';\n\nexport default function Video() {\n  const ref = useRef();\n\n  useLayoutEffect(() => {\n    const videoRef = ref.current\n\n    return () => {\n      videoRef.pause()\n    };\n  }, [])\n\n  return (\n    <video\n      ref={ref}\n      controls\n      playsInline\n      // 'Big Buck Bunny' licensed under CC 3.0 by the Blender foundation. Hosted by archive.org\n      src=\"https://archive.org/download/BigBuckBunny_124/Content/big_buck_bunny_720p_surround.mp4\"\n    />\n\n  );\n}\n```\n\n```css\nbody { height: 275px; }\nbutton { margin-right: 10px }\nb { display: inline-block; margin-right: 10px; }\n.pending { color: #777; }\nvideo { width: 300px; margin-top: 10px; aspect-ratio: 16/9; }\n```\n\n</Sandpack>\n\n완벽하게 작동합니다! 클린업 함수는 Activity 경계에 의해 숨겨질 때마다 비디오 재생이 중지되도록 보장하며, 더 좋은 점은 `<video>` 태그가 제거되지 않기 때문에 타임코드가 보존되고, 사용자가 시청을 계속하기 위해 다시 전환할 때 비디오를 초기화하거나 다시 다운로드할 필요가 없다는 것입니다.\n\n이는 Activity를 사용하여 숨겨지지만 사용자가 곧 다시 상호작용할 가능성이 있는 UI 부분의 임시 DOM 상태를 보존하는 좋은 예시입니다.\n\n---\n\n예시에서 보듯이 `<video>`와 같은 특정 태그의 경우 마운트 해제와 숨기기가 다른 동작을 보입니다. 컴포넌트가 부작용이 있는 DOM을 렌더링하고, Activity 경계가 이를 숨길 때 해당 부작용을 방지하고 싶다면 클린업을 위한 return 함수가 있는 Effect를 추가하세요.\n\n가장 흔한 경우는 다음 태그일 것입니다.\n\n  - `<video>`\n  - `<audio>`\n  - `<iframe>`\n\n하지만 일반적으로 대부분의 React 컴포넌트는 이미 Activity 경계에 의해 숨겨지는 것에 대해 견고해야 합니다. 개념적으로 \"숨겨진\" Activity는 마운트 해제된 것으로 생각해야 합니다.\n\n적절한 클린업이 없는 다른 Effect를 미리 발견하려면 [`<StrictMode>`](/reference/react/StrictMode) 사용을 권장합니다. 이는 Activity 경계뿐만 아니라 React의 다른 많은 동작에도 중요합니다.\n\n---\n\n\n### 숨겨진 컴포넌트의 Effect가 실행되지 않습니다 {/*my-hidden-components-have-effects-that-arent-running*/}\n\n`<Activity>`가 \"hidden\" 상태일 때 모든 자식의 Effect가 클린업됩니다. 개념적으로 자식은 마운트 해제되지만, React는 나중을 위해 상태를 저장합니다. 이는 Activity의 기능입니다. 숨겨진 UI 부분에 대한 구독이 활성화되지 않아 숨겨진 콘텐츠에 필요한 작업량이 줄어들기 때문입니다.\n\n컴포넌트의 부작용을 클린업하기 위해 Effect가 마운트되는 것에 의존하고 있다면, 대신 반환된 클린업 함수에서 작업을 수행하도록 Effect를 리팩터링하세요.\n\n문제가 있는 Effect를 미리 찾으려면 [`<StrictMode>`](/reference/react/StrictMode) 추가를 권장합니다. 이는 예상치 못한 부작용을 포착하기 위해 Activity 마운트 해제와 마운트를 미리 수행합니다.\n"
  },
  {
    "path": "src/content/reference/react/Children.md",
    "content": "---\ntitle: Children\n---\n\n<Pitfall>\n\n`Children`을 사용하는 것은 일반적이지 않고 불안정한 코드를 만들 수 있습니다. [일반적으로 사용하는 대안을 살펴보세요.](#alternatives)\n\n</Pitfall>\n\n<Intro>\n\n`Children`을 사용해서 [`children` Prop](/learn/passing-props-to-a-component#passing-jsx-as-children)로 받은 JSX를 조작하고 변환할 수 있습니다.\n\n```js\nconst mappedChildren = Children.map(children, child =>\n  <div className=\"Row\">\n    {child}\n  </div>\n);\n\n```\n\n</Intro>\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `Children.count(children)` {/*children-count*/}\n\n`Children.count(children)`는 `children` 데이터 구조의 자식 요소 수를 반환합니다.\n\n```js src/RowList.js active\nimport { Children } from 'react';\n\nfunction RowList({ children }) {\n  return (\n    <>\n      <h1>Total rows: {Children.count(children)}</h1>\n      ...\n    </>\n  );\n}\n```\n\n[아래 예시 보기](#counting-children).\n\n#### 매개변수 {/*children-count-parameters*/}\n\n* `children`: 컴포넌트에서 받은 [`children` Prop](/learn/passing-props-to-a-component#passing-jsx-as-children)의 값.\n\n#### 반환값 {/*children-count-returns*/}\n\n`children` 내부 노드의 수.\n\n#### 주의 사항 {/*children-count-caveats*/}\n\n- 빈 노드(`null`, `undefined` 혹은 Boolean), 문자열, 숫자, [React 엘리먼트](/reference/react/createElement)는 개별 노드로 간주합니다. 배열 자체는 개별 노드가 아니지만 배열의 자식 요소는 개별 노드로 간주합니다. **React 엘리먼트의 하위 요소는 순회하지 않습니다.** React 엘리먼트는 렌더링 되지 않으며 자식 요소를 순회하지 않습니다. [Fragment](/reference/react/Fragment) 역시 순회하지 않습니다.\n\n---\n\n### `Children.forEach(children, fn, thisArg?)` {/*children-foreach*/}\n\n`Children.forEach(children, fn, thisArg?)`는 `children` 데이터 구조의 모든 자식 요소에 대해 특정 코드를 실행합니다.\n\n```js src/RowList.js active\nimport { Children } from 'react';\n\nfunction SeparatorList({ children }) {\n  const result = [];\n  Children.forEach(children, (child, index) => {\n    result.push(child);\n    result.push(<hr key={index} />);\n  });\n  // ...\n```\n\n[아래 예시 보기](#running-some-code-for-each-child).\n\n#### 매개변수 {/*children-foreach-parameters*/}\n\n* `children`: 컴포넌트에서 받은 [`children` Prop](/learn/passing-props-to-a-component#passing-jsx-as-children)의 값.\n* `fn`: [배열의`forEach` 메서드](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach) 콜백처럼 각 자식 요소에서 실행할 함수. 자식 요소를 첫 번째 인수로, 인덱스를 두 번째 인수로 받습니다. 인덱스는 0에서 시작해서 호출할 때마다 증가합니다.\n* **optional** `thisArg`: `fn` 함수가 호출될 때 사용될 [`this`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this)의 값. 생략 시 `undefined`로 간주합니다.\n\n#### 반환값 {/*children-foreach-returns*/}\n\n`Children.forEach`는 `undefined`를 반환합니다.\n\n#### 주의 사항 {/*children-foreach-caveats*/}\n\n- 빈 노드(`null`, `undefined` 혹은 Boolean), 문자열, 숫자, [React 엘리먼트](/reference/react/createElement)는 개별 노드로 간주합니다. 배열 자체는 개별 노드가 아니지만 배열의 자식 요소는 개별 노드로 간주합니다. **React 엘리먼트의 하위 요소는 순회하지 않습니다.** React 엘리먼트는 렌더링 되지 않으며 자식 요소를 순회하지 않습니다. [Fragments](/reference/react/Fragment) 역시 순회하지 않습니다.\n\n---\n\n### `Children.map(children, fn, thisArg?)` {/*children-map*/}\n\n`Children.map(children, fn, thisArg?)`은 `children` 데이터 구조에서 각 자식 요소를 매핑하거나 변환합니다.\n\n```js src/RowList.js active\nimport { Children } from 'react';\n\nfunction RowList({ children }) {\n  return (\n    <div className=\"RowList\">\n      {Children.map(children, child =>\n        <div className=\"Row\">\n          {child}\n        </div>\n      )}\n    </div>\n  );\n}\n```\n\n[아래 예시 보기](#transforming-children).\n\n#### 매개변수 {/*children-map-parameters*/}\n\n* `children`: 컴포넌트에서 받은 [`children` Prop](/learn/passing-props-to-a-component#passing-jsx-as-children)의 값.\n* `fn`: [베열의 `map` 메서드](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map) 콜백같은 매핑 함수. 자식 요소를 첫 번째 인수로, 인덱스를 두 번째 인수로 받습니다. 인덱스는 0에서 시작해서 호출할 때마다 증가합니다. 함수는 빈 노드(`null`, `undefined` 혹은 Boolean), 문자열, 숫자, React 엘리먼트 혹은 다른 React 노드의 배열과 같은 React 노드를 반환해야 합니다.\n* **optional** `thisArg`: `fn` 함수가 호출될 때 사용될 [`this`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this)의 값. 생략시 `undefined`로 간주합니다.\n\n#### 반환값 {/*children-map-returns*/}\n\n`children`이 `null`이거나 `undefined`일 땐 해당 값을 반환합니다.\n\n\n그렇지 않은 경우 `fn` 함수에서 반환한 노드들로 구성된 평면 배열을 반환합니다. 반환된 배열은 `null`과 `undefined`를 제외하고 반환된 노드를 모두 포함합니다.\n\n#### 주의 사항 {/*children-map-caveats*/}\n\n- 빈 노드(`null`, `undefined` 혹은 Boolean), 문자열, 숫자, [React 엘리먼트](/reference/react/createElement)는 개별 노드로 간주합니다. 배열 자체는 개별 노드가 아니지만 배열의 자식 요소는 개별 노드로 간주합니다. **React 엘리먼트의 하위 요소는 순회하지 않습니다.** React 엘리먼트는 렌더링 되지 않으며 자식 요소를 순회하지 않습니다. [Fragments](/reference/react/Fragment) 역시 순회하지 않습니다.\n\n- `fn`에서 key를 가진 엘리먼트(혹은 엘리먼트의 배열)을 반환하는 경우, **반환된 엘리먼트의 key는 `children`의 원본 항목의 key와 자동으로 결합됩니다.** `fn`에서 배열로 여러 엘리먼트를 반환하는 경우, 각 엘리먼트의 key는 서로 간에만 고유하면 됩니다.\n---\n\n### `Children.only(children)` {/*children-only*/}\n\n\n`Children.only(children)`은 `children`이 단일 React 엘리먼트인지 확인합니다.\n\n```js\nfunction Box({ children }) {\n  const element = Children.only(children);\n  // ...\n```\n\n#### 매개변수 {/*children-only-parameters*/}\n\n* `children`: 컴포넌트에서 받은 [`children` Prop](/learn/passing-props-to-a-component#passing-jsx-as-children)의 값.\n\n#### 반환값 {/*children-only-returns*/}\n\n`children`이 [유효한 엘리먼트](/reference/react/isValidElement)라면 그 엘리먼트를 반환합니다.\n그렇지 않다면, 에러를 던집니다.\n\n#### 주의 사항 {/*children-only-caveats*/}\n\n- 이 메서드는 **`children`으로 배열 (예를 들어 `Children.map`의 반환 값)을 넘기면 항상 예외를 발생시킵니다.** 다시 말해 `children`은 단일 엘리먼트의 배열이 아니라 단일 React 엘리먼트여야 합니다.\n\n---\n\n### `Children.toArray(children)` {/*children-toarray*/}\n\n`Children.toArray(children)`은 `children` 데이터 구조로부터 배열을 생성합니다.\n\n```js src/ReversedList.js active\nimport { Children } from 'react';\n\nexport default function ReversedList({ children }) {\n  const result = Children.toArray(children);\n  result.reverse();\n  // ...\n```\n\n#### 매개변수 {/*children-toarray-parameters*/}\n\n* `children`: 컴포넌트에서 받은 [`children` Prop](/learn/passing-props-to-a-component#passing-jsx-as-children)의 값.\n\n#### 반환값 {/*children-toarray-returns*/}\n\n`children`에 속한 엘리먼트를 평면 배열로 반환합니다.\n\n#### 주의 사항 {/*children-toarray-caveats*/}\n\n\n- 빈 노드(`null`, `undefined`, 혹은 Boolean)는 반환된 배열에서 생략됩니다. **반환된 엘리먼트의 key는 기존 엘리먼트의 key, 중첩 수준과 위치를 기준으로 계산되므로** 배열을 평면화하더라도 동작이 변경되지 않습니다.\n\n---\n\n## 사용법 {/*usage*/}\n\n### `children` 변환하기 {/*transforming-children*/}\n\n`Children.map`은 [`children` Prop로 받은](/learn/passing-props-to-a-component#passing-jsx-as-children) JSX를 변환합니다.\n\n```js {6,10}\nimport { Children } from 'react';\n\nfunction RowList({ children }) {\n  return (\n    <div className=\"RowList\">\n      {Children.map(children, child =>\n        <div className=\"Row\">\n          {child}\n        </div>\n      )}\n    </div>\n  );\n}\n```\n\n위 예시에서 `RowList`는 모든 자식 요소를 `<div className=\"Row\">`로 감싸줍니다. 부모 컴포넌트가 `RowList`에 세 개의 `<p>` 태그를 `children` Prop로 넘겨준다고 가정해 봅시다.\n\n```js\n<RowList>\n  <p>This is the first item.</p>\n  <p>This is the second item.</p>\n  <p>This is the third item.</p>\n</RowList>\n```\n\n위에 나온 `RowList` 구현을 통해 렌더링 된 최종 결과는 다음과 같습니다.\n\n```js\n<div className=\"RowList\">\n  <div className=\"Row\">\n    <p>This is the first item.</p>\n  </div>\n  <div className=\"Row\">\n    <p>This is the second item.</p>\n  </div>\n  <div className=\"Row\">\n    <p>This is the third item.</p>\n  </div>\n</div>\n```\n\n`Children.map`은 [`map()`을 사용해 배열을 변환](/learn/rendering-lists)하는 것과 유사하지만, `children` 데이터 구조가 *불분명하게* 취급된다는 차이가 있습니다. 즉, 배열일 수 있다고 하더라도 항상 배열이거나 다른 특정한 데이터 타입일 것이라고 가정해서는 안 됩니다. 그렇기 때문에 `children`을 변환할 때는 `Children.map`을 사용해야 합니다.\n\n<Sandpack>\n\n```js\nimport RowList from './RowList.js';\n\nexport default function App() {\n  return (\n    <RowList>\n      <p>This is the first item.</p>\n      <p>This is the second item.</p>\n      <p>This is the third item.</p>\n    </RowList>\n  );\n}\n```\n\n```js src/RowList.js active\nimport { Children } from 'react';\n\nexport default function RowList({ children }) {\n  return (\n    <div className=\"RowList\">\n      {Children.map(children, child =>\n        <div className=\"Row\">\n          {child}\n        </div>\n      )}\n    </div>\n  );\n}\n```\n\n```css\n.RowList {\n  display: flex;\n  flex-direction: column;\n  border: 2px solid grey;\n  padding: 5px;\n}\n\n.Row {\n  border: 2px dashed black;\n  padding: 5px;\n  margin: 5px;\n}\n```\n\n</Sandpack>\n\n<DeepDive>\n\n#### `children` Prop는 왜 항상 배열이 아닌가요? {/*why-is-the-children-prop-not-always-an-array*/}\n\nReact에서 `children` Prop는 *불분명한* 데이터 구조로 취급됩니다. `children`이 구조화된 방식에 의존할 수 없다는 의미입니다. 변환하거나 필터링하거나 개수를 세기 위해서는 `Children` 메서드를 사용해야 합니다.\n\n실제로 `children` 데이터 구조는 내부적으로 배열로 표현되고는 합니다. 그러나 만약 하나의 자식만 있다면 React는 불필요한 메모리 오버헤드를 방지하기 위해 배열을 추가로 생성하지 않습니다. `children` Prop에 직접 접근하지 않고 `Children` 메서드를 사용한다면, 실제로 React가 데이터 구조를 어떻게 구현했더라도 코드는 깨지지 않습니다.\n\n`children`이 배열이더라도 `Children.map`을 유용하게 쓸 수 있습니다. 예를 들어 `Children.map`은 반환된 엘리먼트의 [key](/learn/rendering-lists#keeping-list-items-in-order-with-key)를 전달받은 `children`의 key와 병합합니다. 이를 통해 위 예시처럼 감싸지더라도 원본 JSX 자식 요소는 key를 잃어버리지 않습니다.\n\n</DeepDive>\n\n<Pitfall>\n\n`children` 데이터 구조는 JSX로 전달된 컴포넌트의 **렌더링 결과를 포함하지 않습니다.** 아래 예시에서 `RowList`가 받은 `children`에는 세 개가 아닌 두 개의 아이템만 포함합니다.\n\n1. `<p>This is the first item.</p>`\n2. `<MoreRows />`\n\n그렇기 때문에 아래 예시에서는 두 개의 래퍼만 생성합니다.\n\n<Sandpack>\n\n```js\nimport RowList from './RowList.js';\n\nexport default function App() {\n  return (\n    <RowList>\n      <p>This is the first item.</p>\n      <MoreRows />\n    </RowList>\n  );\n}\n\nfunction MoreRows() {\n  return (\n    <>\n      <p>This is the second item.</p>\n      <p>This is the third item.</p>\n    </>\n  );\n}\n```\n\n```js src/RowList.js\nimport { Children } from 'react';\n\nexport default function RowList({ children }) {\n  return (\n    <div className=\"RowList\">\n      {Children.map(children, child =>\n        <div className=\"Row\">\n          {child}\n        </div>\n      )}\n    </div>\n  );\n}\n```\n\n```css\n.RowList {\n  display: flex;\n  flex-direction: column;\n  border: 2px solid grey;\n  padding: 5px;\n}\n\n.Row {\n  border: 2px dashed black;\n  padding: 5px;\n  margin: 5px;\n}\n```\n\n</Sandpack>\n\n`children`을 조작할 때 `<MoreRows />`와 같은 **내부 컴포넌트의 렌더링 된 결과에 접근할 방법은 없습니다.** 그렇기 때문에 [대안을 사용하는 것이 좋습니다.](#alternatives)\n\n</Pitfall>\n\n---\n\n### 각 자식 요소에서 코드 실행하기 {/*running-some-code-for-each-child*/}\n\n`Children.forEach`는 `children` 데이터 구조의 각 자식 요소를 반복합니다. 반환되는 값은 없고 [배열의 `forEach` 메서드](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach)와 유사합니다. 자체 배열을 구성하는 등 커스텀 로직을 실행할 때 사용할 수 있습니다.\n\n<Sandpack>\n\n```js\nimport SeparatorList from './SeparatorList.js';\n\nexport default function App() {\n  return (\n    <SeparatorList>\n      <p>This is the first item.</p>\n      <p>This is the second item.</p>\n      <p>This is the third item.</p>\n    </SeparatorList>\n  );\n}\n```\n\n```js src/SeparatorList.js active\nimport { Children } from 'react';\n\nexport default function SeparatorList({ children }) {\n  const result = [];\n  Children.forEach(children, (child, index) => {\n    result.push(child);\n    result.push(<hr key={index} />);\n  });\n  result.pop(); // Remove the last separator\n  return result;\n}\n```\n\n</Sandpack>\n\n<Pitfall>\n\n위에서 언급했듯 `children`을 조작할 때 내부 컴포넌트의 렌더링 된 결과에 접근할 방법은 없습니다. 그렇기 때문에 [대안을 사용하는 것이 좋습니다.](#alternatives)\n\n</Pitfall>\n\n---\n\n### `children` 카운팅하기 {/*counting-children*/}\n\n`Children.count(children)`는 자식 요소의 수를 계산합니다.\n\n<Sandpack>\n\n```js\nimport RowList from './RowList.js';\n\nexport default function App() {\n  return (\n    <RowList>\n      <p>This is the first item.</p>\n      <p>This is the second item.</p>\n      <p>This is the third item.</p>\n    </RowList>\n  );\n}\n```\n\n```js src/RowList.js active\nimport { Children } from 'react';\n\nexport default function RowList({ children }) {\n  return (\n    <div className=\"RowList\">\n      <h1 className=\"RowListHeader\">\n        Total rows: {Children.count(children)}\n      </h1>\n      {Children.map(children, child =>\n        <div className=\"Row\">\n          {child}\n        </div>\n      )}\n    </div>\n  );\n}\n```\n\n```css\n.RowList {\n  display: flex;\n  flex-direction: column;\n  border: 2px solid grey;\n  padding: 5px;\n}\n\n.RowListHeader {\n  padding-top: 5px;\n  font-size: 25px;\n  font-weight: bold;\n  text-align: center;\n}\n\n.Row {\n  border: 2px dashed black;\n  padding: 5px;\n  margin: 5px;\n}\n```\n\n</Sandpack>\n\n<Pitfall>\n\n위에서 언급했듯 `children`을 조작할 때 내부 컴포넌트의 렌더링 된 결과에 접근할 방법은 없습니다. 그렇기 때문에 [대안을 사용하는 것이 좋습니다.](#alternatives)\n\n</Pitfall>\n\n---\n\n### `children` 배열로 병합하기 {/*converting-children-to-an-array*/}\n\n`Children.toArray(children)`는 `children` 데이터 구조를 일반적인 자바스크립트 배열로 변경합니다. 이것을 사용해서 [`filter`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter), [`sort`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort), [`reverse`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reverse)와 같은 배열의 내장 메서드를 조작할 수 있습니다.\n\n<Sandpack>\n\n```js\nimport ReversedList from './ReversedList.js';\n\nexport default function App() {\n  return (\n    <ReversedList>\n      <p>This is the first item.</p>\n      <p>This is the second item.</p>\n      <p>This is the third item.</p>\n    </ReversedList>\n  );\n}\n```\n\n```js src/ReversedList.js active\nimport { Children } from 'react';\n\nexport default function ReversedList({ children }) {\n  const result = Children.toArray(children);\n  result.reverse();\n  return result;\n}\n```\n\n</Sandpack>\n\n<Pitfall>\n\n위에서 언급했듯 `children`을 조작할 때 내부 컴포넌트의 렌더링 된 결과에 접근할 방법은 없습니다. 그렇기 때문에 [대안을 사용하는 것이 좋습니다.](#alternatives)\n\n</Pitfall>\n\n---\n\n## 대안 {/*alternatives*/}\n\n<Note>\n\n아래와 같이 사용되는 `Children`(대문자 `C`) API의 대안에 대한 설명입니다.\n\n```js\nimport { Children } from 'react';\n```\n\n소문자 `c`인 [`children` Prop](/learn/passing-props-to-a-component#passing-jsx-as-children)와 혼동해서는 안 됩니다. [`children` Prop](/learn/passing-props-to-a-component#passing-jsx-as-children)는 좋은 방법이고 권장되는 방식입니다.\n\n</Note>\n\n### 여러 컴포넌트 노출하기 {/*exposing-multiple-components*/}\n\n`Children` 메서드로 자식 요소를 조작하는 코드는 취약할 수 있습니다. JSX에서 컴포넌트에 자식 요소를 전달할 때 해당 컴포넌트가 개별 자식 요소를 조작하거나 변환될 수 있기 때문입니다.\n\n가능한 한 `Children` 메서드는 사용하지 않는 것이 좋습니다. 예를 들어 `RowList`의 각 자식 요소를 `<div className=\"Row\">`로 감싸려면, `Row` 컴포넌트를 내보내고 다음과 같이 각 row를 직접 감싸는 것을 권장합니다.\n\n<Sandpack>\n\n```js\nimport { RowList, Row } from './RowList.js';\n\nexport default function App() {\n  return (\n    <RowList>\n      <Row>\n        <p>This is the first item.</p>\n      </Row>\n      <Row>\n        <p>This is the second item.</p>\n      </Row>\n      <Row>\n        <p>This is the third item.</p>\n      </Row>\n    </RowList>\n  );\n}\n```\n\n```js src/RowList.js\nexport function RowList({ children }) {\n  return (\n    <div className=\"RowList\">\n      {children}\n    </div>\n  );\n}\n\nexport function Row({ children }) {\n  return (\n    <div className=\"Row\">\n      {children}\n    </div>\n  );\n}\n```\n\n```css\n.RowList {\n  display: flex;\n  flex-direction: column;\n  border: 2px solid grey;\n  padding: 5px;\n}\n\n.Row {\n  border: 2px dashed black;\n  padding: 5px;\n  margin: 5px;\n}\n```\n\n</Sandpack>\n\n`Children.map`과 달리 이 방식은 모든 자식 요소를 자동으로 감싸지 않습니다. **그러나 [`Children.map`의 앞선 예시](#transforming-children)와 달리 더 많은 컴포넌트를 추출해도 동작한다는 중요한 이점이 있습니다.**\n예를 들어 `MoreRows` 컴포넌트를 직접 추출하더라도 동작합니다.\n\n<Sandpack>\n\n```js\nimport { RowList, Row } from './RowList.js';\n\nexport default function App() {\n  return (\n    <RowList>\n      <Row>\n        <p>This is the first item.</p>\n      </Row>\n      <MoreRows />\n    </RowList>\n  );\n}\n\nfunction MoreRows() {\n  return (\n    <>\n      <Row>\n        <p>This is the second item.</p>\n      </Row>\n      <Row>\n        <p>This is the third item.</p>\n      </Row>\n    </>\n  );\n}\n```\n\n```js src/RowList.js\nexport function RowList({ children }) {\n  return (\n    <div className=\"RowList\">\n      {children}\n    </div>\n  );\n}\n\nexport function Row({ children }) {\n  return (\n    <div className=\"Row\">\n      {children}\n    </div>\n  );\n}\n```\n\n```css\n.RowList {\n  display: flex;\n  flex-direction: column;\n  border: 2px solid grey;\n  padding: 5px;\n}\n\n.Row {\n  border: 2px dashed black;\n  padding: 5px;\n  margin: 5px;\n}\n```\n\n</Sandpack>\n\n`<MoreRows />`가 단일 자식 요소이자 단일 row로 취급되기 때문에 `Children.map`은 동작하지 않습니다.\n\n---\n\n### Prop로 객체 배열 받기 {/*accepting-an-array-of-objects-as-a-prop*/}\n\nProp로 명시적으로 배열을 전달할 수 있습니다. 예를 들어, 아래 예시에서 `RowList`는 `rows` 배열을 Prop로 받습니다.\n\n<Sandpack>\n\n```js\nimport { RowList, Row } from './RowList.js';\n\nexport default function App() {\n  return (\n    <RowList rows={[\n      { id: 'first', content: <p>This is the first item.</p> },\n      { id: 'second', content: <p>This is the second item.</p> },\n      { id: 'third', content: <p>This is the third item.</p> }\n    ]} />\n  );\n}\n```\n\n```js src/RowList.js\nexport function RowList({ rows }) {\n  return (\n    <div className=\"RowList\">\n      {rows.map(row => (\n        <div className=\"Row\" key={row.id}>\n          {row.content}\n        </div>\n      ))}\n    </div>\n  );\n}\n```\n\n```css\n.RowList {\n  display: flex;\n  flex-direction: column;\n  border: 2px solid grey;\n  padding: 5px;\n}\n\n.Row {\n  border: 2px dashed black;\n  padding: 5px;\n  margin: 5px;\n}\n```\n\n</Sandpack>\n\n`rows`는 일반적인 자바스크립트 배열이기 때문에, `RowList` 컴포넌트는 [`map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map)과 같은 내장 배열 메서드를 사용할 수 있습니다.\n\n이 패턴은 구조화된 데이터를 자식 요소와 함께 전달할 때 더 유용합니다. 아래 예시에서 `TabSwitcher` 컴포넌트는 `tabs` Prop로 객체 배열을 받습니다.\n\n<Sandpack>\n\n```js\nimport TabSwitcher from './TabSwitcher.js';\n\nexport default function App() {\n  return (\n    <TabSwitcher tabs={[\n      {\n        id: 'first',\n        header: 'First',\n        content: <p>This is the first item.</p>\n      },\n      {\n        id: 'second',\n        header: 'Second',\n        content: <p>This is the second item.</p>\n      },\n      {\n        id: 'third',\n        header: 'Third',\n        content: <p>This is the third item.</p>\n      }\n    ]} />\n  );\n}\n```\n\n```js src/TabSwitcher.js\nimport { useState } from 'react';\n\nexport default function TabSwitcher({ tabs }) {\n  const [selectedId, setSelectedId] = useState(tabs[0].id);\n  const selectedTab = tabs.find(tab => tab.id === selectedId);\n  return (\n    <>\n      {tabs.map(tab => (\n        <button\n          key={tab.id}\n          onClick={() => setSelectedId(tab.id)}\n        >\n          {tab.header}\n        </button>\n      ))}\n      <hr />\n      <div key={selectedId}>\n        <h3>{selectedTab.header}</h3>\n        {selectedTab.content}\n      </div>\n    </>\n  );\n}\n```\n\n</Sandpack>\n\nJSX로 자식 요소를 전달할 때와 달리 이런 방식은 각 아이템에 `header`와 같은 추가 정보를 연결할 수 있습니다. `tabs`가 배열 구조이고 직접 다뤄지기 때문에 `Children` 메서드는 필요하지 않습니다.\n\n---\n\n### 렌더링 Prop로 렌더링 커스텀하기 {/*calling-a-render-prop-to-customize-rendering*/}\n\n모든 개별 항목에 대해 JSX를 생성하는 대신 JSX를 반환하는 함수를 전달하고 필요할 때 해당 함수를 호출할 수도 있습니다. 아래 예시에서 `App` 컴포넌트는 `renderContent` 함수를 `TabSwitcher` 컴포넌트에 전달합니다. `TabSwitcher` 컴포넌트는 선택된 탭에 대해서만 `renderContent`를 호출합니다.\n\n<Sandpack>\n\n```js\nimport TabSwitcher from './TabSwitcher.js';\n\nexport default function App() {\n  return (\n    <TabSwitcher\n      tabIds={['first', 'second', 'third']}\n      getHeader={tabId => {\n        return tabId[0].toUpperCase() + tabId.slice(1);\n      }}\n      renderContent={tabId => {\n        return <p>This is the {tabId} item.</p>;\n      }}\n    />\n  );\n}\n```\n\n```js src/TabSwitcher.js\nimport { useState } from 'react';\n\nexport default function TabSwitcher({ tabIds, getHeader, renderContent }) {\n  const [selectedId, setSelectedId] = useState(tabIds[0]);\n  return (\n    <>\n      {tabIds.map((tabId) => (\n        <button\n          key={tabId}\n          onClick={() => setSelectedId(tabId)}\n        >\n          {getHeader(tabId)}\n        </button>\n      ))}\n      <hr />\n      <div key={selectedId}>\n        <h3>{getHeader(selectedId)}</h3>\n        {renderContent(selectedId)}\n      </div>\n    </>\n  );\n}\n```\n\n</Sandpack>\n\n`renderContent`와 같이 사용자 인터페이스의 일부를 어떻게 렌더링할지 정의하는 Prop를 *렌더링 Prop*라고 합니다. 하지만 특별한 것은 아닙니다. 단지 일반적인 함수의 Prop일 뿐입니다.\n\n렌더링 Prop은은 함수이므로 정보를 전달할 수 있습니다.\n아래 예시에서 `RowList` 컴포넌트는 각 row의 `id`와 `index`를 `renderRow`에 렌더링 Prop로 전달하고, `index`가 짝수인 row를 강조합니다.\n\n<Sandpack>\n\n```js\nimport { RowList, Row } from './RowList.js';\n\nexport default function App() {\n  return (\n    <RowList\n      rowIds={['first', 'second', 'third']}\n      renderRow={(id, index) => {\n        return (\n          <Row isHighlighted={index % 2 === 0}>\n            <p>This is the {id} item.</p>\n          </Row>\n        );\n      }}\n    />\n  );\n}\n```\n\n```js src/RowList.js\nimport { Fragment } from 'react';\n\nexport function RowList({ rowIds, renderRow }) {\n  return (\n    <div className=\"RowList\">\n      <h1 className=\"RowListHeader\">\n        Total rows: {rowIds.length}\n      </h1>\n      {rowIds.map((rowId, index) =>\n        <Fragment key={rowId}>\n          {renderRow(rowId, index)}\n        </Fragment>\n      )}\n    </div>\n  );\n}\n\nexport function Row({ children, isHighlighted }) {\n  return (\n    <div className={[\n      'Row',\n      isHighlighted ? 'RowHighlighted' : ''\n    ].join(' ')}>\n      {children}\n    </div>\n  );\n}\n```\n\n```css\n.RowList {\n  display: flex;\n  flex-direction: column;\n  border: 2px solid grey;\n  padding: 5px;\n}\n\n.RowListHeader {\n  padding-top: 5px;\n  font-size: 25px;\n  font-weight: bold;\n  text-align: center;\n}\n\n.Row {\n  border: 2px dashed black;\n  padding: 5px;\n  margin: 5px;\n}\n\n.RowHighlighted {\n  background: #ffa;\n}\n```\n\n</Sandpack>\n\n부모 컴포넌트와 자식 컴포넌트가 자식 요소를 직접 조작하지 않고도 효과적으로 협력할 수 있는 좋은 예시입니다.\n\n---\n\n## 문제 해결 {/*troubleshooting*/}\n\n### 커스텀 컴포넌트를 전달했을 때 `Children` 메서드가 렌더링 결과를 보여주지 않는 경우 {/*i-pass-a-custom-component-but-the-children-methods-dont-show-its-render-result*/}\n\n`RowList`에 두 개의 자식 요소를 아래와 같이 전달했다고 가정해 봅시다.\n\n```js\n<RowList>\n  <p>First item</p>\n  <MoreRows />\n</RowList>\n```\n\n`RowList` 내부에서 `Children.count(children)`를 실행시킨다면 결과는 `2`입니다. `MoreRows`가 10개의 다른 요소를 렌더링하거나 `null`을 반환하더라도, `Children.count(children)`는 `2`입니다. `RowList` 관점에서는 그것이 받은 JSX만 고려할 뿐 `MoreRows` 컴포넌트의 내부는 고려하지 않습니다.\n\n이런 제약때문에 컴포넌트를 추출하기 어려울 수 있습니다. 그렇기 때문에 `Children` 대신 [대안](#alternatives)을 사용하는 것이 좋습니다.\n"
  },
  {
    "path": "src/content/reference/react/Component.md",
    "content": "---\ntitle: Component\n---\n\n<Pitfall>\n\n컴포넌트를 클래스 대신 함수로 정의하는 것을 추천합니다. [마이그레이션 방법을 확인하세요.](#alternatives)\n\n</Pitfall>\n\n<Intro>\n\n`Component`는 [자바스크립트 클래스](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Classes)로 정의된 React 컴포넌트의 기본 클래스입니다. React에서 클래스 컴포넌트를 계속 지원하지만, 새 코드에서는 사용하지 않는 것을 추천합니다.\n\n```js\nclass Greeting extends Component {\n  render() {\n    return <h1>Hello, {this.props.name}!</h1>;\n  }\n}\n```\n\n</Intro>\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `Component` {/*component*/}\n\nReact 컴포넌트를 클래스로 정의하려면, 내장 `Component` 클래스를 확장하고 [`render` 메서드](#render)를 정의하세요.\n\n```js\nimport { Component } from 'react';\n\nclass Greeting extends Component {\n  render() {\n    return <h1>Hello, {this.props.name}!</h1>;\n  }\n}\n```\n\n`render` 메서드만 필수고 다른 메서드는 선택 사항입니다.\n\n[아래의 더 많은 예시를 확인하세요.](#usage)\n\n---\n\n### `context` {/*context*/}\n\n클래스 컴포넌트의 [context](/learn/passing-data-deeply-with-context)는 `this.context`로 사용할 수 있습니다. [`static contextType`](#static-contexttype)를 사용하여 *어떤* context를 받을지 지정해야만 사용할 수 있습니다.\n\n클래스 컴포넌트는 한 번에 하나의 context만 읽을 수 있습니다.\n\n```js {2,5}\nclass Button extends Component {\n  static contextType = ThemeContext;\n\n  render() {\n    const theme = this.context;\n    const className = 'button-' + theme;\n    return (\n      <button className={className}>\n        {this.props.children}\n      </button>\n    );\n  }\n}\n\n```\n\n<Note>\n\n클래스 컴포넌트에서 `this.context`를 읽는 것은 함수 컴포넌트에서 [`useContext`](/reference/react/useContext)와 같습니다.\n\n[마이그레이션 방법을 확인하세요.](#migrating-a-component-with-context-from-a-class-to-a-function)\n\n</Note>\n\n---\n\n### `props` {/*props*/}\n\n클래스 컴포넌트에 전달되는 props는 `this.props`로 사용할 수 있습니다.\n\n```js {3}\nclass Greeting extends Component {\n  render() {\n    return <h1>Hello, {this.props.name}!</h1>;\n  }\n}\n\n<Greeting name=\"Taylor\" />\n```\n\n<Note>\n\n클래스 컴포넌트에서 `this.props`를 읽는 것은 함수 컴포넌트에서 [props를 선언하는 것](/learn/passing-props-to-a-component#step-2-read-props-inside-the-child-component)과 같습니다.\n\n[마이그레이션 방법을 확인하세요.](#migrating-a-simple-component-from-a-class-to-a-function)\n\n</Note>\n\n---\n\n### `state` {/*state*/}\n\n클래스 컴포넌트의 state는 `this.state`로 사용할 수 있습니다. `state` 필드는 반드시 객체여야합니다. state를 직접 변경하지 마세요. state를 변경하려면 새 state로 `setState`를 호출하세요.\n\n```js {2-4,7-9,18}\nclass Counter extends Component {\n  state = {\n    age: 42,\n  };\n\n  handleAgeChange = () => {\n    this.setState({\n      age: this.state.age + 1\n    });\n  };\n\n  render() {\n    return (\n      <>\n        <button onClick={this.handleAgeChange}>\n        Increment age\n        </button>\n        <p>You are {this.state.age}.</p>\n      </>\n    );\n  }\n}\n```\n\n<Note>\n\n클래스 컴포넌트에서 `state`를 정의하는 것은 함수 컴포넌트에서 [`useState`](/reference/react/useState)를 호출하는 것과 같습니다.\n\n[마이그레이션 방법을 확인하세요.](#migrating-a-component-with-state-from-a-class-to-a-function)\n\n</Note>\n\n---\n\n### `constructor(props)` {/*constructor*/}\n\n클래스 컴포넌트가 *마운트*(화면에 추가됨)되기 전에 [constructor](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Classes/constructor)가 실행됩니다. 일반적으로 constructor는 React에서 두 가지 목적으로만 사용됩니다. state를 선언하고 클래스 메서드를 클래스 인스턴스에 [바인딩](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_objects/Function/bind)할 수 있습니다.\n\n```js {2-6}\nclass Counter extends Component {\n  constructor(props) {\n    super(props);\n    this.state = { counter: 0 };\n    this.handleClick = this.handleClick.bind(this);\n  }\n\n  handleClick() {\n    // ...\n  }\n```\n\n최신 자바스크립트 문법을 사용한다면 constructor는 거의 필요하지 않습니다. 대신 최신 브라우저와 [Babel](https://babeljs.io/)과 같은 도구에서 모두 지원되는 [공용 클래스 필드 문법](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Classes/Public_class_fields)을 사용하여 위의 코드를 다시 작성할 수 있습니다.\n\n```js {2,4}\nclass Counter extends Component {\n  state = { counter: 0 };\n\n  handleClick = () => {\n    // ...\n  }\n```\n\nconstructor는 부수 효과 또는 구독을 포함하면 안됩니다.\n\n#### 매개변수 {/*constructor-parameters*/}\n\n* `props`: 컴포넌트의 초기 props.\n\n#### 반환값 {/*constructor-returns*/}\n\n`constructor`는 아무것도 반환하면 안 됩니다.\n\n#### 주의 사항 {/*constructor-caveats*/}\n\n* constructor에서 부수 효과 또는 구독을 실행하지 마세요. 대신 [`componentDidMount`](#componentdidmount)를 사용하세요.\n\n* constructor 내부에서는 다른 명령어보다 `super(props)`를 먼저 호출해야 합니다. 그렇게 하지 않으면, constructor가 실행되는 동안 `this.props`는 `undefined`가 되어 혼란스럽고 버그가 발생할 수 있습니다.\n\n* constructor는 [`this.state`](#state)를 직접 할당할 수 있는 유일한 위치입니다. 다른 모든 메서드에서는 [`this.setState()`](#setstate)를 대신 사용해야 합니다. constructor에서 setState를 호출하지 마십시오.\n\n* [서버 렌더링](/reference/react-dom/server)을 사용할 때, constructor는 서버에서 역시 실행되고, 뒤이어 [`render`](#render) 메서드도 실행됩니다. 그러나 `componentDidMount` 또는 `componentWillUnmount`와 같은 수명 주기 메서드는 서버에서 실행되지 않습니다.\n\n* [Strict 모드](/reference/react/StrictMode)가 설정되면 React는 개발 중인 `constructor`를 두 번 호출한 다음 인스턴스 중 하나를 삭제합니다. 이를 통해 `constructor` 외부로 옮겨져야 하는 우발적인 부수 효과를 파악할 수 있습니다.\n\n<Note>\n\n함수 컴포넌트에 `constructor`와 정확히 동등한 것은 없습니다. 함수 컴포넌트에 state를 선언하려면 [`useState`](/reference/react/useState)를 호출하세요. 초기 state를 다시 계산하지 않으려면 [함수를 `useState`에 전달](/reference/react/useState#avoiding-recreating-the-initial-state)하세요.\n\n</Note>\n\n---\n\n### `componentDidCatch(error, info)` {/*componentdidcatch*/}\n\n`componentDidCatch`를 정의하면, 일부 자식 컴포넌트(먼 자식을 포함)가 에러를 발생시킬 때 React가 이를 호출합니다. 이를 통해 운영 중인 에러 보고 서비스에 에러를 기록할 수 있습니다.\n\n일반적으로, 에러에 대한 응답으로 State를 업데이트하고 사용자에게 에러 메시지를 표시할 수 있는 [`static getDerivedStateFromError`](#static-getderivedstatefromerror)와 함께 사용합니다. 이런 여러 메서드를 사용하는 컴포넌트를 *Error Boundary*라고 합니다.\n\n[예시를 확인하세요.](#catching-rendering-errors-with-an-error-boundary)\n\n#### 매개변수 {/*componentdidcatch-parameters*/}\n\n* `error`: 발생한 에러입니다. 실제로, 보통은 [`에러`](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Error)의 인스턴스가 되지만 JavaScript에서 문자열 또는 `null`을 포함한 어떤 값이든 [`throw`](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Statements/throw)할 수 있기 때문에 보장되지 않습니다.\n\n* `info`: 에러에 대한 추가 정보를 포함하는 객체입니다. 이것의 `componentStack` 필드는 모든 부모 컴포넌트의 이름과 출처 위치뿐만 아니라 에러를 throw한 컴포넌트의 stack trace을 포함합니다. 프로덕션에서, 컴포넌트의 이름은 최소화됩니다. 프로덕션 에러 보고를 설정하면 일반 JavaScript 에러 스택과 동일한 방법으로 소스맵을 사용하여 컴포넌트 스택을 디코딩할 수 있습니다.\n\n#### 반환값 {/*componentdidcatch-returns*/}\n\n`componentDidCatch`는 아무것도 반환하면 안 됩니다.\n\n#### 주의 사항 {/*componentdidcatch-caveats*/}\n\n* 과거에는 UI를 업데이트하고 대체 에러 메세지를 표시하기 위해 `setState`를 `componentDidCatch` 안에서 호출하는 것이 일반적이었습니다. 이는 [`static getDerivedStateFromError`](#static-getderivedstatefromerror)를 정의하기 위해 더 이상 사용되지 않습니다.\n\n* React의 프로덕션과 개발 빌드는 `componentDidCatch`가 에러를 처리하는 방식이 약간 다릅니다. 개발에서는, 에러는 `window`까지 버블링될 것이며, 이는 `window.onerror` 또는 `window.addEventListener('error', callback)`가 `componentDidCatch`에 의해 탐지된 에러를 가로챈다는 것을 의미합니다. 대신 프로덕션에서, 에러는 버블링되지 않을 것이며, 이는 어떤 상위의 에러 핸들러가 `componentDidCatch`에 의해 명시적으로 탐지되지 않은 에러만을 수신하는 것을 의미합니다.\n\n<Note>\n\n함수 컴포넌트에 `componentDidCatch`와 직접적으로 동등한 것은 아직 없습니다. 클래스 컴포넌트 생성을 피하려면, 위와 같이 `ErrorBoundary` 컴포넌트를 하나 작성하여 앱 전체에 사용합니다. 그렇지 않으면, 그것을 해주는 [`react-error-boundary`](https://github.com/bvaughn/react-error-boundary) package를 사용할 수 있습니다.\n\n</Note>\n\n---\n\n### `componentDidMount()` {/*componentdidmount*/}\n\n`componentDidMount` 메서드를 정의하면 구성 요소가 화면에 추가 *(마운트)* 될 때 React가 호출합니다. 이것은 데이터 가져오기를 시작하거나, 구독을 설정하거나, DOM 노드를 조작하는 일반적인 장소입니다.\n\n`componentDidMount`를 구현하면 일반적으로 버그를 방지하기 위해 다른 생명주기 메서드를 구현해야 합니다. 예를 들어, `componentDidMount`가 일부 state나 props를 읽는 경우 변경 사항을 처리하기 위해 [`componentDidUpdate`](#componentdidupdate)를 구현하고 `componentDidMount`가 수행하던 작업을 정리하기 위해 [`componentWillUnmount`](#componentwillunmount)도 구현해야 합니다.\n\n```js {6-8}\nclass ChatRoom extends Component {\n  state = {\n    serverUrl: 'https://localhost:1234'\n  };\n\n  componentDidMount() {\n    this.setupConnection();\n  }\n\n  componentDidUpdate(prevProps, prevState) {\n    if (\n      this.props.roomId !== prevProps.roomId ||\n      this.state.serverUrl !== prevState.serverUrl\n    ) {\n      this.destroyConnection();\n      this.setupConnection();\n    }\n  }\n\n  componentWillUnmount() {\n    this.destroyConnection();\n  }\n\n  // ...\n}\n```\n\n[더 많은 예시를 확인하세요.](#adding-lifecycle-methods-to-a-class-component)\n\n#### 매개변수 {/*componentdidmount-parameters*/}\n\n`componentDidMount`는 매개변수를 사용하지 않습니다.\n\n#### 반환값 {/*componentdidmount-returns*/}\n\n`componentDidMount`는 아무것도 반환하면 안 됩니다.\n\n#### 주의 사항 {/*componentdidmount-caveats*/}\n\n- [Strict 모드](/reference/react/StrictMode)가 켜져 있으면 개발 중인 React가 `componentDidMount`를 호출한 다음 [`componentWillUnmount`](#componentwillunmount)를 호출하고 `componentDidMount`를 다시 호출합니다. 이를 통해 `componentWillUnmount`를 구현하는 것을 잊었거나 로직이 `componentDidMount`가 수행하는 작업을 완전히 \"미러링\"하지 않는 경우를 알 수 있습니다.\n\n- `componentDidMount`에서 [`setState`](#setstate)를 즉시 호출할 수 있지만, 가능하면 피하는 것이 가장 좋습니다. 이는 추가 렌더링을 일으키지만 브라우저가 화면을 업데이트하기 전에 일어납니다. 이 경우 [`render`](#render)가 두 번 호출되더라도 사용자는 중간 state를 볼 수 없습니다. 이 패턴은 종종 성능 문제를 발생시키므로 주의하여 사용하십시오. 대부분의 경우 [`constructor`](#constructor)에서 초기 state를 대신 할당할 수 있습니다. 그러나 모달이나 툴팁과 같이 크기나 위치에 따라 달라지는 것을 렌더링하기 전에 DOM 노드를 측정해야 하는 경우에는 필요할 수 있습니다.\n\n<Note>\n\n많은 사용 사례에서 `componentDidMount`, `componentDidUpdate` 및 `componentWillUnmount`를 클래스 컴포넌트에 함께 정의하는 것은 함수 컴포넌트에서 [`useEffect`](/reference/react/useEffect)를 호출하는 것과 같습니다. 드물지만 브라우저 페인트 전에 코드를 실행하는 것이 중요한 경우에는 [`useLayoutEffect`](/reference/react/useLayoutEffect)를 사용하는 것이 더 적합합니다.\n\n[마이그레이션 방법을 확인하세요.](#migrating-a-component-with-lifecycle-methods-from-a-class-to-a-function)\n\n</Note>\n\n---\n\n### `componentDidUpdate(prevProps, prevState, snapshot?)` {/*componentdidupdate*/}\n\n`componentDidUpdate` 메서드를 정의하면 컴포넌트가 업데이트된 props 또는 state로 다시 렌더링된 직후 React가 이 메서드를 호출합니다. 이 메서드는 초기 렌더링에 호출되지 않습니다.\n\n업데이트 후 DOM을 조작하는 데 사용할 수 있습니다. 또한 현재 props를 이전 props와 비교하는 한 네트워크 요청을 수행하는 일반적인 장소이기도 합니다(예: props가 변경되지 않은 경우 네트워크 요청이 필요하지 않을 수 있습니다). 일반적으로 [`componentDidMount`](#componentdidmount) 및 [`componentWillUnmount`](#componentwillunmount)와 함께 사용합니다.\n\n```js {10-18}\nclass ChatRoom extends Component {\n  state = {\n    serverUrl: 'https://localhost:1234'\n  };\n\n  componentDidMount() {\n    this.setupConnection();\n  }\n\n  componentDidUpdate(prevProps, prevState) {\n    if (\n      this.props.roomId !== prevProps.roomId ||\n      this.state.serverUrl !== prevState.serverUrl\n    ) {\n      this.destroyConnection();\n      this.setupConnection();\n    }\n  }\n\n  componentWillUnmount() {\n    this.destroyConnection();\n  }\n\n  // ...\n}\n```\n\n[더 많은 예시를 확인하세요.](#adding-lifecycle-methods-to-a-class-component)\n\n\n#### 매개변수 {/*componentdidupdate-parameters*/}\n\n* `prevProps`: 업데이트 이전의 props. `prevProps`와 [`this.props`](#props)를 비교하여 변경된 내용을 확인합니다.\n\n* `prevState`: 업데이트 전 state. `prevState`를 [`this.state`](#state)와 비교하여 변경된 내용을 확인합니다.\n\n* `snapshot`: [`getSnapshotBeforeUpdate`](#getsnapshotbeforeupdate)를 구현한 경우, `snapshot`에는 해당 메서드에서 반환한 값이 포함됩니다. 그렇지 않으면 `undefined`가 됩니다.\n\n#### 반환값 {/*componentdidupdate-returns*/}\n\n`componentDidUpdate`는 아무것도 반환하지 않아야 합니다.\n\n#### 주의 사항 {/*componentdidupdate-caveats*/}\n\n- [`shouldComponentUpdate`](#shouldcomponentupdate)가 정의되어 있으면 `componentDidUpdate`가 호출되지 않고 `false`를 반환합니다.\n\n- `componentDidUpdate` 내부의 로직은 일반적으로 `this.props`를 `prevProps`와 비교하고 `this.state`를 `prevState`와 비교하는 조건으로 래핑해야 합니다. 그렇지 않으면 무한 루프가 생성될 위험이 있습니다.\n\n- `componentDidUpdate`에서 [`setState`](#setstate)를 즉시 호출할 수도 있지만 가능하면 피하는 것이 가장 좋습니다. 추가 렌더링이 트리거되지만 브라우저가 화면을 업데이트하기 전에 발생합니다. 이렇게 하면 이 경우 [`render`](#render)가 두 번 호출되더라도 사용자에게 중간 state가 표시되지 않습니다. 이 패턴은 종종 성능 문제를 일으키지만 드물게 모달이나 툴팁처럼 크기나 위치에 따라 달라지는 것을 렌더링하기 전에 DOM 노드를 측정해야 하는 경우에 필요할 수 있습니다.\n\n<Note>\n\n많은 사용 사례에서 `componentDidMount`, `componentDidUpdate` 및 `componentWillUnmount`를 클래스 컴포넌트에 함께 정의하는 것은 함수 컴포넌트에서 [`useEffect`](/reference/react/useEffect)를 호출하는 것과 같습니다. 드물지만 브라우저 페인트 전에 코드를 실행하는 것이 중요한 경우에는 [`useLayoutEffect`](/reference/react/useLayoutEffect)를 사용하는 것이 더 적합합니다.\n\n[마이그레이션 방법을 확인하세요.](#migrating-a-component-with-lifecycle-methods-from-a-class-to-a-function)\n\n</Note>\n---\n\n### `componentWillMount()` {/*componentwillmount*/}\n\n<Deprecated>\n\n이 API의 이름이 `componentWillMount`에서 [`UNSAFE_componentWillMount`](#unsafe_componentwillmount)로 변경되었습니다. 이전 이름은 더 이상 사용되지 않습니다. 향후 React의 주요 버전에서는 새로운 이름만 작동합니다.\n\n컴포넌트를 자동으로 업데이트하려면 [`rename-unsafe-lifecycles` codemod](https://github.com/reactjs/react-codemod#rename-unsafe-lifecycles)를 실행하세요.\n\n</Deprecated>\n\n---\n\n### `componentWillReceiveProps(nextProps)` {/*componentwillreceiveprops*/}\n\n<Deprecated>\n\n이 API의 이름이 `componentWillReceiveProps`에서 [`UNSAFE_componentWillReceiveProps`](#unsafe_componentwillreceiveprops)로 변경되었습니다. 이전 이름은 더 이상 사용되지 않습니다. 향후 React의 주요 버전에서는 새로운 이름만 작동합니다.\n\n컴포넌트를 자동으로 업데이트하려면 [`rename-unsafe-lifecycles` codemod](https://github.com/reactjs/react-codemod#rename-unsafe-lifecycles)를 실행하세요.\n\n</Deprecated>\n\n---\n\n### `componentWillUpdate(nextProps, nextState)` {/*componentwillupdate*/}\n\n<Deprecated>\n\n이 API의 이름이 `componentWillUpdate`에서 [`UNSAFE_componentWillUpdate`](#unsafe_componentwillupdate)로 변경되었습니다. 이전 이름은 더 이상 사용되지 않습니다. 향후 React의 주요 버전에서는 새로운 이름만 작동합니다.\n\n컴포넌트를 자동으로 업데이트하려면 [`rename-unsafe-lifecycles` codemod](https://github.com/reactjs/react-codemod#rename-unsafe-lifecycles)를 실행하세요.\n\n</Deprecated>\n\n---\n\n### `componentWillUnmount()` {/*componentwillunmount*/}\n\n`componentWillUnmount` 메서드를 정의하면 React는 컴포넌트가 화면에서 제거 *(마운트 해제)* 되기 전에 이 메서드를 호출합니다. 이는 데이터 불러오기를 취소하거나 구독을 제거하는 일반적인 장소입니다.\n\n`componentWillUnmount` 내부의 로직은 [`componentDidMount`](#componentdidmount) 내부의 로직을 \"미러링\"해야 합니다. 예를 들어 `componentDidMount`가 구독을 설정하면 `componentWillUnmount`는 해당 구독을 정리해야 합니다. `componentWillUnmount`의 정리 로직이 일부 props나 state를 읽는 경우, 일반적으로 이전 props나 state에 해당하는 리소스(예: 구독)를 정리하기 위해 [`componentDidUpdate`](#componentdidupdate)도 구현해야 합니다.\n\n```js {20-22}\nclass ChatRoom extends Component {\n  state = {\n    serverUrl: 'https://localhost:1234'\n  };\n\n  componentDidMount() {\n    this.setupConnection();\n  }\n\n  componentDidUpdate(prevProps, prevState) {\n    if (\n      this.props.roomId !== prevProps.roomId ||\n      this.state.serverUrl !== prevState.serverUrl\n    ) {\n      this.destroyConnection();\n      this.setupConnection();\n    }\n  }\n\n  componentWillUnmount() {\n    this.destroyConnection();\n  }\n\n  // ...\n}\n```\n\n[더 많은 예시를 확인하세요.](#adding-lifecycle-methods-to-a-class-component)\n\n#### 매개변수 {/*componentwillunmount-parameters*/}\n\n`componentWillUnmount`는 어떤 매개변수도 받지 않습니다.\n\n#### 반환값 {/*componentwillunmount-returns*/}\n\n`componentWillUnmount`는 아무것도 반환하지 않아야 합니다.\n\n#### 주의 사항 {/*componentwillunmount-caveats*/}\n\n- [Strict 모드](/reference/react/StrictMode)가 켜져 있으면 개발 시 React는 [`componentDidMount`](#componentdidmount)를 호출한 다음 즉시 `componentWillUnmount`를 호출한 다음 `componentDidMount`를 다시 호출합니다. 이렇게 하면 `componentWillUnmount`를 구현하는 것을 잊어버렸거나 그 로직이 `componentDidMount`의 동작을 완전히 \"미러링\"하지 않는지 확인할 수 있습니다.\n\n<Note>\n\n많은 사용 사례에서 `componentDidMount`, `componentDidUpdate` 및 `componentWillUnmount`를 클래스 컴포넌트에 함께 정의하는 것은 함수 컴포넌트에서 [`useEffect`](/reference/react/useEffect)를 호출하는 것과 같습니다. 드물지만 브라우저 페인트 전에 코드를 실행하는 것이 중요한 경우에는 [`useLayoutEffect`](/reference/react/useLayoutEffect)를 사용하는 것이 더 적합합니다.\n\n[마이그레이션 방법을 확인하세요.](#migrating-a-component-with-lifecycle-methods-from-a-class-to-a-function)\n\n</Note>\n\n---\n\n### `forceUpdate(callback?)` {/*forceupdate*/}\n\n컴포넌트를 강제로 다시 렌더링합니다.\n\n일반적으로는 필요하지 않습니다. 컴포넌트의 [`render`](#render) 메서드가 [`this.props`](#props), [`this.state`](#state) 또는 [`this.context`](#context)에서만 읽는 경우, 컴포넌트 내부 또는 부모 중 하나에서 [`setState`](#setstate)를 호출하면 자동으로 다시 렌더링됩니다. 하지만 컴포넌트의 `render` 메서드가 외부 데이터 소스로부터 직접 읽어오는 경우, 데이터 소스가 변경될 때 사용자 인터페이스를 업데이트하도록 React에 지시해야 합니다. 이것이 바로 `forceUpdate`가 할 수 있는 일입니다.\n\n`forceUpdate`의 모든 사용을 피하고 `render`에서 `this.props`와 `this.state`로부터만 읽도록 하세요.\n\n#### 매개변수 {/*forceupdate-parameters*/}\n\n* **optional** `callback`: 지정한 경우 React는 업데이트가 커밋된 후 사용자가 제공한 `callback`을 호출합니다.\n\n#### 반환값 {/*forceupdate-returns*/}\n\n`forceUpdate`는 아무것도 반환하지 않습니다.\n\n#### 주의 사항 {/*forceupdate-caveats*/}\n\n- `forceUpdate`를 호출하면 React는 [`shouldComponentUpdate`](#shouldcomponentupdate)를 호출하지 않고 다시 렌더링합니다.\n\n<Note>\n\n외부 데이터 소스를 읽는 것과 `forceUpdate`를 사용하여 변경된 내용에 따라 클래스 컴포넌트를 다시 렌더링하도록 강제하는 것은 함수 컴포넌트에서 [`useSyncExternalStore`](/reference/react/useSyncExternalStore)로 대체되었습니다.\n\n</Note>\n\n---\n\n### `getSnapshotBeforeUpdate(prevProps, prevState)` {/*getsnapshotbeforeupdate*/}\n\n`getSnapshotBeforeUpdate`를 구현하면 React가 DOM을 업데이트하기 바로 전에 호출합니다. 이를 통해 컴포넌트가 잠재적으로 변경되기 전에 DOM에서 일부 정보(예: 스크롤 위치)를 캡처할 수 있습니다. 이 생명주기 메서드가 반환하는 모든 값은 [`componentDidUpdate`](#componentdidupdate)에 매개변수로 전달됩니다.\n\n예를 들어 업데이트 중에 스크롤 위치를 유지해야 하는 채팅 스레드와 같은 UI에서 이 메서드를 사용할 수 있습니다.\n\n```js {7-15,17}\nclass ScrollingList extends React.Component {\n  constructor(props) {\n    super(props);\n    this.listRef = React.createRef();\n  }\n\n  getSnapshotBeforeUpdate(prevProps, prevState) {\n    // 목록에 새 항목을 추가하고 있나요?\n    // 나중에 스크롤을 조정할 수 있도록 스크롤 위치를 캡처합니다.\n    if (prevProps.list.length < this.props.list.length) {\n      const list = this.listRef.current;\n      return list.scrollHeight - list.scrollTop;\n    }\n    return null;\n  }\n\n  componentDidUpdate(prevProps, prevState, snapshot) {\n    // 스냅샷 값이 있으면 방금 새 항목을 추가한 것입니다.\n    // 새 항목이 기존 항목을 시야 밖으로 밀어내지 않도록 스크롤을 조정합니다.\n    // (여기서 스냅샷은 getSnapshotBeforeUpdate에서 반환된 값입니다.)\n    if (snapshot !== null) {\n      const list = this.listRef.current;\n      list.scrollTop = list.scrollHeight - snapshot;\n    }\n  }\n\n  render() {\n    return (\n      <div ref={this.listRef}>{/* ...contents... */}</div>\n    );\n  }\n}\n```\n\n위의 예시에서는 `getSnapshotBeforeUpdate`에서 직접 `scrollHeight` 속성을 읽는 것이 중요합니다. [`render`](#render), [`UNSAFE_componentWillReceiveProps`](#unsafe_componentwillreceiveprops) 또는 [`UNSAFE_componentWillUpdate`](#unsafe_componentwillupdate)가 호출되는 시점과 React가 DOM을 업데이트하는 시점 사이에 잠재적인 시간 간격이 있기 때문에 이러한 여러 메서드에서 이(역주: `scrollHeight`)를 읽는 것은 안전하지 않습니다.\n\n#### 매개변수 {/*getsnapshotbeforeupdate-parameters*/}\n\n* `prevProps`: 업데이트 이전의 props. `prevProps`와 [`this.props`](#props)를 비교하여 변경된 내용을 확인합니다.\n\n* `prevState`: 업데이트 전 state. `prevState`를 [`this.state`](#state)와 비교하여 변경된 내용을 확인합니다.\n\n#### 반환값 {/*getsnapshotbeforeupdate-returns*/}\n\n원하는 유형의 스냅샷 값 또는 `null`을 반환해야 합니다. 반환한 값은 [componentDidUpdate](#componentdidupdate)의 세 번째 인자로 전달됩니다.\n\n#### 주의 사항 {/*getsnapshotbeforeupdate-caveats*/}\n\n- [`shouldComponentUpdate`](#shouldcomponentupdate)가 정의되어 있으면 `getSnapshotBeforeUpdate`가 호출되지 않고 `false`를 반환합니다.\n\n<Note>\n\n현재로서는 함수 컴포넌트에 대한 `getSnapshotBeforeUpdate`와 동등한 함수가 없습니다. 이 사용 사례는 매우 드물지만, 이 기능이 필요한 경우 현재로서는 클래스 컴포넌트를 작성해야 합니다.\n\n</Note>\n\n---\n\n### `render()` {/*render*/}\n\n`render` 메서드는 클래스 컴포넌트에서 유일하게 필요한 메서드입니다.\n\n`render` 메서드는 화면에 표시할 내용을 지정해야 합니다, 예를 들어\n\n```js {4-6}\nimport { Component } from 'react';\n\nclass Greeting extends Component {\n  render() {\n    return <h1>Hello, {this.props.name}!</h1>;\n  }\n}\n```\n\nReact는 언제든 `render`를 호출할 수 있으므로 특정 시간에 실행된다고 가정하면 안 됩니다. 일반적으로 `render` 메서드는 [JSX](/learn/writing-markup-with-jsx)를 반환해야 하지만 몇 가지 (문자열과 같은) [다른 반환 유형](#render-returns)이 지원됩니다. 반환된 JSX를 계산하기 위해 `render` 메서드는 [`this.props`](#props), [`this.state`](#state) 및 [`this.context`](#context)를 읽을 수 있습니다.\n\n`render` 메서드는 순수 함수로 작성해야 합니다. 즉, props, state 및 context가 동일한 경우 동일한 결과를 반환해야 합니다. 또한 (구독 설정과 같은) 부수 효과를 포함하거나 브라우저 API와 상호작용하면 안 됩니다. 부수 효과는 이벤트 핸들러나 [`componentDidMount`](#componentdidmount)와 같은 메서드에서 발생해야 합니다.\n\n#### 매개변수 {/*render-parameters*/}\n\n* `render`: 어떤 매개변수도 받지 않습니다.\n\n#### 반환값 {/*render-returns*/}\n\n`render`는 유효한 모든 React 노드를 반환할 수 있습니다. 여기에는 `<div />`, 문자열, 숫자, [portals](/reference/react-dom/createPortal), 빈 노드(`null`, `undefined`, `true`, `false`) 및 React 노드의 배열과 같은 React 엘리먼트가 포함됩니다.\n\n#### 주의 사항 {/*render-caveats*/}\n\n- `render`는 props, state, context의 순수한 함수로 작성되어야 합니다. 부수 효과가 없어야 합니다.\n\n- [`shouldComponentUpdate`](#shouldcomponentupdate)가 정의되고 `false`를 반환하면 `render`가 호출되지 않습니다.\n\n- [Strict 모드](/reference/react/StrictMode)가 켜져 있으면 React는 개발 과정에서 `render`를 두 번 호출한 다음 결과 중 하나를 버립니다. 이렇게 하면 `render` 메서드에서 제거해야 하는 우발적인 부수 효과를 알아차릴 수 있습니다.\n\n- `render` 호출과 후속 `componentDidMount` 또는 `componentDidUpdate` 호출 사이에는 일대일 대응이 없습니다. `render` 호출 결과 중 일부는 유익할 때 React에 의해 버려질 수 있습니다.\n\n---\n\n### `setState(nextState, callback?)` {/*setstate*/}\n\n`setState`를 호출하여 React 컴포넌트의 state를 업데이트합니다.\n\n```js {8-10}\nclass Form extends Component {\n  state = {\n    name: 'Taylor',\n  };\n\n  handleNameChange = (e) => {\n    const newName = e.target.value;\n    this.setState({\n      name: newName\n    });\n  }\n\n  render() {\n    return (\n      <>\n        <input value={this.state.name} onChange={this.handleNameChange} />\n        <p>Hello, {this.state.name}.</p>\n      </>\n    );\n  }\n}\n```\n\n`setState`는 컴포넌트 state에 대한 변경 사항을 큐에 넣습니다. 이 컴포넌트와 그 자식이 새로운 state로 다시 렌더링해야 한다는 것을 React에게 알려줍니다. 이것이 상호작용에 반응하여 사용자 인터페이스를 업데이트하는 주요 방법입니다.\n\n<Pitfall>\n\n`setState`를 호출해도 이미 실행 중인 코드의 현재 state는 변경되지 **않습니다**.\n\n```js {6}\nfunction handleClick() {\n  console.log(this.state.name); // \"Taylor\"\n  this.setState({\n    name: 'Robin'\n  });\n  console.log(this.state.name); // Still \"Taylor\"!\n}\n```\n\n오로지 *다음* 렌더링부터 `this.state`가 반환할 내용에만 영향을 줍니다.\n\n</Pitfall>\n\n`setState`에 함수를 전달할 수도 있습니다. 이 함수를 사용하면 이전 state를 기반으로 state를 업데이트할 수 있습니다.\n\n```js {2-6}\n  handleIncreaseAge = () => {\n    this.setState(prevState => {\n      return {\n        age: prevState.age + 1\n      };\n    });\n  }\n```\n\n이 작업을 수행할 필요는 없지만 동일한 이벤트 중에 state를 여러 번 업데이트하려는 경우 유용합니다.\n\n#### 매개변수 {/*setstate-parameters*/}\n\n* `nextState`: 객체 또는 함수 중 하나입니다.\n  * 객체를 `nextState`로 전달하면 `this.state`에 얕게(shallowly) 병합됩니다.\n  * 함수를 `nextState`로 전달하면 _업데이터 함수_ 로 취급됩니다. 이 함수는 순수해야 하고, pending state와 props를 인자로 받아야 하며, `this.state`에 얕게(shallowly) 병합할 객체를 반환해야 합니다. React는 업데이터 함수를 큐에 넣고 컴포넌트를 다시 렌더링합니다. 다음 렌더링 중에 React는 큐에 있는 모든 업데이터를 이전 state에 적용하여 다음 state를 계산합니다.\n\n* **optional** `callback`: 지정한 경우 React는 업데이트가 커밋된 후 사용자가 제공한 `callback`을 호출합니다.\n\n#### 반환값 {/*setstate-returns*/}\n\n`setState`는 아무것도 반환하지 않습니다.\n\n#### 주의 사항 {/*setstate-caveats*/}\n\n- `setState`를 컴포넌트를 업데이트하는 즉각적인 명령이 아닌 *요청*으로 생각하세요. 여러 컴포넌트가 이벤트에 반응하여 state를 업데이트하면 React는 업데이트를 batch하고 이벤트가 끝날 때 단일 패스로 함께 다시 렌더링합니다. 드물게 특정 state 업데이트를 강제로 동기화하여 적용해야 하는 경우, [`flushSync`](/reference/react-dom/flushSync)로 래핑할 수 있지만, 이 경우 성능이 저하될 수 있습니다.\n\n- `setState`는 `this.state`를 즉시 업데이트하지 않습니다. 따라서 `setState`를 호출한 직후 `this.state`를 읽는 것은 잠재적인 위험이 될 수 있습니다. 대신, 업데이트가 적용된 후에 실행되도록 보장되는 [`componentDidUpdate`](#componentdidupdate) 또는 setState `callback` 인자를 사용하십시오. 이전 state를 기반으로 state를 설정해야 하는 경우 위에서 설명한 대로 함수를 `nextState`에 전달할 수 있습니다.\n\n<Note>\n\n클래스 컴포넌트에서 `setState`를 호출하는 것은 함수 컴포넌트에서 [`set` 함수](/reference/react/useState#setstate)를 호출하는 것과 유사합니다.\n\n[마이그레이션 방법을 확인하세요.](#migrating-a-component-with-state-from-a-class-to-a-function)\n\n</Note>\n\n---\n\n### `shouldComponentUpdate(nextProps, nextState, nextContext)` {/*shouldcomponentupdate*/}\n\n`shouldComponentUpdate`를 정의하면 React가 이를 호출하여 재렌더링을 건너뛸 수 있는지 여부를 결정합니다.\n\n직접 작성을 원하는 것이 확실하다면, `this.props`를 `nextProps`와, `this.state`를 `nextState`와 비교하고 `false`를 반환하여 React에 업데이트를 건너뛸 수 있음을 알릴 수 있습니다.\n\n```js {6-18}\nclass Rectangle extends Component {\n  state = {\n    isHovered: false\n  };\n\n  shouldComponentUpdate(nextProps, nextState) {\n    if (\n      nextProps.position.x === this.props.position.x &&\n      nextProps.position.y === this.props.position.y &&\n      nextProps.size.width === this.props.size.width &&\n      nextProps.size.height === this.props.size.height &&\n      nextState.isHovered === this.state.isHovered\n    ) {\n      // 변경된 사항이 없으므로 다시 렌더링할 필요가 없습니다.\n      return false;\n    }\n    return true;\n  }\n\n  // ...\n}\n\n```\n\n새로운 props나 state가 수신되면 렌더링하기 전에 React가 `shouldComponentUpdate`를 호출합니다. 기본값은 `true`입니다. 이 메서드는 초기 렌더링이나 [`forceUpdate`](#forceupdate)가 사용될 때는 호출되지 않습니다.\n\n#### 매개변수 {/*shouldcomponentupdate-parameters*/}\n\n- `nextProps`: 컴포넌트가 렌더링할 다음 props입니다. `nextProps`와 [`this.props`](#props)를 비교하여 무엇이 변경되었는지 확인합니다.\n- `nextState`: 컴포넌트가 렌더링할 다음 state입니다. `nextState`와 [`this.state`](#props)를 비교하여 무엇이 변경되었는지 확인합니다.\n- `nextContext`: 컴포넌트가 렌더링할 다음 context입니다. `nextContext`를 [`this.context`](#context)와 비교하여 변경된 내용을 확인합니다. [`static contextType`](#static-contexttype)(modern)을 지정한 경우에만 사용할 수 있습니다.\n\n#### 반환값 {/*shouldcomponentupdate-returns*/}\n\n컴포넌트를 다시 렌더링하려면 `true`를 반환합니다. 이것이 기본 동작입니다.\n\nReact에 재렌더링을 건너뛸 수 있음을 알리려면 `false`를 반환합니다.\n\n#### 주의 사항 {/*shouldcomponentupdate-caveats*/}\n\n- 이 메서드는 *오직* 성능 최적화를 위해서만 존재합니다. 이 메서드 없이 컴포넌트가 중단되는 경우 먼저 그 문제를 해결하세요.\n\n- `shouldComponentUpdate`를 직접 작성하는 대신 [`PureComponent`](/reference/react/PureComponent)를 사용하는 것을 고려하세요. `PureComponent`는 props와 state를 얕게(shallowly) 비교하여 필요한 업데이트를 건너뛸 가능성을 줄여줍니다.\n\n- `shouldComponentUpdate`에서 완전 일치(deep equality) 검사를 하거나 `JSON.stringify`를 사용하는 것은 권장하지 않습니다. 이는 성능을 예측할 수 없고 모든 prop과 state의 데이터 구조에 의존적이게 합니다. 최상의 경우 애플리케이션에 몇 초씩 멈추는 현상이 발생하고 최악의 경우 애플리케이션이 충돌할 위험이 있습니다.\n\n- `false`를 반환해도 자식 컴포넌트들에서 *그들의* state가 변경될 때 다시 렌더링되는 것을 막지는 못합니다.\n\n- `false`를 반환한다고 해서 컴포넌트가 다시 렌더링되지 않는다는 *보장*은 없습니다. React는 반환값을 힌트로 사용하지만 다른 이유로 컴포넌트를 다시 렌더링하는 것이 합리적일 경우 여전히 렌더링을 선택할 수 있습니다.\n\n<Note>\n\n`shouldComponentUpdate`로 클래스 컴포넌트를 최적화하는 것은 [`memo`](/reference/react/memo)로 함수 컴포넌트를 최적화하는 것과 유사합니다. 함수 컴포넌트는 [`useMemo`](/reference/react/useMemo)를 통해 더 세분화된 최적화도 제공합니다.\n\n</Note>\n\n---\n\n### `UNSAFE_componentWillMount()` {/*unsafe_componentwillmount*/}\n\n`UNSAFE_componentWillMount`를 정의하면 React는 [`constructor`](#constructor) 바로 뒤에 이를 호출합니다. 이 메서드는 역사적인 이유로만 존재하며 새로운 코드에서 사용하면 안 됩니다. 대신 다른 대안을 사용하세요.\n\n- state를 초기화하려면 [`state`](#state)를 클래스 필드로 선언하거나 [`constructor`](#constructor) 내에서 `this.state`를 설정하세요.\n- 부수 효과를 실행하거나 구독을 설정해야 하는 경우 해당 로직을 [`componentDidMount`](#componentdidmount)로 옮기세요.\n\n[안전하지 않은 생명주기에서 벗어나 마이그레이션한 사례를 확인하세요.](https://ko.legacy.reactjs.org/blog/2018/03/27/update-on-async-rendering.html#examples)\n\n#### 매개변수 {/*unsafe_componentwillmount-parameters*/}\n\n`UNSAFE_componentWillMount`는 어떠한 매개변수도 받지 않습니다.\n\n#### 반환값 {/*unsafe_componentwillmount-returns*/}\n\n`UNSAFE_componentWillMount`는 아무것도 반환하지 않아야 합니다.\n\n#### 주의 사항 {/*unsafe_componentwillmount-caveats*/}\n\n- 컴포넌트가 [`static getDerivedStateFromProps`](#static-getderivedstatefromprops) 또는 [`getSnapshotBeforeUpdate`](#getsnapshotbeforeupdate)를 구현하는 경우 `UNSAFE_componentWillMount`가 호출되지 않습니다.\n\n- 이름과는 달리, 앱이 [`Suspense`](/reference/react/Suspense)와 같은 최신 React 기능을 사용하는 경우 `UNSAFE_componentWillMount`는 컴포넌트가 마운트*될 것*을 보장하지 않습니다. 렌더링 시도가 일시 중단되면(예를 들어 일부 자식 컴포넌트의 코드가 아직 로드되지 않았기 때문에) React는 진행 중인 트리를 버리고 다음 시도에서 컴포넌트를 처음부터 구성하려고 시도합니다. 이것이 바로 이 메서드가 \"안전하지 않은\" 이유입니다. 마운팅에 의존하는 코드(예: 구독 추가)는 [`componentDidMount`](#componentdidmount)로 이동해야 합니다.\n\n- `UNSAFE_componentWillMount`는 [서버 렌더링](/reference/react-dom/server) 중에 실행되는 유일한 생명주기 메서드입니다. 모든 실용적인 용도로 볼 때 [`constructor`](#constructor)와 동일하므로 이러한 유형의 로직에는 `constructor`를 대신 사용해야 합니다.\n\n<Note>\n\n클래스 컴포넌트 내 `UNSAFE_componentWillMount` 내부에서 [`setState`](#setstate)를 호출하여 state를 초기화하는 것은 함수 컴포넌트에서 해당 state를 [`useState`](/reference/react/useState)에 초기 state로 전달하는 것과 동일합니다.\n\n</Note>\n\n---\n\n### `UNSAFE_componentWillReceiveProps(nextProps, nextContext)` {/*unsafe_componentwillreceiveprops*/}\n\n`UNSAFE_componentWillReceiveProps`를 정의하면 컴포넌트가 새로운 props를 수신할 때 React가 이를 호출합니다. 이 메서드는 역사적인 이유로만 존재하며 새로운 코드에서 사용하면 안 됩니다. 대신 다른 방법을 사용하세요.\n\n- props 변경에 대한 응답으로 **부수 효과를 실행**(예: 데이터 가져오기, 애니메이션 실행, 구독 재초기화)해야 하는 경우 해당 로직을 [`componentDidUpdate`](#componentdidupdate)로 옮기세요.\n- **props가 변경될 때만 일부 데이터를 다시 계산하지 않아야** 하는 경우 대신 [memoization helper](https://ko.legacy.reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html#what-about-memoization)를 사용하세요.\n- **props가 변경될 때 일부 state를 \"초기화\"** 해야 하는 경우, 컴포넌트를 [완전히 제어](https://ko.legacy.reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html#recommendation-fully-controlled-component)하거나 [key로 완전히 제어하지 않도록](https://ko.legacy.reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html#recommendation-fully-uncontrolled-component-with-a-key) 만드는 것이 좋습니다.\n- **props가 변경될 때 일부 state를 \"조정\"** 해야 하는 경우 렌더링 중에 props만으로 필요한 모든 정보를 계산할 수 있는지 확인하세요. 계산할 수 없는 경우 [`static getDerivedStateFromProps`](/reference/react/Component#static-getderivedstatefromprops)를 대신 사용하세요.\n\n[안전하지 않은 생명주기에서 벗어나 마이그레이션한 사례를 확인하세요.](https://ko.legacy.reactjs.org/blog/2018/03/27/update-on-async-rendering.html#updating-state-based-on-props)\n\n#### 매개변수 {/*unsafe_componentwillreceiveprops-parameters*/}\n\n- `nextProps`: 컴포넌트가 부모 컴포넌트로부터 받으려는 다음 props입니다. `nextProps`와 [`this.props`](#props)를 비교하여 무엇이 변경되었는지 확인합니다.\n- `nextContext`: 컴포넌트가 가장 가까운 공급자(provider)로부터 받으려는 다음 props입니다. `nextContext`를 [`this.context`](#context)와 비교하여 변경된 내용을 확인합니다. [`static contextType`](#static-contexttype)(modern)을 지정한 경우에만 사용할 수 있습니다.\n\n#### 반환값 {/*unsafe_componentwillreceiveprops-returns*/}\n\n`UNSAFE_componentWillReceiveProps`는 아무것도 반환하지 않아야 합니다.\n\n#### 주의 사항 {/*unsafe_componentwillreceiveprops-caveats*/}\n\n- 컴포넌트가 [`static getDerivedStateFromProps`](#static-getderivedstatefromprops) 또는 [`getSnapshotBeforeUpdate`](#getsnapshotbeforeupdate)를 구현하는 경우 `UNSAFE_componentWillReceiveProps`가 호출되지 않습니다.\n\n- 이름과는 달리, 앱이 [`Suspense`](/reference/react/Suspense)와 같은 최신 React 기능을 사용하는 경우 `UNSAFE_componentWillReceiveProps`는 컴포넌트가 해당 props를 수신*할 것*을 보장하지 않습니다. 렌더링 시도가 일시 중단되면(예를 들어 일부 자식 컴포넌트의 코드가 아직 로드되지 않았기 때문에) React는 진행 중인 트리를 버리고 다음 시도 중에 컴포넌트를 처음부터 다시 구성하려고 시도합니다. 다음 렌더링을 시도할 때쯤이면 props가 달라져 있을 수 있습니다. 이것이 바로 이 메서드가 \"안전하지 않은\" 이유입니다. 커밋된 업데이트에 대해서만 실행되어야 하는 코드(예: 구독 재설정)는 [`componentDidUpdate`](#componentdidupdate)로 이동해야 합니다.\n\n- `UNSAFE_componentWillReceiveProps`는 컴포넌트가 지난번과 *다른* props를 받았다는 것을 의미하지 않습니다. `nextProps`와 `this.props`를 직접 비교하여 변경된 사항이 있는지 확인해야 합니다.\n\n- React는 마운트하는 동안 초기 props와 함께 `UNSAFE_componentWillReceiveProps`를 호출하지 않습니다. 컴포넌트의 일부 props가 업데이트될 경우에만 이 메서드를 호출합니다. 예를 들어, 일반적으로 같은 컴포넌트 내부에서 [`setState`](#setstate)를 호출해도 `UNSAFE_componentWillReceiveProps`가 트리거되지 않습니다.\n\n<Note>\n\n클래스 컴포넌트에서 `UNSAFE_componentWillReceiveProps` 내부의 [`setState`](#setstate)를 호출하여 state를 \"조정\"하는 것은 함수 컴포넌트에서 [렌더링 중에 `useState`에서 `set` 함수를 호출하는 것](/reference/react/useState#storing-information-from-previous-renders)과 동일합니다.\n\n</Note>\n\n---\n\n### `UNSAFE_componentWillUpdate(nextProps, nextState)` {/*unsafe_componentwillupdate*/}\n\n\n`UNSAFE_componentWillUpdate`를 정의하면 React는 새 props나 state로 렌더링하기 전에 이를 호출합니다. 이 메서드는 역사적인 이유로만 존재하며 새로운 코드에서 사용하면 안 됩니다. 대신 다른 대안을 사용하세요.\n\n- props나 state 변경에 대한 응답으로 부수 효과(예: 데이터 가져오기, 애니메이션 실행, 구독 재초기화)를 실행해야 하는 경우, 해당 로직을 [`componentDidUpdate`](#componentdidupdate)로 이동하세요.\n- 나중에 [`componentDidUpdate`](#componentdidupdate)에서 사용할 수 있도록 DOM에서 일부 정보(예: 현재 스크롤 위치를 저장하기 위해)를 읽어야 하는 경우, 대신 [`getSnapshotBeforeUpdate`](#getsnapshotbeforeupdate) 내부에서 읽습니다.\n\n[안전하지 않은 생명주기에서 벗어나 마이그레이션한 사례를 확인하세요.](https://ko.legacy.reactjs.org/blog/2018/03/27/update-on-async-rendering.html#examples)\n\n#### 매개변수 {/*unsafe_componentwillupdate-parameters*/}\n\n- `nextProps`: 컴포넌트가 렌더링할 다음 props입니다. `nextProps`와 [`this.props`](#props)를 비교하여 무엇이 변경되었는지 확인합니다.\n- `nextState`: 컴포넌트가 렌더링할 다음 state입니다. `nextState`와 [`this.state`](#props)를 비교하여 무엇이 변경되었는지 확인합니다.\n\n#### 반환값 {/*unsafe_componentwillupdate-returns*/}\n\n`UNSAFE_componentWillUpdate`는 아무것도 반환하지 않아야 합니다.\n\n#### 주의 사항 {/*unsafe_componentwillupdate-caveats*/}\n\n- [`shouldComponentUpdate`](#shouldcomponentupdate)가 정의된 경우 `UNSAFE_componentWillUpdate`는 호출되지 않으며 `false`를 반환합니다.\n\n- 컴포넌트가 [`static getDerivedStateFromProps`](#static-getderivedstatefromprops) 또는 [`getSnapshotBeforeUpdate`](#getsnapshotbeforeupdate)를 구현하는 경우 `UNSAFE_componentWillUpdate`가 호출되지 않습니다.\n\n- `componentWillUpdate` 중에 [`setState`](#setstate)를 호출하는 것(또는 Redux 액션을 dispatch하는 것과 같이 `setState`가 호출되도록 하는 모든 메서드)은 지원되지 않습니다.\n\n- 이름과는 달리, 앱이 [`Suspense`](/reference/react/Suspense)와 같은 최신 React 기능을 사용하는 경우 `UNSAFE_componentWillUpdate`는 컴포넌트가 업데이트*될 것*을 보장하지는 않습니다. 렌더링 시도가 일시 중단되면(예를 들어 일부 하위 컴포넌트의 코드가 아직 로드되지 않았기 때문에) React는 진행 중인 트리를 버리고 다음 시도에서 컴포넌트를 처음부터 새로 구성하려고 시도합니다. 다음 렌더링 시도 시에는 props와 state가 달라질 수 있습니다. 이것이 바로 이 메서드가 \"안전하지 않은\" 이유입니다. 커밋된 업데이트에 대해서만 실행되어야 하는 코드(예: 구독 재설정)는 [`componentDidUpdate`](#componentdidupdate)로 이동해야 합니다.\n\n- `UNSAFE_componentWillUpdate`는 컴포넌트가 지난번과 *다른* props나 state를 받았다는 것을 의미하지 않습니다. `nextProps`를 `this.props`와, `nextState`를 `this.state`와 직접 비교하여 변경된 사항이 있는지 확인해야 합니다.\n\n- React는 마운트하는 동안 초기 props와 state와 함께 `UNSAFE_componentWillUpdate`를 호출하지 않습니다.\n\n<Note>\n\n함수 컴포넌트에는 `UNSAFE_componentWillUpdate`와 직접적으로 대응하는 것이 없습니다.\n\n</Note>\n\n---\n\n### `static contextType` {/*static-contexttype*/}\n\n클래스 컴포넌트에서 [`this.context`](#context-instance-field) 를 읽으려면 읽어야 하는 context를 지정해야 합니다. `static contextType`으로 지정하는 context는 이전에 [`createContext`](/reference/react/createContext)로 생성한 값이어야 합니다.\n\n```js {2}\nclass Button extends Component {\n  static contextType = ThemeContext;\n\n  render() {\n    const theme = this.context;\n    const className = 'button-' + theme;\n    return (\n      <button className={className}>\n        {this.props.children}\n      </button>\n    );\n  }\n}\n```\n\n<Note>\n\n클래스 컴포넌트에서 `this.context`를 읽는 것은 함수 컴포넌트에서 [`useContext`](/reference/react/useContext)와 같습니다.\n\n[마이그레이션 방법을 확인하세요.](#migrating-a-component-with-context-from-a-class-to-a-function)\n\n</Note>\n\n---\n\n### `static defaultProps` {/*static-defaultprops*/}\n\n`static defaultProps`를 정의하여 클래스의 기본 props을 설정할 수 있습니다. `undefined`와 누락된 props에는 사용되지만 `null` props에는 사용되지 않습니다.\n\n예를 들어, `color` prop의 기본값을 `'blue'`로 정의하는 방법은 다음과 같습니다.\n\n```js {2-4}\nclass Button extends Component {\n  static defaultProps = {\n    color: 'blue'\n  };\n\n  render() {\n    return <button className={this.props.color}>click me</button>;\n  }\n}\n```\n\n`color` props이 제공되지 않거나 `undefined`인 경우 기본적으로 '`blue'`로 설정됩니다.\n\n```js\n<>\n  {/* this.props.color is \"blue\" */}\n  <Button />\n\n  {/* this.props.color is \"blue\" */}\n  <Button color={undefined} />\n\n  {/* this.props.color is null */}\n  <Button color={null} />\n\n  {/* this.props.color is \"red\" */}\n  <Button color=\"red\" />\n</>\n```\n\n<Note>\n\n클래스 컴포넌트에서 `defaultProps`를 정의하는 것은 함수 컴포넌트에서 [default values](/learn/passing-props-to-a-component#specifying-a-default-value-for-a-prop)를 사용하는 것과 유사합니다.\n\n</Note>\n\n---\n\n### `static getDerivedStateFromError(error)` {/*static-getderivedstatefromerror*/}\n\n`static getDerivedStateFromError`를 정의하면 렌더링 도중 자식 컴포넌트(멀리 떨어진 자식 포함)가 에러를 throw 할 때 React가 이를 호출합니다. 이렇게 하면 UI를 지우는 대신 오류 메시지를 표시할 수 있습니다.\n\n일반적으로 일부 분석 서비스에 오류 보고서를 보낼 수 있는 [`componentDidCatch`](#componentdidcatch)와 함께 사용합니다. 이러한 메서드가 있는 컴포넌트를 *Error Boundary* 라고 합니다.\n\n[예시를 확인하세요.](#catching-rendering-errors-with-an-error-boundary)\n\n#### 매개변수 {/*static-getderivedstatefromerror-parameters*/}\n\n* `error`: throw 된 오류입니다. 실제로는 일반적으로 [`Error`](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Error)의 인스턴스가 되지만, 자바스크립트에서는 문자열이나 심지어 `null`을 포함한 모든 값을 [`throw`](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Statements/throw) 할 수 있으므로 보장되지는 않습니다.\n\n#### 반환값 {/*static-getderivedstatefromerror-returns*/}\n\n`static getDerivedStateFromError`는 컴포넌트에 오류 메시지를 표시하도록 지시하는 state를 반환해야 합니다.\n\n#### 주의 사항 {/*static-getderivedstatefromerror-caveats*/}\n\n* `static getDerivedStateFromError`는 순수 함수여야 합니다. 예를 들어 분석 서비스를 호출하는 등의 부수 효과를 수행하려면 [`componentDidCatch`](#componentdidcatch)도 구현해야 합니다.\n\n<Note>\n\n함수 컴포넌트에서 `static getDerivedStateFromError`에 대해 직접적으로 동등한 것은 아직 없습니다. 클래스 컴포넌트를 만들지 않으려면 위와 같이 하나의 `ErrorBoundary` 컴포넌트를 작성하고 앱 전체에서 사용하세요. 또는 이를 수행하는 [`react-error-boundary`](https://github.com/bvaughn/react-error-boundary) package를 사용하세요.\n\n</Note>\n\n---\n\n### `static getDerivedStateFromProps(props, state)` {/*static-getderivedstatefromprops*/}\n\n`static getDerivedStateFromProps`를 정의하면 React는 초기 마운트 및 후속 업데이트 모두에서 [`render`](#render)를 호출하기 바로 전에 이를 호출합니다. state를 업데이트하려면 객체를 반환하고, 아무것도 업데이트하지 않으려면 `null`을 반환해야 합니다.\n\n이 메서드는 시간이 지남에 따라 props의 변경에 따라 state가 달라지는 [드문 사용 사례](https://ko.legacy.reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html#when-to-use-derived-state)를 위해 존재합니다. 예를 들어, 이 `Form` 컴포넌트는 `userId` props가 변경되면 `email` state를 재설정합니다.\n\n```js {7-18}\nclass Form extends Component {\n  state = {\n    email: this.props.defaultEmail,\n    prevUserID: this.props.userID\n  };\n\n  static getDerivedStateFromProps(props, state) {\n    // 현재 사용자가 변경될 때마다,\n    // 해당 사용자와 연결된 state의 모든 부분을 재설정합니다.\n    // 이 간단한 예시에서는 이메일만 해당됩니다.\n    if (props.userID !== state.prevUserID) {\n      return {\n        prevUserID: props.userID,\n        email: props.defaultEmail\n      };\n    }\n    return null;\n  }\n\n  // ...\n}\n```\n\n이 패턴을 사용하려면 props의 이전 값(예: userID)을 state(예: prevUserID)로 유지해야 한다는 점에 유의하세요.\n\n<Pitfall>\n\nstate를 파생하면 코드가 장황해지고 컴포넌트에 대해 생각하기 어려워집니다. [더 간단한 대안에 익숙해지도록 하세요.](https://ko.legacy.reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html)\n\n- props 변경에 대한 응답으로 부수 효과(예: 데이터 불러오기 또는 애니메이션)를 수행해야 하는 경우, 대신 [`componentDidUpdate`](#componentdidupdate) 메서드를 사용하세요.\n- **props이 변경될 때만 일부 데이터를 다시 계산**하려면 [memoization helper를 대신 사용하세요.](https://ko.legacy.reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html#what-about-memoization)\n- **prop이 변경될 때 일부 state를 \"초기화\"** 하려면 컴포넌트를 [완전히 제어](https://ko.legacy.reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html#recommendation-fully-controlled-component)하거나 [key를 사용해 완전히 제어하지 않도록](https://ko.legacy.reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html#recommendation-fully-uncontrolled-component-with-a-key) 만드는 것이 좋습니다.\n\n</Pitfall>\n\n#### 매개변수 {/*static-getderivedstatefromprops-parameters*/}\n\n- `props`: 컴포넌트가 렌더링할 다음 props입니다.\n- `state`: 컴포넌트가 렌더링할 다음 state입니다.\n\n#### 반환값 {/*static-getderivedstatefromprops-returns*/}\n\n`static getDerivedStateFromProps`는 state를 업데이트할 객체를 반환하거나, 아무것도 업데이트하지 않으면 `null`을 반환합니다.\n\n#### 주의 사항 {/*static-getderivedstatefromprops-caveats*/}\n\n- 이 메서드는 원인에 관계없이 *모든* 렌더링에서 호출됩니다. 이는 부모가 다시 렌더링을 일으킬 때만 발동하고 로컬 `setState`의 결과가 아닐 때만 발동하는 [`UNSAFE_componentWillReceiveProps`](#unsafe_cmoponentwillreceiveprops)와는 다릅니다.\n\n- 이 메서드에는 컴포넌트 인스턴스에 대한 액세스 권한이 없습니다. 원하는 경우 클래스 정의 외부 컴포넌트 props 및 state의 순수 함수를 추출하여 `static getDerivedStateFromProps`와 다른 클래스 메서드 사이에 일부 코드를 재사용할 수 있습니다.\n\n<Note>\n\n클래스 컴포넌트에서 `static getDerivedStateFromProps`를 구현하는 것은 함수 컴포넌트에서 [렌더링하는 동안 `useState`에서 `set` 함수를 호출하는 것](/reference/react/useState#storing-information-from-previous-renders)과 동일합니다.\n\n</Note>\n\n---\n\n## 사용법 {/*usage*/}\n\n### 클래스 컴포넌트 정의하기 {/*defining-a-class-component*/}\n\nReact 컴포넌트를 클래스로 정의하려면 기본 제공 `Component` 클래스를 확장하고 [`render` 메서드](#render)를 정의합니다,\n\n```js\nimport { Component } from 'react';\n\nclass Greeting extends Component {\n  render() {\n    return <h1>Hello, {this.props.name}!</h1>;\n  }\n}\n```\n\nReact는 화면에 표시할 내용을 파악해야 할 때마다 [`render`](#render) 메서드를 호출합니다. 보통은 [JSX](/learn/writing-markup-with-jsx)를 반환합니다. `render` 메서드는 [순수 함수](https://en.wikipedia.org/wiki/Pure_function)여야 합니다. JSX만 계산해야 합니다.\n\n[함수 컴포넌트](/learn/your-first-component#defining-a-component)와 마찬가지로 클래스 컴포넌트는 부모 컴포넌트로부터 [props로 정보를 받는 것](/learn/your-first-component#defining-a-component)이 가능합니다. 하지만 props를 읽는 문법은 다릅니다. 예를 들어, 부모 컴포넌트가 `<Greeting name=\"Taylor\" />`를 렌더링하는 경우, `this.props.name`과 같이 [`this.props`](#props)에서 `name` prop을 읽을 수 있습니다.\n\n<Sandpack>\n\n```js\nimport { Component } from 'react';\n\nclass Greeting extends Component {\n  render() {\n    return <h1>Hello, {this.props.name}!</h1>;\n  }\n}\n\nexport default function App() {\n  return (\n    <>\n      <Greeting name=\"Sara\" />\n      <Greeting name=\"Cahal\" />\n      <Greeting name=\"Edite\" />\n    </>\n  );\n}\n```\n\n</Sandpack>\n\n클래스 컴포넌트 내부에서는 Hooks(`use`로 시작하는 함수, 예를 들어 [`useState`](/reference/react/useState))가 지원되지 않습니다.\n\n<Pitfall>\n\n컴포넌트를 클래스 대신 함수로 정의하는 것을 추천합니다. [마이그레이션 방법을 확인하세요.](#migrating-a-simple-component-from-a-class-to-a-function)\n\n</Pitfall>\n\n---\n\n### 클래스 컴포넌트에 state 추가하기 {/*adding-state-to-a-class-component*/}\n\n클래스에 [state](/learn/state-a-components-memory)를 추가하려면 [`state`](#state)라는 프로퍼티에 객체를 할당합니다. state를 업데이트하려면 [`this.setState`](#setstate)를 호출합니다.\n\n<Sandpack>\n\n```js\nimport { Component } from 'react';\n\nexport default class Counter extends Component {\n  state = {\n    name: 'Taylor',\n    age: 42,\n  };\n\n  handleNameChange = (e) => {\n    this.setState({\n      name: e.target.value\n    });\n  }\n\n  handleAgeChange = () => {\n    this.setState({\n      age: this.state.age + 1\n    });\n  };\n\n  render() {\n    return (\n      <>\n        <input\n          value={this.state.name}\n          onChange={this.handleNameChange}\n        />\n        <button onClick={this.handleAgeChange}>\n          Increment age\n        </button>\n        <p>Hello, {this.state.name}. You are {this.state.age}.</p>\n      </>\n    );\n  }\n}\n```\n\n```css\nbutton { display: block; margin-top: 10px; }\n```\n\n</Sandpack>\n\n<Pitfall>\n\n컴포넌트를 클래스 대신 함수로 정의하는 것을 추천합니다. [마이그레이션 방법을 확인하세요.](#migrating-a-component-with-state-from-a-class-to-a-function)\n\n</Pitfall>\n\n---\n\n### 클래스 컴포넌트에 생명주기 메서드 추가하기 {/*adding-lifecycle-methods-to-a-class-component*/}\n\n클래스에서 정의할 수 있는 몇 가지 특별한 메서드가 있습니다.\n\n[`componentDidMount`](#componentdidmount) 메서드를 정의하면 컴포넌트가 화면에 추가 *(마운트)* 될 때 React가 이를 호출합니다. 컴포넌트가 props나 state 변경으로 인해 다시 렌더링되면 React는 [`componentDidUpdate`](#componentdidupdate)를 호출합니다. 컴포넌트가 화면에서 제거 *(마운트 해제)* 된 후 React는 [`componentWillUnmount`](#componentwillunmount)를 호출합니다.\n\n`componentDidMount`를 구현하는 경우 일반적으로 버그를 피하기 위해 세 가지 생명주기를 모두 구현해야 합니다. 예를 들어 `componentDidMount`가 state나 props를 읽었다면 해당 변경 사항을 처리하기 위해 `componentDidUpdate`도 구현해야 하고, `componentDidMount`가 수행하던 작업을 정리하기 위해 `componentWillUnmount`도 구현해야 합니다.\n\n예를 들어 이 `ChatRoom` 컴포넌트는 채팅 연결을 props 및 state와 동기화하여 유지합니다:\n\n<Sandpack>\n\n```js src/App.js\nimport { useState } from 'react';\nimport ChatRoom from './ChatRoom.js';\n\nexport default function App() {\n  const [roomId, setRoomId] = useState('general');\n  const [show, setShow] = useState(false);\n  return (\n    <>\n      <label>\n        Choose the chat room:{' '}\n        <select\n          value={roomId}\n          onChange={e => setRoomId(e.target.value)}\n        >\n          <option value=\"general\">general</option>\n          <option value=\"travel\">travel</option>\n          <option value=\"music\">music</option>\n        </select>\n      </label>\n      <button onClick={() => setShow(!show)}>\n        {show ? 'Close chat' : 'Open chat'}\n      </button>\n      {show && <hr />}\n      {show && <ChatRoom roomId={roomId} />}\n    </>\n  );\n}\n```\n\n```js src/ChatRoom.js active\nimport { Component } from 'react';\nimport { createConnection } from './chat.js';\n\nexport default class ChatRoom extends Component {\n  state = {\n    serverUrl: 'https://localhost:1234'\n  };\n\n  componentDidMount() {\n    this.setupConnection();\n  }\n\n  componentDidUpdate(prevProps, prevState) {\n    if (\n      this.props.roomId !== prevProps.roomId ||\n      this.state.serverUrl !== prevState.serverUrl\n    ) {\n      this.destroyConnection();\n      this.setupConnection();\n    }\n  }\n\n  componentWillUnmount() {\n    this.destroyConnection();\n  }\n\n  setupConnection() {\n    this.connection = createConnection(\n      this.state.serverUrl,\n      this.props.roomId\n    );\n    this.connection.connect();\n  }\n\n  destroyConnection() {\n    this.connection.disconnect();\n    this.connection = null;\n  }\n\n  render() {\n    return (\n      <>\n        <label>\n          Server URL:{' '}\n          <input\n            value={this.state.serverUrl}\n            onChange={e => {\n              this.setState({\n                serverUrl: e.target.value\n              });\n            }}\n          />\n        </label>\n        <h1>Welcome to the {this.props.roomId} room!</h1>\n      </>\n    );\n  }\n}\n```\n\n```js src/chat.js\nexport function createConnection(serverUrl, roomId) {\n  // A real implementation would actually connect to the server\n  return {\n    connect() {\n      console.log('✅ Connecting to \"' + roomId + '\" room at ' + serverUrl + '...');\n    },\n    disconnect() {\n      console.log('❌ Disconnected from \"' + roomId + '\" room at ' + serverUrl);\n    }\n  };\n}\n```\n\n```css\ninput { display: block; margin-bottom: 20px; }\nbutton { margin-left: 10px; }\n```\n\n</Sandpack>\n\n[Strict 모드](/reference/react/StrictMode)가 켜져 있을 때 개발할 때 React는 `componentDidMount`를 호출하고, 즉시 `componentWillUnmount`를 호출한 다음, `componentDidMount`를 다시 호출합니다. 이렇게 하면 `componentWillUnmount`를 구현하는 것을 잊어버렸거나 그 로직이 `componentDidMount`의 동작을 완전히 \"미러링\"하지 않는지 알 수 있습니다.\n\n<Pitfall>\n\n컴포넌트를 클래스 대신 함수로 정의하는 것을 추천합니다. [마이그레이션 방법을 확인하세요.](#migrating-a-component-with-lifecycle-methods-from-a-class-to-a-function)\n\n</Pitfall>\n\n---\n\n### Error Boundary로 렌더링 오류 포착하기 {/*catching-rendering-errors-with-an-error-boundary*/}\n\n기본적으로 애플리케이션이 렌더링 도중 에러를 발생시키면 React는 화면에서 해당 UI를 제거합니다. 이를 방지하기 위해 UI의 일부를 *Error Boundary*로 감싸면 됩니다. Error Boundary는 에러가 발생한 부분 대신 오류 메시지와 같은 Fallback UI를 표시할 수 있는 특수 컴포넌트입니다.\n\n<Note>\nError boundaries do not catch errors for:\n\n- Event handlers [(learn more)](/learn/responding-to-events)\n- [Server side rendering](/reference/react-dom/server) \n- Errors thrown in the error boundary itself (rather than its children)\n- Asynchronous code (e.g. `setTimeout` or `requestAnimationFrame` callbacks); an exception is the usage of the [`startTransition`](/reference/react/useTransition#starttransition) function returned by the [`useTransition`](/reference/react/useTransition) Hook. Errors thrown inside the transition function are caught by error boundaries [(learn more)](/reference/react/useTransition#displaying-an-error-to-users-with-error-boundary)\n\n</Note>\n\nError Boundary 컴포넌트를 구현하려면 오류에 대한 응답으로 State를 업데이트하고 사용자에게 오류 메시지를 표시할 수 있는 [`static getDerivedStateFromError`](#static-getderivedstatefromerror)를 제공해야 합니다. 또한 선택적으로 [`componentDidCatch`](#componentdidcatch)를 구현하여 분석 서비스에 오류를 기록하는 등의 추가 로직을 추가할 수도 있습니다.\n\nWith [`captureOwnerStack`](/reference/react/captureOwnerStack) you can include the Owner Stack during development.\n\n```js {9-12,14-27}\nimport * as React from 'react';\n\nclass ErrorBoundary extends React.Component {\n  constructor(props) {\n    super(props);\n    this.state = { hasError: false };\n  }\n\n  static getDerivedStateFromError(error) {\n    // state를 업데이트하여 다음 렌더링에 fallback UI가 표시되도록 합니다.\n    return { hasError: true };\n  }\n\n  componentDidCatch(error, info) {\n    logErrorToMyService(\n      error,\n      // Example \"componentStack\":\n      //   in ComponentThatThrows (created by App)\n      //   in ErrorBoundary (created by App)\n      //   in div (created by App)\n      //   in App\n      info.componentStack,\n      // Warning: `captureOwnerStack` is not available in production.\n      React.captureOwnerStack(),\n    );\n  }\n\n  render() {\n    if (this.state.hasError) {\n      // 사용자 지정 fallback UI를 렌더링할 수 있습니다.\n      return this.props.fallback;\n    }\n\n    return this.props.children;\n  }\n}\n```\n\n그런 다음 컴포넌트 트리의 일부를 래핑할 수 있습니다.\n\n```js {1,3}\n<ErrorBoundary fallback={<p>Something went wrong</p>}>\n  <Profile />\n</ErrorBoundary>\n```\n\n`Profile` 또는 그 하위 컴포넌트가 오류를 발생시키면 `ErrorBoundary`가 해당 오류를 \"포착\"하고 사용자가 제공한 오류 메시지와 함께 fallback UI를 표시한 다음 프로덕션 오류 보고서를 오류 보고 서비스에 전송합니다.\n\n모든 컴포넌트를 별도의 Error Boundary로 묶을 필요는 없습니다. [Error Boundary의 세분화](https://www.brandondail.com/posts/fault-tolerance-react)를 고려할 때는 오류 메시지를 표시하는 것이 적절한 위치를 고려하세요. 예를 들어 메시징 앱의 경우 Error Boundary를 대화 목록 주위에 위치시키는 것이 좋습니다. 또한 모든 개별 메시지 주위에 위치시키는 것도 좋습니다. 하지만 모든 아바타 주위에 Boundary를 위치시키는 것은 적절하지 않습니다.\n\n<Note>\n\n현재 Error Boundary를 함수 컴포넌트로 작성할 수 있는 방법은 없습니다. 하지만 Error Boundary 클래스를 직접 작성할 필요는 없습니다. 예를 들어 [`react-error-boundary`](https://github.com/bvaughn/react-error-boundary)를 대신 사용할 수 있습니다.\n\n</Note>\n\n---\n\n## 대안 {/*alternatives*/}\n\n### 클래스에서 함수로 간단한 컴포넌트 마이그레이션하기 {/*migrating-a-simple-component-from-a-class-to-a-function*/}\n\n일반적으로 [컴포넌트를 함수로 대신 정의합니다.](/learn/your-first-component#defining-a-component)\n\n예를 들어 이 `Greeting` 클래스 컴포넌트를 함수로 변환한다고 가정해 보겠습니다.\n\n<Sandpack>\n\n```js\nimport { Component } from 'react';\n\nclass Greeting extends Component {\n  render() {\n    return <h1>Hello, {this.props.name}!</h1>;\n  }\n}\n\nexport default function App() {\n  return (\n    <>\n      <Greeting name=\"Sara\" />\n      <Greeting name=\"Cahal\" />\n      <Greeting name=\"Edite\" />\n    </>\n  );\n}\n```\n\n</Sandpack>\n\n`Greeting`이라는 함수를 정의합니다. 여기로 `render` 함수의 본문을 이동합니다.\n\n```js\nfunction Greeting() {\n  // ... render 메서드의 코드를 여기로 옮깁니다 ...\n}\n```\n\n`this.props.name` 대신 [구조 분해 문법을 사용하여](/learn/passing-props-to-a-component) `name` prop을 정의하고 직접 읽습니다.\n\n```js\nfunction Greeting({ name }) {\n  return <h1>Hello, {name}!</h1>;\n}\n```\n\n다음은 전체 예시입니다.\n\n<Sandpack>\n\n```js\nfunction Greeting({ name }) {\n  return <h1>Hello, {name}!</h1>;\n}\n\nexport default function App() {\n  return (\n    <>\n      <Greeting name=\"Sara\" />\n      <Greeting name=\"Cahal\" />\n      <Greeting name=\"Edite\" />\n    </>\n  );\n}\n```\n\n</Sandpack>\n\n---\n\n### state가 있는 컴포넌트를 클래스에서 함수로 마이그레이션하기 {/*migrating-a-component-with-state-from-a-class-to-a-function*/}\n\n이 `Counter` 클래스 컴포넌트를 함수로 변환한다고 가정해 봅시다.\n\n<Sandpack>\n\n```js\nimport { Component } from 'react';\n\nexport default class Counter extends Component {\n  state = {\n    name: 'Taylor',\n    age: 42,\n  };\n\n  handleNameChange = (e) => {\n    this.setState({\n      name: e.target.value\n    });\n  }\n\n  handleAgeChange = (e) => {\n    this.setState({\n      age: this.state.age + 1\n    });\n  };\n\n  render() {\n    return (\n      <>\n        <input\n          value={this.state.name}\n          onChange={this.handleNameChange}\n        />\n        <button onClick={this.handleAgeChange}>\n          Increment age\n        </button>\n        <p>Hello, {this.state.name}. You are {this.state.age}.</p>\n      </>\n    );\n  }\n}\n```\n\n```css\nbutton { display: block; margin-top: 10px; }\n```\n\n</Sandpack>\n\n필요한 [state 변수](/reference/react/useState#adding-state-to-a-component)가 있는 함수를 선언하는 것으로 시작하세요.\n\n```js {4-5}\nimport { useState } from 'react';\n\nfunction Counter() {\n  const [name, setName] = useState('Taylor');\n  const [age, setAge] = useState(42);\n  // ...\n```\n\n다음으로 이벤트 핸들러를 변환합니다.\n\n```js {5-7,9-11}\nfunction Counter() {\n  const [name, setName] = useState('Taylor');\n  const [age, setAge] = useState(42);\n\n  function handleNameChange(e) {\n    setName(e.target.value);\n  }\n\n  function handleAgeChange() {\n    setAge(age + 1);\n  }\n  // ...\n```\n\n마지막으로, `this`으로 시작하는 모든 레퍼런스를 컴포넌트에서 정의한 변수 및 함수로 바꿉니다. 예를 들어, `this.state.age`를 `age`로 바꾸고 `this.handleNameChange`를 `handleNameChange`로 바꿉니다.\n\n다음은 완전히 변환된 컴포넌트입니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function Counter() {\n  const [name, setName] = useState('Taylor');\n  const [age, setAge] = useState(42);\n\n  function handleNameChange(e) {\n    setName(e.target.value);\n  }\n\n  function handleAgeChange() {\n    setAge(age + 1);\n  }\n\n  return (\n    <>\n      <input\n        value={name}\n        onChange={handleNameChange}\n      />\n      <button onClick={handleAgeChange}>\n        Increment age\n      </button>\n      <p>Hello, {name}. You are {age}.</p>\n    </>\n  )\n}\n```\n\n```css\nbutton { display: block; margin-top: 10px; }\n```\n\n</Sandpack>\n\n---\n\n### 생명주기 메서드가 있는 컴포넌트를 클래스에서 함수로 마이그레이션하기 {/*migrating-a-component-with-lifecycle-methods-from-a-class-to-a-function*/}\n\n생명주기 메서드가 있는 `ChatRoom` 클래스 컴포넌트를 함수로 변환한다고 가정해 보겠습니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { useState } from 'react';\nimport ChatRoom from './ChatRoom.js';\n\nexport default function App() {\n  const [roomId, setRoomId] = useState('general');\n  const [show, setShow] = useState(false);\n  return (\n    <>\n      <label>\n        Choose the chat room:{' '}\n        <select\n          value={roomId}\n          onChange={e => setRoomId(e.target.value)}\n        >\n          <option value=\"general\">general</option>\n          <option value=\"travel\">travel</option>\n          <option value=\"music\">music</option>\n        </select>\n      </label>\n      <button onClick={() => setShow(!show)}>\n        {show ? 'Close chat' : 'Open chat'}\n      </button>\n      {show && <hr />}\n      {show && <ChatRoom roomId={roomId} />}\n    </>\n  );\n}\n```\n\n```js src/ChatRoom.js active\nimport { Component } from 'react';\nimport { createConnection } from './chat.js';\n\nexport default class ChatRoom extends Component {\n  state = {\n    serverUrl: 'https://localhost:1234'\n  };\n\n  componentDidMount() {\n    this.setupConnection();\n  }\n\n  componentDidUpdate(prevProps, prevState) {\n    if (\n      this.props.roomId !== prevProps.roomId ||\n      this.state.serverUrl !== prevState.serverUrl\n    ) {\n      this.destroyConnection();\n      this.setupConnection();\n    }\n  }\n\n  componentWillUnmount() {\n    this.destroyConnection();\n  }\n\n  setupConnection() {\n    this.connection = createConnection(\n      this.state.serverUrl,\n      this.props.roomId\n    );\n    this.connection.connect();\n  }\n\n  destroyConnection() {\n    this.connection.disconnect();\n    this.connection = null;\n  }\n\n  render() {\n    return (\n      <>\n        <label>\n          Server URL:{' '}\n          <input\n            value={this.state.serverUrl}\n            onChange={e => {\n              this.setState({\n                serverUrl: e.target.value\n              });\n            }}\n          />\n        </label>\n        <h1>Welcome to the {this.props.roomId} room!</h1>\n      </>\n    );\n  }\n}\n```\n\n```js src/chat.js\nexport function createConnection(serverUrl, roomId) {\n  // 실제 구현은 실제로 서버에 연결됩니다.\n  return {\n    connect() {\n      console.log('✅ Connecting to \"' + roomId + '\" room at ' + serverUrl + '...');\n    },\n    disconnect() {\n      console.log('❌ Disconnected from \"' + roomId + '\" room at ' + serverUrl);\n    }\n  };\n}\n```\n\n```css\ninput { display: block; margin-bottom: 20px; }\nbutton { margin-left: 10px; }\n```\n\n</Sandpack>\n\n먼저 [`componentWillUnmount`](#componentwillunmount)가 [`componentDidMount`](#componentdidmount)와 반대 동작을 하는지 확인합니다. 위의 예시에서는 `componentDidMount`가 설정한 연결을 끊습니다. 이러한 로직이 누락된 경우 먼저 추가하세요.\n\n다음으로, [`componentDidUpdate`](#componentdidupdate) 메서드가 `componentDidMount`에서 사용 중인 props 및 state의 변경 사항을 처리하는지 확인합니다. 위의 예시에서 `componentDidMount`는 `this.state.serverUrl`과 `this.props.roomId`를 읽는 `setupConnection`을 호출합니다. 이 때문에 `componentDidUpdate`는 `this.state.serverUrl`과 `this.props.roomId`가 변경되었는지 확인하고, 변경된 경우 연결을 재설정합니다. `componentDidUpdate` 로직이 누락되었거나 모든 관련 props 및 state의 변경 사항을 처리하지 않는 경우 먼저 해당 로직을 수정하세요.\n\n위의 예시에서, 생명주기 메서드 내부의 로직은 컴포넌트를 React 외부의 시스템(채팅 서버)에 연결합니다. 컴포넌트를 외부 시스템에 연결하려면 [이 로직을 하나의 Effect로 설명하세요.](/reference/react/useEffect#connecting-to-an-external-system)\n\n```js {6-12}\nimport { useState, useEffect } from 'react';\n\nfunction ChatRoom({ roomId }) {\n  const [serverUrl, setServerUrl] = useState('https://localhost:1234');\n\n  useEffect(() => {\n    const connection = createConnection(serverUrl, roomId);\n    connection.connect();\n    return () => {\n      connection.disconnect();\n    };\n  }, [serverUrl, roomId]);\n\n  // ...\n}\n```\n\n이 [`useEffect`](/reference/react/useEffect) 호출은 위의 생명주기 메서드의 로직과 동일합니다. 생명주기 메서드가 서로 관련이 없는 여러 가지 작업을 수행하는 경우, [이를 여러 개의 독립적인 Effect로 분할하세요.](/learn/removing-effect-dependencies#is-your-effect-doing-several-unrelated-things) 다음은 완전한 예시입니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { useState } from 'react';\nimport ChatRoom from './ChatRoom.js';\n\nexport default function App() {\n  const [roomId, setRoomId] = useState('general');\n  const [show, setShow] = useState(false);\n  return (\n    <>\n      <label>\n        Choose the chat room:{' '}\n        <select\n          value={roomId}\n          onChange={e => setRoomId(e.target.value)}\n        >\n          <option value=\"general\">general</option>\n          <option value=\"travel\">travel</option>\n          <option value=\"music\">music</option>\n        </select>\n      </label>\n      <button onClick={() => setShow(!show)}>\n        {show ? 'Close chat' : 'Open chat'}\n      </button>\n      {show && <hr />}\n      {show && <ChatRoom roomId={roomId} />}\n    </>\n  );\n}\n```\n\n```js src/ChatRoom.js active\nimport { useState, useEffect } from 'react';\nimport { createConnection } from './chat.js';\n\nexport default function ChatRoom({ roomId }) {\n  const [serverUrl, setServerUrl] = useState('https://localhost:1234');\n\n  useEffect(() => {\n    const connection = createConnection(serverUrl, roomId);\n    connection.connect();\n    return () => {\n      connection.disconnect();\n    };\n  }, [roomId, serverUrl]);\n\n  return (\n    <>\n      <label>\n        Server URL:{' '}\n        <input\n          value={serverUrl}\n          onChange={e => setServerUrl(e.target.value)}\n        />\n      </label>\n      <h1>Welcome to the {roomId} room!</h1>\n    </>\n  );\n}\n```\n\n```js src/chat.js\nexport function createConnection(serverUrl, roomId) {\n  // 실제 구현은 실제로 서버에 연결됩니다.\n  return {\n    connect() {\n      console.log('✅ Connecting to \"' + roomId + '\" room at ' + serverUrl + '...');\n    },\n    disconnect() {\n      console.log('❌ Disconnected from \"' + roomId + '\" room at ' + serverUrl);\n    }\n  };\n}\n```\n\n```css\ninput { display: block; margin-bottom: 20px; }\nbutton { margin-left: 10px; }\n```\n\n</Sandpack>\n\n<Note>\n\n컴포넌트가 외부 시스템과 동기화되지 않는 경우 [Effect가 필요하지 않을 수 있습니다.](/learn/you-might-not-need-an-effect)\n\n</Note>\n\n---\n\n### context가 있는 컴포넌트를 클래스에서 함수로 마이그레이션하기 {/*migrating-a-component-with-context-from-a-class-to-a-function*/}\n\n이 예시에서 `Panel` 및 `Button` 클래스 컴포넌트는 [`this.context`](#context)에서 [context](/learn/passing-data-deeply-with-context)를 읽습니다.\n\n<Sandpack>\n\n```js\nimport { createContext, Component } from 'react';\n\nconst ThemeContext = createContext(null);\n\nclass Panel extends Component {\n  static contextType = ThemeContext;\n\n  render() {\n    const theme = this.context;\n    const className = 'panel-' + theme;\n    return (\n      <section className={className}>\n        <h1>{this.props.title}</h1>\n        {this.props.children}\n      </section>\n    );\n  }\n}\n\nclass Button extends Component {\n  static contextType = ThemeContext;\n\n  render() {\n    const theme = this.context;\n    const className = 'button-' + theme;\n    return (\n      <button className={className}>\n        {this.props.children}\n      </button>\n    );\n  }\n}\n\nfunction Form() {\n  return (\n    <Panel title=\"Welcome\">\n      <Button>Sign up</Button>\n      <Button>Log in</Button>\n    </Panel>\n  );\n}\n\nexport default function MyApp() {\n  return (\n    <ThemeContext value=\"dark\">\n      <Form />\n    </ThemeContext>\n  )\n}\n```\n\n```css\n.panel-light,\n.panel-dark {\n  border: 1px solid black;\n  border-radius: 4px;\n  padding: 20px;\n}\n.panel-light {\n  color: #222;\n  background: #fff;\n}\n\n.panel-dark {\n  color: #fff;\n  background: rgb(23, 32, 42);\n}\n\n.button-light,\n.button-dark {\n  border: 1px solid #777;\n  padding: 5px;\n  margin-right: 10px;\n  margin-top: 10px;\n}\n\n.button-dark {\n  background: #222;\n  color: #fff;\n}\n\n.button-light {\n  background: #fff;\n  color: #222;\n}\n```\n\n</Sandpack>\n\n함수 컴포넌트로 변환할 때는 `this.context`를 [`useContext`](/reference/react/useContext) 호출로 바꿔주세요.\n\n<Sandpack>\n\n```js\nimport { createContext, useContext } from 'react';\n\nconst ThemeContext = createContext(null);\n\nfunction Panel({ title, children }) {\n  const theme = useContext(ThemeContext);\n  const className = 'panel-' + theme;\n  return (\n    <section className={className}>\n      <h1>{title}</h1>\n      {children}\n    </section>\n  )\n}\n\nfunction Button({ children }) {\n  const theme = useContext(ThemeContext);\n  const className = 'button-' + theme;\n  return (\n    <button className={className}>\n      {children}\n    </button>\n  );\n}\n\nfunction Form() {\n  return (\n    <Panel title=\"Welcome\">\n      <Button>Sign up</Button>\n      <Button>Log in</Button>\n    </Panel>\n  );\n}\n\nexport default function MyApp() {\n  return (\n    <ThemeContext value=\"dark\">\n      <Form />\n    </ThemeContext>\n  )\n}\n```\n\n```css\n.panel-light,\n.panel-dark {\n  border: 1px solid black;\n  border-radius: 4px;\n  padding: 20px;\n}\n.panel-light {\n  color: #222;\n  background: #fff;\n}\n\n.panel-dark {\n  color: #fff;\n  background: rgb(23, 32, 42);\n}\n\n.button-light,\n.button-dark {\n  border: 1px solid #777;\n  padding: 5px;\n  margin-right: 10px;\n  margin-top: 10px;\n}\n\n.button-dark {\n  background: #222;\n  color: #fff;\n}\n\n.button-light {\n  background: #fff;\n  color: #222;\n}\n```\n\n</Sandpack>\n"
  },
  {
    "path": "src/content/reference/react/Fragment.md",
    "content": "---\ntitle: <Fragment> (<>...</>)\n---\n\n<Intro>\n\n`<Fragment>`는 `<>...</>` 문법으로 자주 사용되며, 래퍼 노드 없이 엘리먼트를 그룹화할 수 있게 해줍니다.\n\n<Canary> Fragment는 `ref`를 받을 수도 있으며, 래퍼 엘리먼트를 추가하지 않고도 기본 DOM 노드와 상호작용할 수 있습니다. 아래 레퍼런스와 사용법을 참고하세요.</Canary>\n\n```js\n<>\n  <OneChild />\n  <AnotherChild />\n</>\n```\n\n</Intro>\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `<Fragment>` {/*fragment*/}\n\n하나의 엘리먼트가 필요한 상황에서 엘리먼트를 `<Fragment>`로 감싸서 그룹화하세요. `Fragment` 안에서 그룹화된 엘리먼트는 DOM 결과물에 영향을 주지 않습니다. 즉, 엘리먼트가 그룹화되지 않은 것과 같습니다. 대부분의 경우 빈 JSX 태그인 `<></>`는 `<Fragment></Fragment>`의 축약형입니다.\n\n#### Props {/*props*/}\n- **optional** `key`: 명시적 `<Fragment>`로 선언된 `Fragment`에는 [`key`](/learn/rendering-lists#keeping-list-items-in-order-with-key)를 사용할 수 있습니다.\n\n- <CanaryBadge />  **optional** `ref`: ref 객체(예: [`useRef`](/reference/react/useRef)에서 반환된 것) 또는 [콜백 함수](/reference/react-dom/components/common#ref-callback)입니다. React는 `Fragment`로 감싼 DOM 노드와 상호작용하기 위한 메서드를 구현한 `FragmentInstance`를 ref 값으로 제공합니다.\n\n### <CanaryBadge /> FragmentInstance {/*fragmentinstance*/}\n\n`Fragment`에 `ref`를 전달하면, React는 `Fragment`로 감싼 DOM 노드와 상호작용하기 위한 메서드가 포함된 `FragmentInstance` 객체를 제공합니다.\n\n**이벤트 처리 메서드**\n- `addEventListener(type, listener, options?)`: Fragment의 모든 최상위 DOM 자식에 이벤트 리스너를 추가합니다.\n- `removeEventListener(type, listener, options?)`: Fragment의 모든 최상위 DOM 자식에서 이벤트 리스너를 제거합니다.\n- `dispatchEvent(event)`: Fragment의 가상 자식에 이벤트를 디스패치하여 추가된 리스너를 호출하며, DOM 부모로 버블링될 수 있습니다.\n\n**레이아웃 메서드**\n- `compareDocumentPosition(otherNode)`: Fragment의 문서 위치를 다른 노드와 비교합니다.\n  - Fragment에 자식이 있으면 네이티브 `compareDocumentPosition` 값이 반환됩니다.\n  - 빈 Fragment는 React 트리 내에서 위치를 비교하며 `Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC`을 포함합니다.\n  - 포탈이나 다른 삽입으로 인해 React 트리와 DOM 트리에서 다른 관계를 가진 엘리먼트는 `Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC`입니다.\n- `getClientRects()`: 모든 자식의 경계 사각형을 나타내는 `DOMRect` 객체의 평탄화된 배열을 반환합니다.\n- `getRootNode()`: Fragment의 부모 DOM 노드를 포함하는 루트 노드를 반환합니다.\n\n**포커스 관리 메서드**\n- `focus(options?)`: Fragment 내의 첫 번째 포커스 가능한 DOM 노드에 포커스합니다. 중첩된 자식에 대해 깊이 우선으로 포커스를 시도합니다.\n- `focusLast(options?)`: Fragment 내의 마지막 포커스 가능한 DOM 노드에 포커스합니다. 중첩된 자식에 대해 깊이 우선으로 포커스를 시도합니다.\n- `blur()`: `document.activeElement`가 Fragment 내에 있으면 포커스를 제거합니다.\n\n**옵저버 메서드**\n- `observeUsing(observer)`: IntersectionObserver 또는 ResizeObserver로 Fragment의 DOM 자식을 관찰하기 시작합니다.\n- `unobserveUsing(observer)`: 지정된 옵저버로 Fragment의 DOM 자식 관찰을 중지합니다.\n\n#### 주의 사항 {/*caveats*/}\n\n- Fragment에 `key`를 사용하려면 `<>...</>` 구문을 사용할 수 없습니다. 명시적으로 `react`에서 `Fragment`를 불러오고<sup>Import</sup> `<Fragment key={yourKey}>...</Fragment>`를 렌더링해야 합니다.\n\n- React는 `<><Child /></>`에서 `[<Child />]`로 렌더링하거나 (또는 반대의 경우), 혹은 `<><Child /></>` 에서 `<Child />` 렌더링하거나 (또는 반대의 경우) [State를 초기화](/learn/preserving-and-resetting-state)하지 않습니다. 이는 오직 한 단계 깊이<sup>Single Level Deep</sup>까지만 적용됩니다. 예를 들어 `<><><Child /></></>` 에서 `<Child />`로 렌더링하는 것은 State가 초기화됩니다. 정확한 의미는 [여기](https://gist.github.com/clemmy/b3ef00f9507909429d8aa0d3ee4f986b)서 확인할 수 있습니다.\n\n- <CanaryBadge /> Fragment에 `ref`를 전달하려면 `<>...</>` 문법을 사용할 수 없습니다. 명시적으로 `'react'`에서 `Fragment`를 불러오고 `<Fragment ref={yourRef}>...</Fragment>`를 렌더링해야 합니다.\n\n---\n\n## 사용법 {/*usage*/}\n\n### 여러 엘리먼트 반환하기 {/*returning-multiple-elements*/}\n\n여러 엘리먼트를 함께 그룹화하기 위해 `Fragment`나 `<>...</>` 문법을 사용하세요. 한 개의 엘리먼트가 존재할 수 있는 곳에 여러 엘리먼트를 넣을 수 있습니다. 예를 들어 컴포넌트는 한 개의 엘리먼트만 반환할 수 있지만 `Fragment`를 사용하여 여러 엘리먼트를 함께 그룹화하여 반환할 수 있습니다.\n\n```js {3,6}\nfunction Post() {\n  return (\n    <>\n      <PostTitle />\n      <PostBody />\n    </>\n  );\n}\n```\n\n`Fragment`로 엘리먼트를 그룹화하면 DOM 엘리먼트와 같은 다른 컨테이너로 엘리먼트를 감싸는 경우와는 달리, 레이아웃이나 스타일에 영향을 주지 않기 때문에 `Fragment`는 효과적입니다. 브라우저로 아래 예시를 검사하면 모든 `<h1>`, `<article>` DOM 노드가 래퍼 없이 형제 노드로 나타나는 것을 볼 수 있습니다.\n\n<Sandpack>\n\n```js\nexport default function Blog() {\n  return (\n    <>\n      <Post title=\"An update\" body=\"It's been a while since I posted...\" />\n      <Post title=\"My new blog\" body=\"I am starting a new blog!\" />\n    </>\n  )\n}\n\nfunction Post({ title, body }) {\n  return (\n    <>\n      <PostTitle title={title} />\n      <PostBody body={body} />\n    </>\n  );\n}\n\nfunction PostTitle({ title }) {\n  return <h1>{title}</h1>\n}\n\nfunction PostBody({ body }) {\n  return (\n    <article>\n      <p>{body}</p>\n    </article>\n  );\n}\n```\n\n</Sandpack>\n\n<DeepDive>\n\n#### 특별한 문법 없이 `Fragment`를 작성하는 방법은 무엇인가요? {/*how-to-write-a-fragment-without-the-special-syntax*/}\n\n위의 예시는 React에서 `Fragment`를 불러오는<sup>Import</sup> 것과 동일합니다.\n\n```js {1,5,8}\nimport { Fragment } from 'react';\n\nfunction Post() {\n  return (\n    <Fragment>\n      <PostTitle />\n      <PostBody />\n    </Fragment>\n  );\n}\n```\n\n일반적으로 [`Fragment`에 `key`를 넘겨야 하는 경우](#rendering-a-list-of-fragments)가 아니라면 이 기능은 필요하지 않습니다.\n\n</DeepDive>\n\n---\n\n### 변수에 여러 엘리먼트 할당 {/*assigning-multiple-elements-to-a-variable*/}\n\n다른 엘리먼트와 마찬가지로 `Fragment`를 변수에 할당하고 Props로 전달하는 등의 작업을 할 수 있습니다.\n\n```js\nfunction CloseDialog() {\n  const buttons = (\n    <>\n      <OKButton />\n      <CancelButton />\n    </>\n  );\n  return (\n    <AlertDialog buttons={buttons}>\n      Are you sure you want to leave this page?\n    </AlertDialog>\n  );\n}\n```\n\n---\n\n### 텍스트와 함께 엘리먼트 그룹화 {/*grouping-elements-with-text*/}\n\n`Fragment`를 사용하여 텍스트를 컴포넌트와 함께 그룹화할 수 있습니다.\n\n```js\nfunction DateRangePicker({ start, end }) {\n  return (\n    <>\n      From\n      <DatePicker date={start} />\n      to\n      <DatePicker date={end} />\n    </>\n  );\n}\n```\n\n---\n\n### `Fragment` 리스트 렌더링 {/*rendering-a-list-of-fragments*/}\n\n`<></>` 문법을 사용하는 대신 명시적으로 `Fragment`를 작성해야 하는 상황이 있습니다. [반복을 통해 여러 엘리먼트를 렌더링할 때](/learn/rendering-lists) 각 요소에 `key`를 할당해야 합니다. 반복 안에 엘리먼트가 `Fragment`인 경우 `key` 속성을 제공하기 위해 일반 JSX 엘리먼트 문법을 사용해야 합니다.\n\n```js {3,6}\nfunction Blog() {\n  return posts.map(post =>\n    <Fragment key={post.id}>\n      <PostTitle title={post.title} />\n      <PostBody body={post.body} />\n    </Fragment>\n  );\n}\n```\n\nDOM을 검사하여 `Fragment` 자식 주위에 래퍼 엘리먼트가 없는 것을 확인할 수 있습니다.\n\n<Sandpack>\n\n```js\nimport { Fragment } from 'react';\n\nconst posts = [\n  { id: 1, title: 'An update', body: \"It's been a while since I posted...\" },\n  { id: 2, title: 'My new blog', body: 'I am starting a new blog!' }\n];\n\nexport default function Blog() {\n  return posts.map(post =>\n    <Fragment key={post.id}>\n      <PostTitle title={post.title} />\n      <PostBody body={post.body} />\n    </Fragment>\n  );\n}\n\nfunction PostTitle({ title }) {\n  return <h1>{title}</h1>\n}\n\nfunction PostBody({ body }) {\n  return (\n    <article>\n      <p>{body}</p>\n    </article>\n  );\n}\n```\n\n</Sandpack>\n\n---\n\n### <CanaryBadge /> Fragment ref를 사용한 DOM 상호작용 {/*using-fragment-refs-for-dom-interaction*/}\n\nFragment ref를 사용하면 래퍼 엘리먼트를 추가하지 않고도 Fragment로 감싼 DOM 노드와 상호작용할 수 있습니다. 이벤트 처리, 가시성 추적, 포커스 관리, 그리고 `ReactDOM.findDOMNode()`와 같이 더 이상 사용되지 않는 패턴을 대체하는 데 유용합니다.\n\n```js\nimport { Fragment } from 'react';\n\nfunction ClickableFragment({ children, onClick }) {\n  return (\n    <Fragment ref={fragmentInstance => {\n      fragmentInstance.addEventListener('click', handleClick);\n      return () => fragmentInstance.removeEventListener('click', handleClick);\n    }}>\n      {children}\n    </Fragment>\n  );\n}\n```\n---\n\n### <CanaryBadge /> Fragment ref로 가시성 추적하기 {/*tracking-visibility-with-fragment-refs*/}\n\nFragment ref는 가시성 추적과 교차 관찰에 유용합니다. 자식 컴포넌트가 ref를 노출하지 않아도 콘텐츠가 화면에 보이는 시점을 모니터링할 수 있습니다.\n\n```js {19,21,31-34}\nimport { Fragment, useRef, useLayoutEffect } from 'react';\n\nfunction VisibilityObserverFragment({ threshold = 0.5, onVisibilityChange, children }) {\n  const fragmentRef = useRef(null);\n\n  useLayoutEffect(() => {\n    const observer = new IntersectionObserver(\n      (entries) => {\n        onVisibilityChange(entries.some(entry => entry.isIntersecting))\n      },\n      { threshold }\n    );\n    \n    fragmentRef.current.observeUsing(observer);\n    return () => fragmentRef.current.unobserveUsing(observer);\n  }, [threshold, onVisibilityChange]);\n\n  return (\n    <Fragment ref={fragmentRef}>\n      {children}\n    </Fragment>\n  );\n}\n\nfunction MyComponent() {\n  const handleVisibilityChange = (isVisible) => {\n    console.log('Component is', isVisible ? 'visible' : 'hidden');\n  };\n\n  return (\n    <VisibilityObserverFragment onVisibilityChange={handleVisibilityChange}>\n      <SomeThirdPartyComponent />\n      <AnotherComponent />\n    </VisibilityObserverFragment>\n  );\n}\n```\n\n이 패턴은 Effect 기반 가시성 로깅의 대안이며, Effect 기반 방식은 대부분의 경우 안티패턴입니다. Effect에만 의존하면 렌더링된 컴포넌트가 사용자에게 실제로 보이는지 보장할 수 없습니다.\n\n---\n\n### <CanaryBadge /> Fragment ref로 포커스 관리하기 {/*focus-management-with-fragment-refs*/}\n\nFragment ref는 Fragment 내의 모든 DOM 노드에서 동작하는 포커스 관리 메서드를 제공합니다.\n\n```js\nimport { Fragment, useRef } from 'react';\n\nfunction FocusFragment({ children }) {\n  return (\n    <Fragment ref={(fragmentInstance) => fragmentInstance?.focus()}>\n      {children}\n    </Fragment>\n  );\n}\n```\n\n`focus()` 메서드는 Fragment 내의 첫 번째 포커스 가능한 엘리먼트에 포커스하고, `focusLast()`는 마지막 포커스 가능한 엘리먼트에 포커스합니다.\n"
  },
  {
    "path": "src/content/reference/react/Profiler.md",
    "content": "---\ntitle: <Profiler>\n---\n\n<Intro>\n\n`<Profiler>`를 통해 React 트리의 렌더링 성능을 프로그래밍 방식으로 측정할 수 있습니다.\n\n```js\n<Profiler id=\"App\" onRender={onRender}>\n  <App />\n</Profiler>\n```\n\n</Intro>\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `<Profiler>` {/*profiler*/}\n\n렌더링 성능을 측정하기 위해서 컴포넌트 트리를 `<Profiler>`로 감싸줍니다.\n\n```js\n<Profiler id=\"App\" onRender={onRender}>\n  <App />\n</Profiler>\n```\n\n#### Props {/*props*/}\n\n* `id`: 성능을 측정하는 UI 컴포넌트를 식별하기 위한 문자열입니다.\n* `onRender`: 프로파일링된 트리 내의 컴포넌트가 업데이트될 때마다 React가 호출하는 [`onRender` 콜백](#onrender-callback)입니다. 렌더링된 내용과 소요된 시간에 대한 정보를 받습니다.\n\n#### Caveats {/*caveats*/}\n\n* Profiling adds some additional overhead, so **it is disabled in the production build by default.** To opt into production profiling, you need to enable a [special production build with profiling enabled.](/reference/dev-tools/react-performance-tracks#using-profiling-builds)\n\n* 프로파일링은 추가적인 오버헤드를 더하기 때문에, **프로덕션 빌드에서는 기본적으로 비활성화 되어있습니다.** 프로덕션 프로파일링을 사용하려면, [프로파일링 기능이 활성화된 특수한 프로덕션 빌드](https://fb.me/react-profiling)를 사용해야 합니다.\n---\n\n### `onRender` 콜백 {/*onrender-callback*/}\n\nReact는 `onRender` 콜백을 렌더링된 내용과 같이 호출합니다.\n\n```js\nfunction onRender(id, phase, actualDuration, baseDuration, startTime, commitTime) {\n  // 렌더링 시간 집계 혹은 로그...\n}\n```\n\n#### 매개변수 {/*onrender-parameters*/}\n\n* `id`: 커밋된 `<Profiler>` 트리의 문자열 `id` 프로퍼티입니다. 프로파일러를 다중으로 사용하고 있는 트리 내에서 어떤 부분이 커밋 되었는지 식별할 수 있도록 해줍니다.\n* `phase`: `\"mount\"`, `\"update\"` 혹은 `\"nested-update\"`. 트리가 최초로 마운트되었는지 또는 Props, State, Hook의 변경으로 인해 리렌더링 되었는지 알 수 있습니다.\n* `actualDuration`: 현재 업데이트에 대해 `<Profiler>`와 자식들을 렌더링하는데 소요된 시간(밀리초)입니다. 이는 하위 트리가 메모이제이션(예: [`memo`](/reference/react/memo)와 [`useMemo`](/reference/react/useMemo))을 얼마나 잘 사용하는지를 나타냅니다. 많은 자식들이 특정 Props가 변경되는 경우에만 다시 렌더링되어야 하므로, 이상적으로는 이 값은 최초 마운트 이후에는 많이 감소해야 합니다.\n* `baseDuration`: 최적화 없이 전체 `<Profiler>` 하위 트리에 대해 걸리는 시간을 추정하는 소요된 시간(밀리초)입니다. 트리에 있는 각 컴포넌트의 가장 최근 렌더링 시간을 합산하여 계산됩니다. 이 값은 최악의 렌더링 비용(예: 최초 마운트 또는 메모이제이션이 없는 트리)을 추정합니다. `actualDuration`과 비교하여 메모이제이션이 작동하는지 확인합니다.\n* `startTime`: React가 현재 업데이트 렌더링을 시작한 시점에 대한 숫자 타임스탬프입니다.\n* `commitTime`: React가 현재 업데이트를 커밋한 시점에 대한 숫자 타임스탬프입니다. 이 값은 커밋된 모든 프로파일러 간에 공유되므로 원하는 경우 그룹화할 수 있습니다.\n\n---\n\n## 사용법 {/*usage*/}\n\n### 프로그래밍 방식으로 렌더링 성능 측정 {/*measuring-rendering-performance-programmatically*/}\n\nReact 트리를 `<Profiler>` 컴포넌트로 감싸서 렌더링 성능을 측정합니다.\n\n```js {2,4}\n<App>\n  <Profiler id=\"Sidebar\" onRender={onRender}>\n    <Sidebar />\n  </Profiler>\n  <PageContent />\n</App>\n```\n\nUI 컴포넌트를 식별하기 위한 `id` 문자열과 트리 내의 컴포넌트가 업데이트를 커밋할 때마다 React가 호출하는 `onRender` 콜백 함수 두 개의 Props가 요구됩니다.\n\n<Pitfall>\n\nProfiling adds some additional overhead, so **it is disabled in the production build by default.** To opt into production profiling, you need to enable a [special production build with profiling enabled.](/reference/dev-tools/react-performance-tracks#using-profiling-builds)\n\n</Pitfall>\n\n<Note>\n\n`<Profiler>`는 프로그래밍 방식으로 측정값들을 모아줍니다. 상호작용할 수 있는 프로파일러를 찾고 있다면, [React 개발자 도구](/learn/react-developer-tools)의 프로파일러 탭을 사용해 보세요. 브라우저 확장 프로그램으로써 유사한 기능을 제공합니다.\n\nComponents wrapped in `<Profiler>` will also be marked in the [Component tracks](/reference/dev-tools/react-performance-tracks#components) of React Performance tracks even in profiling builds.\nIn development builds, all components are marked in the Components track regardless of whether they're wrapped in `<Profiler>`.\n\n</Note>\n\n---\n\n### 애플리케이션의 부분별 측정 {/*measuring-different-parts-of-the-application*/}\n\n`<Profiler>` 컴포넌트를 여러개 사용하여 애플리케이션을 부분별로 측정할 수 있습니다.\n\n```js {5,7}\n<App>\n  <Profiler id=\"Sidebar\" onRender={onRender}>\n    <Sidebar />\n  </Profiler>\n  <Profiler id=\"Content\" onRender={onRender}>\n    <Content />\n  </Profiler>\n</App>\n```\n\n`<Profiler>` 컴포넌트들을 중첩해서 사용할 수 있습니다.\n\n```js {5,7,9,12}\n<App>\n  <Profiler id=\"Sidebar\" onRender={onRender}>\n    <Sidebar />\n  </Profiler>\n  <Profiler id=\"Content\" onRender={onRender}>\n    <Content>\n      <Profiler id=\"Editor\" onRender={onRender}>\n        <Editor />\n      </Profiler>\n      <Preview />\n    </Content>\n  </Profiler>\n</App>\n```\n\n`<Profiler>`는 가벼운 컴포넌트이지만 사용할 때마다 애플리케이션에 약간의 CPU 및 메모리 오버헤드를 추가하기 때문에 필요할 때만 사용해야 합니다.\n\n---\n\n"
  },
  {
    "path": "src/content/reference/react/PureComponent.md",
    "content": "---\ntitle: PureComponent\n---\n\n<Pitfall>\n\n컴포넌트를 클래스 대신 함수로 정의하는 것을 권장합니다. [마이그레이션 방법.](#alternatives)\n\n</Pitfall>\n\n<Intro>\n\n`PureComponent`는 [`Component`](https://react.dev/reference/react/Component)와 비슷하지만 같은 props와 state에 대해서 다시 렌더링하지 않는다는 점에서 다릅니다. 클래스 컴포넌트를 계속 사용할 수 있지만 새로운 코드에서는 클래스 컴포넌트 사용을 추천하지 않습니다.\n\n```js\nclass Greeting extends PureComponent {\n  render() {\n    return <h1>Hello, {this.props.name}!</h1>;\n  }\n}\n```\n\n</Intro>\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `PureComponent` {/*purecomponent*/}\n\n같은 props와 state에 대해서 다시 렌더링하지 않으려면 [`Component`](/reference/react/Component) 대신 `PureComponent`를 extend 해주세요.\n\n```js\nimport { PureComponent } from 'react';\n\nclass Greeting extends PureComponent {\n  render() {\n    return <h1>Hello, {this.props.name}!</h1>;\n  }\n}\n```\n\n`PureComponent`는 [`Component`의 모든 API](/reference/react/Component#reference)를 지원하는 `Component`의 서브클래스 입니다. `PureComponent`를 extend 하는 것은 단순히 props와 state를 비교하는 사용자 [`shouldComponentUpdate`](/reference/react/Component#shouldcomponentupdate) 메서드를 정의하는 것과 같습니다.\n\n[아래 예시 보기.](#usage)\n\n---\n\n## 사용법 {/*usage*/}\n\n### 클래스 컴포넌트에서 불필요한 재 렌더링 건너뛰기 {/*skipping-unnecessary-re-renders-for-class-components*/}\n\nReact는 일반적으로 부모가 다시 렌더링 될 때마다 자식 컴포넌트도 다시 렌더링 합니다. 하지만 `PureComponent`를 extend 하여 새 props 및 state가 이전 props 및 state와 같다면 부모가 다시 렌더링 되더라도 자식 컴포넌트는 다시 렌더링 되지 않도록 [Class component](/reference/react/Component)를 최적화할 수 있습니다.\n\n```js {1}\nclass Greeting extends PureComponent {\n  render() {\n    return <h1>Hello, {this.props.name}!</h1>;\n  }\n}\n```\n\nReact 컴포넌트에는 항상 [순수한 렌더링 로직](/learn/keeping-components-pure)이 있어야 합니다. 즉, props, state 및 context가 변경되지 않은 경우 같은 출력을 반환해야 합니다. `PureComponent`를 사용하면 컴포넌트가 이 요구 사항을 준수한다고 React에게 알리므로 props 및 state가 변경되지 않는 한 React는 다시 렌더링하지 않습니다. 그러나 사용 중인 context가 변경된다면 컴포넌트는 다시 렌더링 됩니다.\n\n이 예시에서 `Greeting` 컴포넌트는 `name`이 변경될 때마다 다시 렌더링 되지만 (props 중 하나이기 때문에) `address`가 변경될 때에는 다시 렌더링 되지 않습니다 (`Greeting`에 prop으로 전달되지 않기 때문에).\n\n<Sandpack>\n\n```js\nimport { PureComponent, useState } from 'react';\n\nclass Greeting extends PureComponent {\n  render() {\n    console.log(\"Greeting was rendered at\", new Date().toLocaleTimeString());\n    return <h3>Hello{this.props.name && ', '}{this.props.name}!</h3>;\n  }\n}\n\nexport default function MyApp() {\n  const [name, setName] = useState('');\n  const [address, setAddress] = useState('');\n  return (\n    <>\n      <label>\n        Name{': '}\n        <input value={name} onChange={e => setName(e.target.value)} />\n      </label>\n      <label>\n        Address{': '}\n        <input value={address} onChange={e => setAddress(e.target.value)} />\n      </label>\n      <Greeting name={name} />\n    </>\n  );\n}\n```\n\n```css\nlabel {\n  display: block;\n  margin-bottom: 16px;\n}\n```\n\n</Sandpack>\n\n<Pitfall>\n\n컴포넌트를 클래스 대신 함수로 정의하는 것을 권장합니다. [마이그레이션 방법.](#alternatives)\n\n</Pitfall>\n\n---\n\n## 대안 {/*alternatives*/}\n\n### `PureComponent` 클래스 컴포넌트에서 함수 컴포넌트로 마이그레이션 하기 {/*migrating-from-a-purecomponent-class-component-to-a-function*/}\n\n새로운 코드에서는 [클래스 컴포넌트](/reference/react/Component) 대신 함수 컴포넌트 사용을 권장합니다. `PureComponent`를 사용하는 기존 클래스 컴포넌트가 있는 경우 다음과 같이 변환할 수 있습니다.\n\n아래는 기존 코드 입니다.\n\n<Sandpack>\n\n```js\nimport { PureComponent, useState } from 'react';\n\nclass Greeting extends PureComponent {\n  render() {\n    console.log(\"Greeting was rendered at\", new Date().toLocaleTimeString());\n    return <h3>Hello{this.props.name && ', '}{this.props.name}!</h3>;\n  }\n}\n\nexport default function MyApp() {\n  const [name, setName] = useState('');\n  const [address, setAddress] = useState('');\n  return (\n    <>\n      <label>\n        Name{': '}\n        <input value={name} onChange={e => setName(e.target.value)} />\n      </label>\n      <label>\n        Address{': '}\n        <input value={address} onChange={e => setAddress(e.target.value)} />\n      </label>\n      <Greeting name={name} />\n    </>\n  );\n}\n```\n\n```css\nlabel {\n  display: block;\n  margin-bottom: 16px;\n}\n```\n\n</Sandpack>\n\n이 [컴포넌트를 클래스 컴포넌트에서 함수 컴포넌트로 변환](/reference/react/Component#alternatives)할 때 [`memo`](/reference/react/memo)로 감싸면 됩니다.\n\n<Sandpack>\n\n```js\nimport { memo, useState } from 'react';\n\nconst Greeting = memo(function Greeting({ name }) {\n  console.log(\"Greeting was rendered at\", new Date().toLocaleTimeString());\n  return <h3>Hello{name && ', '}{name}!</h3>;\n});\n\nexport default function MyApp() {\n  const [name, setName] = useState('');\n  const [address, setAddress] = useState('');\n  return (\n    <>\n      <label>\n        Name{': '}\n        <input value={name} onChange={e => setName(e.target.value)} />\n      </label>\n      <label>\n        Address{': '}\n        <input value={address} onChange={e => setAddress(e.target.value)} />\n      </label>\n      <Greeting name={name} />\n    </>\n  );\n}\n```\n\n```css\nlabel {\n  display: block;\n  margin-bottom: 16px;\n}\n```\n\n</Sandpack>\n\n<Note>\n\n`PureComponent`와 달리 [`memo`](/reference/react/memo)는 새 state와 이전 state를 비교하지 않습니다. 함수 컴포넌트에서 동일한 state로 [`set` 함수](/reference/react/useState#setstate)를 호출하면 [기본적으로 `memo` 없이도 다시 렌더링 되지 않습니다.](/reference/react/memo#updating-a-memoized-component-using-state)\n\n</Note>\n"
  },
  {
    "path": "src/content/reference/react/StrictMode.md",
    "content": "---\ntitle: <StrictMode>\n---\n\n\n<Intro>\n\n`<StrictMode>`를 통해 개발 중에 컴포넌트에서 일반적인 버그를 빠르게 찾을 수 있도록 합니다.\n\n\n```js\n<StrictMode>\n  <App />\n</StrictMode>\n```\n\n</Intro>\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `<StrictMode>` {/*strictmode*/}\n\n컴포넌트 트리 내부에서 추가적인 개발 동작 및 경고를 활성화하기 위해 `StrictMode`를 사용하세요.\n\n```js\nimport { StrictMode } from 'react';\nimport { createRoot } from 'react-dom/client';\n\nconst root = createRoot(document.getElementById('root'));\nroot.render(\n  <StrictMode>\n    <App />\n  </StrictMode>\n);\n```\n\n[아래 예시를 참고하세요.](#usage)\n\nStrict Mode는 다음과 같은 개발 전용 동작을 활성화합니다.\n\n- 컴포넌트가 순수하지 않은 렌더링으로 인한 버그를 찾기 위해 [추가로 다시 렌더링합니다.](#fixing-bugs-found-by-double-rendering-in-development)\n- 컴포넌트가 Effect 클린업이 누락되어 발생한 버그를 찾기 위해 [Effect를 다시 실행합니다.](#fixing-bugs-found-by-re-running-effects-in-development)\n- 컴포넌트가 Ref 클린업이 누락되어 발생한 버그를 찾기 위해 [Ref 콜백을 다시 실행합니다.](#fixing-bugs-found-by-re-running-ref-callbacks-in-development)\n- 컴포넌트가 [더 이상 사용되지 않는 API를 사용하는지 확인합니다.](#fixing-deprecation-warnings-enabled-by-strict-mode)\n\n\n#### Props {/*props*/}\n\n`StrictMode`는 Props를 받지 않습니다.\n\n#### 주의 사항 {/*caveats*/}\n\n* `<StrictMode>`로 래핑된 트리 내에서 Strict Mode를 해제할 수 있는 방법은 없습니다. 이를 통해 `<StrictMode>` 내부의 모든 컴포넌트가 검사되었음을 확신할 수 있습니다. 제품을 개발하는 두 팀이 검사가 가치 있는지에 대해 의견이 갈리는 경우, 합의에 도달하거나 `<StrictMode>`를 트리에서 하단으로 옮겨야 합니다.\n\n---\n\n## 사용법 {/*usage*/}\n\n### 전체 앱에 대해 Strict Mode 활성화 {/*enabling-strict-mode-for-entire-app*/}\n\nStrict Mode는 `<StrictMode>` 컴포넌트 내부의 모든 컴포넌트 트리에 대해 추가적인 개발 전용 검사를 활성화합니다. 이러한 검사는 개발 프로세스 초기에 컴포넌트에서 일반적인 버그를 찾는 데 도움이 됩니다.\n\n\n전체 앱에 대한 Strict Mode를 활성화하려면 렌더링할 때 루트 컴포넌트를 `<StrictMode>`로 래핑하세요.\n\n```js {6,8}\nimport { StrictMode } from 'react';\nimport { createRoot } from 'react-dom/client';\n\nconst root = createRoot(document.getElementById('root'));\nroot.render(\n  <StrictMode>\n    <App />\n  </StrictMode>\n);\n```\n\n전체 앱을 (특히 새로 생성된 앱의 경우) Strict Mode로 래핑하는 것을 권장합니다. `createRoot`를 호출하는 프레임워크를 사용하는 경우, Strict Mode를 활성화하는 방법에 대한 문서를 확인하세요.\n\nStrict Mode 검사는 **개발 환경에서만 실행되지만**, 이미 코드에 존재하는 버그를 찾아내는 데 도움을 줍니다. 이러한 버그는 실제 운영 환경에서 재현하기 까다로울 수 있습니다. Strict Mode를 사용하면 사용자가 보고하기 전에 버그를 수정할 수 있습니다.\n\n<Note>\n\nStrict Mode에서는 개발 시 다음과 같은 검사를 가능하게 합니다.\n\n- 컴포넌트가 순수하지 않은 렌더링으로 인한 버그를 찾기 위해 [추가로 다시 렌더링합니다.](#fixing-bugs-found-by-double-rendering-in-development)\n- 컴포넌트가 Effect 클린업이 누락되어 발생한 버그를 찾기 위해 [Effect를 다시 실행합니다.](#fixing-bugs-found-by-re-running-effects-in-development)\n- 컴포넌트가 Ref 클린업이 누락되어 발생한 버그를 찾기 위해 [Ref 콜백을 다시 실행합니다.](#fixing-bugs-found-by-re-running-ref-callbacks-in-development)\n- 컴포넌트가 [더 이상 사용되지 않는 API를 사용하는지 확인합니다.](#fixing-deprecation-warnings-enabled-by-strict-mode)\n\n\n**이러한 모든 검사는 개발 환경 전용이며 프로덕션 빌드에는 영향을 미치지 않습니다.**\n\n</Note>\n\n---\n\n### 앱의 일부분에서 Strict Mode 활성화 {/*enabling-strict-mode-for-a-part-of-the-app*/}\n\n애플리케이션의 어떤 부분에서라도 Strict Mode를 활성화할 수 있습니다.\n\n```js {7,12}\nimport { StrictMode } from 'react';\n\nfunction App() {\n  return (\n    <>\n      <Header />\n      <StrictMode>\n        <main>\n          <Sidebar />\n          <Content />\n        </main>\n      </StrictMode>\n      <Footer />\n    </>\n  );\n}\n```\n\n이 예시에서 `Header`와 `Footer` 컴포넌트에서는 Strict Mode 검사가 실행되지 않습니다. 그러나 `Sidebar`와 `Content`, 그리고 그 자손 컴포넌트는 깊이에 상관없이 검사가 실행됩니다.\n\n<Note>\n\n앱의 일부에서 `StrictMode`가 활성화되면 React는 실제 운영 환경에서만 가능한 동작만을 허용합니다. 예를 들어, 앱의 루트에서 `<StrictMode>`가 활성화되지 않으면 초기 마운트 시 [Effect를 다시 실행](#fixing-bugs-found-by-re-running-effects-in-development)하지 않습니다. 이는 부모 Effect 없이 자식 Effect가 두 번 실행되는 상황을 방지하기 위함이며, 이러한 상황은 실제 운영 환경에서는 발생하지 않습니다.\n\n</Note>\n\n---\n\n### 개발 중 이중 렌더링으로 발견한 버그 수정 {/*fixing-bugs-found-by-double-rendering-in-development*/}\n\n[React는 작성하는 모든 컴포넌트가 순수 함수라고 가정합니다.](/learn/keeping-components-pure) 이것은 React 컴포넌트는 항상 동일한 입력(Props, State, Context)에 대해 동일한 JSX를 반환해야 한다는 것을 의미합니다.\n\n이 규칙을 위반하는 컴포넌트는 예기치 않게 동작하며 버그를 일으킵니다. Strict Mode는 실수로 작성된 순수하지 않은 코드를 찾아내기 위해 몇 가지 함수(순수 함수여야 하는 것만)를 **개발 환경에서 두 번 호출**합니다. 이에는 다음이 포함됩니다.\n\n- 컴포넌트 함수 본문. (단, 최상위 로직만 해당하며, 이벤트 핸들러 내부의 코드는 포함하지 않음.)\n- [`useState`](/reference/react/useState), [`set` 함수](/reference/react/useState#setstate), [`useMemo`](/reference/react/useMemo), 또는 [`useReducer`](/reference/react/useReducer)에 전달한 함수.\n- [`constructor`](/reference/react/Component#constructor), [`render`](/reference/react/Component#render), [`shouldComponentUpdate`](/reference/react/Component#shouldcomponentupdate)와 같은 일부 클래스 컴포넌트 메소드. ([전체 목록 보기](https://reactjs.org/docs/strict-mode.html#detecting-unexpected-side-effects))\n\n함수가 순수한 경우 두 번 실행하여도 동작이 변경되지 않습니다. 순수 함수는 항상 같은 결과를 생성하기 때문입니다. 그러나 함수가 순수하지 않다면 (예: 받은 데이터를 변경하는 함수) 두 번 실행하면 보통 알아챌 수 있습니다. (이것이 바로 함수가 순수하지 않다는 것을 의미합니다!) 이는 버그를 조기에 발견하고 수정하는 데 도움이 됩니다.\n\n**다음은 Strict Mode의 이중 렌더링이 어떻게 버그를 조기에 발견하는 데 도움이 되는지 보여주는 예시입니다.**\n\n`StoryTray` 컴포넌트는 `stories` 배열을 받아 마지막에 \"이야기 만들기\" 항목을 추가합니다.\n\n<Sandpack>\n\n```js src/index.js\nimport { createRoot } from 'react-dom/client';\nimport './styles.css';\n\nimport App from './App';\n\nconst root = createRoot(document.getElementById(\"root\"));\nroot.render(<App />);\n```\n\n```js src/App.js\nimport { useState } from 'react';\nimport StoryTray from './StoryTray.js';\n\nlet initialStories = [\n  {id: 0, label: \"Ankit의 이야기\" },\n  {id: 1, label: \"Taylor의 이야기\" },\n];\n\nexport default function App() {\n  let [stories, setStories] = useState(initialStories)\n  return (\n    <div\n      style={{\n        width: '100%',\n        height: '100%',\n        textAlign: 'center',\n      }}\n    >\n      <StoryTray stories={stories} />\n    </div>\n  );\n}\n```\n\n```js src/StoryTray.js active\nexport default function StoryTray({ stories }) {\n  const items = stories;\n  items.push({ id: 'create', label: '이야기 만들기' });\n  return (\n    <ul>\n      {items.map(story => (\n        <li key={story.id}>\n          {story.label}\n        </li>\n      ))}\n    </ul>\n  );\n}\n```\n\n```css\nul {\n  margin: 0;\n  list-style-type: none;\n  height: 100%;\n  display: flex;\n  flex-wrap: wrap;\n  padding: 10px;\n}\n\nli {\n  border: 1px solid #aaa;\n  border-radius: 6px;\n  float: left;\n  margin: 5px;\n  padding: 5px;\n  width: 70px;\n  height: 100px;\n}\n```\n\n</Sandpack>\n\n위 코드에는 실수가 있습니다. 하지만 초기 출력이 올바르게 나타나기 때문에 놓치기 쉽습니다.\n\nThis mistake will become more noticeable if the `StoryTray` component re-renders multiple times. For example, let's make the `StoryTray` re-render with a different background color whenever you hover over it:\n\n<Sandpack>\n\n```js src/index.js\nimport { createRoot } from 'react-dom/client';\nimport './styles.css';\n\nimport App from './App';\n\nconst root = createRoot(document.getElementById('root'));\nroot.render(<App />);\n```\n\n```js src/App.js\nimport { useState } from 'react';\nimport StoryTray from './StoryTray.js';\n\nlet initialStories = [\n  {id: 0, label: \"Ankit의 이야기\" },\n  {id: 1, label: \"Taylor의 이야기\" },\n];\n\nexport default function App() {\n  let [stories, setStories] = useState(initialStories)\n  return (\n    <div\n      style={{\n        width: '100%',\n        height: '100%',\n        textAlign: 'center',\n      }}\n    >\n      <StoryTray stories={stories} />\n    </div>\n  );\n}\n```\n\n```js src/StoryTray.js active\nimport { useState } from 'react';\n\nexport default function StoryTray({ stories }) {\n  const [isHover, setIsHover] = useState(false);\n  const items = stories;\n  items.push({ id: 'create', label: '이야기 만들기' });\n  return (\n    <ul\n      onPointerEnter={() => setIsHover(true)}\n      onPointerLeave={() => setIsHover(false)}\n      style={{\n        backgroundColor: isHover ? '#ddd' : '#fff'\n      }}\n    >\n      {items.map(story => (\n        <li key={story.id}>\n          {story.label}\n        </li>\n      ))}\n    </ul>\n  );\n}\n```\n\n```css\nul {\n  margin: 0;\n  list-style-type: none;\n  height: 100%;\n  display: flex;\n  flex-wrap: wrap;\n  padding: 10px;\n}\n\nli {\n  border: 1px solid #aaa;\n  border-radius: 6px;\n  float: left;\n  margin: 5px;\n  padding: 5px;\n  width: 70px;\n  height: 100px;\n}\n```\n\n</Sandpack>\n\n`StoryTray` 컴포넌트 위로 마우스를 가져갈 때마다 \"이야기 만들기\"가 목록에 다시 추가되는 것을 확인할 수 있습니다. 이 코드의 의도는 마지막에 한 번 추가하는 것이었습니다. 하지만 `StoryTray`는 소품의 `stories` 배열을 직접 수정합니다. `StoryTray`는 렌더링할 때마다 같은 배열의 끝에 \"이야기 만들기\"를 다시 추가합니다. 즉, `StoryTray`는 순수 함수가 아니므로 여러 번 실행하면 다른 결과가 생성됩니다.\n\n이 문제를 해결하기 위해 배열의 사본을 만든 다음 원본이 아닌 사본을 수정할 수 있습니다.\n\n```js {2}\nexport default function StoryTray({ stories }) {\n  const items = stories.slice(); // 배열 복제\n  // ✅ Good: 새로운 배열에 추가\n  items.push({ id: 'create', label: '이야기 만들기' });\n```\n\n이렇게 하면 [`StoryTray` 함수를 순수하게 만들 수 있습니다.](/learn/keeping-components-pure) 함수가 호출될 때마다 배열의 사본만 수정하고, 외부 객체나 변수에는 영향을 미치지 않습니다. 이렇게 하면 버그를 해결할 수 있지만, 컴포넌트를 여러번 다시 렌더링하도록 만들어야 비로소 컴포넌트의 동작에 문제가 있다는 것이 명확해졌습니다.\n\n**원래 예시에서는 버그가 명확하지 않았습니다. 이제 원래 (버그가 있는) 코드를 `<StrictMode>`로 래핑해 보겠습니다.**\n\n<Sandpack>\n\n```js src/index.js\nimport { StrictMode } from 'react';\nimport { createRoot } from 'react-dom/client';\nimport './styles.css';\n\nimport App from './App';\n\nconst root = createRoot(document.getElementById(\"root\"));\nroot.render(\n  <StrictMode>\n    <App />\n  </StrictMode>\n);\n```\n\n```js src/App.js\nimport { useState } from 'react';\nimport StoryTray from './StoryTray.js';\n\nlet initialStories = [\n  {id: 0, label: \"Ankit의 이야기\" },\n  {id: 1, label: \"Taylor의 이야기\" },\n];\n\nexport default function App() {\n  let [stories, setStories] = useState(initialStories)\n  return (\n    <div\n      style={{\n        width: '100%',\n        height: '100%',\n        textAlign: 'center',\n      }}\n    >\n      <StoryTray stories={stories} />\n    </div>\n  );\n}\n```\n\n```js src/StoryTray.js active\nexport default function StoryTray({ stories }) {\n  const items = stories;\n  items.push({ id: 'create', label: '이야기 만들기' });\n  return (\n    <ul>\n      {items.map(story => (\n        <li key={story.id}>\n          {story.label}\n        </li>\n      ))}\n    </ul>\n  );\n}\n```\n\n```css\nul {\n  margin: 0;\n  list-style-type: none;\n  height: 100%;\n  display: flex;\n  flex-wrap: wrap;\n  padding: 10px;\n}\n\nli {\n  border: 1px solid #aaa;\n  border-radius: 6px;\n  float: left;\n  margin: 5px;\n  padding: 5px;\n  width: 70px;\n  height: 100px;\n}\n```\n\n</Sandpack>\n\n**Strict Mode에서는 *항상* 렌더링 함수를 두 번 호출하므로 실수를 바로 확인할 수 있습니다.** (\"이야기 만들기\"가 두 번 나타남.) 따라서 프로세스 초기에 이러한 실수를 발견할 수 있습니다. 컴포넌트가 Strict Mode에서 렌더링되도록 수정하면 이전의 호버 기능과 같이 향후 발생할 수 있는 많은 프로덕션 버그도 *수정*할 수 있습니다.\n\n<Sandpack>\n\n```js src/index.js\nimport { StrictMode } from 'react';\nimport { createRoot } from 'react-dom/client';\nimport './styles.css';\n\nimport App from './App';\n\nconst root = createRoot(document.getElementById('root'));\nroot.render(\n  <StrictMode>\n    <App />\n  </StrictMode>\n);\n```\n\n```js src/App.js\nimport { useState } from 'react';\nimport StoryTray from './StoryTray.js';\n\nlet initialStories = [\n  {id: 0, label: \"Ankit의 이야기\" },\n  {id: 1, label: \"Taylor의 이야기\" },\n];\n\nexport default function App() {\n  let [stories, setStories] = useState(initialStories)\n  return (\n    <div\n      style={{\n        width: '100%',\n        height: '100%',\n        textAlign: 'center',\n      }}\n    >\n      <StoryTray stories={stories} />\n    </div>\n  );\n}\n```\n\n```js src/StoryTray.js active\nimport { useState } from 'react';\n\nexport default function StoryTray({ stories }) {\n  const [isHover, setIsHover] = useState(false);\n  const items = stories.slice(); // 배열 복제\n  items.push({ id: 'create', label: '이야기 만들기' });\n  return (\n    <ul\n      onPointerEnter={() => setIsHover(true)}\n      onPointerLeave={() => setIsHover(false)}\n      style={{\n        backgroundColor: isHover ? '#ddd' : '#fff'\n      }}\n    >\n      {items.map(story => (\n        <li key={story.id}>\n          {story.label}\n        </li>\n      ))}\n    </ul>\n  );\n}\n```\n\n```css\nul {\n  margin: 0;\n  list-style-type: none;\n  height: 100%;\n  display: flex;\n  flex-wrap: wrap;\n  padding: 10px;\n}\n\nli {\n  border: 1px solid #aaa;\n  border-radius: 6px;\n  float: left;\n  margin: 5px;\n  padding: 5px;\n  width: 70px;\n  height: 100px;\n}\n```\n\n</Sandpack>\n\nStrict Mode가 없으면 리렌더링을 더 추가하기 전까지는 버그를 놓치기 쉬웠습니다. Strict Mode를 사용하면 동일한 버그가 즉시 나타납니다. Strict Mode는 버그를 팀이나 사용자에게 푸시하기 전에 발견할 수 있도록 도와줍니다.\n\n[컴포넌트를 순수하게 유지하는 방법에 대해 자세히 알아보세요.](/learn/keeping-components-pure)\n\n<Note>\n\n[React 개발자 도구](/learn/react-developer-tools)가 설치되어 있다면, 두 번째 렌더링 호출 중 `console.log` 호출이 약간 흐리게 표시됩니다. React 개발자 도구는 이를 완전히 억제하는 설정(기본값은 꺼짐)도 제공합니다.\n\n</Note>\n\n---\n\n### 개발 환경에서 Effect를 다시 실행하여 발견된 버그 수정 {/*fixing-bugs-found-by-re-running-effects-in-development*/}\n\nStrict Mode는 [Effect](/learn/synchronizing-with-effects)의 버그를 찾는 데도 도움이 될 수 있습니다.\n\n모든 Effect에는 몇 가지 셋업 코드가 있고 어쩌면 클린업 코드가 있을 수 있습니다. 일반적으로 React는 컴포넌트가 *마운트*(화면에 추가)될 때 셋업을 호출하고 컴포넌트가 *마운트 해제*(화면에서 제거)될 때 클린업을 호출합니다. 그런 다음 React는 마지막 렌더링 이후로부터 의존성이 변경된 경우 클린업과 셋업을 다시 호출합니다.\n\nStrict Mode가 켜져 있으면 React는 **모든 Effect에 대해 개발 환경에서 한 번 더 셋업+클린업 사이클을 실행합니다.** 의외로 느껴질 수도 있지만 수동으로 파악하기 어려운 미묘한 버그를 드러내는 데 도움이 됩니다.\n\n**다음은 Strict Mode에서 Effect를 다시 실행하는 것이 버그를 조기에 발견하는 데 어떻게 도움이 되는지 보여주는 예시입니다.**\n\n컴포넌트를 채팅에 연결하는 이 예시를 살펴봅시다.\n\n<Sandpack>\n\n```js src/index.js\nimport { createRoot } from 'react-dom/client';\nimport './styles.css';\n\nimport App from './App';\n\nconst root = createRoot(document.getElementById(\"root\"));\nroot.render(<App />);\n```\n\n```js\nimport { useState, useEffect } from 'react';\nimport { createConnection } from './chat.js';\n\nconst serverUrl = 'https://localhost:1234';\nconst roomId = '일반';\n\nexport default function ChatRoom() {\n  useEffect(() => {\n    const connection = createConnection(serverUrl, roomId);\n    connection.connect();\n  }, []);\n  return <h1>{roomId} 방에 오신 것을 환영합니다!</h1>;\n}\n```\n\n```js src/chat.js\nlet connections = 0;\n\nexport function createConnection(serverUrl, roomId) {\n  // 현실 구현은 실제로 서버에 연결할 것입니다.\n  return {\n    connect() {\n      console.log('✅ Connecting to \"' + roomId + '\" room at ' + serverUrl + '...');\n      connections++;\n      console.log('활성화된 연결 수: ' + connections);\n    },\n    disconnect() {\n      console.log('❌ Disconnected from \"' + roomId + '\" room at ' + serverUrl);\n      connections--;\n      console.log('활성화된 연결 수: ' + connections);\n    }\n  };\n}\n```\n\n```css\ninput { display: block; margin-bottom: 20px; }\nbutton { margin-left: 10px; }\n```\n\n</Sandpack>\n\n이 코드에는 문제가 있지만 즉시 파악하기 어려울 수 있습니다.\n\n문제를 더 명확하게 드러내기 위해 기능을 구현해 보겠습니다. 아래 예시에서는 `roomId`가 하드코딩되어 있지 않습니다. 대신 사용자가 연결하려는 `roomId`를 드롭다운에서 선택할 수 있습니다. \"대화 열기\"을 클릭한 다음 다른 대화방을 하나씩 선택합니다. 콘솔에서 활성화된 연결 수를 추적합니다.\n\n<Sandpack>\n\n```js src/index.js\nimport { createRoot } from 'react-dom/client';\nimport './styles.css';\n\nimport App from './App';\n\nconst root = createRoot(document.getElementById(\"root\"));\nroot.render(<App />);\n```\n\n```js\nimport { useState, useEffect } from 'react';\nimport { createConnection } from './chat.js';\n\nconst serverUrl = 'https://localhost:1234';\n\nfunction ChatRoom({ roomId }) {\n  useEffect(() => {\n    const connection = createConnection(serverUrl, roomId);\n    connection.connect();\n  }, [roomId]);\n\n  return <h1>{roomId} 방에 오신 것을 환영합니다!</h1>;\n}\n\nexport default function App() {\n  const [roomId, setRoomId] = useState('일반');\n  const [show, setShow] = useState(false);\n  return (\n    <>\n      <label>\n        대화방 선택하기:{' '}\n        <select\n          value={roomId}\n          onChange={e => setRoomId(e.target.value)}\n        >\n          <option value=\"일반\">일반</option>\n          <option value=\"여행\">여행</option>\n          <option value=\"음악\">음악</option>\n        </select>\n      </label>\n      <button onClick={() => setShow(!show)}>\n        {show ? '대화 닫기' : '대화 열기'}\n      </button>\n      {show && <hr />}\n      {show && <ChatRoom roomId={roomId} />}\n    </>\n  );\n}\n```\n\n```js src/chat.js\nlet connections = 0;\n\nexport function createConnection(serverUrl, roomId) {\n  // 현실 구현은 실제로 서버에 연결될 것입니다.\n  return {\n    connect() {\n      console.log('✅ Connecting to \"' + roomId + '\" room at ' + serverUrl + '...');\n      connections++;\n      console.log('활성화된 연결 수: ' + connections);\n    },\n    disconnect() {\n      console.log('❌ Disconnected from \"' + roomId + '\" room at ' + serverUrl);\n      connections--;\n      console.log('활성화된 연결 수: ' + connections);\n    }\n  };\n}\n```\n\n```css\ninput { display: block; margin-bottom: 20px; }\nbutton { margin-left: 10px; }\n```\n\n</Sandpack>\n\n열린 연결 수가 항상 계속 증가하는 것을 알 수 있습니다. 실제 앱에서는 성능 및 네트워크 문제가 발생할 수 있습니다. 문제는 [Effect에 클린업 함수가 누락되었다는 것입니다.](/learn/synchronizing-with-effects#step-3-add-cleanup-if-needed)\n\n```js {4}\n  useEffect(() => {\n    const connection = createConnection(serverUrl, roomId);\n    connection.connect();\n    return () => connection.disconnect();\n  }, [roomId]);\n```\n\n이제 Effect가 자체적으로 \"클린업\"하고 오래된 연결을 파괴하므로 누수가 해결되었습니다. 그러나 더 많은 기능(선택 상자)을 추가하기 전까지는 문제가 드러나지 않았음을 알 수 있습니다.\n\n**원래 예시에서는 버그가 명확하지 않았습니다. 이제 원래 (버그가 있는) 코드를 `<StrictMode>`로 래핑해 보겠습니다.**\n\n<Sandpack>\n\n```js src/index.js\nimport { StrictMode } from 'react';\nimport { createRoot } from 'react-dom/client';\nimport './styles.css';\n\nimport App from './App';\n\nconst root = createRoot(document.getElementById(\"root\"));\nroot.render(\n  <StrictMode>\n    <App />\n  </StrictMode>\n);\n```\n\n```js\nimport { useState, useEffect } from 'react';\nimport { createConnection } from './chat.js';\n\nconst serverUrl = 'https://localhost:1234';\nconst roomId = '일반';\n\nexport default function ChatRoom() {\n  useEffect(() => {\n    const connection = createConnection(serverUrl, roomId);\n    connection.connect();\n  }, []);\n  return <h1>{roomId} 방에 오신 것을 환영합니다!</h1>;\n}\n```\n\n```js src/chat.js\nlet connections = 0;\n\nexport function createConnection(serverUrl, roomId) {\n  // 현실 구현은 실제로 서버에 연결될 것입니다.\n  return {\n    connect() {\n      console.log('✅ Connecting to \"' + roomId + '\" room at ' + serverUrl + '...');\n      connections++;\n      console.log('활성화된 연결 수: ' + connections);\n    },\n    disconnect() {\n      console.log('❌ Disconnected from \"' + roomId + '\" room at ' + serverUrl);\n      connections--;\n      console.log('활성화된 연결 수: ' + connections);\n    }\n  };\n}\n```\n\n```css\ninput { display: block; margin-bottom: 20px; }\nbutton { margin-left: 10px; }\n```\n\n</Sandpack>\n\n**Strict Mode를 사용하면 문제가 있음을 즉시 알 수 있습니다**(활성화된 연결 수가 2개로 증가함). Strict Mode는 모든 Effect에 대해 추가 셋업+클린업 사이클을 실행합니다. 이 Effect에는 클린업 로직이 없으므로 추가 연결을 생성하지만 파괴하지는 않습니다. 이것은 클린업 함수가 누락되었다는 힌트입니다.\n\nStrict Mode를 사용하면 이러한 실수를 프로세스 초기에 발견할 수 있습니다. Strict Mode에서 클린업 함수를 추가하여 Effect를 수정하면 이전의 선택 상자와 같이 향후 프로덕션에서 발생할 수 있는 많은 버그 *또한* 수정합니다.\n\n<Sandpack>\n\n```js src/index.js\nimport { StrictMode } from 'react';\nimport { createRoot } from 'react-dom/client';\nimport './styles.css';\n\nimport App from './App';\n\nconst root = createRoot(document.getElementById(\"root\"));\nroot.render(\n  <StrictMode>\n    <App />\n  </StrictMode>\n);\n```\n\n```js\nimport { useState, useEffect } from 'react';\nimport { createConnection } from './chat.js';\n\nconst serverUrl = 'https://localhost:1234';\n\nfunction ChatRoom({ roomId }) {\n  useEffect(() => {\n    const connection = createConnection(serverUrl, roomId);\n    connection.connect();\n    return () => connection.disconnect();\n  }, [roomId]);\n\n  return <h1>{roomId} 방에 오신 것을 환영합니다!</h1>;\n}\n\nexport default function App() {\n  const [roomId, setRoomId] = useState('일반');\n  const [show, setShow] = useState(false);\n  return (\n    <>\n      <label>\n        대화방 선택하기:{' '}\n        <select\n          value={roomId}\n          onChange={e => setRoomId(e.target.value)}\n        >\n          <option value=\"일반\">일반</option>\n          <option value=\"여행\">여행</option>\n          <option value=\"음악\">음악</option>\n        </select>\n      </label>\n      <button onClick={() => setShow(!show)}>\n        {show ? '대화 닫기' : '대화 열기'}\n      </button>\n      {show && <hr />}\n      {show && <ChatRoom roomId={roomId} />}\n    </>\n  );\n}\n```\n\n```js src/chat.js\nlet connections = 0;\n\nexport function createConnection(serverUrl, roomId) {\n  // 현실 구현은 실제로 서버에 연결될 것입니다.\n  return {\n    connect() {\n      console.log('✅ Connecting to \"' + roomId + '\" room at ' + serverUrl + '...');\n      connections++;\n      console.log('활성화된 연결 수: ' + connections);\n    },\n    disconnect() {\n      console.log('❌ Disconnected from \"' + roomId + '\" room at ' + serverUrl);\n      connections--;\n      console.log('활성화된 연결 수: ' + connections);\n    }\n  };\n}\n```\n\n```css\ninput { display: block; margin-bottom: 20px; }\nbutton { margin-left: 10px; }\n```\n\n</Sandpack>\n\n콘솔의 활성화된 연결 수가 더 이상 증가하지 않는 것을 확인할 수 있습니다.\n\nStrict Mode가 없으면 Effect를 클린업해야 한다는 사실을 놓치기 쉬웠습니다. 개발 환경에서 Effect에 대해 *셋업* 대신 *셋업 → 클린업 → 셋업*을 실행하면 Strict Mode에서 누락된 클린업 로직이 더 눈에 띄게 됩니다.\n\n[Effect 클린업을 구현하는 방법에 대해 자세히 알아보세요.](/learn/synchronizing-with-effects#how-to-handle-the-effect-firing-twice-in-development)\n\n---\n### 개발 환경에서 ref 콜백을 다시 실행하여 발견된 버그 수정 {/*fixing-bugs-found-by-re-running-ref-callbacks-in-development*/}\n\nStrict Mode는 [callbacks refs](/learn/manipulating-the-dom-with-refs)의 버그를 찾는 데도 도움이 됩니다.\n\n모든 콜백 `ref`에는 몇 가지 셋업 코드가 있고 어쩌면 클린업 코드가 있을 수 있습니다. 일반적으로 React는 엘리먼트가 생성(DOM에 추가)될 때 셋업 코드를 실행하고, 엘리먼트가 제거(DOM에서 삭제)될 때 셋업 코드를 실행합니다.\n\nStrict Mode가 켜져 있으면 React는 **모든 콜백 `ref`에 대해 개발 환경에서 한 번 더 셋업+클린업 사이클을 실행합니다.** 이외로 느껴질 수도 있지만, 수동으로 파악하기 어려운 미묘한 버그를 드러내는 데 도움이 됩니다.\n\n다음 예시를 살펴봅시다. 이 예시는 동물을 선택한 후 목록 중 하나로 스크롤 할 수 있게 해줍니다. \"cats\"에서 \"dogs\"로 전환할 때 콘솔 로그를 보면 목록에 있는 동물의 수가 계속 증가하고, \"Scroll to\" 버튼이 동작하지 않게 되는 점을 확인할 수 있습니다.\n\n<Sandpack>\n\n```js src/index.js\nimport { createRoot } from 'react-dom/client';\nimport './styles.css';\n\nimport App from './App';\n\nconst root = createRoot(document.getElementById(\"root\"));\n// ❌ Not using StrictMode.\nroot.render(<App />);\n```\n\n```js src/App.js active\nimport { useRef, useState } from \"react\";\n\nexport default function CatFriends() {\n  const itemsRef = useRef([]);\n  const [catList, setCatList] = useState(setupCatList);\n  const [cat, setCat] = useState('neo');\n\n  function scrollToCat(index) {\n    const list = itemsRef.current;\n    const {node} = list[index];\n    node.scrollIntoView({\n      behavior: \"smooth\",\n      block: \"nearest\",\n      inline: \"center\",\n    });\n  }\n\n  const cats = catList.filter(c => c.type === cat)\n\n  return (\n    <>\n      <nav>\n        <button onClick={() => setCat('neo')}>Neo</button>\n        <button onClick={() => setCat('millie')}>Millie</button>\n      </nav>\n      <hr />\n      <nav>\n        <span>Scroll to:</span>{cats.map((cat, index) => (\n          <button key={cat.src} onClick={() => scrollToCat(index)}>\n            {index}\n          </button>\n        ))}\n      </nav>\n      <div>\n        <ul>\n          {cats.map((cat) => (\n            <li\n              key={cat.src}\n              ref={(node) => {\n                const list = itemsRef.current;\n                const item = {cat: cat, node};\n                list.push(item);\n                console.log(`✅ Adding cat to the map. Total cats: ${list.length}`);\n                if (list.length > 10) {\n                  console.log('❌ Too many cats in the list!');\n                }\n                return () => {\n                  // 🚩 No cleanup, this is a bug!\n                }\n              }}\n            >\n              <img src={cat.src} />\n            </li>\n          ))}\n        </ul>\n      </div>\n    </>\n  );\n}\n\nfunction setupCatList() {\n  const catList = [];\n  for (let i = 0; i < 10; i++) {\n    catList.push({type: 'neo', src: \"https://placecats.com/neo/320/240?\" + i});\n  }\n  for (let i = 0; i < 10; i++) {\n    catList.push({type: 'millie', src: \"https://placecats.com/millie/320/240?\" + i});\n  }\n\n  return catList;\n}\n\n```\n\n```css\ndiv {\n  width: 100%;\n  overflow: hidden;\n}\n\nnav {\n  text-align: center;\n}\n\nbutton {\n  margin: .25rem;\n}\n\nul,\nli {\n  list-style: none;\n  white-space: nowrap;\n}\n\nli {\n  display: inline;\n  padding: 0.5rem;\n}\n```\n\n</Sandpack>\n\n\n**이것은 프로덕션 버그입니다!** ref 콜백이 클린업 과정에서 동물 목록을 제거하지 않으므로 동물 목록이 계속 증가합니다. 이는 메모리 누수를 일으켜 실제 앱에서 성능 문제를 유발할 수 있으며, 앱의 동작을 망가뜨립니다.\n\n문제는 ref 콜백이 스스로 클린업을 하지 않는 점입니다.\n\n```js {6-8}\n<li\n  ref={node => {\n    const list = itemsRef.current;\n    const item = {animal, node};\n    list.push(item);\n    return () => {\n      // 🚩 No cleanup, this is a bug!\n    }\n  }}\n</li>\n```\n\n이제 원본 (버그가 있는) 코드를 `<StrictMode>`로 감싸봅시다.\n\n<Sandpack>\n\n```js src/index.js\nimport { createRoot } from 'react-dom/client';\nimport {StrictMode} from 'react';\nimport './styles.css';\n\nimport App from './App';\n\nconst root = createRoot(document.getElementById(\"root\"));\n// ✅ Using StrictMode.\nroot.render(\n  <StrictMode>\n    <App />\n  </StrictMode>\n);\n```\n\n```js src/App.js active\nimport { useRef, useState } from \"react\";\n\nexport default function CatFriends() {\n  const itemsRef = useRef([]);\n  const [catList, setCatList] = useState(setupCatList);\n  const [cat, setCat] = useState('neo');\n\n  function scrollToCat(index) {\n    const list = itemsRef.current;\n    const {node} = list[index];\n    node.scrollIntoView({\n      behavior: \"smooth\",\n      block: \"nearest\",\n      inline: \"center\",\n    });\n  }\n\n  const cats = catList.filter(c => c.type === cat)\n\n  return (\n    <>\n      <nav>\n        <button onClick={() => setCat('neo')}>Neo</button>\n        <button onClick={() => setCat('millie')}>Millie</button>\n      </nav>\n      <hr />\n      <nav>\n        <span>Scroll to:</span>{cats.map((cat, index) => (\n          <button key={cat.src} onClick={() => scrollToCat(index)}>\n            {index}\n          </button>\n        ))}\n      </nav>\n      <div>\n        <ul>\n          {cats.map((cat) => (\n            <li\n              key={cat.src}\n              ref={(node) => {\n                const list = itemsRef.current;\n                const item = {cat: cat, node};\n                list.push(item);\n                console.log(`✅ Adding cat to the map. Total cats: ${list.length}`);\n                if (list.length > 10) {\n                  console.log('❌ Too many cats in the list!');\n                }\n                return () => {\n                  // 🚩 No cleanup, this is a bug!\n                }\n              }}\n            >\n              <img src={cat.src} />\n            </li>\n          ))}\n        </ul>\n      </div>\n    </>\n  );\n}\n\nfunction setupCatList() {\n  const catList = [];\n  for (let i = 0; i < 10; i++) {\n    catList.push({type: 'neo', src: \"https://placecats.com/neo/320/240?\" + i});\n  }\n  for (let i = 0; i < 10; i++) {\n    catList.push({type: 'millie', src: \"https://placecats.com/millie/320/240?\" + i});\n  }\n\n  return catList;\n}\n\n```\n\n```css\ndiv {\n  width: 100%;\n  overflow: hidden;\n}\n\nnav {\n  text-align: center;\n}\n\nbutton {\n  margin: .25rem;\n}\n\nul,\nli {\n  list-style: none;\n  white-space: nowrap;\n}\n\nli {\n  display: inline;\n  padding: 0.5rem;\n}\n```\n\n</Sandpack>\n\n**Strict Mode를 사용하면 문제를 즉시 찾을 수 있습니다.** Strict Mode는 모든 콜백 ref에 대해 추가적인 셋업+클린업 사이클을 실행 실행합니다. 이 콜백 ref에는 클린업 로직이 없기 때문에 ref를 추가만 하고 제거하지 않습니다. 이는 클린업 함수가 누락되었다는 힌트입니다.\n\nStrict Mode를 통해 콜백 ref에서 발생하는 실수를 조기에 발견할 수 있습니다. Strict Mode에서 클린업 함수를 추가해 콜백을 수정하면, 이전에 발생했던 \"Scroll to\" 버그와 같은 많은 잠재적인 프로덕션 버그도 함께 해결할 수 있습니다.\n\n<Sandpack>\n\n```js src/index.js\nimport { createRoot } from 'react-dom/client';\nimport {StrictMode} from 'react';\nimport './styles.css';\n\nimport App from './App';\n\nconst root = createRoot(document.getElementById(\"root\"));\n// ✅ Using StrictMode.\nroot.render(\n  <StrictMode>\n    <App />\n  </StrictMode>\n);\n```\n\n```js src/App.js active\nimport { useRef, useState } from \"react\";\n\nexport default function CatFriends() {\n  const itemsRef = useRef([]);\n  const [catList, setCatList] = useState(setupCatList);\n  const [cat, setCat] = useState('neo');\n\n  function scrollToCat(index) {\n    const list = itemsRef.current;\n    const {node} = list[index];\n    node.scrollIntoView({\n      behavior: \"smooth\",\n      block: \"nearest\",\n      inline: \"center\",\n    });\n  }\n\n  const cats = catList.filter(c => c.type === cat)\n\n  return (\n    <>\n      <nav>\n        <button onClick={() => setCat('neo')}>Neo</button>\n        <button onClick={() => setCat('millie')}>Millie</button>\n      </nav>\n      <hr />\n      <nav>\n        <span>Scroll to:</span>{cats.map((cat, index) => (\n          <button key={cat.src} onClick={() => scrollToCat(index)}>\n            {index}\n          </button>\n        ))}\n      </nav>\n      <div>\n        <ul>\n          {cats.map((cat) => (\n            <li\n              key={cat.src}\n              ref={(node) => {\n                const list = itemsRef.current;\n                const item = {cat: cat, node};\n                list.push(item);\n                console.log(`✅ Adding cat to the map. Total cats: ${list.length}`);\n                if (list.length > 10) {\n                  console.log('❌ Too many cats in the list!');\n                }\n                return () => {\n                  list.splice(list.indexOf(item), 1);\n                  console.log(`❌ Removing cat from the map. Total cats: ${itemsRef.current.length}`);\n                }\n              }}\n            >\n              <img src={cat.src} />\n            </li>\n          ))}\n        </ul>\n      </div>\n    </>\n  );\n}\n\nfunction setupCatList() {\n  const catList = [];\n  for (let i = 0; i < 10; i++) {\n    catList.push({type: 'neo', src: \"https://placecats.com/neo/320/240?\" + i});\n  }\n  for (let i = 0; i < 10; i++) {\n    catList.push({type: 'millie', src: \"https://placecats.com/millie/320/240?\" + i});\n  }\n\n  return catList;\n}\n\n```\n\n```css\ndiv {\n  width: 100%;\n  overflow: hidden;\n}\n\nnav {\n  text-align: center;\n}\n\nbutton {\n  margin: .25rem;\n}\n\nul,\nli {\n  list-style: none;\n  white-space: nowrap;\n}\n\nli {\n  display: inline;\n  padding: 0.5rem;\n}\n```\n\n</Sandpack>\n\n이제 StrictMode에서 초기 마운트 시, ref 콜백이 모두 셋업되고, 클린업 후, 다시 셋업 됩니다.\n\n```\n...\n✅ 동물을 목록에 추가하는 중. 총 동물 수: 10\n...\n❌ 목록에서 동물을 제거합니다. 총 동물 수: 0\n...\n✅ 동물을 목록에 추가하는 중. 총 동물 수: 10\n```\n\n**이것이 예상된 결과입니다.** Strict Mode는 ref 콜백이 올바르게 클린업 되었는지 확인해 주기 때문에 크기가 예상된 양을 초과하지 않습니다. 수정 후에는 메모리 누수가 발생하지 않으며, 모든 기능이 예상대로 작동합니다.\n\nStrict Mode 없이는 고장 난 기능을 알아차릴 때까지 여기저기 클릭해야 하므로 버그를 놓치기 쉽습니다. Strict Mode는 버그를 즉시 드러나도록 하여 프로덕션에 배포하기 전에 문제를 발견할 수 있습니다.\n\n---\n### Fixing deprecation warnings enabled by Strict Mode {/*fixing-deprecation-warnings-enabled-by-strict-mode*/}\n\nReact는 `<StrictMode>` 트리 내부의 컴포넌트가 더 이상 사용되지 않는 다음 API 중 하나를 사용하는 경우 경고를 표시합니다.\n\n* [`UNSAFE_componentWillMount`](/reference/react/Component#unsafe_componentwillmount)와 같은 `UNSAFE_` 클래스 생명주기 메서드. [대안 확인하기](https://reactjs.org/blog/2018/03/27/update-on-async-rendering.html#migrating-from-legacy-lifecycles).\n\n이러한 API는 주로 이전 [클래스 컴포넌트](/reference/react/Component)에서 사용되므로 최신 앱에서는 거의 나타나지 않습니다.\n"
  },
  {
    "path": "src/content/reference/react/Suspense.md",
    "content": "---\ntitle: <Suspense>\n---\n\n<Intro>\n\n`<Suspense>`는 자식 요소를 로드하기 전까지 화면에 대체 UI<sup>Fallback</sup>를 보여줍니다.\n\n\n```js\n<Suspense fallback={<Loading />}>\n  <SomeComponent />\n</Suspense>\n```\n\n</Intro>\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `<Suspense>` {/*suspense*/}\n\n#### Props {/*props*/}\n* `children`: 궁극적으로 렌더링하려는 실제 UI입니다. `children`의 렌더링이 지연되면, Suspense는 `fallback`을 대신 렌더링합니다.\n* `fallback`: 실제 UI가 로딩되기 전까지 대신 렌더링되는 대체 UI입니다. 올바른 React 노드 형식은 무엇이든 대체 UI로 활용할 수 있지만, 실제로는 보통 로딩 스피너나 스켈레톤처럼 간단한 Placeholder를 활용합니다. Suspense는 `children`의 렌더링이 지연되면 자동으로 `fallback`으로 전환하고, 데이터가 준비되면 `children`으로 다시 전환합니다. 만약 `fallback`의 렌더링이 지연되면, 가장 가까운 부모 Suspense가 활성화됩니다.\n\n#### 주의 사항 {/*caveats*/}\n\n- React는 컴포넌트가 처음으로 마운트 되기 전에 지연된 렌더링을 하는 동안의 어떤 State도 유지하지 않습니다. 컴포넌트가 로딩되면 React는 일시 중지된 트리를 처음부터 다시 렌더링합니다.\n- Suspense가 트리의 콘텐츠를 보여주고 있을 때 또 다시 지연되면 [`startTransition`](/reference/react/startTransition)나 [`useDeferredValue`](/reference/react/useDeferredValue)로 인한 업데이트가 아닌 한, `fallback`이 다시 보입니다.\n- React가 다시 일시 중지되어 보이는 콘텐츠를 숨겨야 하는 경우, 콘텐츠 트리에서 [Layout Effect](/reference/react/useLayoutEffect)들을 정리합니다. 콘텐츠가 다시 보일 준비가 되면 React는 Layout Effect들을 다시 실행합니다. 이로써 DOM 레이아웃을 측정하는 Effect가 콘텐츠가 숨겨져 있는 동안 동작하지 않도록 보장합니다.\n- React는 Suspense와 통합된 *스트리밍 서버 렌더링*과 *선택적 Hydration* 같은 내부 최적화를 포함하고 있습니다. [아키텍처 개요](https://github.com/reactwg/react-18/discussions/37)를 읽고 [기술 강연](https://www.youtube.com/watch?v=pj5N-Khihgc)을 시청하여 더 자세히 알아보세요.\n\n---\n\n## 사용법 {/*usage*/}\n\n### 콘텐츠를 로딩하는 동안 대체 UI<sup>Fallback</sup> 보여주기 {/*displaying-a-fallback-while-content-is-loading*/}\n\n애플리케이션의 모든 곳을 Suspense 경계로 감쌀 수 있습니다.\n\n```js [[1, 1, \"<Loading />\"], [2, 2, \"<Albums />\"]]\n<Suspense fallback={<Loading />}>\n  <Albums />\n</Suspense>\n```\n\nReact는 <CodeStep step={2}>children</CodeStep>에 필요한 모든 코드와 데이터를 로딩할 때까지 <CodeStep step={1}>loading fallback</CodeStep>을 보여줍니다.\n\n아래 예시에서는 앨범 목록을 가져오는 동안 `Albums` 컴포넌트가 <em>지연(Suspend)</em>됩니다. 렌더링할 준비가 될 때까지 가장 가까운 Suspense는 Fallback, 즉 `Loading` 컴포넌트를 표시합니다. 데이터를 모두 로딩하면 React는 `Loading` Fallback을 숨기고 로딩된 데이터로 `Albums` 컴포넌트를 렌더링합니다.\n\n<Sandpack>\n\n```js src/App.js hidden\nimport { useState } from 'react';\nimport ArtistPage from './ArtistPage.js';\n\nexport default function App() {\n  const [show, setShow] = useState(false);\n  if (show) {\n    return (\n      <ArtistPage\n        artist={{\n          id: 'the-beatles',\n          name: 'The Beatles',\n        }}\n      />\n    );\n  } else {\n    return (\n      <button onClick={() => setShow(true)}>\n        Open The Beatles artist page\n      </button>\n    );\n  }\n}\n```\n\n```js src/ArtistPage.js active\nimport { Suspense } from 'react';\nimport Albums from './Albums.js';\n\nexport default function ArtistPage({ artist }) {\n  return (\n    <>\n      <h1>{artist.name}</h1>\n      <Suspense fallback={<Loading />}>\n        <Albums artistId={artist.id} />\n      </Suspense>\n    </>\n  );\n}\n\nfunction Loading() {\n  return <h2>🌀 Loading...</h2>;\n}\n```\n\n```js src/Albums.js\nimport {use} from 'react';\nimport { fetchData } from './data.js';\n\nexport default function Albums({ artistId }) {\n  const albums = use(fetchData(`/${artistId}/albums`));\n  return (\n    <ul>\n      {albums.map(album => (\n        <li key={album.id}>\n          {album.title} ({album.year})\n        </li>\n      ))}\n    </ul>\n  );\n}\n```\n\n```js src/data.js hidden\n// Note: the way you would do data fetching depends on\n// the framework that you use together with Suspense.\n// Normally, the caching logic would be inside a framework.\n\nlet cache = new Map();\n\nexport function fetchData(url) {\n  if (!cache.has(url)) {\n    cache.set(url, getData(url));\n  }\n  return cache.get(url);\n}\n\nasync function getData(url) {\n  if (url === '/the-beatles/albums') {\n    return await getAlbums();\n  } else {\n    throw Error('Not implemented');\n  }\n}\n\nasync function getAlbums() {\n  // Add a fake delay to make waiting noticeable.\n  await new Promise(resolve => {\n    setTimeout(resolve, 3000);\n  });\n\n  return [{\n    id: 13,\n    title: 'Let It Be',\n    year: 1970\n  }, {\n    id: 12,\n    title: 'Abbey Road',\n    year: 1969\n  }, {\n    id: 11,\n    title: 'Yellow Submarine',\n    year: 1969\n  }, {\n    id: 10,\n    title: 'The Beatles',\n    year: 1968\n  }, {\n    id: 9,\n    title: 'Magical Mystery Tour',\n    year: 1967\n  }, {\n    id: 8,\n    title: 'Sgt. Pepper\\'s Lonely Hearts Club Band',\n    year: 1967\n  }, {\n    id: 7,\n    title: 'Revolver',\n    year: 1966\n  }, {\n    id: 6,\n    title: 'Rubber Soul',\n    year: 1965\n  }, {\n    id: 5,\n    title: 'Help!',\n    year: 1965\n  }, {\n    id: 4,\n    title: 'Beatles For Sale',\n    year: 1964\n  }, {\n    id: 3,\n    title: 'A Hard Day\\'s Night',\n    year: 1964\n  }, {\n    id: 2,\n    title: 'With The Beatles',\n    year: 1963\n  }, {\n    id: 1,\n    title: 'Please Please Me',\n    year: 1963\n  }];\n}\n```\n\n</Sandpack>\n\n<Note>\n\n**Suspense가 가능한 데이터만이 Suspense 컴포넌트를 활성화합니다.** 아래와 같은 것들이 해당합니다.\n\n- [Relay](https://relay.dev/docs/guided-tour/rendering/loading-states/)와 [Next.js](https://nextjs.org/docs/app/building-your-application/routing/loading-ui-and-streaming#streaming-with-suspense) 같이 Suspense가 가능한 프레임워크를 사용한 데이터 가져오기.\n- [`lazy`](/reference/react/lazy)를 활용한 지연 로딩 컴포넌트.\n- [`use`](/reference/react/use)를 사용해서 캐시된 Promise 값 읽기.\n\nSuspense는 Effect 또는 이벤트 핸들러 내부에서 가져오는 데이터를 감지하지 **않습니다**.\n\n위의 `Albums` 컴포넌트에서 데이터를 로딩하는 정확한 방법은 프레임워크마다 다릅니다. Suspense가 가능한 프레임워크를 사용하는 경우, 프레임워크의 데이터 불러오기 관련 문서에서 자세한 내용을 확인할 수 있습니다.\n\n독단적인 프레임워크를 사용하지 않는 Suspense가 가능한 데이터 가져오기 기능은 아직 지원되지 않습니다. Suspense 지원 데이터 소스를 구현하기 위한 요구 사항은 불안정하고 문서화되지 않았습니다. 데이터 소스를 Suspense와 통합하기 위한 공식 API는 향후 React 버전에서 출시될 예정입니다.\n\n</Note>\n\n---\n\n### 콘텐츠를 한꺼번에 함께 보여주기 {/*revealing-content-together-at-once*/}\n\n기본적으로 Suspense 내부의 전체 트리는 하나의 단위로 취급됩니다. 예를 들어, 이러한 구성 요소 중 *하나라도* 어떤 데이터에 의해 지연되더라도 *모든* 구성 요소가 함께 로딩 표시로 대체됩니다.\n\n```js {2-5}\n<Suspense fallback={<Loading />}>\n  <Biography />\n  <Panel>\n    <Albums />\n  </Panel>\n</Suspense>\n```\n\n그런 다음 모두 보일 준비가 되면 한꺼번에 모두 함께 보입니다.\n\n아래 예시에서는 `Biography`와 `Albums` 모두 어떤 데이터를 가져옵니다. 하지만 두 구성 요소는 같은 단일 Suspense 아래에 그룹화되어 있기 때문에 항상 동시에 함께 그려지게 됩니다.\n\n<Sandpack>\n\n```js src/App.js hidden\nimport { useState } from 'react';\nimport ArtistPage from './ArtistPage.js';\n\nexport default function App() {\n  const [show, setShow] = useState(false);\n  if (show) {\n    return (\n      <ArtistPage\n        artist={{\n          id: 'the-beatles',\n          name: 'The Beatles',\n        }}\n      />\n    );\n  } else {\n    return (\n      <button onClick={() => setShow(true)}>\n        Open The Beatles artist page\n      </button>\n    );\n  }\n}\n```\n\n```js src/ArtistPage.js active\nimport { Suspense } from 'react';\nimport Albums from './Albums.js';\nimport Biography from './Biography.js';\nimport Panel from './Panel.js';\n\nexport default function ArtistPage({ artist }) {\n  return (\n    <>\n      <h1>{artist.name}</h1>\n      <Suspense fallback={<Loading />}>\n        <Biography artistId={artist.id} />\n        <Panel>\n          <Albums artistId={artist.id} />\n        </Panel>\n      </Suspense>\n    </>\n  );\n}\n\nfunction Loading() {\n  return <h2>🌀 Loading...</h2>;\n}\n```\n\n```js src/Panel.js\nexport default function Panel({ children }) {\n  return (\n    <section className=\"panel\">\n      {children}\n    </section>\n  );\n}\n```\n\n```js src/Biography.js\nimport {use} from 'react';\nimport { fetchData } from './data.js';\n\nexport default function Biography({ artistId }) {\n  const bio = use(fetchData(`/${artistId}/bio`));\n  return (\n    <section>\n      <p className=\"bio\">{bio}</p>\n    </section>\n  );\n}\n```\n\n```js src/Albums.js\nimport {use} from 'react';\nimport { fetchData } from './data.js';\n\nexport default function Albums({ artistId }) {\n  const albums = use(fetchData(`/${artistId}/albums`));\n  return (\n    <ul>\n      {albums.map(album => (\n        <li key={album.id}>\n          {album.title} ({album.year})\n        </li>\n      ))}\n    </ul>\n  );\n}\n```\n\n```js src/data.js hidden\n// Note: the way you would do data fetching depends on\n// the framework that you use together with Suspense.\n// Normally, the caching logic would be inside a framework.\n\nlet cache = new Map();\n\nexport function fetchData(url) {\n  if (!cache.has(url)) {\n    cache.set(url, getData(url));\n  }\n  return cache.get(url);\n}\n\nasync function getData(url) {\n  if (url === '/the-beatles/albums') {\n    return await getAlbums();\n  } else if (url === '/the-beatles/bio') {\n    return await getBio();\n  } else {\n    throw Error('Not implemented');\n  }\n}\n\nasync function getBio() {\n  // Add a fake delay to make waiting noticeable.\n  await new Promise(resolve => {\n    setTimeout(resolve, 1500);\n  });\n\n  return `The Beatles were an English rock band,\n    formed in Liverpool in 1960, that comprised\n    John Lennon, Paul McCartney, George Harrison\n    and Ringo Starr.`;\n}\n\nasync function getAlbums() {\n  // Add a fake delay to make waiting noticeable.\n  await new Promise(resolve => {\n    setTimeout(resolve, 3000);\n  });\n\n  return [{\n    id: 13,\n    title: 'Let It Be',\n    year: 1970\n  }, {\n    id: 12,\n    title: 'Abbey Road',\n    year: 1969\n  }, {\n    id: 11,\n    title: 'Yellow Submarine',\n    year: 1969\n  }, {\n    id: 10,\n    title: 'The Beatles',\n    year: 1968\n  }, {\n    id: 9,\n    title: 'Magical Mystery Tour',\n    year: 1967\n  }, {\n    id: 8,\n    title: 'Sgt. Pepper\\'s Lonely Hearts Club Band',\n    year: 1967\n  }, {\n    id: 7,\n    title: 'Revolver',\n    year: 1966\n  }, {\n    id: 6,\n    title: 'Rubber Soul',\n    year: 1965\n  }, {\n    id: 5,\n    title: 'Help!',\n    year: 1965\n  }, {\n    id: 4,\n    title: 'Beatles For Sale',\n    year: 1964\n  }, {\n    id: 3,\n    title: 'A Hard Day\\'s Night',\n    year: 1964\n  }, {\n    id: 2,\n    title: 'With The Beatles',\n    year: 1963\n  }, {\n    id: 1,\n    title: 'Please Please Me',\n    year: 1963\n  }];\n}\n```\n\n```css\n.bio { font-style: italic; }\n\n.panel {\n  border: 1px solid #aaa;\n  border-radius: 6px;\n  margin-top: 20px;\n  padding: 10px;\n}\n```\n\n</Sandpack>\n\n데이터를 로딩하는 컴포넌트가 Suspense의 직접적인 자식일 필요는 없습니다. 예를 들어, `Biography`와 `Albums`를 새로운 `Details` 컴포넌트로 이동할 수 있습니다. 이렇게 해도 동작은 변경되지 않습니다. `Biography`와 `Albums`는 가장 가까운 상위 Suspense를 공유하므로 두 컴포넌트의 노출 여부는 함께 조정됩니다.\n\n```js {2,8-11}\n<Suspense fallback={<Loading />}>\n  <Details artistId={artist.id} />\n</Suspense>\n\nfunction Details({ artistId }) {\n  return (\n    <>\n      <Biography artistId={artistId} />\n      <Panel>\n        <Albums artistId={artistId} />\n      </Panel>\n    </>\n  );\n}\n```\n\n---\n\n### 중첩된 콘텐츠가 로딩될 때 보여주기 {/*revealing-nested-content-as-it-loads*/}\n\n컴포넌트가 일시 중단되면 가장 가까운 상위 Suspense 컴포넌트가 Fallback을 보여줍니다. 이를 통해 여러 Suspense 컴포넌트를 중첩하여 로딩 순서를 만들 수 있습니다. 각 Suspense의 Fallback은 다음 레벨의 콘텐츠를 사용할 수 있게 되면 채워집니다. 예를 들어 앨범 목록에 자체 Fallback을 지정할 수 있습니다.\n\n```js {3,7}\n<Suspense fallback={<BigSpinner />}>\n  <Biography />\n  <Suspense fallback={<AlbumsGlimmer />}>\n    <Panel>\n      <Albums />\n    </Panel>\n  </Suspense>\n</Suspense>\n```\n\n이 변경으로 `Biography`를 보여줄 때 `Albums`가 로딩될 때까지 \"기다릴\" 필요가 없습니다.\n\n순서는 다음과 같습니다.\n\n1. `Biography`가 아직 로딩되지 않은 경우, 전체 콘텐츠 영역 대신 `BigSpinner`가 표시됩니다.\n1. `Biography`의 로딩이 완료되면 `BigSpinner`가 콘텐츠로 대체됩니다.\n1. `Albums`가 아직 로딩되지 않은 경우, `Albums`와 그 상위 `Panel` 대신 `AlbumsGlimmer`가 표시됩니다.\n1. 마지막으로 `Albums`가 로딩을 완료하면 `AlbumsGlimmer`를 대체합니다.\n\n<Sandpack>\n\n```js src/App.js hidden\nimport { useState } from 'react';\nimport ArtistPage from './ArtistPage.js';\n\nexport default function App() {\n  const [show, setShow] = useState(false);\n  if (show) {\n    return (\n      <ArtistPage\n        artist={{\n          id: 'the-beatles',\n          name: 'The Beatles',\n        }}\n      />\n    );\n  } else {\n    return (\n      <button onClick={() => setShow(true)}>\n        Open The Beatles artist page\n      </button>\n    );\n  }\n}\n```\n\n```js src/ArtistPage.js active\nimport { Suspense } from 'react';\nimport Albums from './Albums.js';\nimport Biography from './Biography.js';\nimport Panel from './Panel.js';\n\nexport default function ArtistPage({ artist }) {\n  return (\n    <>\n      <h1>{artist.name}</h1>\n      <Suspense fallback={<BigSpinner />}>\n        <Biography artistId={artist.id} />\n        <Suspense fallback={<AlbumsGlimmer />}>\n          <Panel>\n            <Albums artistId={artist.id} />\n          </Panel>\n        </Suspense>\n      </Suspense>\n    </>\n  );\n}\n\nfunction BigSpinner() {\n  return <h2>🌀 Loading...</h2>;\n}\n\nfunction AlbumsGlimmer() {\n  return (\n    <div className=\"glimmer-panel\">\n      <div className=\"glimmer-line\" />\n      <div className=\"glimmer-line\" />\n      <div className=\"glimmer-line\" />\n    </div>\n  );\n}\n```\n\n```js src/Panel.js\nexport default function Panel({ children }) {\n  return (\n    <section className=\"panel\">\n      {children}\n    </section>\n  );\n}\n```\n\n```js src/Biography.js\nimport {use} from 'react';\nimport { fetchData } from './data.js';\n\nexport default function Biography({ artistId }) {\n  const bio = use(fetchData(`/${artistId}/bio`));\n  return (\n    <section>\n      <p className=\"bio\">{bio}</p>\n    </section>\n  );\n}\n```\n\n```js src/Albums.js\nimport {use} from 'react';\nimport { fetchData } from './data.js';\n\nexport default function Albums({ artistId }) {\n  const albums = use(fetchData(`/${artistId}/albums`));\n  return (\n    <ul>\n      {albums.map(album => (\n        <li key={album.id}>\n          {album.title} ({album.year})\n        </li>\n      ))}\n    </ul>\n  );\n}\n```\n\n```js src/data.js hidden\n// Note: the way you would do data fetching depends on\n// the framework that you use together with Suspense.\n// Normally, the caching logic would be inside a framework.\n\nlet cache = new Map();\n\nexport function fetchData(url) {\n  if (!cache.has(url)) {\n    cache.set(url, getData(url));\n  }\n  return cache.get(url);\n}\n\nasync function getData(url) {\n  if (url === '/the-beatles/albums') {\n    return await getAlbums();\n  } else if (url === '/the-beatles/bio') {\n    return await getBio();\n  } else {\n    throw Error('Not implemented');\n  }\n}\n\nasync function getBio() {\n  // Add a fake delay to make waiting noticeable.\n  await new Promise(resolve => {\n    setTimeout(resolve, 500);\n  });\n\n  return `The Beatles were an English rock band,\n    formed in Liverpool in 1960, that comprised\n    John Lennon, Paul McCartney, George Harrison\n    and Ringo Starr.`;\n}\n\nasync function getAlbums() {\n  // Add a fake delay to make waiting noticeable.\n  await new Promise(resolve => {\n    setTimeout(resolve, 3000);\n  });\n\n  return [{\n    id: 13,\n    title: 'Let It Be',\n    year: 1970\n  }, {\n    id: 12,\n    title: 'Abbey Road',\n    year: 1969\n  }, {\n    id: 11,\n    title: 'Yellow Submarine',\n    year: 1969\n  }, {\n    id: 10,\n    title: 'The Beatles',\n    year: 1968\n  }, {\n    id: 9,\n    title: 'Magical Mystery Tour',\n    year: 1967\n  }, {\n    id: 8,\n    title: 'Sgt. Pepper\\'s Lonely Hearts Club Band',\n    year: 1967\n  }, {\n    id: 7,\n    title: 'Revolver',\n    year: 1966\n  }, {\n    id: 6,\n    title: 'Rubber Soul',\n    year: 1965\n  }, {\n    id: 5,\n    title: 'Help!',\n    year: 1965\n  }, {\n    id: 4,\n    title: 'Beatles For Sale',\n    year: 1964\n  }, {\n    id: 3,\n    title: 'A Hard Day\\'s Night',\n    year: 1964\n  }, {\n    id: 2,\n    title: 'With The Beatles',\n    year: 1963\n  }, {\n    id: 1,\n    title: 'Please Please Me',\n    year: 1963\n  }];\n}\n```\n\n```css\n.bio { font-style: italic; }\n\n.panel {\n  border: 1px solid #aaa;\n  border-radius: 6px;\n  margin-top: 20px;\n  padding: 10px;\n}\n\n.glimmer-panel {\n  border: 1px dashed #aaa;\n  background: linear-gradient(90deg, rgba(221,221,221,1) 0%, rgba(255,255,255,1) 100%);\n  border-radius: 6px;\n  margin-top: 20px;\n  padding: 10px;\n}\n\n.glimmer-line {\n  display: block;\n  width: 60%;\n  height: 20px;\n  margin: 10px;\n  border-radius: 4px;\n  background: #f0f0f0;\n}\n```\n\n</Sandpack>\n\nSuspense 경계를 사용하면 UI의 어떤 부분이 항상 동시에 그려져야 하는지, 어떤 부분이 로딩 순서에서 점진적으로 더 많은 콘텐츠를 보여줘야 하는지 조정할 수 있습니다. 앱의 나머지 동작에 영향을 주지 않고 트리의 어느 위치에서나 Suspense를 추가, 이동, 삭제할 수 있습니다.\n\n모든 컴포넌트 주위에 Suspense를 두지 마세요. Suspense는 사용자가 경험하기를 원하는 로딩 순서보다 더 세분화되어서는 안 됩니다. 디자이너와 함께 작업하는 경우 로딩 상태를 어디에 배치해야 하는지 디자이너에게 물어보세요. 디자이너가 이미 디자인 와이어 프레임에 포함했을 가능성이 높습니다.\n\n---\n\n### 새 콘텐츠가 로딩되는 동안 이전 콘텐츠 보여주기 {/*showing-stale-content-while-fresh-content-is-loading*/}\n\n이 예시에서는 검색 결과를 가져오는 동안 `SearchResults` 컴포넌트가 지연됩니다. `\"a\"`를 입력하고 결과를 기다린 다음 `\"ab\"`로 바꿔보세요. `\"a\"`에 대한 결과는 로딩 Fallback으로 바뀝니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { Suspense, useState } from 'react';\nimport SearchResults from './SearchResults.js';\n\nexport default function App() {\n  const [query, setQuery] = useState('');\n  return (\n    <>\n      <label>\n        Search albums:\n        <input value={query} onChange={e => setQuery(e.target.value)} />\n      </label>\n      <Suspense fallback={<h2>Loading...</h2>}>\n        <SearchResults query={query} />\n      </Suspense>\n    </>\n  );\n}\n```\n\n```js src/SearchResults.js\nimport {use} from 'react';\nimport { fetchData } from './data.js';\n\nexport default function SearchResults({ query }) {\n  if (query === '') {\n    return null;\n  }\n  const albums = use(fetchData(`/search?q=${query}`));\n  if (albums.length === 0) {\n    return <p>No matches for <i>\"{query}\"</i></p>;\n  }\n  return (\n    <ul>\n      {albums.map(album => (\n        <li key={album.id}>\n          {album.title} ({album.year})\n        </li>\n      ))}\n    </ul>\n  );\n}\n```\n\n```js src/data.js hidden\n// Note: the way you would do data fetching depends on\n// the framework that you use together with Suspense.\n// Normally, the caching logic would be inside a framework.\n\nlet cache = new Map();\n\nexport function fetchData(url) {\n  if (!cache.has(url)) {\n    cache.set(url, getData(url));\n  }\n  return cache.get(url);\n}\n\nasync function getData(url) {\n  if (url.startsWith('/search?q=')) {\n    return await getSearchResults(url.slice('/search?q='.length));\n  } else {\n    throw Error('Not implemented');\n  }\n}\n\nasync function getSearchResults(query) {\n  // Add a fake delay to make waiting noticeable.\n  await new Promise(resolve => {\n    setTimeout(resolve, 500);\n  });\n\n  const allAlbums = [{\n    id: 13,\n    title: 'Let It Be',\n    year: 1970\n  }, {\n    id: 12,\n    title: 'Abbey Road',\n    year: 1969\n  }, {\n    id: 11,\n    title: 'Yellow Submarine',\n    year: 1969\n  }, {\n    id: 10,\n    title: 'The Beatles',\n    year: 1968\n  }, {\n    id: 9,\n    title: 'Magical Mystery Tour',\n    year: 1967\n  }, {\n    id: 8,\n    title: 'Sgt. Pepper\\'s Lonely Hearts Club Band',\n    year: 1967\n  }, {\n    id: 7,\n    title: 'Revolver',\n    year: 1966\n  }, {\n    id: 6,\n    title: 'Rubber Soul',\n    year: 1965\n  }, {\n    id: 5,\n    title: 'Help!',\n    year: 1965\n  }, {\n    id: 4,\n    title: 'Beatles For Sale',\n    year: 1964\n  }, {\n    id: 3,\n    title: 'A Hard Day\\'s Night',\n    year: 1964\n  }, {\n    id: 2,\n    title: 'With The Beatles',\n    year: 1963\n  }, {\n    id: 1,\n    title: 'Please Please Me',\n    year: 1963\n  }];\n\n  const lowerQuery = query.trim().toLowerCase();\n  return allAlbums.filter(album => {\n    const lowerTitle = album.title.toLowerCase();\n    return (\n      lowerTitle.startsWith(lowerQuery) ||\n      lowerTitle.indexOf(' ' + lowerQuery) !== -1\n    )\n  });\n}\n```\n\n```css\ninput { margin: 10px; }\n```\n\n</Sandpack>\n\n일반적인 대체 UI 패턴은 목록들에 대한 업데이트를 <em>지연<sup>Defer</sup></em>하고 새 결과가 준비될 때까지 이전 결과를 계속 보여주는 것입니다. [`useDeferredValue`](/reference/react/useDeferredValue) Hook을 사용하면 쿼리의 지연된 버전을 아래로 전달할 수 있습니다.\n\n```js {3,11}\nexport default function App() {\n  const [query, setQuery] = useState('');\n  const deferredQuery = useDeferredValue(query);\n  return (\n    <>\n      <label>\n        Search albums:\n        <input value={query} onChange={e => setQuery(e.target.value)} />\n      </label>\n      <Suspense fallback={<h2>Loading...</h2>}>\n        <SearchResults query={deferredQuery} />\n      </Suspense>\n    </>\n  );\n}\n```\n\n`query`는 즉시 업데이트되므로 입력에 새 값이 표시됩니다. 그러나 `deferredQuery`는 데이터가 로딩될 때까지 이전 값을 유지하므로 `SearchResults`는 잠시 동안 이전 결과를 보여줍니다.\n\n사용자에게 더 명확하게 알리기 위해 이전 결과 목록이 표시될 때 시각적 표시를 추가할 수 있습니다.\n\n```js {2}\n<div style={{\n  opacity: query !== deferredQuery ? 0.5 : 1\n}}>\n  <SearchResults query={deferredQuery} />\n</div>\n```\n\n아래 예시에서 `\"a\"`를 입력하고 결과가 로딩될 때까지 기다린 다음 입력을 `\"ab\"`로 편집해보세요. 이제 새 결과가 로드될 때까지 Suspense Fallback 대신 희미한 이전 결과 목록이 표시되는 것을 확인할 수 있습니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { Suspense, useState, useDeferredValue } from 'react';\nimport SearchResults from './SearchResults.js';\n\nexport default function App() {\n  const [query, setQuery] = useState('');\n  const deferredQuery = useDeferredValue(query);\n  const isStale = query !== deferredQuery;\n  return (\n    <>\n      <label>\n        Search albums:\n        <input value={query} onChange={e => setQuery(e.target.value)} />\n      </label>\n      <Suspense fallback={<h2>Loading...</h2>}>\n        <div style={{ opacity: isStale ? 0.5 : 1 }}>\n          <SearchResults query={deferredQuery} />\n        </div>\n      </Suspense>\n    </>\n  );\n}\n```\n\n```js src/SearchResults.js hidden\nimport {use} from 'react';\nimport { fetchData } from './data.js';\n\nexport default function SearchResults({ query }) {\n  if (query === '') {\n    return null;\n  }\n  const albums = use(fetchData(`/search?q=${query}`));\n  if (albums.length === 0) {\n    return <p>No matches for <i>\"{query}\"</i></p>;\n  }\n  return (\n    <ul>\n      {albums.map(album => (\n        <li key={album.id}>\n          {album.title} ({album.year})\n        </li>\n      ))}\n    </ul>\n  );\n}\n```\n\n```js src/data.js hidden\n// Note: the way you would do data fetching depends on\n// the framework that you use together with Suspense.\n// Normally, the caching logic would be inside a framework.\n\nlet cache = new Map();\n\nexport function fetchData(url) {\n  if (!cache.has(url)) {\n    cache.set(url, getData(url));\n  }\n  return cache.get(url);\n}\n\nasync function getData(url) {\n  if (url.startsWith('/search?q=')) {\n    return await getSearchResults(url.slice('/search?q='.length));\n  } else {\n    throw Error('Not implemented');\n  }\n}\n\nasync function getSearchResults(query) {\n  // Add a fake delay to make waiting noticeable.\n  await new Promise(resolve => {\n    setTimeout(resolve, 500);\n  });\n\n  const allAlbums = [{\n    id: 13,\n    title: 'Let It Be',\n    year: 1970\n  }, {\n    id: 12,\n    title: 'Abbey Road',\n    year: 1969\n  }, {\n    id: 11,\n    title: 'Yellow Submarine',\n    year: 1969\n  }, {\n    id: 10,\n    title: 'The Beatles',\n    year: 1968\n  }, {\n    id: 9,\n    title: 'Magical Mystery Tour',\n    year: 1967\n  }, {\n    id: 8,\n    title: 'Sgt. Pepper\\'s Lonely Hearts Club Band',\n    year: 1967\n  }, {\n    id: 7,\n    title: 'Revolver',\n    year: 1966\n  }, {\n    id: 6,\n    title: 'Rubber Soul',\n    year: 1965\n  }, {\n    id: 5,\n    title: 'Help!',\n    year: 1965\n  }, {\n    id: 4,\n    title: 'Beatles For Sale',\n    year: 1964\n  }, {\n    id: 3,\n    title: 'A Hard Day\\'s Night',\n    year: 1964\n  }, {\n    id: 2,\n    title: 'With The Beatles',\n    year: 1963\n  }, {\n    id: 1,\n    title: 'Please Please Me',\n    year: 1963\n  }];\n\n  const lowerQuery = query.trim().toLowerCase();\n  return allAlbums.filter(album => {\n    const lowerTitle = album.title.toLowerCase();\n    return (\n      lowerTitle.startsWith(lowerQuery) ||\n      lowerTitle.indexOf(' ' + lowerQuery) !== -1\n    )\n  });\n}\n```\n\n```css\ninput { margin: 10px; }\n```\n\n</Sandpack>\n\n<Note>\n\n지연된 값<sup>Deferred Value</sup>과 [Transition](#preventing-already-revealed-content-from-hiding)을 사용하면 Suspense Fallback을 표시하지 않을 수 있습니다. Transition은 전체 업데이트를 긴급하지 않은 것으로 처리하므로 일반적으로 프레임워크와 Router 라이브러리에서 Navigation을 위해 사용합니다. 반면에 지연된 값<sup>Deferred Value</sup>은 UI의 일부를 긴급하지 않은 것으로 처리하고 나머지 UI보다 \"지연\"시키려는 목적의 애플리케이션 코드에서 유용합니다.\n\n</Note>\n\n---\n\n### 이미 보인 콘텐츠가 숨겨지지 않도록 방지 {/*preventing-already-revealed-content-from-hiding*/}\n\n컴포넌트가 지연되면 가장 가까운 상위 Suspense가 Fallback을 보여주도록 전환합니다. 이미 일부 콘텐츠가 보이는 경우 사용자 경험이 끊길 수 있습니다. 이 버튼을 눌러 보세요.\n\n<Sandpack>\n\n```js src/App.js\nimport { Suspense, useState } from 'react';\nimport IndexPage from './IndexPage.js';\nimport ArtistPage from './ArtistPage.js';\nimport Layout from './Layout.js';\n\nexport default function App() {\n  return (\n    <Suspense fallback={<BigSpinner />}>\n      <Router />\n    </Suspense>\n  );\n}\n\nfunction Router() {\n  const [page, setPage] = useState('/');\n\n  function navigate(url) {\n    setPage(url);\n  }\n\n  let content;\n  if (page === '/') {\n    content = (\n      <IndexPage navigate={navigate} />\n    );\n  } else if (page === '/the-beatles') {\n    content = (\n      <ArtistPage\n        artist={{\n          id: 'the-beatles',\n          name: 'The Beatles',\n        }}\n      />\n    );\n  }\n  return (\n    <Layout>\n      {content}\n    </Layout>\n  );\n}\n\nfunction BigSpinner() {\n  return <h2>🌀 Loading...</h2>;\n}\n```\n\n```js src/Layout.js\nexport default function Layout({ children }) {\n  return (\n    <div className=\"layout\">\n      <section className=\"header\">\n        Music Browser\n      </section>\n      <main>\n        {children}\n      </main>\n    </div>\n  );\n}\n```\n\n```js src/IndexPage.js\nexport default function IndexPage({ navigate }) {\n  return (\n    <button onClick={() => navigate('/the-beatles')}>\n      Open The Beatles artist page\n    </button>\n  );\n}\n```\n\n```js src/ArtistPage.js\nimport { Suspense } from 'react';\nimport Albums from './Albums.js';\nimport Biography from './Biography.js';\nimport Panel from './Panel.js';\n\nexport default function ArtistPage({ artist }) {\n  return (\n    <>\n      <h1>{artist.name}</h1>\n      <Biography artistId={artist.id} />\n      <Suspense fallback={<AlbumsGlimmer />}>\n        <Panel>\n          <Albums artistId={artist.id} />\n        </Panel>\n      </Suspense>\n    </>\n  );\n}\n\nfunction AlbumsGlimmer() {\n  return (\n    <div className=\"glimmer-panel\">\n      <div className=\"glimmer-line\" />\n      <div className=\"glimmer-line\" />\n      <div className=\"glimmer-line\" />\n    </div>\n  );\n}\n```\n\n```js src/Albums.js\nimport {use} from 'react';\nimport { fetchData } from './data.js';\n\nexport default function Albums({ artistId }) {\n  const albums = use(fetchData(`/${artistId}/albums`));\n  return (\n    <ul>\n      {albums.map(album => (\n        <li key={album.id}>\n          {album.title} ({album.year})\n        </li>\n      ))}\n    </ul>\n  );\n}\n```\n\n```js src/Biography.js\nimport {use} from 'react';\nimport { fetchData } from './data.js';\n\nexport default function Biography({ artistId }) {\n  const bio = use(fetchData(`/${artistId}/bio`));\n  return (\n    <section>\n      <p className=\"bio\">{bio}</p>\n    </section>\n  );\n}\n```\n\n```js src/Panel.js\nexport default function Panel({ children }) {\n  return (\n    <section className=\"panel\">\n      {children}\n    </section>\n  );\n}\n```\n\n```js src/data.js hidden\n// Note: the way you would do data fetching depends on\n// the framework that you use together with Suspense.\n// Normally, the caching logic would be inside a framework.\n\nlet cache = new Map();\n\nexport function fetchData(url) {\n  if (!cache.has(url)) {\n    cache.set(url, getData(url));\n  }\n  return cache.get(url);\n}\n\nasync function getData(url) {\n  if (url === '/the-beatles/albums') {\n    return await getAlbums();\n  } else if (url === '/the-beatles/bio') {\n    return await getBio();\n  } else {\n    throw Error('Not implemented');\n  }\n}\n\nasync function getBio() {\n  // Add a fake delay to make waiting noticeable.\n  await new Promise(resolve => {\n    setTimeout(resolve, 500);\n  });\n\n  return `The Beatles were an English rock band,\n    formed in Liverpool in 1960, that comprised\n    John Lennon, Paul McCartney, George Harrison\n    and Ringo Starr.`;\n}\n\nasync function getAlbums() {\n  // Add a fake delay to make waiting noticeable.\n  await new Promise(resolve => {\n    setTimeout(resolve, 3000);\n  });\n\n  return [{\n    id: 13,\n    title: 'Let It Be',\n    year: 1970\n  }, {\n    id: 12,\n    title: 'Abbey Road',\n    year: 1969\n  }, {\n    id: 11,\n    title: 'Yellow Submarine',\n    year: 1969\n  }, {\n    id: 10,\n    title: 'The Beatles',\n    year: 1968\n  }, {\n    id: 9,\n    title: 'Magical Mystery Tour',\n    year: 1967\n  }, {\n    id: 8,\n    title: 'Sgt. Pepper\\'s Lonely Hearts Club Band',\n    year: 1967\n  }, {\n    id: 7,\n    title: 'Revolver',\n    year: 1966\n  }, {\n    id: 6,\n    title: 'Rubber Soul',\n    year: 1965\n  }, {\n    id: 5,\n    title: 'Help!',\n    year: 1965\n  }, {\n    id: 4,\n    title: 'Beatles For Sale',\n    year: 1964\n  }, {\n    id: 3,\n    title: 'A Hard Day\\'s Night',\n    year: 1964\n  }, {\n    id: 2,\n    title: 'With The Beatles',\n    year: 1963\n  }, {\n    id: 1,\n    title: 'Please Please Me',\n    year: 1963\n  }];\n}\n```\n\n```css\nmain {\n  min-height: 200px;\n  padding: 10px;\n}\n\n.layout {\n  border: 1px solid black;\n}\n\n.header {\n  background: #222;\n  padding: 10px;\n  text-align: center;\n  color: white;\n}\n\n.bio { font-style: italic; }\n\n.panel {\n  border: 1px solid #aaa;\n  border-radius: 6px;\n  margin-top: 20px;\n  padding: 10px;\n}\n\n.glimmer-panel {\n  border: 1px dashed #aaa;\n  background: linear-gradient(90deg, rgba(221,221,221,1) 0%, rgba(255,255,255,1) 100%);\n  border-radius: 6px;\n  margin-top: 20px;\n  padding: 10px;\n}\n\n.glimmer-line {\n  display: block;\n  width: 60%;\n  height: 20px;\n  margin: 10px;\n  border-radius: 4px;\n  background: #f0f0f0;\n}\n```\n\n</Sandpack>\n\n버튼을 눌렀을 때 `Router` 컴포넌트가 `IndexPage` 대신 `ArtistPage`를 렌더링했습니다. `ArtistPage` 내부의 컴포넌트가 지연됐기 때문에 가장 가까운 Suspense가 Fallback을 보여주기 시작했습니다. 가장 가까운 Suspense가 Root 근처에 있었기 때문에 전체 사이트 레이아웃이 `BigSpinner`로 대체되었습니다.\n\n이를 방지하려면 [`startTransition`](/reference/react/startTransition)을 사용하여 Navigation State 업데이트를 *Transition*으로 처리할 수 있습니다.\n\n```js {5,7}\nfunction Router() {\n  const [page, setPage] = useState('/');\n\n  function navigate(url) {\n    startTransition(() => {\n      setPage(url);\n    });\n  }\n  // ...\n```\n\n이는 State 전환이 급하지 않으며, 이미 공개된 콘텐츠를 숨기는 대신 이전 페이지를 계속 표시하는 것이 좋다는 것을 React에게 알려줍니다. 이제 버튼을 클릭하면 `Biography`가 로딩될 때까지 \"대기\"합니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { Suspense, startTransition, useState } from 'react';\nimport IndexPage from './IndexPage.js';\nimport ArtistPage from './ArtistPage.js';\nimport Layout from './Layout.js';\n\nexport default function App() {\n  return (\n    <Suspense fallback={<BigSpinner />}>\n      <Router />\n    </Suspense>\n  );\n}\n\nfunction Router() {\n  const [page, setPage] = useState('/');\n\n  function navigate(url) {\n    startTransition(() => {\n      setPage(url);\n    });\n  }\n\n  let content;\n  if (page === '/') {\n    content = (\n      <IndexPage navigate={navigate} />\n    );\n  } else if (page === '/the-beatles') {\n    content = (\n      <ArtistPage\n        artist={{\n          id: 'the-beatles',\n          name: 'The Beatles',\n        }}\n      />\n    );\n  }\n  return (\n    <Layout>\n      {content}\n    </Layout>\n  );\n}\n\nfunction BigSpinner() {\n  return <h2>🌀 Loading...</h2>;\n}\n```\n\n```js src/Layout.js\nexport default function Layout({ children }) {\n  return (\n    <div className=\"layout\">\n      <section className=\"header\">\n        Music Browser\n      </section>\n      <main>\n        {children}\n      </main>\n    </div>\n  );\n}\n```\n\n```js src/IndexPage.js\nexport default function IndexPage({ navigate }) {\n  return (\n    <button onClick={() => navigate('/the-beatles')}>\n      Open The Beatles artist page\n    </button>\n  );\n}\n```\n\n```js src/ArtistPage.js\nimport { Suspense } from 'react';\nimport Albums from './Albums.js';\nimport Biography from './Biography.js';\nimport Panel from './Panel.js';\n\nexport default function ArtistPage({ artist }) {\n  return (\n    <>\n      <h1>{artist.name}</h1>\n      <Biography artistId={artist.id} />\n      <Suspense fallback={<AlbumsGlimmer />}>\n        <Panel>\n          <Albums artistId={artist.id} />\n        </Panel>\n      </Suspense>\n    </>\n  );\n}\n\nfunction AlbumsGlimmer() {\n  return (\n    <div className=\"glimmer-panel\">\n      <div className=\"glimmer-line\" />\n      <div className=\"glimmer-line\" />\n      <div className=\"glimmer-line\" />\n    </div>\n  );\n}\n```\n\n```js src/Albums.js\nimport {use} from 'react';\nimport { fetchData } from './data.js';\n\nexport default function Albums({ artistId }) {\n  const albums = use(fetchData(`/${artistId}/albums`));\n  return (\n    <ul>\n      {albums.map(album => (\n        <li key={album.id}>\n          {album.title} ({album.year})\n        </li>\n      ))}\n    </ul>\n  );\n}\n```\n\n```js src/Biography.js\nimport {use} from 'react';\nimport { fetchData } from './data.js';\n\nexport default function Biography({ artistId }) {\n  const bio = use(fetchData(`/${artistId}/bio`));\n  return (\n    <section>\n      <p className=\"bio\">{bio}</p>\n    </section>\n  );\n}\n```\n\n```js src/Panel.js\nexport default function Panel({ children }) {\n  return (\n    <section className=\"panel\">\n      {children}\n    </section>\n  );\n}\n```\n\n```js src/data.js hidden\n// Note: the way you would do data fetching depends on\n// the framework that you use together with Suspense.\n// Normally, the caching logic would be inside a framework.\n\nlet cache = new Map();\n\nexport function fetchData(url) {\n  if (!cache.has(url)) {\n    cache.set(url, getData(url));\n  }\n  return cache.get(url);\n}\n\nasync function getData(url) {\n  if (url === '/the-beatles/albums') {\n    return await getAlbums();\n  } else if (url === '/the-beatles/bio') {\n    return await getBio();\n  } else {\n    throw Error('Not implemented');\n  }\n}\n\nasync function getBio() {\n  // Add a fake delay to make waiting noticeable.\n  await new Promise(resolve => {\n    setTimeout(resolve, 500);\n  });\n\n  return `The Beatles were an English rock band,\n    formed in Liverpool in 1960, that comprised\n    John Lennon, Paul McCartney, George Harrison\n    and Ringo Starr.`;\n}\n\nasync function getAlbums() {\n  // Add a fake delay to make waiting noticeable.\n  await new Promise(resolve => {\n    setTimeout(resolve, 3000);\n  });\n\n  return [{\n    id: 13,\n    title: 'Let It Be',\n    year: 1970\n  }, {\n    id: 12,\n    title: 'Abbey Road',\n    year: 1969\n  }, {\n    id: 11,\n    title: 'Yellow Submarine',\n    year: 1969\n  }, {\n    id: 10,\n    title: 'The Beatles',\n    year: 1968\n  }, {\n    id: 9,\n    title: 'Magical Mystery Tour',\n    year: 1967\n  }, {\n    id: 8,\n    title: 'Sgt. Pepper\\'s Lonely Hearts Club Band',\n    year: 1967\n  }, {\n    id: 7,\n    title: 'Revolver',\n    year: 1966\n  }, {\n    id: 6,\n    title: 'Rubber Soul',\n    year: 1965\n  }, {\n    id: 5,\n    title: 'Help!',\n    year: 1965\n  }, {\n    id: 4,\n    title: 'Beatles For Sale',\n    year: 1964\n  }, {\n    id: 3,\n    title: 'A Hard Day\\'s Night',\n    year: 1964\n  }, {\n    id: 2,\n    title: 'With The Beatles',\n    year: 1963\n  }, {\n    id: 1,\n    title: 'Please Please Me',\n    year: 1963\n  }];\n}\n```\n\n```css\nmain {\n  min-height: 200px;\n  padding: 10px;\n}\n\n.layout {\n  border: 1px solid black;\n}\n\n.header {\n  background: #222;\n  padding: 10px;\n  text-align: center;\n  color: white;\n}\n\n.bio { font-style: italic; }\n\n.panel {\n  border: 1px solid #aaa;\n  border-radius: 6px;\n  margin-top: 20px;\n  padding: 10px;\n}\n\n.glimmer-panel {\n  border: 1px dashed #aaa;\n  background: linear-gradient(90deg, rgba(221,221,221,1) 0%, rgba(255,255,255,1) 100%);\n  border-radius: 6px;\n  margin-top: 20px;\n  padding: 10px;\n}\n\n.glimmer-line {\n  display: block;\n  width: 60%;\n  height: 20px;\n  margin: 10px;\n  border-radius: 4px;\n  background: #f0f0f0;\n}\n```\n\n</Sandpack>\n\nTransition은 *모든* 콘텐츠가 로딩될 때까지 기다리지 않습니다. 이미 보여진 콘텐츠가 숨겨지지 않도록 충분히 오래 기다립니다. 예를 들어 웹사이트 `Layout`은 이미 보이므로 로딩 스피너 뒤에 숨기는 것은 좋지 않을 것입니다. 그러나 `Albums` 주위에 중첩된 `Suspense`는 새로운 것이므로 Transition이 기다리지 않습니다.\n\n<Note>\n\nSuspense를 지원하는 Router는 기본적으로 Navigation 업데이트를 Transition으로 래핑할 것으로 예상합니다.\n\n</Note>\n\n---\n\n### Transition이 발생하고 있음을 보여주기 {/*indicating-that-a-transition-is-happening*/}\n\n위의 예시에서는 버튼을 클릭해도 Navigation이 진행 중이라는 시각적 표시가 없습니다. 표시기를 추가하려면 [`startTransition`](/reference/react/startTransition)을 불리언<sup>Boolean</sup> 값인 `isPending` 값을 제공하는 [`useTransition`](/reference/react/useTransition)으로 바꾸면 됩니다. 아래 예시에서는 Transition이 진행되는 동안 웹사이트 헤더 스타일을 변경하는 데 사용됩니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { Suspense, useState, useTransition } from 'react';\nimport IndexPage from './IndexPage.js';\nimport ArtistPage from './ArtistPage.js';\nimport Layout from './Layout.js';\n\nexport default function App() {\n  return (\n    <Suspense fallback={<BigSpinner />}>\n      <Router />\n    </Suspense>\n  );\n}\n\nfunction Router() {\n  const [page, setPage] = useState('/');\n  const [isPending, startTransition] = useTransition();\n\n  function navigate(url) {\n    startTransition(() => {\n      setPage(url);\n    });\n  }\n\n  let content;\n  if (page === '/') {\n    content = (\n      <IndexPage navigate={navigate} />\n    );\n  } else if (page === '/the-beatles') {\n    content = (\n      <ArtistPage\n        artist={{\n          id: 'the-beatles',\n          name: 'The Beatles',\n        }}\n      />\n    );\n  }\n  return (\n    <Layout isPending={isPending}>\n      {content}\n    </Layout>\n  );\n}\n\nfunction BigSpinner() {\n  return <h2>🌀 Loading...</h2>;\n}\n```\n\n```js src/Layout.js\nexport default function Layout({ children, isPending }) {\n  return (\n    <div className=\"layout\">\n      <section className=\"header\" style={{\n        opacity: isPending ? 0.7 : 1\n      }}>\n        Music Browser\n      </section>\n      <main>\n        {children}\n      </main>\n    </div>\n  );\n}\n```\n\n```js src/IndexPage.js\nexport default function IndexPage({ navigate }) {\n  return (\n    <button onClick={() => navigate('/the-beatles')}>\n      Open The Beatles artist page\n    </button>\n  );\n}\n```\n\n```js src/ArtistPage.js\nimport { Suspense } from 'react';\nimport Albums from './Albums.js';\nimport Biography from './Biography.js';\nimport Panel from './Panel.js';\n\nexport default function ArtistPage({ artist }) {\n  return (\n    <>\n      <h1>{artist.name}</h1>\n      <Biography artistId={artist.id} />\n      <Suspense fallback={<AlbumsGlimmer />}>\n        <Panel>\n          <Albums artistId={artist.id} />\n        </Panel>\n      </Suspense>\n    </>\n  );\n}\n\nfunction AlbumsGlimmer() {\n  return (\n    <div className=\"glimmer-panel\">\n      <div className=\"glimmer-line\" />\n      <div className=\"glimmer-line\" />\n      <div className=\"glimmer-line\" />\n    </div>\n  );\n}\n```\n\n```js src/Albums.js\nimport {use} from 'react';\nimport { fetchData } from './data.js';\n\nexport default function Albums({ artistId }) {\n  const albums = use(fetchData(`/${artistId}/albums`));\n  return (\n    <ul>\n      {albums.map(album => (\n        <li key={album.id}>\n          {album.title} ({album.year})\n        </li>\n      ))}\n    </ul>\n  );\n}\n```\n\n```js src/Biography.js\nimport {use} from 'react';\nimport { fetchData } from './data.js';\n\nexport default function Biography({ artistId }) {\n  const bio = use(fetchData(`/${artistId}/bio`));\n  return (\n    <section>\n      <p className=\"bio\">{bio}</p>\n    </section>\n  );\n}\n```\n\n```js src/Panel.js\nexport default function Panel({ children }) {\n  return (\n    <section className=\"panel\">\n      {children}\n    </section>\n  );\n}\n```\n\n```js src/data.js hidden\n// Note: the way you would do data fetching depends on\n// the framework that you use together with Suspense.\n// Normally, the caching logic would be inside a framework.\n\nlet cache = new Map();\n\nexport function fetchData(url) {\n  if (!cache.has(url)) {\n    cache.set(url, getData(url));\n  }\n  return cache.get(url);\n}\n\nasync function getData(url) {\n  if (url === '/the-beatles/albums') {\n    return await getAlbums();\n  } else if (url === '/the-beatles/bio') {\n    return await getBio();\n  } else {\n    throw Error('Not implemented');\n  }\n}\n\nasync function getBio() {\n  // Add a fake delay to make waiting noticeable.\n  await new Promise(resolve => {\n    setTimeout(resolve, 500);\n  });\n\n  return `The Beatles were an English rock band,\n    formed in Liverpool in 1960, that comprised\n    John Lennon, Paul McCartney, George Harrison\n    and Ringo Starr.`;\n}\n\nasync function getAlbums() {\n  // Add a fake delay to make waiting noticeable.\n  await new Promise(resolve => {\n    setTimeout(resolve, 3000);\n  });\n\n  return [{\n    id: 13,\n    title: 'Let It Be',\n    year: 1970\n  }, {\n    id: 12,\n    title: 'Abbey Road',\n    year: 1969\n  }, {\n    id: 11,\n    title: 'Yellow Submarine',\n    year: 1969\n  }, {\n    id: 10,\n    title: 'The Beatles',\n    year: 1968\n  }, {\n    id: 9,\n    title: 'Magical Mystery Tour',\n    year: 1967\n  }, {\n    id: 8,\n    title: 'Sgt. Pepper\\'s Lonely Hearts Club Band',\n    year: 1967\n  }, {\n    id: 7,\n    title: 'Revolver',\n    year: 1966\n  }, {\n    id: 6,\n    title: 'Rubber Soul',\n    year: 1965\n  }, {\n    id: 5,\n    title: 'Help!',\n    year: 1965\n  }, {\n    id: 4,\n    title: 'Beatles For Sale',\n    year: 1964\n  }, {\n    id: 3,\n    title: 'A Hard Day\\'s Night',\n    year: 1964\n  }, {\n    id: 2,\n    title: 'With The Beatles',\n    year: 1963\n  }, {\n    id: 1,\n    title: 'Please Please Me',\n    year: 1963\n  }];\n}\n```\n\n```css\nmain {\n  min-height: 200px;\n  padding: 10px;\n}\n\n.layout {\n  border: 1px solid black;\n}\n\n.header {\n  background: #222;\n  padding: 10px;\n  text-align: center;\n  color: white;\n}\n\n.bio { font-style: italic; }\n\n.panel {\n  border: 1px solid #aaa;\n  border-radius: 6px;\n  margin-top: 20px;\n  padding: 10px;\n}\n\n.glimmer-panel {\n  border: 1px dashed #aaa;\n  background: linear-gradient(90deg, rgba(221,221,221,1) 0%, rgba(255,255,255,1) 100%);\n  border-radius: 6px;\n  margin-top: 20px;\n  padding: 10px;\n}\n\n.glimmer-line {\n  display: block;\n  width: 60%;\n  height: 20px;\n  margin: 10px;\n  border-radius: 4px;\n  background: #f0f0f0;\n}\n```\n\n</Sandpack>\n\n---\n\n### Navigation에서 Suspense 재설정하기 {/*resetting-suspense-boundaries-on-navigation*/}\n\nTransition이 진행되는 동안 React는 이미 보인 콘텐츠를 숨기지 않습니다. 하지만 다른 매개변수가 있는 경로로 이동하는 경우 React에 *다른* 콘텐츠라고 알려주고 싶을 수 있습니다. 이를 `key`로 표현할 수 있습니다.\n\n```js\n<ProfilePage key={queryParams.id} />\n```\n\n사용자의 프로필 페이지 내에서 이동 중인데 무언가가 지연되었다고 가정해 보세요. 해당 업데이트가 Transition으로 감싸져 있으면 이미 표시된 콘텐츠에 대한 Fallback이 트리거되지 않습니다. 이것이 예상되는 동작입니다.\n\n하지만 이제 두 개의 서로 다른 사용자 프로필 사이를 이동한다고 가정해 보겠습니다. 이 경우 Fallback을 표시하는 것이 좋습니다. 예를 들어 한 사용자의 타임라인이 다른 사용자의 타임라인과 *다른 콘텐츠*라고, 가정해 보겠습니다. `key`를 지정하면 React가 서로 다른 사용자의 프로필을 서로 다른 컴포넌트로 취급하고 탐색하는 동안 Suspense를 재설정하도록 할 수 있습니다. Suspense 통합 라우터는 이 동작을 자동으로 수행해야 합니다.\n\n---\n\n### 서버 에러 및 클라이언트 전용 콘텐츠에 대한 Fallback 제공 {/*providing-a-fallback-for-server-errors-and-client-only-content*/}\n\n[스트리밍 서버 렌더링 API](/reference/react-dom/server) 중 하나(또는 이에 의존하는 프레임워크)를 사용하는 경우, React는 서버의 에러를 처리하기 위해 `<Suspense>` 경계도 사용할 것입니다. 컴포넌트가 서버에서 에러를 발생시키더라도 React는 서버 렌더링을 중단하지 않습니다. 대신, 그 위에 있는 가장 가까운 `<Suspense>` 컴포넌트를 찾아서 그 Fallback(예: 스피너)을 생성된 서버 HTML에 포함합니다. 사용자는 처음에는 스피너를 보게 됩니다.\n\n클라이언트에서 React는 동일한 컴포넌트를 다시 렌더링하려고 시도합니다. 클라이언트에서도 에러가 발생하면 React는 에러를 던지고 가장 가까운 [Error Boundary](/reference/react/Component#static-getderivedstatefromerror)를 표시합니다. 그러나 클라이언트에서 에러가 발생하지 않으면 콘텐츠가 결국 성공적으로 보였기 때문에 React는 사용자에게 에러를 보여주지 않습니다.\n\n이를 사용하여 일부 컴포넌트를 서버에서 렌더링하지 않도록 선택할 수 있습니다. 이렇게 하려면 서버 환경에서 에러를 발생시킨 다음 `<Suspense>` 경계로 감싸서 해당 HTML을 Fallback으로 대체합니다.\n\n```js\n<Suspense fallback={<Loading />}>\n  <Chat />\n</Suspense>\n\nfunction Chat() {\n  if (typeof window === 'undefined') {\n    throw Error('Chat should only render on the client.');\n  }\n  // ...\n}\n```\n\n서버 HTML에 로딩 UI가 포함됩니다. 클라이언트에서는 `Chat` 컴포넌트로 대체됩니다.\n\n---\n\n## 문제 해결 {/*troubleshooting*/}\n\n### 업데이트 중에 UI가 Fallback으로 대체되는 것을 방지하려면 어떻게 해야 하나요? {/*preventing-unwanted-fallbacks*/}\n\n표시되는 UI를 Fallback으로 대체하면 사용자 환경이 불안정해집니다. 이는 업데이트로 인해 컴포넌트가 지연되고 가장 가까운 Suspense가 이미 사용자에게 콘텐츠를 보여주고 있을 때 발생할 수 있습니다.\n\n이런 일이 발생하지 않도록 하려면, [`startTransition`](#preventing-already-revealed-content-from-hiding)을 사용하여 업데이트를 긴급하지 않은 것으로 처리하세요. Transition이 진행되는 동안 React는 원치 않는 Fallback이 나타나지 않도록 충분한 데이터가 로딩될 때까지 기다립니다.\n\n```js {2-3,5}\nfunction handleNextPageClick() {\n  // If this update suspends, don't hide the already displayed content\n  startTransition(() => {\n    setCurrentPage(currentPage + 1);\n  });\n}\n```\n\n이렇게 하면 기존 콘텐츠가 숨겨지지 않습니다. 그러나 새로 렌더링된 `Suspense`는 여전히 즉시 Fallback을 보여줘서 UI를 차단하지 않고 사용자가 콘텐츠를 이용할 수 있게 합니다.\n\n**React는 긴급하지 않은 업데이트 중에만 원치 않는 Fallback을 방지합니다**. 긴급한 업데이트의 결과인 경우 렌더링을 지연시키지 않습니다. [`startTransition`](/reference/react/startTransition) 또는 [`useDeferredValue`](/reference/react/useDeferredValue)와 같은 API를 사용해야 합니다.\n\nRouter가 Suspense와 통합된 경우, Router는 업데이트를 자동으로 [`startTransition`](/reference/react/startTransition)에 래핑해야 합니다.\n"
  },
  {
    "path": "src/content/reference/react/ViewTransition.md",
    "content": "---\ntitle: <ViewTransition>\nversion: canary\n---\n\n<Canary>\n\n**`<ViewTransition />` API는 현재 React의 카나리 및 실험적 채널에서만 사용할 수 있습니다.** \n\n[React의 배포 채널에 대해 더 알아보세요.](/community/versioning-policy#all-release-channels)\n\n</Canary>\n\n<Intro>\n\n`<ViewTransition>`을 사용하면 Transition 내부에서 업데이트되는 엘리먼트에 애니메이션을 적용할 수 있습니다.\n\n\n```js\nimport {ViewTransition} from 'react';\n\n<ViewTransition>\n  <div>...</div>\n</ViewTransition>\n```\n\n</Intro>\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `<ViewTransition>` {/*viewtransition*/}\n\n엘리먼트를 `<ViewTransition>`으로 감싸면 [Transition](/reference/react/useTransition) 내부에서 업데이트할 때 애니메이션을 적용할 수 있습니다. React는 다음 휴리스틱을 사용하여 View Transition이 애니메이션에 활성화되는지 판단합니다.\n\n- `enter`: 해당 Transition에서 `ViewTransition` 자체가 삽입되면 활성화됩니다.\n- `exit`: 해당 Transition에서 `ViewTransition` 자체가 삭제되면 활성화됩니다.\n- `update`: `ViewTransition` 내부에서 React가 수행하는 DOM 변경(예: 프로퍼티 변경)이 있거나 인접한 형제 엘리먼트의 영향으로 `ViewTransition` 경계 자체의 크기나 위치가 변경되는 경우 활성화됩니다. 중첩된 `ViewTransition`이 있으면 변경이 부모가 아닌 해당 항목에 적용됩니다.\n- `share`: 이름이 지정된 `ViewTransition`이 삭제된 서브트리 내부에 있고 같은 이름을 가진 다른 이름 있는 `ViewTransition`이 같은 Transition에서 삽입된 서브트리의 일부인 경우 공유 엘리먼트 Transition을 형성하며, 삭제된 것에서 삽입된 것으로 애니메이션됩니다.\n\n기본적으로 `<ViewTransition>`은 부드러운 크로스 페이드(브라우저 기본 View Transition)로 애니메이션됩니다. `<ViewTransition>` 컴포넌트에 [View Transition 클래스](#view-transition-class)를 제공하여 애니메이션을 커스터마이징할 수 있습니다. 각 트리거 유형에 대해 애니메이션을 커스터마이징할 수 있습니다([View Transition 스타일링](#styling-view-transitions) 참고).\n\n<DeepDive>\n\n#### `<ViewTransition>`은 어떻게 작동하나요? {/*how-does-viewtransition-work*/}\n\n내부적으로 React는 `<ViewTransition>` 컴포넌트 내부에 중첩된 가장 가까운 DOM 노드의 인라인 스타일에 `view-transition-name`을 적용합니다. `<ViewTransition><div /><div /></ViewTransition>`처럼 여러 형제 DOM 노드가 있을 경우, React는 각 노드의 이름이 고유하도록 접미사를 추가하지만, 개념적으로는 동일한 전환에 속하는 것으로 간주합니다.\n\nReact는 내부적으로 `startViewTransition`을 자체적으로 호출하므로 직접 호출해서는 안됩니다. 실제로 페이지에서 다른 스크립트나 코드가 ViewTransition을 실행하고 있다면 React가 이를 중단합니다. 따라서 React 자체를 사용하여 이를 조정하는 것을 권장합니다. 과거에 ViewTransition을 트리거하는 다른 방법이 있었다면 내장 방법으로 마이그레이션하는 것을 권장합니다.\n\n다른 React ViewTransition이 이미 실행 중이라면, React는 그것들을 완료할 때까지 다음 전환을 시작하지 않습니다. 그러나 중요한 점은 첫 번째 전환이 진행되는 동안 여러 업데이트가 발생하면, 그 업데이트들은 모두 하나로 묶여 처리된다는 것입니다. 예를 들어 A에서 B로 이동하는 전환을 시작했다고 가정합시다. 그 사이에 C로 가는 업데이트가 발생하고 다시 D로 가는 업데이트가 발생한다면, 첫 번째 A->B 애니메이션이 끝난 후 다음 애니메이션은 B에서 D로 전환됩니다.\n\n`getSnapshotBeforeUpdate` 생명주기는 `startViewTransition` 전에 호출되고 일부 `view-transition-name`은 동시에 업데이트됩니다.\n\n그런 다음 React는 `startViewTransition`을 호출합니다. `updateCallback` 내부에서 React는 다음을 수행합니다.\n\n- DOM에 변경을 적용하고 `useInsertionEffect`를 호출합니다.\n- 폰트가 로드될 때까지 기다립니다.\n- componentDidMount, componentDidUpdate, useLayoutEffect, refs를 호출합니다.\n- 대기 중인 탐색이 완료될 때까지 기다립니다.\n- 그런 다음 React는 레이아웃의 변경 사항을 측정하여 어떤 경계가 애니메이션되어야 하는지 확인합니다.\n\n`startViewTransition`의 ready Promise가 해결된 이후, React는 `view-transition-name`을 되돌립니다. 그 다음 React는 `onEnter`, `onExit`, `onUpdate`, `onShare` 콜백들을 호출하여 애니메이션에 대해 수동으로 프로그래밍 방식의 제어를 할 수 있도록 합니다. 이 호출은 내장된 기본 애니메이션이 이미 계산된 이후에 이루어집니다.\n\n이 시퀀스 중간에 `flushSync`가 발생하면 동기적으로 완료되어야 하는 특성 때문에 React는 해당 Transition을 건너뜁니다.\n\n`startViewTransition`의 finished Promise가 해결된 이후에 React는 `useEffect`를 호출합니다. 이렇게 하면 `useEffect`가 애니메이션 성능에 영향을 주지 않도록 방지할 수 있습니다. 그러나 이것이 반드시 보장되는 것은 아닙니다. 만약 애니메이션이 실행되는 도중에 다른 `setState`가 발생하면, 순차적 동작 보장을 유지하기 위해 `useEffect`를 더 일찍 호출해야 할 수도 있습니다.\n\n</DeepDive>\n\n#### Props {/*props*/}\n\n기본적으로 `<ViewTransition>`은 부드러운 크로스 페이드로 애니메이션됩니다. 이러한 프로퍼티로 애니메이션을 커스터마이즈하거나 공유 엘리먼트 Transition을 지정할 수 있습니다.\n\n* **optional** `enter`: 문자열 또는 객체. \"enter\"가 활성화될 때 적용할 [View Transition 클래스](#view-transition-class)입니다.\n* **optional** `exit`: 문자열 또는 객체. \"exit\"이 활성화될 때 적용할 [View Transition 클래스](#view-transition-class)입니다.\n* **optional** `update`: 문자열 또는 객체. \"update\"가 활성화될 때 적용할 [View Transition 클래스](#view-transition-class)입니다.\n* **optional** `share`: 문자열 또는 객체. 공유 엘리먼트가 활성화될 때 적용할 [View Transition 클래스](#view-transition-class)입니다.\n* **optional** `default`: 문자열 또는 객체. 다른 일치하는 활성화 프로퍼티가 없을 때 사용되는 [View Transition 클래스](#view-transition-class)입니다.\n* **optional** `name`: 문자열 또는 객체. 공유 엘리먼트 transition에 사용되는 View Transition의 이름입니다. 제공되지 않으면 React는 예상치 못한 애니메이션을 방지하기 위해 각 View Transition에 대해 고유한 이름을 사용합니다.\n\n#### 콜백 {/*events*/}\n\n이 콜백을 사용하면 [animate](https://developer.mozilla.org/en-US/docs/Web/API/Element/animate) API를 사용하여 애니메이션을 명령적으로 조정할 수 있습니다.\n\n* **optional** `onEnter`: 함수. React는 \"enter\" 애니메이션 후에 `onEnter`를 호출합니다.\n* **optional** `onExit`: 함수. React는 \"exit\" 애니메이션 후에 `onExit`를 호출합니다.\n* **optional** `onShare`: 함수. React는 \"share\" 애니메이션 후에 `onShare`를 호출합니다.\n* **optional** `onUpdate`: 함수. React는 \"update\" 애니메이션 후에 `onUpdate`를 호출합니다.\n\n각 콜백은 다음을 인수로 받습니다.\n- `element`: 애니메이션된 DOM 엘리먼트입니다.\n- `types`: 애니메이션에 포함된 [Transition 타입](/reference/react/addTransitionType)입니다.\n\n### View Transition 클래스 {/*view-transition-class*/}\n\nView Transition 클래스는 ViewTransition이 활성화될 때 Transition 중에 React가 적용하는 CSS 클래스 이름입니다. 문자열 또는 객체일 수 있습니다.\n- `string`: 활성화될 때 자식 엘리먼트에 추가되는 `class`입니다. `'none'`이 제공되면 클래스가 추가되지 않습니다.\n- `object`: 자식 엘리먼트에 추가되는 클래스는 `addTransitionType`으로 추가된 View Transition 타입과 일치하는 키입니다. 객체는 일치하는 타입이 없을 때 사용할 `default`도 지정할 수 있습니다.\n\n값 `'none'`은 특정 트리거에 대해 View Transition이 활성화되지 않도록 하는 데 사용할 수 있습니다.\n\n### View Transition 스타일링 {/*styling-view-transitions*/}\n\n<Note>\n\n웹에서 View Transition의 많은 초기 예시에서 [`view-transition-name`](https://developer.mozilla.org/en-US/docs/Web/CSS/view-transition-name)을 사용한 다음 `::view-transition-...(my-name)` 선택자를 사용하여 스타일을 지정하는 것을 볼 수 있습니다. 그러나 이러한 방식으로 스타일링하는 것을 권장하지 않습니다. 대신, 일반적으로 View Transition 클래스를 사용하는 것을 권장합니다.\n\n</Note>\n\n`<ViewTransition>`의 애니메이션을 커스터마이즈하려면 활성화 프로퍼티 중 하나에 View Transition 클래스를 제공할 수 있습니다. View Transition 클래스는 ViewTransition이 활성화될 때 React가 자식 엘리먼트에 적용하는 CSS 클래스 이름입니다.\n\n예를 들어 \"enter\" 애니메이션을 커스터마이즈하려면 `enter` 프로퍼티에 클래스 이름을 제공합니다.\n\n```js\n<ViewTransition enter=\"slide-in\">\n```\n\n`<ViewTransition>`이 \"enter\" 애니메이션을 활성화하면 React는 클래스 이름 `slide-in`을 추가합니다. 그런 다음 [View Transition 가상 선택자](https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API#pseudo-elements)를 사용하여 이 클래스를 참조하여 재사용 가능한 애니메이션을 구축할 수 있습니다.\n\n```css\n::view-transition-group(.slide-in) {\n}\n::view-transition-old(.slide-in) {\n}\n::view-transition-new(.slide-in) {\n}\n```\n향후 CSS 라이브러리에서 View Transition 클래스를 사용한 내장 애니메이션을 추가하여 사용하기 쉽게 만들 수 있습니다.\n\n#### 주의 사항 {/*caveats*/}\n\n- 기본적으로 `setState`는 즉시 업데이트되며 `<ViewTransition>`을 활성화하지 않습니다. [Transition](/reference/react/useTransition)으로 감싼 업데이트만 활성화됩니다. `<Suspense>`](/reference/react/Suspense)를 사용하여 Transition을 선택적으로 적용하고 [콘텐츠를 한 번에 표시](/reference/react/Suspense#revealing-content-together-at-once)할 수도 있습니다.\n- `<ViewTransition>`은 이동, 크기 조정 및 크로스-페이드가 가능한 이미지를 생성합니다. React Native나 Motion에서 볼 수 있는 레이아웃 애니메이션과 달리, 내부의 모든 개별 요소가 위치를 애니메이션하는 것이 아닙니다. 이는 모든 개별 요소를 애니메이션하는 것에 비해 더 나은 성능과 더 연속적이고 부드러운 애니메이션을 제공할 수 있습니다. 하지만 독립적으로 움직여야 하는 요소들의 연속성을 잃을 수도 있습니다. 따라서 수동으로 더 많은 `<ViewTransition>` 경계를 추가해야 할 수 있습니다.\n- 많은 사용자가 페이지의 애니메이션을 선호하지 않을 수 있습니다. React는 이 경우에 대해 자동으로 애니메이션을 비활성화하지 않습니다. 사용자 선호도에 따라 애니메이션을 비활성화하거나 줄이기 위해 `@media (prefers-reduced-motion)` 미디어 쿼리를 사용할 것을 권장합니다. 향후 CSS 라이브러리에서 프리셋에 이 기능이 내장될 수 있습니다.\n- 현재 `<ViewTransition>`은 DOM에서만 작동합니다. React Native 및 기타 플랫폼에 대한 지원을 추가하기 위해 작업 중입니다.\n\n\n---\n\n\n## 사용법 {/*usage*/}\n\n### enter/exit에서 엘리먼트 애니메이션 적용하기 {/*animating-an-element-on-enter*/}\n\nEnter/Exit Transition은 `<ViewTransition>`이 Transition에서 컴포넌트에 의해 추가되거나 제거될 때 발생합니다.\n\n```js {3}\nfunction Child() {\n  return (\n    <ViewTransition enter=\"auto\" exit=\"auto\" default=\"none\">\n      <div>Hi</div>\n    </ViewTransition>\n  );\n}\n\nfunction Parent() {\n  const [show, setShow] = useState();\n  if (show) {\n    return <Child />;\n  }\n  return null;\n}\n```\n\n`setShow`가 호출되면 `show`가 `true`로 바뀌고 `Child` 컴포넌트가 렌더링됩니다. `setShow`가 `startTransition` 내부에서 호출되고 `Child`가 다른 DOM 노드보다 먼저 `ViewTransition`을 렌더링하면 `enter` 애니메이션이 발생합니다.\n\n`show`가 다시 `false`로 바뀌면 `exit` 애니메이션이 발생합니다.\n\n<Sandpack>\n\n```js src/Video.js hidden\nfunction Thumbnail({video, children}) {\n  return (\n    <div\n      aria-hidden=\"true\"\n      tabIndex={-1}\n      className={`thumbnail ${video.image}`}\n    />\n  );\n}\n\nexport function Video({video}) {\n  return (\n    <div className=\"video\">\n      <div className=\"link\">\n        <Thumbnail video={video}></Thumbnail>\n        <div className=\"info\">\n          <div className=\"video-title\">{video.title}</div>\n          <div className=\"video-description\">{video.description}</div>\n        </div>\n      </div>\n    </div>\n  );\n}\n```\n\n```js\nimport {ViewTransition, useState, startTransition} from 'react';\nimport {Video} from './Video';\nimport videos from './data';\n\nfunction Item() {\n  return (\n    <ViewTransition enter=\"auto\" exit=\"auto\" default=\"none\">\n      <Video video={videos[0]} />\n    </ViewTransition>\n  );\n}\n\nexport default function Component() {\n  const [showItem, setShowItem] = useState(false);\n  return (\n    <>\n      <button\n        onClick={() => {\n          startTransition(() => {\n            setShowItem((prev) => !prev);\n          });\n        }}>\n        {showItem ? '➖' : '➕'}\n      </button>\n\n      {showItem ? <Item /> : null}\n    </>\n  );\n}\n```\n\n```js src/data.js hidden\nexport default [\n  {\n    id: '1',\n    title: 'First video',\n    description: 'Video description',\n    image: 'blue',\n  },\n];\n```\n\n```css\n#root {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  min-height: 200px;\n}\nbutton {\n  border: none;\n  border-radius: 50%;\n  width: 50px;\n  height: 50px;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  background-color: #f0f8ff;\n  color: white;\n  font-size: 20px;\n  cursor: pointer;\n  transition: background-color 0.3s, border 0.3s;\n}\nbutton:hover {\n  border: 2px solid #ccc;\n  background-color: #e0e8ff;\n}\n.thumbnail {\n  position: relative;\n  aspect-ratio: 16 / 9;\n  display: flex;\n  overflow: hidden;\n  flex-direction: column;\n  justify-content: center;\n  align-items: center;\n  border-radius: 0.5rem;\n  outline-offset: 2px;\n  width: 8rem;\n  vertical-align: middle;\n  background-color: #ffffff;\n  background-size: cover;\n  user-select: none;\n}\n.thumbnail.blue {\n  background-image: conic-gradient(at top right, #c76a15, #087ea4, #2b3491);\n}\n.video {\n  display: flex;\n  flex-direction: row;\n  gap: 0.75rem;\n  align-items: center;\n  margin-top: 1em;\n}\n.video .link {\n  display: flex;\n  flex-direction: row;\n  flex: 1 1 0;\n  gap: 0.125rem;\n  outline-offset: 4px;\n  cursor: pointer;\n}\n.video .info {\n  display: flex;\n  flex-direction: column;\n  justify-content: center;\n  margin-left: 8px;\n  gap: 0.125rem;\n}\n.video .info:hover {\n  text-decoration: underline;\n}\n.video-title {\n  font-size: 15px;\n  line-height: 1.25;\n  font-weight: 700;\n  color: #23272f;\n}\n.video-description {\n  color: #5e687e;\n  font-size: 13px;\n}\n```\n\n```json package.json hidden\n{\n  \"dependencies\": {\n    \"react\": \"canary\",\n    \"react-dom\": \"canary\",\n    \"react-scripts\": \"latest\"\n  }\n}\n```\n\n</Sandpack>\n\n<Pitfall>\n\n`<ViewTransition>`은 DOM 노드보다 앞에 배치되어야만 활성화됩니다. `Child`가 다음과 같다면 애니메이션이 발생하지 않습니다.\n\n```js [3, 5]\nfunction Item() {\n  return (\n    <div> {/* 🚩<div> above <ViewTransition> breaks exit/enter */}\n      <ViewTransition enter=\"auto\" exit=\"auto\" default=\"none\">\n        <Video video={videos[0]} />\n      </ViewTransition>\n    </div>\n  );\n}\n```\n\nThis constraint prevents subtle bugs where too much or too little animates.\n\n</Pitfall>\n\n---\n### 공유 엘리먼트 애니메이션 적용하기 {/*animating-a-shared-element*/}\n\n일반적으로 `<ViewTransition>`에 이름을 할당하는 것보다 React가 자동으로 이름을 할당하도록 하는 것을 권장합니다. 이름을 할당하고 싶은 경우는 하나의 트리가 마운트 해제되고 다른 트리가 동시에 마운트될 때 완전히 다른 컴포넌트 간에 애니메이션을 적용하여 연속성을 보존하고자 할 때입니다.\n\n```js\n<ViewTransition name={UNIQUE_NAME}>\n  <Child />\n</ViewTransition>\n```\n\n하나의 트리가 마운트 해제되고 다른 트리가 마운트될 때 마운트 해제되는 트리와 마운트되는 트리에서 동일한 이름이 존재하는 쌍이 있으면 둘 다에서 \"share\" 애니메이션이 발생합니다. 마운트 해제되는 쪽에서 마운트되는 쪽으로 애니메이션이 적용됩니다.\n\nexit/enter 애니메이션과 달리 삭제되거나 새로 마운트된 트리의 깊숙한 곳에서도 적용될 수 있습니다. `<ViewTransition>`이 exit/enter에도 해당한다면 \"share\" 애니메이션이 우선순위를 갖습니다.\n\nTransition이 먼저 한쪽을 마운트 해제하고 새로운 이름이 마운트되기 전에 `<Suspense>` 폴백이 표시되는 경우 공유 엘리먼트 Transition은 발생하지 않습니다.\n\n<Sandpack>\n\n```js\nimport {ViewTransition, useState, startTransition} from 'react';\nimport {Video, Thumbnail, FullscreenVideo} from './Video';\nimport videos from './data';\n\nexport default function Component() {\n  const [fullscreen, setFullscreen] = useState(false);\n  if (fullscreen) {\n    return (\n      <FullscreenVideo\n        video={videos[0]}\n        onExit={() => startTransition(() => setFullscreen(false))}\n      />\n    );\n  }\n  return (\n    <Video\n      video={videos[0]}\n      onClick={() => startTransition(() => setFullscreen(true))}\n    />\n  );\n}\n```\n\n```js src/Video.js\nimport {ViewTransition} from 'react';\n\nconst THUMBNAIL_NAME = 'video-thumbnail';\n\nexport function Thumbnail({video, children}) {\n  return (\n    <ViewTransition name={THUMBNAIL_NAME}>\n      <div\n        aria-hidden=\"true\"\n        tabIndex={-1}\n        className={`thumbnail ${video.image}`}\n      />\n    </ViewTransition>\n  );\n}\n\nexport function Video({video, onClick}) {\n  return (\n    <div className=\"video\">\n      <div className=\"link\" onClick={onClick}>\n        <Thumbnail video={video} />\n        <div className=\"info\">\n          <div className=\"video-title\">{video.title}</div>\n          <div className=\"video-description\">{video.description}</div>\n        </div>\n      </div>\n    </div>\n  );\n}\n\nexport function FullscreenVideo({video, onExit}) {\n  return (\n    <div className=\"fullscreenLayout\">\n      <ViewTransition name={THUMBNAIL_NAME}>\n        <div\n          aria-hidden=\"true\"\n          tabIndex={-1}\n          className={`thumbnail ${video.image} fullscreen`}\n        />\n        <button className=\"close-button\" onClick={onExit}>\n          ✖\n        </button>\n      </ViewTransition>\n    </div>\n  );\n}\n```\n\n```js src/data.js hidden\nexport default [\n  {\n    id: '1',\n    title: 'First video',\n    description: 'Video description',\n    image: 'blue',\n  },\n];\n```\n\n```css\n#root {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  height: 300px;\n}\nbutton {\n  border: none;\n  border-radius: 50%;\n  width: 50px;\n  height: 50px;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  background-color: #f0f8ff;\n  color: white;\n  font-size: 20px;\n  cursor: pointer;\n  transition: background-color 0.3s, border 0.3s;\n}\nbutton:hover {\n  border: 2px solid #ccc;\n  background-color: #e0e8ff;\n}\n.thumbnail {\n  position: relative;\n  aspect-ratio: 16 / 9;\n  display: flex;\n  overflow: hidden;\n  flex-direction: column;\n  justify-content: center;\n  align-items: center;\n  border-radius: 0.5rem;\n  outline-offset: 2px;\n  width: 8rem;\n  vertical-align: middle;\n  background-color: #ffffff;\n  background-size: cover;\n  user-select: none;\n}\n.thumbnail.blue {\n  background-image: conic-gradient(at top right, #c76a15, #087ea4, #2b3491);\n}\n.thumbnail.red {\n  background-image: conic-gradient(at top right, #c76a15, #a6423a, #2b3491);\n}\n.thumbnail.fullscreen {\n  width: 100%;\n}\n.video {\n  display: flex;\n  flex-direction: row;\n  gap: 0.75rem;\n  align-items: center;\n  margin-top: 1em;\n}\n.video .link {\n  display: flex;\n  flex-direction: row;\n  flex: 1 1 0;\n  gap: 0.125rem;\n  outline-offset: 4px;\n  cursor: pointer;\n}\n.video .info {\n  display: flex;\n  flex-direction: column;\n  justify-content: center;\n  margin-left: 8px;\n  gap: 0.125rem;\n}\n.video .info:hover {\n  text-decoration: underline;\n}\n.video-title {\n  font-size: 15px;\n  line-height: 1.25;\n  font-weight: 700;\n  color: #23272f;\n}\n.video-description {\n  color: #5e687e;\n  font-size: 13px;\n}\n.fullscreenLayout {\n  position: relative;\n  height: 100%;\n  width: 100%;\n}\n.close-button {\n  position: absolute;\n  top: 10px;\n  right: 10px;\n  color: black;\n}\n@keyframes progress-animation {\n  from {\n    width: 0;\n  }\n  to {\n    width: 100%;\n  }\n}\n```\n\n```json package.json hidden\n{\n  \"dependencies\": {\n    \"react\": \"canary\",\n    \"react-dom\": \"canary\",\n    \"react-scripts\": \"latest\"\n  }\n}\n```\n\n</Sandpack>\n\n<Note>\n\n한 쌍의 마운트된 쪽이나 마운트 해제된 쪽 중 하나가 뷰포트 밖에 있으면 쌍이 형성되지 않습니다. 이는 무언가가 스크롤될 때 뷰포트 안팎으로 날아가는 것을 방지합니다. 대신 일반적인 enter/exit로 자체적으로 처리됩니다.\n\n동일한 컴포넌트 인스턴스가 위치를 변경하는 경우에는 이런 일이 발생하지 않으며 \"update\"가 발생합니다. 한 위치가 뷰포트 밖에 있어도 애니메이션이 적용됩니다.\n\n현재 한 가지 특이한 점이 있는데, 깊게 중첩된 마운트 해제된 `<ViewTransition>`이 뷰포트 안에 있고, 마운트되는 쪽이 뷰포트 밖에 있는 경우, 해당 마운트 해제된 요소는 부모 애니메이션의 일부로 동작하는 대신, 깊게 중첩되어 있더라도 자체적인 \"exit\" 애니메이션으로 동작하게 됩니다.\n\n</Note>\n\n<Pitfall>\n\n전체 앱에서 동시에 동일한 `name`으로 마운트된 것이 하나만 있어야 한다는 것이 중요합니다. 따라서 충돌을 피하기 위해 `name`에 고유한 네임스페이스를 사용하는 것이 중요합니다. 이를 확실히 하기 위해 가져올 수 있는 별도 모듈에 상수를 추가하는 것이 좋습니다.\n\n```js\nexport const MY_NAME = \"my-globally-unique-name\";\nimport {MY_NAME} from './shared-name';\n...\n<ViewTransition name={MY_NAME}>\n```\n\n</Pitfall>\n\n---\n\n### 목록에서 항목 순서 변경 애니메이션 적용하기 {/*animating-reorder-of-items-in-a-list*/}\n\n```js\nitems.map((item) => <Component key={item.id} item={item} />);\n```\n\n콘텐츠를 업데이트하지 않고 목록 순서를 변경할 때 DOM 노드 밖에 있으면 목록의 각 `<ViewTransition>`에서 \"update\" 애니메이션이 발생합니다. enter/exit 애니메이션과 유사합니다.\n\n이는 이 `<ViewTransition>`에서 애니메이션이 발생한다는 의미입니다.\n\n```js\nfunction Component() {\n  return (\n    <ViewTransition>\n      <div>...</div>\n    </ViewTransition>\n  );\n}\n```\n\n<Sandpack>\n\n```js src/Video.js hidden\nfunction Thumbnail({video}) {\n  return (\n    <div\n      aria-hidden=\"true\"\n      tabIndex={-1}\n      className={`thumbnail ${video.image}`}\n    />\n  );\n}\n\nexport function Video({video}) {\n  return (\n    <div className=\"video\">\n      <div className=\"link\">\n        <Thumbnail video={video}></Thumbnail>\n        <div className=\"info\">\n          <div className=\"video-title\">{video.title}</div>\n          <div className=\"video-description\">{video.description}</div>\n        </div>\n      </div>\n    </div>\n  );\n}\n```\n\n```js\nimport {ViewTransition, useState, startTransition} from 'react';\nimport {Video} from './Video';\nimport videos from './data';\n\nexport default function Component() {\n  const [orderedVideos, setOrderedVideos] = useState(videos);\n  const reorder = () => {\n    startTransition(() => {\n      setOrderedVideos((prev) => {\n        return [...prev.sort(() => Math.random() - 0.5)];\n      });\n    });\n  };\n  return (\n    <>\n      <button onClick={reorder}>🎲</button>\n      <div className=\"listContainer\">\n        {orderedVideos.map((video, i) => {\n          return (\n            <ViewTransition key={video.title}>\n              <Video video={video} />\n            </ViewTransition>\n          );\n        })}\n      </div>\n    </>\n  );\n}\n```\n\n```js src/data.js hidden\nexport default [\n  {\n    id: '1',\n    title: 'First video',\n    description: 'Video description',\n    image: 'blue',\n  },\n  {\n    id: '2',\n    title: 'Second video',\n    description: 'Video description',\n    image: 'red',\n  },\n  {\n    id: '3',\n    title: 'Third video',\n    description: 'Video description',\n    image: 'green',\n  },\n  {\n    id: '4',\n    title: 'Fourth video',\n    description: 'Video description',\n    image: 'purple',\n  },\n];\n```\n\n```css\n#root {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  min-height: 150px;\n}\nbutton {\n  border: none;\n  border-radius: 50%;\n  width: 50px;\n  height: 50px;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  background-color: #f0f8ff;\n  color: white;\n  font-size: 20px;\n  cursor: pointer;\n  transition: background-color 0.3s, border 0.3s;\n}\nbutton:hover {\n  border: 2px solid #ccc;\n  background-color: #e0e8ff;\n}\n.thumbnail {\n  position: relative;\n  aspect-ratio: 16 / 9;\n  display: flex;\n  overflow: hidden;\n  flex-direction: column;\n  justify-content: center;\n  align-items: center;\n  border-radius: 0.5rem;\n  outline-offset: 2px;\n  width: 8rem;\n  vertical-align: middle;\n  background-color: #ffffff;\n  background-size: cover;\n  user-select: none;\n}\n.thumbnail.blue {\n  background-image: conic-gradient(at top right, #c76a15, #087ea4, #2b3491);\n}\n.thumbnail.red {\n  background-image: conic-gradient(at top right, #c76a15, #a6423a, #2b3491);\n}\n.thumbnail.green {\n  background-image: conic-gradient(at top right, #c76a15, #388f7f, #2b3491);\n}\n.thumbnail.purple {\n  background-image: conic-gradient(at top right, #c76a15, #575fb7, #2b3491);\n}\n.video {\n  display: flex;\n  flex-direction: row;\n  gap: 0.75rem;\n  align-items: center;\n  margin-top: 1em;\n}\n.video .link {\n  display: flex;\n  flex-direction: row;\n  flex: 1 1 0;\n  gap: 0.125rem;\n  outline-offset: 4px;\n}\n.video .info {\n  display: flex;\n  flex-direction: column;\n  justify-content: center;\n  margin-left: 8px;\n  gap: 0.125rem;\n}\n.video .info:hover {\n  text-decoration: underline;\n}\n.video-title {\n  font-size: 15px;\n  line-height: 1.25;\n  font-weight: 700;\n  color: #23272f;\n}\n.video-description {\n  color: #5e687e;\n  font-size: 13px;\n}\n```\n\n```json package.json hidden\n{\n  \"dependencies\": {\n    \"react\": \"canary\",\n    \"react-dom\": \"canary\",\n    \"react-scripts\": \"latest\"\n  }\n}\n```\n\n</Sandpack>\n\n하지만 다음은 각 개별 항목에 애니메이션을 적용하지 않습니다.\n\n```js\nfunction Component() {\n  return (\n    <div>\n      <ViewTransition>...</ViewTransition>\n    </div>\n  );\n}\n```\n대신 부모 `<ViewTransition>`이 크로스 페이드됩니다. 부모 `<ViewTransition>`이 없으면 별도의 애니메이션이 적용되지 않습니다.\n\n<Sandpack>\n\n```js src/Video.js hidden\nfunction Thumbnail({video}) {\n  return (\n    <div\n      aria-hidden=\"true\"\n      tabIndex={-1}\n      className={`thumbnail ${video.image}`}\n    />\n  );\n}\n\nexport function Video({video}) {\n  return (\n    <div className=\"video\">\n      <div className=\"link\">\n        <Thumbnail video={video}></Thumbnail>\n        <div className=\"info\">\n          <div className=\"video-title\">{video.title}</div>\n          <div className=\"video-description\">{video.description}</div>\n        </div>\n      </div>\n    </div>\n  );\n}\n```\n\n```js\nimport {ViewTransition, useState, startTransition} from 'react';\nimport {Video} from './Video';\nimport videos from './data';\n\nexport default function Component() {\n  const [orderedVideos, setOrderedVideos] = useState(videos);\n  const reorder = () => {\n    startTransition(() => {\n      setOrderedVideos((prev) => {\n        return [...prev.sort(() => Math.random() - 0.5)];\n      });\n    });\n  };\n  return (\n    <>\n      <button onClick={reorder}>🎲</button>\n      <ViewTransition>\n        <div className=\"listContainer\">\n          {orderedVideos.map((video, i) => {\n            return <Video video={video} key={video.title} />;\n          })}\n        </div>\n      </ViewTransition>\n    </>\n  );\n}\n```\n\n```js src/data.js hidden\nexport default [\n  {\n    id: '1',\n    title: 'First video',\n    description: 'Video description',\n    image: 'blue',\n  },\n  {\n    id: '2',\n    title: 'Second video',\n    description: 'Video description',\n    image: 'red',\n  },\n  {\n    id: '3',\n    title: 'Third video',\n    description: 'Video description',\n    image: 'green',\n  },\n  {\n    id: '4',\n    title: 'Fourth video',\n    description: 'Video description',\n    image: 'purple',\n  },\n];\n```\n\n```css\n#root {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  min-height: 150px;\n}\nbutton {\n  border: none;\n  border-radius: 50%;\n  width: 50px;\n  height: 50px;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  background-color: #f0f8ff;\n  color: white;\n  font-size: 20px;\n  cursor: pointer;\n  transition: background-color 0.3s, border 0.3s;\n}\nbutton:hover {\n  border: 2px solid #ccc;\n  background-color: #e0e8ff;\n}\n.thumbnail {\n  position: relative;\n  aspect-ratio: 16 / 9;\n  display: flex;\n  overflow: hidden;\n  flex-direction: column;\n  justify-content: center;\n  align-items: center;\n  border-radius: 0.5rem;\n  outline-offset: 2px;\n  width: 8rem;\n  vertical-align: middle;\n  background-color: #ffffff;\n  background-size: cover;\n  user-select: none;\n}\n.thumbnail.blue {\n  background-image: conic-gradient(at top right, #c76a15, #087ea4, #2b3491);\n}\n.thumbnail.red {\n  background-image: conic-gradient(at top right, #c76a15, #a6423a, #2b3491);\n}\n.thumbnail.green {\n  background-image: conic-gradient(at top right, #c76a15, #388f7f, #2b3491);\n}\n.thumbnail.purple {\n  background-image: conic-gradient(at top right, #c76a15, #575fb7, #2b3491);\n}\n.video {\n  display: flex;\n  flex-direction: row;\n  gap: 0.75rem;\n  align-items: center;\n  margin-top: 1em;\n}\n.video .link {\n  display: flex;\n  flex-direction: row;\n  flex: 1 1 0;\n  gap: 0.125rem;\n  outline-offset: 4px;\n}\n.video .info {\n  display: flex;\n  flex-direction: column;\n  justify-content: center;\n  margin-left: 8px;\n  gap: 0.125rem;\n}\n.video .info:hover {\n  text-decoration: underline;\n}\n.video-title {\n  font-size: 15px;\n  line-height: 1.25;\n  font-weight: 700;\n  color: #23272f;\n}\n.video-description {\n  color: #5e687e;\n  font-size: 13px;\n}\n```\n\n```json package.json hidden\n{\n  \"dependencies\": {\n    \"react\": \"canary\",\n    \"react-dom\": \"canary\",\n    \"react-scripts\": \"latest\"\n  }\n}\n```\n\n</Sandpack>\n\n이는 컴포넌트가 자체적으로 순서 변경 애니메이션을 제어할 수 있도록 하고 싶을 때는 리스트 안에 래퍼 요소를 두지 않는 것이 좋다는 뜻입니다.\n\n```\nitems.map(item => <div><Component key={item.id} item={item} /></div>)\n```\n\n위 규칙은 항목 중 하나가 크기 조정을 위해 업데이트되어 형제 항목들이 크기 조정되는 경우에도 적용되며, 이는 형제 `<ViewTransition>`도 애니메이션시키지만 직접적인 형제인 경우에만 해당합니다.\n\n이것은 업데이트가 발생하여 레이아웃이 크게 변경될 때, 페이지에 있는 모든 `<ViewTransition>`을 각각 개별적으로 애니메이션하지 않는다는 뜻입니다. 그렇게 하면 실제 변화와 관계없는 많은 산만한 애니메이션이 발생해 주의를 흐트러뜨리게 됩니다. 따라서 React는 개별 애니메이션을 언제 트리거할지에 대해 보다 보수적으로 동작합니다.\n\n<Pitfall>\n\n목록 순서를 변경할 때 아이덴티티를 보존하기 위해 키를 적절히 사용하는 것이 중요합니다. \"name\"이나 공유 엘리먼트 Transition을 사용하여 순서 변경을 애니메이션할 수 있을 것 같지만 한쪽이 뷰포트 밖에 있으면 발생하지 않습니다. 리스트를 재정렬하는 애니메이션을 만들 때는, 해당 항목이 화면에 보이지 않는 위치로 이동했음을 사용자에게 보여주는 것이 중요한 경우가 많습니다.\n\n</Pitfall>\n\n---\n\n### Suspense 콘텐츠에서 애니메이션 적용하기 {/*animating-from-suspense-content*/}\n\n다른 Transition과 마찬가지로 React는 애니메이션을 실행하기 전에 데이터와 새로운 CSS(`<link rel=\"stylesheet\" precedence=\"...\">`)를 기다립니다. 이에 더해 ViewTransition은 새로운 폰트가 나중에 깜빡이는 것을 방지하기 위해 애니메이션을 시작하기 전에 새로운 폰트가 로드될 때까지 최대 500ms까지 기다립니다. 같은 이유로 ViewTransition으로 래핑된 이미지는 이미지가 로드될 때까지 기다립니다.\n\n새로운 Suspense 경계 인스턴스 내부에 있으면 폴백이 먼저 표시됩니다. Suspense 경계가 완전히 로드된 후 `<ViewTransition>`이 콘텐츠로 전환되는 애니메이션을 실행합니다.\n\n현재 이 동작은 클라이언트 측 Transition에서만 발생합니다. 향후에는 초기 로드 중에 서버의 콘텐츠가 일시 중단될 때 스트리밍 SSR에 대한 Suspense 경계도 애니메이션할 예정입니다.\n\n`<ViewTransition>`을 배치하는 위치에 따라 Suspense 경계를 애니메이션하는 두 가지 방법이 있습니다.\n\n**Update:**\n\n```\n<ViewTransition>\n  <Suspense fallback={<A />}>\n    <B />\n  </Suspense>\n</ViewTransition>\n```\n이 시나리오에서 콘텐츠가 A에서 B로 바뀔 때 \"update\"로 처리되며 적절한 경우 해당 클래스를 적용합니다. A와 B 모두 동일한 view-transition-name을 갖게 되므로 기본적으로 크로스 페이드로 작동합니다.\n\n<Sandpack>\n\n```js src/Video.js hidden\nfunction Thumbnail({video, children}) {\n  return (\n    <div\n      aria-hidden=\"true\"\n      tabIndex={-1}\n      className={`thumbnail ${video.image}`}\n    />\n  );\n}\n\nexport function Video({video}) {\n  return (\n    <div className=\"video\">\n      <div className=\"link\">\n        <Thumbnail video={video}></Thumbnail>\n        <div className=\"info\">\n          <div className=\"video-title\">{video.title}</div>\n          <div className=\"video-description\">{video.description}</div>\n        </div>\n      </div>\n    </div>\n  );\n}\n\nexport function VideoPlaceholder() {\n  const video = {image: 'loading'};\n  return (\n    <div className=\"video\">\n      <div className=\"link\">\n        <Thumbnail video={video}></Thumbnail>\n        <div className=\"info\">\n          <div className=\"video-title loading\" />\n          <div className=\"video-description loading\" />\n        </div>\n      </div>\n    </div>\n  );\n}\n```\n\n```js\nimport {ViewTransition, useState, startTransition, Suspense} from 'react';\nimport {Video, VideoPlaceholder} from './Video';\nimport {useLazyVideoData} from './data';\n\nfunction LazyVideo() {\n  const video = useLazyVideoData();\n  return <Video video={video} />;\n}\n\nexport default function Component() {\n  const [showItem, setShowItem] = useState(false);\n  return (\n    <>\n      <button\n        onClick={() => {\n          startTransition(() => {\n            setShowItem((prev) => !prev);\n          });\n        }}>\n        {showItem ? '➖' : '➕'}\n      </button>\n      {showItem ? (\n        <ViewTransition>\n          <Suspense fallback={<VideoPlaceholder />}>\n            <LazyVideo />\n          </Suspense>\n        </ViewTransition>\n      ) : null}\n    </>\n  );\n}\n```\n\n```js src/data.js hidden\nimport {use} from 'react';\n\nlet cache = null;\n\nfunction fetchVideo() {\n  if (!cache) {\n    cache = new Promise((resolve) => {\n      setTimeout(() => {\n        resolve({\n          id: '1',\n          title: 'First video',\n          description: 'Video description',\n          image: 'blue',\n        });\n      }, 1000);\n    });\n  }\n  return cache;\n}\n\nexport function useLazyVideoData() {\n  return use(fetchVideo());\n}\n```\n\n```css\n#root {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  min-height: 200px;\n}\nbutton {\n  border: none;\n  border-radius: 50%;\n  width: 50px;\n  height: 50px;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  background-color: #f0f8ff;\n  color: white;\n  font-size: 20px;\n  cursor: pointer;\n  transition: background-color 0.3s, border 0.3s;\n}\nbutton:hover {\n  border: 2px solid #ccc;\n  background-color: #e0e8ff;\n}\n.thumbnail {\n  position: relative;\n  aspect-ratio: 16 / 9;\n  display: flex;\n  overflow: hidden;\n  flex-direction: column;\n  justify-content: center;\n  align-items: center;\n  border-radius: 0.5rem;\n  outline-offset: 2px;\n  width: 8rem;\n  vertical-align: middle;\n  background-color: #ffffff;\n  background-size: cover;\n  user-select: none;\n}\n.thumbnail.blue {\n  background-image: conic-gradient(at top right, #c76a15, #087ea4, #2b3491);\n}\n.loading {\n  background-image: linear-gradient(\n    90deg,\n    rgba(173, 216, 230, 0.3) 25%,\n    rgba(135, 206, 250, 0.5) 50%,\n    rgba(173, 216, 230, 0.3) 75%\n  );\n  background-size: 200% 100%;\n  animation: shimmer 1.5s infinite;\n}\n@keyframes shimmer {\n  0% {\n    background-position: -200% 0;\n  }\n  100% {\n    background-position: 200% 0;\n  }\n}\n.video {\n  display: flex;\n  flex-direction: row;\n  gap: 0.75rem;\n  align-items: center;\n  margin-top: 1em;\n}\n.video .link {\n  display: flex;\n  flex-direction: row;\n  flex: 1 1 0;\n  gap: 0.125rem;\n  outline-offset: 4px;\n  cursor: pointer;\n}\n.video .info {\n  display: flex;\n  flex-direction: column;\n  justify-content: center;\n  margin-left: 8px;\n  gap: 0.125rem;\n}\n.video .info:hover {\n  text-decoration: underline;\n}\n.video-title {\n  font-size: 15px;\n  line-height: 1.25;\n  font-weight: 700;\n  color: #23272f;\n}\n.video-title.loading {\n  height: 20px;\n  width: 80px;\n  border-radius: 0.5rem;\n}\n.video-description {\n  color: #5e687e;\n  font-size: 13px;\n  border-radius: 0.5rem;\n}\n.video-description.loading {\n  height: 15px;\n  width: 100px;\n}\n```\n\n```json package.json hidden\n{\n  \"dependencies\": {\n    \"react\": \"canary\",\n    \"react-dom\": \"canary\",\n    \"react-scripts\": \"latest\"\n  }\n}\n```\n\n</Sandpack>\n\n**Enter/Exit:**\n\n```\n<Suspense fallback={<ViewTransition><A /></ViewTransition>}>\n  <ViewTransition><B /></ViewTransition>\n</Suspense>\n```\n\n이 시나리오에서는 각각 고유한 `view-transition-name`을 갖는 두 개의 별도 ViewTransition 인스턴스입니다. 이는 `<A>`의 \"exit\"와 `<B>`의 \"enter\"로 처리됩니다.\n\n`<ViewTransition>` 경계를 배치하는 위치에 따라 다른 효과를 얻을 수 있습니다.\n\n---\n### 애니메이션 제외하기 {/*opting-out-of-an-animation*/}\n\n때로는 전체 페이지와 같은 큰 기존 컴포넌트를 래핑하고 테마 변경과 같은 일부 업데이트를 애니메이션하고 싶지만 전체 페이지 내부의 모든 업데이트가 업데이트될 때 크로스 페이드에 포함되는 것을 원하지 않을 수 있습니다. 특히 점진적으로 더 많은 애니메이션을 추가하는 경우에 그렇습니다.\n\n클래스 \"none\"을 사용하여 애니메이션을 제외할 수 있습니다. 자식을 \"none\"으로 래핑하면 부모가 여전히 발생하는 동안 자식에 대한 업데이트 애니메이션을 비활성화할 수 있습니다.\n\n```js\n<ViewTransition>\n  <div className={theme}>\n    <ViewTransition update=\"none\">{children}</ViewTransition>\n  </div>\n</ViewTransition>\n```\n\n이는 테마가 변경될 때만 애니메이션되며 자식만 업데이트될 때는 애니메이션되지 않습니다. 자식은 여전히 자체 `<ViewTransition>`으로 다시 참여할 수 있지만 최소한 다시 수동으로 제어하는 방식이 됩니다.\n\n---\n\n### 애니메이션 커스터마이징 {/*customizing-animations*/}\n\n기본적으로 `<ViewTransition>`은 브라우저의 기본 크로스 페이드를 포함합니다.\n\n애니메이션을 커스터마이징하려면 `<ViewTransition>` 컴포넌트에 props를 제공하여 `<ViewTransition>`이 활성화되는 방식에 따라 사용할 애니메이션을 지정할 수 있습니다.\n\n예를 들어 기본 크로스 페이드 애니메이션을 느리게 할 수 있습니다.\n\n```js\n<ViewTransition default=\"slow-fade\">\n  <Video />\n</ViewTransition>\n```\n\n그리고 View Transition 클래스를 사용하여 CSS에서 slow-fade를 정의합니다.\n\n```css\n::view-transition-old(.slow-fade) {\n  animation-duration: 500ms;\n}\n\n::view-transition-new(.slow-fade) {\n  animation-duration: 500ms;\n}\n```\n\n<Sandpack>\n\n```js src/Video.js hidden\nfunction Thumbnail({video, children}) {\n  return (\n    <div\n      aria-hidden=\"true\"\n      tabIndex={-1}\n      className={`thumbnail ${video.image}`}\n    />\n  );\n}\n\nexport function Video({video}) {\n  return (\n    <div className=\"video\">\n      <div className=\"link\">\n        <Thumbnail video={video}></Thumbnail>\n\n        <div className=\"info\">\n          <div className=\"video-title\">{video.title}</div>\n          <div className=\"video-description\">{video.description}</div>\n        </div>\n      </div>\n    </div>\n  );\n}\n```\n\n```js\nimport {ViewTransition, useState, startTransition} from 'react';\nimport {Video} from './Video';\nimport videos from './data';\n\nfunction Item() {\n  return (\n    <ViewTransition default=\"slow-fade\">\n      <Video video={videos[0]} />\n    </ViewTransition>\n  );\n}\n\nexport default function Component() {\n  const [showItem, setShowItem] = useState(false);\n  return (\n    <>\n      <button\n        onClick={() => {\n          startTransition(() => {\n            setShowItem((prev) => !prev);\n          });\n        }}>\n        {showItem ? '➖' : '➕'}\n      </button>\n\n      {showItem ? <Item /> : null}\n    </>\n  );\n}\n```\n\n```js src/data.js hidden\nexport default [\n  {\n    id: '1',\n    title: 'First video',\n    description: 'Video description',\n    image: 'blue',\n  },\n];\n```\n\n```css\n::view-transition-old(.slow-fade) {\n  animation-duration: 500ms;\n}\n\n::view-transition-new(.slow-fade) {\n  animation-duration: 500ms;\n}\n\n#root {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  min-height: 200px;\n}\nbutton {\n  border: none;\n  border-radius: 50%;\n  width: 50px;\n  height: 50px;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  background-color: #f0f8ff;\n  color: white;\n  font-size: 20px;\n  cursor: pointer;\n  transition: background-color 0.3s, border 0.3s;\n}\nbutton:hover {\n  border: 2px solid #ccc;\n  background-color: #e0e8ff;\n}\n.thumbnail {\n  position: relative;\n  aspect-ratio: 16 / 9;\n  display: flex;\n  overflow: hidden;\n  flex-direction: column;\n  justify-content: center;\n  align-items: center;\n  border-radius: 0.5rem;\n  outline-offset: 2px;\n  width: 8rem;\n  vertical-align: middle;\n  background-color: #ffffff;\n  background-size: cover;\n  user-select: none;\n}\n.thumbnail.blue {\n  background-image: conic-gradient(at top right, #c76a15, #087ea4, #2b3491);\n}\n.video {\n  display: flex;\n  flex-direction: row;\n  gap: 0.75rem;\n  align-items: center;\n  margin-top: 1em;\n}\n.video .link {\n  display: flex;\n  flex-direction: row;\n  flex: 1 1 0;\n  gap: 0.125rem;\n  outline-offset: 4px;\n  cursor: pointer;\n}\n.video .info {\n  display: flex;\n  flex-direction: column;\n  justify-content: center;\n  margin-left: 8px;\n  gap: 0.125rem;\n}\n.video .info:hover {\n  text-decoration: underline;\n}\n.video-title {\n  font-size: 15px;\n  line-height: 1.25;\n  font-weight: 700;\n  color: #23272f;\n}\n.video-description {\n  color: #5e687e;\n  font-size: 13px;\n}\n```\n\n```json package.json hidden\n{\n  \"dependencies\": {\n    \"react\": \"canary\",\n    \"react-dom\": \"canary\",\n    \"react-scripts\": \"latest\"\n  }\n}\n```\n\n</Sandpack>\n\n`default` 설정 외에도 `enter`, `exit`, `update`, `share` 애니메이션에 대한 구성을 제공할 수 있습니다.\n\n<Sandpack>\n\n```js src/Video.js hidden\nfunction Thumbnail({video, children}) {\n  return (\n    <div\n      aria-hidden=\"true\"\n      tabIndex={-1}\n      className={`thumbnail ${video.image}`}\n    />\n  );\n}\n\nexport function Video({video}) {\n  return (\n    <div className=\"video\">\n      <div className=\"link\">\n        <Thumbnail video={video}></Thumbnail>\n\n        <div className=\"info\">\n          <div className=\"video-title\">{video.title}</div>\n          <div className=\"video-description\">{video.description}</div>\n        </div>\n      </div>\n    </div>\n  );\n}\n```\n\n```js\nimport {ViewTransition, useState, startTransition} from 'react';\nimport {Video} from './Video';\nimport videos from './data';\n\nfunction Item() {\n  return (\n    <ViewTransition enter=\"slide-in\" exit=\"slide-out\">\n      <Video video={videos[0]} />\n    </ViewTransition>\n  );\n}\n\nexport default function Component() {\n  const [showItem, setShowItem] = useState(false);\n  return (\n    <>\n      <button\n        onClick={() => {\n          startTransition(() => {\n            setShowItem((prev) => !prev);\n          });\n        }}>\n        {showItem ? '➖' : '➕'}\n      </button>\n\n      {showItem ? <Item /> : null}\n    </>\n  );\n}\n```\n\n```js src/data.js hidden\nexport default [\n  {\n    id: '1',\n    title: 'First video',\n    description: 'Video description',\n    image: 'blue',\n  },\n];\n```\n\n```css\n::view-transition-old(.slide-in) {\n  animation-name: slideOutRight;\n  animation-duration: 500ms;\n  animation-timing-function: ease-in-out;\n}\n\n::view-transition-new(.slide-in) {\n  animation-name: slideInRight;\n  animation-duration: 500ms;\n  animation-timing-function: ease-in-out;\n}\n\n::view-transition-old(.slide-out) {\n  animation-name: slideOutLeft;\n  animation-duration: 500ms;\n  animation-timing-function: ease-in-out;\n}\n\n::view-transition-new(.slide-out) {\n  animation-name: slideInLeft;\n  animation-duration: 500ms;\n  animation-timing-function: ease-in-out;\n}\n\n@keyframes slideOutLeft {\n  from {\n    transform: translateX(0);\n    opacity: 1;\n  }\n  to {\n    transform: translateX(-100%);\n    opacity: 0;\n  }\n}\n\n@keyframes slideInLeft {\n  from {\n    transform: translateX(-100%);\n    opacity: 0;\n  }\n  to {\n    transform: translateX(0);\n    opacity: 1;\n  }\n}\n\n@keyframes slideOutRight {\n  from {\n    transform: translateX(0);\n    opacity: 1;\n  }\n  to {\n    transform: translateX(100%);\n    opacity: 0;\n  }\n}\n\n@keyframes slideInRight {\n  from {\n    transform: translateX(100%);\n    opacity: 0;\n  }\n  to {\n    transform: translateX(0);\n    opacity: 1;\n  }\n}\n\n@keyframes slideInRight {\n  from {\n    transform: translateX(100%);\n    opacity: 0;\n  }\n  to {\n    transform: translateX(0);\n    opacity: 1;\n  }\n}\n\n#root {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  min-height: 200px;\n}\nbutton {\n  border: none;\n  border-radius: 50%;\n  width: 50px;\n  height: 50px;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  background-color: #f0f8ff;\n  color: white;\n  font-size: 20px;\n  cursor: pointer;\n  transition: background-color 0.3s, border 0.3s;\n}\nbutton:hover {\n  border: 2px solid #ccc;\n  background-color: #e0e8ff;\n}\n.thumbnail {\n  position: relative;\n  aspect-ratio: 16 / 9;\n  display: flex;\n  overflow: hidden;\n  flex-direction: column;\n  justify-content: center;\n  align-items: center;\n  border-radius: 0.5rem;\n  outline-offset: 2px;\n  width: 8rem;\n  vertical-align: middle;\n  background-color: #ffffff;\n  background-size: cover;\n  user-select: none;\n}\n.thumbnail.blue {\n  background-image: conic-gradient(at top right, #c76a15, #087ea4, #2b3491);\n}\n.video {\n  display: flex;\n  flex-direction: row;\n  gap: 0.75rem;\n  align-items: center;\n  margin-top: 1em;\n}\n.video .link {\n  display: flex;\n  flex-direction: row;\n  flex: 1 1 0;\n  gap: 0.125rem;\n  outline-offset: 4px;\n  cursor: pointer;\n}\n.video .info {\n  display: flex;\n  flex-direction: column;\n  justify-content: center;\n  margin-left: 8px;\n  gap: 0.125rem;\n}\n.video .info:hover {\n  text-decoration: underline;\n}\n.video-title {\n  font-size: 15px;\n  line-height: 1.25;\n  font-weight: 700;\n  color: #23272f;\n}\n.video-description {\n  color: #5e687e;\n  font-size: 13px;\n}\n```\n\n```json package.json hidden\n{\n  \"dependencies\": {\n    \"react\": \"canary\",\n    \"react-dom\": \"canary\",\n    \"react-scripts\": \"latest\"\n  }\n}\n```\n\n</Sandpack>\n\n### 타입으로 애니메이션 커스터마이징하기 {/*customizing-animations-with-types*/}\n특정 활성화 트리거에 대해 특정 Transition 타입이 활성화될 때 자식 엘리먼트에 클래스 이름을 추가하기 위해 [`addTransitionType`](/reference/react/addTransitionType) API를 사용할 수 있습니다. 이를 통해 각 Transition 타입에 대한 애니메이션을 커스터마이징할 수 있습니다.\n\n예를 들어 모든 앞으로 및 뒤로 네비게이션에 대한 애니메이션을 커스터마이징하려면,\n\n```js\n<ViewTransition\n  default={{\n    'navigation-back': 'slide-right',\n    'navigation-forward': 'slide-left',\n  }}>\n  <div>...</div>\n</ViewTransition>\n \n// 라우터에서:\nstartTransition(() => {\n  addTransitionType('navigation-' + navigationType);\n});\n```\n\nViewTransition이 \"navigation-back\" 애니메이션을 활성화하면 React는 \"slide-right\" 클래스 이름을 추가합니다. ViewTransition이 \"navigation-forward\" 애니메이션을 활성화하면 React는 \"slide-left\" 클래스 이름을 추가합니다.\n\n향후 라우터와 다른 라이브러리들이 표준 view-transition 타입과 스타일에 대한 지원을 추가할 수 있습니다.\n\n<Sandpack>\n\n```js src/Video.js hidden\nfunction Thumbnail({video, children}) {\n  return (\n    <div\n      aria-hidden=\"true\"\n      tabIndex={-1}\n      className={`thumbnail ${video.image}`}\n    />\n  );\n}\n\nexport function Video({video}) {\n  return (\n    <div className=\"video\">\n      <div className=\"link\">\n        <Thumbnail video={video}></Thumbnail>\n        <div className=\"info\">\n          <div className=\"video-title\">{video.title}</div>\n          <div className=\"video-description\">{video.description}</div>\n        </div>\n      </div>\n    </div>\n  );\n}\n```\n\n```js\nimport {\n  ViewTransition,\n  addTransitionType,\n  useState,\n  startTransition,\n} from 'react';\nimport {Video} from './Video';\nimport videos from './data';\n\nfunction Item() {\n  return (\n    <ViewTransition\n      enter={{\n        'add-video-back': 'slide-in-back',\n        'add-video-forward': 'slide-in-forward',\n      }}\n      exit={{\n        'remove-video-back': 'slide-in-forward',\n        'remove-video-forward': 'slide-in-back',\n      }}>\n      <Video video={videos[0]} />\n    </ViewTransition>\n  );\n}\n\nexport default function Component() {\n  const [showItem, setShowItem] = useState(false);\n  return (\n    <>\n      <div className=\"button-container\">\n        <button\n          onClick={() => {\n            startTransition(() => {\n              if (showItem) {\n                addTransitionType('remove-video-back');\n              } else {\n                addTransitionType('add-video-back');\n              }\n              setShowItem((prev) => !prev);\n            });\n          }}>\n          ⬅️\n        </button>\n        <button\n          onClick={() => {\n            startTransition(() => {\n              if (showItem) {\n                addTransitionType('remove-video-forward');\n              } else {\n                addTransitionType('add-video-forward');\n              }\n              setShowItem((prev) => !prev);\n            });\n          }}>\n          ➡️\n        </button>\n      </div>\n      {showItem ? <Item /> : null}\n    </>\n  );\n}\n```\n\n```js src/data.js hidden\nexport default [\n  {\n    id: '1',\n    title: 'First video',\n    description: 'Video description',\n    image: 'blue',\n  },\n];\n```\n\n```css\n::view-transition-old(.slide-in-back) {\n  animation-name: slideOutRight;\n  animation-duration: 500ms;\n  animation-timing-function: ease-in-out;\n}\n\n::view-transition-new(.slide-in-back) {\n  animation-name: slideInRight;\n  animation-duration: 500ms;\n  animation-timing-function: ease-in-out;\n}\n\n::view-transition-old(.slide-out-back) {\n  animation-name: slideOutLeft;\n  animation-duration: 500ms;\n  animation-timing-function: ease-in-out;\n}\n\n::view-transition-new(.slide-out-back) {\n  animation-name: slideInLeft;\n  animation-duration: 500ms;\n  animation-timing-function: ease-in-out;\n}\n\n::view-transition-old(.slide-in-forward) {\n  animation-name: slideOutLeft;\n  animation-duration: 500ms;\n  animation-timing-function: ease-in-out;\n}\n\n::view-transition-new(.slide-in-forward) {\n  animation-name: slideInLeft;\n  animation-duration: 500ms;\n  animation-timing-function: ease-in-out;\n}\n\n::view-transition-old(.slide-out-forward) {\n  animation-name: slideOutRight;\n  animation-duration: 500ms;\n  animation-timing-function: ease-in-out;\n}\n\n::view-transition-new(.slide-out-forward) {\n  animation-name: slideInRight;\n  animation-duration: 500ms;\n  animation-timing-function: ease-in-out;\n}\n\n@keyframes slideOutLeft {\n  from {\n    transform: translateX(0);\n    opacity: 1;\n  }\n  to {\n    transform: translateX(-100%);\n    opacity: 0;\n  }\n}\n\n@keyframes slideInLeft {\n  from {\n    transform: translateX(-100%);\n    opacity: 0;\n  }\n  to {\n    transform: translateX(0);\n    opacity: 1;\n  }\n}\n\n@keyframes slideOutRight {\n  from {\n    transform: translateX(0);\n    opacity: 1;\n  }\n  to {\n    transform: translateX(100%);\n    opacity: 0;\n  }\n}\n\n@keyframes slideInRight {\n  from {\n    transform: translateX(100%);\n    opacity: 0;\n  }\n  to {\n    transform: translateX(0);\n    opacity: 1;\n  }\n}\n\n@keyframes slideInRight {\n  from {\n    transform: translateX(100%);\n    opacity: 0;\n  }\n  to {\n    transform: translateX(0);\n    opacity: 1;\n  }\n}\n\n#root {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  min-height: 200px;\n}\nbutton {\n  border: none;\n  border-radius: 50%;\n  width: 50px;\n  height: 50px;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  background-color: #f0f8ff;\n  color: white;\n  font-size: 20px;\n  cursor: pointer;\n  transition: background-color 0.3s, border 0.3s;\n}\nbutton:hover {\n  border: 2px solid #ccc;\n  background-color: #e0e8ff;\n}\n.button-container {\n  display: flex;\n}\n.thumbnail {\n  position: relative;\n  aspect-ratio: 16 / 9;\n  display: flex;\n  overflow: hidden;\n  flex-direction: column;\n  justify-content: center;\n  align-items: center;\n  border-radius: 0.5rem;\n  outline-offset: 2px;\n  width: 8rem;\n  vertical-align: middle;\n  background-color: #ffffff;\n  background-size: cover;\n  user-select: none;\n}\n.thumbnail.blue {\n  background-image: conic-gradient(at top right, #c76a15, #087ea4, #2b3491);\n}\n.video {\n  display: flex;\n  flex-direction: row;\n  gap: 0.75rem;\n  align-items: center;\n  margin-top: 1em;\n}\n.video .link {\n  display: flex;\n  flex-direction: row;\n  flex: 1 1 0;\n  gap: 0.125rem;\n  outline-offset: 4px;\n  cursor: pointer;\n}\n.video .info {\n  display: flex;\n  flex-direction: column;\n  justify-content: center;\n  margin-left: 8px;\n  gap: 0.125rem;\n}\n.video .info:hover {\n  text-decoration: underline;\n}\n.video-title {\n  font-size: 15px;\n  line-height: 1.25;\n  font-weight: 700;\n  color: #23272f;\n}\n.video-description {\n  color: #5e687e;\n  font-size: 13px;\n}\n```\n\n```json package.json hidden\n{\n  \"dependencies\": {\n    \"react\": \"canary\",\n    \"react-dom\": \"canary\",\n    \"react-scripts\": \"latest\"\n  }\n}\n```\n\n</Sandpack>\n\n### View Transition 지원 라우터 구축하기 {/*building-view-transition-enabled-routers*/}\n\n스크롤 복원이 애니메이션 중에 정상적으로 동작하도록, React는 대기 중인 내비게이션이 완료될 때까지 기다립니다. 네비게이션이 React에서 차단되는 경우 `useEffect`는 교착 상태로 이어질 수 있으므로 라우터는 `useLayoutEffect`에서 차단을 해제해야 합니다.\n\n\"뒤로\" 네비게이션 중처럼 레거시 popstate 이벤트에서 `startTransition`이 시작되면 스크롤과 폼 복원이 올바르게 작동하도록 동기적으로 완료되어야 합니다. 이는 View Transition 애니메이션 실행과 충돌합니다. 따라서 React는 popstate에서 애니메이션을 건너뜁니다. 따라서 뒤로 버튼에 대해서는 애니메이션이 실행되지 않습니다. Navigation API를 사용하도록 라우터를 업그레이드하여 이를 해결할 수 있습니다.\n\n---\n\n## 문제 해결 {/*troubleshooting*/}\n\n### `<ViewTransition>`이 활성화되지 않습니다 {/*my-viewtransition-is-not-activating*/}\n\n`<ViewTransition>` only activates if it is placed before any DOM node:\n\n```js [3, 5]\nfunction Component() {\n  return (\n    <div>\n      <ViewTransition>Hi</ViewTransition>\n    </div>\n  );\n}\n```\n\n해결하려면 `<ViewTransition>`이 다른 DOM 노드보다 앞에 오도록 하세요.\n\n```js [3, 5]\nfunction Component() {\n  return (\n    <ViewTransition>\n      <div>Hi</div>\n    </ViewTransition>\n  );\n}\n```\n\n### \"동일한 이름으로 마운트된 `<ViewTransition name=%s>` 컴포넌트가 두 개 있습니다.\"라는 오류가 발생합니다 {/*two-viewtransition-with-same-name*/}\n\n이 오류는 동일한 `name`을 가진 두 개의 `<ViewTransition>` 컴포넌트가 동시에 마운트될 때 발생합니다.\n\n```js [3]\nfunction Item() {\n  // 🚩 모든 항목이 동일한 \"name\"을 갖게 됩니다.\n  return <ViewTransition name=\"item\">...</ViewTransition>;\n}\n\nfunction ItemList({items}) {\n  return (\n    <>\n      {items.map((item) => (\n        <Item key={item.id} />\n      ))}\n    </>\n  );\n}\n```\n\n이는 View Transition에서 오류를 발생시킵니다. 개발 중에 React는 이 문제를 감지하여 표면화하고 두 개의 오류를 기록합니다.\n\n<ConsoleBlockMulti>\n<ConsoleLogLine level=\"error\">\n\nThere are two `<ViewTransition name=%s>` components with the same name mounted at the same time. This is not supported and will cause View Transitions to error. Try to use a more unique name e.g. by using a namespace prefix and adding the id of an item to the name.\n{' '}at Item\n{' '}at ItemList\n\n</ConsoleLogLine>\n\n<ConsoleLogLine level=\"error\">\n\nThe existing `<ViewTransition name=%s>` duplicate has this stack trace.\n{' '}at Item\n{' '}at ItemList\n\n</ConsoleLogLine>\n</ConsoleBlockMulti>\n\n해결하려면 `name`이 고유하도록 하거나 이름에 `id`를 추가하여 전체 앱에서 동일한 이름을 가진 `<ViewTransition>`이 한 번에 하나만 마운트되도록 하세요.\n\n```js [3]\nfunction Item({id}) {\n  // ✅ 모든 항목이 고유한 \"name\"을 갖게 됩니다.\n  return <ViewTransition name={`item-${id}`}>...</ViewTransition>;\n}\n\nfunction ItemList({items}) {\n  return (\n    <>\n      {items.map((item) => (\n        <Item key={item.id} item={item} />\n      ))}\n    </>\n  );\n}\n```\n"
  },
  {
    "path": "src/content/reference/react/act.md",
    "content": "---\ntitle: act\n---\n\n<Intro>\n\n`act`는 테스트 헬퍼<sup>Helper</sup>로, 대기 중인 React 업데이트를 모두 적용한 뒤 단언<sup>Assert</sup>할 수 있게 도움을 줍니다.\n\n```js\nawait act(async actFn)\n```\n\n</Intro>\n\n컴포넌트를 단언<sup>Assertion</sup>할 수 있도록 준비하려면 `await act()` 호출 안에 컴포넌트를 렌더링하고 업데이트하는 코드를 감싸세요. 이렇게 하면 테스트가 브라우저에서 작동하는 실제 React 방식과 더 유사하게 실행됩니다.\n\n<Note>\n`act()`를 직접 사용하는 것이 다소 장황하다고 느껴질 수 있습니다. 반복되는 코드를 줄이고 싶다면 [React Testing Library](https://testing-library.com/docs/react-testing-library/intro)처럼 내부적으로 `act()`로 감싼 헬퍼를 제공하는 라이브러리를 사용하는 것도 좋습니다.\n</Note>\n\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `await act(async actFn)` {/*await-act-async-actfn*/}\n\nUI 테스트를 작성할 때 렌더링, 사용자 이벤트, 데이터 가져오기 등은 사용자 인터페이스와의 상호작용 \"단위\"로 볼 수 있습니다. React는 `act()`라는 헬퍼를 제공하는데 이는 이 \"단위\"와 관련된 모든 업데이트가 DOM에 적용되기 전까지 단언이 실행되지 않도록 보장해 줍니다.\n\n`act` 라는 이름은 [Arrange-Act-Assert](https://wiki.c2.com/?ArrangeActAssert) 패턴에서 따온 것입니다.\n\n```js {2,4}\nit ('renders with button disabled', async () => {\n  await act(async () => {\n    root.render(<TestComponent />)\n  });\n  expect(container.querySelector('button')).toBeDisabled();\n});\n```\n\n<Note>\n\n`act`는 `await`와 `async` 함수와 함께 사용하는 것을 권장합니다. 동기 버전도 대부분의 경우 동작하지만 React가 내부적으로 업데이트를 예약하는 방식 때문에 언제 동기 버전을 써도 되는지 예측하기 어렵습니다.\n\n앞으로 동기 버전은 더 이상 사용되지 않을 예정이며 제거될 예정입니다.\n\n</Note>\n\n#### 매개변수 {/*parameters*/}\n\n* `async actFn`: 테스트할 컴포넌트를 렌더링하거나 상호작용을 수행하는 비동기 함수입니다. `actFn` 내부에서 발생하는 업데이트는 내부 act 큐에 추가되며 모두 모아서 DOM에 적용됩니다. 비동기 함수이기 때문에 React는 비동기 경계를 넘는 코드도 실행하고 예약된 업데이트도 함께 처리합니다.\n\n#### 반환값 {/*returns*/}\n\n`act`는 아무 값도 반환하지 않습니다.\n\n## 사용법 {/*usage*/}\n\n컴포넌트를 테스트할 때 `act`를 사용하면 출력 결과에 대한 단언을 더 안전하게 할 수 있습니다.\n\n예시로 `Counter`라는 컴포넌트가 있다고 가정하고 아래 사용 예시는 이를 테스트하는 방법을 보여줍니다.\n\n```js\nfunction Counter() {\n  const [count, setCount] = useState(0);\n  const handleClick = () => {\n    setCount(prev => prev + 1);\n  }\n\n  useEffect(() => {\n    document.title = `You clicked ${count} times`;\n  }, [count]);\n\n  return (\n    <div>\n      <p>You clicked {count} times</p>\n      <button onClick={handleClick}>\n        Click me\n      </button>\n    </div>\n  )\n}\n```\n\n### 테스트에서 컴포넌트를 렌더링하는 방법 {/*rendering-components-in-tests*/}\n\n컴포넌트의 렌더링 결과를 테스트하려면 렌더링 코드를 `act()`로 감싸야 합니다.\n\n```js  {10,12}\nimport {act} from 'react';\nimport ReactDOMClient from 'react-dom/client';\nimport Counter from './Counter';\n\nit('can render and update a counter', async () => {\n  container = document.createElement('div');\n  document.body.appendChild(container);\n\n  // ✅ 컴포넌트를 act() 안에서 렌더링합니다.\n  await act(() => {\n    ReactDOMClient.createRoot(container).render(<Counter />);\n  });\n\n  const button = container.querySelector('button');\n  const label = container.querySelector('p');\n  expect(label.textContent).toBe('You clicked 0 times');\n  expect(document.title).toBe('You clicked 0 times');\n});\n```\n\n위 예시에서는 컨테이너를 만들고 문서에 추가한 뒤 `Counter` 컴포넌트를 `act()` 안에서 렌더링합니다. 이렇게 하면 컴포넌트가 렌더링되고 효과가 적용된 후에 단언을 수행할 수 있습니다.\n\n`act`를 사용하면 모든 업데이트가 적용된 뒤 단언을 실행할 수 있습니다.\n\n### 테스트에서 이벤트 디스패칭하는 방법 {/*dispatching-events-in-tests*/}\n\n이벤트를 테스트하려면 이벤트를 `act()`로 감싸세요.\n\n```js {14,16}\nimport {act} from 'react';\nimport ReactDOMClient from 'react-dom/client';\nimport Counter from './Counter';\n\nit.only('can render and update a counter', async () => {\n  const container = document.createElement('div');\n  document.body.appendChild(container);\n\n  await act( async () => {\n    ReactDOMClient.createRoot(container).render(<Counter />);\n  });\n\n  // ✅ 이벤트 디스패치를 act() 안에서 실행합니다.\n  await act(async () => {\n    button.dispatchEvent(new MouseEvent('click', { bubbles: true }));\n  });\n\n  const button = container.querySelector('button');\n  const label = container.querySelector('p');\n  expect(label.textContent).toBe('You clicked 1 times');\n  expect(document.title).toBe('You clicked 1 times');\n});\n```\n\n위 예시에서는 컴포넌트를 먼저 `act`로 감싸 렌더링하고, 이벤트 디스패치도 `act()`로 감쌉니다. 이렇게 하면 해당 이벤트로 인한 모든 업데이트가 적용된 뒤 단언이 수행됩니다.\n\n<Pitfall>\n\nDOM 이벤트를 디스패치할 때는 DOM 컨테이너가 문서에 추가되어 있어야 합니다. 반복되는 설정 코드를 줄이고 싶다면 [React Testing Library](https://testing-library.com/docs/react-testing-library/intro)를 사용하는 것도 고려해보세요.\n\n</Pitfall>\n\n## 문제 해결 {/*troubleshooting*/}\n\n### \"The current testing environment is not configured to support act(...)\" 오류가 발생하는 경우 {/*error-the-current-testing-environment-is-not-configured-to-support-act*/}\n\n`act`를 사용하려면 테스트 환경에서 `global.IS_REACT_ACT_ENVIRONMENT=true`를 설정해야 합니다. 이 설정은 act가 올바른 환경에서만 사용되도록 보장합니다.\n\n이 전역 설정이 없으면 다음과 같은 오류가 표시됩니다.\n\n<ConsoleBlock level=\"error\">\n\nWarning: The current testing environment is not configured to support act(...)\n\n</ConsoleBlock>\n\n이 문제를 해결하려면 React 테스트를 위한 전역 설정 파일에 다음 코드를 추가하세요.\n\n```js\nglobal.IS_REACT_ACT_ENVIRONMENT=true\n```\n\n<Note>\n\n[React Testing Library](https://testing-library.com/docs/react-testing-library/intro)같은 테스트 프레임워크에서는 `IS_REACT_ACT_ENVIRONMENT`가 이미 설정되어 있습니다.\n\n</Note>\n"
  },
  {
    "path": "src/content/reference/react/addTransitionType.md",
    "content": "---\ntitle: addTransitionType\nversion: canary\n---\n\n<Canary>\n\n**`addTransitionType` API는 현재 React의 카나리 및 실험적 채널에서만 사용할 수 있습니다.** \n\n[React의 배포 채널에 대해 더 알아보세요.](/community/versioning-policy#all-release-channels)\n\n</Canary>\n\n<Intro>\n\n`addTransitionType`은 트랜지션의 원인을 명시할 수 있습니다.\n\n\n```js\nstartTransition(() => {\n  addTransitionType('my-transition-type');\n  setState(newState);\n});\n```\n\n</Intro>\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `addTransitionType` {/*addtransitiontype*/}\n\n#### 매개변수 {/*parameters*/}\n\n- `type`: 추가할 트랜지션의 타입입니다. 어떤 문자열이든 될 수 있습니다.\n\n#### 반환값 {/*returns*/}\n\n`startTransition`은 아무것도 반환하지 않습니다.\n\n#### 주의 사항 {/*caveats*/}\n\n- 여러 트랜지션이 결합되면 모든 트랜지션 타입이 수집됩니다. 하나의 트랜지션에 두 개 이상의 타입을 추가할 수도 있습니다.\n- 트랜지션 타입은 커밋마다 초기화됩니다. 즉, `<Suspense>`의 Fallback은 `startTransition` 이후 타입을 연결하며, 내용이 나타날 때는 그렇지 않습니다.\n\n---\n\n## 사용법 {/*usage*/}\n\n### 트랜지션의 원인 추가하기 {/*adding-the-cause-of-a-transition*/}\n\n트랜지션의 원인을 나타내기 위해 `startTransition` 내부에서 `addTransitionType`을 호출합니다\n\n``` [[1, 6, \"addTransitionType\"], [2, 5, \"startTransition\", [3, 6, \"'submit-click'\"]]\nimport { startTransition, addTransitionType } from 'react';\n\nfunction Submit({action) {\n  function handleClick() {\n    startTransition(() => {\n      addTransitionType('submit-click');\n      action();\n    });\n  }\n\n  return <button onClick={handleClick}>Click me</button>;\n}\n\n```\n\n<CodeStep step={1}>addTransitionType</CodeStep>을 <CodeStep step={2}>startTransition</CodeStep>의 범위 내에서 호출하면, React는 해당 트랜지션에 <CodeStep step={3}>submit-click</CodeStep>을 원인으로 연결합니다.\n\n현재 트랜지션 타입은 원인에 따라 서로 다른 애니메이션을 커스터마이즈하는 데 사용할 수 있습니다. 사용할 수 있는 방식은 세 가지입니다.\n\n- [브라우저 View Transition 타입으로 애니메이션 커스텀하기](#customize-animations-using-browser-view-transition-types)\n- [`View Transition` 클래스로 애니메이션 커스텀하기](#customize-animations-using-view-transition-class)\n- [`ViewTransition`이벤트로 애니메이션 커스텀하기](#customize-animations-using-viewtransition-events) \n\n향후에는 트랜지션의 원인을 활용할 수 있는 다양한 용례를 지원할 예정입니다.\n\n---\n### 브라우저 View Transition 타입으로 애니메이션 커스텀하기 {/*customize-animations-using-browser-view-transition-types*/}\n\n트랜지션에서 [`ViewTransition`](/reference/react/ViewTransition)이 활성화되면, React는 모든 트랜지션 타입을 브라우저의 [View Transition Types](https://www.w3.org/TR/css-view-transitions-2/#active-view-transition-pseudo-examples)으로 해당 요소에 추가합니다.\n\n이렇게 하면 CSS 범위에서 다른 애니메이션을 커스텀할 수 있습니다.\n\n```js [11]\nfunction Component() {\n  return (\n    <ViewTransition>\n      <div>Hello</div>\n    </ViewTransition>\n  );\n}\n\nstartTransition(() => {\n  addTransitionType('my-transition-type');\n  setShow(true);\n});\n```\n\n```css\n:root:active-view-transition-type(my-transition-type) {\n  &::view-transition-...(...) {\n    ...\n  }\n}\n```\n\n---\n\n### `View Transition` 클래스로 애니메이션 커스텀하기 {/*customize-animations-using-view-transition-class*/}\n\n활성화된 `ViewTransition`에서 타입에 따라 애니메이션을 커스터마이즈하려면, View Transition 클래스에 객체를 전달하면 됩니다.\n\n```js\nfunction Component() {\n  return (\n    <ViewTransition enter={{\n      'my-transition-type': 'my-transition-class',\n    }}>\n      <div>Hello</div>\n    </ViewTransition>\n  );\n}\n\n// ...\nstartTransition(() => {\n  addTransitionType('my-transition-type');\n  setState(newState);\n});\n```\n\n여러 타입이 매칭되면 값들이 결합됩니다. 매칭되는 타입이 없으면 \"default\" 엔트리가 사용됩니다. 어떤 타입이라도 값이 \"none\"이면 해당 값이 우선하며 `ViewTransition`은 비활성화됩니다. (이름이 할당되지 않습니다).\n\n이 방식은 enter/exit/update/layout/share Props와 결합하여 트리거 종류와 트랜지션 타입에 따라 동작을 맞출 수 있습니다.\n\n```js\n<ViewTransition enter={{\n  'navigation-back': 'enter-right',\n  'navigation-forward': 'enter-left',\n}}\nexit={{\n  'navigation-back': 'exit-right',\n  'navigation-forward': 'exit-left',\n}}>\n```\n\n---\n\n### `ViewTransition` 이벤트로 애니메이션 커스텀하기 {/*customize-animations-using-viewtransition-events*/}\n\nView Transition 이벤트를 활용하여 타입에 따라 활성화된 `ViewTransition`의 애니메이션을 즉시 커스터마이즈할 수 있습니다.\n\n```\n<ViewTransition onUpdate={(inst, types) => {\n  if (types.includes('navigation-back')) {\n    ...\n  } else if (types.includes('navigation-forward')) {\n    ...\n  } else {\n    ...\n  }\n}}>\n```\n\n이를 통해 원인에 따라 다른 명령형 애니메이션을 선택할 수 있습니다.\n"
  },
  {
    "path": "src/content/reference/react/apis.md",
    "content": "---\ntitle: \"내장 React API\"\n---\n\n<Intro>\n\n[Hook](/reference/react/hooks)과 [컴포넌트](/reference/react/components) 외에도 `react` 패키지는 컴포넌트를 정의하는데 유용한 몇 가지 API를 가지고 있습니다. 이 페이지는 최신 React API를 모두 나열합니다.\n\n</Intro>\n{/*React 영문 공식 문서에 반영되지 않은 내용 임의로 수정하여 반영하였습니다. `cache` 및 `use`에 대한 내용 설명을 제외하고 수정하지 말아주세요*/}\n---\n* [`act`](/reference/react/act)를 통해 테스트에서 렌더링이나 상호작용을 감싸서 관련된 업데이트가 모두 처리된 뒤에 검증합니다.\n* [`cache`](/reference/react/cache)를 통해 가져온 데이터나 연산의 결과를 캐싱합니다.\n* [`captureOwnerStack`](/reference/react/captureOwnerStack)을 통해 개발 환경에서 현재 Owner Stack을 읽고, 사용가능한 문자열을 반환합니다.\n* [`createContext`](/reference/react/createContext)를 통해 자식 컴포넌트들에게 전달할 수 있는 컨텍스트를 정의하고 제공합니다. 보통 [`useContext`](/reference/react/useContext)와 함께 사용합니다.\n* [`lazy`](/reference/react/lazy)를 통해 컴포넌트가 처음 렌더링될 때까지 해당 컴포넌트의 코드를 로딩하는 것을 지연합니다.\n* [`memo`](/reference/react/memo)를 통해 동일한 Props일 경우 컴포넌트가 다시 렌더링되지 않도록 최적화합니다. 주로 [`useMemo`](/reference/react/useMemo), [`useCallback`](/reference/react/useCallback)과 함께 사용합니다.\n* [`startTransition`](/reference/react/startTransition)을 통해 상태 업데이트를 \"덜 긴급한 작업\"으로 표시하여 UI의 반응성을 유지합니다. [`useTransition`](/reference/react/useTransition)과 유사합니다.\n* [`use`](/reference/react/use)는 Promise나 Context와 같은 데이터를 참조하는 React Hook입니다.\n* [`taintObjectReference`](/reference/react/experimental_taintObjectReference)를 통해 `user` 객체와 같은 특정한 객체 인스턴스를 클라이언트 컴포넌트로 전송하는 것을 방지합니다.\n* [`taintUniqueValue`](/reference/react/experimental_taintUniqueValue)를 통해 패스워드, 키 또는 토큰과 같은 고유 값을 클라이언트 컴포넌트로 전송하는 것을 방지합니다.\n* [`addTransitionType`](/reference/react/addTransitionType)를 통해, 트랜지션이 발생한 원인을 상세히 나타냅니다.\n---\n\n## Resource APIs {/*resource-apis*/}\n\n*Resource*를 State의 일부로 포함하지 않고도 컴포넌트에서 Resource에 액세스할 수 있습니다. 예를 들어, 컴포넌트는 Promise에서 메시지를 읽거나 Context에서 스타일 정보를 읽을 수 있습니다.\n\nResource에서 값을 읽으려면 다음 API를 사용하세요.\n\n- [`use`](/reference/react/use)를 사용하면 [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)나 [Context](/learn/passing-data-deeply-with-context)와 같은 Resource의 값을 읽을 수 있습니다.\n```js\nfunction MessageComponent({ messagePromise }) {\n  const message = use(messagePromise);\n  const theme = use(ThemeContext);\n  // ...\n}\n```\n"
  },
  {
    "path": "src/content/reference/react/cache.md",
    "content": "---\ntitle: cache\n---\n\n<RSC>\n\n`cache` is only for use with [React Server Components](/reference/rsc/server-components).\n\n</RSC>\n\n<Intro>\n\n`cache`를 통해 가져온 데이터나 연산의 결과를 캐싱합니다.\n\n```js\nconst cachedFn = cache(fn);\n```\n\n</Intro>\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `cache(fn)` {/*cache*/}\n\n컴포넌트 외부에서 `cache`를 호출해 캐싱 기능을 가진 함수의 한 버전을 만들 수 있습니다.\n\n```js {4,7}\nimport {cache} from 'react';\nimport calculateMetrics from 'lib/metrics';\n\nconst getMetrics = cache(calculateMetrics);\n\nfunction Chart({data}) {\n  const report = getMetrics(data);\n  // ...\n}\n```\n`getMetrics`가 처음 `data`를 호출할 때, `getMetrics`는 `calculateMetrics(data)`를 호출하고 캐시에 결과를 저장합니다. `getMetrics`가 같은 `data`와 함께 다시 호출되면, `calculateMetrics(data)`를 다시 호출하는 대신에 캐싱된 결과를 반환합니다.\n\n[아래 예시를 참고하세요.](#usage)\n\n#### 매개변수 {/*parameters*/}\n\n- `fn`: 결과를 저장하고 싶은 함수. `fn`은 어떤 인수도 받을 수 있고 어떠한 결과도 반환할 수 있습니다.\n\n#### 반환값 {/*returns*/}\n\n`cache`는 같은 타입 시그니처를 가진 `fn`의 캐싱된 버전을 반환합니다. 이 과정에서 `fn`을 호출하지 않습니다.\n\n주어진 인수와 함께 `cachedFn`을 호출할 때, 캐시에 캐싱된 데이터가 있는지 먼저 확인합니다. 만약 캐싱된 데이터가 있다면, 그 결과를 반환합니다. 만약 없다면, 매개변수와 함께 `fn`을 호출하고 결과를 캐시에 저장하고 값을 반환합니다. `fn`이 유일하게 호출되는 경우는 캐싱된 데이터가 없는 경우입니다.\n\n<Note>\n\n입력을 기반으로 반환 값 캐싱을 최적화하는 것을 [_메모이제이션_](https://ko.wikipedia.org/wiki/%EB%A9%94%EB%AA%A8%EC%9D%B4%EC%A0%9C%EC%9D%B4%EC%85%98)이라고 합니다. `cache`에서 반환되는 함수를 메모화된 함수라고 합니다.\n\n</Note>\n\n#### 주의 사항 {/*caveats*/}\n\n- React will invalidate the cache for all memoized functions for each server request.\n- Each call to `cache` creates a new function. This means that calling `cache` with the same function multiple times will return different memoized functions that do not share the same cache.\n- `cachedFn` will also cache errors. If `fn` throws an error for certain arguments, it will be cached, and the same error is re-thrown when `cachedFn` is called with those same arguments.\n- `cache` is for use in [Server Components](/reference/rsc/server-components) only.\n\n---\n\n## 사용법 {/*usage*/}\n\n### 고비용 연산 캐싱하기 {/*cache-expensive-computation*/}\n\n반복 작업을 피하기 위해 `cache`를 사용하세요.\n\n```js [[1, 7, \"getUserMetrics(user)\"],[2, 13, \"getUserMetrics(user)\"]]\nimport {cache} from 'react';\nimport calculateUserMetrics from 'lib/user';\n\nconst getUserMetrics = cache(calculateUserMetrics);\n\nfunction Profile({user}) {\n  const metrics = getUserMetrics(user);\n  // ...\n}\n\nfunction TeamReport({users}) {\n  for (let user in users) {\n    const metrics = getUserMetrics(user);\n    // ...\n  }\n  // ...\n}\n```\n같은 `user` 객체가 `Profile`과 `TeamReport`에서 렌더링될 때, 두 컴포넌트는 작업을 공유하고, `user`를 위한 `calculateUserMetrics`를 한 번만 호출합니다.\n\nIf the same `user` object is rendered in both `Profile` and `TeamReport`, the two components can share work and only call `calculateUserMetrics` once for that `user`.\n\nAssume `Profile` is rendered first. It will call <CodeStep step={1}>`getUserMetrics`</CodeStep>, and check if there is a cached result. Since it is the first time `getUserMetrics` is called with that `user`, there will be a cache miss. `getUserMetrics` will then call `calculateUserMetrics` with that `user` and write the result to cache.\n\nWhen `TeamReport` renders its list of `users` and reaches the same `user` object, it will call <CodeStep step={2}>`getUserMetrics`</CodeStep> and read the result from cache.\n\nIf `calculateUserMetrics` can be aborted by passing an [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal), you can use [`cacheSignal()`](/reference/react/cacheSignal) to cancel the expensive computation if React has finished rendering. `calculateUserMetrics` may already handle cancellation internally by using `cacheSignal` directly.\n\n<Pitfall>\n\n##### 다른 메모화된 함수를 호출하면 다른 캐시에서 읽습니다. {/*pitfall-different-memoized-functions*/}\n\n같은 캐시에 접근하기 위해선, 컴포넌트는 반드시 같은 메모화된 함수를 호출해야 합니다.\n\n```js [[1, 7, \"getWeekReport\"], [1, 7, \"cache(calculateWeekReport)\"], [1, 8, \"getWeekReport\"]]\n// Temperature.js\nimport {cache} from 'react';\nimport {calculateWeekReport} from './report';\n\nexport function Temperature({cityData}) {\n  // 🚩 Wrong: 컴포넌트에서 `cache`를 호출하면 각 렌더링에 대해 `getWeekReport`가 생성됩니다.\n  const getWeekReport = cache(calculateWeekReport);\n  const report = getWeekReport(cityData);\n  // ...\n}\n```\n\n```js [[2, 6, \"getWeekReport\"], [2, 6, \"cache(calculateWeekReport)\"], [2, 9, \"getWeekReport\"]]\n// Precipitation.js\nimport {cache} from 'react';\nimport {calculateWeekReport} from './report';\n\n// 🚩 Wrong: `getWeekReport`는 `Precipitation` 컴포넌트에서만 적용할 수 있습니다.\nconst getWeekReport = cache(calculateWeekReport);\n\nexport function Precipitation({cityData}) {\n  const report = getWeekReport(cityData);\n  // ...\n}\n```\n\n위의 예시에서, <CodeStep step={2}>`Precipitation`</CodeStep>와 <CodeStep step={1}>`Temperature`</CodeStep>는 각각 `cache`를 호출하여 자체 캐시 조회를 통해 새로운 메모화된 함수를 만들어 냅니다. 두 컴포넌트가 같은 `cityData`를 렌더링한다면, `calculateWeekReport`를 호출하는 반복 작업을 하게 됩니다.\n\n게다가, `Temperature`는 컴포넌트가 렌더링될 때마다 어떤 캐시 공유도 허용하지 않는 <CodeStep step={1}>새로운 메모화된 함수</CodeStep>를 생성하게 됩니다.\n\n캐시 사용을 늘리고 작업을 줄이기 위해서 두 컴포넌트는 같은 캐시에 접근하는 같은 메모화된 함수를 호출해야 합니다. 대신, 컴포넌트끼리 [`import` 할 수 있는](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import) 전용 모듈에 메모화된 함수를 정의하세요.\n\n```js [[3, 5, \"export default cache(calculateWeekReport)\"]]\n// getWeekReport.js\nimport {cache} from 'react';\nimport {calculateWeekReport} from './report';\n\nexport default cache(calculateWeekReport);\n```\n\n```js [[3, 2, \"getWeekReport\", 0], [3, 5, \"getWeekReport\"]]\n// Temperature.js\nimport getWeekReport from './getWeekReport';\n\nexport default function Temperature({cityData}) {\n  const report = getWeekReport(cityData);\n  // ...\n}\n```\n\n```js [[3, 2, \"getWeekReport\", 0], [3, 5, \"getWeekReport\"]]\n// Precipitation.js\nimport getWeekReport from './getWeekReport';\n\nexport default function Precipitation({cityData}) {\n  const report = getWeekReport(cityData);\n  // ...\n}\n```\nHere, both components call the <CodeStep step={3}>same memoized function</CodeStep> exported from `./getWeekReport.js` to read and write to the same cache.\n</Pitfall>\n\n### 데이터의 스냅샷 공유하기 {/*take-and-share-snapshot-of-data*/}\n\nTo share a snapshot of data between components, call `cache` with a data-fetching function like `fetch`. When multiple components make the same data fetch, only one request is made and the data returned is cached and shared across components. All components refer to the same snapshot of data across the server render.\n\n```js [[1, 4, \"city\"], [1, 5, \"fetchTemperature(city)\"], [2, 4, \"getTemperature\"], [2, 9, \"getTemperature\"], [1, 9, \"city\"], [2, 14, \"getTemperature\"], [1, 14, \"city\"]]\nimport {cache} from 'react';\nimport {fetchTemperature} from './api.js';\n\nconst getTemperature = cache(async (city) => {\n  return await fetchTemperature(city);\n});\n\nasync function AnimatedWeatherCard({city}) {\n  const temperature = await getTemperature(city);\n  // ...\n}\n\nasync function MinimalWeatherCard({city}) {\n  const temperature = await getTemperature(city);\n  // ...\n}\n```\n\nIf `AnimatedWeatherCard` and `MinimalWeatherCard` both render for the same <CodeStep step={1}>city</CodeStep>, they will receive the same snapshot of data from the <CodeStep step={2}>memoized function</CodeStep>.\n\n`AnimatedWeatherCard`와 `MinimalWeatherCard`가 다른 <CodeStep step={1}>city</CodeStep>를 <CodeStep step={2}>`getTemperature`</CodeStep>의 인수로 받게 된다면, `fetchTemperature`는 두 번 호출되고 호출마다 다른 데이터를 받게됩니다.\n\n<CodeStep step={1}>city</CodeStep>가 캐시 키<sup>Key</sup>처럼 동작하게 됩니다.\n\n<Note>\n\n<CodeStep step={3}>Asynchronous rendering</CodeStep> is only supported for Server Components.\n\n```js [[3, 1, \"async\"], [3, 2, \"await\"]]\nasync function AnimatedWeatherCard({city}) {\n  const temperature = await getTemperature(city);\n  // ...\n}\n```\n\nTo render components that use asynchronous data in Client Components, see [`use()` documentation](/reference/react/use).\n\n</Note>\n\n### 사전에 데이터 받아두기 {/*preload-data*/}\n\n긴 실행 시간이 소요되는 데이터 가져오기를 캐싱하면, 컴포넌트를 렌더링하기 전에 비동기 작업을 시작할 수 있습니다.\n\n```jsx [[2, 6, \"await getUser(id)\"], [1, 17, \"getUser(id)\"]]\nconst getUser = cache(async (id) => {\n  return await db.user.query(id);\n});\n\nasync function Profile({id}) {\n  const user = await getUser(id);\n  return (\n    <section>\n      <img src={user.profilePic} />\n      <h2>{user.name}</h2>\n    </section>\n  );\n}\n\nfunction Page({id}) {\n  // ✅ Good: 사용자 데이터 가져오기를 시작합니다.\n  getUser(id);\n  // ... 몇몇의 계산 작업들\n  return (\n    <>\n      <Profile id={id} />\n    </>\n  );\n}\n```\n\n`Page`를 렌더링할 때, 컴포넌트는 <CodeStep step={1}>`getUser`</CodeStep>를 호출하지만, 반환된 데이터를 사용하지 않는다는 점에 유의하세요. 이 초기 <CodeStep step={1}>`getUser`</CodeStep> 호출은 페이지가 다른 계산 작업을 수행하고 자식을 렌더링하는 동안 발생하는, 비동기 데이터베이스 쿼리를 시작합니다.\n\n`Profile`을 렌더링할 때, <CodeStep step={2}>`getUser`</CodeStep>를 다시 호출합니다. 초기 <CodeStep step={1}>`getUser`</CodeStep> 호출이 이미 사용자 데이터에 반환되고 캐싱되었다면, `Profile`이 <CodeStep step={2}>해당 데이터를 요청하고 기다릴 때</CodeStep>, 다른 원격 프로시저 호출 없이 쉽게 캐시에서 읽어올 수 있습니다. <CodeStep step={1}> 초기 데이터 요청</CodeStep>이 완료되지 않은 경우, 이 패턴으로 데이터를 미리 로드하면 데이터를 받아올 때 생기는 지연이 줄어듭니다.\n\n<DeepDive>\n\n#### 비동기 작업 캐싱하기 {/*caching-asynchronous-work*/}\n\n[비동기 함수](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Statements/async_function)의 결과를 보면, [Promise](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Promise)를 받습니다. 이 Promise는 작업에 대한 상태(_대기_, _완료_, _실패_)와 최종적으로 확정된 결과를 가지고 있습니다.\n\nIn this example, the asynchronous function <CodeStep step={1}>`fetchData`</CodeStep> returns a promise that is awaiting the `fetch`.\n\n```js [[1, 1, \"fetchData()\"], [2, 8, \"getData()\"], [3, 10, \"getData()\"]]\nasync function fetchData() {\n  return await fetch(`https://...`);\n}\n\nconst getData = cache(fetchData);\n\nasync function MyComponent() {\n  getData();\n  // ... some computational work\n  await getData();\n  // ...\n}\n```\n\n<CodeStep step={2}>`getData`</CodeStep>를 처음 호출할 때, <CodeStep step={1}>`fetchData`</CodeStep>에서 반환된 Promise가 캐싱됩니다. 이후 조회 시, 같은 Promise를 반환합니다.\n\n첫 번째 <CodeStep step={2}>`getData`</CodeStep> 호출은 기다리지<sup>`await`</sup> 않지만 <CodeStep step={3}>두 번째</CodeStep>는 기다립니다. [`await`](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Operators/await)은 자바스크립트 연산자로, 기다렸다가 확정된 Promise의 결과를 반환합니다. 첫 번째 <CodeStep step={2}>`getData`</CodeStep>는 단순히 조회할 두 번째 <CodeStep step={3}>`getData`</CodeStep>에 대한 Promise를 캐싱하기 위해 `fetch`를 실행합니다.\n\nIf by the <CodeStep step={3}>second call</CodeStep> the promise is still _pending_, then `await` will pause for the result. The optimization is that while we wait on the `fetch`, React can continue with computational work, thus reducing the wait time for the <CodeStep step={3}>second call</CodeStep>.\n\n_완료된_ 결과나 오류에 대한 Promise가 이미 정해진 경우, `await`는 즉시 값을 반환합니다. 두 결과 모두 성능상의 이점이 있습니다.\n</DeepDive>\n\n<Pitfall>\n\n##### 컴포넌트 외부에서 메모화된 함수를 사용하면 캐시가 사용되지 않습니다. {/*pitfall-memoized-call-outside-component*/}\n\n```jsx [[1, 3, \"getUser\"]]\nimport {cache} from 'react';\n\nconst getUser = cache(async (userId) => {\n  return await db.user.query(userId);\n});\n\n// 🚩 Wrong: 컴포넌트 외부에서 메모화된 함수를 호출하면 메모화하지 않습니다.\ngetUser('demo-id');\n\nasync function DemoProfile() {\n  // ✅ Good: `getUser`는 메모화 됩니다.\n  const user = await getUser('demo-id');\n  return <Profile user={user} />;\n}\n```\n\nReact는 컴포넌트에서 메모화된 함수의 캐시 접근만 제공합니다. 컴포넌트 외부에서 <CodeStep step={1}>`getUser`</CodeStep>를 호출하면 여전히 함수를 실행하지만, 캐시를 읽거나 업데이트하지는 않습니다.\n\nThis is because cache access is provided through a [context](/learn/passing-data-deeply-with-context) which is only accessible from a component.\n\n</Pitfall>\n\n<DeepDive>\n\n#### `cache`, [`memo`](/reference/react/memo), [`useMemo`](/reference/react/useMemo) 중 언제 어떤 걸 사용해야 하나요? {/*cache-memo-usememo*/}\n\n언급된 모든 API들은 메모이제이션을 제공하지만, 메모화 대상, 캐시 접근 권한, 캐시 무효화 시점에 차이가 있습니다.\n\n#### `useMemo` {/*deep-dive-use-memo*/}\n\nIn general, you should use [`useMemo`](/reference/react/useMemo) for caching an expensive computation in a Client Component across renders. As an example, to memoize a transformation of data within a component.\n\n```jsx {4}\n'use client';\n\nfunction WeatherReport({record}) {\n  const avgTemp = useMemo(() => calculateAvg(record), record);\n  // ...\n}\n\nfunction App() {\n  const record = getRecord();\n  return (\n    <>\n      <WeatherReport record={record} />\n      <WeatherReport record={record} />\n    </>\n  );\n}\n```\n\nHowever, `useMemo` does ensure that if `App` re-renders and the `record` object doesn't change, each component instance would skip work and use the memoized value of `avgTemp`. `useMemo` will only cache the last computation of `avgTemp` with the given dependencies.\n\n#### `cache` {/*deep-dive-cache*/}\n\n일반적으로 `cache`는 서버 컴포넌트에서 컴포넌트 간에 공유할 수 있는 작업을 메모화하기 위해 사용합니다.\n\n```js [[1, 12, \"<WeatherReport city={city} />\"], [3, 13, \"<WeatherReport city={city} />\"], [2, 1, \"cache(fetchReport)\"]]\nconst cachedFetchReport = cache(fetchReport);\n\nfunction WeatherReport({city}) {\n  const report = cachedFetchReport(city);\n  // ...\n}\n\nfunction App() {\n  const city = \"Los Angeles\";\n  return (\n    <>\n      <WeatherReport city={city} />\n      <WeatherReport city={city} />\n    </>\n  );\n}\n```\n이전 예시를 `cache`를 이용해 재작성하면, 이 경우에 <CodeStep step={3}>`WeatherReport`의 두 번째 인스턴스</CodeStep>는 중복 작업을 생략하고 <CodeStep step={1}>첫 번째 `WeatherReport`</CodeStep>와 같은 캐시를 읽게 됩니다. 이전 예시와 다른 점은 계산에만 사용되는 `useMemo`와 달리 `cache`는 <CodeStep step={2}>데이터 가져오기를 메모화하는 데</CodeStep>도 권장된다는 점입니다.\n\n이때, `cache`는 서버 컴포넌트에서만 사용해야 하며 캐시는 서버 요청 전체에서 무효화가 됩니다.\n\n#### `memo` {/*deep-dive-memo*/}\n\n[`memo`](reference/react/memo)는 프로퍼티가 변경되지 않았을 때 컴포넌트가 다시 렌더링되는 것을 막기 위해 사용합니다.\n\n```js\n'use client';\n\nfunction WeatherReport({record}) {\n  const avgTemp = calculateAvg(record);\n  // ...\n}\n\nconst MemoWeatherReport = memo(WeatherReport);\n\nfunction App() {\n  const record = getRecord();\n  return (\n    <>\n      <MemoWeatherReport record={record} />\n      <MemoWeatherReport record={record} />\n    </>\n  );\n}\n```\n\nIn this example, both `MemoWeatherReport` components will call `calculateAvg` when first rendered. However, if `App` re-renders, with no changes to `record`, none of the props have changed and `MemoWeatherReport` will not re-render.\n\n`useMemo`와 비교하면 `memo`는 프로퍼티와 특정 계산을 기반으로 컴포넌트 렌더링을 메모화합니다. `useMemo`와 유사하게, 메모화된 컴포넌트는 마지막 프로퍼티 값에 대한 마지막 렌더링을 캐싱합니다. 프로퍼티가 변경되면, 캐시는 무효화되고 컴포넌트는 다시 렌더링됩니다.\n\n</DeepDive>\n\n---\n\n## 문제 해결 {/*troubleshooting*/}\n\n### 동일한 인수로 함수를 호출해도 메모된 함수가 계속 실행됩니다 {/*memoized-function-still-runs*/}\n\n앞서 언급된 주의 사항들을 확인하세요.\n* [다른 메모화된 함수를 호출하면 다른 캐시에서 읽습니다.](#pitfall-different-memoized-functions)\n* [컴포넌트 외부에서 메모화된 함수를 사용하면 캐시가 사용되지 않습니다.](#pitfall-memoized-call-outside-component)\n\n위의 어느 것도 해당하지 않는다면, React가 캐시에 무엇이 존재하는지 확인하는 방식에 문제가 있을 수 있습니다.\n\n인자가 [원시 값](https://developer.mozilla.org/ko/docs/Glossary/Primitive)(객체, 함수, 배열 등)이 아니라면, 같은 객체 참조를 넘겼는지 확인하세요.\n\n메모화된 함수 호출 시, React는 입력된 인자값을 조회해 결과가 이미 캐싱되어 있는지 확인합니다. React는 인수들의 얕은 동등성을 사용해 캐시 히트가 있는지를 결정합니다.\n\n```js\nimport {cache} from 'react';\n\nconst calculateNorm = cache((vector) => {\n  // ...\n});\n\nfunction MapMarker(props) {\n  // 🚩 Wrong: 인자가 매 렌더링마다 변경되는 객체입니다.\n  const length = calculateNorm(props);\n  // ...\n}\n\nfunction App() {\n  return (\n    <>\n      <MapMarker x={10} y={10} z={10} />\n      <MapMarker x={10} y={10} z={10} />\n    </>\n  );\n}\n```\n이 경우 두 `MapMarker`는 동일한 작업을 수행하고 동일한 값인 `{x: 10, y: 10, z:10}`와 함께 `calculateNorm`를 호출하는 듯 보입니다. 객체에 동일한 값이 포함되어 있더라도 각 컴포넌트가 자체 프로퍼티 객체를 생성하므로, 동일한 객체 참조가 아닙니다.\n\nReact는 입력에서 [`Object.is`](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Object/is)를 호출해 캐시 히트가 있는지 확인합니다.\n\n```js {3,9}\nimport {cache} from 'react';\n\nconst calculateNorm = cache((x, y, z) => {\n  // ...\n});\n\nfunction MapMarker(props) {\n  // ✅ Good: 메모화 함수에 인자로 원시값 제공하기\n  const length = calculateNorm(props.x, props.y, props.z);\n  // ...\n}\n\nfunction App() {\n  return (\n    <>\n      <MapMarker x={10} y={10} z={10} />\n      <MapMarker x={10} y={10} z={10} />\n    </>\n  );\n}\n```\n\n이 문제를 해결하는 한 가지 방법은 벡터 차원을 `calculateNorm`에 전달하는 것입니다. 차원 자체가 원시 값이기 때문에 가능합니다.\n\n다른 방법은 벡터 객체를 컴포넌트의 프로퍼티로 전달하는 방법입니다. 두 컴포넌트 인스턴스에 동일한 객체를 전달해야 합니다.\n\n```js {3,9,14}\nimport {cache} from 'react';\n\nconst calculateNorm = cache((vector) => {\n  // ...\n});\n\nfunction MapMarker(props) {\n  // ✅ Good: 동일한 `vector` 객체를 넘겨줍니다.\n  const length = calculateNorm(props.vector);\n  // ...\n}\n\nfunction App() {\n  const vector = [10, 10, 10];\n  return (\n    <>\n      <MapMarker vector={vector} />\n      <MapMarker vector={vector} />\n    </>\n  );\n}\n```\n"
  },
  {
    "path": "src/content/reference/react/cacheSignal.md",
    "content": "---\ntitle: cacheSignal\n---\n\n<RSC>\n\n`cacheSignal`은 현재 [React 서버 컴포넌트](/blog/2023/03/22/react-labs-what-we-have-been-working-on-march-2023#react-server-components)에서만 사용할 수 있습니다.\n\n</RSC>\n\n<Intro>\n\n`cacheSignal`을 사용하면 `cache()` 수명이 언제 끝나는지 알 수 있습니다.\n\n```js\nconst signal = cacheSignal();\n```\n\n</Intro>\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `cacheSignal` {/*cachesignal*/}\n\n`cacheSignal`을 호출하면 `AbortSignal`을 얻을 수 있습니다.\n\n```js {3,7}\nimport {cacheSignal} from 'react';\nasync function Component() {\n  await fetch(url, { signal: cacheSignal() });\n}\n```\n\nReact가 렌더링을 완료하면 `AbortSignal`이 중단됩니다. 이를 통해 더 이상 필요하지 않은 진행 중인 작업을 취소할 수 있습니다.\n렌더링이 완료된 것으로 간주하는 경우는 다음과 같습니다.\n- React가 성공적으로 렌더링을 완료한 경우\n- 렌더링이 중단된 경우\n- 렌더링이 실패한 경우\n\n#### 매개변수 {/*parameters*/}\n\n이 함수는 매개변수를 받지 않습니다.\n\n#### 반환값 {/*returns*/}\n\n`cacheSignal`은 렌더링 중에 호출되면 `AbortSignal`을 반환합니다. 그 외의 경우에 `cacheSignal()`은 `null`을 반환합니다.\n\n#### 주의 사항 {/*caveats*/}\n\n- `cacheSignal`은 현재 [React 서버 컴포넌트](/reference/rsc/server-components)에서만 사용할 수 있습니다. 클라이언트 컴포넌트에서는 항상 `null`을 반환합니다. 향후 클라이언트 캐시가 갱신되거나 무효화될 때 클라이언트 컴포넌트에서도 사용될 예정입니다. 클라이언트에서 항상 `null`을 반환한다고 가정하면 안 됩니다.\n- 렌더링 외부에서 호출하면 `cacheSignal`은 `null`을 반환하여 현재 스코프가 영원히 캐시되지 않음을 명확히 합니다.\n\n---\n\n## 사용법 {/*usage*/}\n\n### 진행 중인 요청 취소하기 {/*cancel-in-flight-requests*/}\n\n<CodeStep step={1}>`cacheSignal`</CodeStep>을 호출하여 진행 중인 요청을 중단할 수 있습니다.\n\n```js [[1, 4, \"cacheSignal()\"]]\nimport {cache, cacheSignal} from 'react';\nconst dedupedFetch = cache(fetch);\nasync function Component() {\n  await dedupedFetch(url, { signal: cacheSignal() });\n}\n```\n\n<Pitfall>\n아래의 예시처럼 렌더링 외부에서 시작된 비동기 작업을 `cacheSignal`로 중단할 수 없습니다.\n\n```js\nimport {cacheSignal} from 'react';\n// 🚩 Pitfall: The request will not actually be aborted if the rendering of `Component` is finished.\nconst response = fetch(url, { signal: cacheSignal() });\nasync function Component() {\n  await response;\n}\n```\n</Pitfall>\n\n### React가 렌더링을 완료한 후 오류 무시하기 {/*ignore-errors-after-react-has-finished-rendering*/}\n\n함수가 오류를 던지는 경우 취소로 인한 것일 수 있습니다. (예를 들어, <CodeStep step={1}>데이터베이스 연결</CodeStep>이 닫힌 경우) <CodeStep step={2}>`aborted` 속성</CodeStep>을 사용하여 오류가 취소로 인한 것인지 실제 오류인지 확인할 수 있습니다. 취소로 인한 <CodeStep step={3}>오류는 무시할 수 있습니다</CodeStep>.\n\n```js [[1, 2, \"./database\"], [2, 8, \"cacheSignal()?.aborted\"], [3, 12, \"return null\"]]\nimport {cacheSignal} from \"react\";\nimport {queryDatabase, logError} from \"./database\";\n\nasync function getData(id) {\n  try {\n     return await queryDatabase(id);\n  } catch (x) {\n     if (!cacheSignal()?.aborted) {\n        // only log if it's a real error and not due to cancellation\n       logError(x);\n     }\n     return null;\n  }\n}\n\nasync function Component({id}) {\n  const data = await getData(id);\n  if (data === null) {\n    return <div>No data available</div>;\n  }\n  return <div>{data.name}</div>;\n}\n```\n"
  },
  {
    "path": "src/content/reference/react/captureOwnerStack.md",
    "content": "---\ntitle: captureOwnerStack\n---\n\n<Intro>\n\n`captureOwnerStack`는 개발 환경에서 현재 Owner Stack을 읽고, 사용할 수 있다면 문자열 반환합니다.\n\n```js\nconst stack = captureOwnerStack();\n```\n\n</Intro>\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `captureOwnerStack()` {/*captureownerstack*/}\n\n`captureOwnerStack`을 호출하여 현재 Owner Stack을 가져옵니다.\n\n```js {5,5}\nimport * as React from 'react';\n\nfunction Component() {\n  if (process.env.NODE_ENV !== 'production') {\n    const ownerStack = React.captureOwnerStack();\n    console.log(ownerStack);\n  }\n}\n```\n\n#### 매개변수 {/*parameters*/}\n\n`captureOwnerStack`는 매개변수를 받지 않습니다.\n\n#### 반환값 {/*returns*/}\n\n`captureOwnerStack`은 `string`이나 `null`을 반환합니다.\n\nOwner Stacks은 다음 경우에 사용할 수 있습니다.\n- 컴포넌트 렌더링 시\n- Effect (예: `useEffect`)\n- React 이벤트 핸들러 (예: `<button onClick={...} />`)\n- React 오류 핸들러 ([React 루트 옵션](/reference/react-dom/client/createRoot#parameters) `onCaughtError`, `onRecoverableError`, `onUncaughtError`)\n\nOwner Stack을 사용할 수 없는 경우, `null`을 반환합니다. ([문제해결: Owner Stack이 `null`인 경우](#the-owner-stack-is-null))\n\n#### 주의 사항 {/*caveats*/}\n\n- Owner Stack은 개발 환경에서만 사용할 수 있습니다. `captureOwnerStack`은 개발 환경 밖에서는 항상 `null`을 반환합니다.\n\n<DeepDive>\n\n#### Owner Stack vs Component Stack {/*owner-stack-vs-component-stack*/}\n\nThe Owner Stack is different from the Component Stack available in React error handlers like [`errorInfo.componentStack` in `onUncaughtError`](/reference/react-dom/client/hydrateRoot#error-logging-in-production).\n\n예를 들어 다음 코드를 살펴보겠습니다.\n\n<Sandpack>\n\n```js src/App.js\nimport {Suspense} from 'react';\n\nfunction SubComponent({disabled}) {\n  if (disabled) {\n    throw new Error('disabled');\n  }\n}\n\nexport function Component({label}) {\n  return (\n    <fieldset>\n      <legend>{label}</legend>\n      <SubComponent key={label} disabled={label === 'disabled'} />\n    </fieldset>\n  );\n}\n\nfunction Navigation() {\n  return null;\n}\n\nexport default function App({children}) {\n  return (\n    <Suspense fallback=\"loading...\">\n      <main>\n        <Navigation />\n        {children}\n      </main>\n    </Suspense>\n  );\n}\n```\n\n```js src/index.js\nimport {captureOwnerStack} from 'react';\nimport {createRoot} from 'react-dom/client';\nimport App, {Component} from './App.js';\nimport './styles.css';\n\ncreateRoot(document.createElement('div'), {\n  onUncaughtError: (error, errorInfo) => {\n    // The stacks are logged instead of showing them in the UI directly to\n    // highlight that browsers will apply sourcemaps to the logged stacks.\n    // Note that sourcemapping is only applied in the real browser console not\n    // in the fake one displayed on this page.\n    // Press \"fork\" to be able to view the sourcemapped stack in a real console.\n    console.log(errorInfo.componentStack);\n    console.log(captureOwnerStack());\n  },\n}).render(\n  <App>\n    <Component label=\"disabled\" />\n  </App>\n);\n```\n\n```html public/index.html hidden\n<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Document</title>\n  </head>\n  <body>\n    <p>Check the console output.</p>\n  </body>\n</html>\n```\n\n</Sandpack>\n\n`SubComponent`에서 오류가 날 수 있습니다.\n해당 오류의 컴포넌트 Stack은 다음과 같을 것입니다.\n\n```\nat SubComponent\nat fieldset\nat Component\nat main\nat React.Suspense\nat App\n```\n\n그러나, Owner Stack에는 다음 내용만 나타납니다.\n\n```\nat Component\n```\n\n`App`과 DOM 컴포넌트들(예: `fieldset`)은 `SubComponent`를 포함하는 노드를 \"생성하는\" 데에 기여하지 않기 때문 이 스택에 포함되지 않습니다. `App`과 DOM 컴포넌트들은 노드를 전달할 뿐입니다. `App`은 `<SubComponent />`를 통해 `SubComponent`를 포함한 노드를 생성하는 `Component`와 달리 `children` 노드만 렌더링합니다.\n\n`Navigation`과 `legend`는 `<SubComponent />`를 포함하는 노드의 형제 요소이기 때문에 스택에 전혀 포함되지 않습니다.\n\n`SubComponent`는 이미 호출 스택에 포함되어 있기 떄문에 Owner Stack에 나타나지 않습니다.\n\n</DeepDive>\n\n## 사용법 {/*usage*/}\n\n### 커스텀 오류 오버레이 개선하기 {/*enhance-a-custom-error-overlay*/}\n\n```js [[1, 5, \"console.error\"], [4, 7, \"captureOwnerStack\"]]\nimport { captureOwnerStack } from \"react\";\nimport { instrumentedConsoleError } from \"./errorOverlay\";\n\nconst originalConsoleError = console.error;\nconsole.error = function patchedConsoleError(...args) {\n  originalConsoleError.apply(console, args);\n  const ownerStack = captureOwnerStack();\n  onConsoleError({\n    // Keep in mind that in a real application, console.error can be\n    // called with multiple arguments which you should account for.\n    consoleMessage: args[0],\n    ownerStack,\n  });\n};\n```\n\n<CodeStep step={1}>`console.error`</CodeStep> 호출을 가로채서 오류 오버레이에 표시하고 싶다면, <CodeStep step={2}>`captureOwnerStack`</CodeStep>을 호출하여 `OwnerStack`을 포함할 수 있습니다.\n\n<Sandpack>\n\n```css src/styles.css\n* {\n  box-sizing: border-box;\n}\n\nbody {\n  font-family: sans-serif;\n  margin: 20px;\n  padding: 0;\n}\n\nh1 {\n  margin-top: 0;\n  font-size: 22px;\n}\n\nh2 {\n  margin-top: 0;\n  font-size: 20px;\n}\n\ncode {\n  font-size: 1.2em;\n}\n\nul {\n  padding-inline-start: 20px;\n}\n\nlabel, button { display: block; margin-bottom: 20px; }\nhtml, body { min-height: 300px; }\n\n#error-dialog {\n  position: absolute;\n  top: 0;\n  right: 0;\n  bottom: 0;\n  left: 0;\n  background-color: white;\n  padding: 15px;\n  opacity: 0.9;\n  text-wrap: wrap;\n  overflow: scroll;\n}\n\n.text-red {\n  color: red;\n}\n\n.-mb-20 {\n  margin-bottom: -20px;\n}\n\n.mb-0 {\n  margin-bottom: 0;\n}\n\n.mb-10 {\n  margin-bottom: 10px;\n}\n\npre {\n  text-wrap: wrap;\n}\n\npre.nowrap {\n  text-wrap: nowrap;\n}\n\n.hidden {\n display: none;  \n}\n```\n\n```html public/index.html hidden\n<!DOCTYPE html>\n<html>\n<head>\n  <title>My app</title>\n</head>\n<body>\n<!--\n  Error dialog in raw HTML\n  since an error in the React app may crash.\n-->\n<div id=\"error-dialog\" class=\"hidden\">\n  <h1 id=\"error-title\" class=\"text-red\">Error</h1>\n  <p>\n    <pre id=\"error-body\"></pre>\n  </p>\n  <h2 class=\"-mb-20\">Owner Stack:</h4>\n  <pre id=\"error-owner-stack\" class=\"nowrap\"></pre>\n  <button\n    id=\"error-close\"\n    class=\"mb-10\"\n    onclick=\"document.getElementById('error-dialog').classList.add('hidden')\"\n  >\n    Close\n  </button>\n</div>\n<!-- This is the DOM node -->\n<div id=\"root\"></div>\n</body>\n</html>\n\n```\n\n```js src/errorOverlay.js\n\nexport function onConsoleError({ consoleMessage, ownerStack }) {\n  const errorDialog = document.getElementById(\"error-dialog\");\n  const errorBody = document.getElementById(\"error-body\");\n  const errorOwnerStack = document.getElementById(\"error-owner-stack\");\n\n  // Display console.error() message\n  errorBody.innerText = consoleMessage;\n\n  // Display owner stack\n  errorOwnerStack.innerText = ownerStack;\n\n  // Show the dialog\n  errorDialog.classList.remove(\"hidden\");\n}\n```\n\n```js src/index.js active\nimport { captureOwnerStack } from \"react\";\nimport { createRoot } from \"react-dom/client\";\nimport App from './App';\nimport { onConsoleError } from \"./errorOverlay\";\nimport './styles.css';\n\nconst originalConsoleError = console.error;\nconsole.error = function patchedConsoleError(...args) {\n  originalConsoleError.apply(console, args);\n  const ownerStack = captureOwnerStack();\n  onConsoleError({\n    // Keep in mind that in a real application, console.error can be\n    // called with multiple arguments which you should account for.\n    consoleMessage: args[0],\n    ownerStack,\n  });\n};\n\nconst container = document.getElementById(\"root\");\ncreateRoot(container).render(<App />);\n```\n\n```js src/App.js\nfunction Component() {\n  return <button onClick={() => console.error('Some console error')}>Trigger console.error()</button>;\n}\n\nexport default function App() {\n  return <Component />;\n}\n```\n\n</Sandpack>\n\n## 문제 해결 {/*troubleshooting*/}\n\n### Owner Stack이 `null`인 경우 {/*the-owner-stack-is-null*/}\n\n`captureOwnerStack`이 `setTimeout` 콜백과 같이 React가 제어하지 않는 함수 바깥에서 호출됐을 경우, `fetch` 호출 후, 커스텀 DOM 이벤트 핸들러 등에서는 Owner Stack이 `null`이 됩니다. 렌더링 중이나 Effect, React 이벤트 핸들러, React 오류 핸들러 (예: `hydrateRoot#options.onCaughtError`) 내에서만 생성됩니다.\n\n아래 예시에서, 버튼을 클릭하면 빈 Owner Stack이 로그로 출력됩니다. 그 이유는 `captureOwnerStack`이 커스텀 이벤트 핸들러 내에서 호출되었기 때문입니다. Owner Stack은 더 이른 시점, 예를 들어 이펙트 내부에서 `captureOwnerStack`를 호출하도록 이동시켜야 올바르게 캡처할 수 있습니다.\n<Sandpack>\n\n```js\nimport {captureOwnerStack, useEffect} from 'react';\n\nexport default function App() {\n  useEffect(() => {\n    // Should call `captureOwnerStack` here.\n    function handleEvent() {\n      // Calling it in a custom DOM event handler is too late.\n      // The Owner Stack will be `null` at this point.\n      console.log('Owner Stack: ', captureOwnerStack());\n    }\n\n    document.addEventListener('click', handleEvent);\n\n    return () => {\n      document.removeEventListener('click', handleEvent);\n    }\n  })\n\n  return <button>Click me to see that Owner Stacks are not available in custom DOM event handlers</button>;\n}\n```\n\n</Sandpack>\n\n### `captureOwnerStack`을 사용할 수 없는 경우 {/*captureownerstack-is-not-available*/}\n\n`captureOwnerStack`은 개발 환경 빌드에서만 Export됩니다. 프로덕션 환경 빌드에서는 `undefined`입니다. `captureOwnerStack`이 개발과 프로덕션이 모두 번들링되는 파일에서 사용될 때는 네임스페이스 `import`를 사용하고 조건부로 접근해야 합니다.\n\n```js\n// Don't use named imports of `captureOwnerStack` in files that are bundled for development and production.\nimport {captureOwnerStack} from 'react';\n// Use a namespace import instead and access `captureOwnerStack` conditionally.\nimport * as React from 'react';\n\nif (process.env.NODE_ENV !== 'production') {\n  const ownerStack = React.captureOwnerStack();\n  console.log('Owner Stack', ownerStack);\n}\n```\n"
  },
  {
    "path": "src/content/reference/react/cloneElement.md",
    "content": "---\ntitle: cloneElement\n---\n\n<Pitfall>\n\n`cloneElement`사용하는 것은 일반적이지 않고 불안정한 코드를 만들 수 있습니다. [일반적으로 사용하는 대안을 살펴보세요.](#alternatives)\n\n</Pitfall>\n\n<Intro>\n\n`cloneElement`를 사용하면 엘리먼트를 기준으로 새로운 React 엘리먼트를 만들 수 있습니다.\n\n```js\nconst clonedElement = cloneElement(element, props, ...children)\n```\n\n</Intro>\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `cloneElement(element, props, ...children)` {/*cloneelement*/}\n\n새로운 React 엘리먼트를 만들기 위해 `element`를 기준으로 하고, `props`와 `children`을 다르게 하여 `cloneElement`를 호출하세요.\n\n```js\nimport { cloneElement } from 'react';\n\n// ...\nconst clonedElement = cloneElement(\n  <Row title=\"Cabbage\">\n    Hello\n  </Row>,\n  { isHighlighted: true },\n  'Goodbye'\n);\n\nconsole.log(clonedElement); // <Row title=\"Cabbage\" isHighlighted={true}>Goodbye</Row>\n```\n\n[아래에서 더 많은 예시를 볼 수 있습니다.](#usage)\n\n#### 매개변수 {/*parameters*/}\n\n* `element`: `element` 인자는 유효한 React 엘리먼트여야 합니다. 예를 들어, `<Something />`과 같은 JSX 노드, [`createElement`](/reference/react/createElement)로 호출해 얻은 결과물 또는 다른 `cloneElement`로 호출해 얻은 결과물이 될 수 있습니다.\n\n* `props`: `props` 인자는 객체 또는 `null`이어야 합니다. `null`을 전달하면 복제된 엘리먼트는 원본 `element.props`를 모두 유지합니다. 그렇지 않으면 `props` 객체의 각 prop에 대해 반환된 엘리먼트는 `element.props`의 값보다 `props`의 값을 \"우선\"합니다. 나머지 `props`는 원본 `element.props`에서 채워집니다. `props.key` 또는 `props.ref`를 전달하면 원본의 것을 대체합니다.\n\n* **(선택사항)** `...children`: 0개 이상의 자식 노드가 필요합니다. React 엘리먼트, 문자열, 숫자, [portals](/reference/react-dom/createPortal), 빈 노드 (`null`, `undefined`, `true`, `false`) 및 React 노드 배열을 포함한 모든 React 노드가 해당할 수 있습니다. `...children` 인자를 전달하지 않으면 원본 `element.props.children`이 유지됩니다.\n\n#### 반환값 {/*returns*/}\n\n`cloneElement`는 다음과 같은 프로퍼티를 가진 React 엘리먼트 객체를 반환합니다.\n\n* `type`: `element.type`과 동일합니다.\n* `props`: `element.props`와 전달한 `props`를 얕게 병합한 결과입니다.\n* `ref`: `props.ref`에 의해 재정의되지 않은 경우 원본 `element.ref`입니다.\n* `key`: `props.key`에 의해 재정의되지 않은 경우 원본 `element.key`입니다.\n\n일반적으로 컴포넌트에서 엘리먼트를 반환하거나 다른 엘리먼트의 자식으로 만듭니다. 엘리먼트의 프로퍼티를 읽을 수 있지만, 생성된 후에는 모든 엘리먼트의 프로퍼티를 읽을 수 없는 것처럼 취급하고 렌더링하는 것이 좋습니다.\n\n#### 주의 사항 {/*caveats*/}\n\n* 엘리먼트를 복제해도 **원본 엘리먼트는 수정되지 않습니다.**\n\n* **자식이 모두 정적인 경우에만** `cloneElement(element, null, child1, child2, child3)`와 같이 **자식을 여러 개의 인자로 전달해야 합니다.** 자식이 동적으로 생성되었다면 `cloneElement(element, null, listItems)`와 같이 전체 배열을 세 번째 인자로 전달해야 합니다. 이렇게 하면 React가 모든 동적 리스트에 대해 [key가 누락되었다는 경고](/learn/rendering-lists#keeping-list-items-in-order-with-key)를 보여줍니다. 정적 리스트의 경우는 순서가 변경되지 않으므로 이 작업은 필요하지 않습니다.\n\n* `cloneElement`는 데이터 흐름을 추적하기 어렵기 때문에 다음 [대안](#alternatives)을 사용해 보세요.\n\n---\n\n## 사용법 {/*usage*/}\n\n### 엘리먼트의 props 재정의하기 {/*overriding-props-of-an-element*/}\n\n일부 <CodeStep step={1}>React 엘리먼트</CodeStep>의 props를 재정의하려면 <CodeStep step={2}>재정의하려는 props</CodeStep>를 `cloneElement`에 전달하세요.\n\n```js [[1, 5, \"<Row title=\\\\\"Cabbage\\\\\" />\"], [2, 6, \"{ isHighlighted: true }\"], [3, 4, \"clonedElement\"]]\nimport { cloneElement } from 'react';\n\n// ...\nconst clonedElement = cloneElement(\n  <Row title=\"Cabbage\" />,\n  { isHighlighted: true }\n);\n```\n\n<CodeStep step={3}>clonedElement</CodeStep>의 결과는 `<Row title=\"Cabbage\" isHighlighted={true} />`가 됩니다.\n\n**어떤 경우에 유용한지 예시를 통해 알아보도록 하겠습니다.**\n\n[`children`](/learn/passing-props-to-a-component#passing-jsx-as-children)을 선택할 수 있는 행 목록으로 렌더링하고, 선택된 행을 변경하는 \"다음\" 버튼이 있는 `List` 컴포넌트를 상상해 보세요. `List` 컴포넌트는 선택된 행을 다르게 렌더링해야 하므로 전달받은 모든 `<Row>` 자식 요소를 복제합니다. 그리고 `isHighlighted: true` 또는 `isHighlighted: false`인 `prop`을 추가합니다.\n\n```js {6-8}\nexport default function List({ children }) {\n  const [selectedIndex, setSelectedIndex] = useState(0);\n  return (\n    <div className=\"List\">\n      {Children.map(children, (child, index) =>\n        cloneElement(child, {\n          isHighlighted: index === selectedIndex\n        })\n      )}\n```\n\n다음과 같이 `List`에서 전달받은 원본 JSX가 있다고 가정합시다.\n\n```js {2-4}\n<List>\n  <Row title=\"Cabbage\" />\n  <Row title=\"Garlic\" />\n  <Row title=\"Apple\" />\n</List>\n```\n\n자식 요소를 복제함으로써 `List`는 모든 `Row` 안에 추가적인 정보를 전달할 수 있습니다. 결과는 다음과 같습니다.\n\n```js {4,8,12}\n<List>\n  <Row\n    title=\"Cabbage\"\n    isHighlighted={true}\n  />\n  <Row\n    title=\"Garlic\"\n    isHighlighted={false}\n  />\n  <Row\n    title=\"Apple\"\n    isHighlighted={false}\n  />\n</List>\n```\n\n\"다음\" 버튼을 누르면 `List`의 state가 업데이트되고 다른 행이 하이라이트 표시가 되는 것을 확인할 수 있습니다.\n\n<Sandpack>\n\n```js\nimport List from './List.js';\nimport Row from './Row.js';\nimport { products } from './data.js';\n\nexport default function App() {\n  return (\n    <List>\n      {products.map(product =>\n        <Row\n          key={product.id}\n          title={product.title}\n        />\n      )}\n    </List>\n  );\n}\n```\n\n```js src/List.js active\nimport { Children, cloneElement, useState } from 'react';\n\nexport default function List({ children }) {\n  const [selectedIndex, setSelectedIndex] = useState(0);\n  return (\n    <div className=\"List\">\n      {Children.map(children, (child, index) =>\n        cloneElement(child, {\n          isHighlighted: index === selectedIndex\n        })\n      )}\n      <hr />\n      <button onClick={() => {\n        setSelectedIndex(i =>\n          (i + 1) % Children.count(children)\n        );\n      }}>\n        다음\n      </button>\n    </div>\n  );\n}\n```\n\n```js src/Row.js\nexport default function Row({ title, isHighlighted }) {\n  return (\n    <div className={[\n      'Row',\n      isHighlighted ? 'RowHighlighted' : ''\n    ].join(' ')}>\n      {title}\n    </div>\n  );\n}\n```\n\n```js src/data.js\nexport const products = [\n  { title: 'Cabbage', id: 1 },\n  { title: 'Garlic', id: 2 },\n  { title: 'Apple', id: 3 },\n];\n```\n\n```css\n.List {\n  display: flex;\n  flex-direction: column;\n  border: 2px solid grey;\n  padding: 5px;\n}\n\n.Row {\n  border: 2px dashed black;\n  padding: 5px;\n  margin: 5px;\n}\n\n.RowHighlighted {\n  background: #ffa;\n}\n\nbutton {\n  height: 40px;\n  font-size: 20px;\n}\n```\n\n</Sandpack>\n\n요약하자면, `List`는 전달받은 `<Row />` 엘리먼트를 복제하고 추가로 들어오는 prop 또한 추가합니다.\n\n<Pitfall>\n\n자식 요소를 복제하는 것은 앱에서 데이터가 어떻게 흘러가는지 파악하기 어렵기 때문에 다음 [대안](#alternatives)을 사용해 보세요.\n\n</Pitfall>\n\n---\n\n## 대안 {/*alternatives*/}\n\n### 렌더링 prop으로 데이터를 전달하기 {/*passing-data-with-a-render-prop*/}\n\n`cloneElement`를 사용하는 대신에 `renderItem`과 같은 *렌더링 prop*을 사용하는 것을 고려해 보세요. 다음 예시의 `List`는 `renderItem`을 prop으로 받습니다. `List`는 모든 item에 대해 `renderItem`을 호출하고 `isHighlighted`를 인자로 전달합니다.\n\n```js {1,7}\nexport default function List({ items, renderItem }) {\n  const [selectedIndex, setSelectedIndex] = useState(0);\n  return (\n    <div className=\"List\">\n      {items.map((item, index) => {\n        const isHighlighted = index === selectedIndex;\n        return renderItem(item, isHighlighted);\n      })}\n```\n\n`renderItem` prop은 렌더링 방법을 지정하는 prop이기 때문에 \"렌더링 prop\"이라고 불립니다. 예를 들어, 주어진 `isHighlighted` 값으로 `<Row>`를 렌더링하는 `renderItem`을 전달할 수 있습니다.\n\n```js {3,7}\n<List\n  items={products}\n  renderItem={(product, isHighlighted) =>\n    <Row\n      key={product.id}\n      title={product.title}\n      isHighlighted={isHighlighted}\n    />\n  }\n/>\n```\n\n최종적으로 `cloneElement`와 같은 결과가 됩니다.\n\n```js {4,8,12}\n<List>\n  <Row\n    title=\"Cabbage\"\n    isHighlighted={true}\n  />\n  <Row\n    title=\"Garlic\"\n    isHighlighted={false}\n  />\n  <Row\n    title=\"Apple\"\n    isHighlighted={false}\n  />\n</List>\n```\n\n하지만 `isHighlighted` 값의 출처를 명확하게 추적할 수 있습니다.\n\n<Sandpack>\n\n```js\nimport List from './List.js';\nimport Row from './Row.js';\nimport { products } from './data.js';\n\nexport default function App() {\n  return (\n    <List\n      items={products}\n      renderItem={(product, isHighlighted) =>\n        <Row\n          key={product.id}\n          title={product.title}\n          isHighlighted={isHighlighted}\n        />\n      }\n    />\n  );\n}\n```\n\n```js src/List.js active\nimport { useState } from 'react';\n\nexport default function List({ items, renderItem }) {\n  const [selectedIndex, setSelectedIndex] = useState(0);\n  return (\n    <div className=\"List\">\n      {items.map((item, index) => {\n        const isHighlighted = index === selectedIndex;\n        return renderItem(item, isHighlighted);\n      })}\n      <hr />\n      <button onClick={() => {\n        setSelectedIndex(i =>\n          (i + 1) % items.length\n        );\n      }}>\n        다음\n      </button>\n    </div>\n  );\n}\n```\n\n```js src/Row.js\nexport default function Row({ title, isHighlighted }) {\n  return (\n    <div className={[\n      'Row',\n      isHighlighted ? 'RowHighlighted' : ''\n    ].join(' ')}>\n      {title}\n    </div>\n  );\n}\n```\n\n```js src/data.js\nexport const products = [\n  { title: 'Cabbage', id: 1 },\n  { title: 'Garlic', id: 2 },\n  { title: 'Apple', id: 3 },\n];\n```\n\n```css\n.List {\n  display: flex;\n  flex-direction: column;\n  border: 2px solid grey;\n  padding: 5px;\n}\n\n.Row {\n  border: 2px dashed black;\n  padding: 5px;\n  margin: 5px;\n}\n\n.RowHighlighted {\n  background: #ffa;\n}\n\nbutton {\n  height: 40px;\n  font-size: 20px;\n}\n```\n\n</Sandpack>\n\n이러한 패턴은 더 명시적이기 때문에 `cloneElement` 보다 선호됩니다.\n\n---\n\n### Context를 통해 데이터 전달하기 {/*passing-data-through-context*/}\n\n`cloneElement`의 또 다른 대안으로는 [Context를 통해 데이터를 전달하는 것](/learn/passing-data-deeply-with-context)입니다.\n\n\n예를 들어, [`createContext`](/reference/react/createContext)를 호출하여 `HighlightContext`를 정의할 수 있습니다.\n\n```js\nexport const HighlightContext = createContext(false);\n```\n\n`List` 컴포넌트는 렌더링하는 모든 item을 `HighlightContext.Provider`로 감쌀 수 있습니다.\n\n```js {8,10}\nexport default function List({ items, renderItem }) {\n  const [selectedIndex, setSelectedIndex] = useState(0);\n  return (\n    <div className=\"List\">\n      {items.map((item, index) => {\n        const isHighlighted = index === selectedIndex;\n        return (\n          <HighlightContext key={item.id} value={isHighlighted}>\n            {renderItem(item)}\n          </HighlightContext>\n        );\n      })}\n```\n\n이러한 접근 방식으로 인해 `Row`는 `isHighlighted` prop을 받을 필요가 없어집니다. 대신 context를 읽습니다.\n\n```js src/Row.js {2}\nexport default function Row({ title }) {\n  const isHighlighted = useContext(HighlightContext);\n  // ...\n```\n\n이에 따라 `isHighlighted`를 `<Row>`로 전달하는 것에 대해 호출된 컴포넌트가 알거나 걱정하지 않아도 됩니다.\n\n```js {4}\n<List\n  items={products}\n  renderItem={product =>\n    <Row title={product.title} />\n  }\n/>\n```\n\n대신에 `List`와 `Row`는 context를 통해 하이라이팅 로직을 조정합니다.\n\n<Sandpack>\n\n```js\nimport List from './List.js';\nimport Row from './Row.js';\nimport { products } from './data.js';\n\nexport default function App() {\n  return (\n    <List\n      items={products}\n      renderItem={(product) =>\n        <Row title={product.title} />\n      }\n    />\n  );\n}\n```\n\n```js src/List.js active\nimport { useState } from 'react';\nimport { HighlightContext } from './HighlightContext.js';\n\nexport default function List({ items, renderItem }) {\n  const [selectedIndex, setSelectedIndex] = useState(0);\n  return (\n    <div className=\"List\">\n      {items.map((item, index) => {\n        const isHighlighted = index === selectedIndex;\n        return (\n          <HighlightContext\n            key={item.id}\n            value={isHighlighted}\n          >\n            {renderItem(item)}\n          </HighlightContext>\n        );\n      })}\n      <hr />\n      <button onClick={() => {\n        setSelectedIndex(i =>\n          (i + 1) % items.length\n        );\n      }}>\n        다음\n      </button>\n    </div>\n  );\n}\n```\n\n```js src/Row.js\nimport { useContext } from 'react';\nimport { HighlightContext } from './HighlightContext.js';\n\nexport default function Row({ title }) {\n  const isHighlighted = useContext(HighlightContext);\n  return (\n    <div className={[\n      'Row',\n      isHighlighted ? 'RowHighlighted' : ''\n    ].join(' ')}>\n      {title}\n    </div>\n  );\n}\n```\n\n```js src/HighlightContext.js\nimport { createContext } from 'react';\n\nexport const HighlightContext = createContext(false);\n```\n\n```js src/data.js\nexport const products = [\n  { title: 'Cabbage', id: 1 },\n  { title: 'Garlic', id: 2 },\n  { title: 'Apple', id: 3 },\n];\n```\n\n```css\n.List {\n  display: flex;\n  flex-direction: column;\n  border: 2px solid grey;\n  padding: 5px;\n}\n\n.Row {\n  border: 2px dashed black;\n  padding: 5px;\n  margin: 5px;\n}\n\n.RowHighlighted {\n  background: #ffa;\n}\n\nbutton {\n  height: 40px;\n  font-size: 20px;\n}\n```\n\n</Sandpack>\n\n[context를 통해 데이터를 전달하는 것에 대하여 자세히 알아보세요.](/reference/react/useContext#passing-data-deeply-into-the-tree)\n\n---\n\n### 커스텀 Hook으로 로직 추출하기 {/*extracting-logic-into-a-custom-hook*/}\n\n다른 접근 방식으로는 자체 hook을 통해 \"비시각적인\" 로직을 추출하는 것을 시도해 볼 수 있습니다. 그리고 hook에 의해서 반환된 정보를 사용하여 렌더링할 내용을 정합니다. 예를 들어 다음과 같이 `useList` 같은 커스텀 hook을 작성할 수 있습니다.\n\n```js\nimport { useState } from 'react';\n\nexport default function useList(items) {\n  const [selectedIndex, setSelectedIndex] = useState(0);\n\n  function onNext() {\n    setSelectedIndex(i =>\n      (i + 1) % items.length\n    );\n  }\n\n  const selected = items[selectedIndex];\n  return [selected, onNext];\n}\n```\n\n그러므로 다음과 같이 사용할 수 있습니다.\n\n```js {2,9,13}\nexport default function App() {\n  const [selected, onNext] = useList(products);\n  return (\n    <div className=\"List\">\n      {products.map(product =>\n        <Row\n          key={product.id}\n          title={product.title}\n          isHighlighted={selected === product}\n        />\n      )}\n      <hr />\n      <button onClick={onNext}>\n        다음\n      </button>\n    </div>\n  );\n}\n```\n\n데이터 흐름은 명시적이지만 state는 모든 컴포넌트에서 사용할 수 있는 `useList` custom hook 내부에 있습니다.\n\n<Sandpack>\n\n```js\nimport Row from './Row.js';\nimport useList from './useList.js';\nimport { products } from './data.js';\n\nexport default function App() {\n  const [selected, onNext] = useList(products);\n  return (\n    <div className=\"List\">\n      {products.map(product =>\n        <Row\n          key={product.id}\n          title={product.title}\n          isHighlighted={selected === product}\n        />\n      )}\n      <hr />\n      <button onClick={onNext}>\n        다음\n      </button>\n    </div>\n  );\n}\n```\n\n```js src/useList.js\nimport { useState } from 'react';\n\nexport default function useList(items) {\n  const [selectedIndex, setSelectedIndex] = useState(0);\n\n  function onNext() {\n    setSelectedIndex(i =>\n      (i + 1) % items.length\n    );\n  }\n\n  const selected = items[selectedIndex];\n  return [selected, onNext];\n}\n```\n\n```js src/Row.js\nexport default function Row({ title, isHighlighted }) {\n  return (\n    <div className={[\n      'Row',\n      isHighlighted ? 'RowHighlighted' : ''\n    ].join(' ')}>\n      {title}\n    </div>\n  );\n}\n```\n\n```js src/data.js\nexport const products = [\n  { title: 'Cabbage', id: 1 },\n  { title: 'Garlic', id: 2 },\n  { title: 'Apple', id: 3 },\n];\n```\n\n```css\n.List {\n  display: flex;\n  flex-direction: column;\n  border: 2px solid grey;\n  padding: 5px;\n}\n\n.Row {\n  border: 2px dashed black;\n  padding: 5px;\n  margin: 5px;\n}\n\n.RowHighlighted {\n  background: #ffa;\n}\n\nbutton {\n  height: 40px;\n  font-size: 20px;\n}\n```\n\n</Sandpack>\n\n이러한 접근 방식은 다른 컴포넌트 간에 해당 로직을 재사용하고 싶을 때 특히 유용합니다.\n"
  },
  {
    "path": "src/content/reference/react/components.md",
    "content": "---\ntitle: \"내장 React 컴포넌트\"\n---\n\n<Intro>\n\nReact는 JSX에서 사용할 수 있는 몇 가지 내장 컴포넌트를 제공합니다.\n\n</Intro>\n\n---\n\n## 내장 컴포넌트 {/*built-in-components*/}\n\n* [`<Fragment>`](/reference/react/Fragment) 또는 `<>...</>` 로 표기하며, 여러 JSX 노드를 함께 그룹화할 수 있습니다.\n* [`<Profiler>`](/reference/react/Profiler)를 통해 React 트리의 렌더링 성능을 프로그래밍 방식으로 측정할 수 있습니다.\n* [`<StrictMode>`](/reference/react/StrictMode)를 통해 초기에 버그를 찾는 데 도움이 되는 추가 개발 전용 검사를 사용할 수 있습니다.\n* [`<Suspense>`](/reference/react/Suspense)를 통해 자식 컴포넌트를 로딩하는 동안 Fallback을 표시할 수 있습니다.\n* [`<Activity>`](/reference/react/Activity) lets you hide and restore the UI and internal state of its children.\n\n---\n\n## 자신만의 컴포넌트 {/*your-own-components*/}\n\n자바스크립트 [함수로 자신만의 컴포넌트를 정의](/learn/your-first-component)할 수도 있습니다.\n"
  },
  {
    "path": "src/content/reference/react/createContext.md",
    "content": "---\ntitle: createContext\n---\n\n<Intro>\n\n`createContext`를 사용하면 컴포넌트가 [Context](/learn/passing-data-deeply-with-context)를 제공하거나 읽을 수 있습니다.\n\n\n```js\nconst SomeContext = createContext(defaultValue)\n```\n\n</Intro>\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `createContext(defaultValue)` {/*createcontext*/}\n\n컴포넌트 외부에서 `createContext`를 호출하여 컨텍스트를 생성합니다.\n\n```js\nimport { createContext } from 'react';\n\nconst ThemeContext = createContext('light');\n```\n\n[아래 예시를 참고하세요.](#usage)\n\n#### 매개변수 {/*parameters*/}\n\n* `defaultValue`: 컴포넌트가 컨텍스트를 읽을 때 상위에 일치하는 컨텍스트 제공자가 없는 경우 컨텍스트가 가져야 할 값입니다. 의미 있는 기본값이 없으면 `null`을 지정하세요. 기본값은 \"최후의 수단\"으로 사용됩니다. 이 값은 정적이며 시간이 지나도 변경되지 않습니다.\n\n#### 반환값 {/*returns*/}\n\n`createContext`는 컨텍스트 객체를 반환합니다.\n\n**컨텍스트 객체 자체는 어떠한 정보도 가지고 있지 않습니다.** 다른 컴포넌트가 읽거나 제공하는 어떤 컨텍스트를 나타냅니다. 일반적으로 상위 컴포넌트에서 컨텍스트 값을 지정하기 위해 [`SomeContext`](#provider)를 사용하고, 아래 컴포넌트에서 읽기 위해 [`useContext(SomeContext)`](/reference/react/useContext)를 호출합니다. 컨텍스트 객체에는 몇 가지 속성이 있습니다.\n\n* `SomeContext`는 컴포넌트에게 컨텍스트 값을 제공합니다.\n* `SomeContext.Consumer`는 컨텍스트 값을 읽는 대안이며 드물게 사용됩니다.\n* `SomeContext.Provider`는 React 19 이전 버전에서 사용되는 오래된 컨텍스트 값 제공 방법입니다.\n\n---\n\n### `SomeContext` Provider {/*provider*/}\n\n컴포넌트를 컨텍스트 제공자<sup>Provider</sup>로 감싸서 이 컨텍스트의 값을 모든 내부 컴포넌트에 지정합니다.\n\n```js\nfunction App() {\n  const [theme, setTheme] = useState('light');\n  // ...\n  return (\n    <ThemeContext value={theme}>\n      <Page />\n    </ThemeContext>\n  );\n}\n```\n\n<Note>\n\nReact 19부터는 `<SomeContext>`를 제공자<sup>Provider</sup>로 렌더링 할 수 있습니다.\n\n오래된 React 버전은 `<SomeContext.Provider>`를 사용합니다.\n\n</Note>\n\n#### Props {/*provider-props*/}\n\n* `value`: 이 제공자 내부의 컨텍스트를 읽는 모든 컴포넌트에 전달하려는 값입니다. 컨텍스트 값은 어떠한 유형이든 될 수 있습니다. 제공자 내부에서 [`useContext(SomeContext)`](/reference/react/useContext)를 호출하는 컴포넌트는 그 위의 가장 가까운 해당 컨텍스트 제공자의 `value`를 받게 됩니다.\n\n---\n\n### `SomeContext.Consumer` {/*consumer*/}\n\n`useContext`가 등장하기 전에 컨텍스트를 읽는 이전 방식이 있었습니다.\n\n```js\nfunction Button() {\n  // 🟡 이전 방식 (권장하지 않음)\n  return (\n    <ThemeContext.Consumer>\n      {theme => (\n        <button className={theme} />\n      )}\n    </ThemeContext.Consumer>\n  );\n}\n```\n\n이 예전 방식은 여전히 작동하지만, **새로 작성된 코드는 대신 [`useContext()`](/reference/react/useContext)로 컨텍스트를 읽어야 합니다**.\n\n```js\nfunction Button() {\n  // ✅ 권장하는 방법\n  const theme = useContext(ThemeContext);\n  return <button className={theme} />;\n}\n```\n\n#### Props {/*consumer-props*/}\n\n* `children`: 함수입니다. React는 [`useContext()`](/reference/react/useContext)와 동일한 알고리즘으로 결정된 현재 컨텍스트 값을 전달하여 함수를 호출하고, 이 함수에서 반환하는 결과를 렌더링합니다. 부모 컴포넌트에서 컨텍스트가 변경되면 React는 이 함수를 다시 실행하고 UI를 업데이트합니다.\n\n---\n\n## 사용법 {/*usage*/}\n\n### 컨텍스트 생성 {/*creating-context*/}\n\n컨텍스트를 사용하면 컴포넌트가 [정보를 깊게 전달](/learn/passing-data-deeply-with-context)할 수 있습니다.\n\n컴포넌트 외부에서 `createContext`를 호출하여 하나 이상의 컨텍스트를 생성합니다.\n\n```js [[1, 3, \"ThemeContext\"], [1, 4, \"AuthContext\"], [3, 3, \"'light'\"], [3, 4, \"null\"]]\nimport { createContext } from 'react';\n\nconst ThemeContext = createContext('light');\nconst AuthContext = createContext(null);\n```\n\n`createContext`는 <CodeStep step={1}>컨텍스트 객체</CodeStep>를 반환합니다. 컴포넌트는 이를 [`useContext()`](/reference/react/useContext)에 전달하여 컨텍스트를 읽을 수 있습니다.\n\n```js [[1, 2, \"ThemeContext\"], [1, 7, \"AuthContext\"]]\nfunction Button() {\n  const theme = useContext(ThemeContext);\n  // ...\n}\n\nfunction Profile() {\n  const currentUser = useContext(AuthContext);\n  // ...\n}\n```\n\n기본적으로, 그들이 받는 값은 컨텍스트를 생성할 때 지정한 <CodeStep step={3}>기본값</CodeStep>이 됩니다. 그러나 자체적으로 이는 유용하지 않습니다. 왜냐하면 기본값은 절대 변경되지 않기 때문입니다.\n\n컨텍스트는 **다른 동적 값들을 컴포넌트에서 제공**할 수 있기 때문에 유용합니다.\n\n\n```js {8-9,11-12}\nfunction App() {\n  const [theme, setTheme] = useState('dark');\n  const [currentUser, setCurrentUser] = useState({ name: 'Taylor' });\n\n  // ...\n\n  return (\n    <ThemeContext value={theme}>\n      <AuthContext value={currentUser}>\n        <Page />\n      </AuthContext>\n    </ThemeContext>\n  );\n}\n```\n\n이제 `Page` 컴포넌트와 그 안의 모든 컴포넌트, 얼마나 깊든지 간에 전달된 컨텍스트 값을 \"볼\" 수 있습니다. 전달된 컨텍스트 값이 변경되면, React는 컨텍스트를 읽는 컴포넌트를 다시 렌더링합니다.\n\n[컨텍스트를 읽고 제공하는 방법에 대해 더 알아보고 예시를 확인하세요.](/reference/react/useContext)\n\n---\n\n### 파일에서 컨텍스트 가져오기 및 내보내기 {/*importing-and-exporting-context-from-a-file*/}\n\n종종 서로 다른 파일에 있는 컴포넌트들이 동일한 컨텍스트에 접근해야 할 때가 있습니다. 이것이 별도의 파일에서 컨텍스트를 선언하는 것이 일반적인 이유입니다. 그런 다음 [`export` 문](https://developer.mozilla.org/en-US/docs/web/javascript/reference/statements/export)을 사용하여 다른 파일에서 사용할 수 있도록 컨텍스트를 사용할 수 있습니다.\n\n```js {4-5}\n// Contexts.js\nimport { createContext } from 'react';\n\nexport const ThemeContext = createContext('light');\nexport const AuthContext = createContext(null);\n```\n\n다른 파일에서 선언된 컴포넌트는 이 컨텍스트를 읽거나 제공하기 위해 [`import`](https://developer.mozilla.org/en-US/docs/web/javascript/reference/statements/import) 문을 사용할 수 있습니다.\n\n```js {2}\n// Button.js\nimport { ThemeContext } from './Contexts.js';\n\nfunction Button() {\n  const theme = useContext(ThemeContext);\n  // ...\n}\n```\n\n```js {2}\n// App.js\nimport { ThemeContext, AuthContext } from './Contexts.js';\n\nfunction App() {\n  // ...\n  return (\n    <ThemeContext value={theme}>\n      <AuthContext value={currentUser}>\n        <Page />\n      </AuthContext>\n    </ThemeContext>\n  );\n}\n```\n\n이것은 [컴포넌트 `import` 및 `export`하기](/learn/importing-and-exporting-components)와 유사하게 동작합니다.\n\n---\n\n## 문제 해결 {/*troubleshooting*/}\n\n### 컨텍스트 값을 바꾸는 방법을 모르겠어요 {/*i-cant-find-a-way-to-change-the-context-value*/}\n\n이런 코드는 *기본* 컨텍스트 값을 지정합니다.\n\n```js\nconst ThemeContext = createContext('light');\n```\n\n이 값은 절대 변경되지 않습니다. React는 상위에 일치하는 제공자를 찾을 수 없는 경우에만 이 값을 기본값으로 사용합니다.\n\n컨텍스트가 시간에 따라 변경되도록 만들려면, [State를 추가하고 컴포넌트를 컨텍스트 제공자로 감싸세요.](/reference/react/useContext#updating-data-passed-via-context)\n"
  },
  {
    "path": "src/content/reference/react/createElement.md",
    "content": "---\ntitle: createElement\n---\n\n<Intro>\n\n`createElement`를 사용하면 React 엘리먼트를 생성할 수 있습니다. [JSX](/learn/writing-markup-with-jsx)를 작성하는 대신 사용할 수 있습니다.\n```js\nconst element = createElement(type, props, ...children)\n```\n\n</Intro>\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `createElement(type, props, ...children)` {/*createelement*/}\n\n`type`, `prop`, `children`를 인수로 제공하고 `createElement`을 호출하여 React 엘리먼트를 생성합니다.\n\n\n```js\nimport { createElement } from 'react';\n\nfunction Greeting({ name }) {\n  return createElement(\n    'h1',\n    { className: 'greeting' },\n    'Hello'\n  );\n}\n```\n\n[아래에서 더 많은 예시를 확인하세요.](#usage)\n\n#### 매개변수 {/*parameters*/}\n\n* `type`: `type` 인수는 유효한 React 컴포넌트여야 합니다. 예를 들어 태그 이름 문자열 (예: 'div', 'span') 또는 React 컴포넌트(함수, 클래스, [`Fragment`](/reference/react/Fragment) 같은 특수 컴포넌트)가 될 수 있습니다.\n\n* `props`: `props` 인수는 객체 또는 `null`이어야 합니다. `null`을 전달하면 빈 객체와 동일하게 처리됩니다. React는 전달한 `props`와 일치하는 프로퍼티를 가진 엘리먼트를 생성합니다. 전달한 `props` 객체의 `ref`와 `key`는 특수하기 때문에 생성한 `element`에서 `element.props.ref` 와 `element.props.key`는 사용할 수 *없다*는 점에 유의하세요. `element.ref` 또는 `element.key`로 사용할 수 있습니다.\n\n* **선택사항** `...children`: 0개 이상의 자식 노드. React 엘리먼트, 문자열, 숫자, [포탈](/reference/react-dom/createPortal), 빈 노드(`null`, `undefined`, `true`, `false`) 그리고 React 노드 배열을 포함한 모든 React 노드가 될 수 있습니다.\n\n#### 반환값 {/*returns*/}\n\n`createElement`는 아래 프로퍼티를 가지는 React 엘리먼트 객체를 반환합니다.\n\n* `type`: 전달받은 `type`.\n* `props`: `ref`와 `key`를 제외한 전달받은 `props`.\n* `ref`: 전달받은 `ref`. 누락된 경우 `null`.\n* `key`: 전달받은 `key`를 강제 변환한 문자열. 누락된 경우 `null`.\n\n일반적으로 엘리먼트는 컴포넌트에서 반환되거나 다른 엘리먼트의 자식으로 만듭니다. 엘리먼트의 프로퍼티에는 접근할 수 있지만, 엘리먼트 생성 후에는 모든 엘리먼트에 접근할 수 없는 것처럼 대하고 렌더링만 하는 것이 좋습니다.\n\n#### 주의 사항 {/*caveats*/}\n\n* 반드시 **React 엘리먼트와 그 프로퍼티는 [불변](https://en.wikipedia.org/wiki/Immutable_object)하게 취급**해야하며 엘리먼트 생성 후에는 그 내용이 변경되어선 안 됩니다. 개발환경에서 React는 이를 강제하기 위해 반환된 엘리먼트와 그 프로퍼티를 얕게 [freeze](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze)합니다.\n\n* JSX를 사용한다면 **태그를 대문자로 시작해야만 사용자 컴포넌트를 렌더링할 수 있습니다.** 즉, `<Something />`은 `createElement(Something)`과 동일하지만 `<something />`(소문자) 은 `createElement('something')`와 동일합니다. (문자열임을 주의하세요. 내장된 HTML 태그로 취급됩니다.)\n\n* `createElement('h1', {}, child1, child2, child3)`와 같이 **`children`이 모두 정적인 경우에만 `createElement`에 여러 인수로 전달해야 합니다.** `children`이 동적이라면 전체 배열을 세 번째 인수로 전달해야 합니다. 이렇게 하면 React는 [누락된 `키`에 대한 경고](/learn/rendering-lists#keeping-list-items-in-order-with-key)를 표시합니다. 정적 목록인 경우 재정렬하지 않기 때문에 작업이 필요하지 않습니다.\n\n---\n\n## 사용법 {/*usage*/}\n\n### JSX 없이 엘리먼트 생성하기 {/*creating-an-element-without-jsx*/}\n\n[JSX](/learn/writing-markup-with-jsx)가 마음에 들지 않거나 프로젝트에서 사용할 수 없는 경우, `createElement`를 대안으로 사용할 수 있습니다.\n\nJSX 없이 엘리먼트를 생성하려면  <CodeStep step={1}>type</CodeStep>, <CodeStep step={2}>props</CodeStep>,  <CodeStep step={3}>children</CodeStep>와 함께 `createElement`를 호출합니다\n\n```js [[1, 5, \"'h1'\"], [2, 6, \"{ className: 'greeting' }\"], [3, 7, \"'Hello ',\"], [3, 8, \"createElement('i', null, name),\"], [3, 9, \"'. Welcome!'\"]]\nimport { createElement } from 'react';\n\nfunction Greeting({ name }) {\n  return createElement(\n    'h1',\n    { className: 'greeting' },\n    'Hello ',\n    createElement('i', null, name),\n    '. Welcome!'\n  );\n}\n```\n\n<CodeStep step={3}>children</CodeStep>은 선택 사항이며 필요한 만큼 전달할 수 있습니다. (위 예시에는 3개의 children이 있습니다.) 위 코드는 인사말이 포함된 `<h1>`를 표시합니다. 비교를 위해 동일한 예시를 JSX로 재작성했습니다.\n\n\n```js [[1, 3, \"h1\"], [2, 3, \"className=\\\\\"greeting\\\\\"\"], [3, 4, \"Hello <i>{name}</i>. Welcome!\"], [1, 5, \"h1\"]]\nfunction Greeting({ name }) {\n  return (\n    <h1 className=\"greeting\">\n      Hello <i>{name}</i>. Welcome!\n    </h1>\n  );\n}\n```\n\n자신만의 React 컴포넌트를 렌더링하려면 `'h1'` 같은 문자열 대신 `Greeting` 같은 함수를 <CodeStep step={1}>type</CodeStep>에 전달하세요.\n\n```js [[1, 2, \"Greeting\"], [2, 2, \"{ name: 'Taylor' }\"]]\nexport default function App() {\n  return createElement(Greeting, { name: 'Taylor' });\n}\n```\n\nJSX를 사용하면 다음과 같습니다.\n\n```js [[1, 2, \"Greeting\"], [2, 2, \"name=\\\\\"Taylor\\\\\"\"]]\nexport default function App() {\n  return <Greeting name=\"Taylor\" />;\n}\n```\n\n\n`createElement`를 사용하여 작성한 전체 예시입니다.\n\n<Sandpack>\n\n```js\nimport { createElement } from 'react';\n\nfunction Greeting({ name }) {\n  return createElement(\n    'h1',\n    { className: 'greeting' },\n    'Hello ',\n    createElement('i', null, name),\n    '. Welcome!'\n  );\n}\n\nexport default function App() {\n  return createElement(\n    Greeting,\n    { name: 'Taylor' }\n  );\n}\n```\n\n```css\n.greeting {\n  color: darkgreen;\n  font-family: Georgia;\n}\n```\n\n</Sandpack>\n\nJSX를 사용하여 작성한 전체 예시입니다.\n\n\n<Sandpack>\n\n```js\nfunction Greeting({ name }) {\n  return (\n    <h1 className=\"greeting\">\n      Hello <i>{name}</i>. Welcome!\n    </h1>\n  );\n}\n\nexport default function App() {\n  return <Greeting name=\"Taylor\" />;\n}\n```\n\n```css\n.greeting {\n  color: darkgreen;\n  font-family: Georgia;\n}\n```\n\n</Sandpack>\n\n두 코딩 스타일 모두 허용되므로 프로젝트에 맞는 스타일을 사용하면 됩니다. `createElement`와 비교하여 JSX를 사용할 때의 장점은 어떤 닫는 태그가 어떤 여는 태그에 대응되는지 쉽게 확인할 수 있다는 것입니다.\n\n<DeepDive>\n\n#### React 엘리먼트란 정확히 무엇인가요? {/*what-is-a-react-element-exactly*/}\n\n엘리먼트는 사용자 인터페이스의 일부에 대한 표현입니다. 예를 들어 `<Greeting name=\"Taylor\" />`와 `createElement(Greeting, { name: 'Taylor' })`는 모두 다음과 같은 객체를 생성합니다.\n\n```js\n// 약간 단순화됨\n{\n  type: Greeting,\n  props: {\n    name: 'Taylor'\n  },\n  key: null,\n  ref: null,\n}\n```\n\n**이 객체를 생성해도 `Greeting` 컴포넌트가 렌더링 되거나 DOM 엘리먼트가 생성되지는 않는다는 점을 주의하세요.**\n\nReact 엘리먼트는 나중에 React가 `Greeting` 컴포넌트를 렌더링하도록 지시하는 설명서와 비슷합니다. `App` 컴포넌트에서 이 객체를 반환함으로써 React에게 다음 할 일을 지시할 수 있습니다.\n\n엘리먼트 생성 비용은 매우 저렴하므로 엘리먼트 생성을 최적화하거나 피하려고 노력할 필요가 없습니다.\n</DeepDive>\n"
  },
  {
    "path": "src/content/reference/react/createFactory.md",
    "content": "---\ntitle: createFactory\n---\n\n<Deprecated>\n\n이 API는 향후 React의 주요 버전에서 제거될 예정입니다. [대안들을 살펴보세요.](#alternatives)\n\n</Deprecated>\n\n<Intro>\n\n`createFactory`는 특정 type의 React 엘리먼트를 만드는 함수를 생성합니다.\n\n```js\nconst factory = createFactory(type)\n```\n\n</Intro>\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `createFactory(type)` {/*createfactory*/}\n\n주어진 `type`의 React 엘리먼트를 만들어 내는 팩토리 함수를 생성하기 위해 `createFactory(type)`를 호출하세요.\n\n```js\nimport { createFactory } from 'react';\n\nconst button = createFactory('button');\n```\n\n이후 JSX 없이 React 엘리먼트를 만들기 위해, 해당 함수를 사용할 수 있습니다.\n\n```js\nexport default function App() {\n  return button({\n    onClick: () => {\n      alert('Clicked!')\n    }\n  }, 'Click me');\n}\n```\n\n[아래에서 더 많은 사용법을 확인하세요.](#usage)\n\n#### 매개변수 {/*parameters*/}\n\n* `type`: `type`은 반드시 유효한 React 컴포넌트 type이어야 합니다. 예를 들어 태그 이름 문자열(`'div'` 나 `'span'`) 혹은 React 컴포넌트(함수 컴포넌트, 클래스 컴포넌트, [`Fragment`](/reference/react/Fragment)와 같은 특별한 컴포넌트)가 될 수 있습니다.\n\n#### 반환값 {/*returns*/}\n\n팩토리 함수를 반환합니다. 이 함수는 자식 인수의 리스트에 뒤이어, 첫 번째 인수로 `props` 객체를 받으며, 주어진 `types`, `props` 그리고 `자식`을 가진 React 엘리먼트를 반환합니다.\n\n---\n\n## 사용법 {/*usage*/}\n\n### 팩토리 함수로 React 엘리먼트 만들기 {/*creating-react-elements-with-a-factory*/}\n\n비록 대부분의 React 프로젝트들은 [JSX](/learn/writing-markup-with-jsx)를 사용하여 사용자 인터페이스를 표현하지만, JSX가 필수는 아닙니다. 과거에는 `createFactory`를 JSX 없이 사용자 인터페이스를 표현하는 방법의 하나로 사용하였습니다.\n\n`button`과 같이 특정 엘리먼트 type을 반환하는 *팩토리 함수*를 생성하기 위해 `createFactory`를 호출합니다.\n\n```js\nimport { createFactory } from 'react';\n\nconst button = createFactory('button');\n```\n\n그 다음, 제공된 props와 자식으로 React 엘리먼트를 만들어내는 팩토리 함수를 실행합니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { createFactory } from 'react';\n\nconst button = createFactory('button');\n\nexport default function App() {\n  return button({\n    onClick: () => {\n      alert('Clicked!')\n    }\n  }, 'Click me');\n}\n```\n\n</Sandpack>\n\n이는 `createFactory`을 JSX의 대안으로 사용하는 방법입니다. 하지만 `createFactory`는 더 이상 사용하지 않으며, 이후 새로운 코드를 작성할 때 `createFactory`를 사용하지 않아야 합니다. 아래에서 `createFactory` 대신 다른 방법을 사용하는 방식을 살펴보세요.\n\n---\n\n## 대안 {/*alternatives*/}\n\n### 프로젝트에 `createFactory` 복사하기 {/*copying-createfactory-into-your-project*/}\n\n만약 프로젝트에 `createFactory`가 많이 사용된다면, 다음의 `createFactory.js` 내용을 프로젝트 내부에서 사용할 수 있도록 복사하세요.\n\n<Sandpack>\n\n```js src/App.js\nimport { createFactory } from './createFactory.js';\n\nconst button = createFactory('button');\n\nexport default function App() {\n  return button({\n    onClick: () => {\n      alert('Clicked!')\n    }\n  }, 'Click me');\n}\n```\n\n```js src/createFactory.js\nimport { createElement } from 'react';\n\nexport function createFactory(type) {\n  return createElement.bind(null, type);\n}\n```\n\n</Sandpack>\n\n이러한 작업을 통해, import 문을 제외하고 다른 코드를 바꾸지 않은 상태로 유지할 수 있습니다.\n\n---\n\n### `createFactory`를 `createElement`로 대체하기 {/*replacing-createfactory-with-createelement*/}\n\n직접 옮겨와도 무방할 정도로 `createFactory`를 몇 개만 호출하고 있고 JSX를 사용하고 싶지 않다면, [`createElement`](/reference/react/createElement)를 실행하여 팩토리 함수를 대체할 수 있습니다. 예를 들어 이 코드는,\n\n\n```js {1,3,6}\nimport { createFactory } from 'react';\n\nconst button = createFactory('button');\n\nexport default function App() {\n  return button({\n    onClick: () => {\n      alert('Clicked!')\n    }\n  }, 'Click me');\n}\n```\n\n아래와 같이 바꿀 수 있습니다.\n\n\n```js {1,4}\nimport { createElement } from 'react';\n\nexport default function App() {\n  return createElement('button', {\n    onClick: () => {\n      alert('Clicked!')\n    }\n  }, 'Click me');\n}\n```\n\n최종적으로 JSX 없이 React를 사용하는 예시입니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { createElement } from 'react';\n\nexport default function App() {\n  return createElement('button', {\n    onClick: () => {\n      alert('Clicked!')\n    }\n  }, 'Click me');\n}\n```\n\n</Sandpack>\n\n---\n\n### `createFactory`를 JSX로 대체하기 {/*replacing-createfactory-with-jsx*/}\n\n마지막으로 `createFactory` 대신 JSX를 사용할 수 있습니다. 이는 React를 사용하기 위해 가장 흔하게 사용하는 방법입니다.\n\n<Sandpack>\n\n```js src/App.js\nexport default function App() {\n  return (\n    <button onClick={() => {\n      alert('Clicked!');\n    }}>\n      Click me\n    </button>\n  );\n};\n```\n\n</Sandpack>\n\n<Pitfall>\n\n`button`과 같은 상수 대신 `type`을 특정 변수로 사용할 수도 있습니다.\n\n```js {3}\nfunction Heading({ isSubheading, ...props }) {\n  const type = isSubheading ? 'h2' : 'h1';\n  const factory = createFactory(type);\n  return factory(props);\n}\n```\n\nJSX를 사용해 같은 방식으로 구현한다면 `Type`처럼 대문자로 시작하는 변수 이름을 새롭게 설정해야 합니다.\n\n```js {2,3}\nfunction Heading({ isSubheading, ...props }) {\n  const Type = isSubheading ? 'h2' : 'h1';\n  return <Type {...props} />;\n}\n```\n\n그렇게 하지 않는 경우 React는 소문자로 작성된 `<type>`을 내장된 HTML 태그로 해석할 것입니다.\n\n</Pitfall>\n"
  },
  {
    "path": "src/content/reference/react/createRef.md",
    "content": "---\ntitle: createRef\n---\n\n<Pitfall>\n\n`createRef`는 주로 [클래스 컴포넌트](/reference/react/Component)에 사용됩니다. 일반적으로 함수 컴포넌트는 [`useRef`](/reference/react/useRef)를 대신 사용합니다.\n\n</Pitfall>\n\n<Intro>\n\n`createRef`는 임의의 값을 포함할 수 있는 [ref](/learn/referencing-values-with-refs) 객체를 생성합니다.\n\n```js\nclass MyInput extends Component {\n  inputRef = createRef();\n  // ...\n}\n```\n\n</Intro>\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `createRef()` {/*createref*/}\n\n[ref](/learn/referencing-values-with-refs)를 [클래스 컴포넌트](/reference/react/Component) 안에 선언하려면 `createRef`를 호출합니다.\n\n```js\nimport { createRef, Component } from 'react';\n\nclass MyComponent extends Component {\n  intervalRef = createRef();\n  inputRef = createRef();\n  // ...\n```\n\n[아래에서 더 많은 예시를 볼 수 있습니다.](#usage)\n\n#### 매개변수 {/*parameters*/}\n\n`createRef`는 매개변수를 받지 않습니다.\n\n#### 반환값 {/*returns*/}\n\ncreateRef`는 단일 속성을 가진 객체를 반환합니다.\n\n* `current`: 처음에는 `null`로 설정됩니다. 이를 나중에 다른 것으로 설정할 수 있습니다. ref 객체를 JSX 노드의 `ref` 어트리뷰트로 React에 전달하면 React는 이를 `current` 프로퍼티로 설정합니다.\n\n#### 주의 사항 {/*caveats*/}\n\n* `createRef`는 항상 *다른* 객체를 반환합니다. 이는 `{ current: null }`을 직접 작성하는 것과 같습니다.\n* 함수 컴포넌트에서는 항상 동일한 객체를 반환하는 [`useRef`](/reference/react/useRef)를 대신 사용할 수 있습니다.\n* `const ref = useRef()`는 `const [ref, _] = useState(() => createRef(null))`와 동일합니다.\n\n---\n\n## 사용법 {/*usage*/}\n\n### 클래스 컴포넌트에서 ref 선언하기 {/*declaring-a-ref-in-a-class-component*/}\n\n [클래스 컴포넌트](/reference/react/Component)내에서 참조를 선언하려면 `createRef`를 호출하고 그 결과를 클래스 필드에 할당합니다.\n\n```js {4}\nimport { Component, createRef } from 'react';\n\nclass Form extends Component {\n  inputRef = createRef();\n\n  // ...\n}\n```\n\n이제 `ref={this.inputRef}`를 JSX에 있는 `<input>`에 전달하면, React는 `this.inputRef.current`를 input DOM 노드가 차지하게 합니다. 예를 들어 input에 포커싱하는 버튼을 만드는 방법은 다음과 같습니다.\n\n<Sandpack>\n\n```js\nimport { Component, createRef } from 'react';\n\nexport default class Form extends Component {\n  inputRef = createRef();\n\n  handleClick = () => {\n    this.inputRef.current.focus();\n  }\n\n  render() {\n    return (\n      <>\n        <input ref={this.inputRef} />\n        <button onClick={this.handleClick}>\n          input에 포커스\n        </button>\n      </>\n    );\n  }\n}\n```\n\n</Sandpack>\n\n<Pitfall>\n\n`createRef`는 주로 [클래스 컴포넌트](/reference/react/Component)에 사용됩니다. 일반적으로 함수 컴포넌트는 [`useRef`](/reference/react/useRef)를 대신 사용합니다.\n\n</Pitfall>\n\n---\n\n## 대안 {/*alternatives*/}\n\n### `createRef`를 사용하는 클래스에서 `useRef`를 사용하는 함수로 마이그레이션하기 {/*migrating-from-a-class-with-createref-to-a-function-with-useref*/}\n\n새로운 코드를 작성한다면 [클래스 컴포넌트](/reference/react/Component) 대신 함수 컴포넌트를 사용하는 것을 추천합니다. `createRef`를 사용하는 기존 클래스 컴포넌트가 있는 경우, 이를 변환하는 방법은 다음과 같습니다. 다음은 원본 코드입니다.\n\n<Sandpack>\n\n```js\nimport { Component, createRef } from 'react';\n\nexport default class Form extends Component {\n  inputRef = createRef();\n\n  handleClick = () => {\n    this.inputRef.current.focus();\n  }\n\n  render() {\n    return (\n      <>\n        <input ref={this.inputRef} />\n        <button onClick={this.handleClick}>\n          input에 포커스\n        </button>\n      </>\n    );\n  }\n}\n```\n\n</Sandpack>\n\n[이 컴포넌트를 클래스에서 함수로 변환하려면,](/reference/react/Component#alternatives) `createRef` 호출을 [`useRef`](/reference/react/useRef) 호출로 바꿔줍니다.\n\n<Sandpack>\n\n```js\nimport { useRef } from 'react';\n\nexport default function Form() {\n  const inputRef = useRef(null);\n\n  function handleClick() {\n    inputRef.current.focus();\n  }\n\n  return (\n    <>\n      <input ref={inputRef} />\n      <button onClick={handleClick}>\n        input에 포커스\n      </button>\n    </>\n  );\n}\n```\n\n</Sandpack>\n"
  },
  {
    "path": "src/content/reference/react/experimental_taintObjectReference.md",
    "content": "---\ntitle: experimental_taintObjectReference\nversion: experimental\n---\n\n<Experimental>\n\n**이 API는 실험적이며 React 안정 버전에서는 아직 사용할 수 없습니다.**\n\n이 API를 사용하려면 React 패키지를 가장 최신의 실험적인 버전으로 업그레이드해야 합니다.\n\n- `react@experimental`\n- `react-dom@experimental`\n- `eslint-plugin-react-hooks@experimental`\n\n실험적인 버전의 React에는 버그가 있을 수 있습니다. 프로덕션에서는 사용하지 마세요.\n\n이 API는 [React 서버 컴포넌트](/reference/rsc/server-components)에서만 사용할 수 있습니다.\n\n</Experimental>\n\n\n<Intro>\n\n`taintObjectReference`를 사용하면 `user` 객체와 같은 특정한 객체 인스턴스를 클라이언트 컴포넌트로 전송하는 것을 방지할 수 있습니다.\n\n```js\nexperimental_taintObjectReference(message, object);\n```\n키, 해시 또는 토큰이 전달되는 것을 방지하는 방법은 [`taintUniqueValue`](/reference/react/experimental_taintUniqueValue)를 참고하세요.\n\n</Intro>\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `taintObjectReference(message, object)` {/*taintobjectreference*/}\n\n클라이언트로 전달되지 않아야 할 객체를 `taintObjectReference`와 함께 호출하여 React에 등록합니다.\n\n```js\nimport {experimental_taintObjectReference} from 'react';\n\nexperimental_taintObjectReference(\n  '환경 변수는 클라이언트로 전달하지 마세요.',\n  process.env\n);\n```\n\n[아래 예시를 참고하세요.](#usage)\n\n#### 매개변수 {/*parameters*/}\n\n* `message`: 객체가 클라이언트 컴포넌트로 전달될 때 표시할 메시지. 객체가 클라이언트 컴포넌트로 전달될 때 발생하는 오류 객체에 포함되어 나타나는 메시지입니다.\n\n* `object`: 오염(taint)될 객체. 함수와 클래스 인스턴스도 `object`로서 `taintObjectReference`에 전달될 수 있습니다. 함수와 클래스는 클라이언트 컴포넌트로 전달되지 않도록 이미 막혀있지만 React의 기본 오류 메시지 대신 `message`에 설정한 메시지를 보여줄 수 있습니다. 타입 배열<sup>Typed Array</sup>의 인스턴스를 `object`로서 `taintObjectReference`에 전달하면 같은 타입 배열의 다른 인스턴스가 오염되지 않습니다.\n\n#### 반환값 {/*returns*/}\n\n`experimental_taintObjectReference`는 `undefined`를 반환합니다.\n\n#### 주의 사항 {/*caveats*/}\n\n- 오염된 객체를 다시 작성하거나 복제하면 오염되지 않은 객체가 새로 만들어집니다. 새로 만들어진 객체는 민감한 데이터를 포함할 수 있습니다. 예를 들어, 오염된 `user` 객체가 있다고 할 때, `const userInfo = {name: user.name, ssn: user.ssn}` 혹은 `{...user}`를 실행하면 오염되지 않은 새로운 객체를 작성합니다. `taintObjectReference`는 객체가 변경되지 않은 상태에서 클라이언트 컴포넌트로 그대로 전달되는 것만 방지합니다.\n\n<Pitfall>\n\n**보안을 오염<sup>Tainting</sup>에만 의존하지 마세요.** 객체를 오염시켰다고 해서 모든 누출 가능성을 막을 수는 없습니다. 예를 들어 오염된 객체를 복제하면 오염되지 않은 새로운 객체가 만들어집니다. 오염된 객체에서 가져온 데이터를 사용하여(예: `{secret: taintedObj.secret}`) 작성된 새 값이나 객체는 오염되지 않습니다. 오염은 한 겹의 보호 장치일 뿐입니다. 보안성이 높은 애플리케이션은 여러 겹의 보호 장치와 잘 설계된 API를 마련해 두고 격리 패턴을 따릅니다.\n\n</Pitfall>\n\n---\n\n## 사용법 {/*usage*/}\n\n### 사용자 데이터가 의도치 않게 클라이언트로 전달되는 것을 방지하기 {/*prevent-user-data-from-unintentionally-reaching-the-client*/}\n\n클라이언트 컴포넌트에는 민감한 데이터를 담은 객체가 전달되어서는 안 됩니다. 이상적으로, 데이터 가져오기<sup>Data Fetching</sup> 함수는 현재 사용자가 접근할 수 없는 데이터를 노출하면 안 됩니다. 하지만 리팩토링 도중 가끔 실수가 발생하기도 합니다. 데이터 API에서 사용자 객체를 \"오염<sup>Taint</sup>\"시켜서 이러한 실수를 방지할 수 있습니다.\n\n```js\nimport {experimental_taintObjectReference} from 'react';\n\nexport async function getUser(id) {\n  const user = await db`SELECT * FROM users WHERE id = ${id}`;\n  experimental_taintObjectReference(\n    'user 객체 전체를 클라이언트로 전달하지 마세요.' +\n      '필요하다면 일부 특정한 프로퍼티만 뽑아서 사용하는 것이 좋습니다.',\n    user,\n  );\n  return user;\n}\n```\n\n이제 누군가 이 객체를 클라이언트 컴포넌트로 전달하려고 하면 전달된 오류 메시지와 함께 오류가 발생합니다.\n\n<DeepDive>\n\n#### 데이터 가져오기에서 누출 방지하기 {/*protecting-against-leaks-in-data-fetching*/}\n\n민감한 데이터에 접근할 수 있는 서버 컴포넌트 환경을 실행하고 있다면 객체를 그대로 전달할 때 주의를 기울여야 합니다.\n\n```js\n// api.js\nexport async function getUser(id) {\n  const user = await db`SELECT * FROM users WHERE id = ${id}`;\n  return user;\n}\n```\n\n```js\nimport { getUser } from 'api.js';\nimport { InfoCard } from 'components.js';\n\nexport async function Profile(props) {\n  const user = await getUser(props.userId);\n  // DO NOT DO THIS\n  return <InfoCard user={user} />;\n}\n```\n\n```js\n// components.js\n\"use client\";\n\nexport async function InfoCard({ user }) {\n  return <div>{user.name}</div>;\n}\n```\n\n이상적으로, `getUser`는 현재 사용자가 접근할 수 없는 데이터를 노출하지 않아야 합니다. `user` 객체가 클라이언트 컴포넌트로 전달되는 것을 방지하려면 사용자 객체를 \"오염<sup>Taint</sup>\"시켜야 합니다.\n\n```js\n// api.js\nimport {experimental_taintObjectReference} from 'react';\n\nexport async function getUser(id) {\n  const user = await db`SELECT * FROM users WHERE id = ${id}`;\n  experimental_taintObjectReference(\n    'user 객체 전체를 클라이언트로 전달하지 마세요. ' +\n      '필요하다면 일부 특정한 프로퍼티만 뽑아서 사용하는 것이 좋습니다.',\n    user,\n  );\n  return user;\n}\n```\n\n이제 누군가 `user` 객체를 클라이언트 컴포넌트로 전달하려고 하면 설정한 오류 메시지와 함께 오류가 발생합니다.\n\n</DeepDive>\n"
  },
  {
    "path": "src/content/reference/react/experimental_taintUniqueValue.md",
    "content": "---\ntitle: experimental_taintUniqueValue\nversion: experimental\n---\n\n<Experimental>\n\n**이 API는 실험적이며 React 안정 버전에서는 아직 사용할 수 없습니다.**\n\n이 API를 사용하려면 React 패키지를 가장 최신의 실험적인 버전으로 업그레이드해야 합니다.\n\n- `react@experimental`\n- `react-dom@experimental`\n- `eslint-plugin-react-hooks@experimental`\n\n실험적인 버전의 React에는 버그가 있을 수 있습니다. 프로덕션에서는 사용하지 마세요.\n\n이 API는 [React 서버 컴포넌트](/reference/rsc/server-components)에서만 사용할 수 있습니다.\n\n</Experimental>\n\n\n<Intro>\n\n`taintUniqueValue`를 사용하면 패스워드, 키 또는 토큰과 같은 고유 값을 클라이언트 컴포넌트로 전송하는 것을 방지할 수 있습니다.\n\n```js\ntaintUniqueValue(errMessage, lifetime, value)\n```\n\n민감한 데이터가 포함된 객체가 전달되는 것을 방지하는 방법은 [`taintObjectReference`](/reference/react/experimental_taintObjectReference)를 참고하세요.\n\n</Intro>\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `taintUniqueValue(message, lifetime, value)` {/*taintuniquevalue*/}\n\n클라이언트에 전달되지 않아야 할 패스워드, 토큰, 키, 해시를 `taintUniqueValue`와 함께 호출하여 React에 등록합니다.\n\n```js\nimport {experimental_taintUniqueValue} from 'react';\n\nexperimental_taintUniqueValue(\n  '시크릿 키를 클라이언트로 전달하지 마세요.',\n  process,\n  process.env.SECRET_KEY\n);\n```\n\n[아래 예시를 참고하세요.](#usage)\n\n#### 매개변수 {/*parameters*/}\n\n* `message`: 클라이언트 컴포넌트에 `value`가 전달될 경우 표시하고자 하는 메시지입니다. 이 메시지는 `value`가 클라이언트 컴포넌트에 전달될 경우 발생하는 오류의 일부로 표시됩니다.\n\n* `lifetime`: `value`가 얼마나 오랫동안 오염<sup>Taint</sup> 상태를 유지해야 하는지를 나타내는 객체입니다.`value`는 이 객체가 존재하는 동안 클라이언트 컴포넌트로 전달되지 않도록 차단됩니다. 예를 들어 `globalThis`를 전달하면 앱이 종료될 때까지 값이 차단됩니다. `lifetime`은 일반적으로 `value`를 프로퍼티로 가지는 객체입니다.\n\n* `value`: 문자열, bigint 또는 TypedArray입니다. `value`는 암호화 토큰, 개인 키, 해시, 긴 비밀번호와 같이 높은 엔트로피를 가진 고유한 문자 또는 바이트 시퀀스여야 합니다. `value`는 클라이언트 컴포넌트로 전송되지 않도록 차단됩니다.\n\n#### 반환값 {/*returns*/}\n\n`experimental_taintUniqueValue`는 `undefined`를 반환합니다.\n\n#### 주의 사항 {/*caveats*/}\n\n* 오염된 값을 이용해서 새로운 값을 만들어 내면 오염 보호가 손상될 수 있습니다. 오염된 값을 대문자로 변경하거나, 다른 문자열과 연결하거나, Base64로 변환하거나, 잘라내는 등 기타 유사한 변환을 통해서 새롭게 생성된 값은 `taintUniqueValue`을 명시적으로 호출하지 않으면 오염되지 않습니다.\n* PIN 코드나 전화번호와 같이 복잡도가 낮은 값을 보호하기 위해 `tainUniqueValue`를 사용하지 마세요. 공격자가 요청의 값을 이용하여 암호의 가능한 모든 값을 열거하여 어떤 값이 오염되었는지 추론할 수 있습니다.\n\n---\n\n## 사용법 {/*usage*/}\n\n### 토큰이 클라이언트 구성 요소로 전달되지 않도록 방지하기 {/*prevent-a-token-from-being-passed-to-client-components*/}\n\n패스워드, 세션 토큰 또는 기타 고유 값과 같은 민감한 정보가 실수로 클라이언트 컴포넌트로 전달되지 않도록 `taintUniqueValue` 함수는 보호 레이어을 제공합니다. 값이 오염되면 클라이언트 컴포넌트로 전달하려는 시도는 오류를 발생시킵니다.\n\n`lifetime` 인자는 값이 오염된 상태로 남아 있는 기간을 정의합니다. 오염된 상태로 무기한 유지되어야 하는 값의 경우 [`globalThis`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/globalThis) 또는 `process`와 같은 객체가 `lifetime` 인자로 사용될 수 있습니다. 이 객체들은 앱이 실행되는 전체 기간을 수명으로 가집니다.\n\n```js\nimport {experimental_taintUniqueValue} from 'react';\n\nexperimental_taintUniqueValue(\n  '패스워드를 클라이언트로 전달하지 마세요.',\n  globalThis,\n  process.env.SECRET_KEY\n);\n```\n\n만약 오염된 값의 수명이 객체에 묶여 있다면, `lifetime`은 그 값을 캡슐화하는 객체이어야 합니다. 이렇게 하면 오염된 값이 캡슐화 객체의 수명 동안 보호될 수 있습니다.\n\n```js\nimport {experimental_taintUniqueValue} from 'react';\n\nexport async function getUser(id) {\n  const user = await db`SELECT * FROM users WHERE id = ${id}`;\n  experimental_taintUniqueValue(\n    '세션 토큰을 클라이언트로 전달하지 마세요.',\n    user,\n    user.session.token\n  );\n  return user;\n}\n```\n\n이 예시에서 `user` 객체는 `lifetime` 인수 역할을 합니다. 이 객체가 전역 캐시에 저장되거나 다른 요청에 의해 접근할 수 있다면 세션 토큰은 오염된 상태로 유지됩니다.\n\n<Pitfall>\n\n**보안을 오염에만 의존하지 마세요.** 값을 오염시킨다고 해서 모든 파생 값의 누출이 방지되는 것은 아닙니다. 예를 들어 오염된 문자열을 대문자로 바꾸어 새로운 값을 만들면 오염되지 않은 새로운 값이 만들어집니다.\n\n\n```js\nimport {experimental_taintUniqueValue} from 'react';\n\nconst password = 'correct horse battery staple';\n\nexperimental_taintUniqueValue(\n  '패스워드를 클라이언트로 전달하지 마세요.',\n  globalThis,\n  password\n);\n\nconst uppercasePassword = password.toUpperCase() // `uppercasePassword`는 오염되지 않았습니다.\n```\n\n이 예시에서는 상수 `password`가 오염되어 있습니다. 이러한 `password`에 `toUpperCase`메서드를 사용하여 `uppercasePassword`라는 새로운 값을 만들었습니다. 이렇게 새로 생성된 `uppercasePassword`는 오염되지 않았습니다.\n\n오염되지 않은 새로운 값이 만들어지는 다른 유사한 방법에는 오염된 값을 다른 문자열과 연결하거나, Base64로 변환하거나, 잘라내는 것이 있습니다.\n\n오염은 비밀 값을 클라이언트에 전달하는 것과 같이 단순한 실수만 방지합니다. `lifetime` 객체 없이 React 외부의 전역 스토어를 사용하는 것과 같이 `taintUniqueValue`를 호출하는 실수는 오염된 값을 오염되지 않은 값으로 만들 수 있습니다. 오염은 보호 레이어이며 안전한 앱에는 여러 개의 보호 레이어와 잘 설계된 API, 격리 패턴이 있습니다.\n\n</Pitfall>\n\n<DeepDive>\n\n#### 비밀 누출 방지를 위해서 `server-only`와 `taintUniqueValue` 사용하기 {/*using-server-only-and-taintuniquevalue-to-prevent-leaking-secrets*/}\n\n데이터베이스 패스워드와 같은 개인 키 또는 패스워드에 접근할 수 있는 서버 컴포넌트 환경을 실행하는 경우 개인 키나 패스워드를 클라이언트 컴포넌트로 전달하지 않도록 주의해야 합니다.\n\n```js\nexport async function Dashboard(props) {\n  // DO NOT DO THIS\n  return <Overview password={process.env.API_PASSWORD} />;\n}\n```\n\n```js\n\"use client\";\n\nimport {useEffect} from '...'\n\nexport async function Overview({ password }) {\n  useEffect(() => {\n    const headers = { Authorization: password };\n    fetch(url, { headers }).then(...);\n  }, [password]);\n  ...\n}\n```\n\n이 예시는 비밀 API 토큰을 클라이언트에 유출시킵니다. 이 API 토큰을 사용하여 특정 사용자가 접근해서는 안되는 데이터에 접근한다면 데이터 유출로 이어질 수 있습니다.\n\n[comment]: <> (TODO: Link to `server-only` docs once they are written)\n\n이와 같은 비밀은 서버의 신뢰할 수 있는 데이터 유틸리티에서만 불러올<sup>Import</sup> 수 있는 단일 헬퍼<sup>Helper</sup> 파일로 추상화되는 것이 이상적입니다. 헬퍼는 [`server-only`](https://www.npmjs.com/package/server-only)라는 태그를 지정하여 이 파일을 클라이언트에서 불러와지지 않도록 할 수 있습니다.\n\n```js\nimport \"server-only\";\n\nexport function fetchAPI(url) {\n  const headers = { Authorization: process.env.API_PASSWORD };\n  return fetch(url, { headers });\n}\n```\n\n때때로 리팩토링 중에 실수가 발생할 수도 있으며 이것에 대해서 잘 모르는 동료가 있을 수도 있습니다.\n이러한 실수를 방지하기 위해서 실제 패스워드를 \"오염\"시킬 수 있습니다.\n\n```js\nimport \"server-only\";\nimport {experimental_taintUniqueValue} from 'react';\n\nexperimental_taintUniqueValue(\n  'API 토큰 패스워드를 클라이언트에 전달하지 마세요. ' +\n    '서버에서 모든 fetch를 수행하는 것이 좋습니다.'\n  process,\n  process.env.API_PASSWORD\n);\n```\n\n이제 다른 사용자가 이 패스워드를 클라이언트 컴포넌트로 전달하거나, 서버 함수로 클라이언트 컴포넌트에 패스워드를 보내려고 할 때마다, `taintUniqueValue`를 호출했을 때 정의한 메시지와 함께 오류가 발생합니다.\n\n</DeepDive>\n\n---\n"
  },
  {
    "path": "src/content/reference/react/experimental_useEffectEvent.md",
    "content": "---\ntitle: experimental_useEffectEvent\nversion: experimental\n---\n\n<Experimental>\n\n**이 API는 실험 단계이므로 아직 안정된 버전의 React에는 사용할 수 없습니다.**\n\nReact 패키지를 최신 실험 버전으로 업그레이드하여 API를 사용해 볼 수 있습니다.\n\n- `react@experimental`\n- `react-dom@experimental`\n- `eslint-plugin-react-hooks@experimental`\n\n실험 버전의 React에는 버그가 있을 수 있습니다. 프로덕션 환경에서는 이 버전을 사용하지 마세요.\n\n</Experimental>\n\n\n<Intro>\n\n`useEffectEvent`는 [Effect Event](/learn/separating-events-from-effects#declaring-an-effect-event)에 반응하지 않는 로직을 추출하는 React Hook입니다.\n\n```js\nconst onSomething = useEffectEvent(callback)\n```\n\n</Intro>\n\n<InlineToc />\n"
  },
  {
    "path": "src/content/reference/react/forwardRef.md",
    "content": "---\ntitle: forwardRef\n---\n\n<Deprecated>\n\nReact 19부터는 더 이상 `forwardRef`이 필요하지 않습니다. 이제 `ref`를 Prop으로 직접 전달하면 됩니다.\n\n`forwardRef`는 향후 릴리스에서 사용 중단<sup>Deprecated</sup>될 예정입니다. 자세한 내용은 [여기](/blog/2024/12/05/react-19#ref-as-a-prop)에서 확인하세요.\n\n</Deprecated>\n\n<Intro>\n\n`forwardRef` lets your component expose a DOM node to the parent component with a [ref.](/learn/manipulating-the-dom-with-refs)\n\n```js\nconst SomeComponent = forwardRef(render)\n```\n\n</Intro>\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `forwardRef(render)` {/*forwardref*/}\n\n컴포넌트가 Ref를 받아 하위 컴포넌트로 전달하도록 하려면 `forwardRef()`를 호출하세요.\n\n```js\nimport { forwardRef } from 'react';\n\nconst MyInput = forwardRef(function MyInput(props, ref) {\n  // ...\n});\n```\n\n[아래에서 더 많은 예시를 확인하세요.](#usage)\n\n#### 매개변수 {/*parameters*/}\n\n* `render`: 컴포넌트의 렌더링 함수입니다. React는 컴포넌트가 부모로부터 받은 `props`와 `ref`로 이 함수를 호출합니다. 반환하는 JSX는 컴포넌트의 결과가 됩니다.\n\n#### 반환값 {/*returns*/}\n\n`forwardRef`는 JSX에서 렌더링할 수 있는 React 컴포넌트를 반환합니다. 일반 함수로 정의된 React 컴포넌트와 다르게, `forwardRef`가 반환하는 컴포넌트는 `ref` Prop도 받을 수 있습니다.\n\n#### 주의 사항 {/*caveats*/}\n\n* Strict Mode에서 React는 [실수로 발생한 결함을 찾기 위해](/reference/react/useState#my-initializer-or-updater-function-runs-twice) **렌더링 함수를 두 번 호출**합니다. 이는 개발 환경 전용 동작이며 프로덕션 환경에는 영향을 미치지 않습니다. 렌더링 함수가 순수 함수인 경우(그래야만 합니다), 컴포넌트 로직에 영향을 미치지 않습니다. 호출 결과 중 하나의 결과는 무시됩니다.\n\n\n---\n\n### `render` 함수 {/*render-function*/}\n\n`forwardRef`는 `render` 함수를 인수로 받습니다. React는 `props` 및 `ref`와 함께 이 함수를 호출합니다.\n\n```js\nconst MyInput = forwardRef(function MyInput(props, ref) {\n  return (\n    <label>\n      {props.label}\n      <input ref={ref} />\n    </label>\n  );\n});\n```\n\n#### 매개변수 {/*render-parameters*/}\n\n* `props`: 부모 컴포넌트가 전달한 Props입니다.\n\n* `ref`: 부모 컴포넌트가 전달한 `ref` 어트리뷰트입니다. `ref`는 객체나 함수일 수 있습니다. 부모 컴포넌트가 Ref를 전달하지 않은 경우 `null`이 됩니다. 전달받은 `ref`를 다른 컴포넌트에 전달하거나 [`useImperativeHandle`](/reference/react/useImperativeHandle)에 전달해야 합니다.\n\n#### 반환값 {/*render-returns*/}\n\n`forwardRef`는 JSX에서 렌더링할 수 있는 React 컴포넌트를 반환합니다. 일반 함수로 정의된 React 컴포넌트와 다르게 `forwardRef`에 의해 반환되는 컴포넌트는 `ref` Prop를 받을 수 있습니다.\n\n---\n\n## 사용법 {/*usage*/}\n\n### 부모 컴포넌트에 DOM 노드 노출하기 {/*exposing-a-dom-node-to-the-parent-component*/}\n\n기본적으로 각 컴포넌트의 DOM 노드는 비공개입니다. 그러나 때로는 부모에 DOM 노드를 노출하는 것이 유용할 수 있습니다. 예를 들어 포커스<sup>Focus</sup> 하기 위해 노출할 수 있습니다. 이를 위해 컴포넌트 정의를 `forwardRef()`로 감싸주면 됩니다.\n\n```js {3,11}\nimport { forwardRef } from 'react';\n\nconst MyInput = forwardRef(function MyInput(props, ref) {\n  const { label, ...otherProps } = props;\n  return (\n    <label>\n      {label}\n      <input {...otherProps} />\n    </label>\n  );\n});\n```\n\n`props` 다음에 두 번째 인수로 <CodeStep step={1}>ref</CodeStep>를 받게 됩니다. 노출하려는 DOM 노드에 이를 전달합니다.\n\n```js {8} [[1, 3, \"ref\"], [1, 8, \"ref\", 30]]\nimport { forwardRef } from 'react';\n\nconst MyInput = forwardRef(function MyInput(props, ref) {\n  const { label, ...otherProps } = props;\n  return (\n    <label>\n      {label}\n      <input {...otherProps} ref={ref} />\n    </label>\n  );\n});\n```\n\n이렇게 하면 부모인 `Form` 컴포넌트가 `MyInput`에 의해 노출된 <CodeStep step={2}>`<input>` DOM 노드</CodeStep>에 접근할 수 있습니다.\n\n```js [[1, 2, \"ref\"], [1, 10, \"ref\", 41], [2, 5, \"ref.current\"]]\nfunction Form() {\n  const ref = useRef(null);\n\n  function handleClick() {\n    ref.current.focus();\n  }\n\n  return (\n    <form>\n      <MyInput label=\"Enter your name:\" ref={ref} />\n      <button type=\"button\" onClick={handleClick}>\n        Edit\n      </button>\n    </form>\n  );\n}\n```\n\n이 `Form` 컴포넌트는 `MyInput` 에게 [Ref를 전달](/reference/react/useRef#manipulating-the-dom-with-a-ref)합니다. `MyInput` 컴포넌트는 해당 Ref를 `<input>` 태그에 전달합니다. 결과적으로 `Form` 컴포넌트는 해당 `<input>` DOM 노드에 접근하여 [`focus()`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus)를 호출할 수 있습니다.\n\n컴포넌트 내부 DOM 노드의 Ref를 노출하면 나중에 컴포넌트의 내부를 변경하기가 더 어려워진다는 점에 유의하세요. 일반적으로 버튼이나 텍스트 Input과 같이 재사용할 수 있는 저수준 컴포넌트에서 DOM 노드를 노출하지만, 아바타나 댓글 같은 애플리케이션 레벨의 컴포넌트에서는 노출하고 싶지 않을 것입니다.\n\n<Recipes title=\"ref 전달 예시\">\n\n#### 텍스트 Input에 초점 맞추기 {/*focusing-a-text-input*/}\n\n버튼을 클릭하면 Input에 포커스됩니다. `Form` 컴포넌트는 Ref를 정의하고 이를 `MyInput` 컴포넌트로 전달합니다. `MyInput` 컴포넌트는 해당 Ref를 `input`으로 전달합니다. 이렇게 하면 `Form` 컴포넌트가 `input`에 포커스를 줄 수 있습니다.\n\n<Sandpack>\n\n```js\nimport { useRef } from 'react';\nimport MyInput from './MyInput.js';\n\nexport default function Form() {\n  const ref = useRef(null);\n\n  function handleClick() {\n    ref.current.focus();\n  }\n\n  return (\n    <form>\n      <MyInput label=\"Enter your name:\" ref={ref} />\n      <button type=\"button\" onClick={handleClick}>\n        Edit\n      </button>\n    </form>\n  );\n}\n```\n\n```js src/MyInput.js\nimport { forwardRef } from 'react';\n\nconst MyInput = forwardRef(function MyInput(props, ref) {\n  const { label, ...otherProps } = props;\n  return (\n    <label>\n      {label}\n      <input {...otherProps} ref={ref} />\n    </label>\n  );\n});\n\nexport default MyInput;\n```\n\n```css\ninput {\n  margin: 5px;\n}\n```\n\n</Sandpack>\n\n<Solution />\n\n#### 비디오 재생 및 정지하기 {/*playing-and-pausing-a-video*/}\n\n버튼을 클릭하면 `<video>` DOM 노드에서 [`play()`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/play) 및 [`pause()`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/pause)를 호출합니다. `App` 컴포넌트는 Ref를 정의하고 이를 `MyVideoPlayer` 컴포넌트에 전달합니다. `MyVideoPlayer` 컴포넌트는 해당 Ref를 브라우저 `<video>` 노드로 전달합니다. 이렇게 하면 `App` 컴포넌트가 `<video>`를 재생하고 정지할 수 있습니다.\n\n<Sandpack>\n\n```js\nimport { useRef } from 'react';\nimport MyVideoPlayer from './MyVideoPlayer.js';\n\nexport default function App() {\n  const ref = useRef(null);\n  return (\n    <>\n      <button onClick={() => ref.current.play()}>\n        Play\n      </button>\n      <button onClick={() => ref.current.pause()}>\n        Pause\n      </button>\n      <br />\n      <MyVideoPlayer\n        ref={ref}\n        src=\"https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4\"\n        type=\"video/mp4\"\n        width=\"250\"\n      />\n    </>\n  );\n}\n```\n\n```js src/MyVideoPlayer.js\nimport { forwardRef } from 'react';\n\nconst VideoPlayer = forwardRef(function VideoPlayer({ src, type, width }, ref) {\n  return (\n    <video width={width} ref={ref}>\n      <source\n        src={src}\n        type={type}\n      />\n    </video>\n  );\n});\n\nexport default VideoPlayer;\n```\n\n```css\nbutton { margin-bottom: 10px; margin-right: 10px; }\n```\n\n</Sandpack>\n\n<Solution />\n\n</Recipes>\n\n---\n\n### 여러 컴포넌트를 통해 Ref 전달하기 {/*forwarding-a-ref-through-multiple-components*/}\n\n`ref`를 DOM 노드로 전달하지 않고 `MyInput`과 같은 컴포넌트로 전달할 수 있습니다.\n\n```js {1,5}\nconst FormField = forwardRef(function FormField(props, ref) {\n  // ...\n  return (\n    <>\n      <MyInput ref={ref} />\n      ...\n    </>\n  );\n});\n```\n\n`MyInput` 컴포넌트가 `<input>`에 `ref`를 전달하면 `FormField`의 `ref`는 해당 `<input>`을 얻을 수 있습니다.\n\n```js {2,5,10}\nfunction Form() {\n  const ref = useRef(null);\n\n  function handleClick() {\n    ref.current.focus();\n  }\n\n  return (\n    <form>\n      <FormField label=\"Enter your name:\" ref={ref} isRequired={true} />\n      <button type=\"button\" onClick={handleClick}>\n        Edit\n      </button>\n    </form>\n  );\n}\n```\n\n`Form` 컴포넌트는 Ref를 정의하고 이를 `FormField`에 전달합니다. `FormField` 컴포넌트는 해당 Ref를 `MyInput`으로 전달하고, 이 컴포넌트는 `<input>` DOM 노드로 전달합니다. 이것이 `Form`이 `<input>` DOM 노드에 접근하는 방식입니다.\n\n\n<Sandpack>\n\n```js\nimport { useRef } from 'react';\nimport FormField from './FormField.js';\n\nexport default function Form() {\n  const ref = useRef(null);\n\n  function handleClick() {\n    ref.current.focus();\n  }\n\n  return (\n    <form>\n      <FormField label=\"Enter your name:\" ref={ref} isRequired={true} />\n      <button type=\"button\" onClick={handleClick}>\n        Edit\n      </button>\n    </form>\n  );\n}\n```\n\n```js src/FormField.js\nimport { forwardRef, useState } from 'react';\nimport MyInput from './MyInput.js';\n\nconst FormField = forwardRef(function FormField({ label, isRequired }, ref) {\n  const [value, setValue] = useState('');\n  return (\n    <>\n      <MyInput\n        ref={ref}\n        label={label}\n        value={value}\n        onChange={e => setValue(e.target.value)}\n      />\n      {(isRequired && value === '') &&\n        <i>Required</i>\n      }\n    </>\n  );\n});\n\nexport default FormField;\n```\n\n\n```js src/MyInput.js\nimport { forwardRef } from 'react';\n\nconst MyInput = forwardRef((props, ref) => {\n  const { label, ...otherProps } = props;\n  return (\n    <label>\n      {label}\n      <input {...otherProps} ref={ref} />\n    </label>\n  );\n});\n\nexport default MyInput;\n```\n\n```css\ninput, button {\n  margin: 5px;\n}\n```\n\n</Sandpack>\n\n---\n\n### DOM 노드 대신 명령형 핸들 노출하기 {/*exposing-an-imperative-handle-instead-of-a-dom-node*/}\n\n전체 DOM 노드를 노출하는 대신 제한된 메서드 집합과 함께 *명령형 핸들*이라고 하는 사용자 정의 객체를 노출할 수 있습니다. 이를 위해 DOM 노드를 보유할 별도의 Ref를 정의해야 합니다.\n\n```js {2,6}\nconst MyInput = forwardRef(function MyInput(props, ref) {\n  const inputRef = useRef(null);\n\n  // ...\n\n  return <input {...props} ref={inputRef} />;\n});\n```\n\n전달받은 `ref`를 [`useImperativeHandle`](/reference/react/useImperativeHandle)에 전달하고 노출하려는 값을 `ref`에 지정합니다.\n\n```js {6-15}\nimport { forwardRef, useRef, useImperativeHandle } from 'react';\n\nconst MyInput = forwardRef(function MyInput(props, ref) {\n  const inputRef = useRef(null);\n\n  useImperativeHandle(ref, () => {\n    return {\n      focus() {\n        inputRef.current.focus();\n      },\n      scrollIntoView() {\n        inputRef.current.scrollIntoView();\n      },\n    };\n  }, []);\n\n  return <input {...props} ref={inputRef} />;\n});\n```\n\n일부 컴포넌트가 `MyInput`의 Ref를 받으면 DOM 노드 대신 `{ focus, scrollIntoView }` 객체만 받습니다. 이를 통해 노출하는 DOM 노드의 정보를 최소한으로 제한할 수 있습니다.\n\n<Sandpack>\n\n```js\nimport { useRef } from 'react';\nimport MyInput from './MyInput.js';\n\nexport default function Form() {\n  const ref = useRef(null);\n\n  function handleClick() {\n    ref.current.focus();\n    // This won't work because the DOM node isn't exposed:\n    // ref.current.style.opacity = 0.5;\n  }\n\n  return (\n    <form>\n      <MyInput placeholder=\"Enter your name\" ref={ref} />\n      <button type=\"button\" onClick={handleClick}>\n        Edit\n      </button>\n    </form>\n  );\n}\n```\n\n```js src/MyInput.js\nimport { forwardRef, useRef, useImperativeHandle } from 'react';\n\nconst MyInput = forwardRef(function MyInput(props, ref) {\n  const inputRef = useRef(null);\n\n  useImperativeHandle(ref, () => {\n    return {\n      focus() {\n        inputRef.current.focus();\n      },\n      scrollIntoView() {\n        inputRef.current.scrollIntoView();\n      },\n    };\n  }, []);\n\n  return <input {...props} ref={inputRef} />;\n});\n\nexport default MyInput;\n```\n\n```css\ninput {\n  margin: 5px;\n}\n```\n\n</Sandpack>\n\n[명령형 핸들 사용에 대해 자세히 알아보세요.](/reference/react/useImperativeHandle)\n\n<Pitfall>\n\n**ref를 과도하게 사용하지 마세요.** 노드로 스크롤 하기, 노드에 포커스하기, 애니메이션 트리거하기, 텍스트 선택하기 등 prop로 표현할 수 없는 필수적인 동작에만 ref를 사용해야 합니다.\n\n**prop로 무언가를 표현할 수 있다면 ref를 사용해서는 안 됩니다.** 예를 들어 `Modal` 컴포넌트에서 `{ open, close }`와 같은 명령형 핸들을 노출하는 대신 `<Modal isOpen={isOpen} />`과 같이 prop `isOpen`을 사용하는 것이 더 좋습니다. [Effects](/learn/synchronizing-with-effects)는 props를 통해 명령형 동작을 노출하는 데 도움이 될 수 있습니다.\n\n</Pitfall>\n\n---\n\n## 문제해결 {/*troubleshooting*/}\n\n### 컴포넌트가 `forwardRef`로 감싸져 있지만, 컴포넌트의 `ref`는 항상 `null`입니다. {/*my-component-is-wrapped-in-forwardref-but-the-ref-to-it-is-always-null*/}\n\n일반적으로 `ref`를 실제로 사용하는 것을 잊어버렸다는 것입니다.\n\n예를 들어 이 컴포넌트는 `ref`로 아무것도 하지 않습니다.\n\n```js {1}\nconst MyInput = forwardRef(function MyInput({ label }, ref) {\n  return (\n    <label>\n      {label}\n      <input />\n    </label>\n  );\n});\n```\n\n이 문제를 해결하려면 `ref`를 DOM 노드나 `ref`를 받을 수 있는 다른 컴포넌트에 전달하세요.\n\n```js {1,5}\nconst MyInput = forwardRef(function MyInput({ label }, ref) {\n  return (\n    <label>\n      {label}\n      <input ref={ref} />\n    </label>\n  );\n});\n```\n\n일부 로직이 조건부인 경우 `MyInput`의 `ref`가 `null`일 수 있습니다.\n\n```js {1,5}\nconst MyInput = forwardRef(function MyInput({ label, showInput }, ref) {\n  return (\n    <label>\n      {label}\n      {showInput && <input ref={ref} />}\n    </label>\n  );\n});\n```\n\n`showInput`이 `false`이면 `ref`가 어떤 노드로도 전달되지 않으며 `MyInput`의 `ref`는 비어 있게 됩니다. 이 예시의 `Panel`과 같이 조건이 다른 컴포넌트 안에 숨겨져 있는 경우 특히 이 점을 놓치기 쉽습니다.\n\n```js {5,7}\nconst MyInput = forwardRef(function MyInput({ label, showInput }, ref) {\n  return (\n    <label>\n      {label}\n      <Panel isExpanded={showInput}>\n        <input ref={ref} />\n      </Panel>\n    </label>\n  );\n});\n```\n"
  },
  {
    "path": "src/content/reference/react/hooks.md",
    "content": "---\ntitle: \"내장 React Hook\"\n---\n\n<Intro>\n\n*Hook*을 사용하면 컴포넌트에서 다양한 React 기능을 사용할 수 있습니다. 내장된 Hook을 이용하거나 이를 결합하여 자신만의 Hook을 만들 수 있습니다. 이 페이지에는 React에 내장된 모든 Hook이 나열되어 있습니다.\n\n</Intro>\n\n---\n\n## State Hooks {/*state-hooks*/}\n\n*State*를 통해 컴포넌트는 [사용자 입력과 같은 정보를 \"기억\"할 수 있습니다.](/learn/state-a-components-memory) 예를 들어, 폼 컴포넌트는 State를 사용하여 입력값을 저장할 수 있고, 이미지 갤러리 컴포넌트는 State를 사용하여 선택한 이미지 인덱스를 저장할 수 있습니다.\n\n컴포넌트에 State를 추가하려면, 다음 Hook 중 하나를 사용하세요.\n\n* [`useState`](/reference/react/useState)는 직접 업데이트할 수 있는 State 변수를 선언합니다.\n* [`useReducer`](/reference/react/useReducer)는 [Reducer 함수](/learn/extracting-state-logic-into-a-reducer) 내부의 업데이트 로직을 사용하여 State 변수를 선언합니다.\n\n```js\nfunction ImageGallery() {\n  const [index, setIndex] = useState(0);\n  // ...\n```\n\n---\n\n## Context Hooks {/*context-hooks*/}\n\n*Context*는 컴포넌트가 [Props를 전달하지 않고도 멀리 있는 부모 컴포넌트로부터 정보를 받을 수 있게 해줍니다.](/learn/passing-props-to-a-component) 예를 들어, 애플리케이션의 최상위 컴포넌트는 현재 UI 테마를 아래의 모든 컴포넌트에 깊이와 상관없이 전달할 수 있습니다.\n\n* [`useContext`](/reference/react/useContext)는 Context를 읽고 구독합니다.\n\n```js\nfunction Button() {\n  const theme = useContext(ThemeContext);\n  // ...\n```\n\n---\n\n## Ref Hooks {/*ref-hooks*/}\n\n*Ref*를 사용하면 컴포넌트가 DOM 노드나 Timeout ID와 같이 [렌더링에 사용되지 않는 일부 정보를 보유할 수 있습니다.](/learn/referencing-values-with-refs) State와 달리, Ref는 업데이트를 해도 컴포넌트가 다시 렌더링 되지 않습니다. Ref는 React 패러다임의 \"탈출구\"입니다. 내장된 브라우저 API와 같이, React가 아닌 시스템으로 작업해야 할 때 유용합니다.\n\n* [`useRef`](/reference/react/useRef)는 Ref를 선언합니다. 여기에는 어떤 값이라도 담을 수 있지만, 대부분 DOM 노드를 담는 데 사용됩니다.\n* [`useImperativeHandle`](/reference/react/useImperativeHandle)을 사용하면 컴포넌트에 노출되는 Ref를 커스텀할 수 있습니다. 이는 드물게 사용됩니다.\n\n```js\nfunction Form() {\n  const inputRef = useRef(null);\n  // ...\n```\n\n---\n\n## Effect Hooks {/*effect-hooks*/}\n\n*Effect*를 통해 컴포넌트를 [외부 시스템에 연결하고 동기화할 수 있습니다.](/learn/synchronizing-with-effects) 여기에는 네트워크, 브라우저 DOM, 애니메이션, 다른 UI 라이브러리를 사용하여 작성된 위젯, 기타 React가 아닌 코드를 다루는 것이 포함됩니다.\n\n* [`useEffect`](/reference/react/useEffect)는 컴포넌트를 외부 시스템에 연결합니다.\n\n```js\nfunction ChatRoom({ roomId }) {\n  useEffect(() => {\n    const connection = createConnection(roomId);\n    connection.connect();\n    return () => connection.disconnect();\n  }, [roomId]);\n  // ...\n```\n\nEffect는 React 패러다임의 \"탈출구\"입니다. 애플리케이션의 데이터 흐름을 조정하기 위해 Effect를 쓰지 마세요. 외부 시스템과 상호작용하지 않는다면, [Effect가 필요하지 않을 수도 있습니다.](/learn/you-might-not-need-an-effect)\n\n타이밍에서 차이가 있는 `useEffect`의 두 가지 드물게 사용되는 변형이 있습니다.\n\n* [`useLayoutEffect`](/reference/react/useLayoutEffect)는 브라우저가 화면을 다시 그리기 전에 실행됩니다. 여기에서 레이아웃을 계산할 수 있습니다.\n* [`useInsertionEffect`](/reference/react/useInsertionEffect)는 React가 DOM을 변경하기 전에 실행됩니다. 라이브러리는 여기에 동적 CSS를 삽입할 수 있습니다.\n\nYou can also separate events from Effects:\n\n- [`useEffectEvent`](/reference/react/useEffectEvent) creates a non-reactive event to fire from any Effect hook.\n---\n\n## Performance Hooks {/*performance-hooks*/}\n\n재렌더링 성능을 최적화하는 일반적인 방법은 불필요한 작업을 건너뛰는 것입니다. 예를 들어, 이전 렌더링 이후 데이터가 변경되지 않은 경우 캐시된 계산을 재사용하거나 재렌더링을 건너뛰도록 React에 지시할 수 있습니다.\n\n계산과 불필요한 재렌더링을 건너뛰려면 다음 Hook 중 하나를 사용하세요.\n\n- [`useMemo`](/reference/react/useMemo)를 사용하면 비용이 많이 드는 계산 결과를 캐시할 수 있습니다.\n- [`useCallback`](/reference/react/useCallback)을 사용하면 함수 정의를 최적화된 컴포넌트에 전달하기 전에 캐시할 수 있습니다.\n\n```js\nfunction TodoList({ todos, tab, theme }) {\n  const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);\n  // ...\n}\n```\n\n화면을 실제로 업데이트해야 하므로 재렌더링을 건너뛸 수 없는 경우도 있습니다. 이 경우, 동기식이어야 하는 Blocking 업데이트(예: Input에 입력)와 사용자 인터페이스를 차단할 필요가 없는 Non-Blocking 업데이트(예: 차트 업데이트)를 분리하여 성능을 향상시킬 수 있습니다.\n\n렌더링 우선순위를 지정하려면, 다음 Hook 중 하나를 사용하세요.\n\n- [`useTransition`](/reference/react/useTransition)을 사용하면 State 전환을 Non-Blocking으로 표시하고, 다른 업데이트가 이를 중단하도록 허용할 수 있습니다.\n- [`useDeferredValue`](/reference/react/useDeferredValue)를 사용하면 UI의 중요하지 않은 부분에 대한 업데이트를 지연하고, 다른 부분이 먼저 업데이트되도록 할 수 있습니다.\n\n---\n\n## Other Hooks {/*other-hooks*/}\n\n다음 Hook은 대부분 라이브러리 작성자에게 유용하며 애플리케이션 코드에서는 일반적으로 사용되지 않습니다.\n\n- [`useDebugValue`](/reference/react/useDebugValue)를 사용하면 커스텀 Hook에 대해 React 개발자 도구에 표시하는 레이블을 커스텀할 수 있습니다.\n- [`useId`](/reference/react/useId)를 사용하면 컴포넌트가 고유 ID를 자신과 연결할 수 있습니다. 일반적으로 접근성 API와 함께 사용됩니다.\n- [`useSyncExternalStore`](/reference/react/useSyncExternalStore)를 사용하면 컴포넌트가 외부 저장소를 구독할 수 있습니다.\n* [`useActionState`](/reference/react/useActionState)를 사용하면 액션을 통해 State를 관리할 수 있습니다.\n\n---\n\n## 나만의 Hooks {/*your-own-hooks*/}\n또한 자바스크립트 함수로 [나만의 커스텀 Hook을 정의할 수도 있습니다.](/learn/reusing-logic-with-custom-hooks#extracting-your-own-custom-hook-from-a-component)\n"
  },
  {
    "path": "src/content/reference/react/index.md",
    "content": "---\ntitle: React 참고서 개요\n---\n\n<Intro>\n\n이 섹션은 React와 관련된 작업에 대한 상세한 참고서를 제공합니다. React에 대한 소개는 [학습하기](/learn) 섹션을 참고하세요.\n\n</Intro>\n\nReact 참고서는 다음과 같은 기능적인 하위 섹션으로 구성되어 있습니다.\n\n## React {/*react*/}\n\nReact의 프로그래밍 기능.\n\n* [Hook](/reference/react/hooks) - 컴포넌트에서 다양한 React 기능을 사용하세요.\n* [컴포넌트](/reference/react/components) - JSX에서 사용할 수 있는 내장 컴포넌트입니다.\n* [API](/reference/react/apis) - 컴포넌트 정의에 유용한 API들을 다룹니다.\n* [지시어](/reference/rsc/directives) - React 서버 컴포넌트와 호환되는 번들러에게 지시를 제공합니다.\n\n## React DOM {/*react-dom*/}\n\nReact DOM은 브라우저 DOM 환경에서 실행되는 웹 애플리케이션에서만 지원되는 기능을 포함하고 있습니다. 이 섹션은 다음과 같이 나뉩니다.\n\n* [Hook](/reference/react-dom/hooks) - 브라우저 DOM 환경에서 실행되는 웹 애플리케이션을 위한 Hook입니다.\n* [컴포넌트](/reference/react-dom/components) - React는 브라우저 내장 HTML 및 SVG 컴포넌트를 모두 지원합니다.\n* [API](/reference/react-dom) - `react-dom` 패키지에는 웹 애플리케이션에서만 지원되는 메서드가 포함되어 있습니다.\n* [클라이언트 API](/reference/react-dom/client) - `react-dom/client` API를 사용하면 브라우저에서 React 컴포넌트를 렌더링할 수 있습니다.\n* [서버 API](/reference/react-dom/server) - `react-dom/server` API를 사용하면 서버에서 React 컴포넌트를 HTML로 렌더링할 수 있습니다.\n\n## React Compiler {/*react-compiler*/}\n\nThe React Compiler is a build-time optimization tool that automatically memoizes your React components and values:\n\n* [Configuration](/reference/react-compiler/configuration) - Configuration options for React Compiler.\n* [Directives](/reference/react-compiler/directives) - Function-level directives to control compilation.\n* [라이브러리 컴파일](/reference/react-compiler/compiling-libraries) - Guide for shipping pre-compiled library code.\n\n## ESLint Plugin React Hooks {/*eslint-plugin-react-hooks*/}\n\nThe [ESLint plugin for React Hooks](/reference/eslint-plugin-react-hooks) helps enforce the Rules of React:\n\n* [린트](/reference/eslint-plugin-react-hooks) - Detailed documentation for each lint with examples.\n\n## Rules of React {/*rules-of-react*/}\n\nReact에는 패턴 이해를 쉽게 하며 고품질의 애플리케이션을 만들 수 있게 하는 일종의 규칙 혹은 모범적인 방식이 있습니다.\n\n* [컴포넌트와 Hook은 순수해야 합니다](/reference/rules/components-and-hooks-must-be-pure) – 순수성은 코드를 더 쉽게 이해하고 디버그할 수 있도록 하며, React가 올바르게 컴포넌트와 Hook을 자동으로 최적화할 수 있도록 합니다.\n* [React가 컴포넌트와 Hook을 호출하는 방식](/reference/rules/react-calls-components-and-hooks) – React는 사용자 경험을 최적화하기 위해 필요할 때마다 컴포넌트와 Hook을 렌더링합니다.\n* [Hook의 규칙](/reference/rules/rules-of-hooks) – Hook은 자바스크립트 함수로 정의되지만 호출 위치에 제약이 있는 특별한 유형의 재사용 가능한 UI 로직입니다.\n\n## 레거시 API {/*legacy-apis*/}\n\n* [레거시 API](/reference/react/legacy) - `react` 패키지에서 내보냈지만<sup>Exported</sup> 새로 작성할 코드에서는 권장하지 않습니다.\n"
  },
  {
    "path": "src/content/reference/react/isValidElement.md",
    "content": "---\ntitle: isValidElement\n---\n\n<Intro>\n\n`isValidElement`는 값이 React 엘리먼트인지 확인합니다.\n\n```js\nconst isElement = isValidElement(value)\n```\n\n</Intro>\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `isValidElement(value)` {/*isvalidelement*/}\n\n`isValidElement(value)`를 호출하여 `value`가 React 엘리먼트인지 확인합니다.\n\n```js\nimport { isValidElement, createElement } from 'react';\n\n// ✅ React 엘리먼트\nconsole.log(isValidElement(<p />)); // true\nconsole.log(isValidElement(createElement('p'))); // true\n\n// ❌ React 엘리먼트가 아님\nconsole.log(isValidElement(25)); // false\nconsole.log(isValidElement('Hello')); // false\nconsole.log(isValidElement({ age: 42 })); // false\n```\n\n[아래에서 더 많은 예시를 확인하세요](#usage)\n\n#### 매개변수 {/*parameters*/}\n\n`value`: 확인하려는 `value`입니다. 모든 종류의 값이 될 수 있습니다.\n\n#### 반환값 {/*returns*/}\n\n`isValidElement`는 `value`가 React 엘리먼트인 경우 `true`를 반환합니다. 그렇지 않으면 `false`를 반환합니다.\n\n#### 주의 사항 {/*caveats*/}\n\n* **[`createElement`](/reference/react/createElement)가 반환한 [JSX 태그](/learn/writing-markup-with-jsx)와 객체는 React 엘리먼트로 간주합니다.** 예를 들어, `42`와 같은 숫자는 유효한 React *노드* (컴포넌트에서 반환될 수 있지만)이지만, 유효한 React 엘리먼트는 아닙니다. [`createPortal`](/reference/react-dom/createPortal)로 만들어진 배열과 portal도 React 엘리먼트로 간주하지 *않습니다*.\n\n---\n\n## 사용법 {/*usage*/}\n\n### 어떤 것이 React 엘리먼트인지 확인하기 {/*checking-if-something-is-a-react-element*/}\n\n어떤 값이 React 엘리먼트인지 확인하려면 `isValidElement`를 호출해 보세요.\n\nReact 엘리먼트는 다음과 같습니다.\n\n- [JSX tag](/learn/writing-markup-with-jsx)를 작성하여 생성된 값\n- [`createElement`](/reference/react/createElement)를 호출하여 생성된 값\n\nReact 엘리먼트의 경우 `isValidElement`는 `true`를 반환합니다.\n\n```js\nimport { isValidElement, createElement } from 'react';\n\n// ✅ JSX 태그는 React 엘리먼트입니다.\nconsole.log(isValidElement(<p />)); // true\nconsole.log(isValidElement(<MyComponent />)); // true\n\n// ✅ createElement가 반환하는 값은 React 엘리먼트입니다.\nconsole.log(isValidElement(createElement('p'))); // true\nconsole.log(isValidElement(createElement(MyComponent))); // true\n```\n\n문자열, 숫자, 임의의 객체 및 배열과 같은 값들은 React 엘리먼트가 아닙니다.\n\n이 경우 `isValidElement`는 `false`를 반환합니다.\n\n```js\n// ❌ 이것들은 React 엘리먼트가 *아닙니다*.\nconsole.log(isValidElement(null)); // false\nconsole.log(isValidElement(25)); // false\nconsole.log(isValidElement('Hello')); // false\nconsole.log(isValidElement({ age: 42 })); // false\nconsole.log(isValidElement([<div />, <div />])); // false\nconsole.log(isValidElement(MyComponent)); // false\n```\n\n`isValidElement`가 필요한 경우는 매우 드뭅니다. 주로 \"엘리먼트만\" 허용하는 다른 API를 호출할 때와 ([`cloneElement`](/reference/react/cloneElement)가 하는 것처럼) 인수가 React 엘리먼트가 아닌 경우 오류를 피하고 싶을 때 유용합니다.\n\n`isValidElement`확인을 추가 해야 하는 구체적인 이유가 없는 한 이 확인은 필요하지 않을 수 있습니다.\n\n<DeepDive>\n\n#### React 엘리먼트 vs React 노드 {/*react-elements-vs-react-nodes*/}\n\n컴포넌트를 작성할 때 모든 종류의 *React 노드*를 반환할 수 있습니다.\n\n```js\nfunction MyComponent() {\n  // ... React 노드를 반환할수 있습니다. ...\n}\n```\n\nReact 노드는 다음과 같습니다.\n- `<div />` 또는 `createElement('div')`와 같이 생성된 React 엘리먼트입니다.\n- [`createPortal`](/reference/react-dom/createPortal)로 생성된 portal입니다.\n- 문자열\n- 숫자\n- `true`, `false`, `null`, 또는 `undefined` (표시되지 않는 경우)\n- 다른 React 노드의 배열\n\n**주의 `isValidElement`는 인수가 React 노드의 여부가 아니라 *React 엘리먼트*의 여부를 확인합니다.** 예를 들어 `42`는 유효한 React 엘리먼트가 아닙니다. 하지만 완벽하게 유효한 React 노드입니다.\n\n```js\nfunction MyComponent() {\n  return 42; // 컴포넌트에서 숫자를 반환해도 괜찮습니다.\n}\n```\n\n이것이 무언가를 렌더링할 수 있는지 확인하는 여부로 `isValidElement`를 사용해서는 안 되는 이유입니다.\n\n</DeepDive>\n"
  },
  {
    "path": "src/content/reference/react/lazy.md",
    "content": "---\ntitle: lazy\n---\n\n<Intro>\n\n`lazy`를 사용하면 컴포넌트가 처음 렌더링될 때까지 해당 컴포넌트의 코드를 로딩하는 것을 지연할 수 있습니다.\n\n```js\nconst SomeComponent = lazy(load)\n```\n\n</Intro>\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `lazy(load)` {/*lazy*/}\n\n`lazy`를 컴포넌트 외부에서 호출하여 지연 로딩된 React 컴포넌트를 선언하세요.\n\n```js\nimport { lazy } from 'react';\n\nconst MarkdownPreview = lazy(() => import('./MarkdownPreview.js'));\n```\n\n[아래 예시를 참고하세요.](#usage)\n\n#### 매개변수 {/*parameters*/}\n\n* `load`: [Promise](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Promise) 혹은 또 다른 *thenable* (`then` 메서드가 있는 Promise 유사 객체)을 반환하는 함수입니다. React는 반환된 컴포넌트를 처음 렌더링하려고 시도할 때까지 `load`를 호출하지 않습니다. React가 처음으로 `load`를 호출하면, 그것이 해결될 때까지 기다리고, 그 후 해결된 값의 `.default`를 React 컴포넌트로 렌더링합니다. 반환된 Promise와 Promise의 해결된 값은 캐시되므로, React는 `load`를 한 번만 호출합니다. 만약 Promise가 거부되면, React는 거부 이유를 가장 가까운 Error Boundary가 처리할 수 있도록 `throw` 합니다.\n\n#### 반환값 {/*returns*/}\n\n`lazy`는 트리에 렌더링할 수 있는 React 컴포넌트를 반환합니다. 컴포넌트의 코드가 여전히 로딩되는 동안 렌더링을 시도하면 일시 중지됩니다. 로딩 중에 Loading Indicator를 표시하려면 [`<Suspense>`](/reference/react/Suspense)를 사용하세요.\n\n---\n\n### `load` 함수 {/*load*/}\n\n#### 매개변수 {/*load-parameters*/}\n\n`load`는 매개변수를 받지 않습니다.\n\n#### 반환값 {/*load-returns*/}\n\n[Promise](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Promise) 또는 다른 *thenable* (`then` 메서드가 있는 Promise 유사 객체)을 반환해야 합니다. 결국  `.default` 프로퍼티가 함수, [`memo`](/reference/react/memo), [`forwardRef`](/reference/react/forwardRef) 컴포넌트와 같은 유효한 React 컴포넌트 유형인 객체로 해석되어야 합니다.\n\n---\n\n## 사용법 {/*usage*/}\n\n### Suspense와 지연 로딩 컴포넌트 {/*suspense-for-code-splitting*/}\n\n일반적으로 정적 [`import`](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Statements/import) 선언으로 컴포넌트를 가져옵니다.\n\n```js\nimport MarkdownPreview from './MarkdownPreview.js';\n```\n\n해당 컴포넌트 코드가 처음 렌더링 될 때까지 로드하는 것을 연기하려면 `import`를 다음과 같이 대체합니다.\n\n```js\nimport { lazy } from 'react';\n\nconst MarkdownPreview = lazy(() => import('./MarkdownPreview.js'));\n```\n\n위의 코드는 [동적 `import()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import)에 의존하므로 번들러 또는 프레임워크의 지원이 필요할 수 있습니다. 이 패턴을 사용하려면 임포트하려는 `lazy` 컴포넌트가 `default` 내보내기로 내보내져 있어야 합니다.\n\n이제 요청에 따라 컴포넌트의 코드가 로딩되므로, 로딩하는 동안 표시할 항목도 지정해야 합니다. `lazy` 컴포넌트 또는 해당 부모 컴포넌트 중 하나를 [`<Suspense>`](/reference/react/Suspense) 경계<sup>Boundary</sup>로 감싸서 이 작업을 수행할 수 있습니다.\n\n```js {1,4}\n<Suspense fallback={<Loading />}>\n  <h2>Preview</h2>\n  <MarkdownPreview />\n</Suspense>\n```\n\n이 예시에서 `MarkdownPreview` 코드는 렌더링을 시도할 때까지 로딩되지 않습니다. `MarkdownPreview`가 아직 로딩되지 않는 경우에는 그 자리에 `Loading` 코드가 대신 표시됩니다. 체크박스를 선택해 보세요.\n\n<Sandpack>\n\n```js src/App.js\nimport { useState, Suspense, lazy } from 'react';\nimport Loading from './Loading.js';\n\nconst MarkdownPreview = lazy(() => delayForDemo(import('./MarkdownPreview.js')));\n\nexport default function MarkdownEditor() {\n  const [showPreview, setShowPreview] = useState(false);\n  const [markdown, setMarkdown] = useState('Hello, **world**!');\n  return (\n    <>\n      <textarea value={markdown} onChange={e => setMarkdown(e.target.value)} />\n      <label>\n        <input type=\"checkbox\" checked={showPreview} onChange={e => setShowPreview(e.target.checked)} />\n        Show preview\n      </label>\n      <hr />\n      {showPreview && (\n        <Suspense fallback={<Loading />}>\n          <h2>Preview</h2>\n          <MarkdownPreview markdown={markdown} />\n        </Suspense>\n      )}\n    </>\n  );\n}\n\n// 로딩 상태를 확인하기 위해, 테스트를 위한 지연값을 추가합니다.\nfunction delayForDemo(promise) {\n  return new Promise(resolve => {\n    setTimeout(resolve, 2000);\n  }).then(() => promise);\n}\n```\n\n```js src/Loading.js\nexport default function Loading() {\n  return <p><i>Loading...</i></p>;\n}\n```\n\n```js src/MarkdownPreview.js\nimport { Remarkable } from 'remarkable';\n\nconst md = new Remarkable();\n\nexport default function MarkdownPreview({ markdown }) {\n  return (\n    <div\n      className=\"content\"\n      dangerouslySetInnerHTML={{__html: md.render(markdown)}}\n    />\n  );\n}\n```\n\n```json package.json hidden\n{\n  \"dependencies\": {\n    \"immer\": \"1.7.3\",\n    \"react\": \"latest\",\n    \"react-dom\": \"latest\",\n    \"react-scripts\": \"latest\",\n    \"remarkable\": \"2.0.1\"\n  },\n  \"scripts\": {\n    \"start\": \"react-scripts start\",\n    \"build\": \"react-scripts build\",\n    \"test\": \"react-scripts test --env=jsdom\",\n    \"eject\": \"react-scripts eject\"\n  }\n}\n```\n\n```css\nlabel {\n  display: block;\n}\n\ninput, textarea {\n  margin-bottom: 10px;\n}\n\nbody {\n  min-height: 200px;\n}\n```\n\n</Sandpack>\n\n이 데모는 인위적인 지연으로 로딩됩니다. 다음에 체크박스를 선택 해제하고 다시 선택하면 `Preview`가 캐시 되어 로딩 상태가 되지 않습니다. 로딩 상태를 다시 보려면 샌드박스에서 \"Reset\"을 클릭하세요.\n\n[Suspense를 사용하여 로딩 상태를 관리하는 방법에 대해 자세히 알아보세요.](/reference/react/Suspense)\n\n---\n\n## 문제 해결 {/*troubleshooting*/}\n\n### `lazy` 컴포넌트의 상태가 의도치 않게 재설정됩니다. {/*my-lazy-components-state-gets-reset-unexpectedly*/}\n\n`lazy` 컴포넌트를 다른 컴포넌트 내부에서 선언하지 마세요.\n\n```js {4-5}\nimport { lazy } from 'react';\n\nfunction Editor() {\n  // 🔴 잘못된 방법: 이렇게 하면 다시 렌더링할 때 모든 상태가 재설정됩니다.\n  const MarkdownPreview = lazy(() => import('./MarkdownPreview.js'));\n  // ...\n}\n```\n\n대신 항상 모듈의 최상위 수준에서 선언하세요.\n\n```js {3-4}\nimport { lazy } from 'react';\n\n// ✅ 올바른 방법: `lazy` 컴포넌트를 컴포넌트 외부에 선언합니다.\nconst MarkdownPreview = lazy(() => import('./MarkdownPreview.js'));\n\nfunction Editor() {\n  // ...\n}\n```\n"
  },
  {
    "path": "src/content/reference/react/legacy.md",
    "content": "---\ntitle: \"Legacy React API\"\n---\n\n<Intro>\n\n아래 API는 `react` 패키지에서 내보냈지만<sup>Exported</sup> 새로 작성할 코드에서는 권장하지 않습니다. 링크를 통해 각각의 API 페이지에서 제시한 대안을 확인해주세요.\n\n</Intro>\n\n---\n\n## Legacy APIs {/*legacy-apis*/}\n\n* [`Children`](/reference/react/Children)은 `children` Prop으로 받은 JSX를 조작하고 변형할 수 있습니다. [대안 확인하기](/reference/react/Children#alternatives).\n* [`cloneElement`](/reference/react/cloneElement)를 통해 다른 엘리먼트를 시작점으로 사용하여 React 엘리먼트를 생성할 수 있습니다. [대안 확인하기](/reference/react/cloneElement#alternatives).\n* [`Component`](/reference/react/Component)는 자바스크립트 클래스로써 React 컴포넌트를 정의합니다. [대안 확인하기](/reference/react/Component#alternatives).\n* [`createElement`](/reference/react/createElement)로 React 엘리먼트를 생성합니다. 일반적으로 JSX를 대신 사용합니다.\n* [`createRef`](/reference/react/createRef)는 임의의 값을 포함할 수 있는 참조 객체를 생성합니다. [대안 확인하기](/reference/react/createRef#alternatives).\n* [`forwardRef`](/reference/react/forwardRef)는 컴포넌트가 [ref](/learn/manipulating-the-dom-with-refs)로 DOM 노드를 부모 컴포넌트에 노출시킵니다.\n* [`isValidElement`](/reference/react/isValidElement)는 값의 React 엘리먼트 여부를 확인합니다. 일반적으로 [`cloneElement`](/reference/react/cloneElement)와 함께 사용합니다.\n* [`PureComponent`](/reference/react/PureComponent)는 [`Component`](/reference/react/Component)와 유사하지만, 동일한 Prop의 재렌더링은 생략합니다. [대안 확인하기](/reference/react/PureComponent#alternatives).\n\n---\n\n## Removed APIs {/*removed-apis*/}\n\n아래 API들은 React 19에서 제거되었습니다.\n\n* [`createFactory`](https://18.react.dev/reference/react/createFactory): 대신 JSX를 사용하세요.\n* 클래스 컴포넌트: [`static contextTypes`](https://18.react.dev//reference/react/Component#static-contexttypes): 대신 [`static contextType`](#static-contexttype)를 사용하세요.\n* 클래스 컴포넌트: [`static childContextTypes`](https://18.react.dev//reference/react/Component#static-childcontexttypes): 대신 [`static contextType`](#static-contexttype)를 사용하세요.\n* 클래스 컴포넌트: [`static getChildContext`](https://18.react.dev//reference/react/Component#getchildcontext): 대신 [`Context`](/reference/react/createContext#provider)를 사용하세요.\n* 클래스 컴포넌트: [`static propTypes`](https://18.react.dev//reference/react/Component#static-proptypes): 대신 [TypeScript](https://www.typescriptlang.org/)같은 타입 시스템을 사용하세요.\n* 클래스 컴포넌트: [`this.refs`](https://18.react.dev//reference/react/Component#refs): 대신 [`createRef`](/reference/react/createRef)를 사용하세요.\n"
  },
  {
    "path": "src/content/reference/react/memo.md",
    "content": "---\ntitle: memo\n---\n\n<Intro>\n\n`memo`를 사용하면 컴포넌트의 Props가 변경되지 않은 경우 리렌더링을 건너뛸 수 있습니다.\n\n```\nconst MemoizedComponent = memo(SomeComponent, arePropsEqual?)\n```\n\n</Intro>\n\n<Note>\n\n[React 컴파일러](/learn/react-compiler)는 모든 컴포넌트에 `memo`와 동일한 최적화를 자동으로 적용하므로 수동으로 메모이제이션을 할 필요가 줄어듭니다. 컴파일러를 사용해 컴포넌트 메모이제이션을 자동으로 처리할 수 있습니다.\n\n</Note>\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `memo(Component, arePropsEqual?)` {/*memo*/}\n\n컴포넌트를 `memo`로 감싸면 해당 컴포넌트의 메모된<sup>Memoized</sup> 버전을 얻을 수 있습니다. 메모된 버전의 컴포넌트는 일반적으로 부모 컴포넌트가 리렌더링 되어도 Props가 변경되지 않았다면 리렌더링되지 않습니다. 그러나 메모이제이션은 성능을 최적화하는 것이지, 보장하는 것은 아니기 때문에 React는 여전히 다시 렌더링될 수도 있습니다.\n\n```js\nimport { memo } from 'react';\n\nconst SomeComponent = memo(function SomeComponent(props) {\n  // ...\n});\n```\n\n[아래 예시를 참고하세요.](#usage)\n\n#### 매개변수 {/*parameters*/}\n\n* `Component`: 메모<sup>Memoize</sup>하려는 컴포넌트입니다. `memo`는 이 컴포넌트를 수정하지 않고 대신 새로운 메모된 컴포넌트를 반환합니다. 함수와 [`forwardRef`](/reference/react/forwardRef) 컴포넌트를 포함한 모든 유효한 React 컴포넌트가 허용됩니다.\n\n* **optional** `arePropsEqual`: 컴포넌트의 이전 Props와 새로운 Props의 두 가지 인수를 받는 함수입니다. 이전 Props와 새로운 Props가 동일한 경우, 컴포넌트가 이전 Props와 동일한 결과를 렌더링하고 새로운 Props에서도 이전 Props와 동일한 방식으로 동작하는 경우 `true`를 반환해야 합니다. 그렇지 않으면 `false`를 반환해야 합니다. 일반적으로 이 함수를 지정하지 않습니다. React는 기본적으로 [`Object.is`](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Object/is)로 각 Props를 비교합니다.\n\n#### 반환값 {/*returns*/}\n\n`memo`는 새로운 React 컴포넌트를 반환합니다. `memo`에 제공한 컴포넌트와 동일하게 동작하지만, 부모가 리렌더링되더라도 Props가 변경되지 않는 한 React는 이를 리렌더링하지 않습니다.\n\n---\n\n## 사용법 {/*usage*/}\n\n### Props가 변경되지 않았을 때 리렌더링 건너뛰기 {/*skipping-re-rendering-when-props-are-unchanged*/}\n\nReact는 일반적으로 부모가 리렌더링될 때마다 컴포넌트를 리렌더링합니다. `memo`를 사용하면, 새로운 Props가 이전 Props와 같으면 부모 컴포넌트가 다시 렌더링되더라도 React가 해당 컴포넌트를 다시 렌더링하지 않도록 만들 수 있습니다. 이러한 컴포넌트를 메모된<sup>Memoized</sup> 상태라고 합니다.\n\n컴포넌트를 메모하려면 `memo`로 감싸고 기존 컴포넌트 대신에 반환된 값을 사용하세요.\n\n```js\nconst Greeting = memo(function Greeting({ name }) {\n  return <h1>Hello, {name}!</h1>;\n});\n\nexport default Greeting;\n```\nReact 컴포넌트는 항상 [순수한 렌더링 로직](/learn/keeping-components-pure)을 가져야 합니다. 이는 Props, State 그리고 Context가 변경되지 않으면 항상 동일한 결과를 반환해야 함을 의미합니다. `memo`를 사용하면 컴포넌트가 이 요구 사항을 준수한다고 알리므로, Props가 변경되지 않는 한 React는 리렌더링 될 필요가 없습니다. `memo`를 사용하더라도 컴포넌트의 State가 변경되거나 사용 중인 Context가 변경되면 리렌더링 됩니다.\n\n아래 예시에서 `Greeting` 컴포넌트는 `name`이 Props 중 하나이기 때문에 `name`이 변경될 때마다 리렌더링 됩니다. 하지만 `address`는 `Greeting`의 Props가 아니기 때문에 `address`가 변경될 때는 리렌더링되지 않습니다.\n\n<Sandpack>\n\n```js\nimport { memo, useState } from 'react';\n\nexport default function MyApp() {\n  const [name, setName] = useState('');\n  const [address, setAddress] = useState('');\n  return (\n    <>\n      <label>\n        Name{': '}\n        <input value={name} onChange={e => setName(e.target.value)} />\n      </label>\n      <label>\n        Address{': '}\n        <input value={address} onChange={e => setAddress(e.target.value)} />\n      </label>\n      <Greeting name={name} />\n    </>\n  );\n}\n\nconst Greeting = memo(function Greeting({ name }) {\n  console.log(\"Greeting was rendered at\", new Date().toLocaleTimeString());\n  return <h3>Hello{name && ', '}{name}!</h3>;\n});\n```\n\n```css\nlabel {\n  display: block;\n  margin-bottom: 16px;\n}\n```\n\n</Sandpack>\n\n<Note>\n\n**`memo`는 성능 최적화를 위해서 사용해야 합니다.** `memo` 없이 코드가 작동하지 않는다면, 먼저 근본적인 문제를 찾아서 해결하세요. 이후에 `memo`를 추가하여 성능을 개선할 수 있습니다.\n\n</Note>\n\n<DeepDive>\n\n#### 모든 곳에 `memo`를 추가해야할까요? {/*should-you-add-memo-everywhere*/}\n\nIf your app is like this site, and most interactions are coarse (like replacing a page or an entire section), memoization is usually unnecessary. On the other hand, if your app is more like a drawing editor, and most interactions are granular (like moving shapes), then you might find memoization very helpful.\n\n`memo`로 최적화하는 것은 컴포넌트가 정확히 동일한 Props로 자주 리렌더링 되고, 리렌더링 로직이 비용이 많이 드는 경우에만 유용합니다. 컴포넌트가 리렌더링 될 때 인지할 수 있을 만큼의 지연이 없다면 `memo`가 필요하지 않습니다. `memo`는 객체 또는 렌더링 중에 정의된 일반 함수처럼 *항상 다른* Props가 컴포넌트에 전달되는 경우에 완전히 무용지물입니다. 따라서 `memo`와 함께 [`useMemo`](/reference/react/useMemo#skipping-re-rendering-of-components)와 [`useCallback`](/reference/react/useCallback#skipping-re-rendering-of-components)이 종종 필요합니다.\n\n그 외의 경우에는 컴포넌트를 `memo`로 감싸는 이점이 없습니다. 그렇다고 해서 크게 해가 되지 않기 때문에 일부 팀에서는 개별 사례에 대해 고려하지 않고 가능한 한 많이 메모이제이션하는 방식을 선택하기도 합니다. 이 접근 방식은 코드 가독성이 떨어진다는 단점이 있습니다. 또한 모든 메모이제이션이 효과적이지는 않습니다. 항상 변경되는 값이 하나라도 있다면, 컴포넌트 전체의 메모이제이션을 중단하기에 충분합니다.\n\n**실제로 몇가지 원칙을 따르면 메모이제이션이 불필요할 수 있습니다.**\n\n1. 컴포넌트가 다른 컴포넌트를 시각적으로 감쌀 때 [JSX를 자식으로 받아들이도록 하세요.](/learn/passing-props-to-a-component#passing-jsx-as-children) 이렇게 하면 래퍼 컴포넌트가 자신의 State를 업데이트할 때 React는 그 자식 컴포넌트가 리렌더링 될 필요가 없다는 것을 알 수 있습니다.\n2. 지역 State를 선호하고 필요 이상으로 [State 끌어올리기](/learn/sharing-state-between-components)를 하지 마세요. 예를 들어, 최상위 트리나 전역 State 라이브러리에 폼이나 아이템이 호버<sup>Hover</sup>되었는지와 같은 일시적인 State를 두지 마세요.\n3. [렌더링 로직을 순수하게](/learn/keeping-components-pure) 유지하세요. 컴포넌트를 렌더링했을 때 문제가 발생하거나 눈에 띄는 시각적 아티팩트가 생성된다면 컴포넌트에 버그가 있는 것입니다! 메모이제이션하는 대신 버그를 수정하세요.\n4. [State를 업데이트하는 불필요한 Effect](/learn/you-might-not-need-an-effect)를 피하세요. React 앱에서 대부분의 성능 문제는 컴포넌트를 반복해서 렌더링하게 만드는 Effect에서 발생하는 일련의 업데이트로 인해 발생합니다.\n5. [Effect에서 불필요한 의존성을 제거](/learn/removing-effect-dependencies)하세요. 예를 들어, 메모이제이션 대신에 일부 객체나 함수를 Effect 내부나 컴포넌트 외부로 이동하는 것이 더 간단할 때가 많습니다.\n\n특정 상호작용이 여전히 느리게 느껴진다면 [React 개발자 도구 Profiler](https://legacy.reactjs.org/blog/2018/09/10/introducing-the-react-profiler.html)를 사용해 어떤 컴포넌트가 메모이제이션을 통해 가장 큰 이점을 얻을 수 있는지 확인하고 필요한 경우에 메모이제이션하세요. 이러한 원칙은 컴포넌트를 더 쉽게 디버깅하고 이해할 수 있게 해주므로 어떤 경우든 이 원칙을 따르는 것이 좋습니다. 장기적으로는 이 문제를 완전히 해결하기 위해 [세분된 메모이제이션을 자동으로 수행하는 방법](https://www.youtube.com/watch?v=lGEMwh32soc)을 연구하고 있습니다.\n\n</DeepDive>\n\n---\n\n### State를 사용해 메모이제이션된 컴포넌트 업데이트하기 {/*updating-a-memoized-component-using-state*/}\n\n컴포넌트가 메모이제이션된 경우에도, 컴포넌트의 State가 변경되면 리렌더링됩니다. 메모이제이션은 부모에서 컴포넌트로 전달되는 Props에만 적용됩니다.\n\n<Sandpack>\n\n```js\nimport { memo, useState } from 'react';\n\nexport default function MyApp() {\n  const [name, setName] = useState('');\n  const [address, setAddress] = useState('');\n  return (\n    <>\n      <label>\n        Name{': '}\n        <input value={name} onChange={e => setName(e.target.value)} />\n      </label>\n      <label>\n        Address{': '}\n        <input value={address} onChange={e => setAddress(e.target.value)} />\n      </label>\n      <Greeting name={name} />\n    </>\n  );\n}\n\nconst Greeting = memo(function Greeting({ name }) {\n  console.log('Greeting was rendered at', new Date().toLocaleTimeString());\n  const [greeting, setGreeting] = useState('Hello');\n  return (\n    <>\n      <h3>{greeting}{name && ', '}{name}!</h3>\n      <GreetingSelector value={greeting} onChange={setGreeting} />\n    </>\n  );\n});\n\nfunction GreetingSelector({ value, onChange }) {\n  return (\n    <>\n      <label>\n        <input\n          type=\"radio\"\n          checked={value === 'Hello'}\n          onChange={e => onChange('Hello')}\n        />\n        Regular greeting\n      </label>\n      <label>\n        <input\n          type=\"radio\"\n          checked={value === 'Hello and welcome'}\n          onChange={e => onChange('Hello and welcome')}\n        />\n        Enthusiastic greeting\n      </label>\n    </>\n  );\n}\n```\n\n```css\nlabel {\n  display: block;\n  margin-bottom: 16px;\n}\n```\n\n</Sandpack>\n\nState 변수를 현재 값으로 설정하면 React는 `memo` 없이도 컴포넌트 리렌더링을 건너뜁니다. 컴포넌트가 한 번 더 호출될 수 있지만, 결과는 무시됩니다.\n\n---\n\n### Context를 사용하여 메모화된 컴포넌트 업데이트하기 {/*updating-a-memoized-component-using-a-context*/}\n\n컴포넌트가 메모되었더라도, 사용 중인 Context가 변경될 때 컴포넌트는 리렌더링됩니다. 메모는 부모로부터 전달되는 Props에만 적용됩니다.\n\n<Sandpack>\n\n```js\nimport { createContext, memo, useContext, useState } from 'react';\n\nconst ThemeContext = createContext(null);\n\nexport default function MyApp() {\n  const [theme, setTheme] = useState('dark');\n\n  function handleClick() {\n    setTheme(theme === 'dark' ? 'light' : 'dark');\n  }\n\n  return (\n    <ThemeContext value={theme}>\n      <button onClick={handleClick}>\n        Switch theme\n      </button>\n      <Greeting name=\"Taylor\" />\n    </ThemeContext>\n  );\n}\n\nconst Greeting = memo(function Greeting({ name }) {\n  console.log(\"Greeting was rendered at\", new Date().toLocaleTimeString());\n  const theme = useContext(ThemeContext);\n  return (\n    <h3 className={theme}>Hello, {name}!</h3>\n  );\n});\n```\n\n```css\nlabel {\n  display: block;\n  margin-bottom: 16px;\n}\n\n.light {\n  color: black;\n  background-color: white;\n}\n\n.dark {\n  color: white;\n  background-color: black;\n}\n```\n\n</Sandpack>\n\n일부 Context의 <em>일정 부분</em>이 변경될 때만 컴포넌트가 리렌더링되도록 하려면 컴포넌트를 두 개로 나눠야 합니다. 외부 컴포넌트의 Context에서 필요한 내용을 읽고, 메모화된 자식에게 Prop으로 전달하세요.\n\n---\n\n### Props 변경 최소화하기 {/*minimizing-props-changes*/}\n\n`memo`를 사용할 때 어떤 Prop든 이전의 Prop과 *얕은 비교 결과*가 같지 않을 때마다 컴포넌트가 리렌더링 됩니다. 즉 React는 [`Object.is`](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Object/is) 비교를 사용하여 컴포넌트의 모든 Prop을 이전 값과 비교합니다. `Object.is(3, 3)`는 `true`이지만 `Object.is({}, {})`는 `false`입니다.\n\n\n`memo`를 최대한 활용하려면, Props가 변경되는 횟수를 최소화해야 합니다. 예를 들어 Prop이 객체인 경우, [`useMemo`](/reference/react/useMemo)를 사용하여 부모 컴포넌트가 해당 객체를 매번 다시 만드는 것을 방지하세요.\n\n```js {5-8}\nfunction Page() {\n  const [name, setName] = useState('Taylor');\n  const [age, setAge] = useState(42);\n\n  const person = useMemo(\n    () => ({ name, age }),\n    [name, age]\n  );\n\n  return <Profile person={person} />;\n}\n\nconst Profile = memo(function Profile({ person }) {\n  // ...\n});\n```\n\nProps의 변경을 최소화하는 더 좋은 방법은 컴포넌트가 Props에 필요한 최소한의 정보만 받도록 하는 것입니다. 예를 들어, 전체 객체 대신 개별 값을 받을 수 있습니다.\n\n```js {4,7}\nfunction Page() {\n  const [name, setName] = useState('Taylor');\n  const [age, setAge] = useState(42);\n  return <Profile name={name} age={age} />;\n}\n\nconst Profile = memo(function Profile({ name, age }) {\n  // ...\n});\n```\n\n때로는 개별 값도 자주 변경되지 않는 값으로 사용할 수 있습니다. 예를 들어 다음 컴포넌트는 값 자체가 아니라 값의 존재를 나타내는 불리언 값을 받습니다.\n\n```js {3}\nfunction GroupsLanding({ person }) {\n  const hasGroups = person.groups !== null;\n  return <CallToAction hasGroups={hasGroups} />;\n}\n\nconst CallToAction = memo(function CallToAction({ hasGroups }) {\n  // ...\n});\n```\n메모화된 컴포넌트에 함수를 전달해야 하는 경우, 컴포넌트 외부에 함수를 선언하여 변경되지 않도록 하거나, [`useCallback`](/reference/react/useCallback#skipping-re-rendering-of-components)을 사용하여 리렌더링 사이에 함수의 선언을 캐시합니다.\n\n---\n\n### 사용자 정의 비교 함수 지정하기 {/*specifying-a-custom-comparison-function*/}\n\n드물지만 메모화된 컴포넌트의 Props 변경을 최소화하는 것이 불가능할 수 있습니다. 이 경우 사용자 정의 비교 함수를 제공하여 React가 얕은 비교를 사용하는 대신에 이전 Props와 새로운 Props를 비교할 수 있습니다. 이 함수는 `memo`의 두 번째 인수로 전달됩니다. 새로운 Props가 이전 Props와 동일한 결과를 생성하는 경우에만 `true`를 반환해야 합니다. 그렇지 않으면 `false`를 반환해야 합니다.\n\n```js {3}\nconst Chart = memo(function Chart({ dataPoints }) {\n  // ...\n}, arePropsEqual);\n\nfunction arePropsEqual(oldProps, newProps) {\n  return (\n    oldProps.dataPoints.length === newProps.dataPoints.length &&\n    oldProps.dataPoints.every((oldPoint, index) => {\n      const newPoint = newProps.dataPoints[index];\n      return oldPoint.x === newPoint.x && oldPoint.y === newPoint.y;\n    })\n  );\n}\n```\n\n이 경우 브라우저 개발자 도구의 성능 패널을 사용하여 비교 기능이 실제로 컴포넌트를 다시 렌더링하는 것보다 빠른지 확인하세요. 놀랄 수도 있습니다.\n\n성능 측정을 할 때, React가 프로덕션 환경에서 실행되고 있는지 확인하세요.\n\n<Pitfall>\n\n`arePropsEqual`를 구현하는 경우 **함수를 포함하여 모든 Prop를 비교해야 합니다.** 함수는 종종 부모 컴포넌트의 Props와 State를 [클로저<sup>Closure</sup>로 다룹니다](https://developer.mozilla.org/ko/docs/Web/JavaScript/Closures). `oldProps.onClick !== newProps.onClick`일 때 `true`를 반환하면 컴포넌트가 `onClick` 핸들러 내에서 이전 렌더링의 Props와 State를 계속 \"인식\"하여 매우 혼란스러운 버그가 발생할 수 있습니다.\n\n작업 중인 데이터 구조가 알려진 제한된 깊이를 가지고 있다고 100% 확신하지 않는 한, `arePropsEqual` 내에서 깊은 비교를 수행하지 마세요. **깊은 비교는 매우 느려질 수 있으며** 나중에 누군가 데이터 구조를 변경하면 앱이 잠깐 정지될 수 있습니다.\n\n</Pitfall>\n\n---\n\n### Do I still need React.memo if I use React Compiler? {/*react-compiler-memo*/}\n\nWhen you enable [React Compiler](/learn/react-compiler), you typically don't need `React.memo` anymore. The compiler automatically optimizes component re-rendering for you.\n\nHere's how it works:\n\n**Without React Compiler**, you need `React.memo` to prevent unnecessary re-renders:\n\n```js\n// Parent re-renders every second\nfunction Parent() {\n  const [seconds, setSeconds] = useState(0);\n\n  useEffect(() => {\n    const interval = setInterval(() => {\n      setSeconds(s => s + 1);\n    }, 1000);\n    return () => clearInterval(interval);\n  }, []);\n\n  return (\n    <>\n      <h1>Seconds: {seconds}</h1>\n      <ExpensiveChild name=\"John\" />\n    </>\n  );\n}\n\n// Without memo, this re-renders every second even though props don't change\nconst ExpensiveChild = memo(function ExpensiveChild({ name }) {\n  console.log('ExpensiveChild rendered');\n  return <div>Hello, {name}!</div>;\n});\n```\n\n**With React Compiler enabled**, the same optimization happens automatically:\n\n```js\n// No memo needed - compiler prevents re-renders automatically\nfunction ExpensiveChild({ name }) {\n  console.log('ExpensiveChild rendered');\n  return <div>Hello, {name}!</div>;\n}\n```\n\nHere's the key part of what the React Compiler generates:\n\n```js {6-12}\nfunction Parent() {\n  const $ = _c(7);\n  const [seconds, setSeconds] = useState(0);\n  // ... other code ...\n\n  let t3;\n  if ($[4] === Symbol.for(\"react.memo_cache_sentinel\")) {\n    t3 = <ExpensiveChild name=\"John\" />;\n    $[4] = t3;\n  } else {\n    t3 = $[4];\n  }\n  // ... return statement ...\n}\n```\n\nNotice the highlighted lines: The compiler wraps `<ExpensiveChild name=\"John\" />` in a cache check. Since the `name` prop is always `\"John\"`, this JSX is created once and reused on every parent re-render. This is exactly what `React.memo` does - it prevents the child from re-rendering when its props haven't changed.\n\nThe React Compiler automatically:\n1. Tracks that the `name` prop passed to `ExpensiveChild` hasn't changed\n2. Reuses the previously created JSX for `<ExpensiveChild name=\"John\" />`\n3. Skips re-rendering `ExpensiveChild` entirely\n\nThis means **you can safely remove `React.memo` from your components when using React Compiler**. The compiler provides the same optimization automatically, making your code cleaner and easier to maintain.\n\n<Note>\n\nThe compiler's optimization is actually more comprehensive than `React.memo`. It also memoizes intermediate values and expensive computations within your components, similar to combining `React.memo` with `useMemo` throughout your component tree.\n\n</Note>\n\n---\n\n## Troubleshooting {/*troubleshooting*/}\n### My component re-renders when a prop is an object, array, or function {/*my-component-rerenders-when-a-prop-is-an-object-or-array*/}\n\nReact는 얕은 비교를 기준으로 이전 Props와 새로운 Props를 비교합니다. 즉, 각각의 새로운 Prop가 이전 Prop와 참조가 동일한지 여부를 고려합니다. 부모가 리렌더링 될 때마다 새로운 객체나 배열을 생성하면, 개별 요소들이 모두 동일하더라도 React는 여전히 변경된 것으로 간주합니다. 마찬가지로 부모 컴포넌트를 렌더링할 때 새로운 함수를 만들면 React는 함수의 정의가 동일하더라도 변경된 것으로 간주합니다. 이를 방지하려면 [부모 컴포넌트에서 Props를 단순화하거나 메모화 하세요.](#minimizing-props-changes)\n"
  },
  {
    "path": "src/content/reference/react/startTransition.md",
    "content": "---\ntitle: startTransition\n---\n\n<Intro>\n\n`startTransition`을 사용하면 UI의 일부를 백그라운드에서 렌더링할 수 있습니다.\n\n```js\nstartTransition(action)\n```\n\n</Intro>\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `startTransition(action)` {/*starttransition*/}\n\n`startTransition` 함수는 State 업데이트를 Transition으로 표시할 수 있게 해줍니다.\n\n```js {7,9}\nimport { startTransition } from 'react';\n\nfunction TabContainer() {\n  const [tab, setTab] = useState('about');\n\n  function selectTab(nextTab) {\n    startTransition(() => {\n      setTab(nextTab);\n    });\n  }\n  // ...\n}\n```\n\n[아래 예시를 참고하세요.](#usage)\n\n#### 매개변수 {/*parameters*/}\n\n* `action`: 하나 이상의 [`set` 함수](/reference/react/useState#setstate)를 호출하여 일부 State를 업데이트하는 함수입니다. React는 매개변수 없이 `action`을 즉시 호출하고 `action` 함수를 호출하는 동안 동기적으로 예약된 모든 State 업데이트를 Transition으로 표시합니다. `action`에서 await된 비동기 호출은 Transition에 포함되지만, 현재로서는 `await` 이후의 `set` 함수 호출을 추가적인 `startTransition`으로 감싸야 합니다([문제 해결 참조](/reference/react/useTransition#react-doesnt-treat-my-state-update-after-await-as-a-transition)). Transitions으로 표시된 상태 업데이트는 [non-blocking](#marking-a-state-update-as-a-non-blocking-transition) 방식으로 처리되며, [불필요한 로딩 표시가 나타나지 않습니다](/reference/react/useTransition#preventing-unwanted-loading-indicators).\n\n#### 반환값 {/*returns*/}\n\n`startTransition`은 아무것도 반환하지 않습니다.\n\n#### 주의 사항 {/*caveats*/}\n\n* `startTransition`은 Transition이 대기<sup>Pending</sup> 중인지 추적할 수 있는 방법을 제공하지 않습니다. 대기 중인 Transition을 표시하려면 [`useTransition`](/reference/react/useTransition)이 필요합니다.\n\n* 해당 State의 `set` 함수에 접근할 수 있는 경우에만 업데이트를 Transition으로 래핑할 수 있습니다. 일부 Props나 Custom Hook 반환 값에 대한 응답으로 Transition을 시작하려면 [`useDeferredValue`](/reference/react/useDeferredValue)를 대신 사용하세요.\n\n* `startTransition`에 전달하는 함수는 즉시 호출되며, 실행 중 발생하는 모든 상태 업데이트를 Transition으로 표시합니다. 예를 들어 `setTimeout` 내에서 상태를 업데이트하려고 하면, 해당 업데이트는 Transition으로 표시되지 않습니다.\n\n* 비동기 요청 이후의 State 업데이트를 Transition으로 표시하려면, 반드시 또 다른 `startTransition`으로 감싸야 합니다. 이는 알려진 제한 사항으로 향후 수정될 예정입니다. ([문제 해결 참조](/reference/react/useTransition#react-doesnt-treat-my-state-update-after-await-as-a-transition))\n\n* Transition으로 표시된 State 업데이트는 다른 State 업데이트에 의해 중단됩니다. 예를 들어, Transition 내에서 차트 컴포넌트를 업데이트하지만 차트가 다시 렌더링되는 동안 입력을 시작하면 React는 입력 State 업데이트를 처리한 후 차트 컴포넌트에서 렌더링 작업을 다시 시작합니다.\n\n* Transition 업데이트는 텍스트 입력을 제어하는 데 사용할 수 없습니다.\n\n* 만약 진행 중인 Transition이 여러 개 있는 경우, React에서는 함께 일괄 처리 합니다. 이는 향후 릴리즈에서 제거될 가능성이 높은 제한 사항입니다.\n\n---\n\n## 사용법 {/*usage*/}\n\n### State 업데이트를 Non-Blocking Transition으로 표시 {/*marking-a-state-update-as-a-non-blocking-transition*/}\n\n`startTransition`으로 래핑함으로써 State 업데이트를 *Transition*으로 표시할 수 있습니다.\n\n```js {7,9}\nimport { startTransition } from 'react';\n\nfunction TabContainer() {\n  const [tab, setTab] = useState('about');\n\n  function selectTab(nextTab) {\n    startTransition(() => {\n      setTab(nextTab);\n    });\n  }\n  // ...\n}\n```\n\nTransition을 사용하면 느린 장치에서도 사용자 인터페이스 업데이트의 반응성을 유지할 수 있습니다.\n\nTransition을 사용하면 UI가 리렌더링 도중에도 반응성을 유지합니다. 예를 들어 사용자가 탭을 클릭 했다가 마음이 바뀌어 다른 탭을 클릭하면 첫 번째 리렌더링이 완료될 때 까지 기다릴 필요 없이 다른 탭을 클릭할 수 있습니다.\n\n<Note>\n\n`startTransition`은 [`useTransition`](/reference/react/useTransition)과 매우 유사하지만, Transition이 대기 중인지 추적하는 `isPending` 플래그를 제공하지 않습니다. `useTransition`을 사용할 수 없을 때 `startTransition`을 호출할 수 있습니다. 예를 들어, `startTransition`은 데이터 라이브러리에서와 같이 컴포넌트 외부에서 작동합니다.\n\n[Transition에 대한 학습 및 예시는 `useTransition` 페이지에서 확인하세요.](/reference/react/useTransition)\n\n\n</Note>\n"
  },
  {
    "path": "src/content/reference/react/use.md",
    "content": "---\ntitle: use\n---\n\n<Intro>\n\n`use`는 [Promise](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Promise)나 [Context](/learn/passing-data-deeply-with-context)와 같은 데이터를 참조하는 React API입니다.\n\n```js\nconst value = use(resource);\n```\n\n</Intro>\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `use(resource)` {/*use*/}\n\n컴포넌트에서 [Promise](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Promise)나 [Context](/learn/passing-data-deeply-with-context)와 같은 데이터를 참조하려면 `use`를 사용하세요.\n\n```jsx\nimport { use } from 'react';\n\nfunction MessageComponent({ messagePromise }) {\n  const message = use(messagePromise);\n  const theme = use(ThemeContext);\n  // ...\n```\n\n다른 React Hook과 달리 `use`는 `if`와 같은 조건문과 반복문 내부에서 호출할 수 있습니다. 다만, 다른 React Hook과 같이 `use`는 컴포넌트 또는 Hook에서만 호출해야 합니다.\n\nPromise와 함께 호출될 때 `use` API는 [`Suspense`](/reference/react/Suspense) 및 [Error Boundary](/reference/react/Component#catching-rendering-errors-with-an-error-boundary)와 통합됩니다. `use`에 전달된 Promise가 대기<sup>Pending</sup>하는 동안 `use`를 호출하는 컴포넌트는 *Suspend*됩니다. `use`를 호출하는 컴포넌트가 Suspense 경계로 둘러싸여 있으면 Fallback이 표시됩니다. Promise가 리졸브되면 Suspense Fallback은 `use` API가 반환한 컴포넌트로 대체됩니다. `use`에 전달된 Promise가 Reject되면 가장 가까운 Error Boundary의 Fallback이 표시됩니다.\n\n[아래 예시를 참고하세요.](#usage)\n\n#### 매개변수 {/*parameters*/}\n\n* `resource`: 참조하려는 데이터입니다. 데이터는 [Promise](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Promise)나 [Context](/learn/passing-data-deeply-with-context)일 수 있습니다.\n\n#### 반환값 {/*returns*/}\n\n`use` Hook은 [Promise](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Promise)나 [Context](/learn/passing-data-deeply-with-context)에서 참조한 값을 반환합니다.\n\n#### 주의 사항 {/*caveats*/}\n\n* `use` API는 컴포넌트나 Hook 내부에서 호출되어야 합니다.\n* [서버 컴포넌트](/reference/rsc/use-server)에서 데이터를 가져올 때는 `use`보다 `async` 및 `await`을 사용합니다. `async` 및 `await`은 `await`이 호출된 시점부터 렌더링을 시작하는 반면, `use`는 데이터가 리졸브된 후 컴포넌트를 리렌더링합니다.\n* [클라이언트 컴포넌트](/reference/rsc/use-client)에서 Promise를 생성하는 것보다 [서버 컴포넌트](/reference/rsc/use-server)에서 Promise를 생성하여 클라이언트 컴포넌트에 전달하는 것이 좋습니다. 클라이언트 컴포넌트에서 생성된 Promise는 렌더링할 때마다 다시 생성됩니다. 서버 컴포넌트에서 클라이언트 컴포넌트로 전달된 Promise는 리렌더링 전반에 걸쳐 안정적입니다. [예시를 확인하세요](#streaming-data-from-server-to-client).\n\n---\n\n## 사용법 {/*usage*/}\n\n### `use`를 사용하여 Context 참조하기 {/*reading-context-with-use*/}\n\n[Context](/learn/passing-data-deeply-with-context)가 `use`에 전달되면 [`useContext`](/reference/react/useContext)와 유사하게 작동합니다. `useContext`는 컴포넌트의 최상위 수준에서 호출해야 하지만, `use`는 `if`와 같은 조건문이나 `for`와 같은 반복문 내부에서 호출할 수 있습니다. `use`는 유연하므로 `useContext`보다 선호됩니다.\n\n```js [[2, 4, \"theme\"], [1, 4, \"ThemeContext\"]]\nimport { use } from 'react';\n\nfunction Button() {\n  const theme = use(ThemeContext);\n  // ...\n```\n\n`use`는 전달한 <CodeStep step={1}>Context</CodeStep>의 <CodeStep step={2}>Context Value</CodeStep>를 반환합니다. Context 값을 결정하기 위해 React는 컴포넌트 트리를 탐색하고 **위에서 가장 가까운 Context Provider**를 찾습니다.\n\nContext를 `Button`에 전달하려면 `Button` 또는 상위 컴포넌트 중 하나를 Context Provider로 래핑합니다.\n\n```js [[1, 3, \"ThemeContext\"], [2, 3, \"\\\\\"dark\\\\\"\"], [1, 5, \"ThemeContext\"]]\nfunction MyPage() {\n  return (\n    <ThemeContext value=\"dark\">\n      <Form />\n    </ThemeContext>\n  );\n}\n\nfunction Form() {\n  // ... 버튼 렌더링 ...\n}\n```\n\nProvider와 `Button` 사이에 얼마나 많은 컴포넌트가 있는지는 중요하지 않습니다. `Form` 내부의 *어느 곳이든* `Button`이 `use(ThemeContext)`를 호출하면 `\"dark\"`를 값으로 받습니다.\n\n[`useContext`](/reference/react/useContext)와 달리, <CodeStep step={2}>`use`</CodeStep>는 <CodeStep step={1}>`if`</CodeStep>와 같은 조건문과 반복문 내부에서 호출할 수 있습니다.\n\n```js [[1, 2, \"if\"], [2, 3, \"use\"]]\nfunction HorizontalRule({ show }) {\n  if (show) {\n    const theme = use(ThemeContext);\n    return <hr className={theme} />;\n  }\n  return false;\n}\n```\n\n<CodeStep step={2}>`use`</CodeStep>는 <CodeStep step={1}>`if`</CodeStep> 내부에서 호출되므로 Context에서 조건부로 값을 참조할 수 있습니다.\n\n<Pitfall>\n\n`useContext`와 마찬가지로, `use(context)`는 항상 이를 호출하는 컴포넌트의 **위쪽에서** 가장 가까운 Context Provider를 찾습니다. 위쪽으로 탐색하며, `use(context)`를 호출하는 컴포넌트 내부의 Context Provider는 고려하지 **않습니다**.\n\n</Pitfall>\n\n<Sandpack>\n\n```js\nimport { createContext, use } from 'react';\n\nconst ThemeContext = createContext(null);\n\nexport default function MyApp() {\n  return (\n    <ThemeContext value=\"dark\">\n      <Form />\n    </ThemeContext>\n  )\n}\n\nfunction Form() {\n  return (\n    <Panel title=\"Welcome\">\n      <Button show={true}>Sign up</Button>\n      <Button show={false}>Log in</Button>\n    </Panel>\n  );\n}\n\nfunction Panel({ title, children }) {\n  const theme = use(ThemeContext);\n  const className = 'panel-' + theme;\n  return (\n    <section className={className}>\n      <h1>{title}</h1>\n      {children}\n    </section>\n  )\n}\n\nfunction Button({ show, children }) {\n  if (show) {\n    const theme = use(ThemeContext);\n    const className = 'button-' + theme;\n    return (\n      <button className={className}>\n        {children}\n      </button>\n    );\n  }\n  return false\n}\n```\n\n```css\n.panel-light,\n.panel-dark {\n  border: 1px solid black;\n  border-radius: 4px;\n  padding: 20px;\n}\n.panel-light {\n  color: #222;\n  background: #fff;\n}\n\n.panel-dark {\n  color: #fff;\n  background: rgb(23, 32, 42);\n}\n\n.button-light,\n.button-dark {\n  border: 1px solid #777;\n  padding: 5px;\n  margin-right: 10px;\n  margin-top: 10px;\n}\n\n.button-dark {\n  background: #222;\n  color: #fff;\n}\n\n.button-light {\n  background: #fff;\n  color: #222;\n}\n```\n\n</Sandpack>\n\n### 서버에서 클라이언트로 데이터 스트리밍하기 {/*streaming-data-from-server-to-client*/}\n\n<CodeStep step={1}>서버 컴포넌트</CodeStep>에서 <CodeStep step={2}>클라이언트 컴포넌트</CodeStep>로 Promise Prop을 전달하여 서버에서 클라이언트로 데이터를 스트리밍할 수 있습니다.\n\n```js [[1, 4, \"App\"], [2, 2, \"Message\"], [3, 7, \"Suspense\"], [4, 8, \"messagePromise\", 30], [4, 5, \"messagePromise\"]]\nimport { fetchMessage } from './lib.js';\nimport { Message } from './message.js';\n\nexport default function App() {\n  const messagePromise = fetchMessage();\n  return (\n    <Suspense fallback={<p>waiting for message...</p>}>\n      <Message messagePromise={messagePromise} />\n    </Suspense>\n  );\n}\n```\n\n<CodeStep step={2}>클라이언트 컴포넌트</CodeStep>는 <CodeStep step={4}> Prop으로 받은 Promise</CodeStep>를 <CodeStep step={5}>`use`</CodeStep> API에 전달합니다. <CodeStep step={2}>클라이언트 컴포넌트</CodeStep>는 서버 컴포넌트가 처음에 생성한 <CodeStep step={4}>Promise</CodeStep>에서 값을 읽을 수 있습니다.\n\n```js [[2, 6, \"Message\"], [4, 6, \"messagePromise\"], [4, 7, \"messagePromise\"], [5, 7, \"use\"]]\n// message.js\n'use client';\n\nimport { use } from 'react';\n\nexport function Message({ messagePromise }) {\n  const messageContent = use(messagePromise);\n  return <p>Here is the message: {messageContent}</p>;\n}\n```\n<CodeStep step={2}>`Message`</CodeStep>는 <CodeStep step={3}>[`Suspense`](/reference/react/Suspense)</CodeStep>로 래핑되어 있으므로 Promise가 리졸브될 때까지 Fallback이 표시됩니다. Promise가 리졸브되면 <CodeStep step={5}>`use`</CodeStep> Hook이 값을 참조하고 <CodeStep step={2}>`Message`</CodeStep> 컴포넌트가 Suspense Fallback을 대체합니다.\n\n<Sandpack>\n\n```js src/message.js active\n\"use client\";\n\nimport { use, Suspense } from \"react\";\n\nfunction Message({ messagePromise }) {\n  const messageContent = use(messagePromise);\n  return <p>Here is the message: {messageContent}</p>;\n}\n\nexport function MessageContainer({ messagePromise }) {\n  return (\n    <Suspense fallback={<p>⌛Downloading message...</p>}>\n      <Message messagePromise={messagePromise} />\n    </Suspense>\n  );\n}\n```\n\n```js src/App.js hidden\nimport { useState } from \"react\";\nimport { MessageContainer } from \"./message.js\";\n\nfunction fetchMessage() {\n  return new Promise((resolve) => setTimeout(resolve, 1000, \"⚛️\"));\n}\n\nexport default function App() {\n  const [messagePromise, setMessagePromise] = useState(null);\n  const [show, setShow] = useState(false);\n  function download() {\n    setMessagePromise(fetchMessage());\n    setShow(true);\n  }\n\n  if (show) {\n    return <MessageContainer messagePromise={messagePromise} />;\n  } else {\n    return <button onClick={download}>Download message</button>;\n  }\n}\n```\n\n```js src/index.js hidden\nimport React, { StrictMode } from 'react';\nimport { createRoot } from 'react-dom/client';\nimport './styles.css';\n\n// TODO: 이 예시를 업데이트하여 작성한 후 Codesandbox Server Component 데모 환경을 사용합니다\nimport App from './App';\n\nconst root = createRoot(document.getElementById('root'));\nroot.render(\n  <StrictMode>\n    <App />\n  </StrictMode>\n);\n```\n\n</Sandpack>\n\n<Note>\n\n서버 컴포넌트에서 클라이언트 컴포넌트로 Promise를 전달할 때 리졸브된 값이 직렬화 가능해야 합니다. 함수는 직렬화할 수 없으므로 Promise의 리졸브 값이 될 수 없습니다.\n\n</Note>\n\n\n<DeepDive>\n\n#### Promise를 서버 컴포넌트에서 처리해야 하나요, 아니면 클라이언트 컴포넌트에서 처리해야 하나요? {/*resolve-promise-in-server-or-client-component*/}\n\nPromise는 서버 컴포넌트에서 클라이언트 컴포넌트로 전달할 수 있으며 `use` API를 통해 클라이언트 컴포넌트에서 리졸브됩니다. 또한 서버 컴포넌트에서 `await`을 사용하여 Promise를 리졸브하고 데이터를 클라이언트 컴포넌트에 Prop으로 전달하는 방법도 존재합니다.\n\n```js\nexport default async function App() {\n  const messageContent = await fetchMessage();\n  return <Message messageContent={messageContent} />\n}\n```\n\n하지만 [서버 컴포넌트](/reference/rsc/server-components)에서 `await`을 사용하면 `await` 문이 완료될 때까지 렌더링이 차단됩니다. 서버 컴포넌트에서 클라이언트 컴포넌트로 Promise를 Prop으로 전달하면 Promise가 서버 컴포넌트의 렌더링을 차단하는 것을 방지할 수 있습니다.\n\n</DeepDive>\n\n### 거부된 Promise 처리하기 {/*dealing-with-rejected-promises*/}\n\n경우에 따라 `use`에 전달된 Promise가 거부될 수 있습니다. 거부된 프로미스를 처리하는 방법은 2가지가 존재합니다.\n\n1. [Error Boundary를 사용하여 오류 표시하기](#displaying-an-error-to-users-with-error-boundary)\n2. [`Promise.catch`로 대체 값 제공하기](#providing-an-alternative-value-with-promise-catch)\n\n<Pitfall>\n\n`use`는 `try`-`catch` 블록에서 호출할 수 없습니다. `try`-`catch` 블록 대신 [컴포넌트를 Error Boundary로 래핑]((#displaying-an-error-to-users-with-error-boundary))하거나, Promise의 [`catch` 메서드를 사용하여 대체 값을 제공해야 합니다.]((#providing-an-alternative-value-with-promise-catch))\n</Pitfall>\n\n#### Error Boundary를 사용하여 오류 표시하기 {/*displaying-an-error-to-users-with-error-boundary*/}\n\nPromise가 거부될 때 오류를 표시하고 싶다면 [Error Boundary](/reference/react/Component#catching-rendering-errors-with-an-error-boundary)를 사용합니다. Error Boundary를 사용하려면 `use` API 를 호출하는 컴포넌트를 Error Boundary로 래핑합니다. `use`에 전달된 Promise가 거부되면 Error Boundary에 대한 Fallback이 표시됩니다.\n\n<Sandpack>\n\n```js src/message.js active\n\"use client\";\n\nimport { use, Suspense } from \"react\";\nimport { ErrorBoundary } from \"react-error-boundary\";\n\nexport function MessageContainer({ messagePromise }) {\n  return (\n    <ErrorBoundary fallback={<p>⚠️Something went wrong</p>}>\n      <Suspense fallback={<p>⌛Downloading message...</p>}>\n        <Message messagePromise={messagePromise} />\n      </Suspense>\n    </ErrorBoundary>\n  );\n}\n\nfunction Message({ messagePromise }) {\n  const content = use(messagePromise);\n  return <p>Here is the message: {content}</p>;\n}\n```\n\n```js src/App.js hidden\nimport { useState } from \"react\";\nimport { MessageContainer } from \"./message.js\";\n\nfunction fetchMessage() {\n  return new Promise((resolve, reject) => setTimeout(reject, 1000));\n}\n\nexport default function App() {\n  const [messagePromise, setMessagePromise] = useState(null);\n  const [show, setShow] = useState(false);\n  function download() {\n    setMessagePromise(fetchMessage());\n    setShow(true);\n  }\n\n  if (show) {\n    return <MessageContainer messagePromise={messagePromise} />;\n  } else {\n    return <button onClick={download}>Download message</button>;\n  }\n}\n```\n\n```js src/index.js hidden\nimport React, { StrictMode } from 'react';\nimport { createRoot } from 'react-dom/client';\nimport './styles.css';\n\n// TODO: 이 예시를 업데이트하여 작성한 후 Codesandbox Server Component 데모 환경을 사용합니다\nimport App from './App';\n\nconst root = createRoot(document.getElementById('root'));\nroot.render(\n  <StrictMode>\n    <App />\n  </StrictMode>\n);\n```\n\n```json package.json hidden\n{\n  \"dependencies\": {\n    \"react\": \"19.0.0\",\n    \"react-dom\": \"19.0.0\",\n    \"react-scripts\": \"^5.0.0\",\n    \"react-error-boundary\": \"4.0.3\"\n  },\n  \"main\": \"/index.js\"\n}\n```\n</Sandpack>\n\n#### `Promise.catch`로 대체 값 제공하기 {/*providing-an-alternative-value-with-promise-catch*/}\n\n`use`에 전달된 Promise가 거부될 때 대체 값을 제공하려면 Promise의 <CodeStep step={1}>[`catch`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/catch)</CodeStep> 메서드를 사용합니다.\n\n```js [[1, 6, \"catch\"],[2, 7, \"return\"]]\nimport { Message } from './message.js';\n\nexport default function App() {\n  const messagePromise = new Promise((resolve, reject) => {\n    reject();\n  }).catch(() => {\n    return \"no new message found.\";\n  });\n\n  return (\n    <Suspense fallback={<p>waiting for message...</p>}>\n      <Message messagePromise={messagePromise} />\n    </Suspense>\n  );\n}\n```\n\nPromise의 <CodeStep step={1}>`catch`</CodeStep> 메서드를 사용하려면 Promise 객체에서 <CodeStep step={1}>`catch`</CodeStep>를 호출합니다. <CodeStep step={1}>`catch`</CodeStep>는 오류 메시지를 인수로 받는 함수를 인수로 받습니다. <CodeStep step={1}>`catch`</CodeStep>에 전달된 함수가 <CodeStep step={2}>반환</CodeStep>하는 값은 모두 Promise의 리졸브 값으로 사용됩니다.\n\n---\n\n## 문제 해결 {/*troubleshooting*/}\n\n\n### \"Suspense Exception: This is not a real error!\" {/*suspense-exception-error*/}\n\nReact 컴포넌트 또는 Hook 함수 외부에서, 혹은 `try`-`catch` 블록에서 `use`를 호출하고 있는 경우입니다. `try`-`catch` 블록 내에서 `use`를 호출하는 경우 컴포넌트를 Error Boundary로 래핑하거나 Promise의 `catch`를 호출하여 오류를 발견하고 Promise를 다른 값으로 리졸브합니다. [이러한 예시들을 확인하세요](#dealing-with-rejected-promises).\n\n\n```jsx\nfunction MessageComponent({messagePromise}) {\n  function download() {\n    // ❌ `use`를 호출하는 함수가 컴포넌트나 Hook이 아닙니다.\n    const message = use(messagePromise);\n    // ...\n```\n\n대신, 컴포넌트 클로저 외부에서 `use`를 호출하세요. 여기서 `use`를 호출하는 함수는 컴포넌트 또는 Hook입니다.\n\n```jsx\nfunction MessageComponent({messagePromise}) {\n  // ✅ `use`를 컴포넌트에서 호출하고 있습니다.\n  const message = use(messagePromise);\n  // ...\n```\n"
  },
  {
    "path": "src/content/reference/react/useActionState.md",
    "content": "---\ntitle: useActionState\n---\n\n<Intro>\n\n`useActionState`는 폼 액션의 결과를 기반으로 State를 업데이트할 수 있도록 제공하는 Hook입니다.\n\n```js\nconst [state, dispatchAction, isPending] = useActionState(reducerAction, initialState, permalink?);\n```\n\n</Intro>\n\n<Note>\n\n이전 React Canary 버전에서는 이 API가 React DOM에 포함되어 있었고, `useFormState`라고 불렸습니다.\n\n</Note>\n\n\n\n```js\nimport { useActionState } from 'react';\n\nfunction reducerAction(previousState, actionPayload) {\n  // ...\n}\n\nfunction StatefulForm({}) {\n  const [state, formAction] = useActionState(increment, 0);\n  return (\n    <form>\n      {state}\n      <button formAction={formAction}>Increment</button>\n    </form>\n  );\n}\n```\n\n폼 State는 폼을 마지막으로 제출했을 때 액션에서 반환되는 값입니다. 아직 폼을 제출하지 않았다면, `initialState`로 설정됩니다.\n\n서버 함수<sup>Server Function</sup>와 함께 사용하는 경우, `useActionState`를 통해 하이드레이션<sup>Hydration</sup>이 끝나기 전에도 폼 제출에 대한 서버 응답을 표시할 수 있습니다.\n\n[아래 예시를 참고하세요.](#usage)\n\n#### 매개변수 {/*parameters*/}\n\n* `fn`: 폼이 제출되거나 버튼이 눌렸을 때 호출되는 함수입니다. 함수가 호출되면 첫 번째 인수로 폼의 이전 State(처음에는 전달한 `initialState`, 이후에는 이전 반환값)가 전달되고, 그 뒤로는 폼 액션이 일반적으로 받는 인수들이 전달됩니다.\n* `initialState`: State가 처음에 가지기를 원하는 값입니다. 이는 직렬화 가능한 값이면 무엇이든 될 수 있습니다. 이 인수는 액션이 처음 호출된 후에는 무시됩니다.\n* **optional** `permalink`: 이 폼이 수정하는 고유한 페이지 URL을 포함하는 문자열입니다. 동적 콘텐츠가 있는 페이지(예: 피드)에서 점진적 향상<sup>Progressive Enhancement</sup>과 함께 사용됩니다. 만약 `fn`이 [서버 함수](/reference/rsc/server-functions)이고, 폼이 자바스크립트 번들이 로드되기 전에 제출되면, 브라우저는 현재 페이지의 URL 대신 지정된 영구 링크<sup>Permalink</sup> URL로 이동합니다. React가 State를 전달하는 방법을 알 수 있도록, 동일한 폼 컴포넌트가 대상 페이지에 렌더링되도록 해야 합니다. (동일한 액션 `fn`과 `permalink` 포함.) 폼이 하이드레이션된 후, 이 매개변수는 더 이상 효과가 없습니다.\n\n{/* TODO T164397693: link to serializable values docs once it exists */}\n\n#### 반환값 {/*returns*/}\n\n`useActionState`는 다음 세 가지 값을 담은 배열을 반환합니다.\n\n1. 현재 State입니다. 첫 렌더링 시에는 `initialState`와 일치하며, 액션이 실행된 후에는 해당 액션이 반환한 값과 일치합니다.\n2. `form` 컴포넌트의 `action` Prop이나, 폼 내부 `button` 컴포넌트의 `formAction` Prop에 전달할 수 있는 새 액션입니다. 이 액션은 [`startTransition`](/reference/react/startTransition) 내에서 수동으로 호출할 수도 있습니다.\n3. 현재 Transition이 대기 중인지 알려주는 `isPending` 플래그입니다.\n\n\n#### 주의 사항 {/*caveats*/}\n\n* React 서버 컴포넌트를 지원하는 프레임워크에서 `useActionState`를 사용하면, 클라이언트 자바스크립트 실행 전에도 폼과 상호작용할 수 있습니다. 만약 서버 컴포넌트를 사용하지 않는다면, 이는 단순히 컴포넌트 지역 State와 동일하게 동작합니다.\n* `useActionState`에 전달된 함수는 첫 번째 인수로 이전 또는 초기 State를 추가로 받습니다. 즉, 직접 폼 액션을 사용했을 때와 비교해 함수의 시그니처가 달라질 수 있습니다.\n\n---\n\n## 사용법 {/*usage*/}\n\n### 폼 액션에서 반환된 정보 사용하기 {/*using-information-returned-by-a-form-action*/}\n\n컴포넌트의 최상위 레벨에서 `useActionState`를 호출하면, 폼이 마지막으로 제출되었을 때 액션이 반환한 값에 접근할 수 있습니다.\n\n```js [[1, 7, \"count\"], [2, 7, \"dispatchAction\"], [3, 7, \"isPending\"]]\nimport { useActionState } from 'react';\n\nasync function addToCartAction(prevCount) {\n  // ...\n}\nfunction Counter() {\n  const [count, dispatchAction, isPending] = useActionState(addToCartAction, 0);\n\n  // ...\n}\n```\n\n`useActionState`가 반환하는 배열은 다음과 같은 요소를 갖습니다.\n\n1. 폼의 <CodeStep step={1}>현재 State</CodeStep>는, 처음에는 전달한 <CodeStep step={4}>초기 State</CodeStep>로 설정되며, 폼이 제출된 후에는 전달한 <CodeStep step={3}>액션</CodeStep>의 반환값으로 설정됩니다.\n2. `<form>`의 `action` Prop에 전달하거나 `startTransition` 안에서 직접 호출할 수 있는<CodeStep step={2}>새로운 액션</CodeStep>입니다.\n3. 액션이 처리되는 동안 사용할 수 있는 <CodeStep step={1}>대기<sup>Pending</sup> State</CodeStep>입니다.\n\n\n폼이 제출되면, 제공한 <CodeStep step={3}>액션</CodeStep> 함수가 호출되며, 해당 함수의 반환값이 새로운 <CodeStep step={1}>현재 State</CodeStep>로 설정됩니다.\n\n이 <CodeStep step={3}>액션</CodeStep> 함수는 첫 번째 인수로 <CodeStep step={1}>현재 State</CodeStep>를 추가로 전달받습니다.\n처음 제출될 때는<CodeStep step={4}>초기 State</CodeStep>가 전달되며, 이후 제출부터는 직전 호출 시 반환된 값이 전달됩니다. 나머지 인수들은 useActionState를 사용하지 않았을 때와 동일합니다.\n\n\n```js [[3, 1, \"action\"], [1, 1, \"currentState\"]]\nfunction action(currentState, formData) {\n  // ...\n  return 'next state';\n}\n```\n\n#### 오류 표시하기 {/*display-form-errors*/}\n\n서버 함수<sup>Server Function</sup>에서 반환된 오류 메시지나 토스트 메시지를 표시하려면, 해당 액션을 `useActionState`로 감싸주세요.\n\n<Sandpack>\n\n```js src/App.js\nimport { useActionState } from \"react\";\nimport { addToCart } from \"./actions.js\";\n\nexport default function Checkout() {\n  const [count, dispatchAction, isPending] = useActionState(async (prevCount) => {\n    return await addToCart(prevCount)\n  }, 0);\n\nexport default function App() {\n  return (\n    <>\n      <AddToCartForm itemID=\"1\" itemTitle=\"JavaScript: The Definitive Guide\" />\n      <AddToCartForm itemID=\"2\" itemTitle=\"JavaScript: The Good Parts\" />\n    </>\n  );\n}\n```\n\n```js src/actions.js\n\"use server\";\n\nexport async function addToCart(prevState, queryData) {\n  const itemID = queryData.get('itemID');\n  if (itemID === \"1\") {\n    return \"Added to cart\";\n  } else {\n    // Add a fake delay to make waiting noticeable.\n    await new Promise(resolve => {\n      setTimeout(resolve, 2000);\n    });\n  }\n\n  return (\n    <div className=\"checkout\">\n      <h2>Checkout</h2>\n      <div className=\"row\">\n        <span>Eras Tour Tickets</span>\n        <span>Qty: {count}</span>\n      </div>\n      <div className=\"row\">\n        <button onClick={handleClick}>Add Ticket{isPending ? ' 🌀' : '  '}</button>\n      </div>\n      <hr />\n      <Total quantity={count} />\n    </div>\n  );\n}\n```\n\n```css src/styles.css hidden\nform {\n  border: solid 1px black;\n  margin-bottom: 24px;\n  padding: 12px;\n}\n\nexport default function Total({quantity}) {\n  return (\n    <div className=\"row total\">\n      <span>Total</span>\n      <span>{formatter.format(quantity * 9999)}</span>\n    </div>\n  );\n}\n```\n\n```js src/api.js\nexport async function addToCart(count) {\n  await new Promise(resolve => setTimeout(resolve, 1000));\n  return count + 1;\n}\n\nexport async function removeFromCart(count) {\n  await new Promise(resolve => setTimeout(resolve, 1000));\n  return Math.max(0, count - 1);\n}\n```\n\n```css\n.checkout {\n  display: flex;\n  flex-direction: column;\n  gap: 12px;\n  padding: 16px;\n  border: 1px solid #ccc;\n  border-radius: 8px;\n  font-family: system-ui;\n}\n\n.checkout h2 {\n  margin: 0 0 8px 0;\n}\n\n.row {\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n}\n\n.row button {\n  margin-left: auto;\n  min-width: 150px;\n}\n\n.total {\n  font-weight: bold;\n}\n\nhr {\n  width: 100%;\n  border: none;\n  border-top: 1px solid #ccc;\n  margin: 4px 0;\n}\n\nbutton {\n  padding: 8px 16px;\n  cursor: pointer;\n}\n```\n\n</Sandpack>\n\nEvery time you click \"Add Ticket,\" React queues a call to `addToCartAction`. React shows the pending state until all the tickets are added, and then re-renders with the final state.\n\n<DeepDive>\n\n#### How `useActionState` queuing works {/*how-useactionstate-queuing-works*/}\n\nTry clicking \"Add Ticket\" multiple times. Every time you click, a new `addToCartAction` is queued. Since there's an artificial 1 second delay, that means 4 clicks will take ~4 seconds to complete.\n\n**This is intentional in the design of `useActionState`.**\n\nWe have to wait for the previous result of `addToCartAction` in order to pass the `prevCount` to the next call to `addToCartAction`. That means React has to wait for the previous Action to finish before calling the next Action.\n\nYou can typically solve this by [using with useOptimistic](/reference/react/useActionState#using-with-useoptimistic) but for more complex cases you may want to consider [cancelling queued actions](#cancelling-queued-actions) or not using `useActionState`.\n\n</DeepDive>\n\n#### 폼 제출 후 구조화된 정보 표시하기 {/*display-structured-information-after-submitting-a-form*/}\n\n서버 함수<sup>Server Function</sup>의 반환값은 직렬화 가능한 어떤 값이든 가능합니다. 예를 들어, 액션 성공 여부를 나타내는 불리언, 오류 메시지, 업데이트된 객체 등 다양하게 활용할 수 있습니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { useActionState, startTransition } from 'react';\nimport { addToCart, removeFromCart } from './api';\nimport Total from './Total';\n\nexport default function Checkout() {\n  const [count, dispatchAction, isPending] = useActionState(updateCartAction, 0);\n\n  function handleAdd() {\n    startTransition(() => {\n      dispatchAction({ type: 'ADD' });\n    });\n  }\n\n  function handleRemove() {\n    startTransition(() => {\n      dispatchAction({ type: 'REMOVE' });\n    });\n  }\n\n  return (\n    <div className=\"checkout\">\n      <h2>Checkout</h2>\n      <div className=\"row\">\n        <span>Eras Tour Tickets</span>\n        <span className=\"stepper\">\n          <span className=\"qty\">{isPending ? '🌀' : count}</span>\n          <span className=\"buttons\">\n            <button onClick={handleAdd}>▲</button>\n            <button onClick={handleRemove}>▼</button>\n          </span>\n        </span>\n      </div>\n      <hr />\n      <Total quantity={count} isPending={isPending}/>\n    </div>\n  );\n}\n\nasync function updateCartAction(prevCount, actionPayload) {\n  switch (actionPayload.type) {\n    case 'ADD': {\n      return await addToCart(prevCount);\n    }\n    case 'REMOVE': {\n      return await removeFromCart(prevCount);\n    }\n  }\n  return prevCount;\n}\n```\n\n```js src/Total.js\nconst formatter = new Intl.NumberFormat('en-US', {\n  style: 'currency',\n  currency: 'USD',\n  minimumFractionDigits: 0,\n});\n\nexport default function Total({quantity, isPending}) {\n  return (\n    <div className=\"row total\">\n      <span>Total</span>\n      {isPending ? '🌀 Updating...' : formatter.format(quantity * 9999)}\n    </div>\n  );\n}\n```\n\n```js src/api.js hidden\nexport async function addToCart(count) {\n  await new Promise(resolve => setTimeout(resolve, 1000));\n  return count + 1;\n}\n\nexport async function removeFromCart(count) {\n  await new Promise(resolve => setTimeout(resolve, 1000));\n  return Math.max(0, count - 1);\n}\n```\n\n```css\n.checkout {\n  display: flex;\n  flex-direction: column;\n  gap: 12px;\n  padding: 16px;\n  border: 1px solid #ccc;\n  border-radius: 8px;\n  font-family: system-ui;\n}\n\n.checkout h2 {\n  margin: 0 0 8px 0;\n}\n\n.row {\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n}\n\n.stepper {\n  display: flex;\n  align-items: center;\n  gap: 8px;\n}\n\n.qty {\n  min-width: 20px;\n  text-align: center;\n}\n\n.buttons {\n  display: flex;\n  flex-direction: column;\n  gap: 2px;\n}\n\n.buttons button {\n  padding: 0 8px;\n  font-size: 10px;\n  line-height: 1.2;\n  cursor: pointer;\n}\n\n.pending {\n  width: 20px;\n  text-align: center;\n}\n\n.total {\n  font-weight: bold;\n}\n\nhr {\n  width: 100%;\n  border: none;\n  border-top: 1px solid #ccc;\n  margin: 4px 0;\n}\n```\n\n</Sandpack>\n\nWhen you click to increase or decrease the quantity, an `\"ADD\"` or `\"REMOVE\"` is dispatched. In the `reducerAction`, different APIs are called to update the quantity.\n\nIn this example, we use the pending state of the Actions to replace both the quantity and the total. If you want to provide immediate feedback, such as immediately updating the quantity, you can use `useOptimistic`.\n\n<DeepDive>\n\n#### How is `useActionState` different from `useReducer`? {/*useactionstate-vs-usereducer*/}\n\nYou might notice this example looks a lot like `useReducer`, but they serve different purposes:\n\n- **Use `useReducer`** to manage state of your UI. The reducer must be pure.\n\n- **Use `useActionState`** to manage state of your Actions. The reducer can perform side effects.\n\nYou can think of `useActionState` as `useReducer` for side effects from user Actions. Since it computes the next Action to take based on the previous Action, it has to [order the calls sequentially](/reference/react/useActionState#how-useactionstate-queuing-works). If you want to perform Actions in parallel, use `useState` and `useTransition` directly.\n\n</DeepDive>\n\n---\n\n### Using with `useOptimistic` {/*using-with-useoptimistic*/}\n\nYou can combine `useActionState` with [`useOptimistic`](/reference/react/useOptimistic) to show immediate UI feedback:\n\n\n<Sandpack>\n\n```js src/App.js\nimport { useActionState, startTransition, useOptimistic } from 'react';\nimport { addToCart, removeFromCart } from './api';\nimport Total from './Total';\n\nexport default function Checkout() {\n  const [count, dispatchAction, isPending] = useActionState(updateCartAction, 0);\n  const [optimisticCount, setOptimisticCount] = useOptimistic(count);\n\n  function handleAdd() {\n    startTransition(() => {\n      setOptimisticCount(c => c + 1);\n      dispatchAction({ type: 'ADD' });\n    });\n  }\n\n  function handleRemove() {\n    startTransition(() => {\n      setOptimisticCount(c => c - 1);\n      dispatchAction({ type: 'REMOVE' });\n    });\n  }\n\n  return (\n    <div className=\"checkout\">\n      <h2>Checkout</h2>\n      <div className=\"row\">\n        <span>Eras Tour Tickets</span>\n        <span className=\"stepper\">\n          <span className=\"pending\">{isPending && '🌀'}</span>\n          <span className=\"qty\">{optimisticCount}</span>\n          <span className=\"buttons\">\n            <button onClick={handleAdd}>▲</button>\n            <button onClick={handleRemove}>▼</button>\n          </span>\n        </span>\n      </div>\n      <hr />\n      <Total quantity={optimisticCount} isPending={isPending}/>\n    </div>\n  );\n}\n\nasync function updateCartAction(prevCount, actionPayload) {\n  switch (actionPayload.type) {\n    case 'ADD': {\n      return await addToCart(prevCount);\n    }\n    case 'REMOVE': {\n      return await removeFromCart(prevCount);\n    }\n  }\n  return prevCount;\n}\n```\n\n```js src/Total.js\nconst formatter = new Intl.NumberFormat('en-US', {\n  style: 'currency',\n  currency: 'USD',\n  minimumFractionDigits: 0,\n});\n\nexport default function Total({quantity, isPending}) {\n  return (\n    <div className=\"row total\">\n      <span>Total</span>\n      <span>{isPending ? '🌀 Updating...' : formatter.format(quantity * 9999)}</span>\n    </div>\n  );\n}\n```\n\n```js src/api.js hidden\nexport async function addToCart(count) {\n  await new Promise(resolve => setTimeout(resolve, 1000));\n  return count + 1;\n}\n\nexport async function removeFromCart(count) {\n  await new Promise(resolve => setTimeout(resolve, 1000));\n  return Math.max(0, count - 1);\n}\n```\n\n```css\n.checkout {\n  display: flex;\n  flex-direction: column;\n  gap: 12px;\n  padding: 16px;\n  border: 1px solid #ccc;\n  border-radius: 8px;\n  font-family: system-ui;\n}\n\n.checkout h2 {\n  margin: 0 0 8px 0;\n}\n\n.row {\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n}\n\n.stepper {\n  display: flex;\n  align-items: center;\n  gap: 8px;\n}\n\n.qty {\n  min-width: 20px;\n  text-align: center;\n}\n\n.buttons {\n  display: flex;\n  flex-direction: column;\n  gap: 2px;\n}\n\n.buttons button {\n  padding: 0 8px;\n  font-size: 10px;\n  line-height: 1.2;\n  cursor: pointer;\n}\n\n.pending {\n  width: 20px;\n  text-align: center;\n}\n\n.total {\n  font-weight: bold;\n}\n\nhr {\n  width: 100%;\n  border: none;\n  border-top: 1px solid #ccc;\n  margin: 4px 0;\n}\n```\n\n</Sandpack>\n\n\n`setOptimisticCount` immediately updates the quantity, and `dispatchAction()` queues the `updateCartAction`. A pending indicator appears on both the quantity and total to give the user feedback that their update is still being applied.\n\n---\n\n\n### Using with Action props {/*using-with-action-props*/}\n\nWhen you pass the `dispatchAction` function to a component that exposes an [Action prop](/reference/react/useTransition#exposing-action-props-from-components), you don't need to call `startTransition` or `useOptimistic` yourself.\n\nThis example shows using the `increaseAction` and `decreaseAction` props of a QuantityStepper component:\n\n<Sandpack>\n\n```js src/App.js\nimport { useActionState } from 'react';\nimport { addToCart, removeFromCart } from './api';\nimport QuantityStepper from './QuantityStepper';\nimport Total from './Total';\n\nexport default function Checkout() {\n  const [count, dispatchAction, isPending] = useActionState(updateCartAction, 0);\n\n  function addAction() {\n    dispatchAction({type: 'ADD'});\n  }\n\n  function removeAction() {\n    dispatchAction({type: 'REMOVE'});\n  }\n\n  return (\n    <div className=\"checkout\">\n      <h2>Checkout</h2>\n      <div className=\"row\">\n        <span>Eras Tour Tickets</span>\n        <QuantityStepper\n          value={count}\n          increaseAction={addAction}\n          decreaseAction={removeAction}\n        />\n      </div>\n      <hr />\n      <Total quantity={count} isPending={isPending} />\n    </div>\n  );\n}\n\nasync function updateCartAction(prevCount, actionPayload) {\n  switch (actionPayload.type) {\n    case 'ADD': {\n      return await addToCart(prevCount);\n    }\n    case 'REMOVE': {\n      return await removeFromCart(prevCount);\n    }\n  }\n  return prevCount;\n}\n```\n\n```js src/QuantityStepper.js\nimport { startTransition, useOptimistic } from 'react';\n\nexport default function QuantityStepper({value, increaseAction, decreaseAction}) {\n  const [optimisticValue, setOptimisticValue] = useOptimistic(value);\n  const isPending = value !== optimisticValue;\n  function handleIncrease() {\n    startTransition(async () => {\n      setOptimisticValue(c => c + 1);\n      await increaseAction();\n    });\n  }\n\n  function handleDecrease() {\n    startTransition(async () => {\n      setOptimisticValue(c => Math.max(0, c - 1));\n      await decreaseAction();\n    });\n  }\n\n  return (\n    <span className=\"stepper\">\n      <span className=\"pending\">{isPending && '🌀'}</span>\n      <span className=\"qty\">{optimisticValue}</span>\n      <span className=\"buttons\">\n        <button onClick={handleIncrease}>▲</button>\n        <button onClick={handleDecrease}>▼</button>\n      </span>\n    </span>\n  );\n}\n```\n\n```js src/Total.js\nconst formatter = new Intl.NumberFormat('en-US', {\n  style: 'currency',\n  currency: 'USD',\n  minimumFractionDigits: 0,\n});\n\nexport default function Total({quantity, isPending}) {\n  return (\n    <div className=\"row total\">\n      <span>Total</span>\n      {isPending ? '🌀 Updating...' : formatter.format(quantity * 9999)}\n    </div>\n  );\n}\n```\n\n```js src/api.js hidden\nexport async function addToCart(count) {\n  await new Promise(resolve => setTimeout(resolve, 1000));\n  return count + 1;\n}\n\nexport async function removeFromCart(count) {\n  await new Promise(resolve => setTimeout(resolve, 1000));\n  return Math.max(0, count - 1);\n}\n```\n\n```css\n.checkout {\n  display: flex;\n  flex-direction: column;\n  gap: 12px;\n  padding: 16px;\n  border: 1px solid #ccc;\n  border-radius: 8px;\n  font-family: system-ui;\n}\n\n.checkout h2 {\n  margin: 0 0 8px 0;\n}\n\n.row {\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n}\n\n.stepper {\n  display: flex;\n  align-items: center;\n  gap: 8px;\n}\n\n.qty {\n  min-width: 20px;\n  text-align: center;\n}\n\n.buttons {\n  display: flex;\n  flex-direction: column;\n  gap: 2px;\n}\n\n.buttons button {\n  padding: 0 8px;\n  font-size: 10px;\n  line-height: 1.2;\n  cursor: pointer;\n}\n\n.pending {\n  width: 20px;\n  text-align: center;\n}\n\n.total {\n  font-weight: bold;\n}\n\nhr {\n  width: 100%;\n  border: none;\n  border-top: 1px solid #ccc;\n  margin: 4px 0;\n}\n```\n\n</Sandpack>\n\nSince `<QuantityStepper>` has built-in support for transitions, pending state, and optimistically updating the count, you just need to tell the Action _what_ to change, and _how_ to change it is handled for you.\n\n---\n\n### Cancelling queued Actions {/*cancelling-queued-actions*/}\n\nYou can use an `AbortController` to cancel pending Actions:\n\n<Sandpack>\n\n```js src/App.js\nimport { useActionState, useRef } from 'react';\nimport { addToCart, removeFromCart } from './api';\nimport QuantityStepper from './QuantityStepper';\nimport Total from './Total';\n\nexport default function Checkout() {\n  const abortRef = useRef(null);\n  const [count, dispatchAction, isPending] = useActionState(updateCartAction, 0);\n  \n  async function addAction() {\n    if (abortRef.current) {\n      abortRef.current.abort();\n    }\n    abortRef.current = new AbortController();\n    await dispatchAction({ type: 'ADD', signal: abortRef.current.signal });\n  }\n\n  async function removeAction() {\n    if (abortRef.current) {\n      abortRef.current.abort();\n    }\n    abortRef.current = new AbortController();\n    await dispatchAction({ type: 'REMOVE', signal: abortRef.current.signal });\n  }\n\n  return (\n    <div className=\"checkout\">\n      <h2>Checkout</h2>\n      <div className=\"row\">\n        <span>Eras Tour Tickets</span>\n        <QuantityStepper\n          value={count}\n          increaseAction={addAction}\n          decreaseAction={removeAction}\n        />\n      </div>\n      <hr />\n      <Total quantity={count} isPending={isPending} />\n    </div>\n  );\n}\n\nasync function updateCartAction(prevCount, actionPayload) {\n  switch (actionPayload.type) {\n    case 'ADD': {\n      try {\n        return await addToCart(prevCount, { signal: actionPayload.signal });\n      } catch (e) {\n        return prevCount + 1;\n      }\n    }\n    case 'REMOVE': {\n      try {\n        return await removeFromCart(prevCount, { signal: actionPayload.signal });\n      } catch (e) {\n        return Math.max(0, prevCount - 1);\n      }\n    }\n  }\n  return prevCount;\n}\n```\n\n```js src/QuantityStepper.js\nimport { startTransition, useOptimistic } from 'react';\n\nexport default function QuantityStepper({value, increaseAction, decreaseAction}) {\n  const [optimisticValue, setOptimisticValue] = useOptimistic(value);\n  const isPending = value !== optimisticValue;\n  function handleIncrease() {\n    startTransition(async () => {\n      setOptimisticValue(c => c + 1);\n      await increaseAction();\n    });\n  }\n\n  function handleDecrease() {\n    startTransition(async () => {\n      setOptimisticValue(c => Math.max(0, c - 1));\n      await decreaseAction();\n    });\n  }\n\n  return (\n          <span className=\"stepper\">\n      <span className=\"pending\">{isPending && '🌀'}</span>\n      <span className=\"qty\">{optimisticValue}</span>\n      <span className=\"buttons\">\n        <button onClick={handleIncrease}>▲</button>\n        <button onClick={handleDecrease}>▼</button>\n      </span>\n    </span>\n  );\n}\n```\n\n```js src/Total.js\nconst formatter = new Intl.NumberFormat('en-US', {\n  style: 'currency',\n  currency: 'USD',\n  minimumFractionDigits: 0,\n});\n\nexport default function Total({quantity, isPending}) {\n  return (\n    <div className=\"row total\">\n      <span>Total</span>\n      {isPending ? '🌀 Updating...' : formatter.format(quantity * 9999)}\n    </div>\n  );\n}\n```\n\n```js src/api.js hidden\nclass AbortError extends Error {\n  name = 'AbortError';\n  constructor(message = 'The operation was aborted') {\n    super(message);\n  }\n}\n\nfunction sleep(ms, signal) {\n  if (!signal) return new Promise((resolve) => setTimeout(resolve, ms));\n  if (signal.aborted) return Promise.reject(new AbortError());\n\n  return new Promise((resolve, reject) => {\n    const id = setTimeout(() => {\n      signal.removeEventListener('abort', onAbort);\n      resolve();\n    }, ms);\n\n    const onAbort = () => {\n      clearTimeout(id);\n      reject(new AbortError());\n    };\n\n    signal.addEventListener('abort', onAbort, { once: true });\n  });\n}\nexport async function addToCart(count, opts) {\n  await sleep(1000, opts?.signal);\n  return count + 1;\n}\n\nexport async function removeFromCart(count, opts) {\n  await sleep(1000, opts?.signal);\n  return Math.max(0, count - 1);\n}\n```\n\n```css\n.checkout {\n  display: flex;\n  flex-direction: column;\n  gap: 12px;\n  padding: 16px;\n  border: 1px solid #ccc;\n  border-radius: 8px;\n  font-family: system-ui;\n}\n\n.checkout h2 {\n  margin: 0 0 8px 0;\n}\n\n.row {\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n}\n\n.stepper {\n  display: flex;\n  align-items: center;\n  gap: 8px;\n}\n\n.qty {\n  min-width: 20px;\n  text-align: center;\n}\n\n.buttons {\n  display: flex;\n  flex-direction: column;\n  gap: 2px;\n}\n\n.buttons button {\n  padding: 0 8px;\n  font-size: 10px;\n  line-height: 1.2;\n  cursor: pointer;\n}\n\n.pending {\n  width: 20px;\n  text-align: center;\n}\n\n.total {\n  font-weight: bold;\n}\n\nhr {\n  width: 100%;\n  border: none;\n  border-top: 1px solid #ccc;\n  margin: 4px 0;\n}\n```\n\n</Sandpack>\n\nTry clicking increase or decrease multiple times, and notice that the total updates within 1 second no matter how many times you click. This works because it uses an `AbortController` to \"complete\" the previous Action so the next Action can proceed.\n\n<Pitfall>\n\nAborting an Action isn't always safe.\n\nFor example, if the Action performs a mutation (like writing to a database), aborting the network request doesn't undo the server-side change. This is why `useActionState` doesn't abort by default. It's only safe when you know the side effect can be safely ignored or retried.\n\n</Pitfall>\n\n---\n\n### Using with `<form>` Action props {/*use-with-a-form*/}\n\nYou can pass the `dispatchAction` function as the `action` prop to a `<form>`.\n\nWhen used this way, React automatically wraps the submission in a Transition, so you don't need to call `startTransition` yourself. The `reducerAction` receives the previous state and the submitted `FormData`:\n\n<Sandpack>\n\n```js src/App.js\nimport { useActionState, useOptimistic } from 'react';\nimport { addToCart, removeFromCart } from './api';\nimport Total from './Total';\n\nexport default function Checkout() {\n  const [count, dispatchAction, isPending] = useActionState(updateCartAction, 0);\n  const [optimisticCount, setOptimisticCount] = useOptimistic(count);\n\n  async function formAction(formData) {\n    const type = formData.get('type');\n    if (type === 'ADD') {\n      setOptimisticCount(c => c + 1);\n    } else {\n      setOptimisticCount(c => Math.max(0, c - 1));\n    }\n    return dispatchAction(formData);\n  }\n\n  return (\n    <form action={formAction} className=\"checkout\">\n      <h2>Checkout</h2>\n      <div className=\"row\">\n        <span>Eras Tour Tickets</span>\n        <span className=\"stepper\">\n          <span className=\"pending\">{isPending && '🌀'}</span>\n          <span className=\"qty\">{optimisticCount}</span>\n          <span className=\"buttons\">\n            <button type=\"submit\" name=\"type\" value=\"ADD\">▲</button>\n            <button type=\"submit\" name=\"type\" value=\"REMOVE\">▼</button>\n          </span>\n        </span>\n      </div>\n      <hr />\n      <Total quantity={count} isPending={isPending} />\n    </form>\n  );\n}\n\nexport default function App() {\n  return (\n    <>\n      <AddToCartForm itemID=\"1\" itemTitle=\"JavaScript: The Definitive Guide\" />\n      <AddToCartForm itemID=\"2\" itemTitle=\"JavaScript: The Good Parts\" />\n    </>\n  );\n}\n```\n\n```js src/actions.js\n\"use server\";\n\nexport async function addToCart(prevState, queryData) {\n  const itemID = queryData.get('itemID');\n  if (itemID === \"1\") {\n    return {\n      success: true,\n      cartSize: 12,\n    };\n  } else {\n    return {\n      success: false,\n      message: \"The item is sold out.\",\n    };\n  }\n  return prevCount;\n}\n```\n\n```css src/styles.css hidden\nform {\n  border: solid 1px black;\n  margin-bottom: 24px;\n  padding: 12px;\n}\n\nexport default function Total({quantity, isPending}) {\n  return (\n    <div className=\"row total\">\n      <span>Total</span>\n      {isPending ? '🌀 Updating...' : formatter.format(quantity * 9999)}\n    </div>\n  );\n}\n```\n\n```js src/api.js hidden\nexport async function addToCart(count) {\n  await new Promise(resolve => setTimeout(resolve, 1000));\n  return count + 1;\n}\n\nexport async function removeFromCart(count) {\n  await new Promise(resolve => setTimeout(resolve, 1000));\n  return Math.max(0, count - 1);\n}\n```\n\n```css\n.checkout {\n  display: flex;\n  flex-direction: column;\n  gap: 12px;\n  padding: 16px;\n  border: 1px solid #ccc;\n  border-radius: 8px;\n  font-family: system-ui;\n}\n\n.checkout h2 {\n  margin: 0 0 8px 0;\n}\n\n.row {\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n}\n\n.stepper {\n  display: flex;\n  align-items: center;\n  gap: 8px;\n}\n\n.qty {\n  min-width: 20px;\n  text-align: center;\n}\n\n.buttons {\n  display: flex;\n  flex-direction: column;\n  gap: 2px;\n}\n\n.buttons button {\n  padding: 0 8px;\n  font-size: 10px;\n  line-height: 1.2;\n  cursor: pointer;\n}\n\n.pending {\n  width: 20px;\n  text-align: center;\n}\n\n.total {\n  font-weight: bold;\n}\n\nhr {\n  width: 100%;\n  border: none;\n  border-top: 1px solid #ccc;\n  margin: 4px 0;\n}\n```\n\n</Sandpack>\n\nIn this example, when the user clicks the stepper arrows, the button submits the form and `useActionState` calls `updateCartAction` with the form data. The example uses `useOptimistic` to immediately show the new quantity while the server confirms the update.\n\n<RSC>\n\nWhen used with a [Server Function](/reference/rsc/server-functions), `useActionState` allows the server's response to be shown before hydration (when React attaches to server-rendered HTML) completes. You can also use the optional `permalink` parameter for progressive enhancement (allowing the form to work before JavaScript loads) on pages with dynamic content. This is typically handled by your framework for you.\n\n</RSC>\n\nSee the [`<form>`](/reference/react-dom/components/form#handle-form-submission-with-a-server-function) docs for more information on using Actions with forms. \n\n---\n\n### Handling errors {/*handling-errors*/}\n\nThere are two ways to handle errors with `useActionState`.\n\nFor known errors, such as \"quantity not available\" validation errors from your backend, you can return it as part of your `reducerAction` state and display it in the UI.\n\nFor unknown errors, such as `undefined is not a function`, you can throw an error. React will cancel all queued Actions and shows the nearest [Error Boundary](/reference/react/Component#catching-rendering-errors-with-an-error-boundary) by rethrowing the error from the `useActionState` hook.\n\n<Sandpack>\n\n```js src/App.js\nimport {useActionState, startTransition} from 'react';\nimport {ErrorBoundary} from 'react-error-boundary';\nimport {addToCart} from './api';\nimport Total from './Total';\n\nfunction Checkout() {\n  const [state, dispatchAction, isPending] = useActionState(\n    async (prevState, quantity) => {\n      const result = await addToCart(prevState.count, quantity);\n      if (result.error) {\n        // Return the error from the API as state\n        return {...prevState, error: `Could not add quanitiy ${quantity}: ${result.error}`};\n      }\n      \n      if (!isPending) {\n        // Clear the error state for the first dispatch.\n        return {count: result.count, error: null};    \n      }\n      \n      // Return the new count, and any errors that happened.\n      return {count: result.count, error: prevState.error};\n      \n      \n    },\n    {\n      count: 0,\n      error: null,\n    }\n  );\n\n  function handleAdd(quantity) {\n    startTransition(() => {\n      dispatchAction(quantity);\n    });\n  }\n\n  return (\n    <div className=\"checkout\">\n      <h2>Checkout</h2>\n      <div className=\"row\">\n        <span>Eras Tour Tickets</span>\n        <span>\n          {isPending && '🌀 '}Qty: {state.count}\n        </span>\n      </div>\n      <div className=\"buttons\">\n        <button onClick={() => handleAdd(1)}>Add 1</button>\n        <button onClick={() => handleAdd(10)}>Add 10</button>\n        <button onClick={() => handleAdd(NaN)}>Add NaN</button>\n      </div>\n      {state.error && <div className=\"error\">{state.error}</div>}\n      <hr />\n      <Total quantity={state.count} isPending={isPending} />\n    </div>\n  );\n}\n\n\n\nexport default function App() {\n  return (\n    <ErrorBoundary\n      fallbackRender={({resetErrorBoundary}) => (\n        <div className=\"checkout\">\n          <h2>Something went wrong</h2>\n          <p>The action could not be completed.</p>\n          <button onClick={resetErrorBoundary}>Try again</button>\n        </div>\n      )}>\n      <Checkout />\n    </ErrorBoundary>\n  );\n}\n```\n\n```js src/Total.js\nconst formatter = new Intl.NumberFormat('en-US', {\n  style: 'currency',\n  currency: 'USD',\n  minimumFractionDigits: 0,\n});\n\nexport default function Total({quantity, isPending}) {\n  return (\n    <div className=\"row total\">\n      <span>Total</span>\n      <span>\n        {isPending ? '🌀 Updating...' : formatter.format(quantity * 9999)}\n      </span>\n    </div>\n  );\n}\n```\n\n```js src/api.js hidden\nexport async function addToCart(count, quantity) {\n  await new Promise((resolve) => setTimeout(resolve, 1000));\n  if (quantity > 5) {\n    return {error: 'Quantity not available'};\n  } else if (isNaN(quantity)) {\n    throw new Error('Quantity must be a number');\n  }\n  return {count: count + quantity};\n}\n```\n\n```css\n.checkout {\n  display: flex;\n  flex-direction: column;\n  gap: 12px;\n  padding: 16px;\n  border: 1px solid #ccc;\n  border-radius: 8px;\n  font-family: system-ui;\n}\n\n.checkout h2 {\n  margin: 0 0 8px 0;\n}\n\n.row {\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n}\n\n.total {\n  font-weight: bold;\n}\n\nhr {\n  width: 100%;\n  border: none;\n  border-top: 1px solid #ccc;\n  margin: 4px 0;\n}\n\nbutton {\n  padding: 8px 16px;\n  cursor: pointer;\n}\n\n.buttons {\n  display: flex;\n  gap: 8px;\n}\n\n.error {\n  color: red;\n  font-size: 14px;\n}\n```\n\n```json package.json hidden\n{\n  \"dependencies\": {\n    \"react\": \"19.0.0\",\n    \"react-dom\": \"19.0.0\",\n    \"react-scripts\": \"^5.0.0\",\n    \"react-error-boundary\": \"4.0.3\"\n  },\n  \"main\": \"/index.js\"\n}\n```\n\n</Sandpack>\n\nIn this example, \"Add 10\" simulates an API that returns a validation error, which `updateCartAction` stores in state and displays inline. \"Add NaN\" results in an invalid count, so `updateCartAction` throws, which propagates through `useActionState` to the `ErrorBoundary` and shows a reset UI.\n\n\n---\n\n## 문제 해결 {/*troubleshooting*/}\n\n### 액션이 더 이상 제출된 폼 데이터를 읽을 수 없습니다 {/*my-action-can-no-longer-read-the-submitted-form-data*/}\n\n액션을 `useActionState`로 감싸면 *첫 번째 인수*로 \"이전(또는 현재) State\"가 추가됩니다. 따라서 일반적인 폼 액션과 달리, 제출된 폼 데이터는 *두 번째 인수*에서 확인해야 합니다.\n\n```js\nimport { useActionState, startTransition } from 'react';\n\nfunction MyComponent() {\n  const [state, dispatchAction, isPending] = useActionState(myAction, null);\n\n  function handleClick() {\n    // ✅ Correct: wrap in startTransition\n    startTransition(() => {\n      dispatchAction();\n    });\n  }\n\n  // ...\n}\n```\n\nWhen `dispatchAction` is passed to an Action prop, React automatically wraps it in a Transition.\n\n---\n\n### My Action cannot read form data {/*action-cannot-read-form-data*/}\n\nWhen you use `useActionState`, the `reducerAction` receives an extra argument as its first argument: the previous or initial state. The submitted form data is therefore its second argument instead of its first.\n\n```js {2,7}\n// Without useActionState\nfunction action(formData) {\n  const name = formData.get('name');\n}\n\n// With useActionState\nfunction action(prevState, formData) {\n  const name = formData.get('name');\n}\n```\n\n---\n\n### My actions are being skipped {/*actions-skipped*/}\n\nIf you call `dispatchAction` multiple times and some of them don't run, it may be because an earlier `dispatchAction` call threw an error.\n\nWhen a `reducerAction` throws, React skips all subsequently queued `dispatchAction` calls.\n\nTo handle this, catch errors within your `reducerAction` and return an error state instead of throwing:\n\n```js\nasync function myReducerAction(prevState, data) {\n  try {\n    const result = await submitData(data);\n    return { success: true, data: result };\n  } catch (error) {\n    // ✅ Return error state instead of throwing\n    return { success: false, error: error.message };\n  }\n}\n```\n\n---\n\n### My state doesn't reset {/*reset-state*/}\n\n`useActionState` doesn't provide a built-in reset function. To reset the state, you can design your `reducerAction` to handle a reset signal:\n\n```js\nconst initialState = { name: '', error: null };\n\nasync function formAction(prevState, payload) {\n  // Handle reset\n  if (payload === null) {\n    return initialState;\n  }\n  // Normal action logic\n  const result = await submitData(payload);\n  return result;\n}\n\nfunction MyComponent() {\n  const [state, dispatchAction, isPending] = useActionState(formAction, initialState);\n\n  function handleReset() {\n    startTransition(() => {\n      dispatchAction(null); // Pass null to trigger reset\n    });\n  }\n\n  // ...\n}\n```\n\nAlternatively, you can add a `key` prop to the component using `useActionState` to force it to remount with fresh state, or a `<form>` `action` prop, which resets automatically after submission.\n\n---\n\n### I'm getting an error: \"An async function with useActionState was called outside of a transition.\" {/*async-function-outside-transition*/}\n\nA common mistake is to forget to call `dispatchAction` from inside a Transition:\n\n<ConsoleBlockMulti>\n<ConsoleLogLine level=\"error\">\n\nAn async function with useActionState was called outside of a transition. This is likely not what you intended (for example, isPending will not update correctly). Either call the returned function inside startTransition, or pass it to an `action` or `formAction` prop.\n\n</ConsoleLogLine>\n</ConsoleBlockMulti>\n\n\nThis error happens because `dispatchAction` must run inside a Transition:\n\n```js\nfunction MyComponent() {\n  const [state, dispatchAction, isPending] = useActionState(myAsyncAction, null);\n\n  function handleClick() {\n    // ❌ Wrong: calling dispatchAction outside a Transition\n    dispatchAction();\n  }\n\n  // ...\n}\n```\n\nTo fix, either wrap the call in [`startTransition`](/reference/react/startTransition):\n\n```js\nimport { useActionState, startTransition } from 'react';\n\nfunction MyComponent() {\n  const [state, dispatchAction, isPending] = useActionState(myAsyncAction, null);\n\n  function handleClick() {\n    // ✅ Correct: wrap in startTransition\n    startTransition(() => {\n      dispatchAction();\n    });\n  }\n\n  // ...\n}\n```\n\nOr pass `dispatchAction` to an Action prop, is call in a Transition:\n\n```js\nfunction MyComponent() {\n  const [state, dispatchAction, isPending] = useActionState(myAsyncAction, null);\n\n  // ✅ Correct: action prop wraps in a Transition for you\n  return <Button action={dispatchAction}>...</Button>;\n}\n```\n\n---\n\n### I'm getting an error: \"Cannot update action state while rendering\" {/*cannot-update-during-render*/}\n\nYou cannot call `dispatchAction` during render:\n\n<ConsoleBlock level=\"error\">\n\nCannot update action state while rendering.\n\n</ConsoleBlock>\n\nThis causes an infinite loop because calling `dispatchAction` schedules a state update, which triggers a re-render, which calls `dispatchAction` again.\n\n```js\nfunction MyComponent() {\n  const [state, dispatchAction, isPending] = useActionState(myAction, null);\n\n  // ❌ Wrong: calling dispatchAction during render\n  dispatchAction();\n\n  // ...\n}\n```\n\nTo fix, only call `dispatchAction` in response to user events (like form submissions or button clicks).\n"
  },
  {
    "path": "src/content/reference/react/useCallback.md",
    "content": "---\ntitle: useCallback\n---\n\n<Intro>\n\n`useCallback`은 리렌더링 간에 함수 정의를 캐싱해 주는 React Hook입니다.\n\n```js\nconst cachedFn = useCallback(fn, dependencies)\n```\n\n</Intro>\n\n<Note>\n\n[React 컴파일러](/learn/react-compiler)는 값과 함수를 자동으로 메모이제이션하므로 `useCallback`을 수동으로 사용할 일이 줄어듭니다. 컴파일러를 사용해 메모이제이션을 자동으로 처리할 수 있습니다.\n\n</Note>\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `useCallback(fn, dependencies)` {/*usecallback*/}\n\n리렌더링 간에 함수 정의를 캐싱하려면 컴포넌트의 최상단에서 `useCallback`을 호출하세요.\n\n```js {4,9}\nimport { useCallback } from 'react';\n\nexport default function ProductPage({ productId, referrer, theme }) {\n  const handleSubmit = useCallback((orderDetails) => {\n    post('/product/' + productId + '/buy', {\n      referrer,\n      orderDetails,\n    });\n  }, [productId, referrer]);\n```\n\n[아래에서 더 많은 예시를 확인해보세요.](#usage)\n\n#### 매개변수 {/*parameters*/}\n\n* `fn`: 캐싱할 함숫값입니다. 이 함수는 어떤 인자나 반환값도 가질 수 있습니다. React는 첫 렌더링에서 이 함수를 반환합니다. (호출하는 것이 아닙니다!) 다음 렌더링에서 `dependencies` 값이 이전과 같다면 React는 같은 함수를 다시 반환합니다. 반대로 `dependencies` 값이 변경되었다면 이번 렌더링에서 전달한 함수를 반환하고 나중에 재사용할 수 있도록 이를 저장합니다. React는 함수를 호출하지 않습니다. 이 함수는 호출 여부와 호출 시점을 개발자가 결정할 수 있도록 반환됩니다.\n\n* `dependencies`: `fn` 내에서 참조되는 모든 반응형 값의 목록입니다. 반응형 값은 props와 state, 그리고 컴포넌트 안에서 직접 선언된 모든 변수와 함수를 포함합니다. 린터가 [React를 위한 설정](/learn/editor-setup#linting)으로 구성되어 있다면 모든 반응형 값이 의존성으로 올바르게 명시되어 있는지 검증합니다. 의존성 목록은 항목 수가 일정해야 하며 `[dep1, dep2, dep3]`처럼 인라인으로 작성해야 합니다. React는 [`Object.is`](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Object/is) 비교 알고리즘을 이용해 각 의존성을 이전 값과 비교합니다.\n\n#### 반환값 {/*returns*/}\n\n최초 렌더링에서는 `useCallback`은 전달한 `fn` 함수를 그대로 반환합니다.\n\n후속 렌더링에서는 이전 렌더링에서 이미 저장해 두었던 `fn` 함수를 반환하거나 (의존성이 변하지 않았을 때), 현재 렌더링 중에 전달한 `fn` 함수를 그대로 반환합니다.\n\n#### 주의 사항 {/*caveats*/}\n\n* `useCallback`은 Hook이므로, **컴포넌트의 최상위 레벨** 또는 커스텀 Hook에서만 호출할 수 있습니다. 반복문이나 조건문 내에서 호출할 수 없습니다. 이 작업이 필요하다면 새로운 컴포넌트로 분리해서 state를 새 컴포넌트로 옮기세요.\n* React는 **특별한 이유가 없는 한 캐시 된 함수를 삭제하지 않습니다.** 예를 들어 개발 환경에서는 컴포넌트 파일을 편집할 때 React가 캐시를 삭제합니다. 개발 환경과 프로덕션 환경 모두에서, 초기 마운트 중에 컴포넌트가 일시 중단되면 React는 캐시를 삭제합니다. 앞으로 React는 캐시 삭제를 활용하는 더 많은 기능을 추가할 수 있습니다. 예를 들어, React에 가상화된 목록에 대한 빌트인 지원이 추가한다면, 가상화된 테이블 뷰포트에서 스크롤 밖의 항목에 대해 캐시를 삭제하는것이 적절할 것 입니다. 이는 `useCallback`을 성능 최적화 방법으로 의존하는 경우에 개발자의 예상과 일치해야 합니다. 그렇지 않다면 [state 변수](/reference/react/useState#im-trying-to-set-state-to-a-function-but-it-gets-called-instead) 나 [ref](/reference/react/useRef#avoiding-recreating-the-ref-contents)가 더 적절할 수 있습니다.\n---\n\n## 용법 {/*usage*/}\n\n### 컴포넌트의 리렌더링 건너뛰기 {/*skipping-re-rendering-of-components*/}\n\n렌더링 성능을 최적화할 때 자식 컴포넌트에 넘기는 함수를 캐싱할 필요가 있습니다. 먼저 이 작업을 수행하는 방법에 대한 구문을 살펴본 다음 어떤 경우에 유용한지 알아보겠습니다.\n\n컴포넌트의 리렌더링 간에 함수를 캐싱하려면 함수 정의를 `useCallback` Hook으로 감싸세요.\n\n```js [[3, 4, \"handleSubmit\"], [2, 9, \"[productId, referrer]\"]]\nimport { useCallback } from 'react';\n\nfunction ProductPage({ productId, referrer, theme }) {\n  const handleSubmit = useCallback((orderDetails) => {\n    post('/product/' + productId + '/buy', {\n      referrer,\n      orderDetails,\n    });\n  }, [productId, referrer]);\n  // ...\n```\n\n`useCallback`에게 두 가지를 전달해야 합니다\n\n1. 리렌더링 간에 캐싱할 함수 정의\n2. 함수에서 사용되는 컴포넌트 내부의 모든 값을 포함하고 있는 <CodeStep step={2}>의존성 목록</CodeStep>\n\n최초 렌더링에서 `useCallback`으로부터 <CodeStep step={3}>반환되는 함수</CodeStep>는 호출시에 전달할 함수입니다.\n\n이어지는 렌더링에서 React는 <CodeStep step={2}>의존성</CodeStep>을 이전 렌더링에서 전달한 의존성과 비교합니다. 의존성 중 하나라도 변한 값이 없다면([`Object.is`](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Object/is)로 비교), `useCallback`은 전과 똑같은 함수를 반환합니다. 그렇지 않으면 `useCallback`은 *이번* 렌더링에서 전달한 함수를 반환합니다.\n\n다시 말하면, `useCallback`은 의존성이 변하기 전까지 리렌더링 간에 함수를 캐싱합니다.\n\n**이 기능이 언제 유용한지 예시를 통해 살펴보겠습니다.**\n\n`handleSubmit` 함수를 `ProductPage`에서 `ShippingForm` 컴포넌트로 전달한다고 가정해 봅시다.\n\n```js {5}\nfunction ProductPage({ productId, referrer, theme }) {\n  // ...\n  return (\n    <div className={theme}>\n      <ShippingForm onSubmit={handleSubmit} />\n    </div>\n  );\n```\n\n`theme` prop을 토글 하면 앱이 잠시 멈춘다는 것을 알게 되었는데, JSX에서 `<ShippingForm />`을 제거하면 앱이 빨라진 것처럼 느껴집니다. 이는 `<ShippingForm />` 컴포넌트의 최적화를 시도해 볼 가치가 있다는 것을 나타냅니다.\n\n**기본적으로, 컴포넌트가 리렌더링할 때 React는 해당 컴포넌트의 모든 자식을 재귀적으로 리렌더링합니다.** 이는 `ProductPage`가 다른 `theme` 값으로 리렌더링 할 때, `ShippingForm` 컴포넌트 **또한** 리렌더링 하는 이유입니다. 이것은 리렌더링에 많은 계산을 요구하지 않는 컴포넌트에서는 괜찮습니다. 하지만 리렌더링이 느린 것을 확인한 경우, `ShippingForm`을 [`memo`](/reference/react/memo)로 감싸면 마지막 렌더링과 동일한 props일 때 리렌더링을 건너뛰도록 할 수 있습니다.\n\n```js {3,5}\nimport { memo } from 'react';\n\nconst ShippingForm = memo(function ShippingForm({ onSubmit }) {\n  // ...\n});\n```\n\n**이렇게 변경한 `ShippingForm`은 모든 props가 마지막 렌더링과 *같다면* 리렌더링을 건너뜁니다.** 여기가 함수 캐싱이 중요해지는 순간입니다! `useCallback` 없이 `handleSubmit`을 정의했다고 가정해 봅시다.\n\n```js {2,3,8,12-13}\nfunction ProductPage({ productId, referrer, theme }) {\n  // theme이 바뀔때마다 다른 함수가 될 것입니다...\n  function handleSubmit(orderDetails) {\n    post('/product/' + productId + '/buy', {\n      referrer,\n      orderDetails,\n    });\n  }\n\n  return (\n    <div className={theme}>\n      {/* ... 그래서 ShippingForm의 props는 같은 값이 아니므로 매번 리렌더링 할 것입니다.*/}\n      <ShippingForm onSubmit={handleSubmit} />\n    </div>\n  );\n}\n```\n\n**자바스크립트에서 `function () {}` 나 `() => {}`은 항상 _다른_ 함수를 생성합니다.** 이는 `{}` 객체 리터럴이 항상 새로운 객체를 생성하는 방식과 유사합니다. 보통의 경우에는 문제가 되지 않지만, 여기서는 `ShippingForm` props는 절대 같아질 수 없고 [`memo`](/reference/react/memo) 최적화는 동작하지 않을 것이라는 걸 의미합니다. 여기서 `useCallback`이 유용하게 사용됩니다.\n\n```js {2,3,8,12-13}\nfunction ProductPage({ productId, referrer, theme }) {\n  // React에게 리렌더링 간에 함수를 캐싱하도록 요청합니다...\n  const handleSubmit = useCallback((orderDetails) => {\n    post('/product/' + productId + '/buy', {\n      referrer,\n      orderDetails,\n    });\n  }, [productId, referrer]); // ...이 의존성이 변경되지 않는 한...\n\n  return (\n    <div className={theme}>\n      {/* ...ShippingForm은 같은 props를 받게 되고 리렌더링을 건너뛸 수 있습니다.*/}\n      <ShippingForm onSubmit={handleSubmit} />\n    </div>\n  );\n}\n```\n\n**`handleSubmit`을 `useCallback`으로 감쌈으로써 리렌더링 간에 이것이 (의존성이 변경되기 전까지는) 같은 함수라는 것을 보장합니다.** 특별한 이유가 없다면 함수를 꼭 `useCallback`으로 감쌀 필요는 없습니다. 이 예시에서의 이유는 ['memo'](/reference/react/memo)로 감싼 컴포넌트에 전달하기 때문에 해당 함수가 리렌더링을 건너뛸 수 있기 때문입니다. `useCallback`이 필요한 다른 이유는 이 페이지의 뒷부분에서 설명하겠습니다.\n\n<Note>\n\n**`useCallback`은 성능 최적화를 위한 용도로만 사용해야 합니다.** 만약 코드가 `useCallback` 없이 작동하지 않는다면 먼저 근본적인 문제를 찾아 해결해야 합니다. 그다음에 `useCallback`을 다시 추가할 수 있습니다.\n\n</Note>\n\n<DeepDive>\n\n#### `useCallback`과 `useMemo`는 어떤 연관이 있나요? {/*how-is-usecallback-related-to-usememo*/}\n\n[`useMemo`](/reference/react/useMemo)가 `useCallback`과 함께 쓰이는 것을 자주 봤을 것입니다. 두 hook은 모두 자식 컴포넌트를 최적화할 때 유용합니다. 무언가를 전달할 때 [memoization](https://ko.wikipedia.org/wiki/%EB%A9%94%EB%AA%A8%EC%9D%B4%EC%A0%9C%EC%9D%B4%EC%85%98)(다른 말로는 캐싱)을 할 수 있도록 해줍니다.\n\n```js {6-8,10-15,19}\nimport { useMemo, useCallback } from 'react';\n\nfunction ProductPage({ productId, referrer }) {\n  const product = useData('/product/' + productId);\n\n  const requirements = useMemo(() => { // 함수를 호출하고 그 결과를 캐싱합니다.\n    return computeRequirements(product);\n  }, [product]);\n\n  const handleSubmit = useCallback((orderDetails) => { // 함수 자체를 캐싱합니다.\n    post('/product/' + productId + '/buy', {\n      referrer,\n      orderDetails,\n    });\n  }, [productId, referrer]);\n\n  return (\n    <div className={theme}>\n      <ShippingForm requirements={requirements} onSubmit={handleSubmit} />\n    </div>\n  );\n}\n```\n\n차이점은 *무엇을* 캐싱하는지 입니다.\n\n* **[`useMemo`](/reference/react/useMemo) 는 호출한 함수의 결과값을 캐싱합니다.** 이 예시에서는 `computeRequirements(product)` 함수 호출 결과를 캐싱해서 `product`가 변경되지 않는 한 이 결과값이 변경되지 않도록 합니다. 이는 불필요하게 `ShippingForm`을 리렌더링하지 않고 `requirements` 객체를 넘겨줄 수 있도록 해줍니다. 필요할 때 React는 렌더링 중에 넘겨주었던 함수를 호출하여 결과를 계산합니다.\n* **`useCallback`은 *함수 자체*를 캐싱합니다.** `useMemo`와 달리, 전달한 함수를 호출하지 않습니다. 그 대신, 전달한 함수를 캐싱해서 `productId`나 `referrer`이 변하지 않으면 `handleSubmit` 자체가 변하지 않도록 합니다. 이는 불필요하게 `ShippingForm`을 리렌더링하지 않고 `handleSubmit` 함수를 전달할 수 있도록 해줍니다. 함수의 코드는 사용자가 폼을 제출하기 전까지 실행되지 않을 것입니다.\n\n이미 [`useMemo`](/reference/react/useMemo)에 익숙하다면 `useCallback`을 다음과 같이 생각하는 것이 도움이 될 수 있습니다.\n\n```js\n// (React 내부의) 단순화된 구현 형태\nfunction useCallback(fn, dependencies) {\n  return useMemo(() => fn, dependencies);\n}\n```\n\n[`useMemo`와 `useCallback`의 차이점에 대해 더 알아보세요.](/reference/react/useMemo#memoizing-a-function)\n\n</DeepDive>\n\n<DeepDive>\n\n#### 항상 `useCallback`을 사용해야 할까요? {/*should-you-add-usecallback-everywhere*/}\n\n이 사이트처럼 대부분의 상호작용이 (페이지 전체나 전체 부문을 교체하는 것처럼) 굵직한 경우, 보통 memoization이 필요하지 않습니다. 반면에 앱이 (도형을 이동하는 것과 같이) 미세한 상호작용을 하는 그림 편집기 같은 경우, memoization이 매우 유용할 수 있습니다.\n\n`useCallback`으로 함수를 캐싱하는 것은 몇 가지 경우에만 가치 있습니다.\n\n- [`memo`](/reference/react/memo)로 감싸진 컴포넌트에 prop으로 넘깁니다. 이 값이 변하지 않으면 리렌더링을 건너뛰고 싶습니다. memoization은 의존성이 변했을 때만 컴포넌트가 리렌더링하도록 합니다.\n- 넘긴 함수가 나중에 어떤 Hook의 의존성으로 사용됩니다. 예를 들어, `useCallback`으로 감싸진 다른 함수가 이 함수에 의존하거나, [`useEffect`](/reference/react/useEffect)에서 이 함수에 의존합니다.\n\n다른 경우에서 `useCallback`으로 함수를 감싸는 것은 아무런 이익이 없습니다. 또한 이렇게 하는 것이 큰 불이익을 가져오지도 않으므로 일부 팀은 개별적인 경우를 따로 생각하지 않고, 가능한 한 많이 memoization하는 방식을 택합니다. 단점은 코드의 가독성이 떨어지는 것입니다. 또한, 모든 memoization이 효과적인 것은 아닙니다. \"항상 새로운\" 하나의 값이 있다면 전체 컴포넌트의 memoization을 깨기에 충분합니다.\n\n`useCallback`이 함수의 *생성*을 막지 않는다는 점을 주의하세요. 항상 함수를 생성하지만 (이건 괜찮습니다!), 그러나 React는 변경이 없는 경우에는 무시하고 캐시된 함수를 반환합니다\n\n**실제로 몇 가지 원칙을 따르면 많은 memoization을 불필요하게 만들 수 있습니다.**\n\n1. 컴포넌트가 다른 컴포넌트를 시각적으로 감싸고 있다면 [JSX를 자식으로 받게](/learn/passing-props-to-a-component#passing-jsx-as-children) 하세요. 감싸는 컴포넌트가 자신의 State를 업데이트하면, React는 자식들은 리렌더링할 필요가 없다는 것을 알게 됩니다.\n1. 가능한 한 로컬 State를 선호하고, [컴포넌트 간 상태 공유](/learn/sharing-state-between-components)를 필요 이상으로 하지 마세요. 폼이나 항목이 호버되었는지와 같은 일시적인 State를 트리의 상단이나 전역 State 라이브러리에 유지하지 마세요.\n1. [렌더링 로직을 순수하게 유지](/learn/keeping-components-pure)하세요. 컴포넌트를 리렌더링하는 것이 문제를 일으키거나 눈에 띄는 시각적인 형체를 생성한다면, 그것은 컴포넌트의 버그입니다! Memoization을 추가하는 대신 버그를 해결하세요.\n1. [State를 업데이트하는 불필요한 Effect](/learn/you-might-not-need-an-effect)를 피하세요. React 앱에서 대부분의 성능 문제는 Effect로부터 발생한 연속된 업데이트가 컴포넌트를 계속해서 렌더링하는 것이 원인입니다.\n1. [Effect에서 불필요한 의존성을 제거](/learn/removing-effect-dependencies)하세요. 예를 들어, Memoization 대신 객체나 함수를 Effect 안이나 컴포넌트 외부로 이동시키는 것이 더 간단한 경우가 많습니다.\n\n만약 특정 상호작용이 여전히 느리게 느껴진다면, [React Developer Tools profiler](https://legacy.reactjs.org/blog/2018/09/10/introducing-the-react-profiler.html)를 사용하여, 어떤 컴포넌트가 memoization을 가장 필요로 하는지 살펴보고, 필요한 곳에 memoization을 추가하세요. 이런 원칙들은 컴포넌트를 더 쉽게 디버깅하고 이해할 수 있도록 해주기 때문에 어떤 경우라도 따르는 것이 좋습니다. 장기적으로 이러한 문제를 해결하기 위해 우리는 [memoization을 자동화하는 기술](https://www.youtube.com/watch?v=lGEMwh32soc)을 연구하고 있습니다.\n\n</DeepDive>\n\n<Recipes titleText=\"useCallback과 함수를 직접 선언하는 것의 차이점\" titleId=\"examples-rerendering\">\n\n#### `useCallback`과 `memo`로 리렌더링 건너뛰기 {/*skipping-re-rendering-with-usecallback-and-memo*/}\n\n이 예시에서 `ShippingForm` 컴포넌트는 **인위적으로 느리게 만들었기 때문에** 렌더링하는 React 컴포넌트가 실제로 느릴 때 어떤 일이 일어나는 지 볼 수 있습니다. 카운터를 증가시키고 테마를 토글 해보세요.\n\n카운터를 증가시키면 느려진 `ShippingForm`이 리렌더링하기 때문에 느리다고 느껴집니다. 이는 예상된 동작입니다. 카운터가 변경되었으므로 사용자의 새로운 선택을 화면에 반영해야 하기 때문입니다.\n\n다음으로 테마를 토글 해보세요. **`useCallback`을 [`memo`](/reference/react/memo)와 함께 사용한 덕분에, 인위적인 지연에도 불구하고 빠릅니다!** `ShippingForm`은 `handleSubmit` 함수가 변하지 않았기 때문에 리렌더링을 건너뛰었습니다. `productId` 와 `referrer` (`useCallback`의 의존성) 모두 마지막 렌더링으로부터 변하지 않았기 때문에 `handleSubmit` 함수도 변하지 않았습니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { useState } from 'react';\nimport ProductPage from './ProductPage.js';\n\nexport default function App() {\n  const [isDark, setIsDark] = useState(false);\n  return (\n    <>\n      <label>\n        <input\n          type=\"checkbox\"\n          checked={isDark}\n          onChange={e => setIsDark(e.target.checked)}\n        />\n        Dark mode\n      </label>\n      <hr />\n      <ProductPage\n        referrerId=\"wizard_of_oz\"\n        productId={123}\n        theme={isDark ? 'dark' : 'light'}\n      />\n    </>\n  );\n}\n```\n\n```js src/ProductPage.js active\nimport { useCallback } from 'react';\nimport ShippingForm from './ShippingForm.js';\n\nexport default function ProductPage({ productId, referrer, theme }) {\n  const handleSubmit = useCallback((orderDetails) => {\n    post('/product/' + productId + '/buy', {\n      referrer,\n      orderDetails,\n    });\n  }, [productId, referrer]);\n\n  return (\n    <div className={theme}>\n      <ShippingForm onSubmit={handleSubmit} />\n    </div>\n  );\n}\n\nfunction post(url, data) {\n  // 요청을 보낸다고 생각하세요...\n  console.log('POST /' + url);\n  console.log(data);\n}\n```\n\n```js src/ShippingForm.js\nimport { memo, useState } from 'react';\n\nconst ShippingForm = memo(function ShippingForm({ onSubmit }) {\n  const [count, setCount] = useState(1);\n\n  console.log('[ARTIFICIALLY SLOW] Rendering <ShippingForm />');\n  let startTime = performance.now();\n  while (performance.now() - startTime < 500) {\n    // 매우 느린 코드를 재현하기 위해 500ms동안 아무것도 하지 않습니다\n  }\n\n  function handleSubmit(e) {\n    e.preventDefault();\n    const formData = new FormData(e.target);\n    const orderDetails = {\n      ...Object.fromEntries(formData),\n      count\n    };\n    onSubmit(orderDetails);\n  }\n\n  return (\n    <form onSubmit={handleSubmit}>\n      <p><b>Note: <code>ShippingForm</code> is artificially slowed down!</b></p>\n      <label>\n        Number of items:\n        <button type=\"button\" onClick={() => setCount(count - 1)}>–</button>\n        {count}\n        <button type=\"button\" onClick={() => setCount(count + 1)}>+</button>\n      </label>\n      <label>\n        Street:\n        <input name=\"street\" />\n      </label>\n      <label>\n        City:\n        <input name=\"city\" />\n      </label>\n      <label>\n        Postal code:\n        <input name=\"zipCode\" />\n      </label>\n      <button type=\"submit\">Submit</button>\n    </form>\n  );\n});\n\nexport default ShippingForm;\n```\n\n```css\nlabel {\n  display: block; margin-top: 10px;\n}\n\ninput {\n  margin-left: 5px;\n}\n\nbutton[type=\"button\"] {\n  margin: 5px;\n}\n\n.dark {\n  background-color: black;\n  color: white;\n}\n\n.light {\n  background-color: white;\n  color: black;\n}\n```\n\n</Sandpack>\n\n<Solution />\n\n#### 컴포넌트를 항상 리렌더링하기 {/*always-re-rendering-a-component*/}\n\n이 예시에서 `ShippingForm` 컴포넌트 또한 **인위적으로 느리게 만들었기 때문에** 렌더링하는 React 컴포넌트가 실제로 느릴 때 어떤 일이 일어나는 지 볼 수 있습니다. 카운터를 증가시키고 테마를 토글 해보세요.\n\n이전 예시와 다르게 지금은 테마를 토글 하는 것도 느립니다! **이 버전에서는 `useCallback`을 호출하고 있지 않기** 때문에 `handleSubmit`은 항상 새로운 함수이고, 느려진 `ShippingForm` 컴포넌트는 리렌더링을 건너뛸 수 없습니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { useState } from 'react';\nimport ProductPage from './ProductPage.js';\n\nexport default function App() {\n  const [isDark, setIsDark] = useState(false);\n  return (\n    <>\n      <label>\n        <input\n          type=\"checkbox\"\n          checked={isDark}\n          onChange={e => setIsDark(e.target.checked)}\n        />\n        Dark mode\n      </label>\n      <hr />\n      <ProductPage\n        referrerId=\"wizard_of_oz\"\n        productId={123}\n        theme={isDark ? 'dark' : 'light'}\n      />\n    </>\n  );\n}\n```\n\n```js src/ProductPage.js active\nimport ShippingForm from './ShippingForm.js';\n\nexport default function ProductPage({ productId, referrer, theme }) {\n  function handleSubmit(orderDetails) {\n    post('/product/' + productId + '/buy', {\n      referrer,\n      orderDetails,\n    });\n  }\n\n  return (\n    <div className={theme}>\n      <ShippingForm onSubmit={handleSubmit} />\n    </div>\n  );\n}\n\nfunction post(url, data) {\n  // 요청을 보낸다고 생각하세요...\n  console.log('POST /' + url);\n  console.log(data);\n}\n```\n\n```js src/ShippingForm.js\nimport { memo, useState } from 'react';\n\nconst ShippingForm = memo(function ShippingForm({ onSubmit }) {\n  const [count, setCount] = useState(1);\n\n  console.log('[ARTIFICIALLY SLOW] Rendering <ShippingForm />');\n  let startTime = performance.now();\n  while (performance.now() - startTime < 500) {\n    // 매우 느린 코드를 재현하기 위해 500ms동안 아무것도 하지 않습니다\n  }\n\n  function handleSubmit(e) {\n    e.preventDefault();\n    const formData = new FormData(e.target);\n    const orderDetails = {\n      ...Object.fromEntries(formData),\n      count\n    };\n    onSubmit(orderDetails);\n  }\n\n  return (\n    <form onSubmit={handleSubmit}>\n      <p><b>Note: <code>ShippingForm</code> is artificially slowed down!</b></p>\n      <label>\n        Number of items:\n        <button type=\"button\" onClick={() => setCount(count - 1)}>–</button>\n        {count}\n        <button type=\"button\" onClick={() => setCount(count + 1)}>+</button>\n      </label>\n      <label>\n        Street:\n        <input name=\"street\" />\n      </label>\n      <label>\n        City:\n        <input name=\"city\" />\n      </label>\n      <label>\n        Postal code:\n        <input name=\"zipCode\" />\n      </label>\n      <button type=\"submit\">Submit</button>\n    </form>\n  );\n});\n\nexport default ShippingForm;\n```\n\n```css\nlabel {\n  display: block; margin-top: 10px;\n}\n\ninput {\n  margin-left: 5px;\n}\n\nbutton[type=\"button\"] {\n  margin: 5px;\n}\n\n.dark {\n  background-color: black;\n  color: white;\n}\n\n.light {\n  background-color: white;\n  color: black;\n}\n```\n\n</Sandpack>\n\n\n하지만 여기 같지만 **인위적인 지연이 제거된** 코드가 있습니다. `useCallback`이 없을 때 차이가 크게 느껴지시나요?\n\n<Sandpack>\n\n```js src/App.js\nimport { useState } from 'react';\nimport ProductPage from './ProductPage.js';\n\nexport default function App() {\n  const [isDark, setIsDark] = useState(false);\n  return (\n    <>\n      <label>\n        <input\n          type=\"checkbox\"\n          checked={isDark}\n          onChange={e => setIsDark(e.target.checked)}\n        />\n        Dark mode\n      </label>\n      <hr />\n      <ProductPage\n        referrerId=\"wizard_of_oz\"\n        productId={123}\n        theme={isDark ? 'dark' : 'light'}\n      />\n    </>\n  );\n}\n```\n\n```js src/ProductPage.js active\nimport ShippingForm from './ShippingForm.js';\n\nexport default function ProductPage({ productId, referrer, theme }) {\n  function handleSubmit(orderDetails) {\n    post('/product/' + productId + '/buy', {\n      referrer,\n      orderDetails,\n    });\n  }\n\n  return (\n    <div className={theme}>\n      <ShippingForm onSubmit={handleSubmit} />\n    </div>\n  );\n}\n\nfunction post(url, data) {\n  // 요청을 보낸다고 생각하세요...\n  console.log('POST /' + url);\n  console.log(data);\n}\n```\n\n```js src/ShippingForm.js\nimport { memo, useState } from 'react';\n\nconst ShippingForm = memo(function ShippingForm({ onSubmit }) {\n  const [count, setCount] = useState(1);\n\n  console.log('Rendering <ShippingForm />');\n\n  function handleSubmit(e) {\n    e.preventDefault();\n    const formData = new FormData(e.target);\n    const orderDetails = {\n      ...Object.fromEntries(formData),\n      count\n    };\n    onSubmit(orderDetails);\n  }\n\n  return (\n    <form onSubmit={handleSubmit}>\n      <label>\n        Number of items:\n        <button type=\"button\" onClick={() => setCount(count - 1)}>–</button>\n        {count}\n        <button type=\"button\" onClick={() => setCount(count + 1)}>+</button>\n      </label>\n      <label>\n        Street:\n        <input name=\"street\" />\n      </label>\n      <label>\n        City:\n        <input name=\"city\" />\n      </label>\n      <label>\n        Postal code:\n        <input name=\"zipCode\" />\n      </label>\n      <button type=\"submit\">Submit</button>\n    </form>\n  );\n});\n\nexport default ShippingForm;\n```\n\n```css\nlabel {\n  display: block; margin-top: 10px;\n}\n\ninput {\n  margin-left: 5px;\n}\n\nbutton[type=\"button\"] {\n  margin: 5px;\n}\n\n.dark {\n  background-color: black;\n  color: white;\n}\n\n.light {\n  background-color: white;\n  color: black;\n}\n```\n\n</Sandpack>\n\n\n많은 경우에 memoization이 없어도 코드는 잘 동작합니다. 상호작용이 충분히 빠르다면 memoization을 사용하지 않아도 됩니다.\n\n프로덕션 모드로 React를 실행시키고, [React Developer Tools](/learn/react-developer-tools)를 비활성화하고, 앱 사용자와 유사한 기기를 사용해서 앱을 실제로 느리게 만드는 원인을 실감해야 한다는 것을 명심하세요.\n\n<Solution />\n\n</Recipes>\n\n---\n\n### Memoized 콜백에서 상태 업데이트하기 {/*updating-state-from-a-memoized-callback*/}\n\n때때로 memoized 콜백에서 이전 상태를 기반으로 상태를 업데이트해야 할 때가 있습니다.\n\n`handleAddTodo` 함수는 `todos`로부터 다음 할 일을 계산하기 때문에 이를 의존성으로 명시했습니다.\n\n```js {6,7}\nfunction TodoList() {\n  const [todos, setTodos] = useState([]);\n\n  const handleAddTodo = useCallback((text) => {\n    const newTodo = { id: nextId++, text };\n    setTodos([...todos, newTodo]);\n  }, [todos]);\n  // ...\n```\n\n보통은 memoized 함수가 가능한 한 적은 의존성을 갖는 것이 좋습니다. 다음 상태를 계산하기 위해 어떤 상태를 읽는 경우, [업데이트 함수](/reference/react/useState#updating-state-based-on-the-previous-state)를 대신 넘겨줌으로써 의존성을 제거할 수 있습니다.\n\n```js {6,7}\nfunction TodoList() {\n  const [todos, setTodos] = useState([]);\n\n  const handleAddTodo = useCallback((text) => {\n    const newTodo = { id: nextId++, text };\n    setTodos(todos => [...todos, newTodo]);\n  }, []); // ✅ todos 의존성은 필요하지 않습니다.\n  // ...\n```\n\n여기서 `todos`를 의존성으로 만들고 안에서 값을 읽는 대신, React에 *어떻게* 상태를 업데이트할지에 대한 지침을 넘겨줍니다. [업데이트 함수에 대해 더 알아보세요.](/reference/react/useState#updating-state-based-on-the-previous-state)\n\n---\n\n### Effect가 너무 자주 실행되는 것을 방지하기 {/*preventing-an-effect-from-firing-too-often*/}\n\n가끔 [Effect 안에서 함수를 호출해야 할 수도 있습니다.](/learn/synchronizing-with-effects)\n\n```js {4-9,12}\nfunction ChatRoom({ roomId }) {\n  const [message, setMessage] = useState('');\n\n  function createOptions() {\n    return {\n      serverUrl: 'https://localhost:1234',\n      roomId: roomId\n    };\n  }\n\n  useEffect(() => {\n    const options = createOptions();\n    const connection = createConnection(options);\n    connection.connect();\n    // ...\n```\n\n이것은 문제를 발생시킵니다. [모든 반응형 값은 Effect의 의존성으로 선언되어야 합니다.](/learn/lifecycle-of-reactive-effects#react-verifies-that-you-specified-every-reactive-value-as-a-dependency) 하지만 `createOptions`를 의존성으로 선언하면 Effect가 채팅방과 계속 재연결되는 문제가 발생합니다.\n\n\n```js {6}\n  useEffect(() => {\n    const options = createOptions();\n    const connection = createConnection(options);\n    connection.connect();\n    return () => connection.disconnect();\n  }, [createOptions]); // 🔴 문제점: 이 의존성은 매 렌더링마다 변경됩니다.\n  // ...\n```\n\n이를 해결하기 위해, Effect에서 호출하려는 함수를 `useCallback`으로 감쌀 수 있습니다.\n\n```js {4-9,16}\nfunction ChatRoom({ roomId }) {\n  const [message, setMessage] = useState('');\n\n  const createOptions = useCallback(() => {\n    return {\n      serverUrl: 'https://localhost:1234',\n      roomId: roomId\n    };\n  }, [roomId]); // ✅ roomId가 변경될 때만 변경됩니다.\n\n  useEffect(() => {\n    const options = createOptions();\n    const connection = createConnection(options);\n    connection.connect();\n    return () => connection.disconnect();\n  }, [createOptions]); // ✅ createOptions가 변경될 때만 변경됩니다.\n  // ...\n```\n\n이는 리렌더링 간에 `roomId`가 같다면 `createOptions` 함수는 같다는 것을 보장합니다. **하지만, 함수 의존성을 제거하는 것이 더 좋습니다.** 함수를 Effect *안으로* 이동시키세요.\n\n```js {5-10,16}\nfunction ChatRoom({ roomId }) {\n  const [message, setMessage] = useState('');\n\n  useEffect(() => {\n    function createOptions() { // ✅ useCallback이나 함수 의존성이 필요하지 않습니다.\n      return {\n        serverUrl: 'https://localhost:1234',\n        roomId: roomId\n      };\n    }\n\n    const options = createOptions();\n    const connection = createConnection(options);\n    connection.connect();\n    return () => connection.disconnect();\n  }, [roomId]); // ✅ roomId가 변경될 때만 변경됩니다.\n  // ...\n```\n\n이제 코드는 더 간단해졌고 `useCallback`은 필요하지 않습니다. [Effect의 의존성 제거에 대해 더 알아보세요.](/learn/removing-effect-dependencies#move-dynamic-objects-and-functions-inside-your-effect)\n\n---\n\n### 커스텀 Hook 최적화하기 {/*optimizing-a-custom-hook*/}\n\n[커스텀 Hook](/learn/reusing-logic-with-custom-hooks)을 작성하는 경우, 반환하는 모든 함수를 `useCallback`으로 감싸는 것이 좋습니다.\n\n```js {4-6,8-10}\nfunction useRouter() {\n  const { dispatch } = useContext(RouterStateContext);\n\n  const navigate = useCallback((url) => {\n    dispatch({ type: 'navigate', url });\n  }, [dispatch]);\n\n  const goBack = useCallback(() => {\n    dispatch({ type: 'back' });\n  }, [dispatch]);\n\n  return {\n    navigate,\n    goBack,\n  };\n}\n```\n\n이렇게 하면 Hook을 사용하는 컴포넌트가 필요할 때 가지고 있는 코드를 최적화할 수 있습니다.\n\n---\n\n## 문제 해결 {/*troubleshooting*/}\n\n### 컴포넌트가 렌더링 될 때마다 `useCallback`이 다른 함수를 반환합니다. {/*every-time-my-component-renders-usecallback-returns-a-different-function*/}\n\n두 번째 인수로 의존성 배열을 지정했는지 확인하세요!\n\n의존성 배열을 까먹으면 `useCallback`은 매번 새로운 함수를 반환합니다.\n\n```js {7}\nfunction ProductPage({ productId, referrer }) {\n  const handleSubmit = useCallback((orderDetails) => {\n    post('/product/' + productId + '/buy', {\n      referrer,\n      orderDetails,\n    });\n  }); // 🔴 매번 새로운 함수를 반환합니다: 의존성 배열 없음\n  // ...\n```\n\n다음은 두 번째 인수로 의존성 배열을 넘겨주도록 수정한 코드입니다.\n\n```js {7}\nfunction ProductPage({ productId, referrer }) {\n  const handleSubmit = useCallback((orderDetails) => {\n    post('/product/' + productId + '/buy', {\n      referrer,\n      orderDetails,\n    });\n  }, [productId, referrer]); // ✅ 불필요하게 새로운 함수를 반환하지 않습니다.\n  // ...\n```\n\n이것이 도움이 되지 않는다면 의존성 중 적어도 하나가 이전 렌더링과 다른 것이 문제입니다. 의존성을 콘솔에 직접 기록하여 이 문제를 디버깅할 수 있습니다.\n\n```js {5}\n  const handleSubmit = useCallback((orderDetails) => {\n    // ..\n  }, [productId, referrer]);\n\n  console.log([productId, referrer]);\n```\n\n그런 다음 콘솔에서 서로 다른 렌더링의 배열을 마우스 오른쪽 클릭 후 \"전역 변수로 저장\"을 선택할 수 있습니다. 첫 번째 것이 `temp1`, 두 번째 것이 `temp2`로 저장됐다면, 브라우저 콘솔을 통해 각 의존성이 두 배열에서 같은지 확인할 수 있습니다.\n\n```js\nObject.is(temp1[0], temp2[0]); // 첫 번째 의존성이 배열 간에 동일한가요?\nObject.is(temp1[1], temp2[1]); // 두 번째 의존성이 배열 간에 동일한가요?\nObject.is(temp1[2], temp2[2]); // ... 나머지 모든 의존성도 확인합니다  ...\n```\n\n어떤 의존성이 memoization을 깨고 있는지 찾았다면 이를 제거하거나 [memoization](/reference/react/useMemo#memoizing-a-dependency-of-another-hook)하는 방법을 찾으세요.\n\n---\n\n### 반복문에서 각 항목마다 `useCallback`을 호출하고 싶지만, 이는 허용되지 않습니다. {/*i-need-to-call-usememo-for-each-list-item-in-a-loop-but-its-not-allowed*/}\n\n`Chart` 컴포넌트가 [`memo`](/reference/react/memo)로 감싸져 있다고 생각해 봅시다. `ReportList` 컴포넌트가 렌더링 될 때마다, 모든 `Chart` 항목이 리렌더링 하는 것을 막고 싶습니다. 하지만 반복문에서 `useCallback`을 호출할 수 없습니다.\n\n```js {5-14}\nfunction ReportList({ items }) {\n  return (\n    <article>\n      {items.map(item => {\n        // 🔴 이렇게 반복문 안에서 useCallback을 호출할 수 없습니다.\n        const handleClick = useCallback(() => {\n          sendReport(item)\n        }, [item]);\n\n        return (\n          <figure key={item.id}>\n            <Chart onClick={handleClick} />\n          </figure>\n        );\n      })}\n    </article>\n  );\n}\n```\n\n대신 개별 항목을 컴포넌트로 분리하고, 거기에 `useCallback`을 넣으세요.\n\n```js {5,12-21}\nfunction ReportList({ items }) {\n  return (\n    <article>\n      {items.map(item =>\n        <Report key={item.id} item={item} />\n      )}\n    </article>\n  );\n}\n\nfunction Report({ item }) {\n  // ✅ useCallback을 최상위 레벨에서 호출하세요\n  const handleClick = useCallback(() => {\n    sendReport(item)\n  }, [item]);\n\n  return (\n    <figure>\n      <Chart onClick={handleClick} />\n    </figure>\n  );\n}\n```\n\n대안으로 마지막 스니펫에서 `useCallback`을 제거하고 대신 `Report` 자체를 [`memo`](/reference/react/memo)로 감싸도 됩니다. `item` prop이 변경되지 않으면 `Report`는 리렌더링하지 않기 때문에 `Chart`도 리렌더링을 건너뜁니다.\n\n```js {5,6-8,15}\nfunction ReportList({ items }) {\n  // ...\n}\n\nconst Report = memo(function Report({ item }) {\n  function handleClick() {\n    sendReport(item);\n  }\n\n  return (\n    <figure>\n      <Chart onClick={handleClick} />\n    </figure>\n  );\n});\n```\n"
  },
  {
    "path": "src/content/reference/react/useContext.md",
    "content": "---\ntitle: useContext\n---\n\n<Intro>\n\n`useContext`는 컴포넌트에서 [Context](/learn/passing-data-deeply-with-context)를 읽고 구독할 수 있는 React Hook입니다.\n\n```js\nconst value = useContext(SomeContext)\n```\n\n</Intro>\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `useContext(SomeContext)` {/*usecontext*/}\n\n`useContext`를 컴포넌트의 최상위 수준에서 호출하여 [Context](/learn/passing-data-deeply-with-context)를 읽고 구독합니다.\n\n```js\nimport { useContext } from 'react';\n\nfunction MyComponent() {\n  const theme = useContext(ThemeContext);\n  // ...\n```\n\n[아래 예시를 참고하세요.](#usage)\n\n#### 매개변수 {/*parameters*/}\n\n* `SomeContext`: [`createContext`](/reference/react/createContext)로 생성한 Context입니다. Context 자체는 정보를 담고 있지 않으며, 컴포넌트에서 제공하거나 읽을 수 있는 정보의 종류를 나타냅니다.\n\n#### 반환값 {/*returns*/}\n\n`useContext`는 호출하는 컴포넌트에 대한 Context 값을 반환합니다. 이 값은 트리에서 호출하는 컴포넌트 상위의 가장 가까운 `SomeContext`에 전달된 값으로 결정됩니다. Provider가 없으면 반환된 값은 해당 Context에 대해 [`createContext`](/reference/react/createContext)에 전달한 `defaultValue`가 됩니다. 반환된 값은 항상 최신 상태입니다. Context가 변경되면 React는 자동으로 해당 Context를 읽는 컴포넌트를 다시 렌더링합니다.\n\n#### 주의 사항 {/*caveats*/}\n\n* 컴포넌트 내의 `useContext()` 호출은 **동일한** 컴포넌트에서 반환된 Provider에 영향을 받지 않습니다. 해당하는 `<Context>`는 `useContext()` 호출을 하는 컴포넌트 ***상위에* 배치되어야 합니다.**\n* React는 다른 `value`을 받는 Provider로부터 시작해서 특정 Context를 사용하는 모든 자식들을 **자동으로 리렌더링**합니다. 이전 값과 다음 값은 [`Object.is`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is)를 통해 비교합니다. [`memo`](/reference/react/memo)로 리렌더링을 건너뛰어도 자식들이 새로운 Context 값을 받는 것을 막지는 못합니다.\n* 빌드 시스템이 결과물에 중복 모듈을 생성하는 경우(심볼릭 링크에서 발생할 수 있음) Context가 손상될 수 있습니다. Context를 통해 무언가를 전달하는 것은 `===` 비교에 의해 결정되는 것처럼 Context를 제공하는 데 사용하는 `SomeContext`와 Context를 읽는 데 사용하는 `SomeContext`가 ***정확하게* 동일한 객체**인 경우에만 작동합니다.\n\n---\n\n## 사용법 {/*usage*/}\n\n\n### 트리의 깊은 곳에 데이터 전달하기 {/*passing-data-deeply-into-the-tree*/}\n\n컴포넌트의 최상위 수준에서 `useContext`를 호출하여 [Context](/learn/passing-data-deeply-with-context)를 읽고 구독합니다.\n\n```js [[2, 4, \"theme\"], [1, 4, \"ThemeContext\"]]\nimport { useContext } from 'react';\n\nfunction Button() {\n  const theme = useContext(ThemeContext);\n  // ...\n```\n\n`useContext`는 전달한 <CodeStep step={1}>Context</CodeStep>에 대한 <CodeStep step={2}>Context Value</CodeStep>를 반환합니다. Context 값을 결정하기 위해 React는 컴포넌트 트리를 탐색하고 특정 Context에 대해 **상위에서 가장 가까운 Context Provider**를 찾습니다.\n\nContext를 `Button`에 전달하려면 해당 버튼 또는 상위 컴포넌트 중 하나를 해당 Context Provider로 감쌉니다.\n\n```js [[1, 3, \"ThemeContext\"], [2, 3, \"\\\\\"dark\\\\\"\"], [1, 5, \"ThemeContext\"]]\nfunction MyPage() {\n  return (\n    <ThemeContext value=\"dark\">\n      <Form />\n    </ThemeContext>\n  );\n}\n\nfunction Form() {\n  // ... 내부에서 버튼을 렌더링합니다. ...\n}\n```\n\nProvider와 `Button`사이에 얼마나 많은 컴포넌트 레이어가 있는지는 중요하지 않습니다. `Form` 내부의 `Button`이 *어디에서나* `useContext(ThemeContext)`를 호출하면,`\"dark\"`를 값으로 받습니다.\n\n<Pitfall>\n\n`useContext()`는 항상 호출하는 컴포넌트 *상위*에서 가장 가까운 Provider를 찾습니다. 위쪽 방향으로 찾고 `useContext()`를 호출하는 컴포넌트 안의 Provider는 **고려하지 않습니다.**\n\n</Pitfall>\n\n<Sandpack>\n\n```js\nimport { createContext, useContext } from 'react';\n\nconst ThemeContext = createContext(null);\n\nexport default function MyApp() {\n  return (\n    <ThemeContext value=\"dark\">\n      <Form />\n    </ThemeContext>\n  )\n}\n\nfunction Form() {\n  return (\n    <Panel title=\"Welcome\">\n      <Button>Sign up</Button>\n      <Button>Log in</Button>\n    </Panel>\n  );\n}\n\nfunction Panel({ title, children }) {\n  const theme = useContext(ThemeContext);\n  const className = 'panel-' + theme;\n  return (\n    <section className={className}>\n      <h1>{title}</h1>\n      {children}\n    </section>\n  )\n}\n\nfunction Button({ children }) {\n  const theme = useContext(ThemeContext);\n  const className = 'button-' + theme;\n  return (\n    <button className={className}>\n      {children}\n    </button>\n  );\n}\n```\n\n```css\n.panel-light,\n.panel-dark {\n  border: 1px solid black;\n  border-radius: 4px;\n  padding: 20px;\n}\n.panel-light {\n  color: #222;\n  background: #fff;\n}\n\n.panel-dark {\n  color: #fff;\n  background: rgb(23, 32, 42);\n}\n\n.button-light,\n.button-dark {\n  border: 1px solid #777;\n  padding: 5px;\n  margin-right: 10px;\n  margin-top: 10px;\n}\n\n.button-dark {\n  background: #222;\n  color: #fff;\n}\n\n.button-light {\n  background: #fff;\n  color: #222;\n}\n```\n\n</Sandpack>\n\n---\n\n### Context를 통해 전달된 데이터 업데이트하기 {/*updating-data-passed-via-context*/}\n\n때떄로 Context가 시간이 지남에 따라 변경되기를 원할 것입니다. Context를 업데이트 하려면 [State](/reference/react/useState)와 결합하세요. 부모 컴포넌트에서 State변수를 선언하고 현재 State를 <CodeStep step={2}>Context Value</CodeStep>로 Provider에 전달합니다.\n\n```js {2} [[1, 4, \"ThemeContext\"], [2, 4, \"theme\"], [1, 11, \"ThemeContext\"]]\nfunction MyPage() {\n  const [theme, setTheme] = useState('dark');\n  return (\n    <ThemeContext value={theme}>\n      <Form />\n      <Button onClick={() => {\n        setTheme('light');\n      }}>\n        Switch to light theme\n      </Button>\n    </ThemeContext>\n  );\n}\n```\n\n이제 Provider 내부의 모든 `Button`은 현재 `theme` 값을 받게 됩니다. Provider에 전달된 `theme` 값을 업데이트 하기 위해 `setTheme`을 호출하면, 모든 `Button` 컴포넌트가 새로운 `'light'` 값으로 다시 렌더링됩니다.\n\n<Recipes titleText=\"Context 업데이트 예시\" titleId=\"examples-basic\">\n\n#### Context를 통해 값 업데이트 {/*updating-a-value-via-context*/}\n\n이 예시에서 `MyApp` 컴포넌트는 State 변수를 가지고 있고, 이 State 변수는 `ThemeContext` Provider로 전달됩니다. \"Use dark mode\" 체크박스를 체크하면 State가 업데이트 됩니다. 제공된 값을 변경하면 해당 Context를 사용하는 모든 컴포넌트가 다시 렌더링됩니다.\n\n<Sandpack>\n\n```js\nimport { createContext, useContext, useState } from 'react';\n\nconst ThemeContext = createContext(null);\n\nexport default function MyApp() {\n  const [theme, setTheme] = useState('light');\n  return (\n    <ThemeContext value={theme}>\n      <Form />\n      <label>\n        <input\n          type=\"checkbox\"\n          checked={theme === 'dark'}\n          onChange={(e) => {\n            setTheme(e.target.checked ? 'dark' : 'light')\n          }}\n        />\n        Use dark mode\n      </label>\n    </ThemeContext>\n  )\n}\n\nfunction Form({ children }) {\n  return (\n    <Panel title=\"Welcome\">\n      <Button>Sign up</Button>\n      <Button>Log in</Button>\n    </Panel>\n  );\n}\n\nfunction Panel({ title, children }) {\n  const theme = useContext(ThemeContext);\n  const className = 'panel-' + theme;\n  return (\n    <section className={className}>\n      <h1>{title}</h1>\n      {children}\n    </section>\n  )\n}\n\nfunction Button({ children }) {\n  const theme = useContext(ThemeContext);\n  const className = 'button-' + theme;\n  return (\n    <button className={className}>\n      {children}\n    </button>\n  );\n}\n```\n\n```css\n.panel-light,\n.panel-dark {\n  border: 1px solid black;\n  border-radius: 4px;\n  padding: 20px;\n  margin-bottom: 10px;\n}\n.panel-light {\n  color: #222;\n  background: #fff;\n}\n\n.panel-dark {\n  color: #fff;\n  background: rgb(23, 32, 42);\n}\n\n.button-light,\n.button-dark {\n  border: 1px solid #777;\n  padding: 5px;\n  margin-right: 10px;\n  margin-top: 10px;\n}\n\n.button-dark {\n  background: #222;\n  color: #fff;\n}\n\n.button-light {\n  background: #fff;\n  color: #222;\n}\n```\n\n</Sandpack>\n\n`value=\"dark\"`는 `\"dark\"` 문자열을 전달하지만, `value={theme}`는 [JSX 중괄호](/learn/javascript-in-jsx-with-curly-braces)를 사용하여 자바스크립트 `theme` 변수 값을 전달합니다. 중괄호를 사용하면 문자열이 아닌 Context 값도 전달할 수 있습니다.\n\n<Solution />\n\n#### Context를 통해 객체 업데이트 {/*updating-an-object-via-context*/}\n\n이 예시에서는 객체를 가지고 있는 `currentUser` State 변수가 있습니다. `{ currentUser, setCurrentUser }`를 하나의 객체로 결합하여 `value={}` 내부의 Context를 통해 전달합니다. 이렇게 하면 `LoginButton`과 같은 하위의 모든 컴포넌트가 `currentUser`와 `setCurrentUser`를 모두 읽은 다음 필요할 때 `setCurrentUser`를 호출할 수 있습니다.\n\n<Sandpack>\n\n```js\nimport { createContext, useContext, useState } from 'react';\n\nconst CurrentUserContext = createContext(null);\n\nexport default function MyApp() {\n  const [currentUser, setCurrentUser] = useState(null);\n  return (\n    <CurrentUserContext\n      value={{\n        currentUser,\n        setCurrentUser\n      }}\n    >\n      <Form />\n    </CurrentUserContext>\n  );\n}\n\nfunction Form({ children }) {\n  return (\n    <Panel title=\"Welcome\">\n      <LoginButton />\n    </Panel>\n  );\n}\n\nfunction LoginButton() {\n  const {\n    currentUser,\n    setCurrentUser\n  } = useContext(CurrentUserContext);\n\n  if (currentUser !== null) {\n    return <p>You logged in as {currentUser.name}.</p>;\n  }\n\n  return (\n    <Button onClick={() => {\n      setCurrentUser({ name: 'Advika' })\n    }}>Log in as Advika</Button>\n  );\n}\n\nfunction Panel({ title, children }) {\n  return (\n    <section className=\"panel\">\n      <h1>{title}</h1>\n      {children}\n    </section>\n  )\n}\n\nfunction Button({ children, onClick }) {\n  return (\n    <button className=\"button\" onClick={onClick}>\n      {children}\n    </button>\n  );\n}\n```\n\n```css\nlabel {\n  display: block;\n}\n\n.panel {\n  border: 1px solid black;\n  border-radius: 4px;\n  padding: 20px;\n  margin-bottom: 10px;\n}\n\n.button {\n  border: 1px solid #777;\n  padding: 5px;\n  margin-right: 10px;\n  margin-top: 10px;\n}\n```\n\n</Sandpack>\n\n<Solution />\n\n#### 다양한 Context {/*multiple-contexts*/}\n\n이 예시에서는 두 개의 독립적인 Context가 있습니다. `ThemeContext`는 현재 테마를 문자열로 제공하고 `CurrentUserContext`는 현재 사용자를 나타내는 객체를 보유합니다.\n\n<Sandpack>\n\n```js\nimport { createContext, useContext, useState } from 'react';\n\nconst ThemeContext = createContext(null);\nconst CurrentUserContext = createContext(null);\n\nexport default function MyApp() {\n  const [theme, setTheme] = useState('light');\n  const [currentUser, setCurrentUser] = useState(null);\n  return (\n    <ThemeContext value={theme}>\n      <CurrentUserContext\n        value={{\n          currentUser,\n          setCurrentUser\n        }}\n      >\n        <WelcomePanel />\n        <label>\n          <input\n            type=\"checkbox\"\n            checked={theme === 'dark'}\n            onChange={(e) => {\n              setTheme(e.target.checked ? 'dark' : 'light')\n            }}\n          />\n          Use dark mode\n        </label>\n      </CurrentUserContext>\n    </ThemeContext>\n  )\n}\n\nfunction WelcomePanel({ children }) {\n  const {currentUser} = useContext(CurrentUserContext);\n  return (\n    <Panel title=\"Welcome\">\n      {currentUser !== null ?\n        <Greeting /> :\n        <LoginForm />\n      }\n    </Panel>\n  );\n}\n\nfunction Greeting() {\n  const {currentUser} = useContext(CurrentUserContext);\n  return (\n    <p>You logged in as {currentUser.name}.</p>\n  )\n}\n\nfunction LoginForm() {\n  const {setCurrentUser} = useContext(CurrentUserContext);\n  const [firstName, setFirstName] = useState('');\n  const [lastName, setLastName] = useState('');\n  const canLogin = firstName.trim() !== '' && lastName.trim() !== '';\n  return (\n    <>\n      <label>\n        First name{': '}\n        <input\n          required\n          value={firstName}\n          onChange={e => setFirstName(e.target.value)}\n        />\n      </label>\n      <label>\n        Last name{': '}\n        <input\n        required\n          value={lastName}\n          onChange={e => setLastName(e.target.value)}\n        />\n      </label>\n      <Button\n        disabled={!canLogin}\n        onClick={() => {\n          setCurrentUser({\n            name: firstName + ' ' + lastName\n          });\n        }}\n      >\n        Log in\n      </Button>\n      {!canLogin && <i>Fill in both fields.</i>}\n    </>\n  );\n}\n\nfunction Panel({ title, children }) {\n  const theme = useContext(ThemeContext);\n  const className = 'panel-' + theme;\n  return (\n    <section className={className}>\n      <h1>{title}</h1>\n      {children}\n    </section>\n  )\n}\n\nfunction Button({ children, disabled, onClick }) {\n  const theme = useContext(ThemeContext);\n  const className = 'button-' + theme;\n  return (\n    <button\n      className={className}\n      disabled={disabled}\n      onClick={onClick}\n    >\n      {children}\n    </button>\n  );\n}\n```\n\n```css\nlabel {\n  display: block;\n}\n\n.panel-light,\n.panel-dark {\n  border: 1px solid black;\n  border-radius: 4px;\n  padding: 20px;\n  margin-bottom: 10px;\n}\n.panel-light {\n  color: #222;\n  background: #fff;\n}\n\n.panel-dark {\n  color: #fff;\n  background: rgb(23, 32, 42);\n}\n\n.button-light,\n.button-dark {\n  border: 1px solid #777;\n  padding: 5px;\n  margin-right: 10px;\n  margin-top: 10px;\n}\n\n.button-dark {\n  background: #222;\n  color: #fff;\n}\n\n.button-light {\n  background: #fff;\n  color: #222;\n}\n```\n\n</Sandpack>\n\n<Solution />\n\n#### 컴포넌트로 Provider 분리 {/*extracting-providers-to-a-component*/}\n\n앱이 성장함에 따라 앱의 루트에 더 가까운 Context \"피라미드\"를 갖게 될 것입니다. 이는 잘못된 것이 아닙니다. 하지만 중첩이 보기에 좋지 않다면 Provider들을 단일 컴포넌트로 분리할 수 있습니다. 이 예시에서 `MyProviders`는 \"Context\"들을 숨기고 필요한 Provider들의 내부에 전달된 자식을 렌더링합니다. `theme` 및 `setTheme` State는 `MyApp` 자체에 필요하므로 `MyApp`이 여전히 해당 State를 소유하고 있습니다.\n\n<Sandpack>\n\n```js\nimport { createContext, useContext, useState } from 'react';\n\nconst ThemeContext = createContext(null);\nconst CurrentUserContext = createContext(null);\n\nexport default function MyApp() {\n  const [theme, setTheme] = useState('light');\n  return (\n    <MyProviders theme={theme} setTheme={setTheme}>\n      <WelcomePanel />\n      <label>\n        <input\n          type=\"checkbox\"\n          checked={theme === 'dark'}\n          onChange={(e) => {\n            setTheme(e.target.checked ? 'dark' : 'light')\n          }}\n        />\n        Use dark mode\n      </label>\n    </MyProviders>\n  );\n}\n\nfunction MyProviders({ children, theme, setTheme }) {\n  const [currentUser, setCurrentUser] = useState(null);\n  return (\n    <ThemeContext value={theme}>\n      <CurrentUserContext\n        value={{\n          currentUser,\n          setCurrentUser\n        }}\n      >\n        {children}\n      </CurrentUserContext>\n    </ThemeContext>\n  );\n}\n\nfunction WelcomePanel({ children }) {\n  const {currentUser} = useContext(CurrentUserContext);\n  return (\n    <Panel title=\"Welcome\">\n      {currentUser !== null ?\n        <Greeting /> :\n        <LoginForm />\n      }\n    </Panel>\n  );\n}\n\nfunction Greeting() {\n  const {currentUser} = useContext(CurrentUserContext);\n  return (\n    <p>You logged in as {currentUser.name}.</p>\n  )\n}\n\nfunction LoginForm() {\n  const {setCurrentUser} = useContext(CurrentUserContext);\n  const [firstName, setFirstName] = useState('');\n  const [lastName, setLastName] = useState('');\n  const canLogin = firstName !== '' && lastName !== '';\n  return (\n    <>\n      <label>\n        First name{': '}\n        <input\n          required\n          value={firstName}\n          onChange={e => setFirstName(e.target.value)}\n        />\n      </label>\n      <label>\n        Last name{': '}\n        <input\n        required\n          value={lastName}\n          onChange={e => setLastName(e.target.value)}\n        />\n      </label>\n      <Button\n        disabled={!canLogin}\n        onClick={() => {\n          setCurrentUser({\n            name: firstName + ' ' + lastName\n          });\n        }}\n      >\n        Log in\n      </Button>\n      {!canLogin && <i>Fill in both fields.</i>}\n    </>\n  );\n}\n\nfunction Panel({ title, children }) {\n  const theme = useContext(ThemeContext);\n  const className = 'panel-' + theme;\n  return (\n    <section className={className}>\n      <h1>{title}</h1>\n      {children}\n    </section>\n  )\n}\n\nfunction Button({ children, disabled, onClick }) {\n  const theme = useContext(ThemeContext);\n  const className = 'button-' + theme;\n  return (\n    <button\n      className={className}\n      disabled={disabled}\n      onClick={onClick}\n    >\n      {children}\n    </button>\n  );\n}\n```\n\n```css\nlabel {\n  display: block;\n}\n\n.panel-light,\n.panel-dark {\n  border: 1px solid black;\n  border-radius: 4px;\n  padding: 20px;\n  margin-bottom: 10px;\n}\n.panel-light {\n  color: #222;\n  background: #fff;\n}\n\n.panel-dark {\n  color: #fff;\n  background: rgb(23, 32, 42);\n}\n\n.button-light,\n.button-dark {\n  border: 1px solid #777;\n  padding: 5px;\n  margin-right: 10px;\n  margin-top: 10px;\n}\n\n.button-dark {\n  background: #222;\n  color: #fff;\n}\n\n.button-light {\n  background: #fff;\n  color: #222;\n}\n```\n\n</Sandpack>\n\n<Solution />\n\n#### Context와 Reducer를 통한 확장 {/*scaling-up-with-context-and-a-reducer*/}\n\n규모가 큰 앱에서는 컨텍스트와 [Reducer](/reference/react/useReducer)를 결합하여 컴포넌트에서 특정 State와 관련된 로직을 분리하는 것이 일반적입니다. 이 예시에서는 모든 \"Wiring\"이 Reducer와 두 개의 개별 Context가 포함된 `TasksContext.js`에 숨겨져 있습니다.\n\n이 예시에 대한 [전체 안내](/learn/scaling-up-with-reducer-and-context)를 읽어보세요.\n\n<Sandpack>\n\n```js src/App.js\nimport AddTask from './AddTask.js';\nimport TaskList from './TaskList.js';\nimport { TasksProvider } from './TasksContext.js';\n\nexport default function TaskApp() {\n  return (\n    <TasksProvider>\n      <h1>Day off in Kyoto</h1>\n      <AddTask />\n      <TaskList />\n    </TasksProvider>\n  );\n}\n```\n\n```js src/TasksContext.js\nimport { createContext, useContext, useReducer } from 'react';\n\nconst TasksContext = createContext(null);\n\nconst TasksDispatchContext = createContext(null);\n\nexport function TasksProvider({ children }) {\n  const [tasks, dispatch] = useReducer(\n    tasksReducer,\n    initialTasks\n  );\n\n  return (\n    <TasksContext value={tasks}>\n      <TasksDispatchContext value={dispatch}>\n        {children}\n      </TasksDispatchContext>\n    </TasksContext>\n  );\n}\n\nexport function useTasks() {\n  return useContext(TasksContext);\n}\n\nexport function useTasksDispatch() {\n  return useContext(TasksDispatchContext);\n}\n\nfunction tasksReducer(tasks, action) {\n  switch (action.type) {\n    case 'added': {\n      return [...tasks, {\n        id: action.id,\n        text: action.text,\n        done: false\n      }];\n    }\n    case 'changed': {\n      return tasks.map(t => {\n        if (t.id === action.task.id) {\n          return action.task;\n        } else {\n          return t;\n        }\n      });\n    }\n    case 'deleted': {\n      return tasks.filter(t => t.id !== action.id);\n    }\n    default: {\n      throw Error('Unknown action: ' + action.type);\n    }\n  }\n}\n\nconst initialTasks = [\n  { id: 0, text: 'Philosopher’s Path', done: true },\n  { id: 1, text: 'Visit the temple', done: false },\n  { id: 2, text: 'Drink matcha', done: false }\n];\n```\n\n```js src/AddTask.js\nimport { useState, useContext } from 'react';\nimport { useTasksDispatch } from './TasksContext.js';\n\nexport default function AddTask() {\n  const [text, setText] = useState('');\n  const dispatch = useTasksDispatch();\n  return (\n    <>\n      <input\n        placeholder=\"Add task\"\n        value={text}\n        onChange={e => setText(e.target.value)}\n      />\n      <button onClick={() => {\n        setText('');\n        dispatch({\n          type: 'added',\n          id: nextId++,\n          text: text,\n        });\n      }}>Add</button>\n    </>\n  );\n}\n\nlet nextId = 3;\n```\n\n```js src/TaskList.js\nimport { useState, useContext } from 'react';\nimport { useTasks, useTasksDispatch } from './TasksContext.js';\n\nexport default function TaskList() {\n  const tasks = useTasks();\n  return (\n    <ul>\n      {tasks.map(task => (\n        <li key={task.id}>\n          <Task task={task} />\n        </li>\n      ))}\n    </ul>\n  );\n}\n\nfunction Task({ task }) {\n  const [isEditing, setIsEditing] = useState(false);\n  const dispatch = useTasksDispatch();\n  let taskContent;\n  if (isEditing) {\n    taskContent = (\n      <>\n        <input\n          value={task.text}\n          onChange={e => {\n            dispatch({\n              type: 'changed',\n              task: {\n                ...task,\n                text: e.target.value\n              }\n            });\n          }} />\n        <button onClick={() => setIsEditing(false)}>\n          Save\n        </button>\n      </>\n    );\n  } else {\n    taskContent = (\n      <>\n        {task.text}\n        <button onClick={() => setIsEditing(true)}>\n          Edit\n        </button>\n      </>\n    );\n  }\n  return (\n    <label>\n      <input\n        type=\"checkbox\"\n        checked={task.done}\n        onChange={e => {\n          dispatch({\n            type: 'changed',\n            task: {\n              ...task,\n              done: e.target.checked\n            }\n          });\n        }}\n      />\n      {taskContent}\n      <button onClick={() => {\n        dispatch({\n          type: 'deleted',\n          id: task.id\n        });\n      }}>\n        Delete\n      </button>\n    </label>\n  );\n}\n```\n\n```css\nbutton { margin: 5px; }\nli { list-style-type: none; }\nul, li { margin: 0; padding: 0; }\n```\n\n</Sandpack>\n\n<Solution />\n\n</Recipes>\n\n---\n\n### Fallback 기본값 지정 {/*specifying-a-fallback-default-value*/}\n\nReact가 부모 트리에서 특정 <CodeStep step={1}>Context</CodeStep> Provider를 찾을 수 없는 경우, `useContext()`가 반환하는 Context 값은 [해당 Context를 생성할 때](/reference/react/createContext) 지정한 기본값과 동일합니다.\n\n```js [[1, 1, \"ThemeContext\"], [3, 1, \"null\"]]\nconst ThemeContext = createContext(null);\n```\n\n기본값은 **변경되지 않습니다**. Context를 업데이트하려면 [위에서 설명한 대로](#updating-data-passed-via-context) State와 함께 사용하세요.\n\n예를 들어 `null` 대신에 기본값으로 사용할 수 있는 더 의미 있는 값이 있는 경우가 많습니다.\n\n```js [[1, 1, \"ThemeContext\"], [3, 1, \"light\"]]\nconst ThemeContext = createContext('light');\n```\n\n이렇게 하면 실수로 해당 Provider 없이 일부 컴포넌트를 렌더링해도 깨지지 않습니다. 또한 테스트 환경에서 많은 Provider를 설정하지 않고도 컴포넌트가 테스트 환경에서 잘 작동하는 데 도움이 됩니다.\n\n아래 예시에서 \"Toggle theme\" 버튼은 **테마 Context Provider의 외부**에 있고 기본 컨텍스트 테마 값이 `'light'`이기 때문에 항상 밝게 표시되어 있습니다. 기본 테마를 `'dark'`로 변경해 보세요.\n\n<Sandpack>\n\n```js\nimport { createContext, useContext, useState } from 'react';\n\nconst ThemeContext = createContext('light');\n\nexport default function MyApp() {\n  const [theme, setTheme] = useState('light');\n  return (\n    <>\n      <ThemeContext value={theme}>\n        <Form />\n      </ThemeContext>\n      <Button onClick={() => {\n        setTheme(theme === 'dark' ? 'light' : 'dark');\n      }}>\n        Toggle theme\n      </Button>\n    </>\n  )\n}\n\nfunction Form({ children }) {\n  return (\n    <Panel title=\"Welcome\">\n      <Button>Sign up</Button>\n      <Button>Log in</Button>\n    </Panel>\n  );\n}\n\nfunction Panel({ title, children }) {\n  const theme = useContext(ThemeContext);\n  const className = 'panel-' + theme;\n  return (\n    <section className={className}>\n      <h1>{title}</h1>\n      {children}\n    </section>\n  )\n}\n\nfunction Button({ children, onClick }) {\n  const theme = useContext(ThemeContext);\n  const className = 'button-' + theme;\n  return (\n    <button className={className} onClick={onClick}>\n      {children}\n    </button>\n  );\n}\n```\n\n```css\n.panel-light,\n.panel-dark {\n  border: 1px solid black;\n  border-radius: 4px;\n  padding: 20px;\n  margin-bottom: 10px;\n}\n.panel-light {\n  color: #222;\n  background: #fff;\n}\n\n.panel-dark {\n  color: #fff;\n  background: rgb(23, 32, 42);\n}\n\n.button-light,\n.button-dark {\n  border: 1px solid #777;\n  padding: 5px;\n  margin-right: 10px;\n  margin-top: 10px;\n}\n\n.button-dark {\n  background: #222;\n  color: #fff;\n}\n\n.button-light {\n  background: #fff;\n  color: #222;\n}\n```\n\n</Sandpack>\n\n---\n\n### 트리의 일부 Context 오버라이딩 하기 {/*overriding-context-for-a-part-of-the-tree*/}\n\n트리의 일부분을 다른 값의 Provider로 감싸서 해당 부분에 대한 Context를 오버라이딩 할 수 있습니다.\n\n```js {3,5}\n<ThemeContext value=\"dark\">\n  ...\n  <ThemeContext value=\"light\">\n    <Footer />\n  </ThemeContext>\n  ...\n</ThemeContext>\n```\n\n필요한 만큼 Provider를 중첩하고 오버라이딩 할 수 있습니다.\n\n<Recipes titleText=\"Context 오버라이딩 예시\">\n\n#### 테마 오버라이드 {/*overriding-a-theme*/}\n\n여기서 `Footer` *내부의* 버튼은 외부의 버튼(`\"dark\"`)과 다른 Context 값(`\"light\"`)을 받습니다.\n\n<Sandpack>\n\n```js\nimport { createContext, useContext } from 'react';\n\nconst ThemeContext = createContext(null);\n\nexport default function MyApp() {\n  return (\n    <ThemeContext value=\"dark\">\n      <Form />\n    </ThemeContext>\n  )\n}\n\nfunction Form() {\n  return (\n    <Panel title=\"Welcome\">\n      <Button>Sign up</Button>\n      <Button>Log in</Button>\n      <ThemeContext value=\"light\">\n        <Footer />\n      </ThemeContext>\n    </Panel>\n  );\n}\n\nfunction Footer() {\n  return (\n    <footer>\n      <Button>Settings</Button>\n    </footer>\n  );\n}\n\nfunction Panel({ title, children }) {\n  const theme = useContext(ThemeContext);\n  const className = 'panel-' + theme;\n  return (\n    <section className={className}>\n      {title && <h1>{title}</h1>}\n      {children}\n    </section>\n  )\n}\n\nfunction Button({ children }) {\n  const theme = useContext(ThemeContext);\n  const className = 'button-' + theme;\n  return (\n    <button className={className}>\n      {children}\n    </button>\n  );\n}\n```\n\n```css\nfooter {\n  margin-top: 20px;\n  border-top: 1px solid #aaa;\n}\n\n.panel-light,\n.panel-dark {\n  border: 1px solid black;\n  border-radius: 4px;\n  padding: 20px;\n}\n.panel-light {\n  color: #222;\n  background: #fff;\n}\n\n.panel-dark {\n  color: #fff;\n  background: rgb(23, 32, 42);\n}\n\n.button-light,\n.button-dark {\n  border: 1px solid #777;\n  padding: 5px;\n  margin-right: 10px;\n  margin-top: 10px;\n}\n\n.button-dark {\n  background: #222;\n  color: #fff;\n}\n\n.button-light {\n  background: #fff;\n  color: #222;\n}\n```\n\n</Sandpack>\n\n<Solution />\n\n#### 자동으로 중첩된 제목 {/*automatically-nested-headings*/}\n\nCcontext Provider를 중첩할 때 정보를 \"누적\"할 수 있습니다. 이 예시에서 `Section` 컴포넌트는 섹션 중첩의 깊이를 지정하는 `LevelContext`를 추적합니다. 이 컴포넌트는 부모 섹션에서 `LevelContext`를 읽은 다음 1씩 증가한 `LevelContext` 숫자를 자식에게 제공합니다. 그 결과 `Heading`  컴포넌트는 얼마나 많은 `Section` 컴포넌트가 중첩되어 있는지에 따라 `<h1>`, `<h2>`, `<h3>`, ..., 태그 중 어떤 태그를 사용할지 자동으로 결정할 수 있습니다.\n\n이 예시에 대한 [자세한 안내](/learn/passing-data-deeply-with-context)를 읽어보세요.\n\n<Sandpack>\n\n```js\nimport Heading from './Heading.js';\nimport Section from './Section.js';\n\nexport default function Page() {\n  return (\n    <Section>\n      <Heading>Title</Heading>\n      <Section>\n        <Heading>Heading</Heading>\n        <Heading>Heading</Heading>\n        <Heading>Heading</Heading>\n        <Section>\n          <Heading>Sub-heading</Heading>\n          <Heading>Sub-heading</Heading>\n          <Heading>Sub-heading</Heading>\n          <Section>\n            <Heading>Sub-sub-heading</Heading>\n            <Heading>Sub-sub-heading</Heading>\n            <Heading>Sub-sub-heading</Heading>\n          </Section>\n        </Section>\n      </Section>\n    </Section>\n  );\n}\n```\n\n```js src/Section.js\nimport { useContext } from 'react';\nimport { LevelContext } from './LevelContext.js';\n\nexport default function Section({ children }) {\n  const level = useContext(LevelContext);\n  return (\n    <section className=\"section\">\n      <LevelContext value={level + 1}>\n        {children}\n      </LevelContext>\n    </section>\n  );\n}\n```\n\n```js src/Heading.js\nimport { useContext } from 'react';\nimport { LevelContext } from './LevelContext.js';\n\nexport default function Heading({ children }) {\n  const level = useContext(LevelContext);\n  switch (level) {\n    case 0:\n      throw Error('Heading must be inside a Section!');\n    case 1:\n      return <h1>{children}</h1>;\n    case 2:\n      return <h2>{children}</h2>;\n    case 3:\n      return <h3>{children}</h3>;\n    case 4:\n      return <h4>{children}</h4>;\n    case 5:\n      return <h5>{children}</h5>;\n    case 6:\n      return <h6>{children}</h6>;\n    default:\n      throw Error('Unknown level: ' + level);\n  }\n}\n```\n\n```js src/LevelContext.js\nimport { createContext } from 'react';\n\nexport const LevelContext = createContext(0);\n```\n\n```css\n.section {\n  padding: 10px;\n  margin: 5px;\n  border-radius: 5px;\n  border: 1px solid #aaa;\n}\n```\n\n</Sandpack>\n\n<Solution />\n\n</Recipes>\n\n---\n\n### 객체와 함수를 전달할 때 리렌더링 최적화하기 {/*optimizing-re-renders-when-passing-objects-and-functions*/}\n\nContext를 통해 객체와 함수를 포함한 모든 값을 전달할 수 있습니다.\n\n```js [[2, 10, \"{ currentUser, login }\"]]\nfunction MyApp() {\n  const [currentUser, setCurrentUser] = useState(null);\n\n  function login(response) {\n    storeCredentials(response.credentials);\n    setCurrentUser(response.user);\n  }\n\n  return (\n    <AuthContext value={{ currentUser, login }}>\n      <Page />\n    </AuthContext>\n  );\n}\n```\n\n여기서 <CodeStep step={2}>Context Value</CodeStep>는 두 개의 프로퍼티를 가진 자바스크립트 객체이며, 그 중 하나는 함수입니다. `MyApp`이 다시 렌더링할 때마다(예를 들어 경로 업데이트 시) *다른* 함수를 가리키는 *다른* 객체가 될 것이므로 React는 `useContext(AuthContext)`를 호출하는 트리 깊숙한 곳에 있는 모든 컴포넌트도 다시 렌더링해야 합니다.\n\n작은 앱에서는 문제가 되지 않습니다. 그러나 `currentUser`와 같은 기본적인 데이터가 변경되지 않았다면 다시 렌더링할 필요가 없습니다. React가 이 사실을 활용할 수 있도록 `login` 함수를 [`useCallback`](/reference/react/useCallback)으로 감싸고 객체 생성을 [`useMemo`](/reference/react/useMemo)로 감싸면 됩니다. 이것이 성능 최적화입니다.\n\n```js {6,9,11,14,17}\nimport { useCallback, useMemo } from 'react';\n\nfunction MyApp() {\n  const [currentUser, setCurrentUser] = useState(null);\n\n  const login = useCallback((response) => {\n    storeCredentials(response.credentials);\n    setCurrentUser(response.user);\n  }, []);\n\n  const contextValue = useMemo(() => ({\n    currentUser,\n    login\n  }), [currentUser, login]);\n\n  return (\n    <AuthContext value={contextValue}>\n      <Page />\n    </AuthContext>\n  );\n}\n```\n\n이 변경으로 인해 `MyApp`이 다시 렌더링해야 하는 경우에도 `currentUser`가 변경되지 않는 한 `useContext(AuthContext)`를 호출하는 컴포넌트는 다시 렌더링할 필요가 없습니다.\n\n[`useMemo`](/reference/react/useMemo#skipping-re-rendering-of-components)와 [`useCallback`](/reference/react/useCallback#skipping-re-rendering-of-components)에 대해 자세히 알아보세요.\n\n---\n\n## 문제 해결 {/*troubleshooting*/}\n\n### 컴포넌트가 Provider에서 값을 인식하지 못하고 있습니다. {/*my-component-doesnt-see-the-value-from-my-provider*/}\n\n이런 일이 발생하는 몇 가지 이유가 있습니다.\n\n1. `useContext()`를 호출하는 컴포넌트와 동일한 컴포넌트(또는 그 아래)에서 `<SomeContext>`를 렌더링하는 경우, `<SomeContext>`를 `useContext()`를 호출하는 컴포넌트의 *위와 바깥*으로 이동하세요.\n2. 컴포넌트를 `<SomeContext>`로 감싸는 것을 잊었거나 생각했던 것과 다른 트리의 다른 부분에 배치했을 수 있습니다. [React 개발자 도구](/learn/react-developer-tools)를 사용하여 계층 구조가 올바른지 확인하세요.\n3. 사용 중인 도구에서 발생하는 빌드 문제로 인해, 제공하는 컴포넌트에서의 `someContext`와 값을 읽는 컴포넌트에서의 `someContext`가 서로 다른 객체로 처리되는 문제가 발생할 수 있습니다. 예를 들어 심볼릭 링크를 사용하는 경우 이런 문제가 발생할 수 있습니다. 이를 확인하려면 `window.SomeContext1`과 `window.SomeContext2`를 전역에 할당하고 콘솔에서 `window.SomeContext1 === window.SomeContext2`인지 확인하면 됩니다. 동일하지 않은 경우 빌드 도구 수준에서 해당 문제를 수정하세요.\n### 기본값이 다른데도 Context가 `undefined`를 반환합니다. {/*i-am-always-getting-undefined-from-my-context-although-the-default-value-is-different*/}\n\n트리에 `value`가 없는 Provider가 있을 수 있습니다.\n\n```js {1,2}\n// 🚩 Doesn't work: no value prop\n<ThemeContext>\n   <Button />\n</ThemeContext>\n```\n\n`value`를 지정하는 것을 잊어버린 경우, `value={undefined}`를 전달하는 것과 같습니다.\n\n실수로 다른 Prop의 이름을 실수로 사용했을 수도 있습니다.\n\n```js {1,2}\n// 🚩 Doesn't work: prop should be called \"value\"\n<ThemeContext theme={theme}>\n   <Button />\n</ThemeContext>\n```\n\n두 가지 경우 모두 콘솔에 React에서 경고가 표시될 것입니다. 이를 수정하려면 Prop `value`를 호출하세요.\n\n```js {1,2}\n// ✅ Passing the value prop\n<ThemeContext value={theme}>\n   <Button />\n</ThemeContext>\n```\n\n[`createContext(defaultValue)` 호출의 기본값](#specifying-a-fallback-default-value)은 **위에 일치하는 Provider가 전혀 없는 경우**에만 사용된다는 점에 유의하세요. 부모 트리 어딘가에 `<SomeContext value={undefined}>` 컴포넌트가 있는 경우, `useContext(SomeContext)`를 호출하는 컴포넌트는 `undefined`를 Context 값으로 받습니다.\n"
  },
  {
    "path": "src/content/reference/react/useDebugValue.md",
    "content": "---\ntitle: useDebugValue\n---\n\n<Intro>\n\n`useDebugValue`는 [React DevTools](/learn/react-developer-tools)에서 커스텀 훅에 라벨을 추가할 수 있게 해주는 React Hook입니다.\n\n```js\nuseDebugValue(value, format?)\n```\n\n</Intro>\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `useDebugValue(value, format?)` {/*usedebugvalue*/}\n\n읽을 수 있는 디버그 값을 표시하기 위해 [커스텀 Hook](/learn/reusing-logic-with-custom-hooks)의 최상위 레벨에서 `useDebugValue`를 호출하세요.\n\n```js\nimport { useDebugValue } from 'react';\n\nfunction useOnlineStatus() {\n  // ...\n  useDebugValue(isOnline ? 'Online' : 'Offline');\n  // ...\n}\n```\n\n[아래에서 더 많은 예시를 확인해 보세요.](#usage)\n\n#### 매개변수 {/*parameters*/}\n\n* `value`: React DevTools에 표시하고 싶은 값입니다. 어떤 타입이든 될 수 있습니다.\n* **선택사항** `format`: 포맷팅 함수입니다. 컴포넌트가 검사될 때, React DevTools는 `value`를 인자로 포맷팅 함수를 호출하고, 포맷팅 함수가 반환한 포맷팅 된 값을 표시합니다. (포맷팅 된 값은 어떤 타입이든 될 수 있습니다.) 포맷팅 함수를 지정하지 않으면, 원래의 `value`가 표시됩니다.\n\n#### 반환값 {/*returns*/}\n\n`useDebugValue`는 아무것도 반환하지 않습니다.\n\n## 사용법 {/*usage*/}\n\n### 커스텀 Hook에 라벨 추가하기 {/*adding-a-label-to-a-custom-hook*/}\n\n읽을 수 있는 <CodeStep step={1}>디버그 값</CodeStep>을 표시하기 위해 [커스텀 Hook](/learn/reusing-logic-with-custom-hooks)의 최상위 레벨에서 `useDebugValue`를 호출하세요.\n\n```js [[1, 5, \"isOnline ? 'Online' : 'Offline'\"]]\nimport { useDebugValue } from 'react';\n\nfunction useOnlineStatus() {\n  // ...\n  useDebugValue(isOnline ? 'Online' : 'Offline');\n  // ...\n}\n```\n\n이렇게 하면 `useOnlineStatus`를 호출하는 컴포넌트를 검사할 때, `OnlineStatus: \"Online\"`와 같은 라벨이 붙습니다.\n\n![디버그 값이 표시된 React DevTools의 스크린샷](/images/docs/react-devtools-usedebugvalue.png)\n\n`useDebugValue`를 호출하지 않으면, 기본 데이터(이 예시에서는 `true`)만 표시됩니다.\n\n<Sandpack>\n\n```js\nimport { useOnlineStatus } from './useOnlineStatus.js';\n\nfunction StatusBar() {\n  const isOnline = useOnlineStatus();\n  return <h1>{isOnline ? '✅ Online' : '❌ Disconnected'}</h1>;\n}\n\nexport default function App() {\n  return <StatusBar />;\n}\n```\n\n```js src/useOnlineStatus.js active\nimport { useSyncExternalStore, useDebugValue } from 'react';\n\nexport function useOnlineStatus() {\n  const isOnline = useSyncExternalStore(subscribe, () => navigator.onLine, () => true);\n  useDebugValue(isOnline ? 'Online' : 'Offline');\n  return isOnline;\n}\n\nfunction subscribe(callback) {\n  window.addEventListener('online', callback);\n  window.addEventListener('offline', callback);\n  return () => {\n    window.removeEventListener('online', callback);\n    window.removeEventListener('offline', callback);\n  };\n}\n```\n\n</Sandpack>\n\n<Note>\n\n모든 커스텀 Hook에 디버그 값을 추가하지 마세요. 이는 공유 라이브러리의 일부이고 내부 구조가 복잡하여 검사하기 어려운 커스텀 Hook에 가장 유용합니다.\n\n</Note>\n\n---\n\n### 디버그 값의 포맷팅 지연하기 {/*deferring-formatting-of-a-debug-value*/}\n\n`useDebugValue`의 두 번째 인자로 포맷팅 함수를 전달할 수 있습니다.\n\n```js [[1, 1, \"date\", 18], [2, 1, \"date.toDateString()\"]]\nuseDebugValue(date, date => date.toDateString());\n```\n\n포맷팅 함수는 <CodeStep step={1}>디버그 값</CodeStep>을 인자로 받고, <CodeStep step={2}>포맷팅된 표시 값</CodeStep>을 반환해야 합니다. 컴포넌트가 검사될 때, React DevTools는 이 함수를 호출하고 그 결과를 표시합니다.\n\n이는 컴포넌트가 실제로 검사될 때까지 높은 비용이 들 수 있는 포맷팅 로직을 실행하지 않도록 해줍니다. 예를 들어, `date`가 Date 값이라면, 이는 렌더링마다 `toDateString()`을 호출하는 것을 피할 수 있습니다.\n"
  },
  {
    "path": "src/content/reference/react/useDeferredValue.md",
    "content": "---\ntitle: useDeferredValue\n---\n\n<Intro>\n\n`useDeferredValue`는 일부 UI 업데이트를 지연시킬 수 있는 React Hook입니다.\n\n```js\nconst deferredValue = useDeferredValue(value)\n```\n\n</Intro>\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `useDeferredValue(value, initialValue?)` {/*usedeferredvalue*/}\n\n컴포넌트의 최상위 레벨에서 `useDeferredValue`를 호출하여 지연된 버전의 값을 가져옵니다.\n\n```js\nimport { useState, useDeferredValue } from 'react';\n\nfunction SearchPage() {\n  const [query, setQuery] = useState('');\n  const deferredQuery = useDeferredValue(query);\n  // ...\n}\n```\n\n[아래에서 더 많은 예시를 확인하세요](#usage).\n\n#### 매개변수 {/*parameters*/}\n\n* `value`: 지연시키려는 값입니다. 모든 타입을 가질 수 있습니다.\n* **선택사항** `initialValue`: 컴포넌트 초기 렌더링 시 사용할 값입니다. 이 옵션을 생략하면 초기 렌더링 동안 `useDeferredValue`는 값을 지연시키지 않습니다. 이는 대신 렌더링할 `value`의 이전 버전이 없기 때문입니다.\n\n#### 반환값 {/*returns*/}\n\n- `currentValue`: 초기 렌더링 중 반환된 '지연된 값'은 사용자가 제공한 값과 같습니다. 업데이트가 발생하면 React는 먼저 이전 값으로 리렌더링을 시도(반환값이 이전 값과 일치하도록)하고, 그 다음 백그라운드에서 다시 새 값으로 리렌더링을 시도(반환값이 업데이트된 새 값과 일치하도록)합니다.\n\n\n#### 주의 사항 {/*caveats*/}\n\n- Transition 내에서 업데이트할 때 `useDeferredValue`는 항상 새로운 `value`를 반환하며 지연된 렌더링을 생성하지 않습니다. 이미 업데이트가 지연되었기 때문입니다.\n\n- `useDeferredValue`에 전달하는 값은 문자열 및 숫자와 같은 원시값이거나, 컴포넌트의 외부에서 생성된 객체여야 합니다. 렌더링 중에 새 객체를 생성하고 즉시 `useDeferredValue`에 전달하면 렌더링할 때마다 값이 달라져 불필요한 백그라운드 리렌더링이 발생할 수 있습니다.\n\n- `useDeferredValue`가 현재 렌더링(여전히 이전 값을 사용하는 경우) 외에 다른 값([`Object.is`](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Object/is)로 비교)을 받으면 백그라운드에서 새 값으로 리렌더링하도록 예약합니다. `value`에 대한 또 다른 업데이트가 있으면 백그라운드 리렌더링은 중단될 수 있습니다. React는 백그라운드 리렌더링을 처음부터 다시 시작할 것입니다. 예를 들어 차트가 리렌더링 가능한 지연된 값을 받는 속도보다 사용자가 Input에 값을 입력하는 속도가 더 빠른 경우, 차트는 사용자가 입력을 멈춘 후에만 리렌더링됩니다.\n\n- `useDeferredValue`는 [`<Suspense>`](/reference/react/Suspense)와 통합됩니다. 새로운 값으로 인한 백그라운드 업데이트로 인해 UI가 일시 중단되면 사용자는 Fallback을 볼 수 없습니다. 데이터가 로딩될 때까지 이전 지연된 값이 표시됩니다.\n\n- `useDeferredValue`는 그 자체로 추가 네트워크 요청을 방지하지 않습니다.\n\n- `useDeferredValue` 자체로 인한 고정된 지연은 없습니다. React는 원래의 리렌더링을 완료하자마자 즉시 새로운 지연된 값으로 백그라운드 리렌더링 작업을 시작합니다. 그러나 이벤트로 인한 업데이트(예: 타이핑)는 백그라운드 리렌더링을 중단하고 우선순위를 갖습니다.\n\n- `useDeferredValue`로 인한 백그라운드 리렌더링은 화면에 커밋될 때까지 Effect를 실행하지 않습니다. 백그라운드 리렌더링이 일시 중단되면 데이터가 로딩되고 UI가 업데이트된 후에 해당 Effect가 실행됩니다.\n\n---\n\n## 사용법 {/*usage*/}\n\n### 새 콘텐츠가 로딩되는 동안 오래된 콘텐츠 표시하기 {/*showing-stale-content-while-fresh-content-is-loading*/}\n\n컴포넌트의 최상위 레벨에서 `useDeferredValue`를 호출하여 UI 일부 업데이트를 지연할 수 있습니다.\n\n```js [[1, 5, \"query\"], [2, 5, \"deferredQuery\"]]\nimport { useState, useDeferredValue } from 'react';\n\nfunction SearchPage() {\n  const [query, setQuery] = useState('');\n  const deferredQuery = useDeferredValue(query);\n  // ...\n}\n```\n\n초기 렌더링 중에 <CodeStep step={2}>지연된 값</CodeStep>은 사용자가 제공한 <CodeStep step={1}>값</CodeStep>과 일치합니다.\n\n업데이트가 발생하면 <CodeStep step={2}>지연된 값</CodeStep>은 최신 <CodeStep step={1}>값</CodeStep>보다 \"뒤쳐지게\" 됩니다. React는 먼저 지연된 값을 업데이트하지 *않은 채로* 렌더링한 다음, 백그라운드에서 새로 받은 값으로 리렌더링을 시도합니다.\n\n\n**이것이 언제 유용한지 예시를 통해 살펴보겠습니다.**\n\n<Note>\n\n이 예시에서는 Suspense 지원 데이터 소스 중 하나를 사용한다고 가정합니다.\n\n- [Relay](https://relay.dev/docs/guided-tour/rendering/loading-states/)와 [Next.js](https://nextjs.org/docs/app/getting-started/fetching-data#with-suspense) 같이 Suspense를 지원하는 프레임워크로 데이터 가져오기.\n- [`lazy`](/reference/react/lazy)를 활용한 지연 로딩 컴포넌트.\n- [`use`](/reference/react/use)를 사용해서 Promise 값 읽기.\n\n[Suspense와 그 한계에 대해 자세히 알아보기](/reference/react/Suspense).\n\n</Note>\n\n\n아래 예시에서는 검색 결과를 불러오는 동안 `SearchResults` 컴포넌트가 [일시 중지<sup>Suspend</sup>](/reference/react/Suspense#displaying-a-fallback-while-content-is-loading)됩니다. `\"a\"`를 입력하고 결과를 기다린 다음 `\"ab\"`로 수정해 보세요. `\"a\"`에 대한 결과가 로딩 폴백으로 대체될 것입니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { Suspense, useState } from 'react';\nimport SearchResults from './SearchResults.js';\n\nexport default function App() {\n  const [query, setQuery] = useState('');\n  return (\n    <>\n      <label>\n        Search albums:\n        <input value={query} onChange={e => setQuery(e.target.value)} />\n      </label>\n      <Suspense fallback={<h2>Loading...</h2>}>\n        <SearchResults query={query} />\n      </Suspense>\n    </>\n  );\n}\n```\n\n```js src/SearchResults.js\nimport {use} from 'react';\nimport { fetchData } from './data.js';\n\nexport default function SearchResults({ query }) {\n  if (query === '') {\n    return null;\n  }\n  const albums = use(fetchData(`/search?q=${query}`));\n  if (albums.length === 0) {\n    return <p>No matches for <i>\"{query}\"</i></p>;\n  }\n  return (\n    <ul>\n      {albums.map(album => (\n        <li key={album.id}>\n          {album.title} ({album.year})\n        </li>\n      ))}\n    </ul>\n  );\n}\n```\n\n```js src/data.js hidden\n// Note: the way you would do data fetching depends on\n// the framework that you use together with Suspense.\n// Normally, the caching logic would be inside a framework.\n\nlet cache = new Map();\n\nexport function fetchData(url) {\n  if (!cache.has(url)) {\n    cache.set(url, getData(url));\n  }\n  return cache.get(url);\n}\n\nasync function getData(url) {\n  if (url.startsWith('/search?q=')) {\n    return await getSearchResults(url.slice('/search?q='.length));\n  } else {\n    throw Error('Not implemented');\n  }\n}\n\nasync function getSearchResults(query) {\n  // Add a fake delay to make waiting noticeable.\n  await new Promise(resolve => {\n    setTimeout(resolve, 1000);\n  });\n\n  const allAlbums = [{\n    id: 13,\n    title: 'Let It Be',\n    year: 1970\n  }, {\n    id: 12,\n    title: 'Abbey Road',\n    year: 1969\n  }, {\n    id: 11,\n    title: 'Yellow Submarine',\n    year: 1969\n  }, {\n    id: 10,\n    title: 'The Beatles',\n    year: 1968\n  }, {\n    id: 9,\n    title: 'Magical Mystery Tour',\n    year: 1967\n  }, {\n    id: 8,\n    title: 'Sgt. Pepper\\'s Lonely Hearts Club Band',\n    year: 1967\n  }, {\n    id: 7,\n    title: 'Revolver',\n    year: 1966\n  }, {\n    id: 6,\n    title: 'Rubber Soul',\n    year: 1965\n  }, {\n    id: 5,\n    title: 'Help!',\n    year: 1965\n  }, {\n    id: 4,\n    title: 'Beatles For Sale',\n    year: 1964\n  }, {\n    id: 3,\n    title: 'A Hard Day\\'s Night',\n    year: 1964\n  }, {\n    id: 2,\n    title: 'With The Beatles',\n    year: 1963\n  }, {\n    id: 1,\n    title: 'Please Please Me',\n    year: 1963\n  }];\n\n  const lowerQuery = query.trim().toLowerCase();\n  return allAlbums.filter(album => {\n    const lowerTitle = album.title.toLowerCase();\n    return (\n      lowerTitle.startsWith(lowerQuery) ||\n      lowerTitle.indexOf(' ' + lowerQuery) !== -1\n    )\n  });\n}\n```\n\n```css\ninput { margin: 10px; }\n```\n\n</Sandpack>\n\n흔히 사용되는 또 다른 UI 패턴은 결과 목록의 업데이트를 지연(*defer*) 하고, 새로운 결과가 준비될 때까지 이전 결과를 계속 표시하는 것입니다. `useDeferredValue`를 호출하여 쿼리의 지연된 버전을 전달하세요.\n\n```js {3,11}\nexport default function App() {\n  const [query, setQuery] = useState('');\n  const deferredQuery = useDeferredValue(query);\n  return (\n    <>\n      <label>\n        Search albums:\n        <input value={query} onChange={e => setQuery(e.target.value)} />\n      </label>\n      <Suspense fallback={<h2>Loading...</h2>}>\n        <SearchResults query={deferredQuery} />\n      </Suspense>\n    </>\n  );\n}\n```\n\n`query`는 즉시 업데이트되므로 Input에 새 값이 표시됩니다. 그러나 `deferredQuery`는 데이터가 로딩될 때까지 이전 값을 유지하므로 `SearchResults`는 잠시 동안 오래된 결과를 표시합니다.\n\n아래 예시에서 `\"a\"`를 입력하고 결과가 로딩될 때까지 기다린 다음, 입력값을 `\"ab\"`로 수정해보세요. 이제 새 결과가 로딩될 때까지 Suspense 폴백 대신 오래된 결과 목록이 표시되는 것을 확인할 수 있습니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { Suspense, useState, useDeferredValue } from 'react';\nimport SearchResults from './SearchResults.js';\n\nexport default function App() {\n  const [query, setQuery] = useState('');\n  const deferredQuery = useDeferredValue(query);\n  return (\n    <>\n      <label>\n        Search albums:\n        <input value={query} onChange={e => setQuery(e.target.value)} />\n      </label>\n      <Suspense fallback={<h2>Loading...</h2>}>\n        <SearchResults query={deferredQuery} />\n      </Suspense>\n    </>\n  );\n}\n```\n\n```js src/SearchResults.js\nimport {use} from 'react';\nimport { fetchData } from './data.js';\n\nexport default function SearchResults({ query }) {\n  if (query === '') {\n    return null;\n  }\n  const albums = use(fetchData(`/search?q=${query}`));\n  if (albums.length === 0) {\n    return <p>No matches for <i>\"{query}\"</i></p>;\n  }\n  return (\n    <ul>\n      {albums.map(album => (\n        <li key={album.id}>\n          {album.title} ({album.year})\n        </li>\n      ))}\n    </ul>\n  );\n}\n```\n\n```js src/data.js hidden\n// Note: the way you would do data fetching depends on\n// the framework that you use together with Suspense.\n// Normally, the caching logic would be inside a framework.\n\nlet cache = new Map();\n\nexport function fetchData(url) {\n  if (!cache.has(url)) {\n    cache.set(url, getData(url));\n  }\n  return cache.get(url);\n}\n\nasync function getData(url) {\n  if (url.startsWith('/search?q=')) {\n    return await getSearchResults(url.slice('/search?q='.length));\n  } else {\n    throw Error('Not implemented');\n  }\n}\n\nasync function getSearchResults(query) {\n  // Add a fake delay to make waiting noticeable.\n  await new Promise(resolve => {\n    setTimeout(resolve, 1000);\n  });\n\n  const allAlbums = [{\n    id: 13,\n    title: 'Let It Be',\n    year: 1970\n  }, {\n    id: 12,\n    title: 'Abbey Road',\n    year: 1969\n  }, {\n    id: 11,\n    title: 'Yellow Submarine',\n    year: 1969\n  }, {\n    id: 10,\n    title: 'The Beatles',\n    year: 1968\n  }, {\n    id: 9,\n    title: 'Magical Mystery Tour',\n    year: 1967\n  }, {\n    id: 8,\n    title: 'Sgt. Pepper\\'s Lonely Hearts Club Band',\n    year: 1967\n  }, {\n    id: 7,\n    title: 'Revolver',\n    year: 1966\n  }, {\n    id: 6,\n    title: 'Rubber Soul',\n    year: 1965\n  }, {\n    id: 5,\n    title: 'Help!',\n    year: 1965\n  }, {\n    id: 4,\n    title: 'Beatles For Sale',\n    year: 1964\n  }, {\n    id: 3,\n    title: 'A Hard Day\\'s Night',\n    year: 1964\n  }, {\n    id: 2,\n    title: 'With The Beatles',\n    year: 1963\n  }, {\n    id: 1,\n    title: 'Please Please Me',\n    year: 1963\n  }];\n\n  const lowerQuery = query.trim().toLowerCase();\n  return allAlbums.filter(album => {\n    const lowerTitle = album.title.toLowerCase();\n    return (\n      lowerTitle.startsWith(lowerQuery) ||\n      lowerTitle.indexOf(' ' + lowerQuery) !== -1\n    )\n  });\n}\n```\n\n```css\ninput { margin: 10px; }\n```\n\n</Sandpack>\n\n<DeepDive>\n\n#### 값을 지연시키는 것은 내부적으로 어떻게 작동하나요? {/*how-does-deferring-a-value-work-under-the-hood*/}\n\n두 단계로 진행된다고 생각하면 됩니다.\n\n1. **먼저 React는 새로운 `query` 값(`\"ab\"`)으로 다시 렌더링하지만, `deferredQuery`는 이전 값(여전히 `\"a\"`)을 사용합니다.** 결과 목록에는 이 지연(*deferred*)된 `deferredQuery` 값이 전달되며, 이는 `query` 값보다 뒤처져서 동작합니다.\n\n2. **백그라운드에서 React는 `query`와 `deferredQuery`를 모두 `\"ab\"`로 업데이트한 상태로 리렌더링을 시도합니다.** 이 리렌더링이 완료되면 React는 이를 화면에 표시합니다. 그러나 일시 중단되는 경우(`\"ab\"`에 대한 결과가 아직 로딩되지 않은 경우) React는 이 렌더링 시도를 포기하며, 데이터가 로딩된 후 이 리렌더링을 다시 시도합니다. 사용자는 데이터가 준비될 때까지 오래된 지연된 값을 계속 보게 됩니다.\n\n지연된 \"background\" 렌더링은 중단할 수 있습니다. 예를 들어 Input을 다시 입력하면 React는 지연된 값을 버리고 새 값으로 다시 시작합니다. React는 항상 가장 최근에 제공받은 값을 사용합니다.\n\n여전히 각 키 입력마다 네트워크 요청이 있다는 점에 주의하세요. 여기서 지연되는 것은 네트워크 요청 자체가 아니라 결과가 준비될 때까지 결과를 표시하는 것입니다. 사용자가 계속 입력하더라도 각 키 입력에 대한 응답은 캐시 되므로 백스페이스를 누르면 즉시 다시 가져오지 않습니다.\n\n</DeepDive>\n\n---\n\n### 콘텐츠가 오래되었음을 표시하기 {/*indicating-that-the-content-is-stale*/}\n\n위 예시에서는 최신 쿼리에 대한 결과 목록이 아직 로딩 중이라는 표시가 없습니다. 새 결과를 로딩하는 데 시간이 오래 걸리는 경우 사용자에게 혼란을 줄 수 있습니다. 결과 목록이 최신 쿼리와 일치하지 않는다는 것을 사용자에게 더 명확하게 알리기 위해, 오래된 결과 목록이 표시될 때 시각적 표시를 추가할 수 있습니다.\n\n```js {2}\n<div style={{\n  opacity: query !== deferredQuery ? 0.5 : 1,\n}}>\n  <SearchResults query={deferredQuery} />\n</div>\n```\n\n이렇게 변경하면 입력을 시작하자마자 새 결과 목록이 로딩될 때까지 오래된 결과 목록이 약간 어두워집니다. 아래 예시에서와 같이 점진적으로 어두워진다고 느껴지도록 CSS Transition을 추가하여 흐리게 표시되는 것을 지연시킬 수도 있습니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { Suspense, useState, useDeferredValue } from 'react';\nimport SearchResults from './SearchResults.js';\n\nexport default function App() {\n  const [query, setQuery] = useState('');\n  const deferredQuery = useDeferredValue(query);\n  const isStale = query !== deferredQuery;\n  return (\n    <>\n      <label>\n        Search albums:\n        <input value={query} onChange={e => setQuery(e.target.value)} />\n      </label>\n      <Suspense fallback={<h2>Loading...</h2>}>\n        <div style={{\n          opacity: isStale ? 0.5 : 1,\n          transition: isStale ? 'opacity 0.2s 0.2s linear' : 'opacity 0s 0s linear'\n        }}>\n          <SearchResults query={deferredQuery} />\n        </div>\n      </Suspense>\n    </>\n  );\n}\n```\n\n```js src/SearchResults.js\nimport {use} from 'react';\nimport { fetchData } from './data.js';\n\nexport default function SearchResults({ query }) {\n  if (query === '') {\n    return null;\n  }\n  const albums = use(fetchData(`/search?q=${query}`));\n  if (albums.length === 0) {\n    return <p>No matches for <i>\"{query}\"</i></p>;\n  }\n  return (\n    <ul>\n      {albums.map(album => (\n        <li key={album.id}>\n          {album.title} ({album.year})\n        </li>\n      ))}\n    </ul>\n  );\n}\n```\n\n```js src/data.js hidden\n// Note: the way you would do data fetching depends on\n// the framework that you use together with Suspense.\n// Normally, the caching logic would be inside a framework.\n\nlet cache = new Map();\n\nexport function fetchData(url) {\n  if (!cache.has(url)) {\n    cache.set(url, getData(url));\n  }\n  return cache.get(url);\n}\n\nasync function getData(url) {\n  if (url.startsWith('/search?q=')) {\n    return await getSearchResults(url.slice('/search?q='.length));\n  } else {\n    throw Error('Not implemented');\n  }\n}\n\nasync function getSearchResults(query) {\n  // Add a fake delay to make waiting noticeable.\n  await new Promise(resolve => {\n    setTimeout(resolve, 1000);\n  });\n\n  const allAlbums = [{\n    id: 13,\n    title: 'Let It Be',\n    year: 1970\n  }, {\n    id: 12,\n    title: 'Abbey Road',\n    year: 1969\n  }, {\n    id: 11,\n    title: 'Yellow Submarine',\n    year: 1969\n  }, {\n    id: 10,\n    title: 'The Beatles',\n    year: 1968\n  }, {\n    id: 9,\n    title: 'Magical Mystery Tour',\n    year: 1967\n  }, {\n    id: 8,\n    title: 'Sgt. Pepper\\'s Lonely Hearts Club Band',\n    year: 1967\n  }, {\n    id: 7,\n    title: 'Revolver',\n    year: 1966\n  }, {\n    id: 6,\n    title: 'Rubber Soul',\n    year: 1965\n  }, {\n    id: 5,\n    title: 'Help!',\n    year: 1965\n  }, {\n    id: 4,\n    title: 'Beatles For Sale',\n    year: 1964\n  }, {\n    id: 3,\n    title: 'A Hard Day\\'s Night',\n    year: 1964\n  }, {\n    id: 2,\n    title: 'With The Beatles',\n    year: 1963\n  }, {\n    id: 1,\n    title: 'Please Please Me',\n    year: 1963\n  }];\n\n  const lowerQuery = query.trim().toLowerCase();\n  return allAlbums.filter(album => {\n    const lowerTitle = album.title.toLowerCase();\n    return (\n      lowerTitle.startsWith(lowerQuery) ||\n      lowerTitle.indexOf(' ' + lowerQuery) !== -1\n    )\n  });\n}\n```\n\n```css\ninput { margin: 10px; }\n```\n\n</Sandpack>\n\n---\n\n### UI 일부에 대해 리렌더링 지연하기 {/*deferring-re-rendering-for-a-part-of-the-ui*/}\n\n`useDeferredValue`를 성능 최적화 용도로 적용할 수도 있습니다. UI 일부의 리렌더링 속도가 느리고, 이를 최적화할 쉬운 방법이 없으며, 나머지 UI를 차단하지 않도록 하려는 경우에 유용합니다.\n\n키 입력 시마다 리렌더링되는 텍스트 필드와 컴포넌트(예: 차트 또는 긴 목록)가 있다고 상상해 보세요.\n\n```js\nfunction App() {\n  const [text, setText] = useState('');\n  return (\n    <>\n      <input value={text} onChange={e => setText(e.target.value)} />\n      <SlowList text={text} />\n    </>\n  );\n}\n```\n\n먼저, Props가 같은 경우 리렌더링을 건너뛰도록 `SlowList`를 최적화합니다. 이렇게 하려면, [`memo`](/reference/react/memo#skipping-re-rendering-when-props-are-unchanged)로 감싸주세요.\n\n```js {1,3}\nconst SlowList = memo(function SlowList({ text }) {\n  // ...\n});\n```\n\n그러나 이는 `SlowList` Props가 이전 렌더링 때와 *동일한* 경우에만 도움이 됩니다. 지금 직면하고 있는 문제는 Props가 *다르고* 실제로 다른 시각적 출력을 보여줘야 할 때 속도가 느리다는 것입니다.\n\n구체적으로, 주요 성능 문제는 Input에 타이핑할 때마다 `SlowList`가 새로운 Props를 수신하고 전체 트리를 리렌더링하면 타이핑이 끊기는 느낌이 든다는 것입니다. 이 경우 `useDeferredValue`를 사용하면 입력 업데이트(빨라야 하는)를 결과 목록 업데이트(느려도 되는)보다 높은 우선순위에 둘 수 있습니다.\n\n```js {3,7}\nfunction App() {\n  const [text, setText] = useState('');\n  const deferredText = useDeferredValue(text);\n  return (\n    <>\n      <input value={text} onChange={e => setText(e.target.value)} />\n      <SlowList text={deferredText} />\n    </>\n  );\n}\n```\n\n이렇게 한다고 해서 `SlowList`의 리렌더링 속도가 빨라지지는 않습니다. 하지만 키 입력을 차단하지 않도록 목록 리렌더링의 우선순위를 낮출 수 있다는 것을 React에 알려줍니다. 목록은 입력보다 \"지연\"되었다가 \"따라잡을\" 것입니다. 이전과 마찬가지로 React는 가능한 한 빨리 목록을 업데이트하려고 시도하지만, 사용자가 입력하는 것을 차단하지는 않습니다.\n\n<Recipes titleText=\"useDeferredValue와 최적화되지 않은 리렌더링의 차이점\" titleId=\"examples\">\n\n#### 목록 리렌더링 지연 {/*deferred-re-rendering-of-the-list*/}\n\n이 예시에서는 `SlowList` 컴포넌트의 각 항목을 **인위적으로 느려지도록 하여** `useDeferredValue`를 통해 input의 반응성을 유지하는 방법을 확인할 수 있습니다. input에 타이핑하면 입력은 빠르게 느껴지는 반면 목록은 \"지연\"되는 것을 확인할 수 있습니다.\n\n<Sandpack>\n\n```js\nimport { useState, useDeferredValue } from 'react';\nimport SlowList from './SlowList.js';\n\nexport default function App() {\n  const [text, setText] = useState('');\n  const deferredText = useDeferredValue(text);\n  return (\n    <>\n      <input value={text} onChange={e => setText(e.target.value)} />\n      <SlowList text={deferredText} />\n    </>\n  );\n}\n```\n\n```js src/SlowList.js\nimport { memo } from 'react';\n\nconst SlowList = memo(function SlowList({ text }) {\n  // Log once. The actual slowdown is inside SlowItem.\n  console.log('[ARTIFICIALLY SLOW] Rendering 250 <SlowItem />');\n\n  let items = [];\n  for (let i = 0; i < 250; i++) {\n    items.push(<SlowItem key={i} text={text} />);\n  }\n  return (\n    <ul className=\"items\">\n      {items}\n    </ul>\n  );\n});\n\nfunction SlowItem({ text }) {\n  let startTime = performance.now();\n  while (performance.now() - startTime < 1) {\n    // Do nothing for 1 ms per item to emulate extremely slow code\n  }\n\n  return (\n    <li className=\"item\">\n      Text: {text}\n    </li>\n  )\n}\n\nexport default SlowList;\n```\n\n```css\n.items {\n  padding: 0;\n}\n\n.item {\n  list-style: none;\n  display: block;\n  height: 40px;\n  padding: 5px;\n  margin-top: 10px;\n  border-radius: 4px;\n  border: 1px solid #aaa;\n}\n```\n\n</Sandpack>\n\n<Solution />\n\n#### 목록의 최적화되지 않은 리렌더링 {/*unoptimized-re-rendering-of-the-list*/}\n\n이 예시에서는 `SlowList` 컴포넌트의 각 항목이 인위적으로 느려지도록 하여 `useDeferredValue`를 통해 입력 반응성을 유지하는 방법을 확인할 수 있습니다. input에 타이핑하면 입력은 빠르게 느껴지는 반면 목록은 \"지연\"되는 것을 확인할 수 있습니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\nimport SlowList from './SlowList.js';\n\nexport default function App() {\n  const [text, setText] = useState('');\n  return (\n    <>\n      <input value={text} onChange={e => setText(e.target.value)} />\n      <SlowList text={text} />\n    </>\n  );\n}\n```\n\n```js src/SlowList.js\nimport { memo } from 'react';\n\nconst SlowList = memo(function SlowList({ text }) {\n  // Log once. The actual slowdown is inside SlowItem.\n  console.log('[ARTIFICIALLY SLOW] Rendering 250 <SlowItem />');\n\n  let items = [];\n  for (let i = 0; i < 250; i++) {\n    items.push(<SlowItem key={i} text={text} />);\n  }\n  return (\n    <ul className=\"items\">\n      {items}\n    </ul>\n  );\n});\n\nfunction SlowItem({ text }) {\n  let startTime = performance.now();\n  while (performance.now() - startTime < 1) {\n    // Do nothing for 1 ms per item to emulate extremely slow code\n  }\n\n  return (\n    <li className=\"item\">\n      Text: {text}\n    </li>\n  )\n}\n\nexport default SlowList;\n```\n\n```css\n.items {\n  padding: 0;\n}\n\n.item {\n  list-style: none;\n  display: block;\n  height: 40px;\n  padding: 5px;\n  margin-top: 10px;\n  border-radius: 4px;\n  border: 1px solid #aaa;\n}\n```\n\n</Sandpack>\n\n<Solution />\n\n</Recipes>\n\n<Pitfall>\n\n이 최적화를 위해서는 `SlowList`를 [`memo`](/reference/react/memo)로 감싸야 합니다. `text`가 변경될 때마다 React는 부모 컴포넌트를 빠르게 리렌더링할 수 있어야 하기 때문입니다. 리렌더링하는 동안 `deferredText`는 여전히 이전 값을 가지므로 `SlowList`는 리렌더링을 건너뛸 수 있습니다. (Props는 변경되지 않았습니다.) [`memo`](/reference/react/memo)가 없으면 어쨌든 리렌더링해야 하므로 최적화의 취지가 무색해집니다.\n\n</Pitfall>\n\n<DeepDive>\n\n#### 값을 지연하는 것은 디바운싱 및 스로틀링과 어떤 점이 다른가요? {/*how-is-deferring-a-value-different-from-debouncing-and-throttling*/}\n\n이 시나리오에서 이전에 사용했을 수 있는 두 가지 일반적인 최적화 기술이 있습니다.\n\n- *디바운싱*은 타이핑을 멈출 때까지(예: 1초 동안) 기다렸다가 목록을 업데이트하는 것을 의미합니다.\n- *스로틀링*은 가끔씩(예: 최대 1초에 한 번) 목록을 업데이트하는 것을 의미합니다.\n\n이러한 기법들은 경우에 따라 유용하지만, `useDeferredValue`는 React 자체와 깊게 통합되어 있고 사용자의 기기에 맞게 조정되기 때문에 렌더링을 최적화하는 데 더 적합합니다.\n\n디바운싱이나 스로틀링과 달리 고정된 지연을 선택할 필요가 없습니다. 사용자의 디바이스가 빠른 경우(예: 고성능 노트북) 지연된 리렌더링은 거의 즉시 발생하며 눈에 띄지 않습니다. 사용자의 디바이스가 느린 경우, 기기 속도에 비례하여 목록이 Input에 '지연'됩니다.\n\n또한 디바운싱이나 스로틀링과 달리, `useDeferredValue`에 의해 수행되는 지연된 리렌더링은 기본적으로 중단할 수 있습니다. 즉, React가 큰 목록을 리렌더링하는 도중에 사용자가 다른 키 입력을 하면 React는 해당 리렌더링을 중단하고 키 입력을 처리한 다음 백그라운드에서 리렌더링을 시작합니다. 반면 디바운싱과 스로틀링은 렌더링이 키 입력을 차단하는 순간을 지연할 뿐이므로 여전히 불안정한 경험을 만들어 냅니다.\n\n최적화하려는 작업이 렌더링 중에 발생하지 않는 경우에도 디바운싱과 스로틀링은 여전히 유용합니다. 예를 들어 디바운싱과 스로틀링을 사용하면 네트워크 요청을 더 적게 처리할 수 있습니다. 이러한 기술을 함께 사용할 수도 있습니다.\n\n</DeepDive>\n"
  },
  {
    "path": "src/content/reference/react/useEffect.md",
    "content": "---\ntitle: useEffect\n---\n\n<Intro>\n\n`useEffect`는 [외부 시스템과 컴포넌트를 동기화](/learn/synchronizing-with-effects)하는 React Hook입니다.\n\n```js\nuseEffect(setup, dependencies?)\n```\n\n</Intro>\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `useEffect(setup, dependencies?)` {/*useeffect*/}\n\n컴포넌트의 최상위 레벨에서 `useEffect`를 호출하여 Effect를 선언할 수 있습니다.\n\n```js\nimport { useState, useEffect } from 'react';\nimport { createConnection } from './chat.js';\n\nfunction ChatRoom({ roomId }) {\n  const [serverUrl, setServerUrl] = useState('https://localhost:1234');\n\n  useEffect(() => {\n    const connection = createConnection(serverUrl, roomId);\n    connection.connect();\n    return () => {\n      connection.disconnect();\n    };\n  }, [serverUrl, roomId]);\n  // ...\n}\n```\n\n[아래에서 더 많은 예시를 보세요.](#usage)\n\n#### 매개변수 {/*parameters*/}\n\n+ `setup(설정)`: Effect의 로직이 포함된 함수입니다. 설정 함수는 선택적으로 *clean up(정리)* 함수를 반환할 수 있습니다. React는 컴포넌트가 DOM에 추가된 이후에 설정 함수를 실행합니다. 의존성의 변화에 따라 컴포넌트가 리렌더링이 되었을 경우, (설정 함수에 정리 함수를 추가했었다면) React는 이전 렌더링에 사용된 값으로 정리 함수를 실행한 후 새로운 값으로 설정 함수를 실행합니다. 컴포넌트가 DOM에서 제거된 경우에도 정리 함수를 실행합니다.\n\n+ `dependencies` **선택사항** : `설정` 함수의 코드 내부에서 참조되는 모든 반응형 값들이 포함된 배열로 구성됩니다. 반응형 값에는 props와 state, 모든 변수 및 컴포넌트 body에 직접적으로 선언된 함수들이 포함됩니다. 린터가 [React 환경에 맞게 설정되어 있을 경우](/learn/editor-setup#linting), 린터는 모든 반응형 값들이 의존성에 제대로 명시되어 있는지 검증할 것입니다. 의존성 배열은 항상 일정한 수의 항목을 가지고 있어야 하며 `[dep1, dep2, dep3]`과 같이 작성되어야 합니다. React는 각각의 의존성들을 [`Object.is`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is) 비교법을 통해 이전 값과 비교합니다. 의존성을 생략할 경우, Effect는 컴포넌트가 리렌더링될 때마다 실행됩니다. [인수에 의존성 배열을 추가했을 때, 빈 배열을 추가했을 때, 의존성을 추가하지 않았을 때의 차이를 확인해 보세요.](#examples-dependencies)\n\n#### 반환값 {/*returns*/}\n\n`useEffect`는 `undefined`를 반환합니다.\n\n#### 주의 사항 {/*caveats*/}\n\n* `useEffect`는 Hook이므로 컴포넌트의 최상위 또는 커스텀 Hook에서만 호출할 수 있습니다. 반복문이나 조건문에서는 사용할 수 없습니다. 필요한 경우 새로운 컴포넌트를 추출하고 해당 컴포넌트로 state를 이동해서 사용할 수 있습니다.\n\n* 외부 시스템과 컴포넌트를 동기화할 필요가 없는 경우, [Effect를 선언할 필요가 없을 수 있습니다.](/learn/you-might-not-need-an-effect)\n\n* Strict Mode를 사용할 경우, React는 실제 첫 번째 설정 함수가 실행되기 이전에 **개발 모드에만 한정하여 한 번의 추가적인 설정 + 정리 사이클을 실행합니다.** 이는 정리 로직이 설정 로직을 완벽히 \"반영\"하고 설정 로직이 수행하는 작업을 중단하거나 취소할 수 있는지를 확인하는 스트레스 테스트입니다. 이에 따라 문제가 생길 경우, [정리 함수를 구현하십시오.](/learn/synchronizing-with-effects#how-to-handle-the-effect-firing-twice-in-development)\n\n* 만약 의존성이 객체이거나 컴포넌트 내부에 선언된 함수일 경우에는 Effect가 필요 이상으로 재실행될 수 있습니다. 이를 수정하려면 불필요한 [객체 의존성](#removing-unnecessary-object-dependencies)이나 [함수 의존성](#updating-state-based-on-previous-state-from-an-effect)을 제거하세요. 또는 [state 업데이트를 추출](#updating-state-based-on-previous-state-from-an-effect)하거나 Effect 밖으로 [비 반응형 로직](#reading-the-latest-props-and-state-from-an-effect)을 빼낼 수 있습니다.\n\n* Effect가 사용자 상호작용(클릭 등)에 의해 발생하지 않았다면, React는 일반적으로 **Effect를 실행하기 전에 브라우저가 업데이트된 화면을 먼저 렌더링하도록 합니다.** 만약 Effect가 시각적인 작업을 수행하고 (예: 툴팁의 위치 조정), 이에 따라 지연이 눈에 띄게 나타난다면 (예: 깜빡임 현상), `useEffect` 대신 [`useLayoutEffect`](/reference/react/useLayoutEffect)를 사용하세요.\n\n* Effect가 사용자 상호작용(클릭 등)으로 인해 발생한 경우, **React는 화면이 업데이트되어 브라우저가 화면을 그리기 전에 Effect를 실행할 수 있습니다.** 이것이 Effect의 결과를 이벤트 시스템이 관찰할 수 있도록 보장합니다. 이는 대개 예상대로 작동하지만, `alert()`와 같이 작업을 브라우저가 화면을 그린 후로 미뤄야 하는 경우 `setTimeout`을 활용할 수 있습니다. 자세한 내용은 [reactwg/react-18/128](https://github.com/reactwg/react-18/discussions/128)을 참조하세요.\n\n* Effect가 사용자 상호작용(클릭 등)에 의해 발생했더라도, **React는 때로 Effect 내부의 상태 업데이트를 처리하기 전에 브라우저가 화면을 다시 그리도록 허용할 수 있습니다.** 이는 대개 예상대로 작동하지만, 브라우저가 화면을 다시 그리지 않도록 막아야 하는 상황이라면 `useEffect` 대신 [`useLayoutEffect`](/reference/react/useLayoutEffect)를 사용해야 합니다.\n\n* Effect는 **client 환경에서만 동작합니다.** 서버 렌더링에서는 동작하지 않습니다.\n\n---\n\n## 사용방법 {/*usage*/}\n\n### 외부 시스템과 연결 {/*connecting-to-an-external-system*/}\n\n몇몇 컴포넌트들은 페이지에 표시되는 동안 네트워크나 브라우저 API, 또는 서드파티 라이브러리와의 연결이 유지되어야 합니다. React에 제어되지 않는 이러한 시스템들을 *외부 시스템(external)* 이라 부릅니다.\n\n[컴포넌트를 외부 시스템과 연결](/learn/synchronizing-with-effects)하려면 컴포넌트의 최상위 레벨에서 `useEffect`를 호출해야 합니다.\n\n```js [[1, 8, \"const connection = createConnection(serverUrl, roomId);\"], [1, 9, \"connection.connect();\"], [2, 11, \"connection.disconnect();\"], [3, 13, \"[serverUrl, roomId]\"]]\nimport { useState, useEffect } from 'react';\nimport { createConnection } from './chat.js';\n\nfunction ChatRoom({ roomId }) {\n  const [serverUrl, setServerUrl] = useState('https://localhost:1234');\n\n  useEffect(() => {\n    const connection = createConnection(serverUrl, roomId);\n    connection.connect();\n    return () => {\n      connection.disconnect();\n    };\n  }, [serverUrl, roomId]);\n  // ...\n}\n```\n\n`useEffect`는 2개의 인수가 필요합니다.\n\n1. 외부 시스템과 컴포넌트를 연결하는 <CodeStep step={1}>설정 코드</CodeStep>가 포함된 *설정 함수*\n   - 외부 시스템과의 연결을 해제하는 <CodeStep step={2}>정리 코드</CodeStep>가 포함된 *정리 함수*를 반환할 수 있습니다.\n2. 위 함수 내부에서 사용하는 컴포넌트에서 비롯된 반응형 값들을 포함하는 <CodeStep step={3}>의존성 배열</CodeStep>\n\n**React는 설정 함수와 정리 함수가 필요할 때마다 호출할 수 있으며, 이는 여러 번 호출될 수 있습니다.**\n\n1. 컴포넌트가 화면에 추가되었을 때 <CodeStep step={1}>설정 코드</CodeStep>가 동작합니다 *(마운트 시)*.\n2. <CodeStep step={3}>의존성</CodeStep>이 변경된 컴포넌트가 리렌더링 될 때마다 아래 동작을 수행합니다.\n   - 먼저 <CodeStep step={2}>정리 코드</CodeStep>가 오래된 props와 state와 함께 실행됩니다.\n   - 이후, <CodeStep step={1}>설정 코드</CodeStep>가 새로운 props와 state와 함께 실행됩니다.\n3. 컴포넌트가 화면에서 제거된 이후에 <CodeStep step={2}>정리 코드</CodeStep>가 마지막으로 실행됩니다 *(마운트 해제 시)*.\n\n**위의 예시를 통해 순서를 설명해 보겠습니다.**\n\n위의 `ChatRoom` 컴포넌트가 화면에 추가되면 초기 `serverUrl`과 `roomId`를 이용해 채팅방과 연결될 것입니다. 리렌더링에 의해 `serverUrl` 또는 `roomId`가 변경된다면 (예를 들어 사용자가 드롭다운 메뉴를 이용해 다른 채팅방을 선택할 경우) *Effect는 이전 채팅방과의 연결을 해제하고 다음 채팅방과 연결합니다.* `ChatRoom` 컴포넌트가 화면에서 제거된다면 Effect는 마지막 채팅방과 이뤄진 연결을 해제할 것입니다.\n\nReact는 **[버그를 발견하기 위해](/learn/synchronizing-with-effects#step-3-add-cleanup-if-needed) 개발모드에서 <CodeStep step={1}>설정</CodeStep>이 실행되기 전에 <CodeStep step={1}>설정</CodeStep>과 <CodeStep step={2}>정리</CodeStep>를 한 번 더 실행시킵니다.** 이는 스트레스 테스트의 하나로써 Effect의 로직이 정확하게 수행되고 있는지를 검증합니다. 만약 가시적인 이슈가 보인다면 정리 함수의 로직에 놓친 부분이 있는 것입니다. 정리 함수는 설정 함수의 어떠한 동작이라도 중지하거나 실행 취소를 할 수 있어야 하며, 사용자는 *설정* 함수가 한 번 호출될 때와 *설정* → *정리* → *설정* 순서로 호출될 때의 차이를 느낄 수 없어야 합니다.\n\n**[각각의 Effect를 독립적인 프로세스로 작성](/learn/lifecycle-of-reactive-effects#each-effect-represents-a-separate-synchronization-process)하고 [정확한 설정/정리 사이클을 고려하세요.](/learn/lifecycle-of-reactive-effects#thinking-from-the-effects-perspective)** 컴포넌트의 마운트, 업데이트, 마운트 해제 여부는 중요하지 않아야 합니다. 정리 로직이 설정 로직과 정확하게 \"미러링\"될 때, Effect는 설정과 정리를 필요한 만큼 견고하게 처리합니다.\n\n<Note>\n\nEffect는 (채팅 시스템과 같은) 외부 시스템과 [컴포넌트가 동기화를 유지](/learn/synchronizing-with-effects)할 수 있도록 합니다. *외부 시스템*은 React에 의해 컨트롤되지 않는 모든 코드를 의미합니다. 예를 들어:\n\n* <CodeStep step={1}>[`setInterval()`](https://developer.mozilla.org/en-US/docs/Web/API/setInterval)</CodeStep>에 의해 관리되는 타이머 또는 <CodeStep step={2}>[`clearInterval()`](https://developer.mozilla.org/en-US/docs/Web/API/clearInterval)</CodeStep>.\n* <CodeStep step={1}>[`window.addEventListener()`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener)</CodeStep>을 이용한 이벤트 구독 또는 <CodeStep step={2}>[`window.removeEventListener()`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/removeEventListener)</CodeStep>.\n* <CodeStep step={1}>`animation.start()`</CodeStep>와 같은 서드 파티 애니메이션 라이브러리 API 또는 <CodeStep step={2}>`animation.reset()`</CodeStep>.\n\n**만약 외부 시스템과 React를 연결할 필요가 없다면 [Effect를 사용할 필요가 없을 수 있습니다.](/learn/you-might-not-need-an-effect)**\n\n</Note>\n\n<Recipes titleText=\"외부 시스템과 연결 예시\" titleId=\"examples-connecting\">\n\n#### 채팅 서버와 연결 {/*connecting-to-a-chat-server*/}\n\n이 예시에서는 `ChatRoom` 컴포넌트의 Effect를 통해 `chat.js`로 정의된 외부 시스템과 연결을 유지합니다. \"Open chat\"을 누르면 `ChatRoom` 컴포넌트가 나타납니다. 이 샌드박스는 개발 모드에서 동작하므로 [추가적인 연결-연결해제 사이클](/learn/synchronizing-with-effects#step-3-add-cleanup-if-needed)이 동작합니다. 드롭다운 메뉴나 input을 이용해 `roomId` 또는 `serverUrl`를 변경하고 어떻게 Effect가 chat을 재연결하는지 확인해 보세요. \"Close chat\"을 눌러 Effect가 마지막에 연결되었던 chat을 연결 해제하는 것도 확인해 보세요.\n\n<Sandpack>\n\n```js\nimport { useState, useEffect } from 'react';\nimport { createConnection } from './chat.js';\n\nfunction ChatRoom({ roomId }) {\n  const [serverUrl, setServerUrl] = useState('https://localhost:1234');\n\n  useEffect(() => {\n    const connection = createConnection(serverUrl, roomId);\n    connection.connect();\n    return () => {\n      connection.disconnect();\n    };\n  }, [roomId, serverUrl]);\n\n  return (\n    <>\n      <label>\n        Server URL:{' '}\n        <input\n          value={serverUrl}\n          onChange={e => setServerUrl(e.target.value)}\n        />\n      </label>\n      <h1>Welcome to the {roomId} room!</h1>\n    </>\n  );\n}\n\nexport default function App() {\n  const [roomId, setRoomId] = useState('general');\n  const [show, setShow] = useState(false);\n  return (\n    <>\n      <label>\n        Choose the chat room:{' '}\n        <select\n          value={roomId}\n          onChange={e => setRoomId(e.target.value)}\n        >\n          <option value=\"general\">general</option>\n          <option value=\"travel\">travel</option>\n          <option value=\"music\">music</option>\n        </select>\n      </label>\n      <button onClick={() => setShow(!show)}>\n        {show ? 'Close chat' : 'Open chat'}\n      </button>\n      {show && <hr />}\n      {show && <ChatRoom roomId={roomId} />}\n    </>\n  );\n}\n```\n\n```js src/chat.js\nexport function createConnection(serverUrl, roomId) {\n  // 실제 구현은 실제로 서버에 연결됩니다.\n  return {\n    connect() {\n      console.log('✅ Connecting to \"' + roomId + '\" room at ' + serverUrl + '...');\n    },\n    disconnect() {\n      console.log('❌ Disconnected from \"' + roomId + '\" room at ' + serverUrl);\n    }\n  };\n}\n```\n\n```css\ninput { display: block; margin-bottom: 20px; }\nbutton { margin-left: 10px; }\n```\n\n</Sandpack>\n\n<Solution />\n\n#### 전역 브라우저 이벤트 감시하기 {/*listening-to-a-global-browser-event*/}\n\n이 예시에서는 DOM 자체를 외부 시스템으로 사용합니다. 일반적으로 JSX와 함께 이벤트 리스너를 명시하지만 이 예시에서 외부 시스템은 브라우저 DOM 자체입니다. 일반적으로 JSX를 이용해 이벤트 리스너를 지정하지만 이 방식만으로는 전역 window 객체를 감시할 수 없습니다. Effect을 이용해 React를 window 객체와 연결해서 이벤트를 감시할 수 있습니다. `pointermove` 이벤트를 감시할 경우, 커서(또는 손가락)의 위치를 추적하고 빨간 점을 해당 위치로 이동시킬 수 있습니다.\n\n<Sandpack>\n\n```js\nimport { useState, useEffect } from 'react';\n\nexport default function App() {\n  const [position, setPosition] = useState({ x: 0, y: 0 });\n\n  useEffect(() => {\n    function handleMove(e) {\n      setPosition({ x: e.clientX, y: e.clientY });\n    }\n    window.addEventListener('pointermove', handleMove);\n    return () => {\n      window.removeEventListener('pointermove', handleMove);\n    };\n  }, []);\n\n  return (\n    <div style={{\n      position: 'absolute',\n      backgroundColor: 'pink',\n      borderRadius: '50%',\n      opacity: 0.6,\n      transform: `translate(${position.x}px, ${position.y}px)`,\n      pointerEvents: 'none',\n      left: -20,\n      top: -20,\n      width: 40,\n      height: 40,\n    }} />\n  );\n}\n```\n\n```css\nbody {\n  min-height: 300px;\n}\n```\n\n</Sandpack>\n\n<Solution />\n\n#### 애니메이션 동작시키기 {/*triggering-an-animation*/}\n\n이 예시에서 외부 시스템은 `animation.js`파일에 있는 라이브러리입니다. 이 라이브러리는 DOM 노드를 인자로 받는 `FadeInAnimation`라는 자바스크립트 클래스를 제공하며, 이 클래스는 애니메이션을 제어하기 위한 `start()`과 `stop()` 메서드를 노출합니다. 이 컴포넌트는 [ref를 이용하여](/learn/manipulating-the-dom-with-refs) DOM 노드에 접근합니다. Effect는 ref를 통해 DOM 노드를 읽고, 컴포넌트가 나타날 때 해당 노드의 애니메이션을 자동으로 시작시킵니다.\n\n<Sandpack>\n\n```js\nimport { useState, useEffect, useRef } from 'react';\nimport { FadeInAnimation } from './animation.js';\n\nfunction Welcome() {\n  const ref = useRef(null);\n\n  useEffect(() => {\n    const animation = new FadeInAnimation(ref.current);\n    animation.start(1000);\n    return () => {\n      animation.stop();\n    };\n  }, []);\n\n  return (\n    <h1\n      ref={ref}\n      style={{\n        opacity: 0,\n        color: 'white',\n        padding: 50,\n        textAlign: 'center',\n        fontSize: 50,\n        backgroundImage: 'radial-gradient(circle, rgba(63,94,251,1) 0%, rgba(252,70,107,1) 100%)'\n      }}\n    >\n      Welcome\n    </h1>\n  );\n}\n\nexport default function App() {\n  const [show, setShow] = useState(false);\n  return (\n    <>\n      <button onClick={() => setShow(!show)}>\n        {show ? 'Remove' : 'Show'}\n      </button>\n      <hr />\n      {show && <Welcome />}\n    </>\n  );\n}\n```\n\n```js src/animation.js\nexport class FadeInAnimation {\n  constructor(node) {\n    this.node = node;\n  }\n  start(duration) {\n    this.duration = duration;\n    if (this.duration === 0) {\n      // Jump to end immediately\n      this.onProgress(1);\n    } else {\n      this.onProgress(0);\n      // Start animating\n      this.startTime = performance.now();\n      this.frameId = requestAnimationFrame(() => this.onFrame());\n    }\n  }\n  onFrame() {\n    const timePassed = performance.now() - this.startTime;\n    const progress = Math.min(timePassed / this.duration, 1);\n    this.onProgress(progress);\n    if (progress < 1) {\n      // We still have more frames to paint\n      this.frameId = requestAnimationFrame(() => this.onFrame());\n    }\n  }\n  onProgress(progress) {\n    this.node.style.opacity = progress;\n  }\n  stop() {\n    cancelAnimationFrame(this.frameId);\n    this.startTime = null;\n    this.frameId = null;\n    this.duration = 0;\n  }\n}\n```\n\n```css\nlabel, button { display: block; margin-bottom: 20px; }\nhtml, body { min-height: 300px; }\n```\n\n</Sandpack>\n\n<Solution />\n\n#### 모달 대화 상자 제어하기 {/*controlling-a-modal-dialog*/}\n\n이 예시에서 외부 시스템은 브라우저 DOM입니다. `ModalDialog` 컴포넌트는 [`<dialog>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog) 요소를 렌더링합니다. Effect를 사용하여 `isOpen` prop을 [`showModal()`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/showModal)과 [`close()`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/close)메서드 호출에 동기화합니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\nimport ModalDialog from './ModalDialog.js';\n\nexport default function App() {\n  const [show, setShow] = useState(false);\n  return (\n    <>\n      <button onClick={() => setShow(true)}>\n        Open dialog\n      </button>\n      <ModalDialog isOpen={show}>\n        Hello there!\n        <br />\n        <button onClick={() => {\n          setShow(false);\n        }}>Close</button>\n      </ModalDialog>\n    </>\n  );\n}\n```\n\n```js src/ModalDialog.js active\nimport { useEffect, useRef } from 'react';\n\nexport default function ModalDialog({ isOpen, children }) {\n  const ref = useRef();\n\n  useEffect(() => {\n    if (!isOpen) {\n      return;\n    }\n    const dialog = ref.current;\n    dialog.showModal();\n    return () => {\n      dialog.close();\n    };\n  }, [isOpen]);\n\n  return <dialog ref={ref}>{children}</dialog>;\n}\n```\n\n```css\nbody {\n  min-height: 300px;\n}\n```\n\n</Sandpack>\n\n<Solution />\n\n#### 요소의 가시성 추적 {/*tracking-element-visibility*/}\n\n이 예시에서 외부 시스템은 브라우저 DOM입니다. `App` 컴포넌트는 긴 리스트 목록을 표시한 다음 `Box` 컴포넌트를 표시하고 다시 긴 리스트 목록을 표시합니다. 목록을 아래로 스크롤 해보세요. `Box` 컴포넌트 전체가 뷰포트 내에서 완전히 보일 때 배경 색상이 검은색으로 변경되는 것을 확인해 보세요. 이를 구현하기 위해 `Box` 컴포넌트는 [`IntersectionObserver`](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API)를 관리하는 Effect를 사용합니다. 이 브라우저 API는 DOM 요소가 뷰포트 내에서 가시성이 변경될 때를 알려줍니다.\n\n<Sandpack>\n\n```js\nimport Box from './Box.js';\n\nexport default function App() {\n  return (\n    <>\n      <LongSection />\n      <Box />\n      <LongSection />\n      <Box />\n      <LongSection />\n    </>\n  );\n}\n\nfunction LongSection() {\n  const items = [];\n  for (let i = 0; i < 50; i++) {\n    items.push(<li key={i}>Item #{i} (keep scrolling)</li>);\n  }\n  return <ul>{items}</ul>\n}\n```\n\n```js src/Box.js active\nimport { useRef, useEffect } from 'react';\n\nexport default function Box() {\n  const ref = useRef(null);\n\n  useEffect(() => {\n    const div = ref.current;\n    const observer = new IntersectionObserver(entries => {\n      const entry = entries[0];\n      if (entry.isIntersecting) {\n        document.body.style.backgroundColor = 'black';\n        document.body.style.color = 'white';\n      } else {\n        document.body.style.backgroundColor = 'white';\n        document.body.style.color = 'black';\n      }\n    }, {\n       threshold: 1.0\n    });\n    observer.observe(div);\n    return () => {\n      observer.disconnect();\n    }\n  }, []);\n\n  return (\n    <div ref={ref} style={{\n      margin: 20,\n      height: 100,\n      width: 100,\n      border: '2px solid black',\n      backgroundColor: 'blue'\n    }} />\n  );\n}\n```\n\n</Sandpack>\n\n<Solution />\n\n</Recipes>\n\n---\n\n### 커스텀 Hook을 Effect로 감싸기 {/*wrapping-effects-in-custom-hooks*/}\n\nEffect는 [\"탈출구\"](/learn/escape-hatches) 입니다. \"React 바깥으로 나가야 할 때\"와 유즈케이스에 필요한 빌트인 솔루션이 없을 때 사용합니다. 만약 Effect를 자주 작성해야 한다면 컴포넌트가 의존하고 있는 공통적인 동작들을 [커스텀 Hook](/learn/reusing-logic-with-custom-hooks)으로 추출해야 한다는 신호일 수 있습니다.\n\n예시로 아래의 `useChatRoom` 커스텀 Hook은 Effect의 로직을 조금 더 선언적인 API로 보일 수 있도록 숨겨줍니다.\n\n```js {1,11}\nfunction useChatRoom({ serverUrl, roomId }) {\n  useEffect(() => {\n    const options = {\n      serverUrl: serverUrl,\n      roomId: roomId\n    };\n    const connection = createConnection(options);\n    connection.connect();\n    return () => connection.disconnect();\n  }, [roomId, serverUrl]);\n}\n```\n\n이제 이 커스텀 Hook을 어떤 컴포넌트에서도 이용할 수 있습니다.\n\n```js {4-7}\nfunction ChatRoom({ roomId }) {\n  const [serverUrl, setServerUrl] = useState('https://localhost:1234');\n\n  useChatRoom({\n    roomId: roomId,\n    serverUrl: serverUrl\n  });\n  // ...\n```\n\n\n또한 React 생태계에는 각종 목적에 맞는 훌륭한 커스텀 Hook들도 많이 존재합니다.\n\n[이 링크를 통해 커스텀 Hook에 대해 더 많이 공부해보세요.](/learn/reusing-logic-with-custom-hooks)\n\n<Recipes titleText=\"커스텀 Hook에서 Effect를 활용하는 예시\" titleId=\"examples-custom-hooks\">\n\n#### 커스텀 `useChatRoom` Hook {/*custom-usechatroom-hook*/}\n\n이 예시는 [이전 예시](#examples-connecting) 중 하나와 동일하지만 로직이 커스텀 Hook으로 추출되었습니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\nimport { useChatRoom } from './useChatRoom.js';\n\nfunction ChatRoom({ roomId }) {\n  const [serverUrl, setServerUrl] = useState('https://localhost:1234');\n\n  useChatRoom({\n    roomId: roomId,\n    serverUrl: serverUrl\n  });\n\n  return (\n    <>\n      <label>\n        Server URL:{' '}\n        <input\n          value={serverUrl}\n          onChange={e => setServerUrl(e.target.value)}\n        />\n      </label>\n      <h1>Welcome to the {roomId} room!</h1>\n    </>\n  );\n}\n\nexport default function App() {\n  const [roomId, setRoomId] = useState('general');\n  const [show, setShow] = useState(false);\n  return (\n    <>\n      <label>\n        Choose the chat room:{' '}\n        <select\n          value={roomId}\n          onChange={e => setRoomId(e.target.value)}\n        >\n          <option value=\"general\">general</option>\n          <option value=\"travel\">travel</option>\n          <option value=\"music\">music</option>\n        </select>\n      </label>\n      <button onClick={() => setShow(!show)}>\n        {show ? 'Close chat' : 'Open chat'}\n      </button>\n      {show && <hr />}\n      {show && <ChatRoom roomId={roomId} />}\n    </>\n  );\n}\n```\n\n```js src/useChatRoom.js\nimport { useEffect } from 'react';\nimport { createConnection } from './chat.js';\n\nexport function useChatRoom({ serverUrl, roomId }) {\n  useEffect(() => {\n    const connection = createConnection(serverUrl, roomId);\n    connection.connect();\n    return () => {\n      connection.disconnect();\n    };\n  }, [roomId, serverUrl]);\n}\n```\n\n```js src/chat.js\nexport function createConnection(serverUrl, roomId) {\n  // 실제 구현은 실제로 서버에 연결됩니다.\n  return {\n    connect() {\n      console.log('✅ Connecting to \"' + roomId + '\" room at ' + serverUrl + '...');\n    },\n    disconnect() {\n      console.log('❌ Disconnected from \"' + roomId + '\" room at ' + serverUrl);\n    }\n  };\n}\n```\n\n```css\ninput { display: block; margin-bottom: 20px; }\nbutton { margin-left: 10px; }\n```\n\n</Sandpack>\n\n<Solution />\n\n#### 커스텀 `useWindowListener` Hook {/*custom-usewindowlistener-hook*/}\n\n이 예시는 [이전 예시](#examples-connecting) 중 하나와 동일하지만 로직이 커스텀 Hook으로 추출되었습니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\nimport { useWindowListener } from './useWindowListener.js';\n\nexport default function App() {\n  const [position, setPosition] = useState({ x: 0, y: 0 });\n\n  useWindowListener('pointermove', (e) => {\n    setPosition({ x: e.clientX, y: e.clientY });\n  });\n\n  return (\n    <div style={{\n      position: 'absolute',\n      backgroundColor: 'pink',\n      borderRadius: '50%',\n      opacity: 0.6,\n      transform: `translate(${position.x}px, ${position.y}px)`,\n      pointerEvents: 'none',\n      left: -20,\n      top: -20,\n      width: 40,\n      height: 40,\n    }} />\n  );\n}\n```\n\n```js src/useWindowListener.js\nimport { useState, useEffect } from 'react';\n\nexport function useWindowListener(eventType, listener) {\n  useEffect(() => {\n    window.addEventListener(eventType, listener);\n    return () => {\n      window.removeEventListener(eventType, listener);\n    };\n  }, [eventType, listener]);\n}\n```\n\n```css\nbody {\n  min-height: 300px;\n}\n```\n\n</Sandpack>\n\n<Solution />\n\n#### 커스텀 `useIntersectionObserver` Hook {/*custom-useintersectionobserver-hook*/}\n\n이 예시는 [이전 예시](#examples-connecting) 중 하나와 동일하지만 로직이 부분적으로 커스텀 Hook으로 추출되었습니다.\n\n<Sandpack>\n\n```js\nimport Box from './Box.js';\n\nexport default function App() {\n  return (\n    <>\n      <LongSection />\n      <Box />\n      <LongSection />\n      <Box />\n      <LongSection />\n    </>\n  );\n}\n\nfunction LongSection() {\n  const items = [];\n  for (let i = 0; i < 50; i++) {\n    items.push(<li key={i}>Item #{i} (keep scrolling)</li>);\n  }\n  return <ul>{items}</ul>\n}\n```\n\n```js src/Box.js active\nimport { useRef, useEffect } from 'react';\nimport { useIntersectionObserver } from './useIntersectionObserver.js';\n\nexport default function Box() {\n  const ref = useRef(null);\n  const isIntersecting = useIntersectionObserver(ref);\n\n  useEffect(() => {\n   if (isIntersecting) {\n      document.body.style.backgroundColor = 'black';\n      document.body.style.color = 'white';\n    } else {\n      document.body.style.backgroundColor = 'white';\n      document.body.style.color = 'black';\n    }\n  }, [isIntersecting]);\n\n  return (\n    <div ref={ref} style={{\n      margin: 20,\n      height: 100,\n      width: 100,\n      border: '2px solid black',\n      backgroundColor: 'blue'\n    }} />\n  );\n}\n```\n\n```js src/useIntersectionObserver.js\nimport { useState, useEffect } from 'react';\n\nexport function useIntersectionObserver(ref) {\n  const [isIntersecting, setIsIntersecting] = useState(false);\n\n  useEffect(() => {\n    const div = ref.current;\n    const observer = new IntersectionObserver(entries => {\n      const entry = entries[0];\n      setIsIntersecting(entry.isIntersecting);\n    }, {\n       threshold: 1.0\n    });\n    observer.observe(div);\n    return () => {\n      observer.disconnect();\n    }\n  }, [ref]);\n\n  return isIntersecting;\n}\n```\n\n</Sandpack>\n\n<Solution />\n\n</Recipes>\n\n---\n\n### React로 작성되지 않은 위젯 제어하기 {/*controlling-a-non-react-widget*/}\n\n가끔은 컴포넌트의 prop 또는 state를 외부 시스템과 동기화해야할 때가 있습니다.\n\n예를 들어 React 없이 작성된 서드 파티 지도 위젯이나 비디오 플레이어 컴포넌트가 있다면 이 컴포넌트의 state를 현재 React 컴포넌트의 state와 일치하도록 하기 위해 Effect를 사용할 수 있습니다. 이 Effect는 `map-widget.js`에 정의된 `MapWidget` 클래스의 인스턴스를 생성합니다. `Map` 컴포넌트의 `zoomLevel` prop을 변경할 때, Effect는 해당 클래스 인스턴스의 `setZoom()`을 호출하여 동기화를 유지합니다.\n\n<Sandpack>\n\n```json package.json hidden\n{\n  \"dependencies\": {\n    \"leaflet\": \"1.9.1\",\n    \"react\": \"latest\",\n    \"react-dom\": \"latest\",\n    \"react-scripts\": \"latest\",\n    \"remarkable\": \"2.0.1\"\n  },\n  \"scripts\": {\n    \"start\": \"react-scripts start\",\n    \"build\": \"react-scripts build\",\n    \"test\": \"react-scripts test --env=jsdom\",\n    \"eject\": \"react-scripts eject\"\n  }\n}\n```\n\n```js src/App.js\nimport { useState } from 'react';\nimport Map from './Map.js';\n\nexport default function App() {\n  const [zoomLevel, setZoomLevel] = useState(0);\n  return (\n    <>\n      Zoom level: {zoomLevel}x\n      <button onClick={() => setZoomLevel(zoomLevel + 1)}>+</button>\n      <button onClick={() => setZoomLevel(zoomLevel - 1)}>-</button>\n      <hr />\n      <Map zoomLevel={zoomLevel} />\n    </>\n  );\n}\n```\n\n```js src/Map.js active\nimport { useRef, useEffect } from 'react';\nimport { MapWidget } from './map-widget.js';\n\nexport default function Map({ zoomLevel }) {\n  const containerRef = useRef(null);\n  const mapRef = useRef(null);\n\n  useEffect(() => {\n    if (mapRef.current === null) {\n      mapRef.current = new MapWidget(containerRef.current);\n    }\n\n    const map = mapRef.current;\n    map.setZoom(zoomLevel);\n  }, [zoomLevel]);\n\n  return (\n    <div\n      style={{ width: 200, height: 200 }}\n      ref={containerRef}\n    />\n  );\n}\n```\n\n```js src/map-widget.js\nimport 'leaflet/dist/leaflet.css';\nimport * as L from 'leaflet';\n\nexport class MapWidget {\n  constructor(domNode) {\n    this.map = L.map(domNode, {\n      zoomControl: false,\n      doubleClickZoom: false,\n      boxZoom: false,\n      keyboard: false,\n      scrollWheelZoom: false,\n      zoomAnimation: false,\n      touchZoom: false,\n      zoomSnap: 0.1\n    });\n    L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {\n      maxZoom: 19,\n      attribution: '© OpenStreetMap'\n    }).addTo(this.map);\n    this.map.setView([0, 0], 0);\n  }\n  setZoom(level) {\n    this.map.setZoom(level);\n  }\n}\n```\n\n```css\nbutton { margin: 5px; }\n```\n\n</Sandpack>\n\n이 예시에서는 정리 함수가 필요하지 않습니다. 이는 `MapWidget` 클래스가 클래스에 전달된 DOM 노드만 관리하기 때문입니다. `Map` 컴포넌트가 트리에서 제거된 후, 브라우저의 자바스크립트 엔진에 의해 DOM 노드와 `MapWidget` 클래스 인스턴스 모두가 자동으로 가비지 컬렉션에 의해 정리됩니다.\n\n---\n\n### Effect를 이용한 데이터 페칭 {/*fetching-data-with-effects*/}\n\nYou can use an Effect to fetch data for your component. Note that [if you use a framework,](/learn/creating-a-react-app#full-stack-frameworks) using your framework's data fetching mechanism will be a lot more efficient than writing Effects manually.\n\n만약 직접 Effect를 작성하여 데이터를 페칭하고 싶다면, 코드는 다음과 같을 수 있습니다.\n\n```js\nimport { useState, useEffect } from 'react';\nimport { fetchBio } from './api.js';\n\nexport default function Page() {\n  const [person, setPerson] = useState('Alice');\n  const [bio, setBio] = useState(null);\n\n  useEffect(() => {\n    let ignore = false;\n    setBio(null);\n    fetchBio(person).then(result => {\n      if (!ignore) {\n        setBio(result);\n      }\n    });\n    return () => {\n      ignore = true;\n    };\n  }, [person]);\n\n  // ...\n```\n\n`ignore` 변수의 초기값이 `false`로 설정되고 정리 함수 동작 중에 `true`로 설정되는 것에 주목하세요. 이 로직은 [코드가 \"경쟁 상태(race conditions)\"에 빠지지 않도록 보장해 줍니다.](https://maxrozen.com/race-conditions-fetching-data-react-with-useeffect) 네트워크 요청을 보낸 순서와 응답을 받는 순서가 다르게 동작할 수 있기 때문에 이러한 처리가 필요합니다.\n\n<Sandpack>\n\n{/* TODO(@poteto) - investigate potential false positives in react compiler validation */}\n```js src/App.js\nimport { useState, useEffect } from 'react';\nimport { fetchBio } from './api.js';\n\nexport default function Page() {\n  const [person, setPerson] = useState('Alice');\n  const [bio, setBio] = useState(null);\n  useEffect(() => {\n    let ignore = false;\n    setBio(null);\n    fetchBio(person).then(result => {\n      if (!ignore) {\n        setBio(result);\n      }\n    });\n    return () => {\n      ignore = true;\n    }\n  }, [person]);\n\n  return (\n    <>\n      <select value={person} onChange={e => {\n        setPerson(e.target.value);\n      }}>\n        <option value=\"Alice\">Alice</option>\n        <option value=\"Bob\">Bob</option>\n        <option value=\"Taylor\">Taylor</option>\n      </select>\n      <hr />\n      <p><i>{bio ?? 'Loading...'}</i></p>\n    </>\n  );\n}\n```\n\n```js src/api.js hidden\nexport async function fetchBio(person) {\n  const delay = person === 'Bob' ? 2000 : 200;\n  return new Promise(resolve => {\n    setTimeout(() => {\n      resolve('This is ' + person + '’s bio.');\n    }, delay);\n  })\n}\n```\n\n</Sandpack>\n\n또한 [`async` / `await`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function) 구문을 사용하여 코드를 다시 작성할 수 있지만 여전히 정리 함수를 제공해야 합니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { useState, useEffect } from 'react';\nimport { fetchBio } from './api.js';\n\nexport default function Page() {\n  const [person, setPerson] = useState('Alice');\n  const [bio, setBio] = useState(null);\n  useEffect(() => {\n    async function startFetching() {\n      setBio(null);\n      const result = await fetchBio(person);\n      if (!ignore) {\n        setBio(result);\n      }\n    }\n\n    let ignore = false;\n    startFetching();\n    return () => {\n      ignore = true;\n    }\n  }, [person]);\n\n  return (\n    <>\n      <select value={person} onChange={e => {\n        setPerson(e.target.value);\n      }}>\n        <option value=\"Alice\">Alice</option>\n        <option value=\"Bob\">Bob</option>\n        <option value=\"Taylor\">Taylor</option>\n      </select>\n      <hr />\n      <p><i>{bio ?? 'Loading...'}</i></p>\n    </>\n  );\n}\n```\n\n```js src/api.js hidden\nexport async function fetchBio(person) {\n  const delay = person === 'Bob' ? 2000 : 200;\n  return new Promise(resolve => {\n    setTimeout(() => {\n      resolve('This is ' + person + '’s bio.');\n    }, delay);\n  })\n}\n```\n\n</Sandpack>\n\nEffect에서 직접 데이터 페칭 로직을 작성하면 나중에 캐싱 기능이나 서버 렌더링과 같은 최적화를 추가하기 어려워집니다. [자체 제작된 커스텀 Hook이나 커뮤니티에 의해 유지보수되는 Hook을 사용하는 편이 더 간단합니다.](/learn/reusing-logic-with-custom-hooks#when-to-use-custom-hooks)\n\n<DeepDive>\n\n#### Effect에서 데이터를 페칭하는 좋은 대안은 무엇인가요? {/*what-are-good-alternatives-to-data-fetching-in-effects*/}\n\nEffect 내부에서 `fetch` 호출을 작성하는 것은 클라이언트 사이드 앱에서 데이터를 페칭하는 [가장 인기 있는 방법입니다.](https://www.robinwieruch.de/react-hooks-fetch-data/) 하지만 이것은 매우 수동적인 접근 방식이며 큰 단점이 있습니다.\n\n- **Effect는 서버에서는 실행되지 않습니다.** 이는 초기 서버 렌더링 된 HTML이 데이터가 없는 state만을 포함한다는 것을 의미합니다. 클라이언트 컴퓨터는 모든 자바스크립트를 다운로드 받고 앱을 렌더링한 다음 데이터를 로드합니다. 이는 효율적이지 않을 수 있습니다.\n- **Effect 내부에서 직접 페칭을 하는 것은 네트워크 폭포(network waterfalls)가 생성되기 쉽게 합니다.** 부모 컴포넌트 렌더링 후 일부 데이터를 페칭하고 나서 자식 컴포넌트가 렌더링 됩니다. 이후 자식 컴포넌트가 자신의 데이터를 페칭하기 시작합니다. 네트워크의 속도가 빠르지 않다면 이 방법은 모든 데이터를 병렬로 페칭하는 것보다 훨씬 느립니다.\n- **Effect 내부에서 직접 데이터를 페칭하는 것은 일반적으로 데이터를 미리 로드하거나 캐싱하지 않는다는 것을 의미합니다.** 예를 들어 컴포넌트가 마운트 해제되고 다시 마운트되었을 때 데이터를 다시 가져와야 합니다.\n- **사용하기 매우 불편한 방법입니다.** [경쟁 조건](https://maxrozen.com/race-conditions-fetching-data-react-with-useeffect)과 같은 버그를 발생시키지 않도록 fetch 호출을 작성할 때 상당한 양의 보일러 플레이트 코드가 필요합니다.\n\n이러한 단점은 React만 해당되는 것이 아닙니다. 다른 라이브러리를 사용하여 데이터를 페칭할 때도 해당됩니다. 라우팅과 마찬가지로 데이터 페칭은 세부적인 사항이 많으므로 다음과 같은 접근 방식을 권장합니다.\n\n- **If you use a [framework](/learn/creating-a-react-app#full-stack-frameworks), use its built-in data fetching mechanism.** Modern React frameworks have integrated data fetching mechanisms that are efficient and don't suffer from the above pitfalls.\n- **Otherwise, consider using or building a client-side cache.** Popular open source solutions include [TanStack Query](https://tanstack.com/query/latest/), [useSWR](https://swr.vercel.app/), and [React Router 6.4+.](https://beta.reactrouter.com/en/main/start/overview) You can build your own solution too, in which case you would use Effects under the hood but also add logic for deduplicating requests, caching responses, and avoiding network waterfalls (by preloading data or hoisting data requirements to routes).\n\n만약 이러한 접근 방식이 적합하지 않다면 Effect 내부에서 데이터를 페칭하는 것을 계속 진행할 수 있습니다.\n\n</DeepDive>\n\n---\n\n### 반응형값 의존성 지정 {/*specifying-reactive-dependencies*/}\n\n**Effect의 의존성을 \"선택\"할 수 없다는 점에 유의하세요.** Effect 코드에서 사용하는 모든 <CodeStep step={2}>반응형 값</CodeStep>은 의존성으로 선언되어야 합니다. Effect의 의존성 배열은 코드에 의해 결정됩니다.\n\n```js [[2, 1, \"roomId\"], [2, 2, \"serverUrl\"], [2, 5, \"serverUrl\"], [2, 5, \"roomId\"], [2, 8, \"serverUrl\"], [2, 8, \"roomId\"]]\nfunction ChatRoom({ roomId }) { // 이것은 반응형 값입니다\n  const [serverUrl, setServerUrl] = useState('https://localhost:1234'); // 이것도 반응형 값입니다\n\n  useEffect(() => {\n    const connection = createConnection(serverUrl, roomId); // 이 Effect는 이 반응형 값들을 읽습니다\n    connection.connect();\n    return () => connection.disconnect();\n  }, [serverUrl, roomId]); // ✅ 그래서 이 값들을 Effect의 의존성으로 지정해야 합니다\n  // ...\n}\n```\n\n`serverUrl` 또는 `roomId`가 변경될 때마다 Effect는 새로운 값을 이용해 채팅을 다시 연결할 것입니다.\n\n**[반응형 값](/learn/lifecycle-of-reactive-effects#effects-react-to-reactive-values)** 에는 props와 컴포넌트 내부에 선언된 모든 변수나 함수들이 포함됩니다. `roomId`와 `serverUrl`은 반응형 값이므로 이들을 의존성에서 제거하면 안 됩니다. 이들을 누락했을 때 [린터가 React 환경에 맞게 설정되어 있었다면](/learn/editor-setup#linting) 린터는 이것을 수정해야 하는 실수로 표시합니다.\n\n```js {8}\nfunction ChatRoom({ roomId }) {\n  const [serverUrl, setServerUrl] = useState('https://localhost:1234');\n\n  useEffect(() => {\n    const connection = createConnection(serverUrl, roomId);\n    connection.connect();\n    return () => connection.disconnect();\n  }, []); // 🔴 React Hook useEffect has missing dependencies: 'roomId' and 'serverUrl'\n  // ...\n}\n```\n\n**의존성을 제거하려면 [그것이 의존성이 되지 않아야 함을 린터에 증명해야 합니다.](/learn/removing-effect-dependencies#removing-unnecessary-dependencies)** 예를 들어, `serverUrl` 을 컴포넌트 밖으로 이동하여 그것이 반응적이지 않고 리렌더링될 때 변경되지 않을 것임을 증명할 수 있습니다.\n\n```js {1,8}\nconst serverUrl = 'https://localhost:1234'; // 더 이상 반응형 값이 아님\n\nfunction ChatRoom({ roomId }) {\n  useEffect(() => {\n    const connection = createConnection(serverUrl, roomId);\n    connection.connect();\n    return () => connection.disconnect();\n  }, [roomId]); // ✅ 모든 의존성이 선언됨\n  // ...\n}\n```\n\n이제 `serverUrl`은 반응형 값이 아니며 (리렌더링될 때 변경되지 않을 것이므로), 의존성에 추가할 필요가 없습니다. **Effect의 코드가 어떤 반응형 값도 사용하지 않는다면 그 의존성 목록은 비어있어야 합니다. (`[]`)**\n\n```js {1,2,9}\nconst serverUrl = 'https://localhost:1234'; // 더 이상 반응형 값이 아님\nconst roomId = 'music'; // 더 이상 반응형 값이 아님\n\nfunction ChatRoom() {\n  useEffect(() => {\n    const connection = createConnection(serverUrl, roomId);\n    connection.connect();\n    return () => connection.disconnect();\n  }, []); // ✅ 모든 의존성이 선언됨\n  // ...\n}\n```\n\n[의존성이 비어있는 Effect](/learn/lifecycle-of-reactive-effects#what-an-effect-with-empty-dependencies-means)는 컴포넌트의 props나 state가 변경되도 다시 실행되지 않습니다.\n\n<Pitfall>\n\n기존의 코드 베이스에서 아래와 같이 린터를 억제하고 있는 일부 Effect가 있을 수 있습니다.\n\n```js {3-4}\nuseEffect(() => {\n  // ...\n  // 🔴 Avoid suppressing the linter like this:\n  // eslint-ignore-next-line react-hooks/exhaustive-deps\n}, []);\n```\n\n**의존성이 코드와 일치하지 않을 때 버그가 도입될 위험이 큽니다.** 린터를 억제함으로써 Effect가 의존하는 값에 대해 React가 '거짓말'을 하게 됩니다. 린터를 속이는 대신 [이러한 값들이 불필요하다는 것을 증명하세요.](/learn/removing-effect-dependencies#removing-unnecessary-dependencies)\n\n</Pitfall>\n\n<Recipes titleText=\"반응형 값을 의존성으로 추가하는 예시\" titleId=\"examples-dependencies\">\n\n#### 의존성 배열 전달 {/*passing-a-dependency-array*/}\n\n의존성을 명시하면 Effect는 **초기 렌더링 후 _그리고_ 의존성 값 변경과 함께 리렌더링이 된 후 동작합니다.**\n\n```js {3}\nuseEffect(() => {\n  // ...\n}, [a, b]); // a나 b가 다르면 다시 실행됨\n```\n\n아래 예시에서는 `serverUrl`와 `roomId`은 [반응형 값](/learn/lifecycle-of-reactive-effects#effects-react-to-reactive-values)이므로 둘 다 의존성으로 지정해야 합니다. 결과적으로 드롭다운에서 다른 방을 선택하거나 서버 URL 입력을 편집하면 채팅이 다시 연결됩니다. 그러나 `message`는 Effect에서 사용되지 않으므로(의존성이 아니므로), 메세지를 편집해도 대화가 다시 연결되지 않습니다.\n\n<Sandpack>\n\n```js\nimport { useState, useEffect } from 'react';\nimport { createConnection } from './chat.js';\n\nfunction ChatRoom({ roomId }) {\n  const [serverUrl, setServerUrl] = useState('https://localhost:1234');\n  const [message, setMessage] = useState('');\n\n  useEffect(() => {\n    const connection = createConnection(serverUrl, roomId);\n    connection.connect();\n    return () => {\n      connection.disconnect();\n    };\n  }, [serverUrl, roomId]);\n\n  return (\n    <>\n      <label>\n        Server URL:{' '}\n        <input\n          value={serverUrl}\n          onChange={e => setServerUrl(e.target.value)}\n        />\n      </label>\n      <h1>Welcome to the {roomId} room!</h1>\n      <label>\n        Your message:{' '}\n        <input value={message} onChange={e => setMessage(e.target.value)} />\n      </label>\n    </>\n  );\n}\n\nexport default function App() {\n  const [show, setShow] = useState(false);\n  const [roomId, setRoomId] = useState('general');\n  return (\n    <>\n      <label>\n        Choose the chat room:{' '}\n        <select\n          value={roomId}\n          onChange={e => setRoomId(e.target.value)}\n        >\n          <option value=\"general\">general</option>\n          <option value=\"travel\">travel</option>\n          <option value=\"music\">music</option>\n        </select>\n        <button onClick={() => setShow(!show)}>\n          {show ? 'Close chat' : 'Open chat'}\n        </button>\n      </label>\n      {show && <hr />}\n      {show && <ChatRoom roomId={roomId}/>}\n    </>\n  );\n}\n```\n\n```js src/chat.js\nexport function createConnection(serverUrl, roomId) {\n  // 실제 구현은 실제로 서버에 연결됩니다.\n  return {\n    connect() {\n      console.log('✅ Connecting to \"' + roomId + '\" room at ' + serverUrl + '...');\n    },\n    disconnect() {\n      console.log('❌ Disconnected from \"' + roomId + '\" room at ' + serverUrl);\n    }\n  };\n}\n```\n\n```css\ninput { margin-bottom: 10px; }\nbutton { margin-left: 5px; }\n```\n\n</Sandpack>\n\n<Solution />\n\n#### 빈 의존성 배열 전달 {/*passing-an-empty-dependency-array*/}\n\n만약 Effect가 정말 어떤 반응형 값도 사용하지 않는다면 그것은 **초기 렌더링 이후 한번만 실행됩니다.**\n\n```js {3}\nuseEffect(() => {\n  // ...\n}, []); // 다시 실행되지 않음 (개발 환경에서만 한번 실행)\n```\n\n**개발 환경에서는 빈 의존성 배열이 있더라도 버그를 찾기 위해 설정과 정리가 [한번 더 실행됩니다.](/learn/synchronizing-with-effects#how-to-handle-the-effect-firing-twice-in-development)**\n\n이 예시에서 `serverUrl`와 `roomId`는 모두 하드코딩되어 있습니다. 컴포넌트 외부에서 선언되었으므로 반응형 값이 아니며, 따라서 의존성이 아닙니다. 의존성 배열이 비어있으므로 Effect는 리렌더링될 때까지 실행되지 않습니다.\n\n<Sandpack>\n\n```js\nimport { useState, useEffect } from 'react';\nimport { createConnection } from './chat.js';\n\nconst serverUrl = 'https://localhost:1234';\nconst roomId = 'music';\n\nfunction ChatRoom() {\n  const [message, setMessage] = useState('');\n\n  useEffect(() => {\n    const connection = createConnection(serverUrl, roomId);\n    connection.connect();\n    return () => connection.disconnect();\n  }, []);\n\n  return (\n    <>\n      <h1>Welcome to the {roomId} room!</h1>\n      <label>\n        Your message:{' '}\n        <input value={message} onChange={e => setMessage(e.target.value)} />\n      </label>\n    </>\n  );\n}\n\nexport default function App() {\n  const [show, setShow] = useState(false);\n  return (\n    <>\n      <button onClick={() => setShow(!show)}>\n        {show ? 'Close chat' : 'Open chat'}\n      </button>\n      {show && <hr />}\n      {show && <ChatRoom />}\n    </>\n  );\n}\n```\n\n```js src/chat.js\nexport function createConnection(serverUrl, roomId) {\n  // 실제 구현은 실제로 서버에 연결됩니다.\n  return {\n    connect() {\n      console.log('✅ Connecting to \"' + roomId + '\" room at ' + serverUrl + '...');\n    },\n    disconnect() {\n      console.log('❌ Disconnected from \"' + roomId + '\" room at ' + serverUrl);\n    }\n  };\n}\n```\n\n</Sandpack>\n\n<Solution />\n\n\n#### 의존성 배열을 전달하지 않았을 때 {/*passing-no-dependency-array-at-all*/}\n\n의존성 배열을 아예 사용하지 않을 경우, Effect는 컴포넌트의 모든 렌더링과 리렌더링마다 동작합니다.\n\n```js {3}\nuseEffect(() => {\n  // ...\n}); // 항상 다시 실행됨\n```\n\n이 예시에서 Effect는 `serverUrl`과 `roomId`를 변경할 때 다시 실행하는 것은 합리적입니다. 그러나 `message`를 변경할때도 다시 실행되므로 바람직하지 않습니다. 보통은 이런 이슈를 방지하고자 의존성 배열을 명시합니다.\n\n<Sandpack>\n\n```js\nimport { useState, useEffect } from 'react';\nimport { createConnection } from './chat.js';\n\nfunction ChatRoom({ roomId }) {\n  const [serverUrl, setServerUrl] = useState('https://localhost:1234');\n  const [message, setMessage] = useState('');\n\n  useEffect(() => {\n    const connection = createConnection(serverUrl, roomId);\n    connection.connect();\n    return () => {\n      connection.disconnect();\n    };\n  }); // 의존성 배열이 아예 없음\n\n  return (\n    <>\n      <label>\n        Server URL:{' '}\n        <input\n          value={serverUrl}\n          onChange={e => setServerUrl(e.target.value)}\n        />\n      </label>\n      <h1>Welcome to the {roomId} room!</h1>\n      <label>\n        Your message:{' '}\n        <input value={message} onChange={e => setMessage(e.target.value)} />\n      </label>\n    </>\n  );\n}\n\nexport default function App() {\n  const [show, setShow] = useState(false);\n  const [roomId, setRoomId] = useState('general');\n  return (\n    <>\n      <label>\n        Choose the chat room:{' '}\n        <select\n          value={roomId}\n          onChange={e => setRoomId(e.target.value)}\n        >\n          <option value=\"general\">general</option>\n          <option value=\"travel\">travel</option>\n          <option value=\"music\">music</option>\n        </select>\n        <button onClick={() => setShow(!show)}>\n          {show ? 'Close chat' : 'Open chat'}\n        </button>\n      </label>\n      {show && <hr />}\n      {show && <ChatRoom roomId={roomId}/>}\n    </>\n  );\n}\n```\n\n```js src/chat.js\nexport function createConnection(serverUrl, roomId) {\n  // 실제 구현은 실제로 서버에 연결됩니다.\n  return {\n    connect() {\n      console.log('✅ Connecting to \"' + roomId + '\" room at ' + serverUrl + '...');\n    },\n    disconnect() {\n      console.log('❌ Disconnected from \"' + roomId + '\" room at ' + serverUrl);\n    }\n  };\n}\n```\n\n```css\ninput { margin-bottom: 10px; }\nbutton { margin-left: 5px; }\n```\n\n</Sandpack>\n\n<Solution />\n\n</Recipes>\n\n---\n\n### Effect에서 이전 state를 기반으로 state 업데이트하기 {/*updating-state-based-on-previous-state-from-an-effect*/}\n\nEffect에서 이전 state를 기반으로 state를 업데이트하려면 문제가 발생할 수 있습니다.\n\n```js {6,9}\nfunction Counter() {\n  const [count, setCount] = useState(0);\n\n  useEffect(() => {\n    const intervalId = setInterval(() => {\n      setCount(count + 1); // 초마다 카운터를 증가시키고 싶습니다...\n    }, 1000)\n    return () => clearInterval(intervalId);\n  }, [count]); // 🚩 ... 하지만 'count'를 의존성으로 명시하면 항상 인터벌이 초기화됩니다.\n  // ...\n}\n```\n\n`count`가 반응형 값이므로 반드시 의존성 배열에 추가해야 합니다. 그러나 `count`가 변경되는 것은 Effect가 정리된 후 다시 설정되는 것을 야기하므로 `count`는 계속 증가할 것입니다. 이상적이지 않은 방식입니다.\n\n이러한 현상을 방지하려면 [`c => c + 1` state 변경함수](/reference/react/useState#updating-state-based-on-the-previous-state)를 `setCount`에 추가하세요.\n\n<Sandpack>\n\n```js\nimport { useState, useEffect } from 'react';\n\nexport default function Counter() {\n  const [count, setCount] = useState(0);\n\n  useEffect(() => {\n    const intervalId = setInterval(() => {\n      setCount(c => c + 1); // ✅ State 업데이터를 전달\n    }, 1000);\n    return () => clearInterval(intervalId);\n  }, []); // ✅ 이제 count는 의존성이 아닙니다\n\n  return <h1>{count}</h1>;\n}\n```\n\n```css\nlabel {\n  display: block;\n  margin-top: 20px;\n  margin-bottom: 20px;\n}\n\nbody {\n  min-height: 150px;\n}\n```\n\n</Sandpack>\n\n`c => c + 1`을 `count + 1` 대신 전달하고 있으므로, [Effect는 더 이상 `count`에 의존하지 않습니다.](/learn/removing-effect-dependencies#are-you-reading-some-state-to-calculate-the-next-state) 이 수정으로 인해 `count`가 변경될 때마다 Effect가 정리 및 설정을 다시 실행할 필요가 없게 됩니다.\n\n---\n\n\n### 불필요한 객체 의존성 제거하기 {/*removing-unnecessary-object-dependencies*/}\n\nEffect가 렌더링 중에 생성된 객체나 함수에 의존하는 경우, 너무 자주 실행될 수 있습니다. 예를 들어 이 Effect는 매 렌더링 후에 다시 연결됩니다. 이는 [렌더링마다 `options` 객체가 다르기 때문입니다.](/learn/removing-effect-dependencies#does-some-reactive-value-change-unintentionally)\n\n```js {6-9,12,15}\nconst serverUrl = 'https://localhost:1234';\n\nfunction ChatRoom({ roomId }) {\n  const [message, setMessage] = useState('');\n\n  const options = { // 🚩 이 객체는 재 렌더링 될 때마다 새로 생성됩니다\n    serverUrl: serverUrl,\n    roomId: roomId\n  };\n\n  useEffect(() => {\n    const connection = createConnection(options); // 객체가 Effect 안에서 사용됩니다\n    connection.connect();\n    return () => connection.disconnect();\n  }, [options]); // 🚩 결과적으로, 의존성이 재 렌더링 때마다 다릅니다\n  // ...\n```\n\n렌더링 중에 생성된 객체를 의존성으로 사용하는 것을 피하세요. 대신 객체를 Effect 내에서 생성하세요.\n\n<Sandpack>\n\n```js\nimport { useState, useEffect } from 'react';\nimport { createConnection } from './chat.js';\n\nconst serverUrl = 'https://localhost:1234';\n\nfunction ChatRoom({ roomId }) {\n  const [message, setMessage] = useState('');\n\n  useEffect(() => {\n    const options = {\n      serverUrl: serverUrl,\n      roomId: roomId\n    };\n    const connection = createConnection(options);\n    connection.connect();\n    return () => connection.disconnect();\n  }, [roomId]);\n\n  return (\n    <>\n      <h1>Welcome to the {roomId} room!</h1>\n      <input value={message} onChange={e => setMessage(e.target.value)} />\n    </>\n  );\n}\n\nexport default function App() {\n  const [roomId, setRoomId] = useState('general');\n  return (\n    <>\n      <label>\n        Choose the chat room:{' '}\n        <select\n          value={roomId}\n          onChange={e => setRoomId(e.target.value)}\n        >\n          <option value=\"general\">general</option>\n          <option value=\"travel\">travel</option>\n          <option value=\"music\">music</option>\n        </select>\n      </label>\n      <hr />\n      <ChatRoom roomId={roomId} />\n    </>\n  );\n}\n```\n\n```js src/chat.js\nexport function createConnection({ serverUrl, roomId }) {\n  // 실제 구현은 실제로 서버에 연결됩니다.\n  return {\n    connect() {\n      console.log('✅ Connecting to \"' + roomId + '\" room at ' + serverUrl + '...');\n    },\n    disconnect() {\n      console.log('❌ Disconnected from \"' + roomId + '\" room at ' + serverUrl);\n    }\n  };\n}\n```\n\n```css\ninput { display: block; margin-bottom: 20px; }\nbutton { margin-left: 10px; }\n```\n\n</Sandpack>\n\n이제 `options` 객체를 Effect 내에서 생성하면, Effect 자체는 roomId 문자열에만 의존합니다.\n\n이 수정으로 입력란에 텍스트를 입력하더라도 채팅이 다시 연결되지 않습니다. 객체와는 달리 `roomId`와 같은 문자열은 다른 값으로 설정하지 않는 한 변경되지 않습니다. [의존성 제거에 관한 자세한 내용은 여기를 참고하세요.](/learn/removing-effect-dependencies)\n\n---\n\n### 불필요한 함수 의존성 제거하기 {/*removing-unnecessary-function-dependencies*/}\n\nEffect가 렌더링 중에 생성된 객체나 함수에 의존하는 경우, 너무 자주 실행될 수 있습니다. 예를 들어 이 Effect는 매 렌더링 후에 다시 연결됩니다. 이는 [렌더링마다 `createOptions` 함수가 다르기 때문입니다.](/learn/removing-effect-dependencies#does-some-reactive-value-change-unintentionally)\n\n```js {4-9,12,16}\nfunction ChatRoom({ roomId }) {\n  const [message, setMessage] = useState('');\n\n  function createOptions() { // 🚩 이 함수는 재 렌더링 될 때마다 새로 생성됩니다\n    return {\n      serverUrl: serverUrl,\n      roomId: roomId\n    };\n  }\n\n  useEffect(() => {\n    const options = createOptions(); // 함수가 Effect 안에서 사용됩니다\n    const connection = createConnection();\n    connection.connect();\n    return () => connection.disconnect();\n  }, [createOptions]); // 🚩 결과적으로, 의존성이 재 렌더링 때마다 다릅니다\n  // ...\n```\n\n리렌더링마다 함수를 처음부터 생성하는 것 그 자체로는 문제가 되지 않고, 이를 최적화할 필요는 없습니다. 그러나 이것을 Effect의 의존성으로 사용하는 경우 Effect가 리렌더링 후마다 다시 실행되게 합니다.\n\n렌더링 중에 생성된 함수를 의존성으로 사용하는 것을 피하세요. 대신 Effect 내에서 함수를 선언하세요.\n\n<Sandpack>\n\n```js\nimport { useState, useEffect } from 'react';\nimport { createConnection } from './chat.js';\n\nconst serverUrl = 'https://localhost:1234';\n\nfunction ChatRoom({ roomId }) {\n  const [message, setMessage] = useState('');\n\n  useEffect(() => {\n    function createOptions() {\n      return {\n        serverUrl: serverUrl,\n        roomId: roomId\n      };\n    }\n\n    const options = createOptions();\n    const connection = createConnection(options);\n    connection.connect();\n    return () => connection.disconnect();\n  }, [roomId]);\n\n  return (\n    <>\n      <h1>Welcome to the {roomId} room!</h1>\n      <input value={message} onChange={e => setMessage(e.target.value)} />\n    </>\n  );\n}\n\nexport default function App() {\n  const [roomId, setRoomId] = useState('general');\n  return (\n    <>\n      <label>\n        Choose the chat room:{' '}\n        <select\n          value={roomId}\n          onChange={e => setRoomId(e.target.value)}\n        >\n          <option value=\"general\">general</option>\n          <option value=\"travel\">travel</option>\n          <option value=\"music\">music</option>\n        </select>\n      </label>\n      <hr />\n      <ChatRoom roomId={roomId} />\n    </>\n  );\n}\n```\n\n```js src/chat.js\nexport function createConnection({ serverUrl, roomId }) {\n  // 실제 구현은 실제로 서버에 연결됩니다.\n  return {\n    connect() {\n      console.log('✅ Connecting to \"' + roomId + '\" room at ' + serverUrl + '...');\n    },\n    disconnect() {\n      console.log('❌ Disconnected from \"' + roomId + '\" room at ' + serverUrl);\n    }\n  };\n}\n```\n\n```css\ninput { display: block; margin-bottom: 20px; }\nbutton { margin-left: 10px; }\n```\n\n</Sandpack>\n\n`createOptions` 함수가 Effect 내부에 선언되었으므로, Effect 자체는 `roomId` 문자열에만 의존합니다. 이 수정을 통해 입력란에 입력하는 것만으로 채팅이 다시 연결되지 않습니다. `roomId`와 같은 문자열은 다른 값으로 설정하지 않는 한 변경되지 않기 때문입니다. [의존성 제거에 대한 자세한 내용은 여기를 참고하세요.](/learn/removing-effect-dependencies)\n\n---\n\n### Effect에서 최신 props와 state를 읽기 {/*reading-the-latest-props-and-state-from-an-effect*/}\n\nBy default, when you read a reactive value from an Effect, you have to add it as a dependency. This ensures that your Effect \"reacts\" to every change of that value. For most dependencies, that's the behavior you want.\n\n**그러나 때로는 Effect에서 최신 props와 state를 '반응'하지 않고 읽고 싶을 수 있습니다.** 예를 들어 페이지 방문마다 쇼핑 카트에 담긴 항목 수를 기록하고 싶다고 가정해 보겠습니다.\n\n```js {3}\nfunction Page({ url, shoppingCart }) {\n  useEffect(() => {\n    logVisit(url, shoppingCart.length);\n  }, [url, shoppingCart]); // ✅ 모든 의존성이 선언됨\n  // ...\n}\n```\n\n**What if you want to log a new page visit after every `url` change, but *not* if only the `shoppingCart` changes?** You can't exclude `shoppingCart` from dependencies without breaking the [reactivity rules.](#specifying-reactive-dependencies) However, you can express that you *don't want* a piece of code to \"react\" to changes even though it is called from inside an Effect. [Declare an *Effect Event*](/learn/separating-events-from-effects#declaring-an-effect-event) with the [`useEffectEvent`](/reference/react/useEffectEvent) Hook, and move the code reading `shoppingCart` inside of it:\n\n```js {2-4,7,8}\nfunction Page({ url, shoppingCart }) {\n  const onVisit = useEffectEvent(visitedUrl => {\n    logVisit(visitedUrl, shoppingCart.length)\n  });\n\n  useEffect(() => {\n    onVisit(url);\n  }, [url]); // ✅ 모든 의존성이 선언됨\n  // ...\n}\n```\n\n**Effect 이벤트는 반응적이지 않으며 Effect의 의존성에서 배제되어야 합니다.** Effect 이벤트에는 비 반응형 코드(Effect 이벤트 로직은 최신 props와 state를 읽을 수 있음)를 배치할 수 있습니다. `onVisit`내의 `shoppingCart`를 읽음으로써 `shoppingCart`의 변경으로 인한 Effect의 재실행을 방지합니다.\n\n[Effect 이벤트가 어떻게 반응형 및 비 반응형 코드를 분리하는 데 도움이 되는지에 대한 자세한 내용은 여기를 읽어보세요.](/learn/separating-events-from-effects#reading-latest-props-and-state-with-effect-events)\n\n\n---\n\n### 서버와 클라이언트에서 다른 컨텐츠를 표시하기 {/*displaying-different-content-on-the-server-and-the-client*/}\n\nIf your app uses server rendering (either [directly](/reference/react-dom/server) or via a [framework](/learn/creating-a-react-app#full-stack-frameworks)), your component will render in two different environments. On the server, it will render to produce the initial HTML. On the client, React will run the rendering code again so that it can attach your event handlers to that HTML. This is why, for [hydration](/reference/react-dom/client/hydrateRoot#hydrating-server-rendered-html) to work, your initial render output must be identical on the client and the server.\n\n드물게 클라이언트에서 다른 내용을 표시해야 할 수 있습니다. 예를 들어 앱이 [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage)에서 일부 데이터를 읽는 경우, 이를 서버에서 구현할 수 없습니다. 다음은 이것을 구현하는 방법입니다.\n\n\n{/* TODO(@poteto) - investigate potential false positives in react compiler validation */}\n```js\nfunction MyComponent() {\n  const [didMount, setDidMount] = useState(false);\n\n  useEffect(() => {\n    setDidMount(true);\n  }, []);\n\n  if (didMount) {\n    // ... 클라이언트 전용 JSX 반환 ...\n  }  else {\n    // ... 초기 JSX 반환 ...\n  }\n}\n```\n\n앱이 로딩 중인 동안 사용자는 초기 렌더링 출력을 볼 것입니다. 그다음 로딩 및 hydration이 완료되면 Effect가 실행되어 `didMount`를 `true`로 설정하면서 다시 렌더링이 동작합니다. 이로써 클라이언트 전용 렌더링 출력으로 전환됩니다. Effect는 서버에서 실행되지 않으므로 초기 서버 렌더링 중의 `didMount`는 `false`가 됩니다.\n\n이 패턴은 적절히 사용해야 합니다. 느린 연결 환경을 가진 사용자는 초기 렌더링 화면을 상당한 시간 동안 볼 것이므로 컴포넌트의 모양을 급변시키지 않는 것이 좋습니다. 많은 경우에는 CSS를 사용하여 조건부로 다양한 것들을 표시하는 방법으로 대처할 수 있습니다.\n\n---\n\n## 트러블 슈팅 {/*troubleshooting*/}\n\n### Effect가 컴포넌트 마운트 시 2번 동작합니다. {/*my-effect-runs-twice-when-the-component-mounts*/}\n\n개발 환경에서 Strict Mode가 활성화되면 React는 실제 설정 이전에 설정과 정리를 한번 더 실행합니다.\n\n이것은 Effect의 로직이 올바르게 구현되었는지 확인하는 스트레스 테스트입니다. 이에 따라 눈에 띄는 문제가 발생한다면 정리 함수에 어떤 로직이 누락되었을 수 있습니다. 정리 함수는 설정 함수가 수행한 것을 중지하거나 되돌릴 수 있어야 합니다. 일반적인 지침으로는 사용자가 설정이 한번 호출되는 것(배포 환경과 같이)과 설정 → 정리 → 설정 순서로 호출되는 것을 구별할 수 없어야 한다는 것입니다.\n\n이것이 [버그를 찾는 데 어떻게 도움이 되며,](/learn/synchronizing-with-effects#step-3-add-cleanup-if-needed) [로직을 어떻게 수정하는지](/learn/synchronizing-with-effects#how-to-handle-the-effect-firing-twice-in-development)에 자세히 알아보려면 여기를 읽어보세요.\n\n---\n\n### Effect가 매 리렌더링마다 실행됩니다. {/*my-effect-runs-after-every-re-render*/}\n\n먼저 의존성 배열에 값을 추가했는지 확인해 보세요.\n\n```js {3}\nuseEffect(() => {\n  // ...\n}); // 🚩 의존성 배열이 없음. 재 렌더링 될 때마다 재실행됨!\n```\n\n의존성 배열을 명시했음에도 Effect가 여전히 반복해서 실행된다면 의존성이 렌더링마다 다르기 때문입니다.\n\n이 문제를 해결하기 위해 콘솔에 의존성을 수동으로 기록하는 방법으로 디버깅할 수 있습니다.\n\n```js {5}\n  useEffect(() => {\n    // ..\n  }, [serverUrl, roomId]);\n\n  console.log([serverUrl, roomId]);\n```\n\n그다음 콘솔에서 기록된 다른 렌더링 배열을 마우스 오른쪽 버튼으로 클릭하고 두 배열 모두에 대해 전역 변수로 저장을 선택할 수 있습니다. 첫 번째 요소가 `temp1`이고 두 번째 요소가 `temp2`라고 가정하면 브라우저 콘솔을 사용하여 양쪽 배열의 각 의존성이 동일한지 확인할 수 있습니다.\n\n```js\nObject.is(temp1[0], temp2[0]); // 첫 번째 의존성이 배열 간에 동일한가요?\nObject.is(temp1[1], temp2[1]); // 두 번째 의존성이 배열 간에 동일한가요?\nObject.is(temp1[2], temp2[2]); // ... 나머지 모든 의존성도 확인합니다  ...\n```\n\n렌더링마다 다른 의존성을 찾아냈다면 일반적으로 다음 중 하나의 방법으로 수정할 수 있습니다.\n\n- [Effect에서 이전 state를 기반으로 state 업데이트하기](#updating-state-based-on-previous-state-from-an-effect)\n- [불필요한 객체 의존성 제거하기](#removing-unnecessary-object-dependencies)\n- [불필요한 함수 의존성 제거하기](#removing-unnecessary-function-dependencies)\n- [Effect에서 최신 props와 state를 읽기](#reading-the-latest-props-and-state-from-an-effect)\n\n최후의 수단으로 (이러한 방법들이 도움이 되지 않은 경우), [`useMemo`](/reference/react/useMemo#memoizing-a-dependency-of-another-hook)나 [`useCallback`](/reference/react/useCallback#preventing-an-effect-from-firing-too-often)(함수의 경우)을 이용할 수 있습니다.\n\n---\n\n### Effect가 무한 반복됩니다. {/*my-effect-keeps-re-running-in-an-infinite-cycle*/}\n\nEffect가 무한 반복되려면 다음 두 가지 조건이 충족되어야 합니다.\n\n- Effect에서 state를 업데이트함.\n- 변경된 state가 리렌더링을 유발하며, 이에 따라 Effect의 종속성이 변경됨.\n\n문제를 해결하기 전에 Effect가 외부 시스템(DOM, 네트워크, 서드파티 위젯 등)에 연결되어 있는지 스스로 자문해보세요. Effect에서 왜 state를 변경했나요? 변경된 state가 외부 시스템과 동기화됐나요? 또는 Effect를 통해 애플리케이션의 데이터 흐름을 관리하려고 하는 건가요?\n\n외부 시스템이 없다면 [Effect를 제거](/learn/you-might-not-need-an-effect)해서 로직을 단순화할 수 있는지 고려해보세요.\n\n만약 실제로 어떤 외부 시스템과 동기화 중이라면 Effect가 state를 언제 어떤 조건에서 업데이트해야 하는지에 대해 고려해 보세요. 컴포넌트의 시각적 출력에 영향을 주는 state가 변했나요? 렌더링에 사용되지 않는 데이터를 추적해야 한다면 리렌더링을 야기하지 않는 [ref](/reference/react/useRef#referencing-a-value-with-a-ref)가 더 적합할 수 있습니다. Effect가 필요 이상으로 state를 업데이트하는지(리렌더링을 야기하지 않도록) 확인해 보세요.\n\n마지막으로 Effect가 제대로 된 시점에 state를 업데이트했지만 여전히 무한 반복되는 경우, 해당 state의 업데이트가 Effect의 종속성의 변경을 야기했을 수 있습니다. [종속성 변경을 디버깅하는 방법을 읽어보세요.](/reference/react/useEffect#my-effect-runs-after-every-re-render)\n\n---\n\n### 컴포넌트가 마운트 해제되지 않았음에도 정리 함수가 실행됩니다. {/*my-cleanup-logic-runs-even-though-my-component-didnt-unmount*/}\n\n정리 함수는 마운트 해제 시 뿐만 아니라 변경된 종속성으로 인한 모든 리렌더링 전에 실행됩니다. 또한 개발 환경에서는 React가 [컴포넌트가 마운트된 직후에 한 번 더 설정과 정리를 실행합니다.](#my-effect-runs-twice-when-the-component-mounts)\n\n설정 코드와 상응하는 정리 코드가 없다면 보통은 코드에 문제가 있을 가능성이 높습니다.\n\n```js {2-5}\nuseEffect(() => {\n  // 🔴 피하세요: 상응하는 설정 로직이 없는 정리 로직\n  return () => {\n    doSomething();\n  };\n}, []);\n```\n\n정리 로직은 설정 로직과 '대칭'이어야 하며 설정이 수행한 것을 중지하거나 되돌릴 수 있어야 합니다.\n\n```js {2-3,5}\n  useEffect(() => {\n    const connection = createConnection(serverUrl, roomId);\n    connection.connect();\n    return () => {\n      connection.disconnect();\n    };\n  }, [serverUrl, roomId]);\n```\n\n[Effect의 생명주기와 컴포넌트의 생명주기가 어떻게 다른지 확인해 보세요.](/learn/lifecycle-of-reactive-effects#the-lifecycle-of-an-effect)\n\n---\n\n### Effect가 시각적인 작업을 수행하며, 실행되기 전에 깜빡임이 보입니다. {/*my-effect-does-something-visual-and-i-see-a-flicker-before-it-runs*/}\n\nEffect가 `브라우저가 화면을 그리는 것`을 차단해야 하는 경우 `useEffect`를 [`useLayoutEffect`](/reference/react/useLayoutEffect)로 대체하세요. **이것은 대부분의 Effect에는 필요하지 않습니다.** 브라우저 페인팅 이전에 Effect를 실행하는 것이 중요한 경우에만 필요합니다. 예를 들어 사용자가 보기 전에 툴팁의 위치를 측정하고 지정해야 하는 경우가 있습니다.\n"
  },
  {
    "path": "src/content/reference/react/useEffectEvent.md",
    "content": "---\ntitle: useEffectEvent\n---\n\n<Intro>\n\n`useEffectEvent`는 Effect 내부의 비반응형 로직을 추출해 [Effect 이벤트](/learn/separating-events-from-effects#declaring-an-effect-event)라고 불리는 재사용 가능한 함수로 만들 수 있게 해주는 React Hook입니다.\n\n```js\nconst onEvent = useEffectEvent(callback)\n```\n\n</Intro>\n\n<InlineToc />\n\n## 레퍼런스 {/*reference*/}\n\n### `useEffectEvent(callback)` {/*useeffectevent*/}\n\nEffect 이벤트를 선언하기 위해 컴포넌트의 최상위 레벨에서 `useEffectEvent`를 호출하세요. Effect 이벤트는 `useEffect`와 같이 Effect 내부에서 호출 가능한 함수입니다.\n\n```js {4,6}\nimport { useEffectEvent, useEffect } from 'react';\n\nfunction ChatRoom({ roomId, theme }) {\n  const onConnected = useEffectEvent(() => {\n    showNotification('Connected!', theme);\n  });\n}\n```\n\n[아래에서 더 많은 예시를 확인하세요.](#usage)\n\n#### 매개변수 {/*parameters*/}\n\n- `callback`: Effect 이벤트를 위한 로직을 포함하는 함수입니다. `useEffectEvent`로 Effect 이벤트를 정의했을 때, `callback`은 실행할 때마다 항상 최신의 props와 state 값을 참조합니다. 이를 통해 오래된 클로저 문제를 피할 수 있습니다.\n\n#### 반환값 {/*returns*/}\n\nEffect 이벤트 함수를 반환합니다. `useEffect`, `useLayoutEffect` 또는 `useInsertionEffect` 내부에서 이 함수를 호출할 수 있습니다.\n\n#### 주의 사항 {/*caveats*/}\n\n- **Effect 내부에서만 호출하세요:** Effect 이벤트는 오로지 Effect 내부에서만 호출해야 합니다. 그것을 사용하는 Effect 이전에 그것을 정의하세요. 다른 컴포넌트나 훅으로 그것을 전달하지 마세요. [`eslint-plugin-react-hooks`](/reference/eslint-plugin-react-hooks) 린터(버전 6.1.1 또는 최신)는 Effect 이벤트를 잘못된 맥락에서 호출하는 것을 방지하기 위해 이 제한을 강제할 것입니다.\n- **의존성 지름길이 아닙니다:** Effect의 의존성 배열에 의존성을 적는 것을 피하기 위해 `useEffectEvent`를 사용하지 마세요. 이것은 버그를 숨기고 코드를 이해하는 것을 어렵게 합니다. 명시적으로 의존성을 작성하거나 필요한 경우 이전 값을 비교하기 위해 ref를 사용하세요.\n- **비반응형 로직을 위해 사용하세요:** 변하는 값에 의존하지 않는 로직을 추출하기 위해서만 `useEffectEvent`를 사용하세요.\n\n<DeepDive>\n\n#### Why are Effect Events not stable? {/*why-are-effect-events-not-stable*/}\n\nUnlike `set` functions from `useState` or refs, Effect Event functions do not have a stable identity. Their identity intentionally changes on every render:\n\n```js\n// 🔴 Wrong: including Effect Event in dependencies\nuseEffect(() => {\n  onSomething();\n}, [onSomething]); // ESLint will warn about this\n```\n\nThis is a deliberate design choice. Effect Events are meant to be called only from within Effects in the same component. Since you can only call them locally and cannot pass them to other components or include them in dependency arrays, a stable identity would serve no purpose, and would actually mask bugs.\n\nThe non-stable identity acts as a runtime assertion: if your code incorrectly depends on the function identity, you'll see the Effect re-running on every render, making the bug obvious.\n\nThis design reinforces that Effect Events conceptually belong to a particular effect, and are not a general purpose API to opt-out of reactivity.\n\n</DeepDive>\n\n---\n\n## 사용법 {/*usage*/}\n\n### 최신 props와 state를 읽기 {/*reading-the-latest-props-and-state*/}\n\n전형적으로, Effect 내부에서 반응형 값을 읽을 때, 의존성 배열에 그것을 포함해야 합니다. 이것은 값이 바뀔 때 마다 Effect가 다시 동작하도록 하고, 이것은 보통 바람직한 동작입니다.\n\n그러나 몇몇의 사례에서, 이 값들이 변할 때 Effect가 다시 동작하지 않고 Effect 내부에서 가장 최신의 props 또는 state를 읽고 싶어할 수 있습니다.\n\nEffect 내부에서 이 값들을 반응형으로 만드는 것 없이 [최신 props와 state를 읽기 위해](/learn/separating-events-from-effects#reading-latest-props-and-state-with-effect-events) Effect 이벤트 내부에 그것들을 포함하세요.\n\n```js [[1, 1, \"onConnected\"]]\nconst onConnected = useEffectEvent(() => {\n  if (!muted) {\n    showNotification('Connected!');\n  }\n});\n```\n\n`useEffectEvent` accepts an `event callback` and returns an <CodeStep step={1}>Effect Event</CodeStep>. The Effect Event is a function that can be called inside of Effects without re-connecting the Effect:\n\n```js [[1, 3, \"onConnected\"]]\nuseEffect(() => {\n  const connection = createConnection(roomId);\n  connection.on('connected', onConnected);\n  connection.connect();\n  return () => {\n    connection.disconnect();\n  }\n}, [roomId]);\n```\n\nSince `onConnected` is an <CodeStep step={1}>Effect Event</CodeStep>, `muted` and `onConnect` are not in the Effect dependencies.\n\n<Pitfall>\n\n##### Don't use Effect Events to skip dependencies {/*pitfall-skip-dependencies*/}\n\nIt might be tempting to use `useEffectEvent` to avoid listing dependencies that you think are \"unnecessary.\" However, this hides bugs and makes your code harder to understand:\n\n```js\n// 🔴 Wrong: Using Effect Events to hide dependencies\nconst logVisit = useEffectEvent(() => {\n  log(pageUrl);\n});\n\nuseEffect(() => {\n  logVisit()\n}, []); // Missing pageUrl means you miss logs\n```\n\nIf a value should cause your Effect to re-run, keep it as a dependency. Only use Effect Events for logic that genuinely should not re-trigger your Effect.\n\nSee [Separating Events from Effects](/learn/separating-events-from-effects) to learn more.\n\n</Pitfall>\n\n---\n\n### Using a timer with latest values {/*using-a-timer-with-latest-values*/}\n\nWhen you use `setInterval` or `setTimeout` in an Effect, you often want to read the latest values from render without restarting the timer whenever those values change.\n\nThis counter increments `count` by the current `increment` value every second. The `onTick` Effect Event reads the latest `count` and `increment` without causing the interval to restart:\n\n<Sandpack>\n\n```js\nimport { useState, useEffect, useEffectEvent } from 'react';\n\nexport default function Timer() {\n  const [count, setCount] = useState(0);\n  const [increment, setIncrement] = useState(1);\n\n  const onTick = useEffectEvent(() => {\n    setCount(count + increment);\n  });\n\n  useEffect(() => {\n    const id = setInterval(() => {\n      onTick();\n    }, 1000);\n    return () => {\n      clearInterval(id);\n    };\n  }, []);\n\n  return (\n    <>\n      <h1>\n        Counter: {count}\n        <button onClick={() => setCount(0)}>Reset</button>\n      </h1>\n      <hr />\n      <p>\n        Every second, increment by:\n        <button disabled={increment === 0} onClick={() => {\n          setIncrement(i => i - 1);\n        }}>–</button>\n        <b>{increment}</b>\n        <button onClick={() => {\n          setIncrement(i => i + 1);\n        }}>+</button>\n      </p>\n    </>\n  );\n}\n```\n\n</Sandpack>\n\nTry changing the increment value while the timer is running. The counter immediately uses the new increment value, but the timer keeps ticking smoothly without restarting.\n\n---\n\n### Using an event listener with latest values {/*using-an-event-listener-with-latest-values*/}\n\nWhen you set up an event listener in an Effect, you often need to read the latest values from render in the callback. Without `useEffectEvent`, you would need to include the values in your dependencies, causing the listener to be removed and re-added on every change.\n\nThis example shows a dot that follows the cursor, but only when \"Can move\" is checked. The `onMove` Effect Event always reads the latest `canMove` value without re-running the Effect:\n\n<Sandpack>\n\n```js\nimport { useState, useEffect, useEffectEvent } from 'react';\n\nexport default function App() {\n  const [position, setPosition] = useState({ x: 0, y: 0 });\n  const [canMove, setCanMove] = useState(true);\n\n  const onMove = useEffectEvent(e => {\n    if (canMove) {\n      setPosition({ x: e.clientX, y: e.clientY });\n    }\n  });\n\n  useEffect(() => {\n    window.addEventListener('pointermove', onMove);\n    return () => window.removeEventListener('pointermove', onMove);\n  }, []);\n\n  return (\n    <>\n      <label>\n        <input\n          type=\"checkbox\"\n          checked={canMove}\n          onChange={e => setCanMove(e.target.checked)}\n        />\n        The dot is allowed to move\n      </label>\n      <hr />\n      <div style={{\n        position: 'absolute',\n        backgroundColor: 'pink',\n        borderRadius: '50%',\n        opacity: 0.6,\n        transform: `translate(${position.x}px, ${position.y}px)`,\n        pointerEvents: 'none',\n        left: -20,\n        top: -20,\n        width: 40,\n        height: 40,\n      }} />\n    </>\n  );\n}\n```\n\n```css\nbody {\n  height: 200px;\n}\n```\n\n</Sandpack>\n\nToggle the checkbox and move your cursor. The dot responds immediately to the checkbox state, but the event listener is only set up once when the component mounts.\n\n---\n\n### Avoid reconnecting to external systems {/*showing-a-notification-without-reconnecting*/}\n\nA common use case for `useEffectEvent` is when you want to do something in response to an Effect, but that \"something\" depends on a value you don't want to react to.\n\nIn this example, a chat component connects to a room and shows a notification when connected. The user can mute notifications with a checkbox. However, you don't want to reconnect to the chat room every time the user changes the settings:\n\n<Sandpack>\n\n```json package.json hidden\n{\n  \"dependencies\": {\n    \"react\": \"latest\",\n    \"react-dom\": \"latest\",\n    \"react-scripts\": \"latest\",\n    \"toastify-js\": \"1.12.0\"\n  },\n  \"scripts\": {\n    \"start\": \"react-scripts start\",\n    \"build\": \"react-scripts build\",\n    \"test\": \"react-scripts test --env=jsdom\",\n    \"eject\": \"react-scripts eject\"\n  }\n}\n```\n\n```js\nimport { useState, useEffect, useEffectEvent } from 'react';\nimport { createConnection } from './chat.js';\nimport { showNotification } from './notifications.js';\n\nfunction ChatRoom({ roomId, muted }) {\n  const onConnected = useEffectEvent((roomId) => {\n    console.log('✅ Connected to ' + roomId + ' (muted: ' + muted + ')');\n    if (!muted) {\n      showNotification('Connected to ' + roomId);\n    }\n  });\n\n  useEffect(() => {\n    const connection = createConnection(roomId);\n    console.log('⏳ Connecting to ' + roomId + '...');\n    connection.on('connected', () => {\n      onConnected(roomId);\n    });\n    connection.connect();\n    return () => {\n      console.log('❌ Disconnected from ' + roomId);\n      connection.disconnect();\n    }\n  }, [roomId]);\n\n  return <h1>Welcome to the {roomId} room!</h1>;\n}\n\nexport default function App() {\n  const [roomId, setRoomId] = useState('general');\n  const [muted, setMuted] = useState(false);\n  return (\n    <>\n      <label>\n        Choose the chat room:{' '}\n        <select\n          value={roomId}\n          onChange={e => setRoomId(e.target.value)}\n        >\n          <option value=\"general\">general</option>\n          <option value=\"travel\">travel</option>\n          <option value=\"music\">music</option>\n        </select>\n      </label>\n      <label>\n        <input\n          type=\"checkbox\"\n          checked={muted}\n          onChange={e => setMuted(e.target.checked)}\n        />\n        Mute notifications\n      </label>\n      <hr />\n      <ChatRoom\n        roomId={roomId}\n        muted={muted}\n      />\n    </>\n  );\n}\n```\n\n```js src/chat.js\nconst serverUrl = 'https://localhost:1234';\n\nexport function createConnection(roomId) {\n  // A real implementation would actually connect to the server\n  let connectedCallback;\n  let timeout;\n  return {\n    connect() {\n      timeout = setTimeout(() => {\n        if (connectedCallback) {\n          connectedCallback();\n        }\n      }, 100);\n    },\n    on(event, callback) {\n      if (connectedCallback) {\n        throw Error('Cannot add the handler twice.');\n      }\n      if (event !== 'connected') {\n        throw Error('Only \"connected\" event is supported.');\n      }\n      connectedCallback = callback;\n    },\n    disconnect() {\n      clearTimeout(timeout);\n    }\n  };\n}\n```\n\n```js src/notifications.js\nimport Toastify from 'toastify-js';\nimport 'toastify-js/src/toastify.css';\n\nexport function showNotification(message, theme) {\n  Toastify({\n    text: message,\n    duration: 2000,\n    gravity: 'top',\n    position: 'right',\n    style: {\n      background: theme === 'dark' ? 'black' : 'white',\n      color: theme === 'dark' ? 'white' : 'black',\n    },\n  }).showToast();\n}\n```\n\n```css\nlabel { display: block; margin-top: 10px; }\n```\n\n</Sandpack>\n\nTry switching rooms. The chat reconnects and shows a notification. Now mute the notifications. Since `muted` is read inside the Effect Event rather than the Effect, the chat stays connected.\n\n---\n\n### Using Effect Events in custom Hooks {/*using-effect-events-in-custom-hooks*/}\n\nYou can use `useEffectEvent` inside your own custom Hooks. This lets you create reusable Hooks that encapsulate Effects while keeping some values non-reactive:\n\n<Sandpack>\n\n```js\nimport { useState, useEffect, useEffectEvent } from 'react';\n\nfunction useInterval(callback, delay) {\n  const onTick = useEffectEvent(callback);\n\n  useEffect(() => {\n    if (delay === null) {\n      return;\n    }\n    const id = setInterval(() => {\n      onTick();\n    }, delay);\n    return () => clearInterval(id);\n  }, [delay]);\n}\n\nfunction Counter({ incrementBy }) {\n  const [count, setCount] = useState(0);\n\n  useInterval(() => {\n    setCount(c => c + incrementBy);\n  }, 1000);\n\n  return (\n    <div>\n      <h2>Count: {count}</h2>\n      <p>Incrementing by {incrementBy} every second</p>\n    </div>\n  );\n}\n\nexport default function App() {\n  const [incrementBy, setIncrementBy] = useState(1);\n\n  return (\n    <>\n      <label>\n        Increment by:{' '}\n        <select\n          value={incrementBy}\n          onChange={(e) => setIncrementBy(Number(e.target.value))}\n        >\n          <option value={1}>1</option>\n          <option value={5}>5</option>\n          <option value={10}>10</option>\n        </select>\n      </label>\n      <hr />\n      <Counter incrementBy={incrementBy} />\n    </>\n  );\n}\n```\n\n```css\nlabel { display: block; margin-bottom: 8px; }\n```\n\n</Sandpack>\n\nIn this example, `useInterval` is a custom Hook that sets up an interval. The `callback` passed to it is wrapped in an Effect Event, so the interval does not reset even if a new `callback` is passed in every render.\n\n---\n\n## Troubleshooting {/*troubleshooting*/}\n\n### I'm getting an error: \"A function wrapped in useEffectEvent can't be called during rendering\" {/*cant-call-during-rendering*/}\n\nThis error means you're calling an Effect Event function during the render phase of your component. Effect Events can only be called from inside Effects or other Effect Events.\n\n```js\nfunction MyComponent({ data }) {\n  const onLog = useEffectEvent(() => {\n    console.log(data);\n  });\n\n  // 🔴 Wrong: calling during render\n  onLog();\n\n  // ✅ Correct: call from an Effect\n  useEffect(() => {\n    onLog();\n  }, []);\n\n  return <div>{data}</div>;\n}\n```\n\nIf you need to run logic during render, don't wrap it in `useEffectEvent`. Call the logic directly or move it into an Effect.\n\n---\n\n### I'm getting a lint error: \"Functions returned from useEffectEvent must not be included in the dependency array\" {/*effect-event-in-deps*/}\n\nIf you see a warning like \"Functions returned from `useEffectEvent` must not be included in the dependency array\", remove the Effect Event from your dependencies:\n\n```js\nconst onSomething = useEffectEvent(() => {\n  // ...\n});\n\n// 🔴 Wrong: Effect Event in dependencies\nuseEffect(() => {\n  onSomething();\n}, [onSomething]);\n\n// ✅ Correct: no Effect Event in dependencies\nuseEffect(() => {\n  onSomething();\n}, []);\n```\n\nEffect Events are designed to be called from Effects without being listed as dependencies. The linter enforces this because the function identity is [intentionally not stable](#why-are-effect-events-not-stable). Including it would cause your Effect to re-run on every render.\n\n---\n\n### I'm getting a lint error: \"... is a function created with useEffectEvent, and can only be called from Effects\" {/*effect-event-called-outside-effect*/}\n\nIf you see a warning like \"... is a function created with React Hook `useEffectEvent`, and can only be called from Effects and Effect Events\", you're calling the function from the wrong place:\n\n```js\nconst onSomething = useEffectEvent(() => {\n  console.log(value);\n});\n\n// 🔴 Wrong: calling from event handler\nfunction handleClick() {\n  onSomething();\n}\n\n// 🔴 Wrong: passing to child component\nreturn <Child onSomething={onSomething} />;\n\n// ✅ Correct: calling from Effect\nuseEffect(() => {\n  onSomething();\n}, []);\n```\n\nEffect Events are specifically designed to be used in Effects local to the component they're defined in. If you need a callback for event handlers or to pass to children, use a regular function or `useCallback` instead.\n"
  },
  {
    "path": "src/content/reference/react/useId.md",
    "content": "---\ntitle: useId\n---\n\n<Intro>\n\n`useId`는 접근성 어트리뷰트에 전달할 수 있는 고유 ID를 생성하기 위한 React Hook입니다.\n\n```js\nconst id = useId()\n```\n\n</Intro>\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `useId()` {/*useid*/}\n\n`useId`를 컴포넌트의 최상위에서 호출하여 고유 ID를 생성합니다.\n\n```js\nimport { useId } from 'react';\n\nfunction PasswordField() {\n  const passwordHintId = useId();\n  // ...\n```\n\n[아래에서 더 많은 예시를 확인하세요.](#usage)\n\n#### 매개변수 {/*parameters*/}\n\n`useId`는 어떤 매개변수도 받지 않습니다.\n\n#### 반환값 {/*returns*/}\n\n`useId`를 호출한 특정 컴포넌트와 특정 `useId`에 관련된 고유 ID 문자열을 반환합니다.\n\n#### 주의 사항 {/*caveats*/}\n\n* `useId`는 Hook이므로 **컴포넌트의 최상위** 또는 커스텀 Hook에서만 호출할 수 있습니다. 반복문이나 조건문에서는 사용할 수 없습니다. 필요한 경우 새로운 컴포넌트를 추출하고 해당 컴포넌트로 state를 이동해서 사용할 수 있습니다.\n\n* `useId`를 리스트의 **key를 생성하기 위해 사용하면 안 됩니다**. [Key는 데이터로부터 생성해야 합니다.](/learn/rendering-lists#where-to-get-your-key)\n\n* `useId`는 현재 [비동기 서버 컴포넌트](/reference/rsc/server-components#async-components-with-server-components)에서 사용할 수 없습니다.\n\n---\n\n## 사용법 {/*usage*/}\n\n<Pitfall>\n\n**`useId`를 리스트의 key를 생성하기 위해 사용하면 안 됩니다.** [Key는 데이터로부터 생성해야 합니다.](/learn/rendering-lists#where-to-get-your-key)\n\n</Pitfall>\n\n### 접근성 어트리뷰트를 위한 고유 ID 생성하기 {/*generating-unique-ids-for-accessibility-attributes*/}\n\n고유 ID를 생성하기 위해 `useId`를 컴포넌트의 최상단에서 호출합니다.\n\n```js [[1, 4, \"passwordHintId\"]]\nimport { useId } from 'react';\n\nfunction PasswordField() {\n  const passwordHintId = useId();\n  // ...\n```\n\n<CodeStep step={1}>생성된 ID</CodeStep>를 다른 어트리뷰트로 전달할 수 있습니다.\n\n```js [[1, 2, \"passwordHintId\"], [1, 3, \"passwordHintId\"]]\n<>\n  <input type=\"password\" aria-describedby={passwordHintId} />\n  <p id={passwordHintId}>\n</>\n```\n\n**예시를 통해 유용한 상황에 대해 알아보겠습니다.**\n\n[`aria-describedby`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-describedby)와 같은 [HTML 접근성 어트리뷰트](https://developer.mozilla.org/ko/docs/Web/Accessibility/ARIA)를 사용하면 두 개의 태그가 서로 연관되어 있다는 것을 명시할 수 있습니다. 예를 들어 엘리먼트(input)를 다른 엘리먼트(paragraph)에서 설명하도록 명시할 수 있습니다.\n\nHTML에서는 일반적으로 다음과 같이 작성합니다.\n\n```html {5,8}\n<label>\n  Password:\n  <input\n    type=\"password\"\n    aria-describedby=\"password-hint\"\n  />\n</label>\n<p id=\"password-hint\">\n  The password should contain at least 18 characters\n</p>\n```\n\nReact에서 ID를 직접 코드에 입력하는 것은 좋은 사례가 아닙니다. 페이지에서 컴포넌트는 몇 번이고 렌더링 될 수 있지만 ID는 고유해야 합니다. ID를 직접 입력하는 대신 `useId`를 활용해서 고유한 ID를 생성할 수 있습니다.\n\n```js {4,11,14}\nimport { useId } from 'react';\n\nfunction PasswordField() {\n  const passwordHintId = useId();\n  return (\n    <>\n      <label>\n        Password:\n        <input\n          type=\"password\"\n          aria-describedby={passwordHintId}\n        />\n      </label>\n      <p id={passwordHintId}>\n        The password should contain at least 18 characters\n      </p>\n    </>\n  );\n}\n```\n\n이제 `PasswordField`가 화면에 여러 번 나타나도 생성된 ID는 충돌하지 않습니다.\n\n<Sandpack>\n\n```js\nimport { useId } from 'react';\n\nfunction PasswordField() {\n  const passwordHintId = useId();\n  return (\n    <>\n      <label>\n        Password:\n        <input\n          type=\"password\"\n          aria-describedby={passwordHintId}\n        />\n      </label>\n      <p id={passwordHintId}>\n        The password should contain at least 18 characters\n      </p>\n    </>\n  );\n}\n\nexport default function App() {\n  return (\n    <>\n      <h2>Choose password</h2>\n      <PasswordField />\n      <h2>Confirm password</h2>\n      <PasswordField />\n    </>\n  );\n}\n```\n\n```css\ninput { margin: 5px; }\n```\n\n</Sandpack>\n\n[영상](https://www.youtube.com/watch?v=0dNzNcuEuOo)을 통해 보조 기술을 활용했을 때 사용자 경험의 차이점을 확인할 수 있습니다.\n\n<Pitfall>\n\n[서버 렌더링](/reference/react-dom/server)에서 **`useId`는 서버와 클라이언트에서 동일한 컴포넌트 트리가 필요합니다.** 서버와 클라이언트에서 렌더링하는 트리가 정확히 일치하지 않으면 생성된 ID는 일치하지 않습니다.\n\n</Pitfall>\n\n<DeepDive>\n\n#### useId를 사용하는 것이 카운터를 증가하는 것보다 나은 이유는 무엇일까요? {/*why-is-useid-better-than-an-incrementing-counter*/}\n\n`useId`를 사용하는 것이 `nextId++`처럼 전역 변수를 증가하는 것보다 나은 이유에 대해 궁금할 수 있습니다.\n\n`useId`의 주요 이점은 React가 [서버 렌더링](/reference/react-dom/server)과 함께 작동하도록 보장한다는 것입니다. 서버 렌더링을 하는 동안 컴포넌트는 HTML 결과물을 생성합니다. 이후, 클라이언트에서 [hydration](/reference/react-dom/client/hydrateRoot)이 HTML 결과물에 이벤트 핸들러를 연결합니다. hydration이 동작하려면 클라이언트의 출력이 서버 HTML과 일치해야 합니다.\n\n클라이언트 컴포넌트의 hydrated 순서가 서버 HTML이 생성된 순서와 일치하지 않을 수 있기 때문에 카운터 증가로 이를 보장하기는 매우 어렵습니다. `useId`를 사용하면 hydration이 동작하고 서버와 클라이언트 간에 출력이 일치하는 것을 보장할 수 있습니다.\n\nReact에서 `useId`는 호출한 컴포넌트의 \"부모 경로\"에서 생성됩니다. 클라이언트와 서버 트리가 동일한 경우 렌더링 순서와 관계없이 \"부모 경로\"가 일치하는 이유입니다.\n\n</DeepDive>\n\n---\n\n### 여러 개의 연관된 엘리먼트의 ID 생성하기 {/*generating-ids-for-several-related-elements*/}\n\n여러 개의 연관된 엘리먼트에 ID를 전달하는 과정이 필요할 때 `useId`를 사용해서 공유 접두사를 생성할 수 있습니다.\n\n<Sandpack>\n\n```js\nimport { useId } from 'react';\n\nexport default function Form() {\n  const id = useId();\n  return (\n    <form>\n      <label htmlFor={id + '-firstName'}>First Name:</label>\n      <input id={id + '-firstName'} type=\"text\" />\n      <hr />\n      <label htmlFor={id + '-lastName'}>Last Name:</label>\n      <input id={id + '-lastName'} type=\"text\" />\n    </form>\n  );\n}\n```\n\n```css\ninput { margin: 5px; }\n```\n\n</Sandpack>\n\n`useId`를 고유한 ID가 필요한 모든 엘리먼트에서 실행하는 것을 방지할 수 있습니다.\n\n---\n\n### 생성된 모든 ID에 대해 공유 접두사 지정하기 {/*specifying-a-shared-prefix-for-all-generated-ids*/}\n\n여러 개의 독립된 React 애플리케이션을 하나의 페이지에서 렌더링한다면 `identifierPrefix`를  [`createRoot`](/reference/react-dom/client/createRoot#parameters) 또는 [`hydrateRoot`](/reference/react-dom/client/hydrateRoot) 호출에 대한 옵션으로 전달합니다. `useId`로 생성된 모든 식별자가 별개의 접두사로 시작하므로 서로 다른 두 개의 앱에서 생성된 ID가 충돌하지 않는 것을 보장합니다.\n\n<Sandpack>\n\n```html public/index.html\n<!DOCTYPE html>\n<html>\n  <head><title>My app</title></head>\n  <body>\n    <div id=\"root1\"></div>\n    <div id=\"root2\"></div>\n  </body>\n</html>\n```\n\n```js\nimport { useId } from 'react';\n\nfunction PasswordField() {\n  const passwordHintId = useId();\n  console.log('Generated identifier:', passwordHintId)\n  return (\n    <>\n      <label>\n        Password:\n        <input\n          type=\"password\"\n          aria-describedby={passwordHintId}\n        />\n      </label>\n      <p id={passwordHintId}>\n        The password should contain at least 18 characters\n      </p>\n    </>\n  );\n}\n\nexport default function App() {\n  return (\n    <>\n      <h2>Choose password</h2>\n      <PasswordField />\n    </>\n  );\n}\n```\n\n```js src/index.js active\nimport { createRoot } from 'react-dom/client';\nimport App from './App.js';\nimport './styles.css';\n\nconst root1 = createRoot(document.getElementById('root1'), {\n  identifierPrefix: 'my-first-app-'\n});\nroot1.render(<App />);\n\nconst root2 = createRoot(document.getElementById('root2'), {\n  identifierPrefix: 'my-second-app-'\n});\nroot2.render(<App />);\n```\n\n```css\n#root1 {\n  border: 5px solid blue;\n  padding: 10px;\n  margin: 5px;\n}\n\n#root2 {\n  border: 5px solid green;\n  padding: 10px;\n  margin: 5px;\n}\n\ninput { margin: 5px; }\n```\n\n</Sandpack>\n\n---\n\n### 클라이언트와 서버에서 동일한 ID 접두사 사용하기 {/*using-the-same-id-prefix-on-the-client-and-the-server*/}\n\n[동일한 페이지에서 여러 독립적인 React 앱을 렌더링하는 경우](#specifying-a-shared-prefix-for-all-generated-ids), 이러한 앱 중 일부가 서버에서 렌더링되는 경우, 클라이언트 측에서 [`hydrateRoot`](/reference/react-dom/client/hydrateRoot) 호출에 전달하는 `identifierPrefix가` [`renderToPipeableStream`](/reference/react-dom/server/renderToPipeableStream)와 같은 [서버 API](/reference/react-dom/server)에 전달하는 `identifierPrefix`와 동일한지 확인해야 합니다.\n\n```js\n// Server\nimport { renderToPipeableStream } from 'react-dom/server';\n\nconst { pipe } = renderToPipeableStream(\n  <App />,\n  { identifierPrefix: 'react-app1' }\n);\n```\n\n```js\n// Client\nimport { hydrateRoot } from 'react-dom/client';\n\nconst domNode = document.getElementById('root');\nconst root = hydrateRoot(\n  domNode,\n  reactNode,\n  { identifierPrefix: 'react-app1' }\n);\n```\n\n페이지에 React 앱이 하나만 있는 경우에는 `identifierPrefix`를 전달할 필요가 없습니다.\n"
  },
  {
    "path": "src/content/reference/react/useImperativeHandle.md",
    "content": "---\ntitle: useImperativeHandle\n---\n\n<Intro>\n\n`useImperativeHandle`은 [Ref](/learn/manipulating-the-dom-with-refs)로 노출되는 핸들을 사용자가 직접 정의할 수 있게 해주는 React Hook입니다.\n\n```js\nuseImperativeHandle(ref, createHandle, dependencies?)\n```\n\n</Intro>\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `useImperativeHandle(ref, createHandle, dependencies?)` {/*useimperativehandle*/}\n\n컴포넌트의 최상위 레벨에서 `useImperativeHandle`을 호출하여 노출할 Ref 핸들을 사용자가 직접 정의할 수 있습니다.\n\n```js\nimport { useImperativeHandle } from 'react';\n\nfunction MyInput({ ref }) {\n  useImperativeHandle(ref, () => {\n    return {\n      // ... 메서드를 여기에 입력하세요 ...\n    };\n  }, []);\n  // ...\n```\n\n[아래에서 더 많은 예시를 확인하세요.](#usage)\n\n#### 매개변수 {/*parameters*/}\n\n* `ref`: `MyInput` 컴포넌트의 Prop으로 받은 `ref`입니다.\n\n* `createHandle`: 인수가 없고 노출하려는 Ref 핸들을 반환하는 함수입니다. 해당 Ref 핸들은 어떠한 유형이든 될 수 있습니다. 일반적으로 노출하려는 메서드가 있는 객체를 반환합니다.\n\n* **(선택적)** `dependencies`: `createHandle` 코드 내에서 참조하는 모든 반응형 값을 나열한 목록입니다. 반응형 값은 Props, State 및 컴포넌트 내에서 직접 선언한 모든 변수와 함수를 포함합니다. [React에 대한 린터<sup>Linter</sup>를 구성한 경우](/learn/editor-setup#linting), 모든 반응형 값이 올바르게 의존성으로 지정되었는지 확인합니다. 의존성 목록은 항상 일정한 수의 항목을 가지고 `[dep1, dep2, dep3]`와 같이 인라인으로 작성되어야 합니다. React는 각 의존성을 [`Object.is`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is) 비교를 사용하여 이전 값과 비교합니다. 리렌더링으로 인해 일부 의존성이 변경되거나, 이 인수를 생략한 경우 `createHandle` 함수가 다시 실행되고 새로 생성된 핸들이 Ref에 할당됩니다.\n\n<Note>\n\nReact 19 부터 [`ref`를 Prop으로 받을 수 있습니다.](/blog/2024/12/05/react-19#ref-as-a-prop) React 18 또는 그 이전 버전에서는 `ref`를 받기위해 [`forwardRef`](/reference/react/forwardRef)를 사용해야 합니다.\n\n</Note>\n\n#### 반환값 {/*returns*/}\n\n`useImperativeHandle`은 `undefined`를 반환합니다.\n\n---\n\n## 사용법 {/*usage*/}\n\n### 부모 컴포넌트에 커스텀 Ref 핸들 노출 {/*exposing-a-custom-ref-handle-to-the-parent-component*/}\n\n부모 엘리먼트에 DOM 노드를 노출하려면 해당 노드에 `ref` Prop을 전달해야 합니다.\n\n```js {2}\nfunction MyInput({ ref }) {\n  return <input ref={ref} />;\n};\n```\n\n위의 코드에서 [`MyInput`에 대한 Ref는 `<input>` DOM 노드를 받게 됩니다.](/reference/react/forwardRef#exposing-a-dom-node-to-the-parent-component) 그러나 대신 사용자 지정 값을 노출할 수 있습니다. 노출된 핸들을 사용자 정의하려면 컴포넌트의 최상위 레벨에서 `useImperativeHandle`을 호출하세요.\n\n```js {4-8}\nimport { useImperativeHandle } from 'react';\n\nfunction MyInput({ ref }) {\n  useImperativeHandle(ref, () => {\n    return {\n      // ... 메서드를 여기에 입력하세요 ...\n    };\n  }, []);\n\n  return <input />;\n};\n```\n\n위의 코드에서 `<input>`에 대한 `ref`는 더이상 전달되지 않습니다.\n\n예를 들어 전체 `<input>` DOM 노드를 노출하지 않고 `focus`와 `scrollIntoView`의 두 메서드만을 노출하고 싶다고 가정해 봅시다. 그러기 위해서는 실제 브라우저 DOM을 별도의 Ref에 유지해야 합니다. 그리고 `useImperativeHandle`을 사용하여 부모 컴포넌트에서 호출할 메서드만 있는 핸들을 노출합니다.\n\n```js {7-14}\nimport { useRef, useImperativeHandle } from 'react';\n\nfunction MyInput({ ref }) {\n  const inputRef = useRef(null);\n\n  useImperativeHandle(ref, () => {\n    return {\n      focus() {\n        inputRef.current.focus();\n      },\n      scrollIntoView() {\n        inputRef.current.scrollIntoView();\n      },\n    };\n  }, []);\n\n  return <input ref={inputRef} />;\n};\n```\n\n이제 부모 컴포넌트가 `MyInput`에 대한 Ref를 가져오면 `focus` 및 `scrollIntoView` 메서드를 호출할 수 있습니다. 그러나 기본 `<input>` DOM 노드의 전체 엑세스 권한은 없습니다.\n\n<Sandpack>\n\n```js\nimport { useRef } from 'react';\nimport MyInput from './MyInput.js';\n\nexport default function Form() {\n  const ref = useRef(null);\n\n  function handleClick() {\n    ref.current.focus();\n    // 이 작업은 DOM 노드가 노출되지 않으므로 작동하지 않습니다.\n    // ref.current.style.opacity = 0.5;\n  }\n\n  return (\n    <form>\n      <MyInput placeholder=\"Enter your name\" ref={ref} />\n      <button type=\"button\" onClick={handleClick}>\n        Edit\n      </button>\n    </form>\n  );\n}\n```\n\n```js src/MyInput.js\nimport { useRef, useImperativeHandle } from 'react';\n\nfunction MyInput({ ref, ...props }) {\n  const inputRef = useRef(null);\n\n  useImperativeHandle(ref, () => {\n    return {\n      focus() {\n        inputRef.current.focus();\n      },\n      scrollIntoView() {\n        inputRef.current.scrollIntoView();\n      },\n    };\n  }, []);\n\n  return <input {...props} ref={inputRef} />;\n};\n\nexport default MyInput;\n```\n\n```css\ninput {\n  margin: 5px;\n}\n```\n\n</Sandpack>\n\n---\n\n### 사용자 정의 명령형 메서드 노출 {/*exposing-your-own-imperative-methods*/}\n\n명령형 핸들<sup>Imperative Handle</sup>을 통해 노출하는 메서드는 DOM 메서드와 정확하게 일치할 필요가 없습니다. 예를 들어, 이 `Post` 컴포넌트는 명령형 핸들을 통해 `scrollAndFocusAddComment` 메서드를 표시합니다. 이렇게 하면 부모 `Page`에서 버튼을 클릭할 때 댓글 목록을 스크롤하고 입력 필드에 초점을 맞출 수 있습니다.\n\n<Sandpack>\n\n```js\nimport { useRef } from 'react';\nimport Post from './Post.js';\n\nexport default function Page() {\n  const postRef = useRef(null);\n\n  function handleClick() {\n    postRef.current.scrollAndFocusAddComment();\n  }\n\n  return (\n    <>\n      <button onClick={handleClick}>\n        Write a comment\n      </button>\n      <Post ref={postRef} />\n    </>\n  );\n}\n```\n\n```js src/Post.js\nimport { useRef, useImperativeHandle } from 'react';\nimport CommentList from './CommentList.js';\nimport AddComment from './AddComment.js';\n\nfunction Post({ ref }) {\n  const commentsRef = useRef(null);\n  const addCommentRef = useRef(null);\n\n  useImperativeHandle(ref, () => {\n    return {\n      scrollAndFocusAddComment() {\n        commentsRef.current.scrollToBottom();\n        addCommentRef.current.focus();\n      }\n    };\n  }, []);\n\n  return (\n    <>\n      <article>\n        <p>Welcome to my blog!</p>\n      </article>\n      <CommentList ref={commentsRef} />\n      <AddComment ref={addCommentRef} />\n    </>\n  );\n};\n\nexport default Post;\n```\n\n\n```js src/CommentList.js\nimport { useRef, useImperativeHandle } from 'react';\n\nfunction CommentList({ ref }) {\n  const divRef = useRef(null);\n\n  useImperativeHandle(ref, () => {\n    return {\n      scrollToBottom() {\n        const node = divRef.current;\n        node.scrollTop = node.scrollHeight;\n      }\n    };\n  }, []);\n\n  let comments = [];\n  for (let i = 0; i < 50; i++) {\n    comments.push(<p key={i}>Comment #{i}</p>);\n  }\n\n  return (\n    <div className=\"CommentList\" ref={divRef}>\n      {comments}\n    </div>\n  );\n}\n\nexport default CommentList;\n```\n\n```js src/AddComment.js\nimport { useRef, useImperativeHandle } from 'react';\n\nfunction AddComment({ ref }) {\n  return <input placeholder=\"Add comment...\" ref={ref} />;\n}\n\nexport default AddComment;\n```\n\n```css\n.CommentList {\n  height: 100px;\n  overflow: scroll;\n  border: 1px solid black;\n  margin-top: 20px;\n  margin-bottom: 20px;\n}\n```\n\n</Sandpack>\n\n<Pitfall>\n\n**Ref를 과도하게 사용하지 마세요.** Ref는 Props로 표현할 수 없는 필수적인 행동에만 사용해야 합니다. 예를 들어 특정 노드로 스크롤 하기, 노드에 초점 맞추기, 애니메이션 실행하기, 텍스트 선택하기 등이 있습니다.\n\n**Prop으로 표현할 수 있는 것에 Ref를 사용하지 마세요.** 예를 들어 `Modal` 컴포넌트에서 `{ open, close }`와 같은 명령형 핸들<sup>Imperative Handle</sup>을 노출하는 대신 `<Modal isOpen={isOpen} />`과 같은 `isOpen` Prop을 사용하는 것이 더 좋습니다. [Effect](/learn/synchronizing-with-effects)를 사용하면 Prop을 통해 명령형 동작<sup>Imperative Behavior</sup>을 노출할 수 있습니다.\n</Pitfall>\n"
  },
  {
    "path": "src/content/reference/react/useInsertionEffect.md",
    "content": "---\ntitle: useInsertionEffect\n---\n\n<Pitfall>\n\n`useInsertionEffect`는 CSS-in-JS 라이브러리 작성자를 위한 것입니다. CSS-in-JS 라이브러리 작업 중에 스타일을 주입할 위치가 필요한 것이 아니라면, [`useEffect`](/reference/react/useEffect) 또는 [`useLayoutEffect`](/reference/react/useLayoutEffect)를 사용하세요.\n\n</Pitfall>\n\n<Intro>\n\n`useInsertionEffect`는 layout Effects 가 실행되기 전에 전체 요소를 DOM 에 주입 할 수 있습니다.\n\n```js\nuseInsertionEffect(setup, dependencies?)\n```\n\n</Intro>\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `useInsertionEffect(setup, dependencies?)` {/*useinsertioneffect*/}\n\n`useInsertionEffect`를 호출하여 layout 을 읽어야하는 Effects 가 호출 되기 전에 스타일을 주입할 수 있습니다.\n\n```js\nimport { useInsertionEffect } from 'react';\n\n// CSS-in-JS 라이브러리 안에서\nfunction useCSS(rule) {\n  useInsertionEffect(() => {\n    // ... <style> 태그를 여기에서 주입하세요 ...\n  });\n  return rule;\n}\n```\n\n[더 많은 예시 보기](#usage)\n\n#### 매개변수 {/*parameters*/}\n\n* `setup`: Effects 의 로직이 포함된 함수입니다. setup 함수는 선택적으로 *cleanup* 함수를 반환할 수도 있습니다. 컴포넌트가 DOM에 추가되기 전에, layout Effects 가 실행되기 전에, React는 setup 함수를 실행합니다. `dependencies`가 변경되어 다시 렌더링할 때마다, React는 먼저 이전 값으로 cleanup 함수(제공한 경우)를 실행한 다음 새 값으로 setup 함수를 실행합니다. 컴포넌트가 DOM에서 제거되기 전에 React는 cleanup 함수를 한 번 더 실행합니다.\n\n* **선택사항** `dependencies`: `setup` 코드 내에서 참조된 모든 반응형 값의 목록입니다. 반응형 값에는 props, state, 그리고 컴포넌트 본문에 직접 선언된 모든 변수와 함수가 포함됩니다. linter가 [React용으로 설정된](/learn/editor-setup#linting) 경우, 모든 반응형 값이 의존성으로 올바르게 지정되었는지 확인합니다. 의존성 목록에는 일정한 수의 항목이 있어야 하며 `[dep1, dep2, dep3]`와 같이 작성해야 합니다. React는 [`Object.is`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is) 비교 알고리즘을 사용하여 각 의존성을 이전 값과 비교합니다. 의존성을 전혀 지정하지 않으면 컴포넌트를 다시 렌더링할 때마다 Effect가 다시 실행됩니다.\n\n#### 반환값 {/*returns*/}\n\n`useInsertionEffect`는 `undefined`를 반환합니다.\n\n#### 주의 사항 {/*caveats*/}\n\n* 이펙트는 클라이언트에서만 실행됩니다. 서버 렌더링 중에는 실행되지 않습니다.\n* `useInsertionEffect` 내부에서는 상태를 업데이트할 수 없습니다.\n* `useInsertionEffect`가 실행되는 시점에 ref는 아직 연결되지 않습니다.\n* `useInsertionEffect` 는 DOM 의 업데이트 전 또는 후에 실행됩니다. DOM 이 업데이트 되는 특정시점에 의존해서는 안됩니다.\n* 매번 모든 cleanup 을 실행하고 setup 하는 다른 Effects 와 달리, `useInsertionEffect` 는 하나의 컴포넌트에 대해 cleanup 과 setup 을 모두 실행합니다. 그 결과 cleanup 과 setup 이 'interleaving' 됩니다.\n\n---\n\n## 사용법 {/*usage*/}\n\n### CSS-in-JS 라이브러리에서 동적 스타일 주입하기 {/*injecting-dynamic-styles-from-css-in-js-libraries*/}\n\n전통적으로 plain CSS를 사용해 React 컴포넌트의 스타일을 지정했습니다.\n\n```js\n// JS 파일 안에서\n<button className=\"success\" />\n\n// CSS 파일 안에서\n.success { color: green; }\n```\n\n일부 팀은 CSS 파일을 작성하는 대신 자바스크립트 코드에서 직접 스타일을 작성하는 것을 선호합니다. 이 경우 일반적으로 CSS-in-JS 라이브러리 또는 도구를 사용해야 합니다. CSS-in-JS에는 세 가지 일반적인 접근 방식이 있습니다:\n\n1. 컴파일러를 사용하여 CSS 파일로 정적 추출\n2. 인라인 스타일, 예: `<div style={{ opacity: 1 }}>`\n3. 런타임에 `<style>` 태그 주입\n\nCSS-in-JS를 사용하는 경우 처음 두 가지 접근 방식(정적 스타일의 경우 CSS 파일, 동적 스타일의 경우 인라인 스타일)을 조합하여 사용하는 것이 좋습니다. **런타임 `<style>` 태그 주입은 다음 두 가지 이유로 권장하지 않습니다.**\n\n1. 런타임 주입은 브라우저에서 스타일을 훨씬 더 자주 다시 계산하도록 합니다.\n2. 런타임 주입이 React 생명주기 중에 잘못된 시점에 발생하면 속도가 매우 느려질 수 있습니다.\n\n첫 번째 문제는 해결할 수 없지만 `useInsertionEffect`를 사용하면 두 번째 문제를 해결할 수 있습니다.\n\n`useInsertionEffect`를 호출하여 layout Effects 가 발생하기 전에 스타일을 주입합니다:\n\n```js {4-11}\n// CSS-in-JS 라이브러리 안에서\nlet isInserted = new Set();\nfunction useCSS(rule) {\n  useInsertionEffect(() => {\n    // 앞서 설명했듯이 <style> 태그의 런타임 주입은 권장하지 않습니다.\n    // 하지만 꼭 주입해야 한다면 useInsertionEffect에서 주입하는 것이 중요합니다.\n    if (!isInserted.has(rule)) {\n      isInserted.add(rule);\n      document.head.appendChild(getStyleForRule(rule));\n    }\n  });\n  return rule;\n}\n\nfunction Button() {\n  const className = useCSS('...');\n  return <div className={className} />;\n}\n```\n\n`useEffect`와 마찬가지로 `useInsertionEffect`는 서버에서 실행되지 않습니다. 서버에서 어떤 CSS 규칙이 사용되었는지 수집해야 하는 경우 렌더링 중에 수집할 수 있습니다:\n\n```js {1,4-6}\nlet collectedRulesSet = new Set();\n\nfunction useCSS(rule) {\n  if (typeof window === 'undefined') {\n    collectedRulesSet.add(rule);\n  }\n  useInsertionEffect(() => {\n    // ...\n  });\n  return rule;\n}\n```\n\n[런타임 인젝션이 있는 CSS-in-JS 라이브러리를 `useInsertionEffect`로 업그레이드하는 방법에 대해 자세히 알아보세요.](https://github.com/reactwg/react-18/discussions/110)\n\n<DeepDive>\n\n#### 이것이 렌더링 중에 스타일을 주입하거나 useLayoutEffect를 사용하는 것보다 어떻게 더 나은가요? {/*how-is-this-better-than-injecting-styles-during-rendering-or-uselayouteffect*/}\n\nIf you insert styles during rendering and React is processing a [non-blocking update,](/reference/react/useTransition#perform-non-blocking-updates-with-actions) the browser will recalculate the styles every single frame while rendering a component tree, which can be **extremely slow.**\n\n`useInsertionEffect`는 컴포넌트에서 다른 Effect가 실행될 때 `<style>` 태그가 주입되어 있음을 보장하기 때문에 [`useLayoutEffect`](/reference/react/useLayoutEffect) 또는 [`useEffect`](/reference/react/useEffect)로 스타일을 주입하는 것보다 낫습니다. 그렇지 않으면 오래된 스타일로 인해 일반 Effects의 레이아웃 계산이 잘못될 수 있습니다.\n\n</DeepDive>\n"
  },
  {
    "path": "src/content/reference/react/useLayoutEffect.md",
    "content": "---\ntitle: useLayoutEffect\n---\n\n<Pitfall>\n\n`useLayoutEffect`를 사용하면 성능이 저하될 수 있습니다. 가능하다면 [`useEffect`](/reference/react/useEffect)를 사용하세요.\n\n</Pitfall>\n\n<Intro>\n\n`useLayoutEffect`는 브라우저가 화면을 다시 그리기 전에 실행되는 [`useEffect`](/reference/react/useEffect)입니다.\n\n\n```js\nuseLayoutEffect(setup, dependencies?)\n```\n\n</Intro>\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `useLayoutEffect(setup, dependencies?)` {/*useinsertioneffect*/}\n\n`useLayoutEffect`를 호출하여 브라우저가 화면을 다시 그리기 전에 레이아웃을 계산합니다.\n\n```js\nimport { useState, useRef, useLayoutEffect } from 'react';\n\nfunction Tooltip() {\n  const ref = useRef(null);\n  const [tooltipHeight, setTooltipHeight] = useState(0);\n\n  useLayoutEffect(() => {\n    const { height } = ref.current.getBoundingClientRect();\n    setTooltipHeight(height);\n  }, []);\n  // ...\n```\n\n\n[아래에서 더 많은 예시를 확인하세요.](#usage)\n\n#### 매개변수 {/*parameters*/}\n\n* `setup`: The function with your Effect's logic. Your setup function may also optionally return a *cleanup* function. Before your [component commits](/learn/render-and-commit#step-3-react-commits-changes-to-the-dom), React will run your setup function. After every commit with changed dependencies, React will first run the cleanup function (if you provided it) with the old values, and then run your setup function with the new values. Before your component is removed from the DOM, React will run your cleanup function.\n\n* **optional** `dependencies`: The list of all reactive values referenced inside of the `setup` code. Reactive values include props, state, and all the variables and functions declared directly inside your component body. If your linter is [configured for React](/learn/editor-setup#linting), it will verify that every reactive value is correctly specified as a dependency. The list of dependencies must have a constant number of items and be written inline like `[dep1, dep2, dep3]`. React will compare each dependency with its previous value using the [`Object.is`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is) comparison. If you omit this argument, your Effect will re-run after every commit of the component.\n\n* **선택사항** `dependencies`: `setup`코드 내에서 참조된 모든 반응형 값의 목록입니다. 반응형 값에는 props, state, 그리고 컴포넌트 본문에 직접 선언된 모든 변수와 함수가 포함됩니다. linter가 [React용으로 설정된](/learn/editor-setup#linting) 경우, 모든 반응형 값이 의존성으로 올바르게 지정되었는지 확인합니다. 의존성 목록에는 일정한 수의 항목이 있어야 하며 `[dep1, dep2, dep3]`와 같이 작성해야 합니다. React는 [`Object.is`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is) 비교 알고리즘을 사용하여 각 의존성을 이전 값과 비교합니다. 의존성을 전혀 지정하지 않으면 컴포넌트를 다시 렌더링할 때마다 Effect가 다시 실행됩니다.\n\n#### 반환값 {/*returns*/}\n\n`useLayoutEffect`는 `undefined`를 반환합니다.\n\n#### 주의 사항 {/*caveats*/}\n\n* `useLayoutEffect`는 Hook이므로, **컴포넌트의 최상위 레벨** 또는 커스텀 Hook에서만 호출할 수 있습니다. 반복문이나 조건문 내에서 호출할 수 없습니다. 이 작업이 필요하다면 새로운 컴포넌트로 분리해서 Effect를 새 컴포넌트로 옮기세요.\n\n* Strict Mode가 켜져 있으면, React는 실제 첫 번째 setup 함수가 실행되기 이전에 **개발 모드에만 한정하여 한 번의 추가적인 setup + cleanup 사이클을 실행합니다.** 이는 cleanup 로직이 setup 로직을 완벽히 \"반영\"하고, setup 로직이 수행하는 작업을 중단하거나 되돌리는 지를 확인하는 스트레스 테스트입니다. 이로 인해 문제가 발생하면 [cleanup 함수를 구현하세요.](/learn/synchronizing-with-effects#how-to-handle-the-effect-firing-twice-in-development)\n\n* 의존성 중에 컴포넌트 내부에서 정의된 객체나 함수가 있는 경우, **Effect 가 필요 이상으로 다시 실행될 위험이 있습니다.** 이를 해결하려면 불필요한 [객체 의존성](/reference/react/useEffect#removing-unnecessary-object-dependencies)이나 [함수 의존성](/reference/react/useEffect#removing-unnecessary-function-dependencies)을 제거하세요. [State 업데이트](/reference/react/useEffect#updating-state-based-on-previous-state-from-an-effect)나  [비 반응형 로직](/reference/react/useEffect#reading-the-latest-props-and-state-from-an-effect)을 effect 밖으로 빼낼 수 도 있습니다.\n\n* Effect는 **클라이언트 환경에서만 동작합니다.** 서버 렌더링 중에는 실행되지 않습니다.\n\n*  `useLayoutEffect` 내부의 코드와 이로 인한 모든 state 업데이트는 **브라우저가 화면을 다시 그리는 것을 막습니다.** 과도하게 사용하면 앱이 느려집니다. 가능하면 [`useEffect`](/reference/react/useEffect)를 사용하세요.\n\n* `useLayoutEffect` 내부에서 state 업데이트를 실행하면 React는 `useEffect`를 포함한 나머지 모든 Effect를 즉시 실행합니다.\n\n---\n\n## 사용법 {/*usage*/}\n\n### 브라우저가 화면을 다시 그리기 전에 레이아웃 계산하기 {/*measuring-layout-before-the-browser-repaints-the-screen*/}\n\n대부분의 컴포넌트는 렌더링을 위해 해당 컴포넌트의 화면상 위치와 크기를 알 필요가 없습니다. 컴포넌트가 JSX를 반환하면 브라우저가 컴포넌트의 *레이아웃*(위치와 크기)를 계산하고 화면을 다시 그립니다.\n\n가끔은 이것만으로는 부족한 경우가 있습니다. 마우스 커서를 올리면 툴팁이 요소 옆에 나타나는 경우를 생각해 보세요. 충분한 공간이 있다면 툴팁은 요소 위에 나타나겠지만, 공간이 부족하다면 아래에 나타나야 합니다. 결국 툴팁을 올바른 위치에 렌더링하려면 툴팁의 높이를 알아야 합니다. (위쪽 공간에 들어가는지 판단 해야 함)\n\n이를 위해 두 번의 렌더링을 거쳐야 합니다.\n\n1. 툴팁을 (잘못된 위치라도) 아무 위치에 렌더링합니다\n2. 툴팁의 높이를 계산해서 툴팁을 배치할 위치를 결정합니다.\n3. 올바른 위치에 툴팁을 *다시* 렌더링합니다.\n\n**이 작업은 브라우저가 화면을 다시 그리기 전에 모두 이루어져야 합니다.** 툴팁이 움직이는 걸 사용자에게 보이고 싶지 않으니까요. `useLayoutEffect`를 호출해서 브라우저가 화면을 다시 그리기 전에 레이아웃을 계산하세요.\n\n```js {5-8}\nfunction Tooltip() {\n  const ref = useRef(null);\n  const [tooltipHeight, setTooltipHeight] = useState(0); // 아직 실제 높이를 모릅니다.\n\n  useLayoutEffect(() => {\n    const { height } = ref.current.getBoundingClientRect();\n    setTooltipHeight(height); // 실제 높이를 알았으니 다시 렌더링합니다.\n  }, []);\n\n  // ...아래에 올 렌더링 로직에서 tooltipHeight를 사용하세요...\n}\n```\n\n작동 방식을 단계별로 알아봅시다.\n\n1. `Tooltip` 은 초기화된 값인 `tooltipHeight = 0`으로 렌더링 됩니다 (따라서 툴팁의 위치는 잘못될 수 있습니다).\n2. React가 이 툴팁을 DOM에 배치하고 `useLayoutEffect` 안의 코드를 실행합니다.\n3. `useLayoutEffect`가 툴팁의 [높이를 계산하고](https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect) 바로 다시 렌더링시킵니다.\n4. `Tooltip` 이 실제 `tooltipHeight`로 렌더링 됩니다. (따라서 툴팁이 올바른 위치에 배치됩니다.)\n5. React가 DOM에서 이를 업데이트하고 마침내 브라우저가 툴팁을 표시합니다.\n\n아래의 버튼들 위로 마우스 커서를 올려서 툴팁이 공간에 들어가는지에 따라 위치를 조정하는 것을 확인하세요.\n\n<Sandpack>\n\n```js\nimport ButtonWithTooltip from './ButtonWithTooltip.js';\n\nexport default function App() {\n  return (\n    <div>\n      <ButtonWithTooltip\n        tooltipContent={\n          <div>\n            This tooltip does not fit above the button.\n            <br />\n            This is why it's displayed below instead!\n          </div>\n        }\n      >\n        Hover over me (tooltip above)\n      </ButtonWithTooltip>\n      <div style={{ height: 50 }} />\n      <ButtonWithTooltip\n        tooltipContent={\n          <div>This tooltip fits above the button</div>\n        }\n      >\n        Hover over me (tooltip below)\n      </ButtonWithTooltip>\n      <div style={{ height: 50 }} />\n      <ButtonWithTooltip\n        tooltipContent={\n          <div>This tooltip fits above the button</div>\n        }\n      >\n        Hover over me (tooltip below)\n      </ButtonWithTooltip>\n    </div>\n  );\n}\n```\n\n```js src/ButtonWithTooltip.js\nimport { useState, useRef } from 'react';\nimport Tooltip from './Tooltip.js';\n\nexport default function ButtonWithTooltip({ tooltipContent, ...rest }) {\n  const [targetRect, setTargetRect] = useState(null);\n  const buttonRef = useRef(null);\n  return (\n    <>\n      <button\n        {...rest}\n        ref={buttonRef}\n        onPointerEnter={() => {\n          const rect = buttonRef.current.getBoundingClientRect();\n          setTargetRect({\n            left: rect.left,\n            top: rect.top,\n            right: rect.right,\n            bottom: rect.bottom,\n          });\n        }}\n        onPointerLeave={() => {\n          setTargetRect(null);\n        }}\n      />\n      {targetRect !== null && (\n        <Tooltip targetRect={targetRect}>\n          {tooltipContent}\n        </Tooltip>\n      )\n    }\n    </>\n  );\n}\n```\n\n```js src/Tooltip.js active\nimport { useRef, useLayoutEffect, useState } from 'react';\nimport { createPortal } from 'react-dom';\nimport TooltipContainer from './TooltipContainer.js';\n\nexport default function Tooltip({ children, targetRect }) {\n  const ref = useRef(null);\n  const [tooltipHeight, setTooltipHeight] = useState(0);\n\n  useLayoutEffect(() => {\n    const { height } = ref.current.getBoundingClientRect();\n    setTooltipHeight(height);\n    console.log('Measured tooltip height: ' + height);\n  }, []);\n\n  let tooltipX = 0;\n  let tooltipY = 0;\n  if (targetRect !== null) {\n    tooltipX = targetRect.left;\n    tooltipY = targetRect.top - tooltipHeight;\n    if (tooltipY < 0) {\n      // 위쪽 공간에 들어가지 못하므로 아래에 배치합니다.\n      tooltipY = targetRect.bottom;\n    }\n  }\n\n  return createPortal(\n    <TooltipContainer x={tooltipX} y={tooltipY} contentRef={ref}>\n      {children}\n    </TooltipContainer>,\n    document.body\n  );\n}\n```\n\n```js src/TooltipContainer.js\nexport default function TooltipContainer({ children, x, y, contentRef }) {\n  return (\n    <div\n      style={{\n        position: 'absolute',\n        pointerEvents: 'none',\n        left: 0,\n        top: 0,\n        transform: `translate3d(${x}px, ${y}px, 0)`\n      }}\n    >\n      <div ref={contentRef} className=\"tooltip\">\n        {children}\n      </div>\n    </div>\n  );\n}\n```\n\n```css\n.tooltip {\n  color: white;\n  background: #222;\n  border-radius: 4px;\n  padding: 4px;\n}\n```\n\n</Sandpack>\n\n`Tooltip` 컴포넌트는 두 번의 렌더링을 거치지만 (처음은 `0`으로 초기화된 `tooltipHeight`로 렌더링 되고, 그다음 실제로 계산된 높이로 렌더링 됨), 실제로 보이는 건 최종 결과뿐입니다. 이 예시에서 [`useEffect`](/reference/react/useEffect) 대신 `useLayoutEffect`가 필요한 이유입니다. 아래에서 차이점을 자세하게 살펴봅시다.\n\n<Recipes titleText=\"useLayoutEffect vs useEffect\" titleId=\"examples\">\n\n#### `useLayoutEffect` 는 브라우저가 화면을 다시 그리는 것을 막습니다 {/*uselayouteffect-blocks-the-browser-from-repainting*/}\n\nReact는 `useLayoutEffect` 내부의 코드와 이로 인한 모든 state 업데이트가 **브라우저가 화면을 다시 그리기 전에** 처리되는 것을 보장합니다. 덕분에 툴팁을 렌더링하고, 위치와 크기를 계산하고 다시 렌더링하면서 첫 번째 렌더링은 사용자가 모르게 할 수 있습니다. 즉, `useLayoutEffect`는 브라우저가 화면을 그리는 것을 막습니다.\n\n<Sandpack>\n\n```js\nimport ButtonWithTooltip from './ButtonWithTooltip.js';\n\nexport default function App() {\n  return (\n    <div>\n      <ButtonWithTooltip\n        tooltipContent={\n          <div>\n            This tooltip does not fit above the button.\n            <br />\n            This is why it's displayed below instead!\n          </div>\n        }\n      >\n        Hover over me (tooltip above)\n      </ButtonWithTooltip>\n      <div style={{ height: 50 }} />\n      <ButtonWithTooltip\n        tooltipContent={\n          <div>This tooltip fits above the button</div>\n        }\n      >\n        Hover over me (tooltip below)\n      </ButtonWithTooltip>\n      <div style={{ height: 50 }} />\n      <ButtonWithTooltip\n        tooltipContent={\n          <div>This tooltip fits above the button</div>\n        }\n      >\n        Hover over me (tooltip below)\n      </ButtonWithTooltip>\n    </div>\n  );\n}\n```\n\n```js src/ButtonWithTooltip.js\nimport { useState, useRef } from 'react';\nimport Tooltip from './Tooltip.js';\n\nexport default function ButtonWithTooltip({ tooltipContent, ...rest }) {\n  const [targetRect, setTargetRect] = useState(null);\n  const buttonRef = useRef(null);\n  return (\n    <>\n      <button\n        {...rest}\n        ref={buttonRef}\n        onPointerEnter={() => {\n          const rect = buttonRef.current.getBoundingClientRect();\n          setTargetRect({\n            left: rect.left,\n            top: rect.top,\n            right: rect.right,\n            bottom: rect.bottom,\n          });\n        }}\n        onPointerLeave={() => {\n          setTargetRect(null);\n        }}\n      />\n      {targetRect !== null && (\n        <Tooltip targetRect={targetRect}>\n          {tooltipContent}\n        </Tooltip>\n      )\n    }\n    </>\n  );\n}\n```\n\n```js src/Tooltip.js active\nimport { useRef, useLayoutEffect, useState } from 'react';\nimport { createPortal } from 'react-dom';\nimport TooltipContainer from './TooltipContainer.js';\n\nexport default function Tooltip({ children, targetRect }) {\n  const ref = useRef(null);\n  const [tooltipHeight, setTooltipHeight] = useState(0);\n\n  useLayoutEffect(() => {\n    const { height } = ref.current.getBoundingClientRect();\n    setTooltipHeight(height);\n  }, []);\n\n  let tooltipX = 0;\n  let tooltipY = 0;\n  if (targetRect !== null) {\n    tooltipX = targetRect.left;\n    tooltipY = targetRect.top - tooltipHeight;\n    if (tooltipY < 0) {\n      // 위쪽 공간에 들어가지 못하므로 아래에 배치합니다.\n      tooltipY = targetRect.bottom;\n    }\n  }\n\n  return createPortal(\n    <TooltipContainer x={tooltipX} y={tooltipY} contentRef={ref}>\n      {children}\n    </TooltipContainer>,\n    document.body\n  );\n}\n```\n\n```js src/TooltipContainer.js\nexport default function TooltipContainer({ children, x, y, contentRef }) {\n  return (\n    <div\n      style={{\n        position: 'absolute',\n        pointerEvents: 'none',\n        left: 0,\n        top: 0,\n        transform: `translate3d(${x}px, ${y}px, 0)`\n      }}\n    >\n      <div ref={contentRef} className=\"tooltip\">\n        {children}\n      </div>\n    </div>\n  );\n}\n```\n\n```css\n.tooltip {\n  color: white;\n  background: #222;\n  border-radius: 4px;\n  padding: 4px;\n}\n```\n\n</Sandpack>\n\n<Solution />\n\n#### `useEffect` 는 브라우저를 막지 않습니다 {/*useeffect-does-not-block-the-browser*/}\n\n`useLayoutEffect` 대신 [`useEffect`](/reference/react/useEffect)를 사용한 동일한 예시입니다. 더 느린 디바이스 환경이라면 가끔 툴팁이 \"깜빡\"일 수 있고 수정되기 전의 초기 위치를 잠깐 보게 될 수 있습니다.\n\n<Sandpack>\n\n```js\nimport ButtonWithTooltip from './ButtonWithTooltip.js';\n\nexport default function App() {\n  return (\n    <div>\n      <ButtonWithTooltip\n        tooltipContent={\n          <div>\n            This tooltip does not fit above the button.\n            <br />\n            This is why it's displayed below instead!\n          </div>\n        }\n      >\n        Hover over me (tooltip above)\n      </ButtonWithTooltip>\n      <div style={{ height: 50 }} />\n      <ButtonWithTooltip\n        tooltipContent={\n          <div>This tooltip fits above the button</div>\n        }\n      >\n        Hover over me (tooltip below)\n      </ButtonWithTooltip>\n      <div style={{ height: 50 }} />\n      <ButtonWithTooltip\n        tooltipContent={\n          <div>This tooltip fits above the button</div>\n        }\n      >\n        Hover over me (tooltip below)\n      </ButtonWithTooltip>\n    </div>\n  );\n}\n```\n\n```js src/ButtonWithTooltip.js\nimport { useState, useRef } from 'react';\nimport Tooltip from './Tooltip.js';\n\nexport default function ButtonWithTooltip({ tooltipContent, ...rest }) {\n  const [targetRect, setTargetRect] = useState(null);\n  const buttonRef = useRef(null);\n  return (\n    <>\n      <button\n        {...rest}\n        ref={buttonRef}\n        onPointerEnter={() => {\n          const rect = buttonRef.current.getBoundingClientRect();\n          setTargetRect({\n            left: rect.left,\n            top: rect.top,\n            right: rect.right,\n            bottom: rect.bottom,\n          });\n        }}\n        onPointerLeave={() => {\n          setTargetRect(null);\n        }}\n      />\n      {targetRect !== null && (\n        <Tooltip targetRect={targetRect}>\n          {tooltipContent}\n        </Tooltip>\n      )\n    }\n    </>\n  );\n}\n```\n\n```js src/Tooltip.js active\nimport { useRef, useEffect, useState } from 'react';\nimport { createPortal } from 'react-dom';\nimport TooltipContainer from './TooltipContainer.js';\n\nexport default function Tooltip({ children, targetRect }) {\n  const ref = useRef(null);\n  const [tooltipHeight, setTooltipHeight] = useState(0);\n\n  useEffect(() => {\n    const { height } = ref.current.getBoundingClientRect();\n    setTooltipHeight(height);\n  }, []);\n\n  let tooltipX = 0;\n  let tooltipY = 0;\n  if (targetRect !== null) {\n    tooltipX = targetRect.left;\n    tooltipY = targetRect.top - tooltipHeight;\n    if (tooltipY < 0) {\n      // 위쪽 공간에 들어가지 못하므로 아래에 배치합니다.\n      tooltipY = targetRect.bottom;\n    }\n  }\n\n  return createPortal(\n    <TooltipContainer x={tooltipX} y={tooltipY} contentRef={ref}>\n      {children}\n    </TooltipContainer>,\n    document.body\n  );\n}\n```\n\n```js src/TooltipContainer.js\nexport default function TooltipContainer({ children, x, y, contentRef }) {\n  return (\n    <div\n      style={{\n        position: 'absolute',\n        pointerEvents: 'none',\n        left: 0,\n        top: 0,\n        transform: `translate3d(${x}px, ${y}px, 0)`\n      }}\n    >\n      <div ref={contentRef} className=\"tooltip\">\n        {children}\n      </div>\n    </div>\n  );\n}\n```\n\n```css\n.tooltip {\n  color: white;\n  background: #222;\n  border-radius: 4px;\n  padding: 4px;\n}\n```\n\n</Sandpack>\n\n버그를 더 쉽게 재현하기 위해 렌더링 중에 인위적인 지연을 추가한 버전입니다. React는 `useEffect` 내부의 state 업데이트를 처리하기 전에 브라우저가 화면을 그리도록 할 것입니다. 결과적으로 툴팁이 \"깜빡\"입니다.\n\n<Sandpack>\n\n```js\nimport ButtonWithTooltip from './ButtonWithTooltip.js';\n\nexport default function App() {\n  return (\n    <div>\n      <ButtonWithTooltip\n        tooltipContent={\n          <div>\n            This tooltip does not fit above the button.\n            <br />\n            This is why it's displayed below instead!\n          </div>\n        }\n      >\n        Hover over me (tooltip above)\n      </ButtonWithTooltip>\n      <div style={{ height: 50 }} />\n      <ButtonWithTooltip\n        tooltipContent={\n          <div>This tooltip fits above the button</div>\n        }\n      >\n        Hover over me (tooltip below)\n      </ButtonWithTooltip>\n      <div style={{ height: 50 }} />\n      <ButtonWithTooltip\n        tooltipContent={\n          <div>This tooltip fits above the button</div>\n        }\n      >\n        Hover over me (tooltip below)\n      </ButtonWithTooltip>\n    </div>\n  );\n}\n```\n\n```js src/ButtonWithTooltip.js\nimport { useState, useRef } from 'react';\nimport Tooltip from './Tooltip.js';\n\nexport default function ButtonWithTooltip({ tooltipContent, ...rest }) {\n  const [targetRect, setTargetRect] = useState(null);\n  const buttonRef = useRef(null);\n  return (\n    <>\n      <button\n        {...rest}\n        ref={buttonRef}\n        onPointerEnter={() => {\n          const rect = buttonRef.current.getBoundingClientRect();\n          setTargetRect({\n            left: rect.left,\n            top: rect.top,\n            right: rect.right,\n            bottom: rect.bottom,\n          });\n        }}\n        onPointerLeave={() => {\n          setTargetRect(null);\n        }}\n      />\n      {targetRect !== null && (\n        <Tooltip targetRect={targetRect}>\n          {tooltipContent}\n        </Tooltip>\n      )\n    }\n    </>\n  );\n}\n```\n\n```js src/Tooltip.js active\nimport { useRef, useEffect, useState } from 'react';\nimport { createPortal } from 'react-dom';\nimport TooltipContainer from './TooltipContainer.js';\n\nexport default function Tooltip({ children, targetRect }) {\n  const ref = useRef(null);\n  const [tooltipHeight, setTooltipHeight] = useState(0);\n\n  // 렌더링을 인위적으로 느리게 합니다.\n  let now = performance.now();\n  while (performance.now() - now < 100) {\n    // 잠시 아무것도 하지 않는 중 ...\n  }\n\n  useEffect(() => {\n    const { height } = ref.current.getBoundingClientRect();\n    setTooltipHeight(height);\n  }, []);\n\n  let tooltipX = 0;\n  let tooltipY = 0;\n  if (targetRect !== null) {\n    tooltipX = targetRect.left;\n    tooltipY = targetRect.top - tooltipHeight;\n    if (tooltipY < 0) {\n      // 위쪽 공간에 들어가지 못하므로 아래에 배치합니다.\n      tooltipY = targetRect.bottom;\n    }\n  }\n\n  return createPortal(\n    <TooltipContainer x={tooltipX} y={tooltipY} contentRef={ref}>\n      {children}\n    </TooltipContainer>,\n    document.body\n  );\n}\n```\n\n```js src/TooltipContainer.js\nexport default function TooltipContainer({ children, x, y, contentRef }) {\n  return (\n    <div\n      style={{\n        position: 'absolute',\n        pointerEvents: 'none',\n        left: 0,\n        top: 0,\n        transform: `translate3d(${x}px, ${y}px, 0)`\n      }}\n    >\n      <div ref={contentRef} className=\"tooltip\">\n        {children}\n      </div>\n    </div>\n  );\n}\n```\n\n```css\n.tooltip {\n  color: white;\n  background: #222;\n  border-radius: 4px;\n  padding: 4px;\n}\n```\n\n</Sandpack>\n\n이 예시를 `useLayoutEffect`로 고쳐서 렌더링은 느려지더라도 브라우저가 화면을 그리는 것을 막는 것을 확인하세요.\n\n<Solution />\n\n</Recipes>\n\n<Note>\n\n두 번에 걸쳐서 렌더링하고 브라우저를 막는 것은 성능을 저하합니다. 가능하면 피하세요.\n\n</Note>\n\n---\n\n## 문제 해결 {/*troubleshooting*/}\n\n### 오류가 발생했습니다: \"`useLayoutEffect` does nothing on the server\" {/*im-getting-an-error-uselayouteffect-does-nothing-on-the-server*/}\n\n`useLayoutEffect`의 목적은 [레이아웃 정보를 사용해서](#measuring-layout-before-the-browser-repaints-the-screen) 컴포넌트를 렌더링하는 것입니다.\n\n1. 초기 콘텐츠를 렌더링합니다.\n2. *브라우저가 화면을 다시 그리기 전에* 레이아웃을 계산합니다.\n3. 읽은 레이아웃 정보를 사용해서 최종 콘텐츠를 렌더링합니다.\n\n[서버 렌더링](/reference/react-dom/server)을 직접 사용하거나 프레임워크에서 사용하는 경우라면, React 앱은 서버에서 초기 렌더링을 해서 HTML을 만듭니다. 이를 통해 JavaScript 코드가 로드되기 전에 초기 HTML을 보여주게 됩니다.\n\n문제는 서버에는 레이아웃 정보가 없다는 것입니다.\n\n[앞선 예시](#measuring-layout-before-the-browser-repaints-the-screen)에선 `Tooltip` 컴포넌트에서 `useLayoutEffect`를 호출하여 툴팁을 콘텐츠의 높이에 따라 (콘텐츠의 위쪽과 아래쪽 중) 올바른 위치에 배치합니다. 초기 서버 HTML의 일부로 `Tooltip`을 렌더링하려 하면, 이때는 툴팁의 위치를 올바르게 결정할 수 없을 것입니다. 서버에서는 아직 레이아웃 정보가 없으니까요! 따라서 툴팁을 서버에서 렌더링하기를 원하더라도, 클라이언트로 옮겨서 자바스크립트가 로드되고 실행된 후에 렌더링해야 합니다.\n\n일반적으로 레이아웃 정보에 의존하는 컴포넌트는 어차피 서버에서 렌더링할 필요가 없습니다. 예를 들면, 초기 렌더링 중에 `Tooltip`이 보이는 것은 말이 안 됩니다. `Tooltip`은 클라이언트 상호작용에 의해서 보이는 것이니까요.\n\n그럼에도 이런 문제를 마주친다면 몇 가지 다른 옵션이 있습니다.\n\n- `useLayoutEffect`를 [`useEffect`](/reference/react/useEffect)로 대체 하세요. 화면을 그리는 것을 막지 말고 (초기 HTML이 Effect 실행 전에 보이기 때문에) 초기 렌더링이 보이더라도 괜찮다고 React에게 말해주는 것입니다.\n\n- 또는 [해당 컴포넌트를 클라이언트 전용으로 만드세요.](/reference/react/Suspense#providing-a-fallback-for-server-errors-and-client-only-content) React가 가장 가까운 [`<Suspense>`](/reference/react/Suspense) 경계 안의 콘텐츠를 서버렌더링 동안 (스피너나 글리머같은) loading fallbck으로 대체 하게 합니다.\n\n- 또는 `useLayoutEffect`가 있는 컴포넌트를 hydration 이후에만 렌더링할 수도 있습니다. 불리언 타입인 `isMounted` state를 초깃값인 `false`로 유지하다가, `useEffect` 호출되면 거기서 `true`로 값을 변경하세요. 그러면 렌더링 로직은 `return isMounted ? <RealContent /> : <FallbackContent />` 처럼 될 수 있습니다. 서버에서 렌더링하는 중이거나 hydration 동안 사용자는 `FallbackContent`를 볼 것이고 `FallbackContent`는 `useLayoutEffect`를 호출하지 않아야 합니다. 그 후에 React가 `FallbackContent`를 클라이언트 전용이면서 `useLayoutEffect`를 호출하는 `RealContent`로 변경할 겁니다.\n\n- 컴포넌트를 외부 데이터 저장소와 동기화하고, 레이아웃 계산 외에 다른 이유로 `useLayoutEffect`에 의존하는 경우라면, 대신 [`useSyncExternalStore`](/reference/react/useSyncExternalStore)를 고려해 보세요. 이 Hook은 [서버 렌더링을 지원합니다.](/reference/react/useSyncExternalStore#adding-support-for-server-rendering)\n"
  },
  {
    "path": "src/content/reference/react/useMemo.md",
    "content": "---\ntitle: useMemo\n---\n\n<Intro>\n\n`useMemo` 는 재렌더링 사이에 계산 결과를 캐싱할 수 있게 해주는 React Hook 입니다.\n\n```js\nconst cachedValue = useMemo(calculateValue, dependencies)\n```\n\n</Intro>\n\n<Note>\n\n[React 컴파일러](/learn/react-compiler)는 값과 함수를 자동으로 메모이제이션하므로 `useMemo`를 수동으로 사용할 일이 줄어듭니다. 컴파일러를 사용해 메모이제이션을 자동으로 처리할 수 있습니다.\n\n</Note>\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `useMemo(calculateValue, dependencies)` {/*usememo*/}\n\n컴포넌트의 최상위 레벨에 있는 'useMemo'를 호출하여 재렌더링 사이의 계산을 캐싱합니다.\n\n```js\nimport { useMemo } from 'react';\n\nfunction TodoList({ todos, tab }) {\n  const visibleTodos = useMemo(\n    () => filterTodos(todos, tab),\n    [todos, tab]\n  );\n  // ...\n}\n```\n\n[아래로 이동하여 더 많은 예시를 확인하세요.](#usage)\n\n#### 매개변수 {/*parameters*/}\n\n* `calculateValue`: 캐싱하려는 값을 계산하는 함수입니다. 순수해야 하며 인자를 받지 않고, 모든 타입의 값을 반환할 수 있어야 합니다. React는 초기 렌더링 중에 함수를 호출합니다. 다음 렌더링에서, React는 마지막 렌더링 이후 `dependencies`가 변경되지 않았을 때 동일한 값을 다시 반환합니다. 그렇지 않다면 `calculateValue`를 호출하고 결과를 반환하며, 나중에 재사용할 수 있도록 저장합니다.\n\n* `dependencies`: `calculateValue` 코드 내에서 참조된 모든 반응형 값들의 목록입니다. 반응형 값에는 props, state와 컴포넌트 바디에 직접 선언된 모든 변수와 함수가 포함됩니다. 만약 linter가 [React용으로 설정된 경우](/learn/editor-setup#linting) 모든 반응형 값이 의존성으로 올바르게 설정되었는지 확인할 수 있습니다. 의존성 목록은 일정한 수의 항목을 가져야 하며, `[dep1, dep2, dep3]`와 같이 인라인 형태로 작성돼야 합니다. React는 [`Object.is`](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Object/is) 비교를 통해 각 의존성 들을 이전 값과 비교합니다.\n\n#### 반환값 {/*returns*/}\n\n초기 렌더링에서 `useMemo`는 인자 없이 `calculateValue`를 호출한 결과를 반환합니다.\n\n다음 렌더링에서, 마지막 렌더링에서 저장된 값을 반환하거나(종속성이 변경되지 않은 경우), `calculateValue`를 다시 호출하고 반환된 값을 저장합니다.\n\n#### 주의 사항 {/*caveats*/}\n\n* `useMemo`는 Hook이므로  **컴포넌트의 최상위 레벨** 또는 자체 Hook에서만 호출할 수 있습니다. 반복문이나 조건문 내부에서는 호출할 수 없습니다. 만일 호출이 필요하다면 새 컴포넌트를 추출하고 상태를 그 안으로 옮겨야 합니다.\n* Strict Mode에서는 , React는 [실수로 발생한 오류를 찾기 위해](#my-calculation-runs-twice-on-every-re-render) **계산 함수를 두 번 호출합니다.** 이는 개발 환경에서만 동작하는 방식이며, 실제 프로덕션 환경에는 영향을 미치지 않습니다. (원래 그래야 하는 것처럼) 연산 함수가 순수하다면, 로직에는 영향을 미치지 않습니다. 호출 결과 중 하나는 무시됩니다.\n* React는 **캐싱 된 값을 버려야 할 특별한 이유가 없는 한 버리지 않습니다.** 예를 들어, 개발 단계에서 컴포넌트 파일을 편집할 때 React는 캐시를 버립니다. 개발과 프로덕션 환경 모두에서는 컴포넌트가 초기 마운트 중에 일시 중단되면 React는 캐시를 버립니다. 앞으로 React는 캐시를 버리는 것을 활용하는 더 많은 기능을 추가할 수 있습니다. 예를 들어, 앞으로 React에 가상화된 목록에 대한 기본적인 지원이 추가된다면 가상화된 테이블 뷰포트에서 스크롤 되는 항목에 대한 캐시를 버리는 것이 합리적일 것입니다. 이는 성능 최적화를 위해 `useMemo`에만 의존한다면 괜찮을 것입니다. 그러나 이는 [상태 변수](/reference/react/useState#avoiding-recreating-the-initial-state)나 [ref](/reference/react/useRef#avoiding-recreating-the-ref-contents)를 사용하는 것이 더 적합할 수 있습니다.\n\n<Note>\n\n이와 같이 반환값을 캐싱하는 것을 [*memoization*](https://ko.wikipedia.org/wiki/%EB%A9%94%EB%AA%A8%EC%9D%B4%EC%A0%9C%EC%9D%B4%EC%85%98)라고 하며, 이 훅을 `useMemo`라고 부르는 이유입니다.\n\n</Note>\n\n---\n\n## 사용법 {/*usage*/}\n\n### 비용이 높은 로직의 재계산 생략하기 {/*skipping-expensive-recalculations*/}\n\n재렌더링 사이에 계산을 캐싱하려면 컴포넌트의 최상위 레벨에서 `useMemo`를 호출하여 계산을 감싸면 됩니다.\n\n```js [[3, 4, \"visibleTodos\"], [1, 4, \"() => filterTodos(todos, tab)\"], [2, 4, \"[todos, tab]\"]]\nimport { useMemo } from 'react';\n\nfunction TodoList({ todos, tab, theme }) {\n  const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);\n  // ...\n}\n```\n\n`useMemo`에 두 가지를 전달해야 합니다.\n\n1. `() =>`와 같이 인수를 받지 않고 계산하려는 값을 반환하는 <CodeStep step={1}>계산 함수</CodeStep> 입니다.\n2. 계산 내부에서 사용되는 컴포넌트 내의 모든 값을 포함하는 <CodeStep step={2}>종속성 목록</CodeStep> 입니다.\n\n초기 렌더링에서 `useMemo`에서 얻을 수 있는 <CodeStep step={3}>값</CodeStep>은 <CodeStep step={1}>계산 함수</CodeStep>를 호출한 결과값 입니다.\n\n이후 모든 렌더링에서 React는 <CodeStep step={2}>종속성 목록을</CodeStep> 마지막 렌더링 중에 전달한 종속성 목록과 비교합니다. 만일 ([`Object.is`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is)와 비교했을 때) 종속성이 변경되지 않았다면, `useMemo`는 이전에 이미 계산해둔 값을 반환합니다. 그렇지 않다면 React는 계산을 다시 실행하고 새로운 값을 반환합니다.\n\n즉, `useMemo`는 종속성이 변경되기 전까지 재렌더링 사이의 계산 결과를 캐싱합니다.\n\n**이 기능이 언제 유용한지 예시를 통해 살펴보겠습니다.**\n\n기본적으로 React는 컴포넌트를 다시 렌더링할 때마다 컴포넌트의 전체 본문을 다시 실행합니다. 예를 들어, `TodoList`가 상태를 업데이트하거나 부모로부터 새로운 props를 받으면 `filterTodos` 함수가 다시 실행됩니다.\n\n```js {2}\nfunction TodoList({ todos, tab, theme }) {\n  const visibleTodos = filterTodos(todos, tab);\n  // ...\n}\n```\n\n일반적으로 대부분의 계산은 매우 빠르기 때문에 문제가 되지 않습니다. 그러나 큰 배열을 필터링 혹은 변환하거나 비용이 많이 드는 계산을 수행하는 경우, 데이터가 변경되지 않았다면 계산을 생략하는 것이 좋습니다. 만약 `todos`과 `tab`이 마지막 렌더링 때와 동일한 경우, 앞서 언급한 것처럼 `useMemo`로 계산을 감싸면 이전에 계산된 `visibleTodos`를 재사용할 수 있습니다.\n\n이러한 유형의 캐싱을 *[메모이제이션](https://ko.wikipedia.org/wiki/%EB%A9%94%EB%AA%A8%EC%9D%B4%EC%A0%9C%EC%9D%B4%EC%85%98)* 라고 합니다.\n\n<Note>\n\n**성능 최적화를 위해서만`useMemo`를 사용해야 합니다.** 이 기능이 없어서 코드가 작동하지 않는다면 근본적인 문제를 먼저 찾아서 수정하세요. 그 후 `useMemo`를 사용하여 성능을 개선해야 합니다.\n\n</Note>\n\n<DeepDive>\n\n#### 비싼 연산인지 어떻게 알 수 있나요? {/*how-to-tell-if-a-calculation-is-expensive*/}\n\n일반적으로 수천 개의 개체를 만들거나 반복하는 경우가 아니라면 비용이 많이 들지 않습니다. 조금 더 정확하게 확인하고 싶다면 콘솔 로그를 추가하여 코드에 소요된 시간을 측정할 수 있습니다.\n\n```js {1,3}\nconsole.time('filter array');\nconst visibleTodos = filterTodos(todos, tab);\nconsole.timeEnd('filter array');\n```\n\n측정하려는 상호작용(예시: Input에 입력)을 수행합니다. 그러면 `filter array: 0.15ms`와 같은 로그가 콘솔에 표시됩니다. 전체적으로 기록된 시간이 클 때(예시: `1ms` 이상) 해당 계산을 메모해 두는 것이 좋습니다. 그런 다음 실험적으로 `useMemo`로 계산을 감싸서 상호작용에 대한 총 시간이 감소했는지를 확인할 수 있습니다.\n\n```js\nconsole.time('filter array');\nconst visibleTodos = useMemo(() => {\n  return filterTodos(todos, tab); // todo와 tab이 변경되지 않은 경우 건너뜁니다.\n}, [todos, tab]);\nconsole.timeEnd('filter array');\n```\n\n`useMemo`는 *처음* 렌더링을 더 빠르게 만들지 않습니다. 이는 업데이트 시 불필요한 작업을 건너뛰는 데 도움이 될 뿐입니다.\n\n컴퓨터가 사용자의 컴퓨터보다 빠를 수 있으므로 인위적으로 속도를 낮추어 성능을 테스트하는 것이 좋습니다. 예를들어 Chrome은 [CPU 스로틀링](https://developer.chrome.com/blog/new-in-devtools-61/#throttling) 옵션을 제공합니다.\n\n개발환경은 가장 정확한 결과를 제공하지는 않습니다(예를 들어 [Strict 모드](/reference/react/StrictMode)가 켜져 있다면 각 컴포넌트가 한 번이 아닌 두 번 렌더링 되는 것을 볼 수 있습니다). 가장 정확한 타이밍을 얻으려면 프로덕션용 앱을 빌드하고 사용자가 사용하는 것과 동일한 기기에서 테스트하세요.\n\n</DeepDive>\n\n<DeepDive>\n\n#### 모든 곳에 useMemo를 추가해야 하나요? {/*should-you-add-usememo-everywhere*/}\n\nIf your app is like this site, and most interactions are coarse (like replacing a page or an entire section), memoization is usually unnecessary. On the other hand, if your app is more like a drawing editor, and most interactions are granular (like moving shapes), then you might find memoization very helpful.\n\n`useMemo`로 최적화하는 것은 몇몇 경우에만 유용합니다.\n\n- `useMemo`에 입력하는 계산이 눈에 띄게 느리고 종속성이 거의 변경되지 않는 경우.\n- [`memo`](/reference/react/memo)로 감싸진 컴포넌트에 prop로 전달할 경우. 값이 변경되지 않았다면 렌더링을 건너뛰고 싶을 것입니다. 메모이제이션을 사용하면 의존성이 동일하지 않은 경우에만 컴포넌트를 다시 렌더링할 수 있습니다.\n- 전달한 값을 나중에 일부 Hook의 종속성으로 이용할 경우. 예를 들어, 다른 `useMemo`의 계산 값이 여기에 종속되어 있을 수 있습니다. 또는 [`useEffect`](/reference/react/useEffect)의 값에 종속되어 있을 수 있습니다.\n\n이 외는 계산을 `useMemo`로 감싸는 것에 대한 이득이 없습니다. 그러나 그렇게 한다고 해서 크게 문제가 되는 것도 아니므로 일부 팀에서는 개별 사례에 대해 생각하지 않고 가능한 한 많이 메모하는 방식을 선택합니다. 이 접근 방식의 단점은 코드 가독성이 떨어진다는 것입니다. 또한, 모든 메모이제이션이 효과적인 것은 아닙니다. \"항상 새로운\" 단일 값만으로도 전체 컴포넌트에 대한 메모화가 깨질 수 있기 때문입니다.\n\n**실제로 몇 가지 원칙을 지키면 많은 메모이제이션을 불필요하게 만들 수 있습니다.**\n\n1. 컴포넌트가 다른 컴포넌트를 시각적으로 감쌀 때 [JSX를 자식처럼 받아들이도록 하세요.](/learn/passing-props-to-a-component#passing-jsx-as-children) 이렇게 하면 감싸는 구성 요소가 자신의 상태를 업데이트하더라도 React는 자식을 다시 렌더링할 필요가 없습니다.\n1. 지역 상태를 선호하고 필요 이상으로 [상태를 위로 올리지](/learn/sharing-state-between-components) 마세요. 예를 들어, 폼과 같이 일시적인 상태나 어떤 항목이 트리의 맨 위에 위치하거나, 전역 상태 라이브러리에 있게 하지 마세요.\n1. [순수한 렌더링 로직](/learn/keeping-components-pure)을 유지하세요. 컴포넌트를 다시 렌더링할 때 문제가 발생하거나 눈에 띄는 시각적인 부작용이 발생하면 컴포넌트에 버그가 있는 것입니다! 메모이제이션을 하는 대신 버그를 수정하세요.\n1. [상태를 업데이트하는 불필요한 Effect](/learn/you-might-not-need-an-effect)를 피하세요. React 앱의 대부분의 성능 문제는 컴포넌트를 반복적으로 렌더링하게 만드는 Effect의 업데이트 체인으로부터 발생합니다.\n1. [Effects에서 불필요한 종속성을 제거하세요.](/learn/removing-effect-dependencies) 예를 들어, 메모이제이션을 하는 대신 일부 객체나 함수를 Effect 내부 또는 컴포넌트 외부로 이동하는 것이 더 간단할 때가 있습니다.\n\n특정 상호작용이 여전히 느리게 느껴진다면 [React 개발자 도구 프로파일러](https://legacy.reactjs.org/blog/2018/09/10/introducing-the-react-profiler.html)를 사용하여 어떤 컴포넌트가 메모이제이션을 통해 가장 큰 이점을 얻을 수 있는지 확인하고 필요하다면 추가하세요. 이러한 원칙은 컴포넌트를 더 쉽게 디버깅하고 이해할 수 있게 해주므로 어떤 경우든 이 원칙을 따르는 것이 좋습니다. 장기적으로 우리는 이 문제를 완전히 해결하기 위해 [자동적 세분화 메모이제이션](https://www.youtube.com/watch?v=lGEMwh32soc)을 연구하고 있습니다.\n\n</DeepDive>\n\n<Recipes titleText=\"useMemo와 값을 직접 계산하는 것의 차이점\" titleId=\"examples-recalculation\">\n\n#### `useMemo`로 재계산 건너뛰기 {/*skipping-recalculation-with-usememo*/}\n\n이 예시에서는 렌더링 중에 호출하는 자바스크립트 함수가 실제로 느릴 때 어떤 일이 발생하는지 확인할 수 있도록 `filterTodos`을 **인위적으로 느리게** 만들었습니다. 탭을 전환하고 테마를 토글해 보세요.\n\n탭을 전환하면 느려진 `filterTodos`가 다시 실행되므로 느리게 느껴집니다. 이는 `tab`이 변경되었으므로 전체 계산이 *필수적으로* 다시 실행되기 때문에 나타나는 현상입니다. (왜 두 번 실행되는지 궁금하다면 [여기](#my-calculation-runs-twice-on-every-re-render)를 클릭해서 설명을 확인하세요.)\n\n테마를 전환합니다. **인위적인 속도 저하에도 불구하고 빠른 이유는 `useMemo` 덕분입니다!** 느린 속도의 `filterTodos`는 마지막 렌더링 이후 (`useMemo`에 종속성으로 전달한)`todos`와 `tab`이 모두 변경되지 않았기 때문에 호출을 건너뛰었습니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { useState } from 'react';\nimport { createTodos } from './utils.js';\nimport TodoList from './TodoList.js';\n\nconst todos = createTodos();\n\nexport default function App() {\n  const [tab, setTab] = useState('all');\n  const [isDark, setIsDark] = useState(false);\n  return (\n    <>\n      <button onClick={() => setTab('all')}>\n        All\n      </button>\n      <button onClick={() => setTab('active')}>\n        Active\n      </button>\n      <button onClick={() => setTab('completed')}>\n        Completed\n      </button>\n      <br />\n      <label>\n        <input\n          type=\"checkbox\"\n          checked={isDark}\n          onChange={e => setIsDark(e.target.checked)}\n        />\n        Dark mode\n      </label>\n      <hr />\n      <TodoList\n        todos={todos}\n        tab={tab}\n        theme={isDark ? 'dark' : 'light'}\n      />\n    </>\n  );\n}\n\n```\n\n```js src/TodoList.js active\nimport { useMemo } from 'react';\nimport { filterTodos } from './utils.js'\n\nexport default function TodoList({ todos, theme, tab }) {\n  const visibleTodos = useMemo(\n    () => filterTodos(todos, tab),\n    [todos, tab]\n  );\n  return (\n    <div className={theme}>\n      <p><b>Note: <code>filterTodos</code> is artificially slowed down!</b></p>\n      <ul>\n        {visibleTodos.map(todo => (\n          <li key={todo.id}>\n            {todo.completed ?\n              <s>{todo.text}</s> :\n              todo.text\n            }\n          </li>\n        ))}\n      </ul>\n    </div>\n  );\n}\n```\n\n```js src/utils.js\nexport function createTodos() {\n  const todos = [];\n  for (let i = 0; i < 50; i++) {\n    todos.push({\n      id: i,\n      text: \"Todo \" + (i + 1),\n      completed: Math.random() > 0.5\n    });\n  }\n  return todos;\n}\n\nexport function filterTodos(todos, tab) {\n  console.log('[ARTIFICIALLY SLOW] Filtering ' + todos.length + ' todos for \"' + tab + '\" tab.');\n  let startTime = performance.now();\n  while (performance.now() - startTime < 500) {\n    // 매우 느린 코드를 구현하기 위해 500ms 동안 아무것도 하지 않음.\n  }\n\n  return todos.filter(todo => {\n    if (tab === 'all') {\n      return true;\n    } else if (tab === 'active') {\n      return !todo.completed;\n    } else if (tab === 'completed') {\n      return todo.completed;\n    }\n  });\n}\n```\n\n```css\nlabel {\n  display: block;\n  margin-top: 10px;\n}\n\n.dark {\n  background-color: black;\n  color: white;\n}\n\n.light {\n  background-color: white;\n  color: black;\n}\n```\n\n</Sandpack>\n\n<Solution />\n\n#### 항상 값을 재계산하기 {/*always-recalculating-a-value*/}\n\n이 예시에서는 렌더링 중에 호출하는 자바스크립트 함수가 실제로 느릴 때 어떤 일이 발생하는지 확인할 수 있도록 `filterTodos`을 **인위적으로 느리게** 만들었습니다. 탭을 전환하고 테마를 토글해 보세요.\n\n이전 예시와 달리 테마 전환도 이제 느려졌습니다! **이 예시에는 `useMemo` 호출이 없기 때문에** 렌더링마다 느려진 `filterTodos`가 호출되기 때문입니다. 이는 `theme`만 변경하는 경우에도 호출됩니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { useState } from 'react';\nimport { createTodos } from './utils.js';\nimport TodoList from './TodoList.js';\n\nconst todos = createTodos();\n\nexport default function App() {\n  const [tab, setTab] = useState('all');\n  const [isDark, setIsDark] = useState(false);\n  return (\n    <>\n      <button onClick={() => setTab('all')}>\n        All\n      </button>\n      <button onClick={() => setTab('active')}>\n        Active\n      </button>\n      <button onClick={() => setTab('completed')}>\n        Completed\n      </button>\n      <br />\n      <label>\n        <input\n          type=\"checkbox\"\n          checked={isDark}\n          onChange={e => setIsDark(e.target.checked)}\n        />\n        Dark mode\n      </label>\n      <hr />\n      <TodoList\n        todos={todos}\n        tab={tab}\n        theme={isDark ? 'dark' : 'light'}\n      />\n    </>\n  );\n}\n\n```\n\n```js src/TodoList.js active\nimport { filterTodos } from './utils.js'\n\nexport default function TodoList({ todos, theme, tab }) {\n  const visibleTodos = filterTodos(todos, tab);\n  return (\n    <div className={theme}>\n      <ul>\n        <p><b>Note: <code>filterTodos</code> is artificially slowed down!</b></p>\n        {visibleTodos.map(todo => (\n          <li key={todo.id}>\n            {todo.completed ?\n              <s>{todo.text}</s> :\n              todo.text\n            }\n          </li>\n        ))}\n      </ul>\n    </div>\n  );\n}\n```\n\n```js src/utils.js\nexport function createTodos() {\n  const todos = [];\n  for (let i = 0; i < 50; i++) {\n    todos.push({\n      id: i,\n      text: \"Todo \" + (i + 1),\n      completed: Math.random() > 0.5\n    });\n  }\n  return todos;\n}\n\nexport function filterTodos(todos, tab) {\n  console.log('[ARTIFICIALLY SLOW] Filtering ' + todos.length + ' todos for \"' + tab + '\" tab.');\n  let startTime = performance.now();\n  while (performance.now() - startTime < 500) {\n    // 매우 느린 코드를 구현하기 위해 500ms 동안 아무것도 하지 않음.\n  }\n\n  return todos.filter(todo => {\n    if (tab === 'all') {\n      return true;\n    } else if (tab === 'active') {\n      return !todo.completed;\n    } else if (tab === 'completed') {\n      return todo.completed;\n    }\n  });\n}\n```\n\n```css\nlabel {\n  display: block;\n  margin-top: 10px;\n}\n\n.dark {\n  background-color: black;\n  color: white;\n}\n\n.light {\n  background-color: white;\n  color: black;\n}\n```\n\n</Sandpack>\n\n그러나 다음은 **인위적으로 속도 저하된 부분을 제거하고 나머지는 동일한 코드입니다.** `useMemo`를 사용하지 않은 것이 체감 되시나요?\n\n<Sandpack>\n\n```js src/App.js\nimport { useState } from 'react';\nimport { createTodos } from './utils.js';\nimport TodoList from './TodoList.js';\n\nconst todos = createTodos();\n\nexport default function App() {\n  const [tab, setTab] = useState('all');\n  const [isDark, setIsDark] = useState(false);\n  return (\n    <>\n      <button onClick={() => setTab('all')}>\n        All\n      </button>\n      <button onClick={() => setTab('active')}>\n        Active\n      </button>\n      <button onClick={() => setTab('completed')}>\n        Completed\n      </button>\n      <br />\n      <label>\n        <input\n          type=\"checkbox\"\n          checked={isDark}\n          onChange={e => setIsDark(e.target.checked)}\n        />\n        Dark mode\n      </label>\n      <hr />\n      <TodoList\n        todos={todos}\n        tab={tab}\n        theme={isDark ? 'dark' : 'light'}\n      />\n    </>\n  );\n}\n\n```\n\n```js src/TodoList.js active\nimport { filterTodos } from './utils.js'\n\nexport default function TodoList({ todos, theme, tab }) {\n  const visibleTodos = filterTodos(todos, tab);\n  return (\n    <div className={theme}>\n      <ul>\n        {visibleTodos.map(todo => (\n          <li key={todo.id}>\n            {todo.completed ?\n              <s>{todo.text}</s> :\n              todo.text\n            }\n          </li>\n        ))}\n      </ul>\n    </div>\n  );\n}\n```\n\n```js src/utils.js\nexport function createTodos() {\n  const todos = [];\n  for (let i = 0; i < 50; i++) {\n    todos.push({\n      id: i,\n      text: \"Todo \" + (i + 1),\n      completed: Math.random() > 0.5\n    });\n  }\n  return todos;\n}\n\nexport function filterTodos(todos, tab) {\n  console.log('Filtering ' + todos.length + ' todos for \"' + tab + '\" tab.');\n\n  return todos.filter(todo => {\n    if (tab === 'all') {\n      return true;\n    } else if (tab === 'active') {\n      return !todo.completed;\n    } else if (tab === 'completed') {\n      return todo.completed;\n    }\n  });\n}\n```\n\n```css\nlabel {\n  display: block;\n  margin-top: 10px;\n}\n\n.dark {\n  background-color: black;\n  color: white;\n}\n\n.light {\n  background-color: white;\n  color: black;\n}\n```\n\n</Sandpack>\n\n메모이제이션 없이도 코드가 잘 작동하는 경우가 많습니다. 상호 작용이 충분이 빠르다면 메모이제이션은 필요하지 않을 수도 있습니다.\n\n`utils.js`에서 할 일의 항목 수를 늘려보고 동작이 어떻게 바뀌는지 확인할 수 있습니다. 이 연산은 처음에는 비용이 많이 들지 않았지만 할 일의 수가 크게 증가하면 대부분의 오버헤어가 필터링이 아닌 재렌더링에 발생합니다. 아래에서 `useMemo`로 재렌더링을 최적화하는 방법에 대해 알아보세요.\n\n<Solution />\n\n</Recipes>\n\n---\n\n### 컴포넌트 재렌더링 건너뛰기 {/*skipping-re-rendering-of-components*/}\n\n경우에 따라 `useMemo`는 하위 컴포넌트 재렌더링 성능을 최적화하는데 도움이 될 수도 있습니다. 이를 설명하기 위해 `TodoList` 컴포넌트가 자식 컴포넌트인 `List`에  `visibleTodos`를 prop로 전달한다고 가정하겠습니다.\n\n```js {5}\nexport default function TodoList({ todos, tab, theme }) {\n  // ...\n  return (\n    <div className={theme}>\n      <List items={visibleTodos} />\n    </div>\n  );\n}\n```\n\n`theme` prop를 토글하면 앱이 잠시 멈추는 것을 확인할 수 있습니다. 그러나 JSX에서 `<List />`를 제거하면 빠르게 느껴집니다. 이는 `List` 컴포넌트를 최적화할 가치가 있다는 것을 알려줍니다.\n\n**기본적으로 React는 컴포넌트가 다시 렌더링 될 때, 모든 자식 컴포넌트를 재귀적으로 다시 렌더링합니다.** 그러므로 `TodoList`가 다른 `theme`로 다시 렌더링 되면 `List` 컴포넌트 *또한* 다시 렌더링 됩니다. 다시 렌더링하는 데 많은 계산이 필요하지 않는 컴포넌트는 괜찮지만, 다시 렌더링하는 것이 느리다는 것을 확인했다면 `List`를 [`memo`](/reference/react/memo)를 통해 감싸서 props가 마지막 렌더링 시점과 동일 할 때 다시 렌더링하는 것을 생략할 수 있습니다.\n\n```js {3,5}\nimport { memo } from 'react';\n\nconst List = memo(function List({ items }) {\n  // ...\n});\n```\n\n**이 변경으로 `List`는 모든 props가 마지막 렌더링 때와 *동일*한 경우 다시 렌더링하지 않습니다.** 여기서 계산을 캐싱하는 것이 중요합니다! `useMemo`없이 `visibleTodos`를 계산한다고 가정해 봅시다.\n\n```js {2-3,6-7}\nexport default function TodoList({ todos, tab, theme }) {\n  // 테마가 변경될 때 마다 다른 배열이 표시됩니다.\n  const visibleTodos = filterTodos(todos, tab);\n  return (\n    <div className={theme}>\n      {/* ... List의 props는 동일하지 않으며 매번 다시 렌더링 됩니다. */}\n      <List items={visibleTodos} />\n    </div>\n  );\n}\n```\n\n**위의 예시에서 `filterTodos` 함수는 항상 *다른* 배열을 생성합니다.** 이는 `{}` 객체 리터럴이 항상 새 객체를 생성하는 것과 유사합니다. 일반적으로 이는 문제가 되지 않지만 `List`의 props는 동일하지 않으며 [`memo`](/reference/react/memo)를 사용한 최적화가 작동하지 않는다는 것을 의미합니다. 이러한 경우 `useMemo`가 유용합니다.\n\n```js {2-3,5,9-10}\nexport default function TodoList({ todos, tab, theme }) {\n  // 재렌더링 사이에 계산을 캐싱하도록 React에 지시합니다...\n  const visibleTodos = useMemo(\n    () => filterTodos(todos, tab),\n    [todos, tab] // ...따라서 해당 종속성이 변경되지 않는 한...\n  );\n  return (\n    <div className={theme}>\n      {/* ...List에 동일한 props가 전달되어 재렌더링을 생략할 수 있습니다. */}\n      <List items={visibleTodos} />\n    </div>\n  );\n}\n```\n\n\n**`visibleTodos`연산을 `useMemo`로 감싸면 다시 렌더링 될 때마다 *같은* 값을 갖게 할 수 있습니다** (종속성이 변경되기 전까지). *특별한 이유가 없는 한* 연산을 `useMemo`로 감싸지 않아도 됩니다. 이 예시에서는 [`memo`](/reference/react/memo)로 감싸진 컴포넌트에 전달하면 재렌더링을 건너뛸 수 있기 때문입니다. 이 페이지에서 자세히 설명하는 `useMemo`를 추가해야 하는 몇 가지 다른 이유가 있습니다.\n\n<DeepDive>\n\n#### 개별 JSX 노드 메모화 {/*memoizing-individual-jsx-nodes*/}\n\n`List`를 [`memo`](/reference/react/memo)로 감싸는 대신, `<List />` 노드 자체를 `useMemo`로 감싸면 됩니다.\n\n```js {3,6}\nexport default function TodoList({ todos, tab, theme }) {\n  const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);\n  const children = useMemo(() => <List items={visibleTodos} />, [visibleTodos]);\n  return (\n    <div className={theme}>\n      {children}\n    </div>\n  );\n}\n```\n\n동작 방식은 동일합니다. `visibleTodos`이 변경되지 않은 경우 `List`는 다시 렌더링 되지 않습니다.\n\n`<List items={visibleTodos} />`와 같은 JSX 노드는 `{ type: List, props: { items: visibleTodos } }`와 같은 객체입니다. 이 객체를 생성하는 것은 매우 저렴하지만, React는 그 내용이 지난번과 동일한지 알 수 없습니다. 그래서 기본적으로 React는 `List` 컴포넌트를 다시 렌더링합니다.\n\n하지만 React가 이전 렌더링과 동일한 JSX를 발견하면 컴포넌트를 다시 렌더링하려고 시도하지 않습니다. JSX 노드는 [불변](https://en.wikipedia.org/wiki/Immutable_object)하기 때문입니다. JSX 노드 객체는 시간이 지나도 변경되지 않으므로 React는 재렌더링을 생략해도 안전하다는 것을 알고 있습니다. 그러나 이것이 동작하려면 노드가 단순히 코드적으로 동일해 보이는 것이 아닌 *실제로 동일한 객체*여야 합니다. 이 예시에서는 `useMemo`가 해당 일을 수행합니다.\n\nJSX 노드를 `useMemo`로 수동으로 감싸는 것은 편리한 방법은 아닙니다. 예를 들어, 조건부로는 이 작업을 수행할 수 없습니다. 그래서 보통 JSX 노드를 감싸는 대신 컴포넌트를 [`memo`](/reference/react/memo)로 감쌉니다.\n\n</DeepDive>\n\n<Recipes titleText=\"재렌더링을 건너뛰는 것과 항상 재렌더링을 하는 것의 차이점\" titleId=\"examples-rerendering\">\n\n#### `useMemo` 및 `memo`로 재렌더링 건너뛰기 {/*skipping-re-rendering-with-usememo-and-memo*/}\n\n이 예시에서는 `List` 컴포넌트를 **인위적으로 느리게 만들어** 렌더링 중인 React 컴포넌트가 실제로 느려질 때 어떤 일이 발생하는 지를 확인할 수 있습니다. 탭을 전환하고 테마를 토글해 보세요.\n\n탭을 전환하면 느려진 `List`가 다시 렌더링 되기 때문에 느리게 느껴집니다. 이는 `tab`이 변경되었으므로 사용자의 새로운 선택 사항을 화면에 반영해야 하기 때문에 예상되는 현상입니다.\n\n다음으로 테마를 토글해 보겠습니다. **인위적인 속도 저하에도 불구하고 [`memo`](/reference/react/memo)와 함께 사용된 `useMemo` 덕분에 빠릅니다!** `List`는 마지막 렌더링 이후 `visibleItems` 배열이 변경되지 않았기 때문에 재렌더링을 생략했습니다. (`useMemo`에 종속성으로 전달된) `todos`와 `tab`이 모두 마지막 렌더링 이후 변경되지 않았으므로 `visibleItems` 배열이 변경되지 않았습니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { useState } from 'react';\nimport { createTodos } from './utils.js';\nimport TodoList from './TodoList.js';\n\nconst todos = createTodos();\n\nexport default function App() {\n  const [tab, setTab] = useState('all');\n  const [isDark, setIsDark] = useState(false);\n  return (\n    <>\n      <button onClick={() => setTab('all')}>\n        All\n      </button>\n      <button onClick={() => setTab('active')}>\n        Active\n      </button>\n      <button onClick={() => setTab('completed')}>\n        Completed\n      </button>\n      <br />\n      <label>\n        <input\n          type=\"checkbox\"\n          checked={isDark}\n          onChange={e => setIsDark(e.target.checked)}\n        />\n        Dark mode\n      </label>\n      <hr />\n      <TodoList\n        todos={todos}\n        tab={tab}\n        theme={isDark ? 'dark' : 'light'}\n      />\n    </>\n  );\n}\n```\n\n```js src/TodoList.js active\nimport { useMemo } from 'react';\nimport List from './List.js';\nimport { filterTodos } from './utils.js'\n\nexport default function TodoList({ todos, theme, tab }) {\n  const visibleTodos = useMemo(\n    () => filterTodos(todos, tab),\n    [todos, tab]\n  );\n  return (\n    <div className={theme}>\n      <p><b>Note: <code>List</code> is artificially slowed down!</b></p>\n      <List items={visibleTodos} />\n    </div>\n  );\n}\n```\n\n```js src/List.js\nimport { memo } from 'react';\n\nconst List = memo(function List({ items }) {\n  console.log('[ARTIFICIALLY SLOW] Rendering <List /> with ' + items.length + ' items');\n  let startTime = performance.now();\n  while (performance.now() - startTime < 500) {\n    // 매우 느린 코드를 구현하기 위해 500ms 동안 아무것도 하지 않음.\n  }\n\n  return (\n    <ul>\n      {items.map(item => (\n        <li key={item.id}>\n          {item.completed ?\n            <s>{item.text}</s> :\n            item.text\n          }\n        </li>\n      ))}\n    </ul>\n  );\n});\n\nexport default List;\n```\n\n```js src/utils.js\nexport function createTodos() {\n  const todos = [];\n  for (let i = 0; i < 50; i++) {\n    todos.push({\n      id: i,\n      text: \"Todo \" + (i + 1),\n      completed: Math.random() > 0.5\n    });\n  }\n  return todos;\n}\n\nexport function filterTodos(todos, tab) {\n  return todos.filter(todo => {\n    if (tab === 'all') {\n      return true;\n    } else if (tab === 'active') {\n      return !todo.completed;\n    } else if (tab === 'completed') {\n      return todo.completed;\n    }\n  });\n}\n```\n\n```css\nlabel {\n  display: block;\n  margin-top: 10px;\n}\n\n.dark {\n  background-color: black;\n  color: white;\n}\n\n.light {\n  background-color: white;\n  color: black;\n}\n```\n\n</Sandpack>\n\n<Solution />\n\n#### 항상 컴포넌트 재렌더링 하기 {/*always-re-rendering-a-component*/}\n\n이 예시에서는 `List` 컴포넌트를 **인위적으로 느리게 만들어** 렌더링 중인 React 컴포넌트가 실제로 느려질 때 어떤 일이 발생하는 지를 확인할 수 있습니다. 탭을 전환하고 테마를 토글해 보세요.\n\n이전 예시와 다르게 이제는 테마 전환도 느려졌습니다! **이 버전에는 `useMemo` 호출이 없기 때문에** `visibleTodos`는 항상 다른 배열이 되고 `List` 컴포넌트는 재렌더링을 생략할 수 없기 때문입니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { useState } from 'react';\nimport { createTodos } from './utils.js';\nimport TodoList from './TodoList.js';\n\nconst todos = createTodos();\n\nexport default function App() {\n  const [tab, setTab] = useState('all');\n  const [isDark, setIsDark] = useState(false);\n  return (\n    <>\n      <button onClick={() => setTab('all')}>\n        All\n      </button>\n      <button onClick={() => setTab('active')}>\n        Active\n      </button>\n      <button onClick={() => setTab('completed')}>\n        Completed\n      </button>\n      <br />\n      <label>\n        <input\n          type=\"checkbox\"\n          checked={isDark}\n          onChange={e => setIsDark(e.target.checked)}\n        />\n        Dark mode\n      </label>\n      <hr />\n      <TodoList\n        todos={todos}\n        tab={tab}\n        theme={isDark ? 'dark' : 'light'}\n      />\n    </>\n  );\n}\n```\n\n```js src/TodoList.js active\nimport List from './List.js';\nimport { filterTodos } from './utils.js'\n\nexport default function TodoList({ todos, theme, tab }) {\n  const visibleTodos = filterTodos(todos, tab);\n  return (\n    <div className={theme}>\n      <p><b>Note: <code>List</code> is artificially slowed down!</b></p>\n      <List items={visibleTodos} />\n    </div>\n  );\n}\n```\n\n```js src/List.js\nimport { memo } from 'react';\n\nconst List = memo(function List({ items }) {\n  console.log('[ARTIFICIALLY SLOW] Rendering <List /> with ' + items.length + ' items');\n  let startTime = performance.now();\n  while (performance.now() - startTime < 500) {\n    // 매우 느린 코드를 구현하기 위해 500ms 동안 아무것도 하지 않음.\n  }\n\n  return (\n    <ul>\n      {items.map(item => (\n        <li key={item.id}>\n          {item.completed ?\n            <s>{item.text}</s> :\n            item.text\n          }\n        </li>\n      ))}\n    </ul>\n  );\n});\n\nexport default List;\n```\n\n```js src/utils.js\nexport function createTodos() {\n  const todos = [];\n  for (let i = 0; i < 50; i++) {\n    todos.push({\n      id: i,\n      text: \"Todo \" + (i + 1),\n      completed: Math.random() > 0.5\n    });\n  }\n  return todos;\n}\n\nexport function filterTodos(todos, tab) {\n  return todos.filter(todo => {\n    if (tab === 'all') {\n      return true;\n    } else if (tab === 'active') {\n      return !todo.completed;\n    } else if (tab === 'completed') {\n      return todo.completed;\n    }\n  });\n}\n```\n\n```css\nlabel {\n  display: block;\n  margin-top: 10px;\n}\n\n.dark {\n  background-color: black;\n  color: white;\n}\n\n.light {\n  background-color: white;\n  color: black;\n}\n```\n\n</Sandpack>\n\n그러나 다음은 **인위적인 속도 저하를 제거한 동일한 코드입니다.** `useMemo`가 없는 것이 체감 되시나요?\n\n<Sandpack>\n\n```js src/App.js\nimport { useState } from 'react';\nimport { createTodos } from './utils.js';\nimport TodoList from './TodoList.js';\n\nconst todos = createTodos();\n\nexport default function App() {\n  const [tab, setTab] = useState('all');\n  const [isDark, setIsDark] = useState(false);\n  return (\n    <>\n      <button onClick={() => setTab('all')}>\n        All\n      </button>\n      <button onClick={() => setTab('active')}>\n        Active\n      </button>\n      <button onClick={() => setTab('completed')}>\n        Completed\n      </button>\n      <br />\n      <label>\n        <input\n          type=\"checkbox\"\n          checked={isDark}\n          onChange={e => setIsDark(e.target.checked)}\n        />\n        Dark mode\n      </label>\n      <hr />\n      <TodoList\n        todos={todos}\n        tab={tab}\n        theme={isDark ? 'dark' : 'light'}\n      />\n    </>\n  );\n}\n```\n\n```js src/TodoList.js active\nimport List from './List.js';\nimport { filterTodos } from './utils.js'\n\nexport default function TodoList({ todos, theme, tab }) {\n  const visibleTodos = filterTodos(todos, tab);\n  return (\n    <div className={theme}>\n      <List items={visibleTodos} />\n    </div>\n  );\n}\n```\n\n```js src/List.js\nimport { memo } from 'react';\n\nfunction List({ items }) {\n  return (\n    <ul>\n      {items.map(item => (\n        <li key={item.id}>\n          {item.completed ?\n            <s>{item.text}</s> :\n            item.text\n          }\n        </li>\n      ))}\n    </ul>\n  );\n}\n\nexport default memo(List);\n```\n\n```js src/utils.js\nexport function createTodos() {\n  const todos = [];\n  for (let i = 0; i < 50; i++) {\n    todos.push({\n      id: i,\n      text: \"Todo \" + (i + 1),\n      completed: Math.random() > 0.5\n    });\n  }\n  return todos;\n}\n\nexport function filterTodos(todos, tab) {\n  return todos.filter(todo => {\n    if (tab === 'all') {\n      return true;\n    } else if (tab === 'active') {\n      return !todo.completed;\n    } else if (tab === 'completed') {\n      return todo.completed;\n    }\n  });\n}\n```\n\n```css\nlabel {\n  display: block;\n  margin-top: 10px;\n}\n\n.dark {\n  background-color: black;\n  color: white;\n}\n\n.light {\n  background-color: white;\n  color: black;\n}\n```\n\n</Sandpack>\n\n메모이제이션 없이도 코드가 잘 동작하는 경우가 많습니다. 상호 작용이 충분히 빠르다면 메모이제이션을 할 필요가 없습니다.\n\n앱의 속도를 실제로 저하시키는 요인을 현실적으로 파악하려면 프로덕션 모드에서 React를 실행하고, [React 개발자 도구](/learn/react-developer-tools)를 비활성화하고, 앱 사용자가 사용하는 것과 유사한 기기를 사용해야 한다는 점을 명심하세요.\n\n<Solution />\n\n</Recipes>\n\n---\n\n### Effect가 자주 실행되지 않도록 하기 {/*preventing-an-effect-from-firing-too-often*/}\n\n때때로, [Effect](/learn/synchronizing-with-effects) 안에 값을 사용하고 싶을 것입니다.\n\n```js {4-7,10}\nfunction ChatRoom({ roomId }) {\n  const [message, setMessage] = useState('');\n\n  const options = {\n    serverUrl: 'https://localhost:1234',\n    roomId: roomId\n  }\n\n  useEffect(() => {\n    const connection = createConnection(options);\n    connection.connect();\n    // ...\n```\n\n이것은 문제를 일으킵니다. [모든 반응형 값은 Effect의 종속성으로 선언되어야 합니다.](/learn/lifecycle-of-reactive-effects#react-verifies-that-you-specified-every-reactive-value-as-a-dependency) 그러나 만약 `options`을 종속성으로 선언한다면, 이것은 Effect가 chat room과 계속해서 재연결되도록 할 것입니다.\n\n```js {5}\n  useEffect(() => {\n    const connection = createConnection(options);\n    connection.connect();\n    return () => connection.disconnect();\n  }, [options]); // 🔴 문제: 이 종속성은 렌더링마다 변경됩니다.\n  // ...\n```\n\n이 문제를 해결하기 위해서는, Effect 안에서 사용되는 객체를 `useMemo`로 감싸면 됩니다.\n\n```js {4-9,16}\nfunction ChatRoom({ roomId }) {\n  const [message, setMessage] = useState('');\n\n  const options = useMemo(() => {\n    return {\n      serverUrl: 'https://localhost:1234',\n      roomId: roomId\n    };\n  }, [roomId]); // ✅ roomId가 변경될때만 실행됩니다.\n\n  useEffect(() => {\n    const connection = createConnection(options);\n    connection.connect();\n    return () => connection.disconnect();\n  }, [options]); // ✅ options가 변경될때만 실행됩니다.\n  // ...\n```\n\n이것은 만약 `useMemo`가 캐시된 객체를 반환할 경우, 재렌더링시 `options` 객체가 동일하다는 것을 보장합니다.\n\n그러나 `useMemo`는 성능 최적화를 위한 것이지 의미론적 보장은 아니기 때문에 React는 [특별한 이유가 있는 경우](#caveats) 캐시된 값을 버릴 수 있습니다. 이로 인해 Effect가 다시 실행될 수 있습니다. 따라서 객체를 Effect *안으로* 이동시켜 **함수 종속성의 필요성을 제거하는 것이 더 좋습니다.**\n\n\n```js {5-8,13}\nfunction ChatRoom({ roomId }) {\n  const [message, setMessage] = useState('');\n\n  useEffect(() => {\n    const options = { // ✅ 더이상 useMemo나 object dependencies가 필요없습니다!\n      serverUrl: 'https://localhost:1234',\n      roomId: roomId\n    }\n\n    const connection = createConnection(options);\n    connection.connect();\n    return () => connection.disconnect();\n  }, [roomId]); // ✅ roomId가 변경될때에만 실행됩니다.\n  // ...\n```\n\n이제 코드는 더 간단해지고 `useMemo`가 필요하지 않습니다. [Effect 종속성 제거에 대해 더 알아보세요.](/learn/removing-effect-dependencies#move-dynamic-objects-and-functions-inside-your-effect)\n\n### 다른 Hook의 종속성 메모화 {/*memoizing-a-dependency-of-another-hook*/}\n\n컴포넌트 본문에서 직접 생성된 객체에 의존하는 연산이 있다고 가정하겠습니다.\n\n```js {2}\nfunction Dropdown({ allItems, text }) {\n  const searchOptions = { matchMode: 'whole-word', text };\n\n  const visibleItems = useMemo(() => {\n    return searchItems(allItems, searchOptions);\n  }, [allItems, searchOptions]); // 🚩 주의: 컴포넌트 본문에서 생성된 객체에 대한 종속성\n  // ...\n```\n\n이렇게 객체에 의존하는 것은 메모이제이션의 목적을 무색하게 합니다. 컴포넌트가 다시 렌더링 되면 컴포넌트 본문 내부의 모든 코드가 다시 실행되기 때문입니다. **`searchOptions` 객체를 생성하는 코드도 다시 렌더링 될 때마다 실행됩니다.** `searchOptions`은 `useMemo` 호출의 종속성이고 매번 다르기 때문에, React는 종속성이 다른 것을 알고`searchItems`을 매번 다시 계산합니다.\n\n이 문제를 해결하기 위해 `searchOptions` 객체 *자체를* 종속성으로 전달하기 전에 메모해두면 됩니다.\n\n```js {2-4}\nfunction Dropdown({ allItems, text }) {\n  const searchOptions = useMemo(() => {\n    return { matchMode: 'whole-word', text };\n  }, [text]); // ✅ text가 변경될 때만 변경\n\n  const visibleItems = useMemo(() => {\n    return searchItems(allItems, searchOptions);\n  }, [allItems, searchOptions]); // ✅ allItems이나 searchOptions이 변경될 때만 변경\n  // ...\n```\n\n위의 예시에서 `text`가 변경되지 않았다면 `searchOptions` 객체도 변경되지 않습니다. 그러나 이보다 더 나은 방법은 `searchOptions`를 `useMemo` 계산 함수의 *내부에* 선언하는 것입니다.\n\n```js {3}\nfunction Dropdown({ allItems, text }) {\n  const visibleItems = useMemo(() => {\n    const searchOptions = { matchMode: 'whole-word', text };\n    return searchItems(allItems, searchOptions);\n  }, [allItems, text]); // ✅ allItems이나 text가 변경될 때만 변경\n  // ...\n```\n\n이제 연산은 `text` 에 직접적으로 의존합니다 (문자열이므로 \"실수로\" 달라질 수 없음).\n\n---\n\n### 함수 메모화 {/*memoizing-a-function*/}\n\n`Form` 컴포넌트가 [`memo`](/reference/react/memo)로 감싸져 있고 여기에 prop로 함수를 전달하고 싶다고 가정해봅시다.\n\n```js {2-7}\nexport default function ProductPage({ productId, referrer }) {\n  function handleSubmit(orderDetails) {\n    post('/product/' + productId + '/buy', {\n      referrer,\n      orderDetails\n    });\n  }\n\n  return <Form onSubmit={handleSubmit} />;\n}\n```\n\n`{}`가 다른 객체를 생성하는 것처럼 `function() {}`와 같은 함수 선언과 `() => {}` 같은 표현식은 다시 렌더링 될 때마다 *다른* 함수를 생성합니다. 새로운 함수를 만드는 것 자체는 문제가 되지 않으며 피해야 할 일이 아닙니다! 그러나 `Form` 컴포넌트가 메모화되어 있다면 props가 변경되지 않았을 때 다시 렌더링하는 것을 생략하고 싶을 것입니다. *항상* 다른 prop은 메모이제이션의 목적을 무색하게 만들 수 있습니다.\n\n`useMemo`로 함수를 메모하려면 계산 함수에서 다른 함수를 반환해야 합니다.\n\n```js {2-3,8-9}\nexport default function Page({ productId, referrer }) {\n  const handleSubmit = useMemo(() => {\n    return (orderDetails) => {\n      post('/product/' + productId + '/buy', {\n        referrer,\n        orderDetails\n      });\n    };\n  }, [productId, referrer]);\n\n  return <Form onSubmit={handleSubmit} />;\n}\n```\n\n위 예시는 투박해 보입니다! **함수를 메모하는 것은 충분히 일반적이기 때문에 React에는 이를 위한 Hook이 내장되어 있습니다. `useMemo` 대신 [`useCallback`](/reference/react/useCallback)으로 함수를 감싸서** 중첩된 함수를 추가로 작성하지 않도록 하세요.\n\n```js {2,7}\nexport default function Page({ productId, referrer }) {\n  const handleSubmit = useCallback((orderDetails) => {\n    post('/product/' + productId + '/buy', {\n      referrer,\n      orderDetails\n    });\n  }, [productId, referrer]);\n\n  return <Form onSubmit={handleSubmit} />;\n}\n```\n\n위 두 예시는 완전히 동일하게 동작합니다. `useCallback`의 유일한 장점은 내부에 중첩된 함수를 추가로 작성하지 않아도 된다는 것입니다. 그 외에는 아무것도 하지 않습니다. [`useCallback`에 대해 더 읽어보세요.](/reference/react/useCallback)\n\n---\n\n## 문제 해결하기 {/*troubleshooting*/}\n\n### 렌더링할 때마다 계산이 두 번 실행됩니다 {/*my-calculation-runs-twice-on-every-re-render*/}\n\n[Strict 모드](/reference/react/StrictMode)에서 React는 일부 함수를 한 번이 아닌 두 번 호출합니다.\n\n```js {2,5,6}\nfunction TodoList({ todos, tab }) {\n  // 이 컴포넌트 함수는 렌더링할 때마다 두 번 실행됩니다.\n\n  const visibleTodos = useMemo(() => {\n    // 종속성 중 하나라도 변경되면 이 계산은 두 번 실행됩니다.\n    return filterTodos(todos, tab);\n  }, [todos, tab]);\n\n  // ...\n```\n\n이는 예상되는 현상이며 코드를 손상시키지 않습니다.\n\n이 **개발 전용** 동작은 [컴포넌트가 순수하게 유지될 수 있도록](/learn/keeping-components-pure) 도와줍니다. React는 호출 결과 중 하나를 사용하고 다른 호출 결과는 무시합니다. 컴포넌트와 계산 함수가 순수하다면 로직에 영향을 미치지 않을 것입니다. 그러나 실수로 발생하는 불순한 경우에 발생하는 실수를 발견하고 수정하는 데 도움을 줍니다.\n\n예를 들어 아래의 불순한 계산 함수는 prop으로 받은 배열을 변경합니다.\n\n```js {2-3}\n  const visibleTodos = useMemo(() => {\n    // 🚩 Mistake: mutating a prop\n    todos.push({ id: 'last', text: 'Go for a walk!' });\n    const filtered = filterTodos(todos, tab);\n    return filtered;\n  }, [todos, tab]);\n```\n\nReact가 함수를 두 번 호출하므로 todo가 두 번 추가됩니다. 계산이 기존의 객체를 변경해서는 안 되지만 계산 중에 생성된 *새로운* 객체를 변경하는 것은 괜찮습니다. 예를 들어 `filterTodos` 함수가 항상 *다른* 배열을 반환하는 경우 대신 *해당 배열*을 변경할 수 있습니다.\n\n```js {3,4}\n  const visibleTodos = useMemo(() => {\n    const filtered = filterTodos(todos, tab);\n    // ✅ 정답: 계산 중에 생성한 객체를 변경합니다.\n    filtered.push({ id: 'last', text: 'Go for a walk!' });\n    return filtered;\n  }, [todos, tab]);\n```\n\n순수성에 대해 자세히 알아보려면 [컴포넌트 순수하게 유지하기](/learn/keeping-components-pure)를 읽어보세요.\n\n또한 변경사항이 없는 [객체 업데이트](/learn/updating-objects-in-state) 및 [배열 업데이트](/learn/updating-arrays-in-state)에 대한 가이드도 확인해보세요.\n\n---\n\n### `useMemo`가 객체를 반환해야 하는데 undefined를 반환합니다. {/*my-usememo-call-is-supposed-to-return-an-object-but-returns-undefined*/}\n\n이 코드는 작동하지 않습니다.\n\n```js {1-2,5}\n  // 🔴 () => { 와 같은 화살표 함수는 객체를 반환하지 않습니다.\n  const searchOptions = useMemo(() => {\n    matchMode: 'whole-word',\n    text: text\n  }, [text]);\n```\n\n자바스크립트의 `() => {`는 화살표 함수의 본문의 시작이므로 `{` 중괄호는 객체의 일부가 아닙니다. 이것이 객체를 반환하지 않고 실수하는 지점입니다. `({` 과 `})` 같은 괄호를 추가하여 이 문제를 해결할 수 있습니다.\n\n```js {1-2,5}\n  // T이것은 작동하지만 누군가가 다시 위반하기 쉽습니다.\n  const searchOptions = useMemo(() => ({\n    matchMode: 'whole-word',\n    text: text\n  }), [text]);\n```\n\n하지만 해당 방식은 여전히 혼란을 주고, 괄호를 제거하면서 누군가 쉽게 위반할 수 있습니다.\n\n이 실수를 방지하기 위해 `return` 문을 명시적으로 작성하세요.\n\n```js {1-3,6-7}\n  // ✅ 이것은 작동하며 명확합니다.\n  const searchOptions = useMemo(() => {\n    return {\n      matchMode: 'whole-word',\n      text: text\n    };\n  }, [text]);\n```\n\n---\n\n### 컴포넌트가 렌더링 될 때마다 `useMemo`의 계산이 다시 실행됩니다. {/*every-time-my-component-renders-the-calculation-in-usememo-re-runs*/}\n\n두 번째 인수로 종속성 배열을 지정했는지 확인하세요!\n\n종속성 배열을 지정하지 않았을 경우 `useMemo`는 매번 다시 계산을 실행합니다.\n\n```js {2-3}\nfunction TodoList({ todos, tab }) {\n  // 🔴 종속성 배열이 없어 매번 재계산 됨.\n  const visibleTodos = useMemo(() => filterTodos(todos, tab));\n  // ...\n```\n\n이것은 두 번째 인수로 종속성 배열을 전달하는 수정된 예시입니다.\n\n```js {2-3}\nfunction TodoList({ todos, tab }) {\n  // ✅ 불필요한 재계산을 하지 않음.\n  const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);\n  // ...\n```\n\n만일 위의 예시가 도움이 되지 않았다면, 종속성 중 하나 이상이 이전 렌더링과 달라졌다는 문제일 수 있습니다. 종속성 들을 콘솔에 수동으로 로깅하여 이 문제를 디버그할 수 있습니다.\n\n```js\n  const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);\n  console.log([todos, tab]);\n```\n\n그런 다음 콘솔에서 서로 다른 리렌더의 배열을 마우스 오른쪽 버튼으로 클릭하고 두 배열 모두에 대해 \"전역 변수로 저장\"을 선택합니다. 첫 번째 배열은 `temp1`, 두 번째 배열이 `temp2`로 저장되었다고 가정하면 브라우저 콘솔에서 두 배열의 각 종속성이 동일한지에 대해 확인할 수 있습니다.\n\n```js\nObject.is(temp1[0], temp2[0]); // 배열 간의 첫 번째 종속성이 동일합니까?\nObject.is(temp1[1], temp2[1]); // 배열 간의 두 번째 종속성이 동일합니까?\nObject.is(temp1[2], temp2[2]); // ... 그리고 기타 모든 종속성들이 동일합니까? ...\n```\n\n메모를 방해하는 종속성을 발견하면 제거할 방법을 찾거나 [메모할 방법을 찾으세요.](#memoizing-a-dependency-of-another-hook)\n\n---\n\n### 반복문에서 각 목록 항목에 대해 `useMemo`를 호출해야 하는데 허용되지 않습니다. {/*i-need-to-call-usememo-for-each-list-item-in-a-loop-but-its-not-allowed*/}\n\n`Chart` 컴포넌트가 [`memo`](/reference/react/memo)로 감싸져 있다고 가정해보겠습니다. `ReportList` 컴포넌트가 다시 렌더링 될 때 목록의 모든 `Chart`를 다시 렌더링하는 것을 생략하고 싶을 것입니다. 그러나 반복문에서 `useMemo`를 호출할 수 없습니다.\n\n```js {5-11}\nfunction ReportList({ items }) {\n  return (\n    <article>\n      {items.map(item => {\n        // 🔴 반복문에서는 useMemo를 호출할 수 없습니다.\n        const data = useMemo(() => calculateReport(item), [item]);\n        return (\n          <figure key={item.id}>\n            <Chart data={data} />\n          </figure>\n        );\n      })}\n    </article>\n  );\n}\n```\n\n대신 각 항목에 대한 컴포넌트를 추출하고 개별 항목에 대한 데이터를 메모하세요.\n\n```js {5,12-18}\nfunction ReportList({ items }) {\n  return (\n    <article>\n      {items.map(item =>\n        <Report key={item.id} item={item} />\n      )}\n    </article>\n  );\n}\n\nfunction Report({ item }) {\n  // ✅ 최상위 수준에서 useMemo를 호출합니다.\n  const data = useMemo(() => calculateReport(item), [item]);\n  return (\n    <figure>\n      <Chart data={data} />\n    </figure>\n  );\n}\n```\n\n또는 `useMemo`를 제거하고 `Report` 자체를 [`memo`](/reference/react/memo)로 감싸는 방법도 있습니다. `item` prop가 변경되지 않으면 `Report`는 재렌더링을 건너뛰므로 `Chart` 역시 재렌더링을 건너뛰게 됩니다.\n\n```js {5,6,12}\nfunction ReportList({ items }) {\n  // ...\n}\n\nconst Report = memo(function Report({ item }) {\n  const data = calculateReport(item);\n  return (\n    <figure>\n      <Chart data={data} />\n    </figure>\n  );\n});\n```\n"
  },
  {
    "path": "src/content/reference/react/useOptimistic.md",
    "content": "---\ntitle: useOptimistic\n---\n\n<Intro>\n\n`useOptimistic` 는 UI를 낙관적으로 업데이트할 수 있게 해주는 React Hook입니다.\n\n```js\nconst [optimisticState, setOptimistic] = useOptimistic(value, reducer?);\n```\n\n</Intro>\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `useOptimistic(value, reducer?)` {/*useoptimistic*/}\n\n`useOptimistic`은 React Hook으로, 비동기 작업이 진행 중일 때 다른 상태를 보여줄 수 있게 해줍니다. 인자로 주어진 일부 상태를 받아, 네트워크 요청과 같은 비동기 작업 기간 동안 달라질 수 있는 그 상태의 복사본을 반환합니다. 현재 상태와 작업의 입력을 취하는 함수를 제공하고, 작업이 대기 중일 때 사용할 낙관적인 상태를 반환합니다.\n\n이 상태는 \"낙관적\" 상태라고 불리는데, 실제로 작업을 완료하는 데 시간이 걸리더라도 사용자에게 즉시 작업의 결과를 표시하기 위해 일반적으로 사용됩니다.\n\n```js\nimport { useOptimistic } from 'react';\n\nfunction MyComponent({name, todos}) {\n  const [optimisticAge, setOptimisticAge] = useOptimistic(28);\n  const [optimisticName, setOptimisticName] = useOptimistic(name);\n  const [optimisticTodos, setOptimisticTodos] = useOptimistic(todos, todoReducer);\n  // ...\n}\n```\n\n[아래에 더 많은 예시를 참조하세요.](#usage)\n\n#### 매개변수 {/*parameters*/}\n\n* `state`: 작업이 대기 중이지 않을 때 초기에 반환될 값입니다.\n* `updateFn(currentState, optimisticValue)`: 현재 상태와 addOptimistic에 전달된 낙관적인 값을 취하는 함수로, 결과적인 낙관적인 상태를 반환합니다. 순수 함수여야 합니다. `updateFn`은 두 개의 매개변수를 취합니다. `currentState`와 `optimisticValue`. 반환 값은 `currentState`와 `optimisticValue`의 병합된 값입니다.\n\n#### 반환값 {/*returns*/}\n\n* `optimisticState`: 결과적인 낙관적인 상태입니다. 작업이 대기 중이지 않을 때는 `state`와 동일하며, 그렇지 않은 경우 `updateFn`에서 반환된 값과 동일합니다.\n* `addOptimistic`: `addOptimistic`는 낙관적인 업데이트가 있을 때 호출하는 dispatch 함수입니다. 어떠한 타입의 `optimisticValue`라는 하나의 인자를 취하며, `state`와 `optimisticValue`로 `updateFn`을 호출합니다.\n\n---\n\n## 사용법 {/*usage*/}\n\n### 폼을 낙관적으로 업데이트하기 {/*optimistically-updating-with-forms*/}\n\n`useOptimistic` Hook은 네트워크 요청과 같은 백그라운드 작업이 완료되기 전에 사용자 인터페이스를 낙관적으로 업데이트하는 방법을 제공합니다. 폼의 맥락에서, 이 기술은 앱이 더 반응적으로 느껴지도록 도와줍니다. 사용자가 폼을 제출할 때, 서버의 응답을 기다리는 대신 인터페이스는 기대하는 결과로 즉시 업데이트됩니다.\n\n예를 들어, 사용자가 폼에 메시지를 입력하고 \"전송\" 버튼을 누르면, `useOptimistic` Hook은 메시지가 실제로 서버로 전송되기 전에 \"전송 중...\" 라벨이 있는 목록에 메시지가 즉시 나타나도록 합니다. 이 \"낙관적\" 접근법은 속도와 반응성의 느낌을 줍니다. 그런 다음 폼은 백그라운드에서 메시지를 실제로 전송하려고 시도합니다. 서버가 메시지를 받았음을 확인하면, \"전송 중...\" 라벨이 제거됩니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { useState, startTransition } from 'react';\nimport Button from './Button';\nimport { submitForm } from './actions.js';\n\nexport default function App() {\n  const [count, setCount] = useState(0);\n  return (\n    <div>\n      <Button action={async () => {         \n        await submitForm();\n        startTransition(() => {\n          setCount(c => c + 1);\n        });\n      }}>Increment</Button>\n      {count > 0 && <p>Submitted {count}!</p>}\n    </div>\n  );\n}\n```\n\n```js src/Button.js active\nimport { useOptimistic, startTransition } from 'react';\n\nexport default function Button({ action, children }) {\n  const [isPending, setIsPending] = useOptimistic(false);\n\n  return (\n    <button\n      disabled={isPending}\n      onClick={() => {\n        startTransition(async () => {\n          setIsPending(true);\n          await action();\n        });\n      }}\n    >\n      {isPending ? 'Submitting...' : children}\n    </button>\n  );\n}\n```\n\n```js src/actions.js hidden\nexport async function submitForm() {\n  await new Promise((res) => setTimeout(res, 1000));\n}\n```\n\n</Sandpack>\n\nWhen the button is clicked, `setIsPending(true)` uses optimistic state to immediately show \"Submitting...\" and disable the button. When the Action is done, `isPending` is rendered as `false` automatically.\n\nThis pattern automatically shows a pending state however `action` prop is used with `Button`:\n\n```js\n// Show pending state for a state update\n<Button action={() => { setState(c => c + 1) }} />\n\n// Show pending state for a navigation\n<Button action={() => { navigate('/done') }} />\n\n// Show pending state for a POST\n<Button action={async () => { await fetch(/* ... */) }} />\n\n// Show pending state for any combination\n<Button action={async () => {\n  setState(c => c + 1);\n  await fetch(/* ... */);\n  navigate('/done');\n}} />\n```\n\nThe pending state will be shown until everything in the `action` prop is finished.\n\n<Note>\n\nYou can also use [`useTransition`](/reference/react/useTransition) to get pending state via `isPending`. \n\nThe difference is that `useTransition` gives you the `startTransition` function, while `useOptimistic` works with any Transition. Use whichever fits your component's needs.\n\n</Note>\n\n---\n\n### Updating props or state optimistically {/*updating-props-or-state-optimistically*/}\n\nYou can wrap props or state in `useOptimistic` to update it immediately while an Action is in progress.\n\nIn this example, `LikeButton` receives `isLiked` as a prop and immediately toggles it when clicked:\n\n<Sandpack>\n\n```js src/App.js\nimport { useState, useOptimistic, startTransition } from 'react';\nimport { toggleLike } from './actions.js';\n\nexport default function App() {\n  const [isLiked, setIsLiked] = useState(false);\n  const [optimisticIsLiked, setOptimisticIsLiked] = useOptimistic(isLiked);\n\n  function handleClick() {\n    startTransition(async () => {\n      const newValue = !optimisticIsLiked\n      console.log('⏳ setting optimistic state: ' + newValue);\n      \n      setOptimisticIsLiked(newValue);\n      const updatedValue = await toggleLike(newValue);\n      \n      startTransition(() => {\n        console.log('⏳ setting real state: ' + updatedValue );\n        setIsLiked(updatedValue);\n      });\n    });\n  }\n\n  if (optimisticIsLiked !== isLiked) {\n    console.log('✅ rendering optimistic state: ' + optimisticIsLiked);  \n  } else {\n    console.log('✅ rendering real value: ' + optimisticIsLiked);\n  }\n  \n\n  return (\n    <button onClick={handleClick}>\n      {optimisticIsLiked ? '❤️ Unlike' : '🤍 Like'}\n    </button>\n  );\n}\n```\n\n```js src/actions.js hidden\nexport async function toggleLike(value) {\n  return await new Promise((res) => setTimeout(() => res(value), 1000));\n  // In a real app, this would update the server\n}\n```\n\n```js src/index.js hidden\nimport React from 'react';\nimport {createRoot} from 'react-dom/client';\nimport './styles.css';\n\nimport App from './App';\n\nconst root = createRoot(document.getElementById('root'));\n// Not using StrictMode so double render logs are not shown.\nroot.render(<App />);\n```\n\n</Sandpack>\n\nWhen the button is clicked, `setOptimisticIsLiked` immediately updates the displayed state to show the heart as liked. Meanwhile, `await toggleLike` runs in the background. When the `await` completes, `setIsLiked` parent updates the \"real\" `isLiked` state, and the optimistic state is rendered to match this new value.\n\n<Note>\n\nThis example reads from `optimisticIsLiked` to calculate the next value. This works when the base state won't change, but if the base state might change while your Action is pending, you may want to use a state updater or the reducer.\n\nSee [Updating state based on the current state](#updating-state-based-on-current-state) for an example.\n\n</Note>\n\n---\n\n### Updating multiple values together {/*updating-multiple-values-together*/}\n\nWhen an optimistic update affects multiple related values, use a reducer to update them together. This ensures the UI stays consistent. \n\nHere's a follow button that updates both the follow state and follower count:\n\n<Sandpack>\n\n```js src/App.js\nimport { useState, startTransition } from 'react';\nimport { followUser, unfollowUser } from './actions.js';\nimport FollowButton from './FollowButton';\n\nexport default function App() {\n  const [user, setUser] = useState({\n    name: 'React',\n    isFollowing: false,\n    followerCount: 10500\n  });\n\n  async function followAction(shouldFollow) {\n    if (shouldFollow) {\n      await followUser(user.name);\n    } else {\n      await unfollowUser(user.name);\n    }\n    startTransition(() => {\n      setUser(current => ({\n        ...current,\n        isFollowing: shouldFollow,\n        followerCount: current.followerCount + (shouldFollow ? 1 : -1)\n      }));\n    });\n  }\n\n  return <FollowButton user={user} followAction={followAction} />;\n}\n```\n\n```js src/FollowButton.js active\nimport { useOptimistic, startTransition } from 'react';\n\nexport default function FollowButton({ user, followAction }) {\n  const [optimisticState, updateOptimistic] = useOptimistic(\n    { isFollowing: user.isFollowing, followerCount: user.followerCount },\n    (current, isFollowing) => ({\n      isFollowing,\n      followerCount: current.followerCount + (isFollowing ? 1 : -1)\n    })\n  );\n\n  function handleClick() {\n    const newFollowState = !optimisticState.isFollowing;\n    startTransition(async () => {\n      updateOptimistic(newFollowState);\n      await followAction(newFollowState);\n    });\n  }\n\n  return (\n    <div>\n      <p><strong>{user.name}</strong></p>\n      <p>{optimisticState.followerCount} followers</p>\n      <button onClick={handleClick}>\n        {optimisticState.isFollowing ? 'Unfollow' : 'Follow'}\n      </button>\n    </div>\n  );\n}\n```\n\n```js src/actions.js hidden\nexport async function followUser(name) {\n  await new Promise((res) => setTimeout(res, 1000));\n}\n\nexport async function unfollowUser(name) {\n  await new Promise((res) => setTimeout(res, 1000));\n}\n```\n\n</Sandpack>\n\nThe reducer receives the new `isFollowing` value and calculates both the new follow state and the updated follower count in a single update. This ensures the button text and count always stay in sync.\n\n\n<DeepDive>\n\n#### Choosing between updaters and reducers {/*choosing-between-updaters-and-reducers*/}\n\n`useOptimistic` supports two patterns for calculating state based on current state:\n\n**Updater functions** work like [useState updaters](/reference/react/useState#updating-state-based-on-the-previous-state). Pass a function to the setter:\n\n```js\nconst [optimistic, setOptimistic] = useOptimistic(value);\nsetOptimistic(current => !current);\n```\n\n**Reducers** separate the update logic from the setter call:\n\n```js\nconst [optimistic, dispatch] = useOptimistic(value, (current, action) => {\n  // Calculate next state based on current and action\n});\ndispatch(action);\n```\n\n**Use updaters** for calculations where the setter call naturally describes the update. This is similar to using `setState(prev => ...)` with `useState`.\n\n**Use reducers** when you need to pass data to the update (like which item to add) or when handling multiple types of updates with a single hook.\n\n**Why use a reducer?**\n\nReducers are essential when the base state might change while your Transition is pending. If `todos` changes while your add is pending (for example, another user added a todo), React will re-run your reducer with the new `todos` to recalculate what to show. This ensures your new todo is added to the latest list, not an outdated copy.\n\nAn updater function like `setOptimistic(prev => [...prev, newItem])` would only see the state from when the Transition started, missing any updates that happened during the async work.\n\n</DeepDive>\n\n---\n\n### Optimistically adding to a list {/*optimistically-adding-to-a-list*/}\n\nWhen you need to optimistically add items to a list, use a `reducer`:\n\n<Sandpack>\n\n```js src/App.js\nimport { useState, startTransition } from 'react';\nimport { addTodo } from './actions.js';\nimport TodoList from './TodoList';\n\nexport default function App() {\n  const [todos, setTodos] = useState([\n    { id: 1, text: 'Learn React' }\n  ]);\n\n  async function addTodoAction(newTodo) {\n    const savedTodo = await addTodo(newTodo);\n    startTransition(() => {\n      setTodos(todos => [...todos, savedTodo]);\n    });\n  }\n\n  return <TodoList todos={todos} addTodoAction={addTodoAction} />;\n}\n```\n\n```js src/TodoList.js active\nimport { useOptimistic, startTransition } from 'react';\n\nexport default function TodoList({ todos, addTodoAction }) {\n  const [optimisticTodos, addOptimisticTodo] = useOptimistic(\n    todos,\n    (currentTodos, newTodo) => [\n      ...currentTodos,\n      { id: newTodo.id, text: newTodo.text, pending: true }\n    ]\n  );\n\n  function handleAddTodo(text) {\n    const newTodo = { id: crypto.randomUUID(), text: text };\n    startTransition(async () => {\n      addOptimisticTodo(newTodo);\n      await addTodoAction(newTodo);\n    });\n  }\n\n  return (\n    <div>\n      <button onClick={() => handleAddTodo('New todo')}>Add Todo</button>\n      <ul>\n        {optimisticTodos.map(todo => (\n          <li key={todo.id}>\n            {todo.text} {todo.pending && \"(Adding...)\"}\n          </li>\n        ))}\n      </ul>\n    </div>\n  );\n}\n```\n\n```js src/actions.js hidden\nexport async function addTodo(todo) {\n  await new Promise((res) => setTimeout(res, 1000));\n  // In a real app, this would save to the server\n  return { ...todo, pending: false };\n}\n```\n\n</Sandpack>\n\nThe `reducer` receives the current list of todos and the new todo to add. This is important because if the `todos` prop changes while your add is pending (for example, another user added a todo), React will update your optimistic state by re-running the reducer with the updated list. This ensures your new todo is added to the latest list, not an outdated copy.\n\n<Note>\n\nEach optimistic item includes a `pending: true` flag so you can show loading state for individual items. When the server responds and the parent updates the canonical `todos` list with the saved item, the optimistic state updates to the confirmed item without the pending flag.\n\n</Note>\n\n---\n\n### Handling multiple `action` types {/*handling-multiple-action-types*/}\n\nWhen you need to handle multiple types of optimistic updates (like adding and removing items), use a reducer pattern with `action` objects. \n\nThis shopping cart example shows how to handle add and remove with a single reducer:\n\n<Sandpack>\n\n```js src/App.js\nimport { useState, startTransition } from 'react';\nimport { addToCart, removeFromCart, updateQuantity } from './actions.js';\nimport ShoppingCart from './ShoppingCart';\n\nexport default function App() {\n  const [cart, setCart] = useState([]);\n\n  const cartActions = {\n    async add(item) {\n      await addToCart(item);\n      startTransition(() => {\n        setCart(current => {\n          const exists = current.find(i => i.id === item.id);\n          if (exists) {\n            return current.map(i =>\n              i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i\n            );\n          }\n          return [...current, { ...item, quantity: 1 }];\n        });\n      });\n    },\n    async remove(id) {\n      await removeFromCart(id);\n      startTransition(() => {\n        setCart(current => current.filter(item => item.id !== id));\n      });\n    },\n    async updateQuantity(id, quantity) {\n      await updateQuantity(id, quantity);\n      startTransition(() => {\n        setCart(current =>\n          current.map(item =>\n            item.id === id ? { ...item, quantity } : item\n          )\n        );\n      });\n    }\n  };\n\n  return <ShoppingCart cart={cart} cartActions={cartActions} />;\n}\n```\n\n```js src/ShoppingCart.js active\nimport { useOptimistic, startTransition } from 'react';\n\nexport default function ShoppingCart({ cart, cartActions }) {\n  const [optimisticCart, dispatch] = useOptimistic(\n    cart,\n    (currentCart, action) => {\n      switch (action.type) {\n        case 'add':\n          const exists = currentCart.find(item => item.id === action.item.id);\n          if (exists) {\n            return currentCart.map(item =>\n              item.id === action.item.id\n                ? { ...item, quantity: item.quantity + 1, pending: true }\n                : item\n            );\n          }\n          return [...currentCart, { ...action.item, quantity: 1, pending: true }];\n        case 'remove':\n          return currentCart.filter(item => item.id !== action.id);\n        case 'update_quantity':\n          return currentCart.map(item =>\n            item.id === action.id\n              ? { ...item, quantity: action.quantity, pending: true }\n              : item\n          );\n        default:\n          return currentCart;\n      }\n    }\n  );\n\n  function handleAdd(item) {\n    startTransition(async () => {\n      dispatch({ type: 'add', item });\n      await cartActions.add(item);\n    });\n  }\n\n  function handleRemove(id) {\n    startTransition(async () => {\n      dispatch({ type: 'remove', id });\n      await cartActions.remove(id);\n    });\n  }\n\n  function handleUpdateQuantity(id, quantity) {\n    startTransition(async () => {\n      dispatch({ type: 'update_quantity', id, quantity });\n      await cartActions.updateQuantity(id, quantity);\n    });\n  }\n\n  const total = optimisticCart.reduce(\n    (sum, item) => sum + item.price * item.quantity,\n    0\n  );\n\n  return (\n    <div>\n      <h2>Shopping Cart</h2>\n      <div style={{ marginBottom: 16 }}>\n        <button onClick={() => handleAdd({\n          id: 1, name: 'T-Shirt', price: 25\n        })}>\n          Add T-Shirt ($25)\n        </button>{' '}\n        <button onClick={() => handleAdd({\n          id: 2, name: 'Mug', price: 15\n        })}>\n          Add Mug ($15)\n        </button>\n      </div>\n      {optimisticCart.length === 0 ? (\n        <p>Your cart is empty</p>\n      ) : (\n        <ul>\n          {optimisticCart.map(item => (\n            <li key={item.id}>\n              {item.name} - ${item.price} ×\n              {item.quantity}\n              {' '}= ${item.price * item.quantity}\n              <button\n                onClick={() => handleRemove(item.id)}\n                style={{ marginLeft: 8 }}\n              >\n                Remove\n              </button>\n              {item.pending && ' ...'}\n            </li>\n          ))}\n        </ul>\n      )}\n      <p><strong>Total: ${total}</strong></p>\n    </div>\n  );\n}\n```\n\n```js src/actions.js hidden\nexport async function addToCart(item) {\n  await new Promise((res) => setTimeout(res, 800));\n}\n\nexport async function removeFromCart(id) {\n  await new Promise((res) => setTimeout(res, 800));\n}\n\nexport async function updateQuantity(id, quantity) {\n  await new Promise((res) => setTimeout(res, 800));\n}\n```\n\n</Sandpack>\n\nThe reducer handles three `action` types (`add`, `remove`, `update_quantity`) and returns the new optimistic state for each. Each `action` sets a `pending: true` flag so you can show visual feedback while the [Server Function](/reference/rsc/server-functions) runs.\n\n---\n\n### Optimistic delete with error recovery {/*optimistic-delete-with-error-recovery*/}\n\nWhen deleting items optimistically, you should handle the case where the Action fails.\n\nThis example shows how to display an error message when a delete fails, and the UI automatically rolls back to show the item again.\n\n<Sandpack>\n\n```js src/App.js\nimport { useState, startTransition } from 'react';\nimport { deleteItem } from './actions.js';\nimport ItemList from './ItemList';\n\nexport default function App() {\n  const [items, setItems] = useState([\n    { id: 1, name: 'Learn React' },\n    { id: 2, name: 'Build an app' },\n    { id: 3, name: 'Deploy to production' },\n  ]);\n\n  async function deleteAction(id) {\n    await deleteItem(id);\n    startTransition(() => {\n      setItems(current => current.filter(item => item.id !== id));\n    });\n  }\n\n  return <ItemList items={items} deleteAction={deleteAction} />;\n}\n```\n\n```js src/ItemList.js active\nimport { useState, useOptimistic, startTransition } from 'react';\n\nexport default function ItemList({ items, deleteAction }) {\n  const [error, setError] = useState(null);\n  const [optimisticItems, removeItem] = useOptimistic(\n    items,\n    (currentItems, idToRemove) =>\n      currentItems.map(item =>\n        item.id === idToRemove\n          ? { ...item, deleting: true }\n          : item\n      )\n  );\n\n  function handleDelete(id) {\n    setError(null);\n    startTransition(async () => {\n      removeItem(id);\n      try {\n        await deleteAction(id);\n      } catch (e) {\n        setError(e.message);\n      }\n    });\n  }\n\n  return (\n    <div>\n      <h2>Your Items</h2>\n      <ul>\n        {optimisticItems.map(item => (\n          <li\n            key={item.id}\n            style={{\n              opacity: item.deleting ? 0.5 : 1,\n              textDecoration: item.deleting ? 'line-through' : 'none',\n              transition: 'opacity 0.2s'\n            }}\n          >\n            {item.name}\n            <button\n              onClick={() => handleDelete(item.id)}\n              disabled={item.deleting}\n              style={{ marginLeft: 8 }}\n            >\n              {item.deleting ? 'Deleting...' : 'Delete'}\n            </button>\n          </li>\n        ))}\n      </ul>\n      {error && (\n        <p style={{ color: 'red', padding: 8, background: '#fee' }}>\n          {error}\n        </p>\n      )}\n    </div>\n  );\n}\n```\n\n```js src/actions.js hidden\nexport async function deleteItem(id) {\n  await new Promise((res) => setTimeout(res, 1000));\n  // Item 3 always fails to demonstrate error recovery\n  if (id === 3) {\n    throw new Error('Cannot delete. Permission denied.');\n  }\n}\n```\n\n</Sandpack>\n\nTry deleting 'Deploy to production'. When the delete fails, the item automatically reappears in the list. \n\n---\n\n## Troubleshooting {/*troubleshooting*/}\n\n### I'm getting an error: \"An optimistic state update occurred outside a Transition or Action\" {/*an-optimistic-state-update-occurred-outside-a-transition-or-action*/}\n\nYou may see this error:\n\n<ConsoleBlockMulti>\n\n<ConsoleLogLine level=\"error\">\n\nAn optimistic state update occurred outside a Transition or Action. To fix, move the update to an Action, or wrap with `startTransition`.\n\n</ConsoleLogLine>\n\n</ConsoleBlockMulti>\n\nThe optimistic setter function must be called inside `startTransition`: \n\n```js\n// 🚩 Incorrect: outside a Transition\nfunction handleClick() {\n  setOptimistic(newValue);  // Warning!\n  // ...\n}\n\n// ✅ Correct: inside a Transition\nfunction handleClick() {\n  startTransition(async () => {\n    setOptimistic(newValue);\n    // ...\n  });\n}\n\n// ✅ Also correct: inside an Action prop\nfunction submitAction(formData) {\n  setOptimistic(newValue);\n  // ...\n}\n```\n\nWhen you call the setter outside an Action, the optimistic state will briefly appear and then immediately revert back to the original value. This happens because there's no Transition to \"hold\" the optimistic state while your Action runs.\n\n### I'm getting an error: \"Cannot update optimistic state while rendering\" {/*cannot-update-optimistic-state-while-rendering*/}\n\nYou may see this error:\n\n<ConsoleBlockMulti>\n\n<ConsoleLogLine level=\"error\">\n\nCannot update optimistic state while rendering.\n\n</ConsoleLogLine>\n\n</ConsoleBlockMulti>\n\nThis error occurs when you call the optimistic setter during the render phase of a component. You can only call it from event handlers, effects, or other callbacks:\n\n```js\n// 🚩 Incorrect: calling during render\nfunction MyComponent({ items }) {\n  const [isPending, setPending] = useOptimistic(false);\n\n  // This runs during render - not allowed!\n  setPending(true);\n  \n  // ...\n}\n\n// ✅ Correct: calling inside startTransition\nfunction MyComponent({ items }) {\n  const [isPending, setPending] = useOptimistic(false);\n\n  function handleClick() {\n    startTransition(() => {\n      setPending(true);\n      // ...\n    });\n  }\n\n  // ...\n}\n\n// ✅ Also correct: calling from an Action\nfunction MyComponent({ items }) {\n  const [isPending, setPending] = useOptimistic(false);\n\n  function action() {\n    setPending(true);\n    // ...\n  }\n\n  // ...\n}\n```\n\n### My optimistic updates show stale values {/*my-optimistic-updates-show-stale-values*/}\n\nIf your optimistic state seems to be based on old data, consider using an updater function or reducer to calculate the optimistic state relative to the current state.\n\n```js\n// May show stale data if state changes during Action\nconst [optimistic, setOptimistic] = useOptimistic(count);\nsetOptimistic(5);  // Always sets to 5, even if count changed\n\n// Better: relative updates handle state changes correctly\nconst [optimistic, adjust] = useOptimistic(count, (current, delta) => current + delta);\nadjust(1);  // Always adds 1 to whatever the current count is\n```\n\nSee [Updating state based on the current state](#updating-state-based-on-current-state) for details.\n\n### I don't know if my optimistic update is pending {/*i-dont-know-if-my-optimistic-update-is-pending*/}\n\nTo know when `useOptimistic` is pending, you have three options:\n\n1. **Check if `optimisticValue === value`**\n\n```js\nconst [optimistic, setOptimistic] = useOptimistic(value);\nconst isPending = optimistic !== value;\n```\n\nIf the values are not equal, there's a Transition in progress.\n\n2. **Add a `useTransition`**\n\n```js\nconst [isPending, startTransition] = useTransition();\nconst [optimistic, setOptimistic] = useOptimistic(value);\n\n//...\nstartTransition(() => {\n  setOptimistic(state);\n})\n```\n\nSince `useTransition` uses `useOptimistic` for `isPending` under the hood, this is equivalent to option 1.\n\n3. **Add a `pending` flag in your reducer**\n\n```js\nconst [optimistic, addOptimistic] = useOptimistic(\n  items,\n  (state, newItem) => [...state, { ...newItem, isPending: true }]\n);\n```\n\nSince each optimistic item has its own flag, you can show loading state for individual items.\n"
  },
  {
    "path": "src/content/reference/react/useReducer.md",
    "content": "---\ntitle: useReducer\n---\n\n<Intro>\n\n`useReducer`는 컴포넌트에 [reducer](/learn/extracting-state-logic-into-a-reducer)를 추가하는 React Hook입니다.\n\n```js\nconst [state, dispatch] = useReducer(reducer, initialArg, init?)\n```\n\n</Intro>\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `useReducer(reducer, initialArg, init?)` {/*usereducer*/}\n\n`useReducer`를 컴포넌트의 최상위에 호출하고, [reducer](/learn/extracting-state-logic-into-a-reducer)를 이용해 state를 관리합니다.\n\n```js\nimport { useReducer } from 'react';\n\nfunction reducer(state, action) {\n  // ...\n}\n\nfunction MyComponent() {\n  const [state, dispatch] = useReducer(reducer, { age: 42 });\n  // ...\n```\n\n[아래에서 더 많은 예시를 확인하세요.](#usage)\n\n#### 매개변수 {/*parameters*/}\n\n* `reducer`: state가 어떻게 업데이트 되는지 지정하는 Reducer 함수입니다. Reducer 함수는 반드시 순수 함수여야 하며, State와 Action을 인수로 받아야 하고, 다음 State를 반환해야 합니다. State와 Action에는 모든 데이터 타입이 할당될 수 있습니다.\n* `initialArg`: 초기 State가 계산되는 값입니다. 모든 데이터 타입이 할당될 수 있습니다. 초기 State가 어떻게 계산되는지는 다음 `init` 인수에 따라 달라집니다.\n* **선택사항** `init`: 초기 State를 반환하는 초기화 함수입니다. 이 함수가 인수에 할당되지 않으면 초기 State는 `initialArg`로 설정됩니다. 할당되었다면 초기 State는 `init(initialArg)`를 호출한 결과가 할당됩니다.\n\n#### 반환값 {/*returns*/}\n\n`useReducer`는 2개의 엘리먼트로 구성된 배열을 반환합니다.\n\n1. 현재 state. 첫번째 렌더링에서의 state는 `init(initialArg)` 또는 `initialArg`로 설정됩니다 (`init`이 없을 경우 `initialArg`로 설정됩니다).\n2. [`dispatch` 함수](#dispatch). `dispatch`는 state를 새로운 값으로 업데이트하고 리렌더링을 일으킵니다.\n\n#### 주의 사항 {/*caveats*/}\n\n* `useReducer`는 Hook이므로 **컴포넌트의 최상위** 또는 커스텀 Hook에서만 호출할 수 있습니다. 반복문이나 조건문에서는 사용할 수 없습니다. 필요한 경우 새로운 컴포넌트를 추출하고 해당 컴포넌트로 State를 옮겨서 사용할 수 있습니다.\n* `dispatch` 함수는 안정된 식별성을 가지고 있기 때문에, 흔히 Effect의 의존성에서 제외하는 것을 볼 수 있습니다. 하지만 포함해도 Effect를 실행하지는 않습니다. 린터에서 의존성을 생략해도 오류가 발생하지 않는다면, 그렇게 하는 것이 안전합니다. [Effect 의존성 제거에 대해 자세히 알아보세요.](/learn/removing-effect-dependencies#move-dynamic-objects-and-functions-inside-your-effect)\n* Strict Mode에서는 [우연한 비순수성](#my-reducer-or-initializer-function-runs-twice)을 찾아내기 위해 Reducer와 `init` 함수를 두번 호출합니다. 개발 환경에서만 한정된 동작이며, 배포<sup>Production</sup> 환경에는 영향을 미치지 않습니다. Reducer와 `init` 함수가 순수 함수라면(그래야만 하듯이) 로직에 어떠한 영향도 미치지 않습니다. 호출 중 하나의 결과는 무시합니다.\n\n---\n\n### `dispatch` 함수 {/*dispatch*/}\n\n`useReducer`에 의해 반환되는 `dispatch` 함수는 state를 새로운 값으로 업데이트하고 리렌더링을 일으킵니다. `dispatch`의 유일한 인수는 action입니다.\n\n```js\nconst [state, dispatch] = useReducer(reducer, { age: 42 });\n\nfunction handleClick() {\n  dispatch({ type: 'incremented_age' });\n  // ...\n```\n\nReact는 현재 `state`와 `dispatch`를 통해 전달된 action을 제공받아 호출된 `reducer`의 반환값을 통해 다음 state값을 설정합니다.\n\n#### 매개변수 {/*dispatch-parameters*/}\n\n* `action`: 사용자에 의해 수행된 활동입니다. 모든 데이터 타입이 할당될 수 있습니다. 컨벤션에 의해 action은 일반적으로 action을 정의하는 `type` 프로퍼티와 추가적인 정보를 표현하는 기타 프로퍼티를 포함한 객체로 구성됩니다.\n\n#### 반환값 {/*dispatch-returns*/}\n\n`dispatch` 함수는 어떤 값도 반환하지 않습니다.\n\n#### 주의 사항 {/*setstate-caveats*/}\n\n* `dispatch` 함수는 **오직 *다음* 렌더링에 사용할 state 변수만 업데이트 합니다.** 만약 `dispatch` 함수를 호출한 직후에 state 변수를 읽는다면 호출 이전의 [최신화되지 않은 값을 참조할 것입니다.](#ive-dispatched-an-action-but-logging-gives-me-the-old-state-value)\n\n* [`Object.is`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is) 비교를 통해 새롭게 제공된 값과 현재 `state`를 비교한 값이 같을 경우, React는 컴포넌트와 해당 컴포넌트의 자식 요소들의 리렌더링을 건너뜁니다. 이것은 최적화에 관련된 동작으로써 결과를 무시하기 전에 컴포넌트가 호출되지만, 호출된 결과가 코드에 영향을 미치지는 않습니다.\n\n* React는 [state의 업데이트를 batch합니다.](/learn/queueing-a-series-of-state-updates) **이벤트 핸들러의 모든 코드가 수행**되고 `set` 함수가 모두 호출된 후에 화면을 업데이트 합니다. 이는 하나의 이벤트에 리렌더링이 여러번 일어나는 것을 방지합니다. DOM 접근 등 이른 화면 업데이트를 강제해야할 특수한 상황이 있을 경우 [`flushSync`](/reference/react-dom/flushSync)를 사용할 수 있습니다.\n\n---\n\n## 사용법 {/*usage*/}\n\n### 컴포넌트에 reducer 추가하기 {/*adding-a-reducer-to-a-component*/}\n\nstate를 [reducer](/learn/extracting-state-logic-into-a-reducer)로 관리하기 위해 `useReducer`를 컴포넌트의 최상단에서 호출합니다.\n\n```js [[1, 8, \"state\"], [2, 8, \"dispatch\"], [4, 8, \"reducer\"], [3, 8, \"{ age: 42 }\"]]\nimport { useReducer } from 'react';\n\nfunction reducer(state, action) {\n  // ...\n}\n\nfunction MyComponent() {\n  const [state, dispatch] = useReducer(reducer, { age: 42 });\n  // ...\n```\n\n`useReducer`는 정확히 2개의 항목이 포함된 배열 반환합니다.\n\n1. state 변수의 <CodeStep step={1}>현재 state</CodeStep>. 최초에는 사용자가 제공한 <CodeStep step={3}>초기 state</CodeStep>로 초기화됩니다.\n2. <CodeStep step={2}>`dispatch` 함수</CodeStep>. 상호작용에 대응하여 state를 변경합니다.\n\n화면을 업데이트하려면 사용자가 수행한 활동을 의미하는 *action* 객체를 인수로하여 `dispatch` 함수를 호출하세요.\n\n```js [[2, 2, \"dispatch\"]]\nfunction handleClick() {\n  dispatch({ type: 'incremented_age' });\n}\n```\n\nReact는 현재 state와 action을 <CodeStep step={4}>reducer 함수</CodeStep>로 전달합니다. reducer는 다음 state를 계산한 후 반환합니다. React는 다음 state를 저장한 뒤에 컴포넌트와 함께 렌더링 하고 UI를 업데이트 합니다.\n\n<Sandpack>\n\n```js\nimport { useReducer } from 'react';\n\nfunction reducer(state, action) {\n  if (action.type === 'incremented_age') {\n    return {\n      age: state.age + 1\n    };\n  }\n  throw Error('Unknown action.');\n}\n\nexport default function Counter() {\n  const [state, dispatch] = useReducer(reducer, { age: 42 });\n\n  return (\n    <>\n      <button onClick={() => {\n        dispatch({ type: 'incremented_age' })\n      }}>\n        Increment age\n      </button>\n      <p>Hello! You are {state.age}.</p>\n    </>\n  );\n}\n```\n\n```css\nbutton { display: block; margin-top: 10px; }\n```\n\n</Sandpack>\n\n`useReducer`는 [`useState`](/reference/react/useState)와 매우 유사하지만, state 업데이트 로직을 이벤트 핸들러에서 컴포넌트 외부의 단일함수로 분리할 수 있다는 차이점이 있습니다. 자세한 사항은 [`useState`와 `useReducer` 비교하기](/learn/extracting-state-logic-into-a-reducer#comparing-usestate-and-usereducer)를 읽어보세요.\n\n---\n\n### reducer 함수 작성하기 {/*writing-the-reducer-function*/}\n\nreducer 함수는 아래와 같이 선언합니다.\n\n```js\nfunction reducer(state, action) {\n  // ...\n}\n```\n\n이후 다음 state를 계산할 코드를 작성하고, 계산된 state를 반환합니다. 보통은 컨벤션에 따라 [`switch` 문](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/switch)을 사용합니다. `switch`는 각 `case`를 이용해 다음 state를 계산하고 반환합니다.\n\n```js {4-7,10-13}\nfunction reducer(state, action) {\n  switch (action.type) {\n    case 'incremented_age': {\n      return {\n        name: state.name,\n        age: state.age + 1\n      };\n    }\n    case 'changed_name': {\n      return {\n        name: action.nextName,\n        age: state.age\n      };\n    }\n  }\n  throw Error('Unknown action: ' + action.type);\n}\n```\n\nActions은 다양한 형태가 될 수 있습니다. 하지만 컨벤션에 따라 액션이 무엇인지 정의하는 `type` 프로퍼티를 포함한 객체로 선언하는 것이 일반적입니다. `type`은 reducer가 다음 state를 계산하는데 필요한 최소한의 정보를 포함해야 합니다.\n\n```js {5,9-12}\nfunction Form() {\n  const [state, dispatch] = useReducer(reducer, { name: 'Taylor', age: 42 });\n\n  function handleButtonClick() {\n    dispatch({ type: 'incremented_age' });\n  }\n\n  function handleInputChange(e) {\n    dispatch({\n      type: 'changed_name',\n      nextName: e.target.value\n    });\n  }\n  // ...\n```\n\naction type 이름은 컴포넌트 내에서 지역적입니다. [각 action은 단일 상호작용을 설명하며, 데이터에 여러 변경 사항을 초래하더라도 하나의 상호작용만을 나타냅니다.](/learn/extracting-state-logic-into-a-reducer#writing-reducers-well) state의 형태는 임의적이지만, 일반적으로 객체나 배열일 것입니다.\n\n자세한 내용은 [state 로직을 reducer로 작성하기](/learn/extracting-state-logic-into-a-reducer)를 읽어보세요.\n\n<Pitfall>\n\nstate는 읽기 전용입니다. state의 객체나 배열을 변경하지 마세요!\n\n```js {4,5}\nfunction reducer(state, action) {\n  switch (action.type) {\n    case 'incremented_age': {\n      // 🚩 Don't mutate an object in state like this:\n      state.age = state.age + 1;\n      return state;\n    }\n```\n\n대신 reducer에서 새로운 객체를 반환하세요.\n\n```js {4-8}\nfunction reducer(state, action) {\n  switch (action.type) {\n    case 'incremented_age': {\n      // ✅ Instead, return a new object\n      return {\n        ...state,\n        age: state.age + 1\n      };\n    }\n```\n\n\n자세한 내용을 공부하시려면 [객체 State 업데이트하기](/learn/updating-objects-in-state)와 [배열 State 업데이트하기](/learn/updating-arrays-in-state)를 읽어보세요.\n\n</Pitfall>\n\n<Recipes titleText=\"기본적인 useReducer 예시\" titleId=\"examples-basic\">\n\n#### 폼 (객체) {/*form-object*/}\n\n이 예시에서는 reducer를 이용해 `name`과 `age` 필드를 가진 객체를 state로 관리합니다.\n\n<Sandpack>\n\n```js\nimport { useReducer } from 'react';\n\nfunction reducer(state, action) {\n  switch (action.type) {\n    case 'incremented_age': {\n      return {\n        name: state.name,\n        age: state.age + 1\n      };\n    }\n    case 'changed_name': {\n      return {\n        name: action.nextName,\n        age: state.age\n      };\n    }\n  }\n  throw Error('Unknown action: ' + action.type);\n}\n\nconst initialState = { name: 'Taylor', age: 42 };\n\nexport default function Form() {\n  const [state, dispatch] = useReducer(reducer, initialState);\n\n  function handleButtonClick() {\n    dispatch({ type: 'incremented_age' });\n  }\n\n  function handleInputChange(e) {\n    dispatch({\n      type: 'changed_name',\n      nextName: e.target.value\n    });\n  }\n\n  return (\n    <>\n      <input\n        value={state.name}\n        onChange={handleInputChange}\n      />\n      <button onClick={handleButtonClick}>\n        Increment age\n      </button>\n      <p>Hello, {state.name}. You are {state.age}.</p>\n    </>\n  );\n}\n```\n\n```css\nbutton { display: block; margin-top: 10px; }\n```\n\n</Sandpack>\n\n<Solution />\n\n#### 투두 리스트 (배열) {/*todo-list-array*/}\n\n이 예시에서는 Reducer를 이용해 할 일 목록들을 배열로 관리합니다. 배열의 업데이트는 [Mutation이 없이](/learn/updating-arrays-in-state) 이루어져야 합니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { useReducer } from 'react';\nimport AddTask from './AddTask.js';\nimport TaskList from './TaskList.js';\n\nfunction tasksReducer(tasks, action) {\n  switch (action.type) {\n    case 'added': {\n      return [...tasks, {\n        id: action.id,\n        text: action.text,\n        done: false\n      }];\n    }\n    case 'changed': {\n      return tasks.map(t => {\n        if (t.id === action.task.id) {\n          return action.task;\n        } else {\n          return t;\n        }\n      });\n    }\n    case 'deleted': {\n      return tasks.filter(t => t.id !== action.id);\n    }\n    default: {\n      throw Error('Unknown action: ' + action.type);\n    }\n  }\n}\n\nexport default function TaskApp() {\n  const [tasks, dispatch] = useReducer(\n    tasksReducer,\n    initialTasks\n  );\n\n  function handleAddTask(text) {\n    dispatch({\n      type: 'added',\n      id: nextId++,\n      text: text,\n    });\n  }\n\n  function handleChangeTask(task) {\n    dispatch({\n      type: 'changed',\n      task: task\n    });\n  }\n\n  function handleDeleteTask(taskId) {\n    dispatch({\n      type: 'deleted',\n      id: taskId\n    });\n  }\n\n  return (\n    <>\n      <h1>Prague itinerary</h1>\n      <AddTask\n        onAddTask={handleAddTask}\n      />\n      <TaskList\n        tasks={tasks}\n        onChangeTask={handleChangeTask}\n        onDeleteTask={handleDeleteTask}\n      />\n    </>\n  );\n}\n\nlet nextId = 3;\nconst initialTasks = [\n  { id: 0, text: 'Visit Kafka Museum', done: true },\n  { id: 1, text: 'Watch a puppet show', done: false },\n  { id: 2, text: 'Lennon Wall pic', done: false }\n];\n```\n\n```js src/AddTask.js hidden\nimport { useState } from 'react';\n\nexport default function AddTask({ onAddTask }) {\n  const [text, setText] = useState('');\n  return (\n    <>\n      <input\n        placeholder=\"Add task\"\n        value={text}\n        onChange={e => setText(e.target.value)}\n      />\n      <button onClick={() => {\n        setText('');\n        onAddTask(text);\n      }}>Add</button>\n    </>\n  )\n}\n```\n\n```js src/TaskList.js hidden\nimport { useState } from 'react';\n\nexport default function TaskList({\n  tasks,\n  onChangeTask,\n  onDeleteTask\n}) {\n  return (\n    <ul>\n      {tasks.map(task => (\n        <li key={task.id}>\n          <Task\n            task={task}\n            onChange={onChangeTask}\n            onDelete={onDeleteTask}\n          />\n        </li>\n      ))}\n    </ul>\n  );\n}\n\nfunction Task({ task, onChange, onDelete }) {\n  const [isEditing, setIsEditing] = useState(false);\n  let taskContent;\n  if (isEditing) {\n    taskContent = (\n      <>\n        <input\n          value={task.text}\n          onChange={e => {\n            onChange({\n              ...task,\n              text: e.target.value\n            });\n          }} />\n        <button onClick={() => setIsEditing(false)}>\n          Save\n        </button>\n      </>\n    );\n  } else {\n    taskContent = (\n      <>\n        {task.text}\n        <button onClick={() => setIsEditing(true)}>\n          Edit\n        </button>\n      </>\n    );\n  }\n  return (\n    <label>\n      <input\n        type=\"checkbox\"\n        checked={task.done}\n        onChange={e => {\n          onChange({\n            ...task,\n            done: e.target.checked\n          });\n        }}\n      />\n      {taskContent}\n      <button onClick={() => onDelete(task.id)}>\n        Delete\n      </button>\n    </label>\n  );\n}\n```\n\n```css\nbutton { margin: 5px; }\nli { list-style-type: none; }\nul, li { margin: 0; padding: 0; }\n```\n\n</Sandpack>\n\n<Solution />\n\n#### Immer를 이용해 업데이트 로직을 보다 간결하게 작성하기 {/*writing-concise-update-logic-with-immer*/}\n\nmutation 없이 배열이나 객체를 수정하는 것이 신경 쓰이고, 반복되는 코드를 줄이고 싶으시다면 [Immer](https://github.com/immerjs/use-immer#useimmerreducer)같은 라이브러리를 사용하실 수 있습니다. Immer는 코드를 간결하게 작성할 수 있게 해주며, mutation이 일어나는 것 처럼 코드를 작성하더라도 내부 동작에서는 immutable한 업데이트가 일어납니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { useImmerReducer } from 'use-immer';\nimport AddTask from './AddTask.js';\nimport TaskList from './TaskList.js';\n\nfunction tasksReducer(draft, action) {\n  switch (action.type) {\n    case 'added': {\n      draft.push({\n        id: action.id,\n        text: action.text,\n        done: false\n      });\n      break;\n    }\n    case 'changed': {\n      const index = draft.findIndex(t =>\n        t.id === action.task.id\n      );\n      draft[index] = action.task;\n      break;\n    }\n    case 'deleted': {\n      return draft.filter(t => t.id !== action.id);\n    }\n    default: {\n      throw Error('Unknown action: ' + action.type);\n    }\n  }\n}\n\nexport default function TaskApp() {\n  const [tasks, dispatch] = useImmerReducer(\n    tasksReducer,\n    initialTasks\n  );\n\n  function handleAddTask(text) {\n    dispatch({\n      type: 'added',\n      id: nextId++,\n      text: text,\n    });\n  }\n\n  function handleChangeTask(task) {\n    dispatch({\n      type: 'changed',\n      task: task\n    });\n  }\n\n  function handleDeleteTask(taskId) {\n    dispatch({\n      type: 'deleted',\n      id: taskId\n    });\n  }\n\n  return (\n    <>\n      <h1>Prague itinerary</h1>\n      <AddTask\n        onAddTask={handleAddTask}\n      />\n      <TaskList\n        tasks={tasks}\n        onChangeTask={handleChangeTask}\n        onDeleteTask={handleDeleteTask}\n      />\n    </>\n  );\n}\n\nlet nextId = 3;\nconst initialTasks = [\n  { id: 0, text: 'Visit Kafka Museum', done: true },\n  { id: 1, text: 'Watch a puppet show', done: false },\n  { id: 2, text: 'Lennon Wall pic', done: false },\n];\n```\n\n```js src/AddTask.js hidden\nimport { useState } from 'react';\n\nexport default function AddTask({ onAddTask }) {\n  const [text, setText] = useState('');\n  return (\n    <>\n      <input\n        placeholder=\"Add task\"\n        value={text}\n        onChange={e => setText(e.target.value)}\n      />\n      <button onClick={() => {\n        setText('');\n        onAddTask(text);\n      }}>Add</button>\n    </>\n  )\n}\n```\n\n```js src/TaskList.js hidden\nimport { useState } from 'react';\n\nexport default function TaskList({\n  tasks,\n  onChangeTask,\n  onDeleteTask\n}) {\n  return (\n    <ul>\n      {tasks.map(task => (\n        <li key={task.id}>\n          <Task\n            task={task}\n            onChange={onChangeTask}\n            onDelete={onDeleteTask}\n          />\n        </li>\n      ))}\n    </ul>\n  );\n}\n\nfunction Task({ task, onChange, onDelete }) {\n  const [isEditing, setIsEditing] = useState(false);\n  let taskContent;\n  if (isEditing) {\n    taskContent = (\n      <>\n        <input\n          value={task.text}\n          onChange={e => {\n            onChange({\n              ...task,\n              text: e.target.value\n            });\n          }} />\n        <button onClick={() => setIsEditing(false)}>\n          Save\n        </button>\n      </>\n    );\n  } else {\n    taskContent = (\n      <>\n        {task.text}\n        <button onClick={() => setIsEditing(true)}>\n          Edit\n        </button>\n      </>\n    );\n  }\n  return (\n    <label>\n      <input\n        type=\"checkbox\"\n        checked={task.done}\n        onChange={e => {\n          onChange({\n            ...task,\n            done: e.target.checked\n          });\n        }}\n      />\n      {taskContent}\n      <button onClick={() => onDelete(task.id)}>\n        Delete\n      </button>\n    </label>\n  );\n}\n```\n\n```css\nbutton { margin: 5px; }\nli { list-style-type: none; }\nul, li { margin: 0; padding: 0; }\n```\n\n```json package.json\n{\n  \"dependencies\": {\n    \"immer\": \"1.7.3\",\n    \"react\": \"latest\",\n    \"react-dom\": \"latest\",\n    \"react-scripts\": \"latest\",\n    \"use-immer\": \"0.5.1\"\n  },\n  \"scripts\": {\n    \"start\": \"react-scripts start\",\n    \"build\": \"react-scripts build\",\n    \"test\": \"react-scripts test --env=jsdom\",\n    \"eject\": \"react-scripts eject\"\n  }\n}\n```\n\n</Sandpack>\n\n<Solution />\n\n</Recipes>\n\n---\n\n### 초기 state 재생성 방지하기 {/*avoiding-recreating-the-initial-state*/}\n\nReact는 초기 state를 저장한 후, 다음 렌더링에서는 이를 무시합니다.\n\n```js\nfunction createInitialState(username) {\n  // ...\n}\n\nfunction TodoList({ username }) {\n  const [state, dispatch] = useReducer(reducer, createInitialState(username));\n  // ...\n```\n\n`createInitialState(username)`의 반환값이 초기 렌더링에만 사용되더라도 함수는 매 렌더링마다 호출될 것입니다. 함수가 큰 배열이나 무거운 연산을 다룰 경우에는 성능상 낭비가 될 수 있습니다.\n\n이를 해결하기 위한 방법으로는 `useReducer`의 3번째 인수에 **_초기화 함수_ 를 전달하는 방법**이 있습니다.\n\n```js {6}\nfunction createInitialState(username) {\n  // ...\n}\n\nfunction TodoList({ username }) {\n  const [state, dispatch] = useReducer(reducer, username, createInitialState);\n  // ...\n```\n\n`createInitialState()`처럼 함수를 호출해서 전달하는 것이 아니라, `createInitialState` *함수 자체*를 전달해야 한다는 것을 기억하세요. 이 방법을 이용하면 초기화 이후에 초기 state가 다시 생성되는 일은 발생하지 않습니다.\n\n위의 예시에서는 `createInitialState` 함수가 `username`을 인수로 받습니다. 만약 초기화 함수가 초기 state를 계산하는 것에 어떤 인수도 필요하지 않다면, `useReducer`의 두번째 인수에 `null`을 전달할 수 있습니다.\n\n<Recipes titleText=\"초기화 함수를 전달하는 것과 초기 state를 직접 전달하는 것의 차이점\" titleId=\"examples-initializer\">\n\n#### 초기화 함수 전달 {/*passing-the-initializer-function*/}\n\n이 예시에서는 초기화 단계에서만 동작하는 함수인 `createInitialState`를 초기화 함수로 전달합니다. 이 함수는 인풋에 입력 할 때 발생하는 리렌더링 상황 등에서는 호출되지 않습니다.\n\n<Sandpack>\n\n```js src/App.js hidden\nimport TodoList from './TodoList.js';\n\nexport default function App() {\n  return <TodoList username=\"Taylor\" />;\n}\n```\n\n```js src/TodoList.js active\nimport { useReducer } from 'react';\n\nfunction createInitialState(username) {\n  const initialTodos = [];\n  for (let i = 0; i < 50; i++) {\n    initialTodos.push({\n      id: i,\n      text: username + \"'s task #\" + (i + 1)\n    });\n  }\n  return {\n    draft: '',\n    todos: initialTodos,\n  };\n}\n\nfunction reducer(state, action) {\n  switch (action.type) {\n    case 'changed_draft': {\n      return {\n        draft: action.nextDraft,\n        todos: state.todos,\n      };\n    };\n    case 'added_todo': {\n      return {\n        draft: '',\n        todos: [{\n          id: state.todos.length,\n          text: state.draft\n        }, ...state.todos]\n      }\n    }\n  }\n  throw Error('Unknown action: ' + action.type);\n}\n\nexport default function TodoList({ username }) {\n  const [state, dispatch] = useReducer(\n    reducer,\n    username,\n    createInitialState\n  );\n  return (\n    <>\n      <input\n        value={state.draft}\n        onChange={e => {\n          dispatch({\n            type: 'changed_draft',\n            nextDraft: e.target.value\n          })\n        }}\n      />\n      <button onClick={() => {\n        dispatch({ type: 'added_todo' });\n      }}>Add</button>\n      <ul>\n        {state.todos.map(item => (\n          <li key={item.id}>\n            {item.text}\n          </li>\n        ))}\n      </ul>\n    </>\n  );\n}\n```\n\n</Sandpack>\n\n<Solution />\n\n#### 초기 state 직접 전달 {/*passing-the-initial-state-directly*/}\n\n이 예시에서는 초기화 함수를 **전달하지 않으므로**, `createInitialState` 함수는 인풋에 입력을 할때 발생하는 리렌더링에서도 매번 호출됩니다. 이 코드는 동작에는 큰 차이가 없을 수 있지만, 효율성이 떨어집니다.\n\n<Sandpack>\n\n```js src/App.js hidden\nimport TodoList from './TodoList.js';\n\nexport default function App() {\n  return <TodoList username=\"Taylor\" />;\n}\n```\n\n```js src/TodoList.js active\nimport { useReducer } from 'react';\n\nfunction createInitialState(username) {\n  const initialTodos = [];\n  for (let i = 0; i < 50; i++) {\n    initialTodos.push({\n      id: i,\n      text: username + \"'s task #\" + (i + 1)\n    });\n  }\n  return {\n    draft: '',\n    todos: initialTodos,\n  };\n}\n\nfunction reducer(state, action) {\n  switch (action.type) {\n    case 'changed_draft': {\n      return {\n        draft: action.nextDraft,\n        todos: state.todos,\n      };\n    };\n    case 'added_todo': {\n      return {\n        draft: '',\n        todos: [{\n          id: state.todos.length,\n          text: state.draft\n        }, ...state.todos]\n      }\n    }\n  }\n  throw Error('Unknown action: ' + action.type);\n}\n\nexport default function TodoList({ username }) {\n  const [state, dispatch] = useReducer(\n    reducer,\n    createInitialState(username)\n  );\n  return (\n    <>\n      <input\n        value={state.draft}\n        onChange={e => {\n          dispatch({\n            type: 'changed_draft',\n            nextDraft: e.target.value\n          })\n        }}\n      />\n      <button onClick={() => {\n        dispatch({ type: 'added_todo' });\n      }}>Add</button>\n      <ul>\n        {state.todos.map(item => (\n          <li key={item.id}>\n            {item.text}\n          </li>\n        ))}\n      </ul>\n    </>\n  );\n}\n```\n\n</Sandpack>\n\n<Solution />\n\n</Recipes>\n\n---\n\n## 트러블 슈팅 {/*troubleshooting*/}\n\n### dispatch로 action을 호출해도 오래된 state 값이 출력됩니다. {/*ive-dispatched-an-action-but-logging-gives-me-the-old-state-value*/}\n\n`dispatch` 함수의 호출은 **현재 동작하고 있는 코드의 state를 변경하지 않습니다.**\n\n```js {4,5,8}\nfunction handleClick() {\n  console.log(state.age);  // 42\n\n  dispatch({ type: 'incremented_age' }); // Request a re-render with 43\n  console.log(state.age);  // Still 42!\n\n  setTimeout(() => {\n    console.log(state.age); // Also 42!\n  }, 5000);\n}\n```\n\n이러한 현상은 [State가 스냅샷으로서](/learn/state-as-a-snapshot) 사용되기 때문에 일어납니다. state를 업데이트하면 새로운 state를 이용한 또 다른 렌더링이 요청되지만, 이미 실행중인 이벤트 핸들러 내의 `state` 자바스크립트 변수에는 영향을 미치지 않습니다.\n\n만약 다음 state 값을 알고 싶다면, reducer 함수를 직접 호출해서 다음 값을 계산해볼 수 있습니다.\n\n```js\nconst action = { type: 'incremented_age' };\ndispatch(action);\n\nconst nextState = reducer(state, action);\nconsole.log(state);     // { age: 42 }\nconsole.log(nextState); // { age: 43 }\n```\n\n---\n\n### dispatch로 action을 호출해도 화면이 업데이트되지 않습니다. {/*ive-dispatched-an-action-but-the-screen-doesnt-update*/}\n\nReact는 **이전 state와 다음 state를 비교했을 때, 값이 일치한다면 업데이트가 무시됩니다.** 비교는 [`Object.is`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is)를 통해 이루어집니다. 이런 현상은 보통 객체나 배열의 state를 직접적으로 수정했을 때 발생합니다.\n\n```js {4-5,9-10}\nfunction reducer(state, action) {\n  switch (action.type) {\n    case 'incremented_age': {\n      // 🚩 Wrong: mutating existing object\n      state.age++;\n      return state;\n    }\n    case 'changed_name': {\n      // 🚩 Wrong: mutating existing object\n      state.name = action.nextName;\n      return state;\n    }\n    // ...\n  }\n}\n```\n\nReact는 기존의 `state` 객체가 mutation된 상태로 반환된다면 업데이트를 무시합니다. 이러한 현상을 방지하기 위해서는 객체나 배열을 mutation시키지 않고 [객체 state를 변경](/learn/updating-objects-in-state)하거나 [배열 state](/learn/updating-arrays-in-state)를 변경해야 합니다.\n\n```js {4-8,11-15}\nfunction reducer(state, action) {\n  switch (action.type) {\n    case 'incremented_age': {\n      // ✅ Correct: creating a new object\n      return {\n        ...state,\n        age: state.age + 1\n      };\n    }\n    case 'changed_name': {\n      // ✅ Correct: creating a new object\n      return {\n        ...state,\n        name: action.nextName\n      };\n    }\n    // ...\n  }\n}\n```\n\n---\n\n### reducer의 state 일부가 dispatch된 이후에 undefined가 할당됩니다. {/*a-part-of-my-reducer-state-becomes-undefined-after-dispatching*/}\n\n각각의 `case`가 **새로운 state를 반환할 때 기존에 있던 필드를 모두 복사하는지** 확인해보세요.\n\n```js {5}\nfunction reducer(state, action) {\n  switch (action.type) {\n    case 'incremented_age': {\n      return {\n        ...state, // Don't forget this!\n        age: state.age + 1\n      };\n    }\n    // ...\n```\n\n위의 코드에서는 `...state`가 없다면 다음 state는 오로지 `age` 필드만 포함하거나, 아무것도 포함하지 않을 것입니다.\n\n---\n\n### reducer의 모든 state가 dispatch가 이루어 진 후 undefined가 할당됩니다. {/*my-entire-reducer-state-becomes-undefined-after-dispatching*/}\n\nstate에 예기치 않은 `undefined`가 할당되고 있다면 case 중 하나에 `return`이 누락되었거나 action의 타입이 `case`와 짝지어지지 않았을 수 있습니다. 이유를 찾기 위해 switch문 밖에서 에러를 throw 할 수 있습니다.\n\n```js {10}\nfunction reducer(state, action) {\n  switch (action.type) {\n    case 'incremented_age': {\n      // ...\n    }\n    case 'edited_name': {\n      // ...\n    }\n  }\n  throw Error('Unknown action: ' + action.type);\n}\n```\n\n이 외에도 실수를 방지하기 위해 타입스크립트 같은 정적 타입 체커를 사용할 수 있습니다.\n\n---\n\n### \"Too many re-renders\" 오류가 발생합니다. {/*im-getting-an-error-too-many-re-renders*/}\n\n`Too many re-renders. React limits the number of renders to prevent an infinite loop.`라는 에러 메세지를 받을 수 있습니다. 일반적으로는 렌더링 과정에서 dispatch가 실행될 때 이러한 일이 일어납니다. 렌더링은 dispatch를 야기하고, dispatch는 렌더링을 야기하므로 렌더링 무한 루프가 일어납니다. 이러한 상황은 이벤트 핸들러를 잘못 호출할 때 종종 발생합니다.\n\n```js {1-2}\n// 🚩 Wrong: calls the handler during render\nreturn <button onClick={handleClick()}>Click me</button>\n\n// ✅ Correct: passes down the event handler\nreturn <button onClick={handleClick}>Click me</button>\n\n// ✅ Correct: passes down an inline function\nreturn <button onClick={(e) => handleClick(e)}>Click me</button>\n```\n\n오류의 원인을 찾을 수 없는 경우에는 어느 `dispatch` 함수에서 에러가 생성되는지 확인하기 위해 콘솔창의 오류 옆에 있는 화살표를 클릭한 후 자바스크립트 스택을 찾아보세요.\n\n---\n\n### reducer와 초기화 함수가 두번 호출됩니다. {/*my-reducer-or-initializer-function-runs-twice*/}\n\nReact는 [엄격 모드](/reference/react/StrictMode)일 때 reducer와 초기화 함수를 두번씩 호출합니다. 이 현상은 코드 실행에 문제가 되지 않습니다.\n\n이러한 현상은 컴포넌트가 [순수함수로 유지될 수 있도록](/learn/keeping-components-pure) 오직 개발 환경에서만 일어나며, 두개의 호출 중 하나는 무시됩니다. 컴포넌트, 초기화 함수, reducer가 순수하다면 로직에 아무런 영향을 미치지 않지만, 순수하지 않다면 실수를 알아챌 수 있도록 알려줍니다.\n\n예시로, 아래의 순수하지 않은 reducer 함수는 state 배열에 mutation을 일으키고 있습니다.\n\n```js {4-6}\nfunction reducer(state, action) {\n  switch (action.type) {\n    case 'added_todo': {\n      // 🚩 Mistake: mutating state\n      state.todos.push({ id: nextId++, text: action.text });\n      return state;\n    }\n    // ...\n  }\n}\n```\n\nReact는 reducer 함수를 두 번 호출하므로 todo가 두 개 추가되는 것을 볼 수 있고, 이를 통해 reducer 함수 작성에 실수가 있다는 것을 알아낼 수 있습니다. 이러한 실수는 [배열을 mutation 하지 않고 교체하는 방법](/learn/updating-arrays-in-state#adding-to-an-array)을 통해 수정할 수 있습니다.\n\n```js {4-11}\nfunction reducer(state, action) {\n  switch (action.type) {\n    case 'added_todo': {\n      // ✅ Correct: replacing with new state\n      return {\n        ...state,\n        todos: [\n          ...state.todos,\n          { id: nextId++, text: action.text }\n        ]\n      };\n    }\n    // ...\n  }\n}\n```\n\n이제 reducer 함수는 순수하므로, 여러번 호출되어도 같은 값을 보장할 수 있습니다. React는 순수성을 보장하기 위해 개발 환경에서 두번씩 호출합니다. **오로지 컴포넌트와 초기화 함수, reducer 함수만 순수할 필요가 있습니다.** 이벤트 핸들러는 순수할 필요가 없습니다. 따라서 이벤트 핸들러는 두 번씩 호출되지 않습니다.\n\n자세한 사항은 [컴포넌트를 순수하게 유지하기](/learn/keeping-components-pure)를 읽어보세요.\n"
  },
  {
    "path": "src/content/reference/react/useRef.md",
    "content": "---\ntitle: useRef\n---\n\n<Intro>\n\n`useRef`는 렌더링에 필요하지 않은 값을 참조할 수 있는 React Hook입니다.\n\n```js\nconst ref = useRef(initialValue)\n```\n\n</Intro>\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `useRef(initialValue)` {/*useref*/}\n\n컴포넌트의 최상위 레벨에서 `useRef`를 호출하여 [ref](/learn/referencing-values-with-refs)를 선언합니다.\n\n```js\nimport { useRef } from 'react';\n\nfunction MyComponent() {\n  const intervalRef = useRef(0);\n  const inputRef = useRef(null);\n  // ...\n```\n\n[아래에서 더 많은 예시를 확인하세요.](#usage)\n\n#### 매개변수 {/*parameters*/}\n\n* `initialValue`: ref 객체의 `current`프로퍼티 초기 설정값입니다. 여기에는 어떤 유형의 값이든 지정할 수 있습니다. 이 인자는 초기 렌더링 이후부터는 무시됩니다.\n\n#### 반환값 {/*returns*/}\n\n`useRef`는 단일 프로퍼티를 가진 객체를 반환합니다:\n\n* `current`: 처음에는 전달한 `initialValue`로 설정됩니다. 나중에 다른 값으로 바꿀 수 있습니다. ref 객체를 JSX 노드의 `ref`어트리뷰트로 React에 전달하면 React는 `current`프로퍼티를 설정합니다.\n\n다음 렌더링에서 `useRef`는 동일한 객체를 반환합니다.\n\n#### 주의 사항 {/*caveats*/}\n\n* `ref.current` 프로퍼티는 state와 달리 변이할 수 있습니다. 그러나 렌더링에 사용되는 객체(예: state의 일부)를 포함하는 경우 해당 객체를 변이해서는 안 됩니다.\n* `ref.current` 프로퍼티를 변경해도 React는 컴포넌트를 다시 렌더링하지 않습니다. ref는 일반 JavaScript 객체이기 때문에 React는 사용자가 언제 변경했는지 알지 못합니다.\n* [초기화](#avoiding-recreating-the-ref-contents)를 제외하고는 렌더링 중에 `ref.current`를 쓰거나 *읽지* 마세요. 이렇게 하면 컴포넌트의 동작을 예측할 수 없게 됩니다.\n* Strict Mode에서 React는 **컴포넌트 함수를 두 번 호출하여** [의도하지 않은 변경을 찾을 수 있도록 돕습니다.](/reference/react/useState#my-initializer-or-updater-function-runs-twice) 이는 개발 환경 전용 동작이며 Production 환경에는 영향을 미치지 않습니다. 각 ref 객체는 두 번 생성되고 그중 하나는 버려집니다. 컴포넌트 함수가 순수하다면(그래야만 합니다), 컴포넌트의 로직에 영향을 미치지 않습니다.\n\n---\n\n## 사용법 {/*usage*/}\n\n### ref로 값 참조하기 {/*referencing-a-value-with-a-ref*/}\n\n컴포넌트의 최상위 레벨에서 `useRef`를 호출하여 하나 이상의 [ref](/learn/referencing-values-with-refs)를 선언합니다.\n\n```js [[1, 4, \"intervalRef\"], [3, 4, \"0\"]]\nimport { useRef } from 'react';\n\nfunction Stopwatch() {\n  const intervalRef = useRef(0);\n  // ...\n```\n\n`useRef`는 처음에 제공한 <CodeStep step={3}>초기값</CodeStep>으로 설정된 단일 <CodeStep step={2}>`current` 프로퍼티</CodeStep>가 있는 <CodeStep step={1}>ref 객체</CodeStep>를 반환합니다.\n\n다음 렌더링에서 `useRef`는 동일한 객체를 반환합니다. 정보를 저장하고 나중에 읽을 수 있도록 `current` 속성을 변경할 수 있습니다. [state](/reference/react/useState)가 떠오를 수 있지만, 둘 사이에는 중요한 차이점이 있습니다.\n\n**ref를 변경해도 리렌더링을 촉발하지 않습니다.** 즉 ref는 컴포넌트의 시각적 출력에 영향을 미치지 않는 정보를 저장하는 데 적합합니다. 예를 들어 [interval ID](https://developer.mozilla.org/en-US/docs/Web/API/setInterval)를 저장했다가 나중에 불러와야 하는 경우 ref에 넣을 수 있습니다. ref 내부의 값을 업데이트하려면 <CodeStep step={2}>`current` 프로퍼티</CodeStep>를 수동으로 변경해야 합니다:\n\n```js [[2, 5, \"intervalRef.current\"]]\nfunction handleStartClick() {\n  const intervalId = setInterval(() => {\n    // ...\n  }, 1000);\n  intervalRef.current = intervalId;\n}\n```\n\n나중에 ref에서 해당 interval ID를 읽어 [해당 interval을 취소](https://developer.mozilla.org/en-US/docs/Web/API/clearInterval)할 수 있습니다:\n\n```js [[2, 2, \"intervalRef.current\"]]\nfunction handleStopClick() {\n  const intervalId = intervalRef.current;\n  clearInterval(intervalId);\n}\n```\n\nref를 사용하면 다음을 보장합니다:\n\n- (렌더링할 때마다 재설정되는 일반 변수와 달리) 리렌더링 사이에 **정보를 저장**할 수 있습니다.\n- (리렌더링을 촉발하는 state 변수와 달리) 변경해도 **리렌더링을 촉발하지 않습니다.**\n- (정보가 공유되는 외부 변수와 달리) 각각의 컴포넌트에 **로컬로 저장됩니다.**\n\nref를 변경해도 다시 렌더링되지 않으므로 화면에 표시되는 정보를 저장하는 데는 ref가 적합하지 않습니다. 대신 state를 사용하세요. 더 자세한 내용은 [`useRef`와 `useState` 중 선택하기](/learn/referencing-values-with-refs#differences-between-refs-and-state)에서 확인하세요.\n\n<Recipes titleText=\"useRef로 값을 참조하는 예시\" titleId=\"examples-value\">\n\n#### counter 클릭하기 {/*click-counter*/}\n\n이 컴포넌트는 ref를 사용하여 버튼이 클릭된 횟수를 추적합니다. 클릭 횟수는 이벤트 핸들러에서만 읽고 쓰기 때문에 여기서는 state 대신 ref를 사용해도 괜찮습니다.\n\n<Sandpack>\n\n```js\nimport { useRef } from 'react';\n\nexport default function Counter() {\n  let ref = useRef(0);\n\n  function handleClick() {\n    ref.current = ref.current + 1;\n    alert('You clicked ' + ref.current + ' times!');\n  }\n\n  return (\n    <button onClick={handleClick}>\n      Click me!\n    </button>\n  );\n}\n```\n\n</Sandpack>\n\nJSX에 `{ref.current}`를 표시하면 클릭 시 번호가 업데이트되지 않습니다. `ref.current`를 설정해도 리렌더링을 촉발하지 않기 때문입니다. 렌더링에 사용하는 정보는 ref가 아닌 state여야 합니다.\n\n<Solution />\n\n#### 스톱워치 {/*a-stopwatch*/}\n\n예시에서는 state와 ref의 조합을 사용합니다. `startTime`과 `now`는 모두 렌더링에 사용되기 때문에 state 변수입니다. 그러나 버튼을 누를 때 interval을 멈출 수 있게 하기 위해선 [interval ID](https://developer.mozilla.org/en-US/docs/Web/API/setInterval)도 보유해야 합니다. interval ID는 렌더링에 사용되지 않으므로 ref에 보관하고 수동으로 업데이트하는 것이 적절합니다.\n\n<Sandpack>\n\n```js\nimport { useState, useRef } from 'react';\n\nexport default function Stopwatch() {\n  const [startTime, setStartTime] = useState(null);\n  const [now, setNow] = useState(null);\n  const intervalRef = useRef(null);\n\n  function handleStart() {\n    setStartTime(Date.now());\n    setNow(Date.now());\n\n    clearInterval(intervalRef.current);\n    intervalRef.current = setInterval(() => {\n      setNow(Date.now());\n    }, 10);\n  }\n\n  function handleStop() {\n    clearInterval(intervalRef.current);\n  }\n\n  let secondsPassed = 0;\n  if (startTime != null && now != null) {\n    secondsPassed = (now - startTime) / 1000;\n  }\n\n  return (\n    <>\n      <h1>Time passed: {secondsPassed.toFixed(3)}</h1>\n      <button onClick={handleStart}>\n        Start\n      </button>\n      <button onClick={handleStop}>\n        Stop\n      </button>\n    </>\n  );\n}\n```\n\n</Sandpack>\n\n<Solution />\n\n</Recipes>\n\n<Pitfall>\n\n**렌더링 중에는 `ref.current`를 쓰거나 _읽지_ 마세요.**\n\nReact는 컴포넌트의 본문이 [순수 함수처럼 동작하기](/learn/keeping-components-pure)를 기대합니다:\n\n- 입력값들([props](/learn/passing-props-to-a-component), [state](/learn/state-a-components-memory), [context](/learn/passing-data-deeply-with-context))이 동일하면 완전히 동일한 JSX를 반환해야 합니다.\n- 다른 순서나 다른 인수를 사용하여 호출해도 다른 호출의 결과에 영향을 미치지 않아야 합니다.\n\n**렌더링 중에** ref를 읽거나 쓰면 이러한 기대가 깨집니다.\n\n```js {3-4,6-7}\nfunction MyComponent() {\n  // ...\n  // 🚩 Don't write a ref during rendering\n  myRef.current = 123;\n  // ...\n  // 🚩 Don't read a ref during rendering\n  return <h1>{myOtherRef.current}</h1>;\n}\n```\n\n**대신 이벤트 핸들러나 Effect에서** ref를 읽거나 쓸 수 있습니다.\n\n```js {4-5,9-10}\nfunction MyComponent() {\n  // ...\n  useEffect(() => {\n    // ✅ You can read or write refs in effects\n    myRef.current = 123;\n  });\n  // ...\n  function handleClick() {\n    // ✅ You can read or write refs in event handlers\n    doSomething(myOtherRef.current);\n  }\n  // ...\n}\n```\n\n렌더링 중에 무언가를 읽거나 [써야](/reference/react/useState#storing-information-from-previous-renders)*만* 하는 경우, 대신 [state를 사용](/reference/react/useState)하세요.\n\n컴포넌트는 이러한 규칙을 어기더라도 여전히 작동할 수도 있지만, React에 추가되는 대부분의 새로운 기능들은 이러한 기대에 의존합니다. 자세한 내용은 [컴포넌트를 순수하게 유지하기](/learn/keeping-components-pure#where-you-_can_-cause-side-effects)에서 확인하세요.\n\n</Pitfall>\n\n---\n\n### ref로 DOM 조작하기 {/*manipulating-the-dom-with-a-ref*/}\n\nref를 사용하여 [DOM](https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API)을 조작하는 것은 특히 일반적입니다. React에는 이를 위한 기본 지원이 있습니다.\n\n먼저 <CodeStep step={3}>초기값</CodeStep>이 `null`인 <CodeStep step={1}>ref 객체</CodeStep>를 선언하세요:\n\n```js [[1, 4, \"inputRef\"], [3, 4, \"null\"]]\nimport { useRef } from 'react';\n\nfunction MyComponent() {\n  const inputRef = useRef(null);\n  // ...\n```\n\n그런 다음 ref 객체를 `ref` 속성으로 조작하려는 DOM 노드의 JSX에 전달하세요:\n\n```js [[1, 2, \"inputRef\"]]\n  // ...\n  return <input ref={inputRef} />;\n```\n\nReact가 DOM 노드를 생성하고 화면에 그린 후, React는 ref 객체의 <CodeStep step={2}>`current`프로퍼티</CodeStep>를 DOM 노드로 설정합니다. 이제 DOM 노드 `<input>` 접근해 [`focus()`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus)와 같은 메서드를 호출할 수 있습니다.\n\n```js [[2, 2, \"inputRef.current\"]]\n  function handleClick() {\n    inputRef.current.focus();\n  }\n```\n\n노드가 화면에서 제거되면 React는 `current` 프로퍼티를 다시 `null`로 설정합니다.\n\n자세한 내용은 [ref로 DOM 조작하기](/learn/manipulating-the-dom-with-refs)에서 알아보세요.\n\n<Recipes titleText=\"useRef로 DOM을 조작하는 예시\" titleId=\"examples-dom\">\n\n#### 텍스트 input에 초점 맞추기 {/*focusing-a-text-input*/}\n\n이 예시에서는 버튼을 클릭하면 입력에 초점이 맞춰집니다.\n\n<Sandpack>\n\n```js\nimport { useRef } from 'react';\n\nexport default function Form() {\n  const inputRef = useRef(null);\n\n  function handleClick() {\n    inputRef.current.focus();\n  }\n\n  return (\n    <>\n      <input ref={inputRef} />\n      <button onClick={handleClick}>\n        Focus the input\n      </button>\n    </>\n  );\n}\n```\n\n</Sandpack>\n\n<Solution />\n\n#### 이미지 스크롤하기 {/*scrolling-an-image-into-view*/}\n\n이 예시에서는 버튼을 클릭하면 이미지가 스크롤됩니다. 목록 DOM 노드에 대한 ref를 사용한 다음 DOM [`querySelectorAll`](https://developer.mozilla.org/ko/docs/Web/API/Document/querySelectorAll) API를 호출하여 스크롤하려는 이미지를 찾습니다.\n\n<Sandpack>\n\n```js\nimport { useRef } from 'react';\n\nexport default function CatFriends() {\n  const listRef = useRef(null);\n\n  function scrollToIndex(index) {\n    const listNode = listRef.current;\n    // 다음 코드는 특정 DOM 구조를 가정합니다:\n    const imgNode = listNode.querySelectorAll('li > img')[index];\n    imgNode.scrollIntoView({\n      behavior: 'smooth',\n      block: 'nearest',\n      inline: 'center'\n    });\n  }\n\n  return (\n    <>\n      <nav>\n        <button onClick={() => scrollToIndex(0)}>\n          Neo\n        </button>\n        <button onClick={() => scrollToIndex(1)}>\n          Millie\n        </button>\n        <button onClick={() => scrollToIndex(2)}>\n          Bella\n        </button>\n      </nav>\n      <div>\n        <ul ref={listRef}>\n          <li>\n            <img\n              src=\"https://placecats.com/neo/300/200\"\n              alt=\"Neo\"\n            />\n          </li>\n          <li>\n            <img\n              src=\"https://placecats.com/millie/200/200\"\n              alt=\"Millie\"\n            />\n          </li>\n          <li>\n            <img\n              src=\"https://placecats.com/bella/199/200\"\n              alt=\"Bella\"\n            />\n          </li>\n        </ul>\n      </div>\n    </>\n  );\n}\n```\n\n```css\ndiv {\n  width: 100%;\n  overflow: hidden;\n}\n\nnav {\n  text-align: center;\n}\n\nbutton {\n  margin: .25rem;\n}\n\nul,\nli {\n  list-style: none;\n  white-space: nowrap;\n}\n\nli {\n  display: inline;\n  padding: 0.5rem;\n}\n```\n\n</Sandpack>\n\n<Solution />\n\n#### 비디오 재생 및 정지하기 {/*playing-and-pausing-a-video*/}\n\n이 예시에서는 ref를 사용하여 `<video>` DOM 노드에서 [`play()`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/play) 및 [`pause()`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/pause)를 호출합니다.\n\n<Sandpack>\n\n```js\nimport { useState, useRef } from 'react';\n\nexport default function VideoPlayer() {\n  const [isPlaying, setIsPlaying] = useState(false);\n  const ref = useRef(null);\n\n  function handleClick() {\n    const nextIsPlaying = !isPlaying;\n    setIsPlaying(nextIsPlaying);\n\n    if (nextIsPlaying) {\n      ref.current.play();\n    } else {\n      ref.current.pause();\n    }\n  }\n\n  return (\n    <>\n      <button onClick={handleClick}>\n        {isPlaying ? 'Pause' : 'Play'}\n      </button>\n      <video\n        width=\"250\"\n        ref={ref}\n        onPlay={() => setIsPlaying(true)}\n        onPause={() => setIsPlaying(false)}\n      >\n        <source\n          src=\"https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4\"\n          type=\"video/mp4\"\n        />\n      </video>\n    </>\n  );\n}\n```\n\n```css\nbutton { display: block; margin-bottom: 20px; }\n```\n\n</Sandpack>\n\n<Solution />\n\n#### 컴포넌트에 ref 노출하기 {/*exposing-a-ref-to-your-own-component*/}\n\n때로는 부모 컴포넌트가 컴포넌트 내부의 DOM을 조작할 수 있도록 하고 싶을 때가 있습니다. 예를 들어 `MyInput` 컴포넌트를 작성하는 중인데, 부모 컴포넌트가 (부모가 접근할 수 없는) `MyInput`의 Input에 포커스를 맞출 수 있게 하고 싶을 수 있습니다. 이때 부모는 `ref`를 만들고, 이 `ref`를 자식 컴포넌트로 넘겨줌으로써 부모가 접근할 수 있도록 만들 수 있습니다. [자세한 내용은 여기에서 확인하세요.](/learn/manipulating-the-dom-with-refs#accessing-another-components-dom-nodes)\n\n<Sandpack>\n\n```js\nimport { useRef } from 'react';\n\nfunction MyInput({ ref }) {\n  return <input ref={ref} />;\n};\n\nexport default function Form() {\n  const inputRef = useRef(null);\n\n  function handleClick() {\n    inputRef.current.focus();\n  }\n\n  return (\n    <>\n      <MyInput ref={inputRef} />\n      <button onClick={handleClick}>\n        Focus the input\n      </button>\n    </>\n  );\n}\n```\n\n</Sandpack>\n\n<Solution />\n\n</Recipes>\n\n---\n\n### ref 콘텐츠 재생성 피하기 {/*avoiding-recreating-the-ref-contents*/}\n\nReact는 초기에 ref 값을 한 번 저장하고, 다음 렌더링부터는 이를 무시합니다.\n\n```js\nfunction Video() {\n  const playerRef = useRef(new VideoPlayer());\n  // ...\n```\n\n`new VideoPlayer()`의 결과는 초기 렌더링에만 사용되지만, 호출 자체는 이후의 모든 렌더링에서도 여전히 계속 이뤄집니다. 이는 값비싼 객체를 생성하는 경우 낭비일 수 있습니다.\n\n이 문제를 해결하려면 대신 다음과 같이 ref를 초기화할 수 있습니다:\n\n```js\nfunction Video() {\n  const playerRef = useRef(null);\n  if (playerRef.current === null) {\n    playerRef.current = new VideoPlayer();\n  }\n  // ...\n```\n\n일반적으로 렌더링 중에 `ref.current`를 쓰거나 읽는 것은 허용되지 않습니다. 하지만 이 경우에는 결과가 항상 동일하고 초기화 중에만 조건이 실행되므로 충분히 예측할 수 있으므로 괜찮습니다.\n\n<DeepDive>\n\n#### `useRef`를 초기화할 때 null 검사를 피하는 방법 {/*how-to-avoid-null-checks-when-initializing-use-ref-later*/}\n\n타입 검사기를 사용하면서 항상 `null`을 검사하고 싶지 않다면 다음과 같은 패턴을 대신 사용해 볼 수 있습니다:\n\n```js\nfunction Video() {\n  const playerRef = useRef(null);\n\n  function getPlayer() {\n    if (playerRef.current !== null) {\n      return playerRef.current;\n    }\n    const player = new VideoPlayer();\n    playerRef.current = player;\n    return player;\n  }\n\n  // ...\n```\n\n여기서 `playerRef` 자체는 nullable합니다. 하지만 타입 검사기에 `getPlayer()`가 `null`을 반환하는 경우가 없다는 것을 확신시킬 수 있어야 합니다. 그런 다음 이벤트 핸들러에서 `getPlayer()`를 사용하십시오.\n\n</DeepDive>\n\n---\n\n## 문제 해결 {/*troubleshooting*/}\n\n### 커스텀 컴포넌트에 대한 ref를 얻을 수 없습니다 {/*i-cant-get-a-ref-to-a-custom-component*/}\n\n컴포넌트에 `ref`를 전달하고자 다음과 같이 하면:\n\n```js\nconst inputRef = useRef(null);\n\nreturn <MyInput ref={inputRef} />;\n```\n\n다음과 같은 오류가 발생할 것입니다:\n\n<ConsoleBlock level=\"error\">\n\nTypeError: Cannot read properties of null\n\n</ConsoleBlock>\n\n기본적으로 컴포넌트는 내부의 DOM 노드에 대한 ref를 외부로 노출하지 않습니다.\n\n이 문제를 해결하려면 ref를 가져오고자 하는 컴포넌트를 찾으세요:\n\n```js\nexport default function MyInput({ value, onChange }) {\n  return (\n    <input\n      value={value}\n      onChange={onChange}\n    />\n  );\n}\n```\n\n그리고 `ref`를 컴포넌트가 받는 Props 목록에 추가한 뒤, 아래처럼 해당 자식 [내장 컴포넌트](/reference/react-dom/components/common)에 Prop으로 `ref`를 전달하세요.\n\n```js {1,6}\nfunction MyInput({ value, onChange, ref }) {\n  return (\n    <input\n      value={value}\n      onChange={onChange}\n      ref={ref}\n    />\n  );\n};\n\nexport default MyInput;\n```\n\n그러면 부모 컴포넌트가 ref를 가져올 수 있습니다.\n\n자세한 내용은 [다른 컴포넌트의 DOM 노드에 접근하기](/learn/manipulating-the-dom-with-refs#accessing-another-components-dom-nodes)에서 확인하세요.\n"
  },
  {
    "path": "src/content/reference/react/useState.md",
    "content": "---\ntitle: useState\n---\n\n<Intro>\n\n`useState`는 컴포넌트에 [state 변수](/learn/state-a-components-memory)를 추가할 수 있는 React Hook입니다.\n\n```js\nconst [state, setState] = useState(initialState)\n```\n\n</Intro>\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `useState(initialState)` {/*usestate*/}\n\n컴포넌트의 최상위 레벨에서 `useState`를 호출하여 [state 변수](/learn/state-a-components-memory)를 선언합니다.\n\n```js\nimport { useState } from 'react';\n\nfunction MyComponent() {\n  const [age, setAge] = useState(28);\n  const [name, setName] = useState('Taylor');\n  const [todos, setTodos] = useState(() => createTodos());\n  // ...\n```\n\n[배열 구조 분해](https://ko.javascript.info/destructuring-assignment)를 사용하여 `[something, setSomething]`과 같은 state 변수의 이름을 지정하는 것이 규칙입니다.\n\n[아래에서 더 많은 예시를 확인하세요.](#usage)\n\n#### 매개변수 {/*parameters*/}\n\n* `initialState`: state의 초기 설정값입니다. 어떤 유형의 값이든 지정할 수 있지만 함수에 대해서는 특별한 동작이 있습니다. 이 인수는 초기 렌더링 이후에는 무시됩니다.\n  * 함수를 `initialState`로 전달하면 이를 *초기화 함수*로 취급합니다. 이 함수는 순수해야 하고 인수를 받지 않아야 하며 반드시 어떤 값을 반환해야 합니다. React는 컴포넌트를 초기화할 때 초기화 함수를 호출하고, 그 반환값을 초기 state로 저장합니다. [아래 예시를 참고하세요.](#avoiding-recreating-the-initial-state)\n\n#### 반환값 {/*returns*/}\n\n`useState`는 정확히 두 개의 값을 가진 배열을 반환합니다.\n\n1. 현재 state입니다. 첫 번째 렌더링 중에는 전달한 `initialState`와 일치합니다.\n2. state를 다른 값으로 업데이트하고 리렌더링을 촉발할 수 있는 [`set` 함수](#setstate)입니다.\n\n#### 주의 사항 {/*caveats*/}\n\n* `useState`는 Hook이므로 **컴포넌트의 최상위 레벨**이나 직접 만든 Hook에서만 호출할 수 있습니다. 반복문이나 조건문 안에서는 호출할 수 없습니다. 필요한 경우 새 컴포넌트를 추출하고 state를 그 안으로 옮기세요.\n* Strict Mode에서 React는 [의도치 않은 불순물을 찾기 위해](#my-initializer-or-updater-function-runs-twice) **초기화 함수를 두 번 호출합니다.** 이는 개발 환경 전용 동작이며 프로덕션 환경에는 영향을 미치지 않습니다. 초기화 함수가 순수하다면(그래야 합니다) 동작에 영향을 미치지 않습니다. 호출 중 하나의 결과는 무시됩니다.\n\n---\n\n### `setSomething(nextState)`과 같은 `set` 함수 {/*setstate*/}\n\n`useState`가 반환하는 `set` 함수를 사용하면 state를 다른 값으로 업데이트하고 리렌더링을 촉발할 수 있습니다. 여기에는 다음 state를 직접 전달하거나, 이전 state로부터 계산한 함수를 전달할 수도 있습니다.\n\n```js\nconst [name, setName] = useState('Edward');\n\nfunction handleClick() {\n  setName('Taylor');\n  setAge(a => a + 1);\n  // ...\n```\n\n#### 매개변수 {/*setstate-parameters*/}\n\n* `nextState`: state가 될 값입니다. 값은 모든 데이터 타입이 허용되지만, 함수에 대해서는 특별한 동작이 있습니다.\n  * 함수를 `nextState`로 전달하면 *업데이터 함수*로 취급합니다. 이 함수는 순수해야 하고, 대기 중인 state를 유일한 인수로 사용해야 하며, 다음 state를 반환해야 합니다. React는 업데이터 함수를 대기열에 넣고 컴포넌트를 리렌더링 합니다. 다음 렌더링 중에 React는 대기열에 있는 모든 업데이터를 이전 state에 적용하여 다음 state를 계산합니다. [아래 예시를 참고하세요.](#updating-state-based-on-the-previous-state)\n\n#### 반환값 {/*setstate-returns*/}\n\n`set` 함수는 반환값이 없습니다.\n\n#### 주의 사항 {/*setstate-caveats*/}\n\n* `set` 함수는 ***다음* 렌더링에 대한 state 변수만 업데이트합니다.** `set` 함수를 호출한 후에도 state 변수에는 여전히 호출 전 화면에 있던 [이전 값이 담겨 있습니다.](#ive-updated-the-state-but-logging-gives-me-the-old-value)\n\n* 사용자가 제공한 새로운 값이 [`Object.is`](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Object/is)에 의해 현재 `state`와 동일하다고 판정되면, React는 **컴포넌트와 그 자식들을 리렌더링하지 않습니다.** 이것이 바로 최적화입니다. 경우에 따라 React가 자식을 건너뛰기 전에 컴포넌트를 호출해야 할 수도 있지만, 코드에 영향을 미치지는 않습니다.\n\n* React는 [state 업데이트를 batch 합니다. ](/learn/queueing-a-series-of-state-updates) **모든 이벤트 핸들러가 실행되고** `set` 함수를 호출한 후에 화면을 업데이트합니다. 이렇게 하면 단일 이벤트 중에 여러 번 리렌더링 하는 것을 방지할 수 있습니다. 드물지만 DOM에 접근하기 위해 React가 화면을 더 일찍 업데이트하도록 강제해야 하는 경우, [`flushSync`](/reference/react-dom/flushSync)를 사용할 수 있습니다.\n\n* `set` 함수는 항상 동일한 식별자를 가지기 때문에 Effect 의존성 목록에서 자주 생략됩니다. 하지만 의존성에 포함하더라도 Effect가 다시 실행되지는 않습니다. 린터가 오류 없이 생략을 허용한다면, 그대로 생략해도 안전합니다. [Effect 의존성 제거 방법에 대해 더 알아보세요](/learn/removing-effect-dependencies#move-dynamic-objects-and-functions-inside-your-effect).\n\n* *렌더링 도중* `set` 함수를 호출하는 것은 현재 렌더링 중인 컴포넌트 내에서만 허용됩니다. React는 해당 출력을 버리고 즉시 새로운 state로 다시 렌더링을 시도합니다. 이 패턴은 거의 필요하지 않지만 **이전 렌더링의 정보를 저장하는 데 사용할 수 있습니다**. [아래 예시를 참고하세요.](#storing-information-from-previous-renders)\n\n* Strict Mode에서 React는 [의도치않은 불순물을 찾기 위해](#my-initializer-or-updater-function-runs-twice) **업데이터 함수를 두 번 호출합니다**. 이는 개발 환경 전용 동작이며 프로덕션 환경에는 영향을 미치지 않습니다. 만약 업데이터 함수가 순수하다면(그래야 합니다) 동작에 영향을 미치지 않습니다. 호출 중 하나의 결과는 무시됩니다.\n\n---\n\n## 사용법 {/*usage*/}\n\n### 컴포넌트에 state 추가하기 {/*adding-state-to-a-component*/}\n\n컴포넌트의 최상위 레벨에서 `useState`를 호출하여 하나 이상의 [state 변수](/learn/state-a-components-memory)를 선언하세요.\n\n```js [[1, 4, \"age\"], [2, 4, \"setAge\"], [3, 4, \"42\"], [1, 5, \"name\"], [2, 5, \"setName\"], [3, 5, \"'Taylor'\"]]\nimport { useState } from 'react';\n\nfunction MyComponent() {\n  const [age, setAge] = useState(42);\n  const [name, setName] = useState('Taylor');\n  // ...\n```\n\n[배열 구조 분해](https://ko.javascript.info/destructuring-assignment)를 사용하여 `[something, setSomething]`과 같은 state 변수의 이름을 지정하는 것이 관례입니다.\n\n`useState`는 정확히 두 개의 항목이 있는 배열을 반환합니다.\n\n1. 이 state 변수의 <CodeStep step={1}>현재 state</CodeStep>로, 처음에 제공한 <CodeStep step={3}>초기 state</CodeStep>로 설정됩니다.\n2. 상호작용에 반응하여 다른 값으로 변경할 수 있는 <CodeStep step={2}>`set` 함수</CodeStep>입니다.\n\n화면의 내용을 업데이트하려면 다음 state로 `set` 함수를 호출합니다.\n\n```js [[2, 2, \"setName\"]]\nfunction handleClick() {\n  setName('Robin');\n}\n```\n\nReact는 다음 state를 저장하고 새로운 값으로 컴포넌트를 다시 렌더링한 후 UI를 업데이트합니다.\n\n<Pitfall>\n\n`set` 함수를 호출해도 [이미 실행 중인 코드의 현재 state는 변경되지 **않습니다**](#ive-updated-the-state-but-logging-gives-me-the-old-value).\n\n```js {3}\nfunction handleClick() {\n  setName('Robin');\n  console.log(name); // 아직 \"Taylor\"입니다!\n}\n```\n\n`set`함수는 **다음** 렌더링에서 반환할 `useState`에만 영향을 줍니다.\n\n</Pitfall>\n\n<Recipes titleText=\"useState 기본 예시\" titleId=\"examples-basic\">\n\n#### 카운터 (숫자) {/*counter-number*/}\n\n예시에서 `count` state 변수는 숫자를 받습니다. 버튼을 클릭하면 숫자가 증가합니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function Counter() {\n  const [count, setCount] = useState(0);\n\n  function handleClick() {\n    setCount(count + 1);\n  }\n\n  return (\n    <button onClick={handleClick}>\n      You pressed me {count} times\n    </button>\n  );\n}\n```\n\n</Sandpack>\n\n<Solution />\n\n#### 텍스트 필드 (문자열) {/*text-field-string*/}\n\n예시에서 `text` state 변수는 문자열을 받습니다. input에 타이핑하면 `handleChange`는 input DOM 요소에서 최신 input 값을 읽고 `setText`를 호출하여 state를 업데이트합니다. 이렇게 하면 아래에 현재 `text`를 표시할 수 있습니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function MyInput() {\n  const [text, setText] = useState('hello');\n\n  function handleChange(e) {\n    setText(e.target.value);\n  }\n\n  return (\n    <>\n      <input value={text} onChange={handleChange} />\n      <p>You typed: {text}</p>\n      <button onClick={() => setText('hello')}>\n        Reset\n      </button>\n    </>\n  );\n}\n```\n\n</Sandpack>\n\n<Solution />\n\n#### 체크박스 (불리언) {/*checkbox-boolean*/}\n\n예시에서 `liked` state 변수는 불리언을 받습니다. input을 클릭하면 `setLiked`는 체크박스가 선택되어 있는지 여부에 따라 `liked` state 변수를 업데이트합니다. `liked` 변수는 체크박스 아래의 텍스트를 렌더링하는 데 사용됩니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function MyCheckbox() {\n  const [liked, setLiked] = useState(true);\n\n  function handleChange(e) {\n    setLiked(e.target.checked);\n  }\n\n  return (\n    <>\n      <label>\n        <input\n          type=\"checkbox\"\n          checked={liked}\n          onChange={handleChange}\n        />\n        I liked this\n      </label>\n      <p>You {liked ? 'liked' : 'did not like'} this.</p>\n    </>\n  );\n}\n```\n\n</Sandpack>\n\n<Solution />\n\n#### 폼 (두 개의 변수) {/*form-two-variables*/}\n\n동일한 컴포넌트에 두 개 이상의 state 변수를 선언할 수 있습니다. 각 state 변수는 완전히 독립적입니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function Form() {\n  const [name, setName] = useState('Taylor');\n  const [age, setAge] = useState(42);\n\n  return (\n    <>\n      <input\n        value={name}\n        onChange={e => setName(e.target.value)}\n      />\n      <button onClick={() => setAge(age + 1)}>\n        Increment age\n      </button>\n      <p>Hello, {name}. You are {age}.</p>\n    </>\n  );\n}\n```\n\n```css\nbutton { display: block; margin-top: 10px; }\n```\n\n</Sandpack>\n\n<Solution />\n\n</Recipes>\n\n---\n\n### 이전 state를 기반으로 state 업데이트하기 {/*updating-state-based-on-the-previous-state*/}\n\n`age`가 `42`라고 가정합니다. 이 핸들러는 `setAge(age + 1)`를 세 번 호출합니다.\n\n```js\nfunction handleClick() {\n  setAge(age + 1); // setAge(42 + 1)\n  setAge(age + 1); // setAge(42 + 1)\n  setAge(age + 1); // setAge(42 + 1)\n}\n```\n\n하지만 클릭해보면 `age`는 `45`가 아니라 `43`이 됩니다! 이는 `set` 함수를 호출해도 이미 실행 중인 코드에서 `age` state 변수가 [업데이트되지 않기](/learn/state-as-a-snapshot) 때문입니다. 따라서 각 `setAge(age + 1)` 호출은 `setAge(43)`이 됩니다.\n\n이 문제를 해결하려면 다음 state 대신 `setAge`에 ***업데이터 함수*를 전달할 수 있습니다**.\n\n```js [[1, 2, \"a\", 0], [2, 2, \"a + 1\"], [1, 3, \"a\", 0], [2, 3, \"a + 1\"], [1, 4, \"a\", 0], [2, 4, \"a + 1\"]]\nfunction handleClick() {\n  setAge(a => a + 1); // setAge(42 => 43)\n  setAge(a => a + 1); // setAge(43 => 44)\n  setAge(a => a + 1); // setAge(44 => 45)\n}\n```\n\n여기서 `a => a + 1`은 업데이터 함수입니다. 이 함수는 <CodeStep step={1}>대기 중인 state</CodeStep>를 가져와서 <CodeStep step={2}>다음 state</CodeStep>를 계산합니다.\n\nReact는 업데이터 함수를 [큐](/learn/queueing-a-series-of-state-updates)에 넣습니다. 그러면 다음 렌더링 중에 동일한 순서로 호출합니다.\n\n1. `a => a + 1`은 대기 중인 state로 `42`를 받고 다음 state로 `43`을 반환합니다.\n2. `a => a + 1`은 대기 중인 state로 `43`을 받고 다음 state로 `44`를 반환합니다.\n3. `a => a + 1`은 대기 중인 state로 `44`를 받고 다음 state로 `45`를 반환합니다.\n\n대기 중인 다른 업데이트가 없으므로, React는 결국 `45`를 현재 state로 저장합니다.\n\n규칙상 대기 중인 state 인수의 이름을 `age`의 `a`와 같이 state 변수 이름의 첫 글자로 지정하는 것이 일반적입니다. 그러나 `prevAge` 또는 더 명확하다고 생각하는 다른 이름으로 지정해도 됩니다.\n\nReact는 개발 환경에서 [순수](/learn/keeping-components-pure)한지 확인하기 위해 [업데이터를 두 번 호출](#my-initializer-or-updater-function-runs-twice)할 수 있습니다.\n\n<DeepDive>\n\n#### 항상 업데이터를 사용하는 것이 더 좋은가요? {/*is-using-an-updater-always-preferred*/}\n\n설정하려는 state가 이전 state에서 계산되는 경우 항상 `setAge(a => a + 1)`처럼 업데이터를 사용하는걸 추천한다는 말을 들어보았을 것입니다. 나쁠 건 없지만 항상 그래야만 하는 것은 아닙니다.\n\n대부분의 경우, 이 두 가지 접근 방식 사이에는 차이가 없습니다. React는 클릭과 같은 의도적인 사용자 액션에 대해 항상 다음 클릭 전에 `age` state 변수가 업데이트 되도록 합니다. 즉, 클릭 핸들러가 이벤트 핸들러를 시작할 때 \"오래된\" `age`를 볼 위험은 없습니다.\n\n다만 동일한 이벤트 내에서 여러 업데이트를 수행하는 경우에는 업데이터가 도움이 될 수 있습니다. state 변수 자체에 접근하는 것이 어려운 경우에도 유용합니다. (리렌더링을 최적화할 때 이 문제가 발생할 수 있습니다).\n\n친절한 문법보다 일관성을 더 선호한다면 설정하려는 state가 이전 state에서 계산되는 경우 항상 업데이터를 작성하는 것이 합리적일 것입니다. 만약 어떤 state가 *다른* state 변수의 이전 state로부터 계산되는 경우라면, 이를 하나의 객체로 결합하고 [reducer를 사용](/learn/extracting-state-logic-into-a-reducer)하는 것이 좋습니다.\n\n</DeepDive>\n\n<Recipes titleText=\"업데이터를 전달하는 것과 다음 state를 직접 전달하는 것의 차이점\" titleId=\"examples-updater\">\n\n#### 업데이터 함수 전달하기 {/*passing-the-updater-function*/}\n\n이 예시는 업데이터 함수를 전달하므로 \"+3\" 버튼이 작동합니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function Counter() {\n  const [age, setAge] = useState(42);\n\n  function increment() {\n    setAge(a => a + 1);\n  }\n\n  return (\n    <>\n      <h1>Your age: {age}</h1>\n      <button onClick={() => {\n        increment();\n        increment();\n        increment();\n      }}>+3</button>\n      <button onClick={() => {\n        increment();\n      }}>+1</button>\n    </>\n  );\n}\n```\n\n```css\nbutton { display: block; margin: 10px; font-size: 20px; }\nh1 { display: block; margin: 10px; }\n```\n\n</Sandpack>\n\n<Solution />\n\n#### 다음 state 바로 전달하기 {/*passing-the-next-state-directly*/}\n\n이 예시는 업데이터 함수를 전달하지 **않으므로** \"+3\" 버튼이 **의도한 대로 작동하지 않습니다**.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function Counter() {\n  const [age, setAge] = useState(42);\n\n  function increment() {\n    setAge(age + 1);\n  }\n\n  return (\n    <>\n      <h1>Your age: {age}</h1>\n      <button onClick={() => {\n        increment();\n        increment();\n        increment();\n      }}>+3</button>\n      <button onClick={() => {\n        increment();\n      }}>+1</button>\n    </>\n  );\n}\n```\n\n```css\nbutton { display: block; margin: 10px; font-size: 20px; }\nh1 { display: block; margin: 10px; }\n```\n\n</Sandpack>\n\n<Solution />\n\n</Recipes>\n\n---\n\n### 객체 및 배열 state 업데이트하기 {/*updating-objects-and-arrays-in-state*/}\n\nstate에는 객체와 배열도 넣을 수 있습니다. React에서 state는 읽기 전용으로 간주되므로 **기존 객체를 *변경*하지 않고, *교체* 해야 합니다**. 예를 들어, state에 `form` 객체가 있는 경우 변경하지 마세요.\n\n```js\n// 🚩 state 안에 있는 객체를 다음과 같이 변경하지 마세요.\nform.firstName = 'Taylor';\n```\n\n대신 새로운 객체를 생성하여 전체 객체를 교체하세요.\n\n```js\n// ✅ 새로운 객체로 state를 교체합니다.\nsetForm({\n  ...form,\n  firstName: 'Taylor'\n});\n```\n\n자세한 내용은 [객체 state 업데이트하기](/learn/updating-objects-in-state) 및 [배열 state 업데이트하기](/learn/updating-arrays-in-state)에서 확인하세요.\n\n<Recipes titleText=\"객체 및 배열 state 예시\" titleId=\"examples-objects\">\n\n#### 폼 (객체) {/*form-object*/}\n\n이 예시에서 `form` state 변수는 객체를 받습니다. 각 input에는 전체 `form`의 다음 state로 `setForm`을 호출하는 change 핸들러가 있습니다. 전개 구문인 `{ ...form }`은 state 객체를 변경하지 않고 교체합니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function Form() {\n  const [form, setForm] = useState({\n    firstName: 'Barbara',\n    lastName: 'Hepworth',\n    email: 'bhepworth@sculpture.com',\n  });\n\n  return (\n    <>\n      <label>\n        First name:\n        <input\n          value={form.firstName}\n          onChange={e => {\n            setForm({\n              ...form,\n              firstName: e.target.value\n            });\n          }}\n        />\n      </label>\n      <label>\n        Last name:\n        <input\n          value={form.lastName}\n          onChange={e => {\n            setForm({\n              ...form,\n              lastName: e.target.value\n            });\n          }}\n        />\n      </label>\n      <label>\n        Email:\n        <input\n          value={form.email}\n          onChange={e => {\n            setForm({\n              ...form,\n              email: e.target.value\n            });\n          }}\n        />\n      </label>\n      <p>\n        {form.firstName}{' '}\n        {form.lastName}{' '}\n        ({form.email})\n      </p>\n    </>\n  );\n}\n```\n\n```css\nlabel { display: block; }\ninput { margin-left: 5px; }\n```\n\n</Sandpack>\n\n<Solution />\n\n#### 폼 (중첩 객체) {/*form-nested-object*/}\n\n이 예시에서는 state가 더 중첩되어 있습니다. 중첩된 state를 업데이트할 때는 업데이트하려는 객체의 복사본을 만들어야 하며, 위쪽으로 올라갈 때마다 해당 객체를 \"포함하는\" 모든 객체에 대한 복사본을 만들어야 합니다. 자세히 알아보려면 [중첩된 객체 업데이트하기](/learn/updating-objects-in-state#updating-a-nested-object)를 읽어보세요.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function Form() {\n  const [person, setPerson] = useState({\n    name: 'Niki de Saint Phalle',\n    artwork: {\n      title: 'Blue Nana',\n      city: 'Hamburg',\n      image: 'https://i.imgur.com/Sd1AgUOm.jpg',\n    }\n  });\n\n  function handleNameChange(e) {\n    setPerson({\n      ...person,\n      name: e.target.value\n    });\n  }\n\n  function handleTitleChange(e) {\n    setPerson({\n      ...person,\n      artwork: {\n        ...person.artwork,\n        title: e.target.value\n      }\n    });\n  }\n\n  function handleCityChange(e) {\n    setPerson({\n      ...person,\n      artwork: {\n        ...person.artwork,\n        city: e.target.value\n      }\n    });\n  }\n\n  function handleImageChange(e) {\n    setPerson({\n      ...person,\n      artwork: {\n        ...person.artwork,\n        image: e.target.value\n      }\n    });\n  }\n\n  return (\n    <>\n      <label>\n        Name:\n        <input\n          value={person.name}\n          onChange={handleNameChange}\n        />\n      </label>\n      <label>\n        Title:\n        <input\n          value={person.artwork.title}\n          onChange={handleTitleChange}\n        />\n      </label>\n      <label>\n        City:\n        <input\n          value={person.artwork.city}\n          onChange={handleCityChange}\n        />\n      </label>\n      <label>\n        Image:\n        <input\n          value={person.artwork.image}\n          onChange={handleImageChange}\n        />\n      </label>\n      <p>\n        <i>{person.artwork.title}</i>\n        {' by '}\n        {person.name}\n        <br />\n        (located in {person.artwork.city})\n      </p>\n      <img\n        src={person.artwork.image}\n        alt={person.artwork.title}\n      />\n    </>\n  );\n}\n```\n\n```css\nlabel { display: block; }\ninput { margin-left: 5px; margin-bottom: 5px; }\nimg { width: 200px; height: 200px; }\n```\n\n</Sandpack>\n\n<Solution />\n\n#### 리스트 (배열) {/*list-array*/}\n\n이 예시에서 `todos` state 변수는 배열을 받습니다. 각 버튼 핸들러는 해당 배열의 다음 버전으로 `setTodos`를 호출합니다. `[...todos]` 전개 구문, `todos.map()` 및 `todos.filter()`는 state 배열이 변경하지 않고 교체합니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { useState } from 'react';\nimport AddTodo from './AddTodo.js';\nimport TaskList from './TaskList.js';\n\nlet nextId = 3;\nconst initialTodos = [\n  { id: 0, title: 'Buy milk', done: true },\n  { id: 1, title: 'Eat tacos', done: false },\n  { id: 2, title: 'Brew tea', done: false },\n];\n\nexport default function TaskApp() {\n  const [todos, setTodos] = useState(initialTodos);\n\n  function handleAddTodo(title) {\n    setTodos([\n      ...todos,\n      {\n        id: nextId++,\n        title: title,\n        done: false\n      }\n    ]);\n  }\n\n  function handleChangeTodo(nextTodo) {\n    setTodos(todos.map(t => {\n      if (t.id === nextTodo.id) {\n        return nextTodo;\n      } else {\n        return t;\n      }\n    }));\n  }\n\n  function handleDeleteTodo(todoId) {\n    setTodos(\n      todos.filter(t => t.id !== todoId)\n    );\n  }\n\n  return (\n    <>\n      <AddTodo\n        onAddTodo={handleAddTodo}\n      />\n      <TaskList\n        todos={todos}\n        onChangeTodo={handleChangeTodo}\n        onDeleteTodo={handleDeleteTodo}\n      />\n    </>\n  );\n}\n```\n\n```js src/AddTodo.js\nimport { useState } from 'react';\n\nexport default function AddTodo({ onAddTodo }) {\n  const [title, setTitle] = useState('');\n  return (\n    <>\n      <input\n        placeholder=\"Add todo\"\n        value={title}\n        onChange={e => setTitle(e.target.value)}\n      />\n      <button onClick={() => {\n        setTitle('');\n        onAddTodo(title);\n      }}>Add</button>\n    </>\n  )\n}\n```\n\n```js src/TaskList.js\nimport { useState } from 'react';\n\nexport default function TaskList({\n  todos,\n  onChangeTodo,\n  onDeleteTodo\n}) {\n  return (\n    <ul>\n      {todos.map(todo => (\n        <li key={todo.id}>\n          <Task\n            todo={todo}\n            onChange={onChangeTodo}\n            onDelete={onDeleteTodo}\n          />\n        </li>\n      ))}\n    </ul>\n  );\n}\n\nfunction Task({ todo, onChange, onDelete }) {\n  const [isEditing, setIsEditing] = useState(false);\n  let todoContent;\n  if (isEditing) {\n    todoContent = (\n      <>\n        <input\n          value={todo.title}\n          onChange={e => {\n            onChange({\n              ...todo,\n              title: e.target.value\n            });\n          }} />\n        <button onClick={() => setIsEditing(false)}>\n          Save\n        </button>\n      </>\n    );\n  } else {\n    todoContent = (\n      <>\n        {todo.title}\n        <button onClick={() => setIsEditing(true)}>\n          Edit\n        </button>\n      </>\n    );\n  }\n  return (\n    <label>\n      <input\n        type=\"checkbox\"\n        checked={todo.done}\n        onChange={e => {\n          onChange({\n            ...todo,\n            done: e.target.checked\n          });\n        }}\n      />\n      {todoContent}\n      <button onClick={() => onDelete(todo.id)}>\n        Delete\n      </button>\n    </label>\n  );\n}\n```\n\n```css\nbutton { margin: 5px; }\nli { list-style-type: none; }\nul, li { margin: 0; padding: 0; }\n```\n\n</Sandpack>\n\n<Solution />\n\n#### Immer로 간결한 업데이트 로직 작성 {/*writing-concise-update-logic-with-immer*/}\n\n변경 없이 배열과 객체를 업데이트하는 것이 귀찮게 느껴진다면 [Immer](https://github.com/immerjs/use-immer)와 같은 라이브러리를 사용하여 반복적인 코드를 줄일 수 있습니다. Immer를 사용하면 객체를 변경하는 것처럼 코드를 간결하게 작성하더라도 내부적으로는 불변성을 유지한 업데이트를 수행합니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\nimport { useImmer } from 'use-immer';\n\nlet nextId = 3;\nconst initialList = [\n  { id: 0, title: 'Big Bellies', seen: false },\n  { id: 1, title: 'Lunar Landscape', seen: false },\n  { id: 2, title: 'Terracotta Army', seen: true },\n];\n\nexport default function BucketList() {\n  const [list, updateList] = useImmer(initialList);\n\n  function handleToggle(artworkId, nextSeen) {\n    updateList(draft => {\n      const artwork = draft.find(a =>\n        a.id === artworkId\n      );\n      artwork.seen = nextSeen;\n    });\n  }\n\n  return (\n    <>\n      <h1>Art Bucket List</h1>\n      <h2>My list of art to see:</h2>\n      <ItemList\n        artworks={list}\n        onToggle={handleToggle} />\n    </>\n  );\n}\n\nfunction ItemList({ artworks, onToggle }) {\n  return (\n    <ul>\n      {artworks.map(artwork => (\n        <li key={artwork.id}>\n          <label>\n            <input\n              type=\"checkbox\"\n              checked={artwork.seen}\n              onChange={e => {\n                onToggle(\n                  artwork.id,\n                  e.target.checked\n                );\n              }}\n            />\n            {artwork.title}\n          </label>\n        </li>\n      ))}\n    </ul>\n  );\n}\n```\n\n```json package.json\n{\n  \"dependencies\": {\n    \"immer\": \"1.7.3\",\n    \"react\": \"latest\",\n    \"react-dom\": \"latest\",\n    \"react-scripts\": \"latest\",\n    \"use-immer\": \"0.5.1\"\n  },\n  \"scripts\": {\n    \"start\": \"react-scripts start\",\n    \"build\": \"react-scripts build\",\n    \"test\": \"react-scripts test --env=jsdom\",\n    \"eject\": \"react-scripts eject\"\n  }\n}\n```\n\n</Sandpack>\n\n<Solution />\n\n</Recipes>\n\n---\n\n### 초기 state 다시 생성하지 않기 {/*avoiding-recreating-the-initial-state*/}\n\nReact는 초기 state를 한 번 저장하고 다음 렌더링부터는 이를 무시합니다.\n\n```js\nfunction TodoList() {\n  const [todos, setTodos] = useState(createInitialTodos());\n  // ...\n```\n\n`createInitialTodos()`의 결과는 초기 렌더링에만 사용되지만, 여전히 모든 렌더링에서 이 함수를 호출합니다. 이는 큰 배열을 생성하거나 값비싼 계산을 수행하는 경우 낭비일 수 있습니다.\n\n이 문제를 해결하려면, `useState`에 **초기화 함수로 전달하세요**.\n\n```js\nfunction TodoList() {\n  const [todos, setTodos] = useState(createInitialTodos);\n  // ...\n```\n\n함수를 호출한 결과인 `createInitialTodos()`가 아니라 함수 자체인 `createInitialTodos`를 전달하고 있다는 것에 주목하세요. 함수를 `useState`에 전달하면 React는 초기화 중에만 함수를 호출합니다.\n\n개발 환경에서는 React가 초기화 함수가 [순수](#my-initializer-or-updater-function-runs-twice)한지 확인하기 위해 [초기화 함수를 두 번 호출](/learn/keeping-components-pure)할 수 있습니다.\n\n<Recipes titleText=\"초기화 함수를 전달하는 것과 초기 state를 직접 전달하는 것의 차이점\" titleId=\"examples-initializer\">\n\n#### 초기화 함수 전달하기 {/*passing-the-initializer-function*/}\n\n이 예시에서는 초기화 함수를 전달하므로, `createInitialTodos` 함수는 초기화 중에만 실행됩니다. input에 타이핑할 때 같이 컴포넌트가 리렌더링할 때에는 실행되지 않습니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nfunction createInitialTodos() {\n  const initialTodos = [];\n  for (let i = 0; i < 50; i++) {\n    initialTodos.push({\n      id: i,\n      text: 'Item ' + (i + 1)\n    });\n  }\n  return initialTodos;\n}\n\nexport default function TodoList() {\n  const [todos, setTodos] = useState(createInitialTodos);\n  const [text, setText] = useState('');\n\n  return (\n    <>\n      <input\n        value={text}\n        onChange={e => setText(e.target.value)}\n      />\n      <button onClick={() => {\n        setText('');\n        setTodos([{\n          id: todos.length,\n          text: text\n        }, ...todos]);\n      }}>Add</button>\n      <ul>\n        {todos.map(item => (\n          <li key={item.id}>\n            {item.text}\n          </li>\n        ))}\n      </ul>\n    </>\n  );\n}\n```\n\n</Sandpack>\n\n<Solution />\n\n#### 초기 state 직접 전달하기 {/*passing-the-initial-state-directly*/}\n\n이 예시에서는 초기화 함수를 전달하지 **않으므로,** input을 타이핑할 때 같이 모든 렌더링에서 `createInitialTodos` 함수가 실행됩니다. 동작에 눈에 띄는 차이는 없지만 이 코드는 효율성이 떨어집니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nfunction createInitialTodos() {\n  const initialTodos = [];\n  for (let i = 0; i < 50; i++) {\n    initialTodos.push({\n      id: i,\n      text: 'Item ' + (i + 1)\n    });\n  }\n  return initialTodos;\n}\n\nexport default function TodoList() {\n  const [todos, setTodos] = useState(createInitialTodos());\n  const [text, setText] = useState('');\n\n  return (\n    <>\n      <input\n        value={text}\n        onChange={e => setText(e.target.value)}\n      />\n      <button onClick={() => {\n        setText('');\n        setTodos([{\n          id: todos.length,\n          text: text\n        }, ...todos]);\n      }}>Add</button>\n      <ul>\n        {todos.map(item => (\n          <li key={item.id}>\n            {item.text}\n          </li>\n        ))}\n      </ul>\n    </>\n  );\n}\n```\n\n</Sandpack>\n\n<Solution />\n\n</Recipes>\n\n---\n\n### key로 state 초기화하기 {/*resetting-state-with-a-key*/}\n\n[목록을 렌더링](/learn/rendering-lists)할 때 `key` 속성을 자주 접하게 됩니다. 하지만 `key` 속성은 다른 용도로도 사용됩니다.\n\n**컴포넌트에 다른 `key`를 전달하여 컴포넌트의 state를 초기화**할 수 있습니다. 이 예시에서는 Reset 버튼이 `version` state 변수를 변경하고, 이를 `Form`에 `key`로 전달합니다. `key`가 변경되면 React는 `Form` 컴포넌트(및 그 모든 자식)를 처음부터 다시 생성하므로 state가 초기화됩니다.\n\n자세히 알아보려면 [State를 보존하고 초기화하기](/learn/preserving-and-resetting-state)를 읽어보세요.\n\n<Sandpack>\n\n```js src/App.js\nimport { useState } from 'react';\n\nexport default function App() {\n  const [version, setVersion] = useState(0);\n\n  function handleReset() {\n    setVersion(version + 1);\n  }\n\n  return (\n    <>\n      <button onClick={handleReset}>Reset</button>\n      <Form key={version} />\n    </>\n  );\n}\n\nfunction Form() {\n  const [name, setName] = useState('Taylor');\n\n  return (\n    <>\n      <input\n        value={name}\n        onChange={e => setName(e.target.value)}\n      />\n      <p>Hello, {name}.</p>\n    </>\n  );\n}\n```\n\n```css\nbutton { display: block; margin-bottom: 20px; }\n```\n\n</Sandpack>\n\n---\n\n### 이전 렌더링에서 얻은 정보 저장하기 {/*storing-information-from-previous-renders*/}\n\n보통은 이벤트 핸들러에서 state를 업데이트합니다. 하지만 드물게 렌더링에 대한 응답으로 state를 조정해야 하는 경우도 있습니다. 예를 들어, props가 변경될 때 state 변수를 변경하고 싶을 수 있습니다.\n\n대부분의 경우 이 기능은 필요하지 않습니다.\n\n* **필요한 값을 현재 props나 다른 state에서 모두 계산할 수 있는 경우, [중복되는 state를 모두 제거하세요](#resetting-state-with-a-key)**. 너무 자주 재계산하는 것이 걱정된다면, [`useMemo` Hook](#resetting-state-with-a-key)을 사용하면 도움이 될 수 있습니다.\n* 전체 컴포넌트 트리의 state를 초기화하려면 [컴포넌트에 다른 `key`를 전달하세요](#resetting-state-with-a-key).\n* 가능하다면 이벤트 핸들러의 모든 관련 state를 업데이트하세요.\n\n이 중 어느 것에도 해당하지 않는 희귀한 경우에는, 컴포넌트가 렌더링되는 동안 `set` 함수를 호출하여 지금까지 렌더링된 값을 바탕으로 state를 업데이트하는 데 사용할 수 있는 패턴이 있습니다.\n\n다음은 그 예시입니다. `CountLabel` 컴포넌트는 전달된 `count` props를 표시합니다.\n\n```js src/CountLabel.js\nexport default function CountLabel({ count }) {\n  return <h1>{count}</h1>\n}\n```\n\n카운터가 마지막 변경 이후 *증가 또는 감소했는지*를 표시하고 싶다고 가정해 보겠습니다. `count` prop는 이를 알려주지 않으므로 이전 값을 추적해야 합니다. 이를 추적하기 위해 `prevCount` state 변수를 추가합니다. `trend`라는 또 다른 state 변수를 추가하여 count의 증가 또는 감소 여부를 추적합니다. `prevCount`와 `count`를 비교해서, 같지 않은 경우 `prevCount`와 `trend`를 모두 업데이트합니다. 이제 현재 count props와 마지막 렌더링 이후 count가 *어떻게* 변경되었는지 모두 표시할 수 있습니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { useState } from 'react';\nimport CountLabel from './CountLabel.js';\n\nexport default function App() {\n  const [count, setCount] = useState(0);\n  return (\n    <>\n      <button onClick={() => setCount(count + 1)}>\n        Increment\n      </button>\n      <button onClick={() => setCount(count - 1)}>\n        Decrement\n      </button>\n      <CountLabel count={count} />\n    </>\n  );\n}\n```\n\n```js src/CountLabel.js active\nimport { useState } from 'react';\n\nexport default function CountLabel({ count }) {\n  const [prevCount, setPrevCount] = useState(count);\n  const [trend, setTrend] = useState(null);\n  if (prevCount !== count) {\n    setPrevCount(count);\n    setTrend(count > prevCount ? 'increasing' : 'decreasing');\n  }\n  return (\n    <>\n      <h1>{count}</h1>\n      {trend && <p>The count is {trend}</p>}\n    </>\n  );\n}\n```\n\n```css\nbutton { margin-bottom: 10px; }\n```\n\n</Sandpack>\n\n렌더링하는 동안 `set` 함수를 호출하는 경우, 그 `set` 함수는 `prevCount !== count`와 같은 조건 안에 있어야 하며, 조건 내부에 `setPrevCount(count)`와 같은 호출이 있어야 한다는 점에 유의하세요. 그렇지 않으면 리렌더링을 반복하다가 결국 깨질 것입니다. 또한 이 방식은 오직 *현재* 렌더링 중인 컴포넌트의 state만을 업데이트할 수 있습니다. 렌더링 중에 다른 컴포넌트의 `set` 함수를 호출하는 것은 에러입니다. 마지막으로, 이 경우에도 `set` 함수 호출은 여전히 [변경이 아닌 state 업데이트](#updating-objects-and-arrays-in-state)여야만 합니다. [순수 함수](/learn/keeping-components-pure)의 다른 규칙을 어겨도 된다는 의미가 아닙니다.\n\n이 패턴은 이해하기 어려울 수 있으며 일반적으로 피하는 것이 가장 좋습니다. 하지만 Effect에서 state를 업데이트하는 것보다는 낫습니다. 렌더링 도중 `set` 함수를 호출하면 React는 컴포넌트가 `return`문으로 종료된 직후, 자식을 렌더링하기 전에 해당 컴포넌트를 리렌더링 합니다. 이렇게 하면 자식 컴포넌트를 두 번 렌더링할 필요가 없습니다. 나머지 컴포넌트 함수는 계속 실행되고 결과는 버려집니다. 조건이 모든 Hook 호출보다 아래에 있으면 이른(early) `return;`을 통해 렌더링을 더 일찍 다시 시작할 수 있습니다.\n\n---\n\n## 문제 해결 {/*troubleshooting*/}\n\n### state를 업데이트했지만 로그에는 계속 이전 값이 표시됩니다 {/*ive-updated-the-state-but-logging-gives-me-the-old-value*/}\n\n`set` 함수를 호출해도 **실행 중인 코드의 state는 변경되지 않습니다**.\n\n```js {4,5,8}\nfunction handleClick() {\n  console.log(count);  // 0\n\n  setCount(count + 1); // 1로 리렌더링 요청합니다.\n  console.log(count);  // 아직 0입니다!\n\n  setTimeout(() => {\n    console.log(count); // 여기도 0이고요!\n  }, 5000);\n}\n```\n\n그 이유는 [state가 스냅샷처럼 동작](/learn/state-as-a-snapshot)하기 때문입니다. state를 업데이트하면 새로운 state 값으로 다른 렌더링을 요청하지만 이미 실행 중인 이벤트 핸들러의 `count` 변수에는 영향을 미치지 않습니다.\n\n다음 state를 사용해야 하는 경우에는, `set` 함수에 전달하기 전에 변수에 저장할 수 있습니다:\n\n```js\nconst nextCount = count + 1;\nsetCount(nextCount);\n\nconsole.log(count);     // 0\nconsole.log(nextCount); // 1\n```\n\n---\n\n### state를 업데이트해도 화면이 바뀌지 않습니다 {/*ive-updated-the-state-but-the-screen-doesnt-update*/}\n\nReact는 [Object.is](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Object/is)로 비교한 뒤 **다음 state가 이전 state와 같으면 업데이트를 무시**합니다. 이는 보통 객체나 배열의 state를 직접 변경할 때 발생합니다.\n\n```js\nobj.x = 10;  // 🚩 잘못된 방법: 기존 객체를 변경\nsetObj(obj); // 🚩 아무것도 하지 않습니다.\n```\n\n기존 `obj` 객체를 변경한 후 다시 `setObj`로 전달했기 때문에 React가 업데이트를 무시했습니다. 이 문제를 해결하려면 [객체나 배열 state를 변경하는 대신 항상 교체](#updating-objects-and-arrays-in-state)해야 합니다.\n\n```js\n// ✅ 올바른 방법: 새로운 객체 생성\nsetObj({\n  ...obj,\n  x: 10\n});\n```\n\n---\n\n### 에러가 발생했습니다: \"리렌더링 횟수가 너무 많습니다\" {/*im-getting-an-error-too-many-re-renders*/}\n\n다음과 같은 에러가 발생할 수 있습니다. `리렌더링 횟수가 너무 많습니다. React는 무한 루프를 방지하기 위해 렌더링 횟수를 제한합니다.` 전형적으로 이는 *렌더링 중*에 state를 무조건적으로 설정하고 있음을 의미 하기 때문에, 컴포넌트가 렌더링, state 설정(렌더링 유발), 렌더링, state 설정(렌더링 유발) 등의 루프에 들어가는 것입니다. 이 문제는 이벤트 핸들러를 지정하는 과정에서 실수로 발생하는 경우가 많습니다.\n\n```js {1-2}\n// 🚩 잘못된 방법: 렌더링 동안 핸들러 요청\nreturn <button onClick={handleClick()}>Click me</button>\n\n// ✅ 올바른 방법: 이벤트 핸들러로 전달\nreturn <button onClick={handleClick}>Click me</button>\n\n// ✅ 올바른 방법: 인라인 함수로 전달\nreturn <button onClick={(e) => handleClick(e)}>Click me</button>\n```\n\n이 에러의 원인을 찾을 수 없는 경우, 콘솔에서 에러 옆에 있는 화살표를 클릭한 뒤 JavaScript 스택에서 에러의 원인이 되는 특정 `set` 함수 호출을 찾아보세요.\n\n---\n\n### 초기화 함수 또는 업데이터 함수가 두 번 실행됩니다 {/*my-initializer-or-updater-function-runs-twice*/}\n\n[Strict Mode](/reference/react/StrictMode)에서 React는 일부 함수를 한 번이 아닌 두 번 호출합니다.\n\n```js {2,5-6,11-12}\nfunction TodoList() {\n  // 해당 함수형 컴포넌트는 렌더링 될 때마다 두 번 호출됩니다.\n\n  const [todos, setTodos] = useState(() => {\n    // 해당 초기화 함수는 초기화 동안 두 번 호출됩니다.\n    return createTodos();\n  });\n\n  function handleClick() {\n    setTodos(prevTodos => {\n      // 해당 업데이터 함수는 클릭할 때마다 두 번 호출됩니다.\n      return [...prevTodos, createTodo()];\n    });\n  }\n  // ...\n```\n\n이는 정상적인 현상이며, 이로 인해 코드가 손상되지 않아야 합니다.\n\n이 **개발 환경 전용** 동작은 [컴포넌트를 순수하게 유지](/learn/keeping-components-pure)하는 데 도움이 됩니다. React는 호출 중 하나의 결과를 사용하고 다른 호출의 결과는 무시합니다. 컴포넌트, 초기화 함수, 업데이터 함수가 순수하다면 이 동작이 로직에 영향을 미치지 않습니다. 반면 의도치 않게 순수하지 않을 경우에는 실수를 알아차리는 데 도움이 됩니다.\n\n예를 들어, 순수하지 않은 업데이터 함수는 state의 배열을 다음과 같이 변경합니다.\n\n```js {2,3}\nsetTodos(prevTodos => {\n  // 🚩 실수: state 변경\n  prevTodos.push(createTodo());\n});\n```\n\nReact는 업데이터 함수를 두 번 호출하기 때문에 할 일이 두 번 추가되었음을 알 수 있으므로, 실수가 있음을 파악할 수 있습니다. 이 예시에서는 [배열을 변경하는 대신 교체](#updating-objects-and-arrays-in-state)하여 실수를 수정할 수 있습니다:\n\n```js {2,3}\nsetTodos(prevTodos => {\n  // ✅ 올바른 방법: 새로운 state로 교체\n  return [...prevTodos, createTodo()];\n});\n```\n\n이제 이 업데이터 함수는 순수하므로 한 번 더 호출해도 동작에 차이가 없습니다. 그렇기 때문에 React가 두 번 호출하는 것이 실수를 찾는 데 도움이 된다는 것입니다. **컴포넌트, 초기화 함수, 업데이터 함수는 순수해야 합니다**. 이벤트 핸들러는 순수할 필요가 없으므로 React는 이벤트 핸들러를 두 번 호출하지 않습니다.\n\n자세히 알아보려면 [컴포넌트 순수하게 유지하기](/learn/keeping-components-pure)를 읽어보세요.\n\n---\n\n### state의 값으로 함수를 설정하려고 하면 설정 대신 호출됩니다 {/*im-trying-to-set-state-to-a-function-but-it-gets-called-instead*/}\n\nstate에 함수를 넣을 수 없습니다.\n\n```js\nconst [fn, setFn] = useState(someFunction);\n\nfunction handleClick() {\n  setFn(someOtherFunction);\n}\n```\n\n함수를 값으로 전달하면 React는 `someFunction`을 [초기화 함수](#avoiding-recreating-the-initial-state)로 여기고, `someOtherFunction`은 [업데이터 함수](#updating-state-based-on-the-previous-state)라고 여깁니다. 따라서 이들을 호출해서 그 결과를 저장하려고 시도합니다. 정말로 함수를 저장하길 원한다면, 두 경우 모두 함수 앞에 `() =>`를 넣어야 합니다. 그러면 React는 전달한 함수를 값으로 저장합니다.\n\n```js {1,4}\nconst [fn, setFn] = useState(() => someFunction);\n\nfunction handleClick() {\n  setFn(() => someOtherFunction);\n}\n```\n"
  },
  {
    "path": "src/content/reference/react/useSyncExternalStore.md",
    "content": "---\ntitle: useSyncExternalStore\n---\n\n<Intro>\n\n`useSyncExternalStore`는 외부 store를 구독할 수 있는 React Hook입니다.\n\n```js\nconst snapshot = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)\n```\n\n</Intro>\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)` {/*usesyncexternalstore*/}\n\n컴포넌트의 최상위 레벨에서 `useSyncExternalStore`를 호출하여 외부 데이터 저장소에서 값을 읽습니다.\n\n```js\nimport { useSyncExternalStore } from 'react';\nimport { todosStore } from './todoStore.js';\n\nfunction TodosApp() {\n  const todos = useSyncExternalStore(todosStore.subscribe, todosStore.getSnapshot);\n  // ...\n}\n```\nstore에 있는 데이터의 스냅샷을 반환합니다. 두 개의 함수를 인수로 전달해야 합니다.\n\n1. `subscribe` 함수는 store를 구독하고 구독을 취소하는 함수를 반환해야 합니다.\n2. `getSnapshot` 함수는 store에서 데이터의 스냅샷을 읽어야 합니다.\n\n[아래 예시 참조](#usage)\n\n#### 매개변수 {/*parameters*/}\n\n* `subscribe`: 하나의 `callback` 인수를 받아 store에 구독하는 함수입니다. store가 변경될 때, 제공된 `callback`이 호출되어 React가 `getSnapshot`을 다시 호출하고 (필요한 경우) 컴포넌트를 다시 렌더링하도록 해야 합니다. `subscribe` 함수는 구독을 정리하는 함수를 반환해야 합니다.\n\n* `getSnapshot`: 컴포넌트에 필요한 store 데이터의 스냅샷을 반환하는 함수입니다. store가 변경되지 않은 상태에서 `getSnapshot`을 반복적으로 호출하면 동일한 값을 반환해야 합니다. 저장소가 변경되어 반환된 값이 다르면 ([`Object.is`](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Object/is)와 비교하여) React는 컴포넌트를 리렌더링합니다.\n\n* **optional** `getServerSnapshot`: store에 있는 데이터의 초기 스냅샷을 반환하는 함수입니다. 서버 렌더링 도중과 클라이언트에서 서버 렌더링 된 콘텐츠의 하이드레이션 중에만 사용됩니다. 서버 스냅샷은 클라이언트와 서버 간에 동일해야 하며 일반적으로 직렬화되어 서버에서 클라이언트로 전달됩니다. 이 함수가 제공되지 않으면 서버에서 컴포넌트를 렌더링할 때 오류가 발생합니다.\n\n#### 반환값 {/*returns*/}\n\n렌더링 로직에 사용할 수 있는 store의 현재 스냅샷입니다.\n\n#### 주의 사항 {/*caveats*/}\n\n* `getSnapshot`이 반환하는 store 스냅샷은 불변이어야 합니다. 기본 스토어에 변경 가능한 데이터가 있는 경우 데이터가 변경된 경우 변경 불가능한 새 스냅샷을 반환합니다. 그렇지 않으면 캐시 된 마지막 스냅샷을 반환합니다.\n\n* 리렌더링하는 동안 다른 `subscribe` 함수가 전달되면 React는 새로 전달된 `subscribe` 함수를 사용하여 저장소를 다시 구독합니다. 컴포넌트 외부에서 `subscribe` 를 선언하면 이를 방지할 수 있습니다.\n\n* [non-blocking transition 업데이트](/reference/react/useTransition) 중에 스토어가 변경되면, React는 해당 업데이트를 blocking으로 수행하도록 되돌아갑니다. 구체적으로, 모든 Transition 업데이트에 대해 React는 DOM에 변경 사항을 적용하기 직전에 `getSnapshot`을 한 번 더 호출합니다. 처음 호출했을 때와 다른 값을 반환하면, React는 처음부터 다시 업데이트를 시작하고, 이번에는 blocking 업데이트를 적용하여 화면의 모든 컴포넌트가 같은 스토어 버전을 반영하도록 합니다.\n\n* `useSyncExternalStore`가 반환한 스토어 값을 기반으로 렌더링을 _일시 중단_ 하는 것은 권장하지 않습니다. 그 이유는 외부 스토어에 대한 변형을 [non-blocking transition 업데이트](/reference/react/useTransition)로 표시할 수 없기 때문에, 가장 가까운 [`Suspense` fallback](/reference/react/Suspense)을 트리거해서, 화면에서 로딩 스피너로 대체하여, 일반적으로 UX가 좋지 않기 때문입니다.\n\n  예를 들어, 다음은 권장되지 않습니다.\n\n  ```js\n  const LazyProductDetailPage = lazy(() => import('./ProductDetailPage.js'));\n\n  function ShoppingApp() {\n    const selectedProductId = useSyncExternalStore(...);\n\n    // ❌ `selectedProductId`에 종속된 Promise로 `use`를 호출하는 것\n    const data = use(fetchItem(selectedProductId))\n\n    // ❌ `selectedProductId`를 기반으로 지연 컴포넌트를 조건부로 렌더링하는 것\n    return selectedProductId != null ? <LazyProductDetailPage /> : <FeaturedProducts />;\n  }\n  ```\n\n---\n\n## 사용법 {/*usage*/}\n\n### 외부 store 구독 {/*subscribing-to-an-external-store*/}\n\n대부분의 React 컴포넌트는 [props](/learn/passing-props-to-a-component), [state](/reference/react/useState), 그리고 [context](/reference/react/useContext)에서만 데이터를 읽습니다. 하지만 때로는 컴포넌트가 시간이 지남에 따라 변경되는 React 외부의 일부 저장소에서 일부 데이터를 읽어야 하는 경우가 있습니다. 다음이 포함됩니다.\n\n* React 외부에 state를 보관하는 서드파티 상태 관리 라이브러리.\n* 변경 가능한 값을 노출하는 브라우저 API와 그 변경 사항을 구독하는 이벤트.\n\n외부 데이터 저장소에서 값을 읽으려면 컴포넌트의 최상위 레벨에서 `useSyncExternalStore`를 호출하세요.\n\n```js [[1, 5, \"todosStore.subscribe\"], [2, 5, \"todosStore.getSnapshot\"], [3, 5, \"todos\", 0]]\nimport { useSyncExternalStore } from 'react';\nimport { todosStore } from './todoStore.js';\n\nfunction TodosApp() {\n  const todos = useSyncExternalStore(todosStore.subscribe, todosStore.getSnapshot);\n  // ...\n}\n```\n\nstore에 있는 데이터의 <CodeStep step={3}>snapshot</CodeStep>을 반환합니다. 두 개의 함수를 인수로 전달해야 합니다.\n\n1. <CodeStep step={1}>`subscribe`</CodeStep> 함수는 store에 구독하고 구독을 취소하는 함수를 반환해야 합니다.\n2. <CodeStep step={2}>`getSnapshot`</CodeStep> 함수는 store에서 데이터의 스냅샷을 읽어야 합니다.\n\nReact는 이 함수를 사용해 컴포넌트를 store에 구독한 상태로 유지하고 변경 사항이 있을 때 리렌더링합니다.\n\n예를 들어 아래 샌드박스에서 `todosStore`는 React 외부에 데이터를 저장하는 외부 store로 구현되어 있습니다. `TodosApp`컴포넌트는 `useSyncExternalStore` Hook으로 해당 외부 store에 연결합니다.\n\n<Sandpack>\n\n```js\nimport { useSyncExternalStore } from 'react';\nimport { todosStore } from './todoStore.js';\n\nexport default function TodosApp() {\n  const todos = useSyncExternalStore(todosStore.subscribe, todosStore.getSnapshot);\n  return (\n    <>\n      <button onClick={() => todosStore.addTodo()}>Add todo</button>\n      <hr />\n      <ul>\n        {todos.map(todo => (\n          <li key={todo.id}>{todo.text}</li>\n        ))}\n      </ul>\n    </>\n  );\n}\n```\n\n```js src/todoStore.js\n// 이것은 third-party store의 예시입니다\n// 해당 store를 사용하는 경우 React와 통합할 필요가 있을 수 있습니다.\n\n// 앱이 React로 완전히 빌드된 경우,\n// React state를 사용하는 것을 추천드립니다.\n\nlet nextId = 0;\nlet todos = [{ id: nextId++, text: 'Todo #1' }];\nlet listeners = [];\n\nexport const todosStore = {\n  addTodo() {\n    todos = [...todos, { id: nextId++, text: 'Todo #' + nextId }]\n    emitChange();\n  },\n  subscribe(listener) {\n    listeners = [...listeners, listener];\n    return () => {\n      listeners = listeners.filter(l => l !== listener);\n    };\n  },\n  getSnapshot() {\n    return todos;\n  }\n};\n\nfunction emitChange() {\n  for (let listener of listeners) {\n    listener();\n  }\n}\n```\n\n</Sandpack>\n\n<Note>\n\n가능하면 내장된 React state를 [`useState`](/reference/react/useState) 및 [`useReducer`](/reference/react/useReducer)와 함께 사용하는 것이 좋습니다. `useSyncExternalStore` API는 기존 비 React 코드와 통합해야 할 때 주로 유용합니다.\n\n</Note>\n\n---\n\n### 브라우저 API 구독 {/*subscribing-to-a-browser-api*/}\n\n`useSyncExternalStore`를 추가하는 또 다른 이유는 시간이 지남에 따라 변경되는 브라우저에 노출되는 일부 값을 구독하려는 경우입니다. 예를 들어 컴포넌트에 네트워크 연결이 활성화되어 있는지 여부를 표시하고 싶다고 가정해 보겠습니다. 브라우저는 [`navigator.onLine`.](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/onLine)이라는 속성을 통해 이 정보를 노출합니다.\n\n이 값은 시간이 지남에 따라 React가 알지 못하는 사이에 변경될 수 있으므로 `useSyncExternalStore`로 값을 읽어야 합니다.\n\n```js\nimport { useSyncExternalStore } from 'react';\n\nfunction ChatIndicator() {\n  const isOnline = useSyncExternalStore(subscribe, getSnapshot);\n  // ...\n}\n```\n\n`getSnapshot` 함수를 구현하려면 브라우저 API에서 현재 값을 읽습니다.\n\n```js\nfunction getSnapshot() {\n  return navigator.onLine;\n}\n```\n\n다음으로 `subscribe` 함수를 구현해야 합니다. 예를 들어 `navigator.onLine`이 변경되면 브라우저는 `window` 객체에서 [`online`](https://developer.mozilla.org/en-US/docs/Web/API/Window/online_event) 및 [`offline`](https://developer.mozilla.org/en-US/docs/Web/API/Window/offline_event) 이벤트를 실행합니다. `callback` 인수를 해당 이벤트에 구독한 다음 구독을 정리하는 함수를 반환해야 합니다.\n\n```js\nfunction subscribe(callback) {\n  window.addEventListener('online', callback);\n  window.addEventListener('offline', callback);\n  return () => {\n    window.removeEventListener('online', callback);\n    window.removeEventListener('offline', callback);\n  };\n}\n```\n\n이제 React는 외부 `navigator.onLine` API에서 값을 읽는 방법과 그 변경 사항을 구독하는 방법을 알고 있습니다. 네트워크에서 디바이스의 연결을 끊어보면 컴포넌트가 응답으로 리렌더링되는 것을 확인할 수 있습니다.\n\n<Sandpack>\n\n```js\nimport { useSyncExternalStore } from 'react';\n\nexport default function ChatIndicator() {\n  const isOnline = useSyncExternalStore(subscribe, getSnapshot);\n  return <h1>{isOnline ? '✅ Online' : '❌ Disconnected'}</h1>;\n}\n\nfunction getSnapshot() {\n  return navigator.onLine;\n}\n\nfunction subscribe(callback) {\n  window.addEventListener('online', callback);\n  window.addEventListener('offline', callback);\n  return () => {\n    window.removeEventListener('online', callback);\n    window.removeEventListener('offline', callback);\n  };\n}\n```\n\n</Sandpack>\n\n---\n\n### custom Hook으로 로직 추출하기 {/*extracting-the-logic-to-a-custom-hook*/}\n\n일반적으로 컴포넌트에서 직접 `useSyncExternalStore`를 작성하지는 않습니다. 대신 일반적으로 custom Hook에서 호출합니다. 이렇게 하면 서로 다른 컴포넌트에서 동일한 외부 저장소를 사용할 수 있습니다.\n\n예를 들어 이 custom `useOnlineStatus` Hook은 네트워크가 온라인 상태인지 여부를 추적합니다.\n\n```js {3,6}\nimport { useSyncExternalStore } from 'react';\n\nexport function useOnlineStatus() {\n  const isOnline = useSyncExternalStore(subscribe, getSnapshot);\n  return isOnline;\n}\n\nfunction getSnapshot() {\n  // ...\n}\n\nfunction subscribe(callback) {\n  // ...\n}\n```\n\n이제 다른 컴포넌트에서 기본 구현을 반복하지 않고도 `useOnlineStatus`를 호출할 수 있습니다.\n\n<Sandpack>\n\n```js\nimport { useOnlineStatus } from './useOnlineStatus.js';\n\nfunction StatusBar() {\n  const isOnline = useOnlineStatus();\n  return <h1>{isOnline ? '✅ Online' : '❌ Disconnected'}</h1>;\n}\n\nfunction SaveButton() {\n  const isOnline = useOnlineStatus();\n\n  function handleSaveClick() {\n    console.log('✅ Progress saved');\n  }\n\n  return (\n    <button disabled={!isOnline} onClick={handleSaveClick}>\n      {isOnline ? 'Save progress' : 'Reconnecting...'}\n    </button>\n  );\n}\n\nexport default function App() {\n  return (\n    <>\n      <SaveButton />\n      <StatusBar />\n    </>\n  );\n}\n```\n\n```js src/useOnlineStatus.js\nimport { useSyncExternalStore } from 'react';\n\nexport function useOnlineStatus() {\n  const isOnline = useSyncExternalStore(subscribe, getSnapshot);\n  return isOnline;\n}\n\nfunction getSnapshot() {\n  return navigator.onLine;\n}\n\nfunction subscribe(callback) {\n  window.addEventListener('online', callback);\n  window.addEventListener('offline', callback);\n  return () => {\n    window.removeEventListener('online', callback);\n    window.removeEventListener('offline', callback);\n  };\n}\n```\n\n</Sandpack>\n\n---\n\n### 서버 렌더링 지원 추가 {/*adding-support-for-server-rendering*/}\n\nReact 앱이 [server rendering](/reference/react-dom/server)을 사용하는 경우 React 컴포넌트는 브라우저 환경 외부에서도 실행되어 초기 HTML을 생성합니다. 이로 인해 외부 store에 연결할 때 몇 가지 문제가 발생합니다.\n\n- 브라우저 전용 API에 연결하는 경우 서버에 해당 API가 존재하지 않으므로 작동하지 않습니다.\n- 서드 파티 데이터 저장소에 연결하는 경우 서버와 클라이언트 간에 일치하는 데이터가 필요합니다.\n\n이러한 문제를 해결하려면 `getServerSnapshot` 함수를 `useSyncExternalStore`의 세 번째 인수로 전달하세요.\n\n```js {4,12-14}\nimport { useSyncExternalStore } from 'react';\n\nexport function useOnlineStatus() {\n  const isOnline = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);\n  return isOnline;\n}\n\nfunction getSnapshot() {\n  return navigator.onLine;\n}\n\nfunction getServerSnapshot() {\n  return true; // 서버에서 생성된 HTML에는 항상 \"Online\"을 표시합니다.\n}\n\nfunction subscribe(callback) {\n  // ...\n}\n```\n\n`getServerSnapshot` 함수는 `getSnapshot`과 유사하지만 두 가지 상황에서만 실행됩니다.\n\n- HTML을 생성할 때 서버에서 실행됩니다.\n- [hydration](/reference/react-dom/client/hydrateRoot) 중 즉 React가 서버 HTML을 가져와서 인터랙티브하게 만들 때 클라이언트에서 실행됩니다.\n\n이를 통해 앱이 상호작용하기 전에 사용될 초기 스냅샷 값을 제공할 수 있습니다. 서버 렌더링에 의미 있는 초기값이 없다면 [컴포넌트가 클라이언트에서만 렌더링되도록 강제 설정](/reference/react/Suspense#providing-a-fallback-for-server-errors-and-client-only-content)할 수 있습니다.\n\n<Note>\n\n`getServerSnapshot`이 초기 클라이언트 렌더링에서 서버에서 반환한 것과 동일한 정확한 데이터를 반환하는지 확인하세요. 예를 들어 `getServerSnapshot`이 서버에서 미리 채워진 store 콘텐츠를 반환한 경우 이 콘텐츠를 클라이언트로 전송해야 합니다. 이를 수행하는 일반적인 방법 중 하나는 서버 렌더링 중에 `window.MY_STORE_DATA`와 같은 글로벌을 설정하는 `<script>` 태그를 생성한 다음 클라이언트에서 `getServerSnapshot`에서 해당 글로벌을 읽어오는 것입니다. 외부 스토어에서 이를 수행하는 방법에 대한 지침을 제공해야 합니다.\n\n</Note>\n\n---\n\n## 트러블 슈팅 {/*troubleshooting*/}\n\n### 오류가 발생했습니다: \"`getSnapshot`의 결과를 캐시해야 합니다.\" {/*오류가-발생했습니다-getsnapshot의-결과를-캐시해야-합니다*/}\n\n이 오류가 발생하면 `getSnapshot` 함수가 호출될 때마다 새 객체를 반환한다는 의미입니다.\n\n```js {2-5}\nfunction getSnapshot() {\n  // 🔴 getSnapshot에서 항상 다른 객체를 반환하지 마세요.\n  return {\n    todos: myStore.todos\n  };\n}\n```\n\nReact는 `getSnapshot` 반환 값이 지난번과 다르면 컴포넌트를 리렌더링합니다. 그렇기 때문에 항상 다른 값을 반환하면 무한 루프에 들어가서 이 오류가 발생합니다.\n\n실제로 변경된 사항이 있는 경우에만 `getSnapshot` 객체가 다른 객체를 반환해야 합니다. store에 변경 불가능한 데이터가 포함된 경우 해당 데이터를 직접 반환할 수 있습니다.\n\n```js {2-3}\nfunction getSnapshot() {\n  // ✅ 불변 데이터를 반환할 수 있습니다.\n  return myStore.todos;\n}\n```\n\nstore 데이터가 변경 가능한 경우 `getSnapshot` 함수는 해당 데이터의 변경 불가능한 스냅샷을 반환해야 합니다. 즉 새 객체를 생성해야 하지만 매번 호출할 때마다 이 작업을 수행해서는 안 됩니다. 대신 마지막으로 계산된 스냅샷을 저장하고 저장소의 데이터가 변경되지 않은 경우 지난번과 동일한 스냅샷을 반환해야 합니다. 변경 가능한 데이터가 변경되었는지 확인하는 방법은 변경 가능한 저장소가 구현된 방식에 따라 다릅니다.\n\n---\n\n### 리렌더링할 때마다 `subscribe` 함수가 호출됩니다. {/*my-subscribe-function-gets-called-after-every-re-render*/}\n\nsubscribe 함수는 컴포넌트 내부에 정의되므로 리렌더링할 때마다 달라집니다.\n\n```js {2-5}\nfunction ChatIndicator() {\n  // 🚩 항상 다른 함수를 사용하므로 React는 렌더링할 때마다 다시 구독합니다.\n  function subscribe() {\n    // ...\n  }\n  \n  const isOnline = useSyncExternalStore(subscribe, getSnapshot);\n\n  // ...\n}\n```\n\n리렌더링 사이에 다른 `subscribe` 함수를 전달하면 React가 store를 다시 구독합니다. 이로 인해 성능 문제가 발생하고 store 재구독을 피하고 싶다면 `subscribe` 함수를 외부로 이동하세요.\n\n```js {1-4}\n// ✅ 항상 동일한 함수이므로 React는 다시 구독할 필요가 없습니다.\nfunction subscribe() {\n  // ...\n}\n\nfunction ChatIndicator() {\n  const isOnline = useSyncExternalStore(subscribe, getSnapshot);\n  // ...\n}\n```\n\n또는 일부 인수가 변경될 때만 다시 구독하도록 `subscribe`을 [`useCallback`](/reference/react/useCallback)으로 래핑합니다.\n\n```js {2-5}\nfunction ChatIndicator({ userId }) {\n  // ✅ userId가 변경되지 않는 한 동일한 함수입니다.\n  const subscribe = useCallback(() => {\n    // ...\n  }, [userId]);\n  \n  const isOnline = useSyncExternalStore(subscribe, getSnapshot);\n\n  // ...\n}\n```\n"
  },
  {
    "path": "src/content/reference/react/useTransition.md",
    "content": "---\ntitle: useTransition\n---\n\n<Intro>\n\n`useTransition`은 UI의 일부를 백그라운드에서 렌더링 할 수 있도록 해주는 React Hook입니다.\n\n```js\nconst [isPending, startTransition] = useTransition()\n```\n\n</Intro>\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `useTransition()` {/*usetransition*/}\n\n컴포넌트의 최상위 수준에서 `useTransition`을 호출하여 일부 state 업데이트를 Transition 으로 표시합니다.\n\n```js\nimport { useTransition } from 'react';\n\nfunction TabContainer() {\n  const [isPending, startTransition] = useTransition();\n  // ...\n}\n```\n\n[아래에서 더 많은 예시를 확인하세요.](#usage)\n\n#### 매개변수 {/*parameters*/}\n\n`useTransition`은 어떤 매개변수도 받지 않습니다.\n\n#### 반환값 {/*returns*/}\n\n`useTransition`은 정확히 두 개의 항목이 있는 배열을 반환합니다.\n\n1. `isPending` 플래그는 대기 중인 Transition이 있는지 알려줍니다.\n2. [`startTransition` 함수](#starttransition)는 업데이트를 Transition으로 표시할 수 있게 해주는 함수입니다.\n\n---\n\n### `startTransition(action)` {/*starttransition*/}\n\n`useTransition`이 반환하는 `startTransition` 함수를 사용하면 업데이트를 Transition으로 표시할 수 있습니다.\n\n```js {6,8}\nfunction TabContainer() {\n  const [isPending, startTransition] = useTransition();\n  const [tab, setTab] = useState('about');\n\n  function selectTab(nextTab) {\n    startTransition(() => {\n      setTab(nextTab);\n    });\n  }\n  // ...\n}\n```\n\n<Note>\n#### `startTransition` 내에서 호출되는 함수를 \"Action\"이라고 합니다. {/*functions-called-in-starttransition-are-called-actions*/}\n\n`startTransition`에 전달된 함수를 \"Action\"이라고 합니다. 관례적으로, `startTransition` 내부에서 호출되는 모든 콜백(예: 콜백 프로퍼티)의 이름은 `action`이거나 \"Action\" 접미사를 포함해야 합니다.\n\n```js {1,9}\nfunction SubmitButton({ submitAction }) {\n  const [isPending, startTransition] = useTransition();\n\n  return (\n    <button\n      disabled={isPending}\n      onClick={() => {\n        startTransition(async () => {\n          await submitAction();\n        });\n      }}\n    >\n      Submit\n    </button>\n  );\n}\n\n```\n\n</Note>\n\n\n\n#### 매개변수 {/*starttransition-parameters*/}\n\n* `action`: 하나 이상의 [`set` 함수](/reference/react/useState#setstate)를 호출하여 일부 상태를 업데이트하는 함수입니다. React는 매개변수 없이 즉시 `action`을 호출하고 `action` 함수 호출 중에 동기적으로 예약된 모든 상태 업데이트를 Transition으로 표시합니다. `action`에서 `await`된 비동기 호출은 Transition에 포함되지만, 현재로서는 `await` 이후의 `set` 함수 호출을 추가적인 `startTransition`으로 감싸야 합니다([문제 해결 참조](#react-doesnt-treat-my-state-update-after-await-as-a-transition)).\nTransition으로 표시된 상태 업데이트는 [non-blocking](#marking-a-state-update-as-a-non-blocking-transition) 방식으로 처리되며, [불필요한 로딩 표시가 나타나지 않습니다](#preventing-unwanted-loading-indicators).\n\n#### 반환값 {/*starttransition-returns*/}\n\n`startTransition`은 아무것도 반환하지 않습니다.\n\n#### 주의 사항 {/*starttransition-caveats*/}\n\n* `useTransition`은 Hook이므로 컴포넌트나 커스텀 Hook 내부에서만 호출할 수 있습니다. 다른 곳(예시: 데이터 라이브러리)에서 Transition을 시작해야 하는 경우, 독립형 [`startTransition`](/reference/react/startTransition)을 호출하세요.\n\n* 해당 state의 `set` 함수에 액세스할 수 있는 경우에만 업데이트를 Transition 으로 래핑할 수 있습니다. 일부 prop이나 커스텀 Hook 값에 대한 응답으로 Transition을 시작하려면 [`useDeferredValue`](/reference/react/useDeferredValue)를 사용해 보세요.\n\n* `startTransition`에 전달하는 함수는 동기식이어야 합니다. React는 이 함수를 즉시 실행하여 실행하는 동안 발생하는 모든 state 업데이트를 Transition으로 표시합니다. 나중에 더 많은 state 업데이트를 수행하려고 하면(예시: timeout), Transition 으로 표시되지 않습니다.\n\n* `startTransition`에 전달하는 함수는 즉시 호출되며, 실행 중 발생하는 모든 상태 업데이트를 Transition으로 표시합니다. 예를 들어 `setTimeout` 내에서 상태를 업데이트하려고 하면, 해당 업데이트는 Transition으로 표시되지 않습니다.\n\n* 비동기 요청 이후의 상태 업데이트를 전환으로 표시하려면, 반드시 또 다른 `startTransition`으로 감싸야 합니다. 이는 알려진 제한 사항으로 향후 수정될 예정입니다([문제 해결 참조](#react-doesnt-treat-my-state-update-after-await-as-a-transition)).\n\n* `startTransition` 함수는 안정된 식별성(stable identity)을 가지므로 Effect 의존성에서 생략되는 경우가 많습니다. 하지만 포함해도 Effect가 실행되지는 않습니다. linter가 의존성을 생략해도 오류를 발생시키지 않는다면 생략해도 안전합니다. [자세한 내용은 Effect 의존성 제거에 대한 문서를 참고하세요.](/learn/removing-effect-dependencies#move-dynamic-objects-and-functions-inside-your-effect)\n\n* Transition으로 표시된 state 업데이트는 다른 state 업데이트에 의해 중단됩니다. 예를 들어 Transition 내에서 차트 컴포넌트를 업데이트한 다음 차트가 다시 렌더링 되는 도중에 입력을 시작하면 React는 입력 업데이트를 처리한 후 차트 컴포넌트에서 렌더링 작업을 다시 시작합니다.\n\n* Transition 업데이트는 텍스트 입력을 제어하는 데 사용할 수 없습니다.\n\n* 진행 중인 Transition 이 여러 개 있는 경우, React는 현재 Transition 을 함께 일괄 처리합니다. 이는 향후 릴리즈에서 제거될 가능성이 높은 제한 사항입니다.\n\n## 사용법 {/*usage*/}\n\n### Actions으로 non-blocking 업데이트 수행 {/*perform-non-blocking-updates-with-actions*/}\n\n컴포넌트 상단에서 `useTransition`을 호출하여 Actions을 생성하고, 대기 상태에 접근하세요.\n\n```js [[1, 4, \"isPending\"], [2, 4, \"startTransition\"]]\nimport {useState, useTransition} from 'react';\n\nfunction CheckoutForm() {\n  const [isPending, startTransition] = useTransition();\n  // ...\n}\n```\n\n`useTransition`은 정확히 두 개의 항목이 있는 배열을 반환합니다.\n\n1. <CodeStep step={1}>`isPending` 플래그</CodeStep>는 대기 중인 Transition 이 있는지 알려줍니다.\n2. <CodeStep step={2}>`startTransition` 함수</CodeStep>는 상태 업데이트를 Transition으로 표시할 수 있게 해주는 함수입니다.\n\nTransition을 시작하려면 다음과 같이 `startTransition`에 함수를 전달합니다.\n```js\nimport {useState, useTransition} from 'react';\nimport {updateQuantity} from './api';\n\nfunction CheckoutForm() {\n  const [isPending, startTransition] = useTransition();\n  const [quantity, setQuantity] = useState(1);\n\n  function onSubmit(newQuantity) {\n    startTransition(async function () {\n      const savedQuantity = await updateQuantity(newQuantity);\n      startTransition(() => {\n        setQuantity(savedQuantity);\n      });\n    });\n  }\n  // ...\n}\n```\n\n`startTransition`에 전달한 함수를 \"Action\"이라고 합니다. Action 안에서는 state를 업데이트할 수 있으며, 필요하다면 부수 효과도 수행할 수 있습니다. 이 작업은 페이지의 사용자 상호작용을 차단하지 않고 백그라운드에서 처리됩니다. 하나의 Transition에는 여러 Action이 포함될 수 있고, Transition이 진행되는 동안에도 UI는 반응성을 유지합니다. 예를 들어 사용자가 탭을 클릭한 뒤 마음이 바뀌어 다른 탭을 다시 클릭하더라도, 첫 번째 업데이트가 끝나기를 기다리지 않고 두 번째 클릭이 즉시 처리됩니다.\n\n진행 중인 Transition에 대해 사용자에게 피드백을 제공하기 위해 `isPending` 상태는 `startTransition`을 처음 호출할 때 `true`로 전환되며, 모든 Action이 완료되어 최종 상태가 사용자에게 표시될 때까지 `true` 상태를 유지합니다. Transition은 Action 내의 사이드 이펙트가 완료되도록 보장하여 [원치 않는 로딩 표시기가 표시되지 않도록 합니다.](#preventing-unwanted-loading-indicators) 또한, Transition이 진행 중일 때 `useOptimistic`을 사용하여 즉각적인 피드백을 제공할 수 있습니다.\n\n<Recipes titleText=\"Action과 일반 이벤트 처리의 차이점\">\n\n#### Action에서 수량 업데이트 {/*updating-the-quantity-in-an-action*/}\n\n이 예시에서 `updateQuantity` 함수는 카트에 있는 품목의 수량을 업데이트하기 위해 서버에 요청하는 시뮬레이션을 수행합니다. 이 함수는 요청을 완료하는 데 최소 1초가 소요되도록 *인위적으로 속도가 늦춰져 있습니다*.\n\n수량을 빠르게 여러 번 업데이트하면, 요청이 진행 중인 동안에는 \"Total\" 상태가 대기 중으로 표시됩니다. 그리고 최종 요청이 완료된 후에만 \"Total\"이 업데이트됩니다. 업데이트가 Action 내에서 발생하기 때문에, 요청이 진행 중인 동안에도 \"quantity\"은 계속해서 업데이트될 수 있습니다.\n\n<Sandpack>\n\n```json package.json hidden\n{\n  \"dependencies\": {\n    \"react\": \"beta\",\n    \"react-dom\": \"beta\"\n  },\n  \"scripts\": {\n    \"start\": \"react-scripts start\",\n    \"build\": \"react-scripts build\",\n    \"test\": \"react-scripts test --env=jsdom\",\n    \"eject\": \"react-scripts eject\"\n  }\n}\n```\n\n```js src/App.js\nimport { useState, useTransition } from \"react\";\nimport { updateQuantity } from \"./api\";\nimport Item from \"./Item\";\nimport Total from \"./Total\";\n\nexport default function App({}) {\n  const [quantity, setQuantity] = useState(1);\n  const [isPending, startTransition] = useTransition();\n\n  const updateQuantityAction = async newQuantity => {\n    // transition의 보류 중인 상태에 액세스하려면,\n    // startTransition을 다시 호출하세요.\n    startTransition(async () => {\n      const savedQuantity = await updateQuantity(newQuantity);\n      startTransition(() => {\n        setQuantity(savedQuantity);\n      });\n    });\n  };\n\n  return (\n    <div>\n      <h1>Checkout</h1>\n      <Item action={updateQuantityAction}/>\n      <hr />\n      <Total quantity={quantity} isPending={isPending} />\n    </div>\n  );\n}\n```\n\n```js src/Item.js\nimport { startTransition } from \"react\";\n\nexport default function Item({action}) {\n  function handleChange(event) {\n    // To expose an action prop, await the callback in startTransition.\n    startTransition(async () => {\n      await action(event.target.value);\n    })\n  }\n  return (\n    <div className=\"item\">\n      <span>Eras Tour Tickets</span>\n      <label htmlFor=\"name\">Quantity: </label>\n      <input\n        type=\"number\"\n        onChange={handleChange}\n        defaultValue={1}\n        min={1}\n      />\n    </div>\n  )\n}\n```\n\n```js src/Total.js\nconst intl = new Intl.NumberFormat(\"en-US\", {\n  style: \"currency\",\n  currency: \"USD\"\n});\n\nexport default function Total({quantity, isPending}) {\n  return (\n    <div className=\"total\">\n      <span>Total:</span>\n      <span>\n        {isPending ? \"🌀 Updating...\" : `${intl.format(quantity * 9999)}`}\n      </span>\n    </div>\n  )\n}\n```\n\n```js src/api.js\nexport async function updateQuantity(newQuantity) {\n  return new Promise((resolve, reject) => {\n    // 네트워크 요청을 느리게 시뮬레이션합니다.\n    setTimeout(() => {\n      resolve(newQuantity);\n    }, 2000);\n  });\n}\n```\n\n```css\n.item {\n  display: flex;\n  align-items: center;\n  justify-content: start;\n}\n\n.item label {\n  flex: 1;\n  text-align: right;\n}\n\n.item input {\n  margin-left: 4px;\n  width: 60px;\n  padding: 4px;\n}\n\n.total {\n  height: 50px;\n  line-height: 25px;\n  display: flex;\n  align-content: center;\n  justify-content: space-between;\n}\n```\n\n</Sandpack>\n\n이 예시는 Actions의 작동 방식을 보여주기 위한 기본 예시이지만, 순서대로 완료되는 요청은 처리하지 않습니다. 수량을 여러 번 업데이트하는 경우 이전 요청이 완료된 후 나중에 요청이 완료되어 수량이 순서대로 업데이트되지 않을 수 있습니다. 이는 알려진 제한 사항으로 향후 수정될 예정입니다([문제 해결 참조](#my-state-updates-in-transitions-are-out-of-order)).\n\n일반적인 사용 사례를 위해 React는 다음과 같은 내장 추상화 기능을 제공합니다.\n- [`useActionState`](/reference/react/useActionState)\n- [`<form>` actions](/reference/react-dom/components/form)\n- [Server Functions](/reference/rsc/server-functions)\n\n이러한 솔루션은 요청 순서를 자동으로 처리합니다. Transitions를 사용하여 custom Hook 또는 라이브러리를 구축하여 비동기 상태 전환을 관리하는 경우, 요청 순서를 더욱 세밀하게 제어할 수 있지만, 직접 처리해야 합니다.\n\n<Solution />\n\n#### Action 없이 수량 업데이트 {/*updating-the-users-name-without-an-action*/}\n\n이 예시에서 `updateQuantity` 함수는 장바구니 내 아이템의 수량을 업데이트하기 위해 서버로 요청을 보내는 동작도 시뮬레이션합니다. 이 함수는 요청 완료에 최소 1초가 소요되도록 *인위적으로 속도가 늦춰져 있습니다*.\n\n수량을 빠르게 여러 번 업데이트하면, 요청이 진행 중인 동안에는 \"Total\" 상태가 대기 중으로 표시됩니다. 그러나 \"quantity\"을 클릭할 때마다 \"Total\"이 여러 번 업데이트됩니다.\n\n<Sandpack>\n\n```json package.json hidden\n{\n  \"dependencies\": {\n    \"react\": \"beta\",\n    \"react-dom\": \"beta\"\n  },\n  \"scripts\": {\n    \"start\": \"react-scripts start\",\n    \"build\": \"react-scripts build\",\n    \"test\": \"react-scripts test --env=jsdom\",\n    \"eject\": \"react-scripts eject\"\n  }\n}\n```\n\n```js src/App.js\nimport { useState } from \"react\";\nimport { updateQuantity } from \"./api\";\nimport Item from \"./Item\";\nimport Total from \"./Total\";\n\nexport default function App({}) {\n  const [quantity, setQuantity] = useState(1);\n  const [isPending, setIsPending] = useState(false);\n\n  const onUpdateQuantity = async newQuantity => {\n    // isPending의 상태를 수동으로 설정합니다.\n    setIsPending(true);\n    const savedQuantity = await updateQuantity(newQuantity);\n    setIsPending(false);\n    setQuantity(savedQuantity);\n  };\n\n  return (\n    <div>\n      <h1>Checkout</h1>\n      <Item onUpdateQuantity={onUpdateQuantity}/>\n      <hr />\n      <Total quantity={quantity} isPending={isPending} />\n    </div>\n  );\n}\n\n```\n\n```js src/Item.js\nexport default function Item({onUpdateQuantity}) {\n  function handleChange(event) {\n    onUpdateQuantity(event.target.value);\n  }\n  return (\n    <div className=\"item\">\n      <span>Eras Tour Tickets</span>\n      <label htmlFor=\"name\">Quantity: </label>\n      <input\n        type=\"number\"\n        onChange={handleChange}\n        defaultValue={1}\n        min={1}\n      />\n    </div>\n  )\n}\n```\n\n```js src/Total.js\nconst intl = new Intl.NumberFormat(\"en-US\", {\n  style: \"currency\",\n  currency: \"USD\"\n});\n\nexport default function Total({quantity, isPending}) {\n  return (\n    <div className=\"total\">\n      <span>Total:</span>\n      <span>\n        {isPending ? \"🌀 Updating...\" : `${intl.format(quantity * 9999)}`}\n      </span>\n    </div>\n  )\n}\n```\n\n```js src/api.js\nexport async function updateQuantity(newQuantity) {\n  return new Promise((resolve, reject) => {\n    // 네트워크 요청을 느리게 시뮬레이션합니다.\n    setTimeout(() => {\n      resolve(newQuantity);\n    }, 2000);\n  });\n}\n```\n\n```css\n.item {\n  display: flex;\n  align-items: center;\n  justify-content: start;\n}\n\n.item label {\n  flex: 1;\n  text-align: right;\n}\n\n.item input {\n  margin-left: 4px;\n  width: 60px;\n  padding: 4px;\n}\n\n.total {\n  height: 50px;\n  line-height: 25px;\n  display: flex;\n  align-content: center;\n  justify-content: space-between;\n}\n```\n\n</Sandpack>\n\n이 문제에 대한 일반적인 해결책은 수량이 업데이트되는 동안 사용자가 변경을 수행하지 못하도록 하는 것입니다.\n\n<Sandpack>\n\n```json package.json hidden\n{\n  \"dependencies\": {\n    \"react\": \"beta\",\n    \"react-dom\": \"beta\"\n  },\n  \"scripts\": {\n    \"start\": \"react-scripts start\",\n    \"build\": \"react-scripts build\",\n    \"test\": \"react-scripts test --env=jsdom\",\n    \"eject\": \"react-scripts eject\"\n  }\n}\n```\n\n```js src/App.js\nimport { useState, useTransition } from \"react\";\nimport { updateQuantity } from \"./api\";\nimport Item from \"./Item\";\nimport Total from \"./Total\";\n\nexport default function App({}) {\n  const [quantity, setQuantity] = useState(1);\n  const [isPending, setIsPending] = useState(false);\n\n  const onUpdateQuantity = async event => {\n    const newQuantity = event.target.value;\n    // isPending의 상태를 수동으로 설정합니다.\n    setIsPending(true);\n    const savedQuantity = await updateQuantity(newQuantity);\n    setIsPending(false);\n    setQuantity(savedQuantity);\n  };\n\n  return (\n    <div>\n      <h1>Checkout</h1>\n      <Item isPending={isPending} onUpdateQuantity={onUpdateQuantity}/>\n      <hr />\n      <Total quantity={quantity} isPending={isPending} />\n    </div>\n  );\n}\n\n```\n\n```js src/Item.js\nexport default function Item({isPending, onUpdateQuantity}) {\n  return (\n    <div className=\"item\">\n      <span>Eras Tour Tickets</span>\n      <label htmlFor=\"name\">Quantity: </label>\n      <input\n        type=\"number\"\n        disabled={isPending}\n        onChange={onUpdateQuantity}\n        defaultValue={1}\n        min={1}\n      />\n    </div>\n  )\n}\n```\n\n```js src/Total.js\nconst intl = new Intl.NumberFormat(\"en-US\", {\n  style: \"currency\",\n  currency: \"USD\"\n});\n\nexport default function Total({quantity, isPending}) {\n  return (\n    <div className=\"total\">\n      <span>Total:</span>\n      <span>\n        {isPending ? \"🌀 Updating...\" : `${intl.format(quantity * 9999)}`}\n      </span>\n    </div>\n  )\n}\n```\n\n```js src/api.js\nexport async function updateQuantity(newQuantity) {\n  return new Promise((resolve, reject) => {\n    // 네트워크 요청을 느리게 시뮬레이션합니다.\n    setTimeout(() => {\n      resolve(newQuantity);\n    }, 2000);\n  });\n}\n```\n\n```css\n.item {\n  display: flex;\n  align-items: center;\n  justify-content: start;\n}\n\n.item label {\n  flex: 1;\n  text-align: right;\n}\n\n.item input {\n  margin-left: 4px;\n  width: 60px;\n  padding: 4px;\n}\n\n.total {\n  height: 50px;\n  line-height: 25px;\n  display: flex;\n  align-content: center;\n  justify-content: space-between;\n}\n```\n\n</Sandpack>\n\n이 솔루션은 사용자가 수량을 업데이트할 때마다 기다려야 하므로 앱이 느리게 느껴질 수 있습니다. 수량이 업데이트되는 동안에도 사용자가 UI와 계속해서 상호 작용할 수 있도록 수동으로 더 복잡한 처리를 추가할 수 있지만, Actions은 이러한 경우를 간단하게 처리할 수 있는 내장 API를 제공합니다.\n\n<Solution />\n\n</Recipes>\n\n---\n\n### 컴포넌트에서 Action 프로퍼티를 노출하기 {/*exposing-action-props-from-components*/}\n\n컴포넌트에서 `action` 프로퍼티를 노출시켜 부모 컴포넌트에서 Action을 호출할 수 있습니다.\n\n예를 들어, 이 `TabButton` 컴포넌트는 `onClick`에서 실행될 로직이 `action` prop으로 감싸져있습니다.\n\n```js {8-12}\nexport default function TabButton({ action, children, isActive }) {\n  const [isPending, startTransition] = useTransition();\n  if (isActive) {\n    return <b>{children}</b>\n  }\n  return (\n    <button onClick={() => {\n      startTransition(async () => {\n        // await the action that's passed in.\n        // This allows it to be either sync or async.\n        await action();\n      });\n    }}>\n      {children}\n    </button>\n  );\n}\n```\n\n부모 컴포넌트가 `action` 내부에서 상태를 업데이트하기 때문에, 해당 상태 업데이트는 Transition으로 표시됩니다. 그렇기 때문에 \"Posts\"을 클릭한 후 즉시 \"Contact\"를 클릭해도 사용자 상호작용이 차단되지 않습니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\nimport TabButton from './TabButton.js';\nimport AboutTab from './AboutTab.js';\nimport PostsTab from './PostsTab.js';\nimport ContactTab from './ContactTab.js';\n\nexport default function TabContainer() {\n  const [tab, setTab] = useState('about');\n  return (\n    <>\n      <TabButton\n        isActive={tab === 'about'}\n        action={() => setTab('about')}\n      >\n        About\n      </TabButton>\n      <TabButton\n        isActive={tab === 'posts'}\n        action={() => setTab('posts')}\n      >\n        Posts (slow)\n      </TabButton>\n      <TabButton\n        isActive={tab === 'contact'}\n        action={() => setTab('contact')}\n      >\n        Contact\n      </TabButton>\n      <hr />\n      {tab === 'about' && <AboutTab />}\n      {tab === 'posts' && <PostsTab />}\n      {tab === 'contact' && <ContactTab />}\n    </>\n  );\n}\n```\n\n```js src/TabButton.js active\nimport { useTransition } from 'react';\n\nexport default function TabButton({ action, children, isActive }) {\n  const [isPending, startTransition] = useTransition();\n  if (isActive) {\n    return <b>{children}</b>\n  }\n  if (isPending) {\n    return <b className=\"pending\">{children}</b>;\n  }\n  return (\n    <button onClick={async () => {\n      startTransition(async () => {\n        // await the action that's passed in.\n        // This allows it to be either sync or async.\n        await action();\n      });\n    }}>\n      {children}\n    </button>\n  );\n}\n```\n\n```js src/AboutTab.js\nexport default function AboutTab() {\n  return (\n    <p>Welcome to my profile!</p>\n  );\n}\n```\n\n```js src/PostsTab.js\nimport { memo } from 'react';\n\nconst PostsTab = memo(function PostsTab() {\n  // 한 번 로깅합니다. 실제 속도 저하는 SlowPost 컴포넌트 내부에 있습니다.\n  console.log('[ARTIFICIALLY SLOW] Rendering 500 <SlowPost />');\n\n  let items = [];\n  for (let i = 0; i < 500; i++) {\n    items.push(<SlowPost key={i} index={i} />);\n  }\n  return (\n    <ul className=\"items\">\n      {items}\n    </ul>\n  );\n});\n\nfunction SlowPost({ index }) {\n  let startTime = performance.now();\n  while (performance.now() - startTime < 1) {\n    // 항목당 1 ms 동안 아무것도 하지 않음으로써 매우 느린 코드를 대리 실행합니다.\n  }\n\n  return (\n    <li className=\"item\">\n      Post #{index + 1}\n    </li>\n  );\n}\n\nexport default PostsTab;\n```\n\n```js src/ContactTab.js\nexport default function ContactTab() {\n  return (\n    <>\n      <p>\n        You can find me online here:\n      </p>\n      <ul>\n        <li>admin@mysite.com</li>\n        <li>+123456789</li>\n      </ul>\n    </>\n  );\n}\n```\n\n```css\nbutton { margin-right: 10px }\nb { display: inline-block; margin-right: 10px; }\n.pending { color: #777; }\n```\n\n</Sandpack>\n\n<Note>\n\nWhen exposing an `action` prop from a component, you should `await` it inside the transition.\n\n이렇게 하면 `action` 콜백이 동기적이든 비동기적이든 상관없이 작동할 수 있으며, `action` 내부의 `await`을 추가적인 `startTransition`으로 감쌀 필요가 없습니다.\n\n</Note>\n\n---\n\n### 대기 상태를 시각적으로 표현하기 {/*displaying-a-pending-visual-state*/}\n\n`useTransition`이 반환하는 `isPending` boolean 값을 사용하여 transition이 진행 중임을 사용자에게 표시할 수 있습니다. 예를 들어 탭 버튼은 특별한 \"pending\" 시각적 상태를 가질 수 있습니다.\n\n```js {4-6}\nfunction TabButton({ action, children, isActive }) {\n  const [isPending, startTransition] = useTransition();\n  // ...\n  if (isPending) {\n    return <b className=\"pending\">{children}</b>;\n  }\n  // ...\n```\n\n이제 탭 버튼 자체가 바로 업데이트되므로 \"Posts\"을 클릭하는 반응이 더 빨라진 것을 확인할 수 있습니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\nimport TabButton from './TabButton.js';\nimport AboutTab from './AboutTab.js';\nimport PostsTab from './PostsTab.js';\nimport ContactTab from './ContactTab.js';\n\nexport default function TabContainer() {\n  const [tab, setTab] = useState('about');\n  return (\n    <>\n      <TabButton\n        isActive={tab === 'about'}\n        action={() => setTab('about')}\n      >\n        About\n      </TabButton>\n      <TabButton\n        isActive={tab === 'posts'}\n        action={() => setTab('posts')}\n      >\n        Posts (slow)\n      </TabButton>\n      <TabButton\n        isActive={tab === 'contact'}\n        action={() => setTab('contact')}\n      >\n        Contact\n      </TabButton>\n      <hr />\n      {tab === 'about' && <AboutTab />}\n      {tab === 'posts' && <PostsTab />}\n      {tab === 'contact' && <ContactTab />}\n    </>\n  );\n}\n```\n\n```js src/TabButton.js active\nimport { useTransition } from 'react';\n\nexport default function TabButton({ action, children, isActive }) {\n  const [isPending, startTransition] = useTransition();\n  if (isActive) {\n    return <b>{children}</b>\n  }\n  if (isPending) {\n    return <b className=\"pending\">{children}</b>;\n  }\n  return (\n    <button onClick={() => {\n      startTransition(async () => {\n        await action();\n      });\n    }}>\n      {children}\n    </button>\n  );\n}\n```\n\n```js src/AboutTab.js\nexport default function AboutTab() {\n  return (\n    <p>Welcome to my profile!</p>\n  );\n}\n```\n\n```js src/PostsTab.js\nimport { memo } from 'react';\n\nconst PostsTab = memo(function PostsTab() {\n  // 한 번 로깅합니다. 실제 속도 저하는 SlowPost 컴포넌트 내부에 있습니다.\n  console.log('[ARTIFICIALLY SLOW] Rendering 500 <SlowPost />');\n\n  let items = [];\n  for (let i = 0; i < 500; i++) {\n    items.push(<SlowPost key={i} index={i} />);\n  }\n  return (\n    <ul className=\"items\">\n      {items}\n    </ul>\n  );\n});\n\nfunction SlowPost({ index }) {\n  let startTime = performance.now();\n  while (performance.now() - startTime < 1) {\n    // 항목당 1 ms 동안 아무것도 하지 않음으로써 매우 느린 코드를 대리 실행합니다.\n  }\n\n  return (\n    <li className=\"item\">\n      Post #{index + 1}\n    </li>\n  );\n}\n\nexport default PostsTab;\n```\n\n```js src/ContactTab.js\nexport default function ContactTab() {\n  return (\n    <>\n      <p>\n        You can find me online here:\n      </p>\n      <ul>\n        <li>admin@mysite.com</li>\n        <li>+123456789</li>\n      </ul>\n    </>\n  );\n}\n```\n\n```css\nbutton { margin-right: 10px }\nb { display: inline-block; margin-right: 10px; }\n.pending { color: #777; }\n```\n\n</Sandpack>\n\n---\n\n### 원치 않는 로딩 표시기 방지 {/*preventing-unwanted-loading-indicators*/}\n\n이 예시에서 `PostsTab` 컴포넌트는 [use](/reference/react/use)를 사용하여 데이터를 가져옵니다. \"Posts\" 탭을 클릭하면 `PostsTab` 컴포넌트가 *suspend* 되어 가장 가까운 로딩 Fallback이 나타납니다.\n\n<Sandpack>\n\n```js\nimport { Suspense, useState } from 'react';\nimport TabButton from './TabButton.js';\nimport AboutTab from './AboutTab.js';\nimport PostsTab from './PostsTab.js';\nimport ContactTab from './ContactTab.js';\n\nexport default function TabContainer() {\n  const [tab, setTab] = useState('about');\n  return (\n    <Suspense fallback={<h1>🌀 Loading...</h1>}>\n      <TabButton\n        isActive={tab === 'about'}\n        action={() => setTab('about')}\n      >\n        About\n      </TabButton>\n      <TabButton\n        isActive={tab === 'posts'}\n        action={() => setTab('posts')}\n      >\n        Posts\n      </TabButton>\n      <TabButton\n        isActive={tab === 'contact'}\n        action={() => setTab('contact')}\n      >\n        Contact\n      </TabButton>\n      <hr />\n      {tab === 'about' && <AboutTab />}\n      {tab === 'posts' && <PostsTab />}\n      {tab === 'contact' && <ContactTab />}\n    </Suspense>\n  );\n}\n```\n\n```js src/TabButton.js\nexport default function TabButton({ action, children, isActive }) {\n  if (isActive) {\n    return <b>{children}</b>\n  }\n  return (\n    <button onClick={() => {\n      action();\n    }}>\n      {children}\n    </button>\n  );\n}\n```\n\n```js src/AboutTab.js hidden\nexport default function AboutTab() {\n  return (\n    <p>Welcome to my profile!</p>\n  );\n}\n```\n\n```js src/PostsTab.js hidden\nimport {use} from 'react';\nimport { fetchData } from './data.js';\n\nfunction PostsTab() {\n  const posts = use(fetchData('/posts'));\n  return (\n    <ul className=\"items\">\n      {posts.map(post =>\n        <Post key={post.id} title={post.title} />\n      )}\n    </ul>\n  );\n}\n\nfunction Post({ title }) {\n  return (\n    <li className=\"item\">\n      {title}\n    </li>\n  );\n}\n\nexport default PostsTab;\n```\n\n```js src/ContactTab.js hidden\nexport default function ContactTab() {\n  return (\n    <>\n      <p>\n        You can find me online here:\n      </p>\n      <ul>\n        <li>admin@mysite.com</li>\n        <li>+123456789</li>\n      </ul>\n    </>\n  );\n}\n```\n\n\n```js src/data.js hidden\n// Note: 데이터 가져오기를 수행하는 방식은 Suspense와 함께\n// 사용하는 프레임워크에 따라 다릅니다.\n// 일반적으로 캐싱 로직은 프래임워크 내부에 있습니다.\n\nlet cache = new Map();\n\nexport function fetchData(url) {\n  if (!cache.has(url)) {\n    cache.set(url, getData(url));\n  }\n  return cache.get(url);\n}\n\nasync function getData(url) {\n  if (url.startsWith('/posts')) {\n    return await getPosts();\n  } else {\n    throw Error('Not implemented');\n  }\n}\n\nasync function getPosts() {\n  // 가짜 딜레이를 추가하여 대기 시간을 눈에 띄게 만듭니다.\n  await new Promise(resolve => {\n    setTimeout(resolve, 1000);\n  });\n  let posts = [];\n  for (let i = 0; i < 500; i++) {\n    posts.push({\n      id: i,\n      title: 'Post #' + (i + 1)\n    });\n  }\n  return posts;\n}\n```\n\n```css\nbutton { margin-right: 10px }\nb { display: inline-block; margin-right: 10px; }\n.pending { color: #777; }\n```\n\n</Sandpack>\n\n로딩 표시기를 표시하기 위해 전체 탭 컨테이너를 숨기면 사용자 경험이 어색해집니다. `useTransition`을 `TabButton`에 추가하면 탭 버튼 내부에 대기 중인 상태를 표시할 수 있습니다.\n\n\n\"Posts\"을 클릭하면 더 이상 전체 탭 컨테이너가 스피너로 바뀌지 않습니다.\n\n<Sandpack>\n\n```js\nimport { Suspense, useState } from 'react';\nimport TabButton from './TabButton.js';\nimport AboutTab from './AboutTab.js';\nimport PostsTab from './PostsTab.js';\nimport ContactTab from './ContactTab.js';\n\nexport default function TabContainer() {\n  const [tab, setTab] = useState('about');\n  return (\n    <Suspense fallback={<h1>🌀 Loading...</h1>}>\n      <TabButton\n        isActive={tab === 'about'}\n        action={() => setTab('about')}\n      >\n        About\n      </TabButton>\n      <TabButton\n        isActive={tab === 'posts'}\n        action={() => setTab('posts')}\n      >\n        Posts\n      </TabButton>\n      <TabButton\n        isActive={tab === 'contact'}\n        action={() => setTab('contact')}\n      >\n        Contact\n      </TabButton>\n      <hr />\n      {tab === 'about' && <AboutTab />}\n      {tab === 'posts' && <PostsTab />}\n      {tab === 'contact' && <ContactTab />}\n    </Suspense>\n  );\n}\n```\n\n```js src/TabButton.js active\nimport { useTransition } from 'react';\n\nexport default function TabButton({ action, children, isActive }) {\n  const [isPending, startTransition] = useTransition();\n  if (isActive) {\n    return <b>{children}</b>\n  }\n  if (isPending) {\n    return <b className=\"pending\">{children}</b>;\n  }\n  return (\n    <button onClick={() => {\n      startTransition(async () => {\n        await action();\n      });\n    }}>\n      {children}\n    </button>\n  );\n}\n```\n\n```js src/AboutTab.js hidden\nexport default function AboutTab() {\n  return (\n    <p>Welcome to my profile!</p>\n  );\n}\n```\n\n```js src/PostsTab.js hidden\nimport {use} from 'react';\nimport { fetchData } from './data.js';\n\nfunction PostsTab() {\n  const posts = use(fetchData('/posts'));\n  return (\n    <ul className=\"items\">\n      {posts.map(post =>\n        <Post key={post.id} title={post.title} />\n      )}\n    </ul>\n  );\n}\n\nfunction Post({ title }) {\n  return (\n    <li className=\"item\">\n      {title}\n    </li>\n  );\n}\n\nexport default PostsTab;\n```\n\n```js src/ContactTab.js hidden\nexport default function ContactTab() {\n  return (\n    <>\n      <p>\n        You can find me online here:\n      </p>\n      <ul>\n        <li>admin@mysite.com</li>\n        <li>+123456789</li>\n      </ul>\n    </>\n  );\n}\n```\n\n\n```js src/data.js hidden\n// Note: 데이터 가져오기를 수행하는 방식은 Suspense와 함께\n// 사용하는 프레임워크에 따라 다릅니다.\n// 일반적으로 캐싱 로직은 프래임워크 내부에 있습니다.\n\nlet cache = new Map();\n\nexport function fetchData(url) {\n  if (!cache.has(url)) {\n    cache.set(url, getData(url));\n  }\n  return cache.get(url);\n}\n\nasync function getData(url) {\n  if (url.startsWith('/posts')) {\n    return await getPosts();\n  } else {\n    throw Error('Not implemented');\n  }\n}\n\nasync function getPosts() {\n  // 가짜 딜레이를 추가하여 대기 시간을 눈에 띄게 만듭니다.\n  await new Promise(resolve => {\n    setTimeout(resolve, 1000);\n  });\n  let posts = [];\n  for (let i = 0; i < 500; i++) {\n    posts.push({\n      id: i,\n      title: 'Post #' + (i + 1)\n    });\n  }\n  return posts;\n}\n```\n\n```css\nbutton { margin-right: 10px }\nb { display: inline-block; margin-right: 10px; }\n.pending { color: #777; }\n```\n\n</Sandpack>\n\n[Suspense에서 Transition 을 사용하는 방법에 대해 자세히 알아보세요.](/reference/react/Suspense#preventing-already-revealed-content-from-hiding)\n\n<Note>\n\nTransition은 *이미 표시된* 콘텐츠(예시: 탭 컨테이너)를 숨기지 않을 만큼만 \"대기\"합니다. 만약 Posts 탭에 [중첩된 `<Suspense>` 경계](/reference/react/Suspense#revealing-nested-content-as-it-loads)가 있는 경우 Transition 은 이를 \"대기\"하지 않습니다.\n\n</Note>\n\n---\n\n### Suspense-enabled 라우터 구축 {/*building-a-suspense-enabled-router*/}\n\nReact 프레임워크나 라우터를 구축하는 경우 페이지 탐색을 Transition 으로 표시하는 것이 좋습니다.\n\n```js {3,6,8}\nfunction Router() {\n  const [page, setPage] = useState('/');\n  const [isPending, startTransition] = useTransition();\n\n  function navigate(url) {\n    startTransition(() => {\n      setPage(url);\n    });\n  }\n  // ...\n```\n\n세 가지 이유로 이 방법을 권장합니다.\n\n- [Transition은 중단할 수 있으므로](#marking-a-state-update-as-a-non-blocking-transition) 사용자는 리렌더링이 완료될 때까지 기다릴 필요 없이 바로 클릭할 수 있습니다.\n- [Transition은 원치 않는 로딩 표시기를 방지하므로](#preventing-unwanted-loading-indicators) 사용자가 탐색 시 갑작스러운 이동을 방지할 수 있습니다.\n- [Transition은 모든 보류 중인 작업을 대기하므로](#perform-non-blocking-updates-with-actions) 사용자는 사이드 이펙트가 완료된 후에 새로운 페이지를 볼 수 있습니다.\n\n다음은 navigation에 Transitions를 사용하는 간단한 라우터 예시입니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { Suspense, useState, useTransition } from 'react';\nimport IndexPage from './IndexPage.js';\nimport ArtistPage from './ArtistPage.js';\nimport Layout from './Layout.js';\n\nexport default function App() {\n  return (\n    <Suspense fallback={<BigSpinner />}>\n      <Router />\n    </Suspense>\n  );\n}\n\nfunction Router() {\n  const [page, setPage] = useState('/');\n  const [isPending, startTransition] = useTransition();\n\n  function navigate(url) {\n    startTransition(() => {\n      setPage(url);\n    });\n  }\n\n  let content;\n  if (page === '/') {\n    content = (\n      <IndexPage navigate={navigate} />\n    );\n  } else if (page === '/the-beatles') {\n    content = (\n      <ArtistPage\n        artist={{\n          id: 'the-beatles',\n          name: 'The Beatles',\n        }}\n      />\n    );\n  }\n  return (\n    <Layout isPending={isPending}>\n      {content}\n    </Layout>\n  );\n}\n\nfunction BigSpinner() {\n  return <h2>🌀 Loading...</h2>;\n}\n```\n\n```js src/Layout.js\nexport default function Layout({ children, isPending }) {\n  return (\n    <div className=\"layout\">\n      <section className=\"header\" style={{\n        opacity: isPending ? 0.7 : 1\n      }}>\n        Music Browser\n      </section>\n      <main>\n        {children}\n      </main>\n    </div>\n  );\n}\n```\n\n```js src/IndexPage.js\nexport default function IndexPage({ navigate }) {\n  return (\n    <button onClick={() => navigate('/the-beatles')}>\n      Open The Beatles artist page\n    </button>\n  );\n}\n```\n\n```js src/ArtistPage.js\nimport { Suspense } from 'react';\nimport Albums from './Albums.js';\nimport Biography from './Biography.js';\nimport Panel from './Panel.js';\n\nexport default function ArtistPage({ artist }) {\n  return (\n    <>\n      <h1>{artist.name}</h1>\n      <Biography artistId={artist.id} />\n      <Suspense fallback={<AlbumsGlimmer />}>\n        <Panel>\n          <Albums artistId={artist.id} />\n        </Panel>\n      </Suspense>\n    </>\n  );\n}\n\nfunction AlbumsGlimmer() {\n  return (\n    <div className=\"glimmer-panel\">\n      <div className=\"glimmer-line\" />\n      <div className=\"glimmer-line\" />\n      <div className=\"glimmer-line\" />\n    </div>\n  );\n}\n```\n\n```js src/Albums.js\nimport {use} from 'react';\nimport { fetchData } from './data.js';\n\nexport default function Albums({ artistId }) {\n  const albums = use(fetchData(`/${artistId}/albums`));\n  return (\n    <ul>\n      {albums.map(album => (\n        <li key={album.id}>\n          {album.title} ({album.year})\n        </li>\n      ))}\n    </ul>\n  );\n}\n```\n\n```js src/Biography.js\nimport {use} from 'react';\nimport { fetchData } from './data.js';\n\nexport default function Biography({ artistId }) {\n  const bio = use(fetchData(`/${artistId}/bio`));\n  return (\n    <section>\n      <p className=\"bio\">{bio}</p>\n    </section>\n  );\n}\n```\n\n```js src/Panel.js\nexport default function Panel({ children }) {\n  return (\n    <section className=\"panel\">\n      {children}\n    </section>\n  );\n}\n```\n\n```js src/data.js hidden\n// Note: 데이터 가져오기를 수행하는 방식은 Suspense와 함께\n// 사용하는 프레임워크에 따라 다릅니다.\n// 일반적으로 캐싱 로직은 프래임워크 내부에 있습니다.\nlet cache = new Map();\n\nexport function fetchData(url) {\n  if (!cache.has(url)) {\n    cache.set(url, getData(url));\n  }\n  return cache.get(url);\n}\n\nasync function getData(url) {\n  if (url === '/the-beatles/albums') {\n    return await getAlbums();\n  } else if (url === '/the-beatles/bio') {\n    return await getBio();\n  } else {\n    throw Error('Not implemented');\n  }\n}\n\nasync function getBio() {\n  // 가짜 딜레이를 추가하여 대기 시간을 눈에 띄게 만듭니다.\n  await new Promise(resolve => {\n    setTimeout(resolve, 500);\n  });\n\n  return `The Beatles were an English rock band,\n    formed in Liverpool in 1960, that comprised\n    John Lennon, Paul McCartney, George Harrison\n    and Ringo Starr.`;\n}\n\nasync function getAlbums() {\n  // 가짜 딜레이를 추가하여 대기 시간을 눈에 띄게 만듭니다.\n  await new Promise(resolve => {\n    setTimeout(resolve, 3000);\n  });\n\n  return [{\n    id: 13,\n    title: 'Let It Be',\n    year: 1970\n  }, {\n    id: 12,\n    title: 'Abbey Road',\n    year: 1969\n  }, {\n    id: 11,\n    title: 'Yellow Submarine',\n    year: 1969\n  }, {\n    id: 10,\n    title: 'The Beatles',\n    year: 1968\n  }, {\n    id: 9,\n    title: 'Magical Mystery Tour',\n    year: 1967\n  }, {\n    id: 8,\n    title: 'Sgt. Pepper\\'s Lonely Hearts Club Band',\n    year: 1967\n  }, {\n    id: 7,\n    title: 'Revolver',\n    year: 1966\n  }, {\n    id: 6,\n    title: 'Rubber Soul',\n    year: 1965\n  }, {\n    id: 5,\n    title: 'Help!',\n    year: 1965\n  }, {\n    id: 4,\n    title: 'Beatles For Sale',\n    year: 1964\n  }, {\n    id: 3,\n    title: 'A Hard Day\\'s Night',\n    year: 1964\n  }, {\n    id: 2,\n    title: 'With The Beatles',\n    year: 1963\n  }, {\n    id: 1,\n    title: 'Please Please Me',\n    year: 1963\n  }];\n}\n```\n\n```css\nmain {\n  min-height: 200px;\n  padding: 10px;\n}\n\n.layout {\n  border: 1px solid black;\n}\n\n.header {\n  background: #222;\n  padding: 10px;\n  text-align: center;\n  color: white;\n}\n\n.bio { font-style: italic; }\n\n.panel {\n  border: 1px solid #aaa;\n  border-radius: 6px;\n  margin-top: 20px;\n  padding: 10px;\n}\n\n.glimmer-panel {\n  border: 1px dashed #aaa;\n  background: linear-gradient(90deg, rgba(221,221,221,1) 0%, rgba(255,255,255,1) 100%);\n  border-radius: 6px;\n  margin-top: 20px;\n  padding: 10px;\n}\n\n.glimmer-line {\n  display: block;\n  width: 60%;\n  height: 20px;\n  margin: 10px;\n  border-radius: 4px;\n  background: #f0f0f0;\n}\n```\n\n</Sandpack>\n\n<Note>\n\n[Suspense-enabled](/reference/react/Suspense) 라우터는 기본적으로 탐색 업데이트를 Transition 으로 래핑할 것으로 예상됩니다.\n\n</Note>\n\n---\n\n### Error boundary로 사용자에게 오류 표시하기 {/*displaying-an-error-to-users-with-error-boundary*/}\n\n`startTransition`에 전달된 함수에서 오류가 발생하면 [error boundary](/reference/react/Component#catching-rendering-errors-with-an-error-boundary)를 사용하여 사용자에게 오류를 표시할 수 있습니다. error boundary를 사용하려면 `useTransition`을 호출하는 컴포넌트를 error boundary로 감싸면 됩니다. `startTransition`에 전달된 함수에서 오류가 발생하면 error boundary의 Fallback이 표시됩니다.\n\n<Sandpack>\n\n```js src/AddCommentContainer.js active\nimport { useTransition } from \"react\";\nimport { ErrorBoundary } from \"react-error-boundary\";\n\nexport function AddCommentContainer() {\n  return (\n    <ErrorBoundary fallback={<p>⚠️Something went wrong</p>}>\n      <AddCommentButton />\n    </ErrorBoundary>\n  );\n}\n\nfunction addComment(comment) {\n  // For demonstration purposes to show Error Boundary\n  if (comment == null) {\n    throw new Error(\"Example Error: An error thrown to trigger error boundary\");\n  }\n}\n\nfunction AddCommentButton() {\n  const [pending, startTransition] = useTransition();\n\n  return (\n    <button\n      disabled={pending}\n      onClick={() => {\n        startTransition(() => {\n          // Intentionally not passing a comment\n          // so error gets thrown\n          addComment();\n        });\n      }}\n    >\n      Add comment\n    </button>\n  );\n}\n```\n\n```js src/App.js hidden\nimport { AddCommentContainer } from \"./AddCommentContainer.js\";\n\nexport default function App() {\n  return <AddCommentContainer />;\n}\n```\n\n```js src/index.js hidden\nimport React, { StrictMode } from 'react';\nimport { createRoot } from 'react-dom/client';\nimport './styles.css';\nimport App from './App';\n\nconst root = createRoot(document.getElementById('root'));\nroot.render(\n  <StrictMode>\n    <App />\n  </StrictMode>\n);\n```\n\n```json package.json hidden\n{\n  \"dependencies\": {\n    \"react\": \"19.0.0-rc-3edc000d-20240926\",\n    \"react-dom\": \"19.0.0-rc-3edc000d-20240926\",\n    \"react-scripts\": \"^5.0.0\",\n    \"react-error-boundary\": \"4.0.3\"\n  },\n  \"main\": \"/index.js\"\n}\n```\n</Sandpack>\n\n---\n\n## Troubleshooting {/*troubleshooting*/}\n\n### Transition에서 입력 업데이트가 작동하지 않습니다 {/*updating-an-input-in-a-transition-doesnt-work*/}\n\n입력을 제어하는 state 변수에는 Transition 을 사용할 수 없습니다.\n\n```js {4,10}\nconst [text, setText] = useState('');\n// ...\nfunction handleChange(e) {\n  // ❌ 제어된 입력 state에 Transition 을 사용할 수 없습니다.\n  startTransition(() => {\n    setText(e.target.value);\n  });\n}\n// ...\nreturn <input value={text} onChange={handleChange} />;\n```\n\n이는 Transition 이 non-blocking이지만, 변경 이벤트에 대한 응답으로 입력을 업데이트하는 것은 동기적으로 이루어져야 하기 때문입니다. 입력에 대한 응답으로 Transition 을 실행하려면 두 가지 옵션이 있습니다.\n\n1. 두 개의 개별 state 변수를 선언할 수 있습니다. 하나는 입력 state(항상 동기적으로 업데이트됨) 용이고 다른 하나는 Transition 시 업데이트할 state입니다. 이를 통해 동기 state를 사용하여 입력을 제어하고 (입력보다 \"지연\"되는) Transition state 변수를 나머지 렌더링 로직에 전달할 수 있습니다.\n2. 또는 state 변수가 하나 있고 실제 값보다 \"지연\"되는 [`useDeferredValue`](/reference/react/useDeferredValue)를 추가할 수 있습니다. 그러면 non-blocking 리렌더링이 새로운 값을 자동으로 \"따라잡기\" 위해 트리거됩니다.\n\n---\n\n### React가 state 업데이트를 transition으로 처리하지 않습니다 {/*react-doesnt-treat-my-state-update-as-a-transition*/}\n\nstate 업데이트를 transition으로 래핑할 때는 `startTransition` 호출 *도중*에 발생해야 합니다.\n\n```js\nstartTransition(() => {\n  // ✅ startTransition 호출 *도중* state 설정\n  setPage('/about');\n});\n```\n\n`startTransition`에 전달하는 함수는 동기식이어야 합니다. You can't mark an update as a Transition like this:\n\n```js\nstartTransition(() => {\n  // ❌ startTransition 호출 *후에* state 설정\n  setTimeout(() => {\n    setPage('/about');\n  }, 1000);\n});\n```\n\n대신 다음과 같이 할 수 있습니다.\n\n```js\nsetTimeout(() => {\n  startTransition(() => {\n    // ✅ startTransition 호출 *도중* state 설정\n    setPage('/about');\n  });\n}, 1000);\n```\n\n---\n\n### React는 `await` 이후의 상태 업데이트를 Transition으로 처리하지 않습니다. {/*react-doesnt-treat-my-state-update-after-await-as-a-transition*/}\n\n`startTransition` 함수 내부에서 `await`를 사용할 경우, `await` 이후에 발생하는 상태 업데이트는 Transition으로 처리되지 않습니다. 각 `await` 이후에 발생하는 상태 업데이트를 별도의 `startTransition` 호출로 감싸야 합니다.\n\n```js\nstartTransition(async () => {\n  await someAsyncFunction();\n  // ❌ await 이후에 startTransition을 사용하지 않음\n  setPage('/about');\n});\n```\n\n하지만 이 방법이 대신 동작합니다.\n\n```js\nstartTransition(async () => {\n  await someAsyncFunction();\n  // ✅ await *이후에* startTransition을 사용\n  startTransition(() => {\n    setPage('/about');\n  });\n});\n```\n\n이는 JavaScript의 한계로 인해 React가 [AsyncContext](https://github.com/tc39/proposal-async-context)의 범위를 잃기 때문입니다. 향후 AsyncContext가 지원되면 이러한 제한 사항은 해결될 것입니다.\n\n---\n\n### 컴포넌트 외부에서 `useTransition`을 호출하고 싶습니다 {/*i-want-to-call-usetransition-from-outside-a-component*/}\n\nHook이기 때문에 컴포넌트 외부에서 `useTransition`을 호출할 수 없습니다. 이 경우 대신 독립형 [`startTransition`](/reference/react/startTransition) 메서드를 사용하세요. 동일한 방식으로 작동하지만 `isPending` 표시기를 제공하지 않습니다.\n\n---\n\n### `startTransition`에 전달한 함수는 즉시 실행됩니다 {/*the-function-i-pass-to-starttransition-executes-immediately*/}\n\n이 코드를 실행하면 1, 2, 3이 출력됩니다.\n\n```js {1,3,6}\nconsole.log(1);\nstartTransition(() => {\n  console.log(2);\n  setPage('/about');\n});\nconsole.log(3);\n```\n\n**1, 2, 3을 출력할 것으로 예상됩니다.** `startTransition`에 전달한 함수는 지연되지 않습니다. 브라우저 `setTimeout`과 달리 나중에 콜백을 실행하지 않습니다. React는 함수를 즉시 실행하지만, *함수가 실행되는 동안* 예약된 모든 상태 업데이트는 Transition 으로 표시됩니다. 아래와 같이 작동한다고 상상하면 됩니다.\n\n```js\n// React 작동 방식의 간소화된 버전\n\nlet isInsideTransition = false;\n\nfunction startTransition(scope) {\n  isInsideTransition = true;\n  scope();\n  isInsideTransition = false;\n}\n\nfunction setState() {\n  if (isInsideTransition) {\n    // ... Transition state 업데이트 예약 ...\n  } else {\n    // ... 긴급 state 업데이트 예약 ...\n  }\n}\n```\n\n### Transitions에서 상태 업데이트가 순서대로 이루어지지 않아요 {/*my-state-updates-in-transitions-are-out-of-order*/}\n\n`startTransition` 내부에서 `await`를 사용하면 상태 업데이트가 순서대로 발생하지 않을 수 있습니다.\n\n이 예시에서 `updateQuantity` 함수는 카트에 있는 품목의 수량을 업데이트하기 위해 서버에 요청하는 시뮬레이션을 수행합니다. 또한 네트워크 요청에서 발생할 수 있는 경쟁 상태(race condition)를 재현하도록 *다른 요청들이 이전 요청보다 늦게 완료되도록 인위적으로 응답 순서를 조정합니다.*\n\n수량을 한 번 업데이트한 후, 빠르게 여러 번 업데이트를 시도해 보세요. 그러면 잘못된 총합이 표시될 수 있습니다\n\n<Sandpack>\n\n```json package.json hidden\n{\n  \"dependencies\": {\n    \"react\": \"beta\",\n    \"react-dom\": \"beta\"\n  },\n  \"scripts\": {\n    \"start\": \"react-scripts start\",\n    \"build\": \"react-scripts build\",\n    \"test\": \"react-scripts test --env=jsdom\",\n    \"eject\": \"react-scripts eject\"\n  }\n}\n```\n\n```js src/App.js\nimport { useState, useTransition } from \"react\";\nimport { updateQuantity } from \"./api\";\nimport Item from \"./Item\";\nimport Total from \"./Total\";\n\nexport default function App({}) {\n  const [quantity, setQuantity] = useState(1);\n  const [isPending, startTransition] = useTransition();\n  // 실제 수량을 별도의 state에 저장하여 불일치를 표시합니다.\n  const [clientQuantity, setClientQuantity] = useState(1);\n\n  const updateQuantityAction = newQuantity => {\n    setClientQuantity(newQuantity);\n\n    // 트랜지션의 대기 상태에 접근하기 위해 startTransition을 다시 감쌉니다.\n    startTransition(async () => {\n      const savedQuantity = await updateQuantity(newQuantity);\n      startTransition(() => {\n        setQuantity(savedQuantity);\n      });\n    });\n  };\n\n  return (\n    <div>\n      <h1>Checkout</h1>\n      <Item action={updateQuantityAction}/>\n      <hr />\n      <Total clientQuantity={clientQuantity} savedQuantity={quantity} isPending={isPending} />\n    </div>\n  );\n}\n\n```\n\n```js src/Item.js\nimport {startTransition} from 'react';\n\nexport default function Item({action}) {\n  function handleChange(e) {\n    // 수량을 업데이트하는 Action입니다.\n    startTransition(async () => {\n      await action(e.target.value);\n    });\n  }\n  return (\n    <div className=\"item\">\n      <span>Eras Tour Tickets</span>\n      <label htmlFor=\"name\">Quantity: </label>\n      <input\n        type=\"number\"\n        onChange={handleChange}\n        defaultValue={1}\n        min={1}\n      />\n    </div>\n  )\n}\n```\n\n```js src/Total.js\nconst intl = new Intl.NumberFormat(\"en-US\", {\n  style: \"currency\",\n  currency: \"USD\"\n});\n\nexport default function Total({ clientQuantity, savedQuantity, isPending }) {\n  return (\n    <div className=\"total\">\n      <span>Total:</span>\n      <div>\n        <div>\n          {isPending\n            ? \"🌀 Updating...\"\n            : `${intl.format(savedQuantity * 9999)}`}\n        </div>\n        <div className=\"error\">\n          {!isPending &&\n            clientQuantity !== savedQuantity &&\n            `Wrong total, expected: ${intl.format(clientQuantity * 9999)}`}\n        </div>\n      </div>\n    </div>\n  );\n}\n```\n\n```js src/api.js\nlet firstRequest = true;\nexport async function updateQuantity(newName) {\n  return new Promise((resolve, reject) => {\n    if (firstRequest === true) {\n      firstRequest = false;\n      setTimeout(() => {\n        firstRequest = true;\n        resolve(newName);\n        // 매번 다른 요청이 더 느리게 반환되도록 시뮬레이션합니다.\n      }, 1000);\n    } else {\n      setTimeout(() => {\n        resolve(newName);\n      }, 50);\n    }\n  });\n}\n```\n\n```css\n.item {\n  display: flex;\n  align-items: center;\n  justify-content: start;\n}\n\n.item label {\n  flex: 1;\n  text-align: right;\n}\n\n.item input {\n  margin-left: 4px;\n  width: 60px;\n  padding: 4px;\n}\n\n.total {\n  height: 50px;\n  line-height: 25px;\n  display: flex;\n  align-content: center;\n  justify-content: space-between;\n}\n\n.total div {\n  display: flex;\n  flex-direction: column;\n  align-items: flex-end;\n}\n\n.error {\n  color: red;\n}\n```\n\n</Sandpack>\n\n여러 번 클릭하면 먼저 보낸 요청이 나중에 보낸 요청보다 늦게 처리될 수 있습니다. 이런 경우 React는 현재 의도한 순서를 알 수 있는 방법이 없습니다. 이는 업데이트가 비동기적으로 예약되고, React가 비동기 경계를 거쳐 순서에 대한 컨텍스트를 잃기 때문입니다.\n\n이것은 예상된 동작입니다. Transition 내에서의 액션은 실행 순서를 보장하지 않기 때문입니다. 일반적인 사용 사례에서는 React가 [`useActionState`](/reference/react/useActionState)나 [`<form>` actions](/reference/react-dom/components/form)과 같은 더 높은 수준의 추상화를 제공하여 순서를 처리해 줍니다. 고급 사용 사례에서는 이 문제를 처리하기 위해 자체적인 큐잉(queuing) 및 취소 로직을 구현해야 합니다.\n\nExample of `useActionState` handling execution order:\n\n<Sandpack>\n\n```json package.json hidden\n{\n  \"dependencies\": {\n    \"react\": \"beta\",\n    \"react-dom\": \"beta\"\n  },\n  \"scripts\": {\n    \"start\": \"react-scripts start\",\n    \"build\": \"react-scripts build\",\n    \"test\": \"react-scripts test --env=jsdom\",\n    \"eject\": \"react-scripts eject\"\n  }\n}\n```\n\n```js src/App.js\nimport { useState, useActionState } from \"react\";\nimport { updateQuantity } from \"./api\";\nimport Item from \"./Item\";\nimport Total from \"./Total\";\n\nexport default function App({}) {\n  // Store the actual quantity in separate state to show the mismatch.\n  const [clientQuantity, setClientQuantity] = useState(1);\n  const [quantity, updateQuantityAction, isPending] = useActionState(\n    async (prevState, payload) => {\n      setClientQuantity(payload);\n      const savedQuantity = await updateQuantity(payload);\n      return savedQuantity; // Return the new quantity to update the state\n    },\n    1 // Initial quantity\n  );\n\n  return (\n    <div>\n      <h1>Checkout</h1>\n      <Item action={updateQuantityAction}/>\n      <hr />\n      <Total clientQuantity={clientQuantity} savedQuantity={quantity} isPending={isPending} />\n    </div>\n  );\n}\n\n```\n\n```js src/Item.js\nimport {startTransition} from 'react';\n\nexport default function Item({action}) {\n  function handleChange(e) {\n    // Update the quantity in an Action.\n    startTransition(() => {\n      action(e.target.value);\n    });\n  }\n  return (\n    <div className=\"item\">\n      <span>Eras Tour Tickets</span>\n      <label htmlFor=\"name\">Quantity: </label>\n      <input\n        type=\"number\"\n        onChange={handleChange}\n        defaultValue={1}\n        min={1}\n      />\n    </div>\n  )\n}\n```\n\n```js src/Total.js\nconst intl = new Intl.NumberFormat(\"en-US\", {\n  style: \"currency\",\n  currency: \"USD\"\n});\n\nexport default function Total({ clientQuantity, savedQuantity, isPending }) {\n  return (\n    <div className=\"total\">\n      <span>Total:</span>\n      <div>\n        <div>\n          {isPending\n            ? \"🌀 Updating...\"\n            : `${intl.format(savedQuantity * 9999)}`}\n        </div>\n        <div className=\"error\">\n          {!isPending &&\n            clientQuantity !== savedQuantity &&\n            `Wrong total, expected: ${intl.format(clientQuantity * 9999)}`}\n        </div>\n      </div>\n    </div>\n  );\n}\n```\n\n```js src/api.js\nlet firstRequest = true;\nexport async function updateQuantity(newName) {\n  return new Promise((resolve, reject) => {\n    if (firstRequest === true) {\n      firstRequest = false;\n      setTimeout(() => {\n        firstRequest = true;\n        resolve(newName);\n        // Simulate every other request being slower\n      }, 1000);\n    } else {\n      setTimeout(() => {\n        resolve(newName);\n      }, 50);\n    }\n  });\n}\n```\n\n```css\n.item {\n  display: flex;\n  align-items: center;\n  justify-content: start;\n}\n\n.item label {\n  flex: 1;\n  text-align: right;\n}\n\n.item input {\n  margin-left: 4px;\n  width: 60px;\n  padding: 4px;\n}\n\n.total {\n  height: 50px;\n  line-height: 25px;\n  display: flex;\n  align-content: center;\n  justify-content: space-between;\n}\n\n.total div {\n  display: flex;\n  flex-direction: column;\n  align-items: flex-end;\n}\n\n.error {\n  color: red;\n}\n```\n\n</Sandpack>\n"
  },
  {
    "path": "src/content/reference/react-compiler/compilationMode.md",
    "content": "---\ntitle: compilationMode\n---\n\n<Intro>\n\n`compilationMode` 옵션은 React 컴파일러가 어떤 함수를 컴파일할지 선택하는 방식을 제어합니다.\n\n</Intro>\n\n```js\n{\n  compilationMode: 'infer' // or 'annotation', 'syntax', 'all'\n}\n```\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `compilationMode` {/*compilationmode*/}\n\nReact 컴파일러가 최적화할 함수를 결정하는 전략을 제어합니다.\n\n#### 타입 {/*type*/}\n\n```\n'infer' | 'syntax' | 'annotation' | 'all'\n```\n\n#### 기본값 {/*default-value*/}\n\n`'infer'`\n\n#### 옵션 {/*options*/}\n\n- **`'infer'`** (기본값): 컴파일러가 지능형 휴리스틱을 사용하여 React 컴포넌트와 Hook을 식별합니다.\n  - `\"use memo\"` 지시어로 명시적으로 표시된 함수.\n  - 컴포넌트(PascalCase) 또는 Hook(`use` 접두사)처럼 이름이 지어진 함수이면서 JSX를 생성하거나 다른 Hook을 호출하는 함수.\n\n- **`'annotation'`**: `\"use memo\"` 지시어로 명시적으로 표시된 함수만 컴파일합니다. 점진적 도입에 이상적입니다.\n\n- **`'syntax'`**: Flow의 [Component](https://flow.org/en/docs/react/component-syntax/) 및 [Hook](https://flow.org/en/docs/react/hook-syntax/) 문법을 사용하는 컴포넌트와 Hook만 컴파일합니다.\n\n- **`'all'`**: 모든 최상위 함수를 컴파일합니다. React가 아닌 함수도 컴파일할 수 있으므로 권장하지 않습니다.\n\n#### 주의 사항 {/*caveats*/}\n\n- `'infer'` 모드는 함수가 React 명명 규칙을 따라야 감지할 수 있습니다.\n- `'all'` 모드를 사용하면 유틸리티 함수까지 컴파일하여 성능에 부정적인 영향을 미칠 수 있습니다.\n- `'syntax'` 모드는 Flow가 필요하며 TypeScript와는 작동하지 않습니다.\n- 모드와 관계없이 `\"use no memo\"` 지시어가 있는 함수는 항상 건너뜁니다.\n\n---\n\n## 사용법 {/*usage*/}\n\n### 기본 추론 모드 {/*default-inference-mode*/}\n\n기본 `'infer'` 모드는 React의 규칙을 따르는 대부분의 코드베이스에서 잘 작동합니다.\n\n```js\n{\n  compilationMode: 'infer'\n}\n```\n\n이 모드에서는 다음 함수들이 컴파일됩니다.\n\n```js\n// ✅ 컴파일됨: 컴포넌트처럼 이름이 지어졌고 JSX를 반환함\nfunction Button(props) {\n  return <button>{props.label}</button>;\n}\n\n// ✅ 컴파일됨: Hook처럼 이름이 지어졌고 Hook을 호출함\nfunction useCounter() {\n  const [count, setCount] = useState(0);\n  return [count, setCount];\n}\n\n// ✅ 컴파일됨: 명시적인 지시어\nfunction expensiveCalculation(data) {\n  \"use memo\";\n  return data.reduce(/* ... */);\n}\n\n// ❌ 컴파일되지 않음: 컴포넌트/Hook 패턴이 아님\nfunction calculateTotal(items) {\n  return items.reduce((a, b) => a + b, 0);\n}\n```\n\n### 어노테이션 모드를 사용한 점진적 도입 {/*incremental-adoption*/}\n\n점진적 마이그레이션을 위해 `'annotation'` 모드를 사용하여 표시된 함수만 컴파일합니다.\n\n```js\n{\n  compilationMode: 'annotation'\n}\n```\n\n그런 다음 컴파일할 함수를 명시적으로 표시합니다.\n\n```js\n// 이 함수만 컴파일됩니다\nfunction ExpensiveList(props) {\n  \"use memo\";\n  return (\n    <ul>\n      {props.items.map(item => (\n        <li key={item.id}>{item.name}</li>\n      ))}\n    </ul>\n  );\n}\n\n// 지시어가 없으면 컴파일되지 않습니다\nfunction NormalComponent(props) {\n  return <div>{props.content}</div>;\n}\n```\n\n### Flow 문법 모드 사용하기 {/*flow-syntax-mode*/}\n\n코드베이스가 TypeScript 대신 Flow를 사용하는 경우입니다.\n\n```js\n{\n  compilationMode: 'syntax'\n}\n```\n\n그런 다음 Flow의 컴포넌트 문법을 사용합니다.\n\n```js\n// 컴파일됨: Flow 컴포넌트 문법\ncomponent Button(label: string) {\n  return <button>{label}</button>;\n}\n\n// 컴파일됨: Flow Hook 문법\nhook useCounter(initial: number) {\n  const [count, setCount] = useState(initial);\n  return [count, setCount];\n}\n\n// 컴파일되지 않음: 일반 함수 문법\nfunction helper(data) {\n  return process(data);\n}\n```\n\n### 특정 함수 제외하기 {/*opting-out*/}\n\n컴파일 모드와 관계없이 `\"use no memo\"`를 사용하여 컴파일을 건너뛸 수 있습니다.\n\n```js\nfunction ComponentWithSideEffects() {\n  \"use no memo\"; // 컴파일 방지\n\n  // 이 컴포넌트는 메모이제이션되어서는 안 되는 사이드 이펙트가 있습니다\n  logToAnalytics('component_rendered');\n\n  return <div>Content</div>;\n}\n```\n\n---\n\n## 문제 해결 {/*troubleshooting*/}\n\n### `'infer'` 모드에서 컴포넌트가 컴파일되지 않는 경우 {/*component-not-compiled-infer*/}\n\n`'infer'` 모드에서는 컴포넌트가 React의 규칙을 따르는지 확인하세요.\n\n```js\n// ❌ 컴파일되지 않음: 소문자 이름\nfunction button(props) {\n  return <button>{props.label}</button>;\n}\n\n// ✅ 컴파일됨: PascalCase 이름\nfunction Button(props) {\n  return <button>{props.label}</button>;\n}\n\n// ❌ 컴파일되지 않음: JSX를 생성하거나 Hook을 호출하지 않음\nfunction useData() {\n  return window.localStorage.getItem('data');\n}\n\n// ✅ 컴파일됨: Hook을 호출함\nfunction useData() {\n  const [data] = useState(() => window.localStorage.getItem('data'));\n  return data;\n}\n```\n"
  },
  {
    "path": "src/content/reference/react-compiler/compiling-libraries.md",
    "content": "---\ntitle: 라이브러리 컴파일\n---\n\n<Intro>\n이 가이드는 라이브러리 작성자가 React 컴파일러를 사용하여 최적화된 라이브러리 코드를 사용자에게 제공하는 방법을 설명합니다.\n</Intro>\n\n<InlineToc />\n\n## 컴파일된 코드를 배포해야 하는 이유 {/*why-ship-compiled-code*/}\n\n라이브러리 작성자는 npm에 배포하기 전에 라이브러리 코드를 컴파일할 수 있습니다. 이는 여러 가지 이점을 제공합니다.\n\n- **모든 사용자를 위한 성능 향상** - 라이브러리 사용자가 아직 React 컴파일러를 사용하지 않더라도 최적화된 코드를 얻습니다.\n- **사용자에게 설정이 필요 없음** - 최적화가 바로 작동합니다.\n- **일관된 동작** - 모든 사용자가 빌드 설정에 관계없이 동일한 최적화된 버전을 얻습니다.\n\n## 컴파일 설정하기 {/*setting-up-compilation*/}\n\n라이브러리의 빌드 프로세스에 React 컴파일러를 추가하세요.\n\n<TerminalBlock>\nnpm install -D babel-plugin-react-compiler@latest\n</TerminalBlock>\n\n라이브러리를 컴파일하도록 빌드 도구를 설정하세요. Babel을 사용하는 예시입니다.\n\n```js\n// babel.config.js\nmodule.exports = {\n  plugins: [\n    'babel-plugin-react-compiler',\n  ],\n  // ... other config\n};\n```\n\n## 하위 호환성 {/*backwards-compatibility*/}\n\n라이브러리가 React 19 미만 버전을 지원하는 경우 추가 설정이 필요합니다.\n\n### 1. 런타임 패키지 설치하기 {/*install-runtime-package*/}\n\n`react-compiler-runtime`을 직접 의존성<sup>`dependencies`</sup>으로 설치하는 것을 권장합니다.\n\n<TerminalBlock>\nnpm install react-compiler-runtime@latest\n</TerminalBlock>\n\n```json\n{\n  \"dependencies\": {\n    \"react-compiler-runtime\": \"^1.0.0\"\n  },\n  \"peerDependencies\": {\n    \"react\": \"^17.0.0 || ^18.0.0 || ^19.0.0\"\n  }\n}\n```\n\n### 2. `target` 버전 설정하기 {/*configure-target-version*/}\n\n라이브러리가 지원하는 최소 React 버전을 설정하세요.\n\n```js\n{\n  target: '17', // 지원하는 최소 React 버전\n}\n```\n\n## 테스트 전략 {/*testing-strategy*/}\n\n호환성을 보장하기 위해 컴파일 유무에 관계없이 라이브러리를 테스트하세요. 컴파일된 코드에 대해 기존 테스트를 실행하고 컴파일러를 우회하는 별도의 테스트 설정도 만드세요. 이렇게 하면 컴파일 과정에서 발생할 수 있는 문제를 발견하고 모든 시나리오에서 라이브러리가 올바르게 작동하는지 확인할 수 있습니다.\n\n## 문제 해결 {/*troubleshooting*/}\n\n### 라이브러리가 이전 React 버전에서 작동하지 않는 경우 {/*library-doesnt-work-with-older-react-versions*/}\n\n컴파일된 라이브러리가 React 17 또는 18에서 오류를 발생시키는 경우입니다.\n\n1. `react-compiler-runtime`이 의존성으로 설치되어 있는지 확인하세요.\n2. `target` 설정이 지원하는 최소 React 버전과 일치하는지 확인하세요.\n3. 런타임 패키지가 배포된 번들에 포함되어 있는지 확인하세요.\n\n### 다른 Babel 플러그인과의 컴파일 충돌 {/*compilation-conflicts-with-other-babel-plugins*/}\n\n일부 Babel 플러그인은 React 컴파일러와 충돌할 수 있습니다.\n\n1. `babel-plugin-react-compiler`를 플러그인 목록의 앞쪽에 배치하세요.\n2. 다른 플러그인에서 충돌하는 최적화를 비활성화하세요.\n3. 빌드 출력을 철저히 테스트하세요.\n\n### 런타임 모듈을 찾을 수 없는 경우 {/*runtime-module-not-found*/}\n\n사용자가 \"Cannot find module 'react-compiler-runtime'\" 오류를 보는 경우입니다.\n\n1. 런타임이 `devDependencies`가 아닌 `dependencies`에 나열되어 있는지 확인하세요.\n2. 번들러가 출력에 런타임을 포함하는지 확인하세요.\n3. 패키지가 라이브러리와 함께 npm에 배포되었는지 확인하세요.\n\n## 다음 단계 {/*next-steps*/}\n\n- 컴파일된 코드를 위한 [디버깅 기법](/learn/react-compiler/debugging)에 대해 알아보세요.\n- 모든 컴파일러 옵션을 위한 [설정 옵션](/reference/react-compiler/configuration)을 확인하세요.\n- 선택적 최적화를 위한 [컴파일 모드](/reference/react-compiler/compilationMode)를 살펴보세요.\n"
  },
  {
    "path": "src/content/reference/react-compiler/configuration.md",
    "content": "---\ntitle: 설정\n---\n\n<Intro>\n\n이 페이지에서는 React 컴파일러에서 사용할 수 있는 모든 설정 옵션을 나열합니다.\n\n</Intro>\n\n<Note>\n\n대부분의 앱에서는 기본 옵션이 기본적으로 잘 작동합니다. 특별한 필요성이 있는 경우에 이러한 고급 옵션을 사용할 수 있습니다.\n\n</Note>\n\n```js\n// babel.config.js\nmodule.exports = {\n  plugins: [\n    [\n      'babel-plugin-react-compiler', {\n        // compiler options\n      }\n    ]\n  ]\n};\n```\n\n---\n\n## 컴파일 제어 {/*compilation-control*/}\n\n이 옵션들은 컴파일러가 *무엇*을 최적화하고, *어떻게* 컴포넌트와 Hook을 컴파일 대상으로 선택할지를 제어합니다.\n\n* [`compilationMode`](/reference/react-compiler/compilationMode)는 컴파일할 함수를 선택하는 전략을 제어합니다. (예: 모든 함수, 어노테이션된 함수만, 또는 컴파일러의 자동 감지)\n\n```js\n{\n  compilationMode: 'annotation' // \"use memo\"가 명시된 함수만 컴파일합니다.\n}\n```\n\n---\n\n## 버전 호환성 {/*version-compatibility*/}\n\nReact 버전 설정은 컴파일러가 현재 사용 중인 React 버전과 호환되는 코드를 생성하도록 합니다.\n\n[`target`](/reference/react-compiler/target)은 현재 사용 중인 React 버전(17, 18, 또는 19)을 지정합니다.\n\n```js\n// React 18을 사용하는 프로젝트의 경우\n{\n  target: '18' // react-compiler-runtime 패키지가 필요합니다.\n}\n```\n\n---\n\n## 에러 처리 {/*error-handling*/}\n\n이 옵션들은 [React의 규칙](/reference/rules)을 따르지 않는 코드에 대해 컴파일러가 어떻게 대응하는지를 제어합니다.\n\n[`panicThreshold`](/reference/react-compiler/panicThreshold)는 빌드를 실패로 처리할지, 문제가 있는 컴포넌트를 건너뛸지를 결정합니다.\n\n```js\n// 프로덕션 환경에 권장\n{\n  panicThreshold: 'none' // 빌드를 실패시키는 대신 오류가 있는 컴포넌트를 건너뜁니다.\n}\n```\n\n---\n\n## 디버깅 {/*debugging*/}\n\n로깅 및 분석 옵션은 컴파일러의 동작을 이해하는 데 도움을 줍니다.\n\n[`logger`](/reference/react-compiler/logger)는 컴파일 이벤트에 대한 커스텀 로깅을 제공합니다.\n\n```js\n{\n  logger: {\n    logEvent(filename, event) {\n      if (event.kind === 'CompileSuccess') {\n        console.log('Compiled:', filename);\n      }\n    }\n  }\n}\n```\n\n---\n\n## 기능 플래그 {/*feature-flags*/}\n\n조건부 컴파일을 사용하면 최적화된 코드가 언제 사용될지를 제어할 수 있습니다.\n\n[`gating`](/reference/react-compiler/gating)은 A/B 테스트나 점진적 배포를 위한 런타임 기능 플래그를 활성화합니다.\n\n```js\n{\n  gating: {\n    source: 'my-feature-flags',\n    importSpecifierName: 'isCompilerEnabled'\n  }\n}\n```\n\n---\n\n## 공통 설정 패턴 {/*common-patterns*/}\n\n### 기본 설정 {/*default-configuration*/}\n\n대부분의 React 19 애플리케이션에서는 별도의 설정 없이도 컴파일러가 정상적으로 동작합니다.\n\n```js\n// babel.config.js\nmodule.exports = {\n  plugins: [\n    'babel-plugin-react-compiler'\n  ]\n};\n```\n\n### React 17/18 프로젝트 {/*react-17-18*/}\n\nReact 17/18 프로젝트에서는 런타임 패키지와 `target` 설정이 필요합니다.\n\n```bash\nnpm install react-compiler-runtime@latest\n```\n\n```js\n{\n  target: '18' // or '17'\n}\n```\n\n### 점진적 적용 {/*incremental-adoption*/}\n\n특정 디렉토리부터 시작해 점진적으로 확장할 수 있습니다.\n\n```js\n{\n  compilationMode: 'annotation' // \"use memo\"가 명시된 함수만 컴파일합니다.\n}\n```\n\n"
  },
  {
    "path": "src/content/reference/react-compiler/directives/use-memo.md",
    "content": "---\ntitle: \"use memo\"\ntitleForTitleTag: \"'use memo' 지시어\"\n---\n\n<Intro>\n\n`\"use memo\"`는 특정 함수를 React 컴파일러의 최적화 대상으로 표시합니다.\n\n</Intro>\n\n<Note>\n\n대부분의 경우 `\"use memo\"`는 필요하지 않습니다. 이 지시어는 최적화 대상을 명시적으로 표시해야 하는 `annotation` 모드에서 주로 사용합니다. `infer` 모드에서는 컴파일러가 이름 규칙(컴포넌트는 PascalCase, Hook은 `use` 접두사)을 기반으로 컴포넌트와 Hook을 자동으로 감지합니다. `infer` 모드에서 컴포넌트나 Hook이 컴파일되지 않는다면, `\"use memo\"`로 강제로 컴파일하기 보다는 이름 규칙을 올바르게 수정해야 합니다.\n\n</Note>\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `\"use memo\"` {/*use-memo*/}\n\n특정 함수를 React 컴파일러 최적화 대상으로 표시하려면 함수의 시작 부분에 `\"use memo\"`를 추가하세요.\n\n```js {2}\nfunction MyComponent() {\n  \"use memo\";\n  // ...\n}\n```\n\n함수에 `\"use memo\"`가 포함되어 있으면, React 컴파일러는 빌드 시간에 이를 분석하고 최적화합니다. 컴파일러는 불필요한 재계산과 리렌더링을 방지하기 위해 값과 컴포넌트를 자동으로 메모이제이션합니다.\n\n#### 주의 사항 {/*caveats*/}\n\n* `\"use memo\"`는 함수 본문의 최상단에 있어야 하며, import나 다른 코드보다 앞에 있어야 합니다. (주석은 괜찮습니다.)\n* 지시어는 백틱이 아니라 큰따옴표 또는 작은따옴표로 작성해야 합니다.\n* 지시어는 `\"use memo\"`와 정확히 일치해야 합니다.\n* 함수의 첫 번째 지시어만 처리되며, 그 이후의 지시어는 무시됩니다.\n* 지시어의 동작 방식은 [`compilationMode`](/reference/react-compiler/compilationMode) 설정에 따라 달라집니다.\n\n### `\"use memo\"`가 함수를 최적화 대상으로 표시하는 방법 {/*how-use-memo-marks*/}\n\nReact 컴파일러를 사용하는 React 앱에서는, 빌드 시점에 함수를 분석하여 최적화가 가능한지 판단합니다. 기본적으로 컴파일러는 어떤 컴포넌트를 메모이제이션할지 자동으로 추론하지만, 이는 설정한 [`compilationMode`](/reference/react-compiler/compilationMode)에 따라 달라질 수 있습니다.\n\n`\"use memo\"`는 기본 동작을 재정의하여 함수를 명시적으로 최적화 대상으로 표시합니다.\n\n* `annotation` 모드: `\"use memo\"`를 선언한 함수만 최적화합니다.\n* `infer` 모드: 컴파일러가 휴리스틱을 사용해 판단하지만, `\"use memo\"`를 사용하면 최적화를 강제합니다.\n* `all` 모드: 기본적으로 모든 코드가 최적화되므로, `\"use memo\"`는 불필요합니다.\n\n이 지시어는 코드베이스에서 최적화된 코드와 최적화되지 않은 코드 사이에 명확한 경계를 만들어, 컴파일 과정을 세밀하게 제어할 수 있게 합니다.\n\n### `\"use memo\"`를 사용해야 하는 경우 {/*when-to-use*/}\n\n다음과 같은 경우에 `\"use memo\"` 사용을 고려할 수 있습니다.\n\n#### `annotation` 모드를 사용하는 경우 {/*annotation-mode-use*/}\n`compilationMode: 'annotation'`에서는, 최적화하려는 모든 함수에 이 지시어를 반드시 선언해야 합니다.\n\n```js\n// ✅ 이 컴포넌트는 최적화됩니다\nfunction OptimizedList() {\n  \"use memo\";\n  // ...\n}\n\n// ❌ 이 컴포넌트는 최적화되지 않습니다\nfunction SimpleWrapper() {\n  // ...\n}\n```\n\n#### React 컴파일러를 점진적으로 도입하는 경우 {/*gradual-adoption*/}\n먼저 `annotation` 모드로 시작한 뒤, 안정적인 컴포넌트부터 선택적으로 최적화하세요.\n\n```js\n// 리프(Leaf) 컴포넌트부터 최적화 시작\nfunction Button({ onClick, children }) {\n  \"use memo\";\n  // ...\n}\n\n// 동작을 검증하면서 점차 상위 컴포넌트로 확장\nfunction ButtonGroup({ buttons }) {\n  \"use memo\";\n  // ...\n}\n```\n\n---\n\n## 사용법 {/*usage*/}\n\n### 다양한 컴파일 모드에서 사용하기 {/*compilation-modes*/}\n\n`\"use memo\"`의 동작은 컴파일러 설정에 따라 달라집니다.\n\n```js\n// babel.config.js\nmodule.exports = {\n  plugins: [\n    ['babel-plugin-react-compiler', {\n      compilationMode: 'annotation' // 또는 'infer', 'all'\n    }]\n  ]\n};\n```\n\n#### Annotation 모드 {/*annotation-mode-example*/}\n```js\n// ✅ \"use memo\"로 최적화됨\nfunction ProductCard({ product }) {\n  \"use memo\";\n  // ...\n}\n\n// ❌ 최적화되지 않음 (지시어 없음)\nfunction ProductList({ products }) {\n  // ...\n}\n```\n\n#### Infer 모드 (기본값) {/*infer-mode-example*/}\n```js\n// 컴포넌트 이름 규칙을 따르므로 자동으로 메모이제이션됨\nfunction ComplexDashboard({ data }) {\n  // ...\n}\n\n// 건너뜀: 컴포넌트 이름 규칙을 따르지 않음\nfunction simpleDisplay({ text }) {\n  // ...\n}\n```\n\n`infer` 모드에서는 컴파일러가 이름 규칙(컴포넌트는 PascalCase, Hook은 `use` 접두사)을 기반으로 컴포넌트와 Hook을 자동으로 감지합니다. `infer` 모드에서 컴포넌트나 Hook이 컴파일되지 않는다면, `\"use memo\"`를 사용해 강제로 컴파일하기 보다는 이름 규칙을 수정하는 것을 권장합니다.\n\n---\n\n## 문제 해결 {/*troubleshooting*/}\n\n### 최적화 여부 확인하기 {/*verifying-optimization*/}\n\n컴포넌트가 최적화되었는지 확인하려면\n\n1. 빌드 결과물에서 컴파일된 코드를 확인하세요.\n2. React DevTools에서 Memo ✨ 배지를 확인하세요.\n\n### 참고 {/*see-also*/}\n\n* [`\"use no memo\"`](/reference/react-compiler/directives/use-no-memo) - 컴파일 대상에서 제외\n* [`compilationMode`](/reference/react-compiler/compilationMode) - 컴파일 동작 설정\n* [React Compiler](/learn/react-compiler) - 시작 가이드\n"
  },
  {
    "path": "src/content/reference/react-compiler/directives/use-no-memo.md",
    "content": "---\ntitle: \"use no memo\"\ntitleForTitleTag: \"'use no memo' directive\"\n---\n\n<Intro>\n\n`\"use no memo\"`는 React 컴파일러가 특정 함수를 최적화하지 않도록 합니다.\n\n</Intro>\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `\"use no memo\"` {/*use-no-memo*/}\n\nReact 컴파일러 최적화를 방지하려면 함수의 시작 부분에 `\"use no memo\"`를 추가하세요.\n\n```js {2}\nfunction MyComponent() {\n  \"use no memo\";\n  // ...\n}\n```\n\n함수에 `\"use no memo\"`가 포함되어 있으면 React 컴파일러는 최적화 중에 해당 함수를 완전히 건너뜁니다. 이것은 디버깅할 때나, 컴파일러와 제대로 동작하지 않는 코드를 다룰 때 임시 탈출구로 유용합니다.\n\n#### 주의 사항 {/*caveats*/}\n\n* `\"use no memo\"`는 import나 다른 코드보다 먼저 함수 본문의 맨 처음에 있어야 합니다. (주석은 괜찮습니다.)\n* 지시어는 백틱이 아닌 큰따옴표나 작은따옴표로 작성해야 합니다.\n* 지시어는 `\"use no memo\"` 또는 별칭인 `\"use no forget\"`과 정확히 일치해야 합니다.\n* 이 지시어는 모든 컴파일 모드와 다른 지시어보다 우선합니다.\n* 이것은 영구적인 해결책이 아닌 임시 디버깅 도구로 사용하기 위한 것입니다.\n\n### `\"use no memo\"`가 최적화를 제외하는 방법 {/*how-use-no-memo-opts-out*/}\n\nReact 컴파일러는 빌드 시간에 코드를 분석하여 최적화를 적용합니다. `\"use no memo\"`는 컴파일러에게 함수를 완전히 건너뛰도록 알려주는 명시적인 경계를 만듭니다.\n\n이 지시어는 다른 모든 설정보다 우선합니다.\n* `all` 모드에서: 전역 설정에도 불구하고 함수를 건너뜁니다.\n* `infer` 모드에서: 휴리스틱이 최적화할 경우에도 함수를 건너뜁니다.\n\n컴파일러는 React 컴파일러가 활성화되지 않은 것처럼 이러한 함수를 처리하여 작성된 그대로 유지합니다.\n\n### `\"use no memo\"`를 사용해야 하는 경우 {/*when-to-use*/}\n\n`\"use no memo\"`는 드물게 그리고 임시로 사용해야 합니다. 일반적인 시나리오는 다음과 같습니다.\n\n#### 컴파일러 문제 디버깅 {/*debugging-compiler*/}\n컴파일러가 문제를 일으키는 것으로 의심되면 문제를 분리하기 위해 일시적으로 최적화를 비활성화하세요.\n\n```js\nfunction ProblematicComponent({ data }) {\n  \"use no memo\"; // TODO: 이슈 #123 수정 후 제거\n\n  // 정적으로 감지되지 않은 React 규칙 위반\n  // ...\n}\n```\n\n#### 서드 파티 라이브러리 통합 {/*third-party*/}\n컴파일러와 호환되지 않을 수 있는 라이브러리와 통합할 때 사용합니다.\n\n```js\nfunction ThirdPartyWrapper() {\n  \"use no memo\";\n\n  useThirdPartyHook(); // 컴파일러가 잘못 최적화할 수 있는 사이드 이펙트가 있음\n  // ...\n}\n```\n\n---\n\n## 사용법 {/*usage*/}\n\n`\"use no memo\"` 지시어는 React 컴파일러가 해당 함수를 최적화하지 않도록 함수 본문의 시작 부분에 배치합니다.\n\n```js\nfunction MyComponent() {\n  \"use no memo\";\n  // 함수 본문\n}\n```\n\n지시어는 해당 모듈의 모든 함수에 영향을 미치도록 파일 상단에 배치할 수도 있습니다.\n\n```js\n\"use no memo\";\n\n// 이 파일의 모든 함수는 컴파일러에 의해 건너뛰어집니다\n```\n\n함수 수준의 `\"use no memo\"`는 모듈 수준의 지시어를 재정의합니다.\n\n---\n\n## 문제 해결 {/*troubleshooting*/}\n\n### 지시어가 컴파일을 방지하지 않는 경우 {/*not-preventing*/}\n\n`\"use no memo\"`가 작동하지 않는 경우입니다.\n\n```js\n// ❌ 잘못된 예 - 코드 뒤에 지시어\nfunction Component() {\n  const data = getData();\n  \"use no memo\"; // 너무 늦음!\n}\n\n// ✅ 올바른 예 - 지시어가 먼저\nfunction Component() {\n  \"use no memo\";\n  const data = getData();\n}\n```\n\n다음도 확인하세요.\n* 철자 - `\"use no memo\"`와 정확히 일치해야 합니다.\n* 따옴표 - 백틱이 아닌 작은따옴표나 큰따옴표를 사용해야 합니다.\n\n### 모범 사례 {/*best-practices*/}\n\n최적화를 비활성화하는 **이유를 항상 문서화하세요**.\n\n```js\n// ✅ 좋은 예 - 명확한 설명과 추적\nfunction DataProcessor() {\n  \"use no memo\"; // TODO: React 규칙 위반 수정 후 제거\n  // ...\n}\n\n// ❌ 나쁜 예 - 설명 없음\nfunction Mystery() {\n  \"use no memo\";\n  // ...\n}\n```\n\n### 참고 {/*see-also*/}\n\n* [`\"use memo\"`](/reference/react-compiler/directives/use-memo) - 컴파일에 포함하기\n* [React 컴파일러](/learn/react-compiler) - 시작 가이드\n"
  },
  {
    "path": "src/content/reference/react-compiler/directives.md",
    "content": "---\ntitle: 지시어\n---\n\n<Intro>\nReact 컴파일러 지시어는 특정 함수에 대한 컴파일 적용 여부를 제어하는 특수 문자열 리터럴입니다.\n</Intro>\n\n```js\nfunction MyComponent() {\n  \"use memo\"; // 이 컴포넌트를 컴파일 대상으로 설정합니다.\n  return <div>{/* ... */}</div>;\n}\n```\n\n<InlineToc />\n\n---\n\n## 개요 {/*overview*/}\n\nReact 컴파일러 지시어는 컴파일러가 최적화할 함수를 세밀하게 제어할 수 있도록 합니다. 함수 본문의 시작 부분이나 모듈의 최상단에 배치되는 문자열 리터럴입니다.\n\n### 사용 가능한 지시어 {/*available-directives*/}\n\n* **[`\"use memo\"`](/reference/react-compiler/directives/use-memo)** - 함수를 컴파일 대상으로 선택합니다.\n* **[`\"use no memo\"`](/reference/react-compiler/directives/use-no-memo)** - 함수를 컴파일 대상에서 제외합니다.\n\n### 빠른 비교 {/*quick-comparison*/}\n\n| 지시어 | 목적 | 사용 시점 |\n|-----------|---------|-------------|\n| [`\"use memo\"`](/reference/react-compiler/directives/use-memo) | 컴파일 강제 | `annotation` 모드를 사용하거나 `infer` 모드의 휴리스틱을 재정의<sup>Override</sup>할 때 |\n| [`\"use no memo\"`](/reference/react-compiler/directives/use-no-memo) | 컴파일 제외 | 이슈를 디버깅하거나 호환되지 않는 코드를 다룰 때 |\n\n---\n\n## 사용법 {/*usage*/}\n\n### 함수 수준 지시어 {/*function-level*/}\n\n함수의 시작 부분에 지시어를 선언하여 컴파일을 제어합니다.\n\n```js\n// 컴파일 대상으로 설정\nfunction OptimizedComponent() {\n  \"use memo\";\n  return <div>This will be optimized</div>;\n}\n\n// 컴파일 대상에서 제외\nfunction UnoptimizedComponent() {\n  \"use no memo\";\n  return <div>This won't be optimized</div>;\n}\n```\n\n### 모듈 수준 지시어 {/*module-level*/}\n\n파일의 최상단에 선언하여 해당 모듈의 모든 함수에 적용합니다.\n\n```js\n// 파일의 최상단에 선언\n\"use memo\";\n\n// 이 파일의 모든 함수가 컴파일됩니다\nfunction Component1() {\n  return <div>Compiled</div>;\n}\n\nfunction Component2() {\n  return <div>Also compiled</div>;\n}\n\n// 함수 수준에서 재정의 가능\nfunction Component3() {\n  \"use no memo\"; // 모듈 수준에서 재정의됩니다\n  return <div>Not compiled</div>;\n}\n```\n\n### 컴파일 모드와의 상호작용 {/*compilation-modes*/}\n\n지시어는 [`compilationMode`](/reference/react-compiler/compilationMode)에 따라 다르게 동작합니다.\n\n* **`annotation` 모드**: `\"use memo\"`가 선언된 함수만 컴파일됩니다.\n* **`infer` 모드**: 컴파일러가 컴파일할 대상을 결정하며 지시어는 이 결정을 재정의<sup>Override</sup>합니다.\n* **`all` 모드**: 모든 것이 컴파일되며 `\"use no memo\"`로 특정 함수를 제외할 수 있습니다.\n\n---\n\n## 모범 사례 {/*best-practices*/}\n\n### 지시어는 신중하게 사용하세요 {/*use-sparingly*/}\n\n지시어는 탈출구<sup>Escape Hatch</sup>입니다. 컴파일러는 프로젝트 수준에서 설정하는 것을 권장합니다.\n\n```js\n// ✅ Good - 프로젝트 전역 설정\n{\n  plugins: [\n    ['babel-plugin-react-compiler', {\n      compilationMode: 'infer'\n    }]\n  ]\n}\n\n// ⚠️ 필요할 때마다 지시어 사용\nfunction SpecialCase() {\n  \"use no memo\"; // 왜 필요한지 문서화하세요\n  // ...\n}\n```\n\n### 지시어 사용 이유를 문서화하세요 {/*document-usage*/}\n\n지시어를 사용하는 이유를 항상 명확히 설명하세요.\n\n```js\n// ✅ Good - 명확한 설명\nfunction DataGrid() {\n  \"use no memo\"; // TODO: 동적 row height 이슈 해결 후 제거 (JIRA-123)\n  // 복잡한 그리드 구현\n}\n\n// ❌ Bad - 설명 없음\nfunction Mystery() {\n  \"use no memo\";\n  // ...\n}\n```\n\n### 제거 계획을 세우세요 {/*plan-removal*/}\n\n컴파일 제외 지시어는 임시로 사용해야 합니다.\n\n1. TODO 주석과 함께 지시어를 추가합니다.\n2. 추적용 이슈를 생성합니다.\n3. 근본적인 문제를 해결합니다.\n4. 지시어를 제거합니다.\n\n```js\nfunction TemporaryWorkaround() {\n  \"use no memo\"; // TODO: ThirdPartyLib를 v2.0으로 업그레이드한 후 제거\n  return <ThirdPartyComponent />;\n}\n```\n\n---\n\n## 일반적인 패턴 {/*common-patterns*/}\n\n### 점진적 도입 {/*gradual-adoption*/}\n\n대규모 코드베이스에서 React 컴파일러를 도입할 때 다음과 같은 방식이 일반적입니다.\n\n```js\n// annotation 모드로 시작\n{\n  compilationMode: 'annotation'\n}\n\n// 안정적인 컴포넌트를 컴파일 대상으로 설정\nfunction StableComponent() {\n  \"use memo\";\n  // 충분히 테스트된 컴포넌트\n}\n\n// 나중에 infer 모드로 전환하고 문제가 있는 컴포넌트는 제외\nfunction ProblematicComponent() {\n  \"use no memo\"; // 제거 전에 이슈를 해결하세요\n  // ...\n}\n```\n\n\n---\n\n## 문제 해결 {/*troubleshooting*/}\n\n지시어와 관련된 구체적인 문제는 다음 문제 해결 섹션을 참고하세요.\n\n* [`\"use memo\"` 문제 해결](/reference/react-compiler/directives/use-memo#troubleshooting)\n* [`\"use no memo\"` 문제 해결](/reference/react-compiler/directives/use-no-memo#troubleshooting)\n\n### 자주 발생하는 이슈 {/*common-issues*/}\n\n1. **지시어가 무시됨**: 위치(파일 또는 함수의 첫 줄인지)와 철자를 확인하세요.\n2. **컴파일이 계속됨**: `ignoreUseNoForget` 설정을 확인하세요.\n3. **모듈 수준 지시어가 작동하지 않음**: 모든 import 문보다 앞에 있는지 확인하세요.\n\n---\n\n## 참고 {/*see-also*/}\n\n* [`compilationMode`](/reference/react-compiler/compilationMode) - 컴파일러가 최적화 대상을 선택하는 방식을 설정합니다.\n* [`Configuration`](/reference/react-compiler/configuration) - 전체 컴파일러 설정 옵션.\n* [React Compiler documentation](/learn/react-compiler) - 시작 가이드.\n"
  },
  {
    "path": "src/content/reference/react-compiler/gating.md",
    "content": "---\ntitle: gating\n---\n\n<Intro>\n\n`gating` 옵션은 조건부 컴파일을 활성화하여 런타임에 최적화된 코드가 사용되는 시점을 제어할 수 있게 합니다.\n\n</Intro>\n\n```js\n{\n  gating: {\n    source: 'my-feature-flags',\n    importSpecifierName: 'shouldUseCompiler'\n  }\n}\n```\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `gating` {/*gating*/}\n\n컴파일된 함수에 대한 런타임 기능 플래그 Gating을 설정합니다.\n\n#### 타입 {/*type*/}\n\n```\n{\n  source: string;\n  importSpecifierName: string;\n} | null\n```\n\n#### 기본값 {/*default-value*/}\n\n`null`\n\n#### 프로퍼티 {/*properties*/}\n\n- **`source`**: 기능 플래그를 가져올 모듈 경로.\n- **`importSpecifierName`**: `import`해서 사용하려는 내보낸<sup>Export</sup> 함수의 이름.\n\n#### 주의 사항 {/*caveats*/}\n\n- Gating 함수는 반드시 boolean을 반환해야 합니다.\n- 컴파일된 버전과 원본 버전 모두 번들 크기를 증가시킵니다.\n- `import`는 컴파일된 함수가 있는 모든 파일에 추가됩니다.\n\n---\n\n## 사용법 {/*usage*/}\n\n### 기본 기능 플래그 설정 {/*basic-setup*/}\n\n1. 기능 플래그 모듈을 생성합니다.\n\n```js\n// src/utils/feature-flags.js\nexport function shouldUseCompiler() {\n  // your logic here\n  return getFeatureFlag('react-compiler-enabled');\n}\n```\n\n2. 컴파일러를 설정합니다.\n\n```js\n{\n  gating: {\n    source: './src/utils/feature-flags',\n    importSpecifierName: 'shouldUseCompiler'\n  }\n}\n```\n\n3. 컴파일러가 게이트된 코드를 생성합니다.\n\n```js\n// Input\nfunction Button(props) {\n  return <button>{props.label}</button>;\n}\n\n// Output (simplified)\nimport { shouldUseCompiler } from './src/utils/feature-flags';\n\nconst Button = shouldUseCompiler()\n  ? function Button_optimized(props) { /* compiled version */ }\n  : function Button_original(props) { /* original version */ };\n```\n\nGating 함수는 모듈 시간에 한 번만 평가되므로, JS 번들이 파싱되고 평가된 후에는 선택된 컴포넌트가 브라우저 세션이 끝날 때까지 정적으로 유지됩니다.\n\n---\n\n## 문제 해결 {/*troubleshooting*/}\n\n### 기능 플래그가 작동하지 않는 경우 {/*flag-not-working*/}\n\n플래그 모듈이 올바른 함수를 내보내는지 확인하세요.\n\n```js\n// ❌ 잘못된 예: Default export\nexport default function shouldUseCompiler() {\n  return true;\n}\n\n// ✅ 올바른 예: importSpecifierName과 일치하는 Named export\nexport function shouldUseCompiler() {\n  return true;\n}\n```\n\n### Import 오류 {/*import-errors*/}\n\n`source`의 경로가 올바른지 확인하세요.\n\n```js\n// ❌ 잘못된 예: `babel.config.js`에 상대적인 경로\n{\n  source: './src/flags',\n  importSpecifierName: 'flag'\n}\n\n// ✅ 올바른 예: 모듈 해석 경로\n{\n  source: '@myapp/feature-flags',\n  importSpecifierName: 'flag'\n}\n\n// ✅ 올바른 예: 프로젝트 루트로부터의 절대 경로\n{\n  source: './src/utils/flags',\n  importSpecifierName: 'flag'\n}\n```\n"
  },
  {
    "path": "src/content/reference/react-compiler/logger.md",
    "content": "---\ntitle: logger\n---\n\n<Intro>\n\n`logger` 옵션은 컴파일 중 React 컴파일러 이벤트에 대한 커스텀 로깅을 제공합니다.\n\n</Intro>\n\n```js\n{\n  logger: {\n    logEvent(filename, event) {\n      console.log(`[Compiler] ${event.kind}: ${filename}`);\n    }\n  }\n}\n```\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `logger` {/*logger*/}\n\n컴파일러 동작을 추적하고 문제를 디버깅하기 위한 커스텀 로깅을 설정합니다.\n\n#### 타입 {/*type*/}\n\n```\n{\n  logEvent: (filename: string | null, event: LoggerEvent) => void;\n} | null\n```\n\n#### 기본값 {/*default-value*/}\n\n`null`\n\n#### 메서드 {/*methods*/}\n\n- **`logEvent`**: 각 컴파일러 이벤트에 대해 파일명, 이벤트 세부 정보와 함께 호출됩니다.\n\n#### 이벤트 타입 {/*event-types*/}\n\n- **`CompileSuccess`**: 함수가 성공적으로 컴파일됨.\n- **`CompileError`**: 오류로 인해 함수가 건너뛰어짐.\n- **`CompileDiagnostic`**: 치명적이지 않은 진단 정보.\n- **`CompileSkip`**: 다른 이유로 함수가 건너뛰어짐.\n- **`PipelineError`**: 예기치 않은 컴파일 오류.\n- **`Timing`**: 성능 타이밍 정보.\n\n#### 주의 사항 {/*caveats*/}\n\n- 이벤트 구조는 버전 간에 변경될 수 있습니다.\n- 대규모 코드베이스는 많은 로그 항목을 생성합니다.\n\n---\n\n## 사용법 {/*usage*/}\n\n### 기본 로깅 {/*basic-logging*/}\n\n컴파일 성공과 실패를 추적합니다.\n\n```js\n{\n  logger: {\n    logEvent(filename, event) {\n      switch (event.kind) {\n        case 'CompileSuccess': {\n          console.log(`✅ Compiled: ${filename}`);\n          break;\n        }\n        case 'CompileError': {\n          console.log(`❌ Skipped: ${filename}`);\n          break;\n        }\n        default: {}\n      }\n    }\n  }\n}\n```\n\n### 상세 오류 로깅 {/*detailed-error-logging*/}\n\n컴파일 실패에 대한 구체적인 정보를 확인합니다.\n\n```js\n{\n  logger: {\n    logEvent(filename, event) {\n      if (event.kind === 'CompileError') {\n        console.error(`\\nCompilation failed: ${filename}`);\n        console.error(`Reason: ${event.detail.reason}`);\n\n        if (event.detail.description) {\n          console.error(`Details: ${event.detail.description}`);\n        }\n\n        if (event.detail.loc) {\n          const { line, column } = event.detail.loc.start;\n          console.error(`Location: Line ${line}, Column ${column}`);\n        }\n\n        if (event.detail.suggestions) {\n          console.error('Suggestions:', event.detail.suggestions);\n        }\n      }\n    }\n  }\n}\n```\n\n"
  },
  {
    "path": "src/content/reference/react-compiler/panicThreshold.md",
    "content": "---\ntitle: panicThreshold\n---\n\n<Intro>\n\n`panicThreshold` 옵션은 React 컴파일러가 컴파일 중 오류를 처리하는 방식을 제어합니다.\n\n</Intro>\n\n```js\n{\n  panicThreshold: 'none' // Recommended\n}\n```\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `panicThreshold` {/*panicthreshold*/}\n\n컴파일 오류가 빌드를 실패시켜야 하는지 아니면 최적화를 건너뛰어야 하는지를 결정합니다.\n\n#### 타입 {/*type*/}\n\n```\n'none' | 'critical_errors' | 'all_errors'\n```\n\n#### 기본값 {/*default-value*/}\n\n`'none'`\n\n#### 옵션 {/*options*/}\n\n- **`'none'`** (기본값, 권장): 컴파일할 수 없는 컴포넌트를 건너뛰고 빌드를 계속 진행합니다.\n- **`'critical_errors'`**: 치명적인 컴파일러 오류에서만 빌드를 실패시킵니다.\n- **`'all_errors'`**: 모든 컴파일러 진단에서 빌드를 실패시킵니다.\n\n#### 주의 사항 {/*caveats*/}\n\n- 프로덕션 빌드에서는 항상 `'none'`을 사용해야 합니다.\n- 빌드 실패는 애플리케이션이 빌드되지 않도록 합니다.\n- 컴파일러는 `'none'`을 사용하면 문제가 있는 코드를 자동으로 감지하고 건너뜁니다.\n- 더 높은 임계값은 개발 중 디버깅에만 유용합니다.\n\n---\n\n## 사용법 {/*usage*/}\n\n### 프로덕션 설정 (권장) {/*production-configuration*/}\n\n프로덕션 빌드에서는 항상 `'none'`을 사용하세요. 이것이 기본값입니다.\n\n```js\n{\n  panicThreshold: 'none'\n}\n```\n\n이렇게 하면 다음을 보장합니다.\n- 컴파일러 문제로 인해 빌드가 실패하지 않습니다.\n- 최적화할 수 없는 컴포넌트도 정상적으로 실행됩니다.\n- 최대한 많은 컴포넌트가 최적화됩니다.\n- 안정적인 프로덕션 배포가 가능합니다.\n\n### 개발 중 디버깅 {/*development-debugging*/}\n\n문제를 찾기 위해 일시적으로 더 엄격한 임계값을 사용합니다.\n\n```js\nconst isDevelopment = process.env.NODE_ENV === 'development';\n\n{\n  panicThreshold: isDevelopment ? 'critical_errors' : 'none',\n  logger: {\n    logEvent(filename, event) {\n      if (isDevelopment && event.kind === 'CompileError') {\n        // ...\n      }\n    }\n  }\n}\n```\n"
  },
  {
    "path": "src/content/reference/react-compiler/target.md",
    "content": "---\ntitle: target\n---\n\n<Intro>\n\n`target` 옵션은 컴파일러가 어떤 React 버전을 위한 코드를 생성해야 하는지 지정합니다.\n\n</Intro>\n\n```js\n{\n  target: '19' // or '18', '17'\n}\n```\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `target` {/*target*/}\n\n컴파일된 출력의 React 버전 호환성을 설정합니다.\n\n#### 타입 {/*type*/}\n\n```\n'17' | '18' | '19'\n```\n\n#### 기본값 {/*default-value*/}\n\n`'19'`\n\n#### 유효한 값 {/*valid-values*/}\n\n- **`'19'`**: React 19를 대상으로 합니다 (기본값). 추가 런타임이 필요하지 않습니다.\n- **`'18'`**: React 18을 대상으로 합니다. `react-compiler-runtime` 패키지가 필요합니다.\n- **`'17'`**: React 17을 대상으로 합니다. `react-compiler-runtime` 패키지가 필요합니다.\n\n#### 주의 사항 {/*caveats*/}\n\n- 숫자가 아닌 문자열 값을 사용하세요. (예: `17`이 아닌 `'17'`)\n- 패치 버전은 포함하지 마세요. (예: `'18.2.0'`이 아닌 `'18'`을 사용)\n- React 19는 컴파일러 런타임 API가 내장되어 있습니다.\n- React 17과 18은 `react-compiler-runtime@latest` 설치가 필요합니다.\n\n---\n\n## 사용법 {/*usage*/}\n\n### React 19 대상으로 하기 (기본값) {/*targeting-react-19*/}\n\nReact 19의 경우 특별한 설정이 필요하지 않습니다.\n\n```js\n{\n  // defaults to target: '19'\n}\n```\n\n컴파일러는 React 19의 내장 런타임 API를 사용합니다.\n\n```js\n// 컴파일된 출력은 React 19의 네이티브 API를 사용합니다.\nimport { c as _c } from 'react/compiler-runtime';\n```\n\n### React 17 또는 18 대상으로 하기 {/*targeting-react-17-or-18*/}\n\nReact 17과 React 18 프로젝트의 경우 두 단계가 필요합니다.\n\n1. 런타임 패키지를 설치합니다.\n\n```bash\nnpm install react-compiler-runtime@latest\n```\n\n2. `target`을 설정합니다.\n\n```js\n// For React 18\n{\n  target: '18'\n}\n\n// For React 17\n{\n  target: '17'\n}\n```\n\n컴파일러는 두 버전 모두에 대해 폴리필 런타임을 사용합니다.\n\n```js\n// 컴파일된 출력은 폴리필을 사용합니다.\nimport { c as _c } from 'react-compiler-runtime';\n```\n\n---\n\n## 문제 해결 {/*troubleshooting*/}\n\n### 컴파일러 런타임 누락에 관한 런타임 오류 {/*missing-runtime*/}\n\n\"Cannot find module 'react/compiler-runtime'\"와 같은 오류가 표시되는 경우에는 다음과 같이 합니다.\n\n1. React 버전을 확인합니다.\n   ```bash\n   npm why react\n   ```\n\n2. React 17 또는 18을 사용하는 경우 런타임을 설치합니다.\n   ```bash\n   npm install react-compiler-runtime@latest\n   ```\n\n3. `target`이 React 버전과 일치하는지 확인합니다.\n   ```js\n   {\n     target: '18' // React 메이저 버전과 일치해야 합니다\n   }\n   ```\n\n### 런타임 패키지가 작동하지 않는 경우 {/*runtime-not-working*/}\n\n런타임 패키지가 다음 조건을 만족하는지 확인하세요.\n\n1. 프로젝트에 설치되어 있어야 합니다. (전역이 아닌)\n2. `package.json`의 `dependencies`에 나열되어 있어야 합니다.\n3. 올바른 버전이어야 합니다. (`@latest` 태그)\n4. `devDependencies`에 있으면 안 됩니다. (런타임에 필요합니다)\n\n### 컴파일된 출력 확인하기 {/*checking-output*/}\n\n올바른 런타임이 사용되고 있는지 확인하려면 서로 다른 import를 주목하세요. (내장의 경우 `react/compiler-runtime`, 17/18용 독립 패키지의 경우 `react-compiler-runtime`)\n\n```js\n// React 19용 (내장 런타임)\nimport { c } from 'react/compiler-runtime'\n//                      ^\n\n// React 17/18용 (폴리필 런타임)\nimport { c } from 'react-compiler-runtime'\n//                      ^\n```\n"
  },
  {
    "path": "src/content/reference/react-dom/client/createRoot.md",
    "content": "---\ntitle: createRoot\n---\n\n<Intro>\n\n`createRoot`로 브라우저 DOM 노드 안에 React 컴포넌트를 표시하는 루트를 생성할 수 있습니다.\n\n```js\nconst root = createRoot(domNode, options?)\n```\n\n</Intro>\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `createRoot(domNode, options?)` {/*createroot*/}\n\n`createRoot`를 호출하면 브라우저 DOM 엘리먼트 안에 콘텐츠를 표시할 수 있는 React 루트를 생성합니다.\n```js\nimport { createRoot } from 'react-dom/client';\n\nconst domNode = document.getElementById('root');\nconst root = createRoot(domNode);\n```\n\nReact는 `domNode`에 대한 루트를 생성하고 그 안에 있는 DOM을 관리합니다. 루트를 생성한 후에는 [`root.render`](#root-render)를 호출해 그 안에 React 컴포넌트를 표시해야 합니다.\n\n```js\nroot.render(<App />);\n```\n\n온전히 React만으로 작성된 앱에서는 일반적으로 루트 컴포넌트에 대한 `createRoot` 호출이 하나만 있습니다. 페이지의 일부에 React를 \"뿌려서\" 사용하는 페이지의 경우에는 루트를 필요로 하는 만큼 작성할 수 있습니다.\n\n[아래 예시를 참고하세요.](#usage)\n\n#### 매개변수 {/*parameters*/}\n\n* `domNode`: [DOM 엘리먼트](https://developer.mozilla.org/en-US/docs/Web/API/Element). React는 DOM 엘리먼트에 대한 루트를 생성하고 렌더링된 React 콘텐츠를 표시하는 `render`와 같은 함수를 루트에서 호출할 수 있도록 합니다.\n\n* **optional** `options`: React 루트에 대한 옵션을 가진 객체입니다.\n  * **optional** `onCaughtError`: React가 Error Boundary에서 오류를 잡을 때 호출되는 콜백. Error Boundary에서 잡은 `error`와 `componentStack`을 포함하는 `errorInfo` 객체와 함께 호출됩니다.\n  * **optional** `onUncaughtError`: 오류가 Error Boundary에 의해 잡히지 않을 때 호출되는 콜백. 오류가 발생한 `error`와 `componentStack`을 포함하는 `errorInfo` 객체와 함께 호출됩니다.\n  * **optional** `onRecoverableError`: React가 오류로부터 자동으로 복구될 때 호출되는 콜백. React가 던지는 `error`와 `componentStack`을 포함하는 `errorInfo` 객체와 함께 호출됩니다. 복구 가능한 오류는 원본 오류 원인을 `error.cause`로 포함할 수 있습니다.\n  * **optional** `identifierPrefix`: React가 [`useId`](/reference/react/useId)에 의해 생성된 ID에 사용하는 문자열 접두사. 같은 페이지에서 여러개의 루트를 사용할 때 충돌을 피하는 데 유용합니다.\n\n#### 반환값 {/*returns*/}\n\n`createRoot`는 [`render`](#root-render)와 [`unmount`](#root-unmount) 두 가지 메서드를 포함한 객체를 반환합니다.\n\n#### 주의 사항 {/*caveats*/}\n* 앱이 서버에서 렌더링 되는 경우 `createRoot()`는 사용할 수 없습니다. 대신 [`hydrateRoot()`](/reference/react-dom/client/hydrateRoot)를 사용하세요.\n* 앱에서 `createRoot` 호출이 단 한번만 있을 가능성이 높습니다. 프레임워크를 사용하는 경우 프레임워크가 이 호출을 대신 수행할 수도 있습니다.\n* 컴포넌트의 자식이 아닌 DOM 트리의 다른 부분(예: 모달 또는 툴팁)에 JSX 조각을 렌더링하려는 경우, `createRoot` 대신 [`createPortal`](/reference/react-dom/createPortal)을 사용하세요.\n\n---\n\n### `root.render(reactNode)` {/*root-render*/}\n\n`root.render`를 호출하여 [JSX](/learn/writing-markup-with-jsx) 조각(\"React 노드\")을 React 루트의 브라우저 DOM 노드에 표시합니다.\n\n```js\nroot.render(<App />);\n```\n\nReact는 `root`에 `<App />`을 표시하고 그 안에 있는 DOM을 관리합니다.\n\n[아래 예시를 참고하세요.](#usage)\n\n#### 매개변수 {/*root-render-parameters*/}\n\n* `reactNode`: 표시하려는 *React 노드*. 일반적으로 `<App />`과 같은 JSX 조각이 되지만, [`createElement()`](/reference/react/createElement)로 작성한 React 엘리먼트, 문자열, 숫자, `null`, `undefined` 등을 전달할 수도 있습니다.\n\n\n#### 반환값 {/*root-render-returns*/}\n\n`root.render`는 `undefined`를 반환합니다.\n\n#### 주의 사항 {/*root-render-caveats*/}\n\n* `root.render`를 처음 호출하면 React는 React 컴포넌트를 렌더링하기 전에 React 루트 내부의 모든 기존 HTML 콘텐츠를 지웁니다.\n\n* 서버에서 또는 빌드 중에 React에 의해 생성된 HTML이 루트의 DOM 노드에 포함된 경우, 대신 이벤트 핸들러를 기존 HTML에 첨부하는 [`hydrateRoot()`](/reference/react-dom/client/hydrateRoot)를 사용하세요.\n\n* 동일한 루트에서 `render`를 두 번 이상 호출하면, React는 필요에 따라 DOM을 업데이트하여 사용자가 전달한 최신 JSX를 반영합니다. React는 이전에 렌더링 된 트리와 [\"비교\"](/learn/preserving-and-resetting-state)해서 재사용할 수 있는 부분과 다시 만들어야 하는 부분을 결정합니다. 동일한 루트에서 `render`를 다시 호출하는 것은 루트 컴포넌트에서 [`set` 함수](/reference/react/useState#setstate)를 호출하는 것과 비슷합니다. React는 불필요한 DOM 업데이트를 피합니다.\n\n* 렌더링이 시작된 이후에는 동기적으로 실행되지만, `root.render(...)` 자체는 비동기적입니다. 즉, `root.render()` 이후에 작성된 코드가 해당 렌더링의 `useLayoutEffect`나 `useEffect`보다 먼저 실행될 수 있습니다. 일반적인 상황에서는 이러한 동작도 문제 없이 잘 작동하며, 대부분 수정이 필요하지 않습니다. 다만, Effect의 실행 순서가 중요한 경우에는 [`flushSync`](/reference/react-dom/flushSync)로 `root.render(...)` 호출을 감싸면 초기 렌더링이 완전히 동기적으로 실행되도록 보장할 수 있습니다.\n\n  ```js\n  const root = createRoot(document.getElementById('root'));\n  root.render(<App />);\n  // 🚩 HTML에는 아직 렌더링된 <App /> 내용이 포함되지 않습니다.\n  console.log(document.body.innerHTML);\n  ```\n\n---\n\n### `root.unmount()` {/*root-unmount*/}\n\n`root.unmount`를 호출하면 React 루트 내부에서 렌더링된 트리를 삭제합니다.\n\n```js\nroot.unmount();\n```\n\n온전히 React만으로 작성된 앱에는 일반적으로 `root.unmount`에 대한 호출이 없습니다.\n\n이 함수는 주로 React 루트의 DOM 노드(또는 그 조상 노드)가 다른 코드에 의해 DOM에서 제거될 수 있는 경우에 유용합니다. 예를 들어 DOM에서 비활성 탭을 제거하는 jQuery 탭 패널을 상상해 보세요. 탭이 제거되면 그 안에 있는 모든 것(내부의 React 루트를 포함)이 DOM에서 제거됩니다. 이 경우 `root.unmount`를 호출하여 제거된 루트의 콘텐츠 관리를 \"중지\"하도록 React에 지시해야 합니다. 그렇지 않으면 제거된 루트 내부의 컴포넌트는 구독과 같은 전역 리소스를 정리하고 확보하는 법을 모르는 채로 있게 됩니다.\n\n`root.unmount`를 호출하면 루트에 있는 모든 컴포넌트가 마운트 해제되고, 트리상의 이벤트 핸들러나 State가 제거되며, 루트 DOM 노드에서 React가 \"분리\"됩니다.\n\n\n#### 매개변수 {/*root-unmount-parameters*/}\n\n`root.unmount`는 매개변수를 받지 않습니다.\n\n\n#### 반환값 {/*root-unmount-returns*/}\n\n`root.unmount` returns `undefined`.\n\n#### 주의 사항 {/*root-unmount-caveats*/}\n\n* `root.unmount`를 호출하면 트리의 모든 컴포넌트가 마운트 해제되고 루트 DOM 노드에서 React가 \"분리\"됩니다.\n\n* `root.unmount`를 한 번 호출한 후에는 같은 루트에서 `root.render`를 다시 호출할 수 없습니다. 마운트 해제된 루트에서 `root.render`를 호출하려고 하면 \"마운트 해제된 루트를 업데이트할 수 없습니다.<sup>Cannot update an unmounted root</sup>\" 오류가 발생합니다. 그러나 해당 노드의 이전 루트가 마운트 해제된 후 동일한 DOM 노드에 새로운 루트를 만들 수는 있습니다.\n\n---\n\n## 사용법 {/*usage*/}\n\n### 온전히 React만으로 작성된 앱 렌더링하기 {/*rendering-an-app-fully-built-with-react*/}\n\n앱이 온전히 React만으로 작성된 경우, 전체 앱에 대해 단일 루트를 생성하세요.\n\n```js [[1, 3, \"document.getElementById('root')\"], [2, 4, \"<App />\"]]\nimport { createRoot } from 'react-dom/client';\n\nconst root = createRoot(document.getElementById('root'));\nroot.render(<App />);\n```\n\n일반적으로 이 코드는 시작할 때 한 번만 실행하면 됩니다.\n\n1. HTML에 정의된 <CodeStep step={1}>브라우저 DOM 노드</CodeStep>를 찾으세요.\n2. 앱 내부에 <CodeStep step={2}>React 컴포넌트</CodeStep>를 표시하세요.\n\n<Sandpack>\n\n```html public/index.html\n<!DOCTYPE html>\n<html>\n  <head><title>My app</title></head>\n  <body>\n    <!-- 이것은은 DOM 노드입니다. -->\n    <div id=\"root\"></div>\n  </body>\n</html>\n```\n\n```js src/index.js active\nimport { createRoot } from 'react-dom/client';\nimport App from './App.js';\nimport './styles.css';\n\nconst root = createRoot(document.getElementById('root'));\nroot.render(<App />);\n```\n\n```js src/App.js\nimport { useState } from 'react';\n\nexport default function App() {\n  return (\n    <>\n      <h1>Hello, world!</h1>\n      <Counter />\n    </>\n  );\n}\n\nfunction Counter() {\n  const [count, setCount] = useState(0);\n  return (\n    <button onClick={() => setCount(count + 1)}>\n      You clicked me {count} times\n    </button>\n  );\n}\n```\n\n</Sandpack>\n\n**앱이 온전히 React만으로 작성된 경우, 추가적으로 루트를 더 만들거나 [`root.render`](#root-render)를 다시 호출할 필요가 없습니다.**\n\n이 시점부터 React는 전체 앱의 DOM을 관리합니다. 컴포넌트를 더 추가하려면 [ `App` 컴포넌트 안에 중첩](/learn/importing-and-exporting-components)시키세요. UI 업데이트는 각 컴포넌트의 [State를 통해](/reference/react/useState) 수행할 수 있습니다. 모달이나 툴팁과 같은 추가 콘텐츠를 DOM 노드 외부에 표시해야 하는 경우 [Portal로 렌더링](/reference/react-dom/createPortal)하세요.\n\n<Note>\n\nHTML이 비어있으면, 앱의 자바스크립트 코드가 로드되고 실행될 때까지 사용자에게 빈 페이지가 표시됩니다.\n\n```html\n<div id=\"root\"></div>\n```\n\nThis can feel very slow! To solve this, you can generate the initial HTML from your components [on the server or during the build.](/reference/react-dom/server) Then your visitors can read text, see images, and click links before any of the JavaScript code loads. We recommend [using a framework](/learn/creating-a-react-app#full-stack-frameworks) that does this optimization out of the box. Depending on when it runs, this is called *server-side rendering (SSR)* or *static site generation (SSG).*\n\n</Note>\n\n<Pitfall>\n\n**서버 측 렌더링이나 정적 사이트 생성을 사용하는 앱은 `createRoot` 대신 [`hydrateRoot`](/reference/react-dom/client/hydrateRoot)를 호출해야 합니다.** 그러면 React는 DOM 노드를 파괴하고 다시 생성하는 대신 HTML으로부터 *Hydrate*(재사용)합니다.\n</Pitfall>\n\n---\n\n### React로 부분적으로 작성된 페이지 렌더링하기 {/*rendering-a-page-partially-built-with-react*/}\n\n페이지가 [React만으로 작성되지 않은 경우](/learn/add-react-to-an-existing-project#using-react-for-a-part-of-your-existing-page), React가 관리하는 각 최상위 UI에 대한 루트를 생성하기 위해 `createRoot`를 여러 번 호출할 수 있습니다. 루트마다 [`root.render`](#root-render)를 호출함으로써 각각 다른 콘텐츠를 표시할 수 있습니다.\n\n다음 예시에서는 서로 다른 두 개의 React 컴포넌트를 `index.html` 파일에 정의된 두 개의 DOM 노드에 렌더링합니다.\n\n<Sandpack>\n\n```html public/index.html\n<!DOCTYPE html>\n<html>\n  <head><title>My app</title></head>\n  <body>\n    <nav id=\"navigation\"></nav>\n    <main>\n      <p>This paragraph is not rendered by React (open index.html to verify).</p>\n      <section id=\"comments\"></section>\n    </main>\n  </body>\n</html>\n```\n\n```js src/index.js active\nimport './styles.css';\nimport { createRoot } from 'react-dom/client';\nimport { Comments, Navigation } from './Components.js';\n\nconst navDomNode = document.getElementById('navigation');\nconst navRoot = createRoot(navDomNode);\nnavRoot.render(<Navigation />);\n\nconst commentDomNode = document.getElementById('comments');\nconst commentRoot = createRoot(commentDomNode);\ncommentRoot.render(<Comments />);\n```\n\n```js src/Components.js\nexport function Navigation() {\n  return (\n    <ul>\n      <NavLink href=\"/\">Home</NavLink>\n      <NavLink href=\"/about\">About</NavLink>\n    </ul>\n  );\n}\n\nfunction NavLink({ href, children }) {\n  return (\n    <li>\n      <a href={href}>{children}</a>\n    </li>\n  );\n}\n\nexport function Comments() {\n  return (\n    <>\n      <h2>Comments</h2>\n      <Comment text=\"Hello!\" author=\"Sophie\" />\n      <Comment text=\"How are you?\" author=\"Sunil\" />\n    </>\n  );\n}\n\nfunction Comment({ text, author }) {\n  return (\n    <p>{text} — <i>{author}</i></p>\n  );\n}\n```\n\n```css\nnav ul { padding: 0; margin: 0; }\nnav ul li { display: inline-block; margin-right: 20px; }\n```\n\n</Sandpack>\n\n[`document.createElement()`](https://developer.mozilla.org/en-US/docs/Web/API/Document/createElement)를 사용하여 새 DOM 노드를 생성하고 문서에 수동으로 추가할 수도 있습니다.\n\n```js\nconst domNode = document.createElement('div');\nconst root = createRoot(domNode);\nroot.render(<Comment />);\ndocument.body.appendChild(domNode); // You can add it anywhere in the document\n```\n\nDOM 노드에서 React 트리를 제거하고 이 트리가 사용하는 모든 리소스를 정리하려면 [`root.unmount`](#root-unmount)를 호출하세요.\n\n```js\nroot.unmount();\n```\n\n이 기능은 React 컴포넌트가 다른 프레임워크로 작성된 앱 내부에 있는 경우에 주로 유용합니다.\n\n---\n\n### 루트 컴포넌트 업데이트하기 {/*updating-a-root-component*/}\n\n같은 루트에서 `render`를 두 번 이상 호출할 수도 있습니다. 컴포넌트 트리 구조가 이전 렌더링과 일치하는 한, React는 [기존 State를 유지](/learn/preserving-and-resetting-state)합니다. 다음 예시에서 입력 창에 어떻게 타이핑하든 관계없이, 매 초 반복되는 `render` 호출로 인한 업데이트가 아무런 문제를 일으키지 않음을 주목하세요.\n\n<Sandpack>\n\n```js src/index.js active\nimport { createRoot } from 'react-dom/client';\nimport './styles.css';\nimport App from './App.js';\n\nconst root = createRoot(document.getElementById('root'));\n\nlet i = 0;\nsetInterval(() => {\n  root.render(<App counter={i} />);\n  i++;\n}, 1000);\n```\n\n```js src/App.js\nexport default function App({counter}) {\n  return (\n    <>\n      <h1>Hello, world! {counter}</h1>\n      <input placeholder=\"Type something here\" />\n    </>\n  );\n}\n```\n\n</Sandpack>\n\n`render`를 여러 번 호출하는 경우는 흔하지 않습니다. 일반적으로는, 컴포넌트가 [State를 업데이트](/reference/react/useState)합니다.\n\n### 프로덕션 환경에서 오류 로깅하기 {/*error-logging-in-production*/}\n\nReact는 기본적으로 모든 오류를 콘솔에 출력합니다. 사용자 정의 오류 보고 기능을 구현하기 위해서 `onUncaughtError`, `onCaughtError`, `onRecoverableError`와 같은 에러 핸들러 루트 옵션을 제공할 수 있습니다.\n\n```js [[1, 6, \"onCaughtError\"], [2, 6, \"error\", 1], [3, 6, \"errorInfo\"], [4, 10, \"componentStack\", 15]]\nimport { createRoot } from \"react-dom/client\";\nimport { reportCaughtError } from \"./reportError\";\n\nconst container = document.getElementById(\"root\");\nconst root = createRoot(container, {\n  onCaughtError: (error, errorInfo) => {\n    if (error.message !== \"Known error\") {\n      reportCaughtError({\n        error,\n        componentStack: errorInfo.componentStack,\n      });\n    }\n  },\n});\n```\n\n<CodeStep step={1}>onCaughtError</CodeStep> 옵션은 다음 두 개의 인자를 받는 함수입니다.\n\n1. 발생한 <CodeStep step={2}>error</CodeStep> 객체.\n2. 오류의 <CodeStep step={4}>componentStack</CodeStep> 정보를 포함한 <CodeStep step={3}>errorInfo</CodeStep> 객체.\n\n`onUncaughtError`와 `onRecoverableError`를 함께 사용하면, 사용자 정의 오류 보고 시스템을 구현할 수 있습니다.\n\n<Sandpack>\n\n```js src/reportError.js\nfunction reportError({ type, error, errorInfo }) {\n  // 구체적인 구현은 여러분에게 맡깁니다.\n  // `console.error()`는 설명을 위한 용도입니다.\n  console.error(type, error, \"Component Stack: \");\n  console.error(\"Component Stack: \", errorInfo.componentStack);\n}\n\nexport function onCaughtErrorProd(error, errorInfo) {\n  if (error.message !== \"Known error\") {\n    reportError({ type: \"Caught\", error, errorInfo });\n  }\n}\n\nexport function onUncaughtErrorProd(error, errorInfo) {\n  reportError({ type: \"Uncaught\", error, errorInfo });\n}\n\nexport function onRecoverableErrorProd(error, errorInfo) {\n  reportError({ type: \"Recoverable\", error, errorInfo });\n}\n```\n\n```js src/index.js active\nimport { createRoot } from \"react-dom/client\";\nimport App from \"./App.js\";\nimport {\n  onCaughtErrorProd,\n  onRecoverableErrorProd,\n  onUncaughtErrorProd,\n} from \"./reportError\";\n\nconst container = document.getElementById(\"root\");\nconst root = createRoot(container, {\n  // 개발 환경에서는 이 옵션들을 제거하고\n  // React의 기본 핸들러를 사용하거나 직접 오버레이를 구현하는 것을 권장합니다.\n  // 여기서는 편의를 위해 조건 없이 핸들러를 지정했습니다.\n  onCaughtError: onCaughtErrorProd,\n  onRecoverableError: onRecoverableErrorProd,\n  onUncaughtError: onUncaughtErrorProd,\n});\nroot.render(<App />);\n```\n\n```js src/App.js\nimport { Component, useState } from \"react\";\n\nfunction Boom() {\n  foo.bar = \"baz\";\n}\n\nclass ErrorBoundary extends Component {\n  state = { hasError: false };\n\n  static getDerivedStateFromError(error) {\n    return { hasError: true };\n  }\n\n  render() {\n    if (this.state.hasError) {\n      return <h1>Something went wrong.</h1>;\n    }\n    return this.props.children;\n  }\n}\n\nexport default function App() {\n  const [triggerUncaughtError, settriggerUncaughtError] = useState(false);\n  const [triggerCaughtError, setTriggerCaughtError] = useState(false);\n\n  return (\n    <>\n      <button onClick={() => settriggerUncaughtError(true)}>\n        Trigger uncaught error\n      </button>\n      {triggerUncaughtError && <Boom />}\n      <button onClick={() => setTriggerCaughtError(true)}>\n        Trigger caught error\n      </button>\n      {triggerCaughtError && (\n        <ErrorBoundary>\n          <Boom />\n        </ErrorBoundary>\n      )}\n    </>\n  );\n}\n```\n\n</Sandpack>\n\n\n---\n## 문제 해결 {/*troubleshooting*/}\n\n### 루트를 생성했는데 아무것도 표시되지 않습니다 {/*ive-created-a-root-but-nothing-is-displayed*/}\n\n실제로 앱을 루트에 **렌더링**하는 것을 잊지 않았는지 확인하세요.\n\n```js {5}\nimport { createRoot } from 'react-dom/client';\nimport App from './App.js';\n\nconst root = createRoot(document.getElementById('root'));\nroot.render(<App />);\n```\n\n`root.render(...)` 명령 없이는 아무것도 표시되지 않습니다.\n\n---\n\n### 오류 발생: \"root.render에 두 번째 인수를 전달했습니다\" {/*im-getting-an-error-you-passed-a-second-argument-to-root-render*/}\n\n흔히 하는 실수는 `createRoot`의 옵션을 `root.render(...)`에 전달하는 것입니다.\n\n<ConsoleBlock level=\"error\">\n\nWarning: You passed a second argument to root.render(...) but it only accepts one argument.\n\n</ConsoleBlock>\n\n해결 방법: 루트 옵션은 `root.render(...)`가 아니라 `createRoot(...)`에 전달하세요.\n```js {2,5}\n// 🚩 잘못된 방법: root.render는 하나의 인자만 받습니다.\nroot.render(App, {onUncaughtError});\n\n// ✅ 올바른 방법: 옵션은 createRoot에 전달합니다.\nconst root = createRoot(container, {onUncaughtError});\nroot.render(<App />);\n```\n\n---\n\n### \"대상 컨테이너가 DOM 엘리먼트가 아닙니다\" 라는 오류가 발생합니다. {/*im-getting-an-error-target-container-is-not-a-dom-element*/}\n\n이 오류는 `createRoot`에 전달한 값이 DOM 노드가 아니라는 뜻입니다.\n\n무슨 상황인지 잘 모르겠다면, 해당 값을 콘솔에 출력해서 확인해 보세요.\n\n```js {2}\nconst domNode = document.getElementById('root');\nconsole.log(domNode); // ???\nconst root = createRoot(domNode);\nroot.render(<App />);\n```\n\n예를 들어 `domNode`가 `null`이면 [`getElementById`](https://developer.mozilla.org/ko/docs/Web/API/Document/getElementById) 가 `null`을 반환했음을 의미합니다. 이는 호출 시점에 문서에 지정된 ID를 가진 노드가 없는 경우에 발생합니다. 여기에는 몇 가지 이유가 있을 수 있습니다.\n\n1. 찾고자 하는 ID가 HTML 파일에서 사용한 ID와 다를 수 있습니다. 오타가 있는지 확인하세요!\n2. 번들의 `<script>` 태그는 HTML에서 그보다 *뒤에* 있는 DOM 노드를 \"인식할\" 수 없습니다.\n\n또다른 일반적인 사례는 `createRoot(domNode)` 대신 `createRoot(<App />)`으로 작성했을 경우입니다.\n\n---\n\n### \"함수가 React 자식으로 유효하지 않습니다\" 오류가 발생합니다. {/*im-getting-an-error-functions-are-not-valid-as-a-react-child*/}\n\n이 오류는 `root.render`에 전달하는 것이 React 컴포넌트가 아님을 의미합니다.\n\n이 오류는 `<Component />` 대신 `Component`로 `root.render`를 호출할 때 발생할 수 있습니다.\n\n```js {2,5}\n// 🚩 잘못된 방법: App은 컴포넌트가 아니라 함수입니다.\nroot.render(App);\n\n// ✅ 올바른 방법: <App />은 컴포넌트입니다.\nroot.render(<App />);\n```\n\n또는 함수를 호출한 결과 대신 `root.render`에 함수 자체를 전달했을 때도 발생할 수 있습니다.\n\n```js {2,5}\n// 🚩 잘못된 방법: createApp은 컴포넌트가 아니라 함수입니다.\nroot.render(createApp);\n\n// ✅ 올바른 방법: createApp을 호출하여 컴포넌트를 반환합니다.\nroot.render(createApp());\n```\n\n---\n\n### 서버에서 렌더링된 HTML이 처음부터 다시 생성됩니다 {/*my-server-rendered-html-gets-re-created-from-scratch*/}\n\n앱이 서버에서 렌더링되고 React의 초기 HTML을 포함하는 경우에, 루트를 생성해서 `root.render`를 호출하면, 모든 HTML이 삭제되고 모든 DOM 노드가 처음부터 다시 생성되는 것을 볼 수 있습니다. 이렇게 하면 속도가 느려지고, 포커스와 스크롤 위치가 재설정되며, 그 밖의 다른 사용자 입력들이 손실될 수 있습니다.\n\n서버에서 렌더링된 앱은 `createRoot` 대신 [`hydrateRoot`](/reference/react-dom/client/hydrateRoot)를 사용해야 합니다.\n\n```js {1,4-7}\nimport { hydrateRoot } from 'react-dom/client';\nimport App from './App.js';\n\nhydrateRoot(\n  document.getElementById('root'),\n  <App />\n);\n```\n\nAPI가 다르다는 점에 유의하세요. 특히, 일반적으로는 `root.render`를 아예 호출하지 않습니다.\n"
  },
  {
    "path": "src/content/reference/react-dom/client/hydrateRoot.md",
    "content": "---\ntitle: hydrateRoot\n---\n\n<Intro>\n\n`hydrateRoot`는 이전에 [`react-dom/server`](/reference/react-dom/server)로 생성된 HTML 콘텐츠를 가진 브라우저 DOM 노드 안에 React 컴포넌트를 표시할 수 있게 해줍니다.\n\n```js\nconst root = hydrateRoot(domNode, reactNode, options?)\n```\n\n</Intro>\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `hydrateRoot(domNode, reactNode, options?)` {/*hydrateroot*/}\n\n`hydrateRoot`를 호출하여 이미 서버 환경에서 렌더링된 기존 HTML에 React를 \"붙여넣기\" 합니다.\n\n```js\nimport { hydrateRoot } from 'react-dom/client';\n\nconst domNode = document.getElementById('root');\nconst root = hydrateRoot(domNode, reactNode);\n```\n\nReact는 `domNode` 내부에 존재하는 HTML에 연결되어, 그 내부의 DOM 관리를 맡게 됩니다. React로 완전히 구축된 앱은 일반적으로 루트 컴포넌트와 함께 하나의 `hydrateRoot` 호출만 가집니다.\n\n[아래 예시를 참고하세요.](#usage)\n\n#### 매개변수 {/*parameters*/}\n\n* `domNode`: 서버에서 루트 요소<sup>Element</sup>로 렌더링된 [DOM 요소](https://developer.mozilla.org/en-US/docs/Web/API/Element).\n\n* `reactNode`: 기존 HTML에 렌더링하기 위한 \"React 노드\" 입니다. 주로 `ReactDOM Server`의 `renderToPipeableStream(<App />)`와 같은 메서드로 렌더링된 `<App />`과 같은 JSX 조각들입니다.\n\n* **optional** `options`: React 루트에 대한 옵션을 가진 객체입니다.\n  * **optional** `onCaughtError`: React가 Error Boundary에서 오류를 잡았을 때 호출되는 콜백입니다. Error Boundary에서 잡은 `error`와 `componentStack`을 포함하는 `errorInfo` 객체와 함께 호출됩니다.\n  * **optional** `onUncaughtError`: 오류가 Error Boundary에 의해 잡히지 않았을 때 호출되는 콜백입니다. 발생한 `error`와 `componentStack`을 포함하는 `errorInfo` 객체와 함께 호출됩니다.\n  * **optional** `onRecoverableError`: React가 오류로부터 자동으로 복구될 때 호출되는 콜백입니. React가 던지는 `error`와 `componentStack`을 포함하는 `errorInfo` 객체와 함께 호출됩니다. 복구 가능한 오류는 원본 오류 원인을 `error.cause`로 포함할 수 있습니다.\n  * **optional** `identifierPrefix`: React가 [`useId`](/reference/react/useId)에 의해 생성된 ID에 사용하는 문자열 접두사. 같은 페이지에서 여러개의 루트를 사용할 때 충돌을 피하는 데 유용합니다. 서버에서 사용한 값과 반드시 동일한 값이어야 합니다.\n\n#### 반환값 {/*returns*/}\n\n`hydrateRoot`는 [`render`](#root-render)와 [`unmount`](#root-unmount) 두 가지 메서드를 포함한 객체를 반환합니다.\n\n#### 주의 사항 {/*caveats*/}\n\n* `hydrateRoot()`는 렌더링된 컨텐츠가 서버에서 렌더링된 컨텐츠와 동일할 것을 기대합니다. 따라서 불일치 사항은 버그로 취급하고 수정해야 합니다.\n* 개발 모드에서는 React가 Hydration 중 불일치에 대해 경고합니다. 불일치가 발생할 경우 속성 차이가 수정될 것이라는 보장은 없습니다. 이는 성능상의 이유로 중요한데, 대부분의 앱에서 불일치는 드물기 때문에 모든 마크업을 검증하는 것은 매우 비효율적이기 때문입니다.\n* 앱에서 `hydrateRoot` 호출이 단 한번만 있을 가능성이 높습니다. 프레임워크를 사용한다면, 프레임워크가 이 호출을 대신 수행할 수도 있습니다.\n* 앱을 사전에 렌더링된 HTML 없이 클라이언트에서 직접 렌더링한다면, `hydrateRoot()`는 지원되지 않습니다. [`createRoot()`](/reference/react-dom/client/createRoot)를 대신 사용해주세요.\n\n---\n\n### `root.render(reactNode)` {/*root-render*/}\n\n브라우저 DOM 요소 내에서 Hydrate된 React 루트 안의 React 컴포넌트를 업데이트 하려면 `root.render`를 호출하세요.\n\n```js\nroot.render(<App />);\n```\n\nReact는 Hydrate된 `root`에서 `<App />`을 업데이트합니다.\n\n[아래 예시를 참고하세요.](#usage)\n\n#### 매개변수 {/*root-render-parameters*/}\n\n* `reactNode`: 업데이트하고 싶은 \"React 노드\"입니다. 주로 `<App />`같은 JSX를 매개변수로 넘기지만, [`createElement()`](/reference/react/createElement)로 만든 React 요소 혹은 문자열, 숫자, `null`, `undefined`를 넘겨도 됩니다.\n\n#### 반환값 {/*root-render-returns*/}\n\n`root.render`는 `undefined`를 반환합니다.\n\n#### 주의 사항 {/*root-render-caveats*/}\n\n* 루트가 Hydrate를 완료하기 전에 `root.render`를 호출하면, React는 서버에서 렌더링된 HTML을 모두 없애고 클라이언트에서 렌더링된 컴포넌트들로 완전히 교체합니다.\n\n---\n\n### `root.unmount()` {/*root-unmount*/}\n\n`root.unmount`를 호출하면 React 루트 내부에서 렌더링된 트리를 삭제합니다.\n\n```js\nroot.unmount();\n```\n\n온전히 React만으로 작성된 앱에는 일반적으로 `root.unmount`에 대한 호출이 없습니다.\n\n이 함수는 주로 React 루트의 DOM 노드(또는 그 조상 노드)가 다른 코드에 의해 DOM에서 제거될 수 있는 경우에 유용합니다. 예를 들어 DOM에서 비활성 탭을 제거하는 jQuery 탭 패널을 상상해 보세요. 탭이 제거되면 그 안에 있는 모든 것(내부의 React 루트를 포함)이 DOM에서 제거됩니다. 이 경우 `root.unmount`를 호출하여 제거된 루트의 콘텐츠 관리를 \"중지\"하도록 React에 지시해야 합니다. 그렇지 않으면 제거된 루트 내부의 컴포넌트는 구독과 같은 전역 리소스를 정리하고 확보하는 법을 모르는 채로 있게 됩니다.\n\n`root.unmount`를 호출하면 루트에 있는 모든 컴포넌트가 마운트 해제되고, 트리상의 이벤트 핸들러나 State가 제거되며, 루트 DOM 노드에서 React가 \"분리\"됩니다.\nCalling `root.unmount` will unmount all the components in the root and \"detach\" React from the root DOM node, including removing any event handlers or state in the tree.\n\n#### 매개변수 {/*root-unmount-parameters*/}\n\n`root.unmount`는 매개변수를 받지 않습니다.\n\n\n#### Returns {/*root-unmount-returns*/}\n\n`root.unmount` returns `undefined`.\n\n#### 주의 사항 {/*root-unmount-caveats*/}\n\n* `root.unmount`를 호출하면 트리의 모든 컴포넌트가 마운트 해제되고 루트 DOM 노드에서 React가 \"분리\"됩니다.\n\n* `root.unmount`를 한 번 호출한 후에는 같은 루트에서 `root.render`를 다시 호출할 수 없습니다. 마운트 해제된 루트에서 `root.render`를 호출하려고 하면 \"마운트 해제된 루트를 업데이트할 수 없습니다.<sup>Cannot update an unmounted root</sup>\" 오류가 발생합니다.\n\n---\n\n## 사용법 {/*usage*/}\n\n### 서버에서 렌더링된 HTML을 Hydrate하기 {/*hydrating-server-rendered-html*/}\n\n[`react-dom/server`](/reference/react-dom/client/createRoot)로 앱의 HTML을 생성했다면, 클라이언트에서 *Hydrate* 해주어야 합니다.\n\n```js [[1, 3, \"document.getElementById('root')\"], [2, 3, \"<App />\"]]\nimport { hydrateRoot } from 'react-dom/client';\n\nhydrateRoot(document.getElementById('root'), <App />);\n```\n\n위 코드를 통해 서버 HTML을 <CodeStep step={1}>브라우저 DOM 노드</CodeStep>에서 <CodeStep step={2}>React 컴포넌트</CodeStep>를 이용해 Hydrate 해줄 것 입니다. 주로 앱을 시작할 때 단 한 번 실행할 것입니다. 프레임워크를 사용중이라면 프레임워크가 대신 실행해 줄 것입니다.\n\n앱을 Hydrate 하기 위해서 React는 컴포넌트의 로직을 사전에 서버에서 만들어 진 HTML에 \"붙여넣을\"것 입니다. Hydration을 통해 서버에서 만들어진 최초의 HTML 스냅샷을 브라우저에서 완전히 인터랙티브한 앱으로 바꿔주게 됩니다.\n\n<Sandpack>\n\n```html public/index.html\n<!--\n  <div id=\"root\">...</div> 안의 HTML 내용들은\n  react-dom/server으로 만들어진 App입니다.\n-->\n<div id=\"root\"><h1>Hello, world!</h1><button>You clicked me <!-- -->0<!-- --> times</button></div>\n```\n\n```js src/index.js active\nimport './styles.css';\nimport { hydrateRoot } from 'react-dom/client';\nimport App from './App.js';\n\nhydrateRoot(\n  document.getElementById('root'),\n  <App />\n);\n```\n\n```js src/App.js\nimport { useState } from 'react';\n\nexport default function App() {\n  return (\n    <>\n      <h1>Hello, world!</h1>\n      <Counter />\n    </>\n  );\n}\n\nfunction Counter() {\n  const [count, setCount] = useState(0);\n  return (\n    <button onClick={() => setCount(count + 1)}>\n      You clicked me {count} times\n    </button>\n  );\n}\n```\n\n</Sandpack>\n\n`hydrateRoot`를 다시 호출하거나 다른 곳에서 더 호출할 필요는 없습니다. 이 시점부터 React가 애플리케이션의 DOM을 다루게 됩니다. 대신 UI를 갱신하기 위해선 [State를 사용](/reference/react/useState)해야 합니다.\n\n<Pitfall>\n\n`hydrateRoot`에 전달한 React 트리는 서버에서 만들었던 React 트리 결과물과 동일해야 합니다.\n\n이는 사용자 경험을 위해서 중요합니다. 사용자는 서버에서 만들어진 HTML을 자바스크립트 코드가 로드될 때까지 둘러보게 됩니다. 앱의 로딩을 더 빠르게 하기 위해 서버는 일종의 신기루로서 React 결과물인 HTML 스냅샷을 만들어 보여줍니다. 갑자기 다른 컨텐츠를 보여주게 되면 신기루가 깨져버리게 됩니다. 이런 이유로 서버에서 렌더링한 결과물과 클라이언트에서 최초로 렌더링한 결과물이 같아야 합니다.\n\n주로 아래와 같은 원인들로 Hydration 오류가 일어납니다.\n\n* React를 통해 만들어진 HTML의 루트 노드안에 공백 혹은 개행같은 추가적인 공백.\n* `typeof window !== 'undefined'`과 같은 조건을 렌더링 로직에서 사용.\n* [`window.matchMedia`](https://developer.mozilla.org/en-US/docs/Web/API/Window/matchMedia)같은 브라우저에서만 사용가능한 API를 렌더링 로직에 사용.\n* 서버와 클라이언트에서 서로 다른 데이터를 렌더링.\n\nReact는 Hydration 오류에서 복구됩니다, 하지만 **다른 버그들과 같이 반드시 고쳐줘야 합니다.** 가장 나은 경우는 그저 느려지기만 할 뿐이지만, 최악의 경우엔 이벤트 핸들러가 다른 요소<sup>Element</sup>에 붙어버립니다.\n\n</Pitfall>\n\n---\n\n### `document` 전체를 Hydrate하기 {/*hydrating-an-entire-document*/}\n\nReact로 앱을 모두 만들었을 경우 [`<html>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/html) 태그를 포함해 JSX로 된 전체 `document`를 렌더링할 수 있습니다.\n\n```js {3,13}\nfunction App() {\n  return (\n    <html>\n      <head>\n        <meta charSet=\"utf-8\" />\n        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n        <link rel=\"stylesheet\" href=\"/styles.css\"></link>\n        <title>My app</title>\n      </head>\n      <body>\n        <Router />\n      </body>\n    </html>\n  );\n}\n```\n\n전체 `document`를 Hydrate하기 위해선 전역 변수인 [`document`](https://developer.mozilla.org/en-US/docs/Web/API/Window/document)를 `hydrateRoot`의 첫번째 인수로 넘깁니다.\n\n```js {4}\nimport { hydrateRoot } from 'react-dom/client';\nimport App from './App.js';\n\nhydrateRoot(document, <App />);\n```\n\n---\n\n### 어쩔 수 없는 Hydration 불일치 오류 억제하기 {/*suppressing-unavoidable-hydration-mismatch-errors*/}\n\n어떤 요소<sup>Element</sup>의 속성이나 텍스트 컨텐츠가 서버와 클라이언트에서 어쩔 수 없이 다를 땐(예를 들어, timestamp를 이용했다거나), Hydration 불일치 경고를 안보이게 할 수 있습니다.\n\n해당 요소에서 Hydration 경고를 끄기 위해선 `suppressHydrationWarning={true}`를 추가하면 됩니다.\n\n<Sandpack>\n\n```html public/index.html\n<!--\n  <div id=\"root\">...</div> 안의 HTML 내용들은\n  react-dom/server으로 만들어진 App입니다.\n-->\n<div id=\"root\"><h1>Current Date: <!-- -->01/01/2020</h1></div>\n```\n\n```js src/index.js\nimport './styles.css';\nimport { hydrateRoot } from 'react-dom/client';\nimport App from './App.js';\n\nhydrateRoot(document.getElementById('root'), <App />);\n```\n\n```js src/App.js active\nexport default function App() {\n  return (\n    <h1 suppressHydrationWarning={true}>\n      Current Date: {new Date().toLocaleDateString()}\n    </h1>\n  );\n}\n```\n\n</Sandpack>\n\n이것은 한 단계 아래까지만 적용되며 탈출구<sup>Escape Hatch</sup>를 의도한 것입니다. 남용하지 마세요. 텍스트 컨텐츠가 아닌 한 React는 잘못된 부분을 수정하지 않을 것이며, 갱신이 일어나기 전까지는 불일치 상태로 남아있을 것입니다.\n\n---\n\n### 서로 다른 클라이언트와 서버 컨텐츠 다루기 {/*handling-different-client-and-server-content*/}\n\n의도적으로 서버와 클라이언트에서 서로 다른 내용을 렌더링하길 원한다면, 서버와 클라이언트에서 서로 다른 방법으로 렌더링하면 됩니다. 클라이언트에서 서버와는 다른 것을 렌더링할 때 클라이언트에선 [Effect](/reference/react/useEffect)에서 `true`로 할당되는 `isClient`같은 [State 변수](/reference/react/useState)를 읽을 수 있습니다.\n\n<Sandpack>\n\n```html public/index.html\n<!--\n  <div id=\"root\">...</div> 안의 HTML 내용들은\n  react-dom/server으로 만들어진 App입니다.\n-->\n<div id=\"root\"><h1>Is Server</h1></div>\n```\n\n```js src/index.js\nimport './styles.css';\nimport { hydrateRoot } from 'react-dom/client';\nimport App from './App.js';\n\nhydrateRoot(document.getElementById('root'), <App />);\n```\n\n{/* kind of an edge case, seems fine to use this hack here */}\n```js src/App.js active\nimport { useState, useEffect } from \"react\";\n\nexport default function App() {\n  const [isClient, setIsClient] = useState(false);\n\n  useEffect(() => {\n    setIsClient(true);\n  }, []);\n\n  return (\n    <h1>\n      {isClient ? 'Is Client' : 'Is Server'}\n    </h1>\n  );\n}\n```\n\n</Sandpack>\n\n이 방법은 처음엔 서버와 동일한 결과물을 렌더링하여 불일치 문제를 피하고, Hydration 후에 새로운 결과물이 동기적으로 렌더링됩니다.\n\n<Pitfall>\n\n이 방법은 두 번 렌더링해야 하기 때문에 Hydration을 느리게 합니다. 느린 통신 상태일 경우에 사용자 경험을 염두하세요. 초기 HTML이 렌더링된 한참 후에야 자바스크립트 코드를 불러옵니다. 따라서 Hydration 이후에 바로 다른 UI를 렌더링하는 것은 사용자에게 UI가 삐걱거리는 것처럼 보일 수 있습니다.\n\n</Pitfall>\n\n---\n\n### Hydration된 루트 컴포넌트를 업데이트하기 {/*updating-a-hydrated-root-component*/}\n\n루트의 Hydration이 끝난 후에, [`root.render`](#root-render)를 호출해 React 컴포넌트의 루트를 업데이트 할 수 있습니다. **[`createRoot`](/reference/react-dom/client/createRoot)와는 다르게 HTML로 최초의 컨텐츠가 이미 렌더링 되어 있기 때문에 자주 사용할 필요는 없습니다.**\n\nHydration 후 어떤 시점에 `root.render`를 호출한다면, 그리고 컴포넌트의 트리 구조가 이전에 렌더링했던 구조와 일치한다면, React는 [State를 그대로 보존합니다.](/learn/preserving-and-resetting-state) 입력 창<sup>Input</sup>에 어떻게 타이핑하든지 간에 문제가 발생하지 않습니다. 즉, 아래 예시에서처럼 매초 마다 상태를 업데이트하는 반복적인 `render`를 문제 없이 렌더링 한다는 것을 알 수 있습니다.\n\n<Sandpack>\n\n```html public/index.html\n<!--\n  <div id=\"root\">...</div>안의 모든 HTML 컨텐츠는 react-dom/server를 통해 만들어 렌더링한 <App />입니다.\n-->\n<div id=\"root\"><h1>Hello, world! <!-- -->0</h1><input placeholder=\"Type something here\"/></div>\n```\n\n```js src/index.js active\nimport { hydrateRoot } from 'react-dom/client';\nimport './styles.css';\nimport App from './App.js';\n\nconst root = hydrateRoot(\n  document.getElementById('root'),\n  <App counter={0} />\n);\n\nlet i = 0;\nsetInterval(() => {\n  root.render(<App counter={i} />);\n  i++;\n}, 1000);\n```\n\n```js src/App.js\nexport default function App({counter}) {\n  return (\n    <>\n      <h1>Hello, world! {counter}</h1>\n      <input placeholder=\"Type something here\" />\n    </>\n  );\n}\n```\n\n</Sandpack>\n\nHydration된 루트에서 [`root.render`](#root-render)를 호출하는 것은 흔한 일은 아닙니다. 내부 컴포넌트 중 한 곳에서 [useState](/reference/react/useState)를 사용하는 것이 일반적입니다.\n\n### 프로덕션 환경에서 오류 로깅하기 {/*error-logging-in-production*/}\n\nReact는 기본적으로 모든 오류를 콘솔에 출력합니다. 사용자 정의 오류 보고 기능을 구현하기 위해서 `onUncaughtError`, `onCaughtError`, `onRecoverableError`와 같은 에러 핸들러 루트 옵션을 제공할 수 있습니다.\n\n```js [[1, 7, \"onCaughtError\"], [2, 7, \"error\", 1], [3, 7, \"errorInfo\"], [4, 11, \"componentStack\", 15]]\nimport { hydrateRoot } from \"react-dom/client\";\nimport App from \"./App.js\";\nimport { reportCaughtError } from \"./reportError\";\n\nconst container = document.getElementById(\"root\");\nconst root = hydrateRoot(container, <App />, {\n  onCaughtError: (error, errorInfo) => {\n    if (error.message !== \"Known error\") {\n      reportCaughtError({\n        error,\n        componentStack: errorInfo.componentStack,\n      });\n    }\n  },\n});\n```\n\n<CodeStep step={1}>onCaughtError</CodeStep> 옵션은 다음 두 개의 인자를 받는 함수입니다.\n\n1. 발생한 <CodeStep step={2}>error</CodeStep> 객체.\n2. 오류의 <CodeStep step={4}>componentStack</CodeStep> 정보를 포함한 <CodeStep step={3}>errorInfo</CodeStep> 객체.\n\n`onUncaughtError`와 `onRecoverableError`를 함께 사용하면, 사용자 정의 오류 보고 시스템을 구현할 수 있습니다.\n\n<Sandpack>\n\n```js src/reportError.js\nfunction reportError({ type, error, errorInfo }) {\n  // 구체적인 구현은 여러분에게 맡깁니다.\n  // `console.error()`는 설명을 위한 용도입니다.\n  console.error(type, error, \"Component Stack: \");\n  console.error(\"Component Stack: \", errorInfo.componentStack);\n}\n\nexport function onCaughtErrorProd(error, errorInfo) {\n  if (error.message !== \"Known error\") {\n    reportError({ type: \"Caught\", error, errorInfo });\n  }\n}\n\nexport function onUncaughtErrorProd(error, errorInfo) {\n  reportError({ type: \"Uncaught\", error, errorInfo });\n}\n\nexport function onRecoverableErrorProd(error, errorInfo) {\n  reportError({ type: \"Recoverable\", error, errorInfo });\n}\n```\n\n```js src/index.js active\nimport { hydrateRoot } from \"react-dom/client\";\nimport App from \"./App.js\";\nimport {\n  onCaughtErrorProd,\n  onRecoverableErrorProd,\n  onUncaughtErrorProd,\n} from \"./reportError\";\n\nconst container = document.getElementById(\"root\");\nhydrateRoot(container, <App />, {\n  // 개발 환경에서는 이 옵션들을 제거하고\n  // React의 기본 핸들러를 사용하거나 직접 오버레이를 구현하는 것을 권장합니다.\n  // 여기서는 편의를 위해 조건 없이 핸들러를 지정했습니다.\n  onCaughtError: onCaughtErrorProd,\n  onRecoverableError: onRecoverableErrorProd,\n  onUncaughtError: onUncaughtErrorProd,\n});\n```\n\n```js src/App.js\nimport { Component, useState } from \"react\";\n\nfunction Boom() {\n  foo.bar = \"baz\";\n}\n\nclass ErrorBoundary extends Component {\n  state = { hasError: false };\n\n  static getDerivedStateFromError(error) {\n    return { hasError: true };\n  }\n\n  render() {\n    if (this.state.hasError) {\n      return <h1>Something went wrong.</h1>;\n    }\n    return this.props.children;\n  }\n}\n\nexport default function App() {\n  const [triggerUncaughtError, settriggerUncaughtError] = useState(false);\n  const [triggerCaughtError, setTriggerCaughtError] = useState(false);\n\n  return (\n    <>\n      <button onClick={() => settriggerUncaughtError(true)}>\n        Trigger uncaught error\n      </button>\n      {triggerUncaughtError && <Boom />}\n      <button onClick={() => setTriggerCaughtError(true)}>\n        Trigger caught error\n      </button>\n      {triggerCaughtError && (\n        <ErrorBoundary>\n          <Boom />\n        </ErrorBoundary>\n      )}\n    </>\n  );\n}\n```\n\n```html public/index.html hidden\n<!DOCTYPE html>\n<html>\n<head>\n  <title>My app</title>\n</head>\n<body>\n<!--\n  Purposefully using HTML content that differs from the server-rendered content to trigger recoverable errors.\n-->\n<div id=\"root\">Server content before hydration.</div>\n</body>\n</html>\n```\n</Sandpack>\n\n## 문제 해결 {/*troubleshooting*/}\n\n\n### 다음과 같은 오류가 발생합니다: \"You passed a second argument to root.render\" {/*im-getting-an-error-you-passed-a-second-argument-to-root-render*/}\n\n`hydrateRoot` 옵션을 `root.render(...)`에 전달하는 실수가 흔히 일어나곤 합니다.\n\n<ConsoleBlock level=\"error\">\n\nWarning: You passed a second argument to root.render(…) but it only accepts one argument.\n\n</ConsoleBlock>\n\n수정하려면 루트 옵션을 `root.render(...)` 대신  `hydrateRoot(...)`에 전달하세요.\n```js {2,5}\n// 🚩 잘못된 방법: `root.render`는 하나의 인수만 받습니다.\nroot.render(App, {onUncaughtError});\n\n// ✅ 올바른 방법: 옵션을 `createRoot`에 전달하세요.\nconst root = hydrateRoot(container, <App />, {onUncaughtError});\n```\n"
  },
  {
    "path": "src/content/reference/react-dom/client/index.md",
    "content": "---\ntitle: Client React DOM API\n---\n\n<Intro>\n\n`react-dom/client` API를 사용하면 클라이언트(브라우저)에서 React 컴포넌트를 렌더링할 수 있습니다. 이 API는 일반적으로 앱의 최상위 레벨에서 React 트리를 초기화하는 데 사용됩니다. [프레임워크](/learn/creating-a-react-app#full-stack-frameworks)가 대신 호출할 수도 있습니다. 대부분의 컴포넌트는 이를 import하거나 사용할 필요가 없습니다.\n\n</Intro>\n\n---\n\n## 클라이언트 API {/*client-apis*/}\n\n* [`createRoot`](/reference/react-dom/client/createRoot)를 사용하면 브라우저 DOM 노드 안에 React 컴포넌트를 표시하는 루트를 생성할 수 있습니다.\n* [`hydrateRoot`](/reference/react-dom/client/hydrateRoot)를 사용하면 이전에 [`react-dom/server`](/reference/react-dom/server)에 의해 생성된 HTML 콘텐츠가 있는 브라우저 DOM 노드 안에 React 컴포넌트를 표시할 수 있습니다.\n---\n\n## 지원 브라우저 {/*browser-support*/}\n\nReact는 Internet Explorer 9 이상을 포함한 모든 대중적인 브라우저를 지원합니다. IE 9, IE 10 이하 같은 구형 브라우저에서는 일부 폴리필이 필요합니다.\n"
  },
  {
    "path": "src/content/reference/react-dom/components/common.md",
    "content": "---\ntitle: \"공통 컴포넌트 (예: <div>)\"\n---\n\n<Intro>\n\n모든 내장 브라우저 컴포넌트 (예: [`<div>`](https://developer.mozilla.org/ko/docs/Web/HTML/Element/div))는 공통의 Props와 이벤트를 지원합니다.\n\n</Intro>\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### 공통 컴포넌트 (예: `<div>`) {/*common*/}\n\n```js\n<div className=\"wrapper\">Some content</div>\n```\n\n[아래 예시를 참고하세요.](#usage)\n\n#### Props {/*common-props*/}\n\n아래의 특별한 React Props는 내장된 모든 컴포넌트에서 지원합니다.\n\n* `children`: React 노드(요소, 문자열, 숫자, [Portal](/reference/react-dom/createPortal), `null`, `undefined`, 불리언 타입과 같은 빈 노드, 또는 다른 React 노드의 배열) 입니다. 컴포넌트 내부의 콘텐츠를 지정합니다. JSX를 사용하면 일반적으로 `<div><span /></div>`처럼 태그를 중첩하여 `children` Prop을 암묵적으로 지정합니다.\n\n* `dangerouslySetInnerHTML`: 원시 HTML 문자열이 포함된`{ __html: '<p>some html</p>' }` 형식의 객체입니다. DOM 노드의 [`innerHTML`](https://developer.mozilla.org/ko/docs/Web/API/Element/innerHTML) 프로퍼티를 덮어쓰고 전달된 HTML을 내부에 표시합니다. 이것은 매우 주의해서 사용해야 합니다. 내부 HTML을 신뢰할 수 없는 경우 (예: 사용자 데이터를 기반으로 하는 경우) [XSS](https://ko.wikipedia.org/wiki/%EC%82%AC%EC%9D%B4%ED%8A%B8_%EA%B0%84_%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8C%85) 취약점이 발생할 수 있습니다. [`dangerouslySetInnerHTML`에 대해 더 알아보려면 읽어보세요.](#dangerously-setting-the-inner-html)\n\n* `ref`: [`useRef`](/reference/react/useRef)나 [`createRef`](/reference/react/createRef)의 `ref` 객체, 또는 [`ref` 콜백 함수](#ref-callback)거나 [legacy refs](https://ko.legacy.reactjs.org/docs/refs-and-the-dom.html#legacy-api-string-refs)의 문자열입니다. 해당 `ref`는 해당 노드의 DOM 요소로 채워집니다. [`ref`를 사용하여 DOM을 조작하는 방법에 대해 더 자세히 알아보세요.](#manipulating-a-dom-node-with-a-ref)\n\n* `suppressContentEditableWarning`: 불리언 타입입니다. `true` 일 때, 일반적으로 같이 사용하지 않는 `children`과 `contentEditable={true}`가 모두 존재하는 요소에 대해 React에서 발생하는 경고를 나타내지 않습니다. 이는`contentEditable` 콘텐츠를 수동으로 관리하는 텍스트 입력 라이브러리를 빌드할 때 사용됩니다.\n\n* `suppressHydrationWarning`: 불리언 타입입니다. [서버 렌더링](/reference/react-dom/server)을 사용할 때, 일반적으로 서버와 클라이언트가 서로 다른 콘텐츠를 렌더링하면 경고가 표시됩니다. 일부 드문 사례(예: 타임스탬프)에서는 정확한 일치를 보장하기가 매우 어렵거나 불가능합니다. `suppressHydrationWarning`를 `true`로 설정하면, React는 해당 요소의 어트리뷰트와 콘텐츠가 일치하지 않아도 경고를 표시하지 않습니다. 이는 한 단계의 깊이에서만 작동하며, 탈출구로 사용하기 위한 것입니다. 과도하게 사용하지 마세요. [Suppressing Hydration 오류에 대해서 읽어보세요.](/reference/react-dom/client/hydrateRoot#suppressing-unavoidable-hydration-mismatch-errors)\n\n* `style`: `{ fontWeight: 'bold', margin: 20 }`와 같이 CSS 스타일이 있는 객체입니다. DOM의 [`style`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/style) 프로퍼티에서 `fontWeight` 대신 `font-weight`로 작성하는 것과 마찬가지로 CSS 프로퍼티의 이름도 `camelCase`로 작성해야 합니다. 또한 문자열이나 숫자를 값으로 전달할 수 있습니다. `width: 100`와 같은 숫자를 전달한다면 React는 [단위가 없는 프로퍼티](https://github.com/facebook/react/blob/81d4ee9ca5c405dce62f64e61506b8e155f38d8d/packages/react-dom-bindings/src/shared/CSSProperty.js#L8-L57)가 아니라면 자동으로 `px` (\"픽셀\")로 값을 추가합니다. `style`은 스타일 값을 미리 알 수 없는 동적 스타일에만 사용하는 것을 권장합니다. 그 외의 경우에는 `className`을 사용하여 일반 CSS 클래스를 사용하는 것이 더 효율적입니다. [`className`과 `style`에 대해서 더 자세히 알아보세요.](#applying-css-styles)\n\n아래의 표준 DOM Props는 내장된 모든 컴포넌트에서 지원합니다.\n\n* [`accessKey`](https://developer.mozilla.org/ko/docs/Web/HTML/Global_attributes/accesskey): 문자열 타입입니다. 요소의 바로 가기 키를 지정합니다. [일반적으로 권장하지 않습니다.](https://developer.mozilla.org/ko/docs/Web/HTML/Global_attributes/accesskey)\n* [`aria-*`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes): ARIA 속성을 사용하면 이 요소에 대한 접근성 트리 정보를 지정할 수 있습니다. 전체적인 레퍼런스는 [ARIA 어트리뷰트](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes)를 참조하세요. React에서 모든 ARIA 어트리뷰트의 이름은 HTML에서의 이름과 완전히 동일합니다.\n* [`autoCapitalize`](https://developer.mozilla.org/ko/docs/Web/HTML/Global_attributes/autocapitalize): 문자열 타입입니다. 사용자의 입력을 대문자로 표시할지 여부와 방법을 지정합니다.\n* [`className`](https://developer.mozilla.org/ko/docs/Web/API/Element/className): 문자열 타입입니다. 요소의 CSS 클래스 이름을 지정합니다. [CSS 스타일 적용에 대해 자세히 알아보세요.](#applying-css-styles)\n* [`contentEditable`](https://developer.mozilla.org/ko/docs/Web/HTML/Global_attributes/contenteditable): 불리언 타입입니다. `true`일 때 브라우저는 사용자가 렌더링 된 요소를 직접 편집할 수 있도록 합니다. 이는 [Lexical](https://lexical.dev/)과 같은 서식이 있는 텍스트 입력 라이브러리를 구현하는 데 사용됩니다. React는 사용자가 편집한 후에 React가 그 내용을 업데이트할 수 없기 때문에 `contentEditable={true}`가 있는 요소에 React의 자식을 전달하려고 하면 경고를 표시합니다.\n* [`data-*`](https://developer.mozilla.org/ko/docs/Web/HTML/Global_attributes/data-*): 데이터 속성을 사용하면 요소에 일부 문자열 데이터를 첨부할 수 있습니다. (예: `data-fruit=\"banana\"`) React에서는 일반적으로 Props나 State에서 데이터를 읽어오기 때문에 일반적으로 사용되지는 않습니다.\n* [`dir`](https://developer.mozilla.org/ko/docs/Web/HTML/Global_attributes/dir): `'ltr'` 또는 `'rtl'`입니다. 요소의 텍스트 방향을 지정합니다.\n* [`draggable`](https://developer.mozilla.org/ko/docs/Web/HTML/Global_attributes/draggable): 불리언 타입입니다. 요소의 드래그 가능 여부를 지정합니다. [HTML 드래그 앤 드롭 API](https://developer.mozilla.org/ko/docs/Web/API/HTML_Drag_and_Drop_API)의 일부입니다.\n* [`enterKeyHint`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/enterKeyHint): 문자열 타입입니다. 가상 키보드의 입력 키에 어떤 동작을 표시할지 지정합니다.\n* [`htmlFor`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLLabelElement/htmlFor): 문자열 타입입니다. [`<label>`](https://developer.mozilla.org/ko/docs/Web/HTML/Element/label) 이나 [`<output>`](https://developer.mozilla.org/ko/docs/Web/HTML/Element/output)의 경우 [label을 일부 동작에 연결할 수 있습니다.](/reference/react-dom/components/input#providing-a-label-for-an-input) 이는 [HTML 어트리뷰트의 `for` 과 동일합니다.](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/for) React는 HTML 어트리뷰트의 이름 대신 `htmlFor`와 같은 표준 DOM 프로퍼티의 이름을 사용합니다.\n* [`hidden`](https://developer.mozilla.org/ko/docs/Web/HTML/Global_attributes/hidden): 불리언 혹은 문자열 타입입니다. 요소를 숨길지에 대한 여부를 지정합니다.\n* [`id`](https://developer.mozilla.org/ko/docs/Web/HTML/Global_attributes/id): 문자열 타입입니다. 요소의 고유 식별자를 지정하여 나중에 찾거나 다른 요소와 연결하는 데 사용할 수 있습니다. 동일한 컴포넌트의 여러 인스턴스 간의 충돌을 피하고자 [`useId`](/reference/react/useId)로 생성합니다.\n* [`is`](https://developer.mozilla.org/ko/docs/Web/HTML/Global_attributes/is): 문자열 타입입니다. 지정하게 되면 컴포넌트가 [사용자 정의 요소](/reference/react-dom/components#custom-html-elements)처럼 작동합니다.\n* [`inputMode`](https://developer.mozilla.org/ko/docs/Web/HTML/Global_attributes/inputmode): 문자열 타입입니다. 표시할 키보드의 종류(예시: 텍스트, 숫자 또는 전화번호)를 지정합니다.\n* [`itemProp`](https://developer.mozilla.org/ko/docs/Web/HTML/Global_attributes/itemprop): 문자열 타입입니다. 구조화된 데이터 크롤러에 대해 요소가 나타내는 속성을 지정합니다.\n* [`lang`](https://developer.mozilla.org/ko/docs/Web/HTML/Global_attributes/lang): 문자열 타입입니다. 요소의 언어를 지정합니다.\n* [`onAnimationEnd`](https://developer.mozilla.org/en-US/docs/Web/API/Element/animationend_event): [`AnimationEvent` 핸들러](#animationevent-handler) 함수입니다. CSS 애니메이션이 완료될 때 발생합니다.\n* `onAnimationEndCapture`: [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 `onAnimationEnd`의 버전입니다.\n* [`onAnimationIteration`](https://developer.mozilla.org/en-US/docs/Web/API/Element/animationiteration_event): [`AnimationEvent` 핸들러](#animationevent-handler) 함수입니다. CSS 애니메이션의 반복이 끝나고 다른 애니메이션이 시작될 때 발생합니다.\n* `onAnimationIterationCapture`: [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 `onAnimationIteration`의 버전입니다.\n* [`onAnimationStart`](https://developer.mozilla.org/en-US/docs/Web/API/Element/animationstart_event): [`AnimationEvent` 핸들러](#animationevent-handler) 함수입니다. CSS 애니메이션이 시작될 때 발생합니다.\n* `onAnimationStartCapture`: `onAnimationStart`입니다. 그러나 [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행됩니다.\n* [`onAuxClick`](https://developer.mozilla.org/en-US/docs/Web/API/Element/auxclick_event): [`MouseEvent` 핸들러](#mouseevent-handler) 함수입니다. 기본 포인터가 아닌 버튼을 클릭했을 때 발생합니다.\n* `onAuxClickCapture`: `onAuxClick`의 [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 버전입니다.\n* `onBeforeInput`: [`InputEvent` 핸들러](#inputevent-handler) 함수입니다. 편집할 수 있는 요소의 값이 수정되기 전에 발생합니다. React는 아직 네이티브 [`beforeinput`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/beforeinput_event) 이벤트를 *사용하지 않습니다.* 대신 다른 이벤트를 사용하여 폴리필을 시도합니다.\n* `onBeforeInputCapture`: `onBeforeInput`의 [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 버전입니다.\n* `onBlur`: [`FocusEvent` 핸들러](#focusevent-handler) 함수입니다. 요소가 포커싱을 잃었을 때 발생합니다. 브라우저에 내장된 [`blur`](https://developer.mozilla.org/ko/docs/Web/API/Element/blur_event) 이벤트와 달리 React에서는 `onBlur` 이벤트가 버블링을 발생시킵니다.\n* `onBlurCapture`: `onBlur`의 [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 버전입니다.\n* [`onClick`](https://developer.mozilla.org/ko/docs/Web/API/Element/click_event): [`MouseEvent` 핸들러](#mouseevent-handler) 함수입니다. 포인팅 디바이스에서 기본 버튼이 클릭 되었을 때 발생합니다.\n* `onClickCapture`: `onClick`의 [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 버전입니다.\n* [`onCompositionStart`](https://developer.mozilla.org/en-US/docs/Web/API/Element/compositionstart_event):  [`CompositionEvent` 핸들러](#compositionevent-handler) 함수입니다. [입력 메서드 편집기](https://developer.mozilla.org/en-US/docs/Glossary/Input_method_editor)가 새로운 구성 세션을 시작할 때 발생합니다.\n* `onCompositionStartCapture`: `onCompositionStart`의 [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 버전입니다.\n* [`onCompositionEnd`](https://developer.mozilla.org/en-US/docs/Web/API/Element/compositionend_event):  [`CompositionEvent` 핸들러](#compositionevent-handler) 함수입니다. [입력 메서드 편집기](https://developer.mozilla.org/en-US/docs/Glossary/Input_method_editor)가 구성 세션을 완료하거나 취소할 때 발생합니다.\n* `onCompositionEndCapture`: `onCompositionEnd`의 [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 버전입니다.\n* [`onCompositionUpdate`](https://developer.mozilla.org/en-US/docs/Web/API/Element/compositionupdate_event):  [`CompositionEvent` 핸들러](#compositionevent-handler) 함수입니다. [입력 메서드 편집기](https://developer.mozilla.org/en-US/docs/Glossary/Input_method_editor)에 새로운 문자가 입력되면 발생합니다.\n* `onCompositionUpdateCapture`: `onCompositionUpdate`의 [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 버전입니다.\n* [`onContextMenu`](https://developer.mozilla.org/ko/docs/Web/API/Element/contextmenu_event): [`MouseEvent` 핸들러](#mouseevent-handler) 함수입니다. 컨텍스트 메뉴를 열려고 할 때 발생합니다.\n* `onContextMenuCapture`: `onContextMenu`의 [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 버전입니다.\n* [`onCopy`](https://developer.mozilla.org/ko/docs/Web/API/Element/copy_event): [`ClipboardEvent` 핸들러](#clipboardevent-handler) 함수입니다. 클립보드에 무언가를 복사하려고 할 때 발생합니다.\n* `onCopyCapture`: `onCopy`의 [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 버전입니다.\n* [`onCut`](https://developer.mozilla.org/ko/docs/Web/API/Element/cut_event): [`ClipboardEvent` 핸들러](#clipboardevent-handler) 함수입니다. 클립보드에서 무언가를 잘라내려고 할 때 발생합니다.\n* `onCutCapture`: `onCut`의 [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 버전입니다.\n* `onDoubleClick`: [`MouseEvent` 핸들러](#mouseevent-handler) 함수입니다. 두 번 클릭하면 발생합니다. 브라우저의 [`dblclick` 이벤트](https://developer.mozilla.org/en-US/docs/Web/API/Element/dblclick_event)에 해당합니다.\n* `onDoubleClickCapture`: `onDoubleClick`의 [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 버전입니다.\n* [`onDrag`](https://developer.mozilla.org/ko/docs/Web/API/HTMLElement/drag_event): [`DragEvent` 핸들러](#dragevent-handler) 함수입니다. 무언가를 드래그하는 동안 실행됩니다.\n* `onDragCapture`: `onDrag`의 [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 버전입니다.\n* [`onDragEnd`](https://developer.mozilla.org/ko/docs/Web/API/HTMLElement/dragend_event): [`DragEvent` 핸들러](#dragevent-handler) 함수입니다. 드래그를 멈추면 발생합니다.\n* `onDragEndCapture`: `onDragEnd`의 [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 버전입니다.\n* [`onDragEnter`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dragenter_event): [`DragEvent` 핸들러](#dragevent-handler) 함수입니다. 드래그한 콘텐츠가 유효한 드롭 대상에 들어가면 발생합니다.\n* `onDragEnterCapture`: `onDragEnter`의 [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 버전입니다.\n* [`onDragOver`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dragover_event): [`DragEvent` 핸들러](#dragevent-handler) 함수입니다. 드래그된 콘텐츠를 드래그하는 동안 유효한 드롭 대상에서 발생합니다. 드롭을 허용하려면 여기서 `e.preventDefault()`를 호출해야 합니다.\n* `onDragOverCapture`: `onDragOver`의 [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 버전입니다.\n* [`onDragStart`](https://developer.mozilla.org/ko/docs/Web/API/HTMLElement/dragstart_event): [`DragEvent` 핸들러](#dragevent-handler) 함수입니다. 요소를 드래그하기 시작할 때 발생합니다.\n* `onDragStartCapture`: `onDragStart`의 [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 버전입니다.\n* [`onDrop`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/drop_event): [`DragEvent` 핸들러](#dragevent-handler) 함수입니다. 유효한 드롭 대상에 무언가를 떨어뜨리면 발동합니다.\n* `onDropCapture`: `onDrop`의 [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 버전입니다.\n* `onFocus`: [`FocusEvent` 핸들러](#focusevent-handler) 함수입니다. 요소가 포커싱을 얻었을 때 발생합니다. 브라우저에 내장된 [`focus`](https://developer.mozilla.org/en-US/docs/Web/API/Element/focus_event) 이벤트와 달리 React에서는 `onFocus` 이벤트가 버블링을 발생시킵니다.\n* `onFocusCapture`: `onFocus`의 [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 버전입니다.\n* [`onGotPointerCapture`](https://developer.mozilla.org/en-US/docs/Web/API/Element/gotpointercapture_event): [`PointerEvent` 핸들러](#pointerevent-handler) 함수입니다. 요소가 프로그래밍 방식으로 포인터를 캡처할 때 발생합니다.\n* `onGotPointerCaptureCapture`: `onGotPointerCapture`의 [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 버전입니다.\n* [`onKeyDown`](https://developer.mozilla.org/ko/docs/Web/API/Element/keydown_event): [`KeyboardEvent` 핸들러](#keyboardevent-handler) 함수입니다. 키를 누르면 실행됩니다.\n* `onKeyDownCapture`: `onKeyDown`의 [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 버전입니다.\n* [`onKeyPress`](https://developer.mozilla.org/en-US/docs/Web/API/Element/keypress_event): [`KeyboardEvent` 핸들러](#keyboardevent-handler) 함수입니다. 사용되지 않습니다. 대신 `onKeyDown` 또는 `onBeforeInput`을 사용하세요.\n* `onKeyPressCapture`: `onKeyPress`의 [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 버전입니다.\n* [`onKeyUp`](https://developer.mozilla.org/ko/docs/Web/API/Element/keyup_event): [`KeyboardEvent` 핸들러](#keyboardevent-handler) 함수입니다. 키를 놓으면 실행됩니다.\n* `onKeyUpCapture`: `onKeyUp`의 [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 버전입니다.\n* [`onLostPointerCapture`](https://developer.mozilla.org/en-US/docs/Web/API/Element/lostpointercapture_event): [`PointerEvent` 핸들러](#pointerevent-handler) 함수입니다. 요소가 포인터 캡처를 중지하면 발생합니다.\n* `onLostPointerCaptureCapture`: `onLostPointerCapture`의 [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 버전입니다.\n* [`onMouseDown`](https://developer.mozilla.org/en-US/docs/Web/API/Element/mousedown_event): [`MouseEvent` 핸들러](#mouseevent-handler) 함수입니다. 마우스 포인터를 눌렀을 때 실행됩니다.\n* `onMouseDownCapture`: `onMouseDown`의 [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 버전입니다.\n* [`onMouseEnter`](https://developer.mozilla.org/en-US/docs/Web/API/Element/mouseenter_event): [`MouseEvent` 핸들러](#mouseevent-handler) 함수입니다. 마우스 포인터가 요소 내부로 이동할 때 발생합니다. 캡처 단계가 없습니다. 대신 `onMouseLeave`와 `onMouseEnter`는 떠나는 요소에서 입력되는 요소로 전파됩니다.\n* [`onMouseLeave`](https://developer.mozilla.org/en-US/docs/Web/API/Element/mouseleave_event): [`MouseEvent` 핸들러](#mouseevent-handler) 함수입니다. 마우스 포인터가 요소 외부로 이동하면 발생합니다. 캡처 단계가 없습니다. 대신 `onMouseLeave`와 `onMouseEnter`는 떠나는 요소에서 입력되는 요소로 전파됩니다.\n* [`onMouseMove`](https://developer.mozilla.org/en-US/docs/Web/API/Element/mousemove_event): [`MouseEvent` 핸들러](#mouseevent-handler) 함수입니다. 마우스 포인터의 좌표를 변경할 때 발생합니다.\n* `onMouseMoveCapture`: `onMouseMove`의 [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 버전입니다.\n* [`onMouseOut`](https://developer.mozilla.org/en-US/docs/Web/API/Element/mouseout_event): [`MouseEvent` 핸들러](#mouseevent-handler) 함수입니다. 마우스 포인터가 요소 외부로 이동하거나 하위 요소로 이동하면 발생합니다.\n* `onMouseOutCapture`: `onMouseOut`의 [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 버전입니다.\n* [`onMouseUp`](https://developer.mozilla.org/en-US/docs/Web/API/Element/mouseup_event): [`MouseEvent` 핸들러](#mouseevent-handler) 함수입니다. 마우스 포인터에서 손을 떼면 발생합니다.\n* `onMouseUpCapture`: `onMouseUp`의 [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 버전입니다.\n* [`onPointerCancel`](https://developer.mozilla.org/en-US/docs/Web/API/Element/pointercancel_event): [`PointerEvent` 핸들러](#pointerevent-handler) 함수입니다. 브라우저가 포인터와 상호작용을 취소할 때 발생합니다.\n* `onPointerCancelCapture`: `onPointerCancel`의 [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 버전입니다.\n* [`onPointerDown`](https://developer.mozilla.org/en-US/docs/Web/API/Element/pointerdown_event): [`PointerEvent` 핸들러](#pointerevent-handler) 함수입니다. 포인터가 활성화되면 발생합니다.\n* `onPointerDownCapture`: `onPointerDown`의 [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 버전입니다.\n* [`onPointerEnter`](https://developer.mozilla.org/en-US/docs/Web/API/Element/pointerenter_event): [`PointerEvent` 핸들러](#pointerevent-handler) 함수입니다. 포인터가 요소 내부로 이동할 때 발생합니다. 캡처 단계가 없습니다. 대신 `onPointerLeave`와 `onPointerEnter`는 떠나는 요소에서 입력되는 요소로 전파됩니다.\n* [`onPointerLeave`](https://developer.mozilla.org/en-US/docs/Web/API/Element/pointerleave_event): [`PointerEvent` 핸들러](#pointerevent-handler) 함수입니다. 포인터가 요소 내부로 이동할 때 발생합니다. 캡처 단계가 없습니다. 대신 `onPointerLeave`와 `onPointerEnter`는 떠나는 요소에서 입력되는 요소로 전파됩니다.\n* [`onPointerMove`](https://developer.mozilla.org/en-US/docs/Web/API/Element/pointermove_event): [`PointerEvent` 핸들러](#pointerevent-handler) 함수입니다. 포인터의 좌표를 변경할 때 발생합니다.\n* `onPointerMoveCapture`: `onPointerMove`의 [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 버전입니다.\n* [`onPointerOut`](https://developer.mozilla.org/en-US/docs/Web/API/Element/pointerout_event): [`PointerEvent` 핸들러](#pointerevent-handler) 함수입니다. 포인터가 요소 외부로 이동하거나 포인터 상호 작용이 취소되는 경우, 그리고 [그 외 몇 가지 이유](https://developer.mozilla.org/en-US/docs/Web/API/Element/pointerout_event)로 인해 발생합니다.\n* `onPointerOutCapture`: `onPointerOut`의 [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 버전입니다.\n* [`onPointerUp`](https://developer.mozilla.org/en-US/docs/Web/API/Element/pointerup_event): [`PointerEvent` 핸들러](#pointerevent-handler) 함수입니다. 포인터가 더 이상 활성화되지 않을 때 발생합니다.\n* `onPointerUpCapture`: `onPointerUp`의 [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 버전입니다.\n* [`onPaste`](https://developer.mozilla.org/ko/docs/Web/API/Element/paste_event): [`ClipboardEvent` 핸들러](#clipboardevent-handler) 함수입니다. 사용자가 클립보드에서 붙여 넣으려고 할 때 발생합니다.\n* `onPasteCapture`: `onPaste`의 [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 버전입니다.\n* [`onScroll`](https://developer.mozilla.org/en-US/docs/Web/API/Element/scroll_event): [`Event` 핸들러](#event-handler) 함수입니다. 요소를 스크롤 할 때 발생합니다. 이 이벤트는 버블링이 발생하지 않습니다.\n* `onScrollCapture`: `onScroll`의 [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 버전입니다.\n* [`onSelect`](https://developer.mozilla.org/ko/docs/Web/API/HTMLInputElement/select_event): [`Event` 핸들러](#event-handler) 함수입니다. 입력 변경과 같이 편집할 수 있는 요소 내부에서 선택되면 실행됩니다. React는 `onSelect` 이벤트를 `contentEditable={true}` 요소에도 작동하도록 확장합니다. 또한 React는 빈 선택과 (선택에 영향을 줄 수 있는) 편집 시에도 발동되도록 확장합니다.\n* `onSelectCapture`: `onSelect`의 [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 버전입니다.\n* [`onTouchCancel`](https://developer.mozilla.org/en-US/docs/Web/API/Element/touchcancel_event): [`TouchEvent` 핸들러](#touchevent-handler) 함수입니다. 브라우저가 터치 상호작용을 취소할 때 발생합니다.\n* `onTouchCancelCapture`: `onTouchCancel`의 [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 버전입니다.\n* [`onTouchEnd`](https://developer.mozilla.org/en-US/docs/Web/API/Element/touchend_event): [`TouchEvent` 핸들러](#touchevent-handler) 함수입니다. 하나 이상의 터치 포인트가 사라지면 발생합니다.\n* `onTouchEndCapture`: `onTouchEnd`의 [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 버전입니다.\n* [`onTouchMove`](https://developer.mozilla.org/en-US/docs/Web/API/Element/touchmove_event): [`TouchEvent` 핸들러](#touchevent-handler) 함수입니다. 하나 이상의 터치 포인트가 이동하면 발생합니다.\n* `onTouchMoveCapture`: `onTouchMove`의 [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 버전입니다.\n* [`onTouchStart`](https://developer.mozilla.org/en-US/docs/Web/API/Element/touchstart_event): [`TouchEvent` 핸들러](#touchevent-handler) 함수입니다. 하나 이상의 터치 포인트가 위치하면 발생합니다.\n* `onTouchStartCapture`: `onTouchStart`의 [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 버전입니다.\n* [`onTransitionEnd`](https://developer.mozilla.org/en-US/docs/Web/API/Element/transitionend_event): [`TransitionEvent` 헨들러](#transitionevent-handler) 함수입니다. CSS 전환을 완료하면 발생합니다.\n* `onTransitionEndCapture`: `onTransitionEnd`의 [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 버전입니다.\n* [`onWheel`](https://developer.mozilla.org/en-US/docs/Web/API/Element/wheel_event): [`WheelEvent` 핸들러](#wheelevent-handler) 함수입니다. 휠 버튼을 돌리면 발생합니다.\n* `onWheelCapture`: `onWheel`의 [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 버전입니다.\n* [`role`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles): 문자열 타입입니다. 보조 기술에 대한 요소의 역할을 명시적으로 지정합니다.\n* [`slot`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles): 문자열 타입입니다. 그림자 DOM을 사용할 때 슬롯의 이름을 지정합니다. React에서는 일반적으로 JSX를 프로퍼티로 전달하여 동일한 패턴을 얻을 수 있습니다. (예시: `<Layout left={<Sidebar />} right={<Content />} />`.\n* [`spellCheck`](https://developer.mozilla.org/ko/docs/Web/HTML/Global_attributes/spellcheck): 불리언 또는 null 타입입니다. `true` 또는 `false`로 설정하여 맞춤법 검사를 활성화 또는 비활성화합니다.\n* [`tabIndex`](https://developer.mozilla.org/ko/docs/Web/HTML/Global_attributes/tabindex): 숫자 타입입니다. 기본 탭 버튼 동작을 재정의합니다. [`-1`과 `0` 이외의 값은 사용하지 마십시오.](https://www.tpgi.com/using-the-tabindex-attribute/)\n* [`title`](https://developer.mozilla.org/ko/docs/Web/HTML/Global_attributes/title): 문자열 타입입니다. 요소의 툴팁 텍스트를 지정합니다.\n* [`translate`](https://developer.mozilla.org/ko/docs/Web/HTML/Global_attributes/translate): `'yes'`나`'no'` 중 하나입니다. `'no'` 를 전달하면 요소의 콘텐츠가 번역에서 제외됩니다.\n\n사용자 정의 어트리뷰트를 Props로 전달할 수도 있습니다. (예: `mycustomprop=\"someValue\"`) 이는 서드파티 라이브러리와 통합할 때 유용할 수 있습니다. 사용자 정의 어트리뷰트의 이름은 소문자여야 하며 `on`으로 시작하지 않아야 합니다. 값은 문자열로 변환됩니다. `null` 또는 `undefined`를 전달하면 사용자 정의 어트리뷰트가 제거됩니다.\n\n다음의 이벤트는 [`<form>`](https://developer.mozilla.org/ko/docs/Web/HTML/Element/form) 요소에 대해서만 발생합니다.\n\n* [`onReset`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/reset_event): [`Event` 핸들러](#event-handler) 함수입니다. 폼을 재설정할 때 발생합니다.\n* `onResetCapture`: `onReset`의 [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 버전입니다.\n* [`onSubmit`](https://developer.mozilla.org/ko/docs/Web/API/HTMLFormElement/submit_event): [`Event` 핸들러](#event-handler) 함수입니다. 폼을 제출할 때 발생합니다.\n* `onSubmitCapture`: `onSubmit`의 [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 버전입니다.\n\n다음의 이벤트는 [`<dialog>`](https://developer.mozilla.org/ko/docs/Web/HTML/Element/dialog) 요소에 대해서만 발생합니다. 그리고 브라우저 이벤트와 달리 React에서는 버블링이 발생합니다.\n\n* [`onCancel`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/cancel_event): [`Event` 핸들러](#event-handler) 함수입니다. 사용자가 대화상자를 닫으려고 할 때 발생합니다.\n* `onCancelCapture`: `onCancel`의 [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 버전입니다.\n* [`onClose`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/close_event): [`Event` 핸들러](#event-handler) 함수입니다. 대화 상자가 닫혔을 때 발생합니다.\n* `onCloseCapture`: `onClose`의 [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 버전입니다.\n\n다음의 이벤트는 [`<details>`](https://developer.mozilla.org/ko/docs/Web/HTML/Element/details) 요소에 대해서만 발생합니다. 그리고 브라우저 이벤트와 달리 React에서는 버블링이 발생합니다.\n\n* [`onToggle`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLDetailsElement/toggle_event): [`Event` 핸들러](#event-handler) 함수입니다. 세부사항을 토글할 때 발생합니다.\n* `onToggleCapture`: `onToggle`의 [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 버전입니다.\n\n다음의 이벤트는 [`<img>`](https://developer.mozilla.org/ko/docs/Web/HTML/Element/img), [`<iframe>`](https://developer.mozilla.org/ko/docs/Web/HTML/Element/iframe), [`<object>`](https://developer.mozilla.org/ko/docs/Web/HTML/Element/object), [`<embed>`](https://developer.mozilla.org/ko/docs/Web/HTML/Element/embed), [`<link>`](https://developer.mozilla.org/ko/docs/Web/HTML/Element/link) 그리고 [SVG `<image>`](https://developer.mozilla.org/ko/docs/Web/SVG/Tutorial/SVG_Image_Tag) 요소들에 대해서 발생합니다. 그리고 브라우저 이벤트와 달리 React에서는 버블링이 발생합니다.\n\n* `onLoad`: [`Event` 핸들러](#event-handler) 함수입니다. 자원이 로드되면 발생합니다.\n* `onLoadCapture`: `onLoad`의 [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 버전입니다.\n* [`onError`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/error_event): [`Event` 핸들러](#event-handler) 함수입니다. 자원을 로드할 수 없을 때 발생합니다.\n* `onErrorCapture`: `onError`의 [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 버전입니다.\n\n다음의 이벤트는 [`<audio>`](https://developer.mozilla.org/ko/docs/Web/HTML/Element/audio) 및 [`<video>`](https://developer.mozilla.org/ko/docs/Web/HTML/Element/video)와 같은 자원에 대해 발생합니다. 그리고 브라우저 이벤트와 달리 React에서는 버블링이 발생합니다.\n\n* [`onAbort`](https://developer.mozilla.org/ko/docs/Web/API/HTMLMediaElement/abort_event): [`Event` 핸들러](#event-handler) 함수입니다. 자원이 완전히 로드되지 않았지만 오류로 인한 것이 아닌 경우 발생합니다.\n* `onAbortCapture`: `onAbort`의 [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 버전입니다.\n* [`onCanPlay`](https://developer.mozilla.org/ko/docs/Web/API/HTMLMediaElement/canplay_event): [`Event` 핸들러](#event-handler) 함수입니다. 재생을 시작하기에 충분한 데이터가 있지만 버퍼링 없이 끝까지 재생할 수 없을 때 발생합니다.\n* `onCanPlayCapture`: `onCanPlay`의 [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 버전입니다.\n* [`onCanPlayThrough`](https://developer.mozilla.org/ko/docs/Web/API/HTMLMediaElement/canplaythrough_event): [`Event` 핸들러](#event-handler) 함수입니다. 데이터가 충분하여 끝까지 버퍼링 없이 재생을 시작할 수 있을 때 발생합니다.\n* `onCanPlayThroughCapture`: `onCanPlayThrough`의 [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 버전입니다.\n* [`onDurationChange`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/durationchange_event): [`Event` 핸들러](#event-handler) 함수입니다. 미디어 지속 시간이 업데이트되면 발생합니다.\n* `onDurationChangeCapture`: `onDurationChange`의 [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 버전입니다.\n* [`onEmptied`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/emptied_event): [`Event` 핸들러](#event-handler) 함수입니다. 미디어가 비어있을 때 발생합니다.\n* `onEmptiedCapture`: `onEmptied`의 [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 버전입니다.\n* [`onEncrypted`](https://w3c.github.io/encrypted-media/#dom-evt-encrypted): [`Event` 핸들러](#event-handler) 함수입니다. 브라우저에서 암호화된 미디어를 발견하면 발생합니다.\n* `onEncryptedCapture`: `onEncrypted`의 [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 버전입니다.\n* [`onEnded`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/ended_event): [`Event` 핸들러](#event-handler) 함수입니다. 재생할 내용이 남아 있지 않아 재생이 중지되면 발생합니다.\n* `onEndedCapture`: `onEnded`의 [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 버전입니다.\n* [`onError`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/error_event): [`Event` 핸들러](#event-handler) 함수입니다. 리소스를 로딩할 수 없을 때 발생합니다.\n* `onErrorCapture`: `onError`의 [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 버전입니다.\n* [`onLoadedData`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/loadeddata_event): [`Event` 핸들러](#event-handler) 함수입니다. 현재 재생 프레임이 로딩되면 발생합니다.\n* `onLoadedDataCapture`: `onLoadedData`의 [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 버전입니다.\n* [`onLoadedMetadata`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/loadedmetadata_event): [`Event` 핸들러](#event-handler) 함수입니다. 메타데이터가 로딩될 때 발생합니다.\n* `onLoadedMetadataCapture`: `onLoadedMetadata`의 [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 버전입니다.\n* [`onLoadStart`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/loadstart_event): [`Event` 핸들러](#event-handler) 함수입니다. 브라우저가 자원 로딩을 시작하면 발생합니다.\n* `onLoadStartCapture`: `onLoadStart`의 [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 버전입니다.\n* [`onPause`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/pause_event): [`Event` 핸들러](#event-handler) 함수입니다. 미디어가 일시 중지되었을 때 발생합니다.\n* `onPauseCapture`: `onPause`의 [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 버전입니다.\n* [`onPlay`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/play_event): [`Event` 핸들러](#event-handler) 함수입니다. 미디어가 더 이상 일시 정지되지 않을 때 발생합니다.\n* `onPlayCapture`: `onPlay`의 [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 버전입니다.\n* [`onPlaying`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/playing_event): [`Event` 핸들러](#event-handler) 함수입니다. 미디어 재생이 시작되거나 재시작될 때 발생합니다.\n* `onPlayingCapture`: `onPlaying`의 [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 버전입니다.\n* [`onProgress`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/progress_event): [`Event` 핸들러](#event-handler) 함수입니다. 자원이 로드되는 동안 주기적으로 실행됩니다.\n* `onProgressCapture`: `onProgress`의 [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 버전입니다.\n* [`onRateChange`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/ratechange_event): [`Event` 핸들러](#event-handler) 함수입니다. 재생 속도가 변경되면 발생합니다.\n* `onRateChangeCapture`: `onRateChange`의 [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 버전입니다.\n* `onResize`: [`Event` 핸들러](#event-handler) 함수입니다. 동영상 크기가 변경될 때 발생합니다.\n* `onResizeCapture`: `onResize`의 [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 버전입니다.\n* [`onSeeked`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/seeked_event): [`Event` 핸들러](#event-handler) 함수입니다. 탐색 작업이 완료되면 발생합니다.\n* `onSeekedCapture`: `onSeeked`의 [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 버전입니다.\n* [`onSeeking`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/seeking_event): [`Event` 핸들러](#event-handler) 함수입니다. 탐색 작업이 시작될 때 발생합니다.\n* `onSeekingCapture`: `onSeeking`의 [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 버전입니다.\n* [`onStalled`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/stalled_event): [`Event` 핸들러](#event-handler) 함수입니다. 브라우저가 데이터를 기다리지만 계속 로드되지 않을 때 발생합니다.\n* `onStalledCapture`: `onStalled`의 [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 버전입니다.\n* [`onSuspend`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/suspend_event): [`Event` 핸들러](#event-handler) 함수입니다. 자원 로딩이 일시 중단되었을 때 발생합니다.\n* `onSuspendCapture`: `onSuspend`의 [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 버전입니다.\n* [`onTimeUpdate`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/timeupdate_event): [`Event` 핸들러](#event-handler) 함수입니다. 현재 재생 시간이 업데이트될 때 발생합니다.\n* `onTimeUpdateCapture`: `onTimeUpdate`의 [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 버전입니다.\n* [`onVolumeChange`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/volumechange_event): [`Event` 핸들러](#event-handler) 함수입니다. 볼륨이 변경되었을 때 발생합니다.\n* `onVolumeChangeCapture`: `onVolumeChange`의 [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 버전입니다.\n* [`onWaiting`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/waiting_event): [`Event` 핸들러](#event-handler) 함수입니다. 일시적인 데이터 부족으로 인해 재생이 중지된 경우 발생합니다.\n* `onWaitingCapture`: `onWaiting`의 [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 발생하는 버전입니다.\n\n#### 주의 사항 {/*common-caveats*/}\n\n- `children`과 `dangerouslySetInnerHTML`을 동시에 전달할 수 없습니다.\n- 일부 이벤트(예: `onAbort`, `onLoad`)는 브라우저에서 버블링이 발생하지 않지만, React에서는 버블링이 발생합니다.\n\n---\n\n### `ref` 콜백 함수 {/*ref-callback*/}\n\n[`useRef`](/reference/react/useRef#manipulating-the-dom-with-a-ref) 에서 반환되는 `ref` 객체 대신 `ref` 속성에 함수를 전달할 수 있습니다.\n\n```js\n<div ref={(node) => {\n  console.log('Attached', node);\n\n  return () => {\n    console.log('Clean up', node)\n  }\n}}>\n```\n\n[`ref` 콜백을 사용하는 예시를 확인해 보세요.](/learn/manipulating-the-dom-with-refs#how-to-manage-a-list-of-refs-using-a-ref-callback)\n\n화면에 `<div>` DOM 노드가 추가되면, React는 `ref` 콜백을 호출하고 그 인자로 DOM `node`를 전달합니다. 해당 `<div>` DOM 노드가 제거되면, React는 콜백에서 반환한 Cleanup 함수를 호출합니다.\n\nReact는 *다른* `ref` 콜백을 전달할 때마다 `ref` 콜백도 호출합니다. 위 예시에서 `(node) => { ... }`는 렌더링마다 서로 다른 함수입니다. 컴포넌트가 다시 렌더링 될 때, *이전* 함수는 인자로 `null`을 받아 호출되고, *다음* 함수는 DOM 노드를 인자로 받아 호출됩니다.\n\n#### 매개변수 {/*ref-callback-parameters*/}\n\n* `node`: DOM 노드. Ref가 DOM 노드에 연결될 때 React가 해당 DOM 노드를 전달합니다. 매 렌더링에서 `ref` 콜백에 동일한 함수 참조를 넘기지 않으면, 컴포넌트가 리렌더링될 때마다 콜백이 일시적으로 Cleanup 됐다가 다시 생성됩니다.\n\n<Note>\n\n#### React 19는 `ref` 콜백을 위한 Cleanup 함수를 추가했습니다. {/*react-19-added-cleanup-functions-for-ref-callbacks*/}\n\n하위 호환성을 위해, `ref` 콜백이 Cleanup 함수를 반환하지 않으면, `ref`가 분리될 때 `node`가 `null`로 호출됩니다. 이 동작은 향후 버전에서 제거될 예정입니다.\n\n</Note>\n\n#### 반환값 {/*returns*/}\n\n* **optional** `Cleanup 함수`: `ref`가 분리되면, React는 cleanup 함수를 호출합니다. `ref` 콜백에 의해 함수가 반환되지 않으면 React는 `ref`가 분리되면 인수로 `null`을 사용하여 다시 콜백을 호출합니다. 이 동작은 향후 버전에서 제거될 예정입니다.\n\n#### 주의 사항 {/*caveats*/}\n\n* Strict Mode가 켜져있으면, React는 첫 번째 실제 설정 전에 **개발 전용 Setup + cleanup 주기**를 하나 더 실행할 것입니다. 이는 스트레스 테스트로, Cleanup 로직이 Setup 로직을 \"거울처럼\" 따라가며 Setup이 하는 일을 중지하거나 되돌리도록 보장하기 위한 것입니다. 이 때문에 문제가 발생한다면 Cleanup 함수를 구현하세요.\n* *다른* `ref` 콜백을 전달하면, React는 먼저 *이전* 콜백의 Cleanup 함수가 있다면 그것을 호출합니다. Cleanup 함수가 없으면, 이전 `ref` 콜백을 `null`을 인수로 하여 한 번 호출합니다. *다음* 함수는 DOM 노드와 함께 호출됩니다.\n\n---\n\n### React 이벤트 객체 {/*react-event-object*/}\n\n이벤트 핸들러는 *React 이벤트 객체*를 받게 되며, \"합성 이벤트\"라고도 합니다.\n\n```js\n<button onClick={e => {\n  console.log(e); // React 이벤트 객체\n}} />\n```\n\n이것은 기본 DOM 이벤트와 같은 표준을 준수하지만 일부 브라우저의 불일치를 수정합니다.\n\n\n일부 React의 이벤트는 브라우저의 네이티브 이벤트에 직접 매핑되지 않습니다. 예를 들어 `onMouseLeave`에서 `e.nativeEvent`는 `mouseout` 이벤트를 가리킵니다. 특정 매핑은 퍼블릭 API의 일부가 아니며 추후 변경될 수 있습니다. 어떠한 이유로 기본 브라우저 이벤트가 필요한 경우 `e.nativeEvent`에서 읽어와야 합니다.\n\n#### 프로퍼티 {/*react-event-object-properties*/}\n\nReact 이벤트 객체는 표준 [`Event`](https://developer.mozilla.org/ko/docs/Web/API/Event) 프로퍼티의 일부를 구현했습니다.\n\n* [`bubbles`](https://developer.mozilla.org/ko/docs/Web/API/Event/bubbles): 불리언 타입입니다. 이벤트가 DOM을 통해 버블링되는지 여부를 반환합니다.\n* [`cancelable`](https://developer.mozilla.org/ko/docs/Web/API/Event/cancelable): 불리언 타입입니다. 이벤트를 취소할 수 있는지를 반환합니다.\n* [`currentTarget`](https://developer.mozilla.org/en-US/docs/Web/API/Event/currentTarget): DOM 노드입니다. React 트리에서 현재 핸들러가 연결된 노드를 반환합니다.\n* [`defaultPrevented`](https://developer.mozilla.org/ko/docs/Web/API/Event/defaultPrevented): 불리언 타입입니다. `preventDefault`가 호출되었는지 여부를 반환합니다.\n* [`eventPhase`](https://developer.mozilla.org/ko/docs/Web/API/Event/eventPhase): 숫자 타입입니다. 이벤트가 현재 어느 단계에 있는지 반환합니다.\n* [`isTrusted`](https://developer.mozilla.org/ko/docs/Web/API/Event/isTrusted): 불리언 타입입니다. 사용자에 의해 이벤트가 시작되었는지에 대한 여부를 반환합니다.\n* [`target`](https://developer.mozilla.org/ko/docs/Web/API/Event/target): DOM 노드입니다. (멀리 있는 자식일 수도 있는) 이벤트가 발생한 노드를 반환합니다.\n* [`timeStamp`](https://developer.mozilla.org/ko/docs/Web/API/Event/timeStamp): 숫자 타입입니다. 이벤트가 발생한 시간을 반환합니다.\n\n추가로 React 이벤트 객체는 다음과 같은 프로퍼티를 제공합니다.\n\n* `nativeEvent`: DOM [`Event`](https://developer.mozilla.org/ko/docs/Web/API/Event) 이벤트입니다. 원래의 브라우저 이벤트 객체입니다.\n\n#### 메서드 {/*react-event-object-methods*/}\n\nReact 이벤트 객체는 표준 [`Event`](https://developer.mozilla.org/ko/docs/Web/API/Event) 메서드의 일부를 구현했습니다.\n\n* [`preventDefault()`](https://developer.mozilla.org/ko/docs/Web/API/Event/preventDefault): 이벤트에 대한 기본 브라우저 동작을 방지합니다.\n* [`stopPropagation()`](https://developer.mozilla.org/ko/docs/Web/API/Event/stopPropagation): React 트리를 통한 이벤트 전파를 중지합니다.\n\n추가로 React 이벤트 객체는 다음과 같은 프로퍼티를 제공합니다.\n\n* `isDefaultPrevented()`: `preventDefault`가 호출되었는지에 대한 여부를 나타내는 불리언 값을 반환합니다.\n* `isPropagationStopped()`: `stopPropagation`이 호출되었는지에 대한 여부를 나타내는 불리언 값을 반환합니다.\n* `persist()`: React DOM에서는 사용되지 않습니다. React Native에서는 이벤트가 발생한 후 이벤트의 프로퍼티를 읽으려면 해당 함수를 호출해야 합니다.\n* `isPersistent()`: React DOM에서는 사용되지 않습니다. React Native에서는 `persist`가 호출되었는지 여부를 반환합니다.\n\n#### 주의 사항 {/*react-event-object-caveats*/}\n\n* `currentTarget`, `eventPhase`, `target`, `type`의 값은 React 코드가 예상하는 값을 반영합니다. 내부적으로는 React는 이벤트 핸들러를 루트에 첨부하지만, React 이벤트 객체에는 반영되지 않습니다. 예를 들어 `e.currentTarget`은 기본`e.nativeEvent.currentTarget`과 동일하지 않을 수 있습니다. 폴리필 된 이벤트의 경우 `e.type` (React 이벤트 타입)이 `e.nativeEvent.type` (기본 타입)과 다를 수 있습니다.\n\n---\n\n### `AnimationEvent` 핸들러 함수 {/*animationevent-handler*/}\n\n[CSS 애니메이션](https://developer.mozilla.org/ko/docs/Web/CSS/CSS_Animations/Using_CSS_animations) 이벤트에 대한 이벤트 핸들러 유형입니다.\n\n```js\n<div\n  onAnimationStart={e => console.log('onAnimationStart')}\n  onAnimationIteration={e => console.log('onAnimationIteration')}\n  onAnimationEnd={e => console.log('onAnimationEnd')}\n/>\n```\n\n#### 매개변수 {/*animationevent-handler-parameters*/}\n\n* `e`: 다음과 같은 추가 [`AnimationEvent`](https://developer.mozilla.org/ko/docs/Web/API/AnimationEvent) 프로퍼티가 있는 [React 이벤트 객체](#react-event-object)입니다.\n  * [`animationName`](https://developer.mozilla.org/ko/docs/Web/API/AnimationEvent/animationName)\n  * [`elapsedTime`](https://developer.mozilla.org/ko/docs/Web/API/AnimationEvent/elapsedTime)\n  * [`pseudoElement`](https://developer.mozilla.org/en-US/docs/Web/API/AnimationEvent/pseudoElement)\n\n---\n\n### `ClipboardEvent` 핸들러 함수 {/*clipboadevent-handler*/}\n\n[클립보드 API](https://developer.mozilla.org/ko/docs/Web/API/Clipboard_API) 이벤트에 대한 이벤트 핸들러 유형입니다.\n\n```js\n<input\n  onCopy={e => console.log('onCopy')}\n  onCut={e => console.log('onCut')}\n  onPaste={e => console.log('onPaste')}\n/>\n```\n\n#### 매개변수 {/*clipboadevent-handler-parameters*/}\n\n* `e`: 다음과 같은 추가 [`ClipboardEvent`](https://developer.mozilla.org/ko/docs/Web/API/ClipboardEvent) 프로퍼티가 있는 [React 이벤트 객체](#react-event-object)입니다.\n\n  * [`clipboardData`](https://developer.mozilla.org/ko/docs/Web/API/ClipboardEvent/clipboardData)\n\n---\n\n### `CompositionEvent` 핸들러 함수 {/*compositionevent-handler*/}\n\n[입력 메서드 편집기 (IME)](https://developer.mozilla.org/en-US/docs/Glossary/Input_method_editor) 이벤트에 대한 이벤트 핸들러 유형입니다.\n\n```js\n<input\n  onCompositionStart={e => console.log('onCompositionStart')}\n  onCompositionUpdate={e => console.log('onCompositionUpdate')}\n  onCompositionEnd={e => console.log('onCompositionEnd')}\n/>\n```\n\n#### 매개변수 {/*compositionevent-handler-parameters*/}\n\n* `e`: 다음과 같은 추가 [`CompositionEvent`](https://developer.mozilla.org/en-US/docs/Web/API/CompositionEvent) 프로퍼티가 있는 [React 이벤트 객체](#react-event-object)입니다.\n  * [`data`](https://developer.mozilla.org/en-US/docs/Web/API/CompositionEvent/data)\n\n---\n\n### `DragEvent` 핸들러 함수 {/*dragevent-handler*/}\n\n[HTML 드래그 앤 드롭 API](https://developer.mozilla.org/ko/docs/Web/API/HTML_Drag_and_Drop_API) 이벤트의 이벤트 핸들러 유형입니다.\n\n```js\n<>\n  <div\n    draggable={true}\n    onDragStart={e => console.log('onDragStart')}\n    onDragEnd={e => console.log('onDragEnd')}\n  >\n    Drag source\n  </div>\n\n  <div\n    onDragEnter={e => console.log('onDragEnter')}\n    onDragLeave={e => console.log('onDragLeave')}\n    onDragOver={e => { e.preventDefault(); console.log('onDragOver'); }}\n    onDrop={e => console.log('onDrop')}\n  >\n    Drop target\n  </div>\n</>\n```\n\n#### 매개변수 {/*dragevent-handler-parameters*/}\n\n* `e`: 다음과 같은 추가 [`DragEvent`](https://developer.mozilla.org/en-US/docs/Web/API/DragEvent) 프로퍼티가 있는 [React 이벤트 객체](#react-event-object)입니다.\n  * [`dataTransfer`](https://developer.mozilla.org/en-US/docs/Web/API/DragEvent/dataTransfer)\n\n  이는 상속된 [`MouseEvent`](https://developer.mozilla.org/ko/docs/Web/API/MouseEvent)의 프로퍼티도 포함합니다.\n\n  * [`altKey`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/altKey)\n  * [`button`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button)\n  * [`buttons`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/buttons)\n  * [`ctrlKey`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/ctrlKey)\n  * [`clientX`](https://developer.mozilla.org/ko/docs/Web/API/MouseEvent/clientX)\n  * [`clientY`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/clientY)\n  * [`getModifierState(key)`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/getModifierState)\n  * [`metaKey`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/metaKey)\n  * [`movementX`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/movementX)\n  * [`movementY`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/movementY)\n  * [`pageX`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/pageX)\n  * [`pageY`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/pageY)\n  * [`relatedTarget`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/relatedTarget)\n  * [`screenX`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/screenX)\n  * [`screenY`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/screenY)\n  * [`shiftKey`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/shiftKey)\n\n  또한 상속된 [`UIEvent`](https://developer.mozilla.org/en-US/docs/Web/API/UIEvent)의 프로퍼티도 포함합니다.\n\n  * [`detail`](https://developer.mozilla.org/en-US/docs/Web/API/UIEvent/detail)\n  * [`view`](https://developer.mozilla.org/en-US/docs/Web/API/UIEvent/view)\n\n---\n\n### `FocusEvent` 핸들러 함수 {/*focusevent-handler*/}\n\n포커싱 이벤트에 대한 이벤트 핸들러 유형입니다.\n\n```js\n<input\n  onFocus={e => console.log('onFocus')}\n  onBlur={e => console.log('onBlur')}\n/>\n```\n\n[아래 예시를 참고하세요.](#handling-focus-events)\n\n#### 매개변수 {/*focusevent-handler-parameters*/}\n\n* `e`: 다음과 같은 추가 [`FocusEvent`](https://developer.mozilla.org/en-US/docs/Web/API/FocusEvent) 프로퍼티가 있는 [React 이벤트 객체](#react-event-object)입니다.\n  * [`relatedTarget`](https://developer.mozilla.org/en-US/docs/Web/API/FocusEvent/relatedTarget)\n\n  또한 상속된 [`UIEvent`](https://developer.mozilla.org/en-US/docs/Web/API/UIEvent)의 프로퍼티도 포함합니다.\n\n  * [`detail`](https://developer.mozilla.org/en-US/docs/Web/API/UIEvent/detail)\n  * [`view`](https://developer.mozilla.org/en-US/docs/Web/API/UIEvent/view)\n\n---\n\n### `Event` 핸들러 함수 {/*event-handler*/}\n\n일반 이벤트를 위한 이벤트 핸들러 유형입니다.\n\n#### 매개변수 {/*event-handler-parameters*/}\n\n* `e`: 추가 프로퍼티가 없는 [React 이벤트 객체](#react-event-object)입니다.\n\n---\n\n### `InputEvent` 핸들러 함수 {/*inputevent-handler*/}\n\n`onBeforeInput` 이벤트에 대한 이벤트 핸들러 유형입니다.\n\n```js\n<input onBeforeInput={e => console.log('onBeforeInput')} />\n```\n\n#### 매개변수 {/*inputevent-handler-parameters*/}\n\n* `e`: 다음과 같은 추가 [`InputEvent`](https://developer.mozilla.org/ko/docs/Web/API/InputEvent) 프로퍼티가 있는 [React 이벤트 객체](#react-event-object)입니다.\n  * [`data`](https://developer.mozilla.org/en-US/docs/Web/API/InputEvent/data)\n\n---\n\n### `KeyboardEvent` 핸들러 함수 {/*keyboardevent-handler*/}\n\n키보드 이벤트에 대한 이벤트 핸들러 유형입니다.\n\n```js\n<input\n  onKeyDown={e => console.log('onKeyDown')}\n  onKeyUp={e => console.log('onKeyUp')}\n/>\n```\n\n[아래 예시를 참고하세요.](#handling-keyboard-events)\n\n#### 매개변수 {/*keyboardevent-handler-parameters*/}\n\n* `e`: 다음과 같은 추가 [`KeyboardEvent`](https://developer.mozilla.org/ko/docs/Web/API/KeyboardEvent) 프로퍼티가 있는 [React 이벤트 객체](#react-event-object)입니다.\n  * [`altKey`](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/altKey)\n  * [`charCode`](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/charCode)\n  * [`code`](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code)\n  * [`ctrlKey`](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/ctrlKey)\n  * [`getModifierState(key)`](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/getModifierState)\n  * [`key`](https://developer.mozilla.org/ko/docs/Web/API/KeyboardEvent/key)\n  * [`keyCode`](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode)\n  * [`locale`](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/locale)\n  * [`metaKey`](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/metaKey)\n  * [`location`](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/location)\n  * [`repeat`](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/repeat)\n  * [`shiftKey`](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/shiftKey)\n  * [`which`](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/which)\n\n  또한 상속된 [`UIEvent`](https://developer.mozilla.org/en-US/docs/Web/API/UIEvent)의 프로퍼티도 포함합니다.\n\n  * [`detail`](https://developer.mozilla.org/en-US/docs/Web/API/UIEvent/detail)\n  * [`view`](https://developer.mozilla.org/en-US/docs/Web/API/UIEvent/view)\n\n---\n\n### `MouseEvent` 핸들러 함수 {/*mouseevent-handler*/}\n\n마우스 이벤트에 대한 이벤트 핸들러 유형입니다.\n\n```js\n<div\n  onClick={e => console.log('onClick')}\n  onMouseEnter={e => console.log('onMouseEnter')}\n  onMouseOver={e => console.log('onMouseOver')}\n  onMouseDown={e => console.log('onMouseDown')}\n  onMouseUp={e => console.log('onMouseUp')}\n  onMouseLeave={e => console.log('onMouseLeave')}\n/>\n```\n\n[아래 예시를 참고하세요.](#handling-mouse-events)\n\n#### 매개변수 {/*mouseevent-handler-parameters*/}\n\n* `e`: 다음과 같은 추가 [`MouseEvent`](https://developer.mozilla.org/ko/docs/Web/API/MouseEvent) 프로퍼티가 있는 [React 이벤트 객체](#react-event-object)입니다.\n  * [`altKey`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/altKey)\n  * [`button`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button)\n  * [`buttons`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/buttons)\n  * [`ctrlKey`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/ctrlKey)\n  * [`clientX`](https://developer.mozilla.org/ko/docs/Web/API/MouseEvent/clientX)\n  * [`clientY`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/clientY)\n  * [`getModifierState(key)`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/getModifierState)\n  * [`metaKey`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/metaKey)\n  * [`movementX`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/movementX)\n  * [`movementY`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/movementY)\n  * [`pageX`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/pageX)\n  * [`pageY`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/pageY)\n  * [`relatedTarget`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/relatedTarget)\n  * [`screenX`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/screenX)\n  * [`screenY`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/screenY)\n  * [`shiftKey`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/shiftKey)\n\n  또한 상속된 [`UIEvent`](https://developer.mozilla.org/en-US/docs/Web/API/UIEvent)의 프로퍼티도 포함합니다.\n\n  * [`detail`](https://developer.mozilla.org/en-US/docs/Web/API/UIEvent/detail)\n  * [`view`](https://developer.mozilla.org/en-US/docs/Web/API/UIEvent/view)\n\n---\n\n### `PointerEvent` 핸들러 함수 {/*pointerevent-handler*/}\n\n[포인터 이벤트](https://developer.mozilla.org/en-US/docs/Web/API/Pointer_events)에 대한 이벤트 핸들러 유형입니다.\n\n```js\n<div\n  onPointerEnter={e => console.log('onPointerEnter')}\n  onPointerMove={e => console.log('onPointerMove')}\n  onPointerDown={e => console.log('onPointerDown')}\n  onPointerUp={e => console.log('onPointerUp')}\n  onPointerLeave={e => console.log('onPointerLeave')}\n/>\n```\n\n[아래 예시를 참고하세요.](#handling-pointer-events)\n\n#### 매개변수 {/*pointerevent-handler-parameters*/}\n\n* `e`: 다음과 같은 추가[`PointerEvent`](https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent) 프로퍼티가 있는 [React 이벤트 객체](#react-event-object)입니다.\n  * [`height`](https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent/height)\n  * [`isPrimary`](https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent/isPrimary)\n  * [`pointerId`](https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent/pointerId)\n  * [`pointerType`](https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent/pointerType)\n  * [`pressure`](https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent/pressure)\n  * [`tangentialPressure`](https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent/tangentialPressure)\n  * [`tiltX`](https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent/tiltX)\n  * [`tiltY`](https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent/tiltY)\n  * [`twist`](https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent/twist)\n  * [`width`](https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent/width)\n\n  이는 상속된 [`MouseEvent`](https://developer.mozilla.org/ko/docs/Web/API/MouseEvent)의 프로퍼티도 포함합니다.\n\n  * [`altKey`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/altKey)\n  * [`button`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button)\n  * [`buttons`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/buttons)\n  * [`ctrlKey`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/ctrlKey)\n  * [`clientX`](https://developer.mozilla.org/ko/docs/Web/API/MouseEvent/clientX)\n  * [`clientY`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/clientY)\n  * [`getModifierState(key)`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/getModifierState)\n  * [`metaKey`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/metaKey)\n  * [`movementX`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/movementX)\n  * [`movementY`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/movementY)\n  * [`pageX`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/pageX)\n  * [`pageY`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/pageY)\n  * [`relatedTarget`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/relatedTarget)\n  * [`screenX`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/screenX)\n  * [`screenY`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/screenY)\n  * [`shiftKey`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/shiftKey)\n\n  또한 상속된 [`UIEvent`](https://developer.mozilla.org/en-US/docs/Web/API/UIEvent)의 프로퍼티도 포함합니다.\n\n  * [`detail`](https://developer.mozilla.org/en-US/docs/Web/API/UIEvent/detail)\n  * [`view`](https://developer.mozilla.org/en-US/docs/Web/API/UIEvent/view)\n\n---\n\n### `TouchEvent` 핸들러 함수 {/*touchevent-handler*/}\n\n[터치 이벤트](https://developer.mozilla.org/en-US/docs/Web/API/Touch_events)에 대한 이벤트 핸들러 유형입니다.\n\n```js\n<div\n  onTouchStart={e => console.log('onTouchStart')}\n  onTouchMove={e => console.log('onTouchMove')}\n  onTouchEnd={e => console.log('onTouchEnd')}\n  onTouchCancel={e => console.log('onTouchCancel')}\n/>\n```\n\n#### 매개변수 {/*touchevent-handler-parameters*/}\n\n* `e`: 다음과 같은 추가 [`TouchEvent`](https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent) 프로퍼티가 있는 [React 이벤트 객체](#react-event-object)입니다.\n  * [`altKey`](https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent/altKey)\n  * [`ctrlKey`](https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent/ctrlKey)\n  * [`changedTouches`](https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent/changedTouches)\n  * [`getModifierState(key)`](https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent/getModifierState)\n  * [`metaKey`](https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent/metaKey)\n  * [`shiftKey`](https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent/shiftKey)\n  * [`touches`](https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent/touches)\n  * [`targetTouches`](https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent/targetTouches)\n\n  또한 상속된 [`UIEvent`](https://developer.mozilla.org/en-US/docs/Web/API/UIEvent)의 프로퍼티도 포함합니다.\n\n  * [`detail`](https://developer.mozilla.org/en-US/docs/Web/API/UIEvent/detail)\n  * [`view`](https://developer.mozilla.org/en-US/docs/Web/API/UIEvent/view)\n\n---\n\n### `TransitionEvent` 핸들러 함수 {/*transitionevent-handler*/}\n\nCSS 전환 이벤트에 대한 이벤트 핸들러 유형입니다.\n\n```js\n<div\n  onTransitionEnd={e => console.log('onTransitionEnd')}\n/>\n```\n\n#### 매개변수 {/*transitionevent-handler-parameters*/}\n\n* `e`: 다음과 같은 추가 [`TransitionEvent`](https://developer.mozilla.org/en-US/docs/Web/API/TransitionEvent)의 프로퍼티가 있는 [React 이벤트 객체](#react-event-object)입니다.\n  * [`elapsedTime`](https://developer.mozilla.org/en-US/docs/Web/API/TransitionEvent/elapsedTime)\n  * [`propertyName`](https://developer.mozilla.org/en-US/docs/Web/API/TransitionEvent/propertyName)\n  * [`pseudoElement`](https://developer.mozilla.org/en-US/docs/Web/API/TransitionEvent/pseudoElement)\n\n---\n\n### `UIEvent` 핸들러 함수 {/*uievent-handler*/}\n\n일반적인 UI 이벤트를 위한 이벤트 핸들러 유형입니다.\n\n```js\n<div\n  onScroll={e => console.log('onScroll')}\n/>\n```\n\n#### 매개변수 {/*uievent-handler-parameters*/}\n\n* `e`: 다음과 같은 추가 [`UIEvent`](https://developer.mozilla.org/en-US/docs/Web/API/UIEvent) 프로퍼티를 가진 [React 이벤트 객체](#react-event-object)입니다.\n  * [`detail`](https://developer.mozilla.org/en-US/docs/Web/API/UIEvent/detail)\n  * [`view`](https://developer.mozilla.org/en-US/docs/Web/API/UIEvent/view)\n\n---\n\n### `WheelEvent` 핸들러 함수 {/*wheelevent-handler*/}\n\n`onWheel` 이벤트에 대한 이벤트 핸들러 유형입니다.\n\n```js\n<div\n  onWheel={e => console.log('onWheel')}\n/>\n```\n\n#### 매개변수 {/*wheelevent-handler-parameters*/}\n\n* `e`: 다음과 같은 추가 [`WheelEvent`](https://developer.mozilla.org/en-US/docs/Web/API/WheelEvent)의 프로퍼티가 있는 [React 이벤트 객체](#react-event-object)입니다.\n  * [`deltaMode`](https://developer.mozilla.org/en-US/docs/Web/API/WheelEvent/deltaMode)\n  * [`deltaX`](https://developer.mozilla.org/en-US/docs/Web/API/WheelEvent/deltaX)\n  * [`deltaY`](https://developer.mozilla.org/en-US/docs/Web/API/WheelEvent/deltaY)\n  * [`deltaZ`](https://developer.mozilla.org/en-US/docs/Web/API/WheelEvent/deltaZ)\n\n\n  또한 다음과 같이 상속된 [`MouseEvent`](https://developer.mozilla.org/ko/docs/Web/API/MouseEvent)의 프로퍼티도 포함합니다.\n\n  * [`altKey`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/altKey)\n  * [`button`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button)\n  * [`buttons`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/buttons)\n  * [`ctrlKey`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/ctrlKey)\n  * [`clientX`](https://developer.mozilla.org/ko/docs/Web/API/MouseEvent/clientX)\n  * [`clientY`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/clientY)\n  * [`getModifierState(key)`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/getModifierState)\n  * [`metaKey`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/metaKey)\n  * [`movementX`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/movementX)\n  * [`movementY`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/movementY)\n  * [`pageX`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/pageX)\n  * [`pageY`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/pageY)\n  * [`relatedTarget`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/relatedTarget)\n  * [`screenX`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/screenX)\n  * [`screenY`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/screenY)\n  * [`shiftKey`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/shiftKey)\n\n  더불어 아래의 상속된 [`UIEvent`](https://developer.mozilla.org/en-US/docs/Web/API/UIEvent)의 프로퍼티도 포함합니다.\n\n  * [`detail`](https://developer.mozilla.org/en-US/docs/Web/API/UIEvent/detail)\n  * [`view`](https://developer.mozilla.org/en-US/docs/Web/API/UIEvent/view)\n\n---\n\n## 사용법 {/*usage*/}\n\n### CSS 스타일 적용하기 {/*applying-css-styles*/}\n\nReact는 [`className`](https://developer.mozilla.org/ko/docs/Web/API/Element/className)을 사용하여 CSS 클래스를 지정합니다. 이것은 HTML의 클래스 속성처럼 작동합니다.\n\n```js\n<img className=\"avatar\" />\n```\n\n그런 다음 별도의 CSS 파일에 CSS 규칙을 지정합니다.\n\n```css\n/* In your CSS */\n.avatar {\n  border-radius: 50%;\n}\n```\n\nReact는 CSS 파일을 추가하는 방법을 규정하지 않습니다. 가장 간단한 방법은 HTML에 [`<link>`](https://developer.mozilla.org/ko/docs/Web/HTML/Element/link) 태그를 추가하는 것입니다. 빌드 도구나 프레임워크를 사용하고 있다면, 해당 기술의 문서를 참조하여 프로젝트에 CSS 파일을 추가하는 방법을 알아보세요.\n\n때때로 스타일 값은 데이터에 따라 달라집니다. `style` 어트리뷰트를 사용하여 일부 스타일을 동적으로 전달할 수 있습니다.\n\n```js {3-6}\n<img\n  className=\"avatar\"\n  style={{\n    width: user.imageSize,\n    height: user.imageSize\n  }}\n/>\n```\n\n\n위의 예시에서 `style={{}}`은 특별한 구문이 아니라 `style={ }`와 같이 [중괄호가 있는 JSX](/learn/javascript-in-jsx-with-curly-braces) 내에 있는 일반 `{}` 객체입니다. 스타일이 자바스크립트 변수에 의존하는 경우에만 `style` 어트리뷰트를 사용하는 것이 좋습니다.\n\n<Sandpack>\n\n```js src/App.js\nimport Avatar from './Avatar.js';\n\nconst user = {\n  name: 'Hedy Lamarr',\n  imageUrl: 'https://i.imgur.com/yXOvdOSs.jpg',\n  imageSize: 90,\n};\n\nexport default function App() {\n  return <Avatar user={user} />;\n}\n```\n\n```js src/Avatar.js active\nexport default function Avatar({ user }) {\n  return (\n    <img\n      src={user.imageUrl}\n      alt={'Photo of ' + user.name}\n      className=\"avatar\"\n      style={{\n        width: user.imageSize,\n        height: user.imageSize\n      }}\n    />\n  );\n}\n```\n\n```css src/styles.css\n.avatar {\n  border-radius: 50%;\n}\n```\n\n</Sandpack>\n\n<DeepDive>\n\n#### 여러 CSS 클래스를 조건부로 적용하기 위해 어떻게 해야하나요? {/*how-to-apply-multiple-css-classes-conditionally*/}\n\n조건부로 CSS 클래스를 적용하려면 자바스크립트를 사용하여 `className` 직접 문자열을 생성해야 합니다.\n\n예를 들어 `className={'row ' + (isSelected ? 'selected': '')}`는 `isSelected`가 `true`인지 여부에 따라 `className=\"row\"` 또는 `className=\"row selected\"`를 생성합니다.\n\n가독성을 높이고 싶다면 [`classnames`](https://github.com/JedWatson/classnames)와 같은 작은 헬퍼 라이브러리를 사용할 수 있습니다.\n\n```js\nimport cn from 'classnames';\n\nfunction Row({ isSelected }) {\n  return (\n    <div className={cn('row', isSelected && 'selected')}>\n      ...\n    </div>\n  );\n}\n```\n\n이는 조건부 클래스가 여러 개 있는 경우 특히 편리합니다.\n\n```js\nimport cn from 'classnames';\n\nfunction Row({ isSelected, size }) {\n  return (\n    <div className={cn('row', {\n      selected: isSelected,\n      large: size === 'large',\n      small: size === 'small',\n    })}>\n      ...\n    </div>\n  );\n}\n```\n\n</DeepDive>\n\n---\n\n### `ref`를 사용하여 DOM 노드 조작하기 {/*manipulating-a-dom-node-with-a-ref*/}\n\n때로는 JSX에서 태그와 연결된 브라우저 DOM 노드를 가져와야 하는 경우가 있습니다. 예를 들어 버튼이 클릭 될 때 `<input>`에 포커싱을 맞추려면 브라우저의 `<input>` DOM 노드에서 [`focus()`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus)를 호출하면 됩니다.\n\n태그에 대한 브라우저의 DOM 노드를 가져오려면 [`ref`를 선언](/reference/react/useRef)하고 해당 태그에 `ref` 어트리뷰트로 전달합니다.\n\n```js {7}\nimport { useRef } from 'react';\n\nexport default function Form() {\n  const inputRef = useRef(null);\n  // ...\n  return (\n    <input ref={inputRef} />\n    // ...\n```\n\nReact는 DOM 노드를 화면에 렌더링 한 후 `inputRef.current`에 넣습니다.\n\n<Sandpack>\n\n```js\nimport { useRef } from 'react';\n\nexport default function Form() {\n  const inputRef = useRef(null);\n\n  function handleClick() {\n    inputRef.current.focus();\n  }\n\n  return (\n    <>\n      <input ref={inputRef} />\n      <button onClick={handleClick}>\n        Focus the input\n      </button>\n    </>\n  );\n}\n```\n\n</Sandpack>\n\n[Ref로 DOM 조작하기](/learn/manipulating-the-dom-with-refs) 및 [더 많은 예시](/reference/react/useRef#usage)에 대해 더 자세히 읽어보세요.\n\n고급 사용 사례의 경우 `ref` 어트리뷰트는 [콜백 함수](#ref-callback)도 허용합니다.\n\n---\n\n### 내부 HTML을 위험하게 설정하는 경우 {/*dangerously-setting-the-inner-html*/}\n\n다음과 같이 원시 HTML 문자열을 요소에 전달할 수 있습니다.\n\n```js\nconst markup = { __html: '<p>some raw html</p>' };\nreturn <div dangerouslySetInnerHTML={markup} />;\n```\n\n**이것은 위험합니다. 기본 DOM의 [`innerHTML`](https://developer.mozilla.org/ko/docs/Web/API/Element/innerHTML) 프로퍼티와 마찬가지로 각별히 주의해야 합니다. 마크업이 완전히 신뢰할 수 있는 출처에서 제공되는 것이 아니라면,  [XSS](https://ko.wikipedia.org/wiki/%EC%82%AC%EC%9D%B4%ED%8A%B8_%EA%B0%84_%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8C%85) 취약점이 쉽게 나타날 수 있습니다.**\n\n예를 들어, 마크다운을 HTML로 변환하는 라이브러리를 사용할 때, 해당 파서에 버그가 없고 사용자가 자신의 입력만 볼 수 있다고 믿는다면 다음과 같이 결과 HTML을 표시할 수 있습니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\nimport MarkdownPreview from './MarkdownPreview.js';\n\nexport default function MarkdownEditor() {\n  const [postContent, setPostContent] = useState('_Hello,_ **Markdown**!');\n  return (\n    <>\n      <label>\n        Enter some markdown:\n        <textarea\n          value={postContent}\n          onChange={e => setPostContent(e.target.value)}\n        />\n      </label>\n      <hr />\n      <MarkdownPreview markdown={postContent} />\n    </>\n  );\n}\n```\n\n```js src/MarkdownPreview.js active\nimport { Remarkable } from 'remarkable';\n\nconst md = new Remarkable();\n\nfunction renderMarkdownToHTML(markdown) {\n  // 출력되는 HTML이 동일한 사용자에게 표시되고,\n  // 이 마크다운 파서에 버그가 없다고\n  // 신뢰하기 때문에 안전합니다.\n  const renderedHTML = md.render(markdown);\n  return {__html: renderedHTML};\n}\n\nexport default function MarkdownPreview({ markdown }) {\n  const markup = renderMarkdownToHTML(markdown);\n  return <div dangerouslySetInnerHTML={markup} />;\n}\n```\n\n```json package.json\n{\n  \"dependencies\": {\n    \"react\": \"latest\",\n    \"react-dom\": \"latest\",\n    \"react-scripts\": \"latest\",\n    \"remarkable\": \"2.0.1\"\n  },\n  \"scripts\": {\n    \"start\": \"react-scripts start\",\n    \"build\": \"react-scripts build\",\n    \"test\": \"react-scripts test --env=jsdom\",\n    \"eject\": \"react-scripts eject\"\n  }\n}\n```\n\n```css\ntextarea { display: block; margin-top: 5px; margin-bottom: 10px; }\n```\n\n</Sandpack>\n\n위 예시의 `renderMarkdownToHTML` 함수처럼, `{__html}` 객체는 가능한 한 HTML이 만들어지는 곳 가까이에서 생성되어야 합니다. 이렇게 하면 코드에서 사용되는 모든 원시 HTML이 명시적으로 표시되고 HTML을 포함할 것으로 예상되는 변수만 `dangerouslySetInnerHTML`로 전달됩니다. `<div dangerouslySetInnerHTML={{__html: markup}} />`처럼 인라인으로 객체를 생성하는 것은 권장하지 않습니다.\n\n임의의 HTML을 렌더링하는 것이 왜 위험한지를 알아보려면 위의 코드를 다음과 같이 바꿔보세요.\n\n```js {1-4,7,8}\nconst post = {\n  // 이 콘텐츠가 데이터베이스에 저장되어 있다고 가정해보겠습니다.\n  content: `<img src=\"\" onerror='alert(\"you were hacked\")'>`\n};\n\nexport default function MarkdownPreview() {\n  // 🔴 보안 취약점: 신뢰할 수 없는 입력을 dangerouslySetInnerHTML로 전달했습니다.\n  const markup = { __html: post.content };\n  return <div dangerouslySetInnerHTML={markup} />;\n}\n```\n\nHTML에 포함된 코드가 실행됩니다. 해커는 이 보안 허점을 이용하여 사용자의 정보를 훔치거나 사용자 대신 작업을 수행할 수 있습니다. **신뢰할 수 있고 유해한 정보가 포함되어 있지 않은 데이터를 사용할 때만 `dangerouslySetInnerHTML`을 사용하세요.**\n\n---\n\n### 마우스 이벤트 처리 {/*handling-mouse-events*/}\n\n이 예시는 일반적인 [마우스 이벤트](#mouseevent-handler)와 해당 이벤트가 언제 발생하는지 보여줍니다.\n\n<Sandpack>\n\n```js\nexport default function MouseExample() {\n  return (\n    <div\n      onMouseEnter={e => console.log('onMouseEnter (parent)')}\n      onMouseLeave={e => console.log('onMouseLeave (parent)')}\n    >\n      <button\n        onClick={e => console.log('onClick (first button)')}\n        onMouseDown={e => console.log('onMouseDown (first button)')}\n        onMouseEnter={e => console.log('onMouseEnter (first button)')}\n        onMouseLeave={e => console.log('onMouseLeave (first button)')}\n        onMouseOver={e => console.log('onMouseOver (first button)')}\n        onMouseUp={e => console.log('onMouseUp (first button)')}\n      >\n        First button\n      </button>\n      <button\n        onClick={e => console.log('onClick (second button)')}\n        onMouseDown={e => console.log('onMouseDown (second button)')}\n        onMouseEnter={e => console.log('onMouseEnter (second button)')}\n        onMouseLeave={e => console.log('onMouseLeave (second button)')}\n        onMouseOver={e => console.log('onMouseOver (second button)')}\n        onMouseUp={e => console.log('onMouseUp (second button)')}\n      >\n        Second button\n      </button>\n    </div>\n  );\n}\n```\n\n```css\nlabel { display: block; }\ninput { margin-left: 10px; }\n```\n\n</Sandpack>\n\n---\n\n### 포인터 이벤트 처리 {/*handling-pointer-events*/}\n\n이 예시는 일반적인 [포인터 이벤트](#pointerevent-handler)와 해당 이벤트가 언제 발생하는지 보여줍니다.\n\n<Sandpack>\n\n```js\nexport default function PointerExample() {\n  return (\n    <div\n      onPointerEnter={e => console.log('onPointerEnter (parent)')}\n      onPointerLeave={e => console.log('onPointerLeave (parent)')}\n      style={{ padding: 20, backgroundColor: '#ddd' }}\n    >\n      <div\n        onPointerDown={e => console.log('onPointerDown (first child)')}\n        onPointerEnter={e => console.log('onPointerEnter (first child)')}\n        onPointerLeave={e => console.log('onPointerLeave (first child)')}\n        onPointerMove={e => console.log('onPointerMove (first child)')}\n        onPointerUp={e => console.log('onPointerUp (first child)')}\n        style={{ padding: 20, backgroundColor: 'lightyellow' }}\n      >\n        First child\n      </div>\n      <div\n        onPointerDown={e => console.log('onPointerDown (second child)')}\n        onPointerEnter={e => console.log('onPointerEnter (second child)')}\n        onPointerLeave={e => console.log('onPointerLeave (second child)')}\n        onPointerMove={e => console.log('onPointerMove (second child)')}\n        onPointerUp={e => console.log('onPointerUp (second child)')}\n        style={{ padding: 20, backgroundColor: 'lightblue' }}\n      >\n        Second child\n      </div>\n    </div>\n  );\n}\n```\n\n```css\nlabel { display: block; }\ninput { margin-left: 10px; }\n```\n\n</Sandpack>\n\n---\n\n### 포커스 이벤트 처리 {/*handling-focus-events*/}\n\nReact에서는 [포커스 이벤트](#focusevent-handler)가 버블링됩니다. 부모 요소의 바깥 부분에서 발생한 이벤트가 focus 혹은 blur인지 구분하기 위해 `currentTarget`과 `relatedTarget`를 사용할 수 있습니다.\n\n<Sandpack>\n\n```js\nexport default function FocusExample() {\n  return (\n    <div\n      tabIndex={1}\n      onFocus={(e) => {\n        if (e.currentTarget === e.target) {\n          console.log('focused parent');\n        } else {\n          console.log('focused child', e.target.name);\n        }\n        if (!e.currentTarget.contains(e.relatedTarget)) {\n          // children간의 focus를 이동할 때는 발생되지 않음.\n          console.log('focus entered parent');\n        }\n      }}\n      onBlur={(e) => {\n        if (e.currentTarget === e.target) {\n          console.log('unfocused parent');\n        } else {\n          console.log('unfocused child', e.target.name);\n        }\n        if (!e.currentTarget.contains(e.relatedTarget)) {\n          // children간의 focus를 이동할 때는 발생되지 않음.\n          console.log('focus left parent');\n        }\n      }}\n    >\n      <label>\n        First name:\n        <input name=\"firstName\" />\n      </label>\n      <label>\n        Last name:\n        <input name=\"lastName\" />\n      </label>\n    </div>\n  );\n}\n```\n\n```css\nlabel { display: block; }\ninput { margin-left: 10px; }\n```\n\n</Sandpack>\n\n---\n\n### 키보드 이벤트 처리 {/*handling-keyboard-events*/}\n\n이 예시는 일반적인 [키보드 이벤트](#keyboardevent-handler)와 해당 이벤트가 언제 발생하는지 보여줍니다.\n\n<Sandpack>\n\n```js\nexport default function KeyboardExample() {\n  return (\n    <label>\n      First name:\n      <input\n        name=\"firstName\"\n        onKeyDown={e => console.log('onKeyDown:', e.key, e.code)}\n        onKeyUp={e => console.log('onKeyUp:', e.key, e.code)}\n      />\n    </label>\n  );\n}\n```\n\n```css\nlabel { display: block; }\ninput { margin-left: 10px; }\n```\n\n</Sandpack>\n"
  },
  {
    "path": "src/content/reference/react-dom/components/form.md",
    "content": "---\ntitle: \"<form>\"\n---\n\n<Intro>\n\n[내장 브라우저 `<form>` 컴포넌트](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form)로 정보 제출을 위한 대화형 컨트롤을 만들 수 있습니다.\n\n```js\n<form action={search}>\n    <input name=\"query\" />\n    <button type=\"submit\">검색</button>\n</form>\n```\n\n</Intro>\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `<form>` {/*form*/}\n\n정보 제출을 위한 대화형 컨트롤을 생성하기 위해, [내장 브라우저 `<form>` 컴포넌트](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form)를 렌더링하세요.\n\n```js\n<form action={search}>\n    <input name=\"query\" />\n    <button type=\"submit\">검색</button>\n</form>\n```\n\n[아래 예시를 참고하세요.](#usage)\n\n#### Props {/*props*/}\n\n`<form>`은 모든 [공통 엘리먼트 Props](/reference/react-dom/components/common#common-props)를 지원합니다.\n\n[`action`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form#action): a URL or function. When a URL is passed to `action` the form will behave like the HTML form component. When a function is passed to `action` the function will handle the form submission in a Transition following [the Action prop pattern](/reference/react/useTransition#exposing-action-props-from-components). The function passed to `action` may be async and will be called with a single argument containing the [form data](https://developer.mozilla.org/en-US/docs/Web/API/FormData) of the submitted form. The `action` prop can be overridden by a `formAction` attribute on a `<button>`, `<input type=\"submit\">`, or `<input type=\"image\">` component.\n\n#### 주의 사항 {/*caveats*/}\n\n* 함수를 `action`이나 `formAction`에 전달하면, HTTP 메서드는 `method` 프로퍼티의 값과 관계없이 POST로 처리합니다.\n\n---\n\n## 사용법 {/*usage*/}\n\n### 클라이언트에서 폼 제출 처리하기 {/*handle-form-submission-on-the-client*/}\n\n폼이 제출될 때 함수를 실행하기 위해, 폼의 `action` 프로퍼티에 함수를 전달하세요. [`formData`](https://developer.mozilla.org/ko/docs/Web/API/FormData)가 함수에 인수로 전달되어, 폼에서 전달된 데이터에 접근할 수 있습니다. 이 점이 URL만 받던 기존 [HTML action](https://developer.mozilla.org/ko/docs/Web/HTML/Element/form)과의 차이점입니다. After the `action` function succeeds, all uncontrolled field elements in the form are reset.\n\n<Sandpack>\n\n```js src/App.js\nexport default function Search() {\n  function search(formData) {\n    const query = formData.get(\"query\");\n    alert(`'${query}'을(를) 검색했습니다.`);\n  }\n  return (\n    <form action={search}>\n      <input name=\"query\" />\n      <button type=\"submit\">검색</button>\n    </form>\n  );\n}\n```\n\n</Sandpack>\n\n### 서버 함수에서 폼 제출 처리하기 {/*handle-form-submission-with-a-server-function*/}\n\n입력 및 제출 버튼과 함께 `<form>`을 렌더링하세요. 폼을 제출할 때 해당 함수를 실행하기 위해 서버 함수([`'use server'`](/reference/rsc/use-server)가 표시된 함수)를 폼의 `action` 프로퍼티로 전달하세요.\n\n`<form action>`에 서버 함수를 전달하면 자바스크립트가 활성화되기 전이나 코드가 로드되기 전에 사용자가 폼을 제출할 수 있습니다. 이는 연결 상태나 기계가 느리거나 자바스크립트가 비활성화된 사용자에게 유용하고, `action` 프로퍼티에 URL이 전달될 때와 폼이 동작하는 방식은 비슷합니다.\n\n`<form>`의 액션에 데이터를 제공하기 위해 폼 필드의 `hidden`을 사용할 수 있습니다. 서버 함수는 [`formData`](https://developer.mozilla.org/ko/docs/Web/API/FormData) 대신 `hidden`이 적용된 폼 필드 데이터를 불러올 수 있습니다.\n\n\n```jsx\nimport { updateCart } from './lib.js';\n\nfunction AddToCart({productId}) {\n  async function addToCart(formData) {\n    'use server'\n    const productId = formData.get('productId')\n    await updateCart(productId)\n  }\n  return (\n    <form action={addToCart}>\n        <input type=\"hidden\" name=\"productId\" value={productId} />\n        <button type=\"submit\">장바구니에 추가</button>\n    </form>\n  );\n}\n```\n\n폼 액션에 따른 데이터를 제공하기 위해 `hidden` 폼 필드를 사용하는 대신에 <CodeStep step={1}>`bind`</CodeStep>를 호출해 추가 인수를 제공할 수 있습니다. 이렇게 하면 함수에 인수로 전달되는 <CodeStep step={3}>`formData`</CodeStep> 외에 새 인수(<CodeStep step={2}>`productId`</CodeStep>)가 함수에 바인딩됩니다.\n\n```jsx [[1, 8, \"bind\"], [2,8, \"productId\"], [2,4, \"productId\"], [3,4, \"formData\"]]\nimport { updateCart } from './lib.js';\n\nfunction AddToCart({productId}) {\n  async function addToCart(productId, formData) {\n    \"use server\";\n    await updateCart(productId)\n  }\n  const addProductToCart = addToCart.bind(null, productId);\n  return (\n    <form action={addProductToCart}>\n      <button type=\"submit\">장바구니에 추가</button>\n    </form>\n  );\n}\n```\n\n`<form>`이 [서버 컴포넌트](/reference/rsc/use-client)에 의해 렌더링되고 [서버 함수](/reference/rsc/server-functions)가 `<form>`의 `action` 프로퍼티에 전달되면, 폼은 [점진적으로 향상](https://developer.mozilla.org/ko/docs/Glossary/Progressive_Enhancement)됩니다.\n\n### 폼이 제출되는 동안 대기 상태 보여주기 {/*display-a-pending-state-during-form-submission*/}\n\n폼이 제출되는 동안 대기<sup>Pending</sup> 상태를 보여주기 위해, `<form>`이 렌더링되는 컴포넌트 안에서 `useFormStatus` Hook을 호출해 반환된 `pending` 프로퍼티를 읽을 수 있습니다.\n\n여기 폼이 제출되고 있음을 나타내기 위해 `pending` 프로퍼티를 사용하였습니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { useFormStatus } from \"react-dom\";\nimport { submitForm } from \"./actions.js\";\n\nfunction Submit() {\n  const { pending } = useFormStatus();\n  return (\n    <button type=\"submit\" disabled={pending}>\n      {pending ? \"제출중...\" : \"제출\"}\n    </button>\n  );\n}\n\nfunction Form({ action }) {\n  return (\n    <form action={action}>\n      <Submit />\n    </form>\n  );\n}\n\nexport default function App() {\n  return <Form action={submitForm} />;\n}\n```\n\n```js src/actions.js hidden\nexport async function submitForm(query) {\n    await new Promise((res) => setTimeout(res, 1000));\n}\n```\n\n</Sandpack>\n\n`useFormStatus` Hook에 대해 더 알고 싶다면, [참고 문서](/reference/react-dom/hooks/useFormStatus)를 확인하세요.\n\n### 낙관적으로 폼 데이터 업데이트하기 {/*optimistically-updating-form-data*/}\n\n`useOptimistic` Hook은 네트워크 요청과 같은 백그라운드의 작업이 끝나기 전에 사용자 인터페이스에 낙관적으로 업데이트하는 방법을 제공합니다. 폼의 맥락에서 이 기술은 앱을 더욱 반응형으로 느끼게 해줍니다. 사용자가 폼을 제출하면 인터페이스는 사용자가 기대하는 결과물로 즉시 업데이트됩니다.\n\n예를 들어, 사용자가 폼에 메시지를 입력하고 \"전송\" 버튼을 클릭하면 `useOptimistic` Hook은 \"전송중...\" 라벨과 함께 메시지가 서버에 보내지기 전에 리스트에 즉시 보입니다. 이러한 '낙관적인' 접근 방식은 속도와 반응성이 뛰어나다는 인상을 줍니다. 그다음 폼은 실제로 백그라운드에 메시지 보내기를 시도합니다. 서버에 메시지가 잘 도착하면, \"전송중...\" 라벨은 사라집니다.\n\n\n<Sandpack>\n\n\n```js src/App.js\nimport { useOptimistic, useState, useRef } from \"react\";\nimport { deliverMessage } from \"./actions.js\";\n\nfunction Thread({ messages, sendMessage }) {\n  const formRef = useRef();\n  async function formAction(formData) {\n    addOptimisticMessage(formData.get(\"message\"));\n    formRef.current.reset();\n    await sendMessage(formData);\n  }\n  const [optimisticMessages, addOptimisticMessage] = useOptimistic(\n    messages,\n    (state, newMessage) => [\n      ...state,\n      {\n        text: newMessage,\n        sending: true\n      }\n    ]\n  );\n\n  return (\n    <>\n      {optimisticMessages.map((message, index) => (\n        <div key={index}>\n          {message.text}\n          {!!message.sending && <small> (전송중...)</small>}\n        </div>\n      ))}\n      <form action={formAction} ref={formRef}>\n        <input type=\"text\" name=\"message\" placeholder=\"Hello!\" />\n        <button type=\"submit\">전송</button>\n      </form>\n    </>\n  );\n}\n\nexport default function App() {\n  const [messages, setMessages] = useState([\n    { text: \"Hello there!\", sending: false, key: 1 }\n  ]);\n  async function sendMessage(formData) {\n    const sentMessage = await deliverMessage(formData.get(\"message\"));\n    setMessages((messages) => [...messages, { text: sentMessage }]);\n  }\n  return <Thread messages={messages} sendMessage={sendMessage} />;\n}\n```\n\n```js src/actions.js\nexport async function deliverMessage(message) {\n  await new Promise((res) => setTimeout(res, 1000));\n  return message;\n}\n```\n\n</Sandpack>\n\n[//]: # 'Uncomment the next line, and delete this line after the `useOptimistic` reference documentation page is published'\n[//]: # 'To learn more about the `useOptimistic` Hook see the [reference documentation](/reference/react/useOptimistic).'\n\n### 폼 제출 오류 처리하기 {/*handling-form-submission-errors*/}\n\n`<form>`의 `action` 프로퍼티로 전달된 어떤 함수는 오류를 던지기도 합니다. 이런 오류를 `<form>`에 에러 바운더리를 감싸 해결할 수 있습니다. 만약 `<form>`의 `action` 프로퍼티에서 호출된 함수가 오류를 던진다면 에러 바운더리의 Fallback이 보이게 됩니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { ErrorBoundary } from \"react-error-boundary\";\n\nexport default function Search() {\n  function search() {\n    throw new Error(\"search error\");\n  }\n  return (\n    <ErrorBoundary\n      fallback={<p>폼 제출 중에 오류가 발생했습니다.</p>}\n    >\n      <form action={search}>\n        <input name=\"query\" />\n        <button type=\"submit\">검색</button>\n      </form>\n    </ErrorBoundary>\n  );\n}\n\n```\n\n```json package.json hidden\n{\n  \"dependencies\": {\n    \"react\": \"19.0.0-rc-3edc000d-20240926\",\n    \"react-dom\": \"19.0.0-rc-3edc000d-20240926\",\n    \"react-scripts\": \"^5.0.0\",\n    \"react-error-boundary\": \"4.0.3\"\n  },\n  \"main\": \"/index.js\",\n  \"devDependencies\": {}\n}\n```\n\n</Sandpack>\n\n### 자바스크립트 없이 폼 제출 오류 보여주기 {/*display-a-form-submission-error-without-javascript*/}\n\n점진적 향상을 위해 자바스크립트 번들이 로드되기 전 오류 메시지를 보여주기 위해 다음 요소들이 지켜져야 합니다.\n\n1. `<form>` be rendered by a [Client Component](/reference/rsc/use-client)\n1. the function passed to the `<form>`'s `action` prop be a [Server Function](/reference/rsc/server-functions)\n1. the `useActionState` Hook be used to display the error message\n\n`useActionState`는 [서버 함수](/reference/rsc/use-server)와 초기 State라는 두 개의 매개변수를 가집니다. `useActionState`는 State 변수와 액션이라는 두 개의 값을 반환합니다. `useActionState`를 통해 반환된 액션은 폼의 `action` 프로퍼티에 전달될 수 있습니다. `useActionState`를 통해 반환된 상태 변수는 오류 메시지를 보여주는 데 사용됩니다. `useActionState`에 전달된 [서버 함수](/reference/rsc/server-functions)에서 반환된 값은 State 변수를 업데이트하는 데 사용됩니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { useActionState } from \"react\";\nimport { signUpNewUser } from \"./api\";\n\nexport default function Page() {\n  async function signup(prevState, formData) {\n    \"use server\";\n    const email = formData.get(\"email\");\n    try {\n      await signUpNewUser(email);\n      alert(`\"${email}\"을 등록했어요`);\n    } catch (err) {\n      return err.toString();\n    }\n  }\n  const [message, signupAction] = useActionState(signup, null);\n  return (\n    <>\n      <h1>뉴스레터에 가입하세요</h1>\n      <p>같은 이메일로 두 번 가입하여 오류를 확인하세요.</p>\n      <form action={signupAction} id=\"signup-form\">\n        <label htmlFor=\"email\">이메일: </label>\n        <input name=\"email\" id=\"email\" placeholder=\"react@example.com\" />\n        <button>가입하기</button>\n        {!!message && <p>{message}</p>}\n      </form>\n    </>\n  );\n}\n```\n\n```js src/api.js hidden\nlet emails = [];\n\nexport async function signUpNewUser(newEmail) {\n  if (emails.includes(newEmail)) {\n    throw new Error(\"이 이메일 주소는 이미 등록되었습니다.\");\n  }\n  emails.push(newEmail);\n}\n```\n\n</Sandpack>\n\n[`useActionState`](/reference/react/useActionState) 문서를 통해 폼 작업에서 상태를 업데이트하는 방법에 대해 자세히 알아보세요.\n\n### 다양한 제출 타입 처리하기 {/*handling-multiple-submission-types*/}\n\n사용자가 누른 버튼에 따라 여러 제출 작업을 처리하도록 폼을 설계할 수 있습니다. 폼 내부의 각 버튼은 `formAction` 프로퍼티를 설정하여 고유한 동작 또는 동작과 연결할 수 있습니다.\n\n사용자가 특정 버튼을 클릭하면 폼이 제출되고 해당 버튼의 속성 및 동작으로 정의된 해당 동작이 실행됩니다. 예를 들어, 폼은 기본적으로 검토를 위해 문서를 제출하지만 `formAction`이 설정된 별도의 버튼이 있어 문서를 초안으로 저장할 수 있습니다.\n\n<Sandpack>\n\n```js src/App.js\nexport default function Search() {\n  function publish(formData) {\n    const content = formData.get(\"content\");\n    const button = formData.get(\"button\");\n    alert(`'${button}' 버튼으로 '${content}'가 발행되었습니다.`);\n  }\n\n  function save(formData) {\n    const content = formData.get(\"content\");\n    alert(`'${content}' 초안이 저장되었습니다!`);\n  }\n\n  return (\n    <form action={publish}>\n      <textarea name=\"content\" rows={4} cols={40} />\n      <br />\n      <button type=\"submit\" name=\"button\" value=\"submit\">발행</button>\n      <button formAction={save}>초안 저장</button>\n    </form>\n  );\n}\n```\n\n</Sandpack>\n"
  },
  {
    "path": "src/content/reference/react-dom/components/index.md",
    "content": "---\ntitle: \"React DOM 컴포넌트\"\n---\n\n<Intro>\n\nReact는 브라우저에 내장된 모든 [HTML](https://developer.mozilla.org/en-US/docs/Web/HTML/Element)과 [SVG](https://developer.mozilla.org/en-US/docs/Web/SVG/Element) 컴포넌트를 지원합니다.\n\n</Intro>\n\n---\n\n## 공통 컴포넌트 {/*common-components*/}\n\n브라우저에 내장된 모든 컴포넌트는 일부 Props와 이벤트를 지원합니다.\n\n* [공통 컴포넌트 (예: `<div>`)](/reference/react-dom/components/common)\n\n`ref`와 `dangerouslySetInnerHTML`같은 React 고유의 Props를 포함합니다.\n\n---\n\n## 폼<sup>Form</sup> 컴포넌트 {/*form-components*/}\n\n다음과 같은 브라우저에 내장된 컴포넌트는 사용자 입력을 받습니다.\n\n* [`<input>`](/reference/react-dom/components/input)\n* [`<select>`](/reference/react-dom/components/select)\n* [`<textarea>`](/reference/react-dom/components/textarea)\n\n`value` 프로퍼티를 전달하여 <em>[제어](/reference/react-dom/components/input#controlling-an-input-with-a-state-variable)</em>할 수 있기 때문에 React에서 특별합니다.\n\n---\n\n## Resource and Metadata Components {/*resource-and-metadata-components*/}\n\n다음 브라우저 컴포넌트들을 사용하면 외부 리소스를 로드하거나 메타데이터로 문서에 주석을 달 수 있습니다.\n\n* [`<link>`](/reference/react-dom/components/link)\n* [`<meta>`](/reference/react-dom/components/meta)\n* [`<script>`](/reference/react-dom/components/script)\n* [`<style>`](/reference/react-dom/components/style)\n* [`<title>`](/reference/react-dom/components/title)\n\n위 컴포넌트들은 React에서 특별하게 다뤄집니다. React는 위 컴포넌트들을 document head 내부에 렌더링하고, 리소스를 불러올 동안 일시 중단하고, 각 특정 구성 요소의 참조 페이지에 설명된 다른 동작을 시행합니다.\n\n---\n\n## 모든 HTML 컴포넌트 {/*all-html-components*/}\n\nReact는 브라우저에 내장된 모든 HTML 컴포넌트를 지원합니다. 이는 다음과 같은 컴포넌트들을 포함합니다.\n\n* [`<aside>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/aside)\n* [`<audio>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/audio)\n* [`<b>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/b)\n* [`<base>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base)\n* [`<bdi>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/bdi)\n* [`<bdo>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/bdo)\n* [`<blockquote>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/blockquote)\n* [`<body>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/body)\n* [`<br>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/br)\n* [`<button>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button)\n* [`<canvas>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/canvas)\n* [`<caption>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/caption)\n* [`<cite>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/cite)\n* [`<code>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/code)\n* [`<col>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/col)\n* [`<colgroup>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/colgroup)\n* [`<data>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/data)\n* [`<datalist>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/datalist)\n* [`<dd>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dd)\n* [`<del>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/del)\n* [`<details>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/details)\n* [`<dfn>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dfn)\n* [`<dialog>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog)\n* [`<div>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/div)\n* [`<dl>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dl)\n* [`<dt>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dt)\n* [`<em>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/em)\n* [`<embed>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/embed)\n* [`<fieldset>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/fieldset)\n* [`<figcaption>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/figcaption)\n* [`<figure>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/figure)\n* [`<footer>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/footer)\n* [`<form>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form)\n* [`<h1>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/h1)\n* [`<head>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/head)\n* [`<header>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/header)\n* [`<hgroup>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/hgroup)\n* [`<hr>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/hr)\n* [`<html>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/html)\n* [`<i>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/i)\n* [`<iframe>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe)\n* [`<img>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img)\n* [`<input>`](/reference/react-dom/components/input)\n* [`<ins>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/ins)\n* [`<kbd>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/kbd)\n* [`<label>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/label)\n* [`<legend>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/legend)\n* [`<li>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/li)\n* [`<link>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link)\n* [`<main>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/main)\n* [`<map>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/map)\n* [`<mark>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/mark)\n* [`<menu>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/menu)\n* [`<meta>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta)\n* [`<meter>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meter)\n* [`<nav>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/nav)\n* [`<noscript>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/noscript)\n* [`<object>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/object)\n* [`<ol>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/ol)\n* [`<optgroup>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/optgroup)\n* [`<option>`](/reference/react-dom/components/option)\n* [`<output>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/output)\n* [`<p>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/p)\n* [`<picture>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/picture)\n* [`<pre>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/pre)\n* [`<progress>`](/reference/react-dom/components/progress)\n* [`<q>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/q)\n* [`<rp>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/rp)\n* [`<rt>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/rt)\n* [`<ruby>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/ruby)\n* [`<s>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/s)\n* [`<samp>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/samp)\n* [`<script>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script)\n* [`<section>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/section)\n* [`<select>`](/reference/react-dom/components/select)\n* [`<slot>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/slot)\n* [`<small>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/small)\n* [`<source>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/source)\n* [`<span>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/span)\n* [`<strong>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/strong)\n* [`<style>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/style)\n* [`<sub>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/sub)\n* [`<summary>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/summary)\n* [`<sup>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/sup)\n* [`<table>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/table)\n* [`<tbody>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/tbody)\n* [`<td>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/td)\n* [`<template>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template)\n* [`<textarea>`](/reference/react-dom/components/textarea)\n* [`<tfoot>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/tfoot)\n* [`<th>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/th)\n* [`<thead>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/thead)\n* [`<time>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/time)\n* [`<title>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/title)\n* [`<tr>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/tr)\n* [`<track>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/track)\n* [`<u>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/u)\n* [`<ul>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/ul)\n* [`<var>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/var)\n* [`<video>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/video)\n* [`<wbr>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/wbr)\n\n<Note>\n\n[DOM 표준](https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model)과 유사하게 React는 프로퍼티에 `camelCase` 표기법을 사용합니다. 예를 들어 `tabindex` 대신 `tabIndex`로 작성합니다. [온라인 변환기](https://transform.tools/html-to-jsx)를 사용하여 기존 HTML을 JSX로 변환할 수 있습니다.\n\n</Note>\n\n---\n\n### 커스텀 HTML 요소 {/*custom-html-elements*/}\n\nIf you render a tag with a dash, like `<my-element>`, React will assume you want to render a [custom HTML element.](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements)\n\n[`is`](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/is) 어트리뷰트를 사용하여 브라우저 내장 HTML 요소를 렌더링하면 커스텀 엘리먼트로 취급됩니다.\n\n#### Setting values on custom elements {/*attributes-vs-properties*/}\n\nCustom elements have two methods of passing data into them:\n\n1) Attributes: Which are displayed in markup and can only be set to string values\n2) Properties: Which are not displayed in markup and can be set to arbitrary JavaScript values\n\nBy default, React will pass values bound in JSX as attributes:\n\n```jsx\n<my-element value=\"Hello, world!\"></my-element>\n```\n\nNon-string JavaScript values passed to custom elements will be serialized by default:\n\n```jsx\n// Will be passed as `\"1,2,3\"` as the output of `[1,2,3].toString()`\n<my-element value={[1,2,3]}></my-element>\n```\n\nReact will, however, recognize an custom element's property as one that it may pass arbitrary values to if the property name shows up on the class during construction:\n\n<Sandpack>\n\n```js src/index.js hidden\nimport {MyElement} from './MyElement.js';\nimport { createRoot } from 'react-dom/client';\nimport {App} from \"./App.js\";\n\ncustomElements.define('my-element', MyElement);\n\nconst root = createRoot(document.getElementById('root'))\nroot.render(<App />);\n```\n\n```js src/MyElement.js active\nexport class MyElement extends HTMLElement {\n  constructor() {\n    super();\n    // The value here will be overwritten by React \n    // when initialized as an element\n    this.value = undefined;\n  }\n\n  connectedCallback() {\n    this.innerHTML = this.value.join(\", \");\n  }\n}\n```\n\n```js src/App.js\nexport function App() {\n  return <my-element value={[1,2,3]}></my-element>\n}\n```\n\n</Sandpack>\n\n#### Listening for events on custom elements {/*custom-element-events*/}\n\nA common pattern when using custom elements is that they may dispatch [`CustomEvent`s](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent) rather than accept a function to call when an event occur. You can listen for these events using an `on` prefix when binding to the event via JSX.\n\n<Sandpack>\n\n```js src/index.js hidden\nimport {MyElement} from './MyElement.js';\nimport { createRoot } from 'react-dom/client';\nimport {App} from \"./App.js\";\n\ncustomElements.define('my-element', MyElement);\n\nconst root = createRoot(document.getElementById('root'))\nroot.render(<App />);\n```\n\n```javascript src/MyElement.js\nexport class MyElement extends HTMLElement {\n  constructor() {\n    super();\n    this.test = undefined;\n    this.emitEvent = this._emitEvent.bind(this);\n  }\n\n  _emitEvent() {\n    const event = new CustomEvent('speak', {\n      detail: {\n        message: 'Hello, world!',\n      },\n    });\n    this.dispatchEvent(event);\n  }\n\n  connectedCallback() {\n    this.el = document.createElement('button');\n    this.el.innerText = 'Say hi';\n    this.el.addEventListener('click', this.emitEvent);\n    this.appendChild(this.el);\n  }\n\n  disconnectedCallback() {\n    this.el.removeEventListener('click', this.emitEvent);\n  }\n}\n```\n\n```jsx src/App.js active\nexport function App() {\n  return (\n    <my-element\n      onspeak={e => console.log(e.detail.message)}\n    ></my-element>\n  )\n}\n```\n\n</Sandpack>\n\n<Note>\n\nEvents are case-sensitive and support dashes (`-`). Preserve the casing of the event and include all dashes when listening for custom element's events:\n\n```jsx\n// Listens for `say-hi` events\n<my-element onsay-hi={console.log}></my-element>\n// Listens for `sayHi` events\n<my-element onsayHi={console.log}></my-element>\n```\n\n</Note>\n---\n\n## 모든 SVG 컴포넌트 {/*all-svg-components*/}\n\nReact는 브라우저에 내장된 모든 SVG 엘리먼트를 지원합니다. 이는 다음과 같은 것을 포함합니다.\n\n* [`<a>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/a)\n* [`<animate>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/animate)\n* [`<animateMotion>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/animateMotion)\n* [`<animateTransform>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/animateTransform)\n* [`<circle>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/circle)\n* [`<clipPath>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/clipPath)\n* [`<defs>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/defs)\n* [`<desc>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/desc)\n* [`<discard>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/discard)\n* [`<ellipse>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/ellipse)\n* [`<feBlend>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/feBlend)\n* [`<feColorMatrix>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/feColorMatrix)\n* [`<feComponentTransfer>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/feComponentTransfer)\n* [`<feComposite>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/feComposite)\n* [`<feConvolveMatrix>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/feConvolveMatrix)\n* [`<feDiffuseLighting>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/feDiffuseLighting)\n* [`<feDisplacementMap>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/feDisplacementMap)\n* [`<feDistantLight>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/feDistantLight)\n* [`<feDropShadow>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/feDropShadow)\n* [`<feFlood>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/feFlood)\n* [`<feFuncA>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/feFuncA)\n* [`<feFuncB>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/feFuncB)\n* [`<feFuncG>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/feFuncG)\n* [`<feFuncR>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/feFuncR)\n* [`<feGaussianBlur>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/feGaussianBlur)\n* [`<feImage>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/feImage)\n* [`<feMerge>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/feMerge)\n* [`<feMergeNode>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/feMergeNode)\n* [`<feMorphology>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/feMorphology)\n* [`<feOffset>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/feOffset)\n* [`<fePointLight>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/fePointLight)\n* [`<feSpecularLighting>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/feSpecularLighting)\n* [`<feSpotLight>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/feSpotLight)\n* [`<feTile>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/feTile)\n* [`<feTurbulence>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/feTurbulence)\n* [`<filter>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/filter)\n* [`<foreignObject>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/foreignObject)\n* [`<g>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/g)\n* `<hatch>`\n* `<hatchpath>`\n* [`<image>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/image)\n* [`<line>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/line)\n* [`<linearGradient>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/linearGradient)\n* [`<marker>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/marker)\n* [`<mask>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/mask)\n* [`<metadata>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/metadata)\n* [`<mpath>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/mpath)\n* [`<path>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/path)\n* [`<pattern>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/pattern)\n* [`<polygon>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/polygon)\n* [`<polyline>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/polyline)\n* [`<radialGradient>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/radialGradient)\n* [`<rect>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/rect)\n* [`<script>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/script)\n* [`<set>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/set)\n* [`<stop>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/stop)\n* [`<style>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/style)\n* [`<svg>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/svg)\n* [`<switch>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/switch)\n* [`<symbol>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/symbol)\n* [`<text>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/text)\n* [`<textPath>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/textPath)\n* [`<title>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/title)\n* [`<tspan>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/tspan)\n* [`<use>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/use)\n* [`<view>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/view)\n\n<Note>\n\n[DOM 표준](https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model)과 유사하게 React는 프로퍼티에 `camelCase` 표기법을 사용합니다. 예를 들어 `tabindex` 대신 `tabIndex`를 작성합니다. [온라인 변환기](https://transform.tools/)를 사용하여 기존 SVG를 JSX로 변환할 수 있습니다.\n\n네임스페이스 어트리뷰트 또한 콜론 없이 작성해야 합니다.\n\n* `xlink:actuate` 대신 `xlinkActuate`\n* `xlink:arcrole` 대신 `xlinkArcrole`\n* `xlink:href` 대신 `xlinkHref`\n* `xlink:role` 대신 `xlinkRole`\n* `xlink:show` 대신 `xlinkShow`\n* `xlink:title` 대신 `xlinkTitle`\n* `xlink:type` 대신 `xlinkType`\n* `xml:base` 대신 `xmlBase`\n* `xml:lang` 대신 `xmlLang`\n* `xml:space` 대신 `xmlSpace`\n* `xmlns:xlink` 대신 `xmlnsXlink`\n\n</Note>\n"
  },
  {
    "path": "src/content/reference/react-dom/components/input.md",
    "content": "---\ntitle: \"<input>\"\n---\n\n<Intro>\n\n[내장 브라우저 `<input>` 컴포넌트](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input)를 사용하면 여러 종류의 폼 입력<sup>Input</sup>을 렌더링할 수 있습니다.\n\n```js\n<input />\n```\n\n</Intro>\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `<input>` {/*input*/}\n\n입력<sup>Input</sup>을 표시하려면, [`<input>` 브라우저 내장 컴포넌트](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input)를 렌더링하세요.\n\n```js\n<input name=\"myInput\" />\n```\n\n[아래 예시를 참고하세요.](#usage)\n\n#### Props {/*props*/}\n\n`<input>`은 모든 [공통 엘리먼트 Props](/reference/react-dom/components/common#common-props)를 지원합니다.\n\n- [`formAction`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#formaction): A string or function. Overrides the parent `<form action>` for `type=\"submit\"` and `type=\"image\"`. When a URL is passed to `action` the form will behave like a standard HTML form. When a function is passed to `formAction` the function will handle the form submission. See [`<form action>`](/reference/react-dom/components/form#props).\n\nYou can [make an input controlled](#controlling-an-input-with-a-state-variable) by passing one of these props:\n\n* [`checked`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement#checked): 불리언 타입. 체크박스 입력 또는 라디오 버튼에서 선택 여부를 제어합니다.\n* [`value`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement#value): 문자열 타입. 텍스트 입력의 경우 텍스트를 제어합니다. (라디오 버튼의 경우 폼 데이터를 지정합니다.)\n\n둘 중 하나를 전달할 때는 반드시 전달된 값을 업데이트하는 `onChange` 핸들러도 함께 전달해야 합니다.\n\n다음의 `<input>` Props는 제어되지 않는 입력들에만 적용됩니다.\n\n* [`defaultChecked`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement#defaultChecked): 불리언 타입. `type=\"checkbox\"` 와 `type=\"radio\"` 입력에 대한 [초기값](#providing-an-initial-value-for-an-input)을 지정합니다.\n* [`defaultValue`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement#defaultValue): 문자열 타입. 텍스트 입력에 대한 [초기값](#providing-an-initial-value-for-an-input)을 지정합니다.\n\n다음의 `<input>` Props는 제어되지 않는 입력과 제어되는 입력 모두에 적용됩니다.\n\n* [`accept`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#accept): 문자열 타입. `type=\"file\"` 입력에서 허용할 파일 형식을 지정합니다.\n* [`alt`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#alt): 문자열 타입. `type=\"image\"` 입력에서 대체 이미지 텍스트를 지정합니다.\n* [`capture`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#capture): 문자열 타입. `type=\"file\"` 입력으로 캡처한 미디어(마이크, 비디오 또는 카메라)를 지정합니다.\n* [`autoComplete`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#autocomplete): 문자열 타입.  [자동 완성 동작들](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete#values) 중 가능한 하나를 지정합니다.\n* [`autoFocus`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#autofocus): 불리언 타입. `true`일 경우 React는 마운트 시 엘리먼트에 포커스를 맞춥니다.\n* [`dirname`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#dirname): 문자열 타입. 엘리먼트 방향성에 대한 폼 필드 이름을 지정합니다.\n* [`disabled`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#disabled): 불리언 타입. `true`일 경우, 입력은 상호작용이 불가능해지며 흐릿하게 보입니다.\n* `children`: `<input>` 은 자식을 받지 않습니다.\n* [`form`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#form): 문자열 타입. 입력이 속하는 `<form>`의 `id`를 지정합니다. 생략 시 가장 가까운 부모 폼으로 설정됩니다.\n* [`formAction`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#formaction): 문자열 타입. `type=\"submit\"` 과 `type=\"image\"`의 부모 `<form action>` 을 덮어씁니다.\n* [`formEnctype`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#formenctype): 문자열 타입. `type=\"submit\"` 과 `type=\"image\"`의 부모 `<form enctype>` 을 덮어씁니다.\n* [`formMethod`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#formmethod): 문자열 타입. `type=\"submit\"` 과 `type=\"image\"`의 부모 `<form method>` 를 덮어씁니다.\n* [`formNoValidate`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#formnovalidate): 문자열 타입. `type=\"submit\"` 과 `type=\"image\"`의 부모 `<form noValidate>` 를 덮어씁니다.\n* [`formTarget`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#formtarget): 문자열 타입. `<form target>` for `type=\"submit\"` 과 `type=\"image\"`의 부모 `<form target>` 을 덮어씁니다.\n* [`height`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#height): 문자열 타입. `type=\"image\"` 의 이미지 높이를 지정합니다.\n* [`list`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#list): 문자열 타입.  `<datalist>` 의 `id` 를 자동 완성 옵션들로 지정합니다.\n* [`max`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#max): 숫자 타입. 숫자 와 날짜 입력들의 최댓값을 지정합니다.\n* [`maxLength`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#maxlength): 숫자 타입. 텍스트와 다른 입력들의 최대 길이를 지정합니다.\n* [`min`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#min): 숫자 타입. 숫자 와 날짜 입력들의 최솟값을 지정합니다.\n* [`minLength`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#minlength): 숫자 타입. 텍스트와 다른 입력들의 최소 길이를 지정합니다.\n* [`multiple`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#multiple): 불리언 타입. `type=\"file\"` 과 `type=\"email\"` 에 대해 여러 값을 허용할지 여부를 지정합니다.\n* [`name`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#name): 문자열 타입. [폼과 제출](#reading-the-input-values-when-submitting-a-form) 되는 입력의 이름을 지정합니다.\n* `onChange`: [`이벤트` 핸들러](/reference/react-dom/components/common#event-handler) 함수. [제어되는 입력](#controlling-an-input-with-a-state-variable) 필수 요소로 가령 사용자가 키보드를 누를 때마다 실행되는 방식으로 입력 값을 변경하는 즉시 실행되며 브라우저 [`input` 이벤트](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/input_event)처럼 동작합니다.\n* `onChangeCapture`: [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 `onChange`\n* [`onInput`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/input_event): [`이벤트` 핸들러](/reference/react-dom/components/common#event-handler) 함수. 사용자가 값을 변경하는 즉시 실행됩니다. 지금까지의 용법에 비춰봤을 때 React에서는 유사하게 동작하는 `onChange`를 사용하는 것이 관습적입니다.\n* `onInputCapture`: [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 `onInput`\n* [`onInvalid`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/invalid_event): [`이벤트` 핸들러](/reference/react-dom/components/common#event-handler) 함수. 폼 제출 시 input이 유효하지 않을 경우 실행되며 `invalid` 내장 이벤트와 달리 React `onInvalid` 이벤트는 버블링됩니다.\n* `onInvalidCapture`: [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 `onInvalid`\n* [`onSelect`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/select_event): [`이벤트` 핸들러](/reference/react-dom/components/common#event-handler) 함수. `<input>` 내부의 선택 사항이 변경된 후 실행됩니다. React는 `onSelect` 이벤트를 확장하여 선택 사항이 비거나 편집 시 선택 사항에 영향을 끼치게 될 때도 실행됩니다.\n* `onSelectCapture`: [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 `onSelect`\n* [`pattern`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#pattern): 문자열 타입. `value`가 일치해야 하는 패턴을 지정합니다.\n* [`placeholder`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#placeholder): 문자열 타입. 입력 값이 비었을 때 흐린 색으로 표시됩니다.\n* [`readOnly`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#readonly): 불리언 타입. `true`일 경우 사용자가 입력을 편집할 수 없습니다.\n* [`required`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#required): 불리언 타입. `true`일 경우 폼이 제출할 값을 반드시 제공해야 합니다.\n* [`size`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#size): 숫자 타입. 너비를 설정하는 것과 비슷하지만 단위는 제어에 따라 다릅니다.\n* [`src`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#src): 문자열 타입. `type=\"image\"` 입력의 이미지 소스를 지정합니다.\n* [`step`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#step): 양수 또는 `'any'` 문자열. 유효한 값 사이의 간격을 지정합니다.\n* [`type`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#type): 문자열 타입.  [input types](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#input_types) 중의 하나\n* [`width`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#width): 문자열 타입. `type=\"image\"` 입력의 이미지 너비를 지정합니다.\n\n#### 주의 사항 {/*caveats*/}\n\n- 체크박스에는 `value` (또는 `defaultValue`)가 아닌 `checked` (또는 `defaultChecked`)가 필요합니다.\n- 텍스트 입력 영역은 문자열 `value` Prop을 받을 경우 [제어되는 것으로 취급](#controlling-an-input-with-a-state-variable)됩니다.\n- 체크박스 또는 라디오 버튼이 불리언 `checked` Prop을 받을 경우 [제어되는 것으로 취급](#controlling-an-input-with-a-state-variable)됩니다.\n- 입력은 제어되면서 동시에 비제어될 수 없습니다.\n- 입력은 생명주기 동안 제어 또는 비제어 상태를 오갈 수 없습니다.\n- 제어되는 입력엔 모두 백업 값을 동기적으로 업데이트하는 `onChange` 이벤트 핸들러가 필요합니다.\n\n---\n\n## 사용법 {/*usage*/}\n\n### 다양한 유형의 입력 표시 {/*displaying-inputs-of-different-types*/}\n\n입력을 표시하기 위해 `<input>` 컴포넌트를 렌더링하세요. 기본적으로 텍스트로 입력됩니다. 체크박스에는 `type=\"checkbox\"`, 라디오 버튼에는 `type=\"radio\"`를 전달하거나 [다른 입력 유형들 중 하나](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#input_types)를 전달할 수 있습니다.\n\n<Sandpack>\n\n```js\nexport default function MyForm() {\n  return (\n    <>\n      <label>\n        Text input: <input name=\"myInput\" />\n      </label>\n      <hr />\n      <label>\n        Checkbox: <input type=\"checkbox\" name=\"myCheckbox\" />\n      </label>\n      <hr />\n      <p>\n        Radio buttons:\n        <label>\n          <input type=\"radio\" name=\"myRadio\" value=\"option1\" />\n          Option 1\n        </label>\n        <label>\n          <input type=\"radio\" name=\"myRadio\" value=\"option2\" />\n          Option 2\n        </label>\n        <label>\n          <input type=\"radio\" name=\"myRadio\" value=\"option3\" />\n          Option 3\n        </label>\n      </p>\n    </>\n  );\n}\n```\n\n```css\nlabel { display: block; }\ninput { margin: 5px; }\n```\n\n</Sandpack>\n\n---\n\n### 입력에 레이블 제공하기 {/*providing-a-label-for-an-input*/}\n\n일반적으로 모든 `<input>`은 [`<label>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/label) 태그 안에 존재하는데, 이렇게 하면 해당 레이블이 해당 입력과 연관됨을 브라우저가 알 수 있습니다. 사용자가 레이블을 클릭하면 브라우저는 입력에 자동적으로 포커스를 맞춥니다. 스크린 리더는 사용자가 연관된 입력에 포커스를 맞출 때 레이블 캡션을 읽게 되므로 이 방식은 접근성을 위해서도 필수입니다.\n\n`<label>` 안에 `<input>`을 감쌀 수 없다면, `<input id>` 와 [`<label htmlFor>`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLLabelElement/htmlFor)에 동일한 ID를 전달해서 연관성을 부여하세요. 한 컴포넌트의 여러 인스턴스 간 충돌을 피하려면 [`useId`](/reference/react/useId)로 그러한 ID를 생성하세요.\n\n<Sandpack>\n\n```js\nimport { useId } from 'react';\n\nexport default function Form() {\n  const ageInputId = useId();\n  return (\n    <>\n      <label>\n        Your first name:\n        <input name=\"firstName\" />\n      </label>\n      <hr />\n      <label htmlFor={ageInputId}>Your age:</label>\n      <input id={ageInputId} name=\"age\" type=\"number\" />\n    </>\n  );\n}\n```\n\n```css\ninput { margin: 5px; }\n```\n\n</Sandpack>\n\n---\n\n### 입력에 초기값 제공하기 {/*providing-an-initial-value-for-an-input*/}\n\n입력의 초기값은 선택적으로 지정할 수 있습니다. 텍스트 입력을 위한 `defaultValue` 문자열을 전달하세요. 대신 체크박스와 라디오 버튼은 `defaultChecked` 불리언으로 초기값을 지정해야 합니다.\n\n<Sandpack>\n\n```js\nexport default function MyForm() {\n  return (\n    <>\n      <label>\n        Text input: <input name=\"myInput\" defaultValue=\"Some initial value\" />\n      </label>\n      <hr />\n      <label>\n        Checkbox: <input type=\"checkbox\" name=\"myCheckbox\" defaultChecked={true} />\n      </label>\n      <hr />\n      <p>\n        Radio buttons:\n        <label>\n          <input type=\"radio\" name=\"myRadio\" value=\"option1\" />\n          Option 1\n        </label>\n        <label>\n          <input\n            type=\"radio\"\n            name=\"myRadio\"\n            value=\"option2\"\n            defaultChecked={true}\n          />\n          Option 2\n        </label>\n        <label>\n          <input type=\"radio\" name=\"myRadio\" value=\"option3\" />\n          Option 3\n        </label>\n      </p>\n    </>\n  );\n}\n```\n\n```css\nlabel { display: block; }\ninput { margin: 5px; }\n```\n\n</Sandpack>\n\n---\n\n### 폼 제출 시 입력 값 읽기 {/*reading-the-input-values-when-submitting-a-form*/}\n\n입력과 [`<button type=\"submit\">`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button) 바깥을 [`<form>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form)으로 감싸면 버튼을 클릭했을 때 `<form onSubmit>` 이벤트 핸들러가 호출됩니다. 기본적으로 브라우저는 현재 URL에 폼 데이터를 전송한 후 페이지를 새로고침하며, 이러한 동작은 `e.preventDefault()`를 호출하여 덮어쓸 수 있습니다. 폼 데이터는 [`new FormData(e.target)`](https://developer.mozilla.org/en-US/docs/Web/API/FormData)로 읽으세요.\n<Sandpack>\n\n```js\nexport default function MyForm() {\n  function handleSubmit(e) {\n    // 브라우저가 페이지를 다시 로드하지 못하도록 방지합니다.\n    e.preventDefault();\n\n    // 폼 데이터를 읽습니다.\n    const form = e.target;\n    const formData = new FormData(form);\n\n    // formData를 직접 fetch body로 전달할 수 있습니다.\n    fetch('/some-api', { method: form.method, body: formData });\n\n    // 또는 순수 object로 작업할 수 있습니다.\n    const formJson = Object.fromEntries(formData.entries());\n    console.log(formJson);\n  }\n\n  return (\n    <form method=\"post\" onSubmit={handleSubmit}>\n      <label>\n        Text input: <input name=\"myInput\" defaultValue=\"Some initial value\" />\n      </label>\n      <hr />\n      <label>\n        Checkbox: <input type=\"checkbox\" name=\"myCheckbox\" defaultChecked={true} />\n      </label>\n      <hr />\n      <p>\n        Radio buttons:\n        <label><input type=\"radio\" name=\"myRadio\" value=\"option1\" /> Option 1</label>\n        <label><input type=\"radio\" name=\"myRadio\" value=\"option2\" defaultChecked={true} /> Option 2</label>\n        <label><input type=\"radio\" name=\"myRadio\" value=\"option3\" /> Option 3</label>\n      </p>\n      <hr />\n      <button type=\"reset\">Reset form</button>\n      <button type=\"submit\">Submit form</button>\n    </form>\n  );\n}\n```\n\n```css\nlabel { display: block; }\ninput { margin: 5px; }\n```\n\n</Sandpack>\n\n<Note>\n\n`<input name=\"firstName\" defaultValue=\"Taylor\" />` 예시와 같이 모든 `<input>`에 `name`을 부여하세요. 해당 `name`은 `{ firstName: \"Taylor\" }` 예시처럼 폼 데이터의 key로 쓰입니다.\n\n</Note>\n\n<Pitfall>\n\n기본적으로 `<form>` 내부의 *어느* `<button>`이든 폼을 제출합니다. 뜻밖인가요? 커스텀 `Button` React 컴포넌트의 경우 `<button>` 대신 [`<button type=\"button\">`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/button) 반환을 고려하세요. 명시성을 부여하기 위해 폼 제출용 버튼으로는 `<button type=\"submit\">`을 사용하세요.\nBy default, a `<button>` inside a `<form>` without a `type` attribute will submit it. This can be surprising! If you have your own custom `Button` React component, consider using [`<button type=\"button\">`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button) instead of `<button>` (with no type). Then, to be explicit, use `<button type=\"submit\">` for buttons that *are* supposed to submit the form.\n\n</Pitfall>\n\n---\n\n### State 변수로 입력 제어하기 {/*controlling-an-input-with-a-state-variable*/}\n\n`<input />`과 같은 입력은 *제어되지 않습니다.* `<input defaultValue=\"Initial text\" />`와 같은 [초기값을 전달](#providing-an-initial-value-for-an-input)한대도 JSX는 당장의 값을 제어하지 않고 초기값만을 지정합니다.\n\n**_제어되는_ 입력을 렌더링하려면, `value` (또는 체크박스와 라디오에는 `checked`) Prop 을 전달하세요.** React는 전달한 `value`를 항상 갖도록 입력에 강제합니다. 일반적으로 [State 변수](/reference/react/useState)를 선언하여 할 수 있습니다.\n\n```js {2,6,7}\nfunction Form() {\n  const [firstName, setFirstName] = useState(''); // State 변수를 선언합니다.\n  // ...\n  return (\n    <input\n      value={firstName} // 입력 값이 State 변수와 일치하도록 강제합니다.\n      onChange={e => setFirstName(e.target.value)} // 입력을 편집할 때마다 State 변수를 업데이트합니다.\n    />\n  );\n}\n```\n\n예를 들어 수정할 때마다 UI를 리렌더링 하는 등 State가 필요한 경우 제어된 입력이 적합합니다.\n\n```js {2,9}\nfunction Form() {\n  const [firstName, setFirstName] = useState('');\n  return (\n    <>\n      <label>\n        First name:\n        <input value={firstName} onChange={e => setFirstName(e.target.value)} />\n      </label>\n      {firstName !== '' && <p>Your name is {firstName}.</p>}\n      ...\n```\n\n또한 버튼을 클릭하는 등의 입력 State를 조정하는 여러 가지 방법을 제공하려는 경우에도 유용합니다.\n\n```js {3-4,10-11,14}\nfunction Form() {\n  // ...\n  const [age, setAge] = useState('');\n  const ageAsNumber = Number(age);\n  return (\n    <>\n      <label>\n        Age:\n        <input\n          value={age}\n          onChange={e => setAge(e.target.value)}\n          type=\"number\"\n        />\n        <button onClick={() => setAge(ageAsNumber + 10)}>\n          Add 10 years\n        </button>\n```\n\n제어되는 컴포넌트에 전달되는 `value`는 `undefined`나 `null`이 되어서는 안됩니다. 아래의 `firstName` 필드처럼 초기값을 비워두어야 하는 경우 State 변수를 빈 문자열(`''`)로 초기화 하세요.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function Form() {\n  const [firstName, setFirstName] = useState('');\n  const [age, setAge] = useState('20');\n  const ageAsNumber = Number(age);\n  return (\n    <>\n      <label>\n        First name:\n        <input\n          value={firstName}\n          onChange={e => setFirstName(e.target.value)}\n        />\n      </label>\n      <label>\n        Age:\n        <input\n          value={age}\n          onChange={e => setAge(e.target.value)}\n          type=\"number\"\n        />\n        <button onClick={() => setAge(ageAsNumber + 10)}>\n          Add 10 years\n        </button>\n      </label>\n      {firstName !== '' &&\n        <p>Your name is {firstName}.</p>\n      }\n      {ageAsNumber > 0 &&\n        <p>Your age is {ageAsNumber}.</p>\n      }\n    </>\n  );\n}\n```\n\n```css\nlabel { display: block; }\ninput { margin: 5px; }\np { font-weight: bold; }\n```\n\n</Sandpack>\n\n<Pitfall>\n\n**입력에 `onChange` 없이 `value`를 전달하면 해당 입력에 타이핑을 할 수 없게 됩니다.** `value`를 전달하여 입력을 제어하면 항상 해당 값을 가지도록 *강제합니다*. 그러므로 State 변수를 `value`로 전달해도 `onChange` 이벤트 핸들러 내에서 해당 State 변수를 동기적으로 업데이트하지 않으면 React는 키보드를 누를 때마다 입력을 처음 지정한 `value`로 되돌리게 됩니다.\n\n</Pitfall>\n\n---\n\n### 키보드를 누를 때마다 리렌더링 최적화하기 {/*optimizing-re-rendering-on-every-keystroke*/}\n\n제어된 입력을 사용할 때는 키보드를 누를 때마다 State를 설정합니다. State를 포함하는 컴포넌트가 큰 트리를 리렌더링할 경우 속도가 느려질 수 있습니다. 리렌더링 성능을 최적화할 수 있는 몇 가지 방법이 있습니다.\n\n예를 들어, 키보드를 누를 때마다 모든 페이지 내용을 리렌더링하는 폼으로 시작한다고 가정해보세요.\n\n```js {5-8}\nfunction App() {\n  const [firstName, setFirstName] = useState('');\n  return (\n    <>\n      <form>\n        <input value={firstName} onChange={e => setFirstName(e.target.value)} />\n      </form>\n      <PageContent />\n    </>\n  );\n}\n```\n\n`<PageContent />`는 입력 State에 의존하지 않으므로 입력 State를 자체 컴포넌트로 이동할 수 있습니다.\n\n```js {4,10-17}\nfunction App() {\n  return (\n    <>\n      <SignupForm />\n      <PageContent />\n    </>\n  );\n}\n\nfunction SignupForm() {\n  const [firstName, setFirstName] = useState('');\n  return (\n    <form>\n      <input value={firstName} onChange={e => setFirstName(e.target.value)} />\n    </form>\n  );\n}\n```\n\n이렇게 하면 키보드를 누를 때마다 `SignupForm`만 리렌더링하기 때문에 성능이 크게 향상됩니다.\n\n`PageContent`가 검색 입력 값에 의존하는 경우처럼 리렌더링을 피할 방법이 없는 경우 [`useDeferredValue`](/reference/react/useDeferredValue#deferring-re-rendering-for-a-part-of-the-ui)를 사용하면 많은 리렌더링 중에도 제어된 입력이 응답하도록 할 수 있습니다.\n\n---\n\n## 문제 해결 {/*troubleshooting*/}\n\n### 텍스트 입력에 타이핑을 해도 업데이트되지 않습니다 {/*my-text-input-doesnt-update-when-i-type-into-it*/}\n\n`onChange` 없이 `value`만 전달하여 입력을 렌더링하면 콘솔에 오류가 나타납니다.\n\n```js\n// 🔴 버그: 제어되는 `<input>`에 `onChange` 핸들러가 없습니다.\n<input value={something} />\n```\n\n<ConsoleBlock level=\"error\">\n\n폼 필드에 `onChange` 핸들러 없이 `value` Prop만 전달했습니다. 이렇게 하면 읽기 전용 필드를 렌더링하게 됩니다. 필드가 변경 가능해야 하는 경우 `defaultValue`를 사용하고 그렇지 않은 경우 `onChange` 또는 `readOnly`를 설정하세요.\n\n</ConsoleBlock>\n\n오류 메시지가 제안하듯 [*초기값*만 지정](#providing-an-initial-value-for-an-input)하려면 `defaultValue`를 대신 전달하세요.\n\n```js\n// ✅ 좋은 예: 제어되지 않는 `<input>`에 초기값 전달\n<input defaultValue={something} />\n```\n\n[입력을 State 변수로 제어](#controlling-an-input-with-a-state-variable)하려면 `onChange` 핸들러를 지정하세요.\n\n```js\n// ✅ 좋은 예: 제어되는 `<input>`에 `onChange` 전달\n<input value={something} onChange={e => setSomething(e.target.value)} />\n```\n\n값이 의도적으로 읽기 전용이라면 오류를 막기 위해 `readOnly` Prop을 추가하세요.\n\n```js\n// ✅ 좋은 예: 제어되는 읽기 전용 `<input>`에 `onChange` 생략\n<input value={something} readOnly={true} />\n```\n\n---\n\n### 체크박스를 클릭해도 업데이트되지 않습니다 {/*my-checkbox-doesnt-update-when-i-click-on-it*/}\n\n`onChange` 없이 `checked`만 전달하여 체크박스를 렌더링하면 콘솔에 에러가 나타납니다.\n\n```js\n// 🔴 버그: 제어되는 체크박스에 onChange 핸들러가 없습니다.\n<input type=\"checkbox\" checked={something} />\n```\n\n<ConsoleBlock level=\"error\">\n\n폼 필드에 `onChange` 핸들러 없이 `checked` Prop을 전달했습니다. 이렇게 하면 읽기 전용 필드를 렌더링하게 됩니다. 필드가 변경 가능해야 하는 경우 `defaultChecked`를 사용하고 그렇지 않은 경우 `onChange` 또는 `readOnly`를 설정하세요.\n\n</ConsoleBlock>\n\n오류 메시지가 제안하듯 [*초기값*만 지정](#providing-an-initial-value-for-an-input)하려면 `defaultChecked`를 대신 전달하세요.\n\n```js\n// ✅ 좋은 예: 제어되지 않는 체크박스에 초기값 전달\n<input type=\"checkbox\" defaultChecked={something} />\n```\n\n[체크박스를 State 변수로 제어](#controlling-an-input-with-a-state-variable)하려면 `onChange` 핸들러를 지정하세요.\n\n```js\n// ✅ 좋은 예: 제어되는 체크박스에 onChange 전달\n<input type=\"checkbox\" checked={something} onChange={e => setSomething(e.target.checked)} />\n```\n\n<Pitfall>\n\n체크박스에서는 `e.target.value`가 아닌 `e.target.checked`를 읽어야 합니다.\n\n</Pitfall>\n\n체크박스가 의도적으로 읽기 전용이라면 오류를 막기 위해 `readOnly` Prop을 추가하세요.\n\n```js\n// ✅ 좋은 예: 제어되는 읽기 전용 input에 onChange 생략\n<input type=\"checkbox\" checked={something} readOnly={true} />\n```\n\n---\n\n### 키보드를 누를 때마다 입력 커서가 입력의 처음으로 돌아갑니다 {/*my-input-caret-jumps-to-the-beginning-on-every-keystroke*/}\n\n[입력을 제어](#controlling-an-input-with-a-state-variable)할 경우 `onChange` 안에서 State 변수를 DOM에서 받아온 입력 값으로 업데이트해야 합니다.\n\nState 변수는 `e.target.value` (혹은 체크박스에서는 `e.target.checked`) 외의 값으로 업데이트할 수 없습니다.\n\n```js\nfunction handleChange(e) {\n  // 🔴 버그: 입력을 `e.target.value` 외의 값으로 업데이트합니다.\n  setFirstName(e.target.value.toUpperCase());\n}\n```\n\n비동기로 업데이트할 수도 없습니다.\n\n```js\nfunction handleChange(e) {\n  // 🔴 버그: 입력을 비동기로 업데이트합니다.\n  setTimeout(() => {\n    setFirstName(e.target.value);\n  }, 100);\n}\n```\n\n코드를 고치려면 `e.target.value`로 동기 업데이트하세요.\n\n```js\nfunction handleChange(e) {\n  // ✅ 제어되는 입력을 `e.target.value`로 동기 업데이트합니다.\n  setFirstName(e.target.value);\n}\n```\n\n이 방법으로 문제가 해결되지 않을 경우 키보드를 누를 때마다 입력이 DOM에서 제거된 후 다시 추가되고 있을 가능성이 있습니다. 실수로 리렌더링마다 [State를 재설정](/learn/preserving-and-resetting-state)하고 있다면 나타날 수 있는 현상입니다. 가령 입력이나 입력의 부모 중 하나가 매번 다른 `key` 어트리뷰트를 받거나 컴포넌트 함수 정의를 중첩시키는 경우(이는 지원되지 않으며 '내부' 컴포넌트가 항상 다른 트리로 간주되도록 합니다)에 해당 문제가 발생할 수 있습니다.\n\n---\n\n### 다음과 같은 에러가 납니다. \"A component is changing an uncontrolled input to be controlled(컴포넌트가 제어되지 않는 입력을 제어 상태로 변경합니다)\" {/*im-getting-an-error-a-component-is-changing-an-uncontrolled-input-to-be-controlled*/}\n\n\n컴포넌트에 `value`를 제공할 경우 반드시 생명주기 내내 문자열 타입으로 남아야 합니다.\n\nReact는 컴포넌트를 비제어할 것인지 제어할 것인지 의도를 알 수 없기 때문에 처음엔 `value={undefined}`를 전달했다가 나중에 다시 `value=\"some string\"`을 전달할 수는 없습니다. 제어되는 컴포넌트는 항상 `null`이나 `undefined`가 아닌 문자열 `value`를 받아야 합니다.\n\n`value`가 API나 state 변수에서 온다면 `null`이나 `undefined`로 초기화할 수 있습니다. 그럴 경우 빈 문자열(`''`)을 초기값으로 설정하거나 `value`가 문자열임을 보장하기 위해 `value={someValue ?? ''}`를 전달하세요.\n\n마찬가지로 체크박스에 `checked`를 전달하는 경우 불리언임을 보장하세요.\n"
  },
  {
    "path": "src/content/reference/react-dom/components/link.md",
    "content": "---\nlink: \"<link>\"\n---\n\n<Intro>\n\n[내장 브라우저 `<link>` 컴포넌트](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link)는 스타일시트와 같은 외부 리소스를 사용하거나 링크 메타데이터로 문서를 주석 처리할 수 있게 해줍니다.\n\n```js\n<link rel=\"icon\" href=\"favicon.ico\" />\n```\n\n</Intro>\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `<link>` {/*link*/}\n\n스타일시트, 글꼴, 아이콘과 같은 외부 리소스를 링크하거나 링크 메타데이터로 문서를 주석 처리하려면, [내장 브라우저 `<link>` 컴포넌트](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link)를 렌더링하세요. 어떤 컴포넌트에서든 `<link>`를 렌더링할 수 있으며, React는 [대부분의 경우](#special-rendering-behavior) 해당 DOM 요소를 `<head>`에 배치합니다.\n\n```js\n<link rel=\"icon\" href=\"favicon.ico\" />\n```\n\n[아래 예시를 참고하세요.](#usage)\n\n#### Props {/*props*/}\n\n`<link>`는 모든 [공통 엘리먼트 Props](/reference/react-dom/components/common#common-props)를 지원합니다.\n\n* `rel`: 문자열 타입, 필수, [리소스와의 관계](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel)를 지정합니다. React는 다른 링크와는 달리 [`rel=\"stylesheet\"` 링크를 특별하게 처리](#special-rendering-behavior)합니다.\n\n다음 속성들은 `rel=\"stylesheet\"`인 경우에 적용됩니다.\n\n* `precedence`: 문자열 타입. `<link>` DOM 노드를 문서의 `<head>` 내 다른 요소와 비교하여 순위를 지정해야 합니다. 이를 통해 어떤 스타일시트가 다른 스타일시트를 덮어쓸 수 있는지 결정합니다. 값은 우선순위에 따라 `\"reset\"`, `\"low\"`, `\"medium\"`, `\"high\"`가 될 수 있습니다. 동일한 우선순위를 가진 스타일시트는 `<link>` 또는 인라인 `<style>` 태그 또는 [`preload`](/reference/react-dom/preload)나 [`preinit`](/reference/react-dom/preinit) 함수로 로드되었는지에 관계없이 함께 적용됩니다.\n* `media`: 문자열 타입. 스타일시트를 특정 [미디어 쿼리](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_media_queries/Using_media_queries)에 제한합니다.\n* `title`: 문자열 타입. [대체 스타일시트](https://developer.mozilla.org/en-US/docs/Web/CSS/Alternative_style_sheets)의 이름을 지정합니다.\n\n다음 속성들은 `rel=stylesheet`인 경우에 적용되지만, React의 [스타일시트에 대한 특별한 처리](#special-rendering-behavior)를 비활성화합니다.\n\n* `disabled`: 불리언 타입. 스타일시트를 비활성화합니다.\n* `onError`: 함수. 스타일시트 불러오기에 실패했을 때 호출됩니다.\n* `onLoad`: 함수. 스타일시트 불러오기가 완료되었을 때 호출됩니다.\n\n다음 속성들은 `rel=\"preload\"` 나 `rel=\"modulepreload\"`인 경우에 적용됩니다.\n\n* `as`: 문자열 타입. 리소스의 유형을 지정합니다. 가능한 값은 `audio`, `document`, `embed`, `fetch`, `font`, `image`, `object`, `script`, `style`, `track`, `video`, `worker`입니다.\n* `imageSrcSet`: 문자열 타입. `as=\"image\"`인 경우에만 적용됩니다. [이미지 소스의 집합](https://developer.mozilla.org/en-US/docs/Learn/HTML/Multimedia_and_embedding/Responsive_images)을 지정합니다.\n* `imageSizes`: 문자열 타입. `as=\"image\"`인 경우에만 적용됩니다. [이미지의 크기](https://developer.mozilla.org/en-US/docs/Learn/HTML/Multimedia_and_embedding/Responsive_images)를 지정합니다.\n\n다음 속성들은 `rel=\"icon\"`이나 `rel=\"apple-touch-icon\"`인 경우에 적용됩니다.\n\n* `sizes`: 문자열 타입. [아이콘의 크기](https://developer.mozilla.org/en-US/docs/Learn/HTML/Multimedia_and_embedding/Responsive_images)를 지정합니다.\n\n다음 속성들은 모든 경우에 적용됩니다.\n\n* `href`: 문자열 타입. 연결된 리소스의 URL입니다.\n* `crossOrigin`: 문자열 타입. 사용할 [CORS 정책](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/crossorigin). 가능한 값은 `anonymous`와 `use-credentials`입니다. `as`가 `\"fetch\"`로 설정된 경우 필수입니다.\n* `referrerPolicy`: 문자열 타입. 리소스를 가져올 때 보낼 [Referrer 헤더](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#referrerpolicy)를 지정합니다. 가능한 값은 `no-referrer-when-downgrade` (기본값), `no-referrer`, `origin`, `origin-when-cross-origin`, `unsafe-url`입니다.\n* `fetchPriority`: 문자열 타입. 리소스를 가져오는 우선순위를 지정합니다. 가능한 값은 `auto` (기본값), `high`, `low`입니다.\n* `hrefLang`: 문자열 타입. 연결된 리소스의 언어입니다.\n* `integrity`: 문자열 타입. 리소스의 암호 해시로 [진위를 확인](https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity)합니다.\n* `type`: 문자열 타입. 연결된 리소스의 MIME 유형입니다.\n\n다음 React 속성들은 **권장하지 않습니다.**\n\n* `blocking`: 문자열 타입. `\"render\"`로 설정하면 스타일시트가 로드될 때까지 브라우저가 페이지를 렌더링하지 않도록 지시합니다. React는 Suspense를 사용하여 더 세밀하게 제어할 수 있습니다.\n\n#### 특별한 렌더링 동작 {/*special-rendering-behavior*/}\n\nReact는 `<link>` 컴포넌트에 해당하는 DOM 요소를 React 트리의 어디에 렌더링하든 상관없이 항상 문서의 `<head>`에 배치합니다. `<head>`는 DOM 내에서 `<link>`가 위치할 수 있는 유일한 위치이지만, 특정 페이지를 나타내는 컴포넌트가 `<link>` 컴포넌트를 자체적으로 렌더링할 수 있다면 편리하고 구성이 용이합니다.\n\n여기에는 몇 가지 예외가 있습니다.\n\n* `<link>`에 `rel=\"stylesheet\"` 속성이 있는 경우, 이 특별한 동작을 위해 반드시 `precedence` 속성이 있어야 합니다. 이는 문서 내 스타일시트의 순서가 중요하기 때문입니다. React는 다른 스타일시트와의 순서를 결정하기 위해 `precedence` 속성을 사용합니다. `precedence` 속성이 생략된 경우 특별한 동작이 없습니다.\n* `<link>`에 [`itemProp`](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/itemprop) 속성이 있는 경우, 특별한 동작이 없습니다. 이 속성은 문서 전체가 아니라 페이지의 특정 부분에 대한 메타데이터를 나타냅니다.\n* `<link>`에 `onLoad`또는 `onError` 속성이 있는 경우, 연결된 리소스의 로딩을 React 컴포넌트 내에서 수동으로 관리하기 때문입니다.\n\n#### 스타일시트에 대한 특별한 동작 {/*special-behavior-for-stylesheets*/}\n\n또한, `<link>`가 스타일시트로 연결된 경우 (즉, 속성에 `rel=\"stylesheet\"`가 있는 경우) React는 다음과 같은 방식으로 특별하게 동작합니다.\n\n* 스타일시트가 로드되는 동안 `<link>`를 렌더링하는 컴포넌트는 [일시 중단](/reference/react/Suspense)됩니다.\n* 여러 컴포넌트가 동일한 스타일시트에 대한 링크를 렌더링하는 경우, React는 중복된 링크를 제거하고 DOM에 단일 링크만 배치합니다. 두 링크는 `href` 속성이 동일하면 같은 것으로 간주합니다.\n\n위 특별한 동작에는 두 가지 예외가 있습니다.\n\n* 링크에 `precedence` 속성이 없으면 특별한 동작이 없습니다. 이는 문서 내 스타일시트의 순서가 중요하기 때문에 React는 다른 스타일시트와의 순서를 결정하기 위해 `precedence` 속성을 사용합니다.\n* `onLoad`, `onError`, `disabled` 속성을 제공하는 경우 특별한 동작이 없습니다. 이러한 속성들은 스타일시트 로딩을 컴포넌트 내에서 수동으로 관리하고 있음을 나타내기 때문입니다.\n\n위 특별한 처리에는 두 가지 주의 사항이 있습니다.\n\n* 링크가 렌더링 된 후에 React가 속성 변경을 무시합니다. (개발 중에 경고 메시지가 표시됩니다.)\n* 링크를 렌더링한 컴포넌트가 마운트 해제된 후에도 React는 링크를 DOM에 남길 수 있습니다.\n\n---\n\n## 사용법 {/*usage*/}\n\n### 관련 리소스 연결하기 {/*linking-to-related-resources*/}\n\n아이콘, 정규화된 URL, 핑백<sup>Pingback</sup>과 같은 관련 리소스에 대한 링크로 문서에 주석을 추가할 수 있습니다. React는 이 메타데이터를 React 트리의 어디에 렌더링 되든 상관없이 문서의 `<head>`에 배치합니다.\n\n<SandpackWithHTMLOutput>\n\n```js src/App.js active\nimport ShowRenderedHTML from './ShowRenderedHTML.js';\n\nexport default function BlogPage() {\n  return (\n    <ShowRenderedHTML>\n      <link rel=\"icon\" href=\"favicon.ico\" />\n      <link rel=\"pingback\" href=\"http://www.example.com/xmlrpc.php\" />\n      <h1>My Blog</h1>\n      <p>...</p>\n    </ShowRenderedHTML>\n  );\n}\n```\n\n</SandpackWithHTMLOutput>\n\n### 스타일시트 연결하기 {/*linking-to-a-stylesheet*/}\n\n컴포넌트가 올바르게 표시되기 위해 특정 스타일시트에 의존하는 경우 해당 스타일시트에 대한 링크를 컴포넌트 내에서 렌더링할 수 있습니다. 스타일시트가 로드되는 동안 컴포넌트는 [일시 중단](/reference/react/Suspense)됩니다. `precedence` 속성을 제공해야 하며 이는 React에 이 스타일시트를 다른 스타일시트와 비교하여 어디에 배치해야 하는지 알려줍니다. 높은 우선순위의 스타일시트는 낮은 우선순위의 스타일시트를 덮어쓸 수 있습니다.\n\n<Note>\n스타일시트를 사용하고 싶을 때 [`preinit`](/reference/react-dom/preinit) 함수를 호출하는 것이 유용할 수 있습니다. 이 함수를 호출하면 단순히 `<link>` 컴포넌트를 렌더링하는 것보다 브라우저가 스타일시트를 더 빨리 가져올 수 있습니다. 예를 들어 [HTTP Early Hints 응답](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/103)을 보내는 방식으로 가능합니다.\n</Note>\n\n<SandpackWithHTMLOutput>\n\n```js src/App.js active\nimport ShowRenderedHTML from './ShowRenderedHTML.js';\n\nexport default function SiteMapPage() {\n  return (\n    <ShowRenderedHTML>\n      <link rel=\"stylesheet\" href=\"sitemap.css\" precedence=\"medium\" />\n      <p>...</p>\n    </ShowRenderedHTML>\n  );\n}\n```\n\n</SandpackWithHTMLOutput>\n\n### 스타일시트 우선순위 제어하기 {/*controlling-stylesheet-precedence*/}\n\n스타일시트는 서로 충돌할 수 있으며, 이 경우 브라우저는 문서에서 나중에 오는 스타일시트를 적용합니다. React는 `precedence` 속성을 사용하여 스타일시트의 순서를 제어할 수 있도록 합니다. 이 예시에서는 세 개의 컴포넌트가 스타일시트를 렌더링하며, 동일한 `precedence` 값을 가진 스타일시트는 `<head>`에서 함께 그룹화됩니다.\n\n<SandpackWithHTMLOutput>\n\n```js src/App.js active\nimport ShowRenderedHTML from './ShowRenderedHTML.js';\n\nexport default function HomePage() {\n  return (\n    <ShowRenderedHTML>\n      <FirstComponent />\n      <SecondComponent />\n      <ThirdComponent/>\n      ...\n    </ShowRenderedHTML>\n  );\n}\n\nfunction FirstComponent() {\n  return <link rel=\"stylesheet\" href=\"first.css\" precedence=\"first\" />;\n}\n\nfunction SecondComponent() {\n  return <link rel=\"stylesheet\" href=\"second.css\" precedence=\"second\" />;\n}\n\nfunction ThirdComponent() {\n  return <link rel=\"stylesheet\" href=\"third.css\" precedence=\"first\" />;\n}\n\n```\n\n</SandpackWithHTMLOutput>\n\nNote the `precedence` values themselves are arbitrary and their naming is up to you. React will infer that precedence values it discovers first are \"lower\" and precedence values it discovers later are \"higher\".\n\n### 중복이 제거된 스타일시트 렌더링 {/*deduplicated-stylesheet-rendering*/}\n\n여러 컴포넌트에서 동일한 스타일시트를 렌더링하면 React는 문서의 `<head>`에 단일 `<link>`만 배치합니다.\n\n<SandpackWithHTMLOutput>\n\n```js src/App.js active\nimport ShowRenderedHTML from './ShowRenderedHTML.js';\n\nexport default function HomePage() {\n  return (\n    <ShowRenderedHTML>\n      <Component />\n      <Component />\n      ...\n    </ShowRenderedHTML>\n  );\n}\n\nfunction Component() {\n  return <link rel=\"stylesheet\" href=\"styles.css\" precedence=\"medium\" />;\n}\n```\n\n</SandpackWithHTMLOutput>\n\n### 문서 내 특정 항목에 링크로 주석 달기 {/*annotating-specific-items-within-the-document-with-links*/}\n\n`itemProp` 속성을 사용하여 `<link>` 컴포넌트를 문서 내 특정 항목에 관련 리소스 링크로 주석을 달 수 있습니다. 이 경우 React는 이러한 주석을 문서의 `<head>`에 *배치하지 않고* 다른 React 컴포넌트와 같이 배치합니다.\n\n```js\n<section itemScope>\n  <h3>Annotating specific items</h3>\n  <link itemProp=\"author\" href=\"http://example.com/\" />\n  <p>...</p>\n</section>\n```\n"
  },
  {
    "path": "src/content/reference/react-dom/components/meta.md",
    "content": "---\nmeta: \"<meta>\"\n---\n\n<Intro>\n\n[내장 브라우저 `<meta>` 컴포넌트](https://developer.mozilla.org/ko/docs/Web/HTML/Element/meta)를 사용하면 문서에 메타데이터를 추가할 수 있습니다.\n\n```js\n<meta name=\"keywords\" content=\"React, JavaScript, semantic markup, html\" />\n```\n\n</Intro>\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `<meta>` {/*meta*/}\n\n문서 메타데이터를 추가하려면 [내장 브라우저 `<meta>` 컴포넌트](https://developer.mozilla.org/ko/docs/Web/HTML/Element/meta)를 렌더링하세요. 어느 컴포넌트에서나 `<meta>`를 렌더링할 수 있으며, React는 항상 해당 DOM 요소를 문서의 `<head>`에 배치합니다.\n\n```js\n<meta name=\"keywords\" content=\"React, JavaScript, semantic markup, html\" />\n```\n\n[아래에서 더 많은 예시를 확인하세요.](#usage)\n\n#### Props {/*props*/}\n\n`<meta>`는 모든 [공통 엘리먼트 Props](/reference/react-dom/components/common#common-props)를 지원합니다.\n\n다음 속성 중 _하나만_ 가져야 합니다. `name`, `httpEquiv`, `charset`, `itemProp`.\n\n`<meta>` 컴포넌트는 지정된 Props에 따라 각각 다른 동작을 합니다.\n\n* `name`: 문자열. 문서에 첨부될 [메타데이터 종류](https://developer.mozilla.org/ko/docs/Web/HTML/Element/meta/name)를 지정합니다.\n* `charset`: 문자열. 문서에서 사용되는 문자 인코딩을 지원합니다. 유효한 값은 `\"utf-8\"` 뿐 입니다.\n* `httpEquiv`: 문자열. 문서를 처리할 지시 사항을 지정합니다.\n* `itemProp`: 문자열. 문서 전체가 아닌 문서 내 특정 항목에 대한 메타데이터를 지정합니다.\n* `content`: 문자열. `name` 또는 `itemProp` Props와 함께 사용 시 첨부될 메타데이터를 지정하거나, `httpEquiv` Props와 함께 사용 시 지시 사항의 동작을 지정합니다.\n\n#### 특수 렌더링 동작 {/*special-rendering-behavior*/}\n\nReact는 `<meta>` 컴포넌트가 React 트리 어디에서 렌더링되든 상관없이 해당하는 DOM 요소를 항상 문서의 `<head>` 내에 배치합니다. DOM 내에서 `<head>`는 `<meta>`가 존재할 수 있는 유일한 유효한 위치이지만, 특정 페이지를 나타내는 컴포넌트가 `<meta>` 컴포넌트를 자체적으로 렌더링할 수 있다는 점이 편리하고, 구성 가능성을 유지해 줍니다.\n\n단, `<meta>`에 [`itemProp`](https://developer.mozilla.org/ko/docs/Web/HTML/Global_attributes/itemprop) Props가 있는 경우에는 예외입니다. 이 경우에는 문서에 대한 메타데이터가 아닌 페이지의 특정 부분에 대한 메타데이터를 나타내므로 특수한 동작이 적용되지 않습니다.\n\n---\n\n## 사용법 {/*usage*/}\n\n### 문서에 메타데이터 추가하기 {/*annotating-the-document-with-metadata*/}\n\n키워드, 요약 또는 저자의 이름과 같은 메타데이터를 문서에 추가할 수 있습니다. React는 해당 메타데이터를 문서 `<head>`에 배치하며, React 트리 내에서 어디에 렌더링되든 상관없이 해당 작업이 이루어집니다.\n\n```html\n<meta name=\"author\" content=\"John Smith\" />\n<meta name=\"keywords\" content=\"React, JavaScript, semantic markup, html\" />\n<meta name=\"description\" content=\"API reference for the <meta> component in React DOM\" />\n```\n\n어느 컴포넌트에서나 `<meta>` 컴포넌트를 렌더링할 수 있습니다. React는 문서 `<head>`에 `<meta>` DOM 노드를 배치합니다.\n\n<SandpackWithHTMLOutput>\n\n```js src/App.js active\nimport ShowRenderedHTML from './ShowRenderedHTML.js';\n\nexport default function SiteMapPage() {\n  return (\n    <ShowRenderedHTML>\n      <meta name=\"keywords\" content=\"React\" />\n      <meta name=\"description\" content=\"A site map for the React website\" />\n      <h1>Site Map</h1>\n      <p>...</p>\n    </ShowRenderedHTML>\n  );\n}\n```\n\n</SandpackWithHTMLOutput>\n\n### 문서 내 특정 항목에 메타데이터 추가하기 {/*annotating-specific-items-within-the-document-with-metadata*/}\n\n`itemProp` Props와 함께 `<meta>` 컴포넌트를 사용하여 문서 내 특정 항목에 메타데이터를 추가할 수 있습니다. 이 경우, React는 이러한 주석을 문서 내 `<head>`에 배치하지 않고, 다른 React 컴포넌트처럼 배치합니다.\n\n```js\n<section itemScope>\n  <h3>Annotating specific items</h3>\n  <meta itemProp=\"description\" content=\"API reference for using <meta> with itemProp\" />\n  <p>...</p>\n</section>\n```\n"
  },
  {
    "path": "src/content/reference/react-dom/components/option.md",
    "content": "---\ntitle: \"<option>\"\n---\n\n<Intro>\n\n[내장 브라우저 `<option>` 컴포넌트](https://developer.mozilla.org/ko/docs/Web/HTML/Element/option)를 사용해 [`<select>`](/reference/react-dom/components/select) 박스 안에 옵션을 렌더링할 수 있습니다.\n\n```js\n<select>\n  <option value=\"someOption\">Some option</option>\n  <option value=\"otherOption\">Other option</option>\n</select>\n```\n\n</Intro>\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `<option>` {/*option*/}\n\n[내장 브라우저 `<option>` 컴포넌트](https://developer.mozilla.org/ko/docs/Web/HTML/Element/option)를 사용해 [`<select>`](/reference/react-dom/components/select) 박스 안에 옵션을 렌더링할 수 있습니다.\n\n```js\n<select>\n  <option value=\"someOption\">Some option</option>\n  <option value=\"otherOption\">Other option</option>\n</select>\n```\n\n[아래 예시를 참고하세요.](#usage)\n\n#### Props {/*props*/}\n\n`<option>`은 모든 [공통 엘리먼트 Props](/reference/react-dom/components/common#common-props)를 지원합니다.\n\n또한, `<option>`은 아래와 같은 Props를 지원합니다.\n\n* [`disabled`](https://developer.mozilla.org/ko/docs/Web/HTML/Element/option#disabled): 불리언 타입. `true`면 옵션을 선택할 수 없으며 흐리게 표시됩니다.\n* [`label`](https://developer.mozilla.org/ko/docs/Web/HTML/Element/option#label): 문자열 타입. 옵션의 의미를 지정합니다. 지정하지 않으면 옵션 내부의 텍스트가 사용됩니다.\n* [`value`](https://developer.mozilla.org/ko/docs/Web/HTML/Element/option#value): 이 옵션을 선택한 경우 [폼에서 상위 `<select>`를 제출할 때](/reference/react-dom/components/select#reading-the-select-box-value-when-submitting-a-form) 사용할 값입니다.\n\n#### 주의 사항 {/*caveats*/}\n\n* React는 `<option>`에서 `selected` 속성을 지원하지 않습니다. 대신, 이 옵션의 `value`를 제어되지 않은 `<select>` 박스의 경우 상위 [`<select defaultValue>`](/reference/react-dom/components/select#providing-an-initially-selected-option)에 전달하거나, 제어되는 `<select>` 박스의 경우 [`<select value>`](/reference/react-dom/components/select#controlling-a-select-box-with-a-state-variable)에 전달하세요.\n\n---\n\n## 사용법 {/*usage*/}\n\n### 옵션이 포함된 `<select>` 박스 표시하기 {/*displaying-a-select-box-with-options*/}\n\n내부에 `<option>` 컴포넌트 목록이 있는 `<select>`를 렌더링하여 `<select>` 박스를 보여줍니다. 각 `<option>`에 양식과 함께 제출할 데이터를 나타내는 `value`를 지정하세요.\n\n[`<option>` 컴포넌트 목록과 함께 `<select>`를 표시하는 방법에 대해 알아보세요.](/reference/react-dom/components/select)\n\n<Sandpack>\n\n```js\nexport default function FruitPicker() {\n  return (\n    <label>\n      Pick a fruit:\n      <select name=\"selectedFruit\">\n        <option value=\"apple\">Apple</option>\n        <option value=\"banana\">Banana</option>\n        <option value=\"orange\">Orange</option>\n      </select>\n    </label>\n  );\n}\n```\n\n```css\nselect { margin: 5px; }\n```\n\n</Sandpack>\n\n"
  },
  {
    "path": "src/content/reference/react-dom/components/progress.md",
    "content": "---\ntitle: \"<progress>\"\n---\n\n<Intro>\n\n[내장 브라우저 `<progress>` 컴포넌트](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/progress)를 사용하면 진행률 표시기를 렌더링할 수 있습니다.\n\n```js\n<progress value={0.5} />\n```\n\n</Intro>\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `<progress>` {/*progress*/}\n\n진행률 표시기를 표시하려면 [내장 브라우저 `<progress>` 컴포넌트](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/progress)를 렌더링합니다.\n\n```js\n<progress value={0.5} />\n```\n\n[아래 예시를 참고하세요.](#usage)\n\n#### Props {/*props*/}\n\n`<progress>`는 모든 [공통 엘리먼트 Props](/reference/react-dom/components/common#common-props)를 지원합니다.\n\n또한 `<progress>`는 아래와 같은 Props를 지원합니다.\n\n* [`max`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/progress#max): 숫자. 최대 `value`를 지정합니다. 기본값은 `1`입니다.\n* [`value`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/progress#value): `0`에서 `max` 사이의 숫자 또는 결정되지 않은 상태인 경우 `null`입니다. 완료된 양을 나타냅니다.\n\n---\n\n## 사용법 {/*usage*/}\n\n### 진행률 표시기 제어 {/*controlling-a-progress-indicator*/}\n\n진행률 표시기를 표시하려면 `<progress>` 컴포넌트를 렌더링합니다. `0`에서 지정한 `max` 값 사이의 숫자 `value`를 전달할 수 있습니다. `max` 값을 전달하지 않으면 기본적으로 `1`로 간주됩니다.\n\n작업이 진행 중이 아닌 경우, 진행률 표시기를 불확정 상태로 설정하려면 `value={null}`을 전달합니다.\n\n<Sandpack>\n\n```js\nexport default function App() {\n  return (\n    <>\n      <progress value={0} />\n      <progress value={0.5} />\n      <progress value={0.7} />\n      <progress value={75} max={100} />\n      <progress value={1} />\n      <progress value={null} />\n    </>\n  );\n}\n```\n\n```css\nprogress { display: block; }\n```\n\n</Sandpack>\n"
  },
  {
    "path": "src/content/reference/react-dom/components/script.md",
    "content": "---\nscript: \"<script>\"\n---\n\n<Intro>\n\n[내장 브라우저 `<script>` 컴포넌트](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script)를 사용하면 문서에 스크립트를 추가할 수 있습니다.\n\n```js\n<script> alert(\"hi!\") </script>\n```\n\n</Intro>\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `<script>` {/*script*/}\n\n문서에 인라인 또는 외부 스크립트를 추가하려면 [내장 브라우저 `<script>` 컴포넌트](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script)를 렌더링하세요. `<script>`는 어떤 컴포넌트에서든 렌더링할 수 있으며, React는 [특정 경우](#special-rendering-behavior)에 해당 DOM 요소를 문서의 `<head>`에 배치하고 중복된 동일 스크립트를 제거합니다.\n\n```js\n<script> alert(\"hi!\") </script>\n<script src=\"script.js\" />\n```\n\n[아래 예시를 참고하세요.](#usage)\n\n#### Props {/*props*/}\n\n`<script>`는 모든 [공통 엘리먼트 Props](/reference/react-dom/components/common#common-props)를 지원합니다.\n\n`children` 또는 `src` 속성을 가져야 합니다.\n\n* `children`: 문자열. 인라인 스크립트의 소스 코드.\n* `src`: 문자열. 외부 스크립트의 URL.\n\n지원하는 다른 속성들:\n\n* `async`: 불리언 값. 브라우저가 문서의 남은 부분을 처리할 때까지 스크립트 실행을 연기할 수 있도록 합니다. 이는 성능을 위한 우선적인 동작 방식입니다.\n*  `crossOrigin`: 문자열. 사용할 [CORS 정책](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/crossorigin)입니다. 가능한 값은 `anonymous`와 `use-credentials`입니다.\n* `fetchPriority`: 문자열. 여러 스크립트를 동시에 가져올 때 브라우저가 스크립트를 우선순위로 순위 지정할 수 있도록 합니다. `\"high\"`, `\"low\"`, 또는 `\"auto\"` (기본값)일 수 있습니다.\n* `integrity`: 문자열. 스크립트의 암호화 해시로, [진위성을 검증](https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity)합니다.\n* `noModule`: 불리언 값. ES 모듈을 지원하는 브라우저에서 스크립트를 비활성화합니다. ES 모듈을 지원하지 않는 브라우저에 대한 대체 스크립트를 허용합니다.\n* `nonce`: 문자열. 엄격한 콘텐츠 보안 정책을 사용할 때 [리소스를 허용하기 위해](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/nonce) 사용하는 암호화된 nonce입니다.\n* `referrer`: 문자열. 스크립트를 가져오고 스크립트가 다시 가져온 리소스를 가져올 때 보낼 [Referer 헤더를 지정합니다.](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#referrerpolicy)\n* `type`: 문자열. 스크립트가 [전통적인 스크립트, ES 모듈 또는 import 맵](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type)인지를 나타냅니다.\n\nReact의 [스크립트 특수 처리](#special-rendering-behavior)를 비활성화하는 속성들:\n\n* `onError`: 함수. 스크립트의 로딩을 실패하였을 때 호출됩니다.\n* `onLoad`: 함수. 스크립트의 로딩을 완료하였을 때 호출됩니다.\n\nReact에서 **권장하지 않는** 속성들:\n\n* `blocking`: 문자열. `\"render\"`로 설정하면 페이지가 스크립트시트를 로드할 때까지 브라우저에게 페이지를 렌더링하지 않도록 지시합니다. React는 Suspense를 사용하여 더 세밀한 제어를 제공합니다.\n* `defer`: 문자열. 문서가 로딩될 때까지 브라우저가 스크립트를 실행하지 못하도록 합니다. 스트리밍 서버에 렌더링된 컴포넌트와 호환되지 않습니다. 대신 `async` 속성을 사용하세요.\n\n#### 특별한 렌더링 동작 {/*special-rendering-behavior*/}\n\nReact는 `<script>` 컴포넌트를 문서의 `<head>`로 이동시키고, 중복된 동일 스크립트를 제거합니다.\n\n이 동작을 사용하려면 `src`와 `async={true}` 속성을 제공하세요. React는 `src`가 동일한 경우에만 중복된 스크립트를 제거합니다. 스크립트를 안전하게 이동하려면 `async` 속성이 반드시 `true`여야 합니다.\n\n이 특별한 처리에는 두 가지 주의 사항이 있습니다.\n\n* React는 스크립트를 렌더링한 후에 속성 변경을 무시합니다. (개발 환경에서는 이러한 경우에 경고가 발생합니다.)\n* React는 컴포넌트를 마운트 해제한 후에도 DOM에 스크립트를 남길 수 있습니다. (스크립트는 DOM에 삽입될 때 한 번만 실행되므로 이것은 영향을 미치지 않습니다.)\n\n---\n\n## 사용법 {/*usage*/}\n\n### 외부 스크립트 렌더링 {/*rendering-an-external-script*/}\n\n특정 스크립트에 의존하여 컴포넌트를 올바르게 표시해야 한다면, 컴포넌트 내에서 `<script>`를 렌더링할 수 있습니다.\n그러나 스크립트 로딩이 완료되기 전에 컴포넌트가 커밋될 수 있습니다.\n`load` 이벤트가 발생하면 스크립트 내용에 따라 시작할 수 있습니다. 예를 들어 `onLoad` prop 을 이용할 수 있습니다.\n\nReact는 동일한 `src`를 가진 중복된 스크립트를 제거하여 여러 컴포넌트를 렌더링하더라도 그 중 하나만 DOM에 삽입합니다.\n\n<SandpackWithHTMLOutput>\n\n```js src/App.js active\nimport ShowRenderedHTML from './ShowRenderedHTML.js';\n\nfunction Map({lat, long}) {\n  return (\n    <>\n      <script async src=\"map-api.js\" onLoad={() => console.log('script loaded')} />\n      <div id=\"map\" data-lat={lat} data-long={long} />\n    </>\n  );\n}\n\nexport default function Page() {\n  return (\n    <ShowRenderedHTML>\n      <Map />\n    </ShowRenderedHTML>\n  );\n}\n```\n\n</SandpackWithHTMLOutput>\n\n<Note>\n스크립트를 사용하려는 경우, [`preinit`](/reference/react-dom/preinit) 함수를 호출하는 것이 유리할 수 있습니다. 이 함수를 호출하면 `<script>` 컴포넌트를 그냥 렌더링하는 것보다 브라우저가 스크립트를 더 빨리 가져오도록 할 수 있습니다. 예를 들어 [HTTP Early Hints 응답](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/103)을 통해 스크립트를 더 빨리 가져올 수 있습니다.\n</Note>\n\n### 인라인 스크립트 렌더링 {/*rendering-an-inline-script*/}\n\n인라인 스크립트를 포함하려면 `<script>` 컴포넌트를 자식으로 스크립트 소스 코드와 함께 렌더링하세요. 인라인 스크립트는 중복 제거되거나 문서의 `<head>`로 이동하지 않습니다.\n\n<SandpackWithHTMLOutput>\n\n```js src/App.js active\nimport ShowRenderedHTML from './ShowRenderedHTML.js';\n\nfunction Tracking() {\n  return (\n    <script>\n      ga('send', 'pageview');\n    </script>\n  );\n}\n\nexport default function Page() {\n  return (\n    <ShowRenderedHTML>\n      <h1>My Website</h1>\n      <Tracking />\n      <p>Welcome</p>\n    </ShowRenderedHTML>\n  );\n}\n```\n\n</SandpackWithHTMLOutput>\n"
  },
  {
    "path": "src/content/reference/react-dom/components/select.md",
    "content": "---\ntitle: \"<select>\"\n---\n\n<Intro>\n\n[내장 브라우저 `<select>` 컴포넌트](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/select)는 옵션을 포함하는 select box를 렌더링합니다.\n\n```js\n<select>\n  <option value=\"someOption\">Some option</option>\n  <option value=\"otherOption\">Other option</option>\n</select>\n```\n\n</Intro>\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `<select>` {/*select*/}\n\nselect box를 표시하려면 [내장 브라우저 `<select>` 컴포넌트](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/select)를 렌더링합니다.\n\n```js\n<select>\n  <option value=\"someOption\">Some option</option>\n  <option value=\"otherOption\">Other option</option>\n</select>\n```\n\n[아래 예시를 참고하세요.](#usage)\n\n#### Props {/*props*/}\n\n`<select>`는 모든 [공통 엘리먼트 Props](/reference/react-dom/components/common#common-props)를 지원합니다.\n\n[select box를 제어](#controlling-a-select-box-with-a-state-variable)하려면 `value` Prop을 전달하세요.\n\n* `value` : 문자열 타입 (또는 [`multiple={true}`](#enabling-multiple-selection)에서 사용하는 문자열 배열)이며 어떤 옵션을 선택할지 제어합니다. `value`는 `<select>` 내부에 중첩된 `<option>`의 `value`와 일치합니다.\n\n`value`를 전달할 때, 전달된 `value`를 업데이트하는 `onChange` 핸들러를 전달해야 합니다.\n\n`<select>`가 제어되지 않는다면, `defaultValue` Prop을 전달합니다.\n\n* `defaultValue` : 문자열 타입 (또는 [`multiple={true}`](#enabling-multiple-selection)에서 사용하는 문자열 배열)이며 [초기 선택 옵션을](#providing-an-initially-selected-option) 지정합니다.\n\n`<select>` Props는 제어되지 않는 상태와 제어되는 상태 모두에 적용됩니다.\n* [`autoComplete`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/select#autocomplete): 문자열 타입. 가능한 [자동완성 동작](https://developer.mozilla.org/ko/docs/Web/HTML/Attributes/autocomplete#%EA%B0%92) 중 하나를 지정합니다.\n* [`autoFocus`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/select#autofocus): 불리언 타입. `true`이면 React가 마운트 시 해당 요소에 포커싱합니다.\n* `children`: `<select>`는 자식으로 [`<option>`](https://developer.mozilla.org/ko/docs/Web/HTML/Element/option), [`<optgroup>`](https://developer.mozilla.org/ko/docs/Web/HTML/Element/optgroup), [`<datalist>`](https://developer.mozilla.org/ko/docs/Web/HTML/Element/datalist) 컴포넌트를 받습니다. 또한 허용되는 컴포넌트 중 하나를 렌더링하기만 한다면 사용자 정의 컴포넌트를 전달할 수도 있습니다. 최종적으로 `<option>` 태그를 렌더링하는 사용자 정의 컴포넌트를 전달한다면, 렌더링되는 모든 `<option>` 은 `value`를 가져야 합니다.\n* [`disabled`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/select#disabled): 불리언 타입. `true`일 경우 select box는 상호작용할 수 없게 되고 흐리게 표시됩니다.\n* [`form`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/select#form): 문자열 타입. 이 select box가 속하는 `<form>`의  `id`를 지정합니다. 생략되면 가장 가까운 부모 폼으로 지정됩니다.\n* [`multiple`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/select#multiple): 불리언 타입. `true`이면 브라우저가 [다중 선택](#enabling-multiple-selection)을 허용합니다.\n* [`name`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/select#name): 문자열 타입. [양식과 함께 제출되는](#reading-the-select-box-value-when-submitting-a-form) select box의 이름을 지정합니다.\n* `onChange`: [`Event` 핸들러](/reference/react-dom/components/common#event-handler) 함수. [제어되는 select box](#controlling-a-select-box-with-a-state-variable)에 필수적입니다. 사용자가 다른 옵션을 선택하는 즉시 실행됩니다. 브라우저의 [`input` 이벤트](https://developer.mozilla.org/ko/docs/Web/API/HTMLElement/input_event)처럼 동작합니다.\n* `onChangeCapture`: [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 버전의 `onChange`.\n* [`onInput`](https://developer.mozilla.org/ko/docs/Web/API/HTMLElement/input_event): [`Event` 핸들러](/reference/react-dom/components/common#event-handler) 함수. 사용자에 의해 값이 변경되는 즉시 실행됩니다. 역사적인 이유로 React에서는 유사하게 동작하는 `onChange`를 대신 사용하는 것이 관용적입니다.\n* `onInputCapture`: [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 버전의 `inputCapture`.\n* [`onInvalid`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/invalid_event): [`Event` 핸들러](/reference/react-dom/components/common#event-handler) 함수. 양식 제출 시 입력 유효성 검사에 실패하면 실행됩니다. 내장 `invalid` 이벤트와 다르게, React의 `onInvalid` 이벤트는 버블링됩니다.\n* `onInvalidCapture`: [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 버전의 `invalidCapture`.\n* [`required`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/select#required): 불리언 타입. `true`일 경우 양식을 제출할 때 값이 반드시 제공되어야 합니다.\n* [`size`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/select#size): 숫자 타입. `multiple={true}`인 select에 대해, 초기에 표시되는 기본 항목의 수를 지정합니다.\n\n#### 주의 사항 {/*caveats*/}\n\n- HTML과는 달리, `selected` 어트리뷰트를 `<option>`에 전달하는 것은 지원하지 않습니다. 대신, [제어되지 않는 select box](#controlling-a-select-box-with-a-state-variable)인 경우 [`<select defaultValue>`](#providing-an-initially-selected-option)를 사용하고, [제어되어야 하는 select box](#controlling-a-select-box-with-a-state-variable)인 경우 [`<select value>`](#controlling-a-select-box-with-a-state-variable)를 사용해야 합니다.\n- `<select>`에 `value` Prop이 전달된다면, [제어되는 것으로 간주합니다.](#controlling-a-select-box-with-a-state-variable)\n- select box는 제어 상태와 비제어 상태를 동시에 행할 수 없습니다. 둘 중 하나의 상태만 가질 수 있습니다.\n- select box는 생명주기 동안 처음 설정한 제어 상태를 변경할 수 없습니다.\n- 제어되는 모든 select box는 제공되는 값을 동기적으로 업데이트하는 `onChange` 이벤트 핸들러가 필요합니다.\n\n---\n\n## 사용법 {/*usage*/}\n\n### 옵션이 담긴 select box 표시 {/*providing-options-to-a-select-box*/}\n\n`<select>`는 `<option>` 컴포넌트의 리스트를 포함하는 `<select>`를 렌더링합니다. 각 `<option>`에는 폼과 함께 제출되는 데이터인 `value`를 지정합니다.\n\n<Sandpack>\n\n```js\nexport default function FruitPicker() {\n  return (\n    <label>\n      Pick a fruit:\n      <select name=\"selectedFruit\">\n        <option value=\"apple\">Apple</option>\n        <option value=\"banana\">Banana</option>\n        <option value=\"orange\">Orange</option>\n      </select>\n    </label>\n  );\n}\n```\n\n```css\nselect { margin: 5px; }\n```\n\n</Sandpack>\n\n---\n\n### select box가 포함된 레이블 제공 {/*providing-a-label-for-a-select-box*/}\n\n레이블이 해당 select box와 연결되어 있음을 브라우저에 알리기 위해 [`<label>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/label) 태그 안에 `<select>`를 배치합니다. 사용자가 레이블을 클릭하면 브라우저는 자동으로 select box에 초점을 맞춥니다. 또한, 접근성을 위해 필수적입니다. 사용자가 select box에 초점을 맞추면 스크린 리더가 레이블 캡션을 알립니다.\n\n`<select>`를 `<label>` 안에 중첩 시킬 수 없다면, 같은 ID를 `<select id>`와 [`<label htmlFor>`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLLabelElement/htmlFor)에 전달하여 연결해야 합니다. 한 컴포넌트에서 여러 인스턴스 간 충돌을 피하려면 [`useId`를 사용하여](/reference/react/useId) ID를 생성하세요.\n\n<Sandpack>\n\n```js\nimport { useId } from 'react';\n\nexport default function Form() {\n  const vegetableSelectId = useId();\n  return (\n    <>\n      <label>\n        Pick a fruit:\n        <select name=\"selectedFruit\">\n          <option value=\"apple\">Apple</option>\n          <option value=\"banana\">Banana</option>\n          <option value=\"orange\">Orange</option>\n        </select>\n      </label>\n      <hr />\n      <label htmlFor={vegetableSelectId}>\n        Pick a vegetable:\n      </label>\n      <select id={vegetableSelectId} name=\"selectedVegetable\">\n        <option value=\"cucumber\">Cucumber</option>\n        <option value=\"corn\">Corn</option>\n        <option value=\"tomato\">Tomato</option>\n      </select>\n    </>\n  );\n}\n```\n\n```css\nselect { margin: 5px; }\n```\n\n</Sandpack>\n\n\n---\n\n### 초기 선택 옵션 제공 {/*providing-an-initially-selected-option*/}\n\n기본적으로 브라우저는 목록에서 첫 번째 `<option>`을 선택합니다. 다른 옵션을 기본값으로 선택하려면 `<select>` 요소 `<option>`의 `value`를 `defaultValue`로 전달해야 합니다.\n\n<Sandpack>\n\n```js\nexport default function FruitPicker() {\n  return (\n    <label>\n      Pick a fruit:\n      <select name=\"selectedFruit\" defaultValue=\"orange\">\n        <option value=\"apple\">Apple</option>\n        <option value=\"banana\">Banana</option>\n        <option value=\"orange\">Orange</option>\n      </select>\n    </label>\n  );\n}\n```\n\n```css\nselect { margin: 5px; }\n```\n\n</Sandpack>\n\n<Pitfall>\n\nHTML과는 달리 개별 `<option>`에 `selected` 어트리뷰트를 전달하는 것은 지원되지 않습니다.\n\n</Pitfall>\n\n---\n\n### 다중 선택 활성화 {/*enabling-multiple-selection*/}\n\n사용자가 여러 옵션을 선택할 수 있도록 `<select>`에 `multiple={true}`를 전달해야 합니다. 초기 선택 옵션을 선택하려면 `defaultValue`를 배열로 지정해야 합니다.\n\n<Sandpack>\n\n```js\nexport default function FruitPicker() {\n  return (\n    <label>\n      Pick some fruits:\n      <select\n        name=\"selectedFruit\"\n        defaultValue={['orange', 'banana']}\n        multiple={true}\n      >\n        <option value=\"apple\">Apple</option>\n        <option value=\"banana\">Banana</option>\n        <option value=\"orange\">Orange</option>\n      </select>\n    </label>\n  );\n}\n```\n\n```css\nselect { display: block; margin-top: 10px; width: 200px; }\n```\n\n</Sandpack>\n\n---\n\n### 폼을 제출할 때 선택 상자에서 제공하는 값 읽기 {/*reading-the-select-box-value-when-submitting-a-form*/}\n\n내부에 [`<button type=\"submit\">`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button)이 있는 select box 주변에 [`<form>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form)을 추가하면 `<form onSubmit>` 이벤트 핸들러를 호출해 값을 전달할 수 있습니다. 아무런 설정이 되어 있지 않다면 브라우저는 양식 데이터를 현재 URL로 보내고 페이지를 새로 고칩니다. `e.preventDefault()`를 호출하여 해당 동작을 재정의할 수 있습니다. [`new FormData(e.target)`](https://developer.mozilla.org/en-US/docs/Web/API/FormData)로 양식 데이터 읽는 방법은 다음과 같습니다.\n<Sandpack>\n\n```js\nexport default function EditPost() {\n  function handleSubmit(e) {\n    // 브라우저가 페이지를 다시 로드하지 않도록 합니다.\n    e.preventDefault();\n    // 폼 데이터를 읽습니다.\n    const form = e.target;\n    const formData = new FormData(form);\n    // formData를 fetch의 본문으로 직접 전달할 수 있습니다.\n    fetch('/some-api', { method: form.method, body: formData });\n    // 브라우저의 기본 동작처럼 URL을 생성할 수 있습니다.\n    console.log(new URLSearchParams(formData).toString());\n    // 일반 오브젝트로 작업할 수 있습니다.\n    const formJson = Object.fromEntries(formData.entries());\n    console.log(formJson); // (!) 여기에는 두 개 이상의 선택 값이 포함되지 않습니다.\n    // 또는 이름-값 쌍의 배열을 얻을 수 있습니다.\n    console.log([...formData.entries()]);\n  }\n\n  return (\n    <form method=\"post\" onSubmit={handleSubmit}>\n      <label>\n        Pick your favorite fruit:\n        <select name=\"selectedFruit\" defaultValue=\"orange\">\n          <option value=\"apple\">Apple</option>\n          <option value=\"banana\">Banana</option>\n          <option value=\"orange\">Orange</option>\n        </select>\n      </label>\n      <label>\n        Pick all your favorite vegetables:\n        <select\n          name=\"selectedVegetables\"\n          multiple={true}\n          defaultValue={['corn', 'tomato']}\n        >\n          <option value=\"cucumber\">Cucumber</option>\n          <option value=\"corn\">Corn</option>\n          <option value=\"tomato\">Tomato</option>\n        </select>\n      </label>\n      <hr />\n      <button type=\"reset\">Reset</button>\n      <button type=\"submit\">Submit</button>\n    </form>\n  );\n}\n```\n\n```css\nlabel, select { display: block; }\nlabel { margin-bottom: 20px; }\n```\n\n</Sandpack>\n\n<Note>\n\n`<select>`에 `name`을 지정해야 합니다. (예: `<select name=\"selectedFruit\" />`) 지정한 `name`은 폼 데이터에서 키로 사용됩니다. (예: `{ selectedFruit: \"orange\" }`)\n\n`<select multiple={true}>`를 사용하면 [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData)에서 각 선택한 값을 별도의 이름-값 쌍으로 포함합니다. 위의 예시에서 콘솔 로그를 확인해 주세요.\n\n</Note>\n\n<Pitfall>\n\n기본적으로 `<form>` 내부의 *모든* `<button>`은 select box의 값을 제출합니다. 의도치 않은 동작으로 인해 당황할 수 있습니다! 사용자 정의 `Button` React 컴포넌트가 있다면 `<button>` 대신 [`<button type=\"button\">`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/button)을 반환하는 것을 고려해야 합니다. 그런 다음 명시적으로 폼을 제출해야 하는 곳에 `<button type=\"submit\">`을 사용해 주세요.\n\n</Pitfall>\n\n---\n\n### State 변수와 함께 select box 제어 {/*controlling-a-select-box-with-a-state-variable*/}\n\n`<select>`와 같은 select box는 *제어되지 않습니다.* `<select defaultValue=\"orange\" />`와 같이 [처음에 선택한 값](#providing-an-initially-selected-option)을 전달하더라도 JSX는 현재 값이 아닌 초기 값만 지정합니다.\n\n**제어된 select box를 렌더링하려면 `value` Prop을 전달해야 합니다.** React는 select box가 항상 전달한 `value`를 갖도록 강제합니다. 보통 [State 변수로 선언](/reference/react/useState)하여 선택 상자를 제어합니다.\n\n```js {2,6,7}\nfunction FruitPicker() {\n  const [selectedFruit, setSelectedFruit] = useState('orange'); // State 변수를 선언합니다.\n  // ...\n  return (\n    <select\n      value={selectedFruit} // ...select의 값이 State 변수와 일치하도록 강제합니다....\n      onChange={e => setSelectedFruit(e.target.value)} // ... 변경 사항이 있을 때마다 State 변수를 업데이트 합니다!\n    >\n      <option value=\"apple\">Apple</option>\n      <option value=\"banana\">Banana</option>\n      <option value=\"orange\">Orange</option>\n    </select>\n  );\n}\n```\n\n모든 선택에 대한 응답으로 UI 일부를 다시 렌더링하려는 경우 유용합니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\n\nexport default function FruitPicker() {\n  const [selectedFruit, setSelectedFruit] = useState('orange');\n  const [selectedVegs, setSelectedVegs] = useState(['corn', 'tomato']);\n  return (\n    <>\n      <label>\n        Pick a fruit:\n        <select\n          value={selectedFruit}\n          onChange={e => setSelectedFruit(e.target.value)}\n        >\n          <option value=\"apple\">Apple</option>\n          <option value=\"banana\">Banana</option>\n          <option value=\"orange\">Orange</option>\n        </select>\n      </label>\n      <hr />\n      <label>\n        Pick all your favorite vegetables:\n        <select\n          multiple={true}\n          value={selectedVegs}\n          onChange={e => {\n            const options = [...e.target.selectedOptions];\n            const values = options.map(option => option.value);\n            setSelectedVegs(values);\n          }}\n        >\n          <option value=\"cucumber\">Cucumber</option>\n          <option value=\"corn\">Corn</option>\n          <option value=\"tomato\">Tomato</option>\n        </select>\n      </label>\n      <hr />\n      <p>Your favorite fruit: {selectedFruit}</p>\n      <p>Your favorite vegetables: {selectedVegs.join(', ')}</p>\n    </>\n  );\n}\n```\n\n```css\nselect { margin-bottom: 10px; display: block; }\n```\n\n</Sandpack>\n\n<Pitfall>\n\n**`onChange` 없이 `value`를 전달하면 옵션을 선택할 수 없습니다.** `value`를 전달하여 select box를 제어하면 전달한 값이 항상 있도록 *강제*합니다. 따라서 `value`를 State 변수로 전달했지만 `onChange` 이벤트 핸들러에서 State 변수를 동기적으로 업데이트하지 않으면 React는 키를 누를 때마다 select box를 지정한 `value`로 되돌립니다.\n\nHTML과는 달리 개별 `<option>`에 `selected` 어트리뷰트를 전달하는 것은 지원하지 않습니다.\n\n</Pitfall>\n"
  },
  {
    "path": "src/content/reference/react-dom/components/style.md",
    "content": "---\nstyle: \"<style>\"\n---\n\n<Intro>\n\n[내장 브라우저 `<style>` 컴포넌트](https://developer.mozilla.org/ko/docs/Web/HTML/Element/style)를 사용하면 문서에 인라인 CSS 스타일시트를 추가할 수 있습니다.\n\n```js\n<style>{` p { color: red; } `}</style>\n```\n\n</Intro>\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `<style>` {/*style*/}\n\n문서에 인라인 스타일을 추가하려면, [내장 브라우저 `<style>` 컴포넌트](https://developer.mozilla.org/ko/docs/Web/HTML/Element/style)를 렌더링하세요. 어떤 컴포넌트에서든 `<style>`을 렌더링할 수 있으며, React는 [특정 경우](#special-rendering-behavior)에 해당 DOM 요소를 문서의 `<head>`에 배치하고 동일한 스타일을 중복 제거합니다.\n\n```js\n<style>{` p { color: red; } `}</style>\n```\n\n[아래 예시를 참고하세요.](#usage)\n\n#### Props {/*props*/}\n\n`<style>`은 모든 [공통 엘리먼트 Props](/reference/react-dom/components/common#common-props)를 지원합니다.\n\n* `children`: 문자열 타입. 필수 항목. 스타일시트의 내용.\n* `precedence`: 문자열 타입. 문서의 `<head>` 내 다른 요소들에 비해 `<style>` DOM 노드의 순위를 지정하여, 어떤 스타일시트가 다른 스타일시트를 덮어쓸 수 있는지를 결정합니다. React는 먼저 발견한 우선순위를 \"낮게\", 나중에 발견한 우선순위를 \"높게\" 추론합니다. 많은 스타일 시스템은 스타일 규칙이 원자적이기 때문에 단일 우선순위 값을 사용해도 잘 작동할 수 있습니다. 동일한 우선순위를 가지는 스타일시트는 `<link>` 태그인지 인라인 `<style>` 태그인지 [`preinit`](/reference/react-dom/preinit) 함수로 로드된 것인지와 무관하게 함께 적용됩니다.\n* `href`: 문자열 타입. 동일한 `href`를 가진 스타일의 [중복 적용을 제거](#special-rendering-behavior)합니다.\n* `media`: 문자열 타입. 스타일시트를 특정 [미디어 쿼리](https://developer.mozilla.org/ko/docs/Web/CSS/CSS_media_queries/Using_media_queries)로 제한합니다.\n* `nonce`: 문자열 타입. 엄격한 콘텐츠 보안 정책을 사용할 때 [리소스를 허용하기 위한 암호화 난수](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/nonce)입니다.\n* `title`: 문자열 타입. [대체 스타일시트](https://developer.mozilla.org/ko/docs/Web/CSS/Alternative_style_sheets)의 이름을 지정합니다.\n\n다음 React 속성들은 **권장하지 않습니다.**\n\n* `blocking`: 문자열 타입. `\"render\"`로 설정하면 스타일시트가 로드될 때까지 브라우저가 페이지를 렌더링하지 않도록 지시합니다. React는 Suspense를 사용하여 더 세밀하게 제어할 수 있습니다.\n\n#### 특별한 렌더링 동작 {/*special-rendering-behavior*/}\n\nReact는 `<style>` 컴포넌트를 문서의 `<head>`로 이동시키고, 동일한 스타일시트의 중복을 제거하며, 스타일시트가 로딩되는 동안 [서스펜스](/reference/react/Suspense)할 수 있습니다.\n\n이 동작을 사용하려면 `href`와 `precedence` 속성을 제공하세요. React는 동일한 `href`를 가진 스타일의 중복을 제거합니다. `precedence` 속성은 문서의 `<head>` 내 다른 요소에 비해 `<style>` DOM 노드의 순위를 지정하며, 어떤 스타일시트가 다른 스타일시트를 덮어쓸 수 있는지를 결정합니다.\n\nThis special treatment comes with three caveats:\n\n* React will ignore changes to props after the style has been rendered. (React will issue a warning in development if this happens.)\n* React will drop all extraneous props when using the `precedence` prop (beyond `href` and `precedence`).\n* React may leave the style in the DOM even after the component that rendered it has been unmounted.\n\n* 스타일이 렌더링된 후에는 React가 Props 변경을 무시합니다. (개발 중에 이 상황이 발생하면 React는 경고를 표시합니다.)\n* React는 `precedence` Prop을 사용할 때 불필요한 Props를 제거합니다. (단, `href`와 `precedence`는 제외.)\n* 스타일을 렌더링한 컴포넌트가 마운트 해제된 후에도 DOM에 스타일이 유지될 수 있습니다.\n---\n\n## 사용법 {/*usage*/}\n\n### 인라인 CSS 스타일시트 렌더링하기 {/*rendering-an-inline-css-stylesheet*/}\n\n컴포넌트가 올바르게 표시되기 위해 특정 CSS 스타일에 의존하는 경우, 컴포넌트 내에서 인라인 스타일시트를 렌더링할 수 있습니다.\n\nReact는 동일한 `href`를 가진 스타일시트의 중복을 제거하므로 `href` 속성은 스타일시트를 고유하게 식별해야 합니다.\n`precedence` Prop을 제공하면 React는 컴포넌트 트리에서 해당 값이 표시되는 순서에 따라 인라인 스타일시트의 순서를 다시 지정합니다.\n\n인라인 스타일시트는 로딩 중이더라도 Suspense 경계를 트리거하지 않습니다.\n이는, 글꼴이나 이미지와 같은 비동기 리소스를 로드하는 경우에도 마찬가지입니다.\n\n<SandpackWithHTMLOutput>\n\n```js src/App.js active\nimport ShowRenderedHTML from './ShowRenderedHTML.js';\nimport { useId } from 'react';\n\nfunction PieChart({data, colors}) {\n  const id = useId();\n  const stylesheet = colors.map((color, index) =>\n    `#${id} .color-${index}: \\{ color: \"${color}\"; \\}`\n  ).join();\n  return (\n    <>\n      <style href={\"PieChart-\" + JSON.stringify(colors)} precedence=\"medium\">\n        {stylesheet}\n      </style>\n      <svg id={id}>\n        …\n      </svg>\n    </>\n  );\n}\n\nexport default function App() {\n  return (\n    <ShowRenderedHTML>\n      <PieChart data=\"...\" colors={['red', 'green', 'blue']} />\n    </ShowRenderedHTML>\n  );\n}\n```\n\n</SandpackWithHTMLOutput>\n"
  },
  {
    "path": "src/content/reference/react-dom/components/textarea.md",
    "content": "---\ntitle: \"<textarea>\"\n---\n\n<Intro>\n\n[내장 브라우저 `<textarea>` 컴포넌트](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea)를 사용하면 여러 줄의 텍스트 입력을 렌더링할 수 있습니다.\n\n```js\n<textarea />\n```\n\n</Intro>\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `<textarea>` {/*textarea*/}\n\n텍스트 영역을 표시하려면 [내장 브라우저 `<textarea>` 컴포넌트](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea)를 렌더링하세요.\n\n```js\n<textarea name=\"postContent\" />\n```\n\n[아래 예시를 참고하세요.](#usage)\n\n#### Props {/*props*/}\n\n`<textarea>`는 모든 [공통 엘리먼트 Props](/reference/react-dom/components/common#common-props)를 지원합니다.\n\n[텍스트 영역을 제어](#controlling-a-select-box-with-a-state-variable)하려면 `value` Prop을 전달하세요.\n\n* `value`: 문자열 타입. 텍스트 영역 내부의 텍스트를 제어합니다.\n\n`value`를 전달할 땐 반드시 해당 값을 업데이트하는 `onChange` 핸들러도 함께 전달해야 합니다.\n\n`<textarea>`가 제어되지 않는 경우 `defaultValue` Prop을 대신 전달해도 됩니다.\n\n* `defaultValue`: 문자열 타입. 텍스트 영역 [초기값](#providing-an-initial-value-for-a-text-area)을 지정합니다.\n\n다음의 `<textarea>` props는 제어되지 않는 텍스트 영역과 제어되는 텍스트 영역 모두에 적용됩니다.\n\n* [`autoComplete`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea#autocomplete): 'on' 또는 'off'. 자동 완성 동작을 지정합니다.\n* [`autoFocus`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea#autofocus): 불리언 타입. `true`일 경우 React는 마운트 시 엘리먼트에 포커스를 맞춥니다.\n* `children`: `<textarea>`는 자식을 받지 않습니다. 초기값을 설정하려면 `defaultValue`를 사용하세요.\n* [`cols`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea#cols): 숫자 타입. 평균 문자 너비의 기본 너비를 지정하세요. 기본값은 `20`입니다.\n* [`disabled`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea#disabled): 불리언 타입. `true`일 경우 입력은 상호작용이 불가능해지며 흐릿하게 보입니다.\n* [`form`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea#form): 문자열 타입. 텍스트 영역 입력이 속하는 `<form>`의 `id`를 지정합니다. 생략 시 가장 가까운 부모 폼으로 설정됩니다.\n* [`maxLength`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea#maxlength): 숫자 타입. 텍스트의 최대 길이를 지정합니다.\n* [`minLength`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea#minlength): 숫자 타입. 텍스트의 최소 길이를 지정합니다.\n* [`name`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#name): 문자열 타입. [폼과 제출](#reading-the-textarea-value-when-submitting-a-form)되는 입력의 이름을 지정합니다.\n* `onChange`: [`Event` 핸들러](/reference/react-dom/components/common#event-handler) 함수. [제어되는 텍스트 영역](#controlling-a-text-area-with-a-state-variable) 필수 요소로 가령 사용자가 키보드를 누를 때마다 실행되는 방식으로 입력 값을 변경하는 즉시 실행되며 브라우저 [`input` 이벤트](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/input_event)처럼 동작합니다.\n* `onChangeCapture`: [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 `onChange`.\n* [`onInput`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/input_event): [`Event` 핸들러](/reference/react-dom/components/common#event-handler) 함수. 사용자가 값을 변경하는 즉시 실행됩니다. 지금까지의 용법에 비춰봤을 때 React에서는 유사하게 동작하는 `onChange`를 사용하는 것이 관습적입니다.\n* `onInputCapture`: [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 `onInput`.\n* [`onInvalid`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/invalid_event): [`Event` 핸들러](/reference/react-dom/components/common#event-handler) 함수. 폼 제출 시 입력이 유효하지 않을 경우 실행되며 `invalid` 내장 이벤트와 달리 React `onInvalid` 이벤트는 버블링됩니다.\n* `onInvalidCapture`: [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 `onInvalid`.\n* [`onSelect`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLTextAreaElement/select_event): [`Event` 핸들러](/reference/react-dom/components/common#event-handler) 함수. `<textarea>` 내부의 선택 사항이 변경된 후 실행됩니다. React는 `onSelect` 이벤트를 확장하여 선택 사항이 비거나 편집 시 선택 사항에 영향을 끼치게 될 때도 실행됩니다.\n* `onSelectCapture`: [캡처 단계](/learn/responding-to-events#capture-phase-events)에서 실행되는 `onSelect`.\n* [`placeholder`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea#attr-placeholder): 문자열 타입. 텍스트 영역 값이 비었을 때 흐린 색으로 표시됩니다.\n* [`readOnly`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea#attr-readonly): 불리언 타입. `true`일 경우 사용자가 텍스트 영역을 편집할 수 없습니다.\n* [`required`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea#attr-required): 불리언 타입. `true`일 경우 폼이 제출할 값을 반드시 제공해야 합니다.\n* [`rows`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea#attr-rows): 숫자 타입. 평균 문자 높이의 기본 높이를 지정하세요. 기본값은 `2`입니다.\n* [`wrap`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea#attr-wrap): `'hard'`, `'soft'`, `'off'` 중 하나. 폼 제출 시 텍스트를 감싸는 방식을 지정합니다.\n\n#### 주의 사항 {/*caveats*/}\n\n- `<textarea>something</textarea>`와 같이 자식을 전달하는 것은 허용되지 않습니다. [초기 콘텐츠로 `defaultValue`를 사용하세요.](#providing-an-initial-value-for-a-text-area)\n- 텍스트 영역은 문자열 `value` Prop을 받을 경우 [제어되는 것으로 취급](#controlling-a-text-area-with-a-state-variable)됩니다.\n- 텍스트 영역은 제어되면서 동시에 비제어될 수 없습니다.\n- 텍스트 영역은 생명주기 동안 제어 또는 비제어 상태를 오갈 수 없습니다.\n- 제어되는 텍스트 영역엔 모두 백업 값을 동기적으로 업데이트하는 `onChange` 이벤트 핸들러가 필요합니다.\n\n---\n\n## 사용법 {/*usage*/}\n\n### 텍스트 영역 표시하기 {/*displaying-a-text-area*/}\n\n텍스트 영역을 표시하려면 `<textarea>`를 렌더링하세요. [`rows`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea#rows)와 [`cols`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea#cols) 어트리뷰트로 텍스트 영역의 기본 크기를 지정할 수도 있지만 기본적으로 사용자가 텍스트 영역의 크기를 조정할 수 있습니다. 크기 조정을 비활성화하려면 CSS에서 `resize: none`을 지정하세요.\n\n<Sandpack>\n\n```js\nexport default function NewPost() {\n  return (\n    <label>\n      Write your post:\n      <textarea name=\"postContent\" rows={4} cols={40} />\n    </label>\n  );\n}\n```\n\n```css\ninput { margin-left: 5px; }\ntextarea { margin-top: 10px; }\nlabel { margin: 10px; }\nlabel, textarea { display: block; }\n```\n\n</Sandpack>\n\n---\n\n### 텍스트 영역 레이블 제공하기 {/*providing-a-label-for-a-text-area*/}\n\n일반적으로 모든 `<textarea>`는 [`<label>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/label) 태그 안에 두게 되는데, 이렇게 하면 해당 레이블이 해당 텍스트 영역과 연관됨을 브라우저가 알 수 있습니다. 사용자가 레이블을 클릭하면 브라우저는 텍스트 영역에 포커스를 맞춥니다. 스크린 리더는 사용자가 텍스트 영역에 포커스를 맞출 때 레이블 캡션을 읽게 되므로 이 방식은 접근성을 위해서도 필수입니다.\n\n`<label>` 안에 `<textarea>`를 감쌀 수 없다면 `<textarea id>`와 [`<label htmlFor>`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLLabelElement/htmlFor)에 동일한 ID를 전달해서 연관성을 부여하세요. 한 컴포넌트의 여러 인스턴스 간 충돌을 피하려면 [`useId`](/reference/react/useId)로 그러한 ID를 생성하세요.\n\n<Sandpack>\n\n```js\nimport { useId } from 'react';\n\nexport default function Form() {\n  const postTextAreaId = useId();\n  return (\n    <>\n      <label htmlFor={postTextAreaId}>\n        Write your post:\n      </label>\n      <textarea\n        id={postTextAreaId}\n        name=\"postContent\"\n        rows={4}\n        cols={40}\n      />\n    </>\n  );\n}\n```\n\n```css\ninput { margin: 5px; }\n```\n\n</Sandpack>\n\n---\n\n### 텍스트 영역 초기값 제공하기 {/*providing-an-initial-value-for-a-text-area*/}\n\n텍스트 영역 초기값은 선택적으로 지정할 수 있습니다. `defaultValue` 문자열로 전달하세요.\n\n<Sandpack>\n\n```js\nexport default function EditPost() {\n  return (\n    <label>\n      Edit your post:\n      <textarea\n        name=\"postContent\"\n        defaultValue=\"I really enjoyed biking yesterday!\"\n        rows={4}\n        cols={40}\n      />\n    </label>\n  );\n}\n```\n\n```css\ninput { margin-left: 5px; }\ntextarea { margin-top: 10px; }\nlabel { margin: 10px; }\nlabel, textarea { display: block; }\n```\n\n</Sandpack>\n\n<Pitfall>\n\nHTML과 달리 `<textarea>Some content</textarea>`와 같은 초기 텍스트 전달은 지원되지 않습니다.\n\n</Pitfall>\n\n---\n\n### 폼 제출 시 텍스트 영역 값 읽기 {/*reading-the-text-area-value-when-submitting-a-form*/}\n\n텍스트 영역과 [`<button type=\"submit\">`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button) 바깥을 [`<form>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form)으로 감싸면 버튼을 클릭했을 때 `<form onSubmit>` 이벤트 핸들러가 호출됩니다. 기본적으로 브라우저는 현재 URL에 폼 데이터를 전송한 후 페이지를 새로고침하며, 이러한 동작은 `e.preventDefault()`를 호출하여 덮어쓸 수 있습니다. 폼 데이터는 [`new FormData(e.target)`](https://developer.mozilla.org/en-US/docs/Web/API/FormData)로 읽으세요.\n<Sandpack>\n\n```js\nexport default function EditPost() {\n  function handleSubmit(e) {\n    // Prevent the browser from reloading the page\n    e.preventDefault();\n\n    // Read the form data\n    const form = e.target;\n    const formData = new FormData(form);\n\n    // You can pass formData as a fetch body directly:\n    fetch('/some-api', { method: form.method, body: formData });\n\n    // Or you can work with it as a plain object:\n    const formJson = Object.fromEntries(formData.entries());\n    console.log(formJson);\n  }\n\n  return (\n    <form method=\"post\" onSubmit={handleSubmit}>\n      <label>\n        Post title: <input name=\"postTitle\" defaultValue=\"Biking\" />\n      </label>\n      <label>\n        Edit your post:\n        <textarea\n          name=\"postContent\"\n          defaultValue=\"I really enjoyed biking yesterday!\"\n          rows={4}\n          cols={40}\n        />\n      </label>\n      <hr />\n      <button type=\"reset\">Reset edits</button>\n      <button type=\"submit\">Save post</button>\n    </form>\n  );\n}\n```\n\n```css\nlabel { display: block; }\ninput { margin: 5px; }\n```\n\n</Sandpack>\n\n<Note>\n\n`<textarea name=\"postContent\" />` 예시와 같이 `<textarea>`에 `name`을 부여하세요. 해당 `name`은 `{ postContent: \"Your post\" }` 예시처럼 데이터의 key로 쓰입니다.\n\n</Note>\n\n<Pitfall>\n\n기본적으로 `<form>` 내부의 *어느* `<button>`이든 폼을 제출합니다. 뜻밖인가요? 커스텀 `Button` React 컴포넌트의 경우 `<button>` 대신 [`<button type=\"button\">`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/button) 반환을 고려하세요. 명시성을 부여하기 위해 폼 제출용 버튼으로는 `<button type=\"submit\">`을 사용하세요.\n\n</Pitfall>\n\n---\n\n### State 변수로 텍스트 영역 제어하기 {/*controlling-a-text-area-with-a-state-variable*/}\n\n`<textarea />`와 같은 텍스트 영역은 *제어되지 않습니다*. `<textarea defaultValue=\"Initial text\" />`와 같은 [초기값을 전달](#providing-an-initial-value-for-a-text-area)한대도 JSX는 당장의 값이 아닌 초기값만을 지정합니다.\n\n**_제어되는_ 텍스트 영역을 렌더링하려면 `value` Prop을 전달하세요.** React는 전달한 `value`를 항상 갖도록 텍스트 영역에 강제합니다. 일반적으로 텍스트 영역은 [State 변수](/reference/react/useState)를 선언하여 제어합니다.\n\n```js {2,6,7}\nfunction NewPost() {\n  const [postContent, setPostContent] = useState(''); // state 변수를 선언합니다.\n  // ...\n  return (\n    <textarea\n      value={postContent} // 입력 값이 state 변수와 일치하도록 강제합니다.\n      onChange={e => setPostContent(e.target.value)} // 텍스트 영역을 편집할 때마다 state 변수를 업데이트합니다.\n    />\n  );\n}\n```\n\n이 방식은 키보드를 누를 때마다 그에 대한 반응으로 UI 일부를 리렌더링하고자 할 때 유용합니다.\n\n<Sandpack>\n\n```js\nimport { useState } from 'react';\nimport MarkdownPreview from './MarkdownPreview.js';\n\nexport default function MarkdownEditor() {\n  const [postContent, setPostContent] = useState('_Hello,_ **Markdown**!');\n  return (\n    <>\n      <label>\n        Enter some markdown:\n        <textarea\n          value={postContent}\n          onChange={e => setPostContent(e.target.value)}\n        />\n      </label>\n      <hr />\n      <MarkdownPreview markdown={postContent} />\n    </>\n  );\n}\n```\n\n```js src/MarkdownPreview.js\nimport { Remarkable } from 'remarkable';\n\nconst md = new Remarkable();\n\nexport default function MarkdownPreview({ markdown }) {\n  const renderedHTML = md.render(markdown);\n  return <div dangerouslySetInnerHTML={{__html: renderedHTML}} />;\n}\n```\n\n```json package.json\n{\n  \"dependencies\": {\n    \"react\": \"latest\",\n    \"react-dom\": \"latest\",\n    \"react-scripts\": \"latest\",\n    \"remarkable\": \"2.0.1\"\n  },\n  \"scripts\": {\n    \"start\": \"react-scripts start\",\n    \"build\": \"react-scripts build\",\n    \"test\": \"react-scripts test --env=jsdom\",\n    \"eject\": \"react-scripts eject\"\n  }\n}\n```\n\n```css\ntextarea { display: block; margin-top: 5px; margin-bottom: 10px; }\n```\n\n</Sandpack>\n\n<Pitfall>\n\n**텍스트 영역에 `onChange` 없이 `value`를 전달하면 해당 텍스트 영역에 타이핑을 할 수 없게 됩니다.** `value`를 전달하여 텍스트 영역을 제어하면 항상 해당 `value`를 가지도록 *강제합니다*. 그러므로 State 변수를 `value`로 전달해도 `onChange` 이벤트 핸들러 내에서 해당 State 변수를 동기적으로 업데이트하지 않으면 React는 키보드를 누를 때마다 텍스트 영역을 처음 지정한 `value`로 되돌리게 됩니다.\n\n</Pitfall>\n\n---\n\n## 문제 해결 {/*troubleshooting*/}\n\n### 텍스트 영역에 타이핑을 해도 업데이트되지 않습니다 {/*my-text-area-doesnt-update-when-i-type-into-it*/}\n\n`onChange` 없이 `value`만 전달하여 텍스트 영역을 렌더링하면 콘솔에 오류가 나타납니다.\n\n```js\n// 🔴 버그: 제어되는 텍스트 영역에 `onChange` 핸들러가 없습니다.\n<textarea value={something} />\n```\n\n<ConsoleBlock level=\"error\">\n\n폼 필드에 `onChange` 핸들러 없이 `value` Prop만 전달했습니다. 이렇게 하면 읽기 전용 필드를 렌더링하게 됩니다. 필드가 변경 가능해야 하는 경우 `defaultValue`를 사용하고 그렇지 않은 경우 `onChange` 또는 `readOnly`를 설정하세요.\n\n</ConsoleBlock>\n\n에러 메시지가 제안하듯 [*초기값*만 지정](#providing-an-initial-value-for-a-text-area)하려면 `defaultVallue`를 대신 전달하세요.\n\n```js\n// ✅ 좋은 예: 제어되지 않는 텍스트 영역에 초기값 전달\n<textarea defaultValue={something} />\n```\n\n[텍스트 영역을 State 변수로 제어](#controlling-a-text-area-with-a-state-variable)하려면 `onChange` 핸들러를 지정하세요.\n\n```js\n// ✅ 좋은 예: 제어되는 텍스트 영역에 onChange 전달\n<textarea value={something} onChange={e => setSomething(e.target.value)} />\n```\n\n값이 내부적으로 읽기 전용이라면 오류를 막기 위해 `readOnly` Prop을 추가하세요.\n\n```js\n// ✅ 좋은 예: 제어되는 읽기 전용 텍스트 영역에 onChange 생략\n<textarea value={something} readOnly={true} />\n```\n\n---\n\n### 키보드를 누를 때마다 텍스트 커서가 텍스트 영역의 처음으로 돌아갑니다 {/*my-text-area-caret-jumps-to-the-beginning-on-every-keystroke*/}\n\n[텍스트 영역을 제어](#controlling-a-text-area-with-a-state-variable)할 경우 `onChange` 안에서 State 변수를 DOM에서 받아온 텍스트 영역 값으로 업데이트해야 합니다.\n\nState 변수는 `e.target.value` 외의 값으로 업데이트할 수 없습니다.\n\n```js\nfunction handleChange(e) {\n  // 🔴 버그: 입력을 e.target.value 외의 값으로 업데이트합니다.\n  setFirstName(e.target.value.toUpperCase());\n}\n```\n\n비동기로 업데이트할 수도 없습니다.\n\n```js\nfunction handleChange(e) {\n  // 🔴 버그: 입력을 비동기로 업데이트합니다.\n  setTimeout(() => {\n    setFirstName(e.target.value);\n  }, 100);\n}\n```\n\n코드를 고치려면 `e.target.value`로 동기 업데이트하세요.\n\n```js\nfunction handleChange(e) {\n  // ✅ 제어되는 입력을 `e.target.value`로 동기 업데이트합니다.\n  setFirstName(e.target.value);\n}\n```\n\n이 방법으로 문제가 해결되지 않을 경우 키보드를 누를 때마다 텍스트 영역이 제거 후 다시 추가되고 있을 가능성이 있습니다. 실수로 리렌더링마다 [State를 재설정](/learn/preserving-and-resetting-state)하고 있다면 나타날 수 있는 현상입니다. 가령 텍스트 영역이나 텍스트 영역의 부모 중 하나가 매번 다른 `key` 어트리뷰트를 받거나 컴포넌트 정의를 중첩시키는 경우(이는 React에서 허용되지 않으며 렌더링마다 '내부' 컴포넌트가 리마운트되는 원인이 됩니다)에 해당 문제가 발생할 수 있습니다.\n\n---\n\n### 다음과 같은 에러가 납니다. \"A component is changing an uncontrolled input to be controlled(컴포넌트가 제어되지 않는 입력을 제어 상태로 변경합니다)\" {/*im-getting-an-error-a-component-is-changing-an-uncontrolled-input-to-be-controlled*/}\n\n\n컴포넌트에 `value`를 제공할 경우 반드시 생명주기 내내 문자열 타입으로 남아야 합니다.\n\nReact는 컴포넌트를 비제어할 것인지 제어할 것인지 의도를 알 수 없기 때문에 처음엔 `value={undefined}`를 전달했다가 나중에 다시 `value=\"some string\"`을 전달할 수는 없습니다. 제어되는 컴포넌트는 항상 `null`이나 `undefined`가 아닌 문자열 `value`를 받아야 합니다.\n\n`value`가 API나 State 변수에서 온다면 `null`이나 `undefined`로 초기화될 수 있습니다. 그럴 경우 빈 문자열(`''`)을 초기값으로 설정하거나 `value`가 문자열임을 보장하기 위해 `value={someValue ?? ''}`를 전달하세요.\n"
  },
  {
    "path": "src/content/reference/react-dom/components/title.md",
    "content": "---\ntitle: \"<title>\"\n---\n\n<Intro>\n\n[내장 브라우저 `<title>` 컴포넌트](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/title)를 사용하면 문서의 제목을 지정할 수 있습니다.\n\n```js\n<title>My Blog</title>\n```\n\n</Intro>\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `<title>` {/*title*/}\n\n문서의 제목을 지정하려면 [내장 브라우저 `<title>` 컴포넌트](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/title)를 렌더링하세요. `<title>`은 어떤 컴포넌트에서든 렌더링할 수 있으며, React는 항상 해당하는 DOM 요소를 문서의 `<head>`에 배치합니다.\n\n```js\n<title>My Blog</title>\n```\n\n[아래 예시를 참고하세요.](#usage)\n\n#### Props {/*props*/}\n\n`<title>`은 모든 [공통 엘리먼트 Props](/reference/react-dom/components/common#common-props)를 지원합니다.\n\n* `children`: `<title>`은 자식으로 텍스트만 허용합니다. 이 텍스트는 문서의 제목이 됩니다. 텍스트만 렌더링하는 한, 사용자 정의 컴포넌트도 전달할 수 있습니다.\n\n#### 특별한 렌더링 동작 {/*special-rendering-behavior*/}\n\nReact는 `<title>` 컴포넌트에 해당하는 DOM 요소를 React 트리 내 어디에서 렌더링하든 항상 문서의 `<head>` 내에 배치합니다. `<head>`는 DOM 내에서 `<title>`이 존재할 수 있는 유일한 위치이지만, 특정 페이지를 나타내는 컴포넌트가 자체적으로 `<title>`을 렌더링할 수 있으면 편리하고 구성 가능하게 유지됩니다.\n\n여기에는 두 가지 예외가 있습니다.\n* `<title>`이 `<svg>` 컴포넌트 내에 있는 경우, 특별한 동작이 없습니다. 이 맥락에서는 문서의 제목을 나타내는 것이 아니라 [해당 SVG 그래픽에 대한 접근성 주석](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/title)이기 때문입니다.\n* `<title>`에 [`itemProp`](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/itemprop) 속성이 있는 경우, 특별한 동작이 없습니다. 이 경우 문서의 제목이 아니라 페이지의 특정 부분에 대한 메타데이터를 나타내기 때문입니다.\n\n<Pitfall>\n\n한 번에 하나의 `<title>`만 렌더링하세요. 여러 구성 요소가 동시에 `<title>` 태그를 렌더링하면 React는 모든 제목을 문서의 `<head>`에 배치합니다. 이렇게 되면 브라우저와 검색 엔진의 동작이 정의되지 않습니다.\n\n</Pitfall>\n\n---\n\n## 사용법 {/*usage*/}\n\n### 문서 제목 설정하기 {/*set-the-document-title*/}\n\n텍스트를 자식으로 갖는 `<title>` 컴포넌트를 어떤 컴포넌트에서도 렌더링할 수 있습니다. React는 문서의 `<head>`에 `<title>` DOM 노드를 배치합니다.\n\n<SandpackWithHTMLOutput>\n\n```js src/App.js active\nimport ShowRenderedHTML from './ShowRenderedHTML.js';\n\nexport default function ContactUsPage() {\n  return (\n    <ShowRenderedHTML>\n      <title>My Site: Contact Us</title>\n      <h1>Contact Us</h1>\n      <p>Email us at support@example.com</p>\n    </ShowRenderedHTML>\n  );\n}\n```\n\n</SandpackWithHTMLOutput>\n\n### 제목에 변수 사용하기 {/*use-variables-in-the-title*/}\n\n`<title>` 컴포넌트의 자식은 단일 텍스트 문자열이어야 합니다. (또는 단일 숫자나 `toString` 메서드를 가진 단일 객체). JSX 중괄호를 다음과 같이 사용하면 명확하지 않을 수 있습니다.\n\n```js\n<title>Results page {pageNumber}</title> // 🔴 Problem: This is not a single string\n```\n\n실제로는 `<title>` 컴포넌트에 두 개의 요소로 구성된 배열이 자식으로 전달됩니다. (문자열 `\"Results page\"`와 `pageNumber`의 값.) 이는 오류를 발생시킵니다. 대신에 문자열 보간을 사용하여 `<title>`에 단일 문자열을 전달하세요.\n\n```js\n<title>{`Results page ${pageNumber}`}</title>\n```\n\n"
  },
  {
    "path": "src/content/reference/react-dom/createPortal.md",
    "content": "---\ntitle: createPortal\n---\n\n<Intro>\n\n`createPortal`을 사용하면 일부 자식을 DOM의 다른 부분으로 렌더링할 수 있습니다.\n\n```js\n<div>\n  <SomeComponent />\n  {createPortal(children, domNode, key?)}\n</div>\n```\n\n</Intro>\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `createPortal(children, domNode, key?)` {/*createportal*/}\n\nPortal을 생성하려면 `createPortal`을 호출하여 일부 JSX와 렌더링할 DOM 노드를 전달합니다.\n\n```js\nimport { createPortal } from 'react-dom';\n\n// ...\n\n<div>\n  <p>This child is placed in the parent div.</p>\n  {createPortal(\n    <p>This child is placed in the document body.</p>,\n    document.body\n  )}\n</div>\n```\n\n[아래 예시를 참고하세요.](#usage)\n\nPortal은 DOM 노드의 물리적 배치만 변경합니다. 이외의 모든 측면에서, Portal로 렌더링된 JSX는 이를 렌더링하는 React 컴포넌트의 자식 노드처럼 동작합니다. 예를 들어, 자식은 부모 트리가 제공하는 Context에 접근할 수 있으며, 이벤트는 React 트리를 따라 자식에서 부모로 전파됩니다.\n\n#### 매개변수 {/*parameters*/}\n\n* `children` : JSX의 일부(`<div />` 또는 `<SomeComponent />`), [`<Fragment>`](/reference/react/Fragment)(`<>...</>`), 문자열이나 숫자 또는 이들의 배열과 같이 React로 렌더링할 수 있는 모든 것입니다.\n\n* `domNode` : `document.getElementById()`가 반환하는 것과 같은 일부 DOM 노드. 노드가 이미 존재해야 합니다. 업데이트 중에 다른 DOM 노드를 전달하면 Portal 콘텐츠가 다시 생성됩니다.\n\n* **optional** `key`: A unique string or number to be used as the portal's [key.](/learn/rendering-lists#keeping-list-items-in-order-with-key)\n\n#### 반환값 {/*returns*/}\n\n`createPortal`은 JSX를 포함하거나 React 컴포넌트를 반환할 수 있는 React 노드를 반환합니다. React가 렌더링 출력에서 이를 발견하면, 제공된 `children`을 제공된 `domNode` 안에 배치합니다.\n\n#### 주의 사항 {/*caveats*/}\n\n* Portal의 이벤트는 DOM 트리가 아닌 React 트리를 따라 전파됩니다. 예를 들어, Portal 내부를 클릭했을 때 포털이 `<div onClick>`으로 감싸져 있으면 해당 `onClick` 핸들러가 실행됩니다. 이로 인해 문제가 발생하면 Portal 내부에서 이벤트 전파를 중지하거나 Portal 자체를 React 트리에서 위로 이동하세요.\n\n---\n\n## 사용법 {/*usage*/}\n\n### DOM의 다른 부분으로 렌더링하기 {/*rendering-to-a-different-part-of-the-dom*/}\n\n*Portal*을 사용하면 컴포넌트가 일부 자식을 DOM의 다른 위치로 렌더링할 수 있습니다. 이를 통해 컴포넌트의 일부가 어떤 컨테이너에 있든 그 컨테이너에서 \"탈출\"할 수 있습니다. 예를 들어, 컴포넌트는 페이지의 나머지 부분 위쪽과 바깥에 표시되는 모달 대화상자나 툴팁을 보여줄 수 있습니다.\n\nPortal을 생성하려면 <CodeStep step={1}>일부 JSX</CodeStep>와 함께 `createPortal`의 결과를 렌더링하고 <CodeStep step={2}>DOM 노드가 있어야 할 위치</CodeStep>를 지정합니다.\n\n```js [[1, 8, \"<p>This child is placed in the document body.</p>\"], [2, 9, \"document.body\"]]\nimport { createPortal } from 'react-dom';\n\nfunction MyComponent() {\n  return (\n    <div style={{ border: '2px solid black' }}>\n      <p>This child is placed in the parent div.</p>\n      {createPortal(\n        <p>This child is placed in the document body.</p>,\n        document.body\n      )}\n    </div>\n  );\n}\n```\n\nReact는 사용자가 <CodeStep step={1}>전달한 JSX</CodeStep>에 대한 DOM 노드를 사용자가 <CodeStep step={2}>제공한 DOM 노드</CodeStep> 안에 넣습니다.\n\nPortal이 없다면 두 번째 `<p>`는 상위 `<div>` 안에 배치되지만, Portal은 이를 [`document.body`](https://developer.mozilla.org/ko/docs/Web/API/Document/body) 안으로 \"순간이동\"시킵니다.\n\n<Sandpack>\n\n```js\nimport { createPortal } from 'react-dom';\n\nexport default function MyComponent() {\n  return (\n    <div style={{ border: '2px solid black' }}>\n      <p>This child is placed in the parent div.</p>\n      {createPortal(\n        <p>This child is placed in the document body.</p>,\n        document.body\n      )}\n    </div>\n  );\n}\n```\n\n</Sandpack>\n\n두 번째 단락이 테두리가 있는 부모 `<div>` 외부에 시각적으로 어떻게 나타나는지 주목하세요. 개발자 도구로 DOM 구조를 검사하면 두 번째 `<p>`가 `<body>` 안에 직접 배치된 것을 확인할 수 있습니다.\n\n```html {4-6,9}\n<body>\n  <div id=\"root\">\n    ...\n      <div style=\"border: 2px solid black\">\n        <p>This child is placed inside the parent div.</p>\n      </div>\n    ...\n  </div>\n  <p>This child is placed in the document body.</p>\n</body>\n```\n\nPortal은 DOM 노드의 물리적 배치만 변경합니다. 이외의 모든 측면에서, Portal로 렌더링된 JSX는 이를 렌더링하는 React 컴포넌트의 자식 노드처럼 동작합니다. 예를 들어, 자식은 부모 트리가 제공하는 Context에 접근할 수 있으며, 이벤트는 React 트리를 따라 자식에서 부모로 전파됩니다.\n\n---\n\n### Portal이 있는 모달 대화 상자 렌더링하기 {/*rendering-a-modal-dialog-with-a-portal*/}\n\n대화 상자를 불러오는 컴포넌트가 `overflow: hidden` 또는 대화 상자에 영향을 주는 다른 스타일이 있는 컨테이너 안에 있더라도 Portal을 사용하여 페이지의 나머지 부분 위에 떠 있는 모달 대화 상자를 만들 수 있습니다.\n\n이 예시에서 두 컨테이너에는 모달 대화 상자에 영향을 주는 스타일이 있지만, Portal에 렌더링된 스타일은 영향을 받지 않는데, 그 이유는 DOM에서 모달이 상위 JSX 요소에 포함되지 않기 때문입니다.\n\n<Sandpack>\n\n```js src/App.js active\nimport NoPortalExample from './NoPortalExample';\nimport PortalExample from './PortalExample';\n\nexport default function App() {\n  return (\n    <>\n      <div className=\"clipping-container\">\n        <NoPortalExample  />\n      </div>\n      <div className=\"clipping-container\">\n        <PortalExample />\n      </div>\n    </>\n  );\n}\n```\n\n```js src/NoPortalExample.js\nimport { useState } from 'react';\nimport ModalContent from './ModalContent.js';\n\nexport default function NoPortalExample() {\n  const [showModal, setShowModal] = useState(false);\n  return (\n    <>\n      <button onClick={() => setShowModal(true)}>\n        Show modal without a portal\n      </button>\n      {showModal && (\n        <ModalContent onClose={() => setShowModal(false)} />\n      )}\n    </>\n  );\n}\n```\n\n```js src/PortalExample.js active\nimport { useState } from 'react';\nimport { createPortal } from 'react-dom';\nimport ModalContent from './ModalContent.js';\n\nexport default function PortalExample() {\n  const [showModal, setShowModal] = useState(false);\n  return (\n    <>\n      <button onClick={() => setShowModal(true)}>\n        Show modal using a portal\n      </button>\n      {showModal && createPortal(\n        <ModalContent onClose={() => setShowModal(false)} />,\n        document.body\n      )}\n    </>\n  );\n}\n```\n\n```js src/ModalContent.js\nexport default function ModalContent({ onClose }) {\n  return (\n    <div className=\"modal\">\n      <div>I'm a modal dialog</div>\n      <button onClick={onClose}>Close</button>\n    </div>\n  );\n}\n```\n\n\n```css src/styles.css\n.clipping-container {\n  position: relative;\n  border: 1px solid #aaa;\n  margin-bottom: 12px;\n  padding: 12px;\n  width: 250px;\n  height: 80px;\n  overflow: hidden;\n}\n\n.modal {\n  display: flex;\n  justify-content: space-evenly;\n  align-items: center;\n  box-shadow: rgba(100, 100, 111, 0.3) 0px 7px 29px 0px;\n  background-color: white;\n  border: 2px solid rgb(240, 240, 240);\n  border-radius: 12px;\n  position:  absolute;\n  width: 250px;\n  top: 70px;\n  left: calc(50% - 125px);\n  bottom: 70px;\n}\n```\n\n</Sandpack>\n\n<Pitfall>\n\nPortal을 사용할 때 앱의 접근성<sup>accessibility, a11y</sup>이 준수되는지 확인하는 것이 중요합니다. 예를 들어 사용자가 Portal 안팎으로 자연스럽게 초점을 이동할 수 있도록 키보드 포커스를 관리해야 할 수 있습니다.\n\n모달을 만들 때는 [WAI-ARIA 모달 제작 관행](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal)을 따르세요. 커뮤니티 패키지를 사용하는 경우 해당 패키지가 접근 가능한지, 이 가이드라인을 따르고 있는지 확인하세요.\n\n</Pitfall>\n\n---\n\n### React 컴포넌트를 React가 아닌 서버 마크업으로 렌더링하기 {/*rendering-react-components-into-non-react-server-markup*/}\n\nPortal은 React 루트가 React로 빌드되지 않은 정적 또는 서버 렌더링 페이지의 일부일 때 유용할 수 있습니다. 예를 들어, 페이지가 Rails와 같은 서버 프레임워크로 빌드된 경우 사이드바와 같은 정적 영역 내에 인터랙티브 영역을 만들 수 있습니다. [여러 개의 개별 React 루트](/reference/react-dom/client/createRoot#rendering-a-page-partially-built-with-react)를 사용하는 것과 비교하여, Portal을 사용하면 앱의 일부가 DOM의 다른 부분에 렌더링 되더라도 공유 상태를 가진 단일 React 트리로 취급할 수 있습니다.\n\n<Sandpack>\n\n```html public/index.html\n<!DOCTYPE html>\n<html>\n  <head><title>My app</title></head>\n  <body>\n    <h1>Welcome to my hybrid app</h1>\n    <div class=\"parent\">\n      <div class=\"sidebar\">\n        This is server non-React markup\n        <div id=\"sidebar-content\"></div>\n      </div>\n      <div id=\"root\"></div>\n    </div>\n  </body>\n</html>\n```\n\n```js src/index.js\nimport { StrictMode } from 'react';\nimport { createRoot } from 'react-dom/client';\nimport App from './App.js';\nimport './styles.css';\n\nconst root = createRoot(document.getElementById('root'));\nroot.render(\n  <StrictMode>\n    <App />\n  </StrictMode>\n);\n```\n\n```js src/App.js active\nimport { createPortal } from 'react-dom';\n\nconst sidebarContentEl = document.getElementById('sidebar-content');\n\nexport default function App() {\n  return (\n    <>\n      <MainContent />\n      {createPortal(\n        <SidebarContent />,\n        sidebarContentEl\n      )}\n    </>\n  );\n}\n\nfunction MainContent() {\n  return <p>This part is rendered by React</p>;\n}\n\nfunction SidebarContent() {\n  return <p>This part is also rendered by React!</p>;\n}\n```\n\n```css\n.parent {\n  display: flex;\n  flex-direction: row;\n}\n\n#root {\n  margin-top: 12px;\n}\n\n.sidebar {\n  padding:  12px;\n  background-color: #eee;\n  width: 200px;\n  height: 200px;\n  margin-right: 12px;\n}\n\n#sidebar-content {\n  margin-top: 18px;\n  display: block;\n  background-color: white;\n}\n\np {\n  margin: 0;\n}\n```\n\n</Sandpack>\n\n---\n\n### React 컴포넌트를 React가 아닌 DOM 노드로 렌더링하기 {/*rendering-react-components-into-non-react-dom-nodes*/}\n\nPortal을 사용해 React 외부에서 관리되는 DOM 노드의 콘텐츠를 관리할 수도 있습니다. 예를 들어, React가 아닌 맵 위젯과 통합하고 팝업 안에 React 콘텐츠를 렌더링하고 싶다고 가정해 봅시다. 이렇게 하려면 렌더링할 DOM 노드를 저장할 `popupContainer` 상태 변수를 선언하세요.\n\n```js\nconst [popupContainer, setPopupContainer] = useState(null);\n```\n\n서드파티 위젯을 만들 때 위젯이 반환하는 DOM 노드를 저장하여 렌더링할 수 있도록 합니다.\n\n```js {5-6}\nuseEffect(() => {\n  if (mapRef.current === null) {\n    const map = createMapWidget(containerRef.current);\n    mapRef.current = map;\n    const popupDiv = addPopupToMapWidget(map);\n    setPopupContainer(popupDiv);\n  }\n}, []);\n```\n\n이렇게 하면 `createPortal`을 사용하여 React 콘텐츠가 사용 가능해지면 `popupContainer`로 렌더링할 수 있습니다.\n\n```js {3-6}\nreturn (\n  <div style={{ width: 250, height: 250 }} ref={containerRef}>\n    {popupContainer !== null && createPortal(\n      <p>Hello from React!</p>,\n      popupContainer\n    )}\n  </div>\n);\n```\n\n다음은 실행할 수 있는 전체 예시입니다.\n\n<Sandpack>\n\n```json package.json hidden\n{\n  \"dependencies\": {\n    \"leaflet\": \"1.9.1\",\n    \"react\": \"latest\",\n    \"react-dom\": \"latest\",\n    \"react-scripts\": \"latest\",\n    \"remarkable\": \"2.0.1\"\n  },\n  \"scripts\": {\n    \"start\": \"react-scripts start\",\n    \"build\": \"react-scripts build\",\n    \"test\": \"react-scripts test --env=jsdom\",\n    \"eject\": \"react-scripts eject\"\n  }\n}\n```\n\n```js src/App.js\nimport { useRef, useEffect, useState } from 'react';\nimport { createPortal } from 'react-dom';\nimport { createMapWidget, addPopupToMapWidget } from './map-widget.js';\n\nexport default function Map() {\n  const containerRef = useRef(null);\n  const mapRef = useRef(null);\n  const [popupContainer, setPopupContainer] = useState(null);\n\n  useEffect(() => {\n    if (mapRef.current === null) {\n      const map = createMapWidget(containerRef.current);\n      mapRef.current = map;\n      const popupDiv = addPopupToMapWidget(map);\n      setPopupContainer(popupDiv);\n    }\n  }, []);\n\n  return (\n    <div style={{ width: 250, height: 250 }} ref={containerRef}>\n      {popupContainer !== null && createPortal(\n        <p>Hello from React!</p>,\n        popupContainer\n      )}\n    </div>\n  );\n}\n```\n\n```js src/map-widget.js\nimport 'leaflet/dist/leaflet.css';\nimport * as L from 'leaflet';\n\nexport function createMapWidget(containerDomNode) {\n  const map = L.map(containerDomNode);\n  map.setView([0, 0], 0);\n  L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {\n    maxZoom: 19,\n    attribution: '© OpenStreetMap'\n  }).addTo(map);\n  return map;\n}\n\nexport function addPopupToMapWidget(map) {\n  const popupDiv = document.createElement('div');\n  L.popup()\n    .setLatLng([0, 0])\n    .setContent(popupDiv)\n    .openOn(map);\n  return popupDiv;\n}\n```\n\n```css\nbutton { margin: 5px; }\n```\n\n</Sandpack>\n"
  },
  {
    "path": "src/content/reference/react-dom/flushSync.md",
    "content": "---\ntitle: flushSync\n---\n\n<Pitfall>\n\n`flushSync`를 사용하는 것은 일반적이지 않으며 애플리케이션의 성능을 저하할 수 있습니다.\n\n</Pitfall>\n\n<Intro>\n\n`flushSync`는 React에 제공된 콜백 내부의 모든 업데이트를 동기적으로 처리하도록 강제합니다. DOM이 즉시 업데이트되는 것을 보장합니다.\n\n```js\nflushSync(callback)\n```\n\n</Intro>\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `flushSync(callback)` {/*flushsync*/}\n\n`flushSync`를 호출해서 React가 보류<sup>Pending</sup> 중인 모든 작업을 강제로 처리하고 DOM을 동기적으로 업데이트할 수 있습니다.\n\n```js\nimport { flushSync } from 'react-dom';\n\nflushSync(() => {\n  setSomething(123);\n});\n```\n\n대부분의 경우 `flushSync`의 사용을 권장하지 않습니다. `flushSync`는 최후의 수단으로 사용하세요.\n\n[아래 예시를 참고하세요.](#usage)\n\n#### 매개변수 {/*parameters*/}\n\n* `callback`: 함수입니다. React는 즉시 콜백을 호출하고 콜백 내의 모든 업데이트를 동기적으로 처리합니다. 또한 보류 중인 업데이트나 Effect 또는 Effect 내부의 업데이트도 처리할 수 있습니다. `flushSync` 호출로 인해 업데이트가 중단되면 Fallback이 다시 표시될 수 있습니다.\n\n#### 반환값 {/*returns*/}\n\n`flushSync`는 `undefined`를 반환합니다.\n\n#### 주의 사항 {/*caveats*/}\n\n* `flushSync`를 사용하면 애플리케이션의 성능이 크게 저하될 수 있습니다. 가급적 사용하지 마세요.\n* `flushSync`는 보류 중인 Suspense 바운더리의 `fallback` State를 표시하도록 강제할 수 있습니다.\n* `flushSync`는 보류 중인 Effect를 실행하고 포함된 모든 업데이트를 반환하기 전에 동기적으로 적용할 수 있습니다.\n* `flushSync`는 콜백 내부의 업데이트를 처리할 때 필요한 경우 콜백 외부의 업데이트를 처리할 수 있습니다. 예를 들어 클릭으로 인한 보류 중인 업데이트가 있는 경우 React는 콜백 내부의 업데이트를 처리하기 전에 해당 업데이트를 처리할 수 있습니다.\n\n---\n\n## 사용법 {/*usage*/}\n\n### 서드 파티 통합을 위한 업데이트 Flushing {/*flushing-updates-for-third-party-integrations*/}\n\n브라우저 API 또는 UI 라이브러리와 같은 서드 파티 코드를 통합할 때 React가 업데이트를 처리하도록 강제할 필요가 있을 수 있습니다. `flushSync`를 사용해서 React가 콜백 내부의 모든 <CodeStep step={1}>State 업데이트</CodeStep>를 동기적으로 처리하도록 할 수 있습니다.\n\n```js [[1, 2, \"setSomething(123)\"]]\nflushSync(() => {\n  setSomething(123);\n});\n// By this line, the DOM is updated.\n```\n\n이렇게 함으로써 React가 DOM을 이미 업데이트한 후에 다음 줄의 코드를 실행하는 것을 보장합니다.\n\n**`flushSync`를 사용하는 것은 일반적이지 않고 자주 사용하면 애플리케이션의 성능이 크게 저하될 수 있습니다.** 애플리케이션이 React API만 사용하고 서드 파티 라이브러리와 통합하지 않는다면 `flushSync`는 필요하지 않습니다.\n\n그러나 브라우저 API와 같은 서드 파티 코드와 통합할 때 유용할 수 있습니다.\n\n일부 브라우저 API는 콜백 내부의 결과가 DOM에서 동기적으로 사용될 것으로 예상하므로 콜백이 완료될 때까지 렌더링된 DOM을 사용해서 브라우저가 작업할 수 있습니다. 대부분의 경우 React가 이를 자동으로 처리하지만, 때에 따라 강제로 동기적 업데이트를 해야 할 수 있습니다.\n\n예를 들어 `onbeforeprint` 브라우저 API를 사용하면 프린트 대화 상자가 열리기 직전에 페이지를 변경할 수 있습니다. 문서를 더 잘 표시하기 위해 사용자가 정의한 프린트 스타일을 적용하는 데 유용합니다. 아래 예시에서는 `onbeforeprint` 콜백 내에서 `flushSync`를 사용하여 React State를 DOM으로 즉시 \"Flush\"합니다. 그런 다음 프린트 다이얼로그가 열릴 때까지 `isPrinting`이 \"yes\"로 표시됩니다.\n\n<Sandpack>\n\n```js src/App.js active\nimport { useState, useEffect } from 'react';\nimport { flushSync } from 'react-dom';\n\nexport default function PrintApp() {\n  const [isPrinting, setIsPrinting] = useState(false);\n\n  useEffect(() => {\n    function handleBeforePrint() {\n      flushSync(() => {\n        setIsPrinting(true);\n      })\n    }\n\n    function handleAfterPrint() {\n      setIsPrinting(false);\n    }\n\n    window.addEventListener('beforeprint', handleBeforePrint);\n    window.addEventListener('afterprint', handleAfterPrint);\n    return () => {\n      window.removeEventListener('beforeprint', handleBeforePrint);\n      window.removeEventListener('afterprint', handleAfterPrint);\n    }\n  }, []);\n\n  return (\n    <>\n      <h1>isPrinting: {isPrinting ? 'yes' : 'no'}</h1>\n      <button onClick={() => window.print()}>\n        Print\n      </button>\n    </>\n  );\n}\n```\n\n</Sandpack>\n\n`flushSync`를 사용하지 않으면 프린트 대화 상자는 `isPrinting`을 \"no\"로 표시합니다. React가 업데이트를 비동기적으로 Batch하고 프린트 대화 상자를 State가 업데이트되기 전에 표시하기 때문입니다.\n\n<Pitfall>\n\n`flushSync`를 사용하면 애플리케이션의 성능이 크게 저하될 수 있으며 보류 중인 Suspense 바운더리가 Fallback State를 표시하도록 강제할 수 있습니다.\n\n대부분의 경우 `flushSync`를 사용하지 않을 수 있으므로 최후의 수단으로 사용하세요.\n\n</Pitfall>\n\n---\n\n## Troubleshooting {/*troubleshooting*/}\n\n### I'm getting an error: \"flushSync was called from inside a lifecycle method\" {/*im-getting-an-error-flushsync-was-called-from-inside-a-lifecycle-method*/}\n\n\nReact cannot `flushSync` in the middle of a render. If you do, it will noop and warn:\n\n<ConsoleBlock level=\"error\">\n\nWarning: flushSync was called from inside a lifecycle method. React cannot flush when React is already rendering. Consider moving this call to a scheduler task or micro task.\n\n</ConsoleBlock>\n\nThis includes calling `flushSync` inside:\n\n- rendering a component.\n- `useLayoutEffect` or `useEffect` hooks.\n- Class component lifecycle methods.\n\nFor example, calling `flushSync` in an Effect will noop and warn:\n\n```js\nimport { useEffect } from 'react';\nimport { flushSync } from 'react-dom';\n\nfunction MyComponent() {\n  useEffect(() => {\n    // 🚩 Wrong: calling flushSync inside an effect\n    flushSync(() => {\n      setSomething(newValue);\n    });\n  }, []);\n\n  return <div>{/* ... */}</div>;\n}\n```\n\nTo fix this, you usually want to move the `flushSync` call to an event:\n\n```js\nfunction handleClick() {\n  // ✅ Correct: flushSync in event handlers is safe\n  flushSync(() => {\n    setSomething(newValue);\n  });\n}\n```\n\n\nIf it's difficult to move to an event, you can defer `flushSync` in a microtask:\n\n```js {3,7}\nuseEffect(() => {\n  // ✅ Correct: defer flushSync to a microtask\n  queueMicrotask(() => {\n    flushSync(() => {\n      setSomething(newValue);\n    });\n  });\n}, []);\n```\n\nThis will allow the current render to finish and schedule another syncronous render to flush the updates.\n\n<Pitfall>\n\n`flushSync` can significantly hurt performance, but this particular pattern is even worse for performance. Exhaust all other options before calling `flushSync` in a microtask as an escape hatch.\n\n</Pitfall>\n"
  },
  {
    "path": "src/content/reference/react-dom/hooks/index.md",
    "content": "---\ntitle: \"Built-in React DOM Hooks\"\n---\n\n<Intro>\n\n`react-dom` 패키지는 웹 애플리케이션만 지원하는 (브라우저의 DOM 환경에서 실행되는) Hook을 포함하고 있습니다. 이 Hook은 iOS, 안드로이드, Windows 애플리케이션과 같은 브라우저가 아닌 환경들은 지원하지 않습니다. 웹 브라우저뿐만 아니라 *다른 환경*에서도 지원되는 Hook을 찾고 있다면 [React Hook 페이지](/reference/react/hooks)를 참고하세요. 이 페이지는 `react-dom` 패키지에 포함된 모든 Hook을 나열하고 있습니다.\n\n</Intro>\n\n---\n\n## 폼 Hook {/*form-hooks*/}\n\n*폼*은 정보 제출을 위한 상호 작용형 제어를 만들 수 있도록 해줍니다. 컴포넌트에 있는 폼을 관리하기 위해 다음과 같은 Hook 중 하나를 사용할 수 있습니다.\n\n* [`useFormStatus`](/reference/react-dom/hooks/useFormStatus) 폼의 상태에 따라 UI를 업데이트할 수 있게 해줍니다.\n\n```js\nfunction Form({ action }) {\n  async function increment(n) {\n    return n + 1;\n  }\n  const [count, incrementFormAction] = useActionState(increment, 0);\n  return (\n    <form action={action}>\n      <button formAction={incrementFormAction}>Count: {count}</button>\n      <Button />\n    </form>\n  );\n}\n\nfunction Button() {\n  const { pending } = useFormStatus();\n  return (\n    <button disabled={pending} type=\"submit\">\n      Submit\n    </button>\n  );\n}\n```\n"
  },
  {
    "path": "src/content/reference/react-dom/hooks/useFormStatus.md",
    "content": "---\ntitle: useFormStatus\n---\n\n<Intro>\n\n`useFormStatus`는 마지막 폼 제출의 상태 정보를 제공하는 Hook입니다.\n\n```js\nconst { pending, data, method, action } = useFormStatus();\n```\n\n</Intro>\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `useFormStatus()` {/*use-form-status*/}\n\n`useFormStatus` Hook은 마지막 폼 제출의 상태 정보를 제공합니다.\n\n```js {5},[[1, 6, \"status.pending\"]]\nimport { useFormStatus } from \"react-dom\";\nimport action from './actions';\n\nfunction Submit() {\n  const status = useFormStatus();\n  return <button disabled={status.pending}>Submit</button>\n}\n\nexport default function App() {\n  return (\n    <form action={action}>\n      <Submit />\n    </form>\n  );\n}\n```\n\n상태 정보를 제공받기 위해 `Submit` 컴포넌트를 `<form>` 내부에 렌더링해야 합니다. 이 Hook은 폼이 현재 제출하고 있는 상태인지를 의미하는 <CodeStep step={1}>`pending`</CodeStep> 프로퍼티와 같은 상태 정보를 반환합니다.\n\n위의 예시에서 `Submit` 컴포넌트는 폼이 제출 중일 때 `<button>`을 누를 수 없도록 하기 위해 이 정보를 활용합니다.\n\n[아래 예시를 참고하세요.](#usage)\n\n#### 매개변수 {/*parameters*/}\n\n`useFormStatus`은 어떤 매개변수도 받지 않습니다.\n\n#### 반환값 {/*returns*/}\n\n다음의 프로퍼티를 가지는 `status` 객체를 반환합니다.\n\n* `pending`: 불리언 값입니다. `true`라면 상위 `<form>`이 아직 제출 중이라는 것을 의미합니다. 그렇지 않으면 `false`입니다.\n\n* `data`: [`FormData` 인터페이스](https://developer.mozilla.org/ko/docs/Web/API/FormData)를 구현한 객체로, 상위 `<form>`이 제출하는 데이터를 포함합니다. 활성화된 제출이 없거나 상위에 `<form>`이 없는 경우에는 `null`입니다.\n\n* `method`: `'get'` 또는 `'post'` 중 하나의 문자열 값입니다. 이 프로퍼티는 상위 `<form>`이 `GET` 또는 `POST` [HTTP 메서드](https://developer.mozilla.org/ko/docs/Web/HTTP/Methods)를 사용하여 제출되는지를 나타냅니다. 기본적으로 `<form>`은 `GET` 메서드를 사용하며 [`method`](https://developer.mozilla.org/ko/docs/Web/HTML/Element/form#method) 프로퍼티를 통해 지정할 수 있습니다.\n\n[//]: # (Link to `<form>` documentation. \"Read more on the `action` prop on `<form>`.\")\n* `action`: 상위 `<form>`의 `action` Prop에 전달한 함수의 레퍼런스입니다. 상위 `<form>`이 없는 경우에는 이 프로퍼티는 `null`입니다. `action` Prop에 URI 값이 제공되었거나 `action` prop를 지정하지 않았을 경우에는 `status.action`은 `null`입니다.\n\n#### 주의 사항 {/*caveats*/}\n\n* `useFormStatus` Hook은 `<form>` 내부에 렌더링한 컴포넌트에서 호출해야 합니다.\n* `useFormStatus`는 오직 상위 `<form>`에 대한 상태 정보만 반환합니다. 동일한 컴포넌트나 자식 컴포넌트에서 렌더링한 `<form>`의 상태 정보는 반환하지 않습니다.\n\n---\n\n## 사용법 {/*usage*/}\n\n### 폼을 제출하는 동안 대기 중인 상태로 표시하기 {/*display-a-pending-state-during-form-submission*/}\n폼을 제출하는 동안 대기<sup>Pending</sup> 상태를 표시하려면 `<form>` 내에서 렌더링한 컴포넌트에서 `useFormStatus` Hook을 호출하고 반환된 `pending` 프로퍼티를 확인하세요.\n\n여기서는 `pending` 프로퍼티를 사용하여 폼이 제출 중인지를 나타냅니다.\n\n<Sandpack>\n\n```js src/App.js\nimport { useFormStatus } from \"react-dom\";\nimport { submitForm } from \"./actions.js\";\n\nfunction Submit() {\n  const { pending } = useFormStatus();\n  return (\n    <button type=\"submit\" disabled={pending}>\n      {pending ? \"Submitting...\" : \"Submit\"}\n    </button>\n  );\n}\n\nfunction Form({ action }) {\n  return (\n    <form action={action}>\n      <Submit />\n    </form>\n  );\n}\n\nexport default function App() {\n  return <Form action={submitForm} />;\n}\n```\n\n```js src/actions.js hidden\nexport async function submitForm(query) {\n    await new Promise((res) => setTimeout(res, 1000));\n}\n```\n</Sandpack>\n\n<Pitfall>\n\n##### `useFormStatus`는 동일한 컴포넌트에서 렌더링한 `<form>`에 대한 상태 정보를 반환하지 않습니다. {/*useformstatus-will-not-return-status-information-for-a-form-rendered-in-the-same-component*/}\n\n`useFormStatus` Hook은 상위 `<form>`에 대한 정보만 반환합니다. Hook을 호출하는 동일한 컴포넌트나 자식 컴포넌트에서 렌더링한 `<form>`의 상태 정보는 반환하지 않습니다.\n\n```js\nfunction Form() {\n  // 🚩 `pending`은 절대 true가 되지 않습니다\n  // useFormStatus는 현재 컴포넌트에서 렌더링한 폼을 추적하지 않습니다\n  const { pending } = useFormStatus();\n  return <form action={submit}></form>;\n}\n```\n\n대신 `<form>` 내부에 위치한 컴포넌트에서 `useFormStatus`를 호출합니다.\n\n```js\nfunction Submit() {\n  // ✅ `pending`은 Submit 컴포넌트를 감싸는 폼에서 파생됩니다\n  const { pending } = useFormStatus();\n  return <button disabled={pending}>...</button>;\n}\n\nfunction Form() {\n  // `useFormStatus`가 추적하는 <form>입니다\n  return (\n    <form action={submit}>\n      <Submit />\n    </form>\n  );\n}\n```\n\n</Pitfall>\n\n### 제출한 폼 데이터 읽기 {/*read-form-data-being-submitted*/}\n\n`useFormStatus`에서 반환된 상태 정보의 `data` 프로퍼티를 사용하여 사용자가 제출한 데이터를 표시할 수 있습니다.\n\n여기에 사용자가 이름을 요청할 수 있는 폼이 있습니다. `useFormStatus`를 사용하여 사용자가 요청한 사용자 이름을 확인하는 임시 상태 메시지를 표시할 수 있습니다.\n\n<Sandpack>\n\n```js src/UsernameForm.js active\nimport {useState, useMemo, useRef} from 'react';\nimport {useFormStatus} from 'react-dom';\n\nexport default function UsernameForm() {\n  const {pending, data} = useFormStatus();\n\n  return (\n    <div>\n      <h3>Request a Username: </h3>\n      <input type=\"text\" name=\"username\" disabled={pending}/>\n      <button type=\"submit\" disabled={pending}>\n        Submit\n      </button>\n      <br />\n      <p>{data ? `Requesting ${data?.get(\"username\")}...`: ''}</p>\n    </div>\n  );\n}\n```\n\n```js src/App.js\nimport UsernameForm from './UsernameForm';\nimport { submitForm } from \"./actions.js\";\nimport {useRef} from 'react';\n\nexport default function App() {\n  const ref = useRef(null);\n  return (\n    <form ref={ref} action={async (formData) => {\n      await submitForm(formData);\n      ref.current.reset();\n    }}>\n      <UsernameForm />\n    </form>\n  );\n}\n```\n\n```js src/actions.js hidden\nexport async function submitForm(query) {\n    await new Promise((res) => setTimeout(res, 2000));\n}\n```\n\n```css\np {\n    height: 14px;\n    padding: 0;\n    margin: 2px 0 0 0 ;\n    font-size: 14px\n}\n\nbutton {\n    margin-left: 2px;\n}\n\n```\n\n</Sandpack>\n\n---\n\n## 문제 해결 {/*troubleshooting*/}\n\n### `status.pending`이 절대로 `true`가 되지 않습니다 {/*pending-is-never-true*/}\n\n`useFormStatus`는 오직 상위 `<form>`에 대한 상태 정보만 반환합니다.\n\n`useFormStatus`를 호출하는 컴포넌트가 `<form>`에 감싸져 있지 않다면, `status.pending`은 항상 `false`를 반환합니다. `useFormStatus`가 `<form>` 엘리먼트의 자식 컴포넌트에서 호출되는지 확인하세요.\n\n`useFormStatus`는 동일한 컴포넌트에서 렌더링한 `<form>`의 상태를 추적하지 않습니다. 자세한 내용은 ['주의하세요!'](#useformstatus-will-not-return-status-information-for-a-form-rendered-in-the-same-component)에서 확인할 수 있습니다.\n\n"
  },
  {
    "path": "src/content/reference/react-dom/index.md",
    "content": "---\ntitle: React DOM APIs\n---\n\n<Intro>\n\n`react-dom` 패키지는 웹 애플리케이션(브라우저 DOM 환경)에서만 지원되는 메서드를 포함합니다. React Native에서는 지원되지 않습니다.\n\n</Intro>\n\n---\n\n## API {/*apis*/}\n\n아래 API는 컴포넌트에서 불러올 수 있습니다. 사용할 일은 거의 없습니다.\n\n* [`createPortal`](/reference/react-dom/createPortal)을 사용하면 자식 컴포넌트를 DOM 트리의 다른 부분에 렌더링 할 수 있습니다.\n* [`flushSync`](/reference/react-dom/flushSync)를 사용하면 React가 State 업데이트를 수행하고 동기적으로 DOM을 업데이트하도록 강제할 수 있습니다.\n\n## Resource Preloading APIs {/*resource-preloading-apis*/}\n\n아래 API는 스크립트, 스타일시트, 글꼴과 같은 리소스를 미리 로드하여 앱 속도를 개선하는 데 사용할 수 있습니다. 예를 들어 특정 리소스가 사용될 다른 페이지로 이동하기 전에 리소스를 미리 불러올 수 있습니다.\n\n[React 기반 프레임워크](/learn/creating-a-react-app)에서는 일반적으로 리소스 로딩을 자동으로 처리해 주기 때문에 API를 직접 호출하지 않아도 됩니다. 자세한 내용은 사용하는 프레임워크의 문서를 참고하세요.\n\n* [`preconnect`](/reference/react-dom/preconnect)를 사용하면 어떤 리소스가 필요한지 모르더라도 리소스를 요청할 것으로 예상되는 서버와 미리 연결할 수 있습니다.\n* [`prefetchDNS`](/reference/react-dom/prefetchDNS)를 사용하면 접속할 가능성이 있는 DNS 도메인의 IP 주소를 미리 조회할 수 있습니다.\n* [`preinit`](/reference/react-dom/preinit)을 사용하면 외부 스크립트나 스타일시트를 미리 가져오고 실행할 수 있습니다.\n* [`preinitModule`](/reference/react-dom/preinitModule)을 사용하면 외부 ESM 모듈을 미리 가져오고 평가<sup>Evaluate</sup>할 수 있게 해줍니다.\n* [`preload`](/reference/react-dom/preload)를 사용하면 스타일시트, 글꼴, 이미지 또는 외부 스크립트 같은 리소스를 미리 가져올 수 있습니다.\n* [`preloadModule`](/reference/react-dom/preloadModule)을 사용하면 사용할 ESM 모듈을 미리 가져올 수 있습니다.\n\n---\n\n## 진입점 {/*entry-points*/}\n\n`react-dom` 패키지는 두 개의 진입점<sup>Entry Points</sup>을 제공합니다.\n\n* [`react-dom/client`](/reference/react-dom/client)는 React 컴포넌트를 클라이언트(브라우저)에 렌더링하는 API를 포함합니다.\n* [`react-dom/server`](/reference/react-dom/server)는 React 컴포넌트를 서버에 렌더링하는 API를 포함합니다.\n\n---\n\n## 제거된 API {/*removed-apis*/}\n\n아래 API는 React 19에서 제거되었습니다.\n\n* [`findDOMNode`](https://18.react.dev/reference/react-dom/findDOMNode): see [alternatives](https://18.react.dev/reference/react-dom/findDOMNode#alternatives).\n* [`hydrate`](https://18.react.dev/reference/react-dom/hydrate): use [`hydrateRoot`](/reference/react-dom/client/hydrateRoot) instead.\n* [`render`](https://18.react.dev/reference/react-dom/render): use [`createRoot`](/reference/react-dom/client/createRoot) instead.\n* [`unmountComponentAtNode`](/reference/react-dom/unmountComponentAtNode): use [`root.unmount()`](/reference/react-dom/client/createRoot#root-unmount) instead.\n* [`renderToNodeStream`](https://18.react.dev/reference/react-dom/server/renderToNodeStream): use [`react-dom/server`](/reference/react-dom/server) APIs instead.\n* [`renderToStaticNodeStream`](https://18.react.dev/reference/react-dom/server/renderToStaticNodeStream): use [`react-dom/server`](/reference/react-dom/server) APIs instead.\n"
  },
  {
    "path": "src/content/reference/react-dom/preconnect.md",
    "content": "---\ntitle: preconnect\n---\n\n<Intro>\n\n`preconnect`를 사용하면 리소스를 가져올 것으로 예상하는 서버에 연결할 수 있습니다.\n\n```js\npreconnect(\"https://example.com\");\n```\n\n</Intro>\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `preconnect(href)` {/*preconnect*/}\n\n호스트에 미리 연결하려면, `react-dom`에서 `preconnect`를 호출합니다.\n\n```js\nimport { preconnect } from 'react-dom';\n\nfunction AppRoot() {\n  preconnect(\"https://example.com\");\n  // ...\n}\n\n```\n\n[아래 예시를 참고하세요.](#usage)\n\n`preconnect`는 브라우저가 해당 서버와 연결을 맺어야 한다는 힌트를 제공합니다. 브라우저가 해당 서버를 선택하면, 해당 서버에서 리소스를 불러오는 속도가 빨라질 수 있습니다.\n\n#### 매개변수 {/*parameters*/}\n\n* `href`: 문자열. 연결할 서버의 URL입니다.\n\n\n#### 반환값 {/*returns*/}\n\n`preconnect`는 아무 값도 반환하지 않습니다.\n\n#### 주의 사항 {/*caveats*/}\n\n* 동일한 서버에 대해 여러 번의 `preconnect` 호출은 한 번의 호출과 동일한 효과를 갖습니다.\n* 브라우저에서 `preconnect`는 컴포넌트를 렌더링할 때, Effect 내부, 이벤트 핸들러 내부 등 모든 상황에서 호출할 수 있습니다.\n* 서버 사이드 렌더링 또는 서버 컴포넌트를 렌더링할 때, `preconnect`는 컴포넌트를 렌더링하는 동안 호출하거나 컴포넌트를 렌더링하는 컨텍스트에서 시작된 비동기 컨텍스트 내에서 호출할 때만 효과가 있습니다. 그 외의 호출은 무시됩니다.\n* 필요한 특정 리소스를 알고 있다면, [다른 함수](/reference/react-dom/#resource-preloading-apis)를 호출하여 리소스를 즉시 로드할 수 있습니다.\n* 웹 페이지 자체가 호스팅되는 동일한 서버에 사전 연결하는 것은 이점이 없습니다. 힌트가 주어질 때 웹 페이지 자체가 이미 연결되어 있기 때문입니다.\n\n---\n\n## 사용법 {/*usage*/}\n\n### 렌더링 시 사전 연결 {/*preconnecting-when-rendering*/}\n\n자식 컴포넌트가 해당 호스트에서 외부 리소스를 로드할 것임을 알고 있다면, 컴포넌트를 렌더링할 때 `preconnect`를 호출하세요.\n\n```js\nimport { preconnect } from 'react-dom';\n\nfunction AppRoot() {\n  preconnect(\"https://example.com\");\n  return ...;\n}\n```\n\n### 이벤트 핸들러에서 사전 연결 {/*preconnecting-in-an-event-handler*/}\n\n외부 리소스가 필요한 페이지나 State로 전환하기 전에 이벤트 핸들러에서 `preconnect`를 호출하세요. 이렇게 하면 새로운 페이지나 State를 렌더링할 때 호출하는 것보다 더 일찍 프로세스를 시작할 수 있습니다.\n\n```js\nimport { preconnect } from 'react-dom';\n\nfunction CallToAction() {\n  const onClick = () => {\n    preconnect('http://example.com');\n    startWizard();\n  }\n  return (\n    <button onClick={onClick}>Start Wizard</button>\n  );\n}\n```\n"
  },
  {
    "path": "src/content/reference/react-dom/prefetchDNS.md",
    "content": "---\ntitle: prefetchDNS\n---\n\n<Intro>\n\n`prefetchDNS`는 리소스를 가져올 것으로 예상하는 서버의 IP를 미리 조회할 수 있게 해줍니다.\n\n```js\nprefetchDNS(\"https://example.com\");\n```\n\n</Intro>\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `prefetchDNS(href)` {/*prefetchdns*/}\n\n호스트에 미리 연결하려면, `react-dom`에서 `prefetchDNS`를 호출합니다.\n\n```js\nimport { prefetchDNS } from 'react-dom';\n\nfunction AppRoot() {\n  prefetchDNS(\"https://example.com\");\n  // ...\n}\n\n```\n\n[아래 예시를 참고하세요.](#usage)\n\n`prefetchDNS`는 브라우저에게 주어진 서버의 IP 주소를 조회해야 한다는 힌트를 제공합니다. 브라우저가 이를 수행하기로 선택하면 해당 서버에서 리소스를 로딩하는 속도가 빨라질 수 있습니다.\n\n#### 매개변수 {/*parameters*/}\n\n* `href`: 문자열. 연결하려는 서버의 URL입니다.\n\n#### 반환값 {/*returns*/}\n\n`prefetchDNS`는 아무 값도 반환하지 않습니다.\n\n#### 주의 사항 {/*caveats*/}\n\n* 동일한 서버에 대해 여러 번의 `prefetchDNS` 호출은 한 번의 호출과 동일한 효과를 갖습니다.\n* 브라우저에서는 `prefetchDNS`를 컴포넌트 렌더링할 때, Effect 내부, 이벤트 핸들러 등 모든 상황에서 호출할 수 있습니다.\n* 서버 사이드 렌더링 또는 서버 컴포넌트를 렌더링할 때, `prefetchDNS`는 컴포넌트를 렌더링하는 동안 또는 컴포넌트 렌더링에서 시작된 비동기 컨텍스트 내에서 호출하는 경우에만 효과가 있습니다. 그 외의 호출은 무시됩니다.\n* 필요한 특정 리소스를 알고 있다면, 리소스를 즉시 로딩할 수 있는 [다른 함수](/reference/react-dom/#resource-preloading-apis)를 호출할 수 있습니다.\n* 웹페이지 자체가 호스팅되는 동일한 서버를 미리 조회하는 것은 이점이 없습니다. 힌트가 주어진 시점에는 이미 조회가 완료되었기 때문입니다.\n* [`preconnect`](/reference/react-dom/preconnect)와 비교했을 때, `prefetchDNS` 는 여러 도메인에 대해 사전 연결을 시도하는 경우, 사전 연결의 오버헤드가 이점을 상쇄할 수 있다는 점에서 더 나을 수 있습니다.\n\n---\n\n## 사용법 {/*usage*/}\n\n### 렌더링 시 DNS 미리 가져오기 {/*prefetching-dns-when-rendering*/}\n\n해당 컴포넌트의 자식들이 해당 호스트에서 외부 리소스를 로딩할 것을 알고 있다면, 컴포넌트를 렌더링할 때 `prefetchDNS`를 호출하세요.\n\n```js\nimport { prefetchDNS } from 'react-dom';\n\nfunction AppRoot() {\n  prefetchDNS(\"https://example.com\");\n  return ...;\n}\n```\n\n### 이벤트 핸들러에서 DNS 미리 가져오기 {/*prefetching-dns-in-an-event-handler*/}\n\n외부 리소스가 필요한 페이지나 State로 전환하기 전에 이벤트 핸들러에서 `prefetchDNS`를 호출하세요. 이렇게 하면 새로운 페이지나 State를 렌더링할 때 호출하는 것보다 더 일찍 프로세스를 시작할 수 있습니다.\n\n```js\nimport { prefetchDNS } from 'react-dom';\n\nfunction CallToAction() {\n  const onClick = () => {\n    prefetchDNS('http://example.com');\n    startWizard();\n  }\n  return (\n    <button onClick={onClick}>Start Wizard</button>\n  );\n}\n```\n"
  },
  {
    "path": "src/content/reference/react-dom/preinit.md",
    "content": "---\ntitle: preinit\n---\n\n<Note>\n\n[React 기반 프레임워크](/learn/creating-a-react-app)는 종종 리소스 로딩을 자동으로 처리하므로, 이 API를 직접 호출하지 않아도 됩니다. 자세한 내용은 사용하는 프레임워크의 문서를 참고하세요.\n\n</Note>\n\n<Intro>\n\n`preinit`은 스타일시트나 외부 스크립트를 미리 가져오고 실행할 수 있게 합니다.\n\n```js\npreinit(\"https://example.com/script.js\", {as: \"script\"});\n```\n\n</Intro>\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `preinit(href, options)` {/*preinit*/}\n\n스크립트나 스타일시트를 preinit 하려면 `react-dom`에서 `preinit`함수를 호출하세요.\n\n```js\nimport { preinit } from 'react-dom';\n\nfunction AppRoot() {\n  preinit(\"https://example.com/script.js\", {as: \"script\"});\n  // ...\n}\n\n```\n\n[아래 예시에서 더 보기.](#usage)\n\n`preinit` 함수는 브라우저에게 주어진 리소스를 다운로드하고 실행하라는 힌트를 제공하여 시간 절약에 도움을 줍니다. `preinit`한 스크립트는 다운로드가 완료되면 즉시 실행됩니다. 스타일시트는 문서에 삽입되어 곧바로 적용됩니다.\n\n#### 매개변수 {/*parameters*/}\n\n* `href`: 문자열. 다운로드하고 실행할 리소스의 URL입니다.\n* `options`: 객체. 다음 속성들을 포함할 수 있습니다.\n  * `as`: 필수 문자열. 리소스의 유형입니다. 가능한 값은 `script`와 `style`입니다.\n  * `precedence`: 문자열. 스타일시트에 필수입니다. 다른 스타일시트와의 삽입 순서를 결정합니다. 우선순위가 높은 스타일시트가 낮은 것을 덮어쓸 수 있습니다. 가능한 값은 `reset`, `low`, `medium`, `high`입니다.\n  * `crossOrigin`: 문자열. 사용할 [CORS 정책](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/crossorigin)입니다. 가능한 값은 `anonymous` 와 `use-credentials`입니다.\n  * `integrity`: 문자열. 리소스의 [무결성을 검증](https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity)하기 위한 암호화 해시입니다.\n  * `nonce`: 문자열. 엄격한 콘텐츠 보안 정책을 사용할 때, 리소스를 허용하기 위한 암호화된 [nonce to allow the resource](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/nonce)입니다.\n  * `fetchPriority`: 문자열. 리소스를 가져오는 데 사용할 상대적인 우선순위를 제안합니다. 가능한 값은 `auto` (기본값), `high`, `low`입니다.\n\n#### 반환값 {/*returns*/}\n\n`preinit`은 아무것도 반환하지 않습니다.\n\n#### 주의 사항 {/*caveats*/}\n\n* 동일한 `href`로 `preinit`을 여러 번 호출해도, 한 번 호출한 것과 동일한 효과만 발생합니다.\n* 브라우저에서는 컴포넌트를 렌더링할 때, Effect 내부에서, 이벤트 핸들러 안에서 등 어떤 상황에서든 `preinit`을 호출할 수 있습니다.\n* 서버 사이드 렌더링 또는 서버 컴포넌트를 렌더링할 때는, 컴포넌트를 렌더링하면서 또는 컴포넌트 렌더링에서 시작된 비동기 컨텍스트 내에서만 `preinit` 호출이 효과를 가집니다. 그 외의 호출은 무시됩니다.\n\n\n\n---\n\n## 사용법 {/*usage*/}\n\n### 렌더링 시 preinit 하기 {/*preiniting-when-rendering*/}\n\n특정 컴포넌트나 그 자식 컴포넌트가 특정 리소스를 사용할 것을 알고 있고, 해당 리소스가 다운로드되자마자 바로 실행되거나 적용되는 것이 괜찮다면, 컴포넌트를 렌더링할 때 `preinit`을 호출하세요.\n\n<Recipes titleText=\"preinit 사용 예시\">\n\n#### 외부 스크립트 preinit 하기 {/*preiniting-an-external-script*/}\n\n```js\nimport { preinit } from 'react-dom';\n\nfunction AppRoot() {\n  preinit(\"https://example.com/script.js\", {as: \"script\"});\n  return ...;\n}\n```\n\n스크립트를 다운로드하되 즉시 실행하지 않으려면 [`preload`](/reference/react-dom/preload)를 사용하세요. ESM 모듈을 로드하려면 [`preinitModule`](/reference/react-dom/preinitModule)을 사용하세요.\n\n<Solution />\n\n#### 스타일시트 preinit 하기 {/*preiniting-a-stylesheet*/}\n\n```js\nimport { preinit } from 'react-dom';\n\nfunction AppRoot() {\n  preinit(\"https://example.com/style.css\", {as: \"style\", precedence: \"medium\"});\n  return ...;\n}\n```\n\n스타일시트의 삽입 순서를 제어하려면 필수 옵션인 `precedence`를 지정하세요. 우선순위가 높은 스타일시트가 낮은 것을 덮어쓸 수 있습니다.\n\n스타일시트를 다운로드하되 문서에 바로 삽입하지 않으려면 [`preload`](/reference/react-dom/preload)를 사용하세요.\n\n<Solution />\n\n</Recipes>\n\n### 이벤트 핸들러 내에서 preinit 하기 {/*preiniting-in-an-event-handler*/}\n\n외부 리소스가 필요한 페이지나 상태로 전환하기 전에 이벤트 핸들러에서 `preinit`을 호출하세요. 이렇게 하면 새 페이지나 상태 렌더링 시점보다 더 일찍 리소스 다운로드가 시작됩니다.\n\n```js\nimport { preinit } from 'react-dom';\n\nfunction CallToAction() {\n  const onClick = () => {\n    preinit(\"https://example.com/wizardStyles.css\", {as: \"style\"});\n    startWizard();\n  }\n  return (\n    <button onClick={onClick}>Start Wizard</button>\n  );\n}\n```\n"
  },
  {
    "path": "src/content/reference/react-dom/preinitModule.md",
    "content": "---\ntitle: preinitModule\n---\n\n<Note>\n\n[React 기반 프레임워크](/learn/creating-a-react-app)에서는 리소스 로딩을 자동으로 처리해주는 경우가 많기 때문에 이 API를 직접 호출할 필요가 없을 수도 있습니다. 자세한 내용은 사용하는 프레임워크의 문서를 참고하세요.\n\n</Note>\n\n<Intro>\n\n`preinitModule`은 ESM 모듈을 미리 가져오고 평가(evaluate)할 수 있게 해줍니다.\n\n```js\npreinitModule(\"https://example.com/module.js\", {as: \"script\"});\n```\n\n</Intro>\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `preinitModule(href, options)` {/*preinitmodule*/}\n\nESM 모듈을 사전에 preinit하려면, `react-dom` 패키지에서 `preinitModule` 함수를 호출하세요.\n\n```js\nimport { preinitModule } from 'react-dom';\n\nfunction AppRoot() {\n  preinitModule(\"https://example.com/module.js\", {as: \"script\"});\n  // ...\n}\n\n```\n\n[아래 예시를 더 참고하세요.](#usage)\n\n`preinitModule` 함수는 브라우저에 해당 모듈을 다운로드하고 실행할 수 있다는 힌트를 제공하므로, 로딩 시간을 단축하는 데 도움이 됩니다. `preinit`된 모듈은 다운로드가 완료되는 즉시 실행됩니다.\n\n#### 매개변수 {/*parameters*/}\n\n* `href`: 문자열입니다. 다운로드하고 실행할 모듈의 URL입니다.\n* `options`: 객체입니다. 다음 속성을 포함합니다:\n  *  `as`: 필수 문자열입니다. 반드시 `'script'`여야 합니다.\n  *  `crossOrigin`: 문자열입니다. 사용할 [CORS policy](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/crossorigin)을 지정합니다. 가능한 값은 `anonymous` 또는 `use-credentials`입니다.\n  *  `integrity`: 문자열입니다. 모듈의 암호학적 해시로, [무결성을 검증](https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity)하는 데 사용됩니다.\n  *  `nonce`: 문자열입니다. 엄격한 Content Security Policy를 사용할 때 모듈을 허용하기 위한 암호학적 [nonce](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/nonce)입니다.\n\n#### 반환값 {/*returns*/}\n\n`preinitModule`은 값을 반환하지 않습니다.\n\n#### 주의 사항 {/*caveats*/}\n\n* 동일한 `href`로 `preinitModule`을 여러 번 호출해도, 한 번 호출한 것과 동일한 효과만 발생합니다.\n* 브라우저에서는 컴포넌트를 렌더링할 때, 이펙트 안에서, 이벤트 핸들러 등 어떤 상황에서도 `preinitModule`을 호출할 수 있습니다.\n* 서버 측 렌더링이나 Server Components를 렌더링할 때는, `preinitModule`는 컴포넌트를 렌더링하는 중이거나 렌더링에서 파생된 비동기 컨텍스트 내에서 호출한 경우에만 효과가 있습니다. 그 외의 호출은 무시됩니다.\n\n\n---\n\n## 사용법 {/*usage*/}\n\n### 렌더링 중 사전 로딩하기 {/*preloading-when-rendering*/}\n\n특정 모듈이 현재 컴포넌트나 자식 컴포넌트에서 사용될 것임을 알고 있고, 해당 모듈이 다운로드 즉시 평가되어 효과를 발휘해도 괜찮다면, 컴포넌트를 렌더링할 때 `preinitModule`을 호출하세요.\n\n```js\nimport { preinitModule } from 'react-dom';\n\nfunction AppRoot() {\n  preinitModule(\"https://example.com/module.js\", {as: \"script\"});\n  return ...;\n}\n```\n\n모듈을 다운로드하되 즉시 실행하지 않으려면 [`preloadModule`](/reference/react-dom/preloadModule)을 사용하세요. ESM 모듈이 아닌 스크립트를 사전 초기화하려면 [`preinit`](/reference/react-dom/preinit)을 사용하세요.\n\n### 이벤트 핸들러에서 사전 로딩하기 {/*preloading-in-an-event-handler*/}\n\n이벤트 핸들러에서 모듈이 필요해질 페이지나 상태로 전환하기 전에 `preinitModule`을 호출하세요. 이렇게 하면 새로운 페이지나 상태를 렌더링할 때보다 더 일찍 로딩을 시작할 수 있습니다.\n\n```js\nimport { preinitModule } from 'react-dom';\n\nfunction CallToAction() {\n  const onClick = () => {\n    preinitModule(\"https://example.com/module.js\", {as: \"script\"});\n    startWizard();\n  }\n  return (\n    <button onClick={onClick}>Start Wizard</button>\n  );\n}\n```\n"
  },
  {
    "path": "src/content/reference/react-dom/preload.md",
    "content": "---\ntitle: preload\n---\n\n<Note>\n\n[React 기반 프레임워크](/learn/creating-a-react-app)는 리소스 로딩을 대신 처리하는 경우가 많으므로 이 API를 직접 호출할 필요가 없을 수도 있습니다. 자세한 내용은 해당 프레임워크의 문서를 참조하세요.\n\n</Note>\n\n<Intro>\n\n`preload`를 사용하면 사용할 스타일시트, 글꼴 또는 외부 스크립트와 같은 리소스를 미리 가져올 수 있습니다.\n\n```js\npreload(\"https://example.com/font.woff2\", {as: \"font\"});\n```\n\n</Intro>\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `preload(href, options)` {/*preload*/}\n\n리소스를 미리 불러오려면 `react-dom`에서 `preload` 함수를 호출합니다.\n\n```js\nimport { preload } from 'react-dom';\n\nfunction AppRoot() {\n  preload(\"https://example.com/font.woff2\", {as: \"font\"});\n  // ...\n}\n\n```\n\n[아래 예시를 참조하세요.](#usage)\n\n`preload` 기능은 브라우저에 주어진 리소스 다운로드를 시작해야 한다는 힌트를 제공하여 시간을 절약할 수 있습니다.\n\n#### 매개변수 {/*parameters*/}\n\n* `href`: 문자열입니다. 다운로드하려는 리소스의 URL입니다.\n* `options`: 객체입니다. 여기에는 다음과 같은 속성이 포함되어 있습니다.\n  *  `as`: 필수 문자열입니다. 리소스의 타입입니다. [가능한 값](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#as)은 `audio`, `document`, `embed`, `fetch`, `font`, `image`, `object`, `script`, `style`, `track`, `video`, `worker`입니다.\n  *  `crossOrigin`: 문자열입니다. 사용할 [CORS 정책](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/crossorigin)입니다. 가능한 값은 `anonymous`와 `use-credentials`입니다. `as`가 `\"fetch\"`로 설정된 경우 필수입니다.\n  *  `referrerPolicy`: 문자열입니다. 가져오기<sup>Fetching</sup>할 때 전송할 [Referrer 헤더](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#referrerpolicy)입니다. 사용할 수 있는 값은 `no-referrer-when-downgrade` (기본값), `no-referrer`, `origin`, `origin-when-cross-origin` 그리고 `unsafe-url`입니다.\n  *  `integrity`: 문자열입니다. 리소스의 [진위 확인](https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity)을 위한 리소스의 암호화 해시입니다.\n  *  `type`: 문자열입니다. 리소스의 MIME 타입입니다.\n  *  `nonce`: 문자열입니다. 엄격한 콘텐츠 보안 정책을 사용할 때 [리소스를 허용하기 위한 암호화 논스](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/nonce)입니다.\n  *  `fetchPriority`: 문자열입니다. 리소스 페칭의 상대적 우선순위를 제안합니다. 사용할 수 있는 값은 `auto` (기본값), `high` 그리고 `low`입니다.\n  *  `imageSrcSet`: 문자열입니다. `as: \"image\"`와만 함께 사용합니다. [이미지의 소스 세트](https://developer.mozilla.org/en-US/docs/Learn/HTML/Multimedia_and_embedding/Responsive_images)를 지정합니다.\n  *  `imageSizes`: 문자열입니다. `as: \"image\"`와만 함께 사용합니다. [이미지의 크기](https://developer.mozilla.org/en-US/docs/Learn/HTML/Multimedia_and_embedding/Responsive_images)를 지정합니다.\n\n#### 반환값 {/*returns*/}\n\n`preload`는 아무것도 반환하지 않습니다.\n\n#### 주의 사항 {/*caveats*/}\n\n* `preload`에 대한 여러 번의 동등한 호출은 한 번의 호출과 동일한 효과를 갖습니다. `preload`에 대한 호출은 다음 규칙에 따라 동등한 것으로 간주합니다.\n  * 아래 경우를 제외하고 두 호출의 `href`가 같으면 두 호출은 동일합니다.\n  * `as`가 `image`로 설정된 경우 두 호출의 `href`, `imageSrcSet` 및 `imageSizes`가 같으면 두 호출은 동일합니다.\n* 브라우저에서는 컴포넌트 렌더링 중, Effect, 이벤트 핸들러 등 어떤 상황에서도 `preload`를 호출할 수 있습니다.\n* 서버 사이드 렌더링 또는 서버 컴포넌트를 렌더링할 때 `preload`는 컴포넌트를 렌더링하는 동안 또는 컴포넌트 렌더링에서 발생하는 비동기 컨텍스트에서 호출하는 경우에만 영향을 미칩니다. 다른 호출은 무시됩니다.\n\n---\n\n## 사용법 {/*usage*/}\n\n### 렌더링 시 미리 불러오기 {/*preloading-when-rendering*/}\n\n컴포넌트 또는 그 자식 컴포넌트가 특정 리소스를 사용한다는 것을 알고 있다면 컴포넌트를 렌더링할 때 `preload`를 호출하세요.\n\n<Recipes titleText=\"미리 불러오기 예시\">\n\n#### 외부 스크립트 미리 불러오기 {/*preloading-an-external-script*/}\n\n```js\nimport { preload } from 'react-dom';\n\nfunction AppRoot() {\n  preload(\"https://example.com/script.js\", {as: \"script\"});\n  return ...;\n}\n```\n\n브라우저에서 스크립트 실행을 즉시 시작하려면(스크립트를 다운로드하는 대신) [`preinit`](/reference/react-dom/preinit)을 대신 사용하세요. ESM 모듈을 로드하려면 [`preloadModule`](/reference/react-dom/preloadModule)을 사용하세요.\n\n<Solution />\n\n#### 스타일시트 미리 불러오기 {/*preloading-a-stylesheet*/}\n\n```js\nimport { preload } from 'react-dom';\n\nfunction AppRoot() {\n  preload(\"https://example.com/style.css\", {as: \"style\"});\n  return ...;\n}\n```\n\n스타일시트를 문서에 즉시 삽입하려면(즉, 브라우저가 다운로드하는 대신 즉시 구문 분석을 시작한다는 의미) [`preinit`](/reference/react-dom/preinit)을 대신 사용하세요.\n\n<Solution />\n\n#### 글꼴 미리 불러오기 {/*preloading-a-font*/}\n\n```js\nimport { preload } from 'react-dom';\n\nfunction AppRoot() {\n  preload(\"https://example.com/style.css\", {as: \"style\"});\n  preload(\"https://example.com/font.woff2\", {as: \"font\"});\n  return ...;\n}\n```\n\n스타일시트를 미리 불러오는 경우 스타일시트가 참조하는 모든 글꼴도 미리 불러오는 것이 좋습니다. 이렇게 하면 브라우저가 스타일시트를 다운로드하고 구문 분석하기 전에 글꼴 다운로드를 시작할 수 있습니다.\n\n<Solution />\n\n#### 이미지 미리 불러오기 {/*preloading-an-image*/}\n\n```js\nimport { preload } from 'react-dom';\n\nfunction AppRoot() {\n  preload(\"/banner.png\", {\n    as: \"image\",\n    imageSrcSet: \"/banner512.png 512w, /banner1024.png 1024w\",\n    imageSizes: \"(max-width: 512px) 512px, 1024px\",\n  });\n  return ...;\n}\n```\n\n이미지를 미리 불러올 때 `imageSrcSet` 과 `imageSizes` 옵션은 브라우저가 [화면 크기에 맞는 올바른 크기의 이미지를 가져오는 데](https://developer.mozilla.org/en-US/docs/Learn/HTML/Multimedia_and_embedding/Responsive_images) 도움이 됩니다.\n\n<Solution />\n\n</Recipes>\n\n### 이벤트 핸들러에서 미리 불러오기 {/*preloading-in-an-event-handler*/}\n\n외부 리소스가 필요한 페이지나 State로 전환하기 전에 이벤트 핸들러에서 `preload`를 호출하세요. 이렇게 하면 새 페이지나 State를 렌더링하는 동안 호출하는 것보다 프로세스가 더 빨리 시작됩니다.\n\n```js\nimport { preload } from 'react-dom';\n\nfunction CallToAction() {\n  const onClick = () => {\n    preload(\"https://example.com/wizardStyles.css\", {as: \"style\"});\n    startWizard();\n  }\n  return (\n    <button onClick={onClick}>Start Wizard</button>\n  );\n}\n```\n"
  },
  {
    "path": "src/content/reference/react-dom/preloadModule.md",
    "content": "---\ntitle: preloadModule\n---\n\n<Note>\n\n[React 기반 프레임워크](/learn/creating-a-react-app)는 리소스 로딩을 대신 처리하는 경우가 많으므로 이 API를 직접 호출할 필요가 없을 수도 있습니다. 자세한 내용은 해당 프레임워크의 문서를 참조하세요.\n\n</Note>\n\n<Intro>\n\n`preloadModule`을 사용하면 사용할 ESM 모듈을 미리 가져올 수 있습니다.\n\n```js\npreloadModule(\"https://example.com/module.js\", {as: \"script\"});\n```\n\n</Intro>\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `preloadModule(href, options)` {/*preloadmodule*/}\n\nESM 모듈을 미리 불러오려면 `react-dom`에서 `preloadModule` 함수를 호출합니다.\n\n```js\nimport { preloadModule } from 'react-dom';\n\nfunction AppRoot() {\n  preloadModule(\"https://example.com/module.js\", {as: \"script\"});\n  // ...\n}\n\n```\n\n[아래 예시를 참조하세요.](#usage)\n\n`preloadModule` 기능은 브라우저에 주어진 모듈 다운로드를 시작해야 한다는 힌트를 제공하여 시간을 절약할 수 있습니다.\n\n#### 매개변수 {/*parameters*/}\n\n* `href`: 문자열입니다. 다운로드하려는 모듈의 URL입니다.\n* `options`: 객체입니다. 여기에는 다음과 같은 속성들이 포함되어 있습니다.\n  *  `as`: 필수 문자열입니다. `'script'`여야 합니다.\n  *  `crossOrigin`: 문자열입니다. 사용할 [CORS 정책](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/crossorigin)입니다. 가능한 값은 `anonymous`와 `use-credentials`입니다.\n  *  `integrity`: 문자열입니다. 모듈의 [신뢰성 확인](https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity)을 위한\n  모듈의 암호화 해시입니다.\n  *  `nonce`: 문자열입니다. 엄격한 컨텐츠 보안 정책을 사용할 때 [모듈을 허용하기 위한 암호화 논스(Nonce)](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/nonce)입니다.\n\n\n#### 반환값 {/*returns*/}\n\n`preloadModule`는 아무것도 반환하지 않습니다.\n\n#### 주의 사항 {/*caveats*/}\n\n* 동일한 `href`로 `preloadModule`을 여러 번 호출하는 것은 한 번 호출하는 것과 동일한 효과가 있습니다.\n* 브라우저에서는 컴포넌트를 렌더링하는 중이거나, Effect, 이벤트 핸들러 등 어떤 상황에서도 `preloadModule`을 호출할 수 있습니다.\n* 서버 측 렌더링이나 서버 컴포넌트를 렌더링할 때, `preloadModule`은 컴포넌트를 렌더링하는 동안 호출하거나, 컴포넌트 렌더링에서 시작된 비동기 컨텍스트 내에서 호출할 때만 효과가 있습니다. 그 외의 호출은 무시됩니다.\n\n---\n\n## 사용법 {/*usage*/}\n\n### 렌더링 시 미리 불러오기 {/*preloading-when-rendering*/}\n\n컴포넌트 또는 그 자식 컴포넌트가 특정 모듈을 사용한다는 것을 알고 있다면, 컴포넌트를 렌더링할 때 `preloadModule`을 호출하세요.\n\n```js\nimport { preloadModule } from 'react-dom';\n\nfunction AppRoot() {\n  preloadModule(\"https://example.com/module.js\", {as: \"script\"});\n  return ...;\n}\n```\n\n브라우저에서 모듈을 즉시 실행하려면(단순히 다운로드하는 것 대신) [`preinitModule`](/reference/react-dom/preinitModule)을 대신 사용하세요. ESM 모듈이 아닌 스크립트를 로드하려면 [`preload`](/reference/react-dom/preload)를 사용하세요.\n\n### 이벤트 핸들러에서 미리 불러오기 {/*preloading-in-an-event-handler*/}\n\n모듈이 필요한 페이지나 State를 전환되기 전에 이벤트 핸들러에서 `preloadModule`을 호출하세요. 이렇게 하면 새로운 페이지나 State를 렌더링할 때 호출하는 것보다 더 일찍 프로세스를 시작할 수 있습니다.\n\n```js\nimport { preloadModule } from 'react-dom';\n\nfunction CallToAction() {\n  const onClick = () => {\n    preloadModule(\"https://example.com/module.js\", {as: \"script\"});\n    startWizard();\n  }\n  return (\n    <button onClick={onClick}>Start Wizard</button>\n  );\n}\n```\n"
  },
  {
    "path": "src/content/reference/react-dom/server/index.md",
    "content": "---\ntitle: Server React DOM APIs\n---\n\n<Intro>\n\nThe `react-dom/server` APIs let you server-side render React components to HTML. These APIs are only used on the server at the top level of your app to generate the initial HTML. A [framework](/learn/creating-a-react-app#full-stack-frameworks) may call them for you. Most of your components don't need to import or use them.\n\n</Intro>\n\n---\n\n## Server APIs for Web Streams {/*server-apis-for-web-streams*/}\n\n다음 메서드들은 브라우저, Deno 및 일부 최신 엣지 런타임을 포함하는 [Web Stream](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API)이 있는 환경에서만 사용할 수 있습니다.\n\n* [`renderToReadableStream`](/reference/react-dom/server/renderToReadableStream) renders a React tree to a [Readable Web Stream.](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream)\n* [`resume`](/reference/react-dom/server/renderToPipeableStream) resumes [`prerender`](/reference/react-dom/static/prerender) to a [Readable Web Stream](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream).\n\n\n<Note>\n\nNode.js also includes these methods for compatibility, but they are not recommended due to worse performance. Use the [dedicated Node.js APIs](#server-apis-for-nodejs-streams) instead.\n\n</Note>\n---\n\n## Server APIs for Node.js Streams {/*server-apis-for-nodejs-streams*/}\n\nThese methods are only available in the environments with [Node.js Streams:](https://nodejs.org/api/stream.html)\n\n* [`renderToPipeableStream`](/reference/react-dom/server/renderToPipeableStream) renders a React tree to a pipeable [Node.js Stream.](https://nodejs.org/api/stream.html)\n* [`resumeToPipeableStream`](/reference/react-dom/server/renderToPipeableStream) resumes [`prerenderToNodeStream`](/reference/react-dom/static/prerenderToNodeStream) to a pipeable [Node.js Stream.](https://nodejs.org/api/stream.html)\n\n---\n\n## Legacy Server APIs for non-streaming environments {/*legacy-server-apis-for-non-streaming-environments*/}\n\n다음 메서드들은 Stream을 지원하지 않는 환경에서 사용할 수 있습니다.\n\n* [`renderToString`](/reference/react-dom/server/renderToString)은 React 트리를 문자열로 렌더링합니다.\n* [`renderToStaticMarkup`](/reference/react-dom/server/renderToStaticMarkup)은 상호작용하지 않는 React 트리를 문자열로 렌더링합니다.\n\n위 메서드들은 스트리밍 API와 비교하여 기능이 제한적입니다.\n"
  },
  {
    "path": "src/content/reference/react-dom/server/renderToPipeableStream.md",
    "content": "---\ntitle: renderToPipeableStream\n---\n\n<Intro>\n\n`renderToPipeableStream`은 React 트리를 파이프 가능한 [Node.js 스트림](https://nodejs.org/api/stream.html)으로 렌더링합니다.\n\n```js\nconst { pipe, abort } = renderToPipeableStream(reactNode, options?)\n```\n\n</Intro>\n\n<InlineToc />\n\n<Note>\n\n이 API는 Node.js 전용입니다. Deno 및 최신 엣지 런타임과 같은 [Web 스트림](https://developer.mozilla.org/ko/docs/Web/API/Streams_API)이 있는 환경에서는 [`renderToReadableStream`](/reference/react-dom/server/renderToReadableStream)을 대신 사용하세요.\n\n</Note>\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `renderToPipeableStream(reactNode, options?)` {/*rendertopipeablestream*/}\n\n`renderToPipeableStream`을 호출하여 React 트리를 HTML로 [Node.js 스트림](https://nodejs.org/api/stream.html#writable-streams)에 렌더링합니다.\n\n```js\nimport { renderToPipeableStream } from 'react-dom/server';\n\nconst { pipe } = renderToPipeableStream(<App />, {\n  bootstrapScripts: ['/main.js'],\n  onShellReady() {\n    response.setHeader('content-type', 'text/html');\n    pipe(response);\n  }\n});\n```\n\n클라이언트에서 [`hydrateRoot`](/reference/react-dom/client/hydrateRoot)를 호출하여 서버에서 생성된 HTML을 상호작용할 수 있도록 만듭니다.\n\n[아래에서 더 많은 예시를 확인하세요.](#usage)\n\n#### 매개변수 {/*parameters*/}\n\n* `reactNode`: HTML로 렌더링하려는 React 노드. 예를 들어, `<App />`과 같은 JSX 엘리먼트입니다. 전체 문서를 나타낼 것으로 예상되므로 `App` 컴포넌트는 `<html>` 태그를 렌더링해야 합니다.\n\n* **선택 사항** `options`: 스트리밍 옵션이 있는 객체입니다.\n  * **선택 사항** `bootstrapScriptContent`: 지정하면 이 문자열이 인라인 `<script>` 태그에 배치됩니다.\n  * **선택 사항** `bootstrapScripts`: 페이지에 표시할 `<script>` 태그에 대한 문자열 URL 배열입니다. 이를 사용하여 [`hydrateRoot`](/reference/react-dom/client/hydrateRoot)를 호출하는 `<script>`를 포함하세요. 클라이언트에서 React를 전혀 실행하지 않으려면 생략하세요.\n  * **선택 사항** `bootstrapModules`: `bootstrapScripts`와 같지만 대신 [`<script type=\"module\">`](https://developer.mozilla.org/ko/docs/Web/JavaScript/Guide/Modules)를 출력합니다.\n  * **선택 사항** `identifierPrefix`: React가 [`useId`](/reference/react/useId)에 의해 생성된 ID에 사용하는 문자열 접두사입니다. 같은 페이지에서 여러 루트를 사용할 때 충돌을 피하는 데 유용합니다. [`hydrateRoot`](/reference/react-dom/client/hydrateRoot#parameters)에 전달된 것과 동일한 접두사여야 합니다.\n  * **선택 사항** `namespaceURI`: 스트림의 루트 [네임스페이스 URI](https://developer.mozilla.org/ko/docs/Web/API/Document/createElementNS#important_namespace_uris)가 포함된 문자열입니다. 기본값은 일반 HTML입니다. SVG의 경우 `'http://www.w3.org/2000/svg'`를, MathML의 경우 `'http://www.w3.org/1998/Math/MathML'`를 전달합니다.\n  * **선택 사항** `nonce`: [`script-src` Content-Security-Policy](https://developer.mozilla.org/ko/docs/Web/HTTP/Headers/Content-Security-Policy/script-src)에 대한 스크립트를 허용하는 [`nonce`](http://developer.mozilla.org/ko/docs/Web/HTML/Element/script#nonce) 문자열입니다.\n  * **선택 사항** `onAllReady`: [셸](#specifying-what-goes-into-the-shell)과 모든 추가 [콘텐츠](#streaming-more-content-as-it-loads)를 포함하여 모든 렌더링이 완료되면 호출되는 콜백입니다. [크롤러 및 정적 생성에](#waiting-for-all-content-to-load-for-crawlers-and-static-generation) `onShellReady` 대신 이 함수를 사용할 수 있습니다. 여기서 스트리밍을 시작하면 프로그레시브 로딩이 발생하지 않습니다. 스트림에는 최종 HTML이 포함됩니다.\n  * **선택 사항** `onError`: [복구 가능](#recovering-from-errors-outside-the-shell) 또는 [불가능](#recovering-from-errors-inside-the-shell)에 관계없이 서버 오류가 발생할 때마다 호출되는 콜백입니다. 기본적으로 `console.error`만 호출합니다. 이 함수를 재정의하여 [크래시 리포트를 기록](#logging-crashes-on-the-server)하는 경우 `console.error`를 계속 호출해야 합니다. 셸이 출력되기 전에 [상태 코드를 조정](#setting-the-status-code)하는 데 사용할 수도 있습니다.\n  * **선택 사항** `onShellReady`: [초기 셸](#specifying-what-goes-into-the-shell)이 렌더링된 직후에 실행되는 콜백입니다. 여기서 [상태 코드를 설정](#setting-the-status-code)하고 `pipe`를 호출하여 스트리밍을 시작할 수 있습니다. React는 HTML 로딩 폴백을 콘텐츠로 대체하는 인라인 `<script>` 태그와 함께 셸 뒤에 [추가 콘텐츠를 스트리밍](#streaming-more-content-as-it-loads)합니다.\n  * **선택 사항** `onShellError`: 초기 셸을 렌더링하는 데 오류가 발생하면 호출되는 콜백입니다. 오류를 인자로 받습니다. 스트림에서 아직 바이트가 전송되지 않았고, `onShellReady`나 `onAllReady`도 호출되지 않으므로 [폴백 HTML 셸을 출력](#recovering-from-errors-inside-the-shell) 할 수 있습니다.\n  * **선택 사항** `progressiveChunkSize`: 청크의 바이트 수입니다. [기본 휴리스틱에 대해 자세히 알아보세요.](https://github.com/facebook/react/blob/14c2be8dac2d5482fda8a0906a31d239df8551fc/packages/react-server/src/ReactFizzServer.js#L210-L225)\n\n\n#### 반환값 {/*returns*/}\n\n`renderToPipeableStream`은 두 개의 메서드가 있는 객체를 반환합니다.\n\n* `pipe`는 HTML을 제공된 [쓰기 가능한 Node.js 스트림](https://nodejs.org/api/stream.html#writable-streams)으로 출력합니다. 스트리밍을 활성화하려면 `onShellReady`에서, 크롤러와 정적 생성을 사용하려면 `onAllReady`에서 `pipe`를 호출하세요.\n* `abort`를 사용하면 [서버 렌더링을 중단](#aborting-server-rendering)하고 나머지는 클라이언트에서 렌더링할 수 있습니다.\n\n---\n\n## 사용법 {/*usage*/}\n\n### React 트리를 HTML로 Node.js 스트림에 렌더링하기 {/*rendering-a-react-tree-as-html-to-a-nodejs-stream*/}\n\n`renderToPipeableStream`을 호출하여 React 트리를 HTML로 [Node.js 스트림](https://nodejs.org/api/stream.html#writable-streams)에 렌더링합니다.\n\n```js [[1, 5, \"<App />\"], [2, 6, \"['/main.js']\"]]\nimport { renderToPipeableStream } from 'react-dom/server';\n\n// 경로 핸들러 문법은 백엔드 프레임워크에 따라 다릅니다.\napp.use('/', (request, response) => {\n  const { pipe } = renderToPipeableStream(<App />, {\n    bootstrapScripts: ['/main.js'],\n    onShellReady() {\n      response.setHeader('content-type', 'text/html');\n      pipe(response);\n    }\n  });\n});\n```\n\n<CodeStep step={1}>루트 컴포넌트</CodeStep>와 함께 <CodeStep step={2}>부트스트랩 `<script>` 경로 목록</CodeStep>을 제공해야 합니다. 루트 컴포넌트는 **루트 `<html>` 태그를 포함한 전체 문서를 반환해야 합니다.**\n\n예를 들어 다음과 같이 표시할 수 있습니다.\n\n```js [[1, 1, \"App\"]]\nexport default function App() {\n  return (\n    <html>\n      <head>\n        <meta charSet=\"utf-8\" />\n        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n        <link rel=\"stylesheet\" href=\"/styles.css\"></link>\n        <title>My app</title>\n      </head>\n      <body>\n        <Router />\n      </body>\n    </html>\n  );\n}\n```\n\nReact는 [doctype](https://developer.mozilla.org/ko/docs/Glossary/Doctype)과 <CodeStep step={2}>부트스트랩 `<script>` 태그</CodeStep>를 결과 HTML 스트림에 삽입합니다.\n\n```html [[2, 5, \"/main.js\"]]\n<!DOCTYPE html>\n<html>\n  <!-- ... 컴포넌트의 HTML ... -->\n</html>\n<script src=\"/main.js\" async=\"\"></script>\n```\n\n클라이언트에서 부트스트랩 스크립트는 [`hydrateRoot`를 호출하여 전체 `document`를 하이드레이트해야 합니다.](/reference/react-dom/client/hydrateRoot#hydrating-an-entire-document)\n\n```js [[1, 4, \"<App />\"]]\nimport { hydrateRoot } from 'react-dom/client';\nimport App from './App.js';\n\nhydrateRoot(document, <App />);\n```\n\n이렇게 하면 서버에서 생성된 HTML에 이벤트 리스너가 첨부되어 상호작용이 가능해집니다.\n\n<DeepDive>\n\n#### 빌드 출력에서 CSS 및 JS 에셋 경로 읽기 {/*reading-css-and-js-asset-paths-from-the-build-output*/}\n\n최종 에셋 URL(예: 자바스크립트 및 CSS 파일)은 빌드 후에 해시 처리되는 경우가 많습니다. 예를 들어 `styles.css` 대신 `styles.123456.css`로 끝날 수 있습니다. 정적 에셋 파일명을 해시하면 동일한 에셋의 모든 별개의 빌드에서 다른 파일명을 가질 수 있습니다. 이는 정적 자산에 대한 장기 캐싱을 안전하게 활성화할 수 있기 때문에 유용합니다. 특정 이름을 가진 파일은 콘텐츠가 변경되지 않습니다.\n\n하지만 빌드가 끝날 때까지 에셋 URL을 모르는 경우 소스 코드에 넣을 방법이 없습니다. 예를 들어, 앞서처럼 `\"/styles.css\"`를 JSX에 하드코딩하면 작동하지 않습니다. 소스 코드에 포함되지 않도록 하려면 루트 컴포넌트가 프로퍼티로 전달된 맵에서 실제 파일명을 읽을 수 있습니다.\n\n```js {1,6}\nexport default function App({ assetMap }) {\n  return (\n    <html>\n      <head>\n        ...\n        <link rel=\"stylesheet\" href={assetMap['styles.css']}></link>\n        ...\n      </head>\n      ...\n    </html>\n  );\n}\n```\n\n서버에서 `<App assetMap={assetMap} />`를 렌더링하고 에셋 URL과 함께 `assetMap`을 전달합니다.\n\n```js {1-5,8,9}\n// 빌드 도구에서 이 JSON을 가져와야 합니다(예: 빌드 출력에서 읽어오기).\nconst assetMap = {\n  'styles.css': '/styles.123456.css',\n  'main.js': '/main.123456.js'\n};\n\napp.use('/', (request, response) => {\n  const { pipe } = renderToPipeableStream(<App assetMap={assetMap} />, {\n    bootstrapScripts: [assetMap['main.js']],\n    onShellReady() {\n      response.setHeader('content-type', 'text/html');\n      pipe(response);\n    }\n  });\n});\n```\n\n이제 서버에서 `<App assetMap={assetMap} />`를 렌더링하고 있으므로 클라이언트에서도 `assetMap`을 사용하여 렌더링해야 하이드레이션 오류를 방지할 수 있습니다. 다음과 같이 `assetMap`을 직렬화하여 클라이언트에 전달할 수 있습니다.\n\n```js {9-10}\n// 빌드 도구에서 이 JSON을 가져와야 합니다.\nconst assetMap = {\n  'styles.css': '/styles.123456.css',\n  'main.js': '/main.123456.js'\n};\n\napp.use('/', (request, response) => {\n  const { pipe } = renderToPipeableStream(<App assetMap={assetMap} />, {\n    // 조심하세요: 이 데이터는 사용자가 생성한 것이 아니므로 stringify()하는 것이 안전합니다.\n    bootstrapScriptContent: `window.assetMap = ${JSON.stringify(assetMap)};`,\n    bootstrapScripts: [assetMap['main.js']],\n    onShellReady() {\n      response.setHeader('content-type', 'text/html');\n      pipe(response);\n    }\n  });\n});\n```\n\n위 예시에서 `bootstrapScriptContent` 옵션은 클라이언트에서 전역 `window.assetMap` 변수를 설정하는 추가 인라인 `<script>` 태그를 추가합니다. 이렇게 하면 클라이언트 코드가 동일한 `assetMap`을 읽을 수 있습니다.\n\n```js {4}\nimport { hydrateRoot } from 'react-dom/client';\nimport App from './App.js';\n\nhydrateRoot(document, <App assetMap={window.assetMap} />);\n```\n\n클라이언트와 서버 모두 동일한 `assetMap` 프로퍼티로 `App`을 렌더링하므로 하이드레이션 오류가 발생하지 않습니다.\n\n</DeepDive>\n\n---\n\n### 콘텐츠가 로드되는 동안 더 많은 콘텐츠 스트리밍하기 {/*streaming-more-content-as-it-loads*/}\n\n스트리밍을 사용하면 모든 데이터가 서버에 로드되기 전에도 사용자가 콘텐츠를 볼 수 있습니다. 예를 들어 표지와 친구 및 사진이 있는 사이드바, 글 목록이 표시되는 프로필 페이지를 생각해 보세요.\n\n```js\nfunction ProfilePage() {\n  return (\n    <ProfileLayout>\n      <ProfileCover />\n      <Sidebar>\n        <Friends />\n        <Photos />\n      </Sidebar>\n      <Posts />\n    </ProfileLayout>\n  );\n}\n```\n\n`<Posts />`에 대한 데이터를 로드하는 데 시간이 걸린다고 가정해 보겠습니다. 이상적으로는 게시물을 기다리지 않고 나머지 프로필 페이지 콘텐츠를 사용자에게 표시하고 싶을 것입니다. 이렇게 하려면, [`<Posts>`를 `<Suspense>` 경계로 감싸면 됩니다](/reference/react/Suspense#displaying-a-fallback-while-content-is-loading).\n\n```js {9,11}\nfunction ProfilePage() {\n  return (\n    <ProfileLayout>\n      <ProfileCover />\n      <Sidebar>\n        <Friends />\n        <Photos />\n      </Sidebar>\n      <Suspense fallback={<PostsGlimmer />}>\n        <Posts />\n      </Suspense>\n    </ProfileLayout>\n  );\n}\n```\n\n이것은 `Posts`가 데이터를 로드하기 전에 React가 HTML 스트리밍을 시작하도록 지시합니다. React는 로딩 폴백(`PostsGlimmer`)을 위한 HTML을 먼저 전송한 다음, `Posts`가 데이터 로딩을 완료하면 나머지 HTML을 인라인 `<script>` 태그와 함께 전송하여 로딩 폴백을 해당 HTML로 대체할 것입니다. 사용자 입장에서는 페이지가 먼저 `PostsGlimmer`로 표시되고 나중에 `Posts`로 대체됩니다.\n\n[`<Suspense>` 경계를 더 중첩](/reference/react/Suspense#revealing-nested-content-as-it-loads)하여 보다 세분화된 로딩 시퀀스를 만들 수 있습니다.\n\n```js {5,13}\nfunction ProfilePage() {\n  return (\n    <ProfileLayout>\n      <ProfileCover />\n      <Suspense fallback={<BigSpinner />}>\n        <Sidebar>\n          <Friends />\n          <Photos />\n        </Sidebar>\n        <Suspense fallback={<PostsGlimmer />}>\n          <Posts />\n        </Suspense>\n      </Suspense>\n    </ProfileLayout>\n  );\n}\n```\n\n이 예시에서 React는 페이지 스트리밍을 더 일찍 시작할 수 있습니다. `ProfileLayout`과 `ProfileCover`만 `<Suspense>` 경계로 둘러싸여 있지 않기 때문에 먼저 렌더링을 완료해야 합니다. 하지만 `Sidebar`, `Friends`, `Photos`가 일부 데이터를 로드해야 하는 경우, React는 대신 `BigSpinner` 폴백을 위한 HTML을 전송합니다. 그러면 더 많은 데이터를 사용할 수 있게 되면 모든 데이터가 표시될 때까지 더 많은 콘텐츠가 계속 표시됩니다.\n\n스트리밍은 브라우저에서 React 자체가 로드되거나 앱이 상호작용 가능해질 때까지 기다릴 필요가 없습니다. 서버의 HTML 콘텐츠는 `<script>` 태그가 로드되기 전에 점진적으로 표시됩니다.\n\n[스트리밍 HTML의 작동 방식에 대해 자세히 알아보세요.](https://github.com/reactwg/react-18/discussions/37)\n\n<Note>\n\n**Suspense를 지원하는 데이터 소스만 Suspense 컴포넌트를 활성화합니다.** 이는 다음과 같습니다.\n\n- [Relay](https://relay.dev/docs/guided-tour/rendering/loading-states/)와 [Next.js](https://nextjs.org/docs/getting-started/react-essentials) 같은 Suspense가 가능한 프레임워크를 사용한 데이터 가져오기.\n- [`lazy`](/reference/react/lazy)를 활용한 지연 로딩 컴포넌트.\n- [`use`](/reference/react/use)를 사용해서 Promise 값 읽기.\n\nSuspense는 Effect 또는 이벤트 핸들러 내부에서 데이터를 가져올 경우, **이를 감지하지 못합니다.**\n\n`Posts` 컴포넌트에서 데이터를 불러오는 정확한 방법은 앞서 설명한 프레임워크에 따라 다릅니다. Suspense를 지원하는 프레임워크를 사용하는 경우, 데이터를 가져오는 자세한 방법은 해당 프레임워크 문서에서 찾을 수 있습니다.\n\n독자적인 프레임워크를 사용하지 않는 Suspense 지원 데이터 가져오기는 아직 지원하지 않습니다. Suspense를 지원하는 데이터 소스를 구현하기 위한 요구 사항은 불안정하고 문서화되지 않았습니다. 데이터 소스를 Suspense와 통합하기 위한 공식 API는 React의 향후 버전에서 출시할 예정입니다.\n\n</Note>\n\n---\n\n### 셸에 들어갈 내용 지정하기 {/*specifying-what-goes-into-the-shell*/}\n\n앱의 `<Suspense>` 경계 밖에 있는 부분을 *셸*이라고 합니다.\n\n```js {3-5,13,14}\nfunction ProfilePage() {\n  return (\n    <ProfileLayout>\n      <ProfileCover />\n      <Suspense fallback={<BigSpinner />}>\n        <Sidebar>\n          <Friends />\n          <Photos />\n        </Sidebar>\n        <Suspense fallback={<PostsGlimmer />}>\n          <Posts />\n        </Suspense>\n      </Suspense>\n    </ProfileLayout>\n  );\n}\n```\n\n사용자가 볼 수 있는 가장 빠른 로딩 상태를 결정합니다.\n\n```js {3-5,13\n<ProfileLayout>\n  <ProfileCover />\n  <BigSpinner />\n</ProfileLayout>\n```\n\n전체 앱을 루트의 `<Suspense>` 경계로 감싸면 셸에는 해당 스피너만 포함됩니다. 하지만 화면에 큰 스피너가 표시되면 조금 더 기다렸다가 실제 레이아웃을 보는 것보다 느리고 성가시게 느껴질 수 있으므로 사용자 경험이 좋지 않습니다. 그렇기 때문에 일반적으로 셸이 전체 페이지 레이아웃의 스켈레톤처럼 *최소한의 완전함*을 느낄 수 있도록 `<Suspense>` 경계를 배치하는 것이 좋습니다.\n\n전체 셸이 렌더링되면 `onShellReady` 콜백이 실행됩니다. 보통 이때 스트리밍이 시작됩니다.\n\n```js {3-6}\nconst { pipe } = renderToPipeableStream(<App />, {\n  bootstrapScripts: ['/main.js'],\n  onShellReady() {\n    response.setHeader('content-type', 'text/html');\n    pipe(response);\n  }\n});\n```\n\n`onShellReady`가 실행될 때 중첩된 `<Suspense>` 경계에 있는 컴포넌트는 여전히 데이터를 로드하고 있을 수 있습니다.\n\n---\n\n### 서버에서의 충돌을 기록하기 {/*logging-crashes-on-the-server*/}\n\n기본적으로 서버의 모든 오류는 콘솔에 기록<sup>Logging</sup>됩니다. 이 동작을 재정의하여 충돌<sup>Crash</sup> 보고서를 기록할 수 있습니다.\n\n```js {7-10}\nconst { pipe } = renderToPipeableStream(<App />, {\n  bootstrapScripts: ['/main.js'],\n  onShellReady() {\n    response.setHeader('content-type', 'text/html');\n    pipe(response);\n  },\n  onError(error) {\n    console.error(error);\n    logServerCrashReport(error);\n  }\n});\n```\n\n사용자 정의 `onError` 구현을 제공하는 경우 위와 같이 콘솔에 오류를 기록하는 것도 잊지 마세요.\n\n---\n\n### 셸 내부의 오류로부터 복구하기 {/*recovering-from-errors-inside-the-shell*/}\n\n이 예시에서는 셸에 `ProfileLayout`, `ProfileCover`, `PostsGlimmer`가 포함되어 있습니다.\n\n```js {3-5,7-8}\nfunction ProfilePage() {\n  return (\n    <ProfileLayout>\n      <ProfileCover />\n      <Suspense fallback={<PostsGlimmer />}>\n        <Posts />\n      </Suspense>\n    </ProfileLayout>\n  );\n}\n```\n\n이러한 컴포넌트를 렌더링하는 동안 오류가 발생하면 React는 클라이언트에 보낼 의미 있는 HTML을 갖지 못합니다. 마지막 수단으로 서버 렌더링에 의존하지 않는 폴백 HTML을 보내려면 `onShellError`를 재정의하세요.\n\n```js {7-11}\nconst { pipe } = renderToPipeableStream(<App />, {\n  bootstrapScripts: ['/main.js'],\n  onShellReady() {\n    response.setHeader('content-type', 'text/html');\n    pipe(response);\n  },\n  onShellError(error) {\n    response.statusCode = 500;\n    response.setHeader('content-type', 'text/html');\n    response.send('<h1>Something went wrong</h1>');\n  },\n  onError(error) {\n    console.error(error);\n    logServerCrashReport(error);\n  }\n});\n```\n\n셸을 생성하는 동안 오류가 발생하면 `onError`와 `onShellError`가 모두 실행됩니다. 오류 보고에는 `onError`를 사용하고, 대체 HTML 문서를 보내려면 `onShellError`를 사용합니다. 폴백 HTML이 오류 페이지일 필요는 없습니다. 대신 클라이언트에서만 앱을 렌더링하는 대체 셸을 포함할 수 있습니다.\n\n---\n\n### 셸 외부의 오류로부터 복구하기 {/*recovering-from-errors-outside-the-shell*/}\n\n이 예시에서는 `<Posts />` 컴포넌트가 `<Suspense>`로 래핑되어 있으므로 셸의 일부가 *아닙니다.*\n\n```js {6}\nfunction ProfilePage() {\n  return (\n    <ProfileLayout>\n      <ProfileCover />\n      <Suspense fallback={<PostsGlimmer />}>\n        <Posts />\n      </Suspense>\n    </ProfileLayout>\n  );\n}\n```\n\n`Posts` 컴포넌트 또는 그 내부 어딘가에서 오류가 발생하면 React는 [이를 복구하려고 시도합니다.](/reference/react/Suspense#providing-a-fallback-for-server-errors-and-client-only-content)\n\n1. 가장 가까운 `<Suspense>` 경계(`PostsGlimmer`)에 대한 로딩 폴백을 HTML로 방출합니다.\n2. 더 이상 서버에서 `Posts` 콘텐츠를 렌더링하는 것을 \"포기\"합니다.\n3. 자바스크립트 코드가 클라이언트에서 로드되면 React는 클라이언트에서 `Posts` 렌더링을 *재시도*합니다.\n\n클라이언트에서 `Posts` 렌더링을 *다시 시도해도* 실패하면 React는 클라이언트에서 오류를 던집니다. 렌더링 중에 발생하는 모든 오류와 마찬가지로, [가장 가까운 부모 오류 경계](/reference/react/Component#static-getderivedstatefromerror)에 따라 사용자에게 오류를 표시하는 방법이 결정됩니다. 실제로는 오류를 복구할 수 없다는 것이 확실해질 때까지 사용자에게 로딩 표시기가 표시된다는 의미입니다.\n\n클라이언트에서 `Posts` 렌더링을 다시 시도하여 성공하면 서버의 로딩 폴백이 클라이언트 렌더링 출력으로 대체됩니다. 사용자는 서버 오류가 발생했다는 사실을 알 수 없습니다. 그러나 서버 `onError` 콜백 및 클라이언트 [`onRecoverableError`](/reference/react-dom/client/hydrateRoot#hydrateroot) 콜백이 실행되어 오류에 대한 알림을 받을 수 있습니다.\n\n---\n\n### 상태 코드 설정하기 {/*setting-the-status-code*/}\n\n스트리밍에는 장단점이 있습니다. 사용자가 콘텐츠를 더 빨리 볼 수 있도록 가능한 한 빨리 페이지 스트리밍을 시작하고 싶을 수 있습니다. 그러나 스트리밍을 시작하면 더 이상 응답 상태 코드를 설정할 수 없습니다.\n\n앱을 셸(특히 `<Suspense>` 경계 바깥)과 나머지 콘텐츠로 [나누면](#specifying-what-goes-into-the-shell) 이 문제의 일부를 이미 해결한 것입니다. 셸에 오류가 발생하면 오류 상태 코드를 설정할 수 있는 `onShellError` 콜백을 받게 됩니다. 그렇지 않으면 앱이 클라이언트에서 복구될 수 있으므로 \"OK\"를 보낼 수 있습니다.\n\n```js {4}\nconst { pipe } = renderToPipeableStream(<App />, {\n  bootstrapScripts: ['/main.js'],\n  onShellReady() {\n    response.statusCode = 200;\n    response.setHeader('content-type', 'text/html');\n    pipe(response);\n  },\n  onShellError(error) {\n    response.statusCode = 500;\n    response.setHeader('content-type', 'text/html');\n    response.send('<h1>Something went wrong</h1>');\n  },\n  onError(error) {\n    console.error(error);\n    logServerCrashReport(error);\n  }\n});\n```\n\n셸 *외부*(즉, `<Suspense>` 경계 안쪽)에 있는 컴포넌트가 오류를 던져도 React는 렌더링을 멈추지 않습니다. 즉, `onError` 콜백이 실행되지만 `onShellError` 대신 `onShellReady`가 반환됩니다. 이는 [위에서 설명한 것처럼](#recovering-from-errors-outside-the-shell) React가 클라이언트에서 해당 오류를 복구하려고 시도하기 때문입니다.\n\n그러나 원하는 경우 오류가 발생했다는 사실을 사용하여 상태 코드를 설정할 수 있습니다.\n\n```js {1,6,16}\nlet didError = false;\n\nconst { pipe } = renderToPipeableStream(<App />, {\n  bootstrapScripts: ['/main.js'],\n  onShellReady() {\n    response.statusCode = didError ? 500 : 200;\n    response.setHeader('content-type', 'text/html');\n    pipe(response);\n  },\n  onShellError(error) {\n    response.statusCode = 500;\n    response.setHeader('content-type', 'text/html');\n    response.send('<h1>Something went wrong</h1>');\n  },\n  onError(error) {\n    didError = true;\n    console.error(error);\n    logServerCrashReport(error);\n  }\n});\n```\n\n이는 초기 셸 콘텐츠를 생성하는 동안 발생한 셸 외부의 오류만 포착하므로 완전한 것은 아닙니다. 일부 콘텐츠에서 오류가 발생했는지 여부를 파악하는 것이 중요한 경우 해당 콘텐츠를 셸로 이동하면 됩니다.\n\n---\n\n### 다양한 오류를 서로 다른 방식으로 처리하기 {/*handling-different-errors-in-different-ways*/}\n\n[자신만의 `Error` 서브 클래스를 생성](https://ko.javascript.info/custom-errors)하고 [`instanceof`](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Operators/instanceof) 연산자를 사용해 어떤 오류가 발생하는지 확인할 수 있습니다. 예를 들어, 사용자 정의 `NotFoundError`를 정의하고 컴포넌트에서 이를 던질 수 있습니다. 그러면 오류 유형에 따라 `onError`, `onShellReady`, `onShellError` 콜백이 다른 작업을 수행할 수 있습니다.\n\n```js {2,4-14,19,24,30}\nlet didError = false;\nlet caughtError = null;\n\nfunction getStatusCode() {\n  if (didError) {\n    if (caughtError instanceof NotFoundError) {\n      return 404;\n    } else {\n      return 500;\n    }\n  } else {\n    return 200;\n  }\n}\n\nconst { pipe } = renderToPipeableStream(<App />, {\n  bootstrapScripts: ['/main.js'],\n  onShellReady() {\n    response.statusCode = getStatusCode();\n    response.setHeader('content-type', 'text/html');\n    pipe(response);\n  },\n  onShellError(error) {\n   response.statusCode = getStatusCode();\n   response.setHeader('content-type', 'text/html');\n   response.send('<h1>Something went wrong</h1>');\n  },\n  onError(error) {\n    didError = true;\n    caughtError = error;\n    console.error(error);\n    logServerCrashReport(error);\n  }\n});\n```\n\n셸을 내보내고 스트리밍을 시작하면 상태 코드를 변경할 수 없다는 점에 유의하세요.\n\n---\n\n### 크롤러 및 정적 생성을 위해 모든 콘텐츠가 로드될 때까지 기다리기 {/*waiting-for-all-content-to-load-for-crawlers-and-static-generation*/}\n\n스트리밍은 콘텐츠가 제공될 때 바로 볼 수 있기 때문에 더 나은 사용자 경험을 제공합니다.\n\n그러나 크롤러가 페이지를 방문하거나 빌드 시점에 페이지를 생성하는 경우 모든 콘텐츠를 점진적으로 표시하는 대신 모든 콘텐츠를 먼저 로드한 다음 최종 HTML 출력을 생성하는 것이 좋을 수 있습니다.\n\n`onAllReady` 콜백을 사용하여 모든 콘텐츠가 로드될 때까지 기다릴 수 있습니다.\n\n```js {2,7,11,18-24}\nlet didError = false;\nlet isCrawler = // ... 봇 탐지 전략에 따라 달라집니다 ...\n\nconst { pipe } = renderToPipeableStream(<App />, {\n  bootstrapScripts: ['/main.js'],\n  onShellReady() {\n    if (!isCrawler) {\n      response.statusCode = didError ? 500 : 200;\n      response.setHeader('content-type', 'text/html');\n      pipe(response);\n    }\n  },\n  onShellError(error) {\n    response.statusCode = 500;\n    response.setHeader('content-type', 'text/html');\n    response.send('<h1>Something went wrong</h1>');\n  },\n  onAllReady() {\n    if (isCrawler) {\n      response.statusCode = didError ? 500 : 200;\n      response.setHeader('content-type', 'text/html');\n      pipe(response);\n    }\n  },\n  onError(error) {\n    didError = true;\n    console.error(error);\n    logServerCrashReport(error);\n  }\n});\n```\n\n일반 방문자는 점진적으로 로드되는 콘텐츠 스트림을 받게 됩니다. 크롤러는 모든 데이터가 로드된 후 최종 HTML 출력을 받게 됩니다. 그러나 이는 크롤러가 *모든* 데이터를 기다려야 한다는 것을 의미하며, 그중 일부는 로드 속도가 느리거나 오류가 발생할 수 있습니다. 앱에 따라 크롤러에도 셸을 보내도록 선택할 수 있습니다.\n\n---\n\n### 서버 렌더링 중단하기 {/*aborting-server-rendering*/}\n\n시간 초과 후 서버 렌더링을 강제로 '포기'할 수 있습니다.\n\n```js {1,5-7}\nconst { pipe, abort } = renderToPipeableStream(<App />, {\n  // ...\n});\n\nsetTimeout(() => {\n  abort();\n}, 10000);\n```\n\nReact는 나머지 로딩 폴백을 HTML로 플러시하고 나머지는 클라이언트에서 렌더링을 시도합니다.\n"
  },
  {
    "path": "src/content/reference/react-dom/server/renderToReadableStream.md",
    "content": "---\ntitle: renderToReadableStream\n---\n\n<Intro>\n\n`renderToReadableStream`는 [Readable Web Stream](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream)을 이용해 React 트리를 그립니다.\n\n```js\nconst stream = await renderToReadableStream(reactNode, options?)\n```\n\n</Intro>\n\n<InlineToc />\n\n<Note>\n\n이 API는 [Web Stream](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API)에 의존합니다. Node.js의 경우, [`renderToPipeableStream`](/reference/react-dom/server/renderToPipeableStream)을 대신 사용하세요.\n\n</Note>\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `renderToReadableStream(reactNode, options?)` {/*rendertoreadablestream*/}\n\n`renderToReadableStream`을 호출하여 React 트리를 HTML로 [Readable Web Stream](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream)에 렌더링합니다.\n\n```js\nimport { renderToReadableStream } from 'react-dom/server';\n\nasync function handler(request) {\n  const stream = await renderToReadableStream(<App />, {\n    bootstrapScripts: ['/main.js']\n  });\n  return new Response(stream, {\n    headers: { 'content-type': 'text/html' },\n  });\n}\n```\n\n클라이언트에서, [`hydrateRoot`](/reference/react-dom/client/hydrateRoot)를 호출해 서버에서 생성된 HTML을 상호작용 가능하도록 만듭니다.\n\n[아래에서 더 많은 예시를 확인하세요.](#usage)\n\n#### 매개변수 {/*parameters*/}\n\n* `reactNode`: 사용자가 HTML로 렌더링하고 하고자하는 React node입니다. `<App />`같은 JSX 요소가 그 예시입니다. reactNode 인자는 문서 전체를 표현할 수 있는 것이어야하며, 따라서 `App` 컴포넌트는 `<html>`에 렌더링됩니다.\n\n* **optional** `options`: 스트리밍 옵션을 지정할 수 있는 객체입니다.\n  * **optional** `bootstrapScriptContent`: 지정될 경우, 해당 문자열은 `<script>` 태그에 인라인 형식으로 추가됩니다.\n  * **optional** `bootstrapScripts`: 문자열 배열 형식의 단수 혹은 복수의 URL로 페이지에 함께 작성될 `<script>` 태그에 사용됩니다. [`hydrateRoot`](/reference/react-dom/client/hydrateRoot)를 호출할 떄, `<script>` 태그를 포함 시키기 위해 사용합니다. 클라이언트에서 React가 실행되길 원하지 않는다면, 제외시켜주세요.\n  * **optional** `bootstrapModules`: `bootstrapScripts`와 비슷합니다, 하지만 [`<script type=\"module\">`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules)형식으로 추가됩니다.\n  * **optional** `identifierPrefix`: React가 ID로서 사용할 문자열 앞머리로 [`useId`](/reference/react/useId)로 생성된 문자열입니다. 같은 페이지에서 여러 root를 사용할 때, 각 root간의 충돌을 방지하기 위해 유용합니다. [`hydrateRoot`](/reference/react-dom/client/hydrateRoot#parameters)에 전달한 앞머리와 반드시 동일해야합니다.\n  * **optional** `namespaceURI`: 문자열로 스트림을 위한 기준 [namespace URI](https://developer.mozilla.org/en-US/docs/Web/API/Document/createElementNS#important_namespace_uris)입니다. 일반 HTML에 해당하는 기본값이 설정되어있습니다. SVG를 위해 `'http://www.w3.org/2000/svg'`를 설정하거나 MathML을 위해 `'http://www.w3.org/1998/Math/MathML'`을 설정할 수 있습니다.\n  * **optional** `nonce`: [`script-src` Content-Security-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src)를 허용하기 위한 [`nonce`](http://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#nonce) (한번만 사용되는) 문자열입니다.\n  * **optional** `onError`: [회복할 수 있든](#recovering-from-errors-outside-the-shell) 있든 [없든] 상관없이, 서버에서 오류가 발생할 때마다 호출되는 콜백입니다. 기본적으로, 이 콜백은 `console.error`만 호출합니다. [크래시 리포트를 로그하기](#logging-crashes-on-the-server) 위해 오버라이드하거나, [상태 코드를 조정하기](#setting-the-status-code) 위해 오버라이드할 수 있습니다.\n  * **optional** `progressiveChunkSize`: 청크의 바이트 수를 설정합니다. [기본 휴리스틱에 대해 더 읽어보기.](https://github.com/facebook/react/blob/14c2be8dac2d5482fda8a0906a31d239df8551fc/packages/react-server/src/ReactFizzServer.js#L210-L225)\n  * **optional** `signal`: [서버 렌더링을 취소](#aborting-server-rendering)하고, 그 나머지를 클라이언트에 렌더링하기 위한 [거절 신호(abort signal)](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal)를 설정합니다.\n\n#### 반환값 {/*returns*/}\n\n`renderToReadableStream`는 [Promise](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Promise)를 반환합니다.\n\n- [shell](#specifying-what-goes-into-the-shell) 렌더링에 성공했다면, 반환된 Promise는 [Readable Web Stream](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream)으로 해결됩니다.\n- [shell](#specifying-what-goes-into-the-shell) 렌더링에 실패하면, 반환된 Promise는 취소됩니다. [shell 렌더링에 실패시, 이것을 이용해 실패 결과를 출력하세요.](#recovering-from-errors-inside-the-shell)\n\n반환된 스트림은 다음과 같은 추가적인 프로퍼티를 가지고 있습니다.\n\n* `allReady`: 모든 추가 [컨텐츠](#streaming-more-content-as-it-loads)와 [shell](#specifying-what-goes-into-the-shell)의 렌더링을 포함한 모든 렌더링이 완료된 Promise의 추가 프로퍼티입니다. [크롤러와 정적 생성을 위해](#waiting-for-all-content-to-load-for-crawlers-and-static-generation) `await stream.allReady`를 응답 반환 전에 사용할 수 있습니다. 설정 시엔, 로딩 진행 상태를 받을 수 없습니다. 스트림은 최종 HTML을 포함할 것입니다.\n\n---\n\n## 사용 예시 {/*usage*/}\n\n### Readable Web Stream을 이용해 React 트리를 HTML처럼 렌더링하기 {/*rendering-a-react-tree-as-html-to-a-readable-web-stream*/}\n\n`renderToReadableStream`을 호출해 [Readable Web Stream](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream)을 통해 React 트리를 HTML로 렌더링합니다.\n\n```js [[1, 4, \"<App />\"], [2, 5, \"['/main.js']\"]]\nimport { renderToReadableStream } from 'react-dom/server';\n\nasync function handler(request) {\n  const stream = await renderToReadableStream(<App />, {\n    bootstrapScripts: ['/main.js']\n  });\n  return new Response(stream, {\n    headers: { 'content-type': 'text/html' },\n  });\n}\n```\n\n<CodeStep step={1}>root 컴포넌트</CodeStep>와 함께, <CodeStep step={2}>bootstrap `<script>` 경로</CodeStep> 리스트를 제공해야합니다. 제공된 root 컴포넌트는 **최상위 `<html>` 태그를 포함한 모든 문서를 포함해서** 반환되어야 합니다.\n\n예를 들어, 다음과 같은 형태가 되어야 합니다.\n\n```js [[1, 1, \"App\"]]\nexport default function App() {\n  return (\n    <html>\n      <head>\n        <meta charSet=\"utf-8\" />\n        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n        <link rel=\"stylesheet\" href=\"/styles.css\"></link>\n        <title>My app</title>\n      </head>\n      <body>\n        <Router />\n      </body>\n    </html>\n  );\n}\n```\n\nReact는 [doctype](https://developer.mozilla.org/en-US/docs/Glossary/Doctype)과 <CodeStep step={2}>bootstrap `<script>` 태그들</CodeStep>을 결과 HTML 스트림에 주입합니다.\n\n```html [[2, 5, \"/main.js\"]]\n<!DOCTYPE html>\n<html>\n  <!-- ... 사용자가 직접 작성한 컴포넌트의 HTML ... -->\n</html>\n<script src=\"/main.js\" async=\"\"></script>\n```\n\n클라이언트에선, 추가된 bootstrap 스크립트는 [`hydrateRoot`를 호출해 `document` 전체를 hydrate 해야합니다](/reference/react-dom/client/hydrateRoot#hydrating-an-entire-document).\n\n```js [[1, 4, \"<App />\"]]\nimport { hydrateRoot } from 'react-dom/client';\nimport App from './App.js';\n\nhydrateRoot(document, <App />);\n```\n\n이 과정은 서버에서 렌더링된 HTML에 이벤트 리스너들을 붙이고, HTML을 상호작용 가능하게 만듭니다.\n\n<DeepDive>\n\n#### 빌드 결과물에서 CSS와 JS의 경로 읽어오기 {/*reading-css-and-js-asset-paths-from-the-build-output*/}\n\nJS와 CSS같은 최종 에셋들에 대한 URL들은 종종 빌드 후에 해시됩니다. 예를 들어, `styles.css` 대신 `styles.123456.css`와 같은 형태로 끝날 수 있습니다. 에셋들의 파일명을 해시하는 것은 모든 빌드의 결과물이 각각 다른 파일명을 가지도록 보장합니다. 이는 정적 에셋들에 대한 장기 캐싱을 안전하게 활성화할 수 있도록 해줍니다. 즉, 특정 이름의 파일 내용은 절대 바뀌지 않는 다는 것을 보장합니다.\n\n하지만, 빌드 후에 에셋들의 URL을 알 수 없다면, 소스 코드에 URL을 넣을 수 없습니다. 예를 들어, JSX에 `\"/styles.css\"`를 하드코딩하는 것은 작동하지 않습니다. 소스 코드에 URL을 넣지 않으려면, 루트 컴포넌트는 props로 전달된 맵에서 실제 파일명을 읽어야합니다.\n\n```js {1,6}\nexport default function App({ assetMap }) {\n  return (\n    <html>\n      <head>\n        <title>My app</title>\n        <link rel=\"stylesheet\" href={assetMap['styles.css']}></link>\n      </head>\n      ...\n    </html>\n  );\n}\n```\n\n서버에선 `<App assetMap={assetMap} />`을 렌더링하고, 에셋 URL들과 함께 `assetMap`을 전달합니다.\n\n```js {1-5,8,9}\n// 빌드 도구로부터 이 JSON을 얻어야합니다. 예를 들어, 빌드 결과물에서 읽어올 수 있습니다.\nconst assetMap = {\n  'styles.css': '/styles.123456.css',\n  'main.js': '/main.123456.js'\n};\n\nasync function handler(request) {\n  const stream = await renderToReadableStream(<App assetMap={assetMap} />, {\n    bootstrapScripts: [assetMap['/main.js']]\n  });\n  return new Response(stream, {\n    headers: { 'content-type': 'text/html' },\n  });\n}\n```\n\n서버가 `<App assetMap={assetMap} />`를 렌더링한 이후엔, 클라이언트에서도 hydration 오류를 피하기 위해 `assetMap`과 함께 렌더링해야합니다. `assetMap`을 직렬화하고 클라이언트에 전달하기 위해 다음과 같이 할 수 있습니다.\n\n```js {9-10}\n// 빌드 도구로부터 이 JSON을 얻어야합니다.\nconst assetMap = {\n  'styles.css': '/styles.123456.css',\n  'main.js': '/main.123456.js'\n};\n\nasync function handler(request) {\n  const stream = await renderToReadableStream(<App assetMap={assetMap} />, {\n    // 주의: 이 데이터는 사용자가 생성하지 않았기 때문에 stringify()를 사용해도 안전합니다.\n    bootstrapScriptContent: `window.assetMap = ${JSON.stringify(assetMap)};`,\n    bootstrapScripts: [assetMap['/main.js']],\n  });\n  return new Response(stream, {\n    headers: { 'content-type': 'text/html' },\n  });\n}\n```\n\n위의 예시에서, `bootstrapScriptContent` 옵션은 클라이언트에서 `window.assetMap` 전역 변수를 설정하는 인라인 `<script>` 태그를 추가합니다. 이는 클라이언트 코드가 동일한 `assetMap`을 읽을 수 있게 해줍니다.\n\n```js {4}\nimport { hydrateRoot } from 'react-dom/client';\nimport App from './App.js';\n\nhydrateRoot(document, <App assetMap={window.assetMap} />);\n```\n\n클라이언트와 서버는 모두 같은 `assetMap` prop을 이용해 `App`을 렌더링하므로, hydration 오류가 일어나지 않습니다.\n\n</DeepDive>\n\n---\n\n### 더 많은 컨텐츠를 스트리밍하면서 로드하기 {/*streaming-more-content-as-it-loads*/}\n\n스트리밍은 사용자가 모든 데이터를 서버로부터 로드해오기 전에 컨텐츠를 볼 수 있도록 합니다. 예를 들어, 프로필 커버사진, 친구들과 사진들이 있는 사이드바 그리고 포스트 목록을 보여주는 프로필 페이지를 생각해봅시다:\n\n```js\nfunction ProfilePage() {\n  return (\n    <ProfileLayout>\n      <ProfileCover />\n      <Sidebar>\n        <Friends />\n        <Photos />\n      </Sidebar>\n      <Posts />\n    </ProfileLayout>\n  );\n}\n```\n\n`<Posts />`의 데이터를 불러오는데 약간의 시간이 필요하다고 가정해봅시다. 이 경우, 사용자가 포스트 목록을 기다리지 않고도 프로필 페이지의 나머지 컨텐츠를 볼 수 있도록 하고 싶을 것입니다. 이를 위해, [`<Suspense>`를 사용해 `Posts`를 감싸주세요](/reference/react/Suspense#displaying-a-fallback-while-content-is-loading).\n\n```js {9,11}\nfunction ProfilePage() {\n  return (\n    <ProfileLayout>\n      <ProfileCover />\n      <Sidebar>\n        <Friends />\n        <Photos />\n      </Sidebar>\n      <Suspense fallback={<PostsGlimmer />}>\n        <Posts />\n      </Suspense>\n    </ProfileLayout>\n  );\n}\n```\n\n이렇게 하면 React는 `Posts`가 모든 데이터를 불러오기 전까지, HTML 스트리밍을 시작합니다. React는 먼저 로딩 대체 컨텐츠인 `<PostsGlimmer />`를 HTML로 보내고, `Posts`의 데이터 로딩이 완료되면, `<PostsGlimmer />`를 `<Posts />`로 교체할 HTML과 인라인 `<script>` 태그를 함께 보냅니다. 사용자 입장에선, 먼저 `<PostsGlimmer />`를 보고, 후에 `<Posts />`를 보게 됩니다.\n\n더 정밀한 로딩 순서를 만들기 위해 [`<Suspense>` 경계를 중첩](/reference/react/Suspense#revealing-nested-content-as-it-loads)할 수 있습니다.\n\n```js {5,13}\nfunction ProfilePage() {\n  return (\n    <ProfileLayout>\n      <ProfileCover />\n      <Suspense fallback={<BigSpinner />}>\n        <Sidebar>\n          <Friends />\n          <Photos />\n        </Sidebar>\n        <Suspense fallback={<PostsGlimmer />}>\n          <Posts />\n        </Suspense>\n      </Suspense>\n    </ProfileLayout>\n  );\n}\n```\n\n\n이 예시를 보았을 때, React가 더 빠르게 스트리밍을 시작하게 할 수 있습니다. `<ProfileLayout>`과 `<ProfileCover>`는 어떤 `<Suspense>` 경계에도 감싸져있지 않기 때문에, React는 먼저 이 두 컴포넌트를 렌더링합니다. 하지만, `Sidebar`나 `Friends` 혹은 `Photos`가 데이터를 불러올 필요가 있는 경우엔, `BigSpinner`를 대체 HTML로 보냅니다. 그 후, 데이터가 더 불러와지면, 더 많은 컨텐츠가 보여지게 되고 이 과정은 모든 컨텐츠가 보여질 때까지 반복됩니다.\n\n스트리밍은 브라우저에서 React 자체가 로드되거나 앱이 상호 작용 가능해질 때까지 기다릴 필요가 없습니다. 서버로부터 로딩되는 HTML 콘텐츠는 `<script>` 태그 중 하나가 로드되기 전까지 점진적으로 표시될 것입니다.\n\n[스트리밍 HTML이 어떻게 동작하는지 더 읽어보기.](https://github.com/reactwg/react-18/discussions/37)\n\n<Note>\n\n**Suspense를 지원하는 데이터 소스만 Suspense 컴포넌트를 활성화합니다.** 이는 다음과 같습니다.\n\n- [Relay](https://relay.dev/docs/guided-tour/rendering/loading-states/)와 [Next.js](https://nextjs.org/docs/getting-started/react-essentials) 같은 Suspense가 가능한 프레임워크를 사용한 데이터 가져오기.\n- [`lazy`](/reference/react/lazy)를 활용한 지연 로딩 컴포넌트.\n- [`use`](/reference/react/use)를 사용해서 Promise 값 읽기.\n\nSuspense는 Effect 또는 이벤트 핸들러 내부에서 데이터를 가져올 경우, **이를 감지하지 못합니다.**\n\n`Posts` 컴포넌트에서 데이터를 불러오는 정확한 방법은 앞서 설명한 프레임워크에 따라 다릅니다. Suspense를 지원하는 프레임워크를 이용하는 경우, 데이터를 가져오는 자세한 방법은 해당 프레임워크 문서에서 찾을 수 있습니다.\n\n독자적인 프레임워크를 사용하지 않는 Suspense 지원 데이터 가져오기는 아직 지원하지 않습니다. Suspense를 지원하는 데이터 소스를 구현하기 위한 요구 사항은 불안정하고 문서화되지 않았습니다. 데이터 소스를 Suspense와 통합하기 위한 공식 API는 React의 향후 버전에서 출시할 예정입니다.\n\n</Note>\n\n---\n\n### Specifying what goes into the shell {/*specifying-what-goes-into-the-shell*/}\n\n앱에서 `<Suspense>` 경계 밖에 있는 부분을 *shell*이라고 합니다.\n\n\n```js {3-5,13,14}\nfunction ProfilePage() {\n  return (\n    <ProfileLayout>\n      <ProfileCover />\n      <Suspense fallback={<BigSpinner />}>\n        <Sidebar>\n          <Friends />\n          <Photos />\n        </Sidebar>\n        <Suspense fallback={<PostsGlimmer />}>\n          <Posts />\n        </Suspense>\n      </Suspense>\n    </ProfileLayout>\n  );\n}\n```\n\n이는 사용자가 보는 최초의 로딩 상태를 정해줍니다.\n\n```js {3-5,13}\n<ProfileLayout>\n  <ProfileCover />\n  <BigSpinner />\n</ProfileLayout>\n```\n\n만약, `<Suspense>` 경계를 root에 걸어 앱 전체를 감쌌다면, shell은 spinner만을 보여줄 것입니다. 하지만, 이는 사용자 경험에 있어서 좋지 않습니다. 큰 spinner를 보는 것은 비록 더 기다리게 될 지 언정, 실제 레이아웃이 나타나는 것보다 더 느리고 더 짜증나는 경험을 줄 수 있습니다. 이런 이유로 개발자들은 `<Suspense>` 경계를 통해 shell을 전체 페이지 레이아웃의 뼈대처럼 *최소한으로 완성된* 상태라는 느낌을 줄 수 있도록 하고 싶을 것입니다.\n\n`renderToReadableStream`를 비동기 호출하여 모든 shell이 렌더링될 때까지 `stream`으로 위 문제를 해결합니다. 보통, `stream`을 가진 응답을 생성하고 반환함으로서 스트리밍을 시작합니다.\n\n```js {5}\nasync function handler(request) {\n  const stream = await renderToReadableStream(<App />, {\n    bootstrapScripts: ['/main.js']\n  });\n  return new Response(stream, {\n    headers: { 'content-type': 'text/html' },\n  });\n}\n```\n\n`stream`이 반환되었을 때, 중첩된 내부의 `<Suspense>` 경계의 컴포넌트는 아직 데이터를 로딩중일 수도 있습니다.\n\n---\n\n### 서버에서의 충돌을 기록하기 {/*logging-crashes-on-the-server*/}\n\n기본적으로 서버의 모든 오류는 콘솔에 기록<sup>Logging</sup>됩니다. 이 동작을 재정의하여 충돌<sup>Crash</sup> 보고서를 기록할 수 있습니다.\n\n```js {4-7}\nasync function handler(request) {\n  const stream = await renderToReadableStream(<App />, {\n    bootstrapScripts: ['/main.js'],\n    onError(error) {\n      console.error(error);\n      logServerCrashReport(error);\n    }\n  });\n  return new Response(stream, {\n    headers: { 'content-type': 'text/html' },\n  });\n}\n```\n\n만약 `onError`를 직접 제공했다면, 위와 같이 콘솔에 오류를 로깅하는 것도 잊지 마세요.\n\n---\n\n### shell 내부의 오류로부터 회복하기 {/*recovering-from-errors-inside-the-shell*/}\n\n이번 예시에서, shell은 `ProfileLayout`, `ProfileCover` 그리고 `PostsGlimmer`를 포함하고 있습니다.\n\n```js {3-5,7-8}\nfunction ProfilePage() {\n  return (\n    <ProfileLayout>\n      <ProfileCover />\n      <Suspense fallback={<PostsGlimmer />}>\n        <Posts />\n      </Suspense>\n    </ProfileLayout>\n  );\n}\n```\n\n만약, 위의 컴포넌트들을 렌더링하다가 오류가 발생했다면, React는 클라이언트로 보낼 의미있는 HTML을 가지고 있지 않을 것 입니다. 이런 때를 대비해 `renderToReadableStream`을 `try...catch`로 감싸 서버 렌더링에 의존하지 않는 대체 HTML을 보낼 수 있도록 하세요.\n\n```js {2,13-18}\nasync function handler(request) {\n  try {\n    const stream = await renderToReadableStream(<App />, {\n      bootstrapScripts: ['/main.js'],\n      onError(error) {\n        console.error(error);\n        logServerCrashReport(error);\n      }\n    });\n    return new Response(stream, {\n      headers: { 'content-type': 'text/html' },\n    });\n  } catch (error) {\n    return new Response('<h1>Something went wrong</h1>', {\n      status: 500,\n      headers: { 'content-type': 'text/html' },\n    });\n  }\n}\n```\n\nshell을 렌더링하면서 오류가 발생한다면, `onError`와 `catch` 블록이 동시에 실행됩니다. `onError`는 오류를 보고하기 위해 사용하고, `catch` 블록은 대체 HTML 문서를 보내기 위해 사용하세요. 대체 HTML은 반드시 오류 페이지일 필요는 없습니다. 대신, 클라이언트에서만 렌더링되는 대체 shell을 포함할 수 있습니다.\n\n---\n\n### shell 외부의 오류로부터 회복하기 {/*recovering-from-errors-outside-the-shell*/}\n\n이번 예시에서, `<Posts />` 컴포넌트는 `<Suspense>`에 감싸져있기 때문에, shell의 일부가 *아닙니다.*\n\n```js {6}\nfunction ProfilePage() {\n  return (\n    <ProfileLayout>\n      <ProfileCover />\n      <Suspense fallback={<PostsGlimmer />}>\n        <Posts />\n      </Suspense>\n    </ProfileLayout>\n  );\n}\n```\n\n`Posts` 컴포넌트 혹은 그 내부 어딘가에서 오류가 발생했을 경우, React는 [오류로 부터 회복하려고 할 것입니다](/reference/react/Suspense#providing-a-fallback-for-server-errors-and-client-only-content).\n\n1. 가장 가까운 `<Suspense>` 경계의 로딩 대체인 (`PostsGlimmer`)를 HTML로 보냅니다.\n2. 서버에서 더이상의 `Posts`와 그 내부를 렌더링하는 것을 \"포기\"합니다.\n3. 클라이언트에서 자바스크립트 코드가 로딩되었을 때, React는 `Posts`를 다시 렌더링하려고 시도할 것입니다.\n\n만약 클라이언트에서도 `Posts` 렌더링 재시도가 실패한다면, React는 클라이언트에서 오류를 던지게 됩니다. 렌더링 중에 일어난 모든 오류과 함께, [가장 가까운 부모 오류 경계](/reference/react/Component#static-getderivedstatefromerror)로 사용자에게 어떤 오류를 보여줘야할 지를 결정하게 됩니다. 실제로는, 사용자가 오류가 복구될 수 없다는 것이 확실시 될 때까지 로딩 표시기를 보고있어야 한 다는 것을 의미합니다.\n\n클라이언트에서 `Posts` 렌더링 재시도가 성공하면, 서버에서 온 로딩 대체 HTML이 클라이언트에서 렌더링된 결과로 교체됩니다. 사용자는 서버에서 오류가 있었는지 모를 것입니다. 하지만, 서버의 `onError` 콜백과 클라이언트의 [`onRecoverableError`](/reference/react-dom/client/hydrateRoot#hydrateroot) 콜백은 그대로 실행됩니다. 이를 통해 오류 내용을 받아서 로깅할 수 있습니다.\n\n---\n\n### 상태 코드 설정하기 {/*setting-the-status-code*/}\n\n스트리밍은 트레이드오프를 동반합니다. 사용자가 컨텐츠를 더 빨리 볼 수 있도록 페이지를 스트리밍하겠지만, 한번 스트리밍을 시작하면, 응답 상태 코드를 설정할 수 없습니다.\n\n앱을 shell(`<Suspense>` 경계 바깥의 모든 것)과 나머지 컨텐츠들로 [나누는 것](#specifying-what-goes-into-the-shell)으로, 이 문제는 이미 해결된 것입니다. 만약 shell에 오류가 있다면, `catch` 블록이 실행되기 때문에, 상태 코드를 설정할 수 있습니다. 혹은, 클라이언트에서 오류가 복구된 다는 것을 알고 있다면, 그냥 \"OK\"를 보낼 수도 있습니다.\n\n```js {11}\nasync function handler(request) {\n  try {\n    const stream = await renderToReadableStream(<App />, {\n      bootstrapScripts: ['/main.js'],\n      onError(error) {\n        console.error(error);\n        logServerCrashReport(error);\n      }\n    });\n    return new Response(stream, {\n      status: 200,\n      headers: { 'content-type': 'text/html' },\n    });\n  } catch (error) {\n    return new Response('<h1>Something went wrong</h1>', {\n      status: 500,\n      headers: { 'content-type': 'text/html' },\n    });\n  }\n}\n```\n\n만약 shell 바깥 (`<Suspense>` 경계의 안쪽)에서 오류가 발생했다면, React는 렌더링을 멈추지 않을 것입니다. 즉, `onError` 콜백은 실행되지만, `catch` 블록은 실행되지 않은 채로 코드가 계속해서 실행된다는 의미입니다. 그 이유는, [위에서 설명했던 것 처럼](#recovering-from-errors-outside-the-shell), React가 클라이언트에서 해당 오류를 복구하려고 하기 때문입니다.\n\n하지만, 그래도 상태 코드를 설정하고 싶다면, 오류가 발생했다는 사실을 이용하여 상태 코드를 설정할 수 있습니다.\n\n```js {3,7,13}\nasync function handler(request) {\n  try {\n    let didError = false;\n    const stream = await renderToReadableStream(<App />, {\n      bootstrapScripts: ['/main.js'],\n      onError(error) {\n        didError = true;\n        console.error(error);\n        logServerCrashReport(error);\n      }\n    });\n    return new Response(stream, {\n      status: didError ? 500 : 200,\n      headers: { 'content-type': 'text/html' },\n    });\n  } catch (error) {\n    return new Response('<h1>Something went wrong</h1>', {\n      status: 500,\n      headers: { 'content-type': 'text/html' },\n    });\n  }\n}\n```\n\n이는, 초기 shell 콘텐츠를 생성하는 동안 발생한 shell 외부에서 일어난 오류만 잡을 것이므로, 완전한 방법은 아닙니다. 만약, 어떤 컨텐츠가 정말 중요해서 해당 컨텐츠에 발생한 오류를 알고 싶다면, 그것을 shell 안으로 옮겨 오류를 알아낼 수 있습니다.\n\n---\n\n### 각기 다른 방식으로 다른 종류의 오류를 처리하기 {/*handling-different-errors-in-different-ways*/}\n\n[`Error` 서브클래스를 직접 만들 수 있고](https://javascript.info/custom-errors), [`instanceof`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/instanceof) 연산자를 이용해 어떤 오류가 발생했는지 구별할 수 있습니다. 예를 들어, `NotFoundError`라는 서브클래스를 정의했고 이를 컴포넌트에서 발생시켰다고 한다면, `onError`에서 오류를 저장하고 응답을 반환하기 전에 오류 타입에 따라 다른 동작을 할 수 있습니다.\n\n```js {2-3,5-15,22,28,33}\nasync function handler(request) {\n  let didError = false;\n  let caughtError = null;\n\n  function getStatusCode() {\n    if (didError) {\n      if (caughtError instanceof NotFoundError) {\n        return 404;\n      } else {\n        return 500;\n      }\n    } else {\n      return 200;\n    }\n  }\n\n  try {\n    const stream = await renderToReadableStream(<App />, {\n      bootstrapScripts: ['/main.js'],\n      onError(error) {\n        didError = true;\n        caughtError = error;\n        console.error(error);\n        logServerCrashReport(error);\n      }\n    });\n    return new Response(stream, {\n      status: getStatusCode(),\n      headers: { 'content-type': 'text/html' },\n    });\n  } catch (error) {\n    return new Response('<h1>Something went wrong</h1>', {\n      status: getStatusCode(),\n      headers: { 'content-type': 'text/html' },\n    });\n  }\n}\n```\n\n명심해야 할 것은, shell을 전송하고 스트리밍을 시작한 후엔 상태 코드를 변경할 수 없다는 것입니다.\n\n---\n\n### 정적 생성과 크롤러를 위해 모든 컨텐츠가 로딩되는 것을 기다리기 {/*waiting-for-all-content-to-load-for-crawlers-and-static-generation*/}\n\n스트리밍은 사용자가 컨텐츠 상호작용이 가능해지는 것을 기다리지 않고도 컨텐츠를 볼 수 있어 더 나은 사용자 경험을 제공합니다.\n\n하지만, 크롤러가 이 페이지를 방문했을 때, 혹은 페이지를 빌드했을 때 정적으로 생성한 경우엔 컨텐츠가 점진적으로 드러나는 것이 아니라 모든 컨텐츠가 처음부터 모두 불러와진 다음 최종 HTML 출력물을 생성하는 것을 원할 것입니다.\n\n`stream.allReady` Promise를 기다림으로써 모든 컨텐츠가 로드될 때까지 기다릴 수 있습니다.\n\n```js {12-15}\nasync function handler(request) {\n  try {\n    let didError = false;\n    const stream = await renderToReadableStream(<App />, {\n      bootstrapScripts: ['/main.js'],\n      onError(error) {\n        didError = true;\n        console.error(error);\n        logServerCrashReport(error);\n      }\n    });\n    let isCrawler = // ... depends on your bot detection strategy ...\n    if (isCrawler) {\n      await stream.allReady;\n    }\n    return new Response(stream, {\n      status: didError ? 500 : 200,\n      headers: { 'content-type': 'text/html' },\n    });\n  } catch (error) {\n    return new Response('<h1>Something went wrong</h1>', {\n      status: 500,\n      headers: { 'content-type': 'text/html' },\n    });\n  }\n}\n```\n\n일반적인 방문자라면 컨텐츠를 점진적으로 받게 될 것입니다. 크롤러라면, 모든 컨텐츠가 로드될 때까지 기다린 후에 최종 HTML을 받게 될 것입니다. 하지만, 이는 크롤러가 모든 데이터를 받을 때까지 기다려야 한다는 것으로, 그 중에 어떤 데이터가 로드되는데 느리거나 오류가 발생할 수 있는 상황까지 기다려야 한다는 것을 의미합니다. 따라서 앱의 특성에 따라 크롤러에게 shell을 보내는 것이 더 좋을 수도 있습니다.\n\n---\n\n### 서버 렌더링 멈추기 {/*aborting-server-rendering*/}\n\n일정 시간이 지난 후, 서버에게 강제로 렌더링을 \"포기\"하라고 할 수 있습니다.\n\n```js {3,4-6,9}\nasync function handler(request) {\n  try {\n    const controller = new AbortController();\n    setTimeout(() => {\n      controller.abort();\n    }, 10000);\n\n    const stream = await renderToReadableStream(<App />, {\n      signal: controller.signal,\n      bootstrapScripts: ['/main.js'],\n      onError(error) {\n        didError = true;\n        console.error(error);\n        logServerCrashReport(error);\n      }\n    });\n    // ...\n  }\n}\n```\n\nReact는 나머지 로딩 대체 내용을 HTML로 내보낼 것이고, 클라이언트에서 그 나머지 렌더링을 계속할 것입니다.\n"
  },
  {
    "path": "src/content/reference/react-dom/server/renderToStaticMarkup.md",
    "content": "---\ntitle: renderToStaticMarkup\n---\n\n<Intro>\n\n`renderToStaticMarkup`은 상호작용하지 않는 React 트리를 HTML 문자열로 렌더링합니다.\n\n```js\nconst html = renderToStaticMarkup(reactNode, options?)\n```\n\n</Intro>\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `renderToStaticMarkup(reactNode, options?)` {/*rendertostaticmarkup*/}\n\n서버에서 `renderToStaticMarkup`을 호출하여 앱을 HTML로 렌더링합니다.\n\n```js\nimport { renderToStaticMarkup } from 'react-dom/server';\n\nconst html = renderToStaticMarkup(<Page />);\n```\n\n이는 React 컴포넌트의 상호작용하지 않는 HTML 출력을 생성합니다.\n\n[아래 예시를 참고하세요.](#usage)\n\n#### 매개변수 {/*parameters*/}\n\n* `reactNode`: HTML로 렌더링할 React 노드입니다. 예를 들어, `<Page />`와 같은 JSX 노드입니다.\n* **optional** `options`: 서버 렌더링을 위한 객체입니다.\n  * **optional** `identifierPrefix`: [`useId`](/reference/react/useId)에 의해 생성된 ID에 대해 React가 사용하는 문자열 접두사입니다. 같은 페이지에서 여러 루트를 사용할 때 충돌을 피하기 위해 유용합니다.\n\n#### 반환값 {/*returns*/}\n\nHTML 문자열을 반환합니다.\n\n#### 주의 사항 {/*caveats*/}\n\n* `renderToStaticMarkup`의 출력값은 Hydrate 될 수 없습니다.\n\n* `renderToStaticMarkup`은 Suspense를 제한적으로 지원합니다. 만약 Suspense 컴포넌트라면, `renderToStaticMarkup`은 즉시 HTML을 Fallback으로 보냅니다.\n\n* `renderToStaticMarkup`은 브라우저에서 동작하지만, 클라이언트 코드에서 사용하는 것은 권장하지 않습니다. 브라우저에서 컴포넌트를 HTML로 렌더링해야 하는 경우, [HTML을 DOM 노드로 렌더링해서 가져오세요.](/reference/react-dom/server/renderToString#removing-rendertostring-from-the-client-code)\n\n---\n\n## 사용법 {/*usage*/}\n\n### 상호작용하지 않는 React 트리를 HTML 문자열로 렌더링하기 {/*rendering-a-non-interactive-react-tree-as-html-to-a-string*/}\n\n`renderToStaticMarkup`을 서버 응답과 함께 보낼 수 있는 HTML 문자열로 앱에 렌더링하기 위해 호출하세요.\n\n```js {5-6}\nimport { renderToStaticMarkup } from 'react-dom/server';\n\n// The route handler syntax depends on your backend framework\napp.use('/', (request, response) => {\n  const html = renderToStaticMarkup(<Page />);\n  response.send(html);\n});\n```\n\n이것은 React 컴포넌트의 상호작용하지 않는 초기 HTML 출력을 생성합니다.\n\n<Pitfall>\n\n이 메서드는 **Hydrate 될 수 없는 상호작용하지 않는 HTML**을 렌더링합니다. 이 메서드는 React를 간단한 정적 페이지 생성기로 사용하거나, 이메일과 같은 완전히 정적인 콘텐츠를 렌더링할 때 유용합니다.\n\n상호작용을 위한 앱은 서버에서 [`renderToString`](/reference/react-dom/server/renderToString), 클라이언트에서는 [`hydrateRoot`](/reference/react-dom/client/hydrateRoot)를 사용해야 합니다.\n\n</Pitfall>\n"
  },
  {
    "path": "src/content/reference/react-dom/server/renderToString.md",
    "content": "---\ntitle: renderToString\n---\n\n<Pitfall>\n\n`renderToString`은 스트리밍이나 데이터 대기를 지원하지 않습니다. [대안을 참고하세요.](#alternatives)\n\n</Pitfall>\n\n<Intro>\n\n`renderToString`은 React 트리를 HTML 문자열로 렌더링합니다.\n\n```js\nconst html = renderToString(reactNode, options?)\n```\n\n</Intro>\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `renderToString(reactNode, options?)` {/*rendertostring*/}\n\n서버에서 `renderToString`을 실행하면 앱을 HTML로 렌더링합니다.\n\n```js\nimport { renderToString } from 'react-dom/server';\n\nconst html = renderToString(<App />);\n```\n\n클라이언트에서 [`hydrateRoot`](/reference/react-dom/client/hydrateRoot)를 호출하면 서버에서 생성된 HTML을 상호작용하게 만듭니다.\n\n[아래 예시를 참고하세요.](#usage)\n\n#### 매개변수 {/*parameters*/}\n\n* `reactNode`: HTML로 렌더링할 React 노드입니다. 예를 들어 `<App />`과 같은 JSX 노드입니다.\n* **optional** `options`: 서버 렌더링을 위한 객체입니다.\n  * **optional** `identifierPrefix`: [`useId`](/reference/react/useId)에 의해 생성된 ID에 대해 React가 사용하는 문자열 접두사입니다. 같은 페이지에서 여러 루트를 사용할 때 충돌을 피하기 위해 유용합니다. [`hydrateRoot`](/reference/react-dom/client/hydrateRoot#parameters)에 전달된 접두사와 동일해야 합니다.\n\n#### 반환값 {/*returns*/}\n\nHTML 문자열.\n\n#### 주의 사항 {/*caveats*/}\n\n* `renderToString`는 Suspense 지원에 한계가 있습니다. 컴포넌트가 중단된다면 `renderToString`는 즉시 해당 폴백을 HTML로 보냅니다.\n\n* `renderToString`은 브라우저에서 동작하지만, 클라이언트 코드에서 사용하는 것은 [권장하지 않습니다.](#removing-rendertostring-from-the-client-code)\n\n---\n\n## 사용법 {/*usage*/}\n\n### React 트리를 HTML 문자열로 렌더링하기 {/*rendering-a-react-tree-as-html-to-a-string*/}\n\n서버 응답과 함께 보낼 수 있는 HTML 문자열로 앱을 렌더링하려면 `renderToString`을 호출하세요.\n\n```js {5-6}\nimport { renderToString } from 'react-dom/server';\n\n// 라우트 핸들러 구문은 백엔드 프레임워크에 따라 다릅니다\napp.use('/', (request, response) => {\n  const html = renderToString(<App />);\n  response.send(html);\n});\n```\n\n이는 React 컴포넌트의 초기 상호작용하지 않는 HTML 출력을 생성합니다. 클라이언트에서 서버에서 생성된 HTML을 *Hydrate*하여 상호작용할 수 있도록 [`hydrateRoot`](/reference/react-dom/client/hydrateRoot)를 실행해야 합니다.\n\n\n<Pitfall>\n\n`renderToString`은 스트리밍 또는 데이터 대기를 지원하지 않습니다. [대안을 참고하세요.](#alternatives)\n\n</Pitfall>\n\n---\n\n## 대안 {/*alternatives*/}\n\n### 서버에서 `renderToString`을 스트리밍 렌더링으로 마이그레이션 {/*migrating-from-rendertostring-to-a-streaming-method-on-the-server*/}\n\n`renderToString`은 문자열을 즉시 반환하므로, 로딩 중인 콘텐츠를 스트리밍하는 것을 지원하지 않습니다.\n\n가능하면 다음과 같은 완전한 기능을 갖춘 대안을 사용하는 것을 권장합니다.\n\n* Node.js를 사용하는 경우 [`renderToPipeableStream`](/reference/react-dom/server/renderToPipeableStream)을 사용하세요.\n* Deno와 최신 엣지 런타임에서 [Web Stream](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API)을 사용하는 경우 [`renderToReadableStream`](/reference/react-dom/server/renderToReadableStream)을 사용하세요.\n\n서버 환경에서 스트림을 지원하지 않는 경우에도 `renderToString`을 계속 사용할 수 있습니다.\n\n---\n\n### 서버에서 `renderToString`을 정적 프리렌더로 마이그레이션 {/*migrating-from-rendertostring-to-a-static-prerender-on-the-server*/}\n\n`renderToString`은 문자열을 즉시 반환하므로, 정적 HTML 생성을 위해 데이터 로딩이 완료될 때까지 기다리는 것을 지원하지 않습니다.\n\n가능하면 다음과 같은 완전한 기능을 갖춘 대안을 사용하는 것을 권장합니다.\n\n* Node.js를 사용하는 경우 [`prerenderToNodeStream`](/reference/react-dom/static/prerenderToNodeStream)을 사용하세요.\n* Deno와 최신 엣지 런타임에서 [Web Streams](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API)을 사용하는 경우 [`prerender`](/reference/react-dom/static/prerender)를 사용하세요.\n\n정적 사이트 생성 환경에서 스트림을 지원하지 않는 경우에는 `renderToString`을 계속 사용할 수 있습니다.\n\n---\n\n### 클라이언트 코드에서 `renderToString` 제거하기 {/*removing-rendertostring-from-the-client-code*/}\n\n클라이언트에서 일부 컴포넌트를 HTML로 변환하기 위해 `renderToString`을 사용하기도 합니다.\n\n```js {1-2}\n// 🚩 불필요: 클라이언트에서 `renderToString` 사용하기.\nimport { renderToString } from 'react-dom/server';\n\nconst html = renderToString(<MyIcon />);\nconsole.log(html); // 예를 들어, \"<svg>...</svg>\"\n```\n\n**클라이언트에서** `react-dom/server`를 가져오면 불필요하게 번들 크기가 커지므로 피해야 합니다. 브라우저에서 일부 컴포넌트를 HTML로 렌더링해야 할 경우 [`createRoot`](/reference/react-dom/client/createRoot)를 사용하고 DOM에서 HTML을 읽으세요.\n\n```js\nimport { createRoot } from 'react-dom/client';\nimport { flushSync } from 'react-dom';\n\nconst div = document.createElement('div');\nconst root = createRoot(div);\nflushSync(() => {\n  root.render(<MyIcon />);\n});\nconsole.log(div.innerHTML); // 예를 들어, \"<svg>...</svg>\"\n```\n\n[`flushSync`](/reference/react-dom/flushSync) 호출은 [`innerHTML`](https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML) 속성을 읽기 전에 DOM을 업데이트하기 위해 필요합니다.\n\n---\n\n## 문제 해결 {/*troubleshooting*/}\n\n### 컴포넌트가 일시 중단되면 HTML에 항상 폴백을 포함합니다. {/*when-a-component-suspends-the-html-always-contains-a-fallback*/}\n\n`renderToString`은 Suspense를 완벽하게 지원하지 않습니다.\n\n일부 컴포넌트가 일시 중단<sup>Suspend</sup>되거나 (예를 들어, [`lazy`](/reference/react/lazy)와 함께 정의되거나 데이터를 가져올 때) `renderToString`은 콘텐츠가 해결될 때까지 기다리지 않습니다. `renderToString`는 그 위에 가장 가까운 [`<Suspense>`](/reference/react/Suspense) 경계를 찾아 `fallback` 프로퍼티를 HTML에 렌더링합니다. 내용<sup>Content</sup>은 클라이언트 코드가 로드될 때까지 나타나지 않습니다.\n\n이를 해결하려면 [권장되는 스트리밍 솔루션](#alternatives) 중 하나를 사용하면 좋습니다. 서버 사이드 렌더링의 경우, 서버에서 해결되는 대로 콘텐츠를 작은 단위<sup>chunk</sup>로 스트리밍할 수 있어 클라이언트 코드가 로드되기 전에 사용자가 페이지가 단계적으로 나타나는 것을 볼 수 있습니다. 정적 사이트 생성의 경우, 정적 HTML을 생성하기 전에 모든 콘텐츠가 해결될 때까지 기다릴 수 있습니다.\n\n"
  },
  {
    "path": "src/content/reference/react-dom/server/resume.md",
    "content": "---\ntitle: resume\n---\n\n<Intro>\n\n`resume`은 [Readable Web Stream](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream)을 이용해 사전 렌더링된 React 트리를 스트리밍합니다.\n\n```js\nconst stream = await resume(reactNode, postponedState, options?)\n```\n\n</Intro>\n\n<InlineToc />\n\n<Note>\n\n이 API는 [Web Streams](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API)에 의존합니다. Node.js에서는 [`resumeToPipeableStream`](/reference/react-dom/server/resumeToPipeableStream)을 대신 사용하세요.\n\n</Note>\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `resume(node, postponedState, options?)` {/*resume*/}\n\n`resume`을 호출해 사전 렌더링된 React 트리의 렌더링을 재개하고, 이를 HTML로 [Readable Web Stream](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream)에 렌더링합니다.\n\n\n```js\nimport { resume } from 'react-dom/server';\nimport {getPostponedState} from './storage';\n\nasync function handler(request, writable) {\n  const postponed = await getPostponedState(request);\n  const resumeStream = await resume(<App />, postponed);\n  return resumeStream.pipeTo(writable)\n}\n```\n\n[아래에서 더 많은 예시를 확인하세요.](#usage)\n\n#### 매개변수 {/*parameters*/}\n\n* `reactNode`: `prerender`를 호출할 때 전달한 React 노드입니다. 예를 들어, `<App />`과 같은 JSX 엘리먼트입니다. 전체 문서를 나타낼 것으로 예상되므로 `App` 컴포넌트는 `<html>` 태그를 렌더링해야 합니다.\n* `postponedState`: [prerender API](/reference/react-dom/static/prerender)에서 반환된 불분명한 `postpone` 객체로, 저장해 둔 위치(예: Redis, 파일, S3)에서 불러옵니다.\n* **optional** `options`: 스트리밍 옵션을 지정할 수 있는 객체입니다.\n  * **optional** `nonce`: [`script-src` Content-Security-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src)에서 스크립트를 허용하기 위한 [`nonce`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#nonce) 문자열입니다.\n  * **optional** `signal`: [서버 렌더링을 중단](/reference/react-dom/server/renderToReadableStream#aborting-server-rendering)하고 나머지를 클라이언트에서 렌더링할 수 있게 하는 [중단 신호](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal)입니다.\n  * **optional** `onError`: 서버 오류가 발생할 때마다, [복구 가능](/reference/react-dom/server/renderToReadableStream#recovering-from-errors-outside-the-shell) 또는 [불가능](/reference/react-dom/server/renderToReadableStream#recovering-from-errors-inside-the-shell)에 관계없이 호출되는 콜백입니다. 기본적으로 `console.error`만 호출합니다. [충돌 보고를 기록](/reference/react-dom/server/renderToReadableStream#logging-crashes-on-the-server)하도록 재정의하는 경우에도 반드시 `console.error`를 호출해야 합니다.\n\n\n#### 반환값 {/*returns*/}\n\n`resume`은 Promise를 반환합니다.\n\n- `resume`이 [shell](/reference/react-dom/server/renderToReadableStream#specifying-what-goes-into-the-shell)을 성공적으로 생성하면, 해당 Promise는 [Writable Web Stream](https://developer.mozilla.org/en-US/docs/Web/API/WritableStream)으로 파이프할 수 있는 [Readable Web Stream](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream)으로 이행됩니다.\n- shell에서 오류가 발생하면, 해당 Promise는 그 오류와 함께 거부됩니다.\n\n반환된 스트림은 다음과 같은 추가적인 프로퍼티를 가지고 있습니다.\n\n* `allReady`: 모든 렌더링이 완료되면 이행되는 Promise입니다. [크롤러와 정적 생성을 위해](/reference/react-dom/server/renderToReadableStream#waiting-for-all-content-to-load-for-crawlers-and-static-generation) 응답을 반환하기 전에 `await stream.allReady`를 사용할 수 있습니다. 이렇게 하면 점진적 로딩은 사용할 수 없습니다. 스트림에는 최종 HTML이 포함됩니다.\n\n#### 주의 사항 {/*caveats*/}\n\n- `resume`은 `bootstrapScripts`, `bootstrapScriptContent`, `bootstrapModules` 옵션을 받지 않습니다. 대신 `postponedState`를 생성하는 `prerender` 호출에 이 옵션들을 전달해야 합니다. 또한 쓰기 가능한 스트림에 부트스트랩 콘텐츠를 수동으로 주입할 수도 있습니다.\n- `prerender`와 `resume`에서 접두사가 동일해야 하므로, `resume`은 `identifierPrefix`를 받지 않습니다.\n- `nonce`는 prerender에 전달할 수 없으므로, prerender에 스크립트를 제공하지 않는 경우에만 `resume`에 `nonce`를 전달해야 합니다.\n- `resume`은 사전 렌더링이 완전히 완료되지 않은 컴포넌트를 찾을 때까지 루트부터 다시 렌더링합니다. 사전 렌더링이 완전히 완료된 컴포넌트(해당 컴포넌트와 자식들의 사전 렌더링이 모두 완료된 경우)만 완전히 건너뜁니다.\n\n\n## 사용법 {/*usage*/}\n\n### 사전 렌더링 재개하기 {/*resuming-a-prerender*/}\n\n<Sandpack>\n\n```js src/App.js hidden \n```\n\n```html public/index.html\n<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"UTF-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <title>Document</title>\n</head>\n<body>\n  <iframe id=\"container\"></iframe>\n</body>\n</html>\n```\n\n```js src/index.js\nimport {\n  flushReadableStreamToFrame,\n  getUser,\n  Postponed,\n  sleep,\n} from \"./demo-helpers\";\nimport { StrictMode, Suspense, use, useEffect } from \"react\";\nimport { prerender } from \"react-dom/static\";\nimport { resume } from \"react-dom/server\";\nimport { hydrateRoot } from \"react-dom/client\";\n\nfunction Header() {\n  return <header>Me and my descendants can be prerendered</header>;\n}\n\nconst { promise: cookies, resolve: resolveCookies } = Promise.withResolvers();\n\nfunction Main() {\n  const { sessionID } = use(cookies);\n  const user = getUser(sessionID);\n\n  useEffect(() => {\n    console.log(\"reached interactivity!\");\n  }, []);\n\n  return (\n    <main>\n      Hello, {user.name}!\n      <button onClick={() => console.log(\"hydrated!\")}>\n        Clicking me requires hydration.\n      </button>\n    </main>\n  );\n}\n\nfunction Shell({ children }) {\n  // In a real app, this is where you would put your html and body.\n  // We're just using tags here we can include in an existing body for demonstration purposes\n  return (\n    <html>\n      <body>{children}</body>\n    </html>\n  );\n}\n\nfunction App() {\n  return (\n    <Shell>\n      <Suspense fallback=\"loading header\">\n        <Header />\n      </Suspense>\n      <Suspense fallback=\"loading main\">\n        <Main />\n      </Suspense>\n    </Shell>\n  );\n}\n\nasync function main(frame) {\n  // Layer 1\n  const controller = new AbortController();\n  const prerenderedApp = prerender(<App />, {\n    signal: controller.signal,\n    onError(error) {\n      if (error instanceof Postponed) {\n      } else {\n        console.error(error);\n      }\n    },\n  });\n  // We're immediately aborting in a macrotask.\n  // Any data fetching that's not available synchronously, or in a microtask, will not have finished.\n  setTimeout(() => {\n    controller.abort(new Postponed());\n  });\n\n  const { prelude, postponed } = await prerenderedApp;\n  await flushReadableStreamToFrame(prelude, frame);\n\n  // Layer 2\n  // Just waiting here for demonstration purposes.\n  // In a real app, the prelude and postponed state would've been serialized in Layer 1 and Layer would deserialize them.\n  // The prelude content could be flushed immediated as plain HTML while\n  // React is continuing to render from where the prerender left off.\n  await sleep(2000);\n\n  // You would get the cookies from the incoming HTTP request\n  resolveCookies({ sessionID: \"abc\" });\n\n  const stream = await resume(<App />, postponed);\n\n  await flushReadableStreamToFrame(stream, frame);\n\n  // Layer 3\n  // Just waiting here for demonstration purposes.\n  await sleep(2000);\n\n  hydrateRoot(frame.contentWindow.document, <App />);\n}\n\nmain(document.getElementById(\"container\"));\n\n```\n\n```js src/demo-helpers.js\nexport async function flushReadableStreamToFrame(readable, frame) {\n  const document = frame.contentWindow.document;\n  const decoder = new TextDecoder();\n  for await (const chunk of readable) {\n    const partialHTML = decoder.decode(chunk);\n    document.write(partialHTML);\n  }\n}\n\n// This doesn't need to be an error.\n// You can use any other means to check if an error during prerender was\n// from an intentional abort or a real error.\nexport class Postponed extends Error {}\n\n// We're just hardcoding a session here.\nexport function getUser(sessionID) {\n  return {\n    name: \"Alice\",\n  };\n}\n\nexport function sleep(timeoutMS) {\n  return new Promise((resolve) => {\n    setTimeout(() => {\n      resolve();\n    }, timeoutMS);\n  });\n}\n```\n\n</Sandpack>\n\n### 추가로 읽어보기 {/*further-reading*/}\n\n재개 동작은 `renderToReadableStream`과 유사합니다. 더 많은 예시는 [`renderToReadableStream`의 사용법 섹션](/reference/react-dom/server/renderToReadableStream#usage)을 확인하세요.\n[`prerender`의 사용법 섹션](/reference/react-dom/static/prerender#usage)에는 `prerender` 사용 방법에 대한 예시가 포함되어 있습니다.\n"
  },
  {
    "path": "src/content/reference/react-dom/server/resumeToPipeableStream.md",
    "content": "---\ntitle: resumeToPipeableStream\n---\n\n<Intro>\n\n`resumeToPipeableStream`은 사전 렌더링된 React 트리를 파이프 가능한 [Node.js Stream](https://nodejs.org/api/stream.html)으로 스트리밍합니다.\n\n```js\nconst {pipe, abort} = await resumeToPipeableStream(reactNode, postponedState, options?)\n```\n\n</Intro>\n\n<InlineToc />\n\n<Note>\n\n이 API는 Node.js 전용입니다. Deno 및 최신 엣지 런타임처럼 [Web Streams](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API)을 지원하는 환경에서는 [`resume`](/reference/react-dom/server/resume)을 대신 사용하세요.\n\n</Note>\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `resumeToPipeableStream(node, postponed, options?)` {/*resume-to-pipeable-stream*/}\n\n`resumeToPipeableStream`을 호출해 사전 렌더링된 React 트리의 렌더링을 재개하고, 이를 HTML로 [Node.js Stream](https://nodejs.org/api/stream.html#writable-streams)에 렌더링합니다.\n\n\n```js\nimport { resumeToPipeableStream } from 'react-dom/server';\nimport {getPostponedState} from './storage';\n\nasync function handler(request, response) {\n  const postponed = await getPostponedState(request);\n  const {pipe} = resumeToPipeableStream(<App />, postponed, {\n    onShellReady: () => {\n      pipe(response);\n    }\n  });\n}\n```\n\n[아래에서 더 많은 예시를 확인하세요.](#usage)\n\n#### 매개변수 {/*parameters*/}\n\n* `reactNode`: `prerender`를 호출할 때 전달한 React 노드입니다. 예를 들어, `<App />`과 같은 JSX 엘리먼트입니다. 전체 문서를 나타낼 것으로 예상되므로 `App` 컴포넌트는 `<html>` 태그를 렌더링해야 합니다.\n* `postponedState`: [prerender API](/reference/react-dom/static/prerender)에서 반환된 불분명한 `postpone` 객체로, 저장해 둔 위치(예: Redis, 파일, S3)에서 불러옵니다.\n* **optional** `options`: 스트리밍 옵션을 지정할 수 있는 객체입니다.\n  * **optional** `nonce`: [`script-src` Content-Security-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src)에서 스크립트를 허용하기 위한 [`nonce`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#nonce) 문자열입니다.\n  * **optional** `signal`: [서버 렌더링을 중단](/reference/react-dom/server/renderToPipeableStream#aborting-server-rendering)하고 나머지를 클라이언트에서 렌더링할 수 있게 하는 [중단 신호](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal)입니다.\n  * **optional** `onError`: 서버 오류가 발생할 때마다, [복구 가능](/reference/react-dom/server/renderToPipeableStream#recovering-from-errors-outside-the-shell) 또는 [불가능](/reference/react-dom/server/renderToPipeableStream#recovering-from-errors-inside-the-shell)에 관계없이 호출되는 콜백입니다. 기본적으로 `console.error`만 호출합니다. [충돌 보고를 기록](/reference/react-dom/server/renderToPipeableStream#logging-crashes-on-the-server)하도록 재정의하는 경우에도 반드시 `console.error`를 호출해야 합니다.\n  * **optional** `onShellReady`: [셸](/reference/react-dom/server/renderToPipeableStream#specifying-what-goes-into-the-shell)이 렌더링된 직후에 실행되는 콜백입니다. 여기서 `pipe`를 호출해 스트리밍을 시작할 수 있습니다. React는 HTML 로딩 폴백을 콘텐츠로 대체하는 인라인 `<script>` 태그와 함께 셸 뒤에 [추가 콘텐츠를 스트리밍](/reference/react-dom/server/renderToPipeableStream#streaming-more-content-as-it-loads)합니다.\n  * **optional** `onShellError`: 초기 셸을 렌더링하는 데 오류가 발생하면 호출되는 콜백입니다. 오류를 인자로 받습니다. 스트림에서 아직 바이트가 전송되지 않았고, `onShellReady`나 `onAllReady`도 호출되지 않으므로 [폴백 HTML 셸을 출력](/reference/react-dom/server/renderToPipeableStream#recovering-from-errors-inside-the-shell)하거나 prelude를 사용할 수 있습니다.\n\n\n#### 반환값 {/*returns*/}\n\n`resumeToPipeableStream`은 두 개의 메서드를 가진 객체를 반환합니다.\n\n* `pipe`는 HTML을 제공된 [쓰기 가능한 Node.js 스트림](https://nodejs.org/api/stream.html#writable-streams)으로 출력합니다. 스트리밍을 활성화하려면 `onShellReady`에서, 크롤러와 정적 생성을 사용하려면 `onAllReady`에서 `pipe`를 호출합니다.\n* `abort`를 사용하면 [서버 렌더링을 중단](/reference/react-dom/server/renderToPipeableStream#aborting-server-rendering)하고 나머지는 클라이언트에서 렌더링할 수 있습니다.\n\n#### 주의 사항 {/*caveats*/}\n\n- `resumeToPipeableStream`은 `bootstrapScripts`, `bootstrapScriptContent`, `bootstrapModules` 옵션을 받지 않습니다. 대신 `postponedState`를 생성하는 `prerender` 호출에 이 옵션들을 전달해야 합니다. 또한 쓰기 가능한 스트림에 부트스트랩 콘텐츠를 수동으로 주입할 수도 있습니다.\n- `prerender`와 `resumeToPipeableStream`에서 접두사가 동일해야 하므로, `resumeToPipeableStream`은 `identifierPrefix`를 받지 않습니다.\n- `nonce`는 prerender에 전달할 수 없으므로, prerender에 스크립트를 제공하지 않는 경우에만 `resumeToPipeableStream`에 `nonce`를 전달해야 합니다.\n- `resumeToPipeableStream`은 사전 렌더링이 완전히 완료되지 않은 컴포넌트를 찾을 때까지 루트부터 다시 렌더링합니다. 사전 렌더링이 완전히 완료된 컴포넌트(해당 컴포넌트와 자식들의 사전 렌더링이 모두 완료된 경우)만 완전히 건너뜁니다.\n\n## 사용법 {/*usage*/}\n\n### 추가로 읽어보기 {/*further-reading*/}\n\n재개 동작은 `renderToPipeableStream`과 유사합니다. 더 많은 예시는 [`renderToPipeableStream`의 사용법 섹션](/reference/react-dom/server/renderToPipeableStream#usage)을 확인하세요.\n[`prerenderToNodeStream`의 사용법 섹션](/reference/react-dom/static/prerenderToNodeStream#usage)에는 `prerenderToNodeStream` 사용 방법에 대한 예시가 포함되어 있습니다.\n"
  },
  {
    "path": "src/content/reference/react-dom/static/index.md",
    "content": "---\ntitle: Static React DOM APIs\n---\n\n<Intro>\n\nThe `react-dom/static` APIs let you generate static HTML for React components. They have limited functionality compared to the streaming APIs. A [framework](/learn/creating-a-react-app#full-stack-frameworks) may call them for you. Most of your components don't need to import or use them.\n\n</Intro>\n\n---\n\n## Static APIs for Web Streams {/*static-apis-for-web-streams*/}\n\n다음 메서드들은 브라우저, Deno, 및 일부 최신 엣지 런타임을 포함하는 [Web Streams](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API) 환경에서만 사용할 수 있습니다.\n\n* [`prerender`](/reference/react-dom/static/prerender) renders a React tree to static HTML with a [Readable Web Stream.](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream)\n* <ExperimentalBadge /> [`resumeAndPrerender`](/reference/react-dom/static/resumeAndPrerender) continues a prerendered React tree to static HTML with a [Readable Web Stream](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream).\n\nNode.js also includes these methods for compatibility, but they are not recommended due to worse performance. Use the [dedicated Node.js APIs](#static-apis-for-nodejs-streams) instead.\n\n---\n\n## Static APIs for Node.js Streams {/*static-apis-for-nodejs-streams*/}\n\n다음 메서드들은 [Node.js Streams](https://nodejs.org/api/stream.html) 환경에서만 사용할 수 있습니다.\n\n* [`prerenderToNodeStream`](/reference/react-dom/static/prerenderToNodeStream) renders a React tree to static HTML with a [Node.js Stream.](https://nodejs.org/api/stream.html)\n* <ExperimentalBadge /> [`resumeAndPrerenderToNodeStream`](/reference/react-dom/static/resumeAndPrerenderToNodeStream) continues a prerendered React tree to static HTML with a [Node.js Stream.](https://nodejs.org/api/stream.html)\n\n"
  },
  {
    "path": "src/content/reference/react-dom/static/prerender.md",
    "content": "---\ntitle: prerender\n---\n\n<Intro>\n\n`prerender`는 [Web Stream](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API)을 사용하여 React 트리를 정적 HTML 문자열로 렌더링합니다.\n\n```js\nconst {prelude, postponed} = await prerender(reactNode, options?)\n```\n\n</Intro>\n\n<InlineToc />\n\n<Note>\n\n이 API는 [Web Stream](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API)에 의존합니다. Node.js에서는 [`prerenderToNodeStream`](/reference/react-dom/static/prerenderToNodeStream)을 대신 사용하세요.\n\n</Note>\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `prerender(reactNode, options?)` {/*prerender*/}\n\n`prerender`를 호출하여 앱을 정적 HTML로 렌더링합니다.\n\n```js\nimport { prerender } from 'react-dom/static';\n\nasync function handler(request, response) {\n  const {prelude} = await prerender(<App />, {\n    bootstrapScripts: ['/main.js']\n  });\n  return new Response(prelude, {\n    headers: { 'content-type': 'text/html' },\n  });\n}\n```\n\n클라이언트에서 [`hydrateRoot`](/reference/react-dom/client/hydrateRoot)를 호출하여 서버에서 생성된 HTML을 상호작용할 수 있도록 만듭니다.\n\n[아래에서 더 많은 예시를 확인하세요.](#usage)\n\n#### 매개변수 {/*parameters*/}\n\n* `reactNode`: HTML로 렌더링하려는 React 노드. 예를 들어 `<App />`과 같은 JSX 엘리먼트입니다. 전체 문서를 나타낼 것으로 예상되므로 `App` 컴포넌트는 `<html>` 태그를 렌더링해야 합니다.\n\n* **optional** `options`: 정적 생성 옵션을 가진 객체입니다.\n  * **optional** `bootstrapScriptContent`: 지정될 경우, 해당 문자열은 `<script>` 태그에 인라인 형식으로 추가됩니다.\n  * **optional** `bootstrapScripts`: 페이지에 표시할 `<script>` 태그에 대한 문자열 URL 배열입니다. [`hydrateRoot`](/reference/react-dom/client/hydrateRoot)를 호출하는 `<script>`를 포함하려면 이것을 사용하세요. 클라이언트에서 React를 전혀 실행하지 않으려면 생략하세요.\n  * **optional** `bootstrapModules`: `bootstrapScripts`와 유사하지만 대신 [`<script type=\"module\">`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules)을 추가합니다.\n  * **optional** `identifierPrefix`: React가 [`useId`](/reference/react/useId)에 의해 생성된 ID를 사용하는 문자열 접두사입니다. 같은 페이지에서 여러 루트를 사용할 때 충돌을 피하는 데 유용합니다. [`hydrateRoot`](/reference/react-dom/client/hydrateRoot#parameters)에 전달된 것과 동일한 접두사여야 합니다.\n  * **optional** `namespaceURI`: 스트림의 루트 [namespace URI](https://developer.mozilla.org/en-US/docs/Web/API/Document/createElementNS#important_namespace_uris)를 가진 문자열입니다. 기본값은 일반 HTML입니다. SVG의 경우 `'http://www.w3.org/2000/svg'`를, MathML의 경우 `'http://www.w3.org/1998/Math/MathML'`을 전달합니다.\n  * **optional** `onError`: [복구 가능](/reference/react-dom/server/renderToReadableStream#recovering-from-errors-outside-the-shell) 또는 [불가능](/reference/react-dom/server/renderToReadableStream#recovering-from-errors-inside-the-shell)에 관계없이 서버 오류가 발생할 때마다 호출되는 콜백입니다. 기본적으로 `console.error`만 호출합니다. 이 함수를 재정의하여 [크래시 리포트를 로깅](/reference/react-dom/server/renderToReadableStream#logging-crashes-on-the-server)하는 경우 `console.error`를 계속 호출해야 합니다. 또한 셸이 출력되기 전에 [상태 코드를 설정](/reference/react-dom/server/renderToReadableStream#setting-the-status-code)하는 데 사용할 수도 있습니다.\n  * **optional** `progressiveChunkSize`: 청크의 바이트 수입니다. [기본 휴리스틱에 대해 더 읽어보기.](https://github.com/facebook/react/blob/14c2be8dac2d5482fda8a0906a31d239df8551fc/packages/react-server/src/ReactFizzServer.js#L210-L225)\n  * **optional** `signal`: [사전 렌더링을 중단](#aborting-prerendering)하고 나머지를 클라이언트에서 렌더링하기 위한 [중단 신호<sup>Abort Signal</sup>](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal)를 설정합니다.\n\n#### 반환값 {/*returns*/}\n\n`prerender` returns a Promise:\n- If rendering the is successful, the Promise will resolve to an object containing:\n  - `prelude`: a [Web Stream](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API) of HTML. You can use this stream to send a response in chunks, or you can read the entire stream into a string.\n  - `postponed`: a JSON-serializeable, opaque object that can be passed to [`resume`](/reference/react-dom/server/resume) if `prerender` did not finish. Otherwise `null` indicating that the `prelude` contains all the content and no resume is necessary.\n- If rendering fails, the Promise will be rejected. [Use this to output a fallback shell.](/reference/react-dom/server/renderToReadableStream#recovering-from-errors-inside-the-shell)\n\n#### 주의 사항 {/*caveats*/}\n\n`nonce`는 사전 렌더링할 때 사용할 수 없는 옵션입니다. Nonce는 요청마다 고유해야 하며, [CSP](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP)로 애플리케이션을 보호하기 위해 Nonce를 사용한다면 Nonce 값을 사전 렌더링 자체에 포함하는 것은 부적절하고 안전하지 않습니다.\n\n<Note>\n\n### `prerender`를 언제 사용해야 하나요? {/*when-to-use-prerender*/}\n\n정적 `prerender` API는 정적 사이트 생성<sup>SSG, Static Site Generation</sup>에 사용됩니다. `renderToString`과 달리 `prerender`는 해결되기 전에 모든 데이터가 로드될 때까지 대기합니다. 이는 Suspense를 사용하여 가져와야 하는 데이터를 포함하여 전체 페이지에 대한 정적 HTML을 생성하는 데 적합합니다. 콘텐츠가 로드되면서 스트리밍하려면 [`renderToReadableStream`](/reference/react-dom/server/renderToReadableStream)과 같은 스트리밍 서버 사이드 렌더링(SSR) API를 사용하세요.\n\n`prerender` can be aborted and later either continued with `resumeAndPrerender` or resumed with `resume` to support partial pre-rendering.\n\n</Note>\n\n---\n\n## 사용법 {/*usage*/}\n\n### React 트리를 정적 HTML 스트림으로 렌더링하기 {/*rendering-a-react-tree-to-a-stream-of-static-html*/}\n\n`prerender`를 호출해 [Readable Web Stream](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream)을 통해 React 트리를 정적 HTML로 렌더링합니다.\n\n```js [[1, 4, \"<App />\"], [2, 5, \"['/main.js']\"]]\nimport { prerender } from 'react-dom/static';\n\nasync function handler(request) {\n  const {prelude} = await prerender(<App />, {\n    bootstrapScripts: ['/main.js']\n  });\n  return new Response(prelude, {\n    headers: { 'content-type': 'text/html' },\n  });\n}\n```\n\n<CodeStep step={1}>루트 컴포넌트</CodeStep>와 함께 <CodeStep step={2}>부트스트랩 `<script>` 경로 목록</CodeStep>을 제공해야 합니다. 루트 컴포넌트는 **루트 `<html>` 태그를 포함하여 전체 문서를 반환해야 합니다.**\n\n예를 들어 다음과 같을 수 있습니다.\n\n```js [[1, 1, \"App\"]]\nexport default function App() {\n  return (\n    <html>\n      <head>\n        <meta charSet=\"utf-8\" />\n        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n        <link rel=\"stylesheet\" href=\"/styles.css\"></link>\n        <title>My app</title>\n      </head>\n      <body>\n        <Router />\n      </body>\n    </html>\n  );\n}\n```\n\nReact는 [doctype](https://developer.mozilla.org/en-US/docs/Glossary/Doctype)과 <CodeStep step={2}>부트스트랩 `<script>` 태그</CodeStep>를 결과 HTML 스트림에 삽입합니다.\n\n```html [[2, 5, \"/main.js\"]]\n<!DOCTYPE html>\n<html>\n  <!-- ... HTML from your components ... -->\n</html>\n<script src=\"/main.js\" async=\"\"></script>\n```\n\n클라이언트에서 부트스트랩 스크립트는 [`hydrateRoot`를 호출하여 전체 `document`를 Hydrate해야 합니다.](/reference/react-dom/client/hydrateRoot#hydrating-an-entire-document)\n\n```js [[1, 4, \"<App />\"]]\nimport { hydrateRoot } from 'react-dom/client';\nimport App from './App.js';\n\nhydrateRoot(document, <App />);\n```\n\n이렇게 하면 정적 서버 생성 HTML에 이벤트 리스너가 연결되어 상호작용하게 만들어집니다.\n\n<DeepDive>\n\n#### 빌드 출력에서 CSS 및 JS 에셋 경로 읽기 {/*reading-css-and-js-asset-paths-from-the-build-output*/}\n\n최종 에셋 URL(JavaScript 및 CSS 파일 등)은 빌드 후 해시되는 경우가 많습니다. 예를 들어, `styles.css` 대신 `styles.123456.css`로 끝날 수 있습니다. 정적 에셋 파일명을 해시하면 동일한 에셋의 모든 개별 빌드가 다른 파일명을 갖게 됩니다. 이는 정적 에셋에 대한 장기 캐싱을 안전하게 활성화할 수 있게 해주므로 유용합니다. 특정 이름의 파일은 절대 콘텐츠가 변경되지 않기 때문입니다.\n\n하지만 빌드가 끝날 때까지 에셋 URL을 모르는 경우 소스 코드에 넣을 방법이 없습니다. 예를 들어, JSX에 `\"/styles.css\"`를 하드코딩하는 것은 작동하지 않습니다. 소스 코드에 URL을 넣지 않으려면 루트 컴포넌트는 Props로 전달된 맵에서 실제 파일명을 읽어야 합니다.\n\n```js {1,6}\nexport default function App({ assetMap }) {\n  return (\n    <html>\n      <head>\n        <title>My app</title>\n        <link rel=\"stylesheet\" href={assetMap['styles.css']}></link>\n      </head>\n      ...\n    </html>\n  );\n}\n```\n\n서버에선 `<App assetMap={assetMap} />`를 렌더링하고, 에셋 URL들과 함께 `assetMap`을 전달합니다.\n\n```js {1-5,8,9}\n// 빌드 도구에서 이 JSON을 가져와야 합니다. 예를 들어, 빌드 결과물에서 읽어올 수 있습니다.\nconst assetMap = {\n  'styles.css': '/styles.123456.css',\n  'main.js': '/main.123456.js'\n};\n\nasync function handler(request) {\n  const {prelude} = await prerender(<App assetMap={assetMap} />, {\n    bootstrapScripts: [assetMap['/main.js']]\n  });\n  return new Response(prelude, {\n    headers: { 'content-type': 'text/html' },\n  });\n}\n```\n\n서버에서 `<App assetMap={assetMap} />`를 렌더링하고 있으므로, Hydration 오류를 방지하기 위해 클라이언트에서도 `assetMap`과 함께 렌더링해야 합니다. 다음과 같이 `assetMap`을 직렬화하여 클라이언트에 전달할 수 있습니다.\n\n```js {9-10}\n// 빌드 도구에서 이 JSON을 가져와야 합니다.\nconst assetMap = {\n  'styles.css': '/styles.123456.css',\n  'main.js': '/main.123456.js'\n};\n\nasync function handler(request) {\n  const {prelude} = await prerender(<App assetMap={assetMap} />, {\n    // 주의: 이 데이터는 사용자가 생성한 것이 아니므로 stringify()를 사용해도 안전합니다.\n    bootstrapScriptContent: `window.assetMap = ${JSON.stringify(assetMap)};`,\n    bootstrapScripts: [assetMap['/main.js']],\n  });\n  return new Response(prelude, {\n    headers: { 'content-type': 'text/html' },\n  });\n}\n```\n\n위 예시에서 `bootstrapScriptContent` 옵션은 클라이언트에서 전역 `window.assetMap` 변수를 설정하는 추가 인라인 `<script>` 태그를 추가합니다. 이를 통해 클라이언트 코드가 동일한 `assetMap`을 읽을 수 있습니다.\n\n```js {4}\nimport { hydrateRoot } from 'react-dom/client';\nimport App from './App.js';\n\nhydrateRoot(document, <App assetMap={window.assetMap} />);\n```\n\n클라이언트와 서버 모두 동일한 `assetMap` Prop으로 `App`을 렌더링하므로 Hydration 오류가 발생하지 않습니다.\n\n</DeepDive>\n\n---\n\n### React 트리를 정적 HTML 문자열로 렌더링하기 {/*rendering-a-react-tree-to-a-string-of-static-html*/}\n\n`prerender`를 호출하여 앱을 정적 HTML 문자열로 렌더링합니다.\n\n```js\nimport { prerender } from 'react-dom/static';\n\nasync function renderToString() {\n  const {prelude} = await prerender(<App />, {\n    bootstrapScripts: ['/main.js']\n  });\n\n  const reader = prelude.getReader();\n  let content = '';\n  while (true) {\n    const {done, value} = await reader.read();\n    if (done) {\n      return content;\n    }\n    content += Buffer.from(value).toString('utf8');\n  }\n}\n```\n\n이렇게 하면 React 컴포넌트의 초기 상호작용하지 않는 HTML 출력이 생성됩니다. 클라이언트에서는 [`hydrateRoot`](/reference/react-dom/client/hydrateRoot)를 호출하여 서버에서 생성된 HTML을 *Hydrate*하고 상호작용하게 만들어야 합니다.\n\n---\n\n### 모든 데이터 로드 대기 {/*waiting-for-all-data-to-load*/}\n\n`prerender`는 정적 HTML 생성을 완료하고 해결되기 전에 모든 데이터가 로드될 때까지 대기합니다. 예를 들어, 표지, 친구와 사진이 있는 사이드바, 게시물 목록을 보여주는 프로필 페이지를 생각해 보세요.\n\n```js\nfunction ProfilePage() {\n  return (\n    <ProfileLayout>\n      <ProfileCover />\n      <Sidebar>\n        <Friends />\n        <Photos />\n      </Sidebar>\n      <Suspense fallback={<PostsGlimmer />}>\n        <Posts />\n      </Suspense>\n    </ProfileLayout>\n  );\n}\n```\n\n`<Posts />`가 데이터를 로드해야 하는데 시간이 걸린다고 가정해 보겠습니다. 이상적으로는 게시물이 완료될 때까지 기다려서 HTML에 포함하고 싶을 것입니다. 이를 위해 Suspense를 사용하여 데이터를 일시 중단할 수 있으며, `prerender`는 일시 중단된 콘텐츠가 완료될 때까지 기다린 후 정적 HTML로 해결됩니다.\n\n<Note>\n\n**Suspense를 지원하는 데이터 소스만 Suspense 컴포넌트를 활성화합니다.** 이는 다음과 같습니다.\n\n- [Relay](https://relay.dev/docs/guided-tour/rendering/loading-states/)와 [Next.js](https://nextjs.org/docs/getting-started/react-essentials) 같은 Suspense가 가능한 프레임워크를 사용한 데이터 가져오기.\n- [`lazy`](/reference/react/lazy)를 사용한 지연 로딩 컴포넌트.\n- [`use`](/reference/react/use)를 사용한 Promise 값 읽기.\n\nSuspense는 Effect나 이벤트 핸들러 내부에서 데이터를 가져올 경우, **이를 감지하지 못합니다.**.\n\n`Posts` 컴포넌트에서 데이터를 불러오는 정확한 방법은 프레임워크에 따라 다릅니다. Suspense를 지원하는 프레임워크를 사용하는 경우, 데이터를 가져오는 자세한 방법은 해당 프레임워크 문서에서 찾을 수 있습니다.\n\n독자적인 프레임워크를 사용하지 않는 Suspense 지원 데이터 가져오기는 아직 지원되지 않습니다. Suspense를 지원하는 데이터 소스를 구현하기 위한 요구 사항은 불안정하고 문서화되지 않았습니다. 데이터 소스를 Suspense와 통합하기 위한 공식 API는 React의 향후 버전에서 출시할 예정입니다.\n\n</Note>\n\n---\n\n### 사전 렌더링 중단 {/*aborting-prerendering*/}\n\n타임아웃 후 사전 렌더링을 \"포기\"하도록 강제할 수 있습니다.\n\n```js {2-5,11}\nasync function renderToString() {\n  const controller = new AbortController();\n  setTimeout(() => {\n    controller.abort()\n  }, 10000);\n\n  try {\n    // prelude에는 컨트롤러가 중단되기 전에\n    // 사전 렌더링된 모든 HTML이 포함됩니다.\n    const {prelude} = await prerender(<App />, {\n      signal: controller.signal,\n    });\n    //...\n```\n\n불완전한 자식을 가진 모든 Suspense 경계는 폴백 상태로 prelude에 포함됩니다.\n\nThis can be used for partial prerendering together with [`resume`](/reference/react-dom/server/resume) or [`resumeAndPrerender`](/reference/react-dom/static/resumeAndPrerender).\n\n## 문제 해결 {/*troubleshooting*/}\n\n### 전체 앱이 렌더링될 때까지 스트림이 시작되지 않습니다 {/*my-stream-doesnt-start-until-the-entire-app-is-rendered*/}\n\n`prerender` 응답은 모든 Suspense 경계가 해결될 때까지 기다리는 것을 포함하여 전체 앱이 렌더링을 완료할 때까지 기다린 후 해결됩니다. 이는 사전에 정적 사이트 생성(SSG)을 위해 설계되었으며 콘텐츠가 로드되면서 더 많은 콘텐츠를 스트리밍하는 것을 지원하지 않습니다.\n\n콘텐츠가 로드되면서 스트리밍하려면 [`renderToReadableStream`](/reference/react-dom/server/renderToReadableStream)과 같은 스트리밍 서버 렌더링 API를 사용하세요.\n"
  },
  {
    "path": "src/content/reference/react-dom/static/prerenderToNodeStream.md",
    "content": "---\ntitle: prerenderToNodeStream\n---\n\n<Intro>\n\n`prerenderToNodeStream`은 [Node.js Stream](https://nodejs.org/api/stream.html)을 사용하여 React 트리를 정적 HTML 문자열로 렌더링합니다.\n\n```js\nconst {prelude, postponed} = await prerenderToNodeStream(reactNode, options?)\n```\n\n</Intro>\n\n<InlineToc />\n\n<Note>\n\n이 API는 Node.js 전용입니다. Deno나 최신 엣지 런타임처럼 [Web Streams](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API)를 지원하는 환경에서는 [`prerender`](/reference/react-dom/static/prerender)를 사용해야 합니다.\n\n</Note>\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `prerenderToNodeStream(reactNode, options?)` {/*prerender*/}\n\n`prerenderToNodeStream`을 호출해 앱을 정적 HTML로 렌더링합니다.\n\n```js\nimport { prerenderToNodeStream } from 'react-dom/static';\n\n// 라우트 핸들러 문법은 사용하는 백엔드 프레임워크에 따라 다릅니다.\napp.use('/', async (request, response) => {\n  const { prelude } = await prerenderToNodeStream(<App />, {\n    bootstrapScripts: ['/main.js'],\n  });\n\n  response.setHeader('Content-Type', 'text/plain');\n  prelude.pipe(response);\n});\n```\n\n클라이언트에서 [`hydrateRoot`](/reference/react-dom/client/hydrateRoot)를 호출해 서버에서 생성된 HTML을 인터랙티브하게 만듭니다.\n\n[아래에서 더 많은 예시를 확인하세요.](#usage)\n\n#### 매개변수 {/*parameters*/}\n\n* `reactNode`: HTML로 렌더링할 React 노드입니다. 예를 들어 `<App />`과 같은 JSX 노드가 해당됩니다. 전체 문서를 나타내야 하므로, App 컴포넌트는 `<html>` 태그를 렌더링해야 합니다.\n\n* **optional** `options`: 정적 생성 옵션을 가진 객체입니다.\n  * **optional** `bootstrapScriptContent`: 지정될 경우, 이 문자열이 인라인 `<script>` 태그에 삽입됩니다.\n  * **optional** `bootstrapScripts`: 페이지에 출력할 `<script>` 태그의 문자열 URL 배열입니다. [`hydrateRoot`](/reference/react-dom/client/hydrateRoot)를 호출하는 `<script>`를 포함하려면 이 옵션을 사용하세요. 클라이언트에서 React를 전혀 실행하지 않으려면 생략하세요.\n  * **optional** `bootstrapModules`: `bootstrapScripts`와 같지만, [`<script type=\"module\">`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules)을 출력합니다.\n  * **optional** `identifierPrefix`: [`useId`](/reference/react/useId)로 생성된 ID에 React가 사용하는 문자열 접두사입니다. 한 페이지에서 여러 개의 루트를 사용할 때 충돌을 피하는 데 유용합니다. [`hydrateRoot`](/reference/react-dom/client/hydrateRoot#parameters)에 전달한 접두사와 동일해야 합니다.\n  * **optional** `namespaceURI`: 스트림의 루트 [namespace URI](https://developer.mozilla.org/en-US/docs/Web/API/Document/createElementNS#important_namespace_uris)를 담은 문자열입니다. 기본값은 일반 HTML입니다. SVG의 경우 `'http://www.w3.org/2000/svg'`, MathML의 경우 `'http://www.w3.org/1998/Math/MathML'`을 전달하세요.\n  * **optional** `onError`: 서버 오류가 발생할 때마다, [복구 가능](/reference/react-dom/server/renderToPipeableStream#recovering-from-errors-outside-the-shell)  [불가능](/reference/react-dom/server/renderToPipeableStream#recovering-from-errors-inside-the-shell) 여부와 관계없이 호출되는 콜백입니다. 기본적으로 `console.error`만 호출합니다. [충돌 보고를 기록](/reference/react-dom/server/renderToPipeableStream#logging-crashes-on-the-server)하도록 재정의하는 경우에도 반드시 `console.error`를 호출해야 합니다. 셸이 출력되기 전에 [상태 코드를 설정](/reference/react-dom/server/renderToPipeableStream#setting-the-status-code)하는 데에도 사용할 수 있습니다.\n  * **optional** `progressiveChunkSize`: 청크의 바이트 수입니다. [기본 휴리스틱에 대해 더 읽어보세요.](https://github.com/facebook/react/blob/14c2be8dac2d5482fda8a0906a31d239df8551fc/packages/react-server/src/ReactFizzServer.js#L210-L225)\n  * **optional** `signal`: [프리렌더링을 중단하고](#aborting-prerendering) 나머지를 클라이언트에서 렌더링할 수 있게 하는 [중단 신호](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal)입니다.\n\n#### 반환값 {/*returns*/}\n\n`prerenderToNodeStream` returns a Promise:\n- If rendering the is successful, the Promise will resolve to an object containing:\n  - `prelude`: a [Node.js Stream.](https://nodejs.org/api/stream.html) of HTML. You can use this stream to send a response in chunks, or you can read the entire stream into a string.\n  - `postponed`: a JSON-serializeable, opaque object that can be passed to [`resumeToPipeableStream`](/reference/react-dom/server/resumeToPipeableStream) if `prerenderToNodeStream` did not finish. Otherwise `null` indicating that the `prelude` contains all the content and no resume is necessary.\n- If rendering fails, the Promise will be rejected. [Use this to output a fallback shell.](/reference/react-dom/server/renderToPipeableStream#recovering-from-errors-inside-the-shell)\n\n#### 주의 사항 {/*caveats*/}\n\n프리렌더링 시 `nonce` 옵션은 사용할 수 없습니다. Nonce는 요청마다 고유해야 하며, [CSP](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP)로 애플리케이션을 보호할 때 Nonce 값을 프리렌더링 결과에 포함하는 것은 부적절하고 안전하지 않습니다.\n\n<Note>\n\n### `prerenderToNodeStream`은 언제 사용해야 하나요? {/*when-to-use-prerender*/}\n\n정적 `prerenderToNodeStream` API는 정적 서버 사이드 생성(SSG)에 사용합니다. `renderToString`과 달리, `prerenderToNodeStream`은 모든 데이터가 로드될 때까지 기다린 후에 성공합니다. 이는 Suspense를 사용해 가져와야 하는 데이터를 포함해, 전체 페이지의 정적 HTML을 생성하는 데 적합합니다. 콘텐츠가 로드되는 동안 스트리밍하려면, [`renderToReadableStream`](/reference/react-dom/server/renderToReadableStream)과 같은 스트리밍 서버 사이드 렌더링(SSR) API를 사용하세요.\n\n`prerenderToNodeStream` can be aborted and resumed later with `resumeToPipeableStream` to support partial pre-rendering.\n\n</Note>\n\n---\n\n## 사용법 {/*usage*/}\n\n### React 트리를 정적 HTML 스트림으로 렌더링하기 {/*rendering-a-react-tree-to-a-stream-of-static-html*/}\n\n`prerenderToNodeStream`를 호출해 React 트리를 정적 HTML로 렌더링하고, 이를 [Node.js Stream](https://nodejs.org/api/stream.html)에 출력합니다.\n\n```js [[1, 5, \"<App />\"], [2, 6, \"['/main.js']\"]]\nimport { prerenderToNodeStream } from 'react-dom/static';\n\n// 라우터 핸들러 문법은 사용하는 백엔드 프레임워크에 따라 다릅니다.\napp.use('/', async (request, response) => {\n  const { prelude } = await prerenderToNodeStream(<App />, {\n    bootstrapScripts: ['/main.js'],\n  });\n\n  response.setHeader('Content-Type', 'text/plain');\n  prelude.pipe(response);\n});\n```\n\n<CodeStep step={1}>루트 컴포넌트</CodeStep>와 함께, <CodeStep step={2}>부트스트랩 `<script>` 경로</CodeStep> 목록을 제공해야 합니다. 루트 컴포넌트는 **루트 `<html>` 태그를 포함한 전체 문서를 반환해야 합니다.**\n\n예를 들어, 다음과 같을 수 있습니다. \n\n```js [[1, 1, \"App\"]]\nexport default function App() {\n  return (\n    <html>\n      <head>\n        <meta charSet=\"utf-8\" />\n        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n        <link rel=\"stylesheet\" href=\"/styles.css\"></link>\n        <title>My app</title>\n      </head>\n      <body>\n        <Router />\n      </body>\n    </html>\n  );\n}\n```\n\nReact는 [doctype](https://developer.mozilla.org/en-US/docs/Glossary/Doctype)과  <CodeStep step={2}>부트스트랩 `<script>` 태그</CodeStep>를 결과 HTML 스트림에 삽입합니다.\n\n```html [[2, 5, \"/main.js\"]]\n<!DOCTYPE html>\n<html>\n  <!-- ... 컴포넌트에서 생성된 HTML ... -->\n</html>\n<script src=\"/main.js\" async=\"\"></script>\n```\n\n클라이언트에서 부트스트랩 스크립트는 [`hydrateRoot`를 호출해 `document` 전체를 hydrate해야 합니다.](/reference/react-dom/client/hydrateRoot#hydrating-an-entire-document)\n\n```js [[1, 4, \"<App />\"]]\nimport { hydrateRoot } from 'react-dom/client';\nimport App from './App.js';\n\nhydrateRoot(document, <App />);\n```\n\n이는 정적 서버 생성 HTML에 이벤트 리스너를 연결해 인터랙티브하게 만듭니다.\n\n<DeepDive>\n\n#### 빌드 출력에서 CSS 및 JS 에셋 경로 읽기 {/*reading-css-and-js-asset-paths-from-the-build-output*/}\n\n최종 에셋 URL(예: JavaScript와 CSS 파일)은 빌드 후 종종 해시<sup>Hash</sup>가 추가됩니다. 예를 들어 `styles.css` 대신 `styles.123456.css`와 같은 파일명이 될 수 있습니다. 정적 에셋 파일명에 해시를 추가하면 동일한 에셋이라도 빌드마다 다른 파일명을 갖게 됩니다. 이는 정적 에셋에 대해 장기 캐싱을 안전하게 활성화할 수 있게 해줍니다. 특정 이름을 가진 파일은 그 내용이 절대 변경되지 않기 때문입니다.\n\n그러나 빌드가 끝난 후에야 에셋 URL을 알 수 있다면, 이를 소스 코드에 넣을 방법이 없습니다. 예를 들어 앞서처럼 JSX에 `\"/styles.css\"`를 하드코딩하면 동작하지 않습니다. 소스 코드에 에셋 경로를 넣지 않으려면, 루트 컴포넌트가 Prop으로 전달된 맵에서 실제 파일명을 읽어올 수 있습니다.\n\n```js {1,6}\nexport default function App({ assetMap }) {\n  return (\n    <html>\n      <head>\n        <title>My app</title>\n        <link rel=\"stylesheet\" href={assetMap['styles.css']}></link>\n      </head>\n      ...\n    </html>\n  );\n}\n```\n\n서버에서 `<App assetMap={assetMap} />`을 렌더링하고 에셋 URL이 포함된 `assetMap`을 전달합니다.\n\n```js {1-5,8,9}\n// 이 JSON은 빌드 도구에서 가져와야 합니다. 예를 들어 빌드 출력에서 읽어올 수 있습니다.\nconst assetMap = {\n  'styles.css': '/styles.123456.css',\n  'main.js': '/main.123456.js'\n};\n\napp.use('/', async (request, response) => {\n  const { prelude } = await prerenderToNodeStream(<App />, {\n    bootstrapScripts: [assetMap['/main.js']]\n  });\n\n  response.setHeader('Content-Type', 'text/html');\n  prelude.pipe(response);\n});\n```\n\n서버에서 `<App assetMap={assetMap} />`을 렌더링하고 있으므로, Hydration 오류를 피하려면 클라이언트에서도 `assetMap`과 함께 렌더링해야 합니다. 다음과 같이 `assetMap`을 직렬화해 클라이언트로 전달할 수 있습니다.\n\n```js {9-10}\n// 이 JSON은 빌드 도구에서 가져와야 합니다.\nconst assetMap = {\n  'styles.css': '/styles.123456.css',\n  'main.js': '/main.123456.js'\n};\n\napp.use('/', async (request, response) => {\n  const { prelude } = await prerenderToNodeStream(<App />, {\n    // 주의: 이 데이터는 사용자가 생성한 것이 아니므로 `stringify()`해도 안전합니다.\n    bootstrapScriptContent: `window.assetMap = ${JSON.stringify(assetMap)};`,\n    bootstrapScripts: [assetMap['/main.js']],\n  });\n\n  response.setHeader('Content-Type', 'text/html');\n  prelude.pipe(response);\n});\n```\n\n위 예시에서 `bootstrapScriptContent` 옵션은 클라이언트에서 전역 변수 `window.assetMap`을 설정하는 인라인 `<script>` 태그를 추가합니다. 이렇게 하면 클라이언트 코드에서 동일한 `assetMap`을 읽을 수 있습니다.\n\n```js {4}\nimport { hydrateRoot } from 'react-dom/client';\nimport App from './App.js';\n\nhydrateRoot(document, <App assetMap={window.assetMap} />);\n```\n\n클라이언트와 서버 모두 `App`을 동일한 `assetMap` Prop으로 렌더링하므로 하이드레이션 오류가 발생하지 않습니다.\n\n</DeepDive>\n\n---\n\n### React 트리를 정적 HTML 문자열로 렌더링하기 {/*rendering-a-react-tree-to-a-string-of-static-html*/}\n\n`prerenderToNodeStream`을 호출해 앱을 정적 HTML 문자열로 렌더링합니다.\n\n```js\nimport { prerenderToNodeStream } from 'react-dom/static';\n\nasync function renderToString() {\n  const {prelude} = await prerenderToNodeStream(<App />, {\n    bootstrapScripts: ['/main.js']\n  });\n\n  return new Promise((resolve, reject) => {\n    let data = '';\n    prelude.on('data', chunk => {\n      data += chunk;\n    });\n    prelude.on('end', () => resolve(data));\n    prelude.on('error', reject);\n  });\n}\n```\n\n이렇게 하면 React 컴포넌트의 초기 상호작용하지 않은 HTML 출력이 생성됩니다. 클라이언트에서는 [`hydrateRoot`](/reference/react-dom/client/hydrateRoot)를 호출하여 서버에서 생성된 HTML을 *Hydrate*하고 상호작용하게 만들어야 합니다.\n\n---\n\n### 모든 데이터가 로드될 때까지 기다리기 {/*waiting-for-all-data-to-load*/}\n\n`prerenderToNodeStream`는 모든 데이터가 로드될 때까지 기다린 뒤 정적 HTML 생성을 완료하고 Promise를 해결합니다. 예를 들어 표지 이미지, 친구와 사진이 포함된 사이드바, 게시물 목록을 표시하는 프로필 페이지를 생각해 보겠습니다.\n\n```js\nfunction ProfilePage() {\n  return (\n    <ProfileLayout>\n      <ProfileCover />\n      <Sidebar>\n        <Friends />\n        <Photos />\n      </Sidebar>\n      <Suspense fallback={<PostsGlimmer />}>\n        <Posts />\n      </Suspense>\n    </ProfileLayout>\n  );\n}\n```\n\n예를 들어 `<Posts />`가 데이터를 로드해야 하고, 이 과정에 시간이 걸린다고 가정해 보겠습니다. 이 경우, 이상적으로는 게시물 데이터가 모두 로드된 뒤 HTML에 포함되기를 원할 것입니다. 이를 위해 Suspense를 사용해 데이터 로드가 완료될 때까지 렌더링을 일시 중단할 수 있으며, `prerenderToNodeStream`는 해당 중단된 콘텐츠가 완료될 때까지 기다린 후 정적 HTML로 변환을 완료합니다.\n\n<Note>\n\n**Suspense를 지원하는 데이터 소스만이 Suspense 컴포넌트를 활성화합니다.** 여기에는 다음이 포함됩니다.\n\n- [Relay](https://relay.dev/docs/guided-tour/rendering/loading-states/) 혹은 [Next.js](https://nextjs.org/docs/getting-started/react-essentials) 처럼 Suspense를 지원하는 프레임워크를 사용한 데이터 가져오기.\n- [`lazy`](/reference/react/lazy)를 사용한 컴포넌트 코드의 지연로딩.\n- [`use`](/reference/react/use)를 사용해 Promise의 값을 읽기.\n\nSuspense는 Effect나 이벤트 핸들러 내부에서 데이터가 패칭될 때 이를 **감지하지 않습니다.** \n\n위 예시의 `Posts` 컴포넌트에서 데이터를 로드하는 구체적인 방법은 사용하는 프레임워크에 따라 다릅니다. Suspense를 지원하는 프레임워크를 사용한다면, 해당 프레임워크의 데이터 패칭 문서에서 자세한 내용을 확인할 수 있습니다. \n\n특정 프레임워크를 사용하지 않는 Suspense 지원 데이터 패칭은 아직 지원되지 않습니다. Suspense를 지원하는 데이터 소스를 구현하기 위한 요구 사항은 현재 불안정하고 문서화되어 있지 않습니다. 데이터 소스를 Suspense와 통합하기 위한 공식 API는 React의 향후 버전에서 제공될 예정입니다.\n\n</Note>\n\n---\n\n### 사전 렌더링 중단하기 {/*aborting-prerendering*/}\n\n타임아웃 이후 사전 렌더링을 \"포기\"하도록 강제할 수 있습니다.\n\n```js {2-5,11}\nasync function renderToString() {\n  const controller = new AbortController();\n  setTimeout(() => {\n    controller.abort()\n  }, 10000);\n\n  try {\n    // Prelude에는 컨트롤러가 중단하기 전에\n    // 사전렌더링된 모든 HTML이 포함됩니다.\n    const {prelude} = await prerenderToNodeStream(<App />, {\n      signal: controller.signal,\n    });\n    //...\n```\n\n불완전한 자식을 가진 모든 Suspense 경계는 풀백 상태로 Prelude에 포함됩니다.\n\nThis can be used for partial prerendering together with [`resumeToPipeableStream`](/reference/react-dom/server/resumeToPipeableStream) or [`resumeAndPrerenderToNodeStream`](/reference/react-dom/static/resumeAndPrerenderToNodeStream).\n\n## 문제 해결 {/*troubleshooting*/}\n\n### 전체 앱이 렌더링될 때까지 스트림이 시작되지 않았습니다. {/*my-stream-doesnt-start-until-the-entire-app-is-rendered*/}\n\n`prerenderToNodeStream` 응답은 모든 Suspense 경계가 해결될 때까지 기다리는 것을 포함하여 전체 앱이 렌더링이 완료될 때까지 기다린 후 완료됩니다. 이 API는 정적 사이트 생성(SSG)을 위해 설계되었으며 콘텐츠가 로드되면서 더 많은 콘텐츠를 스트리밍하는 것을 지원하지 않습니다.\n\n콘텐츠가 로드되면서 스트리밍하려면 [`renderToPipeableStream`](/reference/react-dom/server/renderToPipeableStream)과 같은 스트리밍 서버 렌더링 API를 사용하세요.\n"
  },
  {
    "path": "src/content/reference/react-dom/static/resumeAndPrerender.md",
    "content": "---\ntitle: resumeAndPrerender\n---\n\n<Intro>\n\n`resumeAndPrerender` continues a prerendered React tree to a static HTML string using a [Web Stream](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API).\n\n```js\nconst { prelude,postpone } = await resumeAndPrerender(reactNode, postponedState, options?)\n```\n\n</Intro>\n\n<InlineToc />\n\n<Note>\n\nThis API depends on [Web Streams.](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API) For Node.js, use [`resumeAndPrerenderToNodeStream`](/reference/react-dom/static/resumeAndPrerenderToNodeStream) instead.\n\n</Note>\n\n---\n\n## Reference {/*reference*/}\n\n### `resumeAndPrerender(reactNode, postponedState, options?)` {/*resumeandprerender*/}\n\nCall `resumeAndPrerender` to continue a prerendered React tree to a static HTML string.\n\n```js\nimport { resumeAndPrerender } from 'react-dom/static';\nimport { getPostponedState } from 'storage';\n\nasync function handler(request, response) {\n  const postponedState = getPostponedState(request);\n  const { prelude } = await resumeAndPrerender(<App />, postponedState, {\n    bootstrapScripts: ['/main.js']\n  });\n  return new Response(prelude, {\n    headers: { 'content-type': 'text/html' },\n  });\n}\n```\n\nOn the client, call [`hydrateRoot`](/reference/react-dom/client/hydrateRoot) to make the server-generated HTML interactive.\n\n[See more examples below.](#usage)\n\n#### Parameters {/*parameters*/}\n\n* `reactNode`: The React node you called `prerender` (or a previous `resumeAndPrerender`) with. For example, a JSX element like `<App />`. It is expected to represent the entire document, so the `App` component should render the `<html>` tag.\n* `postponedState`: The opaque `postpone` object returned from a [prerender API](/reference/react-dom/static/index), loaded from wherever you stored it (e.g. redis, a file, or S3).\n* **optional** `options`: An object with streaming options.\n  * **optional** `signal`: An [abort signal](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) that lets you [abort server rendering](#aborting-server-rendering) and render the rest on the client.\n  * **optional** `onError`: A callback that fires whenever there is a server error, whether [recoverable](#recovering-from-errors-outside-the-shell) or [not.](#recovering-from-errors-inside-the-shell) By default, this only calls `console.error`. If you override it to [log crash reports,](#logging-crashes-on-the-server) make sure that you still call `console.error`.\n\n#### Returns {/*returns*/}\n\n`prerender` returns a Promise:\n- If rendering the is successful, the Promise will resolve to an object containing:\n  - `prelude`: a [Web Stream](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API) of HTML. You can use this stream to send a response in chunks, or you can read the entire stream into a string.\n  - `postponed`: an JSON-serializeable, opaque object that can be passed to [`resume`](/reference/react-dom/server/resume) or [`resumeAndPrerender`](/reference/react-dom/static/resumeAndPrerender) if `prerender` is aborted.\n- If rendering fails, the Promise will be rejected. [Use this to output a fallback shell.](/reference/react-dom/server/renderToReadableStream#recovering-from-errors-inside-the-shell)\n\n#### Caveats {/*caveats*/}\n\n`nonce` is not an available option when prerendering. Nonces must be unique per request and if you use nonces to secure your application with [CSP](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP) it would be inappropriate and insecure to include the nonce value in the prerender itself.\n\n<Note>\n\n### When should I use `resumeAndPrerender`? {/*when-to-use-prerender*/}\n\nThe static `resumeAndPrerender` API is used for static server-side generation (SSG). Unlike `renderToString`, `resumeAndPrerender` waits for all data to load before resolving. This makes it suitable for generating static HTML for a full page, including data that needs to be fetched using Suspense. To stream content as it loads, use a streaming server-side render (SSR) API like [renderToReadableStream](/reference/react-dom/server/renderToReadableStream).\n\n`resumeAndPrerender` can be aborted and later either continued with another `resumeAndPrerender` or resumed with `resume` to support partial pre-rendering.\n\n</Note>\n\n---\n\n## Usage {/*usage*/}\n\n### Further reading {/*further-reading*/}\n\n`resumeAndPrerender` behaves similarly to [`prerender`](/reference/react-dom/static/prerender) but can be used to continue a previously started prerendering process that was aborted.\nFor more information about resuming a prerendered tree, see the [resume documentation](/reference/react-dom/server/resume#resuming-a-prerender).\n"
  },
  {
    "path": "src/content/reference/react-dom/static/resumeAndPrerenderToNodeStream.md",
    "content": "---\ntitle: resumeAndPrerenderToNodeStream\n---\n\n<Intro>\n\n`resumeAndPrerenderToNodeStream` continues a prerendered React tree to a static HTML string using a a [Node.js Stream.](https://nodejs.org/api/stream.html).\n\n```js\nconst {prelude, postponed} = await resumeAndPrerenderToNodeStream(reactNode, postponedState, options?)\n```\n\n</Intro>\n\n<InlineToc />\n\n<Note>\n\nThis API is specific to Node.js. Environments with [Web Streams,](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API) like Deno and modern edge runtimes, should use [`prerender`](/reference/react-dom/static/prerender) instead.\n\n</Note>\n\n---\n\n## Reference {/*reference*/}\n\n### `resumeAndPrerenderToNodeStream(reactNode, postponedState, options?)` {/*resumeandprerendertolnodestream*/}\n\nCall `resumeAndPrerenderToNodeStream` to continue a prerendered React tree to a static HTML string.\n\n```js\nimport { resumeAndPrerenderToNodeStream } from 'react-dom/static';\nimport { getPostponedState } from 'storage';\n\nasync function handler(request, writable) {\n  const postponedState = getPostponedState(request);\n  const { prelude } = await resumeAndPrerenderToNodeStream(<App />, JSON.parse(postponedState));\n  prelude.pipe(writable);\n}\n```\n\nOn the client, call [`hydrateRoot`](/reference/react-dom/client/hydrateRoot) to make the server-generated HTML interactive.\n\n[See more examples below.](#usage)\n\n#### Parameters {/*parameters*/}\n\n* `reactNode`: The React node you called `prerender` (or a previous `resumeAndPrerenderToNodeStream`) with. For example, a JSX element like `<App />`. It is expected to represent the entire document, so the `App` component should render the `<html>` tag.\n* `postponedState`: The opaque `postpone` object returned from a [prerender API](/reference/react-dom/static/index), loaded from wherever you stored it (e.g. redis, a file, or S3).\n* **optional** `options`: An object with streaming options.\n  * **optional** `signal`: An [abort signal](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) that lets you [abort server rendering](#aborting-server-rendering) and render the rest on the client.\n  * **optional** `onError`: A callback that fires whenever there is a server error, whether [recoverable](#recovering-from-errors-outside-the-shell) or [not.](#recovering-from-errors-inside-the-shell) By default, this only calls `console.error`. If you override it to [log crash reports,](#logging-crashes-on-the-server) make sure that you still call `console.error`.\n\n#### Returns {/*returns*/}\n\n`resumeAndPrerenderToNodeStream` returns a Promise:\n- If rendering the is successful, the Promise will resolve to an object containing:\n  - `prelude`: a [Web Stream](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API) of HTML. You can use this stream to send a response in chunks, or you can read the entire stream into a string.\n  - `postponed`: an JSON-serializeable, opaque object that can be passed to [`resumeToNodeStream`](/reference/react-dom/server/resume) or [`resumeAndPrerenderToNodeStream`](/reference/react-dom/static/resumeAndPrerenderToNodeStream) if `resumeAndPrerenderToNodeStream` is aborted.\n- If rendering fails, the Promise will be rejected. [Use this to output a fallback shell.](/reference/react-dom/server/renderToReadableStream#recovering-from-errors-inside-the-shell)\n\n#### Caveats {/*caveats*/}\n\n`nonce` is not an available option when prerendering. Nonces must be unique per request and if you use nonces to secure your application with [CSP](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP) it would be inappropriate and insecure to include the nonce value in the prerender itself.\n\n<Note>\n\n### When should I use `resumeAndPrerenderToNodeStream`? {/*when-to-use-prerender*/}\n\nThe static `resumeAndPrerenderToNodeStream` API is used for static server-side generation (SSG). Unlike `renderToString`, `resumeAndPrerenderToNodeStream` waits for all data to load before resolving. This makes it suitable for generating static HTML for a full page, including data that needs to be fetched using Suspense. To stream content as it loads, use a streaming server-side render (SSR) API like [renderToReadableStream](/reference/react-dom/server/renderToReadableStream).\n\n`resumeAndPrerenderToNodeStream` can be aborted and later either continued with another `resumeAndPrerenderToNodeStream` or resumed with `resume` to support partial pre-rendering.\n\n</Note>\n\n---\n\n## Usage {/*usage*/}\n\n### Further reading {/*further-reading*/}\n\n`resumeAndPrerenderToNodeStream` behaves similarly to [`prerender`](/reference/react-dom/static/prerender) but can be used to continue a previously started prerendering process that was aborted.\nFor more information about resuming a prerendered tree, see the [resume documentation](/reference/react-dom/server/resume#resuming-a-prerender).\n\n"
  },
  {
    "path": "src/content/reference/rsc/directives.md",
    "content": "---\ntitle: \"지시어\"\n---\n\n<RSC>\n\n지시어는 [React 서버 컴포넌트](/reference/rsc/server-components)에서 사용합니다.\n\n</RSC>\n\n<Intro>\n\n지시어는 [React 서버 컴포넌트와 호환되는 번들러](/learn/creating-a-react-app#full-stack-frameworks)에게 지시사항을 제공합니다.\n\n</Intro>\n\n---\n\n## 소스 코드 지시어 {/*source-code-directives*/}\n\n* [`'use client'`](/reference/rsc/use-client)를 사용하면 클라이언트에서 실행되는 코드를 표시할 수 있습니다.\n* [`'use server'`](/reference/rsc/use-server)는 클라이언트 측 코드에서 호출할 수 있는 서버 측 함수를 표시합니다.\n"
  },
  {
    "path": "src/content/reference/rsc/server-components.md",
    "content": "---\ntitle: 서버 컴포넌트\n---\n\n<Intro>\n\n서버 컴포넌트는 번들링 전에 클라이언트 앱이나 SSR(Server Side Rendering) 서버와는 분리된 환경에서 미리 렌더링되는 새로운 유형의 컴포넌트입니다.\n\n</Intro>\n\n이 별도의 환경이 바로 React 서버 컴포넌트에서의 \"서버\"입니다. 서버 컴포넌트는 빌드 시간에 CI 서버에서 한 번 실행되거나, 각 요청마다 웹 서버를 통해 실행될 수 있습니다.\n\n<InlineToc />\n\n<Note>\n\n#### 서버 컴포넌트를 지원하려면 어떻게 해야 하나요? {/*how-do-i-build-support-for-server-components*/}\n\nReact 19의 서버 컴포넌트는 안정적이며 마이너<sup>Minor</sup> 버전 간에는 변경되지 않습니다. 그러나 React 서버 컴포넌트 번들러나 프레임워크를 구현하는 데 사용되는 기본 API는 시맨틱 버전<sup>SemVer</sup>을 따르지 않으며 React 19.x의 마이너<sup>Minor</sup> 버전 간에 변경될 수 있습니다.\n\nReact 서버 컴포넌트를 번들러나 프레임워크로 지원하려면, 특정 React 버전에 고정하거나 Canary 릴리즈를 사용하는 것을 권장합니다. 향후 React 서버 컴포넌트를 구현하는 데 사용되는 API를 안정화하기 위해 번들러 및 프레임워크와 계속 협력할 것입니다.\n\n</Note>\n\n### 서버 없이 서버 컴포넌트 사용하기 {/*server-components-without-a-server*/}\n서버 컴포넌트는 빌드 시간에 파일 시스템을 읽거나 정적 콘텐츠를 가져올 수 있으므로 웹 서버가 필요하지 않습니다. 예를 들어, 콘텐츠 관리 시스템<sup>CMS</sup>에서 정적 데이터를 읽고 싶을 때 유용합니다.\n\n서버 컴포넌트 없이 클라이언트에서 Effect를 사용해 정적 데이터를 가져오는 일반적인 패턴은 다음과 같습니다.\n```js\n// bundle.js\nimport marked from 'marked'; // 35.9K (11.2K gzipped)\nimport sanitizeHtml from 'sanitize-html'; // 206K (63.3K gzipped)\n\nfunction Page({page}) {\n  const [content, setContent] = useState('');\n  // NOTE: loads *after* first page render.\n  useEffect(() => {\n    fetch(`/api/content/${page}`).then((data) => {\n      setContent(data.content);\n    });\n  }, [page]);\n\n  return <div>{sanitizeHtml(marked(content))}</div>;\n}\n```\n```js\n// api.js\napp.get(`/api/content/:page`, async (req, res) => {\n  const page = req.params.page;\n  const content = await file.readFile(`${page}.md`);\n  res.send({content});\n});\n```\n\n이 패턴은 사용자가 정적 콘텐츠를 렌더링하기 위해 페이지가 로드된 후 추가 75K (gzipped) 라이브러리를 다운로드하고 파싱해야 하며, 데이터를 가져오기 위한 두 번째 요청을 기다려야 합니다.\n\n서버 컴포넌트를 사용하면 이러한 컴포넌트를 빌드 시간에 한 번만 렌더링할 수 있습니다.\n\n```js\nimport marked from 'marked'; // Not included in bundle\nimport sanitizeHtml from 'sanitize-html'; // Not included in bundle\n\nasync function Page({page}) {\n  // NOTE: loads *during* render, when the app is built.\n  const content = await file.readFile(`${page}.md`);\n\n  return <div>{sanitizeHtml(marked(content))}</div>;\n}\n```\n\n렌더링된 출력은 서버 측 렌더링<sup>SSR</sup>을 통해 HTML로 변환되어 CDN에 업로드될 수 있습니다. 앱이 로드될 때, 클라이언트는 기존의 `Page` 컴포넌트나 마크다운 렌더링을 위한 고비용의 라이브러리를 보지 않게 됩니다. 클라이언트는 렌더링된 출력만 보게 됩니다.\n\n```js\n<div><!-- html for markdown --></div>\n```\n\n이렇게 하면 첫 페이지 로드 시 콘텐츠가 표시되고, 번들에 정적 콘텐츠를 렌더링하는 데 필요한 고비용의 라이브러리가 포함되지 않습니다.\n\n<Note>\n\n아래의 서버 컴포넌트는 비동기 함수임을 알 수 있습니다.\n\n```js\nasync function Page({page}) {\n  //...\n}\n```\n\n비동기 컴포넌트는 렌더링 중에 `await`을 사용할 수 있게 해주는 서버 컴포넌트의 새로운 기능입니다.\n\n자세한 내용은 아래의 [서버 컴포넌트와 함께 비동기 컴포넌트 사용하기](#async-components-with-server-components)를 참조하세요.\n\n</Note>\n\n### 서버와 함께 서버 컴포넌트 사용하기 {/*server-components-with-a-server*/}\n서버 컴포넌트는 웹 서버에서 페이지 요청 시 실행될 수 있어, API를 구축할 필요 없이 데이터 레이어에 접근할 수 있습니다. 이들은 애플리케이션이 번들링되기 전에 렌더링되며, 데이터와 JSX를 클라이언트 컴포넌트에 Props로 전달할 수 있습니다.\n\n서버 컴포넌트 없이 클라이언트에서 Effect를 사용해 동적 데이터를 가져오는 일반적인 패턴은 다음과 같습니다.\n\n```js\n// bundle.js\nfunction Note({id}) {\n  const [note, setNote] = useState('');\n  // NOTE: loads *after* first render.\n  useEffect(() => {\n    fetch(`/api/notes/${id}`).then(data => {\n      setNote(data.note);\n    });\n  }, [id]);\n\n  return (\n    <div>\n      <Author id={note.authorId} />\n      <p>{note}</p>\n    </div>\n  );\n}\n\nfunction Author({id}) {\n  const [author, setAuthor] = useState('');\n  // NOTE: loads *after* Note renders.\n  // Causing an expensive client-server waterfall.\n  useEffect(() => {\n    fetch(`/api/authors/${id}`).then(data => {\n      setAuthor(data.author);\n    });\n  }, [id]);\n\n  return <span>By: {author.name}</span>;\n}\n```\n```js\n// api\nimport db from './database';\n\napp.get(`/api/notes/:id`, async (req, res) => {\n  const note = await db.notes.get(id);\n  res.send({note});\n});\n\napp.get(`/api/authors/:id`, async (req, res) => {\n  const author = await db.authors.get(id);\n  res.send({author});\n});\n```\n\n서버 컴포넌트를 사용하면 데이터를 읽고 컴포넌트 내에서 렌더링할 수 있습니다.\n\n```js\nimport db from './database';\n\nasync function Note({id}) {\n  // NOTE: loads *during* render.\n  const note = await db.notes.get(id);\n  return (\n    <div>\n      <Author id={note.authorId} />\n      <p>{note}</p>\n    </div>\n  );\n}\n\nasync function Author({id}) {\n  // NOTE: loads *after* Note,\n  // but is fast if data is co-located.\n  const author = await db.authors.get(id);\n  return <span>By: {author.name}</span>;\n}\n```\n\n번들러는 데이터를 결합하여 서버 컴포넌트를 렌더링하고 동적 클라이언트 컴포넌트와 함께 번들을 만듭니다. 선택적으로 이 번들은 서버 측 렌더링(SSR)을 통해 페이지의 초기 HTML을 생성할 수 있습니다. 페이지가 로드될 때 브라우저는 기존의 `Note` 및 `Author` 컴포넌트를 보지 않고, 렌더링된 출력만 클라이언트에 전송합니다.\n\n```js\n<div>\n  <span>By: The React Team</span>\n  <p>React 19 is...</p>\n</div>\n```\n\n서버 컴포넌트는 서버에서 다시 페칭함으로써 데이터에 액세스하고 다시 렌더링하여 동적으로 만들 수 있습니다. 이 새로운 애플리케이션 아키텍처는 서버 중심의 다중 페이지 앱(Server-Centric Multi-Page Apps)의 간단한 \"request/response\" 모델과 클라이언트 중심의 단일 페이지 앱(Client-Centric Single-Page Apps)의 원활한 상호작용을 결합하여 두 가지 장점을 모두 제공합니다.\n\n### 서버 컴포넌트에 상호작용 추가하기 {/*adding-interactivity-to-server-components*/}\n\n서버 컴포넌트는 브라우저로 전송되지 않으므로 `useState`와 같은 상호작용 API를 사용할 수 없습니다. 서버 컴포넌트에 상호작용을 추가하려면 `\"use client\"` 지시어를 사용하여 클라이언트 컴포넌트와 함께 구성할 수 있습니다.\n\n<Note>\n\n#### 서버 컴포넌트에 대한 지시어는 없습니다. {/*there-is-no-directive-for-server-components*/}\n\n서버 컴포넌트는 `\"use server\"`로 표시된다는 오해가 있지만, 서버 컴포넌트에 대한 지시어는 없습니다. `\"use server\"` 지시어는 서버 함수에 사용됩니다.\n\n자세한 내용은 [지시어](/reference/rsc/directives)를 참조하세요.\n\n</Note>\n\n\n다음 예시에서는 `Notes` 서버 컴포넌트가 State를 사용하여 `expanded` 상태를 토글하는 `Expandable` 클라이언트 컴포넌트를 가져옵니다.\n```js\n// Server Component\nimport Expandable from './Expandable';\n\nasync function Notes() {\n  const notes = await db.notes.getAll();\n  return (\n    <div>\n      {notes.map(note => (\n        <Expandable key={note.id}>\n          <p note={note} />\n        </Expandable>\n      ))}\n    </div>\n  )\n}\n```\n```js\n// Client Component\n\"use client\"\n\nexport default function Expandable({children}) {\n  const [expanded, setExpanded] = useState(false);\n  return (\n    <div>\n      <button\n        onClick={() => setExpanded(!expanded)}\n      >\n        Toggle\n      </button>\n      {expanded && children}\n    </div>\n  )\n}\n```\n\n이 예시는 먼저 `Notes`를 서버 컴포넌트로 렌더링한 다음 번들러에 `Expandable` 클라이언트 컴포넌트의 번들을 생성하도록 지시합니다. 브라우저에서는 클라이언트 컴포넌트가 서버 컴포넌트의 출력을 Props로 받게 됩니다.\n\n```js\n<head>\n  <!-- the bundle for Client Components -->\n  <script src=\"bundle.js\" />\n</head>\n<body>\n  <div>\n    <Expandable key={1}>\n      <p>this is the first note</p>\n    </Expandable>\n    <Expandable key={2}>\n      <p>this is the second note</p>\n    </Expandable>\n    <!--...-->\n  </div>\n</body>\n```\n\n### 서버 컴포넌트와 함께 비동기 컴포넌트 사용하기 {/*async-components-with-server-components*/}\n\n서버 컴포넌트는 `async`와 `await`을 사용하는 새로운 방법을 소개합니다. 비동기 컴포넌트에서 `await`을 사용할 때, React는 렌더링을 지연<sup>Suspend</sup>하고 Promise가 해결될 때까지 기다린 후 렌더링을 다시 시작합니다. 이는 서버와 클라이언트의 경계를 넘어 동작하며, Suspense에 대한 스트리밍을 지원합니다.\n\n심지어 서버에서 Promise를 생성하고 클라이언트에서 이를 기다릴 수 있습니다.\n\n```js\n// Server Component\nimport db from './database';\n\nasync function Page({id}) {\n  // Will suspend the Server Component.\n  const note = await db.notes.get(id);\n\n  // NOTE: not awaited, will start here and await on the client.\n  const commentsPromise = db.comments.get(note.id);\n  return (\n    <div>\n      {note}\n      <Suspense fallback={<p>Loading Comments...</p>}>\n        <Comments commentsPromise={commentsPromise} />\n      </Suspense>\n    </div>\n  );\n}\n```\n\n```js\n// Client Component\n\"use client\";\nimport {use} from 'react';\n\nfunction Comments({commentsPromise}) {\n  // NOTE: this will resume the promise from the server.\n  // It will suspend until the data is available.\n  const comments = use(commentsPromise);\n  return comments.map(comment => <p>{comment}</p>);\n}\n```\n\n`note` 콘텐츠는 페이지 렌더링에 중요한 데이터이므로 서버에서 `await` 합니다. 댓글은 중요도가 낮아 페이지 아래에 표시되므로 서버에서 Promise를 시작하고 클라이언트에서 `use` API를 사용하여 기다립니다. 이는 클라이언트에서 지연되지만 `note` 콘텐츠가 렌더링되는 것을 차단하지 않습니다.\n\n비동기 컴포넌트는 클라이언트에서 지원되지 않으므로 Promise를 `use`로 기다립니다.\n"
  },
  {
    "path": "src/content/reference/rsc/server-functions.md",
    "content": "---\ntitle: 서버 함수\n---\n\n<RSC>\n\n서버 함수는 [React 서버 컴포넌트](/reference/rsc/server-components)에서 사용합니다.\n\n**참고:** 2024년 9월까지, 우리는 모든 서버 함수를 \"서버 액션\"으로 불렀습니다. 만약 서버 함수를 action prop으로 전달하거나 action 내부에서 호출된다면 이는 서버 액션이지만, 모든 서버 함수가 서버 액션은 아닙니다. 이 문서의 명명 규칙은 서버 함수가 여러 용도로 사용될 수 있다는 점을 반영하여 업데이트했습니다.\n\n</RSC>\n\n<Intro>\n\n서버 함수<sup>Server Functions</sup>를 사용하면 클라이언트 컴포넌트가 서버에서 실행되는 비동기 함수를 호출할 수 있습니다.\n\n</Intro>\n\n<InlineToc />\n\n<Note>\n\n#### 서버 함수를 지원하려면 어떻게 해야 하나요? {/*how-do-i-build-support-for-server-functions*/}\n\nReact 19의 서버 함수는 안정적이며 마이너<sup>Minor</sup> 버전 간에는 변경되지 않습니다. 그러나 React 서버 컴포넌트 번들러나 프레임워크에서 서버 함수를 구현하는 데 사용되는 기본 API는 유의적 버전<sup>SemVer</sup>을 따르지 않으며 React 19.x의 마이너<sup>Minor</sup> 버전 간에 변경될 수 있습니다.\n\n서버 함수를 번들러나 프레임워크로 지원하려면, 특정 React 버전에 고정하거나 Canary 릴리즈를 사용하는 것을 권장합니다. 향후 서버 함수를 구현하는 데 사용되는 API를 안정화하기 위해 번들러 및 프레임워크와 계속 협력할 것입니다.\n\n</Note>\n\n서버 함수가 [`\"use server\"`](/reference/rsc/use-server) 지시어로 정의되면, 프레임워크는 자동으로 서버 함수에 대한 참조를 생성하고 해당 참조를 클라이언트 컴포넌트에 전달합니다. 클라이언트에서 해당 함수를 호출하면, React는 서버에 함수를 실행하라는 요청<sup>Request</sup>을 보내고 결과를 반환합니다.\n\n서버 함수는 서버 컴포넌트에서 생성하여 클라이언트 컴포넌트에 Props로 전달할 수 있으며, 클라이언트 컴포넌트에서 가져와서 사용할 수도 있습니다.\n\n## 사용법 {/*usage*/}\n\n### 서버 컴포넌트에서 서버 함수 만들기 {/*creating-a-server-function-from-a-server-component*/}\n\n서버 컴포넌트는 `\"use server\"` 지시어로 서버 함수를 정의할 수 있습니다.\n\n```js [[2, 7, \"'use server'\"], [1, 5, \"createNote\"], [1, 12, \"createNote\"]]\n// 서버 컴포넌트\nimport Button from './Button';\n\nfunction EmptyNote () {\n  async function createNote() {\n    // 서버 함수\n    'use server';\n\n    await db.notes.create();\n  }\n\n  return <Button onClick={createNote}/>;\n}\n```\n\nReact가 `EmptyNote` 서버 컴포넌트를 렌더링할 때, `createNoteAction` 함수에 대한 참조를 생성하고, 그 참조를 `Button` 클라이언트 컴포넌트에 전달합니다. 버튼을 클릭하면, React는 제공된 참조를 통해 `createNoteAction` 함수를 실행하도록 서버에 요청<sup>Request</sup>을 보냅니다.\n\n```js {5}\n\"use client\";\n\nexport default function Button({onClick}) {\n  console.log(onClick);\n  // {$$typeof: Symbol.for(\"react.server.reference\"), $$id: 'createNoteAction'}\n  return <button onClick={() => onClick()}>Create Empty Note</button>\n}\n```\n\n자세한 내용은 [`\"use server\"`](/reference/rsc/use-server) 문서를 참조하세요.\n\n\n### 클라이언트 컴포넌트에서 서버 함수 가져오기 {/*importing-server-functions-from-client-components*/}\n\n클라이언트 컴포넌트는 `\"use server\"` 지시어를 사용하는 파일에서 서버 함수를 가져올 수 있습니다.\n\n```js [[1, 3, \"createNote\"]]\n\"use server\";\n\nexport async function createNote() {\n  await db.notes.create();\n}\n\n```\n\n번들러가 `EmptyNote` 클라이언트 컴포넌트를 빌드할 때, 번들에서 `createNote` 함수에 대한 참조를 생성합니다. 버튼을 클릭하면, React는 제공된 참조를 통해 `createNote` 함수를 실행하도록 서버에 요청<sup>Request</sup>을 보냅니다.\n\n```js [[1, 3, \"createNote\"], [1, 6, \"createNote\"], [1, 8, \"createNote\"]]\n\"use client\";\n\nimport {createNote} from './actions';\n\nfunction EmptyNote() {\n  console.log(createNote);\n  // {$$typeof: Symbol.for(\"react.server.reference\"), $$id: 'createNote'}\n  <button onClick={() => createNote()} />\n}\n```\n\n자세한 내용은 [`\"use server\"`](/reference/rsc/use-server) 문서를 참조하세요.\n\n### 액션으로 서버 함수 구성하기 {/*server-functions-with-actions*/}\n\n서버 함수는 클라이언트의 액션<sup>Action</sup>과 함께 구성할 수 있습니다.\n\n```js [[1, 3, \"updateName\"]]\n\"use server\";\n\nexport async function updateName(name) {\n  if (!name) {\n    return {error: 'Name is required'};\n  }\n  await db.users.updateName(name);\n}\n```\n\n```js [[1, 3, \"updateName\"], [1, 13, \"updateName\"], [2, 11, \"submitAction\"],  [2, 23, \"submitAction\"]]\n\"use client\";\n\nimport {updateName} from './actions';\n\nfunction UpdateName() {\n  const [name, setName] = useState('');\n  const [error, setError] = useState(null);\n\n  const [isPending, startTransition] = useTransition();\n\n  const submitAction = async () => {\n    startTransition(async () => {\n      const {error} = await updateName(name);\n      if (error) {\n        setError(error);\n      } else {\n        setName('');\n      }\n    })\n  }\n\n  return (\n    <form action={submitAction}>\n      <input type=\"text\" name=\"name\" disabled={isPending}/>\n      {error && <span>Failed: {error}</span>}\n    </form>\n  )\n}\n```\n\n이렇게 하면 클라이언트의 액션으로 래핑하여 서버 함수의 `isPending` 상태에 접근할 수 있습니다.\n\n자세한 내용은 [`<form>` 외부에서 서버 함수 호출하기](/reference/rsc/use-server#calling-a-server-function-outside-of-form) 문서를 참조하세요.\n\n### 서버 함수를 사용한 폼 액션 {/*using-server-functions-with-form-actions*/}\n\n서버 함수는 React 19의 새로운 폼<sup>Form</sup> 기능과 함께 동작합니다.\n\n서버 함수를 폼에 전달하여 폼을 서버에 자동으로 제출할 수 있습니다.\n\n\n```js [[1, 3, \"updateName\"], [1, 7, \"updateName\"]]\n\"use client\";\n\nimport {updateName} from './actions';\n\nfunction UpdateName() {\n  return (\n    <form action={updateName}>\n      <input type=\"text\" name=\"name\" />\n    </form>\n  )\n}\n```\n\n폼 제출이 성공하면, React는 자동으로 폼을 재설정합니다. `useActionState`를 추가하여 대기<sup>Pending</sup> 상태 혹은 마지막 응답<sup>Response</sup>에 접근하거나, 점진적 향상을 지원할 수 있습니다.\n\n자세한 내용은 [폼<sup>Form</sup>에서의 서버 함수](/reference/rsc/use-server#server-functions-in-forms) 문서를 참조하세요.\n\n### `useActionState`를 사용한 서버 함수 {/*server-functions-with-use-action-state*/}\n\n액션 대기<sup>Pending</sup> 상태와 마지막으로 반환된 응답<sup>Response</sup>에 접근하는 일반적인 경우에는 `useActionState`를 사용하여 서버 함수를 호출할 수 있습니다.\n\n```js [[1, 3, \"updateName\"], [1, 6, \"updateName\"], [2, 6, \"submitAction\"], [2, 9, \"submitAction\"]]\n\"use client\";\n\nimport {updateName} from './actions';\n\nfunction UpdateName() {\n  const [state, submitAction, isPending] = useActionState(updateName, {error: null});\n\n  return (\n    <form action={submitAction}>\n      <input type=\"text\" name=\"name\" disabled={isPending}/>\n      {state.error && <span>Failed: {state.error}</span>}\n    </form>\n  );\n}\n```\n\n서버 함수 함께 `useActionState`를 사용하는 경우, React는 Hydration이 완료되기 전에 입력된 폼 제출을 자동으로 다시 실행합니다. 즉, 사용자는 앱이 Hydration 되기 전에도 앱과 상호작용을 할 수 있습니다.\n\n자세한 내용은 [`useActionState`](/reference/react/useActionState) 문서를 참조하세요.\n\n### `useActionState`를 통한 점진적 향상 {/*progressive-enhancement-with-useactionstate*/}\n\n서버 함수는 `useActionState`의 세 번째 인수를 통한 점진적 향상도 지원합니다.\n\n```js [[1, 3, \"updateName\"], [1, 6, \"updateName\"], [2, 6, \"/name/update\"], [3, 6, \"submitAction\"], [3, 9, \"submitAction\"]]\n\"use client\";\n\nimport {updateName} from './actions';\n\nfunction UpdateName() {\n  const [, submitAction] = useActionState(updateName, null, `/name/update`);\n\n  return (\n    <form action={submitAction}>\n      ...\n    </form>\n  );\n}\n```\n\n<CodeStep step={2}>permalink</CodeStep>가 `useActionState`에 제공될 때, 자바스크립트 번들이 로드되기 전에 폼이 제출되면 React는 제공된 URL로 리디렉션합니다.\n\n자세한 내용은 [`useActionState`](/reference/react/useActionState) 문서를 참조하세요.\n"
  },
  {
    "path": "src/content/reference/rsc/use-client.md",
    "content": "---\ntitle: \"'use client'\"\ntitleForTitleTag: \"'use client' 지시어\"\n---\n\n<RSC>\n\n`'use client'`는 [React 서버 컴포넌트](/reference/rsc/server-components)와 함께 사용합니다.\n\n</RSC>\n\n\n<Intro>\n\n`'use client'`를 사용하여 클라이언트에서 실행되는 코드를 표시합니다.\n\n</Intro>\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `'use client'` {/*use-client*/}\n\n파일의 최상단에 `'use client'`를 추가하여 모듈과 해당 모듈의 전이적 의존성을 클라이언트 코드로 표시하세요.\n\n```js {1}\n'use client';\n\nimport { useState } from 'react';\nimport { formatDate } from './formatters';\nimport Button from './button';\n\nexport default function RichTextEditor({ timestamp, text }) {\n  const date = formatDate(timestamp);\n  // ...\n  const editButton = <Button />;\n  // ...\n}\n```\n\n서버 컴포넌트에서 `'use client'`라 표시된 파일을 가져오면 [호환되는 번들러](/learn/creating-a-react-app#full-stack-frameworks)는 모듈 불러오기<sup>Module Import</sup>를 서버 실행 코드와 클라이언트 실행 코드 사이의 경계로 처리합니다.\n\n`RichTextEditor`의 의존성으로 인하여, `formatDate`와 `Button`의 모듈에 `'use client'` 지시어가 포함되어 있지 않더라도 클라이언트에서 평가됩니다. 하나의 모듈이 서버 코드에서 가져올 때는 서버에서, 클라이언트 코드에서 가져올 때는 클라이언트에서 평가될 수 있음을 유의해야 합니다.\n\n#### 주의 사항 {/*caveats*/}\n\n* `'use client'`는 파일의 맨 처음에 있어야 하며, 다른 코드나 import 문보다 위에 있어야 합니다. (주석은 괜찮습니다.) 작은따옴표나 큰따옴표로 작성해야 하며 백틱은 사용할 수 없습니다.\n* `'use client'` 모듈을 다른 클라이언트 렌더링 모듈에서 가져오면 지시어가 동작하지 않습니다.\n* 컴포넌트 모듈에 `'use client'` 지시어가 포함된 경우 해당 컴포넌트의 사용은 클라이언트 컴포넌트임이 보장됩니다. 하지만 컴포넌트에 `'use client'` 지시어가 없더라도 클라이언트에서 평가될 수 있습니다.\n  * 컴포넌트 사용은 `'use client'` 지시어가 포함된 모듈에 정의되어 있거나 `'use client'` 지시어를 포함한 모듈의 전이적 의존성일 경우 클라이언트 컴포넌트로 간주합니다. 그렇지 않으면 서버 컴포넌트로 간주합니다.\n* 클라이언트 평가로 표시된 코드는 컴포넌트에만 국한되지 않습니다. 클라이언트 모듈 하위 트리의 모든 코드는 클라이언트에 전송되어 클라이언트에서 실행됩니다.\n* 서버 평가 모듈이 `'use client'` 모듈에서 값을 가져올 때, 그 값은 React 컴포넌트이거나 클라이언트 컴포넌트에 전달될 수 있는 [지원되는 직렬화 가능한 prop 값](#passing-props-from-server-to-client-components)이어야 합니다.\n\n### `'use client'`가 클라이언트 코드를 표시하는 방법 {/*how-use-client-marks-client-code*/}\n\nReact 앱에서 컴포넌트는 종종 별도의 파일 또는 [모듈](/learn/importing-and-exporting-components#exporting-and-importing-a-component)로 분리됩니다.\n\nReact 서버 컴포넌트를 사용하는 앱의 경우, 기본적으로 앱은 서버에서 렌더링됩니다. `'use client'`는 [모듈 의존성 트리](/learn/understanding-your-ui-as-a-tree#the-module-dependency-tree)에 서버-클라이언트 경계를 도입하여 효과적으로 클라이언트 모듈의 하위 트리를 만듭니다.\n\n이를 더 잘 설명하기 위해 다음과 같은 React 서버 컴포넌트 앱을 고려해 보세요.\n\n<Sandpack>\n\n```js src/App.js\nimport FancyText from './FancyText';\nimport InspirationGenerator from './InspirationGenerator';\nimport Copyright from './Copyright';\n\nexport default function App() {\n  return (\n    <>\n      <FancyText title text=\"Get Inspired App\" />\n      <InspirationGenerator>\n        <Copyright year={2004} />\n      </InspirationGenerator>\n    </>\n  );\n}\n\n```\n\n```js src/FancyText.js\nexport default function FancyText({title, text}) {\n  return title\n    ? <h1 className='fancy title'>{text}</h1>\n    : <h3 className='fancy cursive'>{text}</h3>\n}\n```\n\n```js src/InspirationGenerator.js\n'use client';\n\nimport { useState } from 'react';\nimport inspirations from './inspirations';\nimport FancyText from './FancyText';\n\nexport default function InspirationGenerator({children}) {\n  const [index, setIndex] = useState(0);\n  const quote = inspirations[index];\n  const next = () => setIndex((index + 1) % inspirations.length);\n\n  return (\n    <>\n      <p>Your inspirational quote is:</p>\n      <FancyText text={quote} />\n      <button onClick={next}>Inspire me again</button>\n      {children}\n    </>\n  );\n}\n```\n\n```js src/Copyright.js\nexport default function Copyright({year}) {\n  return <p className='small'>©️ {year}</p>;\n}\n```\n\n```js src/inspirations.js\nexport default [\n  \"Don’t let yesterday take up too much of today.” — Will Rogers\",\n  \"Ambition is putting a ladder against the sky.\",\n  \"A joy that's shared is a joy made double.\",\n];\n```\n\n```css\n.fancy {\n  font-family: 'Georgia';\n}\n.title {\n  color: #007AA3;\n  text-decoration: underline;\n}\n.cursive {\n  font-style: italic;\n}\n.small {\n  font-size: 10px;\n}\n```\n\n</Sandpack>\n\n예시 앱의 모듈 의존성 트리에서 `InspirationGenerator.js`의 `'use client'` 지시어는 해당 모듈과 모든 전이적 의존성을 클라이언트 모듈로 표시합니다. 이제 `InspirationGenerator.js`에서 시작하는 하위 트리는 클라이언트 모듈로 표시됩니다.\n\n<Diagram name=\"use_client_module_dependency\" height={250} width={545} alt=\"A tree graph with the top node representing the module 'App.js'. 'App.js' has three children: 'Copyright.js', 'FancyText.js', and 'InspirationGenerator.js'. 'InspirationGenerator.js' has two children: 'FancyText.js' and 'inspirations.js'. The nodes under and including 'InspirationGenerator.js' have a yellow background color to signify that this sub-graph is client-rendered due to the 'use client' directive in 'InspirationGenerator.js'.\">\n`'use client'`는 React 서버 컴포넌트 앱의 모듈 의존성 트리를 분할하여 `InspirationGenerator.js`와 모든 의존성을 클라이언트-렌더링으로 표시합니다.\n</Diagram>\n\n렌더링하는 동안 프레임워크는 루트 컴포넌트를 서버-렌더링하고 [렌더 트리](/learn/understanding-your-ui-as-a-tree#the-render-tree)를 통해 계속 진행하여 클라이언트에서 가져온 코드를 평가하지 않습니다.\n\n그런 다음 서버에서 렌더링한 렌더 트리 부분을 클라이언트로 보냅니다. 클라이언트 코드를 다운로드한 클라이언트는 트리의 나머지 부분 렌더링을 완료합니다.\n\n<Diagram name=\"use_client_render_tree\" height={250} width={500} alt=\"A tree graph where each node represents a component and its children as child components. The top-level node is labelled 'App' and it has two child components 'InspirationGenerator' and 'FancyText'. 'InspirationGenerator' has two child components, 'FancyText' and 'Copyright'. Both 'InspirationGenerator' and its child component 'FancyText' are marked to be client-rendered.\">\nReact 서버 컴포넌트 앱을 위한 렌더 트리에서 `InspirationGenerator`와 그 자식 컴포넌트 `FancyText`는 클라이언트 표시 코드에서 내보낸 컴포넌트이며 클라이언트 컴포넌트로 간주합니다.\n</Diagram>\n\n다음 정의를 소개합니다.\n\n* **클라이언트 컴포넌트**는 클라이언트에서 렌더링되는 렌더 트리의 컴포넌트입니다.\n* **서버 컴포넌트**는 서버에서 렌더링 되는 렌더 트리의 컴포넌트입니다.\n\n예시 앱이 실행되는 동안 `App`, `FancyText` 및 `Copyright`는 모두 서버에서 렌더링되며 서버 컴포넌트로 간주합니다. `InspirationGenerator.js`와 그 전이적 의존성이 클라이언트 코드로 표시되므로 컴포넌트 `InspirationGenerator`와 그 자식 컴포넌트 `FancyText`는 클라이언트 컴포넌트입니다.\n\n<DeepDive>\n#### 어떻게 `FancyText`는 서버 컴포넌트이면서 클라이언트 컴포넌트인가요? {/*how-is-fancytext-both-a-server-and-a-client-component*/}\n\n위 정의에 따르면 `FancyText` 컴포넌트는 서버 컴포넌트이자 클라이언트 컴포넌트입니다. 어떻게 그럴 수 있을까요?\n\n우선, \"컴포넌트\"라는 용어가 그다지 정확하지 않다는 점을 분명히 해 두겠습니다. \"컴포넌트\"를 이해할 수 있는 두 가지 방법이 있습니다.\n\n1. \"컴포넌트\"는 **컴포넌트 정의**를 가리킬 수 있습니다. 대부분의 경우 이것은 함수일 것입니다.\n\n```js\n// This is a definition of a component\nfunction MyComponent() {\n  return <p>My Component</p>\n}\n```\n\n2. \"컴포넌트\"는 그 정의의 **컴포넌트 사용**을 참조할 수 있습니다.\n```js\nimport MyComponent from './MyComponent';\n\nfunction App() {\n  // This is a usage of a component\n  return <MyComponent />;\n}\n```\n\n개념을 설명할 때 종종 부정확성은 중요하지 않지만, 이 경우에는 중요합니다.\n\n서버 또는 클라이언트 컴포넌트에 대해 이야기할 때, 컴포넌트 사용을 언급하고 있습니다.\n\n* 컴포넌트가 `'use client'` 지시어가 있는 모듈에서 정의되었거나, 컴포넌트가 클라이언트 컴포넌트 내부에서 가져와<sup>Imported</sup> 호출된다면 해당 컴포넌트는 클라이언트 컴포넌트입니다.\n* 그렇지 않은 경우, 컴포넌트는 서버 컴포넌트입니다.\n\n\n<Diagram name=\"use_client_render_tree\" height={150} width={450} alt=\"A tree graph where each node represents a component and its children as child components. The top-level node is labelled 'App' and it has two child components 'InspirationGenerator' and 'FancyText'. 'InspirationGenerator' has two child components, 'FancyText' and 'Copyright'. Both 'InspirationGenerator' and its child component 'FancyText' are marked to be client-rendered.\">렌더 트리는 컴포넌트 사용을 보여줍니다.</Diagram>\n\n`FancyText`에 관한 질문으로 돌아가서 이 컴포넌트 정의에는 `'use client'` 지시어가 없으며 두 가지 사용 방법이 있습니다.\n\n`FancyText`를 `App`의 자식으로 사용하면 서버 컴포넌트로 사용할 수 있습니다. `FancyText`를 가져와서 `InspirationGenerator`에서 호출할 때 `InspirationGenerator`에 `'use client'` 지시어가 포함되어 있으므로 `FancyText`의 사용은 클라이언트 컴포넌트입니다.\n\n이는 `FancyText`의 컴포넌트 정의가 서버에서 평가될 뿐만 아니라, 클라이언트 컴포넌트로 사용되기 위해 클라이언트에 다운로드된다는 것을 의미합니다.\n\n</DeepDive>\n\n<DeepDive>\n\n#### 왜 `Copyright`가 서버 컴포넌트인가요? {/*why-is-copyright-a-server-component*/}\n\n`Copyright` 컴포넌트가 클라이언트 컴포넌트 `InspirationGenerator`의 자식으로 렌더링되기 때문에, 이것이 서버 컴포넌트라는 점이 놀랍게 느껴질 수 있습니다.\n\n`'use client'` 지시어는 (렌더 트리가 아닌) <em>모듈 의존성 트리</em>에서 서버와 클라이언트 코드 간의 경계를 정의한다는 점을 기억하세요.\n\n<Diagram name=\"use_client_module_dependency\" height={200} width={500} alt=\"A tree graph with the top node representing the module 'App.js'. 'App.js' has three children: 'Copyright.js', 'FancyText.js', and 'InspirationGenerator.js'. 'InspirationGenerator.js' has two children: 'FancyText.js' and 'inspirations.js'. The nodes under and including 'InspirationGenerator.js' have a yellow background color to signify that this sub-graph is client-rendered due to the 'use client' directive in 'InspirationGenerator.js'.\">\n`'use client'` 지시어는 모듈 의존성 트리에서 서버와 클라이언트 코드의 경계를 정의합니다.\n</Diagram>\n\n모듈 의존성 트리에서 `App.js`는 `Copyright.js` 모듈로부터 `Copyright`를 가져와 호출합니다. `Copyright.js`에는 `'use client'` 지시어가 없기 때문에 컴포넌트를 서버에서 렌더링합니다. `App`은 루트 컴포넌트로 서버에서 렌더링합니다.\n\n클라이언트 컴포넌트는 JSX를 props로 전달할 수 있기 때문에 서버 컴포넌트를 렌더링할 수 있습니다. 이 경우 `InspirationGenerator`는 `Copyright`를 [자식](/learn/passing-props-to-a-component#passing-jsx-as-children)으로 받습니다. 그러나 `InspirationGenerator` 모듈은 `Copyright` 모듈을 직접 가져오거나 컴포넌트를 호출하지 않으며 이 모든 작업은 `App`에 의해 실행됩니다. 실제로 `InspirationGenerator`가 렌더링을 시작하기 전에 `Copyright` 컴포넌트는 완전히 실행됩니다.\n\n중요한 점은 부모-자식 간의 렌더링 관계가 동일한 렌더링 환경을 보장하지 않는다는 것입니다.\n\n</DeepDive>\n\n### `'use client'`의 사용 시기 {/*when-to-use-use-client*/}\n\n`'use client'`를 사용하면 컴포넌트가 클라이언트 컴포넌트인지 확인할 수 있습니다. 서버 컴포넌트가 기본값이므로, 클라이언트에서 렌더링할 것을 표시해야 하는 시기를 결정하기 위해 서버 컴포넌트의 장단점을 간단히 살펴보겠습니다.\n\n간단히 설명하기 위해 서버 컴포넌트에 대해 이야기하지만, 서버에서 실행되는 앱의 모든 코드에는 동일한 원칙이 적용됩니다.\n\n#### 서버 컴포넌트의 장점 {/*advantages*/}\n* 서버 컴포넌트는 클라이언트에 전송되고 실행되는 코드의 양을 줄일 수 있습니다. 클라이언트 모듈만 클라이언트에서 번들링되고 평가됩니다.\n* 서버 컴포넌트는 서버에서 실행할 때 이점이 있습니다. 로컬 파일 시스템에 접근할 수 있으며 데이터 가져오기 및 네트워크 요청에 대한 짧은 지연 시간을 경험할 수 있습니다.\n\n#### 서버 컴포넌트의 한계 {/*limitations*/}\n* 클라이언트에서 이벤트 핸들러를 등록하고 트리거해야 하므로 서버 컴포넌트는 상호작용을 지원할 수 없습니다.\n  * 예를 들어 `onClick`과 같은 이벤트 핸들러는 클라이언트 컴포넌트에서만 정의할 수 있습니다.\n* 서버 컴포넌트는 대부분의 Hook을 사용할 수 없습니다.\n  * 서버 컴포넌트가 렌더링될 때, 그 출력은 기본적으로 클라이언트가 렌더링할 컴포넌트 목록입니다. 서버 컴포넌트는 렌더링 후 메모리에 유지되지 않으며 자체적인 State를 가질 수 없습니다.\n\n### 서버 컴포넌트에서 반환하는 직렬화 가능한 유형 {/*serializable-types*/}\n\nReact 앱에서와 같이 부모 컴포넌트는 자식 컴포넌트에 데이터를 전달합니다. 서로 다른 환경에서 렌더링되므로 서버 컴포넌트에서 클라이언트 컴포넌트로 데이터를 전달하는 것은 추가적인 고려 사항이 필요합니다.\n\n서버 컴포넌트에서 클라이언트 컴포넌트로 전달하는 Prop 값은 직렬화할 수 있어야 합니다.\n\n직렬화할 수 있는 Props는 다음과 같습니다.\n* 원시 자료형\n  * [string](https://developer.mozilla.org/ko/docs/Glossary/String)\n  * [number](https://developer.mozilla.org/ko/docs/Glossary/Number)\n  * [bigint](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/BigInt)\n  * [boolean](https://developer.mozilla.org/ko/docs/Glossary/Boolean)\n  * [undefined](https://developer.mozilla.org/ko/docs/Glossary/Undefined)\n  * [null](https://developer.mozilla.org/ko/docs/Glossary/Null)\n  * [symbol](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Symbol) ( [`Symbol.for`](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Symbol/for)를 통해 전역 심볼 레지스트리에 등록된 심볼만 해당)\n* 직렬화할 수 있는 값을 포함하는 이터러블\n  * [String](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/String)\n  * [Array](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array)\n  * [Map](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Map)\n  * [Set](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Set)\n  * [TypedArray](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/TypedArray) 및 [ArrayBuffer](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer)\n* [Date](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Date)\n* 일반 [객체](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Object) (직렬화할 수 있는 프로퍼티를 사용하여 [객체 이니셜라이저](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Operators/Object_initializer)로 생성된 객체)\n* [서버 함수](/reference/rsc/server-functions)로서의 함수\n* 클라이언트 또는 서버 컴포넌트 요소(JSX)\n* [Promise](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Promise)\n\n단, 다음은 지원되지 않습니다.\n* 클라이언트로 표시된 모듈에서 내보내지 않았거나 [`'use server'`](/reference/rsc/use-server)로 표시된 [함수](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Function)\n* [클래스](https://developer.mozilla.org/ko/docs/Learn/JavaScript/Objects/Classes_in_JavaScript)\n* 위에서 언급한 내장형 클래스의 인스턴스가 아닌 객체 혹은 [null 프로토타입](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Object#null-prototype_objects)을 가진 객체\n* 전역에 등록되지 않은 Symbol (예: `Symbol('my new symbol')`)\n\n\n## 사용법 {/*usage*/}\n\n### 상호작용 및 State를 가진 컴포넌트 구축 {/*building-with-interactivity-and-state*/}\n\n<Sandpack>\n\n```js src/App.js\n'use client';\n\nimport { useState } from 'react';\n\nexport default function Counter({initialValue = 0}) {\n  const [countValue, setCountValue] = useState(initialValue);\n  const increment = () => setCountValue(countValue + 1);\n  const decrement = () => setCountValue(countValue - 1);\n  return (\n    <>\n      <h2>Count Value: {countValue}</h2>\n      <button onClick={increment}>+1</button>\n      <button onClick={decrement}>-1</button>\n    </>\n  );\n}\n```\n\n</Sandpack>\n\n`Counter`에는 값을 증가시키거나 감소시키기 위해 `useState` Hook과 이벤트 핸들러가 모두 필요하므로 이 컴포넌트는 클라이언트 컴포넌트여야 하며 파일 최상단에 `'use client'` 지시어가 필요합니다.\n\n반면에 상호작용 없이 UI를 렌더링하는 컴포넌트는 클라이언트 컴포넌트일 필요가 없습니다.\n\n```js\nimport { readFile } from 'node:fs/promises';\nimport Counter from './Counter';\n\nexport default async function CounterContainer() {\n  const initialValue = await readFile('/path/to/counter_value');\n  return <Counter initialValue={initialValue} />\n}\n```\n\n예를 들어, `Counter`의 상위 컴포넌트인 `CounterContainer`는 상호작용이 없고 State를 사용하지 않기 때문에 `'use client'`를 사용할 필요가 없습니다. 또한 `CounterContainer`는 서버의 로컬 파일 시스템을 읽어야 하므로, 이것이 가능한 서버 컴포넌트여야만 합니다.\n\n서버나 클라이언트 전용 기능을 사용하지 않고 렌더링 위치에 구애받지 않는 컴포넌트도 있습니다. 앞서 예로 든 `FancyText`가 그러한 컴포넌트 중 하나입니다.\n\n```js\nexport default function FancyText({title, text}) {\n  return title\n    ? <h1 className='fancy title'>{text}</h1>\n    : <h3 className='fancy cursive'>{text}</h3>\n}\n```\n\n이 경우 `'use client'` 지시어를 추가하지 않으면 `FancyText`의 _산출물_(소스 코드가 아닌)이 서버 컴포넌트에서 참조될 때 브라우저로 전송됩니다. 앞서 Inspirations 앱 예시에서 보여준 것처럼 `FancyText`는 가져오고 사용되는 위치에 따라 서버 또는 클라이언트 컴포넌트로 사용됩니다.\n\n하지만 `FancyText`의 HTML 출력이 (의존성을 포함한) 소스 코드에 비해 크다면, 항상 클라이언트 컴포넌트로 강제하는 것이 더 효율적일 수 있습니다. 한 예로 긴 SVG 경로 문자열을 반환하는 컴포넌트를 클라이언트 컴포넌트로 강제하는 것이 더 효율적일 수 있는 것처럼 말입니다.\n\n### 클라이언트 API 사용 {/*using-client-apis*/}\n\nReact 앱에서는 웹 스토리지, 오디오 및 비디오 조작, 하드웨어 장치 등과 같은 [브라우저의 API](https://developer.mozilla.org/ko/docs/Web/API)를 포함한 클라이언트 전용 API를 사용할 수 있습니다.\n\n이 예시에서 컴포넌트는 [DOM API](https://developer.mozilla.org/ko/docs/Glossary/DOM)를 사용해 [`canvas`](https://developer.mozilla.org/ko/docs/Web/HTML/Element/canvas) 요소를 조작합니다. 이러한 API는 브라우저에서만 사용할 수 있으므로 클라이언트 컴포넌트로 표시되어야 합니다.\n\n```js\n'use client';\n\nimport {useRef, useEffect} from 'react';\n\nexport default function Circle() {\n  const ref = useRef(null);\n  useLayoutEffect(() => {\n    const canvas = ref.current;\n    const context = canvas.getContext('2d');\n    context.reset();\n    context.beginPath();\n    context.arc(100, 75, 50, 0, 2 * Math.PI);\n    context.stroke();\n  });\n  return <canvas ref={ref} />;\n}\n```\n\n### 서드파티 라이브러리 사용 {/*using-third-party-libraries*/}\n\nReact 앱에서는 서드파티 라이브러리를 활용하여 일반적인 UI 패턴이나 로직을 처리하는 경우가 많습니다.\n\n이러한 라이브러리들은 컴포넌트 Hook이나 클라이언트 API에 의존할 수 있습니다. 다음 React API를 사용하는 서드파티 컴포넌트는 클라이언트에서 실행되어야 합니다.\n* [`createContext`](/reference/react/createContext)\n* [`use`](/reference/react/use) 및 [`useId`](/reference/react/useId)를 제외한 [`react`](/reference/react/hooks)와 [`react-dom`](/reference/react-dom/hooks)의 Hook\n* [`forwardRef`](/reference/react/forwardRef)\n* [`memo`](/reference/react/memo)\n* [`startTransition`](/reference/react/startTransition)\n* 클라이언트 API를 사용하는 경우(예: DOM 삽입 혹은 네이티브 플랫폼 뷰 등)\n\n이 라이브러리들이 React 서버 컴포넌트와 호환되도록 업데이트되었다면 이미 `'use client'`를 포함하고 있어 서버 컴포넌트에서 직접 사용할 수 있습니다. 라이브러리가 업데이트되지 않았거나 컴포넌트가 클라이언트에서만 사용할 수 있는 이벤트 핸들러와 같은 Props가 필요한 경우, 사용할 서드파티 클라이언트 컴포넌트와 서버 컴포넌트 사이에 자체 클라이언트 컴포넌트 파일을 추가해야 할 수 있습니다.\n\n[TODO]: <> (Troubleshooting - need use-cases)\n"
  },
  {
    "path": "src/content/reference/rsc/use-server.md",
    "content": "---\ntitle: \"'use server'\"\ntitleForTitleTag: \"'use server' 지시어\"\n---\n\n<RSC>\n\n`'use server'`는 [React 서버 컴포넌트](/reference/rsc/server-components)와 함께 사용합니다.\n\n</RSC>\n\n\n<Intro>\n\n`'use server'`를 사용하여 클라이언트 측 코드에서 호출할 수 있는 서버 측 함수를 표시합니다.\n\n</Intro>\n\n<InlineToc />\n\n---\n\n## 레퍼런스 {/*reference*/}\n\n### `'use server'` {/*use-server*/}\n\n함수를 클라이언트에서 호출할 수 있음을 표시하기 위해, 비동기 함수의 최상단에 `'use server';`를 추가하세요. 이를 [_서버 함수_](/reference/rsc/server-functions)라고 부릅니다.\n\n```js {2}\nasync function addToCart(data) {\n  'use server';\n  // ...\n}\n```\n\n클라이언트에서 서버 함수를 호출하면, 전달된 모든 인수의 직렬화된 사본을 포함한 네트워크 요청을 서버로 전송합니다. 서버 함수가 값을 반환하면, 그 값을 직렬화하여 클라이언트로 반환합니다.\n\n함수 각각에 `'use server'`를 표기하는 대신, 파일의 최상단에 지시어를 추가하여 파일의 모든 내보내기<sup>Export</sup>를 클라이언트를 포함한 모든 곳에서 사용할 수 있는 서버 함수로 표기할 수 있습니다.\n\n#### 주의 사항 {/*caveats*/}\n* `'use server'`는 함수 또는 모듈의 최상단에 있어야 합니다. `import`를 포함한 다른 코드보다 위에 있어야 합니다. (지시어 위의 주석은 괜찮습니다.) 백틱이 아닌 단일 또는 이중 따옴표로 작성해야 합니다.\n* `'use server'`는 서버 측 파일에서만 사용할 수 있습니다. 결과적으로 생성된 서버 함수는 Props를 통해 클라이언트 컴포넌트로 전달할 수 있습니다. 지원되는 [직렬화 타입](#serializable-parameters-and-return-values)을 참고하세요.\n* 서버 함수를 [클라이언트 코드](/reference/rsc/use-client)에서 가져오기<sup>Import</sup> 위해, 지시어를 모듈 수준에서 사용해야 합니다.\n* 기본 네트워크 호출이 항상 비동기적이므로, `'use server'`는 비동기 함수에서만 사용할 수 있습니다.\n* 항상 서버 함수의 인수를 신뢰할 수 없는 입력으로 취급하고 모든 변경을 검토하세요. [보안 고려사항](#security)을 확인하세요.\n* 서버 함수는 [Transition](/reference/react/useTransition) 안에서 호출되어야 합니다. [`<form action>`](/reference/react-dom/components/form#props) 또는 [`formAction`](/reference/react-dom/components/input#props)으로 전달된 서버 함수는 자동으로 Transition 내에서 호출됩니다.\n* 서버 함수는 서버 측 상태를 업데이트하는 Mutation을 위해 설계되었으며, 데이터 가져오기<sup>Fetching</sup>에는 권장하지 않습니다. 따라서, 서버 함수를 구현하는 프레임워크는 일반적으로 한 번에 하나의 작업만 처리하며, 반환 값을 캐시하는 방법을 제공하지 않습니다.\n\n### 보안 고려사항 {/*security*/}\n\n서버 함수에 대한 인수는 클라이언트에서 완전히 제어됩니다. 보안을 위해 항상 신뢰할 수 없는 입력으로 취급하여, 인수를 적절하게 검증하고 이스케이프 하는지 확인하세요.\n\n모든 서버 함수에서 로그인한 사용자가 해당 작업을 수행할 수 있는지 확인하세요.\n\n<Wip>\n\n서버 함수에서 중요한 데이터를 전송하지 않기 위해, 고유한 값과 객체를 클라이언트 코드로 전달하는 것을 방지하기 위한 실험적인 Taint API가 있습니다.\n\n[experimental_taintUniqueValue](/reference/react/experimental_taintUniqueValue)와 [experimental_taintObjectReference](/reference/react/experimental_taintObjectReference)를 참고하세요.\n\n</Wip>\n\n### 직렬화 가능 인수와 반환값 {/*serializable-parameters-and-return-values*/}\n\n클라이언트 코드가 네트워크를 통해 서버 함수를 호출하므로, 전달하는 모든 인수는 직렬화 가능해야 합니다.\n\n다음은 서버 함수의 인수로 지원되는 타입입니다.\n\n* 원시 자료형\n  * [string](https://developer.mozilla.org/ko/docs/Glossary/String)\n  * [number](https://developer.mozilla.org/ko/docs/Glossary/Number)\n  * [bigint](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/BigInt)\n  * [boolean](https://developer.mozilla.org/ko/docs/Glossary/Boolean)\n  * [undefined](https://developer.mozilla.org/ko/docs/Glossary/Undefined)\n  * [null](https://developer.mozilla.org/ko/docs/Glossary/Null)\n  * [symbol](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Symbol) ( [`Symbol.for`](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Symbol/for)를 통해 전역 심볼 레지스트리에 등록된 심볼만 해당)\n* 직렬화할 수 있는 값을 포함하는 이터러블\n  * [String](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/String)\n  * [Array](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array)\n  * [Map](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Map)\n  * [Set](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Set)\n  * [TypedArray](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/TypedArray)와 [ArrayBuffer](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer)\n* [Date](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Date)\n* [FormData](https://developer.mozilla.org/ko/docs/Web/API/FormData) 인스턴스\n* 일반 [객체](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Object) (직렬화할 수 있는 프로퍼티를 사용하여 [객체 이니셜라이저](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Operators/Object_initializer)로 생성된 객체)\n* [서버 함수](/reference/rsc/server-functions)로서의 함수\n* [Promise](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Promise)\n\n단, 다음은 지원되지 않습니다.\n* React 엘리먼트 또는 [JSX](/learn/writing-markup-with-jsx)\n* 컴포넌트 함수 또는 서버 함수가 아닌 다른 함수를 포함하는 함수\n* [클래스](https://developer.mozilla.org/ko/docs/Learn/JavaScript/Objects/Classes_in_JavaScript)\n* 클래스의 인스턴스인 객체(언급된 내장 객체 제외)또는 [null 프로토타입](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Object#null-prototype_objects)이 있는 객체\n* 전역에 등록되지 않은 Symbol (예: `Symbol('my new symbol')`)\n* Events from event handlers\n\n지원되는 직렬화 가능한 반환 값은 경계 클라이언트 컴포넌트의 [직렬화 가능한 Props](/reference/rsc/use-client#serializable-types)와 동일합니다.\n\n## 사용법 {/*usage*/}\n\n### Server Functions in forms {/*server-functions-in-forms*/}\n\n서버 함수의 가장 일반적인 사용 사례는, 데이터를 변경하는 서버 함수를 호출하는 것입니다. 브라우저의 [HTML 폼 엘리먼트](https://developer.mozilla.org/ko/docs/Web/HTML/Element/form)는 사용자가 Mutation을 제출하는 전통적인 접근 방식입니다. React 서버 컴포넌트를 통해, React는 [폼<sup>Form</sup>](/reference/react-dom/components/form)에서 액션으로 사용되는 서버 함수에 대한 최상의 지원을 제공합니다.\n\n여기, 사용자가 사용자 이름을 요청할 수 있는 폼<sup>Form</sup>이 있습니다.\n\n```js [[1, 3, \"formData\"]]\n// App.js\n\nasync function requestUsername(formData) {\n  'use server';\n  const username = formData.get('username');\n  // ...\n}\n\nexport default function App() {\n  return (\n    <form action={requestUsername}>\n      <input type=\"text\" name=\"username\" />\n      <button type=\"submit\">Request</button>\n    </form>\n  );\n}\n```\n\n예시에서 `requestUsername`은 `<form>`을 통한 서버 함수입니다. 사용자가 이 폼<sup>Form</sup>을 제출하면 서버 함수인 `requestUsername`에 네트워크 요청을 보냅니다. 폼<sup>Form</sup>에서 서버 함수를 호출할 때, React는 폼<sup>Form</sup>의 <CodeStep step={1}>[formData](https://developer.mozilla.org/ko/docs/Web/API/FormData)</CodeStep>를 서버 함수의 첫 번째 인자로 제공합니다.\n\n서버 함수를 폼 `action`에 전달하여, React는 폼을 [점진적 향상](https://developer.mozilla.org/ko/docs/Glossary/Progressive_Enhancement)할 수 있습니다. 이것은 자바스크립트 번들을 로드하기 전에 양식을 제출할 수 있다는 것을 의미합니다.\n\n#### 폼에서 반환 값 처리 {/*handling-return-values*/}\n\nIn the username request form, there might be the chance that a username is not available. `requestUsername` should tell us if it fails or not.\n\n점진적 향상을 지원하며 서버 함수의 결과를 기반으로 UI를 업데이트하려면, [`useActionState`](/reference/react/useActionState)를 사용하세요.\n\n```js\n// requestUsername.js\n'use server';\n\nexport default async function requestUsername(formData) {\n  const username = formData.get('username');\n  if (canRequest(username)) {\n    // ...\n    return 'successful';\n  }\n  return 'failed';\n}\n```\n\n```js {4,8}, [[2, 2, \"'use client'\"]]\n// UsernameForm.js\n'use client';\n\nimport { useActionState } from 'react';\nimport requestUsername from './requestUsername';\n\nfunction UsernameForm() {\n  const [state, action] = useActionState(requestUsername, null, 'n/a');\n\n  return (\n    <>\n      <form action={action}>\n        <input type=\"text\" name=\"username\" />\n        <button type=\"submit\">Request</button>\n      </form>\n      <p>Last submission request returned: {state}</p>\n    </>\n  );\n}\n```\n\n대부분의 Hook과 마찬가지로 `useActionState`는 <CodeStep step={2}>[클라이언트 코드](/reference/rsc/use-client)</CodeStep>에서만 호출할 수 있습니다.\n\n### `<form>`외부에서 서버 함수 호출하기 {/*calling-a-server-function-outside-of-form*/}\n\n서버 함수는 노출된 서버 엔드포인트이며 클라이언트 코드 어디에서나 호출할 수 있습니다.\n\n[폼<sup>Form</sup>](/reference/react-dom/components/form) 외부에서 서버 함수를 사용할 때, [Transition](/reference/react/useTransition)에서 서버 함수를 호출하면 로딩 인디케이터<sup>Loading Indicator</sup>를 표시하고, [낙관적 상태 업데이트](/reference/react/useOptimistic)를 표시하며 예기치 않은 오류를 처리할 수 있습니다. 폼은 Transition의 서버 함수를 자동으로 래핑합니다.\n\n```js {9-12}\nimport incrementLike from './actions';\nimport { useState, useTransition } from 'react';\n\nfunction LikeButton() {\n  const [isPending, startTransition] = useTransition();\n  const [likeCount, setLikeCount] = useState(0);\n\n  const onClick = () => {\n    startTransition(async () => {\n      const currentCount = await incrementLike();\n      setLikeCount(currentCount);\n    });\n  };\n\n  return (\n    <>\n      <p>Total Likes: {likeCount}</p>\n      <button onClick={onClick} disabled={isPending}>Like</button>;\n    </>\n  );\n}\n```\n\n```js\n// actions.js\n'use server';\n\nlet likeCount = 0;\nexport default async function incrementLike() {\n  likeCount++;\n  return likeCount;\n}\n```\n\n서버 함수의 반환 값을 읽으려면 반환된 Promise를 `await` 해야합니다.\n"
  },
  {
    "path": "src/content/reference/rules/components-and-hooks-must-be-pure.md",
    "content": "---\ntitle: 컴포넌트와 Hook은 순수해야 합니다\n---\n\n<Intro>\n순수 함수는 오직 계산만 수행하고 그 외의 작업은 하지 않습니다. 이는 코드의 이해와 디버깅을 더 쉽게 만들어 주며 React가 컴포넌트와 Hook을 자동으로 최적화할 수 있게 해줍니다.\n</Intro>\n\n<Note>\n이 페이지는 고급 주제를 다루며 [컴포넌트 순수하게 유지하기](/learn/keeping-components-pure)에서 다룬 개념에 대한 이해가 필요합니다.\n</Note>\n\n<InlineToc />\n\n### 순수성이 중요한 이유 {/*why-does-purity-matter*/}\n\nReact를 만드는 핵심 개념 중 하나는 _순수성_ 입니다. 순수한 컴포넌트나 Hook은 다음과 같습니다.\n\n* **멱등성** – 컴포넌트의 Props, State, Context 혹은 Hook의 인수에 대한 동일한 입력으로 실행할 때마다 [항상 같은 결과](/learn/keeping-components-pure#purity-components-as-formulas)를 얻습니다.\n* **렌더링 시 사이드 이펙트가 없어야 합니다** – 사이드 이펙트가 있는 코드는 [**렌더링과 별도로 실행**](#how-does-react-run-your-code)해야 합니다. 예를 들어, [이벤트 핸들러](/learn/responding-to-events)를 통해 사용자가 UI와 상호작용하여 UI를 업데이트하는 경우나, 렌더링 후에 실행되는 [Effect](/reference/react/useEffect)를 사용할 수 있습니다.\n* **지역 변수가 아닌 값을 변경하지 마세요** – 컴포넌트와 Hook은 렌더링 시 [지역에서 생성되지 않은 값을 수정하지 않아야](#mutation) 합니다.\n\n렌더링을 순수하게 유지하면, React는 어떤 업데이트가 사용자에게 먼저 보이는 것이 중요한지를 이해할 수 있습니다. 이는 렌더링의 순수성 덕분에 가능합니다. 컴포넌트가 [렌더링하는 동안](#how-does-react-run-your-code) 사이드 이펙트가 없기 때문에 React는 중요하지 않은 컴포넌트의 렌더링을 일시 중지하고 나중에 필요할 때 다시 돌아올 수 있습니다.\n\n구체적으로, 이는 렌더링 로직을 여러 번 실행할 수 있어 React가 사용자에게 쾌적한 사용자 경험을 제공할 수 있음을 의미합니다. 그러나 [렌더링 중](#how-does-react-run-your-code)에 전역 변수 값을 수정하는 것처럼 컴포넌트에 추적되지 않은 사이드 이펙트가 있는 경우, React가 렌더링 코드를 다시 실행할 때 사이드 이펙트가 원하지 않는 방식으로 트리거될 수 있습니다. 이는 종종 예상치 못한 버그로 이어져 사용자의 앱 경험을 저하할 수 있습니다. [컴포넌트를 순수하게 유지하기](/learn/keeping-components-pure#side-effects-unintended-consequences)에서 이 예시를 볼 수 있습니다.\n\n#### React가 코드를 실행하는 방식 {/*how-does-react-run-your-code*/}\n\nReact는 선언적입니다. React에 _무엇을_ 렌더링할지 말해주면, React는 사용자에게 _어떻게_ 최적으로 표시할지 알아냅니다. 이를 위해 React는 코드를 실행하는 몇 가지 단계를 거칩니다. React를 잘 사용하기 위해 이 모든 단계를 알 필요는 없습니다. 하지만 고수준에서 _렌더링_ 중에 실행되는 코드와 그 외부에서 실행되는 코드에 대해 알아야 합니다.\n\n<em>렌더링</em>은 UI의 다음 버전이 어떻게 보일지 계산하는 것을 의미합니다. 렌더링 후에 [Effect](/reference/react/useEffect)가 _flushed_ 됩니다. (더 이상 남아 있지 않을 때까지 실행됨.) 그리고 Effect가 레이아웃에 영향을 미치는 경우, 계산을 업데이트할 수 있습니다. React는 새로운 계산을 이전 UI 버전을 만드는 데 사용된 계산과 비교한 다음, 최신 버전과 일치시키기 위해 [DOM](https://developer.mozilla.org/ko/docs/Web/API/Document_Object_Model)(사용자가 실제로 보는 것)에 필요한 최소한의 변경만을 _커밋_ 합니다.\n\n<DeepDive>\n\n#### 코드가 렌더링 중에 실행되는지 확인하는 방법 {/*how-to-tell-if-code-runs-in-render*/}\n\n코드가 렌더링 중에 실행되는지 확인하는 빠른 방법은 코드의 위치를 확인하는 것입니다. 아래 예시와 같이 최상위 수준에 작성되어 있다면, 렌더링 중에 실행될 가능성이 큽니다.\n\n```js {2}\nfunction Dropdown() {\n  const selectedItems = new Set(); // 렌더링 중에 생성됩니다.\n  // ...\n}\n```\n\n이벤트 핸들러와 Effect는 렌더링 중에 실행되지 않습니다.\n\n```js {4}\nfunction Dropdown() {\n  const selectedItems = new Set();\n  const onSelect = (item) => {\n    // 이벤트 핸들러 안에 있으므로 사용자가 트리거할 때만 실행됩니다.\n    selectedItems.add(item);\n  }\n}\n```\n\n```js {4}\nfunction Dropdown() {\n  const selectedItems = new Set();\n  useEffect(() => {\n    // Effect 안에 있으므로 렌더링 후에만 실행됩니다.\n    logForAnalytics(selectedItems);\n  }, [selectedItems]);\n}\n```\n</DeepDive>\n\n---\n\n## 컴포넌트와 Hook은 멱등해야 합니다 {/*components-and-hooks-must-be-idempotent*/}\n\n컴포넌트는 항상 Props, State, Context와 같은 입력에 대해 동일한 출력을 반환해야 합니다. 이를 <em>멱등성</em>이라고 합니다. [멱등성](https://ko.wikipedia.org/wiki/%EB%A9%B1%EB%93%B1%EB%B2%95%EC%B9%99)은 함수형 프로그래밍에서 널리 사용되는 용어입니다. 이는 동일한 입력으로 해당 코드를 실행할 때마다 [항상 동일한 결과를 얻는 것](learn/keeping-components-pure)을 의미합니다.\n\n이는 _모든_ 코드가 [렌더링 중](#how-does-react-run-your-code)에도 멱등성을 유지해야 한다는 것을 의미합니다. 예를 들어 다음 코드 라인은 멱등하지 않습니다. (따라서 컴포넌트도 멱등하지 않습니다.)\n\n```js {2}\nfunction Clock() {\n  const time = new Date(); // 🔴 Bad: 항상 다른 결과를 반환합니다!\n  return <span>{time.toLocaleString()}</span>\n}\n```\n\n`new Date()` 함수는 항상 현재 날짜를 반환하고 호출할 때마다 결과가 변경되기 때문에 멱등하지 않습니다. 위 컴포넌트를 렌더링하면 화면에 표시되는 시간이 컴포넌트가 렌더링된 시간에 고정됩니다. 마찬가지로 `Math.random()`과 같은 함수도 멱등하지 않습니다. 입력이 동일해도 호출할 때마다 다른 결과를 반환하기 때문입니다.\n\n이는 `new Date()`와 같은 비멱등 함수를 _전혀_ 사용하지 말아야 한다는 뜻이 아닙니다. 단지 이를 [렌더링 중](#how-does-react-run-your-code)에 사용하지 않아야 한다는 것입니다. 이 경우 [Effect](/reference/react/useEffect)를 사용하여 최신 날짜를 컴포넌트에 _동기화_ 할 수 있습니다.\n\n<Sandpack>\n\n```js\nimport { useState, useEffect } from 'react';\n\nfunction useTime() {\n  // 1. 현재 날짜의 State를 추적합니다. `useState`는 초기 State로 초기화 함수를 받습니다.\n  //    이 함수는 Hook이 호출될 때 한 번만 실행되므로, Hook이 호출되는 시점의 현재 날짜가\n  //    처음으로 설정됩니다.\n  const [time, setTime] = useState(() => new Date());\n\n  useEffect(() => {\n    // 2. `setInterval`을 사용하여 현재 날짜를 매초마다 업데이트합니다.\n    const id = setInterval(() => {\n      setTime(new Date()); // ✅ Good: 렌더링에서 비멱등 코드가 더 이상 실행되지 않음.\n    }, 1000);\n    // 3. `setInterval` 타이머가 누수되지 않도록 Cleanup 함수를 반환합니다.\n    return () => clearInterval(id);\n  }, []);\n\n  return time;\n}\n\nexport default function Clock() {\n  const time = useTime();\n  return <span>{time.toLocaleString()}</span>;\n}\n```\n\n</Sandpack>\n\n멱등하지 않은 `new Date()` 호출을 Effect로 감싸면, 그 계산을 [렌더링 외부](#how-does-react-run-your-code)로 이동시킵니다.\n\nReact와 외부 상태를 동기화할 필요가 없다면, 사용자 상호작용에 대한 응답으로만 업데이트할 경우 [이벤트 핸들러](/learn/responding-to-events)를 사용하는 것도 고려해 볼 수 있습니다.\n\n---\n\n## 사이드 이펙트는 렌더링 외부에서 실행되어야 합니다 {/*side-effects-must-run-outside-of-render*/}\n\n[사이드 이펙트](/learn/keeping-components-pure#side-effects-unintended-consequences)는 [렌더링 중에](#how-does-react-run-your-code) 실행되어서는 안 됩니다. 이는, React가 최상의 사용자 경험을 제공하기 위해 컴포넌트를 여러 번 렌더링할 수 있기 때문입니다!\n\n<Note>\n사이드 이펙트는 Effect보다 더 넓은 개념입니다. Effect는 특히 `useEffect` 안에 감싸진 코드를 지칭하는 반면, 사이드 이펙트는 호출자에게 값을 반환하는 기본 결과 외에도 어떤 관찰 가능한 영향을 미치는 코드를 지칭하는 일반적인 용어입니다.\n\n사이드 이펙트는 일반적으로 [이벤트 핸들러](/learn/responding-to-events)나 Effect 안에서 작성합니다. 하지만 렌더링 중에는 절대로 작성해서는 안 됩니다.\n</Note>\n\n렌더링은 순수하게 유지되어야 하지만, 화면에 무언가를 보여주는 것처럼 앱이 흥미로운 동작을 하려면 어느 시점에서는 사이드 이펙트가 필요합니다! 이 규칙의 핵심은 React가 컴포넌트를 여러 번 렌더링할 수 있기 때문에, 사이드 이펙트가 [렌더링 중에](#how-does-react-run-your-code) 실행되어서는 안 된다는 것입니다. 대부분의 경우 [이벤트 핸들러](learn/responding-to-events)를 사용하여 사이드 이펙트를 처리합니다. 이벤트 핸들러를 사용하면 이 코드가 렌더링 중에 실행될 필요가 없다는 것을 React에 명시적으로 알려주어 렌더링을 순수하게 유지합니다. 모든 옵션을 다 시도해 보고 나서도 해결되지 않는 경우, 정말 최후의 수단으로 `useEffect`를 사용하여 사이드 이펙트를 처리할 수도 있습니다.\n\n### 변경이 허용되는 경우 {/*mutation*/}\n\n#### 지역 변경 {/*local-mutation*/}\n사이드 이펙트의 일반적인 예로 변경<sup>Mutation</sup>이 있습니다. 자바스크립트에서 변경은 [원시 값](https://developer.mozilla.org/ko/docs/Glossary/Primitive)이 아닌 값을 변경하는 것을 의미합니다. 흔히 React에서는 변경을 권장하지 않지만, _지역_ 변경은 전혀 문제가 되지 않습니다.\n\n```js {2,7}\nfunction FriendList({ friends }) {\n  const items = []; // ✅ Good: 지역 변수로 생성됨.\n  for (let i = 0; i < friends.length; i++) {\n    const friend = friends[i];\n    items.push(\n      <Friend key={friend.id} friend={friend} />\n    ); // ✅ Good: 지역 변경은 괜찮습니다.\n  }\n  return <section>{items}</section>;\n}\n```\n\n지역 변경을 피하고자 코드를 비틀 필요는 없습니다. [`Array.map`](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array/map)을 사용하여 간결하게 만들 수도 있지만, 지역 배열을 생성한 다음 [렌더링 중](#how-does-react-run-your-code)에 항목을 추가하는 것은 전혀 문제가 되지 않습니다.\n\n비록 `items`를 변경시키는 것처럼 보이지만, 이 코드를 _지역에서만_ 변경한다는 점이 중요합니다. 즉 이 변경은 컴포넌트가 다시 렌더링될 때 \"기억되지\" 않습니다. `items`는 컴포넌트가 존재하는 동안에만 유지됩니다. `<FriendList />`가 렌더링 될 때마다 `items`는 항상 _재생성_ 되므로, 컴포넌트는 항상 동일한 결과를 반환합니다.\n\n반면 `items`가 컴포넌트 외부에서 생성되면 이전 값을 유지하고 변경 사항을 기억합니다.\n\n```js {1,7}\nconst items = []; // 🔴 Bad: 컴포넌트 외부에서 생성됨.\nfunction FriendList({ friends }) {\n  for (let i = 0; i < friends.length; i++) {\n    const friend = friends[i];\n    items.push(\n      <Friend key={friend.id} friend={friend} />\n    ); // 🔴 Bad: 렌더링 외부에서 생성된 값을 변경합니다.\n  }\n  return <section>{items}</section>;\n}\n```\n\n`<FriendList />`가 다시 실행될 때마다 `items`에 `friends`를 계속 추가하여 중복된 결과가 여러 번 나타나게 됩니다. 이 버전의 `<FriendList />`는 [렌더링 중](#how-does-react-run-your-code)에 관찰 가능한 사이드 이펙트가 발생하며 **규칙을 위반**합니다.\n\n#### Lazy 초기화 {/*lazy-initialization*/}\n\nLazy 초기화는 완전히 \"순수\"하지 않더라도 괜찮습니다.\n\n```js {2}\nfunction ExpenseForm() {\n  SuperCalculator.initializeIfNotReady(); // ✅ Good: 다른 컴포넌트에 영향을 미치지 않는 경우\n  // 계속 렌더링...\n}\n```\n\n#### DOM 변경 {/*changing-the-dom*/}\n\n사용자에게 직접적으로 보이는 사이드 이펙트는 React 컴포넌트의 렌더링 로직에서 허용되지 않습니다. 즉, 단순히 컴포넌트 함수를 호출하는 것만으로 화면에 변화를 일으켜서는 안 됩니다.\n\n```js {2}\nfunction ProductDetailPage({ product }) {\n  document.title = product.title; // 🔴 Bad: DOM을 변경함\n}\n```\n\n원하는 결과를 얻기 위해 렌더링 외부에서 `document.title`을 업데이트하는 한 가지 방법은 [컴포넌트를 `document`와 동기화하는 것](/learn/synchronizing-with-effects)입니다.\n\n컴포넌트를 여러 번 호출해도 안전하고 다른 컴포넌트의 렌더링에 영향을 주지 않는 한, React는 엄격한 함수형 프로그래밍의 의미에서 100% 순수하지 않아도 상관하지 않습니다. 더 중요한 것은 [컴포넌트가 반드시 멱등해야 한다는 것](/reference/rules/components-and-hooks-must-be-pure)입니다.\n\n---\n\n## Props와 State는 불변입니다 {/*props-and-state-are-immutable*/}\n\n컴포넌트의 Props와 State는 불변의 [스냅샷](learn/state-as-a-snapshot)입니다. 직접적으로 변경하면 안 됩니다. 대신 새로운 Props를 전달하거나 `useState`에서 제공하는 Setter 함수를 사용하세요.\n\nProps와 State 값을 렌더링 후에 업데이트되는 스냅샷으로 생각할 수 있습니다. 이러한 이유로 Props나 State 변수를 직접 수정하지 않아야 합니다. 새로운 Props를 전달하거나 제공된 Setter 함수를 사용하여 컴포넌트가 다음 번에 렌더링 될 때 State가 업데이트되어야 함을 React에 알려줍니다.\n\n### Props를 변경하지 마세요 {/*props*/}\nProps는 불변입니다. Props를 변경한다면 애플리케이션이 일관성 없는 출력을 생성하게 되며, 상황에 따라 작동할 수도 있고 안 할 수도 있기 때문에 디버깅이 어려워질 수 있습니다.\n\n```js {2}\nfunction Post({ item }) {\n  item.url = new Url(item.url, base); // 🔴 Bad: 절대 Props를 직접 변경하지 마세요.\n  return <Link url={item.url}>{item.title}</Link>;\n}\n```\n\n```js {2}\nfunction Post({ item }) {\n  const url = new Url(item.url, base); // ✅ Good: 대신, 복사본을 만드세요.\n  return <Link url={url}>{item.title}</Link>;\n}\n```\n\n### State를 변경하지 마세요 {/*state*/}\n`useState`는 State 변수와 그 State를 변경하기 위한 Setter 함수를 반환합니다.\n\n```js\nconst [stateVariable, setter] = useState(0);\n```\n\nState 변수를 직접 변경하는 대신, `useState`에 의해 반환된 Setter 함수를 사용하여 변경해야 합니다. State 변수의 값을 변경하면 컴포넌트가 변경되지 않아 사용자는 이전 UI를 보게 됩니다. Setter 함수를 사용하면 React에 State가 변경되었으며 UI를 변경하기 위해 리렌더링을 대기열에 추가해야 한다는 것을 알립니다.\n\n```js {5}\nfunction Counter() {\n  const [count, setCount] = useState(0);\n\n  function handleClick() {\n    count = count + 1; // 🔴 Bad: 절대 State를 직접 변경하지 마세요.\n  }\n\n  return (\n    <button onClick={handleClick}>\n      You pressed me {count} times\n    </button>\n  );\n}\n```\n\n```js {5}\nfunction Counter() {\n  const [count, setCount] = useState(0);\n\n  function handleClick() {\n    setCount(count + 1); // ✅ Good: useState에서 반환된 setter 함수를 사용하세요\n  }\n\n  return (\n    <button onClick={handleClick}>\n      You pressed me {count} times\n    </button>\n  );\n}\n```\n\n---\n\n## Hook의 반환값과 인수는 불변입니다 {/*return-values-and-arguments-to-hooks-are-immutable*/}\n\n값이 Hook에 전달되면, 이를 수정해서는 안 됩니다. JSX의 Props와 마찬가지로 Hook에 전달된 값은 불변입니다.\n\n```js {4}\nfunction useIconStyle(icon) {\n  const theme = useContext(ThemeContext);\n  if (icon.enabled) {\n    icon.className = computeStyle(icon, theme); // 🔴 Bad: 절대 Hook 인수를 직접 변경하지 마세요.\n  }\n  return icon;\n}\n```\n\n```js {3}\nfunction useIconStyle(icon) {\n  const theme = useContext(ThemeContext);\n  const newIcon = { ...icon }; // ✅ Good: 대신 복사본을 만드세요.\n  if (icon.enabled) {\n    newIcon.className = computeStyle(icon, theme);\n  }\n  return newIcon;\n}\n```\n\nReact의 중요한 원칙 중 하나는 _지역 추론_ 입니다. 이는 코드를 독립적으로 살펴봄으로써 컴포넌트나 Hook이 하는 일을 이해할 수 있는 능력입니다. Hook은 호출될 때 \"블랙박스\"처럼 다뤄져야 합니다. 예를 들어 커스텀 Hook은 내부에서 값을 메모이제이션 하기 위해 인수를 의존성으로 사용할 수 있습니다.\n\n```js {4}\nfunction useIconStyle(icon) {\n  const theme = useContext(ThemeContext);\n\n  return useMemo(() => {\n    const newIcon = { ...icon };\n    if (icon.enabled) {\n      newIcon.className = computeStyle(icon, theme);\n    }\n    return newIcon;\n  }, [icon, theme]);\n}\n```\n\nHook 인수를 변경하면 커스텀 Hook의 메모이제이션이 잘못되므로, 이를 피하는 것이 중요합니다.\n\n```js {4}\nstyle = useIconStyle(icon);         // `style`은 `icon`을 기준으로 메모이제이션됨.\nicon.enabled = false;               // Bad: 🔴 절대 Hook 인수를 직접 변경하지 마세요.\nstyle = useIconStyle(icon);         // 이전에 메모이제이션된 결과가 반환됨.\n```\n\n```js {4}\nstyle = useIconStyle(icon);         // `style`은 `icon`을 기준으로 메모이제이션됨.\nicon = { ...icon, enabled: false }; // Good: ✅ 대신 복사본을 만드세요.\nstyle = useIconStyle(icon);         // `style`의 새로운 값이 계산됨.\n```\n\n마찬가지로 Hook의 반환 값을 수정하지 않는 것이 중요합니다. 반환 값이 메모이제이션되어 있을 수 있기 때문입니다.\n\n---\n\n## JSX로 전달된 값은 불변입니다 {/*values-are-immutable-after-being-passed-to-jsx*/}\n\nJSX에 사용된 후에는 값을 변경하지 마세요. JSX가 생성되기 전에 변경을 수행하세요.\n\nJSX를 표현식에서 사용할 때, React는 컴포넌트의 렌더링이 끝나기 전에 JSX를 성급하게 평가할 수 있습니다. 이는 JSX에 전달된 후에 값을 변경하면 React가 컴포넌트의 출력을 업데이트할지 여부를 알지 못하므로 오래된 UI로 이어질 수 있음을 의미합니다.\n\n```js {4}\nfunction Page({ colour }) {\n  const styles = { colour, size: \"large\" };\n  const header = <Header styles={styles} />;\n  styles.size = \"small\"; // 🔴 Bad: `styles`는 이미 위의 JSX에서 사용되었습니다.\n  const footer = <Footer styles={styles} />;\n  return (\n    <>\n      {header}\n      <Content />\n      {footer}\n    </>\n  );\n}\n```\n\n```js {4}\nfunction Page({ colour }) {\n  const headerStyles = { colour, size: \"large\" };\n  const header = <Header styles={headerStyles} />;\n  const footerStyles = { colour, size: \"small\" }; // ✅ Good: 새로운 값을 생성했습니다.\n  const footer = <Footer styles={footerStyles} />;\n  return (\n    <>\n      {header}\n      <Content />\n      {footer}\n    </>\n  );\n}\n```\n"
  },
  {
    "path": "src/content/reference/rules/index.md",
    "content": "---\ntitle: React의 규칙\n---\n\n<Intro>\n각 프로그래밍 언어마다 개념을 표현하는 고유한 방식이 있듯이, React에도 패턴 이해를 쉽게 하며 고품질의 애플리케이션을 만들 수 있게 하는 일종의 규칙 혹은 모범적인 방식이 있습니다.\n</Intro>\n\n<InlineToc />\n\n---\n\n<Note>\nReact를 사용하여 UI를 표현하는 방법에 대해 더 알고 싶다면 [React로 사고하기](/learn/thinking-in-react)를 읽어보는 것을 추천합니다.\n</Note>\n\n이 섹션에서는 모범적인 React 코드를 작성하기 위한 규칙을 설명합니다. 모범적인 React 코드를 작성하면 애플리케이션을 체계적으로 조직하고 안전하며 쉽게 구성할 수 있습니다. 이러한 특성은 애플리케이션이 변화에 더 잘 대처할 수 있게 하고 다른 개발자나 라이브러리, 도구와의 협업을 더 원활하게 합니다.\n\n이러한 규칙을 **React의 규칙**이라고 합니다. 이는 단순한 지침이 아니라 규칙으로, 이를 어길 경우 애플리케이션에 버그가 생길 가능성이 높으며 코드가 일반적이지 않게 변해 이해하기 어렵고 논리적으로 설명하기 힘들어집니다.\n\n코드베이스가 React의 규칙을 따르도록 하기 위해 React의 [ESLint](https://www.npmjs.com/package/eslint-plugin-react-hooks) 플러그인과 함께 [Strict Mode](/reference/react/StrictMode)를 사용하는 것을 강력히 권장합니다. React의 규칙을 따르면 버그를 찾아 해결할 수 있으며 애플리케이션의 유지 보수성을 높일 수 있습니다.\n\n---\n\n## 컴포넌트와 Hook은 순수해야 합니다 {/*components-and-hooks-must-be-pure*/}\n\n[컴포넌트와 Hook의 순수성](/reference/rules/components-and-hooks-must-be-pure)은 React의 주요 규칙으로, 이를 통해 앱이 예측 가능하고 디버깅이 쉬워지며 React가 코드를 자동으로 최적화할 수 있습니다.\n\n* [컴포넌트는 멱등해야 합니다](/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent) – React 컴포넌트는 항상 입력 값(Props, State, Context)에 따라 동일한 출력을 반환한다고 가정합니다.\n* [사이드 이펙트는 렌더링 외부에서 실행되어야 합니다](/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render) – [사이드 이펙트](/learn/keeping-components-pure#side-effects-unintended-consequences)는 React가 최상의 사용자 경험을 제공하기 위해 컴포넌트를 여러 번 렌더링할 수 있기 때문에 렌더링 중에 실행되어서는 안 됩니다.\n* [Props와 State는 불변입니다](/reference/rules/components-and-hooks-must-be-pure#props-and-state-are-immutable) – 컴포넌트의 Props와 State는 단일 렌더링에 대한 불변의 [스냅샷](/learn/state-as-a-snapshot)입니다. 절대 이를 직접 변경하지 마세요.\n* [Hook의 반환값과 인수는 불변입니다](/reference/rules/components-and-hooks-must-be-pure#return-values-and-arguments-to-hooks-are-immutable) – 값이 Hook에 전달되면 이를 수정해서는 안 됩니다. JSX의 Props와 마찬가지로 Hook에 전달된 값도 불변입니다.\n* [JSX로 전달된 값은 불변입니다](/reference/rules/components-and-hooks-must-be-pure#values-are-immutable-after-being-passed-to-jsx) – JSX에 사용된 후에는 값을 변경하지 마세요. JSX가 생성되기 전에 변경을 수행하세요.\n\n---\n\n## React가 컴포넌트와 Hook을 호출하는 방식 {/*react-calls-components-and-hooks*/}\n\n[React는 사용자 경험을 최적화하기 위해 필요할 때마다 컴포넌트와 Hook을 렌더링합니다.](/reference/rules/react-calls-components-and-hooks) React는 선언적입니다. 즉 컴포넌트 로직에서 무엇을 렌더링할지 React에게 지시하면, React는 이를 사용자가 최적으로 볼 수 있도록 알아서 처리합니다.\n\n* [컴포넌트 함수를 직접 호출하지 마세요](/reference/rules/react-calls-components-and-hooks#never-call-component-functions-directly) – 컴포넌트는 JSX에서만 사용해야 합니다. 일반 함수처럼 호출하지 마세요.\n* [Hook을 일반 값으로 전달하지 마세요 ](/reference/rules/react-calls-components-and-hooks#never-pass-around-hooks-as-regular-values) – Hook은 반드시 컴포넌트 내부에서만 호출되어야 합니다. Hook을 일반 값처럼 전달하지 마세요.\n\n---\n\n## Hook의 규칙 {/*rules-of-hooks*/}\n\nHook은 자바스크립트 함수로 정의하지만, 호출 위치에 제약이 있는 특별한 유형의 재사용 가능한 UI 로직입니다. Hook을 사용할 때는 [Hook의 규칙](/reference/rules/rules-of-hooks)을 따라야 합니다.\n\n* [Hook을 최상위 레벨에서만 호출하세요](/reference/rules/rules-of-hooks#only-call-hooks-at-the-top-level) – Hook을 반복문, 조건문, 또는 중첩된 함수 내부에서 호출하지 마세요. 대신 Hook을 항상 React 함수 최상위 레벨에서 호출하고, early return 이전에 사용해야 합니다.\n* [Hook을 React 함수에서만 호출하세요 ](/reference/rules/rules-of-hooks#only-call-hooks-from-react-functions) – 일반 자바스크립트 함수에서 Hook을 호출하지 마세요.\n\n"
  },
  {
    "path": "src/content/reference/rules/react-calls-components-and-hooks.md",
    "content": "---\ntitle: React가 컴포넌트와 Hook을 호출하는 방식\n---\n\n<Intro>\nReact는 사용자 경험을 최적화하기 위해 필요할 때마다 컴포넌트와 Hook을 렌더링하는 역할을 합니다. React는 선언적입니다. 즉 컴포넌트의 로직에서 무엇을 렌더링할지를 React에 알려주면, React는 이를 사용자에게 가장 잘 표시할 방법을 찾아냅니다.\n</Intro>\n\n<InlineToc />\n\n---\n\n## 컴포넌트 함수를 직접 호출하지 마세요 {/*never-call-component-functions-directly*/}\n컴포넌트는 JSX에서만 사용해야 합니다. 일반 함수처럼 호출하지 마세요. React가 호출해야 합니다.\n\nReact는 [렌더링하는 동안](/reference/rules/components-and-hooks-must-be-pure#how-does-react-run-your-code) 컴포넌트 함수가 언제 호출될지 결정해야 합니다. React는 이를 JSX로 수행합니다.\n\n```js {2}\nfunction BlogPost() {\n  return <Layout><Article /></Layout>; // ✅ Good: 컴포넌트를 JSX에서만 사용합니다.\n}\n```\n\n```js {2}\nfunction BlogPost() {\n  return <Layout>{Article()}</Layout>; // 🔴 Bad: 컴포넌트 함수를 직접 호출하지 마세요.\n}\n```\n\n컴포넌트가 Hook을 포함하고 있다면, 컴포넌트를 반복문이나 조건문에서 직접 호출할 때 [Hook의 규칙](/reference/rules/rules-of-hooks)을 위반하기 쉽습니다.\n\nReact가 렌더링을 조정하도록 하면 여러 이점이 있습니다.\n\n* **컴포넌트가 함수 이상의 역할을 하게 됩니다.** React는 Hook을 사용해 컴포넌트에 <em>지역 State</em>와 같은 기능을 추가하여, 컴포넌트가 트리 내에서 고유한 정체성을 갖도록 할 수 있습니다.\n* **컴포넌트 타입이 재조정 과정에 참여합니다.** React가 컴포넌트를 호출하도록 하면 트리의 개념적 구조에 대해 더 많은 정보를 제공하게 됩니다. 예를 들어 `<Feed>`에서 `<Profile>` 페이지로 전환될 때 React는 이를 재사용하려고 하지 않습니다.\n* **React가 사용자 경험을 향상할 수 있습니다.** 예를 들어 React는 컴포넌트를 호출하는 사이에 브라우저가 일부 작업을 수행할 수 있도록 하여, 큰 컴포넌트 트리를 다시 렌더링하는 것이 메인 스레드를 차단하지 않도록 할 수 있습니다.\n* **더 나은 디버깅 경험을 제공합니다.** 컴포넌트가 라이브러리에서 일급 객체로 취급되면, 개발 중에 분석할 수 있는 풍부한 개발 도구를 제공할 수 있습니다.\n* **재조정 과정이 더 효율적입니다.** React는 트리에서 정확히 어떤 컴포넌트가 다시 렌더링이 필요한지 결정하고, 필요 없는 컴포넌트는 건너뛸 수 있습니다. 이는 앱을 더 빠르고 민첩하게 만듭니다.\n\n---\n\n## Hook을 일반 값처럼 전달하지 마세요 {/*never-pass-around-hooks-as-regular-values*/}\n\nHook은 컴포넌트나 Hook 내부에서만 호출되어야 합니다. Hook을 일반 값처럼 전달하지 마세요.\n\nHook은 컴포넌트에 React 기능을 추가할 수 있게 합니다. Hook은 항상 함수로 호출되어야 하며 일반 값처럼 전달하면 안 됩니다. Hook을 함수로 호출해야, 개발자가 컴포넌트를 독립적으로 이해할 수 있는 _지역 추론_ 이 가능합니다.\n\n이 규칙을 어기면 React가 컴포넌트를 자동으로 최적화하지 못합니다.\n\n### Hook을 동적으로 변경하지 마세요 {/*dont-dynamically-mutate-a-hook*/}\n\nHook은 가능한 한 \"정적\"으로 유지되어야 합니다. 이는 Hook을 동적으로 변경해서는 안 된다는 의미입니다. 예를 들어 고차 Hook을 작성하면 안됩니다.\n\n```js {2}\nfunction ChatInput() {\n  const useDataWithLogging = withLogging(useData); // 🔴 Bad: 고차 Hook을 작성하지 마세요.\n  const data = useDataWithLogging();\n}\n```\n\nHook은 변경 불가능해야 하며 수정되지 않아야 합니다. Hook을 동적으로 변경하는 대신, 원하는 기능을 가진 정적인 Hook을 작성하세요.\n\n```js {2,6}\nfunction ChatInput() {\n  const data = useDataWithLogging(); // ✅ Good: Hook을 새로 작성하세요.\n}\n\nfunction useDataWithLogging() {\n  // ... 여기에 새로운 Hook의 로직을 작성하세요\n}\n```\n\n### Hook을 동적으로 사용하지 마세요 {/*dont-dynamically-use-hooks*/}\n\nHook은 동적으로 사용해서도 안 됩니다. 예를 들어 Hook을 값으로 전달하여 컴포넌트에 의존성을 주입하는 대신,\n\n```js {2}\nfunction ChatInput() {\n  return <Button useData={useDataWithLogging} /> // 🔴 Bad: Hook을 Props로 전달하지 마세요.\n}\n```\n\n항상 Hook 호출을 해당 컴포넌트에 직접 작성하고, 모든 로직을 그 안에서 처리해야 합니다.\n\n```js {6}\nfunction ChatInput() {\n  return <Button />\n}\n\nfunction Button() {\n  const data = useDataWithLogging(); // ✅ Good: Hook을 직접 사용하세요.\n}\n\nfunction useDataWithLogging() {\n  // Hook의 동작을 변경하는 조건부 로직이 있다면, 이는 Hook 내부에 포함해야 합니다.\n}\n```\n\n이렇게 하면 `<Button />` 컴포넌트가 훨씬 이해하기 쉽고 디버깅하기도 쉬워집니다. Hook을 동적으로 사용하면 앱의 복잡성이 크게 증가하고, 지역 추론을 방해하여 장기적으로 팀의 생산성을 저하시킵니다. 또한 Hook을 조건부로 호출하면 안된다는 [Hook의 규칙](/reference/rules/rules-of-hooks)을 실수로 어기기 쉽습니다. 테스트를 위해 컴포넌트를 모킹<sup>Mocking</sup>해야 한다면, 대신 서버를 모킹<sup>Mocking</sup>하여 미리 준비된 데이터에 응답하도록 하는 것이 낫습니다. 가능하다면 앱을 end-to-end 테스트하는 것이 더 효과적입니다.\n"
  },
  {
    "path": "src/content/reference/rules/rules-of-hooks.md",
    "content": "---\ntitle: Hook의 규칙\n---\n\n<Intro>\nHook은 자바스크립트 함수로 정의되지만 호출 위치에 제약이 있는 특별한 유형의 재사용 가능한 UI 로직입니다.\n</Intro>\n\n<InlineToc />\n\n---\n\n##  Hook을 최상위 레벨에서만 호출하세요 {/*only-call-hooks-at-the-top-level*/}\n\nReact에서는 `use`로 시작하는 함수를 [*Hook*](/reference/react)이라고 부릅니다.\n\n**Hook을 반복문, 조건문, 중첩 함수, 또는 `try`/`catch`/`finally` 블록 내부에서 호출하지 마세요.** 대신 Hook을 항상 React 함수의 최상위 레벨에서 호출하고, early return 이전에 사용해야 합니다. Hook은 React가 함수 컴포넌트를 렌더링하는 동안에만 호출할 수 있습니다.\n\n* ✅ [함수 컴포넌트](/learn/your-first-component)의 본문 최상위 레벨에서 호출하세요.\n* ✅ [커스텀 Hook](/learn/reusing-logic-with-custom-hooks)의 본문 최상위 레벨에서 호출하세요.\n\n```js{2-3,8-9}\nfunction Counter() {\n  // ✅ Good: 함수 컴포넌트의 최상위 레벨에서 사용합니다.\n  const [count, setCount] = useState(0);\n  // ...\n}\n\nfunction useWindowWidth() {\n  // ✅ Good: 커스텀 Hook의 최상위 레벨에서 사용합니다.\n  const [width, setWidth] = useState(window.innerWidth);\n  // ...\n}\n```\n\n다음과 같이 Hook(`use`로 시작하는 함수)을 호출하는 것은 지원되지 **않습니다**.\n\n* 🔴 조건문이나 반복문 내부에서 Hook을 호출하지 마세요.\n* 🔴 조건부 `return`문 이후에 Hook을 호출하지 마세요.\n* 🔴 이벤트 핸들러에서 Hook을 호출하지 마세요.\n* 🔴 클래스 컴포넌트에서 Hook을 호출하지 마세요.\n* 🔴 `useMemo`, `useReducer`, `useEffect`에 전달된 함수 내부에서 Hook을 호출하지 마세요.\n* 🔴 `try`/`catch`/`finally` 블록 내부에서 Hook을 호출하지 마세요.\n\n이 규칙을 어기면 오류가 발생할 수 있습니다.\n\n```js{3-4,11-12,20-21}\nfunction Bad({ cond }) {\n  if (cond) {\n    // 🔴 Bad: 조건부 내부 (수정하려면 외부로 이동하세요!)\n    const theme = useContext(ThemeContext);\n  }\n  // ...\n}\n\nfunction Bad() {\n  for (let i = 0; i < 10; i++) {\n    // 🔴 Bad: 반복문 내부 (수정하려면 외부로 이동하세요!)\n    const theme = useContext(ThemeContext);\n  }\n  // ...\n}\n\nfunction Bad({ cond }) {\n  if (cond) {\n    return;\n  }\n  // 🔴 Bad: 조건부 `return`문 이후 (수정하려면 `return`문 이전으로 이동하세요!)\n  const theme = useContext(ThemeContext);\n  // ...\n}\n\nfunction Bad() {\n  function handleClick() {\n    // 🔴 Bad: 이벤트 핸들러 내부 (수정하려면 외부로 이동하세요!)\n    const theme = useContext(ThemeContext);\n  }\n  // ...\n}\n\nfunction Bad() {\n  const style = useMemo(() => {\n    // 🔴 Bad: `useMemo` 내부 (수정하려면 외부로 이동하세요!)\n    const theme = useContext(ThemeContext);\n    return createStyle(theme);\n  });\n  // ...\n}\n\nclass Bad extends React.Component {\n  render() {\n    // 🔴 Bad: 클래스 컴포넌트 내부 (수정하려면 클래스 컴포넌트 대신 함수 컴포넌트를 사용하세요!)\n    useEffect(() => {})\n    // ...\n  }\n}\n\nfunction Bad() {\n  try {\n    // 🔴 Bad: `try`/`catch`/`finally` 블록 내부 (수정하려면 외부로 이동하세요!)\n    const [x, setX] = useState(0);\n  } catch {\n    const [x, setX] = useState(1);\n  }\n}\n```\n\n이러한 실수를 잡기 위해 [`eslint-plugin-react-hooks` 플러그인](https://www.npmjs.com/package/eslint-plugin-react-hooks)을 사용할 수 있습니다.\n\n<Note>\n\n[커스텀 Hook](/learn/reusing-logic-with-custom-hooks)은 다른 Hook을 *호출할 수 있습니다*. (이것이 바로 커스텀 Hook의 주된 목적입니다.) 커스텀 Hook도 함수 컴포넌트가 렌더링되는 동안에만 호출될 수 있기 때문입니다.\n\n</Note>\n\n---\n\n## Hook을 React 함수에서만 호출하세요 {/*only-call-hooks-from-react-functions*/}\n\n일반 자바스크립트 함수에서 Hook을 호출하지 마세요. 대신 다음과 같이 사용할 수 있습니다.\n\n✅ Hook을 React 함수 컴포넌트에서 호출하세요.\n✅ Hook을 [커스텀 Hook](/learn/reusing-logic-with-custom-hooks#extracting-your-own-custom-hook-from-a-component)에서 호출하세요.\n\n이 규칙을 따르면 컴포넌트의 모든 상태 관리 로직이 소스 코드에서 명확히 보입니다.\n\n```js {2,5}\nfunction FriendList() {\n  const [onlineStatus, setOnlineStatus] = useOnlineStatus(); // ✅\n}\n\nfunction setOnlineStatus() { // ❌ 컴포넌트나 커스텀 Hook이 아닙니다!\n  const [onlineStatus, setOnlineStatus] = useOnlineStatus();\n}\n```\n"
  },
  {
    "path": "src/content/versions.md",
    "content": "---\ntitle: React 버전\n---\n\n<Intro>\n\n[ko.react.dev](/)의 React 문서는 최신 버전의 React에 대한 문서를 제공합니다.\n\n</Intro>\n\n주<sup>Major, 主</sup> 버전 내에서 문서를 최신 상태로 유지하기 위해 노력하며, 부<sup>Minor, 部</sup> 또는 수<sup>Patch, 修</sup> 버전에 대한 문서는 게시하지 않습니다. 새로운 주<sup>Major, 主</sup> 버전이 출시되면 이전 버전의 문서는 `x.react.dev`로 보관합니다. 자세한 내용은 [버전 관리 정책](/community/versioning-policy)을 참고하세요.\n\n이전의 주<sup>Major, 主</sup> 버전에 대한 기록은 아래에서 찾을 수 있습니다.\n\n## 최신 버전: 19.2 {/*latest-version*/}\n\n- [react.dev](https://react.dev) {/*docs-19*/}\n\n## 이전 버전 {/*previous-versions*/}\n\n- [18.react.dev](https://18.react.dev) {/*docs-18*/} ([React v18 한글](https://ko-react-exy5xcwjj-fbopensource.vercel.app/))\n- [17.react.dev](https://17.react.dev) {/*docs-17*/}\n- [16.react.dev](https://16.react.dev) {/*docs-16*/}\n- [15.react.dev](https://15.react.dev) {/*docs-15*/}\n\n<Note>\n\n#### 이전 문서들 {/*legacy-docs*/}\n\n2023년, React 18에 대한 [새로운 문서](/blog/2023/03/16/introducing-react-dev)를 [react.dev](https://react.dev)로 출시했습니다. 이전의 React 18 문서는 [legacy.reactjs.org](https://legacy.reactjs.org)에서 볼 수 있습니다. 버전 17 이하의 문서는 이전 사이트에 호스팅됩니다.\n\nReact 15 이전 버전의 경우, [15.react.dev](https://15.react.dev)를 참고하세요.\n\n</Note>\n\n## Changelog {/*changelog*/}\n\n### React 19 {/*react-19*/}\n\n**Blog Posts**\n- [React v19](/blog/2024/12/05/react-19)\n- [React 19 Upgrade Guide](/blog/2024/04/25/react-19-upgrade-guide)\n- [React Compiler Beta Release](/blog/2024/10/21/react-compiler-beta-release)\n- [React Compiler v1.0](/blog/2025/10/07/react-compiler-1)\n- [React 19.2](/blog/2025/10/01/react-19-2)\n\n**Talks**\n- [React 19 Keynote](https://www.youtube.com/watch?v=lyEKhv8-3n0)\n- [A Roadmap to React 19](https://www.youtube.com/watch?v=R0B2HsSM78s)\n- [What's new in React 19](https://www.youtube.com/watch?v=AJOGzVygGcY)\n- [React for Two Computers](https://www.youtube.com/watch?v=ozI4V_29fj4)\n- [React Compiler Deep Dive](https://www.youtube.com/watch?v=uA_PVyZP7AI)\n- [React Compiler Case Studies](https://www.youtube.com/watch?v=lvhPq5chokM)\n- [React 19 Deep Dive: Coordinating HTML](https://www.youtube.com/watch?v=IBBN-s77YSI)\n\n**Releases**\n- [v19.2.1 (December, 2025)](https://github.com/facebook/react/blob/main/CHANGELOG.md#1922-dec-11-2025)\n- [v19.2.1 (December, 2025)](https://github.com/facebook/react/blob/main/CHANGELOG.md#1921-dec-3-2025)\n- [v19.2.0 (October, 2025)](https://github.com/facebook/react/blob/main/CHANGELOG.md#1920-october-1st-2025)\n- [v19.1.3 (December, 2025)](https://github.com/facebook/react/blob/main/CHANGELOG.md#1913-dec-11-2025)\n- [v19.1.2 (December, 2025)](https://github.com/facebook/react/blob/main/CHANGELOG.md#1912-dec-3-2025)\n- [v19.1.1 (July, 2025)](https://github.com/facebook/react/blob/main/CHANGELOG.md#1911-july-28-2025)\n- [v19.1.0 (March, 2025)](https://github.com/facebook/react/blob/main/CHANGELOG.md#1910-march-28-2025)\n- [v19.0.2 (December, 2025)](https://github.com/facebook/react/blob/main/CHANGELOG.md#1902-dec-11-2025)\n- [v19.0.1 (December, 2025)](https://github.com/facebook/react/blob/main/CHANGELOG.md#1901-dec-3-2025)\n- [v19.0.0 (December, 2024)](https://github.com/facebook/react/blob/main/CHANGELOG.md#1900-december-5-2024)\n\n### React 18 {/*react-18*/}\n\n**Blog Posts**\n- [React v18.0](/blog/2022/03/29/react-v18)\n- [How to Upgrade to React 18](/blog/2022/03/08/react-18-upgrade-guide)\n- [The Plan for React 18](/blog/2021/06/08/the-plan-for-react-18)\n\n**Talks**\n- [React 18 Keynote](https://www.youtube.com/watch?v=FZ0cG47msEk)\n- [React 18 for app developers](https://www.youtube.com/watch?v=ytudH8je5ko)\n- [Streaming Server Rendering with Suspense](https://www.youtube.com/watch?v=pj5N-Khihgc)\n- [React without memo](https://www.youtube.com/watch?v=lGEMwh32soc)\n- [React Docs Keynote](https://www.youtube.com/watch?v=mneDaMYOKP8)\n- [React Developer Tooling](https://www.youtube.com/watch?v=oxDfrke8rZg)\n- [The first React Working Group](https://www.youtube.com/watch?v=qn7gRClrC9U)\n- [React 18 for External Store Libraries](https://www.youtube.com/watch?v=oPfSC5bQPR8)\n\n**Releases**\n- [v18.3.1 (April, 2024)](https://github.com/facebook/react/blob/main/CHANGELOG.md#1831-april-26-2024)\n- [v18.3.0 (April, 2024)](https://github.com/facebook/react/blob/main/CHANGELOG.md#1830-april-25-2024)\n- [v18.2.0 (June, 2022)](https://github.com/facebook/react/blob/main/CHANGELOG.md#1820-june-14-2022)\n- [v18.1.0 (April, 2022)](https://github.com/facebook/react/blob/main/CHANGELOG.md#1810-april-26-2022)\n- [v18.0.0 (March 2022)](https://github.com/facebook/react/blob/main/CHANGELOG.md#1800-march-29-2022)\n\n### React 17 {/*react-17*/}\n\n**Blog Posts**\n- [React v17.0](https://legacy.reactjs.org/blog/2020/10/20/react-v17.html)\n- [Introducing the New JSX Transform](https://legacy.reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html)\n- [React v17.0 Release Candidate: No New Features](https://legacy.reactjs.org/blog/2020/08/10/react-v17-rc.html)\n\n**Releases**\n- [v17.0.2 (March 2021)](https://github.com/facebook/react/blob/main/CHANGELOG.md#1702-march-22-2021)\n- [v17.0.1 (October 2020)](https://github.com/facebook/react/blob/main/CHANGELOG.md#1701-october-22-2020)\n- [v17.0.0 (October 2020)](https://github.com/facebook/react/blob/main/CHANGELOG.md#1700-october-20-2020)\n\n### React 16 {/*react-16*/}\n\n**Blog Posts**\n- [React v16.0](https://legacy.reactjs.org/blog/2017/09/26/react-v16.0.html)\n- [DOM Attributes in React 16](https://legacy.reactjs.org/blog/2017/09/08/dom-attributes-in-react-16.html)\n- [Error Handling in React 16](https://legacy.reactjs.org/blog/2017/07/26/error-handling-in-react-16.html)\n- [React v16.2.0: Improved Support for Fragments](https://legacy.reactjs.org/blog/2017/11/28/react-v16.2.0-fragment-support.html)\n- [React v16.4.0: Pointer Events](https://legacy.reactjs.org/blog/2018/05/23/react-v-16-4.html)\n- [React v16.4.2: Server-side vulnerability fix](https://legacy.reactjs.org/blog/2018/08/01/react-v-16-4-2.html)\n- [React v16.6.0: lazy, memo and contextType](https://legacy.reactjs.org/blog/2018/10/23/react-v-16-6.html)\n- [React v16.7: No, This Is Not the One With Hooks](https://legacy.reactjs.org/blog/2018/12/19/react-v-16-7.html)\n- [React v16.8: The One With Hooks](https://legacy.reactjs.org/blog/2019/02/06/react-v16.8.0.html)\n- [React v16.9.0 and the Roadmap Update](https://legacy.reactjs.org/blog/2019/08/08/react-v16.9.0.html)\n- [React v16.13.0](https://legacy.reactjs.org/blog/2020/02/26/react-v16.13.0.html)\n\n**Releases**\n- [v16.14.0 (October 2020)](https://github.com/facebook/react/blob/main/CHANGELOG.md#16140-october-14-2020)\n- [v16.13.1 (March 2020)](https://github.com/facebook/react/blob/main/CHANGELOG.md#16131-march-19-2020)\n- [v16.13.0 (February 2020)](https://github.com/facebook/react/blob/main/CHANGELOG.md#16130-february-26-2020)\n- [v16.12.0 (November 2019)](https://github.com/facebook/react/blob/main/CHANGELOG.md#16120-november-14-2019)\n- [v16.11.0 (October 2019)](https://github.com/facebook/react/blob/main/CHANGELOG.md#16110-october-22-2019)\n- [v16.10.2 (October 2019)](https://github.com/facebook/react/blob/main/CHANGELOG.md#16102-october-3-2019)\n- [v16.10.1 (September 2019)](https://github.com/facebook/react/blob/main/CHANGELOG.md#16101-september-28-2019)\n- [v16.10.0 (September 2019)](https://github.com/facebook/react/blob/main/CHANGELOG.md#16100-september-27-2019)\n- [v16.9.0 (August 2019)](https://github.com/facebook/react/blob/main/CHANGELOG.md#1690-august-8-2019)\n- [v16.8.6 (March 2019)](https://github.com/facebook/react/blob/main/CHANGELOG.md#1686-march-27-2019)\n- [v16.8.5 (March 2019)](https://github.com/facebook/react/blob/main/CHANGELOG.md#1685-march-22-2019)\n- [v16.8.4 (March 2019)](https://github.com/facebook/react/blob/main/CHANGELOG.md#1684-march-5-2019)\n- [v16.8.3 (February 2019)](https://github.com/facebook/react/blob/main/CHANGELOG.md#1683-february-21-2019)\n- [v16.8.2 (February 2019)](https://github.com/facebook/react/blob/main/CHANGELOG.md#1682-february-14-2019)\n- [v16.8.1 (February 2019)](https://github.com/facebook/react/blob/main/CHANGELOG.md#1681-february-6-2019)\n- [v16.8.0 (February 2019)](https://github.com/facebook/react/blob/main/CHANGELOG.md#1680-february-6-2019)\n- [v16.7.0 (December 2018)](https://github.com/facebook/react/blob/main/CHANGELOG.md#1670-december-19-2018)\n- [v16.6.3 (November 2018)](https://github.com/facebook/react/blob/main/CHANGELOG.md#1663-november-12-2018)\n- [v16.6.2 (November 2018)](https://github.com/facebook/react/blob/main/CHANGELOG.md#1662-november-12-2018)\n- [v16.6.1 (November 2018)](https://github.com/facebook/react/blob/main/CHANGELOG.md#1661-november-6-2018)\n- [v16.6.0 (October 2018)](https://github.com/facebook/react/blob/main/CHANGELOG.md#1660-october-23-2018)\n- [v16.5.2 (September 2018)](https://github.com/facebook/react/blob/main/CHANGELOG.md#1652-september-18-2018)\n- [v16.5.1 (September 2018)](https://github.com/facebook/react/blob/main/CHANGELOG.md#1651-september-13-2018)\n- [v16.5.0 (September 2018)](https://github.com/facebook/react/blob/main/CHANGELOG.md#1650-september-5-2018)\n- [v16.4.2 (August 2018)](https://github.com/facebook/react/blob/main/CHANGELOG.md#1642-august-1-2018)\n- [v16.4.1 (June 2018)](https://github.com/facebook/react/blob/main/CHANGELOG.md#1641-june-13-2018)\n- [v16.4.0 (May 2018)](https://github.com/facebook/react/blob/main/CHANGELOG.md#1640-may-23-2018)\n- [v16.3.3 (August 2018)](https://github.com/facebook/react/blob/main/CHANGELOG.md#1633-august-1-2018)\n- [v16.3.2 (April 2018)](https://github.com/facebook/react/blob/main/CHANGELOG.md#1632-april-16-2018)\n- [v16.3.1 (April 2018)](https://github.com/facebook/react/blob/main/CHANGELOG.md#1631-april-3-2018)\n- [v16.3.0 (March 2018)](https://github.com/facebook/react/blob/main/CHANGELOG.md#1630-march-29-2018)\n- [v16.2.1 (August 2018)](https://github.com/facebook/react/blob/main/CHANGELOG.md#1621-august-1-2018)\n- [v16.2.0 (November 2017)](https://github.com/facebook/react/blob/main/CHANGELOG.md#1620-november-28-2017)\n- [v16.1.2 (August 2018)](https://github.com/facebook/react/blob/main/CHANGELOG.md#1612-august-1-2018)\n- [v16.1.1 (November 2017)](https://github.com/facebook/react/blob/main/CHANGELOG.md#1611-november-13-2017)\n- [v16.1.0 (November 2017)](https://github.com/facebook/react/blob/main/CHANGELOG.md#1610-november-9-2017)\n- [v16.0.1 (August 2018)](https://github.com/facebook/react/blob/main/CHANGELOG.md#1601-august-1-2018)\n- [v16.0 (September 2017)](https://github.com/facebook/react/blob/main/CHANGELOG.md#1600-september-26-2017)\n\n### React 15 {/*react-15*/}\n\n**Blog Posts**\n- [React v15.0](https://legacy.reactjs.org/blog/2016/04/07/react-v15.html)\n- [React v15.0 Release Candidate 2](https://legacy.reactjs.org/blog/2016/03/16/react-v15-rc2.html)\n- [React v15.0 Release Candidate](https://legacy.reactjs.org/blog/2016/03/07/react-v15-rc1.html)\n- [New Versioning Scheme](https://legacy.reactjs.org/blog/2016/02/19/new-versioning-scheme.html)\n- [Discontinuing IE 8 Support in React DOM](https://legacy.reactjs.org/blog/2016/01/12/discontinuing-ie8-support.html)\n- [Introducing React's Error Code System](https://legacy.reactjs.org/blog/2016/07/11/introducing-reacts-error-code-system.html)\n- [React v15.0.1](https://legacy.reactjs.org/blog/2016/04/08/react-v15.0.1.html)\n- [React v15.4.0](https://legacy.reactjs.org/blog/2016/11/16/react-v15.4.0.html)\n- [React v15.5.0](https://legacy.reactjs.org/blog/2017/04/07/react-v15.5.0.html)\n- [React v15.6.0](https://legacy.reactjs.org/blog/2017/06/13/react-v15.6.0.html)\n- [React v15.6.2](https://legacy.reactjs.org/blog/2017/09/25/react-v15.6.2.html)\n\n**Releases**\n- [v15.7.0 (October 2017)](https://github.com/facebook/react/blob/main/CHANGELOG.md#1570-october-14-2020)\n- [v15.6.2 (September 2017)](https://github.com/facebook/react/blob/main/CHANGELOG.md#1562-september-25-2017)\n- [v15.6.1 (June 2017)](https://github.com/facebook/react/blob/main/CHANGELOG.md#1561-june-14-2017)\n- [v15.6.0 (June 2017)](https://github.com/facebook/react/blob/main/CHANGELOG.md#1560-june-13-2017)\n- [v15.5.4 (April 2017)](https://github.com/facebook/react/blob/main/CHANGELOG.md#1554-april-11-2017)\n- [v15.5.3 (April 2017)](https://github.com/facebook/react/blob/main/CHANGELOG.md#1553-april-7-2017)\n- [v15.5.2 (April 2017)](https://github.com/facebook/react/blob/main/CHANGELOG.md#1552-april-7-2017)\n- [v15.5.1 (April 2017)](https://github.com/facebook/react/blob/main/CHANGELOG.md#1551-april-7-2017)\n- [v15.5.0 (April 2017)](https://github.com/facebook/react/blob/main/CHANGELOG.md#1550-april-7-2017)\n- [v15.4.2 (January 2016)](https://github.com/facebook/react/blob/main/CHANGELOG.md#1542-january-6-2017)\n- [v15.4.1 (November 2016)](https://github.com/facebook/react/blob/main/CHANGELOG.md#1541-november-22-2016)\n- [v15.4.0 (November 2016)](https://github.com/facebook/react/blob/main/CHANGELOG.md#1540-november-16-2016)\n- [v15.3.2 (September 2016)](https://github.com/facebook/react/blob/main/CHANGELOG.md#1532-september-19-2016)\n- [v15.3.1 (August 2016)](https://github.com/facebook/react/blob/main/CHANGELOG.md#1531-august-19-2016)\n- [v15.3.0 (July 2016)](https://github.com/facebook/react/blob/main/CHANGELOG.md#1530-july-29-2016)\n- [v15.2.1 (July 2016)](https://github.com/facebook/react/blob/main/CHANGELOG.md#1521-july-8-2016)\n- [v15.2.0 (July 2016)](https://github.com/facebook/react/blob/main/CHANGELOG.md#1520-july-1-2016)\n- [v15.1.0 (May 2016)](https://github.com/facebook/react/blob/main/CHANGELOG.md#1510-may-20-2016)\n- [v15.0.2 (April 2016)](https://github.com/facebook/react/blob/main/CHANGELOG.md#1502-april-29-2016)\n- [v15.0.1 (April 2016)](https://github.com/facebook/react/blob/main/CHANGELOG.md#1501-april-8-2016)\n- [v15.0.0 (April 2016)](https://github.com/facebook/react/blob/main/CHANGELOG.md#1500-april-7-2016)\n\n### React 0.14 {/*react-14*/}\n\n**Blog Posts**\n- [React v0.14](https://legacy.reactjs.org/blog/2015/10/07/react-v0.14.html)\n- [React v0.14 Release Candidate](https://legacy.reactjs.org/blog/2015/09/10/react-v0.14-rc1.html)\n- [React v0.14 Beta 1](https://legacy.reactjs.org/blog/2015/07/03/react-v0.14-beta-1.html)\n- [New React Developer Tools](https://legacy.reactjs.org/blog/2015/09/02/new-react-developer-tools.html)\n- [New React Devtools Beta](https://legacy.reactjs.org/blog/2015/08/03/new-react-devtools-beta.html)\n- [React v0.14.1](https://legacy.reactjs.org/blog/2015/10/28/react-v0.14.1.html)\n- [React v0.14.2](https://legacy.reactjs.org/blog/2015/11/02/react-v0.14.2.html)\n- [React v0.14.3](https://legacy.reactjs.org/blog/2015/11/18/react-v0.14.3.html)\n- [React v0.14.4](https://legacy.reactjs.org/blog/2015/12/29/react-v0.14.4.html)\n- [React v0.14.8](https://legacy.reactjs.org/blog/2016/03/29/react-v0.14.8.html)\n\n**Releases**\n- [v0.14.10 (October 2020)](https://github.com/facebook/react/blob/main/CHANGELOG.md#01410-october-14-2020)\n- [v0.14.8 (March 2016)](https://github.com/facebook/react/blob/main/CHANGELOG.md#0148-march-29-2016)\n- [v0.14.7 (January 2016)](https://github.com/facebook/react/blob/main/CHANGELOG.md#0147-january-28-2016)\n- [v0.14.6 (January 2016)](https://github.com/facebook/react/blob/main/CHANGELOG.md#0146-january-6-2016)\n- [v0.14.5 (December 2015)](https://github.com/facebook/react/blob/main/CHANGELOG.md#0145-december-29-2015)\n- [v0.14.4 (December 2015)](https://github.com/facebook/react/blob/main/CHANGELOG.md#0144-december-29-2015)\n- [v0.14.3 (November 2015)](https://github.com/facebook/react/blob/main/CHANGELOG.md#0143-november-18-2015)\n- [v0.14.2 (November 2015)](https://github.com/facebook/react/blob/main/CHANGELOG.md#0142-november-2-2015)\n- [v0.14.1 (October 2015)](https://github.com/facebook/react/blob/main/CHANGELOG.md#0141-october-28-2015)\n- [v0.14.0 (October 2015)](https://github.com/facebook/react/blob/main/CHANGELOG.md#0140-october-7-2015)\n\n### React 0.13 {/*react-13*/}\n\n**Blog Posts**\n- [React Native v0.4](https://legacy.reactjs.org/blog/2015/04/17/react-native-v0.4.html)\n- [React v0.13](https://legacy.reactjs.org/blog/2015/03/10/react-v0.13.html)\n- [React v0.13 RC2](https://legacy.reactjs.org/blog/2015/03/03/react-v0.13-rc2.html)\n- [React v0.13 RC](https://legacy.reactjs.org/blog/2015/02/24/react-v0.13-rc1.html)\n- [React v0.13.0 Beta 1](https://legacy.reactjs.org/blog/2015/01/27/react-v0.13.0-beta-1.html)\n- [Streamlining React Elements](https://legacy.reactjs.org/blog/2015/02/24/streamlining-react-elements.html)\n- [Introducing Relay and GraphQL](https://legacy.reactjs.org/blog/2015/02/20/introducing-relay-and-graphql.html)\n- [Introducing React Native](https://legacy.reactjs.org/blog/2015/03/26/introducing-react-native.html)\n- [React v0.13.1](https://legacy.reactjs.org/blog/2015/03/16/react-v0.13.1.html)\n- [React v0.13.2](https://legacy.reactjs.org/blog/2015/04/18/react-v0.13.2.html)\n- [React v0.13.3](https://legacy.reactjs.org/blog/2015/05/08/react-v0.13.3.html)\n\n**Releases**\n- [v0.13.3 (May 2015)](https://github.com/facebook/react/blob/main/CHANGELOG.md#0133-may-8-2015)\n- [v0.13.2 (April 2015)](https://github.com/facebook/react/blob/main/CHANGELOG.md#0132-april-18-2015)\n- [v0.13.1 (March 2015)](https://github.com/facebook/react/blob/main/CHANGELOG.md#0131-march-16-2015)\n- [v0.13.0 (March 2015)](https://github.com/facebook/react/blob/main/CHANGELOG.md#0130-march-10-2015)\n\n### React 0.12 {/*react-12*/}\n\n**Blog Posts**\n- [React v0.12](https://legacy.reactjs.org/blog/2014/10/28/react-v0.12.html)\n- [React v0.12 RC](https://legacy.reactjs.org/blog/2014/10/16/react-v0.12-rc1.html)\n- [Introducing React Elements](https://legacy.reactjs.org/blog/2014/10/14/introducing-react-elements.html)\n- [React v0.12.2](https://legacy.reactjs.org/blog/2014/12/18/react-v0.12.2.html)\n\n**Releases**\n- [v0.12.2 (December 2014)](https://github.com/facebook/react/blob/main/CHANGELOG.md#0122-december-18-2014)\n- [v0.12.1 (November 2014)](https://github.com/facebook/react/blob/main/CHANGELOG.md#0121-november-18-2014)\n- [v0.12.0 (October 2014)](https://github.com/facebook/react/blob/main/CHANGELOG.md#0120-october-28-2014)\n\n### React 0.11 {/*react-11*/}\n\n**Blog Posts**\n- [React v0.11](https://legacy.reactjs.org/blog/2014/07/17/react-v0.11.html)\n- [React v0.11 RC](https://legacy.reactjs.org/blog/2014/07/13/react-v0.11-rc1.html)\n- [One Year of Open-Source React](https://legacy.reactjs.org/blog/2014/05/29/one-year-of-open-source-react.html)\n- [The Road to 1.0](https://legacy.reactjs.org/blog/2014/03/28/the-road-to-1.0.html)\n- [React v0.11.1](https://legacy.reactjs.org/blog/2014/07/25/react-v0.11.1.html)\n- [React v0.11.2](https://legacy.reactjs.org/blog/2014/09/16/react-v0.11.2.html)\n- [Introducing the JSX Specificaion](https://legacy.reactjs.org/blog/2014/09/03/introducing-the-jsx-specification.html)\n\n**Releases**\n- [v0.11.2 (September 2014)](https://github.com/facebook/react/blob/main/CHANGELOG.md#0112-september-16-2014)\n- [v0.11.1 (July 2014)](https://github.com/facebook/react/blob/main/CHANGELOG.md#0111-july-24-2014)\n- [v0.11.0 (July 2014)](https://github.com/facebook/react/blob/main/CHANGELOG.md#0110-july-17-2014)\n\n### React 0.10 and below {/*react-10-and-below*/}\n\n**Blog Posts**\n- [React v0.10](https://legacy.reactjs.org/blog/2014/03/21/react-v0.10.html)\n- [React v0.10 RC](https://legacy.reactjs.org/blog/2014/03/19/react-v0.10-rc1.html)\n- [React v0.9](https://legacy.reactjs.org/blog/2014/02/20/react-v0.9.html)\n- [React v0.9 RC](https://legacy.reactjs.org/blog/2014/02/16/react-v0.9-rc1.html)\n- [React Chrome Developer Tools](https://legacy.reactjs.org/blog/2014/01/02/react-chrome-developer-tools.html)\n- [React v0.8](https://legacy.reactjs.org/blog/2013/12/19/react-v0.8.0.html)\n- [React v0.5.2, v0.4.2](https://legacy.reactjs.org/blog/2013/12/18/react-v0.5.2-v0.4.2.html)\n- [React v0.5.1](https://legacy.reactjs.org/blog/2013/10/29/react-v0-5-1.html)\n- [React v0.5](https://legacy.reactjs.org/blog/2013/10/16/react-v0.5.0.html)\n- [React v0.4.1](https://legacy.reactjs.org/blog/2013/07/26/react-v0-4-1.html)\n- [React v0.4.0](https://legacy.reactjs.org/blog/2013/07/17/react-v0-4-0.html)\n- [New in React v0.4: Prop Validation and Default Values](https://legacy.reactjs.org/blog/2013/07/11/react-v0-4-prop-validation-and-default-values.html)\n- [New in React v0.4: Autobind by Default](https://legacy.reactjs.org/blog/2013/07/02/react-v0-4-autobind-by-default.html)\n- [React v0.3.3](https://legacy.reactjs.org/blog/2013/07/02/react-v0-4-autobind-by-default.html)\n\n**Releases**\n- [v0.10.0 (March 2014)](https://github.com/facebook/react/blob/main/CHANGELOG.md#0100-march-21-2014)\n- [v0.9.0 (February 2014)](https://github.com/facebook/react/blob/main/CHANGELOG.md#090-february-20-2014)\n- [v0.8.0 (December 2013)](https://github.com/facebook/react/blob/main/CHANGELOG.md#080-december-19-2013)\n- [v0.5.2 (December 2013)](https://github.com/facebook/react/blob/main/CHANGELOG.md#052-042-december-18-2013)\n- [v0.5.1 (October 2013)](https://github.com/facebook/react/blob/main/CHANGELOG.md#051-october-29-2013)\n- [v0.5.0 (October 2013)](https://github.com/facebook/react/blob/main/CHANGELOG.md#050-october-16-2013)\n- [v0.4.1 (July 2013)](https://github.com/facebook/react/blob/main/CHANGELOG.md#041-july-26-2013)\n- [v0.4.0 (July 2013)](https://github.com/facebook/react/blob/main/CHANGELOG.md#040-july-17-2013)\n- [v0.3.3 (June 2013)](https://github.com/facebook/react/blob/main/CHANGELOG.md#033-june-20-2013)\n- [v0.3.2 (May 2013)](https://github.com/facebook/react/blob/main/CHANGELOG.md#032-may-31-2013)\n- [v0.3.1 (May 2013)](https://github.com/facebook/react/blob/main/CHANGELOG.md#031-may-30-2013)\n- [v0.3.0 (May 2013)](https://github.com/facebook/react/blob/main/CHANGELOG.md#031-may-30-2013)\n\n### 최초의 커밋 {/*initial-commit*/}\n\nReact는 2013년 5월 29일에 오픈소스로 공개되었습니다. 최초의 커밋은 [`75897c`: 최초 공개 릴리즈](https://github.com/facebook/react/commit/75897c2dcd1dd3a6ca46284dd37e13d22b4b16b4)입니다.\n\n첫 블로그 게시글을 참고하세요. [왜 우리는 React를 만들었는가?](https://legacy.reactjs.org/blog/2013/06/05/why-react.html)\n\nReact는 2013년에 페이스북<sup>Facebook</sup> 시애틀에서 오픈소스로 공개되었습니다.\n\n<iframe width=\"560\" height=\"315\" src=\"https://www.youtube.com/embed/XxVg_s8xAms?si=466vSJrnXTn05j9A\" title=\"YouTube video player\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share\" referrerpolicy=\"strict-origin-when-cross-origin\" allowfullscreen></iframe>\n"
  },
  {
    "path": "src/content/warnings/invalid-aria-prop.md",
    "content": "---\ntitle: 유효하지 않은 ARIA Prop 경고\n---\n\n이 경고는 WAI-ARIA(Web Accessibility Initiative - Accessible Rich Internet Application) [명세](https://www.w3.org/TR/wai-aria-1.1/#states_and_properties)에 존재하지 않는 `aria-*` 프로퍼티를 가진 DOM 요소를 렌더링하려고 할 때 발생합니다.\n\n1. 유효한 prop을 사용하고 있다고 생각하는 경우 철자를 주의 깊게 확인해 보세요. `aria-labelledby`와 `aria-activedescendant`는 철자 실수가 잦습니다.\n\n2. `aria-role`을 작성한 경우 `role`을 사용하려고 한 것일 수 있습니다.\n\n3. 그렇지 않은 경우, 최신 버전의 React DOM을 사용하고 ARIA 명세에 나열된 유효한 프로퍼티 이름을 사용하고 있는지 확인한 경우 [버그를 보고해 주세요](https://github.com/facebook/react/issues/new/choose).\n"
  },
  {
    "path": "src/content/warnings/invalid-hook-call-warning.md",
    "content": "---\ntitle: Hooks의 규칙\n---\n\n아래와 같은 오류 메시지를 받았기 때문에 여기에 오신 것 같습니다:\n\n<ConsoleBlock level=\"error\">\n\nHooks can only be called inside the body of a function component.\n\n</ConsoleBlock>\n\n이 오류 메시지를 보는 이유는 보통 다음과 같습니다:\n\n1. **Hooks의 규칙을 위반**했을 수 있습니다.\n2. React와 React DOM의 **버전이 일치하지 않을** 수 있습니다.\n3. 동일한 앱에서 **여러 개의 React 복사본**이 있을 수 있습니다.\n\n이러한 경우들을 살펴보겠습니다.\n\n## Hooks의 규칙을 위반 {/*breaking-rules-of-hooks*/}\n\nReact에서 `use`로 시작하는 함수를 [*Hooks*](/reference/react)라고 합니다.\n\n**루프, 조건문 또는 중첩된 함수 내에서 Hooks를 호출하지 마세요.** 대신 Hooks를 항상 React 함수의 최상위 수준에서, return문 전에 사용하세요. Hooks는 React가 함수형 컴포넌트를 렌더링하는 동안에만 호출할 수 있습니다:\n\n* ✅ [함수형 컴포넌트](/learn/your-first-component)의 본문의 최상위 수준에서 호출하세요.\n* ✅ [사용자정의 Hook](/learn/reusing-logic-with-custom-hooks)의 본문에서 최상위 수준에서 호출하세요.\n\n```js{2-3,8-9}\nfunction Counter() {\n  // ✅ 좋은 예: 함수 컴포넌트의 최상위 수준\n  const [count, setCount] = useState(0);\n  // ...\n}\n\nfunction useWindowWidth() {\n  // ✅ 좋은 예: 사용자 정의 Hook의 최상위 수준\n  const [width, setWidth] = useState(window.innerWidth);\n  // ...\n}\n```\n\n그 외의 경우에는 Hooks(이름이 `use`로 시작하는 함수)를 호출하는 것은 지원되지 **않습니다**. 예를 들어:\n\n🔴 조건문 또는 루프 내에서 Hooks를 호출하지 마세요.\n🔴 조건부 `return` 문 이후에 Hooks를 호출하지 마세요.\n🔴 이벤트 핸들러에서 Hooks를 호출하지 마세요.\n🔴 클래스형 컴포넌트에서 Hooks를 호출하지 마세요.\n🔴 `useMemo`, `useReducer`, 또는 `useEffect`에 전달되는 함수 내에서 Hooks를 호출하지 마세요.\n\n이러한 규칙을 어긴다면 이 오류를 볼 수 있습니다.\n\n```js{3-4,11-12,20-21}\nfunction Bad({ cond }) {\n  if (cond) {\n    // 🔴 잘못된 예: 조건문 내부 (해결하기 위해서는 밖으로 옮겨주세요!)\n    const theme = useContext(ThemeContext);\n  }\n  // ...\n}\n\nfunction Bad() {\n  for (let i = 0; i < 10; i++) {\n    // 🔴 잘못된 예: 루프 내부 (해결하기 위해서는 밖으로 옮겨주세요!)\n    const theme = useContext(ThemeContext);\n  }\n  // ...\n}\n\nfunction Bad({ cond }) {\n  if (cond) {\n    return;\n  }\n  // 🔴 잘못된 예: 조건부 반환 이후 (해결하기 위해서는 return문 보다 전으로 옮겨주세요!)\n  const theme = useContext(ThemeContext);\n  // ...\n}\n\nfunction Bad() {\n  function handleClick() {\n    // 🔴 잘못된 예: 이벤트 핸들러 내부 (해결하기 위해서는 밖으로 옮겨주세요!)\n    const theme = useContext(ThemeContext);\n  }\n  // ...\n}\n\nfunction Bad() {\n  const style = useMemo(() => {\n    // 🔴 잘못된 예: useMemo 내부 (해결하기 위해서는 밖으로 옮겨주세요!)\n    const theme = useContext(ThemeContext);\n    return createStyle(theme);\n  });\n  // ...\n}\n\nclass Bad extends React.Component {\n  render() {\n    // 🔴 잘못된 예: 클래스 컴포넌트 내부 (해결하기 위해서는 클래스형 대신 함수형 컴포넌트를 작성해주세요!)\n    useEffect(() => {})\n    // ...\n  }\n}\n```\n\n이러한 실수를 잡기 위해 [`eslint-plugin-react-hooks` 플러그인](https://www.npmjs.com/package/eslint-plugin-react-hooks)을 사용할 수 있습니다.\n\n<Note>\n\n[사용자 정의 Hook](/learn/reusing-logic-with-custom-hooks)은 다른 Hooks를 호출할 수 *있습니다* (그것이 사용자 정의 Hook의 목적이기 때문입니다). 이는 사용자 정의 Hook도 함수형 컴포넌트가 렌더링되는 동안에만 호출되기 때문에 가능합니다.\n\n</Note>\n\n## React와 React DOM 버전의 불일치 {/*mismatching-versions-of-react-and-react-dom*/}\n\n`react-dom` (< 16.8.0)이나 `react-native` (< 0.59)의 버전이 Hooks를 아직 지원하지 않을 수 있습니다. 애플리케이션 폴더에서 `npm ls react-dom` 또는 `npm ls react-native`을 실행하여 사용 중인 버전을 확인할 수 있습니다. 여러 개의 버전이 발견된 경우에도, 문제를 일으킬 수 있습니다(아래에서 더 자세히 설명합니다).\n\n## 중복된 React {/*duplicate-react*/}\n\nHooks가 작동하려면 애플리케이션 코드에서 가져온 `react`의 import가 react-dom 패키지 내부에서 가져온 `react` import와 동일한 모듈을 가져와야 합니다.\n\n이러한 `react` import가 두 개의 다른 exports 객체를 가져온다면, 이 경고가 표시됩니다. 이는 실수로 **두 개의 `react` 패키지 사본이 생긴 경우**에 발생할 수 있습니다.\n\n패키지 관리를 위해 Node를 사용하는 경우 프로젝트 폴더에서 다음을 실행하여 이를 확인할 수 있습니다:\n\n<TerminalBlock>\n\nnpm ls react\n\n</TerminalBlock>\n\n만약 두 개 이상의 React가 보일 경우, 이러한 상황이 발생한 이유를 찾고 의존성 트리를 수정해야 합니다. 예를 들어, 사용 중인 라이브러리가 `react`를 피어 종속성(peer dependency)이 아닌 종속성(dependency)으로 잘못 지정한 경우입니다. 해당 라이브러리가 수정되기 전까지는 [Yarn resolutions](https://yarnpkg.com/lang/en/docs/selective-version-resolutions/)이 가능한 해결책입니다.\n\n또한 몇 가지 로그를 추가하고 개발 서버를 다시 시작하여 이 문제를 디버깅해 볼 수도 있습니다:\n\n```js\n// node_modules/react-dom/index.js에 추가\nwindow.React1 = require('react');\n\n// 컴포넌트 파일에 추가\nrequire('react-dom');\nwindow.React2 = require('react');\nconsole.log(window.React1 === window.React2);\n```\n\n만약 `false`가 출력된다면 두 개의 React가 있을 수 있으며, 이러한 상황이 발생한 이유를 찾아야 합니다. [이 이슈](https://github.com/facebook/react/issues/13991)는 커뮤니티에서 발견한 일반적인 원인에 대해 설명하고 있습니다.\n\n이 문제는 `npm link` 또는 유사한 기능을 사용할 때에도 발생할 수 있습니다. 이 경우 번들러는 2개의 React - 애플리케이션 폴더와 라이브러리 폴더에서 각각 하나씩을 \"보게\" 될 수 있습니다. `myapp`과 `mylib`이 형제 폴더라고 가정하면, `mylib`에서 `npm link ../myapp/node_modules/react`를 실행하여 라이브러리가 애플리케이션의 React 복사본을 사용하도록 할 수 있습니다.\n\n<Note>\n\n일반적으로 React는 한 페이지에서 여러 개의 독립적인 복사본을 사용할 수 있습니다 (예를 들어 앱과 타사 위젯이 모두 사용하는 경우). 문제는 컴포넌트와 그것과 함께 렌더링된 `react-dom` 복사본에서 `require('react')`가 다르게 처리된 경우에만 발생합니다.\n\n</Note>\n\n## 기타 원인 {/*other-causes*/}\n\n만약 이 모든 방법이 도움이 되지 않는다면, [이 이슈](https://github.com/facebook/react/issues/13991)에 의견을 남기고 도움을 요청하세요. 작은 재현 가능한 예시를 만들어보면 문제를 발견할 수도 있습니다.\n"
  },
  {
    "path": "src/content/warnings/react-dom-test-utils.md",
    "content": "---\ntitle: react-dom/test-utils Deprecation Warnings\n---\n\n## ReactDOMTestUtils.act() warning {/*reactdomtestutilsact-warning*/}\n\n`act` from `react-dom/test-utils` has been deprecated in favor of `act` from `react`.\n\nBefore:\n\n```js\nimport {act} from 'react-dom/test-utils';\n```\n\nAfter:\n\n```js\nimport {act} from 'react';\n```\n\n## Rest of ReactDOMTestUtils APIS {/*rest-of-reactdomtestutils-apis*/}\n\nAll APIs except `act` have been removed.\n\nThe React Team recommends migrating your tests to [@testing-library/react](https://testing-library.com/docs/react-testing-library/intro/) for a modern and well supported testing experience.\n\n### ReactDOMTestUtils.renderIntoDocument {/*reactdomtestutilsrenderintodocument*/}\n\n`renderIntoDocument` can be replaced with `render` from `@testing-library/react`.\n\nBefore:\n\n```js\nimport {renderIntoDocument} from 'react-dom/test-utils';\n\nrenderIntoDocument(<Component />);\n```\n\nAfter:\n\n```js\nimport {render} from '@testing-library/react';\n\nrender(<Component />);\n```\n\n### ReactDOMTestUtils.Simulate {/*reactdomtestutilssimulate*/}\n\n`Simulate` can be replaced with `fireEvent` from `@testing-library/react`.\n\nBefore:\n\n```js\nimport {Simulate} from 'react-dom/test-utils';\n\nconst element = document.querySelector('button');\nSimulate.click(element);\n```\n\nAfter:\n\n```js\nimport {fireEvent} from '@testing-library/react';\n\nconst element = document.querySelector('button');\nfireEvent.click(element);\n```\n\nBe aware that `fireEvent` dispatches an actual event on the element and doesn't just synthetically call the event handler.\n\n### List of all removed APIs {/*list-of-all-removed-apis-list-of-all-removed-apis*/}\n\n- `mockComponent()`\n- `isElement()`\n- `isElementOfType()`\n- `isDOMComponent()`\n- `isCompositeComponent()`\n- `isCompositeComponentWithType()`\n- `findAllInRenderedTree()`\n- `scryRenderedDOMComponentsWithClass()`\n- `findRenderedDOMComponentWithClass()`\n- `scryRenderedDOMComponentsWithTag()`\n- `findRenderedDOMComponentWithTag()`\n- `scryRenderedComponentsWithType()`\n- `findRenderedComponentWithType()`\n- `renderIntoDocument`\n- `Simulate`\n"
  },
  {
    "path": "src/content/warnings/react-test-renderer.md",
    "content": "---\ntitle: react-test-renderer Deprecation Warnings\n---\n\n## ReactTestRenderer.create() warning {/*reacttestrenderercreate-warning*/}\n\nreact-test-renderer is deprecated. A warning will fire whenever calling ReactTestRenderer.create() or ReactShallowRender.render(). The react-test-renderer package will remain available on NPM but will not be maintained and may break with new React features or changes to React's internals.\n\nThe React Team recommends migrating your tests to [@testing-library/react](https://testing-library.com/docs/react-testing-library/intro/) or [@testing-library/react-native](https://callstack.github.io/react-native-testing-library/docs/start/intro) for a modern and well supported testing experience.\n\n\n## new ShallowRenderer() warning {/*new-shallowrenderer-warning*/}\n\nThe react-test-renderer package no longer exports a shallow renderer at `react-test-renderer/shallow`. This was simply a repackaging of a previously extracted separate package: `react-shallow-renderer`. Therefore you can continue using the shallow renderer in the same way by installing it directly. See [Github](https://github.com/enzymejs/react-shallow-renderer) / [NPM](https://www.npmjs.com/package/react-shallow-renderer).\n"
  },
  {
    "path": "src/content/warnings/special-props.md",
    "content": "---\ntitle: 특별한 Props 경고\n---\n\nJSX 요소의 대부분의 props는 컴포넌트로 전달됩니다. 그러나 두 가지 특별한 props(`ref`와 `key`)는 React에서 사용되므로 컴포넌트로 전달되지 않습니다.\n\n예를 들어, 컴포넌트에서 `props.key`를 읽을 수는 없습니다. 자식 컴포넌트 내에서 동일한 값을 액세스해야 하는 경우 다른 prop으로 전달해야 합니다. (예:`<ListItemWrapper key={result.id} id={result.id} />`와 같이 전달하고 `props.id`를 읽습니다.). 이는 불필요한 중복처럼 보일 수 있지만, 앱 로직을 React의 힌트와 분리하는 것이 중요합니다.\n"
  },
  {
    "path": "src/content/warnings/unknown-prop.md",
    "content": "---\ntitle: 알 수 없는 Prop 경고\n---\n\n알 수 없는 Prop 경고는 React가 유효한 DOM 속성/프로퍼티로 인식하지 못하는 속성을 가진 DOM 요소를 렌더링하려고 할 때 발생합니다. DOM 요소에 불필요한 속성이 존재하지 않도록 확인해야 합니다.\n\n이 경고가 나타나는 가능한 이유는 다음과 같습니다:\n\n1. `{...props}` 또는 `cloneElement(element, props)`를 사용하고 있습니까? props를 자식 컴포넌트로 복사할 때, 부모 컴포넌트에만 해당하는 속성이 실수로 자식 컴포넌트로 전달되지 않도록 확인해야 합니다. 이 문제에 대한 보통의 해결 방법은 아래에서 설명합니다.\n\n2. 네이티브 DOM 노드에서 비표준 DOM 속성을 사용하고 있을 수 있습니다. 표준 DOM 요소에 사용자 정의 데이터를 전달하려면, [MDN에서](https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Using_data_attributes) 설명하는 사용자 정의 데이터 속성을 사용하는 것을 고려해 보세요.\n\n3. React가 지정한 속성을 아직 인식하지 못할 수 있습니다. 이는 나중에 나올 React 버전에서 수정될 가능성이 있습니다. 만약 속성 이름을 소문자로 작성하면, React는 경고 없이 해당 속성을 전달할 수 있습니다.\n\n4. `<myButton />`과같이 대문자 없이 React 컴포넌트를 사용하고 있을 수 있습니다. React는 대문자와 소문자의 컨벤션을 사용하여 사용자 정의 컴포넌트와 DOM 태그를 구분합니다. 자신의 React 컴포넌트를 작성할 때는 PascalCase를 사용해야 합니다. 예를 들어 `<myButton />` 대신 `<MyButton />`과같이 작성하세요.\n\n---\n\n만약 `{...props}`와 같은 방식으로 props를 전달하여 이 경고가 나타난다면, 부모 컴포넌트는 자식 컴포넌트에는 해당하지 않고 부모 컴포넌트에만 해당하는 속성을 \"소비(consume)\"해야 합니다. 예시:\n\n**나쁜 예:** 예기치 않은 `layout` prop이 `div` 태그로 전달됩니다.\n\n```js\nfunction MyDiv(props) {\n  if (props.layout === 'horizontal') {\n    // 나쁜 예! \"layout\"이 <div> tag가 이해하는 prop이 아닌 것을 알고 있기 때문입니다.\n    return <div {...props} style={getHorizontalStyle()} />\n  } else {\n    // 나쁜 예! \"layout\"이 <div> tag가 이해하는 prop이 아닌 것을 알고 있기 때문입니다.\n    return <div {...props} style={getVerticalStyle()} />\n  }\n}\n```\n\n**좋은 예:** 전개 구문을 사용하여 변수를 props에서 추출하고, 남은 props를 변수에 할당합니다.\n\n```js\nfunction MyDiv(props) {\n  const { layout, ...rest } = props\n  if (layout === 'horizontal') {\n    return <div {...rest} style={getHorizontalStyle()} />\n  } else {\n    return <div {...rest} style={getVerticalStyle()} />\n  }\n}\n```\n\n**좋은 예:** 속성을 새로운 객체에 할당하고, 사용한 키를 객체에서 삭제할 수도 있습니다. 원본 `this.props` 객체의 props를 삭제하지 않도록 주의해야 합니다. 해당 객체는 변경 불가능한 것으로 간주되어야 합니다.\n\n```js\nfunction MyDiv(props) {\n  const divProps = Object.assign({}, props);\n  delete divProps.layout;\n\n  if (props.layout === 'horizontal') {\n    return <div {...divProps} style={getHorizontalStyle()} />\n  } else {\n    return <div {...divProps} style={getVerticalStyle()} />\n  }\n}\n```\n"
  },
  {
    "path": "src/hooks/usePendingRoute.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport {useRouter} from 'next/router';\nimport {useState, useRef, useEffect} from 'react';\n\nconst usePendingRoute = () => {\n  const {events} = useRouter();\n  const [pendingRoute, setPendingRoute] = useState<string | null>(null);\n  const currentRoute = useRef<string | null>(null);\n  useEffect(() => {\n    let routeTransitionTimer: any = null;\n\n    const handleRouteChangeStart = (url: string) => {\n      clearTimeout(routeTransitionTimer);\n      routeTransitionTimer = setTimeout(() => {\n        if (currentRoute.current !== url) {\n          currentRoute.current = url;\n          setPendingRoute(url);\n        }\n      }, 100);\n    };\n    const handleRouteChangeComplete = () => {\n      setPendingRoute(null);\n      clearTimeout(routeTransitionTimer);\n    };\n    events.on('routeChangeStart', handleRouteChangeStart);\n    events.on('routeChangeComplete', handleRouteChangeComplete);\n\n    return () => {\n      events.off('routeChangeStart', handleRouteChangeStart);\n      events.off('routeChangeComplete', handleRouteChangeComplete);\n      clearTimeout(routeTransitionTimer);\n    };\n  }, [events]);\n\n  return pendingRoute;\n};\n\nexport default usePendingRoute;\n"
  },
  {
    "path": "src/pages/404.js",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport {Page} from 'components/Layout/Page';\nimport {MDXComponents} from 'components/MDX/MDXComponents';\nimport sidebarLearn from '../sidebarLearn.json';\n\nconst {Intro, MaxWidth, p: P, a: A} = MDXComponents;\n\nexport default function NotFound() {\n  return (\n    <Page\n      toc={[]}\n      meta={{title: '페이지를 찾을 수 없습니다'}}\n      routeTree={sidebarLearn}>\n      <MaxWidth>\n        <Intro>\n          <P>요청하신 페이지가 존재하지 않습니다.</P>\n          <P>\n            저희 잘못으로 인한 오류라면{', '}\n            수정할 수 있도록{' '}\n            <A href=\"https://github.com/reactjs/ko.react.dev/issues/new\">\n              저희에게 알려주세요.\n            </A>\n          </P>\n        </Intro>\n      </MaxWidth>\n    </Page>\n  );\n}\n"
  },
  {
    "path": "src/pages/500.js",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport {Page} from 'components/Layout/Page';\nimport {MDXComponents} from 'components/MDX/MDXComponents';\nimport sidebarLearn from '../sidebarLearn.json';\n\nconst {Intro, MaxWidth, p: P, a: A} = MDXComponents;\n\nexport default function NotFound() {\n  return (\n    <Page\n      toc={[]}\n      routeTree={sidebarLearn}\n      meta={{title: '문제가 발생했습니다'}}>\n      <MaxWidth>\n        <Intro>\n          <P>아주 큰 문제가 발생했습니다.</P>\n          <P>불편을 드려 죄송합니다.</P>\n          <P>\n            가능하시다면{' '}\n            <A href=\"https://github.com/reactjs/ko.react.dev/issues/new\">\n              버그를 신고\n            </A>\n            해주실 수 있으실까요?\n          </P>\n        </Intro>\n      </MaxWidth>\n    </Page>\n  );\n}\n"
  },
  {
    "path": "src/pages/[[...markdownPath]].js",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport {Fragment, useMemo} from 'react';\nimport {useRouter} from 'next/router';\nimport {Page} from 'components/Layout/Page';\nimport sidebarHome from '../sidebarHome.json';\nimport sidebarLearn from '../sidebarLearn.json';\nimport sidebarReference from '../sidebarReference.json';\nimport sidebarCommunity from '../sidebarCommunity.json';\nimport sidebarBlog from '../sidebarBlog.json';\nimport {MDXComponents} from 'components/MDX/MDXComponents';\nimport compileMDX from 'utils/compileMDX';\nimport {generateRssFeed} from '../utils/rss';\n\nexport default function Layout({content, toc, meta, languages}) {\n  const parsedContent = useMemo(\n    () => JSON.parse(content, reviveNodeOnClient),\n    [content]\n  );\n  const parsedToc = useMemo(() => JSON.parse(toc, reviveNodeOnClient), [toc]);\n  const section = useActiveSection();\n  let routeTree;\n  switch (section) {\n    case 'home':\n    case 'unknown':\n      routeTree = sidebarHome;\n      break;\n    case 'learn':\n      routeTree = sidebarLearn;\n      break;\n    case 'reference':\n      routeTree = sidebarReference;\n      break;\n    case 'community':\n      routeTree = sidebarCommunity;\n      break;\n    case 'blog':\n      routeTree = sidebarBlog;\n      break;\n  }\n  return (\n    <Page\n      toc={parsedToc}\n      routeTree={routeTree}\n      meta={meta}\n      section={section}\n      languages={languages}>\n      {parsedContent}\n    </Page>\n  );\n}\n\nfunction useActiveSection() {\n  const {asPath} = useRouter();\n  const cleanedPath = asPath.split(/[\\?\\#]/)[0];\n  if (cleanedPath === '/') {\n    return 'home';\n  } else if (cleanedPath.startsWith('/reference')) {\n    return 'reference';\n  } else if (asPath.startsWith('/learn')) {\n    return 'learn';\n  } else if (asPath.startsWith('/community')) {\n    return 'community';\n  } else if (asPath.startsWith('/blog')) {\n    return 'blog';\n  } else {\n    return 'unknown';\n  }\n}\n\n// Deserialize a client React tree from JSON.\nfunction reviveNodeOnClient(parentPropertyName, val) {\n  if (Array.isArray(val) && val[0] == '$r') {\n    // Assume it's a React element.\n    let Type = val[1];\n    let key = val[2];\n    if (key == null) {\n      key = parentPropertyName; // Index within a parent.\n    }\n    let props = val[3];\n    if (Type === 'wrapper') {\n      Type = Fragment;\n      props = {children: props.children};\n    }\n    if (Type in MDXComponents) {\n      Type = MDXComponents[Type];\n    }\n    if (!Type) {\n      console.error('Unknown type: ' + Type);\n      Type = Fragment;\n    }\n    return <Type key={key} {...props} />;\n  } else {\n    return val;\n  }\n}\n\n// Put MDX output into JSON for client.\nexport async function getStaticProps(context) {\n  generateRssFeed();\n  const fs = require('fs');\n  const rootDir = process.cwd() + '/src/content/';\n\n  // Read MDX from the file.\n  let path = (context.params.markdownPath || []).join('/') || 'index';\n  let mdx;\n  try {\n    mdx = fs.readFileSync(rootDir + path + '.md', 'utf8');\n  } catch {\n    mdx = fs.readFileSync(rootDir + path + '/index.md', 'utf8');\n  }\n\n  const {toc, content, meta, languages} = await compileMDX(mdx, path, {});\n  return {\n    props: {\n      toc,\n      content,\n      meta,\n      languages,\n    },\n  };\n}\n\n// Collect all MDX files for static generation.\nexport async function getStaticPaths() {\n  const {promisify} = require('util');\n  const {resolve} = require('path');\n  const fs = require('fs');\n  const readdir = promisify(fs.readdir);\n  const stat = promisify(fs.stat);\n  const rootDir = process.cwd() + '/src/content';\n\n  // Pages that should only be available in development.\n  const devOnlyPages = new Set(['learn/rsc-sandbox-test']);\n\n  // Find all MD files recursively.\n  async function getFiles(dir) {\n    const subdirs = await readdir(dir);\n    const files = await Promise.all(\n      subdirs.map(async (subdir) => {\n        const res = resolve(dir, subdir);\n        return (await stat(res)).isDirectory()\n          ? getFiles(res)\n          : res.slice(rootDir.length + 1);\n      })\n    );\n    return (\n      files\n        .flat()\n        // ignores `errors/*.md`, they will be handled by `pages/errors/[errorCode].tsx`\n        .filter((file) => file.endsWith('.md') && !file.startsWith('errors/'))\n    );\n  }\n\n  // 'foo/bar/baz.md' -> ['foo', 'bar', 'baz']\n  // 'foo/bar/qux/index.md' -> ['foo', 'bar', 'qux']\n  function getSegments(file) {\n    let segments = file.slice(0, -3).replace(/\\\\/g, '/').split('/');\n    if (segments[segments.length - 1] === 'index') {\n      segments.pop();\n    }\n    return segments;\n  }\n\n  const files = await getFiles(rootDir);\n\n  const paths = files\n    .map((file) => ({\n      params: {\n        markdownPath: getSegments(file),\n        // ^^^ CAREFUL HERE.\n        // If you rename markdownPath, update patches/next-remote-watch.patch too.\n        // Otherwise you'll break Fast Refresh for all MD files.\n      },\n    }))\n    .filter((entry) => {\n      if (process.env.NODE_ENV !== 'production') return true;\n      const pagePath = entry.params.markdownPath.join('/');\n      return !devOnlyPages.has(pagePath);\n    });\n\n  return {\n    paths: paths,\n    fallback: false,\n  };\n}\n"
  },
  {
    "path": "src/pages/_app.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport {useEffect} from 'react';\nimport {AppProps} from 'next/app';\nimport {useRouter} from 'next/router';\n\nimport '@docsearch/css';\nimport '../styles/algolia.css';\nimport '../styles/index.css';\nimport '../styles/sandpack.css';\n\nif (typeof window !== 'undefined') {\n  const terminationEvent = 'onpagehide' in window ? 'pagehide' : 'unload';\n  window.addEventListener(terminationEvent, function () {\n    // @ts-ignore\n    gtag('event', 'timing', {\n      event_label: 'JS Dependencies',\n      event: 'unload',\n    });\n  });\n}\n\nexport default function MyApp({Component, pageProps}: AppProps) {\n  const router = useRouter();\n\n  useEffect(() => {\n    // Taken from StackOverflow. Trying to detect both Safari desktop and mobile.\n    const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);\n    if (isSafari) {\n      // This is kind of a lie.\n      // We still rely on the manual Next.js scrollRestoration logic.\n      // However, we *also* don't want Safari grey screen during the back swipe gesture.\n      // Seems like it doesn't hurt to enable auto restore *and* Next.js logic at the same time.\n      history.scrollRestoration = 'auto';\n    } else {\n      // For other browsers, let Next.js set scrollRestoration to 'manual'.\n      // It seems to work better for Chrome and Firefox which don't animate the back swipe.\n    }\n  }, []);\n\n  useEffect(() => {\n    const handleRouteChange = (url: string) => {\n      const cleanedUrl = url.split(/[\\?\\#]/)[0];\n      // @ts-ignore\n      gtag('event', 'pageview', {\n        event_label: cleanedUrl,\n      });\n    };\n    router.events.on('routeChangeComplete', handleRouteChange);\n    return () => {\n      router.events.off('routeChangeComplete', handleRouteChange);\n    };\n  }, [router.events]);\n\n  return <Component {...pageProps} />;\n}\n"
  },
  {
    "path": "src/pages/_document.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport {Html, Head, Main, NextScript} from 'next/document';\nimport {siteConfig} from '../siteConfig';\n\nconst MyDocument = () => {\n  return (\n    <Html lang={siteConfig.languageCode} dir={siteConfig.isRTL ? 'rtl' : 'ltr'}>\n      <Head />\n      <link\n        rel=\"apple-touch-icon\"\n        sizes=\"180x180\"\n        href=\"/apple-touch-icon.png\"\n      />\n      <link\n        rel=\"icon\"\n        type=\"image/png\"\n        sizes=\"32x32\"\n        href=\"/favicon-32x32.png\"\n      />\n      <link\n        rel=\"icon\"\n        type=\"image/png\"\n        sizes=\"16x16\"\n        href=\"/favicon-16x16.png\"\n      />\n      <link rel=\"manifest\" href=\"/site.webmanifest\" />\n      <link rel=\"mask-icon\" href=\"/safari-pinned-tab.svg\" color=\"#404756\" />\n      <meta name=\"msapplication-TileColor\" content=\"#2b5797\" />\n      <meta name=\"theme-color\" content=\"#23272f\" />\n      <script\n        async\n        src={`https://www.googletagmanager.com/gtag/js?id=${process.env.NEXT_PUBLIC_GA_TRACKING_ID}`}\n      />\n      <script\n        dangerouslySetInnerHTML={{\n          __html: `window.dataLayer = window.dataLayer || [];function gtag(){dataLayer.push(arguments);}gtag('js', new Date());gtag('config', '${process.env.NEXT_PUBLIC_GA_TRACKING_ID}');`,\n        }}\n      />\n      <body className=\"font-text font-medium antialiased text-lg bg-wash dark:bg-wash-dark text-secondary dark:text-secondary-dark leading-base\">\n        <script\n          dangerouslySetInnerHTML={{\n            __html: `\n              (function () {\n                try {\n                  let logShown = false;\n                  function setUwu(isUwu) {\n                    try {\n                      if (isUwu) {\n                        localStorage.setItem('uwu', true);\n                        document.documentElement.classList.add('uwu');\n                        if (!logShown) {\n                          console.log('uwu mode! turn off with ?uwu=0');\n                          console.log('logo credit to @sawaratsuki1004 via https://github.com/SAWARATSUKI/KawaiiLogos');\n                          logShown = true;\n                        }\n                      } else {\n                        localStorage.removeItem('uwu');\n                        document.documentElement.classList.remove('uwu');\n                        console.log('uwu mode off. turn on with ?uwu');\n                      }\n                    } catch (err) { }\n                  }\n                  window.__setUwu = setUwu;\n                  function checkQueryParam() {\n                    const params = new URLSearchParams(window.location.search);\n                    const value = params.get('uwu');\n                    switch(value) {\n                      case '':\n                      case 'true':\n                      case '1':\n                        return true;\n                      case 'false':\n                      case '0':\n                        return false;\n                      default:\n                        return null;\n                    }\n                  }\n                  function checkLocalStorage() {\n                    try {\n                      return localStorage.getItem('uwu') === 'true';\n                    } catch (err) {\n                      return false;\n                    }\n                  }\n                  const uwuQueryParam = checkQueryParam();\n                  if (uwuQueryParam != null) {\n                    setUwu(uwuQueryParam);\n                  } else if (checkLocalStorage()) {\n                    document.documentElement.classList.add('uwu');\n                  }\n                } catch (err) { }\n              })();\n            `,\n          }}\n        />\n        <script\n          dangerouslySetInnerHTML={{\n            __html: `\n                (function () {\n                  function setTheme(newTheme) {\n                    window.__theme = newTheme;\n                    if (newTheme === 'dark') {\n                      document.documentElement.classList.add('dark');\n                    } else if (newTheme === 'light') {\n                      document.documentElement.classList.remove('dark');\n                    }\n                  }\n\n                  var preferredTheme;\n                  try {\n                    preferredTheme = localStorage.getItem('theme');\n                  } catch (err) { }\n\n                  window.__setPreferredTheme = function(newTheme) {\n                    preferredTheme = newTheme;\n                    setTheme(newTheme);\n                    try {\n                      localStorage.setItem('theme', newTheme);\n                    } catch (err) { }\n                  };\n\n                  var initialTheme = preferredTheme;\n                  var darkQuery = window.matchMedia('(prefers-color-scheme: dark)');\n\n                  if (!initialTheme) {\n                    initialTheme = darkQuery.matches ? 'dark' : 'light';\n                  }\n                  setTheme(initialTheme);\n\n                  darkQuery.addEventListener('change', function (e) {\n                    if (!preferredTheme) {\n                      setTheme(e.matches ? 'dark' : 'light');\n                    }\n                  });\n\n                  // Detect whether the browser is Mac to display platform specific content\n                  // An example of such content can be the keyboard shortcut displayed in the search bar\n                  document.documentElement.classList.add(\n                      window.navigator.platform.includes('Mac')\n                      ? \"platform-mac\"\n                      : \"platform-win\"\n                  );\n                })();\n              `,\n          }}\n        />\n        <Main />\n        <NextScript />\n      </body>\n    </Html>\n  );\n};\n\nexport default MyDocument;\n"
  },
  {
    "path": "src/pages/api/md/[...path].ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nimport type {NextApiRequest, NextApiResponse} from 'next';\nimport fs from 'fs';\nimport path from 'path';\n\nconst FOOTER = `\n---\n\n## Sitemap\n\n[Overview of all docs pages](/llms.txt)\n`;\n\nexport default function handler(req: NextApiRequest, res: NextApiResponse) {\n  const pathSegments = req.query.path;\n  if (!pathSegments) {\n    return res.status(404).send('Not found');\n  }\n\n  const filePath = Array.isArray(pathSegments)\n    ? pathSegments.join('/')\n    : pathSegments;\n\n  // Block /index.md URLs - use /foo.md instead of /foo/index.md\n  if (filePath.endsWith('/index') || filePath === 'index') {\n    return res.status(404).send('Not found');\n  }\n\n  // Try exact path first, then with /index\n  const candidates = [\n    path.join(process.cwd(), 'src/content', filePath + '.md'),\n    path.join(process.cwd(), 'src/content', filePath, 'index.md'),\n  ];\n\n  for (const fullPath of candidates) {\n    try {\n      const content = fs.readFileSync(fullPath, 'utf8');\n      res.setHeader('Content-Type', 'text/plain; charset=utf-8');\n      res.setHeader('Cache-Control', 'public, max-age=3600');\n      return res.status(200).send(content + FOOTER);\n    } catch {\n      // Try next candidate\n    }\n  }\n\n  res.status(404).send('Not found');\n}\n"
  },
  {
    "path": "src/pages/errors/[errorCode].tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nimport {Fragment, useMemo} from 'react';\nimport {Page} from 'components/Layout/Page';\nimport {MDXComponents} from 'components/MDX/MDXComponents';\nimport sidebarLearn from 'sidebarLearn.json';\nimport type {RouteItem} from 'components/Layout/getRouteMeta';\nimport {GetStaticPaths, GetStaticProps, InferGetStaticPropsType} from 'next';\nimport {ErrorDecoderContext} from 'components/ErrorDecoderContext';\nimport compileMDX from 'utils/compileMDX';\n\ninterface ErrorDecoderProps {\n  errorCode: string | null;\n  errorMessage: string | null;\n  content: string;\n  toc: string;\n  meta: any;\n}\n\nexport default function ErrorDecoderPage({\n  errorMessage,\n  errorCode,\n  content,\n}: InferGetStaticPropsType<typeof getStaticProps>) {\n  const parsedContent = useMemo<React.ReactNode>(\n    () => JSON.parse(content, reviveNodeOnClient),\n    [content]\n  );\n\n  return (\n    <ErrorDecoderContext value={{errorMessage, errorCode}}>\n      <Page\n        toc={[]}\n        meta={{\n          title: errorCode\n            ? `Minified React error #${errorCode}`\n            : 'Minified Error Decoder',\n        }}\n        routeTree={sidebarLearn as RouteItem}\n        section=\"unknown\">\n        <div>{parsedContent}</div>\n        {/* <MaxWidth>\n          <P>\n            We highly recommend using the development build locally when debugging\n            your app since it tracks additional debug info and provides helpful\n            warnings about potential problems in your apps, but if you encounter\n            an exception while using the production build, this page will\n            reassemble the original error message.\n          </P>\n          <ErrorDecoder />\n        </MaxWidth> */}\n      </Page>\n    </ErrorDecoderContext>\n  );\n}\n\n// Deserialize a client React tree from JSON.\nfunction reviveNodeOnClient(parentPropertyName: unknown, val: any) {\n  if (Array.isArray(val) && val[0] == '$r') {\n    // Assume it's a React element.\n    let Type = val[1];\n    let key = val[2];\n    if (key == null) {\n      key = parentPropertyName; // Index within a parent.\n    }\n    let props = val[3];\n    if (Type === 'wrapper') {\n      Type = Fragment;\n      props = {children: props.children};\n    }\n    if (Type in MDXComponents) {\n      Type = MDXComponents[Type as keyof typeof MDXComponents];\n    }\n    if (!Type) {\n      console.error('Unknown type: ' + Type);\n      Type = Fragment;\n    }\n    return <Type key={key} {...props} />;\n  } else {\n    return val;\n  }\n}\n\n/**\n * Next.js Page Router doesn't have a way to cache specific data fetching request.\n * But since Next.js uses limited number of workers, keep \"cachedErrorCodes\" as a\n * module level memory cache can reduce the number of requests down to once per worker.\n *\n * TODO: use `next/unstable_cache` when migrating to Next.js App Router\n */\nlet cachedErrorCodes: Record<string, string> | null = null;\n\nexport const getStaticProps: GetStaticProps<ErrorDecoderProps> = async ({\n  params,\n}) => {\n  const errorCodes: {[key: string]: string} = (cachedErrorCodes ||= await (\n    await fetch(\n      'https://raw.githubusercontent.com/facebook/react/main/scripts/error-codes/codes.json'\n    )\n  ).json());\n\n  const code = typeof params?.errorCode === 'string' ? params?.errorCode : null;\n  if (code && !errorCodes[code]) {\n    return {\n      notFound: true,\n    };\n  }\n\n  const fs = require('fs');\n  const rootDir = process.cwd() + '/src/content/errors';\n\n  // Read MDX from the file.\n  let path = params?.errorCode || 'index';\n  let mdx;\n  try {\n    mdx = fs.readFileSync(rootDir + '/' + path + '.md', 'utf8');\n  } catch {\n    // if [errorCode].md is not found, fallback to generic.md\n    mdx = fs.readFileSync(rootDir + '/generic.md', 'utf8');\n  }\n\n  const {content, toc, meta} = await compileMDX(mdx, path, {code, errorCodes});\n\n  return {\n    props: {\n      content,\n      toc,\n      meta,\n      errorCode: code,\n      errorMessage: code ? errorCodes[code] : null,\n    },\n  };\n};\n\nexport const getStaticPaths: GetStaticPaths = async () => {\n  /**\n   * Fetch error codes from GitHub\n   */\n  const errorCodes = (cachedErrorCodes ||= await (\n    await fetch(\n      'https://raw.githubusercontent.com/facebook/react/main/scripts/error-codes/codes.json'\n    )\n  ).json());\n\n  const paths = Object.keys(errorCodes).map((code) => ({\n    params: {\n      errorCode: code,\n    },\n  }));\n\n  return {\n    paths,\n    fallback: 'blocking',\n  };\n};\n"
  },
  {
    "path": "src/pages/errors/index.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nimport ErrorDecoderPage from './[errorCode]';\nexport default ErrorDecoderPage;\nexport {getStaticProps} from './[errorCode]';\n"
  },
  {
    "path": "src/pages/llms.txt.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nimport type {GetServerSideProps} from 'next';\nimport {siteConfig} from '../siteConfig';\nimport sidebarLearn from '../sidebarLearn.json';\nimport sidebarReference from '../sidebarReference.json';\n\ninterface RouteItem {\n  title?: string;\n  path?: string;\n  routes?: RouteItem[];\n  hasSectionHeader?: boolean;\n  sectionHeader?: string;\n}\n\ninterface Sidebar {\n  title: string;\n  routes: RouteItem[];\n}\n\ninterface Page {\n  title: string;\n  url: string;\n}\n\ninterface SubGroup {\n  heading: string;\n  pages: Page[];\n}\n\ninterface Section {\n  heading: string | null;\n  pages: Page[];\n  subGroups: SubGroup[];\n}\n\n// Clean up section header names (remove version placeholders)\nfunction cleanSectionHeader(header: string): string {\n  return header\n    .replace(/@\\{\\{version\\}\\}/g, '')\n    .replace(/-/g, ' ')\n    .split(' ')\n    .map((word) => word.charAt(0).toUpperCase() + word.slice(1))\n    .join(' ')\n    .trim();\n}\n\n// Extract routes for sidebars that use hasSectionHeader to define major sections\n// (like the API Reference sidebar)\nfunction extractSectionedRoutes(\n  routes: RouteItem[],\n  baseUrl: string\n): Section[] {\n  const sections: Section[] = [];\n  let currentSection: Section | null = null;\n\n  for (const route of routes) {\n    // Skip external links\n    if (route.path?.startsWith('http')) {\n      continue;\n    }\n\n    // Start a new section when we hit a section header\n    if (route.hasSectionHeader && route.sectionHeader) {\n      if (currentSection) {\n        sections.push(currentSection);\n      }\n      currentSection = {\n        heading: cleanSectionHeader(route.sectionHeader),\n        pages: [],\n        subGroups: [],\n      };\n      continue;\n    }\n\n    // If no section started yet, skip\n    if (!currentSection) {\n      continue;\n    }\n\n    // Route with children - create a sub-group\n    if (route.title && route.routes && route.routes.length > 0) {\n      const subGroup: SubGroup = {\n        heading: route.title,\n        pages: [],\n      };\n\n      // Include parent page if it has a path\n      if (route.path) {\n        subGroup.pages.push({\n          title: route.title,\n          url: `${baseUrl}${route.path}.md`,\n        });\n      }\n\n      // Add child pages\n      for (const child of route.routes) {\n        if (child.title && child.path && !child.path.startsWith('http')) {\n          subGroup.pages.push({\n            title: child.title,\n            url: `${baseUrl}${child.path}.md`,\n          });\n        }\n      }\n\n      if (subGroup.pages.length > 0) {\n        currentSection.subGroups.push(subGroup);\n      }\n    }\n    // Single page without children\n    else if (route.title && route.path) {\n      currentSection.pages.push({\n        title: route.title,\n        url: `${baseUrl}${route.path}.md`,\n      });\n    }\n  }\n\n  // Don't forget the last section\n  if (currentSection) {\n    sections.push(currentSection);\n  }\n\n  return sections;\n}\n\n// Extract routes for sidebars that use routes with children as the primary grouping\n// (like the Learn sidebar)\nfunction extractGroupedRoutes(\n  routes: RouteItem[],\n  baseUrl: string\n): SubGroup[] {\n  const groups: SubGroup[] = [];\n\n  for (const route of routes) {\n    // Skip section headers\n    if (route.hasSectionHeader) {\n      continue;\n    }\n\n    // Skip external links\n    if (route.path?.startsWith('http')) {\n      continue;\n    }\n\n    // Route with children - create a group\n    if (route.title && route.routes && route.routes.length > 0) {\n      const pages: Page[] = [];\n\n      // Include parent page if it has a path\n      if (route.path) {\n        pages.push({\n          title: route.title,\n          url: `${baseUrl}${route.path}.md`,\n        });\n      }\n\n      // Add child pages\n      for (const child of route.routes) {\n        if (child.title && child.path && !child.path.startsWith('http')) {\n          pages.push({\n            title: child.title,\n            url: `${baseUrl}${child.path}.md`,\n          });\n        }\n      }\n\n      if (pages.length > 0) {\n        groups.push({\n          heading: route.title,\n          pages,\n        });\n      }\n    }\n    // Single page without children - group under its own heading\n    else if (route.title && route.path) {\n      groups.push({\n        heading: route.title,\n        pages: [\n          {\n            title: route.title,\n            url: `${baseUrl}${route.path}.md`,\n          },\n        ],\n      });\n    }\n  }\n\n  return groups;\n}\n\n// Check if sidebar uses section headers as primary grouping\nfunction usesSectionHeaders(routes: RouteItem[]): boolean {\n  return routes.some((r) => r.hasSectionHeader && r.sectionHeader);\n}\n\nexport const getServerSideProps: GetServerSideProps = async ({res}) => {\n  const subdomain =\n    siteConfig.languageCode === 'en' ? '' : siteConfig.languageCode + '.';\n  const baseUrl = 'https://' + subdomain + 'react.dev';\n\n  const lines = [\n    '# React Documentation',\n    '',\n    '> The library for web and native user interfaces.',\n  ];\n\n  const sidebars: Sidebar[] = [\n    sidebarLearn as Sidebar,\n    sidebarReference as Sidebar,\n  ];\n\n  for (const sidebar of sidebars) {\n    lines.push('');\n    lines.push(`## ${sidebar.title}`);\n\n    if (usesSectionHeaders(sidebar.routes)) {\n      // API Reference style: section headers define major groups\n      const sections = extractSectionedRoutes(sidebar.routes, baseUrl);\n      for (const section of sections) {\n        if (section.heading) {\n          lines.push('');\n          lines.push(`### ${section.heading}`);\n        }\n\n        // Output pages directly under section\n        for (const page of section.pages) {\n          lines.push(`- [${page.title}](${page.url})`);\n        }\n\n        // Output sub-groups with #### headings\n        for (const subGroup of section.subGroups) {\n          lines.push('');\n          lines.push(`#### ${subGroup.heading}`);\n          for (const page of subGroup.pages) {\n            lines.push(`- [${page.title}](${page.url})`);\n          }\n        }\n      }\n    } else {\n      // Learn style: routes with children define groups\n      const groups = extractGroupedRoutes(sidebar.routes, baseUrl);\n      for (const group of groups) {\n        lines.push('');\n        lines.push(`### ${group.heading}`);\n        for (const page of group.pages) {\n          lines.push(`- [${page.title}](${page.url})`);\n        }\n      }\n    }\n  }\n\n  const content = lines.join('\\n');\n\n  res.setHeader('Content-Type', 'text/plain; charset=utf-8');\n  res.write(content);\n  res.end();\n\n  return {props: {}};\n};\n\nexport default function LlmsTxt() {\n  return null;\n}\n"
  },
  {
    "path": "src/sidebarBlog.json",
    "content": "{\n  \"title\": \"블로그\",\n  \"path\": \"/blog\",\n  \"routes\": [\n    {\n      \"hasSectionHeader\": true,\n      \"sectionHeader\": \"최신 소식\"\n    },\n    {\n      \"title\": \"블로그\",\n      \"path\": \"/blog\",\n      \"skipBreadcrumb\": true,\n      \"routes\": [\n        {\n          \"title\": \"The React Foundation: A New Home for React Hosted by the Linux Foundation\",\n          \"titleForHomepage\": \"The React Foundation: A New Home for React Hosted by the Linux Foundation\",\n          \"icon\": \"blog\",\n          \"date\": \"February 24, 2026\",\n          \"path\": \"/blog/2026/02/24/the-react-foundation\"\n        },\n        {\n          \"title\": \"Denial of Service and Source Code Exposure in React Server Components\",\n          \"titleForHomepage\": \"Additional Vulnerabilities in RSC\",\n          \"icon\": \"blog\",\n          \"date\": \"December 11, 2025\",\n          \"path\": \"/blog/2025/12/11/denial-of-service-and-source-code-exposure-in-react-server-components\"\n        },\n        {\n          \"title\": \"Critical Security Vulnerability in React Server Components\",\n          \"titleForHomepage\": \"Vulnerability in React Server Components\",\n          \"icon\": \"blog\",\n          \"date\": \"December 3, 2025\",\n          \"path\": \"/blog/2025/12/03/critical-security-vulnerability-in-react-server-components\"\n        },\n        {\n          \"title\": \"React Conf 2025 요약\",\n          \"titleForHomepage\": \"React Conf 2025 요약\",\n          \"icon\": \"blog\",\n          \"date\": \"2025.10.16\",\n          \"path\": \"/blog/2025/10/16/react-conf-2025-recap\"\n        },\n        {\n          \"title\": \"React 컴파일러 v1.0\",\n          \"titleForHomepage\": \"React 컴파일러 v1.0\",\n          \"icon\": \"blog\",\n          \"date\": \"2025.10.08\",\n          \"path\": \"/blog/2025/10/07/react-compiler-1\"\n        },\n        {\n          \"title\": \"React Foundation 소개\",\n          \"titleForHomepage\": \"React Foundation 소개\",\n          \"icon\": \"blog\",\n          \"date\": \"2025.10.07\",\n          \"path\": \"/blog/2025/10/07/introducing-the-react-foundation\"\n        },\n        {\n          \"title\": \"React 19.2\",\n          \"titleForHomepage\": \"React 19.2\",\n          \"icon\": \"blog\",\n          \"date\": \"2025.10.01\",\n          \"path\": \"/blog/2025/10/01/react-19-2\"\n        },\n        {\n          \"title\": \"React Labs: View Transitions, Activity 등\",\n          \"titleForHomepage\": \"View Transitions와 Activity\",\n          \"icon\": \"labs\",\n          \"date\": \"2025.04.23\",\n          \"path\": \"/blog/2025/04/23/react-labs-view-transitions-activity-and-more\"\n        },\n        {\n          \"title\": \"React 컴파일러 RC\",\n          \"titleForHomepage\": \"React 컴파일러 RC\",\n          \"icon\": \"blog\",\n          \"date\": \"2025.04.21\",\n          \"path\": \"/blog/2025/04/21/react-compiler-rc\"\n        },\n        {\n          \"title\": \"Create React App 지원 종료\",\n          \"titleForHomepage\": \"Create React App 지원 종료\",\n          \"icon\": \"blog\",\n          \"date\": \"2025.02.14\",\n          \"path\": \"/blog/2025/02/14/sunsetting-create-react-app\"\n        },\n        {\n          \"title\": \"React 19\",\n          \"titleForHomepage\": \"React 19\",\n          \"icon\": \"blog\",\n          \"date\": \"2024.12.05\",\n          \"path\": \"/blog/2024/12/05/react-19\"\n        },\n        {\n          \"title\": \"React 컴파일러 베타 릴리스 및 로드맵\",\n          \"titleForHomepage\": \"React 컴파일러 베타 릴리스 및 로드맵\",\n          \"icon\": \"blog\",\n          \"date\": \"2024.10.21\",\n          \"path\": \"/blog/2024/10/21/react-compiler-beta-release\"\n        },\n        {\n          \"title\": \"React Conf 2024 요약\",\n          \"titleForHomepage\": \"React Conf 2024 요약\",\n          \"icon\": \"blog\",\n          \"date\": \"2024.05.22\",\n          \"path\": \"/blog/2024/05/22/react-conf-2024-recap\"\n        },\n        {\n          \"title\": \"React 19 RC\",\n          \"titleForHomepage\": \"React 19 RC\",\n          \"icon\": \"blog\",\n          \"date\": \"2024.04.25\",\n          \"path\": \"/blog/2024/04/25/react-19\"\n        },\n        {\n          \"title\": \"React 19 RC 업그레이드 가이드\",\n          \"titleForHomepage\": \"React 19 RC 업그레이드 가이드\",\n          \"icon\": \"blog\",\n          \"date\": \"2024.04.25\",\n          \"path\": \"/blog/2024/04/25/react-19-upgrade-guide\"\n        },\n        {\n          \"title\": \"React Labs: 그동안의 작업 - 2024년 2월\",\n          \"titleForHomepage\": \"React Labs: 2024년 2월\",\n          \"icon\": \"labs\",\n          \"date\": \"2024.02.15\",\n          \"path\": \"/blog/2024/02/15/react-labs-what-we-have-been-working-on-february-2024\"\n        },\n        {\n          \"title\": \"React Canaries: Meta 외부에서의 점진적 기능 출시\",\n          \"titleForHomepage\": \"React Canaries: 점진적 기능 출시\",\n          \"icon\": \"blog\",\n          \"date\": \"2023.05.03\",\n          \"path\": \"/blog/2023/05/03/react-canaries\"\n        },\n        {\n          \"title\": \"React Labs: 그동안의 작업 - 2023년 3월\",\n          \"titleForHomepage\": \"React Labs: 2023년 3월\",\n          \"icon\": \"labs\",\n          \"date\": \"2023.03.22\",\n          \"path\": \"/blog/2023/03/22/react-labs-what-we-have-been-working-on-march-2023\"\n        },\n        {\n          \"title\": \"react.dev 소개\",\n          \"titleForHomepage\": \"react.dev 소개\",\n          \"icon\": \"blog\",\n          \"date\": \"2023.03.16\",\n          \"path\": \"/blog/2023/03/16/introducing-react-dev\"\n        },\n        {\n          \"title\": \"React Labs: 그동안의 작업 - 2022년 6월\",\n          \"titleForHomepage\": \"React Labs: 2022년 6월\",\n          \"icon\": \"labs\",\n          \"date\": \"2022.06.15\",\n          \"path\": \"/blog/2022/06/15/react-labs-what-we-have-been-working-on-june-2022\"\n        },\n        {\n          \"title\": \"React v18.0\",\n          \"titleForHomepage\": \"React v18.0\",\n          \"icon\": \"blog\",\n          \"date\": \"2022.03.29\",\n          \"path\": \"/blog/2022/03/29/react-v18\"\n        },\n        {\n          \"title\": \"React 18로 업그레이드하는 방법\",\n          \"titleForHomepage\": \"React 18로 업그레이드하는 방법\",\n          \"icon\": \"blog\",\n          \"date\": \"2022.03.08\",\n          \"path\": \"/blog/2022/03/08/react-18-upgrade-guide\"\n        },\n        {\n          \"title\": \"React Conf 2021 요약\",\n          \"titleForHomepage\": \"React Conf 2021 요약\",\n          \"icon\": \"blog\",\n          \"date\": \"2021.12.17\",\n          \"path\": \"/blog/2021/12/17/react-conf-2021-recap\"\n        },\n        {\n          \"title\": \"React 18에 대한 계획\",\n          \"titleForHomepage\": \"React 18에 대한 계획\",\n          \"icon\": \"blog\",\n          \"date\": \"2021.06.08\",\n          \"path\": \"/blog/2021/06/08/the-plan-for-react-18\"\n        },\n        {\n          \"title\": \"제로 번들 사이즈 React 서버 컴포넌트를 소개합니다\",\n          \"titleForHomepage\": \"서버 컴포넌트 소개\",\n          \"icon\": \"labs\",\n          \"date\": \"2020.12.21\",\n          \"path\": \"/blog/2020/12/21/data-fetching-with-react-server-components\"\n        },\n        {\n          \"title\": \"이전 글\",\n          \"path\": \"https://reactjs.org/blog/all.html\"\n        }\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "src/sidebarCommunity.json",
    "content": "{\n  \"title\": \"커뮤니티\",\n  \"path\": \"/community\",\n  \"routes\": [\n    {\n      \"hasSectionHeader\": true,\n      \"sectionHeader\": \"참여하기\"\n    },\n    {\n      \"title\": \"커뮤니티\",\n      \"path\": \"/community\",\n      \"skipBreadcrumb\": true,\n      \"routes\": [\n        {\n          \"title\": \"React 컨퍼런스\",\n          \"path\": \"/community/conferences\"\n        },\n        {\n          \"title\": \"React 밋업\",\n          \"path\": \"/community/meetups\"\n        },\n        {\n          \"title\": \"React 영상\",\n          \"path\": \"/community/videos\"\n        },\n        {\n          \"title\": \"팀 소개\",\n          \"path\": \"/community/team\"\n        },\n        {\n          \"title\": \"문서 기여자\",\n          \"path\": \"/community/docs-contributors\"\n        },\n        {\n          \"title\": \"번역\",\n          \"path\": \"/community/translations\"\n        },\n        {\n          \"title\": \"감사의 말\",\n          \"path\": \"/community/acknowledgements\"\n        },\n        {\n          \"title\": \"버전 관리 정책\",\n          \"path\": \"/community/versioning-policy\"\n        }\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "src/sidebarHome.json",
    "content": "{\n  \"title\": \"React 문서\",\n  \"path\": \"/\",\n  \"routes\": [\n    {\n      \"hasSectionHeader\": true,\n      \"sectionHeader\": \"시작하기\"\n    },\n    {\n      \"title\": \"빠르게 시작하기\",\n      \"path\": \"/learn\"\n    },\n    {\n      \"title\": \"설치하기\",\n      \"path\": \"/learn/installation\"\n    },\n    {\n      \"title\": \"Setup\",\n      \"path\": \"/learn/setup\"\n    },\n    {\n      \"title\": \"React 컴파일러\",\n      \"path\": \"/learn/react-compiler\"\n    },\n    {\n      \"hasSectionHeader\": true,\n      \"sectionHeader\": \"React 학습하기\"\n    },\n    {\n      \"title\": \"UI 표현하기\",\n      \"path\": \"/learn/describing-the-ui\"\n    },\n    {\n      \"title\": \"상호작용 추가하기\",\n      \"path\": \"/learn/adding-interactivity\"\n    },\n    {\n      \"title\": \"State 관리하기\",\n      \"path\": \"/learn/managing-state\"\n    },\n    {\n      \"title\": \"탈출구\",\n      \"path\": \"/learn/escape-hatches\"\n    },\n    {\n      \"hasSectionHeader\": true,\n      \"sectionHeader\": \"REACT API\"\n    },\n    {\n      \"title\": \"Hook\",\n      \"path\": \"/reference/react\"\n    },\n    {\n      \"title\": \"컴포넌트\",\n      \"path\": \"/reference/react/components\"\n    },\n    {\n      \"title\": \"API\",\n      \"path\": \"/reference/react/apis\"\n    },\n    {\n      \"title\": \"레거시 API\",\n      \"path\": \"/reference/react/legacy\"\n    },\n    {\n      \"hasSectionHeader\": true,\n      \"sectionHeader\": \"REACT DOM API\"\n    },\n    {\n      \"title\": \"컴포넌트\",\n      \"path\": \"/reference/react-dom/components\"\n    },\n    {\n      \"title\": \"API\",\n      \"path\": \"/reference/react-dom\"\n    },\n    {\n      \"title\": \"클라이언트 API\",\n      \"path\": \"/reference/react-dom/client\"\n    },\n    {\n      \"title\": \"서버 API\",\n      \"path\": \"/reference/react-dom/server\"\n    },\n    {\n      \"hasSectionHeader\": true,\n      \"sectionHeader\": \"React 컴파일러 API\"\n    },\n    {\n      \"title\": \"설정\",\n      \"path\": \"/reference/react-compiler/configuration\"\n    },\n    {\n      \"title\": \"지시어\",\n      \"path\": \"/reference/react-compiler/directives\"\n    },\n    {\n      \"title\": \"라이브러리 컴파일\",\n      \"path\": \"/reference/react-compiler/compiling-libraries\"\n    },\n    {\n      \"hasSectionHeader\": true,\n      \"sectionHeader\": \"참여하기\"\n    },\n    {\n      \"title\": \"React 커뮤니티\",\n      \"path\": \"/community\"\n    },\n    {\n      \"hasSectionHeader\": true,\n      \"sectionHeader\": \"최신 소식\"\n    },\n    {\n      \"title\": \"React 블로그\",\n      \"path\": \"/blog\"\n    }\n  ]\n}\n"
  },
  {
    "path": "src/sidebarLearn.json",
    "content": "{\n  \"title\": \"React 학습하기\",\n  \"path\": \"/learn\",\n  \"routes\": [\n    {\n      \"hasSectionHeader\": true,\n      \"sectionHeader\": \"시작하기\"\n    },\n    {\n      \"title\": \"빠르게 시작하기\",\n      \"path\": \"/learn\",\n      \"routes\": [\n        {\n          \"title\": \"자습서: 틱택토 게임\",\n          \"path\": \"/learn/tutorial-tic-tac-toe\"\n        },\n        {\n          \"title\": \"React로 사고하기\",\n          \"path\": \"/learn/thinking-in-react\"\n        }\n      ]\n    },\n    {\n      \"title\": \"설치하기\",\n      \"path\": \"/learn/installation\",\n      \"routes\": [\n        {\n          \"title\": \"새로운 React 앱 만들기\",\n          \"path\": \"/learn/creating-a-react-app\"\n        },\n        {\n          \"title\": \"처음부터 React 앱 만들기\",\n          \"path\": \"/learn/build-a-react-app-from-scratch\"\n        },\n        {\n          \"title\": \"기존 프로젝트에 React 추가하기\",\n          \"path\": \"/learn/add-react-to-an-existing-project\"\n        }\n      ]\n    },\n    {\n      \"title\": \"설정하기\",\n      \"path\": \"/learn/setup\",\n      \"routes\": [\n        {\n          \"title\": \"에디터 설정하기\",\n          \"path\": \"/learn/editor-setup\"\n        },\n        {\n          \"title\": \"TypeScript 사용하기\",\n          \"path\": \"/learn/typescript\"\n        },\n        {\n          \"title\": \"React 개발자 도구\",\n          \"path\": \"/learn/react-developer-tools\"\n        }\n      ]\n    },\n    {\n      \"title\": \"React 컴파일러\",\n      \"path\": \"/learn/react-compiler\",\n      \"canary\": true,\n      \"routes\": [\n        {\n          \"title\": \"소개\",\n          \"path\": \"/learn/react-compiler/introduction\"\n        },\n        {\n          \"title\": \"설치\",\n          \"path\": \"/learn/react-compiler/installation\"\n        },\n        {\n          \"title\": \"점진적 도입\",\n          \"path\": \"/learn/react-compiler/incremental-adoption\"\n        },\n        {\n          \"title\": \"디버깅 및 문제 해결\",\n          \"path\": \"/learn/react-compiler/debugging\"\n        }\n      ]\n    },\n    {\n      \"hasSectionHeader\": true,\n      \"sectionHeader\": \"React 학습하기\"\n    },\n    {\n      \"title\": \"UI 표현하기\",\n      \"tags\": [],\n      \"path\": \"/learn/describing-the-ui\",\n      \"routes\": [\n        {\n          \"title\": \"첫 번째 컴포넌트\",\n          \"path\": \"/learn/your-first-component\"\n        },\n        {\n          \"title\": \"컴포넌트 Import 및 Export하기\",\n          \"path\": \"/learn/importing-and-exporting-components\"\n        },\n        {\n          \"title\": \"JSX로 마크업 작성하기\",\n          \"path\": \"/learn/writing-markup-with-jsx\"\n        },\n        {\n          \"title\": \"중괄호가 있는 JSX에서 자바스크립트 사용하기\",\n          \"path\": \"/learn/javascript-in-jsx-with-curly-braces\"\n        },\n        {\n          \"title\": \"컴포넌트에 Props 전달하기\",\n          \"path\": \"/learn/passing-props-to-a-component\"\n        },\n        {\n          \"title\": \"조건부 렌더링\",\n          \"path\": \"/learn/conditional-rendering\"\n        },\n        {\n          \"title\": \"리스트 렌더링\",\n          \"path\": \"/learn/rendering-lists\"\n        },\n        {\n          \"title\": \"컴포넌트를 순수하게 유지하기\",\n          \"path\": \"/learn/keeping-components-pure\"\n        },\n        {\n          \"title\": \"트리로서의 UI\",\n          \"path\": \"/learn/understanding-your-ui-as-a-tree\"\n        }\n      ]\n    },\n    {\n      \"title\": \"상호작용성 더하기\",\n      \"path\": \"/learn/adding-interactivity\",\n      \"tags\": [],\n      \"routes\": [\n        {\n          \"title\": \"이벤트에 응답하기\",\n          \"path\": \"/learn/responding-to-events\"\n        },\n        {\n          \"title\": \"State: 컴포넌트의 기억 저장소\",\n          \"path\": \"/learn/state-a-components-memory\"\n        },\n        {\n          \"title\": \"렌더링 그리고 커밋\",\n          \"path\": \"/learn/render-and-commit\"\n        },\n        {\n          \"title\": \"스냅샷으로서의 State\",\n          \"path\": \"/learn/state-as-a-snapshot\"\n        },\n        {\n          \"title\": \"State 업데이트 큐\",\n          \"path\": \"/learn/queueing-a-series-of-state-updates\"\n        },\n        {\n          \"title\": \"객체 State 업데이트하기\",\n          \"path\": \"/learn/updating-objects-in-state\"\n        },\n        {\n          \"title\": \"배열 State 업데이트하기\",\n          \"path\": \"/learn/updating-arrays-in-state\"\n        }\n      ]\n    },\n    {\n      \"title\": \"State 관리하기\",\n      \"path\": \"/learn/managing-state\",\n      \"tags\": [\"intermediate\"],\n      \"routes\": [\n        {\n          \"title\": \"State를 사용해 Input 다루기\",\n          \"path\": \"/learn/reacting-to-input-with-state\"\n        },\n        {\n          \"title\": \"State 구조 선택하기\",\n          \"path\": \"/learn/choosing-the-state-structure\"\n        },\n        {\n          \"title\": \"컴포넌트 간 State 공유하기\",\n          \"path\": \"/learn/sharing-state-between-components\"\n        },\n        {\n          \"title\": \"State를 보존하고 초기화하기\",\n          \"path\": \"/learn/preserving-and-resetting-state\"\n        },\n        {\n          \"title\": \"State 로직을 Reducer로 작성하기\",\n          \"path\": \"/learn/extracting-state-logic-into-a-reducer\"\n        },\n        {\n          \"title\": \"Context를 사용해 데이터를 깊게 전달하기\",\n          \"path\": \"/learn/passing-data-deeply-with-context\"\n        },\n        {\n          \"title\": \"Reducer와 Context로 앱 확장하기\",\n          \"path\": \"/learn/scaling-up-with-reducer-and-context\"\n        }\n      ]\n    },\n    {\n      \"title\": \"탈출구\",\n      \"path\": \"/learn/escape-hatches\",\n      \"tags\": [\"advanced\"],\n      \"routes\": [\n        {\n          \"title\": \"Ref로 값 참조하기\",\n          \"path\": \"/learn/referencing-values-with-refs\"\n        },\n        {\n          \"title\": \"Ref로 DOM 조작하기\",\n          \"path\": \"/learn/manipulating-the-dom-with-refs\"\n        },\n        {\n          \"title\": \"Effect로 동기화하기\",\n          \"path\": \"/learn/synchronizing-with-effects\"\n        },\n        {\n          \"title\": \"Effect가 필요하지 않은 경우\",\n          \"path\": \"/learn/you-might-not-need-an-effect\"\n        },\n        {\n          \"title\": \"React Effect의 생명주기\",\n          \"path\": \"/learn/lifecycle-of-reactive-effects\"\n        },\n        {\n          \"title\": \"Effect에서 이벤트 분리하기\",\n          \"path\": \"/learn/separating-events-from-effects\"\n        },\n        {\n          \"title\": \"Effect의 의존성 제거하기\",\n          \"path\": \"/learn/removing-effect-dependencies\"\n        },\n        {\n          \"title\": \"커스텀 Hook으로 로직 재사용하기\",\n          \"path\": \"/learn/reusing-logic-with-custom-hooks\"\n        }\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "src/sidebarReference.json",
    "content": "{\n  \"title\": \"API 참고서\",\n  \"path\": \"/reference/react\",\n  \"routes\": [\n    {\n      \"hasSectionHeader\": true,\n      \"sectionHeader\": \"react@{{version}}\"\n    },\n    {\n      \"title\": \"개요\",\n      \"path\": \"/reference/react\"\n    },\n    {\n      \"title\": \"Hook\",\n      \"path\": \"/reference/react/hooks\",\n      \"routes\": [\n        {\n          \"title\": \"useActionState\",\n          \"path\": \"/reference/react/useActionState\"\n        },\n        {\n          \"title\": \"useCallback\",\n          \"path\": \"/reference/react/useCallback\"\n        },\n        {\n          \"title\": \"useContext\",\n          \"path\": \"/reference/react/useContext\"\n        },\n        {\n          \"title\": \"useDebugValue\",\n          \"path\": \"/reference/react/useDebugValue\"\n        },\n        {\n          \"title\": \"useDeferredValue\",\n          \"path\": \"/reference/react/useDeferredValue\"\n        },\n        {\n          \"title\": \"useEffect\",\n          \"path\": \"/reference/react/useEffect\"\n        },\n        {\n          \"title\": \"useEffectEvent\",\n          \"path\": \"/reference/react/useEffectEvent\"\n        },\n        {\n          \"title\": \"useId\",\n          \"path\": \"/reference/react/useId\"\n        },\n        {\n          \"title\": \"useImperativeHandle\",\n          \"path\": \"/reference/react/useImperativeHandle\"\n        },\n        {\n          \"title\": \"useInsertionEffect\",\n          \"path\": \"/reference/react/useInsertionEffect\"\n        },\n        {\n          \"title\": \"useLayoutEffect\",\n          \"path\": \"/reference/react/useLayoutEffect\"\n        },\n        {\n          \"title\": \"useMemo\",\n          \"path\": \"/reference/react/useMemo\"\n        },\n        {\n          \"title\": \"useOptimistic\",\n          \"path\": \"/reference/react/useOptimistic\"\n        },\n        {\n          \"title\": \"useReducer\",\n          \"path\": \"/reference/react/useReducer\"\n        },\n        {\n          \"title\": \"useRef\",\n          \"path\": \"/reference/react/useRef\"\n        },\n        {\n          \"title\": \"useState\",\n          \"path\": \"/reference/react/useState\"\n        },\n        {\n          \"title\": \"useSyncExternalStore\",\n          \"path\": \"/reference/react/useSyncExternalStore\"\n        },\n        {\n          \"title\": \"useTransition\",\n          \"path\": \"/reference/react/useTransition\"\n        }\n      ]\n    },\n    {\n      \"title\": \"컴포넌트\",\n      \"path\": \"/reference/react/components\",\n      \"routes\": [\n        {\n          \"title\": \"<Fragment> (<>)\",\n          \"path\": \"/reference/react/Fragment\"\n        },\n        {\n          \"title\": \"<Profiler>\",\n          \"path\": \"/reference/react/Profiler\"\n        },\n        {\n          \"title\": \"<StrictMode>\",\n          \"path\": \"/reference/react/StrictMode\"\n        },\n        {\n          \"title\": \"<Suspense>\",\n          \"path\": \"/reference/react/Suspense\"\n        },\n        {\n          \"title\": \"<Activity>\",\n          \"path\": \"/reference/react/Activity\"\n        },\n        {\n          \"title\": \"<ViewTransition>\",\n          \"path\": \"/reference/react/ViewTransition\",\n          \"version\": \"canary\"\n        }\n      ]\n    },\n    {\n      \"title\": \"API\",\n      \"path\": \"/reference/react/apis\",\n      \"routes\": [\n        {\n          \"title\": \"act\",\n          \"path\": \"/reference/react/act\"\n        },\n        {\n          \"title\": \"addTransitionType\",\n          \"path\": \"/reference/react/addTransitionType\",\n          \"version\": \"canary\"\n        },\n        {\n          \"title\": \"cache\",\n          \"path\": \"/reference/react/cache\"\n        },\n        {\n          \"title\": \"cacheSignal\",\n          \"path\": \"/reference/react/cacheSignal\"\n        },\n        {\n          \"title\": \"captureOwnerStack\",\n          \"path\": \"/reference/react/captureOwnerStack\"\n        },\n        {\n          \"title\": \"createContext\",\n          \"path\": \"/reference/react/createContext\"\n        },\n        {\n          \"title\": \"lazy\",\n          \"path\": \"/reference/react/lazy\"\n        },\n        {\n          \"title\": \"memo\",\n          \"path\": \"/reference/react/memo\"\n        },\n        {\n          \"title\": \"startTransition\",\n          \"path\": \"/reference/react/startTransition\"\n        },\n        {\n          \"title\": \"use\",\n          \"path\": \"/reference/react/use\"\n        },\n        {\n          \"title\": \"experimental_taintObjectReference\",\n          \"path\": \"/reference/react/experimental_taintObjectReference\",\n          \"version\": \"experimental\"\n        },\n        {\n          \"title\": \"experimental_taintUniqueValue\",\n          \"path\": \"/reference/react/experimental_taintUniqueValue\",\n          \"version\": \"experimental\"\n        }\n      ]\n    },\n    {\n      \"hasSectionHeader\": true,\n      \"sectionHeader\": \"react-dom@{{version}}\"\n    },\n    {\n      \"title\": \"Hook\",\n      \"path\": \"/reference/react-dom/hooks\",\n      \"routes\": [\n        {\n          \"title\": \"useFormStatus\",\n          \"path\": \"/reference/react-dom/hooks/useFormStatus\"\n        }\n      ]\n    },\n    {\n      \"title\": \"컴포넌트\",\n      \"path\": \"/reference/react-dom/components\",\n      \"routes\": [\n        {\n          \"title\": \"공통 컴포넌트 (예: <div>)\",\n          \"path\": \"/reference/react-dom/components/common\"\n        },\n        {\n          \"title\": \"<form>\",\n          \"path\": \"/reference/react-dom/components/form\"\n        },\n        {\n          \"title\": \"<input>\",\n          \"path\": \"/reference/react-dom/components/input\"\n        },\n        {\n          \"title\": \"<option>\",\n          \"path\": \"/reference/react-dom/components/option\"\n        },\n        {\n          \"title\": \"<progress>\",\n          \"path\": \"/reference/react-dom/components/progress\"\n        },\n        {\n          \"title\": \"<select>\",\n          \"path\": \"/reference/react-dom/components/select\"\n        },\n        {\n          \"title\": \"<textarea>\",\n          \"path\": \"/reference/react-dom/components/textarea\"\n        },\n        {\n          \"title\": \"<link>\",\n          \"path\": \"/reference/react-dom/components/link\"\n        },\n        {\n          \"title\": \"<meta>\",\n          \"path\": \"/reference/react-dom/components/meta\"\n        },\n        {\n          \"title\": \"<script>\",\n          \"path\": \"/reference/react-dom/components/script\"\n        },\n        {\n          \"title\": \"<style>\",\n          \"path\": \"/reference/react-dom/components/style\"\n        },\n        {\n          \"title\": \"<title>\",\n          \"path\": \"/reference/react-dom/components/title\"\n        }\n      ]\n    },\n    {\n      \"title\": \"API\",\n      \"path\": \"/reference/react-dom\",\n      \"routes\": [\n        {\n          \"title\": \"createPortal\",\n          \"path\": \"/reference/react-dom/createPortal\"\n        },\n        {\n          \"title\": \"flushSync\",\n          \"path\": \"/reference/react-dom/flushSync\"\n        },\n        {\n          \"title\": \"preconnect\",\n          \"path\": \"/reference/react-dom/preconnect\"\n        },\n        {\n          \"title\": \"prefetchDNS\",\n          \"path\": \"/reference/react-dom/prefetchDNS\"\n        },\n        {\n          \"title\": \"preinit\",\n          \"path\": \"/reference/react-dom/preinit\"\n        },\n        {\n          \"title\": \"preinitModule\",\n          \"path\": \"/reference/react-dom/preinitModule\"\n        },\n        {\n          \"title\": \"preload\",\n          \"path\": \"/reference/react-dom/preload\"\n        },\n        {\n          \"title\": \"preloadModule\",\n          \"path\": \"/reference/react-dom/preloadModule\"\n        }\n      ]\n    },\n    {\n      \"title\": \"클라이언트 API\",\n      \"path\": \"/reference/react-dom/client\",\n      \"routes\": [\n        {\n          \"title\": \"createRoot\",\n          \"path\": \"/reference/react-dom/client/createRoot\"\n        },\n        {\n          \"title\": \"hydrateRoot\",\n          \"path\": \"/reference/react-dom/client/hydrateRoot\"\n        }\n      ]\n    },\n    {\n      \"title\": \"서버 API\",\n      \"path\": \"/reference/react-dom/server\",\n      \"routes\": [\n        {\n          \"title\": \"renderToPipeableStream\",\n          \"path\": \"/reference/react-dom/server/renderToPipeableStream\"\n        },\n        {\n          \"title\": \"renderToReadableStream\",\n          \"path\": \"/reference/react-dom/server/renderToReadableStream\"\n        },\n        {\n          \"title\": \"renderToStaticMarkup\",\n          \"path\": \"/reference/react-dom/server/renderToStaticMarkup\"\n        },\n        {\n          \"title\": \"renderToString\",\n          \"path\": \"/reference/react-dom/server/renderToString\"\n        },\n        {\n          \"title\": \"resume\",\n          \"path\": \"/reference/react-dom/server/resume\"\n        },\n        {\n          \"title\": \"resumeToPipeableStream\",\n          \"path\": \"/reference/react-dom/server/resumeToPipeableStream\"\n        }\n      ]\n    },\n    {\n      \"title\": \"정적 API\",\n      \"path\": \"/reference/react-dom/static\",\n      \"routes\": [\n        {\n          \"title\": \"prerender\",\n          \"path\": \"/reference/react-dom/static/prerender\"\n        },\n        {\n          \"title\": \"prerenderToNodeStream\",\n          \"path\": \"/reference/react-dom/static/prerenderToNodeStream\"\n        },\n        {\n          \"title\": \"resumeAndPrerender\",\n          \"path\": \"/reference/react-dom/static/resumeAndPrerender\"\n        },\n        {\n          \"title\": \"resumeAndPrerenderToNodeStream\",\n          \"path\": \"/reference/react-dom/static/resumeAndPrerenderToNodeStream\"\n        }\n      ]\n    },\n    {\n      \"hasSectionHeader\": true,\n      \"sectionHeader\": \"React Compiler\"\n    },\n    {\n      \"title\": \"설정\",\n      \"path\": \"/reference/react-compiler/configuration\",\n      \"routes\": [\n        {\n          \"title\": \"compilationMode\",\n          \"path\": \"/reference/react-compiler/compilationMode\"\n        },\n        {\n          \"title\": \"gating\",\n          \"path\": \"/reference/react-compiler/gating\"\n        },\n        {\n          \"title\": \"logger\",\n          \"path\": \"/reference/react-compiler/logger\"\n        },\n        {\n          \"title\": \"panicThreshold\",\n          \"path\": \"/reference/react-compiler/panicThreshold\"\n        },\n        {\n          \"title\": \"target\",\n          \"path\": \"/reference/react-compiler/target\"\n        }\n      ]\n    },\n    {\n      \"title\": \"지시어\",\n      \"path\": \"/reference/react-compiler/directives\",\n      \"routes\": [\n        {\n          \"title\": \"\\\"use memo\\\"\",\n          \"path\": \"/reference/react-compiler/directives/use-memo\"\n        },\n        {\n          \"title\": \"\\\"use no memo\\\"\",\n          \"path\": \"/reference/react-compiler/directives/use-no-memo\"\n        }\n      ]\n    },\n    {\n      \"title\": \"라이브러리 컴파일\",\n      \"path\": \"/reference/react-compiler/compiling-libraries\"\n    },\n    {\n      \"hasSectionHeader\": true,\n      \"sectionHeader\": \"React 개발자 도구\"\n    },\n    {\n      \"title\": \"React Performance 트랙\",\n      \"path\": \"/reference/dev-tools/react-performance-tracks\"\n    },\n    {\n      \"hasSectionHeader\": true,\n      \"sectionHeader\": \"eslint-plugin-react-hooks\"\n    },\n    {\n      \"title\": \"린트\",\n      \"path\": \"/reference/eslint-plugin-react-hooks\",\n      \"routes\": [\n        {\n          \"title\": \"exhaustive-deps\",\n          \"path\": \"/reference/eslint-plugin-react-hooks/lints/exhaustive-deps\"\n        },\n        {\n          \"title\": \"rules-of-hooks\",\n          \"path\": \"/reference/eslint-plugin-react-hooks/lints/rules-of-hooks\"\n        },\n        {\n          \"title\": \"component-hook-factories\",\n          \"path\": \"/reference/eslint-plugin-react-hooks/lints/component-hook-factories\"\n        },\n        {\n          \"title\": \"config\",\n          \"path\": \"/reference/eslint-plugin-react-hooks/lints/config\"\n        },\n        {\n          \"title\": \"error-boundaries\",\n          \"path\": \"/reference/eslint-plugin-react-hooks/lints/error-boundaries\"\n        },\n        {\n          \"title\": \"gating\",\n          \"path\": \"/reference/eslint-plugin-react-hooks/lints/gating\"\n        },\n        {\n          \"title\": \"globals\",\n          \"path\": \"/reference/eslint-plugin-react-hooks/lints/globals\"\n        },\n        {\n          \"title\": \"immutability\",\n          \"path\": \"/reference/eslint-plugin-react-hooks/lints/immutability\"\n        },\n        {\n          \"title\": \"incompatible-library\",\n          \"path\": \"/reference/eslint-plugin-react-hooks/lints/incompatible-library\"\n        },\n\n        {\n          \"title\": \"preserve-manual-memoization\",\n          \"path\": \"/reference/eslint-plugin-react-hooks/lints/preserve-manual-memoization\"\n        },\n        {\n          \"title\": \"purity\",\n          \"path\": \"/reference/eslint-plugin-react-hooks/lints/purity\"\n        },\n        {\n          \"title\": \"refs\",\n          \"path\": \"/reference/eslint-plugin-react-hooks/lints/refs\"\n        },\n\n        {\n          \"title\": \"set-state-in-effect\",\n          \"path\": \"/reference/eslint-plugin-react-hooks/lints/set-state-in-effect\"\n        },\n        {\n          \"title\": \"set-state-in-render\",\n          \"path\": \"/reference/eslint-plugin-react-hooks/lints/set-state-in-render\"\n        },\n        {\n          \"title\": \"static-components\",\n          \"path\": \"/reference/eslint-plugin-react-hooks/lints/static-components\"\n        },\n        {\n          \"title\": \"unsupported-syntax\",\n          \"path\": \"/reference/eslint-plugin-react-hooks/lints/unsupported-syntax\"\n        },\n        {\n          \"title\": \"use-memo\",\n          \"path\": \"/reference/eslint-plugin-react-hooks/lints/use-memo\"\n        }\n      ]\n    },\n    {\n      \"hasSectionHeader\": true,\n      \"sectionHeader\": \"React의 규칙\"\n    },\n    {\n      \"title\": \"개요\",\n      \"path\": \"/reference/rules\",\n      \"routes\": [\n        {\n          \"title\": \"컴포넌트와 Hook은 순수해야 합니다\",\n          \"path\": \"/reference/rules/components-and-hooks-must-be-pure\"\n        },\n        {\n          \"title\": \"React가 컴포넌트와 Hook을 호출하는 방식\",\n          \"path\": \"/reference/rules/react-calls-components-and-hooks\"\n        },\n        {\n          \"title\": \"Hook의 규칙\",\n          \"path\": \"/reference/rules/rules-of-hooks\"\n        }\n      ]\n    },\n    {\n      \"hasSectionHeader\": true,\n      \"sectionHeader\": \"React 서버 컴포넌트\"\n    },\n    {\n      \"title\": \"서버 컴포넌트\",\n      \"path\": \"/reference/rsc/server-components\"\n    },\n    {\n      \"title\": \"서버 함수\",\n      \"path\": \"/reference/rsc/server-functions\"\n    },\n    {\n      \"title\": \"지시어\",\n      \"path\": \"/reference/rsc/directives\",\n      \"routes\": [\n        {\n          \"title\": \"'use client'\",\n          \"path\": \"/reference/rsc/use-client\"\n        },\n        {\n          \"title\": \"'use server'\",\n          \"path\": \"/reference/rsc/use-server\"\n        }\n      ]\n    },\n    {\n      \"hasSectionHeader\": true,\n      \"sectionHeader\": \"레거시 API\"\n    },\n    {\n      \"title\": \"레거시 React API\",\n      \"path\": \"/reference/react/legacy\",\n      \"routes\": [\n        {\n          \"title\": \"Children\",\n          \"path\": \"/reference/react/Children\"\n        },\n        {\n          \"title\": \"cloneElement\",\n          \"path\": \"/reference/react/cloneElement\"\n        },\n        {\n          \"title\": \"Component\",\n          \"path\": \"/reference/react/Component\"\n        },\n        {\n          \"title\": \"createElement\",\n          \"path\": \"/reference/react/createElement\"\n        },\n        {\n          \"title\": \"createRef\",\n          \"path\": \"/reference/react/createRef\"\n        },\n        {\n          \"title\": \"forwardRef\",\n          \"path\": \"/reference/react/forwardRef\"\n        },\n        {\n          \"title\": \"isValidElement\",\n          \"path\": \"/reference/react/isValidElement\"\n        },\n        {\n          \"title\": \"PureComponent\",\n          \"path\": \"/reference/react/PureComponent\"\n        }\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "src/siteConfig.js",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\nexports.siteConfig = {\n  version: '19.2',\n  // --------------------------------------\n  // Translations should replace these lines:\n  languageCode: 'ko',\n  hasLegacySite: true,\n  isRTL: false,\n  // --------------------------------------\n  copyright: `Copyright © ${new Date().getFullYear()} Facebook Inc. All Rights Reserved.`,\n  repoUrl: 'https://github.com/facebook/react',\n  twitterUrl: 'https://twitter.com/reactjs',\n  algolia: {\n    appId: '1FCF9AYYAT',\n    apiKey: '1b7ad4e1c89e645e351e59d40544eda1',\n    indexName: 'beta-react',\n  },\n};\n"
  },
  {
    "path": "src/styles/algolia.css",
    "content": "/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\n/* Algolia v3 overrides */\n:root {\n  --docsearch-modal-background: #fff;\n  --docsearch-highlight-color: #087ea4;\n  --docsearch-primary-color: #0074a6;\n  --docsearch-container-background: rgba(52, 58, 70, 0.8);\n  --docsearch-modal-shadow: none;\n  --docsearch-searchbox-shadow: 0 0 0 3px rgba(66, 153, 225, 0.5);\n  --ifm-z-index-fixed: 1000;\n  --docsearch-hit-height: 48px;\n  --docsearch-searchbox-height: 72px;\n  --docsearch-footer-height: 108px;\n  --docsearch-icon-stroke-width: 1.4;\n  --hover-overlay: rgba(0, 0, 0, 0.05);\n  --fds-animation-fade-in: cubic-bezier(0, 0, 1, 1);\n  --fds-animation-fade-out: cubic-bezier(0, 0, 1, 1);\n}\nhtml.dark {\n  --docsearch-modal-background: #23272f;\n  --docsearch-hit-background: #23272f;\n  --docsearch-highlight-color: #149eca;\n}\n.DocSearch--active #__next {\n  -webkit-filter: blur(0px);\n  filter: blur(0px);\n}\n.DocSearch-SearchBar {\n  @apply py-3;\n  @apply px-5;\n}\n.DocSearch-Form {\n  @apply rounded-full;\n  @apply shadow-none;\n  @apply text-base;\n  @apply text-tertiary;\n  @apply bg-gray-10;\n  @apply focus:outline-link;\n  @apply h-10;\n  @apply focus-within:outline-none;\n}\nhtml.dark .DocSearch-Form {\n  @apply bg-gray-80;\n}\n.DocSearch-Dropdown {\n  @apply px-0;\n  @apply h-full;\n  @apply max-h-full;\n}\n.DocSearch-Commands {\n  @apply w-full;\n  @apply justify-between;\n  @apply border-t;\n  @apply border-border;\n  @apply pt-4;\n}\nhtml.dark .DocSearch-Commands {\n  @apply border-border-dark;\n}\n.DocSearch-Commands-Key {\n  @apply shadow-none me-[.4rem] ms-0;\n  @apply bg-gray-10;\n  @apply text-primary;\n}\n.DocSearch-Logo {\n  @apply pt-4;\n  @apply pb-2;\n}\n.DocSearch-Logo svg {\n  @apply ms-2 me-0;\n}\n.DocSearch-Label {\n  @apply text-xs;\n}\n.DocSearch-Footer {\n  @apply bg-transparent;\n  @apply flex-col-reverse;\n  @apply items-start;\n  @apply h-auto;\n  @apply pb-2;\n  @apply px-5;\n  @apply shadow-none;\n}\nhtml.dark .DocSearch-Footer {\n  @apply bg-wash-dark;\n}\n.DocSearch-Input {\n  @apply py-3 ps-2 pe-0;\n  @apply text-base;\n  @apply leading-tight;\n  @apply text-primary;\n  @apply appearance-none !important;\n  @apply focus:outline-link !important;\n}\nhtml.dark .DocSearch-Input {\n  @apply text-primary-dark;\n}\n.DocSearch-Hit a {\n  @apply rounded-e-lg;\n  @apply rounded-s-none;\n  @apply shadow-none;\n  @apply ps-5;\n}\n.DocSearch-Hit-source {\n  @apply uppercase;\n  @apply tracking-wide;\n  @apply text-sm;\n  @apply text-secondary;\n  @apply font-bold;\n  @apply pt-0;\n  @apply ps-5;\n  @apply m-0;\n}\nhtml.dark .DocSearch-Hit-source {\n  @apply text-secondary-dark;\n}\n.DocSearch-Dropdown ul {\n  @apply me-5;\n}\n.DocSearch-Hit-title {\n  @apply text-base;\n  @apply text-primary;\n  @apply font-normal;\n  @apply text-ellipsis;\n  @apply whitespace-nowrap;\n  @apply overflow-hidden;\n}\nhtml.dark .DocSearch-Hit-title {\n  @apply text-primary-dark;\n}\n.DocSearch-Hit-path {\n  @apply font-normal;\n}\n.DocSearch-LoadingIndicator svg,\n.DocSearch-MagnifierLabel svg {\n  width: 15px;\n  height: 15px;\n  @apply text-gray-30;\n  @apply mx-1;\n}\n.DocSearch-Container {\n  @apply flex;\n  @apply justify-center;\n}\n.DocSearch-Modal {\n  margin: 0;\n  @apply flex;\n  @apply justify-center;\n  @apply max-w-3xl;\n  @apply w-full;\n  @apply rounded-2xl;\n  @apply overflow-hidden;\n  @apply my-4;\n  @apply shadow-nav;\n}\nhtml.dark .DocSearch-Modal {\n  @apply shadow-nav-dark;\n}\n.DocSearch-Cancel {\n  @apply ps-5;\n  @apply ms-0;\n  @apply text-base;\n  @apply text-link;\n  @apply font-normal;\n}\n.DocSearch-Screen-Icon {\n  @apply flex;\n  @apply justify-center;\n}\n.DocSearch-Help {\n  @apply text-center;\n  @apply mt-4;\n}\n@media (max-width: 1024px) {\n  .DocSearch-Modal {\n    @apply max-w-full;\n  }\n  .DocSearch-Cancel {\n    @apply inline-block;\n  }\n  .DocSearch-Commands {\n    @apply hidden;\n  }\n  .DocSearch-Modal {\n    @apply rounded-none;\n    @apply my-0;\n  }\n}\n.DocSearch-Search-Icon {\n  height: 20px;\n  width: 20px;\n  stroke-width: 1.6;\n  @apply text-gray-60;\n}\n"
  },
  {
    "path": "src/styles/index.css",
    "content": "/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\n@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n@layer base {\n  @font-face {\n    font-family: 'Source Code Pro';\n    font-style: normal;\n    font-weight: 400;\n    font-display: swap;\n    src: url('https://react.dev/fonts/Source-Code-Pro-Regular.woff2')\n      format('woff2');\n  }\n\n  @font-face {\n    font-family: 'Source Code Pro';\n    font-style: normal;\n    font-weight: 700;\n    font-display: swap;\n    src: url('https://react.dev/fonts/Source-Code-Pro-Bold.woff2')\n      format('woff2');\n  }\n\n  /* Latin */\n\n  @font-face {\n    font-family: 'Optimistic Display';\n    src: url('https://react.dev/fonts/Optimistic_Display_W_Md.woff2')\n      format('woff2');\n    font-weight: 500;\n    font-style: normal;\n    font-display: swap;\n  }\n\n  @font-face {\n    font-family: 'Optimistic Display';\n    src: url('https://react.dev/fonts/Optimistic_Display_W_MdIt.woff2')\n      format('woff2');\n    font-weight: 500;\n    font-style: italic;\n    font-display: swap;\n  }\n\n  @font-face {\n    font-family: 'Optimistic Display';\n    src: url('https://react.dev/fonts/Optimistic_Display_W_SBd.woff2')\n      format('woff2');\n    font-weight: 600;\n    font-style: normal;\n    font-display: swap;\n  }\n\n  @font-face {\n    font-family: 'Optimistic Display';\n    src: url('https://react.dev/fonts/Optimistic_Display_W_SBdIt.woff2')\n      format('woff2');\n    font-weight: 600;\n    font-style: italic;\n    font-display: swap;\n  }\n\n  @font-face {\n    font-family: 'Optimistic Display';\n    src: url('https://react.dev/fonts/Optimistic_Display_W_Bd.woff2')\n      format('woff2');\n    font-weight: 700;\n    font-style: normal;\n    font-display: swap;\n  }\n\n  @font-face {\n    font-family: 'Optimistic Display';\n    src: url('https://react.dev/fonts/Optimistic_Display_W_BdIt.woff2')\n      format('woff2');\n    font-weight: 700;\n    font-style: italic;\n    font-display: swap;\n  }\n\n  @font-face {\n    font-family: 'Optimistic Text';\n    src: url('https://react.dev/fonts/Optimistic_Text_W_Rg.woff2')\n      format('woff2');\n    font-weight: 400;\n    font-style: normal;\n    font-display: swap;\n  }\n\n  @font-face {\n    font-family: 'Optimistic Text';\n    src: url('https://react.dev/fonts/Optimistic_Text_W_It.woff2')\n      format('woff2');\n    font-weight: 400;\n    font-style: italic;\n    font-display: swap;\n  }\n\n  @font-face {\n    font-family: 'Optimistic Text';\n    src: url('https://react.dev/fonts/Optimistic_Text_W_Md.woff2')\n      format('woff2');\n    font-weight: 500;\n    font-style: normal;\n    font-display: swap;\n  }\n\n  @font-face {\n    font-family: 'Optimistic Text';\n    src: url('https://react.dev/fonts/Optimistic_Text_W_MdIt.woff2')\n      format('woff2');\n    font-weight: 500;\n    font-style: italic;\n    font-display: swap;\n  }\n\n  @font-face {\n    font-family: 'Optimistic Text';\n    src: url('https://react.dev/fonts/Optimistic_Text_W_Bd.woff2')\n      format('woff2');\n    font-weight: 700;\n    font-style: normal;\n    font-display: swap;\n  }\n\n  @font-face {\n    font-family: 'Optimistic Text';\n    src: url('https://react.dev/fonts/Optimistic_Text_W_BdIt.woff2')\n      format('woff2');\n    font-weight: 700;\n    font-style: italic;\n    font-display: swap;\n  }\n\n  /* Arabic */\n\n  @font-face {\n    font-family: 'Optimistic Display';\n    src: url('https://react.dev/fonts/Optimistic_Display_Arbc_W_Md.woff2')\n      format('woff2');\n    font-weight: 500;\n    font-style: normal;\n    font-display: swap;\n    unicode-range: U+0600-06FF;\n  }\n\n  @font-face {\n    font-family: 'Optimistic Display';\n    src: url('https://react.dev/fonts/Optimistic_Display_Arbc_W_SBd.woff2')\n      format('woff2');\n    font-weight: 600;\n    font-style: normal;\n    font-display: swap;\n    unicode-range: U+0600-06FF;\n  }\n\n  @font-face {\n    font-family: 'Optimistic Display';\n    src: url('https://react.dev/fonts/Optimistic_Display_Arbc_W_Bd.woff2')\n      format('woff2');\n    font-weight: 700;\n    font-style: normal;\n    font-display: swap;\n    unicode-range: U+0600-06FF;\n  }\n\n  @font-face {\n    font-family: 'Optimistic Text';\n    src: url('https://react.dev/fonts/Optimistic_Text_Arbc_W_Rg.woff2')\n      format('woff2');\n    font-weight: 400;\n    font-style: normal;\n    font-display: swap;\n    unicode-range: U+0600-06FF;\n  }\n\n  @font-face {\n    font-family: 'Optimistic Text';\n    src: url('https://react.dev/fonts/Optimistic_Text_Arbc_W_Md.woff2')\n      format('woff2');\n    font-weight: 500;\n    font-style: normal;\n    font-display: swap;\n    unicode-range: U+0600-06FF;\n  }\n\n  @font-face {\n    font-family: 'Optimistic Text';\n    src: url('https://react.dev/fonts/Optimistic_Text_Arbc_W_Bd.woff2')\n      format('woff2');\n    font-weight: 700;\n    font-style: normal;\n    font-display: swap;\n    unicode-range: U+0600-06FF;\n  }\n\n  /* Cyrillic */\n\n  @font-face {\n    font-family: 'Optimistic Display';\n    src: url('https://react.dev/fonts/Optimistic_Display_Cyrl_W_Md.woff2')\n      format('woff2');\n    font-weight: 500;\n    font-style: normal;\n    font-display: swap;\n    unicode-range: U+0400-045F, U+2116;\n  }\n\n  @font-face {\n    font-family: 'Optimistic Display';\n    src: url('https://react.dev/fonts/Optimistic_Display_Cyrl_W_SBd.woff2')\n      format('woff2');\n    font-weight: 600;\n    font-style: normal;\n    font-display: swap;\n    unicode-range: U+0400-045F, U+2116;\n  }\n\n  @font-face {\n    font-family: 'Optimistic Display';\n    src: url('https://react.dev/fonts/Optimistic_Display_Cyrl_W_Bd.woff2')\n      format('woff2');\n    font-weight: 700;\n    font-style: normal;\n    font-display: swap;\n    unicode-range: U+0400-045F, U+2116;\n  }\n\n  @font-face {\n    font-family: 'Optimistic Text';\n    src: url('https://react.dev/fonts/Optimistic_Text_Cyrl_W_Rg.woff2')\n      format('woff2');\n    font-weight: 400;\n    font-style: normal;\n    font-display: swap;\n    unicode-range: U+0400-045F, U+2116;\n  }\n\n  @font-face {\n    font-family: 'Optimistic Text';\n    src: url('https://react.dev/fonts/Optimistic_Text_Cyrl_W_Md.woff2')\n      format('woff2');\n    font-weight: 500;\n    font-style: normal;\n    font-display: swap;\n    unicode-range: U+0400-045F, U+2116;\n  }\n\n  @font-face {\n    font-family: 'Optimistic Text';\n    src: url('https://react.dev/fonts/Optimistic_Text_Cyrl_W_Bd.woff2')\n      format('woff2');\n    font-weight: 700;\n    font-style: normal;\n    font-display: swap;\n    unicode-range: U+0400-045F, U+2116;\n  }\n\n  /* Devanagari */\n\n  @font-face {\n    font-family: 'Optimistic Display';\n    src: url('https://react.dev/fonts/Optimistic_Display_Deva_W_Md.woff2')\n      format('woff2');\n    font-weight: 500;\n    font-style: normal;\n    font-display: swap;\n    unicode-range: U+0900-097F, U+1CD0-1CF6, U+1CF8-1CF9, U+200C-200D, U+20A8,\n      U+20B9, U+25CC, U+A830-A839, U+A8E0-A8FB;\n  }\n\n  @font-face {\n    font-family: 'Optimistic Display';\n    src: url('https://react.dev/fonts/Optimistic_Display_Deva_W_SBd.woff2')\n      format('woff2');\n    font-weight: 600;\n    font-style: normal;\n    font-display: swap;\n    unicode-range: U+0900-097F, U+1CD0-1CF6, U+1CF8-1CF9, U+200C-200D, U+20A8,\n      U+20B9, U+25CC, U+A830-A839, U+A8E0-A8FB;\n  }\n\n  @font-face {\n    font-family: 'Optimistic Display';\n    src: url('https://react.dev/fonts/Optimistic_Display_Deva_W_Bd.woff2')\n      format('woff2');\n    font-weight: 700;\n    font-style: normal;\n    font-display: swap;\n    unicode-range: U+0900-097F, U+1CD0-1CF6, U+1CF8-1CF9, U+200C-200D, U+20A8,\n      U+20B9, U+25CC, U+A830-A839, U+A8E0-A8FB;\n  }\n\n  @font-face {\n    font-family: 'Optimistic Text';\n    src: url('https://react.dev/fonts/Optimistic_Text_Deva_W_Rg.woff2')\n      format('woff2');\n    font-weight: 400;\n    font-style: normal;\n    font-display: swap;\n    unicode-range: U+0900-097F, U+1CD0-1CF6, U+1CF8-1CF9, U+200C-200D, U+20A8,\n      U+20B9, U+25CC, U+A830-A839, U+A8E0-A8FB;\n  }\n\n  @font-face {\n    font-family: 'Optimistic Text';\n    src: url('https://react.dev/fonts/Optimistic_Text_Deva_W_Md.woff2')\n      format('woff2');\n    font-weight: 500;\n    font-style: normal;\n    font-display: swap;\n    unicode-range: U+0900-097F, U+1CD0-1CF6, U+1CF8-1CF9, U+200C-200D, U+20A8,\n      U+20B9, U+25CC, U+A830-A839, U+A8E0-A8FB;\n  }\n\n  @font-face {\n    font-family: 'Optimistic Text';\n    src: url('https://react.dev/fonts/Optimistic_Text_Deva_W_Bd.woff2')\n      format('woff2');\n    font-weight: 700;\n    font-style: normal;\n    font-display: swap;\n    unicode-range: U+0900-097F, U+1CD0-1CF6, U+1CF8-1CF9, U+200C-200D, U+20A8,\n      U+20B9, U+25CC, U+A830-A839, U+A8E0-A8FB;\n  }\n\n  /* Vietnamese */\n\n  @font-face {\n    font-family: 'Optimistic Display';\n    src: url('https://react.dev/fonts/Optimistic_Display_Viet_W_Md.woff2')\n      format('woff2');\n    font-weight: 500;\n    font-style: normal;\n    font-display: swap;\n    unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB;\n  }\n\n  @font-face {\n    font-family: 'Optimistic Display';\n    src: url('https://react.dev/fonts/Optimistic_Display_Viet_W_SBd.woff2')\n      format('woff2');\n    font-weight: 600;\n    font-style: normal;\n    font-display: swap;\n    unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB;\n  }\n\n  @font-face {\n    font-family: 'Optimistic Display';\n    src: url('https://react.dev/fonts/Optimistic_Display_Viet_W_Bd.woff2')\n      format('woff2');\n    font-weight: 700;\n    font-style: normal;\n    font-display: swap;\n    unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB;\n  }\n\n  @font-face {\n    font-family: 'Optimistic Text';\n    src: url('https://react.dev/fonts/Optimistic_Text_Viet_W_Rg.woff2')\n      format('woff2');\n    font-weight: 400;\n    font-style: normal;\n    font-display: swap;\n    unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB;\n  }\n\n  @font-face {\n    font-family: 'Optimistic Text';\n    src: url('https://react.dev/fonts/Optimistic_Text_Viet_W_Md.woff2')\n      format('woff2');\n    font-weight: 500;\n    font-style: normal;\n    font-display: swap;\n    unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB;\n  }\n\n  @font-face {\n    font-family: 'Optimistic Text';\n    src: url('https://react.dev/fonts/Optimistic_Text_Viet_W_Bd.woff2')\n      format('woff2');\n    font-weight: 700;\n    font-style: normal;\n    font-display: swap;\n    unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB;\n  }\n\n  /* Write your own custom base styles here */\n  html {\n    color-scheme: light;\n\n    -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n    -webkit-tap-highlight-color: transparent;\n  }\n\n  html.dark {\n    color-scheme: dark;\n  }\n\n  html .dark-image {\n    display: none;\n  }\n\n  html .light-image {\n    display: block;\n  }\n\n  html.dark .dark-image {\n    display: block;\n  }\n\n  html.dark .light-image {\n    display: none;\n  }\n\n  /* Hide all content that's relevant only to a specific platform */\n  html.platform-mac [data-platform='win'] {\n    display: none;\n  }\n\n  html.platform-win [data-platform='mac'] {\n    display: none;\n  }\n\n  html,\n  body {\n    padding: 0;\n    margin: 0;\n  }\n\n  @media screen and (max-width: 1023px) {\n    body {\n      overflow-x: hidden;\n    }\n  }\n\n  /* Start purging... */\n  /* Force GPU Accelerated scrolling, credit: Twitter Lite */\n  .scrolling-gpu {\n    transform: translateZ(0);\n  }\n\n  @layer utilities {\n    .text-7xl {\n      font-size: 5rem;\n    }\n\n    .text-8xl {\n      font-size: 6rem;\n    }\n  }\n\n  a > code {\n    color: #087ea4 !important; /* blue-50 */\n    text-decoration: none !important;\n  }\n\n  html.dark a > code {\n    color: #58c4dc !important; /* blue-40 */\n  }\n\n  .text-code {\n    font-size: calc(1em - 10%) !important;\n  }\n\n  .text-gradient {\n    background-clip: text;\n    -webkit-background-clip: text;\n    -webkit-text-fill-color: transparent;\n    box-decoration-break: clone;\n    background-repeat: no-repeat;\n    color: transparent;\n  }\n\n  .text-gradient-electric-blue {\n    background-image: linear-gradient(45deg, #61dafb, #0072ff);\n  }\n  /* Stop purging. */\n  /* Your own custom utilities */\n\n  details {\n    margin-bottom: 1rem;\n  }\n\n  table {\n    width: 100%;\n    margin-bottom: 1rem;\n    display: block;\n    overflow-x: auto;\n  }\n\n  table td,\n  table th {\n    padding: 0.75rem;\n    vertical-align: top;\n    border: 1px solid #dee2e6;\n    overflow: auto;\n  }\n\n  summary::-webkit-details-marker {\n    display: none;\n  }\n\n  /*\n   * Hopefully when scrollbar-color lands everywhere,\n   * (and not just in FF), we'll be able to keep just this.\n   */\n  html .no-bg-scrollbar {\n    scrollbar-color: rgba(0, 0, 0, 0.2) transparent;\n  }\n  html.dark .no-bg-scrollbar {\n    scrollbar-color: rgba(255, 255, 255, 0.2) transparent;\n  }\n  /*\n   * Until then, we have ... this.\n   * If you're changing this, make sure you've tested:\n   * - Different browsers (Chrome, Safari, FF)\n   * - Dark and light modes\n   * - System scrollbar settings (\"always on\" vs \"when scrolling\")\n   * - Switching between modes should never jump width\n   * - When you interact with a sidebar, it should always be visible\n   * - For each combination, test overflowing and non-overflowing sidebar\n   * I've spent hours picking these so I expect no less diligence from you.\n   */\n  html .no-bg-scrollbar::-webkit-scrollbar,\n  html .no-bg-scrollbar::-webkit-scrollbar-track {\n    background-color: transparent;\n  }\n  html .no-bg-scrollbar:hover::-webkit-scrollbar-thumb,\n  html .no-bg-scrollbar:focus::-webkit-scrollbar-thumb,\n  html .no-bg-scrollbar:focus-within::-webkit-scrollbar-thumb,\n  html .no-bg-scrollbar:active::-webkit-scrollbar-thumb {\n    background-color: rgba(0, 0, 0, 0.2);\n    border: 4px solid transparent;\n    background-clip: content-box;\n    border-radius: 10px;\n  }\n  html .no-bg-scrollbar::-webkit-scrollbar-thumb:hover,\n  html .no-bg-scrollbar::-webkit-scrollbar-thumb:active {\n    background-color: rgba(0, 0, 0, 0.35) !important;\n  }\n  html.dark .no-bg-scrollbar:hover::-webkit-scrollbar-thumb,\n  html.dark .no-bg-scrollbar:focus::-webkit-scrollbar-thumb,\n  html.dark .no-bg-scrollbar:focus-within::-webkit-scrollbar-thumb,\n  html.dark .no-bg-scrollbar:active::-webkit-scrollbar-thumb {\n    background-color: rgba(255, 255, 255, 0.2);\n  }\n  html.dark .no-bg-scrollbar::-webkit-scrollbar-thumb:hover,\n  html.dark .no-bg-scrollbar::-webkit-scrollbar-thumb:active {\n    background-color: rgba(255, 255, 255, 0.35) !important;\n  }\n}\n\n@layer utilities {\n  [class*='space-x-'] {\n    @apply rtl:space-x-reverse;\n  }\n}\n\n.code-step * {\n  color: inherit !important;\n}\n\n.code-step code {\n  background: none !important;\n  padding: 2px !important;\n}\n\n.dark .console-block code {\n  background: rgba(235 236 240 / 0.05) !important;\n  color: rgba(208, 125, 119) !important;\n}\n\n.console-block code {\n  background: rgba(235 236 240 / 0.95) !important;\n  color: rgb(166, 66, 58) !important;\n}\n\nhtml.dark .code-step * {\n  color: inherit !important;\n}\n\n.mdx-heading {\n  scroll-margin-top: calc(4rem + 20px);\n  /* Space for the anchor */\n  padding-inline-end: 1em;\n}\n\n.mdx-heading:before {\n  height: 6rem;\n  margin-top: -6rem;\n  visibility: hidden;\n  content: '';\n}\n.mdx-heading .mdx-header-anchor {\n  /* Prevent the anchor from\n     overflowing to its own line */\n  height: 0px;\n  width: 0px;\n}\n.mdx-heading .mdx-header-anchor svg {\n  display: inline;\n}\n.mdx-heading .mdx-header-anchor svg {\n  visibility: hidden;\n}\n.mdx-heading:hover .mdx-header-anchor svg {\n  visibility: visible;\n}\n.mdx-heading .mdx-header-anchor:focus svg {\n  visibility: visible;\n}\n\n.mdx-blockquote > span > p:first-of-type {\n  margin-bottom: 0;\n}\n.mdx-blockquote > span > p:last-of-type {\n  margin-bottom: 1rem;\n}\n.mdx-illustration-block {\n  display: flex;\n  flex-direction: row;\n  flex-wrap: nowrap;\n  justify-content: center;\n  align-content: stretch;\n  align-items: stretch;\n  gap: 42px;\n}\nol.mdx-illustration-block {\n  gap: 60px;\n}\n.mdx-illustration-block li {\n  display: flex;\n  align-items: flex-start;\n  align-content: stretch;\n  justify-content: space-around;\n  position: relative;\n  padding: 1rem;\n}\n.mdx-illustration-block figure {\n  display: flex;\n  flex-direction: column;\n  align-content: center;\n  align-items: center;\n\n  justify-content: space-between;\n  position: relative;\n  height: 100%;\n}\n.mdx-illustration-block li:after {\n  content: ' ';\n  display: block;\n  position: absolute;\n  top: 50%;\n  inset-inline-end: 100%;\n  transform: translateY(-50%);\n  width: 60px;\n  height: 49px;\n  background: center / contain no-repeat url('/images/g_arrow.png');\n}\n.mdx-illustration-block li:first-child:after {\n  content: ' ';\n  display: none;\n}\n.mdx-illustration-block img {\n  max-height: 250px;\n  width: 100%;\n}\n@media (max-width: 680px) {\n  .mdx-illustration-block {\n    flex-direction: column;\n  }\n  .mdx-illustration-block img {\n    max-height: 200px;\n    width: auto;\n  }\n  .mdx-illustration-block li:after {\n    top: 0;\n    inset-inline-start: 50%;\n    inset-inline-end: auto;\n    transform: translateX(-50%) translateY(-100%) rotate(90deg);\n  }\n}\n\n@keyframes fadein {\n  from {\n    opacity: 0;\n  }\n  to {\n    opacity: 1;\n  }\n}\n\n.animation-pulse-button {\n  animation: pulse-button 2s infinite;\n}\n\n.animation-pulse-shadow {\n  animation: pulse-shadow 2s infinite;\n}\n\n@keyframes pulse-button {\n  0% {\n    transform: scale(0.9);\n  }\n  70% {\n    transform: scale(1);\n  }\n  100% {\n    transform: scale(0.9);\n  }\n}\n\n@keyframes pulse-shadow {\n  0% {\n    transform: scale(0.65);\n    opacity: 1;\n  }\n\n  70% {\n    transform: scale(1);\n    opacity: 0;\n  }\n\n  100% {\n    transform: scale(0.65);\n    opacity: 0;\n  }\n}\n\n@keyframes progressbar {\n  from {\n    width: 0;\n  }\n  to {\n    width: 100%;\n  }\n}\n\n.uwu-visible {\n  display: none;\n}\n.uwu-hidden {\n  display: flex;\n}\n\n.uwu .uwu-visible {\n  display: flex;\n}\n\n.uwu .uwu-hidden {\n  display: none;\n}\n"
  },
  {
    "path": "src/styles/sandpack.css",
    "content": "/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\n.sandpack {\n  color-scheme: inherit;\n  -webkit-font-smoothing: antialiased;\n\n  --sp-space-1: 4px;\n  --sp-space-2: 8px;\n  --sp-space-3: 12px;\n  --sp-space-4: 16px;\n  --sp-space-5: 20px;\n  --sp-space-6: 24px;\n  --sp-space-7: 28px;\n  --sp-space-8: 32px;\n  --sp-space-9: 36px;\n  --sp-space-10: 40px;\n  --sp-space-11: 44px;\n  --sp-border-radius: 4px;\n  --sp-layout-height: 300px;\n  --sp-layout-headerHeight: 40px;\n  --sp-transitions-default: 150ms ease;\n  --sp-zIndices-base: 1;\n  --sp-zIndices-overlay: 2;\n  --sp-zIndices-top: 3;\n\n  --sp-font-body: Optimistic Display, -apple-system, ui-sans-serif, system-ui,\n    -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial,\n    Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol,\n    Noto Color Emoji;\n  --sp-font-mono: Source Code Pro, ui-monospace, SFMono-Regular, Menlo, Monaco,\n    Consolas, Liberation Mono, Courier New, monospace;\n  --sp-font-size: calc(1em - 20%);\n  --sp-font-lineHeight: 24px;\n}\n\n/* Default theme */\nhtml .sandpack {\n  --sp-colors-accent: #087ea4;\n  --sp-colors-clickable: #959da5;\n  --sp-colors-disabled: #24292e;\n  --sp-colors-error: #811e18;\n  --sp-colors-error-surface: #ffcdca;\n  --sp-colors-surface1: #fff;\n  --sp-colors-surface2: #e4e7eb;\n\n  --sp-syntax-color-plain: #24292e;\n  --sp-syntax-color-comment: #6a737d;\n  --sp-syntax-color-keyword: #d73a49;\n  --sp-syntax-color-tag: #22863a;\n  --sp-syntax-color-punctuation: #24292e;\n  --sp-syntax-color-definition: #6f42c1;\n  --sp-syntax-color-property: #005cc5;\n  --sp-syntax-color-static: #032f62;\n  --sp-syntax-color-string: #032f62;\n}\n\n/* Dark theme */\nhtml.dark .sp-wrapper {\n  --sp-colors-accent: #58c4dc;\n  --sp-colors-clickable: #999;\n  --sp-colors-disabled: #fff;\n  --sp-colors-error: #811e18;\n  --sp-colors-error-surface: #ffcdca;\n  --sp-colors-surface1: #16181d;\n  --sp-colors-surface2: #343a46;\n\n  --sp-syntax-color-plain: #ffffff;\n  --sp-syntax-color-comment: #757575;\n  --sp-syntax-color-keyword: #77b7d7;\n  --sp-syntax-color-tag: #dfab5c;\n  --sp-syntax-color-punctuation: #ffffff;\n  --sp-syntax-color-definition: #86d9ca;\n  --sp-syntax-color-property: #77b7d7;\n  --sp-syntax-color-static: #c64640;\n  --sp-syntax-color-string: #977cdc;\n}\n\n/**\n * Reset\n */\n.sandpack .sp-wrapper {\n  width: 100%;\n\n  font-size: var(--sp-font-size);\n  font-family: var(--sp-font-body);\n  line-height: var(--sp-font-lineHeight);\n}\n\n/**\n * Layout\n */\n.sandpack .sp-layout {\n  display: flex;\n  flex-wrap: wrap;\n  align-items: stretch;\n  background-color: var(--sp-colors-surface2);\n\n  -webkit-mask-image: -webkit-radial-gradient(\n    var(--sp-colors-surface1),\n    var(--sp-colors-surface1)\n  ); /* safest way to make all corner rounded */\n\n  border-bottom-left-radius: 0.5rem;\n  border-bottom-right-radius: 0.5rem;\n  overflow: initial;\n\n  gap: 1px;\n}\n\n.sandpack .sp-stack {\n  display: flex;\n  flex-direction: column;\n  width: 100%;\n  position: relative;\n}\n\n@media screen and (max-width: 768px) {\n  .sandpack .sp-layout > .sp-stack {\n    height: auto;\n    min-width: 100% !important;\n  }\n}\n\n.sandpack .sp-layout > .sp-stack {\n  flex: 1 1 0px;\n  height: var(--sp-layout-height);\n}\n\n/**\n * Focus ring\n */\n.sandpack--playground .sp-tab-button {\n  transition: none;\n}\n\n.sandpack--playground .sp-tab-button:focus {\n  outline: revert;\n}\n\n.sandpack--playground .sp-tab-button:focus-visible {\n  box-shadow: none;\n}\n\n.sandpack .sp-cm:focus-visible {\n  box-shadow: inset 0 0 0 4px rgba(20, 158, 202, 0.4);\n  outline: none;\n  height: 100%;\n}\n\n/**\n * Navigation\n */\n.sandpack .sp-tabs-scrollable-container {\n  overflow: auto;\n  display: flex;\n  flex-wrap: nowrap;\n  align-items: stretch;\n  min-height: 40px;\n  margin-bottom: -1px;\n}\n\n.sp-tabs .sp-tab-button {\n  padding: 0 6px;\n  border-bottom: 2px solid transparent;\n}\n\n@media (min-width: 768px) {\n  .sp-tabs .sp-tab-button {\n    margin: 0 12px 0 0;\n  }\n}\n\n.sp-tabs .sp-tab-button,\n.sp-tabs .sp-tab-button:hover:not(:disabled, [data-active='true']),\n.sp-tabs .sp-tab-button[data-active='true'] {\n  color: var(--sp-colors-accent);\n}\n\n.sp-tabs .sp-tab-button[data-active='true'] {\n  border-bottom: 2px solid var(--sp-colors-accent);\n}\n\n/**\n * Code block\n */\n.cm-line {\n  padding-left: var(--sp-space-5);\n}\n\n/**\n * Editor\n */\n.sandpack .sp-code-editor {\n  flex: 1 1;\n  position: relative;\n  overflow: auto;\n  background: var(--sp-colors-surface1);\n}\n\n.sandpack .sp-code-editor .cm-editor {\n  background-color: transparent;\n}\n\n.sandpack .sp-code-editor .cm-content,\n.sandpack .sp-code-editor .cm-gutters,\n.sandpack .sp-code-editor .cm-gutterElement {\n  padding: 0;\n  -webkit-font-smoothing: auto; /* Improve the legibility */\n}\n\n.sandpack .sp-code-editor .cm-content {\n  padding-bottom: 18px;\n}\n\n.sandpack--playground .sp-code-editor .cm-line {\n  padding: 0 var(--sp-space-3);\n  width: max-content;\n}\n\n.sandpack--playground .sp-code-editor .cm-lineNumbers {\n  padding-left: var(--sp-space-3);\n  padding-right: var(--sp-space-1);\n  font-size: 13.6px;\n}\n\n.sandpack--playground .sp-code-editor .cm-line.cm-errorLine {\n  @apply bg-red-400;\n  --tw-bg-opacity: 0.1; /* Background tweak: base color + opacity */\n  position: relative;\n  padding-right: 2em;\n  display: inline-block;\n  min-width: 100%;\n}\n\n.sp-code-editor .cm-errorLine:after {\n  @apply text-red-500;\n  position: absolute;\n  right: 8px;\n  top: 0;\n  content: '\\26A0';\n  font-size: 22px;\n  line-height: 20px;\n}\n\n.sp-code-editor .cm-tooltip {\n  border: 0;\n  max-width: 200px;\n}\n\n.sp-code-editor .cm-diagnostic-error {\n  @apply border-red-40;\n}\n\n.sandpack .sp-cm {\n  margin: 0px;\n  outline: none;\n  height: 100%;\n}\n\n.sp-code-editor .sp-cm .cm-scroller {\n  padding-top: 18px;\n}\n\n/**\n * Syntax highlight (code editor + code block)\n */\n.sandpack .sp-syntax-string {\n  color: var(--sp-syntax-color-string);\n}\n\n.sandpack .sp-syntax-plain {\n  color: var(--sp-syntax-color-plain);\n}\n\n.sandpack .sp-syntax-comment {\n  color: var(--sp-syntax-color-comment);\n}\n\n.sandpack .sp-syntax-keyword {\n  color: var(--sp-syntax-color-keyword);\n}\n\n.sandpack .sp-syntax-definition {\n  color: var(--sp-syntax-color-definition);\n}\n\n.sandpack .sp-syntax-punctuation {\n  color: var(--sp-syntax-color-punctuation);\n}\n\n.sandpack .sp-syntax-property {\n  color: var(--sp-syntax-color-property);\n}\n\n.sandpack .sp-syntax-tag {\n  color: var(--sp-syntax-color-tag);\n}\n\n.sandpack .sp-syntax-static {\n  color: var(--sp-syntax-color-static);\n}\n\n/**\n * Loading & error overlay component\n */\n.sandpack .sp-cube-wrapper {\n  background-color: var(--sp-colors-surface1);\n  position: absolute;\n  right: var(--sp-space-2);\n  bottom: var(--sp-space-2);\n  z-index: var(--sp-zIndices-top);\n  width: 32px;\n  height: 32px;\n  border-radius: var(--sp-border-radius);\n}\n\n.sandpack .sp-button {\n  display: flex;\n  align-items: center;\n  margin: auto;\n  width: 100%;\n  height: 100%;\n}\n\n.sandpack .sp-button svg {\n  min-width: var(--sp-space-5);\n  width: var(--sp-space-5);\n  height: var(--sp-space-5);\n  margin: auto;\n}\n\n.sandpack .sp-cube-wrapper .sp-cube {\n  display: flex;\n}\n\n.sandpack .sp-cube-wrapper .sp-button {\n  display: none;\n}\n\n.sandpack .sp-cube-wrapper:hover .sp-button {\n  display: block;\n}\n\n.sandpack .sp-cube-wrapper:hover .sp-cube {\n  display: none;\n}\n\n.sandpack .sp-cube {\n  transform: translate(-4px, 9px) scale(0.13, 0.13);\n}\n\n.sandpack .sp-cube * {\n  position: absolute;\n  width: 96px;\n  height: 96px;\n}\n\n@keyframes cubeRotate {\n  0% {\n    transform: rotateX(-25.5deg) rotateY(45deg);\n  }\n\n  100% {\n    transform: rotateX(-25.5deg) rotateY(405deg);\n  }\n}\n\n.sandpack .sp-sides {\n  animation: cubeRotate 1s linear infinite;\n  animation-fill-mode: forwards;\n  transform-style: preserve-3d;\n  transform: rotateX(-25.5deg) rotateY(45deg);\n}\n\n.sandpack .sp-sides * {\n  border: 10px solid var(--sp-colors-clickable);\n  border-radius: 8px;\n  background: var(--sp-colors-surface1);\n}\n\n.sandpack .sp-sides .top {\n  transform: rotateX(90deg) translateZ(44px);\n  transform-origin: 50% 50%;\n}\n.sandpack .sp-sides .bottom {\n  transform: rotateX(-90deg) translateZ(44px);\n  transform-origin: 50% 50%;\n}\n.sandpack .sp-sides .front {\n  transform: rotateY(0deg) translateZ(44px);\n  transform-origin: 50% 50%;\n}\n.sandpack .sp-sides .back {\n  transform: rotateY(-180deg) translateZ(44px);\n  transform-origin: 50% 50%;\n}\n.sandpack .sp-sides .left {\n  transform: rotateY(-90deg) translateZ(44px);\n  transform-origin: 50% 50%;\n}\n.sandpack .sp-sides .right {\n  transform: rotateY(90deg) translateZ(44px);\n  transform-origin: 50% 50%;\n}\n\n.sandpack .sp-overlay {\n  @apply bg-card;\n  position: absolute;\n  inset: 0;\n  z-index: var(--sp-zIndices-top);\n}\n\n.sandpack .sp-error {\n  padding: var(--sp-space-4);\n  white-space: pre-wrap;\n  font-family: var(--sp-font-mono);\n  background-color: var(--sp-colors-error-surface);\n}\n\n@keyframes fadeIn {\n  0% {\n    opacity: 0;\n    transform: translateY(4px);\n  }\n\n  100% {\n    opacity: 1;\n    transform: translateY(0);\n  }\n}\n\n.sandpack .sp-error-message {\n  animation: fadeIn 150ms ease;\n  color: var(--sp-colors-error);\n}\n\nhtml.dark .sandpack--playground .sp-overlay {\n  @apply bg-wash-dark;\n}\n\n/**\n * Placeholder\n */\n.sandpack .sp-code-editor .sp-pre-placeholder {\n  @apply font-mono;\n  font-size: 13.6px;\n  line-height: 24px;\n  padding: 18px 0;\n  -webkit-font-smoothing: auto;\n}\n\n.sandpack--playground .sp-code-editor .sp-pre-placeholder {\n  padding-left: 48px !important;\n  margin-left: 0px !important;\n}\n\n.text-xl .sp-pre-placeholder {\n  font-size: 16px !important;\n  line-height: 24px !important;\n}\n\n/**\n * Expand button\n */\n.sandpack .sp-layout {\n  min-height: 216px;\n}\n\n.sandpack .sp-layout > .sp-stack:nth-child(1) {\n  /* Force vertical if there isn't enough space. */\n  min-width: 431px;\n  /* No min height on mobile because we know code in advance. */\n  /* Max height is needed to avoid too long files. */\n  max-height: 40vh;\n}\n\n.sandpack .sp-layout > .sp-stack:nth-child(2) {\n  /* Force vertical if there isn't enough space. */\n  min-width: 431px;\n  /* Keep preview a fixed size on mobile to avoid jumps. */\n  /* This is because we don't know its content in advance. */\n  min-height: 40vh;\n  max-height: 40vh;\n}\n.sandpack .sp-layout.sp-layout-expanded > .sp-stack:nth-child(1) {\n  /* Clicking \"show more\" lets mobile editor go full height. */\n  max-height: unset;\n  height: auto;\n}\n\n.sandpack .sp-layout.sp-layout-expanded > .sp-stack:nth-child(2) {\n  /* Clicking \"show more\" lets mobile preview go full height. */\n  max-height: unset;\n  height: auto;\n}\n\n@media (min-width: 1280px) {\n  .sandpack .sp-layout > .sp-stack:nth-child(1) {\n    /* On desktop, clamp height by pixels instead. */\n    height: auto;\n    min-height: unset;\n    max-height: 406px;\n  }\n  .sandpack .sp-layout > .sp-stack:nth-child(2) {\n    /* On desktop, clamp height by pixels instead. */\n    height: auto;\n    min-height: unset;\n    max-height: 406px;\n  }\n  .sandpack .sp-layout.sp-layout-expanded > .sp-stack:nth-child(1) {\n    max-height: unset;\n  }\n  .sandpack .sp-layout.sp-layout-expanded > .sp-stack:nth-child(2) {\n    max-height: unset;\n  }\n}\n\n.sandpack .sp-layout .sandpack-expand {\n  border-left: none;\n  margin-left: 0;\n}\n\n.expandable-callout .sp-stack:nth-child(2) {\n  min-width: 431px;\n  min-height: 40vh;\n  max-height: 40vh;\n}\n\n/**\n * Integrations: console\n */\n.sandpack .console .sp-cm,\n.sandpack .console .sp-cm .cm-scroller,\n.sandpack .console .sp-cm .cm-line {\n  padding: 0px !important;\n}\n\n/**\n * Integrations: eslint\n */\n.sandpack .sp-code-editor .cm-diagnostic {\n  @apply text-secondary;\n}\n\n/**\n * Overwrite inline sty\n */\n.sandpack .sp-devtools > div {\n  --color-background: var(--sp-colors-surface1) !important;\n  --color-background-inactive: var(--sp-colors-surface2) !important;\n  --color-background-selected: var(--sp-colors-accent) !important;\n  --color-background-hover: transparent !important;\n  --color-modal-background: #ffffffd2 !important;\n\n  --color-tab-selected-border: #087ea4 !important;\n\n  --color-component-name: var(--sp-syntax-color-definition) !important;\n  --color-attribute-name: var(--sp-syntax-color-property) !important;\n  --color-attribute-value: var(--sp-syntax-color-string) !important;\n  --color-attribute-editable-value: var(--sp-syntax-color-property) !important;\n  --color-attribute-name-not-editable: var(--sp-colors-clickable) !important;\n  --color-button-background-focus: var(--sp-colors-surface2) !important;\n\n  --color-button-active: var(--sp-colors-accent) !important;\n  --color-button-background: transparent !important;\n  --color-button: var(--sp-colors-clickable) !important;\n  --color-button-hover: var(--sp-colors-disabled) !important;\n\n  --color-border: var(--sp-colors-surface2) !important;\n  --color-text: rgb(35, 39, 47) !important;\n}\n\nhtml.dark .sp-devtools > div {\n  --color-text: var(--sp-colors-clickable) !important;\n  --color-modal-background: #16181de0 !important;\n}\n\n.sandpack .sp-devtools table td {\n  border: 1px solid var(--sp-colors-surface2);\n}\n\n/**\n * Hard fixes\n */\n\n/**\n  * The text-size-adjust CSS property controls the text inflation\n  * algorithm used on some smartphones and tablets\n  */\n.sandpack .sp-cm {\n  -webkit-text-size-adjust: none;\n}\n\n/**\n * For iOS: prevent browser zoom when clicking on sandbox.\n * Does NOT apply to code blocks.\n */\n@media screen and (max-width: 768px) {\n  @supports (-webkit-overflow-scrolling: touch) {\n    .sandpack--playground .cm-content,\n    .sandpack--playground .sp-code-editor .sp-pre-placeholder {\n      font-size: initial;\n    }\n    .DocSearch-Input {\n      font-size: initial;\n    }\n  }\n}\n\n.sp-loading .sp-icon-standalone span {\n  display: none;\n}\n"
  },
  {
    "path": "src/types/docsearch-react-modal.d.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\n// This module must be declared and because the dynamic import in\n// \"src/components/Search.tsx\" is not able to resolve the types from\n// the package.\ndeclare module '@docsearch/react/modal' {\n  // re-exports the types from @docsearch/react/dist/esm/index.d.ts\n  export * from '@docsearch/react/dist/esm/index';\n}\n"
  },
  {
    "path": "src/utils/analytics.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nlet buffer: Array<any> = [];\nlet galite: null | Function = null;\nlet galitePromise: null | Promise<any> = null;\n\nexport function ga(...args: any[]): void {\n  if (typeof galite === 'function') {\n    galite.apply(null, args);\n    return;\n  }\n  buffer.push(args);\n  if (!galitePromise) {\n    // @ts-ignore\n    galitePromise = import('ga-lite').then((mod) => {\n      galite = mod.default;\n      galitePromise = null;\n      buffer.forEach((args) => {\n        mod.default.apply(null, args);\n      });\n      buffer = [];\n    });\n  }\n}\n"
  },
  {
    "path": "src/utils/compileMDX.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nimport {LanguageItem} from 'components/MDX/LanguagesContext';\nimport {MDXComponents} from 'components/MDX/MDXComponents';\n\n// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n// ~~~~ IMPORTANT: BUMP THIS IF YOU CHANGE ANY CODE BELOW ~~~\nconst DISK_CACHE_BREAKER = 11;\n// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nexport default async function compileMDX(\n  mdx: string,\n  path: string | string[],\n  params: {[key: string]: any}\n): Promise<{content: string; toc: string; meta: any}> {\n  const fs = require('fs');\n  const {\n    prepareMDX,\n    PREPARE_MDX_CACHE_BREAKER,\n  } = require('../utils/prepareMDX');\n  const mdxComponentNames = Object.keys(MDXComponents);\n\n  // See if we have a cached output first.\n  const {FileStore, stableHash} = require('metro-cache');\n  const store = new FileStore({\n    root: process.cwd() + '/node_modules/.cache/react-docs-mdx/',\n  });\n  const hash = Buffer.from(\n    stableHash({\n      // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n      // ~~~~ IMPORTANT: Everything that the code below may rely on.\n      // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n      mdx,\n      ...params,\n      mdxComponentNames,\n      DISK_CACHE_BREAKER,\n      PREPARE_MDX_CACHE_BREAKER,\n      lockfile: fs.readFileSync(process.cwd() + '/yarn.lock', 'utf8'),\n    })\n  );\n  const cached = await store.get(hash);\n  if (cached) {\n    console.log(\n      'Reading compiled MDX for /' + path + ' from ./node_modules/.cache/'\n    );\n    return cached;\n  }\n  if (process.env.NODE_ENV === 'production') {\n    console.log(\n      'Cache miss for MDX for /' + path + ' from ./node_modules/.cache/'\n    );\n  }\n\n  // If we don't add these fake imports, the MDX compiler\n  // will insert a bunch of opaque components we can't introspect.\n  // This will break the prepareMDX() call below.\n  let mdxWithFakeImports =\n    mdx +\n    '\\n\\n' +\n    mdxComponentNames\n      .map((key) => 'import ' + key + ' from \"' + key + '\";\\n')\n      .join('\\n');\n\n  // Turn the MDX we just read into some JS we can execute.\n  const {remarkPlugins} = require('../../plugins/markdownToHtml');\n  const {compile: compileMdx} = await import('@mdx-js/mdx');\n  const visit = (await import('unist-util-visit')).default;\n  const jsxCode = await compileMdx(mdxWithFakeImports, {\n    remarkPlugins: [\n      ...remarkPlugins,\n      (await import('remark-gfm')).default,\n      (await import('remark-frontmatter')).default,\n    ],\n    rehypePlugins: [\n      // Support stuff like ```js App.js {1-5} active by passing it through.\n      function rehypeMetaAsAttributes() {\n        return (tree) => {\n          visit(tree, 'element', (node) => {\n            if (\n              // @ts-expect-error -- tagName is a valid property\n              node.tagName === 'code' &&\n              node.data &&\n              node.data.meta\n            ) {\n              // @ts-expect-error -- properties is a valid property\n              node.properties.meta = node.data.meta;\n            }\n          });\n        };\n      },\n    ],\n  });\n  const {transform} = require('@babel/core');\n  const jsCode = await transform(jsxCode, {\n    plugins: ['@babel/plugin-transform-modules-commonjs'],\n    presets: ['@babel/preset-react'],\n  }).code;\n\n  // Prepare environment for MDX.\n  let fakeExports = {};\n  const fakeRequire = (name: string) => {\n    if (name === 'react/jsx-runtime') {\n      return require('react/jsx-runtime');\n    } else {\n      // For each fake MDX import, give back the string component name.\n      // It will get serialized later.\n      return name;\n    }\n  };\n  const evalJSCode = new Function('require', 'exports', jsCode);\n  // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n  // THIS IS A BUILD-TIME EVAL. NEVER DO THIS WITH UNTRUSTED MDX (LIKE FROM CMS)!!!\n  // In this case it's okay because anyone who can edit our MDX can also edit this file.\n  evalJSCode(fakeRequire, fakeExports);\n  // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n  // @ts-expect-error -- default exports is existed after eval\n  const reactTree = fakeExports.default({});\n\n  // Pre-process MDX output and serialize it.\n  let {toc, children} = prepareMDX(reactTree.props.children);\n  if (path === 'index') {\n    toc = [];\n  }\n\n  // Parse Frontmatter headers from MDX.\n  const fm = require('gray-matter');\n  const meta = fm(mdx).data;\n\n  // Load the list of translated languages conditionally.\n  let languages: Array<LanguageItem> | null = null;\n  if (typeof path === 'string' && path.endsWith('/translations')) {\n    languages = await (\n      await fetch(\n        'https://raw.githubusercontent.com/reactjs/translations.react.dev/main/langs/langs.json'\n      )\n    ).json(); // { code: string; name: string; enName: string}[]\n  }\n\n  const output = {\n    content: JSON.stringify(children, stringifyNodeOnServer),\n    toc: JSON.stringify(toc, stringifyNodeOnServer),\n    meta,\n    languages,\n  };\n\n  // Serialize a server React tree node to JSON.\n  function stringifyNodeOnServer(key: unknown, val: any) {\n    if (\n      val != null &&\n      val.$$typeof === Symbol.for('react.transitional.element')\n    ) {\n      // Remove fake MDX props.\n      // eslint-disable-next-line @typescript-eslint/no-unused-vars\n      const {mdxType, originalType, parentName, ...cleanProps} = val.props;\n      return [\n        '$r',\n        typeof val.type === 'string' ? val.type : mdxType,\n        val.key,\n        cleanProps,\n      ];\n    } else {\n      return val;\n    }\n  }\n\n  // Cache it on the disk.\n  await store.set(hash, output);\n  return output;\n}\n"
  },
  {
    "path": "src/utils/finishedTranslations.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n// This is a list of languages with enough translated content.\n// Add more languages here when they have enough content.\n// Please DO NOT edit this list without a discussion in the reactjs/react.dev repo.\n// It must be the same between all translations.\n// This will also affect the 'Translations' article.\n\n// prettier-ignore\nexport const finishedTranslations = [\n  'en',\n  'zh-hans',\n  'es',\n  'fr',\n  'ja',\n  'tr',\n  'ko'\n];\n"
  },
  {
    "path": "src/utils/forwardRefWithAs.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\n/**\n * Copied from Reach UI utils...\n *\n * It fixes TypeScript type inferencing to work with <Comp as={AnotherComp} />\n */\n\nimport * as React from 'react';\nimport {ValidationMap} from 'prop-types';\n\n/**\n * React.Ref uses the readonly type `React.RefObject` instead of\n * `React.MutableRefObject`, We pretty much always assume ref objects are\n * mutable (at least when we create them), so this type is a workaround so some\n * of the weird mechanics of using refs with TS.\n */\nexport type AssignableRef<ValueType> =\n  | {\n      bivarianceHack(instance: ValueType | null): void;\n    }['bivarianceHack']\n  | React.MutableRefObject<ValueType | null>\n  | null;\n\n////////////////////////////////////////////////////////////////////////////////\n// The following types help us deal with the `as` prop.\n// I kind of hacked around until I got this to work using some other projects,\n// as a rough guide, but it does seem to work so, err, that's cool? Yay TS! 🙃\n// P = additional props\n// T = type of component to render\n\nexport type As<BaseProps = any> = React.ElementType<BaseProps>;\n\nexport type PropsWithAs<\n  ComponentType extends As,\n  ComponentProps\n> = ComponentProps &\n  Omit<\n    React.ComponentPropsWithRef<ComponentType>,\n    'as' | keyof ComponentProps\n  > & {\n    as?: ComponentType;\n  };\n\nexport type PropsFromAs<\n  ComponentType extends As,\n  ComponentProps\n> = (PropsWithAs<ComponentType, ComponentProps> & {as: ComponentType}) &\n  PropsWithAs<ComponentType, ComponentProps>;\n\nexport type ComponentWithForwardedRef<\n  ElementType extends React.ElementType,\n  ComponentProps\n> = React.ForwardRefExoticComponent<\n  ComponentProps &\n    React.HTMLProps<React.ElementType<ElementType>> &\n    React.ComponentPropsWithRef<ElementType>\n>;\n\nexport interface ComponentWithAs<ComponentType extends As, ComponentProps> {\n  // These types are a bit of a hack, but cover us in cases where the `as` prop\n  // is not a JSX string type. Makes the compiler happy so 🤷‍♂️\n  <TT extends As>(\n    props: PropsWithAs<TT, ComponentProps>\n  ): React.ReactElement | null;\n  (\n    props: PropsWithAs<ComponentType, ComponentProps>\n  ): React.ReactElement | null;\n\n  displayName?: string;\n  propTypes?: ValidationMap<React.ReactNode>;\n  contextTypes?: ValidationMap<React.ReactNode>;\n  defaultProps?: Partial<PropsWithAs<ComponentType, ComponentProps>>;\n}\n\n/**\n * This is a hack for sure. The thing is, getting a component to intelligently\n * infer props based on a component or JSX string passed into an `as` prop is\n * kind of a huge pain. Getting it to work and satisfy the constraints of\n * `forwardRef` seems dang near impossible. To avoid needing to do this awkward\n * type song-and-dance every time we want to forward a ref into a component\n * that accepts an `as` prop, we abstract all of that mess to this function for\n * the time time being.\n *\n * TODO: Eventually we should probably just try to get the type defs above\n * working across the board, but ain't nobody got time for that mess!\n *\n * @param Comp\n */\nexport function forwardRefWithAs<Props, ComponentType extends As>(\n  comp: (\n    props: PropsFromAs<ComponentType, Props>,\n    ref: React.RefObject<any>\n  ) => React.ReactElement | null\n) {\n  return React.forwardRef(comp as any) as unknown as ComponentWithAs<\n    ComponentType,\n    Props\n  >;\n}\n\n/*\nTest components to make sure our dynamic As prop components work as intended\ntype PopupProps = {\n  lol: string;\n  children?: React.ReactNode | ((value?: number) => JSX.Element);\n};\nexport const Popup = forwardRefWithAs<PopupProps, 'input'>(\n  ({ as: Comp = 'input', lol, className, children, ...props }, ref) => {\n    return (\n      <Comp ref={ref} {...props}>\n        {typeof children === 'function' ? children(56) : children}\n      </Comp>\n    );\n  }\n);\nexport const TryMe1: React.FC = () => {\n  return <Popup as=\"input\" lol=\"lol\" name=\"me\" />;\n};\nexport const TryMe2: React.FC = () => {\n  let ref = React.useRef(null);\n  return <Popup ref={ref} as=\"div\" lol=\"lol\" />;\n};\n\nexport const TryMe4: React.FC = () => {\n  return <Popup as={Whoa} lol=\"lol\" test=\"123\" name=\"boop\" />;\n};\nexport const Whoa: React.FC<{\n  help?: boolean;\n  lol: string;\n  name: string;\n  test: string;\n}> = props => {\n  return <input {...props} />;\n};\n*/\n// export const TryMe3: React.FC = () => {\n//   return <Popup as={Cool} lol=\"lol\" name=\"me\" test=\"123\" />;\n// };\n// let Cool = styled(Whoa)`\n//   padding: 10px;\n// `\n"
  },
  {
    "path": "src/utils/prepareMDX.js",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport {Children} from 'react';\n\n// TODO: This logic could be in MDX plugins instead.\n\n// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\nexport const PREPARE_MDX_CACHE_BREAKER = 3;\n// !!! IMPORTANT !!! Bump this if you change any logic.\n// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nexport function prepareMDX(rawChildren) {\n  const toc = getTableOfContents(rawChildren, /* depth */ 10);\n  const children = wrapChildrenInMaxWidthContainers(rawChildren);\n  return {toc, children};\n}\n\nfunction wrapChildrenInMaxWidthContainers(children) {\n  // Auto-wrap everything except a few types into\n  // <MaxWidth> wrappers. Keep reusing the same\n  // wrapper as long as we can until we meet\n  // a full-width section which interrupts it.\n  let fullWidthTypes = [\n    'Sandpack',\n    'FullWidth',\n    'Illustration',\n    'IllustrationBlock',\n    'Challenges',\n    'Recipes',\n  ];\n  let wrapQueue = [];\n  let finalChildren = [];\n  function flushWrapper(key) {\n    if (wrapQueue.length > 0) {\n      const Wrapper = 'MaxWidth';\n      finalChildren.push(<Wrapper key={key}>{wrapQueue}</Wrapper>);\n      wrapQueue = [];\n    }\n  }\n  function handleChild(child, key) {\n    if (child == null) {\n      return;\n    }\n    if (typeof child !== 'object') {\n      wrapQueue.push(child);\n      return;\n    }\n    if (fullWidthTypes.includes(child.type)) {\n      flushWrapper(key);\n      finalChildren.push(child);\n    } else {\n      wrapQueue.push(child);\n    }\n  }\n  Children.forEach(children, handleChild);\n  flushWrapper('last');\n  return finalChildren;\n}\n\nfunction getTableOfContents(children, depth) {\n  const anchors = [];\n  extractHeaders(children, depth, anchors);\n  if (anchors.length > 0) {\n    anchors.unshift({\n      url: '#',\n      text: '훑어보기',\n      depth: 2,\n    });\n  }\n  return anchors;\n}\n\nconst headerTypes = new Set([\n  'h1',\n  'h2',\n  'h3',\n  'Challenges',\n  'Recap',\n  'TeamMember',\n]);\nfunction extractHeaders(children, depth, out) {\n  for (const child of Children.toArray(children)) {\n    if (child.type && headerTypes.has(child.type)) {\n      let header;\n      if (child.type === 'Challenges') {\n        header = {\n          url: '#challenges',\n          depth: 2,\n          text: '챌린지 도전하기',\n        };\n      } else if (child.type === 'Recap') {\n        header = {\n          url: '#recap',\n          depth: 2,\n          text: '요약',\n        };\n      } else if (child.type === 'TeamMember') {\n        header = {\n          url: '#' + child.props.permalink,\n          depth: 3,\n          text: child.props.name,\n        };\n      } else {\n        header = {\n          url: '#' + child.props.id,\n          depth: (child.type && parseInt(child.type.replace('h', ''), 0)) ?? 0,\n          text: child.props.children,\n        };\n      }\n      out.push(header);\n    } else if (child.children && depth > 0) {\n      extractHeaders(child.children, depth - 1, out);\n    }\n  }\n}\n"
  },
  {
    "path": "src/utils/processShim.js",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\n// Used in next.config.js to remove the process transitive dependency.\nmodule.exports = {\n  env: {},\n  cwd() {},\n};\n"
  },
  {
    "path": "src/utils/rafShim.js",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\n// Used in next.config.js to remove the raf transitive dependency.\nexport default window.requestAnimationFrame;\n"
  },
  {
    "path": "src/utils/rss.js",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\nconst Feed = require('rss');\nconst fs = require('fs');\nconst path = require('path');\nconst matter = require('gray-matter');\n\nconst getAllFiles = function (dirPath, arrayOfFiles) {\n  const files = fs.readdirSync(dirPath);\n\n  arrayOfFiles = arrayOfFiles || [];\n\n  files.forEach(function (file) {\n    if (fs.statSync(dirPath + '/' + file).isDirectory()) {\n      arrayOfFiles = getAllFiles(dirPath + '/' + file, arrayOfFiles);\n    } else {\n      arrayOfFiles.push(path.join(dirPath, '/', file));\n    }\n  });\n\n  return arrayOfFiles;\n};\n\nexports.generateRssFeed = function () {\n  const feed = new Feed({\n    title: 'React Blog',\n    description:\n      'This blog is the official source for the updates from the React team. Anything important, including release notes or deprecation notices, will be posted here first.',\n    feed_url: 'https://react.dev/rss.xml',\n    site_url: 'https://react.dev/',\n    language: 'en',\n    favicon: 'https://react.dev/favicon.ico',\n    pubDate: new Date(),\n    generator: 'react.dev rss module',\n  });\n\n  const dirPath = path.join(process.cwd(), 'src/content/blog');\n  const filesByOldest = getAllFiles(dirPath);\n  const files = filesByOldest.reverse();\n\n  for (const filePath of files) {\n    const id = path.basename(filePath);\n    if (id !== 'index.md') {\n      const content = fs.readFileSync(filePath, 'utf-8');\n      const {data} = matter(content);\n      const slug = filePath.split('/').slice(-4).join('/').replace('.md', '');\n\n      if (data.title == null || data.title.trim() === '') {\n        throw new Error(\n          `${id}: Blog posts must include a title in the metadata, for RSS feeds`\n        );\n      }\n      if (data.author == null || data.author.trim() === '') {\n        throw new Error(\n          `${id}: Blog posts must include an author in the metadata, for RSS feeds`\n        );\n      }\n      if (data.date == null || data.date.trim() === '') {\n        throw new Error(\n          `${id}: Blog posts must include a date in the metadata, for RSS feeds`\n        );\n      }\n      if (data.description == null || data.description.trim() === '') {\n        throw new Error(\n          `${id}: Blog posts must include a description in the metadata, for RSS feeds`\n        );\n      }\n\n      feed.item({\n        id,\n        title: data.title,\n        author: data.author || '',\n        date: new Date(data.date),\n        url: `https://react.dev/blog/${slug}`,\n        description: data.description,\n      });\n    }\n  }\n\n  fs.writeFileSync('./public/rss.xml', feed.xml({indent: true}));\n};\n"
  },
  {
    "path": "src/utils/toCommaSeparatedList.tsx",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nimport * as React from 'react';\n\nconst addString = (list: React.ReactNode[], string: string) =>\n  list.push(<span key={`${list.length}-${string}`}>{string}</span>);\n\nfunction toCommaSeparatedList<Item>(\n  array: Item[],\n  renderCallback: (item: Item, index: number) => React.ReactNode\n): React.ReactNode[] {\n  if (array.length <= 1) {\n    return array.map(renderCallback);\n  }\n\n  const list: React.ReactNode[] = [];\n\n  array.forEach((item, index) => {\n    if (index === array.length - 1) {\n      addString(list, array.length === 2 ? ' and ' : ', and ');\n      list.push(renderCallback(item, index));\n    } else if (index > 0) {\n      addString(list, ', ');\n      list.push(renderCallback(item, index));\n    } else {\n      list.push(renderCallback(item, index));\n    }\n  });\n\n  return list;\n}\n\nexport default toCommaSeparatedList;\n"
  },
  {
    "path": "tailwind.config.js",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n/*\n * Copyright (c) Facebook, Inc. and its affiliates.\n */\n\nconst defaultTheme = require('tailwindcss/defaultTheme');\nconst colors = require('./colors');\n\nmodule.exports = {\n  content: [\n    './src/components/**/*.{js,ts,jsx,tsx}',\n    './src/pages/**/*.{js,ts,jsx,tsx}',\n    './src/styles/**/*.{js,ts,jsx,tsx}',\n  ],\n  darkMode: 'class',\n  theme: {\n    // Override base screen sizes\n    screens: {\n      ...defaultTheme.screens,\n      betterhover: {raw: '(hover: hover)'},\n      xs: '374px',\n      '3xl': '1919px',\n    },\n    boxShadow: {\n      sm: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',\n      DEFAULT:\n        '0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)',\n      md: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',\n      lg: '0px 0.8px 2px rgba(0, 0, 0, 0.032), 0px 2.7px 6.7px rgba(0, 0, 0, 0.048), 0px 12px 30px rgba(0, 0, 0, 0.08)',\n      'lg-dark':\n        '0 0 0 1px rgba(255,255,255,.15), 0px 0.8px 2px rgba(0, 0, 0, 0.032), 0px 2.7px 6.7px rgba(0, 0, 0, 0.048), 0px 12px 30px rgba(0, 0, 0, 0.08)',\n      xl: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)',\n      '2xl': '0 25px 50px -12px rgba(0, 0, 0, 0.25)',\n      '3xl': '0 35px 60px -15px rgba(0, 0, 0, 0.3)',\n      nav: '0 16px 32px -16px rgba(0, 0, 0, 0.1), 0 0 0 1px rgba(0,0,0,.10)',\n      'nav-dark':\n        '0 16px 32px -16px rgba(0, 0, 0, 0.1), 0 0 0 1px rgba(255,255,255,.05)',\n      inner: 'inset 0 1px 4px 0 rgba(0, 0, 0, 0.05)',\n      'inner-border': 'inset 0 0 0 1px rgba(0, 0, 0, 0.08)',\n      'inner-border-dark': 'inset 0 0 0 1px rgba(255, 255, 255, 0.08)',\n      'outer-border': '0 0 0 1px rgba(0, 0, 0, 0.1)',\n      'outer-border-dark': '0 0 0 1px rgba(255, 255, 255, 0.1)',\n      'secondary-button-stroke': 'inset 0 0 0 1px #D9DBE3',\n      'secondary-button-stroke-dark': 'inset 0 0 0 1px #404756',\n      none: 'none',\n    },\n    extend: {\n      backgroundImage: {\n        'gradient-left-dark':\n          'conic-gradient(from 90deg at -10% 100%, #2B303B 0deg, #2B303B 90deg, #16181D 360deg)',\n        'gradient-right-dark':\n          'conic-gradient(from -90deg at 110% 100%, #2B303B 0deg, #16181D 90deg, #16181D 360deg)',\n        'gradient-left':\n          'conic-gradient(from 90deg at -10% 100%, #BCC1CD 0deg, #BCC1CD 90deg, #FFFFFF 360deg)',\n        'gradient-right':\n          'conic-gradient(from -90deg at 110% 100%, #FFFFFF 0deg, #EBECF0 90deg, #EBECF0 360deg)',\n        'meta-gradient': \"url('/images/meta-gradient.png')\",\n        'meta-gradient-dark': \"url('/images/meta-gradient-dark.png')\",\n      },\n      maxWidth: {\n        ...defaultTheme.maxWidth,\n        'custom-xs': '21rem',\n      },\n      outline: {\n        blue: ['1px auto ' + colors.link, '3px'],\n      },\n      opacity: {\n        8: '0.08',\n      },\n      fontFamily: {\n        display: [\n          'Optimistic Display',\n          '-apple-system',\n          ...defaultTheme.fontFamily.sans,\n        ],\n        text: [\n          'Optimistic Text',\n          '-apple-system',\n          ...defaultTheme.fontFamily.sans,\n        ],\n        mono: ['\"Source Code Pro\"', ...defaultTheme.fontFamily.mono],\n      },\n      lineHeight: {\n        base: '30px',\n        large: '38px',\n        xl: '1.15',\n      },\n      fontSize: {\n        '6xl': '52px',\n        '5xl': '40px',\n        '4xl': '32px',\n        '3xl': '28px',\n        '2xl': '24px',\n        xl: '20px',\n        lg: '17px',\n        base: '15px',\n        sm: '13px',\n        xs: '11px',\n        code: 'calc(1em - 20%)',\n      },\n      animation: {\n        marquee: 'marquee 40s linear infinite',\n        marquee2: 'marquee2 40s linear infinite',\n        'large-marquee': 'large-marquee 80s linear infinite',\n        'large-marquee2': 'large-marquee2 80s linear infinite',\n        'fade-up': 'fade-up 1s 100ms both',\n      },\n      keyframes: {\n        shimmer: {\n          '100%': {\n            transform: 'translateX(100%)',\n          },\n        },\n        rotate: {\n          from: {transform: 'rotate(0deg)'},\n          to: {transform: 'rotate(180deg)'},\n        },\n        scale: {\n          from: {transform: 'scale(0.8)'},\n          '90%': {transform: 'scale(1.05)'},\n          to: {transform: 'scale(1)'},\n        },\n        circle: {\n          from: {transform: 'scale(0)', strokeWidth: '16px'},\n          '50%': {transform: 'scale(0.5)', strokeWidth: '16px'},\n          to: {transform: 'scale(1)', strokeWidth: '0px'},\n        },\n        marquee: {\n          '0%': {transform: 'translateX(0%)'},\n          '100%': {transform: 'translateX(-400%)'},\n        },\n        marquee2: {\n          '0%': {transform: 'translateX(400%)'},\n          '100%': {transform: 'translateX(0%)'},\n        },\n        'large-marquee': {\n          '0%': {transform: 'translateX(0%)'},\n          '100%': {transform: 'translateX(-200%)'},\n        },\n        'large-marquee2': {\n          '0%': {transform: 'translateX(200%)'},\n          '100%': {transform: 'translateX(0%)'},\n        },\n        'fade-up': {\n          '0%': {\n            opacity: '0',\n            transform: 'translateY(2rem)',\n          },\n          '100%': {\n            opacity: '1',\n            transform: 'translateY(0)',\n          },\n        },\n      },\n      colors,\n      gridTemplateColumns: {\n        'only-content': 'auto',\n        'sidebar-content': '20rem auto',\n        'sidebar-content-toc': '20rem auto 20rem',\n      },\n    },\n  },\n  plugins: [],\n};\n"
  },
  {
    "path": "textlint/data/rules/translateGlossary.js",
    "content": "// `sources`에 속한 단어들은 특수한 경우를 제외하고는 기본적으로 '원자성'을 유지해야 합니다. ex) 'stateless component'(x) -> 'stateless'(O), 'component'(O)\n// 단, `-`(dash)로 이어진 단어 ex) 'full-stack'은 한개의 단어로 취급합니다.\nmodule.exports = {\n  translated: {\n    react: [\n      {\n        sources: [/\\bTutorial\\b/, /[듀튜]토리얼/],\n        target: '자습서',\n        meta: {\n          term: 'Tutorial',\n          discussions: [2],\n          note: '',\n        },\n      },\n      {\n        sources: [/\\bDeclarative\\b/],\n        target: '선언적인',\n        meta: {\n          term: 'Declarative',\n          discussions: [2],\n          note: '',\n        },\n      },\n      {\n        sources: [/\\bComponent\\b/, /컴퍼넌트/, /컴포넌츠/],\n        target: '컴포넌트',\n        meta: {\n          term: 'Component',\n          discussions: [2],\n          note: '',\n        },\n      },\n      {\n        sources: [/\\bStateful\\b/],\n        target: '유상태',\n        meta: {\n          term: 'Stateful',\n          discussions: [2],\n          note: '',\n        },\n      },\n      {\n        sources: [/\\bStateless\\b/],\n        target: '무상태',\n        meta: {\n          term: 'Stateless',\n          discussions: [2],\n          note: '',\n        },\n      },\n      {\n        sources: [\n          /\\bRender(?!er)(?:ing)?\\b/,\n          /랜더링/,\n          /[렌랜]더(?!링)\\s?[하한할함합]/,\n        ],\n        target: '렌더링(하다)',\n        meta: {\n          term: 'Render',\n          discussions: [2],\n          note: '',\n        },\n      },\n      {\n        sources: [/\\bData\\b/, /대이터/],\n        target: '데이터',\n        meta: {\n          term: 'Data',\n          discussions: [2],\n          note: '',\n        },\n      },\n      {\n        sources: [/\\bApplication\\b/, /어플리케이[선션]/, /응용\\s?프로그램/],\n        target: '애플리케이션',\n        meta: {\n          term: 'Application',\n          discussions: [2],\n          note: '',\n        },\n      },\n      {\n        sources: [/\\bExternal\\b/],\n        target: '외부',\n        meta: {\n          term: 'External',\n          discussions: [2],\n          note: '',\n        },\n      },\n      {\n        sources: [/\\bPlugin\\b/],\n        target: '플러그인',\n        meta: {\n          term: 'Plugin',\n          discussions: [2],\n          note: '',\n        },\n      },\n      {\n        sources: [/\\bThird\\b/, /써드/],\n        target: '서드',\n        meta: {\n          term: 'Third',\n          discussions: [2],\n          note: '',\n        },\n      },\n      {\n        sources: [/\\bSyntax\\b/, /[신씬]택스/],\n        target: '문법',\n        meta: {\n          term: 'Syntax',\n          discussions: [2],\n          note: '',\n        },\n      },\n      {\n        sources: [/\\bEmbedding\\s?Expression\\b/],\n        target: '표현식 포함하기',\n        meta: {\n          term: 'Embedding Expression',\n          discussions: [2],\n          note: '',\n        },\n      },\n      {\n        sources: [/\\bAttribute\\b/, /애트리뷰트/],\n        target: '어트리뷰트',\n        meta: {\n          term: 'Attribute',\n          discussions: [2],\n          note: '',\n        },\n      },\n      {\n        sources: [/\\bElement\\b/, /[엘앨]리먼츠/, /앨리먼트/],\n        target: '엘리먼트',\n        meta: {\n          term: 'Element',\n          discussions: [2],\n          note: '',\n        },\n      },\n      {\n        sources: [/\\bFunction(?:al)?\\b/],\n        target: '함수',\n        meta: {\n          term: 'Function',\n          discussions: [2],\n          note: '',\n        },\n      },\n      {\n        sources: [/\\bClass\\b/],\n        target: '클래스',\n        meta: {\n          term: 'Class',\n          discussions: [2],\n          note: '',\n        },\n      },\n      {\n        sources: [/\\bComposition\\b/, /[컴콤][퍼포]지[선션]/],\n        target: '합성',\n        meta: {\n          term: 'Composition',\n          discussions: [2],\n          note: '',\n        },\n      },\n      {\n        sources: [/\\bInheritance\\b/],\n        target: '상속',\n        meta: {\n          term: 'Inheritance',\n          discussions: [2],\n          note: '',\n        },\n      },\n      {\n        sources: [/\\bLife\\s?Cycle\\b/, /라이프\\s?사이클/, /생명 주기/],\n        target: '생명주기',\n        meta: {\n          term: 'Lifecycle',\n          discussions: [2],\n          note: '',\n        },\n      },\n      {\n        sources: [/\\bHandling\\b/, /핸들링/],\n        target: '처리',\n        meta: {\n          term: 'Handling',\n          discussions: [2],\n          note: '',\n        },\n      },\n      {\n        sources: [/\\bConditional\\b/, /컨디[서셔][날널]/],\n        target: '조건부',\n        meta: {\n          term: 'Conditional',\n          discussions: [2],\n          note: '',\n        },\n      },\n      {\n        sources: [/\\bOperator\\b/, /오퍼[레래]이터/],\n        target: '연산자',\n        meta: {\n          term: 'Operator',\n          discussions: [2],\n          note: '',\n        },\n      },\n      {\n        sources: [/\\bReuse\\b/],\n        target: '재사용',\n        meta: {\n          term: 'Reuse',\n          discussions: [2],\n          note: '',\n        },\n      },\n      {\n        sources: [/\\bMock\\b/],\n        target: '모의',\n        meta: {\n          term: 'Mock',\n          discussions: [2],\n          note: '',\n        },\n      },\n      {\n        sources: [/\\bCallback\\b/],\n        target: '콜백',\n        meta: {\n          term: 'Callback',\n          discussions: [2],\n          note: '',\n        },\n      },\n      {\n        sources: [/\\bSynthetic\\b/],\n        target: '합성',\n        meta: {\n          term: 'Synthetic',\n          discussions: [2],\n          note: '',\n        },\n      },\n      {\n        sources: [/\\bEvent\\b/],\n        target: '이벤트',\n        meta: {\n          term: 'Event',\n          discussions: [2],\n          note: '',\n        },\n      },\n      {\n        sources: [/\\bHigher\\s?Order\\b/],\n        target: '고차',\n        meta: {\n          term: 'Higher Order',\n          discussions: [2],\n          note: '',\n        },\n      },\n      {\n        sources: [/\\b(?<!Un)Mount\\b/],\n        target: '마운트',\n        meta: {\n          term: 'Mount',\n          discussions: [2],\n          note: '',\n        },\n      },\n      {\n        sources: [/\\bUnmount\\b/, /언마운트/],\n        target: '마운트 해제',\n        meta: {\n          term: 'Unmount',\n          discussions: [2],\n          note: '',\n        },\n      },\n      {\n        sources: [/\\bForm\\b/],\n        target: '폼',\n        meta: {\n          term: 'Form',\n          discussions: [2],\n          note: '',\n        },\n      },\n      {\n        sources: [/\\bWrapper\\b/],\n        target: '래퍼',\n        meta: {\n          term: 'Wrapper',\n          discussions: [2],\n          note: '',\n        },\n      },\n      {\n        sources: [/\\bChild(?:ren)?\\b/],\n        target: '자식',\n        meta: {\n          term: 'Children',\n          discussions: [2],\n          note: '',\n        },\n      },\n      {\n        sources: [/\\bCode[-\\s]?Splitting\\b/],\n        target: '코드 분할',\n        meta: {\n          term: 'Code-Splitting',\n          discussions: [2],\n          note: '',\n        },\n      },\n      {\n        sources: [/\\bReconciliation\\b/],\n        target: '재조정',\n        meta: {\n          term: 'Reconciliation',\n          discussions: [2],\n          note: '',\n        },\n      },\n      {\n        sources: [/\\bPropert(?:y|ies)\\b/],\n        target: '프로퍼티',\n        meta: {\n          term: 'Property',\n          discussions: [2],\n          note: '',\n        },\n      },\n      {\n        sources: [/\\bReference\\b/, /래퍼런스/],\n        target: '레퍼런스',\n        meta: {\n          term: 'Reference',\n          discussions: [569],\n          note: '',\n        },\n      },\n      {\n        sources: [/\\bUser\\b/, /유저/],\n        target: '사용자',\n        meta: {\n          term: 'User',\n          discussions: [569],\n          note: '',\n        },\n      },\n      {\n        sources: [/\\bInterface\\b/],\n        target: '인터페이스',\n        meta: {\n          term: 'Interface',\n          discussions: [569],\n          note: '',\n        },\n      },\n      {\n        sources: [/\\bMarkup\\b/, /마크 업/],\n        target: '마크업',\n        meta: {\n          term: 'Markup',\n          discussions: [569],\n          note: '',\n        },\n      },\n      {\n        sources: [/\\bInteracti(?:vity|on)\\b/, /인터[랙렉][선션]/],\n        target: '상호작용',\n        meta: {\n          term: 'Interactivity',\n          discussions: [569],\n          note: '',\n        },\n      },\n      {\n        sources: [/\\bArchitecture\\b/, /아키택처/, /아키[택텍]쳐/],\n        target: '아키텍처',\n        meta: {\n          term: 'Architecture',\n          discussions: [569],\n          note: '',\n        },\n      },\n      {\n        sources: [/\\bFull[-\\s]?Stack\\b/],\n        target: '풀스택',\n        meta: {\n          term: 'Full-Stack',\n          discussions: [569],\n          note: '',\n        },\n      },\n      {\n        sources: [/\\bBrowser\\b/],\n        target: '브라우저',\n        meta: {\n          term: 'Browser',\n          discussions: [610],\n          note: '',\n        },\n      },\n      {\n        sources: [/\\bExtension\\b/, /확장프로그램/],\n        target: '확장 프로그램',\n        meta: {\n          term: 'Extension',\n          discussions: [610],\n          note: '',\n        },\n      },\n      {\n        sources: [/\\bEscape[-\\s]?Hatches\\b/],\n        target: '탈출구',\n        meta: {\n          term: 'Escape Hatches',\n          discussions: [738],\n          note: '',\n        },\n      },\n      {\n        sources: [/\\bBundles?\\b/],\n        target: '번들',\n        meta: {\n          term: 'Bundle',\n          discussions: [829],\n          note: '',\n        },\n      },\n      {\n        sources: [/\\bBundlers?\\b/],\n        target: '번들러',\n        meta: {\n          term: 'Bundler',\n          discussions: [829],\n          note: '',\n        },\n      },\n      {\n        sources: [/\\bBundling\\b/],\n        target: '번들링',\n        meta: {\n          term: 'Bundling',\n          discussions: [829],\n          note: '',\n        },\n      },\n      {\n        sources: [/\\bCompiler\\b/],\n        target: '컴파일러',\n        meta: {\n          term: 'Compiler',\n          discussions: [1400],\n          note: '',\n        },\n      },\n    ],\n    others: [\n      {\n        sources: [/\\bTips?\\b/],\n        target: '팁',\n        meta: {\n          term: 'Tip',\n          discussions: [2],\n          note: '',\n        },\n      },\n      {\n        sources: [/\\bExamples?\\b/, /예제/],\n        target: '예시',\n        meta: {\n          term: 'Example',\n          discussions: [2],\n          note: '',\n        },\n      },\n      {\n        sources: [/\\bChapters?\\b/, /[챕쳅]터/],\n        target: '장',\n        meta: {\n          term: 'Chapter',\n          discussions: [2],\n          note: '',\n        },\n      },\n      {\n        sources: [/\\bSpec(?:ification)?s?\\b/, /스[펙팩]/],\n        target: '명세',\n        meta: {\n          term: 'Specification',\n          discussions: [2],\n          note: 'Spec도 동일하게 번역',\n        },\n      },\n      {\n        sources: [/\\bcamel\\s?Case\\b/, /[캐카][맬멜]\\s?케이스/],\n        target: '캐멀 케이스',\n        meta: {\n          term: 'camelCase',\n          discussions: [2],\n          note: '',\n        },\n      },\n      {\n        sources: [/\\bParam(?:eter)?s?\\b/, /[파패][라러]미터/, /매개 변수/],\n        target: '매개변수',\n        meta: {\n          term: 'Parameter',\n          discussions: [614],\n          note: '',\n        },\n      },\n      {\n        sources: [/\\bDeprecated\\b/],\n        target: '더 이상 사용되지 않습니다.',\n        meta: {\n          term: 'Deprecated',\n          discussions: [632],\n          note: '',\n        },\n      },\n      {\n        sources: [/\\bPitfall\\b/],\n        target: '주의하세요!',\n        meta: {\n          term: 'Pitfall',\n          discussions: [632],\n          note: '',\n        },\n      },\n      {\n        sources: [/\\bNote\\b/],\n        target: '중요합니다!',\n        meta: {\n          term: 'Note',\n          discussions: [632],\n          note: '',\n        },\n      },\n      {\n        sources: [/\\bWip\\b/],\n        target: '개발중이에요',\n        meta: {\n          term: 'Wip',\n          discussions: [632],\n          note: '',\n        },\n      },\n      {\n        sources: [/\\bReturns\\b/, /반환\\s+(?:값\\s+)?{\\//],\n        target: '반환값',\n        meta: {\n          term: 'Returns',\n          discussions: [725],\n          note: '제목에 사용된 경우',\n        },\n      },\n      {\n        sources: [/\\bCaveats?\\b/, /주의사항/],\n        target: '주의 사항',\n        meta: {\n          term: 'Caveats',\n          discussions: [1095],\n          note: '',\n        },\n      },\n      {\n        sources: [/\\bLogic\\b/],\n        target: '로직',\n        meta: {\n          term: 'Logic',\n          discussions: [695],\n          note: '',\n        },\n      },\n      {\n        sources: [/\\bDependenc(?:y|ies)\\b/],\n        target: '의존성',\n        meta: {\n          term: 'Dependency',\n          discussions: [841],\n          note: '',\n        },\n      },\n      {\n        sources: [/\\bDirectives?\\b/],\n        target: '지시어',\n        meta: {\n          term: 'Directive',\n          discussions: [819],\n          note: '',\n        },\n      },\n      {\n        sources: [/\\bUsage\\b/],\n        target: '사용법',\n        meta: {\n          term: 'Usage',\n          discussions: [1425],\n          note: '',\n        },\n      },\n      {\n        sources: [/\\bImperative\\b/],\n        target: '명령형',\n        meta: {\n          term: 'Imperative',\n          discussions: [1425],\n          note: '',\n        },\n      },\n    ],\n  },\n  // untranslated: {\n  //   react: [],\n  //   others: [],\n  // },\n};\n"
  },
  {
    "path": "textlint/data/utils/errMsg.spec.js",
    "content": "module.exports = {\n  errMsgTranslateGlossary: [\n    // source: Nothing, target: Nothing\n    {\n      actual: {\n        source: '',\n        target: '',\n      },\n      expected: `''은/는 ''(으)로 번역되어야 합니다.`,\n    },\n    // source: Nothing, target: Something\n    {\n      actual: {\n        source: '',\n        target: '무언가',\n      },\n      expected: `''은/는 '무언가'(으)로 번역되어야 합니다.`,\n    },\n    // source: Something, target: Nothing\n    {\n      actual: {\n        source: 'Something',\n        target: '',\n      },\n      expected: `'Something'은/는 ''(으)로 번역되어야 합니다.`,\n    },\n    // source: Something, target: Something\n    {\n      actual: {\n        source: 'Something',\n        target: '무언가',\n      },\n      expected: `'Something'은/는 '무언가'(으)로 번역되어야 합니다.`,\n    },\n  ],\n};\n"
  },
  {
    "path": "textlint/data/utils/is.spec.js",
    "content": "module.exports = {\n  isKoreanIncluded: [\n    // Should return true for string containing Korean characters\n    {\n      actual: '안녕하세요',\n      expected: true,\n    },\n    {\n      actual: 'ㄱ',\n      expected: true,\n    },\n    {\n      actual: 'ㅏ',\n      expected: true,\n    },\n    {\n      actual: 'Hello 안녕하세요',\n      expected: true,\n    },\n    {\n      actual: '123 안녕하세요',\n      expected: true,\n    },\n    {\n      actual: 'Hello 123 !@#$%^&*() 한글 こんにちは 你好     ',\n      expected: true,\n    },\n    // Should return false for string not containing Korean characters\n    {\n      actual: 'Hello', // English\n      expected: false,\n    },\n    {\n      actual: 'こんにちは', // Japanese\n      expected: false,\n    },\n    {\n      actual: '你好', // Chinese\n      expected: false,\n    },\n    {\n      actual: '123', // Number\n      expected: false,\n    },\n    {\n      actual: '!@#$%^&*()', // Special\n      expected: false,\n    },\n    // Should return false for empty string\n    {\n      actual: '',\n      expected: false,\n    },\n    // Should return false for string containing only spaces\n    {\n      actual: ' ',\n      expected: false,\n    },\n    {\n      actual: '     ',\n      expected: false,\n    },\n  ],\n};\n"
  },
  {
    "path": "textlint/data/utils/strip.spec.js",
    "content": "module.exports = {\n  stripDoubleQuotes: [\n    // Left: Nothing, Inner: Nothing, Right: Nothing\n    {\n      actual: '\"\"',\n      expected: '',\n    },\n    // Left: Nothing, Inner: Nothing, Right: Something\n    {\n      actual: '\"\" not-stripped-right',\n      expected: ' not-stripped-right',\n    },\n    // Left: Nothing, Inner: Something, Right: Nothing\n    {\n      actual: '\"stripped\"',\n      expected: '',\n    },\n    // Left: Nothing, Inner: Something, Right: Something\n    {\n      actual: '\"stripped\" not-stripped-right',\n      expected: ' not-stripped-right',\n    },\n    // Left: Something, Inner: Nothing, Right: Nothing\n    {\n      actual: 'not-stripped-left \"\"',\n      expected: 'not-stripped-left ',\n    },\n    // Left: Something, Inner: Nothing, Right: Something\n    {\n      actual: 'not-stripped-left \"\" not-stripped-right',\n      expected: 'not-stripped-left  not-stripped-right',\n    },\n    // Left: Something, Inner: Something, Right: Nothing\n    {\n      actual: 'not-stripped-left \"stripped\"',\n      expected: 'not-stripped-left ',\n    },\n    // Left: Something, Inner: Something, Right: Something\n    {\n      actual: 'not-stripped-left \"stripped\" not-stripped-right',\n      expected: 'not-stripped-left  not-stripped-right',\n    },\n    // With only one double quote\n    {\n      actual: 'this double quote \" should not be stripped',\n      expected: 'this double quote \" should not be stripped',\n    },\n  ],\n  stripParentheses: [\n    // Left: Nothing, Inner: Nothing, Right: Nothing\n    {\n      actual: '()',\n      expected: '',\n    },\n    // Left: Nothing, Inner: Nothing, Right: Something\n    {\n      actual: '() not-stripped-right',\n      expected: ' not-stripped-right',\n    },\n    // Left: Nothing, Inner: Something, Right: Nothing\n    {\n      actual: '(stripped)',\n      expected: '',\n    },\n    // Left: Nothing, Inner: Something, Right: Something\n    {\n      actual: '(stripped) not-stripped-right',\n      expected: ' not-stripped-right',\n    },\n    // Left: Something, Inner: Nothing, Right: Nothing\n    {\n      actual: 'not-stripped-left ()',\n      expected: 'not-stripped-left ',\n    },\n    // Left: Something, Inner: Nothing, Right: Something\n    {\n      actual: 'not-stripped-left () not-stripped-right',\n      expected: 'not-stripped-left  not-stripped-right',\n    },\n    // Left: Something, Inner: Something, Right: Nothing\n    {\n      actual: 'not-stripped-left (stripped)',\n      expected: 'not-stripped-left ',\n    },\n    // Left: Something, Inner: Something, Right: Something\n    {\n      actual: 'not-stripped-left (stripped) not-stripped-right',\n      expected: 'not-stripped-left  not-stripped-right',\n    },\n    // With only one left parentheses\n    {\n      actual: 'this left parentheses ( should not be stripped',\n      expected: 'this left parentheses ( should not be stripped',\n    },\n    // With only one right parentheses\n    {\n      actual: 'this right parentheses ) should not be stripped',\n      expected: 'this right parentheses ) should not be stripped',\n    },\n  ],\n};\n"
  },
  {
    "path": "textlint/generators/genTranslateGlossaryDocs.js",
    "content": "const fs = require('fs');\nconst path = require('path');\nconst data = require('../data/rules/translateGlossary');\n\nconst urlIssues = 'https://github.com/reactjs/ko.react.dev/issues/';\nconst pathExport = '../../wiki/translate-glossary.md';\n\nclass Markdown {\n  // Property\n  #content = '';\n  get content() {\n    return this.#content;\n  }\n\n  // Method-Constructor\n  constructor(text) {\n    this.h1(text);\n  }\n  // Method-Utils\n  #add(text) {\n    this.#content += text;\n  }\n  // Method-Markdown\n  h1(text) {\n    this.#add(`# ${text}\\n\\n`);\n  }\n  h2(text) {\n    this.#add(`## ${text}\\n\\n`);\n  }\n  h3(text) {\n    this.#add(`### ${text}\\n\\n`);\n  }\n  blockQuote(text) {\n    this.#add(`> ${text}\\n\\n`);\n  }\n  tableHeader(...headers) {\n    headers.forEach((header) => {\n      this.#add(header);\n      this.#add('|');\n    });\n    this.#add('\\n');\n    headers.forEach(() => {\n      this.#add('---');\n      this.#add('|');\n    });\n    this.#add('\\n');\n  }\n  tableBody(...bodies) {\n    bodies.forEach((body) => {\n      this.#add(body);\n      this.#add('|');\n    });\n    this.#add('\\n');\n  }\n  tableEnd() {\n    this.#add('\\n');\n  }\n}\n\nclass Utils {\n  static keyToStr(keyText) {\n    switch (keyText) {\n      case 'translated':\n        return '번역해야 하는 용어';\n      case 'untranslated':\n        return '번역하면 안되는 용어';\n      case 'react':\n        return 'React';\n      case 'others':\n        return 'Others';\n    }\n  }\n}\n\nconst genTranslateGlossaryDocs = () => {\n  const md = new Markdown('Translate Glossary');\n\n  md.blockQuote(\n    `해당 문서는 \\`textlint/data/rules/translateGlossary.js\\` 파일을 기반으로 자동 생성되므로, 임의 수정을 금지합니다.`\n  );\n\n  Object.keys(data).forEach((key1) => {\n    md.h2(Utils.keyToStr(key1));\n\n    Object.keys(data[key1]).forEach((key2) => {\n      md.h3(Utils.keyToStr(key2));\n\n      md.tableHeader(\n        '용어 `term`',\n        '정규표현식 `sources`',\n        '번역 `target`',\n        '논의 `discussions`',\n        '비고 `note`'\n      );\n      data[key1][key2].forEach(({sources, target, meta}) => {\n        md.tableBody(\n          meta.term,\n          sources\n            .map((source) => `\\`${source.toString().replace(/\\|/g, '\\\\|')}\\``) // Handle `|` symbol.\n            .join(', '),\n          target,\n          meta.discussions\n            .map((discussion) => `[#${discussion}](${urlIssues}${discussion})`)\n            .join(', '),\n          meta.note\n        );\n      });\n      md.tableEnd();\n    });\n  });\n\n  return md.content;\n};\n\nfs.writeFileSync(\n  path.resolve(__dirname, pathExport),\n  genTranslateGlossaryDocs()\n);\n"
  },
  {
    "path": "textlint/rules/translateGlossary.js",
    "content": "const data = require('../data/rules/translateGlossary');\nconst {errMsgTranslateGlossary} = require('../utils/errMsg');\nconst {isKoreanIncluded} = require('../utils/is');\nconst {stripDoubleQuotes, stripParentheses} = require('../utils/strip');\n\n/**\n * Rule for the Translate Glossary\n *\n * @param {RuleContext} context\n * @returns\n */\nmodule.exports = function ({Syntax, report, getSource, locator, RuleError}) {\n  return {\n    [Syntax.Str](node) {\n      const text = getSource(node);\n      const textStripped = stripParentheses(stripDoubleQuotes(text));\n\n      if (!isKoreanIncluded(textStripped)) return; // Textlint only when korean is included in `textStripped`.\n\n      Object.values(data).forEach((type1) => {\n        Object.values(type1).forEach((type2) => {\n          type2.forEach(({sources, target}) => {\n            sources.forEach((source) => {\n              const matchIndex = text.match(new RegExp(source, 'i')); // Do not use 'g' flag with textlint's CLI 'pretty-error' option. It prevents textlint from finding the exact locations.\n              const match = textStripped.match(new RegExp(source, 'i'));\n\n              if (match) {\n                report(\n                  node,\n                  new RuleError(errMsgTranslateGlossary(match[0], target), {\n                    padding: locator.range([\n                      matchIndex.index,\n                      matchIndex.index + text.length,\n                    ]),\n                  })\n                );\n              }\n            });\n          });\n        });\n      });\n    },\n  };\n};\n"
  },
  {
    "path": "textlint/tests/rules/translateGlossary.spec.js",
    "content": "const TextLintTester = require('textlint-tester').default;\nconst data = require('../../data/rules/translateGlossary');\nconst rule = require('../../rules/translateGlossary');\nconst {errMsgTranslateGlossary} = require('../../utils/errMsg');\n\nconst tester = new TextLintTester();\n\ndescribe('translateGlossary', function () {\n  Object.values(data).forEach((type1) => {\n    Object.values(type1).forEach((type2) => {\n      type2.forEach(({target, meta}) => {\n        tester.run(`term: ${meta.term}`, rule, {\n          invalid: [\n            {\n              text: `한글이 포함된 Str node. ${meta.term} 가나다 abc.`,\n              errors: [\n                {\n                  message: errMsgTranslateGlossary(meta.term, target),\n                },\n              ],\n            },\n          ],\n          valid: [\n            `한글이 포함된 Str node. \"${meta.term}\" 라마바 def.`, // stripDoubleQuotes func should be applied.\n            `한글이 포함된 Str node. (${meta.term}) 사아자 ghi.`, // stripParentheses func should be applied.\n            target,\n          ],\n        });\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "textlint/tests/utils/errMsg.spec.js",
    "content": "const assert = require('assert');\nconst functions = require('../../utils/errMsg');\nconst testCases = require('../../data/utils/errMsg.spec');\n\ndescribe('Util errMsg strictEqual testing', function () {\n  Object.keys(testCases).forEach((funcName) => {\n    describe(funcName, function () {\n      testCases[funcName].forEach((testCase) => {\n        it(`${testCase.actual.source}, ${testCase.actual.target} => ${testCase.expected}`, function () {\n          assert.strictEqual(\n            functions[funcName](testCase.actual.source, testCase.actual.target),\n            testCase.expected\n          );\n        });\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "textlint/tests/utils/is.spec.js",
    "content": "const assert = require('assert');\nconst functions = require('../../utils/is');\nconst testCases = require('../../data/utils/is.spec');\n\ndescribe('Util is strictEqual testing', function () {\n  Object.keys(testCases).forEach((funcName) => {\n    describe(funcName, function () {\n      testCases[funcName].forEach((testCase) => {\n        it(`${testCase.actual} => ${testCase.expected}`, function () {\n          assert.strictEqual(\n            functions[funcName](testCase.actual),\n            testCase.expected\n          );\n        });\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "textlint/tests/utils/strip.spec.js",
    "content": "const assert = require('assert');\nconst functions = require('../../utils/strip');\nconst testCases = require('../../data/utils/strip.spec');\n\ndescribe('Util strip strictEqual testing', function () {\n  Object.keys(testCases).forEach((funcName) => {\n    describe(funcName, function () {\n      testCases[funcName].forEach((testCase) => {\n        it(`${testCase.actual} => ${testCase.expected}`, function () {\n          assert.strictEqual(\n            functions[funcName](testCase.actual),\n            testCase.expected\n          );\n        });\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "textlint/utils/errMsg.js",
    "content": "/**\n * Returns error message using the given source and target texts.\n *\n * @param {string} source `'${source}'` part in return value.\n * @param {string} target `'${target}'` part in return value.\n * @returns {string} The error message. `'${source}'은/는 '${target}'(으)로 번역되어야 합니다.`\n */\nfunction errMsgTranslateGlossary(source, target) {\n  return `'${source}'은/는 '${target}'(으)로 번역되어야 합니다.`;\n}\n\n// Export the module\nmodule.exports = {\n  errMsgTranslateGlossary,\n};\n"
  },
  {
    "path": "textlint/utils/is.js",
    "content": "/**\n * Check if a string contains any Korean characters.\n *\n * @param {string} text The string to check.\n * @returns {boolean} Returns true if the string contains Korean characters, false otherwise.\n */\nfunction isKoreanIncluded(text) {\n  const regex = /[ㄱ-ㅎㅏ-ㅣ가-힣]/;\n\n  return regex.test(text);\n}\n\nmodule.exports = {\n  isKoreanIncluded,\n};\n"
  },
  {
    "path": "textlint/utils/strip.js",
    "content": "/**\n * Remove text inside double quotes `\"\"` from the input string.\n *\n * @param {string} text The input string.\n * @returns {string} The string with text inside double quotes `\"\"` removed.\n */\nfunction stripDoubleQuotes(text) {\n  return text.replace(/\"[^\"]*\"/g, '');\n}\n\n/**\n * Remove text inside parentheses `()` from the input string.\n *\n * @param {string} text The input string.\n * @returns {string} The string with text inside parentheses `()` removed.\n */\nfunction stripParentheses(text) {\n  return text.replace(/\\([^)]*\\)/g, '');\n}\n\nmodule.exports = {\n  stripDoubleQuotes,\n  stripParentheses,\n};\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"es5\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"noImplicitAny\": true,\n    \"noImplicitReturns\": true,\n    \"noImplicitThis\": true,\n    \"strictNullChecks\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"node\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"preserve\",\n    \"baseUrl\": \"src\",\n    \"incremental\": true,\n    \"plugins\": [\n      {\n        \"name\": \"next\"\n      }\n    ]\n  },\n  \"include\": [\n    \"next-env.d.ts\",\n    \"src/**/*.ts\",\n    \"src/**/*.tsx\",\n    \"src/**/*.source.js\",\n    \".next/types/**/*.ts\"\n  ],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "vercel.json",
    "content": "{\n  \"github\": {\n    \"silent\": true\n  },\n  \"trailingSlash\": false,\n  \"redirects\": [\n    {\n      \"source\": \"/reference\",\n      \"destination\": \"/reference/react\",\n      \"permanent\": true\n    },\n    {\n      \"source\": \"/reference/react-dom/hooks/useFormState\",\n      \"destination\": \"/reference/react/useActionState\",\n      \"permanent\": true\n    },\n    {\n      \"source\": \"/learn/meet-the-team\",\n      \"destination\": \"/community/team\",\n      \"permanent\": true\n    },\n    {\n      \"source\": \"/link/warning-keys\",\n      \"destination\": \"/learn/rendering-lists#keeping-list-items-in-order-with-key\",\n      \"permanent\": false\n    },\n    {\n      \"source\": \"/docs/lists-and-keys\",\n      \"destination\": \"/learn/rendering-lists#keeping-list-items-in-order-with-key\",\n      \"permanent\": false\n    },\n    {\n      \"source\": \"/link/invalid-hook-call\",\n      \"destination\": \"/warnings/invalid-hook-call-warning\",\n      \"permanent\": false\n    },\n    {\n      \"source\": \"/link/hooks-data-fetching\",\n      \"destination\": \"/reference/react/useEffect#fetching-data-with-effects\",\n      \"permanent\": false\n    },\n    {\n      \"source\": \"/link/special-props\",\n      \"destination\": \"/warnings/special-props\",\n      \"permanent\": false\n    },\n    {\n      \"source\": \"/link/dangerously-set-inner-html\",\n      \"destination\": \"/reference/react-dom/components/common#dangerously-setting-the-inner-html\",\n      \"permanent\": false\n    },\n    {\n      \"source\": \"/link/controlled-components\",\n      \"destination\": \"/reference/react-dom/components/input#controlling-an-input-with-a-state-variable\",\n      \"permanent\": false\n    },\n    {\n      \"source\": \"/link/react-devtools\",\n      \"destination\": \"/learn/react-developer-tools\",\n      \"permanent\": false\n    },\n    {\n      \"source\": \"/link/invalid-aria-props\",\n      \"destination\": \"/warnings/invalid-aria-prop\",\n      \"permanent\": false\n    },\n    {\n      \"source\": \"/link/hydration-mismatch\",\n      \"destination\": \"/reference/react-dom/client/hydrateRoot#hydrating-server-rendered-html\",\n      \"permanent\": false\n    },\n    {\n      \"source\": \"/link/switch-to-createroot\",\n      \"destination\": \"/blog/2022/03/08/react-18-upgrade-guide#updates-to-client-rendering-apis\",\n      \"permanent\": false\n    },\n    {\n      \"source\": \"/link/error-boundaries\",\n      \"destination\": \"/reference/react/Component#catching-rendering-errors-with-an-error-boundary\",\n      \"permanent\": false\n    },\n    {\n      \"source\": \"/link/strict-mode-find-node\",\n      \"destination\": \"https://18.react.dev/reference/react-dom/findDOMNode#alternatives\",\n      \"permanent\": false\n    },\n    {\n      \"source\": \"/link/rules-of-hooks\",\n      \"destination\": \"/warnings/invalid-hook-call-warning\",\n      \"permanent\": false\n    },\n    {\n      \"source\": \"/link/event-pooling\",\n      \"destination\": \"https://legacy.reactjs.org/docs/legacy-event-pooling.html\",\n      \"permanent\": false\n    },\n    {\n      \"source\": \"/link/legacy-context\",\n      \"destination\": \"/blog/2024/04/25/react-19-upgrade-guide#removed-removing-legacy-context\",\n      \"permanent\": false\n    },\n    {\n      \"source\": \"/link/crossorigin-error\",\n      \"destination\": \"https://legacy.reactjs.org/docs/cross-origin-errors.html\",\n      \"permanent\": false\n    },\n    {\n      \"source\": \"/link/react-polyfills\",\n      \"destination\": \"https://legacy.reactjs.org/docs/javascript-environment-requirements.html\",\n      \"permanent\": false\n    },\n    {\n      \"source\": \"/link/wrap-tests-with-act\",\n      \"destination\": \"https://legacy.reactjs.org/docs/test-utils.html#act\",\n      \"permanent\": false\n    },\n    {\n      \"source\": \"/link/refs-must-have-owner\",\n      \"destination\": \"https://legacy.reactjs.org/warnings/refs-must-have-owner.html\",\n      \"permanent\": false\n    },\n    {\n      \"source\": \"/link/derived-state\",\n      \"destination\": \"https://legacy.reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html\",\n      \"permanent\": false\n    },\n    {\n      \"source\": \"/link/strict-mode-string-ref\",\n      \"destination\": \"https://legacy.reactjs.org/docs/refs-and-the-dom.html#legacy-api-string-refs\",\n      \"permanent\": false\n    },\n    {\n      \"source\": \"/link/perf-use-production-build\",\n      \"destination\": \"https://legacy.reactjs.org/docs/optimizing-performance.html#use-the-production-build\",\n      \"permanent\": false\n    },\n    {\n      \"source\": \"/link/unsafe-component-lifecycles\",\n      \"destination\": \"https://legacy.reactjs.org/blog/2018/03/27/update-on-async-rendering.html\",\n      \"permanent\": false\n    },\n    {\n      \"source\": \"/link/test-utils-mock-component\",\n      \"destination\": \"https://gist.github.com/bvaughn/fbf41b3f895bf2d297935faa5525eee9\",\n      \"permanent\": false\n    },\n    {\n      \"source\": \"/link/attribute-behavior\",\n      \"destination\": \"https://legacy.reactjs.org/blog/2017/09/08/dom-attributes-in-react-16.html#changes-in-detail\",\n      \"permanent\": false\n    },\n    {\n      \"source\": \"/link/react-devtools-faq\",\n      \"destination\": \"https://github.com/facebook/react/tree/main/packages/react-devtools#faq\",\n      \"permanent\": false\n    },\n    {\n      \"source\": \"/link/setstate-in-render\",\n      \"destination\": \"https://github.com/facebook/react/issues/18178#issuecomment-595846312\",\n      \"permanent\": false\n    },\n    {\n      \"source\": \"/link/new-jsx-transform\",\n      \"destination\": \"https://legacy.reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html\",\n      \"permanent\": false\n    },\n    {\n      \"source\": \"/link/cra\",\n      \"destination\": \"/blog/2025/02/14/sunsetting-create-react-app\",\n      \"permanent\": false\n    },\n    {\n      \"source\": \"/warnings/version-mismatch\",\n      \"destination\": \"/warnings/invalid-hook-call-warning#mismatching-versions-of-react-and-react-dom\",\n      \"permanent\": false\n    },\n    {\n      \"source\": \"/reference/react/directives\",\n      \"destination\": \"/reference/rsc/directives\",\n      \"permanent\": true\n    },\n    {\n      \"source\": \"/reference/react/use-client\",\n      \"destination\": \"/reference/rsc/use-client\",\n      \"permanent\": true\n    },\n    {\n      \"source\": \"/reference/react/use-server\",\n      \"destination\": \"/reference/rsc/use-server\",\n      \"permanent\": true\n    },\n    {\n      \"source\": \"/reference/rsc/server-actions\",\n      \"destination\": \"/reference/rsc/server-functions\",\n      \"permanent\": true\n    },\n    {\n      \"source\": \"/reference/react-dom/findDOMNode\",\n      \"destination\": \"https://18.react.dev/reference/react-dom/findDOMNode\",\n      \"permanent\": true\n    },\n    {\n      \"source\": \"/reference/react/createFactory\",\n      \"destination\": \"https://18.react.dev/reference/react/createFactory\",\n      \"permanent\": true\n    },\n    {\n      \"source\": \"/reference/react-dom/render\",\n      \"destination\": \"https://18.react.dev/reference/react-dom/render\",\n      \"permanent\": true\n    },\n    {\n      \"source\": \"/reference/react-dom/hydrate\",\n      \"destination\": \"https://18.react.dev/reference/react-dom/hydrate\",\n      \"permanent\": true\n    },\n    {\n      \"source\": \"/reference/react-dom/unmountComponentAtNode\",\n      \"destination\": \"https://18.react.dev/reference/react-dom/unmountComponentAtNode\",\n      \"permanent\": true\n    },\n    {\n      \"source\": \"/reference/react-dom/server/renderToStaticNodeStream\",\n      \"destination\": \"https://18.react.dev/reference/react-dom/server/renderToStaticNodeStream\",\n      \"permanent\": true\n    },\n    {\n      \"source\": \"/reference/react-dom/server/renderToNodeStream\",\n      \"destination\": \"https://18.react.dev/reference/react-dom/server/renderToNodeStream\",\n      \"permanent\": true\n    },\n    {\n      \"source\": \"/learn/start-a-new-react-project\",\n      \"destination\": \"/learn/creating-a-react-app\",\n      \"permanent\": true\n    },\n    {\n      \"source\": \"/learn/building-a-react-framework\",\n      \"destination\": \"/learn/build-a-react-app-from-scratch\",\n      \"permanent\": true\n    },\n    {\n      \"source\": \"/blog/2024/04/25/react-19\",\n      \"destination\": \"/blog/2024/12/05/react-19\",\n      \"permanent\": true\n    },\n    {\n      \"source\": \"/feed.xml\",\n      \"destination\": \"/rss.xml\",\n      \"permanent\": true\n    },\n    {\n      \"source\": \"/reference/react/experimental_useEffectEvent\",\n      \"destination\": \"/reference/react/useEffectEvent\",\n      \"permanent\": true\n    },\n    {\n      \"source\": \"/blog/2025/04/21/react-compiler-rc\",\n      \"destination\": \"/blog/2025/10/07/react-compiler-1\",\n      \"permanent\": true\n    }\n  ],\n  \"headers\": [\n    {\n      \"source\": \"/fonts/(.*).woff2\",\n      \"headers\": [\n        {\n          \"key\": \"Cache-Control\",\n          \"value\": \"public, max-age=31536000, immutable\"\n        }\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "wiki/best-practices-for-translation.md",
    "content": "# 번역을 위한 모범 사례\n\n<https://github.com/reactjs/ko.react.dev/issues/27>\n\n## 쌍점은 거의 사용하지 않습니다\n\n한국어 맞춤법에서는 쌍점(colon, 콜론)을 사용하는 경우를 규정하고 있는데 항목의 열거나 희곡 대사 등에서만 제한적으로 사용합니다([참고](https://korean.go.kr/kornorms/regltn/regltnView.do?regltn_code=0001&regltn_no=753#a753)). 특히 영어처럼 문장 끝에 쌍점을 붙이는 경우는 없으므로 주의하세요.\n\n```text\n👍모범 사례\n다음 예시를 보세요.\n\n👎잘못된 사례\n다음 예시를 보세요:\n```\n\n## '시'는 제한적으로 사용합니다\n\n존칭을 표현하는 '-시-'는 문장의 간결함을 해칠 수 있습니다. `~하세요`처럼 제한적인 경우에만 사용하고 그 외에는 사용하지 않는 편이 좋습니다. 다만, 작업자들끼리 대화할 때는 존중의 의미에서 사용해도 상관없습니다.\n\n```text\n👍모범 사례\n살펴보면 좋습니다.\n\n👍모범 사례\n살펴보세요.\n\n👎잘못된 사례\n살펴보시면 좋습니다.\n```\n\n## 쉼표를 모두 옮길 필요는 없습니다\n\n영어와 우리말에서 쉼표를 사용하는 사례가 다릅니다. 우리말에서 쉼표를 사용하는 몇 가지 경우는 [맞춤법](https://korean.go.kr/kornorms/regltn/regltnView.do?regltn_code=0001&regltn_no=732#a732)에 규정되어 있습니다. 꼭 필요할 때만 사용하고 그 외에는 제한하는 편이 좋을 것 같습니다.\n\n```text\n🇬🇧 For example, if `id` is the row ID, either of the following would work\n\n👍모범 사례\n예를 들어 `id`가 행의 ID일 경우 다음 코드가 모두 작동합니다.\n\n👎잘못된 사례\n예를 들어, `id`가 행의 ID일 경우 다음 코드가 모두 작동합니다.\n```\n\n## render = 렌더링하다\n\n`render`나 `bind`처럼 우리말 번역이 없고 원문의 음만 따서 쓰는 동사는 명사형 + `~하다`로 번역하는 게 좋습니다.\n\n```text\n🇬🇧 React renders DOM elements.\n\n👍모범 사례\nReact는 DOM 엘리먼트를 렌더링합니다.\n\n👎잘못된 사례\nReact는 DOM 엘리먼트를 렌더합니다.\n\n👍모범 사례\n함수를 바인딩해야 합니다.\n```\n\n## '당신'과 '여러분'은 없어도 됩니다\n\n아주 가끔 꼭 필요할 때가 있기는 하지만 영어 표현에 많은 `당신`이나 `여러분`, `우리`같은 표현은 안 쓰는 게 훨씬 자연스럽습니다.\n\n```text\n🇬🇧 Let's say you want to output \"Hello, world\" in a DOM element.\n\n👍모범 사례\nDOM 엘리먼트에 \"Hello, World\"를 표현한다고 생각해봅시다.\n\n👎잘못된 사례\n당신이 DOM 엘리먼트에 \"Hello, World\"를 표현하고 싶다고 생각해봅시다.\n```\n\n## 피동형 문장에 주의하세요\n\n사실 원칙대로라면 `~되다`같은 수동태도 가능하면 억제하는 편이 좋지만 실제로 작업을 하다 보면 써야 문장이 자연스러울 때도 있습니다. 다만 `보여지다`, `생각되다`와 같은 불필요한 피동형 문장은 자제하는 편이 좋습니다.\n\n```text\n🇬🇧 Elements appeared on the screen.\n\n👍모범 사례\n화면에 보이는 엘리먼트.\n\n👎잘못된 사례\n화면에 보여지는 엘리먼트.\n```\n\n## 똑같은 표현은 비슷한 표현으로 대체하면 문장이 더 유려해집니다\n\n번역문에서 흔히 나타나는 문제가 영어 단어를 1:1로 번역하다 보니 표현이 단조로워진다는 점입니다. 예를 들어 `common`은 `일반적으로`라고만 번역한다거나 `call`은 `호출한다`라고만 번역하는 사례가 많습니다. 가까운 위치에서 똑같은 표현이 여러 차례 등장할 때 조금씩 표현을 바꾸어주면 문장이 더 읽기 좋아집니다. `common`이나 `commonly`는 `일반적으로`, `널리`, `흔히` 등으로 바꿀 수 있고 `call`, `invoke` 등은 `호출하다`, `실행하다` 등으로 바꿀 수 있습니다.\n\n```text\n👍모범 사례\n이때 흔히 사용되는 패턴은 일반 자바스크립트 객체를 전달하는 것이다.\n\n👎잘못된 사례\n이때 일반적으로 사용되는 패턴은 일반 자바스크립트 객체를 전달하는 것이다.\n```\n\n## 어색하지 않다면 단수형을 사용합시다\n\n원래 우리말에서는 `~들`이라는 복수형 표현을 그리 널리 사용하지 않습니다. 물론 문장의 의미에 따라 반드시 복수의 객체임을 명시해야 할 때가 있긴 하지만, 그 외의 경우라면 단수형으로 표현하는 편이 대체로 자연스럽습니다.\n\n```text\n🇬🇧 The examples below demonstrate the differences.\n\n👍모범 사례\n아래 예시는 위에서 언급한 차이점을 보여줍니다.\n\n👍모범 사례 - 복수형 표현이 필요한 경우\n아래 예시는 위에서 언급한 여러 차이점을 보여줍니다.\n\n👎잘못된 사례\n아래 예시들은 위에서 언급한 차이점들을 보여줍니다.\n```\n\n## If는 생략할 수도 있습니다\n\n`If`로 시작하는 문장의 번역이 항상 `만약`으로 시작할 필요는 없습니다. 틀린 번역은 아니지만 한국어에서는 `~한다면`이라는 어미가 이미 조건 또는 상황의 가정을 의미하므로 때로는 `만약`이 없어도 충분히 말이 됩니다. 아래 예시는 인접한 두 문장이 모두 `만약`으로 시작하고 있었습니다. 한 문장 만이라도 `만약`을 생략하면 조금 더 읽기 수월해집니다.\n\n```text\n🇬🇧If you load React from a `<script>` tag, .... If you use ES6 with npm, ...\n\n👍모범 사례\nReact를 <script> 태그로 불러온다면 ... ES6와 npm을 함께 사용한다면 ...\n\n👎잘못된 사례\n만약 React를 <script> 태그로 불러온다면 ... 만약 ES6와 npm을 함께 사용한다면 ...\n```\n"
  },
  {
    "path": "wiki/textlint-guide.md",
    "content": "# `textlint` 가이드\n\nReact 공식 문서 한국어 번역 시 활용하는 [`textlint`](https://textlint.github.io/)에 대해 설명합니다.\n\n## 무엇인가요?\n\n```bash\ntranslateGlossary: '인터랙션'은/는 '상호작용'(으)로 번역되어야 합니다.\nko.react.dev/src/content/blog/2022/06/15/react-labs-what-we-have-been-working-on-june-2022.md:74:22\n                                           v\n    73.\n    74. 이러한 문제를 해결하는 새로운 버전의 인터랙션 추적 API(`startTransition`을 통해 시작되므로 가칭 트랜지션 추적이라고 함)를 개발 중입니다.\n    75.\n                                           ^\n\ntranslateGlossary: '튜토리얼'은/는 '자습서'(으)로 번역되어야 합니다.\nko.react.dev/src/content/blog/2023/03/16/introducing-react-dev.md:60:35\n                                                       v\n    59.\n    60. 직접 해보며 배우고 싶다면, 다음으로 [Tic-Tac-Toe 튜토리얼](/learn/tutorial-tic-tac-toe)을 확인하는 것을 추천합니다. React로 작은 게임을 구현하는 것을 자\n세히 설명하면서, 동시에 일상적으로 사용할 기술을 가르칩니다. 여기에 구현하게 될 내용이 있습니다.\n    61.\n                                                       ^\n```\n\n`textlint`는 텍스트(`.txt`)와 마크다운(`.md`, `.mdx`)을 위한 린터<sup>Linter</sup>이며 자바스크립트<sup>JavaScript</sup>로 구현되어 있습니다. [ESLint](https://eslint.org/)가 자바스크립트에 가지는 역할과 같습니다.\n\n## 어떻게 실행할 수 있나요?\n\n[`package.json`](/package.json) 상에 `scripts`로 등록해 두었기에, 아래와 같은 커맨드로 실행할 수 있습니다.\n\n### 1. 규칙 검사\n\n가장 많이 활용되는 커맨드입니다.\n\n```bash\nyarn textlint-lint\n```\n\n### 2. 테스트 실행\n\n```bash\nyarn textlint-test\n```\n\n### 3. 규칙에 따른 문서 생성\n\n```bash\nyarn textlint-docs\n```\n\n## 어떤 영역을 검사하나요?\n\n`textlint`는 공식 문서에 실질적으로 나타나는 부분인 [`/src/content`](/src/content/) 폴더 내부의 마크다운(`.md`) 파일만을 검사합니다.\n\n### 마크다운 문서 내부에서 검사하지 않는 영역\n\n> [!NOTE]\n>\n> - `textlint`는 마크다운 문서를 AST Tree로 파싱<sup>Parsing</sup> 합니다. ko.react.dev에서 구현한 규칙은 `@textlint/text-to-ast-14.0.5` 패키지에 의해 AST Tree로 파싱 된 노드<sup>Node</sup>들 중 `Str` 노드만을 검사합니다. ([`/textlint/rules/translateGlossary.js` 참고](/textlint/rules/translateGlossary.js))\n>\n> - 또한, 모든 `Str` 노드를 검사하는 것은 아닙니다. 영어 원문 번역본이 계속해서 추가되기에, 오직 영어만으로 구성된 `Str` 노드는 검사에서 제외합니다. 즉, 파싱된 `Str` 노드 중 한글이 하나라도 포함된 문장만을 검사합니다. ([`/textlint/utils/is.js` 참고](/textlint/utils/is.js))\n>\n> - 이외에도, ko.react.dev에서는 `\"\"` 및 `()`로 감싸져 있는 문장은 검사하지 않습니다. `\"\"`에는 주로 에러 메시지 등 영어 원문 그 자체의 내용이 들어가는 경우가 많으며, `()` 역시 독자의 이해를 위해 영어 원문이 그대로 들어가는 경우가 많기 때문입니다. ([`/textlint/utils/strip.js` 참고](/textlint/utils/strip.js))\n\n#### 1. 코드 블럭\n\n````md\n```js\nconst hello = 'world';\n```\n````\n\n#### 2. 인라인 코드 블럭\n\n```md\n`hello world`\n```\n\n#### 3. 한글이 포함되지 않은 문장\n\n```md\nThis text will not be linted.\n```\n\n#### 4. 쌍따옴표(`\"\"`)로 감싸져 있는 문장\n\n```md\n\"이 문장은 검사되지 않습니다.\"\n```\n\n#### 5. 소괄호(`()`)로 감싸져 있는 문장\n\n```md\n(이 문장은 검사되지 않습니다.)\n```\n\n## 특정 문맥에서 비활성화할 수 있나요?\n\n영어 표현을 부득이 하게 사용해야 할 경우, 위에서 언급한 쌍따옴표(`\"\"`)및 소괄호(`()`)를 활용하여 특정 문장을 감쌀 것을 권장합니다.\n\n위 방법을 사용할 수 없는 경우, `textlint`에서 제공하는 [Filter Rule](https://textlint.github.io/docs/configuring.html#filter-rule) 중 하나인 [`textlint-filter-rule-comments`](https://github.com/textlint/textlint-filter-rule-comments)를 사용해서 비활성화할 수 있습니다. 이미 추가되어 있으니 아래처럼 사용하시면 됩니다.\n\n```md\n<!-- textlint-disable -->\n\n주석 사이에 있는 글은 모든 규칙이 비활성화됩니다.\n\n<!-- textlint-enable -->\n```\n\n예를 들어, 한글 문장 안에 의도적으로 번역하지 않은 영어 원문을 사용해야 하는 경우 사용을 고려해 볼 수 있습니다. 이는 `textlint`가 검사하는 일부 영역에 대해 의도적으로 **규칙을 해제(예외를 설정)** 하는 것입니다.\n\n## 새로운 규칙(rule)을 어떻게 만드나요?\n\n[`textlint`의 공식 문서 Creating Rules](https://textlint.github.io/docs/rule.html)를 숙지하고 다음 과정을 진행해주세요.\n\n### ko.react.dev에서만 사용하는 규칙인 경우\n\n`textlint`와 관련된 모든 코드는 [`/textlint`](/textlint) 폴더에 작성합니다.\n\n#### 1. [`/textlint/rules`](/textlint/rules) 폴더에 1개 규칙에 1개 파일 생성\n\n특정 규칙은 `textlint` 커맨드 라인의 `--rulesdir` 옵션을 통해 실행되므로, `/textlint/rules` 폴더 하위에는 규칙과 파일을 대응시켜 작성해주세요.\n\n#### 2. [`/textlint/tests/rules`](/textlint/tests/rules) 폴더에 테스트 코드 작성\n\n[`textlint-tester`](https://github.com/textlint/textlint/tree/master/packages/textlint-tester)를 활용해서 작성한 규칙에 대응되는 테스트를 작성해주세요. 올바른 사례와 올바르지 못한 사용 사례를 포함하고, 올바르지 못한 사례는 번역자가 빠르게 수정할 수 있도록 `index`를 통해 오류가 발생한 위치를 알맞게 안내하고 있는지 검증해주세요.\n\n아래처럼 실행하면 모든 규칙 구현에 대한 테스트를 실행할 수 있습니다.\n\n```bash\nyarn textlint-test\n```\n\n### 외부 규칙을 사용하는 경우\n\n아래와 같은 예시를 따라주세요.\n\n#### 1. `yarn`을 통해 특정 패키지를 개발 의존성으로 설치\n\n```bash\nyarn add --dev textlint-rule-allowed-uris\n```\n\n#### 2. [`/.textlintrc`](/.textlintrc) 파일을 해당 규칙에 맞게 수정\n\n```javascript\nmodule.exports = {\n  rules: {\n    'allowed-uris': {\n      allowed: {\n        links: [/google/],\n      },\n    },\n  },\n};\n```\n\n## 주의해야 할 사항이 있나요?\n\n- `--fix` 옵션을 통해 자동으로 수정할 수 있는 [Fixable Rule](https://textlint.github.io/docs/rule-fixable.html)은 의도적으로 작성하지 않았습니다. 사람이 코드로 작성한 규칙이기 때문에 완벽하지 않으며 번역자가 인지하지 못한 채로 수정되기보다 문맥을 확인하고 수정하는 방향이 바람직하다고 생각하기 때문입니다.\n\n- `textlint`의 검사 기능은 의도적으로 최대한 느슨하게 만들었습니다. 엄격하게 검사를 진행할 경우, 번역 간 문장 이해 및 흐름에 방해가 될 수 있기 때문입니다.\n\n- `.json` 파일 형식으로 구현된 사이드바 메뉴 상의 내용들은 검사하지 않습니다. 따라서, 사이드바 메뉴 상에 표현된 내용들은 직접 수정해야 합니다. `src/sidebarBlog.json` 등이 이에 해당합니다.\n\n- React 문서상의 링크를 연결하기 위해 구현한 `{/* ... */}` 내부의 문자열은 항상 영어로만 구성되므로, [마크다운 문서 내부에서 검사하지 않는 영역](#마크다운-문서-내부에서-검사하지-않는-영역)의 설정에 의해 검사가 자동으로 제외됩니다.\n"
  },
  {
    "path": "wiki/translate-glossary-legacy.md",
    "content": "## React\n\n| 용어 | 번역 | 논의 |\n| :---: | :---:| :---: |\n| Tutorial | 자습서 | [#2](https://github.com/reactjs/ko.reactjs.org/issues/2) |\n| Declarative | 선언적인 | [#2](https://github.com/reactjs/ko.reactjs.org/issues/2) |\n| Component | 컴포넌트 | [#2](https://github.com/reactjs/ko.reactjs.org/issues/2) |\n| Stateful Component | 유상태 컴포넌트 | [#2](https://github.com/reactjs/ko.reactjs.org/issues/2) |\n| Stateless Component | 무상태 컴포넌트 | [#2](https://github.com/reactjs/ko.reactjs.org/issues/2) |\n| render | 렌더링하다 |   [#2](https://github.com/reactjs/ko.reactjs.org/issues/2) |\n| data | 데이터 | [#2](https://github.com/reactjs/ko.reactjs.org/issues/2) |\n| Application | 애플리케이션 | [#2](https://github.com/reactjs/ko.reactjs.org/issues/2) |\n| External Plugins | 외부 플러그인 | [#2](https://github.com/reactjs/ko.reactjs.org/issues/2) |\n| Third Plugins | 서드파티 플러그인 | [#2](https://github.com/reactjs/ko.reactjs.org/issues/2) |\n| syntax | 문법 | [#2](https://github.com/reactjs/ko.reactjs.org/issues/2) |\n| Embedding Expressions | 표현식 포함하기 | [#2](https://github.com/reactjs/ko.reactjs.org/issues/2) |\n| Attributes | 어트리뷰트 | [#2](https://github.com/reactjs/ko.reactjs.org/issues/2) |\n| Elements | 엘리먼트 | [#2](https://github.com/reactjs/ko.reactjs.org/issues/2) |\n| Function / Functional Components | 함수 컴포넌트 | [#2](https://github.com/reactjs/ko.reactjs.org/issues/2) |\n| Class Components | 클래스 컴포넌트 | [#2](https://github.com/reactjs/ko.reactjs.org/issues/2) |\n| Composition | 합성 | [#2](https://github.com/reactjs/ko.reactjs.org/issues/2) |\n| Inheritance | 상속 | [#2](https://github.com/reactjs/ko.reactjs.org/issues/2) |\n| Lifecycle | 생명주기 | [#2](https://github.com/reactjs/ko.reactjs.org/issues/2) |\n| Handling Events | 이벤트 처리 | [#2](https://github.com/reactjs/ko.reactjs.org/issues/2) |\n| Conditional Rendering | 조건부 렌더링 | [#2](https://github.com/reactjs/ko.reactjs.org/issues/2) |\n| Operator | 연산자 | [#2](https://github.com/reactjs/ko.reactjs.org/issues/2) |\n| reuse | 재사용 | [#2](https://github.com/reactjs/ko.reactjs.org/issues/2) |\n| mock | 모의 | [#2](https://github.com/reactjs/ko.reactjs.org/issues/2) |\n| callback | 콜백 |  [#2](https://github.com/reactjs/ko.reactjs.org/issues/2) |\n| synthetic event| 합성 이벤트 | [#2](https://github.com/reactjs/ko.reactjs.org/issues/2) |\n| higher order component | 고차 컴포넌트 | [#2](https://github.com/reactjs/ko.reactjs.org/issues/2) |\n| mount | 마운트 | [#2](https://github.com/reactjs/ko.reactjs.org/issues/2) |\n| unmount | 마운트 해제 | [#2](https://github.com/reactjs/ko.reactjs.org/issues/2) |\n| form | 폼 | [#2](https://github.com/reactjs/ko.reactjs.org/issues/2) |\n| wrapper | 래퍼 | [#2](https://github.com/reactjs/ko.reactjs.org/issues/2) |\n| children | 자식 | [#2](https://github.com/reactjs/ko.reactjs.org/issues/2) |\n| Code-Splitting | 코드 분할 | [#2](https://github.com/reactjs/ko.reactjs.org/issues/2) |\n| reconciliation | 재조정 | [#2](https://github.com/reactjs/ko.reactjs.org/issues/2) |\n| property | 프로퍼티 | [#2](https://github.com/reactjs/ko.reactjs.org/issues/2) |\n| reference | 레퍼런스 | [#569](https://github.com/reactjs/ko.react.dev/issues/569) |\n| API reference | API 레퍼런스 | [#569](https://github.com/reactjs/ko.react.dev/issues/569) |\n| user interfaces | 사용자 인터페이스 | [#569](https://github.com/reactjs/ko.react.dev/issues/569) |\n| markup | 마크업 | [#569](https://github.com/reactjs/ko.react.dev/issues/569) |\n| interactivity | 상호작용 | [#569](https://github.com/reactjs/ko.react.dev/issues/569) |\n| architecture | 아키텍처 | [#569](https://github.com/reactjs/ko.react.dev/issues/569) |\n| full-stack | 풀스택 | [#569](https://github.com/reactjs/ko.react.dev/issues/569) |\n| browser extension | 브라우저 확장 프로그램 | [#610](https://github.com/reactjs/ko.react.dev/issues/610) |\n| Escape Hatches | 탈출구 | [#738](https://github.com/reactjs/ko.react.dev/issues/738)\n\n## 일반\n\n| 용어 | 번역 | 논의 |\n| :---: | :---: | :---: | \n| tip | 팁 |  |\n| example | 예시 |  |\n| chapter | 장 |  |\n| spec, specifiation | 명세 |  |\n| camelCase | 캐멀 케이스 |  |\n| note | 주의 |  |\n| parameter | 매개변수 | [#614](https://github.com/reactjs/ko.react.dev/issues/614) |\n| deprecated | 더 이상 사용되지 않습니다. | [#632](https://github.com/reactjs/ko.react.dev/issues/632) |\n| pitfall | 주의하세요! | [#632](https://github.com/reactjs/ko.react.dev/issues/632) |\n| note | 중요합니다! | [#632](https://github.com/reactjs/ko.react.dev/issues/632) |\n| wip | 개발중이에요 | [#632](https://github.com/reactjs/ko.react.dev/issues/632) |\n| Returns | 반환값 | 제목으로 사용된 경우 [#725](https://github.com/reactjs/ko.react.dev/issues/725) |\n| logic | 로직 | [#695](https://github.com/reactjs/ko.react.dev/issues/695) |\n| dependency | 의존성 | [#841](https://github.com/reactjs/ko.react.dev/issues/841) |\n\n## 번역하면 안되는 용어\n\n> 번역하면 안되는 용어의 경우, 조사를 정하기 위해 **읽는 법** 항목이 필요\n| 용어 | 읽는 법 | 논의 |\n| :-----: | :---: | :---: |\n| React | 리액트 | |\n| props | 프로퍼티즈. 단수형이라면 프로퍼티 | |\n| state | 스테이트 | |\n| dispatch | 디스패치 | |\n| context | 컨텍스트 | |\n| DOM | 돔 | |\n| ref | 레퍼런스 | |\n| fragments | 프래그먼츠. 단수형은 프래그먼트 | |\n| portal | 포탈 | |\n| class | 클래스 | |\n| Web（대문자로 된 Web의 경우 번역하지 않음）| 웹 | |\n| UI | 유아이 | |\n| Tick | 틱 | |\n| bundle | 번들 | |\n| bundler | 번들러 | [#829](https://github.com/reactjs/ko.react.dev/issues/829) |\n| package | 패키지 | |\n| Create React App | 크리에이트 리액트 앱 |\n"
  },
  {
    "path": "wiki/translate-glossary.md",
    "content": "# Translate Glossary\n\n> 해당 문서는 `textlint/data/rules/translateGlossary.js` 파일을 기반으로 자동 생성되므로, 임의 수정을 금지합니다.\n\n## 번역해야 하는 용어\n\n### React\n\n용어 `term`|정규표현식 `sources`|번역 `target`|논의 `discussions`|비고 `note`|\n---|---|---|---|---|\nTutorial|`/\\bTutorial\\b/`, `/[듀튜]토리얼/`|자습서|[#2](https://github.com/reactjs/ko.react.dev/issues/2)||\nDeclarative|`/\\bDeclarative\\b/`|선언적인|[#2](https://github.com/reactjs/ko.react.dev/issues/2)||\nComponent|`/\\bComponent\\b/`, `/컴퍼넌트/`, `/컴포넌츠/`|컴포넌트|[#2](https://github.com/reactjs/ko.react.dev/issues/2)||\nStateful|`/\\bStateful\\b/`|유상태|[#2](https://github.com/reactjs/ko.react.dev/issues/2)||\nStateless|`/\\bStateless\\b/`|무상태|[#2](https://github.com/reactjs/ko.react.dev/issues/2)||\nRender|`/\\bRender(?!er)(?:ing)?\\b/`, `/랜더링/`, `/[렌랜]더(?!링)\\s?[하한할함합]/`|렌더링(하다)|[#2](https://github.com/reactjs/ko.react.dev/issues/2)||\nData|`/\\bData\\b/`, `/대이터/`|데이터|[#2](https://github.com/reactjs/ko.react.dev/issues/2)||\nApplication|`/\\bApplication\\b/`, `/어플리케이[선션]/`, `/응용\\s?프로그램/`|애플리케이션|[#2](https://github.com/reactjs/ko.react.dev/issues/2)||\nExternal|`/\\bExternal\\b/`|외부|[#2](https://github.com/reactjs/ko.react.dev/issues/2)||\nPlugin|`/\\bPlugin\\b/`|플러그인|[#2](https://github.com/reactjs/ko.react.dev/issues/2)||\nThird|`/\\bThird\\b/`, `/써드/`|서드|[#2](https://github.com/reactjs/ko.react.dev/issues/2)||\nSyntax|`/\\bSyntax\\b/`, `/[신씬]택스/`|문법|[#2](https://github.com/reactjs/ko.react.dev/issues/2)||\nEmbedding Expression|`/\\bEmbedding\\s?Expression\\b/`|표현식 포함하기|[#2](https://github.com/reactjs/ko.react.dev/issues/2)||\nAttribute|`/\\bAttribute\\b/`, `/애트리뷰트/`|어트리뷰트|[#2](https://github.com/reactjs/ko.react.dev/issues/2)||\nElement|`/\\bElement\\b/`, `/[엘앨]리먼츠/`, `/앨리먼트/`|엘리먼트|[#2](https://github.com/reactjs/ko.react.dev/issues/2)||\nFunction|`/\\bFunction(?:al)?\\b/`|함수|[#2](https://github.com/reactjs/ko.react.dev/issues/2)||\nClass|`/\\bClass\\b/`|클래스|[#2](https://github.com/reactjs/ko.react.dev/issues/2)||\nComposition|`/\\bComposition\\b/`, `/[컴콤][퍼포]지[선션]/`|합성|[#2](https://github.com/reactjs/ko.react.dev/issues/2)||\nInheritance|`/\\bInheritance\\b/`|상속|[#2](https://github.com/reactjs/ko.react.dev/issues/2)||\nLifecycle|`/\\bLife\\s?Cycle\\b/`, `/라이프\\s?사이클/`, `/생명 주기/`|생명주기|[#2](https://github.com/reactjs/ko.react.dev/issues/2)||\nHandling|`/\\bHandling\\b/`, `/핸들링/`|처리|[#2](https://github.com/reactjs/ko.react.dev/issues/2)||\nConditional|`/\\bConditional\\b/`, `/컨디[서셔][날널]/`|조건부|[#2](https://github.com/reactjs/ko.react.dev/issues/2)||\nOperator|`/\\bOperator\\b/`, `/오퍼[레래]이터/`|연산자|[#2](https://github.com/reactjs/ko.react.dev/issues/2)||\nReuse|`/\\bReuse\\b/`|재사용|[#2](https://github.com/reactjs/ko.react.dev/issues/2)||\nMock|`/\\bMock\\b/`|모의|[#2](https://github.com/reactjs/ko.react.dev/issues/2)||\nCallback|`/\\bCallback\\b/`|콜백|[#2](https://github.com/reactjs/ko.react.dev/issues/2)||\nSynthetic|`/\\bSynthetic\\b/`|합성|[#2](https://github.com/reactjs/ko.react.dev/issues/2)||\nEvent|`/\\bEvent\\b/`|이벤트|[#2](https://github.com/reactjs/ko.react.dev/issues/2)||\nHigher Order|`/\\bHigher\\s?Order\\b/`|고차|[#2](https://github.com/reactjs/ko.react.dev/issues/2)||\nMount|`/\\b(?<!Un)Mount\\b/`|마운트|[#2](https://github.com/reactjs/ko.react.dev/issues/2)||\nUnmount|`/\\bUnmount\\b/`, `/언마운트/`|마운트 해제|[#2](https://github.com/reactjs/ko.react.dev/issues/2)||\nForm|`/\\bForm\\b/`|폼|[#2](https://github.com/reactjs/ko.react.dev/issues/2)||\nWrapper|`/\\bWrapper\\b/`|래퍼|[#2](https://github.com/reactjs/ko.react.dev/issues/2)||\nChildren|`/\\bChild(?:ren)?\\b/`|자식|[#2](https://github.com/reactjs/ko.react.dev/issues/2)||\nCode-Splitting|`/\\bCode[-\\s]?Splitting\\b/`|코드 분할|[#2](https://github.com/reactjs/ko.react.dev/issues/2)||\nReconciliation|`/\\bReconciliation\\b/`|재조정|[#2](https://github.com/reactjs/ko.react.dev/issues/2)||\nProperty|`/\\bPropert(?:y\\|ies)\\b/`|프로퍼티|[#2](https://github.com/reactjs/ko.react.dev/issues/2)||\nReference|`/\\bReference\\b/`, `/래퍼런스/`|레퍼런스|[#569](https://github.com/reactjs/ko.react.dev/issues/569)||\nUser|`/\\bUser\\b/`, `/유저/`|사용자|[#569](https://github.com/reactjs/ko.react.dev/issues/569)||\nInterface|`/\\bInterface\\b/`|인터페이스|[#569](https://github.com/reactjs/ko.react.dev/issues/569)||\nMarkup|`/\\bMarkup\\b/`, `/마크 업/`|마크업|[#569](https://github.com/reactjs/ko.react.dev/issues/569)||\nInteractivity|`/\\bInteracti(?:vity\\|on)\\b/`, `/인터[랙렉][선션]/`|상호작용|[#569](https://github.com/reactjs/ko.react.dev/issues/569)||\nArchitecture|`/\\bArchitecture\\b/`, `/아키택처/`, `/아키[택텍]쳐/`|아키텍처|[#569](https://github.com/reactjs/ko.react.dev/issues/569)||\nFull-Stack|`/\\bFull[-\\s]?Stack\\b/`|풀스택|[#569](https://github.com/reactjs/ko.react.dev/issues/569)||\nBrowser|`/\\bBrowser\\b/`|브라우저|[#610](https://github.com/reactjs/ko.react.dev/issues/610)||\nExtension|`/\\bExtension\\b/`, `/확장프로그램/`|확장 프로그램|[#610](https://github.com/reactjs/ko.react.dev/issues/610)||\nEscape Hatches|`/\\bEscape[-\\s]?Hatches\\b/`|탈출구|[#738](https://github.com/reactjs/ko.react.dev/issues/738)||\nBundle|`/\\bBundles?\\b/`|번들|[#829](https://github.com/reactjs/ko.react.dev/issues/829)||\nBundler|`/\\bBundlers?\\b/`|번들러|[#829](https://github.com/reactjs/ko.react.dev/issues/829)||\nBundling|`/\\bBundling\\b/`|번들링|[#829](https://github.com/reactjs/ko.react.dev/issues/829)||\nCompiler|`/\\bCompiler\\b/`|컴파일러|[#1400](https://github.com/reactjs/ko.react.dev/issues/1400)||\n\n### Others\n\n용어 `term`|정규표현식 `sources`|번역 `target`|논의 `discussions`|비고 `note`|\n---|---|---|---|---|\nTip|`/\\bTips?\\b/`|팁|[#2](https://github.com/reactjs/ko.react.dev/issues/2)||\nExample|`/\\bExamples?\\b/`, `/예제/`|예시|[#2](https://github.com/reactjs/ko.react.dev/issues/2)||\nChapter|`/\\bChapters?\\b/`, `/[챕쳅]터/`|장|[#2](https://github.com/reactjs/ko.react.dev/issues/2)||\nSpecification|`/\\bSpec(?:ification)?s?\\b/`, `/스[펙팩]/`|명세|[#2](https://github.com/reactjs/ko.react.dev/issues/2)|Spec도 동일하게 번역|\ncamelCase|`/\\bcamel\\s?Case\\b/`, `/[캐카][맬멜]\\s?케이스/`|캐멀 케이스|[#2](https://github.com/reactjs/ko.react.dev/issues/2)||\nParameter|`/\\bParam(?:eter)?s?\\b/`, `/[파패][라러]미터/`, `/매개 변수/`|매개변수|[#614](https://github.com/reactjs/ko.react.dev/issues/614)||\nDeprecated|`/\\bDeprecated\\b/`|더 이상 사용되지 않습니다.|[#632](https://github.com/reactjs/ko.react.dev/issues/632)||\nPitfall|`/\\bPitfall\\b/`|주의하세요!|[#632](https://github.com/reactjs/ko.react.dev/issues/632)||\nNote|`/\\bNote\\b/`|중요합니다!|[#632](https://github.com/reactjs/ko.react.dev/issues/632)||\nWip|`/\\bWip\\b/`|개발중이에요|[#632](https://github.com/reactjs/ko.react.dev/issues/632)||\nReturns|`/\\bReturns\\b/`, `/반환\\s+(?:값\\s+)?{\\//`|반환값|[#725](https://github.com/reactjs/ko.react.dev/issues/725)|제목에 사용된 경우|\nCaveats|`/\\bCaveats?\\b/`, `/주의사항/`|주의 사항|[#1095](https://github.com/reactjs/ko.react.dev/issues/1095)||\nLogic|`/\\bLogic\\b/`|로직|[#695](https://github.com/reactjs/ko.react.dev/issues/695)||\nDependency|`/\\bDependenc(?:y\\|ies)\\b/`|의존성|[#841](https://github.com/reactjs/ko.react.dev/issues/841)||\nDirective|`/\\bDirectives?\\b/`|지시어|[#819](https://github.com/reactjs/ko.react.dev/issues/819)||\nUsage|`/\\bUsage\\b/`|사용법|[#1425](https://github.com/reactjs/ko.react.dev/issues/1425)||\nImperative|`/\\bImperative\\b/`|명령형|[#1425](https://github.com/reactjs/ko.react.dev/issues/1425)||\n\n"
  },
  {
    "path": "wiki/universal-style-guide.md",
    "content": "# 공통 스타일 가이드\n\n이 문서는 **모든** 언어에 적용돼야 할 규칙을 설명합니다.\n\n## 제목 아이디\n\n모든 제목에는 다음과 같이 아이디가 명시적으로 설정되어 있습니다.\n\n```md\n## Try React {/*try-react*/}\n```\n\n**아이디는 번역하면 안됩니다!** 이 아이디는 탐색을 위해 사용되므로 번역하면 아래처럼 외부에서 문서를 참조할 때 링크가 깨질 수 있습니다.\n\n```md\n자세한 내용은 [시작 부분](/getting-started#try-react)을 참조해주세요.\n```\n\n✅ 권장\n\n```md\n## React 시도해보기 {/*try-react*/}\n```\n\n❌ 금지:\n\n```md\n## React 시도해보기 {/*react-시도해보기*/}\n```\n\n이는 위에 있는 링크를 깨지게 만듭니다.\n\n## 코드에 있는 문자\n\n주석을 제외한 모든 코드는 번역하지 않고 그대로 놔둬 주세요(단, 주석에 포함된 로그나 에러 메시지는 영어 원문으로 남겨주세요).\n\n선택적으로 문자열에 있는 텍스트를 수정할 수 있지만, 코드를 참조하는 문자열은 번역하지 않도록 주의해주세요. \n\n콘솔 로그는 번역하지 않고 그대로 놔둬 주세요. 실제 출력되는 로그는 대부분 영어로 표시되기 때문이에요.\n\n예시는 다음과 같습니다.\n```js\n// Example\nconst element = <h1>Hello, world</h1>;\nReactDOM.render(element, document.getElementById('root'));\n```\n\n✅ 권장\n\n```js\n// 예시\nconst element = <h1>Hello, world</h1>;\nReactDOM.render(element, document.getElementById('root'));\n```\n\n✅ 허용:\n\n```js\n// 예시\nconst element = <h1>안녕 세상</h1>;\nReactDOM.render(element, document.getElementById('root'));\n```\n\n❌ 금지:\n\n```js\n// 예시\nconst element = <h1>안녕 세상</h1>;\n// \"root\"는 HTML 엘리먼트의 아이디를 의미합니다.\n// 번역하지 마세요.\nReactDOM.render(element, document.getElementById('뿌리'));\n```\n\n❌ 절대 금지:\n\n```js\n// 예시\nconst 요소 = <h1>안녕 세상</h1>;\nReactDOM.그리다(요소, 문서.아이디로부터_엘리먼트_가져오기('뿌리'));\n```\n\n## 외부 링크\n\n외부 링크가 [MDN]이나 [Wikipedia]같은 참고 문헌의 문서에 연결되어 있고 해당 문서가 자국어로 잘 번역되어 있다면 번역 문서를 링크하는 것도 고려해보세요.\n\n[MDN]: https://developer.mozilla.org/en-US/\n[Wikipedia]: https://en.wikipedia.org/wiki/Main_Page\n\n예시는 다음과 같습니다.\n\n```md\nReact elements are [immutable](https://en.wikipedia.org/wiki/Immutable_object).\n```\n\n✅ 허용:\n\n```md\nReact 엘리먼트는 [불변객체](https://ko.wikipedia.org/wiki/불변객체)입니다.\n```\n\n외부 링크를 대체할 만한 자국어 자료가 없다면 (Stack Overflow, YouTube 비디오 등) 영어 링크를 사용해주세요.\n"
  }
]