<?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/pt-BR</link>
        <description>프론트엔드 개발자 이지훈(후니)의 기술 블로그. React, TypeScript, Next.js 등 웹 개발 기록과 학습 노트를 공유합니다.</description>
        <lastBuildDate>Wed, 19 Aug 2026 00:42:24 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>pt-BR</language>
        <copyright>All rights reserved 2026, 이지훈</copyright>
        <item>
            <title><![CDATA[Gerenciamento de estado]]></title>
            <link>https://hooninedev.com/pt-BR/260518</link>
            <guid isPermaLink="false">https://hooninedev.com/pt-BR/260518</guid>
            <pubDate>Mon, 18 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Neste post, quero falar sobre gerenciamento de estado (State Management). Não se trata de uma comparação entre bibliotecas. Mais importante do que decidir qual ferramenta é melhor é desenvolver uma pe...]]></description>
            <content:encoded><![CDATA[<p>Neste post, quero falar sobre <strong>gerenciamento de estado (State Management)</strong>. Não se trata de uma comparação entre bibliotecas. Mais importante do que decidir qual ferramenta é melhor é desenvolver uma percepção sobre <strong>como enxergar</strong> o estado e onde <strong>traçar seus limites</strong>.</p>
<p>Nos últimos tempos, as ferramentas de IA (Claude, ChatGPT, Cursor, Gemini, Copilot) passaram a ocupar um espaço cada vez maior ao nosso lado. A velocidade de desenvolvimento cresceu exponencialmente, mas, para ser sincero, tenho a impressão de que a qualidade final dos serviços não acompanhou esse ritmo. Tornou-se comum encontrar tantos bugs quanto funcionalidades novas, assim como ouvir: “Não sei por que isso ficou assim”.</p>
<p>À medida que desenvolvemos mais rápido, deixamos de examinar cada linha de código com o mesmo cuidado. Por isso mesmo, acredito que se tornou ainda mais necessário ter <strong>uma base sólida para orientar a IA na direção correta</strong>. Para manter a qualidade do resultado, precisamos identificar problemas no código gerado pela IA e redirecioná-la para aquilo que realmente queremos. Essa base pode envolver desenvolvimento orientado ao domínio, abstração, TDD (Test-Driven Development, desenvolvimento orientado a testes), uso adequado de bibliotecas, vantagens de performance, entre outros aspectos.</p>
<p>No entanto, sempre que pergunto a colegas de frontend — e também a profissionais de outras áreas de TI — “Qual é a tarefa mais difícil no desenvolvimento frontend?”, a resposta que mais ouço é sempre a mesma: <strong>“Gerenciar o fluxo de estado.”</strong></p>
<p>Neste artigo, pretendo explicar por que gerenciar esse fluxo é tão difícil e que tipo de discernimento e sensibilidade precisamos desenvolver para lidar bem com ele.</p>
<h2 id="o-que-é-estado-state"><a class="anchor" href="#o-que-é-estado-state">O que é estado (State)?</a></h2>
<p>Antes de entrar no assunto propriamente dito, vamos começar pela pergunta mais básica: afinal, o que exatamente chamamos de “estado”?</p>
<p>Enquanto estudava desenvolvimento frontend, eu lia com frequência os textos de <a href="https://blog.hoseung.me/2021-12-05-state-management" target="_blank" rel="noopener noreferrer">hoseung.me</a>. Ali, estado é definido como <strong>“todo dado capaz de afetar a UI”</strong>. Número de curtidas, itens do carrinho, modal aberto ou fechado, valores digitados, informações do usuário autenticado, aba selecionada, resultados de busca, estado de carregamento: tudo isso é estado.</p>
<p>A documentação oficial do React oferece uma definição mais formal. O próprio título da página é <a href="https://react.dev/learn/state-a-components-memory" target="_blank" rel="noopener noreferrer">“State: A Component's Memory”</a>; em outras palavras, trata-se de <strong>“um mecanismo que permite ao componente reter dados entre renderizações e acionar uma nova renderização no React quando esses dados são atualizados”</strong>. Ou seja, são dados que não desaparecem com o tempo, mudam em resposta a algum evento e fazem a UI ser renderizada novamente quando mudam. Há ainda outro ponto importante: o estado é <strong>isolado por instância do componente</strong>. Mesmo que o mesmo componente apareça dez vezes em uma página, cada instância terá seu próprio estado independente. Esse fato se conecta diretamente à discussão posterior sobre “onde o estado deve ficar”.</p>
<p>As duas definições apontam para o mesmo lugar: estado é <strong>“um valor que muda ao longo do tempo e afeta a renderização”</strong>. Uma constante que não muda não é estado. Um design token primitivo fixado no build time não é estado, mas um dark mode que o usuário pode alternar é. (A rigor, o valor em si é resolvido conforme o estado do tema, dark ou light; portanto, é mais preciso dizer que a “seleção do tema” é o estado e que o token é o espelho no qual esse estado se reflete.)</p>
<p>Há um aspecto que vale destacar: <strong>nem todo estado vive em um componente</strong>. Alguns estados vivem em cookies; outros, em localStorage, sessionStorage ou IndexedDB; outros ainda, na URL. Quando trazemos dados do servidor para o cliente e os armazenamos em cache, eles também se tornam uma forma de estado. Até a posição de scroll e a pilha de histórico mantidas pelo próprio navegador às vezes precisam ser tratadas como estado, pois determinam o comportamento da aplicação.</p>
<h2 id="por-que-é-tão-difícil"><a class="anchor" href="#por-que-é-tão-difícil">Por que é tão difícil?</a></h2>
<p>Vamos começar pensando de forma simples sobre por que lidar com estado é difícil. Não bastaria criar os estados necessários, levá-los até onde são usados e tratar corretamente suas atualizações e resets?</p>
<p>Com essa pergunta em mente, abra uma página do serviço em que você trabalha hoje.</p>
<p>Quantos componentes existem nessa página? Mesmo em uma página simples, provavelmente há de dezenas a centenas de componentes formando uma árvore. Cada componente pode manter seu próprio estado, compartilhá-lo com componentes irmãos ou recebê-lo do pai. O estado também transita entre páginas; alguns valores precisam sobreviver a um refresh, enquanto outros devem desaparecer quando a aba é fechada.</p>
<p>O verdadeiro motivo de ser tão difícil gerenciar estado é este: <strong>não conseguimos visualizar de imediato onde os inúmeros estados são declarados, como são atualizados e quando deixam de existir</strong>. Conforme aumenta o número de componentes com papéis semelhantes, torna-se mais difícil tanto nomear estados quanto rastrear o código que os altera.</p>
<p>É assim que surge uma teia invisível. Um clique no componente A invalida os dados de B; a invalidação de B fecha a UI de C; ao fechar C, os dados digitados no formulário desaparecem. Se essa cadeia não estiver explicitada em nenhum lugar do código, teremos de reconstruir mentalmente toda a teia ao depurar um bug.</p>
<p>Como, então, organizar essa teia? Para mim, o primeiro passo é reconhecer que <strong>“existem diferentes tipos de estado”</strong>.</p>
<h2 id="nem-todo-estado-é-igual"><a class="anchor" href="#nem-todo-estado-é-igual">Nem todo estado é igual</a></h2>
<p><a href="https://kentcdodds.com/blog/application-state-management-with-react" target="_blank" rel="noopener noreferrer">Kent C. Dodds</a> divide o estado em <strong>Server Cache</strong> (informações que existem no servidor e são mantidas pelo cliente para acesso rápido) e <strong>UI State</strong> (informações que existem apenas na UI para controlar o comportamento da interface). Muitas vezes erramos justamente ao agrupar os dois.</p>
<p>A <a href="https://tanstack.com/query/latest/docs/framework/react/guides/does-this-replace-client-state" target="_blank" rel="noopener noreferrer">documentação oficial do TanStack Query</a> define a ferramenta como uma biblioteca de Server State, responsável por gerenciar operações assíncronas entre servidor e cliente, enquanto Redux, MobX e Zustand são definidos como bibliotecas de Client State. (É possível armazenar dados assíncronos nelas, mas isso é ineficiente.)</p>
<p>O ponto central é claro: <strong>Server State e Client State são problemas diferentes</strong>. Server State é assíncrono, pode ser alterado por outros usuários e se torna stale com o tempo. Client State é síncrono, está sob nosso controle e desaparece com um refresh. (Mais precisamente, quando a página é descarregada, <strong>o runtime JavaScript é reiniciado, e a árvore de componentes mantida na memória heap, junto com seus estados, é coletada.</strong> Por isso, na próxima montagem, tudo recomeça pelo valor inicial de <code>useState</code>.) Se tentarmos tratar os dois com a mesma ferramenta, teremos de implementar por conta própria padrões como invalidação de cache, atualização em background e optimistic updates.</p>
<p>Vou um passo além e divido o estado do frontend em <strong>sete categorias</strong>. Vale esclarecer desde já que essas categorias não se separam perfeitamente em um único eixo. Local de armazenamento, origem, ciclo de vida e função se misturam, de modo que um mesmo estado pode pertencer a mais de uma categoria. Em vez de uma taxonomia perfeita, encare-as como <strong>perguntas que ajudam a decidir como gerenciar um estado</strong>.</p>
<ul>
<li><strong>Estado local (Local State)</strong> — Estado usado apenas em um componente ou em uma subárvore restrita</li>
<li><strong>Estado global (Global State)</strong> — Estado que precisa ser compartilhado por toda a aplicação</li>
<li><strong>Estado do servidor (Server State)</strong> — Estado cuja fonte da verdade é o servidor e cuja cópia no cliente é um cache</li>
<li><strong>Estado de formulário (Form State)</strong> — Estado temporário que existe enquanto o usuário preenche dados</li>
<li><strong>Estado da URL (URL State)</strong> — Estado compartilhável que vive na barra de endereços e sobrevive ao refresh</li>
<li><strong>Estado externo (External State)</strong> — Estado fora do React, como cookies, localStorage, sessionStorage e IndexedDB</li>
<li><strong>Guard de estado (State Guard)</strong> — Lógica que bloqueia, permite ou valida acessos e ações conforme combinações de estado, em vez de ser um estado em si</li>
</ul>
<p>Além dessas categorias, há estados de fluxo que podem exigir uma máquina de estados e estados colaborativos em tempo real baseados em WebSocket ou CRDT.</p>
<p>Vamos analisar, uma a uma, por que cada categoria exige ferramentas diferentes e com que perspectiva devemos abordá-la.</p>
<h2 id="estado-local-local-state"><a class="anchor" href="#estado-local-local-state">Estado local (Local State)</a></h2>
<p>É a forma mais simples de estado. Ele é usado apenas dentro de um componente, e quem está de fora não precisa — nem deveria precisar — conhecê-lo. Alguns exemplos são: modal aberto ou fechado, botão de alternância on/off, hover e termo de busca enquanto está sendo digitado.</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>Isso provavelmente já é familiar. A verdadeira dificuldade do estado local, porém, está na decisão de <strong>“onde esse estado deve ficar”</strong>.</p>
<p>No artigo de Kent C. Dodds sobre <a href="https://kentcdodds.com/blog/state-colocation-will-make-your-react-app-faster" target="_blank" rel="noopener noreferrer">State Colocation</a>, ele observa que <strong>as pessoas estão acostumadas a “elevar (lift up)” o estado, mas raramente voltam a “aproximá-lo (colocate)” quando o código muda</strong>.</p>
<p>Elevar o estado é algo que fazemos naturalmente quando componentes irmãos precisam compartilhá-lo. Como os dois precisam enxergar os mesmos dados, levamos o estado para o pai comum e o distribuímos via props.</p>
<p>O problema surge quando os componentes irmãos deixam de precisar desse estado. Raramente fazemos o caminho inverso, <strong>descendo-o</strong> de volta para o filho. Como resultado, o componente pai acumula vários estados que na verdade não lhe dizem respeito e, sempre que renderiza novamente, acaba levando consigo toda a árvore de filhos.</p>
<p>Por isso, o primeiro princípio do estado local é: <strong>para tornar o código mais rápido e simples, mantenha o estado o mais próximo possível do código que o utiliza</strong>. Se um estado é usado apenas por um único filho, não há motivo para o pai mantê-lo. Mova-o para dentro desse filho; o pai ficará mais leve.</p>
<h2 id="estado-global-global-state"><a class="anchor" href="#estado-global-global-state">Estado global (Global State)</a></h2>
<p>Estado global é aquele que precisa estar acessível de qualquer ponto da aplicação. Informações de login, tema, idioma e notificações (toasts) são possíveis candidatos.</p>
<p>A diferença entre estado local e global não se resume a “onde ele vive”. O que muda é o <strong>contrato de referência</strong>. O estado local assume o compromisso de que <strong>“isso só tem significado dentro deste componente”</strong>; já o estado global publica para todo o código o compromisso de que <strong>“esse valor pode ser acessado por esse nome em qualquer lugar da aplicação”</strong>. A essência do estado global está no custo desse compromisso.</p>
<p>Criar um estado global significa, na prática, adicionar <strong>uma dependência implícita em toda a aplicação</strong>.</p>
<h2 id="estado-de-servidor-server-state"><a class="anchor" href="#estado-de-servidor-server-state">Estado de servidor (Server State)</a></h2>
<p>Colocamos os dados recebidos de uma API no estado do cliente, gerenciamos loading e error manualmente com booleanos e, em algum momento, chegamos à pergunta: <strong>“Por que estou escrevendo o mesmo boilerplate toda vez?”</strong></p>
<p>Tanner Linsley, principal mantenedor do TanStack, diz que <strong>“Client State é síncrono e previsível. Server State é assíncrono, compartilhado entre vários componentes e exige atenção ao cache, às atualizações em background e aos estados de erro.”</strong> Em outras palavras, Server State é <strong>uma espécie fundamentalmente diferente</strong> de Client State. Não devemos tratá-los com a mesma ferramenta.</p>
<p>A dificuldade do Server State não decorre das ferramentas, mas da <strong>natureza dos dados</strong>.</p>
<p>Os dados que o cliente exibe pertencem ao servidor. Aquilo que o cliente possui é apenas <strong>um snapshot de determinado momento</strong>. Com o passar do tempo, esses dados ficam stale. Além disso, são assíncronos, podem falhar e passam por estados como pending, error e success.</p>
<p>A característica mais importante é que <strong>não existe garantia de que as respostas retornarão na ordem em que as requisições foram enviadas</strong>. Imagine digitar rapidamente “react” em uma caixa de busca. As requisições r → re → rea → reac → react são enviadas nessa ordem; porém, se a resposta de “react” chegar primeiro e a de “rea” chegar depois, a tela exibirá os resultados de “rea”. Para evitar esse problema, é preciso lidar com <strong>riscos de concorrência (race conditions)</strong> que exigiriam implementar manualmente, a cada vez, um AbortController ou o rastreamento de IDs das requisições.</p>
<h2 id="estado-de-formulário-form-state"><a class="anchor" href="#estado-de-formulário-form-state">Estado de formulário (Form State)</a></h2>
<p>Formulários têm um tipo peculiar de estado. Enquanto o usuário digita, ele muda intensamente; depois do envio, em geral desaparece. Não é compartilhado com nenhum outro lugar e, na maioria dos casos, também não há onde armazená-lo.</p>
<p>O problema é que essa “mudança intensa” custa caro. Se cada tecla pressionada causar uma nova renderização do React, o atraso na digitação pode se tornar perceptível em formulários grandes. Além disso, um formulário não serve apenas para “guardar valores”. <strong>Validação, dirty check, estado de envio, mensagens de erro e fluxos em múltiplas etapas</strong> coexistem e mudam ao mesmo tempo dentro de um único formulário.</p>
<p>Espera-se que um formulário em várias etapas, como um checkout de três passos, <strong>“preserve o progresso mesmo após um refresh no meio do processo”</strong>. Se seus valores forem mantidos apenas com useState, todos desaparecerão no refresh. É natural armazená-los em <strong>sessionStorage</strong> (armazenamento temporário por aba) ou na <strong>URL</strong> (para etapas compartilháveis). Ou seja, dependendo dos requisitos de ciclo de vida, o estado de formulário se combina com <strong>External State</strong> ou <strong>URL State</strong>.</p>
<h2 id="estado-da-url-url-state"><a class="anchor" href="#estado-da-url-url-state">Estado da URL (URL State)</a></h2>
<p>Imagine uma página de busca em que categoria, ordenação e número da página são usados como filtros. Se esses estados forem mantidos com useState, três problemas surgirão ao mesmo tempo.</p>
<ul>
<li>Ao atualizar a página, todos os filtros voltam aos valores iniciais</li>
<li>Ao compartilhar a URL com alguém, essa pessoa verá a página sem os filtros aplicados</li>
<li>Ao clicar em voltar, você não retornará aos filtros anteriores</li>
</ul>
<p>Para resolver esses problemas, <strong>é natural colocar o estado na URL</strong>. A própria URL é um armazenamento persistente gratuito, compatível com refresh, compartilhamento e histórico.</p>
<pre><code>/products?category=shoes&#x26;sort=price-desc&#x26;page=2
</code></pre>
<p>Essa única URL contém o estado completo de <strong>“página 2 da categoria de calçados, ordenada por preço decrescente”</strong>. Não é necessário mantê-lo separadamente com useState.</p>
<p>Quando, então, é apropriado tratar estado na URL? <strong>A URL é uma interface pública.</strong> Senhas, tokens de autenticação e anotações temporárias que o usuário não queira mostrar a outras pessoas não devem estar na URL. Além disso, inserir diretamente na URL valores que mudam com muita frequência — como uma busca atualizada a cada tecla — enche a pilha de histórico de lixo. Nesses casos, devemos aplicar a alteração após um debounce, reservar <code>push</code> para quando fizer sentido e usar <code>replace</code> nas atualizações que não devem adicionar uma entrada ao histórico.</p>
<p>Os valores da URL são <strong>sempre strings</strong>. Números, booleanos, arrays e objetos precisam passar por serialização e desserialização. Além disso, a URL precisa seguir as regras de <a href="https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams" target="_blank" rel="noopener noreferrer">percent-encoding</a>, que dão tratamento especial a <code>&#x26;</code>, <code>=</code>, caracteres coreanos, espaços e outros elementos. Implementar tudo isso manualmente a cada vez logo se transforma em uma fonte de 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>Bibliotecas como <a href="https://nuqs.dev/" target="_blank" rel="noopener noreferrer">nuqs</a> resolvem os dois problemas por meio do conceito de <em>parser</em>. Parsers como <code>parseAsInteger</code>, <code>parseAsBoolean</code> e <code>parseAsJson</code> cuidam de uma só vez da serialização, da desserialização e dos tipos. A biblioteca oferece suporte à maioria dos ambientes, incluindo Next.js (App Router e Pages Router), React Router v6/v7, TanStack Router e Remix.</p>
<p>Isso significa que podemos colocar qualquer quantidade de estado na URL? Além dos problemas de serialização e tipos, ainda existe uma última restrição a considerar. A <a href="https://datatracker.ietf.org/doc/html/rfc7230" target="_blank" rel="noopener noreferrer">RFC 7230</a> não define um limite exato, mas recomenda que “o servidor ofereça suporte a pelo menos 8.000 octetos” (octeto é a unidade usada em redes e comunicação de dados para designar, sem ambiguidade, um conjunto de 8 bits, isto é, 1 byte). Os limites também variam entre navegadores: browsers modernos geralmente aceitam de 8 KB a dezenas de milhares de caracteres, mas <strong>mecanismos de busca, o processamento de OG/compartilhamento em redes sociais e alguns gateways podem truncar a URL por volta de 2 KB</strong>. Portanto, não devemos inserir dados indefinidamente na URL. É mais seguro manter nela apenas os <strong>principais filtros compartilháveis</strong> e deixar o restante a cargo do sessionStorage ou de um armazenamento no servidor.</p>
<h2 id="estado-externo-external-state"><a class="anchor" href="#estado-externo-external-state">Estado externo (External State)</a></h2>
<p>O React conhece apenas o estado dentro dele próprio. Nossa aplicação, porém, conversa constantemente com o mundo fora do React. Os estados que vivem nesse mundo sobrevivem e mudam independentemente do ciclo de vida do React. O External State abordado aqui inclui <strong>Cookie, localStorage,sessionStorage,IndexedDB</strong>.</p>
<p>Como escolher o armazenamento adequado? Costumo pensar em quatro perspectivas: <strong>duração, capacidade, sincronicidade e segurança</strong>.</p>
<p>Para <strong>tokens de autenticação</strong>, a <a href="https://owasp.org/www-community/HttpOnly" target="_blank" rel="noopener noreferrer">recomendação da OWASP</a> prioriza <strong>cookies HttpOnly + Secure</strong>. Como o localStorage é acessível via JavaScript, <strong>basta uma exposição a XSS para que o token seja roubado diretamente</strong>. Alguns guias de segurança recomendam um padrão híbrido: <strong>access token na memória e refresh token em um cookie HttpOnly</strong>. Para dados persistentes, não sensíveis e que mudam pouco, usa-se localStorage; para dados que devem desaparecer com a aba, sessionStorage. IndexedDB costuma ser usado para cache offline, grandes volumes de dados e arquivos.</p>
<p>Cookies e Web Storage (local/session) <strong>armazenam apenas strings</strong>. Por isso, inserir um objeto exige passar por <code>JSON.stringify</code>/<code>JSON.parse</code>. O JSON, contudo, tem limitações.</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> se torna string em um round trip por JSON, enquanto <code>Map</code>, <code>Set</code> e <code>undefined</code> podem perder dados. No comportamento padrão, <code>BigInt</code> faz <code>JSON.stringify</code> lançar um <code>TypeError</code>, portanto a serialização falha por completo. Ao armazenar objetos fora do React, devemos sempre ter consciência de <strong>quais tipos podem desaparecer, ser alterados ou fazer a serialização falhar</strong> e, se necessário, criar um adapter.</p>
<p>A verdadeira dificuldade do External State é que <strong>o React não detecta suas mudanças automaticamente</strong>. Gravar um valor no localStorage não faz um componente React renderizar novamente. Em geral, há três padrões para resolver isso.</p>
<ul>
<li><strong>Encapsular o armazenamento em um hook customizado (useLocalStorage) e sincronizar o External State com o estado do React.</strong> É uma solução leve, mas, quando implementada do zero, exige lidar com casos extremos como múltiplas abas, SSR e tearing.</li>
<li>Usar o hook <code>useSyncExternalStore</code>, introduzido no React 18, para <strong>“sincronizar o React com estados externos”</strong>. Com isso, <strong>é possível garantir que não ocorra tearing durante a renderização concorrente</strong>. É a ferramenta padrão para integrar localStorage, APIs do navegador e stores externos.</li>
<li>Como bibliotecas de estado oferecem integração com armazenamento externo como recurso de primeira classe — por exemplo, o middleware <code>persist</code> do Zustand e o <code>atomWithStorage</code> do Jotai —, podemos aproveitar implementações já prontas.</li>
</ul>
<p>Há ainda outro princípio importante: <strong>no momento em que trazemos um estado externo para o React, a responsabilidade pela sincronização passa a ser nossa</strong>. E se ele for atualizado em outra aba? E se o servidor alterar o cookie? E se o usuário modificar diretamente o localStorage pelas ferramentas de desenvolvedor do navegador? Essas situações frequentemente se tornam grandes fontes de bugs.</p>
<h2 id="guard-de-estado-state-guard"><a class="anchor" href="#guard-de-estado-state-guard">Guard de estado (State Guard)</a></h2>
<p>A última categoria tem uma natureza um pouco diferente. Não é o estado em si, mas <strong>a lógica que usa uma combinação de estados para impedir, permitir ou validar determinado fluxo</strong>.</p>
<p>O exemplo mais comum é o <strong>guard de autenticação (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>Aqui, o estado <code>isAuthenticated</code> controla o fluxo de roteamento. Isso é uma lógica de guard. Existem vários tipos: guard de autenticação (usuário autenticado), guard de autorização (papel ou permissão específica), guard de fluxo (ramificação de entrada) e guard de validação (habilitação de etapa), entre outros.</p>
<p>A lógica de guards tende a se concentrar em um único lugar. É comum um componente reunir tudo: <strong>“se não estiver autenticado, vá para o login; se não tiver permissão, mostre 403; se o carrinho estiver vazio, vá para a página de produtos; se o usuário estiver suspenso, exiba o aviso de suspensão”</strong>. Quanto mais inchado fica o guard, mais difícil é depurar qual condição bloqueou o fluxo e onde isso aconteceu.</p>
<p>Um bom guard <strong>verifica apenas uma coisa</strong>. A combinação é feita por 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>Cada guard toma apenas uma decisão, e a estrutura em árvore é responsável pela composição. Para adicionar um novo guard, não é necessário alterar os existentes.</p>
<p>Ao trabalhar com guards, há algo que exige ainda mais reflexão do que decidir o que bloquear: <strong>definir para onde encaminhar o usuário e como tratar o fluxo depois disso</strong>. Um guard que apenas bloqueia, sem fallback, termina em uma tela branca ou em um spinner infinito.</p>
<p>O bug mais comum ocorre quando <strong>“o conteúdo protegido pisca brevemente antes de a verificação assíncrona do guard terminar”</strong>. A validação do token de autenticação e a consulta de permissões quase sempre são assíncronas; nesse intervalo, <code>isAuthenticated</code> pode ficar temporariamente como <code>undefined</code> ou <code>false</code>. <strong>Se o estado de loading não for tratado explicitamente, a tela protegida pode ser exposta nesse intervalo ou o usuário pode ser redirecionado por engano para a página de login.</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>Dois modelos são usados com frequência na implementação de guards de autorização.</p>
<ul>
<li><strong>RBAC (Role-Based Access Control)</strong>: concede permissões por papel. Por exemplo: “admin pode ver as informações de todos os usuários”. É simples e rápido, mas o número de papéis explode à medida que eles se tornam mais granulares</li>
<li><strong>ABAC (Attribute-Based Access Control)</strong>: determina permissões a partir de uma combinação de atributos. Por exemplo: “se o usuário for o autor do post, pertencer à mesma equipe ou for admin”. Tem grande poder de expressão, mas é difícil de implementar e depurar</li>
</ul>
<p>Como mostra o <a href="https://tanstack.com/router/v1/docs/framework/react/how-to/setup-rbac" target="_blank" rel="noopener noreferrer">guia de RBAC do TanStack Router</a>, recomenda-se o padrão de inserir o guard em <code>beforeLoad</code>, no nível do router. O ponto central é que <strong>as verificações de permissão devem poder ser representadas como dados — uma lista de papéis ou permissões — em vez de ficarem espalhadas pelo código</strong>. Assim, uma mudança na política de acesso se resume a uma <em>mudança de dados</em>.</p>
<h2 id="conclusão"><a class="anchor" href="#conclusão">Conclusão</a></h2>
<p>Vamos recapitular. O gerenciamento de estado não é difícil porque as bibliotecas são difíceis. Ele é difícil porque <strong>frequentemente esquecemos que existem diferentes tipos de estado</strong> e deixamos passar o fato de que cada tipo exige ferramentas e formas de pensar distintas.</p>
<p>Mantenha o estado local o mais próximo possível; questione mais uma vez se o estado global é realmente global; trate Server State como cache; separe os formulários do domínio; use a URL de forma mais ativa; assuma conscientemente as responsabilidades envolvidas no armazenamento externo; e divida os guards em partes pequenas que possam ser compostas. Esses são os fundamentos para lidar com as sete categorias.</p>
<p>Acima de tudo isso, o discernimento necessário se resume, no fim, a quatro perguntas.</p>
<ul>
<li>Onde está a Single Source of Truth destes dados?</li>
<li>Este valor pode ser calculado ou realmente precisa ser armazenado?</li>
<li>Existe alguma combinação impossível entre esses estados?</li>
<li>Este estado realmente deveria estar neste lugar?</li>
</ul>
<p>Fazer essas perguntas sempre que criamos uma nova tela, revisamos um PR ou recebemos código produzido por IA é, acredito, o caminho mais seguro para desenvolver esse discernimento e essa sensibilidade.</p>
<p>Como mencionei no início, a IA permanecerá ao nosso lado por muito tempo. O tempo que dedicamos a examinar linha por linha continuará diminuindo. Mas, justamente por isso, a capacidade de responder a pequenas perguntas como <strong>“Onde este estado deveria ficar?”</strong> se tornará ainda mais valiosa. É fácil pedir à IA: “adicione mais um useState aqui”. Saber que novo fio essa linha acrescenta à teia da nossa aplicação, porém, depende exclusivamente do discernimento de quem lê o código.</p>
<p>Não existe uma resposta única. Ainda assim, há uma diferença evidente entre <strong>“criar estado sem saber o que é estado”</strong> e <strong>“criá-lo tendo consciência de seu tipo e de sua localização”</strong>. Espero que, da próxima vez que você estiver prestes a escrever uma linha de <code>useState</code>, pare por um instante e pergunte: “A qual categoria de estado isto pertence?”</p>
<h3 id="referências"><a class="anchor" href="#referências">Referências</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>gerenciamento</category>
            <category>de</category>
            <category>estado</category>
            <category>no</category>
            <category>frontend</category>
            <category>arquitetura</category>
            <category>React</category>
        </item>
        <item>
            <title><![CDATA[Modelo de domínio]]></title>
            <link>https://hooninedev.com/pt-BR/260418</link>
            <guid isPermaLink="false">https://hooninedev.com/pt-BR/260418</guid>
            <pubDate>Sat, 18 Apr 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Neste post, quero falar sobre domínio (Domain). Ao longo da minha experiência com desenvolvimento, encontrei a palavra "domínio (Domain)" com bastante frequência. Mas, quando alguém pergunta "afinal, ...]]></description>
            <content:encoded><![CDATA[<p>Neste post, quero falar sobre <strong>domínio (Domain)</strong>.</p>
<p>Ao longo da minha experiência com desenvolvimento, encontrei a palavra <strong>"domínio (Domain)"</strong> com bastante frequência. Mas, quando alguém pergunta "afinal, o que exatamente é um domínio?", não é tão fácil dar uma resposta clara. (Sinceramente, quando comecei a programar, achava que domínio significava <a href="http://www" target="_blank" rel="noopener noreferrer">www</a>.)</p>
<p>Ao procurar informações sobre domínio, naturalmente chegamos a conceitos como <strong>modelo de domínio</strong>, <strong>objeto de domínio</strong> e <strong>modelo de objetos de domínio</strong>. Sempre senti falta, porém, de textos que organizassem tanto as diferenças entre eles quanto o significado desses conceitos no <strong>frontend</strong>, e não no backend. Neste texto, começarei pelas definições de cada conceito e, com exemplos, mostrarei como separar e abstrair a lógica de domínio no frontend de forma adequada.</p>
<p>Ultimamente, tenho me interessado bastante pelo domínio tributário. Como a declaração do imposto de renda global se aproxima em maio, os exemplos deste texto tratarão de impostos.</p>
<hr>
<h2 id="domínio-domain"><a class="anchor" href="#domínio-domain">Domínio (Domain)</a></h2>
<p>Comecemos pela pergunta mais básica. O que é um <strong>domínio</strong>?</p>
<p>Eric Evans define domínio da seguinte forma em seu livro <strong>Domain-Driven Design: Tackling Complexity in the Heart of Software (2003)</strong>.</p>
<blockquote class="bilingual-quote"><div class="quote-translation" lang="ko"><p>Uma esfera de conhecimento, influência ou atividade.</p></div><div class="quote-original" lang="en"><p>"A sphere of knowledge, influence, or activity."</p></div></blockquote>
<p>Em termos simples, domínio é a própria <strong>área do problema que se pretende resolver por meio da programação</strong>. Se estamos criando um serviço de declaração de impostos, "declaração de impostos" é o domínio; se estamos criando uma plataforma de sinistros de seguros, "sinistros de seguros" é o domínio. O domínio não é código. É uma área de problemas do mundo real que existe antes do software.</p>
<p>O que isso significa para quem desenvolve frontend? A UI que criamos é, no fim das contas, uma <strong>janela (window)</strong> que permite apresentar esse domínio ao usuário e possibilitar sua manipulação. Se desenvolvemos serviços de restituição de impostos como Toss Income ou 3o3, cujo domínio principal é tributário, estamos representando na UI conceitos do domínio como tipo de renda, coeficiente de despesas, dedução da renda, crédito tributário e valor da restituição. Portanto, quem desenvolve frontend também precisa compreender profundamente o domínio com que trabalha. Isso significa que entender <strong>"qual problema este serviço resolve"</strong> é tão importante quanto construir bons componentes de UI.</p>
<p>Mas até um único domínio como "impostos" contém inúmeros subdomínios quando examinado por dentro. Basta olhar para o pipeline de cálculo do imposto de renda global, que conheço apenas superficialmente.</p>
<p><img src="/content/260418/1.png" alt="1.png" width="1090" height="566" loading="eager" fetchpriority="high" decoding="async"></p>
<p>Cada etapa desse pipeline é um subdomínio com regras e dados próprios. Dentro do grande domínio de "impostos", entrelaçam-se os subdomínios de renda (Income), deduções (Deduction), imposto (Tax) e declaração (Filing). Como dividi-los no código é justamente a questão central da modelagem de domínio.</p>
<h2 id="modelo-de-domínio-domain-model"><a class="anchor" href="#modelo-de-domínio-domain-model">Modelo de domínio (Domain Model)</a></h2>
<p>Então, o que é um modelo de domínio? Qual é a diferença entre domínio e "modelo de domínio"?</p>
<p>Martin Fowler e Eric Evans definem modelo de domínio da seguinte maneira.</p>
<blockquote class="bilingual-quote"><div class="quote-translation" lang="ko"><p>Um modelo de objetos do domínio que incorpora tanto comportamento quanto dados. — 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>Um sistema de abstrações que descreve aspectos selecionados de um domínio e pode ser usado para resolver problemas relacionados a esse domínio. — 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>O ponto central é a <strong>"abstração seletiva"</strong>. Um modelo de domínio não contém tudo o que existe no mundo real. Assim como um diretor de cinema não registra todas as cenas da realidade, mas escolhe apenas as necessárias para contar a história, o modelo de domínio também <strong>seleciona e estrutura apenas os aspectos necessários para resolver o problema</strong>.</p>
<p>Há um ponto importante aqui. Um modelo de domínio não precisa necessariamente ser código. Pode ser um diagrama desenhado em um quadro branco ou um modelo mental (Mental Model) compartilhado entre os integrantes da equipe. Em última análise, o próprio termo modelo de domínio pode designar um conceito independente do software.</p>
<p>Há uma parte que costuma confundir especialmente quem desenvolve frontend: olhar para a estrutura de uma resposta de API e pensar "este é o modelo de domínio". Mas isso é um <strong>modelo de dados (Data Model)</strong>, não um modelo de domínio.</p>
<p>Podemos distinguir modelo de dados e modelo de domínio da seguinte forma.</p>
<table>
<thead>
<tr>
<th>Critério</th>
<th>Modelo de domínio</th>
<th>Modelo de dados</th>
</tr>
</thead>
<tbody>
<tr>
<td>Objetivo</td>
<td>Expressar conceitos e regras de negócio</td>
<td>Definir a estrutura de armazenamento/transmissão</td>
</tr>
<tr>
<td>Linguagem</td>
<td>Termos de negócio (base tributável, crédito, restituição)</td>
<td>Termos técnicos (string, number, array)</td>
</tr>
<tr>
<td>Elementos</td>
<td>Dados + comportamento (regras)</td>
<td>Apenas a estrutura dos dados</td>
</tr>
<tr>
<td>Exemplo</td>
<td>"A faixa de até 14 milhões de won tem alíquota de 6%"</td>
<td><code>{ taxableBase: number, taxRate: number }</code></td>
</tr>
</tbody>
</table>
<p>O modelo de dados define "em que formato os dados circulam", enquanto <strong>o modelo de domínio define "o que esses dados significam para o negócio e quais regras seguem".</strong> Quando não distinguimos os dois, os componentes passam a depender diretamente da estrutura da resposta da API, e qualquer mudança no schema do backend acaba abalando todo o frontend.</p>
<h2 id="objeto-de-domínio-domain-object"><a class="anchor" href="#objeto-de-domínio-domain-object">Objeto de domínio (Domain Object)</a></h2>
<p>Se o modelo de domínio é um sistema de conceitos, o <strong>objeto de domínio</strong> é a concretização desse conceito em código.</p>
<p>Em um <a href="https://www.codewithjason.com/difference-domains-domain-models-object-models-domain-objects/" target="_blank" rel="noopener noreferrer">texto de Jason Swett</a>, responsável pelo Code with Jason, o objeto de domínio é definido assim.</p>
<blockquote class="bilingual-quote"><div class="quote-translation" lang="ko"><p>Eu chamaria de objeto de domínio qualquer objeto do meu modelo de objetos que também exista como conceito no meu modelo de domínio.</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>Ou seja, se existe o conceito de "renda global" no modelo de domínio e um tipo chamado <code>Income</code> no código, esse <code>Income</code> é um objeto de domínio. Mas nem todo objeto no código é um objeto de domínio. Elementos como <code>HttpClient</code>, <code>LocalStorageAdapter</code> e <code>useDebounce</code> são ferramentas técnicas, não conceitos do domínio.</p>
<h3 id="entity-e-value-object"><a class="anchor" href="#entity-e-value-object">Entity e Value Object</a></h3>
<p>Evans classifica os objetos de domínio em três categorias: <strong>Entity</strong>, <strong>Value Object</strong> e <strong>Service</strong>. (Martin Fowler chama essa divisão de "Evans Classification".) Service é um conceito separado que representa "uma operação de domínio que não pertence naturalmente a um objeto específico". Como o foco deste texto é a forma de identificar os dados, examinaremos principalmente Entity e Value Object.</p>
<p>Uma <strong>Entity</strong> é um objeto com identidade própria que persiste ao longo do tempo e entre diferentes representações. Uma declaração de impostos (TaxFiling), um contribuinte (Taxpayer) e um registro de renda (IncomeRecord) são identificados por um ID próprio; mesmo que seus atributos mudem, continuam sendo a mesma Entity se o ID for o mesmo. Ainda que os itens de dedução de uma declaração sejam alterados, ela continua sendo a mesma declaração enquanto seu ID não mudar.</p>
<p>Um <strong>Value Object</strong> é um objeto cujo significado decorre apenas da combinação de seus atributos; quando todos os atributos têm os mesmos valores, os objetos são considerados iguais. Dinheiro (Money), alíquota (TaxRate) e faixa tributária (TaxBracket) são objetos em que o próprio valor carrega o significado. Uma "alíquota de 6%" é simplesmente uma "alíquota de 6%", onde quer que seja usada.</p>
<p>Por que essa distinção é importante no frontend? Vejamos o exemplo de código abaixo.</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 é uma Entity porque usa o id como critério de identidade. (O simples fato de ter um campo id não define uma Entity; o ponto central é que "esse id determina se é o mesmo objeto ou outro".) Money é identificado apenas pela combinação de amount e currency, sem id, e é considerado o mesmo valor quando todos os seus atributos são iguais.</p>
<p>Entity é comparada por ID; Value Object, por atributos. Quando essa distinção está clara, a lógica de gerenciamento de estado que decide "se estes dados são iguais ou diferentes" se organiza naturalmente. Ao atualizar um item de uma lista, por exemplo, localizamos e substituímos uma Entity pelo ID, enquanto um Value Object é substituído de forma imutável (immutable replace).</p>
<h2 id="modelo-de-objetos-de-domínio-domain-object-model"><a class="anchor" href="#modelo-de-objetos-de-domínio-domain-object-model">Modelo de objetos de domínio (Domain Object Model)</a></h2>
<p>Já entendemos "modelo de domínio" e "objeto de domínio", mas o que é um <strong>modelo de objetos de domínio</strong>?</p>
<p>Ao pesquisar, descobri que, surpreendentemente, não há uma definição consensual. Grande parte da literatura trata "modelo de domínio", "modelo de objetos de domínio", "modelo conceitual (conceptual model)" e "modelo de objetos de análise (analysis object model)" como <strong>praticamente sinônimos</strong>. Segundo essa visão, são apenas nomes diferentes para o modelo conceitual elaborado durante a análise orientada a objetos.</p>
<p>Há, por outro lado, quem os veja como camadas um pouco mais separadas. Uma explicação representativa é que o <strong>modelo de objetos é justamente o ponto em que o modelo de domínio é transformado em código real</strong>.</p>
<p>Nessa segunda perspectiva, o <strong>modelo de objetos</strong> é a estrutura de <strong>todos os objetos de código</strong> do sistema. Isso inclui ferramentas técnicas como <code>HttpClient</code> e <code>useDebounce</code>. Dentro dele, o <strong>subconjunto dos objetos que representam conceitos do domínio e as relações entre eles</strong> constitui o <strong>modelo de objetos de domínio</strong>. Essa visão também se alinha à tradição da modelagem orientada a objetos, que define "Object Model" como a estrutura estática de um sistema (classes, atributos, operações e relações).</p>
<p>Considero essa perspectiva mais prática para quem desenvolve frontend. Afinal, no código que escrevemos, objetos de domínio e objetos técnicos estão sempre misturados.</p>
<p>No fim, <strong>domínio → modelo de domínio → modelo de objetos de domínio → objeto de domínio</strong> forma uma hierarquia que vai do abstrato ao concreto. O domínio é o mais amplo, e o objeto de domínio é o mais concreto. Por isso, ao escrever código frontend, a questão prática com que realmente lidamos é <strong>como estruturar o modelo de objetos de domínio — os tipos que representam conceitos do domínio e as relações entre eles</strong>.</p>
<h2 id="onde-a-lógica-de-domínio-deve-ficar-no-frontend"><a class="anchor" href="#onde-a-lógica-de-domínio-deve-ficar-no-frontend">Onde a lógica de domínio deve ficar no frontend?</a></h2>
<p>Encerradas as definições, passemos à prática. <strong>Onde</strong> a lógica de domínio deve ficar no frontend?</p>
<p><a href="https://khalilstemmler.com/about/" target="_blank" rel="noopener noreferrer">Khalil Stemmler</a>, que se interessa profundamente por design de software, primeiro defendeu que "a lógica de negócio não pertence ao frontend". Mais tarde, reviu sua posição e afirmou: "Podemos e devemos fazer no frontend quase tudo o que fazemos arquiteturalmente no backend."</p>
<p>Concordo com essa posição. É claro que o frontend não deve ser a <strong>fonte única da verdade (Single Source of Truth)</strong> da lógica de negócio. Esse é o papel do backend. Mas também existe, sem dúvida, <strong>lógica de domínio própria do frontend</strong>.</p>
<p>Pense no caso em que "é preciso mostrar em tempo real a restituição estimada com base nas informações inseridas pelo usuário". Se essa lógica de cálculo existir apenas no backend, será necessário chamar a API toda vez que o usuário corrigir um único caractere no valor da renda. A UI ficará parada durante o tempo de ida e volta pela rede e, se o usuário digitar rápido, o volume de solicitações desnecessárias crescerá de forma explosiva. Mesmo com debounce, um atraso de algumas centenas de milissegundos já é suficiente para comprometer a experiência de uma "prévia em tempo real". <strong>No fim, cálculos que exigem feedback imediato precisam ser executados diretamente pelo frontend, e passam a existir lógicas que só podem ser executadas nele.</strong></p>
<h3 id="quando-a-lógica-de-domínio-se-mistura-ao-componente"><a class="anchor" href="#quando-a-lógica-de-domínio-se-mistura-ao-componente">Quando a lógica de domínio se mistura ao componente</a></h3>
<p>Tomemos como exemplo uma tela de prévia do imposto de renda global. Nela, quando o usuário informa seus rendimentos, o imposto estimado é exibido em tempo real. Abaixo está um código comum em que a lógica de domínio e a lógica de UI estão misturadas.</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>Você consegue ver o problema desse código? <strong>Regras de negócio definidas pela legislação tributária</strong>, como "dedução pessoal de 1,5 milhão de won por pessoa", "alíquota progressiva em oito faixas" e "retenção na fonte de 3,3%", estão inseridas diretamente no componente React. A legislação tributária muda todos os anos; se essas regras estiverem espalhadas pelos componentes, será preciso caçar todos os lugares que devem ser corrigidos a cada revisão. E, se houver cenários E2E mantidos não só por quem desenvolve, mas também pela equipe de QA, o custo dos testes também não será pequeno.</p>
<p>Com isso, fica difícil distinguir lógica de View e lógica de negócio, e tudo acaba emaranhado em inúmeras condicionais e hooks customizados.</p>
<h3 id="vamos-separar-a-lógica-de-domínio"><a class="anchor" href="#vamos-separar-a-lógica-de-domínio">Vamos separar a lógica de domínio</a></h3>
<p>Tomemos emprestado o princípio central da abordagem de Clean Architecture de Alex Bespoyasov: separar a lógica de domínio em <strong>funções puras que não dependem de framework</strong>.</p>
<blockquote class="bilingual-quote"><div class="quote-translation" lang="ko"><p>O domínio é o núcleo que distingue uma aplicação de outra. Podemos pensar no domínio como algo que não mudaria se migrássemos do React para o 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>Vamos refatorar o exemplo de cálculo de impostos acima.</p>
<p>Primeiro, definimos os tipos e as regras do domínio para manter as informações relacionadas coesas.</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>Depois, separamos a lógica de domínio em funções puras.</p>
<p>Separamos na função <code>computeFullTax</code> as lógicas de cálculo de renda, deduções, base tributável, imposto e restituição mencionadas anteriormente. Cada etapa volta a ser dividida em pequenas funções puras. Se o tipo do resultado for inferido com <code>ReturnType&#x3C;typeof computeFullTax></code>, não será necessário declarar uma interface separada.</p>
<p>Depois disso, o componente apenas "usa" a lógica de domínio.</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>O que mudou?</p>
<ul>
<li>A <strong>tabela das oito faixas progressivas</strong> (<code>TAX_BRACKETS</code>) está reunida em um só lugar; quando a legislação mudar, basta alterar <code>domain/tax.ts</code>.</li>
<li>O <strong>pipeline de cálculo</strong> está coeso em uma única função, <code>computeFullTax</code>, permitindo visualizar o fluxo completo de uma vez. (Agrupamos tudo para manter o exemplo simples, mas, em um projeto real, convém subdividir ainda mais por finalidade, como cálculo de renda, cálculo de deduções e apuração do imposto.)</li>
<li>O <strong>componente se concentra apenas em "como exibir"</strong>. Mesmo que a alíquota mude, não é preciso alterar o componente.</li>
<li>Mesmo que haja uma migração do React para outro framework, <code>domain/tax.ts</code> <strong>não muda</strong>.</li>
</ul>
<p>Quando a lógica de domínio é separada, os testes se tornam surpreendentemente simples. Isso é especialmente importante no domínio tributário, pois <strong>a precisão dos cálculos é o próprio dinheiro do usuário</strong>.</p>
<p>Funções puras que contêm cálculos tributários não precisam de React Testing Library, <code>render</code> nem <code>screen.getByText</code>. Basta fornecer a entrada e verificar a saída. Casos como "alíquota de 6% até 14 milhões de won", "imposto igual a zero quando a base tributável é zero" e "restituição sobre uma renda de 30 milhões de won de um profissional autônomo" podem ser expressos em um <code>it</code> de uma única linha. Os testes unitários do domínio estabelecem naturalmente os critérios de separação dos componentes, e o código de teste ainda funciona como documentação.</p>
<h2 id="modelo-de-domínio-anêmico-anemic-domain-model"><a class="anchor" href="#modelo-de-domínio-anêmico-anemic-domain-model">Modelo de domínio anêmico (Anemic Domain Model)</a></h2>
<p>Na seção anterior, separamos a <strong>lógica de cálculo</strong>. Mas a lógica de domínio também inclui <strong>regras de transição de estado</strong> e <strong>decisões de permissão</strong>. Perguntas como "é possível editar esta declaração agora?", "ela pode ser enviada?" e "é possível mudar o tipo de solicitação?" fazem parte disso. Ao separar essas regras, é fácil cair em uma armadilha que Martin Fowler chamou de <strong>modelo de domínio anêmico (Anemic Domain Model)</strong>.</p>
<p>Um modelo de domínio anêmico é uma situação em que <strong>os tipos estão bem definidos na linguagem do domínio, mas as regras que operam sobre eles ficam espalhadas para fora do domínio</strong>. Vejamos o domínio de declaração de impostos (Filing). Os tipos estão bem organizados.</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>Mas as regras de decisão e transição desse tipo estão inseridas em outros lugares.</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>A mesma regra de domínio existe em três lugares — utils, componente e hook —, cada um com uma forma diferente. Se surgir um requisito dizendo "as condições para solicitação serão alteradas", será necessário procurar todos os pontos que precisam mudar; qualquer um que for esquecido passará a tomar uma decisão incorreta em algum lugar do site. Fowler criticou esse tipo de código afirmando que <strong>"ele não difere de código procedural revestido apenas com uma aparência orientada a objetos"</strong>.</p>
<p>A solução é a mesma aplicada à lógica de cálculo na seção anterior: <strong>colocar as regras ao lado do tipo</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>Agora, as regras relacionadas às declarações são gerenciadas em um único lugar: <code>domain/filing.ts</code>. Qualquer componente pode chamar <code>canAmend(filing)</code>, e, se a regra mudar, basta alterar esse arquivo. O ponto central é <strong>entender o tipo e as regras que operam sobre ele como um único conjunto</strong>. Uma separação parcial que coloca apenas o tipo na pasta de domínio e envia as regras para utils pode parecer organizada por fora, mas continua anêmica.</p>
<h2 id="camada-de-transformação-entre-a-resposta-da-api-e-o-modelo-de-domínio"><a class="anchor" href="#camada-de-transformação-entre-a-resposta-da-api-e-o-modelo-de-domínio">Camada de transformação entre a resposta da API e o modelo de domínio</a></h2>
<p>Há mais um ponto a considerar no trabalho real: a estrutura da resposta da API do backend nem sempre coincide com o modelo de domínio do frontend. Isso é ainda mais verdadeiro em um serviço tributário integrado a órgãos governamentais. Os dados de integração do Hometax, o serviço da Receita Nacional da Coreia, estão cheios de abreviações e códigos; dificilmente chegarão no mesmo formato do modelo de domínio do frontend.</p>
<p>É aí que entra uma <strong>camada de transformação (Mapper)</strong>. Em vez de deixar o tipo da resposta da API fluir diretamente até o componente, primeiro o refinamos para o tipo do domínio e só então o utilizamos. Uma única função pura é suficiente.</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>Assim, abreviações como <code>총수입금액</code> e <code>경비율</code> e classificações baseadas em códigos da resposta da API são transformadas <strong>em um só lugar</strong> para se adequar ao domínio do frontend. Valores como o código do tipo de renda, que precisam ser convertidos para enum, podem ser tratados com uma pequena lookup table dentro do mapper. Mesmo que o nome de um campo da API do Hometax mude, basta alterar um único mapper.</p>
<h2 id="funções-utilitárias-e-lógica-de-domínio"><a class="anchor" href="#funções-utilitárias-e-lógica-de-domínio">Funções utilitárias e lógica de domínio</a></h2>
<p>Ao separar a lógica de domínio, surge inevitavelmente uma pergunta: <strong>"isto não é uma função utilitária?"</strong></p>
<p>Observe as duas funções abaixo.</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> é uma lógica pura de <strong>apresentação (Presentation)</strong> que transforma um número em string. Acrescentar a unidade "won" e separar milhares por vírgulas não é uma regra de negócio, mas uma questão de como apresentar o valor ao usuário. Já <code>calculateTax</code> contém uma <strong>regra de negócio baseada na legislação tributária</strong>: a aplicação de oito faixas progressivas. É uma regra do domínio que precisa continuar igual mesmo sem UI.</p>
<p>Este é o critério que uso no trabalho:</p>
<blockquote>
<p><strong>Se essa lógica desaparecer, o negócio quebra ou apenas a tela quebra?</strong></p>
</blockquote>
<p>Se o negócio quebra, é lógica de domínio; se apenas a tela quebra, é lógica de apresentação. Essa única pergunta permite distinguir a maioria das fronteiras.</p>
<table>
<thead>
<tr>
<th>Critério</th>
<th>Lógica de domínio</th>
<th>Lógica utilitária/de apresentação</th>
</tr>
</thead>
<tbody>
<tr>
<td>O que quebra se ela não existir?</td>
<td>O cálculo do imposto fica incorreto</td>
<td>A tela (UI) fica estranha</td>
</tr>
<tr>
<td>E se o framework mudar?</td>
<td>Permanece igual</td>
<td>Pode mudar</td>
</tr>
<tr>
<td>Está especificada nos requisitos?</td>
<td>"base tributável × alíquota - dedução progressiva"</td>
<td>"valores separados por vírgulas"</td>
</tr>
<tr>
<td>Existe a mesma lógica no backend?</td>
<td>Existe ou deveria existir</td>
<td>Não (é uma preocupação só do frontend)</td>
</tr>
</tbody>
</table>
<p>Mas a realidade não é tão bem delimitada. O caso mais difícil é quando <strong>algo parece lógica de domínio, mas na verdade é lógica de apresentação</strong>.</p>
<p>Observe o código abaixo. Como recebe o conceito de domínio FilingStatus como argumento, ele foi classificado como lógica de domínio. Mas será que realmente é?</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>Embora <code>getStatusBadgeColor</code> e <code>getStatusDisplayText</code> usem o conceito de domínio <code>FilingStatus</code>, o que fazem é <strong>apresentação de tela</strong>. Se a cor do badge mudar, o negócio não quebra de forma alguma. Colocar essas funções em <code>domain/filing.ts</code> faz o módulo de domínio crescer cada vez mais e mistura a verdadeira lógica de domínio com a lógica de apresentação.</p>
<h3 id="separando-o-modelo-de-domínio-e-o-viewmodel"><a class="anchor" href="#separando-o-modelo-de-domínio-e-o-viewmodel">Separando o modelo de domínio e o ViewModel</a></h3>
<p>Há uma maneira prática de resolver esse problema: <strong>separar o ViewModel em outro arquivo dentro da mesma pasta de domínio</strong>. Em vez de <code>.ui.ts</code>, usar o nome <code>.viewModel.ts</code> cria uma conexão natural com o conceito de ViewModel do padrão MVVM. O próprio nome deixa evidente o papel de "camada que transforma os dados do domínio para a tela".</p>
<pre><code>domains/
└── filing/
    ├── filing.ts              # 순수 도메인 모델 + 도메인 로직
    ├── filing.viewModel.ts    # ViewModel (표현 변환 계층)
    ├── filing.test.ts         # 도메인 로직 테스트
    └── filingMapper.ts        # API ↔ 도메인 변환
</code></pre>
<p>Movemos <code>getStatusBadgeColor</code> e <code>getStatusDisplayText</code>, vistos anteriormente, diretamente para <code>filing.viewModel.ts</code>. Outras transformações, como <code>getFilingTypeLabel(type: FilingType): string</code>, que convertem o tipo de declaração em um rótulo em coreano, também ficam reunidas ali. <code>filing.ts</code> fica responsável apenas pelas regras de negócio; <code>filing.viewModel.ts</code>, apenas pela apresentação na tela.</p>
<p>O ponto central é a <strong>direção das dependências</strong>. <code>filing.viewModel.ts</code> importa <code>filing.ts</code>, mas <code>filing.ts</code> jamais importa <code>filing.viewModel.ts</code>. O domínio não conhece a apresentação; a apresentação conhece o domínio. Isso pode ser visto como uma versão em miniatura da regra de dependência (Dependency Rule) de Robert C. Martin.</p>
<p>Coloquei esses arquivos na mesma pasta porque acredito que arquivos que mudam juntos devem ficar no mesmo diretório. Se o tipo <code>FilingStatus</code> receber um novo valor (por exemplo, <code>'rejected'</code>), tanto <code>filing.ts</code> quanto <code>filing.viewModel.ts</code> precisarão ser alterados. Como estão na mesma pasta, o escopo da mudança fica visível de imediato.</p>
<h2 id="fronteiras-e-coesão"><a class="anchor" href="#fronteiras-e-coesão">Fronteiras e coesão</a></h2>
<p>Tão importante quanto separar a lógica de domínio é decidir <strong>onde traçar as fronteiras</strong>. A seguir, organizo alguns problemas de delimitação que encontro com frequência no trabalho.</p>
<p>Os dados tratados no frontend vêm, aproximadamente, de quatro fontes.</p>
<ul>
<li><strong>Dados do servidor</strong>: recebidos como resposta da API</li>
<li><strong>Dados derivados</strong>: calculados a partir dos dados do servidor</li>
<li><strong>Estado da UI</strong>: usado para controlar a tela e as interações do usuário</li>
<li><strong>Entrada do usuário</strong>: dados que estão sendo preenchidos em um formulário</li>
</ul>
<p>Misturar esses quatro tipos em um único tipo contamina o modelo de domínio.</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>Nesse tipo, conceitos do domínio, estado da UI e dados temporários estão todos no mesmo recipiente. Sempre que <code>activeStep</code> muda, é como se o domínio da declaração fosse atualizado. (A mudança de etapa de um formulário não é um evento de negócio.)</p>
<p>A solução é separar os tipos de acordo com suas fronteiras. O <strong>modelo de domínio</strong> contém apenas conceitos de negócio, como <code>id</code>, <code>status</code> e <code>determinedTax</code>; o <strong>estado da UI</strong> (<code>FilingFormViewState</code>) contém apenas controles da tela, como <code>isExpanded</code> e <code>activeStep</code>; e o <strong>estado do formulário</strong> (<code>DeductionEditForm</code>) contém apenas os dados temporários que estão sendo preenchidos.</p>
<p>Assim, cada tipo passa a ter <strong>um único motivo para mudar</strong>. O tipo de domínio só muda quando a legislação tributária muda; o estado da UI, apenas quando o design da tela muda; e o estado do formulário, apenas quando a UX de entrada muda.</p>
<h3 id="mantenha-junto-o-que-muda-junto"><a class="anchor" href="#mantenha-junto-o-que-muda-junto">Mantenha junto o que muda junto</a></h3>
<p>No DDD de Eric Evans, existe o conceito de <strong>Aggregate (agregado)</strong>: "tratar um cluster de objetos relacionados como uma única unidade". Não é necessário aplicar esse conceito literalmente no frontend, mas vale tomar emprestado seu princípio central: <strong>dados e regras que mudam juntos devem ficar juntos.</strong></p>
<p>Em um serviço tributário, por exemplo, <code>Income</code> (renda) e <code>ExpenseRate</code> (coeficiente de despesas) sempre mudam juntos. Quando o tipo de renda muda, o coeficiente de despesas aplicável também muda, o que afeta o cálculo da renda global. Portanto, eles devem ficar coesos em um único arquivo, <code>domain/tax.ts</code>.</p>
<p>Já <code>TaxFiling</code> (declaração) pode mudar independentemente do cálculo do imposto. Mesmo que as regras de transição de estado da declaração mudem, a lógica de cálculo das alíquotas não é afetada. Portanto, o correto é separá-la em <code>domain/filing.ts</code>.</p>
<pre><code>이렇게 묻자: "A가 변할 때 B도 반드시 변해야 하는가?"
  → Yes: 같은 모듈에 둔다 (Income + ExpenseRate + TaxBracket)
  → No: 분리한다 (Tax 계산 ↔ Filing 상태관리)
</code></pre>
<h2 id="class-vs-estilo-funcional"><a class="anchor" href="#class-vs-estilo-funcional">Class vs estilo funcional</a></h2>
<p>Depois de chegar até aqui, pode surgir uma pergunta fundamental. Todos os exemplos até agora usaram a combinação de <code>interface</code> com funções puras; a coesão não seria mais natural se representássemos o domínio com Class?</p>
<p>Sim. Quando representamos o domínio com Class, dados e comportamento ficam reunidos em um único objeto, e a coesão aparece diretamente na estrutura do código.</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>Quando usamos Class, o comportamento pertence aos dados. E o sujeito fica claro no ponto de uso. <code>filing.canAmend()</code> é intuitivo como a leitura de uma frase em linguagem natural. O sujeito (filing) e o verbo (canAmend) estão claramente combinados. É como escrever <code>jihoon.eat('감자탕')</code>: dá para ler imediatamente que "Jihoon come gamjatang".</p>
<p>No estilo funcional, por outro lado, fica assim.</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>No estilo funcional, os dados existem do lado de fora. Os dois códigos acima recebem os dados <code>filing</code> como argumento e executam uma operação. A função <code>eat</code> recebe os dados <code>jihoon</code> e <code>감자탕</code> como argumentos e é executada.</p>
<p>Com isso, a ligação entre sujeito e verbo fica mais frouxa. Para saber que a função <code>canAmend</code> está relacionada a <code>TaxFiling</code>, é necessário abrir o arquivo ou conferir a assinatura do tipo. Se funções como <code>canAmend(filing)</code>, <code>canEdit(filing)</code> e <code>calculateTax(taxableBase)</code> estiverem misturadas no mesmo arquivo, pode ficar difícil perceber de imediato a qual domínio cada função pertence.</p>
<h3 id="então-devemos-usar-class"><a class="anchor" href="#então-devemos-usar-class">Então devemos usar Class?</a></h3>
<p>Sinceramente, a resposta é <strong>"depende da situação"</strong>. Mas, segundo minha experiência, há motivos práticos para Class não ser uma solução universal em ambientes React + TypeScript.</p>
<p><strong>1. Atrito com o gerenciamento de estado do React</strong></p>
<p>O gerenciamento de estado do React combina de forma mais natural com <strong>Plain Object</strong>. Tecnicamente, <code>useState</code> e <code>useReducer</code> podem conter qualquer valor, e o Redux DevTools não remove por si só o protótipo de uma instância de Class. Porém, quando o middleware de persistência de Redux/Zustand salva e restaura o estado como JSON, uma instância de Class perde seus métodos e seu protótipo no ciclo de <code>JSON.stringify</code> → <code>JSON.parse</code> e se degrada em plain object. A fronteira de props entre React Server Component e Client Component tem outra restrição: aceita apenas valores serializáveis (serializable) compatíveis, portanto uma instância arbitrária de Class não pode atravessá-la.</p>
<p>Observe o código abaixo.</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>Atualizar o estado do React, por si só, não faz <code>filing</code> deixar de ser uma instância de <code>TaxFilingModel</code>. Porém, se a persistência de Redux/Zustand o salvar e restaurar como JSON, o valor recuperado pode ser um plain object sem métodos, e uma chamada desatenta a <code>filing.canAmend()</code> pode provocar um erro em runtime. Ao transmiti-lo de React Server Component para Client Component, a falha acontece antes, porque uma instância de Class não é um valor serializável de props compatível.</p>
<p><strong>2. Dificuldade de garantir imutabilidade</strong></p>
<p>O React detecta mudanças de estado com base em <strong>igualdade referencial (referential equality)</strong>. Se um método de uma instância de Class fizer uma mutação interna como <code>this.items.push(...)</code>, a referência permanecerá a mesma e o React não disparará uma nova renderização. Por isso, no fim, é preciso escrever <code>addDeduction(item)</code> de modo que retorne uma nova instância toda vez, como em <code>return new DeductionList([...this.items, item])</code>; isso esvazia a vantagem da Class de oferecer uma "mudança de estado encapsulada". O código deixa de ser muito diferente de uma atualização funcional.</p>
<h3 id="estratégias-para-obter-coesão-no-estilo-funcional"><a class="anchor" href="#estratégias-para-obter-coesão-no-estilo-funcional">Estratégias para obter coesão no estilo funcional</a></h3>
<p>Então, como melhorar no estilo funcional o problema da coesão frouxa visto em <code>eat('jihoon', '감자탕')</code>? Apresento três métodos que considero eficazes.</p>
<p><strong>1. Obter coesão com namespace de módulo</strong></p>
<p>É o método mais intuitivo. Transformamos o próprio arquivo (módulo) em uma unidade de domínio e usamos um namespace na importação. Basta usar diretamente o <code>domain/filing.ts</code> que definimos anteriormente.</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> não é tão conciso quanto <code>filing.canAmend()</code>, mas ao menos torna evidente no próprio código que a função pertence ao domínio de Filing. Também elimina o risco de misturar funções de vários domínios.</p>
<p><strong>2. Padronizar o primeiro argumento como sujeito do domínio</strong></p>
<p>Há outra convenção para expressar coesão no estilo funcional: <strong>o primeiro argumento é sempre o "sujeito da ação"</strong>. Ao padronizar as assinaturas como <code>canAmend(filing)</code> e <code>calculateTotalIncome(income)</code>, <code>canAmend(filing)</code> passa a ser lido como "perguntar canAmend sobre filing". Isso também se alinha à mentalidade de pipeline do Unix (<code>data |> transform</code>). Na verdade, o receiver de método da linguagem Go segue exatamente esse padrão, e, em um bloco <code>impl</code> do Rust, receber <code>self</code> como primeiro argumento parte da mesma ideia.</p>
<p><strong>3. Agrupar comportamentos em uma função de criação de objeto de domínio (Factory)</strong></p>
<p>Esse padrão pode ser usado quando sentimos falta da coesão de uma Class. Uma função Factory retorna de uma só vez o objeto de domínio e seus comportamentos.</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>Esse padrão reúne a expressividade da Class (<code>filing.canAmend()</code>) e a praticidade de compor comportamentos com um objeto literal. Como o objeto retornado contém propriedades que são funções, ele não é, por si só, um dado serializável em JSON. Também há o custo de criar novos objetos de função a cada vez, mas isso raramente se torna um problema de desempenho no volume de dados tratado pelo frontend.</p>
<h2 id="até-que-ponto-devemos-separar"><a class="anchor" href="#até-que-ponto-devemos-separar">Até que ponto devemos separar?</a></h2>
<p>Ao ler sobre Clean Architecture, encontramos uma estrutura ideal que divide três ou quatro camadas e define Port/Adapter. Mas aplicar essa estrutura a todos os projetos pode se tornar engenharia excessiva (over-engineering).</p>
<p>Estes são os critérios práticos que considero adequados.</p>
<ul>
<li><strong>Separe o tipo do domínio do tipo da resposta da API.</strong> Seja com <code>interface</code> ou <code>type</code>, defina em um arquivo separado os conceitos do domínio usados pelo frontend.</li>
<li><strong>Retire do componente toda lógica que contenha regras de negócio.</strong> Ela não precisa estar em uma pasta <code>domain/</code>. O importante é transformá-la em uma função pura que não dependa do React.</li>
<li><strong>Faça a transformação resposta da API → modelo de domínio em um único lugar.</strong> Pode ser uma função Mapper ou um schema Zod; crie uma estrutura em que a mudança não se propague, pois basta alterar esse único ponto.</li>
</ul>
<p>Se o projeto se tornar mais complexo, também vale considerar as situações abaixo.</p>
<ul>
<li><strong>Divida as pastas por Bounded Context.</strong> O <a href="https://frontend-fundamentals.com/" target="_blank" rel="noopener noreferrer">capítulo de frontend da Toss</a> também enfatiza o princípio de "colocar no mesmo diretório os arquivos que mudam juntos". Quando as pastas são divididas por domínio, os caminhos de import revelam naturalmente as fronteiras do domínio.</li>
<li><strong>Introduza uma camada de Use Case.</strong> Quando a composição da lógica de domínio se torna complexa, passa a ser necessária uma camada Application que reúna em uma única função um cenário como "consultar informações de renda → aplicar o coeficiente de despesas → calcular os itens de dedução → apurar o imposto → confirmar a restituição".</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>Mesmo dentro de um único domínio tributário, <strong>cálculo de imposto (tax)</strong>, <strong>gestão de declarações (filing)</strong> e <strong>itens de dedução (deduction)</strong> são separados como subdomínios independentes. Uma mudança nas alíquotas não afeta a lógica de transição de estado das declarações; a adição de um item de dedução não altera o fluxo de envio da declaração. Essa é uma aplicação prática de Bounded Context.</p>
<h2 id="conclusão"><a class="anchor" href="#conclusão">Conclusão</a></h2>
<p>Em resumo, <strong>domínio</strong> é a área do problema que queremos resolver; <strong>modelo de domínio</strong> é o sistema conceitual que abstrai seletivamente esse problema; <strong>modelo de objetos de domínio</strong> é a implementação desse sistema conceitual em código; e <strong>objeto de domínio</strong> é cada objeto individual dentro dessa implementação.</p>
<p>Colocar esses conceitos em prática no frontend não significa simplesmente dividir pastas, mas <strong>avaliar conscientemente várias camadas de fronteiras</strong>. "Isto é regra de negócio ou lógica de apresentação?" "Estes dados são estado do domínio ou estado da UI?" "Esta função tem coesão suficiente?" Só o hábito de fazer essas perguntas já melhora naturalmente a estrutura do código.</p>
<p>É claro que nem todo projeto precisa de todas as camadas da Clean Architecture. Dividir um aplicativo CRUD simples em quatro camadas e aplicar o padrão Factory a todos os domínios pode criar mais estrutura do que valor. Entre a coesão elegante da Class e a flexibilidade prática do estilo funcional, a resposta correta depende da complexidade do projeto e do contexto da equipe.</p>
<p>Não existe uma resposta única. Mas há uma diferença clara entre <strong>"escrever código sem saber o que é o domínio"</strong> e <strong>"reconhecer o domínio, avaliar suas fronteiras e separá-lo de forma consciente"</strong>. Espero que este texto também incentive você a perguntar, ao menos uma vez, em seu próprio projeto: "qual é o domínio aqui, e onde este código deveria ficar?"</p>
<h3 id="referências"><a class="anchor" href="#referências">Referências</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">토스, E2E 자동화 여정</a></div>
</div>
</div></details>]]></content:encoded>
            <author>jihoon7705@gmail.com (이지훈)</author>
            <category>프론트엔드</category>
            <category>아키텍처</category>
            <category>DDD</category>
        </item>
        <item>
            <title><![CDATA[Reflexões sobre a refatoração do 2º simulado do Toss Frontend Fundamentals]]></title>
            <link>https://hooninedev.com/pt-BR/260328</link>
            <guid isPermaLink="false">https://hooninedev.com/pt-BR/260328</guid>
            <pubDate>Sat, 28 Mar 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Neste post, quero contar como foi minha experiência de refatoração durante a 2ª edição do simulado do Toss Frontend Fundamentals. Como sempre tive interesse por revisão de código e refatoração, decidi...]]></description>
            <content:encoded><![CDATA[<p>Neste post, quero contar como foi minha experiência de refatoração durante a 2ª edição do simulado do Toss Frontend Fundamentals.</p>
<p>Como sempre tive interesse por revisão de código e refatoração, decidi encarar esse desafio da Toss, que tinha um formato bem interessante. A tarefa consistia em refatorar um aplicativo de reserva de salas de reunião fornecido pela organização. Como o projeto também vinha acompanhado de testes, havia uma rede de segurança para verificar se alguma funcionalidade havia sido quebrada durante a refatoração.</p>
<p>No fim, trabalhei na refatoração durante dois dias e quero registrar aqui o que percebi ao longo desse processo.</p>
<h2 id="meu-primeiro-contato-com-o-código"><a class="anchor" href="#meu-primeiro-contato-com-o-código">Meu primeiro contato com o código</a></h2>
<p>A primeira coisa que fiz ao abrir o código foi <strong>ler as especificações dos testes</strong>. Afinal, os testes são a documentação que mostra com mais honestidade o que a aplicação deve fazer. Passei por <code>App.easy.spec.tsx</code> e <code>App.hard.spec.tsx</code> para entender os requisitos gerais da aplicação.</p>
<p>Em seguida, examinei o código em si e dois componentes monolíticos chamaram minha atenção.</p>
<ul>
<li><code>ReservationStatusPage</code> era um componente com cerca de 400 linhas que reunia, em um único arquivo, seleção de data, visualização da timeline, tooltip com os detalhes da reserva, lista das minhas reservas e funcionalidade de cancelamento.</li>
<li><code>RoomBookingPage</code> era um componente com cerca de 300 linhas no qual filtros, lista de salas, lógica de criação de reservas e sincronização dos parâmetros da URL estavam todos entrelaçados.</li>
</ul>
<p>Enquanto lia o código, antes mesmo de concluir que ele "precisava melhorar", concentrei-me em <strong>classificar suas características</strong>. A ideia era distinguir o que continha informações de domínio, o que tinha natureza utilitária e o que pertencia puramente à camada de UI.</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>Depois dessa classificação, começou a ficar claro por onde eu deveria começar. Deixei comentários breves em cada área do código para anotar possíveis direções de melhoria. (A sensação era parecida com a que tive ao entrar na empresa atual e migrar um projeto baseado em jquery.)</p>
<p>Mas, afinal, por onde começar?</p>
<h2 id="definição-da-estratégia-de-refatoração"><a class="anchor" href="#definição-da-estratégia-de-refatoração">Definição da estratégia de refatoração</a></h2>
<p>Planejei executar a refatoração na seguinte ordem:</p>
<ol>
<li><strong>Tratamento do código de servidor</strong>: separar query e mutation</li>
<li><strong>Separação da lógica de domínio</strong>: modelos Equipment, Room e Reservation</li>
<li><strong>Declaração de tipos</strong>: organizar o sistema de tipos com base nos modelos de domínio</li>
<li><strong>Separação das funções utilitárias</strong>: formatação de data, cálculos da timeline etc.</li>
<li><strong>Separação da camada de UI</strong>: dividir os componentes em unidades coerentes por responsabilidade</li>
<li><strong>Abstração e separação de responsabilidades</strong>: tratamento de erro/carregamento e gerenciamento de query keys</li>
</ol>
<p>Escolhi essa ordem para avançar <strong>de fora para dentro na direção das dependências</strong>. Primeiro, organizei a infraestrutura — código de servidor e utilitários —, depois estabeleci os modelos de domínio e, por fim, refinei a UI. Se eu separasse os componentes de UI antes, poderia acabar tendo de mover entre vários componentes uma lógica de domínio e um código de query que ainda não estavam organizados.</p>
<p>Com a estratégia definida, era hora de colocá-la em prática, etapa por etapa.</p>
<h2 id="começando-pelo-código-de-servidor-e-pelos-utilitários"><a class="anchor" href="#começando-pelo-código-de-servidor-e-pelos-utilitários">Começando pelo código de servidor e pelos utilitários</a></h2>
<h3 id="extração-do-utilitário-de-exibição-de-data"><a class="anchor" href="#extração-do-utilitário-de-exibição-de-data">Extração do utilitário de exibição de data</a></h3>
<p>Comecei pela função <code>formatDate</code>, pois ela estava definida inline, de forma idêntica, nas duas páginas.</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>Embora fosse uma mudança pequena, ela teve um significado importante como primeiro commit da refatoração. Foi uma espécie de <strong>aquecimento</strong>: começar pela parte mais independente e com menos efeitos colaterais para confirmar que os testes continuavam passando.</p>
<h3 id="extração-dos-hooks-do-react-query"><a class="anchor" href="#extração-dos-hooks-do-react-query">Extração dos hooks do React Query</a></h3>
<p>Depois, extraí para arquivos separados as chamadas a <code>useQuery</code> e <code>useMutation</code> que estavam escritas diretamente dentro dos componentes. Usei o padrão <code>queryOptions</code> para transformar as configurações das queries em unidades reutilizáveis.</p>
<p>Nesse processo, também defini explicitamente os tipos das respostas da API que estavam em <code>remotes.ts</code>. Tipos que antes se propagavam como <code>any</code> passaram a ficar claros, como <code>GetRoomsResponse</code> e <code>GetReservationsResponse</code>.</p>
<p>Com a camada de infraestrutura organizada, voltei minha atenção para os modelos de domínio.</p>
<h2 id="separação-dos-modelos-de-domínio"><a class="anchor" href="#separação-dos-modelos-de-domínio">Separação dos modelos de domínio</a></h2>
<p>O ponto de virada mais importante da refatoração foi <strong>separar os modelos de domínio em um diretório <code>models/</code> próprio</strong>.</p>
<p>No código original, constantes de negócio como <code>EQUIPMENT_LABELS</code> e <code>TIME_SLOTS</code> estavam declaradas no topo dos arquivos de componentes. Os tipos de <code>Room</code> e <code>Reservation</code> também existiam apenas no handler do servidor (<code>_tosslib/server/types.ts</code>), enquanto no código do cliente eram usados praticamente como <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>Por que separar os modelos de domínio é tão importante? Quando a lógica de negócio depende de um componente de UI, qualquer alteração nessa lógica exige que se examine também a lógica de renderização do componente. Quando ela existe de forma independente no diretório <code>models/</code>, porém, as regras de negócio podem mudar separadamente da UI. É claro que, na prática, uma separação perfeita é difícil, mas o essencial é ao menos criar uma estrutura na qual seja possível prever que <strong>"essa lógica estará aqui"</strong>.</p>
<p>Com os modelos de domínio separados, até que ponto a UI poderia ficar mais leve?</p>
<h2 id="decomposição-dos-componentes"><a class="anchor" href="#decomposição-dos-componentes">Decomposição dos componentes</a></h2>
<h3 id="reservationstatuspage"><a class="anchor" href="#reservationstatuspage">ReservationStatusPage</a></h3>
<p>Esse foi o commit que produziu a mudança mais drástica e também o que mais consumiu tempo. Dividi o componente monolítico de 385 linhas da seguinte forma:</p>
<pre><code>ReservationStatusPage/
├── index.tsx                    # 페이지 레벨
└── components/
    ├── DateSelector.tsx         # 날짜 선택 UI
    ├── ReservationTimeline.tsx  # 타임라인
    └── MyReservation.tsx        # 내 예약 목록 + 취소
</code></pre>
<p>O critério para a separação foi: <strong>"este código tem significado por si só?"</strong> A visualização da timeline é uma responsabilidade independente, que recebe os dados das reservas de uma data e desenha uma grade. A lista das minhas reservas é outra responsabilidade independente, que consulta os dados de reserva do usuário e permite cancelá-las. Não havia motivo para que ambas ficassem no mesmo arquivo.</p>
<p>Depois da separação, <code>index.tsx</code> passou a exercer apenas o papel de <strong>orquestrador (orchestrator)</strong>. Ele ficou responsável pelo gerenciamento de estado, pela exibição de mensagens e pela composição dos componentes filhos, enquanto o fetching de dados e os detalhes de renderização foram delegados a esses componentes.</p>
<h3 id="roombookingpage"><a class="anchor" href="#roombookingpage">RoomBookingPage</a></h3>
<p>Separei a página de reservas seguindo o mesmo princípio.</p>
<pre><code>RoomBookingPage/
├── index.tsx                    # 페이지 레벨
├── components/
│   ├── BookingFilter.tsx        # 날짜, 시간, 인원, 장비, 층 UI
│   └── AvailableRoomList.tsx    # 예약 가능 방 목록
└── hooks/
    └── useBookingParams.ts      # URL searchParams 기반 상태 관리
</code></pre>
<p>Durante esse processo, fiz uma escolha interessante. No início, tentei introduzir <code>react-hook-form</code> + <code>zod</code> para validar o formulário. No fim, porém, removi essa abordagem e a substituí pelo hook customizado <code>useBookingParams</code>. Falarei dessa decisão em mais detalhes adiante.</p>
<p>Neste ponto, surge naturalmente uma pergunta: até onde devemos abstrair?</p>
<h2 id="o-nível-adequado-de-abstração"><a class="anchor" href="#o-nível-adequado-de-abstração">O nível adequado de abstração</a></h2>
<p>Esta foi a questão sobre a qual mais refleti durante o simulado.</p>
<h3 id="até-onde-devemos-decompor-condicionais-aninhadas"><a class="anchor" href="#até-onde-devemos-decompor-condicionais-aninhadas">Até onde devemos decompor condicionais aninhadas?</a></h3>
<p>A lógica que determina se uma sala está disponível para reserva combina várias condições: se a capacidade é suficiente, se há os equipamentos necessários, se o andar preferido corresponde e se os horários não se sobrepõem. No código original, todas essas condições estavam escritas inline dentro de um único callback de <code>filter</code>.</p>
<p>Ao extrair essa lógica para <code>models/roomFilter.ts</code>, separei cada condição em uma <strong>função com nome próprio</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>O ponto central aqui é que <strong>só extraí uma função quando havia um nome claro para a abstração</strong>. Nomes como <code>isEnoughCapacity</code> e <code>hasRequiredEquipment</code> permitem prever o comportamento sem olhar a implementação. Se o nome inevitavelmente ficasse vago, como <code>processRoomConditions</code>, a abstração poderia, na verdade, aumentar a carga cognitiva de quem lê.</p>
<p>Isso não significa, é claro, que essa seja a única resposta correta. Meu critério foi: <strong>"é possível prever o comportamento apenas pelo nome da função?"</strong> Se sim, vale abstrair; caso contrário, manter inline pode até favorecer a legibilidade.</p>
<h3 id="searchparams-vs-estado-do-formulário"><a class="anchor" href="#searchparams-vs-estado-do-formulário">searchParams vs. estado do formulário</a></h3>
<p>Também pensei bastante sobre onde manter o estado dos filtros da reserva. No código original, cada valor de filtro era gerenciado com <code>useState</code> e sincronizado com os searchParams da URL por meio de <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>Primeiro, tentei introduzir <code>react-hook-form</code> + <code>zod</code> para gerenciar os filtros como um formulário. No fim, porém, removi essa solução e a substituí pelo hook <code>useBookingParams</code>, que usa os <strong>searchParams como única fonte da verdade (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>A principal razão para essa decisão foi a conclusão de que <strong>"não fazia sentido que os estados evoluíssem separadamente"</strong>. Quando <code>useState</code> e <code>searchParams</code> mantêm estados próprios, podem surgir divergências dependendo do momento da sincronização. Quando apenas os searchParams são usados como estado, por outro lado, a URL passa a ser o próprio estado da aplicação e o problema de sincronização simplesmente desaparece. De quebra, se o usuário compartilhar a URL, o mesmo estado dos filtros será reproduzido.</p>
<p>Encontrei reflexões parecidas nos relatos de outros participantes: <strong>"unifiquei os searchParams da URL como única fonte da verdade" e "optei por reunir as props individuais dos filtros em um único objeto <code>filter</code>"</strong>. As formas de expressar a solução variavam, mas a percepção do problema era a mesma: <strong>"estados dispersos precisam ser reunidos em um único conceito"</strong>.</p>
<h2 id="estabilidade"><a class="anchor" href="#estabilidade">Estabilidade</a></h2>
<h3 id="suspense-e-errorboundary"><a class="anchor" href="#suspense-e-errorboundary">Suspense e ErrorBoundary</a></h3>
<p>Depois de definir a estrutura dos componentes, adicionei o tratamento de erro e carregamento. A ordem é importante porque só é possível decidir onde estabelecer cada boundary depois que a árvore de componentes está definida.</p>
<p>Usando a biblioteca <code>react-error-boundary</code>, envolvi cada unidade independente de fetching de dados com <code>ErrorBoundary</code> e <code>Suspense</code>. Isso porque, mesmo se a timeline falhar, a lista das minhas reservas deve continuar aparecendo normalmente — e 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="gerenciamento-centralizado-de-query-keys"><a class="anchor" href="#gerenciamento-centralizado-de-query-keys">Gerenciamento centralizado de Query Keys</a></h3>
<p>À medida que os hooks de query eram separados durante a refatoração, surgiu o problema de as query keys ficarem espalhadas por vários arquivos. Com isso, ficou difícil rastrear qual key deveria ser usada para invalidation no <code>onSuccess</code> de uma mutation.</p>
<p>Introduzi <code>@lukemorales/query-key-factory</code> para centralizar o gerenciamento das query keys.</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>Assim, é possível usar a forma <code>useSuspenseQueries({ queries: [roomKeys.list, reservationKeys.list(date)] })</code>, mantendo a query key e a função de fetching sempre juntas. Também extraí os caminhos das routes para a constante <code>PATHS</code>, eliminando strings hardcoded.</p>
<h2 id="qual-era-a-intenção-dos-autores-do-desafio"><a class="anchor" href="#qual-era-a-intenção-dos-autores-do-desafio">Qual era a intenção dos autores do desafio?</a></h2>
<p>Depois de concluir a refatoração, dei um passo atrás e me perguntei: o que este simulado pretendia avaliar?</p>
<p>Ao ler os relatos de outros participantes, encontrei um ponto em comum interessante. Em quase todos aparecia a frase: <strong>"código não é lido, é previsto"</strong>. Nosso cérebro não interpreta o código linha por linha; ele o lê fazendo previsões com base nos padrões acumulados pela experiência. Quando essas previsões falham, a carga cognitiva aumenta de forma abrupta.</p>
<p>Sob essa perspectiva, o simulado não avaliava apenas a capacidade de programar, mas uma competência de colaboração: <strong>"até que ponto você consegue tornar o código previsível para seus colegas?"</strong>. (Talvez a verdadeira competência de um engenheiro de software seja justamente ler a mente dos autores do desafio e dos colegas.)</p>
<p>Ao examinar os relatos de outros participantes, identifiquei-me com observações como <strong>"não é fácil entender o código escrito por outra pessoa" e "projetar a interface primeiro é importante, mas essa abordagem pode vacilar diante de uma base de código extensa"</strong>. Passei por algo parecido. Quando o código existente já funciona, surge a tentação de racionalizar sua estrutura: "se já está funcionando, para que mexer?". Mas o ponto central do simulado era justamente superar essa tentação e avaliar <strong>"com que rapidez outra pessoa, que não eu, conseguiria entender este código e se eu seria capaz de julgar o problema com meu próprio raciocínio e avançar até resolvê-lo"</strong>.</p>
<h2 id="o-que-aprendi-com-a-refatoração"><a class="anchor" href="#o-que-aprendi-com-a-refatoração">O que aprendi com a refatoração</a></h2>
<p><strong>A ordem da refatoração determina o resultado.</strong> Avançar de fora — infraestrutura — para dentro — UI — foi um caminho seguro, que evitou emaranhados no meio do processo. Ao separar os componentes depois de organizar os utilitários e os modelos de domínio, ficou claro de que cada componente dependia.</p>
<p><strong>O critério para uma abstração é seu "nome".</strong> Se, ao extrair algo para uma função ou variável, o nome consegue explicar o comportamento, vale a pena abstrair. Se o nome inevitavelmente for vago, manter inline pode ser uma escolha melhor.</p>
<p><strong>A localização do estado é a própria arquitetura.</strong> Estados que precisam se mover juntos devem ficar no mesmo lugar. Em vez de sincronizar <code>useState</code> e <code>searchParams</code>, usar apenas os searchParams como fonte da verdade produz uma estrutura mais saudável.</p>
<h2 id="conclusão"><a class="anchor" href="#conclusão">Conclusão</a></h2>
<p>Depois de terminar a tarefa, conversei com dois colegas. Coisas que eu não percebia ao examinar o código sozinho começaram a aparecer quando desenvolvemos nossas ideias em conjunto. No instante em que alguém pergunta "por que você fez assim?" sobre uma escolha estrutural que eu havia considerado óbvia, tornam-se visíveis as lacunas de raciocínio das quais eu nem sequer tinha me dado conta.</p>
<p>É verdade que a IA está reduzindo drasticamente o tempo necessário para escrever e revisar código. Ainda assim, experiências como essa explicam por que considero code reviews e reuniões diárias tão importantes. A IA pode verificar a consistência do código, mas apontar <strong>"esta é a perspectiva que você deixou passar"</strong> continua sendo papel de colegas que compartilham o mesmo contexto. Descobrir o que eu não consegui enxergar e, a partir dessa descoberta, estabilizar o produto: talvez essa seja a essência da colaboração.</p>
<p>Não existe uma resposta única para escrever código durante o processo de resolução de um problema. Outros participantes do mesmo simulado seguiram caminhos diferentes, cada um com suas próprias razões. O importante é <strong>conseguir explicar "por que foi feito assim"</strong>. Recomendo que os leitores deste texto também tentem olhar para o próprio código pelos olhos de quem o vê pela primeira vez. Essa perspectiva pode ser o critério mais poderoso para determinar a qualidade do código.</p>]]></content:encoded>
            <author>jihoon7705@gmail.com (이지훈)</author>
            <category>프론트엔드</category>
            <category>React</category>
            <category>리팩토링</category>
        </item>
        <item>
            <title><![CDATA[Engenheiro frontend na era da IA]]></title>
            <link>https://hooninedev.com/pt-BR/260302</link>
            <guid isPermaLink="false">https://hooninedev.com/pt-BR/260302</guid>
            <pubDate>Mon, 02 Mar 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Neste post, quero falar, a partir de uma perspectiva pessoal, sobre como engenheiros podem crescer e sobreviver na era da IA. Um dos textos que mais me marcaram no início da carreira foi “Plano de car...]]></description>
            <content:encoded><![CDATA[<p>Neste post, quero falar, a partir de uma perspectiva pessoal, sobre <strong>como engenheiros podem crescer e sobreviver na era da IA</strong>.</p>
<p>Um dos textos que mais me marcaram no início da carreira foi <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">“Plano de carreira para engenheiros frontend: três trilhas de especialização para profissionais juniores”, de Hwidong Bae</a>. O artigo organiza a carreira de engenharia frontend em três trilhas — <strong>especialização em web (Software Engineer) / especialização em produto (Product Engineer) / especialização em operações (Full-Stack Engineer)</strong> — e ainda aborda as “cinco competências fundamentais de um engenheiro excepcional” e os “três pontos para se tornar sênior”. Naquela época, a grande questão era decidir quais competências desenvolver em cada trilha. Mas, menos de dois anos depois de eu ler o texto, a própria questão mudou por completo.</p>
<p>Hoje, quando converso com colegas de engenharia, percebo que as preocupações têm um tom um pouco diferente das que eu vinha ouvindo nos últimos anos.</p>
<ul>
<li>“A empresa adotou IA e, quando entregamos um layout, ela faz quase tudo. É prático, mas...”</li>
<li>“O mercado de contratação está muito frio.”</li>
<li>“Dá medo de simplesmente fazer merge do código escrito pela IA, mas revisar tudo um por um reduz a eficiência. Não sei bem como equilibrar isso.”</li>
</ul>
<p>Passei — e ainda passo — por uma fase parecida. Há apenas um ou dois anos, eu via a IA como “uma boa ferramenta de apoio”; hoje, chegamos a um ambiente em que é difícil até imaginar desenvolver sem ela (inclusive pedi ao Claude que fizesse pesquisas enquanto escrevia este texto). Este artigo funciona como uma espécie de continuação do texto de Hwidong Bae: quero organizar, do meu ponto de vista, como o cenário mudou nesse intervalo e quais outras competências precisamos desenvolver como engenheiros frontend diante dessa nova realidade.</p>
<p>Mais uma vez, procurei consultar e verificar o máximo possível de fontes. Ainda assim, como esta é uma área que muda muito rápido, peço desde já a compreensão de vocês caso parte do conteúdo já esteja desatualizada quando este texto for publicado. Se houver contrapontos ou assuntos que mereçam debate, fiquem à vontade para deixar um comentário.</p>
<h2 id="agora-a-ia-não-faz-tudo"><a class="anchor" href="#agora-a-ia-não-faz-tudo">“Agora a IA não faz tudo?”</a></h2>
<p>Antes de tudo, precisamos esclarecer uma coisa. A frase “a IA faz tudo” é verdadeira? Até que ponto ela é verdade e a partir de onde passa a ser fantasia?</p>
<p>Em fevereiro de 2025, <a href="https://x.com/karpathy/status/1886192184808149383" target="_blank" rel="noopener noreferrer">Andrej Karpathy</a>, cofundador da OpenAI e ex-diretor de IA da Tesla, publicou a seguinte frase no Twitter:</p>
<blockquote class="bilingual-quote"><div class="quote-translation" lang="ko"><p>Existe um novo tipo de programação que chamo de “vibe coding”: você se entrega completamente à vibe, abraça o crescimento exponencial e esquece até que o código existe.</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>Em poucas palavras, <strong>vibe coding</strong> é “uma forma de programar em que você entrega o teclado à IA e apenas descreve, em linguagem natural, o que deseja”. Não há documentação de arquitetura, boilerplate nem busca por ponto e vírgula. O código simplesmente funciona na vibe. Em menos de um ano, o termo entrou no vocabulário corrente das comunidades de desenvolvimento de língua inglesa.</p>
<p>Exatamente um ano depois, em fevereiro de 2026, o mesmo Karpathy <a href="https://thenewstack.io/vibe-coding-is-passe/" target="_blank" rel="noopener noreferrer">recuou um pouco</a>. Ele propôs substituir o termo vibe coding por <strong>“agentic engineering”</strong>. A diferença entre os dois é clara.</p>
<ul>
<li><strong>Vibe coding</strong>: descrever o que se deseja e aceitar o resultado</li>
<li><strong>Agentic engineering</strong>: projetar o sistema, especificar as restrições e usar a IA para acelerar uma implementação cujo raciocínio já foi concluído mentalmente</li>
</ul>
<p>Se, um ano atrás, a premissa era “é só pedir que ela faz tudo”, agora “a capacidade de planejar o que pedir à IA e como pedir” se consolidou como uma competência de engenharia. E essa tendência não se resume ao tweet de uma única pessoa. Na mesma época, o engenheiro do Google <a href="https://addyosmani.com/" target="_blank" rel="noopener noreferrer">Addy Osmani</a> publicou o livro <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>, afirmando categoricamente: “A IA é apenas uma assistente, não uma programadora em quem se possa confiar de forma autônoma. Você é o desenvolvedor sênior, e o LLM existe para acelerar o seu julgamento.”</p>
<h3 id="as-ferramentas-estão-avançando-sem-freio"><a class="anchor" href="#as-ferramentas-estão-avançando-sem-freio">As ferramentas estão avançando sem freio</a></h3>
<p>O ecossistema de ferramentas também evolui rapidamente para acompanhar essa tendência. Em maio de 2026, as ferramentas de programação mais mencionadas são Cursor, Claude Code, GitHub Copilot, Windsurf, v0 by Vercel, Bolt.new e Devin.</p>
<p>A transformação do v0 é especialmente emblemática. A Vercel usa a expressão <a href="https://venturebeat.com/infrastructure/vercel-rebuilt-v0-to-tackle-the-90-problem-connecting-ai-generated-code-to" target="_blank" rel="noopener noreferrer">“90% problem”</a>, que significa que 90% do desenvolvimento no mundo real acontece dentro de bases de código e infraestruturas existentes. No início, bastava ao v0 criar bons protótipos greenfield; agora, ele importa diretamente repositórios do GitHub para trabalhar, impõe o uso do design system e obtém automaticamente as variáveis de ambiente de implantação. É a resposta direta do ecossistema de ferramentas ao contraponto dos profissionais seniores: “A IA não serve apenas para criar demos que parecem brinquedos?”</p>
<p>As bases de código das big techs são a melhor demonstração dessa mudança.</p>
<p>Sundar Pichai, do Google, <a href="https://fortune.com/2024/10/30/googles-code-ai-sundar-pichai/" target="_blank" rel="noopener noreferrer">anunciou na teleconferência de resultados do terceiro trimestre, em outubro de 2024, que “mais de 25% do código novo era gerado por IA e depois revisado e aprovado por engenheiros”</a>; em abril de 2025, afirmou que a proporção havia superado 30%. Satya Nadella, da Microsoft, <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">revelou na LlamaCon, em abril de 2025, que “até 30% do nosso código é escrito por IA”</a>. Na Meta, a meta interna chegou ao ponto de “até o primeiro semestre de 2026, 65% dos engenheiros gerarem com IA pelo menos 75% dos próprios commits”.</p>
<p>Na Coreia do Sul, a tendência não é diferente. A <a href="https://toss.tech/article/toss-frontend-ai-docs" target="_blank" rel="noopener noreferrer">Toss</a> criou um sistema de documentação baseado em IA para melhorar a DX e eliminar a necessidade de desenvolvedores procurarem documentos, e foi além ao abordar temas como <a href="https://toss.tech/article/removing_designers_in_ai_era" target="_blank" rel="noopener noreferrer">“O que aconteceu quando eliminamos os designers na era da IA”</a>. A Danggeun compartilha experimentos de cada equipe toda terça-feira no <a href="https://medium.com/daangn" target="_blank" rel="noopener noreferrer">AI Show &#x26; Tell</a> e passou a usar o <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">slogan de recrutamento</a> “de engenheiro a builder”. Já a Woowa Brothers vem publicando textos como <a href="https://techblog.woowahan.com/22828/" target="_blank" rel="noopener noreferrer">“Na era em que a IA escreve código, você ainda quer ser desenvolvedor?”</a>, com a mensagem de que “a essência do trabalho de desenvolvimento não está no código, mas na capacidade de definir e resolver problemas”.</p>
<h3 id="mas-os-números-contam-uma-história-um-pouco-diferente"><a class="anchor" href="#mas-os-números-contam-uma-história-um-pouco-diferente">Mas os números contam uma história um pouco diferente</a></h3>
<p>Vendo apenas isso, é fácil chegar à conclusão de que “agora basta pedir que tudo fica pronto”. Mas os dados reais contam uma história um pouco diferente.</p>
<p>Comecemos pelos números da <a href="https://survey.stackoverflow.co/2025/ai" target="_blank" rel="noopener noreferrer"><strong>2025 Stack Overflow Developer Survey</strong></a>, uma análise abrangente do estado do desenvolvimento de software.</p>
<ul>
<li>84% dos desenvolvedores disseram que usam ou pretendem usar ferramentas de IA. (Um aumento em relação aos 76% de 2024.)</li>
<li>Entre os desenvolvedores profissionais, 51% usam ferramentas de IA todos os dias.</li>
<li>No entanto, <strong>a percepção positiva sobre as ferramentas de IA caiu</strong>. Depois de superar 70% em 2023 e 2024, chegou a 60% em 2025.</li>
<li>Desenvolvedores seniores com mais de dez anos de experiência são os que menos confiam nos resultados produzidos por IA.</li>
</ul>
<p>Em resumo: <strong>“Todo mundo usa, mas confia cada vez menos.”</strong></p>
<p>Um experimento realizado em 2025 pela organização sem fins lucrativos <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> evidencia de forma ainda mais dramática a distância entre essa percepção e a realidade. Foi um experimento controlado com 16 desenvolvedores open source experientes — em média, cinco anos de experiência e 1.500 commits —, aos quais foram atribuídas 246 tarefas, com a permissão para usar IA definida aleatoriamente. Os resultados foram os seguintes.</p>
<ul>
<li>Antes de começar, os desenvolvedores previram que “seriam 24% mais rápidos usando IA”.</li>
<li>Logo após concluir as tarefas, ainda avaliaram que “parecia ter sido cerca de 20% mais rápido”.</li>
<li>Mas a medição real mostrou que eles ficaram <strong>19% mais lentos</strong>.</li>
</ul>
<p>As causas apontadas pelos pesquisadores são interessantes. A taxa de aceitação do código gerado pela IA ficou abaixo de 44%; mesmo o código rejeitado exigiu tempo de revisão e testes; e até o código aceito demandou um tempo considerável de revisão e ajustes. Essa ilusão de ter ficado mais rápido mesmo ficando mais lento — essa lacuna — é uma das razões pelas quais desenvolvedores seniores têm se tornado cada vez mais céticos em relação à IA.</p>
<p>Além disso, a própria “qualidade do código escrito pela IA” está longe de ser impecável. Vejamos o experimento da <a href="https://www.veracode.com/blog/genai-code-security-report/" target="_blank" rel="noopener noreferrer"><strong>Veracode</strong></a>, que pediu a mais de cem modelos de IA que escrevessem código.</p>
<ul>
<li><strong>45% do código gerado por IA continha vulnerabilidades do OWASP Top 10</strong>.</li>
<li><strong>A taxa de falha na proteção contra XSS (cross-site scripting) foi de 86%</strong>.</li>
<li>A taxa de falha na proteção contra injeção de logs (Log Injection) foi de 88%.</li>
<li>Outro estudo relatou que a densidade de vulnerabilidades do código de IA era <strong>2,7 vezes maior</strong> que a do código humano.</li>
</ul>
<p>Em particular, a taxa de 86% de falha em XSS — um tema diretamente ligado ao frontend — merece ser levada ainda mais a sério. Esse número mostra bem o que significa fazer merge, sem alterações, de um form input escrito pela IA. (Quem tem experiência com auditoria de segurança de frontend já se sente desconfortável e preocupado até ao escrever <code>dangerouslySetInnerHTML</code> com as próprias mãos; quando a IA o insere discretamente, parece ainda mais assustador.)</p>
<p>Os sinais são semelhantes quanto à qualidade. A <a href="https://www.gitclear.com/ai_assistant_code_quality_2025_research" target="_blank" rel="noopener noreferrer"><strong>GitClear</strong></a> analisou 211 milhões de linhas alteradas entre 2020 e 2024 e chegou aos seguintes resultados.</p>
<ul>
<li>Proporção de código revertido em até duas semanas após ser escrito (Code Churn): 5,5% em 2020 → <strong>7,9%</strong> em 2024</li>
<li>Proporção correspondente a refatoração: 25% em 2021 → <strong>menos de 10%</strong> em 2024</li>
<li>Proporção de copiar e colar (clones): 8,3% em 2021 → <strong>12,3%</strong> em 2024 (em 2025, houve um aumento de nada menos que quatro vezes)</li>
</ul>
<p>A interpretação não é tão difícil. A capacidade de produzir código rapidamente aumentou, mas a capacidade de escrever código que valha a pena aprimorar diminuiu. Os <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">dados da Apiiro</a>, baseados em empresas da Fortune 50, são ainda mais contundentes. Desenvolvedores que usam assistência de IA produzem de três a quatro vezes mais commits que seus colegas, mas também geram dez vezes mais findings de segurança. Os caminhos de escalada de privilégios (privilege escalation) dispararam 322%, e as falhas de projeto arquitetural, 153%.</p>
<h2 id="o-que-a-ia-substituiu-e-o-que-não-conseguiu-substituir"><a class="anchor" href="#o-que-a-ia-substituiu-e-o-que-não-conseguiu-substituir">O que a IA substituiu e o que não conseguiu substituir</a></h2>
<p>As ferramentas avançam sem freio, mas os números são ambíguos. Então, o que exatamente a IA substituiu e o que ainda não conseguiu substituir? Só com essa distinção clara conseguimos enxergar onde devemos investir nosso tempo.</p>
<p>O que foi substituído foi parte do trabalho de digitação manual dos desenvolvedores. Há menos situações em que precisamos escrever boilerplate ou código repetitivo; com apenas um layout, uma tela que respeita as convenções pode ficar pronta em poucos minutos; e tanto o tempo gasto pesquisando sintaxe e APIs quanto a curva de aprendizado caíram drasticamente. Em suma, a IA <strong>nivelou a “velocidade de produção”</strong>.</p>
<p>Mas, no campo do “julgamento”, ela ainda não nos substituiu. (Para ser mais preciso, seria melhor dizer que “ainda não correspondeu às expectativas”. Embora a capacidade de usar IA varie de pessoa para pessoa, aqui parto da experiência média de uso.)</p>
<p>O primeiro obstáculo é <strong>traduzir requisitos em especificações</strong>. Transformar necessidades de negócio ambíguas em casos de borda precisos e máquinas de estado ainda exige uma intervenção humana mais profunda. O mesmo vale para <strong>compreender impactos no sistema como um todo</strong>: mesmo quando a IA oferece respostas plausíveis para perguntas como o impacto de um componente no bundle, se uma dependência permite tree shaking ou como um padrão de data fetching afeta, nos <a href="https://web.dev/articles/vitals" target="_blank" rel="noopener noreferrer">Core Web Vitals</a>, a pontuação de <a href="https://web.dev/articles/inp" target="_blank" rel="noopener noreferrer">INP (Interaction to Next Paint)</a>, só ficamos tranquilos depois que uma pessoa confere mais uma vez.</p>
<p>Também não podemos deixar de lado <strong>segurança e avaliação de riscos</strong>, como mostra o problema dos 45% de vulnerabilidades do OWASP visto acima. O mesmo se aplica à <strong>manutenção do design system e da consistência</strong>, que exige verificar se um novo componente está alinhado aos tokens, às regras de acessibilidade e aos padrões de interação do sistema existente, e à <strong>compreensão do contexto do cliente e do mercado</strong>, que demanda perguntar por que uma funcionalidade é necessária e em qual fluxo do usuário ela deve ser inserida.</p>
<p>Por fim, tomando emprestada a expressão usada no texto de <a href="https://yceffort.kr/2026/02/frontend-engineering-in-ai-era" target="_blank" rel="noopener noreferrer">yceffort</a>, se chamarmos de “diferença entre a complexidade do sistema e o grau em que a equipe o compreende”, a <strong>gestão da dívida cognitiva (Cognitive Debt)</strong> é uma área em que essa distância se amplia ainda mais rapidamente após a adoção da IA. Portanto, reduzir essa lacuna continua sendo responsabilidade das pessoas.</p>
<blockquote>
<p>Não são os desenvolvedores que desaparecem, mas a forma do trabalho que eles faziam. O gargalo passou da “velocidade de criação” para a “velocidade de decisão”.</p>
</blockquote>
<p>No mesmo contexto, <a href="https://toss.tech/article/will-ai-replace-developers" target="_blank" rel="noopener noreferrer">“Os desenvolvedores serão substituídos pela IA?”, da Toss</a>, oferece um diagnóstico mais contundente. A ideia central é a seguinte: a IA não está substituindo toda a força de trabalho; está eliminando a escada de aprendizagem (apprenticeship ladder). Daqui a dez ou vinte anos, quando os atuais profissionais seniores se aposentarem, faltarão pessoas da próxima geração capazes de projetar sistemas complexos. Não é um problema da ordem de “o que faremos com as contratações da nossa empresa no ano que vem?”, mas uma espécie de bomba-relógio com efeito retardado para todo o setor. (Acho que é um texto realmente bem escrito para um período de tantas incertezas.)</p>
<p>A “primeira versão que funciona” criada pela IA corresponde a 70%. Os 30% necessários para chegar a uma “versão que pode ser entregue a usuários reais” são território humano. E a capacidade de preencher esses 30% não surge da noite para o dia. Essa é a essência do problema da escada de aprendizagem. Se desaparece o tempo de “pôr a mão na massa” escrevendo boilerplate e componentes simples, também desaparecem as pessoas capazes de completar esses 30%.</p>
<p>O texto original de Hwidong Bae apontava como “cinco competências fundamentais de um engenheiro excepcional” a <strong>escrita de bom código, a maximização do valor atual (equilibrando rapidez de lançamento e manutenção de longo prazo), a tomada de decisões baseada em dados, o apoio eficaz às decisões dos colegas e o aprendizado contínuo</strong>. Todas as cinco continuam válidas na era da IA, mas a última delas ocupa a posição mais vulnerável. O aprendizado em si não desapareceu; o objeto do aprendizado mudou. Antes, aprendíamos “como usar esta ferramenta”; agora, precisamos dedicar tempo a aprender “como este sistema inteiro funciona”. Mais assustador ainda é o <a href="https://evan-moon.github.io/2026/04/18/developers-who-stopped-growing-in-ai-era/" target="_blank" rel="noopener noreferrer">problema apontado por Evan Moon</a>: “no momento em que a IA assume a escrita do código, a carga cognitiva do cérebro cai drasticamente”. Reduzir a carga cognitiva parece bom, mas é perigoso porque essa carga era justamente a matéria-prima do aprendizado. <strong>Quanto mais cômodo, menos se cresce.</strong></p>
<p>Daí surge naturalmente uma pergunta. Então as três trilhas do texto de Hwidong Bae — especialização em web, produto e operações — deixaram de fazer sentido?</p>
<p>Penso diferente. As trilhas continuam válidas. O mais correto é entender que cada uma evoluiu um estágio para se adaptar à era da IA. Vejamos como o cenário mudou em cada uma delas.</p>
<h2 id="de-produtor-a-validador"><a class="anchor" href="#de-produtor-a-validador">De produtor a “validador”</a></h2>
<p>No texto original de Hwidong Bae, a trilha de especialização em web era agrupada sob o nome <strong>Software Engineer</strong>. Seus pontos centrais eram “uma compreensão profunda e o domínio da internet, dos navegadores e de HTML/CSS/JS”, o conhecimento dos pontos fortes e fracos das ferramentas do ecossistema web, a experiência com troubleshooting e uma postura atenta a novas tecnologias. Como caminhos para chegar a sênior, o texto sugeria <strong>engenheiro de empresas que desenvolvem ferramentas para o ecossistema web / educador de frontend / tech lead em organizações com produtos complexos</strong>. Em poucas palavras, são “pessoas que investigam a fundo os princípios de funcionamento dos navegadores e de HTML/CSS/JS”; até um ou dois anos atrás, sua principal arma era “ser capaz de escrever código com mais precisão que qualquer outra pessoa”.</p>
<p>Como o valor dessas pessoas mudou na era da IA? Em termos apenas de velocidade para escrever código, a IA já as alcançou. Porém, <strong>“a capacidade de avaliar com precisão o código escrito pela IA”</strong> é algo que elas praticamente monopolizam.</p>
<ul>
<li>Pessoa sem formação técnica que usa IA: a implementação atende aos meus requisitos e funciona normalmente.</li>
<li>Desenvolvedor que usa IA: funciona, mas esta dependência pode causar certos problemas, e melhorar este padrão desta maneira está mais de acordo com as convenções. Vamos rever as partes relacionadas.</li>
</ul>
<p>No estudo da Veracode abordado acima, vimos taxas de falha de 86% em XSS e 88% em injeção de logs. As pessoas capazes de identificar e corrigir esses problemas são justamente especialistas como nós. Elas evoluem naturalmente para uma função sênior de controle de qualidade (QA) dos resultados produzidos pela IA.</p>
<p>Além disso, surgiu um tema inteiramente novo no território desses especialistas: <strong>UI generativa (Generative UI)</strong> e <strong>design de interfaces de IA</strong>. Alguns exemplos são interfaces de chat que exibem respostas do LLM por streaming, controles de abort para interromper a geração, renderização progressiva de Markdown e blocos de código, UX que exibe inline os resultados de chamadas de ferramentas e integrações de assistentes por meio do <a href="https://sdk.vercel.ai/" target="_blank" rel="noopener noreferrer">Vercel AI SDK</a> ou do <a href="https://modelcontextprotocol.io/" target="_blank" rel="noopener noreferrer">MCP (Model Context Protocol)</a>. A demanda por “pessoas que conhecem com precisão os princípios de funcionamento da web e, ao mesmo tempo, entendem as características dos LLMs e sabem aplicá-las” está explodindo nessa área.</p>
<h2 id="a-evolução-natural-para-product-engineer"><a class="anchor" href="#a-evolução-natural-para-product-engineer">A evolução natural para Product Engineer</a></h2>
<p>A trilha de especialização em produto foi a mais beneficiada. Quem conhece profundamente o mercado e os clientes e se comunica com frequência com stakeholders externos ganhou uma ferramenta muito mais poderosa ao incorporar a IA. Outra característica dessa trilha era a possibilidade de expandir a carreira para outras funções, com caminhos seniores como <strong>engenheiro de growth ou consultor / transição para PM, PO ou CPO</strong>.</p>
<p>Uma mudança interessante é que o nome dessa trilha começou a se consolidar como padrão global. O texto original já a chamava de “Product Engineer”, mas, quando o li, a expressão ainda me parecia pouco familiar. Um ano depois, ela se estabeleceu a ponto de a <a href="https://leerob.com/product-engineers" target="_blank" rel="noopener noreferrer">Vercel substituir “Fullstack Engineer” por “Product Engineer” em todas as descrições de cargo</a>.</p>
<p>Lee Robinson aponta três qualidades essenciais de um Product Engineer.</p>
<ul>
<li><strong>Foco em iteração (Iteration)</strong>: percorre rapidamente o ciclo de implantação → feedback → ajuste.</li>
<li><strong>Centralidade no cliente</strong>: conversa diretamente com clientes para melhorar o produto.</li>
<li><strong>Pragmatismo</strong>: “toda escolha tecnológica é apenas um meio”. Ferramentas que não contribuem para o objetivo do produto são abandonadas sem hesitação.</li>
</ul>
<p>Há uma armadilha aqui: é perigoso enxergar o engenheiro especializado em produto apenas como “quem faz rápido”. Com a chegada da IA, esse risco aumentou. Afinal, “implementar funcionalidades rapidamente” agora é algo que profissionais de qualquer área podem fazer com ferramentas de IA. O diferencial de um Product Engineer está na “capacidade de definir com precisão o problema do cliente e validá-lo rapidamente com a menor solução possível”, não em “ser rápido com as mãos”.</p>
<p>Nesse movimento, <strong>Design Engineer</strong> começou a ganhar status de cargo formal. A Vercel está contratando <a href="https://cjroth.com/blog/2026-02-18-building-an-elite-engineering-culture" target="_blank" rel="noopener noreferrer">engenheiros de design em uma trilha oficial com salários acima de US$ 200 mil</a>, e Linear e Stripe avançam em uma direção semelhante. É uma função que elimina o próprio handoff entre frontend e design. Como a IA desenha rapidamente, as competências necessárias para lidar ao mesmo tempo com “o que desenhar” e “se o resultado está de acordo com um design system consistente” ficaram ainda mais escassas.</p>
<h2 id="orquestrador-de-ia"><a class="anchor" href="#orquestrador-de-ia">Orquestrador de IA</a></h2>
<p>A trilha de especialização em operações é a que passa pela transformação mais drástica. No texto original, Hwidong Bae a classificava como <strong>Full-Stack Engineer</strong> e a definia como “uma pessoa muito interessada em estrutura, integração, testes e implantação de projetos, capaz de lidar diretamente com APIs e infraestrutura simples, preencher lacunas na organização e melhorar processos”. Nos últimos um ou dois anos, <strong>a função de operar os próprios agentes de IA</strong> foi acrescentada a essa base, ampliando rapidamente o alcance da trilha.</p>
<p>Ao resumir as <a href="https://beyond.addy.ie/2026-trends/" target="_blank" rel="noopener noreferrer">tendências de 2026</a>, ele apontou o conceito de <strong>“orquestração de agentes de programação (Orchestrating Coding Agents)”</strong> como um dos pontos centrais. Isso significa ir além de dar ordens a uma única IA: trata-se de projetar e operar um sistema em que vários agentes de IA colaboram simultaneamente. No mesmo contexto, ele propôs um framework chamado <a href="https://github.com/addyosmani/agent-skills" target="_blank" rel="noopener noreferrer">“agent-skills”</a>, e também vem ganhando espaço a ideia de codificar diretamente na lógica de funcionamento dos agentes os workflows profissionais, quality gates e melhores práticas do setor.</p>
<p>Depois de reunir materiais relacionados, estes são, na minha visão, os novos termos com que engenheiros da trilha de operações precisarão lidar.</p>
<ul>
<li><strong>MCP (Model Context Protocol)</strong>: padrão proposto pela Anthropic para conectar LLMs a ferramentas externas</li>
<li><strong>Governança de IA</strong>: gestão de quem pode usar IA, com qual contexto, e garantia de que secrets não sejam expostos</li>
<li><strong>Avaliação de agentes (Evaluation)</strong>: pipeline que pontua automaticamente os resultados produzidos pelos agentes</li>
<li><strong>Gate de IA</strong>: validação automática de segurança e qualidade antes do merge de um PR e rotulagem de código de IA</li>
</ul>
<p>O texto original indicava como caminhos seniores dessa trilha cargos como <strong>engenheiro de equipe de plataforma em grandes organizações / tech lead / agile coach / technical program manager (TPM) / CTO</strong>. Esses caminhos continuam válidos, mas agora podemos acrescentar novas posições, como <strong>“líder de infraestrutura de desenvolvimento com IA”</strong> e <strong>“engenheiro de produtividade de desenvolvimento (DevProd)”</strong>.</p>
<p>Enquanto as três trilhas evoluem cada uma à sua maneira, há competências que se tornaram mais importantes em todas elas. Eu queria pensar inicialmente em um horizonte de cinco anos, mas, diante do ritmo atual de avanço, até a unidade de um ano parece longa demais. Por isso, vou limitar o horizonte a algo como “o próximo ano” e destacar as competências que, na minha visão, ganharão mais importância.</p>
<h2 id="cinco-competências"><a class="anchor" href="#cinco-competências">Cinco competências</a></h2>
<p><strong>A primeira é a capacidade de escrever especificações (Specification).</strong> Na era da IA, o “ponto de partida da programação” não é o teclado, mas a <strong>especificação</strong>. A capacidade de registrar com precisão o que pedir à IA se tornou mais importante que o próprio código. Aqui, “especificação” não significa necessariamente um documento RFC grandioso. Pode ser um <strong>teste</strong> que expressa em código o comportamento esperado da lógica de negócio, uma história do <strong>Storybook</strong> que organiza os cenários e o contrato visual de um componente de UI ou uma <strong>definição de tipos</strong> que explicita o contrato do fluxo de dados. No fim, é o trabalho de estabelecer previamente critérios para validar de forma automática o resultado criado pela IA. Quando programamos com IA sem essa base, os problemas se acumulam.</p>
<p><strong>A segunda é a capacidade de validação e discernimento.</strong> A IA produz com confiança código plausível, porém incorreto. Por isso, considero que “a capacidade de revisar código de IA com rapidez e precisão” se tornou essencial. É preciso identificar se foram esquecidos headers de segurança, sanitização de inputs ou tokens CSRF; se a acessibilidade continua funcionando — ARIA, navegação por teclado e focus trap —; e se há problemas nos impactos de desempenho, como custo de renderização, memória e tamanho do bundle. Jogar AI slop em um PR sem revisão é negligenciar a própria função como engenheiro. Quem aperta o botão de merge ainda é uma pessoa, e essa responsabilidade não pode ser transferida para a IA. O fato de os profissionais seniores apresentarem a menor confiança em IA na pesquisa do Stack Overflow provavelmente se deve, no fim das contas, a terem olhos treinados para encontrar justamente esses detalhes.</p>
<p><strong>A terceira é a compreensão de sistemas e o pensamento arquitetural.</strong> A IA lida bem com um arquivo por vez e tem grande capacidade de perceber fluxos e relações. Ela corrige sintomas rapidamente, mas um desenvolvedor competente encontra a causa raiz. Uma forma de desenvolver essa competência é realizar atividades deliberadas, como Architecture Retrospectives. Como o código muda mais rápido, se não elevarmos conscientemente a compreensão que a equipe tem do sistema, a dívida cognitiva se acumulará depressa.</p>
<p><strong>A quarta é a capacidade de orquestrar IA.</strong> A competência de lidar com a própria IA também está se separando em um conjunto específico de habilidades. Já não se trata simplesmente de “escrever bons prompts”, mas de uma área que exige tratar como um todo a capacidade de dividir o trabalho em tickets pequenos, escolher qual modelo usar para cada tarefa, projetar pipelines de avaliação e validação de agentes e definir estratégias de recuperação (rollback) em caso de falha. <a href="https://sourcegraph.com/blog/revenge-of-the-junior-developer" target="_blank" rel="noopener noreferrer">Steve Yegge</a> organiza essa evolução em <strong>seis waves (traditional → completions → chat → coding agents → agent clusters → agent fleets)</strong>.</p>
<p><strong>A quinta é Context Engineering.</strong> É um conceito que <a href="https://www.faros.ai/blog/context-engineering-for-developers" target="_blank" rel="noopener noreferrer">Karpathy e Tobi Lütke, CEO da Shopify, começaram a promover juntos em meados de 2025</a>. Em poucas palavras, trata-se da “capacidade de planejar qual contexto mostrar à IA, em que formato e em qual quantidade”. Na prática, aparece de formas como o trabalho com <strong>arquivos CLAUDE.md / rules</strong>, que registra convenções do projeto, princípios de arquitetura e proibições em locais acessíveis à IA; a <strong>redução intencional do contexto</strong>, que seleciona apenas os módulos relevantes em vez de inserir todos os arquivos no contexto; a <strong>separação explícita de etapas</strong>, que divide planejamento → implementação → validação em sessões distintas para evitar a contaminação do contexto; e o <strong>contexto externo via MCP</strong>, que conecta, por meio de interfaces padronizadas, fontes externas como design systems, schemas de API e dados de monitoramento. A <a href="https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents" target="_blank" rel="noopener noreferrer">documentação oficial da Anthropic</a> chama isso de “the new prompt engineering” e afirma categoricamente que um único prompt jamais consegue conter o conhecimento arquitetural, os padrões e a tribal wisdom de um sistema. Em outras palavras, “projetar um ambiente em que a IA sempre receba um bom contexto” se tornou muito mais importante do que “escrever um bom prompt de uma só vez”.</p>
<p>Se você chegou até aqui, uma pergunta surge naturalmente: então, concretamente, como estudar? Os métodos que uso são, em linhas gerais, quatro.</p>
<h2 id="como-estudar"><a class="anchor" href="#como-estudar">Como estudar</a></h2>
<p>O item “aprendizado contínuo” citado no texto original continua válido, mas precisamos mudar <strong>a distribuição do tempo de estudo</strong>.</p>
<p>Há áreas às quais dedicávamos muito tempo e que agora podem receber menos. Por outro lado, também há áreas complexas que antes evitávamos por serem difíceis ou demoradas. Entre estas últimas estão a escrita de especificações de testes, o uso de ferramentas de medição de desempenho — <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> e Chrome DevTools Performance —, acessibilidade (<a href="https://www.w3.org/WAI/standards-guidelines/wcag/" target="_blank" rel="noopener noreferrer">WCAG</a>) e segurança, especialmente o <a href="https://owasp.org/www-project-top-ten/" target="_blank" rel="noopener noreferrer">OWASP Top 10</a>. Também há áreas completamente novas a aprender, como Vercel AI SDK, LangChain.js, MCP, padrões de UI com streaming e pipelines de avaliação de agentes. <strong>É importante reconhecer quais competências eu preciso desenvolver e distribuir meu tempo de acordo com isso.</strong></p>
<p>O código produzido por IA tende a crescer demais. Ela gera centenas de linhas por minuto. Por isso, se não gerenciarmos conscientemente o tamanho dos PRs e a frequência de merge, o próprio code review entra em colapso. Dentro das empresas, após a adoção da IA, o tamanho médio dos PRs aumentou 18%, os incidentes por PR subiram 24% e a taxa de falha de mudanças cresceu 30%. Considerando também os dados discutidos acima, quando escrevemos grandes blocos e fazemos merge de tudo de uma vez, <strong>fica difícil compreender o fluxo e refletir a intenção; por isso, é importante granularizar as unidades de trabalho.</strong></p>
<p>Há uma prática diretamente ligada ao problema da redução da carga cognitiva apontado no texto de Evan Moon: é bom reservar uma ou duas horas por dia para escrever código sem IA. Desenhar a arquitetura à mão ou ler diretamente, linha por linha, código de uma área pouco familiar são alguns exemplos. (Eu também tento programar sem IA todos os dias naquele período sonolento depois do almoço. É um tempo que reservo para não me afastar de uma familiaridade antiga.)</p>
<p>Isso não serve apenas para “não esquecer o jeito antigo”. A sua própria profundidade deixa de crescer durante o tempo em que a IA faz o trabalho por você. Competências como validação e discernimento e compreensão do sistema são função do tempo que você passou enfrentando os problemas diretamente.</p>
<h2 id="portanto-nós"><a class="anchor" href="#portanto-nós">Portanto, nós</a></h2>
<p>Depois de tudo isso, a verdade é que o perfil do engenheiro frontend que sobrevive na era da IA não é tão diferente da conclusão apresentada no texto original. Estes eram os três pontos que ele destacava sobre bons engenheiros seniores.</p>
<ul>
<li>Procura permanecer <strong>fiel aos fundamentos</strong>. (Mantém e fortalece continuamente as cinco competências fundamentais.)</li>
<li>Mesmo sem ser o líder formal, exerce influência natural por meio de um comportamento exemplar.</li>
<li>Não se satisfaz apenas em concluir bem o trabalho recebido; observa o contexto anterior e posterior e gera um impacto maior.</li>
</ul>
<p>Aplicando isso à perspectiva da era da IA, temos o seguinte.</p>
<ul>
<li>Mantém-se fiel aos <strong>fundamentos</strong> que vão além do código produzido pela IA: web, sistemas e domínio.</li>
<li>Define a direção por conta própria, não a IA. Mesmo sem ser a pessoa formalmente responsável, decide “para onde devemos ir”.</li>
<li>Não usa a IA apenas como ferramenta de produtividade pessoal; usa-a para eliminar gargalos da equipe e do sistema.</li>
</ul>
<p>Ao examinar os textos de Andrej Karpathy, autoridade em OpenAI, vemos que a essência do <strong>“agentic engineering”</strong> que ele enfatiza agora é, no fim, a mesma: projetar o sistema, especificar as restrições e usar a IA para acelerar uma implementação cujo raciocínio já foi concluído mentalmente. As ferramentas mudam, mas o controle da direção continua nas mãos das pessoas.</p>
<p>A mensagem final do texto original também era que se torna sênior “quem não se satisfaz apenas em concluir bem o trabalho recebido, mas observa o contexto anterior e posterior e gera um impacto maior”. Na era da IA, apenas a definição desse “impacto” mudou. Há quem faça merge de uma tela produzida pela IA em uma hora pensando “funciona, então está pronto”; e há quem passe mais trinta minutos verificando até que ponto essa tela é adequada em termos de acessibilidade, segurança, desempenho e coerência com o sistema. Daqui a um ano, quem será reconhecido como sênior é o segundo. Sobrevive quem se posiciona do lado dos 30% na fronteira entre 70% (funcionamento) e 30% (aplicação e uso).</p>
<p>Espero que os engenheiros frontend que lerem este texto também encontrem sua própria resposta para a pergunta “o que mais devo estudar agora?”. Ninguém sabe a resposta certa, mas tenho bastante convicção de que, quanto mais a IA escreve código, mais sobrevivem as pessoas capazes de enxergar “o que existe além do código”. Encerro na esperança de que, daqui a um ano, eu possa voltar a escrever sobre como esse cenário terá mudado mais uma vez.</p>
<p><strong>(Se este texto parecer óbvio demais ou ultrapassado daqui a um ano, talvez isso signifique que reagimos bem.)</strong></p>
<h2 id="referências"><a class="anchor" href="#referências">Referências</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[Abstração]]></title>
            <link>https://hooninedev.com/pt-BR/260201</link>
            <guid isPermaLink="false">https://hooninedev.com/pt-BR/260201</guid>
            <pubDate>Sun, 01 Feb 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Neste post, quero falar sobre abstração na programação e sobre como escrever um bom código a partir dessa perspectiva. Ao trabalhar com desenvolvimento frontend, já me perguntei inúmeras vezes: "Até q...]]></description>
            <content:encoded><![CDATA[<p>Neste post, quero falar sobre abstração na programação e sobre como escrever um bom código a partir dessa perspectiva.</p>
<p>Ao trabalhar com desenvolvimento frontend, já me perguntei inúmeras vezes: "Até que ponto devo separar esta lógica?" e "Em que unidades devo dividir este componente?". No começo, eu achava que abstração era simplesmente extrair as partes em comum: transformar código repetido em uma função e reunir em um só lugar os pontos comuns de componentes semelhantes. Mas, depois de ver algumas vezes o código criado dessa forma se transformar, com o passar do tempo, em um monstro ainda mais difícil de modificar, comecei a repensar o que é abstração.</p>
<p>Neste texto, pretendo organizar as reflexões que venho fazendo sobre a essência da abstração e sobre como usá-la no desenvolvimento frontend para produzir um bom código.</p>
<h2 id="abstrato-e-abstração"><a class="anchor" href="#abstrato-e-abstração">Abstrato e abstração</a></h2>
<p>Antes de entrar no assunto principal, vale esclarecer o que as palavras "abstrato" e "abstração" significam exatamente na programação. Elas parecem semelhantes, mas têm naturezas bem diferentes.</p>
<p><strong>Abstrato (Abstract)</strong> é um estado e uma propriedade. Quando dizemos que "isto é abstrato", queremos dizer que os detalhes concretos foram omitidos e que <strong>restaram apenas os conceitos essenciais</strong>. Classes ou métodos marcados com a palavra-chave <code>abstract</code> em Java ou TypeScript têm justamente esse sentido. São projetos incompletos nos quais a implementação concreta ainda não foi preenchida e apenas a forma essencial está definida.</p>
<p><strong>Abstração (Abstraction)</strong> é um processo e uma ação. É o próprio processo de simplificar algo complexo, mantendo apenas suas características essenciais e removendo detalhes desnecessários. O ponto importante é que abstrair não significa "agrupar as coisas de qualquer jeito", mas <strong>definir com precisão o papel de cada nível</strong>.</p>
<p>No cotidiano, a palavra "abstrato" costuma carregar a nuance de "vago". Na programação, porém, a abstração é exatamente o contrário. Seu objetivo não é criar ambiguidade, mas estabelecer um novo nível de significado que possa ser absolutamente preciso. Preservar as informações relevantes em determinado contexto e esquecer as irrelevantes é a essência da abstração.</p>
<p>No fim, podemos distinguir os dois conceitos assim: <strong>o abstrato é "o estado em que só resta o essencial", enquanto a abstração é "o processo de deixar apenas o essencial"</strong>. Ao projetar código, é exatamente essa abstração que realizamos: o processo de conservar apenas a interface essencial de uma implementação complexa e esconder o restante.</p>
<p>Então, por que precisamos desse tipo de abstração na programação?</p>
<h2 id="por-que-a-abstração-é-necessária"><a class="anchor" href="#por-que-a-abstração-é-necessária">Por que a abstração é necessária</a></h2>
<p>A razão fundamental para precisarmos de abstração na programação é surpreendentemente simples: <strong>para construir coisas mais complexas</strong>. Quando queremos criar algo mais complexo, torna-se difícil lembrar e lidar com todos os seus muitos elementos. Por isso, agrupamos esses elementos e os transformamos em conceitos abstratos simplificados.</p>
<p>O próprio React, usado diariamente por desenvolvedores frontend, é um exemplo. Para renderizar um único componente, ocorrem internamente processos complexos como a criação do Virtual DOM, a reconciliação (Reconciliation) e a manipulação do DOM real. Ainda assim, basta escrevermos JSX sem nos preocuparmos com nada disso, porque o React abstraiu esse processo complexo para nós.</p>
<p>Observe o código abaixo. Ao usar o componente UserProfile, conseguimos criar e manipular a UI sem conhecer processos complexos que ocorrem internamente, como a criação do VDOM e o 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>Antigamente, configurar o Webpack manualmente fazia parte da rotina de um desenvolvedor frontend, mas hoje frameworks como Next.js e Vite abstraem as configurações de bundling. Graças a isso, podemos desenvolver aplicações sem conhecer o funcionamento interno do bundler e usar esse tempo para <strong>nos concentrar em problemas de nível mais alto, como a lógica de negócio e a experiência do usuário</strong>. (É por isso que considero o conceito de abstração extremamente importante para o papel do desenvolvedor frontend.)</p>
<p>No fim das contas, este é o valor central da abstração: esconder a complexidade para que algo pareça simples e permitir que cada pessoa se concentre apenas em sua própria área. Graças a ela, conseguimos construir softwares cada vez maiores e mais complexos sem que uma única pessoa precise compreender tudo.</p>
<p>Se a abstração é tão boa assim, será que quanto mais abstrairmos, melhor? Vamos pensar com que objetivo devemos abstrair.</p>
<h2 id="reduzir-o-contexto"><a class="anchor" href="#reduzir-o-contexto">Reduzir o contexto</a></h2>
<p>Muitos desenvolvedores entendem abstração como "extrair as partes em comum". Isso não está errado, mas é apenas uma das técnicas usadas para abstrair, não uma explicação de sua essência.</p>
<p>Para mim, a essência da abstração é <strong>"reduzir, ao nível adequado, o contexto que uma pessoa precisa conhecer para ler o código"</strong>.</p>
<p>Vejamos um exemplo simples.</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>Para ler esse código, um desenvolvedor precisa compreender a inicialização, a condição e o incremento do loop, o acesso aos elementos por índice, a ramificação condicionada e até a forma como a variável acumuladora externa é atualizada. O que o código realmente pretende fazer cabe em uma única frase — <strong>"calcular a soma dos pedidos concluídos"</strong> —, mas, para entendê-la, é preciso manter quatro contextos diferentes na cabeça ao mesmo tempo.</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>Graças às abstrações <code>filter</code> e <code>reduce</code>, o desenvolvedor só precisa acompanhar duas intenções: "selecionar apenas os pedidos concluídos" e "acumular os valores". O contexto do gerenciamento de índices e da declaração e atualização da variável acumuladora desapareceu da superfície do código.</p>
<p>Podemos avançar mais um passo.</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>Agora, quem lê o código nem sequer precisa saber que esse cálculo percorre um vetor. Resta apenas a intenção de negócio de "calcular o valor total dos pedidos concluídos". Passamos a nos concentrar não em <strong>como (How) o cálculo é feito</strong>, mas em <strong>o que (What) é calculado</strong>.</p>
<p>Sob essa perspectiva, percebemos que o código React que escrevemos todos os dias também é uma combinação de inúmeras abstrações.</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>E se todo o código interno de <code>emotion</code>, <code>date-fns</code> e <code>react</code> estivesse aberto dentro deste arquivo de componente? Seria difícil saber por onde começar a leitura e distinguir a lógica de negócio do código das bibliotecas. Como a abstração esconde adequadamente o contexto de cada área, podemos nos concentrar apenas na essência: "mostrar a data de hoje".</p>
<p>Então, ao projetar código na prática, em que direção devemos abordar a abstração?</p>
<h2 id="o-que-significa-um-nível-de-abstração-alto-ou-baixo"><a class="anchor" href="#o-que-significa-um-nível-de-abstração-alto-ou-baixo">O que significa um nível de abstração alto ou baixo</a></h2>
<p>Quando falamos de abstração, não podemos deixar de abordar o conceito de <strong>nível de abstração (Level of Abstraction)</strong>. Afinal, o que significa dizer que o nível de abstração de um código é "alto" ou "baixo"?</p>
<p>Um <strong>código com baixo nível de abstração</strong> está próximo dos procedimentos concretos executados pelo computador: fazer o parsing de uma string diretamente, percorrer um vetor por índices ou manipular bytes. Ele expõe claramente <strong>como (How)</strong> funciona.</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>Um <strong>código com alto nível de abstração</strong> é expresso na linguagem do domínio de negócio ou da área do problema. Alguns exemplos são <code>processPayment(order)</code>, <code>sendNotification(user, message)</code> e <code>validateUserInput(formData)</code>. Um código com alto nível de abstração revela <strong>o que (What)</strong> faz e esconde como faz.</p>
<p>Em <em>Clean Code</em>, Robert C. Martin organizou esse conceito no princípio de <strong>"um nível de abstração por função (One Level of Abstraction per Function)"</strong>. Quando uma mesma função mistura código de alto e baixo nível, quem a lê precisa decidir a cada linha: "Isto é a lógica principal ou um detalhe de implementação?".</p>
<p>O problema fica claro quando observado em código real.</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>Quem lê essa função começa acompanhando o contexto de alto nível das regras de negócio do "fluxo de cadastro do usuário", mas de repente é arrastado para o contexto de baixo nível da manipulação de buffers de hash, consultas SQL e strings de templates de e-mail. Em seguida, salta outra vez para o alto nível de <code>sendWelcomeEmail</code>. Quando o nível de abstração sobe e desce dessa forma, a mente de quem lê também precisa subir e descer junto.</p>
<p>Se reescrevermos a mesma função mantendo um nível de abstração uniforme, o resultado será este.</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>Todas as instruções falam no mesmo nível de abstração. Cada função de nível inferior fica responsável por como o envio de e-mails é implementado ou qual algoritmo é usado para gerar o hash da senha. Quem lê essa função só precisa se concentrar em um único contexto: "o fluxo completo de cadastro do usuário".</p>
<p>Martin também chama isso de <strong>"regra descendente (The Stepdown Rule)"</strong>. Ao ler o código de cima para baixo, como em uma matéria de jornal, devemos ver o panorama geral no topo e encontrar cada vez mais detalhes conforme descemos.</p>
<p>Kent Beck apresentou o mesmo princípio em <em>Smalltalk Best Practice Patterns</em> por meio do padrão <strong>Composed Method</strong>. Um método deve ser composto apenas de operações no mesmo nível de abstração, e cada etapa deve ser expressa por uma chamada de método em uma única linha.</p>
<p>No fim, todas essas ideias chegam à mesma conclusão: <strong>uma função deve falar em apenas um nível de abstração.</strong> Só esse cuidado já muda visivelmente a legibilidade do código.</p>
<p>Mas em que direção devemos conduzir a abstração? Devemos partir do concreto ou do abstrato?</p>
<h2 id="pensar-em-composição-de-peças-não-em-extração-de-pontos-comuns"><a class="anchor" href="#pensar-em-composição-de-peças-não-em-extração-de-pontos-comuns">Pensar em composição de peças, não em extração de pontos comuns</a></h2>
<p>Na OOP, é comum ouvir a diretriz "extraia os pontos comuns das coisas concretas para definir algo abstrato". Essa abordagem não está errada por si só, mas acredito que se prender demais a ela traz o risco de criar um design limitado aos requisitos atuais.</p>
<p>Vejamos um exemplo. Suponha que os requisitos incluam os botões A, B e C, todos azuis e arredondados, com diferença apenas no texto do rótulo. Se projetarmos extraindo apenas os pontos comuns, podemos representá-los assim.</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>Os requisitos atuais são atendidos perfeitamente. Alguns dias depois, porém, a pessoa responsável pelo produto diz:</p>
<blockquote>
<p>"Permita alterar a cor do botão B."</p>
</blockquote>
<p>Nesse instante, o próprio nome <code>BlueRoundButton</code> se torna estranho. Até seria possível adicionar uma prop de cor, mas o design já era vulnerável a mudanças porque partira da característica concreta comum de ser "um botão azul e arredondado". (E este ainda é um caso simples. No mundo real, chegam inúmeros requisitos sobre o formato, o tamanho e muitos outros aspectos do botão.)</p>
<p>Quando situações como essa se repetem, percebemos naturalmente uma coisa: <strong>uma abordagem que extrai pontos comuns de requisitos concretos tende a fazer com que até o resultado abstraído reflita apenas os requisitos atuais</strong>.</p>
<p>Por isso, prefiro seguir na direção oposta. Em vez de extrair o abstrato a partir do concreto, gosto de <strong>pensar primeiro em peças abstratas e compô-las para criar algo concreto</strong>.</p>
<p>Imagine que estamos criando um componente de notificação toast. O requisito inicial é simples: "Quando o salvamento for concluído, mostre uma breve mensagem de orientação na parte inferior." Pela abordagem de extração de pontos comuns, teríamos isto.</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>O requisito atual é atendido perfeitamente. Alguns dias depois, porém, a pessoa responsável pelo produto pede: "Adicione um ícone à esquerda de acordo com o estado de sucesso ou alerta". Surgem <code>hasIcon</code> e <code>iconName</code>. Logo chega outro pedido: "Também precisamos de um toast com uma barra de progresso de upload". Mais uma prop, <code>progress</code>, é adicionada. Depois de repetir esse processo algumas vezes, <code>Toast</code> passa a ter mais de dez props e ainda exige que se decorem regras ocultas sobre <strong>quais combinações são permitidas e quais não são</strong>. (E, na maioria das vezes, essas regras nem sequer ficam registradas em comentários.)</p>
<p>O design era vulnerável a mudanças porque havia partido da <strong>imagem concreta, no momento atual, de "como um toast deve ser"</strong>.</p>
<p>Quando abordamos o problema pela composição de peças, a história muda.</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>A essência imutável — "um toast é um contêiner simples que apenas envolve conteúdo" — foi separada dos detalhes concretos mais sujeitos a mudanças — "o que ele abriga". Agora podemos adicionar quantas peças novas quisermos ou organizar as existentes em novas combinações sem tocar no interior de <code>Toast</code>. As regras de validade para combinações de props também desaparecem. Basta <strong>colocar ali o que queremos</strong>.</p>
<p>É claro que um desenvolvedor experiente poderia perguntar: "Não basta projetar desde o início com IoC (inversão de controle)?". É verdade. Mas esse julgamento só é possível porque, depois de incontáveis tentativas e erros no passado, foi desenvolvida uma intuição sobre "quais partes tendem a mudar".</p>
<p>Quando ainda não temos essa intuição, partir da pergunta "De quais peças esta funcionalidade é composta e como cada uma deve ser combinada?" facilita muito a criação de um design aberto a mudanças.</p>
<p>Depois de ler até aqui, surge naturalmente uma pergunta: com base em que critérios devemos separar as peças e como devemos expô-las externamente?</p>
<h2 id="três-pontos-para-uma-boa-abstração"><a class="anchor" href="#três-pontos-para-uma-boa-abstração">Três pontos para uma boa abstração</a></h2>
<h3 id="pensar-na-expressividade"><a class="anchor" href="#pensar-na-expressividade">Pensar na expressividade</a></h3>
<p>A virtude mais importante de um módulo abstraído é permitir que seu comportamento seja deduzido sem que seja necessário abrir o código-fonte. Kent Beck chamou isso de padrão <strong>"nome que revela intenção (Intention-Revealing Name)"</strong> e afirmou que, se não for possível encontrar um nome conciso, a própria abstração precisa ser repensada.</p>
<p>Temos duas grandes ferramentas para isso: <strong>nomes</strong> e <strong>tipos</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>Só pelo nome de <code>calculateDiscountedPrice</code>, sabemos que a função recebe o preço original e a taxa de desconto para calcular o preço com desconto, enquanto a informação de tipo confirma que ela recebe <code>number</code> e retorna <code>number</code>. Não precisamos conhecer a lógica de cálculo aplicada internamente.</p>
<p>Já <code>calculate(price: number, rate: number): number</code> não informa o que está sendo calculado, portanto não nos permite prever o resultado. No fim, só conseguimos usá-la depois de abrir o código-fonte, perdendo a vantagem da abstração.</p>
<p>Nesse ponto, vale observar que a própria forma de nomear reflete o nível de abstração. Na programação, nomes de funções costumam ser compostos pela combinação <strong>verbo + substantivo</strong>, e o verbo escolhido revela em que nível de abstração a função opera.</p>
<blockquote>
<p>No entanto, o verbo sozinho não determina o nível de abstração. O substantivo que o acompanha — isto é, o contexto do domínio — determina o nível final</p>
</blockquote>
<p>Existem verbos frequentemente usados em níveis de abstração <strong>baixos</strong>, como <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> e <code>transform</code>. Essas palavras sugerem uma transformação física dos dados ou uma manipulação direta da estrutura de dados.</p>
<p>No nível intermediário, aparecem verbos como <code>get</code>, <code>save</code>, <code>load</code> e <code>validate</code>. São operações técnicas, mas seu propósito já se revela até certo ponto.</p>
<p>Em níveis de abstração <strong>altos</strong>, são usados verbos como <code>register</code>, <code>refund</code>, <code>confirm</code>, <code>cancel</code> e <code>submit</code>. Essas palavras pertencem à linguagem do domínio de negócio. Elas não revelam nenhum dos procedimentos técnicos internos, expressando apenas <strong>as ações do usuário ou os processos de negócio</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>Em <em>Clean Code</em>, Robert C. Martin afirmou que <strong>"um nome longo e descritivo é melhor que um nome curto e enigmático"</strong>. Ele também apresentou o princípio <strong>"use uma palavra por conceito"</strong>, pois, se misturarmos <code>fetch</code>, <code>retrieve</code> e <code>get</code> para operações no mesmo contexto, quem lê ficará em dúvida: "Essas três operações são diferentes?".</p>
<p>O mesmo princípio se aplica diretamente à nomenclatura de componentes e hooks do React.</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>A especificidade do nome de um componente varia de acordo com seu nível de abstração. Button é usado em um nível baixo como um primitivo genérico de UI, enquanto SubmitOrderButton revela claramente a intenção de negócio em um nível alto.</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> é o nome da prop que o componente expõe externamente. Quem usa o componente declara "a qual evento reagir". <code>handle*</code> é o nome da função de implementação efetivamente passada para essa 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>Hooks customizados usam o prefixo <code>use</code> para seguir as regras do React e permitem que componentes utilizem o estado ou as operações fornecidas pelo hook.</p>
<blockquote>
<p>Jeff Atwood, responsável pelo <a href="https://blog.codinghorror.com/" target="_blank" rel="noopener noreferrer">Coding Horror</a>, já apontou o problema do sufixo <code>Manager</code>. O nome <code>UrlManager</code> não revela se sua função é manter um pool de URLs, validá-las ou criá-las. Nomes como <code>UrlBuilder</code>, <code>UrlValidator</code> e <code>UrlPool</code>, que revelam papéis específicos, são muito melhores. Um nome vago pode ser um sinal de que a própria responsabilidade do módulo é vaga.</p>
</blockquote>
<p>No fim das contas, um bom nome é <strong>aquele que informa imediatamente a quem lê em que nível de abstração o código opera</strong>.</p>
<h3 id="projetar-intencionalmente-o-grau-de-liberdade-das-entradas"><a class="anchor" href="#projetar-intencionalmente-o-grau-de-liberdade-das-entradas">Projetar intencionalmente o grau de liberdade das entradas</a></h3>
<p>Ao projetar um módulo abstraído, há uma dúvida recorrente: "Até que ponto devemos deixar a funcionalidade aberta?". Essa decisão muda bastante a experiência dos desenvolvedores que usam o módulo.</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>O primeiro botão aceita apenas <code>children</code>. Não é possível configurar <code>onClick</code>, <code>type</code> nem <code>disabled</code>. Em compensação, quem o usa não precisa tomar nenhuma decisão.</p>
<p>O segundo botão aceita todos os atributos do elemento <code>button</code>. Ele oferece muita liberdade, mas quem o utiliza precisa decidir quais, entre dezenas de props, deve usar. Eu descrevo essa situação dizendo que <strong>"o componente obriga o desenvolvedor a pensar"</strong>.</p>
<p>Não há uma resposta certa. Precisamos encontrar o nível adequado de acordo com o propósito e os usuários do módulo. Para o botão básico de um design system, pode ser melhor preservar a consistência com Props limitadas; para um componente utilitário genérico, pode ser melhor oferecer flexibilidade.</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>A amplitude da abstração deve ser determinada por quem é seu usuário. Para quem precisa compreender e controlar em detalhes a implementação interna, uma interface de baixo nível é adequada. Por outro lado, oferecer uma interface excessivamente aberta a quem não precisa conhecer os detalhes só aumenta a confusão. Da mesma forma, limitar demais as entradas de quem precisa lidar com diversas situações acaba inviabilizando os próprios casos de uso.</p>
<h3 id="manter-a-unidade-de-abstração-no-tamanho-adequado"><a class="anchor" href="#manter-a-unidade-de-abstração-no-tamanho-adequado">Manter a unidade de abstração no tamanho adequado</a></h3>
<p>A unidade de abstração — isto é, "até onde agrupar em um único módulo" — também é uma questão importante.</p>
<p>Um antipadrão comum no frontend é a extração excessiva de 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>Quando separamos desnecessariamente em um hook uma lógica usada por um único componente, quem lê precisa alternar entre dois arquivos para compreender o contexto. Em vez de reduzi-lo, a abstração acabou aumentando-o.</p>
<p>O contrário — colocar coisas demais em um único hook — também é um problema.</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>Um "God Hook" como esse é difícil de testar, e a alteração de uma única coisa pode afetar partes sem relação com ela.</p>
<p>O critério para determinar a unidade adequada de abstração é <strong>"esta separação realmente reduz o contexto de quem lê o código?"</strong> Se o resultado da separação dispersar o contexto e dificultar sua compreensão, ainda não chegou a hora dessa abstração.</p>
<h2 id="cuidado-com-abstrações-prematuras"><a class="anchor" href="#cuidado-com-abstrações-prematuras">Cuidado com abstrações prematuras</a></h2>
<p>Depois de ler até aqui, pode restar a pergunta: "Então, quando devo abstrair?". Minha opinião é esta: <strong>a premissa básica deve ser não abstrair prematuramente.</strong></p>
<p>Enquanto não houver um sinal claro de abstração, deixar o código como está oferece um resultado mínimo melhor do que criar uma abstração errada e precisar desfazê-la mais tarde. O processo pelo qual uma abstração ruim costuma surgir é mais ou menos este.</p>
<ol>
<li>Surge um padrão semelhante no código A e no código B.</li>
<li>Pensamos: "É o princípio DRY, então vou extrair uma função comum!" e criamos uma abstração.</li>
<li>Um padrão parecido aparece no código C; usamos a mesma função, mas adicionamos um parâmetro para obter um comportamento um pouco diferente.</li>
<li>Quando os códigos D e E também passam a usá-la, as condicionais e os parâmetros continuam aumentando.</li>
<li>Agora a função é usada em toda parte, mas todos têm medo de modificá-la.</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>Se chegamos a essa situação, a solução é clara: colocar o código abstraído novamente inline em cada caso de uso, remover de cada um o que for desnecessário e, quando o verdadeiro ponto em comum se revelar no código já limpo, abstrair outra vez. <strong>"O avanço mais rápido é voltar atrás."</strong></p>
<p>Então, quando devemos abstrair? Os <strong>sinais de abstração</strong> que percebo são mais ou menos estes.</p>
<ul>
<li><strong>A consistência está se perdendo.</strong> Embora a lógica seja a mesma, em alguns componentes ela fica inline e, em outros, é separada em uma função. A mesma lógica de cálculo está espalhada por toda parte.</li>
<li><strong>A estrutura interna está desnecessariamente exposta ao exterior.</strong> O chamador precisa lidar, um por um, com detalhes de implementação que não deveria conhecer.</li>
<li><strong>O procedimento interno continua exposto.</strong> O módulo não consegue ocultar seus próprios procedimentos, e quem o utiliza precisa segui-los diretamente.</li>
</ul>
<p>O problema é que, embora detectar esses sinais costume ser simples, <strong>na prática é fácil ignorá-los e se concentrar em atender a requisitos mais "importantes"</strong>. Quando estamos sob pressão de prazos ou concentrados em implementar funcionalidades, pensamos: "Por enquanto funciona; organizo depois" — e esse depois raramente chega.</p>
<p>Outro ponto importante é <strong>manter critérios de abstração consistentes</strong>. Se o mesmo tipo de lógica fica inline em um lugar da codebase, separado em uma função em outro e extraído para um hook customizado em outro, quem começa a ler o código se pergunta: "Existe alguma intenção nessa diferença?". Abstraindo ou não, a equipe precisa adotar critérios consistentes.</p>
<p>Também vale lembrar, nesse contexto, a <strong>"lei das abstrações com vazamento (The Law of Leaky Abstractions)"</strong>, apresentada por Joel Spolsky em 2002. Essa lei diz que, embora uma abstração tente esconder uma implementação complexa, os detalhes dessa implementação acabam vazando (leak) para fora. Em outras palavras, uma abstração é projetada para que seus usuários não precisem conhecer a implementação interna, mas surgem situações em que só é possível usá-la corretamente conhecendo-a.</p>
<p>O TCP abstrai uma rede instável como se fosse uma conexão confiável, mas, quando um cabo é desconectado, essa abstração se rompe. O React abstrai as atualizações de UI de forma declarativa, mas, para otimizar novas renderizações, acabamos precisando compreender seu funcionamento interno. Como não existe abstração perfeita, ao criar uma também precisamos pensar: <strong>"O usuário conseguirá reagir quando esta abstração se romper?"</strong></p>
<p>No fim, <strong>"a abstração economiza nosso tempo de trabalho, mas não economiza nosso tempo de aprendizado."</strong></p>
<h2 id="a-abstração-é-algo-que-se-internaliza"><a class="anchor" href="#a-abstração-é-algo-que-se-internaliza">A abstração é algo que se internaliza</a></h2>
<p>Certa vez, conversei com um colega sobre abstração, e uma observação feita naquele momento me marcou: detectar sinais de abstração e separar o código no nível adequado, na hora certa, é, no fim das contas, uma questão de <strong>intuição</strong>.</p>
<p>É claro que os princípios discutidos anteriormente — manter o mesmo nível de abstração, dar bons nomes e projetar o grau de liberdade das entradas — são importantes. Porém, tentar lembrar cada um deles enquanto escrevemos código e ponderar conscientemente "Devo separar isto ou não?" pode acabar interrompendo nosso fluxo. Assim como pensar no ângulo do cotovelo ao lançar um jab durante uma luta pode fazer alguém perder o momento certo, ao programar a abstração também deve surgir de uma intuição natural, não de um julgamento consciente.</p>
<p>Há momentos em que estamos escrevendo código e de repente sentimos certa rejeição: "Parece que esta lógica não deveria estar aqui" ou "Parece que este componente sabe coisas demais". Essa sensação é justamente um sinal de abstração, e internalização é a capacidade de detectá-lo e reagir a ele naturalmente.</p>
<p>Mas essa intuição não surge da noite para o dia. Só depois de estudar inúmeros padrões, ler códigos variados e passar pessoalmente por muitas tentativas e erros é que a sensação de <strong>"acho que isto precisa ser separado"</strong> começa a aparecer com naturalidade. Se, mais tarde, um colega perguntar "Por que você separou isto?" e conseguirmos explicar naturalmente "Eu separei porque se trata de X", significa que esse conhecimento foi internalizado.</p>
<p>Acho que o mesmo vale para qualquer área. Quando tentamos fazer algo bem apenas decorando regras, tomar decisões se torna ainda mais difícil. No fim, precisamos preservar o fluxo geral e deixar que os detalhes sejam preenchidos naturalmente. E essa naturalidade nasce da variedade de padrões e experiências que acumulamos no dia a dia.</p>
<h2 id="conclusão"><a class="anchor" href="#conclusão">Conclusão</a></h2>
<p>Na programação, abstrair é esconder a complexidade para fazê-la parecer simples e permitir que quem lê o código se concentre apenas no contexto necessário.</p>
<p>Retomando os pontos que devemos lembrar para criar uma boa abstração:</p>
<ul>
<li>Como premissa básica, não devemos abstrair prematuramente; devemos separar apenas quando surgirem sinais claros.</li>
<li>Uma função deve falar em apenas <strong>um nível de abstração</strong>.</li>
<li>Devemos <strong>expressar</strong> o comportamento suficientemente por meio de nomes e tipos, para que o código possa ser usado sem que seja necessário abrir seu código-fonte.</li>
<li>Devemos <strong>projetar intencionalmente</strong> o grau de liberdade das entradas de acordo com o propósito e os usuários do módulo.</li>
<li>Em vez de acrescentar coisas sobre uma abstração errada, devemos ter <strong>a coragem de desfazê-la e recomeçar</strong>.</li>
<li>E devemos <strong>internalizar padrões variados</strong> até que tudo isso surja naturalmente, sem esforço consciente.</li>
</ul>
<p>É claro que o que apresentei neste texto não é a única resposta correta. O nível adequado de abstração pode variar de acordo com o contexto de negócio, a composição da equipe e a natureza do projeto. Ainda assim, se existe uma coisa que não muda, é que o objetivo final da abstração consiste em <strong>criar um código fácil para as pessoas compreenderem</strong>.</p>
<p>Espero que quem leu este texto também se pergunte, em sua própria codebase: "Esta abstração está realmente reduzindo o contexto?". Acredito que só essa pergunta já pode mudar um pouco a forma de enxergar o código.</p>
<h2 id="referências"><a class="anchor" href="#referências">Referências</a></h2>
<p>Este texto recebeu muita inspiração de diversas documentações oficiais e artigos anteriores. Deixo abaixo as fontes dos trechos citados diretamente e os textos que me ajudaram a estruturar estas ideias.</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, 추상, 그리고 추상화</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/pt-BR/260104</link>
            <guid isPermaLink="false">https://hooninedev.com/pt-BR/260104</guid>
            <pubDate>Sun, 04 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Neste artigo, quero falar sobre a queryKey do TanStack Query. Ao usar o TanStack Query em projetos reais, já precisei reformular várias vezes a maneira de gerenciar queryKey. No começo, eu simplesment...]]></description>
            <content:encoded><![CDATA[<p>Neste artigo, quero falar sobre a <strong>queryKey do TanStack Query</strong>.</p>
<p>Ao usar o TanStack Query em projetos reais, já precisei <strong>reformular várias vezes a maneira de gerenciar queryKey</strong>. No começo, eu simplesmente escrevia vetores como <code>['user', userId]</code> diretamente dentro dos componentes. Depois, comecei a cometer erros de digitação porque precisava repetir a mesma chave em vários lugares sempre que invalidava uma consulta, então migrei tudo para um objeto de constantes como <code>QUERY_KEYS</code>. Mais tarde, após ler um artigo de TkDodo, adotei o padrão de fábrica de chaves de consulta; bastante tempo depois, passei a usar a biblioteca <code>@lukemorales/query-key-factory</code>; então chegou a v5, e reformulei tudo mais uma vez com <code>queryOptions</code>.</p>
<p>Comecei a me perguntar por que tantos padrões haviam surgido em torno de um pequeno vetor que não passava de um identificador de cache. <strong>Por que uma única queryKey carrega tantos vestígios dessa evolução?</strong> E qual problema específico cada etapa tentava resolver?</p>
<p>Neste artigo, vou percorrer a documentação oficial do TanStack Query, a série de artigos de TkDodo e até a implementação interna de <code>queryOptions</code>, introduzida na v5, para explicar como queryKey funciona e por que evoluiu até sua forma atual.</p>
<h2 id="antes-de-querykey"><a class="anchor" href="#antes-de-querykey">Antes de queryKey</a></h2>
<p>Antes de entrar no assunto principal, vale esclarecer um ponto. Hoje usamos bibliotecas como <code>TanStack Query</code> e <code>SWR</code> com toda naturalidade, mas como os dados assíncronos eram tratados antes de elas existirem?</p>
<p>A abordagem mais comum provavelmente era combinar <code>useState</code>, <code>useEffect</code>, <code>fetch</code>, <code>axios</code> e ferramentas semelhantes.</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>O problema desse código é evidente. Se houver apenas dois componentes na página consultando o mesmo <code>userId</code>, <strong>a mesma requisição será feita duas vezes.</strong> Isso acontece porque não há cache. E, se o usuário navegar para outra página e voltar, os dados serão buscados novamente do zero. Como não há como saber se eles foram obtidos há um segundo ou há uma hora, também é difícil reproduzir um comportamento como "mostrar o valor armazenado em cache enquanto ele é atualizado em segundo plano". (É possível criar um sistema próprio de cache para isso, mas considero seu gerenciamento bastante complicado.)</p>
<p>Para resolver esse problema, surgiu a combinação Redux + redux-thunk (ou redux-saga). Ao mover a lógica de busca de dados para uma função thunk e armazenar o resultado no repositório central, outros componentes podiam reutilizar os mesmos dados. No entanto, ainda era necessário definir tipos de ação, escrever redutores e gerenciar manualmente os estados de carregamento, sucesso e falha. A quantidade de código repetitivo para buscar um único dado era enorme. (Comecei a trabalhar profissionalmente nessa época e me perguntava: "Por que preciso criar vários arquivos só para buscar um dado?")</p>
<p>A essência desse fluxo é: <strong>"Só é possível evitar a repetição de uma requisição quando se consegue identificar que requisição é essa."</strong> E o identificador que diz "qual requisição" é justamente a queryKey.</p>
<p>SWR e React Query (hoje TanStack Query) enfrentaram esse problema diretamente. "Uma requisição assíncrona precisa de um identificador, e requisições com o mesmo identificador compartilham o cache." Esse único princípio simples eliminou todo o código repetitivo descrito acima.</p>
<h2 id="a-essência-de-querykey"><a class="anchor" href="#a-essência-de-querykey">A essência de queryKey</a></h2>
<p>Então, o que é exatamente queryKey? A documentação oficial do TanStack Query a define assim.</p>
<blockquote class="bilingual-quote"><div class="quote-translation" lang="ko"><p>Em essência, o TanStack Query gerencia o armazenamento de consultas em cache com base em chaves de consulta. No nível mais alto, as chaves de consulta precisam ser um vetor... Desde que a chave de consulta seja serializável e <strong>exclusiva para os dados da consulta</strong>, ela pode ser usada.</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>Há dois pontos essenciais. <strong>Ela precisa ser serializável e exclusiva para aqueles dados.</strong> A mesma chave deve representar os mesmos dados, e dados diferentes devem ter chaves diferentes. Essa regra simples determina todo o funcionamento do sistema de cache.</p>
<p>Há ainda outro aspecto importante. <strong>queryKey também exerce o papel de vetor de dependências.</strong> Assim como um efeito de <code>useEffect</code> do React é executado novamente quando suas dependências mudam, o TanStack Query busca automaticamente novos dados quando queryKey muda.</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>Quando <code>userId</code> é <code>'A'</code> e quando é <code>'B'</code>, as chaves de consulta são diferentes. Se são diferentes, ocorre uma falha de cache; se há uma falha de cache, os dados são buscados. Tudo automaticamente. Graças a essa simplicidade, não precisamos escrever por conta própria uma lógica que diga: "userId mudou, então é preciso buscar os dados novamente".</p>
<p>Isso suscita uma pergunta: como o TanStack Query determina que uma queryKey é "a mesma chave"? Se a comparação fosse feita apenas com <code>===</code>, as referências dos objetos seriam diferentes e haveria uma falha de cache a cada vez.</p>
<h2 id="por-dentro-de-querycache"><a class="anchor" href="#por-dentro-de-querycache">Por dentro de QueryCache</a></h2>
<p>Segundo o artigo <a href="https://tkdodo.eu/blog/inside-react-query" target="_blank" rel="noopener noreferrer">Por dentro do React Query</a>, de TkDodo, <code>QueryCache</code> é, no fim das contas, apenas <strong>uma estrutura de dados mantida na memória</strong>. Mais precisamente, na <a href="https://github.com/TanStack/query/blob/main/packages/query-core/src/queryCache.ts" target="_blank" rel="noopener noreferrer">implementação oficial</a> da v5, essa estrutura não é um objeto simples, mas um <code>Map&#x3C;string, Query></code>. Ela é declarada dentro da classe como <code>#queries = new Map&#x3C;string, Query>()</code>, e todas as operações de escrita e leitura ocorrem por meio de <code>#queries.set(query.queryHash, query)</code> e <code>#queries.get(queryHash)</code>. A chave é a forma serializada de queryKey (<code>queryHash</code>), e o valor é uma instância da classe <code>Query</code>.</p>
<p>Versões antigas chegaram a usar objetos simples, mas, na v5, a implementação passou a adotar o <code>Map</code> nativo. (<code>Map</code> não apresenta risco de colisão de chaves nem de contaminação do protótipo, preserva a ordem de inserção e oferece, em média, consulta O(1) com chaves de texto, o que o torna uma escolha praticamente canônica para uma estrutura de cache.)</p>
<p>O que acontece a cada chamada de <code>useQuery</code> é simples. <strong>queryKey é convertida em um valor de hash, que é usado para fazer uma consulta no Map.</strong> Se houver um item, a instância de <code>Query</code> armazenada em cache é recuperada; se não houver, uma nova instância é criada e inserida com <code>set</code>.</p>
<p>Surge então outra pergunta natural. <strong>Por que serializar queryKey como texto?</strong> Por que não usar o próprio vetor como chave, como em <code>Map&#x3C;QueryKey, Query></code>?</p>
<p>A resposta está no modelo de igualdade do JavaScript. O <code>Map</code> nativo compara as chaves por <strong>igualdade referencial (reference equality)</strong>. Mesmo que o conteúdo seja idêntico, objetos diferentes na memória são considerados chaves distintas.</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>Em um componente React, porém, <code>useQuery({ queryKey: ['user', userId] })</code> <strong>cria uma nova instância do vetor a cada renderização.</strong> Embora o conteúdo dos vetores da primeira e da segunda renderização seja idêntico, eles são objetos distintos na memória. Se o cache dependesse da igualdade referencial, qualquer componente que consultasse os mesmos dados sofreria uma falha de cache a cada renderização.</p>
<p>A solução para o problema causado pela igualdade referencial é simples: <strong>converter a igualdade referencial em igualdade estrutural (structural equality)</strong>. Basta produzir um texto determinístico com base apenas no conteúdo de queryKey e usar esse texto como chave do Map. Assim, recuperamos a semântica desejada: "conteúdo igual significa chave igual". <code>JSON.stringify</code> é apenas a ferramenta mais simples para realizar essa conversão. (É também por isso que, após testar diferentes estratégias de serialização na época da v3, o TanStack Query acabou adotando uma variação estável de <code>JSON.stringify</code>.)</p>
<p>O elemento central aqui é a função que produz esse valor de hash: <code>hashKey</code>. A implementação oficial, definida em <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>, é exatamente esta.</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>Embora use <code>JSON.stringify</code>, não se trata de uma serialização comum: uma <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#the_replacer_parameter" target="_blank" rel="noopener noreferrer">função substituidora</a> é fornecida para <strong>ordenar alfabeticamente as chaves dos objetos simples</strong> antes da serialização.</p>
<p>Essa ordenação é essencial porque a serialização como texto exige uma condição ainda mais rigorosa: <strong>entradas com o mesmo significado devem sempre ser convertidas no mesmo texto.</strong> No entanto, <code>JSON.stringify</code> normalmente preserva a ordem das chaves. <code>{ a: 1, b: 2 }</code> e <code>{ b: 2, a: 1 }</code> são objetos semanticamente equivalentes, mas são serializados como textos diferentes e, por consequência, ocupam posições diferentes no cache. Isso faria com que os mesmos dados voltassem a ser solicitados duas vezes.</p>
<p>A técnica usada para evitar isso de forma consistente é a <strong>forma canônica (canonical form)</strong>. Ela força toda entrada com o mesmo significado a corresponder a uma única representação. É exatamente por isso que a função substituidora de <code>hashKey</code> ordena as chaves dos objetos simples. Independentemente da ordem de entrada, a saída se torna igual, fazendo o resultado da serialização corresponder univocamente ao significado do objeto. Em termos matemáticos, trata-se de escolher a forma ordenada como elemento representante da classe de equivalência (equivalence class) formada por objetos cujas chaves estão em ordens diferentes.</p>
<p>O fato de os vetores não serem ordenados é o outro lado do mesmo princípio. Como a própria ordem carrega significado nesse tipo de estrutura de dados, ordená-los causaria perda de informação. A ordem das chaves de um objeto é acidental; a ordem dos elementos de um vetor é intencional. <code>hashKey</code> trata corretamente esses dois casos de maneira distinta. Por isso, o guia oficial recomenda organizar queryKey do "genérico para o específico". Enquanto a ordem do vetor carregar significado, cabe a quem escreve o código definir esse significado.</p>
<p>Há mais um detalhe importante: a ordenação das chaves só se aplica a <strong>objetos simples</strong>. No mesmo arquivo, <code>isPlainObject</code> não verifica apenas <code>typeof === 'object'</code>; ela também verifica <code>Object.getPrototypeOf(o) === Object.prototype</code> para distinguir <strong>literais de objeto puros</strong> de <strong>instâncias de classes</strong>. Assim, um literal como <code>{ foo: 1 }</code> é ordenado, enquanto uma instância criada com <code>class User { ... }</code> segue adiante sem ordenação. (Daí surge uma armadilha: ao inserir diretamente uma instância de classe em queryKey, o comportamento de <code>JSON.stringify</code>, que só emite propriedades enumeráveis, pode produzir um hash diferente do esperado.)</p>
<p>Esse funcionamento produz duas consequências importantes.</p>
<p><strong>1. A ordem das chaves de um objeto é irrelevante.</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>Isso ocorre porque as chaves são ordenadas antes da serialização. Sem esse comportamento, seria necessário lembrar a ordem das chaves toda vez que se usasse um literal de objeto.</p>
<p><strong>2. A ordem dos elementos de um vetor é relevante.</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>Isso acontece porque o vetor é uma estrutura de dados em que a própria ordem tem significado. <code>JSON.stringify</code> também preserva a ordem de seus elementos.</p>
<p>Também é útil saber que valores <code>undefined</code> desaparecem durante a serialização. <code>{ a: 1, b: undefined }</code> e <code>{ a: 1 }</code> produzem o mesmo valor de hash. (Eu mesmo já cometi o erro de pensar: "Como incluí undefined explicitamente, deve ser outro cache!")</p>
<p>Além disso, queryKey não pode conter <strong>referências circulares nem funções</strong>, pois <code>JSON.stringify</code> não consegue processá-las. Objetos <code>Date</code>, bem como <code>Map/Set</code>, <code>BigInt</code> e tipos semelhantes, tampouco são recomendados com o comportamento padrão. A estrutura precisa ser pura e serializável.</p>
<p>Um aspecto interessante é que essa restrição não é absoluta. Por meio da opção <code>queryKeyHashFn</code>, o TanStack Query oferece <strong>uma saída para substituir a própria função de hash</strong>. Internamente, <code>hashQueryKeyByOptions(queryKey, options)</code> verifica se <code>queryKeyHashFn</code> foi fornecida nas opções; em caso afirmativo, chama essa função, caso contrário, chama a <code>hashKey</code> padrão.</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>Contudo, essa opção precisa ser definida separadamente para cada consulta e não se aplica a APIs imperativas chamadas sem acesso às opções, como <code>queryClient.setQueryData</code> (<a href="https://github.com/TanStack/query/issues/1343" target="_blank" rel="noopener noreferrer">Issue nº 1343</a>). Por isso, em projetos reais, é muito mais seguro evitar essa saída e <strong>converter queryKey para uma forma serializável no momento em que ela é criada</strong>. (Certa vez, inseri diretamente um <code>Date</code> e passei muito tempo tentando entender: "Por que o cache não é atualizado se o instante é o mesmo?" A resposta era: "Esse <code>Date</code> representa o mesmo instante, mas é outra instância do objeto e, portanto, produz um hash diferente a cada vez.")</p>
<h2 id="regras-para-escrever-querykey"><a class="anchor" href="#regras-para-escrever-querykey">Regras para escrever queryKey</a></h2>
<p>Depois de compreender o funcionamento interno descrito acima, as regras de escrita decorrem naturalmente. As recomendações da documentação oficial podem ser resumidas assim.</p>
<p><strong>Regra 1. queryKey precisa ser um vetor.</strong></p>
<p>Mesmo que uma cadeia de caracteres funcione (ela é convertida internamente em um vetor), é melhor usar um vetor desde o início para manter a consistência.</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>Regra 2. Inclua em queryKey todas as variáveis das quais queryFn depende.</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>O raciocínio é idêntico ao das dependências de <code>useEffect</code>. Todas as variáveis usadas dentro da função precisam estar na chave, isto é, no vetor de dependências. Se essa regra for violada, pode surgir um erro difícil de rastrear: o usuário muda, mas os dados do usuário anterior continuam aparecendo.</p>
<p><strong>Regra 3. Organize os elementos do mais genérico para o mais específico.</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>Essa ordem é importante por causa da <strong>invalidação (invalidation)</strong>. Por padrão, <code>invalidateQueries</code> do TanStack Query usa <strong>correspondência por prefixo</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>Ao projetar as chaves como uma árvore, torna-se possível expressar em uma única linha desde "busque novamente todos os dados deste domínio" até "busque novamente apenas este item específico". (À primeira vista, isso pode parecer pouco relevante; mas basta projetar mal uma vez e ver a invalidação atingir um escopo diferente do pretendido para perceber seu verdadeiro valor.)</p>
<h2 id="evolução-do-gerenciamento-de-querykey"><a class="anchor" href="#evolução-do-gerenciamento-de-querykey">Evolução do gerenciamento de queryKey</a></h2>
<p>Até aqui, tratamos do funcionamento e do uso de queryKey. Agora podemos passar à pergunta principal: <strong>como o gerenciamento de queryKey mudou ao longo do tempo?</strong></p>
<p>Vou organizar em ordem cronológica as etapas pelas quais passei em projetos reais.</p>
<h3 id="1-vetores-declarados-diretamente"><a class="anchor" href="#1-vetores-declarados-diretamente">1. Vetores declarados diretamente</a></h3>
<p>Esta é a forma mais simples. Dentro do componente, combinam-se textos fixos com valores das propriedades.</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>No início, isso pode ser suficiente.</p>
<p>O problema aparece à medida que a base de código cresce. Quando se precisa invalidar dados em uma mutação que altera informações de um usuário, é necessário pesquisar toda vez: "Qual era a chave das consultas de usuário?" Alguns lugares acabam usando <code>['user', userId]</code>, enquanto outros usam <code>['users', userId]</code>, no plural. Como essas chaves ocupam posições totalmente diferentes no cache, a invalidação afeta apenas uma delas.</p>
<h3 id="2-objeto-de-constantes"><a class="anchor" href="#2-objeto-de-constantes">2. Objeto de constantes</a></h3>
<p>Para evitar erros de digitação, as chaves de consulta são reunidas em constantes.</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>Os erros de digitação desaparecem, mas a responsabilidade de montar as chaves continua nos locais de uso. Alguém escreve a combinação <code>[QUERY_KEYS.USER, userId]</code> como <code>[QUERY_KEYS.USER, userId, 'detail']</code>, enquanto outra pessoa usa <code>['user', 'detail', userId]</code>. Chega um momento em que é necessário memorizar à parte qual convenção está correta.</p>
<h3 id="3-fábrica-de-chaves-de-consulta"><a class="anchor" href="#3-fábrica-de-chaves-de-consulta">3. Fábrica de chaves de consulta</a></h3>
<p>Esse padrão foi concretizado no artigo <a href="https://tkdodo.eu/blog/effective-react-query-keys" target="_blank" rel="noopener noreferrer">Chaves eficazes no React Query</a>, de TkDodo. Define-se um objeto que cria as chaves de cada domínio, expressando a hierarquia por meio de funções.</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>Esse padrão é poderoso porque <strong>torna a hierarquia explicitamente visível no código</strong>. <code>todoKeys.all</code> representa todas as consultas relacionadas a tarefas, <code>todoKeys.lists()</code> representa todas as consultas em formato de lista, e <code>todoKeys.detail(1)</code> representa um item específico. Assim, o escopo da invalidação pode ser expresso com precisão em uma única linha de código.</p>
<p>Outra vantagem é a <strong>colocalização (co-location)</strong>. TkDodo não recomenda reunir as chaves em um arquivo global. Em vez disso, recomenda colocar <code>queries.ts</code> dentro do diretório do recurso e manter juntos, nesse arquivo, as chaves e os hooks.</p>
<pre><code>src/
└── features/
    └── todos/
        ├── index.tsx
        └── queries.ts   # 키와 훅을 모두 여기에
</code></pre>
<p>Isso cria um modelo mental simples: "Para alterar algo em tarefas, basta olhar a pasta de tarefas". É uma aplicação fiel do princípio de manter juntas as partes que mudam juntas.</p>
<h3 id="4-lukemoralesquery-key-factory"><a class="anchor" href="#4-lukemoralesquery-key-factory">4. @lukemorales/query-key-factory</a></h3>
<p>Ao escrever manualmente o terceiro padrão repetidas vezes, o código repetitivo se acumula. Além disso, quando surge a necessidade de combinar e gerenciar as chaves de vários domínios, faz falta uma interface padronizada. A biblioteca <a href="https://github.com/lukemorales/query-key-factory" target="_blank" rel="noopener noreferrer">@lukemorales/query-key-factory</a> é o resultado da transformação desse padrão em biblioteca.</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> adiciona automaticamente o prefixo, e <code>mergeQueryKeys</code> permite combinar domínios. Além disso, a propriedade convencionada <code>_def</code> dá acesso à chave de todo o domínio. Com isso, desaparece o trabalho de acrescentar <code>as const</code> a cada vez e restringir manualmente os tipos, como era necessário na fábrica artesanal.</p>
<p>Durante algum tempo, essa biblioteca foi usada praticamente como um padrão de mercado. (Eu também a usei bastante.) Mas a chegada de queryOptions mudou o cenário.</p>
<h3 id="5-queryoptions-api-oficial-da-v5"><a class="anchor" href="#5-queryoptions-api-oficial-da-v5">5. queryOptions (API oficial da v5)</a></h3>
<p>Uma das mudanças mais importantes do TanStack Query v5 foi a introdução da API <code>queryOptions</code>. Na migração da v4 para a v5, os argumentos de todos os hooks foram unificados em um único objeto. O verdadeiro objetivo dessa mudança era permitir que esse objeto fosse extraído como <strong>uma unidade reutilizável</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>À primeira vista, pode surgir a dúvida: "O que há de diferente? Parece apenas um objeto envolvido por uma função." TkDodo reconhece esse ponto no artigo <a href="https://tkdodo.eu/blog/the-query-options-api" target="_blank" rel="noopener noreferrer">A API Query Options</a>. Em tempo de execução, ela realmente se limita a devolver o objeto recebido.</p>
<p>O trabalho realmente útil acontece <strong>dentro do sistema de tipos</strong>. Veremos isso a seguir.</p>
<h2 id="datatag-de-queryoptions"><a class="anchor" href="#datatag-de-queryoptions">DataTag de queryOptions</a></h2>
<p>O motivo pelo qual <code>queryOptions</code> não é apenas uma função auxiliar é que ela <strong>incorpora informações sobre o tipo dos dados na queryKey retornada.</strong> Dentro do TanStack Query, esse mecanismo é chamado de <code>DataTag</code>.</p>
<p>Sua implementação aproximada é esta.</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>Trata-se de um <strong>tipo marcado (branded type)</strong> que usa <code>unique symbol</code>. Em tempo de execução, é apenas uma marca sem efeito algum; para o TypeScript, porém, ela carrega a informação de que "este vetor não é um vetor qualquer, mas um vetor associado a dados do tipo <code>TValue</code>".</p>
<p>Há um motivo específico para usar <code>unique symbol</code>. O artigo da Zenn <a href="https://zenn.dev/tsuboi/articles/tanstack-query-options-unique-symbol?locale=en" target="_blank" rel="noopener noreferrer">Revelando o unique symbol por trás de DataTag</a> compara esse recurso a "uma vaga exclusiva para informações de tipo". Uma chave de texto comum poderia colidir com chaves de outras bibliotecas ou do código do usuário; no entanto, <strong>cada declaração de <code>unique symbol</code> cria por si só um tipo exclusivo</strong> e, portanto, nunca tem o mesmo tipo de qualquer outra declaração. Ela se torna um identificador sem possibilidade de colisão.</p>
<p>A diferença produzida por esse único recurso é significativa.</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>Embora <code>getQueryData</code> e <code>setQueryData</code> recebam apenas uma queryKey, a própria queryKey já contém o tipo dos dados; por isso, o tipo de retorno é inferido automaticamente. Não é necessário fornecer parâmetros genéricos manualmente, e o compilador aponta imediatamente uma tentativa de passar a <code>setQueryData</code> um valor de tipo incorreto.</p>
<p>É claro que existem limitações. Em métodos como <code>getQueriesData</code>, que recuperam várias consultas de uma vez, o resultado é um vetor heterogêneo de tuplas e a inferência de tipos não se aplica. Além disso, como a implementação usa <code>unique symbol</code>, a geração de arquivos <code>.d.ts</code> em um monorrepositório pode causar o erro TS4023; uma forma de contorná-lo é importar explicitamente <code>dataTagSymbol</code>.</p>
<p>Ao resumir o mecanismo até aqui, um fato fica claro. <strong>A inferência de tipos de queryOptions depende inteiramente de queryKey e queryFn serem declaradas juntas no mesmo lugar.</strong> Para incorporar em queryKey o tipo retornado por queryFn, ambas precisam ser declaradas lado a lado.</p>
<p>Esse ponto traz uma implicação importante para o rumo das fábricas de chaves de consulta. Os padrões das gerações anteriores davam prioridade a extrair a gestão de queryKey como uma unidade de abstração separada. A recomendação da v5 segue a direção oposta: <strong>reunir novamente queryKey e queryFn em uma única unidade.</strong> TkDodo chega a dizer que "separar queryKey de queryFn foi um erro". Afinal, a chave é o conjunto das dependências usadas pela função, e as duas têm uma relação inseparável.</p>
<h2 id="padrão-de-composição-com-queryoptions-em-projetos-reais"><a class="anchor" href="#padrão-de-composição-com-queryoptions-em-projetos-reais">Padrão de composição com queryOptions em projetos reais</a></h2>
<p>O verdadeiro valor de <code>queryOptions</code> aparece quando ela é combinada a uma fábrica por domínio. A forma recomendada pela documentação oficial da v5 é esta.</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>Vejamos, um a um, os motivos pelos quais esse padrão funciona bem.</p>
<p><strong>1. Ele oferece, ao mesmo tempo, uma hierarquia e inferência de tipos.</strong></p>
<p><code>todoQueries.all()</code> e <code>todoQueries.lists()</code> retornam apenas vetores, enquanto <code>todoQueries.detail(1)</code> retorna, por meio de <code>queryOptions</code>, um objeto com a marca de tipo dos dados. Usa-se o vetor para invalidar e o objeto de opções para executar a consulta.</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. O componente pode sobrescrever parcialmente as opções.</strong></p>
<p>Como o resultado de <code>queryOptions</code> é, no fim das contas, um objeto, algumas opções podem ser combinadas no momento da chamada.</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>Esse padrão é especialmente poderoso porque o tipo retornado por <code>select</code> é inferido automaticamente e o tipo de <code>data</code> é restringido para <code>string</code>. Do ponto de vista do componente, é possível selecionar apenas a parte necessária, mantendo a definição do domínio intacta em um só lugar.</p>
<p><strong>3. Hooks personalizados que envolvem <code>useQuery</code> tornam-se cada vez menos necessários.</strong></p>
<p>Na época da v4, um padrão comum era criar um hook personalizado para cada domínio.</p>
<p>O problema dessa abordagem era que, <strong>assim que surgia a necessidade de fazer uma busca antecipada, era preciso escrever a mesma definição outra vez</strong>. Como <code>useTodoDetail</code> é um hook, ele não pode ser chamado fora de um componente; portanto, no carregador de uma rota ou em um manipulador de eventos, era necessário escrever novamente <code>queryClient.prefetchQuery({ queryKey: [...], queryFn: ... })</code>.</p>
<p>Com <code>queryOptions</code>, essa duplicação desaparece.</p>
<p>Uma única definição funciona em qualquer lugar. Por isso, TkDodo recomenda: "Na v5, defina queryOptions em vez de criar hooks." O hook passa a ser uma camada fina usada apenas quando necessário, e a definição do domínio existe de maneira autossuficiente, sem depender dele.</p>
<h2 id="invalidação-após-mutações"><a class="anchor" href="#invalidação-após-mutações">Invalidação após mutações</a></h2>
<p>É na invalidação após uma mutação que a hierarquia de queryKey realmente se destaca. Segundo a documentação de <a href="https://tanstack.com/query/v5/docs/framework/react/guides/query-invalidation" target="_blank" rel="noopener noreferrer">Invalidação de consultas</a> do TanStack Query, <code>invalidateQueries</code> usa <strong>correspondência por prefixo</strong> por padrão.</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>Quando as chaves são projetadas hierarquicamente, <strong>o escopo da invalidação corresponde ao significado do código.</strong> "Atualize todas as tarefas" é expresso com <code>all()</code>, "atualize apenas as listas" com <code>lists()</code>, e "atualize somente este item" com <code>detail(id)</code>.</p>
<p>Se as chaves estivessem espalhadas de forma plana, como <code>['todoList']</code> e <code>['todoDetail', 1]</code>, seria preciso fazer duas chamadas separadas para invalidar "todo o domínio de tarefas" ou criar e gerenciar uma constante de prefixo específica. (E, sempre que uma nova chave do domínio fosse adicionada sem que alguém se lembrasse de incluí-la nessa constante, surgiria um erro por omissão na invalidação.)</p>
<h2 id="recuperando-querykey-dentro-de-queryfn"><a class="anchor" href="#recuperando-querykey-dentro-de-queryfn">Recuperando queryKey dentro de queryFn</a></h2>
<p>Por fim, há mais um padrão a considerar. <code>queryFn</code> recebe como argumento um objeto chamado <code>QueryFunctionContext</code>, que contém exatamente a queryKey usada no momento da chamada.</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>Por que esse padrão é útil? Segundo o artigo de TkDodo <a href="https://tkdodo.eu/blog/leveraging-the-query-function-context" target="_blank" rel="noopener noreferrer">Aproveitando o contexto da função de consulta</a>, ele permite <strong>forçar a sincronização entre as dependências de queryKey e queryFn</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>Esse código é perigoso porque queryFn depende de uma variável externa. Além disso, mesmo que <code>sortBy</code> mude, o cache não será atualizado, pois essa dependência não foi incluída na chave. Enquanto <code>queryFn</code> capturar variáveis de um fechamento externo, esse tipo de erro poderá acontecer a qualquer momento.</p>
<p>A solução é simples: fazer com que <code>queryFn</code> não dependa de variáveis externas. <strong>Se todas as dependências forem extraídas de queryKey</strong>, uma variável ausente na chave simplesmente não poderá ser usada dentro da função.</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>Com essa estrutura, quando surge uma nova dependência, não há como usá-la na função sem incluí-la em queryKey. O compilador avisa: "Essa chave não existe." Em vez de depender de uma convenção, a sincronização entre chave e função é <strong>delegada ao sistema de tipos</strong>.</p>
<h2 id="até-que-ponto-separar"><a class="anchor" href="#até-que-ponto-separar">Até que ponto separar</a></h2>
<p>Depois de ler até aqui, pode surgir a pergunta: "Então todas as consultas devem ser extraídas para <code>queryOptions</code>?"</p>
<p>Como sempre, minha resposta é: <strong>"Depende da situação."</strong></p>
<p>É importante lembrar que <strong>uma abstração nem sempre é benéfica</strong>. Se uma consulta é usada apenas uma vez, extraí-la à força para uma fábrica de domínio só obriga quem lê o código a alternar entre dois arquivos. A evolução dos padrões de gerenciamento de queryKey não significa que "a ferramenta mais sofisticada deve ser usada sempre", mas que <strong>"há a opção de subir um degrau de cada vez quando surgir a necessidade"</strong>.</p>
<h2 id="conclusão"><a class="anchor" href="#conclusão">Conclusão</a></h2>
<p>Em resumo, queryKey é <strong>a unidade mais fundamental usada pelo TanStack Query para identificar e armazenar dados assíncronos em cache</strong>. Nesse pequeno vetor estão condensados o identificador de uma posição no cache, o vetor de dependências, o escopo de invalidação e, desde a v5, até informações sobre o tipo dos dados. Como tantas responsabilidades convergem para esse único ponto, a maneira de escrever e gerenciar queryKey afeta diretamente a carga cognitiva de toda a base de código.</p>
<p>Cada etapa foi uma resposta a um problema real enfrentado por alguém naquele momento. Portanto, o caminho correto não é simplesmente pensar: "Agora estamos na v5, então sempre devemos usar apenas <code>queryOptions</code>", mas <strong>"Que tipo de problema minha base de código enfrenta neste momento?"</strong> Introduzir uma fábrica de domínio em um projeto para o qual vetores declarados diretamente já são suficientes pode, por si só, ser um excesso de engenharia.</p>
<p>Espero que quem leu este artigo também examine seu próprio projeto: como queryKey está espalhada pela base de código, como as invalidações são realizadas e se essa estrutura é adequada ao tamanho atual da equipe e à complexidade do domínio.</p>
<h2 id="referências"><a class="anchor" href="#referências">Referências</a></h2>
<details class="ref-block"><summary class="ref-summary">참고 자료</summary><div class="ref-list">
<div class="ref-item">
<div class="ref-item-inner">[documentação] <a href="https://tanstack.com/query/latest/docs/framework/react/guides/query-keys" target="_blank" rel="noopener noreferrer">TanStack Query, Chaves de consulta</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner">[documentação] <a href="https://tanstack.com/query/v5/docs/framework/react/guides/query-options" target="_blank" rel="noopener noreferrer">TanStack Query, Opções de consulta</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner">[documentação] <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:#9ca3af"></span><a href="https://tanstack.com/blog/announcing-tanstack-query-v5" target="_blank" rel="noopener noreferrer">TanStack, Anúncio do 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[Tratamento de erros]]></title>
            <link>https://hooninedev.com/pt-BR/251117</link>
            <guid isPermaLink="false">https://hooninedev.com/pt-BR/251117</guid>
            <pubDate>Mon, 17 Nov 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Neste post, quero falar sobre como capturar erros no frontend. Ao escrever código de tratamento de erros no trabalho, muitas vezes fiquei com uma incômoda sensação de que algo não estava certo. Alguns...]]></description>
            <content:encoded><![CDATA[<p>Neste post, quero falar sobre <strong>como capturar erros no frontend</strong>.</p>
<p>Ao escrever código de tratamento de erros no trabalho, muitas vezes fiquei com uma incômoda sensação de que algo não estava certo. Alguns erros eram capturados com <code>try/catch</code>, outros por <code>ErrorBoundary</code>, e outros ainda pelo <code>onError</code> do TanStack Query. Além disso, os limites de atuação de cada recurso se sobrepunham ou deixavam pequenas lacunas. Em certos dias, um erro escapava; em outros, propagava-se até onde eu não queria.</p>
<p>O problema é que raramente paramos para organizar, de uma só vez, como todas essas ferramentas funcionam. Sabemos que “Error Boundary só captura erros durante a renderização”, mas, se alguém pedir para explicar exatamente o que isso significa na prática, o que acontece internamente ao chamar <code>reset</code> ou em que momento o TanStack Query relança um erro quando <code>throwOnError</code> está habilitado, a resposta já não vem com tanta facilidade.</p>
<p>Com base no guia oficial do React, na biblioteca <code>react-error-boundary</code> e na documentação oficial do TanStack Query v5, este artigo organiza <strong>até onde vai a responsabilidade</strong> de cada ferramenta de tratamento de erros no frontend e <strong>como combiná-las</strong>.</p>
<h2 id="erros-que-o-react-consegue-e-não-consegue-capturar"><a class="anchor" href="#erros-que-o-react-consegue-e-não-consegue-capturar">Erros que o React consegue e não consegue capturar</a></h2>
<p>Vamos começar pela pergunta mais básica: <strong>quais erros o React captura?</strong></p>
<p>A documentação oficial do React distingue com clareza os erros que uma Error Boundary consegue capturar daqueles que ficam fora de seu alcance.</p>
<p><strong>O que uma Error Boundary captura</strong></p>
<ul>
<li>Erros ocorridos durante a <strong>renderização</strong> de componentes filhos</li>
<li>Erros ocorridos em <strong>métodos do ciclo de vida</strong></li>
<li>Erros ocorridos no <strong>construtor</strong></li>
</ul>
<p><strong>O que uma Error Boundary não captura</strong></p>
<ul>
<li>Erros dentro de <strong>manipuladores de eventos</strong></li>
<li>Erros em <strong>código assíncrono</strong>, como <code>setTimeout</code>, <code>requestAnimationFrame</code> e Promise</li>
<li>Erros durante a <strong>renderização no servidor (SSR)</strong></li>
<li>Erros ocorridos na <strong>própria Error Boundary</strong></li>
</ul>
<p>Por que essa distinção é importante? Porque a maioria dos erros com que lidamos no dia a dia pertence, na verdade, <strong>à segunda categoria</strong>. O servidor pode responder com 500 após um clique disparar uma mutação; uma busca de dados pode falhar dentro de <code>useEffect</code>; ou a lógica de validação pode lançar um erro durante o envio de um formulário. O React não captura esses erros automaticamente. Precisamos capturá-los e tratá-los de forma explícita.</p>
<p>Por isso, o tratamento de erros no frontend se divide em dois caminhos: <strong>erros de renderização ficam a cargo da Error Boundary</strong>; <strong>os demais, de try/catch ou das funções de retorno fornecidas pelas bibliotecas</strong>. No ponto em que esses caminhos se cruzam, bibliotecas de gerenciamento de estado assíncrono, como o TanStack Query, funcionam como uma ponte.</p>
<h2 id="o-que-é-uma-error-boundary"><a class="anchor" href="#o-que-é-uma-error-boundary">O que é uma Error Boundary</a></h2>
<p>No fim das contas, uma Error Boundary é um <strong>componente de classe</strong> com dois métodos de ciclo de vida. Segundo a documentação oficial do React, para que um componente seja uma Error Boundary, ele precisa implementar um dos dois métodos abaixo — normalmente, ambos.</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> deve ser uma <strong>função pura</strong>. Sua única função é retornar o novo estado, sem efeitos colaterais. Já <code>componentDidCatch</code> é o lugar destinado aos efeitos colaterais. É ali que enviamos o erro ao Sentry ou registramos a pilha de componentes no console.</p>
<p>Há um ponto importante: esses dois métodos <strong>só existem em componentes de classe</strong>. Ainda não há uma forma oficial de criar uma Error Boundary como componente funcional. A <a href="https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary" target="_blank" rel="noopener noreferrer">documentação oficial do React</a> deixa isso explícito.</p>
<blockquote class="bilingual-quote"><div class="quote-translation" lang="ko"><p>Atualmente, não há como escrever uma Error Boundary como componente funcional.</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>Como é trabalhoso escrever um componente de classe do zero toda vez, normalmente recorremos à biblioteca <code>react-error-boundary</code>. Ela foi criada por Brian Vaughn, ex-membro da equipe principal do React, e é usada praticamente como um padrão.</p>
<h2 id="as-três-formas-de-definir-a-interface-de-contingência-em-react-error-boundary"><a class="anchor" href="#as-três-formas-de-definir-a-interface-de-contingência-em-react-error-boundary">As três formas de definir a interface de contingência em react-error-boundary</a></h2>
<p>Em <code>react-error-boundary</code>, o componente <code>ErrorBoundary</code> oferece <strong>três formas</strong> de definir a propriedade da interface de contingência. Vejamos rapidamente como cada uma é usada.</p>
<h3 id="fallback"><a class="anchor" href="#fallback">fallback</a></h3>
<p>É a forma mais simples: basta passar um JSX estático.</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>É útil quando não precisamos acessar o objeto de erro nem a função de reinicialização. Como normalmente precisamos exibir uma mensagem ou oferecer uma ação de nova tentativa, ainda não tive ocasião de usar essa opção em produção.</p>
<h3 id="fallbackcomponent"><a class="anchor" href="#fallbackcomponent">FallbackComponent</a></h3>
<p>Separamos a interface de contingência em outro componente e passamos a <strong>referência</strong> desse componente.</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>O objeto de erro e a função <code>resetErrorBoundary</code> são injetados automaticamente por meio de propriedades. Essa opção é adequada quando existe a possibilidade de reutilizar a interface de contingência em outros lugares.</p>
<h3 id="fallbackrender"><a class="anchor" href="#fallbackrender">fallbackRender</a></h3>
<p>É a opção usada quando queremos escrever a interface de contingência diretamente no local de uso.</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>Na essência, faz o mesmo que <code>FallbackComponent</code>, mas permite tratar tudo <strong>diretamente no local de uso, sem criar um componente separado</strong>. É útil quando precisamos acessar o escopo léxico externo, como o estado ou um manipulador do componente pai.</p>
<p>Não existe uma única resposta correta entre as três opções. O padrão que mais uso em produção é <strong>criar um componente ErrorFallback comum e injetá-lo por meio de <code>FallbackComponent</code></strong>, porque o sistema de design e o tom da interface precisam ser consistentes. Só escrevo a interface de contingência diretamente no local de uso com <code>fallbackRender</code> quando uma página exige um tratamento diferente.</p>
<h2 id="o-que-a-reinicialização-realmente-faz"><a class="anchor" href="#o-que-a-reinicialização-realmente-faz">O que a reinicialização realmente faz?</a></h2>
<p>Ao usar <code>react-error-boundary</code>, inevitavelmente encontramos a função <code>resetErrorBoundary</code>: aquela chamada quando o usuário clica no botão “Tentar novamente” da interface de contingência. Vejamos o que ela realmente faz.</p>
<p>Em resumo, <code>resetErrorBoundary</code> apenas sinaliza ao componente ErrorBoundary que ele deve <strong>reinicializar o próprio estado e renderizar novamente os componentes filhos</strong>. Ela não altera automaticamente nenhum estado externo, como o cache do TanStack Query.</p>
<p>Internamente, a sequência é a seguinte.</p>
<ol>
<li><code>resetErrorBoundary()</code> é chamada.</li>
<li>O estado <code>hasError</code> interno da ErrorBoundary volta a <code>false</code>.</li>
<li>Opcionalmente, a função de retorno <code>onReset</code> é executada. É aqui que ocorrem os efeitos colaterais definidos pela aplicação.</li>
<li>Os componentes filhos são renderizados novamente. Se a causa do erro — estado, cache ou outra condição — continuar presente, <strong>o mesmo erro será lançado outra vez</strong>.</li>
</ol>
<p>O quarto item é o ponto central. <strong>Reinicializar significa apenas “vamos esquecer o erro e tentar renderizar de novo”; não significa “vamos corrigir a causa do erro”</strong>. Por isso, apenas reinicializar pode repetir o mesmo erro indefinidamente.</p>
<p>Há mais duas ferramentas para resolver esse problema.</p>
<h3 id="onreset"><a class="anchor" href="#onreset">onReset</a></h3>
<p>Funciona como um hook chamado imediatamente antes da reinicialização. Nele, limpamos o estado externo que causou o erro.</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>A ErrorBoundary é reinicializada automaticamente quando os valores da lista mudam. Podemos passar parâmetros da URL, termos de busca, a aba selecionada ou qualquer outra chave cuja mudança indique que vale a pena tentar novamente.</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>Quando <code>userId</code> muda, a reinicialização ocorre automaticamente e os componentes filhos são renderizados de novo. Ao navegar para outro perfil, o erro anterior desaparece de forma natural.</p>
<h2 id="como-capturar-erros-de-manipuladores-de-eventos-e-de-código-assíncrono"><a class="anchor" href="#como-capturar-erros-de-manipuladores-de-eventos-e-de-código-assíncrono">Como capturar erros de manipuladores de eventos e de código assíncrono?</a></h2>
<p>Como vimos, uma Error Boundary não captura erros em manipuladores de eventos nem em código assíncrono. Mas é justamente aí que ocorre a maioria dos erros com que lidamos. O que fazer, então?</p>
<p>Para esse caso, <code>react-error-boundary</code> oferece o <strong>hook <code>useErrorBoundary</code></strong>. Ele retorna uma função chamada <code>showBoundary</code>; ao chamá-la, podemos encaminhar o erro à ErrorBoundary mais próxima.</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>O ponto central é que <strong>o desenvolvedor precisa elevar o erro explicitamente</strong>. O React não faz isso por conta própria. Para levar um erro assíncrono até o domínio de uma ErrorBoundary, é preciso capturá-lo com <code>try/catch</code> e passá-lo a <code>showBoundary</code>.</p>
<p>Entender esse padrão esclarece por que uma ErrorBoundary captura alguns erros e não outros. A resposta é simples: <strong>“o erro foi elevado até a fase de renderização ou não?”</strong></p>
<h2 id="como-o-tanstack-query-trata-erros"><a class="anchor" href="#como-o-tanstack-query-trata-erros">Como o TanStack Query trata erros?</a></h2>
<p>Depois de organizar tudo até aqui, surge naturalmente outra pergunta. O <code>useQuery</code> que usamos todos os dias lida com requisições assíncronas; como são tratados os erros que ocorrem dentro dele?</p>
<p>Por padrão, o TanStack Query <strong>expõe o erro no campo <code>error</code></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>Essa é a forma mais simples. Mesmo quando ocorre um erro, o componente continua renderizando normalmente; apenas o campo <code>error</code> recebe um valor. A ErrorBoundary não participa desse fluxo.</p>
<p>Vale destacar um fato importante: <strong>o comportamento padrão do TanStack Query é “não lançar o erro”</strong>. Não importa se a queryFn lança uma exceção ou rejeita uma Promise: o erro é armazenado no campo <code>error</code> sem interromper o fluxo de renderização do React. Portanto, sem nenhuma configuração adicional, a ErrorBoundary nunca será acionada.</p>
<p>Além disso, por padrão, o TanStack Query <strong>repete automaticamente uma consulta com erro três vezes</strong>.</p>
<p>O <code>retryDelay</code> padrão usa intervalos exponenciais, chegando a no máximo 30 segundos. Isso significa que o erro não aparece para o usuário assim que ocorre a primeira falha. A consulta é repetida após intervalos de 1, 2 e 4 segundos; somente se todas as tentativas falharem o campo <code>error</code> é preenchido. Se você já se perguntou durante o desenvolvimento “por que o erro demora para aparecer?”, é quase certo que esse seja o motivo.</p>
<h3 id="conectando-à-errorboundary-com-throwonerror"><a class="anchor" href="#conectando-à-errorboundary-com-throwonerror">Conectando à ErrorBoundary com throwOnError</a></h3>
<p>Como encaminhar, então, os erros do TanStack Query para uma ErrorBoundary? A resposta é a opção <strong><code>throwOnError</code></strong>. Até a v4, ela se chamava <code>useErrorBoundary</code>; na v5, passou a se chamar <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>Quando essa opção está habilitada, o TanStack Query <strong>relança o erro no próximo ciclo de renderização</strong>. O lançamento passa, então, a ser um erro da fase de renderização, que finalmente pode ser capturado pela ErrorBoundary.</p>
<p><code>throwOnError</code> também pode receber uma função. Assim, podemos encaminhar alguns erros à ErrorBoundary e deixar que o próprio componente trate os demais.</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>Esse padrão é prático porque <strong>erros de cliente 4xx, como falha de validação ou falta de permissão</strong>, normalmente devem ser exibidos no próprio contexto em que ocorreram, enquanto <strong>erros de servidor 5xx</strong> justificam cobrir a página inteira e mostrar uma mensagem como “Tente novamente em alguns instantes”.</p>
<h3 id="usesuspensequery"><a class="anchor" href="#usesuspensequery">useSuspenseQuery</a></h3>
<p>Se você usa <code>useSuspenseQuery</code>, não precisa se preocupar com <code>throwOnError</code>. No modo Suspense, <strong>lançar o erro sempre é o comportamento padrão</strong>.</p>
<p>Em outras palavras, usar <code>useSuspenseQuery</code> significa delegar <strong>o carregamento ao Suspense e os erros à ErrorBoundary</strong>. Deixamos de precisar de ramificações como <code>if (isError)</code> ou <code>if (isLoading)</code> dentro do componente e passamos a envolvê-lo externamente com esses dois limites.</p>
<h2 id="queryerrorresetboundary"><a class="anchor" href="#queryerrorresetboundary">QueryErrorResetBoundary</a></h2>
<p>Neste ponto, surge mais uma pergunta: o que acontece quando o usuário clica em “Tentar novamente” na interface de contingência?</p>
<p>Como vimos, <code>resetErrorBoundary</code> apenas reinicializa o estado <code>hasError</code> da ErrorBoundary. Porém, no cache do TanStack Query, continua existindo <strong>uma consulta presa no estado de erro</strong>. Quando os componentes filhos são renderizados novamente, o TanStack Query consulta o cache, conclui que a consulta já falhou e lança o mesmo erro imediatamente. É um ciclo infinito infernal.</p>
<p>Para resolver esse problema, o TanStack Query oferece o hook <strong><code>useQueryErrorResetBoundary</code></strong> e o componente <strong><code>QueryErrorResetBoundary</code></strong>. Apesar dos nomes longos, a função é simples: emitir o comando <strong>“reinicialize o estado de erro das consultas dentro deste escopo”</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>Vejamos em ordem cronológica o que acontece aqui.</p>
<ol>
<li>O usuário clica em “Tentar novamente” → <code>resetErrorBoundary()</code> é chamada</li>
<li>A ErrorBoundary executa a função de retorno <code>onReset</code> → <code>reset()</code> é chamada, reinicializando o estado de erro do TanStack Query</li>
<li>A ErrorBoundary reinicializa o próprio estado e renderiza os componentes filhos novamente</li>
<li>O <code>useQuery</code> dentro dos componentes filhos é executado → como o estado de erro foi removido, uma nova busca de dados é iniciada</li>
</ol>
<p>O ponto central é a conexão feita em <code>onReset</code> com <code>reset</code>. Graças a essa única linha, a ErrorBoundary e o TanStack Query sincronizam seus estados.</p>
<h3 id="usando-a-forma-de-componente"><a class="anchor" href="#usando-a-forma-de-componente">Usando a forma de componente</a></h3>
<p>É possível fazer a mesma coisa com um componente em vez do hook. Basta escolher uma das duas formas.</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>A maior diferença em relação à versão em hook é que a função <code>reset</code> é passada aos filhos por meio do padrão de <strong>propriedade de renderização</strong>. <code>QueryErrorResetBoundary</code> recebe uma função como conteúdo filho, passa <code>{ reset }</code> como argumento e renderiza o valor retornado por essa função. Assim, podemos conectá-la imediatamente com <code>onReset={reset}</code> dentro do componente.</p>
<p>Quando não há uma <code>QueryErrorResetBoundary</code> próxima, a versão em hook <strong>reinicializa os erros do cache global</strong>. A versão em componente restringe o escopo da reinicialização à própria subárvore. Para controlar esse alcance de forma mais granular, a versão em componente é mais segura.</p>
<p>Vale ressaltar um ponto: <strong>a reinicialização não limpa o cache</strong>. Em vez de apagar todos os dados, ela apenas libera o estado das consultas marcadas com erro. Para invalidar os dados de fato, é preciso chamar <code>queryClient.invalidateQueries()</code> separadamente.</p>
<h2 id="erros-de-mutações"><a class="anchor" href="#erros-de-mutações">Erros de mutações</a></h2>
<p>Quase todos os padrões apresentados até aqui partiram de <code>useQuery</code>. Com <strong><code>useMutation</code>, porém, a situação é um pouco diferente.</strong></p>
<p>A principal diferença é que uma mutação normalmente começa com uma <strong>ação explícita do usuário, como um clique ou envio de formulário</strong>. Por isso, é natural tratar o erro perto dessa ação. Em vez de cobrir a página inteira com uma interface de contingência, faz mais sentido mostrar uma notificação ou um texto de erro ao lado do formulário, como “Falha no pagamento: confira novamente os dados do cartão”.</p>
<p>Em <a href="https://tkdodo.eu/blog/mastering-mutations-in-react-query" target="_blank" rel="noopener noreferrer">Dominando mutações no React Query</a>, TkDodo resume a essência dessa diferença em uma frase: <strong>uma consulta é declarativa, enquanto uma mutação é imperativa</strong>. Uma consulta é executada automaticamente quando o componente é montado, pode ser observada por outros componentes com a mesma chave e é armazenada em cache para reutilização. Já uma mutação só é executada quando o usuário aciona uma operação, não é armazenada em cache e fica vinculada individualmente à instância do componente que a chamou. Essa diferença fundamental separa também as estratégias de tratamento de erros.</p>
<p>No <code>useQuery</code>, o <code>retry</code> padrão é <code>3</code>, mas no <strong><code>useMutation</code> o <code>retry</code> padrão é <code>0</code></strong>. O motivo é simples: uma mutação produz <strong>efeitos colaterais</strong>. Se uma requisição de pagamento falhar por esgotamento do tempo de espera da rede e a biblioteca repetir a chamada automaticamente mais duas vezes, o cartão do usuário poderá ser cobrado três vezes.</p>
<p>Por isso, a regra é habilitar novas tentativas para mutações de forma explícita <strong>apenas quando for possível garantir que a operação é idempotente</strong>. Isso vale para leituras seguras semelhantes a GET, nas quais repetir a requisição produz comprovadamente o mesmo resultado, ou para casos em que o servidor impede duplicações por meio de uma chave de idempotência.</p>
<p>Os erros de <code>useQuery</code> ficam <strong>registrados no cache</strong>. Assim, propagam-se imediatamente para outros componentes que observam a mesma <code>queryKey</code>, exigindo um mecanismo como <code>QueryErrorResetBoundary</code> para reinicializá-los em conjunto.</p>
<p>Com mutações é diferente. Um erro em uma instância de mutação permanece <strong>apenas no estado daquela instância</strong>. Ele não afeta a mutação de outro componente que use a mesma <code>mutationFn</code>. Por isso, o TanStack Query não tem algo como <code>MutationErrorResetBoundary</code>: <strong>não há necessidade</strong>.</p>
<p>Essa diferença tem uma consequência prática. Quando dois componentes chamam o mesmo <code>useMutation</code>, o erro ocorrido em um deles não aparece no outro. Para observar “os erros dessa mutação em toda a aplicação”, o <code>onError</code> no nível do componente não basta; é preciso elevá-los por meio de <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> retorna duas funções de execução. A diferença entre elas determina a forma de tratar erros.</p>
<p>O tipo de retorno de mutate é <code>void</code>. Ela não retorna uma Promise. Portanto, não podemos aguardar o resultado com await, e ele só pode ser recebido por funções de retorno como <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>Já <code>mutateAsync</code> retorna uma Promise. Isso permite tratar o erro com <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>Quando usar cada uma? Adoto os seguintes critérios.</p>
<ul>
<li><strong>Há uma ação subsequente após o fim da mutação</strong>, como navegar em caso de sucesso ou usar o valor retornado → <code>mutateAsync</code></li>
<li><strong>Basta disparar a chamada e delegar os efeitos colaterais às funções de retorno</strong>, como alternar uma curtida ou apenas exibir uma notificação → <code>mutate</code> + <code>onError</code></li>
</ul>
<p>Há um erro comum nesse ponto: <strong>usar <code>mutateAsync</code> sem <code>try/catch</code> causa uma rejeição de promessa não tratada</strong>. A função <code>mutate</code>, baseada em funções de retorno, absorve o erro internamente; <code>mutateAsync</code>, por sua vez, lança o erro para o chamador por padrão. Misturar as duas abordagens sem conhecer essa diferença enche o console de alertas vermelhos.</p>
<h3 id="onerror"><a class="anchor" href="#onerror">onError</a></h3>
<p>Outro detalhe frequentemente ignorado é que, em <code>useMutation</code>, <code>onError</code> pode ser definido <strong>em dois lugares</strong>: no hook e em mutate.</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>No nível do hook, ele sempre é executado; no nível de mutate, é executado apenas no momento da chamada.</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>A ordem de execução indicada pela documentação oficial é: <strong>nível do hook → nível de mutate</strong>. Quando as duas funções de retorno estão definidas, a do hook é executada primeiro e, depois, a de mutate.</p>
<h2 id="tratamento-global-de-erros"><a class="anchor" href="#tratamento-global-de-erros">Tratamento global de erros</a></h2>
<p>Até aqui, todos os padrões atuavam no nível do componente. Mas podemos ter requisitos como “registrar todos os erros de consultas em um único lugar” ou “sempre encerrar a sessão ao receber um erro 401”. Para interesses transversais como esses, podemos registrar funções de retorno em <code>QueryCache</code>/<code>MutationCache</code> ao criar o <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>O ponto central é que <code>QueryCache.onError</code> é chamado <strong>apenas uma vez por consulta</strong>. Mesmo que vários componentes observem a mesma consulta, a função de retorno é executada uma única vez, evitando problemas como notificações duplicadas.</p>
<p>Também podemos verificar <code>query.state.data !== undefined</code>, como no exemplo acima. Quando ocorre <strong>uma falha ao buscar novamente enquanto já existem dados em cache</strong>, o usuário ainda está vendo os dados na tela. Cobrir a página com uma ErrorBoundary seria excessivo; basta informar que a atualização falhou. Por outro lado, se o primeiro carregamento falhar sem que existam dados em cache, faz sentido a ErrorBoundary capturar o erro e exibir a interface de contingência.</p>
<p>Ao combinar esses dois fluxos, podemos definir uma política clara: “falhas no carregamento inicial vão para a ErrorBoundary; falhas ao buscar novamente em segundo plano geram uma notificação”.</p>
<h2 id="componente-compartilhado"><a class="anchor" href="#componente-compartilhado">Componente compartilhado</a></h2>
<p>Depois de chegar até aqui, é natural querer evitar envolver cada trecho, toda vez, em três camadas de <code>QueryErrorResetBoundary</code>, <code>ErrorBoundary</code> e <code>Suspense</code>. Que tal <strong>reuni-las em um único componente reutilizável</strong>?</p>
<p>É uma ideia natural. Eu mesmo já criei e usei um componente <code>AsyncBoundary</code> como o seguinte.</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>Na página, tudo se resume a uma linha.</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>Parece elegante. No entanto, recebi o seguinte feedback de um colega.</p>
<blockquote>
<p>Como AsyncBoundary não é um nome usado de forma tão consagrada, acho que o conteúdo interno não causaria grande estranheza. Mesmo assim, <strong>é um pouco difícil prever que ali também existe uma ResetBoundary do React Query</strong>.</p>
</blockquote>
<blockquote>
<p>Também me incomoda um pouco que <code>pendingFallback</code> e <code>rejectedFallback</code> tenham valores padrão. Ao ver apenas uma linha com <code>&#x3C;AsyncBoundary></code>, não dá para saber qual interface de contingência será exibida; <strong>talvez a pessoa nem perceba que esses valores vêm das propriedades padrão</strong>.</p>
</blockquote>
<h3 id="o-nome-esconde-a-dependência"><a class="anchor" href="#o-nome-esconde-a-dependência">O nome esconde a dependência</a></h3>
<p>O nome desse componente é <code>AsyncBoundary</code>. Ele comunica apenas a ideia de um limite assíncrono. Sua implementação, porém, é <strong>fortemente acoplada ao TanStack Query</strong>: contém <code>QueryErrorResetBoundary</code> e conecta <code>onReset</code> a <code>reset</code>. Na prática, trata-se de <strong>“um limite para áreas assíncronas que usam React Query”</strong>, mas o nome não revela nada disso.</p>
<p>Por que isso é um problema? Porque <strong>contraria as expectativas de quem lê</strong>. Não lemos código interpretando cada linha isoladamente; lemos <strong>antecipando</strong> padrões que acumulamos com a experiência. Quando essa previsão falha, a carga cognitiva aumenta de forma abrupta.</p>
<p>Ao encontrar <code>AsyncBoundary</code> pela primeira vez, um colega imagina “um limite genérico para processamento assíncrono”. Parece que ele poderia ser usado com SWR ou com uma busca direta de dados. Na realidade, porém, há uma <code>QueryErrorResetBoundary</code> embutida, criando <strong>um acoplamento sem utilidade em contextos que não usam TanStack Query</strong>. Existe uma fissura entre o nome e a implementação.</p>
<p>Podemos interpretar isso como uma espécie de abstração com vazamento na direção oposta. Em geral, um vazamento ocorre quando “um detalhe que deveria estar escondido atrás da abstração escapa”; aqui, <strong>uma dependência que deveria estar evidente ficou escondida demais atrás do nome</strong>. Talvez seja ainda pior, porque usamos o componente sem sequer perceber.</p>
<h3 id="tornando-a-dependência-explícita-no-nome"><a class="anchor" href="#tornando-a-dependência-explícita-no-nome">Tornando a dependência explícita no nome</a></h3>
<p>A solução mais simples é mudar o nome. Em vez de <code>AsyncBoundary</code>, usar algo como <strong><code>QueryAsyncBoundary</code></strong>, deixando a dependência explícita. Ao analisar a biblioteca <a href="https://suspensive.org/" target="_blank" rel="noopener noreferrer">Suspensive</a>, criada pela Toss, vi que ela também explicita essa dependência. O pacote <code>@suspensive/react</code> contém apenas as versões genéricas de <code>ErrorBoundary</code> e <code>Suspense</code>; já o componente integrado ao TanStack Query fica separado no pacote <code>@suspensive/react-query</code>, como <code>QueryAsyncBoundary</code>.</p>
<p>Uma única palavra faz muita diferença na quantidade de informação transmitida a quem lê o código. Assim que aparece o prefixo <code>Query</code>, fica imediatamente claro: <strong>“este componente é exclusivo para um ambiente com TanStack Query”</strong>. Isso evita, de antemão, seu uso em um contexto inadequado.</p>
<h3 id="decompondo-em-unidades-combináveis"><a class="anchor" href="#decompondo-em-unidades-combináveis">Decompondo em unidades combináveis</a></h3>
<p>Uma abordagem mais fundamental é <strong>não agrupar</strong>.</p>
<p>ErrorBoundary e Suspense representam, em essência, <strong>interesses diferentes</strong>. Ao reuni-los em um só componente, podemos perder flexibilidade de composição. Algumas páginas precisam apenas de ErrorBoundary; outras, apenas de Suspense; e outras podem querer duas instâncias de Suspense dentro de uma única ErrorBoundary. Agrupar tudo em <code>AsyncBoundary</code> torna essas variações pouco naturais. Mantendo os componentes separados, podemos combiná-los livremente.</p>
<p>Esse padrão deixa o código uma linha mais longo, mas tem a vantagem de permitir que <strong>a responsabilidade de cada limite seja lida diretamente no código</strong>. Além disso, ao usar <code>useSuspenseQuery</code>, a unidade que queremos carregar de uma só vez costuma ser diferente da unidade em que queremos capturar erros, por isso a separação tende a ser mais natural.</p>
<p>Minha conclusão foi esta: <strong>agrupe quando o padrão de composição repetido for realmente idêntico; se houver necessidade de variação, mantenha separado</strong>. E, mesmo ao agrupar, torne a dependência visível no nome. Só esses dois princípios já reduzem a chance de receber em uma revisão o comentário “não sei o que existe dentro de AsyncBoundary”.</p>
<h3 id="propriedades-padrão"><a class="anchor" href="#propriedades-padrão">Propriedades padrão</a></h3>
<p>Corrigir apenas o nome não basta. Voltemos ao código anterior.</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><code>&#x3C;QueryAsyncBoundary>...&#x3C;/QueryAsyncBoundary></code> funciona sozinho em uma única linha porque <code>Spinner</code> e <code>ErrorFallback</code> são inseridos automaticamente. <strong>Essa informação não pode ser inferida pelo nome</strong>.</p>
<p>É outra versão do problema anterior, em que “o nome esconde a dependência”. O prefixo <code>Query</code> passou a revelar a dependência, mas as dependências de interface <code>Spinner</code> e <code>ErrorFallback</code> continuam ocultas atrás das propriedades padrão. <strong>O esconderijo apenas mudou um nível para dentro</strong>.</p>
<p>A solução é simples: <strong>tornar ambas as interfaces de contingência propriedades obrigatórias e injetá-las sempre no ponto de uso</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>O código ganha duas linhas. O motivo para aceitar esse custo é claro: <strong>aumentamos o trabalho de quem escreve para reduzir o custo de investigação de todos que leem</strong>. A interface de contingência exibida fica visível no próprio ponto de uso. Não é preciso abrir outro arquivo para verificar “qual era mesmo o valor padrão deste componente?”. A conhecida máxima de que código é lido muito mais vezes do que é escrito também se aplica aqui.</p>
<h2 id="errorfallback"><a class="anchor" href="#errorfallback">ErrorFallback</a></h2>
<p>Há ainda outro aspecto a considerar. Normalmente, criamos um único componente <code>ErrorFallback</code> como este.</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>É uma implementação bem cuidada, que inclui até <code>role="alert"</code> e <code>aria-live="assertive"</code>. Mas vale fazer uma pergunta: <strong>“é adequado mostrar a mesma tela para 401, 404, 500 e falta de conexão?”</strong></p>
<p>Na maioria dos casos, a resposta é <strong>não</strong>, porque a ação que o usuário precisa tomar varia conforme o tipo de erro.</p>
<table>
<thead>
<tr>
<th>Tipo de erro</th>
<th>Ação do usuário</th>
<th>“Tentar novamente” faz sentido?</th>
</tr>
</thead>
<tbody>
<tr>
<td>Sem conexão</td>
<td>Verificar a conexão e tentar novamente</td>
<td>O</td>
</tr>
<tr>
<td>Erro 5xx do servidor</td>
<td>Tentar novamente após alguns instantes</td>
<td>O</td>
</tr>
<tr>
<td>Falha de autenticação 401</td>
<td>Ir para a tela de login</td>
<td>X</td>
</tr>
<tr>
<td>Sem permissão 403</td>
<td>Ir para outra tela</td>
<td>X</td>
</tr>
<tr>
<td>Recurso não encontrado 404</td>
<td>Voltar à lista</td>
<td>△</td>
</tr>
<tr>
<td>Falha de validação 422</td>
<td>Corrigir os dados inseridos</td>
<td>X</td>
</tr>
</tbody>
</table>
<p>Exibir o botão “Tentar novamente” em todos os casos equivale a <strong>orientar o usuário de forma incorreta sobre “a ação capaz de resolver o erro”</strong>. Clicar em “Tentar novamente” após um 401 só produz outro 401. O que o usuário realmente precisa fazer é entrar novamente.</p>
<p>Por isso, a interface de contingência do erro deve <strong>variar conforme o tipo de erro</strong>. Não é necessário começar com um enorme <code>if/else</code>; podemos criar componentes pequenos e escolher entre eles.</p>
<p>Cada componente de contingência deve expor apenas a mensagem e a ação adequadas ao erro correspondente. A tela deve mostrar somente ações que o usuário realmente pode realizar.</p>
<h3 id="shouldcatch"><a class="anchor" href="#shouldcatch">shouldCatch</a></h3>
<p>Indo um passo além, existe também o padrão de <strong>distinguir, no nível do componente, “os erros que devem ser capturados” daqueles que devem “seguir adiante”</strong>. A <code>ErrorBoundary</code> do Suspensive oferece a propriedade <code>shouldCatch</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:#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>A ErrorBoundary interna captura apenas erros de rede, não erros 5xx. Os erros que ela não captura <strong>sobem para a ErrorBoundary superior</strong> de acordo com o comportamento padrão do React. Assim, a ErrorBoundary externa fica responsável pelos erros 5xx. Em comparação com implementar o mesmo tratamento em uma condicional if/else, é atraente poder <strong>atribuir significado aos próprios limites</strong>.</p>
<p><code>react-error-boundary</code> não oferece essa propriedade, mas podemos obter o mesmo efeito fazendo a ramificação dentro da interface de contingência. O padrão em si é mais importante do que a biblioteca.</p>
<h2 id="conclusão"><a class="anchor" href="#conclusão">Conclusão</a></h2>
<p>Em resumo, o tratamento de erros no frontend <strong>não se resolve com uma única ferramenta</strong>. Erros de renderização ficam a cargo da Error Boundary; erros em manipuladores de eventos, de <code>try/catch</code> ou <code>showBoundary</code>; erros na obtenção assíncrona de dados, de <code>throwOnError</code> e <code>useQueryErrorResetBoundary</code> do TanStack Query; erros de mutação, de <code>mutateAsync</code> ou <code>onError</code>; e interesses transversais, de <code>QueryCache</code>/<code>MutationCache</code>. Além disso, precisamos projetar em conjunto <strong>o nome e a unidade de composição dos componentes compartilhados</strong> e <strong>a modelagem de domínio dos próprios tipos de erro</strong> para chegar a uma política de erros consistente.</p>
<p>Quando entendemos a responsabilidade de cada ferramenta, conseguimos decidir com clareza: <strong>“este erro é capturado aqui; aquele segue adiante até lá”</strong>. O acúmulo dessas decisões é o que torna a experiência do usuário mais estável. Evitar uma tela em branco, impedir que a mesma notificação apareça cinco vezes, não deixar uma falha temporária de rede derrubar a página inteira e mostrar a tela de login em vez de “Tentar novamente” após um erro 401: são esses detalhes que constroem a impressão de um serviço bem-feito.</p>
<p>É claro que nem todo projeto precisa de todos esses padrões. Em uma ferramenta administrativa simples, uma ErrorBoundary e algumas notificações podem bastar. Em um domínio como pagamentos, onde um único erro custa dinheiro, cada mutação precisa de um tratamento minucioso. É o domínio que determina a resposta.</p>
<p>Espero que este texto também motive você a examinar seu projeto e perguntar: “quais erros o nosso serviço captura hoje, onde e em componentes com quais nomes?”. Talvez haja uma quantidade surpreendente de erros que pareciam estar bem capturados, mas na verdade estão escapando ou chegando à interface de contingência errada. Comigo, isso aconteceu repetidas vezes.</p>
<h2 id="referências"><a class="anchor" href="#referências">Referências</a></h2>
<details class="ref-block"><summary class="ref-summary">참고 자료</summary><div class="ref-list">
<div class="ref-item">
<div class="ref-item-inner">[documentação] <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">[documentação] <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">[documentação] <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">[documentação] <a href="https://tanstack.com/query/v5/docs/framework/react/guides/important-defaults" target="_blank" rel="noopener noreferrer">TanStack Query, padrões importantes</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#9ca3af"></span><a href="https://tkdodo.eu/blog/react-query-error-handling" target="_blank" rel="noopener noreferrer">TkDodo, tratamento de erros no React Query</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#9ca3af"></span><a href="https://tkdodo.eu/blog/breaking-react-querys-api-on-purpose" target="_blank" rel="noopener noreferrer">TkDodo, alterando a API do React Query de propósito</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner">[repositório] <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">[documentação] <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[Dominando o React Fiber por completo]]></title>
            <link>https://hooninedev.com/pt-BR/250520</link>
            <guid isPermaLink="false">https://hooninedev.com/pt-BR/250520</guid>
            <pubDate>Tue, 20 May 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Neste post, quero falar sobre a arquitetura Fiber, que pode ser considerada o coração do React. Quando conheci o React, eu via a palavra "Fiber" apenas como mais uma pergunta frequente em entrevistas....]]></description>
            <content:encoded><![CDATA[<p>Neste post, quero falar sobre a <strong>arquitetura Fiber</strong>, que pode ser considerada o coração do React.</p>
<p>Quando conheci o React, eu via a palavra <strong>"Fiber"</strong> apenas como mais uma pergunta frequente em entrevistas. Decorei uma definição de uma linha — "dividir a renderização em unidades de trabalho e processá-las" — e achei que isso era tudo. Mas, quando comecei a examinar de fato o código-fonte do React, percebi que Fiber não era apenas um conceito, mas a arquitetura de runtime que controla <strong>tudo</strong> na renderização do React.</p>
<blockquote>
<p>Ainda não consigo esquecer o impacto de abrir o código-fonte do React pela primeira vez. Pensei: "O que... é tudo isso?"</p>
</blockquote>
<p>Neste artigo, iremos além de responder à pergunta "O que é Fiber?" com "É a divisão do trabalho em unidades". Vamos investigar a fundo <strong>por que</strong> Fiber surgiu, <strong>como</strong> foi projetado e <strong>como</strong> essa estrutura viabiliza as Concurrent Features do React.</p>
<h2 id="por-que-fiber-surgiu"><a class="anchor" href="#por-que-fiber-surgiu">Por que Fiber surgiu?</a></h2>
<p>Para responder a essa pergunta, primeiro precisamos entender os problemas do mundo anterior ao Fiber: o <strong>Stack Reconciler</strong>, usado até o React 15.</p>
<p>Como o nome sugere, o Stack Reconciler era um mecanismo de reconciliação baseado em <strong>chamadas recursivas (recursive)</strong>. Ele percorria a árvore de componentes recursivamente, de cima para baixo, e, depois que a renderização começava, só podia parar após processar toda a árvore. Era como não poder desligar uma ligação até a outra pessoa terminar de falar. (Imagine que ela começa uma sessão de três horas contando a própria vida e você não pode interromper. Terrível.)</p>
<p>Mais especificamente, o Stack Reconciler tinha as seguintes limitações.</p>
<ul>
<li><strong>Impossibilidade de interromper a renderização</strong>: como toda a árvore precisava ser processada de uma vez, a main thread podia ficar ocupada por dezenas ou centenas de milissegundos em UIs complexas</li>
<li><strong>Ausência do conceito de prioridade</strong>: fosse um clique do usuário ou uma atualização de dados em background, todas as atualizações eram processadas da mesma maneira</li>
<li><strong>Dificuldade para lidar com animações e gestos</strong>: manter 60 fps exige concluir todo o trabalho em cerca de 16 ms por frame, algo que a renderização recursiva não conseguia garantir</li>
<li><strong>Um erro interrompia toda a aplicação</strong>: um erro em qualquer ponto da árvore de componentes podia parar a aplicação inteira</li>
</ul>
<p>Para superar essas limitações, a equipe do React concebeu um novo modelo de execução capaz de <strong>dividir</strong> o trabalho, <strong>atribuir prioridades</strong> e, quando necessário, <strong>interromper e retomar</strong> a execução. O resultado foi justamente o <strong>React Fiber</strong>.</p>
<p>O documento <a href="https://github.com/acdlite/react-fiber-architecture" target="_blank" rel="noopener noreferrer">react-fiber-architecture</a>, escrito por Andrew Clark, reúne as ideias centrais desse design e é a referência mais importante para entender Fiber. (Ao que tudo indica, ele entrou para a equipe do React pouco depois de escrever esse documento.)</p>
<h2 id="stack-vs-fiber"><a class="anchor" href="#stack-vs-fiber">Stack vs Fiber</a></h2>
<p>Então, em que o Stack Reconciler e o Fiber Reconciler diferem no nível do código?</p>
<h3 id="stack-reconciler-baseado-em-recursão"><a class="anchor" href="#stack-reconciler-baseado-em-recursão">Stack Reconciler baseado em recursão</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>Na abordagem Stack, ao encontrar um componente filho, entra-se <strong>imediatamente em uma chamada recursiva</strong>. O problema é que ela depende diretamente da call stack do JavaScript. Conforme as chamadas se aprofundam, frames se acumulam na call stack e, até que todos sejam resolvidos, a main thread do navegador não pode fazer mais nada.</p>
<p>Em termos simples, até a call stack esvaziar, o navegador fica <strong>completamente imobilizado</strong>.</p>
<video width="640" height="480" controls>
  <source src="/content/250520/stack.mov" type="video/mp4">
</video>
<p>No vídeo acima, é possível ver a main thread totalmente bloqueada enquanto o Stack Reconciler renderiza.</p>
<h3 id="fiber-reconciler-baseado-em-iteração"><a class="anchor" href="#fiber-reconciler-baseado-em-iteração">Fiber Reconciler baseado em iteração</a></h3>
<p>Fiber substituiu a recursão por um <strong>loop iterativo (iterative loop)</strong>. Em vez da call stack, implementou sua <strong>própria stack virtual</strong> na memória. Cada nó Fiber funciona como um "stack frame" e, como esses nós são objetos JavaScript armazenados na heap, o trabalho pode ser interrompido a qualquer momento e retomado depois.</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>O código acima mostra o modelo conceitual inicial do Fiber. O ponto central é processar apenas uma unidade de trabalho (unit of work) por vez dentro do loop <code>while</code> e, quando o tempo fica curto, sair do loop e devolver o controle ao navegador.</p>
<p>(No início, a abordagem usava <code>requestIdleCallback</code>, mas o React real não usa essa API. Veremos o motivo em detalhes mais adiante.)</p>
<video width="640" height="480" controls>
  <source src="/content/250520/fiber.mov" type="video/mp4">
</video>
<p>Com Fiber, é possível responder imediatamente a eventos do usuário — cliques, digitação etc. — mesmo durante a renderização. Ao executar o trabalho em pequenas partes, o navegador ganha espaço para respirar.</p>
<p>Se quiser experimentar pessoalmente a diferença entre as duas abordagens, clique <strong><a href="https://animated-lollipop-2b6cbb.netlify.app/" target="_blank" rel="noopener noreferrer">aqui</a></strong>. Você poderá observar visualmente como o Stack Reconciler e o Fiber Reconciler se comportam de formas diferentes.</p>
<p>Este é exatamente o objetivo central do Fiber enfatizado por Andrew Clark em seu documento.</p>
<ul>
<li><strong>Interromper o trabalho e voltar a ele depois</strong></li>
<li><strong>Atribuir prioridades a diferentes tipos de trabalho</strong></li>
<li><strong>Reutilizar trabalho concluído anteriormente</strong></li>
<li><strong>Cancelar trabalho que não é mais necessário</strong></li>
</ul>
<h2 id="estrutura-interna-de-um-nó-fiber"><a class="anchor" href="#estrutura-interna-de-um-nó-fiber">Estrutura interna de um nó Fiber</a></h2>
<p>Ao chegar até aqui, surge naturalmente uma pergunta: "Então, qual é a estrutura interna de um nó Fiber?"</p>
<p>A equipe do React não fornece uma documentação oficial separada sobre a implementação interna do Fiber. Ainda assim, podemos entender sua estrutura por meio do documento react-fiber-architecture, de Andrew Clark, e do código-fonte real do React (<code>ReactFiber.js</code>).</p>
<p>Gosto de comparar um nó Fiber a uma <strong>ordem de trabalho (Work Order)</strong>. Quando um produto é montado em uma fábrica, cada ordem registra "que tipo de peça é esta", "quais materiais serão usados", "qual trabalho deve ser feito em seguida" e "qual é a prioridade". Um nó Fiber funciona da mesma forma.</p>
<h3 id="reactelement-e-fibernode"><a class="anchor" href="#reactelement-e-fibernode">ReactElement e FiberNode</a></h3>
<p>Para entender Fiber, primeiro é preciso distinguir <strong>ReactElement</strong> de <strong>FiberNode</strong>. Os dois são confundidos com frequência, mas são entidades completamente diferentes.</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>ReactElement é apenas o <strong>blueprint</strong> da UI. É somente uma solicitação para "renderizar este componente com estas props"; ele não contém lógica real de renderização nem estado.</p>
<p>Já <strong>FiberNode</strong> é a <strong>unidade de trabalho em runtime</strong> criada internamente pelo React com base nesse blueprint. É nele que existem campos ausentes no ReactElement, como <code>tag</code>, <code>stateNode</code>, <code>child/sibling/return</code>, <code>memoizedState</code>, <code>updateQueue</code> e <code>lanes</code>.</p>
<p>Quando o React cria um FiberNode a partir do <code>type</code> de um ReactElement, o valor de <strong>tag</strong> é definido.</p>
<ul>
<li>Se <code>type</code> for uma função e tiver <code>prototype.isReactComponent</code> → <code>tag = ClassComponent(1)</code></li>
<li>Se <code>type</code> for uma função → <code>tag = FunctionComponent(0)</code></li>
<li>Se <code>type</code> for uma string (como <code>"div"</code>) → <code>tag = HostComponent(5)</code></li>
</ul>
<p><strong>tag</strong> é uma constante numérica que representa o tipo do FiberNode. Ela é definida em <code>ReactWorkTags.js</code>, e existem mais de 25 tags, incluindo <code>FunctionComponent(0)</code>, <code>ClassComponent(1)</code>, <code>HostRoot(3)</code>, <code>HostComponent(5)</code> e <code>HostText(6)</code>. Com base nesse valor de tag, o React decide qual lógica executar em <code>beginWork</code>.</p>
<p><strong>type</strong> exerce um papel central no processo de reconciliação (reconciliation). Ao comparar o Fiber da renderização anterior com o novo elemento, <strong>a primeira coisa que o React verifica</strong> é justamente o type. (Esse valor é transferido do ReactElement para o FiberNode sem alterações.)</p>
<ul>
<li>Se antes era <code>div</code> e continua sendo <code>div</code>, o React <strong>reutiliza</strong> esse nó Fiber e atualiza apenas as props</li>
<li>Se antes era <code>div</code>, mas agora mudou para <code>span</code>, o React <strong>descarta</strong> o Fiber anterior e cria um novo</li>
</ul>
<p><strong>key</strong> também é transferida do ReactElement para o FiberNode e é usada principalmente na renderização de listas (arrays). Sem key, quando a ordem dos itens muda, o React não consegue saber com precisão qual item foi movido para onde. Isso pode causar operações desnecessárias no DOM ou fazer com que o estado interno de componentes seja mantido ou perdido de forma inesperada.</p>
<h3 id="child-sibling-return"><a class="anchor" href="#child-sibling-return">child, sibling, return</a></h3>
<p>É aqui que está o segredo que permite ao React Fiber usar iteração em vez de recursão.</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> aponta para o <strong>primeiro</strong> elemento filho retornado pelo render do componente. No exemplo acima, é <code>&#x3C;자식1/></code>. <strong>sibling</strong> representa o <strong>próximo irmão</strong> que tem o mesmo pai. O sibling de <code>&#x3C;자식1/></code> é <code>&#x3C;자식2/></code>. <strong>return</strong> aponta para o Fiber <strong>pai ao qual voltar</strong> depois que o processamento do nó atual terminar. O return tanto de <code>&#x3C;자식1/></code> quanto de <code>&#x3C;자식2/></code> é <code>부모</code>.</p>
<p>A estrutura formada por esses três campos é uma <strong>árvore em formato de lista simplesmente encadeada (Singly Linked List)</strong>. Em uma árvore convencional, seria intuitivo manter um array de filhos (<code>children[]</code>), mas Fiber evita isso deliberadamente.</p>
<p>Por quê? Em uma estrutura de filhos baseada em array, é preciso gerenciar índices durante a travessia e rastrear separadamente "até onde o processamento chegou" quando o trabalho é interrompido e retomado. Já em uma estrutura linked list, basta guardar a referência ao nó atual para continuar a travessia a qualquer momento. Essa é a base estrutural que permite ao Fiber oferecer <strong>interrupção e retomada</strong> de maneira natural.</p>
<p>Com base nessa estrutura, o React percorre os nós em ordem de busca em profundidade (DFS). Ele desce seguindo <code>child</code> (beginWork); ao chegar a um nó folha, verifica <code>sibling</code>; e, quando não há irmão, sobe seguindo <code>return</code> (completeWork).</p>
<h3 id="pendingprops-e-memoizedprops"><a class="anchor" href="#pendingprops-e-memoizedprops">pendingProps e memoizedProps</a></h3>
<p><strong>pendingProps</strong> são as <strong>novas props</strong> recebidas no momento em que o processamento daquele Fiber começa, enquanto <strong>memoizedProps</strong> são as <strong>props anteriores</strong> cujo processamento terminou na renderização anterior.</p>
<p>Se os dois valores forem iguais, o React pode concluir que "não houve mudança neste componente" e reutilizar o resultado da renderização anterior. Esse é o mecanismo central da <strong>otimização por bailout</strong>.</p>
<p>Da mesma forma, <strong>memoizedState</strong> armazena o estado dos hooks daquele Fiber, e <strong>updateQueue</strong> gerencia as atualizações de estado ainda não processadas (chamadas de setState) em uma linked list.</p>
<h3 id="statenode"><a class="anchor" href="#statenode">stateNode</a></h3>
<p><strong>stateNode</strong> referencia a <strong>instância real</strong> apontada pelo nó Fiber.</p>
<ul>
<li>Para <strong>HostComponent</strong> (div, span etc.): o nó DOM real</li>
<li>Para <strong>ClassComponent</strong>: a instância da classe</li>
<li>Para <strong>HostRoot</strong>: o objeto FiberRoot</li>
</ul>
<p>Esse campo funciona como uma ponte entre o mundo virtual do Fiber e o DOM real do navegador.</p>
<h2 id="double-buffering-árvore-current-e-árvore-workinprogress"><a class="anchor" href="#double-buffering-árvore-current-e-árvore-workinprogress">Double buffering: árvore current e árvore workInProgress</a></h2>
<p>Um conceito central que não pode faltar ao entender Fiber é o <strong>double buffering (Double Buffering)</strong>.</p>
<p>Para compreendê-lo, pense em gráficos de jogos. Se os pixels fossem desenhados diretamente na tela atual, o usuário veria um frame pela metade, fenômeno chamado de <strong>screen tearing (tearing)</strong>. Para evitar isso, engines de jogos usam <strong>dois buffers</strong>. O próximo frame é desenhado por completo em um deles e, quando fica pronto, o buffer exibido na tela é trocado de uma só vez.</p>
<p>O React Fiber usa exatamente a mesma estratégia.</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>A <strong>árvore current</strong> é a árvore Fiber que está refletida na tela naquele momento e representa o estado da UI que o usuário vê. A <strong>árvore workInProgress</strong> é a árvore Fiber preparada em background para a próxima renderização.</p>
<p>As duas árvores referenciam uma à outra pela propriedade <code>alternate</code>. Todas as mudanças são feitas na árvore workInProgress e, quando o trabalho termina, as árvores são trocadas com uma única linha: <code>root.current = finishedWork</code>. A antiga workInProgress se torna a nova current, e a antiga current é reciclada como workInProgress na renderização seguinte.</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>Vale destacar o ponto principal. O <code>stateNode</code> (o nó DOM real) é <strong>compartilhado</strong> entre current e workInProgress. Em vez de criar um novo objeto Fiber a cada renderização, o React reutiliza o alternate existente e atualiza somente os campos alterados. Graças a isso, consegue construir a árvore de forma eficiente sem impor a cada renderização o custo do garbage collector (GC).</p>
<p>E se props e state não tiverem mudado? Torna-se possível pular a subárvore inteira com a <strong>otimização por bailout</strong>. Se o double buffering dos jogos otimiza no nível do frame, o double buffering do Fiber permite otimizar até o <strong>nível do componente</strong>.</p>
<h2 id="pendingworkpriority--lanes"><a class="anchor" href="#pendingworkpriority--lanes">pendingWorkPriority => Lanes</a></h2>
<p>Então, como Fiber determina que "este trabalho é mais importante"?</p>
<h3 id="limitações-de-expirationtime"><a class="anchor" href="#limitações-de-expirationtime">Limitações de expirationTime</a></h3>
<p>No início, Fiber usava uma prioridade numérica chamada <code>pendingWorkPriority</code>, que depois evoluiu para um único número chamado <code>expirationTime</code>. Quanto mais próximo o vencimento, maior a prioridade. Mas essa abordagem tinha uma limitação fundamental.</p>
<p>Com um único número, era <strong>impossível fazer classificações flexíveis</strong> como "esta atualização pertence ao grupo A e aquela pertence ao grupo B". Quando uma entrada do usuário e uma atualização de Transition ocorriam ao mesmo tempo, por exemplo, o modelo baseado em expirationTime só conseguia classificá-las por comparação de intervalo (range), o que limitava o processamento seletivo de atualizações específicas.</p>
<h3 id="lane"><a class="anchor" href="#lane">Lane</a></h3>
<p>Para resolver esse problema, Andrew Clark introduziu o <strong>sistema de Lanes</strong> no <a href="https://github.com/facebook/react/pull/18796" target="_blank" rel="noopener noreferrer">PR #18796</a>.</p>
<p>Para entender Lane, pense em uma <strong>rodovia</strong>. Uma rodovia tem várias faixas (lanes), e cada uma serve a uma finalidade diferente. A faixa da esquerda é para ultrapassagens (urgente), as centrais para o tráfego normal e o acostamento para emergências. Cada veículo (atualização) é colocado na faixa adequada à sua natureza, e o sistema de gestão da rodovia (scheduler) decide quais veículos devem passar primeiro.</p>
<p>As Lanes do React funcionam da mesma maneira. Cada atualização recebe <strong>um bit (lane)</strong>, e operações bitwise são usadas para formar e comparar grupos.</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>Ao todo, 31 lanes foram projetadas para caber em um inteiro de 31 bits, permitindo aproveitar a otimização <strong>SMI (Small Integer)</strong> do engine V8. Inteiros de até 31 bits são tratados pelo V8 com pointer tagging e podem ser operados diretamente na stack, sem alocação na heap. Entre as principais lanes, <strong>quanto mais baixo o bit, maior a prioridade</strong>.</p>
<p>Graças a essa estrutura, o React consegue decidir qual trabalho processar primeiro com uma única operação bitwise. A função <code>getNextLanes()</code> seleciona em <code>pendingLanes</code> o grupo de lanes de maior prioridade, ignora lanes interrompidas (suspended) e prioriza novas tentativas de lanes que receberam dados (pinged), possibilitando um scheduling sofisticado.</p>
<p>Além disso, para evitar <strong>starvation</strong>, cada lane recebe um tempo de expiração. Sync/InputContinuous é adicionada a <code>expiredLanes</code> após 250 ms, e Transition após 5.000 ms, forçando o processamento síncrono. Isso significa que nenhum trabalho será ignorado para sempre, por menor que seja sua prioridade. (Se baixa prioridade significasse ser ignorado para sempre, isso não seria um sistema de prioridades, mas um sistema de discriminação.)</p>
<h2 id="o-output-do-fiber"><a class="anchor" href="#o-output-do-fiber">O output do Fiber</a></h2>
<p>Depois de examinar a estrutura do Fiber até aqui, surge outra pergunta: como esses nós Fiber se transformam no <strong>DOM real</strong>?</p>
<p>O output representa informações concretas de nós DOM que podem ser aplicadas ao DOM real. Há uma distinção importante aqui.</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>Somente <strong>host components</strong> (div, span, img etc.) criam nós DOM reais. O navegador não sabe o que é <code>&#x3C;아바타/></code>. Como componentes definidos pelo usuário são conceitos abstratos, precisam ser decompostos em host components para que o navegador consiga entendê-los.</p>
<p>Vamos analisar esse processo mais concretamente.</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>A relação entre a árvore Fiber produzida por esses componentes e o output é a seguinte.</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>A coleta do output ocorre <strong>de baixo para cima</strong>. Primeiro, o DOM é criado nos nós folha (host).</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>Em seguida, o host component pai coleta o output dos filhos.</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>Por fim, componentes definidos pelo usuário simplesmente encaminham o output dos filhos.</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="scheduling-do-fiber"><a class="anchor" href="#scheduling-do-fiber">Scheduling do Fiber</a></h2>
<p>Se o valor central do Fiber está em "dividir o trabalho", onde essa "divisão" realmente acontece? No <strong>Work Loop</strong>.</p>
<h3 id="work-loop-o-coração-da-travessia-do-fiber"><a class="anchor" href="#work-loop-o-coração-da-travessia-do-fiber">Work Loop: o coração da travessia do Fiber</a></h3>
<p>A renderização do React começa no Work Loop, definido em <code>ReactFiberWorkLoop.js</code>. Conforme a situação, o React usa dois Work Loops.</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>Observe a diferença entre as duas funções. <code>workLoopSync</code> roda <strong>incondicionalmente</strong> até <code>workInProgress</code> se tornar <code>null</code>. Já <code>workLoopConcurrent</code> estabelece um <strong>limite de tempo</strong> e sai do loop quando esse limite é excedido.</p>
<p>Um detalhe interessante é a diferença no intervalo de yield. Trabalho <strong>non-idle, perceptível pelo usuário</strong>, como Transition ou Retry, cede a execução em intervalos de <strong>25 ms</strong>, enquanto <strong>trabalho idle, de baixa prioridade, que pode ser processado quando o usuário não está fazendo nada</strong>, cede a cada <strong>5 ms</strong>. O motivo para conceder 25 ms ao trabalho non-idle é limitar intencionalmente as animações a cerca de 30 fps, evitando que a renderização da transition provoque starvation em outros trabalhos.</p>
<h3 id="performunitofwork"><a class="anchor" href="#performunitofwork">performUnitOfWork</a></h3>
<p><code>performUnitOfWork</code> é a função que processa um único nó Fiber. Ela contém o núcleo da travessia do Fiber.</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> processa o nó atual e retorna o primeiro filho. Em seguida, confirma <code>pendingProps</code> como <code>memoizedProps</code>: se houver um filho, avança para ele; caso contrário, chama <code>completeUnitOfWork</code>.</p>
<h3 id="beginwork"><a class="anchor" href="#beginwork">beginWork</a></h3>
<p><code>beginWork</code> percorre os nós Fiber de cima para baixo e realiza em cada um os cálculos necessários. Definida em <code>ReactFiberBeginWork.js</code>, a função se ramifica internamente por um enorme <strong>switch</strong> baseado na <code>tag</code> do Fiber.</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>O ponto central é a <strong>verificação de bailout</strong> no início. Se props e context forem iguais aos anteriores, <code>bailoutOnAlreadyFinishedWork</code> pula a subárvore inteira. Esse é um dos caminhos mais importantes para a otimização de performance do React.</p>
<p>O valor retornado por <code>beginWork</code> é o <strong>primeiro Fiber filho</strong>. Se existir um filho, ele se torna o próximo <code>workInProgress</code>; se não existir (<code>null</code>), a execução entra em <code>completeUnitOfWork</code>.</p>
<h3 id="completework"><a class="anchor" href="#completework">completeWork</a></h3>
<p><code>completeWork</code> começa em um nó folha e finaliza o trabalho subindo em direção ao pai.</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>Os principais trabalhos executados em <code>completeWork</code> são os seguintes.</p>
<ul>
<li><strong>Para HostComponent</strong>: cria o nó DOM real (<code>createInstance</code>) e faz append dos DOMs filhos. Se o DOM já existir, coleta as props alteradas e as armazena em <code>updateQueue</code>.</li>
<li><strong><code>bubbleProperties()</code></strong>: agrega as flags dos filhos em <code>subtreeFlags</code>. Essas informações são usadas na otimização que pula subárvores durante a Commit Phase.</li>
</ul>
<p>Em resumo, a travessia funciona assim: <strong>desce seguindo child (beginWork) -> ao concluir uma folha, avança para sibling -> se não houver irmão, sobe seguindo return (completeWork)</strong>. Essa é a ordem de busca em profundidade do Fiber.</p>
<h3 id="por-que-requestidlecallback-foi-abandonado"><a class="anchor" href="#por-que-requestidlecallback-foi-abandonado">Por que requestIdleCallback foi abandonado</a></h3>
<p>Anteriormente, mostramos um código que usa <code>requestIdleCallback</code> no modelo conceitual do Fiber, mas o React real não utiliza essa API. Os motivos são claros.</p>
<ul>
<li><strong>Frequência de chamada muito baixa</strong>: ela só é chamada em verdadeiro "tempo ocioso", quando o navegador não tem nada a fazer; por isso, em uma página movimentada, o trabalho do React poderia ser adiado indefinidamente. Dan Abramov também afirmou que "requestIdleCallback is called too infrequently to be useful for scheduling React work".</li>
<li><strong>Problemas de compatibilidade entre navegadores</strong>: durante muito tempo, o Safari não a implementou, e o comportamento variava entre navegadores.</li>
<li><strong>Limite superior de 20 ms</strong>: como o idle deadline tinha um teto, não era possível controlar o timing de maneira tão previsível quanto o React precisava.</li>
</ul>
<p>Depois disso, a equipe tentou usar <code>requestAnimationFrame</code> com uma estimativa do orçamento do frame, mas essa abordagem também foi abandonada ao concluir que o trabalho do React não precisava acompanhar o ciclo de vsync (tecnologia que sincroniza a exibição de frames com o momento em que o monitor conclui a varredura vertical).</p>
<h3 id="messagechannel"><a class="anchor" href="#messagechannel">MessageChannel</a></h3>
<p>Por fim, o React escolheu <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>Por que não <code>setTimeout</code>, mas sim <code>MessageChannel</code>? Segundo a especificação HTML, quando <code>setTimeout</code> é aninhado cinco vezes ou mais, um <strong>atraso mínimo de 4 ms</strong> é imposto. Já <code>MessageChannel</code> roda imediatamente como uma macrotask no próximo tick do event loop, sem essa restrição. Para Fiber, que divide o trabalho em unidades de 5 ms, um atraso artificial de 4 ms seria fatal.</p>
<p>(Se 4 dos 5 ms são tempo de espera, sobra apenas 1 ms de trabalho real. Isso não é work-life balance; é só life.)</p>
<p>Internamente, o pacote Scheduler do React mantém <strong>dois min-heaps</strong>.</p>
<pre><code>timerQueue (대기실)                    taskQueue (실행 대기열)
┌──────────────────┐                  ┌──────────────────┐
│ 아직 시작 시간이     │   startTime      │ 지금 실행 가능한     │
│ 안 된 태스크들       │ ──경과 시──→      │ 태스크들           │
│                  │                  │                  │
│ 정렬: startTime   │                  │ 정렬: expiration  │
│ (빠른 순)          │                  │ Time (임박한 순)   │
└──────────────────┘                  └──────────────────┘
</code></pre>
<p><strong>taskQueue</strong> é a fila de tarefas que "podem ser executadas agora". Quanto menor o <code>expirationTime</code> (= startTime + timeout), ou seja, quanto mais próximo o vencimento, mais cedo a tarefa é executada. <strong>timerQueue</strong> é a sala de espera das tarefas "cujo momento de execução ainda não chegou". No instante em que o horário atual ultrapassa startTime, elas passam para taskQueue.</p>
<p>Como, então, é definido o timeout que determina expirationTime? Cada atualização recebe um timeout próprio conforme seu nível de prioridade (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> expira assim que é criada e, portanto, é executada com prioridade máxima assim que entra em taskQueue. (Expirar logo ao nascer é um destino um tanto melancólico.) Os 250 ms de <strong>UserBlocking</strong> correspondem ao limite em que uma pessoa começa a sentir que "a resposta está lenta" (100–300 ms). Se nada acontecer até 0,25 segundo depois de um clique, o usuário se incomoda. Os 5 segundos de <strong>Normal</strong> parecem generosos, mas representam a garantia de que o trabalho será processado mesmo no pior caso. Na prática, ele é executado assim que o trabalho anterior termina. Os cerca de 12,4 dias de <strong>Idle</strong> são praticamente infinitos: ele só roda depois que todo o restante termina. (Como é muito improvável deixar o navegador aberto por 12 dias, podemos tratar isso como infinito.)</p>
<p>Esses valores de timeout também são um mecanismo de prevenção de <strong>starvation</strong>. Por menor que seja a prioridade, depois do timeout o trabalho expira e é executado à força. Assim, mesmo que trabalhos de alta prioridade continuem chegando, um trabalho de baixa prioridade nunca será ignorado para sempre.</p>
<p>O <code>shouldYieldToHost()</code> do Scheduler verifica se o tempo decorrido desde o início do trabalho excedeu <code>frameInterval</code> (por padrão, <strong>5 ms</strong>, definido em <code>SchedulerFeatureFlags.js</code>) e decide se deve devolver o controle à main thread.</p>
<h2 id="render-phase-e-commit-phase"><a class="anchor" href="#render-phase-e-commit-phase">Render Phase e Commit Phase</a></h2>
<p>Até aqui, examinamos a estrutura e o scheduling do Fiber. Agora vamos organizar o fluxo completo para entender como tudo isso se combina e produz uma atualização real da UI.</p>
<p>Internamente, Fiber passa por duas etapas: <strong>Render Phase</strong> e <strong>Commit Phase</strong>. Essa separação é o design central que viabiliza o modelo de concorrência do React. Se quiser conferir diretamente o fluxo de funcionamento do Fiber, clique na imagem abaixo.</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>A Render Phase é a etapa que <strong>calcula quais mudanças a UI requer</strong>. Nela, o DOM real não é afetado de forma alguma. Sua característica mais importante é que ela <strong>pode ser interrompida e retomada de forma assíncrona</strong>.</p>
<p>Essa etapa opera principalmente por meio de <code>beginWork</code> e <code>completeWork</code>, que vimos anteriormente.</p>
<p>Em <strong>beginWork(fiber)</strong>, a lógica apropriada é executada conforme o tipo de cada Fiber (FunctionComponent, ClassComponent, HostComponent etc.). Os nós Fiber filhos são então criados e conectados. Se as props forem iguais às anteriores, a memoization permite pular o trabalho (bailout).</p>
<p>Em <strong>completeWork(fiber)</strong>, são preparados o trabalho de criação do DOM e as informações de effects. Em seguida, <code>bubbleProperties()</code> agrega as flags dos filhos em <code>subtreeFlags</code>, e as informações são completadas enquanto a travessia sobe em direção ao pai.</p>
<p>Como o DOM não é modificado diretamente nessa etapa, o trabalho pode ser interrompido e retomado depois sem expor ao usuário uma UI incompleta. Essa é a base do Concurrent Mode.</p>
<h3 id="subtreeflags"><a class="anchor" href="#subtreeflags">subtreeFlags</a></h3>
<p>Durante a Render Phase, os side effects necessários são registrados em cada Fiber como <strong>bit flags</strong>. Vejamos as principais flags definidas em <code>ReactFiberFlags.js</code>.</p>
<ul>
<li><code>Placement</code>: inserir um novo nó no DOM</li>
<li><code>Update</code>: atualizar propriedades do DOM</li>
<li><code>ChildDeletion</code>: remover um nó filho</li>
<li><code>Ref</code>: conectar ou desconectar uma ref</li>
<li><code>Passive</code>: executar o callback de useEffect</li>
<li><code>Snapshot</code>: executar getSnapshotBeforeUpdate</li>
<li><code>Callback</code>: executar um callback de lifecycle</li>
</ul>
<p>Versões anteriores do React (até a 16) usavam uma linked list conectada por <code>firstEffect</code> -> <code>nextEffect</code> -> <code>lastEffect</code> para reunir apenas os Fibers com side effects. Porém, essa abordagem mantinha referências a Fibers desmontados, causando <strong>memory leaks</strong>, e tinha dificuldade para processar com eficiência novos padrões como Suspense.</p>
<p>A partir do React 17, essa effect list foi removida e substituída pela abordagem de <strong>subtreeFlags</strong> (<a href="https://github.com/facebook/react/pull/19381" target="_blank" rel="noopener noreferrer">PR #19381</a>). Durante <code>completeWork</code>, <code>bubbleProperties()</code> agrega as flags dos filhos no pai.</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>A maior vantagem dessa estrutura é poder <strong>pular uma subárvore inteira</strong> durante a Commit Phase. Se <code>subtreeFlags &#x26; MutationMask === NoFlags</code> para determinado Fiber, nenhum nó daquela subárvore precisa de uma mudança no DOM, então ela pode ser ignorada por completo. Essa otimização era impossível com a antiga linked list.</p>
<h3 id="commit-phase"><a class="anchor" href="#commit-phase">Commit Phase</a></h3>
<p>A Commit Phase é a etapa que <strong>aplica ao DOM real</strong> as mudanças calculadas na Render Phase. Ela é executada <strong>sempre de forma síncrona</strong> e, depois de começar, segue até o fim sem interrupções. Isso impede que o usuário veja uma UI atualizada apenas pela metade.</p>
<p>Internamente, a Commit Phase segue esta ordem detalhada.</p>
<ol>
<li><strong>Before Mutation Phase</strong>: <code>commitBeforeMutationEffects()</code>
<ul>
<li>Lê o estado atual do DOM antes que ele seja alterado. O lifecycle <code>getSnapshotBeforeUpdate</code> é executado aqui. Como, nesse momento, a árvore <code>current</code> ainda representa o estado exibido na tela, informações como posição do scroll e dimensões do DOM podem ser capturadas com segurança.</li>
</ul>
</li>
<li><strong>Mutation Phase</strong>: <code>commitMutationEffects()</code>
<ul>
<li>É a etapa em que ocorre a <strong>manipulação real do DOM</strong>. Novos nós são inseridos, os existentes são modificados e os desnecessários são removidos. <code>componentWillUnmount</code> também é executado nesse momento, pois <code>current</code> ainda aponta para a árvore anterior, permitindo ler o estado antigo.</li>
</ul>
</li>
<li><strong>Troca da árvore</strong>: <code>root.current = finishedWork</code>
<ul>
<li>Este é o ponto central do double buffering: a árvore workInProgress é promovida a árvore current. É importante que essa troca aconteça depois de Mutation e antes de Layout. <code>componentWillUnmount</code> precisa ler a <strong>árvore anterior</strong>, por isso é executado na Mutation Phase; já <code>componentDidMount</code>/<code>componentDidUpdate</code> precisam ler a <strong>nova árvore</strong>, por isso são executados na Layout Phase.</li>
</ul>
</li>
<li><strong>Layout Phase</strong>: <code>commitLayoutEffects()</code>
<ul>
<li>Depois que as mudanças no DOM terminam, são executados os trabalhos baseados no novo estado do DOM.
<ul>
<li>execução de <code>componentDidMount</code> e <code>componentDidUpdate</code></li>
<li>execução dos callbacks de <code>useLayoutEffect</code></li>
<li>nesse momento, <code>current</code> já aponta para a nova árvore, portanto a leitura do DOM retorna os valores atualizados</li>
</ul>
</li>
</ul>
</li>
<li><strong>Passive Effects</strong> (assíncronos)
<ul>
<li>O cleanup e o setup de <code>useEffect</code> são agendados separadamente e executados <strong>de forma assíncrona</strong>. Como eles tratam side effects que não dependem de mudanças no DOM, como data fetching e event subscriptions, não precisam ser executados de maneira síncrona. Ao processá-los assincronamente, o React cede ao navegador a oportunidade de desenhar a tela primeiro.</li>
</ul>
</li>
</ol>
<h2 id="concurrent-features-e-fiber"><a class="anchor" href="#concurrent-features-e-fiber">Concurrent Features e Fiber</a></h2>
<p>Agora vamos ver, por meio das Concurrent Features disponíveis desde o React 18, que tipo de experiência para o usuário todos os designs do Fiber examinados até aqui — double buffering, prioridades baseadas em Lane e Work Loop interrompível — tornam possível.</p>
<h3 id="usetransition"><a class="anchor" href="#usetransition">useTransition</a></h3>
<p>Ao chamar <code>startTransition(() => setState(...))</code>, a atualização recebe uma <code>TransitionLane</code>. As 14 TransitionLanes são atribuídas em round-robin, isto é, uma de cada vez em sequência, para evitar conflitos.</p>
<p>Como TransitionLane tem prioridade menor do que SyncLane ou DefaultLane, quando chega uma atualização urgente, como uma entrada do usuário, o React pode <strong>interromper</strong> a renderização da transition e processar primeiro a atualização urgente. Enquanto isso, a árvore <code>current</code> — o estado anterior — continua na tela, e a transition avança em background na árvore workInProgress.</p>
<p>É aqui que o valor do double buffering se destaca. Uma renderização de transition interrompida afeta apenas a árvore workInProgress; a tela vista pelo usuário, a árvore current, permanece completamente intacta.</p>
<p>A flag <code>isPending</code> indica que a transition ainda não terminou, permitindo, por exemplo, exibir um indicador de loading.</p>
<h3 id="usedeferredvalue"><a class="anchor" href="#usedeferredvalue">useDeferredValue</a></h3>
<p>Na primeira renderização, <code>useDeferredValue(value)</code> retorna diretamente o <code>value</code> recebido. Nas renderizações seguintes, se a renderização atual for urgente, retorna o valor memoized anterior e agenda uma nova renderização com TransitionLane. Assim como uma Transition, a renderização adiada pode ser interrompida.</p>
<p>Conceitualmente, é semelhante a <code>startTransition</code>, mas com uma diferença: ela é aplicada no <strong>lado que recebe o valor</strong>, e não no lado que dispara a atualização. Um caso de uso típico é refletir imediatamente o texto digitado em uma busca, mas adiar a renderização da lista de resultados.</p>
<h3 id="suspense"><a class="anchor" href="#suspense">Suspense</a></h3>
<p>Quando um componente lança uma Promise dentro de <code>&#x3C;Suspense></code>, <code>throwException</code> a captura e marca esse Fiber como <code>Incomplete</code>. Depois, sobe pela cadeia de <code>return</code> procurando o limite de Suspense mais próximo e faz esse limite passar a exibir a fallback UI. Quando a Promise é resolvida, <code>markRootPinged</code> marca essa lane como pinged, e o React renderiza novamente a subárvore suspended.</p>
<p>No Concurrent Mode, os nós <strong>irmãos (sibling)</strong> do componente suspended podem continuar sendo renderizados, portanto uma única requisição de dados não bloqueia a renderização de toda a árvore. Isso é possível porque a estrutura de linked list do Fiber permite avançar livremente para um sibling.</p>
<h3 id="streaming-ssr-e-selective-hydration"><a class="anchor" href="#streaming-ssr-e-selective-hydration">Streaming SSR e Selective Hydration</a></h3>
<p>O <code>renderToPipeableStream</code> do React 18 usa limites de Suspense.</p>
<ul>
<li><strong>Servidor</strong>: quando um limite de Suspense é suspenso, envia primeiro o HTML da fallback e, quando os dados ficam prontos, faz streaming do conteúdo real posteriormente por meio de uma tag <code>&#x3C;script></code></li>
<li><strong>Cliente (Selective Hydration)</strong>: cada limite de Suspense pode sofrer hydration <strong>independentemente</strong>. Se o usuário clicar em uma área que ainda não passou por hydration, <code>SelectiveHydrationLane</code> processa <strong>prioritariamente</strong> a hydration daquele limite e só então dispara o evento</li>
</ul>
<p>Tudo isso é possível porque cada limite de Suspense é um nó Fiber que pode ser agendado de maneira independente. No fim, o design central da arquitetura Fiber — "dividir o trabalho, atribuir prioridades e poder interromper/retomar" — é o fundamento de todos esses recursos.</p>
<h2 id="conclusão"><a class="anchor" href="#conclusão">Conclusão</a></h2>
<p>Se resumíssemos este artigo em uma frase, diríamos que <strong>React Fiber é uma arquitetura que substitui recursão por iteração e move a call stack para a heap, permitindo interromper e retomar a renderização</strong>.</p>
<p>Para isso, vários designs sofisticados foram combinados: uma estrutura de árvore baseada em linked list, double buffering, um sistema de prioridades baseado em Lane e um scheduler baseado em MessageChannel. E tudo isso converge para um objetivo: <strong>maximizar a responsividade da UI percebida pelo usuário</strong>.</p>
<p>Naturalmente, a implementação interna do Fiber continua mudando a cada versão do React, e o conteúdo deste artigo é apenas um snapshot de determinado momento. Ainda assim, acredito que a filosofia central do Fiber — "dividir o trabalho, atribuir prioridades, interromper e retomar" — continuará a mesma.</p>
<p>Espero que este artigo tenha mostrado que React Fiber não é apenas uma palavra-chave de entrevistas, mas a arquitetura de runtime que sustenta todos os recursos do React. Não há uma resposta única, mas espero também que você examine o código-fonte diretamente e construa sua própria compreensão.</p>
<h2 id="fontes"><a class="anchor" href="#fontes">Fontes</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">Código-fonte do React, 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">Código-fonte do React, 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">Código-fonte do React, 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">Código-fonte do React, 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">Código-fonte do React, 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">Código-fonte do React, 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">Post do blog do React v18.0</a></div>
</div>
</div></details>]]></content:encoded>
            <author>jihoon7705@gmail.com (이지훈)</author>
            <category>프론트엔드</category>
            <category>React</category>
        </item>
        <item>
            <title><![CDATA[O Biome pode substituir o ESLint e o Prettier?]]></title>
            <link>https://hooninedev.com/pt-BR/241201</link>
            <guid isPermaLink="false">https://hooninedev.com/pt-BR/241201</guid>
            <pubDate>Sun, 01 Dec 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[Neste post, quero falar sobre uma ferramenta chamada Biome. A equipe em que trabalho enfrentava bastante dificuldade para manter um estilo de código consistente em um ambiente no qual as pessoas usava...]]></description>
            <content:encoded><![CDATA[<p>Neste post, quero falar sobre uma ferramenta chamada Biome.</p>
<p>A equipe em que trabalho enfrentava bastante dificuldade para manter um estilo de código consistente em um ambiente no qual as pessoas usavam IDEs diferentes, como WebStorm e VSCode. Também era trabalhoso gerenciar arquivos de configuração separados para cada IDE, e as revisões de código frequentemente recebiam comentários sobre diferenças de formatação sem relação com a lógica.</p>
<p>Nesse cenário, as regras de formatação do ESLint foram marcadas como Deprecated, e tivemos que procurar uma nova alternativa. A combinação <strong>Prettier + ESLint</strong> exigia configurações adicionais para evitar conflitos entre as ferramentas, enquanto o <strong>@stylistic/eslint-plugin-ts</strong> ainda estava em uma fase inicial de adoção pela comunidade e não tinha estabilidade suficientemente comprovada. Foi então que o Biome chamou nossa atenção.</p>
<p>Mas o que exatamente é o Biome e será que ele realmente pode substituir o ESLint e o Prettier?</p>
<hr>
<h2 id="o-que-é-o-biome"><a class="anchor" href="#o-que-é-o-biome">O que é o Biome?</a></h2>
<p>O Biome é uma toolchain all-in-one para projetos web. Ele oferece, em uma única ferramenta, formatação e linting integrados para código JavaScript, TypeScript, JSX, CSS, JSON, GraphQL e muito mais. Sua filosofia central é cumprir, com um único binário, os papéis que tradicionalmente ficavam divididos entre ESLint e Prettier.</p>
<p>O antecessor do Biome foi o <a href="https://github.com/rome/tools" target="_blank" rel="noopener noreferrer">Rome</a>. A <strong>Rome Tools Inc.</strong> começou com grandes ambições depois de captar US$ 4,5 milhões em investimento de venture capital em 2021, mas, em meados de 2023, demitiu todos os funcionários e arquivou o repositório. Depois disso, os principais contributors fizeram um fork do projeto e o relançaram como Biome em agosto de 2023. Deixando para trás a imagem do Rome de “prometer demais e entregar de menos”, o projeto vem conquistando confiança com releases práticas e constantes.</p>
<p>Sua característica mais marcante é ter sido escrito em Rust. Mais adiante, veremos em detalhes a diferença que isso faz no desempenho.</p>
<hr>
<h2 id="por-que-usar-o-biome"><a class="anchor" href="#por-que-usar-o-biome">Por que usar o Biome?</a></h2>
<p>Há três motivos principais para escolher o Biome.</p>
<p><strong>Uma única ferramenta cuida tanto da formatação quanto do linting.</strong> Com a combinação ESLint + Prettier, eram necessárias configurações adicionais, como <code>eslint-config-prettier</code>, para evitar conflitos de regras entre as duas ferramentas. O Biome elimina essa complexidade pela raiz.</p>
<p><strong>O desempenho é impressionante.</strong> Segundo os benchmarks oficiais, ele é cerca de 25 vezes mais rápido que o Prettier e aproximadamente 15 vezes mais rápido que o ESLint. Mais adiante, vamos comparar diretamente o que esses números representam na prática.</p>
<p><img src="/content/241201/1.png" alt="1.png" width="2250" height="986" loading="eager" fetchpriority="high" decoding="async"></p>
<p><strong>Ele é compatível com as ferramentas existentes.</strong> O Biome oferece cerca de 97% de compatibilidade de formatação com o Prettier e inclui nativamente as principais regras do ESLint. Regras de plugins usados com frequência, como <code>eslint-plugin-react-hooks</code> e <code>eslint-plugin-jsx-a11y</code>, também vêm integradas, o que torna a migração relativamente tranquila.</p>
<hr>
<h2 id="como-usar"><a class="anchor" href="#como-usar">Como usar?</a></h2>
<p>A configuração do Biome é bastante simples. A <a href="https://biomejs.dev/guides/getting-started/" target="_blank" rel="noopener noreferrer">documentação oficial</a> explica tudo de forma clara, então vale consultá-la.</p>
<p>Primeiro, instale o 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>Depois, gere o arquivo de configuração.</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>Isso cria um arquivo <code>biome.json</code>. Nele, você pode definir as regras de formatação e linting da equipe.</p>
<p>Também é preciso instalar uma extensão para a IDE. Se você usa VSCode, instale o <a href="https://marketplace.visualstudio.com/items?itemName=biomejs.biome" target="_blank" rel="noopener noreferrer">VSCode Biome</a>; se usa WebStorm, instale o plugin <a href="https://plugins.jetbrains.com/plugin/22761-biome" target="_blank" rel="noopener noreferrer">WebStorm Biome</a>.</p>
<p>Por fim, adicione a configuração abaixo ao <code>settings.json</code> do VSCode para aplicar automaticamente a formatação e o linting ao salvar.</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="vamos-comparar-diretamente"><a class="anchor" href="#vamos-comparar-diretamente">Vamos comparar diretamente</a></h2>
<p>Dizer apenas que é rápido não torna a diferença muito concreta, então comparei o Biome e o ESLint + Prettier no mesmo projeto. À esquerda está o Biome; à direita, o ESLint + Prettier.</p>
<h3 id="tempo-de-execução-local-de-um-projeto-vite"><a class="anchor" href="#tempo-de-execução-local-de-um-projeto-vite">Tempo de execução local de um projeto Vite</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>O Biome levou <strong>506ms</strong>, enquanto o ESLint + Prettier levou <strong>630ms</strong>, resultando em um tempo de execução cerca de 20% menor.</p>
<hr>
<h3 id="tempo-de-build-de-um-projeto-vite"><a class="anchor" href="#tempo-de-build-de-um-projeto-vite">Tempo de build de um projeto Vite</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>O Biome levou <strong>117.13s</strong>, enquanto o ESLint + Prettier levou <strong>131.48s</strong>, resultando em um tempo de build cerca de 10% menor.</p>
<hr>
<h3 id="tarefa-de-linting"><a class="anchor" href="#tarefa-de-linting">Tarefa de 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>A maior diferença apareceu na tarefa de linting. O Biome levou <strong>0.79s</strong> (CPU 0.470s), enquanto o ESLint levou <strong>16.32s</strong> (CPU 8.600s), ou seja, <strong>o Biome apresentou um desempenho cerca de 20 vezes mais rápido</strong>. O uso da CPU também foi muito mais eficiente.</p>
<p>A diferença já é perceptível no ambiente de desenvolvimento, mas se torna ainda mais drástica quando uma pipeline de CI/CD verifica centenas de arquivos. Como o Biome pode executar seu binário diretamente, sem instalação via npm, ele também reduz o tempo de cold start do CI.</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... (A essa altura, é mais difícil encontrar um motivo para não usar.)</p>
<hr>
<h2 id="por-que-ele-é-tão-rápido"><a class="anchor" href="#por-que-ele-é-tão-rápido">Por que ele é tão rápido?</a></h2>
<p>“É rápido porque foi feito em Rust” é uma afirmação correta, mas não explica tudo. Vamos examinar os fatores técnicos específicos por trás da vantagem de desempenho do Biome.</p>
<hr>
<h3 id="desempenho-de-baixo-nível-do-rust"><a class="anchor" href="#desempenho-de-baixo-nível-do-rust">Desempenho de baixo nível do Rust</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>O Biome é escrito em Rust, uma linguagem de programação de sistemas. O Rust é voltado para abstrações de custo zero (Zero-cost Abstraction), o que significa que abstrações de alto nível podem ter o mesmo desempenho de código de baixo nível otimizado manualmente. Além disso, ele gerencia a memória por meio de um sistema de ownership, sem garbage collector (GC), evitando o overhead de runtime causado pelo GC.</p>
<p>Já o ESLint e o Prettier são escritos em JavaScript e executados sobre o runtime do Node.js. Embora a compilação JIT (Just-In-Time) do motor V8 otimize o JavaScript, ela não consegue evitar completamente as limitações fundamentais de uma linguagem interpretada nem o custo da coleta de lixo.</p>
<hr>
<h3 id="arquitetura-de-parsing-único"><a class="anchor" href="#arquitetura-de-parsing-único">Arquitetura de parsing único</a></h3>
<p>O Biome faz o parsing do código apenas uma vez com um único parser para gerar uma AST (Abstract Syntax Tree, árvore sintática abstrata). Essa AST é reutilizada tanto na formatação quanto no linting.</p>
<p>O que acontece quando usamos a combinação ESLint + Prettier? O ESLint faz o parsing do código, cria uma AST e executa o linting; depois, o Prettier analisa o mesmo código novamente, cria outra AST e executa a formatação. O mesmo arquivo passa por parsing duas vezes. A arquitetura de parsing único do Biome elimina essa duplicação na origem.</p>
<hr>
<h3 id="processamento-paralelo-nativo"><a class="anchor" href="#processamento-paralelo-nativo">Processamento paralelo nativo</a></h3>
<p><img src="/content/241201/7.png" alt="7.png" width="800" height="514" loading="lazy" fetchpriority="low" decoding="async"></p>
<p>Aproveitando o modelo de concorrência do Rust, o Biome processa arquivos em paralelo em várias threads. Ele divide o trabalho em unidades pequenas e distribui a carga de forma eficiente entre as threads por meio de um scheduler de work-stealing. Como o sistema de ownership do Rust impede data races em tempo de compilação, o custo de sincronização durante o runtime também é minimizado.</p>
<p>O Node.js usa, por padrão, um modelo single-thread baseado em event loop. É possível obter processamento paralelo com Worker Threads, mas isso acrescenta overhead devido à criação de threads e ao message passing. O Biome usa diretamente threads nativas no nível do sistema operacional e, por isso, consegue aproveitar ao máximo os núcleos da CPU sem esse overhead.</p>
<hr>
<h3 id="processamento-de-ast-eficiente-em-memória"><a class="anchor" href="#processamento-de-ast-eficiente-em-memória">Processamento de AST eficiente em memória</a></h3>
<p><img src="/content/241201/4.svg" alt="4.svg" loading="lazy" fetchpriority="low" decoding="async"></p>
<p>O Biome usa uma CST (Concrete Syntax Tree, árvore sintática concreta). Segundo a documentação oficial de arquitetura do Biome, essa CST implementa o padrão Green/Red Tree com base em um fork interno da biblioteca rowan, preservando todas as informações do código original, incluindo comentários e espaços em branco. A alocação de memória no estilo arena da rowan coloca os nós em regiões contíguas de memória, melhorando a localidade de cache (Cache Locality) da CPU e minimizando alocações desnecessárias de objetos.</p>
<p>No processamento de AST baseado em objetos do JavaScript, cada nó existe como um objeto independente no heap, o que dispersa a memória e aumenta a pressão sobre o GC. A abordagem do Biome permite percorrer a árvore mais rapidamente usando menos memória.</p>
<hr>
<h2 id="então-vale-a-pena-adotar-o-biome"><a class="anchor" href="#então-vale-a-pena-adotar-o-biome">Então, vale a pena adotar o Biome?</a></h2>
<p>O desempenho e a praticidade do Biome são claramente atraentes. No entanto, não acredito que adotá-lo sem ressalvas seja a resposta certa para todos os projetos. Vamos analisar algumas considerações práticas.</p>
<hr>
<h3 id="quando-o-biome-é-uma-boa-opção"><a class="anchor" href="#quando-o-biome-é-uma-boa-opção">Quando o Biome é uma boa opção</a></h3>
<ul>
<li>Quando você mantém uma <strong>base de código de grande escala</strong> em que o desempenho de build e linting é importante</li>
<li>Quando quer reduzir o tempo de verificação de código em uma pipeline de CI/CD</li>
<li>Quando está cansado da complexidade de configurar ESLint + Prettier</li>
<li>Quando está iniciando um projeto novo e quer uma configuração de ferramentas simples</li>
</ul>
<p>Minha equipe também mantinha um projeto de grande escala no qual o linting consumia muito tempo da pipeline de CI, e os desenvolvedores se incomodavam com a lentidão desse processo. Por isso, decidimos adotar o Biome.</p>
<hr>
<h3 id="pontos-que-exigem-atenção"><a class="anchor" href="#pontos-que-exigem-atenção">Pontos que exigem atenção</a></h3>
<p><strong>A maior limitação é o ecossistema de plugins.</strong> O ESLint tem milhares de plugins da comunidade, enquanto o Biome se concentra em regras integradas. Muitas regras de plugins importantes, como <code>eslint-plugin-react</code>, <code>eslint-plugin-react-hooks</code>, <code>eslint-plugin-jsx-a11y</code>, <code>eslint-plugin-unicorn</code> e <code>typescript-eslint</code>, vêm incluídas, mas nem todas as regras de cada plugin foram portadas. Um sistema de plugins baseado em GritQL foi anunciado para o Biome v2, porém ainda está em fase experimental. Projetos que dependem de regras específicas de frameworks, como <code>@next/eslint-plugin-next</code> ou <code>eslint-plugin-angular</code>, precisam abordar a migração com cautela.</p>
<p><strong>Também é preciso verificar o escopo do suporte a linguagens.</strong> JavaScript, TypeScript, JSX, CSS, JSON e GraphQL têm suporte estável, mas, nos arquivos SFC (Single File Component) do Vue e do Svelte, apenas o bloco <code>&#x3C;script></code> tem suporte parcial. HTML, YAML e Markdown ainda não são compatíveis.</p>
<p><strong>Não podemos esquecer que o ESLint também está evoluindo.</strong> O Flat Config (<code>eslint.config.js</code>), introduzido no ESLint v9 em abril de 2024, simplificou significativamente a complexidade do antigo formato <code>.eslintrc</code>. Além disso, com os lançamentos de <code>@eslint/json</code> em outubro de 2024 e <code>@eslint/css</code> em fevereiro de 2025, o ESLint vem expandindo o linting para linguagens além do JavaScript. O projeto ESLint Stylistic (<code>@stylistic/eslint-plugin</code>) oferece a opção de cuidar da formatação somente com o ESLint, sem o Prettier. Assim, a vantagem “all-in-one” do Biome está sendo um pouco reduzida pela evolução do ecossistema do ESLint.</p>
<p>Também vale lembrar a história da transição do Rome para o Biome. Os transtornos enfrentados pelos usuários existentes quando o Rome foi arquivado mostram como a sustentabilidade de um projeto é importante na escolha de uma ferramenta. Felizmente, o Biome é financiado pelo OpenCollective e pelo GitHub Sponsors e mantém um ritmo constante de releases.</p>
<p><img src="/content/241201/8.png" alt="8.png" width="2722" height="1384" loading="lazy" fetchpriority="low" decoding="async"></p>
<p>Segundo o npm trends, os downloads semanais do Biome, cerca de 6,9 milhões, ainda estão bem abaixo dos aproximadamente 120 milhões do ESLint e dos 82 milhões do Prettier. Mas a velocidade de crescimento do Biome chama a atenção. Em pouco mais de um ano, os downloads semanais aumentaram mais de três a quatro vezes, com uma alta especialmente visível na adoção por projetos novos.</p>
<hr>
<h2 id="conclusão"><a class="anchor" href="#conclusão">Conclusão</a></h2>
<p>Minha resposta à pergunta sobre o Biome poder substituir completamente o ESLint e o Prettier é: <strong>“ainda não, mas ele é uma alternativa muito forte”</strong>.</p>
<p>O desempenho é impressionante, a configuração é simples e o ritmo de desenvolvimento é rápido. Porém, a imaturidade do ecossistema de plugins e as limitações no suporte a algumas linguagens podem ser obstáculos, dependendo do projeto. O ideal é analisar cuidadosamente a stack tecnológica do projeto e as necessidades da equipe antes de decidir pela adoção.</p>
<p>Uma coisa é certa: o ecossistema de ferramentas frontend está avançando na direção de soluções “mais rápidas, mais simples e mais integradas”. É inegável que o Biome está na linha de frente desse movimento. Sem dúvida, é uma ferramenta cujo crescimento futuro merece atenção.</p>
<h2 id="referências"><a class="anchor" href="#referências">Referências</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, afinal, por que você é ProviderLess?]]></title>
            <link>https://hooninedev.com/pt-BR/240818</link>
            <guid isPermaLink="false">https://hooninedev.com/pt-BR/240818</guid>
            <pubDate>Sun, 18 Aug 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[Neste post, quero falar sobre como o Zustand consegue gerenciar estado sem um Provider. Enquanto usava Zustand, eu sempre tratei como natural gerenciar estado sem um Provider. Até que, de repente, me ...]]></description>
            <content:encoded><![CDATA[<p>Neste post, quero falar sobre como o Zustand consegue gerenciar estado sem um Provider.</p>
<p>Enquanto usava Zustand, eu sempre tratei como natural gerenciar estado sem um Provider. Até que, de repente, me veio uma dúvida. Na maioria das bibliotecas do ecossistema React, envolver o app com um Provider virou quase um ritual. O TanStack React Query exige um <code>QueryClientProvider</code> para usar <code>useQuery</code>, e o overlay-kit da toss também exige um <code>OverlayProvider</code> para chamar <code>overlay.open()</code>. A Context API do React também exige que a árvore de componentes seja envolvida por um Provider. Então que tipo de mágica o Zustand faz para não precisar de nada disso?</p>
<p>Por curiosidade, fui examinar diretamente o código-fonte do Zustand e encontrei uma estrutura mais interessante do que esperava. Quero organizar aqui o que aprendi nesse processo.</p>
<hr>
<h2 id="como-o-estado-flui-no-react"><a class="anchor" href="#como-o-estado-flui-no-react">Como o estado flui no React</a></h2>
<p>Em uma aplicação React comum, o estado funciona como mostra a imagem abaixo.</p>
<p><img src="/content/240818/3.png" alt="3.png" width="880" height="509" loading="eager" fetchpriority="high" decoding="async"></p>
<p>O estado interno de um componente é gerenciado com os hooks de gerenciamento de estado fornecidos pelo React (<code>useState</code>, <code>useReducer</code>). Já o estado é passado aos componentes filhos por props. Até aqui, a história é simples.</p>
<p>O problema surge quando precisamos compartilhar estado entre componentes distantes. A solução oficial oferecida pelo React nesse caso é a Context API, mas ela exige que a subárvore seja envolvida por um componente Provider.</p>
<hr>
<h3 id="por-que-a-context-api-precisa-de-um-provider"><a class="anchor" href="#por-que-a-context-api-precisa-de-um-provider">Por que a Context API precisa de um Provider?</a></h3>
<p>Para responder a essa pergunta, precisamos olhar um pouco para o funcionamento interno do React.</p>
<p>O React gerencia a árvore de componentes por meio de uma estrutura de dados interna chamada Fiber. Cada nó Fiber se conecta aos demais por relações de pai e filho e, quando o valor de um Context muda, o React percorre essa árvore Fiber de cima para baixo, encontra os componentes inscritos naquele Context e dispara uma nova renderização.</p>
<p>O ponto central é este: <strong>a propagação do valor de Context depende da estrutura da árvore Fiber.</strong> A posição do Provider na árvore determina o alcance da entrega do valor, e o componente que chama <code>useContext</code> sobe pela própria árvore Fiber até encontrar o Provider mais próximo. E se não houver Provider? Apenas o valor padrão passado a <code>createContext</code> será usado.</p>
<p>Ou seja, a Context API é fortemente acoplada ao sistema de renderização do React. O armazenamento, a propagação e a inscrição do estado acontecem dentro da árvore de componentes do React.</p>
<p>Então como o Zustand contorna essa estrutura?</p>
<hr>
<h2 id="o-zustand-vive-fora-do-react"><a class="anchor" href="#o-zustand-vive-fora-do-react">O Zustand vive fora do 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>O Zustand funciona com base no padrão Flux. O <code>state</code> dentro do closure exerce o papel de Store; as funções definidas pelo usuário, o de Actions; a função <code>set</code>, o de Dispatcher; e os componentes React, o de Views. É aqui que aparece a diferença decisiva.</p>
<p><strong>O Store do Zustand existe fora da árvore de componentes do React, dentro do escopo de um módulo JavaScript.</strong></p>
<p>Estar fora da árvore de componentes significa que, ao contrário do estado interno do React, o estado do Zustand existe de forma independente da árvore Fiber do React. Qualquer componente pode acessar o Store apenas com um <code>import</code>, sem precisar envolver o app com um Provider. (Ele fica acessível de qualquer lugar como uma variável global, mas permanece bem protegido por um closure.)</p>
<p>Como isso é possível? Vamos observar o código abaixo.</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>Nesse código, <code>create</code> é chamado no momento em que o módulo é carregado. Ou seja, o Store já existe na memória antes mesmo de o React começar a renderizar. Esse é o padrão <strong>module-level singleton</strong>.</p>
<hr>
<h3 id="o-que-é-um-module-level-singleton"><a class="anchor" href="#o-que-é-um-module-level-singleton">O que é um module-level singleton?</a></h3>
<p>O sistema de módulos ES do JavaScript <strong>avalia (evaluate) um módulo apenas na primeira vez e armazena o resultado em cache</strong>. Depois disso, qualquer <code>import</code> do mesmo módulo não o executa novamente: ele retorna exatamente o mesmo objeto armazenado. Portanto, não importa se o componente A ou o componente B faz <code>import { useStore } from './store'</code>: ambos apontam para <strong>exatamente a mesma instância do Store</strong>.</p>
<p>Não é preciso implementar uma classe singleton separada nem anexar nada a uma variável global (<code>window.store</code>). O próprio sistema de módulos satisfaz naturalmente as condições de um singleton: “ser criado uma única vez e permitir acesso à mesma instância de qualquer lugar”. O Zustand aproveita diretamente essa garantia da linguagem e permite que todos os componentes compartilhem um único Store sem um Provider separado.</p>
<p>Depois de chegar até aqui, uma pergunta surge naturalmente: como é, em detalhes, o interior do Zustand?</p>
<hr>
<h2 id="estrutura-interna-do-zustand"><a class="anchor" href="#estrutura-interna-do-zustand">Estrutura interna do Zustand</a></h2>
<p>Ao examinar o <a href="https://github.com/pmndrs/zustand/tree/main/src" target="_blank" rel="noopener noreferrer">repositório do Zustand no GitHub</a>, vemos que a lógica central é surpreendentemente concisa. Dois arquivos são fundamentais: <code>vanilla.ts</code> contém o Store propriamente dito, enquanto <code>react.ts</code> cuida da ligação com o 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> é o coração do Zustand. Tudo sobre como o Store é criado e como o estado é gerenciado está contido nesse único arquivo. Em termos mais simples, ele define o estado preso em um closure e as funções que manipulam esse estado.</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>Ao analisar esse código linha por linha, o mecanismo central do Zustand se revela.</p>
<ul>
<li>
<p><strong>Encapsulamento do estado por closure</strong></p>
<ul>
<li>
<p>A variável <code>let state: TState</code> é declarada como variável local da função <code>createStoreImpl</code>. Mesmo depois que a execução da função termina, funções internas como <code>setState</code> e <code>getState</code> continuam referenciando essa variável, por isso ela não é removida pelo garbage collector. Essa é a essência de um closure.</p>
</li>
<li>
<p>Não há como acessar diretamente a variável <code>state</code> de fora. Ela só pode ser lida com <code>getState()</code> e escrita com <code>setState()</code>. (É como implementar em um closure o campo private da programação orientada a objetos.)</p>
</li>
</ul>
</li>
<li>
<p><strong>Detecção de mudanças com <code>Object.is</code></strong></p>
<ul>
<li>
<p>Depois de calcular o novo estado, <code>setState</code> o compara ao estado anterior com <code>Object.is(nextState, state)</code>. Se a referência for a mesma, nada acontece. Essa é a primeira linha de defesa contra renderizações desnecessárias.</p>
</li>
<li>
<p>Porém, essa comparação com <code>Object.is</code> verifica a <strong>igualdade estrita de referência (strict reference equality)</strong>, o que exige atenção de quem usa a biblioteca. Não há problema quando extraímos um único valor primitivo, como um número ou uma 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>Mas a história muda quando o selector <strong>retorna um novo objeto</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>O objeto <code>{ count, name }</code> recebe uma nova referência a cada chamada, mesmo quando os valores são iguais. Como <code>Object.is</code> não compara as propriedades internas e verifica apenas a referência, o Zustand conclui que “o estado mudou” e dispara uma nova renderização toda vez.</p>
<p>Para resolver esse problema, o Zustand oferece o hook <strong><code>useShallow</code></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">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> compara uma a uma as <strong>propriedades de nível superior do objeto retornado</strong> e só provoca uma nova renderização quando os valores realmente mudam. É semelhante ao modo como <code>useSelector</code> do Redux usa comparação por referência por padrão, mas permite passar <code>shallowEqual</code> como segundo argumento. (No entanto, como o próprio nome diz, <code>useShallow</code> faz uma comparação “rasa” e não acompanha o interior de objetos aninhados.)</p>
</li>
</ul>
</li>
<li>
<p><strong>Sistema de listeners com o padrão Pub/Sub</strong></p>
<ul>
<li>A linha <code>const listeners: Set&#x3C;Listener> = new Set()</code> constitui todo o sistema de inscrição do Zustand. Quando o estado muda, <code>listeners.forEach</code> notifica todos os inscritos.</li>
<li>Ao chamar <code>subscribe</code>, o listener é adicionado ao <code>Set</code>; ao chamar a função retornada, ele é removido do <code>Set</code>.</li>
<li>Esse padrão é importante porque forma um <strong>sistema de notificações totalmente independente da árvore Fiber do React</strong>. Em vez de um Provider percorrer a árvore à procura de inscritos, o próprio Store gerencia diretamente a lista de inscritos.</li>
</ul>
</li>
<li>
<p><strong>Criação do estado inicial</strong></p>
<ul>
<li>
<p>Vamos observar a última linha, que trata o estado inicial.</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>Muita coisa está concentrada nessa linha. Em JavaScript, o operador de atribuição (<code>=</code>) é uma expressão (expression) que <strong>retorna o próprio valor atribuído</strong>. Assim, <code>state = createState(...)</code> dentro dos parênteses é executado primeiro e atribui o estado inicial a <code>state</code>; então o valor retornado é atribuído novamente a <code>const initialState</code>. Como resultado, <code>state</code> e <code>initialState</code> <strong>referenciam o mesmo objeto</strong>.</p>
<p>Mas por que guardar deliberadamente o mesmo valor em duas variáveis? O ponto central é que elas desempenham papéis diferentes.</p>
<ul>
<li><strong><code>state</code></strong> é uma variável declarada com <code>let</code>. Ela é substituída por um novo valor sempre que <code>setState</code> é chamado. Portanto, representa <strong>o estado vivo no momento atual</strong>.</li>
<li><strong><code>initialState</code></strong> é uma variável declarada com <code>const</code>. Ela preserva permanentemente o estado existente quando o Store foi criado. Nenhuma chamada posterior a <code>setState</code> altera esse valor. É <strong>o primeiro snapshot do Store</strong>.</li>
</ul>
<p>Esse <code>initialState</code> é exposto externamente pelo método <code>getInitialState()</code> e passado em <code>react.ts</code> como o <strong>terceiro argumento de <code>useSyncExternalStore</code> (snapshot do servidor)</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:#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>Em um ambiente de server-side rendering (SSR), não existem APIs do navegador nem interação do usuário, então <code>setState</code> nunca é chamado. Por isso, o servidor sempre usa <code>initialState</code> (= o estado inicial) como snapshot. Quando a hydration começa no cliente, o React compara o HTML renderizado no servidor com o resultado da primeira renderização do cliente. Como os dois foram renderizados com base no mesmo <code>initialState</code>, é possível <strong>evitar uma divergência de hydration</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> tem o papel de conectar o Store JavaScript puro criado acima ao sistema de renderização do React.</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>O ponto central aqui é <code>useSyncExternalStore</code>. Esse hook foi introduzido no React 18 e projetado para <strong>integrar com segurança ao ciclo de renderização do React um armazenamento de estado que existe fora do React</strong>.</p>
<p>A estrutura fica clara ao observarmos os três argumentos recebidos por <code>useSyncExternalStore</code>. (É quase igual ao que vimos antes em vanilla.ts.)</p>
<ul>
<li><strong><code>api.subscribe</code></strong>: função que se inscreve nas mudanças do Store. Por meio dela, o React pede: “avise quando o estado mudar”.</li>
<li><strong><code>() => selector(api.getState())</code></strong>: retorna o snapshot do estado atual. O React chama essa função a cada renderização para obter o estado mais recente.</li>
<li><strong><code>() => selector(api.getInitialState())</code></strong>: snapshot inicial usado no server-side rendering. Ele evita divergências de estado entre servidor e cliente durante a hydration.</li>
</ul>
<p>Em especial, <code>useSyncExternalStore</code> resolve o <strong>problema de tearing</strong> que pode ocorrer no modo concorrente do React (Concurrent Mode). Tearing é o fenômeno em que, dentro do mesmo passe de renderização, componentes diferentes mostram <strong>snapshots diferentes da mesma fonte de dados</strong>.</p>
<p>Um cenário concreto facilita o entendimento. O componente A lê <code>store.value</code> (= 10) e começa a renderizar. Nesse momento, o React <strong>pausa temporariamente (yield)</strong> a renderização em modo concorrente e devolve o controle ao navegador. Durante essa pausa, chega uma mensagem de WebSocket que altera <code>store.value</code> para 11. Quando o React retoma a renderização, o componente B lê <code>store.value</code> (= 11). Como resultado, no mesmo frame, A mostra 10 e B mostra 11, criando uma <strong>UI rasgada (teared)</strong>. Antes do React 18, a renderização era sempre síncrona, por isso esse problema não ocorria.</p>
<p><code>useSyncExternalStore</code> registra o snapshot existente no início da renderização (<code>getSnapshot</code>). Se o Store externo mudar durante a renderização e o snapshot ficar diferente, ele detecta a mudança e <strong>reinicia a renderização desde o início</strong>. Assim, garante que todos os componentes sejam renderizados com base no mesmo snapshot.</p>
<p>E a função <code>createImpl</code> reúne tudo isso.</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>Ela cria um Store vanilla com <code>createStore</code>, o envolve em um hook personalizado chamado <code>useBoundStore</code> e, com <code>Object.assign</code>, anexa os métodos da API do Store (<code>setState</code>, <code>getState</code>, <code>subscribe</code> etc.) à própria função hook. Como resultado, o <code>useBoundStore</code> retornado tem uma natureza dupla: <strong>é um hook do React e, ao mesmo tempo, a API do Store</strong>. (Uma função que também tem métodos: um padrão bastante característico do JavaScript.)</p>
<hr>
<h2 id="e-as-outras-bibliotecas-de-gerenciamento-de-estado"><a class="anchor" href="#e-as-outras-bibliotecas-de-gerenciamento-de-estado">E as outras bibliotecas de gerenciamento de estado?</a></h2>
<p>Depois de entender tudo isso, é natural querer comparar com outras bibliotecas.</p>
<p>Existem várias bibliotecas de gerenciamento de estado, como Jotai, Recoil, MobX, Xstate e Redux, mas vou me concentrar naquelas que já usei pessoalmente.</p>
<blockquote>
<p>Como referência, o <strong>Recoil</strong> (Meta), frequentemente comparado ao Jotai, teve seu repositório arquivado em janeiro de 2025 e, na prática, seu desenvolvimento foi interrompido. Também não houve suporte ao React 19. Para quem quer um modelo de estado atômico, hoje o Jotai pode ser considerado a única opção realista.</p>
</blockquote>
<hr>
<h3 id="redux"><a class="anchor" href="#redux">Redux</a></h3>
<p>O Redux também usa internamente um Store no nível do módulo. Então por que ele precisa de um Provider?</p>
<p>O <code>&#x3C;Provider store={store}></code> do Redux <strong>injeta (inject)</strong> a instância do Store na árvore de componentes por meio do React Context. <code>useSelector</code> e <code>useDispatch</code> chamam <code>useContext</code> internamente para acessar o Store fornecido pelo Provider. O importante aqui é que o Redux usa Context <strong>não como canal de propagação de estado, mas como mecanismo de injeção de dependência (Dependency Injection)</strong>. O que é transmitido pelo Context não é o próprio valor do estado, mas <strong>uma referência ao objeto Store</strong> que gerencia esse estado. A inscrição e as atualizações reais do estado são tratadas pelo Pub/Sub interno do Store.</p>
<p>Os benefícios desse design são claros. Em testes, envolver uma instância diferente do Store com um Provider oferece isolamento perfeito; além disso, é possível construir várias árvores de Store independentes em um único app por meio da prop <code>context</code>. Como enfatiza Mark Erikson, mantenedor do Redux, “Context é um mecanismo de transporte (transport mechanism), não uma ferramenta de gerenciamento de estado”.</p>
<hr>
<h3 id="jotai"><a class="anchor" href="#jotai">Jotai</a></h3>
<p>O Jotai adota um <strong>modelo de estado atômico (atomic)</strong> fundamentalmente diferente do Redux ou do Zustand. Em vez de reunir todo o estado em um grande objeto Store, a abordagem consiste em <strong>separar cada fragmento de estado em um atom independente</strong>. (A própria documentação oficial do Jotai explica que “se Zustand é parecido com Redux, Jotai é parecido com Recoil”.)</p>
<p>A diferença central dessa estrutura está na <strong>forma de otimizar a renderização</strong>. O Zustand segue uma abordagem <strong>de cima para baixo (top-down)</strong>, extraindo por meio de um selector apenas a parte necessária de um único Store. O desenvolvedor precisa escrever diretamente um selector como <code>useStore((state) => state.count)</code> e, às vezes, usar memoização para manter a igualdade referencial (referential equality). Já o Jotai constrói automaticamente um <strong>grafo de dependências (dependency graph)</strong> entre atoms. Quando um atom muda, ele faz uma propagação <strong>de baixo para cima (bottom-up)</strong> e renderiza novamente apenas os componentes que dependem daquele atom. Esse rastreamento automático de dependências é muito poderoso quando dezenas de estados estão interligados, como em uma planilha ou um editor de canvas.</p>
<p>Do ponto de vista do Provider, o Jotai ocupa um meio-termo interessante. Por padrão, usa um Store global e funciona sem Provider, mas, se necessário, pode ser envolvido por <code>&#x3C;Provider></code> para criar um escopo de Store isolado. Usando a expressão da documentação oficial do Jotai, o Jotai é <strong>“context first, module second”</strong>, enquanto o Zustand é <strong>“module first, context second”</strong>.</p>
<hr>
<h3 id="a-escolha-do-zustand"><a class="anchor" href="#a-escolha-do-zustand">A escolha do Zustand</a></h3>
<p>O Zustand fez a escolha mais radical. Por padrão, ele é um singleton no nível do módulo e não tem Provider algum. O resultado dessa escolha é uma <strong>API extremamente simples</strong>. Basta criar o Store com <code>create</code> e chamar o hook no componente.</p>
<p>Porém, dizer que “não tem Provider algum” descreve, para ser exato, o <strong>design padrão</strong>. Desde a v4, é possível implementar o padrão <strong>Scoped Store</strong> combinando <code>createStore</code> (um Store vanilla) com o <code>createContext</code> do React.</p>
<p>O <a href="https://tkdodo.eu/blog/zustand-and-react-context" target="_blank" rel="noopener noreferrer">blog de TkDodo, mantenedor do React Query</a>, aborda esse padrão em profundidade. O argumento central apresentado por ele é que um Store singleton global possui três limitações.</p>
<ul>
<li><strong>Não pode ser inicializado com props</strong>: como o Store é criado quando o módulo é carregado, não há como usar dados recebidos do servidor ou props do componente pai como valores iniciais.</li>
<li><strong>O isolamento de testes é difícil</strong>: é preciso redefinir manualmente o Store a cada teste.</li>
<li><strong>Não pode ser reutilizado</strong>: se dois componentes que precisam de um Store com a mesma estrutura forem renderizados na mesma página, eles acabarão compartilhando o estado.</li>
</ul>
<p>O padrão Scoped Store resolve as três limitações. A ideia central é <strong>transmitir pelo Context a referência da instância do Store, e não o valor do estado</strong>. (É exatamente a mesma estrutura usada pelo Provider do Redux.)</p>
<p>A implementação concreta é esta.</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>Agora podemos renderizar na mesma página quantos componentes multiselect independentes quisermos.</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>O ponto a observar aqui é que o que passa pelo Context <strong>não é o valor do estado, mas o objeto Store</strong>. Mesmo que o valor do estado mude, o <code>value</code> do Context (= a referência ao Store) não muda; portanto, <strong>não ocorrem renderizações desnecessárias causadas por mudanças no valor do Context.</strong> A renderização real é tratada dentro de <code>useStore</code>, onde <code>useSyncExternalStore</code> aplica o selector. O papel de transporte do Context e o papel de inscrição do Zustand ficam claramente separados.</p>
<p>TkDodo apresentou um caso real em que aplicou esse padrão a um componente multiselect de um design system. A estrutura anterior, que gerenciava o estado interno com <code>useState</code> + Context, sofria perda de desempenho com mais de 50 itens, e o problema foi resolvido ao migrar para a inscrição baseada em selectors do Zustand.</p>
<p>Depois que o helper oferecido na v3 por <code>zustand/context</code>, chamado <code>createContext</code>, foi removido na v4, esse padrão se consolidou como a <strong>combinação direta do <code>createContext</code> nativo do React com <code>createStore</code>/<code>useStore</code> do Zustand</strong>. A API permanece igual na v5, e a <a href="https://github.com/pmndrs/zustand/blob/main/docs/previous-versions/zustand-v3-create-context.md" target="_blank" rel="noopener noreferrer">documentação oficial do Zustand</a> também apresenta esse padrão no guia de migração para v4+.</p>
<hr>
<h2 id="a-sombra-do-providerless"><a class="anchor" href="#a-sombra-do-providerless">A sombra do ProviderLess</a></h2>
<p>É claro que não ter um Provider não traz apenas vantagens. Vou organizar os pontos que, na minha opinião, exigem atenção.</p>
<hr>
<h3 id="o-problema-de-compartilhamento-de-estado-em-ssr"><a class="anchor" href="#o-problema-de-compartilhamento-de-estado-em-ssr">O problema de compartilhamento de estado em SSR</a></h3>
<p>Um singleton no nível do módulo pode ser perigoso em um ambiente de servidor. Um servidor Node.js processa várias requisições em um único processo, enquanto cada módulo é carregado apenas uma vez dentro desse processo. Isso significa que requisições de usuários diferentes podem <strong>compartilhar a mesma instância do Store</strong>.</p>
<p>É por isso que o Zustand oferece <code>getInitialState</code> e passa um snapshot do servidor como terceiro argumento de <code>useSyncExternalStore</code>. No entanto, isso sozinho pode não isolar completamente o estado entre requisições. Em ambientes SSR, recomenda-se usar o padrão Scoped Store mencionado antes (<code>createStore</code> + React Context) para criar um novo Store a cada requisição.</p>
<hr>
<h3 id="a-dificuldade-de-isolar-testes"><a class="anchor" href="#a-dificuldade-de-isolar-testes">A dificuldade de isolar testes</a></h3>
<p>Em bibliotecas baseadas em Provider, envolver cada teste com um Provider diferente isola o Store naturalmente. Em contrapartida, o singleton no nível do módulo do Zustand pode vazar estado entre testes. Por isso, é necessário redefinir explicitamente o Store no <code>beforeEach</code> de cada teste. (Eu também já sofri com esse problema.)</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>Aqui também o padrão Scoped Store é a solução. Quando usamos um Provider, cada teste pode criar e injetar um Store novo, permitindo isolamento perfeito sem lógica de redefinição.</p>
<hr>
<h3 id="a-ausência-de-múltiplas-instâncias"><a class="anchor" href="#a-ausência-de-múltiplas-instâncias">A ausência de múltiplas instâncias</a></h3>
<p>Se uma aplicação precisa de dois Stores independentes com a mesma estrutura, no padrão Provider basta envolver cada um com um Provider diferente. Porém, com um singleton no nível do módulo, é preciso chamar separadamente a função de criação do Store para produzir instâncias distintas. Por exemplo, se a mesma página tiver dois painéis de abas independentes e cada um precisar gerenciar seu estado de seleção separadamente, será difícil representar isso de forma natural com um singleton global.</p>
<p>Também nesse caso, o padrão <code>createStore</code> + Context é a resposta. Se cada componente de painel de abas renderizar seu próprio Provider, serão criadas instâncias totalmente independentes com a mesma estrutura de Store. A documentação oficial do Zustand também recomenda esse padrão “quando um componente reutilizável precisa de um Store”.</p>
<h2 id="conclusão"><a class="anchor" href="#conclusão">Conclusão</a></h2>
<p>Resumindo tudo o que vimos, o design ProviderLess do Zustand é possível pela combinação dos quatro mecanismos a seguir.</p>
<ul>
<li><strong>Singleton no nível do módulo</strong>: o Store é criado fora da árvore de componentes do React, dentro do escopo de um módulo JavaScript.</li>
<li><strong>Encapsulamento do estado por closure</strong>: em <code>vanilla.ts</code>, dentro de <code>createStoreImpl</code>, a variável <code>state</code> e o Set <code>listeners</code> ficam presos em um closure e inacessíveis externamente.</li>
<li><strong>Sistema Pub/Sub próprio</strong>: em vez de percorrer a árvore Fiber, ele gerencia diretamente <code>Set&#x3C;Listener></code> para notificar os inscritos sobre mudanças de estado.</li>
<li><strong>Integração com o React por <code>useSyncExternalStore</code></strong>: sincroniza com segurança as mudanças de estado do Store externo com o ciclo de renderização do React.</li>
</ul>
<p>No fim, a pergunta que o Zustand faz é esta: “O estado precisa mesmo viver dentro do React?”. A resposta do Zustand é clara. O estado pode ficar fora do React; basta construir uma ponte quando necessário. Essa ponte é <code>useSyncExternalStore</code>.</p>
<p>É claro que essa abordagem não é a melhor em todas as situações. Em cenários como SSR, isolamento de testes e múltiplas instâncias, um design baseado em Provider pode ser mais adequado. Não existe uma única resposta certa, mas, se entendermos quais trade-offs de design cada biblioteca escolheu, poderemos selecionar a ferramenta apropriada para cada situação.</p>
<p>Recomendo também que os leitores abram ao menos uma vez o código-fonte de alguma biblioteca que usam. Talvez encontrem uma profundidade que não aparece na documentação oficial.</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="ah-e-uma-novidade"><a class="anchor" href="#ah-e-uma-novidade">Ah, e uma novidade</a></h3>
<p>Enquanto pesquisava o conteúdo acima, descobri que o <strong>Zustand v5.0.0 foi lançado oficialmente em outubro de 2024</strong>.</p>
<p>O interessante é que a v5 quase não traz funcionalidades novas. Durante a v4.x, novas funcionalidades já haviam sido adicionadas enquanto APIs existentes eram marcadas como deprecated; por isso, a v5 tem principalmente o caráter de uma <strong>versão de limpeza (cleanup)</strong>. Estas são as principais mudanças. (Para mais detalhes, consulte a <strong><a href="https://github.com/pmndrs/zustand/releases" target="_blank" rel="noopener noreferrer">página de releases</a></strong> e o <strong><a href="https://zustand.docs.pmnd.rs/reference/migrations/migrating-to-v5" target="_blank" rel="noopener noreferrer">guia de migração</a></strong>.)</p>
<ul>
<li>Os requisitos mínimos subiram para <strong>React 18 e TypeScript 4.5 ou superior</strong>.</li>
<li><strong><code>getServerState</code> foi removido</strong>. (Substituído pelo terceiro argumento de <code>useSyncExternalStore</code>.)</li>
<li>O <strong>suporte ao ES5 foi encerrado</strong>.</li>
<li>A possibilidade de definir uma <strong>função equality personalizada</strong> na função <code>create</code> foi removida.</li>
<li>A função <strong><code>shallow</code> foi aprimorada para oferecer suporte a objetos iteráveis</strong>.</li>
</ul>
<p>Ao migrar da v4 para a v5, recomenda-se primeiro atualizar para a versão mais recente da v4. Como essa versão exibe avisos de deprecation, resolvê-los antes de subir para a v5 permite fazer a transição sem dificuldades.</p>
<hr>
<h3 id="referências"><a class="anchor" href="#referências">Referências</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[Entendendo algoritmos de compressão]]></title>
            <link>https://hooninedev.com/pt-BR/240706</link>
            <guid isPermaLink="false">https://hooninedev.com/pt-BR/240706</guid>
            <pubDate>Sat, 06 Jul 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[Neste artigo, quero falar sobre algoritmos de compressão de software. Fiquei responsável por melhorar o processo de deploy de um projeto interno. A arquitetura exigia o envio de artefatos de build mui...]]></description>
            <content:encoded><![CDATA[<p>Neste artigo, quero falar sobre algoritmos de compressão de software.</p>
<p>Fiquei responsável por melhorar o processo de deploy de um projeto interno. A arquitetura exigia o envio de artefatos de build muito grandes para o S3, e logo percebi que o tamanho da pasta de build afetava diretamente tanto o tempo de upload quanto o custo de armazenamento. Daí surgiu uma pergunta natural: como poderíamos compactar e enviar esses arquivos de forma mais eficiente?</p>
<p>Quando comecei a pesquisar, encontrei muito mais opções do que esperava: zip, gzip, zstd, bzip2, xz e outras. Os nomes eram parecidos, mas não foi fácil achar uma explicação que deixasse claras as diferenças e os casos de uso de cada uma. (Eu achava que compressão era tudo mais ou menos igual, mas o mundo é grande e há muitas maneiras de diminuir arquivos.)</p>
<p>Resolvi então aproveitar a oportunidade para comparar os princípios e as características dos principais formatos e explicar por que acabei escolhendo um deles.</p>
<hr>
<h2 id="o-que-é-compressão-sem-perdas"><a class="anchor" href="#o-que-é-compressão-sem-perdas">O que é compressão sem perdas?</a></h2>
<p>Compressão sem perdas, ou lossless compression, é um método que permite reconstruir perfeitamente os dados originais. Ao contrário da compressão com perdas usada em imagens e áudio, o conteúdo descompactado não difere do original nem por um único bit. Código-fonte e artefatos de build precisam desse tipo de compressão porque a integridade dos dados é essencial.</p>
<p>A ideia central é <strong>aproveitar a redundância estatística presente nos dados</strong>. Ao substituir padrões repetidos por representações mais curtas, reduzimos o tamanho total.</p>
<p>Entre essas técnicas, os métodos <strong>baseados em dicionário (Dictionary-Based)</strong> formam uma das famílias mais usadas. Aqui, dicionário não é um livro de definições, mas uma tabela de consulta que associa trechos vistos anteriormente a códigos curtos. O <strong>LZ77</strong>, apresentado por Abraham Lempel e Jacob Ziv no artigo de 1977 <em>"A Universal Algorithm for Sequential Data Compression"</em>, publicado na IEEE Transactions on Information Theory, e o <strong>LZ78</strong>, publicado no ano seguinte, são os ancestrais dessa família. As letras “LZ” vêm dos sobrenomes dos pesquisadores. Quase todos os algoritmos posteriores baseados em dicionário, como DEFLATE, LZMA, LZ4 e Zstd, têm suas raízes nesses dois. (Não é exagero dizer que boa parte da árvore genealógica da compressão converge em Lempel e Ziv.)</p>
<p>Um exemplo simples ajuda. Se a palavra “Linux” aparecer cem vezes em um texto, o compressor pode registrá-la no dicionário na primeira ocorrência e substituir as seguintes por uma referência curta que signifique “entrada número 1”. “Linux” ocupa cinco bytes, enquanto o ponteiro pode exigir menos, reduzindo o tamanho do conjunto.</p>
<p>Então, qual é exatamente a diferença entre LZ77 e LZ78?</p>
<hr>
<h3 id="lz77-a-abordagem-da-janela-deslizante"><a class="anchor" href="#lz77-a-abordagem-da-janela-deslizante">LZ77: a abordagem da janela deslizante</a></h3>
<p>O LZ77 <strong>não cria um dicionário explícito separado</strong>. Em vez disso, usa uma região do próprio fluxo de entrada como dicionário. Essa região é chamada de <strong>janela deslizante</strong> porque avança enquanto os dados são processados. (É o mesmo conceito que aparece com frequência em exercícios de algoritmos.)</p>
<p>A janela é dividida em duas áreas.</p>
<ul>
<li><strong>Buffer de busca (Search Buffer)</strong>: os dados já processados. Ele funciona como o dicionário.</li>
<li><strong>Buffer de antecipação (Look-ahead Buffer)</strong>: os dados ainda não processados que serão comprimidos em seguida.</li>
</ul>
<p>O algoritmo procura saber se o início do buffer de antecipação já apareceu em algum ponto do buffer de busca. Quando encontra o mesmo padrão, codifica a correspondência em uma tupla <strong>(distância, comprimento, próximo caractere)</strong>. A distância indica quantos caracteres é preciso voltar para encontrar o começo do trecho, e o comprimento informa quantos caracteres coincidem.</p>
<p>Imagine compactar a string <code>"banana_banana"</code> com LZ77. Ao chegar ao segundo <code>"banana"</code>, o algoritmo está efetivamente dizendo: <em>“Volte sete caracteres e copie os próximos seis.”</em> Assim, uma string de seis bytes pode ser representada por apenas dois números.</p>
<p>O ponto principal é que <strong>não é necessário armazenar nem transmitir o dicionário separadamente</strong>. O decodificador reconstrói o buffer de busca naturalmente durante a descompressão, de modo que o dicionário fica implícito nos próprios dados. A contrapartida é que a descompressão precisa avançar sequencialmente desde o início. Em princípio, não é possível começar em um ponto arbitrário no meio do arquivo.</p>
<p>O tamanho da janela tem uma relação direta de compromisso com a taxa de compressão. Uma janela maior consegue referenciar padrões mais distantes e tende a comprimir melhor, mas aumenta o custo da busca e o uso de memória.</p>
<hr>
<h3 id="lz78-um-dicionário-explícito"><a class="anchor" href="#lz78-um-dicionário-explícito">LZ78: um dicionário explícito</a></h3>
<p>Ao contrário do LZ77, o LZ78 <strong>constrói um dicionário explícito</strong> durante a compressão. Não há janela deslizante. Padrões observados anteriormente são guardados como entradas indexadas e, quando se repetem, são substituídos pelos índices.</p>
<p>O LZ78 produz unidades na forma <strong>(índice do dicionário, próximo caractere)</strong>. O codificador encontra a entrada mais longa que coincide, emite o índice junto ao caractere que quebra a correspondência e adiciona <em>“a entrada encontrada mais o novo caractere”</em> ao dicionário. Assim, o dicionário cresce aos poucos durante o processamento.</p>
<p>A variação mais famosa do LZ78 é o <strong>LZW</strong> (Lempel-Ziv-Welch). Terry Welch publicou essa melhoria em 1984, e ela foi usada no formato de imagem GIF e no utilitário Unix <code>compress</code>, com a extensão <code>.Z</code>. (O LZW já esteve no centro de uma disputa de patentes, episódio que contribuiu para o surgimento do PNG.)</p>
<hr>
<h3 id="de-qual-família-descendem-os-algoritmos-modernos"><a class="anchor" href="#de-qual-família-descendem-os-algoritmos-modernos">De qual família descendem os algoritmos modernos?</a></h3>
<p>Curiosamente, quase todos os algoritmos de compressão dominantes hoje são <strong>descendentes do LZ77</strong>.</p>
<p>O <strong>LZSS</strong>, publicado por Storer e Szymanski em 1982, aprimorou o LZ77 adicionando um indicador de um bit para distinguir se cada saída é um literal, isto é, um caractere original, ou um par comprimento-distância. Quando uma correspondência é curta demais e a referência custaria mais, o codificador simplesmente mantém o caractere original.</p>
<p>Em 1993, Phil Katz combinou o LZSS com a <strong>codificação de Huffman</strong>, que atribui sequências de bits mais curtas aos símbolos mais frequentes, e criou o <strong>DEFLATE</strong>. ZIP, GZIP e PNG usam DEFLATE. Ou seja, os arquivos <code>.zip</code>, <code>.gz</code> e <code>.png</code> que manipulamos todos os dias são descendentes diretos do LZ77.</p>
<p>Algoritmos posteriores como <strong>LZMA</strong> (7-Zip e XZ), <strong>LZ4</strong> e <strong>Zstd</strong> também partem da janela deslizante do LZ77 e evoluem as estruturas de busca e os métodos de codificação de entropia. A família LZ78, por outro lado, praticamente deixou o cenário principal depois do LZW.</p>
<p>Foi provado que os dois algoritmos têm capacidade teórica equivalente <em>quando todo o conjunto de dados é descompactado</em>. Ainda assim, o LZ77 sobreviveu porque <strong>incorporar o dicionário aos dados tornou o projeto mais flexível para implementar e estender</strong>. O tamanho da janela, os algoritmos de busca e o codificador de entropia posterior podiam ser combinados livremente, deixando espaço para evoluir com as necessidades de cada época.</p>
<p>O desempenho de compressão costuma ser avaliado em dois eixos: a <strong>taxa de compressão</strong>, ou quanto o arquivo diminui, e a <strong>velocidade de compressão</strong>, ou quanto tempo o processo leva. Buscar uma taxa maior geralmente exige mais processamento e, portanto, mais tempo. Uma estratégia prática consiste em encontrar o ponto certo entre os dois.</p>
<p>Com essa base, vamos comparar os principais formatos um a um.</p>
<hr>
<h2 id="zip"><a class="anchor" href="#zip">ZIP</a></h2>
<p>ZIP é um formato criado por Phil Katz em 1989. Internamente, costuma usar <strong>DEFLATE</strong>, a combinação de LZ77 com Huffman coding. A distinção importante é que ZIP não é um algoritmo de compressão, mas um formato contêiner que armazena dados comprimidos por algoritmos como DEFLATE.</p>
<p>O ZIP <strong>comprime cada arquivo individualmente</strong>. Isso é chamado de arquivo não sólido (Non-solid Archive) e permite extrair um arquivo específico sem descompactar os demais. Em contrapartida, não aproveita dados duplicados entre arquivos, por isso sua taxa pode ser inferior à do tar.gz, que veremos adiante.</p>
<p>Windows, macOS, Linux e a maioria dos sistemas operacionais oferecem suporte sem software adicional. Por isso, é uma escolha segura quando a compatibilidade entre plataformas é importante.</p>
<hr>
<h2 id="gzip-gnu-zip"><a class="anchor" href="#gzip-gnu-zip">GZIP (GNU Zip)</a></h2>
<p>Assim como ZIP, GZIP usa <strong>DEFLATE</strong> internamente. Por que existe um formato separado se o algoritmo é o mesmo? ZIP também funciona como contêiner para vários arquivos, enquanto GZIP é especializado em comprimir <strong>um único arquivo ou fluxo</strong>.</p>
<p>Para comprimir vários arquivos ou um diretório com GZIP, primeiro reunimos tudo em um arquivo TAR e depois comprimimos esse arquivo com GZIP. Esse processo em duas etapas produz um <code>.tar.gz</code> ou <code>.tgz</code>.</p>
<p>A estrutura do GZIP, definida na RFC 1952, é simples: um <strong>cabeçalho fixo de 10 bytes</strong>, um cabeçalho estendido opcional com informações como nome original e comentários, os dados comprimidos com DEFLATE e um <strong>trailer de 8 bytes</strong> contendo o checksum CRC-32 e o tamanho original. O CRC-32 verifica se os dados descompactados são iguais ao original. Portanto, GZIP é uma camada leve em torno de um fluxo DEFLATE.</p>
<p>O DEFLATE usa uma janela deslizante de <strong>no máximo 32 KB</strong>. Esse limite é importante porque padrões separados por mais de 32 KB não podem se referenciar. O GZIP também oferece níveis de 1 a 9. O nível 1 é rápido, mas produz uma taxa menor, em torno de 60%; o nível 9 é lento, mas chega a aproximadamente 75%. O nível 6 é o padrão e procura equilibrar velocidade e tamanho.</p>
<p>Em ambientes Unix e Linux, GZIP é usado como padrão para distribuir código-fonte, comprimir logs e empacotar software. Também segue comum na compressão HTTP por meio de <code>Content-Encoding: gzip</code>, embora o Brotli venha substituindo-o gradualmente nesse uso.</p>
<hr>
<h2 id="zstd-zstandard"><a class="anchor" href="#zstd-zstandard">ZSTD (Zstandard)</a></h2>
<p>ZSTD é um algoritmo desenvolvido por Yann Collet na Meta, antiga Facebook, e publicado como código aberto em 2016. Sua principal vantagem é <strong>comprimir e descomprimir muito mais rápido, mantendo uma taxa comparável à do GZIP</strong>.</p>
<p>Seu funcionamento tem três grandes etapas. Primeiro, um <strong>localizador de correspondências (Match Finder)</strong> da família LZ77 detecta padrões repetidos na entrada. Depois, codifica literais, comprimentos e deslocamentos como <strong>sequências</strong>. Por fim, comprime essas sequências com <strong>codificação de entropia</strong>. Em vez de depender apenas de Huffman como o GZIP, usa <strong>FSE (Finite State Entropy)</strong>, um codificador baseado em ANS (Asymmetric Numeral Systems) que combina propriedades de Huffman e da codificação aritmética (Arithmetic Coding). Huffman só consegue atribuir números inteiros de bits por símbolo; o FSE representa probabilidades equivalentes a bits fracionários e se aproxima mais do limite teórico. (Apesar do nome grandioso, a ideia é apenas expressar os mesmos dados com menos bits de forma mais inteligente.)</p>
<p>O localizador também muda de estratégia conforme o nível. Os níveis baixos, de 1 a 4, usam tabelas hash simples para priorizar velocidade. Os intermediários, de 5 a 12, comparam vários candidatos por uma estratégia Lazy. Os altos, de 13 a 22, usam árvores binárias e programação dinâmica para encontrar correspondências quase ideais. Essa faixa permite aplicar níveis baixos em transmissão em tempo real e altos em arquivamento.</p>
<p>No benchmark Silesia Corpus, o nível padrão 3 do ZSTD comprime a cerca de 300 MB/s e descomprime a aproximadamente 1.200 MB/s. Já o nível padrão 6 do GZIP alcança apenas 34 MB/s e 380 MB/s. <strong>O ZSTD comprime cerca de oito vezes mais rápido e descomprime três vezes mais rápido, enquanto sua taxa é ligeiramente melhor: 3,17 contra 3,09 do GZIP.</strong> Esses números mostram com clareza como o ZSTD melhora o compromisso tradicional.</p>
<p>A adoção cresceu rapidamente. O ZSTD é usado na compressão de módulos do kernel Linux e na compressão transparente de sistemas de arquivos; distribuições como Arch Linux, Fedora, Debian e Ubuntu o adotaram para pacotes. Desde a versão 1.5.7, lançada em fevereiro de 2025, a <strong>compressão multithread fica ativada por padrão</strong> com até quatro threads, ampliando ainda mais a diferença prática em relação ao GZIP single-thread. A AWS também informou ter reduzido em cerca de 30% o armazenamento no S3 ao migrar serviços internos de gzip para zstd.</p>
<hr>
<h2 id="bzip2"><a class="anchor" href="#bzip2">BZIP2</a></h2>
<p>O BZIP2 comprime dados por uma sequência de transformações.</p>
<ol>
<li><strong>RLE (Run-Length Encoding)</strong>: reduz repetições consecutivas nos dados iniciais</li>
<li><strong>BWT (Burrows-Wheeler Transform)</strong>: reorganiza os dados para facilitar a compressão</li>
<li><strong>MTF (Move-to-Front Transform)</strong>: converte a saída do BWT em uma sequência numérica</li>
<li><strong>RLE</strong>: reduz novamente as repetições do resultado do MTF</li>
<li><strong>Huffman Coding</strong>: aplica por fim uma codificação baseada em frequência</li>
</ol>
<p>O BZIP2 oferece taxa maior que o GZIP, mas tanto a compressão quanto a descompressão são mais lentas. Ele foi usado para arquivamento quando o tamanho importava mais do que a velocidade.</p>
<p>Sua última versão foi a 1.0.8, em 2019, e o desenvolvimento ativo praticamente parou. Conforme benchmarks mostram que o ZSTD supera o BZIP2 em taxa e velocidade, projetos novos tendem a escolher ZSTD.</p>
<hr>
<h2 id="xz"><a class="anchor" href="#xz">XZ</a></h2>
<p>XZ é um formato que usa <strong>LZMA2</strong>. LZMA, Lempel-Ziv-Markov chain Algorithm, foi desenvolvido por Igor Pavlov e combina compressão por dicionário baseada em LZ77 com codificação por intervalo (Range Encoding). Em vez de ser apenas uma “versão melhorada do LZMA”, LZMA2 se parece mais com um <strong>formato contêiner</strong> para fluxos LZMA. Ele acrescenta compressão e descompressão multithread e tratamento eficiente para dados que não podem ser comprimidos.</p>
<p>Entre os formatos discutidos aqui, o XZ oferece <strong>a maior taxa de compressão</strong>. O custo é uma compressão muito lenta e alto consumo de memória. É adequado para arquivamento quando economizar espaço é a prioridade absoluta.</p>
<p>Em março de 2024, porém, <strong>foi descoberta uma backdoor no xz-utils, a biblioteca central do XZ, no grave incidente de cadeia de suprimentos CVE-2024-3094</strong>. Uma campanha de engenharia social de dois anos havia obtido permissões de mantenedor, e a vulnerabilidade recebeu a nota máxima CVSS 10.0. As principais distribuições voltaram imediatamente a versões seguras, mas o caso serviu como um alerta importante sobre segurança na cadeia de software open source. (O valor técnico do XZ continua existindo, mas vale considerar esse contexto na escolha de ferramentas.)</p>
<hr>
<h2 id="tar"><a class="anchor" href="#tar">TAR</a></h2>
<p>TAR, Tape Archive, não é um algoritmo de compressão. É uma ferramenta e um formato para <strong>reunir vários arquivos e diretórios em um único arquivo</strong>. Como o nome indica, foi criado originalmente para backups em fita magnética. Como a fita é um meio sequencial, concatenar os dados continuamente era uma estrutura natural.</p>
<p>Sua organização interna é surpreendentemente simples. Tudo é processado em <strong>blocos de 512 bytes</strong>. Cada arquivo começa com um cabeçalho de 512 bytes que contém metadados como nome, com até 100 bytes, modo, UID/GID do proprietário, tamanho, data de modificação e checksum. Os dados vêm depois e recebem padding até um múltiplo de 512 bytes. Dois blocos zerados de 512 bytes marcam o fim do arquivo. A maioria das implementações modernas segue o formato <strong>UStar (Unix Standard TAR)</strong>, definido pelo POSIX, que aceita nomes de até 256 bytes e campos adicionais.</p>
<p>A característica principal é preservar <strong>metadados do sistema de arquivos Unix</strong>, incluindo permissões, propriedade, timestamps e links simbólicos. ZIP nem sempre mantém perfeitamente esses dados específicos do Unix, por isso TAR costuma ser mais adequado para deploys em servidores.</p>
<p>TAR não reduz o tamanho por conta própria; cabeçalhos e padding podem até deixar o resultado um pouco maior. A compressão real ocorre ao combiná-lo com GZIP, BZIP2, XZ ou ZSTD. É daí que vêm extensões como <code>.tar.gz</code>, <code>.tar.bz2</code>, <code>.tar.xz</code> e <code>.tar.zst</code>. TAR cuida de “agrupar”, e a outra ferramenta, de “reduzir”: um exemplo clássico da filosofia Unix de “fazer bem uma única coisa”.</p>
<p>É o método padrão de arquivamento em Unix/Linux, enquanto o Windows pode precisar de software adicional, como 7-Zip.</p>
<hr>
<h2 id="uma-breve-introdução-ao-brotli"><a class="anchor" href="#uma-breve-introdução-ao-brotli">Uma breve introdução ao Brotli</a></h2>
<p>Quem trabalha com frontend também deve conhecer o <strong>Brotli</strong>. O Google desenvolveu esse algoritmo, que em 2015 foi padronizado para compressão de fluxos HTTP como <code>Content-Encoding: br</code>.</p>
<p>Todos os navegadores principais oferecem suporte em HTTPS, com cobertura global acima de 96%, e ele costuma produzir arquivos <strong>cerca de 15% a 25% menores que o GZIP</strong>. É especialmente eficaz para arquivos estáticos de texto, como JavaScript, CSS e HTML. Grandes CDNs, incluindo Cloudflare, usam Brotli como padrão, e a prática moderna pode ser resumida em “Brotli primeiro, GZIP como fallback”.</p>
<p>Se os artefatos são enviados para o S3 e servidos por uma CDN, pré-comprimir os arquivos estáticos com Brotli pode reduzir bastante a transferência de rede. (Naquele momento, eu não tinha evidências específicas do projeto suficientes para adotá-lo imediatamente, mas ele continua sendo uma alternativa que vale conhecer e reavaliar.)</p>
<hr>
<h2 id="por-que-targz-comprime-melhor-que-zip"><a class="anchor" href="#por-que-targz-comprime-melhor-que-zip">Por que tar.gz comprime melhor que ZIP?</a></h2>
<p>A razão está na diferença entre <strong>arquivos sólidos (Solid Archive)</strong> e <strong>não sólidos (Non-solid Archive)</strong>.</p>
<p>Com tar.gz, o TAR reúne todos os arquivos em um fluxo contínuo e o GZIP comprime esse fluxo inteiro de uma vez. Assim, consegue reconhecer e aproveitar <strong>dados duplicados entre arquivos</strong>. Esse é o modelo de arquivo sólido. Se uma pasta de build contém dezenas de bundles JavaScript com estruturas parecidas, um padrão encontrado no arquivo A pode ser referenciado quando reaparece no B. A sobrecarga também diminui porque não é preciso registrar cabeçalho, checksum e tabela de conteúdos separados para cada fluxo comprimido.</p>
<p>ZIP é não sólido e comprime cada arquivo de forma independente, portanto não aproveita redundâncias entre eles. Mesmo que A e B contenham o mesmo bloco de código, seus fluxos DEFLATE não sabem da existência um do outro. É por isso que tar.gz geralmente obtém uma taxa de 5% a 15% melhor que ZIP. A diferença aumenta quando o artefato contém muitos arquivos de estrutura semelhante.</p>
<p>Arquivos sólidos também têm desvantagens claras.</p>
<ul>
<li>Para extrair um único arquivo, pode ser necessário <strong>descomprimir primeiro todos os dados anteriores a ele</strong>. Como tudo pertence a um único fluxo, não é possível saltar diretamente para o meio. ZIP permite acesso aleatório a cada arquivo e pode ser melhor quando itens específicos são extraídos com frequência.</li>
<li>Se uma parte for corrompida, <strong>todos os dados posteriores ao ponto danificado podem se tornar irrecuperáveis</strong>. Em um formato não sólido, às vezes apenas o arquivo afetado é perdido e o restante permanece intacto.</li>
</ul>
<hr>
<p><strong>Adicionado em 2026</strong></p>
<h2 id="em-2024-escolhi-targz-o-que-escolheria-hoje"><a class="anchor" href="#em-2024-escolhi-targz-o-que-escolheria-hoje">Em 2024 escolhi tar.gz. O que escolheria hoje?</a></h2>
<p>Na época, escolhi tar.gz por compatibilidade e estabilidade. Depois do upload para o S3, o artefato precisava ser descompactado em vários ambientes, então um formato disponível praticamente em qualquer lugar era a opção segura.</p>
<p>Se eu enfrentasse a mesma situação hoje, consideraria seriamente <strong>tar.zst (TAR + ZSTD)</strong>. Vale lembrar os números anteriores.</p>
<p>O GZIP comprime a 34 MB/s no nível padrão, enquanto o ZSTD chega a 300 MB/s. Para uma pasta de 2 GB, uma conta simples resulta em aproximadamente 60 segundos com GZIP e sete com ZSTD. Considerando ainda o multithreading ativado por padrão desde o ZSTD v1.5.7, com até quatro threads, a diferença prática pode ser maior. Em uma pipeline de CI/CD, esses segundos se acumulam a cada deploy.</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>O ZSTD também iguala ou supera a taxa do GZIP, portanto praticamente desaparece o compromisso de aceitar um artefato maior para ganhar velocidade. Ele é mais rápido e produz um resultado menor.</p>
<p>Ainda assim, é essencial verificar se o ambiente de destino consegue descompactar zstd. As principais distribuições Linux já o incluem, e no macOS é fácil instalá-lo pelo Homebrew com <code>brew install zstd</code>. Sistemas legados ou instalações mínimas podem exigir uma instalação adicional, então todos os ambientes usados pela equipe devem ser verificados antes. Se compatibilidade for a prioridade absoluta, tar.gz continua sendo a alternativa mais segura.</p>
<hr>
<h2 id="comparação-rápida"><a class="anchor" href="#comparação-rápida">Comparação rápida</a></h2>
<table>
<thead>
<tr>
<th>Formato</th>
<th>Algoritmo</th>
<th>Taxa</th>
<th>Velocidade</th>
<th>Principais características</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>ZIP</strong></td>
<td>DEFLATE</td>
<td>Média</td>
<td>Rápida</td>
<td>Multiplataforma, não sólido</td>
</tr>
<tr>
<td><strong>GZIP</strong></td>
<td>DEFLATE</td>
<td>Média</td>
<td>Rápida</td>
<td>Fluxo único, combinado com TAR</td>
</tr>
<tr>
<td><strong>ZSTD</strong></td>
<td>Zstandard</td>
<td>Alta</td>
<td>Muito rápida</td>
<td>Níveis ajustáveis, padrão moderno</td>
</tr>
<tr>
<td><strong>BZIP2</strong></td>
<td>BWT+MTF+Huffman</td>
<td>Alta</td>
<td>Lenta</td>
<td>Desenvolvimento praticamente parado</td>
</tr>
<tr>
<td><strong>XZ</strong></td>
<td>LZMA2</td>
<td>Muito alta</td>
<td>Muito lenta</td>
<td>Maior taxa, contexto de segurança</td>
</tr>
<tr>
<td><strong>Brotli</strong></td>
<td>Brotli</td>
<td>Alta</td>
<td>Média</td>
<td>Especializado para a web</td>
</tr>
</tbody>
</table>
<hr>
<h2 id="conclusão"><a class="anchor" href="#conclusão">Conclusão</a></h2>
<p>Antes de me aprofundar em compressão, eu sinceramente pensava: “Não basta colocar tudo em um zip?”. Trabalhar com uma pasta de build maior que 2 GB tornou concreto que a escolha do algoritmo pode mudar de forma significativa o tempo de upload e o custo.</p>
<p>Cada formato tem sua própria filosofia e seus compromissos: a compatibilidade do ZIP, a universalidade do GZIP, a velocidade do ZSTD e a taxa do XZ. Não existe uma opção “melhor” para todos os casos; a escolha certa depende do contexto do projeto.</p>
<p>Entender os princípios das ferramentas que usamos sem pensar ajuda a tomar decisões melhores quando aparece um problema parecido. Espero que este artigo sirva como uma pequena referência para quem precisar escolher um algoritmo de compressão.</p>]]></content:encoded>
            <author>jihoon7705@gmail.com (이지훈)</author>
            <category>소박한궁금증</category>
            <category>소프트웨어</category>
        </item>
    </channel>
</rss>