<?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>Thu, 15 Oct 2026 20:24:58 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[As layers de um app React]]></title>
            <link>https://hooninedev.com/pt-BR/261001</link>
            <guid isPermaLink="false">https://hooninedev.com/pt-BR/261001</guid>
            <pubDate>Thu, 01 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Neste post quero falar sobre como dividir o código de um app React em tela, regras de negócio e acesso a dados. É um texto para quem tem fetch, cálculo e render juntos em um único component e precisa ...]]></description>
            <content:encoded><![CDATA[<p>Neste post quero falar sobre como dividir o código de um app React em tela, regras de negócio e acesso a dados. É um texto para quem tem fetch, cálculo e render juntos em um único component e precisa ler tudo toda vez que mexe nele. Lendo até o fim, você vai saber a partir de quais sinais e em que ordem extrair hooks, pure components, domain objects, Strategy e um network client, e onde essa estrutura quebra quando é copiada como está.</p>
<p>A espinha dorsal é <a href="https://martinfowler.com/articles/modularizing-react-apps.html" target="_blank" rel="noopener noreferrer">Modularizing React Applications with Established UI Patterns</a>, publicado em partes por Juntao Qiu, da Thoughtworks, no martinfowler.com em fevereiro de 2023. Não traduzi o original. Reescrevi com as minhas palavras, e todas as figuras do texto vêm desse artigo. Por cima disso, baixei o <a href="https://github.com/abruzzi/payment-round-up-refactoring" target="_blank" rel="noopener noreferrer">repositório de exemplo</a> do autor, rodei a checagem de tipos e os testes, e acrescentei os resultados junto com outros materiais que tratam do mesmo problema.</p>
<h2 id="react-é-uma-view-library"><a class="anchor" href="#react-é-uma-view-library">React é uma view library</a></h2>
<p>Quando se aprende React, a ideia mais atraente é que a UI é uma função pura que transforma dados em DOM. Até certo ponto, é verdade. Mas no momento em que você manda uma requisição ao servidor ou navega para outra página, o component deixa de ser puro. Junte a isso o state global e o state local se entrelaçando, e o código fica complicado rapidinho.</p>
<p>A resposta do autor é mudar o ponto de vista. Não existe uma espécie à parte de software chamada "aplicação React". React é uma library que desenha a UI, e onde colocar cálculos ou regras de negócio nunca foi uma preocupação dela. O que fica por cima é um app JavaScript comum. Sendo assim, dá para usar como estão os designs que vêm desde a época das GUIs de desktop, em especial a divisão do código em três layers: presentation, domain e data.</p>
<p>Um app de frontend real tem, além da view, router, local storage, vários níveis de cache, requisições de rede, integração com serviços externos e login, segurança, logging e ajuste de performance. Se você empurra tudo isso para components e hooks, num mesmo arquivo a linha que pede o status do pedido é seguida por uma linha que tira os espaços do começo de uma string, e depois por uma linha que navega para outra tela. Quem lê precisa ficar indo e voltando enquanto troca de nível de abstração o tempo todo.</p>
<p>Os motivos para dividir se resumem a dois. A tela muda com mais frequência do que as regras de negócio. E, com as duas separadas, basta pensar em uma coisa de cada vez. O segundo motivo é o que Martin Fowler aponta em <a href="https://martinfowler.com/bliki/PresentationDomainDataLayering.html" target="_blank" rel="noopener noreferrer">PresentationDomainDataLayering</a> como a maior vantagem dessa separação. Como dá para pensar nos três temas de forma relativamente isolada, o escopo que exige atenção diminui.</p>
<h2 id="cinco-estágios-de-crescimento-de-um-app"><a class="anchor" href="#cinco-estágios-de-crescimento-de-um-app">Cinco estágios de crescimento de um app</a></h2>
<p>Antes de entrar na refatoração, o autor mostra em cinco figuras como a estrutura muda conforme o app cresce. O que importa é o gatilho para passar ao estágio seguinte. Não é um momento predeterminado: o incômodo que surge em cada estágio é o que chama o próximo. As cores têm o mesmo significado nas cinco figuras. Verde-claro é o container component que tem state, verde-escuro é o presentational component que só desenha, roxo é hook, azul é domain object e laranja é infrastructure, como a rede.</p>
<p>No começo, um único component tem requisição de rede, state, cálculo e render. Para um app pequeno ou um projeto descartável isso basta, e como o código se parece com HTML, fica até mais fácil de ler. Quando o código que percorre uma lista para montar itens se mistura com o código que configura components externos e ler o que está acontecendo começa a tomar tempo, é hora do próximo estágio.</p>
<p><picture><source type="image/webp" srcset="/content/261001/1.720w.webp 720w, /content/261001/1.1440w.webp 1440w, /content/261001/1.1600w.webp 1600w" sizes="(max-width: 752px) calc(100vw - 32px), 720px"><img src="/content/261001/1.png" alt="Estrutura em que um único component tem requisição de rede, gerenciamento de state, domain logic e render, e chama a API diretamente" style="max-width:min(720px, 100%);height:auto;" width="1600" height="661" loading="eager" fetchpriority="high" decoding="async"></picture></p>
<p>Quando a tela cresce, os components são divididos seguindo o formato do HTML resultante. Os components que só desenham se separam, mas no component do topo continuam a requisição de rede, o código que transforma a resposta no formato da tela e o código que junta os dados a mandar para o servidor. Nenhum dos três é UI, e mesmo assim estão dentro do component.</p>
<p><picture><source type="image/webp" srcset="/content/261001/2.720w.webp 720w, /content/261001/2.1440w.webp 1440w, /content/261001/2.1600w.webp 1600w" sizes="(max-width: 752px) calc(100vw - 32px), 720px"><img src="/content/261001/2.png" alt="Estrutura em que os components que só desenham foram separados, mas a requisição de rede e a domain logic continuam no component de cima" style="max-width:min(720px, 100%);height:auto;" width="1600" height="984" loading="lazy" fetchpriority="low" decoding="async"></picture></p>
<p>Em seguida, o state e suas mudanças vão para um custom hook. Aí sobra dentro do hook um cálculo puro, que não é side effect nem state.</p>
<p><picture><source type="image/webp" srcset="/content/261001/3.720w.webp 720w, /content/261001/3.1440w.webp 1440w, /content/261001/3.1600w.webp 1600w" sizes="(max-width: 752px) calc(100vw - 32px), 720px"><img src="/content/261001/3.png" alt="Estrutura em que a requisição de rede e a domain logic foram para o hook, mas continuam misturadas dentro dele" style="max-width:min(720px, 100%);height:auto;" width="1600" height="872" loading="lazy" fetchpriority="low" decoding="async"></picture></p>
<p>Extraindo esse cálculo para um objeto que não tem nada a ver com React, nasce um domain object. Conversão de formato de dados, checagem de null e valores de fallback vêm para cá. Conforme os objetos se multiplicam, herança e polimorfismo começam a ser necessários.</p>
<p><picture><source type="image/webp" srcset="/content/261001/4.720w.webp 720w, /content/261001/4.1440w.webp 1440w, /content/261001/4.1600w.webp 1600w" sizes="(max-width: 752px) calc(100vw - 32px), 720px"><img src="/content/261001/4.png" alt="Estrutura em que só o state fica no hook, a domain logic vai para um objeto Domain e a requisição de rede vai para um Fetcher" style="max-width:min(720px, 100%);height:auto;" width="1600" height="790" loading="lazy" fetchpriority="low" decoding="async"></picture></p>
<p>Por fim, quando aparecem objetos que não pertencem à UI e nem se importam se os dados vêm do servidor, do local storage ou de um cache, eles são agrupados à parte em uma model layer.</p>
<p><picture><source type="image/webp" srcset="/content/261001/5.720w.webp 720w, /content/261001/5.1440w.webp 1440w, /content/261001/5.1600w.webp 1600w" sizes="(max-width: 752px) calc(100vw - 32px), 720px"><img src="/content/261001/5.png" alt="Estrutura em que a faixa do meio é a model layer, com components e hooks acima e infrastructure como Fetcher e Adaptor abaixo" style="max-width:min(720px, 100%);height:auto;" width="1600" height="917" loading="lazy" fetchpriority="low" decoding="async"></picture></p>
<p>Só pelas figuras, fica abstrato. Vamos percorrer esses cinco estágios uma vez com código de verdade. O exemplo escolhido pelo autor é a tela de pagamento de um pedido online.</p>
<h2 id="partindo-de-uma-única-tela-de-pagamento"><a class="anchor" href="#partindo-de-uma-única-tela-de-pagamento">Partindo de uma única tela de pagamento</a></h2>
<p>Os meios de pagamento vêm da configuração do servidor e variam por país. Se o servidor devolve pelo menos um, o pagamento em dinheiro é acrescentado no fim e deixado selecionado como padrão. Se não devolve nenhum, nada é exibido.</p>
<p><picture><source type="image/webp" srcset="/content/261001/6.720w.webp 720w, /content/261001/6.1440w.webp 1440w, /content/261001/6.1600w.webp 1600w" sizes="(max-width: 632px) calc(100vw - 32px), 600px"><img src="/content/261001/6.png" alt="Tela com radios de apple, google e pagamento em dinheiro na área de pagamento abaixo dos detalhes do pedido, com pagamento em dinheiro selecionado" style="max-width:min(600px, 100%);height:auto;" width="1600" height="1130" loading="lazy" fetchpriority="low" decoding="async"></picture></p>
<p>O código inicial tem a cara que se vê em todo tutorial. Faz fetch dentro do <code>useEffect</code>, transforma a resposta no formato da tela, acrescenta o pagamento em dinheiro e desenha a lista de radios. Abaixo está a versão que eu resumi.</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">export</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> const</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> Payment</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">amount</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">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:#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">paymentMethods</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">setPaymentMethods</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">LocalPaymentMethod</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">    fetch</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(url)</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">methods</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> RemotePaymentMethod</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"> (methods.</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">length</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 style="--shiki-dark:#F97583;--shiki-light:#D73A49">return</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> setPaymentMethods</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"> extended</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> methods.</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">m</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"> ({ provider: m.name, label: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">`Pay with ${</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">m</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">.</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">name</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">        extended.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">push</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({ provider: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"cash"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, label: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"Pay in cash"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> });</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">        setPaymentMethods</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(extended);</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"> (</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">      {paymentMethods.</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">method</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">label</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> key</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{method.provider}></span></span>
<span data-line=""><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"> type</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"radio"</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> defaultChecked</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{method.provider </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">===</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> "cash"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">} /></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">          {method.label}</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">        &#x3C;/</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">label</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:#85E89D;--shiki-light:#22863A">button</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">>${amount}&#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></code></pre></figure>
<p>Nesse tamanho, não é problema. Mas para mexer nesse component é preciso entender quatro coisas ao mesmo tempo: como a requisição de rede começa, como os dados do servidor viram o formato que a tela conhece, como se desenha um meio de pagamento e como se desenha a área de pagamento inteira. Conforme o código cresce, quem lê precisa ficar indo e voltando entre essas quatro.</p>
<h2 id="hooks-e-pure-components"><a class="anchor" href="#hooks-e-pure-components">Hooks e pure components</a></h2>
<p>A primeira coisa a extrair é o state e o fetch. O sinal é simples. <strong>Quando o código sem relação com render passa da metade do corpo do component</strong>, ele vai para um hook. O <code>usePaymentMethods</code> cuida do fetch e da conversão, e o component só recebe o array que o hook entrega e desenha.</p>
<p>A segunda é o JSX que desenha a lista. <strong>Quando aparece um bloco que dá para desenhar recebendo só props</strong>, ele vira um component. <code>PaymentMethods</code> é um pure component que recebe apenas um array de meios de pagamento, então é fácil de testar e reutilizar. Mesmo que depois ganhe um callback <code>onSelect</code> para avisar da seleção, continua pure, porque não toca em state externo.</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"> usePaymentMethods</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">paymentMethods</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">setPaymentMethods</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">LocalPaymentMethod</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">[]>([]);</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 style="--shiki-dark:#6A737D;--shiki-light:#6A737D">/* 위의 fetch와 변환 */</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"> { paymentMethods };</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:#B392F0;--shiki-light:#6F42C1"> Payment</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">amount</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">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:#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">paymentMethods</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"> usePaymentMethods</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:#79B8FF;--shiki-light:#005CC5">PaymentMethods</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> paymentMethods</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{paymentMethods} /></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:#E1E4E8;--shiki-light:#24292E">>${amount}&#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></code></pre></figure>
<p>O autor chama as duas operações de <a href="https://refactoring.com/catalog/extractFunction.html" target="_blank" rel="noopener noreferrer">Extract Function</a>, do catálogo de refatorações. É que em React o component também é uma função. O ponto importante é que nenhum conceito novo foi introduzido: apenas uma função foi dividida em duas.</p>
<p>Parece que acabou, mas ainda restam dois lugares. Dentro da view há a decisão "se for dinheiro, vem selecionado por padrão", e dentro do hook há uma função anônima que transforma a resposta no formato da tela. O autor chama esse vazamento de regras de negócio para a view e o hook de logic leak.</p>
<h2 id="regras-reunidas-em-um-domain-object"><a class="anchor" href="#regras-reunidas-em-um-domain-object">Regras reunidas em um domain object</a></h2>
<p>O terceiro sinal é <strong>quando as decisões sobre os mesmos dados estão espalhadas entre a view e o hook</strong>. As decisões e conversões que vazaram são reunidas em uma única class <code>PaymentMethod</code>. É colocar a regra ao lado dos dados, como tratei em <a href="/260418" target="_blank" rel="noopener noreferrer">modelo de domínio</a> aqui no blog.</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:#F97583;--shiki-light:#D73A49">class</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> PaymentMethod</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:#F97583;--shiki-light:#D73A49">private</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209"> remote</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> RemotePaymentMethod</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) {}</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  get</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> provider</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">() { </span><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">.remote.name; }</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  get</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> label</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">() { </span><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">.provider </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">===</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> "cash"</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> ?</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> "Pay in cash"</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> :</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> `Pay with ${</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">this</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">.</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">provider</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">  get</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> isDefaultMethod</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">() { </span><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">.provider </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">===</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> "cash"</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"> convertPaymentMethods</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">methods</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> RemotePaymentMethod</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">  methods.</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">length</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> ===</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 0</span></span>
<span data-line=""><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">    :</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">methods.</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">m</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"> PaymentMethod</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(m)), </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">new</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> PaymentMethod</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({ name: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"cash"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> })];</span></span></code></pre></figure>
<p>O radio da view agora pergunta ao objeto com <code>defaultChecked={method.isDefaultMethod}</code>. O padrão de pagamento em dinheiro também é representado por uma instância da mesma class. Como o que veio do servidor e o que o app acrescentou passam a ter o mesmo formato, a view não precisa distinguir os dois.</p>
<p><picture><source type="image/webp" srcset="/content/261001/7.720w.webp 720w, /content/261001/7.1440w.webp 1440w, /content/261001/7.1600w.webp 1600w" sizes="(max-width: 752px) calc(100vw - 32px), 720px"><img src="/content/261001/7.png" alt="Estrutura em que Payment chama usePaymentMethods, o hook cria PaymentMethod e PaymentMethods só desenha" style="max-width:min(720px, 100%);height:auto;" width="1600" height="662" loading="lazy" fetchpriority="low" decoding="async"></picture></p>
<p>O autor aponta como vantagem dessa estrutura que os testes ficam mais fáceis. O original para nessa frase, mas é mais rápido ver no código o quanto ficam mais fáceis. Abaixo estão testes que eu escrevi. Não há React, nem renderer, nem mock de fetch.</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:#B392F0;--shiki-light:#6F42C1">test</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 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"> methods</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> convertPaymentMethods</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">([{ name: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"apple"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> }, { name: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"google"</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">  expect</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(methods.</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">m</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"> m.label)).</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">toEqual</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">([</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"Pay with apple"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"Pay with google"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"Pay in cash"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">]);</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  expect</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(methods.</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">m</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"> m.isDefaultMethod).</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">m</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"> m.provider)).</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">toEqual</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">([</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"cash"</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">test</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 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">  expect</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">convertPaymentMethods</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">([])).</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">toEqual</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>Para verificar a mesma regra no código inicial, era preciso fazer render do component, interceptar o fetch e esperar os radios serem desenhados. No momento em que a regra sai para um objeto, o teste vira uma linha de chamada de função.</p>
<p>Outra vantagem é que, quando chega um requisito novo, já está definido para onde ele vai. A estrutura de arquivos neste ponto é esta.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="text" data-theme="github-dark github-light"><code data-language="text" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span>src</span></span>
<span data-line=""><span>├── components</span></span>
<span data-line=""><span>│   ├── Payment.tsx</span></span>
<span data-line=""><span>│   └── PaymentMethods.tsx</span></span>
<span data-line=""><span>├── hooks</span></span>
<span data-line=""><span>│   └── usePaymentMethods.ts</span></span>
<span data-line=""><span>└── models</span></span>
<span data-line=""><span>    └── PaymentMethod.ts</span></span></code></pre></figure>
<p>Só dá para saber se a estrutura aguenta de verdade quando chega um requisito novo. Aqui o autor acrescenta mais uma funcionalidade.</p>
<h2 id="a-funcionalidade-de-doação-e-o-hook-como-state-machine"><a class="anchor" href="#a-funcionalidade-de-doação-e-o-hook-como-state-machine">A funcionalidade de doação e o hook como state machine</a></h2>
<p>O requisito novo é arredondar o valor do pedido para cima e permitir doar a diferença a uma instituição de caridade. Num pedido de 19,80 dólares, o app pergunta se o cliente quer doar 0,20 dólar e, se ele concordar, o botão mostra 20 dólares.</p>
<p><picture><source type="image/webp" srcset="/content/261001/8.720w.webp 720w, /content/261001/8.1440w.webp 1440w, /content/261001/8.1600w.webp 1600w" sizes="(max-width: 632px) calc(100vw - 32px), 600px"><img src="/content/261001/8.png" alt="Tela de pagamento com uma checkbox de doação de 0,2 dólar acrescentada abaixo dos meios de pagamento" style="max-width:min(600px, 100%);height:auto;" width="1600" height="1130" loading="lazy" fetchpriority="low" decoding="async"></picture></p>
<p>De propósito, o autor primeiro coloca o state de concordância e o cálculo dentro do component de pagamento. Com o markup da checkbox, a ramificação do texto e o cálculo do total entrando de uma vez, o component volta a ficar pesado. A ordem de limpeza é a mesma de antes. State e cálculo vão para o hook <code>useRoundUp</code>, a montagem do texto vai para uma função helper e o markup da checkbox vai para o component <code>DonationCheckbox</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:#F97583;--shiki-light:#D73A49">export</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> const</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> useRoundUp</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">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:#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">agreeToDonate</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">setAgreeToDonate</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">false</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">total</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">tip</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"> useMemo</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">    total: agreeToDonate </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">floor</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(amount </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:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> amount,</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    tip: </span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">parseFloat</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">(amount </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:#F97583;--shiki-light:#D73A49">-</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> amount).</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">toPrecision</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</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">  }), [amount, agreeToDonate]);</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"> updateAgreeToDonate</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"> setAgreeToDonate</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:#F97583;--shiki-light:#D73A49"> !</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">v);</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { total, tip, agreeToDonate, updateAgreeToDonate };</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">};</span></span></code></pre></figure>
<p>O <code>toPrecision(10)</code> é um trecho cujo motivo o original não explica. Quando rodei no Node 24, <code>20 - 19.8</code> deu <code>0.1999999999999993</code>. É um mecanismo para eliminar o erro de ponto flutuante.</p>
<p>O autor vê esse tipo de hook como "a state machine por trás da view". Quando chega um evento da UI, ele cria um state novo, e o state novo dispara outro render. Os hooks foram criados originalmente para vários components compartilharem logic, mas a posição do autor é que, mesmo com um único lugar usando, vale extrair, porque deixa o component focado no render. Acrescento uma coisa: como aponta <a href="https://react.dev/learn/reusing-logic-with-custom-hooks" target="_blank" rel="noopener noreferrer">Reusing Logic with Custom Hooks</a>, da documentação oficial do React, o que um custom hook compartilha é a logic que lida com o state, não o state em si. Essa state machine nasce separadamente para cada component que chama o hook.</p>
<p>Terminada a limpeza, o component de pagamento chama dois hooks e enfileira dois subcomponents e um botão. Ganhou uma funcionalidade, e o papel do component de pagamento continua o mesmo.</p>
<p><picture><source type="image/webp" srcset="/content/261001/9.720w.webp 720w, /content/261001/9.1440w.webp 1440w, /content/261001/9.1600w.webp 1600w" sizes="(max-width: 752px) calc(100vw - 32px), 720px"><img src="/content/261001/9.png" alt="Estrutura com dois hooks, useRoundUp e usePaymentMethods, e dois subcomponents, PaymentMethods e DonationCheckbox, abaixo de Payment" style="max-width:min(720px, 100%);height:auto;" width="1600" height="775" loading="lazy" fetchpriority="low" decoding="async"></picture></p>
<h2 id="regras-por-país-e-shotgun-surgery"><a class="anchor" href="#regras-por-país-e-shotgun-surgery">Regras por país e shotgun surgery</a></h2>
<p>O requisito seguinte é usar uma unidade de arredondamento diferente por país. No Japão, de 100 em 100 ienes. Na Dinamarca, de 10 em 10 coroas. O autor, dizendo que "parece uma correção fácil", mostra de propósito primeiro o caminho de receber o código do país como prop e enfiar ramificaçõ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">// useRoundUp 안</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">total</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: agreeToDonate</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  ?</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> countryCode </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">===</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> "JP"</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">floor</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(amount </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">/</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 100</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:#F97583;--shiki-light:#D73A49">*</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 100</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">floor</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(amount </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:#F97583;--shiki-light:#D73A49">  :</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> amount,</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 체크박스 문구 helper 안</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> currencySign</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> countryCode </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">===</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> "JP"</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> ?</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> "¥"</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">// 버튼 JSX 안</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:#E1E4E8;--shiki-light:#24292E">>{countryCode </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">===</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> "JP"</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> ?</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> "¥"</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">}{total}&#x3C;/</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">button</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span></code></pre></figure>
<p>A mesma decisão <code>countryCode === "JP"</code> entrou em três lugares: no hook, no helper e no botão. Acrescentando a Dinamarca, é preciso mexer de novo nos três, e quando o operador ternário não dá mais conta, surge uma tabela que busca o símbolo da moeda pelo código do país. Essa tabela, no fim, também acaba sendo consultada separadamente em vários lugares. Esse cheiro, em que uma única mudança obriga a mexer em vários módulos ao mesmo tempo, se chama shotgun surgery.</p>
<p><picture><source type="image/webp" srcset="/content/261001/10.720w.webp 720w, /content/261001/10.1440w.webp 1440w, /content/261001/10.1600w.webp 1600w" sizes="(max-width: 752px) calc(100vw - 32px), 720px"><img src="/content/261001/10.png" alt="Figura com barras coloridas, que representam as regras por país, espalhadas por vários components, hooks e domain objects" style="max-width:min(720px, 100%);height:auto;" width="1600" height="902" loading="lazy" fetchpriority="low" decoding="async"></picture></p>
<p>O que diferencia este estágio dos três anteriores é que o sinal não vem do código, e sim de um <strong>pedido de mudança</strong>. Olhando só o código atual, três ramificações não são difíceis de ler. O problema só aparece quando você conta quantos lugares precisa alterar para acrescentar mais um país. Então, para onde levar as ramificações espalhadas?</p>
<h2 id="diferenças-por-país-reunidas-em-um-strategy"><a class="anchor" href="#diferenças-por-país-reunidas-em-um-strategy">Diferenças por país reunidas em um Strategy</a></h2>
<p>A resposta do autor é transformar a própria diferença entre países em um objeto. Primeiro, com <a href="https://refactoring.com/catalog/extractClass.html" target="_blank" rel="noopener noreferrer">Extract Class</a> e <a href="https://refactoring.com/catalog/replaceConditionalWithPolymorphism.html" target="_blank" rel="noopener noreferrer">Replace Conditional with Polymorphism</a>, ele cria uma interface e uma implementação por país.</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:#F97583;--shiki-light:#D73A49">interface</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> PaymentStrategy</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  getRoundUpAmount</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"> number</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  getTip</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"> 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">class</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> PaymentStrategyAU</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> implements</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> PaymentStrategy</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { </span><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">/* 1달러 단위 */</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> }</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">class</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> PaymentStrategyJP</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> implements</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> PaymentStrategy</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { </span><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">/* 100엔 단위 */</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> }</span></span></code></pre></figure>
<p>Depois de criadas, o que muda de uma implementação para outra são só duas coisas, o símbolo da moeda e a função de arredondamento, e o resto, como <code>getTip</code>, é idêntico. Por isso o autor junta as implementações em uma única class com <a href="https://refactoring.com/catalog/inlineClass.html" target="_blank" rel="noopener noreferrer">Inline Class</a> e recebe só as partes diferentes pelo construtor. Dá para receber o algoritmo de arredondamento como função, sem criar uma subclass por país, porque em JavaScript funções são valores. Abaixo está o resultado, que eu resumi.</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:#F97583;--shiki-light:#D73A49">class</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> CountryPayment</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:#F97583;--shiki-light:#D73A49">readonly</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209"> currencySign</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">private</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> roundUp</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">n</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:#B392F0;--shiki-light:#6F42C1">  getRoundUpAmount</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">n</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">return</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> this</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">roundUp</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(n); }</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  getTip</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">n</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">return</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> parseFloat</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">.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">getRoundUpAmount</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(n) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">-</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> n).</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">toPrecision</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</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>
<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"> japan</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"> CountryPayment</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 style="--shiki-dark:#FFAB70;--shiki-light:#E36209">n</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"> Math.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">floor</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(n </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">/</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 100</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:#F97583;--shiki-light:#D73A49">*</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 100</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">japan.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">getRoundUpAmount</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">3312</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">); </span><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 3400</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">japan.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">getTip</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">3312</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">);           </span><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 88</span></span></code></pre></figure>
<p>Hooks e components agora não sabem nada de país. Eles recebem esse único objeto e só usam <code>strategy.getRoundUpAmount(amount)</code> e <code>strategy.currencySign</code>. Para acrescentar um país, basta criar mais um objeto, e nenhum arquivo precisa ser alterado.</p>
<p><picture><source type="image/webp" srcset="/content/261001/11.720w.webp 720w, /content/261001/11.1440w.webp 1440w, /content/261001/11.1600w.webp 1600w" sizes="(max-width: 752px) calc(100vw - 32px), 720px"><img src="/content/261001/11.png" alt="Estrutura em que o component olha apenas para um PaymentStrategy, com três implementações por país reunidas atrás dele" style="max-width:min(720px, 100%);height:auto;" width="1600" height="847" loading="lazy" fetchpriority="low" decoding="async"></picture></p>
<p>Passar por uma interface e depois juntar tudo de novo pode parecer um desvio desnecessário. Para mim, este é o trecho mais instrutivo do original. O polimorfismo não era o objetivo, e sim uma ferramenta para expor semelhanças e diferenças. Assim que ficou claro que a diferença se reduzia a uma função, a hierarquia de classes foi removida. Mais difícil do que introduzir um padrão é decidir quando removê-lo.</p>
<h2 id="tirando-o-fetch-do-hook"><a class="anchor" href="#tirando-o-fetch-do-hook">Tirando o fetch do hook</a></h2>
<p>A última coisa a extrair é o fetch e a conversão dentro de <code>usePaymentMethods</code>. Hook é um conceito do React, então fica do lado da view. Colocando error handling e retry, o hook incha rápido, e se você migrar para outra view library, o hook não serve. Uma função comum, por outro lado, funciona em qualquer lugar.</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:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> fetchPaymentMethods</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">  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">`${</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">API</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">}/payment-methods?countryCode=AU`</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"> convertPaymentMethods</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</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:#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"> usePaymentMethods</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">paymentMethods</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">setPaymentMethods</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">PaymentMethod</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">[]>([]);</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:#B392F0;--shiki-light:#6F42C1">    fetchPaymentMethods</span><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">(setPaymentMethods);</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"> { paymentMethods };</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">};</span></span></code></pre></figure>
<p>O autor escreve que essa função funciona como uma Anti-Corruption Layer ou um Gateway. Ou seja, mesmo que a estrutura da resposta do servidor mude, o lugar a corrigir fica restrito a essa função. Só que os dois nomes não são a mesma coisa. O <a href="https://martinfowler.com/articles/gateway-pattern.html" target="_blank" rel="noopener noreferrer">Gateway</a> de Fowler é um objeto que encapsula num só lugar o acesso a um sistema externo, enquanto Anti-Corruption Layer é um termo do DDD de Eric Evans: uma fronteira que traduz um modelo externo, com outro sistema de significados, para que ele não contamine o seu modelo. A resposta do servidor neste exemplo é só <code>{ name }</code>, então quase não há diferença de significado a traduzir, e por isso eu acho que Gateway é o nome mais preciso. Quando o servidor é um legado de outro time e o próprio significado de meio de pagamento é diferente, esse mesmo lugar vira uma Anti-Corruption Layer.</p>
<p>A estrutura final é esta. O render está no component, o state no hook, as regras no domain object e a requisição de rede em uma função.</p>
<p><picture><source type="image/webp" srcset="/content/261001/12.720w.webp 720w, /content/261001/12.1440w.webp 1440w, /content/261001/12.1600w.webp 1600w" sizes="(max-width: 752px) calc(100vw - 32px), 720px"><img src="/content/261001/12.png" alt="Estrutura final com Payment e seus subcomponents, os hooks useRoundUp e usePaymentMethods, os domain objects PaymentStrategy e PaymentMethod e o Fetcher, cada um em seu compartimento" style="max-width:min(720px, 100%);height:auto;" width="1600" height="820" loading="lazy" fetchpriority="low" decoding="async"></picture></p>
<p>O autor aponta cinco vantagens resultantes dessa divisão. Fica mais fácil achar onde está um defeito. Fica mais fácil reutilizar e combinar código. Fica mais fácil de ler. Acrescentar funcionalidades não abala o todo. E, como a domain logic não conhece a view, dá para trocar só a view layer. Sobre esta última, o próprio autor faz a ressalva de que, na maioria dos projetos, isso é muito raro. Também vale lembrar que nenhuma das cinco vem acompanhada de medições.</p>
<p>Até aqui é o enredo do original. Só que, rodando esse código de verdade, aparece o que não se via no texto.</p>
<h2 id="o-que-aparece-quando-você-roda-o-exemplo"><a class="anchor" href="#o-que-aparece-quando-você-roda-o-exemplo">O que aparece quando você roda o exemplo</a></h2>
<p>Baixei o repositório de exemplo do autor (16 commits, último commit <code>de186c5</code>) e rodei <code>tsc --noEmit</code> e os 7 testes. Os dois passaram, e o helper cuja definição faltava no original também estava no <code>src/utils.ts</code> do repositório. Mas olhando a estrutura, havia três coisas que é preciso saber antes de copiá-la.</p>
<h3 id="um-strategy-criado-de-novo-a-cada-render"><a class="anchor" href="#um-strategy-criado-de-novo-a-cada-render">Um Strategy criado de novo a cada render</a></h3>
<p>O component de pagamento recebe o Strategy como prop e, se ninguém passa, cria o valor padrão com <code>new</code>. E o <code>useRoundUp</code> coloca esse Strategy na dependency do <code>useMemo</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">export</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> const</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> Payment</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">  amount,</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  strategy </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"> PaymentStrategy</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">, roundUpToNearestInteger),</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 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:#FFAB70;--shiki-light:#E36209">strategy</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">?:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> PaymentStrategy</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">total</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">tip</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"> useRoundUp</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(amount, strategy); </span><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 안에서 [agreeToDonate, amount, strategy]</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>Os default parameters do JavaScript <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Default_parameters" target="_blank" rel="noopener noreferrer">são avaliados a cada chamada</a>. E o <code>useMemo</code> compara as dependencies com <code>Object.is</code>. Quando os dois se encontram, nasce uma instância nova a cada render e o memo recalcula toda vez. Fazendo render da mesma estrutura quatro vezes no React 18, o cálculo rodou 4 vezes usando o valor padrão e 1 vez passando a mesma instância. O <code>App.tsx</code> do repositório não passa Strategy (<code>&#x3C;Payment amount={19.9} /></code>), então o caminho de execução do app é exatamente o do valor padrão.</p>
<p>O cálculo deste exemplo são algumas somas, então não há custo. O problema é a estrutura. Nem o texto nem o repositório definem quem cria o Strategy e onde, e só os testes criam instâncias do Japão e da Dinamarca. No meu lugar, eu deixaria as instâncias como constantes no nível do module e as passaria buscando pelo código do país.</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:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> strategies</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">  AU: </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">new</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> CountryPayment</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 style="--shiki-dark:#FFAB70;--shiki-light:#E36209">n</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"> Math.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">floor</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(n </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">  JP: </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">new</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> CountryPayment</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 style="--shiki-dark:#FFAB70;--shiki-light:#E36209">n</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"> Math.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">floor</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(n </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">/</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 100</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:#F97583;--shiki-light:#D73A49">*</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 100</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">&#x3C;</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">Payment amount</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{amount} strategy</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{strategies[countryCode]} /></span></span></code></pre></figure>
<h3 id="o-fetch-do-repositório-não-tem-dependency-array"><a class="anchor" href="#o-fetch-do-repositório-não-tem-dependency-array">O fetch do repositório não tem dependency array</a></h3>
<p>O <code>useEffect</code> do código no original tem <code>[]</code>, mas o <code>usePaymentMethods</code> do repositório não tem dependency array do primeiro ao último commit. Um effect sem array roda de novo a cada render. Quando a resposta chega, o state é trocado por um array novo, isso dispara um render e o effect faz fetch de novo. Como <code>convertPaymentMethods</code> devolve um array novo toda vez, isso não para.</p>
<p>Trocando o fetch por um mock que responde na hora e contando as chamadas por 300ms no Jest, medi quatro vezes e obtive 152, 225, 231 e 233 chamadas. Com <code>[]</code>, é 1. Os testes do repositório só verificam se o texto aparece na tela, então não pegam essa repetição e passam todos. Quem digitou o código acompanhando o original não tem esse problema. Ele só aparece quando você baixa e usa o repositório como está.</p>
<h3 id="a-race-que-surge-quando-o-código-do-país-vira-prop"><a class="anchor" href="#a-race-que-surge-quando-o-código-do-país-vira-prop">A race que surge quando o código do país vira prop</a></h3>
<p>O fetch effect do código original também não tem cleanup. <a href="https://react.dev/learn/synchronizing-with-effects" target="_blank" rel="noopener noreferrer">Synchronizing with Effects</a>, da documentação oficial do React, diz que, ao fazer fetch dentro de um effect, uma resposta anterior que chega atrasada pode sobrescrever a mais recente, então é preciso descartar a resposta anterior com uma flag <code>ignore</code>. O original faz fetch só uma vez, no mount, então por enquanto não há problema. Mas no momento em que o <code>countryCode=AU</code> da URL passa a vir de uma prop, surge uma dependency e a race se abre. Ao copiar essa estrutura, eu escreveria assim desde o começo.</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:#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"> ignore </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">  fetchPaymentMethods</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(countryCode).</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">methods</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">ignore) </span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">setPaymentMethods</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(methods);</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"> () </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { ignore </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">}, [countryCode]);</span></span></code></pre></figure>
<p>Mais uma coisa: o arredondamento do original é <code>Math.floor(amount + 1)</code>, então, se o valor já é inteiro, ele sobe mais 1. Um pedido de 20 dólares vira 21. O original só dá como exemplos 19,80 dólares e 3312 ienes, então não dá para saber se isso é intencional. Se for levar isso para um serviço real, a primeira coisa a decidir é em que isso difere de <code>Math.ceil</code>.</p>
<h2 id="quando-usar-essa-estrutura"><a class="anchor" href="#quando-usar-essa-estrutura">Quando usar essa estrutura</a></h2>
<p>O próprio autor diz que dividir não é uma regra fixa. Components pequenos e coesos são mais fáceis de entender quando ficam em um único arquivo, e o que se deve evitar é um arquivo crescer a ponto de ficar incompreensível. Ele afirma que, num app de Todo ou num app de um único formulário, tudo bem colocar tudo no component, e que o exemplo foi complicado de propósito para mostrar muitos padrões. Outros materiais fazem ressalvas na mesma direção.</p>
<p>No mesmo texto sobre layering, Fowler diz para usar essa separação apenas em unidades relativamente pequenas. Ter view, model e data como pastas de nível superior tudo bem em sistemas pequenos, mas, se alguma delas crescer demais, o nível superior deve ser dividido por domínio e, dentro dele, por layer. Levar as pastas <code>components/</code>, <code>hooks/</code> e <code>models/</code> vistas acima como estão para a estrutura de nível superior de um app grande resulta justamente no formato que Fowler manda evitar.</p>
<p>Dan Abramov tem uma trajetória parecida. Em 2015, ele propôs dividir em <a href="https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0" target="_blank" rel="noopener noreferrer">presentational e container components</a> e, em 2019, colocou no topo do texto uma atualização dizendo que não recomenda mais dividir assim. O motivo era que os Hooks fazem a mesma coisa sem uma divisão arbitrária, e ele acrescentou que tinha visto essa divisão ser imposta de forma dogmática, sem necessidade, vezes demais. A extração de hooks do original se aproxima do jeito que essa atualização descreve. Não existe um component separado chamado container: o <code>Payment</code>, que chama o hook, acumula esse papel.</p>
<p>Sobre fetch, a documentação oficial do React vai um passo além. <a href="https://react.dev/learn/you-might-not-need-an-effect" target="_blank" rel="noopener noreferrer">You Might Not Need an Effect</a> diz que extrair a fetch logic para um custom hook facilita migrar depois para uma estratégia melhor, mas recomenda em primeiro lugar o fetching embutido do framework ou um client cache como o TanStack Query. Se você extrai o fetch para uma função comum, como no último estágio do original, dá para passar essa função como está para o <code>queryFn</code> do TanStack Query, e a race e as chamadas repetidas da seção anterior ficam por conta da library. Para mim, esse é o ganho mais prático, que o original menciona em uma única frase. Como amarrar chaves e funções eu tratei à parte em <a href="/260104" target="_blank" rel="noopener noreferrer">queryKey</a>.</p>
<p>Resumindo, minha avaliação é esta. A ordem do original (hook, pure component, domain object, Strategy, network client) vale ser seguida como está, porque em cada estágio o incômodo daquele estágio vira o motivo do seguinte. Mas não é preciso ir até o fim: basta ver de qual estágio é o incômodo que você sente agora e parar ali. Se há muito código sem relação com render, vá até o hook. Se a mesma decisão está espalhada, até o domain object. Se uma única mudança faz você mexer em vários arquivos, até o Strategy. E, se for usar Strategy, defina primeiro onde as instâncias são criadas. Se for deixar o fetch em um effect, escreva antes de tudo o cleanup e as dependencies. Essas duas coisas que o original omitiu foram os primeiros lugares a quebrar quando o código foi copiado de verdade.</p>
<p>Não existe resposta certa, mas espero que você, que está lendo, pare um momento para ver de qual estágio é o incômodo que o seu código está sentindo agora.</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:#3b82f6"></span><a href="https://learn.microsoft.com/en-us/azure/architecture/patterns/anti-corruption-layer" target="_blank" rel="noopener noreferrer">Microsoft Azure Architecture Center, padrão Anti-corruption Layer</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#f59e0b"></span><a href="https://martinfowler.com/articles/headless-component.html" target="_blank" rel="noopener noreferrer">Juntao Qiu, Headless Component</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#f59e0b"></span><a href="https://martinfowler.com/articles/data-fetch-spa.html" target="_blank" rel="noopener noreferrer">Juntao Qiu, Data Fetching Patterns in Single-Page Applications</a></div>
</div>
</div></details>]]></content:encoded>
            <author>jihoon7705@gmail.com (이지훈)</author>
            <category>frontend</category>
            <category>React</category>
            <category>arquitetura</category>
        </item>
        <item>
            <title><![CDATA[Modelos de decisão, Jev e Kev]]></title>
            <link>https://hooninedev.com/pt-BR/260922</link>
            <guid isPermaLink="false">https://hooninedev.com/pt-BR/260922</guid>
            <pubDate>Tue, 22 Sep 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Neste post quero falar sobre o Jev, um modelo que a TypeSafe AI lançou na semana passada e que devolve probabilidades em vez de texto. É para desenvolvedores que querem aplicar um limiar à probabilida...]]></description>
            <content:encoded><![CDATA[<p>Neste post quero falar sobre o Jev, um modelo que a TypeSafe AI lançou na semana passada e que devolve probabilidades em vez de texto. É para desenvolvedores que querem aplicar um limiar à probabilidade devolvida por um modelo e tratar casos automaticamente, sem conferência humana. Ao final, você vai saber como o <code>confidence</code> do Jev é calculado, quanto a calibração se move conforme a distribuição e um procedimento para definir o limiar com os seus próprios dados.</p>
<p>O primeiro uso que me chamou atenção foi a automação de navegador. A Browserbase abriu um <a href="https://github.com/browserbase/stagehand/pull/2953" target="_blank" rel="noopener noreferrer">PR</a> que liga o Jev ao <code>act()</code> do Stagehand. A estrutura era mandar a <span class="glossary-term-source" data-glossary-key="accessibility-tree">árvore de acessibilidade</span> da página como <code>state</code> e perguntar, com alternativas fechadas, "qual é o próximo elemento a clicar". Em 40 tarefas, a mediana do <code>act()</code> caiu de 1,97 segundo para 0,46, e de 147 ações só 4 voltaram para um LLM. O ponto onde um script de Playwright quebra assim que um selector muda foi preenchido por uma chamada que devolve uma única probabilidade. Enquanto escrevo, o PR ainda não foi mesclado.</p>
<p><picture><source type="image/webp" srcset="/content/260922/1.720w.webp 720w, /content/260922/1.1440w.webp 1440w, /content/260922/1.1560w.webp 1560w" sizes="(max-width: 752px) calc(100vw - 32px), 720px"><img src="/content/260922/1.png" alt="Como o act() do Stagehand usa o Jev. Uma lista de candidatos é montada a partir da instrução e da árvore de acessibilidade, o Jev é perguntado qual é e se nenhum se aplica, o elemento é clicado quando a probabilidade de «nenhum» é baixa e o passo cai para um LLM quando é alta. 143 de 147 ações terminaram com o Jev" style="max-width:min(720px, 100%);height:auto;" width="1560" height="920" loading="eager" fetchpriority="high" decoding="async"></picture></p>
<p>A linha que esse PR traçou é a pergunta deste texto. Ele aceita a resposta do Jev a partir de uma <a href="https://www.browserbase.com/blog/what-is-jev" target="_blank" rel="noopener noreferrer">probabilidade de 0,7</a> e, abaixo disso, repassa para um LLM. <strong>Dá para confiar na probabilidade que esse modelo devolve e traçar um limiar no código?</strong></p>
<p>O benchmark dos outros não basta para decidir, então quero verificar com os meus próprios dados. Este blog tem um gate que barra estrangeirismos transliterados. A regra manda escrever <code>피커</code> como <code>picker</code>, mas a regex só pega a metade certa, e palavras que aparecem dentro de um composto ou já estão consolidadas em coreano, como <code>멀티스레드</code> ou <code>콜 스택</code>, eu julgo à mão toda vez. Há só duas respostas, reverter ou manter, quem sabe decide em um segundo, e o passa ou falha precisa ser decidido dentro de um script de build sem ninguém conferindo. É o mesmo formato da decisão de selector do Stagehand.</p>
<p>Então a ordem é esta: o que o Jev devolve, de que treinamento sai essa probabilidade, que números aparecem quando entrego meu gate a ele e onde esses números podem ser usados.</p>
<h2 id="um-modelo-que-não-gera-texto"><a class="anchor" href="#um-modelo-que-não-gera-texto">Um modelo que não gera texto</a></h2>
<p>A data de lançamento é 15 de setembro de 2026. A empresa não chama isso de LLM, mas de System One Model, uma categoria nova. Neste texto, chamo isso de modelos de decisão. O fundador, Diogo Almeida, foi o quarto autor do <a href="https://arxiv.org/abs/2203.02155" target="_blank" rel="noopener noreferrer">artigo do InstructGPT</a> na OpenAI. Ele esteve no time que criou o método que se tornou o ancestral direto do ChatGPT e hoje está do lado que aponta os limites desse método.</p>
<p>O Jev não gera frases. Você define de antemão o formato da resposta, ele escolhe dentro dele e devolve a probabilidade junto. Só existem três tipos de pergunta.</p>
<table>
<thead>
<tr>
<th>Tipo</th>
<th>O que pergunta</th>
<th>O que devolve</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>choice</code></td>
<td>Qual destes é</td>
<td>a escolha, <code>probabilities</code>, <code>confidence</code></td>
</tr>
<tr>
<td><code>score</code></td>
<td>Em que nível está</td>
<td>a nota, <code>legend</code>, <code>probabilities</code>, <code>confidence</code></td>
</tr>
<tr>
<td><code>noul</code></td>
<td>É verdadeiro</td>
<td>uma única probabilidade entre 0 e 1</td>
</tr>
</tbody>
</table>
<p>O endpoint também é um só. Um <code>POST /v1/systemone</code> com <code>state</code> (o conteúdo a avaliar), <code>model</code> e <code>questions</code> (o mapa de perguntas) traz a resposta. Várias perguntas sobre o mesmo <code>state</code> cabem em uma requisição, e aumentar o número de perguntas quase não aumenta o tempo de resposta.</p>
<p>Por que é rápido se explica pela assimetria entre <span class="glossary-term-source" data-glossary-key="prefill">prefill</span> e decode que organizei em <a href="/260610" target="_blank" rel="noopener noreferrer">o princípio dos tokens</a>. O LLM é lento porque espreme a saída um token por vez, em sequência; o Jev não tem essa etapa de decode. Ele lê a entrada uma vez, em paralelo, e lê a probabilidade direto. Por isso não existe cobrança por token de saída: só a entrada, a $0.042 por milhão de tokens.</p>
<p>Há uma <a href="https://archerhume.com/posts/jevs-architecture-unmasked/" target="_blank" rel="noopener noreferrer">análise</a> de um desenvolvedor chamado Archer Hume que chamou a API cerca de dez mil vezes e reconstruiu o comportamento de fora. Com 360 tokens de entrada, mediana de 57.5ms; com 29,835 tokens, 218ms; com 1,500 perguntas, 610ms. A evidência mais direta ali é a observação de que uma resposta com 200 alternativas voltou na mesma velocidade que uma com 2. Ou seja, escrever a resposta não custa tempo.</p>
<h2 id="a-decisão-que-ninguém-vê"><a class="anchor" href="#a-decisão-que-ninguém-vê">A decisão que ninguém vê</a></h2>
<p>Velocidade e preço chamam atenção, mas a pergunta que Almeida lançou no texto de anúncio era outra: se os modelos são sobre-humanos em conversa há anos, para onde foi toda a automação?</p>
<p>A resposta dele é que são dois tipos de trabalho diferentes. Chatbots, copilots e agentes de código têm como objetivo satisfazer alguém que está olhando ao lado. A decisão que roda em silêncio no servidor, sem ninguém olhando, não. Se chamarmos a primeira de assistant e a segunda de automation, todos os modelos lançados até agora foram feitos para a primeira.</p>
<p>Fiz uma distinção parecida quando organizei o texto sobre <a href="/260622" target="_blank" rel="noopener noreferrer">design de harness</a>. Dentro de um sistema de agentes há incontáveis decisões que ninguém observa. Passo esta requisição para uma pessoa? Posso executar este comando? Hoje perguntamos tudo isso a um LLM caro. A outra metade que a regex do meu gate não cobre também é uma decisão desse tipo.</p>
<h2 id="o-lugar-que-o-rlhf-deixou"><a class="anchor" href="#o-lugar-que-o-rlhf-deixou">O lugar que o RLHF deixou</a></h2>
<p>Mas por que foi preciso um método de treino novo? Não dá para simplesmente pedir sim ou não a um modelo existente?</p>
<p>A pista está na <span class="glossary-term-source" data-glossary-key="calibration">calibração</span>. Se acurácia é quantos por cento você acerta, calibração é saber quantos por cento você vai acertar, e o indicador que mede esse descompasso é o <span class="glossary-term-source" data-glossary-key="ece">ECE</span> (expected calibration error). É a média da diferença entre a probabilidade declarada e a taxa real de acerto em cada faixa de probabilidade, ponderada pela fração de amostras, então 0 é o valor perfeito. Só que há um caso em que essa escala saiu do lugar depois do <span class="glossary-term-source" data-glossary-key="rlhf">RLHF</span> (reinforcement learning from human feedback), que otimiza a preferência humana. Na Figure 8 do <a href="https://arxiv.org/abs/2303.08774" target="_blank" rel="noopener noreferrer">relatório técnico do GPT-4</a>, o ECE do modelo pré-treinado era <strong>0.007</strong>, e o do modelo pós-treinado, <strong>0.074</strong>, mais de dez vezes pior. O relatório não aponta a causa, e eu também não encontrei uma fonte primária que a estabeleça. A definição e até onde a causa é conhecida estão explicadas à parte em <a href="/260917" target="_blank" rel="noopener noreferrer">Calibração e excesso de confiança em LLMs</a>.</p>
<p><strong>Só que o ECE sozinho não basta.</strong> Um preditor constante que estampa 0.6 em qualquer entrada também tem ECE 0 se a taxa real de acerto for 60%. Se a probabilidade é apenas honesta e não varia de caso para caso, não há onde traçar a linha. Por isso, além da calibração, é preciso olhar <strong>se a probabilidade de fato se separa</strong>, e a “fração que dá para tratar automaticamente dentro de um orçamento de erro”, que uso adiante, é o indicador que junta essas duas coisas num número só.</p>
<p>Do ponto de vista do software, o que importa é esta parte. Se a probabilidade é honesta e se separa, dá para traçar uma linha. Sai uma estrutura em que acima de 0.95 vai para o automático e abaixo disso vai para uma pessoa.</p>
<p>A TypeSafe diz ter aberto um terceiro caminho mirando esse lugar. O RLHF otimiza a preferência humana; o RLVR (reinforcement learning with verifiable rewards) otimiza respostas que a máquina consegue corrigir; e o RLCD, dizem, otimiza decisões calibradas.</p>
<p>Só que tudo o que foi divulgado sobre o RLCD são três linhas de contrato de saída. Até 23 de setembro de 2026, data em que escrevo este texto, não há artigo nem relatório técnico em lugar nenhum da documentação e do texto de anúncio da TypeSafe. Existe um <a href="https://arxiv.org/abs/2307.12950" target="_blank" rel="noopener noreferrer">artigo do ICLR 2024</a> que usa a mesma sigla, mas ali ela significa reinforcement learning from contrastive distillation, um método diferente. Cuidado para não misturar os dois na hora de pesquisar.</p>
<h2 id="o-que-o-confidence-é-de-fato"><a class="anchor" href="#o-que-o-confidence-é-de-fato">O que o confidence é de fato</a></h2>
<p>Então o número que o Jev devolve é honesto? Antes de responder, há uma coisa a ver primeiro. <strong>Não é um número só.</strong></p>
<p><picture><source type="image/webp" srcset="/content/260922/3.720w.webp 720w, /content/260922/3.1440w.webp 1440w, /content/260922/3.1560w.webp 1560w" sizes="(max-width: 752px) calc(100vw - 32px), 720px"><img src="/content/260922/3.png" alt="Na resposta do Jev, probabilities nasce do treino e confidence é essa distribuição dobrada por aritmética; a afirmação de calibração vale apenas para probabilities" style="max-width:min(720px, 100%);height:auto;" width="1560" height="840" loading="lazy" fetchpriority="low" decoding="async"></picture></p>
<p>As respostas de <code>Choice</code> e <code>Score</code> trazem <code>probabilities</code> e <code>confidence</code> juntos. O primeiro é a distribuição de probabilidade sobre todas as alternativas; o segundo é um único número entre 0 e 1. Na hora de pôr um limiar no código, a mão vai primeiro para o <code>confidence</code>.</p>
<p>Esse valor não foi feito pelo modelo. O arquivo <code>_utils/confidence_metrics.py</code> do <a href="https://github.com/typesafe-ai/system-one-adapter-python" target="_blank" rel="noopener noreferrer">system-one-adapter-python</a>, publicado pela organização TypeSafe, tem 32 linhas ao todo, e a parte de <code>Choice</code> é esta.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="python" data-theme="github-dark github-light"><code data-language="python" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">def</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> choice_confidence</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(probs: list[</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">float</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">]) -> </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">float</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">:</span></span>
<span data-line=""><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">    """Scale peak choice probability from uniform to certainty."""</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">    if</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> len</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(probs) </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:#F97583;--shiki-light:#D73A49">        return</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 1.0</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    normalized_probs </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> _normalize(probs)</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    uniform_probability </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 1.0</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> /</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> len</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(normalized_probs)</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:#79B8FF;--shiki-light:#005CC5">max</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(normalized_probs) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">-</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> uniform_probability) </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">1.0</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> -</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> uniform_probability)</span></span></code></pre></figure>
<p>Não há chamada de modelo nem parâmetro aprendido. Entra um vetor <code>probabilities</code> e sai um número real. Em fórmula, <code>c = (p_max − 1/K) / (1 − 1/K)</code>; com três alternativas e probabilidade máxima de 0.8, dá 0.7.</p>
<p>Esse repositório não é o servidor Jev de produção, e sim uma implementação alternativa que imita a mesma API com um LLM. Então não dá para cravar nada só com isso. Mas há outras duas evidências apontando para a mesma fórmula. A <a href="https://docs.typesafe.ai/confidence" target="_blank" rel="noopener noreferrer">página sobre confidence</a> da documentação oficial da TypeSafe descreve o valor como "a statistic computed from the probability distribution" e, no código de demonstração, traz <code>(3 × largest probability − 1) / 2</code> como aproximação para três alternativas. Com K igual a 3, é a mesma fórmula acima. E o <a href="https://github.com/SamuelSacco/jev-exploration" target="_blank" rel="noopener noreferrer">registro de auditoria de afirmações</a> anotou que, na API real, numa resposta em que só foram dadas duas alternativas, ambas erradas, quando o vencedor era 0.52 o <code>confidence</code> veio 0.04. Com K igual a 2, <code>(0.52 − 0.5) / (1 − 0.5) = 0.04</code>. Bate com a fórmula.</p>
<p>No mesmo repositório, <code>Score</code> usa uma fórmula completamente diferente: a distância absoluta média a partir do nível de moda, dividida pela mesma quantidade sob distribuição uniforme, e então <strong>subtraída de 1</strong>. A documentação não publica a fórmula do <code>Score</code>. Ou seja, sob o mesmo nome <code>confidence</code> existem duas fórmulas, e em tipos diferentes o mesmo número quer dizer coisas diferentes. <code>Noul</code> não ter <code>confidence</code> é pela mesma razão: não há distribuição para dobrar.</p>
<p><picture><source type="image/webp" srcset="/content/260922/4.720w.webp 720w, /content/260922/4.1440w.webp 1440w, /content/260922/4.1560w.webp 1560w" sizes="(max-width: 752px) calc(100vw - 32px), 720px"><img src="/content/260922/4.png" alt="Diagrama da escala de p_max esticada até a escala de confidence, levando 1/K a 0 e 1 a 1. Com três alternativas 0,80 vira 0,7, tudo abaixo de 1/3 é descartado e a ordem não muda" style="max-width:min(720px, 100%);height:auto;" width="1560" height="820" loading="lazy" fetchpriority="low" decoding="async"></picture></p>
<p>A documentação também não esconde isso. Chega a orientar que, se você quiser outro cálculo, ela entrega o <code>probabilities</code> inteiro para você fazer como preferir. <strong>É conteúdo que está escrito; o problema é quem usa sem ler.</strong></p>
<p>Mas a tabela comparativa do <a href="https://typesafe.ai/blog/introducing-system-one-models-and-jev" target="_blank" rel="noopener noreferrer">texto de anúncio</a> cola as duas coisas. Lá está escrito "Calibrated: higher confidence means higher accuracy". Quem lê entende que o <code>confidence</code> é o valor calibrado, mas o lado em que a afirmação de calibração de fato se apoia é o <code>probabilities</code>.</p>
<p>Quanto essa distinção abre em números, existe medição. Um desenvolvedor chamado <code>scienthoon</code> publicou uma <a href="https://github.com/scienthoon/jev-ood-calibration" target="_blank" rel="noopener noreferrer">medição independente de calibração</a> que calculou à parte o ECE obtido ao ler o <code>confidence</code> como “probabilidade de estar certo”. Deu 0.035 no OpenBookQA, 0.078 no HellaSwag e 0.18 no conjunto sintético. As probabilidades originais dos mesmos conjuntos foram, respectivamente, 0.024, 0.029 e 0.107. <strong>Nos três conjuntos, o lado do <code>confidence</code> é pior.</strong></p>
<p>Por que piora está na fórmula. Se K é fixo, <code>c</code> é uma transformação monotonicamente crescente de <code>p_max</code>, então a ordem é preservada. O poder discriminativo continua o mesmo; basta reescrever o limiar. <strong>O que quebra não é a informação, é a escala.</strong> Se K varia de pergunta para pergunta, surge mais um problema: o fator de esticamento também varia. O mesmo <code>p_max</code> de 0.8 vira 0.6 com duas alternativas e 0.75 com cinco. No momento em que você usa um único limiar numa carga de trabalho com número de alternativas misturado, isso vira problema.</p>
<h2 id="a-calibração-muda-conforme-a-distribuição"><a class="anchor" href="#a-calibração-muda-conforme-a-distribuição">A calibração muda conforme a distribuição</a></h2>
<p>Então dá para confiar no lado do <code>probabilities</code>?</p>
<p>Existe um registro público de chamadas reais ao Jev. O repositório do <a href="https://github.com/jaredpalmer/kev" target="_blank" rel="noopener noreferrer">Kev</a>, implementação open source de reprodução feita por Jared Palmer, tem o diretório <code>runs/jev-*</code>, e o <code>usage.json</code> registra <code>"model": "typesafe-ai/jev"</code>, atestando que foram chamadas reais passando pelo Vercel AI Gateway. Somando, nos seis diretórios <code>jev-*</code>, os arquivos <code>usage.json</code>, foram 5,364 chamadas entre 18 e 20 de setembro de 2026 e 2,418,260 tokens de entrada. Dos sete conjuntos abaixo, o <code>scienthoon</code> foi importado convertendo o arquivo de resultados daquele desenvolvedor, então não entra nessa soma.</p>
<p>Que a cobrança é de fato só pela entrada se confirma em outro lugar. O autor do benchmark de phishing anotou isso olhando o dashboard da TypeSafe. Dos 4.56 milhões de tokens de duas rodadas, 3.66 milhões foram de entrada e 0.9 milhão de saída, e a cobrança foi de $0.15, valor que bate com o cálculo feito só sobre a entrada.</p>
<p>Tirando do <code>report.json</code> do mesmo diretório os indicadores dos sete conjuntos de avaliação, fica assim. Abaixo, <strong>a certeza não é o campo <code>confidence</code>, e sim o valor máximo de <code>probabilities</code></strong>. O excesso de certeza é <code>평균 확신도 − 정확도</code>, isto é, a certeza média menos a acurácia, e são 5,743 casos no total. Os valores são os do bloco <code>clean</code>, que deixa fora da pontuação de acurácia os itens “desconhecidos”, cuja base de julgamento foi apagada.</p>
<table>
<thead>
<tr>
<th>Conjunto de avaliação</th>
<th>n</th>
<th>Acurácia</th>
<th>Certeza média</th>
<th>Excesso</th>
<th>ECE</th>
</tr>
</thead>
<tbody>
<tr>
<td>semif</td>
<td>144</td>
<td>0.965</td>
<td>0.965</td>
<td>-0.000</td>
<td>0.011</td>
</tr>
<tr>
<td>transfer-v9</td>
<td>1,046</td>
<td>0.854</td>
<td>0.880</td>
<td>+0.026</td>
<td>0.034</td>
</tr>
<tr>
<td>transfer-v4</td>
<td>656</td>
<td>0.857</td>
<td>0.903</td>
<td>+0.046</td>
<td>0.049</td>
</tr>
<tr>
<td>transfer-v2</td>
<td>560</td>
<td>0.855</td>
<td>0.900</td>
<td>+0.045</td>
<td>0.055</td>
</tr>
<tr>
<td>decision-v4</td>
<td>1,264</td>
<td>0.845</td>
<td>0.906</td>
<td>+0.061</td>
<td>0.067</td>
</tr>
<tr>
<td>decision-v2</td>
<td>1,200</td>
<td>0.833</td>
<td>0.904</td>
<td>+0.071</td>
<td>0.074</td>
</tr>
<tr>
<td>scienthoon</td>
<td>873</td>
<td>0.753</td>
<td>0.851</td>
<td>+0.099</td>
<td>0.105</td>
</tr>
</tbody>
</table>
<p>Duas coisas se leem aqui.</p>
<p>Primeira, <strong>o ECE varia quase dez vezes.</strong> De 0.011 a 0.105. Mesmo corretor, mesmo modelo. “Ser calibrado” não é uma propriedade do modelo, mas da relação entre o modelo e a distribuição. Que a calibração desmorone quando a distribuição muda já é um resultado conhecido. O que há de novo aqui é que <strong>o mesmo padrão aparece num modelo que ergueu a calibração como objetivo de treino</strong>.</p>
<p>Os 0.105 do conjunto com maior ECE são até maiores que os 0.074 do GPT-4 pós-RLHF que vimos antes. Mas esses dois não devem ser lidos como um ranking. As tarefas são diferentes, e o ECE é um estimador cujo valor se move conforme o número de faixas em que você divide e conforme a faixa em que as amostras se concentram, de modo que confrontar números vindos de corretores diferentes não constitui comparação.</p>
<p>Segunda, <strong>o ECE medido é explicado quase inteiramente pelo excesso de certeza.</strong> A diferença entre as duas colunas fica entre 0.002 e 0.011 nos sete conjuntos. Como o ECE é a média ponderada das defasagens por faixa, ele não pode ser menor que o excesso global; essa diferença ser quase 0 significa que <strong>o sinal do desvio de cada faixa está todo inclinado para o mesmo lado</strong>. Não é ruído que se alterna entre faixas e se cancela.</p>
<p>Por que é assim se vê lendo na vertical. A certeza média, tirando o <code>semif</code>, tem seis valores grudados entre 0.851 e 0.906. No mesmo intervalo, a acurácia se move duas vezes mais largo, de 0.753 a 0.857. <strong>A certeza quase não se move quando a distribuição muda; só a acurácia se move. A diferença que sobra é o próprio ECE.</strong></p>
<p><picture><source type="image/webp" srcset="/content/260922/5.720w.webp 720w, /content/260922/5.1440w.webp 1440w, /content/260922/5.1560w.webp 1560w" sizes="(max-width: 752px) calc(100vw - 32px), 720px"><img src="/content/260922/5.png" alt="Gráfico de sete conjuntos de avaliação com chamadas reais ao Jev: a confiança média se concentra entre 0,851 e 0,906 enquanto só a acurácia se move de 0,753 a 0,965, e essa diferença quase coincide com o ECE" style="max-width:min(720px, 100%);height:auto;" width="1560" height="920" loading="lazy" fetchpriority="low" decoding="async"></picture></p>
<p>Como essa propriedade aparece na prática, outro benchmark mostra. Numa <a href="https://github.com/anisselbd/jev-phishing-bench" target="_blank" rel="noopener noreferrer">medição</a> com 2,000 e-mails de phishing e o Claude Haiku 4.5 como grupo de controle, a acurácia do Jev foi 62.6% e a do Haiku, 81.3%. Mas o que mais chama atenção é o ECE. O ECE reportado pelo repositório é 0.154 para o Jev e 0.097 para o Haiku, e quando o registro de auditoria de afirmações remede o <code>P(phishing)</code> do Jev em 10 faixas de 0 a 1, dá 0.170. Lido de qualquer dos dois jeitos, <strong>o modelo que ergueu a calibração como objetivo de treino perdeu em calibração para um LLM feito com RLHF.</strong> O mesmo autor tirou 91.6% só com a regra da lista de hosts dos links.</p>
<p>Olhando o mesmo indicador em vários conjuntos, a amplitude fica ainda mais clara. A fração que dá para tratar automaticamente com <span class="glossary-term-source" data-glossary-key="error-budget">orçamento de erro</span> de 5% é 0.486 no conjunto <code>scienthoon</code>, 0.695 no <code>transfer-v9</code> e 1.000 no <code>semif</code>. O corretor do Kev anota que esse valor é o máximo obtido escolhendo o limiar dentro da amostra, e não uma garantia de erro depois do deploy. <strong>Nenhum desses deve ser citado como se fosse a especificação do modelo.</strong></p>
<h2 id="o-gate-de-detecção-de-transliteração"><a class="anchor" href="#o-gate-de-detecção-de-transliteração">O gate de detecção de transliteração</a></h2>
<p>Ler o benchmark dos outros é diferente de medir nos meus dados. Então passei de verdade pelo gate da abertura.</p>
<p>Eu ainda não tenho uma chave de API do Jev. Em vez disso, <strong>rodei localmente o Kev-9B</strong> mencionado antes. É uma implementação de reprodução que põe um <span class="glossary-term-source" data-glossary-key="lora">LoRA</span> rank-16 e uma pointer head sobre o Qwen3.5-9B-Base, sob Apache-2.0. Os números abaixo não são números do Jev. Os dados de treino do Kev são tarefas de decisão em inglês, e na medição do próprio autor o Kev-9B fica atrás do Jev até em tarefas em inglês. A taxa de automação com orçamento de erro de 5% vai de 0.45 a 0.57, contra 0.70 do Jev, e no MMLU-Pro é 0.52 contra 0.84. Além disso, a documentação da TypeSafe também declara, sobre o Jev, que <a href="https://docs.typesafe.ai/models" target="_blank" rel="noopener noreferrer">o inglês é a língua principal de treino e o CJK não está no mesmo nível</a>.</p>
<p>O conjunto de avaliação saiu de 18 posts em coreano deste repositório. O critério de verdade dos rótulos é <strong>qual grafia este repositório usa de forma consistente para aquela palavra</strong>. Ou seja, não é o consenso da comunidade de documentação técnica em coreano, e sim o costume deste blog; nas palavras em que os dois divergem, o modelo pode estar certo pelo critério da comunidade e errado por este.</p>
<ul>
<li><strong>Conjunto do gate, 102 frases</strong>: São palavras que o gate já conhece. Há 23 positivos, que são <strong>frases contrafactuais</strong> em que palavras escritas em inglês no texto original, como <code>calendar</code>, <code>picker</code> e <code>adapter</code>, foram revertidas para a transliteração coreana, e 79 negativos, que são frases em que de fato aparecem palavras marcadas como exceção pelo <code>write-post.md</code>, como <code>리렌더링</code> ou <code>콜 스택</code>. Aqui o gate de regex atual acerta 100% por definição.</li>
<li><strong>Conjunto pendente, 60 frases</strong>: <strong>São palavras que o gate nunca viu.</strong> É o que em machine learning se chama conjunto <span class="glossary-term-source" data-glossary-key="held-out-set">held-out</span>. Há 30 positivos, em que <code>loader</code>, <code>mutation</code> e <code>prefill</code>, que o texto só escreve em inglês, foram revertidos para transliteração, e 30 negativos com <code>리듀서</code>, <code>스냅샷</code> e <code>런타임</code>, que o texto só escreve em coreano. Aqui a regex não pega um único positivo.</li>
</ul>
<p><picture><source type="image/webp" srcset="/content/260922/6.720w.webp 720w, /content/260922/6.1440w.webp 1440w, /content/260922/6.1560w.webp 1560w" sizes="(max-width: 752px) calc(100vw - 32px), 720px"><img src="/content/260922/6.png" alt="Diagrama do conjunto gate de 102 frases e do conjunto reservado de 60 frases, sua composição de positivos e negativos, e como o gate de regex cobre todo o primeiro mas não pega nenhum positivo do segundo" style="max-width:min(720px, 100%);height:auto;" width="1560" height="860" loading="lazy" fetchpriority="low" decoding="async"></picture></p>
<p>Deixo claro desde já a assimetria de que os positivos são frases contrafactuais que eu mesmo construí e os negativos são frases reais. E <strong>a amostra efetiva não é o número de frases, e sim o número de palavras.</strong> Como o rótulo é definido por palavra, as frases que contêm a mesma palavra não são observações independentes. O conjunto do gate tem 24 tipos e o conjunto pendente, 13.</p>
<p>A tabela abaixo agrega apenas uma pergunta <code>noul</code>, a que pergunta “esta palavra deve ser revertida para o inglês?”. Coloquei quatro perguntas na mesma requisição, mas as outras três eram a forma negativa, uma variante em <code>Choice</code> e um julgamento sobre compostos cujos rótulos eu não consegui definir direito, então não as misturei aqui. A <strong>fração de automação com orçamento de 5%</strong> é a maior fração tal que, cortando de cima para baixo pela certeza, o erro da faixa acima do corte não passa de 5%.</p>
<table>
<thead>
<tr>
<th></th>
<th>Conjunto do gate</th>
<th>Conjunto pendente</th>
</tr>
</thead>
<tbody>
<tr>
<td>Frases / palavras</td>
<td>102 / 24 tipos</td>
<td>60 / 13 tipos</td>
</tr>
<tr>
<td>Gate de regex atual</td>
<td><strong>1.000</strong></td>
<td>0.500</td>
</tr>
<tr>
<td>Acurácia do Kev-9B</td>
<td>0.225</td>
<td>0.500</td>
</tr>
<tr>
<td>Acurácia por palavra</td>
<td>6/24</td>
<td>7/13</td>
</tr>
<tr>
<td>Baseline da classe majoritária</td>
<td>0.775</td>
<td>0.500</td>
</tr>
<tr>
<td>ECE</td>
<td>0.664</td>
<td>0.336</td>
</tr>
<tr>
<td>Certeza média</td>
<td>0.876</td>
<td>0.821</td>
</tr>
<tr>
<td>Fração com certeza 0.9 ou mais</td>
<td>0.490</td>
<td>0.317</td>
</tr>
<tr>
<td>Acerto real nessa faixa</td>
<td>0.240</td>
<td>0.526</td>
</tr>
<tr>
<td><strong>Automação com orçamento de 5%</strong></td>
<td><strong>0.010</strong></td>
<td><strong>0.000</strong></td>
</tr>
</tbody>
</table>
<p>O baseline da classe majoritária é a nota de quem não lê a frase e chuta sempre o rótulo mais frequente. O modelo está abaixo disso.</p>
<p>A última linha é a resposta deste experimento. <strong>No conjunto pendente, por mais alto que eu colocasse o limiar, não houve uma única decisão que desse para tratar automaticamente dentro de 5% de erro.</strong></p>
<p>Olhando a distribuição das predições, aparece o que aconteceu. O modelo respondeu <strong>“deve ser revertida” a todas as 162 frases.</strong> A todos os 37 tipos de palavra. Todos os positivos certos, todos os negativos errados. Por isso o 0.500 do conjunto pendente não é competência: é o número que sai porque o conjunto está balanceado.</p>
<p>E, ainda assim, a certeza média foi 0.876. No conjunto do gate, 49% passaram de 0.9 de certeza, e o acerto real dessa faixa é 0.240.</p>
<p>A identidade estrutural também não se sustentou. Somando as probabilidades de “deve ser revertida” e “deve ficar como está”, a média no conjunto do gate deu 1.655, e todas as 102 frases se afastaram de 1 por mais de 0.1.</p>
<p>É assim por design. As duas perguntas são avaliações separadas que não leem uma à outra, e a própria TypeSafe publicou na <a href="https://docs.typesafe.ai/model-jaggedness/jev-1.13" target="_blank" rel="noopener noreferrer">página sobre jaggedness</a> um exemplo em que <code>refund</code> 0.72 e <code>not_refund</code> 0.47 somam 1.19.</p>
<p>O ponto que pega na prática é este. <strong>Um limiar calibrado para <code>Noul</code> não deve ser transferido para <code>Choice</code>.</strong> As frases em que o julgamento é o mesmo mas a conclusão diverge foram de 14% a 17%.</p>
<p>Se fosse um modelo que diz sim a qualquer coisa, este resultado seria sem sentido. Comecei checando isso.</p>
<table>
<thead>
<tr>
<th>Estado / pergunta</th>
<th>O que perguntei</th>
<th>Resposta</th>
</tr>
</thead>
<tbody>
<tr>
<td>Coreano / coreano</td>
<td>Esta frase está escrita em inglês?</td>
<td>0.02</td>
</tr>
<tr>
<td>Coreano / coreano</td>
<td>Esta frase explica uma receita de cozinha?</td>
<td>0.04</td>
</tr>
<tr>
<td>Coreano / coreano</td>
<td>Esta frase está escrita em coreano?</td>
<td>0.98</td>
</tr>
<tr>
<td>Coreano / inglês</td>
<td>Is this sentence written in English?</td>
<td>0.01</td>
</tr>
<tr>
<td>Inglês / inglês</td>
<td>Is this sentence about software?</td>
<td>0.96</td>
</tr>
</tbody>
</table>
<p><strong>Ele lê coreano, diz “não” e separa 0.02 de 0.98.</strong> Não é um viés global de concordância. O que isso descarta vai só até aí. Continua de pé a possibilidade de que o enunciado da própria pergunta do gate seja ruim.</p>
<h2 id="o-resultado-de-pôr-as-regras-no-state"><a class="anchor" href="#o-resultado-de-pôr-as-regras-no-state">O resultado de pôr as regras no state</a></h2>
<p>E se eu der diretamente o conhecimento necessário para distinguir? O método de adaptação de domínio que a documentação da TypeSafe recomenda é esse: sem tocar nos pesos, você manda material de referência dentro do <code>state</code>.</p>
<p>Peguei o parágrafo de regras que já está escrito no <code>write-post.md</code>, coloquei junto no <code>state</code> e rodei de novo. A entrada subiu de 100 tokens por frase para 450. <strong>O conjunto do gate é <span class="glossary-term-source" data-glossary-key="data-leakage">vazamento</span></strong>, porque esse material de referência lista pelo nome as palavras do gate e as palavras de exceção. Então aquele lado é um grupo de controle para ver “ele ao menos lê o material de referência?”, e o teste de verdade é o conjunto pendente.</p>
<table>
<thead>
<tr>
<th></th>
<th>Gate (vazamento)</th>
<th>Gate +regras</th>
<th>Pendente</th>
<th>Pendente +regras</th>
</tr>
</thead>
<tbody>
<tr>
<td>Acurácia em <code>noul</code></td>
<td>0.225</td>
<td>0.676</td>
<td><strong>0.500</strong></td>
<td><strong>0.500</strong></td>
</tr>
<tr>
<td>Acurácia por palavra</td>
<td>6/24</td>
<td></td>
<td>7/13</td>
<td><strong>5/13</strong></td>
</tr>
<tr>
<td>Positivos corretos</td>
<td>23/23</td>
<td>1/23</td>
<td>30/30</td>
<td>8/30</td>
</tr>
<tr>
<td>Negativos corretos</td>
<td>0/79</td>
<td>68/79</td>
<td>0/30</td>
<td>22/30</td>
</tr>
<tr>
<td>Automação com orçamento de 5%</td>
<td>0.010</td>
<td>0.314</td>
<td>0.000</td>
<td>0.050</td>
</tr>
<tr>
<td>Latência mediana</td>
<td>2,924ms</td>
<td>6,903ms</td>
<td>2,908ms</td>
<td>6,918ms</td>
</tr>
</tbody>
</table>
<p>A latência é inferência local rodada num M2 Max, então não está no mesmo eixo da latência de API do Jev vista antes. O que interessa aqui não é o valor absoluto, e sim o fator de 2.4 vezes ao incluir o material de referência.</p>
<p><strong>No conjunto pendente, que é balanceado, a acurácia por frase foi de 0.500 para 0.500: não se moveu em nada.</strong> Por palavra, chegou a cair de 7/13 para 5/13.</p>
<p><picture><source type="image/webp" srcset="/content/260922/7.720w.webp 720w, /content/260922/7.1440w.webp 1440w, /content/260922/7.1560w.webp 1560w" sizes="(max-width: 752px) calc(100vw - 32px), 720px"><img src="/content/260922/7.png" alt="Antes e depois de colocar o parágrafo de regras no state, nas 60 frases fora do gate. Antes acertou as 30 frases a reverter e errou as 30 a manter; depois acertou 8 e 22. O total de acertos é 30 nas duas vezes" style="max-width:min(720px, 100%);height:auto;" width="1560" height="820" loading="lazy" fetchpriority="low" decoding="async"></picture></p>
<p>O salto de 0.225 para 0.676 no conjunto do gate não é ganho de competência. Antes, ele dizia “reverter” para todas as 102 frases; com o material de referência, passou a responder isso em apenas 12. Os negativos subiram de 0/79 para 68/79, mas os positivos desabaram de 23/23 para 1/23. Como naquele conjunto os negativos são 79 contra 23, uma constante do tipo “em geral, deixe como está” recebe automaticamente uma nota mais alta. <strong>Não aprendeu a distinguir; mudou a inclinação.</strong></p>
<p>Isso não quer dizer que o modelo não leia a frase. São as mesmas frases das mesmas palavras, e ao receber o material de referência as predições se espalharam frase a frase. O desvio padrão de <code>로더</code> foi de 0.096 para 0.217, e o de <code>뮤테이션</code>, de 0.042 para 0.236. <strong>Ele lê, mas não consegue distinguir.</strong></p>
<p>E este resultado também abala a interpretação da seção anterior. Se um único texto no state vira a saída inteira do avesso, o “reverte tudo” da primeira execução também pode não ser limite do modelo, e sim insuficiência do enunciado da pergunta. Este texto não conseguiu descartar essa possibilidade.</p>
<p>Uma coisa melhorou visivelmente. A soma das probabilidades de “reverter” e “deixar” no conjunto pendente se aproximou, de 1.508 para 0.963, e as frases muito afastadas de 1 caíram de 59 para 18. <strong>Só que a acurácia não se moveu.</strong> Uma coisa é as probabilidades das duas perguntas baterem entre si; outra é essas probabilidades estarem certas.</p>
<h2 id="o-lugar-dos-modelos-de-decisão"><a class="anchor" href="#o-lugar-dos-modelos-de-decisão">O lugar dos modelos de decisão</a></h2>
<p>Chegando até aqui é fácil pender para a conclusão de que essa categoria inteira de modelo é inútil, mas pôr os benchmarks públicos lado a lado mostra que não é bem assim.</p>
<table>
<thead>
<tr>
<th>Tarefa</th>
<th>n</th>
<th>Jev</th>
<th>Comparação</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="https://github.com/bitnovus/jev-spam-eval" target="_blank" rel="noopener noreferrer">Spam, dentro da distribuição</a></td>
<td>18,514</td>
<td>98.3%</td>
<td>regressão TF-IDF 98.4%</td>
</tr>
<tr>
<td>Spam, fora da distribuição</td>
<td>2,876</td>
<td><strong>98.6%</strong></td>
<td>regressão TF-IDF <strong>73.0%</strong></td>
</tr>
<tr>
<td>Phishing</td>
<td>2,000</td>
<td>62.6%</td>
<td>Haiku 4.5 81.3%, baseline de regras 91.6%</td>
</tr>
<tr>
<td><a href="https://github.com/anessbelbati/jev-rerank-bench" target="_blank" rel="noopener noreferrer">rerank, 8 conjuntos em inglês</a></td>
<td>1,617</td>
<td>nDCG@10 0.692</td>
<td>Cohere Rerank 4 Pro 0.691</td>
</tr>
</tbody>
</table>
<p>Só a linha de rerank é um indicador de qualidade de ranking de busca, então está num eixo diferente da acurácia das outras linhas.</p>
<p><strong>Dentro da distribuição, a regex ou um classificador ajustado ganham ou empatam.</strong> É o mesmo lugar em que a regex ganhou por 1.000 a 0.225 no meu conjunto do gate. Se dá para juntar rótulos, treinar um modelo pequeno sai mais barato, mais rápido e mais preciso. Isso é o que já se faz há muito tempo.</p>
<p><strong>A distância se abre quando a distribuição é nova e não há rótulos.</strong> No spam <span class="glossary-term-source" data-glossary-key="out-of-distribution">fora da distribuição</span>, 98.6% contra 73.0%. O classificador ajustado desmorona diante de um formato que nunca viu, e este lado aguenta. O lugar dos modelos de decisão é <strong>a decisão para a qual não dá para juntar dados, rotular e treinar</strong>. É onde a situação é nova toda vez, os rótulos não se acumulam e mesmo assim a decisão precisa sair em segundos.</p>
<p>Por que a minha detecção de transliteração falhou também se explica por esse quadro. O que aquele julgamento exige não é raciocínio geral, e sim <strong>um conhecimento de domínio específico: que palavras se consolidaram na documentação técnica em coreano</strong>. Para uma implementação de reprodução de 9B treinada em tarefas de decisão em inglês, isso não está fora da distribuição: simplesmente não existe ali. É aí que está a razão de dar as regras por escrito também não ter funcionado.</p>
<p>Numa matéria do TechCrunch, Armin Ronacher, CTO da Earendil, que faz o harness Pi, <a href="https://techcrunch.com/2026/09/18/a-new-kind-of-ai-model-from-a-chatgpt-inventor-is-thrilling-developers/" target="_blank" rel="noopener noreferrer">disse o seguinte</a>.</p>
<blockquote>
<p>At the end of the day, it delegates the hallucination problem a little bit to the user.</p>
</blockquote>
<p>Não é que a alucinação tenha sido eliminada: o julgamento foi passado para quem desenvolve. A documentação da TypeSafe também escreve que calibração é uma propriedade que vale para um conjunto de predições, e não uma garantia de que uma resposta individual está certa, e no item Nuance, abaixo da barra de 0% de alucinação do texto de anúncio, está escrito "Our number is not empirical". A forma é garantida; o conteúdo, não.</p>
<h2 id="os-194-projetos-do-jevable"><a class="anchor" href="#os-194-projetos-do-jevable">Os 194 projetos do jevable</a></h2>
<p>Como nos meus dados o Jev falhou, fui ver onde as outras pessoas o estão usando. O <a href="https://jevable.com/" target="_blank" rel="noopener noreferrer">jevable.com</a> é uma curadoria independente montada por um desenvolvedor chamado Nikunj, que examina os projetos com Jev publicados no X e os reúne. Em 23 de setembro de 2026 eram 194, e as datas de registro se concentram entre 16 e 20 de setembro de 2026. 152 entraram num único dia, 18 de setembro. Três dias depois do lançamento.</p>
<table>
<thead>
<tr>
<th>Categoria</th>
<th>Quantidade</th>
</tr>
</thead>
<tbody>
<tr>
<td>Games</td>
<td>39</td>
</tr>
<tr>
<td>Developer tools</td>
<td>34</td>
</tr>
<tr>
<td>Productivity</td>
<td>31</td>
</tr>
<tr>
<td>Agents</td>
<td>19</td>
</tr>
<tr>
<td>Experiments</td>
<td>18</td>
</tr>
<tr>
<td>Creative tools</td>
<td>16</td>
</tr>
<tr>
<td>Data &#x26; research, Finance, Browser extensions, Robotics, Marketing</td>
<td>37</td>
</tr>
</tbody>
</table>
<p>Se, em vez de por categoria, reagrupamos por “o que se pergunta”, saem três formas. Os números abaixo são todos os que cada autor escreveu na própria publicação.</p>
<p><strong>Primeira, classificação e roteamento.</strong> Classificar 1,500 e-mails, classificar documentos fiscais, classificar 1,891 anúncios de concorrentes em 19 segundos por $0.12, 14 typed checks num único diff de PR, detectar propaganda em notificações do Android, classificar 26 plantas de construção em 2.9 segundos. É a mesma forma do gate de transliteração do início. Como a tabela da seção anterior mostrou, é um lugar onde rótulos se acumulam todo dia, então, com o tempo, um classificador pequeno treinado nos próprios rótulos vence ou empata. A vantagem do Jev está no primeiro dia, quando ainda não há rótulos.</p>
<p><strong>Segunda, loops de escolha de ação.</strong> Um browser agent acoplado ao Browser Use que terminou uma busca de passagens aéreas em 7 segundos por $0.0039, um computer use que controla o Mac por voz, um jogo que, cada vez que o Mario morre, faz fork da VM em quatro ramos e escolhe o que sobrevive, um motor de expressões faciais que decide dez coisas a cada mensagem, como boca, sobrancelhas e olhar de um personagem 3D. Como o action space muda a cada passo, não dá para juntar rótulos, e o julgamento precisa descer para abaixo de um segundo. É o mesmo lugar em que a diferença se abriu no spam fora da distribuição. Acho que é também por isso que Games, com 39, é a maior categoria. Jogo é um loop de escolha de ação em que errar pode ser desfeito.</p>
<p><strong>Terceira, UIs que mostram a probabilidade ao usuário.</strong> O Ask Jev, que devolve só o veredito em vez de uma resposta; o JevForm, formulário ramificado que escolhe a próxima pergunta por probabilidade; o Upweight, que reordena a primeira página do Hacker News com seis sliders como profundidade técnica e drama. Como o limiar não fica cravado no código e é uma pessoa que lê a probabilidade, é onde a exigência de calibração que este texto questionou é mais baixa.</p>
<p>Uma coisa salta aos olhos. Das 194 descrições, 30 mencionam custo e 53 mencionam velocidade, mas só 7 mencionam acurácia ou uma linha de base. Como os resumos só trazem o começo de cada post, isso é um piso, mas a direção é clara. Que é rápido e barato dá para saber no primeiro dia; se está certo, só dá para saber medindo, e quem mede é raro. Um desses 7 é o <code>jevcal</code>. Partindo de que todo mundo escolhe o limiar no chute, é uma ferramenta que recebe os seus dados e a acurácia alvo e devolve o limiar e a fração de processamento automático. É exatamente o procedimento que a próxima seção recomenda, transformado em ferramenta.</p>
<h2 id="como-traçar-a-linha"><a class="anchor" href="#como-traçar-a-linha">Como traçar a linha</a></h2>
<p>A conclusão deste texto é esta. <strong>Não leia a probabilidade que o modelo devolveu como se fosse especificação: meça você mesmo nos seus dados e trace a linha ali.</strong></p>
<p>O procedimento para medir é este. Junte uns 100 casos rotulados, monte a tabela de taxa real de acerto por faixa de probabilidade e vá baixando o limiar de cima para baixo, olhando a taxa de erro da faixa acima dele. Se você definiu um orçamento de erro, a cobertura máxima que respeita esse orçamento é a fração que dá para automatizar. No meu experimento, que o acerto real da faixa acima de 0.9 de certeza fosse 0.240 apareceu com 102 frases.</p>
<p><strong>Uma escala desalinhada quase sempre dá para consertar.</strong> Se o excesso de certeza está espalhado de forma uniforme por todas as faixas, uma única temperatura acerta a maior parte. Foi assim que o Kev baixou o ECE de 0.106 para 0.042. O problema é que ajustar essa temperatura exige os rótulos daquela distribuição, e é exatamente isso que não existe em produção. Além do mais, a temperatura não muda a ordem das probabilidades, então a fração automatizável não sobe na mesma medida. Por isso a ordem vale mais que a escala, e a cobertura dentro do orçamento de erro é um indicador mais prático que o ECE.</p>
<p>Se eu tivesse cravado no código o 0.9 que a documentação usa no exemplo, este gate teria corrigido 38 casos errados em silêncio. É uma falha difícil de perceber, porque não dá erro. A documentação da TypeSafe também escreve, logo abaixo daquele exemplo, que você deve testar nos seus próprios dados. O que acaba cravado no código costuma ser o número de cima, não a frase de baixo.</p>
<p>Mesmo sem a chave, medir já dá para começar hoje. O Kev está publicado sob Apache-2.0 e roda em um notebook.</p>
<p>Seria bom contar quantas chamadas, no serviço que você opera hoje, só perguntam sim ou não a um modelo grande. Antes de mover essas chamadas para um modelo de decisão, recomendo medir primeiro onde deve ficar a linha depois da mudança. Foi por ter seguido essa ordem que eu pude decidir não mexer no gate. Quando eu conseguir uma chave do Jev penso em rodar o mesmo conjunto de novo, e se a conclusão mudar eu escrevo isso também.</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:#8b5cf6"></span><a href="https://arxiv.org/abs/2411.15124" target="_blank" rel="noopener noreferrer">Lambert et al., Tulu 3: a origem do nome RLVR</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/themsquared/jev-benchmark" target="_blank" rel="noopener noreferrer">themsquared/jev-benchmark, julgamento de risco em chamadas de ferramenta</a></div>
</div>
</div></details>]]></content:encoded>
            <author>jihoon7705@gmail.com (이지훈)</author>
            <category>IA</category>
            <category>Calibração</category>
        </item>
        <item>
            <title><![CDATA[overlay-kit]]></title>
            <link>https://hooninedev.com/pt-BR/260921</link>
            <guid isPermaLink="false">https://hooninedev.com/pt-BR/260921</guid>
            <pubDate>Mon, 21 Sep 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Neste post quero falar sobre interfaces que tratam overlays de forma declarativa. Eu gosto de código declarativo. Abrir um overlay, fechá-lo e fazer trabalho assíncrono dentro dele é uma sequência que...]]></description>
            <content:encoded><![CDATA[<p>Neste post quero falar sobre interfaces que tratam overlays de forma declarativa.</p>
<p>Eu gosto de código declarativo. Abrir um overlay, fechá-lo e fazer trabalho assíncrono dentro dele é uma sequência que, na minha opinião, o próprio overlay deveria gerenciar. Quando quem chama guarda <code>isOpen</code> e fica ligando e desligando esse valor, o código que lida com o estado acaba em um lugar diferente do componente que o desenha.</p>
<p>Só que os modais das telas pelas quais eu era responsável na empresa eram exatamente o contrário. Se um modal estava aberto ficava em uma store global, quem chamava abria e quem chamava fechava. Quem chamava também escrevia o código que fechava o modal e os callbacks que recebiam o resultado. Por isso passei bastante tempo pensando em como levar isso para uma direção declarativa e, no fim, reescrevi tudo.</p>
<p>Enquanto fazia esse trabalho, olhei o <a href="https://github.com/toss/overlay-kit" target="_blank" rel="noopener noreferrer">overlay-kit</a>. Era próximo da interface que eu buscava. Este post registra a interface declarativa do overlay-kit e como projetei o sistema de modais interno.</p>
<h2 id="o-modal-que-eu-guardava-em-estado"><a class="anchor" href="#o-modal-que-eu-guardava-em-estado">O modal que eu guardava em estado</a></h2>
<p>No meu post sobre <a href="/260518" target="_blank" rel="noopener noreferrer">gerenciamento de estado</a>, usei a flag de abertura de um modal como o exemplo mais simples de estado local. Escrevi que é um estado usado dentro de um único componente, que o mundo externo não precisa nem tem o direito de conhecer. Continuo achando isso verdade. Mas assim que você tem dezenas de tipos de modal, eles se empilham uns sobre os outros e precisam devolver um valor para quem os chamou, esse estado não fica mais dentro do componente.</p>
<p>O código que herdei mantinha a lista de modais abertos em uma store global. Como é código da empresa, o exemplo abaixo preserva a estrutura e troca os identificadores.</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">openPopup</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">closePopup</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"> useModalStore</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"> [state.openPopup, state.closePopup]),</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"> handleAddNote</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">(() </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">  openPopup</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(PopupType.</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">ADD_NOTE</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, {</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    itemId,</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">    onClose</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"> closePopup</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(PopupType.</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">ADD_NOTE</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">),</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">    onSubmit</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: (</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">note</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">      closePopup</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(PopupType.</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">ADD_NOTE</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">      saveNote</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(note);</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">}, [closePopup, openPopup, itemId]);</span></span></code></pre></figure>
<p>Três coisas nesse código me incomodavam.</p>
<p><strong>Primeira: a função que abre precisa sair de um hook.</strong> <code>openPopup</code> e <code>closePopup</code> são valores selecionados da store, então só é possível obtê-los dentro de um componente. Isso significa que o código que abre um modal não consegue sair do componente. Para abrir um modal em algum lugar onde não dá para usar hooks, como um interceptador de respostas da API ou um guard de rota, você precisa repassar essa função como argumento.</p>
<p><strong>Segunda: o resultado volta por callbacks.</strong> Se a nota foi salva ou se a pessoa simplesmente fechou o diálogo chega por dois ramos distintos, <code>onSubmit</code> e <code>onClose</code>. Tudo o que você quiser fazer depois de abrir o modal vai para dentro de um callback. E se precisar abrir dois modais em sequência, surge um callback dentro de outro callback.</p>
<p><strong>Terceira: fechar é responsabilidade de quem chama.</strong> É quem chama que invoca <code>closePopup(PopupType.ADD_NOTE)</code>. O modal não se fecha sozinho: o lado que o abriu lembra de que tipo de modal se trata e o fecha. É por isso que a mesma constante aparece três vezes em uma só função.</p>
<p>O que as três têm em comum é que abrir um único modal obriga quem chama a escrever três tipos de código: o que abre, o callback que recebe o resultado e o que fecha.</p>
<p>Se decidir o que mostrar é tarefa de quem chama, aqui quem chamava carregava também tudo o que vinha depois. Em especial, <code>closePopup(PopupType.ADD_NOTE)</code> aponta o alvo com uma constante de texto. Escrever a constante de outro modal ainda passa na checagem de tipos porque pertence ao mesmo enum, e o modal errado é fechado.</p>
<h2 id="uma-interface-que-espera-um-resultado"><a class="anchor" href="#uma-interface-que-espera-um-resultado">Uma interface que espera um resultado</a></h2>
<p>Então, qual dessas três dá para eliminar? A resposta a que cheguei foi transformar a abertura de um modal em uma chamada de função e fazer essa função devolver uma Promise com o resultado.</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"> 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:#B392F0;--shiki-light:#6F42C1"> openModalAsync</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'ADD_NOTE'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, { itemId });</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">if</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (result.reason </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">!==</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> 'confirmed'</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>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">await</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> saveNote</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(result.value);</span></span></code></pre></figure>
<p>Três coisas mudaram. A função que abre virou uma função de módulo em vez de um hook, então pode ser chamada fora de um componente. O resultado virou um valor de retorno em vez de um argumento de callback, então o trabalho seguinte permaneceu dentro do fluxo da função que chama. E fechar passou a ser tarefa do próprio componente do modal.</p>
<p>Como o tipo de retorno é <code>Promise&#x3C;ModalResult&#x3C;V>></code>, o fim do modal se concentrou em um único valor.</p>
<p>Uma Promise termina apenas uma vez. Resolver uma Promise já resolvida não faz nada, então todos os motivos pelos quais um modal pode terminar precisam caber nesse único valor. Por isso dividi <code>reason</code> em cinco casos: a pessoa confirmou, a pessoa cancelou explicitamente, fechou sem valor, foi substituído por um modal novo do mesmo tipo, ou foi encerrado à força em uma limpeza geral. Com um único booleano, "cancelado" e "a tela inteira foi desmontada" viram o mesmo valor, e nos fluxos que precisam de limpeza é necessário distinguir esses dois casos.</p>
<p>Na direção oposta, se você nunca a encerra, quem chama espera para sempre. Nada acontece quando um callback não é chamado, mas um <code>await</code> simplesmente não retorna. Por isso, cada vez que surgia um novo caminho para remover um modal da tela, eu precisava verificar se aquele caminho também encerrava a Promise. A substituição e o encerramento forçado têm cada um o seu <code>reason</code> justamente por causa disso.</p>
<h2 id="o-ponto-de-chamada-do-overlay-kit"><a class="anchor" href="#o-ponto-de-chamada-do-overlay-kit">O ponto de chamada do overlay-kit</a></h2>
<p>O ponto de chamada do overlay-kit, da toss, é 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:#79B8FF;--shiki-light:#005CC5"> confirmed</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"> overlay.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">openAsync</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">boolean</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">>(({ </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">isOpen</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">close</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">ConfirmDialog</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">    open</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{isOpen}</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">    onConfirm</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"> close</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:#B392F0;--shiki-light:#6F42C1">    onCancel</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"> close</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 style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">));</span></span></code></pre></figure>
<p>Três pontos me pareceram uma boa interface.</p>
<p><strong>O que abre está no ponto de chamada.</strong> A minha versão passa uma chave de texto, <code>'ADD_NOTE'</code>. Qual componente essa chave representa é o arquivo de registry que sabe: um arquivo com um único objeto que associa cada chave de texto à função <code>import()</code> que carrega aquele modal.</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">export</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> modalImporters</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">  ADD_NOTE</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"> import</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'.../add-note-modal'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">),</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  CONFIRM</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"> import</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'.../confirm-modal'</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>O overlay-kit escreve o JSX ali mesmo. Quem lê o código sabe o que vai aparecer sem precisar ir até aquele arquivo.</p>
<p><strong>O tipo do resultado é decidido no ponto de chamada.</strong> O argumento de tipo de <code>openAsync&#x3C;boolean></code> é ao mesmo tempo o tipo do argumento de <code>close</code> e o tipo do resultado do <code>await</code>. Não é preciso declarar um tipo de resultado por modal e registrá-lo em uma tabela.</p>
<p><strong>Fechar e remover são coisas separadas.</strong> O controller recebe quatro props: <code>overlayId</code>, <code>isOpen</code>, <code>close</code> e <code>unmount</code>. <code>close</code> coloca <code>isOpen</code> em <code>false</code> e deixa o componente onde está. <code>unmount</code> é o que de fato o remove. <a href="https://overlay-kit.slash.page/ko/docs/guides/introduction" target="_blank" rel="noopener noreferrer">O guia oficial sobre abrir e fechar overlays</a> explica assim.</p>
<blockquote>
<p>Essa diferença acontece porque, ao usar <code>close</code>, o overlay é mantido na memória para que a animação de fechamento possa ser exibida.</p>
</blockquote>
<p>A minha versão não tem essa distinção. Ao fechar, sai da lista imediatamente. Isso significa que eu estava deixando as animações de fechamento a cargo da biblioteca de componentes e que só lidava com telas em que isso era possível.</p>
<h2 id="de-overlayopen-até-a-renderização"><a class="anchor" href="#de-overlayopen-até-a-renderização">De overlay.open até a renderização</a></h2>
<p>Quando você gosta de uma interface, a pergunta seguinte é o que há dentro. Para uma única função de módulo desenhar um componente dentro da árvore do React, algo precisa ligar os dois lados.</p>
<h3 id="eventos-e-reducer"><a class="anchor" href="#eventos-e-reducer">Eventos e reducer</a></h3>
<p><code>overlay.open</code> não muda o estado. Ele emite um evento.</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">// packages/src/event.ts</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> open</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">controller</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> OverlayControllerComponent</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">options</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">?:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> OpenOverlayOptions</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"> overlayId</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> options?.overlayId </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">??</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> randomId</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"> componentKey</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> randomId</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"> dispatchOpenEvent</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> createEvent</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'open'</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">  dispatchOpenEvent</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({ controller, overlayId, componentKey });</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> overlayId;</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">};</span></span></code></pre></figure>
<p>Quem recebe é o <code>OverlayProvider</code>. O provider guarda um <code>useReducer</code>, assina o evento e coloca <code>ADD</code> no reducer. O que merece atenção aqui é que <code>isOpen</code> entra como <code>false</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:#6A737D;--shiki-light:#6A737D">// packages/src/context/provider/index.tsx</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">overlayState</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">overlayDispatch</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"> useReducer</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(overlayReducer, {</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  current: </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">  overlayOrderList: [],</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  overlayData: {},</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"> open</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> OverlayEvent</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">[</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'open'</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"> useCallback</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(({ </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">controller</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">overlayId</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">componentKey</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">  overlayDispatch</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    type: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'ADD'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    overlay: { id: overlayId, componentKey, isOpen: </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">false</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, isMounted: </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">false</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, controller },</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>E, no frame seguinte à montagem do controller, ele despacha <code>OPEN</code> para colocar <code>isOpen</code> em <code>true</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:#6A737D;--shiki-light:#6A737D">// packages/src/context/provider/content-overlay-controller.tsx</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:#B392F0;--shiki-light:#6F42C1">  requestAnimationFrame</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">    overlayDispatch</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({ type: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'OPEN'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, overlayId });</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">}, [overlayDispatch, overlayId]);</span></span></code></pre></figure>
<p>Aqui houve uma parte que não bateu com a minha expectativa.</p>
<p><strong>O estado não fica fora do React.</strong> Como é uma API que abre por meio de uma função de módulo, imaginei que a store também estivesse no nível de módulo, mas na verdade é um <code>useReducer</code> dentro do provider. A única coisa fora do React é o barramento de eventos, e ele não guarda estado. Os overlays são renderizados como irmãos ao lado dos <code>children</code> do provider. Ele não usa <code>createPortal</code> nem importa <code>react-dom</code>.</p>
<p>Há mais uma coisa que chama atenção. Do ponto de chamada é uma linha só, <code>overlay.open()</code>, mas por dentro existe um procedimento de ordem fixa: emitir um evento, colocar <code>ADD</code> no reducer, esperar um frame e colocar <code>OPEN</code> de novo. Uma interface declarativa não elimina o código imperativo: ela o move para o outro lado de uma fronteira.</p>
<h3 id="o-lado-que-abre-e-o-lado-que-fecha"><a class="anchor" href="#o-lado-que-abre-e-o-lado-que-fecha">O lado que abre e o lado que fecha</a></h3>
<p>O <code>requestAnimationFrame</code> que acabamos de ver e a separação entre <code>close</code> e <code>unmount</code> da seção anterior parecem coisas distintas à primeira vista. Na prática são dois mecanismos que tapam as duas pontas da mesma restrição.</p>
<p>Essa restrição é que uma transição CSS só roda quando <strong>tanto o estado inicial quanto o final são de fato renderizados</strong>. <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_transitions/Using_CSS_transitions" target="_blank" rel="noopener noreferrer">O guia de transições CSS da MDN</a> alerta sobre elementos recém-adicionados ao DOM.</p>
<blockquote>
<p>This is treated as if the initial state had never occurred and the element was always in its final state.</p>
</blockquote>
<p>A falha aparece de duas formas. Ao abrir não existe estado inicial, então nada roda; ao fechar o elemento some antes de o estado final ser pintado, então também não roda.</p>
<table>
<thead>
<tr>
<th></th>
<th>Ao abrir</th>
<th>Ao fechar</th>
</tr>
</thead>
<tbody>
<tr>
<td>Problema</td>
<td>Não existe estado inicial</td>
<td>Some antes de pintar o estado final</td>
</tr>
<tr>
<td>Solução</td>
<td>Montar fechado primeiro e abrir no frame seguinte</td>
<td>Manter montado, desligar só <code>isOpen</code> e remover depois</td>
</tr>
<tr>
<td>Mecanismo</td>
<td><code>ADD(isOpen: false)</code>, rAF, <code>OPEN</code></td>
<td><code>CLOSE</code>, animação, <code>REMOVE</code></td>
</tr>
<tr>
<td>Quem sabe o momento</td>
<td>A biblioteca</td>
<td>Quem usa</td>
</tr>
</tbody>
</table>
<p>A última linha explica por que o lado que fecha fica a cargo de quem usa. No lado que abre, a biblioteca conhece a constante: um frame basta. No lado que fecha, a biblioteca não tem como saber se a animação dura 200 ou 400 milissegundos.</p>
<h4 id="o-fechamento-entregue-como-unmount"><a class="anchor" href="#o-fechamento-entregue-como-unmount">O fechamento entregue como <code>unmount</code></a></h4>
<p>O que sustenta o lado que fecha está no reducer. <code>CLOSE</code> apenas inverte, dentro de <code>overlayData</code>, o valor <code>isOpen</code>, e não toca em <code>overlayOrderList</code>. O provider renderiza percorrendo essa lista, então o componente continua vivo enquanto permanecer nela. Ele só é desmontado quando <code>REMOVE</code> o tira de lá.</p>
<p><strong>Dito isso, o rAF nem sempre é necessário.</strong> O react-transition-group <a href="https://reactcommunity.org/react-transition-group/transition" target="_blank" rel="noopener noreferrer">não executa a transição de entrada na primeira montagem por padrão</a>. Ele a pula mesmo quando <code>in</code> já é <code>true</code>, e é preciso ativar <code>appear</code>. O rAF do overlay-kit muda <code>isOpen</code> de <code>false</code> para <code>true</code> depois da montagem, o que cobre esse caso.</p>
<h3 id="vinte-linhas-embrulhadas-em-uma-promise"><a class="anchor" href="#vinte-linhas-embrulhadas-em-uma-promise">Vinte linhas embrulhadas em uma Promise</a></h3>
<p><code>openAsync</code> não é uma implementação à parte, e sim uma camada fina em volta de <code>open</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">// packages/src/event.ts</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> openAsync</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"> &#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">controller</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> OverlayAsyncControllerComponent</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">options</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">?:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> OpenOverlayOptions</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:#F97583;--shiki-light:#D73A49"> new</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> Promise</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">_resolve</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">_reject</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">    open</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">((</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">overlayProps</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">...</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">deprecatedLegacyContext</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:#B392F0;--shiki-light:#6F42C1"> close</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">param</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:#B392F0;--shiki-light:#6F42C1">        _resolve</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(param);</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">        overlayProps.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">close</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:#B392F0;--shiki-light:#6F42C1"> reject</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">reason</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:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">        _reject</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(reason);</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">        overlayProps.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">close</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"> props</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> OverlayAsyncControllerProps</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 style="--shiki-dark:#F97583;--shiki-light:#D73A49">...</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">overlayProps, close, reject };</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">      return</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> controller</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(props, </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">...</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">deprecatedLegacyContext);</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    }, 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>Tudo o que ele faz é trocar o <code>close</code> que chega ao controller. O <code>close</code> original não recebe argumentos; este recebe um, resolve a Promise com ele e depois chama o <code>close</code> original. É também por isso que o tipo do resultado é decidido no ponto de chamada. <code>T</code> é ao mesmo tempo o tipo do argumento de <code>close</code> e o tipo da Promise.</p>
<p>Ainda assim, essa estrutura tem um buraco. A resolução só acontece quando <code>close</code> é chamado. Por isso, se você remover um overlay apenas com <code>unmount</code>, a Promise fica pendente para sempre. Está registrado na <a href="https://github.com/toss/overlay-kit/issues/169" target="_blank" rel="noopener noreferrer">issue #169</a> e acho que precisa ser corrigido, mas a issue continua aberta.</p>
<p>O fato de a resolução acontecer em <code>close</code> tem outra implicação. É o momento em que o fechamento começa, não o momento em que a animação de fechamento termina. Isso não é problema em um fluxo que pede confirmação e depois chama uma API, mas em um fluxo que pede confirmação e depois abre o próximo modal, o segundo sobe enquanto o primeiro ainda está desaparecendo.</p>
<h3 id="onde-o-design-deu-uma-volta-completa"><a class="anchor" href="#onde-o-design-deu-uma-volta-completa">Onde o design deu uma volta completa</a></h3>
<p>Segui o histórico de commits para explorar como o design chegou até aqui e descobri que a estrutura não foi assim desde o começo. A 1.0 usava a mesma combinação de eventos e reducer de hoje. A 1.2.0 moveu o estado para uma store externa no nível de módulo, lida com <code>useSyncExternalStore</code>, e a 1.8.0 devolveu tudo para um reducer.</p>
<p>O motivo da volta atrás está escrito na <a href="https://github.com/toss/overlay-kit/issues/148" target="_blank" rel="noopener noreferrer">issue #148</a>.</p>
<blockquote>
<p><code>useSyncExternalStore</code> resolves tearing issues, but it doesn't work well with suspense.</p>
</blockquote>
<p>Quando buscar dados dentro de um overlay o fazia suspender, o React avisava que um componente havia suspendido enquanto respondia a uma entrada síncrona. O objetivo original de migrar para uma store externa era permitir ler o estado do overlay sem um hook, e o PR da época diz claramente que o objetivo não foi alcançado da forma pretendida. No fim o objetivo foi resolvido com um hook e apenas o mecanismo de armazenamento voltou ao lugar de origem.</p>
<p>O que levo desse registro não é uma conclusão, e sim um critério. Colocar o estado fora do React libera de onde você pode chamar as coisas, ao custo de abrir espaço para sair de compasso com o agendamento do React. O overlay-kit deixou de fora apenas o ponto de chamada e manteve o estado dentro.</p>
<h2 id="ao-reabrir-com-o-mesmo-id"><a class="anchor" href="#ao-reabrir-com-o-mesmo-id">Ao reabrir com o mesmo id</a></h2>
<p>Algo me chamou atenção ao ler o reducer. Logo no começo de <code>ADD</code> existe este ramo.</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">// packages/src/context/reducer.ts</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">case</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> 'ADD'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: {</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  if</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (state.overlayData[</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">action</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">.</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">overlay</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:#FFAB70;--shiki-light:#E36209">null</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> &#x26;&#x26; state.overlayData[</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">action</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">.</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">overlay</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">].isOpen === </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">false</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"> overlay</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> state.overlayData[action.overlay.id];</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">    if</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (overlay </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"> overlay.isOpen) {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">      return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> state;</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"> {</span></span>
<span data-line=""><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">      current: action.overlay.id,</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">      overlayData: { </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">...</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">state.overlayData, [action.overlay.id]: { </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">...</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">overlay, isOpen: </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">  }</span></span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">  // ...</span></span></code></pre></figure>
<p>Se você abre um overlay informando um <code>overlayId</code>, faz <code>close</code> e depois o abre de novo com o mesmo id, cai nesse ramo. Só que o valor de retorno reaproveita o <strong>objeto <code>overlay</code> existente</strong>.</p>
<p>Nada usa o <code>action.overlay</code> que acabou de chegar nem o <code>controller</code> dele.</p>
<p>Ler não foi suficiente para me convencer, então clonei o repositório da biblioteca e escrevi um teste. Preparei dois componentes que desenham conteúdos diferentes e os abri em ordem com o mesmo id.</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"> ControllerA</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">isOpen</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">isOpen</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 style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (isOpen </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:#B392F0;--shiki-light:#6F42C1"> data-testid</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"b"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">>AAA&#x3C;/</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">div</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"> 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:#B392F0;--shiki-light:#6F42C1"> ControllerB</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">isOpen</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">isOpen</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 style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (isOpen </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:#B392F0;--shiki-light:#6F42C1"> data-testid</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"b"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">>BBB&#x3C;/</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">div</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"> null</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">overlay.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">open</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(ControllerA, { overlayId: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'id-close'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> });</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">overlay.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">close</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'id-close'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">overlay.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">open</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(ControllerB, { overlayId: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'id-close'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> });</span></span></code></pre></figure>
<p>Estes foram os resultados, executados com vitest na branch main na versão 1.9.0 do overlay-kit.</p>
<table>
<thead>
<tr>
<th>Procedimento</th>
<th>O que foi renderizado</th>
</tr>
</thead>
<tbody>
<tr>
<td>Abrir A, <code>close</code>, reabrir com B</td>
<td><code>AAA</code>. B é descartado</td>
</tr>
<tr>
<td>Abrir A, <code>unmount</code>, reabrir com B</td>
<td><code>BBB</code>. Substituição correta</td>
</tr>
<tr>
<td>Subir um contador para 1, <code>close</code>, reabrir</td>
<td><code>1</code>. O estado do componente é preservado</td>
</tr>
</tbody>
</table>
<p>A terceira linha é um comportamento que a documentação descreve como recurso, então saiu como o esperado. O problema é a primeira linha.</p>
<h3 id="o-jsx-novo-é-ignorado"><a class="anchor" href="#o-jsx-novo-é-ignorado">O JSX novo é ignorado</a></h3>
<p>Aqui verifiquei mais uma coisa. Mais adiante, no mesmo reducer, há este comentário.</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">/**</span></span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D"> * </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">@description</span><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D"> Brings the overlay to the front when reopened after closing without unmounting.</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">overlayOrderList</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.overlayOrderList.</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">item</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"> item </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">!==</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> action.overlay.id), action.overlay.id],</span></span></code></pre></figure>
<p>Ele diz que reabrir depois de fechar traz o overlay para a frente. Mas, como o ramo anterior chega antes ao <code>return</code>, essa linha não é executada nessa situação. Empilhei dois overlays, fiz <code>close</code> no de baixo, reabri, e a ordem do DOM não mudou.</p>
<h3 id="um-comportamento-que-contradiz-o-comentário"><a class="anchor" href="#um-comportamento-que-contradiz-o-comentário">Um comportamento que contradiz o comentário</a></h3>
<p>Só que não verifiquei se isso aparece como um problema visível na tela. O teste olhou apenas a ordem do DOM e, como a maioria das bibliotecas de diálogo usa z-index próprio ou portais, a ordem do DOM não equivale à ordem visual.</p>
<p>A minha versão expõe essa mesma situação como uma opção. Ao reabrir um modal do mesmo tipo, quem chama escolhe entre manter o existente ou substituí-lo com props novas. Se escolher a substituição, a Promise existente termina como <code>replaced</code>. Essa é uma das razões pelas quais dividi antes o tipo do resultado em cinco ramos.</p>
<p><strong>É aqui que os dois designs divergem.</strong> A minha abordagem trata modais como dados. Como a chave de texto e as props ficam em uma store, dá para substituí-los ou atualizá-los depois. O overlay-kit trata modais como funções. O controller é um closure, então, uma vez guardado, não há como mudar o conteúdo dele de fora.</p>
<h2 id="o-preço-do-jsx-no-ponto-de-chamada"><a class="anchor" href="#o-preço-do-jsx-no-ponto-de-chamada">O preço do JSX no ponto de chamada</a></h2>
<p>Então, escrever JSX no ponto de chamada é sempre melhor do que usar uma chave de texto? Eu acho que não. Há três preços a pagar.</p>
<p><strong>A divisão de código deixa de ser o padrão.</strong> Escrever JSX no ponto de chamada significa que quem chama importa aquele componente. Se o modal é uma tela de edição pesada e poucas pessoas apertam aquele botão, esse peso entra no bundle da tela de listagem.</p>
<p><code>React.lazy</code> funciona dentro de um controller. Medi do mesmo jeito que no teste anterior: o módulo não era carregado antes do <code>open</code> e, ao abrir, o fallback do <code>Suspense</code> aparecia primeiro e depois o conteúdo era renderizado.</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"> Heavy</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">lazy</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"> import</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'./HeavyModal'</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">overlay.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">open</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(({ </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">isOpen</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">close</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"> isOpen </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">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">Spinner</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> />}>&#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Heavy</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> onClose</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{close} />&#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">) </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></code></pre></figure>
<p>Ou seja, a diferença não está na possibilidade, e sim no padrão.</p>
<p>Na minha versão todos os valores do arquivo de registry são funções <code>import()</code>, então a divisão de código é imposta pela estrutura. Com o overlay-kit é preciso escrevê-la à mão em cada ponto de chamada.</p>
<p><strong>Quem usa precisa decidir onde chamar <code>unmount</code>.</strong> Se você só chama <code>close</code> e para por aí, as informações do overlay ficam na memória. A documentação oficial alerta sobre isso e dá orientações separadas sobre onde prender <code>unmount</code> em cada design system. MUI e Mantine têm um callback para quando a transição termina, então basta prendê-lo ali, mas para bibliotecas sem esse callback a documentação manda colocar um timer com a duração da animação. O fato de essas orientações estarem divididas por biblioteca já é, em si, o custo dessa decisão.</p>
<p><strong>Os overlays são renderizados onde está o provider.</strong> Como ele não usa portais, o mesmo código roda no React Native, o que é uma vantagem. Em contrapartida, o contexto fornecido abaixo do <code>OverlayProvider</code> não é visível dentro de um overlay. Ao abrir um modal que depende do contexto de rota ou de formulário, convém verificar esse ponto primeiro.</p>
<h2 id="até-onde-vai-a-palavra-declarativo"><a class="anchor" href="#até-onde-vai-a-palavra-declarativo">Até onde vai a palavra declarativo</a></h2>
<p>Depois de escrever até aqui, uma coisa me incomoda. Comecei este post chamando essa interface de declarativa, e no entanto <code>overlay.openAsync(...)</code> é uma chamada de função por qualquer ângulo que se olhe.</p>
<p>Comecemos pela definição. <a href="https://react.dev/learn/reacting-to-input-with-state" target="_blank" rel="noopener noreferrer">A documentação do React</a> traça a linha assim.</p>
<blockquote>
<p>In React, you don't directly manipulate the UI--meaning you don't enable, disable, show, or hide components directly. Instead, you <strong>declare what you want to show,</strong> and React figures out how to update the UI.</p>
</blockquote>
<p>A analogia do mesmo documento é mais nítida. O imperativo é ir cantando cada curva para quem dirige o carro; o declarativo é entrar em um táxi e dizer o destino.</p>
<p><strong>Uma interface declarativa não elimina o procedimento imperativo: ela o move para o outro lado de uma fronteira e dá um nome a essa fronteira.</strong> O interior do overlay-kit que vimos é exatamente isso. O ponto de chamada tem uma linha, enquanto lá dentro roda um procedimento que emite um evento e faz dois dispatch.</p>
<p>Vejamos, então, onde cada projeto aplica a palavra. A documentação do overlay-kit chama a abordagem anterior de imperativa. O que ela aponta é o código que guarda a flag de abertura com <code>useState</code> e a inverte em um manipulador de eventos. O que ela chama de declarativo é o lado que trata o overlay como uma ação, e não como estado. O <a href="https://toss.tech/article/frontend-declarative-code" target="_blank" rel="noopener noreferrer">artigo sobre código declarativo</a> do blog técnico da toss define a palavra de forma mais ampla: código declarativo é código com um nível de abstração mais alto, e um hook que abstrai a ação de exibir um overlay é um exemplo disso.</p>
<p>Por isso decidi estreitar a palavra assim: <strong>quem chama não escreve o código que fecha</strong>. Quando fechar, e o que permanece na tela enquanto fecha, quem decide é o overlay; quem chama escreve apenas o que mostrar e o que espera receber de volta.</p>
<p>Por esse critério, o código escrito com <code>useState</code> que guarda <code>isOpen</code> e o código que mantém uma lista de modais abertos em uma store global enquanto quem chama os fecha estão do mesmo lado. Muda só onde se guarda; nos dois, quem chama escreve a instrução de fechamento.</p>
<h2 id="para-encerrar"><a class="anchor" href="#para-encerrar">Para encerrar</a></h2>
<p>O que eu corrigi no código da empresa não foi, no fim das contas, onde o estado ficava guardado. Foi quem escreve o código que fecha.</p>
<p>Mesmo quando usávamos uma store global, o estado já estava fora do componente, mas as instruções que abriam e fechavam continuavam no ponto de chamada. Só depois de trocar por um único <code>await</code> é que essas instruções entraram no modal.</p>
<p>O overlay-kit chegou ao mesmo lugar e deu mais um passo: ele coloca também no ponto de chamada o que será exibido. Acho que essa interface se lê bem. E, enquanto escrevia isto, descobri quanto custa esse passo.</p>
<p><strong>Era a diferença entre tratar um modal como função e tratá-lo como dados.</strong> Escrever JSX no ponto de chamada transforma o controller em um closure, e o conteúdo de um closure não pode ser alterado de fora depois de guardado. É dessa estrutura que nasce o JSX ignorado ao reabrir com o mesmo id. Tratá-lo com uma chave de texto transforma o modal em um valor guardado em uma store que você pode substituir, atualizar e dividir em chunks, mas quem chama só chega a conhecer um nome.</p>
<p>Acho que a sensação de gostar da interface de uma biblioteca é um sinal bastante confiável. Ainda assim, vale a pena colocar em palavras pelo menos uma vez para o que exatamente essa sensação aponta. Sem fazer isso, você não consegue distinguir se o que agradou foi a interface ou a reputação de quem usa a biblioteca. Recomendo a quem estiver lendo fazer esse exercício uma vez com alguma biblioteca que esteja usando agora.</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:#3b82f6"></span><a href="https://overlay-kit.slash.page/" target="_blank" rel="noopener noreferrer">Documentação oficial do overlay-kit</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/desko27/react-call" target="_blank" rel="noopener noreferrer">desko27/react-call</a></div>
</div>
</div></details>]]></content:encoded>
            <author>jihoon7705@gmail.com (이지훈)</author>
            <category>frontend</category>
            <category>React</category>
            <category>bibliotecas</category>
        </item>
        <item>
            <title><![CDATA[Calibração e excesso de confiança em LLMs]]></title>
            <link>https://hooninedev.com/pt-BR/260917</link>
            <guid isPermaLink="false">https://hooninedev.com/pt-BR/260917</guid>
            <pubDate>Thu, 17 Sep 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Neste post quero falar sobre a calibração de modelos e o excesso de confiança que aparece depois do RLHF. É para desenvolvedores que querem usar no código, como critério de decisão, a probabilidade ou...]]></description>
            <content:encoded><![CDATA[<p>Neste post quero falar sobre a calibração de modelos e o excesso de confiança que aparece depois do RLHF. É para desenvolvedores que querem usar no código, como critério de decisão, a probabilidade ou a confiança que um modelo devolve. Ao final, você vai conseguir explicar como acurácia e calibração diferem, o que o ECE mede, como as probabilidades de um modelo lapidado com preferência humana se afastam da taxa real de acerto e até onde a causa é conhecida.</p>
<p>Precisei organizar esses conceitos primeiro enquanto testava as afirmações do Jev, um modelo de decisão lançado pela TypeSafe AI. Em vez de frases, o Jev devolve uma probabilidade para cada opção e apresenta a calibração como objetivo de treino. Para avaliar essa afirmação, eu precisava saber antes o que é calibração e por que ela sai do lugar nos modelos existentes.</p>
<h2 id="acurácia-e-calibração"><a class="anchor" href="#acurácia-e-calibração">Acurácia e calibração</a></h2>
<p>Primeiro é preciso separar acurácia de <span class="glossary-term-source" data-glossary-key="calibration">calibração</span>. Acurácia é quantos por cento você acerta; calibração é saber quantos por cento você vai acertar. Se, juntando só os dias em que a previsão disse 70% de chance de chuva, choveu de fato em sete de cada dez, aquela previsão está bem calibrada. Isso não quer dizer que a acurácia seja alta. Quer dizer que ela conhece o próprio limite. <strong>Um modelo que acerta só 60%, se disser de si mesmo que acerta 60%, tira nota máxima em calibração.</strong></p>
<p>A escala pode sair do lugar em duas direções. Quando a probabilidade que o modelo declara é maior que a taxa real de acerto, isso se chama overconfidence; quando é menor, underconfidence. Pondo a probabilidade declarada no eixo horizontal e a taxa real de acerto de cada faixa de probabilidade no eixo vertical, dá para ver de relance onde as duas se separam. Um gráfico desenhado assim se chama reliability diagram.</p>
<p><picture><source type="image/webp" srcset="/content/260917/1.720w.webp 720w, /content/260917/1.1440w.webp 1440w" sizes="(max-width: 752px) calc(100vw - 32px), 720px"><img src="/content/260917/1.png" alt="O eixo horizontal é confidence, a probabilidade que o modelo declara, e o eixo vertical é accuracy, a taxa real de acerto. A diagonal tracejada é perfect calibration; a curva laranja que cai abaixo dela é overconfidence, e a região acima da diagonal é underconfidence" style="max-width:min(720px, 100%);height:auto;" width="1440" height="1200" loading="eager" fetchpriority="high" decoding="async"></picture></p>
<p>Se a curva fica sobre a diagonal, o modelo acertou tanto quanto disse. Quanto mais ela cai abaixo da diagonal, menos ele acerta em relação à probabilidade que declara.</p>
<p>O indicador que mede esse descompasso é o <span class="glossary-term-source" data-glossary-key="ece">ECE</span> (expected calibration error). Pela definição de <a href="https://arxiv.org/abs/1706.04599" target="_blank" rel="noopener noreferrer">Guo et al. 2017</a>, é a média, ponderada pela fração de amostras de cada faixa, da diferença entre “a probabilidade declarada” e “a taxa real de acerto” em cada faixa de probabilidade, e 0 é o valor perfeito. O código abaixo transcreve essa fórmula. Ele compara um modelo que acerta 6 de 10 questões quando diz 0.6 toda vez e quando diz 0.95 toda vez.</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">// Guo et al. 2017 의 식 (3). 구간은 등간격 10개(논문 실험은 15개), 경계는 왼쪽 닫힘</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">function</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> ece</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">preds</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">M</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:#F97583;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> bins</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">({ length: </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">M</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"> ({ n: </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">0</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, hit: </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">0</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, conf: </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">  for</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (</span><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">p</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">correct</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> } </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">of</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> preds) {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">    const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> b</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> bins[Math.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">min</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">M</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">, Math.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">floor</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(p </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">*</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> M</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">))]</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    b.n</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">++</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    b.hit </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">+=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> correct </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">?</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 1</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> :</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 0</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    b.conf </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">+=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> p</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"> bins.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">reduce</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">sum</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.n </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"> (b.n </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">/</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> preds.</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">length</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"> Math.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">abs</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(b.hit </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">/</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> b.n </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">-</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> b.conf </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">/</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> b.n) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> sum),</span></span>
<span data-line=""><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:#6A737D;--shiki-light:#6A737D">// 10문제 중 6문제를 맞힌다</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> answers</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">({ length: </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">10</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> }, (</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">i</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"> ({ correct: i </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">&#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 6</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> }))</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">console.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">log</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">ece</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(answers.</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">a</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">a, p: </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">0.6</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> }))).</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">toFixed</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">3</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">)) </span><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 매번 0.6 이라고 말할 때</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">console.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">log</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">ece</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(answers.</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">a</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">a, p: </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">0.95</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> }))).</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">toFixed</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">3</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">)) </span><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 매번 0.95 라고 말할 때</span></span></code></pre></figure>
<p>Este é o resultado de rodá-lo com Node v24.16.0 em 2026-10-08.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="text" data-theme="github-dark github-light"><code data-language="text" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span>0.000</span></span>
<span data-line=""><span>0.350</span></span></code></pre></figure>
<p>A acurácia é 0.6 nos dois casos. Só a probabilidade declarada mudou, e mesmo assim o ECE sobe de 0 para 0.35. Mas como a fórmula usa valor absoluto, o ECE mede só o tamanho do descompasso, não a direção. Um modelo que diz 0.25 toda vez e ainda acerta 6 questões também fica com ECE de exatamente 0.35. Para saber se é overconfidence ou underconfidence, é preciso olhar o reliability diagram ou a confiança média menos a acurácia.</p>
<h2 id="a-função-objetivo-do-rlhf"><a class="anchor" href="#a-função-objetivo-do-rlhf">A função objetivo do RLHF</a></h2>
<p>Então por que essa escala sai do lugar num modelo lapidado com feedback humano? Primeiro precisamos ver o que o <span class="glossary-term-source" data-glossary-key="rlhf">RLHF</span> (reinforcement learning from human feedback) otimiza. O esqueleto de construir um modelo de recompensa a partir de comparações de preferência humana veio de <a href="https://arxiv.org/abs/1706.03741" target="_blank" rel="noopener noreferrer">Christiano et al. 2017</a>; um dos primeiros trabalhos a levá-lo para modelos de linguagem foi <a href="https://arxiv.org/abs/1909.08593" target="_blank" rel="noopener noreferrer">Ziegler et al. 2019</a>, e o <a href="https://arxiv.org/abs/2203.02155" target="_blank" rel="noopener noreferrer">InstructGPT</a> o aplicou ao seguimento de instruções. O que é empurrado para cima é <strong>a nota de um modelo de recompensa que imita a preferência humana</strong>. Os trabalhos com modelos de linguagem somam à recompensa uma KL penalty que impede o modelo de se afastar demais de como estava antes do aprendizado por reforço, e o InstructGPT ainda misturou gradientes dos dados de pré-treino (PPO-ptx), mas o que ele empurra para cima continua sendo essa nota.</p>
<p>Para um chatbot, a preferência humana é o objetivo certo. O problema é que essa preferência pende para evitar o tom que revela incerteza. <a href="https://arxiv.org/abs/2401.06730" target="_blank" rel="noopener noreferrer">Zhou et al. 2024</a> mostraram, em quatro conjuntos públicos de dados de preferência, que os anotadores escolhem menos as respostas com atenuadores como "I'm not sure, maybe". A diferença era pequena, mas significativa. Por outro lado, eles não escolheram mais as respostas com expressões que reforçam a certeza. <a href="https://arxiv.org/abs/2410.09724" target="_blank" rel="noopener noreferrer">Leng et al. 2025</a> mostraram que modelos de recompensa dão nota maior a respostas que declaram um número de confiança alto, independentemente da qualidade real. Os dois resultados tratam de expressões de confiança dentro do texto (verbalized confidence), não de probabilidades de tokens. A TypeSafe, que propõe um método de treino para substituir o RLHF, também observa no seu próprio <a href="https://docs.typesafe.ai/introduction/machine-learning-primer" target="_blank" rel="noopener noreferrer">guia introdutório</a> que o RLHF pode recompensar alucinações que soam confiantes (confident-sounding hallucinations). É a posição de quem vende a alternativa.</p>
<h2 id="as-curvas-de-calibração-do-gpt-4"><a class="anchor" href="#as-curvas-de-calibração-do-gpt-4">As curvas de calibração do GPT-4</a></h2>
<p>Nas probabilidades dos tokens também se observou um descompasso na mesma direção. A Figure 8 do corpo do <a href="https://arxiv.org/pdf/2303.08774v6#page=12" target="_blank" rel="noopener noreferrer">relatório técnico do GPT-4</a> coloca lado a lado os reliability diagrams do modelo pré-treinado e do modelo post-training, e a legenda diz:</p>
<blockquote>
<p>Right: Calibration plot of the post-trained GPT-4 model on the same subset of MMLU. The post-training hurts calibration significantly.</p>
</blockquote>
<p>O eixo horizontal é a probabilidade (logprob) que o modelo atribuiu a cada uma das alternativas A, B, C e D em questões de múltipla escolha do MMLU, e o eixo vertical é a taxa real de acerto naquela faixa. Pelos números impressos na Figure 8, o ECE do modelo pré-treinado é <strong>0.007</strong> e o do modelo que passou por PPO (o algoritmo de aprendizado por reforço usado no RLHF para atualizar o modelo na direção de notas mais altas do modelo de recompensa) é <strong>0.074</strong>. Piorou mais de dez vezes.</p>
<p>No painel direito (PPO) da Figure 8, as barras de 0.4 para cima ficam abaixo da diagonal, mas as faixas entre 0.1 e 0.3 ficam, ao contrário, acima dela. As barras entre 0.1 e 0.9 se amontoam planas numa taxa real de acerto de cerca de 0.3 a 0.5, então, mesmo que a probabilidade que o modelo declara suba, a taxa de acerto quase não acompanha. No 0.074 também estão misturadas essas faixas de underconfidence. E, como Guo et al. apontam, um reliability diagram não mostra quantas amostras caem em cada faixa, de modo que não dá para supor que a distância de uma barra até a diagonal seja proporcional ao quanto aquela faixa contribui para o ECE.</p>
<p>Este painel difere do diagrama conceitual acima em dois pontos. Primeiro, não apenas cai abaixo da diagonal como a curva do diagrama conceitual, mas a cruza uma vez. Segundo, coloca nas faixas as probabilidades das quatro alternativas, então também difere da definição do código acima, que mede a probabilidade da única resposta escolhida. Por isso não comparo o 0.074 com o 0.35 do código acima na mesma escala.</p>
<p>Então por que piorou? O corpo do relatório diz apenas que o modelo pré-treinado estava bem calibrado e que a calibração diminuiu depois do post-training; não aponta uma causa. Uma pista está em <a href="https://arxiv.org/abs/2207.05221" target="_blank" rel="noopener noreferrer">Kadavath et al. 2022</a>, da Anthropic. As políticas RLHF que eles treinaram a partir dos próprios modelos de linguagem pareciam, à primeira vista, muito mal calibradas. O artigo disse que isso não surpreendia, porque o RL fine-tuning tende a concentrar as previsões nos comportamentos que recebem mais recompensa. Foi um experimento curto acrescentado à margem, não o tema central. Ainda assim, ao aplicar uma única temperatura, T=2.5, a todas as avaliações, os problemas de calibração foram em grande parte resolvidos em três avaliações. A temperatura é um valor que divide os logits; quando é maior que 1, mantém a ordem das respostas e só achata a distribuição de probabilidade. Eu leio o fato de um único valor ter resolvido em grande parte o problema como sinal de que o descompasso não estava concentrado em poucas faixas, e sim era a distribuição inteira estreitada para um lado. O artigo também acrescentou a ressalva de que um treino de RL mais intenso poderia distorcer a calibração de formas que não se corrigem assim.</p>
<p>Esse é um resultado sobre modelos da Anthropic, então não dá para transportá-lo diretamente como causa da Figure 8. Os estudos da seção anterior também tratavam de expressões de confiança dentro do texto. Não encontrei uma fonte primária que ligue diretamente o resultado de que as pessoas evitam o tom incerto ao descompasso nas probabilidades dos tokens.</p>
<h2 id="a-probabilidade-que-seu-código-recebe"><a class="anchor" href="#a-probabilidade-que-seu-código-recebe">A probabilidade que seu código recebe</a></h2>
<p>A probabilidade da Figure 8 é a log-probabilidade de um token. Para receber um valor do mesmo tipo, conforme a <a href="https://github.com/openai/openai-openapi/blob/506aff0a8099581b50e119b87f8f2692cdad043f/openapi.yaml" target="_blank" rel="noopener noreferrer">especificação da API da OpenAI</a>, você define <code>logprobs</code> como <code>true</code> numa requisição de Chat Completions e usa <code>top_logprobs</code> para escolher, entre 0 e 20, quantos candidatos receber em cada posição de token. O valor que você obtém pedindo ao modelo que diga um número de confiança junto com a resposta é confiança expressa em palavras, e é um valor diferente da probabilidade do token.</p>
<p>Os estudos sobre esses dois valores comparam pares diferentes. <a href="https://arxiv.org/abs/2305.14975" target="_blank" rel="noopener noreferrer">Tian et al. 2023</a> relataram que, dentro de modelos RLHF como ChatGPT, GPT-4 e Claude, a confiança expressa em palavras costumava ser mais bem calibrada que as probabilidades condicionais, muitas vezes reduzindo o ECE em cerca de 50% relativo em três benchmarks. Mas, nessa comparação, a probabilidade condicional dos modelos cujos pesos não são públicos não era a probabilidade de tokens da API, e sim uma estimativa: a fração de vezes que a resposta saiu ao amostrar a mesma pergunta 10 vezes. Leng et al., vistos acima, relataram que, comparados a modelos anteriores ao RLHF, os modelos RLHF mostram mais overconfidence na confiança que expressam em palavras. Os dois resultados podem valer ao mesmo tempo. E 0.074 é o valor que o modelo post-training do GPT-4 de 2023 obteve num subconjunto do MMLU, então não dá para transportá-lo como está para os modelos que você chama hoje pela API.</p>
<h2 id="para-fechar"><a class="anchor" href="#para-fechar">Para fechar</a></h2>
<p>Resumindo, calibração não é sobre quantos por cento você acerta, e sim sobre saber quantos por cento vai acertar, e o ECE é o número que mede o tamanho desse descompasso. Para ver a direção, é preciso olhar o reliability diagram ou a confiança média menos a acurácia. O RLHF empurra para cima a nota de um modelo de recompensa que imita a preferência humana. Kadavath et al. entendem que essa otimização tende a estreitar as previsões na direção do que recebe mais recompensa. Há pesquisas mostrando que as pessoas escolhem menos as respostas que revelam incerteza e que modelos de recompensa dão nota maior a respostas que declaram um número de confiança alto, independentemente da qualidade. No GPT-4, o ECE ficou registrado indo de 0.007 para 0.074 depois do post-training, mas a OpenAI não chegou a escrever a causa. Como a calibração também sai diferente conforme a probabilidade que o seu código recebe seja uma probabilidade de tokens ou uma confiança dita em palavras, seja qual for, você precisa medi-la de novo nos seus próprios dados.</p>
<p>Se as probabilidades do Jev, um modelo de decisão que apresenta a calibração como objetivo de treino, são de fato honestas é o tema de <a href="/260922" target="_blank" rel="noopener noreferrer">Modelos de decisão, Jev e Kev</a>.</p>
<p>Espero que você também, antes de cravar no código como limiar a probabilidade que um modelo devolve, verifique ao menos uma vez com que objetivo esse modelo foi treinado e se a escala dele se sustenta nos seus próprios dados.</p>]]></content:encoded>
            <author>jihoon7705@gmail.com (이지훈)</author>
            <category>IA</category>
            <category>Calibração</category>
        </item>
        <item>
            <title><![CDATA[Linguagem compartilhada]]></title>
            <link>https://hooninedev.com/pt-BR/260723</link>
            <guid isPermaLink="false">https://hooninedev.com/pt-BR/260723</guid>
            <pubDate>Thu, 23 Jul 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Neste post, quero falar sobre a comunicação com outras áreas. Enquanto desenvolvo, dedico tanto tempo a descobrir o que precisa ser construído quanto a escrever código. Troco requisitos com o product ...]]></description>
            <content:encoded><![CDATA[<p>Neste post, quero falar sobre a comunicação com outras áreas.</p>
<p>Enquanto desenvolvo, dedico tanto tempo a descobrir o que precisa ser construído quanto a escrever código. Troco requisitos com o product manager, alinho telas com o designer e pergunto a quem conhece o domínio o que cada termo significa. E já vivi algumas vezes que o custo de esse processo sair do lugar é muito maior do que o custo de escrever o código errado.</p>
<p>Recentemente li <a href="https://softwareleads.substack.com/p/engineering-leaders-day-to-day-activities" target="_blank" rel="noopener noreferrer">As atividades cotidianas de um líder de engenharia</a>, escrito por James Samuel. O autor divide o trabalho de um líder em seis frentes, e a primeira que ele trata é a coleta de informação. A razão é que toda decisão, toda direção e toda ação dependem de um entendimento preciso do que está acontecendo agora.</p>
<p><picture><source type="image/webp" srcset="/content/260723/1.720w.webp 720w, /content/260723/1.1440w.webp 1440w, /content/260723/1.1951w.webp 1951w" sizes="(max-width: 840px) calc(100vw - 32px), 808px"><img src="/content/260723/1.png" alt="As atividades gerenciais desenhadas como seis círculos ligados: coletar informação, planejar, liderar, influenciar decisões, decidir e compartilhar informação" width="1951" height="1673" loading="eager" fetchpriority="high" decoding="async"></picture></p>
<p>Eu quero assumir um papel de liderança algum dia, mas o que me ocorreu ao ler essa passagem foi um pouco diferente. <strong>Se a primeira coisa que um líder trata é coletar informação, o correspondente disso para quem está na execução é entender requisitos.</strong> O jeito como lido com requisitos hoje será o jeito como vou lidar com a informação de uma organização depois.</p>
<p>Não é minha intenção falar de liderança neste texto. Falo do que eu vinha fazendo errado ao me comunicar com outras áreas na posição em que estou e do que posso usar como ferramenta. Começando pela conclusão: passei a achar que esse problema não é só uma questão de atitude. Existe uma formulação alternativa disponível, chamada ubiquitous language, e vale a pena ver do que se trata.</p>
<h2 id="a-mesma-palavra-significados-diferentes"><a class="anchor" href="#a-mesma-palavra-significados-diferentes">A mesma palavra, significados diferentes</a></h2>
<p>No artigo sobre <a href="https://martinfowler.com/bliki/BoundedContext.html" target="_blank" rel="noopener noreferrer">Bounded Context</a>, Martin Fowler dá o exemplo de uma companhia elétrica. Boa parte dos desalinhamentos que vivi tinha esse formato.</p>
<blockquote class="bilingual-quote"><div class="quote-translation" lang="ko"><p>Aqui a palavra “meter” significava coisas sutilmente diferentes em partes distintas da organização: era a conexão entre a rede e um local, entre a rede e um cliente, ou o medidor físico em si?</p></div><div class="quote-original" lang="en"><p>here the word 'meter' meant subtly different things to different parts of the organization: was it the connection between the grid and a location, the grid and a customer, the physical meter itself</p></div></blockquote>
<p>A mesma palavra, “meter”, significava coisas sutilmente diferentes conforme a área da organização. E Fowler acrescenta que essa confusão não é um acaso daquele caso específico: diz ter visto a mesma confusão se repetir com palavras polissêmicas como Customer e Product.</p>
<p><strong>Isso não aconteceu porque os participantes eram ruins de reunião.</strong> Quando uma organização contém contextos diferentes, é natural que a mesma palavra se divida, e deixar isso correr acaba dividindo também o código.</p>
<p>O pano de fundo técnico dessa história eu tratei uma vez em <a href="/260418" target="_blank" rel="noopener noreferrer">Modelo de domínio</a>. Naquele texto falei de como expressar o modelo dentro do código. Neste, trato da etapa anterior à existência desse modelo, a da conversa entre pessoas.</p>
<h2 id="uma-palavra-com-dois-significados"><a class="anchor" href="#uma-palavra-com-dois-significados">Uma palavra com dois significados</a></h2>
<p>Mas por que isso se repete de forma estrutural? Há duas linhas de explicação.</p>
<p>Uma vem da estrutura organizacional. O artigo <a href="https://melconway.com/Home/Committees_Paper.html" target="_blank" rel="noopener noreferrer">How Do Committees Invent?</a>, que Mel Conway publicou na Datamation em 1968, afirma que, na medida em que uma organização não consegue mudar com flexibilidade sua estrutura de comunicação, essa organização carimba a própria imagem em todos os artefatos de design que produz. A frase única normalmente citada como lei de Conway é, na verdade, uma formulação que o próprio autor elaborou mais tarde, e essa descrição do texto de 1968 me parece mais direta.</p>
<p>O que li em chave prática nessa frase é a expressão “carimba”. Significa que a divisão dos termos não termina na conversa, mas <strong>permanece nos artefatos</strong>. O “meter” acima é exatamente isso. Se cada um constrói mantendo um entendimento diferente do escopo de “meter”, essa diferença não fica na ata da reunião, e sim nos nomes de tabela, nos campos da resposta da API e nos textos da tela. E a partir daí o custo de corrigir não tem comparação com o custo de ter mais uma conversa.</p>
<p>É aqui que se separa o que quem está na execução pode e não pode fazer. Mudar a estrutura de comunicação da organização foge ao alcance de quem executa. Mas <strong>tornar visíveis as fronteiras dos termos</strong> é possível a partir do lugar em que você está hoje. Consiste em anotar qual é o escopo de uma palavra neste documento e explicitar quando ela difere da palavra usada por outro time. Todas as ferramentas que veremos adiante são dispositivos que facilitam esse trabalho.</p>
<p>A outra explicação é um pouco mais fundamental. Do ponto de vista da ciência cognitiva, a colaboração só se sustenta sobre um pano de fundo compartilhado. <a href="https://web.stanford.edu/~clark/1990s/Clark,%20H.H.%20_%20Brennan,%20S.E.%20_Grounding%20in%20communication_%201991.pdf" target="_blank" rel="noopener noreferrer">Grounding in Communication</a>, escrito por Herbert Clark e Susan Brennan em 1991, começa com o exemplo de um dueto de piano. Os dois instrumentistas não conseguem nem começar a se coordenar sem pressupor uma quantidade enorme de informação compartilhada, ou seja, common ground. Aqui common ground significa conhecimento mútuo, crenças mútuas e pressupostos mútuos. E os autores afirmam com firmeza que toda ação coletiva é construída sobre o common ground e sobre seu acúmulo.</p>
<p>A perspectiva a que cheguei depois de colocar essas duas linhas lado a lado é esta. <strong>Os termos divergirem não é acidente, é o estado padrão.</strong> Estar alinhado é a exceção, o estado que precisa ser mantido a um custo. Então a pergunta prática passa a ser quem paga esse custo e como.</p>
<h2 id="vigilância-ativa"><a class="anchor" href="#vigilância-ativa">Vigilância ativa</a></h2>
<p>Em Domain-Driven Design, publicado em 2003, Evans apresenta um padrão chamado ubiquitous language, ou <strong>linguagem compartilhada</strong>. Costuma ser resumido como pouco mais que “developers e especialistas de domínio devem usar os mesmos termos”, mas, lendo o livro original, a prescrição é bem mais concreta. Ele pede que o time tome o modelo como espinha dorsal da linguagem e se comprometa a usar essa linguagem de forma persistente em toda a comunicação do time e no código. E a frase que vem em seguida foi a mais importante para mim.</p>
<blockquote class="bilingual-quote"><div class="quote-translation" lang="ko"><p>Os especialistas de domínio contestam termos ou estruturas que sejam desajeitados ou inadequados para transmitir o entendimento do domínio, enquanto os developers ficam atentos a ambiguidades ou inconsistências que vão derrubar o design.</p></div><div class="quote-original" lang="en"><p>Domain experts object to terms or structures that are awkward or inadequate to convey domain understanding, while developers watch for ambiguity or inconsistency that will trip up design.</p></div></blockquote>
<p>Os especialistas de domínio contestam termos e estruturas desajeitados ou insuficientes para transmitir o entendimento do domínio, e <strong>os developers vigiam a ambiguidade ou inconsistência que vai derrubar o design</strong>.</p>
<p>Ao ler essa frase, corrigi meu entendimento do próprio papel. Até então eu me via como receptor nas reuniões de requisitos. Achava que meu trabalho era receber com exatidão o que o produto decidisse e implementar. Mas a parte que Evans atribui ao developer não era a recepção. <strong>É encontrar a ambiguidade e devolvê-la.</strong> Isso não é cooperação passiva, é vigilância ativa.</p>
<p>(A propósito, Fowler traduziu essa frase no próprio artigo como “Domain experts <strong>should</strong> object ... developers <strong>should</strong> watch”. O livro original não tem verbo modal. É uma diferença pequena, mas o original se lê de forma mais categórica: o tom não é que seria bom fazer assim, e sim que fazer assim é a definição do padrão.)</p>
<p>Então como se encontra a ambiguidade? Evans também responde: usar os termos repetidamente na conversa é o que revela as diferenças de interpretação. Eu li essa frase assim. A ambiguidade não é algo que se acha lendo documentos com lupa. Por mais cuidadosa que seja a leitura de uma especificação, o fato de “meter” ter vários significados não aparece. Essa palavra precisa ser usada repetidamente contra casos concretos para se dividir. O momento em que surge uma pergunta como “o que as outras pessoas entendem por meter?” é exatamente esse ponto.</p>
<h2 id="o-princípio-do-mínimo-esforço-colaborativo"><a class="anchor" href="#o-princípio-do-mínimo-esforço-colaborativo">O princípio do mínimo esforço colaborativo</a></h2>
<p>Podemos conhecer esse método e ainda assim não praticá-lo. Então por que não perguntamos repetidamente?</p>
<p>Voltemos ao artigo de Clark e Brennan. Os autores dão o nome de grounding ao processo de transformar o que foi dito em parte do common ground. E propõem um princípio sobre como as pessoas se comportam nesse processo: o princípio do mínimo esforço colaborativo (the principle of least collaborative effort). Ele nasce da observação de que as pessoas não gostam de trabalhar mais do que o necessário.</p>
<p>Aqui vem a passagem importante.</p>
<blockquote class="bilingual-quote"><div class="quote-translation" lang="ko"><p>Pelo princípio do mínimo esforço colaborativo, as pessoas deveriam tentar fazer grounding com o menor esforço conjunto necessário. Mas o que exige esforço muda dramaticamente conforme o meio de comunicação.</p></div><div class="quote-original" lang="en"><p>By the principle of least collaborative effort, people should try to ground with as little combined effort as needed. But what takes effort changes dramatically with the communication medium.</p></div></blockquote>
<p><strong>O ponto em que o esforço recai muda dramaticamente conforme o meio.</strong> Uma técnica de confirmação disponível em um meio pode ser simplesmente impossível em outro ou, sendo possível, muito mais cara. Os autores apontam em especial que, em meios nos quais o interlocutor não recebe o que foi dito de imediato, o custo de contar que outra pessoa corrija o mal-entendido fica muito alto, então quem fala tende a evitá-lo.</p>
<p>Vale registrar uma coisa aqui. Esse artigo foi escrito em 1991, e os meios comparados pelos autores são a conversa presencial, o telefone, cartas e secretária eletrônica. <strong>Ele não trata de Slack nem de Notion.</strong> O arcabouço de que o custo de grounding varia conforme o meio é dos autores; aplicá-lo aos canais de trabalho assíncronos de hoje é interpretação minha.</p>
<p>Aplicando assim, fica explicado por que eu não perguntava repetidamente. Num ambiente em que você recebe a especificação como texto e confirma como texto, mandar uma mensagem de confirmação a cada ponto ambíguo é caro. Você não sabe quando virá a resposta e teme que perguntar várias vezes faça você parecer alguém que não entendeu. Então a pessoa naturalmente escorrega para a própria interpretação.</p>
<p>Convém separar duas forças aqui. Uma é que o meio encarece a confirmação, e isso é o que dizem Clark e Brennan. A outra é o receio de parecer incompetente por perguntar várias vezes, e isso não é o que o princípio deles prevê, mas pertence à discussão sobre segurança psicológica que vem adiante. Com precisão: o que o princípio do mínimo esforço colaborativo prevê é que <strong>você escolherá um jeito mais barato de confirmar</strong>, não que deixará de confirmar. Abandonar a confirmação e partir para o palpite não é o princípio operando, é o grounding falhando. E o fato de que aquela interpretação estava errada aparece só depois que a implementação termina.</p>
<h2 id="chegar-a-um-nível-suficiente"><a class="anchor" href="#chegar-a-um-nível-suficiente">Chegar a um nível suficiente</a></h2>
<p>Então é preciso perguntar sobre cada ambiguidade? Não é realista, e nunca vi ninguém fazer isso. O artigo também oferece um critério para essa pergunta: o grounding criterion. Ele designa o estado em que ambas as partes acreditam mutuamente que o ouvinte entendeu o que o falante quis dizer <strong>num nível suficiente para o propósito atual</strong>. Os autores o antecedem com a observação de que a compreensão perfeita é impossível desde o início.</p>
<p>O motivo de esse critério ter me parecido prático é que ele baixa a meta. Não é preciso entender os requisitos perfeitamente. <strong>Basta poder confiar mutuamente que vocês se entendem o suficiente para o que estão prestes a fazer.</strong> E os autores dizem que, se o propósito muda, o critério precisa mudar junto.</p>
<p>Na prática, isso vira o critério para calibrar a intensidade da confirmação. Se numa sessão que ainda explora direção você fica preso em “o que meter significa aqui?”, a conversa não anda. O que se precisa nessa etapa é acordo sobre o que se quer fazer e por quê, não a definição dos valores de fronteira. Ao contrário, pouco antes de entrar na implementação essa pergunta tem obrigatoriamente que surgir. Aí é preciso poder confiar que os dois lados estão imaginando o mesmo escopo para “meter”; se não, o código se acumula sobre uma premissa errada.</p>
<p>Por isso, diante da mesma ambiguidade, <strong>eu a trato de forma diferente conforme a etapa em que estou.</strong> Na etapa de exploração eu anoto numa lista e sigo; pouco antes de implementar eu abro essa lista e fecho os itens um a um. Adiar a confirmação e abandonar a confirmação são coisas distintas.</p>
<p>Há uma ferramenta que combina bem neste ponto. Team Topologies, de Matthew Skelton e Manuel Pais, distingue três modos de interação entre times: collaboration, quando por um período definido se descobre algo novo em conjunto; X-as-a-Service, quando um lado provê e o outro consome; e facilitation, quando um lado ajuda e mentora o outro.</p>
<p>Na origem é uma discussão sobre design organizacional, mas achei prático reduzi-la à escala de uma reunião. <strong>Se cada um acredita que esta conversa está num modo diferente, a reunião sai dos trilhos.</strong> Se o product manager acha que é uma sessão para comunicar o que já foi decidido enquanto o developer acha que é uma sessão para descobrir junto, as perguntas do developer soam como implicância, não como colaboração. O contrário também acontece. Se o developer veio receber uma especificação fechada e o product manager ainda estava explorando, o developer se frustra porque não há especificação.</p>
<p>Por isso, ultimamente eu confirmo isso logo no começo da reunião: “isto ainda está aberto ou é uma sessão para confirmar o que já foi decidido?”. Essa única pergunta muda o caráter de tudo o que vem depois. É a mesma coisa que ajustar o grounding criterion ao propósito.</p>
<h2 id="quando-a-resolução-não-bate-a-conversa-gira-em-falso"><a class="anchor" href="#quando-a-resolução-não-bate-a-conversa-gira-em-falso">Quando a resolução não bate, a conversa gira em falso</a></h2>
<p>Mesmo com o modo alinhado, um problema permanece. Outro desalinhamento que vivi com frequência foi falar em <strong>resoluções</strong> diferentes.</p>
<p>Requisitos entregues em prosa pelo product manager costumam ser abstratos demais. De uma frase como “para que o usuário consiga conferir com facilidade o histórico de reservas” não se extrai quantas telas existem nem o que leva a onde. Ao contrário, o layout do designer é concreto demais. Cor de botão e espaçamento já estão definidos, o que torna difícil discutir justamente se este fluxo está certo.</p>
<p>Shape Up, escrito por Ryan Singer na Basecamp, aponta esse problema com precisão: começar por wireframes ou layouts visuais concretos prende você em detalhes desnecessários e impede explorar tão amplamente quanto é preciso. Por isso <a href="https://basecamp.com/shapeup/1.3-chapter-04" target="_blank" rel="noopener noreferrer">o que Shape Up propõe</a> é uma representação intermediária. Chama-se breadboarding e toma o conceito emprestado da engenharia elétrica. Um breadboard é um protótipo que tem todas as peças e a fiação do aparelho real, mas nenhum design industrial. Por isso se desenham exatamente três coisas: os lugares para onde se pode navegar (places), aquilo que o usuário pode acionar (affordances) e as linhas de conexão que mostram para onde essa ação leva o usuário (connection lines).</p>
<p>O motivo de eu ter gostado dessa técnica é que ela é um artefato que o developer consegue produzir. Em vez de pedir ao product manager que escreva com mais detalhe ou de esperar o layout do designer, você desenha ali mesmo o fluxo como o entendeu e devolve com um “entendi assim, está certo?”. É um jeito concreto de executar a vigilância de que Evans falava, a de devolver a ambiguidade. E, pela lente da seção anterior, é um dispositivo que barateia o grounding. Um desenho substitui várias rodadas de confirmação por texto.</p>
<p>Mas essa resolução é mais difícil de sustentar do que parece. Ao desenhar o fluxo, você desce naturalmente para coisas como “não seria melhor colocar este botão embaixo à direita?”. Comigo aconteceu algumas vezes. Uma vez que você desce assim, a pergunta que estava em discussão, ou seja, se este fluxo está certo, some em silêncio. Se você define a posição dos botões enquanto o fluxo ainda não está fechado, toda aquela discussão é jogada fora por inteiro quando o fluxo mudar depois.</p>
<p>Por isso, nesta etapa, tento manter <strong>um desenho deliberadamente ruim</strong>. Se há apenas retângulos, setas e nomes, a outra pessoa nem cogita apontar detalhes e responde só ao fluxo. O baixo acabamento do desenho é, nesta ferramenta, uma funcionalidade. (Segurar a vontade de desenhar bonito é mais difícil do que parece)</p>
<p>Que o próprio nível de abstração possa ser uma ferramenta de colaboração se conecta ao que tratei em <a href="/260201" target="_blank" rel="noopener noreferrer">Abstração</a>. Lá eu falava do nível de abstração do código; na conversa existe a mesma coisa.</p>
<h2 id="separar-regras-exemplos-e-perguntas"><a class="anchor" href="#separar-regras-exemplos-e-perguntas">Separar regras, exemplos e perguntas</a></h2>
<p>Outra maneira de alinhar a resolução é estruturar a própria discussão.</p>
<p>Vejamos o <a href="https://cucumber.io/blog/bdd/example-mapping-introduction/" target="_blank" rel="noopener noreferrer">Example Mapping</a>, apresentado em 2015 por Matt Wynne, que liderou o projeto Cucumber. Ele diagnostica que a razão de muitos times terem dificuldade com discussões de requisitos é a falta de estrutura, que as torna longas e entediantes. E por isso os times deixam de fazê-las com regularidade e com consistência. Comigo era exatamente assim: quando uma reunião de requisitos se alonga, na próxima você tenta terminar rápido, e terminando rápido as ambiguidades ficam como estavam.</p>
<p>O Example Mapping divide a discussão em cartões de quatro cores. O amarelo é a história em pauta, o azul é a regra ou critério de aceitação, o verde é o exemplo concreto que ilustra essa regra e o vermelho é a pergunta cuja resposta ninguém sabe.</p>
<p>Entre eles, <strong>acho que o cartão vermelho é o essencial.</strong> Os outros três são técnicas de organização, mas o vermelho é o dispositivo que transforma o “não sei” em um produto oficial da reunião. Dizer que não sabe deixa de ser um ato que atrasa a reunião e passa a ser um resultado que a reunião deve produzir.</p>
<p>Por que isso importa fica claro ao imaginar o caso oposto. Numa reunião sem cartões vermelhos, esbarrar num ponto ambíguo deixa duas opções: insistir agora e esticar a reunião, ou seguir em frente e interpretar sozinho depois. Como vimos na seção anterior, as pessoas em geral escolhem a segunda. Mas com um cartão à mão surge uma terceira opção. <strong>Anotar e seguir, sem deixar que aquilo desapareça.</strong> A reunião continua andando, e a ambiguidade fica numa lista em vez de sumir.</p>
<p>O cartão verde cumpre papel parecido. Quando só a regra é escrita, todo mundo concorda; no momento em que alguém tenta escrever um exemplo concreto que a ilustre, as interpretações se separam. É o “usar os termos repetidamente na conversa revela as diferenças”, de Evans, transposto para o formato de reunião. Se você pede três exemplos por regra, em um deles costuma aparecer um “peraí, e neste caso, como fica?”. (Pela minha experiência, a reunião em que essa pergunta não aparece não é uma reunião em que todos entenderam, e sim uma em que cada um entendeu de um jeito)</p>
<p>Com uma ideia parecida existe também o Three Amigos, usado há tempos no meio de BDD. A proposta é que as três perspectivas de negócio, desenvolvimento e teste façam cada uma uma pergunta diferente, e o <a href="https://www.agilealliance.org/glossary/three-amigos/" target="_blank" rel="noopener noreferrer">glossário da Agile Alliance</a> organiza essas perguntas como qual problema se está tentando resolver, como construir uma solução para resolvê-lo e, quanto a isto, o que poderia acontecer. O que acontece com uma reunião em que a terceira pergunta nunca aparece provavelmente não precisa de explicação.</p>
<h2 id="crie-um-processo-para-verificar-o-que-não-se-sabe"><a class="anchor" href="#crie-um-processo-para-verificar-o-que-não-se-sabe">Crie um processo para verificar o que não se sabe</a></h2>
<p>Mas por que temos tanta dificuldade de puxar o cartão vermelho? Aqui não dá para desviar do assunto da segurança psicológica.</p>
<p>O <a href="https://web.mit.edu/curhan/www/docs/Articles/15341_Readings/Group_Performance/Edmondson%20Psychological%20safety.pdf" target="_blank" rel="noopener noreferrer">artigo</a> que Amy Edmondson publicou na Administrative Science Quarterly em 1999 é o estudo que estabeleceu formalmente o conceito de segurança psicológica de time. (O próprio artigo aponta como raiz o trabalho de Schein e Bennis de 1965.) Ele define segurança psicológica de time como a crença compartilhada de que o time é um lugar seguro para assumir riscos interpessoais. Dizer que não sabe, discordar e admitir um erro entram aí. O artigo pesquisou 51 times de uma empresa industrial e mostrou que a segurança psicológica se associa ao comportamento de aprendizado e que esse comportamento medeia a relação entre segurança psicológica e desempenho do time.</p>
<p>Mas a frase que achei mais prática nesse artigo não foi a definição, e sim a que vem logo depois.</p>
<blockquote class="bilingual-quote"><div class="quote-translation" lang="ko"><p>Na maior parte das vezes, essa crença tende a ser tácita: é tomada como dada e não recebe atenção direta nem dos indivíduos nem do time como um todo.</p></div><div class="quote-original" lang="en"><p>For the most part, this belief tends to be tacit—taken for granted and not given direct attention either by individuals or by the team as a whole.</p></div></blockquote>
<p>Essa crença costuma ser <strong>tácita</strong>, e nem os indivíduos nem o time lhe dão atenção direta.</p>
<p>Daqui em diante é conjectura minha. O que o artigo diz vai até a crença ser tácita; a prescrição de que, portanto, basta criar um formulário não está no artigo. Dito isso, já vi várias vezes que o tácito não muda por declaração. Dizer “nosso time é um time em que dá para perguntar o que você não sabe” não muda grande coisa sozinho. Por isso acho que o cartão vermelho da seção anterior importa. <strong>Se você transforma o “não sei” em um campo dentro de um formulário, preenchê-lo vira procedimento em vez de coragem.</strong></p>
<p>Cabe uma objeção, claro. Num time que não é seguro, o campo do cartão vermelho vai simplesmente ficar vazio. É uma crítica justa, e não pretendo afirmar que esta ferramenta cria segurança. Mas, se o campo existe, ao menos <strong>o fato de estar vazio se torna visível.</strong> Cria-se um lugar para perguntar se está vazio porque ninguém tem dúvida ou porque é difícil falar.</p>
<p><picture><source type="image/webp" srcset="/content/260723/3.720w.webp 720w, /content/260723/3.1440w.webp 1440w, /content/260723/3.1680w.webp 1680w" sizes="(max-width: 840px) calc(100vw - 32px), 808px"><img src="/content/260723/3.jpg" alt="O modelo Bounded Context Canvas da comunidade DDD, com campos para propósito, classificação estratégica, linguagem ubíqua, premissas, métricas de verificação e questões em aberto" width="1680" height="1100" loading="lazy" fetchpriority="low" decoding="async"></picture></p>
<p>A mesma ideia está incorporada em formatos de documento reais. Olhando o <a href="https://github.com/ddd-crew/bounded-context-canvas" target="_blank" rel="noopener noreferrer">Bounded Context Canvas</a> criado pela comunidade de DDD, é uma ferramenta colaborativa para projetar e documentar um contexto, e a composição dos campos é interessante. Nome e propósito, classificação estratégica, papel no domínio, comunicação de entrada e de saída, e então <strong>Ubiquitous Language</strong>, decisões de negócio, <strong>Assumptions</strong>, métricas de verificação e <strong>Open Questions</strong>.</p>
<p>Há um campo separado para escrever a linguagem compartilhada, outro para os pressupostos e outro para as perguntas em aberto. A descrição do canvas diz que o próprio ato de escrever o propósito força um pensamento nebuloso a ser dito com clareza e coloca o time inteiro na mesma página.</p>
<p>Acho que essa é a abordagem mais realista para problemas de comunicação. Em vez de tentar mudar a atitude das pessoas, <strong>criar um campo para aquilo que é difícil de dizer.</strong> Uma boa estrutura não exige que as pessoas façam bem, ela torna fácil fazer bem.</p>
<h2 id="por-que-isso-não-é-questão-de-gosto"><a class="anchor" href="#por-que-isso-não-é-questão-de-gosto">Por que isso não é questão de gosto</a></h2>
<p>Chegando aqui, cabe uma reação: bonito, mas no fim das contas é “vamos nos comunicar com empenho”, não? E isso não é traço de personalidade?</p>
<p>Também pensei assim por um tempo. Mas sobre essa parte existem dados.</p>
<p>Quando a DORA trata de cultura organizacional, ela toma emprestada a classificação do sociólogo Ron Westrum: pathological, orientada a poder; bureaucratic, orientada a regras; e generative, orientada a resultado. E <a href="https://dora.dev/capabilities/generative-organizational-culture/" target="_blank" rel="noopener noreferrer">a documentação oficial da DORA</a> resume assim os próprios achados.</p>
<blockquote class="bilingual-quote"><div class="quote-translation" lang="ko"><p>Uma cultura organizacional de alta confiança que enfatiza o fluxo de informação é preditiva do desempenho de entrega de software.</p></div><div class="quote-original" lang="en"><p>organizational culture that is high-trust and emphasizes information flow is predictive of software delivery performance</p></div></blockquote>
<p>Uma cultura organizacional de alta confiança que <strong>enfatiza o fluxo de informação</strong> prediz o desempenho de entrega de software. Não é dizer que times que se comunicam bem ficam de bom humor, e sim que essa é uma variável que se move junto com o desempenho.</p>
<p>Dito isso, “predizer” não deve ser lido aqui como causalidade. Os dados da DORA vêm de uma pesquisa em que os mesmos respondentes responderam sobre cultura e sobre desempenho, e é uma correlação em nível organizacional. Permanece a possibilidade de quem está satisfeito com a organização ter avaliado bem os dois lados, e de uma relação em nível organizacional não decorre imediatamente uma prescrição de como um indivíduo deve agir. Então o material permite dizer até aqui: o fluxo de informação não mora só no terreno do gosto, ele está num lugar em que se move junto com o desempenho.</p>
<p>O mesmo documento também organiza as três propriedades da boa informação segundo Westrum: ela responde à pergunta de que quem recebe precisa, chega no momento adequado e é apresentada de um jeito que quem recebe consegue usar com eficácia. Vale reparar que as três colocam o critério em <strong>quem recebe</strong>. Compartilhar muito não é o que faz um fluxo de informação ser bom.</p>
<p>O Project Aristotle do Google é outra fonte citada com frequência. Segundo <a href="https://rework.withgoogle.com/intl/en/guides/understand-team-effectiveness" target="_blank" rel="noopener noreferrer">o que foi publicado no reWork</a>, foram estudados 180 times, rodados mais de 35 modelos estatísticos sobre centenas de variáveis, e apresentados cinco fatores que influenciam a efetividade do time: segurança psicológica, confiabilidade, estrutura e clareza, significado e impacto. E a conclusão foi que importou mais como aquele time trabalhou junto do que quem estava nele.</p>
<p>Ainda assim, quero registrar uma coisa. A página oficial declara que lista os cinco fatores em ordem de importância e coloca a segurança psicológica em primeiro. Mas expressões de magnitude do tipo “primeiro de forma esmagadora”, que aparecem com frequência em textos que citam essa pesquisa, não estão naquela página. Haver uma ordem e um fator dominar os demais são coisas diferentes. Por isso este texto não afirma nada sobre magnitude. Para defender a importância da segurança psicológica, o preciso é se apoiar no artigo original de Edmondson, da seção anterior.</p>
<h2 id="de-volta-ao-assunto-da-liderança"><a class="anchor" href="#de-volta-ao-assunto-da-liderança">De volta ao assunto da liderança</a></h2>
<p>Voltemos ao artigo citado no começo.</p>
<p>Ao tratar da coleta de informação em primeiro lugar, James Samuel diz que os métodos da fase de execução deixam de funcionar. Como contribuidor individual você tem o quadro completo do próprio trabalho, mas, ao assumir responsabilidade sobre pessoas, não dá mais para usar o método anterior. Por isso ele aponta como capacidade necessária a de filtrar o ruído e sintetizar a informação num quadro coerente da realidade, porque nenhum gestor consegue processar tudo.</p>
<p>Senti que isso é o mesmo que as três propriedades da boa informação de Westrum que vimos antes. Não se trata de coletar muito, e sim de transformar em algo utilizável. E isso não é diferente do que estou fazendo agora ao lidar com requisitos. Separar as partes ambíguas das frases de uma especificação, redesenhá-las como breadboard para confirmar e deixar o que não se sabe como cartão vermelho é exatamente o exercício de filtrar ruído e montar um quadro coerente.</p>
<p>O que o autor deixou sobre tomada de decisão também ficou comigo: esperar por certeza é, em si, uma decisão e carrega um custo. Esperar até que os requisitos fiquem completamente claros é a mesma coisa. Por isso o grounding criterion, suficiente para o propósito atual em vez de compreensão perfeita, se torna o critério prático.</p>
<h2 id="conclusão"><a class="anchor" href="#conclusão">Conclusão</a></h2>
<p>Resumindo.</p>
<p>A falha de comunicação com outras áreas em geral não é só uma questão de atitude. Quando uma organização contém contextos diferentes, a mesma palavra se dividir é o estado padrão, e estar alinhado é a exceção que precisa ser mantida a um custo. A forma de pagar esse custo é a linguagem compartilhada, e dentro dela o papel do developer não é transcrever requisitos, e sim vigiar a ambiguidade e devolvê-la.</p>
<p>Mas devolver dá trabalho, e as pessoas tentam minimizar esse trabalho. Então, em vez de me apoiar na força de vontade, decidi usar três coisas: confirmar primeiro em que modo está esta conversa, desenhar o fluxo na resolução entre a prosa e o layout e devolvê-lo, e criar dentro do formato de reunião um campo para anotar o que não se sabe. Nenhuma das três exige corrigir a atitude; as três tornam a coisa certa mais fácil de fazer.</p>
<p>Ainda não sou líder, e nada do que escrevi aqui foi verificado a partir daquele lugar. Com outro tamanho de time ou outra cultura organizacional, algumas coisas não vão funcionar. Mas há uma de que acho que posso ter certeza agora. O jeito como vou lidar com informação depois não será algo aprendido do zero, e sim o hábito com que hoje lido com requisitos, crescido em escala. Por isso ultimamente tento ter menos vergonha do que antes de perguntar numa reunião “o que você quer dizer com isso?”.</p>
<p>Se para quem lê este texto vier à cabeça alguma palavra de uma reunião recente com a qual você concordou balançando a cabeça sem estar realmente certo, recomendo usar essa palavra mais uma vez na próxima. Usando repetidamente, ela aparece.</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://martinfowler.com/bliki/UbiquitousLanguage.html" target="_blank" rel="noopener noreferrer">Martin Fowler, Ubiquitous Language</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#3b82f6"></span><a href="https://www.domainlanguage.com/ddd/reference/" target="_blank" rel="noopener noreferrer">Eric Evans, Domain-Driven Design Reference</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#3b82f6"></span><a href="https://teamtopologies.com/key-concepts" target="_blank" rel="noopener noreferrer">Team Topologies, Key Concepts</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#3b82f6"></span><a href="https://basecamp.com/shapeup/1.2-chapter-03" target="_blank" rel="noopener noreferrer">Basecamp, Shape Up: Set Boundaries</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#f59e0b"></span><a href="https://www.eventstorming.com/book/" target="_blank" rel="noopener noreferrer">Alberto Brandolini, EventStorming</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#f59e0b"></span><a href="https://domainstorytelling.org/" target="_blank" rel="noopener noreferrer">Stefan Hofer, Henning Schwentner, Domain Storytelling</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#f59e0b"></span><a href="https://gojko.net/books/specification-by-example/" target="_blank" rel="noopener noreferrer">Gojko Adzic, Specification by Example</a></div>
</div>
</div></details>]]></content:encoded>
            <author>jihoon7705@gmail.com (이지훈)</author>
            <category>colaboração</category>
            <category>domínio</category>
            <category>DDD</category>
            <category>comunicação</category>
        </item>
        <item>
            <title><![CDATA[Engenharia de Harness (Sistemas)]]></title>
            <link>https://hooninedev.com/pt-BR/260622</link>
            <guid isPermaLink="false">https://hooninedev.com/pt-BR/260622</guid>
            <pubDate>Mon, 22 Jun 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Neste artigo, quero falar sobre prompt engineering, context engineering e o que pode vir depois. Enquanto concluía o artigo anterior sobre como economizar tokens, uma pergunta não saía da minha cabeça...]]></description>
            <content:encoded><![CDATA[<p>Neste artigo, quero falar sobre prompt engineering, context engineering e o que pode vir depois.</p>
<p>Enquanto concluía o artigo anterior sobre <a href="/260611" target="_blank" rel="noopener noreferrer">como economizar tokens</a>, uma pergunta não saía da minha cabeça. Acredito que o centro de gravidade está deixando o trabalho com prompts individuais e passando para a técnica de esvaziar e selecionar informações — context engineering. Quando terminei o texto, a pergunta seguinte surgiu naturalmente: então, o que vem depois do contexto?</p>
<p><img src="/content/260622/3.webp" alt="Arquitetura de 3 camadas para sistemas de IA confiáveis, composta por prompt, context e harness engineering" width="1680" height="920" loading="eager" fetchpriority="high" decoding="async"></p>
<p>É preciso cautela ao falar sobre “a direção do futuro”. Há inúmeras possibilidades pela frente; por isso, o foco aqui é organizar <strong>a mudança de ênfase que pode ser lida em fontes primárias já publicadas</strong>. Embora haja inevitavelmente alguma inferência, espero que o texto seja considerado sob perspectivas diversas.</p>
<h2 id="do-prompt-ao-contexto"><a class="anchor" href="#do-prompt-ao-contexto">Do prompt ao contexto</a></h2>
<p>Comecemos esclarecendo os termos. Por algum tempo, o principal assunto do setor foi <strong>prompt engineering</strong>. A questão era como escrever bem uma única instrução enviada ao model, ou seja, como projetar orientações claras, bons exemplos e o formato de saída.</p>
<p>Depois, a unidade de trabalho cresceu. À medida que agentes que operam por dezenas de turnos se tornaram comuns, o mais importante deixou de ser um único prompt e passou a ser como compor <strong>todo o contexto que o modelo vê em cada turno</strong> (system prompt + definições de ferramentas + histórico da conversa + resultados de busca + memória). Isso é chamado de <strong>context engineering</strong>. A Anthropic organizou esse enquadramento no artigo “Effective context engineering for AI agents”, de setembro de 2025, e o estudo sobre context rot publicado no mesmo ano pela equipe da Chroma (Hong et al.) acrescentou evidências quantitativas. Com 18 modelos, incluindo GPT-4.1, Claude 4, Gemini 2.5 e Qwen3, eles mostraram que o desempenho se degrada de maneira desigual conforme a entrada aumenta, até em tarefas tão simples quanto copiar palavras literalmente. A suposição comum de que um modelo trata da mesma forma o 100º token e o 10,000º token não se sustenta na prática. A conclusão não foi “quanto maior o contexto, melhor”, mas “a forma como a informação é posicionada importa tanto quanto o que ela contém”. Isso reforçou a transição de preencher para selecionar e ajudou o termo a se firmar rapidamente.</p>
<p>Vale apontar um mal-entendido comum. Prompt engineering não foi <strong>substituído</strong> por context engineering. Escrever bons prompts continua sendo fundamental; context engineering se aproxima mais de um conceito superior construído sobre essa base. (O fato de o interesse passar de escrever bom código para projetar bons sistemas não torna coding desnecessário.) A descrição exata, portanto, não é “trocamos um pelo outro”, mas <strong>“o escopo se ampliou incluindo o anterior”</strong>.</p>
<p>Voltemos à pergunta: essa ampliação terminou no contexto? Não parece.</p>
<h2 id="o-blog-de-engenharia-da-anthropic"><a class="anchor" href="#o-blog-de-engenharia-da-anthropic">O blog de engenharia da Anthropic</a></h2>
<p>Acredito que a forma mais honesta de estimar a direção seja ler, em ordem cronológica, o que escrevem as organizações que de fato impulsionam a área. Ao acompanhar o blog de engenharia da Anthropic depois de “Effective context engineering” (setembro de 2025), os próprios títulos começam a desenhar para onde o centro de gravidade seguiu.</p>
<ul>
<li>Outubro de 2025, Equipando agentes com Agent Skills</li>
<li>Novembro de 2025, Code execution with MCP: agentes mais eficientes</li>
<li>Novembro de 2025, Effective harnesses for long-running agents</li>
<li>Janeiro de 2026, Demystifying evals for AI agents</li>
<li>Janeiro de 2026, Projetando avaliações técnicas AI-resistant</li>
<li>Fevereiro de 2026, Quantificando o ruído de infraestrutura em avaliações de agentic coding</li>
<li>Março de 2026, Harness design for long-running application development</li>
<li>Abril de 2026, Scaling Managed Agents: separando cérebro e mãos</li>
<li>Maio de 2026, Como conter Claude em todos os produtos</li>
</ul>
<p>Olhando a lista com certa distância, as palavras-chave se agrupam em três ramos: <strong>harness</strong>, <strong>eval</strong> e <strong>containment</strong>. Minha leitura é que a discussão subiu um nível: de como preencher e esvaziar o contexto para como projetar, medir e controlar todo o sistema de agentes. (Naturalmente, há a limitação de serem as prioridades de uma única empresa. Ainda assim, considerando o peso dela no ecossistema de coding agents, é difícil tratá-las apenas como interesses de um vendor.)</p>
<p>Vamos examinar cada ramo.</p>
<h2 id="harness"><a class="anchor" href="#harness">harness</a></h2>
<p>A palavra harness talvez seja pouco familiar. Literalmente, é o arreio colocado em um cavalo para dirigir sua força. Em um agente de IA, harness é <strong>toda a estrutura externa que envolve o model e o coloca para trabalhar</strong>: quais tools ele pode usar e em que ordem, como se recupera de uma falha, até onde vão suas permissões e quando o loop termina.</p>
<p>Se context engineering pergunta “o que mostrar ao model?”, o design de harness pergunta “como fazer o model se mover dentro desse ambiente?”. Ele está uma camada mais para fora. Se o model fosse uma pessoa recém-contratada e muito capaz, o contexto seriam os materiais de trabalho entregues a ela, enquanto o harness seria o ambiente e o manual de procedimentos. A mesma pessoa pode apresentar resultados instáveis em um ambiente caótico e ir muito mais longe, com as mesmas habilidades, sobre um processo bem projetado.</p>
<p>A importância disso aparece claramente em <a href="https://www.anthropic.com/engineering/effective-harnesses-for-long-running-agents" target="_blank" rel="noopener noreferrer">uma falha enfrentada diretamente pela equipe de engenharia da Anthropic</a>. Eles colocaram seu principal coding model, Opus 4.5, sobre o Claude Agent SDK, forneceram apenas instruções de alto nível, como “crie um clone do claude.ai”, e o executaram em várias sessões. Embora o model fosse inteligente, não produziu um app de nível production. As falhas se repetiam de duas formas. Em uma, a sessão tentava concluir tudo de uma vez, esgotava o contexto no meio da implementação e deixava para a próxima uma funcionalidade inacabada. Na outra, uma sessão posterior percebia que “já houve bastante progresso” e declarava o trabalho concluído apesar das tarefas restantes. (Imagine um trabalho longo passado entre pessoas em turnos, em que cada substituto não tem memória alguma do anterior.)</p>
<p>A solução não foi tornar o model mais inteligente, mas mudar a estrutura. A primeira sessão recebeu um prompt específico para preparar o ambiente (initializer agent), desdobrou mais de 200 especificações funcionais em <code>feature_list.json</code> e criou <code>init.sh</code> para iniciar o servidor de desenvolvimento, além de um log de progresso (<code>claude-progress.txt</code>). Cada sessão seguinte (coding agent) tratava exatamente de uma funcionalidade, deixava um estado limpo por meio de um git commit e notas de progresso, e então terminava. A próxima lia primeiro esse progress file e o git log para entender “até onde chegou o turno anterior” antes de continuar. Era o mesmo model, mas o resultado mudou sobre essa estrutura. O harness, e não o model, decidiu o sucesso. (Curiosamente, as soluções da Anthropic não são novas. Listas de funcionalidades, commits pequenos, notas de progresso e smoke tests executados sempre são exatamente o que uma pessoa experiente em desenvolvimento faz todos os dias. Uma boa estrutura para um agente é, no fim, semelhante a incorporar bons hábitos de engineering ao ambiente.)</p>
<p>A conexão com a economia de tokens também é clara. O isolamento de subagentes, a redução das definições de tools e o model routing abordados no artigo anterior parecem técnicas independentes quando vistos separadamente. Juntos, porém, são partes de <strong>como se projeta um único harness</strong>. Decidir qual model lane recebe cada tarefa, quais tools continuam ativas e onde a exploração verbose é isolada compõe o design de harness. A economia se parece mais com um efeito colateral desse design.</p>
<p>No entanto, o design de harness tem uma armadilha peculiar: <strong>quanto melhor a estrutura, mais provável é que ela envelheça quando o model melhora.</strong> Um harness é, em essência, um conjunto de suposições sobre “o que o model não consegue fazer sozinho”. No momento em que o model passa a fazer essas coisas, as suposições viram excesso de bagagem. Um <a href="https://www.anthropic.com/engineering/managed-agents" target="_blank" rel="noopener noreferrer">caso da Anthropic</a> ilustra isso exatamente. O Sonnet 4.5 tinha o hábito de se apressar para terminar ao se aproximar do limite de contexto — context anxiety —, então a estrutura incorporou um mecanismo de context reset. Quando a mesma estrutura foi usada com o Opus 4.5, esse hábito havia desaparecido, e o reset cuidadosamente adicionado virou peso morto. Cada vez que o model fica um pouco mais capaz, alguma parte da estrutura pode vencer.</p>
<p>Daí surge uma ideia adicional: em vez de aperfeiçoar uma estrutura específica, <strong>projetar interfaces que permaneçam estáveis mesmo quando a estrutura muda</strong>. Managed Agents, da Anthropic, segue essa direção, cujas raízes estão, surpreendentemente, nos sistemas operacionais. O OS sobreviveu por décadas porque virtualizou o hardware em abstrações como processos e arquivos, criando antecipadamente um recipiente para programas que ainda nem existiam. Uma linha de <code>read()</code> funciona da mesma maneira com um disco dos anos 1970 ou um SSD atual. Aplicando o mesmo raciocínio, o agente é dividido em três partes: o <strong>cérebro</strong> que decide (Claude e o harness), as <strong>mãos</strong> que agem (sandbox de execução de código e tools) e o <strong>session log</strong> que registra tudo por append. Com as três partes separadas, se o container morrer, o cérebro pode tratar o evento como um erro de tool call; se o harness morrer, pode despertar novamente a partir do último ponto no session log. Como efeito colateral, custo e latência também caíram. Ao iniciar containers apenas quando realmente necessários, a Anthropic relata que o time to first token (TTFT) caiu cerca de 60% na mediana e, no p95, mais de 90%. (Aqui voltamos ao artigo sobre economia de tokens. Ele recomendava colocar as partes estáticas primeiro para aproveitar prompt caching; decidir “como organizar o contexto para elevar o cache hit rate” é justamente o trabalho do harness ao lado desse cérebro.)</p>
<h2 id="eval"><a class="anchor" href="#eval">eval</a></h2>
<p>O segundo ramo foi o mais interessante para mim. Como se houvesse um acordo, os artigos do início de 2026 convergem para a <strong>avaliação (eval)</strong>.</p>
<p>O motivo é intuitivo. <strong>Como verificamos</strong> se o contexto foi bem composto, o harness bem projetado ou o custo realmente reduzido? Quanto mais longas e complexas são as tarefas que um agente executa de forma autônoma, mais difícil se torna inspecionar cada uma e decidir “isso realmente funcionou bem?”. A base da confiança acaba migrando para a medição. Por isso, questões como “como projetar avaliações de agentes?”, “como remover o ruído da própria avaliação?” e “como lidar com eval awareness, quando o model percebe a avaliação e muda o comportamento?” foram para o primeiro plano.</p>
<p><picture><source type="image/webp" srcset="/content/260622/4.720w.webp 720w, /content/260622/4.1440w.webp 1440w, /content/260622/4.1680w.webp 1680w" sizes="(max-width: 840px) calc(100vw - 32px), 808px"><img src="/content/260622/4.png" alt="Gráfico que contrasta pass@k e pass^k: com mais tentativas, o pass@k sobe para 100% enquanto o pass^k cai para 0%" width="1680" height="946" loading="lazy" fetchpriority="low" decoding="async"></picture></p>
<p>Avaliar agentes é difícil porque isso difere de uma pergunta e resposta pontual. Um agente chama tools e altera estados ao longo de vários turnos; assim, um erro se propaga e se acumula. Além disso, os resultados variam entre execuções mesmo com a mesma entrada. A <a href="https://www.anthropic.com/engineering/demystifying-evals-for-ai-agents" target="_blank" rel="noopener noreferrer">Anthropic</a> divide essa não determinação em duas métricas. <strong>pass@k</strong> é a probabilidade de sucesso pelo menos uma vez em k tentativas, por isso aumenta com mais tentativas. <strong>pass^k</strong> é a probabilidade de sucesso em todas as k tentativas, por isso diminui. Para geração de código que só precisa acertar uma vez, pass@1 importa; para um agente de atendimento que precisa funcionar sempre de modo confiável, pass^k é essencial. (Com taxa de sucesso per-trial de 75%, a probabilidade de três sucessos consecutivos é 0.75³, cerca de 42%. A distância entre “geralmente funciona” e “funciona sempre” é tão grande assim.)</p>
<p>Então, como pontuar uma tentativa? O mesmo artigo divide os graders em três tipos. Os <strong>code-based</strong> (resultado de tests, static analysis e validação de tool calls) são rápidos, baratos e objetivos, mas fracos em tarefas abertas com várias respostas válidas. Os <strong>model-based</strong> (LLM-as-judge e pontuação por rubric) capturam nuances de qualidade, mas não são determinísticos e precisam de calibração periódica com avaliações humanas. Os <strong>human-based</strong> são os mais precisos, porém lentos e caros. Na prática, os três são combinados, de preferência com avaliação determinística como base e avaliação do model como apoio. Há outra distinção. Um <strong>capability eval</strong> pergunta “o que este agente consegue realizar?”, portanto começa com uma pontuação baixa e oferece uma subida. Um <strong>regression eval</strong> pergunta “ele ainda faz o que fazia antes?”, por isso deve permanecer perto de 100%; uma queda indica que algo quebrou.</p>
<p>Aqui há uma armadilha surpreendentemente ignorada: <strong>a pontuação baixa pode ser culpa da avaliação, não do agente.</strong> A Anthropic relata que o Opus 4.5 obteve inicialmente 42% no CORE-Bench. A investigação encontrou pontuação rígida que esperava “96.124991…” e marcava “96.12” como errado, especificações ambíguas e tarefas estocásticas impossíveis de reproduzir. Depois de corrigir bugs e repetir com um scaffold de restrições mais flexíveis, a pontuação saltou para 95%. Por isso, eles enfatizam um princípio: <strong>não aceite a pontuação pelo valor de face; leia você mesmo o transcript (registro de execução).</strong> Se um frontier model fizer 100 tentativas e obtiver 0%, geralmente o problema está quebrado, e não o model incapaz.</p>
<p>Também é interessante que, conforme a medição avança, a própria linha de base se move depressa. No SWE-bench Verified — benchmark de referência para coding agents que fornece issues reais do GitHub e pontua se os tests passam —, os frontier models subiram da faixa de 30% para mais de 80% em 1 ano. Nesse ponto, todos os problemas fáceis estão resolvidos, a pontuação toca o teto (saturation) e surge um paradoxo: grandes avanços de capacidade aparecem apenas como pequenas diferenças. Segundo relatos, uma startup de code review inicialmente desprezou um model novo olhando apenas para avaliações one-shot; só percebeu corretamente a melhoria depois de migrar para avaliações de agentes com tarefas mais longas e complexas. Um eval, portanto, não é criado uma vez e encerrado: torna-se um ativo vivo que precisa ser continuamente substituído por versões mais difíceis. (A Anthropic compara isso ao “Swiss cheese model” da engenharia de segurança. Uma fatia cheia de buracos não basta, mas, ao sobrepor avaliação automática, production monitoring e revisão humana de transcripts, uma falha que atravessa uma camada fica presa na próxima.)</p>
<h2 id="containment"><a class="anchor" href="#containment">containment</a></h2>
<p>O terceiro ramo tem um caráter um pouco diferente. Não trata de custo ou desempenho, mas de <strong>segurança e controle</strong>.</p>
<p>Quanto mais ferramentas um agente recebe e quanto mais autonomia tem, maior é o alcance de um único erro — o blast radius. Para um agente que pode apagar arquivos, enviar solicitações externas e executar ações privilegiadas em nome de alguém, “onde interromper o dano quando algo dá errado?” importa tanto quanto “quão bem ele executa?”. Nesse contexto pode ser lido o destaque dado pela Anthropic, em maio de 2026, ao artigo sobre <a href="https://www.anthropic.com/engineering/how-we-contain-claude" target="_blank" rel="noopener noreferrer">containment em todos os seus produtos</a>. Ele divide o risco dos agentes em três ramos: <strong>user misuse</strong>, quando o usuário pede algo nocivo por malícia ou descuido; <strong>model misbehavior</strong>, quando o modelo age sozinho sem solicitação; e <strong>external attacks</strong> que chegam por ferramentas, arquivos ou redes. Uma observação interessante é que tornar o modelo mais inteligente não reduz apenas o risco. Modelos menos capazes interpretam mal situações e cometem erros óbvios; os mais capazes erram menos, mas encontram melhor caminhos inesperados para contornar restrições que ninguém escreveu explicitamente.</p>
<p>O ponto central enfatizado é o limite de “ter uma pessoa supervisionando cada ação”. Claude Code inicialmente buscava segurança pedindo aprovação para cada escrita, execução e acesso à rede, mas a telemetria mostrou que os usuários simplesmente aprovavam cerca de 93% das solicitações. Quanto mais janelas aparecem, menos atenção cada uma recebe: surge a <strong>approval fatigue</strong>. Uma defesa probabilística dependente de cliques humanos sempre conserva brechas. O centro de gravidade passa de “monitorar o que o agente está fazendo” para “limitar o que ele pode fazer desde o início”. As defesas são empilhadas em três camadas: a <strong>camada de ambiente</strong>, com sandbox, VM e controle de egress; a <strong>camada do model</strong>, com system prompts e classifiers; e a <strong>camada de conteúdo externo</strong>, com MCP, plugins e resultados de busca. O princípio central é <strong>instalar primeiro a camada de ambiente, que bloqueia de forma determinística</strong>. Não porque as defesas do model sejam fracas. De fato, no benchmark da Gray Swan para prompt injection, a taxa de sucesso de um ataque único é de cerca de 0.1%, um resultado de primeira linha. Porém, com 100 tentativas adaptativas, ela sobe para 5–6%, e uma defesa probabilística não consegue, por natureza, atingir 100% de acerto. Por isso, coloca-se no fim uma fronteira rígida. (A introdução de sandbox no nível do OS reduziu as solicitações de aprovação em 84%. O mecanismo de segurança diminuiu a fricção.)</p>
<p><picture><source type="image/webp" srcset="/content/260622/2.720w.webp 720w, /content/260622/2.1440w.webp 1440w, /content/260622/2.1680w.webp 1680w" sizes="(max-width: 752px) calc(100vw - 32px), 720px"><img src="/content/260622/2.png" alt="Defesa de containment em 3 camadas: as camadas do model e de conteúdo externo (probabilísticas) ficam sobre a camada de ambiente (determinística, última linha de defesa)" style="max-width:min(720px, 100%);height:auto;" width="1680" height="1198" loading="lazy" fetchpriority="low" decoding="async"></picture></p>
<p>Embora pareça distante da economia de tokens, ambos compartilham a mesma raiz. Os dois perguntam: <strong>“o que dar ao agente e até onde?”</strong> A Anthropic mostra bem essa conexão com dois casos. Em um, o red team interno fez phishing com um funcionário para que ele executasse Claude Code com um prompt malicioso. Uma instrução discretamente inserida fez o sistema ler <code>~/.aws/credentials</code> e enviar o conteúdo para fora via POST; de 25 tentativas, funcionou 24 vezes. Como o usuário digitou diretamente a instrução, o classifier do model não viu nada suspeito. O que impediu o ataque não foi um model inteligente, mas a fronteira ambiental que mantinha as credenciais fora do sandbox e o controle de egress. O segundo caso é mais sutil. Uma allowlist de egress permitiu corretamente <code>api.anthropic.com</code>, mas um arquivo plantado pelo invasor usou a própria API key dele para chamar a API de upload de arquivos da Anthropic, e os dados saíram para a conta do invasor. O sandbox funcionou perfeitamente, mas os dados ainda vazaram. A lição foi entender a allowlist não como “filtro de destinos”, mas como “permissão para todas as capacidades disponíveis naquele domínio”. (A Anthropic repete este princípio: hypervisors, filtros de syscall e runtimes de containers já verificados resistiram; <strong>o que realmente quebrou foram os componentes construídos por eles sobre essa base</strong>.) Remover tools de MCP não utilizadas pertence ao mesmo contexto: economiza custo e reduz a attack surface. Manter um design leve o torna não apenas mais barato e preciso, mas também mais seguro.</p>
<h2 id="em-resumo"><a class="anchor" href="#em-resumo">Em resumo</a></h2>
<p>Os três ramos podem ser condensados em uma frase. A unidade de atenção se expande um nível de cada vez: <strong>do prompt (uma instrução), para o contexto (o que mostrar em cada turno), e então para todo o sistema de agentes (como operá-lo, medi-lo e contê-lo)</strong>. Harness responde a “como fazê-lo funcionar?”, eval a “como saber se funcionou bem?” e containment a “como detê-lo quando funciona mal?”.</p>
<p><picture><source type="image/webp" srcset="/content/260622/1.720w.webp 720w, /content/260622/1.1440w.webp 1440w, /content/260622/1.1680w.webp 1680w" sizes="(max-width: 752px) calc(100vw - 32px), 720px"><img src="/content/260622/1.png" alt="Expansão em 3 estágios do foco, do prompt ao contexto e então a todo o sistema de agentes, com os ramos harness, eval e containment" style="max-width:min(720px, 100%);height:auto;" width="1680" height="1122" loading="lazy" fetchpriority="low" decoding="async"></picture></p>
<p>Vale reforçar: esses três ramos são <strong>uma mudança de ênfase que o autor infere de materiais já publicados</strong>, não uma previsão como “isso será o padrão no segundo semestre de 2026”. Algumas correntes crescerão, enquanto outras serão absorvidas sob nomes diferentes. O que está claro é que nem o prompt nem o contexto desaparecerão: continuarão como partes de uma estrutura maior. A área avançou acrescentando uma camada sobre as anteriores, e não apagando palavras antigas com novas, e provavelmente continuará assim.</p>
<p>Começamos falando de custos e chegamos até aqui. O artigo anterior recomendava abrir o livro de contas de tokens; este sugere ir um passo além. Vale examinar em conjunto <strong>como verificar</strong> a economia (eval), <strong>sobre qual estrutura ela pode ser repetida</strong> (harness) e <strong>até onde essa estrutura é segura</strong> (containment). No fim, o que deve durar mais não é uma técnica específica de economia, mas o hábito de medir e controlar o próprio sistema.</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://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents" target="_blank" rel="noopener noreferrer">Anthropic, Effective context engineering for AI agents</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#f59e0b"></span><a href="https://www.anthropic.com/engineering/harness-design-long-running-apps" target="_blank" rel="noopener noreferrer">Anthropic, Harness design for long-running application development</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#f59e0b"></span><a href="https://research.trychroma.com/context-rot" target="_blank" rel="noopener noreferrer">Chroma Research, Context Rot</a></div>
</div>
</div></details>]]></content:encoded>
            <author>jihoon7705@gmail.com (이지훈)</author>
            <category>IA</category>
            <category>Agentes</category>
        </item>
        <item>
            <title><![CDATA[Kalyx]]></title>
            <link>https://hooninedev.com/pt-BR/260617</link>
            <guid isPermaLink="false">https://hooninedev.com/pt-BR/260617</guid>
            <pubDate>Wed, 17 Jun 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Neste post, quero falar sobre o Kalyx, a biblioteca headless de DatePicker para React que eu criei. Este texto é uma reescrita de uma retrospectiva que publiquei em junho de 2026. A frase que abria o ...]]></description>
            <content:encoded><![CDATA[<p>Neste post, quero falar sobre o <strong>Kalyx</strong>, a biblioteca headless de DatePicker para React que eu criei.</p>
<p>Este texto é uma reescrita de uma retrospectiva que publiquei em junho de 2026. A frase que abria o post original, "sete pickers em uma única API, menores que um único calendário de uma biblioteca concorrente", ao ser verificada de novo, se mostrou metade errada e metade irrelevante como diferencial. Por isso reorganizo tudo nesta ordem: por que criei, em que difere das opções existentes e qual é a definição técnica.</p>
<p>Adiantando a conclusão: o que diferencia o Kalyx não é a quantidade de componentes, e sim o <strong>modelo de valor</strong>. As versões de referência são <code>@kalyx/react</code> 1.4.7 e <code>@kalyx/core</code> 1.4.8 (MIT, apenas React 19), e os trechos de código vêm do <a href="https://github.com/jiji-hoon96/kalyx" target="_blank" rel="noopener noreferrer">repositório no GitHub</a> na <code>main</code> de 2026-09-16 (<code>0bb302e</code>).</p>
<h2 id="eu-queria-usar-seletores-de-datas-de-forma-declarativa"><a class="anchor" href="#eu-queria-usar-seletores-de-datas-de-forma-declarativa">Eu queria usar seletores de datas de forma declarativa</a></h2>
<p>Tive dois motivos para criá-lo. Eu queria usar bibliotecas de datas complexas e difíceis de usar de um jeito mais declarativo e simples, e queria aprender como uma biblioteca dessas é construída por dentro. "Difícil de usar" é vago, então voltei às definições de tipos de cada biblioteca para conferir se os lugares onde travei ainda existem nas versões mais recentes.</p>
<h3 id="uma-api-que-liga-modos-com-props"><a class="anchor" href="#uma-api-que-liga-modos-com-props">Uma API que liga modos com props</a></h3>
<p>O react-datepicker liga a seleção de horário com <code>showTimeSelect</code>, a de mês com <code>showMonthYearPicker</code>, a de ano com <code>showYearPicker</code> e a de intervalo com <code>selectsRange</code>. É uma estrutura em que um mesmo componente vira outra coisa conforme a combinação de props. O custo aparece nos tipos. Nas definições de tipos da 9.1.0, conforme o valor de <code>selectsRange</code>, a assinatura de <code>onChange</code> se ramifica.</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">// react-datepicker/dist/index.d.ts (발췌)</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    selectsRange</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">    selectsMultiple</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">?:</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> false</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">    formatMultipleDates</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">?:</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> never;</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    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">date</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">Date</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:#B392F0;--shiki-light:#6F42C1">Date</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:#FFAB70;--shiki-light:#E36209">event</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">MouseEvent</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">HTMLElement</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"> React</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">KeyboardEvent</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">HTMLElement</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"> void</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span></code></pre></figure>
<p>É uma union bem feita, mas os ramos se multiplicam à medida que os modos aumentam, e quem usa precisa deduzir qual combinação está ativa só pelos nomes das props. (Tem o mesmo formato de "uma abstração que agrupa as coisas erradas aumenta o acoplamento", tema do meu post sobre <a href="/260201" target="_blank" rel="noopener noreferrer">abstração</a>.) Eu queria que "um campo de entrada, um popover e um calendário" pudessem ser lidos na própria estrutura do JSX.</p>
<h3 id="onde-tipos-de-valor-e-fuso-horário-vazam"><a class="anchor" href="#onde-tipos-de-valor-e-fuso-horário-vazam">Onde tipos de valor e fuso horário vazam</a></h3>
<p>O react-datepicker e o react-day-picker trocam objetos <code>Date</code> nativos. Os dois têm, sim, uma prop <code>timeZone</code> que aceita fusos IANA. O react-datepicker exige a peer opcional <code>date-fns-tz</code> e a do react-day-picker é experimental, mas, em qualquer um dos dois, o tipo do valor continua sendo <code>Date</code>. Como um <code>Date</code> é interpretado no fuso horário local do ambiente de execução, em Seul <code>new Date(2026, 3, 15)</code> passado por <code>toISOString()</code> resulta em <code>2026-04-14T15:00:00.000Z</code>. Você escolhe 15 de abril e o servidor enxerga dia 14. A issue <a href="https://github.com/Hacker0x01/react-datepicker/issues/1018" target="_blank" rel="noopener noreferrer">"Date Selected is One Day Off"</a> do react-datepicker foi aberta em setembro de 2017 e fechada em dezembro de 2025.</p>
<p>Do outro lado, Ark UI e React Aria usam objetos <code>@internationalized/date</code> como <code>CalendarDate</code> e <code>ZonedDateTime</code>. A semântica é precisa, mas em um app em que o estado dos formulários e as respostas do servidor são todos strings, surge código de conversão em cada fronteira.</p>
<p>O suporte a fusos horários também pode ficar preso à escolha da biblioteca de datas. No código dos adapters do MUI X Date Pickers 9.13.0, os adapters de dayjs, Luxon e Moment têm <code>isTimezoneCompatible = true</code>, e a família date-fns tem <code>false</code>. Um app que usa date-fns precisa trazer mais uma biblioteca de datas para usar a prop <code>timezone</code>.</p>
<p>Os modos estavam espalhados em combinações de props, os valores em objetos <code>Date</code> cuja interpretação depende do ambiente e o suporte a fusos horários na escolha da biblioteca de datas. <strong>Era difícil expressar a intenção em uma única declaração.</strong></p>
<h3 id="aprender-construindo"><a class="anchor" href="#aprender-construindo">Aprender construindo</a></h3>
<p>Um seletor de datas parece pequeno, mas contém aritmética de calendário, locales, fusos horários e horário de verão (DST), navegação por teclado e SSR. Por isso defini como objetivo que, do lado de quem usa, desse para entender o que está sendo construído só olhando o JSX, e que, do lado de quem constrói, o código fosse enxuto o bastante para eu conseguir explicar onde os valores são convertidos.</p>
<p>Então, será que não existia mesmo nenhuma biblioteca que já resolvesse essas necessidades?</p>
<h2 id="em-que-ele-difere-das-opções-existentes"><a class="anchor" href="#em-que-ele-difere-das-opções-existentes">Em que ele difere das opções existentes</a></h2>
<p>A resposta curta é que existia. Comecei acreditando que não havia nenhuma biblioteca headless com vários pickers, mas ao pesquisar de novo essa premissa se mostrou errada.</p>
<h3 id="o-modelo-de-valor-que-cada-opção-escolheu"><a class="anchor" href="#o-modelo-de-valor-que-cada-opção-escolheu">O modelo de valor que cada opção escolheu</a></h3>
<p>O conteúdo abaixo foi verificado em 2026-09-16, instalando a versão mais recente do npm e conferindo as definições de tipos e a documentação oficial. A coluna de tamanho foi medida com o mesmo método explicado mais adiante.</p>
<table>
<thead>
<tr>
<th>Biblioteca</th>
<th>Headless</th>
<th>Tipo de valor</th>
<th>Entrada de horário</th>
<th>Seleção só de mês ou ano</th>
<th>gzip</th>
</tr>
</thead>
<tbody>
<tr>
<td>react-day-picker 10.0.1</td>
<td>Não (inclui CSS)</td>
<td><code>Date</code></td>
<td>Não</td>
<td>Não</td>
<td>20.0KB</td>
</tr>
<tr>
<td>react-datepicker 9.1.0</td>
<td>Não (import de CSS)</td>
<td><code>Date</code></td>
<td><code>showTimeSelect</code></td>
<td>Via props</td>
<td>45.4KB</td>
</tr>
<tr>
<td>MUI X 9.13.0</td>
<td>Não (Material)</td>
<td>Objeto do adapter</td>
<td>TimePicker</td>
<td><code>views</code></td>
<td>113.1KB</td>
</tr>
<tr>
<td>Ark UI 5.39.2</td>
<td>Sim</td>
<td><code>@internationalized/date</code></td>
<td>Segmentos com <code>DateInput</code> à parte (fora do tamanho)</td>
<td><code>minView</code></td>
<td>42.7KB</td>
</tr>
<tr>
<td>React Aria Components 1.21.1</td>
<td>Sim</td>
<td><code>@internationalized/date</code></td>
<td>Segmentos de <code>TimeField</code></td>
<td>Não</td>
<td>75.3KB (DatePicker), 78.8KB (com intervalo e hora)</td>
</tr>
<tr>
<td>Kalyx 1.4.7</td>
<td>Sim</td>
<td>String ISO 8601 UTC</td>
<td>HourList e MinuteList em lista</td>
<td>MonthPicker, YearPicker</td>
<td>18.9KB (DatePicker), 25.6KB (tudo)</td>
</tr>
</tbody>
</table>
<p>O react-day-picker afirma no <a href="https://daypicker.dev/guides/timepicker" target="_blank" rel="noopener noreferrer">guia oficial</a> que "DayPicker does not include a built-in time picker". O tamanho do MUI inclui <code>@mui/material</code> e emotion, então, se o seu app já usa MUI, o aumento é bem menor.</p>
<h3 id="opções-headless-completas-já-existem"><a class="anchor" href="#opções-headless-completas-já-existem">Opções headless completas já existem</a></h3>
<p>As linhas que importam na tabela são Ark UI e React Aria. O <a href="https://ark-ui.com/docs/components/date-picker" target="_blank" rel="noopener noreferrer">Ark UI</a> oferece seleção única, múltipla e de intervalo, além de seleção por mês e ano, com uma API de composição em dot notation como <code>DatePicker.Root</code>. O <a href="https://react-aria.adobe.com/DatePicker" target="_blank" rel="noopener noreferrer">React Aria</a> é a implementação da Adobe focada em acessibilidade. Então o espaço do Kalyx é mais estreito do que eu pensava no início.</p>
<blockquote>
<p>Opções headless completas já existem. Porém, Ark UI e React Aria trocam valores como objetos <code>@internationalized/date</code> e oferecem a entrada de horário como campos segmentados. O Kalyx fixa os valores como strings de instante UTC que vão direto para o JSON, e coloca na mesma API de composição um TimePicker baseado em listas e pickers de mês, ano e semana.</p>
</blockquote>
<p>Ainda assim, "os valores são strings, e isso é bom" é um argumento fraco. Um <code>CalendarDate</code> também vira string com um único <code>toString()</code>. O diferencial não é o formato, e sim <strong>o contrato de que essa string é sempre um momento real (um instante) e nunca se mistura com uma célula do calendário (uma coordenada), e os testes que garantem esse contrato</strong>.</p>
<h3 id="medi-o-tamanho-do-bundle-de-novo-com-um-mesmo-método"><a class="anchor" href="#medi-o-tamanho-do-bundle-de-novo-com-um-mesmo-método">Medi o tamanho do bundle de novo com um mesmo método</a></h3>
<p>O tamanho do bundle no badge do README não era uma quantidade comparável à de outras bibliotecas. Os cerca de 19.5KB do badge são um único arquivo de <code>@kalyx/react</code> em <code>dist</code>, e esse arquivo deixa <code>@kalyx/core</code>, <code>@kalyx/adapter-date-fns</code> e <code>@floating-ui/react</code> como imports externos. Eu tinha colocado esse número ao lado dos números de outras bibliotecas que incluíam suas dependências.</p>
<p>Então medi tudo de novo com esta pergunta: "quanto o bundle cresce quando um app consumidor adiciona uma linha de import?"</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:#79B8FF;--shiki-light:#005CC5">echo</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> "import { DatePicker } from '@kalyx/react'; export default DatePicker;"</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> ></span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> entry.jsx</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">npx</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> esbuild</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> entry.jsx</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> --bundle</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> --minify</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> --format=esm</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> --platform=browser</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> \</span></span>
<span data-line=""><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">  --external:react</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> --external:react-dom</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> --external:react/jsx-runtime</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> |</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> gzip</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> -6</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> |</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> wc</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> -c</span></span></code></pre></figure>
<p>Medição de 2026-09-16 com esbuild 0.28.2; KB são bytes divididos por 1024. No react-datepicker excluí o CSS. O React Aria Components medi duas vezes: com os 12 exports necessários para montar um único DatePicker (<code>DatePicker</code>, <code>DateInput</code>, <code>Calendar</code>, <code>Popover</code>, <code>Dialog</code> etc.) e com 14 exports que acrescentam intervalo e hora (incluindo <code>DateRangePicker</code>, <code>RangeCalendar</code> e <code>TimeField</code>).</p>
<p><picture><source type="image/webp" srcset="/content/260617/1.720w.webp 720w, /content/260617/1.1440w.webp 1440w, /content/260617/1.1680w.webp 1680w" sizes="(max-width: 752px) calc(100vw - 32px), 720px"><img src="/content/260617/1.png" alt="Nas mesmas condições de esbuild, o tamanho cresce nesta ordem: Kalyx DatePicker 18.9KB, react-day-picker 20.0KB, Kalyx completo 25.6KB, Ark UI 42.7KB, react-datepicker 45.4KB, React Aria Components 75.3KB (78.8KB com intervalo e hora), MUI X 113.1KB." style="max-width:min(720px, 100%);height:auto;" width="1680" height="980" loading="eager" fetchpriority="high" decoding="async"></picture></p>
<p>Nas mesmas condições, só uma afirmação é verdadeira. Um DatePicker (18.9KB) tem tamanho parecido com o de <code>DayPicker</code> (20.0KB), e os sete juntos (25.6KB) são maiores. (E mesmo assim, o DayPicker é só o calendário, enquanto o Kalyx DatePicker inclui também o campo de entrada e o popover.) O tamanho é sensível à combinação de imports e ao nível de gzip, então é melhor lê-lo como ordem de grandeza, e tamanho não é o motivo central para escolher o Kalyx.</p>
<h2 id="uma-definição-técnica-do-kalyx"><a class="anchor" href="#uma-definição-técnica-do-kalyx">Uma definição técnica do Kalyx</a></h2>
<blockquote>
<p>O Kalyx é um seletor de datas headless para React que fixa todas as entradas e saídas como strings de instante UTC, confina a conversão entre coordenadas de calendário e instantes a duas funções e verifica essa ida e volta com testes de propriedades em todos os fusos horários que o runtime conhece.</p>
</blockquote>
<p>A API do lado de quem usa é esta. <code>value</code> e <code>onChange</code> usam <code>string | null</code>, e <code>displayTimezone</code> decide o calendário de qual fuso é exibido.</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">DatePicker</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">{iso} </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">{setIso} </span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">displayTimezone</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"America/New_York"</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">DatePicker.Input</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">DatePicker.Popover</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">DatePicker.Calendar</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">DatePicker.Popover</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">DatePicker</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span></code></pre></figure>
<h3 id="valores-são-instantes-células-do-calendário-são-coordenadas"><a class="anchor" href="#valores-são-instantes-células-do-calendário-são-coordenadas">Valores são instantes, células do calendário são coordenadas</a></h3>
<p>Dentro do Kalyx circulam dois tipos de strings ISO com o mesmo formato. Uma <strong>coordenada</strong> é uma célula da grade do calendário; é escrita como <code>YYYY-MM-DDT00:00:00.000Z</code>, mas não tem noção de fuso horário. O cálculo da grade roda só em UTC, então não depende do ambiente de execução. Um <strong>instante</strong> é o valor que sai por <code>onChange</code>: o momento real que corresponde à meia-noite daquele dia em <code>displayTimezone</code>. O mesmo 15 de janeiro é <code>2026-01-15T05:00:00.000Z</code> em Nova York e <code>2026-01-14T15:00:00.000Z</code> em Seul.</p>
<p><picture><source type="image/webp" srcset="/content/260617/2.720w.webp 720w, /content/260617/2.1440w.webp 1440w, /content/260617/2.1640w.webp 1640w" sizes="(max-width: 752px) calc(100vw - 32px), 720px"><img src="/content/260617/2.png" alt="A coordenada de calendário 2026-01-15T00:00:00.000Z vira os instantes de Nova York e Seul por meio de civilMidnightFromUtcDay e volta a ser coordenada por meio de calendarDayFromInstant." style="max-width:min(720px, 100%);height:auto;" width="1640" height="1040" loading="lazy" fetchpriority="low" decoding="async"></picture></p>
<p>Só duas funções de <code>@kalyx/core</code> ligam os dois: <code>civilMidnightFromUtcDay</code>, que transforma coordenada em instante, e sua inversa, <code>calendarDayFromInstant</code>. Esta última lê "que dia é esse instante neste fuso" e escreve de novo como coordenada de meia-noite UTC.</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">// packages/core/src/utils/timezone.ts:187-190</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"> calendarDayFromInstant</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">iso</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> ISODateString</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">timeZone</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"> ISODateString</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"> p</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> partsInTimezone</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">(iso), timeZone);</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  return</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">UTC</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(p.year, p.month </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">, p.day)).</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">toISOString</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>A regra é de direção. Ao escolher uma célula e confirmar o valor, a coordenada vira instante; ao decidir, a partir do valor salvo, qual mês exibir e onde colocar o foco, o instante vira coordenada. O <code>selectDate</code> do Root do DatePicker converte, verifica as restrições com o instante convertido e, se passar, confirma.</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">// packages/react/src/components/DatePicker/Root.tsx:182-194</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> normalized</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  coordinate </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">&#x26;&#x26;</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> displayTimezone</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">    ?</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> civilMidnightFromUtcDay</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(coordinate, displayTimezone)</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">    :</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> coordinate;</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"> (normalized </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">&#x26;&#x26;</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> isDateDisabled</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(normalized, disabledRules, adapter, displayTimezone)) {</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:#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">isControlled) {</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  setUncontrolledValue</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(normalized);</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">onChange</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">?.(normalized);</span></span></code></pre></figure>
<p>Esse não é o único ponto de chamada. Buscando em <code>packages/react/src</code>, as duas funções aparecem espalhadas pelo Root e pelo Calendar de DatePicker, RangePicker e DateTimePicker, pelos Presets, pelos utilitários de navegação por teclado e por seis hooks headless. Mesmo assim, todo caminho que confirma uma célula de data ou decide a visualização passa por uma das duas. Confirmar um horário fica com <code>setTimeInTimezone</code>, que por dentro converte com a mesma função interna, <code>resolveCivilDateTime</code>.</p>
<p>Esse contrato é protegido por testes baseados em propriedades (property-based tests). Para toda coordenada <code>c</code> e todo fuso <code>z</code>, <code>calendarDayFromInstant(civilMidnightFromUtcDay(c, z), z) === c</code> precisa valer.</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">// packages/core/src/__tests__/timezone.property.test.ts:86-94</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">it</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'round-trips every UTC calendar coordinate through civil midnight'</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">  fc.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">assert</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    fc.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">property</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">utcCalendarCoordinate</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(), </span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">zone</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(), (</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">coordinate</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">timeZone</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"> instant</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> civilMidnightFromUtcDay</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(coordinate, timeZone);</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">      expect</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">calendarDayFromInstant</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(instant, timeZone)).</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">toBe</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(coordinate);</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    }),</span></span>
<span data-line=""><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">    RUNS</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 <a href="https://fast-check.dev/" target="_blank" rel="noopener noreferrer">fast-check</a> gera datas aleatórias entre 2020 e 2045 e as combina com 14 fusos representativos, como Kathmandu (+5:45), Kiritimati (+14) e Niue (-11), em 300 execuções. O teste logo em seguida verifica a mesma ida e volta 12 vezes por fuso para todos os fusos retornados por <code>Intl.supportedValuesOf('timeZone')</code>. No meu Node 24.16 local, essa lista tem 418 fusos. A 1.4.8 acrescentou um teste exaustivo que compara, em cada transição de horário de verão, a conversão dos horários-limite com uma implementação do Temporal.</p>
<p>Então por que a biblioteca não normaliza sozinha para instante um <code>"2026-01-15T00:00:00.000Z"</code> passado pelo usuário? Esse caminho está fechado, porque só pela string não dá para saber se é coordenada ou instante. Se o valor de Seul <code>2026-01-14T15:00:00.000Z</code>, que já é um instante, passar de novo por <code>civilMidnightFromUtcDay</code>, vira <code>2026-01-13T15:00:00.000Z</code>; em fusos com offset positivo como Seul, a data continua voltando um dia a cada render. (O valor de Nova York não muda se passar de novo.) Trocar por <code>startOfDayInTimezone</code>, que dá o mesmo resultado não importa quantas vezes seja aplicado, elimina o deslocamento, mas deixa de fazer o trabalho original de transformar coordenada em instante.</p>
<p>Por isso desisti da normalização e fixei o contrato na documentação. Quem usa precisa repassar exatamente o valor que o picker emitiu. Sendo sincero, é um custo empurrado para quem consome a biblioteca, e como <code>ISODateString</code> é um alias de <code>string</code>, o compilador também não impede o erro.</p>
<h3 id="resolvendo-dst-só-com-intl"><a class="anchor" href="#resolvendo-dst-só-com-intl">Resolvendo DST só com Intl</a></h3>
<p>Para transformar uma coordenada em instante, é preciso saber quando é, em UTC, "00:00 daquele dia naquele fuso", mas o offset só pode ser obtido quando já se conhece o instante. Além disso, nos dias de transição do horário de verão, o horário local pode não existir (spring forward) ou existir duas vezes (fall back).</p>
<p>O Kalyx resolve isso sem uma biblioteca como <code>date-fns-tz</code>. Ele pergunta ao <code>Intl.DateTimeFormat(...).formatToParts</code> "que horas são neste fuso neste momento UTC" para medir o offset, e lê esse offset em dois pontos: um dia antes e um dia depois do horário pedido.</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">// packages/core/src/utils/timezone.ts:251-254, 259</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> candidate</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">probeEpoch</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:#E1E4E8;--shiki-light:#24292E">  civilEpoch </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">-</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> getTimezoneOffsetMinutes</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">(probeEpoch).</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">toISOString</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(), timeZone) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">*</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 60_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"> epochBefore</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> candidate</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(civilEpoch </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:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> epochAfter</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> candidate</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(civilEpoch </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>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">if</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (epochBefore </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">===</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> epochAfter) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">return</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">(epochBefore).</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">toISOString</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">();</span></span></code></pre></figure>
<p><code>civilEpoch</code> é o horário local desejado lido como se fosse UTC. Os offsets ficam dentro de ±14 horas, então um dia antes e um dia depois dão o offset de cada lado de qualquer transição próxima. Se os dois coincidem não há transição e bastam duas chamadas a <code>formatToParts</code>; como a grade chama isso uma vez para cada uma das suas 42 células, é esse caminho rápido que determina o custo. (A premissa de no máximo uma transição em 48 horas vale em todos os fusos de 2020 a 2045.)</p>
<p>Se forem diferentes, ele relê os dois candidatos naquele fuso e confere qual coincide com o horário pedido (<code>timezone.ts:261-279</code>). Na sobreposição do fall back os dois coincidem, então ele escolhe o mais cedo, montado com o offset anterior à transição; no buraco do spring forward nenhum coincide, então escolhe esse mesmo lado e avança o horário pela duração do buraco. Este é o resultado de rodar de verdade.</p>
<table>
<thead>
<tr>
<th>Pedido</th>
<th>Situação</th>
<th>1.4.7</th>
<th>1.4.8</th>
</tr>
</thead>
<tbody>
<tr>
<td>New_York 2026-03-08 02:30</td>
<td>Horário inexistente</td>
<td><code>2026-03-08T07:30:00.000Z</code></td>
<td>Igual (03:30 EDT, para frente)</td>
</tr>
<tr>
<td>New_York 2026-11-01 01:30</td>
<td>Horário que ocorre duas vezes</td>
<td><code>2026-11-01T05:30:00.000Z</code></td>
<td>Igual (01:30 EDT, o mais cedo)</td>
</tr>
<tr>
<td>London 2026-10-25 01:30</td>
<td>Horário que ocorre duas vezes</td>
<td><code>2026-10-25T01:30:00.000Z</code> (o mais tarde)</td>
<td><code>2026-10-25T00:30:00.000Z</code> (01:30 BST, o mais cedo)</td>
</tr>
</tbody>
</table>
<p>"Buracos vão para frente, ambiguidade fica com o mais cedo" coincide com o padrão de <code>disambiguation</code>, <code>"compatible"</code>, descrito na <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime" target="_blank" rel="noopener noreferrer">documentação do Temporal.ZonedDateTime no MDN</a>. Por que a linha de Londres erra na 1.4.7 fica para mais adiante.</p>
<h3 id="a-fronteira-do-adapter-são-21-métodos-de-strings"><a class="anchor" href="#a-fronteira-do-adapter-são-21-métodos-de-strings">A fronteira do adapter são 21 métodos de strings</a></h3>
<p>Se o core calcula fusos horários, do que date-fns ou dayjs cuidam? Da aritmética de datas e do parsing. Essa fronteira, <code>DateAdapter</code>, tem 21 métodos, e todos os argumentos de data e valores de retorno são strings ISO.</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">// packages/core/src/types.ts:71-102 (발췌)</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"> DateAdapter</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  parse</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"> string</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">format</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"> string</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  format</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">iso</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">formatStr</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">timezone</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"> string</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  addDays</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">iso</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">n</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:#B392F0;--shiki-light:#6F42C1">  isSameDay</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:#79B8FF;--shiki-light:#005CC5"> string</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:#79B8FF;--shiki-light:#005CC5"> string</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">timezone</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 style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  startOfDay</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">iso</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">timezone</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"> string</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  today</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">timezone</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"> string</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">  // addMonths, isBefore, startOfMonth, getYear 등 15개</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>Os métodos que recebem fuso horário são quatro, <code>format</code>, <code>isSameDay</code>, <code>startOfDay</code> e <code>today</code>, e até esses quatro delegam o cálculo ao core. O <code>format</code> do adapter de date-fns, quando recebe <code>timezone</code>, chama diretamente o <code>formatInTimezone</code> do core (<code>packages/adapter-date-fns/src/index.ts:112-115</code>). Por isso, seja qual for o adapter, as respostas sobre fuso horário saem do mesmo código, e os três adapters (date-fns, dayjs, luxon) pegam em <code>@kalyx/core/test-helpers</code> a função <code>runAdapterConformanceTests</code> e a rodam cada um para confirmar que dão as mesmas respostas.</p>
<p>Os dois entry points também se separam sobre essa fronteira. O entry padrão, <code>@kalyx/react</code>, chama <code>setDefaultAdapter(DateFnsAdapter)</code> ao carregar o módulo, então funciona logo após a instalação (<code>packages/react/src/index.ts:9-11</code>). <code>@kalyx/react/headless</code> não faz essa chamada e é empacotado à parte, então nenhum código de date-fns entra.</p>
<p>O formato dessa fronteira teve um preço. Em junho de 2026, desisti de um adapter de Temporal. O valor do Temporal está em <strong>os tipos carregarem o significado</strong>, como <code>PlainDate</code> e <code>ZonedDateTime</code>, mas se a fronteira é uma string, esse significado não consegue atravessá-la. Envolver o Temporal só achataria tudo em string, então concluí que não havia ganho de exatidão. Olhando para trás, o problema da seção anterior, "coordenadas e instantes são o mesmo <code>string</code>", é exatamente o que o Temporal resolve no sistema de tipos com <code>PlainDate</code> e <code>Instant</code>. A fronteira de strings facilitou a troca de adapters, mas em troca fechou o caminho de resolver isso com tipos.</p>
<h3 id="sete-pickers-são-combinações-de-três-contextos"><a class="anchor" href="#sete-pickers-são-combinações-de-três-contextos">Sete pickers são combinações de três contextos</a></h3>
<p>O Kalyx tem sete pickers: DatePicker, RangePicker, TimePicker, DateTimePicker, MonthPicker, YearPicker e WeekPicker. O que importa não é a quantidade, e sim <strong>o fato de os sete não serem implementações independentes</strong>. Existem só três contextos: <code>DatePickerContext</code>, <code>RangePickerContext</code> e <code>TimePickerContext</code>.</p>
<table>
<thead>
<tr>
<th>Picker</th>
<th>Root que guarda o estado</th>
<th>Contexto</th>
<th>Onde está a diferença</th>
</tr>
</thead>
<tbody>
<tr>
<td>DatePicker</td>
<td><code>DatePickerRoot</code></td>
<td>Date</td>
<td></td>
</tr>
<tr>
<td>MonthPicker, YearPicker</td>
<td>Implementação do Root do DatePicker</td>
<td>Date</td>
<td><code>selectionGranularity</code></td>
</tr>
<tr>
<td>RangePicker</td>
<td><code>RangePickerRoot</code></td>
<td>Range</td>
<td></td>
</tr>
<tr>
<td>WeekPicker</td>
<td><code>RangePickerRoot</code> sem alterações</td>
<td>Range</td>
<td><code>selectionMode="week"</code> no Calendar</td>
</tr>
<tr>
<td>TimePicker</td>
<td><code>TimePickerRoot</code></td>
<td>Time</td>
<td></td>
</tr>
<tr>
<td>DateTimePicker</td>
<td>Root próprio</td>
<td>Date e Time aninhados</td>
<td>Sobrepõe dois Providers</td>
</tr>
</tbody>
</table>
<p>O Root do MonthPicker apenas muda o formato de exibição padrão para <code>yyyy-MM</code> e passa <code>selectionGranularity="month"</code> para a implementação do Root do DatePicker (<code>MonthPicker/Root.tsx:11-20</code>). O <code>selectDate</code> do DatePicker, quando a granularity é <code>month</code>, dobra a coordenada para o dia 1 daquele mês e depois passa pela mesma conversão e verificação de restrições vistas antes. O DateTimePicker fornece os dois contextos sobrepostos.</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">// packages/react/src/components/DateTimePicker/Root.tsx:401-402</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">DatePickerContext.Provider</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">{dateContext}></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">TimePickerContext.Provider</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">{timeContext}>{children}&#x3C;/</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">TimePickerContext.Provider</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span></code></pre></figure>
<p>Por isso <code>DateTimePicker.Calendar</code> é o mesmo componente que <code>DatePicker.Calendar</code> e não sabe dentro de qual picker está. Uma correção em um lugar chega a todos os pickers que usam esse componente, e um defeito em um lugar também. Essa base compartilhada também explica por que um único DatePicker representava 74% do total no gráfico anterior.</p>
<h2 id="o-que-custou-manter-o-contrato"><a class="anchor" href="#o-que-custou-manter-o-contrato">O que custou manter o contrato</a></h2>
<p>Os três meses desde a 1.0 foram mais para verificar esse contrato do que para criar funcionalidades, e surgiram três defeitos que testes baseados em exemplos teriam deixado passar. O primeiro foi encontrado por um teste de propriedades, o segundo por uma revisão cruzada do código e o terceiro pela checagem deste post.</p>
<h3 id="uma-hora-em-1º-de-outubro-em-sydney"><a class="anchor" href="#uma-hora-em-1º-de-outubro-em-sydney">Uma hora em 1º de outubro em Sydney</a></h3>
<p>O primeiro foi a propriedade "ler no fuso o resultado de <code>startOfDayInTimezone</code> dá 00:00:00" quebrar em Australia/Sydney. A implementação da época media o offset só uma vez. Sydney passa de +10 para +11 às 02:00 de 1º de outubro de 2034, e 00:00 desse dia ainda é +10, então a resposta certa é <code>2034-09-30T14:00:00.000Z</code>. Mas "o ponto em que 00:00 de 1º de outubro é lido como UTC" fica depois da transição, então retornava +11, e o resultado era 23:00 do dia anterior. Esse contraexemplo continua como teste de regressão (<code>timezone.property.test.ts:276</code>).</p>
<h3 id="um-teste-que-só-passava-em-seul"><a class="anchor" href="#um-teste-que-só-passava-em-seul">Um teste que só passava em Seul</a></h3>
<p>O segundo apareceu em uma revisão cruzada em 3 de agosto de 2026. O código que decide qual célula do calendário marcar como selecionada convertia duas vezes. A célula já era uma coordenada, mas tanto a célula quanto o valor salvo eram convertidos para a data daquele fuso. Veja o resultado de escolher "15 de janeiro".</p>
<table>
<thead>
<tr>
<th>Fuso</th>
<th>Valor salvo</th>
<th>Célula marcada como selecionada</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>Asia/Seoul</code> (+9)</td>
<td><code>2026-01-14T15:00:00.000Z</code></td>
<td>Dia 15 (certo)</td>
</tr>
<tr>
<td><code>America/New_York</code> (-5)</td>
<td><code>2026-01-15T05:00:00.000Z</code></td>
<td>Dia 16 (errado)</td>
</tr>
</tbody>
</table>
<p>Em fusos com offset positivo, os dois deslocamentos se anulavam e por acaso dava certo, e os testes existentes só cobriam Seul. A mesma revisão encontrou também uma violação na direção oposta. Ao decidir qual mês exibir, <code>startOfMonth</code> era aplicado direto a um instante, então, em Seul, passar 1º de janeiro como valor abria o calendário de dezembro.</p>
<p>Os mecanismos são diferentes, mas a regra violada é uma só: converter uma única vez por direção, com a função definida para aquela direção.</p>
<p>Esse incidente ampliou os testes de propriedades de ida e volta do core de "alguns fusos representativos" para "todos os fusos que o runtime conhece". (Os testes de componentes do lado React ainda usam fusos representativos como <code>America/New_York</code>.) <strong>Defeitos que só aparecem onde o sinal muda não são pegos por amostragem.</strong></p>
<h3 id="o-0130-de-londres-que-resolvia-tarde"><a class="anchor" href="#o-0130-de-londres-que-resolvia-tarde">O 01:30 de Londres que resolvia tarde</a></h3>
<p>O terceiro apareceu quando estendi a tabela de DST deste post para outros fusos. A 1.4.7 começava a medir o offset no ponto em que o horário pedido é lido como UTC, e nos fusos cujo offset após a transição é 0 ou mais, esse ponto já fica depois da transição, então convergia para o offset mais tarde. Comparando com o temporal-polyfill todas as transições de 2020 a 2045 em 418 fusos, em 1.908 das 3.395 transições com sobreposição o resultado era o mais tarde, espalhadas por 84 fusos. (As 3.394 transições com buraco estavam todas certas, e Nova York, com offset negativo, acertava por acaso.)</p>
<p>Em <a href="https://github.com/jiji-hoon96/kalyx/pull/226" target="_blank" rel="noopener noreferrer">#226</a> mudei para ler um dia antes e um dia depois, publiquei como <code>@kalyx/core</code> 1.4.8 e deixei a mesma comparação como <code>timezone.dst-oracle.test.ts</code>. (O temporal-polyfill é usado só nos testes.) O comentário no código que dizia erroneamente <code>'earlier'</code> agora diz <code>'compatible'</code>. De novo, <strong>as amostras estavam concentradas em um só sinal.</strong></p>
<h3 id="os-3kb-gastos-com-exatidão"><a class="anchor" href="#os-3kb-gastos-com-exatidão">Os 3KB gastos com exatidão</a></h3>
<p>Essas correções custaram código. O Kalyx define na CI um teto para o bundle do entry padrão, e um PR que o ultrapassa falha em um check obrigatório. Esse teto começou em 12KB e subia 1KB a cada funcionalidade que entrava, e em agosto de 2026, ao refazer por completo a exatidão de fusos horários e restrições, eu o subi de 17KB para 20KB de uma vez.</p>
<p>O tamanho era um argumento de venda exibido no badge do README. Um seletor de datas que desloca um dia em fusos com offset negativo não serve, seja pequeno ou grande.</p>
<p><strong>Se eu tiver que escolher entre pequeno e correto, fico com o correto.</strong> Hoje o teto está apertado. Segundo o documento de mapa de bytes do bundle do repositório, de 2026-09-11, <code>dist/index.cjs</code> medido com o gzip padrão do Node tem 20.259 B, contra um teto de 20.480 B, deixando 221 B de folga. (É o tamanho do próprio arquivo com as dependências externas, então é uma quantidade diferente da do gráfico anterior.) A próxima funcionalidade vai precisar recuperar bytes antes de entrar.</p>
<h2 id="uma-conclusão-diferente-da-inicial"><a class="anchor" href="#uma-conclusão-diferente-da-inicial">Uma conclusão diferente da inicial</a></h2>
<p>O que eu queria era usar bibliotecas de datas complexas de forma declarativa. Depois de construir, percebi que APIs de composição declarativas e opções headless completas já existiam. A diferença que restou estava mais para dentro. <strong>Fixar o valor como um único instante, reduzir a conversão entre coordenadas e instantes a duas funções e proteger essa ida e volta com testes em todos os fusos horários.</strong> O tratamento de DST baseado em Intl, a fronteira de adapter com strings e os sete pickers feitos com três contextos são consequências dessa decisão.</p>
<p>Pelo objetivo de aprender, a maior lição foi como afirmar que algo está "correto". A comparação de bundles do post original media quantidades diferentes, e o teste que passava em Seul estava errado em Nova York. Se você não registra junto o que mediu e com qual amostra verificou, números viram facilmente motivo de vaidade.</p>
<p>Os limites também são claros. Sou o único maintainer, só há suporte a React 19, e os downloads no npm de <code>@kalyx/react</code> de 5 a 11 de setembro de 2026 foram 200, dos quais 156 se concentraram no único dia do lançamento da 1.4.7. A impossibilidade de separar coordenadas e instantes por tipos é contida só pela documentação, e também não decidi se vou garantir como API pública os pontos de contato para estilo, <code>classNames</code> e os atributos <code>data-*</code>. Em acessibilidade, há os papéis <code>grid</code> nos calendários, <code>combobox</code> nos campos de entrada e <code>listbox</code> nas listas de horário, com navegação por setas, Home/End e PageUp/PageDown, e a CI roda as verificações de <code>jest-axe</code> de 8 arquivos de teste, mas ainda não tenho base para dizer que ela é tão comprovada quanto a do React Aria.</p>
<p>Por isso, se o seu app já usa MUI, vale olhar primeiro o MUI X; se entrada por segmentos e acessibilidade comprovada vêm primeiro, o React Aria; e se você só precisa de um calendário, o react-day-picker. Se você já passou por datas deslocando um dia em formulários onde os valores trafegam como JSON, eu ficaria feliz se desse uma olhada no Kalyx nesse momento, e se conhece uma solução melhor, agradeceria se me contasse em uma GitHub Issue.</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">pnpm</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> add</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> @kalyx/react</span></span></code></pre></figure>
<p>No <a href="https://kalyx-docs-site.vercel.app/playground" target="_blank" rel="noopener noreferrer">Playground</a> do site de documentação, você pode testar os sete pickers e mudar as configurações de locale e timezone por conta própria.</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:#3b82f6"></span><a href="https://kalyx-docs-site.vercel.app/" target="_blank" rel="noopener noreferrer">Site de documentação oficial do Kalyx</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#3b82f6"></span><a href="https://mui.com/x/react-date-pickers/timezone/" target="_blank" rel="noopener noreferrer">MUI, Date and Time Pickers Timezone</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#3b82f6"></span><a href="https://floating-ui.com/" target="_blank" rel="noopener noreferrer">Documentação oficial do Floating UI</a></div>
</div>
</div></details>]]></content:encoded>
            <author>jihoon7705@gmail.com (이지훈)</author>
            <category>bibliotecas</category>
            <category>React</category>
            <category>DatePicker</category>
            <category>código-aberto</category>
        </item>
        <item>
            <title><![CDATA[Como economizar tokens de IA]]></title>
            <link>https://hooninedev.com/pt-BR/260611</link>
            <guid isPermaLink="false">https://hooninedev.com/pt-BR/260611</guid>
            <pubDate>Thu, 11 Jun 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Neste artigo, quero falar sobre como economizar tokens de IA. No começo, eu me concentrava mais nos resultados e no processo do que em desempenho e custo. Como as respostas da IA tinham muitas falhas,...]]></description>
            <content:encoded><![CDATA[<p>Neste artigo, quero falar sobre como economizar tokens de IA.</p>
<p>No começo, eu me concentrava mais nos resultados e no processo do que em desempenho e custo. Como as respostas da IA tinham muitas falhas, era preciso verificá-las; e, como queríamos resultados rápidos, muita gente, inclusive eu, comprava mais tokens ou migrava para um plano superior quando eles acabavam. Eu também fiz isso. (Nos primeiros meses, nem sequer prestei atenção em quanto gastava com tokens.)</p>
<p>Com o tempo, porém, fiquei cada vez mais atento ao uso de tokens. Para as pessoas, a mensalidade pesava; para as empresas, aumentavam as preocupações com custos de pessoal e operação. Em <a href="/260524" target="_blank" rel="noopener noreferrer">MCP e function calling</a> expliquei o protocolo que conecta agentes a sistemas externos; em <a href="/260526" target="_blank" rel="noopener noreferrer">Quatro camadas de inteligência de código</a>, as ferramentas de busca de código e o GitHub Trending; e em <a href="/260529" target="_blank" rel="noopener noreferrer">Arquivos de contexto</a>, os arquivos de regras que os agentes leem. Assim, meus textos até aqui se concentraram mais em quais ferramentas existem e em como usá-las bem. Ainda considero isso importante, mas acredito que o custo acabará sendo a maior dúvida.</p>
<p>Expliquei os princípios dos tokens — o que são, como o BPE os cria e o que ocorre dentro do transformador quando prompt caching reduz o preço — em <a href="/260610" target="_blank" rel="noopener noreferrer">Como os tokens funcionam</a>. Com essa base, veremos primeiro como os custos são cobrados e onde surgem ineficiências, depois reuniremos padrões comprovados de economia. Os principais padrões vêm acompanhados do uso de tokens que medi eu mesmo executando a mesma tarefa com estratégias diferentes.</p>
<h2 id="de-onde-vêm-os-custos-dos-tokens"><a class="anchor" href="#de-onde-vêm-os-custos-dos-tokens">De onde vêm os custos dos tokens?</a></h2>
<p>Vamos examinar de forma breve e objetiva como esses custos surgem e como são cobrados.</p>
<p>Todo texto enviado — prompt de sistema, definições de ferramentas, histórico e mensagens do usuário — vira tokens de entrada; a resposta do modelo vira tokens de saída. Para o modelo, cada chamada é uma entrada inédita. Não importa se a conversa aconteceu ontem ou há um minuto: uma nova chamada envia novamente todo o conteúdo como entrada. (Esse fato simples é o núcleo do custo: o modelo não tem memória e nós contamos tudo outra vez.)</p>
<p>Há mais uma variável. Reenviar do zero a parte estática de chamadas repetidas é caro, então os principais provedores de LLM criaram prompt caching. A entrada estática é armazenada uma vez e, nas chamadas seguintes, os tokens lidos da cache custam muito menos. (Em Como os tokens funcionam, tratei do mecanismo: como o BPE cria tokens e como a cache reutiliza a KV cache do transformador. Aqui, o foco é como isso vira custo.)</p>
<h3 id="tokens-de-entrada-tokens-de-saída-e-tokens-de-cache"><a class="anchor" href="#tokens-de-entrada-tokens-de-saída-e-tokens-de-cache">Tokens de entrada, tokens de saída e tokens de cache</a></h3>
<p>Vejamos quatro campos do objeto <code>usage</code> devolvido pelo SDK da Anthropic.</p>
<p><picture><source type="image/webp" srcset="/content/260611/1.720w.webp 720w, /content/260611/1.916w.webp 916w" sizes="(max-width: 840px) calc(100vw - 32px), 808px"><img src="/content/260611/1.png" alt="O objeto usage de uma resposta do SDK da Anthropic, com os campos input_tokens, cache_creation_input_tokens, cache_read_input_tokens e output_tokens" width="916" height="638" loading="eager" fetchpriority="high" decoding="async"></picture></p>
<ul>
<li><code>input_tokens</code> parte da entrada enviada, excluindo leituras da cache</li>
<li><code>output_tokens</code> resposta gerada pelo modelo</li>
<li><code>cache_creation_input_tokens</code> tokens armazenados pela primeira vez nesta chamada</li>
<li><code>cache_read_input_tokens</code> tokens relidos de uma cache existente</li>
</ul>
<p>Cada campo recebe uma tarifa diferente. Tokens de saída são os mais caros; os lidos da cache, os mais baratos. Segundo a documentação da Anthropic, a leitura custa 0,1 vez a tarifa base de entrada, exatamente 10%. A escrita custa 1,25 vez com TTL (Time To Live, período de validade da cache) de cinco minutos e 2 vezes com TTL de uma hora. Paga-se um pouco mais na primeira chamada e economiza-se 90% a partir da segunda.</p>
<p>A cache também tem restrições pouco conhecidas. O mínimo varia por modelo e até por versão da mesma família. Sonnet 4.6 e Opus 4.8 exigem 1.024 tokens, Opus 4.7 exige 2.048, e Haiku 4.5 e os antigos Opus 4.5/4.6 exigem 4.096. Um prompt menor não é armazenado mesmo com <code>cache_control</code>, sem aviso. Uma solicitação aceita até quatro pontos de interrupção <code>cache_control</code>, e a cache é lida na ordem hierárquica <code>tools</code> → <code>system</code> → <code>messages</code>. Assim, mudar uma definição de ferramenta no início invalida toda a cache posterior.</p>
<p><img src="/content/260611/2.webp" alt="Prompt caching exige correspondência exata desde o início; alterar o prefixo invalida tudo que vem depois" width="1680" height="840" loading="lazy" fetchpriority="low" decoding="async"></p>
<p>O desconto de 90% vem da reutilização da KV cache do transformador entre chamadas, mecanismo detalhado em Como os tokens funcionam. Para custos, lembre apenas isto: só há cache hit quando o prefixo é idêntico. Portanto, coloque <strong>o conteúdo estático primeiro e o conteúdo dinâmico, que muda a cada chamada, por último</strong>. Até um caractere de timestamp no início invalida toda a cache seguinte.</p>
<p>Surge uma pergunta: “Mas a entrada muda a cada turno; a cache não deveria quebrar quase sempre?” Não. A conversa não é reescrita como um bloco novo: ela usa uma <strong>estrutura append que preserva o conteúdo acumulado na frente e acrescenta apenas a nova fala no fim</strong>. Prompt de sistema, ferramentas, perguntas e respostas anteriores permanecem intactos; só a nova pergunta é anexada. Comparando as entradas desde o início, a primeira diferença estará no final. A entrada completa muda, mas o prefixo sobrevive. (A cache só quebra em todos os turnos num projeto ruim que insere valores dinâmicos na frente. Claude Code e opencode mantêm o início fixo e anexam ao fim; o usuário não precisa configurar nada. Erros de digitação também não afetam a cache, pois ficam no fim, fora do prefixo já armazenado.)</p>
<h3 id="comparação-entre-provedores-de-ia"><a class="anchor" href="#comparação-entre-provedores-de-ia">Comparação entre provedores de IA</a></h3>
<p>Ao reunir as tabelas oficiais de Anthropic, OpenAI e Google de junho de 2026, o padrão fica claro. (Valores em USD por milhão de tokens, com foco em modelos comuns na programação.)</p>
<table>
<thead>
<tr>
<th>Provedor</th>
<th>Modelo</th>
<th>entrada</th>
<th>entrada em cache</th>
<th>saída</th>
</tr>
</thead>
<tbody>
<tr>
<td>Anthropic</td>
<td>Claude Opus 4.8</td>
<td>$5.00</td>
<td>$0.50</td>
<td>$25.00</td>
</tr>
<tr>
<td>Anthropic</td>
<td>Claude Sonnet 4.6</td>
<td>$3.00</td>
<td>$0.30</td>
<td>$15.00</td>
</tr>
<tr>
<td>Anthropic</td>
<td>Claude Haiku 4.5</td>
<td>$1.00</td>
<td>$0.10</td>
<td>$5.00</td>
</tr>
<tr>
<td>OpenAI</td>
<td>GPT-5.5</td>
<td>$5.00</td>
<td>$0.50</td>
<td>$30.00</td>
</tr>
<tr>
<td>OpenAI</td>
<td>GPT-5.5 Pro</td>
<td>$30.00</td>
<td>$3.00</td>
<td>$180.00</td>
</tr>
<tr>
<td>Google</td>
<td>Gemini 3.1 Pro</td>
<td>$2.00</td>
<td>$0.20</td>
<td>$12.00</td>
</tr>
<tr>
<td>Google</td>
<td>Gemini 3.5 Flash</td>
<td>$1.50</td>
<td>$0.15</td>
<td>$9.00</td>
</tr>
</tbody>
</table>
<p>Nos três provedores, a saída custa cinco a seis vezes mais que a entrada. Respostas longas elevam rapidamente o custo, mesmo dizendo a mesma coisa. Na mesma faixa, os preços dos modelos variam de três a seis vezes. A conta revela três princípios: <strong>pedir saídas curtas, usar um modelo mais barato quando ele entregar a mesma resposta e armazenar entradas estáticas em cache</strong>. (Se um contexto estático de 10 mil tokens for reutilizado 100 vezes, custa $5 na tarifa base do GPT-5.5 e $0.50 como leitura de cache. A economia é $4.5 e, como quase não há acréscimo de escrita inicial, o retorno começa na segunda chamada.)</p>
<h3 id="diferenças-entre-mecanismos-de-cache"><a class="anchor" href="#diferenças-entre-mecanismos-de-cache">Diferenças entre mecanismos de cache</a></h3>
<p>Mesmo com descontos parecidos, o projeto interno varia por provedor. Entender isso facilita definir políticas.</p>
<table>
<thead>
<tr>
<th>Item</th>
<th>Anthropic</th>
<th>OpenAI</th>
<th>Google Gemini</th>
</tr>
</thead>
<tbody>
<tr>
<td>Acionamento</td>
<td>Pontos <code>cache_control</code> explícitos</td>
<td>Automático (sem mudar código)</td>
<td>Automático (implicit) e explícito (explicit)</td>
</tr>
<tr>
<td>Tamanho mínimo</td>
<td>1.024–4.096 tokens (por modelo)</td>
<td>1.024+ (incrementos de 128)</td>
<td>2.048–4.096 (por modelo)</td>
</tr>
<tr>
<td>Custo de escrita</td>
<td>1.25x (5 min) / 2x (1h) da entrada</td>
<td>Grátis</td>
<td>Grátis (mais armazenamento por hora)</td>
</tr>
<tr>
<td>Custo de leitura</td>
<td>0.1x da entrada</td>
<td>Cerca de 0.1x da entrada</td>
<td>0.1x da entrada</td>
</tr>
<tr>
<td>TTL</td>
<td><a href="https://github.com/anthropics/claude-code/issues/46829" target="_blank" rel="noopener noreferrer">5 minutos ou 1 hora (escolha do usuário)</a></td>
<td>5–10 minutos de inatividade por padrão, até 1 hora (24 horas na extensão)</td>
<td>Definido pelo usuário (armazenamento por hora)</td>
</tr>
<tr>
<td>Custo adicional</td>
<td>Nenhum</td>
<td>Nenhum</td>
<td>Armazenamento: Flash $1/M-hora, Pro $4.50/M-hora</td>
</tr>
</tbody>
</table>
<p>A filosofia aparece claramente. <strong>Anthropic</strong> exige marcação explícita, cobra um pequeno prêmio inicial (1,25 vez) e depois dá grande desconto. O controle do prefixo torna previsível a taxa de leitura. <strong>OpenAI</strong> automatiza tudo: entradas com 1.024 tokens ou mais são armazenadas sem custo, mas há menos controle. <strong>Google</strong> oferece os dois métodos e cobra armazenamento quando a cache é gerida explicitamente. Cache implícita serve para reutilização breve e frequente; explícita com armazenamento, para grandes contextos mantidos por uma hora ou mais.</p>
<h3 id="definições-de-ferramentas-e-tokenizadores"><a class="anchor" href="#definições-de-ferramentas-e-tokenizadores">Definições de ferramentas e tokenizadores</a></h3>
<p>Duas outras variáveis têm impacto surpreendente.</p>
<p>A primeira são as definições de ferramentas. Com vários servidores MCP, cada chamada inclui nomes e esquemas de parâmetros — mediremos depois quanto isso aumenta o custo —. Mesmo com ferramentas idênticas, trocar apenas o modelo altera o gasto. A documentação da Anthropic mostra que o prompt de ferramentas varia: com <code>tool_choice: auto</code>, Sonnet 4.6 e Haiku 4.5 usam cerca de 497 tokens, Opus 4.7 usa 675 e Opus 4.8 cai para 290. Antes do model routing, vale verificar quanto o modelo expande essas definições.</p>
<p>A segunda é a eficiência do tokenizador, que divide texto em unidades processáveis. O mesmo texto pode gerar contagens diferentes. o200k_base da OpenAI usa bem menos tokens que cl100k_base em idiomas não ingleses; a Anthropic afirma que, com o novo tokenizador desde Opus 4.7, o mesmo texto pode ser cobrado como até 35% mais tokens do que nos modelos anteriores. Escolher apenas pela tarifa deixa o custo vazar pela ineficiência. A comparação correta é tarifa × quantidade esperada.</p>
<p>A diferença é marcante em coreano. Com dois tokenizadores da OpenAI, frases técnicas em inglês tiveram a mesma contagem, mas cl100k_base usou 31–43% mais tokens em coreano que o200k_base. Um parágrafo de 167 caracteres virou 169 tokens no antigo: <strong>mais tokens que caracteres</strong>. Em média, cada caractere consumiu mais de um token.</p>
<p><picture><source type="image/webp" srcset="/content/260611/3.720w.webp 720w, /content/260611/3.1440w.webp 1440w, /content/260611/3.1680w.webp 1680w" sizes="(max-width: 752px) calc(100vw - 32px), 720px"><img src="/content/260611/3.png" alt="Comparação de tokens em coreano e inglês por tokenizador: cl100k versus o200k" style="max-width:min(720px, 100%);height:auto;" width="1680" height="697" loading="lazy" fetchpriority="low" decoding="async"></picture></p>
<p>O antigo fragmenta o coreano no nível de bytes; o novo agrupa trechos frequentes como “개발” e “입니다” em um token. Mesmo com tarifa igual, uma carga em coreano pode custar mais de 1,5 vez só pela eficiência do tokenizador.</p>
<p>Surge então a pergunta: “Onde exatamente estamos criando toda essa ineficiência?”</p>
<h2 id="padrões-comuns-de-desperdício-de-tokens"><a class="anchor" href="#padrões-comuns-de-desperdício-de-tokens">Padrões comuns de desperdício de tokens</a></h2>
<p>O desperdício é comum mesmo quando acreditamos usar bem a IA. Estes são os padrões que observei no meu trabalho e entre outros desenvolvedores.</p>
<h3 id="servidores-mcp-e-ferramentas-demais"><a class="anchor" href="#servidores-mcp-e-ferramentas-demais">Servidores MCP e ferramentas demais</a></h3>
<p>Vale repetir: depois de conectar Linear, GitHub, Notion, Figma, Slack ou Sentry, raramente removemos algo. Esquemas ociosos inflam a entrada em toda chamada. Para lidar com isso, Claude Code ativa MCP Tool Search: no início entram apenas nomes e descrições; o esquema completo só é carregado quando a ferramenta é chamada.</p>
<p>Medi a diferença com 27 ferramentas MCP de uma sessão — 10 do Serena, oito de quatro integrações OAuth do claude.ai, duas do Figma e sete do agentmemory — e enviei <strong>a mesma mensagem em duas configurações</strong>. Uma não tinha MCP; a outra tinha as 27 conectadas, mas indisponíveis ao modelo. Ambas fizeram zero chamadas de ferramentas.</p>
<p><picture><source type="image/webp" srcset="/content/260611/6.720w.webp 720w, /content/260611/6.1440w.webp 1440w, /content/260611/6.1680w.webp 1680w" sizes="(max-width: 840px) calc(100vw - 32px), 808px"><img src="/content/260611/6.png" alt="A mesma pergunta feita sem servidores MCP e com 27 conectados: os tokens de entrada vão de 41 para 10.335 e o custo por chamada sobe cerca de 11,7 vezes" width="1680" height="638" loading="lazy" fetchpriority="low" decoding="async"></picture></p>
<p>Com mesma pergunta, modelo e sentido da resposta, a entrada foi de <strong>41 → 10,335 (+10,294)</strong> tokens no Opus 4.7. O custo subiu de <strong>$0.0048 → $0.0563, cerca de 12 vezes</strong>; o aumento de 250 vezes na entrada adicionou <strong>+783ms</strong> de latência de prefill. O mais marcante é ser <strong>um custo pago em todas as chamadas mesmo sem usar MCP naquele turno</strong>. Tool Search evita isso. (Desde a medição, removo continuamente servidores que não uso.)</p>
<h3 id="acúmulo-de-contexto-e-lost-in-the-middle"><a class="anchor" href="#acúmulo-de-contexto-e-lost-in-the-middle">Acúmulo de contexto e Lost in the Middle</a></h3>
<p><picture><source type="image/webp" srcset="/content/260611/4.720w.webp 720w, /content/260611/4.840w.webp 840w" sizes="(max-width: 840px) calc(100vw - 32px), 808px"><img src="/content/260611/4.jpg" alt="Primeira página do artigo &#x22;Lost in the Middle&#x22;, com a curva em U em que a precisão despenca quando a resposta está no meio do contexto" width="840" height="727" loading="lazy" fetchpriority="low" decoding="async"></picture></p>
<p>Levar uma conversa longa adiante aumenta a entrada e reduz a precisão. O estudo “Lost in the Middle”, da equipe de Liu em Stanford, quantificou uma curva em U: informações no começo ou fim são recuperadas melhor; no meio, pior. Gastamos mais para receber resposta inferior. Como o self-attention cresce com o quadrado dos tokens, a atenção por token se dilui com o contexto. O meio enfraquece primeiro porque os dados de treinamento tendem a concentrar informação importante nas extremidades.</p>
<p>Mais a fundo, a forma como o modelo trata a “posição” traz duas tendências que enterram o meio. A intuição é simples.</p>
<p>Primeiro, o modelo <strong>escuta mais os tokens próximos</strong>. RoPE (Rotary Position Embedding), usado pela maioria dos modelos abertos, enfraquece a conexão conforme a distância aumenta, um decay effect. Tokens distantes recebem menos atenção.</p>
<p>Segundo, o modelo <strong>envia atenção demais ao primeiro token</strong>. O softmax obriga a distribuir 100% da atenção; quando não há foco específico, o excedente costuma ir ao primeiro token. Isso é attention sink. (A equipe de Xiao no MIT quantificou o fenômeno no StreamingLLM. Não indica informação importante, mas um ralo para atenção excedente.)</p>
<p>As duas tendências concentram atenção nas pontas — tokens recentes e primeiro token — e enfraquecem o meio. Não é bug de um modelo. LLaMA, Mistral e Qwen usam RoPE; Claude e GPT provavelmente usam mecanismos semelhantes. Lost in the middle é um viés da arquitetura moderna.</p>
<p><picture><source type="image/webp" srcset="/content/260611/5.720w.webp 720w, /content/260611/5.1200w.webp 1200w" sizes="(max-width: 840px) calc(100vw - 32px), 808px"><img src="/content/260611/5.png" alt="Gráfico de context rot: conforme o tamanho da entrada cresce, a precisão cai em Claude, Qwen, OpenAI e Gemini igualmente" width="1200" height="622" loading="lazy" fetchpriority="low" decoding="async"></picture></p>
<p>Hoje, o fenômeno também é chamado de <strong>context rot</strong>. Uma <a href="https://research.trychroma.com/context-rot" target="_blank" rel="noopener noreferrer">análise da Chroma</a> submeteu 18 modelos — incluindo GPT-4.1, Claude 4, Gemini 2.5 e Qwen3 — à mesma tarefa NIAH (needle in a haystack). Quando a entrada passou de 10k para mais de 100k tokens, a precisão caiu 20–50%, conforme o modelo. Todos pioraram; Claude caiu mais lentamente. A Anthropic descreve isso como um “orçamento de atenção” derivado do n², consumido entre tokens. Contexto leve reduz custos e preserva precisão.</p>
<h3 id="chamar-subagent-sem-critério"><a class="anchor" href="#chamar-subagent-sem-critério">Chamar subagent sem critério</a></h3>
<p>Delegar tudo porque subagent é útil também é uma armadilha. Ele começa num contexto separado e paga novamente o prompt de sistema e as ferramentas. Delegar um shell curto ou uma consulta simples de git pode custar mais no arranque do que economiza no contexto principal. Segundo a Anthropic, um agente usa cerca de quatro vezes os tokens de um chat; um sistema multiagente, quinze vezes. A delegação só vale se a precisão justificar isso.</p>
<p>O mesmo vale para MCP. Cada ferramenta ativa adiciona seu esquema ao prompt em todo turno e custa mesmo sem uso. “Ativar tudo por garantia” parece seguro, mas os dados discordam.</p>
<p>Testei precisão e custo com perguntas sobre o reconciler do facebook/react v19 em três cenários:</p>
<ul>
<li>Sem ferramentas</li>
<li>Uma ferramenta (CodeGraph, Serena, ripgrep ou grep puro)</li>
<li>As quatro ferramentas conectadas juntas</li>
</ul>
<p><picture><source type="image/webp" srcset="/content/260611/9.720w.webp 720w, /content/260611/9.1440w.webp 1440w, /content/260611/9.1680w.webp 1680w" sizes="(max-width: 532px) calc(100vw - 32px), 500px"><img src="/content/260611/9.png" alt="Recall e uso de tokens por cenário no benchmark de codesearch" style="max-width:min(500px, 100%);height:auto;" width="1680" height="1927" loading="lazy" fetchpriority="low" decoding="async"></picture></p>
<p>Três resultados se destacaram. (Recall indica quão completas foram as respostas corretas.)</p>
<ul>
<li><strong>Sem ferramentas</strong> obteve recall médio de 0,31, confirmando o valor das ferramentas.</li>
<li><strong>Somente Serena (LSP)</strong> atingiu 1,00 por $0.38, a opção individual mais eficiente.</li>
<li><strong>As quatro ferramentas</strong> reduziram o recall a 0,89 e elevaram o custo a $0.47. Em perguntas multi-hop, o resultado igualou CodeGraph sozinho (0.78 / 0.88): o modelo preferiu uma ferramenta e herdou suas fraquezas.</li>
</ul>
<p>O princípio é igual para subagent e ferramentas: o custo extra só importa se elevar a precisão. <strong>Escolher a ferramenta adequada ao domínio é mais barato e preciso que ativar tudo por garantia.</strong></p>
<p>Então, quais métodos realmente economizam tokens?</p>
<h2 id="formas-comprovadas-de-economizar-tokens"><a class="anchor" href="#formas-comprovadas-de-economizar-tokens">Formas comprovadas de economizar tokens</a></h2>
<p>Cada padrão ataca um eixo: reduzir a entrada, baixar o preço da mesma entrada ou entregar o trabalho a um modelo mais barato. Vejamos cada um.</p>
<h3 id="prompt-caching"><a class="anchor" href="#prompt-caching">Prompt caching</a></h3>
<p>É o efeito mais imediato. Leituras custam 0,1 vez a entrada. Após o prêmio de escrita de 1,25 vez na primeira chamada, a seção estática pode ser reutilizada desde a segunda por um décimo do preço.</p>
<p>O ponto essencial é que <strong>as partes quase idênticas entre chamadas</strong> — ferramentas, trechos de código e contexto RAG — são exatamente as armazenáveis. O benefício continua se não forem misturadas com conteúdo dinâmico, como a pergunta atual ou um novo resultado. Ao chamar a API, agrupar blocos estáticos na frente e dinâmicos atrás entrega quase toda a economia. Claude Code e opencode fazem isso automaticamente.</p>
<h3 id="agrupar-trabalho-assíncrono-com-batch-api"><a class="anchor" href="#agrupar-trabalho-assíncrono-com-batch-api">Agrupar trabalho assíncrono com Batch API</a></h3>
<p>Se a chamada não precisa terminar imediatamente, Batch API é a forma mais simples de reduzir a tarifa. Anthropic, OpenAI e Google dão 50% de desconto em entrada e saída em troca de entregar em até 24 horas. Na Anthropic, a maioria termina em uma hora, mas o SLA é 24 horas. A chamada não muda e o preço cai pela metade.</p>
<p>Esse desconto se multiplica com outros. A Anthropic afirma que cache e lote se acumulam: tarifa padrão × 0.5 (lote) × 0.1 (leitura) = 0.05, ou 5% na parte estática. Os descontos se multiplicam, não se somam.</p>
<p>Calculei 100 tarefas com 10 mil tokens estáticos, 500 dinâmicos e mil de saída, usando tarifas do Opus 4.8 e quatro estratégias.</p>
<p><picture><source type="image/webp" srcset="/content/260611/12.720w.webp 720w, /content/260611/12.1440w.webp 1440w, /content/260611/12.1680w.webp 1680w" sizes="(max-width: 752px) calc(100vw - 32px), 720px"><img src="/content/260611/12.png" alt="Economia com Batch API e Prompt caching: os descontos se acumulam por multiplicação" style="max-width:min(720px, 100%);height:auto;" width="1680" height="670" loading="lazy" fetchpriority="low" decoding="async"></picture></p>
<p>Cache reduz 57%, lote 50%, e ambos até 79%. A multiplicação ocorre na entrada estática. (Saída não entra na cache e recebe apenas o desconto de lote; quanto maior sua parcela, menor a redução total. Quanto maior a parte estática, maior o efeito.) Indexação noturna, avaliação antes da publicação, extração de dados e relatórios não exigem alguém diante da tela.</p>
<p>Nem tudo pode esperar. Batch não serve à sessão principal de um agente ou chat interativo, onde a latência tem valor. Dividir entre “preciso agora” e “preciso até o próximo expediente” pode cortar a conta pela metade.</p>
<h3 id="isolar-trabalho-verbose-com-um-subagent"><a class="anchor" href="#isolar-trabalho-verbose-com-um-subagent">Isolar trabalho verbose com um subagent</a></h3>
<p><img src="/content/260611/7.webp" alt="Estrutura de isolamento de contexto com subagent" style="max-width:min(500px, 100%);height:auto;" width="1680" height="1680" loading="lazy" fetchpriority="low" decoding="async"></p>
<p><a href="https://www.anthropic.com/engineering/multi-agent-research-system" target="_blank" rel="noopener noreferrer">Nas palavras da documentação do Claude Code,</a> um subagent “opera em sua própria janela isolada; chamadas e resultados intermediários permanecem nele e só a mensagem final volta ao pai”. Delegar uma tarefa verbose deixa só um resumo limpo no contexto principal. Segundo o texto da Anthropic sobre <a href="https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents" target="_blank" rel="noopener noreferrer">context engineering</a>, mesmo gastando dezenas de milhares de tokens, ele costuma devolver apenas 1.000–2.000 tokens ao pai.</p>
<blockquote>
<p>Uma tarefa verbose produz muitos tokens para obter uma resposta de uma linha. Executar testes, pesquisar documentação e analisar logs são exemplos.</p>
</blockquote>
<p>Mas subagent não reduz automaticamente o custo total. Agentes usam cerca de quatro vezes mais tokens que chats; sistemas multiagente, quinze vezes. O subagent protege a precisão e o contexto principal ao retirar saídas verbose; não é mágica. Delegue apenas se a economia de limpeza superar o arranque. Tarefas curtas saem mais baratas no pai.</p>
<p>O domínio também importa. No teste interno da Anthropic, um líder Opus 4 com subagent Sonnet 4 melhorou 90,2% sobre um Opus 4 sozinho. Porém, domínios onde agentes compartilham contexto ou têm muitas dependências são inadequados — e programação é exatamente assim. Pesquisa explora rumos independentes; código vive num grafo de dependências. (Em programação, um agente com um subagent isolado de exploração pode ser um padrão mais seguro.)</p>
<h3 id="compact-e-progressive-disclosure"><a class="anchor" href="#compact-e-progressive-disclosure">compact e progressive disclosure</a></h3>
<p>O comando <code>/compact</code> resume toda a conversa e reinicia com contexto novo. A compressão é semântica: preserva trabalho atual e mudanças recentes e remove saídas repetitivas. <code>/clear</code> apaga tudo; <code>/compact</code> deixa um resumo. Perto de 95% da janela, auto-compact faz o mesmo. Organizar sessões longas interrompe o acúmulo.</p>
<p>Por dentro, <code>/compact</code> é a última etapa de uma pipeline automática. Segundo <a href="https://github.com/VILA-Lab/Dive-into-Claude-Code" target="_blank" rel="noopener noreferrer">“Dive into Claude Code”,</a> <code>query.ts</code> verifica cinco etapas antes de cada chamada.</p>
<p><picture><source type="image/webp" srcset="/content/260611/8.720w.webp 720w, /content/260611/8.1440w.webp 1440w, /content/260611/8.1680w.webp 1680w" sizes="(max-width: 840px) calc(100vw - 32px), 808px"><img src="/content/260611/8.png" alt="Diagrama do laço do agente: começa no prompt do usuário, repete pedidos de ferramenta e verificações de permissão e passa pela compactação quando o contexto aperta" width="1680" height="1591" loading="lazy" fetchpriority="low" decoding="async"></picture></p>
<ul>
<li><strong>Budget Reduction</strong> corta partes de saídas que excedem o limite.</li>
<li><strong>Snip</strong> remove o histórico antigo no eixo temporal.</li>
<li><strong>Microcompact</strong> comprime finamente preservando a consciência da cache.</li>
<li><strong>Context Collapse</strong> reprojeta históricos enormes em read-time para reduzir dimensões.</li>
<li><strong>Auto-Compact</strong> aciona compressão semântica aos 95% como último recurso.</li>
</ul>
<p>As primeiras são leves e baratas; as últimas, pesadas e eficazes. Uma estratégia não resolve toda pressão. Chamar <code>/compact</code> antecipa a etapa final.</p>
<p>A mesma ideia aparece em Skills. <code>/compact</code> reduz o acumulado; as três fases de <code>/skills</code> evitam acumulá-lo.</p>
<p>Segundo a Anthropic, uma skill carrega em três fases. Nome e descrição — cerca de 100 tokens — entram no início. O corpo (<code>SKILL.md</code>, menos de 5 mil tokens) só entra quando acionado. Scripts e recursos executados via bash retornam apenas a saída; o código não entra. Dezenas de skills quase não aumentam o contexto inicial.</p>
<h3 id="model-routing-a-mesma-resposta-com-um-modelo-mais-barato"><a class="anchor" href="#model-routing-a-mesma-resposta-com-um-modelo-mais-barato">Model routing: a mesma resposta com um modelo mais barato</a></h3>
<p><picture><source type="image/webp" srcset="/content/260611/13.720w.webp 720w, /content/260611/13.1172w.webp 1172w" sizes="(max-width: 840px) calc(100vw - 32px), 808px"><img src="/content/260611/13.png" alt="Desempenho do modelo em relação ao custo em escala logarítmica, com o roteador ideal no canto de baixo custo e alto desempenho" width="1172" height="1039" loading="lazy" fetchpriority="low" decoding="async"></picture></p>
<p>A entrada do Opus 4.8 custa cinco vezes a do Haiku 4.5. Usar o maior modelo para busca, exploração ou resumo simples é desperdício. Encaminhar por dificuldade — Haiku → Sonnet → Opus — e reservar Opus ao raciocínio pesado virou padrão. <a href="https://lmsys.org/blog/2024-07-01-routellm/" target="_blank" rel="noopener noreferrer">RouteLLM, da LMSYS,</a> preservou 95% da qualidade do GPT-4 reduzindo chamadas fortes a 14%, embora o benchmark fosse de raciocínio geral.</p>
<p>O ecossistema se firmou: OpenRouter, Martian e NotDiamond; no código aberto, RouteLLM, LiteLLM e Bifrost. Na Anthropic, a seleção do Agent SDK, o campo <code>model</code> do subagent — Explore usa Haiku — e <code>/model</code> apoiam routing. Mas as APIs não escolhem sozinhas pela dificuldade: executam o modelo indicado. Routing automático existe em produtos como Cursor Auto, ChatGPT auto e <code>openrouter/auto</code>. Para economizar, é preciso construir uma camada.</p>
<p>Adicionar qualquer router não é a resposta. Auto Router, que troca o modelo por chamada, conflita com prompt caching. A cache efêmera exige mesmo modelo e prefixo; mudar a chave causa miss. Perde-se o desconto de 90% e a economia de routing volta em misses. Por isso OpenRouter recomenda stickiness com <code>session_id</code>.</p>
<p>Na prática, não classifique toda chamada; divida estaticamente por tarefa. O campo <code>model</code> segue isso: Explore sempre usa a faixa Haiku; revisão, Opus. Cada faixa repete modelo, prompt e ferramentas e acumula sua própria cache. “Routing virou padrão” significa ramificação estática “coordenador + executor”, não classificador por chamada. Assim routing e cache coexistem.</p>
<h3 id="cursor-composer-25"><a class="anchor" href="#cursor-composer-25">Cursor Composer 2.5</a></h3>
<p><img src="/content/260611/10.webp" alt="Gráfico publicado pela Cursor com a pontuação CursorBench 3.1 em relação ao custo médio por tarefa, com o Composer 2.5 perto das melhores notas na ponta mais barata" width="1680" height="1120" loading="lazy" fetchpriority="low" decoding="async"></p>
<p>Outra opção é usar um agente como Cursor. <a href="https://cursor.com/blog/composer-2-5" target="_blank" rel="noopener noreferrer">Composer 2.5, lançado em 18 de maio de 2026,</a> usa o checkpoint aberto Kimi K2.5 da Moonshot AI e foi ajustado para código. A Cursor afirma desempenho comparável ao Claude Opus 4.7 por cerca de um décimo do preço. A tarifa é $0.50 de entrada e $2.50 de saída, uma ordem abaixo dos $5.00 e $25.00 do Opus 4.8.</p>
<p>Como o benchmark é da própria Cursor, não convém tomar o número absoluto como definitivo. A tendência importa: um modelo menor especializado em código pode se aproximar de um modelo geral de fronteira e reduzir custos em uma ordem de grandeza. É uma tendência clara de 2026.</p>
<p>O mais interessante é que ele também <strong>reduz a própria quantidade de tokens de entrada quando projetado junto ao IDE</strong>. Composer e Cascade consomem eficientemente arquivo atual, vizinhos e símbolos indexados, reduzindo pedidos extras e operações de grep e leitura. A tarifa cai dez vezes e a contagem também diminui, então a economia real pode ser maior.</p>
<h3 id="levar-o-contexto-para-fora-do-contexto"><a class="anchor" href="#levar-o-contexto-para-fora-do-contexto">Levar o contexto para fora do contexto</a></h3>
<p>Uma tendência de 2026 é carregar “apenas o necessário, quando necessário”. A Anthropic chama isso de <strong>contexto just in time (JIT)</strong>. Em vez de incluir tudo, o agente mantém referências leves e busca o material por ferramentas quando precisa. Claude Code lê sob demanda com glob e grep em vez de carregar toda a base.</p>
<p>memory tool e context editing, lançados com Sonnet 4.5, seguem a mesma filosofia. A “memória” de memory tool não é o histórico da sessão nem um <code>CLAUDE.md</code> escrito por alguém e carregado no início. memory tool é <strong>uma ferramenta com que o modelo escreve e lê arquivos diretamente</strong>.</p>
<p>A interface permite criar, ler, atualizar e excluir arquivos num diretório da infraestrutura do usuário. O modelo trata notas como sistema de arquivos. Isso cria memória persistente fora da janela entre sessões, enquanto o usuário controla armazenamento e retenção. A memória de sessão é volátil dentro; esta permanece fora. context editing faz o inverso: perto do limite, remove resultados antigos de ferramentas sem interromper a conversa, permitindo que o agente rode mais.</p>
<p>Na avaliação da Anthropic, as duas ferramentas melhoraram o desempenho em 39%; context editing sozinho, 29%. Numa busca web de 100 turnos, o consumo caiu 84%. (São benchmarks do fornecedor, mas a direção é clara: esvaziar o contexto importa mais.) Isso segue a mesma lógica da pipeline de cinco etapas de <code>/compact</code>. A diferença é que <code>/compact</code> reduz o interior do contexto, enquanto memory tool cria armazenamento exterior; os dois são complementares.</p>
<h2 id="conclusão"><a class="anchor" href="#conclusão">Conclusão</a></h2>
<p><img src="/content/260611/14.webp" alt="Engenharia de prompt ao lado de engenharia de contexto: uma consulta de um turno leva só prompt de sistema e mensagem do usuário, enquanto um agente seleciona documentos, ferramentas e memória e realimenta os resultados" width="1680" height="946" loading="lazy" fetchpriority="low" decoding="async"></p>
<p>No fim, a economia se resume a três eixos: <strong>enviar menos para o mesmo trabalho, pagar menos pela mesma entrada e entregar a mesma resposta a um modelo mais barato</strong>. Prompt caching, Batch API, isolamento com subagent, <code>/compact</code>, memory tool, context editing, model routing e modelos especializados apenas atacam eixos diferentes. Em 2026, o foco migra de preencher o contexto para esvaziá-lo e selecioná-lo — context engineering. Como mostra context rot, contexto leve melhora custo e precisão.</p>
<p>Não há resposta única. Cada equipe trabalha de um jeito, e uma mesma pessoa tem custos diferentes ao escrever texto ou código. Mas, num ecossistema rápido, supor que “o padrão de ontem ainda funciona hoje” é arriscado. Acompanhar modelos, ferramentas e preços e validá-los num pequeno POC do próprio fluxo talvez seja a economia mais duradoura. Recomendo revisar a própria contabilidade de tokens. (Para entender a raiz, leia também BPE e KV cache em Como os tokens funcionam.)</p>
<p>Resta uma pergunta: depois de esvaziar e selecionar contexto, o que vem? O foco avança ao sistema de agentes inteiro: como operá-lo (projeto do harness), medir se funcionou (eval) e confiná-lo quando falha (containment). Quando a postura “meça com um POC” deste artigo escala ao sistema, ela vira exatamente eval. Tratarei disso em <a href="/260622" target="_blank" rel="noopener noreferrer">Além do contexto</a>.</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:#3b82f6"></span><a href="https://platform.claude.com/docs/en/build-with-claude/prompt-caching" target="_blank" rel="noopener noreferrer">Anthropic Prompt Caching</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#3b82f6"></span><a href="https://platform.claude.com/docs/en/build-with-claude/batch-processing" target="_blank" rel="noopener noreferrer">Anthropic Batch Processing</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#3b82f6"></span><a href="https://platform.claude.com/docs/en/about-claude/pricing" target="_blank" rel="noopener noreferrer">Anthropic Claude API Pricing</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#3b82f6"></span><a href="https://code.claude.com/docs/en/sub-agents" target="_blank" rel="noopener noreferrer">Anthropic Claude Code Subagents</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#3b82f6"></span><a href="https://docs.claude.com/en/docs/claude-code/mcp" target="_blank" rel="noopener noreferrer">Anthropic Claude Code MCP Tool Search</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#3b82f6"></span><a href="https://docs.claude.com/en/docs/agents-and-tools/agent-skills/overview" target="_blank" rel="noopener noreferrer">Anthropic Agent Skills</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#3b82f6"></span><a href="https://developers.openai.com/api/docs/guides/prompt-caching" target="_blank" rel="noopener noreferrer">OpenAI Prompt Caching</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#3b82f6"></span><a href="https://openai.com/api/pricing/" target="_blank" rel="noopener noreferrer">OpenAI API Pricing</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#3b82f6"></span><a href="https://ai.google.dev/gemini-api/docs/caching" target="_blank" rel="noopener noreferrer">Google Gemini Context Caching</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#3b82f6"></span><a href="https://ai.google.dev/pricing" target="_blank" rel="noopener noreferrer">Google Gemini API Pricing</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#f59e0b"></span><a href="https://www.anthropic.com/news/context-management" target="_blank" rel="noopener noreferrer">Anthropic, Managing context on the Claude Developer Platform</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#8b5cf6"></span><a href="https://arxiv.org/abs/2601.07206" target="_blank" rel="noopener noreferrer">LLMRouterBench (Findings of ACL 2026)</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#8b5cf6"></span><a href="https://arxiv.org/abs/2604.14228" target="_blank" rel="noopener noreferrer">VILA-Lab, Dive into Claude Code</a></div>
</div>
</div></details>]]></content:encoded>
            <author>jihoon7705@gmail.com (이지훈)</author>
            <category>IA</category>
            <category>Tokens</category>
        </item>
        <item>
            <title><![CDATA[Como os tokens funcionam]]></title>
            <link>https://hooninedev.com/pt-BR/260610</link>
            <guid isPermaLink="false">https://hooninedev.com/pt-BR/260610</guid>
            <pubDate>Wed, 10 Jun 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Neste artigo, quero explicar o que são, afinal, os tokens de IA e como eles funcionam. Até agora, escrevi principalmente sobre como usar bem as ferramentas de IA, quais estão em alta e por quê. Mas, a...]]></description>
            <content:encoded><![CDATA[<p>Neste artigo, quero explicar o que são, afinal, os tokens de IA e como eles funcionam.</p>
<p>Até agora, escrevi principalmente sobre como usar bem as ferramentas de IA, quais estão em alta e por quê. Mas, ao preparar o texto sobre <a href="/260611" target="_blank" rel="noopener noreferrer">como economizar tokens</a>, percebi novamente algo importante: para falar em reduzir custos, primeiro é preciso entender “o que é um token e como ele é cobrado”, mas eu nunca havia explicado essa base de forma adequada. (Enquanto escrevia o guia de economia, a parte sobre o funcionamento dos tokens cresceu o bastante para virar um artigo próprio.)</p>
<p>Este texto, portanto, estabelece os fundamentos antes das técnicas de economia. Vamos ver o que exatamente é um token, se não é palavra nem caractere (BPE); em que formato ele entra no modelo (embedding); por que a saída custa mais que a entrada (prefill/decode); de onde, dentro do Transformer — a arquitetura da rede neural — vem a redução de preço do prompt caching (KV cache); e por que um contexto mais longo fica mais caro (o custo quadrático da atenção). Para medidas práticas, depois deste artigo continue em Como economizar tokens.</p>
<h2 id="token-não-é-palavra-nem-caractere"><a class="anchor" href="#token-não-é-palavra-nem-caractere">Token não é palavra nem caractere</a></h2>
<p>Comecemos pelo fato mais básico: <strong>token não é palavra nem caractere.</strong> É uma unidade de um vocabulário que o modelo cria ao comprimir sequências de caracteres frequentes nos dados de treinamento.</p>
<p>Nossa intuição ao perguntar “quantas palavras há nesta frase?” é diferente da forma como o modelo divide o texto. Ele agrupa em uma unidade as combinações de caracteres que aparecem juntas com frequência e segmenta o texto recebido de acordo com esse vocabulário. Por isso, uma palavra pode corresponder a um único token ou ser dividida em vários.</p>
<p>O algoritmo que constrói esse vocabulário é o BPE, uma base comum a praticamente todos os LLMs modernos.</p>
<h3 id="o-algoritmo-bpe"><a class="anchor" href="#o-algoritmo-bpe">O algoritmo BPE</a></h3>
<p>BPE (Byte Pair Encoding, ou codificação de pares de bytes) expande o vocabulário ao fundir repetidamente o par de símbolos adjacentes mais frequente em um novo símbolo.</p>
<p>O curioso é que ele não nasceu para o processamento de linguagem natural. Philip Gage propôs o BPE pela primeira vez em 1994 como técnica de <a href="/240706" target="_blank" rel="noopener noreferrer">compressão de dados</a>. O método substituía o par de bytes mais frequente por um byte não utilizado nos dados e armazenava as regras de substituição separadamente em uma tabela.</p>
<p>A equipe de Sennrich, da Universidade de Edimburgo, levou a ideia para o problema de vocabulário na tradução neural. No artigo “Neural Machine Translation of Rare Words with Subword Units”, publicado em 2015 e apresentado na ACL 2016, os pesquisadores usaram BPE para superar a incapacidade de um vocabulário fixo de lidar com palavras raras ou desconhecidas. Em vez de memorizar palavras inteiras, elas são representadas como combinações de fragmentos subword menores; assim, até uma palavra fora do vocabulário pode ser codificada com partes conhecidas.</p>
<p>O processo em si é surpreendentemente simples.</p>
<ol>
<li>No início, cada caractere individual é um token.</li>
<li>Encontra-se no corpus o par de tokens adjacentes que aparece com maior frequência.</li>
<li>O par é fundido em um novo token, que substitui todas as ocorrências desse par no corpus.</li>
<li>Os passos 2 e 3 são repetidos até o vocabulário alcançar o tamanho desejado.</li>
</ol>
<p>Assim, embora no começo cada caractere seja um token, combinações frequentes como “th”, “the” e “tion” acabam fundidas em tokens únicos. Quanto mais comum o padrão, maior a chance de ele se tornar um bloco maior.</p>
<p><img src="/content/260610/2.webp" alt="Como um tokenizer BPE expande o vocabulário ao fundir repetidamente pares frequentes de caracteres adjacentes" style="max-width:min(500px, 100%);height:auto;" width="1024" height="1536" loading="eager" fetchpriority="high" decoding="async"></p>
<p>A família GPT vai além e usa <strong>BPE em nível de bytes</strong>. Primeiro, o texto é convertido em um fluxo de bytes UTF-8; depois, aplica-se BPE. Isso permite começar com um vocabulário-base de apenas 256 itens — um para cada byte possível — e ainda codificar qualquer texto representável em UTF-8 sem tokens desconhecidos. Seja um emoji, um ideograma chinês ou um símbolo especial, sempre existe pelo menos uma representação no nível de byte.</p>
<h3 id="mesmo-texto-quantidades-diferentes-de-tokens"><a class="anchor" href="#mesmo-texto-quantidades-diferentes-de-tokens">Mesmo texto, quantidades diferentes de tokens</a></h3>
<p>Por causa dessa estrutura, o mesmo texto pode gerar números de tokens muito diferentes dependendo do tokenizer, a ferramenta que divide o texto escrito por pessoas em unidades que o modelo consegue processar. Conforme a forma de treinamento do vocabulário, a mesma frase pode ser quebrada em partes menores ou em blocos maiores.</p>
<p><picture><source type="image/webp" srcset="/content/260610/3.720w.webp 720w, /content/260610/3.1440w.webp 1440w, /content/260610/3.1680w.webp 1680w" sizes="(max-width: 840px) calc(100vw - 32px), 808px"><img src="/content/260610/3.png" alt="Comparação do número de tokens do mesmo texto em diferentes tokenizers; o200k_base usa menos tokens que cl100k_base em textos não ingleses" width="1680" height="997" loading="lazy" fetchpriority="low" decoding="async"></picture></p>
<p>O material publicado pela OpenAI com o GPT-4o mostra bem essa diferença. O novo tokenizer o200k_base da família GPT-4o representa o mesmo texto com menos tokens que o cl100k_base da família GPT-4. Em inglês, ele é apenas cerca de 1,1 vez mais eficiente, mas a diferença cresce em outros idiomas. Nos exemplos curtos publicados, a contagem caiu aproximadamente 1,4 vez em chinês e japonês, 1,7 vez em coreano e até 2,9 vezes em hindi. (Para quem usa coreano, é interessante como a diferença entre gerações de tokenizer se mostra muito mais perceptível do que em inglês.)</p>
<p>Em uma visão mais ampla, também há diferenças entre os próprios idiomas. Na análise de Yennie Jun com cl100k_base, frases com o mesmo sentido quase sempre usaram menos tokens em inglês, enquanto idiomas com sistemas de escrita próprios tenderam a usar mais. Hindi e bengali podem consumir cinco vezes mais tokens que o inglês; birmanês, mais de dez vezes. Coreano e chinês também são tokenizados em mais partes que o inglês, embora sem chegar a esses extremos.</p>
<p>Daí surge uma consequência prática importante. Ao adotar um novo tokenizer a partir do Opus 4.7, a Anthropic declarou na documentação oficial de preços que “o mesmo texto pode ser cobrado como até 35% mais tokens do que nos modelos anteriores”. <strong>Mesmo que o preço por token permaneça igual, a conta cresce na mesma proporção da quantidade de tokens.</strong> Uma comparação honesta entre modelos precisa considerar não apenas a tarifa, mas a tarifa multiplicada pelo número esperado de tokens.</p>
<p>Mas, quando esses tokens segmentados entram no modelo, eles não entram como caracteres ou palavras literais. Então, em que formato um token serve de entrada para o modelo?</p>
<h2 id="como-os-tokens-entram-no-modelo"><a class="anchor" href="#como-os-tokens-entram-no-modelo">Como os tokens entram no modelo</a></h2>
<p>Redes neurais não trabalham diretamente com texto. No fim das contas, o modelo só calcula com números — mais precisamente, vetores, que são conjuntos de números. Por isso, antes de entrar no modelo, os tokens passam por várias etapas que os transformam em valores numéricos.</p>
<p>A sequência é esta.</p>
<ol>
<li><strong>Texto → tokens</strong>: um tokenizer BPE divide o texto em fragmentos de token.</li>
<li><strong>Token → token ID</strong>: cada token é mapeado para um ID inteiro que indica sua posição no vocabulário. Por exemplo, se <code>" the"</code> for o 1.234º token, seu ID será <code>1234</code>.</li>
<li><strong>Token ID → vetor de embedding</strong>: o ID seleciona a linha correspondente da matriz de embeddings. Essa matriz é uma tabela enorme de tamanho <code>vocabulary size × model dimension (d_model)</code>; cada token ID corresponde a uma linha, isto é, a um vetor. Podemos imaginar esse vetor como coordenadas que carregam o “significado” do token.</li>
<li><strong>Adicionar informação posicional</strong>: sozinha, a autoatenção não sabe a ordem dos tokens. Em outras palavras, veria “eu gosto de você” e “você eu gosto de” como a mesma coisa. Portanto, injeta-se informação posicional em cada vetor de token para informar a ordem da sequência.</li>
</ol>
<p>Como isso soa abstrato apenas em palavras, incluí uma ferramenta interativa para explorar as quatro etapas. Ao mudar a frase ou clicar em um token, é possível acompanhar o caminho do ID ao vetor de embedding e ver a informação posicional ser adicionada até formar o vetor de entrada final. (Só algumas dimensões são exibidas, mas vale lembrar que o vetor real do GPT-4 tem 12.288.)</p>
<p>A dimensão do modelo (d_model) é o comprimento do vetor que representa um token. No artigo original do Transformer, o valor era 512, mas nos LLMs atuais é muito maior. (Até o Llama 2 7B usa 4.096 dimensões.) Uma dimensão maior representa diferenças sutis de significado com mais riqueza, porém também aumenta o volume de cálculo e memória.</p>
<p><picture><source type="image/webp" srcset="/content/260610/4.704w.webp 704w" sizes="(max-width: 840px) calc(100vw - 32px), 808px"><img src="/content/260610/4.png" alt="Como um token ID consulta uma linha da matriz de embeddings e se transforma em um vetor de dimensão d_model" width="704" height="435" loading="lazy" fetchpriority="low" decoding="async"></picture></p>
<p>A forma de injetar informação posicional também mudou ao longo das gerações. O Transformer original somava aos embeddings vetores de posição construídos com funções seno e cosseno. Já o RoPE (Rotary Position Embedding), hoje padrão de fato em LLMs abertos, injeta a posição relativa durante o cálculo da atenção ao rotacionar os vetores Query e Key. (O mecanismo muda, mas o objetivo é o mesmo: dar ao Transformer noção de ordem.)</p>
<p>Em resumo, o fluxo é <strong>texto → tokens → token IDs → vetores de embedding (+ informação posicional) → entrada do Transformer</strong>. A “contagem de tokens” da fatura corresponde à segunda etapa desse fluxo: quantos tokens resultaram da segmentação do texto.</p>
<p>Agora sabemos como os tokens são criados e em que formato entram no modelo. Mas, quando são processados, por que entrada e saída custam tão diferente, e como reduzir o custo de enviar repetidamente a mesma entrada?</p>
<h2 id="por-que-a-entrada-é-barata-e-a-saída-é-cara"><a class="anchor" href="#por-que-a-entrada-é-barata-e-a-saída-é-cara">Por que a entrada é barata e a saída é cara</a></h2>
<p>Quem já trabalhou com tokens provavelmente se perguntou: nas tabelas de preços, <strong>tokens de saída custam várias vezes mais que tokens de entrada.</strong> Na Anthropic, em todos os modelos, a tarifa de saída é exatamente cinco vezes a de entrada. (No Opus, são $5 de entrada e $25 de saída; no Haiku, $1 e $5.) Se é o mesmo token, por que o preço muda conforme entra ou sai?</p>
<p>A resposta é que o modelo processa tokens de maneiras completamente diferentes na entrada e na saída. A inferência de um LLM tem duas etapas.</p>
<ul>
<li><strong>prefill (processamento da entrada)</strong>: processa todos os tokens da instrução <strong>de uma só vez e em paralelo</strong>. Com 1.000 ou 10.000 tokens, a GPU percorre todos em conjunto e calcula os vetores K/V (Key/Value) de cada token. O cálculo total é grande, mas o paralelismo proporciona boa eficiência por token.</li>
<li><strong>decode (geração da saída)</strong>: gera os tokens da resposta <strong>sequencialmente, um de cada vez</strong>. Depois de produzir um token, ele é acrescentado à entrada e o próximo é gerado. O ciclo se repete até o fim da resposta.</li>
</ul>
<p>Essa assimetria cria a diferença de custo. Segundo a análise de desempenho de inferência da Databricks, prefill é uma etapa limitada por computação, enquanto decode é limitada pela largura de banda da memória. Durante decode, para produzir cada token, é preciso reler da memória da GPU todos os pesos do modelo; no entanto, cada passagem gera apenas um token. Grande parte da enorme capacidade de cálculo da GPU fica ociosa. (Nas palavras da Databricks, você paga para manter a GPU ligada sem usar a computação disponível.)</p>
<p>Em outras palavras, <strong>tokens de entrada são processados juntos com eficiência no prefill paralelo, enquanto tokens de saída precisam ser extraídos de forma ineficiente, um a um.</strong> Fatores comerciais como demanda e margem também podem afetar os preços, mas essa ineficiência de decode compõe a explicação técnica. Portanto, “manter a saída curta” não é apenas uma dica genérica de economia: reduz diretamente a etapa mais cara.</p>
<p>Existe alguma forma de tornar decode ao menos um pouco menos ineficiente? É aqui que entra o KV cache.</p>
<h2 id="como-o-armazenamento-em-cache-reduz-o-preço"><a class="anchor" href="#como-o-armazenamento-em-cache-reduz-o-preço">Como o armazenamento em cache reduz o preço</a></h2>
<p>Prompt caching armazena uma vez a entrada estática para que as chamadas seguintes a leiam por um preço bem menor. Segundo a documentação oficial da Anthropic, uma leitura custa 0,1 vez a tarifa-base de entrada, ou exatamente 10%. A escrita custa 1,25 vez com TTL (Time To Live, período de validade dos dados armazenados) de cinco minutos e 2 vezes com TTL de uma hora. Paga-se um pouco mais na primeira chamada e economiza-se 90% a partir da segunda.</p>
<p>Mas como o preço pode cair 90%? E por que a exigência de que “o prefixo coincida exatamente” é tão rigorosa? As duas respostas ficam claras quando olhamos dentro do Transformer.</p>
<h3 id="kv-cache"><a class="anchor" href="#kv-cache">KV cache</a></h3>
<p>Ao processar tokens de entrada, o modelo cria os vetores Query/Key/Value (Q/K/V) correspondentes. A autoatenção calcula pontuações de atenção pelo produto escalar de Query e Key e usa essas pontuações para produzir uma soma ponderada dos valores. É o cálculo que determina quanto um token deve “consultar” cada um dos outros.</p>
<p>É justamente aí que a etapa de decode mencionada antes se torna problemática. Para gerar cada novo token, o modelo precisa dos K/V de todos os tokens anteriores. Recalculá-los do zero em todas as etapas repetiria o mesmo trabalho indefinidamente.</p>
<p><img src="/content/260610/1.webp" alt="Estrutura de funcionamento do KV cache do Transformer" width="1536" height="1024" loading="lazy" fetchpriority="low" decoding="async"></p>
<p>O KV cache elimina essa duplicação. Ele armazena os K/V já calculados e, ao gerar um novo token, lê e reutiliza os K/V anteriores, em vez de calculá-los novamente. Dentro de uma sequência, o custo de gerar mais um token cai de O(n²), que recalcularia toda a sequência a cada etapa, para O(n), que apenas lê os dados armazenados. (A rigor, esse é o custo por etapa; a ideia central é simples: não recalcular o que já foi calculado.) É também por isso que decode é limitado pela memória: cada etapa precisa reler o KV cache da memória.</p>
<h3 id="reutilização-entre-chamadas"><a class="anchor" href="#reutilização-entre-chamadas">Reutilização entre chamadas</a></h3>
<p>Se o KV cache foi criado para acelerar decode dentro de uma resposta, prompt caching propõe reutilizar essa estrutura de memória <strong>não apenas dentro de uma chamada, mas também entre uma chamada e a próxima</strong>.</p>
<p>Os K/V de um prefixo estático — como uma instrução de sistema, definições de ferramentas ou trechos de código cujo início muda pouco — permanecem na memória da GPU por um TTL de cinco minutos ou uma hora. Se a chamada seguinte começar com o mesmo prefixo, o modelo ignora por completo o cálculo desses K/V e os lê diretamente dos dados armazenados. Mais precisamente, o preço dos tokens de entrada não cai por mágica: <strong>o trabalho de GPU necessário para calculá-los deixa de existir.</strong> A redução de 90% reflete a computação evitada.</p>
<p>Ao entender o mecanismo, também fica clara a exigência de correspondência exata do prefixo. Um acerto de cache é determinado pela comparação do hash cumulativo do prefixo. Como a autoatenção é causal, mudar um único token anterior altera os K/V de todos os tokens seguintes. Basta colocar uma marca temporal no começo da instrução: o valor muda a cada chamada, o hash deixa de coincidir e todos os dados armazenados depois desse ponto são invalidados.</p>
<p>A Anthropic lê os dados armazenados na hierarquia <code>tools</code> → <code>system</code> → <code>messages</code>. Por isso, mudar uma única definição de ferramenta no início invalida tudo o que vem depois. A conclusão é simples: <strong>conteúdo estático deve ficar no começo; conteúdo dinâmico, que muda a cada chamada, no fim.</strong></p>
<p>O armazenamento em cache também tem restrições menos conhecidas. A quantidade mínima de tokens que pode ser armazenada varia por modelo e até por versão da mesma família. Uma instrução abaixo desse limite não é armazenada, sem qualquer aviso, mesmo com esse recurso habilitado. (Como esses mínimos e as políticas de cada provedor afetam diretamente o planejamento de custos, eu os detalho junto às tabelas de preços no guia de economia de tokens.)</p>
<h2 id="por-que-a-janela-de-contexto-impõe-um-limite-de-tokens"><a class="anchor" href="#por-que-a-janela-de-contexto-impõe-um-limite-de-tokens">Por que a janela de contexto impõe um limite de tokens</a></h2>
<p>O último conceito indispensável é a janela de contexto: o número máximo de tokens que um modelo aceita de uma vez. Entender por que esse limite existe também esclarece o conhecido aviso de que “um contexto mais longo custa mais”.</p>
<p>A chave está na estrutura de cálculo da autoatenção. A atenção faz cada token consultar todos os outros uma vez. Com n tokens, existem n × n pares; portanto, <strong>a computação e a memória da autoatenção crescem com o quadrado do comprimento n da sequência.</strong> Ao dobrar o número de tokens, o custo da atenção quadruplica. (Vale esclarecer que não é o modelo inteiro que tem custo O(n²), mas a etapa de atenção.)</p>
<p><picture><source type="image/webp" srcset="/content/260610/5.330w.webp 330w" sizes="(max-width: 840px) calc(100vw - 32px), 808px"><img src="/content/260610/5.png" alt="Matriz de autoatenção em que cada token consulta todos os outros; com n tokens, o número de pares cresce como n×n" width="330" height="304" loading="lazy" fetchpriority="low" decoding="async"></picture></p>
<p>As <a href="https://huggingface.co/docs/transformers/en/llm_tutorial_optimization" target="_blank" rel="noopener noreferrer">estimativas da Hugging Face</a> mostram como esse crescimento quadrático é acentuado. Com atenção padrão, sem otimização, apenas armazenar a matriz de pontuações exige cerca de 50 MB para uma entrada de 1.000 tokens, aproximadamente 19 GB para 16.000 tokens e quase 1 TB para 100.000.</p>
<p>O KV cache também ocupa memória proporcionalmente ao número de tokens. À medida que o contexto cresce, os K/V que precisam ser mantidos se acumulam linearmente. No fim, o limite da janela de contexto é uma fronteira física definida pelo modelo: além dela, memória e computação se tornam inviáveis.</p>
<p>Por isso, carregar uma conversa longa sem alterações não significa apenas somar tokens de entrada. O custo da atenção cresce ao quadrado e, quanto mais a contagem se aproxima do limite, menor também é a precisão com que o modelo lida com a informação. (Essa perda de precisão está diretamente ligada à degradação do contexto, abordada no guia de economia; manter o contexto enxuto beneficia tanto o custo quanto a qualidade das respostas.)</p>
<h2 id="conclusão"><a class="anchor" href="#conclusão">Conclusão</a></h2>
<p>Em conjunto, o funcionamento dos tokens se resume a alguns fatos simples. <strong>Token é uma unidade de vocabulário criada pelo BPE ao agrupar padrões frequentes</strong>; depois de ser mapeado para um token ID, ele vira um vetor de embedding e entra no modelo. <strong>A entrada é processada de uma só vez com prefill paralelo, enquanto a saída é mais cara porque precisa ser extraída token por token</strong>; o KV cache reduz essa ineficiência ao reutilizar os K/V. <strong>O armazenamento reduz o preço não por mágica, mas porque o Transformer evita recalcular K/V já obtidos</strong>, e a exigência rigorosa de um prefixo idêntico decorre diretamente dessa estrutura. Por fim, um contexto mais longo custa mais porque o custo da atenção cresce com o quadrado do número de tokens.</p>
<p>Com essa base, a economia dos tokens fica muito mais clara. Encurtar a saída, organizar a instrução para preservar os dados armazenados, manter o contexto enxuto e migrar para um modelo mais barato quando ele oferece a mesma resposta são estratégias apoiadas nesses princípios. Se você chegou até aqui para entender o mecanismo, recomendo continuar no guia de economia de tokens e ver como essas ideias reduzem custos reais.</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:#8b5cf6"></span><a href="https://en.wikipedia.org/wiki/Byte-pair_encoding" target="_blank" rel="noopener noreferrer">Philip Gage, A New Algorithm for Data Compression (1994)</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#8b5cf6"></span><a href="https://arxiv.org/abs/1508.07909" target="_blank" rel="noopener noreferrer">Sennrich, Haddow, Birch, Neural Machine Translation of Rare Words with Subword Units (ACL 2016)</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#f59e0b"></span><a href="https://jalammar.github.io/illustrated-transformer/" target="_blank" rel="noopener noreferrer">Jay Alammar, The Illustrated Transformer</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#3b82f6"></span><a href="https://huggingface.co/learn/nlp-course/en/chapter6/5" target="_blank" rel="noopener noreferrer">HuggingFace, Byte-Pair Encoding tokenization</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#3b82f6"></span><a href="https://huggingface.co/docs/transformers/en/kv_cache" target="_blank" rel="noopener noreferrer">HuggingFace, Transformers KV Cache strategies</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/openai/tiktoken" target="_blank" rel="noopener noreferrer">OpenAI, tiktoken</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#f59e0b"></span><a href="https://openai.com/index/hello-gpt-4o/" target="_blank" rel="noopener noreferrer">OpenAI, Hello GPT-4o (Language tokenization)</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#f59e0b"></span><a href="https://www.artfish.ai/p/all-languages-are-not-created-tokenized" target="_blank" rel="noopener noreferrer">Yennie Jun, All languages are NOT created (tokenized) equal</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#f59e0b"></span><a href="https://www.databricks.com/blog/llm-inference-performance-engineering-best-practices" target="_blank" rel="noopener noreferrer">Databricks, LLM Inference Performance Engineering Best Practices</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#f59e0b"></span><a href="https://www.baseten.co/blog/llm-transformer-inference-guide/" target="_blank" rel="noopener noreferrer">Baseten, A guide to LLM inference and performance</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#3b82f6"></span><a href="https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching" target="_blank" rel="noopener noreferrer">Anthropic, Prompt Caching</a></div>
</div>
</div></details>]]></content:encoded>
            <author>jihoon7705@gmail.com (이지훈)</author>
            <category>IA</category>
            <category>Tokens</category>
        </item>
        <item>
            <title><![CDATA[Arquivos de contexto]]></title>
            <link>https://hooninedev.com/pt-BR/260529</link>
            <guid isPermaLink="false">https://hooninedev.com/pt-BR/260529</guid>
            <pubDate>Fri, 29 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Neste post, quero falar sobre os arquivos de contexto que os agentes de programação com IA leem. Este texto é para quem tem CLAUDE.md, AGENTS.md, SKILL.md e .cursor/rules acumulados no mesmo projeto e...]]></description>
            <content:encoded><![CDATA[<p>Neste post, quero falar sobre <strong>os arquivos de contexto que os agentes de programação com IA leem</strong>.</p>
<p>Este texto é para quem tem <code>CLAUDE.md</code>, <code>AGENTS.md</code>, <code>SKILL.md</code> e <code>.cursor/rules</code> acumulados no mesmo projeto e não sabe ao certo quando cada um é lido nem o que escrever nele. Ao final, você conhecerá as diferenças entre esses arquivos, como os agentes os carregam e um critério apoiado por um estudo da ETH Zurich: escrever apenas as informações que o agente não consegue inferir.</p>
<p>Trabalho como desenvolvedor frontend e uso Claude no dia a dia. Com isso, em algum momento surgiu um <code>CLAUDE.md</code> na raiz do projeto, ao lado havia um <code>AGENTS.md</code> criado por alguém, um <code>.cursorrules</code> continuava esquecido em algum canto e eu cheguei até a criar uma pasta <code>.claude/skills/</code> seguindo um artigo que encontrei por aí. (Quando me dei conta, havia uns cinco arquivos com conteúdos parecidos.)</p>
<h2 id="agentes-sem-memória-persistente"><a class="anchor" href="#agentes-sem-memória-persistente">Agentes sem memória persistente</a></h2>
<p>Os agentes de programação com IA têm uma limitação fundamental: <strong>não possuem memória persistente</strong>. Toda sessão começa do zero, e na conversa seguinte eles não se lembram de convenções combinadas ontem nem da estrutura de pastas explicada uma hora antes. Os arquivos de contexto são o mecanismo mais simples para resolver esse problema. Se o projeto tiver um arquivo lido automaticamente no início de cada sessão, não será necessário repetir sempre as mesmas explicações.</p>
<p>O problema é que cada ferramenta criou seu próprio arquivo a partir da mesma ideia. Claude Code lê <code>CLAUDE.md</code>; Cursor, <code>.cursorrules</code> (que hoje está deprecated, e a recomendação é usar <code>.cursor/rules</code>); GitHub Copilot, <code>.github/copilot-instructions.md</code>; e OpenAI Codex, <code>AGENTS.md</code>. Quando uma equipe usa várias ferramentas, acaba tendo de copiar o mesmo conteúdo para quatro lugares diferentes.</p>
<h3 id="claudemd"><a class="anchor" href="#claudemd">CLAUDE.md</a></h3>
<p><code>CLAUDE.md</code> é um arquivo que o Claude Code lê automaticamente no início da sessão. Segundo a documentação oficial da Anthropic (<code>code.claude.com/docs/en/memory</code>), o Claude Code procura <code>CLAUDE.md</code> nos três níveis abaixo.</p>
<ul>
<li><strong>Memória do usuário</strong> (<code>~/.claude/CLAUDE.md</code>): valores-padrão globais aplicados a todos os projetos da máquina</li>
<li><strong>Memória do projeto</strong> (<code>CLAUDE.md</code> na raiz do projeto): versionada no Git e compartilhada por toda a equipe</li>
<li><strong>Memória local</strong> (<code>CLAUDE.md</code> em um subdiretório): carregada adicionalmente apenas ao trabalhar naquele diretório</li>
</ul>
<p>Se os três níveis existirem, Claude <strong>lê todos e os concatena (concatenate)</strong>. Ele não escolhe apenas um por ordem de prioridade; a estrutura se parece com a cascade do CSS, em que o conteúdo mais específico é acrescentado por cima. (É uma mesclagem, não um override.) Portanto, espalhar regras sobre o mesmo assunto por vários níveis pode gerar conflitos. (A documentação oficial da Anthropic afirma que o comportamento em caso de conflito não é garantido.)</p>
<p>Há um detalhe frequentemente ignorado: <strong>todos os arquivos <code>CLAUDE.md</code> encontrados ao subir do diretório de trabalho atual até a raiz do repositório são lidos</strong>. Assim, ao trabalhar em <code>packages/ui/</code> dentro de um monorepo, tanto o <code>CLAUDE.md</code> da raiz quanto <code>packages/ui/CLAUDE.md</code> são carregados. (Isso é poderoso, mas também significa que o contexto pode crescer sem que ninguém perceba.)</p>
<h3 id="agentsmd"><a class="anchor" href="#agentsmd">AGENTS.md</a></h3>
<p><code>AGENTS.md</code> é um padrão criado para resolver a proliferação de arquivos específicos de cada ferramenta descrita acima. Em dezembro de 2025, a OpenAI doou esse padrão à <strong>Agentic AI Foundation (AAIF)</strong>, vinculada à Linux Foundation. Segundo o <a href="https://www.linuxfoundation.org/press/linux-foundation-announces-the-formation-of-the-agentic-ai-foundation" target="_blank" rel="noopener noreferrer">anúncio da Linux Foundation</a>, a AAIF foi cofundada por Anthropic, Block e OpenAI, e seus projetos fundadores são três: o MCP da Anthropic (o protocolo que conecta agentes a sistemas externos), o goose da Block e o <code>AGENTS.md</code> da OpenAI. O site oficial (<code>agents.md</code>) afirma que <strong>mais de 60 mil repositórios open source já adotam esse arquivo</strong>.</p>
<p>A lista de ferramentas compatíveis deixa isso ainda mais claro. OpenAI Codex, Google Jules, VS Code, GitHub Copilot, Cursor, JetBrains Junie, Aider, Devin, Zed, Factory, Warp, goose, opencode, Amp, RooCode, Gemini CLI, Kilo Code, Phoenix, Semgrep, Ona, Windsurf e Augment Code estão entre as muitas ferramentas com suporte. O GitHub Copilot passou a oferecer suporte nativo a <code>AGENTS.md</code> em agosto de 2025. Um detalhe interessante é que <strong>o suporte nativo do Claude Code a <code>AGENTS.md</code> ainda está no estado de active feature request</strong>. Para o Claude Code, <code>CLAUDE.md</code> continua sendo o arquivo principal.</p>
<p>Mesmo sendo chamado de padrão, é razoável desconfiar se ele está realmente sendo adotado. A evidência mais forte é o <strong>dogfooding</strong> (quando quem cria um padrão também o utiliza).</p>
<ul>
<li>A raiz da branch canary de <strong>Vercel/Next.js</strong> contém um <code>AGENTS.md</code>. Na verdade, trata-se de um link simbólico para <code>CLAUDE.md</code>, cujo conteúdo inclui a estrutura do monorepo, iterações de 1–2 segundos com <code>pnpm --filter=next dev</code>, orientações de testes para Turbopack e Webpack, o script <code>pr-status</code> e regras para variáveis de ambiente e secrets. O fato de <code>create-next-app</code> ter passado a gerar <code>AGENTS.md</code> e <code>CLAUDE.md</code> juntos em novos projetos faz parte do mesmo movimento.</li>
<li>O próprio repositório <strong>OpenAI/codex</strong> mantém seu <code>AGENTS.md</code>.</li>
</ul>
<p>Do ponto de vista estratégico, a abordagem que vem se consolidando é esta: manter <strong><code>AGENTS.md</code> como single source of truth</strong> e reduzir <code>CLAUDE.md</code> ao mínimo, deixando nele apenas uma linha que referencia <code>AGENTS.md</code> e instruções específicas do Claude Code. Isso elimina duplicações e, como o Claude Code lê os dois arquivos, não há perda de informação.</p>
<h3 id="skillmd"><a class="anchor" href="#skillmd">SKILL.md</a></h3>
<p><code>SKILL.md</code> tem uma natureza diferente dos dois arquivos anteriores. Enquanto <code>CLAUDE.md</code> e <code>AGENTS.md</code> são <strong>instruções persistentes, sempre presentes no contexto</strong>, uma Skill é uma <strong>capacidade on-demand, invocada apenas quando necessária</strong>.</p>
<p>Uma Skill é organizada como uma pasta. Dentro dela ficam um arquivo <code>SKILL.md</code>, scripts executados pela Skill e documentos Markdown adicionais. Claude só carrega essa pasta quando a tarefa atual corresponde ao <code>description</code> da Skill. Esse mecanismo é chamado de <strong>progressive disclosure (divulgação progressiva)</strong>, conceito estabelecido por Jakob Nielsen na área de UX em 1995. A técnica reduz a carga cognitiva e os erros ao deslocar funções avançadas ou pouco usadas para telas secundárias, permitindo que o usuário se concentre em uma tarefa de cada vez. No contexto das Claude Skills, o termo se refere ao mecanismo de “trazer o corpo da Skill para o contexto apenas quando necessário”. O resultado é uma economia drástica no custo da janela de contexto.</p>
<p>O frontmatter de <code>SKILL.md</code> possui alguns campos próprios.</p>
<ul>
<li><strong><code>description</code></strong>: descreve em quais situações a Skill é necessária e funciona como gatilho para o modelo decidir se deve invocá-la</li>
<li><strong><code>allowed-tools</code></strong>: restringe as ferramentas disponíveis dentro da Skill (por exemplo, <code>"Read, Glob, Grep, Bash(python:*)"</code>)</li>
<li><strong><code>disable-model-invocation: true</code></strong>: impede que o modelo invoque a Skill; somente o usuário pode acioná-la por um comando slash. É usado em operações com efeitos colaterais, como deploy e commit</li>
<li><strong><code>user-invocable: false</code></strong>: oculta a Skill do menu de comandos slash do usuário; apenas Claude pode invocá-la de forma autônoma como conhecimento de apoio</li>
</ul>
<p>Claude Skills foi lançado simultaneamente no Claude.ai, Claude Code, API e Agent SDK em 16 de outubro de 2025. Em 18 de dezembro de 2025, a Anthropic publicou a própria especificação das Skills como padrão aberto (<code>agentskills.io</code>). Simon Willison chegou a avaliá-las dizendo: “<strong>Skills are awesome, maybe a bigger deal than MCP</strong>”. O motivo é que seu formato é drasticamente mais simples que o MCP e, ao mesmo tempo, resolve o custo da janela de contexto por meio de progressive disclosure.</p>
<p>O MCP (Model Context Protocol), com o qual as Skills foram comparadas aqui, é um protocolo padrão que conecta o agente a sistemas externos como Slack, GitHub ou um banco de dados para que ele possa chamá-los. Se os arquivos de contexto tratam do que informar ao agente, o MCP trata do que permitir que ele faça. Como o MCP difere de function calling está explicado à parte em <a href="/260524" target="_blank" rel="noopener noreferrer">MCP e function calling</a>.</p>
<p>As ferramentas que reduzem o custo de o agente encontrar o código relevante (Repomix, Aider, CodeGraph, Serena) reduzem custos diferentes conforme a profundidade com que entendem o código. Essa comparação está explicada à parte em <a href="/260526" target="_blank" rel="noopener noreferrer">Quatro camadas de inteligência de código</a>.</p>
<h3 id="arquivos-de-outras-ferramentas"><a class="anchor" href="#arquivos-de-outras-ferramentas">Arquivos de outras ferramentas</a></h3>
<p>O <code>.cursorrules</code> do Cursor foi <strong>deprecated a partir da versão 0.43</strong>. A recomendação oficial atual é criar vários arquivos dentro do diretório <code>.cursor/rules/</code>, cada um com a extensão <code>.mdc</code>. Cada arquivo <code>.mdc</code> possui um frontmatter YAML.</p>
<ul>
<li><strong><code>description</code></strong>: usado pelo agente para avaliar a relevância da regra</li>
<li><strong><code>globs</code></strong>: faz auto-attach quando um arquivo correspondente ao padrão é incluído na conversa</li>
<li><strong><code>alwaysApply</code></strong>: quando <code>true</code>, inclui a regra obrigatoriamente em todas as conversas (nesse caso, <code>globs</code> é ignorado)</li>
</ul>
<p>O GitHub Copilot evoluiu em uma direção semelhante. Instruções válidas para todo o repositório ficam em <code>.github/copilot-instructions.md</code>; quando é necessário definir escopo por caminho, criam-se arquivos <code>.github/instructions/*.instructions.md</code>, usando a chave <code>applyTo:</code> no frontmatter para especificar o glob. (O Copilot code review passou a oferecer suporte oficial a path-scoped instructions em setembro de 2025.)</p>
<p>As demais ferramentas também convergem para padrões parecidos. A tabela abaixo resume o cenário.</p>
<table>
<thead>
<tr>
<th>Ferramenta</th>
<th>Arquivo/diretório</th>
<th>Característica</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Claude Code</strong></td>
<td><code>CLAUDE.md</code> (3 níveis)</td>
<td>Mesclagem ao longo da árvore de diretórios</td>
</tr>
<tr>
<td><strong>Cursor</strong></td>
<td><code>.cursor/rules/*.mdc</code></td>
<td>Escopo por padrão de arquivos com <code>globs</code></td>
</tr>
<tr>
<td><strong>GitHub Copilot</strong></td>
<td><code>.github/copilot-instructions.md</code> + <code>.github/instructions/*.instructions.md</code></td>
<td>Suporte a glob com <code>applyTo</code></td>
</tr>
<tr>
<td><strong>Cline</strong></td>
<td>diretório <code>.clinerules/</code></td>
<td>Combina todos os arquivos <code>.md</code>/<code>.txt</code>; ativação condicional por glob em <code>paths</code></td>
</tr>
<tr>
<td><strong>Continue.dev</strong></td>
<td><code>.continue/rules/*.md</code></td>
<td>Frontmatter com <code>name</code>/<code>globs</code>/<code>alwaysApply</code></td>
</tr>
<tr>
<td><strong>Aider</strong></td>
<td><code>CONVENTIONS.md</code> + <code>.aider.conf.yml</code></td>
<td>Incluído em toda solicitação; <strong>recomendação de no máximo 200 linhas</strong></td>
</tr>
<tr>
<td><strong>Windsurf</strong></td>
<td><code>.windsurfrules</code> + <code>global_rules.md</code></td>
<td>Dois níveis: global e projeto</td>
</tr>
<tr>
<td><strong>Padrão</strong></td>
<td><code>AGENTS.md</code> (AAIF)</td>
<td>Adotado por mais de 60.000 repositórios</td>
</tr>
</tbody>
</table>
<p>O <strong><code>CONVENTIONS.md</code> do Aider é especialmente interessante</strong>. Como a documentação oficial informa que o arquivo inteiro é incluído no contexto a cada solicitação, ela determina explicitamente: <strong>“mantenha-o com no máximo 200 linhas”</strong>. (Pode-se dizer que o Aider reconheceu essa limitação cedo e a comunicou diretamente aos usuários.)</p>
<h3 id="memorymd"><a class="anchor" href="#memorymd">MEMORY.md</a></h3>
<p>Além dos arquivos anteriores, outro padrão aparece com frequência cada vez maior: <code>MEMORY.md</code>. Não é um padrão oficial, mas uma convenção que surgiu organicamente na comunidade para <strong>registrar decisões e erros ao longo do tempo</strong>.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="markdown" data-theme="github-dark github-light"><code data-language="markdown" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold;--shiki-light:#005CC5;--shiki-light-font-weight:bold">## 2026-04-10</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">Pages Router에서 App Router로 이전. 신규 라우트는 App Router 컨벤션 사용.</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold;--shiki-light:#005CC5;--shiki-light-font-weight:bold">## 2026-04-22</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">Prisma 쿼리 결과에 optional chaining 쓰지 말 것 — null은 if-check로 명시적 처리.</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(이전에 옵셔널 체이닝으로 null을 흘려보내 프로덕션 이슈 발생.)</span></span></code></pre></figure>
<p>Se <code>CLAUDE.md</code> e <code>AGENTS.md</code> registram <strong>as regras vigentes no momento</strong>, <code>MEMORY.md</code> registra <strong>a história de por que essas regras foram criadas</strong>. (Eles se complementam; um não substitui o outro.)</p>
<h3 id="como-o-agente-lê-esses-arquivos"><a class="anchor" href="#como-o-agente-lê-esses-arquivos">Como o agente lê esses arquivos</a></h3>
<p>Até aqui, vimos quais arquivos existem. Mas há uma pergunta que, surpreendentemente, costuma ficar de fora: <strong>para onde e de que forma exatamente o agente lê esses arquivos?</strong> Entender esse mecanismo ajuda a compreender com mais clareza o resultado da ETH Zurich apresentado adiante — o de que arquivos de contexto não são seguidos tão bem quanto se esperaria.</p>
<p>Primeiro, um fato importante: <strong><code>CLAUDE.md</code> não é um system prompt; ele é injetado como user message.</strong> A documentação oficial da Anthropic afirma o seguinte:</p>
<blockquote class="bilingual-quote"><div class="quote-translation" lang="ko"><p>O conteúdo de CLAUDE.md é entregue como uma mensagem do usuário após o prompt do sistema, e não como parte do próprio prompt do sistema. Claude o lê e tenta segui-lo, mas não há garantia de conformidade estrita.</p></div><div class="quote-original" lang="en"><p>CLAUDE.md content is delivered as a user message after the system prompt, not as part of the system prompt itself. Claude reads it and tries to follow it, but there's no guarantee of strict compliance.</p></div></blockquote>
<p>Ou seja, ele não é uma regra obrigatória, mas um “contexto de referência”. Para impor um comportamento específico, o guia oficial recomenda usar um mecanismo separado, como um hook <code>PreToolUse</code>.</p>
<p>A ordem de carregamento se acumula de broad → specific. Mais precisamente: managed policy (configuração da organização) → global do usuário (<code>~/.claude/CLAUDE.md</code>) → projeto (<code>./CLAUDE.md</code>) → local (<code>./CLAUDE.local.md</code>). Dentro do mesmo diretório, <code>CLAUDE.md</code> vem antes de <code>CLAUDE.local.md</code>. É possível aproveitar o fato de que <strong>a instrução mais próxima é lida por último</strong> para fazer regras mais específicas terem maior peso, graças ao recency bias dos LLMs.</p>
<p>Outro ponto interessante é a sintaxe <code>@import</code>. Ao escrever <code>@path/to/file</code> em qualquer lugar do corpo de CLAUDE.md, esse arquivo é expandido naquele ponto e carregado junto. <strong>A profundidade recursiva máxima é de 4 hops</strong>, e caminhos relativos são resolvidos a partir do arquivo que contém o import. Por isso, a recomendação oficial é usar <code>@AGENTS.md</code> como ponte. Se <code>CLAUDE.md</code> ficar quase vazio, contendo apenas a linha <code>@AGENTS.md</code>, o Claude Code também lerá AGENTS.md naturalmente. (Na situação atual, em que CLAUDE.md ainda não oferece suporte nativo a AGENTS.md, essa é a solução alternativa mais limpa.)</p>
<p>Também vale examinar os tokens. CLAUDE.md não possui um limite explícito de tokens, portanto, <strong>se existir, será carregado por inteiro</strong>. Ainda assim, a recomendação oficial é de <strong>no máximo 200 linhas por arquivo</strong>. Acima de 200 linhas, a documentação diz “consume more context and may reduce adherence”. Curiosamente, no Claude 4.x, <strong>apenas habilitar o tool use adiciona automaticamente +346 tokens a um special system prompt</strong> (com <code>tool_choice: auto</code>). O contexto vaza sem que se perceba.</p>
<p>O Cursor funciona de outra maneira. As regras em <code>.cursor/rules/*.mdc</code> operam em quatro modos.</p>
<ul>
<li><strong>Always Apply</strong>: incluída obrigatoriamente em todos os chats; ignora globs/description</li>
<li><strong>Apply Intelligently</strong> (Agent Requested): o agente lê <code>description</code>, avalia a relevância e usa a regra quando necessário</li>
<li><strong>Apply to Specific Files</strong> (Auto Attached): ativada quando um arquivo que corresponde ao padrão glob entra no contexto</li>
<li><strong>Apply Manually</strong>: invocada explicitamente pelo usuário com <code>@rule-name</code></li>
</ul>
<p>Outras ferramentas funcionam de formas diferentes. O OpenAI Codex percorre o caminho da raiz do repositório Git até o cwd, coleta todos os <code>AGENTS.md</code> e os injeta <strong>imediatamente antes do prompt do usuário</strong>. Já o GitHub Copilot insere <code>.github/copilot-instructions.md</code> em uma prioridade intermediária da janela de contexto: “depois do edit context e das explicit references, mas antes de open files loosely related”. Mesmo quando usam o mesmo arquivo <code>AGENTS.md</code>, ferramentas diferentes podem carregá-lo em momentos distintos, com prioridades e regras de mesclagem diferentes; portanto, <strong>não há garantia de que três ferramentas enxerguem esse arquivo exatamente da mesma maneira.</strong></p>
<p>Mas resta uma pergunta fundamental: <strong>por que o modelo segue apenas parte das instruções presentes no contexto?</strong> Dizer apenas que “as instruções são longas” não basta. Por trás desse fenômeno existe uma limitação estrutural dos LLMs.</p>
<h3 id="alucinação-e-esquecimento-do-contexto"><a class="anchor" href="#alucinação-e-esquecimento-do-contexto">Alucinação e esquecimento do contexto</a></h3>
<p>Se você já viu um agente de IA confundir o contexto da conversa ou esquecer mais adiante algo dito claramente no início, isso também é uma forma de <strong>alucinação (Hallucination)</strong>. Em geral, pensamos primeiro em alucinação como “inventar fatos inexistentes”, mas academicamente ela se divide em três tipos. O survey de 2023 da equipe de Yue Zhang (“Siren's Song in the AI Ocean”) os classifica como <strong>conflito com a entrada</strong> (gerar algo diferente do que o usuário informou explicitamente), <strong>conflito com o contexto</strong> (contradizer algo que o próprio modelo gerou antes) e <strong>conflito factual</strong> (discordar do conhecimento de mundo). Ignorar instruções de arquivos de contexto pertence ao primeiro tipo, não ao terceiro. Ao processar a entrada, o modelo trata parte das informações como se elas “não existissem”.</p>
<p>O problema mais fundamental é que essa alucinação <strong>não pode ser eliminada por completo</strong>. Uma equipe da Universidade Nacional de Singapura demonstrou isso matematicamente por meio da teoria da aprendizagem. Nenhum LLM consegue aprender todas as funções computáveis e, portanto, enquanto for usado como solucionador de problemas de propósito geral, inevitavelmente alucinará em algum ponto.</p>
<p>O efeito da posição também é importante. Pesquisadores de Stanford demonstraram experimentalmente que o modelo consulta melhor informações relevantes quando elas estão <strong>no início ou no fim da janela de contexto</strong>, enquanto o desempenho cai bastante quando ficam <strong>enterradas no meio</strong>. Isso se relaciona diretamente aos arquivos de contexto. Pela ordem de carregamento, <code>CLAUDE.md</code> entra em algum ponto intermediário e, quanto mais longa fica a conversa, mais suas instruções são empurradas para o “meio” do contexto. Isso também se conecta ao outro lado do recency bias mencionado anteriormente: a faixa intermediária do <strong>efeito primazia–recência é a mais vulnerável</strong>.</p>
<p>Ao reunir esses fenômenos, surge uma imagem única. Um arquivo de contexto é apenas <strong>um texto adicional inserido fora do sistema antes do primeiro turno do usuário</strong>. Não é um mecanismo que impõe decisões ao modelo, mas apenas mais um bloco de tokens despejado na janela de contexto. Quanto maior ele for — e quanto mais longa ficar a conversa —, mais suas instruções serão empurradas para o “meio” e menos consultadas. O resultado da ETH Zurich apenas quantificou essa limitação estrutural.</p>
<h3 id="o-estudo-da-eth-zurich"><a class="anchor" href="#o-estudo-da-eth-zurich">O estudo da ETH Zurich</a></h3>
<p>Muitas pessoas devem ter pensado: “Então é melhor colocar o máximo possível nesses arquivos, certo?”. Um estudo recente confrontou diretamente essa intuição: a pesquisa da ETH Zurich mencionada ao longo da seção anterior.</p>
<p>Em fevereiro de 2026, uma equipe da ETH Zurich publicou o artigo “Evaluating AGENTS.md: Are Repository-Level Context Files Helpful for Coding Agents?”. Foram avaliadas 138 tarefas reais de engenharia de software em Python no benchmark AGENTBENCH e no SWE-bench Lite, usando quatro agentes: Claude Code (Sonnet-4.5), Codex (GPT-5.2 / GPT-5.1 mini) e Qwen Code. Os resultados foram inesperados.</p>
<ul>
<li><strong>Arquivos de contexto gerados automaticamente por LLMs</strong> reduziram a taxa de sucesso das tarefas em cerca de <strong>0,5% no SWE-bench Lite e 2% no AGENTBENCH</strong></li>
<li>Mesmo <strong>arquivos escritos manualmente por pessoas</strong> trouxeram apenas uma pequena melhora média, de cerca de 4%</li>
<li>A adição de arquivos de contexto <strong>aumentou o custo de inferência em mais de 20% por instância</strong></li>
<li>Em modelos mais poderosos (GPT-5.2), o efeito dos arquivos de contexto foi ainda menor (quanto mais forte o modelo, maior seu conhecimento paramétrico e maior a chance de o contexto adicional virar ruído)</li>
</ul>
<p>Houve, porém, uma exceção: <strong>quando uma ferramenta não convencional era especificada</strong>. Por exemplo, ao mencionar no contexto o gerenciador de pacotes Python <code>uv</code>, a frequência de uso de <code>uv</code> pelo agente passou de 0,01 para 1,6 vez por instância — um aumento de <strong>aproximadamente 160 vezes</strong>.</p>
<p>A recomendação do Aider mencionada antes — “200 linhas”, “como entra sempre no contexto, mantenha curto” — é uma orientação prática. Já o estudo da ETH Zurich demonstrou quantitativamente que “arquivos de contexto longos reduzem o desempenho”. Na minha opinião, as implicações práticas dessa pesquisa são as seguintes.</p>
<ul>
<li><strong>Arquivos de contexto enormes gerados automaticamente podem causar mais danos do que benefícios</strong>. Se padrões de código, arquitetura e workflow forem todos espremidos em um <code>CLAUDE.md</code> de 300 linhas, o agente seguirá apenas parte e ignorará o restante. Essa inconsistência pode produzir resultados piores do que não ter contexto algum.</li>
<li><strong>O que precisa ser registrado são as informações que não podem ser inferidas</strong>: ferramentas não convencionais, convenções específicas do projeto e falhas do passado. O modelo já conhece as boas práticas gerais de programação.</li>
<li>Use AGENTS.md como fonte única; deixe em CLAUDE.md apenas instruções curtas específicas da ferramenta; e separe workflows detalhados em Skills.</li>
</ul>
<h2 id="e-então"><a class="anchor" href="#e-então">E então?</a></h2>
<p>Os arquivos de contexto mudam de uma ferramenta para outra no nome, no local e no momento em que são lidos. Só neste artigo vimos CLAUDE.md, AGENTS.md, SKILL.md, regras do Cursor, copilot-instructions.md e MEMORY.md, e qual ferramenta oferece suporte a qual arquivo continua mudando. Decorar uma lista de arquivos fica obsoleto rapidamente.</p>
<p>Por isso, meu objetivo neste artigo não foi recomendar um formato de arquivo específico, mas desenvolver <strong>um olhar capaz de enxergar como os agentes leem esses arquivos</strong>. Depois de entender por que CLAUDE.md é injetado como user message, por que cada ferramenta lê o mesmo AGENTS.md de um jeito diferente e por que as instruções se enfraquecem no meio do contexto, fica mais fácil olhar para um novo formato de arquivo de contexto e perceber rapidamente “quando ele é carregado e com que força atua”.</p>
<p>No fim, o que permanece é uma intuição transmitida pelo estudo da ETH Zurich: <strong>o modelo já sabe muitas coisas.</strong> Encher um arquivo de contexto com todo tipo de informação não faz o agente segui-lo melhor. É preferível manter apenas o que o modelo provavelmente não conhece — convenções específicas do projeto, ferramentas não convencionais e erros do passado — e remover o restante. Escrever um arquivo de contexto longo e escrevê-lo bem são problemas diferentes.</p>
<p>Em vez de ampliar CLAUDE.md para centenas de linhas agora mesmo, recomendo que quem leu este artigo investigue ao menos uma vez quando, onde e com que força as ferramentas que já usa leem esse arquivo. Acredito que essa compreensão cria uma base estável, independentemente de como os formatos de arquivo mudarem.</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:#3b82f6"></span><a href="https://code.claude.com/docs/en/memory" target="_blank" rel="noopener noreferrer">Claude Code Memory, Anthropic</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#3b82f6"></span><a href="https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview" target="_blank" rel="noopener noreferrer">Anthropic Tool Use Overview</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#3b82f6"></span><a href="https://cursor.com/docs/context/rules" target="_blank" rel="noopener noreferrer">Cursor Rules Documentation</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#8b5cf6"></span><a href="https://arxiv.org/abs/2602.11988" target="_blank" rel="noopener noreferrer">ETH Zurich, "Evaluating AGENTS.md" (2602.11988)</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#8b5cf6"></span><a href="https://arxiv.org/abs/2309.01219" target="_blank" rel="noopener noreferrer">Yue Zhang et al., "Siren's Song" (2309.01219)</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#8b5cf6"></span><a href="https://arxiv.org/abs/2401.11817" target="_blank" rel="noopener noreferrer">Ziwei Xu et al., "Hallucination is Inevitable" (2401.11817)</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#8b5cf6"></span><a href="https://arxiv.org/abs/2307.03172" target="_blank" rel="noopener noreferrer">Nelson F. Liu et al., "Lost in the Middle" (2307.03172)</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#f59e0b"></span><a href="https://simonwillison.net/2025/Oct/16/claude-skills/" target="_blank" rel="noopener noreferrer">Simon Willison, "Claude Skills are awesome"</a></div>
</div>
</div></details>]]></content:encoded>
            <author>jihoon7705@gmail.com (이지훈)</author>
            <category>IA</category>
            <category>Ferramentas-de-desenvolvimento</category>
            <category>Claude</category>
            <category>MCP</category>
            <category>CodeGraph</category>
        </item>
        <item>
            <title><![CDATA[Quatro camadas de inteligência de código]]></title>
            <link>https://hooninedev.com/pt-BR/260526</link>
            <guid isPermaLink="false">https://hooninedev.com/pt-BR/260526</guid>
            <pubDate>Tue, 26 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Neste post, quero falar sobre o que diferencia entre si as ferramentas que reduzem o custo de um agente de programação com IA encontrar o código relevante. Este texto é para desenvolvedores que já vir...]]></description>
            <content:encoded><![CDATA[<p>Neste post, quero falar sobre <strong>o que diferencia entre si as ferramentas que reduzem o custo de um agente de programação com IA encontrar o código relevante</strong>.</p>
<p>Este texto é para desenvolvedores que já viram um agente gastar tokens repetindo grep e leituras de arquivo em uma base de código grande, e que estão em dúvida sobre qual ferramenta adicionar, como Repomix, CodeGraph ou Serena. Essas ferramentas se dividem em quatro camadas de acordo com a profundidade com que entendem o código (context packing, o repo map do tree-sitter, o grafo de conhecimento e o LSP), e cada camada reduz o custo de busca em um ponto diferente. Para o grafo de conhecimento, incluo o benchmark do seu criador; para as outras três camadas, medições feitas no próprio repositório deste blog.</p>
<p>Desde que vi o <code>codegraph</code> no GitHub Trending e o instalei por conta própria, sempre que encontro uma ferramenta nova fico curioso para saber por qual princípio ela economiza tokens. Muitas das ferramentas deste texto também conheci primeiro no GitHub Trending, que costumo ver por semana, filtrando por TypeScript e Python.</p>
<h2 id="ferramentas-de-inteligência-de-código"><a class="anchor" href="#ferramentas-de-inteligência-de-código">Ferramentas de inteligência de código</a></h2>
<p>Antes de alterar qualquer código, um agente primeiro procura onde está o código relevante. É um loop: busca com grep, lê arquivos, filtra e faz grep de novo. No benchmark do CodeGraph que veremos mais adiante, o lado que respondia sem a ferramenta usou até 43 tool calls para uma única pergunta. As ferramentas de inteligência de código são tentativas de reduzir esse custo de busca.</p>
<p>Mas "custo" não aponta para uma coisa só. Os tokens que o modelo processou, o número de tool calls e os tokens que continuam na context window depois que o trabalho termina se movem separadamente. Em cada camada, este texto olha qual dos três ela reduz.</p>
<p>Divido essas tentativas nas quatro camadas (tiers) abaixo, de acordo com quão profundamente cada ferramenta entende o código. Não há uma classificação estabelecida no setor; o agrupamento é meu. Todos os valores medidos neste artigo foram obtidos em 2026-10-08 no commit <code>36e5cfa</code> deste repositório, e os tokens foram contados com o <code>o200k_base</code> do tiktoken. Os valores diferem dos do tokenizador do Claude, então servem para comparar, não como números absolutos.</p>
<h3 id="empacotamento-de-contexto"><a class="anchor" href="#empacotamento-de-contexto">Empacotamento de contexto</a></h3>
<p>A solução mais simples parte da ideia: "<strong>colocar tudo em uma única context window</strong>". Não constrói grafo nem faz indexação. Simplesmente serializa o repositório inteiro em um bloco de texto e o entrega inteiro ao modelo.</p>
<p>A ferramenta representativa é o <strong>Repomix</strong>. Seu formato de saída padrão é XML, e o README traz um link para a documentação da Anthropic sobre tags XML. Ele tem CLI, versão web, extensão de navegador e servidor MCP.</p>
<p>O ponto forte do <strong>GitIngest</strong> é dispensar instalação. Se você trocar <code>github.com</code> por <code>gitingest.com</code> em uma URL do GitHub, o repositório inteiro vira uma única página de texto. O <strong>code2prompt</strong> (criado por Mufeed VH), uma CLI em Rust, permite mudar o formato de saída com templates.</p>
<p>O <strong>rtk</strong> (<code>rtk-ai/rtk</code>) segue uma direção um pouco diferente. Enquanto as ferramentas acima empacotam o repositório inteiro de uma vez, o rtk comprime a saída dos comandos de CLI que o agente executa. É um único binário feito em Rust, registrado nos hooks de vários agentes, como Claude Code, Cursor, Copilot, Gemini CLI e Codex, de modo que, quando o agente chama <code>git status</code>, ele executa <code>rtk git status</code> no lugar. Aplica filtering, grouping, truncation e deduplication a mais de 100 comandos. O hook do rtk tem como alvo apenas as tool calls do Bash. <code>Read</code>, <code>Grep</code> e <code>Glob</code> do Claude Code passam direto.</p>
<p>O tamanho da redução precisa ser lido com cuidado. O <a href="https://github.com/rtk-ai/rtk/blob/8533612180c60efbcb5827c7db4e910aba096705/README.md#L66-L70" target="_blank" rel="noopener noreferrer">README do rtk</a> diz que reduz a saída do Bash em até 90%, e logo acrescenta que isso não significa reduzir a conta em 90%. Do lado do modelo, a saída de um comando é parte dos tokens de entrada, e os tokens de entrada são parte da conta, então a redução se dilui a cada etapa. O número que o <a href="https://www.rtk-ai.app/" target="_blank" rel="noopener noreferrer">site oficial</a> destaca é 56% em média, nos comandos que o rtk reescreve. Se as ferramentas acima reduzem o texto que entra, o rtk reduz o texto que volta como resultado das tool calls.</p>
<p>O limite desta camada é que <strong>repositórios grandes esbarram no teto de tokens</strong>. Empacotar só os 109 arquivos do <code>src/</code> deste blog já dá 94.596 tokens. Isso ainda cabe na janela, mas um repositório com milhares de arquivos chegaria ao limite logo. O Repomix responde a isso com <code>--compress</code>. Segundo o <a href="https://github.com/yamadashy/repomix/blob/8d6429121e98ed178e4d3a975c2bdbbecc958c4a/README.md#L797-L831" target="_blank" rel="noopener noreferrer">README</a>, ele usa Tree-sitter para manter as assinaturas de funções e classes e descartar os corpos de implementaçã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:#6A737D;--shiki-light:#6A737D"># repomix 1.18.1, 이 리포 커밋 36e5cfa, 2026-10-08</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">npx</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> -y</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> repomix@1.18.1</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> src</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> -o</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> out.xml</span><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">              # Total Tokens: 94,596 tokens</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">npx</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> -y</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> repomix@1.18.1</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> src</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> --compress</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> -o</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> out.xml</span><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">   # Total Tokens: 30,320 tokens</span></span></code></pre></figure>
<p>A redução foi de 68%. Os comentários ficam, então <code>visit-counter.ts</code>, que tem JSDoc longo, só foi de 1.642 para cerca de 1.220 tokens, 26% a menos. A versão comprimida mantém as instruções import e as assinaturas, mas as chamadas dentro dos corpos das funções somem. Dá para ver no texto qual arquivo importa o quê, mas não dá para perguntar quem chama quem. Por isso a fronteira entre esta camada e a próxima está menos em a ferramenta ver a sintaxe do que em poder ser consultada sobre relações.</p>
<h3 id="mapa-do-repositório-com-tree-sitter"><a class="anchor" href="#mapa-do-repositório-com-tree-sitter">Mapa do repositório com tree-sitter</a></h3>
<p>A camada seguinte usa o <strong>tree-sitter</strong> para analisar a estrutura do código, mas sem subir um servidor de índice separado.</p>
<p>O que o tree-sitter, a ferramenta em que esta camada se apoia, constrói é uma <strong>CST (Concrete Syntax Tree)</strong>, uma árvore que representa a estrutura do código-fonte. A <a href="https://tree-sitter.github.io/tree-sitter/" target="_blank" rel="noopener noreferrer">documentação oficial do tree-sitter</a> também diz que ele constrói uma concrete syntax tree. É uma árvore que mantém como nós até os parênteses e a pontuação, e é com ela que as ferramentas abaixo trabalham.</p>
<p>O <strong>tree-sitter</strong> é um gerador de parsers de código aberto e uma biblioteca de parsing incremental (incremental). A <a href="https://docs.github.com/en/repositories/working-with-files/using-files/navigating-code-on-github" target="_blank" rel="noopener noreferrer">code navigation do GitHub</a> usa o tree-sitter. Como ele só refaz o parsing da parte editada, mudar uma linha no editor não refaz o parsing do arquivo inteiro; só corrige a parte da árvore que mudou. Essa vantagem é dos editores, onde as edições não param. O Aider, logo abaixo, mantém um cache baseado na hora de modificação dos arquivos para não refazer o parsing dos que não mudaram.</p>
<p>O <strong>Aider</strong>, uma ferramenta de programação em par com IA usada no terminal, é o exemplo representativo dessa abordagem. O Aider usa o tree-sitter para extrair de cada arquivo as definições e referências de funções, classes e métodos. Depois monta um grafo com os arquivos como nós. Quando o arquivo A referencia um identificador definido no arquivo B, surge uma aresta de A para B.</p>
<p>Para escolher os arquivos importantes desse grafo, o Aider usa o PageRank. O PageRank é um algoritmo que dá mais pontuação a um nó quanto mais links ele recebe e quanto mais pesados eles são. O Aider usa uma variante chamada personalized PageRank, que inclina a pontuação para os nós escolhidos. Depois coloca no orçamento de tokens as definições e assinaturas dos arquivos mais bem classificados.</p>
<p>O que entra no orçamento muda com a conversa atual. No <a href="https://github.com/Aider-AI/aider/blob/5dc9490bb35f9729ef2c95d00a19ccd30c26339c/aider/repomap.py#L487-L525" target="_blank" rel="noopener noreferrer"><code>repomap.py</code></a> do Aider, o peso de uma aresta é a raiz quadrada do número de referências multiplicada por um fator. Identificadores mencionados na conversa recebem 10x, e identificadores em camelCase, snake_case ou kebab-case com 8 ou mais caracteres também 10x. Os que começam com <code>_</code> e os definidos em mais de 5 arquivos recebem 0,1x cada, e as arestas que saem dos arquivos adicionados agora ao chat recebem 50x. Os arquivos do chat, os mencionados na conversa e aqueles cujo caminho ou nome de arquivo coincide com um identificador da conversa também recebem pontuação de personalization no PageRank.</p>
<p>O orçamento também não é fixo. A <a href="https://aider.chat/docs/repomap.html" target="_blank" rel="noopener noreferrer">documentação do Aider</a> dá 1k como padrão de <code>--map-tokens</code>, mas o <a href="https://github.com/Aider-AI/aider/blob/5dc9490bb35f9729ef2c95d00a19ccd30c26339c/aider/models.py#L782-L789" target="_blank" rel="noopener noreferrer">código</a> corta um oitavo do limite de entrada do modelo para um valor entre 1.024 e 4.096. E, quando não há arquivos no chat, amplia até <code>--map-multiplier-no-files</code> (padrão 2) vezes. Conferi isso com o <code>src/</code> deste repositório. A versão 0.86.1 que executei e o commit linkado acima têm o mesmo código de ranking. Os maps impressos foram contados com o200k_base, incluindo as três linhas de texto de orientação que o Aider adiciona.</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:#6A737D;--shiki-light:#6A737D"># aider-chat 0.86.1, 리포 루트에서 시작한다, 2026-10-08</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">mkdir</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> /tmp/aidermap</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> &#x26;&#x26; </span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">cp</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> -R</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> src</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> /tmp/aidermap/</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> &#x26;&#x26; </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">cd</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> /tmp/aidermap</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> &#x26;&#x26; </span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">git</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> init</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> -q</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> &#x26;&#x26; </span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">git</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> add</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> -A</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> &#x26;&#x26; </span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">git</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> commit</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> -qm</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> init</span></span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D"># 키는 더미 값이다. --show-repo-map 은 map 만 출력하고 LLM 을 부르지 않는다</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">export</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> OPENAI_API_KEY</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">dummy</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">aider_map</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">() { </span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">uvx</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> --python</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 3.12</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> --from</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> aider-chat==</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">0.86.1</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> aider</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> --no-check-update</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> --analytics-disable</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> --no-gitignore</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> --model</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> gpt-4o</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> "</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">$@</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:#B392F0;--shiki-light:#6F42C1">aider_map</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> --show-repo-map</span><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">                                              # Repo-map: using 4096 tokens</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">aider_map</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> --map-tokens</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 1024</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> --show-repo-map</span><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">                            # Repo-map: using 1024 tokens</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">aider_map</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> --map-tokens</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 1024</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> --show-repo-map</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> src/lib/filter-posts.ts</span><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">    # 이 파일을 채팅에 올린 상태</span></span></code></pre></figure>
<table>
<thead>
<tr>
<th>Condição</th>
<th>Orçamento escolhido pelo Aider</th>
<th>Map impresso</th>
<th>Arquivos incluídos</th>
</tr>
</thead>
<tbody>
<tr>
<td>Padrões, sem arquivo no chat</td>
<td>4.096</td>
<td>7.370 tokens</td>
<td>61</td>
</tr>
<tr>
<td><code>--map-tokens 1024</code>, sem arquivo no chat</td>
<td>1.024</td>
<td>2.019 tokens</td>
<td>27</td>
</tr>
<tr>
<td><code>--map-tokens 1024</code>, <code>filter-posts.ts</code> adicionado ao chat</td>
<td>1.024</td>
<td>983 tokens</td>
<td>12</td>
</tr>
</tbody>
</table>
<p>O limite de entrada do gpt-4o é de 128k, então um oitavo foi cortado para 4.096, e com o chat vazio o map saiu dentro do dobro disso. Ao adicionar um arquivo ao chat, o map encolheu até caber no orçamento; dos 27 arquivos anteriores só 10 ficaram, e <code>PostList.tsx</code> e <code>SearchModal.tsx</code> entraram como novos. Um repo map não é um resumo fixo do repositório, e sim um trecho ajustado à conversa daquele momento.</p>
<p>Há mais uma ferramenta que vale citar nesta camada: o <strong>ast-grep</strong> (<code>ast-grep/ast-grep</code>). É uma CLI de busca estrutural e rewrite baseada em tree-sitter, que casa nós da árvore sintática em vez de texto. Por exemplo, o padrão <code>console.log($A)</code> pega todas as chamadas a <code>console.log</code> com um único argumento, não importa como estejam as quebras de linha ou os espaços. O que ele pega é a mesma estrutura sintática. Ele não sabe o tipo de <code>$A</code> nem de onde vem o nome <code>console</code>. Também existe um servidor <code>ast-grep-mcp</code>, com o qual dá para fazer um agente usar busca estrutural em vez de grep de texto.</p>
<h3 id="knowledge-graph"><a class="anchor" href="#knowledge-graph">Knowledge Graph</a></h3>
<p>A terceira camada vai um passo além. Ela <strong>faz o parsing de toda a base de código de antemão, constrói um grafo de conhecimento e o salva em disco</strong>; depois o agente envia consultas a esse grafo salvo. O exemplo mais comentado é uma ferramenta chamada <strong>CodeGraph</strong>.</p>
<p>A estrutura que o <a href="https://github.com/colbymchenry/codegraph/blob/b635dd467f0578926a9c01a37b9d28d2b26689f1/README.md" target="_blank" rel="noopener noreferrer">README do CodeGraph</a> descreve é simples. Ele faz o parsing do código com tree-sitter para extrair símbolos, arestas e informações de arquivos, e os salva em um banco de dados SQLite local. A busca por nome passa pelo índice FTS5 do SQLite. O agente consulta esse grafo via MCP. Nos passos que o README descreve não aparece nenhum LLM. Por isso entendo que essa extração é determinística (deterministic), ou seja, que o mesmo código sempre dá o mesmo resultado.</p>
<p>Se um LLM resume o código para montar o grafo, os resultados podem variar até para o mesmo código, e alucinações podem se infiltrar. Já ao fazer o parsing direto da árvore sintática, as relações entre símbolos são extraídas apenas pelas regras da gramática da linguagem, sem espaço para esse tipo de interpretação.</p>
<p>O <strong>FTS5 (SQLite Full-Text Search 5)</strong>, que aparece aqui, é uma extensão de busca de texto completo oferecida pelo SQLite como tabela virtual. Segundo a <a href="https://www.sqlite.org/fts5.html" target="_blank" rel="noopener noreferrer">documentação do SQLite</a>, ela faz parte da amalgamation desde a 3.9.0 (2015-10-14); você cria uma tabela com <code>CREATE VIRTUAL TABLE ... USING fts5(...)</code> e consulta com o operador <code>MATCH</code>. Dá para manter um índice de texto completo em um único arquivo SQLite sem subir um mecanismo de busca separado como o Elasticsearch.</p>
<p>Mas ser determinístico não é o mesmo que ser completo. O mesmo README registra o reconhecimento de rotas de frameworks que dependem de convenção e reflexão entre 74,1% para Django e 83,9% para ASP.NET, e chama isso de limite da análise estática (honest static-analysis ceiling). O mesmo código dá o mesmo resultado, mas a esse resultado podem faltar arestas.</p>
<p>O benchmark foi medido pelo próprio CodeGraph. A nova medição de 2026-08-05 no mesmo README rodou o Claude Opus 4.8 em modo headless e fez uma pergunta de arquitetura a cada um de 7 repositórios de código aberto. O lado com o CodeGraph MCP ativado reduziu o custo médio em 44%, os tokens processados em 62% e as tool calls em 88%. Essa nova medição impediu os dois lados de chamar a CLI <code>codegraph</code> pelo Bash. Em um ambiente de medição sem esse bloqueio, o lado sem a ferramenta encontrou e usou a CLI em 26 de 28 execuções, e o README deixa claro que os números publicados antes foram obtidos sem esse bloqueio.</p>
<p>O tamanho da economia não acompanhou o tamanho do repositório. Nas perguntas em que o lado sem a ferramenta usou de 28 a 43 tool calls, o custo caiu de 57 a 78%, e no Gin, que terminou em 7, ficou praticamente igual. O VS Code, com cerca de 11k arquivos, deu 71%, e o Excalidraw, com cerca de 640, 78%. O README atribui isso à quantidade de busca que cada pergunta exigiu. Como há uma única pergunta por repositório, leio daí apenas a direção.</p>
<p>O README também registra um número na direção oposta. Os tokens processados caem, mas ao fim de uma sessão de vários turnos, os resultados de retrieval que continuam na context window são cerca de 80% maiores do lado do CodeGraph no conjunto dos 7 repositórios. A diferença varia por repositório; só no VS Code são 67k contra 18k tokens, cerca de 3,7 vezes. Ele devolve o texto-fonte denso de uma vez, e esse texto permanece na janela. O CodeGraph reduziu as tool calls e os tokens processados ao preço de deixar mais coisa na janela.</p>
<p>O <strong>Cursor</strong> seguiu outro caminho e depois mudou de rumo. A indexação descrita no <a href="https://cursor.com/blog/secure-codebase-indexing" target="_blank" rel="noopener noreferrer">post do blog do Cursor de janeiro de 2026</a> não era um grafo sintático, e sim <strong>busca semântica baseada em embeddings vetoriais</strong>. Ela dividia os arquivos em chunks localmente, sincronizava com o servidor por meio de hashes de Merkle tree e transformava os chunks em embeddings para a busca semântica. Em julho de 2026, um Community Support Engineer do Cursor respondeu no <a href="https://forum.cursor.com/t/what-do-you-think-about-cursor-removing-the-codebase-indexing-settings/165899" target="_blank" rel="noopener noreferrer">fórum</a>: "Semantic/embeddings indexing is being turned down in favor of grep-based retrieval". No mesmo tópico, outro membro da equipe escreveu que, à medida que os modelos ficaram bons em usar grep, o antigo caminho de busca semântica deixou de ajudar de forma significativa. Hoje a <a href="https://cursor.com/docs/context/codebase-indexing" target="_blank" rel="noopener noreferrer">documentação do Cursor</a> diz que o Instant Grep constrói e consulta seu índice na sua máquina e não guarda embeddings da sua base de código para busca.</p>
<h3 id="lsp"><a class="anchor" href="#lsp">LSP</a></h3>
<p>A última camada <strong>depende diretamente de um servidor de linguagem</strong>. Se o tree-sitter sabe "que um símbolo existe", o LSP sabe "o que esse símbolo é".</p>
<p>O <strong><a href="https://microsoft.github.io/language-server-protocol/" target="_blank" rel="noopener noreferrer">LSP (Language Server Protocol)</a></strong> é um protocolo aberto baseado em JSON-RPC que padroniza a comunicação entre editores e ferramentas de análise de linguagem (autocompletar, ir para a definição, encontrar referências, refatoração etc.). Em 2016, <a href="https://www.redhat.com/en/about/press-releases/red-hat-codenvy-and-microsoft-collaborate-language-server-protocol" target="_blank" rel="noopener noreferrer">Microsoft, Red Hat e Codenvy anunciaram a colaboração</a>. A ideia central é "não reimplementar um analisador de linguagem para cada editor; ter um servidor por linguagem e fazer todos os editores consultá-lo". O rust-analyzer e o pyright do Python são servidores LSP, enquanto o TypeScript usa o typescript-language-server, que envolve em LSP o <code>tsserver</code>, que fala um protocolo próprio.</p>
<p>O <strong>Serena</strong> (<code>oraios/serena</code>) é um servidor MCP desta camada. Em 2026-10-08 tem 30.093 stars, e o repositório foi criado em março de 2025. A ideia central do Serena cabe em uma linha: <strong>mostrar ao agente o código como símbolos.</strong> Suas ferramentas principais incluem <code>find_symbol</code>, <code>find_referencing_symbols</code> e <code>get_symbols_overview</code>. Dá para escolher um de dois backends. O padrão é um servidor de linguagem que implementa LSP (gratuito/código aberto); a outra opção é um plugin pago que usa a análise de código da IDE da JetBrains (com teste gratuito).</p>
<p>Medir neste repositório mostra de onde vem a diferença. Procurei os usos de <code>isHiddenPost</code> (<code>src/lib/filter-posts.ts:12</code>), que filtra os posts privados, de duas formas. O lado de texto é o grep.</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:#6A737D;--shiki-light:#6A737D"># 커밋 36e5cfa, 2026-10-08. 이 글도 같은 이름을 담고 있어 content/ 는 뺐다</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">git</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> grep</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> -n</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> isHiddenPost</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> --</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> ':!content'</span><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">   # 16줄, 파일 9개</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">git</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> grep</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> -n</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> -C1</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> isHiddenPost</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> --</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> ':!content'</span><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">   # 같은 16곳을 앞뒤 1줄과 함께</span></span></code></pre></figure>
<p>No lado do LSP, foi chamado o <a href="https://github.com/oraios/serena/blob/3b99f8b024dafd58c962ea6e74f37c8a730ef532/src/serena/tools/symbol_tools.py#L169-L172" target="_blank" rel="noopener noreferrer"><code>find_referencing_symbols</code></a> do Serena. A busca de referências do LSP pergunta por posição (arquivo, linha e coluna), não por nome, então essa ferramenta também recebe ao mesmo tempo <code>name_path</code>, o nome do símbolo, e <code>relative_path</code>, o arquivo onde está a definição. Em vez de subir o Serena como servidor MCP, a ferramenta foi chamada diretamente do Python no mesmo commit.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="python" data-theme="github-dark github-light"><code data-language="python" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D"># serena 3b99f8b, 커밋 36e5cfa, 2026-10-08. 리포 루트에서 실행한다</span></span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D"># uvx --python 3.12 --from git+https://github.com/oraios/serena@3b99f8b024dafd58c962ea6e74f37c8a730ef532 python refs.py</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">from</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> serena.agent </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> SerenaAgent</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">from</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> serena.config.serena_config </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> SerenaConfig</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">from</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> serena.tools.symbol_tools </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> FindReferencingSymbolsTool</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">agent </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> SerenaAgent(</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">project</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 style="--shiki-dark:#FFAB70;--shiki-light:#E36209">serena_config</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">SerenaConfig().with_headless_mode_overrides())</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">agent.execute_task(</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">lambda</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">None</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">tool </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> agent.get_tool(FindReferencingSymbolsTool)</span></span>
<span data-line=""><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">print</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(agent.execute_task(</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">lambda</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: tool.apply(</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">name_path</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"isHiddenPost"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">relative_path</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"src/lib/filter-posts.ts"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">)))</span></span></code></pre></figure>
<p>A saída é uma única linha de JSON. Este é só o primeiro item. Os números de linha começam em 0.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="text" data-theme="github-dark github-light"><code data-language="text" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span>{"src/lib/filter-posts.ts": {"Function": [{"name_path": "filterPublishedPosts", "body_location": {"start_line": 22, "end_line": 24}, "content_around_reference": "...  22:export function filterPublishedPosts(posts: Post[]): Post[] {\n  >  23:  return posts.filter(post => !isHiddenPost(post))\n...  24:}"}]}, ...</span></span></code></pre></figure>
<p>O Serena devolveu 10 referências. Ele deixa de fora a definição em <code>filter-posts.ts:12</code> e, para as linhas de import de três arquivos, devolve o próprio arquivo como símbolo (<code>File</code>). As 11 posições de código que o grep encontrou são essas 10 mais a linha da definição. Esse nome é único no repositório, então o grep não deixou de fora nem acrescentou nenhuma posição de código. As 5 posições a mais que o grep encontrou eram frases de documentos que explicam essa função: o <code>CLAUDE.md</code> da raiz, os arquivos de <code>.claude/commands/</code> <code>audit.md</code> e <code>write-post.md</code> e, em <code>docs/research/snapshot-20260816/</code>, as cópias que restaram de <code>CLAUDE.md</code> e <code>write-post.md</code>.</p>
<p>O Serena devolve cada referência com <a href="https://github.com/oraios/serena/blob/3b99f8b024dafd58c962ea6e74f37c8a730ef532/src/serena/repl/api/lsp_api.py#L367-L369" target="_blank" rel="noopener noreferrer">uma linha antes e outra depois</a>. Por isso o grep foi executado de duas formas, <code>git grep -n</code>, que devolve só números de linha, e <code>git grep -n -C1</code>, que também devolve uma linha antes e outra depois, e as três saídas foram contadas por inteiro com <code>o200k_base</code>.</p>
<table>
<thead>
<tr>
<th>Saída</th>
<th>Posições</th>
<th>tokens</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>git grep -n</code></td>
<td>16 (código 11, documentos 5)</td>
<td>703</td>
</tr>
<tr>
<td><code>git grep -n -C1</code></td>
<td>16 (código 11, documentos 5)</td>
<td>1.575 (código 733, documentos 841)</td>
</tr>
<tr>
<td>Serena <code>find_referencing_symbols</code></td>
<td>10</td>
<td>892</td>
</tr>
</tbody>
</table>
<p>Comparado a um grep que devolve uma linha antes e outra depois, o Serena usa 683 tokens a menos. A diferença vem de não devolver as 5 posições em documentos. Mas o Serena acrescenta a cada referência o nome do símbolo e as informações de posição em JSON, então, olhando só o lado do código, ele na verdade usa 159 tokens a mais. E os 892 tokens do Serena superam os 703 de <code>git grep -n</code>, que devolve só números de linha. O que a camada LSP economiza neste repositório não é o resultado da busca, e sim o passo seguinte: ler linhas ou arquivos ao redor depois de ver o resultado do grep.</p>
<p>Nesse passo seguinte, se o agente lesse inteiros os 9 arquivos que o grep encontrou, leria 47.034 tokens: 3.716 dos 4 arquivos de código e 43.318 dos 5 documentos. Isso é um limite superior. Em uma sessão real, <code>CLAUDE.md</code> provavelmente já está no context e não seria lido de novo. Com um nome comum como <code>isLocale</code>, os resultados poderiam divergir até nas posições de código.</p>
<h2 id="conclusão"><a class="anchor" href="#conclusão">Conclusão</a></h2>
<p>Resumindo, as quatro camadas se dividem pela profundidade com que entendem o código, e reduzem custos diferentes. O context packing entrega o código como texto e, mesmo com <code>--compress</code>, mantém só a sintaxe, reduzindo os tokens que entram. O repo map do tree-sitter sabe que um símbolo existe, põe um teto de orçamento nos tokens que entram, e a conversa daquele momento decide com o que preenchê-lo. O grafo de conhecimento guarda as relações de antemão e reduz as tool calls e os tokens processados, mas, pela medição do próprio fornecedor, o que fica na janela até aumentou. O LSP sabe o que é um símbolo; neste repositório ele não devolveu o mesmo nome nos documentos, mas em troca acrescentou metadados a cada resultado. O que ele economiza não está no resultado da busca, e sim na leitura que vem depois.</p>
<p>Por isso, ao escolher uma ferramenta, olho primeiro qual custo é o problema agora, mais do que a profundidade da camada. Se a janela é pequena e as sessões são longas, olho o que fica; se as idas e voltas são lentas, o número de tool calls; se no repositório documentos e código compartilham nomes, quantos arquivos acabo lendo depois de um resultado do grep. Leio o Cursor ter removido a busca semântica por embeddings e passado a um índice de texto local (Instant Grep) como um sinal de que um índice caro nem sempre supera a própria capacidade de busca do modelo.</p>
<p>Se essas ferramentas reduzem o custo de um agente encontrar código, quantas regras do projeto o agente deve conhecer desde o início, e em qual arquivo escrevê-las, é outro problema. Trato disso em <a href="/260529" target="_blank" rel="noopener noreferrer">Arquivos de contexto</a>.</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:#10b981"></span><a href="https://github.com/ast-grep/ast-grep" target="_blank" rel="noopener noreferrer">ast-grep/ast-grep</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/ast-grep/ast-grep-mcp" target="_blank" rel="noopener noreferrer">ast-grep/ast-grep-mcp</a></div>
</div>
</div></details>]]></content:encoded>
            <author>jihoon7705@gmail.com (이지훈)</author>
            <category>IA</category>
            <category>Ferramentas-de-desenvolvimento</category>
            <category>Claude</category>
            <category>MCP</category>
            <category>CodeGraph</category>
        </item>
        <item>
            <title><![CDATA[MCP e function calling]]></title>
            <link>https://hooninedev.com/pt-BR/260524</link>
            <guid isPermaLink="false">https://hooninedev.com/pt-BR/260524</guid>
            <pubDate>Sun, 24 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Neste post, quero falar sobre como o MCP (Model Context Protocol) difere de function calling. Este texto é para quem usa servidores MCP com Claude Code ou Cursor, mas tem dificuldade em explicar onde ...]]></description>
            <content:encoded><![CDATA[<p>Neste post, quero falar sobre <strong>como o MCP (Model Context Protocol) difere de function calling</strong>.</p>
<p>Este texto é para quem usa servidores MCP com Claude Code ou Cursor, mas tem dificuldade em explicar onde o MCP se separa do function calling das APIs de LLM. A resposta curta é que o MCP não substitui o function calling. A aplicação host converte a lista de ferramentas que recebe de um servidor MCP no parâmetro <code>tools</code> do function calling e devolve ao servidor MCP a chamada que o modelo escolhe. Ao final, você saberá de que parte do protocolo vem cada uma das quatro diferenças que o MCP acrescenta por cima, quais diferenças sobrevivem à revisão 2026-07-28 e que superfície de ataque essa estrutura abre.</p>
<p>Trabalho como desenvolvedor frontend e uso o Claude no dia a dia, mas, a cada servidor MCP que eu adicionava, nunca ficava muito claro para mim como essas ferramentas entravam no campo de visão do modelo.</p>
<h2 id="mcp-model-context-protocol"><a class="anchor" href="#mcp-model-context-protocol">MCP (Model Context Protocol)</a></h2>
<p>O MCP (Model Context Protocol) resolve o problema de “<strong>o que permitir que o agente faça</strong>”.</p>
<p>Em termos mais concretos: para um agente de IA enviar uma mensagem no Slack, ele precisa conseguir chamar a API do Slack. Para criar uma issue no GitHub, precisa chamar a API do GitHub. Para fazer uma query no Postgres, precisa saber lidar com a conexão ao banco. O MCP <strong>reúne todas essas integrações com sistemas externos em um único protocolo padrão</strong>. (Ou seja, cliente e servidor se conectam pela mesma especificação.)</p>
<p>O MCP é um padrão aberto apresentado pela primeira vez pela Anthropic em <strong>25 de novembro de 2024</strong>. Em <strong>9 de dezembro de 2025</strong>, a Anthropic doou o MCP à <a href="https://www.anthropic.com/news/donating-the-model-context-protocol-and-establishing-of-the-agentic-ai-foundation" target="_blank" rel="noopener noreferrer">Agentic AI Foundation (AAIF)</a>, vinculada à Linux Foundation. A AAIF foi cofundada por Anthropic, Block e OpenAI.</p>
<p>O MCP é um protocolo construído sobre JSON-RPC. O <a href="https://www.jsonrpc.org/specification" target="_blank" rel="noopener noreferrer">JSON-RPC 2.0</a> é um protocolo de RPC (Remote Procedure Call) stateless e leve que usa JSON como wire format. Como é independente da camada de transporte, funciona sobre HTTP, TCP ou entrada e saída padrão. Este texto usa como referência a revisão 2025-11-25, na qual o MCP é um protocolo stateful que estabelece uma sessão para cada conexão. O que mudou na revisão seguinte fica para depois de percorrermos o fluxo de chamadas.</p>
<h3 id="as-seis-primitivas"><a class="anchor" href="#as-seis-primitivas">As seis primitivas</a></h3>
<p>A visão geral da especificação 2025-11-25 lista três recursos oferecidos pelo servidor e três oferecidos pelo cliente. Este texto chama esses seis de primitivas. Aqui, primitiva não tem relação com os tipos primitivos do JavaScript (como string ou number); refere-se a um tipo básico de interação definido pelo protocolo.</p>
<h4 id="primitivas-do-lado-do-servidor"><a class="anchor" href="#primitivas-do-lado-do-servidor">Primitivas do lado do servidor</a></h4>
<ul>
<li><strong>Tool</strong> (model-controlled): uma ação cuja chamada o próprio modelo decide. Essas ações podem ter efeitos colaterais (side effects)</li>
<li><strong>Resource</strong> (application-controlled): dados identificados por uma URI. A especificação só tem <code>resources/read</code> para ler o conteúdo e nenhum método de escrita. A aplicação host decide como colocar esse recurso no contexto</li>
<li><strong>Prompt</strong> (user-controlled): um template reutilizável que o usuário aciona explicitamente, por exemplo com um comando de barra</li>
</ul>
<h4 id="primitivas-do-lado-do-cliente"><a class="anchor" href="#primitivas-do-lado-do-cliente">Primitivas do lado do cliente</a></h4>
<ul>
<li><strong>Sampling</strong>: um mecanismo que permite ao servidor pedir, no sentido inverso, uma completion ao LLM do cliente, tornando bidirecional a relação entre cliente e servidor. Serve para que um servidor que precisa gerar texto enquanto executa uma ferramenta use o modelo do cliente sem ter a própria chave de API. Na revisão 2026-07-28 ficou deprecated, ou seja, com remoção prevista</li>
<li><strong>Roots</strong>: informação sobre os limites do workspace com a qual o cliente diz ao servidor “até aqui vai a área em que você pode trabalhar”</li>
<li><strong>Elicitation</strong>: um recurso que permite ao servidor pedir ao usuário dados adicionais de forma estruturada enquanto executa uma ferramenta</li>
</ul>
<p>Essa distinção importa porque <strong>quem decide chamar ou fornecer algo é diferente</strong>. Um Tool é executado por decisão do modelo, então uma chamada errada traz risco, enquanto um Prompt é escolhido explicitamente pelo usuário. Um Resource é escolhido pela aplicação por padrão, mas a especificação também permite implementações que incluem recursos automaticamente, com base em heurísticas ou na seleção do modelo. As três primitivas do lado do cliente vão no sentido oposto: o servidor pede, e o cliente decide se responde.</p>
<h3 id="os-dois-mecanismos-de-transporte"><a class="anchor" href="#os-dois-mecanismos-de-transporte">Os dois mecanismos de transporte</a></h3>
<p>Há dois <a href="https://modelcontextprotocol.io/specification/2025-11-25/basic/transports" target="_blank" rel="noopener noreferrer">mecanismos de transporte padrão</a>, e a especificação também permite outros transportes personalizados (MAY). O primeiro é o <strong>stdio</strong>, que executa o servidor MCP como um subprocesso local e se comunica pela entrada e saída padrão. É adequado para ferramentas que funcionam localmente, como sistema de arquivos ou git. O segundo é o <strong>Streamable HTTP</strong>, que coloca streaming SSE (Server-Sent Events) sobre HTTP POST e GET para criar uma comunicação quase bidirecional. O SSE é uma forma de o servidor enviar dados ao cliente em uma única direção por uma conexão HTTP. É adequado para cenários que acontecem através da rede, como servidores remotos, autenticação OAuth, conexões de vários clientes e deploy em nuvem.</p>
<h3 id="o-fluxo-de-uma-chamada-de-ferramenta-mcp-por-um-llm"><a class="anchor" href="#o-fluxo-de-uma-chamada-de-ferramenta-mcp-por-um-llm">O fluxo de uma chamada de ferramenta MCP por um LLM</a></h3>
<p>Depois de ver as primitivas e os transportes, vamos seguir o fluxo de <strong>como um LLM realmente descobre e chama uma ferramenta MCP</strong>.</p>
<p>Na revisão 2025-11-25, quando uma conexão começa, acontece o seguinte handshake. Em 2026-10-08, o SDK de TypeScript 1.32.1 e a versão 2.3.1 da linha v2 também seguem essa ordem na configuração padrão.</p>
<ul>
<li><strong>Cliente → servidor</strong>: requisição <code>initialize</code> (envia a versão de protocolo suportada e as capabilities do cliente)</li>
<li><strong>Servidor → cliente</strong>: resposta <code>initialize</code> (capabilities do servidor e, opcionalmente, o campo <code>instructions</code>)</li>
<li><strong>Cliente → servidor</strong>: notificação <code>notifications/initialized</code></li>
<li><strong>Cliente → servidor</strong>: requisição <code>tools/list</code> → recebe a lista de ferramentas disponíveis</li>
<li>(Depois) O LLM decide chamar uma ferramenta → o cliente envia <code>tools/call</code> → recebe o resultado</li>
</ul>
<p>Na resposta <code>initialize</code>, o campo <code>instructions</code> é onde o servidor envia um texto sobre como suas ferramentas devem ser usadas. A linha instructions na saída da demo, mais abaixo, mostra esse valor.</p>
<p>Então, como a própria definição da ferramenta entra no campo de visão do LLM? A definição de uma ferramenta MCP tem esta forma de JSON Schema.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="json" data-theme="github-dark github-light"><code data-language="json" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">  "name"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"get_weather"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">  "description"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"Get current weather information for a location"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">  "inputSchema"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: {</span></span>
<span data-line=""><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">    "type"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"object"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">    "properties"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: { </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">"location"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: { </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">"type"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"string"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> } },</span></span>
<span data-line=""><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">    "required"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: [</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"location"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">]</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  }</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>O host converte a lista recebida por <code>tools/list</code> no <strong>parâmetro <code>tools</code> da Anthropic Messages API</strong> ou no <strong>parâmetro <code>tools</code> do OpenAI function calling</strong> e a inclui na chamada à API do LLM. No caso da Anthropic, quando o parâmetro de ferramentas é enviado, um <a href="https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview" target="_blank" rel="noopener noreferrer">special system prompt é adicionado automaticamente</a> para que o modelo entenda como chamar ferramentas. O tamanho varia conforme o modelo. Na tabela da documentação que consultei em 2026-10-08, os modelos atuais adicionavam de 286 a 675 tokens com <code>tool_choice: auto</code>.</p>
<p>Quando o LLM decide que precisa chamar uma ferramenta, a resposta contém um bloco <code>tool_use</code> (<code>{"type": "tool_use", "name": ..., "input": ...}</code>) e o <code>stop_reason</code> da resposta termina como <code>tool_use</code>. O host recebe isso, envia <code>tools/call</code> ao servidor MCP real, recebe o resultado, coloca-o em um bloco <code>tool_result</code> da próxima mensagem user e o envia de volta ao LLM. <strong>Esse loop se repete até que <code>stop_reason</code> mude de <code>tool_use</code> para outro valor, como <code>end_turn</code> ou <code>max_tokens</code>.</strong> O que costumamos chamar de “o agente trabalhando” é, na prática, uma sequência desses loops de chamada, resultado e nova chamada.</p>
<p>Desenhando, não há nenhuma linha entre o modelo e o servidor MCP. O modelo só vê as definições que recebeu por <code>tools</code>, e o servidor MCP recebe as requisições do host, não do modelo. O que liga os dois protocolos é o host no meio.</p>
<p><picture><source type="image/webp" srcset="/content/260524/1.720w.webp 720w, /content/260524/1.1440w.webp 1440w, /content/260524/1.2000w.webp 2000w" sizes="(max-width: 752px) calc(100vw - 32px), 720px"><img src="/content/260524/1.png" alt="Entre a API do LLM à esquerda e o host no centro vão setas rotuladas tools, tool_use e tool_result, e entre o host e o servidor MCP à direita vão setas rotuladas tools/list e tools/call. Não há nenhuma linha entre a API do LLM e o servidor MCP" style="max-width:min(720px, 100%);height:auto;" width="2000" height="640" loading="eager" fetchpriority="high" decoding="async"></picture></p>
<p>Executei essa conversão para ver o quanto ela é curta de fato. Rodou em 2026-10-08 com Node v24.16.0, <code>@modelcontextprotocol/sdk</code> 1.32.1 e <code>zod</code> 4.6.5. Em vez de stdio, o transporte é o <code>InMemoryTransport</code>, que liga servidor e cliente dentro de um mesmo processo, e nenhum LLM foi chamado. O bloco <code>tool_use</code> foi montado à mão no formato mostrado na documentação da Anthropic.</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">import</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { McpServer } </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">from</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> "@modelcontextprotocol/sdk/server/mcp.js"</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"> { Client } </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">from</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> "@modelcontextprotocol/sdk/client/index.js"</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"> { InMemoryTransport } </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">from</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> "@modelcontextprotocol/sdk/inMemory.js"</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"> { z } </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">from</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> "zod"</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">// MCP 서버: 도구 하나와 instructions 를 둔다</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> server</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"> McpServer</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  { name: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"weather"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, version: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"1.0.0"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> },</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  { instructions: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"Use get_weather for current conditions only."</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">server.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">registerTool</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span></span>
<span data-line=""><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">  "get_weather"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  { description: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"Get current weather information for a location"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, inputSchema: { location: z.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">string</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">() } },</span></span>
<span data-line=""><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">location</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"> ({ content: [{ type: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"text"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, text: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">`${</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">location</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">}: 15C, partly cloudy`</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:#E1E4E8;--shiki-light:#24292E"> [</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">clientT</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">serverT</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"> InMemoryTransport.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">createLinkedPair</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"> send</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> clientT.send.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">bind</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(clientT);</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">clientT.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">send</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">m</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"> { console.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">log</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"C->S"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, m.method, m.params?.protocolVersion </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 style="--shiki-dark:#F97583;--shiki-light:#D73A49">return</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> send</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(m); };</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">await</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> server.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">connect</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(serverT);</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> client</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"> Client</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({ name: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"demo-host"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, version: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"1.0.0"</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"> client.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">connect</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(clientT);</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">console.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">log</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"instructions:"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, client.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">getInstructions</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. tools/list 결과를 LLM API 의 tools 파라미터 모양으로 바꾼다</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">tools</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"> await</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> client.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">listTools</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"> anthropicTools</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> tools.</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">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"> ({ name: t.name, description: t.description, input_schema: t.inputSchema }));</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> openaiTools</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> tools.</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">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"> ({ type: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"function"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, name: t.name, description: t.description, parameters: t.inputSchema }));</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">console.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">log</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"anthropic:"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </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">(anthropicTools[</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">console.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">log</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"openai:"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </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">(openaiTools[</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:#6A737D;--shiki-light:#6A737D">// 2. 모델이 이런 tool_use 블록을 돌려줬다고 가정한다. input 이 그대로 tools/call 의 arguments 가 된다</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> toolUse</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { type: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"tool_use"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, id: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"toolu_demo"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, name: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"get_weather"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, input: { location: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"Seoul"</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"> client.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">callTool</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({ name: toolUse.name, arguments: toolUse.input });</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">console.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">log</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"tool_result:"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </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">({ type: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"tool_result"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, tool_use_id: toolUse.id, content: result.content }));</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">await</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> client.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">close</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">();</span></span></code></pre></figure>
<p>A saída de <code>node post-demo.mjs</code> é esta.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="text" data-theme="github-dark github-light"><code data-language="text" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span>C->S initialize 2025-11-25</span></span>
<span data-line=""><span>C->S notifications/initialized </span></span>
<span data-line=""><span>instructions: Use get_weather for current conditions only.</span></span>
<span data-line=""><span>C->S tools/list </span></span>
<span data-line=""><span>anthropic: {"name":"get_weather","description":"Get current weather information for a location","input_schema":{"type":"object","properties":{"location":{"type":"string"}},"required":["location"],"$schema":"http://json-schema.org/draft-07/schema#"}}</span></span>
<span data-line=""><span>openai: {"type":"function","name":"get_weather","description":"Get current weather information for a location","parameters":{"type":"object","properties":{"location":{"type":"string"}},"required":["location"],"$schema":"http://json-schema.org/draft-07/schema#"}}</span></span>
<span data-line=""><span>C->S tools/call </span></span>
<span data-line=""><span>tool_result: {"type":"tool_result","tool_use_id":"toolu_demo","content":[{"type":"text","text":"Seoul: 15C, partly cloudy"}]}</span></span></code></pre></figure>
<p>A conversão das definições de ferramenta não passa de renomear campos. O <code>inputSchema</code> do MCP vira <code>input_schema</code> na Anthropic e <code>parameters</code> na OpenAI. A saída também mostra que o SDK acrescenta <code>$schema</code> ao schema. A direção contrária é igualmente curta. O <code>tool_use.input</code> da Anthropic é um objeto, então entra direto em <code>tools/call</code> como seus <code>arguments</code>. No lado do resultado, blocos de texto têm o mesmo formato e passam direto, mas resultados com imagem ou de erro têm formato diferente, e o helper de MCP do SDK da Anthropic os converte separadamente. O formato da OpenAI acima é o da Responses API. Os <code>arguments</code> de uma chamada que a OpenAI devolve são uma <a href="https://developers.openai.com/api/docs/guides/function-calling" target="_blank" rel="noopener noreferrer">string JSON</a>, então precisam passar uma vez por <code>JSON.parse</code> antes de serem repassados. O código acima só monta um bloco <code>tool_use</code> no formato da Anthropic, então essa etapa de parse não aparece na saída.</p>
<h3 id="quatro-coisas-que-o-mcp-acrescenta"><a class="anchor" href="#quatro-coisas-que-o-mcp-acrescenta">Quatro coisas que o MCP acrescenta</a></h3>
<p>Então, o que o MCP acrescenta ao function calling? Pela revisão 2025-11-25, são quatro coisas.</p>
<ul>
<li><strong>Descoberta dinâmica</strong>: a lista de ferramentas não é conhecida em tempo de build, e sim obtida em tempo de execução por <code>tools/list</code>. O servidor pode avisar por <code>notifications/tools/list_changed</code> que a lista mudou durante a conexão</li>
<li><strong>Stateful session</strong>: a conexão é estabelecida com <code>initialize</code>, e as requisições são trocadas dentro dela. Não há uma mensagem JSON-RPC de encerramento; a sessão termina com o fechamento do transporte</li>
<li><strong>Primitivas além de Tool</strong>: Resource, Prompt, Sampling, Roots e Elicitation são expostos por capability negotiation. A capability negotiation é a etapa de <code>initialize</code> em que cada lado anuncia os recursos que suporta</li>
<li><strong>Bidirecionalidade</strong>: o servidor pode pedir, no sentido inverso, uma completion ao LLM do cliente por Sampling (deprecated na revisão 2026-07-28)</li>
</ul>
<h3 id="depois-da-revisão-2026-07-28"><a class="anchor" href="#depois-da-revisão-2026-07-28">Depois da revisão 2026-07-28</a></h3>
<p>Em 2026-10-08, a revisão que o site oficial abre como latest é a <a href="https://modelcontextprotocol.io/specification/2026-07-28/changelog" target="_blank" rel="noopener noreferrer">revisão 2026-07-28</a>, e nela metade desta lista mudou. Primeiro, o handshake formado por <code>initialize</code> e <code>notifications/initialized</code> e as sessões no nível do protocolo desapareceram. Em vez disso, cada requisição leva em <code>_meta</code> a versão do protocolo e as capabilities do cliente. Esse campo é um espaço que o MCP reserva para anexar metadados à parte dos parâmetros próprios da mensagem. Os servidores MUST implementar <code>server/discover</code>. É a RPC que devolve as versões do protocolo suportadas, as capabilities e as informações do servidor, e o cliente pode chamá-la antes de qualquer outra requisição para verificar de antemão as versões e capabilities suportadas.</p>
<p>As requisições que o servidor enviava primeiro foram substituídas por um padrão chamado Multi Round-Trip Requests (MRTR). Em vez de enviar uma requisição separada, o servidor devolve um resultado intermediário dizendo que precisa de mais dados (<code>input_required</code>), e o cliente preenche esses dados e reenvia a requisição original.</p>
<p>Sampling e Roots ficaram deprecated junto com Logging. Continuam na especificação e funcionam, mas implementações novas não devem adotá-los, e a especificação recomenda integrar diretamente com as APIs dos provedores de LLM em vez de usar Sampling.</p>
<p>Mesmo assim, o comportamento padrão dos SDKs ainda é o antigo. O SDK de TypeScript 1.32.1 tem <code>2025-11-25</code> como constante de versão mais recente e não conhece a revisão 2026-07-28, e a 2.3.1 suporta essa revisão, mas sua negociação de versão padrão é <code>legacy</code>. A primeira linha da saída acima, <code>initialize 2025-11-25</code>, é o resultado disso.</p>
<p>O que resta das quatro, então, é a descoberta dinâmica e as primitivas além de Tool (Resource, Prompt, Elicitation). A descoberta dinâmica também mudou um pouco de forma: as notificações de mudança de lista só chegam aos clientes que fazem opt-in no stream <code>subscriptions/listen</code>. No fim, o ponto em que o MCP se separa do function calling está menos nas sessões ou na bidirecionalidade e mais em <strong>um contrato para trocar a lista de ferramentas e o contexto em tempo de execução</strong>.</p>
<h3 id="quando-a-api-vira-o-cliente-mcp"><a class="anchor" href="#quando-a-api-vira-o-cliente-mcp">Quando a API vira o cliente MCP</a></h3>
<p>O <a href="https://platform.claude.com/docs/en/agents-and-tools/mcp-connector" target="_blank" rel="noopener noreferrer">MCP connector</a> (beta) da Anthropic mostra quanto desse contrato chega ao modelo. É um recurso em que a Messages API se conecta diretamente a um servidor MCP remoto, e a seção Limitations da documentação diz que, dos recursos da especificação MCP, "only tool calls are currently supported", e que "Local STDIO servers cannot be connected directly". A mesma documentação orienta que, se você precisar de servidores locais, prompts ou resources do MCP, gerencie a conexão por conta própria com um SDK de MCP e use os helpers de conversão do SDK da Anthropic.</p>
<p>Ou seja, quando o MCP é consumido na camada de function calling, só sobra o Tool. Resource e Prompt só fazem sentido quando há um host para levá-los à tela ou ao contexto. O guia de function calling da OpenAI também apresenta uma forma de usar a funcionalidade de um servidor MCP como built-in tool. O <a href="https://developers.openai.com/api/docs/guides/tools-connectors-mcp" target="_blank" rel="noopener noreferrer">guia MCP servers</a> da OpenAI só explica como listar e chamar ferramentas, e não diz se oferece suporte a Resource ou Prompt.</p>
<h3 id="a-superfície-de-ataque-da-descoberta-dinâmica"><a class="anchor" href="#a-superfície-de-ataque-da-descoberta-dinâmica">A superfície de ataque da descoberta dinâmica</a></h3>
<p>As descrições das ferramentas e os resultados de suas chamadas chegam, pelo host, ao contexto do modelo. Então, o que quer que o servidor escreva ali, o modelo lê. Os dois ataques representativos vêm daí.</p>
<ul>
<li>
<p><strong>Tool Poisoning Attack (TPA)</strong>: um ataque que a <a href="https://invariantlabs.ai/blog/mcp-security-notification-tool-poisoning-attacks" target="_blank" rel="noopener noreferrer">Invariant Labs</a> nomeou e para o qual publicou uma PoC em abril de 2025. Se instruções maliciosas forem escondidas na descrição (description) de uma ferramenta de um servidor MCP, o modelo lê esse texto, que o usuário não vê, e pode segui-lo sem que o usuário saiba.</p>
</li>
<li>
<p><strong>Rug Pull</strong>: um ataque, descrito pela Invariant Labs no mesmo post, em que o servidor muda a definição de uma ferramenta depois que o usuário a aprovou. Como no exemplo de Elena Cross <a href="https://simonwillison.net/2025/Apr/9/mcp-prompt-injection/" target="_blank" rel="noopener noreferrer">citado por Simon Willison</a>, você aprova no dia 1 uma ferramenta que parece segura e, no dia 7, ela já foi alterada para enviar suas chaves de API a um atacante; isso acontece porque as definições de ferramenta são obtidas do servidor em tempo de execução, e não no momento da instalação. A <a href="https://modelcontextprotocol.io/specification/2025-11-25/server/tools" target="_blank" rel="noopener noreferrer">especificação de tools</a> recomenda (SHOULD) uma UI que mostre quais ferramentas estão expostas ao modelo, mas não exige uma nova aprovação da definição alterada, então essa aprovação fica a cargo do host.</p>
</li>
</ul>
<h2 id="conclusão"><a class="anchor" href="#conclusão">Conclusão</a></h2>
<p>Em resumo, o MCP não substitui o function calling; é um padrão que se apoia sobre ele. O modelo continua chamando ferramentas pelo parâmetro <code>tools</code> e pelo loop de <code>tool_use</code>, e o host traduz entre os dois protocolos. Na revisão 2025-11-25, o que o MCP acrescentava era a descoberta dinâmica, uma stateful session, primitivas além de Tool e chamadas que vão do servidor para o cliente. Com o fim das sessões e o Sampling deprecated na revisão 2026-07-28, o que resta é um contrato para trocar a lista de ferramentas e o contexto em tempo de execução. Por causa desse contrato, ataques que envenenam as definições de ferramentas ou as mudam em silêncio surgem no mesmo lugar. Quando adicionar mais um servidor MCP, recomendo verificar não só o que esse servidor pode fazer, mas também se o seu host avisa quando as definições dele mudam.</p>
<p>Se o MCP trata do que permitir que o agente faça, o que informar a ele é papel de arquivos de contexto como <code>CLAUDE.md</code> e <code>AGENTS.md</code>. Como o agente lê esses arquivos e até que ponto suas instruções são seguidas é o tema de <a href="/260529" target="_blank" rel="noopener noreferrer">Arquivos de contexto</a>.</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:#3b82f6"></span><a href="https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle" target="_blank" rel="noopener noreferrer">MCP Specification 2025-11-25, Lifecycle</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#3b82f6"></span><a href="https://modelcontextprotocol.io/specification/2026-07-28/basic/versioning" target="_blank" rel="noopener noreferrer">MCP Specification 2026-07-28, Versioning</a></div>
</div>
</div></details>]]></content:encoded>
            <author>jihoon7705@gmail.com (이지훈)</author>
            <category>IA</category>
            <category>Ferramentas-de-desenvolvimento</category>
            <category>Claude</category>
            <category>MCP</category>
        </item>
        <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 <strong>parser</strong>. 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 <strong>mudança de dados</strong>.</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>
<details class="ref-block"><summary class="ref-summary">참고 자료</summary><div class="ref-list">
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#3b82f6"></span><a href="https://react.dev/learn/choosing-the-state-structure" target="_blank" rel="noopener noreferrer">React, Choosing the State Structure</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#3b82f6"></span><a href="https://legacy.reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html" target="_blank" rel="noopener noreferrer">React, You Probably Don't Need Derived State</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#3b82f6"></span><a href="https://xstate.js.org/" target="_blank" rel="noopener noreferrer">XState</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#f59e0b"></span><a href="https://www.syncfusion.com/blogs/post/react-state-management-libraries" target="_blank" rel="noopener noreferrer">Top 5 React State Management Tools in 2026</a></div>
</div>
</div></details>]]></content:encoded>
            <author>jihoon7705@gmail.com (이지훈)</author>
            <category>frontend</category>
            <category>gerenciamento-de-estado</category>
            <category>React</category>
            <category>arquitetura</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 onde a lógica de domínio deve ficar no frontend. É para desenvolvedores frontend cujas regras de negócio, como o cálculo de impostos ou a verificação do status de uma dec...]]></description>
            <content:encoded><![CDATA[<p>Neste post, quero falar sobre <strong>onde a lógica de domínio deve ficar no frontend</strong>.</p>
<p>É para desenvolvedores frontend cujas regras de negócio, como o cálculo de impostos ou a verificação do status de uma declaração, estão espalhadas por componentes, hooks e utils, de modo que cada mudança de regra exige procurar e corrigir vários arquivos. Ao final, você saberá como mover essas regras para funções puras que não dependem do React e com que critério decidir onde essa separação deve parar.</p>
<p>Todos os exemplos usam o cálculo do imposto de renda, um domínio que me interessa.</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>Antes de responder, vamos alinhar um termo. Neste post, um <strong>modelo de domínio</strong> reúne os conceitos de negócio junto com as regras que operam sobre eles. Ele é diferente de um <strong>modelo de dados</strong>, como o tipo de uma resposta da API, que só define o formato em que os dados trafegam. Essa diferença também explica por que, quando os componentes dependem diretamente da estrutura da resposta, todo o frontend balança a cada mudança no schema do backend. Como os quatro termos (domínio, modelo de domínio, objeto de domínio e modelo de objetos de domínio) se diferenciam está explicado à parte em <a href="/260413" target="_blank" rel="noopener noreferrer">Termos de domínio</a>.</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>
<h4 id="atrito-com-o-gerenciamento-de-estado-do-react"><a class="anchor" href="#atrito-com-o-gerenciamento-de-estado-do-react">Atrito com o gerenciamento de estado do React</a></h4>
<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>
<h4 id="dificuldade-de-garantir-imutabilidade"><a class="anchor" href="#dificuldade-de-garantir-imutabilidade">Dificuldade de garantir imutabilidade</a></h4>
<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>
<h4 id="obter-coesão-com-namespace-de-módulo"><a class="anchor" href="#obter-coesão-com-namespace-de-módulo">Obter coesão com namespace de módulo</a></h4>
<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>
<h4 id="o-sujeito-do-domínio-como-primeiro-argumento"><a class="anchor" href="#o-sujeito-do-domínio-como-primeiro-argumento">O sujeito do domínio como primeiro argumento</a></h4>
<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>
<h4 id="agrupar-comportamentos-em-uma-factory"><a class="anchor" href="#agrupar-comportamentos-em-uma-factory">Agrupar comportamentos em uma Factory</a></h4>
<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, o frontend tem sua própria lógica de domínio, e suas regras não ficam nos componentes, mas em funções puras ao lado dos tipos. As respostas da API são convertidas em tipos de domínio em um único mapper, a apresentação é separada em um ViewModel e o que muda junto fica junto.</p>
<p>Colocar isso 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>
<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>frontend</category>
            <category>arquitetura</category>
            <category>DDD</category>
        </item>
        <item>
            <title><![CDATA[Termos de domínio]]></title>
            <link>https://hooninedev.com/pt-BR/260413</link>
            <guid isPermaLink="false">https://hooninedev.com/pt-BR/260413</guid>
            <pubDate>Mon, 13 Apr 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Neste post, quero falar sobre como domínio, modelo de domínio, objeto de domínio e modelo de objetos de domínio se diferenciam. É para desenvolvedores frontend que, lendo sobre DDD, ficaram em dúvida ...]]></description>
            <content:encoded><![CDATA[<p>Neste post, quero falar sobre <strong>como domínio, modelo de domínio, objeto de domínio e modelo de objetos de domínio se diferenciam</strong>.</p>
<p>É para desenvolvedores frontend que, lendo sobre DDD, ficaram em dúvida se essas palavras apontam para a mesma coisa. Ao final, você conseguirá colocar os quatro termos em uma única linha que desce do abstrato ao concreto e explicar por que o tipo de uma resposta da API não é um modelo de domínio.</p>
<p>Encontrei essas palavras com bastante frequência como desenvolvedor, mas quando alguém pergunta "o que exatamente é um domínio?", não é fácil responder com clareza. (Sinceramente, quando comecei a programar, achava que domínio era aquele do "www".) Todos os exemplos usam o cálculo do imposto de renda.</p>
<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><picture><source type="image/webp" srcset="/content/260413/1.720w.webp 720w, /content/260413/1.1090w.webp 1090w" sizes="(max-width: 840px) calc(100vw - 32px), 808px"><img src="/content/260413/1.png" alt="Pipeline de cálculo do imposto de renda, da receita bruta até o pagamento ou a restituição, com cada etapa colorida pelos subdomínios de renda, dedução, imposto e declaração" width="1090" height="566" loading="eager" fetchpriority="high" decoding="async"></picture></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><a href="https://martinfowler.com/eaaCatalog/domainModel.html" target="_blank" rel="noopener noreferrer">Martin Fowler</a> define o modelo de domínio como um modelo de objetos do domínio que incorpora tanto comportamento quanto dados. A definição de Eric Evans vai um passo além.</p>
<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 desta seção é 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="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> é, na perspectiva que adotei aqui, 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>Como essa distinção aparece no código, ou seja, onde a lógica de domínio, como o cálculo de impostos, deve ficar fora dos componentes e até onde separá-la, é o tema de <a href="/260418" target="_blank" rel="noopener noreferrer">Modelo de domínio</a>.</p>
<p>Espero que, da próxima vez que você olhar para o tipo de uma resposta da API, pergunte a si mesmo pelo menos uma vez: "isto é um modelo de dados ou um modelo de domínio?"</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://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></details>]]></content:encoded>
            <author>jihoon7705@gmail.com (이지훈)</author>
            <category>frontend</category>
            <category>arquitetura</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 385 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>frontend</category>
            <category>React</category>
            <category>refatoração</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 março 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. <strong>(Se este texto parecer óbvio demais ou ultrapassado daqui a um ano, talvez isso signifique que reagimos bem.)</strong></p>]]></content:encoded>
            <author>jihoon7705@gmail.com (이지훈)</author>
            <category>frontend</category>
            <category>carreira</category>
            <category>IA</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 <strong>Clean Code</strong>, 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 <strong>Smalltalk Best Practice Patterns</strong> 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 <strong>Clean Code</strong>, 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>frontend</category>
            <category>design</category>
            <category>abstração</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. É para desenvolvedores frontend que usam TanStack Query e estão em dúvida entre manter as queryKeys como vetores inline, reuni-las em um o...]]></description>
            <content:encoded><![CDATA[<p>Neste artigo, quero falar sobre a <strong>queryKey do TanStack Query</strong>.</p>
<p>É para desenvolvedores frontend que usam TanStack Query e estão em dúvida entre manter as queryKeys como vetores inline, reuni-las em um objeto de constantes ou em uma fábrica de chaves, ou migrá-las para o <code>queryOptions</code> da v5. Ao final, você saberá que problema cada abordagem surgiu para resolver e terá um critério para julgar qual etapa combina com o seu código hoje.</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>
<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>Então, como o TanStack Query reconhece como a mesma chave um vetor criado de novo a cada renderização? Ele não compara as referências dos vetores; usa como chave de cache a string obtida ao serializar a queryKey com <code>hashKey</code> (<code>queryHash</code>). Essa função ordena as chaves dos objetos simples enquanto serializa com <code>JSON.stringify</code>, de modo que a ordem das chaves dentro de um objeto não afeta o hash, mas a ordem dos elementos de um vetor afeta. Uma propriedade cujo valor é <code>undefined</code> desaparece durante a serialização, então <code>{ a: 1, b: undefined }</code> e <code>{ a: 1 }</code> viram a mesma chave. Reuni esse comportamento, acompanhando o código da implementação, separadamente em <a href="/251230" target="_blank" rel="noopener noreferrer">Como as queryKey são comparadas</a>.</p>
<h2 id="regras-para-escrever-querykey"><a class="anchor" href="#regras-para-escrever-querykey">Regras para escrever queryKey</a></h2>
<p>Depois de entender que a queryKey é ao mesmo tempo o identificador do cache e um vetor de dependências, as regras de escrita decorrem naturalmente. As recomendações da documentação oficial podem ser resumidas assim.</p>
<h3 id="sempre-um-vetor"><a class="anchor" href="#sempre-um-vetor">Sempre um vetor</a></h3>
<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>
<h3 id="todas-as-variáveis-que-queryfn-usa"><a class="anchor" href="#todas-as-variáveis-que-queryfn-usa">Todas as variáveis que queryFn usa</a></h3>
<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>
<h3 id="do-genérico-ao-específico"><a class="anchor" href="#do-genérico-ao-específico">Do genérico ao específico</a></h3>
<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="vetores-declarados-diretamente"><a class="anchor" href="#vetores-declarados-diretamente">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="objeto-de-constantes"><a class="anchor" href="#objeto-de-constantes">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="fábrica-de-chaves-de-consulta"><a class="anchor" href="#fábrica-de-chaves-de-consulta">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="lukemoralesquery-key-factory"><a class="anchor" href="#lukemoralesquery-key-factory">@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="queryoptions-api-oficial-da-v5"><a class="anchor" href="#queryoptions-api-oficial-da-v5">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>
<h3 id="hierarquia-e-inferência-de-tipos"><a class="anchor" href="#hierarquia-e-inferência-de-tipos">Hierarquia e inferência de tipos</a></h3>
<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>
<h3 id="opções-sobrescritas-por-componente"><a class="anchor" href="#opções-sobrescritas-por-componente">Opções sobrescritas por componente</a></h3>
<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>
<h3 id="menos-hooks-personalizados-sobre-usequery"><a class="anchor" href="#menos-hooks-personalizados-sobre-usequery">Menos hooks personalizados sobre <code>useQuery</code></a></h3>
<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>
<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>frontend</category>
            <category>React</category>
            <category>TanStack-Query</category>
            <category>queryKey</category>
        </item>
        <item>
            <title><![CDATA[Como as queryKey são comparadas]]></title>
            <link>https://hooninedev.com/pt-BR/251230</link>
            <guid isPermaLink="false">https://hooninedev.com/pt-BR/251230</guid>
            <pubDate>Tue, 30 Dec 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Neste artigo, quero falar sobre como o TanStack Query decide que duas queryKeys são a mesma chave. É para quem usa TanStack Query e já se perguntou por que uma queryKey, criada como um vetor novo a ca...]]></description>
            <content:encoded><![CDATA[<p>Neste artigo, quero falar sobre <strong>como o TanStack Query decide que duas queryKeys são a mesma chave</strong>.</p>
<p>É para quem usa TanStack Query e já se perguntou por que uma queryKey, criada como um vetor novo a cada renderização, não causa um cache miss toda vez, e se a ordem das chaves de um objeto ou valores <code>undefined</code> afetam o cache; ao final, você saberá qual regra faz duas chaves serem iguais e como essa regra afeta o cache. Adiantando a conclusão: o TanStack Query usa como chave de cache a string obtida ao serializar a queryKey com <code>hashKey</code>, e nesse processo a ordem das chaves dos objetos é ignorada, enquanto a ordem dos elementos dos vetores é preservada.</p>
<p>A queryKey é o vetor que o TanStack Query usa como base para gerenciar o cache de consultas. A mesma chave significa os mesmos dados, e quando <code>['user', userId]</code> muda porque o seu <code>userId</code> mudou, ocorre um cache miss e os dados são buscados novamente com fetch.</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>. Na <a href="https://github.com/TanStack/query/blob/%40tanstack%2Fquery-core%405.104.1/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>. O campo tem o tipo <code>QueryStore</code>, e o construtor atribui a ele um <code>new Map&#x3C;string, Query>()</code>. As entradas são gravadas e consultadas com <code>queryHash</code> como chave. A chave é a forma serializada de queryKey (<code>queryHash</code>), e o valor é uma instância da classe <code>Query</code>. O código e os resultados deste artigo se baseiam em <code>@tanstack/query-core</code> 5.104.1.</p>
<p>Diferente dos objetos simples usados em versões antigas, um <code>Map</code> nunca colide com chaves herdadas de um protótipo.</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.</p>
<h2 id="ordenação-de-chaves-em-hashkey"><a class="anchor" href="#ordenação-de-chaves-em-hashkey">Ordenação de chaves em hashKey</a></h2>
<p>A função que produz o valor de hash é <code>hashKey</code>. A implementação oficial, definida em <a href="https://github.com/TanStack/query/blob/%40tanstack%2Fquery-core%405.104.1/packages/query-core/src/utils.ts#L284-L295" target="_blank" rel="noopener noreferrer"><code>packages/query-core/src/utils.ts</code></a>, é 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>Usa <code>JSON.stringify</code> com 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> que <strong>ordena lexicograficamente as chaves dos objetos simples</strong> antes da serialização. A rigor, é a ordem de unidades de código UTF-16, a comparação padrão de <code>sort()</code>, então chaves maiúsculas vêm antes das minúsculas.</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, o <code>JSON.stringify</code> comum 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. É 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, de modo que objetos com o mesmo significado sempre viram a mesma string. A direção inversa não é garantida, como veremos mais adiante.</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 esses dois casos de maneira distinta. A recomendação do mantenedor TkDodo, em <a href="https://tkdodo.eu/blog/effective-react-query-keys" target="_blank" rel="noopener noreferrer">Effective React Query Keys</a>, de estruturar queryKey do mais genérico para o mais específico também vem de a ordem do vetor ter significado. O motivo que ele dá é a invalidação: chaves que compartilham a parte inicial podem ser invalidadas de uma vez com <code>['todos']</code>. Essa comparação não fica a cargo do hash, e sim da correspondência por prefixo, que veremos mais adiante.</p>
<p>A ordenação das chaves só se aplica a <strong>objetos simples</strong>. No mesmo arquivo, <code>isPlainObject</code> verifica se <code>Object.prototype.toString</code> retorna <code>[object Object]</code> e se o protótipo é <code>Object.prototype</code> (ou <code>null</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. Ao inserir diretamente uma instância de classe em queryKey, as chaves não são ordenadas e ela é serializada na ordem em que seus campos foram atribuídos, então valores iguais podem produzir hashes diferentes.</p>
<p>Do ponto de vista de quem usa, há dois resultados.</p>
<h3 id="a-ordem-das-chaves-de-um-objeto-é-irrelevante"><a class="anchor" href="#a-ordem-das-chaves-de-um-objeto-é-irrelevante">A ordem das chaves de um objeto é irrelevante</a></h3>
<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>Sem a ordenação de chaves, seria necessário lembrar a ordem das chaves toda vez que se usasse um literal de objeto.</p>
<h3 id="a-ordem-dos-elementos-de-um-vetor-é-relevante"><a class="anchor" href="#a-ordem-dos-elementos-de-um-vetor-é-relevante">A ordem dos elementos de um vetor é relevante</a></h3>
<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>
<h2 id="valores-que-a-serialização-altera"><a class="anchor" href="#valores-que-a-serialização-altera">Valores que a serialização altera</a></h2>
<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>Dentro de um vetor, o comportamento é diferente. Um elemento <code>undefined</code> não desaparece; ele vira <code>null</code>. Por isso <code>['user', undefined]</code> e <code>['user', null]</code> são a mesma chave, e ambas são diferentes de <code>['user']</code>. É o que acontece com <code>['user', userId]</code> quando <code>userId</code> ainda é <code>undefined</code>. Mesmo que você bloqueie o fetch com <code>enabled: false</code>, uma entrada <code>["user",null]</code> é criada no cache. Confirmei isso criando, com as mesmas opções, o objeto que <code>useQuery</code> usa internamente, um <code>QueryObserver</code>.</p>
<p><code>undefined</code> não é o único caso. Como <code>hashKey</code> se apoia em <code>JSON.stringify</code>, a maioria dos valores que o JSON não consegue representar vira outro valor sem nenhum erro. Executei o código abaixo em 2026-10-08 com <code>@tanstack/query-core</code> 5.104.1 e Node v24.16.0; os comentários são a saída real.</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">import</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { hashKey, QueryClient } </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">from</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> '@tanstack/query-core'</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">console.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">log</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">hashKey</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">undefined</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">])) </span><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// ["user",null]</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">console.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">log</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">hashKey</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">null</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">])) </span><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// ["user",null]</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">console.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">log</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">hashKey</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:#6A737D;--shiki-light:#6A737D">// ["user"]</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">console.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">log</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">hashKey</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">([</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'f'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, { </span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">cb</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:#6A737D;--shiki-light:#6A737D">// ["f",{}]</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">console.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">log</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">hashKey</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">([</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'m'</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"> 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 style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// ["m",{}]</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">console.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">log</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">hashKey</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">([</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'d'</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 style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'2025-12-30T00:00:00Z'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">)])) </span><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// ["d","2025-12-30T00:00:00.000Z"]</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">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:#9ECBFF;--shiki-light:#032F62">'m'</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"> 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 style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'mapA'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">console.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">log</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">'m'</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"> Map</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:#79B8FF;--shiki-light:#005CC5">2</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">]])])) </span><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// mapA</span></span></code></pre></figure>
<p>Testando outros valores da mesma forma, o resultado fica assim.</p>
<table>
<thead>
<tr>
<th>Valor na chave</th>
<th>Resultado serializado</th>
<th>Tratado como a mesma chave que</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>undefined</code>, <code>NaN</code>, <code>Infinity</code> ou uma função como elemento de vetor</td>
<td><code>null</code></td>
<td>uma chave com <code>null</code> nessa posição</td>
</tr>
<tr>
<td><code>undefined</code> ou uma função como propriedade de objeto</td>
<td>a propriedade desaparece</td>
<td>uma chave sem essa propriedade</td>
</tr>
<tr>
<td><code>Map</code>, <code>Set</code></td>
<td><code>{}</code></td>
<td>qualquer <code>Map</code>, <code>Set</code> ou objeto vazio, seja qual for o conteúdo</td>
</tr>
<tr>
<td><code>Date</code></td>
<td>uma string ISO</td>
<td>a mesma string ISO</td>
</tr>
<tr>
<td><code>BigInt</code></td>
<td>lança <code>TypeError</code></td>
<td>nenhuma</td>
</tr>
<tr>
<td>referência circular</td>
<td>lança <code>RangeError</code> se o ciclo passa só por objetos simples e <code>TypeError</code> se passa por um vetor ou uma instância de classe</td>
<td>nenhuma</td>
</tr>
</tbody>
</table>
<p>Os mais perigosos são <code>Map</code> e <code>Set</code>. No código acima, os dados guardados com a chave <code>new Map([['a', 1]])</code> voltaram ao consultar com <code>new Map([['b', 2]])</code>. Como não há erro, também não há nenhuma pista de que a tela está exibindo dados errados.</p>
<p>Só dois casos aparecem como erro: <code>BigInt</code> e referências circulares. Nas referências circulares, o erro depende do que forma o ciclo. Um ciclo que passa só por objetos simples termina com <code>RangeError: Maximum call stack size exceeded</code>, enquanto um que passa por pelo menos um vetor ou uma instância de classe, como em <code>arr.push(arr)</code>, termina com <code>TypeError: Converting circular structure to JSON</code>. Como a função substituidora devolve um objeto novo para cada objeto simples, a detecção de ciclos de <code>JSON.stringify</code> nunca reencontra o mesmo objeto; já vetores e instâncias de classe são devolvidos como estão pela função substituidora, e o ciclo é detectado. Já <code>Date</code> vira uma string ISO por meio de <code>toJSON</code>, então, na consulta ao cache, o mesmo instante gera a mesma chave, o que o torna relativamente seguro.</p>
<p>Uma vez coloquei um <code>Date</code> diretamente em uma chave e passei um bom tempo me perguntando: "Por que o cache está sendo atualizado se é o mesmo instante?". Um <code>Date</code> que aponta para o mesmo instante vira a mesma string ISO mesmo sendo outra instância, então gera o mesmo hash. Se saía uma chave diferente a cada vez, o horário em si era diferente, mesmo que parecesse o mesmo instante. Criar um <code>new Date()</code> durante a renderização coloca um horário diferente por milissegundos em cada renderização, e cada um vira uma chave nova.</p>
<p>Por isso, o mais seguro é colocar em uma queryKey apenas strings, números, booleanos, <code>null</code> e vetores e objetos simples formados por eles.</p>
<h2 id="querykeyhashfn"><a class="anchor" href="#querykeyhashfn">queryKeyHashFn</a></h2>
<p>Essa restrição tem uma saída. Por meio da opção <code>queryKeyHashFn</code>, o TanStack Query permite <strong>substituir a própria função de hash</strong>. Internamente, <code>hashQueryKeyByOptions(queryKey, options)</code> decide: se as opções tiverem <code>queryKeyHashFn</code>, chama essa função; caso contrário, chama o <code>hashKey</code> padrão.</p>
<p>Substituir significa trocar <code>hashKey</code> por inteiro. A ordenação de chaves vista antes vai embora junto, então, se você precisar dela, terá de implementá-la. O caso em que essa opção é útil é um valor com o qual a serialização padrão lança erro, como <code>BigInt</code>. O código abaixo também foi executado no mesmo ambiente.</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">import</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { QueryClient } </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">from</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> '@tanstack/query-core'</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"> bigintSafeHash</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">queryKey</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">(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">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"> (</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">typeof</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> v </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">===</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> 'bigint'</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">toString</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>
<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">  defaultOptions: { queries: { queryKeyHashFn: bigintSafeHash } },</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">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:#9ECBFF;--shiki-light:#032F62">'order'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">9007199254740993</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">n</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">], </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'ok'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">console.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">log</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">'order'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">9007199254740993</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">n</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">])) </span><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// ok</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">console.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">log</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">'order'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'9007199254740993'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">])) </span><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// ok</span></span>
<span data-line=""> </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:#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"> }], </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'A'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">)</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">console.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">log</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">'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"> }])) </span><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// undefined</span></span></code></pre></figure>
<p>A segunda saída significa que um <code>BigInt</code> e uma string com o mesmo número viram a mesma chave. A última saída é o efeito de perder a ordenação: objetos que diferem só na ordem das chaves deixam de ser tratados como a mesma chave.</p>
<p>O lugar onde ela é registrada também muda o resultado. Se for registrada no <code>QueryClient</code> por meio de <code>defaultOptions</code> ou de <code>setQueryDefaults</code>, como acima, <code>setQueryData</code> e <code>getQueryData</code> também usam essa função. Isso acontece porque as duas APIs mesclam as opções padrão com <code>defaultQueryOptions</code> antes de calcular o hash. Já se ela for escrita só na chamada de <code>useQuery</code>, as APIs imperativas usam o <code>hashKey</code> padrão, e a mesma chave se divide em duas entradas no cache. Na fase beta da v3.2.0, nem o valor global era aplicado a <code>setQueryData</code>, e quem reportou a <a href="https://github.com/TanStack/query/issues/1343" target="_blank" rel="noopener noreferrer">Issue #1343</a> confirmou que isso foi corrigido na v3.2.0-beta.30.</p>
<p>Por isso, na prática, é muito mais seguro evitar essa saída e <strong>converter os valores para uma forma serializável no momento de construir a queryKey</strong>. Escrever a própria função de hash exige cuidar tanto da ordenação de chaves quanto do lugar onde ela é registrada.</p>
<h2 id="como-os-filtros-comparam-chaves"><a class="anchor" href="#como-os-filtros-comparam-chaves">Como os filtros comparam chaves</a></h2>
<p>Há mais uma forma de julgar se duas chaves são iguais. A "mesma chave" de que falamos até aqui significa que as strings de hash são iguais, e o hash só é usado na consulta ao cache e em filtros com <code>exact: true</code>. Filtros como <code>invalidateQueries</code> e <code>findAll</code> decidem por padrão com <code>partialMatchKey</code>, que compara recursivamente a estrutura da queryKey original, e não a string de hash. Vetores são comparados a partir do início, e nos objetos só são verificadas as chaves escritas no filtro. O código abaixo também foi executado no mesmo ambiente.</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">import</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { partialMatchKey } </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">from</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> '@tanstack/query-core'</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"> queryKey</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">, { 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"> }]</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">console.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">log</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">partialMatchKey</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:#6A737D;--shiki-light:#6A737D">// true</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">console.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">log</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">partialMatchKey</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"> }])) </span><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// true</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">console.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">log</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">partialMatchKey</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(queryKey, [{ status: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'done'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> }])) </span><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// false</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">console.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">log</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">partialMatchKey</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">'todo'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> }])) </span><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// false</span></span></code></pre></figure>
<p>Como essa correspondência não passa pelo hash, trocar <code>queryKeyHashFn</code> não a altera. Em um <code>QueryClient</code> com uma função de hash sem ordenação, como no exemplo anterior, um objeto que difere só na ordem das chaves não é encontrado com <code>exact: true</code>, mas a correspondência por prefixo ainda o encontra.</p>
<p>Por isso não há garantia de que chaves iguais pelo hash também sejam iguais em um filtro. Uma query criada com <code>['user', undefined]</code> não é pega pela correspondência por prefixo de um filtro <code>['user', null]</code>, e uma chave que contém <code>NaN</code> não corresponde nem a si mesma. Na direção oposta, se você colocar um <code>Date</code> ou um <code>Map</code> em um filtro, como ele não tem propriedades enumeráveis para comparar, corresponde a qualquer <code>Date</code> ou objeto na mesma posição.</p>
<h2 id="conclusão"><a class="anchor" href="#conclusão">Conclusão</a></h2>
<p>Em resumo, o TanStack Query não compara as referências dos vetores queryKey. <code>hashKey</code> ordena as chaves dos objetos simples enquanto serializa com <code>JSON.stringify</code>, e a string resultante (<code>queryHash</code>) é usada como chave de um <code>Map</code>. Por isso a ordem das chaves de um objeto não afeta o cache, a ordem dos elementos de um vetor afeta, e uma propriedade cujo valor é <code>undefined</code> equivale, para o hash, a uma propriedade ausente. A maioria dos valores que o JSON não consegue representar vira outro valor sem nenhum erro, e chaves diferentes passam silenciosamente a ser a mesma. É possível trocar a função de hash com <code>queryKeyHashFn</code>, mas isso também descarta a ordenação de chaves, então é mais seguro converter os valores em serializáveis ao montar a chave. Filtros, como os de invalidação, por padrão não passam pelo hash e comparam a estrutura da queryKey a partir do início, então é melhor não esperar que chaves iguais pelo hash também sejam iguais em um filtro. No fim, a consulta ao cache compara o conteúdo da chave por meio de <code>hashKey</code> e os filtros comparam a parte inicial dela, então, se a queryKey tiver só valores simples cujo significado não muda ao serializar, as duas comparações se comportam como esperado.</p>
<p>Como esse critério se reflete na forma de escrever e gerenciar queryKeys, ou seja, o caminho dos vetores inline, passando pelas fábricas de chaves, até <code>queryOptions</code>, é o assunto de <a href="/260104" target="_blank" rel="noopener noreferrer">queryKey</a>.</p>
<p>Da próxima vez que você colocar um objeto ou um <code>Map</code> em uma queryKey, espero que pare um instante para pensar em qual string ele vai virar na serialização.</p>
<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/latest/docs/framework/react/reference/classes/QueryClient" target="_blank" rel="noopener noreferrer">TanStack Query, QueryClient</a></div>
</div>
</div></details>]]></content:encoded>
            <author>jihoon7705@gmail.com (이지훈)</author>
            <category>frontend</category>
            <category>React</category>
            <category>TanStack-Query</category>
            <category>queryKey</category>
        </item>
        <item>
            <title><![CDATA[Onde colocar o ErrorBoundary]]></title>
            <link>https://hooninedev.com/pt-BR/251203</link>
            <guid isPermaLink="false">https://hooninedev.com/pt-BR/251203</guid>
            <pubDate>Wed, 03 Dec 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Neste post quero falar sobre quantos ErrorBoundary ter e onde colocá-los. É para desenvolvedores frontend que montam telas com React Router e TanStack Query e vivem pesando se uma falha deve ser receb...]]></description>
            <content:encoded><![CDATA[<p>Neste post quero falar sobre <strong>quantos <code>ErrorBoundary</code> ter e onde colocá-los</strong>. É para desenvolvedores frontend que montam telas com React Router e TanStack Query e vivem pesando se uma falha deve ser recebida pelo <code>ErrorBoundary</code> da rota, pelo <code>react-error-boundary</code> ou pelo <code>useQuery</code> com seu <code>isError</code>. Ao final, você terá um critério para decidir onde vai cada <code>ErrorBoundary</code> e um jeito de ajustar o fallback e a condição de retry a cada lugar.</p>
<p>Adiantando a conclusão, o número de <code>ErrorBoundary</code> não é decidido por gosto, e sim por duas coisas: <strong>o que lança</strong> e <strong>o que precisa sobrar na tela quando isso morre</strong>. Este post começa pela segunda e trata da primeira onde for preciso.</p>
<h2 id="o-alcance-que-você-pode-ceder"><a class="anchor" href="#o-alcance-que-você-pode-ceder">O alcance que você pode ceder</a></h2>
<p>Quando alguém pergunta onde colocar um <code>ErrorBoundary</code>, costuma pensar em qual componente envolver. Essa pergunta não tem resposta. Sempre existem vários componentes que você poderia envolver, e o código roda qualquer que seja a escolha.</p>
<p>É preciso perguntar de outro jeito. <strong>Se isto morrer, o que precisa sobrar na tela?</strong></p>
<p>A resposta muda conforme o lugar. Se falta o dado que forma o esqueleto de uma tela, aquela tela não se sustenta. Um botão de ação sozinho, sem nome nem status ao lado, não significa nada. Ao contrário, se uma lista secundária falha e você cobre a tela inteira, o usuário perde tudo o mais que conseguiria ver sem problema. Ações difíceis de desfazer são outra coisa. É preciso avisar da falha no mesmo lugar em que se clicou.</p>
<p><strong>O que um <code>ErrorBoundary</code> faz não é capturar a falha, e sim decidir a área sobre a qual o fallback é desenhado.</strong> O fallback é o que você desenha no lugar da tela original quando algo falha. Some tudo o que você envolveu. Por isso a posição de um <code>ErrorBoundary</code> não é decidida pelo que você quer capturar, mas pelo <strong>alcance que você pode ceder</strong>.</p>
<p>Esse alcance se divide em quatro níveis.</p>
<table>
<thead>
<tr>
<th>Camada</th>
<th>O que recebe</th>
<th>Com o que se desfaz</th>
</tr>
</thead>
<tbody>
<tr>
<td>Rota</td>
<td>o que o loader lançou, o que ninguém capturou abaixo</td>
<td><code>revalidate()</code></td>
</tr>
<tr>
<td>Tela</td>
<td>o que foi lançado durante o render</td>
<td><code>resetErrorBoundary()</code></td>
</tr>
<tr>
<td>Região</td>
<td>o que foi lançado dentro dela</td>
<td><code>resetErrorBoundary()</code></td>
</tr>
<tr>
<td>Dentro do componente</td>
<td>um <code>useQuery</code> que falhou, uma mutation que falhou</td>
<td><code>refetch()</code>, um toast</td>
</tr>
</tbody>
</table>
<p><picture><source type="image/webp" srcset="/content/251203/1.720w.webp 720w, /content/251203/1.1440w.webp 1440w, /content/251203/1.1800w.webp 1800w" sizes="(max-width: 752px) calc(100vw - 32px), 720px"><img src="/content/251203/1.png" alt="Retângulos aninhados: um ErrorBoundary de rota contém um ErrorBoundary de tela, que por sua vez contém um ErrorBoundary de região e uma caixa de dentro do componente, lado a lado. Da esquerda entram setas: o que o loader lançou vai para o ErrorBoundary de rota, o que foi lançado durante o render vai para o ErrorBoundary de tela, a falha daquela região vai para o ErrorBoundary de região, e o que não lança vai para a caixa de dentro do componente. Uma seta que sai para cima da caixa de dentro do componente está riscada em vermelho e marcada como não subindo" style="max-width:min(720px, 100%);height:auto;" width="1800" height="1000" loading="eager" fetchpriority="high" decoding="async"></picture></p>
<p>São quatro nomes, mas só dois tipos. <strong>Só o <code>ErrorBoundary</code> de rota é do router; os outros dois são o <code>ErrorBoundary</code> que você coloca na árvore.</strong> O <code>ErrorBoundary</code> de tela e o <code>ErrorBoundary</code> de região são o mesmo componente e só muda onde está preso. A linha de baixo, dentro do componente, não é um <code>ErrorBoundary</code>, e sim um ramo que o próprio componente desenha.</p>
<p>As seções a seguir mostram por que você não pode apagar nenhuma linha desta tabela.</p>
<h2 id="três-camadas-que-você-não-pode-apagar"><a class="anchor" href="#três-camadas-que-você-não-pode-apagar">Três camadas que você não pode apagar</a></h2>
<p>Depois de instalar <code>react-error-boundary</code>, o lado do router parece apagável. Como os nomes coincidem, os trabalhos também parecem coincidir. Mas há três razões para não apagar nenhum deles. As três que conto aqui são rota, tela e região da tabela acima. Dentro do componente fica de fora porque não é um <code>ErrorBoundary</code>.</p>
<h3 id="o-loader-vive-fora-do-errorboundary"><a class="anchor" href="#o-loader-vive-fora-do-errorboundary">O loader vive fora do ErrorBoundary</a></h3>
<p>Um <code>ErrorBoundary</code> é uma classe construída com <code>getDerivedStateFromError</code> e <code>componentDidCatch</code>, então só chega até ele <strong>o que o React capturou dentro da árvore</strong>. O loader é uma função que roda fora da árvore, antes de o render começar. O que ele lança não passa pelo React, então por mais que você envolva, não aparece. Como conferi os destinos lançando o mesmo erro de vários lugares está registrado em <a href="/251117" target="_blank" rel="noopener noreferrer">Propagação de erros</a>.</p>
<p>Por isso, se ao menos uma rota usa loader, <strong>você não pode apagar o <code>ErrorBoundary</code> de rota.</strong> No momento em que apaga, aquela falha fica sem lugar para onde ir.</p>
<h3 id="o-cache-que-o-revalidate-não-consegue-limpar"><a class="anchor" href="#o-cache-que-o-revalidate-não-consegue-limpar">O cache que o revalidate não consegue limpar</a></h3>
<p>A direção contrária também está fechada. O mecanismo de recuperação do <code>ErrorBoundary</code> de rota é <code>revalidate()</code>, e isso só roda o loader de novo: não toca no cache de queries.</p>
<p>Pense em uma rota sem loader. Se um <code>useSuspenseQuery</code> dentro da tela falha, no fim o <code>ErrorBoundary</code> de rota até recebe. O router envolve a árvore de rotas em uma classe própria chamada <code>RenderErrorBoundary</code>, e essa classe também tem <code>getDerivedStateFromError</code>. Mas apertar tentar de novo naquele <code>ErrorBoundary</code> não adianta nada: <strong>não há loader para rodar de novo e o erro cravado no cache continua lá.</strong> Recebeu, mas não tem com o que desfazer.</p>
<p><strong>Receber e desfazer são trabalhos diferentes.</strong> Se você não olhar os dois ao posicionar um <code>ErrorBoundary</code>, acaba com um fallback que recebe coisas que ninguém consegue limpar.</p>
<h3 id="o-errorboundary-de-baixo-estreita-o-alcance"><a class="anchor" href="#o-errorboundary-de-baixo-estreita-o-alcance">O ErrorBoundary de baixo estreita o alcance</a></h3>
<p>O terceiro não é ficar bloqueado, é perder demais.</p>
<p>Quando algo falha, o router escolhe um <code>ErrorBoundary</code>. Como ele escolhe está no código.</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"> findNearestBoundary</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">matches</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">routeId</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"> eligibleMatches </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> routeId </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">?</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> matches.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">slice</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">, matches.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">findIndex</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">((</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">m</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"> m.route.id </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">===</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> routeId) </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:#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">matches];</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> eligibleMatches.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">reverse</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">m</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"> m.route.hasErrorBoundary </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 style="--shiki-dark:#F97583;--shiki-light:#D73A49">||</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> matches[</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>Ele percorre as rotas correspondentes de trás para frente, escolhe a primeira que tenha <code>ErrorBoundary</code> e, se não houver nenhuma, manda para a primeira de todas. Se o filho de uma rota aninhada não tem <code>ErrorBoundary</code>, esse papel fica com <strong>o pai</strong>, e se o pai também não tem, fica com a raiz.</p>
<p>Quando fica com a raiz, a tela inteira some. Só uma região interna falhou, mas o cabeçalho e a navegação vão junto. Prenda um <code>ErrorBoundary</code> na rota filha e o fallback é desenhado só no lugar do <code>&#x3C;Outlet /></code>.</p>
<p><strong>Por isso colocar mais um <code>ErrorBoundary</code> abaixo não é duplicar.</strong> Não é capturar a mesma falha duas vezes: é <strong>mudar até onde se apaga</strong>. Um arranjo que parece três limites sobrepostos é, na verdade, três coisas diferentes recebidas com três alcances diferentes.</p>
<h2 id="entre-o-errorboundary-de-região-e-dentro-do-componente"><a class="anchor" href="#entre-o-errorboundary-de-região-e-dentro-do-componente">Entre o ErrorBoundary de região e dentro do componente</a></h2>
<p>Depois de dividir em quatro camadas, sobra uma última bifurcação. Para uma região sem a qual a tela se sustenta, você escolhe entre <strong>subi-la para um <code>ErrorBoundary</code> ou receber ali mesmo</strong>.</p>
<p>De qualquer um dos dois jeitos você perde só aquela região e mantém o resto. O que muda não é quanto você perde, e sim <strong>o que você desenha ali no lugar</strong>.</p>
<p>Subir para um <code>ErrorBoundary</code> reduz o código. Se você chama <code>useSuspenseQuery</code> dentro, aquele componente não tem nem <code>isPending</code> nem <code>isError</code>. A espera fica com o <code>Suspense</code> de fora e a falha com o <code>ErrorBoundary</code> de fora. O componente desenha só o caso em que há dados.</p>
<p>O <code>QueryAsyncBoundary</code> abaixo é um componente que junta esse <code>Suspense</code> e esse <code>ErrorBoundary</code>. Dentro de <code>QueryErrorResetBoundary</code> ele coloca um <code>ErrorBoundary</code>, liga o seu <code>onReset</code> a <code>reset</code> e envolve o interior em <code>Suspense</code>. Colocar <code>Query</code> no nome é proposital. Se você chamar de <code>AsyncBoundary</code>, lê-se como se servisse para qualquer assincronia, e não serve, porque tem um <code>QueryErrorResetBoundary</code> dentro. Pela mesma razão não dei valor padrão a <code>pendingFallback</code>. Com um valor padrão, olhando só a linha da chamada você não sabe o que está sendo colocado embaixo.</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:#85E89D;--shiki-light:#22863A">section</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:#79B8FF;--shiki-light:#005CC5">QueryAsyncBoundary</span><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:#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:#79B8FF;--shiki-light:#005CC5">CommentList</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> postId</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{postId} /></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>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">section</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span></code></pre></figure>
<p>Em troca, aquela região inteira vira o fallback. Num lugar como uma lista, onde não sobra nada que valha guardar se ela some inteira, não há prejuízo.</p>
<p>Receber ali mesmo é o contrário. Você chama <code>useQuery</code> e desenha os quatro estados (espera, falha, resultado vazio, resultado) por conta própria. Os ramos se multiplicam, mas em troca você pode colocar <strong>textos e ações ajustados àquele lugar</strong>. Se quiser um botãozinho de tentar de novo só na linha que falhou, é por aqui. Se subir para um <code>ErrorBoundary</code>, o formato daquela linha passa a ser decidido pelo componente de fallback, que em geral é o compartilhado com o <code>ErrorBoundary</code> de tela e fica pesado demais para a falha de uma linha só.</p>
<p>O critério fica assim. <strong>Se a região pode sumir inteira, <code>ErrorBoundary</code> é melhor; se são necessários textos ou ações ajustados àquele lugar, <code>useQuery</code> é melhor.</strong> O <code>ErrorBoundary</code> tira os ramos do seu componente, mas leva junto a liberdade de decidir a tela.</p>
<p>Se você decidir não subir para um <code>ErrorBoundary</code>, <strong>precisa tratar os quatro estados.</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">isPending</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:#79B8FF;--shiki-light:#005CC5">refetch</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">isRefetching</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:#B392F0;--shiki-light:#6F42C1">commentsOptions</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(postId))</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"> (isPending) </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:#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>
<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"> (</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">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">{() </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> refetch</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">()} </span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">disabled</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{isRefetching}>재시도&#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:#F97583;--shiki-light:#D73A49">if</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (data.</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">length</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 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">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></code></pre></figure>
<p>Se esquecer o <code>isError</code>, a falha escorre em silêncio para o estado vazio. O <code>data</code> de uma query que falhou é <code>undefined</code>, e uma checagem como <code>data == null || data.length === 0</code> não distingue esse <code>undefined</code> de um array vazio. Na tela aparece que ainda não há comentários enquanto o servidor devolve um 500 naquele exato momento.</p>
<p>Se você filtrar <code>isError</code> primeiro, abaixo disso <code>data</code> se estreita para um array e a checagem <code>== null</code> desaparece.</p>
<h2 id="um-fallback-diferente-por-camada"><a class="anchor" href="#um-fallback-diferente-por-camada">Um fallback diferente por camada</a></h2>
<p>Com as camadas colocadas, é hora de decidir o que cada uma desenha na tela. Aqui o <code>ErrorBoundary</code> de rota e o <code>ErrorBoundary</code> que você coloca na árvore se separam de novo, e <strong>o critério de escolha não é gosto, e sim como aquela camada recebe o erro</strong>.</p>
<h3 id="o-errorboundary-de-rota-lê-sozinho"><a class="anchor" href="#o-errorboundary-de-rota-lê-sozinho">O ErrorBoundary de rota lê sozinho</a></h3>
<p>O <code>ErrorBoundary</code> de rota não precisa que o erro seja passado ao fallback. O componente lê direto com <code>useRouteError()</code> e busca o mecanismo de recuperação por conta própria.</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">export</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> function</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> RootErrorBoundary</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"> error</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> useRouteError</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">revalidate</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">state</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"> useRevalidator</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"> &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">ErrorFallback</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> error</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{error} </span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">onRetry</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{revalidate} </span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">retrying</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:#9ECBFF;--shiki-light:#032F62"> 'loading'</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>Então na rota basta encaixar o componente. O <code>ErrorBoundary</code> de uma rota filha tem o mesmo formato e usa os mesmos hooks. O que separa os dois não é o código, e sim <strong>a rota em que estão presos</strong>. Em qual rota foi preso é exatamente o alcance sobre o qual o fallback é desenhado.</p>
<h3 id="o-errorboundary-que-repassa"><a class="anchor" href="#o-errorboundary-que-repassa">O ErrorBoundary que repassa</a></h3>
<p>Em <code>react-error-boundary</code>, o <code>ErrorBoundary</code> é o contrário. Como ele mesmo segura o erro e a função de reset, precisa repassá-los ao fallback. Por isso existem três props, e a definição de tipos amarra as três como mutuamente exclusivas.</p>
<p><strong><code>fallback</code></strong> recebe um elemento pronto como está. Escreve-se assim: <code>fallback={&#x3C;p role="alert">댓글을 불러오지 못했어요&#x3C;/p>}</code>. Não te dá nem o erro nem a função de reset. Só serve quando a mensagem é fixa e também não há como tentar de novo.</p>
<p><strong><code>FallbackComponent</code></strong> recebe um componente e passa <code>error</code> e <code>resetErrorBoundary</code> como props. Fica limpo quando vários <code>ErrorBoundary</code> compartilham o mesmo fallback, mas o nome da prop é fixo como <code>resetErrorBoundary</code>, então quem recebe precisa conhecer esse nome.</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"> CommentsFallback</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">  return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">ErrorFallback</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> error</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{error} </span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">onRetry</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{resetErrorBoundary} /></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"> 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">{CommentsFallback}></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">CommentList</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> postId</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{postId} /></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 <code>reset</code> do código é a função que o <code>QueryErrorResetBoundary</code> do TanStack Query passa adiante. Ele limpa as queries que ficaram em estado de erro para que o tentar de novo refaça a requisição; por que isso é necessário eu sigo pelo código em <a href="/251128" target="_blank" rel="noopener noreferrer">Por que o botão de tentar de novo não funciona</a>.</p>
<p><code>ErrorFallback</code> recebe <code>onRetry</code>, então os nomes não batem. Isso custa mais um componente cujo único trabalho é mover o nome de um lado para o outro.</p>
<p><strong><code>fallbackRender</code></strong> recebe uma função e renderiza ali mesmo.</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">  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:#79B8FF;--shiki-light:#005CC5">ErrorFallback</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> error</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{error} </span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">onRetry</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{resetErrorBoundary} /></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>Eu escolhi este. A razão é <strong>que dá para trocar o nome ali mesmo</strong>. Acima, <code>resetErrorBoundary</code> virou <code>onRetry</code>. Graças a isso, <code>ErrorFallback</code> vira um componente que conhece apenas <code>error</code> e <code>onRetry</code>, e <code>revalidate</code> como mecanismo do <code>ErrorBoundary</code> de rota e <code>resetErrorBoundary</code> como mecanismo do <code>ErrorBoundary</code> da árvore: os dois <strong>usam o mesmo fallback.</strong></p>
<p><strong>Duas camadas cujas telas não se desalinham é o que essa escolha compra.</strong> Se você divide em quatro camadas, as telas de falha que o usuário vê correm o risco de virar quatro também, e trocar um nome uma vez faz delas uma só.</p>
<h2 id="falhas-em-que-não-se-coloca-tentar-de-novo"><a class="anchor" href="#falhas-em-que-não-se-coloca-tentar-de-novo">Falhas em que não se coloca tentar de novo</a></h2>
<p>Colocar um botão de tentar de novo no fallback não resolve toda falha, porque um <code>ErrorBoundary</code> só restaura o próprio estado. O erro de uma query só é requisitado de novo quando o <code>QueryErrorResetBoundary</code> liga seu <code>reset</code> ao <code>onReset</code>; o erro de render que surge ao ler um valor inesperado que o servidor mandou com um 200 só se resolve se <code>queryFn</code> o checar antes e transformá-lo no erro da query; e a falha ao carregar um chunk do <code>React.lazy</code> só se resolve recarregando. Como conferi os três casos no código está registrado à parte em "Por que o botão de tentar de novo não funciona", linkado acima. Como as falhas se dividem assim entre recuperáveis e não recuperáveis, o botão também precisa ser separado.</p>
<p>Mostrar o mesmo botão em toda falha é <strong>orientar o usuário a uma ação que ele não pode fazer</strong>. Apertar tentar de novo num 404 traz o mesmo 404. Um 403 por falta de permissão é igual. A falha ao carregar um chunk simplesmente não funciona, como vimos acima.</p>
<p>Basta separar uma vez dentro do fallback compartilhado.</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"> describe</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">error</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>
<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:#B392F0;--shiki-light:#6F42C1">isChunkLoadError</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(error)) {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">    return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { title: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'새 버전이 배포됐어요'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, description: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'새로 고침하면 이어서 볼 수 있어요.'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, action: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'reload'</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"> status</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">?</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:#B392F0;--shiki-light:#6F42C1"> isRouteErrorResponse</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(error) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">?</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"> undefined</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"> (status </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">===</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 404</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"> { title: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'찾을 수 없어요'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, description: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'주소가 바뀌었거나 삭제된 항목이에요.'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, action: </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:#F97583;--shiki-light:#D73A49">  if</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (status </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">!==</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> undefined</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> &#x26;&#x26;</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> status </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">>=</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 400</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> &#x26;&#x26;</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> status </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">&#x3C;</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:#F97583;--shiki-light:#D73A49">    return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { title: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'요청을 처리할 수 없어요'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, description: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'입력한 내용을 다시 확인해 주세요.'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, action: </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:#F97583;--shiki-light:#D73A49">  return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { title: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'불러오지 못했어요'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, description: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'잠시 후 다시 시도해 주세요.'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, action: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'retry'</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>Há uma razão para <code>isRouteErrorResponse</code> estar aqui também. Como o <code>ErrorBoundary</code> de rota e o <code>ErrorBoundary</code> da árvore compartilham o fallback, esta função <strong>recebe os dois tipos de erro.</strong> Ler o código de status de um <code>Response</code> que o router lançou e lê-lo de um erro HTTP que você mesmo construiu funcionam de formas diferentes, então ela olha os dois.</p>
<p>Não é preciso montar uma ramificação enorme desde o início. Basta separar <strong>as falhas que dão outro resultado ao serem apertadas de novo das que não dão</strong>.</p>
<h2 id="quando-o-errorboundary-vê-a-falha"><a class="anchor" href="#quando-o-errorboundary-vê-a-falha">Quando o ErrorBoundary vê a falha</a></h2>
<p>Todos os <code>ErrorBoundary</code> colocados, e ainda falta um. <strong>Quando o <code>ErrorBoundary</code> enxerga?</strong></p>
<p>As camadas foram decididas de cima para baixo, mas a ordem de execução é o contrário. O <code>ErrorBoundary</code> só vê alguma coisa depois que todas as tentativas se esgotaram. Por isso a condição de retry é <strong>parte do design do <code>ErrorBoundary</code></strong>.</p>
<p>O valor padrão muda conforme o lugar. Os dois usam o mesmo <code>createRetryer</code>, mas passam valores diferentes. A query não passa nada, então recebe o padrão de <code>retryer.js</code>.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="js" data-theme="github-dark github-light"><code data-language="js" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> retry</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> config.retry </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">isServer</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</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> :</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 3</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">);</span></span></code></pre></figure>
<p>A mutation coloca 0 diretamente em <code>mutation.js</code>.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="js" data-theme="github-dark github-light"><code data-language="js" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">retry</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">.options.retry </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><strong>A mutation em 0 significa que, assim que uma requisição termina em erro, aquela ação falha na hora.</strong> Não ter retry em ações difíceis de desfazer é um padrão seguro, mas se a ação for idempotente, mandar mais uma vez é melhor para o usuário.</p>
<p>É melhor declarar a mesma condição dos dois lados.</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:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> MAX_RETRY</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 2</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"> retryOnServerError</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">failureCount</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">error</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:#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">  if</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (failureCount </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">>=</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> MAX_RETRY</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">return</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> false</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  return</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">||</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>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p><code>failureCount</code> começa em 0 e <strong>pergunta com 0 na primeira falha.</strong> Então com <code>MAX_RETRY</code> em 2 ele manda três requisições no total e para. É a contagem de retries, não a de tentativas.</p>
<p><strong>Deixar os 4xx de fora é o ponto central.</strong> A requisição em si está errada, então por mais vezes que você mande volta a mesma resposta. Mas o motivo de deixá-los de fora não é só desperdício: o atraso padrão cresce de forma exponencial.</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"> defaultRetryDelay</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">failureCount</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"> Math.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">min</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">1e3</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> *</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 2</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> **</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> failureCount, </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">3e4</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 primeiro retry é 1 segundo e o seguinte é 2, então só dois retries já consomem 3 segundos. Se você não filtrar os 4xx, está <strong>escondendo por 3 segundos uma falha que não tem conserto</strong>.</p>
<p>Erros de render e falhas de chunk não têm esse tempo. Como nenhuma requisição sai, não há retry algum, e o <code>ErrorBoundary</code> vê no momento em que é lançado. <strong>É por isso que o mesmo fallback aparece 3 segundos depois em algumas falhas e na hora em outras.</strong></p>
<p>Há uma premissa a confirmar antes de ligar os retries. Aquela requisição é <strong>idempotente?</strong> Mandar a mesma requisição duas vezes tem que dar o mesmo resultado. Se o servidor não garante isso, retry é um recurso que fabrica bugs.</p>
<h2 id="encerrando"><a class="anchor" href="#encerrando">Encerrando</a></h2>
<p>Neste post decidi o que colocar em cada lugar que recebe uma falha. Resumindo:</p>
<ul>
<li>A posição de um <code>ErrorBoundary</code> é decidida pelo alcance que você pode ceder. Você pergunta não o que envolver, mas o que precisa sobrar quando isto morre.</li>
<li>Você não pode apagar nenhuma das três camadas de <code>ErrorBoundary</code>. O que o loader lançou não chega ao <code>ErrorBoundary</code> da árvore, <code>revalidate</code> não limpa o cache de queries, e colocar mais um abaixo é estreitar o alcance.</li>
<li>Se a região pode ser jogada fora inteira, <code>ErrorBoundary</code>; se são necessários textos próprios daquele lugar, <code>useQuery</code>. Escolhendo o segundo, você precisa tratar os quatro estados.</li>
<li>Trocando o nome com <code>fallbackRender</code>, o fallback fica um só mesmo com camadas diferentes.</li>
<li>Tentar de novo só funciona se você limpar o estado que quem lançou está segurando. A query é <code>reset</code>, o erro de render sobe para <code>queryFn</code>, e o chunk é um recarregamento.</li>
<li>Quando o <code>ErrorBoundary</code> vê a falha é decidido pela condição de retry. Se você não filtrar os 4xx, esconde por alguns segundos uma falha sem conserto.</li>
</ul>
<p>Dá para perguntar se um <code>ErrorBoundary</code> e um toast não bastam. Para um produto de duas ou três telas isso soa razoável e, de fato, <strong>o número de camadas é decidido pelo produto.</strong> Se há uma tela só, só há uma tela a ceder, então as diferenças de alcance não aparecem. Quanto mais regiões independentes houver dentro de uma tela, maior fica essa diferença.</p>
<p>Ainda assim, algo sobra independentemente do número. Por mais <code>ErrorBoundary</code> que você ponha na árvore, o que o <code>loader</code> lançou não vai para lá, e tentar de novo não funciona sem limpar o estado que quem lançou está segurando. <strong>O que dá para reduzir são as camadas, não esses fatos.</strong></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:#3b82f6"></span><a href="https://tanstack.com/query/latest/docs/framework/react/reference/QueryErrorResetBoundary" target="_blank" rel="noopener noreferrer">TanStack Query, QueryErrorResetBoundary</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#3b82f6"></span><a href="https://tanstack.com/query/latest/docs/framework/react/guides/suspense" target="_blank" rel="noopener noreferrer">TanStack Query, Suspense</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#3b82f6"></span><a href="https://reactrouter.com/how-to/error-boundary" target="_blank" rel="noopener noreferrer">React Router, Error Boundaries</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/bvaughn/react-error-boundary" target="_blank" rel="noopener noreferrer">bvaughn/react-error-boundary</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#f59e0b"></span><a href="https://tkdodo.eu/blog/react-query-error-handling" target="_blank" rel="noopener noreferrer">TkDodo, React Query Error Handling</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#f59e0b"></span><a href="https://tkdodo.eu/blog/mastering-mutations-in-react-query" target="_blank" rel="noopener noreferrer">TkDodo, Mastering Mutations in React Query</a></div>
</div>
</div></details>]]></content:encoded>
            <author>jihoon7705@gmail.com (이지훈)</author>
            <category>frontend</category>
            <category>React</category>
            <category>TanStack-Query</category>
            <category>tratamento-de-erros</category>
        </item>
        <item>
            <title><![CDATA[Por que o botão de tentar de novo não funciona]]></title>
            <link>https://hooninedev.com/pt-BR/251128</link>
            <guid isPermaLink="false">https://hooninedev.com/pt-BR/251128</guid>
            <pubDate>Fri, 28 Nov 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Neste post quero falar sobre por que o botão de tentar de novo de um ErrorBoundary não funciona. É para desenvolvedores frontend que colocaram um botão de tentar de novo no fallback do react-error-bou...]]></description>
            <content:encoded><![CDATA[<p>Neste post quero falar sobre <strong>por que o botão de tentar de novo de um <code>ErrorBoundary</code> não funciona</strong>.</p>
<p>É para desenvolvedores frontend que colocaram um botão de tentar de novo no fallback do <code>react-error-boundary</code> e, ao apertá-lo, veem a mesma tela voltar, e reúne os três casos em que o estado da falha permanece e como resolver cada um. A resposta curta: um <code>ErrorBoundary</code> só restaura o próprio estado, e o estado que causou a falha continua com quem lançou.</p>
<p>Os exemplos usam TanStack Query junto com <code>react-error-boundary</code>. Conferi o comportamento das bibliotecas abrindo o código instalado, e o código citado tem a mesma estrutura dos arquivos de build de <code>@tanstack/react-query</code> 5.104.1, <code>react-error-boundary</code> 6.1.6 e React 19.2.3 (comparado em 2026-10-08). A indentação e as quebras de linha foram ajustadas em alguns pontos para facilitar a leitura.</p>
<h2 id="três-casos-em-que-tentar-de-novo-não-funciona"><a class="anchor" href="#três-casos-em-que-tentar-de-novo-não-funciona">Três casos em que tentar de novo não funciona</a></h2>
<p>São três causas, e cada uma se resolve em um lugar diferente. Elas têm uma coisa em comum. <strong>Um <code>ErrorBoundary</code> só desfaz o próprio estado.</strong> O estado que quem lançou está segurando tem que ser limpo por quem lançou.</p>
<h3 id="o-erro-de-query-que-o-reset-limpa"><a class="anchor" href="#o-erro-de-query-que-o-reset-limpa">O erro de query que o reset limpa</a></h3>
<p>Tudo o que <code>resetErrorBoundary()</code> faz é voltar o estado interno do <code>ErrorBoundary</code> (<code>didCatch</code>). Quando os children remontam, o hook da query lê o cache de novo. Mas aquela query está <strong>cravada no cache em estado de erro.</strong> Então o hook lança o mesmo erro logo, durante o render, e o <code>ErrorBoundary</code> desenha o fallback de novo.</p>
<p>Por que ela usa o erro antigo em vez de refazer a requisição também está no código. <code>errorBoundaryUtils.js</code> desliga assim a nova requisição ao remontar.</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"> (options.suspense </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">||</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> throwOnError) {</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">errorResetBoundary.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">isReset</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">()) options.retryOnMount </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:#E1E4E8;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>É preciso ler primeiro a guarda de fora. <strong>Essa condição só pega nas queries que lançam.</strong> Se uma query com <code>suspense</code>, ou com <code>throwOnError</code> ligado, monta enquanto <code>isReset()</code> é falso, <code>retryOnMount</code> vira <code>false</code> e a nova requisição fica desligada. Um <code>useQuery</code> que não lança não se aplica e, ao remontar, simplesmente refaz a requisição.</p>
<p>Mas desligar <code>retryOnMount</code> só tem efeito em uma query que nunca teve dados no cache, ou seja, uma query que falhou no primeiro carregamento. Uma query que já tinha dados, ao falhar, fica com <code>isInvalidated</code> verdadeiro, é tratada como stale e refaz a requisição ao remontar por causa de <code>refetchOnMount</code>. Os erros de query de que este texto fala são falhas do primeiro carregamento.</p>
<p>Isso não quer dizer que a nova requisição fique desligada para sempre. Enquanto o fallback está na tela, nenhum componente observa aquela query, então ela vira uma query inativa e, pelos <a href="https://tanstack.com/query/latest/docs/framework/react/guides/important-defaults" target="_blank" rel="noopener noreferrer">padrões do TanStack Query</a>, é removida do cache depois de 5 minutos. Se o botão for clicado depois disso, não há erro no cache e a requisição é feita do zero. Por isso um código que esqueceu o <code>onReset</code> pode parecer funcionar quando se clica bem mais tarde. Se a reprodução está inconsistente, veja primeiro se o clique foi antes ou depois do <code>gcTime</code>.</p>
<p><picture><source type="image/webp" srcset="/content/251128/1.720w.webp 720w, /content/251128/1.1440w.webp 1440w, /content/251128/1.2080w.webp 2080w" sizes="(max-width: 752px) calc(100vw - 32px), 720px"><img src="/content/251128/1.png" alt="Em cima, o fluxo quando o onReset não está ligado: clique em tentar de novo, ErrorBoundary liberado, remontagem, lança de novo o erro do cache, e da última caixa uma seta vermelha volta para a primeira com a legenda o mesmo fallback. Embaixo, o fluxo quando o onReset está ligado: clique em tentar de novo, onReset e a chamada a reset(), ErrorBoundary liberado e remontagem, nova requisição, encadeados em uma só direção com setas azuis" style="max-width:min(720px, 100%);height:auto;" width="2080" height="800" loading="eager" fetchpriority="high" decoding="async"></picture></p>
<p><strong><code>retryOnMount</code> só é desligado na query subida para um <code>ErrorBoundary</code>, e por isso o <code>ErrorBoundary</code> e a query precisam ser limpos juntos.</strong> Quem levanta a flag que <code>isReset()</code> lê é <code>QueryErrorResetBoundary</code>. Abrindo o código, o estado é um único booleano.</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:#B392F0;--shiki-light:#6F42C1">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">	isReset </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></code></pre></figure>
<p>Se a flag só fosse levantada e ninguém a baixasse, os erros seguintes também não desligariam <code>retryOnMount</code> e cada um voltaria a requisitar. Quem a baixa é o hook da query. O hook remontado lê a flag durante o render e não desliga <code>retryOnMount</code>. <code>getHasError</code>, que olha a mesma flag, também não lança o erro do cache. Depois, já montado na tela, ele baixa a flag em um effect. É uma função do mesmo <code>errorBoundaryUtils.js</code>.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="js" data-theme="github-dark github-light"><code data-language="js" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> useClearResetErrorBoundary</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">errorResetBoundary</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">	React.</span><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:#E1E4E8;--shiki-light:#24292E">		errorResetBoundary.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">clearReset</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">	}, [errorResetBoundary]);</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">};</span></span></code></pre></figure>
<p>Assim, um único boolean basta para tentar de novo só desta vez. Passo a passo, uma nova tentativa acontece assim:</p>
<ol>
<li><code>reset()</code> levanta a flag <code>isReset</code>.</li>
<li>O hook da query remontado vê a flag durante o render e não desliga <code>retryOnMount</code>.</li>
<li>Com a flag levantada, o hook vê um estado de requisição em andamento em vez do erro do cache e requisita de novo (<code>useSuspenseQuery</code> envia a requisição durante o render e suspende).</li>
<li>Se a requisição der certo e o componente for montado na tela, <code>clearReset()</code> baixa a flag em um effect; se falhar de novo, quem a baixa é o <code>catch</code> dessa requisição.</li>
<li>Com a flag baixada, os erros seguintes voltam a desligar <code>retryOnMount</code>.</li>
</ol>
<p>Basta ligar este <code>reset</code> ao <code>ErrorBoundary</code>, no seu <code>onReset</code>. A documentação do TanStack Query e os comentários do código também trazem como exemplo um código que os liga 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">export</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> function</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> QueryAsyncBoundary</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:#FFAB70;--shiki-light:#E36209">pendingFallback</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"> 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>
<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:#79B8FF;--shiki-light:#005CC5">ErrorFallback</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> error</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{error} </span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">onRetry</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{resetErrorBoundary} /></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">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>É assim que <code>react-error-boundary</code> implementa <code>resetErrorBoundary</code>. É um arquivo compilado, então os nomes ficaram com uma letra só, mas a estrutura continua legível.</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:#B392F0;--shiki-light:#6F42C1">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">e) {</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">didCatch</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">t</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"> this</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">.state;</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  t </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">&#x26;&#x26;</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">.props.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">onReset</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">?.({ args: e, reason: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"imperative-api"</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">.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">setState</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(d));</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>Só quando <code>didCatch</code> é verdadeiro ele chama <code>onReset</code> e volta o estado para o valor inicial <code>d</code> (<code>didCatch: false</code>). <code>setState</code> é renderizado depois que o handler termina, então, quando os children remontam, a flag que <code>reset()</code> levantou já está de pé. Por isso basta ligar <code>onReset</code> a <code>reset</code>.</p>
<p>Mesmo sem envolver com <code>QueryErrorResetBoundary</code>, se você pegar de <code>useQueryErrorResetBoundary()</code> o <code>reset</code> e ligá-lo ao <code>onReset</code>, a nova tentativa funciona. Sem uma fronteira envolvendo, o hook devolve um valor padrão global do módulo. Em troca, como o <a href="https://tanstack.com/query/latest/docs/framework/react/guides/suspense" target="_blank" rel="noopener noreferrer">guia de Suspense</a> registra, o reset vale globalmente e o app inteiro compartilha uma única flag <code>isReset</code>.</p>
<h3 id="o-erro-de-render-que-o-reset-não-consegue-limpar"><a class="anchor" href="#o-erro-de-render-que-o-reset-não-consegue-limpar">O erro de render que o reset não consegue limpar</a></h3>
<p>O segundo é quando o servidor devolve um 200 com um formato diferente do esperado e o render que o lê lança um <code>TypeError</code>. O mesmo <code>ErrorBoundary</code> recebeu e o <code>onReset</code> está ligado, mas tentar de novo não funciona.</p>
<p>O que <code>reset</code> limpa é <strong>uma query em estado de erro</strong>. Mas essa query teve sucesso. O servidor deu um 200 e o cache guarda aquele valor como dado normal. Quem produziu o erro foi o render que leu esse valor. Então <code>reset</code> não tem nada a limpar, e o componente remontado lê o mesmo valor do cache durante o render e lança de novo na mesma linha. Como lança antes de ser montado na tela, nem chega a se inscrever na query. É por isso que não requisita de novo mesmo com <code>staleTime</code> em 0. Essa query também é removida do cache quando passa o <code>gcTime</code> e é requisitada de novo, mas, enquanto o servidor mandar o mesmo valor, ela lança de novo na mesma linha.</p>
<p>O lugar de corrigir é <strong><code>queryFn</code></strong>.</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:#B392F0;--shiki-light:#6F42C1">queryFn</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: </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">  const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> res</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/posts/${</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">postId</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:#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:#F97583;--shiki-light:#D73A49"> await</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:#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">Array.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">isArray</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(data.comments)) {</span></span>
<span data-line=""><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"> TypeError</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</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>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> data.comments</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">},</span></span></code></pre></figure>
<p>Numa resposta de <code>fetch</code>, <code>json()</code> devolve <code>Promise&#x3C;any></code>, então o tipo que você coloca depois é uma declaração, não uma checagem, e a checagem em tempo de execução que confirma o que o servidor mandou precisa ser colocada por você. <strong>Se você sobe essa checagem para <code>queryFn</code></strong>, a mesma falha vira <strong>o erro da query</strong>. Fica no cache em estado de erro, <code>reset</code> limpa e o tentar de novo refaz a requisição.</p>
<p>Se você adiar a checagem em tempo de execução porque o <code>ErrorBoundary</code> recebe de qualquer jeito, acaba com um fallback que recebe mas não consegue desfazer.</p>
<h3 id="o-lazy-que-só-um-recarregamento-resolve"><a class="anchor" href="#o-lazy-que-só-um-recarregamento-resolve">O lazy que só um recarregamento resolve</a></h3>
<p>O terceiro é uma falha ao carregar um chunk. Desta vez nem <code>reset</code> nem <code>queryFn</code> têm a ver. Quem segura o estado é o próprio <code>lazy</code>.</p>
<p>O <code>lazyInitializer</code> do React anota a rejeição no <code>payload</code> e, daí em diante, lança sempre a mesma coisa.</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">throw</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> payload._result;</span></span></code></pre></figure>
<p>Ele não faz <code>import()</code> de novo. A chamada de <code>lazy()</code> aconteceu uma vez no topo do módulo, e aquele <code>payload</code> fica como está enquanto o app viver. Mesmo soltando o <code>ErrorBoundary</code> e remontando, o mesmo erro volta.</p>
<p>E se criarmos um <code>lazy</code> novo e chamarmos <code>import()</code> de novo? Até agora o navegador impedia. O mapa de módulos lembrava o resultado com falha e não baixava a mesma URL de novo. Uma <a href="https://github.com/whatwg/html/pull/10327" target="_blank" rel="noopener noreferrer">mudança na especificação HTML</a> que altera isso foi mesclada em 2026-07-15. O estado por engine, conferido em 2026-10-08: o Firefox <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=2055211" target="_blank" rel="noopener noreferrer">incluiu na versão 155</a>, lançada em 2026-09-01. O WebKit <a href="https://bugs.webkit.org/show_bug.cgi?id=319492" target="_blank" rel="noopener noreferrer">colocou na main</a> em 2026-08-19, e o registro do bug não indica qual versão estável do Safari o inclui. O Chrome ainda está como Proposed no <a href="https://chromestatus.com/feature/5214647044145152" target="_blank" rel="noopener noreferrer">chromestatus</a>. Então, no Chrome, um <code>import()</code> novo devolve a mesma falha.</p>
<p>Isso vale para builds que carregam os chunks com o <code>import()</code> nativo do navegador, como o Vite. O runtime do webpack carrega os chunks com tags script e apaga o registro do chunk que falhou, então, se for chamado de novo por um <code>lazy</code> novo, ele requisita de novo.</p>
<p>De qualquer forma, um <code>lazy</code> já criado não tenta de novo, então a recuperação padrão dessa falha é baixar a página de novo. Mesmo quando os navegadores passarem a baixar de novo, nem tudo se resolve. Uma falha de carregamento de chunk pode vir de uma rede caída ou, como explica a <a href="https://vite.dev/guide/build#load-error-handling" target="_blank" rel="noopener noreferrer">documentação do Vite</a>, de um deploy novo que apagou os chunks antigos. Um chunk apagado continua sem existir quando requisitado de novo, então nesse caso a recuperação continua sendo recarregar. Como não dá para cravar uma única causa, é melhor o texto do fallback sugerir recarregar do que afirmar que saiu uma versão nova.</p>
<p>Resumindo, o erro de query se resolve com <code>reset</code>, o erro de render transformando-o antes em erro de query no <code>queryFn</code>, e a falha de chunk recarregando.</p>
<h2 id="encerrando"><a class="anchor" href="#encerrando">Encerrando</a></h2>
<p>Se um botão de tentar de novo não funciona, espero que você confira <strong>o que lançou e onde o estado dele continua guardado</strong> antes de olhar para o botão ou para o <code>ErrorBoundary</code>. Onde resolver depende de ser o cache da query, o render que leu o que o servidor mandou ou o <code>lazy</code>.</p>
<p>Onde colocar o <code>ErrorBoundary</code> na tela e quantos, e quais falhas nem deveriam ter botão de tentar de novo, é o tema de <a href="/251203" target="_blank" rel="noopener noreferrer">Onde colocar o ErrorBoundary</a>.</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:#3b82f6"></span><a href="https://tanstack.com/query/latest/docs/framework/react/reference/QueryErrorResetBoundary" target="_blank" rel="noopener noreferrer">TanStack Query, QueryErrorResetBoundary</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#10b981"></span><a href="https://github.com/bvaughn/react-error-boundary" target="_blank" rel="noopener noreferrer">bvaughn/react-error-boundary</a></div>
</div>
</div></details>]]></content:encoded>
            <author>jihoon7705@gmail.com (이지훈)</author>
            <category>frontend</category>
            <category>React</category>
            <category>TanStack-Query</category>
            <category>tratamento-de-erros</category>
        </item>
        <item>
            <title><![CDATA[Propagação 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 até onde um erro sobe no frontend. Desenhar um ErrorBoundary é fácil. Você põe um na rota, envolve cada tela em um ErrorBoundary e, no papel, não sobra nenhum buraco. O di...]]></description>
            <content:encoded><![CDATA[<p>Neste post quero falar sobre <strong>até onde um erro sobe no frontend</strong>.</p>
<p>Desenhar um <code>ErrorBoundary</code> é fácil. Você põe um na rota, envolve cada tela em um <code>ErrorBoundary</code> e, no papel, não sobra nenhum buraco. O difícil é saber o que aquele <code>ErrorBoundary</code> <strong>recebe de verdade</strong>.</p>
<p>A palavra "propagação" do título quer dizer <strong>até que ponto um erro lançado sobe e quem acaba recebendo</strong>. O lugar onde ele é lançado e o lugar onde é recebido nem sempre são o mesmo, e por isso é preciso contar os dois separadamente.</p>
<p>Então resolvi contar. Lancei o mesmo <code>new Error('boom')</code> sete vezes, mudando só o lugar, e observei se ele chegava ao <code>ErrorBoundary</code> ou se passava direto pelo <code>ErrorBoundary</code> e ia parar em outro lugar.</p>
<p>O widget abaixo é esse experimento. Se você escolher à esquerda o lugar de onde lançar, o erro é lançado de verdade dentro da caixa tracejada. Essa caixa é o <code>ErrorBoundary</code>, e quando o <code>ErrorBoundary</code> recebe, o interior vira um fallback. O que não é recebido parece que não aconteceu nada, então o widget também escuta em <code>window</code> os eventos <code>error</code> e <code>unhandledrejection</code> e escreve embaixo para onde foi.</p>
<div class="interactive-widget" data-widget="error-propagation"></div>
<p>Dos sete, só quatro chegam ao <code>ErrorBoundary</code>. Os outros três são lançados dentro do <code>ErrorBoundary</code> e mesmo assim atravessam esse mesmo <code>ErrorBoundary</code> e saem para o escopo global.</p>
<p>Neste post eu verifico por que essa divisão aparece, tipo de erro por tipo de erro: os que terminam em tempo de compilação, os que saem do render e do ciclo de vida, os que vêm dos dados do servidor, os que saem da navegação, os que as bibliotecas externas lançam e os que saem de eventos e código assíncrono. A ideia é descobrir para onde cada um propaga. <strong>Em quantas camadas dividir o lado que recebe, e como desfazer a falha, não são tratados aqui.</strong> Desenhar camadas sem conhecer as rotas de propagação deixa essas camadas vazias, e por isso a ordem é esta.</p>
<p>Verifiquei de duas formas. O comportamento das bibliotecas eu li abrindo as fontes instaladas em vez de confiar na memória, e tudo que dava para confirmar lançando eu rodei de verdade no widget acima.</p>
<h2 id="errorboundary"><a class="anchor" href="#errorboundary">ErrorBoundary</a></h2>
<p>Primeiro convém deixar claro o que é um <code>ErrorBoundary</code>. Um <code>ErrorBoundary</code> são <strong>dois métodos de ciclo de vida de um componente de classe</strong>. Só que a gente chama por outro nome.</p>
<p>Em <code>react-error-boundary</code> existem apenas duas implementações.</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">static </span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">getDerivedStateFromError</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(e) { </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">componentDidCatch</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(e, t) { </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">...</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> }</span></span></code></pre></figure>
<p>A documentação oficial do React separa o momento em que cada método roda. <code>getDerivedStateFromError</code> roda na <strong>fase de render</strong> e produz o estado com que o fallback é desenhado, enquanto <code>componentDidCatch</code> roda na <strong>fase de commit</strong> e cuida de efeitos colaterais como o log. Em qualquer um dos casos, eles só recebem <strong>o que o React capturou dentro da árvore e entregou</strong>.</p>
<p>Então a definição do que um <code>ErrorBoundary</code> recebe é uma só. <strong>Foi lançado de um lugar que o React consegue capturar?</strong></p>
<p>A documentação do React também deixa escrito o lado que não é recebido. São estes quatro.</p>
<blockquote class="bilingual-quote"><div class="quote-translation" lang="ko"><p>Manipuladores de eventos, renderização no servidor, erros lançados pelo próprio <code>ErrorBoundary</code> em vez de pelos seus filhos, código assíncrono (por exemplo, callbacks de <code>setTimeout</code> ou de <code>requestAnimationFrame</code>); há uma exceção: o Hook <code>useTransition</code> e a função <code>startTransition</code> que ele devolve. Erros lançados dentro dessa função de transição são capturados pelos error boundaries.</p></div><div class="quote-original" lang="en"><p>Event handlers, Server side rendering, Errors thrown in the error boundary itself (rather than its children), Asynchronous code (e.g. <code>setTimeout</code> or <code>requestAnimationFrame</code> callbacks); an exception is the usage of the <code>startTransition</code> function returned by the <code>useTransition</code> Hook. Errors thrown inside the transition function are caught by error boundaries</p></div></blockquote>
<p>O resultado do widget bate exatamente com essa frase. Sete lugares se dividem em dois destinos.</p>
<p><picture><source type="image/webp" srcset="/content/251117/1.720w.webp 720w, /content/251117/1.1440w.webp 1440w, /content/251117/1.1760w.webp 1760w" sizes="(max-width: 752px) calc(100vw - 32px), 720px"><img src="/content/251117/1.png" alt="À esquerda há sete lugares de lançamento na vertical e à direita dois destinos. Durante o render, dentro do useEffect, dentro do startTransition e a rejeição do import do lazy vão por setas azuis até ErrorBoundary, enquanto dentro de um manipulador onClick, dentro de um callback de setTimeout e a rejeição de uma Promise vão por setas cinzas até window" style="max-width:min(720px, 100%);height:auto;" width="1760" height="940" loading="eager" fetchpriority="high" decoding="async"></picture></p>
<p><code>startTransition</code> é a exceção porque o trabalho que está dentro dele passa pelo escalonador do React. O React envolve essa execução com as próprias mãos, então consegue capturá-la e devolvê-la para a árvore. Pelo mesmo motivo <code>setTimeout</code> não dá para capturar: quando aquele callback roda, o React já não está ali.</p>
<p>Antes é preciso saber uma coisa. <strong>Com um componente de função não dá para construir um <code>ErrorBoundary</code>.</strong></p>
<p>Quando o React encontra um erro lançado, ele sobe daquele ponto na direção do pai procurando o limite que vai recebê-lo. <code>throwException</code>, que cuida desse percurso, olha a <code>tag</code> presa em cada fiber, e os únicos valores em que ele para são componente de classe e raiz. A <code>tag</code> de um componente de função não é nenhum dos dois, então a busca passa direto. <strong>Ele não vira um limite não porque falte API, mas porque a busca nem sequer olha para ele.</strong></p>
<p><picture><source type="image/webp" srcset="/content/251117/2.720w.webp 720w, /content/251117/2.1440w.webp 1440w, /content/251117/2.2016w.webp 2016w" sizes="(max-width: 752px) calc(100vw - 32px), 720px"><img src="/content/251117/2.png" alt="Quatro caixas na horizontal. Da esquerda, os componentes de função Child e FnBoundary têm tag 0, o componente de classe ErrorBoundary tem tag 1 e a raiz HostRoot tem tag 3. Uma seta que sai de Child passa por FnBoundary e para em ErrorBoundary, e a linha que continua até HostRoot é tracejada" style="max-width:min(720px, 100%);height:auto;" width="2016" height="424" loading="lazy" fetchpriority="low" decoding="async"></picture></p>
<p>Essa <code>tag</code> é decidida por herdar ou não de <code>React.Component</code>. Por isso não adianta pendurar <code>getDerivedStateFromError</code> em uma função como <code>static</code>. Pendurei e rodei: aquela função não foi chamada nenhuma vez, o erro não achou limite, subiu até a raiz e o React tirou a árvore da tela.</p>
<p>Então instalar <code>react-error-boundary</code> não é acrescentar uma funcionalidade que não existe. O <code>ErrorBoundary</code> da 6.1.6 também é uma classe que herda de <code>Component</code> e carrega os mesmos dois métodos. O que a biblioteca faz é escrever essa classe uma vez só e escondê-la.</p>
<h2 id="o-que-termina-em-tempo-de-compilação"><a class="anchor" href="#o-que-termina-em-tempo-de-compilação">O que termina em tempo de compilação</a></h2>
<p>Já que estamos contando tipos de erro, vamos começar pelo primeiro deles: os erros de tipo.</p>
<p>Esse é o único que não chega ao usuário. Se você lê uma propriedade que não existe ou o tipo de um argumento não bate, o build trava, e código travado não é publicado. Por isso não há rota de propagação a seguir. <strong>Os erros de tipo ficam de fora deste post não porque não importem, mas porque não existem em tempo de execução.</strong></p>
<p>O problema vem depois. <strong>Até onde</strong> a checagem de tipos garante?</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:#F97583;--shiki-light:#D73A49">async</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> function</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> getComments</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">postId</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">Comment</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"> res</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/posts/${</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">postId</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:#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:#F97583;--shiki-light:#D73A49"> await</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:#F97583;--shiki-light:#D73A49">  return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> data.comments</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>O tipo de retorno diz <code>Promise&#x3C;Comment[]></code>, então todo código que chama essa função acredita que recebe um array. Mas <code>Response</code> declara seu <code>json()</code> assim no <code>lib.dom.d.ts</code> do TypeScript 5.9.3.</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:#B392F0;--shiki-light:#6F42C1">json</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(): </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Promise</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">&#x3C;</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">any</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span></code></pre></figure>
<p><code>any</code>. É aqui que a checagem de tipos se interrompe. O compilador nunca viu o valor que o servidor devolveu, e o argumento de tipo ou a anotação de retorno que você põe depois são <strong>uma declaração, não uma checagem</strong>. Ninguém insere por você o código que confere essa declaração em tempo de execução.</p>
<p>Então quando o servidor devolve <code>200</code> com <code>{ commits: null }</code>, a linha que lê <code>commits.length</code> lança um <code>TypeError</code> durante o render. O HTTP foi sucesso e os tipos passaram, e ainda assim a tela quebra.</p>
<p><strong>Onde os tipos terminam é onde uma checagem em tempo de execução deve ficar.</strong> Onde você coloca essa checagem decide por qual rota a mesma falha vai. Se estourar ao ler durante o render, vira um erro de render e vai para o <code>ErrorBoundary</code>; se você checar antes, no lugar onde os dados chegam, e lançar ali, vira a falha daquela requisição. A seção <strong>Render e ciclo de vida</strong> mais abaixo trata disso.</p>
<h2 id="render-e-ciclo-de-vida"><a class="anchor" href="#render-e-ciclo-de-vida">Render e ciclo de vida</a></h2>
<p>O que é lançado dentro da árvore do React propaga de forma simples. <strong>O <code>ErrorBoundary</code> mais próximo recebe.</strong></p>
<p>As exceções durante o render entram aqui. O <code>commits.length</code> de antes é uma, e chamar <code>map</code> em um valor que você achava ser um array é outra. Esse grupo não tem o que fazer além de lançar, então sempre chega a um <code>ErrorBoundary</code>.</p>
<p>O que é lançado dentro de <code>useEffect</code> também é capturado. O React roda os effects por conta própria depois do commit, então consegue envolver essa execução. Mas um <strong>callback assíncrono chamado dentro</strong> de um effect é diferente.</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">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">  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">'boom'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">)          </span><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// ErrorBoundary 가 받는다</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">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">  setTimeout</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">    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">'boom'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">)        </span><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// ErrorBoundary 를 지나친다</span></span>
<span data-line=""><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>Os dois trechos estão dentro do mesmo <code>useEffect</code> e ainda assim as rotas de propagação são diferentes. <strong>O critério não é estar dentro de um <code>ErrorBoundary</code>, e sim se o React está segurando aquela execução.</strong></p>
<p>Desse grupo basta lembrar de mais uma coisa. O console de desenvolvimento do React 19 põe o nome do componente no erro capturado. Ao rodar o widget, o render, o effect e a transition deram <code>The above error occurred in the &#x3C;Thrower> component</code>, e só <code>lazy</code> deu <code>occurred in one of your React components</code>. <code>lazy</code> ainda não tem componente no momento em que é rejeitado, então não consegue escrever um nome. Essa diferença vira uma pista quando você só tem a pilha para achar o lugar.</p>
<h2 id="dados-do-servidor"><a class="anchor" href="#dados-do-servidor">Dados do servidor</a></h2>
<p>É aqui que começa a parte chata que a gente precisa tratar desenvolvendo frontend. O motivo é que uma falha do servidor <strong>não vira erro automaticamente</strong>.</p>
<h3 id="fetch-não-rejeita-sozinho-diante-de-um-erro-do-servidor"><a class="anchor" href="#fetch-não-rejeita-sozinho-diante-de-um-erro-do-servidor">fetch não rejeita sozinho diante de um erro do servidor</a></h3>
<p>A MDN deixa isso escrito com clareza.</p>
<blockquote class="bilingual-quote"><div class="quote-translation" lang="ko"><p>A promise de <code>fetch()</code> só é rejeitada quando a própria requisição falha, por exemplo porque a URL está malformada ou porque houve um erro de rede. Ela não é rejeitada quando o servidor responde com um código de status HTTP que indica erro (<code>404</code>, <code>504</code>, etc.).</p></div><div class="quote-original" lang="en"><p>A <code>fetch()</code> promise only rejects when the request fails, for example, because of a badly-formed request URL or a network error. A <code>fetch()</code> promise does not reject if the server responds with HTTP status codes that indicate errors (<code>404</code>, <code>504</code>, etc.).</p></div></blockquote>
<p>Ou seja, se você usa só <code>fetch</code>, uma resposta <code>500</code> é uma <strong>Promise com sucesso</strong>. Como nada foi lançado, o <code>ErrorBoundary</code> não fica sabendo e a biblioteca de dados também não. A documentação do TanStack Query aponta o mesmo: para uma query ser considerada falha, o <code>queryFn</code> precisa lançar ou devolver uma Promise rejeitada, e enquanto o <code>axios</code> lança por conta própria, o <code>fetch</code> não faz isso.</p>
<p>Então transformar a falha do servidor em erro é <strong>um trabalho que você precisa fazer</strong>.</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:#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">'/todos/'</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> +</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> todoId)</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">response.ok) {</span></span>
<span data-line=""><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">'Network response was not ok'</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>Sem estas três linhas, o resto desta seção não significa nada. O que não é lançado não propaga para lugar nenhum.</p>
<h3 id="os-cinco-caminhos-por-onde-uma-falha-se-divide"><a class="anchor" href="#os-cinco-caminhos-por-onde-uma-falha-se-divide">Os cinco caminhos por onde uma falha se divide</a></h3>
<p>Depois que a falha virou falha, começa a próxima divisão. Um mesmo <code>500</code> se espalha para cinco lugares dependendo de <strong>como foi chamado</strong>. O critério são os valores padrão, sem mexer em nenhuma opção.</p>
<p><strong><code>useQuery</code> não lança o erro.</strong> Se você abrir <code>useQuery.js</code>, a string <code>throwOnError</code> não aparece em lugar nenhum. Se lança ou não, quem decide é <code>query-core</code> com seu <code>shouldThrowError</code>.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="js" data-theme="github-dark github-light"><code data-language="js" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">function</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> shouldThrowError</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">throwOnError</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">params</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">typeof</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> throwOnError </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">===</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> "function"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">return</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> throwOnError</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">params);</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">	return</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> !!</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">throwOnError;</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>Sem valor fica <code>!!undefined</code>, portanto <code>false</code>. Por isso a falha entra só em <code>query.error</code> e o componente renderiza normalmente. O <code>ErrorBoundary</code> de fora nunca fica sabendo que chegou a vez dele.</p>
<p><strong><code>useSuspenseQuery</code> lança, mas nem sempre.</strong> Esse hook espalha as opções e depois sobrescreve <code>throwOnError</code>.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="js" data-theme="github-dark github-light"><code data-language="js" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">return</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> useBaseQuery</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">options,</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  enabled: </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">  suspense: </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">  throwOnError: defaultThrowOnError,</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  placeholderData: </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">void</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 0</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}, QueryObserver, queryClient);</span></span></code></pre></figure>
<p>A sobrescrita vem depois de <code>...options</code>, então <strong>o <code>throwOnError</code> passado por quem chama é ignorado.</strong> E a decisão padrão que ocupa esse lugar está em uma linha só de <code>suspense.js</code>.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="js" data-theme="github-dark github-light"><code data-language="js" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> defaultThrowOnError</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">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"> query.state.data </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">===</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> void</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><strong>Se há cache para mostrar, não lança.</strong> Na prática o lugar onde esse desvio se separa é uma revalidação em segundo plano. Quem entra pela primeira vez tem o cache vazio, então a falha vai para o <code>ErrorBoundary</code> e vê o fallback. Quem já estava na tela, vai para outra aba e volta, dispara uma nova requisição, e se ela falhar o cache ainda guarda os dados antigos, então nada é lançado. A tela continua mostrando o valor velho e não muda sozinha.</p>
<p>A tela não quebrar costuma ser um bom comportamento. Só que junto vem <strong>a tela também não avisar</strong>, e escolher isso sabendo é diferente de levar sem saber.</p>
<p><strong>Nenhuma das duas funções que uma mutation devolve vai para o <code>ErrorBoundary</code>.</strong> Os motivos são diferentes. Se você abrir <code>useMutation.js</code>, um dos lados engole a rejeição diretamente.</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">observer.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">mutate</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(args[</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">0</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">], args[</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">catch</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(noop);</span></span></code></pre></figure>
<p>Isto é o <code>mutate</code>. No mesmo arquivo, <code>mutateAsync</code> expõe <code>result.mutate</code> como está, e esse <code>result.mutate</code> é o que <code>mutationObserver.js</code> colocou ali com <code>mutate: this.mutate</code>, então acaba sendo a mesma função que a linha acima envolveu. <strong>Só um dos lados passa por <code>.catch(noop)</code>.</strong> Aquela rejeição estoura no lugar onde se faz <code>await</code>, não é lançada durante o render, então esse lado também não tem nada a ver com o <code>ErrorBoundary</code>.</p>
<p><strong>Isso não quer dizer que uma mutation seja para sempre alheia ao <code>ErrorBoundary</code>.</strong> No corpo do hook há mais um interruptor.</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"> (result.error </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">&#x26;&#x26;</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> shouldThrowError</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(observer.options.throwOnError, [result.error])) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">throw</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> result.error;</span></span></code></pre></figure>
<p>Se você der <code>throwOnError</code>, esta linha lança <strong>durante o render</strong>, e aí sim vai para o <code>ErrorBoundary</code>. As duas funções devolvidas não chegarem ao <code>ErrorBoundary</code> e o hook não lançar são duas histórias diferentes.</p>
<p><picture><source type="image/webp" srcset="/content/251117/3.720w.webp 720w, /content/251117/3.1440w.webp 1440w, /content/251117/3.2080w.webp 2080w" sizes="(max-width: 752px) calc(100vw - 32px), 720px"><img src="/content/251117/3.png" alt="De um único 500 do servidor à esquerda saem cinco setas para useQuery, useSuspenseQuery, um hook com throwOnError ligado, mutate e mutateAsync, e cada uma segue até query.error, ErrorBoundary, ErrorBoundary, mutation.error e o catch de quem chama. Só as duas caixas centrais de ErrorBoundary estão ligadas por uma linha tracejada, marcadas como as duas que ErrorBoundary recebe" style="max-width:min(720px, 100%);height:auto;" width="2080" height="800" loading="lazy" fetchpriority="low" decoding="async"></picture></p>
<p>Resumindo, o mesmo <code>500</code> tem cinco destinos: o campo <code>query.error</code>, o campo <code>mutation.error</code>, o <code>catch</code> de quem chama e os dois casos que vão para o <code>ErrorBoundary</code>. Os dois que vão para o <code>ErrorBoundary</code> são <code>useSuspenseQuery</code> falhando sem cache e <code>throwOnError</code> ligado. <strong>Quem decide o destino é a forma de chamar, não o tipo de falha.</strong></p>
<h2 id="navegação"><a class="anchor" href="#navegação">Navegação</a></h2>
<p>As falhas que aparecem ao trocar de tela se dividem em duas. O critério é <strong>estar dentro ou fora da árvore do React</strong>.</p>
<h3 id="loader-roda-fora-da-árvore"><a class="anchor" href="#loader-roda-fora-da-árvore">loader roda fora da árvore</a></h3>
<p>O <code>loader</code> do router é uma função que roda antes de o render começar. Não é um componente React, então nem <code>getDerivedStateFromError</code> nem <code>componentDidCatch</code> chegam até ele. Por mais que você envolva tudo com <code>react-error-boundary</code>, aquele <code>ErrorBoundary</code> não consegue ver a falha de um loader.</p>
<p>Em vez disso o router mantém o próprio sistema de <code>ErrorBoundary</code>. A documentação do React Router escreve assim.</p>
<blockquote class="bilingual-quote"><div class="quote-translation" lang="ko"><p>Os route modules capturam automaticamente os erros do seu código e renderizam o <code>ErrorBoundary</code> mais próximo.</p></div><div class="quote-original" lang="en"><p>route modules will automatically catch errors in your code and render the closest <code>ErrorBoundary</code>.</p></div></blockquote>
<p>Como o mais próximo é escolhido está na fonte. <code>findNearestBoundary</code> escolhe assim.</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"> findNearestBoundary</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">matches</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">routeId</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"> eligibleMatches </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> routeId </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">?</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> matches.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">slice</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">, matches.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">findIndex</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">((</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">m</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"> m.route.id </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">===</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> routeId) </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:#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">matches];</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> eligibleMatches.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">reverse</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">m</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"> m.route.hasErrorBoundary </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 style="--shiki-dark:#F97583;--shiki-light:#D73A49">||</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> matches[</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>Ele percorre as rotas casadas de trás para frente, escolhe a primeira que tem um <code>ErrorBoundary</code> e, se não houver nenhuma, manda para a rota da frente. <strong>Por isso pôr mais um <code>ErrorBoundary</code> em uma rota inferior não é duplicar trabalho, e sim estreitar a área sobre a qual o fallback é desenhado.</strong></p>
<p>O lado que lê também é diferente. Um <code>ErrorBoundary</code> de rota não recebe o erro por props, ele tira direto com <code>useRouteError()</code>. Se veio ou não com um código de status se distingue com <code>isRouteErrorResponse(error)</code>. Essas duas ferramentas não existem no <code>ErrorBoundary</code> do React.</p>
<h3 id="a-rejeição-do-lazy-está-dentro-da-árvore"><a class="anchor" href="#a-rejeição-do-lazy-está-dentro-da-árvore">A rejeição do lazy está dentro da árvore</a></h3>
<p>Dentro da mesma navegação, o lado que recebe o código mais tarde funciona ao contrário. Quando <code>lazy(() => import('./Tab'))</code> tem seu <code>import()</code> rejeitado, o React pega isso e lança para o <strong><code>ErrorBoundary</code> mais próximo</strong>. O quarto botão do widget é essa rota.</p>
<p>Quando sai um deploy, os arquivos de chunk antigos somem, mas uma tela aberta antes do deploy continua com os endereços antigos. Se nesse estado ela pedir aquele código, o <code>import()</code> é rejeitado e o Chrome lança <code>TypeError: Failed to fetch dynamically imported module</code>.</p>
<p>Aqui entra mais uma coisa. <strong><code>lazy</code> lembra da rejeição.</strong> O <code>lazyInitializer</code> do React anota o resultado no <code>payload</code>, e se for rejeitado ele muda o estado e guarda o motivo.</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">payload._status </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</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">payload._result </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> error;</span></span></code></pre></figure>
<p>Daí em diante, toda vez que esse componente renderiza, o último desvio roda.</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">throw</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> payload._result;</span></span></code></pre></figure>
<p>Ele não faz <code>import()</code> de novo. A chamada de <code>lazy()</code> aconteceu uma vez só no topo do módulo e aquele <code>payload</code> fica ali enquanto a aplicação viver. <strong>Mesmo desfazendo o <code>ErrorBoundary</code> e remontando, o mesmo erro volta.</strong> É por isso que a única recuperação dessa falha é receber a página de novo.</p>
<p>Na mesma navegação, a falha do loader é recebida pelo router e a falha do <code>lazy</code> pelo React. <strong>Se você não separar esses dois antes de decidir onde pôr um <code>ErrorBoundary</code>, um deles fica sem lugar para ir.</strong></p>
<h2 id="manipuladores-globais-fora-do-errorboundary"><a class="anchor" href="#manipuladores-globais-fora-do-errorboundary">Manipuladores globais fora do ErrorBoundary</a></h2>
<p>Para onde foram os três que o <code>ErrorBoundary</code> não capturou? Para <strong>os manipuladores globais do navegador</strong>.</p>
<p>O que é lançado de um manipulador de eventos ou de um callback de <code>setTimeout</code> acaba em <code>window</code>, no evento <code>error</code> dele. A rejeição de uma Promise vai para outro evento. A definição da MDN é esta.</p>
<blockquote class="bilingual-quote"><div class="quote-translation" lang="ko"><p>O evento <code>unhandledrejection</code> é enviado ao escopo global de um script quando uma Promise de JavaScript sem manipulador de rejeição é rejeitada; normalmente esse escopo é o <code>window</code>, mas também pode ser um <code>Worker</code>.</p></div><div class="quote-original" lang="en"><p>The <code>unhandledrejection</code> event is sent to the global scope of a script when a JavaScript Promise that has no rejection handler is rejected; typically, this is the <code>window</code>, but may also be a <code>Worker</code>.</p></div></blockquote>
<p>Esses dois são o último lugar em que o navegador recebe algo. É também onde a maioria das ferramentas de monitoramento captura os erros do navegador.</p>
<p>O React 19 acrescentou mais um lugar de recepção do lado da árvore. É uma opção de <code>createRoot</code>. A documentação oficial separa os três assim.</p>
<table>
<thead>
<tr>
<th>Opção</th>
<th>Quando é chamada</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>onCaughtError</code></td>
<td>Quando o React capturou um erro dentro de um Error Boundary</td>
</tr>
<tr>
<td><code>onUncaughtError</code></td>
<td>Quando um erro foi lançado e nenhum Error Boundary capturou</td>
</tr>
<tr>
<td><code>onRecoverableError</code></td>
<td>Quando o React se recuperou sozinho</td>
</tr>
</tbody>
</table>
<p>Receber e consertar são coisas diferentes. <strong>Um manipulador global capturar não restaura a tela.</strong> Mesmo que um erro lançado dentro de <code>onClick</code> seja recebido por <code>window</code> e enviado para a ferramenta de monitoramento, naquele momento o usuário só vê um botão que não respondeu. Reportar e recuperar são trabalhos diferentes.</p>
<h2 id="para-terminar"><a class="anchor" href="#para-terminar">Para terminar</a></h2>
<p>Decorar o tratamento de erros no frontend como uma lista de ferramentas deixa buraco atrás de buraco. Acontece mesmo sabendo <code>ErrorBoundary</code>, <code>throwOnError</code>, <code>useRouteError</code>, <code>lazy</code> e <code>unhandledrejection</code>. Não é por não conhecer as ferramentas, e sim porque <strong>nunca se contou o que vai para onde</strong>.</p>
<p>O que este post tratou se resume assim.</p>
<ul>
<li>Um <code>ErrorBoundary</code> só recebe o que o React capturou e entregou. Manipuladores de eventos e callbacks assíncronos não estão naquele lugar.</li>
<li>A checagem de tipos termina na resposta. A partir do ponto em que <code>json()</code> devolve <code>any</code>, é declaração e não checagem.</li>
<li>Uma falha do servidor não vira erro sozinha. <code>fetch</code> não rejeita com <code>500</code>, então lançar é um trabalho que você faz.</li>
<li>Depois de lançado, a forma de chamar decide o destino. A mesma falha vai para um campo, para quem chama ou para um <code>ErrorBoundary</code>.</li>
<li>A navegação se divide em duas. O loader está fora da árvore e o router recebe; <code>lazy</code> está dentro da árvore e o React recebe.</li>
<li>Fora do <code>ErrorBoundary</code> existem os manipuladores globais. Eles capturam, mas a tela não se restaura.</li>
</ul>
<p>Então o que fazer antes de desenhar um <code>ErrorBoundary</code> não é escolher o componente a envolver. É <strong>escrever todos os lugares em que esta tela pode falhar e marcar a qual dos seis acima cada um pertence</strong>. O lugar sem marca é justamente o buraco.</p>
<p>Seria bom você descobrir quantos lugares podem falhar na sua tela agora, quantos deles chegam ao <code>ErrorBoundary</code> e para onde estão indo os que não chegam.</p>
<p>Com o que receber cada destino é o tema de <a href="/251203" target="_blank" rel="noopener noreferrer">Onde colocar o ErrorBoundary</a>: em quantas camadas dividir e como tratar a área de tela que uma falha leva. O que mais precisa ser solto para o botão de tentar de novo do fallback realmente tentar de novo fica para <a href="/251128" target="_blank" rel="noopener noreferrer">Por que o botão de tentar de novo não funciona</a>.</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:#3b82f6"></span><a href="https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary" target="_blank" rel="noopener noreferrer">React, o Error Boundary de Component</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/reference/react-dom/client/createRoot" target="_blank" rel="noopener noreferrer">React, os callbacks de erro de createRoot</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/reference/react/lazy" target="_blank" rel="noopener noreferrer">React, lazy</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#3b82f6"></span><a href="https://reactrouter.com/how-to/error-boundary" target="_blank" rel="noopener noreferrer">React Router, Error Boundaries</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#3b82f6"></span><a href="https://tanstack.com/query/latest/docs/framework/react/guides/query-functions" target="_blank" rel="noopener noreferrer">TanStack Query, Query Functions</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#3b82f6"></span><a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/unhandledrejection_event" target="_blank" rel="noopener noreferrer">MDN, unhandledrejection</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#3b82f6"></span><a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch" target="_blank" rel="noopener noreferrer">MDN, fetch</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#3b82f6"></span><a href="https://axios.rest/pages/advanced/error-handling" target="_blank" rel="noopener noreferrer">axios, Error handling</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/bvaughn/react-error-boundary" target="_blank" rel="noopener noreferrer">bvaughn/react-error-boundary</a></div>
</div>
</div></details>]]></content:encoded>
            <author>jihoon7705@gmail.com (이지훈)</author>
            <category>frontend</category>
            <category>React</category>
            <category>TanStack-Query</category>
            <category>tratamento-de-erros</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. É para desenvolvedores frontend que conhecem a definição de uma linha, "dividir o trabalho em unidades",...]]></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>É para desenvolvedores frontend que conhecem a definição de uma linha, "dividir o trabalho em unidades", mas têm dificuldade em explicar como o React a implementa por dentro. Ao final, você conseguirá explicar, com o código-fonte do React como base, por que o Fiber surgiu, como é um nó Fiber e como double buffering, Lanes e o Work Loop se encaixam para tornar as Concurrent Features possíveis.</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>
<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>Esse exemplo, porém, usa <code>requestIdleCallback</code> apenas para ilustrar o conceito; o React real não usa essa API. Ela só é chamada nos períodos ociosos que o navegador define, então, a meu ver, em uma página cuja main thread está ocupada, o trabalho do React pode continuar sendo adiado. Por isso, o pacote Scheduler do React agenda a próxima macrotask com <code>MessageChannel</code> para continuar o trabalho e, entre uma e outra, devolve o controle à main thread. Por que ele deixou <code>setTimeout</code> de lado em favor de <code>MessageChannel</code> está explicado à parte em <a href="/250515" target="_blank" rel="noopener noreferrer">Por que o React usa MessageChannel</a>.</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>cintilação (flicker)</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">// 동시성 렌더링: Scheduler가 양보하라고 할 때까지 처리</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">function</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> workLoopConcurrentByScheduler</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:#F97583;--shiki-light:#D73A49"> &#x26;&#x26;</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> !</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">shouldYield</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></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>workLoopConcurrentByScheduler</code> pergunta <code>shouldYield()</code> a cada Fiber processado e sai do loop quando o Scheduler responde que é hora de ceder.</p>
<p>O Scheduler responde verificando se já se passaram <strong>5 ms</strong> desde o início da tarefa atual. Esses 5 ms não são o tamanho de uma fatia de trabalho, e sim o limite para verificar se é hora de ceder, então, se processar um único Fiber levar mais que isso, esse trabalho não é dividido. O mesmo arquivo também tem <code>workLoopConcurrent</code>, que faz trabalho non-idle, como Transition, ceder a cada 25 ms. Mas essa função fica atrás da flag <code>enableThrottledScheduling</code>, e essa flag está desligada em todos os builds do v19.3.0.</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="as-filas-de-tarefas-do-scheduler"><a class="anchor" href="#as-filas-de-tarefas-do-scheduler">As filas de tarefas do Scheduler</a></h3>
<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>
<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"><picture><source type="image/webp" srcset="/content/250520/2.720w.webp 720w, /content/250520/2.1440w.webp 1440w, /content/250520/2.1680w.webp 1680w" sizes="(max-width: 840px) calc(100vw - 32px), 808px"><img src="/content/250520/2.png" alt="Demo interativa que percorre o React Fiber passo a passo, da fase de render à de layout, ao lado da árvore de componentes e da pilha de trabalho" width="1680" height="1202" loading="eager" fetchpriority="high" decoding="async"></picture></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>
<p><strong>Before Mutation Phase</strong>: <code>commitBeforeMutationEffects()</code></p>
<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>
<p><strong>Mutation Phase</strong>: <code>commitMutationEffects()</code></p>
<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>
<p><strong>Troca da árvore</strong>: <code>root.current = finishedWork</code></p>
<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>
<p><strong>Layout Phase</strong>: <code>commitLayoutEffects()</code></p>
<p>Depois que as mudanças no DOM terminam, são executados os trabalhos baseados no novo estado do DOM.</p>
<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>
<li>
<p><strong>Passive Effects</strong> (assíncronos)</p>
<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>
<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>frontend</category>
            <category>React</category>
        </item>
        <item>
            <title><![CDATA[Por que o React usa MessageChannel]]></title>
            <link>https://hooninedev.com/pt-BR/250515</link>
            <guid isPermaLink="false">https://hooninedev.com/pt-BR/250515</guid>
            <pubDate>Thu, 15 May 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Neste post, quero falar sobre por que o React agenda seu trabalho com MessageChannel em vez de requestIdleCallback. É para desenvolvedores frontend que, estudando Fiber, leram que o React "trabalha um...]]></description>
            <content:encoded><![CDATA[<p>Neste post, quero falar sobre <strong>por que o React agenda seu trabalho com MessageChannel em vez de requestIdleCallback</strong>.</p>
<p>É para desenvolvedores frontend que, estudando Fiber, leram que o React "trabalha um pouco sempre que o navegador está ocioso" e depois ficaram confusos ao encontrar <code>MessageChannel</code> no código-fonte real do React. Adiantando a resposta: <code>requestIdleCallback</code> não era chamado com a frequência de que o React precisava, e <code>setTimeout</code> adiciona um atraso de mais de 4 ms quando as chamadas são aninhadas. Por isso, o pacote Scheduler do React usa <code>MessageChannel</code>, que no navegador consegue agendar a próxima macrotask sem nenhum atraso artificial. O quanto esses 4 ms pesam de verdade, eu confiro com números medidos no Chrome headless.</p>
<h2 id="métodos-de-agendamento-abandonados"><a class="anchor" href="#métodos-de-agendamento-abandonados">Métodos de agendamento abandonados</a></h2>
<p>Ao explicar o conceito de Fiber, é comum usar código que divide o trabalho com <code>requestIdleCallback</code>. É um modelo em que uma unidade de trabalho é processada cada vez que o navegador não tem nada para fazer. O React também usou essa API no início, e passou por vários PRs até chegar à forma atual.</p>
<ul>
<li><strong>Janeiro de 2017</strong>: o React usava o <code>requestIdleCallback</code> nativo e, nos navegadores sem ele, um polyfill que o imitava com <code>requestAnimationFrame</code> e <code>postMessage</code> (<a href="https://github.com/facebook/react/pull/8833" target="_blank" rel="noopener noreferrer">PR #8833</a>). Navegadores sem essa API, como o Safari, ficaram a cargo do polyfill desde o início. A versão estável do Safari <a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback#browser_compatibility" target="_blank" rel="noopener noreferrer">continua sem essa API em outubro de 2026</a>.</li>
<li><strong>Março e abril de 2018</strong>: foi adicionada uma flag para usar o polyfill mesmo quando a API nativa existia. Andrew Clark escreveu na descrição do PR que a ideia era testar se o polyfill reduzia os problemas de starvation (trabalho que não consegue vez para rodar e é adiado repetidamente) que vinham observando, e acrescentou que era difícil ter certeza porque esses problemas eram difíceis de reproduzir (<a href="https://github.com/facebook/react/pull/12385" target="_blank" rel="noopener noreferrer">PR #12385</a>). Um mês depois, após concluírem internamente que ele funcionava melhor que a implementação nativa, a flag foi removida e o polyfill se tornou definitivo (<a href="https://github.com/facebook/react/pull/12648" target="_blank" rel="noopener noreferrer">PR #12648</a>).</li>
<li><strong>Novembro de 2018</strong>: o evento message que o polyfill enviava para <code>window</code> passou a ser enviado por um <code>MessageChannel</code>. Enviar para <code>window</code> faz todos os outros handlers de message da página também rodarem a cada frame (<a href="https://github.com/facebook/react/pull/14234" target="_blank" rel="noopener noreferrer">PR #14234</a>).</li>
<li><strong>Julho de 2019</strong>: em vez de adivinhar o próximo vsync e ceder no fim do frame, uma flag experimental introduziu um loop que trabalha 5 ms dentro de um evento message e depois cede (<a href="https://github.com/facebook/react/pull/16214" target="_blank" rel="noopener noreferrer">PR #16214</a>). Esse loop não usa <code>requestAnimationFrame</code> de forma alguma.</li>
<li><strong>Agosto e novembro de 2019</strong>: em agosto veio um relato de que os testes de desempenho mostravam melhor uso de CPU com o loop de mensagens (<a href="https://github.com/facebook/react/pull/16271" target="_blank" rel="noopener noreferrer">PR #16271</a>, não mesclado), e em novembro a implementação com rAF foi removida (<a href="https://github.com/facebook/react/pull/17252" target="_blank" rel="noopener noreferrer">PR #17252</a>).</li>
</ul>
<p>Por que a starvation acontece dá para deduzir da própria definição de <code>requestIdleCallback</code>. A <a href="https://w3c.github.io/requestidlecallback/" target="_blank" rel="noopener noreferrer">especificação do W3C</a> deixa para o navegador decidir o que é um período ocioso (idle period) e cita como um dos exemplos o tempo que sobra entre um frame e o seguinte. Durante uma animação esses períodos chegam com frequência, mas numa tela de 60 Hz costumam durar menos de 16 ms. Se a thread principal está ocupada com tarefas longas, até esses diminuem, e, a meu ver, o trabalho do React é adiado na mesma medida. Dan Abramov também escreveu num comentário de agosto de 2018 que o React parou de usar essa API porque "it's not as aggressive as we need" (<a href="https://github.com/facebook/react/issues/11171#issuecomment-417349573" target="_blank" rel="noopener noreferrer">facebook/react#11171</a>).</p>
<p>Os motivos para abandonar depois a abordagem com <code>requestAnimationFrame</code> estão na descrição do PR #16214. Essa abordagem precisava adivinhar quando viria o próximo vsync (o sinal ligado ao ciclo de atualização da tela). Ela começava supondo 30 fps, com duração de frame de 33,33 ms, e sempre que dois intervalos consecutivos entre frames eram ambos mais curtos que isso, reduzia a duração do frame para o maior dos dois. O prazo do frame atual era o horário de início do frame somado a essa duração (<a href="https://github.com/facebook/react/blob/6dc2734b41aef944e457eaa23ae218952fce0a54/packages/scheduler/src/forks/SchedulerHostConfig.default.js#L123-L336" target="_blank" rel="noopener noreferrer">SchedulerHostConfig.default.js logo antes do PR #17252</a>). Como sua única regra reduzia a duração, ela detectava a taxa de atualização subindo depois do carregamento da página, mas não descendo, como diz a descrição do PR.</p>
<p>O loop de mensagens cede a cada 5 ms onde quer que esteja no ciclo de vsync. A descrição do PR esperava que assim a thread principal continuasse responsiva mesmo em telas com taxa de atualização muito alta ("should keep the main thread responsive"). O mesmo PR também registrou o risco de que ceder com mais frequência aumentasse a disputa com outras tarefas do navegador, e que não se sabia o quanto os eventos message são limitados numa aba em segundo plano. O comentário atual no código do Scheduler diz que a maioria das tarefas não precisa se alinhar aos limites de frame.</p>
<h2 id="messagechannel"><a class="anchor" href="#messagechannel">MessageChannel</a></h2>
<p>O mecanismo de agendamento que restou depois que o rAF foi removido é o <strong>MessageChannel</strong>. Abaixo estão as <a href="https://github.com/facebook/react/blob/v19.3.0/packages/scheduler/src/forks/Scheduler.js#L530-L561" target="_blank" rel="noopener noreferrer">linhas 530 a 561 de Scheduler.js</a> no React v19.3.0, com os comentários reduzidos.</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">let</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> schedulePerformWorkUntilDeadline;</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">typeof</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> localSetImmediate </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">===</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> 'function'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) {</span></span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">  // Node.js and old IE.</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"> {</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">    localSetImmediate</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(performWorkUntilDeadline);</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:#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">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:#6A737D;--shiki-light:#6A737D">  // DOM and Worker environments.</span></span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">  // We prefer MessageChannel because of the 4ms setTimeout clamping.</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:#F97583;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> port</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> channel.port2;</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"> {</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    port.</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>
<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">  // We should only fallback here in non-browser environments.</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"> {</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">    localSetTimeout</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>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>São três ramos. Os navegadores atuais não têm <code>setImmediate</code>, então o segundo ramo, <code>MessageChannel</code>, é o escolhido. Segundo o comentário do código, o primeiro ramo existe para Node.js e o antigo IE, e <code>MessageChannel</code> impede que um processo Node.js termine, enquanto <code>setImmediate</code> não (<a href="https://github.com/facebook/react/issues/20756" target="_blank" rel="noopener noreferrer">facebook/react#20756</a>). Por isso o ambiente node do Jest vai pelo caminho de <code>setImmediate</code>. Desde o Jest 27, o ambiente jsdom teve <code>setImmediate</code> removido dos globais (<a href="https://github.com/jestjs/jest/pull/11222" target="_blank" rel="noopener noreferrer">jestjs/jest#11222</a>) e também não tem <code>MessageChannel</code>, então vai pelo caminho de <code>setTimeout</code>.</p>
<p>O objetivo de agendar assim a próxima vez é devolver a thread principal ao navegador. Enquanto o JavaScript segura a thread principal, o navegador não consegue tratar entradas nem pintar a tela. Mas só cedem no meio do caminho os renders fatiados no tempo, como os de Transition e Retry. O Reconciler do React v19.3.0 renderiza até o fim sem ceder se o render inclui uma blocking lane como Sync, InputContinuous ou Default, inclui uma lane que esperou tanto que expirou, ou foi chamado com <code>forceSync</code> (<a href="https://github.com/facebook/react/blob/v19.3.0/packages/react-reconciler/src/ReactFiberWorkLoop.js#L1168" target="_blank" rel="noopener noreferrer">linha 1168 de ReactFiberWorkLoop.js</a>).</p>
<p>Nesses renders, o Reconciler pergunta <code>shouldYield()</code> a cada Fiber processado (<a href="https://github.com/facebook/react/blob/v19.3.0/packages/react-reconciler/src/ReactFiberWorkLoop.js#L3073-L3078" target="_blank" rel="noopener noreferrer">linhas 3073 a 3078 de ReactFiberWorkLoop.js</a>). Essa função é exportada pelo Scheduler, e a decisão real fica com o <code>shouldYieldToHost()</code> dentro do Scheduler. O critério é o tempo desde o início da tarefa de mensagem atual. Quando esse tempo chega a <code>frameInterval</code>, ele cede, e se <code>requestPaint()</code> foi chamado depois de um commit, cede independentemente do tempo. O valor inicial de <code>frameInterval</code> é a constante de <code>SchedulerFeatureFlags.js</code> chamada <code>frameYieldMs</code>, ou seja, <strong>5 ms</strong> (<a href="https://github.com/facebook/react/blob/v19.3.0/packages/scheduler/src/SchedulerFeatureFlags.js#L11" target="_blank" rel="noopener noreferrer">linha 11</a>). Os 5 ms não são o tamanho de uma fatia de trabalho, e sim o limite de tempo para verificar se é hora de ceder. Se renderizar um único componente leva 20 ms, esses 20 ms não são divididos. Tudo isso descreve o build stable. Nos builds experimental, <code>enableAlwaysYieldScheduler</code> do mesmo arquivo está ativado, então o Scheduler cede ao navegador assim que termina uma tarefa, a menos que a próxima já tenha expirado, em vez de esgotar os 5 ms, e não considera o sinal de <code>requestPaint()</code>.</p>
<h2 id="o-atraso-de-4-ms-do-settimeout"><a class="anchor" href="#o-atraso-de-4-ms-do-settimeout">O atraso de 4 ms do setTimeout</a></h2>
<p>Por que não <code>setTimeout</code>, e sim <code>MessageChannel</code>, para agendar a próxima fatia depois de ceder? Os <a href="https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#timer-initialisation-steps" target="_blank" rel="noopener noreferrer">passos de inicialização de timers</a> da especificação HTML elevam para 4 ms o atraso de um <code>setTimeout</code> cujo nível de aninhamento passa de 5, se ele for menor que isso. Medindo no Chrome headless uma cadeia em que <code>setTimeout(fn, 0)</code> se reagenda, os intervalos da 1ª à 6ª chamada foram de 0 a 0,1 ms, e a partir da 7ª foram somados de 4 a 5 ms (em geral cerca de 5 ms). As mensagens de um <code>MessageChannel</code> não têm esse atraso mínimo. Uma mensagem apenas entra na fila de tarefas, então o navegador pode encaixar o tratamento de entradas ou a renderização no meio, e é exatamente essa brecha que o Scheduler busca.</p>
<p>4 ms parecem pouco, mas num loop que cede a cada 5 ms a história muda. Dividi 200 ms de trabalho em 40 fatias de 5 ms e agendei a próxima fatia com <code>MessageChannel</code> e com <code>setTimeout(work, 0)</code>. Basta colar no console do DevTools para rodar como está.</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"> busy</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">ms</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) { </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> s</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> performance.</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">while</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (performance.</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"> s </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">&#x3C;</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> ms) {} }</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> ch</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:#F97583;--shiki-light:#D73A49">let</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> done </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">, frames </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">const</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> raf</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"> { frames</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">if</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (done </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">&#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 200</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">requestAnimationFrame</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(raf); };</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> start</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> performance.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">now</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:#B392F0;--shiki-light:#6F42C1"> work</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:#B392F0;--shiki-light:#6F42C1">  busy</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">5</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">); done </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">  if</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (done </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">&#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 200</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">schedule</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  else</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> console.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">log</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(Math.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">round</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(performance.</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"> start), </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'ms,'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, frames, </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'frames'</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">ch.port1.onmessage </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> work;</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> schedule</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"> ch.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 style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// setTimeout(work, 0) 로 바꿔 비교</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">requestAnimationFrame</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(raf);</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">schedule</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">();</span></span></code></pre></figure>
<p>Estes são os resultados de rodar três variantes oito vezes cada em 2026-10-08, no Chrome 154.0.8037.98 (headless) em macOS. Safari e Firefox não foram medidos. A terceira linha da tabela troca <code>busy(5); done += 5;</code> por <code>busy(200); done += 200;</code> para não dividir o trabalho.</p>
<table>
<thead>
<tr>
<th>Como a próxima fatia é agendada</th>
<th>Tempo até terminar</th>
<th>Frames desenhados nesse meio-tempo</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>MessageChannel</code></td>
<td>201 ms</td>
<td>12 a 13</td>
</tr>
<tr>
<td><code>setTimeout(work, 0)</code></td>
<td>cerca de 365 a 371 ms</td>
<td>21 a 22</td>
</tr>
<tr>
<td>Sem dividir</td>
<td>200 ms</td>
<td>0</td>
</tr>
</tbody>
</table>
<p>Com <code>setTimeout</code>, depois das seis primeiras chamadas cada fatia ganhou de 4 a 5 ms de tempo ocioso, e o mesmo trabalho levou cerca de 1,8 vez mais tempo. Os números oscilam conforme a máquina e a carga, mas a proporção de cerca de 1,8 vez se manteve nas repetições. O número maior de frames é porque o trabalho terminou tarde e o intervalo medido ficou mais longo. Não significa responsividade melhor. Sem dividir, o trabalho termina em 200 ms, mas nenhum frame é desenhado nesse tempo. O <code>MessageChannel</code> terminou o trabalho nos mesmos 200 ms e continuou produzindo frames, em geral a cerca de 60 fps.</p>
<h2 id="conclusão"><a class="anchor" href="#conclusão">Conclusão</a></h2>
<p>Em resumo, o que o React precisava não era uma API que esperasse o navegador ficar ocioso, mas uma que permitisse trabalhar por pouco tempo e agendar imediatamente a próxima vez. <code>requestIdleCallback</code> não era chamado com a insistência de que o React precisava; a abordagem com <code>requestAnimationFrame</code> precisava adivinhar quando viria o vsync; e <code>setTimeout</code>, quando aninhado, fazia cada fatia descansar mais de 4 ms. <code>MessageChannel</code> foi o que atendeu a essas condições.</p>
<p>Como são os nós Fiber, as unidades de trabalho que esse Scheduler divide, e como o Work Loop os percorre é o tema de <a href="/250520" target="_blank" rel="noopener noreferrer">Dominando o React Fiber por completo</a>. Espero que, da próxima vez que você encontrar <code>MessageChannel</code> no código-fonte do React, lembre por um momento por que ele está ali.</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:#10b981"></span><a href="https://github.com/facebook/react/blob/v16.0.0/src/renderers/shared/ReactDOMFrameScheduling.js" target="_blank" rel="noopener noreferrer">ReactDOMFrameScheduling.js do React 16.0.0</a></div>
</div>
</div></details>]]></content:encoded>
            <author>jihoon7705@gmail.com (이지훈)</author>
            <category>frontend</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>
<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>
<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 35 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><picture><source type="image/webp" srcset="/content/241201/1.720w.webp 720w, /content/241201/1.1440w.webp 1440w, /content/241201/1.1680w.webp 1680w" sizes="(max-width: 840px) calc(100vw - 32px), 808px"><img src="/content/241201/1.png" alt="Comparativo de formatação do site do Biome: 0,41 s contra 14,35 s do Prettier em 171.127 linhas de 2.104 arquivos, cerca de 35 vezes mais rápido" width="1680" height="736" loading="eager" fetchpriority="high" decoding="async"></picture></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>
<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>
<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><picture><source type="image/webp" srcset="/content/241201/biome1.720w.webp 720w, /content/241201/biome1.798w.webp 798w" sizes="(max-width: 840px) calc(100vw - 32px), 808px"><img src="/content/241201/biome1.png" alt="Servidor de desenvolvimento do Vite com Biome, pronto em 506 ms" width="798" height="330" loading="lazy" fetchpriority="low" decoding="async"></picture>  <picture><source type="image/webp" srcset="/content/241201/lint1.720w.webp 720w, /content/241201/lint1.766w.webp 766w" sizes="(max-width: 840px) calc(100vw - 32px), 808px"><img src="/content/241201/lint1.png" alt="Servidor de desenvolvimento do Vite com ESLint e Prettier, pronto em 630 ms" width="766" height="248" loading="lazy" fetchpriority="low" decoding="async"></picture></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>
<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><picture><source type="image/webp" srcset="/content/241201/biome2.720w.webp 720w, /content/241201/biome2.920w.webp 920w" sizes="(max-width: 840px) calc(100vw - 32px), 808px"><img src="/content/241201/biome2.png" alt="Build do Vite com Biome: 117,13 segundos" width="920" height="222" loading="lazy" fetchpriority="low" decoding="async"></picture> <picture><source type="image/webp" srcset="/content/241201/lint2.720w.webp 720w, /content/241201/lint2.1022w.webp 1022w" sizes="(max-width: 840px) calc(100vw - 32px), 808px"><img src="/content/241201/lint2.png" alt="Build do Vite com ESLint e Prettier: 131,48 segundos" width="1022" height="228" loading="lazy" fetchpriority="low" decoding="async"></picture></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>
<h3 id="tarefa-de-linting"><a class="anchor" href="#tarefa-de-linting">Tarefa de linting</a></h3>
<p><picture><source type="image/webp" srcset="/content/241201/biome3.720w.webp 720w, /content/241201/biome3.894w.webp 894w" sizes="(max-width: 840px) calc(100vw - 32px), 808px"><img src="/content/241201/biome3.png" alt="Execução de lint com Biome: 0,79 segundo e 0,470 segundo de CPU" width="894" height="174" loading="lazy" fetchpriority="low" decoding="async"></picture> <picture><source type="image/webp" srcset="/content/241201/lint3.720w.webp 720w, /content/241201/lint3.974w.webp 974w" sizes="(max-width: 840px) calc(100vw - 32px), 808px"><img src="/content/241201/lint3.png" alt="Execução de lint com ESLint: 16,32 segundos e 8,600 segundos de CPU" width="974" height="186" loading="lazy" fetchpriority="low" decoding="async"></picture></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>
<p><picture><source type="image/webp" srcset="/content/241201/3.265w.webp 265w" sizes="(max-width: 840px) calc(100vw - 32px), 808px"><img src="/content/241201/3.jpeg" alt="O personagem Patrick com a mão no queixo, pensativo" width="265" height="190" loading="lazy" fetchpriority="low" decoding="async"></picture></p>
<p>Hmm... (A essa altura, é mais difícil encontrar um motivo para não usar.)</p>
<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>
<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="Uso médio de CPU com 10, 100 e 300 conexões simultâneas: o Node.js fica entre 89% e 91% e o Rust vai de 145% a 206% usando vários núcleos" width="1200" height="1000" loading="lazy" fetchpriority="low" decoding="async"></th>
<th><img src="/content/241201/6.webp" alt="Uso médio de memória nas mesmas condições: o Node.js consome 64-89 MB e o Rust 6-14 MB" 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>
<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>
<h3 id="processamento-paralelo-nativo"><a class="anchor" href="#processamento-paralelo-nativo">Processamento paralelo nativo</a></h3>
<p><picture><source type="image/webp" srcset="/content/241201/7.720w.webp 720w, /content/241201/7.800w.webp 800w" sizes="(max-width: 840px) calc(100vw - 32px), 808px"><img src="/content/241201/7.png" alt="Processamento síncrono versus assíncrono: o síncrono alterna requisição e resposta, o assíncrono envia requisições em sequência e recebe as respostas depois" width="800" height="514" loading="lazy" fetchpriority="low" decoding="async"></picture></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>
<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="Exemplo de árvore sintática: um -1 unário à esquerda e um 1 + 2 binário à direita, com nós de operador e de inteiro" width="446" height="360" 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>
<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>
<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>
<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><picture><source type="image/webp" srcset="/content/241201/8.720w.webp 720w, /content/241201/8.1440w.webp 1440w, /content/241201/8.1680w.webp 1680w" sizes="(max-width: 840px) calc(100vw - 32px), 808px"><img src="/content/241201/8.png" alt="Downloads semanais no npm trends ao longo de um ano: eslint e prettier entre 30 e 50 milhões, enquanto o biome fica colado no eixo inferior" width="1680" height="854" loading="lazy" fetchpriority="low" decoding="async"></picture></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>
<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>]]></content:encoded>
            <author>jihoon7705@gmail.com (이지훈)</author>
            <category>frontend</category>
            <category>JavaScript</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>
<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><picture><source type="image/webp" srcset="/content/240818/3.720w.webp 720w, /content/240818/3.880w.webp 880w" sizes="(max-width: 840px) calc(100vw - 32px), 808px"><img src="/content/240818/3.png" alt="Diagrama de fluxo de dados no React: os dados descem por props e os eventos sobem" width="880" height="509" loading="eager" fetchpriority="high" decoding="async"></picture></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>
<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>
<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><picture><source type="image/webp" srcset="/content/240818/4.720w.webp 720w, /content/240818/4.1300w.webp 1300w" sizes="(max-width: 840px) calc(100vw - 32px), 808px"><img src="/content/240818/4.png" alt="Diagrama do padrão Flux: Action, Dispatcher, Store e View ligados em uma única direção" width="1300" height="393" loading="lazy" fetchpriority="low" decoding="async"></picture></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> 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>
<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>
<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>
<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>
<h4 id="encapsulamento-do-estado-por-closure"><a class="anchor" href="#encapsulamento-do-estado-por-closure">Encapsulamento do estado por closure</a></h4>
<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>
<h4 id="detecção-de-mudanças-com-objectis"><a class="anchor" href="#detecção-de-mudanças-com-objectis">Detecção de mudanças com <code>Object.is</code></a></h4>
<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>
<h4 id="sistema-de-listeners-com-o-padrão-pubsub"><a class="anchor" href="#sistema-de-listeners-com-o-padrão-pubsub">Sistema de listeners com o padrão Pub/Sub</a></h4>
<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>
<h4 id="criação-do-estado-inicial"><a class="anchor" href="#criação-do-estado-inicial">Criação do estado inicial</a></h4>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<p><picture><source type="image/webp" srcset="/content/240818/7.216w.webp 216w" sizes="(max-width: 840px) calc(100vw - 32px), 808px"><img src="/content/240818/7.jpeg" alt="Um personagem desenhado à mão dizendo &#x22;espera aí!&#x22;" width="216" height="233" loading="lazy" fetchpriority="low" decoding="async"></picture></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>
<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>frontend</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. Este artigo é para desenvolvedores que enviam artefatos de build grandes para um servidor ou armazenamento e estão em dúvida entre...]]></description>
            <content:encoded><![CDATA[<p>Neste artigo, quero falar sobre algoritmos de compressão de software.</p>
<p>Este artigo é para desenvolvedores que enviam artefatos de build grandes para um servidor ou armazenamento e estão em dúvida entre zip, tar.gz e tar.zst. Ao final, você saberá onde os principais formatos diferem em velocidade e taxa de compressão e terá critérios para escolher entre tar.gz e tar.zst.</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>Antes da comparação, vale destacar uma raiz comum. Quase todos os compressores mais usados hoje descendem do LZ77, publicado em 1977. O LZ77 usa como dicionário um trecho dos dados já processados, a janela deslizante, e substitui padrões repetidos por uma referência curta que significa “volte tantos caracteres e copie tantos”. Uma janela maior captura repetições mais distantes, mas exige mais processamento e memória. O DEFLATE, usado por ZIP e GZIP, acrescenta a isso a codificação de Huffman (uma codificação de entropia que atribui sequências de bits mais curtas aos símbolos mais frequentes). Como o LZ77 se separou do irmão LZ78 está explicado à parte em <a href="/240701" target="_blank" rel="noopener noreferrer">A diferença entre LZ77 e LZ78</a>.</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>
<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>. Essa estrutura, em que cada arquivo é comprimido separadamente, é chamada de arquivo não sólido (Non-solid Archive). A abordagem oposta, que junta todos os arquivos em um único fluxo e o comprime de uma vez, é o arquivo sólido. Graças à estrutura não sólida, é possível 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, é a escolha mais segura quando a compatibilidade entre plataformas é importante.</p>
<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>
<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 da codificação de Huffman do GZIP, usa <strong>FSE (Finite State Entropy)</strong>, um codificador baseado em ANS (Asymmetric Numeral Systems). O ANS atualiza um único estado inteiro a cada símbolo processado e emite menos bits quanto mais frequente for o símbolo. O objetivo é alcançar uma taxa de compressão próxima à da codificação aritmética (Arithmetic Coding) com velocidade próxima à de Huffman. 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 cerca de três vezes mais rápido, e ainda assim sua taxa é superior: 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 como formato padrão de compressão de 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>
<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 não está em desenvolvimento ativo. Conforme benchmarks mostram que o ZSTD supera o BZIP2 em taxa e velocidade, projetos novos tendem a escolher ZSTD.</p>
<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>
<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; na verdade, cabeçalhos e padding deixam o resultado um pouco maior que o original. 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>
<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>
<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>
<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>Adicionei esta seção em 2026. 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, o tempo real fica ainda menor. Em uma pipeline de CI/CD, essa diferença se acumula a cada deploy, então não é um número desprezível.</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>
<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>
<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>curiosidades</category>
            <category>software</category>
        </item>
        <item>
            <title><![CDATA[A diferença entre LZ77 e LZ78]]></title>
            <link>https://hooninedev.com/pt-BR/240701</link>
            <guid isPermaLink="false">https://hooninedev.com/pt-BR/240701</guid>
            <pubDate>Mon, 01 Jul 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[Neste artigo, quero falar sobre como LZ77 e LZ78 diferem. Este artigo é para desenvolvedores que usam ferramentas como zip, gzip e zstd e já se perguntaram como funciona a compressão baseada em dicion...]]></description>
            <content:encoded><![CDATA[<p>Neste artigo, quero falar sobre <strong>como LZ77 e LZ78 diferem</strong>.</p>
<p>Este artigo é para desenvolvedores que usam ferramentas como zip, gzip e zstd e já se perguntaram como funciona a compressão baseada em dicionário dentro delas. Ao final, você conseguirá explicar como os dois algoritmos diferem no tratamento do dicionário e de qual dos dois descendem os compressores mais usados hoje.</p>
<p>Ao comparar formatos para compactar artefatos de build, cedo ou tarde se acaba voltando a esses dois algoritmos.</p>
<h2 id="compressão-baseada-em-dicionário"><a class="anchor" href="#compressão-baseada-em-dicionário">Compressão baseada em dicionário</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 família muito usada. Aqui, dicionário é o mecanismo que permite apontar de novo para dados já vistos. Se esse mecanismo são os próprios dados já percorridos ou uma lista construída à parte é o que separa o LZ77 do LZ78. O <strong>LZ77</strong>, proposto por Jacob Ziv e Abraham Lempel em um <a href="https://doi.org/10.1109/TIT.1977.1055714" target="_blank" rel="noopener noreferrer">artigo</a> de 1977 na IEEE Transactions on Information Theory, e o <strong>LZ78</strong>, do <a href="https://doi.org/10.1109/TIT.1978.1055934" target="_blank" rel="noopener noreferrer">artigo que os dois publicaram no ano seguinte</a>, são os ancestrais dessa família. As letras “LZ” vêm dos sobrenomes dos pesquisadores. Os algoritmos posteriores baseados em dicionário, como DEFLATE, LZMA, LZ4 e Zstd, têm suas raízes nesses dois.</p>
<p>Um exemplo simples ajuda. Se a palavra “Linux” se repetir cem vezes, a partir da segunda ocorrência o compressor escreve, no lugar do texto original, uma referência curta que aponta para “aquele trecho visto antes”. Se a referência for mais curta que o original, o tamanho total diminui.</p>
<p>Então, qual é exatamente a diferença entre LZ77 e LZ78?</p>
<h2 id="lz77-a-abordagem-da-janela-deslizante"><a class="anchor" href="#lz77-a-abordagem-da-janela-deslizante">LZ77: a abordagem da janela deslizante</a></h2>
<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 o tanto que já foi processado.</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. O artigo original registrava o primeiro valor como uma posição dentro do buffer; as implementações atuais registram a mesma informação como uma distância.</p>
<p>Por exemplo, ao compactar a string <code>"banana_banana"</code> com o codificador didático incluído mais adiante neste artigo, saem cinco tokens LZ77: <code>(0,0,b)</code> <code>(0,0,a)</code> <code>(0,0,n)</code> <code>(2,3,_)</code> <code>(7,5,a)</code>. Os três primeiros são caracteres vistos pela primeira vez, então carregam apenas o próximo caractere, sem correspondência. O segundo <code>"banana"</code> é resolvido por um único token, o último, <code>(7,5,a)</code>. Ele significa “volte sete caracteres, copie cinco e acrescente <code>a</code>”. Ele não copia os seis caracteres porque este codificador sempre deixa o último caractere da entrada na posição de próximo caractere.</p>
<p>Em <code>(2,3,_)</code>, o comprimento 3 é maior que a distância 2. Com <code>ban</code> já escrito, o decodificador começa a copiar a partir do <code>a</code> dois caracteres atrás e, ao copiar o terceiro caractere, lê de novo o <code>a</code> que acabou de escrever. Assim sai <code>ana</code>, e depois <code>_</code> é acrescentado. Acompanhando caractere por caractere, fica assim.</p>
<table>
<thead>
<tr>
<th>Etapa</th>
<th>Caractere lido</th>
<th>Saída</th>
</tr>
</thead>
<tbody>
<tr>
<td>Início</td>
<td>nenhum</td>
<td><code>ban</code></td>
</tr>
<tr>
<td>Cópia 1</td>
<td>2º caractere, <code>a</code></td>
<td><code>bana</code></td>
</tr>
<tr>
<td>Cópia 2</td>
<td>3º caractere, <code>n</code></td>
<td><code>banan</code></td>
</tr>
<tr>
<td>Cópia 3</td>
<td>4º caractere, <code>a</code> (escrito na cópia 1)</td>
<td><code>banana</code></td>
</tr>
<tr>
<td>Próximo caractere</td>
<td><code>_</code> do token</td>
<td><code>banana_</code></td>
</tr>
</tbody>
</table>
<p>A <a href="https://www.rfc-editor.org/rfc/rfc1951" target="_blank" rel="noopener noreferrer">RFC 1951</a> especifica o mesmo comportamento: se os dois últimos bytes forem X e Y, <code>&#x3C;length = 5, distance = 2></code> acrescenta X,Y,X,Y,X. Isso funciona porque o dicionário é justamente o dado recém-restaurado.</p>
<p>Nessa abordagem, <strong>o dicionário não é armazenado nem transmitido separadamente.</strong> O decodificador reconstrói sozinho o buffer de busca durante a descompressão, de modo que o dicionário fica implícito nos próprios dados. Como as referências apontam para dados anteriores, a descompressão, em princípio, avança em ordem desde o início. No entanto, as referências só alcançam até onde vai a janela. A RFC 1951 limita as referências do DEFLATE a no máximo 32K bytes para trás. O <code>Z_FULL_FLUSH</code> do zlib reinicia o estado de compressão para que a descompressão possa recomeçar a partir desse ponto se os dados comprimidos anteriores estiverem danificados ou se for preciso acesso aleatório. O <a href="https://github.com/madler/zlib/blob/v1.3.1/zlib.h" target="_blank" rel="noopener noreferrer">zlib.h</a> avisa que usá-lo com muita frequência pode degradar seriamente a compressão.</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 comprime melhor, mas usa mais memória e, com mais candidatos a verificar, a busca de correspondências costuma ficar mais lenta.</p>
<h2 id="lz78-um-dicionário-explícito"><a class="anchor" href="#lz78-um-dicionário-explícito">LZ78: um dicionário explícito</a></h2>
<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 <strong>“a entrada encontrada mais o novo caractere”</strong> ao dicionário. Assim, o dicionário cresce aos poucos durante o processamento.</p>
<p>Ao compactar o mesmo <code>"banana_banana"</code> com LZ78, saem oito tokens e o dicionário acumula sete entradas. Os tokens são <code>(0,b)</code> <code>(0,a)</code> <code>(0,n)</code> <code>(2,n)</code> <code>(2,_)</code> <code>(1,a)</code> <code>(3,a)</code> <code>(7,)</code>, e o dicionário é <code>1:b</code> <code>2:a</code> <code>3:n</code> <code>4:an</code> <code>5:a_</code> <code>6:ba</code> <code>7:na</code>. O segundo <code>"banana"</code> se divide em três tokens que produzem <code>ba</code>, <code>na</code> e <code>na</code>. É a parte que o LZ77 resolveu com um único token. Uma entrada do dicionário do LZ78 cresce apenas um caractere por vez, então uma entrada longa só surge depois de ver a mesma palavra várias vezes. O último token, <code>(7,)</code>, é o caso em que a entrada termina e não há próximo caractere. Este codificador não empacota os tokens em bits, então o número de tokens não serve para comparar taxas de compressão.</p>
<p>As duas sequências de tokens podem ser reproduzidas com o código abaixo. É o resultado de salvá-lo em um arquivo e rodar <code>node lz.mjs</code> em 2026-10-08 no macOS 26.6.2 com Node v24.16.0.</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">// LZ77: (거리, 길이, 다음 문자). 가장 긴 일치를 고르고, 겹친 복사를 허용한다</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">function</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> lz77</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">s</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">win</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 4096</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"> out</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"> i </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 0</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  while</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"> s.</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">length</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"> best </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { d: </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">0</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, l: </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">    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"> j </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">, i </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">-</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> win); j </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">&#x3C;</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> i; j</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"> l </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 0</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">      while</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"> l </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">&#x3C;</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> s.</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">length</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> -</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 1</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> &#x26;&#x26;</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> s[j </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">+</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> l] </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">===</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> s[i </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">+</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> l]) l</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">++</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">      if</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (l </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> best.l) best </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { d: i </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">-</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> j, l }</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">    out.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">push</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">([best.d, best.l, s[i </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">+</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> best.l]])</span></span>
<span data-line=""><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"> best.l </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">+</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 1</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"> out</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">// LZ78: (사전 인덱스, 다음 문자). 인덱스 0 은 빈 문자열이다</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">function</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> lz78</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">s</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"> dict</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:#F97583;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> out</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"> w </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> ''</span></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"> s.</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">    const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> c</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> s[i]</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">    if</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (dict.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">has</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(w </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">+</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> c)) {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">      if</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"> s.</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">length</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">) { w </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">+=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> c; </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">continue</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> }</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">      out.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">push</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">([dict.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">get</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(w </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">+</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> c), </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">''</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:#F97583;--shiki-light:#D73A49">      break</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">    out.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">push</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">([w </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">?</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> dict.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">get</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(w) </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">, c])</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    dict.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">set</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(w </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">+</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> c, dict.size </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">    w </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</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 style="--shiki-dark:#F97583;--shiki-light:#D73A49">  return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { out, dict: [</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">...</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">dict.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">keys</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"> unlz77</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">tokens</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"> o </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> ''</span></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">const</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> [</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">d</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">l</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">c</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">] </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">of</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> tokens) {</span></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"> k </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">; k </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">&#x3C;</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> l; k</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">++</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) o </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">+=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> o[o.</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">length</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> -</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> d] </span><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 방금 쓴 글자도 다시 읽는다</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    o </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">+=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> c</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"> o</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">function</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> unlz78</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">tokens</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"> dict</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">''</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"> o </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> ''</span></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">const</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> [</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">i</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">c</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">] </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">of</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> tokens) {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">    const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> entry</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> dict[i] </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">+</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> c</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    o </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">+=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> entry</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    dict.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">push</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(entry) </span><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">  return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> o</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"> s</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> 'banana_banana'</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> a</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> lz77</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(s)</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> b</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> lz78</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(s)</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">console.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">log</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'LZ77'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, a.</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">d</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">l</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">c</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">d</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">},${</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">l</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">},${</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">c</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">})`</span><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 style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">unlz77</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(a) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">===</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> s)</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">console.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">log</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'LZ78'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, b.out.</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">i</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">c</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">i</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">},${</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">c</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">})`</span><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 style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">unlz78</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(b.out) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">===</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> s)</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">console.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">log</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">, b.dict.</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">e</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">k</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">k</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> +</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 1</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">}:${</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">e</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">}`</span><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></code></pre></figure>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="text" data-theme="github-dark github-light"><code data-language="text" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span>LZ77 (0,0,b) (0,0,a) (0,0,n) (2,3,_) (7,5,a) true</span></span>
<span data-line=""><span>LZ78 (0,b) (0,a) (0,n) (2,n) (2,_) (1,a) (3,a) (7,) true</span></span>
<span data-line=""><span>사전 1:b 2:a 3:n 4:an 5:a_ 6:ba 7:na</span></span></code></pre></figure>
<h2 id="onde-os-dois-se-separam"><a class="anchor" href="#onde-os-dois-se-separam">Onde os dois se separam</a></h2>
<p>Os dois decodificadores do código recebem apenas tokens. Isso significa que <strong>o LZ78 também não transmite o dicionário</strong>. O decodificador lê os tokens e adiciona entradas na mesma ordem que o codificador, então o mesmo dicionário é reconstruído. Não transmitir o dicionário é algo que os dois algoritmos têm em comum; onde eles se separam é na <strong>forma de esquecer o conteúdo antigo</strong>. A janela do LZ77 avança e esquece os dados antigos por conta própria. O dicionário do LZ78 continua crescendo se for deixado como está. O artigo original o esvaziava por inteiro sempre que terminava um bloco de comprimento fixo, e as implementações reais limitam o tamanho do dicionário e, quando ele enche, o congelam, o esvaziam ou reutilizam parte das entradas.</p>
<p>A variação mais conhecida do LZ78 é o <strong>LZW</strong> (Lempel-Ziv-Welch). Terry Welch publicou essa melhoria em 1984, e ela foi usada no formato GIF e no utilitário Unix <code>compress</code>, associado à extensão <code>.Z</code>. As duas implementações colocaram um limite no dicionário. A <a href="https://www.w3.org/Graphics/GIF/spec-gif89a.txt" target="_blank" rel="noopener noreferrer">especificação GIF89a</a> limita os códigos a 12 bits (valor máximo 4095) e define à parte um Clear code que devolve o dicionário ao estado inicial. Segundo a <a href="https://github.com/vapier/ncompress/blob/v5.0/compress.1" target="_blank" rel="noopener noreferrer">página de manual do ncompress</a>, o <code>compress</code> acompanha a taxa de compressão depois que o comprimento do código atinge o limite de <code>-b</code> (16 bits por padrão) e, se a taxa cair, descarta o dicionário e o reconstrói do zero.</p>
<p>Cada artigo demonstrou otimalidade assintótica dentro do próprio modelo. O artigo de 1977 mostrou que a taxa de compressão do LZ77 “uniformly approaches the lower bounds” (aproxima-se uniformemente dos limites inferiores) atingíveis por códigos projetados conhecendo a fonte de antemão, e o artigo de 1978 mostrou, para sequências individuais, que o incremental parsing do LZ78 é assintoticamente ótimo. Como os modelos são diferentes, esses resultados sozinhos não permitem comparar os dois.</p>
<h2 id="descendentes-do-lz77"><a class="anchor" href="#descendentes-do-lz77">Descendentes do LZ77</a></h2>
<p>Então, de qual lado vêm os algoritmos de compressão modernos? A maioria dos algoritmos de compressão predominantes hoje é <strong>descendente do LZ77</strong>.</p>
<p>O <strong>LZSS</strong>, que surgiu do artigo de Storer e Szymanski de 1982, é uma variante do LZ77. Quando uma correspondência é tão curta que o ponteiro ficaria mais longo que o original, ele emite um literal (o caractere original) no lugar do ponteiro.</p>
<p>O <strong>DEFLATE</strong> foi projetado por Phil Katz para o PKZIP 2, e sua especificação foi consolidada em 1996 como RFC 1951. A RFC 1951 descreve o DEFLATE como a combinação do LZ77 com a <strong>codificação de Huffman</strong>, que atribui sequências de bits mais curtas aos símbolos mais frequentes. Sua saída é uma sequência que mistura literais e pares (comprimento, distância), e junta literais e comprimentos de correspondência em um único alfabeto (0 a 285), distinguido por um único código de Huffman. O método de compressão que a maioria dos programas ZIP usa por padrão, assim como GZIP e PNG, usam esse DEFLATE. Ou seja, a maioria dos arquivos <code>.zip</code>, <code>.gz</code> e <code>.png</code> que manipulamos todos os dias é descendente direta do LZ77.</p>
<p>Nenhuma razão isolada explica por que o lado do LZ77 se tornou predominante, mas um ponto que as próprias especificações destacam são as patentes. A RFC 1951 observa que muitas variações do LZ77 são patenteadas e afirma que o formato DEFLATE pode ser implementado facilmente de uma forma não coberta por patentes. A <a href="https://www.w3.org/TR/png-3/" target="_blank" rel="noopener noreferrer">especificação PNG</a> apresenta o PNG logo no início como um substituto do GIF livre de patentes. O GIF é o formato que usa o LZW visto antes.</p>
<p>Algoritmos posteriores como <strong>LZMA</strong> (usado pelo 7-Zip e pelo XZ), <strong>LZ4</strong> e <strong>Zstd</strong> também partiram da ideia da janela deslizante do LZ77. LZMA e Zstd desenvolveram juntas a busca de correspondências e a codificação de entropia, enquanto o LZ4 abriu mão por completo da codificação de entropia e escolheu a velocidade com um formato orientado a bytes. A família LZ78 continua presente, na forma do LZW, dentro de formatos como o GIF.</p>
<h2 id="conclusão"><a class="anchor" href="#conclusão">Conclusão</a></h2>
<p>LZ77 e LZ78 partiram da mesma ideia de substituir padrões repetidos por referências curtas. Nenhum dos dois transmite o dicionário e, em ambos, o decodificador o reconstrói, mas eles se separaram em se uma referência aponta para uma posição nos dados já percorridos ou para o número de uma entrada de um dicionário construído à parte, e também esquecem o conteúdo antigo de formas diferentes. Os compressores predominantes que vão do DEFLATE ao LZMA, LZ4 e Zstd descendem do LZ77, enquanto o lado do LZ78 sobrevive como LZW em formatos como o GIF.</p>
<p>Como essa linhagem se reflete na escolha de um formato real, comparando velocidade e taxa de compressão de ZIP, GZIP, ZSTD e XZ e decidindo o que usar para artefatos de build, está em <a href="/240706" target="_blank" rel="noopener noreferrer">Entendendo algoritmos de compressão</a>.</p>
<p>Da próxima vez que você descompactar um arquivo <code>.zip</code> ou <code>.gz</code>, espero que se lembre de que, lá dentro, circulam instruções como “volte tantos caracteres e copie tantos”.</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:#8b5cf6"></span><a href="https://doi.org/10.1145/322344.322346" target="_blank" rel="noopener noreferrer">Storer, Szymanski, Data compression via textual substitution (1982)</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#8b5cf6"></span><a href="https://doi.org/10.1109/MC.1984.1659158" target="_blank" rel="noopener noreferrer">Welch, A Technique for High-Performance Data Compression (1984)</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#3b82f6"></span><a href="https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT" target="_blank" rel="noopener noreferrer">PKWARE, APPNOTE.TXT: .ZIP File Format Specification</a></div>
</div>
</div></details>]]></content:encoded>
            <author>jihoon7705@gmail.com (이지훈)</author>
            <category>curiosidades</category>
            <category>software</category>
        </item>
    </channel>
</rss>