<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>hooninedev.com</title>
        <link>https://hooninedev.com/en</link>
        <description>프론트엔드 개발자 이지훈(후니)의 기술 블로그. React, TypeScript, Next.js 등 웹 개발 기록과 학습 노트를 공유합니다.</description>
        <lastBuildDate>Wed, 19 Aug 2026 00:39:13 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>en</language>
        <copyright>All rights reserved 2026, 이지훈</copyright>
        <item>
            <title><![CDATA[AI Agent Tools]]></title>
            <link>https://hooninedev.com/en/260529</link>
            <guid isPermaLink="false">https://hooninedev.com/en/260529</guid>
            <pubDate>Fri, 29 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[In this post, I want to explore the tooling ecosystem surrounding AI coding agents. As a frontend developer, I use Claude in my day-to-day work. At some point, that meant a CLAUDE.md appeared at the p...]]></description>
            <content:encoded><![CDATA[<p>In this post, I want to explore the <strong>tooling ecosystem surrounding AI coding agents</strong>.</p>
<p>As a frontend developer, I use Claude in my day-to-day work. At some point, that meant a <code>CLAUDE.md</code> appeared at the project root, an <code>AGENTS.md</code> created by someone else sat beside it, a <code>.cursorrules</code> file lingered elsewhere, and I even created a <code>.claude/skills/</code> directory after following an article I had come across. (By the time I stopped to take stock, I had about five files containing roughly the same information.)</p>
<p>The same kind of confusion surfaced in other areas. I added an MCP called <code>serena</code>, installed something called <code>codegraph</code> after seeing it on GitHub Trending, and every time I discovered another new tool, I found myself asking the same question: “Where exactly does this tool fit, and how does it work?” (I was especially hazy, each time, on details such as who built a given tool and exactly how it saved tokens.)</p>
<p>So rather than recommending tools one by one, I decided to <strong>map the tooling landscape itself</strong>. I divided it into four broad axes.</p>
<ul>
<li>The differences among context files (<code>.md</code>)</li>
<li>How MCP works, and Serena</li>
<li>The layers of code intelligence tools, and CodeGraph</li>
<li>Paying attention to GitHub Trending</li>
</ul>
<p>Once you understand these four areas, I believe you can look at a new tool and quickly get a sense of what kind of thing it is.</p>
<h2 id="context-files"><a class="anchor" href="#context-files">Context Files</a></h2>
<p>AI coding agents have a fundamental limitation: <strong>they have no persistent memory</strong>. Every session starts from a blank slate. In the next conversation, they cannot remember a convention you agreed on yesterday or a directory structure you explained an hour ago. Context files are the simplest mechanism for addressing this problem. If a project contains a file that is read automatically whenever a session starts, you no longer need to repeat the same explanation every time.</p>
<p>The problem is that each tool created its own file around the same basic idea. Claude Code reads <code>CLAUDE.md</code>, Cursor reads <code>.cursorrules</code> (now deprecated, with <code>.cursor/rules</code> recommended instead), GitHub Copilot reads <code>.github/copilot-instructions.md</code>, and OpenAI Codex reads <code>AGENTS.md</code>. When a team uses several tools, it can end up copying the same information into four different places.</p>
<h3 id="claudemd"><a class="anchor" href="#claudemd">CLAUDE.md</a></h3>
<p><code>CLAUDE.md</code> is a file that Claude Code reads automatically at the start of a session. According to Anthropic’s official documentation (<code>code.claude.com/docs/en/memory</code>), Claude Code looks for <code>CLAUDE.md</code> at the following three levels.</p>
<ul>
<li><strong>User memory</strong> (<code>~/.claude/CLAUDE.md</code>): global defaults that apply to every project on the machine</li>
<li><strong>Project memory</strong> (<code>CLAUDE.md</code> at the project root): committed to git and shared across the team</li>
<li><strong>Local memory</strong> (<code>CLAUDE.md</code> in a subdirectory): loaded in addition to the others only when working in that directory</li>
</ul>
<p>When all three levels exist, Claude <strong>reads and concatenates all of them</strong>. It does not choose only one according to precedence; instead, more specific instructions are layered on top, much like the CSS cascade. (This is a merge, not an override.) As a result, scattering rules about the same topic across several levels can create conflicts. (Anthropic’s official documentation explicitly states that behavior is not guaranteed when instructions conflict.)</p>
<p>There is one frequently overlooked detail here: Claude <strong>reads every <code>CLAUDE.md</code> it encounters while walking from the current working directory up to the repository root</strong>. If you work inside <code>packages/ui/</code> in a monorepo, both the root <code>CLAUDE.md</code> and <code>packages/ui/CLAUDE.md</code> are loaded. (That is powerful, but it also means the context can quietly expand without you noticing.)</p>
<h3 id="agentsmd"><a class="anchor" href="#agentsmd">AGENTS.md</a></h3>
<p><code>AGENTS.md</code> is a standard created to address the proliferation of tool-specific files described above. In December 2025, Anthropic, Block, and OpenAI donated it to the Linux Foundation’s <strong>Agentic AI Foundation (AAIF)</strong> alongside MCP, making it a de facto industry standard. The official site (<code>agents.md</code>) states that <strong>more than 60,000 open-source repositories have adopted the file</strong>.</p>
<p>The list of supported tools makes the picture even clearer. It includes OpenAI Codex, Google Jules, VS Code, GitHub Copilot, Cursor, JetBrains Junie, Aider, Devin, Zed, Factory, Warp, goose, opencode, Amp, RooCode, Gemini CLI, Kilo Code, Phoenix, Semgrep, Ona, Windsurf, and Augment Code, among many others. GitHub Copilot began supporting <code>AGENTS.md</code> natively in August 2025. One interesting detail is that <strong>native <code>AGENTS.md</code> support in Claude Code is still an active feature request</strong>. Claude Code continues to treat <code>CLAUDE.md</code> as its primary file.</p>
<p>You might still wonder whether this supposed standard is actually being adopted. The strongest evidence is <strong>dogfooding</strong>—the organizations behind the standard using it themselves.</p>
<ul>
<li>The canary branch of <strong>Vercel/Next.js</strong> has an <code>AGENTS.md</code> at its root. It is actually a symbolic link to <code>CLAUDE.md</code>, whose contents cover the monorepo structure, one-to-two-second iteration with <code>pnpm --filter=next dev</code>, testing guidance for both Turbopack and Webpack, the <code>pr-status</code> script, and rules for handling environment variables and secrets. The fact that <code>create-next-app</code> now generates both <code>AGENTS.md</code> and <code>CLAUDE.md</code> for new projects reflects the same trend.</li>
<li>The <strong>OpenAI/codex</strong> repository maintains its own <code>AGENTS.md</code>.</li>
</ul>
<p>A conventional strategy is beginning to emerge: use <strong><code>AGENTS.md</code> as the single source of truth</strong>, while keeping <code>CLAUDE.md</code> minimal, with a one-line reference to <code>AGENTS.md</code> plus instructions specific to Claude Code. This eliminates duplication, and because Claude Code reads both files, nothing is lost.</p>
<h3 id="skillmd"><a class="anchor" href="#skillmd">SKILL.md</a></h3>
<p><code>SKILL.md</code> belongs to a different category from the two files above. Whereas <code>CLAUDE.md</code> and <code>AGENTS.md</code> are <strong>persistent instructions that are always present in the context</strong>, a Skill is an <strong>on-demand capability invoked only when needed</strong>.</p>
<p>A Skill is organized as a directory. The directory contains one <code>SKILL.md</code>, scripts executed by the Skill, and any additional Markdown documents. Claude loads that directory only when the current task matches the Skill’s <code>description</code>. This is called <strong>progressive disclosure</strong>, a concept established in the UX field by Jakob Nielsen in 1995: advanced or rarely used functions are deferred to secondary screens so users can focus on one task at a time, reducing cognitive load and errors. In the context of Claude Skills, it refers to the mechanism of bringing a Skill’s body into the context only when needed. The result can be a dramatic reduction in context-window costs.</p>
<p>The frontmatter in <code>SKILL.md</code> includes several distinctive fields.</p>
<ul>
<li><strong><code>description</code></strong>: explains when the Skill is needed and acts as the trigger the model uses to decide whether to invoke it</li>
<li><strong><code>allowed-tools</code></strong>: restricts which tools may be used inside the Skill (for example, <code>"Read, Glob, Grep, Bash(python:*)"</code>)</li>
<li><strong><code>disable-model-invocation: true</code></strong>: prevents the model from invoking the Skill; only the user can trigger it with a slash command. This is used for operations with side effects, such as deployments and commits</li>
<li><strong><code>user-invocable: false</code></strong>: hides the Skill from the user’s slash-command menu and allows only Claude to invoke it autonomously, for use as background knowledge</li>
</ul>
<p>Claude Skills launched simultaneously across Claude.ai, Claude Code, the API, and Agent SDK on October 16, 2025. Then, on December 18, 2025, Anthropic published the Skills specification itself as an open standard (<code>agentskills.io</code>). Simon Willison even called it “<strong>Skills are awesome, maybe a bigger deal than MCP</strong>,” citing the format’s dramatic simplicity compared with MCP and its use of progressive disclosure to address context-window costs.</p>
<h3 id="files-used-by-other-tools"><a class="anchor" href="#files-used-by-other-tools">Files Used by Other Tools</a></h3>
<p>Cursor’s <code>.cursorrules</code> has been <strong>deprecated since version 0.43</strong>. The current official recommendation is to use the <code>.cursor/rules/</code> directory and place multiple <code>.mdc</code> files inside it. Each <code>.mdc</code> file has YAML frontmatter.</p>
<ul>
<li><strong><code>description</code></strong>: information the agent uses to judge whether the rule is relevant</li>
<li><strong><code>globs</code></strong>: automatically attaches the rule when a matching file is included in the conversation (auto-attach)</li>
<li><strong><code>alwaysApply</code></strong>: when <code>true</code>, includes the rule in every conversation without exception (<code>globs</code> is ignored in this case)</li>
</ul>
<p>GitHub Copilot has evolved in a similar direction. Repository-wide instructions live in <code>.github/copilot-instructions.md</code>; instructions that require path-specific scope go in <code>.github/instructions/*.instructions.md</code>, with globs specified through the frontmatter key <code>applyTo:</code>. (Copilot code review has officially supported path-scoped instructions since September 2025.)</p>
<p>Tools beyond Cursor and Copilot are converging on similar patterns. The following table summarizes them.</p>
<table>
<thead>
<tr>
<th>Tool</th>
<th>File/Directory</th>
<th>Key Characteristics</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Claude Code</strong></td>
<td><code>CLAUDE.md</code> (three levels)</td>
<td>Merged along the directory tree</td>
</tr>
<tr>
<td><strong>Cursor</strong></td>
<td><code>.cursor/rules/*.mdc</code></td>
<td>File-pattern scoping with <code>globs</code></td>
</tr>
<tr>
<td><strong>GitHub Copilot</strong></td>
<td><code>.github/copilot-instructions.md</code> + <code>.github/instructions/*.instructions.md</code></td>
<td>Supports <code>applyTo</code> globs</td>
</tr>
<tr>
<td><strong>Cline</strong></td>
<td><code>.clinerules/</code> directory</td>
<td>Combines all <code>.md</code>/<code>.txt</code> files; conditional activation with <code>paths</code> globs</td>
</tr>
<tr>
<td><strong>Continue.dev</strong></td>
<td><code>.continue/rules/*.md</code></td>
<td><code>name</code>/<code>globs</code>/<code>alwaysApply</code> frontmatter</td>
</tr>
<tr>
<td><strong>Aider</strong></td>
<td><code>CONVENTIONS.md</code> + <code>.aider.conf.yml</code></td>
<td>Included with every request; <strong>200 lines or fewer recommended</strong></td>
</tr>
<tr>
<td><strong>Windsurf</strong></td>
<td><code>.windsurfrules</code> + <code>global_rules.md</code></td>
<td>Two levels: global and project</td>
</tr>
<tr>
<td><strong>Standard</strong></td>
<td><code>AGENTS.md</code> (AAIF)</td>
<td>Adopted by 60,000+ repositories</td>
</tr>
</tbody>
</table>
<p><strong>Aider’s <code>CONVENTIONS.md</code> is particularly interesting</strong>. Because the official documentation says this entire file is included in the context with every request, it explicitly instructs users to <strong>“keep it under 200 lines.”</strong> (In effect, Aider recognized this limitation early and tells users about it directly.)</p>
<h3 id="memorymd"><a class="anchor" href="#memorymd">MEMORY.md</a></h3>
<p>Separate from the files above, another pattern is appearing with increasing frequency: <code>MEMORY.md</code>. It is not an official standard, but an organically developed community convention for <strong>recording decisions and mistakes over time</strong>.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="markdown" data-theme="github-dark github-light"><code data-language="markdown" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold;--shiki-light:#005CC5;--shiki-light-font-weight:bold">## 2026-04-10</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">Pages Router에서 App Router로 이전. 신규 라우트는 App Router 컨벤션 사용.</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold;--shiki-light:#005CC5;--shiki-light-font-weight:bold">## 2026-04-22</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">Prisma 쿼리 결과에 optional chaining 쓰지 말 것 — null은 if-check로 명시적 처리.</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(이전에 옵셔널 체이닝으로 null을 흘려보내 프로덕션 이슈 발생.)</span></span></code></pre></figure>
<p>If <code>CLAUDE.md</code> or <code>AGENTS.md</code> records <strong>the rules as they stand today</strong>, <code>MEMORY.md</code> records <strong>the history of why those rules were created</strong>. (The two are complementary, not interchangeable.)</p>
<h3 id="how-agents-read-these-files"><a class="anchor" href="#how-agents-read-these-files">How Agents Read These Files</a></h3>
<p>So far, we have cataloged the files that exist. But one surprisingly common question remains unanswered: <strong>where, exactly, do agents load these files, and how?</strong> Understanding this mechanism makes it easier to see why the ETH Zurich results discussed later—showing that context-file instructions are not followed reliably—emerged.</p>
<p>First, one essential fact: <strong><code>CLAUDE.md</code> is injected as a user message, not as part of the system prompt.</strong> Anthropic’s official documentation states the following.</p>
<blockquote class="bilingual-quote"><div class="quote-translation" lang="ko"><p>CLAUDE.md content is delivered as a user message after the system prompt, not as part of the system prompt itself. Claude reads it and tries to follow it, but there's no guarantee of strict compliance.</p></div><div class="quote-original" lang="en"><p>CLAUDE.md content is delivered as a user message after the system prompt, not as part of the system prompt itself. Claude reads it and tries to follow it, but there's no guarantee of strict compliance.</p></div></blockquote>
<p>In other words, it is contextual guidance rather than an enforced rule. The official guidance recommends using a separate mechanism, such as a <code>PreToolUse</code> hook, when you need to enforce a specific behavior.</p>
<p>The load order progresses from broad → specific. More precisely, it is managed policy (organization-level settings) → the user’s global file (<code>~/.claude/CLAUDE.md</code>) → the project file (<code>./CLAUDE.md</code>) → the local file (<code>./CLAUDE.local.md</code>). Within the same directory, <code>CLAUDE.md</code> comes before <code>CLAUDE.local.md</code>. By taking advantage of the fact that <strong>the nearest instructions are read last</strong>, you can make more specific rules exert a stronger influence, thanks to the LLM’s recency bias.</p>
<p>The <code>@import</code> syntax is especially interesting. If you put <code>@path/to/file</code> anywhere in the body of CLAUDE.md, that file is expanded in place and loaded with it. <strong>The maximum recursion depth is 4 hops</strong>, and relative paths are resolved from the file containing the import statement. That is why the official recommendation is to bridge to <code>@AGENTS.md</code>. If you leave <code>CLAUDE.md</code> almost empty and put only <code>@AGENTS.md</code> in it, Claude Code will naturally read AGENTS.md as well. (Given that Claude Code does not yet support AGENTS.md natively, this is the cleanest workaround.)</p>
<p>The token implications also deserve attention. CLAUDE.md has no explicit token limit, so <strong>the entire file is loaded if it exists</strong>. The official recommendation, however, is to keep <strong>each file under 200 lines</strong>. Beyond 200 lines, it is said to “consume more context and may reduce adherence.” Interestingly, in Claude 4.x, <strong>merely enabling tool use automatically adds 346 tokens via a special system prompt</strong> (with <code>tool_choice: auto</code>). Context leaks away in ways that are easy to miss.</p>
<p>Cursor takes a different approach. Rules in <code>.cursor/rules/*.mdc</code> operate in four modes.</p>
<ul>
<li><strong>Always Apply</strong>: included in every chat without exception; ignores globs/description</li>
<li><strong>Apply Intelligently</strong> (Agent Requested): the agent reads <code>description</code>, determines relevance, and pulls in the rule</li>
<li><strong>Apply to Specific Files</strong> (Auto Attached): activated when a file matching the glob pattern enters the context</li>
<li><strong>Apply Manually</strong>: explicitly invoked by the user with <code>@rule-name</code></li>
</ul>
<p>Other tools differ again. OpenAI Codex walks from the git repository root toward cwd, collects every <code>AGENTS.md</code>, and injects them <strong>immediately before the user prompt</strong>. GitHub Copilot inserts <code>.github/copilot-instructions.md</code> at a middle priority within the context window: “after edit context and explicit references, but before loosely related open files.” Because the load timing, precedence, and merge rules vary by tool even for the same <code>AGENTS.md</code> file, <strong>there is no guarantee that three tools will interpret it in exactly the same way.</strong></p>
<p>That leaves a more fundamental question: <strong>why does a model follow only some instructions that are present in its context?</strong> Simply saying “because the instructions are long” is not enough. The phenomenon is rooted in structural limitations of LLMs.</p>
<h3 id="hallucination-and-context-forgetting"><a class="anchor" href="#hallucination-and-context-forgetting">Hallucination and Context Forgetting</a></h3>
<p>If you have ever seen an AI agent confuse the conversational context or forget something that was clearly stated earlier, that is a form of <strong>hallucination</strong>. People usually think of hallucination first as “making up facts that do not exist,” but the academic literature divides it into three categories. A 2023 survey by Yue Zhang and colleagues (“Siren’s Song in the AI Ocean”) classifies them as <strong>input-conflicting</strong> (generating something inconsistent with what the user explicitly provided), <strong>context-conflicting</strong> (contradicting something the model previously generated), and <strong>fact-conflicting</strong> (disagreeing with world knowledge). Ignoring instructions in a context file belongs to the <strong>first category</strong>, not the third. The model processes the input while treating part of its information as though it were not there.</p>
<p>The deeper problem is that hallucination <strong>cannot be eliminated in principle</strong>. A research team at the National University of Singapore proved this mathematically using learning theory. No LLM can learn every computable function; therefore, as long as it is used as a general-purpose problem solver, it must hallucinate at some point.</p>
<p>Position effects matter as well. Stanford researchers demonstrated experimentally that models reference relevant information most effectively when it appears <strong>at the beginning or end of the context window</strong>, while performance drops sharply when that information is <strong>buried in the middle</strong>. This maps directly onto context files. <code>CLAUDE.md</code> is inserted somewhere in the middle by the load order, and as a conversation grows longer, its instructions are pushed further into the “middle” of the context. This also connects to the other side of the recency bias mentioned earlier: within the primacy-recency effect, <strong>the middle is the weakest region</strong>.</p>
<p>Taken together, these phenomena form a coherent picture. A context file is merely <strong>additional text inserted outside the system prompt before the LLM’s first user turn</strong>. It is not a mechanism that forces the model’s decisions; it is simply another block of tokens dropped into the context window. The longer the file and the longer the conversation, the farther its instructions drift toward the “middle,” and the less reliably they are referenced. The ETH Zurich results quantitatively confirm this structural limitation.</p>
<h3 id="the-eth-zurich-study"><a class="anchor" href="#the-eth-zurich-study">The ETH Zurich Study</a></h3>
<p>Many people may have thought, “Then I should put as much as possible into these files.” A recent study directly challenges that intuition: the ETH Zurich research referenced throughout the preceding section.</p>
<p>The paper, “Evaluating AGENTS.md: Are Repository-Level Context Files Helpful for Coding Agents?”, was published by an ETH Zurich research team in February 2026. The researchers evaluated four agents—Claude Code (Sonnet-4.5), Codex (GPT-5.2 / GPT-5.1 mini), and Qwen Code—on a benchmark of 138 real-world Python software-engineering tasks (AGENTBENCH) and on SWE-bench Lite. The results were unexpected.</p>
<ul>
<li><strong>LLM-generated context files</strong> actually <strong>reduced task success rates</strong> by about 0.5% on SWE-bench Lite and 2% on AGENTBENCH</li>
<li>Even <strong>human-authored files</strong> produced only a modest average improvement of about 4%</li>
<li>Adding a context file <strong>increased inference costs by more than 20% per instance</strong></li>
<li>Context files had an even smaller effect on the stronger model (GPT-5.2), because more capable models already have sufficient parametric knowledge and additional context can become noise</li>
</ul>
<p>There was one exception: <strong>specifying a nonstandard tool</strong>. When the Python package manager <code>uv</code> was named in the context, for example, the agent’s use of <code>uv</code> rose from 0.01 times per instance to 1.6 times—an increase of <strong>roughly 160×</strong>.</p>
<p>Aider’s “200-line recommendation”—keep the file short because it enters the context every time—is practical guidance, while the ETH Zurich study quantitatively demonstrates that long context files reduce performance statistically. I see the following practical implications in the research.</p>
<ul>
<li><strong>A huge, automatically generated context file can do more harm than good</strong>. If you cram coding standards, architecture, and workflows into a 300-line <code>CLAUDE.md</code>, the agent will follow some of them and ignore the rest. That inconsistency can produce worse results than having no context at all.</li>
<li><strong>What absolutely belongs in the file is information that cannot be inferred</strong>. This includes nonstandard tools, project-specific conventions, and past failure cases. The model already knows general coding best practices.</li>
<li>Use AGENTS.md as the single source, keep CLAUDE.md limited to brief tool-specific instructions, and move detailed workflows into Skills.</li>
</ul>
<h2 id="mcp-model-context-protocol"><a class="anchor" href="#mcp-model-context-protocol">MCP (Model Context Protocol)</a></h2>
<p>If <code>.md</code> files solve the question of “what should we tell the agent?”, MCP (Model Context Protocol) solves the question of “<strong>what should we enable the agent to do?</strong>”</p>
<p>Put more concretely, an AI agent needs to be able to call the Slack API to send a message to Slack. It needs to call the GitHub API to create a GitHub issue. It needs to handle a database connection to query Postgres. MCP <strong>unifies integrations with all these external systems under a single standard protocol</strong>. (The point is that any client can connect to any server through the same interface.)</p>
<p>MCP is an open standard first released by Anthropic on <strong>November 25, 2024</strong>. Then, on <strong>December 9, 2025</strong>, Anthropic, Block, and OpenAI jointly donated the MCP specification to the Linux Foundation’s <strong>Agentic AI Foundation (AAIF)</strong> as founding members. Google, Microsoft, AWS, Cloudflare, and Bloomberg joined as platinum members. (By the time of the December 2025 donation, the SDK had already surpassed 97 million monthly downloads, with more than 10,000 active public MCP servers.)</p>
<p>MCP is a stateful session protocol built on JSON-RPC. <strong>JSON-RPC</strong> is a stateless, lightweight RPC (Remote Procedure Call) protocol that uses JSON as its wire format. It is transport-independent and can run over HTTP, TCP, or standard input/output. It also supports notifications (calls without responses) and batch calls.</p>
<h3 id="inside-the-protocol"><a class="anchor" href="#inside-the-protocol">Inside the Protocol</a></h3>
<p>Every interaction between an MCP client and server is expressed through one of six primitives. The protocol began with three server-side primitives; three client-side primitives were added in the 2025-06-18 specification, bringing the standard total to six.</p>
<p><strong>Server-side primitives</strong></p>
<ul>
<li><strong>Tool</strong> (model-controlled): an action the model autonomously decides whether to invoke. Such actions may have side effects</li>
<li><strong>Resource</strong> (application-controlled): read-only data identified by a URI. The host application decides which resources to expose</li>
<li><strong>Prompt</strong> (user-controlled): a reusable template explicitly triggered by the user, for example through a slash command</li>
</ul>
<p><strong>Client-side primitives</strong></p>
<ul>
<li><strong>Sampling</strong>: a mechanism that allows the server to request a completion from the client’s LLM, making the client-server architecture bidirectional</li>
<li><strong>Roots</strong>: workspace boundary information through which the client tells the server, “This is the extent of the area you may work in”</li>
<li><strong>Elicitation</strong>: a feature that allows the server to request additional user input in a structured form while executing a tool</li>
</ul>
<p>The distinction among these six primitives matters because <strong>authority over invocation and provision belongs to different actors</strong>. A Tool runs at the model’s discretion, so an incorrect invocation carries risk. A Resource is curated by the application and is therefore relatively safe. A Prompt is explicitly triggered by the user and offers the greatest control. Sampling, Roots, and Elicitation refine the permission model through client-side control.</p>
<p>There are <strong>exactly two transport mechanisms</strong>. This is intentional: it prevents the ecosystem from fragmenting into dozens of competing protocols. The first is <strong>stdio</strong>, which runs an MCP server as a local subprocess and communicates through standard input and output. It is well suited to locally operating tools such as filesystem and git integrations. The second is <strong>Streamable HTTP</strong>, which layers SSE streaming over HTTP POST to create near-bidirectional communication. It is suited to scenarios that occur across a network boundary, such as remote servers, OAuth authentication, multi-client connections, and cloud deployments.</p>
<p>Here, SSE (Server-Sent Events) is a W3C standard that lets a server push one-way data to a client over an HTTP connection. Its media type is <code>text/event-stream</code>, and JavaScript accesses it through the <code>EventSource</code> API. Unlike WebSocket, it is unidirectional, but because it operates over HTTP, it works well with proxies and firewalls. Streamable HTTP effectively uses SSE to approximate bidirectional communication. It was introduced in the <strong>March 26, 2025</strong> specification (version <code>2025-03-26</code>), replacing the previous HTTP+SSE transport.</p>
<h3 id="how-an-llm-invokes-an-mcp-tool"><a class="anchor" href="#how-an-llm-invokes-an-mcp-tool">How an LLM Invokes an MCP Tool</a></h3>
<p>Now that we have covered the primitives and transports, let us trace <strong>how an LLM actually discovers and invokes an MCP tool</strong>. (Where <code>.md</code> files raised the question of “where are they injected?”, the question here is “how does MCP enter the LLM’s field of view?”)</p>
<p>When an MCP session begins, the following handshake takes place.</p>
<ul>
<li><strong>Client → server</strong>: <code>initialize</code> request (sends the supported protocol version and client capabilities)</li>
<li><strong>Server → client</strong>: <code>initialize</code> response (server capabilities plus an optional <code>instructions</code> field)</li>
<li><strong>Client → server</strong>: <code>notifications/initialized</code> notification</li>
<li><strong>Client → server</strong>: <code>tools/list</code> request → receives the list of available tools</li>
<li>(Later) The LLM decides to invoke a tool → the client sends <code>tools/call</code> → receives the result</li>
</ul>
<p>One detail is frequently overlooked: the <strong><code>initialize</code> response’s <code>instructions</code> field</strong>. If the server sends text in this field, that content is effectively added to the LLM’s system prompt. In other words, the specification provides a formal slot through which an MCP server can inject guidance directly into the LLM about how its tools should be used. (The existence of this slot is one reason the Tool Poisoning Attack discussed earlier is dangerous.)</p>
<p>How, then, does the tool definition itself enter the LLM’s field of view? An MCP tool definition takes the following JSON Schema form.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="json" data-theme="github-dark github-light"><code data-language="json" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">  "name"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"get_weather"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">  "description"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"Get current weather information for a location"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">  "inputSchema"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: {</span></span>
<span data-line=""><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">    "type"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"object"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">    "properties"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: { </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">"location"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: { </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">"type"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"string"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> } },</span></span>
<span data-line=""><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">    "required"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: [</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"location"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">]</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  }</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>The client transforms the list returned by <code>tools/list</code> into the <strong><code>tools</code> parameter of the Anthropic Messages API</strong> or the <strong><code>tools</code> parameter of OpenAI function calling</strong>, then includes it in the LLM API request. With Anthropic, supplying the tool parameter automatically adds a <strong>special system prompt</strong> that teaches the model how to invoke tools. (That is the source of the additional 346 tokens mentioned earlier.)</p>
<p>When the LLM decides that it should invoke a tool, its response contains a <code>tool_use</code> block (<code>{"type": "tool_use", "name": ..., "input": ...}</code>), and the response ends with a <code>stop_reason</code> of <code>tool_use</code>. The client receives this, sends <code>tools/call</code> to the actual MCP server, receives the result, places it in a <code>tool_result</code> block in the next user message, and sends it back to the LLM. <strong>This loop continues until <code>stop_reason</code> changes from <code>tool_use</code> to another value, such as <code>end_turn</code> or <code>max_tokens</code>.</strong> What we commonly describe as “the agent working” is essentially a sequence of these call-result-call loops.</p>
<p>So how does MCP differ from ordinary function calling? The distinction can be condensed into four points.</p>
<ul>
<li><strong>Dynamic discovery</strong>: the tool list is retrieved at runtime through <code>tools/list</code> rather than known at build time. <code>notifications/tools/list_changed</code> also allows it to change during a session</li>
<li><strong>Stateful session</strong>: lifecycle phases are defined (initialize → operation → shutdown), enabling a clean shutdown</li>
<li><strong>Primitives beyond Tool</strong>: Prompt, Resource, Sampling, Roots, and Elicitation are exposed through capability negotiation</li>
<li><strong>Bidirectionality</strong>: the specification allows a server to invoke the client’s LLM in reverse through sampling</li>
</ul>
<p>(This is why MCP is sometimes described as “a generalized standard for function calling for agents.”)</p>
<h3 id="serena"><a class="anchor" href="#serena">Serena</a></h3>
<p><strong>Serena</strong> (<code>oraios/serena</code>) is one of the most frequently discussed MCP servers in the context of coding agents. As of May 2026, it has about 24.7k stars and has risen in roughly a year from a niche tool to a de facto standard code MCP.</p>
<p>Serena’s core idea can be summarized in one sentence: <strong>show the agent symbols, not text.</strong></p>
<p>To unpack that idea, suppose you need to find every use of a <code>calculateTotal</code> function. A conventional text-based tool such as grep or Read works like this.</p>
<p>It greps the entire codebase for <code>calculateTotal</code>. It then gathers the line number of every match and reads a fixed range of lines from each file to build context. It also captures accidental matches in variable names, string literals, and comments.</p>
<p>LSP-based Serena makes a single call to <code>find_referencing_symbols("calculateTotal")</code> and returns only exact symbol references, without noise from variable-name or comment matches.</p>
<p><strong>LSP (Language Server Protocol)</strong> is an open, JSON-RPC-based protocol that standardizes communication between code editors/IDEs and “language intelligence tools” such as code completion, go to definition, find references, and refactoring. Microsoft, Red Hat, and Codenvy jointly standardized it in 2016. Its central idea is simple: instead of reimplementing a language analyzer for every editor, run one server per language and let every editor query that server. (The TypeScript server, Rust analyzer, and Python’s pyright are all LSP servers.)</p>
<p>Serena’s core tools include <code>find_symbol</code>, <code>find_referencing_symbols</code>, and <code>get_symbols_overview</code>. You can choose between two backends: the default is an LSP-compatible language server, which is free and open source; the other is a paid plugin that uses code analysis from a JetBrains IDE and offers a free trial.</p>
<p>The real reason Serena was adopted so quickly is <strong>token savings</strong>. A text grep-and-file-read loop consumes many tokens, while one precise LSP call consumes very few. The larger the codebase, the greater the difference.</p>
<h3 id="so-is-mcp-safe"><a class="anchor" href="#so-is-mcp-safe">So, Is MCP Safe?</a></h3>
<p>One point needs to be made clearly: <strong>MCP does not automate authorization</strong>. The user remains responsible for deciding which servers an agent can trust, what side effects each tool may have, and whether a tool will continue to behave the same way over time.</p>
<p>It is useful to understand two representative attacks.</p>
<ul>
<li>
<p><strong>Tool Poisoning Attack (TPA)</strong>: an attack named and demonstrated in a PoC by Invariant Labs in April 2025. If malicious instructions are hidden inside an MCP server’s tool description, the model may mistake them for user instructions and follow them. The text is invisible to the user but visible to the model.</p>
</li>
<li>
<p><strong>Rug Pull</strong> (Silent Redefinition): a concept Simon Willison discussed in an analysis published on April 9, 2025. A tool begins as legitimate. The user reviews it, approves it, and integrates it into a workflow. Weeks later, the tool definition quietly changes to include malicious instructions. Because the user is not asked to approve it again, the behavior changes without warning.</p>
</li>
</ul>
<p>A security incident occurred on <strong>April 15, 2026</strong>. OX Security disclosed systemic RCE vulnerabilities affecting every major MCP SDK—Python, TypeScript, Java, and Rust. More than 150 million downloads, roughly 7,000 public servers, and an estimated 200,000 vulnerable deployments were potentially affected. More than 14 CVEs were assigned, and Cursor, VS Code, Windsurf, Claude Code, and Gemini-CLI were all affected.</p>
<p>How has the ecosystem responded? Anthropic <strong>did not change the protocol architecture itself</strong>. Instead, it updated <code>SECURITY.md</code> to state explicitly that downstream developers are responsible for input sanitization when using stdio adapters. At the specification level, the <strong>2025-06-18 revision mandated OAuth 2.1 plus RFC 8707 Resource Indicators</strong> to block token-reuse attacks, and the <strong>2025-11-25 revision introduced incremental scope consent</strong>, through which users approve only the minimum permissions needed, one step at a time. Even so, more than 30 MCP-related CVEs were issued in January and February 2026 alone, and statistics showed that <strong>command injection accounted for 43%</strong> of them. <strong>Security remains very much a work in progress.</strong></p>
<h2 id="code-intelligence-tools"><a class="anchor" href="#code-intelligence-tools">Code Intelligence Tools</a></h2>
<p>If <code>.md</code> files answer “what should we tell the agent?” and MCP answers “what should we enable it to do?”, code intelligence tools answer the question of “<strong>how can we find relevant code quickly?</strong>”</p>
<p>In a large codebase, most of an AI agent’s cost comes not from changing code but from <strong>finding where the relevant code lives</strong>. If every task begins with a repeated grep → read → filter → grep cycle, tokens, time, and tool calls are wasted. Code intelligence tools represent a variety of attempts to reduce this search cost.</p>
<p>The landscape becomes clearer when divided into four tiers.</p>
<h3 id="context-packing"><a class="anchor" href="#context-packing">Context Packing</a></h3>
<p>The simplest solution begins with the idea: “<strong>Put everything into one context window.</strong>” It builds no graph and performs no indexing. It simply serializes the entire repository into a block of text and hands the whole thing to the model.</p>
<p>The representative tool is <strong>Repomix</strong>. It packages an entire repository into a structure optimized for Claude’s XML parsing. With a CLI, web interface, extension, and MCP server, it has the most complete ecosystem in the category.</p>
<p><strong>GitIngest</strong> is known for its zero-friction usability. Change the single word <code>github.com</code> to <code>gitingest.com</code> in a GitHub URL, and the entire repository is transformed into one text page. (For example, <code>github.com/facebook/react</code> → <code>gitingest.com/facebook/react</code>.) Changing one word in the browser’s address bar is all it takes, with no installation required. It is optimized for quick, one-off exploration.</p>
<p><strong>code2prompt</strong>, created by Mufeed VH, is a Rust-based CLI whose strength lies in customization through a template system.</p>
<p>An interesting variant is <strong>rtk</strong> (<code>rtk-ai/rtk</code>, about 55k stars). Whereas the tools above “pack the entire repository at once,” rtk <strong>compresses the output of CLI commands in real time</strong>. It is a single binary written in Rust that automatically registers itself with the shell hooks of 13 tools, including Claude Code, Cursor, Copilot, Gemini CLI, and Codex. When an agent invokes <code>git status</code>, the hook rewrites it internally as <code>rtk git status</code>. (The fact that users do not need to change their workflows is its key differentiator.) It applies smart filtering, grouping, truncation, and deduplication heuristics to more than 100 commands, reducing output tokens by 60–90%. One sentence from the official site neatly summarizes this category: <em>“70% of your bill is noise the LLM doesn't need.”</em> While the earlier tools reduce the volume of context going in, rtk reduces the volume of context returned by tool calls.</p>
<p>The limitation of this tier is clear, however: <strong>large repositories hit token limits</strong>. And because code is delivered only as a “block of text,” there is no structural understanding of relationships among symbols.</p>
<h3 id="tree-sitter-repository-maps"><a class="anchor" href="#tree-sitter-repository-maps">tree-sitter Repository Maps</a></h3>
<p>The next tier uses <strong>tree-sitter</strong> to analyze code structure without running a separate index server.</p>
<p>An <strong>AST (Abstract Syntax Tree)</strong> is a data structure that represents source-code structure as a tree. It is the output of a compiler’s parsing stage: superficial details such as whitespace, semicolons, and parentheses are removed, while meaningful elements such as variables, operators, function calls, and control flow remain as nodes. Every precise form of analysis in code intelligence ultimately operates on an AST.</p>
<p><strong>tree-sitter</strong> is an open-source parser generator and incremental parsing library. It has been adopted by GitHub’s code navigation, Neovim, Zed, and Helix. Its key differentiator is that <strong>it reparses only the edited region</strong>. When you change a single line in an editor, it patches only the changed tree instead of parsing the entire file again. This makes it responsive and well suited to rapid code exploration by AI agents.</p>
<p><strong>Aider</strong>, discussed earlier, is a representative example of this approach. It uses tree-sitter to extract symbol definitions such as functions, classes, and methods from source files; constructs a graph with files as nodes and inter-file dependencies as edges; applies a PageRank-family ranking algorithm—which measures a page’s importance by the number and quality of links pointing to it—and extracts only the most important definitions and signatures within a token budget. (By default, <code>--map-tokens=1024</code> creates a 1k-token repository map.)</p>
<p><strong>AFT</strong> (<code>cortexkit/aft</code>) develops this approach with greater precision. In the words of AFT’s official README: <strong>“Reading a 500-line file costs about 375 tokens. But when an agent needs only one function most of the time, passing the symbol name to <code>aft_zoom</code> returns only that function and a little context. This costs about 40 tokens.”</strong> Line-number-based editing breaks as soon as code above the target moves, whereas AFT’s symbol-mode editing is stable because it addresses functions by name.</p>
<p>Another tool worth noting in the same tier is <strong>ast-grep</strong> (<code>ast-grep/ast-grep</code>, about 13.9k stars). It is a tree-sitter-based structural search and rewriting CLI. Its decisive difference from ordinary grep is that it matches CST (Concrete Syntax Tree) patterns rather than text. A search for the pattern <code>console.log($A)</code>, for example, finds every call with the same semantic structure, regardless of how the text is formatted. A separate <code>ast-grep-mcp</code> server also lets AI agents use structural search instead of text grep.</p>
<h3 id="knowledge-graph"><a class="anchor" href="#knowledge-graph">Knowledge Graph</a></h3>
<p>The third tier goes a step further. It <strong>parses the entire codebase in advance, builds a knowledge graph, stores it on disk</strong>, and lets the agent query that stored graph. The most talked-about example is a tool called <strong>CodeGraph</strong>.</p>
<p>Its architecture is surprisingly simple. Code is parsed with <strong>tree-sitter</strong>; the extracted symbols, edges, and file information are stored in SQLite FTS5 full-text search; and the resulting knowledge graph is exposed to AI agents through MCP. One important detail is that <strong>all extraction is performed deterministically through AST parsing, not through LLM summarization</strong>. In other words, there is no room for hallucination to enter the process.</p>
<p><strong>FTS5 (SQLite Full-Text Search 5)</strong>, mentioned above, is a full-text search extension provided as a SQLite virtual table. It has been included in the SQLite amalgamation since SQLite 3.9.0 (2015-10-14). You create a table with <code>CREATE VIRTUAL TABLE ... USING fts5(...)</code> and query it with the <code>MATCH</code> operator. Its decisive advantage is that you can operate a full-text index in a single SQLite file without running a separate search engine such as Elasticsearch. This is one reason CodeGraph can advertise “100% local operation.”</p>
<p>The <strong>deterministic parsing</strong> just mentioned refers to parsing algorithms that allow only one choice at each stage, without backtracking. LL(1) and LR parsers are representative examples and run in linear time. In the context of CodeGraph, it means that “the symbol relationships extracted from the AST are mathematically precise rather than based on an LLM’s interpretation.” An LLM-generated code summary can hallucinate, while direct AST parsing yields <strong>mathematically precise symbol relationships</strong>; this principle is central to the approach.</p>
<p>The benchmarks are impressive as well. In a comparison of headless Claude Opus 4.7 runs with and without the CodeGraph MCP enabled, the averages reported in the official README show costs falling by <strong>35%</strong>, token usage by <strong>57%</strong>, runtime by <strong>46%</strong>, and tool calls by <strong>71%</strong>. The gains increase with codebase size: on a large repository such as Tokio, the measurements showed an 82% reduction in cost, an 86% reduction in tokens, a 71% improvement in speed, and a 92% reduction in tool calls. (Without CodeGraph, an agent fans out widely across grep/find/Read; with CodeGraph, a single index query replaces all of that.)</p>
<p>The approach also has deep academic roots. <strong>GraphCoder</strong> (ASE 2024) created a Code Context Graph combining control flow with data/control dependence. <strong>CodexGraph</strong> (NAACL 2025) enabled an LLM agent to write and execute graph-database queries directly. <strong>Prometheus</strong> combined a tree-sitter-based knowledge graph with unified memory and applied it to multilingual issue resolution. Academia and industry are clearly converging on this pattern.</p>
<p>One interesting variant deserves mention here. <strong>Cursor’s indexing</strong> takes a different path: semantic search based on vector embeddings rather than an AST graph. Locally, it splits files into chunks at function and class boundaries, synchronizes them with the server through Merkle tree hashes, and stores only the embeddings in a vector database called Turbopuffer. (Its central privacy claim is that the original source code is not stored in the cloud.) At query time, it embeds the question, runs a nearest-neighbor search, then locally reads the file paths and line ranges returned by the search and sends that content to the LLM. Because it seeks <strong>“semantically related code” rather than “exact symbols,”</strong> it has lower precision but performs well with natural-language queries. CodeGraph and Cursor indexing solve the same problem—search cost—from different assumptions.</p>
<h3 id="lsp"><a class="anchor" href="#lsp">LSP</a></h3>
<p>The final tier <strong>depends directly on a language server</strong>. tree-sitter knows “that a symbol exists”; LSP knows “what that symbol is.”</p>
<p>Consider a concrete example. A TypeScript LSP knows that <code>UserService</code> implements the <code>IUserService</code> interface, which generic type parameters it accepts, which overloads it has, and what its return type is. tree-sitter cannot go that far.</p>
<p><strong>Serena</strong>, discussed in the MCP section, belongs precisely to this tier. Because Aider does not use LSP and performs its own file analysis, its recognition is limited to the function and class level. By contrast, an LSP integration such as the one in <strong>OpenCode</strong> provides deeper type awareness, though it is limited by its dependence on a good LSP server for each language.</p>
<h2 id="github-trending"><a class="anchor" href="#github-trending">GitHub Trending</a></h2>
<p><img src="/content/260529/1.webp" alt="AI coding agent tools and code intelligence flow" width="3702" height="2034" loading="eager" fetchpriority="high" decoding="async"></p>
<p>Finally, <strong>GitHub Trending</strong> is where I first discovered many of the tools discussed above. It offers an at-a-glance view of who is building what and which projects are suddenly gaining traction.</p>
<p>At <code>github.com/trending</code>, you can browse three time ranges: today, this week, and this month. You can also filter by language and category. (I usually look at weekly results for TypeScript and Python, occasionally expanding to all languages.)</p>
<p>One interesting pattern I noticed while tracking Trending over the past few weeks is that <strong>this quarter’s leading repositories form clear clusters</strong>. Understanding those clusters makes it easier to place individual tools in context.</p>
<h2 id="so-what-does-this-mean"><a class="anchor" href="#so-what-does-this-mean">So, What Does This Mean?</a></h2>
<p>The thought that occurred to me most often while writing this article was that <strong>the number of tools is growing extraordinarily fast</strong>. Even as I wrote, new MCP servers appeared on GitHub Trending, the status of AGENTS.md support changed, and new security CVEs were issued. Half-finished paragraphs quickly becoming outdated is a familiar fate of technical writing, but the pace of the AI agent ecosystem is unusually steep.</p>
<p>That is why my goal here was not to recommend a particular tool, but to develop <strong>an eye for the relationships among tools</strong>. Once you understand why CLAUDE.md is injected as a user message, exactly how MCP differs from function calling, and why tree-sitter and LSP occupy different tiers, a new tool becomes much easier to read: “This belongs to that tier, and it solves this problem in this way.”</p>
<p>What remains, ultimately, is one intuition from the ETH Zurich study: <strong>the model already knows a great deal</strong>. Stuffing a context file with everything you can think of does not make the agent follow it more faithfully. It is better to keep only what the model is unlikely to know—project-specific conventions, nonstandard tools, and past mistakes—and remove the rest. Installing more tools and using tools well are different problems.</p>
<p>Rather than immediately adding ten MCP servers or expanding CLAUDE.md to hundreds of lines, I encourage readers to spend some time investigating how the tools they already use actually work. I believe that understanding provides a stable foundation, no matter which direction the ecosystem takes next.</p>
<h2 id="references"><a class="anchor" href="#references">References</a></h2>
<details class="ref-block"><summary class="ref-summary">참고 자료</summary><div class="ref-list">
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#3b82f6"></span><a href="https://code.claude.com/docs/en/memory" target="_blank" rel="noopener noreferrer">Claude Code Memory, Anthropic</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#3b82f6"></span><a href="https://modelcontextprotocol.io/specification/2025-06-18/basic/lifecycle" target="_blank" rel="noopener noreferrer">MCP Specification 2025-06-18</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#3b82f6"></span><a href="https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview" target="_blank" rel="noopener noreferrer">Anthropic Tool Use Overview</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#3b82f6"></span><a href="https://cursor.com/docs/context/rules" target="_blank" rel="noopener noreferrer">Cursor Rules Documentation</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#8b5cf6"></span><a href="https://arxiv.org/abs/2602.11988" target="_blank" rel="noopener noreferrer">ETH Zurich, "Evaluating AGENTS.md" (2602.11988)</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#8b5cf6"></span><a href="https://arxiv.org/abs/2309.01219" target="_blank" rel="noopener noreferrer">Yue Zhang et al., "Siren's Song" (2309.01219)</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#8b5cf6"></span><a href="https://arxiv.org/abs/2401.11817" target="_blank" rel="noopener noreferrer">Ziwei Xu et al., "Hallucination is Inevitable" (2401.11817)</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#8b5cf6"></span><a href="https://arxiv.org/abs/2307.03172" target="_blank" rel="noopener noreferrer">Nelson F. Liu et al., "Lost in the Middle" (2307.03172)</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#f59e0b"></span><a href="https://simonwillison.net/2025/Oct/16/claude-skills/" target="_blank" rel="noopener noreferrer">Simon Willison, "Claude Skills are awesome"</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#f59e0b"></span><a href="https://simonwillison.net/2025/Apr/9/mcp-prompt-injection/" target="_blank" rel="noopener noreferrer">Simon Willison, MCP Prompt Injection</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#f59e0b"></span><a href="https://www.ox.security/blog/mcp-supply-chain-advisory-rce-vulnerabilities-across-the-ai-ecosystem/" target="_blank" rel="noopener noreferrer">OX Security, MCP Supply Chain Advisory</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#10b981"></span><a href="https://github.com/rtk-ai/rtk" target="_blank" rel="noopener noreferrer">rtk-ai/rtk</a></div>
</div>
</div></details>]]></content:encoded>
            <author>jihoon7705@gmail.com (이지훈)</author>
            <category>AI</category>
            <category>Developer</category>
            <category>Tools</category>
            <category>Claude</category>
            <category>MCP</category>
            <category>CodeGraph</category>
        </item>
        <item>
            <title><![CDATA[State Management]]></title>
            <link>https://hooninedev.com/en/260518</link>
            <guid isPermaLink="false">https://hooninedev.com/en/260518</guid>
            <pubDate>Mon, 18 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[In this post, I want to talk about State Management. This is not a library comparison. Rather than deciding which tool is better, the goal is to develop a feel for how to think about state and where t...]]></description>
            <content:encoded><![CDATA[<p>In this post, I want to talk about <strong>State Management</strong>. This is not a library comparison. Rather than deciding which tool is better, the goal is to develop a feel for <strong>how to think about</strong> state and <strong>where to draw its boundaries</strong>.</p>
<p>AI tools such as Claude, ChatGPT, Cursor, Gemini, and Copilot have become deeply embedded in our daily work. Development has accelerated exponentially, but if I am honest, the quality of the services we build does not seem to have kept pace. We are increasingly confronted with as many new bugs as new features, and I often hear people say, "I have no idea why this ended up this way."</p>
<p>As development speeds up, we spend less time examining every line of code in detail. That makes it even more important, in my view, to have the <strong>fundamental skills needed to guide AI in the right direction</strong>. To preserve the quality of the result, we need to recognize problems in AI-generated code and steer it back toward the outcome we want. Those fundamentals may include domain-driven development, abstraction, TDD (Test-Driven Development), effective use of libraries, and building performance advantages.</p>
<p>Yet whenever I ask frontend colleagues—and colleagues in other IT roles—"What is the hardest part of frontend development?" the answer I hear most often is remarkably consistent: <strong>"Managing state flow."</strong></p>
<p>This article explores why managing state flow is so difficult and what kind of judgment and intuition we need to cultivate to handle it well.</p>
<h2 id="what-is-state"><a class="anchor" href="#what-is-state">What Is State?</a></h2>
<p>Before getting into the main discussion, let us begin with the most fundamental question: what exactly do we mean by "state"?</p>
<p>While studying frontend development, I often read articles by <a href="https://blog.hoseung.me/2021-12-05-state-management" target="_blank" rel="noopener noreferrer">hoseung.me</a>. There, state is described as <strong>"any data that can affect the UI."</strong> A like count, the contents of a shopping cart, whether a modal is open, an input value, information about the signed-in user, the currently selected tab, search results, and whether something is loading—all of these are state.</p>
<p>The official React documentation defines it more formally. The page is titled <a href="https://react.dev/learn/state-a-components-memory" target="_blank" rel="noopener noreferrer">"State: A Component's Memory"</a>, which can be understood as <strong>"the mechanism by which a component retains data between renders and tells React to trigger a re-render when that data is updated."</strong> In other words, it is data that persists over time, changes in response to events, and causes the UI to be redrawn when it changes. One more important point: state is <strong>isolated to each component instance.</strong> Even if the same component appears ten times on a page, each instance has its own independent state. This fact connects directly to the later discussion of where state should live.</p>
<p>Both definitions point to the same idea: state is <strong>"a value that changes over time and affects rendering."</strong> A constant that never changes is not state. Primitive design tokens fixed at build time are not state, but a dark mode setting that the user can toggle is. (Strictly speaking, the values themselves resolve according to the dark or light theme state. The "theme selection" is the state, while the tokens are more accurately understood as a mirror reflecting that state.)</p>
<p>There is one important caveat: <strong>not all state lives in components.</strong> Some state lives in cookies; some in localStorage, sessionStorage, or IndexedDB; and some in the URL. When server-resident data is brought to the client and cached, that cache also becomes a form of state. Even browser-managed values such as scroll position and the history stack sometimes need to be treated as state because they determine how our application behaves.</p>
<h2 id="why-is-it-so-difficult"><a class="anchor" href="#why-is-it-so-difficult">Why Is It So Difficult?</a></h2>
<p>At first, handling state sounds simple enough. Create the state you need, pass it where it is needed, and correctly handle updates and resets. What could be so difficult about that?</p>
<p>Keep that question in mind and open a page from the service you are currently working on.</p>
<p>How many components does that page contain? Even a simple page may consist of dozens of components, while a complex one may contain hundreds arranged in a tree. Each component may hold its own state, share state with a sibling, or receive state from a parent. State can also carry across pages. Some state must survive a refresh, while other state should disappear when the tab closes.</p>
<p>This is the real reason state is difficult to manage: <strong>we cannot see at a glance where countless pieces of state are declared, how they are updated, or when they cease to exist.</strong> As the number of components with similar roles grows, both naming state and tracing the code that changes it become increasingly difficult.</p>
<p>An invisible web begins to form. A click in component A invalidates B's data; invalidating B closes C's UI; and closing C discards the form input. If that chain is not explicit anywhere in the code, we have to reconstruct the web in our heads whenever we debug a bug.</p>
<p>How, then, should we untangle this web? I believe the first step is recognizing that <strong>"state comes in different kinds."</strong></p>
<h2 id="not-all-state-is-the-same"><a class="anchor" href="#not-all-state-is-the-same">Not All State Is the Same</a></h2>
<p><a href="https://kentcdodds.com/blog/application-state-management-with-react" target="_blank" rel="noopener noreferrer">Kent C. Dodds</a> divides state into <strong>Server Cache</strong> (information that exists on the server and is held by the client for fast access) and <strong>UI State</strong> (information that exists only in the UI to control interface behavior). We often make mistakes when we lump the two together.</p>
<p>The <a href="https://tanstack.com/query/latest/docs/framework/react/guides/does-this-replace-client-state" target="_blank" rel="noopener noreferrer">official TanStack Query documentation</a> defines TanStack Query as a server-state library that manages asynchronous work between the server and the client, while tools such as Redux, MobX, and Zustand are client-state libraries. (They can store asynchronous data, but doing so is inefficient.)</p>
<p>The central point is clear: <strong>Server State and Client State are different problems.</strong> Server State is asynchronous, can be changed by other users, and becomes stale over time. Client State is synchronous, under our control, and disappears on refresh. (More precisely, when the page unloads, <strong>the JavaScript runtime restarts, and the component tree and its state in heap memory are reclaimed together.</strong> When the component mounts again, it starts over from the initial value of <code>useState</code>.) If we try to handle both with the same tool, we must implement patterns such as cache invalidation, background refetching, and optimistic updates ourselves.</p>
<p>I take this one step further and divide frontend state into <strong>seven categories</strong>. To be clear, these seven categories do not separate neatly along a single axis. They mix storage location, source, lifecycle, and role, so a single piece of state may belong to several categories at once. Rather than treating this as a perfect taxonomy, think of the categories as <strong>questions to ask when deciding how state should be managed</strong>.</p>
<ul>
<li><strong>Local State</strong> — State used only within one component or a narrow subtree</li>
<li><strong>Global State</strong> — State that must be shared across the application</li>
<li><strong>Server State</strong> — State whose source of truth is the server and whose client-side copy is a cache</li>
<li><strong>Form State</strong> — Temporary state that exists while the user is entering data</li>
<li><strong>URL State</strong> — Shareable state that lives in the address bar and survives refreshes</li>
<li><strong>External State</strong> — State outside React, such as cookies, localStorage, sessionStorage, and IndexedDB</li>
<li><strong>State Guard</strong> — Logic that blocks, allows, or validates access and actions based on combinations of state, rather than state itself</li>
</ul>
<p>Beyond these categories, there is workflow state that may need to be modeled as a state machine and real-time collaborative state built on WebSocket or CRDTs.</p>
<p>Let us examine why each category calls for different tools and what kind of judgment we should apply to it.</p>
<h2 id="local-state"><a class="anchor" href="#local-state">Local State</a></h2>
<p>This is the simplest kind of state. It is used only within a single component, and the outside world neither needs nor is entitled to know about it. Examples include whether a modal is open, whether a toggle button is on or off, hover state, and a search term currently being entered.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">function</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> SearchBox</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">() {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> [</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">query</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">setQuery</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">] </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> useState</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">""</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> &#x3C;</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">input</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> value</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{query} </span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">onChange</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{(</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">e</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> setQuery</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(e.target.value)} />;</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>This much is probably familiar. The genuinely difficult question with Local State is deciding <strong>"where should this state live?"</strong></p>
<p>In <a href="https://kentcdodds.com/blog/state-colocation-will-make-your-react-app-faster" target="_blank" rel="noopener noreferrer">Kent C. Dodds's article on State Colocation</a>, he explains that <strong>people are accustomed to "lifting up" state, but when the code changes, they rarely move that state back to where it belongs by "colocating" it.</strong></p>
<p>Lifting state is the natural thing to do when sibling components need to share it. Because both siblings need to see the same data, we move the state to their common parent and pass it down through props.</p>
<p>The problem arises when those siblings no longer need the state. We are much less likely to <strong>move it back down</strong> into a child. As a result, parent components accumulate state that has little to do with them, and every parent re-render causes the entire child tree to re-render as well.</p>
<p>The first principle for Local State, then, is this: <strong>to make the code faster and simpler, keep state as close as possible to the code that uses it.</strong> If state is used by only one child of a component, there is no reason for the parent to own it. Move it into that child. The parent becomes lighter as a result.</p>
<h2 id="global-state"><a class="anchor" href="#global-state">Global State</a></h2>
<p>Global State must be accessible from anywhere in the application. Authentication information, theme, language, and notifications such as toasts are common candidates.</p>
<p>The difference between Local State and Global State is not simply where they live. They make different <strong>contracts about how they may be referenced</strong>. Local State promises that <strong>"this has meaning only inside this component,"</strong> while Global State publishes a codebase-wide promise that <strong>"this value can be referenced by this name from anywhere in the application."</strong> The cost of that promise is the essence of Global State.</p>
<p>Creating a piece of Global State is, in effect, adding another <strong>implicit dependency across the entire application</strong>.</p>
<h2 id="server-state"><a class="anchor" href="#server-state">Server State</a></h2>
<p>We put data returned by an API into Client State and manage loading and error states ourselves with booleans—until we finally ask, <strong>"Why am I writing the same boilerplate every time?"</strong></p>
<p>Tanner Linsley, the lead maintainer of TanStack, puts it this way: <strong>"Client State is synchronous and predictable. Server State is asynchronous, shared across multiple components, and requires careful handling of caching, background refetching, and error states."</strong> Server State is therefore <strong>fundamentally a different species</strong> from Client State. It should not be handled with the same tool.</p>
<p>What makes Server State difficult is not the tooling but <strong>the nature of the data itself</strong>.</p>
<p>The data the client displays belongs to the server. What the client holds is merely <strong>a snapshot from a particular moment in time</strong>. As time passes, this data becomes stale. It is also asynchronous, can fail, and moves through states such as pending, error, and success.</p>
<p>The most important fact is that <strong>responses are not guaranteed to arrive in the order requests were sent</strong>. Imagine typing "react" quickly into a search box. Requests for r → re → rea → reac → react are sent in that order, but if the response for "react" arrives first and the response for "rea" arrives afterward, the UI will display the results for "rea". Preventing this requires accounting for <strong>concurrency hazards (race conditions)</strong> that would otherwise force us to implement AbortController logic or request ID tracking by hand every time.</p>
<h2 id="form-state"><a class="anchor" href="#form-state">Form State</a></h2>
<p>Forms are a peculiar kind of state. Their values change intensely while the user is typing, but usually disappear once the form is submitted. They are not shared elsewhere, and in most cases there is nowhere else they need to be stored.</p>
<p>The problem is that this <strong>intense rate of change</strong> can be expensive. If every keystroke causes a React re-render, input lag can become noticeable in a large form. And a form does more than simply hold values. <strong>Validation, dirty checks, submission status, error messages, and multistep flows</strong> all bring several kinds of state to life within a single form at once.</p>
<p>A multistep form such as a three-step checkout flow is often <strong>expected to preserve progress even if the page is refreshed midway through</strong>. If its values live only in useState, a refresh wipes them all out. It is more natural to store them in <strong>sessionStorage</strong> (temporary, tab-scoped storage) or the <strong>URL</strong> (for shareable steps). In other words, depending on its lifecycle requirements, Form State combines with <strong>External State</strong> or <strong>URL State</strong>.</p>
<h2 id="url-state"><a class="anchor" href="#url-state">URL State</a></h2>
<p>Imagine a search page where users can filter by category, sort order, and page number. If we keep these values in useState, three problems arise at once.</p>
<ul>
<li>Refreshing the page resets every filter</li>
<li>Sharing the URL with a friend shows them the unfiltered page</li>
<li>Pressing the Back button does not restore the previous filters</li>
</ul>
<p>A natural solution is to <strong>put the state in the URL.</strong> The URL is effectively a free persistent store with built-in support for refreshes, sharing, and navigation history.</p>
<pre><code>/products?category=shoes&#x26;sort=price-desc&#x26;page=2
</code></pre>
<p>That single URL contains the complete state: <strong>"page 2 of the shoes category, sorted by price in descending order."</strong> There is no need to keep a separate copy in useState.</p>
<p>When is the URL the right place for state? <strong>A URL is a public interface.</strong> Passwords, authentication tokens, and temporary notes that users would not want others to see must not go into it. Nor should values that change too frequently—such as a search term updated on every keystroke—be written directly to the URL, because they will fill the history stack with noise. In those cases, apply the change after a debounce or avoid polluting history by using <code>push</code> only when appropriate and choosing <code>replace</code> for updates that should not add an entry.</p>
<p>Values in a URL are <strong>always strings</strong>. Numbers, booleans, arrays, and objects must be serialized and deserialized. URLs must also follow the rules of <a href="https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams" target="_blank" rel="noopener noreferrer">percent encoding</a>, which gives characters such as <code>&#x26;</code>, <code>=</code>, Korean text, and spaces special handling. Implementing all of this by hand every time quickly becomes a breeding ground for bugs.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> params</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> new</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> URLSearchParams</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(location.search);</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> page</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> Number</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(params.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">get</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"page"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">??</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> "1"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">params.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">set</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"page"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">String</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(page </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">+</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 1</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">));</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">navigate</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">`?${</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">params</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">toString</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">()</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">}`</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> [</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">page</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">setPage</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">] </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> useQueryState</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"page"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, parseAsInteger.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">withDefault</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">1</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">));</span></span></code></pre></figure>
<p>Libraries such as <a href="https://nuqs.dev/" target="_blank" rel="noopener noreferrer">nuqs</a> solve both problems through the concept of a <em>parser</em>. Parsers such as <code>parseAsInteger</code>, <code>parseAsBoolean</code>, and <code>parseAsJson</code> handle serialization, deserialization, and types together. The library supports most common environments, including both the App Router and Pages Router in Next.js, React Router v6/v7, TanStack Router, and Remix.</p>
<p>Does that mean we can put unlimited amounts of state into a URL? Separate from serialization and typing concerns, one final constraint remains. <a href="https://datatracker.ietf.org/doc/html/rfc7230" target="_blank" rel="noopener noreferrer">RFC 7230</a> does not set an exact limit, but recommends that servers support at least 8,000 octets (an octet is the term used in networking and data communications to refer unambiguously to a group of eight bits, or one byte). Browser limits also vary. Modern browsers generally permit anything from 8 KB to tens of thousands of characters, but <strong>search engines, social media Open Graph and sharing pipelines, and some gateways may truncate URLs at around 2 KB</strong>. So do not treat the URL as boundless storage. Keep only the <strong>essential, shareable filters</strong> there, and leave the rest to sessionStorage or server-side storage.</p>
<h2 id="external-state"><a class="anchor" href="#external-state">External State</a></h2>
<p>React knows only about state within its own world. Our applications, however, constantly communicate with the world outside React. State that lives there can persist and change independently of React's lifecycle. Here, External State includes <strong>Cookie, localStorage, sessionStorage, and IndexedDB</strong>.</p>
<p>How should we choose among these storage mechanisms? I usually evaluate them along four dimensions: <strong>lifetime, capacity, synchronicity, and security</strong>.</p>
<p>For <strong>authentication tokens</strong>, the <a href="https://owasp.org/www-community/HttpOnly" target="_blank" rel="noopener noreferrer">OWASP recommendation</a> is to prefer <strong>HttpOnly + Secure cookies</strong>. Because JavaScript can access localStorage, <strong>an XSS compromise can expose the token directly</strong>. Some security guides recommend a hybrid pattern: keep the <strong>access token in memory and the refresh token in an HttpOnly cookie</strong>. Use localStorage for persistent, non-sensitive data that changes infrequently, and sessionStorage for data that should disappear with the tab. IndexedDB is commonly used for offline caches, large datasets, and files.</p>
<p>Cookies and Web Storage (local/session) store <strong>strings only</strong>. Storing an object therefore requires <code>JSON.stringify</code>/<code>JSON.parse</code>. JSON, however, has limitations.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="ts" data-theme="github-dark github-light"><code data-language="ts" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">JSON</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">stringify</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({ when: </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">new</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> Date</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">() });</span></span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// → { "when": "2026-05-19T..." } — Date becomes a string</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">JSON</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">stringify</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({ map: </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">new</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> Map</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">([[</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"a"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">1</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">]]) });</span></span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// → { "map": {} } — Map is lost entirely</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">JSON</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">stringify</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({ value: </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">undefined</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> });</span></span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// → "{}" — the undefined field is omitted</span></span></code></pre></figure>
<p><code>Date</code> becomes a string during a JSON round trip, while <code>Map</code>, <code>Set</code>, and <code>undefined</code> can lose data. With the default behavior, <code>BigInt</code> makes <code>JSON.stringify</code> throw a <code>TypeError</code>, so serialization fails entirely. Whenever you put an object into external storage, remain conscious of <strong>which types may be lost, transformed, or cause serialization to fail</strong>, and introduce a serialization adapter when necessary.</p>
<p>The real difficulty of External State is that <strong>React does not detect its changes automatically</strong>. Writing a value to localStorage does not cause a React component to re-render. There are generally three patterns for solving this.</p>
<ul>
<li><strong>Wrap it in a custom hook (useLocalStorage) that synchronizes External State with React state.</strong> This is lightweight, but a hand-rolled implementation must handle edge cases such as multiple tabs, SSR, and tearing.</li>
<li>Use the <code>useSyncExternalStore</code> hook introduced in React 18 to <strong>"synchronize with state outside React."</strong> This <strong>ensures that tearing does not occur during concurrent rendering.</strong> It is the standard tool for connecting React to localStorage, browser APIs, and external stores.</li>
<li>Use the first-class external storage integrations provided by state libraries, such as Zustand's <code>persist</code> middleware or Jotai's <code>atomWithStorage</code>.</li>
</ul>
<p>Here is one more important principle: <strong>the moment we bring External State into React, synchronization becomes our responsibility.</strong> What happens when another tab updates it? What if the server changes a cookie? What if a user edits localStorage directly through the browser's developer tools? These scenarios often become some of the richest sources of bugs.</p>
<h2 id="state-guard"><a class="anchor" href="#state-guard">State Guard</a></h2>
<p>The final category is somewhat different. It is not state itself, but <strong>logic that uses a combination of state to block, allow, or validate a particular flow</strong>.</p>
<p>The most common example is an <strong>Auth Guard</strong>.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">function</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> ProtectedRoute</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({ </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">children</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> }) {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">isAuthenticated</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">isLoading</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> } </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> useAuth</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">();</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  if</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (isLoading) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Spinner</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> />;</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  if</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">!</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">isAuthenticated) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Navigate</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> to</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"/login"</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> replace</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> />;</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> children;</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>Here, the <code>isAuthenticated</code> state controls the routing flow. That is guard logic. There are many kinds of guards: Auth Guards (authenticated or not), permission guards (specific roles or permissions), flow guards (entry-point branching), and validation guards (enabling a step), among others.</p>
<p>Guard logic tends to accumulate in one place. It is common for a single component to contain every condition: <strong>"if the user is not signed in, go to the login page; if they lack permission, show a 403; if the cart is empty, go to the product page; if the user is suspended, show the suspension notice."</strong> As a guard grows, it becomes harder to debug which condition blocked the flow and where.</p>
<p>A good guard <strong>checks only one thing.</strong> Combine guards through Composition.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">AuthGuard</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">RoleGuard</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> role</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"admin"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">FlowGuard</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> require</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{[</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"cartHasItems"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">]}></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">      &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">CheckoutPage</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> /></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    &#x3C;/</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">FlowGuard</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  &#x3C;/</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">RoleGuard</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">AuthGuard</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span></code></pre></figure>
<p>Each guard makes only one decision, while the tree structure handles composition. Adding a new guard does not require changing any existing guard.</p>
<p>When designing guards, what happens next—<strong>where the user is sent and how the flow continues</strong>—deserves even more thought than the act of blocking access itself. A guard that blocks without a fallback ends in a blank screen or an infinite spinner.</p>
<p>One of the most common bugs occurs when <strong>protected content briefly flashes before the guard's asynchronous check finishes</strong>. Validating an authentication token and fetching permissions are usually asynchronous. During that interval, <code>isAuthenticated</code> may temporarily be <code>undefined</code> or <code>false</code>. <strong>Unless the loading state is handled explicitly, protected content may be exposed during that gap, or the user may be redirected to the login page incorrectly.</strong></p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// Ignores loading and handles only missing data => incorrect</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">if</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">!</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">user) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Navigate</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> to</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"/login"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> />;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// Treat loading as a first-class state (early return) => correct</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">if</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (isLoading) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Spinner</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> />;</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">if</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">!</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">user) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Navigate</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> to</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"/login"</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> replace</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> />;</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> children;</span></span></code></pre></figure>
<p>Two models are commonly used when implementing permission guards.</p>
<ul>
<li><strong>RBAC(Role-Based Access Control)</strong> : Grants permissions by role—for example, "an admin can view all user information." It is simple and fast, but the number of roles can explode as permissions become more granular</li>
<li><strong>ABAC(Attribute-Based Access Control)</strong> : Determines permissions from combinations of attributes—for example, "the user is the author of the post, belongs to the same team, or is an admin." It is highly expressive but more difficult to implement and debug</li>
</ul>
<p>Patterns such as the <a href="https://tanstack.com/router/v1/docs/framework/react/how-to/setup-rbac" target="_blank" rel="noopener noreferrer">TanStack Router RBAC guide</a>, which places guards in <code>beforeLoad</code> at the router level, are recommended. The key is that <strong>permission checks should be expressed as data—a list of roles and permissions—rather than scattered throughout the code</strong>. That way, changing an authorization policy remains a <em>data change</em>.</p>
<h2 id="conclusion"><a class="anchor" href="#conclusion">Conclusion</a></h2>
<p>Let us sum up. State Management is difficult not because the libraries are difficult, but because <strong>we often forget that state comes in different kinds</strong> and overlook the fact that each kind requires different tools and a different way of thinking.</p>
<p>Keep Local State as close as possible; question whether Global State truly needs to be global; treat Server State as a cache; separate forms from the domain; use the URL more actively; understand the responsibility that comes with external storage; and compose small, focused guards. These are the fundamentals of working with the seven categories.</p>
<p>The judgment that ties them together can ultimately be reduced to four questions.</p>
<ul>
<li>Where is the Single Source of Truth for this data?</li>
<li>Is this a value that can be derived, or does it truly need to be stored?</li>
<li>Are any combinations of this state impossible?</li>
<li>Does this state really belong in this location?</li>
</ul>
<p>I believe the surest way to develop sound judgment and intuition is to ask these questions whenever we build a new screen, review a PR, or receive code generated by AI.</p>
<p>As I said at the beginning, AI will remain beside us for a long time. We will spend less and less time inspecting every line. Yet that will only make the ability to answer small questions such as <strong>"Where should this state live?"</strong> more valuable. Asking AI to "add another useState here" is easy. Knowing what new strand that single line adds to the web of our application depends entirely on the judgment of the person reading the code.</p>
<p>There is no single right answer. But there is a clear difference between <strong>"creating state without understanding what state is"</strong> and <strong>"creating state with deliberate awareness of its category and location."</strong> Before readers write their next line of <code>useState</code>, I hope they will pause for a moment and ask, "Which category of state is this?"</p>
<h3 id="references"><a class="anchor" href="#references">References</a></h3>
<details class="ref-block"><summary class="ref-summary">참고 자료</summary><div class="ref-list">
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#3b82f6"></span><a href="https://react.dev/learn/choosing-the-state-structure" target="_blank" rel="noopener noreferrer">React, Choosing the State Structure</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#3b82f6"></span><a href="https://legacy.reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html" target="_blank" rel="noopener noreferrer">React, You Probably Don't Need Derived State</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#3b82f6"></span><a href="https://xstate.js.org/" target="_blank" rel="noopener noreferrer">XState</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#f59e0b"></span><a href="https://www.syncfusion.com/blogs/post/react-state-management-libraries" target="_blank" rel="noopener noreferrer">Top 5 React State Management Tools in 2026</a></div>
</div>
</div></details>]]></content:encoded>
            <author>jihoon7705@gmail.com (이지훈)</author>
            <category>Frontend</category>
            <category>State-Management</category>
            <category>React</category>
            <category>Architecture</category>
        </item>
        <item>
            <title><![CDATA[Domain Models]]></title>
            <link>https://hooninedev.com/en/260418</link>
            <guid isPermaLink="false">https://hooninedev.com/en/260418</guid>
            <pubDate>Sat, 18 Apr 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[In this post, I want to talk about the domain. Throughout my career as a developer, I have encountered the word "domain" quite often. Yet when someone asks, "What exactly is a domain?" it is not easy ...]]></description>
            <content:encoded><![CDATA[<p>In this post, I want to talk about the <strong>domain</strong>.</p>
<p>Throughout my career as a developer, I have encountered the word <strong>"domain"</strong> quite often. Yet when someone asks, "What exactly is a domain?" it is not easy to give a clear answer. (Honestly, when I first started programming, I thought domain meant the "www" kind.)</p>
<p>Looking up information about domains naturally leads to concepts such as <strong>domain models</strong>, <strong>domain objects</strong>, and <strong>domain object models</strong>. I have always found it unfortunate that there are not many articles that clearly explain how these concepts differ, or what they mean on the <strong>frontend</strong> rather than the backend. In this article, I will start with a definition of each concept and use examples to explore how domain logic can be appropriately separated and abstracted on the frontend.</p>
<p>Lately, I have been very interested in the tax domain. With the comprehensive income tax filing season coming up in May, I will use taxes for the examples in this article.</p>
<hr>
<h2 id="domain"><a class="anchor" href="#domain">Domain</a></h2>
<p>Let us begin with the most fundamental question. What is a <strong>domain</strong>?</p>
<p>In his book <strong>Domain-Driven Design: Tackling Complexity in the Heart of Software (2003)</strong>, Eric Evans defines a domain as follows.</p>
<blockquote class="bilingual-quote"><div class="quote-translation" lang="ko"><p>A sphere of knowledge, influence, or activity.</p></div><div class="quote-original" lang="en"><p>"A sphere of knowledge, influence, or activity."</p></div></blockquote>
<p>Put simply, the domain is the <strong>problem space that we intend to solve through programming</strong>. If we are building a tax filing service, "tax filing" is the domain; if we are building an insurance claims platform, "insurance claims" is the domain. A domain is not code. It is a real-world problem space that exists before the software does.</p>
<p>What does this mean for frontend developers? Ultimately, the UI we build is a <strong>window</strong> that lets users see and manipulate the domain. When developing tax-refund services such as Toss Income or 3o3, whose primary domain is tax, we are expressing domain concepts such as income types, expense rates, income deductions, tax credits, and refund amounts through the UI. Frontend developers therefore need a deep understanding of the domain they work with. In other words, knowing <strong>"what problem this service solves"</strong> is just as important as being good at rendering UI components.</p>
<p>But even a single domain called "tax" contains countless subdomains when examined closely. This is true even of the comprehensive income tax calculation pipeline I only understand at a high level.</p>
<p><img src="/content/260418/1.png" alt="1.png" width="1090" height="566" loading="eager" fetchpriority="high" decoding="async"></p>
<p>Each stage of this pipeline is a subdomain with its own rules and data. Within the broad domain of "tax," the detailed domains of Income, Deduction, Tax, and Filing are intertwined. How these should be divided in code is the central question of domain modeling.</p>
<h2 id="domain-model"><a class="anchor" href="#domain-model">Domain Model</a></h2>
<p>Then what is a domain model? How is a domain different from a "domain model"?</p>
<p>Martin Fowler and Eric Evans define a domain model as follows.</p>
<blockquote class="bilingual-quote"><div class="quote-translation" lang="ko"><p>An object model of the domain that incorporates both behavior and data. — Martin Fowler</p></div><div class="quote-original" lang="en"><p>An object model of the domain that incorporates both behavior and data.</p></div></blockquote>
<blockquote class="bilingual-quote"><div class="quote-translation" lang="ko"><p>A system of abstractions that describes selected aspects of a domain and can be used to solve problems related to that domain. — Eric Evans</p></div><div class="quote-original" lang="en"><p>A system of abstractions that describes selected aspects of a domain and can be used to solve problems related to that domain.</p></div></blockquote>
<p>The key is <strong>"selective abstraction."</strong> A domain model does not contain everything in the real world. Just as a film director does not capture every scene in reality but selects only the scenes needed for the story, a domain model <strong>selects and structures only the aspects needed to solve the problem</strong>.</p>
<p>There is one important point here. A domain model does not necessarily have to be code. It might be a diagram on a whiteboard, or a shared mental model in the minds of team members. Ultimately, the term domain model itself can refer to a concept independent of software.</p>
<p>This is where frontend developers are particularly prone to confusion. They see the structure of an API response and think, "So this is the domain model." But that is a <strong>data model</strong>, not a domain model.</p>
<p>The distinction between a data model and a domain model is as follows.</p>
<table>
<thead>
<tr>
<th>Category</th>
<th>Domain Model</th>
<th>Data Model</th>
</tr>
</thead>
<tbody>
<tr>
<td>Purpose</td>
<td>Express business concepts and rules</td>
<td>Define storage/transfer structures</td>
</tr>
<tr>
<td>Language</td>
<td>Business terms (tax base, tax credit, refund)</td>
<td>Technical terms (string, number, array)</td>
</tr>
<tr>
<td>Contains</td>
<td>Data + behavior (rules)</td>
<td>Data structure only</td>
</tr>
<tr>
<td>Example</td>
<td>"The rate is 6% for a tax base up to KRW 14M"</td>
<td><code>{ taxableBase: number, taxRate: number }</code></td>
</tr>
</tbody>
</table>
<p>A data model defines "the shape in which data is exchanged," while <strong>a domain model defines "what that data means to the business and what rules it follows."</strong> If we fail to distinguish the two, components become directly dependent on the API response structure, and the entire frontend is thrown into disarray whenever the backend schema changes.</p>
<h2 id="domain-object"><a class="anchor" href="#domain-object">Domain Object</a></h2>
<p>If a domain model is a system of concepts, a <strong>domain object</strong> is a concrete implementation of one of those concepts in code.</p>
<p>In <a href="https://www.codewithjason.com/difference-domains-domain-models-object-models-domain-objects/" target="_blank" rel="noopener noreferrer">an article by Jason Swett</a>, who runs Code with Jason, he defines a domain object as follows.</p>
<blockquote class="bilingual-quote"><div class="quote-translation" lang="ko"><p>Any object in my object model that also exists as a concept in my domain model, I would call a domain object.</p></div><div class="quote-original" lang="en"><p>Any object in my object model that also exist as a concept in my domain model I would call a domain object.</p></div></blockquote>
<p>In other words, if the domain model contains a concept called "comprehensive income" and the code contains a type called <code>Income</code>, that <code>Income</code> is a domain object. But not every object in code is a domain object. Things such as <code>HttpClient</code>, <code>LocalStorageAdapter</code>, and <code>useDebounce</code> are technical tools, not domain concepts.</p>
<h3 id="entity-and-value-object"><a class="anchor" href="#entity-and-value-object">Entity and Value Object</a></h3>
<p>Evans classifies domain objects into three categories: <strong>Entity</strong>, <strong>Value Object</strong>, and <strong>Service</strong>. (Martin Fowler calls this the "Evans Classification.") A Service is a separate concept that represents "a domain operation that does not naturally belong to a particular object." Because the focus of this article is how data is identified, we will concentrate on Entities and Value Objects.</p>
<p>An <strong>Entity</strong> is an object with a unique identity that persists across time and different representations. A tax filing (TaxFiling), taxpayer (Taxpayer), or income record (IncomeRecord) is identified by a unique ID; even if its properties change, it remains the same Entity as long as its ID is the same. Even when the deductions on a filing are edited, it is still the same filing unless the filing ID changes.</p>
<p>A <strong>Value Object</strong> is an object whose meaning comes solely from the combination of its properties, and two Value Objects are considered equal when all their property values are equal. Money, a tax rate (TaxRate), and a tax bracket (TaxBracket) are objects whose values themselves carry the meaning. A "6% tax rate" is simply a "6% tax rate" wherever it is used.</p>
<p>Why does this distinction matter on the frontend? Let us look at the code below.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="typescript" data-theme="github-dark github-light"><code data-language="typescript" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">interface</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> TaxFiling</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  id</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  taxpayerName</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  taxYear</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> number</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  status</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> FilingStatus</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> isSameFiling</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">a</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> TaxFiling</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">b</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> TaxFiling</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> a.id </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">===</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> b.id;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">interface</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> Money</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  amount</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> number</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  currency</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> "KRW"</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> |</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> "USD"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> isSameMoney</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">a</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> Money</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">b</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> Money</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  a.amount </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">===</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> b.amount </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">&#x26;&#x26;</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> a.currency </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">===</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> b.currency;</span></span></code></pre></figure>
<p>TaxFiling is an Entity because it uses its id as the basis of identity. (Merely having an id field does not make something an Entity; the key is that "the id determines whether two objects are the same or different.") Money has no id and is identified only by the combination of amount and currency; it is considered the same value when all properties are equal.</p>
<p>Entities use ID-based comparison; Value Objects use property-based comparison. Making this distinction explicit naturally clarifies the state-management logic for determining whether data is the same or different. When updating an item in a list, for example, an Entity can be found and replaced by ID, while a Value Object can be replaced immutably.</p>
<h2 id="domain-object-model"><a class="anchor" href="#domain-object-model">Domain Object Model</a></h2>
<p>We now understand the "domain model" and the "domain object," but what is a <strong>domain object model</strong>?</p>
<p>Surprisingly, I could not find a commonly agreed-upon definition. A substantial body of literature treats "domain model," "domain object model," "conceptual model," and "analysis object model" as <strong>effectively synonymous</strong>—different names for the conceptual model created during object-oriented analysis.</p>
<p>Another perspective, however, sees it as a more distinct layer. A representative explanation is that the <strong>object model is the point where a domain model is translated into actual code</strong>.</p>
<p>Under this second view, an <strong>object model</strong> is the structure of <strong>every object in the system's code</strong>. This includes technical tools such as <code>HttpClient</code> and <code>useDebounce</code>. Within it, the <strong>subset of objects that represent domain concepts, together with the relationships among them</strong>, is the <strong>domain object model</strong>. This also aligns with the object-oriented modeling tradition, which has defined an "object model" as the static structure of a system—its classes, properties, operations, and relationships.</p>
<p>I find this perspective more practical for frontend developers because the code we actually write always mixes domain objects with technical objects.</p>
<p>Ultimately, <strong>domain → domain model → domain object model → domain object</strong> is a progression from the abstract to the concrete. The domain is the broadest, and the domain object is the most concrete. Thus, the area we actually wrestle with when writing frontend code is <strong>how to structure the domain object model—the types that express domain concepts and the relationships among them</strong>.</p>
<h2 id="where-should-domain-logic-live-on-the-frontend"><a class="anchor" href="#where-should-domain-logic-live-on-the-frontend">Where Should Domain Logic Live on the Frontend?</a></h2>
<p>That covers the definitions. Now let us turn to practice. <strong>Where</strong> should domain logic live on the frontend?</p>
<p><a href="https://khalilstemmler.com/about/" target="_blank" rel="noopener noreferrer">Khalil Stemmler</a>, who has a deep interest in software design, initially argued that "business logic does not belong on the frontend." He later revised his position, saying, "Almost everything we do architecturally on the backend can and should also be done on the frontend."</p>
<p>I agree with this position. Of course, the frontend must not become the <strong>Single Source of Truth</strong> for business logic. That is the backend's role. But the frontend clearly has <strong>domain logic of its own</strong>.</p>
<p>Consider a case where "the estimated refund must update in real time based on the information the user enters." If this calculation logic exists only on the backend, an API call must be made every time the user changes a single digit in their income. The UI pauses for a network round trip, and a fast typist can trigger a flood of unnecessary requests. Even with debounce, a delay of a few hundred milliseconds is enough to undermine the experience of a "real-time preview." <strong>Ultimately, calculations that require immediate feedback have to be performed directly on the frontend, which means some logic can only be performed there.</strong></p>
<h3 id="when-domain-logic-is-mixed-into-a-component"><a class="anchor" href="#when-domain-logic-is-mixed-into-a-component">When Domain Logic Is Mixed into a Component</a></h3>
<p>Consider a comprehensive income tax preview screen. It shows the estimated tax in real time as the user enters income information. The code below is a common example in which domain logic and UI logic are intertwined.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">function</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> TaxPreviewPage</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">() {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> [</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">총수입</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">set총수입</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">] </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> useState</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">0</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> [</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">경비율</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">set경비율</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">] </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> useState</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">0.641</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">); </span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> [</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">인적공제대상인원</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">set인적공제대상인원</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">] </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> useState</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">1</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">); </span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 종합소득금액</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> 총수입 </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">-</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> 총수입 </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">*</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> 경비율;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 소득공제합계</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> 인적공제대상인원 </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">*</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 1_500_000</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 과세표준</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> Math.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">max</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">0</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, 종합소득금액 </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">-</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> 소득공제합계);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  let</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> calculatedTax </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 0</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  if</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (과세표준 </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">&#x3C;=</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 14_000_000</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) {</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    calculatedTax </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> 과세표준 </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">*</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 0.06</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  } </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">else</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> if</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (과세표준 </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">&#x3C;=</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 50_000_000</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) {</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    calculatedTax </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> 과세표준 </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">*</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 0.15</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> -</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 1_260_000</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  } </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">else</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> if</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (과세표준 </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">&#x3C;=</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 88_000_000</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) {</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    calculatedTax </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> 과세표준 </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">*</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 0.24</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> -</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 5_760_000</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  } </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">else</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> if</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (과세표준 </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">&#x3C;=</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 150_000_000</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) {</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    calculatedTax </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> 과세표준 </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">*</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 0.35</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> -</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 15_440_000</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  } </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">else</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    calculatedTax </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> 과세표준 </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">*</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 0.38</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> -</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 19_940_000</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 기납부세액</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> 총수입 </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">*</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 0.033</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> refundOrPayment</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> 기납부세액 </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">-</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> calculatedTax;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> &#x3C;</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">div</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">>...&#x3C;/</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">div</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">>;</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>Can you see the problem? <strong>Business rules established by tax law</strong>—"KRW 1.5 million in personal deductions per person," "eight progressive tax brackets," and "3.3% withholding"—are embedded directly in a React component. Tax law changes every year. When such rules are scattered across components, each revision sends us hunting for every place that needs to change. And if the QA team maintains E2E scenarios, the testing cost will be substantial as well.</p>
<p>Eventually it becomes difficult to distinguish view logic from business logic, and the code turns into a tangle of conditionals and custom hooks.</p>
<h3 id="let-us-separate-the-domain-logic"><a class="anchor" href="#let-us-separate-the-domain-logic">Let Us Separate the Domain Logic</a></h3>
<p>Let us borrow the central principle from Alex Bespoyasov's Clean Architecture approach: separate domain logic into <strong>pure functions that do not depend on a framework</strong>.</p>
<blockquote class="bilingual-quote"><div class="quote-translation" lang="ko"><p>The domain is the core that distinguishes one application from another. You can think of the domain as something that will not change if we move from React to Angular.</p></div><div class="quote-original" lang="en"><p>The domain is the core that distinguishes one application from another. You can think of the domain as something that won't change if we move from React to Angular.</p></div></blockquote>
<p>Let us refactor the tax calculation example above.</p>
<p>First, we define the domain types and rules, keeping related information together.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="typescript" data-theme="github-dark github-light"><code data-language="typescript" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">export</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> interface</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> Income</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  grossAmount</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> number</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  expenseRate</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> number</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">export</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> interface</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> Deductions</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  personalCount</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> number</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  pensionPaid</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> number</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  additionalDeductions</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> number</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> PERSONAL_DEDUCTION_PER_PERSON</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 1_500_000</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> WITHHOLDING_RATE</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 0.033</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> TAX_BRACKETS</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> [</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  { limit: </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">14_000_000</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, rate: </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">0.06</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, progressiveDeduction: </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">0</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> },</span></span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">  /** ...구간들... **/</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">] </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">as</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> const</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span></code></pre></figure>
<p>Then we separate the domain logic into pure functions.</p>
<p>The logic for income calculation, deductions, tax base, tax, and refunds discussed above is extracted into a <code>computeFullTax</code> function. Each stage is then broken down into smaller pure functions. If the result type is inferred with <code>ReturnType&#x3C;typeof computeFullTax></code>, there is no need to declare a separate interface.</p>
<p>The component then does nothing but "use" the domain logic.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { computeFullTax } </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">from</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> "../domain/tax"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">function</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> TaxPreviewPage</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">() {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> [</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">income</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">setIncome</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">] </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> useState</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">Income</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">>({</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    grossAmount: </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">0</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    expenseRate: </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">0.641</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  });</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> [</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">deductions</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">setDeductions</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">] </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> useState</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">Deductions</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">>({</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    personalCount: </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">1</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    pensionPaid: </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">0</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    additionalDeductions: </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">0</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  });</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> result</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> computeFullTax</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(income, deductions);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    &#x3C;</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">div</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">      &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">IncomeForm</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> value</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{income} </span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">onChange</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{setIncome} /></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">      &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">DeductionForm</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> value</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{deductions} </span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">onChange</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{setDeductions} /></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">      &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">TaxResultSummary</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> result</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{result} /></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    &#x3C;/</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">div</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  );</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>What has changed?</p>
<ul>
<li>The <strong>table of eight progressive tax brackets</strong> (<code>TAX_BRACKETS</code>) is gathered in one place, so when tax law changes, only <code>domain/tax.ts</code> needs to be updated.</li>
<li>The <strong>calculation pipeline</strong> is cohesive within a single <code>computeFullTax</code> function, making the entire flow visible at a glance. (It is grouped into one function to keep the example simple, but in a real project it would be appropriate to divide it further by purpose, such as income calculation, deduction calculation, and tax calculation.)</li>
<li>The <strong>component focuses only on "how to present it."</strong> A change in tax rates does not require modifying the component.</li>
<li>Even if we migrate from React to another framework, <code>domain/tax.ts</code> <strong>does not change</strong>.</li>
</ul>
<p>Once domain logic is separated, testing becomes surprisingly simple. This is especially important in the tax domain, where <strong>calculation accuracy directly affects the user's money</strong>.</p>
<p>Pure functions containing tax calculation logic need neither React Testing Library nor <code>render</code> nor <code>screen.getByText</code>. Supply an input and check the output—that is all. Cases such as "a 6% rate up to KRW 14 million," "zero tax when the tax base is zero," and "the refund on KRW 30 million of freelance income" can each be expressed as a one-line <code>it</code>. Domain unit tests naturally establish boundaries for component separation, while the test code also serves as documentation.</p>
<h2 id="anemic-domain-model"><a class="anchor" href="#anemic-domain-model">Anemic Domain Model</a></h2>
<p>In the previous section, we separated <strong>calculation logic</strong>. But domain logic also includes <strong>state-transition rules</strong> and <strong>authorization decisions</strong>. Questions such as "Can this filing be edited right now?", "Can it be submitted?", and "Can the claim method be changed?" fall into these categories. Separating these rules presents an easy trap: the <strong>Anemic Domain Model</strong>, a term coined by Martin Fowler.</p>
<p>An Anemic Domain Model is a state in which <strong>types are well defined in the domain language, but the rules that operate on them have been scattered outside the domain</strong>. Consider the tax filing domain. The type is tidy.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="typescript" data-theme="github-dark github-light"><code data-language="typescript" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// types/filing.ts</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">export</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> interface</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> TaxFiling</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  id</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  status</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> "draft"</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> |</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> "submitted"</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> |</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> "reviewing"</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> |</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> "completed"</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> |</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> "amended"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  taxYear</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> number</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  filingType</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> "regular"</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> |</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> "late"</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> |</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> "amendment"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  determinedTax</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> number</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>But the rules for decisions and transitions concerning this type are buried elsewhere.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="typescript" data-theme="github-dark github-light"><code data-language="typescript" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// utils/filingHelpers.ts</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">export</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> function</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> canAmendFiling</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">filing</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> TaxFiling</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> filing.status </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">===</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> "completed"</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> &#x26;&#x26;</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> filing.filingType </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">!==</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> "amendment"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// components/FilingDetail.tsx</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">function</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> FilingDetail</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({ </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">filing</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">filing</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> TaxFiling</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> }) {</span></span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">  // 같은 도메인 규칙을 컴포넌트 안에 다시 작성한다</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> canEdit</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> filing.status </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">===</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> "draft"</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> ||</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> filing.status </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">===</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> "reviewing"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">  // ...</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// hooks/useSubmitFiling.ts</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">export</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> function</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> handleSubmitFiling</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">filing</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> TaxFiling</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  if</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (filing.status </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">!==</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> "draft"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">  // ...</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>The same domain rule exists in three different forms across utils, a component, and a hook. If a requirement comes in saying, "The eligibility conditions for claims are changing," we must hunt down every place that needs updating, while any one we miss will make an incorrect decision somewhere on the site. Fowler criticized such code as <strong>"little more than procedural code wearing the skin of an object-oriented model."</strong></p>
<p>The solution is the same one we applied to calculation logic in the previous section: <strong>put the rules next to the type</strong>.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="typescript" data-theme="github-dark github-light"><code data-language="typescript" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">export</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> interface</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> TaxFiling</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  id</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  status</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> FilingStatus</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  taxYear</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> number</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  filingType</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> FilingType</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  determinedTax</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> number</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">export</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> type</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> FilingStatus</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  |</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> "draft"</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  |</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> "submitted"</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  |</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> "reviewing"</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  |</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> "completed"</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  |</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> "amended"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">export</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> type</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> FilingType</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> "regular"</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> |</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> "late"</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> |</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> "amendment"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 도메인 규칙은 도메인 옆에 둔다</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">export</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> function</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> canEdit</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">filing</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> TaxFiling</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">)</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> boolean</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> filing.status </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">===</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> "draft"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">export</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> function</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> canSubmit</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">filing</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> TaxFiling</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">)</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> boolean</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> filing.status </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">===</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> "draft"</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> &#x26;&#x26;</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> filing.determinedTax </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">>=</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 0</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">export</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> function</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> canAmend</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">filing</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> TaxFiling</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">)</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> boolean</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> filing.status </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">===</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> "completed"</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> &#x26;&#x26;</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> filing.filingType </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">!==</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> "amendment"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>Now every filing-related rule is managed in a single place: <code>domain/filing.ts</code>. Any component can call <code>canAmend(filing)</code>, and when a rule changes, only this one file needs to be updated. The key is to <strong>treat the type and the rules that operate on it as one unit</strong>. A partial separation that puts only the type in the domain folder while moving its rules to utils may look clean, but it is still anemic.</p>
<h2 id="the-translation-layer-between-api-responses-and-the-domain-model"><a class="anchor" href="#the-translation-layer-between-api-responses-and-the-domain-model">The Translation Layer Between API Responses and the Domain Model</a></h2>
<p>There is one more factor to consider in practice: the backend API response structure and the frontend domain model do not always match. This is even more true for a tax service integrated with government systems. Data from Korea's National Tax Service Hometax system is full of abbreviations and coded values, so it is unlikely to arrive in the same shape as the frontend domain model.</p>
<p>What we need here is a <strong>transformation layer (Mapper)</strong>. Rather than passing the API response type all the way into components, we first refine it into a domain type. A single pure function is enough.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="typescript" data-theme="github-dark github-light"><code data-language="typescript" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> type</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { Income } </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">from</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> "../domain/tax"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">interface</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> HometaxIncomeResponse</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  총수입금액</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> number</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  경비율</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> number</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  소득유형코드</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">  // ... 나머지 약어 필드들</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">export</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> function</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> toIncome</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">response</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> HometaxIncomeResponse</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">)</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> Income</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    총수입_금액: response.총수입금액,</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    경비_비율: response.경비율,</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  };</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>This converts abbreviated fields such as <code>총수입금액</code> and <code>경비율</code>, as well as code-based classifications in the API response, to suit the frontend domain <strong>in one place</strong>. Values such as the income-type code that need to be expanded into an enum can be handled with a small lookup table inside the mapper. Even if the field names in the Hometax API change, only the mapper needs to be updated.</p>
<h2 id="utility-functions-and-domain-logic"><a class="anchor" href="#utility-functions-and-domain-logic">Utility Functions and Domain Logic</a></h2>
<p>When separating domain logic, one question inevitably arises: <strong>"Isn't this just a utility function?"</strong></p>
<p>Consider the following two functions.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="typescript" data-theme="github-dark github-light"><code data-language="typescript" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">function</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> formatCurrency</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">amount</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> number</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">)</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  return</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> `${</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">amount</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">toLocaleString</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">()</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">}원`</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">function</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> calculateTax</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">taxableBase</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> number</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">)</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> number</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> bracket</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> TAX_BRACKETS</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">find</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">((</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">bracket</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> taxableBase </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">&#x3C;=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> bracket.limit);</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> Math.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">floor</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(taxableBase </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">*</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> bracket.rate </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">-</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> bracket.progressiveDeduction);</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p><code>formatCurrency</code> is pure <strong>presentation logic</strong> that converts a number to a string. Adding the unit "won" and thousands separators is not a business rule; it concerns how a value is shown to the user. By contrast, <code>calculateTax</code> contains a <strong>business rule grounded in tax law</strong>: applying eight progressive tax brackets. It is a domain rule that must apply in exactly the same way even if there is no UI.</p>
<p>Here is the criterion I use in practice.</p>
<blockquote>
<p><strong>If this logic disappears, does the business break, or only the screen?</strong></p>
</blockquote>
<p>If the business breaks, it is domain logic. If only the screen breaks, it is presentation logic. This one question can identify most boundaries.</p>
<table>
<thead>
<tr>
<th>Criterion</th>
<th>Domain Logic</th>
<th>Utility/Presentation Logic</th>
</tr>
</thead>
<tbody>
<tr>
<td>What breaks without it?</td>
<td>Tax calculation</td>
<td>The screen (UI) looks wrong</td>
</tr>
<tr>
<td>What if the framework changes?</td>
<td>Remains unchanged</td>
<td>May change</td>
</tr>
<tr>
<td>Is it in the product spec?</td>
<td>"Tax base × rate - progressive deduction"</td>
<td>"Separate amounts with commas"</td>
</tr>
<tr>
<td>Same logic on the backend?</td>
<td>Exists or should exist</td>
<td>No (frontend-only concern)</td>
</tr>
</tbody>
</table>
<p>Reality, however, is not this tidy. The hardest cases are those that <strong>look like domain logic but are actually presentation logic</strong>.</p>
<p>Consider the code below. It was classified as domain logic because it takes the domain concept FilingStatus as an argument. But is it really domain logic?</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="typescript" data-theme="github-dark github-light"><code data-language="typescript" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// domain/filing.ts</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">function</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> getStatusBadgeColor</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">status</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> FilingStatus</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">)</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> colors</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> Record</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">FilingStatus</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">string</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">> </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    draft: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"gray"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    submitted: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"blue"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    reviewing: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"yellow"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    completed: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"green"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    amended: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"purple"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  };</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> colors[status];</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">function</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> getStatusDisplayText</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">status</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> FilingStatus</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">)</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> labels</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> Record</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">FilingStatus</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">string</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">> </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    draft: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"작성 중"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    submitted: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"제출 완료"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    reviewing: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"검토 중"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    completed: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"신고 완료"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    amended: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"경정청구"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  };</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> labels[status];</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>Although <code>getStatusBadgeColor</code> and <code>getStatusDisplayText</code> use the domain concept <code>FilingStatus</code>, what they do is <strong>screen presentation</strong>. Changing a badge color does not break the business at all. Putting such functions in <code>domain/filing.ts</code> makes the domain module increasingly bloated and mixes genuine domain logic with presentation logic.</p>
<h3 id="separating-the-domain-model-and-viewmodel"><a class="anchor" href="#separating-the-domain-model-and-viewmodel">Separating the Domain Model and ViewModel</a></h3>
<p>There is a practical way to solve this problem: <strong>separate the ViewModel into its own file within the same domain folder</strong>. Rather than <code>.ui.ts</code>, using the name <code>.viewModel.ts</code> connects naturally to the ViewModel concept in the MVVM pattern. The name immediately communicates its role as "a layer that transforms domain data for the screen."</p>
<pre><code>domains/
└── filing/
    ├── filing.ts              # 순수 도메인 모델 + 도메인 로직
    ├── filing.viewModel.ts    # ViewModel (표현 변환 계층)
    ├── filing.test.ts         # 도메인 로직 테스트
    └── filingMapper.ts        # API ↔ 도메인 변환
</code></pre>
<p>Move the <code>getStatusBadgeColor</code> and <code>getStatusDisplayText</code> functions we saw earlier directly into <code>filing.viewModel.ts</code>. Transformations that expand filing types into Korean labels, such as <code>getFilingTypeLabel(type: FilingType): string</code>, are gathered there as well. <code>filing.ts</code> is responsible only for business rules, while <code>filing.viewModel.ts</code> is responsible only for screen presentation.</p>
<p>The key is the <strong>direction of dependency</strong>. <code>filing.viewModel.ts</code> imports <code>filing.ts</code>, but <code>filing.ts</code> never imports <code>filing.viewModel.ts</code>. The domain knows nothing about presentation; presentation knows about the domain. This can be seen as a miniature version of Robert C. Martin's Dependency Rule.</p>
<p>I placed the files in the same folder because I believe files that change together should live in the same directory. If the <code>FilingStatus</code> type gains a new value (such as <code>'rejected'</code>), both <code>filing.ts</code> and <code>filing.viewModel.ts</code> must be updated. Since they are in the same folder, the scope of the change is visible at a glance.</p>
<h2 id="boundaries-and-cohesion"><a class="anchor" href="#boundaries-and-cohesion">Boundaries and Cohesion</a></h2>
<p>Just as important as separating domain logic is deciding <strong>where to draw the boundaries</strong>. Here are several boundary decisions I often encounter in practice.</p>
<p>The data handled by a frontend comes from roughly four sources.</p>
<ul>
<li><strong>Server data</strong>: received in an API response</li>
<li><strong>Derived data</strong>: calculated from server data</li>
<li><strong>UI state</strong>: used to control the screen and reflect user interactions</li>
<li><strong>User input</strong>: values currently being entered in a form</li>
</ul>
<p>Combining all four in one type pollutes the domain model.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="typescript" data-theme="github-dark github-light"><code data-language="typescript" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 안티패턴: 모든 것이 섞인 타입</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">interface</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> TaxFiling</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">  // 서버 데이터 (도메인)</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  id</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  status</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> FilingStatus</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  determinedTax</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> number</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">  // 파생 데이터 (도메인)</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  refundAmount</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> number</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  canAmend</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> boolean</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">  // UI 상태 (표현)</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  isExpanded</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> boolean</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  activeStep</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> number</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">  // 임시 상태</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  editingDeductions</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> Deduction</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">[];</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>This type puts domain concepts, UI state, and temporary data in one basket. Every change to <code>activeStep</code> effectively updates the Filing domain. (Changing a form step is not a business event.)</p>
<p>The improvement is to divide the types along their boundaries. The <strong>domain model</strong> contains only business concepts such as <code>id</code>, <code>status</code>, and <code>determinedTax</code>; <strong>UI state</strong> (<code>FilingFormViewState</code>) contains only screen controls such as <code>isExpanded</code> and <code>activeStep</code>; and <strong>form state</strong> (<code>DeductionEditForm</code>) contains only temporary data being entered.</p>
<p>This gives each type <strong>a single reason to change</strong>. A domain type changes only when tax law changes, UI state only when the screen design changes, and form state only when the input UX changes.</p>
<h3 id="keep-things-that-change-together-together"><a class="anchor" href="#keep-things-that-change-together-together">Keep Things That Change Together Together</a></h3>
<p>Eric Evans's DDD includes the concept of an <strong>Aggregate</strong>: "a cluster of related objects treated as a single unit." We do not need to apply this concept wholesale on the frontend, but its central principle—<strong>keep data and rules that change together together</strong>—is worth borrowing.</p>
<p>In a tax service, for example, <code>Income</code> and <code>ExpenseRate</code> always change together. When the income type changes, the applicable expense rate changes, and the comprehensive income calculation is affected as well. These should therefore be kept together in one file, <code>domain/tax.ts</code>.</p>
<p>By contrast, <code>TaxFiling</code> can change independently of tax calculation. A change to the filing's state-transition rules does not affect tax-rate calculation logic. It is therefore appropriate to separate it into <code>domain/filing.ts</code>.</p>
<pre><code>이렇게 묻자: "A가 변할 때 B도 반드시 변해야 하는가?"
  → Yes: 같은 모듈에 둔다 (Income + ExpenseRate + TaxBracket)
  → No: 분리한다 (Tax 계산 ↔ Filing 상태관리)
</code></pre>
<h2 id="class-vs-functional-style"><a class="anchor" href="#class-vs-functional-style">Class vs. Functional Style</a></h2>
<p>At this point, a fundamental question may come to mind. Every example so far has used a combination of <code>interface</code> and pure functions. Would cohesion be more natural if we represented the domain with a class?</p>
<p>That is a fair point. A class-based representation of the domain binds data and behavior into a single object, making cohesion immediately visible in the code structure.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="typescript" data-theme="github-dark github-light"><code data-language="typescript" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">class</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> TaxFilingModel</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  constructor</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> readonly</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209"> id</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> readonly</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209"> status</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> FilingStatus</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> readonly</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209"> taxYear</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> number</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> readonly</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209"> filingType</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> FilingType</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">    public</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> readonly</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209"> determinedTax</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> number</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  ) {}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  canEdit</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">()</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> boolean</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">    return</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> this</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">.status </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">===</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> "draft"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  canAmend</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">()</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> boolean</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">    return</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> this</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">.status </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">===</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> "completed"</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> &#x26;&#x26;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> this</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">.filingType </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">!==</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> "amendment"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  canSubmit</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">()</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> boolean</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">    return</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> this</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">.status </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">===</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> "draft"</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> &#x26;&#x26;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> this</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">.determinedTax </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">>=</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 0</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  }</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> filing</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> new</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> TaxFilingModel</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">  "F-001"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">  "completed"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">  2025</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">  "regular"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">  547200</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">filing.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">canAmend</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">();</span></span></code></pre></figure>
<p>With a class-based approach, behavior belongs to the data. The subject is also explicit at the call site. <code>filing.canAmend()</code> is as intuitive as reading natural language: the subject (filing) and verb (canAmend) are clearly bound together. It is like writing <code>jihoon.eat('감자탕')</code> and immediately reading, "Jihoon eats gamjatang."</p>
<p>By contrast, the functional style looks like this.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="typescript" data-theme="github-dark github-light"><code data-language="typescript" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">canAmend</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(filing);</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">eat</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"jihoon"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"감자탕"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">);</span></span></code></pre></figure>
<p>In the functional style, the data exists outside the function. The first call takes <code>filing</code> as an argument and performs an action, while <code>eat</code> takes <code>jihoon</code> and <code>감자탕</code> as its arguments.</p>
<p>As a result, the bond between subject and verb is looser. To know that the <code>canAmend</code> function concerns <code>TaxFiling</code>, we have to open the file or inspect its type signature. If functions such as <code>canAmend(filing)</code>, <code>canEdit(filing)</code>, and <code>calculateTax(taxableBase)</code> are mixed in the same file, it may be difficult to see at a glance which domain each function belongs to.</p>
<h3 id="so-should-we-use-classes"><a class="anchor" href="#so-should-we-use-classes">So Should We Use Classes?</a></h3>
<p>Honestly, the answer is <strong>"it depends."</strong> In my experience, however, there are practical reasons classes are not a silver bullet in a React + TypeScript environment.</p>
<p><strong>1. Friction with React State Management</strong></p>
<p>React state management fits most naturally with <strong>plain objects</strong>. <code>useState</code> and <code>useReducer</code> can technically hold any value, and Redux DevTools does not itself remove a class instance's prototype. If Redux/Zustand persistence middleware stores and restores state as JSON, however, a class instance loses its methods and prototype in the <code>JSON.stringify</code> → <code>JSON.parse</code> cycle and becomes a plain object. The props boundary from a React Server Component to a Client Component has a different constraint: it accepts only supported serializable values, so an arbitrary class instance cannot be passed through it in the first place.</p>
<p>Consider the code below.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="typescript" data-theme="github-dark github-light"><code data-language="typescript" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> [</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">filing</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">setFiling</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">] </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> useState</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  new</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> TaxFilingModel</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"F-001"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"draft"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">2025</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"regular"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">0</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">),</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">);</span></span></code></pre></figure>
<p>Updating React state alone does not stop <code>filing</code> from being an instance of <code>TaxFilingModel</code>. If Redux/Zustand persistence saves and restores it as JSON, however, the restored value may be a plain object without methods, so an innocent call to <code>filing.canAmend()</code> can produce a runtime error. Passing it from a React Server Component to a Client Component fails earlier because a class instance is not a supported serializable prop value.</p>
<p><strong>2. The Difficulty of Guaranteeing Immutability</strong></p>
<p>React detects state changes based on <strong>referential equality</strong>. If a method on a class instance mutates internal state with something like <code>this.items.push(...)</code>, the reference stays the same and React does not trigger a rerender. In the end, <code>addDeduction(item)</code> has to return a new instance every time—something like <code>return new DeductionList([...this.items, item])</code>. That undermines the class advantage of "encapsulated state mutation" and leaves code that is not very different from functional updates.</p>
<h3 id="strategies-for-achieving-cohesion-in-a-functional-style"><a class="anchor" href="#strategies-for-achieving-cohesion-in-a-functional-style">Strategies for Achieving Cohesion in a Functional Style</a></h3>
<p>Then how can we improve the loose cohesion of code such as <code>eat('jihoon', '감자탕')</code> in a functional style? Here are three approaches I have found effective.</p>
<p><strong>1. Create Cohesion with a Module Namespace</strong></p>
<p>This is the most straightforward approach. Make the file (module) itself correspond to a domain, and use a namespace when importing it. We can use the <code>domain/filing.ts</code> defined earlier as-is.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="typescript" data-theme="github-dark github-light"><code data-language="typescript" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> *</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> as</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> FilingModel </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">from</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> "../domain/filing"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">FilingModel.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">canEdit</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(filing);</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">FilingModel.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">canAmend</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(filing);</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">FilingModel.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">canSubmit</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(filing);</span></span></code></pre></figure>
<p><code>FilingModel.canAmend(filing)</code> is not quite as compact as <code>filing.canAmend()</code>, but the code makes it immediately clear that this function belongs to the Filing domain. It also eliminates the risk of functions from several domains being mixed together.</p>
<p><strong>2. Always Make the Domain Subject the First Argument</strong></p>
<p>Another convention expresses cohesion in a functional style: <strong>always make the first argument the "subject of the behavior."</strong> Consistent signatures such as <code>canAmend(filing)</code> and <code>calculateTotalIncome(income)</code> allow <code>canAmend(filing)</code> to read as "ask canAmend about filing." This also resonates with the Unix pipeline mindset (<code>data |> transform</code>). In fact, Go's method receiver follows precisely this pattern, and Rust's <code>impl</code> blocks accept <code>self</code> as the first argument from the same underlying idea.</p>
<p><strong>3. Bundle Behavior with a Domain Object Factory</strong></p>
<p>This pattern is useful when we miss the cohesion of a class. A factory function returns a domain object and its behavior together.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="typescript" data-theme="github-dark github-light"><code data-language="typescript" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">export</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> function</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> createFilingModel</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">data</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> TaxFiling</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">    ...</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">data,</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">    canEdit</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: () </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> data.status </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">===</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> "draft"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">    canSubmit</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: () </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> data.status </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">===</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> "draft"</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> &#x26;&#x26;</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> data.determinedTax </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">>=</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 0</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">    canAmend</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: () </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">      data.status </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">===</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> "completed"</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> &#x26;&#x26;</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> data.filingType </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">!==</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> "amendment"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  } </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">as</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> const</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> filing</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> createFilingModel</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(rawFiling);</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">filing.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">canAmend</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">filing.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">canEdit</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">();</span></span></code></pre></figure>
<p>This pattern combines the expressiveness of a class (<code>filing.canAmend()</code>) with the practicality of composing behavior through an object literal. Because the returned object has function properties, it is not itself JSON-serializable data. It also creates new function objects each time, but at the scale of data handled on the frontend, this is rarely a performance problem.</p>
<h2 id="how-far-should-we-separate-things"><a class="anchor" href="#how-far-should-we-separate-things">How Far Should We Separate Things?</a></h2>
<p>Clean Architecture describes an ideal structure with three or four layers and defined Ports/Adapters. Applying this structure to every project in the real world, however, can become over-engineering.</p>
<p>Here are the practical guidelines I use.</p>
<ul>
<li><strong>Separate domain types from API response types.</strong> Whether using an <code>interface</code> or a <code>type</code>, define the domain concepts used by the frontend in separate files.</li>
<li><strong>Move logic containing business rules out of components.</strong> It does not have to live in a <code>domain/</code> folder. What matters is making it a pure function that does not depend on React.</li>
<li><strong>Transform API responses into the domain model in one place.</strong> Whether that is a mapper function or a Zod schema, create a structure where changing that one place prevents the change from propagating.</li>
</ul>
<p>As a project grows more complex, the following may also be worth considering.</p>
<ul>
<li><strong>Divide folders by Bounded Context.</strong> The <a href="https://frontend-fundamentals.com/" target="_blank" rel="noopener noreferrer">Toss Frontend Chapter</a> also emphasizes the principle, "Place files that change together in the same directory." Dividing folders by domain naturally reveals domain boundaries through import paths.</li>
<li><strong>Introduce a Use Case layer.</strong> When combinations of domain logic become complex, an Application layer is needed to wrap a scenario such as "retrieve income information → apply expense rate → calculate deductions → calculate tax → finalize refund" into a single function.</li>
</ul>
<pre><code>src/
├── domains/
│   ├── tax/
│   │   ├── tax.ts                  # 세액 계산 도메인 (세율, 공제, 계산 파이프라인)
│   │   ├── tax.viewModel.ts        # 세액 표현 (금액 포맷, 구간 라벨)
│   │   ├── tax.test.ts             # 세액 계산 테스트
│   │   └── incomeMapper.ts         # 홈택스 API ↔ 도메인 변환
│   ├── filing/
│   │   ├── filing.ts               # 신고 상태 도메인 (상태 전이, 권한)
│   │   ├── filing.viewModel.ts     # 신고 표현 (상태 배지, 라벨)
│   │   ├── filing.test.ts
│   │   └── filingMapper.ts
│   └── deduction/
│       ├── deduction.ts            # 공제 항목 도메인 (자격 조건, 한도)
│       └── deduction.viewModel.ts
├── hooks/                           # React 의존 로직
├── components/                      # UI 컴포넌트
└── api/                             # API 호출
</code></pre>
<p>Even within the single domain of tax, <strong>tax calculation (tax)</strong>, <strong>filing management (filing)</strong>, and <strong>deductions (deduction)</strong> are separated into independent subdomains. A change in tax rates does not affect filing state-transition logic, and adding a deduction does not alter the filing-submission flow. This is a practical application of Bounded Context.</p>
<h2 id="conclusion"><a class="anchor" href="#conclusion">Conclusion</a></h2>
<p>To summarize, a <strong>domain</strong> is the problem space we are trying to solve; a <strong>domain model</strong> is a conceptual system that selectively abstracts that problem; a <strong>domain object model</strong> is the implementation of that conceptual system in code; and a <strong>domain object</strong> is an individual object within that implementation.</p>
<p>Putting these concepts into practice on the frontend is not merely a matter of splitting folders. It means <strong>deliberately reasoning about several layers of boundaries</strong>. "Is this a business rule or presentation logic?" "Is this data domain state or UI state?" "Is this function sufficiently cohesive?" Simply making a habit of asking these questions will naturally improve the structure of the code.</p>
<p>Of course, not every project needs every layer of Clean Architecture. Splitting a simple CRUD app into four layers and applying the factory pattern to every domain would be a case where the cure is worse than the disease. Between the elegant cohesion of classes and the practical flexibility of functions, the right answer is determined by the project's complexity and the team's context.</p>
<p>There is no single right answer. But there is a clear difference between <strong>"writing code without knowing what the domain is"</strong> and <strong>"recognizing the domain, reasoning about boundaries, and separating things deliberately."</strong> I hope readers will take a moment to ask themselves, "What is the domain here, and where should this code live?" in their own projects.</p>
<h3 id="references"><a class="anchor" href="#references">References</a></h3>
<details class="ref-block"><summary class="ref-summary">참고 자료</summary><div class="ref-list">
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#f59e0b"></span><a href="https://www.amazon.com/Domain-Driven-Design-Tackling-Complexity-Software/dp/0321125215" target="_blank" rel="noopener noreferrer">Eric Evans, Domain-Driven Design (Book)</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#f59e0b"></span><a href="https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html" target="_blank" rel="noopener noreferrer">Robert C. Martin, Clean Architecture</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#f59e0b"></span><a href="https://khalilstemmler.com/articles/typescript-domain-driven-design/ddd-frontend/" target="_blank" rel="noopener noreferrer">Khalil Stemmler, Does DDD Belong on the Frontend?</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#f59e0b"></span><a href="https://bespoyasov.me/blog/clean-architecture-on-frontend/" target="_blank" rel="noopener noreferrer">Alex Bespoyasov, Clean Architecture on Frontend</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#f59e0b"></span><a href="https://toss.tech/article/income-qa-e2e-automation" target="_blank" rel="noopener noreferrer">Toss, The Journey to E2E Automation</a></div>
</div>
</div></details>]]></content:encoded>
            <author>jihoon7705@gmail.com (이지훈)</author>
            <category>프론트엔드</category>
            <category>아키텍처</category>
            <category>DDD</category>
        </item>
        <item>
            <title><![CDATA[Reflections on Refactoring the Second Toss Frontend Fundamentals Mock Exam]]></title>
            <link>https://hooninedev.com/en/260328</link>
            <guid isPermaLink="false">https://hooninedev.com/en/260328</guid>
            <pubDate>Sat, 28 Mar 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[In this post, I want to share my experience refactoring the project from the second Toss Frontend Fundamentals mock exam. I have always been interested in code review and refactoring, so I decided to ...]]></description>
            <content:encoded><![CDATA[<p>In this post, I want to share my experience refactoring the project from the second Toss Frontend Fundamentals mock exam.</p>
<p>I have always been interested in code review and refactoring, so I decided to take on Toss's Frontend Fundamentals mock exam, which presented the challenge in an intriguing format. The task was to refactor a provided meeting room reservation app. It also came with tests, giving me a safety net for verifying that the refactoring had not broken any functionality.</p>
<p>In the end, I spent two days on the refactoring. Here are my reflections on the process.</p>
<h2 id="my-first-encounter-with-the-code"><a class="anchor" href="#my-first-encounter-with-the-code">My First Encounter with the Code</a></h2>
<p>The first thing I did after opening the code was <strong>read the test specifications</strong>. Tests are the most honest documentation of what an application is supposed to do. I skimmed through <code>App.easy.spec.tsx</code> and <code>App.hard.spec.tsx</code> to understand the application's overall requirements.</p>
<p>I then turned to the implementation itself, where two monolithic components immediately stood out.</p>
<ul>
<li><code>ReservationStatusPage</code> was a component of roughly 400 lines, with date selection, timeline visualization, reservation detail tooltips, the user's reservation list, and cancellation functionality all packed into a single file.</li>
<li><code>RoomBookingPage</code> was a component of roughly 300 lines, with filters, the room list, reservation creation logic, and URL parameter synchronization all tangled together.</li>
</ul>
<p>As I read through the code, rather than immediately deciding that it “needed improvement,” I first focused on <strong>classifying the characteristics of the code</strong>. I distinguished between code that contained domain knowledge, code that behaved like a utility, and code that belonged purely to the UI layer.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="typescript" data-theme="github-dark github-light"><code data-language="typescript" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 도메인 정보: 장비 라벨, 타임 슬롯 등 비즈니스 상수</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> EQUIPMENT_LABELS</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> Record</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">string</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">string</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">> </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  tv: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'TV'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, whiteboard: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'화이트보드'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, video: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'화상장비'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, speaker: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'스피커'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">};</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 유틸리티: 날짜 포맷, 시간 변환</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">function</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> formatDate</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">date</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> Date</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">)</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">...</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> }</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">function</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> timeToMinutes</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">time</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">)</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> number</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">...</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 서버 상태: 인라인 useQuery, useMutation 호출</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">data</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">rooms</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> [] } </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> useQuery</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">([</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'rooms'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">], getRooms);</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">data</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">reservations</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> [] } </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> useQuery</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">([</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'reservations'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, date], () </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> getReservations</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(date));</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// UI + 비즈니스 로직 혼재: 필터링, 정렬, 충돌 감지가 JSX 사이에 산재</span></span></code></pre></figure>
<p>Once I had classified the code this way, it became much easier to see where to begin. I added brief comments to each area of the code to note possible directions for improvement. (It felt a little like migrating the jQuery-based project I encountered when I first joined my current company.)</p>
<p>So where should I start?</p>
<h2 id="developing-a-refactoring-strategy"><a class="anchor" href="#developing-a-refactoring-strategy">Developing a Refactoring Strategy</a></h2>
<p>I planned to carry out the refactoring in the following order.</p>
<ol>
<li><strong>Handle server code</strong>: separate queries and mutations</li>
<li><strong>Separate domain logic</strong>: create Equipment, Room, and Reservation models</li>
<li><strong>Define types</strong>: organize the type system around the domain models</li>
<li><strong>Extract utility functions</strong>: date formatting, timeline calculations, and so on</li>
<li><strong>Separate the UI layer</strong>: divide components into coherent units by concern</li>
<li><strong>Abstract and separate concerns</strong>: handle errors/loading and manage query keys</li>
</ol>
<p>I chose this order so I could work <strong>from the outside of the dependency structure inward</strong>. The idea was to organize the infrastructure—the server code and utilities—first, establish the domain models next, and refine the UI last. If I split the UI components first, I might end up moving still-unorganized domain logic and query code back and forth across multiple components.</p>
<p>With the strategy in place, it was time to execute it one step at a time.</p>
<h2 id="organizing-the-server-code-and-utilities-first"><a class="anchor" href="#organizing-the-server-code-and-utilities-first">Organizing the Server Code and Utilities First</a></h2>
<h3 id="extracting-the-date-formatting-utility"><a class="anchor" href="#extracting-the-date-formatting-utility">Extracting the Date Formatting Utility</a></h3>
<p>The first thing I tackled was the <code>formatDate</code> function because the same function had been defined inline on both pages.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="typescript" data-theme="github-dark github-light"><code data-language="typescript" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// utils/formatYYYYMMDD.ts</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">export</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> function</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> formatYYYYMMDD</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">date</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> Date</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">)</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> year</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> date.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">getFullYear</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> month</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> String</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(date.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">getMonth</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">() </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">+</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 1</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">).</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">padStart</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">2</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'0'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> date</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> String</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(date.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">getDate</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">()).</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">padStart</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">2</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'0'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  return</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> `${</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">year</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">}-${</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">month</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">}-${</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">date</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">}`</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>It was a small change, but it was an important first commit in the refactoring. Starting with the most independent part with the fewest side effects and confirming that the tests still passed served as a kind of <strong>warm-up</strong>.</p>
<h3 id="extracting-the-react-query-hooks"><a class="anchor" href="#extracting-the-react-query-hooks">Extracting the React Query Hooks</a></h3>
<p>Next, I moved the <code>useQuery</code> and <code>useMutation</code> calls that had been written directly inside the components into separate files. I used the <code>queryOptions</code> pattern to turn the query configuration into reusable units.</p>
<p>During this process, I also explicitly defined the API response types in <code>remotes.ts</code>. Values that had previously flowed through the code as <code>any</code> now had clear types such as <code>GetRoomsResponse</code> and <code>GetReservationsResponse</code>.</p>
<p>With the infrastructure layer organized, it was time to turn to the domain models.</p>
<h2 id="separating-the-domain-models"><a class="anchor" href="#separating-the-domain-models">Separating the Domain Models</a></h2>
<p>The most important turning point in the refactoring was <strong>moving the domain models into a dedicated <code>models/</code> directory</strong>.</p>
<p>In the original code, business constants such as <code>EQUIPMENT_LABELS</code> and <code>TIME_SLOTS</code> were declared at the top of component files. Types for <code>Room</code> and <code>Reservation</code> existed only in the server handler (<code>_tosslib/server/types.ts</code>), while the client code used values that were effectively treated as <code>any</code>.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="ts" data-theme="github-dark github-light"><code data-language="ts" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// models/equipment.ts</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">export</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> EQUIPMENT_LABELS</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  tv: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'TV'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, whiteboard: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'화이트보드'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, video: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'화상장비'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, speaker: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'스피커'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">} </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">as</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> const</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">export</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> type</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> Equipment</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> keyof</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> typeof</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> EQUIPMENT_LABELS</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">export</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> ALL_EQUIPMENT</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> Object.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">keys</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">EQUIPMENT_LABELS</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">as</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> Equipment</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">[];</span></span></code></pre></figure>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="ts" data-theme="github-dark github-light"><code data-language="ts" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// models/reservation.ts</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">export</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> interface</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> Room</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  id</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  name</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  floor</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> number</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  capacity</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> number</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  equipment</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> Equipment</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">[];</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">export</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> interface</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> Reservation</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  id</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  roomId</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  date</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  start</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  end</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  attendees</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> number</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  equipment</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> Equipment</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">[];</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>Why is separating domain models important? When business logic is coupled to UI components, changing that logic requires inspecting the component's rendering logic as well. When it exists independently in a <code>models/</code> directory, changes to business rules can be made separately from the UI. Perfect separation is difficult in practice, of course, but the key is at least to create a structure where people can predict that <strong>“this logic will be here.”</strong></p>
<p>Once the domain models were separated, how much lighter could the UI become?</p>
<h2 id="breaking-apart-the-components"><a class="anchor" href="#breaking-apart-the-components">Breaking Apart the Components</a></h2>
<h3 id="reservationstatuspage"><a class="anchor" href="#reservationstatuspage">ReservationStatusPage</a></h3>
<p>This commit produced the most dramatic change and took the most time. I split the 385-line monolithic component as follows.</p>
<pre><code>ReservationStatusPage/
├── index.tsx                    # 페이지 레벨
└── components/
    ├── DateSelector.tsx         # 날짜 선택 UI
    ├── ReservationTimeline.tsx  # 타임라인
    └── MyReservation.tsx        # 내 예약 목록 + 취소
</code></pre>
<p>My criterion for separating the code was <strong>“Does this code have independent meaning?”</strong> Timeline visualization is an independent concern: it takes reservation data for a date and renders a grid. The user's reservation list is another independent concern: it retrieves and cancels the user's reservations. There was no reason for them to live in the same file.</p>
<p>After the split, <code>index.tsx</code> served only as an <strong>orchestrator</strong>. It managed state, displayed messages, and composed the child components, while delegating the actual data fetching and rendering details to those components.</p>
<h3 id="roombookingpage"><a class="anchor" href="#roombookingpage">RoomBookingPage</a></h3>
<p>I applied the same principle to the booking page.</p>
<pre><code>RoomBookingPage/
├── index.tsx                    # 페이지 레벨
├── components/
│   ├── BookingFilter.tsx        # 날짜, 시간, 인원, 장비, 층 UI
│   └── AvailableRoomList.tsx    # 예약 가능 방 목록
└── hooks/
    └── useBookingParams.ts      # URL searchParams 기반 상태 관리
</code></pre>
<p>One interesting decision emerged during this process. At first, I introduced <code>react-hook-form</code> and <code>zod</code> to validate the form. In the end, however, I removed them and replaced them with a custom <code>useBookingParams</code> hook. I will explain that decision in more detail later.</p>
<p>At this point, a natural question arises: how far should abstraction go?</p>
<h2 id="finding-the-right-level-of-abstraction"><a class="anchor" href="#finding-the-right-level-of-abstraction">Finding the Right Level of Abstraction</a></h2>
<p>This was the part I wrestled with most during the mock exam.</p>
<h3 id="how-far-should-nested-conditionals-be-unraveled"><a class="anchor" href="#how-far-should-nested-conditionals-be-unraveled">How Far Should Nested Conditionals Be Unraveled?</a></h3>
<p>The logic that determines whether a room is available combines several conditions: whether its capacity is sufficient, whether it has the required equipment, whether it is on the preferred floor, and whether its schedule overlaps with another reservation. In the original code, all of these conditions were written inline inside a single <code>filter</code> callback.</p>
<p>When I extracted this logic into <code>models/roomFilter.ts</code>, I separated each condition into a <strong>named function</strong>.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="typescript" data-theme="github-dark github-light"><code data-language="typescript" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> isEnoughCapacity</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">room</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> Room</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">attendees</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> number</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> room.capacity </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">>=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> attendees;</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> hasRequiredEquipment</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">room</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> Room</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">equipment</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> Equipment</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">[]) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  equipment.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">every</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">eq</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> room.equipment.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">includes</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(eq));</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> isOnPreferredFloor</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">room</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> Room</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">floor</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> number</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> |</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> null</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  floor </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">===</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> null</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> ||</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> room.floor </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">===</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> floor;</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> hasNoTimeConflict</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">room</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> Room</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">reservations</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> Reservation</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">[], </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">date</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">start</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">end</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  !</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">reservations.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">some</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">reservation</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> reservation.roomId </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">===</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> room.id </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">&#x26;&#x26;</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> reservation.date </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">===</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> date </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">&#x26;&#x26;</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> reservation.start </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">&#x3C;</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> end </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">&#x26;&#x26;</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> reservation.end </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> start);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">export</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> function</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> filterAvailableRooms</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">rooms</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> Room</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">[], </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">reservations</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> Reservation</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">[], </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">params</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> Params</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">)</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> Room</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">[] {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> rooms</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    .</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">filter</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">room</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =></span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">      isEnoughCapacity</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(room, params.attendees) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">&#x26;&#x26;</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">      hasRequiredEquipment</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(room, params.equipment) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">&#x26;&#x26;</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">      isOnPreferredFloor</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(room, params.floor) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">&#x26;&#x26;</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">      hasNoTimeConflict</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(room, reservations, params.date, params.startTime, params.endTime)</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    )</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    .</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">sort</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">((</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">a</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">b</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">      if</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (a.floor </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">!==</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> b.floor) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> a.floor </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">-</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> b.floor;</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">      return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> a.name.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">localeCompare</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(b.name);</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    });</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>The key here was that I extracted a condition into a function <strong>only when there was a clear name for the abstraction</strong>. Names such as <code>isEnoughCapacity</code> and <code>hasRequiredEquipment</code> let readers predict what the functions do without seeing their implementations. If the name had to be vague, like <code>processRoomConditions</code>, the abstraction could instead impose additional cognitive load on the reader.</p>
<p>This is not to say that my approach is the one correct answer. My criterion was simply <strong>“Can I predict the behavior from the function name alone?”</strong> If so, I abstracted it. If not, I thought leaving it inline might actually make the code easier to read.</p>
<h3 id="searchparams-vs-form-state"><a class="anchor" href="#searchparams-vs-form-state">searchParams vs. Form State</a></h3>
<p>I also spent considerable time deciding where the booking filter state should live. In the original code, each filter value was managed with <code>useState</code> and synchronized with the URL searchParams through <code>useEffect</code>.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="typescript" data-theme="github-dark github-light"><code data-language="typescript" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 원본: useState + useEffect 동기화 방식</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> [</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">date</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">setDate</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">] </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> useState</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(searchParams.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">get</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'date'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">||</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> formatDate</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">new</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> Date</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">()));</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> [</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">startTime</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">setStartTime</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">] </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> useState</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(searchParams.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">get</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'startTime'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">||</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> ''</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// ... 6개의 개별 상태</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">useEffect</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(() </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> params</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> Record</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">string</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">string</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">> </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {};</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  if</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (date) params.date </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> date;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">  // ... 모든 상태를 searchParams에 동기화</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  setSearchParams</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(params, { replace: </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">true</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> });</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}, [date, startTime, endTime, </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">...</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">]);</span></span></code></pre></figure>
<p>I initially tried managing it as a form with <code>react-hook-form</code> and <code>zod</code>. Ultimately, however, I removed them and replaced them with a <code>useBookingParams</code> hook that uses <strong>searchParams as the Single Source of Truth</strong>.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="typescript" data-theme="github-dark github-light"><code data-language="typescript" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// useBookingParams: searchParams가 곧 상태</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">export</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> function</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> useBookingParams</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">() {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> [</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">searchParams</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">setSearchParams</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">] </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> useSearchParams</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">();</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> params</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> useMemo</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">BookingParams</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">>(() </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> ({</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    date: searchParams.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">get</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'date'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">||</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> formatYYYYMMDD</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">new</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> Date</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">()),</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    startTime: searchParams.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">get</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'startTime'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">||</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> ''</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">    // ...</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  }), [searchParams]);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> updateParam</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> useCallback</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(&#x3C;</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">K</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> extends</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> keyof</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> BookingParams</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">>(</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">key</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> K</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">value</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> BookingParams</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">[</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">K</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">]) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">    setSearchParams</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">prev</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">      // 기존 파라미터 병합 후 업데이트</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">      return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> result;</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    }, { replace: </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">true</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> });</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  }, [setSearchParams]);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { params, updateParam };</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>The core rationale for this decision was that <strong>“it does not make sense for these states to evolve separately.”</strong> If <code>useState</code> and <code>searchParams</code> each hold their own state, they can fall out of sync depending on when synchronization occurs. If searchParams alone holds the state, however, the URL becomes the application state and the synchronization problem disappears entirely. As a bonus, sharing the URL reproduces the same filter state for another user.</p>
<p>I found similar considerations in other participants' retrospectives. <strong>“We standardized on URL searchParams as the Single Source of Truth,” and “We chose to consolidate the individual filter props into a single <code>filter</code> object.”</strong> The expressions differed, but the underlying recognition was the same: <strong>“Scattered state needs to be grouped into a single concept.”</strong></p>
<h2 id="resilience"><a class="anchor" href="#resilience">Resilience</a></h2>
<h3 id="suspense-and-errorboundary"><a class="anchor" href="#suspense-and-errorboundary">Suspense and ErrorBoundary</a></h3>
<p>I added error and loading handling after the component structure had been finalized. The order matters because you can only decide where to place a boundary after the component tree has been established.</p>
<p>Using the <code>react-error-boundary</code> library, I wrapped each independent data-fetching unit in an <code>ErrorBoundary</code> and <code>Suspense</code>. If the timeline fails, the user's reservation list should still render normally, and vice versa.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{</span><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">/* 각 영역이 독립적으로 에러/로딩을 처리 */</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">ErrorBoundary</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> FallbackComponent</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{ErrorFallback} </span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">resetKeys</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{[date]}></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Suspense</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> fallback</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{&#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Loading</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> message</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"예약 현황을 불러오는 중..."</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> />}></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">ReservationTimeline</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> date</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{date} /></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  &#x3C;/</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Suspense</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">ErrorBoundary</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">ErrorBoundary</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> FallbackComponent</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{ErrorFallback}></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Suspense</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> fallback</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{&#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Loading</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> message</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"내 예약을 불러오는 중..."</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> />}></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">MyReservation</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> onCancel</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{handleCancel} /></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  &#x3C;/</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Suspense</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">ErrorBoundary</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span></code></pre></figure>
<h3 id="centralizing-query-keys"><a class="anchor" href="#centralizing-query-keys">Centralizing Query Keys</a></h3>
<p>As I extracted the query hooks during the refactoring, query keys became scattered across multiple files. This made it difficult to determine which key to use for invalidation in a mutation's <code>onSuccess</code> callback.</p>
<p>I introduced <code>@lukemorales/query-key-factory</code> to manage the query keys centrally.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="typescript" data-theme="github-dark github-light"><code data-language="typescript" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// queries/queryKeys.ts</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">export</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> roomKeys</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> createQueryKeys</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'rooms'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, {</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  list: { queryKey: </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">null</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">queryFn</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: () </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> remotes.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">getRooms</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">() },</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">});</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">export</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> reservationKeys</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> createQueryKeys</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'reservations'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, {</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  list</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: (</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">date</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> ({ queryKey: [date], </span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">queryFn</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: () </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> remotes.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">getReservations</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(date) }),</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  my: { queryKey: </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">null</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">queryFn</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: () </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> remotes.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">getMyReservations</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">() },</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">});</span></span></code></pre></figure>
<p>This makes it possible to use them in the form <code>useSuspenseQueries({ queries: [roomKeys.list, reservationKeys.list(date)] })</code>, ensuring that a query key and its fetching function always travel together. I also extracted route paths into <code>PATHS</code> constants to eliminate hard-coded strings.</p>
<h2 id="what-was-the-exam-designed-to-assess"><a class="anchor" href="#what-was-the-exam-designed-to-assess">What Was the Exam Designed to Assess?</a></h2>
<p>After completing the refactoring, I took a step back and reflected. What was this mock exam trying to evaluate?</p>
<p>While reading other participants' retrospectives, I noticed an interesting common thread. Nearly every post included the sentence <strong>“Code is not read; it is predicted.”</strong> Our brains do not interpret code one line at a time. We read by making predictions based on patterns accumulated through experience, and cognitive load rises sharply when those predictions are violated.</p>
<p>From this perspective, the mock exam was assessing not merely coding ability, but the collaborative skill of <strong>“making code as predictable as possible for your colleagues.”</strong> (Perhaps the ability to read the minds of exam authors and coworkers is the true mark of a software engineer.)</p>
<p>Looking through other participants' retrospectives, I related to comments such as <strong>“Understanding code written by someone else is not easy,” and “Designing the interface first is important, but that approach can falter when you face a vast existing codebase.”</strong> I had a similar experience. When existing code already works, there is a temptation to rationalize its structure: “It already works, so why bother?” But the point of the mock exam was to move past that temptation and judge the code by <strong>“how quickly someone other than me can understand it, and whether I can assess and solve the problem using my own judgment.”</strong></p>
<h2 id="what-i-learned-from-refactoring"><a class="anchor" href="#what-i-learned-from-refactoring">What I Learned from Refactoring</a></h2>
<p><strong>The order of refactoring shapes the outcome.</strong> Working from the outside—the infrastructure—inward to the UI was a safe path that kept the process from becoming tangled midway through. Once the utilities and domain models were organized, splitting the components made each component's dependencies clear.</p>
<p><strong>The criterion for abstraction is the name.</strong> If the name of an extracted function or variable can explain its behavior, it is worth abstracting. If the name must remain vague, leaving the code inline may be the better choice.</p>
<p><strong>Where state lives is architecture.</strong> State that needs to move together should live in the same place. Using searchParams alone as the source of truth is structurally healthier than synchronizing <code>useState</code> with <code>searchParams</code>.</p>
<h2 id="closing-thoughts"><a class="anchor" href="#closing-thoughts">Closing Thoughts</a></h2>
<p>After finishing the task, I discussed it with two colleagues. Things I had not noticed while examining the code alone began to emerge as I worked through my thoughts in conversation. The moment someone asked “Why did you do it that way?” about a structural choice I had taken for granted, gaps in my reasoning that I had not recognized became visible.</p>
<p>It is true that AI is dramatically reducing the time required to write and review code. Even so, experiences like this are precisely why I believe code reviews and daily meetings remain important. AI can verify the consistency of code, but pointing out <strong>“Here is the perspective you missed”</strong> ultimately falls to colleagues who share the same context. Discovering what I could not see, then using that discovery to make the product more stable—is that not the essence of collaboration?</p>
<p>There is no single correct answer when writing code as part of solving a problem. Other participants who took the same mock exam followed different paths, each with their own rationale. What matters is being able to explain <strong>“why the code was written this way.”</strong> I encourage readers to look at their own code through the eyes of someone seeing it for the first time. That perspective may be the most powerful standard for determining code quality.</p>]]></content:encoded>
            <author>jihoon7705@gmail.com (이지훈)</author>
            <category>프론트엔드</category>
            <category>React</category>
            <category>리팩토링</category>
        </item>
        <item>
            <title><![CDATA[The AI Frontend Engineer]]></title>
            <link>https://hooninedev.com/en/260302</link>
            <guid isPermaLink="false">https://hooninedev.com/en/260302</guid>
            <pubDate>Mon, 02 Mar 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[In this post, I want to share my personal perspective on how engineers can grow and survive alongside AI. One of the articles that made the strongest impression on me as a junior was Hwidong Bae’s “Fr...]]></description>
            <content:encoded><![CDATA[<p>In this post, I want to share my personal perspective on <strong>how engineers can grow and survive alongside AI</strong>.</p>
<p>One of the articles that made the strongest impression on me as a junior was Hwidong Bae’s <a href="https://kr.linkedin.com/posts/hwidongbae_%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4-%EC%BB%A4%EB%A6%AC%EC%96%B4-%EB%A1%9C%EB%93%9C%EB%A7%B5-%EC%A3%BC%EB%8B%88%EC%96%B4%EB%A5%BC-%EC%9C%84%ED%95%9C-3%EA%B0%80%EC%A7%80-%EC%A0%84%EB%AC%B8%EC%84%B1-%ED%8A%B8%EB%9E%99-activity-7013888624140189696-XiIz" target="_blank" rel="noopener noreferrer">“Frontend Engineer Career Roadmap: Three Specialization Tracks for Juniors”</a>. It organizes frontend engineering careers into three tracks—<strong>web specialization (Software Engineer), product specialization (Product Engineer), and operations specialization (Full-Stack Engineer)</strong>—and goes on to identify “five foundational capabilities of exceptional engineers” and “three keys to becoming a senior.” At the time, the biggest question was which capabilities to build for each track. Yet less than two years after I read it, the question itself has changed completely.</p>
<p>When I talk with fellow engineers these days, their concerns feel noticeably different from those I had been hearing over the past several years.</p>
<ul>
<li>“Our company adopted AI, and if we give it a design mockup, it builds almost everything. It’s convenient, but...”</li>
<li>“The hiring market is brutally cold.”</li>
<li>“I’m afraid to merge AI-generated code as is, but reviewing every line defeats the efficiency gains. I’m not sure what to do.”</li>
</ul>
<p>I went through—and am still going through—a similar period. Just a year or two ago, I thought of AI as merely a useful assistant. Now, it is hard to imagine developing without it at all (I am also asking Claude to help with research as I write this). Think of this article as a sequel to Hwidong Bae’s piece: my attempt to describe how the landscape has changed since then and, from my own perspective, what additional capabilities frontend engineers need to develop within it.</p>
<p>Once again, I have tried to find and verify as many sources as possible. But because this field changes so quickly, some parts may already be outdated by the time this article is published. If you have a rebuttal or something worth discussing, please leave a comment anytime.</p>
<h2 id="doesnt-ai-do-everything-now"><a class="anchor" href="#doesnt-ai-do-everything-now">“Doesn’t AI Do Everything Now?”</a></h2>
<p>There is one question we need to address first. Is the statement “AI does everything now” actually true? How much of it is reality, and where does the fantasy begin?</p>
<p>In February 2025, <a href="https://x.com/karpathy/status/1886192184808149383" target="_blank" rel="noopener noreferrer">Andrej Karpathy</a>, an OpenAI co-founder and former director of AI at Tesla, posted the following on Twitter.</p>
<blockquote class="bilingual-quote"><div class="quote-translation" lang="ko"><p>There is a new kind of coding I call “vibe coding,” where you fully surrender to the vibes, embrace exponential growth, and even forget that the code itself exists.</p></div><div class="quote-original" lang="en"><p>There's a new kind of coding I call 'vibe coding', where you fully give in to the vibes, embrace exponentials, and forget that the code even exists.</p></div></blockquote>
<p>In short, <strong>vibe coding</strong> is “a way of coding where you hand the keyboard to AI and simply describe what you want in natural language.” There are no architecture documents, no boilerplate, and no hunting for semicolons. The code simply runs on vibes. In less than a year, the term became standard vocabulary in English-speaking developer communities.</p>
<p>Yet exactly one year later, in February 2026, the same Karpathy <a href="https://thenewstack.io/vibe-coding-is-passe/" target="_blank" rel="noopener noreferrer">took a step back</a>. He proposed replacing the term vibe coding with <strong>“agentic engineering.”</strong> The distinction is clear.</p>
<ul>
<li><strong>Vibe coding</strong>: Describing what you want and accepting the result</li>
<li><strong>Agentic engineering</strong>: Designing the system, specifying constraints, and using AI to accelerate an implementation you have already reasoned through in your head</li>
</ul>
<p>A year ago, the baseline assumption was, “Just tell it what to do and it will build everything.” Now, “the ability to design what to ask AI to do and how to ask it” has itself become an engineering capability. This shift is not merely one person’s tweet. Around the same time, Google engineer <a href="https://addyosmani.com/" target="_blank" rel="noopener noreferrer">Addy Osmani</a> published <a href="https://www.amazon.com/Beyond-Vibe-Coding-AI-Era-Developer/dp/B0F6S5425Y" target="_blank" rel="noopener noreferrer">Beyond Vibe Coding: From Coder to AI-Era Developer</a>, stating plainly: “AI is an assistant, not an autonomously trustworthy coder. You are the senior developer, and the LLM exists to accelerate your judgment.”</p>
<h3 id="the-tools-are-racing-ahead"><a class="anchor" href="#the-tools-are-racing-ahead">The Tools Are Racing Ahead</a></h3>
<p>The tools are also evolving rapidly in step with this shift. As of May 2026, the most frequently discussed coding tools include Cursor, Claude Code, GitHub Copilot, Windsurf, v0 by Vercel, Bolt.new, and Devin.</p>
<p>The evolution of v0 is especially symbolic. Vercel uses the phrase <a href="https://venturebeat.com/infrastructure/vercel-rebuilt-v0-to-tackle-the-90-problem-connecting-ai-generated-code-to" target="_blank" rel="noopener noreferrer">“the 90% problem”</a>, meaning that 90% of real-world development takes place within existing codebases and infrastructure. At first, v0 only needed to excel at greenfield prototypes. Now it can import a GitHub repository directly, work within it, enforce a design system, and automatically pull in deployment environment variables. In effect, toolmakers are directly answering senior engineers who ask, “Isn’t AI only good at making toy demos?”</p>
<p>Big Tech codebases illustrate this change most clearly.</p>
<p>Google’s Sundar Pichai <a href="https://fortune.com/2024/10/30/googles-code-ai-sundar-pichai/" target="_blank" rel="noopener noreferrer">announced during the Q3 2024 earnings call that “more than 25% of new code is generated by AI and then reviewed and accepted by engineers”</a>, and said in April 2025 that the figure had risen above 30%. Microsoft’s Satya Nadella <a href="https://www.cnbc.com/2025/04/29/satya-nadella-says-as-much-as-30percent-of-microsoft-code-is-written-by-ai.html" target="_blank" rel="noopener noreferrer">revealed at LlamaCon in April 2025 that “as much as 30% of our code is written by AI”</a>. Meta’s internal target has risen as high as “by the first half of 2026, 65% of engineers will generate more than 75% of their commits with AI.”</p>
<p>The trend is no different in Korea. <a href="https://toss.tech/article/toss-frontend-ai-docs" target="_blank" rel="noopener noreferrer">Toss</a> built an AI-powered documentation system to improve developer experience so developers no longer have to search for documentation, and went a step further by exploring topics such as <a href="https://toss.tech/article/removing_designers_in_ai_era" target="_blank" rel="noopener noreferrer">“What Happened When We Removed Designers in the AI Era”</a>. Every Tuesday, Karrot shares team experiments through <a href="https://medium.com/daangn" target="_blank" rel="noopener noreferrer">AI Show &#x26; Tell</a>, and has begun using the <a href="https://about.daangn.com/blog/archive/%EB%8B%B9%EA%B7%BC-%ED%95%B4%EC%BB%A4%ED%86%A4-%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4-%EC%B1%84%EC%9A%A9/" target="_blank" rel="noopener noreferrer">recruiting slogan</a> “Beyond Engineer, Become a Builder.” Woowa Brothers has published articles such as <a href="https://techblog.woowahan.com/22828/" target="_blank" rel="noopener noreferrer">“In an Era When AI Writes Code, Do You Still Want to Become a Developer?”</a>, arguing that “the essence of being a developer lies not in code, but in the ability to define and solve problems.”</p>
<h3 id="but-the-numbers-tell-a-slightly-different-story"><a class="anchor" href="#but-the-numbers-tell-a-slightly-different-story">But the Numbers Tell a Slightly Different Story</a></h3>
<p>At this point, it is easy to conclude, “So now you can just tell AI what to do and everything works.” But the actual data tells a somewhat different story.</p>
<p>First, consider the figures from the <a href="https://survey.stackoverflow.co/2025/ai" target="_blank" rel="noopener noreferrer"><strong>2025 Stack Overflow Developer Survey</strong></a>, a comprehensive analysis of the state of software development.</p>
<ul>
<li>84% of developers said they use or plan to use AI tools, up from 76% in 2024.</li>
<li>Among professional developers, 51% use AI tools every day.</li>
<li>Yet <strong>positive sentiment toward AI tools actually declined</strong>. After exceeding 70% in both 2023 and 2024, it fell to 60% in 2025.</li>
<li>Senior developers with more than ten years of experience have the lowest trust in AI output.</li>
</ul>
<p>In short: <strong>“Everyone uses it, but the longer they do, the less they trust it.”</strong></p>
<p>A 2025 experiment conducted by the nonprofit research institute <a href="https://metr.org/blog/2025-07-10-early-2025-ai-experienced-os-dev-study/" target="_blank" rel="noopener noreferrer"><strong>METR</strong></a> revealed the gap between perception and reality even more dramatically. It was a controlled experiment in which 16 experienced open-source developers—averaging five years of experience and 1,500 commits—completed 246 tasks and were randomly assigned whether to use AI. The results were as follows.</p>
<ul>
<li>Before starting, the developers predicted that AI would make them 24% faster.</li>
<li>Immediately after finishing, they still estimated that they had been about 20% faster.</li>
<li>Actual measurements, however, showed that they were <strong>19% slower</strong>.</li>
</ul>
<p>The researchers’ explanation is revealing. The acceptance rate for AI-generated code was below 44%; rejected code still required time to review and test; and even accepted code demanded substantial review and revision. This gap—the illusion of feeling faster despite actually slowing down—is one reason senior developers are becoming increasingly skeptical of AI.</p>
<p>Nor is the quality of AI-generated code itself especially polished. Consider <a href="https://www.veracode.com/blog/genai-code-security-report/" target="_blank" rel="noopener noreferrer"><strong>Veracode’s</strong></a> experiment in which more than 100 AI models were asked to write code.</p>
<ul>
<li><strong>45% of AI-generated code contained OWASP Top 10 security vulnerabilities</strong>.</li>
<li>The failure rate for <strong>XSS (cross-site scripting) protection was 86%</strong>.</li>
<li>The failure rate for log injection protection was 88%.</li>
<li>Another study reported that the vulnerability density of AI code was <strong>2.7 times higher</strong> than that of human-written code.</li>
</ul>
<p>The 86% failure rate for XSS, in particular, deserves serious attention from frontend engineers. It shows exactly what it means to merge an AI-generated form input unchanged. (Developers with frontend security-audit experience feel uneasy and concerned even when they write <code>dangerouslySetInnerHTML</code> themselves; it feels even more frightening when AI quietly slips it in.)</p>
<p>The quality signals are similar elsewhere. <a href="https://www.gitclear.com/ai_assistant_code_quality_2025_research" target="_blank" rel="noopener noreferrer"><strong>GitClear</strong></a> analyzed 211 million lines of code changes made between 2020 and 2024 and found the following.</p>
<ul>
<li>Code churn—the share of code reverted within two weeks of being written: 5.5% in 2020 → <strong>7.9%</strong> in 2024</li>
<li>Share of changes devoted to refactoring: 25% in 2021 → <strong>below 10%</strong> in 2024</li>
<li>Copy-and-paste (clone) rate: 8.3% in 2021 → <strong>12.3%</strong> in 2024 (and an astonishing fourfold increase in 2025)</li>
</ul>
<p>The interpretation is not difficult. Our ability to produce code quickly has increased, while our ability to produce code worth refining has declined. <a href="https://www.softwareseni.com/ai-generated-code-security-risks-why-vulnerabilities-increase-2-74x-and-how-to-prevent-them/" target="_blank" rel="noopener noreferrer">Apiiro’s data</a> from Fortune 50 companies is even more striking. AI-assisted developers produce three to four times as many commits as their peers, but also ten times as many security findings. Privilege-escalation paths rose by 322%, while architectural design flaws surged by 153%.</p>
<h2 id="what-ai-has-replacedand-what-it-has-not"><a class="anchor" href="#what-ai-has-replacedand-what-it-has-not">What AI Has Replaced—and What It Has Not</a></h2>
<p>The tools are racing ahead, yet the numbers are nuanced. So what exactly has AI replaced, and what has it not yet replaced? We need a clear distinction to understand where to invest our time.</p>
<p>What AI has replaced is a large share of developers’ manual typing. We spend less time writing boilerplate and repetitive code; given a design mockup, a screen that follows the conventions can appear within minutes; and the time spent searching for syntax and APIs, as well as the learning curve, has fallen sharply. In short, AI has <strong>flattened the speed of production</strong>.</p>
<p>But it has not yet replaced the domain of “judgment.” (More precisely, it has “not yet met our expectations.” People differ in their ability to use AI, but this discussion is based on the average user experience.)</p>
<p>The first sticking point is <strong>translating requirements into specifications</strong>. Turning ambiguous business requirements into precise edge cases and state machines still requires deeper human involvement. The same is true of <strong>understanding system-wide impact</strong>. Questions such as how a component affects the bundle, whether a dependency can be tree-shaken, or how a data-fetching pattern affects the <a href="https://web.dev/articles/vitals" target="_blank" rel="noopener noreferrer">Core Web Vitals</a> metric <a href="https://web.dev/articles/inp" target="_blank" rel="noopener noreferrer">INP (Interaction to Next Paint)</a> may receive plausible answers from AI, but a human still needs to examine them before we can feel confident.</p>
<p>We also cannot omit <strong>security and risk assessment</strong>, as illustrated by the 45% OWASP vulnerability rate above. The same applies to <strong>maintaining the design system and consistency</strong>—checking whether a new component aligns with the existing system’s tokens, accessibility rules, and interaction patterns—and to <strong>understanding customer and market context</strong>, such as why a feature is needed and where it belongs in the user flow.</p>
<p>Finally, borrowing a phrase from <a href="https://yceffort.kr/2026/02/frontend-engineering-in-ai-era" target="_blank" rel="noopener noreferrer">yceffort’s article</a>, <strong>managing cognitive debt</strong>—the “gap between a system’s complexity and the degree to which the team understands that system”—has actually become a faster-growing concern since AI adoption. Closing that gap remains a human responsibility.</p>
<blockquote>
<p>Developers are not disappearing; the form of developers’ work is changing. The bottleneck has shifted from “the speed of building” to “the speed of deciding.”</p>
</blockquote>
<p>In the same vein, Toss’s <a href="https://toss.tech/article/will-ai-replace-developers" target="_blank" rel="noopener noreferrer">“Will AI Replace Developers?”</a> offers a weightier diagnosis. Its central point is this: AI is not replacing the entire workforce; it is removing the apprenticeship ladder. Ten or twenty years from now, when today’s senior engineers retire, there may be too few people in the next generation capable of designing complex systems. This is not merely a question of “what will our company do about hiring next year?” It is a delayed-action bomb for the entire industry. (I think it is an exceptionally well-written article for a time when so many of us are wrestling with these questions.)</p>
<p>The “first version that works” produced by AI is 70%. The remaining 30%—getting it to “a version safe to serve to real users”—belongs to humans. And the ability to fill that 30% does not appear overnight. This is the essence of the apprenticeship-ladder problem. If the time spent “getting your hands dirty” writing boilerplate and simple components disappears, so do the people who will eventually be able to fill that 30%.</p>
<p>Hwidong Bae’s original article named <strong>writing good code, maximizing present value (balancing fast delivery with long-term maintainability), making data-driven decisions, helping colleagues make effective decisions, and learning continuously</strong> as the “five foundational capabilities of exceptional engineers.” All five remain relevant in the AI era, but the last is in the most precarious position. Learning itself has not disappeared; its subject has changed. Where we once learned “how do I use this tool?”, we now need to spend time learning “how does this entire system work?” More troubling still is the problem <a href="https://evan-moon.github.io/2026/04/18/developers-who-stopped-growing-in-ai-era/" target="_blank" rel="noopener noreferrer">Evan Moon identifies</a>: “the moment AI takes over code writing, the brain’s cognitive load drops dramatically.” Reduced cognitive load sounds appealing, but it is dangerous because that load was the very material from which learning emerged. <strong>The more comfortable it gets, the less you grow.</strong></p>
<p>This naturally raises a question. Do the three tracks in Hwidong Bae’s article—web specialization, product specialization, and operations specialization—no longer matter?</p>
<p>I see it differently. The tracks themselves remain valid. It is more accurate to say that each has evolved by one stage for the AI era. Let us examine how the landscape has changed for each one.</p>
<h2 id="from-producer-to-verifier"><a class="anchor" href="#from-producer-to-verifier">From Producer to “Verifier”</a></h2>
<p>In Hwidong Bae’s original article, the web specialization track was grouped under the title <strong>Software Engineer</strong>. Its core capabilities were “a deep understanding and practical command of the internet, web browsers, and HTML/CSS/JS,” familiarity with the trade-offs of web ecosystem tools, troubleshooting experience, and sensitivity to emerging technologies. The suggested senior paths were <strong>engineer at a web tooling company, frontend educator, or tech lead in a complex product organization</strong>. In short, these were “people who dig deeply into how browsers and HTML/CSS/JS work,” and until a year or two ago, their greatest advantage was that “they could write correct code more accurately than anyone else.”</p>
<p>How has their value changed in the AI era? In terms of coding speed alone, AI has caught up. Yet <strong>the ability to accurately evaluate AI-generated code</strong> has become something they almost uniquely possess.</p>
<ul>
<li>Non-developer using AI: It implemented my requirements, and it works correctly.</li>
<li>Developer using AI: It works, but this dependency could cause certain problems, and improving this pattern in this way would better fit our conventions. Let’s examine the related areas again.</li>
</ul>
<p>The Veracode study discussed earlier found failure rates of 86% for XSS and 88% for log injection. Experts like us are the people capable of finding and fixing those failures. They naturally evolve into senior quality-assurance roles for AI output.</p>
<p>There is another development: an entirely new topic has entered the expert domain—<strong>Generative UI</strong> and <strong>AI interface design</strong>. Examples include chat UIs that render LLM responses as they stream, abort controls that let users stop generation midway, progressive rendering of Markdown and code blocks, UX that displays tool-call results inline, and assistant integrations using the <a href="https://sdk.vercel.ai/" target="_blank" rel="noopener noreferrer">Vercel AI SDK</a> or <a href="https://modelcontextprotocol.io/" target="_blank" rel="noopener noreferrer">MCP (Model Context Protocol)</a>. Demand is exploding for people who “understand precisely how the web works while also understanding the behavioral characteristics of LLMs and knowing how to apply them.”</p>
<h2 id="the-natural-evolution-into-a-product-engineer"><a class="anchor" href="#the-natural-evolution-into-a-product-engineer">The Natural Evolution into a Product Engineer</a></h2>
<p>The product specialization track has benefited the most. People who understand markets and customers well and communicate frequently with external stakeholders gained a far more powerful tool the moment AI entered the picture. Another hallmark of this track was that its suggested senior paths included expansion into other roles, such as <strong>growth engineer or consultant, or a transition to PM, PO, or CPO</strong>.</p>
<p>An interesting change is that the track’s name has begun to establish itself as a global standard. The original article already called it “Product Engineer,” but when I first read it, the term still felt somewhat unfamiliar. A year later, it has become established enough that <a href="https://leerob.com/product-engineers" target="_blank" rel="noopener noreferrer">Vercel renamed every “Fullstack Engineer” role in its job descriptions to “Product Engineer”</a>.</p>
<p>Lee Robinson identifies three core qualities of a Product Engineer.</p>
<ul>
<li><strong>Iteration-mindedness</strong>: Moving quickly through the deploy → feedback → adjustment cycle.</li>
<li><strong>Customer centricity</strong>: Improving the product by speaking directly with customers.</li>
<li><strong>Pragmatism</strong>: “Every technology choice is merely a means to an end.” Tools that do not advance the product goal are discarded without hesitation.</li>
</ul>
<p>There is one trap here: it is dangerous when product-focused engineers are perceived merely as “people who build quickly.” With the arrival of AI, that danger has grown. “Shipping features fast” is now something people in virtually any role can do with AI tools. A Product Engineer’s differentiator is “the ability to define the customer’s problem accurately and validate it quickly with the smallest possible solution,” not “having fast hands.”</p>
<p>Within this trend, <strong>Design Engineer</strong> has begun to rise into a formal role. Vercel is hiring <a href="https://cjroth.com/blog/2026-02-18-building-an-elite-engineering-culture" target="_blank" rel="noopener noreferrer">design engineers as a formal career track with compensation above $200K</a>, and Linear and Stripe are moving in similar directions. The role eliminates the handoff between frontend and design itself. Because AI can draw quickly, the combined ability to decide “what to draw” and determine “whether the result fits a consistent design system” has become even scarcer.</p>
<h2 id="ai-orchestrator"><a class="anchor" href="#ai-orchestrator">AI Orchestrator</a></h2>
<p>The operations specialization track is changing most dramatically. Hwidong Bae’s original article classified this track as <strong>Full-Stack Engineer</strong>, defining it as “someone deeply interested in project structure, integration, testing, and deployment; someone who can handle simple APIs and infrastructure directly, fill gaps in the organization, and improve processes.” Over the past year or two, <strong>the role of operating AI agents themselves</strong> has been added on top, rapidly expanding the track’s scope.</p>
<p>In a review of <a href="https://beyond.addy.ie/2026-trends/" target="_blank" rel="noopener noreferrer">2026 trends</a>, <strong>“Orchestrating Coding Agents”</strong> was identified as a central concept. It means going beyond assigning work to one AI and instead designing and operating a system in which multiple AI agents collaborate simultaneously. In the same vein, the <a href="https://github.com/addyosmani/agent-skills" target="_blank" rel="noopener noreferrer">“agent-skills”</a> framework was proposed, along with the idea of encoding professional workflows, quality gates, and industry best practices directly into agent behavior.</p>
<p>After reviewing the related material, these are the new keywords I believe engineers in the operations track need to work with.</p>
<ul>
<li><strong>MCP (Model Context Protocol)</strong>: Anthropic’s proposed standard for connecting LLMs with external tools</li>
<li><strong>AI governance</strong>: Managing who can use AI, with what context, and whether secrets can leak</li>
<li><strong>Agent evaluation</strong>: Pipelines that automatically score the output produced by agents</li>
<li><strong>AI gates</strong>: Automated security and quality verification before PR merges, plus labeling for AI-generated code</li>
</ul>
<p>The original article named roles such as <strong>platform team engineer in a large organization, tech lead, agile coach, technical program manager (TPM), and CTO</strong> as senior paths for the operations track. Those paths remain valid, but new roles such as <strong>“AI development infrastructure lead”</strong> and <strong>“developer productivity (DevProd) engineer”</strong> have now been added.</p>
<p>While the three tracks are each evolving, there are also capabilities that have become more important across all of them. I originally wanted to think in terms of five years from now, but at today’s pace of progress, even a year feels like too large a unit. So I will narrow the horizon to “next year” and identify the capabilities I expect to become more important.</p>
<h2 id="five-capabilities"><a class="anchor" href="#five-capabilities">Five Capabilities</a></h2>
<p><strong>The first is the ability to write specifications.</strong> In the AI era, the “starting point of coding” is not the keyboard but the <strong>specification</strong>. The ability to state precisely what AI should do has become more important than the code itself. A specification here does not mean a grand RFC document. It can be <strong>tests</strong> that express the expected behavior of business logic in code, <strong>Storybook stories</strong> that define a UI component’s scenarios and visual contract, or <strong>type definitions</strong> that specify the contract for data flow. Ultimately, the work is about establishing in advance the criteria that will automatically verify AI-generated output. If AI coding proceeds without those criteria, problems accumulate.</p>
<p><strong>The second is verification and judgment.</strong> AI confidently produces code that looks plausible but is wrong. That is why I believe “the ability to review AI-generated code quickly and accurately” is itself a core capability. It means determining whether security headers, input sanitization, or CSRF tokens are missing; whether accessibility—ARIA, keyboard navigation, and focus traps—remains intact; and whether there are performance issues involving rendering cost, memory, or bundle size. Throwing AI slop into a PR without review is a dereliction of an engineer’s duty. A human still clicks the merge button, and that responsibility cannot be shifted onto AI. Senior developers may have the lowest trust in AI in the Stack Overflow survey precisely because they have the eye to catch these details.</p>
<p><strong>The third is system understanding and architectural thinking.</strong> AI handles one file at a time well and has strong awareness of flows and relationships. AI fixes symptoms quickly, but highly capable developers find root causes. One way to build this capability is through deliberate practices such as Architecture Retrospectives. As the rate of code change accelerates, cognitive debt will accumulate quickly unless the team deliberately raises its understanding of the system as well.</p>
<p><strong>The fourth is AI orchestration.</strong> The ability to work with AI is separating into its own skill set. This is no longer simply about writing good prompts. It now encompasses the ability to break work into small tickets, choose which model to use for a given task, design agent evaluation and verification pipelines, and define recovery strategies—including rollback—when an agent fails. <a href="https://sourcegraph.com/blog/revenge-of-the-junior-developer" target="_blank" rel="noopener noreferrer">Steve Yegge</a> describes this progression as <strong>six waves (traditional → completions → chat → coding agents → agent clusters → agent fleets)</strong>.</p>
<p><strong>The fifth is Context Engineering.</strong> Promoted since mid-2025 by <a href="https://www.faros.ai/blog/context-engineering-for-developers" target="_blank" rel="noopener noreferrer">Karpathy and Shopify CEO Tobi Lütke</a>, it can be summarized as “the ability to design what context to show AI, in what form, and how much of it.” In practice, it takes forms such as maintaining <strong>CLAUDE.md / rules files</strong> that place project conventions, architectural principles, and prohibitions within AI’s reach; <strong>deliberately reducing context</strong> by selecting only relevant modules instead of loading every file; <strong>explicitly separating phases</strong>—planning → implementation → verification—into different sessions to prevent context contamination; and supplying <strong>external context through MCP</strong>, connecting design systems, API schemas, monitoring data, and other resources through standard interfaces. <a href="https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents" target="_blank" rel="noopener noreferrer">Anthropic’s official documentation</a> calls this “the new prompt engineering” and states plainly that a single prompt can never contain a system’s architectural knowledge, patterns, and tribal wisdom. Put differently, “designing an environment where AI consistently receives good context” has become far more important than “writing one good prompt.”</p>
<p>If you have read this far, a natural question follows: how, specifically, should we study? I use roughly four approaches.</p>
<h2 id="how-to-learn"><a class="anchor" href="#how-to-learn">How to Learn</a></h2>
<p>The “continuous learning” item from the original article remains valid, but we need to allocate our learning time differently.</p>
<p>There are areas where we once spent a great deal of time but can now spend less. By contrast, there are complex areas that we may have avoided because they were difficult or time-consuming. The latter include writing test specifications, using performance measurement tools (<a href="https://developer.chrome.com/docs/lighthouse" target="_blank" rel="noopener noreferrer">Lighthouse</a>, <a href="https://www.webpagetest.org/" target="_blank" rel="noopener noreferrer">WebPageTest</a>, Chrome DevTools Performance), accessibility (<a href="https://www.w3.org/WAI/standards-guidelines/wcag/" target="_blank" rel="noopener noreferrer">WCAG</a>), and security—especially the <a href="https://owasp.org/www-project-top-ten/" target="_blank" rel="noopener noreferrer">OWASP Top 10</a>. There are also entirely new areas to learn, including the Vercel AI SDK, LangChain.js, MCP, streaming UI patterns, and agent evaluation pipelines. <strong>It is important to recognize which capabilities you need and allocate your time accordingly.</strong></p>
<p>AI-generated code tends to grow large. It can produce hundreds of lines in a minute. Unless we deliberately manage PR size and merge cadence, code review itself breaks down. Within companies, average PR size rose 18%, incidents per PR rose 24%, and the change failure rate rose 30% after AI adoption. Viewed alongside the data discussed earlier, this shows why it is important to subdivide work: writing a large change and merging it all at once makes it difficult to understand the flow and preserve intent.</p>
<p>There is a practice directly related to the reduced cognitive-load problem identified in Evan Moon’s article: it is worth setting aside one or two hours a day to write code without AI. Drawing an architecture by hand or reading through code in an unfamiliar area one line at a time are examples. (I also write code without AI during the drowsy period after lunch each day. It is time I use to keep from drifting too far from old, familiar habits.)</p>
<p>This is not merely about “avoiding forgetting the old way.” Your own depth does not grow during the time AI does the work for you. Capabilities such as verification, judgment, and system understanding are functions of the time you spend confronting problems directly.</p>
<h2 id="so-where-does-that-leave-us"><a class="anchor" href="#so-where-does-that-leave-us">So, Where Does That Leave Us?</a></h2>
<p>I have written at length, but the image of a frontend engineer who survives the AI era is not actually so different from the conclusion of the original article. It identified three qualities of a good senior engineer.</p>
<ul>
<li>They strive to <strong>stay grounded in the fundamentals</strong>. (Continuously maintaining and strengthening the five foundational capabilities.)</li>
<li>Even without being the designated leader, they exert natural influence through exemplary behavior.</li>
<li>They do not settle for completing the task in front of them; they examine the broader context and create greater impact.</li>
</ul>
<p>Applied to the AI era, that becomes the following.</p>
<ul>
<li>Stay grounded in the <strong>fundamentals</strong> beyond AI-generated code: the web, systems, and the domain.</li>
<li>Set the direction yourself, not AI. Even when you are not the designated owner, decide “where we should go.”</li>
<li>Use AI not merely as a personal productivity tool, but to remove bottlenecks in the team and the system.</li>
</ul>
<p>Looking at the writing of Andrej Karpathy, an authority on OpenAI, the core of the <strong>“agentic engineering”</strong> he now emphasizes is ultimately the same: design the system, specify the constraints, and use AI to accelerate an implementation you have already reasoned through in your head. The tools may change, but the directional controls remain in human hands.</p>
<p>The original article’s closing message was likewise that the person who becomes a senior is someone who “does not settle for completing the assigned work well, but examines the surrounding context and creates greater impact.” In the AI era, only the definition of that “impact” has changed. One person merges a screen AI built in an hour because “it works.” Another spends thirty more minutes examining how reasonable it is in terms of accessibility, security, performance, and system consistency. A year later, it is the latter who will be recognized as a senior. The people who survive will be those who stand on the 30% side of the boundary between 70% (functionality) and 30% (application and effective use).</p>
<p>I hope the frontend engineers reading this leave with their own answer to the question, “What should I study next?” No one knows the definitive answer, but I am quite confident that the more AI writes our code, the more the people who can see “what lies beyond the code” will survive. I will close with the hope that, a year from now, I can write again about how this landscape has changed.</p>
<p><strong>(If this article feels painfully obvious or outdated a year from now, perhaps that will mean we responded well enough.)</strong></p>
<h2 id="references"><a class="anchor" href="#references">References</a></h2>
<details class="ref-block"><summary class="ref-summary">참고 자료</summary></details>]]></content:encoded>
            <author>jihoon7705@gmail.com (이지훈)</author>
            <category>프론트엔드</category>
            <category>커리어</category>
            <category>AI</category>
        </item>
        <item>
            <title><![CDATA[Abstraction]]></title>
            <link>https://hooninedev.com/en/260201</link>
            <guid isPermaLink="false">https://hooninedev.com/en/260201</guid>
            <pubDate>Sun, 01 Feb 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[In this post, I want to talk about abstraction in programming and how to write good code from the perspective of abstraction. As a frontend developer, I have wrestled countless times with questions li...]]></description>
            <content:encoded><![CDATA[<p>In this post, I want to talk about abstraction in programming and how to write good code from the perspective of abstraction.</p>
<p>As a frontend developer, I have wrestled countless times with questions like, “How much of this logic should I separate?” and “At what level should I break up this component?” At first, I thought abstraction simply meant extracting what different pieces had in common: turning repeated code into a function, or identifying the commonalities among similar components and combining them into one. But after watching code built this way turn into a monster that became harder to touch over time on several occasions, I began to rethink what abstraction really is.</p>
<p>In this article, I want to organize the thoughts I have developed about the essence of abstraction and how we can use it in frontend development to create good code.</p>
<h2 id="abstract-and-abstraction"><a class="anchor" href="#abstract-and-abstraction">Abstract and Abstraction</a></h2>
<p>Before getting into the main discussion, I want to clarify exactly what the words “abstract” and “abstraction” mean in programming. They may look similar, but they are quite different in nature.</p>
<p><strong>Abstract</strong> is a state and a property. When we say, “This is abstract,” we mean that concrete details have been omitted, leaving <strong>only the essential concept</strong>. Classes or methods marked with the <code>abstract</code> keyword in Java or TypeScript carry precisely this meaning. They are incomplete blueprints that define only the essential form, before the concrete implementation has been filled in.</p>
<p><strong>Abstraction</strong> is a process and an act. It is the process of simplifying something complex by retaining only its essential characteristics and removing unnecessary details. Crucially, abstraction is not about “roughly lumping things together”; it is <strong>the act of defining roles precisely at each level</strong>.</p>
<p>In everyday language, “abstract” often carries the nuance of “vague.” In programming, however, abstraction is the exact opposite. Its purpose is not to make things vague, but to create a new level of meaning that can be absolutely precise. The essence of abstraction is preserving information relevant to a given context and forgetting what is irrelevant.</p>
<p>Ultimately, the distinction between abstract and abstraction can be understood like this: <strong>the abstract is “a state in which only the essence remains,” while abstraction is “the process of leaving only the essence.”</strong> What we do when designing code is precisely this process of abstraction: retaining only the essential interface from a complex implementation and hiding the rest.</p>
<p>Then why do we need abstraction in programming?</p>
<h2 id="why-do-we-need-abstraction"><a class="anchor" href="#why-do-we-need-abstraction">Why Do We Need Abstraction?</a></h2>
<p>The fundamental reason abstraction is necessary in programming is surprisingly simple: <strong>to build more complex things</strong>. When we try to build something more complex, there are too many complex elements to remember and manage all at once. So we group those elements and turn them into simplified, abstract concepts.</p>
<p>Consider React, which frontend developers use every day. Rendering a single component internally involves complex processes such as creating the Virtual DOM, reconciliation, and manipulating the actual DOM. Yet we can write JSX without worrying about any of that, because React abstracts those complex processes away for us.</p>
<p>Look at the code below. When using the UserProfile component, we can build and work with the UI perfectly well without knowing about the complex processes happening internally, such as VDOM creation and diffing.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">UserProfile</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> name</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"jihoon"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> /></span></span></code></pre></figure>
<p>Configuring Webpack directly used to be part of a frontend developer’s daily life, but frameworks such as Next.js and Vite have since abstracted away bundler configuration. As a result, we can develop applications without knowing the bundler’s inner workings and use that time to <strong>focus on higher-level problems such as business logic and user experience</strong>. (This is why I believe the concept of abstraction is extremely important to the role of a frontend developer.)</p>
<p>This, ultimately, is the core value of abstraction: hiding complexity so that it appears simple and allowing each of us to focus only on our own domain. Thanks to it, we can build increasingly large and complex software without having to understand everything ourselves.</p>
<p>If abstraction is so beneficial, then is more abstraction always better? Let us consider what purpose abstraction should serve.</p>
<h2 id="reducing-context"><a class="anchor" href="#reducing-context">Reducing Context</a></h2>
<p>Many developers understand abstraction as “extracting the common parts.” That is not wrong, but it is only one technique for performing abstraction; it does not explain abstraction’s essence.</p>
<p>To me, the essence of abstraction is <strong>“reducing the context a reader needs to know to an appropriate level.”</strong></p>
<p>Let us look at a simple example.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="typescript" data-theme="github-dark github-light"><code data-language="typescript" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">interface</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> Order</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  id</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  status</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> "pending"</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> |</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> "completed"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  amount</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> number</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">let</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> total </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 0</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> orders</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> Order</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">[] </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> [</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  { id: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"a"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, status: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"pending"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, amount: </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">10000</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> },</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  { id: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"b"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, status: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"completed"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, amount: </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">5000</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> },</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  { id: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"c"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, status: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"completed"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, amount: </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">8000</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> },</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">];</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">for</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">let</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> i </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 0</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">; i </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">&#x3C;</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> orders.</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">length</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">; i</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">++</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  if</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (orders[i].status </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">===</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> "completed"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) {</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    total </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">+=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> orders[i].amount;</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  }</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>A developer reading this code has to understand the loop’s initialization, condition, and increment; follow element access by index; inspect the conditional branch; and keep track of how the external accumulator is updated. What the code is actually trying to do can be expressed in one sentence—<strong>“calculate the total of the completed orders”</strong>—yet understanding that sentence requires holding four different contexts in mind at once.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="typescript" data-theme="github-dark github-light"><code data-language="typescript" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> total</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> orders</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  .</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">filter</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">((</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">order</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> order.status </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">===</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> "completed"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  .</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">reduce</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">((</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">sum</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">order</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> sum </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">+</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> order.amount, </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">0</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">);</span></span></code></pre></figure>
<p>Thanks to the abstractions <code>filter</code> and <code>reduce</code>, the developer only needs to follow two intentions: “select only completed orders” and “accumulate their amounts.” The context of index management and the declaration and updating of an accumulator disappears from the surface of the code.</p>
<p>We can take this one step further.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="typescript" data-theme="github-dark github-light"><code data-language="typescript" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> total</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> sumCompletedOrders</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(orders);</span></span></code></pre></figure>
<p>Now the reader does not even need to know that the calculation is performed by traversing an array. All that remains is the business intent: “calculate the total amount of completed orders.” The code lets us focus not on <strong>how</strong> the calculation is performed, but on <strong>what</strong> is being calculated.</p>
<p>From this perspective, we can see that the React code we write every day is itself a composition of countless abstractions.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { css } </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">from</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> "@emotion/css"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { format } </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">from</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> "date-fns"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> TodayHeader</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> () </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> now</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> new</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> Date</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(); </span><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// Date 객체 생성이라는 복잡한 과정을 추상화</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> formatted</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> format</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(now, </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"yyyy-MM-dd"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">); </span><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 날짜 포맷팅 로직을 추상화</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    &#x3C;</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">h1</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">      className</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">css</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">`</span></span>
<span data-line=""><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">        font-size: 1.8rem;</span></span>
<span data-line=""><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">      `</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    ></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">      {</span><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">/* CSS-in-JS 처리 과정을 추상화 */</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">      Today is {formatted}</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    &#x3C;/</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">h1</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">> </span><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// React.createElement를 추상화</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  );</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">};</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 그리고 위 모든 것을 다시 추상화</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">TodayHeader</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> />;</span></span></code></pre></figure>
<p>What if all the internal code of <code>emotion</code>, <code>date-fns</code>, and <code>react</code> were expanded directly inside this component file? It would be difficult to know where to begin reading or distinguish the business logic from the library code. Because abstraction hides the context of each domain at an appropriate level, we can focus solely on the essential purpose: “show today’s date.”</p>
<p>Then how should we approach abstraction when designing actual code?</p>
<h2 id="what-it-means-for-a-level-of-abstraction-to-be-high-or-low"><a class="anchor" href="#what-it-means-for-a-level-of-abstraction-to-be-high-or-low">What It Means for a Level of Abstraction to Be High or Low</a></h2>
<p>Any discussion of abstraction must include the concept of the <strong>Level of Abstraction</strong>. What exactly does it mean for code’s level of abstraction to be “high” or “low”?</p>
<p><strong>Code at a low level of abstraction</strong> is close to the concrete procedures performed by the computer: parsing strings directly, iterating through arrays by index, manipulating bytes. It openly exposes <strong>how</strong> it works.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 추상화 수준이 낮은 코드</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> response</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> await</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> fetch</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'/api/users'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> users</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> await</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> response.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">json</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> filteredUsers</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> users.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">filter</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">user</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> user.status </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">===</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> 'active'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">filteredUsers.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">forEach</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">user</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> element</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> document.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">getElementById</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">`user-${</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">user</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">.</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">id</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">}`</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  if</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (element) element.style.display </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> 'block'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">});</span></span></code></pre></figure>
<p><strong>Code at a high level of abstraction</strong> is expressed in the language of the business domain or problem space. Examples include <code>processPayment(order)</code>, <code>sendNotification(user, message)</code>, and <code>validateUserInput(formData)</code>. High-level code reveals <strong>what</strong> it does while hiding how it does it.</p>
<p>In <em>Clean Code</em>, Robert C. Martin summarized this idea as the principle of <strong>“One Level of Abstraction per Function.”</strong> When high-level and low-level code are mixed within one function, the reader must decide at every line, “Is this core logic, or is it an implementation detail?”</p>
<p>The problem becomes clear in actual code.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="typescript" data-theme="github-dark github-light"><code data-language="typescript" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 추상화 수준이 뒤섞인 함수</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">async</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> function</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> registerUser</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">name</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">email</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">password</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) {</span></span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">  // ✅ 높은 수준: 비즈니스 규칙 검증</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  validateUserInput</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({ name, email, password });</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  await</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> ensureEmailNotDuplicated</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(email);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">  // ❌ 낮은 수준: 비밀번호 해싱 직접 구현</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> encoder</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> new</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> TextEncoder</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> data</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> encoder.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">encode</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(password);</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> hashBuffer</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> await</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> crypto.subtle.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">digest</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"SHA-256"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, data);</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> hashedPassword</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> Array.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">from</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">new</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> Uint8Array</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(hashBuffer))</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    .</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">map</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">((</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">b</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> b.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">toString</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">16</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">).</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">padStart</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">2</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"0"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">))</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    .</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">join</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">""</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">  // ✅ 높은 수준: 사용자 저장</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> user</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> await</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> userRepository.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">save</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({ name, email, password: hashedPassword });</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">  // ❌ 낮은 수준: 이메일 전송 직접 구현</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> verifyToken</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> crypto.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">randomBytes</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">32</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">).</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">toString</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"hex"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  await</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> db.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">execute</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">    "INSERT INTO email_tokens (user_id, token, expires_at) VALUES (?, ?, ?)"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    [user.id, verifyToken, </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">new</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> Date</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(Date.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">now</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">() </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">+</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 86_400_000</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">)]</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  );</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  await</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> transporter.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">sendMail</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    from: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"noreply@example.com"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    to: user.email,</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    subject: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"Welcome! Please verify your email"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    html: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">`&#x3C;a href="/verify?token=${</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">verifyToken</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">}">Verify your account&#x3C;/a>`</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  });</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">  // ✅ 높은 수준: 환영 이메일 발송</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  await</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> sendWelcomeEmail</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(user);</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>The reader of this function begins by following the high-level, business-rule context of the “user registration flow,” only to be abruptly dragged down into the low-level contexts of hash buffer manipulation, SQL queries, and email template strings. Then the code jumps back up to the high-level <code>sendWelcomeEmail</code>. When the level of abstraction keeps rising and falling like this, the reader’s mind has to rise and fall with it.</p>
<p>If we rewrite the same function so that its level of abstraction is consistent, it looks like this.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="typescript" data-theme="github-dark github-light"><code data-language="typescript" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 추상화 수준이 일관된 함수</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">async</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> function</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> registerUser</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">name</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">email</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">password</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) {</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  validateUserInput</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({ name, email, password });</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  await</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> ensureEmailNotDuplicated</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(email);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> hashedPassword</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> await</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> hashPassword</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(password);</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> user</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> await</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> userRepository.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">save</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({ name, email, password: hashedPassword });</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  await</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> sendVerificationEmail</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(user);</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  await</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> sendWelcomeEmail</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(user);</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>Every statement speaks at the same level of abstraction. How email delivery is implemented and which algorithm password hashing uses are the responsibilities of their respective lower-level functions. The reader of this function only needs to focus on a single context: the overall user registration flow.</p>
<p>Martin also called this <strong>“The Stepdown Rule.”</strong> When code is read from top to bottom, it should resemble a newspaper article: the big picture appears at the top, and the details emerge as the reader moves downward.</p>
<p>Kent Beck presented the same principle as the <strong>Composed Method pattern</strong> in <em>Smalltalk Best Practice Patterns</em>. A method should consist only of operations at the same level of abstraction, and each step should be expressed as a one-line method call.</p>
<p>All of this ultimately leads to one conclusion: <strong>a function should speak at only one level of abstraction.</strong> Following this principle alone makes a noticeable difference in readability.</p>
<p>Then which direction should abstraction take? Should we begin with the concrete or with the abstract?</p>
<h2 id="think-in-terms-of-assembling-parts-not-extracting-commonalities"><a class="anchor" href="#think-in-terms-of-assembling-parts-not-extracting-commonalities">Think in Terms of Assembling Parts, Not Extracting Commonalities</a></h2>
<p>OOP commonly offers the guideline, “Extract the commonalities among concrete things to define something abstract.” The approach itself is not wrong, but I believe adhering to it too rigidly risks producing a design trapped by the current requirements.</p>
<p>Consider an example. Suppose the requirements call for buttons A, B, and C. All three are blue and rounded; the only difference is their label text. A design based solely on extracting their commonalities might look like this.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> BlueRoundButton</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> ({ </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">label</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">label</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> }) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> &#x3C;</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">button</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> className</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"blue round"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">>{label}&#x3C;/</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">button</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">>;</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">};</span></span></code></pre></figure>
<p>This perfectly satisfies the current requirements. But a few days later, the product manager says:</p>
<blockquote>
<p>“Please make it possible to change the color of button B.”</p>
</blockquote>
<p>At that point, even the name <code>BlueRoundButton</code> becomes awkward. We could add a color prop, but the design was vulnerable to change from the outset because it began with the concrete commonality of “a blue rounded button.” (And this is a mild example. In reality, countless requests arrive about the button’s shape, size, and everything else.)</p>
<p>After this happens repeatedly, one thing becomes clear: <strong>when we extract commonalities from concrete requirements, even the resulting abstraction tends to reflect only the requirements that exist right now</strong>.</p>
<p>That is why I prefer the opposite direction. Instead of extracting abstractions from concrete things, I prefer to <strong>think of abstract parts first and assemble them into concrete things</strong>.</p>
<p>Imagine building a toast notification component. The initial requirement is simple: “When saving is complete, display a short message at the bottom.” An approach based on extracting commonalities would look like this.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 현재 요구사항의 공통 특성에서 출발한 설계</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">type</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> ToastProps</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  message</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  hasAction</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">?:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> boolean</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  actionLabel</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">?:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  onAction</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">?:</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> () </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> void</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">};</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> Toast</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> ({ </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">message</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">hasAction</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">actionLabel</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">onAction</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> ToastProps</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  &#x3C;</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">div</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> className</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"toast"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    &#x3C;</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">span</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">>{message}&#x3C;/</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">span</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    {hasAction </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">&#x26;&#x26;</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> &#x3C;</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">button</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> onClick</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{onAction}>{actionLabel}&#x3C;/</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">button</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">>}</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  &#x3C;/</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">div</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">);</span></span></code></pre></figure>
<p>The current requirement is handled perfectly. But a few days later, the product manager says, “Add an icon on the left depending on whether it is a success or a warning.” <code>hasIcon</code> and <code>iconName</code> are added. Soon another request arrives: “We also need a toast with an upload progress bar.” Another <code>progress</code> prop is added. After repeating this a few times, <code>Toast</code> ends up with more than ten props and hidden rules about <strong>which combinations are valid and which are not</strong> that developers must memorize. (And those rules are usually not documented even in comments.)</p>
<p>The design was vulnerable to change because it began with a <strong>concrete, present-day image of “what a toast should look like.”</strong></p>
<p>The story changes when we approach it as an assembly of parts.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 부품을 조립하는 설계</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> Toast</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> ({ </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">children</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> PropsWithChildren</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  &#x3C;</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">div</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> className</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"toast"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">>{children}&#x3C;/</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">div</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">Toast.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">Icon</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> ({ </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">name</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">name</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> "check"</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> |</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> "warn"</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> |</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> "info"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> }) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">  /* ... */</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">};</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">Toast.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">Message</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> ({ </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">children</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> PropsWithChildren</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> &#x3C;</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">span</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">>{children}&#x3C;/</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">span</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">>;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">Toast.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">Action</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> ({</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  children,</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  onClick,</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> PropsWithChildren</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;{ </span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">onClick</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> () </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> void</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> }>) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  &#x3C;</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">button</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> onClick</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{onClick}>{children}&#x3C;/</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">button</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">Toast.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">Progress</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> ({ </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">value</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">value</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> number</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> }) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">  /* ... */</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">};</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 필요한 부품만 골라 조립한다</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Toast</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Toast.Message</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">>저장되었습니다&#x3C;/</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Toast.Message</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Toast</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">>;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Toast</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Toast.Icon</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> name</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"check"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> /></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Toast.Message</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">>업로드 완료&#x3C;/</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Toast.Message</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Toast.Action</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> onClick</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{undo}>실행 취소&#x3C;/</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Toast.Action</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Toast</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">>;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Toast</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Toast.Progress</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> value</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">0.4</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">} /></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Toast.Message</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">>파일 전송 중...&#x3C;/</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Toast.Message</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Toast</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">>;</span></span></code></pre></figure>
<p>The stable essence—“a toast is a shallow container that holds something”—is now separated from the changeable concrete detail of “what it contains.” We can add as many new parts as we like, or arrange existing parts in new combinations, without touching <code>Toast</code> itself. The validity rules for prop combinations also disappear. We simply <strong>put in what we want to put in</strong>.</p>
<p>Of course, an experienced developer might say, “Couldn’t we just design it with IoC (Inversion of Control) from the beginning?” That is true. But such judgment is possible because countless past mistakes have cultivated an intuition for which parts are likely to change.</p>
<p>When that intuition is not yet well developed, beginning with the question, “What parts make up this feature, and how should those parts be assembled?” makes it much easier to create a design that is open to change.</p>
<p>At this point, a natural question arises: by what criteria should we divide the parts, and how should we present them externally?</p>
<h2 id="three-principles-for-good-abstraction"><a class="anchor" href="#three-principles-for-good-abstraction">Three Principles for Good Abstraction</a></h2>
<h3 id="think-carefully-about-expression"><a class="anchor" href="#think-carefully-about-expression">Think Carefully About Expression</a></h3>
<p>The most important virtue of an abstracted module is that its behavior should be inferable without opening its source code. Kent Beck called this the <strong>“Intention-Revealing Name”</strong> pattern and argued that if an abstraction cannot be given a concise name, the abstraction itself should be reconsidered.</p>
<p>Broadly speaking, we have two tools for achieving this: <strong>names</strong> and <strong>types</strong>.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="typescript" data-theme="github-dark github-light"><code data-language="typescript" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 도대체 뭘 하는 건지 알 수 없는 함수</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">function</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> calculate</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">price</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> number</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">rate</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> number</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">)</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> number</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 이름과 타입만으로 동작을 유추할 수 있는 함수</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">function</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> calculateDiscountedPrice</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">originalPrice</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> number</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">discountRate</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> number</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">)</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> number</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span></code></pre></figure>
<p>From the name alone, <code>calculateDiscountedPrice</code> tells us that it takes an original price and a discount rate and calculates the discounted price, while the type information supports this by showing that it accepts <code>number</code> values and returns a <code>number</code>. We do not need to know which calculation logic is applied internally.</p>
<p>By contrast, <code>calculate(price: number, rate: number): number</code> gives us no information about what is being calculated, so we cannot anticipate the result. We can only use it after opening the source code, which forfeits the benefit of abstraction.</p>
<p>It is worth noting that naming itself reflects the level of abstraction. Function names in programming generally take the form <strong>verb + noun</strong>, and the verb we choose reveals the level of abstraction at which the function operates.</p>
<blockquote>
<p>A verb alone does not determine the level of abstraction. The noun that accompanies it—the domain context—determines the final level.</p>
</blockquote>
<p>Certain verbs often appear at a <strong>low</strong> level of abstraction: <code>parse</code>, <code>encode</code>, <code>decode</code>, <code>serialize</code>, <code>read</code>, <code>write</code>, <code>push</code>, <code>pop</code>, <code>convert</code>, and <code>transform</code>. These words imply physical transformations of data or direct manipulation of data structures.</p>
<p>At the middle level, verbs such as <code>get</code>, <code>save</code>, <code>load</code>, and <code>validate</code> appear. They describe technical operations while revealing their intent to some degree.</p>
<p>At a <strong>high</strong> level of abstraction, verbs such as <code>register</code>, <code>refund</code>, <code>confirm</code>, <code>cancel</code>, and <code>submit</code> are used. These words belong to the language of the business domain. They reveal nothing about the technical procedures happening internally and express only <strong>a user action or business process</strong>.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="typescript" data-theme="github-dark github-light"><code data-language="typescript" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 낮은 수준: 기술적 동작이 드러남</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">function</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> parseJSON</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">text</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">)</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> object</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">function</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> encodeBase64</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">data</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> Uint8Array</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">)</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 중간 수준: 의도가 드러나되 기술적 맥락이 남아있음</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">function</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> getUserById</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">id</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">)</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> Promise</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">User</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">>;</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">function</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> validateEmail</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">email</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">)</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> boolean</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 높은 수준: 비즈니스 의도만 드러남</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">function</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> registerUser</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">form</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> RegistrationForm</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">)</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> Promise</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">User</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">>;</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">function</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> refundPayment</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">orderId</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> OrderId</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">amount</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> Money</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">)</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> Promise</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">Refund</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">>;</span></span></code></pre></figure>
<p>In <em>Clean Code</em>, Robert C. Martin wrote that <strong>“a long descriptive name is better than a short enigmatic name.”</strong> He also proposed the principle <strong>“pick one word per concept.”</strong> If <code>fetch</code>, <code>retrieve</code>, and <code>get</code> are mixed for operations in the same context, the reader is left wondering, “Do these three do different things?”</p>
<p>The same principle applies directly to naming React components and hooks.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Button</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> />           </span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">SearchInput</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> />      </span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">SubmitOrderButton</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> /></span></span></code></pre></figure>
<p>The specificity of a component’s name varies with its level of abstraction. Button is a general-purpose UI primitive at a lower level, whereas SubmitOrderButton clearly expresses business intent at a higher level.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> handleSubmit</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> async</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">data</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> FormData</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { </span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  await</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> registerUser</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(data);</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">};</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Form</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> onSubmit</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{handleSubmit} />           </span></span></code></pre></figure>
<p><code>on*</code> is the name of the prop a component exposes externally. It lets the component’s consumer declare which event to respond to. <code>handle*</code> is the name of the implementation function actually passed to that prop.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> user</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> useAuth</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">();                  </span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> [</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">items</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">setItems</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">] </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> useCartItems</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">isOpen</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">toggle</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> } </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> useModal</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">();   </span></span></code></pre></figure>
<p>Custom hooks use the <code>use</code> prefix to follow React’s rules and make the state or behavior they provide available to components.</p>
<blockquote>
<p>Jeff Atwood, who runs <a href="https://blog.codinghorror.com/" target="_blank" rel="noopener noreferrer">Coding Horror</a>, once pointed out the problem with the <code>Manager</code> suffix. A name like <code>UrlManager</code> tells us nothing about whether it pools, validates, or creates URLs. Names that reveal a specific role—such as <code>UrlBuilder</code>, <code>UrlValidator</code>, or <code>UrlPool</code>—are far better. An ambiguous name can be a sign that the module’s responsibility itself is ambiguous.</p>
</blockquote>
<p>Ultimately, a good name is one that <strong>immediately tells the reader the level of abstraction at which the code operates</strong>.</p>
<h3 id="deliberately-design-the-degree-of-input-freedom"><a class="anchor" href="#deliberately-design-the-degree-of-input-freedom">Deliberately Design the Degree of Input Freedom</a></h3>
<p>One question arises frequently when designing an abstracted module: “How much functionality should we expose?” This decision has a major impact on the experience of developers who use the module.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 기능이 닫힌 컴포넌트</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> Button</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> ({ </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">children</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">children</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">?:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> React</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">ReactNode</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> &#x3C;</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">button</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">>{children}&#x3C;/</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">button</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">>;</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">};</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 기능이 완전히 열린 컴포넌트</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> Button</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">props</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> ComponentProps</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"button"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">>) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> &#x3C;</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">button</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">...</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">props} />;</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">};</span></span></code></pre></figure>
<p>The first button can accept only <code>children</code>. It cannot be given <code>onClick</code>, <code>type</code>, or <code>disabled</code>. In return, its users have nothing to deliberate over.</p>
<p>The second button can accept every attribute of the <code>button</code> element. It offers a high degree of freedom, but its users must decide which of dozens of props they should use. I describe this situation as <strong>“the component forcing developers to deliberate.”</strong></p>
<p>There is no single correct answer. The appropriate level depends on the module’s purpose and its users. For a design system’s base button, restricting Props may be better for maintaining consistency. For a general-purpose utility component, keeping the interface flexible may be preferable.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 지나치게 닫힌 인터페이스 — 다양한 상황에 대응 불가</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Button</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> onClick</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{handleSubmit}>제출&#x3C;/</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Button</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// onClick 외의 이벤트, className, disabled 등을 전달할 방법이 없다</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 지나치게 열린 인터페이스 — 의도가 사라짐</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Button</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">...</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">anyProps} /></span></span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 무엇을 전달해야 하는지 사용자가 직접 파악해야 한다</span></span></code></pre></figure>
<p>The breadth of an abstraction should be determined by who its users are. A low-level interface is appropriate for users who need to understand and finely control the internal implementation. On the other hand, giving an overly open interface to users who should not need to know the details only adds confusion. Conversely, restricting inputs too heavily for users who must handle varied situations can make the module unusable in the first place.</p>
<h3 id="keep-the-unit-of-abstraction-appropriate"><a class="anchor" href="#keep-the-unit-of-abstraction-appropriate">Keep the Unit of Abstraction Appropriate</a></h3>
<p>The unit of abstraction—how much to group into a single module—is another important consideration.</p>
<p>One common frontend antipattern is the over-extraction of Custom Hooks.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 이 훅은 단 하나의 컴포넌트에서만 사용된다</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> useUserProfileData</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> () </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> [</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">user</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">setUser</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">] </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> useState</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">null</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> [</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">loading</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">setLoading</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">] </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> useState</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">true</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  useEffect</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(() </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">    fetchUser</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">      .</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">then</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(setUser)</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">      .</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">finally</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(() </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> setLoading</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">false</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">));</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  }, []);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { user, loading };</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">};</span></span></code></pre></figure>
<p>If logic used by only one component is separated into a hook unnecessarily, the reader has to move back and forth between two files to understand the context. Instead of reducing context, the abstraction has increased it.</p>
<p>The reverse is also a problem: putting too much into a single hook.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 관련 없는 관심사가 하나의 훅에 뒤섞여 있다</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> useEverything</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> () </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> auth</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> useAuth</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> theme</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> useTheme</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> analytics</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> useAnalytics</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> toast</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> useToast</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">();</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { auth, theme, analytics, toast };</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">};</span></span></code></pre></figure>
<p>This kind of “God Hook” is difficult to test, and changing one thing can affect unrelated parts.</p>
<p>The criterion for choosing the right unit of abstraction is <strong>“Does this separation actually reduce the context required by the reader?”</strong> If separating the code instead scatters the context and makes it harder to understand, the time for that abstraction has not yet come.</p>
<h2 id="beware-of-premature-abstraction"><a class="anchor" href="#beware-of-premature-abstraction">Beware of Premature Abstraction</a></h2>
<p>After reading this far, one question may remain: “So when should we abstract?” My view is this: <strong>our default premise should be not to abstract prematurely.</strong></p>
<p>Unless there is a clear signal for abstraction, leaving code as it is gives us a better worst-case outcome than creating the wrong abstraction and later having to unravel it. The process by which a bad abstraction emerges tends to look like this.</p>
<ol>
<li>A similar pattern appears in code A and code B.</li>
<li>We think, “The DRY principle says I should extract this into a shared function!” and abstract it.</li>
<li>A similar pattern appears in code C, so we use the same function but add one parameter to accommodate slightly different behavior.</li>
<li>As code D and E begin using it as well, conditionals and parameters continue to accumulate.</li>
<li>The function is now used everywhere, yet everyone is afraid to touch it.</li>
</ol>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="typescript" data-theme="github-dark github-light"><code data-language="typescript" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 처음에는 단순했던 함수가...</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> formatUserName</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">user</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> User</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> `${</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">user</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">.</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">firstName</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">} ${</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">user</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">.</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">lastName</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">}`</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 요구사항이 추가될 때마다 매개변수가 늘어나고...</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> formatUserName</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  user</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> User</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  includeMiddleName</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">?:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> boolean</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  format</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">?:</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> "full"</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> |</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> "short"</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> |</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> "initials"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  locale</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">?:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  honorific</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">?:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> boolean</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  if</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (format </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">===</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> "initials"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) {</span></span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">    /* ... */</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  }</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  if</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (includeMiddleName </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">&#x26;&#x26;</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> user.middleName) {</span></span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">    /* ... */</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  }</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  if</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (honorific </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">&#x26;&#x26;</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> locale </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">===</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> "ko"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) {</span></span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">    /* ... */</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  }</span></span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">  // ...끝없는 분기</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">};</span></span></code></pre></figure>
<p>If we find ourselves in this situation, the solution is clear. Inline the abstracted code back into each call site, remove the unnecessary code from each one, and then, from that clean state, abstract again only if a genuine commonality becomes visible. <strong>“The fastest way forward is to go back.”</strong></p>
<p>Then when should we abstract? In my experience, the <strong>signals for abstraction</strong> look roughly like these.</p>
<ul>
<li><strong>Consistency is breaking down.</strong> The same logic is inline in one component and separated into its own function in another. The same calculation is scattered throughout the codebase.</li>
<li><strong>The internal structure is being exposed externally without need.</strong> The caller is forced to manage implementation details it has no reason to know.</li>
<li><strong>A module keeps exposing its own procedure.</strong> It fails to hide its internal steps, forcing consumers to follow those steps themselves.</li>
</ul>
<p>The problem is that while detecting these signals is generally straightforward, <strong>in practice, it is easy to ignore them and focus on satisfying more “important” requirements</strong>. Under deadline pressure or absorbed in implementing a feature, we tell ourselves, “It works for now; I’ll clean it up later,” and later rarely comes.</p>
<p>Another crucial point is to <strong>maintain consistent criteria for abstraction</strong>. If the same kind of logic is inline in one part of the codebase, extracted into a function in another, and separated into a custom hook somewhere else, a new reader will wonder, “Is there an intentional reason for these differences?” Whether a team chooses to abstract or not, its criteria must be consistent.</p>
<p>Joel Spolsky’s <strong>“Law of Leaky Abstractions,”</strong> proposed in 2002, is also worth remembering in this context. The law says that abstractions attempt to hide complex implementations, but the details of those implementations eventually leak out. In other words, even when an abstraction is designed so that its users should not need to know the internal implementation, situations arise in which they must understand it to use the abstraction correctly.</p>
<p>TCP abstracts an unreliable network so that it appears to be a reliable connection, but when a cable is disconnected, the abstraction breaks. React abstracts UI updates declaratively, but optimizing rerenders ultimately requires understanding its internal behavior. Because no abstraction is perfect, we must ask when creating one: <strong>“Can users respond when this abstraction breaks?”</strong></p>
<p>Ultimately, <strong>“Abstractions save us time working; they do not save us time learning.”</strong></p>
<h2 id="abstraction-is-something-we-internalize"><a class="anchor" href="#abstraction-is-something-we-internalize">Abstraction Is Something We Internalize</a></h2>
<p>I once discussed abstraction with a colleague, and something from that conversation stayed with me: detecting the signals for abstraction and separating code at the right time and at the right level are ultimately matters of <strong>intuition</strong>.</p>
<p>The principles discussed earlier—keeping levels of abstraction consistent, choosing good names, and designing the degree of input freedom—are certainly important. But trying to recall each principle and consciously weigh “Should I separate this or not?” at the very moment we write code can instead disrupt our flow. Just as consciously thinking about the angle of your elbow while throwing a jab during a match can make you miss the timing, abstraction while coding should emerge from natural intuition rather than conscious judgment.</p>
<p>There are moments while writing code when a feeling of resistance begins to surface: “This logic does not seem like it belongs here,” or “This component seems to know too much.” That feeling is the signal for abstraction, and internalization means being able to detect and respond to it naturally.</p>
<p>This intuition, however, is not built overnight. Only after studying countless patterns, reading a wide variety of code, and making plenty of mistakes yourself does the feeling that <strong>“this seems like something I should separate”</strong> begin to arise naturally. When a colleague later asks, “Why did you separate this?” and you can naturally answer, “I separated it because of X,” then the principle has become internalized.</p>
<p>I think the same is true in any field. Trying to become good by memorizing rules can make judgment harder. Ultimately, we need to hold on to the broad direction while letting the details fill themselves in naturally. And that naturalness comes from the variety of patterns and experiences we have accumulated over time.</p>
<h2 id="closing-thoughts"><a class="anchor" href="#closing-thoughts">Closing Thoughts</a></h2>
<p>Abstraction in programming is the act of hiding complexity so that it appears simple and allowing readers of the code to focus only on the context they need.</p>
<p>To revisit what is worth remembering in order to create good abstractions:</p>
<ul>
<li>As a default premise, do not abstract prematurely; separate code only when a clear signal appears.</li>
<li>Let each function speak at <strong>one level of abstraction</strong> only.</li>
<li><strong>Express</strong> behavior fully through names and types so that the code can be used without opening its source.</li>
<li><strong>Deliberately design</strong> the degree of input freedom to suit the module’s purpose and users.</li>
<li>Rather than piling more onto a bad abstraction, have the <strong>courage to unravel it and start again</strong>.</li>
<li>And <strong>internalize a variety of patterns</strong> until all of this comes naturally without conscious effort.</li>
</ul>
<p>Of course, what I have presented in this article is not the one correct answer. The appropriate level of abstraction inevitably varies with the business situation, the composition of the team, and the nature of the project. But if one thing remains constant, it is that the ultimate purpose of abstraction is <strong>to create code that people can understand easily</strong>.</p>
<p>I hope readers will ask this question of their own codebases: “Is this abstraction actually reducing context?” I believe that question alone can shift, at least slightly, the way we look at code.</p>
<h2 id="references"><a class="anchor" href="#references">References</a></h2>
<p>This article drew substantial inspiration from several official documents and earlier articles. Below are the sources of passages quoted directly, along with writings that helped shape the framework for my thinking.</p>
<details class="ref-block"><summary class="ref-summary">참고 자료</summary><div class="ref-list">
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#f59e0b"></span><a href="https://evan-moon.github.io/2023/01/15/what-is-abstract/" target="_blank" rel="noopener noreferrer">Evan Moon, Abstract and Abstraction</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#3b82f6"></span><a href="https://react.dev/learn/reusing-logic-with-custom-hooks" target="_blank" rel="noopener noreferrer">React, Reusing Logic with Custom Hooks</a></div>
</div>
</div></details>]]></content:encoded>
            <author>jihoon7705@gmail.com (이지훈)</author>
            <category>프론트엔드</category>
            <category>설계</category>
            <category>추상화</category>
        </item>
        <item>
            <title><![CDATA[queryKey]]></title>
            <link>https://hooninedev.com/en/260104</link>
            <guid isPermaLink="false">https://hooninedev.com/en/260104</guid>
            <pubDate>Sun, 04 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[In this post, I want to explore TanStack Query's queryKey. While using TanStack Query in production, I have completely overhauled how I manage queryKeys several times. At first, I simply wrote arrays ...]]></description>
            <content:encoded><![CDATA[<p>In this post, I want to explore <strong>TanStack Query's queryKey</strong>.</p>
<p>While using TanStack Query in production, I have <strong>completely overhauled how I manage queryKeys several times</strong>. At first, I simply wrote arrays like <code>['user', userId]</code> inline inside components. Then I started making typos because I had to repeat the same keys in multiple places whenever I invalidated queries, so I moved them into a constant object such as <code>QUERY_KEYS</code>. After reading TkDodo's article, I switched to the query key factory pattern. Much later, I adopted the <code>@lukemorales/query-key-factory</code> library. Then v5 arrived, and I overhauled everything once more using <code>queryOptions</code>.</p>
<p>I started wondering why so many patterns had emerged around a tiny array that was merely a cache identifier. <strong>Why does a single queryKey bear the marks of so much evolution?</strong> And what exact problem was each stage trying to solve?</p>
<p>In this article, I will trace TanStack Query's official documentation, TkDodo's blog series, and even the internal implementation of <code>queryOptions</code> introduced in v5 to explain how queryKey works and why it evolved into its current form.</p>
<h2 id="before-querykey"><a class="anchor" href="#before-querykey">Before queryKey</a></h2>
<p>Before we get into the main discussion, let's establish some context. Today, we take libraries like <code>TanStack Query</code> and <code>SWR</code> for granted, but how did we handle asynchronous data before they existed?</p>
<p>The most common approach was probably to combine <code>useState</code>, <code>useEffect</code>, <code>fetch</code>, and <code>axios</code>.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">function</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> UserProfile</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({ </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">userId</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">userId</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> }) {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> [</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">user</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">setUser</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">] </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> useState</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">User</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> |</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> null</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">>(</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">null</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> [</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">loading</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">setLoading</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">] </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> useState</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">true</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> [</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">error</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">setError</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">] </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> useState</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">Error</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> |</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> null</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">>(</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">null</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  useEffect</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(() </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">    let</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> cancelled </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> false</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">    setLoading</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">true</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">    fetch</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">`/api/users/${</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">userId</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">}`</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">      .</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">then</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">((</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">res</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> res.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">json</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">())</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">      .</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">then</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">((</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">data</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">        if</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">!</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">cancelled) </span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">setUser</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(data);</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">      })</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">      .</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">catch</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">((</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">err</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">        if</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">!</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">cancelled) </span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">setError</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(err);</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">      })</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">      .</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">finally</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(() </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">        if</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">!</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">cancelled) </span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">setLoading</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">false</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">      });</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">    return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> () </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">      cancelled </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> true</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    };</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  }, [userId]);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">  // ...</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>The problem with this code is obvious. If just two components on the page display the same <code>userId</code>, <strong>the same request is sent twice.</strong> That happens because there is no cache. If the user navigates to another page and returns, the data is fetched from scratch yet again. There is no way to tell whether the data was fetched one second ago or one hour ago, so even approximating behavior such as "show cached data while refreshing in the background" is difficult. (You could build your own cache system to implement this, but I think it would be quite difficult to maintain.)</p>
<p>Redux combined with redux-thunk (or redux-saga) emerged as a way to solve this. By moving data-fetching logic into a thunk and storing the result in the store, other components could reuse the same data. But every request required defining action types, writing a reducer, and manually managing loading, success, and failure states. The amount of boilerplate needed to fetch a single piece of data was enormous. (I started working professionally during this era and found myself wondering, "Why do I need to create several files just to fetch one piece of data?")</p>
<p>The essence of this progression is ultimately this: <strong>"To avoid repeating a request, we need to be able to identify which request it is."</strong> The identifier for that "which request" is exactly what a queryKey is.</p>
<p>SWR and React Query (now TanStack Query) tackled this problem head-on. "An asynchronous request must have an identifier, and requests with the same identifier share a cache." That single, simple principle eliminated all the boilerplate above.</p>
<h2 id="the-essence-of-querykey"><a class="anchor" href="#the-essence-of-querykey">The essence of queryKey</a></h2>
<p>So what exactly is a queryKey? TanStack Query's official documentation defines it this way.</p>
<blockquote class="bilingual-quote"><div class="quote-translation" lang="ko"><p>At its core, TanStack Query manages query caching based on query keys. A query key must be an array at the top level... As long as the query key is serializable and <strong>unique to the query's data</strong>, you can use it.</p></div><div class="quote-original" lang="en"><p>At its core, TanStack Query manages query caching for you based on query keys. Query keys have to be an Array at the top level... As long as the query key is serializable, and <strong>unique to the query's data</strong>, you can use it.</p></div></blockquote>
<p>There are two key requirements. <strong>It must be serializable, and it must be unique to the data.</strong> The same key must represent the same data, and different data must have different keys. This simple rule determines how the entire cache system behaves.</p>
<p>There is one more important point: <strong>a queryKey also acts as a dependency array.</strong> Just as an effect runs again when its dependencies change in React's <code>useEffect</code>, TanStack Query automatically fetches new data when the queryKey changes.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">data</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> } </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> useQuery</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  queryKey: [</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'user'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, userId],</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  queryFn</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: () </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> fetchUser</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(userId),</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">});</span></span></code></pre></figure>
<p>The queryKeys for a <code>userId</code> of <code>'A'</code> and one of <code>'B'</code> are different. A different key means a cache miss, and a cache miss triggers a fetch. It is automatic. Thanks to this simplicity, we do not need to write logic that says, "The userId changed, so fetch again."</p>
<p>This raises a question: how does TanStack Query determine whether two queryKeys are "the same key"? A simple <code>===</code> comparison would find different object references and cause a cache miss every time.</p>
<h2 id="inside-querycache"><a class="anchor" href="#inside-querycache">Inside QueryCache</a></h2>
<p>According to TkDodo's <a href="https://tkdodo.eu/blog/inside-react-query" target="_blank" rel="noopener noreferrer">Inside React Query</a>, <code>QueryCache</code> is ultimately just <strong>an in-memory data structure</strong>. More precisely, in the v5 <a href="https://github.com/TanStack/query/blob/main/packages/query-core/src/queryCache.ts" target="_blank" rel="noopener noreferrer">official implementation</a>, that data structure is not a plain object but a <code>Map&#x3C;string, Query></code>. It is declared inside the class as <code>#queries = new Map&#x3C;string, Query>()</code>, and every write and read goes through <code>#queries.set(query.queryHash, query)</code> and <code>#queries.get(queryHash)</code>. The key is the serialized form of the queryKey (<code>queryHash</code>), and the value is an instance of the <code>Query</code> class.</p>
<p>Older versions did use a plain object, but by v5 the implementation had settled on the native <code>Map</code>. (<code>Map</code> has no risk of key collisions or prototype pollution, preserves insertion order, and offers average O(1) string-key lookup, making it an almost textbook choice for a cache data structure.)</p>
<p>What happens each time <code>useQuery</code> is called is straightforward. <strong>The queryKey is converted into a hash, and that hash is used to look it up in the Map.</strong> If an entry exists, TanStack Query retrieves the cached <code>Query</code> instance. Otherwise, it creates a new one and calls <code>set</code>.</p>
<p>This naturally leads to another question: <strong>why serialize the queryKey into a string at all?</strong> Why not use the array itself as the key, as in <code>Map&#x3C;QueryKey, Query></code>?</p>
<p>The answer lies in JavaScript's equality model. A native <code>Map</code> compares keys using <strong>reference equality</strong>. Even when their contents are identical, objects at different locations in memory are treated as different keys.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="js" data-theme="github-dark github-light"><code data-language="js" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> m</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> new</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> Map</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">m.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">set</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">([</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'user'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">1</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">], </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'alice'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">m.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">get</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">([</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'user'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">1</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">]); </span><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// undefined — 새로 만든 배열은 다른 참조다</span></span></code></pre></figure>
<p>But in a React component, <code>useQuery({ queryKey: ['user', userId] })</code> <strong>creates a new array instance on every render.</strong> The queryKey arrays from the first and second renders are separate objects in memory even if their contents match. If the cache depended on reference equality, a component displaying the same data would tragically miss the cache on every render.</p>
<p>The solution to the problem caused by reference equality is simple: <strong>convert reference equality into structural equality</strong>. Create a deterministic string using only the contents of the queryKey, then use that string as the Map key. This restores the semantics we want: "equal contents mean the same key." <code>JSON.stringify</code> is simply the most straightforward tool for that conversion. (It is also why TanStack Query, after experimenting with several serialization strategies during the v3 era, ultimately settled on a stable variation of <code>JSON.stringify</code>.)</p>
<p>The key here is the function that produces the hash: <code>hashKey</code>. Its official implementation in <a href="https://github.com/TanStack/query/blob/main/packages/query-core/src/utils.ts" target="_blank" rel="noopener noreferrer"><code>packages/query-core/src/utils.ts</code></a> looks exactly like this.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="typescript" data-theme="github-dark github-light"><code data-language="typescript" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">export</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> function</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> hashKey</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">queryKey</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> QueryKey</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> |</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> MutationKey</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">)</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  return</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> JSON</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">stringify</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(queryKey, (</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">_</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">val</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">    isPlainObject</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(val)</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">      ?</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> Object.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">keys</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(val)</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">          .</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">sort</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">()</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">          .</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">reduce</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">((</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">result</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">key</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">            result[key] </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> val[key]</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">            return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> result</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">          }, {} </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">as</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> any</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">      :</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> val,</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  )</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>It does use <code>JSON.stringify</code>, but instead of stringifying directly, it supplies a <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#the_replacer_parameter" target="_blank" rel="noopener noreferrer">replacer callback</a> that <strong>sorts the keys of plain objects alphabetically</strong> before serialization.</p>
<p>This sorting is fundamental because string serialization carries an additional, stronger requirement: <strong>semantically equivalent inputs must always produce the same string.</strong> Ordinary <code>JSON.stringify</code>, however, preserves key order. <code>{ a: 1, b: 2 }</code> and <code>{ b: 2, a: 1 }</code> are semantically equivalent objects, but they serialize into different strings and therefore occupy different cache slots. That would bring back duplicate requests for the same data.</p>
<p>The technique that consistently prevents this is a <strong>canonical form</strong>. It forces semantically equivalent inputs to map to exactly one representation. This is precisely why the <code>hashKey</code> replacer sorts the keys of plain objects. By producing the same output regardless of input order, it creates a one-to-one relationship between the serialized result and the meaning of the object. In mathematical terms, it selects the sorted form as the representative element of the equivalence class formed by objects whose keys appear in different orders.</p>
<p>The fact that arrays are not sorted is the other side of the same principle. An array is a data structure in which order itself carries meaning, so sorting it would destroy information. Object key order is incidental; array element order is intentional. <code>hashKey</code> treats the two accordingly. This is why the official guide recommends arranging a queryKey from "generic → specific." As long as array order carries meaning, the author must define that meaning directly.</p>
<p>There is one more detail worth highlighting: key sorting applies only to <strong>plain objects</strong>. In the same file, <code>isPlainObject</code> does not merely check <code>typeof === 'object'</code>; it goes as far as checking <code>Object.getPrototypeOf(o) === Object.prototype</code> to distinguish <strong>plain object literals</strong> from <strong>class instances</strong>. As a result, a literal such as <code>{ foo: 1 }</code> is sorted, while an instance created with <code>class User { ... }</code> passes through unsorted. (This is where a subtle trap arises: if you put a class instance directly into a queryKey, its interaction with <code>JSON.stringify</code>, which outputs only enumerable properties, may produce a hash different from what you intended.)</p>
<p>This behavior has two important consequences.</p>
<p><strong>1. Object key order does not matter.</strong></p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">useQuery</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({ queryKey: [</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'todos'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, { status: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'done'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, page: </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">1</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> }], queryFn });</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">useQuery</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({ queryKey: [</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'todos'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, { page: </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">1</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, status: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'done'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> }], queryFn });</span></span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 두 쿼리는 같은 캐시 슬롯을 공유한다</span></span></code></pre></figure>
<p>That is because the keys are sorted before serialization. Without this behavior, you would have to remember the key order every time you used an object literal.</p>
<p><strong>2. Array element order matters.</strong></p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">useQuery</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({ queryKey: [</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'todos'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, status, page], queryFn });</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">useQuery</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({ queryKey: [</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'todos'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, page, status], queryFn });</span></span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 두 쿼리는 다른 캐시이다</span></span></code></pre></figure>
<p>That is because an array is a data structure where order itself carries meaning. <code>JSON.stringify</code> also preserves array order.</p>
<p>It is also useful to know that <code>undefined</code> values disappear during serialization. <code>{ a: 1, b: undefined }</code> and <code>{ a: 1 }</code> produce the same hash. (I once made the mistake of thinking, "I explicitly included undefined, so this must be a different cache!")</p>
<p>Another constraint is that a queryKey cannot contain <strong>circular references or functions</strong>, because <code>JSON.stringify</code> cannot handle them. Objects such as <code>Date</code>, <code>Map/Set</code>, and <code>BigInt</code> are likewise not recommended under the default behavior. A queryKey should be a serializable, plain data structure.</p>
<p>Interestingly, this constraint is not absolute. TanStack Query provides an escape hatch through the <code>queryKeyHashFn</code> option, allowing you to <strong>replace the hash function itself</strong>. Internally, <code>hashQueryKeyByOptions(queryKey, options)</code> branches: if <code>queryKeyHashFn</code> exists in the options, it calls that; otherwise, it calls the default <code>hashKey</code>.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">useQuery</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  queryKey: [{ id: userId, fetchedAt: </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">new</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> Date</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">() }],</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  queryFn,</span></span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">  // Date를 ISO 문자열로 바꿔서 해싱</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  queryKeyHashFn</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: (</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">key</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span></span>
<span data-line=""><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">    JSON</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">stringify</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(key, (</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">_</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">v</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (v </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">instanceof</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> Date</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> ?</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> v.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">toISOString</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">() </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> v)),</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">});</span></span></code></pre></figure>
<p>However, this option must be specified separately for each query, and it does not apply to imperative APIs invoked without knowledge of those options, such as <code>queryClient.setQueryData</code> (<a href="https://github.com/TanStack/query/issues/1343" target="_blank" rel="noopener noreferrer">Issue #1343</a>). In production, it is therefore much safer to avoid the escape hatch and <strong>convert values into a serializable form when constructing the queryKey</strong>. (I once placed a <code>Date</code> directly in a key and spent a long time wondering, "Why isn't the cache updating even though it represents the same instant?" The answer turned out to be, "That <code>Date</code> represents the same instant, but it is a different object instance, so it produces a different hash every time.")</p>
<h2 id="rules-for-writing-querykeys"><a class="anchor" href="#rules-for-writing-querykeys">Rules for writing queryKeys</a></h2>
<p>Once you understand the internals above, the rules for writing queryKeys follow naturally. The official recommendations can be summarized as follows.</p>
<p><strong>Rule 1. A queryKey must be an array.</strong></p>
<p>Passing a string still works because it is converted to an array internally. For consistency, however, it is better to use an array from the start.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 비권장</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">useQuery</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({ queryKey: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'todos'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, queryFn });</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 권장</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">useQuery</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({ queryKey: [</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'todos'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">], queryFn });</span></span></code></pre></figure>
<p><strong>Rule 2. Include every variable that the queryFn depends on in the queryKey.</strong></p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 잘못된 예: userId가 쿼리키에 없다</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">useQuery</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  queryKey: [</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'user'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">],</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  queryFn</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: () </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> fetchUser</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(userId),</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">});</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 올바른 예</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">useQuery</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  queryKey: [</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'user'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, userId],</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  queryFn</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: () </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> fetchUser</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(userId),</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">});</span></span></code></pre></figure>
<p>The mental model is exactly the same as the dependency array in <code>useEffect</code>. Every variable used inside the function must be part of the key (= dependency). Violating this rule creates bugs that are difficult to trace, such as continuing to display the previous user's data after switching to another user.</p>
<p><strong>Rule 3. Arrange the key from the most generic element to the most specific.</strong></p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 좋다</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">[</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'todos'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'list'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, { filter: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'done'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> }]</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">[</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'todos'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'detail'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, todoId]</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 안 좋다 (순서가 뒤집혀 있음)</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">[{ filter: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'done'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> }, </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'list'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'todos'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">]</span></span></code></pre></figure>
<p>This order matters because of <strong>invalidation</strong>. By default, TanStack Query's <code>invalidateQueries</code> uses <strong>prefix matching</strong>.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 모든 todos 관련 쿼리 무효화</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">queryClient.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">invalidateQueries</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({ queryKey: [</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'todos'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">] });</span></span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// → ['todos', 'list', ...], ['todos', 'detail', ...] 모두 매치된다</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// list 쿼리만 무효화</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">queryClient.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">invalidateQueries</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({ queryKey: [</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'todos'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'list'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">] });</span></span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// → ['todos', 'list', ...]만 매치된다</span></span></code></pre></figure>
<p>When keys are designed as a tree, a single line can express anything from "fetch all data in this domain again" to "fetch only this exact item again." (It may not seem like much at first, but after designing a key hierarchy poorly and seeing invalidation affect an unintended scope, its value becomes painfully clear.)</p>
<h2 id="the-evolution-of-querykey-management"><a class="anchor" href="#the-evolution-of-querykey-management">The evolution of queryKey management</a></h2>
<p>So far, we have covered how queryKey works and how to use it. Now we can move on to the central question: <strong>how has queryKey management changed over time?</strong></p>
<p>I will walk through the stages I have used in production, in chronological order.</p>
<h3 id="1-inline-arrays"><a class="anchor" href="#1-inline-arrays">1. Inline arrays</a></h3>
<p>This is the simplest form: combine fixed strings with prop values inside the component.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">function</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> UserProfile</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({ </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">userId</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">userId</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> }) {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">data</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> } </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> useQuery</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    queryKey: [</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'user'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, userId],</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">    queryFn</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: () </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> fetchUser</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(userId),</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  });</span></span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">  // ...</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">function</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> PostList</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({ </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">filter</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">filter</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> PostFilter</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> }) {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">data</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> } </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> useQuery</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    queryKey: [</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'posts'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, filter],</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">    queryFn</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: () </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> fetchPosts</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(filter),</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  });</span></span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">  // ...</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>This is enough when you are just getting started.</p>
<p>The problems begin as the codebase grows. When a mutation that updates user information needs to invalidate a query, you have to search for the answer to "What was the query key for user data again?" every time. Some places end up using <code>['user', userId]</code>, while others use <code>['users', userId]</code> in the plural. These are entirely different cache slots, so invalidation affects only one of them.</p>
<h3 id="2-a-constant-object"><a class="anchor" href="#2-a-constant-object">2. A constant object</a></h3>
<p>To prevent typos, gather query keys into constants.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// queryKeys.ts</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">export</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> QUERY_KEYS</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  USER: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'user'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  POSTS: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'posts'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  COMMENTS: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'comments'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">} </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">as</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> const</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 사용처</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">useQuery</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  queryKey: [</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">QUERY_KEYS</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">.</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">USER</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, userId],</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  queryFn</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: () </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> fetchUser</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(userId),</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">});</span></span></code></pre></figure>
<p>The typos disappear, but each call site is still responsible for assembling the key. One person may use <code>[QUERY_KEYS.USER, userId]</code>, another <code>[QUERY_KEYS.USER, userId, 'detail']</code>, and someone else <code>['user', 'detail', userId]</code>. Eventually, you need to memorize a separate convention just to know which form is correct.</p>
<h3 id="3-query-key-factory"><a class="anchor" href="#3-query-key-factory">3. Query Key Factory</a></h3>
<p>This pattern was formalized in TkDodo's <a href="https://tkdodo.eu/blog/effective-react-query-keys" target="_blank" rel="noopener noreferrer">Effective React Query Keys</a>. It defines an object that creates keys for each domain and expresses the hierarchy through functions.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// features/todos/queries.ts</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> todoKeys</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  all: [</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'todos'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">] </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">as</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> const</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  lists</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: () </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> [</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">...</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">todoKeys.all, </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'list'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">] </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">as</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> const</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  list</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: (</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">filters</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> [</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">...</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">todoKeys.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">lists</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(), { filters }] </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">as</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> const</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  details</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: () </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> [</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">...</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">todoKeys.all, </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'detail'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">] </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">as</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> const</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  detail</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: (</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">id</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> number</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> [</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">...</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">todoKeys.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">details</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(), id] </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">as</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> const</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">};</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 사용</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">useQuery</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({ queryKey: todoKeys.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">detail</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">1</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">), queryFn: </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">...</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> });</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">useQuery</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({ queryKey: todoKeys.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">list</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'done'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">), queryFn: </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">...</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> });</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 무효화</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">queryClient.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">invalidateQueries</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({ queryKey: todoKeys.all });        </span><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 전체</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">queryClient.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">invalidateQueries</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({ queryKey: todoKeys.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">lists</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">() });    </span><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 모든 리스트</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">queryClient.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">invalidateQueries</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({ queryKey: todoKeys.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">detail</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">1</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) });  </span><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 특정 항목</span></span></code></pre></figure>
<p>This pattern is powerful because <strong>the hierarchy is made explicit in the code</strong>. <code>todoKeys.all</code> refers to every query related to todos, <code>todoKeys.lists()</code> refers to every list query, and <code>todoKeys.detail(1)</code> refers to one specific item. The precise scope of invalidation can be expressed in a single line of code.</p>
<p>Another benefit is <strong>co-location</strong>. TkDodo does not recommend gathering keys into a global file. Instead, he recommends placing <code>queries.ts</code> inside the feature directory and keeping keys and hooks together there.</p>
<pre><code>src/
└── features/
    └── todos/
        ├── index.tsx
        └── queries.ts   # 키와 훅을 모두 여기에
</code></pre>
<p>This creates a simple mental model: "To change something about todos, I only need to look in the todos directory." It is a faithful application of the principle of keeping things that change together close together.</p>
<h3 id="4-lukemoralesquery-key-factory"><a class="anchor" href="#4-lukemoralesquery-key-factory">4. @lukemorales/query-key-factory</a></h3>
<p>Writing the third pattern by hand every time accumulates boilerplate. And when you want to combine keys from multiple domains, the lack of a standardized interface becomes apparent. <a href="https://github.com/lukemorales/query-key-factory" target="_blank" rel="noopener noreferrer">@lukemorales/query-key-factory</a> is the library form of this pattern.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { createQueryKeys, mergeQueryKeys } </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">from</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> '@lukemorales/query-key-factory'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> users</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> createQueryKeys</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'users'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, {</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  detail</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: (</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">userId</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> ({</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    queryKey: [userId],</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">    queryFn</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: () </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> api.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">getUser</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(userId),</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  }),</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  list</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: (</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">filters</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> UserFilters</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> ({</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    queryKey: [{ filters }],</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">    queryFn</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: () </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> api.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">getUsers</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(filters),</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  }),</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">});</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> todos</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> createQueryKeys</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'todos'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, {</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  detail</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: (</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">id</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> number</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> ({</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    queryKey: [id],</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">    queryFn</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: () </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> api.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">getTodo</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(id),</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  }),</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">});</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">export</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> queries</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> mergeQueryKeys</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(users, todos);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 사용</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">useQuery</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(queries.users.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">detail</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'abc'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">));</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">useQuery</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(queries.todos.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">detail</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">1</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">));</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 무효화</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">queryClient.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">invalidateQueries</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(queries.users._def);            </span><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 모든 user 쿼리</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">queryClient.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">invalidateQueries</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(queries.users.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">detail</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'abc'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">));   </span><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 특정 항목</span></span></code></pre></figure>
<p><code>createQueryKeys</code> automatically adds the prefix, and <code>mergeQueryKeys</code> can combine domains. The conventional <code>_def</code> property provides access to the key for an entire domain. This eliminates the manual work of repeatedly adding <code>as const</code> to narrow types in a hand-written factory.</p>
<p>For a while, this library was effectively the standard. (I was a happy user for quite some time.) Then queryOptions changed the landscape.</p>
<h3 id="5-queryoptions-official-in-v5"><a class="anchor" href="#5-queryoptions-official-in-v5">5. queryOptions (official in v5)</a></h3>
<p>One of the most important changes in TanStack Query v5 was the introduction of the <code>queryOptions</code> API. During the transition from v4 to v5, every hook's arguments were unified into a single object. The real purpose of this change was to make it possible to extract that object into <strong>a reusable unit</strong>.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { queryOptions } </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">from</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> '@tanstack/react-query'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">export</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> const</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> userDetailOptions</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">userId</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  queryOptions</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    queryKey: [</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'user'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, userId],</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">    queryFn</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: () </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> fetchUser</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(userId),</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    staleTime: </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">5</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> *</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 60</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> *</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 1000</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  });</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 어디서나 사용 가능</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">useQuery</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">userDetailOptions</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'abc'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">));</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">useSuspenseQuery</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">userDetailOptions</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'abc'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">));</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">queryClient.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">prefetchQuery</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">userDetailOptions</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'abc'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">));</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">queryClient.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">setQueryData</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">userDetailOptions</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'abc'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">).queryKey, newUser);</span></span></code></pre></figure>
<p>At first glance, you might think, "What is different about this? It looks like an object wrapped in a function." TkDodo acknowledges this in <a href="https://tkdodo.eu/blog/the-query-options-api" target="_blank" rel="noopener noreferrer">The Query Options API</a>. At runtime, it truly does nothing more than return the object it receives.</p>
<p>The real work happens <strong>inside the type system</strong>. Let us continue.</p>
<h2 id="datatag-in-queryoptions"><a class="anchor" href="#datatag-in-queryoptions">DataTag in queryOptions</a></h2>
<p><code>queryOptions</code> is more than a simple helper because it <strong>embeds data type information in the returned queryKey</strong>. TanStack Query calls this mechanism <code>DataTag</code> internally.</p>
<p>A simplified implementation looks like this.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="typescript" data-theme="github-dark github-light"><code data-language="typescript" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">declare</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> dataTagSymbol</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> unique</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> symbol</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">declare</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> dataTagErrorSymbol</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> unique</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> symbol</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">export</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> type</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> DataTag</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">TType</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">TValue</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">TError</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> unknown</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">> </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> TType</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> &#x26;</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  [dataTagSymbol]</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> TValue</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  [dataTagErrorSymbol]</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> TError</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">};</span></span></code></pre></figure>
<p>This is a <strong>branded type</strong> built with a <code>unique symbol</code>. It is merely a marker with no runtime effect, but to TypeScript it carries the information that "this array is not just an array; it is an array associated with data of type <code>TValue</code>."</p>
<p>There is a reason for using a <code>unique symbol</code> here. The Zenn article <a href="https://zenn.dev/tsuboi/articles/tanstack-query-options-unique-symbol?locale=en" target="_blank" rel="noopener noreferrer">Uncovering the unique symbol Behind DataTag</a> compares this mechanism to "a dedicated parking space for type information." An ordinary string key could collide with a key from another library or from user code, but <strong>each <code>unique symbol</code> declaration creates a type unique to itself</strong>, so it cannot have the same type as any other declaration. In effect, it becomes an identifier that can never collide.</p>
<p>The difference this one mechanism makes is significant.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> data</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> queryClient.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">getQueryData</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">([</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'user'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'abc'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">]); </span><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// unknown</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> data</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> queryClient.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">getQueryData</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">userDetailOptions</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'abc'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">).queryKey); </span><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// User | undefined</span></span></code></pre></figure>
<p><code>getQueryData</code> and <code>setQueryData</code> receive only a queryKey. But because that queryKey already carries the data type, the return type is inferred automatically. There is no need to pass a generic explicitly, and if you try to provide <code>setQueryData</code> with a value of the wrong type, the compiler catches it immediately.</p>
<p>There are limitations, of course. Methods such as <code>getQueriesData</code>, which retrieve multiple queries at once, return a heterogeneous array of tuples, so this type inference does not apply. And because the implementation uses a <code>unique symbol</code>, generating <code>.d.ts</code> files in a monorepo can produce a TS4023 error; importing <code>dataTagSymbol</code> explicitly provides a workaround.</p>
<p>The mechanism so far makes one fact clear: <strong>the type inference provided by queryOptions depends entirely on declaring the queryKey and queryFn together in one place.</strong> To embed the return type of the queryFn in the queryKey, they must be declared together.</p>
<p>This has a major implication for the design direction of query key factories. Earlier patterns emphasized separating queryKey management into its own abstraction. The v5 recommendation takes the opposite direction: <strong>bring the queryKey and queryFn back together as a single unit</strong>. TkDodo goes so far as to say that separating the queryKey and queryFn was a mistake. After all, the key is a collection of the function's dependencies, and the two are inseparable.</p>
<h2 id="a-practical-queryoptions-composition-pattern"><a class="anchor" href="#a-practical-queryoptions-composition-pattern">A practical queryOptions composition pattern</a></h2>
<p>The real value of <code>queryOptions</code> emerges when it is combined with a domain-specific factory. The form recommended by the official v5 documentation looks like this.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { queryOptions } </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">from</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> '@tanstack/react-query'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">export</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> todoQueries</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  all</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: () </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> [</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'todos'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">] </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">as</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> const</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  lists</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: () </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> [</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">...</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">todoQueries.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">all</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(), </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'list'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">] </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">as</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> const</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  list</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: (</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">filters</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> TodoFilters</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">    queryOptions</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">      queryKey: [</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">...</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">todoQueries.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">lists</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(), filters],</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">      queryFn</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: () </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> fetchTodos</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(filters),</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">      staleTime: </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">30</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> *</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 1000</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    }),</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  details</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: () </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> [</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">...</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">todoQueries.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">all</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(), </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'detail'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">] </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">as</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> const</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  detail</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: (</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">id</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> number</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">    queryOptions</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">      queryKey: [</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">...</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">todoQueries.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">details</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(), id],</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">      queryFn</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: () </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> fetchTodo</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(id),</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">      staleTime: </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">5</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> *</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 60</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> *</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 1000</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    }),</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">};</span></span></code></pre></figure>
<p>Let us unpack the strengths of this pattern one by one.</p>
<p><strong>1. You get hierarchy and type inference at the same time.</strong></p>
<p><code>todoQueries.all()</code> and <code>todoQueries.lists()</code> return plain arrays, while <code>todoQueries.detail(1)</code> returns an object created through <code>queryOptions</code> with a data tag attached. Use the arrays for invalidation and the options object for query calls.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">useQuery</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(todoQueries.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">detail</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">1</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">));                                </span><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 옵션 객체</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">queryClient.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">invalidateQueries</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({ queryKey: todoQueries.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">all</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">() }); </span><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 배열</span></span></code></pre></figure>
<p><strong>2. Components can partially override the options.</strong></p>
<p>The result of <code>queryOptions</code> is ultimately an object, so individual options can be composed at the call site.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">data</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">title</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> } </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> useQuery</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  ...</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">todoQueries.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">detail</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">1</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">),</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  select</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: (</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">todo</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> todo.title,  </span><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 컴포넌트별로 다른 select 적용</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">});</span></span></code></pre></figure>
<p>What makes this especially powerful is that the return type of <code>select</code> is inferred automatically, narrowing the type of <code>data</code> to <code>string</code>. The component can select only the piece it needs while leaving the domain definition centralized.</p>
<p><strong>3. Custom hooks wrapping <code>useQuery</code> gradually disappear.</strong></p>
<p>A common pattern in v4 was to create a custom hook for each domain.</p>
<p>The problem with that approach was that <strong>the moment prefetching became necessary, you had to write the same definition again</strong>. Because <code>useTodoDetail</code> is a hook, it cannot be called outside a component, so a router loader or event handler had to repeat <code>queryClient.prefetchQuery({ queryKey: [...], queryFn: ... })</code>.</p>
<p>With <code>queryOptions</code>, that duplication disappears.</p>
<p>The same definition works everywhere. That is why TkDodo recommends, "In v5, define queryOptions instead of creating hooks." Hooks become thin wrappers used only when needed, while the domain definition stands on its own without them.</p>
<h2 id="mutation-invalidation"><a class="anchor" href="#mutation-invalidation">Mutation Invalidation</a></h2>
<p>The queryKey hierarchy truly shines when invalidating queries after a mutation. According to TanStack Query's <a href="https://tanstack.com/query/v5/docs/framework/react/guides/query-invalidation" target="_blank" rel="noopener noreferrer">Query Invalidation</a> documentation, <code>invalidateQueries</code> uses <strong>prefix matching</strong> by default.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 모든 todos 관련 쿼리 (list, detail, lists 모두)</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">queryClient.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">invalidateQueries</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({ queryKey: todoQueries.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">all</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">() });</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 모든 list만 (detail은 건드리지 않음)</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">queryClient.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">invalidateQueries</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({ queryKey: todoQueries.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">lists</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">() });</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 정확히 이 키만 (자식 키 매치 안 함)</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">queryClient.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">invalidateQueries</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  queryKey: todoQueries.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">detail</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">1</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">).queryKey,</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  exact: </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">true</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">});</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 더 복잡한 조건은 predicate으로</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">queryClient.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">invalidateQueries</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  predicate</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: (</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">query</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    query.queryKey[</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">0</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">] </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">===</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> 'todos'</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> &#x26;&#x26;</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    (query.queryKey[</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">2</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">] </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">as</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> any</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">)?.version </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">>=</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 10</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">});</span></span></code></pre></figure>
<p>When keys are designed hierarchically, <strong>the scope of invalidation matches the meaning of the code.</strong> "Refresh all todos" is expressed with <code>all()</code>, "refresh only the lists" with <code>lists()</code>, and "refresh only this item" with <code>detail(id)</code>.</p>
<p>If the keys were scattered in a flat structure such as <code>['todoList']</code> and <code>['todoDetail', 1]</code>, invalidating the entire todos domain would require two separate calls or a separately managed prefix constant. (And if you forgot to update that constant whenever a new domain key was added, the new key would be omitted from invalidation, causing a bug.)</p>
<h2 id="reading-the-querykey-back-inside-queryfn"><a class="anchor" href="#reading-the-querykey-back-inside-queryfn">Reading the queryKey back inside queryFn</a></h2>
<p>There is one final pattern worth examining. A <code>queryFn</code> actually receives an object called <code>QueryFunctionContext</code>, which contains the queryKey used for that invocation.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">queryOptions</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  queryKey: [</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'user'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, userId, { include: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'profile'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> }] </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">as</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> const</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  queryFn</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: ({ </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">queryKey</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> }) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">    const</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> [, </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">id</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">options</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">] </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> queryKey;</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">    return</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> fetchUser</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(id, options);</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  },</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">});</span></span></code></pre></figure>
<p>Why is this pattern useful? According to TkDodo's <a href="https://tkdodo.eu/blog/leveraging-the-query-function-context" target="_blank" rel="noopener noreferrer">Leveraging the Query Function Context</a>, it can <strong>force the dependencies of queryKey and queryFn to stay synchronized</strong>.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> sortBy</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> 'name'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">queryOptions</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  queryKey: [</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'users'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">],</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  queryFn</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: () </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> fetchUsers</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({ sortBy }),</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">});</span></span></code></pre></figure>
<p>This code is risky because the queryFn depends on an external variable. The cache will not update when <code>sortBy</code> changes because the dependency was omitted from the key. As long as the <code>queryFn</code> pulls variables from an external closure, this mistake can happen at any time.</p>
<p>The solution is simple: make the <code>queryFn</code> independent of external variables. <strong>If every dependency is read from the queryKey</strong>, a variable omitted from the queryKey cannot be used inside the function in the first place.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">queryOptions</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  queryKey: [</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'users'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, { sortBy }] </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">as</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> const</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  queryFn</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: ({ </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">queryKey</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: [, { </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">sortBy</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> }] }) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> fetchUsers</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({ sortBy }),</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">});</span></span></code></pre></figure>
<p>With this structure, when a new dependency is introduced, there is no way to use it inside the function without adding it to the queryKey. The compiler catches the mistake by saying, in effect, "That key does not exist." Synchronization between the key and the function is delegated to <strong>the type system</strong> rather than left to convention.</p>
<h2 id="how-much-should-you-extract"><a class="anchor" href="#how-much-should-you-extract">How much should you extract?</a></h2>
<p>After reading this far, you may be wondering, "Should every query be extracted into <code>queryOptions</code>?"</p>
<p>As always, my answer is <strong>"it depends."</strong></p>
<p>The important thing to remember is that <strong>abstraction is not always beneficial</strong>. Extracting a query used only once into a domain factory merely forces readers to jump between two files. The evolution of queryKey management patterns does not mean "always use the most sophisticated tool." It means <strong>"you have the option to move up one rung of the ladder when the need arises."</strong></p>
<h2 id="conclusion"><a class="anchor" href="#conclusion">Conclusion</a></h2>
<p>In summary, a queryKey is <strong>the fundamental unit TanStack Query uses to identify and cache asynchronous data</strong>. That small array serves as a cache-slot identifier, a dependency array, and a definition of invalidation scope. In v5, it even carries data type information. Because so many responsibilities converge in this one place, how queryKeys are written and managed directly affects the cognitive load of the entire codebase.</p>
<p>Each stage was an answer to a real problem someone encountered at the time. So the right approach is not simply, "This is v5, so always use <code>queryOptions</code>." Instead, first ask: <strong>"What stage of this problem is my codebase currently facing?"</strong> Introducing a domain factory into a project where inline arrays are sufficient can itself be overengineering.</p>
<p>I hope this article encourages you to examine your own project: how queryKeys are distributed throughout the codebase, how invalidation is performed, and whether that structure fits the current size of your team and the complexity of your domain.</p>
<h2 id="references"><a class="anchor" href="#references">References</a></h2>
<details class="ref-block"><summary class="ref-summary">참고 자료</summary><div class="ref-list">
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#3b82f6"></span><a href="https://tanstack.com/query/latest/docs/framework/react/guides/query-keys" target="_blank" rel="noopener noreferrer">TanStack Query, Query Keys</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#3b82f6"></span><a href="https://tanstack.com/query/v5/docs/framework/react/guides/query-options" target="_blank" rel="noopener noreferrer">TanStack Query, Query Options</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#3b82f6"></span><a href="https://tanstack.com/query/v5/docs/framework/react/typescript" target="_blank" rel="noopener noreferrer">TanStack Query, TypeScript</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#f59e0b"></span><a href="https://tanstack.com/blog/announcing-tanstack-query-v5" target="_blank" rel="noopener noreferrer">TanStack, Announcing TanStack Query v5</a></div>
</div>
</div></details>]]></content:encoded>
            <author>jihoon7705@gmail.com (이지훈)</author>
            <category>프론트엔드</category>
            <category>React</category>
            <category>TanStack-Query</category>
            <category>queryKey</category>
        </item>
        <item>
            <title><![CDATA[Error Handling]]></title>
            <link>https://hooninedev.com/en/251117</link>
            <guid isPermaLink="false">https://hooninedev.com/en/251117</guid>
            <pubDate>Mon, 17 Nov 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[In this post, I want to discuss how to catch errors on the frontend. In practice, I have often felt vaguely uneasy whenever I wrote error-handling code. Some errors are caught with try/catch, some by ...]]></description>
            <content:encoded><![CDATA[<p>In this post, I want to discuss <strong>how to catch errors on the frontend</strong>.</p>
<p>In practice, I have often felt vaguely uneasy whenever I wrote error-handling code. Some errors are caught with <code>try/catch</code>, some by an <code>ErrorBoundary</code>, and others by TanStack Query's <code>onError</code>. Their respective domains subtly overlap or fail to line up. As a result, errors sometimes leak out, while at other times they propagate farther than intended.</p>
<p>The problem is that we rarely take the time to examine how all these tools work together. We may know that "Error Boundaries only catch render-phase errors," but if asked exactly what that means in practice, what happens internally when <code>reset</code> is called, or at what point TanStack Query rethrows an error when <code>throwOnError</code> is enabled, we may struggle to answer.</p>
<p>Based on React's official guide, the <code>react-error-boundary</code> library, and the official TanStack Query v5 documentation, this article explains <strong>where the responsibility of each frontend error-handling tool ends</strong> and <strong>how the tools fit together</strong>.</p>
<h2 id="errors-react-can-and-cannot-catch"><a class="anchor" href="#errors-react-can-and-cannot-catch">Errors React Can and Cannot Catch</a></h2>
<p>Let's begin with the most fundamental question: <strong>Which errors does React catch?</strong></p>
<p>The official React documentation clearly distinguishes between errors that an Error Boundary can catch and those it cannot.</p>
<p><strong>What an Error Boundary catches</strong></p>
<ul>
<li>Errors thrown while a child component is <strong>rendering</strong></li>
<li>Errors thrown inside <strong>lifecycle methods</strong></li>
<li>Errors thrown in <strong>constructors</strong></li>
</ul>
<p><strong>What an Error Boundary does not catch</strong></p>
<ul>
<li>Errors inside <strong>event handlers</strong></li>
<li>Errors in asynchronous code such as <code>setTimeout</code>, <code>requestAnimationFrame</code>, and <strong>Promises</strong></li>
<li>Errors during <strong>server-side rendering (SSR)</strong></li>
<li>Errors thrown by the <strong>Error Boundary itself</strong></li>
</ul>
<p>Why does this distinction matter? Most of the errors we deal with every day actually <strong>belong to the second category.</strong> A button click triggers a mutation and the server returns a 500 response; a fetch inside <code>useEffect</code> fails; validation logic throws while a form is being submitted. React does not catch these errors automatically. We must catch and handle them explicitly.</p>
<p>Frontend error handling therefore splits into two paths: <strong>use an Error Boundary for render-phase errors</strong>, and <strong>use try/catch or library callbacks for everything else</strong>. At the point where these paths intersect, asynchronous state-management libraries such as TanStack Query serve as a bridge.</p>
<h2 id="what-an-error-boundary-really-is"><a class="anchor" href="#what-an-error-boundary-really-is">What an Error Boundary Really Is</a></h2>
<p>An Error Boundary is ultimately a <strong>class component</strong> with two lifecycle methods. According to the official React documentation, a component must implement one of the following methods—usually both—to become an Error Boundary.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="js" data-theme="github-dark github-light"><code data-language="js" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">class</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> ErrorBoundary</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> extends</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> React</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">Component</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  constructor</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">props</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) {</span></span>
<span data-line=""><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">    super</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(props);</span></span>
<span data-line=""><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">    this</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">.state </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { hasError: </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">false</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> };</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">  // 에러 발생 시 state를 업데이트해 다음 렌더에서 fallback UI를 보여준다</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  static</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> getDerivedStateFromError</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">error</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">    return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { hasError: </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">true</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> };</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">  // 에러가 발생한 직후에 호출. 로깅 같은 사이드이펙트는 여기서 처리한다</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  componentDidCatch</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">error</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">info</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) {</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">    logErrorToMyService</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(error, info.componentStack);</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  render</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">() {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">    if</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">this</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">.state.hasError) {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">      return</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> this</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">.props.fallback;</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">    return</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> this</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">.props.children;</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  }</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p><code>getDerivedStateFromError</code> must be a <strong>pure function</strong>. Its sole role is to return new state without side effects. In contrast, <code>componentDidCatch</code> is the place for side effects. This is where you send the error to Sentry or log the component stack to the console.</p>
<p>There is one important point here: these two methods <strong>exist only on class components.</strong> There is still no official way to build an Error Boundary as a function component. The <a href="https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary" target="_blank" rel="noopener noreferrer">official React documentation</a> explicitly says so.</p>
<blockquote class="bilingual-quote"><div class="quote-translation" lang="ko"><p>There is currently no way to write an Error Boundary as a function component.</p></div><div class="quote-original" lang="en"><p>There is currently no way to write an Error Boundary as a function component.</p></div></blockquote>
<p>Writing a class component from scratch every time is cumbersome, so developers typically use the <code>react-error-boundary</code> library. (It was created by former React maintainer Brian Vaughn and is effectively treated as the standard.)</p>
<h2 id="three-types-of-fallback-in-react-error-boundary"><a class="anchor" href="#three-types-of-fallback-in-react-error-boundary">Three Types of Fallback in react-error-boundary</a></h2>
<p>The <code>react-error-boundary</code> library's <code>ErrorBoundary</code> component offers <strong>three ways</strong> to specify fallback UI through props. Let's take a quick look at how each one is used.</p>
<h3 id="fallback"><a class="anchor" href="#fallback">fallback</a></h3>
<p>This is the simplest form: pass static JSX directly.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">ErrorBoundary</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> fallback</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{&#x3C;</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">div</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">>문제가 발생했습니다.&#x3C;/</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">div</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">>}></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Page</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> /></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">ErrorBoundary</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span></code></pre></figure>
<p>Use it when you do not need access to the error object or the reset function. In practice, I have not used it yet because an error message or a retry action is usually necessary.</p>
<h3 id="fallbackcomponent"><a class="anchor" href="#fallbackcomponent">FallbackComponent</a></h3>
<p>Extract the fallback UI into a separate component and pass its <strong>reference</strong>.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">function</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> ErrorFallback</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({ </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">error</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">resetErrorBoundary</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> }) {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    &#x3C;</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">div</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> role</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"alert"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">      &#x3C;</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">p</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">>오류가 발생했습니다.&#x3C;/</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">p</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">      &#x3C;</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">pre</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">>{error.message}&#x3C;/</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">pre</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">      &#x3C;</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">button</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> onClick</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{resetErrorBoundary}>다시 시도&#x3C;/</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">button</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    &#x3C;/</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">div</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  );</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">ErrorBoundary</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> FallbackComponent</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{ErrorFallback}></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Page</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> /></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">ErrorBoundary</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span></code></pre></figure>
<p>The error object and the <code>resetErrorBoundary</code> function are automatically injected as props. This approach is clean when the fallback UI may be reused elsewhere.</p>
<h3 id="fallbackrender"><a class="anchor" href="#fallbackrender">fallbackRender</a></h3>
<p>Use this when you want to render the fallback inline.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">ErrorBoundary</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  fallbackRender</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{({ </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">error</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">resetErrorBoundary</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> }) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    &#x3C;</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">div</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> role</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"alert"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">      &#x3C;</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">p</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">>오류가 발생했습니다: {error.message}&#x3C;/</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">p</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">      &#x3C;</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">button</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> onClick</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{resetErrorBoundary}>다시 시도&#x3C;/</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">button</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    &#x3C;/</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">div</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  )}</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Page</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> /></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">ErrorBoundary</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span></code></pre></figure>
<p>It does essentially the same thing as <code>FallbackComponent</code>, but lets you <strong>handle it inline without creating a separate component</strong>. It is useful when you need access to an outer closure, such as parent state or handlers.</p>
<p>There is no single correct choice among the three, but the pattern I use most often in production is to <strong>create one shared ErrorFallback component and inject it through <code>FallbackComponent</code></strong>. This keeps the design system and tone consistent. I use <code>fallbackRender</code> inline only when a page needs a different fallback.</p>
<h2 id="what-does-reset-actually-do"><a class="anchor" href="#what-does-reset-actually-do">What Does reset Actually Do?</a></h2>
<p>When using <code>react-error-boundary</code>, you naturally encounter the <code>resetErrorBoundary</code> function. It is the function called when the user clicks the "Try again" button in the fallback. Let's examine what it actually does.</p>
<p>In short, <code>resetErrorBoundary</code> only tells the ErrorBoundary component to <strong>reset its own state and render its children again</strong>. It does not automatically touch any external state, such as the TanStack Query cache.</p>
<p>Here is what happens internally, step by step.</p>
<ol>
<li><code>resetErrorBoundary()</code> is called.</li>
<li>The ErrorBoundary's internal <code>hasError</code> state returns to <code>false</code>.</li>
<li>Optionally, the <code>onReset</code> callback runs. This is where custom side effects happen.</li>
<li>The children render again. If the cause of the error—such as state or cache—remains, <strong>the same error is thrown again.</strong></li>
</ol>
<p>The final step is the key. <strong>Reset only means "forget the error and try rendering again"; it does not mean "fix the cause of the error."</strong> Calling reset alone can therefore cause the same error to repeat indefinitely.</p>
<p>Two additional tools help solve this problem.</p>
<h3 id="onreset"><a class="anchor" href="#onreset">onReset</a></h3>
<p>This acts like a hook that runs immediately before the reset. Use it to clean up the external state that caused the error.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">ErrorBoundary</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  FallbackComponent</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{ErrorFallback}</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  onReset</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{() </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    queryClient.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">invalidateQueries</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({ queryKey: [</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'user'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">] });</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  }}</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Page</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> /></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">ErrorBoundary</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span></code></pre></figure>
<h3 id="resetkeys"><a class="anchor" href="#resetkeys">resetKeys</a></h3>
<p>The ErrorBoundary resets automatically when values in this array change. Pass keys for which it makes sense to retry when the value changes, such as URL parameters, a search term, or the selected tab.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">ErrorBoundary</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  FallbackComponent</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{ErrorFallback}</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  resetKeys</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{[userId]}</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">UserProfile</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> userId</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{userId} /></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">ErrorBoundary</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span></code></pre></figure>
<p>When <code>userId</code> changes, the boundary resets automatically and its children render again. If the user navigates to another profile, the previous error naturally disappears.</p>
<h2 id="how-do-we-catch-errors-from-event-handlers-and-asynchronous-code"><a class="anchor" href="#how-do-we-catch-errors-from-event-handlers-and-asynchronous-code">How Do We Catch Errors from Event Handlers and Asynchronous Code?</a></h2>
<p>As discussed earlier, Error Boundaries do not catch errors from event handlers or asynchronous code. Yet that is where most of the errors we deal with occur. So how should we handle them?</p>
<p>For this problem, <code>react-error-boundary</code> provides the <strong><code>useErrorBoundary</code> hook</strong>. The hook returns a function called <code>showBoundary</code>; calling it lets you explicitly send an error to the nearest ErrorBoundary.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { useErrorBoundary } </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">from</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> 'react-error-boundary'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">function</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> MyComponent</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">() {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">showBoundary</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> } </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> useErrorBoundary</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">();</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> handleClick</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> async</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> () </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">    try</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">      await</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> someAsyncOperation</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    } </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">catch</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (error) {</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">      showBoundary</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(error);</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  };</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> &#x3C;</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">button</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> onClick</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{handleClick}>실행&#x3C;/</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">button</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">>;</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>The key is that <strong>the developer must explicitly promote the error</strong>. React does not do it automatically. If you want to move an asynchronous error into the ErrorBoundary's domain, catch it with <code>try/catch</code> and pass it to <code>showBoundary</code>.</p>
<p>Once you understand this pattern, the question "Why does the ErrorBoundary catch some errors but not others?" has a clean answer: <strong>"Was the error promoted into the render phase or not?"</strong></p>
<h2 id="how-does-tanstack-query-handle-errors"><a class="anchor" href="#how-does-tanstack-query-handle-errors">How Does TanStack Query Handle Errors?</a></h2>
<p>At this point, a natural question arises. The <code>useQuery</code> hook we use every day handles asynchronous requests, so what happens to errors thrown inside it?</p>
<p>By default, TanStack Query exposes the error through the <strong><code>error</code> field</strong>.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">data</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">error</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">isError</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> } </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> useQuery</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  queryKey: [</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'todos'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">],</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  queryFn: fetchTodos,</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">});</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">if</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (isError) {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> &#x3C;</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">div</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">>에러: {error.message}&#x3C;/</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">div</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">>;</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>This is the simplest form. Even when an error occurs, the component renders normally; the <code>error</code> field simply contains a value. The ErrorBoundary is not involved.</p>
<p>There is one important fact to note here. <strong>TanStack Query's default behavior is not to throw errors.</strong> Whether the queryFn throws or rejects, the error merely enters the <code>error</code> field and does not disrupt React's render flow. Without additional configuration, the ErrorBoundary will therefore never run.</p>
<p>TanStack Query also <strong>retries errors three times by default</strong>.</p>
<p>The default <code>retryDelay</code> uses exponential backoff, increasing to a maximum of 30 seconds. In other words, an error is not shown to the user immediately after the first failure. TanStack Query retries at intervals of 1, 2, and 4 seconds, and only fills the <code>error</code> field after all those attempts fail. (If you have ever wondered during development, "Why does the error appear so late?", this is almost certainly why.)</p>
<h3 id="connecting-to-an-errorboundary-with-throwonerror"><a class="anchor" href="#connecting-to-an-errorboundary-with-throwonerror">Connecting to an ErrorBoundary with throwOnError</a></h3>
<p>How, then, do we route a TanStack Query error to an ErrorBoundary? The answer is the <strong><code>throwOnError</code></strong> option. (Until v4, it was named <code>useErrorBoundary</code>; in v5, it was renamed to <code>throwOnError</code>.)</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">data</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> } </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> useQuery</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  queryKey: [</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'todos'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">],</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  queryFn: fetchTodos,</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  throwOnError: </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">true</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">});</span></span></code></pre></figure>
<p>When this option is enabled, TanStack Query <strong>throws the error again during the next render cycle</strong>. That throw becomes a render-phase error, allowing the ErrorBoundary to catch it.</p>
<p><code>throwOnError</code> can also accept a function. This lets you route some errors to the ErrorBoundary while handling others directly in the component.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">useQuery</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  queryKey: [</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'todos'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">],</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  queryFn: fetchTodos,</span></span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">  // 5xx 서버 에러만 ErrorBoundary로 보낸다</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  throwOnError</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: (</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">error</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> error.response?.status </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">>=</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 500</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">});</span></span></code></pre></figure>
<p>This pattern is practical because <strong>client errors such as 4xx responses—for example, validation failures or missing permissions—</strong> are usually best displayed at the point where they occur, while <strong>server errors such as 5xx responses</strong> are better handled by covering the entire page and showing "Please try again later."</p>
<h3 id="usesuspensequery"><a class="anchor" href="#usesuspensequery">useSuspenseQuery</a></h3>
<p>If you use <code>useSuspenseQuery</code>, you do not need to worry about <code>throwOnError</code>. In Suspense mode, <strong>errors are always thrown by default</strong>.</p>
<p>In other words, using <code>useSuspenseQuery</code> means <strong>Suspense handles loading and ErrorBoundary handles errors</strong>. You no longer need branches such as <code>if (isError)</code> or <code>if (isLoading)</code> inside the component; instead, you wrap it with the two boundaries externally.</p>
<h2 id="queryerrorresetboundary"><a class="anchor" href="#queryerrorresetboundary">QueryErrorResetBoundary</a></h2>
<p>By now, another question may have occurred to you. What happens when the user clicks the "Try again" button in the fallback?</p>
<p>As we saw earlier, <code>resetErrorBoundary</code> only resets the ErrorBoundary's <code>hasError</code> state. But the TanStack Query cache still contains a <strong>query stuck in an error state</strong>. When the children render again, TanStack Query checks the cache, concludes, "This query is already in an error state," and immediately throws the same error again. (It is an infinite loop from hell.)</p>
<p>To solve this problem, TanStack Query provides the <strong><code>useQueryErrorResetBoundary</code> hook</strong> and the <strong><code>QueryErrorResetBoundary</code> component</strong>. Their names are long, but their job is simple: issue the command <strong>"Reset the error state of the queries in this scope."</strong></p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { useQueryErrorResetBoundary } </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">from</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> '@tanstack/react-query'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { ErrorBoundary } </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">from</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> 'react-error-boundary'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">function</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> App</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">() {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">reset</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> } </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> useQueryErrorResetBoundary</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">();</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">ErrorBoundary</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">      onReset</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{reset}</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">      fallbackRender</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{({ </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">resetErrorBoundary</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> }) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">        &#x3C;</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">div</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">          &#x3C;</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">p</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">>에러가 발생했습니다.&#x3C;/</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">p</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">          &#x3C;</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">button</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> onClick</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{resetErrorBoundary}>다시 시도&#x3C;/</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">button</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">        &#x3C;/</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">div</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">      )}</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    ></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">      &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Page</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> /></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    &#x3C;/</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">ErrorBoundary</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  );</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>Let's walk through what happens here in chronological order.</p>
<ol>
<li>The user clicks the "Try again" button → <code>resetErrorBoundary()</code> is called</li>
<li>The ErrorBoundary runs its <code>onReset</code> callback → <code>reset()</code> is called (resetting TanStack Query's error state)</li>
<li>The ErrorBoundary resets its own state and renders its children again</li>
<li>The <code>useQuery</code> inside the children runs → with the error state cleared, it tries to fetch again</li>
</ol>
<p>The key is the connection from <code>onReset</code> to <code>reset</code>. That single line synchronizes the state of the ErrorBoundary and TanStack Query.</p>
<h3 id="using-the-component-form"><a class="anchor" href="#using-the-component-form">Using the Component Form</a></h3>
<p>You can achieve the same result with a component instead of the hook. You only need one of the two.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { QueryErrorResetBoundary } </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">from</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> '@tanstack/react-query'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { ErrorBoundary } </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">from</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> 'react-error-boundary'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">function</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> App</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">() {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">QueryErrorResetBoundary</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">      {({ </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">reset</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> }) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">        &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">ErrorBoundary</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">          onReset</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{reset}</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">          fallbackRender</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{({ </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">error</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">resetErrorBoundary</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> }) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">            &#x3C;</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">div</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> role</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"alert"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">              &#x3C;</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">p</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">>에러가 발생했습니다: {error.message}&#x3C;/</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">p</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">              &#x3C;</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">button</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> onClick</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{resetErrorBoundary}>다시 시도&#x3C;/</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">button</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">            &#x3C;/</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">div</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">          )}</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">        ></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">          &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Page</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> /></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">        &#x3C;/</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">ErrorBoundary</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">      )}</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    &#x3C;/</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">QueryErrorResetBoundary</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  );</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>The biggest difference from the hook version is that it passes the <code>reset</code> function down to its child through the <strong>render prop pattern</strong>. <code>QueryErrorResetBoundary</code> accepts a function as its children, passes <code>{ reset }</code> as an argument, and renders that function's return value. This lets you connect it directly with <code>onReset={reset}</code> inside.</p>
<p>When there is no nearest <code>QueryErrorResetBoundary</code>, the hook version <strong>resets errors in the global cache</strong>. The component version limits the reset scope to its own child subtree. If you want tighter control over the scope, the component version is safer.</p>
<p>One point is worth emphasizing here: <strong>reset does not clear the cache.</strong> It does not discard all the data; it is closer to releasing queries from their "errored" state. If you actually want to invalidate the data, you must call <code>queryClient.invalidateQueries()</code> separately.</p>
<h2 id="mutation-errors"><a class="anchor" href="#mutation-errors">Mutation Errors</a></h2>
<p>Almost all the patterns discussed so far have focused on <code>useQuery</code>. But <strong><code>useMutation</code> works somewhat differently.</strong></p>
<p>The biggest difference is that a mutation is usually triggered by an <strong>explicit user action, such as a click or submission</strong>. It is therefore natural to handle the error close to that action. Rather than covering the whole page with a fallback, it is more appropriate to show something like "Payment failed: please check your card information" in a toast or next to the form.</p>
<p>TkDodo's <a href="https://tkdodo.eu/blog/mastering-mutations-in-react-query" target="_blank" rel="noopener noreferrer">Mastering Mutations in React Query</a> summarizes the essence of this difference in one sentence: <strong>queries are declarative, while mutations are imperative.</strong> A query runs automatically when its component mounts, can be subscribed to by other components using the same key, and is cached for reuse. In contrast, a mutation only runs when the user presses a button, is not cached, and is tied one-to-one to the component instance that invoked it. This fundamental difference divides their error-handling approaches.</p>
<p><code>useQuery</code> uses a default <code>retry</code> value of <code>3</code>, but <strong><code>useMutation</code> uses a default <code>retry</code> value of <code>0</code>.</strong> The reason is simple: mutations cause <strong>side effects</strong>. If a payment request fails because of a network timeout and the library automatically invokes it twice more, the user's card might be charged three times.</p>
<p>The rule, therefore, is to enable retries for a mutation explicitly <strong>only when you are certain that the operation is idempotent</strong>. This applies to safe GET-style reads whose result is guaranteed to remain the same when the same request is sent twice, or when the server prevents duplicates with an idempotency key.</p>
<p>An error from <code>useQuery</code> is <strong>stored in the cache</strong>. It therefore propagates immediately to other components subscribing to the same <code>queryKey</code>, and must be reset collectively with a mechanism such as <code>QueryErrorResetBoundary</code>.</p>
<p>Mutations are different. An error from a mutation instance in one component <strong>remains only in that instance's state.</strong> It has no effect on a mutation in another component using the same <code>mutationFn</code>. This is why TanStack Query has no equivalent such as <code>MutationErrorResetBoundary</code>: <strong>there is no need for one.</strong></p>
<p>This distinction has one practical consequence. When two components call the same <code>useMutation</code>, an error in one is not visible in the other. If you want "the entire app to know about errors from this mutation," component-level <code>onError</code> is insufficient; you need to promote them through <code>MutationCache.onError</code>.</p>
<h3 id="mutate-vs-mutateasync"><a class="anchor" href="#mutate-vs-mutateasync">mutate vs mutateAsync</a></h3>
<p><code>useMutation</code> returns two execution functions. The difference between them determines how errors are handled.</p>
<p>The return type of mutate is <code>void</code>. It does not return a Promise. You therefore cannot await its result, and can receive the outcome only through callbacks such as <code>onSuccess/onError</code>.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> mutation</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> useMutation</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  mutationFn: createPost,</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  onError</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: (</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">error</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    toast.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">error</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">`등록 실패: ${</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">error</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">.</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">message</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">}`</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  },</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">});</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">mutation.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">mutate</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(newPost);</span></span></code></pre></figure>
<p>In contrast, <code>mutateAsync</code> returns a Promise. You can handle errors with <code>try/catch</code>.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> mutation</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> useMutation</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({ mutationFn: createPost });</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> handleSubmit</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> async</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> () </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  try</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">    const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> result</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> await</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> mutation.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">mutateAsync</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(newPost);</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    router.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">push</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">`/posts/${</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">result</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">.</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">id</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">}`</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  } </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">catch</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (error) {</span></span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">    // 여기서 처리</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  }</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">};</span></span></code></pre></figure>
<p>When should you use each one? I distinguish between them using the following criteria.</p>
<ul>
<li><strong>A follow-up action is required after the mutation finishes</strong> (for example, routing on success or using the returned value) → <code>mutateAsync</code></li>
<li><strong>You only need to invoke it and can leave side effects to callbacks</strong> (for example, toggling a like or only showing a toast) → <code>mutate</code> + <code>onError</code></li>
</ul>
<p>There is one common mistake here. <strong>If you use <code>mutateAsync</code> without <code>try/catch</code>, an unhandled promise rejection occurs.</strong> The callback-based <code>mutate</code> absorbs the error, but <code>mutateAsync</code> throws it to the caller by default. Mixing the two without understanding this difference fills the console with red warnings.</p>
<h3 id="onerror"><a class="anchor" href="#onerror">onError</a></h3>
<p>There is another detail that is often overlooked. <code>useMutation</code>'s <code>onError</code> callback can be defined in <strong>two places</strong>: at the hook level and at the mutate level.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> mutation</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> useMutation</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  mutationFn: createPost,</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  onError</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: (</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">error</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    Sentry.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">captureException</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(error);</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  },</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">});</span></span></code></pre></figure>
<p>The hook-level callback always runs, while the mutate-level callback is defined at the call site.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">mutation.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">mutate</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(newPost, {</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  onError</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: (</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">error</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">    setFormError</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(error.message);</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  },</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">});</span></span></code></pre></figure>
<p>The official documentation specifies this execution order: <strong>hook level → mutate level.</strong> If both callbacks are defined, the hook-level callback runs first, followed by the mutate-level callback.</p>
<h2 id="global-error-handling"><a class="anchor" href="#global-error-handling">Global Error Handling</a></h2>
<p>All the patterns discussed so far operate at the component level. But you may have requirements such as "log every query error in one place" or "always log out on a 401 error." For these cross-cutting concerns, you can attach callbacks to <code>QueryCache</code>/<code>MutationCache</code> when creating the <strong>QueryClient</strong>.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { QueryClient, QueryCache, MutationCache } </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">from</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> '@tanstack/react-query'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> queryClient</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> new</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> QueryClient</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  queryCache: </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">new</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> QueryCache</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">    onError</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: (</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">error</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">query</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">      if</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (query.state.data </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">!==</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> undefined</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) {</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">        toast.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">error</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">`데이터 갱신 실패: ${</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">error</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">.</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">message</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">}`</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">      }</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    },</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  }),</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  mutationCache: </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">new</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> MutationCache</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">    onError</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: (</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">error</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">      if</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (error.status </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">===</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 401</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) {</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">        redirectToLogin</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">      }</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    },</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  }),</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">});</span></span></code></pre></figure>
<p>The key is that <code>QueryCache.onError</code> is called <strong>only once for each query</strong>. Even if multiple components subscribe to the same query, the callback runs only once, preventing problems such as duplicate toasts.</p>
<p>You can also check <code>query.state.data !== undefined</code>, as in the example above. If a <strong>refetch fails while cached data already exists</strong>, the user can still see data on the screen. Covering the page with an ErrorBoundary at that point is excessive; it is more appropriate simply to notify the user that the refresh failed. Conversely, if the initial load fails when no cached data exists, the ErrorBoundary should catch it and show a fallback.</p>
<p>Combining these two flows lets you design a clean policy: "Use an ErrorBoundary for initial-load failures and a toast for background refetch failures."</p>
<h2 id="a-shared-component"><a class="anchor" href="#a-shared-component">A Shared Component</a></h2>
<p>At this point, it is tempting to think: wrapping everything in <code>QueryErrorResetBoundary</code>, <code>ErrorBoundary</code>, and <code>Suspense</code> every time is cumbersome, so why not <strong>combine them into one reusable component</strong>?</p>
<p>It is a natural idea. In fact, I once built and used an <code>AsyncBoundary</code> component like this.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { QueryErrorResetBoundary } </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">from</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> '@tanstack/react-query'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { Suspense, </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">type</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> ComponentType, </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">type</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> ReactNode } </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">from</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> 'react'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { ErrorBoundary, </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">type</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> FallbackProps } </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">from</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> 'react-error-boundary'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { ErrorFallback } </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">from</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> './ErrorFallback'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { Spinner } </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">from</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> './Spinner'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">interface</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> Props</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  children</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> ReactNode</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  pendingFallback</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">?:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> ReactNode</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  rejectedFallback</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">?:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> ComponentType</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">FallbackProps</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">>;</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">export</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> function</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> AsyncBoundary</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  children</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  pendingFallback</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Spinner</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> />,</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  rejectedFallback</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> ErrorFallback,</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> Props</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">QueryErrorResetBoundary</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">      {({ </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">reset</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> }) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">        &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">ErrorBoundary</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> onReset</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{reset} </span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">FallbackComponent</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{rejectedFallback}></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">          &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Suspense</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> fallback</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{pendingFallback}>{children}&#x3C;/</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Suspense</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">        &#x3C;/</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">ErrorBoundary</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">      )}</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    &#x3C;/</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">QueryErrorResetBoundary</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  );</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>On a page, it can be reduced to a single line.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">AsyncBoundary</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Content</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> /></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">AsyncBoundary</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span></code></pre></figure>
<p>It looks clean. But a colleague gave me the following feedback.</p>
<blockquote>
<p>The name AsyncBoundary is not such a universally established term that having different things inside it would feel especially strange, but <strong>the presence of React Query's ResetBoundary is somewhat hard to anticipate.</strong></p>
</blockquote>
<blockquote>
<p>And the default values for <code>pendingFallback</code> and <code>rejectedFallback</code> also bother me a little. From the single line <code>&#x3C;AsyncBoundary></code>, there is no way to tell which fallbacks are applied inside, so <strong>I don't think readers will even realize that these props have default values.</strong></p>
</blockquote>
<h3 id="the-name-hides-a-dependency"><a class="anchor" href="#the-name-hides-a-dependency">The Name Hides a Dependency</a></h3>
<p>This component is named <code>AsyncBoundary</code>. The name conveys only the idea of an asynchronous boundary. But its implementation is <strong>strongly coupled to TanStack Query</strong>. It includes <code>QueryErrorResetBoundary</code>, with <code>onReset</code> connected to <code>reset</code>. In other words, this component is really <strong>"a boundary for asynchronous regions that use React Query,"</strong> but its name does not reveal that at all.</p>
<p>Why is this a problem? Because it <strong>violates the reader's expectations</strong>. We do not read code by interpreting it one line at a time; we read by <strong>predicting</strong> from patterns built through experience. When those predictions fail, cognitive load rises sharply.</p>
<p>When a colleague first sees the name <code>AsyncBoundary</code>, they picture "a general-purpose boundary for asynchronous work." It seems as though it could be used with SWR or direct fetch calls too. In reality, however, <code>QueryErrorResetBoundary</code> is embedded inside it, introducing a <strong>meaningless coupling in contexts that do not use TanStack Query</strong>. There is a mismatch between the name and the implementation.</p>
<p>You could view this as the reverse of a leaky abstraction. An ordinary leak exposes details that should have been hidden behind the abstraction; here, <strong>a dependency that should be visible is hidden too effectively behind the name.</strong> It may be worse. (You use it without realizing.)</p>
<h3 id="reveal-the-dependency-in-the-name"><a class="anchor" href="#reveal-the-dependency-in-the-name">Reveal the Dependency in the Name</a></h3>
<p>The simplest remedy is to rename it. Instead of <code>AsyncBoundary</code>, make the dependency explicit with a name such as <strong><code>QueryAsyncBoundary</code></strong>. Looking at the <a href="https://suspensive.org/" target="_blank" rel="noopener noreferrer">Suspensive</a> library built by Toss, I found that it makes this dependency explicit. <code>@suspensive/react</code> contains only the general-purpose <code>ErrorBoundary</code> and <code>Suspense</code>, while <code>@suspensive/react-query</code> provides the TanStack Query-integrated <code>QueryAsyncBoundary</code> component as a separate package.</p>
<p>That one-word difference communicates a great deal to readers. The moment the <code>Query</code> prefix appears, it immediately signals, <strong>"This is specifically for a TanStack Query environment."</strong> It prevents accidental use in the wrong context.</p>
<h3 id="break-it-down-into-composable-units"><a class="anchor" href="#break-it-down-into-composable-units">Break It Down into Composable Units</a></h3>
<p>A more fundamental approach is <strong>not to bundle them at all</strong>.</p>
<p>ErrorBoundary and Suspense are fundamentally <strong>different concerns</strong>, and combining them into a single component can reduce compositional flexibility. Some pages may need only an ErrorBoundary, others only Suspense, and still others may want two Suspense boundaries inside one ErrorBoundary. Bundling them into <code>AsyncBoundary</code> makes such variations awkward. Keeping them separate allows free composition.</p>
<p>This pattern adds one more line of code, but has the advantage that <strong>each boundary's responsibility is directly visible in the code</strong>. And when using <code>useSuspenseQuery</code>, the unit you usually want to handle as a single loading state differs from the unit whose errors you want to catch, so separating them tends to feel more natural.</p>
<p>My conclusion is this: <strong>bundle the boundaries if the repeated composition pattern is truly identical; separate them if you need variations.</strong> Even when you bundle them, make the dependency visible in the name. Following these two principles alone will reduce review feedback along the lines of, "I don't know what is inside AsyncBoundary."</p>
<h3 id="default-props"><a class="anchor" href="#default-props">Default Props</a></h3>
<p>Fixing the naming issue is not enough. Look again at the code above.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">pendingFallback </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Spinner</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> />,</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">rejectedFallback </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> ErrorFallback,</span></span></code></pre></figure>
<p>The reason a single line such as <code>&#x3C;QueryAsyncBoundary>...&#x3C;/QueryAsyncBoundary></code> works is that <code>Spinner</code> and <code>ErrorFallback</code> are automatically supplied internally. <strong>That is not information you can infer from the name.</strong></p>
<p>This is another version of the earlier criticism that "the name hides a dependency." The <code>Query</code> prefix now reveals the Query dependency, but the UI dependencies on <code>Spinner</code> and <code>ErrorFallback</code> remain hidden behind default props. <strong>The hiding has merely moved one layer deeper.</strong></p>
<p>The solution is simple: <strong>make both fallbacks required props and inject them at every call site.</strong></p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">interface</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> Props</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  children</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> ReactNode</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  pendingFallback</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> ReactNode</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;                    </span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  rejectedFallback</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> ComponentType</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">FallbackProps</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">>;</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span></code></pre></figure>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">QueryAsyncBoundary</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  pendingFallback</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{&#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Spinner</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> />}</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  rejectedFallback</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{ErrorFallback}</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Content</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> /></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">QueryAsyncBoundary</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span></code></pre></figure>
<p>This makes the code two lines longer. The reason to accept that cost is clear: <strong>it increases the cost for the author while reducing the tracing cost for every reader.</strong> At the call site, you can immediately see which fallbacks will appear. There is no need to open another file to check, "What were this component's defaults again?" The familiar principle that code is read far more often than it is written applies here too.</p>
<h2 id="errorfallback"><a class="anchor" href="#errorfallback">ErrorFallback</a></h2>
<p>There is one more point to consider. An <code>ErrorFallback</code> is usually created as a single component like this.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> DEFAULT_ERROR_MESSAGE</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> '문제가 발생했어요. 잠시 후 다시 시도해주세요'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">export</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> function</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> ErrorFallback</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({ </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">error</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">resetErrorBoundary</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> FallbackProps</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> message</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> getErrorMessage</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(error, </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">DEFAULT_ERROR_MESSAGE</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Flex</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> direction</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"column"</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> alignItems</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"center"</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> role</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"alert"</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> aria-live</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"assertive"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">      &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Text</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">>{message}&#x3C;/</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Text</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">      &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Spacing</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> size</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">16</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">} /></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">      &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Button</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> onClick</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{resetErrorBoundary}>다시 시도&#x3C;/</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Button</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    &#x3C;/</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Flex</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  );</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>It is a clean implementation that even includes <code>role="alert"</code> and <code>aria-live="assertive"</code>. But consider one question: <strong>"Is it okay to show the same screen for a 401, a 404, a 500, and a network outage?"</strong></p>
<p>In most cases, the answer is <strong>no</strong>. The action the user should take differs by error type.</p>
<table>
<thead>
<tr>
<th>Error type</th>
<th>User action</th>
<th>Does "Try again" make sense?</th>
</tr>
</thead>
<tbody>
<tr>
<td>Network outage</td>
<td>Check the connection and retry</td>
<td>O</td>
</tr>
<tr>
<td>5xx server error</td>
<td>Try again later</td>
<td>O</td>
</tr>
<tr>
<td>401 authentication failure</td>
<td>Go to the login screen</td>
<td>X</td>
</tr>
<tr>
<td>403 forbidden</td>
<td>Go to another screen</td>
<td>X</td>
</tr>
<tr>
<td>404 resource not found</td>
<td>Return to the list</td>
<td>△</td>
</tr>
<tr>
<td>422 validation failure</td>
<td>Correct the input</td>
<td>X</td>
</tr>
</tbody>
</table>
<p>Showing a "Try again" button in every case incorrectly tells the user <strong>"which action can resolve the error."</strong> Pressing "Try again" for a 401 only produces the same 401 again. The action the user actually needs to take is logging in.</p>
<p>The error fallback should therefore <strong>render differently depending on the error type</strong>. There is no need to start with a giant <code>if/else</code>; you can create small components and branch between them.</p>
<p>Each fallback component should expose only the message and action appropriate for that error. Only actions the user can actually take should remain on the screen.</p>
<h3 id="shouldcatch"><a class="anchor" href="#shouldcatch">shouldCatch</a></h3>
<p>Taking this one step further, there is also a pattern that <strong>distinguishes errors to catch from errors to let through at the component level</strong>. Suspensive's <code>ErrorBoundary</code> provides a <code>shouldCatch</code> prop.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">ErrorBoundary</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  shouldCatch</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{(</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">error</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> isHttpError</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(error) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">&#x26;&#x26;</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> error.status </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">>=</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 500</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  fallback</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{ServerErrorFallback}</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">ErrorBoundary</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> shouldCatch</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{NetworkError} </span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">fallback</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{NetworkErrorFallback}></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Page</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> /></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  &#x3C;/</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">ErrorBoundary</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">ErrorBoundary</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span></code></pre></figure>
<p>The inner ErrorBoundary catches only network errors and does not catch 5xx errors. Following React's default behavior, uncaught errors <strong>propagate to the parent ErrorBoundary</strong>. The outer ErrorBoundary can then catch the 5xx errors. Compared with implementing the same error handling using if/else, the appeal of this approach is that it <strong>gives semantic meaning to the boundaries themselves</strong>.</p>
<p><code>react-error-boundary</code> does not have this prop, but you can achieve the same effect by branching inside the fallback. The pattern matters, not the library.</p>
<h2 id="conclusion"><a class="anchor" href="#conclusion">Conclusion</a></h2>
<p>In summary, frontend error handling <strong>cannot be solved with a single tool</strong>. Error Boundaries handle render-phase errors; <code>try/catch</code> or <code>showBoundary</code> handles errors from event handlers; TanStack Query's <code>throwOnError</code> and <code>useQueryErrorResetBoundary</code> handle asynchronous data-fetching errors; <code>mutateAsync</code> or <code>onError</code> handles mutation errors; and <code>QueryCache</code>/<code>MutationCache</code> handles cross-cutting concerns. On top of that, you must design <strong>the names and composition boundaries of shared components</strong> and <strong>the domain modeling of the error types themselves</strong> to arrive at a consistent error policy.</p>
<p>Once you understand what each tool is responsible for, you can make explicit decisions such as <strong>"Catch this error here and let that error flow there."</strong> The accumulation of those decisions is ultimately what makes the user experience reliable: preventing blank screens, avoiding the same toast appearing five times, keeping a transient network error from taking down the whole page, and showing the login screen instead of "Try again" for a 401. Details like these add up to the impression of a well-built service.</p>
<p>Of course, not every project needs every pattern. For a simple admin tool, one ErrorBoundary and a toast may be enough. In a domain such as payments, where one mistake directly costs money, every mutation may require fine-grained error handling. The domain determines the right answer.</p>
<p>I encourage readers to examine their own projects and ask, "Which errors does our service catch, where does it catch them, and what are the components called?" You may find a surprising number of errors that you believed were handled properly but are actually leaking out or reaching the wrong fallback. (That has happened to me every time.)</p>
<h2 id="references"><a class="anchor" href="#references">References</a></h2>
<details class="ref-block"><summary class="ref-summary">참고 자료</summary><div class="ref-list">
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#3b82f6"></span><a href="https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary" target="_blank" rel="noopener noreferrer">React, Error Boundaries</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#3b82f6"></span><a href="https://tanstack.com/query/latest/docs/framework/react/guides/suspense" target="_blank" rel="noopener noreferrer">TanStack Query, Suspense</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#3b82f6"></span><a href="https://tanstack.com/query/latest/docs/framework/react/reference/QueryErrorResetBoundary" target="_blank" rel="noopener noreferrer">TanStack Query, QueryErrorResetBoundary</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#3b82f6"></span><a href="https://tanstack.com/query/v5/docs/framework/react/guides/important-defaults" target="_blank" rel="noopener noreferrer">TanStack Query, Important Defaults</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#f59e0b"></span><a href="https://tkdodo.eu/blog/react-query-error-handling" target="_blank" rel="noopener noreferrer">TkDodo, React Query Error Handling</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#f59e0b"></span><a href="https://tkdodo.eu/blog/breaking-react-querys-api-on-purpose" target="_blank" rel="noopener noreferrer">TkDodo, Breaking React Query's API on Purpose</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#10b981"></span><a href="https://github.com/toss/suspensive" target="_blank" rel="noopener noreferrer">toss/suspensive, @suspensive/react-query</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#3b82f6"></span><a href="https://reactrouter.com/how-to/error-boundary" target="_blank" rel="noopener noreferrer">React Router, Error Boundaries</a></div>
</div>
</div></details>]]></content:encoded>
            <author>jihoon7705@gmail.com (이지훈)</author>
            <category>프론트엔드</category>
            <category>React</category>
            <category>TanStack-Query</category>
            <category>에러핸들링</category>
        </item>
        <item>
            <title><![CDATA[Mastering React Fiber]]></title>
            <link>https://hooninedev.com/en/250520</link>
            <guid isPermaLink="false">https://hooninedev.com/en/250520</guid>
            <pubDate>Tue, 20 May 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[In this post, I want to talk about the Fiber architecture, which could be called the heart of React. When I first encountered React, I thought of the word "Fiber" as little more than a common intervie...]]></description>
            <content:encoded><![CDATA[<p>In this post, I want to talk about the <strong>Fiber architecture</strong>, which could be called the heart of React.</p>
<p>When I first encountered React, I thought of the word <strong>"Fiber"</strong> as little more than a common interview question. I memorized a one-line definition—"it divides rendering into units of work and processes them"—and assumed that was the whole story. But once I began looking through React's actual source code, I realized that Fiber is not merely a concept. It is the runtime architecture that governs <strong>everything</strong> about React rendering.</p>
<blockquote>
<p>I still cannot forget the shock of opening the React source code for the first time. I remember thinking, "What... is all of this?"</p>
</blockquote>
<p>This article goes beyond answering "What is Fiber?" with "It divides work into units." We will dig deeply into <strong>why</strong> Fiber was created, <strong>how</strong> it was designed, and <strong>how</strong> that structure enables React's Concurrent Features.</p>
<h2 id="why-was-fiber-introduced"><a class="anchor" href="#why-was-fiber-introduced">Why Was Fiber Introduced?</a></h2>
<p>To answer this question, we first need to understand the problems in the world before Fiber: the <strong>Stack Reconciler</strong>, which React used through version 15.</p>
<p>As its name suggests, the Stack Reconciler was a reconciliation engine based on <strong>recursive calls</strong>. It traversed the component tree recursively from top to bottom, and once rendering began, it could not stop until it had processed the entire tree. It was like being unable to hang up a phone call until the other person had finished speaking. (Imagine that they start a three-hour counseling session about their life and you cannot interrupt. Terrifying.)</p>
<p>More specifically, the Stack Reconciler had the following limitations.</p>
<ul>
<li><strong>Rendering could not be interrupted</strong>: Because the entire tree had to be processed at once, the main thread could be occupied for tens or hundreds of milliseconds in a complex UI</li>
<li><strong>No concept of priority</strong>: Whether a user clicked a button or background data was updated, every update was processed in the same way</li>
<li><strong>Poor handling of animations and gestures</strong>: Maintaining 60 fps requires all work to finish within roughly 16 ms per frame, something recursive rendering could not guarantee</li>
<li><strong>One error could halt the entire app</strong>: An error anywhere in the component tree could stop the whole application</li>
</ul>
<p>To overcome these limitations, the React team considered a new execution model that could <strong>split up</strong> work, <strong>assign priorities</strong>, and <strong>pause and resume</strong> when necessary. The result was <strong>React Fiber</strong>.</p>
<p>Andrew Clark's <a href="https://github.com/acdlite/react-fiber-architecture" target="_blank" rel="noopener noreferrer">react-fiber-architecture</a> document captures the core ideas behind this design and is the most important reference for understanding Fiber. (It appears that he joined the React team not long after writing it.)</p>
<h2 id="stack-vs-fiber"><a class="anchor" href="#stack-vs-fiber">Stack vs Fiber</a></h2>
<p>So how do the Stack Reconciler and Fiber Reconciler differ at the code level?</p>
<h3 id="the-recursion-based-stack-reconciler"><a class="anchor" href="#the-recursion-based-stack-reconciler">The Recursion-Based Stack Reconciler</a></h3>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="jsx" data-theme="github-dark github-light"><code data-language="jsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">function</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> renderComponent</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">component</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> element</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> component.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">render</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  element.props.children.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">forEach</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">child</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =></span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> renderComponent</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(child)); </span><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 재귀 호출</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>With the Stack approach, encountering a child component leads <strong>immediately into a recursive call</strong>. The problem is that this approach depends directly on JavaScript's call stack. As recursive calls deepen, frames accumulate on the call stack, and the browser's main thread cannot do anything else until every one of those frames has been resolved.</p>
<p>Simply put, the browser is <strong>completely unable to move</strong> until the call stack is empty.</p>
<video width="640" height="480" controls>
  <source src="/content/250520/stack.mov" type="video/mp4">
</video>
<p>The video above shows the main thread being completely blocked while the Stack Reconciler renders.</p>
<h3 id="the-iteration-based-fiber-reconciler"><a class="anchor" href="#the-iteration-based-fiber-reconciler">The Iteration-Based Fiber Reconciler</a></h3>
<p>Fiber replaced recursion with an <strong>iterative loop</strong>. In place of the call stack, it implements its own <strong>virtual stack</strong> in memory. Each Fiber node is effectively a "stack frame," and because these nodes exist as JavaScript objects in heap memory, work can be paused at any point and resumed later.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="jsx" data-theme="github-dark github-light"><code data-language="jsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">function</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> performWork</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">deadline</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  while</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (nextUnitOfWork </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">&#x26;&#x26;</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> deadline.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">timeRemaining</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">() </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">></span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 5</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) {</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    nextUnitOfWork </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> performUnitOfWork</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(nextUnitOfWork);</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  }</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  requestIdleCallback</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(performWork); </span><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 나눠서 실행</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>The code above illustrates Fiber's early conceptual model. The key is that the <code>while</code> loop processes only one unit of work at a time and, when time runs short, exits the loop and returns control to the browser.</p>
<p>(The initial approach used <code>requestIdleCallback</code>, but React does not actually use it. We will examine why later.)</p>
<video width="640" height="480" controls>
  <source src="/content/250520/fiber.mov" type="video/mp4">
</video>
<p>With Fiber, React can respond immediately to user events such as button clicks and typing even during rendering. Breaking work into small pieces gives the browser room to breathe.</p>
<p>If you want to experience the difference firsthand, click <strong><a href="https://animated-lollipop-2b6cbb.netlify.app/" target="_blank" rel="noopener noreferrer">here</a></strong>. You can see how the Stack Reconciler and Fiber Reconciler behave differently.</p>
<p>These are precisely the core goals of Fiber that Andrew Clark emphasized in his document.</p>
<ul>
<li><strong>Pause work and return to it later</strong></li>
<li><strong>Assign priority to different types of work</strong></li>
<li><strong>Reuse previously completed work</strong></li>
<li><strong>Abort work that is no longer needed</strong></li>
</ul>
<h2 id="inside-a-fiber-node"><a class="anchor" href="#inside-a-fiber-node">Inside a Fiber Node</a></h2>
<p>At this point, one question naturally comes to mind: "So what does a Fiber node look like internally?"</p>
<p>The React team does not provide separate official documentation for Fiber's internal implementation. However, its structure can be understood through Andrew Clark's react-fiber-architecture document and React's actual source code (<code>ReactFiber.js</code>).</p>
<p>I like to compare a Fiber node to a <strong>work order</strong>. When a product is assembled in a factory, each work order specifies what kind of part it is, what materials it uses, which task comes next, and what its priority is. A Fiber node works in much the same way.</p>
<h3 id="reactelement-and-fibernode"><a class="anchor" href="#reactelement-and-fibernode">ReactElement and FiberNode</a></h3>
<p>To understand Fiber, you must first distinguish <strong>ReactElement</strong> from <strong>FiberNode</strong>. They are often confused, but they are completely different things.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="ts" data-theme="github-dark github-light"><code data-language="ts" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// ReactElement — React.createElement()가 반환하는 가벼운 객체</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">export</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> interface</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> ReactElement</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  type</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> |</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> Function</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">; </span><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 문자열(HTML 태그) 또는 함수(컴포넌트)</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  props</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    [</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">key</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">]</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> any</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">    children</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> ReactElement</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">[];</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  };</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  key</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> |</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> null</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  ref</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> any</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  _owner</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> FiberNode</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> |</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> null</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>A ReactElement is only a <strong>blueprint</strong> for the UI. It is a request saying, "Render this component with these props," and contains no actual rendering logic or state.</p>
<p>By contrast, a <strong>FiberNode</strong> is the <strong>runtime unit of work</strong> that React creates internally from that blueprint. It contains fields that do not exist on a ReactElement, including <code>tag</code>, <code>stateNode</code>, <code>child/sibling/return</code>, <code>memoizedState</code>, <code>updateQueue</code>, and <code>lanes</code>.</p>
<p>When React examines a ReactElement's <code>type</code> and creates a FiberNode, it determines the <strong>tag</strong> value.</p>
<ul>
<li>If <code>type</code> is a function with <code>prototype.isReactComponent</code> → <code>tag = ClassComponent(1)</code></li>
<li>If <code>type</code> is a function → <code>tag = FunctionComponent(0)</code></li>
<li>If <code>type</code> is a string (such as <code>"div"</code>) → <code>tag = HostComponent(5)</code></li>
</ul>
<p><strong>tag</strong> is a numeric constant indicating the kind of FiberNode. It is defined in <code>ReactWorkTags.js</code>, and there are more than 25 tags, including <code>FunctionComponent(0)</code>, <code>ClassComponent(1)</code>, <code>HostRoot(3)</code>, <code>HostComponent(5)</code>, and <code>HostText(6)</code>. React uses this tag value in <code>beginWork</code> to decide which processing logic to run.</p>
<p><strong>type</strong> plays a central role in reconciliation. When React compares a Fiber from the previous render with a new element, type is the <strong>very first thing it checks</strong>. (The value is passed directly from the ReactElement to the FiberNode.)</p>
<ul>
<li>If it was a <code>div</code> before and is still a <code>div</code>, React <strong>reuses</strong> that Fiber node and updates only its props</li>
<li>If it was a <code>div</code> before but has changed to a <code>span</code>, React <strong>discards</strong> the old Fiber and creates a new one</li>
</ul>
<p><strong>key</strong> is also passed from the ReactElement to the FiberNode and is used primarily when rendering lists (arrays). Without a key, React cannot accurately determine where each item moved when the order of list items changes. This can cause unnecessary DOM operations or unintentionally preserve or lose a component's internal state.</p>
<h3 id="child-sibling-return"><a class="anchor" href="#child-sibling-return">child, sibling, return</a></h3>
<p>This is the secret that lets React Fiber use iteration instead of recursion.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="js" data-theme="github-dark github-light"><code data-language="js" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">function</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> 부모</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">() {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> [&#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">자식1</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">/>, &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">자식2</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">/>];</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p><strong>child</strong> points to the <strong>first</strong> child element returned by the component's render. In the example above, that is <code>&#x3C;자식1/></code>. <strong>sibling</strong> means the <strong>next sibling</strong> with the same parent. The sibling of <code>&#x3C;자식1/></code> is <code>&#x3C;자식2/></code>. <strong>return</strong> points to the parent Fiber to <strong>return to</strong> once the current Fiber node has been processed. The return value of both <code>&#x3C;자식1/></code> and <code>&#x3C;자식2/></code> is <code>부모</code>.</p>
<p>The structure formed by these three fields is a <strong>tree represented as a singly linked list</strong>. In a conventional tree, keeping a child array (<code>children[]</code>) feels intuitive, but Fiber deliberately avoids doing so.</p>
<p>Why? An array-based child structure requires an index for traversal, and after pausing and resuming, React would need to track separately how far it had progressed. With a linked list structure, remembering only the current node reference is enough to resume traversal at any time. This is the structural foundation that allows Fiber to support <strong>pausing and resuming</strong> naturally.</p>
<p>React traverses nodes in depth-first search (DFS) order using this structure. It descends along <code>child</code> (beginWork); once it reaches a leaf node, it checks <code>sibling</code>; and when there is no sibling, it ascends along <code>return</code> (completeWork).</p>
<h3 id="pendingprops-and-memoizedprops"><a class="anchor" href="#pendingprops-and-memoizedprops">pendingProps and memoizedProps</a></h3>
<p><strong>pendingProps</strong> means the <strong>new props</strong> passed to a Fiber when its processing begins, while <strong>memoizedProps</strong> represents the <strong>previous props</strong> whose processing was completed in the prior render.</p>
<p>If these two values are identical, React can conclude that "nothing changed in this component" and reuse the previous rendering result. This is the core mechanism behind the <strong>bailout optimization</strong>.</p>
<p>Similarly, <strong>memoizedState</strong> stores the Fiber's hook state, while <strong>updateQueue</strong> manages pending state updates (calls to setState) as a linked list.</p>
<h3 id="statenode"><a class="anchor" href="#statenode">stateNode</a></h3>
<p><strong>stateNode</strong> references the <strong>actual instance</strong> represented by a Fiber node.</p>
<ul>
<li>For a <strong>HostComponent</strong> (div, span, and so on): the actual DOM node</li>
<li>For a <strong>ClassComponent</strong>: the class instance</li>
<li>For a <strong>HostRoot</strong>: the FiberRoot object</li>
</ul>
<p>This field serves as the bridge between Fiber's virtual world and the browser's actual DOM.</p>
<h2 id="double-buffering-the-current-and-workinprogress-trees"><a class="anchor" href="#double-buffering-the-current-and-workinprogress-trees">Double Buffering: The current and workInProgress Trees</a></h2>
<p>One essential concept that cannot be left out when discussing Fiber is <strong>double buffering</strong>.</p>
<p>Think about game graphics. If a game draws pixels directly onto the current screen, users may see a partially drawn frame, a visual artifact known as <strong>tearing</strong>. To prevent this, game engines use <strong>two buffers</strong>. They draw the next frame completely in one buffer and, once it is ready, swap the buffer displayed on screen all at once.</p>
<p>React Fiber uses exactly the same strategy.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="js" data-theme="github-dark github-light"><code data-language="js" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">currentFiber.alternate </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">===</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> workInProgressFiber;</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">workInProgressFiber.alternate </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">===</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> currentFiber;</span></span></code></pre></figure>
<p>The <strong>current tree</strong> is the Fiber tree currently reflected on screen. It represents the UI state the user is seeing, while the <strong>workInProgress tree</strong> is the Fiber tree being prepared in the background for the next render.</p>
<p>The two trees reference each other through the <code>alternate</code> property. All changes are made in the workInProgress tree, and when the work is complete, the trees are swapped with a single line: <code>root.current = finishedWork</code>. The previous workInProgress becomes the new current, and the previous current is recycled as workInProgress for the next render.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="js" data-theme="github-dark github-light"><code data-language="js" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">function</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> createWorkInProgress</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">current</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">pendingProps</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  let</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> workInProgress </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> current.alternate;</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  if</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (workInProgress </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">===</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> null</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) {</span></span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">    // 최초 렌더: 새 Fiber를 생성하고 alternate를 연결</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    workInProgress </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> createFiber</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(current.tag, pendingProps, current.key, current.mode);</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    workInProgress.stateNode </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> current.stateNode; </span><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// DOM 노드는 공유!</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    workInProgress.alternate </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> current;</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    current.alternate </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> workInProgress;</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  } </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">else</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">    // 재렌더: 기존 alternate를 재사용, effect만 초기화</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    workInProgress.pendingProps </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> pendingProps;</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    workInProgress.flags </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> NoFlags;</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    workInProgress.subtreeFlags </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> NoFlags;</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    workInProgress.deletions </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> null</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  }</span></span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">  // lanes, child, memoizedState 등을 복사</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  workInProgress.childLanes </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> current.childLanes;</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  workInProgress.child </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> current.child;</span></span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">  // ...</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>Here is the key point: <code>stateNode</code> (the actual DOM node) is <strong>shared</strong> between current and workInProgress. Rather than creating new Fiber objects on every render, React reuses the existing alternate and updates only the fields that changed. This allows it to construct the tree efficiently without adding garbage collection (GC) pressure on every render.</p>
<p>What if neither props nor state has changed? React can skip the entire subtree through the <strong>bailout optimization</strong>. If double buffering in games is a frame-level optimization, Fiber's double buffering enables optimization down to the <strong>component level</strong>.</p>
<h2 id="pendingworkpriority--lanes"><a class="anchor" href="#pendingworkpriority--lanes">pendingWorkPriority => Lanes</a></h2>
<p>How, then, does Fiber decide that one piece of work is more important than another?</p>
<h3 id="the-limits-of-expirationtime"><a class="anchor" href="#the-limits-of-expirationtime">The Limits of expirationTime</a></h3>
<p>Early Fiber used a numeric priority called <code>pendingWorkPriority</code>, which later evolved into a single number called <code>expirationTime</code>. A nearer expiration time meant higher priority, but this approach had a fundamental limitation.</p>
<p>A single number could not provide <strong>flexible grouping</strong> of the form "this update belongs to group A, while that update belongs to group B." For example, when user input and a Transition update occurred at the same time, the expirationTime-based approach could classify them only through range comparisons, limiting React's ability to selectively process particular updates.</p>
<h3 id="lane"><a class="anchor" href="#lane">Lane</a></h3>
<p>To solve this problem, Andrew Clark introduced the <strong>Lane system</strong> in <a href="https://github.com/facebook/react/pull/18796" target="_blank" rel="noopener noreferrer">PR #18796</a>.</p>
<p>To understand Lanes, picture a <strong>highway</strong>. A highway has multiple lanes, each serving a different purpose. The first lane is for passing (urgent), the second for normal travel, and the shoulder for emergencies. Each vehicle (update) is assigned to the lane appropriate to its nature, and the highway management system (scheduler) decides which lane's vehicles should pass first.</p>
<p>React's Lanes work the same way. Each update is assigned <strong>one bit (a lane)</strong>, and bitwise operations are used to create and compare groups.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="js" data-theme="github-dark github-light"><code data-language="js" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 각 업데이트는 하나의 lane(단일 비트)을 가진다</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> SyncLane</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">             /*  */</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 0b0000000000000000000000000000010</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> InputContinuousLane</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">  /*  */</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 0b0000000000000000000000000001000</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> DefaultLane</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">          /*  */</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 0b0000000000000000000000000100000</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> TransitionLane1</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">      /*  */</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 0b0000000000000000000000100000000</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> IdleLane</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">             /*  */</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 0b0001000000000000000000000000000</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 배치(batch)는 여러 비트의 OR 조합이다</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> SyncUpdateLanes</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> SyncLane </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">|</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> InputContinuousLane </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">|</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> DefaultLane;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 특정 lane이 batch에 포함되는지 확인은 단순 비트 연산</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> isIncluded</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (lane </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">&#x26;</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> lanes) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">!==</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 0</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span></code></pre></figure>
<p>The system is designed so that 31 lanes fit into a 31-bit integer, taking advantage of the V8 engine's <strong>SMI (Small Integer)</strong> optimization. Integers of 31 bits or fewer are handled with pointer tagging in V8 and can be operated on directly on the stack without heap allocation. Among the major lanes, a <strong>lower bit means a higher priority</strong>.</p>
<p>Thanks to this structure, React can decide which work to process first with a single bitwise operation. The <code>getNextLanes()</code> function selects the highest-priority lane group from <code>pendingLanes</code>, skips suspended lanes, and gives priority to retrying pinged lanes whose data has arrived, enabling sophisticated scheduling.</p>
<p>Each lane is also assigned an expiration time to prevent <strong>starvation</strong>. Sync/InputContinuous is added to <code>expiredLanes</code> after 250 ms, and Transition after 5,000 ms, forcing synchronous processing. In other words, no matter how low its priority, work is never ignored forever. (If low-priority work were ignored forever, that would not be a priority system; it would be a discrimination system.)</p>
<h2 id="fibers-output"><a class="anchor" href="#fibers-output">Fiber's output</a></h2>
<p>Now that we have examined Fiber's structure, another question arises: how do these Fiber nodes turn into the <strong>actual DOM</strong>?</p>
<p>The output represents concrete DOM node information that can be applied to the actual DOM. There is an important distinction here.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="jsx" data-theme="github-dark github-light"><code data-language="jsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 사용자 정의 컴포넌트 — output 없음</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">function</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> 아바타</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">() {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> &#x3C;</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">img</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> src</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"profile.jpg"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> />;</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 호스트 컴포넌트 — output 생성</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">img</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> src</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"profile.jpg"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> /></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">div</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> className</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"프로필"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> /></span></span></code></pre></figure>
<p>Only <strong>host components</strong> (div, span, img, and so on) create actual DOM nodes. The browser has no idea what <code>&#x3C;아바타/></code> is. A user-defined component is an abstraction, so it must ultimately be broken down into host components before the browser can understand it.</p>
<p>Let us examine this process in more detail.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="jsx" data-theme="github-dark github-light"><code data-language="jsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">function</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> 프로필</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">() {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    &#x3C;</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">div</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> className</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"프로필"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">      &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">아바타</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> /></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">      &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">유저정보</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> /></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    &#x3C;/</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">div</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  );</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">function</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> 아바타</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">() {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> &#x3C;</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">img</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> src</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"profile.jpg"</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> alt</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"프로필"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> />;</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">function</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> 유저정보</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">() {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    &#x3C;</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">div</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">      &#x3C;</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">h2</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">>홍길동&#x3C;/</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">h2</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">      &#x3C;</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">p</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">>개발자&#x3C;/</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">p</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    &#x3C;/</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">div</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  );</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>The relationship between the Fiber tree produced by these components and their output is as follows.</p>
<pre><code>프로필 (출력: 없음, 컴포넌트 함수)
  │
  └─► div.프로필 (출력: &#x3C;div class="프로필">...&#x3C;/div>)
       │
       ├─► 아바타 (출력: 없음, 컴포넌트 함수)
       │    │
       │    └─► img (출력: &#x3C;img src="profile.jpg" alt="프로필">)
       │
       └─► 유저정보 (출력: 없음, 컴포넌트 함수)
            │
            └─► div (출력: &#x3C;div>...&#x3C;/div>)
                 │
                 ├─► h2 (출력: &#x3C;h2>홍길동&#x3C;/h2>)
                 │
                 └─► p (출력: &#x3C;p>개발자&#x3C;/p>)
</code></pre>
<p>output is collected <strong>from the bottom up</strong>. DOM nodes are created first at the leaf (host) nodes.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="js" data-theme="github-dark github-light"><code data-language="js" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 호스트 컴포넌트들이 실제 DOM 정보 생성</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">img_fiber.output </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> createDOMElement</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'img'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, {</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  src: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'profile.jpg'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  alt: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'프로필'</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">});</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">h2_fiber.output </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> createDOMElement</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'h2'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, {}, </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'홍길동'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">p_fiber.output </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> createDOMElement</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'p'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, {}, </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'개발자'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">);</span></span></code></pre></figure>
<p>Next, the parent host component collects the output of its children.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="js" data-theme="github-dark github-light"><code data-language="js" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// div 노드가 자식들의 출력을 수집</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">유저정보_div_fiber.output </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> createDOMElement</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'div'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, {}, [</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  h2_fiber.output,  </span><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// &#x3C;h2>홍길동&#x3C;/h2></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  p_fiber.output    </span><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// &#x3C;p>개발자&#x3C;/p></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">]);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 최상위 div가 모든 자식 출력을 수집</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">프로필_div_fiber.output </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> createDOMElement</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'div'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, {className: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'프로필'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}, [</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  img_fiber.output,           </span><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// &#x3C;img src="profile.jpg" alt="프로필"></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  유저정보_div_fiber.output   </span><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// &#x3C;div>&#x3C;h2>홍길동&#x3C;/h2>&#x3C;p>개발자&#x3C;/p>&#x3C;/div></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">]);</span></span></code></pre></figure>
<p>Finally, user-defined components pass their child's output through unchanged.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="js" data-theme="github-dark github-light"><code data-language="js" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 사용자 정의 컴포넌트는 자식의 출력을 위로 전달</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">아바타_fiber.output </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> img_fiber.output;</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">유저정보_fiber.output </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> 유저정보_div_fiber.output;</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">프로필_fiber.output </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> 프로필_div_fiber.output;</span></span></code></pre></figure>
<h2 id="fiber-scheduling"><a class="anchor" href="#fiber-scheduling">Fiber Scheduling</a></h2>
<p>If Fiber's core value is that it can "divide work," where does that division actually happen? In the <strong>Work Loop</strong>.</p>
<h3 id="work-loop-the-heart-of-fiber-traversal"><a class="anchor" href="#work-loop-the-heart-of-fiber-traversal">Work Loop: The Heart of Fiber Traversal</a></h3>
<p>React rendering begins in the Work Loop defined in <code>ReactFiberWorkLoop.js</code>. React uses one of two Work Loops depending on the situation.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="js" data-theme="github-dark github-light"><code data-language="js" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 동기 렌더링: 중단 없이 모든 Fiber를 처리</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">function</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> workLoopSync</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">() {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  while</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (workInProgress </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">!==</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> null</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) {</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">    performUnitOfWork</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(workInProgress);</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  }</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 동시성 렌더링: 시간 제한 내에서 작업을 나누어 처리</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">function</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> workLoopConcurrent</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">nonIdle</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  if</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (workInProgress </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">!==</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> null</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">    const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> yieldAfter</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> now</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">() </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">+</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (nonIdle </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">?</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 25</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> :</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 5</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">    do</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">      performUnitOfWork</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(workInProgress);</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    } </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">while</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (workInProgress </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">!==</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> null</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> &#x26;&#x26;</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> now</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">() </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">&#x3C;</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> yieldAfter);</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  }</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>Notice the difference between the two functions. <code>workLoopSync</code> runs <strong>unconditionally</strong> until <code>workInProgress</code> becomes <code>null</code>. By contrast, <code>workLoopConcurrent</code> imposes a <strong>time limit</strong> and exits the loop when that limit is exceeded.</p>
<p>The difference between their yield intervals is interesting. <strong>Non-idle work (updates perceptible to the user)</strong>, such as Transition or Retry, yields every <strong>25 ms</strong>, while <strong>idle work (low-priority work that can wait until the user is doing nothing)</strong> yields every <strong>5 ms</strong>. Non-idle work receives 25 ms to intentionally limit animations to roughly 30 fps, preventing transition rendering from starving other work.</p>
<h3 id="performunitofwork"><a class="anchor" href="#performunitofwork">performUnitOfWork</a></h3>
<p><code>performUnitOfWork</code> processes a single Fiber node. The core of Fiber traversal is contained in this function.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="js" data-theme="github-dark github-light"><code data-language="js" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">function</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> performUnitOfWork</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">unitOfWork</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> current</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> unitOfWork.alternate;</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> next</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> beginWork</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(current, unitOfWork, renderLanes);</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  unitOfWork.memoizedProps </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> unitOfWork.pendingProps;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  if</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (next </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">!==</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> null</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) {</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    workInProgress </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> next;</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  } </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">else</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">    completeUnitOfWork</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(unitOfWork);</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  }</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p><code>beginWork</code> processes the current node and returns its first child. It then finalizes <code>pendingProps</code> as <code>memoizedProps</code>; if there is a child, processing moves to that child, and otherwise it calls <code>completeUnitOfWork</code>.</p>
<h3 id="beginwork"><a class="anchor" href="#beginwork">beginWork</a></h3>
<p><code>beginWork</code> traverses Fiber nodes from top to bottom and performs the required computation at each node. It is defined in <code>ReactFiberBeginWork.js</code> and internally branches through a huge <strong>switch statement</strong> based on the Fiber's <code>tag</code>.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="js" data-theme="github-dark github-light"><code data-language="js" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">function</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> beginWork</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">current</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">workInProgress</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">renderLanes</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) {</span></span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">  // bailout 체크: props와 context가 변경되지 않았다면 스킵</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  if</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (current </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">!==</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> null</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">    const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> oldProps</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> current.memoizedProps;</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">    const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> newProps</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> workInProgress.pendingProps;</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">    if</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (oldProps </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">===</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> newProps </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">&#x26;&#x26;</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> !</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">hasContextChanged</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">()) {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">      return</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> bailoutOnAlreadyFinishedWork</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(current, workInProgress, renderLanes);</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  switch</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (workInProgress.tag) {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">    case</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> FunctionComponent:</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">      return</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> updateFunctionComponent</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(current, workInProgress, </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">...</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">    case</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> ClassComponent:</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">      return</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> updateClassComponent</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(current, workInProgress, </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">...</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">    case</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> HostComponent:</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">      return</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> updateHostComponent</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(current, workInProgress, </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">...</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">    case</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> SuspenseComponent:</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">      return</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> updateSuspenseComponent</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(current, workInProgress, </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">...</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">    // ... 약 25가지 이상의 케이스</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  }</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>The key is the <strong>bailout check</strong> at the top. If props and context are the same as before, <code>bailoutOnAlreadyFinishedWork</code> skips the entire subtree. This is one of the most important paths in React's performance optimization.</p>
<p>The return value of <code>beginWork</code> is the <strong>first child Fiber</strong>. If a child exists, it becomes the next <code>workInProgress</code>; if not (<code>null</code>), execution enters <code>completeUnitOfWork</code>.</p>
<h3 id="completework"><a class="anchor" href="#completework">completeWork</a></h3>
<p><code>completeWork</code> starts at a leaf node and finishes work while moving upward toward the parent.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="js" data-theme="github-dark github-light"><code data-language="js" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">function</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> completeUnitOfWork</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">unitOfWork</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  let</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> completedWork </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> unitOfWork;</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  do</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">    // 1. completeWork로 현재 노드의 작업 마무리 (DOM 생성 등)</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">    completeWork</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(current, completedWork, renderLanes);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">    // 2. 형제가 있으면 형제로 이동 (다시 beginWork 시작)</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">    const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> siblingFiber</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> completedWork.sibling;</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">    if</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (siblingFiber </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">!==</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> null</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) {</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">      workInProgress </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> siblingFiber;</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">      return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">    // 3. 형제가 없으면 부모로 올라감</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    completedWork </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> completedWork.return;</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    workInProgress </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> completedWork;</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  } </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">while</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (completedWork </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">!==</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> null</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>The main work performed in <code>completeWork</code> is as follows.</p>
<ul>
<li><strong>For a HostComponent</strong>: It creates the actual DOM node (<code>createInstance</code>) and appends the child DOM nodes. If the DOM already exists, it collects the changed props and stores them in <code>updateQueue</code>.</li>
<li><strong><code>bubbleProperties()</code></strong>: It aggregates the children's flags into <code>subtreeFlags</code>. This information is used to optimize subtree skipping during the Commit Phase.</li>
</ul>
<p>To summarize the traversal: <strong>descend along child (beginWork) -> after completing a leaf, move to sibling -> if there is no sibling, ascend along return (completeWork)</strong>. This is Fiber's depth-first traversal order.</p>
<h3 id="why-react-abandoned-requestidlecallback"><a class="anchor" href="#why-react-abandoned-requestidlecallback">Why React Abandoned requestIdleCallback</a></h3>
<p>Earlier, the Fiber conceptual model showed code using <code>requestIdleCallback</code>, but React does not actually use it. The reasons are clear.</p>
<ul>
<li><strong>It is called too infrequently</strong>: It runs only during truly "idle time" when the browser has nothing else to do, so React work could be delayed indefinitely on a busy page. Dan Abramov has also said, "requestIdleCallback is called too infrequently to be useful for scheduling React work."</li>
<li><strong>Browser compatibility issues</strong>: Safari did not implement it for a long time, and behavior varied between browsers.</li>
<li><strong>A 20 ms cap</strong>: The idle deadline has an upper bound, preventing the predictable degree of timing control React needs.</li>
</ul>
<p>React next tried <code>requestAnimationFrame</code> plus frame-budget estimation, but abandoned that approach as well after deciding that React's work did not need to align with the vsync cycle (the technology that synchronizes frame output to the point at which a monitor completes its vertical refresh).</p>
<h3 id="messagechannel"><a class="anchor" href="#messagechannel">MessageChannel</a></h3>
<p>React ultimately chose <strong>MessageChannel</strong>.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="js" data-theme="github-dark github-light"><code data-language="js" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">if</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">typeof</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> MessageChannel </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">!==</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> 'undefined'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> channel</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> new</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> MessageChannel</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  channel.port1.onmessage </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> performWorkUntilDeadline;</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  schedulePerformWorkUntilDeadline</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> () </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> channel.port2.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">postMessage</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">null</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">} </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">else</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  schedulePerformWorkUntilDeadline</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> () </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> setTimeout</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(performWorkUntilDeadline, </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">0</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>Why not <code>setTimeout</code>, but <code>MessageChannel</code>? Under the HTML specification, <code>setTimeout</code> is forced to wait at least <strong>4 ms</strong> after five or more nested calls. <code>MessageChannel</code>, on the other hand, runs immediately as a macrotask on the next event-loop tick without that restriction. For Fiber, which divides work into 5 ms slices, an artificial 4 ms delay would be devastating.</p>
<p>(If 4 ms out of 5 ms is spent waiting, there is only 1 ms left for actual work. That is not work-life balance; it is just life.)</p>
<p>React's Scheduler package internally manages <strong>two min-heaps</strong>.</p>
<pre><code>timerQueue (대기실)                    taskQueue (실행 대기열)
┌──────────────────┐                  ┌──────────────────┐
│ 아직 시작 시간이     │   startTime      │ 지금 실행 가능한     │
│ 안 된 태스크들       │ ──경과 시──→      │ 태스크들           │
│                  │                  │                  │
│ 정렬: startTime   │                  │ 정렬: expiration  │
│ (빠른 순)          │                  │ Time (임박한 순)   │
└──────────────────┘                  └──────────────────┘
</code></pre>
<p><strong>taskQueue</strong> is the queue of tasks that "can run right now." The smaller the <code>expirationTime</code> (= startTime + timeout)—that is, the closer expiration is—the sooner the task runs. <strong>timerQueue</strong> is the waiting room for tasks whose execution time has not yet arrived. The moment the current time passes startTime, a task moves into taskQueue.</p>
<p>So how is the timeout that determines expirationTime chosen? Each update receives a distinct timeout based on its Priority Level.</p>
<pre><code>우선순위          timeout        만료까지         예시
─────────────────────────────────────────────────────────
Immediate        -1ms          즉시 만료         flushSync
UserBlocking     250ms         0.25초           클릭, 입력
Normal           5,000ms       5초              일반 setState
Low              10,000ms      10초             startTransition
Idle             ~1,073,741,823ms  ~12.4일      오프스크린 렌더링
</code></pre>
<p><strong>Immediate</strong> expires as soon as it is created. It receives top priority the instant it enters taskQueue. (Being born already expired is a rather melancholy fate.) <strong>UserBlocking</strong> uses 250 ms to match the threshold at which people perceive a response as slow (100–300 ms). If nothing happens within a quarter of a second after a click, users become frustrated. <strong>Normal</strong>'s five seconds may seem generous, but it is a guarantee that the task will be processed even in the worst case. In practice, it runs as soon as preceding work finishes. <strong>Idle</strong>'s roughly 12.4 days is effectively infinite. It runs only after all other work is complete. (People rarely leave a browser open for 12 days, so treating it as infinite is reasonable.)</p>
<p>These timeout values also serve as a mechanism for preventing <strong>starvation</strong>. No matter how low the priority, once the timeout passes, the task expires and is forced to run. A constant stream of high-priority work therefore cannot cause low-priority work to be ignored forever.</p>
<p>The Scheduler's <code>shouldYieldToHost()</code> checks whether the time elapsed since work began exceeds <code>frameInterval</code> (by default <strong>5 ms</strong>, defined in <code>SchedulerFeatureFlags.js</code>) and decides whether to return control to the main thread.</p>
<h2 id="render-phase-and-commit-phase"><a class="anchor" href="#render-phase-and-commit-phase">Render Phase and Commit Phase</a></h2>
<p>We have now examined Fiber's structure and scheduling. Let us put the full flow together and see how all these pieces combine to update the actual UI.</p>
<p>Fiber internally passes through two stages: the <strong>Render Phase</strong> and the <strong>Commit Phase</strong>. This separation is the core design that makes React's concurrency model possible. Click the image below to see Fiber's execution flow for yourself.</p>
<p><a href="https://storied-centaur-55230f.netlify.app/" target="_blank" rel="noopener noreferrer"><img src="/content/250520/2.png" alt="2.png" width="2486" height="1778" loading="eager" fetchpriority="high" decoding="async"></a></p>
<h3 id="render-phase"><a class="anchor" href="#render-phase">Render Phase</a></h3>
<p>The Render Phase is the stage that <strong>calculates which UI changes are needed</strong>. It has no actual effect on the DOM. Its most important characteristic is that it can be <strong>paused and resumed asynchronously</strong>.</p>
<p>This stage operates primarily through <code>beginWork</code> and <code>completeWork</code>, which we examined earlier.</p>
<p>In <strong>beginWork(fiber)</strong>, React executes the appropriate logic for each Fiber type (FunctionComponent, ClassComponent, HostComponent, and so on). It then creates and connects child Fiber nodes. If props are the same as before, memoization can be used to skip the work (bailout).</p>
<p>In <strong>completeWork(fiber)</strong>, React prepares DOM creation work or effect information. Through <code>bubbleProperties()</code>, it aggregates the children's flags into <code>subtreeFlags</code> and fills in information while moving upward toward the parent.</p>
<p>Because this phase does not modify the DOM directly, work can be stopped at any time and resumed later without exposing an incomplete UI to the user. This is the foundation of Concurrent Mode.</p>
<h3 id="subtreeflags"><a class="anchor" href="#subtreeflags">subtreeFlags</a></h3>
<p>During the Render Phase, each Fiber records required side effects as <strong>bit flags</strong>. Let us examine the major flags defined in <code>ReactFiberFlags.js</code>.</p>
<ul>
<li><code>Placement</code>: Insert a new node into the DOM</li>
<li><code>Update</code>: A DOM property update is required</li>
<li><code>ChildDeletion</code>: A child node must be deleted</li>
<li><code>Ref</code>: A ref must be attached or detached</li>
<li><code>Passive</code>: A useEffect callback must run</li>
<li><code>Snapshot</code>: Run getSnapshotBeforeUpdate</li>
<li><code>Callback</code>: Run a lifecycle callback</li>
</ul>
<p>Earlier versions of React (through approximately version 16) collected only Fibers with side effects using a linked list connected through <code>firstEffect</code> -> <code>nextEffect</code> -> <code>lastEffect</code>. However, this approach could retain references to unmounted Fibers, causing <strong>memory leaks</strong>, and made it difficult to process new patterns such as Suspense efficiently.</p>
<p>Starting with React 17, this effect list was removed in favor of the <strong>subtreeFlags approach</strong> (<a href="https://github.com/facebook/react/pull/19381" target="_blank" rel="noopener noreferrer">PR #19381</a>). During <code>completeWork</code>, <code>bubbleProperties()</code> aggregates child flags into the parent.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="js" data-theme="github-dark github-light"><code data-language="js" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">function</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> bubbleProperties</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">completedWork</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  let</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> subtreeFlags </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> NoFlags;</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  let</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> child </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> completedWork.child;</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  while</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (child </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">!==</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> null</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) {</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    subtreeFlags </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">|=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> child.subtreeFlags;</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    subtreeFlags </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">|=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> child.flags;</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    child </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> child.sibling;</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  }</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  completedWork.subtreeFlags </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">|=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> subtreeFlags;</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>The greatest advantage of this structure is that the Commit Phase can <strong>skip an entire subtree</strong>. If a Fiber satisfies <code>subtreeFlags &#x26; MutationMask === NoFlags</code>, there are no nodes anywhere in that subtree requiring a DOM change, so the whole subtree can be skipped. This optimization was impossible with the former linked-list approach.</p>
<h3 id="commit-phase"><a class="anchor" href="#commit-phase">Commit Phase</a></h3>
<p>The Commit Phase is the stage that <strong>applies the changes calculated during the Render Phase to the actual DOM</strong>. This phase is <strong>always synchronous</strong>; once it begins, it runs to completion without interruption. This prevents users from seeing a partially updated UI.</p>
<p>Internally, the Commit Phase proceeds in the following detailed order.</p>
<ol>
<li><strong>Before Mutation Phase</strong>: <code>commitBeforeMutationEffects()</code>
<ul>
<li>Reads the current DOM state before changing the DOM. The <code>getSnapshotBeforeUpdate</code> lifecycle runs here. At this point, the <code>current</code> tree still represents the state on screen, so information such as DOM scroll position and dimensions can be captured safely.</li>
</ul>
</li>
<li><strong>Mutation Phase</strong>: <code>commitMutationEffects()</code>
<ul>
<li>This is where <strong>actual DOM manipulation</strong> happens. New nodes are inserted, existing nodes are modified, and unnecessary nodes are removed. <code>componentWillUnmount</code> also runs here because <code>current</code> still points to the previous tree, allowing the previous state to be read.</li>
</ul>
</li>
<li><strong>Tree swap</strong>: <code>root.current = finishedWork</code>
<ul>
<li>This is the essence of double buffering. The workInProgress tree is promoted to the current tree. The reason the swap happens after Mutation but before Layout is important. <code>componentWillUnmount</code> must read the <strong>previous tree</strong>, so it must run during Mutation, while <code>componentDidMount</code>/<code>componentDidUpdate</code> must read the <strong>new tree</strong>, so they must run during Layout.</li>
</ul>
</li>
<li><strong>Layout Phase</strong>: <code>commitLayoutEffects()</code>
<ul>
<li>After DOM changes are complete, tasks based on the new DOM state run.
<ul>
<li>Run <code>componentDidMount</code> and <code>componentDidUpdate</code></li>
<li>Run <code>useLayoutEffect</code> callbacks</li>
<li>At this point, <code>current</code> already points to the new tree, so reading the DOM returns updated values</li>
</ul>
</li>
</ul>
</li>
<li><strong>Passive Effects</strong> (asynchronous)
<ul>
<li><code>useEffect</code> cleanup and setup are scheduled separately and run <strong>asynchronously</strong>. Because they handle side effects that do not depend on DOM changes (such as data fetching and event subscriptions), they do not need to run synchronously. Running them asynchronously yields so the browser can paint the screen first.</li>
</ul>
</li>
</ol>
<h2 id="concurrent-features-and-fiber"><a class="anchor" href="#concurrent-features-and-fiber">Concurrent Features and Fiber</a></h2>
<p>Now let us use the Concurrent Features introduced in React 18 to see what kind of user experience Fiber's entire design—double buffering, Lane-based priority, and an interruptible Work Loop—actually enables.</p>
<h3 id="usetransition"><a class="anchor" href="#usetransition">useTransition</a></h3>
<p>When <code>startTransition(() => setState(...))</code> is called, the update receives a <code>TransitionLane</code>. Fourteen TransitionLanes are assigned in round-robin fashion (one after another in sequence) to prevent collisions.</p>
<p>Because TransitionLane has lower priority than SyncLane or DefaultLane, incoming urgent updates such as user input can <strong>interrupt</strong> transition rendering and be processed first. During that time, the <code>current</code> tree (the previous state) remains on screen while the transition proceeds in the workInProgress tree in the background.</p>
<p>This is where the value of double buffering shines. Interrupted transition rendering affects only the workInProgress tree, leaving the screen the user sees (the current tree) entirely intact.</p>
<p>The <code>isPending</code> flag indicates that the transition is not yet complete, making it possible to display a loading indicator or perform similar handling.</p>
<h3 id="usedeferredvalue"><a class="anchor" href="#usedeferredvalue">useDeferredValue</a></h3>
<p>On the initial render, <code>useDeferredValue(value)</code> returns the supplied <code>value</code> unchanged. On subsequent renders, if the current render is urgent, it returns the previous memoized value and schedules a new render with a TransitionLane. Like a Transition, the deferred render can be interrupted.</p>
<p>Conceptually, it is similar to <code>startTransition</code>, but the difference is that it is applied on the <strong>receiving side of a value</strong>, rather than where an update is dispatched. A typical use case is updating the text in a search field immediately while deferring the rendering of the search results list.</p>
<h3 id="suspense"><a class="anchor" href="#suspense">Suspense</a></h3>
<p>When a component throws a Promise inside <code>&#x3C;Suspense></code>, <code>throwException</code> catches it and marks that Fiber as <code>Incomplete</code>. It then follows the <code>return</code> chain upward to find the nearest Suspense boundary and switches that boundary to display its fallback UI. When the Promise resolves, <code>markRootPinged</code> pings the relevant lane, and React renders the suspended subtree again.</p>
<p>In Concurrent Mode, React can continue rendering the suspended component's <strong>sibling nodes</strong>, so a single data request does not block rendering of the entire tree. This is possible because Fiber's linked-list structure allows free movement to a sibling.</p>
<h3 id="streaming-ssr-and-selective-hydration"><a class="anchor" href="#streaming-ssr-and-selective-hydration">Streaming SSR and Selective Hydration</a></h3>
<p>React 18's <code>renderToPipeableStream</code> uses Suspense boundaries.</p>
<ul>
<li><strong>Server</strong>: When a Suspense boundary suspends, the server sends fallback HTML first and later streams the actual content in a <code>&#x3C;script></code> tag once the data is ready</li>
<li><strong>Client (Selective Hydration)</strong>: Each Suspense boundary can be hydrated <strong>independently</strong>. If the user clicks an area that has not yet been hydrated, React uses <code>SelectiveHydrationLane</code> to hydrate that boundary <strong>first</strong> and then dispatches the event</li>
</ul>
<p>All of this is possible because each Suspense boundary is a Fiber node that can be scheduled independently. Ultimately, the central design of the Fiber architecture—"divide work, assign priorities, and pause/resume"—forms the foundation of these features.</p>
<h2 id="conclusion"><a class="anchor" href="#conclusion">Conclusion</a></h2>
<p>To summarize this article in one sentence, <strong>React Fiber is an architecture that replaces recursion with iteration and moves the call stack into the heap, making rendering interruptible and resumable</strong>.</p>
<p>It combines many sophisticated designs to accomplish this, including a linked-list-based tree structure, double buffering, a Lane-based priority system, and a MessageChannel-based scheduler. All of these ultimately serve one goal: <strong>maximizing the responsiveness of the UI as experienced by the user</strong>.</p>
<p>Of course, Fiber's internal implementation continues to change with each React release, and what this article covers is only a snapshot from a particular point in time. Still, I believe Fiber's core philosophy—"divide work, assign priorities, pause, and resume"—will remain unchanged.</p>
<p>I hope this article has conveyed that React Fiber is not merely an interview keyword but the runtime architecture supporting every React feature. There may be no single correct interpretation, but I also hope readers will inspect the source code themselves and build their own understanding.</p>
<h2 id="sources"><a class="anchor" href="#sources">Sources</a></h2>
<details class="ref-block"><summary class="ref-summary">참고 자료</summary><div class="ref-list">
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#10b981"></span><a href="https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactFiberWorkLoop.js" target="_blank" rel="noopener noreferrer">React source code, ReactFiberWorkLoop.js</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#10b981"></span><a href="https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactFiberBeginWork.js" target="_blank" rel="noopener noreferrer">React source code, ReactFiberBeginWork.js</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#10b981"></span><a href="https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactFiberCompleteWork.js" target="_blank" rel="noopener noreferrer">React source code, ReactFiberCompleteWork.js</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#10b981"></span><a href="https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactFiberLane.js" target="_blank" rel="noopener noreferrer">React source code, ReactFiberLane.js</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#10b981"></span><a href="https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactFiber.js" target="_blank" rel="noopener noreferrer">React source code, ReactFiber.js</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#10b981"></span><a href="https://github.com/facebook/react/blob/main/packages/scheduler/src/forks/Scheduler.js" target="_blank" rel="noopener noreferrer">React source code, Scheduler.js</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#10b981"></span><a href="https://github.com/facebook/react/issues/7942" target="_blank" rel="noopener noreferrer">Issue #7942, Fiber Principles</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#3b82f6"></span><a href="https://github.com/reactwg/react-18/discussions/37" target="_blank" rel="noopener noreferrer">React 18 WG, New Suspense SSR Architecture</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#3b82f6"></span><a href="https://github.com/reactwg/react-18/discussions/27" target="_blank" rel="noopener noreferrer">React 18 WG, Concurrent Scheduling</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#3b82f6"></span><a href="https://react.dev/blog/2022/03/29/react-v18" target="_blank" rel="noopener noreferrer">React v18.0 Blog Post</a></div>
</div>
</div></details>]]></content:encoded>
            <author>jihoon7705@gmail.com (이지훈)</author>
            <category>프론트엔드</category>
            <category>React</category>
        </item>
        <item>
            <title><![CDATA[Can Biome Replace ESLint and Prettier?]]></title>
            <link>https://hooninedev.com/en/241201</link>
            <guid isPermaLink="false">https://hooninedev.com/en/241201</guid>
            <pubDate>Sun, 01 Dec 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[In this post, I want to talk about a tool called Biome. The team I work on had considerable difficulty maintaining a consistent code style in an environment where people used different IDEs, including...]]></description>
            <content:encoded><![CDATA[<p>In this post, I want to talk about a tool called Biome.</p>
<p>The team I work on had considerable difficulty maintaining a consistent code style in an environment where people used different IDEs, including WebStorm and VSCode. Managing separate configuration files for each IDE was cumbersome, and code reviews often filled up with comments about formatting differences unrelated to the actual logic.</p>
<p>When ESLint's formatting-related rules were deprecated, we needed to find a new alternative. The <strong>Prettier + ESLint</strong> combination required additional configuration to prevent conflicts between the tools, while <strong>@stylistic/eslint-plugin-ts</strong> was still in the early stages of community adoption and had not yet been proven stable. That was when Biome caught our attention.</p>
<p>So what exactly is Biome, and can it really replace ESLint and Prettier?</p>
<hr>
<h2 id="what-is-biome"><a class="anchor" href="#what-is-biome">What Is Biome?</a></h2>
<p>Biome is an all-in-one toolchain for web projects. It provides integrated code formatting and linting for JavaScript, TypeScript, JSX, CSS, JSON, GraphQL, and more in a single tool. Its core philosophy is to handle the roles traditionally split between ESLint and Prettier with one binary.</p>
<p>Biome's predecessor was <a href="https://github.com/rome/tools" target="_blank" rel="noopener noreferrer">Rome</a>. <strong>Rome Tools Inc.</strong> launched ambitiously after raising $4.5M in venture funding in 2021, but by mid-2023 its entire staff had been laid off and the repository was archived. Core contributors then forked the project and gave it a fresh start as Biome in August 2023. Moving beyond Rome's image of "overpromising and underdelivering," it has been steadily building trust through practical, consistent releases.</p>
<p>Its most notable characteristic is that it is written in Rust. We will look more closely at the performance difference this creates later.</p>
<hr>
<h2 id="why-use-biome"><a class="anchor" href="#why-use-biome">Why Use Biome?</a></h2>
<p>There are three main reasons to choose Biome.</p>
<p><strong>One tool handles both formatting and linting.</strong> With the ESLint + Prettier combination, additional configuration such as <code>eslint-config-prettier</code> was required to prevent rule conflicts between the two tools. Biome eliminates that complexity at its source.</p>
<p><strong>Its performance is exceptional.</strong> According to official benchmarks, it is roughly 25 times faster than Prettier and about 15 times faster than ESLint. We will compare what those figures look like in practice later.</p>
<p><img src="/content/241201/1.png" alt="1.png" width="2250" height="986" loading="eager" fetchpriority="high" decoding="async"></p>
<p><strong>It is compatible with existing tools.</strong> Biome offers about 97% formatting compatibility with Prettier and includes major ESLint rules out of the box. Rules from commonly used plugins such as <code>eslint-plugin-react-hooks</code> and <code>eslint-plugin-jsx-a11y</code> are built in as well, making migration relatively manageable.</p>
<hr>
<h2 id="how-do-you-use-it"><a class="anchor" href="#how-do-you-use-it">How Do You Use It?</a></h2>
<p>Configuring Biome is quite straightforward. The <a href="https://biomejs.dev/guides/getting-started/" target="_blank" rel="noopener noreferrer">official documentation</a> explains it clearly, so take a look there as well.</p>
<p>First, install Biome.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="bash" data-theme="github-dark github-light"><code data-language="bash" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">npm</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> install</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> --save-dev</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> --save-exact</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> @biomejs/biome</span></span></code></pre></figure>
<p>Then generate a configuration file.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="bash" data-theme="github-dark github-light"><code data-language="bash" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">npx</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> @biomejs/biome</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> init</span></span></code></pre></figure>
<p>This creates a <code>biome.json</code> file. Define your team's formatting and linting rules in that file.</p>
<p>You also need to install an IDE extension. If you use VSCode, install <a href="https://marketplace.visualstudio.com/items?itemName=biomejs.biome" target="_blank" rel="noopener noreferrer">VSCode Biome</a>; if you use WebStorm, install the <a href="https://plugins.jetbrains.com/plugin/22761-biome" target="_blank" rel="noopener noreferrer">WebStorm Biome</a> plugin.</p>
<p>Finally, add the following configuration to VSCode's <code>settings.json</code> to apply formatting and linting automatically whenever you save.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="json" data-theme="github-dark github-light"><code data-language="json" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">  "editor.defaultFormatter"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"biomejs.biome"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">  "editor.codeActionsOnSave"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: {</span></span>
<span data-line=""><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">    "quickfix.biome"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"explicit"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">    "source.organizeImports.biome"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"explicit"</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  }</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span></code></pre></figure>
<hr>
<h2 id="lets-compare-them-directly"><a class="anchor" href="#lets-compare-them-directly">Let's Compare Them Directly</a></h2>
<p>Simply saying it is fast does not make the difference tangible, so I compared Biome and ESLint + Prettier on the same project. Biome is on the left, and ESLint + Prettier is on the right.</p>
<h3 id="local-startup-time-for-a-vite-project"><a class="anchor" href="#local-startup-time-for-a-vite-project">Local Startup Time for a Vite Project</a></h3>
<p><img src="/content/241201/biome1.png" alt="biome1.png" width="798" height="330" loading="lazy" fetchpriority="low" decoding="async">  <img src="/content/241201/lint1.png" alt="lint1.png" width="766" height="248" loading="lazy" fetchpriority="low" decoding="async"></p>
<p>Biome took <strong>506ms</strong>, while ESLint + Prettier took <strong>630ms</strong>, making Biome about 20% faster.</p>
<hr>
<h3 id="build-time-for-a-vite-project"><a class="anchor" href="#build-time-for-a-vite-project">Build Time for a Vite Project</a></h3>
<p><img src="/content/241201/biome2.png" alt="biome2.png" width="920" height="222" loading="lazy" fetchpriority="low" decoding="async"> <img src="/content/241201/lint2.png" alt="lint2.png" width="1022" height="228" loading="lazy" fetchpriority="low" decoding="async"></p>
<p>Biome took <strong>117.13s</strong>, while ESLint + Prettier took <strong>131.48s</strong>, making Biome about 10% faster.</p>
<hr>
<h3 id="linting"><a class="anchor" href="#linting">Linting</a></h3>
<p><img src="/content/241201/biome3.png" alt="biome3.png" width="894" height="174" loading="lazy" fetchpriority="low" decoding="async"> <img src="/content/241201/lint3.png" alt="lint3.png" width="974" height="186" loading="lazy" fetchpriority="low" decoding="async"></p>
<p>The greatest difference appeared during linting. Biome took <strong>0.79s</strong> (CPU 0.470s), while ESLint took <strong>16.32s</strong> (CPU 8.600s), meaning <strong>Biome delivered roughly 20 times faster performance</strong>. Its CPU usage was also much more efficient.</p>
<p>The difference is already noticeable in a development environment, but it becomes even more dramatic when a CI/CD pipeline checks hundreds of files. Because Biome can run its binary directly without an npm installation, it can also reduce CI cold-start time.</p>
<hr>
<p><img src="/content/241201/3.jpeg" alt="3.jpeg" width="265" height="190" loading="lazy" fetchpriority="low" decoding="async"></p>
<p>Hmm... (At this point, it is harder to find a reason not to use it.)</p>
<hr>
<h2 id="why-is-it-so-fast"><a class="anchor" href="#why-is-it-so-fast">Why Is It So Fast?</a></h2>
<p>"It is fast because it was built with Rust" is true, but that alone is not a complete explanation. Let's examine the specific technical factors behind Biome's performance advantage.</p>
<hr>
<h3 id="rusts-low-level-performance"><a class="anchor" href="#rusts-low-level-performance">Rust's Low-Level Performance</a></h3>
<table>
<thead>
<tr>
<th><img src="/content/241201/5.webp" alt="5.webp" width="1200" height="1000" loading="lazy" fetchpriority="low" decoding="async"></th>
<th><img src="/content/241201/6.webp" alt="6.webp" width="1200" height="1000" loading="lazy" fetchpriority="low" decoding="async"></th>
</tr>
</thead>
</table>
<p>Biome is written in Rust, a systems programming language. Rust is designed around zero-cost abstractions, meaning high-level abstractions can deliver the same performance as manually optimized low-level code. It also manages memory through an ownership system without a garbage collector (GC), avoiding the runtime overhead caused by GC.</p>
<p>ESLint and Prettier, by contrast, are written in JavaScript and run on the Node.js runtime. Although the V8 engine's JIT (Just-In-Time) compilation optimizes JavaScript, it cannot completely avoid the fundamental limitations of an interpreted language or the cost of garbage collection.</p>
<hr>
<h3 id="single-parse-architecture"><a class="anchor" href="#single-parse-architecture">Single-Parse Architecture</a></h3>
<p>Biome parses code only once with a single parser to create an AST (Abstract Syntax Tree). It reuses that AST for both formatting and linting.</p>
<p>What happens when you use ESLint + Prettier? ESLint parses the code, creates an AST, and performs linting; then Prettier parses the same code again, creates a separate AST, and performs formatting. The same file is parsed twice. Biome's single-parse architecture eliminates this duplication entirely.</p>
<hr>
<h3 id="native-parallel-processing"><a class="anchor" href="#native-parallel-processing">Native Parallel Processing</a></h3>
<p><img src="/content/241201/7.png" alt="7.png" width="800" height="514" loading="lazy" fetchpriority="low" decoding="async"></p>
<p>Using Rust's concurrency model, Biome processes files in parallel across multiple threads. It divides work into small units and efficiently distributes the load across threads with a work-stealing scheduler. Because Rust's ownership system prevents data races at compile time, runtime synchronization costs are minimized as well.</p>
<p>Node.js uses a single-threaded, event-loop-based model by default. Worker Threads make parallel processing possible, but they add overhead from thread creation and message passing. Biome directly uses native OS-level threads, allowing it to make full use of CPU cores without that overhead.</p>
<hr>
<h3 id="memory-efficient-ast-processing"><a class="anchor" href="#memory-efficient-ast-processing">Memory-Efficient AST Processing</a></h3>
<p><img src="/content/241201/4.svg" alt="4.svg" loading="lazy" fetchpriority="low" decoding="async"></p>
<p>Biome uses a CST (Concrete Syntax Tree). According to Biome's official architecture documentation, this CST implements the Green/Red Tree pattern based on an internal fork of the rowan library, preserving all information from the original code, including comments and whitespace. Rowan's arena-style memory allocation places nodes in contiguous memory regions, improving CPU cache locality and minimizing unnecessary object allocation.</p>
<p>With JavaScript's object-based approach to AST processing, each node exists as an independent heap object, scattering memory and increasing GC pressure. Biome's approach enables faster tree traversal while using less memory.</p>
<hr>
<h2 id="so-should-you-adopt-biome"><a class="anchor" href="#so-should-you-adopt-biome">So, Should You Adopt Biome?</a></h2>
<p>Biome's performance and convenience are clearly appealing. However, I do not think adopting it unconditionally is the right answer for every project. Let's consider a few practical factors.</p>
<hr>
<h3 id="when-biome-is-a-good-fit"><a class="anchor" href="#when-biome-is-a-good-fit">When Biome Is a Good Fit</a></h3>
<ul>
<li>When you maintain a <strong>large codebase</strong> where build and lint performance matter</li>
<li>When you want to reduce code-checking time in a CI/CD pipeline</li>
<li>When you are tired of the complexity of configuring ESLint + Prettier</li>
<li>When you are starting a new project and want a concise tool configuration</li>
</ul>
<p>My team was also maintaining a large-scale project where linting consumed a great deal of time in the CI pipeline, and developers were frustrated by slow linting speeds, so we decided to adopt Biome.</p>
<hr>
<h3 id="points-to-watch"><a class="anchor" href="#points-to-watch">Points to Watch</a></h3>
<p><strong>The limited plugin ecosystem is the biggest concern.</strong> ESLint has thousands of community plugins, while Biome is centered on built-in rules. Many rules from major plugins—including <code>eslint-plugin-react</code>, <code>eslint-plugin-react-hooks</code>, <code>eslint-plugin-jsx-a11y</code>, <code>eslint-plugin-unicorn</code>, and <code>typescript-eslint</code>—are built in, but not every rule from each plugin has been ported. A GritQL-based plugin system has been announced for Biome v2, but it is still experimental. Projects that depend on framework-specific rules such as <code>@next/eslint-plugin-next</code> or <code>eslint-plugin-angular</code> need to approach migration carefully.</p>
<p><strong>You should also verify the scope of language support.</strong> JavaScript, TypeScript, JSX, CSS, JSON, and GraphQL are supported reliably, but Vue and Svelte SFC (Single File Component) files have only partial support for their <code>&#x3C;script></code> blocks. HTML, YAML, and Markdown are not yet supported.</p>
<p><strong>Do not forget that ESLint is evolving too.</strong> Flat Config (<code>eslint.config.js</code>), introduced in ESLint v9 in April 2024, significantly simplified the complexity of the previous <code>.eslintrc</code> approach. ESLint has also expanded linting beyond JavaScript with the releases of <code>@eslint/json</code> in October 2024 and <code>@eslint/css</code> in February 2025. The ESLint Stylistic (<code>@stylistic/eslint-plugin</code>) project provides an option to handle formatting with ESLint alone, without Prettier. In other words, Biome's all-in-one advantage is being somewhat diluted by the evolution of the ESLint ecosystem.</p>
<p>The history of the transition from Rome to Biome is also worth remembering. The disruption experienced by existing users when Rome was archived demonstrates how important a project's sustainability is when choosing a tool. Fortunately, Biome is funded through OpenCollective and GitHub Sponsors and continues to maintain a steady release cadence.</p>
<p><img src="/content/241201/8.png" alt="8.png" width="2722" height="1384" loading="lazy" fetchpriority="low" decoding="async"></p>
<p>According to npm trends, Biome's weekly downloads—about 6.9 million—still lag far behind ESLint's roughly 120 million and Prettier's roughly 82 million. But Biome's growth rate is noteworthy. In just over a year, weekly downloads have increased more than three- to fourfold, with adoption in new projects rising particularly noticeably.</p>
<hr>
<h2 id="closing-thoughts"><a class="anchor" href="#closing-thoughts">Closing Thoughts</a></h2>
<p>My answer to whether Biome can completely replace ESLint and Prettier is <strong>"not yet, but it is a highly credible alternative."</strong></p>
<p>Its performance is exceptional, its configuration is concise, and its development pace is fast. However, the immature plugin ecosystem and limitations in support for certain languages may become obstacles depending on the project. It is best to closely review your project's technology stack and your team's requirements before deciding whether to adopt it.</p>
<p>One thing is certain: the frontend tooling ecosystem is moving toward tools that are "faster, simpler, and more integrated." There is no denying that Biome is at the forefront of that movement. It is clearly a tool whose future growth is worth watching.</p>
<h2 id="references"><a class="anchor" href="#references">References</a></h2>
<details class="ref-block"><summary class="ref-summary">참고 자료</summary></details>]]></content:encoded>
            <author>jihoon7705@gmail.com (이지훈)</author>
            <category>프론트엔드</category>
            <category>자바스크립트</category>
        </item>
        <item>
            <title><![CDATA[Zustand, What Makes You ProviderLess?]]></title>
            <link>https://hooninedev.com/en/240818</link>
            <guid isPermaLink="false">https://hooninedev.com/en/240818</guid>
            <pubDate>Sun, 18 Aug 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[In this post, I want to explore how Zustand manages state without a Provider. While using Zustand, I had always taken it for granted that I could manage state without a Provider. Then a question sudde...]]></description>
            <content:encoded><![CDATA[<p>In this post, I want to explore how Zustand manages state without a Provider.</p>
<p>While using Zustand, I had always taken it for granted that I could manage state without a Provider. Then a question suddenly occurred to me. In most libraries across the React ecosystem, wrapping the app in a Provider has become almost ritualistic. TanStack React Query requires a <code>QueryClientProvider</code> before you can use <code>useQuery</code>, and toss's overlay-kit requires an <code>OverlayProvider</code> before it can call <code>overlay.open()</code>. React's Context API likewise requires the component tree to be wrapped in a Provider. So what kind of magic lets Zustand avoid that entire process?</p>
<p>Out of curiosity, I dug directly into Zustand's source code and found a more interesting structure than I had expected. This post organizes what I learned along the way.</p>
<hr>
<h2 id="how-does-state-flow-in-react"><a class="anchor" href="#how-does-state-flow-in-react">How Does State Flow in React?</a></h2>
<p>In a typical React application, state works as shown below.</p>
<p><img src="/content/240818/3.png" alt="3.png" width="880" height="509" loading="eager" fetchpriority="high" decoding="async"></p>
<p>State inside a component is managed with the state management hooks React provides (<code>useState</code>, <code>useReducer</code>). State is then passed to child components through props. So far, this is straightforward.</p>
<p>The problem arises when state must be shared between components that are far apart. React's official solution is the Context API, which requires wrapping the subtree in a Provider component.</p>
<hr>
<h3 id="why-does-the-context-api-need-a-provider"><a class="anchor" href="#why-does-the-context-api-need-a-provider">Why Does the Context API Need a Provider?</a></h3>
<p>To answer this question, we need to look briefly at React's internals.</p>
<p>React manages the component tree using an internal data structure called Fiber. Each Fiber node is connected through parent-child relationships, and when a Context value changes, React traverses this Fiber tree from top to bottom, finds the components subscribed to that Context, and triggers them to rerender.</p>
<p>The key is this: <strong>Context value propagation depends on the structure of the Fiber tree.</strong> The Provider's position in the tree determines the scope across which its value is delivered, and a component that calls <code>useContext</code> walks up its own Fiber ancestry to find the nearest Provider. What if there is no Provider? It simply uses the default value passed to <code>createContext</code>.</p>
<p>In other words, the Context API is tightly coupled to React's rendering system. State storage, propagation, and subscription all happen inside React's component tree.</p>
<p>How, then, does Zustand bypass this structure?</p>
<hr>
<h2 id="zustand-lives-outside-react"><a class="anchor" href="#zustand-lives-outside-react">Zustand Lives Outside React</a></h2>
<p><img src="/content/240818/4.png" alt="4.png" width="1300" height="393" loading="lazy" fetchpriority="low" decoding="async"></p>
<p>Zustand is based on the Flux pattern. The <code>state</code> inside a closure acts as the Store, user-defined functions act as Actions, the <code>set</code> function acts as the Dispatcher, and React components act as Views. This is where the decisive difference appears.</p>
<p><strong>A Zustand Store exists outside the React component tree, within the scope of a JavaScript module.</strong></p>
<p>Unlike state managed inside React, the phrase "outside the component tree," often used when discussing Zustand, means that the state exists independently of React's Fiber tree. Any component can access the Store simply by using <code>import</code>, with no need to wrap the app in a Provider. (It is accessible from anywhere like a global variable, while still being safely protected inside a closure.)</p>
<p>How is this possible? Consider the code below.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="typescript" data-theme="github-dark github-light"><code data-language="typescript" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { create } </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">from</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> 'zustand'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> useStore</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> create</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">((</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">set</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> ({</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  count: </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">0</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  increment</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: () </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> set</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">((</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">state</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> ({ count: state.count </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">+</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 1</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> })),</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}));</span></span></code></pre></figure>
<p>In this code, <code>create</code> is called when the module is loaded. In other words, the Store already exists in memory before React even begins rendering. This is the <strong>module-level singleton pattern</strong>.</p>
<hr>
<h3 id="what-is-a-module-level-singleton"><a class="anchor" href="#what-is-a-module-level-singleton">What Is a Module-Level Singleton?</a></h3>
<p>JavaScript's ES module system <strong>evaluates a module only once and caches the result</strong>. Any subsequent <code>import</code> of that module returns the same cached object instead of executing the module again. In other words, whether component A or component B uses <code>import { useStore } from './store'</code>, both refer to <strong>the exact same Store instance</strong>.</p>
<p>There is no need to implement a separate singleton class or attach anything to a global variable (<code>window.store</code>). The module system itself naturally satisfies the singleton requirements of "created only once and accessible as the same instance from anywhere." Zustand directly leverages this language-level guarantee, enabling every component to share a single Store without a separate Provider.</p>
<p>At this point, one question naturally follows: what exactly does Zustand look like internally?</p>
<hr>
<h2 id="zustands-internal-structure"><a class="anchor" href="#zustands-internal-structure">Zustand's Internal Structure</a></h2>
<p>Looking through <a href="https://github.com/pmndrs/zustand/tree/main/src" target="_blank" rel="noopener noreferrer">Zustand's GitHub repository</a>, its core logic is surprisingly concise. Two files are central: <code>vanilla.ts</code> contains the Store itself, while <code>react.ts</code> provides the bridge to React.</p>
<hr>
<h3 id="vanillats"><a class="anchor" href="#vanillats">vanilla.ts</a></h3>
<p><a href="https://github.com/pmndrs/zustand/blob/main/src/vanilla.ts" target="_blank" rel="noopener noreferrer">vanilla.ts</a> is the heart of Zustand. Everything about how a Store is created and how its state is managed is contained in this one file. Put more simply, it defines the state enclosed in a closure and the functions that manipulate that state.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="typescript" data-theme="github-dark github-light"><code data-language="typescript" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> createStoreImpl</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> CreateStoreImpl</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">createState</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  type</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> TState</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> ReturnType</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">typeof</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> createState></span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  type</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> Listener</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">state</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> TState</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">prevState</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> TState</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> void</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  let</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> state</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> TState</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> listeners</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> Set</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">Listener</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">> </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> new</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> Set</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">()</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> setState</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> StoreApi</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">TState</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">>[</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'setState'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">] </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">partial</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">replace</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">    const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> nextState</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">      typeof</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> partial </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">===</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> 'function'</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">        ?</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (partial </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">as</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">state</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> TState</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> TState</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">)(state)</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">        :</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> partial</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">    if</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">!</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">Object.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">is</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(nextState, state)) {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">      const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> previousState</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> state</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">      state </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">        (replace </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">??</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">typeof</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> nextState </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">!==</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> 'object'</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> ||</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> nextState </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">===</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> null</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">))</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">          ?</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (nextState </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">as</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> TState</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">          :</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> Object.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">assign</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({}, state, nextState)</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">      listeners.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">forEach</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">((</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">listener</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> listener</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(state, previousState))</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> getState</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> StoreApi</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">TState</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">>[</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'getState'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">] </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> () </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> state</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> getInitialState</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> StoreApi</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">TState</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">>[</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'getInitialState'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">] </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> () </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    initialState</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> subscribe</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> StoreApi</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">TState</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">>[</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'subscribe'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">] </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">listener</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    listeners.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">add</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(listener)</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">    return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> () </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> listeners.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">delete</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(listener)</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> api</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { setState, getState, getInitialState, subscribe }</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> initialState</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (state </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> createState</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(setState, getState, api))</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> api </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">as</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> any</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>Breaking down this code line by line reveals Zustand's core mechanisms.</p>
<ul>
<li>
<p><strong>State encapsulation through a closure</strong></p>
<ul>
<li>
<p>The variable <code>let state: TState</code> is declared locally inside <code>createStoreImpl</code>. Even after the function finishes executing, internal functions such as <code>setState</code> and <code>getState</code> continue to reference it, so it is not garbage-collected. This is the essence of a closure.</p>
</li>
<li>
<p>External code has no way to access the <code>state</code> variable directly. It can only read it through <code>getState()</code> and write it through <code>setState()</code>. (This is effectively an object-oriented private field implemented with a closure.)</p>
</li>
</ul>
</li>
<li>
<p><strong>Change detection with <code>Object.is</code></strong></p>
<ul>
<li>
<p>After calculating the next state, <code>setState</code> compares it with the existing state using <code>Object.is(nextState, state)</code>. If the references are identical, nothing happens. This is the first line of defense against unnecessary rerenders.</p>
</li>
<li>
<p>However, because this <code>Object.is</code> comparison performs a <strong>strict referential equality</strong> check, consumers need to be careful in one particular area. There is no issue when selecting a single primitive value, such as a number or string.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="typescript" data-theme="github-dark github-light"><code data-language="typescript" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> count</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> useStore</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">((</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">state</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> state.count);</span></span></code></pre></figure>
<p>But the situation changes when a selector <strong>returns a new object</strong>.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="typescript" data-theme="github-dark github-light"><code data-language="typescript" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">count</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">name</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> } </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> useStore</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">((</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">state</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> ({</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  count: state.count,</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  name: state.name,</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}));</span></span></code></pre></figure>
<p>Even when the values are identical, the <code>{ count, name }</code> object receives a new reference on every call. Because <code>Object.is</code> compares only the reference rather than the object's properties, Zustand considers the state "changed" and triggers a rerender every time.</p>
<p>To solve this problem, Zustand provides the <strong><code>useShallow</code></strong> hook.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="typescript" data-theme="github-dark github-light"><code data-language="typescript" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { useShallow } </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">from</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> 'zustand/react/shallow'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">count</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">name</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> } </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> useStore</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  useShallow</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">((</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">state</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> ({ count: state.count, name: state.name }))</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">);</span></span></code></pre></figure>
<p><code>useShallow</code> compares the returned object's <strong>top-level properties one by one</strong>, triggering a rerender only when a value actually changes. This is similar to how Redux's <code>useSelector</code> uses reference comparison by default but accepts <code>shallowEqual</code> as its second argument. (As its name suggests, however, <code>useShallow</code> performs a "shallow" comparison, so remember that it does not track changes inside nested objects.)</p>
</li>
</ul>
</li>
<li>
<p><strong>A Pub/Sub listener system</strong></p>
<ul>
<li>The single line <code>const listeners: Set&#x3C;Listener> = new Set()</code> constitutes Zustand's entire subscription system. When state changes, <code>listeners.forEach</code> notifies every subscriber.</li>
<li>Calling <code>subscribe</code> adds a listener to the <code>Set</code>, and calling the returned function removes it from the <code>Set</code>.</li>
<li>This pattern matters because it is <strong>a notification system completely independent of React's Fiber tree</strong>. Rather than having a Provider traverse the tree to find subscribers, the Store manages its subscriber list directly.</li>
</ul>
</li>
<li>
<p><strong>Creating the initial state</strong></p>
<ul>
<li>
<p>Consider the final line that handles the initial state.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="typescript" data-theme="github-dark github-light"><code data-language="typescript" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> initialState</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (state </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> createState</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(setState, getState, api))</span></span></code></pre></figure>
<p>A great deal is compressed into this one line. In JavaScript, the assignment operator (<code>=</code>) is an expression that <strong>returns the assigned value itself</strong>. This means <code>state = createState(...)</code> inside the parentheses runs first and assigns the initial state to <code>state</code>, after which its return value is assigned again to <code>const initialState</code>. As a result, <code>state</code> and <code>initialState</code> <strong>refer to the same object</strong>.</p>
<p>Why store the same value in two separate variables? The key is that the variables have different roles.</p>
<ul>
<li><strong><code>state</code></strong> is declared with <code>let</code>. It is replaced with a new value every time <code>setState</code> is called. In other words, it represents <strong>the live state at the current point in time</strong>.</li>
<li><strong><code>initialState</code></strong> is declared with <code>const</code>. It permanently preserves the state from the moment the Store was created. No later call to <code>setState</code> changes this value. It is <strong>the Store's original snapshot</strong>.</li>
</ul>
<p>This <code>initialState</code> is exposed through the <code>getInitialState()</code> method and passed in <code>react.ts</code> as the <strong>third argument (the server snapshot)</strong> to <code>useSyncExternalStore</code>.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="typescript" data-theme="github-dark github-light"><code data-language="typescript" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> slice</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> React.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">useSyncExternalStore</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  api.subscribe,</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  () </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> selector</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(api.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">getState</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">()),       </span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  () </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> selector</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(api.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">getInitialState</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">()), </span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">)</span></span></code></pre></figure>
<p>In a server-side rendering (SSR) environment, browser APIs are unavailable and there is no user interaction, so <code>setState</code> is never called. The server therefore always uses <code>initialState</code> (= the original state) as its snapshot. When hydration begins on the client, React compares the HTML rendered on the server with the client's initial rendering result. Because both sides rendered from the same <code>initialState</code>, this <strong>prevents a hydration mismatch</strong>.</p>
</li>
</ul>
</li>
</ul>
<hr>
<h3 id="reactts"><a class="anchor" href="#reactts">react.ts</a></h3>
<p><a href="https://github.com/pmndrs/zustand/blob/main/src/react.ts" target="_blank" rel="noopener noreferrer">react.ts</a> connects the pure JavaScript Store created above to React's rendering system.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="typescript" data-theme="github-dark github-light"><code data-language="typescript" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">export</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> function</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> useStore</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">TState</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">StateSlice</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">>(</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  api</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> ReadonlyStoreApi</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">TState</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">>,</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  selector</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">state</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> TState</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> StateSlice</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> identity </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">as</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> any</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> slice</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> React.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">useSyncExternalStore</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    api.subscribe,</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    React.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">useCallback</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(() </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> selector</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(api.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">getState</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">()), [api, selector]),</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    React.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">useCallback</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(() </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> selector</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(api.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">getInitialState</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">()), [api, selector]),</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  )</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  React.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">useDebugValue</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(slice)</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> slice</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>The key here is <code>useSyncExternalStore</code>. Introduced in React 18, this hook was designed to <strong>safely integrate a state store that exists outside React with React's rendering cycle</strong>.</p>
<p>Its structure becomes clear when we examine the three arguments accepted by <code>useSyncExternalStore</code>. (They are almost identical to what we covered earlier in vanilla.ts.)</p>
<ul>
<li><strong><code>api.subscribe</code></strong>: The function that subscribes to Store changes. Through this function, React asks to be notified when the state changes.</li>
<li><strong><code>() => selector(api.getState())</code></strong>: Returns a snapshot of the current state. React calls this function on every render to retrieve the latest state.</li>
<li><strong><code>() => selector(api.getInitialState())</code></strong>: The initial snapshot used during server-side rendering. It prevents server and client state from diverging during hydration.</li>
</ul>
<p>In particular, <code>useSyncExternalStore</code> solves the <strong>tearing problem</strong> that can arise in React's Concurrent Mode. Tearing occurs when different components show <strong>different snapshots of the same data source</strong> within a single render pass.</p>
<p>A concrete scenario makes this easier to understand. Component A reads <code>store.value</code> (= 10) and begins rendering. In Concurrent Mode, React then <strong>yields</strong>, pausing the render and returning control to the browser. In the meantime, a WebSocket message arrives and changes <code>store.value</code> to 11. When React resumes rendering, component B reads <code>store.value</code> (= 11). The result is a <strong>teared UI</strong> in which A displays 10 and B displays 11 in the same frame. Before React 18, rendering was always synchronous, so this problem did not occur.</p>
<p><code>useSyncExternalStore</code> records the snapshot (<code>getSnapshot</code>) from the start of the render. If the external Store changes during rendering and the snapshot no longer matches, it detects the change and <strong>restarts the render from the beginning</strong>. This guarantees that every component renders from the same snapshot.</p>
<p>The <code>createImpl</code> function ties all of this together.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="typescript" data-theme="github-dark github-light"><code data-language="typescript" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> createImpl</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> &#x3C;</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">T</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">>(</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">createState</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> StateCreator</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">T</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, [], []>) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> api</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> createStore</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(createState)</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> useBoundStore</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> any</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">selector</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">?:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> any</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> useStore</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(api, selector)</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  Object.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">assign</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(useBoundStore, api)</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> useBoundStore</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>It creates a vanilla Store with <code>createStore</code>, wraps it in a custom hook called <code>useBoundStore</code>, and then uses <code>Object.assign</code> to attach the Store API methods (<code>setState</code>, <code>getState</code>, <code>subscribe</code>, and so on) directly to the hook function. The returned <code>useBoundStore</code> consequently has a dual nature: it is <strong>both a React hook and the Store API</strong>. (A function that also has methods is a distinctly JavaScript-like pattern.)</p>
<hr>
<h2 id="what-about-other-state-management-libraries"><a class="anchor" href="#what-about-other-state-management-libraries">What About Other State Management Libraries?</a></h2>
<p>Now that we understand this much, it is natural to compare Zustand with other libraries.</p>
<p>There are many state management libraries, including Jotai, Recoil, MobX, Xstate, and Redux, but I will focus on the ones I have personally used.</p>
<blockquote>
<p>For reference, <strong>Recoil</strong> (Meta), which was often compared with Jotai, was effectively discontinued when its repository was archived in January 2025. It also never received React 19 support. If you want an atomic state model, Jotai is arguably the only practical choice today.</p>
</blockquote>
<hr>
<h3 id="redux"><a class="anchor" href="#redux">Redux</a></h3>
<p>Redux also uses a module-level Store internally. Why, then, does it need a Provider?</p>
<p>Redux's <code>&#x3C;Provider store={store}></code> <strong>injects</strong> the Store instance into the component tree through React Context. Internally, <code>useSelector</code> and <code>useDispatch</code> call <code>useContext</code> to access the Store supplied by the Provider. The important point is that Redux uses Context <strong>not as a state propagation channel, but as a means of Dependency Injection</strong>. What Context carries is not the state value itself, but a <strong>reference to the Store object</strong> that manages the state. Actual state subscriptions and updates are handled by the Store's internal Pub/Sub system.</p>
<p>The benefits of this design are clear. Tests can be completely isolated by wrapping them in a Provider with a different Store instance, and a single app can use the <code>context</code> prop to construct multiple independent Store trees. As Mark Erikson, a Redux maintainer, emphasizes, "Context is a transport mechanism, not a state management tool."</p>
<hr>
<h3 id="jotai"><a class="anchor" href="#jotai">Jotai</a></h3>
<p>Jotai adopts an <strong>atomic state model</strong> that is fundamentally different from Redux or Zustand. Rather than collecting state in one large Store object, it <strong>separates each piece of state into an independent atom</strong>. (Jotai's official documentation likewise explains that "Zustand is similar to Redux, while Jotai is similar to Recoil.")</p>
<p>The central difference in this structure is <strong>how rendering is optimized</strong>. Zustand takes a <strong>top-down</strong> approach, extracting only the required portion from a single Store through a selector. Developers must write selectors themselves, as in <code>useStore((state) => state.count)</code>, and memoization is sometimes necessary to preserve referential equality. Jotai, by contrast, automatically builds a <strong>dependency graph</strong> among atoms and performs <strong>bottom-up</strong> propagation: when a particular atom changes, only the components that depend on that atom rerender. This automatic dependency tracking is especially powerful when dozens of pieces of state are intertwined, as in a spreadsheet or canvas editor.</p>
<p>From a Provider perspective, Jotai occupies an interesting middle ground. It uses a global Store by default and works without a Provider, but it can also be wrapped in <code>&#x3C;Provider></code> to create an isolated Store scope when needed. Borrowing the wording of Jotai's official documentation, Jotai is <strong>"context first, module second,"</strong> whereas Zustand is <strong>"module first, context second."</strong></p>
<hr>
<h3 id="zustands-choice"><a class="anchor" href="#zustands-choice">Zustand's Choice</a></h3>
<p>Zustand made the most radical choice. By default, it is a module-level singleton and has no Provider at all. What this choice delivers is an <strong>extremely simple API</strong>: create a Store with <code>create</code>, call the hook from a component, and you are done.</p>
<p>Strictly speaking, however, "there is no Provider at all" describes only the <strong>default design</strong>. Since v4, you can combine <code>createStore</code> (a vanilla Store) with React's <code>createContext</code> to implement the <strong>Scoped Store</strong> pattern.</p>
<p><a href="https://tkdodo.eu/blog/zustand-and-react-context" target="_blank" rel="noopener noreferrer">TkDodo's blog (the React Query maintainer)</a> explores this pattern in depth. His central argument is that a global singleton Store has three limitations.</p>
<ul>
<li><strong>It cannot be initialized from props</strong>: Because the Store is created when the module loads, there is no way to use server-provided data or a parent component's props as initial values.</li>
<li><strong>Test isolation is difficult</strong>: The Store must be reset manually for every test.</li>
<li><strong>It cannot be reused</strong>: If two components that need Stores with the same structure are rendered on one page, they end up sharing state.</li>
</ul>
<p>The Scoped Store pattern solves all three problems. Its central idea is to pass <strong>a reference to the Store instance through Context, rather than the state value</strong>. (This is exactly the same structure used by Redux's Provider.)</p>
<p>Here is a concrete implementation.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="typescript" data-theme="github-dark github-light"><code data-language="typescript" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { createStore, useStore } </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">from</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> 'zustand'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { createContext, useContext, useState } </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">from</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> 'react'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 1. 스토어 팩토리 함수 — props를 받아 스토어를 생성</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> createSelectionStore</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">initialItems</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">[]) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  createStore</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">SelectionState</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">>((</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">set</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> ({</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    items: initialItems,</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    selected: </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">new</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> Set</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">string</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">>(),</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">    toggle</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: (</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">id</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">      set</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">((</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">state</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">        const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> next</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> new</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> Set</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(state.selected);</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">        next.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">has</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(id) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">?</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> next.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">delete</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(id) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> next.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">add</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(id);</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">        return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { selected: next };</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">      }),</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  }));</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 2. Context 생성</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">type</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> SelectionStore</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> ReturnType</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">typeof</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> createSelectionStore>;</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> SelectionContext</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> createContext</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">SelectionStore</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> |</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> null</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">>(</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">null</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 3. Provider — useState로 스토어를 한 번만 생성</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> SelectionProvider</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> ({</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  children,</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  initialItems,</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  children</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> React</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">ReactNode</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  initialItems</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">[];</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> [</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">store</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">] </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> useState</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(() </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> createSelectionStore</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(initialItems));</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">    &#x3C;</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">SelectionContext.Provider value</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{store}</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">      {</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">children</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">    &#x3C;/</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">SelectionContext.Provider</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  );</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">};</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 4. 커스텀 훅 — Context에서 스토어를 꺼내 useStore로 구독</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> useSelectionStore</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> &#x3C;</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">T</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,>(</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">selector</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">state</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> SelectionState</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> T</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> store</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> useContext</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(SelectionContext);</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  if</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">!</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">store) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">throw</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> new</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> Error</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'SelectionProvider가 필요합니다'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  return</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> useStore</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(store, selector);</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">};</span></span></code></pre></figure>
<p>You can now render as many independent multi-select components as you want on the same page.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 각 SelectionProvider가 자신만의 스토어 인스턴스를 가진다</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">SelectionProvider</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> initialItems</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{[</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'A'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'B'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'C'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">]}></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">MultiSelect</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> /></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">SelectionProvider</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">SelectionProvider</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> initialItems</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{[</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'X'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'Y'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'Z'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">]}></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">MultiSelect</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> />  {</span><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">/* 위 컴포넌트와 상태가 완전히 독립 */</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">SelectionProvider</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span></code></pre></figure>
<p>The point to notice is that Context carries <strong>the Store object, not the state value</strong>. Because the Context <code>value</code> (= the Store reference) does not change when the state value changes, <strong>changes to the Context value do not cause unnecessary rerenders.</strong> For actual rerendering, <code>useStore</code> uses its internal <code>useSyncExternalStore</code> with the selector. Context's transport role and Zustand's subscription role remain cleanly separated.</p>
<p>TkDodo described a real-world application of this pattern in a design system's multi-select component. The previous architecture, which managed internal state with <code>useState</code> + Context, suffered performance degradation with more than 50 items; switching to Zustand's selector-based subscription solved the problem.</p>
<p>After v4 removed <code>zustand/context</code> and its <code>createContext</code> helper from v3, this pattern settled on <strong>directly combining React's native <code>createContext</code> with Zustand's <code>createStore</code>/<code>useStore</code></strong>. The API remains unchanged in v5, and <a href="https://github.com/pmndrs/zustand/blob/main/docs/previous-versions/zustand-v3-create-context.md" target="_blank" rel="noopener noreferrer">Zustand's official documentation</a> presents this pattern in its v4+ migration guide.</p>
<hr>
<h2 id="the-shadow-side-of-providerless"><a class="anchor" href="#the-shadow-side-of-providerless">The Shadow Side of ProviderLess</a></h2>
<p>Of course, having no Provider does not bring only advantages. Here are the points that I believe require caution.</p>
<hr>
<h3 id="state-sharing-problems-in-ssr"><a class="anchor" href="#state-sharing-problems-in-ssr">State Sharing Problems in SSR</a></h3>
<p>A module-level singleton can be dangerous in a server environment. A Node.js server handles multiple requests in a single process, while a module is loaded only once within that process. This means that requests from different users may <strong>share the same Store instance</strong>.</p>
<p>This is why Zustand provides <code>getInitialState</code> and passes the server snapshot as the third argument to <code>useSyncExternalStore</code>. Even so, this alone may not fully isolate state between requests. In SSR environments, the recommended approach is therefore to use the Scoped Store pattern mentioned earlier (<code>createStore</code> + React Context) and create a new Store for each request.</p>
<hr>
<h3 id="the-difficulty-of-test-isolation"><a class="anchor" href="#the-difficulty-of-test-isolation">The Difficulty of Test Isolation</a></h3>
<p>With a Provider-based library, wrapping each test in a different Provider naturally isolates the Store. Zustand's module-level singleton, on the other hand, can leak state between tests. You must explicitly reset the Store in each test's <code>beforeEach</code>. (I once struggled with this issue myself.)</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="typescript" data-theme="github-dark github-light"><code data-language="typescript" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 테스트 파일에서의 스토어 리셋 예시</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">beforeEach</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(() </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  useStore.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">setState</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(useStore.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">getInitialState</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">());</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">});</span></span></code></pre></figure>
<p>The Scoped Store pattern is also a solution here. When using a Provider, each test can create and inject a new Store, providing complete isolation without reset logic.</p>
<hr>
<h3 id="the-lack-of-multiple-instances"><a class="anchor" href="#the-lack-of-multiple-instances">The Lack of Multiple Instances</a></h3>
<p>If one application needs two independent Stores with the same structure, the Provider pattern can simply wrap them in separate Providers. With a module-level singleton, however, the Store creation function must be called separately to create distinct Store instances. For example, if one page contains two independent tab panels and each must manage its selection state separately, a global singleton cannot express that arrangement naturally.</p>
<p>Once again, the <code>createStore</code> + Context pattern is the answer. When each tab panel component renders its own Provider, it creates a completely independent instance with the same Store structure. Zustand's official documentation also recommends this pattern "when a reusable component needs a Store."</p>
<h2 id="conclusion"><a class="anchor" href="#conclusion">Conclusion</a></h2>
<p>To summarize what we have examined, Zustand's ProviderLess design is made possible by the combination of four mechanisms.</p>
<ul>
<li><strong>Module-level singleton</strong>: The Store is created outside the React component tree, within the scope of a JavaScript module.</li>
<li><strong>State encapsulation through a closure</strong>: In <code>vanilla.ts</code>, <code>createStoreImpl</code> encloses the <code>state</code> variable and <code>listeners</code> Set in a closure, preventing external access.</li>
<li><strong>Its own Pub/Sub system</strong>: Instead of traversing the Fiber tree, it directly manages a <code>Set&#x3C;Listener></code> to notify subscribers of state changes.</li>
<li><strong>React integration through <code>useSyncExternalStore</code></strong>: It safely synchronizes changes in the external Store with React's rendering cycle.</li>
</ul>
<p>Ultimately, Zustand asks this question: "Does state really have to live inside React?" Zustand's answer is clear. State can remain outside React, with a bridge added only when needed. That bridge is <code>useSyncExternalStore</code>.</p>
<p>Of course, this approach is not the best choice in every situation. A Provider-based design may be more appropriate for SSR, test isolation, or multiple instances. There is no single correct answer, but understanding the design trade-offs each library has chosen helps you select the right tool for the situation.</p>
<p>I also encourage readers to open the source code of a library they use at least once. You may discover a depth that the official documentation does not reveal.</p>
<hr>
<p><img src="/content/240818/7.jpeg" alt="7.jpeg" width="216" height="233" loading="lazy" fetchpriority="low" decoding="async"></p>
<h3 id="one-more-piece-of-news"><a class="anchor" href="#one-more-piece-of-news">One More Piece of News</a></h3>
<p>While researching the material above, I learned that <strong>Zustand v5.0.0 was officially released in October 2024</strong>.</p>
<p>Interestingly, v5 contains almost no new features. New features had already been added throughout v4.x while existing APIs were deprecated, so v5 is primarily a <strong>cleanup release</strong>. The major changes are listed below. (For details, see the <strong><a href="https://github.com/pmndrs/zustand/releases" target="_blank" rel="noopener noreferrer">release page</a></strong> and the <strong><a href="https://zustand.docs.pmnd.rs/reference/migrations/migrating-to-v5" target="_blank" rel="noopener noreferrer">migration guide</a></strong>.)</p>
<ul>
<li>The minimum requirements were raised to <strong>React 18 and TypeScript 4.5 or later</strong>.</li>
<li><strong><code>getServerState</code> was removed</strong>. (It was replaced by the third argument to <code>useSyncExternalStore</code>.)</li>
<li><strong>ES5 support was dropped</strong>.</li>
<li>Support for specifying <strong>a custom equality function was removed</strong> from the <code>create</code> function.</li>
<li>The <strong><code>shallow</code> function was improved</strong> to support iterable objects.</li>
</ul>
<p>When migrating from v4 to v5, the recommended approach is to update to the latest v4 release first. The latest v4 release displays deprecation warnings, so addressing those warnings before upgrading to v5 makes the transition straightforward.</p>
<hr>
<h3 id="references"><a class="anchor" href="#references">References</a></h3>
<details class="ref-block"><summary class="ref-summary">참고 자료</summary><div class="ref-list">
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#3b82f6"></span><a href="https://react.dev/reference/react/useSyncExternalStore" target="_blank" rel="noopener noreferrer">React useSyncExternalStore</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#3b82f6"></span><a href="https://jotai.org/docs/basics/comparison" target="_blank" rel="noopener noreferrer">Jotai Comparison</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#f59e0b"></span><a href="https://interbolt.org/blog/react-ui-tearing/" target="_blank" rel="noopener noreferrer">InterBolt, Concurrent React, External Stores, and Tearing</a></div>
</div>
</div></details>]]></content:encoded>
            <author>jihoon7705@gmail.com (이지훈)</author>
            <category>프론트엔드</category>
            <category>React</category>
        </item>
        <item>
            <title><![CDATA[Understanding Compression Algorithms]]></title>
            <link>https://hooninedev.com/en/240706</link>
            <guid isPermaLink="false">https://hooninedev.com/en/240706</guid>
            <pubDate>Sat, 06 Jul 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[In this post, I want to talk about software compression algorithms. I was asked to improve the deployment process for an internal project. Its large build artifacts had to be uploaded to S3, and I qui...]]></description>
            <content:encoded><![CDATA[<p>In this post, I want to talk about software compression algorithms.</p>
<p>I was asked to improve the deployment process for an internal project. Its large build artifacts had to be uploaded to S3, and I quickly felt how directly the size of the build directory affected both upload time and storage costs. That naturally led to a question: how could we compress and upload these artifacts more efficiently?</p>
<p>Once I started researching compression, I found far more options than I expected: zip, gzip, zstd, bzip2, xz, and others. Their names looked similar, but it was surprisingly difficult to find a clear explanation of how they differed and when each one should be used. (I had assumed compression was more or less all the same, but the world is large and so is the number of ways to make files smaller.)</p>
<p>This seemed like a good opportunity to compare the principles and characteristics of the major formats and explain why I ultimately chose one of them.</p>
<hr>
<h2 id="what-is-lossless-compression"><a class="anchor" href="#what-is-lossless-compression">What is lossless compression?</a></h2>
<p>Lossless compression is a method that lets us restore the original data perfectly. Unlike lossy compression, which is common for images and audio, decompressed data does not differ from the original by even a single bit. Source code and build artifacts require lossless compression because their integrity must be preserved.</p>
<p>The central idea behind lossless compression is <strong>taking advantage of statistical redundancy in data</strong>. Replacing repeated patterns with shorter representations reduces the total size.</p>
<p>Among these techniques, <strong>dictionary-based compression</strong> is one of the most widely used families of lossless algorithms. Here, a dictionary is not a book of word definitions. It is a lookup table that maps previously seen pieces of data to short codes. <strong>LZ77</strong>, introduced by Abraham Lempel and Jacob Ziv in their 1977 paper <em>"A Universal Algorithm for Sequential Data Compression"</em> in IEEE Transactions on Information Theory, and <strong>LZ78</strong>, published the following year, are the ancestors of this family. The letters “LZ” come from the researchers’ surnames. Nearly every dictionary-based compression algorithm that followed, including DEFLATE, LZMA, LZ4, and Zstd, can trace its roots back to these two. (It is not much of an exaggeration to say that most compression family trees converge on Lempel and Ziv.)</p>
<p>Here is a simple example. If the word “Linux” appears 100 times in a text, the compressor can register it in a dictionary the first time and replace later occurrences with a short pointer meaning “dictionary entry number 1.” “Linux” takes five bytes, while the pointer can often be represented with fewer bytes, making the whole document smaller.</p>
<p>So how exactly do LZ77 and LZ78 differ?</p>
<hr>
<h3 id="lz77-the-sliding-window-approach"><a class="anchor" href="#lz77-the-sliding-window-approach">LZ77: the sliding-window approach</a></h3>
<p>LZ77 does <strong>not build a separate explicit dictionary</strong>. Instead, it treats a region of the input stream itself as a dictionary. This region is called a <strong>sliding window</strong> because it moves forward as the input is processed. (It is the same term that appears so often in algorithm exercises.)</p>
<p>The window has two regions.</p>
<ul>
<li><strong>Search buffer</strong>: data that has already been processed. It acts as the dictionary.</li>
<li><strong>Look-ahead buffer</strong>: data that has not yet been processed and is about to be compressed.</li>
</ul>
<p>The algorithm checks whether the beginning of the look-ahead buffer has appeared somewhere in the search buffer. When it finds the same pattern, it encodes the match as a <strong>(distance, length, next character)</strong> tuple. Distance tells the decoder how far back to find the start of the match, while length says how many characters the match contains.</p>
<p>Suppose we compress the string <code>"banana_banana"</code> with LZ77. When the algorithm reaches the second <code>"banana"</code>, it is effectively saying, <em>“Go back seven characters and copy the next six.”</em> A six-byte string can therefore be represented by only two numbers.</p>
<p>The key advantage is that <strong>the dictionary does not need to be stored or transmitted separately</strong>. The decoder naturally reconstructs the search buffer while decompressing, so the dictionary is implicitly embedded in the data itself. The trade-off is that decompression must proceed sequentially from the beginning. In principle, it cannot start at an arbitrary point in the middle.</p>
<p>Window size has a direct trade-off with compression ratio. A larger window can refer to patterns farther away and therefore often compress better, but it also increases the computation required for match searching and uses more memory.</p>
<hr>
<h3 id="lz78-an-explicit-dictionary"><a class="anchor" href="#lz78-an-explicit-dictionary">LZ78: an explicit dictionary</a></h3>
<p>Unlike LZ77, LZ78 <strong>constructs an explicit dictionary</strong> as it compresses the input. There is no sliding window. Previously observed patterns are stored as indexed dictionary entries, and later occurrences are replaced by their indexes.</p>
<p>LZ78 outputs tags in the form <strong>(dictionary index, next character)</strong>. The encoder finds the longest matching dictionary entry, outputs its index together with the next character that breaks the match, and then adds <em>“the matched entry plus the new character”</em> to the dictionary. The dictionary grows incrementally as the input is processed.</p>
<p>The best-known variation of LZ78 is <strong>LZW</strong> (Lempel-Ziv-Welch). Terry Welch published the improvement in 1984, and it was used by the GIF image format and the Unix <code>compress</code> utility with its <code>.Z</code> extension. (LZW was once at the center of a patent dispute, an episode that helped motivate the creation of PNG.)</p>
<hr>
<h3 id="which-family-do-modern-compression-algorithms-belong-to"><a class="anchor" href="#which-family-do-modern-compression-algorithms-belong-to">Which family do modern compression algorithms belong to?</a></h3>
<p>Interestingly, nearly every mainstream compression algorithm we use today is a <strong>descendant of LZ77</strong>.</p>
<p><strong>LZSS</strong>, published by Storer and Szymanski in 1982, improved on LZ77 by adding a one-bit flag to distinguish a literal original character from a length-distance pair. If a match was so short that referencing it would cost more, the encoder could simply output the original character.</p>
<p>In 1993, Phil Katz combined LZSS with <strong>Huffman coding</strong>, an entropy-coding technique that gives shorter bit sequences to more frequent symbols, to create <strong>DEFLATE</strong>. ZIP, GZIP, and PNG all use DEFLATE. In other words, the <code>.zip</code>, <code>.gz</code>, and <code>.png</code> files we handle every day are direct descendants of LZ77.</p>
<p>Later algorithms such as <strong>LZMA</strong> (used by 7-Zip and XZ), <strong>LZ4</strong>, and <strong>Zstd</strong> also begin with LZ77’s sliding-window idea and evolve the data structures for match searching and the methods used for entropy coding. The LZ78 family, by contrast, largely left the mainstream stage after LZW.</p>
<p>The two algorithms have been proven theoretically equivalent in capability <em>when the entire dataset is decompressed</em>. LZ77 nevertheless survived because <strong>embedding the dictionary in the data made the design more flexible to implement and extend</strong>. Window size, match-search algorithms, and entropy coders could be combined freely, giving the family room to evolve as requirements changed.</p>
<p>Compression performance is usually evaluated on two axes: <strong>compression ratio</strong>, or how small the result becomes, and <strong>compression speed</strong>, or how quickly the operation completes. Seeking a higher ratio generally requires more computation and therefore more time. A practical compression strategy is about finding the right point between the two.</p>
<p>With that foundation, let us compare the major compression formats one by one.</p>
<hr>
<h2 id="zip"><a class="anchor" href="#zip">ZIP</a></h2>
<p>ZIP is a file format created by Phil Katz in 1989. Internally, it commonly compresses data with <strong>DEFLATE</strong>, the combination of LZ77 and Huffman coding. The important distinction is that ZIP is not itself a compression algorithm. It is a container format that can hold data compressed by an algorithm such as DEFLATE.</p>
<p>ZIP <strong>compresses each file independently</strong>. This is known as a non-solid archive. It makes it possible to extract a single file without decompressing the rest of the archive. On the other hand, it cannot exploit duplicate data across files, so its compression ratio may be lower than that of tar.gz, which we will examine later.</p>
<p>Because Windows, macOS, Linux, and most other operating systems support ZIP without additional software, it is the safest general choice when cross-platform compatibility matters.</p>
<hr>
<h2 id="gzip-gnu-zip"><a class="anchor" href="#gzip-gnu-zip">GZIP (GNU Zip)</a></h2>
<p>Like ZIP, GZIP uses <strong>DEFLATE</strong> internally. Why does a separate format exist if the algorithm is the same? ZIP also acts as a container for multiple files, whereas GZIP specializes in compressing <strong>one file or one stream</strong>.</p>
<p>To compress multiple files or a directory with GZIP, we first combine them into a single TAR archive and then compress that archive with GZIP. This two-step process produces a <code>.tar.gz</code> or <code>.tgz</code> file.</p>
<p>The GZIP file structure is specified in RFC 1952 and is quite simple: a <strong>fixed 10-byte header</strong>, an optional extended header containing information such as the original filename or comments, the DEFLATE-compressed data, and an <strong>8-byte trailer</strong> containing a CRC-32 checksum and the original size. CRC-32 verifies that the decompressed data matches the original. GZIP is therefore a lightweight wrapper around a DEFLATE stream.</p>
<p>DEFLATE uses a sliding window of at most <strong>32 KB</strong>. This limit is important because patterns more than 32 KB apart cannot refer to one another. GZIP also provides compression levels from 1 to 9. Level 1 is fast but produces a lower ratio of roughly 60%, while level 9 is slow but reaches a higher ratio of roughly 75%. The default is level 6, a compromise between speed and size.</p>
<p>GZIP has long been a standard for distributing source code, compressing logs, and packaging software in Unix and Linux environments. It also remains a common default for HTTP compression through <code>Content-Encoding: gzip</code>, although Brotli has increasingly replaced it in that role.</p>
<hr>
<h2 id="zstd-zstandard"><a class="anchor" href="#zstd-zstandard">ZSTD (Zstandard)</a></h2>
<p>ZSTD is a compression algorithm developed by Yann Collet at Meta, formerly Facebook, and released as open source in 2016. Its main advantage is <strong>dramatically faster compression and decompression while retaining a ratio comparable to GZIP</strong>.</p>
<p>ZSTD has three broad stages. First, an LZ77-family <strong>match finder</strong> detects repeated patterns in the input. It then encodes the results, including literals, match lengths, and offsets, as <strong>sequences</strong>. Finally, it compresses those sequences with <strong>entropy coding</strong>. Instead of relying only on GZIP-style Huffman coding, ZSTD uses <strong>FSE (Finite State Entropy)</strong>. FSE is an entropy coder based on ANS (Asymmetric Numeral Systems), combining useful properties of Huffman and arithmetic coding. Huffman coding can assign only an integer number of bits per symbol, while FSE can represent fractional-bit probabilities and get closer to the theoretical optimum. (Despite the grand name, the key idea is simply a smarter way to express the same data with fewer bits.)</p>
<p>The match finder also changes strategy with the compression level. Lower levels, from 1 to 4, use simple hash tables for speed. Middle levels, from 5 to 12, compare multiple candidates and lazily choose a better match. Higher levels, from 13 to 22, use binary trees and dynamic programming to find near-optimal matches. This broad range makes it possible to choose a low level for real-time transfer and a high level for archival work.</p>
<p>On the Silesia Corpus benchmark, ZSTD’s default level 3 compresses at around 300 MB/s and decompresses at around 1,200 MB/s. GZIP’s default level 6 reaches only around 34 MB/s for compression and 380 MB/s for decompression. <strong>ZSTD is roughly eight times faster at compression and three times faster at decompression, while its compression ratio is slightly better at 3.17 versus GZIP’s 3.09.</strong> These figures make ZSTD’s improved trade-off easy to see.</p>
<p>Adoption has expanded quickly. ZSTD is used for Linux kernel module compression and transparent filesystem compression, and major distributions including Arch Linux, Fedora, Debian, and Ubuntu have adopted it as a default package format. Starting with v1.5.7, released in February 2025, <strong>multithreaded compression is enabled by default</strong> with up to four threads, further widening the practical speed gap with single-threaded GZIP. AWS has also reported reducing S3 storage by about 30% after switching internal services from gzip to zstd.</p>
<hr>
<h2 id="bzip2"><a class="anchor" href="#bzip2">BZIP2</a></h2>
<p>BZIP2 compresses data through a pipeline of transformations.</p>
<ol>
<li><strong>RLE (Run-Length Encoding)</strong>: reduces consecutive repetitions in the initial data</li>
<li><strong>BWT (Burrows-Wheeler Transform)</strong>: rearranges data into a form that is easier to compress</li>
<li><strong>MTF (Move-to-Front Transform)</strong>: converts the BWT output into a numeric sequence</li>
<li><strong>RLE</strong>: reduces repetitions in the MTF result again</li>
<li><strong>Huffman coding</strong>: performs the final frequency-based encoding</li>
</ol>
<p>BZIP2 offers a higher compression ratio than GZIP, but both compression and decompression are slower. It has traditionally been used for archival work where size matters more than speed.</p>
<p>Its latest release was v1.0.8 in 2019, and active development has largely stopped. As benchmarks increasingly show ZSTD outperforming BZIP2 in both ratio and speed, new projects are more likely to choose ZSTD.</p>
<hr>
<h2 id="xz"><a class="anchor" href="#xz">XZ</a></h2>
<p>XZ is a compression format that uses <strong>LZMA2</strong>. LZMA, the Lempel-Ziv-Markov chain Algorithm developed by Igor Pavlov, combines LZ77-based dictionary compression with range encoding. Rather than simply being an “improved LZMA,” LZMA2 is closer to a <strong>container format</strong> around LZMA streams. Its key additions include multithreaded compression and decompression and efficient handling of incompressible data.</p>
<p>Among the formats discussed here, XZ provides <strong>the highest compression ratio</strong>. The cost is very slow compression and high memory consumption. It is well suited to archival work where minimizing storage is the top priority.</p>
<p>In March 2024, however, a <strong>backdoor was discovered in xz-utils, XZ’s core library, in the severe CVE-2024-3094 supply-chain incident</strong>. A two-year social-engineering campaign had obtained maintainer privileges, and the vulnerability received the maximum CVSS score of 10.0. Major distributions immediately rolled back to safe versions, but the incident became a powerful warning about open-source supply-chain security. (XZ’s technical value remains, but this context is worth considering when choosing tools.)</p>
<hr>
<h2 id="tar"><a class="anchor" href="#tar">TAR</a></h2>
<p>TAR, short for Tape Archive, is not a compression algorithm. It is a tool and format for <strong>combining multiple files and directories into one archive</strong>. As its name suggests, it was originally designed for tape backups. Since tape is sequential media, appending data in a continuous sequence was a natural design.</p>
<p>TAR’s internal structure is surprisingly simple. Everything is processed in <strong>512-byte blocks</strong>. Each file begins with a 512-byte header containing metadata such as its name, up to 100 bytes, file mode, owner UID and GID, size, modification time, and checksum. File data follows the header and is padded to a multiple of 512 bytes. Two zero-filled 512-byte blocks mark the end of the archive. Most modern TAR implementations follow the POSIX <strong>UStar (Unix Standard TAR)</strong> format, which supports longer filenames of up to 256 bytes and additional metadata fields.</p>
<p>The key property is that TAR preserves <strong>Unix filesystem metadata</strong>, including permissions, ownership, timestamps, and symbolic links. ZIP does not always preserve this Unix-specific metadata perfectly, which often makes TAR a better fit for server deployment.</p>
<p>TAR does not make data smaller by itself. Its headers and padding can actually make the result slightly larger. Compression is performed by combining TAR with GZIP, BZIP2, XZ, ZSTD, or another compressor. That is why extensions such as <code>.tar.gz</code>, <code>.tar.bz2</code>, <code>.tar.xz</code>, and <code>.tar.zst</code> exist. TAR handles “bundling,” while the compression tool handles “shrinking,” a classic example of the Unix philosophy to “do one thing well.”</p>
<p>TAR is standard in Unix and Linux environments, while Windows may require additional software such as 7-Zip.</p>
<hr>
<h2 id="a-quick-look-at-brotli"><a class="anchor" href="#a-quick-look-at-brotli">A quick look at Brotli</a></h2>
<p>Frontend developers should also understand <strong>Brotli</strong>. Google developed Brotli, and it was standardized for HTTP stream compression as <code>Content-Encoding: br</code> in 2015.</p>
<p>Every major browser supports it over HTTPS, with more than 96% global coverage, and it generally produces files <strong>about 15 to 25% smaller than GZIP</strong>. It is particularly effective for text-based static assets such as JavaScript, CSS, and HTML. Major CDNs including Cloudflare use it as a default compression method, and the modern web-optimization pattern is “Brotli first, GZIP fallback.”</p>
<p>If build artifacts are uploaded to S3 and served through a CDN, precompressing static files with Brotli can substantially reduce network transfer. (There was not enough project-specific evidence for me to introduce it immediately, but it remains an option worth understanding and revisiting.)</p>
<hr>
<h2 id="why-does-targz-compress-better-than-zip"><a class="anchor" href="#why-does-targz-compress-better-than-zip">Why does tar.gz compress better than ZIP?</a></h2>
<p>The difference comes from <strong>solid archives</strong> and <strong>non-solid archives</strong>.</p>
<p>With tar.gz, TAR first combines every file into one continuous data stream, and GZIP compresses that entire stream at once. This allows it to detect and exploit <strong>duplicate data across files</strong>. That is a solid archive. If a build directory contains dozens of similarly structured JavaScript bundles, a pattern in file A can be referenced when it appears again in file B. Metadata overhead is also lower because the compressor does not need a separate header, checksum, and table of contents for every compressed stream.</p>
<p>ZIP is non-solid and compresses each file independently, so it cannot use redundancy across files. Even if files A and B contain the same code block, their separate DEFLATE streams do not know about one another. This is why tar.gz commonly achieves a compression ratio 5 to 15% better than ZIP. The difference becomes larger when a build contains many files with similar structures.</p>
<p>Solid archives also have clear disadvantages.</p>
<ul>
<li>To extract one file, the decoder may need to <strong>decompress all data that appears before it</strong>. Because every file belongs to one stream, it cannot simply jump to an arbitrary point. ZIP supports random access to individual files and may be more suitable when specific files are extracted frequently.</li>
<li>Damage to part of the archive can make <strong>all data after the damaged point unrecoverable</strong>. A non-solid archive may lose only the damaged file while preserving the rest.</li>
</ul>
<hr>
<p><strong>Added in 2026</strong></p>
<h2 id="the-choice-in-2024-was-targz-what-would-i-choose-now"><a class="anchor" href="#the-choice-in-2024-was-targz-what-would-i-choose-now">The choice in 2024 was tar.gz. What would I choose now?</a></h2>
<p>I chose tar.gz at the time because of its compatibility and stability. The artifact had to be decompressed in several environments after being uploaded to S3, so the format supported almost everywhere was the safest option.</p>
<p>If I faced the same situation today, however, I would seriously consider <strong>tar.zst (TAR + ZSTD)</strong>. Recall the benchmark figures above.</p>
<p>GZIP’s default compression speed is 34 MB/s, while ZSTD’s default is 300 MB/s. For a 2 GB build directory, a simple calculation gives about 60 seconds for GZIP and around seven seconds for ZSTD. With multithreading enabled by default in ZSTD v1.5.7, using as many as four threads, the practical difference can be even larger. In a CI/CD pipeline, those seconds accumulate on every deployment cycle.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="sh" data-theme="github-dark github-light"><code data-language="sh" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D"># tar.zst 생성 (멀티스레드 자동 활용)</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">tar</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> --zstd</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> -cf</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> archive.tar.zst</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> directory/</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D"># 또는 압축 레벨 지정 (-T0은 사용 가능한 모든 코어 활용)</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">tar</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> -cf</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> archive.tar.zst</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> -I</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> 'zstd -3 -T0'</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> directory/</span></span></code></pre></figure>
<p>ZSTD also matches or surpasses GZIP’s compression ratio, so there is effectively no longer a trade-off where speed requires accepting a larger artifact. It is both faster and smaller.</p>
<p>The receiving environment must still be able to decompress zstd. Major Linux distributions include it, and macOS users can install it easily with Homebrew using <code>brew install zstd</code>. Legacy or minimal environments may require a separate installation, so every environment used by the team should be checked in advance. When compatibility is the overriding concern, tar.gz remains the safest general choice.</p>
<hr>
<h2 id="comparison-at-a-glance"><a class="anchor" href="#comparison-at-a-glance">Comparison at a glance</a></h2>
<table>
<thead>
<tr>
<th>Format</th>
<th>Algorithm</th>
<th>Ratio</th>
<th>Speed</th>
<th>Key characteristics</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>ZIP</strong></td>
<td>DEFLATE</td>
<td>Medium</td>
<td>Fast</td>
<td>Cross-platform, non-solid</td>
</tr>
<tr>
<td><strong>GZIP</strong></td>
<td>DEFLATE</td>
<td>Medium</td>
<td>Fast</td>
<td>Single stream, commonly with TAR</td>
</tr>
<tr>
<td><strong>ZSTD</strong></td>
<td>Zstandard</td>
<td>High</td>
<td>Very fast</td>
<td>Tunable levels, modern standard</td>
</tr>
<tr>
<td><strong>BZIP2</strong></td>
<td>BWT+MTF+Huffman</td>
<td>High</td>
<td>Slow</td>
<td>Largely inactive development</td>
</tr>
<tr>
<td><strong>XZ</strong></td>
<td>LZMA2</td>
<td>Very high</td>
<td>Very slow</td>
<td>Best ratio, security context</td>
</tr>
<tr>
<td><strong>Brotli</strong></td>
<td>Brotli</td>
<td>High</td>
<td>Medium</td>
<td>Optimized for the web</td>
</tr>
</tbody>
</table>
<hr>
<h2 id="conclusion"><a class="anchor" href="#conclusion">Conclusion</a></h2>
<p>Before digging into compression, I honestly thought, “Can’t we just put everything in a zip file?” Working with a build directory larger than 2 GB made the consequences tangible: the algorithm we choose can meaningfully change both upload time and cost.</p>
<p>Every compression format reflects a different design philosophy and trade-off. ZIP offers compatibility, GZIP ubiquity, ZSTD speed, and XZ maximum compression. There is no universal “best” option; the right choice depends on the project’s constraints.</p>
<p>Understanding the principles behind tools we usually take for granted helps us make better decisions the next time a similar problem appears. I hope this article serves as a useful starting point for someone facing that choice.</p>]]></content:encoded>
            <author>jihoon7705@gmail.com (이지훈)</author>
            <category>소박한궁금증</category>
            <category>소프트웨어</category>
        </item>
    </channel>
</rss>