<?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/es</link>
        <description>프론트엔드 개발자 이지훈(후니)의 기술 블로그. React, TypeScript, Next.js 등 웹 개발 기록과 학습 노트를 공유합니다.</description>
        <lastBuildDate>Wed, 19 Aug 2026 00:40:12 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>es</language>
        <copyright>All rights reserved 2026, 이지훈</copyright>
        <item>
            <title><![CDATA[Gestión del estado]]></title>
            <link>https://hooninedev.com/es/260518</link>
            <guid isPermaLink="false">https://hooninedev.com/es/260518</guid>
            <pubDate>Mon, 18 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[En esta publicación quiero hablar sobre la gestión del estado (State Management). No es una comparativa de librerías. Más que decidir qué herramienta es mejor, el objetivo es ordenar el criterio con e...]]></description>
            <content:encoded><![CDATA[<p>En esta publicación quiero hablar sobre la <strong>gestión del estado (State Management)</strong>. No es una comparativa de librerías. Más que decidir qué herramienta es mejor, el objetivo es ordenar el criterio con el que <strong>entendemos el estado</strong> y determinamos <strong>dónde trazar sus límites</strong>.</p>
<p>Hoy las herramientas de IA (Claude, ChatGPT, Cursor, Gemini, Copilot) ya forman parte integral de nuestro trabajo. La velocidad de desarrollo ha crecido exponencialmente, pero, para ser franco, tengo la impresión de que la calidad final de los servicios no ha avanzado al mismo ritmo. Cada vez es más habitual encontrarnos con tantos errores nuevos como funcionalidades añadidas, y también escuchar: «No sé por qué esto terminó así».</p>
<p>Cuando desarrollamos con tanta rapidez, dejamos de examinar el código línea por línea con el mismo detenimiento. Precisamente por eso, considero aún más necesario contar con <strong>los fundamentos necesarios para orientar a la IA en la dirección correcta</strong>. Para mantener la calidad del resultado, debemos ser capaces de detectar los problemas del código generado por la IA y volver a guiarla hacia el objetivo deseado. Entre esos fundamentos se encuentran el desarrollo desde la perspectiva del dominio, la abstracción, el TDD (Test-Driven Development, desarrollo guiado por pruebas), el uso adecuado de librerías y la optimización del rendimiento.</p>
<p>Sin embargo, cada vez que pregunto a colegas de frontend y de otras áreas de TI «¿cuál es la tarea más difícil del desarrollo frontend?», la respuesta más frecuente es siempre la misma: <strong>«Gestionar el flujo del estado»</strong>.</p>
<p>En este artículo trataré de explicar por qué gestionar el flujo del estado resulta tan difícil y qué criterio e intuición conviene desarrollar para hacerlo bien.</p>
<h2 id="qué-es-el-estado-state"><a class="anchor" href="#qué-es-el-estado-state">¿Qué es el estado (State)?</a></h2>
<p>Antes de entrar de lleno en el tema, empecemos por la pregunta más básica: ¿qué es exactamente eso que llamamos «estado»?</p>
<p>Mientras estudiaba desarrollo frontend, solía leer artículos de <a href="https://blog.hoseung.me/2021-12-05-state-management" target="_blank" rel="noopener noreferrer">hoseung.me</a>. Allí se define el estado como <strong>«todos los datos que pueden afectar a la UI»</strong>. El número de «me gusta», los productos del carrito, si un modal está abierto, los valores introducidos, la información del usuario autenticado, la pestaña seleccionada, los resultados de búsqueda o el estado de carga: todo eso es estado.</p>
<p>La documentación oficial de React ofrece una definición algo más formal. El propio título de la página es <a href="https://react.dev/learn/state-a-components-memory" target="_blank" rel="noopener noreferrer">"State: A Component's Memory"</a>, que podríamos desarrollar como <strong>«el mecanismo mediante el cual un componente retiene datos entre renderizados y hace que React dispare un nuevo renderizado cuando esos datos se actualizan»</strong>. Es decir, son datos que no desaparecen con el paso del tiempo, que se actualizan a raíz de algún evento y que, al hacerlo, provocan que la UI vuelva a dibujarse. Hay otro punto importante: el estado está <strong>aislado en cada instancia del componente</strong>. Aunque haya diez instancias del mismo componente en una página, cada una conserva su propio estado independiente. Este hecho se relaciona directamente con la cuestión que veremos más adelante: «¿dónde debe residir el estado?».</p>
<p>Ambas definiciones apuntan a lo mismo: el estado es <strong>«un valor que cambia con el tiempo y afecta al renderizado»</strong>. Una constante que no cambia no es estado. Un token de diseño primitivo fijado en build time no es estado, mientras que el modo oscuro que activa o desactiva el usuario sí lo es. (En rigor, el valor se resuelve según el estado del tema oscuro o claro; por tanto, es más preciso considerar que la «selección del tema» es el estado y que el token es el espejo en el que ese estado se refleja).</p>
<p>Conviene señalar algo más: <strong>no todo el estado vive en los componentes</strong>. Parte vive en cookies; otra, en localStorage, sessionStorage o IndexedDB; y otra, en la URL. Cuando traemos al cliente datos que residen en el servidor y los almacenamos en caché, también se convierten en una forma de estado. Incluso la posición de scroll o el stack del historial que mantiene el navegador deben tratarse a veces como estado, porque determinan el comportamiento de nuestra aplicación.</p>
<h2 id="por-qué-es-tan-difícil"><a class="anchor" href="#por-qué-es-tan-difícil">¿Por qué es tan difícil?</a></h2>
<p>Pensemos primero en términos sencillos por qué cuesta tanto manejar el estado. ¿No bastaría con crear el estado necesario, llevarlo hasta donde haga falta y gestionar bien sus actualizaciones y su reinicialización?</p>
<p>Con esta pregunta en mente, abramos una página del servicio en el que estamos trabajando.</p>
<p>¿Cuántos componentes contiene? Incluso una página sencilla puede tener desde decenas hasta cientos de componentes organizados en forma de árbol. Cada componente puede mantener su propio estado, compartirlo con componentes hermanos o recibirlo de su padre. El estado también se transfiere entre páginas; parte debe sobrevivir a una recarga y parte debe desaparecer al cerrar la pestaña.</p>
<p>La verdadera razón por la que el estado es difícil de gestionar es esta: <strong>no podemos ver de un vistazo dónde se declaran todos esos estados, cómo se actualizan ni cuándo desaparecen</strong>. Cuantos más componentes con funciones similares aparecen, más difícil resulta nombrar el estado y rastrear el código que lo modifica.</p>
<p>Así se forma una telaraña invisible. Un clic en el componente A invalida los datos de B; esa invalidación cierra la UI de C; y al cerrarse C desaparece el contenido de un formulario. Si esta cadena no está expresada explícitamente en ninguna parte del código, cuando depuramos un error tenemos que reconstruir la telaraña en nuestra cabeza.</p>
<p>Entonces, ¿cómo podemos ordenar esa telaraña? A mi juicio, el primer paso consiste en reconocer que <strong>«existen distintos tipos de estado»</strong>.</p>
<h2 id="no-todos-los-estados-son-iguales"><a class="anchor" href="#no-todos-los-estados-son-iguales">No todos los estados son iguales</a></h2>
<p><a href="https://kentcdodds.com/blog/application-state-management-with-react" target="_blank" rel="noopener noreferrer">Kent C. Dodds</a> divide el estado entre <strong>Server Cache</strong> (información que reside en el servidor y que el cliente conserva para acceder a ella rápidamente) y <strong>UI State</strong> (estado que solo existe en la UI para controlar el comportamiento de la interfaz). A menudo cometemos errores al agrupar ambos.</p>
<p>La <a href="https://tanstack.com/query/latest/docs/framework/react/guides/does-this-replace-client-state" target="_blank" rel="noopener noreferrer">documentación oficial de TanStack Query</a> lo define como una librería de server-state que gestiona operaciones asíncronas entre el servidor y el cliente, mientras que herramientas como Redux, MobX y Zustand son librerías de client-state. (Aunque pueden almacenar datos asíncronos, hacerlo resulta ineficiente).</p>
<p>La idea central es clara: <strong>Server State y Client State son problemas distintos</strong>. Server State es asíncrono, puede ser modificado por otros usuarios y, con el tiempo, pasa a estar stale. Client State es síncrono, está bajo nuestro control y desaparece al recargar la página. (Para ser exactos, cuando la página se descarga, <strong>el runtime de JavaScript se reinicia y tanto el árbol de componentes como el estado alojado en la memoria heap son liberados</strong>. Por eso, al montarse de nuevo, <code>useState</code> vuelve a comenzar desde su valor inicial). Si intentamos gestionar ambos con la misma herramienta, tendremos que implementar por nuestra cuenta patrones como la invalidación de caché, la actualización en background o las actualizaciones optimistas.</p>
<p>Doy un paso más y clasifico el estado del frontend en <strong>siete categorías</strong>. Conviene aclarar de antemano que estas siete categorías no se separan limpiamente sobre un único eje. Mezclan ubicación de almacenamiento, origen, ciclo de vida y función, por lo que un mismo estado puede pertenecer a varias categorías a la vez. No pretenden ser una taxonomía perfecta, sino <strong>preguntas que debemos plantearnos al decidir cómo gestionar el estado</strong>.</p>
<ul>
<li><strong>Estado local (Local State)</strong> — Estado usado solo dentro de un componente o de un subárbol reducido</li>
<li><strong>Estado global (Global State)</strong> — Estado que debe compartirse en toda la aplicación</li>
<li><strong>Estado del servidor (Server State)</strong> — Estado cuya fuente de verdad es el servidor y cuya copia en el cliente es una caché</li>
<li><strong>Estado del formulario (Form State)</strong> — Estado temporal que existe mientras el usuario introduce datos</li>
<li><strong>Estado de la URL (URL State)</strong> — Estado compartible que vive en la barra de direcciones y sobrevive a una recarga</li>
<li><strong>Estado externo (External State)</strong> — Estado fuera de React, como cookies, localStorage, sessionStorage e IndexedDB</li>
<li><strong>Guard de estado (State Guard)</strong> — Lógica que bloquea, permite o valida accesos y acciones según combinaciones de estado, en lugar de ser estado por sí misma</li>
</ul>
<p>Además, existen estados de flujo que conviene modelar con una máquina de estados y estados colaborativos en tiempo real basados en WebSocket o CRDT.</p>
<p>Veamos, una por una, por qué cada categoría requiere herramientas diferentes y con qué criterio debemos abordarla.</p>
<h2 id="estado-local-local-state"><a class="anchor" href="#estado-local-local-state">Estado local (Local State)</a></h2>
<p>Es el tipo de estado más sencillo. Solo se utiliza dentro de un componente y, desde fuera, no hay necesidad ni motivo para conocerlo. Algunos ejemplos son si un modal está abierto, el estado on/off de un botón toggle, el estado de hover o el término de búsqueda que se está escribiendo.</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>Hasta aquí, probablemente todo resulte familiar. Sin embargo, la verdadera dificultad del estado local reside en decidir <strong>«dónde debe ubicarse este estado»</strong>.</p>
<p>En su artículo sobre <a href="https://kentcdodds.com/blog/state-colocation-will-make-your-react-app-faster" target="_blank" rel="noopener noreferrer">State Colocation, Kent C. Dodds</a> señala que <strong>la gente está acostumbrada a «elevar» el estado (lift up), pero rara vez vuelve a «colocarlo cerca» (colocate) cuando el código cambia</strong>.</p>
<p>Elevar el estado es algo que hacemos de forma natural cuando varios componentes hermanos necesitan compartirlo. Como ambos deben ver los mismos datos, trasladamos el estado al padre común y lo pasamos hacia abajo mediante props.</p>
<p>El problema aparece cuando esos componentes hermanos dejan de necesitarlo. No solemos <strong>volver a bajar</strong> el estado hacia los hijos. Como resultado, el componente padre termina acumulando estados que en realidad no le conciernen y, cada vez que vuelve a renderizarse, todo el árbol de hijos se renderiza con él.</p>
<p>Por eso, el primer criterio para el estado local es: <strong>para ganar velocidad y simplicidad, coloca el estado tan cerca como sea posible del código que lo utiliza</strong>. Si un estado solo se usa en uno de los hijos de un componente, no hay razón para que lo mantenga el padre. Movámoslo al interior de ese hijo. El padre será más ligero.</p>
<h2 id="estado-global-global-state"><a class="anchor" href="#estado-global-global-state">Estado global (Global State)</a></h2>
<p>El estado global debe ser accesible desde cualquier parte de la aplicación. La información de autenticación, el tema, el idioma o las notificaciones (toasts) son posibles candidatos.</p>
<p>La diferencia entre el estado local y el global no se reduce a «dónde viven». Lo que cambia es <strong>el contrato de acceso</strong>. El estado local establece el contrato de que <strong>«solo tiene sentido dentro de este componente»</strong>; el global, en cambio, publica en todo el código el contrato de que <strong>«este valor puede consultarse con este nombre desde cualquier lugar de la aplicación»</strong>. La esencia del estado global es que ese contrato resulta costoso.</p>
<p>Crear un estado global equivale, en realidad, a añadir <strong>una dependencia implícita en toda la aplicación</strong>.</p>
<h2 id="estado-del-servidor-server-state"><a class="anchor" href="#estado-del-servidor-server-state">Estado del servidor (Server State)</a></h2>
<p>Guardamos en el estado del cliente los datos recibidos mediante una API, gestionamos manualmente la carga y los errores con valores boolean y terminamos preguntándonos: <strong>«¿por qué escribo siempre el mismo boilerplate?»</strong>.</p>
<p>Tanner Linsley, principal responsable de TanStack, afirma: <strong>«Client State es síncrono y predecible. Server State es asíncrono, se comparte entre varios componentes y exige gestionar con cuidado el caching, las actualizaciones en background y los estados de error»</strong>. Es decir, Server State es <strong>una especie esencialmente distinta</strong> de Client State. No deben tratarse con la misma herramienta.</p>
<p>La complejidad de Server State no se debe a las herramientas, sino a <strong>la propia naturaleza de los datos</strong>.</p>
<p>Los datos que ve el cliente pertenecen al servidor. Lo que conserva el cliente no es más que <strong>un snapshot de un momento concreto</strong>. Con el paso del tiempo, esos datos acumulan staleness. Además, son asíncronos, pueden fallar y atraviesan estados como pending, error y success.</p>
<p>La propiedad esencial más importante es que <strong>nada garantiza que las respuestas regresen en el mismo orden en que se enviaron las solicitudes</strong>. Imaginemos que escribimos rápidamente «react» en un campo de búsqueda. Las solicitudes r → re → rea → reac → react se envían en ese orden, pero si la respuesta de «react» llega primero y después llega la de «rea», la pantalla mostrará los resultados de «rea». Para evitar este problema hay que ocuparse de los <strong>riesgos de concurrencia (race conditions)</strong>, implementando cada vez a mano un AbortController o el seguimiento de los ID de solicitud.</p>
<h2 id="estado-de-formulario-form-state"><a class="anchor" href="#estado-de-formulario-form-state">Estado de formulario (Form State)</a></h2>
<p>Los formularios albergan un tipo de estado peculiar. Mientras el usuario escribe, cambia intensamente; pero, una vez enviado, normalmente desaparece. No se comparte con ningún otro lugar y, en la mayoría de los casos, tampoco tiene otro destino en el que almacenarse.</p>
<p>El problema es que ese «cambio intenso» resulta costoso. Si cada pulsación provoca un nuevo renderizado de React, en los formularios grandes el retraso al escribir puede llegar a ser perceptible. Además, un formulario no se limita a «guardar valores». Dentro de él conviven y cambian al mismo tiempo muchos tipos de estado: <strong>validación, dirty check, estado de envío, mensajes de error y flujos de varios pasos</strong>.</p>
<p>En un formulario de varios pasos, como un proceso de pago en tres etapas, se espera que <strong>«el progreso sobreviva incluso a una recarga a mitad del proceso»</strong>. Si mantenemos los valores del formulario únicamente con useState, la recarga los eliminará todos. Lo natural es almacenarlos en <strong>sessionStorage</strong> (persistencia temporal por pestaña) o en la <strong>URL</strong> (para pasos que puedan compartirse). Es decir, según los requisitos de su ciclo de vida, Form State se combina con <strong>External State</strong> o <strong>URL State</strong>.</p>
<h2 id="estado-de-url-url-state"><a class="anchor" href="#estado-de-url-url-state">Estado de URL (URL State)</a></h2>
<p>Supongamos que en una página de búsqueda estamos filtrando por categoría, orden y número de página. Si mantenemos esos estados con useState, aparecen tres problemas a la vez.</p>
<ul>
<li>Al recargar, todos los filtros se reinician</li>
<li>Aunque compartamos la URL con otra persona, esta verá la página sin los filtros aplicados</li>
<li>Al pulsar «Atrás», no regresaremos a los filtros anteriores</li>
</ul>
<p>Para resolver estos problemas, <strong>resulta natural colocar el estado en la URL</strong>. La URL es, por sí misma, un almacenamiento persistente gratuito que ya admite recargas, uso compartido e historial.</p>
<pre><code>/products?category=shoes&#x26;sort=price-desc&#x26;page=2
</code></pre>
<p>Esa única línea contiene el estado completo de <strong>«la segunda página de la categoría de zapatos, ordenada por precio descendente»</strong>. No hace falta mantenerlo por separado con useState.</p>
<p>Entonces, ¿cuándo conviene gestionar el estado en la URL? <strong>La URL es una interfaz pública</strong>. No debemos incluir en ella contraseñas, tokens de autenticación ni notas temporales que el usuario no quiera mostrar a otras personas. Tampoco conviene insertar directamente valores que cambian con demasiada frecuencia —como un término de búsqueda que se actualiza con cada pulsación—, porque el stack del historial se llenará de basura. En esos casos debemos aplicar el cambio después de un debounce y reservar <code>push</code> para cuando corresponda, usando <code>replace</code> en las actualizaciones que no deban añadir una entrada al historial.</p>
<p>Los valores de la URL son <strong>siempre strings</strong>. Los números, valores boolean, arrays y objetos deben pasar por un proceso de serialización y deserialización. Además, la URL debe seguir las reglas de <a href="https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams" target="_blank" rel="noopener noreferrer">percent-encoding</a>, por lo que caracteres como <code>&#x26;</code>, <code>=</code>, el coreano o los espacios reciben un tratamiento especial. Implementarlo manualmente una y otra vez pronto se convierte en una fuente de errores.</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>Librerías como <a href="https://nuqs.dev/" target="_blank" rel="noopener noreferrer">nuqs</a> resuelven ambos problemas mediante el concepto de <em>parser</em>. Parsers como <code>parseAsInteger</code>, <code>parseAsBoolean</code> y <code>parseAsJson</code> se encargan a la vez de la serialización, la deserialización y los tipos. Son compatibles con la mayoría de los entornos, incluidos Next.js (tanto App Router como Pages Router), React Router v6/v7, TanStack Router y Remix.</p>
<p>¿Significa eso que podemos insertar en la URL todo el estado que queramos? Al margen de los problemas de serialización y tipos, queda una última restricción. <a href="https://datatracker.ietf.org/doc/html/rfc7230" target="_blank" rel="noopener noreferrer">RFC 7230</a> no fija un límite exacto, pero recomienda que «los servidores admitan al menos 8.000 octetos» (unidad que designa inequívocamente un byte formado por ocho bits en redes y comunicaciones de datos). Los límites también varían entre navegadores: los navegadores modernos suelen admitir desde 8 KB hasta decenas de miles de caracteres, pero <strong>el procesamiento de OG y enlaces compartidos de buscadores y redes sociales, así como algunos gateways, puede truncarlos alrededor de los 2 KB</strong>. Por tanto, no introduzcamos datos ilimitados en la URL. Lo seguro es conservar allí únicamente <strong>los filtros esenciales que deban poder compartirse</strong> y delegar el resto en sessionStorage o en almacenamiento del lado del servidor.</p>
<h2 id="estado-externo-external-state"><a class="anchor" href="#estado-externo-external-state">Estado externo (External State)</a></h2>
<p>React solo conoce el estado que hay en su interior. Sin embargo, nuestra aplicación se comunica sin cesar con el mundo exterior a React. Los estados que viven en ese mundo sobreviven y cambian con independencia del ciclo de vida de React. Aquí, External State se refiere a <strong>Cookie, localStorage,sessionStorage,IndexedDB</strong>.</p>
<p>¿Cómo elegir el almacenamiento apropiado? Suelo evaluarlo desde cuatro perspectivas: <strong>duración, capacidad, sincronía y seguridad</strong>.</p>
<p>Para los <strong>tokens de autenticación</strong>, la <a href="https://owasp.org/www-community/HttpOnly" target="_blank" rel="noopener noreferrer">recomendación de OWASP</a> prioriza las <strong>cookies HttpOnly + Secure</strong>. Como JavaScript puede acceder a localStorage, <strong>en cuanto se produce una vulnerabilidad XSS, el token queda directamente expuesto</strong>. Algunas guías de seguridad recomiendan un patrón híbrido: <strong>guardar el access token en memoria y el refresh token en una cookie HttpOnly</strong>. Para datos persistentes, no sensibles y que cambian con poca frecuencia se utiliza localStorage; para datos que deben desaparecer junto con la pestaña, sessionStorage. IndexedDB suele emplearse para caché offline, grandes volúmenes de datos y archivos.</p>
<p>Cookie y Web Storage (local/session) <strong>solo almacenan strings</strong>. Para guardar un objeto hay que pasar por <code>JSON.stringify</code>/<code>JSON.parse</code>. Pero JSON tiene limitaciones.</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 convierte en string al hacer un round trip por JSON, mientras que <code>Map</code>, <code>Set</code> y <code>undefined</code> pueden perder datos. Con el comportamiento predeterminado, <code>BigInt</code> hace que <code>JSON.stringify</code> lance un <code>TypeError</code>, por lo que la serialización falla por completo. Al guardar objetos en almacenamiento externo, debemos tener siempre presente <strong>qué tipos pueden desaparecer, transformarse o hacer fallar la serialización</strong> y añadir un adaptador cuando sea necesario.</p>
<p>La verdadera dificultad de External State es que <strong>React no detecta automáticamente sus cambios</strong>. Aunque escribamos un valor en localStorage, los componentes de React no vuelven a renderizarse. Normalmente existen tres patrones para resolverlo.</p>
<ul>
<li><strong>Envolverlo en un custom hook (useLocalStorage) y sincronizar el estado externo con el estado de React.</strong> Es una solución ligera, pero si la implementamos nosotros mismos debemos cubrir todos los casos límite: múltiples pestañas, SSR, tearing, etc.</li>
<li>Usar el hook <code>useSyncExternalStore</code>, incorporado en React 18, para <strong>«sincronizarnos con un estado externo a React»</strong>. Esto permite <strong>garantizar que no se produzca tearing durante el renderizado concurrente</strong>. Es la herramienta estándar para conectar localStorage, las API del navegador y los stores externos.</li>
<li>Aprovechar librerías existentes, ya que las librerías de estado ofrecen como funcionalidad de primera clase la integración con almacenamiento externo, como el middleware <code>persist</code> de Zustand o <code>atomWithStorage</code> de Jotai.</li>
</ul>
<p>Añadamos otro criterio: <strong>en el momento en que llevamos External State a React, la responsabilidad de sincronizarlo recae sobre nosotros</strong>. ¿Qué ocurre si se actualiza desde otra pestaña? ¿Si el servidor modifica una cookie? ¿Si el usuario manipula localStorage directamente desde las herramientas de desarrollo del navegador? Estas situaciones suelen convertirse en algunas de las mayores fuentes de errores.</p>
<h2 id="guard-del-estado-state-guard"><a class="anchor" href="#guard-del-estado-state-guard">Guard del estado (State Guard)</a></h2>
<p>La última categoría es algo diferente. No es estado en sí mismo, sino <strong>la lógica que bloquea, permite o valida un flujo a partir de una combinación de estados</strong>.</p>
<p>El ejemplo más habitual es el <strong>Auth Guard</strong>.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">function</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> ProtectedRoute</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({ </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">children</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> }) {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">isAuthenticated</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">isLoading</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> } </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> useAuth</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">();</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  if</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (isLoading) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Spinner</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> />;</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  if</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">!</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">isAuthenticated) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Navigate</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> to</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"/login"</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> replace</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> />;</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> children;</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>Aquí, el estado <code>isAuthenticated</code> controla el flujo de routing. Eso es precisamente la lógica de un guard. Existen varios tipos: Auth Guard (autenticación), guards de autorización (roles o permisos específicos), guards de flujo (ramificación al entrar) y guards de validación (activación de etapas), entre otros.</p>
<p>La lógica de guards tiende a concentrarse en un único lugar. Es habitual que un componente reúna condiciones como <strong>«si no ha iniciado sesión, ir a la página de login; si no tiene permisos, mostrar un 403; si el carrito está vacío, ir a la página de productos; si el usuario está suspendido, mostrar el aviso de suspensión»</strong>. Cuanto más crece el guard, más difícil resulta depurar qué condición bloqueó el flujo y dónde.</p>
<p>Un buen guard <strong>solo comprueba una cosa</strong>. La combinación se realiza mediante 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 una única decisión y la composición queda a cargo de la estructura del árbol. Añadir un guard nuevo no requiere modificar los existentes.</p>
<p>Al diseñar guards, hay algo que debemos pensar incluso más que en el bloqueo: <strong>decidir adónde enviar al usuario y qué hacer después</strong>. Un guard que solo bloquea y no ofrece fallback termina en una pantalla en blanco o un spinner infinito.</p>
<p>El error más común consiste en que <strong>«el contenido protegido parpadea brevemente antes de que termine la comprobación asíncrona del guard»</strong>. La validación del token de autenticación y la consulta de permisos suelen ser asíncronas; durante ese intervalo, <code>isAuthenticated</code> puede adoptar temporalmente el valor <code>undefined</code> o <code>false</code>. <strong>Si no tratamos explícitamente el estado de carga, el contenido protegido puede quedar expuesto durante ese instante o el usuario puede ser redirigido erróneamente a la 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>Al implementar guards de autorización se utilizan habitualmente dos modelos.</p>
<ul>
<li><strong>RBAC (Role-Based Access Control)</strong>: asigna permisos por rol. Por ejemplo: «admin puede ver la información de todos los usuarios». Es sencillo y rápido, pero el número de roles se dispara a medida que aumenta la granularidad</li>
<li><strong>ABAC (Attribute-Based Access Control)</strong>: determina los permisos mediante una combinación de atributos. Por ejemplo: «si el usuario es autor de la publicación, pertenece al mismo equipo o es admin». Tiene una gran capacidad expresiva, pero es difícil de implementar y depurar</li>
</ul>
<p>Como muestra la <a href="https://tanstack.com/router/v1/docs/framework/react/how-to/setup-rbac" target="_blank" rel="noopener noreferrer">guía de RBAC de TanStack Router</a>, se recomienda el patrón de colocar los guards en <code>beforeLoad</code>, en el nivel del router. La clave es que <strong>las comprobaciones de autorización no estén dispersas por el código, sino que puedan expresarse como datos (listas de roles y permisos)</strong>. Así, un cambio en la política de permisos se limita a un <em>cambio de datos</em>.</p>
<h2 id="conclusión"><a class="anchor" href="#conclusión">Conclusión</a></h2>
<p>Recapitulemos. La gestión del estado no es difícil porque las librerías lo sean. Es difícil porque <strong>a menudo olvidamos que existen distintos tipos de estado</strong> y porque resulta fácil pasar por alto que cada tipo exige herramientas y formas de pensar diferentes.</p>
<p>Mantener el estado local lo más cerca posible; cuestionar una vez más si el estado global es realmente global; tratar Server State como caché; separar los formularios del dominio; aprovechar la URL de forma más activa; ser conscientes de la responsabilidad que implica el almacenamiento externo; y dividir los guards en unidades pequeñas que puedan componerse. Esos son los fundamentos para trabajar con las siete categorías.</p>
<p>Y el criterio que opera por encima de ellas puede condensarse, en última instancia, en cuatro preguntas.</p>
<ul>
<li>¿Dónde está la Single Source of Truth de estos datos?</li>
<li>¿Es un valor que puede calcularse o un valor que realmente debemos almacenar?</li>
<li>¿Hay alguna combinación imposible entre estos estados?</li>
<li>¿Este estado debe estar realmente en esta ubicación?</li>
</ul>
<p>Plantearnos estas preguntas cada vez que construimos una pantalla nueva, revisamos una PR o recibimos código generado por la IA es, a mi juicio, la forma más segura de desarrollar criterio e intuición.</p>
<p>Como decía al principio, la IA permanecerá a nuestro lado durante mucho tiempo. Cada vez dedicaremos menos tiempo a revisar el código línea por línea. Pero, precisamente por eso, será más valiosa la capacidad de responder a pequeñas preguntas como <strong>«¿dónde debe residir este estado?»</strong>. Pedirle a la IA «añade aquí otro useState» es fácil. Sin embargo, comprender qué hilo añade esa línea a la telaraña de nuestra aplicación depende únicamente del criterio de quien lee el código.</p>
<p>No existe una única respuesta correcta. Pero hay una diferencia evidente entre <strong>«crear estado sin saber qué es el estado»</strong> y <strong>«crearlo siendo conscientes de su tipo y ubicación»</strong>. Espero que, la próxima vez que los lectores de este artículo vayan a escribir una línea de <code>useState</code>, se detengan un instante y se pregunten: «¿a qué categoría de estado pertenece esto?».</p>
<h3 id="referencias"><a class="anchor" href="#referencias">Referencias</a></h3>
<details class="ref-block"><summary class="ref-summary">참고 자료</summary><div class="ref-list">
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#3b82f6"></span><a href="https://react.dev/learn/choosing-the-state-structure" target="_blank" rel="noopener noreferrer">React, Choosing the State Structure</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#3b82f6"></span><a href="https://legacy.reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html" target="_blank" rel="noopener noreferrer">React, You Probably Don't Need Derived State</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#3b82f6"></span><a href="https://xstate.js.org/" target="_blank" rel="noopener noreferrer">XState</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#f59e0b"></span><a href="https://www.syncfusion.com/blogs/post/react-state-management-libraries" target="_blank" rel="noopener noreferrer">Top 5 React State Management Tools in 2026</a></div>
</div>
</div></details>]]></content:encoded>
            <author>jihoon7705@gmail.com (이지훈)</author>
            <category>Gestión-del-estado-frontend</category>
            <category>Arquitectura-React</category>
        </item>
        <item>
            <title><![CDATA[Modelo de dominio]]></title>
            <link>https://hooninedev.com/es/260418</link>
            <guid isPermaLink="false">https://hooninedev.com/es/260418</guid>
            <pubDate>Sat, 18 Apr 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[En este artículo quiero hablar del dominio (Domain). Durante mi trayectoria como desarrollador me he encontrado con bastante frecuencia la palabra «dominio (Domain)». Sin embargo, cuando alguien pregu...]]></description>
            <content:encoded><![CDATA[<p>En este artículo quiero hablar del <strong>dominio (Domain)</strong>.</p>
<p>Durante mi trayectoria como desarrollador me he encontrado con bastante frecuencia la palabra <strong>«dominio (Domain)»</strong>. Sin embargo, cuando alguien pregunta «¿qué es exactamente un dominio?», no resulta fácil dar una respuesta clara. (Sinceramente, cuando empecé a programar, pensaba que dominio se refería a www).</p>
<p>Al buscar información sobre el dominio, uno llega de forma natural a conceptos como <strong>modelo de dominio</strong>, <strong>objeto de dominio</strong> y <strong>modelo de objetos de dominio</strong>. Siempre he echado en falta artículos que expliquen bien en qué se diferencian y qué significan estos conceptos en el <strong>frontend</strong>, no en el backend. En este artículo partiré de la definición de cada concepto y explicaré con ejemplos cómo conviene separar y abstraer la lógica de dominio en el frontend.</p>
<p>Últimamente me interesa mucho el dominio fiscal. Como se acerca mayo, mes de la declaración del impuesto sobre la renta global en Corea, usaré los impuestos como ejemplo en este artículo.</p>
<hr>
<h2 id="dominio-domain"><a class="anchor" href="#dominio-domain">Dominio (Domain)</a></h2>
<p>Empecemos por la pregunta más básica. ¿Qué es un <strong>dominio</strong>?</p>
<p>Eric Evans lo define así en su libro <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>Una esfera de conocimiento, influencia o actividad.</p></div><div class="quote-original" lang="en"><p>"A sphere of knowledge, influence, or activity."</p></div></blockquote>
<p>Dicho de forma sencilla, el dominio es la propia <strong>área problemática que se quiere resolver mediante programación</strong>. Si creamos un servicio para presentar declaraciones de impuestos, el dominio es la «declaración de impuestos»; si creamos una plataforma de reclamaciones de seguros, el dominio es la «reclamación de seguros». El dominio no es código. Es un área problemática del mundo real que existe antes que el software.</p>
<p>¿Qué significa esto para quien desarrolla frontend? La UI que construimos es, al fin y al cabo, una <strong>ventana (window)</strong> que permite mostrar este dominio al usuario y manipularlo. Si desarrollamos un servicio de devolución de impuestos como Toss Income o Samjjeomsam, cuyo dominio principal son los impuestos, representamos en la UI conceptos de dominio como los tipos de ingresos, el coeficiente de gastos, las deducciones sobre la renta, los créditos fiscales y el importe de la devolución. Por eso, quien desarrolla frontend también debe comprender a fondo el dominio con el que trabaja. Tan importante como crear buenos componentes de UI es saber <strong>«qué problema resuelve este servicio»</strong>.</p>
<p>Pero incluso dentro de un único dominio como el de los «impuestos» existen numerosos subdominios. Basta con observar el flujo de cálculo del impuesto sobre la renta global que conozco a grandes rasgos.</p>
<p><img src="/content/260418/1.png" alt="1.png" width="1090" height="566" loading="eager" fetchpriority="high" decoding="async"></p>
<p>Cada etapa de este flujo constituye un subdominio con reglas y datos propios. Dentro del gran dominio de los «impuestos» se entrelazan subdominios como ingresos (Income), deducciones (Deduction), cuota tributaria (Tax) y declaración (Filing). Cómo dividirlos en el código es precisamente la cuestión central del modelado de dominio.</p>
<h2 id="modelo-de-dominio-domain-model"><a class="anchor" href="#modelo-de-dominio-domain-model">Modelo de dominio (Domain Model)</a></h2>
<p>Entonces, ¿qué es un modelo de dominio? ¿En qué se diferencia el dominio del «modelo de dominio»?</p>
<p>Martin Fowler y Eric Evans definen el modelo de dominio de la siguiente manera.</p>
<blockquote class="bilingual-quote"><div class="quote-translation" lang="ko"><p>Un modelo de objetos del dominio que incorpora tanto comportamiento como datos. — Martin Fowler</p></div><div class="quote-original" lang="en"><p>An object model of the domain that incorporates both behavior and data.</p></div></blockquote>
<blockquote class="bilingual-quote"><div class="quote-translation" lang="ko"><p>Un sistema de abstracciones que describe determinados aspectos de un dominio y que puede utilizarse para resolver problemas relacionados con ese dominio. — 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>La clave está en la <strong>«abstracción selectiva»</strong>. Un modelo de dominio no contiene todo lo que existe en el mundo real. Del mismo modo que un director de cine no registra cada escena de la realidad, sino que elige solo las necesarias para contar una historia, el modelo de dominio <strong>selecciona y estructura únicamente los aspectos necesarios para resolver el problema</strong>.</p>
<p>Hay aquí un punto importante: un modelo de dominio no tiene por qué ser código. Puede ser un diagrama dibujado en una pizarra o incluso un modelo mental (Mental Model) compartido por el equipo. En definitiva, el propio término modelo de dominio puede referirse a un concepto independiente del software.</p>
<p>Hay una confusión especialmente frecuente entre quienes desarrollan frontend: ver la estructura de una respuesta de API y pensar «este es el modelo de dominio». Sin embargo, eso es un <strong>modelo de datos (Data Model)</strong>, no un modelo de dominio.</p>
<p>Podemos distinguirlos así.</p>
<table>
<thead>
<tr>
<th>Categoría</th>
<th>Modelo de dominio</th>
<th>Modelo de datos</th>
</tr>
</thead>
<tbody>
<tr>
<td>Propósito</td>
<td>Expresar conceptos y reglas de negocio</td>
<td>Definir estructuras de almacenamiento o transmisión</td>
</tr>
<tr>
<td>Lenguaje</td>
<td>Términos de negocio (base imponible, crédito fiscal, devolución)</td>
<td>Términos técnicos (string, number, array)</td>
</tr>
<tr>
<td>Elementos incluidos</td>
<td>Datos + comportamiento (reglas)</td>
<td>Solo estructura de datos</td>
</tr>
<tr>
<td>Ejemplo</td>
<td>«A una base imponible de hasta 14 millones de wones se le aplica un 6 %»</td>
<td><code>{ taxableBase: number, taxRate: number }</code></td>
</tr>
</tbody>
</table>
<p>El modelo de datos define «qué forma tienen los datos que se intercambian», mientras que <strong>el modelo de dominio define «qué significan esos datos para el negocio y qué reglas siguen»</strong>. Si no se distingue entre ambos, los componentes pasan a depender directamente de la estructura de las respuestas de la API y cualquier cambio en el esquema del backend acaba afectando a todo el frontend.</p>
<h2 id="objeto-de-dominio-domain-object"><a class="anchor" href="#objeto-de-dominio-domain-object">Objeto de dominio (Domain Object)</a></h2>
<p>Si el modelo de dominio es un sistema de conceptos, el <strong>objeto de dominio</strong> es la entidad concreta que implementa uno de esos conceptos en el código.</p>
<p>En <a href="https://www.codewithjason.com/difference-domains-domain-models-object-models-domain-objects/" target="_blank" rel="noopener noreferrer">un artículo de Jason Swett</a>, creador de Code with Jason, se define así el objeto de dominio.</p>
<blockquote class="bilingual-quote"><div class="quote-translation" lang="ko"><p>Llamaría objeto de dominio a cualquier objeto de mi modelo de objetos que también exista como concepto en mi modelo de dominio.</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>Es decir, si en el modelo de dominio existe el concepto de «renta global» y en el código hay un tipo llamado <code>Income</code>, ese <code>Income</code> es un objeto de dominio. Pero no todos los objetos del código son objetos de dominio. Elementos como <code>HttpClient</code>, <code>LocalStorageAdapter</code> o <code>useDebounce</code> son herramientas técnicas, no conceptos de dominio.</p>
<h3 id="entity-y-value-object"><a class="anchor" href="#entity-y-value-object">Entity y Value Object</a></h3>
<p>Evans clasifica los objetos de dominio en tres categorías: <strong>Entity</strong>, <strong>Value Object</strong> y <strong>Service</strong>. (Martin Fowler denomina esta clasificación «Evans Classification»). Un Service representa una operación de dominio que no pertenece de forma natural a un objeto concreto. Como el tema central de este artículo es cómo identificar los datos, nos centraremos en Entity y Value Object.</p>
<p>Una <strong>Entity</strong> es un objeto con una identidad única que persiste a lo largo del tiempo y de sus distintas representaciones. Una declaración de impuestos (TaxFiling), un contribuyente (Taxpayer) o un registro de ingresos (IncomeRecord) se identifican mediante un ID único; aunque cambien sus atributos, si conservan el mismo ID siguen siendo la misma Entity. Aunque se modifiquen las deducciones de una declaración, mientras no cambie su ID, seguirá siendo la misma declaración.</p>
<p>Un <strong>Value Object</strong> es un objeto cuyo significado depende únicamente de la combinación de sus atributos y se considera igual a otro si todos sus valores coinciden. El dinero (Money), un tipo impositivo (TaxRate) o un tramo impositivo (TaxBracket) son objetos cuyo propio valor constituye su significado. Un «tipo impositivo del 6 %» es el mismo «tipo impositivo del 6 %» dondequiera que se utilice.</p>
<p>¿Por qué es importante esta distinción en frontend? Veámoslo con el siguiente ejemplo.</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 es una Entity porque toma el id como criterio de identidad. (Tener un campo id no define por sí solo una Entity; lo esencial es que «ese id permite decidir si dos elementos son iguales o distintos»). Money se identifica únicamente por la combinación de amount y currency, sin id, y se considera el mismo valor cuando coinciden todos sus atributos.</p>
<p>Las Entity se comparan por ID y los Value Object, por atributos. Si esta distinción está clara, la lógica para decidir «si estos datos son iguales o distintos» en la gestión del estado se ordena de forma natural. Al actualizar un elemento de una lista, una Entity se busca por ID y se reemplaza, mientras que un Value Object se sustituye de forma inmutable (immutable replace).</p>
<h2 id="modelo-de-objetos-de-dominio-domain-object-model"><a class="anchor" href="#modelo-de-objetos-de-dominio-domain-object-model">Modelo de objetos de dominio (Domain Object Model)</a></h2>
<p>Ya sabemos qué son un «modelo de dominio» y un «objeto de dominio», pero ¿qué es un <strong>modelo de objetos de dominio</strong>?</p>
<p>Al investigar el tema, descubrí que, sorprendentemente, no existe una definición consensuada. Buena parte de la bibliografía considera «modelo de dominio», «modelo de objetos de dominio», «modelo conceptual (conceptual model)» y «modelo de objetos de análisis (analysis object model)» como <strong>sinónimos en la práctica</strong>: distintos nombres para el modelo conceptual que se dibuja durante la fase de análisis orientado a objetos.</p>
<p>También hay quien los considera capas algo más separadas. Una explicación representativa de esta visión sostiene que <strong>el modelo de objetos es el punto en el que el modelo de dominio se transforma en código real</strong>.</p>
<p>Desde esta segunda perspectiva, el <strong>modelo de objetos</strong> es la estructura de <strong>todos los objetos de código</strong> del sistema. Incluye también herramientas técnicas como <code>HttpClient</code> y <code>useDebounce</code>. Dentro de él, el <strong>subconjunto de objetos que representan conceptos de dominio y las relaciones entre ellos</strong> constituye el <strong>modelo de objetos de dominio</strong>. Esta idea enlaza con la tradición del modelado orientado a objetos, que ha definido el «Object Model» como la estructura estática de un sistema —clases, atributos, operaciones y relaciones—.</p>
<p>Considero que esta perspectiva resulta más práctica para quien desarrolla frontend, porque en el código que escribimos los objetos de dominio y los objetos técnicos siempre aparecen mezclados.</p>
<p>En definitiva, <strong>dominio → modelo de dominio → modelo de objetos de dominio → objeto de dominio</strong> es una jerarquía que va de lo abstracto a lo concreto. El dominio es el concepto más amplio y el objeto de dominio, el más concreto. Por eso, al escribir código frontend, la cuestión que realmente debemos resolver es <strong>cómo estructurar el modelo de objetos de dominio, es decir, los tipos que representan los conceptos de dominio y las relaciones entre ellos</strong>.</p>
<h2 id="dónde-debe-estar-la-lógica-de-dominio-en-el-frontend"><a class="anchor" href="#dónde-debe-estar-la-lógica-de-dominio-en-el-frontend">¿Dónde debe estar la lógica de dominio en el frontend?</a></h2>
<p>Terminadas las definiciones, pasemos a la práctica. ¿<strong>Dónde</strong> debe estar la lógica de dominio en el frontend?</p>
<p><a href="https://khalilstemmler.com/about/" target="_blank" rel="noopener noreferrer">Khalil Stemmler</a>, muy interesado en el diseño de software, sostuvo al principio que «la lógica de negocio no pertenece al frontend», pero más adelante revisó su postura y afirmó que «casi todo lo que hacemos arquitectónicamente en el backend también podemos y debemos hacerlo en el frontend».</p>
<p>Estoy de acuerdo. Por supuesto, el frontend no debe convertirse en la <strong>única fuente de verdad (Single Source of Truth)</strong> de la lógica de negocio. Ese es el papel del backend. Pero en el frontend también existe, sin duda, <strong>lógica de dominio propia del frontend</strong>.</p>
<p>Pensemos en un caso en el que «hay que mostrar en tiempo real la devolución estimada en función de la información introducida por el usuario». Si esta lógica de cálculo solo existe en el backend, habría que llamar a la API cada vez que el usuario corrigiese un solo dígito del importe de sus ingresos. La UI se detendría durante todo el viaje de ida y vuelta por la red y, si el usuario escribe rápido, se dispararía una cantidad enorme de peticiones innecesarias. Incluso con debounce, un retraso de varios cientos de milisegundos basta para romper la experiencia de una «vista previa en tiempo real». <strong>En última instancia, el frontend no tiene más remedio que realizar directamente los cálculos que requieren una respuesta inmediata, por lo que existe lógica que solo puede ejecutarse en el frontend.</strong></p>
<h3 id="cuando-la-lógica-de-dominio-se-mezcla-con-el-componente"><a class="anchor" href="#cuando-la-lógica-de-dominio-se-mezcla-con-el-componente">Cuando la lógica de dominio se mezcla con el componente</a></h3>
<p>Tomemos como ejemplo una pantalla de vista previa del impuesto sobre la renta global. Cuando el usuario introduce la información sobre sus ingresos, se muestra en tiempo real la cuota estimada. Este es un ejemplo habitual de código en el que se mezclan la lógica de dominio y la lógica de UI.</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>¿Se aprecia el problema? Las <strong>reglas de negocio fijadas por la legislación fiscal</strong> —«deducción personal de 1,5 millones de wones por persona», «ocho tramos impositivos progresivos» y «retención del 3,3 %»— están incrustadas directamente en un componente de React. La legislación fiscal cambia cada año; si estas reglas están dispersas por los componentes, cada reforma obliga a buscar todos los lugares que hay que modificar. Si además existen escenarios E2E gestionados por el equipo de QA, el coste de las pruebas tampoco será menor.</p>
<p>Al final, resulta difícil distinguir la lógica de vista de la lógica de negocio, y el código acaba enredado entre innumerables condicionales y hooks personalizados.</p>
<h3 id="separemos-la-lógica-de-dominio"><a class="anchor" href="#separemos-la-lógica-de-dominio">Separemos la lógica de dominio</a></h3>
<p>Tomemos prestado un principio central del enfoque de Clean Architecture de Alex Bespoyasov: separar la lógica de dominio en <strong>funciones puras que no dependan de ningún framework</strong>.</p>
<blockquote class="bilingual-quote"><div class="quote-translation" lang="ko"><p>El dominio es el núcleo que distingue una aplicación de otra. Puede entenderse como aquello que no cambiaría si migrásemos de React a 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>Refactoricemos el ejemplo anterior del cálculo de impuestos.</p>
<p>Primero definimos los tipos y las reglas del dominio para cohesionar la información relacionada.</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>A continuación, separamos la lógica de dominio en funciones puras.</p>
<p>Separamos en la función <code>computeFullTax</code> la lógica anterior para calcular los ingresos, las deducciones, la base imponible, la cuota y la devolución. A su vez, dividimos cada etapa en pequeñas funciones puras. Si inferimos el tipo del resultado con <code>ReturnType&#x3C;typeof computeFullTax></code>, no hace falta declarar otra interfaz.</p>
<p>Después, el componente se limita a «usar» la lógica de dominio.</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>¿Qué ha cambiado?</p>
<ul>
<li>La <strong>tabla de ocho tramos impositivos progresivos</strong> (<code>TAX_BRACKETS</code>) está reunida en un único lugar, por lo que, cuando cambie la legislación fiscal, solo habrá que modificar <code>domain/tax.ts</code>.</li>
<li>El <strong>flujo de cálculo</strong> está cohesionado en una sola función, <code>computeFullTax</code>, lo que permite ver de un vistazo el proceso completo. (Se ha agrupado para simplificar el ejemplo, pero en un proyecto real conviene dividirlo más según su propósito: cálculo de ingresos, cálculo de deducciones, cálculo de la cuota, etc.).</li>
<li>El <strong>componente se concentra únicamente en «cómo mostrarlo»</strong>. Aunque cambien los tipos impositivos, no hace falta modificar el componente.</li>
<li>Aunque se migre de React a otro framework, <code>domain/tax.ts</code> <strong>no cambia</strong>.</li>
</ul>
<p>Cuando se separa la lógica de dominio, las pruebas se vuelven sorprendentemente sencillas. Esto es especialmente importante en el dominio fiscal, donde <strong>la precisión de los cálculos es, literalmente, el dinero del usuario</strong>.</p>
<p>Las funciones puras que contienen cálculos fiscales no necesitan React Testing Library, ni <code>render</code>, ni <code>screen.getByText</code>. Basta con proporcionar una entrada y comprobar la salida. Casos como «tipo del 6 % hasta 14 millones de wones», «si la base imponible es 0 wones, la cuota también es 0» o «devolución para un autónomo con 30 millones de wones de ingresos» pueden expresarse con un <code>it</code> de una sola línea. Las pruebas unitarias del dominio establecen de forma natural el criterio para separar componentes y, además, el código de prueba actúa como documentación.</p>
<h2 id="modelo-de-dominio-anémico-anemic-domain-model"><a class="anchor" href="#modelo-de-dominio-anémico-anemic-domain-model">Modelo de dominio anémico (Anemic Domain Model)</a></h2>
<p>En el apartado anterior separamos la <strong>lógica de cálculo</strong>. Sin embargo, la lógica de dominio también incluye <strong>reglas de transición de estado</strong> y <strong>comprobaciones de permisos</strong>. Preguntas como «¿se puede editar ahora esta declaración?», «¿se puede presentar?» o «¿se puede cambiar el tipo de reclamación?» pertenecen a esta categoría. Al separar estas reglas es fácil caer en una trampa que Martin Fowler denominó <strong>modelo de dominio anémico (Anemic Domain Model)</strong>.</p>
<p>Un modelo de dominio anémico es aquel en el que <strong>los tipos están bien definidos en el lenguaje del dominio, pero las reglas que operan sobre ellos se han dispersado fuera del dominio</strong>. Veamos como ejemplo el dominio de una declaración de impuestos (Filing). El tipo está limpio.</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>Pero las reglas para decidir y realizar transiciones sobre este tipo están incrustadas en otros 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>La misma regla de dominio existe con formas distintas en tres lugares: utils, un componente y un hook. Si en este estado llega un requisito como «cambian las condiciones para presentar una reclamación», habrá que recorrer el código en busca de todos los lugares que deben modificarse, y cualquier omisión provocará una decisión incorrecta en alguna parte del sitio. Fowler criticó este tipo de código por ser <strong>«poco más que código procedimental revestido con una apariencia orientada a objetos»</strong>.</p>
<p>La solución es la misma que aplicamos a la lógica de cálculo en el apartado anterior: <strong>poner las reglas junto al 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>Ahora las reglas relacionadas con las declaraciones se gestionan en un único lugar, <code>domain/filing.ts</code>. Cualquier componente puede llamar a <code>canAmend(filing)</code> y, si cambia una regla, basta con modificar este archivo. La clave es <strong>entender el tipo y las reglas que operan sobre él como una sola unidad</strong>. Dejar solo el tipo en la carpeta del dominio y extraer las reglas a utils es una separación parcial que puede parecer limpia, pero sigue siendo anémica.</p>
<h2 id="capa-de-transformación-entre-la-respuesta-de-la-api-y-el-modelo-de-dominio"><a class="anchor" href="#capa-de-transformación-entre-la-respuesta-de-la-api-y-el-modelo-de-dominio">Capa de transformación entre la respuesta de la API y el modelo de dominio</a></h2>
<p>En un proyecto real hay que tener en cuenta otra cuestión: la estructura de las respuestas de la API del backend no siempre coincide con el modelo de dominio del frontend. Esto es aún más cierto en un servicio fiscal conectado con organismos públicos. Los datos de integración con Hometax, el portal de la Agencia Tributaria Nacional de Corea, están llenos de abreviaturas y códigos, por lo que es poco probable que lleguen con la misma forma que el modelo de dominio del frontend.</p>
<p>Para eso necesitamos una <strong>capa de transformación (Mapper)</strong>. En lugar de llevar el tipo de la respuesta de la API directamente hasta el componente, primero lo depuramos y lo convertimos en un tipo de dominio. Basta con una función pura.</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>De este modo, las abreviaturas y clasificaciones basadas en códigos de la respuesta de la API, como <code>총수입금액</code> y <code>경비율</code>, se transforman <strong>en un único lugar</strong> para adaptarlas al dominio del frontend. Los valores que deben desplegarse como enum, como el código del tipo de ingreso, pueden resolverse con una pequeña lookup table dentro del mapper. Aunque cambien los nombres de los campos de la API de Hometax, solo habrá que modificar el mapper.</p>
<h2 id="funciones-utilitarias-y-lógica-de-dominio"><a class="anchor" href="#funciones-utilitarias-y-lógica-de-dominio">Funciones utilitarias y lógica de dominio</a></h2>
<p>Al separar la lógica de dominio surge inevitablemente una pregunta: <strong>«¿esto no es una función utilitaria?»</strong></p>
<p>Veamos, por ejemplo, estas dos funciones.</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> es pura <strong>lógica de presentación (Presentation)</strong> que transforma un número en texto. Añadir la unidad «won» y los separadores de miles no es una regla de negocio, sino una decisión sobre cómo mostrar el valor al usuario. En cambio, <code>calculateTax</code> contiene una <strong>regla de negocio basada en la legislación fiscal</strong>: «aplicar ocho tramos impositivos progresivos». Es una regla del dominio que debe aplicarse de la misma forma incluso sin UI.</p>
<p>Este es el criterio que uso en la práctica.</p>
<blockquote>
<p><strong>Si esta lógica desaparece, ¿se rompe el negocio o solo se rompe la pantalla?</strong></p>
</blockquote>
<p>Si se rompe el negocio, es lógica de dominio; si solo se rompe la pantalla, es lógica de presentación. Esta pregunta permite trazar la mayoría de los límites.</p>
<table>
<thead>
<tr>
<th>Criterio</th>
<th>Lógica de dominio</th>
<th>Lógica utilitaria/de presentación</th>
</tr>
</thead>
<tbody>
<tr>
<td>¿Qué se rompe si falta?</td>
<td>El cálculo de impuestos</td>
<td>La pantalla (UI) se ve mal</td>
</tr>
<tr>
<td>¿Qué ocurre si cambia el framework?</td>
<td>Se conserva</td>
<td>Puede cambiar</td>
</tr>
<tr>
<td>¿Está especificada como requisito?</td>
<td>«Base imponible × tipo − deducción progresiva»</td>
<td>«Los importes llevan separadores»</td>
</tr>
<tr>
<td>¿Existe la misma lógica en el backend?</td>
<td>Existe o debería existir</td>
<td>No (solo concierne al frontend)</td>
</tr>
</tbody>
</table>
<p>Pero la realidad no es tan limpia. El caso más difícil es el de la <strong>lógica que parece de dominio, pero en realidad es de presentación</strong>.</p>
<p>Veamos el siguiente código. Como recibe un concepto de dominio llamado FilingStatus, se ha clasificado como lógica de dominio. Pero ¿lo es 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>Aunque <code>getStatusBadgeColor</code> y <code>getStatusDisplayText</code> usan el concepto de dominio <code>FilingStatus</code>, su función es la <strong>presentación en pantalla</strong>. Si cambia el color de la insignia, el negocio no se rompe en absoluto. Si ponemos estas funciones en <code>domain/filing.ts</code>, el módulo de dominio crecerá cada vez más y la auténtica lógica de dominio acabará mezclada con la lógica de presentación.</p>
<h3 id="separar-el-modelo-de-dominio-y-el-viewmodel"><a class="anchor" href="#separar-el-modelo-de-dominio-y-el-viewmodel">Separar el modelo de dominio y el ViewModel</a></h3>
<p>Hay una forma práctica de resolver este problema: <strong>separar el ViewModel en un archivo distinto dentro de la misma carpeta del dominio</strong>. En lugar de <code>.ui.ts</code>, usar el nombre <code>.viewModel.ts</code> enlaza de forma natural con el concepto de ViewModel del patrón MVVM. El propio nombre deja claro su papel como «capa que transforma los datos de dominio para adaptarlos a la pantalla».</p>
<pre><code>domains/
└── filing/
    ├── filing.ts              # 순수 도메인 모델 + 도메인 로직
    ├── filing.viewModel.ts    # ViewModel (표현 변환 계층)
    ├── filing.test.ts         # 도메인 로직 테스트
    └── filingMapper.ts        # API ↔ 도메인 변환
</code></pre>
<p>Movemos tal cual <code>getStatusBadgeColor</code> y <code>getStatusDisplayText</code> a <code>filing.viewModel.ts</code>. También reunimos aquí transformaciones como <code>getFilingTypeLabel(type: FilingType): string</code>, que convierte los tipos de declaración en etiquetas coreanas. <code>filing.ts</code> se ocupa solo de las reglas de negocio y <code>filing.viewModel.ts</code>, solo de su presentación en pantalla.</p>
<p>La clave es la <strong>dirección de las dependencias</strong>. <code>filing.viewModel.ts</code> importa <code>filing.ts</code>, pero <code>filing.ts</code> nunca importa <code>filing.viewModel.ts</code>. El dominio no conoce la presentación; la presentación sí conoce el dominio. Puede verse como una versión reducida de la regla de dependencias (Dependency Rule) de Robert C. Martin.</p>
<p>He colocado en la misma carpeta los archivos que cambian juntos porque considero que deben estar en el mismo directorio. Si se añade un valor nuevo al tipo <code>FilingStatus</code> —por ejemplo, <code>'rejected'</code>—, habrá que modificar tanto <code>filing.ts</code> como <code>filing.viewModel.ts</code>. Al encontrarse en la misma carpeta, el alcance del cambio resulta evidente.</p>
<h2 id="límites-y-cohesión"><a class="anchor" href="#límites-y-cohesión">Límites y cohesión</a></h2>
<p>Tan importante como separar la lógica de dominio es decidir <strong>dónde trazar los límites</strong>. Estos son algunos de los problemas al definir límites que encuentro con frecuencia en proyectos reales.</p>
<p>Los datos que maneja un frontend proceden, a grandes rasgos, de cuatro fuentes.</p>
<ul>
<li><strong>Datos del servidor</strong>: recibidos como respuesta de una API.</li>
<li><strong>Datos derivados</strong>: calculados a partir de los datos del servidor.</li>
<li><strong>Estado de la UI</strong>: destinado a controlar la pantalla y las interacciones del usuario.</li>
<li><strong>Entrada del usuario</strong>: datos que se están introduciendo en un formulario.</li>
</ul>
<p>Si mezclamos los cuatro en un único tipo, contaminamos el modelo de dominio.</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>Este tipo mete en el mismo recipiente conceptos de dominio, estado de la UI y datos temporales. Cada vez que cambia <code>activeStep</code>, es como si se actualizase el dominio de la declaración. (Cambiar de paso en un formulario no es un evento de negocio).</p>
<p>La mejora consiste en separar los tipos según sus límites. El <strong>modelo de dominio</strong> solo contiene conceptos de negocio como <code>id</code>, <code>status</code> y <code>determinedTax</code>; el <strong>estado de la UI</strong> (<code>FilingFormViewState</code>) contiene únicamente controles de pantalla como <code>isExpanded</code> y <code>activeStep</code>; y el <strong>estado del formulario</strong> (<code>DeductionEditForm</code>), solo los datos temporales que se están introduciendo.</p>
<p>Así, cada tipo tiene <strong>un único motivo para cambiar</strong>. Los tipos de dominio solo se modifican cuando cambia la legislación fiscal; el estado de la UI, cuando cambia el diseño de la pantalla; y el estado del formulario, cuando cambia la UX de entrada.</p>
<h3 id="mantengamos-juntas-las-cosas-que-cambian-juntas"><a class="anchor" href="#mantengamos-juntas-las-cosas-que-cambian-juntas">Mantengamos juntas las cosas que cambian juntas</a></h3>
<p>En el DDD de Eric Evans existe el concepto de <strong>Aggregate (agregado)</strong>: «tratar un grupo de objetos relacionados como una sola unidad». No hace falta aplicarlo literalmente en el frontend, pero sí merece la pena adoptar su principio central: <strong>mantener juntos los datos y las reglas que cambian juntos</strong>.</p>
<p>En un servicio fiscal, por ejemplo, <code>Income</code> (ingresos) y <code>ExpenseRate</code> (coeficiente de gastos) siempre cambian juntos. Si cambia el tipo de ingreso, también cambia el coeficiente aplicable y se ve afectado el cálculo de la renta global. Por tanto, conviene cohesionarlos en un solo archivo, <code>domain/tax.ts</code>.</p>
<p>En cambio, <code>TaxFiling</code> (declaración) puede cambiar con independencia del cálculo de la cuota. Aunque cambien las reglas de transición de estado de una declaración, la lógica para calcular los tipos impositivos no se ve afectada. Por tanto, es correcto separarla en <code>domain/filing.ts</code>.</p>
<pre><code>이렇게 묻자: "A가 변할 때 B도 반드시 변해야 하는가?"
  → Yes: 같은 모듈에 둔다 (Income + ExpenseRate + TaxBracket)
  → No: 분리한다 (Tax 계산 ↔ Filing 상태관리)
</code></pre>
<h2 id="class-frente-a-estilo-funcional"><a class="anchor" href="#class-frente-a-estilo-funcional">Class frente a estilo funcional</a></h2>
<p>Llegados a este punto puede surgir una pregunta fundamental. Todos los ejemplos anteriores combinan <code>interface</code> y funciones puras; ¿no sería más natural expresar el dominio con Class para lograr una mayor cohesión?</p>
<p>Es cierto. Cuando se expresa un dominio mediante Class, los datos y el comportamiento quedan agrupados en un mismo objeto, por lo que la cohesión se hace visible directamente en la estructura del 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>Con Class, el comportamiento pertenece a los datos y el sujeto queda claro allí donde se utiliza. <code>filing.canAmend()</code> resulta intuitivo, como una frase en lenguaje natural. El sujeto (filing) y el verbo (canAmend) están unidos de forma explícita. Del mismo modo, al escribir <code>jihoon.eat('감자탕')</code> se lee de inmediato que «Jihoon come gamjatang».</p>
<p>En el estilo funcional, en cambio, queda así.</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>En el enfoque funcional, los datos existen fuera de la función. Las dos líneas anteriores reciben <code>filing</code> como argumento y ejecutan alguna operación. La función <code>eat</code> recibe <code>jihoon</code> y <code>감자탕</code> como datos y realiza la acción.</p>
<p>Como resultado, la relación entre sujeto y verbo es más débil. Para saber que la función <code>canAmend</code> está relacionada con <code>TaxFiling</code>, hay que abrir el archivo o consultar su firma de tipos. Si en un mismo archivo se mezclan funciones como <code>canAmend(filing)</code>, <code>canEdit(filing)</code> y <code>calculateTax(taxableBase)</code>, puede resultar difícil saber de un vistazo a qué dominio pertenece cada una.</p>
<h3 id="entonces-deberíamos-usar-class"><a class="anchor" href="#entonces-deberíamos-usar-class">Entonces, ¿deberíamos usar Class?</a></h3>
<p>Sinceramente, la respuesta es <strong>«depende de la situación»</strong>. Sin embargo, según mi experiencia, hay razones prácticas por las que Class no es una solución universal en un entorno React + TypeScript.</p>
<p><strong>1. Fricción con la gestión del estado de React</strong></p>
<p>La gestión del estado de React encaja de forma más natural con <strong>Plain Object</strong>. Aunque <code>useState</code> y <code>useReducer</code> pueden contener técnicamente cualquier valor, y Redux DevTools no elimina por sí mismo el prototipo de una instancia de Class, cuando el middleware de persistencia de Redux/Zustand guarda y restaura el estado como JSON, una instancia de Class pierde sus métodos y su prototipo en el ciclo <code>JSON.stringify</code> → <code>JSON.parse</code> y queda reducida a un plain object. El límite de props entre React Server Component y Client Component impone una restricción distinta: solo admite valores serializables (serializable) compatibles, por lo que una instancia arbitraria de Class no puede atravesarlo.</p>
<p>Veamos el siguiente 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">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>Actualizar el estado de React no hace que <code>filing</code> deje de ser una instancia de <code>TaxFilingModel</code>. Sin embargo, si la persistencia de Redux/Zustand lo guarda y restaura como JSON, el valor recuperado puede ser un plain object sin métodos, y una llamada desprevenida a <code>filing.canAmend()</code> puede provocar un error en tiempo de ejecución. Al pasarlo de React Server Component a Client Component, el fallo ocurre antes, porque una instancia de Class no es un valor de props serializable compatible.</p>
<p><strong>2. Dificultad para garantizar la inmutabilidad</strong></p>
<p>React detecta los cambios de estado mediante <strong>comparación de referencias (referential equality)</strong>. Si un método de una instancia de Class modifica internamente el estado con algo como <code>this.items.push(...)</code>, la referencia no cambia y React no activa un nuevo renderizado. Así que <code>addDeduction(item)</code> tendría que devolver siempre una instancia nueva, por ejemplo con <code>return new DeductionList([...this.items, item])</code>; pero entonces se diluye la ventaja de Class de «modificar un estado encapsulado». El resultado no es muy distinto de una actualización funcional.</p>
<h3 id="estrategias-para-lograr-cohesión-con-el-estilo-funcional"><a class="anchor" href="#estrategias-para-lograr-cohesión-con-el-estilo-funcional">Estrategias para lograr cohesión con el estilo funcional</a></h3>
<p>Entonces, ¿cómo podemos mejorar en el estilo funcional el problema de cohesión débil que vemos en <code>eat('jihoon', '감자탕')</code>? Estas son tres estrategias que me han resultado eficaces.</p>
<p><strong>1. Cohesionar mediante un namespace de módulo</strong></p>
<p>Es la opción más intuitiva. Se convierte el propio archivo —el módulo— en una unidad de dominio y se usa un namespace al importarlo. Podemos reutilizar tal cual el archivo <code>domain/filing.ts</code> definido antes.</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>Aunque <code>FilingModel.canAmend(filing)</code> no llega a ser tan directo como <code>filing.canAmend()</code>, el código deja claro al menos que la función pertenece al dominio Filing. También desaparece el riesgo de mezclar funciones de varios dominios.</p>
<p><strong>2. Usar siempre como primer argumento el sujeto del dominio</strong></p>
<p>Existe otra convención para expresar cohesión en el estilo funcional: <strong>colocar siempre como primer argumento al «sujeto de la acción»</strong>. Si unificamos las firmas como <code>canAmend(filing)</code> y <code>calculateTotalIncome(income)</code>, <code>canAmend(filing)</code> se lee como «consultar canAmend sobre filing». Es coherente con la forma de pensar de las pipelines de Unix (<code>data |> transform</code>). De hecho, el receptor de métodos de Go sigue exactamente este patrón, al igual que el bloque <code>impl</code> de Rust cuando recibe <code>self</code> como primer argumento.</p>
<p><strong>3. Agrupar el comportamiento con una función de creación de objetos de dominio (Factory)</strong></p>
<p>Este patrón resulta útil cuando echamos de menos la cohesión de Class. Una función factory devuelve a la vez el objeto de dominio y su comportamiento.</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>Este patrón combina la expresividad de Class (<code>filing.canAmend()</code>) con la practicidad de componer comportamiento mediante un objeto literal. Como el objeto devuelto contiene propiedades que son funciones, no es en sí mismo un dato serializable como JSON. También tiene el coste de crear cada vez nuevos objetos de función, pero rara vez supone un problema de rendimiento con el volumen de datos habitual en frontend.</p>
<h2 id="hasta-dónde-debemos-separar"><a class="anchor" href="#hasta-dónde-debemos-separar">¿Hasta dónde debemos separar?</a></h2>
<p>Al leer sobre Clean Architecture encontramos estructuras ideales con tres o cuatro capas y definiciones de Port/Adapter. Sin embargo, aplicar esta estructura a todos los proyectos puede convertirse en sobreingeniería (over-engineering).</p>
<p>Estos son, en mi opinión, unos criterios prácticos.</p>
<ul>
<li><strong>Separar los tipos de dominio de los tipos de respuesta de la API</strong>. Ya sea con <code>interface</code> o con <code>type</code>, se definen en un archivo aparte los conceptos de dominio que usa el frontend.</li>
<li><strong>Extraer de los componentes la lógica que contenga reglas de negocio</strong>. No es imprescindible que esté en una carpeta <code>domain/</code>. Lo importante es convertirla en funciones puras que no dependan de React.</li>
<li><strong>Transformar la respuesta de la API en el modelo de dominio en un único lugar</strong>. Ya sea mediante una función Mapper o un esquema de Zod, se crea una estructura en la que basta modificar ese punto para evitar que el cambio se propague.</li>
</ul>
<p>Si el proyecto gana complejidad, también pueden plantearse estas opciones.</p>
<ul>
<li><strong>Dividir las carpetas por Bounded Context</strong>. El <a href="https://frontend-fundamentals.com/" target="_blank" rel="noopener noreferrer">capítulo de frontend de Toss</a> también recalca el principio de «colocar en el mismo directorio los archivos que cambian juntos». Al dividir las carpetas por dominios, las rutas de importación revelan de forma natural sus límites.</li>
<li><strong>Introducir una capa de Use Case</strong>. Cuando la combinación de lógica de dominio se vuelve compleja, hace falta una capa Application que reúna en una sola función un escenario como «consultar información de ingresos → aplicar el coeficiente de gastos → calcular las deducciones → calcular la cuota → determinar la devolución».</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>Incluso dentro de un único dominio fiscal, el <strong>cálculo de la cuota (tax)</strong>, la <strong>gestión de declaraciones (filing)</strong> y las <strong>deducciones (deduction)</strong> se dividen en subdominios independientes. Aunque cambien los tipos impositivos, no se ven afectadas las reglas de transición de estado de las declaraciones; aunque se añadan deducciones, el flujo para presentar una declaración permanece intacto. Esta es una aplicación práctica de Bounded Context.</p>
<h2 id="conclusión"><a class="anchor" href="#conclusión">Conclusión</a></h2>
<p>En resumen, el <strong>dominio</strong> es el área problemática que queremos resolver; el <strong>modelo de dominio</strong> es el sistema conceptual que abstrae de forma selectiva ese problema; el <strong>modelo de objetos de dominio</strong> es la implementación en código de ese sistema conceptual; y el <strong>objeto de dominio</strong> es cada objeto individual de esa implementación.</p>
<p>Llevar estos conceptos a la práctica en frontend no consiste simplemente en dividir carpetas, sino en <strong>evaluar conscientemente varias capas de límites</strong>. «¿Esto es una regla de negocio o lógica de presentación?», «¿estos datos son estado de dominio o estado de la UI?», «¿esta función tiene suficiente cohesión?». El mero hábito de formularse estas preguntas mejora de forma natural la estructura del código.</p>
<p>Por supuesto, no todos los proyectos necesitan todas las capas de Clean Architecture. Dividir una aplicación CRUD sencilla en cuatro capas y aplicar el patrón Factory a todos los dominios sería matar moscas a cañonazos. Entre la elegante cohesión de Class y la flexibilidad práctica del estilo funcional, la respuesta depende de la complejidad del proyecto y del contexto del equipo.</p>
<p>No hay una única respuesta correcta. Pero existe una diferencia clara entre <strong>«escribir código sin saber qué es el dominio»</strong> y <strong>«reconocer el dominio, evaluar sus límites y separarlo conscientemente»</strong>. Espero que quienes lean este artículo se pregunten al menos una vez en sus propios proyectos: «¿cuál es aquí el dominio y dónde debería estar este código?».</p>
<h3 id="referencias"><a class="anchor" href="#referencias">Referencias</a></h3>
<details class="ref-block"><summary class="ref-summary">참고 자료</summary><div class="ref-list">
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#f59e0b"></span><a href="https://www.amazon.com/Domain-Driven-Design-Tackling-Complexity-Software/dp/0321125215" target="_blank" rel="noopener noreferrer">Eric Evans, Domain-Driven Design (Book)</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#f59e0b"></span><a href="https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html" target="_blank" rel="noopener noreferrer">Robert C. Martin, Clean Architecture</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#f59e0b"></span><a href="https://khalilstemmler.com/articles/typescript-domain-driven-design/ddd-frontend/" target="_blank" rel="noopener noreferrer">Khalil Stemmler, Does DDD Belong on the Frontend?</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#f59e0b"></span><a href="https://bespoyasov.me/blog/clean-architecture-on-frontend/" target="_blank" rel="noopener noreferrer">Alex Bespoyasov, Clean Architecture on Frontend</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#f59e0b"></span><a href="https://toss.tech/article/income-qa-e2e-automation" target="_blank" rel="noopener noreferrer">토스, E2E 자동화 여정</a></div>
</div>
</div></details>]]></content:encoded>
            <author>jihoon7705@gmail.com (이지훈)</author>
            <category>프론트엔드</category>
            <category>아키텍처</category>
            <category>DDD</category>
        </item>
        <item>
            <title><![CDATA[Reflexiones sobre la refactorización del segundo simulacro de Toss Frontend Fundamentals]]></title>
            <link>https://hooninedev.com/es/260328</link>
            <guid isPermaLink="false">https://hooninedev.com/es/260328</guid>
            <pubDate>Sat, 28 Mar 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[En este artículo quiero hablar de mi experiencia de refactorización al participar en el segundo simulacro de Toss Frontend Fundamentals. Como siempre me han interesado las revisiones de código y la re...]]></description>
            <content:encoded><![CDATA[<p>En este artículo quiero hablar de mi experiencia de refactorización al participar en el segundo simulacro de Toss Frontend Fundamentals.</p>
<p>Como siempre me han interesado las revisiones de código y la refactorización, decidí afrontar este interesante ejercicio publicado por Toss bajo el formato de simulacro de Frontend Fundamentals. El ejercicio consistía en refactorizar una aplicación de reserva de salas de reuniones. También incluía tests, por lo que contaba con una red de seguridad para comprobar que ninguna funcionalidad se rompiera durante el proceso.</p>
<p>Al final, dediqué dos días a la refactorización y quiero resumir lo que aprendí durante el proceso.</p>
<h2 id="el-primer-encuentro-con-el-código"><a class="anchor" href="#el-primer-encuentro-con-el-código">El primer encuentro con el código</a></h2>
<p>Lo primero que hice al abrir el código fue <strong>leer las especificaciones de los tests</strong>. Los tests son la documentación que explica con mayor honestidad qué debe hacer una aplicación. Revisé <code>App.easy.spec.tsx</code> y <code>App.hard.spec.tsx</code> para comprender todos sus requisitos.</p>
<p>Después examiné el código real y encontré dos componentes monolíticos.</p>
<ul>
<li><code>ReservationStatusPage</code> era un componente de unas 400 líneas que reunía en un único archivo la selección de fecha, la visualización de la línea temporal, el tooltip con los detalles de la reserva, la lista de mis reservas y la función de cancelación.</li>
<li><code>RoomBookingPage</code> era un componente de unas 300 líneas en el que se entrelazaban los filtros, la lista de salas, la lógica de creación de reservas y la sincronización de los parámetros de la URL.</li>
</ul>
<p>Mientras leía el código, antes de decidir que «había que mejorarlo», me centré en <strong>clasificar sus características</strong>: qué partes contenían información de dominio, cuáles tenían carácter de utilidad y cuáles pertenecían exclusivamente a la capa 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>Esta clasificación hizo que empezara a resultar evidente por dónde debía comenzar. Añadí comentarios breves en cada área del código para anotar posibles mejoras. (Me recordó a cuando entré en mi empresa actual y tuve que migrar un proyecto basado en jquery).</p>
<p>Entonces, ¿por dónde debía empezar?</p>
<h2 id="definir-la-estrategia-de-refactorización"><a class="anchor" href="#definir-la-estrategia-de-refactorización">Definir la estrategia de refactorización</a></h2>
<p>Decidí llevar a cabo la refactorización en el siguiente orden.</p>
<ol>
<li><strong>Gestión del código de servidor</strong>: separar queries y mutations</li>
<li><strong>Separación de la lógica de dominio</strong>: modelos Equipment, Room y Reservation</li>
<li><strong>Declaración de tipos</strong>: organizar el sistema de tipos a partir de los modelos de dominio</li>
<li><strong>Separación de funciones de utilidad</strong>: formato de fechas, cálculos de la línea temporal, etc.</li>
<li><strong>Separación de la capa de UI</strong>: dividir los componentes en unidades manejables según sus responsabilidades</li>
<li><strong>Abstracción y separación de responsabilidades</strong>: gestión de errores/carga y de query keys</li>
</ol>
<p>Elegí este orden para avanzar <strong>desde el exterior hacia el interior siguiendo la dirección de las dependencias</strong>. Primero organizaría la infraestructura —código de servidor y utilidades—, después establecería los modelos de dominio y, por último, puliría la UI. Si separaba primero los componentes de UI, podía acabar trasladando entre varios componentes una lógica de dominio y un código de queries que todavía no estaban organizados.</p>
<p>Con la estrategia definida, era hora de ponerla en práctica paso a paso.</p>
<h2 id="empezar-por-el-código-de-servidor-y-las-utilidades"><a class="anchor" href="#empezar-por-el-código-de-servidor-y-las-utilidades">Empezar por el código de servidor y las utilidades</a></h2>
<h3 id="separar-la-utilidad-de-formato-de-fecha"><a class="anchor" href="#separar-la-utilidad-de-formato-de-fecha">Separar la utilidad de formato de fecha</a></h3>
<p>Lo primero que modifiqué fue la función <code>formatDate</code>, porque estaba definida inline por separado y de forma idéntica en ambas 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>Aunque era un cambio pequeño, tenía un significado importante como primer commit de la refactorización. Era una especie de <strong>calentamiento</strong>: empezar por la parte más independiente y con menos efectos secundarios para comprobar que los tests siguieran pasando.</p>
<h3 id="separar-los-hooks-de-react-query"><a class="anchor" href="#separar-los-hooks-de-react-query">Separar los hooks de React Query</a></h3>
<p>A continuación, extraje a archivos independientes las llamadas a <code>useQuery</code> y <code>useMutation</code> que estaban escritas directamente dentro de los componentes. Utilicé el patrón <code>queryOptions</code> para convertir la configuración de las queries en unidades reutilizables.</p>
<p>Durante este proceso también definí de forma explícita los tipos de respuesta de la API que se encontraban en <code>remotes.ts</code>. Los tipos que antes se propagaban como <code>any</code> pasaron a estar claramente definidos como <code>GetRoomsResponse</code>, <code>GetReservationsResponse</code>, etc.</p>
<p>Una vez organizada la capa de infraestructura, podía centrarme en los modelos de dominio.</p>
<h2 id="separar-los-modelos-de-dominio"><a class="anchor" href="#separar-los-modelos-de-dominio">Separar los modelos de dominio</a></h2>
<p>El punto de inflexión más importante de la refactorización fue <strong>separar los modelos de dominio en un directorio <code>models/</code> independiente</strong>.</p>
<p>En el código original, constantes de negocio como <code>EQUIPMENT_LABELS</code> y <code>TIME_SLOTS</code> estaban declaradas al principio de los archivos de componentes. Los tipos de <code>Room</code> y <code>Reservation</code> solo existían en el handler del servidor (<code>_tosslib/server/types.ts</code>) y, en el código del cliente, se utilizaban prácticamente 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 qué es importante separar los modelos de dominio? Cuando la lógica de negocio depende de un componente de UI, para modificarla también hay que revisar la lógica de renderizado del componente. En cambio, si reside de forma independiente en el directorio <code>models/</code>, las reglas de negocio pueden modificarse por separado de la UI. Por supuesto, una separación perfecta es difícil de lograr en la práctica, pero lo esencial es crear, como mínimo, <strong>una estructura que permita predecir que «esta lógica estará aquí»</strong>.</p>
<p>Una vez separados los modelos de dominio, ¿hasta qué punto podía aligerarse la UI?</p>
<h2 id="descomponer-los-componentes"><a class="anchor" href="#descomponer-los-componentes">Descomponer los componentes</a></h2>
<h3 id="reservationstatuspage"><a class="anchor" href="#reservationstatuspage">ReservationStatusPage</a></h3>
<p>Este fue el commit que produjo el cambio más drástico y también el que más tiempo me llevó. Dividí el componente monolítico de 385 líneas de la siguiente manera.</p>
<pre><code>ReservationStatusPage/
├── index.tsx                    # 페이지 레벨
└── components/
    ├── DateSelector.tsx         # 날짜 선택 UI
    ├── ReservationTimeline.tsx  # 타임라인
    └── MyReservation.tsx        # 내 예약 목록 + 취소
</code></pre>
<p>El criterio para separar cada parte fue <strong>«¿tiene este código sentido de manera independiente?»</strong>. Visualizar la línea temporal es una responsabilidad independiente que recibe los datos de las reservas de una fecha y dibuja una cuadrícula. Consultar y cancelar las reservas del usuario también lo es. No había ningún motivo para que estuvieran en el mismo archivo.</p>
<p>Después de la separación, <code>index.tsx</code> quedó limitado al papel de <strong>orquestador (orchestrator)</strong>. Se encargaba de gestionar el estado, mostrar mensajes y componer los subcomponentes, mientras que delegaba en ellos el fetching de datos y los detalles de renderizado.</p>
<h3 id="roombookingpage"><a class="anchor" href="#roombookingpage">RoomBookingPage</a></h3>
<p>Dividí la página de reservas siguiendo el mismo principio.</p>
<pre><code>RoomBookingPage/
├── index.tsx                    # 페이지 레벨
├── components/
│   ├── BookingFilter.tsx        # 날짜, 시간, 인원, 장비, 층 UI
│   └── AvailableRoomList.tsx    # 예약 가능 방 목록
└── hooks/
    └── useBookingParams.ts      # URL searchParams 기반 상태 관리
</code></pre>
<p>Durante el proceso tomé una decisión interesante. Al principio intenté introducir <code>react-hook-form</code> + <code>zod</code> para validar el formulario. Sin embargo, finalmente los eliminé y los sustituí por el hook personalizado <code>useBookingParams</code>. Más adelante explicaré esta decisión con mayor detalle.</p>
<p>Llegados a este punto, surge una pregunta de forma natural: ¿hasta dónde debemos abstraer?</p>
<h2 id="el-nivel-adecuado-de-abstracción"><a class="anchor" href="#el-nivel-adecuado-de-abstracción">El nivel adecuado de abstracción</a></h2>
<p>Esta fue la parte sobre la que más reflexioné durante el simulacro.</p>
<h3 id="hasta-qué-punto-conviene-descomponer-las-condiciones-anidadas"><a class="anchor" href="#hasta-qué-punto-conviene-descomponer-las-condiciones-anidadas">¿Hasta qué punto conviene descomponer las condiciones anidadas?</a></h3>
<p>La lógica que determina si una sala se puede reservar combina varias condiciones: si la capacidad es suficiente, si cuenta con el equipamiento necesario, si está en la planta preferida y si existe un solapamiento horario. En el código original, todas ellas estaban escritas inline dentro de un único callback de <code>filter</code>.</p>
<p>Al extraer esta lógica a <code>models/roomFilter.ts</code>, separé cada condición en <strong>una función con nombre</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>La clave está en que <strong>solo separé en funciones aquello que podía recibir un nombre claro</strong>. Nombres como <code>isEnoughCapacity</code> y <code>hasRequiredEquipment</code> permiten predecir qué hace cada función sin mirar su implementación. Si el nombre tuviera que ser ambiguo, como <code>processRoomConditions</code>, la abstracción podría aumentar la carga cognitiva de quien lee el código.</p>
<p>Esto no significa, por supuesto, que sea la única respuesta correcta. Mi criterio fue <strong>«¿se puede predecir el comportamiento con solo leer el nombre de la función?»</strong>. Si es así, merece la pena abstraer; si no, dejarlo inline puede favorecer la legibilidad.</p>
<h3 id="searchparams-frente-al-estado-del-formulario"><a class="anchor" href="#searchparams-frente-al-estado-del-formulario">searchParams frente al estado del formulario</a></h3>
<p>También reflexioné bastante sobre dónde debía residir el estado de los filtros de reserva. En el código original, cada valor se gestionaba mediante <code>useState</code> y se sincronizaba con los searchParams de la URL mediante <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>Primero probé a introducir <code>react-hook-form</code> + <code>zod</code> para gestionarlos como un formulario. Sin embargo, finalmente los eliminé y los sustituí por el hook <code>useBookingParams</code>, que utiliza <strong>los searchParams como única fuente de verdad (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>El principal motivo de esta decisión fue considerar que <strong>«no conviene que estos estados evolucionen por separado»</strong>. Si tanto <code>useState</code> como <code>searchParams</code> mantienen su propio estado, pueden producirse discrepancias según el momento de la sincronización. En cambio, si solo se utilizan los searchParams como estado, la URL pasa a ser el estado de la aplicación y el problema de sincronización desaparece por completo. Además, si el usuario comparte la URL, se puede reproducir el mismo estado de los filtros.</p>
<p>Encontré reflexiones similares en los artículos de otros participantes: <strong>«unifiqué los searchParams de la URL como única fuente de verdad» y «opté por agrupar las props individuales de los filtros en un único objeto <code>filter</code>»</strong>. Aunque las soluciones se expresaban de forma distinta, partían del mismo diagnóstico: <strong>«los estados dispersos deben agruparse bajo un solo concepto»</strong>.</p>
<h2 id="estabilidad"><a class="anchor" href="#estabilidad">Estabilidad</a></h2>
<h3 id="suspense-y-errorboundary"><a class="anchor" href="#suspense-y-errorboundary">Suspense y ErrorBoundary</a></h3>
<p>Una vez definida la estructura de los componentes, añadí la gestión de errores y estados de carga. El orden es importante porque solo se puede decidir dónde establecer un Boundary después de definir el árbol de componentes.</p>
<p>Utilicé la librería <code>react-error-boundary</code> para envolver cada unidad independiente de fetching de datos con <code>ErrorBoundary</code> y <code>Suspense</code>. Aunque falle la línea temporal, la lista de mis reservas debe seguir mostrándose con normalidad, y viceversa.</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="gestión-centralizada-de-las-query-keys"><a class="anchor" href="#gestión-centralizada-de-las-query-keys">Gestión centralizada de las query keys</a></h3>
<p>Al separar los hooks de las queries durante la refactorización, surgió el problema de que las query keys quedaron dispersas en varios archivos. Esto dificultaba saber qué key debía utilizarse para la invalidation dentro del <code>onSuccess</code> de una mutation.</p>
<p>Introduje <code>@lukemorales/query-key-factory</code> para gestionar las query keys de forma centralizada.</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>Así se pueden utilizar con la forma <code>useSuspenseQueries({ queries: [roomKeys.list, reservationKeys.list(date)] })</code>, de modo que la query key y la función de fetching siempre se desplazan juntas. También extraje las rutas como constantes <code>PATHS</code> para eliminar strings hardcodeados.</p>
<h2 id="cuál-era-la-intención-de-quienes-diseñaron-el-ejercicio"><a class="anchor" href="#cuál-era-la-intención-de-quienes-diseñaron-el-ejercicio">¿Cuál era la intención de quienes diseñaron el ejercicio?</a></h2>
<p>Después de terminar la refactorización, tomé cierta distancia y reflexioné: ¿qué pretendía evaluar este simulacro?</p>
<p>Al leer los artículos de otros participantes, descubrí un punto en común interesante. En casi todos aparecía la frase <strong>«el código no se lee, se predice»</strong>. Nuestro cerebro no interpreta el código línea por línea, sino que lo lee anticipándose a partir de los patrones acumulados mediante la experiencia. Cuando esas predicciones fallan, la carga cognitiva aumenta drásticamente.</p>
<p>Desde esta perspectiva, el simulacro no evalúa simplemente la capacidad de programar, sino la capacidad de colaborar: <strong>«¿hasta qué punto puedes hacer que el código que leerán tus compañeros sea predecible?»</strong>. (Tal vez la auténtica habilidad de un ingeniero de software sea leer la mente tanto de quienes diseñan el ejercicio como de sus compañeros).</p>
<p>Al revisar las experiencias de otros participantes, me identifiqué con ideas como <strong>«no es fácil entender el código que ha escrito otra persona» y «es importante diseñar primero la interfaz, pero ese enfoque puede tambalearse ante una gran base de código existente»</strong>. Yo también viví algo parecido. Cuando el código existente ya funciona, surge la tentación de justificar su estructura: «Si ya funciona, ¿para qué cambiarlo?». Sin embargo, el objetivo central del simulacro era superar precisamente esa tentación y evaluar <strong>«con qué rapidez podría entender este código otra persona y si uno es capaz de analizar y resolver el problema con su propio criterio»</strong>.</p>
<h2 id="lo-que-aprendí-de-la-refactorización"><a class="anchor" href="#lo-que-aprendí-de-la-refactorización">Lo que aprendí de la refactorización</a></h2>
<p><strong>El orden de la refactorización determina el resultado.</strong> Avanzar desde el exterior —la infraestructura— hacia el interior —la UI— fue la ruta más segura para evitar enredos a mitad del proceso. Al dividir los componentes después de organizar las utilidades y los modelos de dominio, las dependencias de cada componente quedaron claras.</p>
<p><strong>El criterio para abstraer es el «nombre».</strong> Si al extraer algo a una función o variable su nombre puede explicar el comportamiento, merece la pena abstraerlo. Si el nombre será inevitablemente ambiguo, dejarlo inline puede ser una opción mejor.</p>
<p><strong>La ubicación del estado es la arquitectura.</strong> Los estados que deben evolucionar juntos tienen que residir en el mismo lugar. Desde un punto de vista estructural, es más sano utilizar únicamente los searchParams como fuente de verdad que sincronizar <code>useState</code> con <code>searchParams</code>.</p>
<h2 id="conclusión"><a class="anchor" href="#conclusión">Conclusión</a></h2>
<p>Después de terminar el ejercicio, conversé con dos compañeros. Al hablar y desarrollar mis ideas empezaron a surgir aspectos que no había visto mientras examinaba el código en solitario. En cuanto alguien pregunta «¿por qué lo hiciste así?» sobre una decisión estructural que yo había dado por sentada, aparecen lagunas de criterio de las que no era consciente.</p>
<p>Es cierto que la IA está reduciendo drásticamente el tiempo necesario para escribir y revisar código. Aun así, experiencias como esta son la razón por la que sigo considerando importantes las revisiones de código y las reuniones diarias. La IA puede comprobar la coherencia del código, pero señalar <strong>«esta es la perspectiva que has pasado por alto»</strong> sigue siendo responsabilidad de un compañero que comparte el mismo contexto. Descubrir lo que yo no había visto y estabilizar el producto gracias a ese descubrimiento: ¿no es esa la esencia de la colaboración?</p>
<p>No existe una única respuesta correcta al escribir código mientras resolvemos un problema. Otros participantes que hicieron el mismo simulacro eligieron caminos distintos, cada uno con sus propios motivos. Lo importante es <strong>poder explicar «por qué está escrito así»</strong>. Recomiendo a quienes lean este artículo que, al menos una vez, observen su código desde la perspectiva de alguien que lo ve por primera vez. Esa mirada puede convertirse en el criterio más poderoso para determinar la calidad del código.</p>]]></content:encoded>
            <author>jihoon7705@gmail.com (이지훈)</author>
            <category>프론트엔드</category>
            <category>React</category>
            <category>리팩토링</category>
        </item>
        <item>
            <title><![CDATA[Ingeniero frontend de IA]]></title>
            <link>https://hooninedev.com/es/260302</link>
            <guid isPermaLink="false">https://hooninedev.com/es/260302</guid>
            <pubDate>Mon, 02 Mar 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[En esta publicación quiero hablar, desde una perspectiva personal, de cómo pueden crecer y sobrevivir los ingenieros en la era de la IA. Uno de los textos que más me impresionó cuando era junior fue «...]]></description>
            <content:encoded><![CDATA[<p>En esta publicación quiero hablar, desde una perspectiva personal, de <strong>cómo pueden crecer y sobrevivir los ingenieros en la era de la IA</strong>.</p>
<p>Uno de los textos que más me impresionó cuando era junior fue <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">«Hoja de ruta profesional para ingenieros frontend: tres vías de especialización para perfiles junior», de Hwidong Bae</a>. El artículo organizaba la carrera de un ingeniero frontend en tres vías: <strong>especialización web (Software Engineer) / especialización en producto (Product Engineer) / especialización en operaciones (Full-Stack Engineer)</strong>, y abordaba además las «cinco competencias básicas de un ingeniero excelente» y los «tres puntos clave para convertirse en senior». En aquel entonces, la gran cuestión era decidir qué competencias desarrollar en cada vía. Sin embargo, ni siquiera habían pasado dos años desde que leí aquel texto cuando la propia cuestión cambió por completo.</p>
<p>Últimamente, cuando hablo con otros ingenieros, percibo que sus inquietudes tienen un tono algo distinto de las que venía oyendo durante los últimos años.</p>
<ul>
<li>«En la empresa hemos adoptado la IA y, si le damos un diseño, prácticamente lo construye todo. Es cómodo, pero...»</li>
<li>«El mercado laboral está realmente frío».</li>
<li>«Me da miedo hacer merge sin más del código generado por la IA, pero revisarlo línea por línea reduce la eficiencia. No sé qué hacer».</li>
</ul>
<p>Yo también pasé por una etapa parecida y todavía sigo en ella. Hace apenas uno o dos años veía la IA como «una buena herramienta de apoyo»; hoy, en cambio, hemos llegado a un entorno en el que cuesta imaginar el desarrollo sin IA (yo mismo le estoy pidiendo a Claude que investigue mientras escribo este artículo). Este texto pretende ser una especie de continuación del de Hwidong Bae: quiero ordenar, desde mi punto de vista, cómo ha cambiado el panorama desde entonces y qué competencias adicionales debemos desarrollar como ingenieros frontend en este nuevo escenario.</p>
<p>Una vez más, he intentado buscar y contrastar tantos datos como fuera posible, pero, dado que este campo cambia a una velocidad extraordinaria, pido de antemano comprensión si alguna parte ya ha quedado anticuada cuando se publique el artículo. Si hay algo que rebatir o debatir, no dudéis en dejarlo en los comentarios.</p>
<h2 id="pero-ahora-no-lo-hace-todo-la-ia"><a class="anchor" href="#pero-ahora-no-lo-hace-todo-la-ia">«¿Pero ahora no lo hace todo la IA?»</a></h2>
<p>Antes que nada, hay una cuestión que debemos aclarar. ¿Es cierta la frase «la IA lo hace todo»? ¿Hasta qué punto es verdad y a partir de dónde empieza la ilusión?</p>
<p>En febrero de 2025, <a href="https://x.com/karpathy/status/1886192184808149383" target="_blank" rel="noopener noreferrer">Andrej Karpathy</a>, cofundador de OpenAI y antiguo director de IA de Tesla, publicó esta frase en Twitter.</p>
<blockquote class="bilingual-quote"><div class="quote-translation" lang="ko"><p>Hay un nuevo tipo de programación al que llamo «vibe coding»: consiste en dejarse llevar por completo por las vibraciones, abrazar el crecimiento exponencial y olvidar incluso que el 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>El <strong>vibe coding</strong> es, en pocas palabras, «una forma de programar en la que se le entrega el teclado a la IA y uno se limita a describir en lenguaje natural lo que quiere». No hay documentos de arquitectura, ni boilerplate, ni búsquedas de puntos y comas. El código simplemente avanza siguiendo el vibe. En menos de un año, el término se asentó como parte del vocabulario habitual de la comunidad de desarrolladores anglófona.</p>
<p>Sin embargo, exactamente un año después, en febrero de 2026, el mismo Karpathy <a href="https://thenewstack.io/vibe-coding-is-passe/" target="_blank" rel="noopener noreferrer">dio un paso atrás</a>. Propuso sustituir la expresión vibe coding por <strong>«agentic engineering»</strong>. La diferencia entre ambos conceptos es clara.</p>
<ul>
<li><strong>Vibe coding</strong>: describir lo que se quiere y aceptar el resultado.</li>
<li><strong>Agentic engineering</strong>: diseñar el sistema, especificar las restricciones y utilizar la IA para acelerar una implementación cuyo razonamiento ya se ha completado mentalmente.</li>
</ul>
<p>Si hace un año el punto de partida era «basta con pedírselo y lo construye todo», ahora la propia «capacidad de diseñar qué pedirle a la IA y cómo hacerlo» se ha convertido en una competencia de ingeniería. Y esta corriente no se limita al tuit de una sola persona. Por esas mismas fechas, el ingeniero de Google <a href="https://addyosmani.com/" target="_blank" rel="noopener noreferrer">Addy Osmani</a> publicó el libro <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>, donde sentenció: «La IA no es más que un asistente, no un programador autónomo en el que se pueda confiar. Tú eres el desarrollador senior y el LLM existe para acelerar tu criterio».</p>
<h3 id="las-herramientas-avanzan-sin-freno"><a class="anchor" href="#las-herramientas-avanzan-sin-freno">Las herramientas avanzan sin freno</a></h3>
<p>El ecosistema de herramientas también evoluciona rápidamente en consonancia con esta corriente. A mayo de 2026, las herramientas de programación más mencionadas son Cursor, Claude Code, GitHub Copilot, Windsurf, v0 by Vercel, Bolt.new y Devin.</p>
<p>La evolución de v0 es especialmente simbólica. Vercel utiliza la expresión <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 el 90% del desarrollo real tiene lugar dentro de una base de código y una infraestructura ya existentes. Al principio bastaba con que v0 creara buenos prototipos greenfield; ahora importa repositorios de GitHub para trabajar directamente con ellos, aplica sistemas de diseño y obtiene automáticamente las variables de entorno de despliegue. En cierto modo, el ecosistema de herramientas está respondiendo directamente a la objeción de los perfiles senior: «¿La IA no sirve únicamente para crear demos de juguete?».</p>
<p>Las bases de código de las grandes tecnológicas son el mejor escaparate de este cambio.</p>
<p>Sundar Pichai, de Google, <a href="https://fortune.com/2024/10/30/googles-code-ai-sundar-pichai/" target="_blank" rel="noopener noreferrer">anunció en la presentación de resultados del tercer trimestre de octubre de 2024 que «más del 25% del código nuevo había sido generado por IA y después revisado y aprobado por ingenieros»</a>, y en abril de 2025 afirmó que la cifra había superado el 30%. Satya Nadella, de 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">reveló en LlamaCon, en abril de 2025, que «hasta el 30% de nuestro código está escrito por IA»</a>. En Meta, el objetivo interno ha llegado al nivel de que «para la primera mitad de 2026, el 65% de los ingenieros genere con IA más del 75% de sus commits».</p>
<p>En Corea la tendencia no es distinta. <a href="https://toss.tech/article/toss-frontend-ai-docs" target="_blank" rel="noopener noreferrer">Toss</a> construyó un sistema de documentación basado en IA para mejorar la DX y evitar que los desarrolladores tuvieran que buscar documentos, y fue aún más lejos al tratar temas como <a href="https://toss.tech/article/removing_designers_in_ai_era" target="_blank" rel="noopener noreferrer">«Qué ocurrió al eliminar a los diseñadores en la era de la IA»</a>. Daangn comparte experimentos de cada equipo todos los martes mediante <a href="https://medium.com/daangn" target="_blank" rel="noopener noreferrer">AI Show &#x26; Tell</a> y empezó a utilizar el <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">eslogan de contratación</a> «De ingeniero a builder». Woowa Brothers, por su parte, transmite mediante artículos como <a href="https://techblog.woowahan.com/22828/" target="_blank" rel="noopener noreferrer">«En una era en la que la IA escribe código, ¿aun así queréis ser desarrolladores?»</a> el mensaje de que «la esencia de un desarrollador no está en el código, sino en la capacidad de definir y resolver problemas».</p>
<h3 id="sin-embargo-las-cifras-cuentan-una-historia-algo-distinta"><a class="anchor" href="#sin-embargo-las-cifras-cuentan-una-historia-algo-distinta">Sin embargo, las cifras cuentan una historia algo distinta</a></h3>
<p>Si nos quedáramos solo con lo anterior, sería fácil concluir que «ahora basta con pedírselo y todo se resuelve». Pero los datos reales cuentan una historia algo distinta.</p>
<p>Veamos primero las cifras de la <a href="https://survey.stackoverflow.co/2025/ai" target="_blank" rel="noopener noreferrer"><strong>2025 Stack Overflow Developer Survey</strong></a>, que analiza de forma integral el estado del desarrollo de software.</p>
<ul>
<li>El 84% de los desarrolladores afirmó utilizar herramientas de IA o tener previsto hacerlo. (Un aumento respecto al 76% de 2024).</li>
<li>El 51% de los desarrolladores profesionales usa herramientas de IA a diario.</li>
<li>Sin embargo, <strong>la opinión favorable hacia las herramientas de IA (positive sentiment) disminuyó</strong>. Tras superar el 70% en 2023 y 2024, cayó hasta el 60% en 2025.</li>
<li>Los desarrolladores senior con más de diez años de experiencia son quienes menos confían en los resultados de la IA.</li>
</ul>
<p>En resumen: <strong>«Todo el mundo las usa, pero cada vez se fía menos»</strong>.</p>
<p>Un experimento realizado en 2025 por el instituto de investigación sin ánimo de lucro <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> muestra de forma aún más llamativa la brecha entre esta percepción y la realidad. Fue un experimento controlado en el que se asignó aleatoriamente el uso o no de IA a 16 desarrolladores expertos de código abierto, con una media de cinco años de experiencia y 1.500 commits, para que completaran 246 tareas. Los resultados fueron los siguientes.</p>
<ul>
<li>Antes de empezar, los desarrolladores predijeron que «con IA serían un 24% más rápidos».</li>
<li>Justo después de terminar las tareas, seguían valorando que «parecía que habían sido alrededor de un 20% más rápidos».</li>
<li>Sin embargo, la medición real mostró que habían sido <strong>un 19% más lentos</strong>.</li>
</ul>
<p>Las causas señaladas por los investigadores son interesantes. La tasa de aceptación del código generado por la IA fue inferior al 44%; incluso el código rechazado exigió tiempo de revisión y pruebas, y hasta el código aceptado requirió bastante tiempo de revisión y corrección. Esa ilusión de haber ido más rápido pese a haber tardado más es una de las razones por las que los desarrolladores senior se muestran cada vez más escépticos ante la IA.</p>
<p>Además, la propia «calidad del código escrito por IA» tampoco es impecable. Veamos el experimento de <a href="https://www.veracode.com/blog/genai-code-security-report/" target="_blank" rel="noopener noreferrer"><strong>Veracode</strong></a>, que pidió a más de cien modelos de IA que escribieran código.</p>
<ul>
<li>El <strong>45% del código generado por IA contenía vulnerabilidades de seguridad del OWASP Top 10</strong>.</li>
<li>La <strong>tasa de fallos en la protección contra XSS (cross-site scripting) fue del 86%</strong>.</li>
<li>La tasa de fallos en la protección contra Log Injection fue del 88%.</li>
<li>Otro estudio informó de que la densidad de vulnerabilidades del código de IA era <strong>2,7 veces mayor</strong> que la del código humano.</li>
</ul>
<p>El 86% de fallos en XSS, algo directamente relacionado con el frontend, merece especial atención. La cifra muestra muy bien qué implica hacer merge sin más de un form input creado por la IA. (A quienes tengan experiencia con auditorías de seguridad frontend ya les resulta incómodo y preocupante escribir personalmente <code>dangerouslySetInnerHTML</code>; parece aún más aterrador cuando la IA lo introduce a escondidas).</p>
<p>Las señales sobre la calidad son parecidas. <a href="https://www.gitclear.com/ai_assistant_code_quality_2025_research" target="_blank" rel="noopener noreferrer"><strong>GitClear</strong></a> analizó 211 millones de líneas de cambios de código entre 2020 y 2024 y obtuvo estos resultados.</p>
<ul>
<li>Porcentaje de código revertido en las dos semanas posteriores a su escritura (Code Churn): 5,5% en 2020 → <strong>7,9%</strong> en 2024.</li>
<li>Porcentaje correspondiente a refactorización: 25% en 2021 → <strong>menos del 10%</strong> en 2024.</li>
<li>Porcentaje de copia y pega (clones): 8,3% en 2021 → <strong>12,3%</strong> en 2024 (en 2025 llegó a multiplicarse por cuatro).</li>
</ul>
<p>La interpretación no es muy difícil. Ha aumentado la capacidad de producir código rápidamente, pero ha disminuido la de escribir código que merezca la pena revisar y mejorar. Los <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">datos de Apiiro</a>, basados en el análisis de empresas Fortune 50, son aún más contundentes. Los desarrolladores asistidos por IA producen entre tres y cuatro veces más commits que sus compañeros, pero generan diez veces más security findings. Las rutas de escalada de privilegios (privilege escalation) aumentaron un 322% y los defectos de diseño arquitectónico, un 153%.</p>
<h2 id="qué-ha-sustituido-la-ia-y-qué-no-ha-podido-sustituir"><a class="anchor" href="#qué-ha-sustituido-la-ia-y-qué-no-ha-podido-sustituir">Qué ha sustituido la IA y qué no ha podido sustituir</a></h2>
<p>Las herramientas avanzan sin freno, pero las cifras presentan matices. Entonces, ¿qué ha sustituido exactamente la IA y qué no ha podido sustituir todavía? Solo si distinguimos claramente ambas cosas podremos saber dónde debemos invertir nuestro tiempo.</p>
<p>Lo que se ha sustituido es parte del trabajo de teclear directamente que hacían los desarrolladores. Hay menos situaciones en las que debamos escribir boilerplate o código repetitivo; con solo un diseño se puede obtener en pocos minutos una pantalla que respete las convenciones, y tanto el tiempo de búsqueda sobre sintaxis y API como la curva de aprendizaje se han reducido drásticamente. En suma, la IA ha <strong>igualado la «velocidad de producción»</strong>.</p>
<p>Pero todavía no ha sustituido el ámbito del «criterio». (Para ser exactos, sería más apropiado decir que «todavía no ha satisfecho las expectativas». Aunque existen diferencias entre personas en la capacidad de aprovechar la IA, aquí desarrollaré el argumento a partir de una experiencia de uso promedio).</p>
<p>El primer escollo es <strong>traducir los requisitos en especificaciones</strong>. Convertir necesidades de negocio ambiguas en casos límite precisos y máquinas de estados sigue requiriendo una intervención humana más profunda. Lo mismo ocurre con <strong>comprender el impacto en todo el sistema</strong>: aunque la IA ofrezca respuestas plausibles a preguntas como qué efecto tiene este componente sobre el bundle, si una dependencia permite tree shaking o cómo afecta un patrón de data fetching a la puntuación de <a href="https://web.dev/articles/vitals" target="_blank" rel="noopener noreferrer">Core Web Vitals</a> denominada <a href="https://web.dev/articles/inp" target="_blank" rel="noopener noreferrer">INP (Interaction to Next Paint)</a>, al final uno solo se queda tranquilo cuando una persona vuelve a revisarlas.</p>
<p>Tampoco pueden dejarse de lado <strong>la seguridad y la evaluación de riesgos</strong>, como demuestra el problema ya mencionado del 45% de vulnerabilidades OWASP; ni ámbitos como <strong>mantener el sistema de diseño y la coherencia</strong>, comprobando que un componente nuevo se ajuste a los tokens, las reglas de accesibilidad y los patrones de interacción existentes; o <strong>entender el contexto del cliente y del mercado</strong>, preguntándose por qué hace falta una función y en qué flujo de usuario debe integrarse.</p>
<p>Por último, tomando prestada una expresión del artículo de <a href="https://yceffort.kr/2026/02/frontend-engineering-in-ai-era" target="_blank" rel="noopener noreferrer">yceffort</a>, la <strong>gestión de la deuda cognitiva (Cognitive Debt)</strong> —«la brecha entre la complejidad del sistema y el grado en que el equipo lo comprende»— es precisamente un ámbito en el que la distancia crece aún más rápido desde la adopción de la IA. Por eso, la tarea de cerrar esa brecha sigue correspondiendo a las personas.</p>
<blockquote>
<p>Lo que desaparece no es el desarrollador, sino la forma que tenía su trabajo. El cuello de botella ha pasado de la «velocidad para construir» a la «velocidad para decidir».</p>
</blockquote>
<p>En la misma línea, el artículo de Toss <a href="https://toss.tech/article/will-ai-replace-developers" target="_blank" rel="noopener noreferrer">«¿Serán sustituidos los desarrolladores por la IA?»</a> ofrece un diagnóstico de mayor calado. Su idea principal es esta: la IA no reemplaza a toda la fuerza laboral, sino que está eliminando la escalera de aprendizaje (apprenticeship ladder). Dentro de diez o veinte años, cuando los senior actuales se jubilen, faltará la siguiente generación capaz de diseñar sistemas complejos. No es una cuestión del nivel de «qué haremos con la contratación del año que viene en nuestra empresa», sino una especie de bomba de relojería para todo el sector. (Creo que es un artículo realmente bien escrito para una época llena de incertidumbre).</p>
<p>La «primera versión que funciona» creada por la IA representa el 70%. El 30% necesario para llegar a una «versión que pueda ofrecerse a usuarios reales» pertenece a las personas. Y la capacidad de completar ese 30% no aparece de la noche a la mañana. Esa es la esencia del problema de la escalera de aprendizaje. Si desaparece el tiempo de «ensuciarse las manos» escribiendo boilerplate y componentes sencillos, también desaparecen quienes podrían completar ese 30%.</p>
<p>El artículo original de Hwidong Bae enumeraba como «cinco competencias básicas de un ingeniero excelente» <strong>escribir buen código, maximizar el valor presente (equilibrar un lanzamiento rápido y la mantenibilidad a largo plazo), tomar decisiones basadas en datos, ayudar eficazmente a los compañeros a decidir y aprender de forma constante</strong>. Las cinco siguen siendo válidas en la era de la IA, pero la última ocupa la posición más vulnerable. El aprendizaje no ha desaparecido; ha cambiado su objeto. Antes aprendíamos «cómo se usa esta herramienta»; ahora debemos dedicar tiempo a aprender «cómo funciona todo este sistema». Más inquietante aún es <a href="https://evan-moon.github.io/2026/04/18/developers-who-stopped-growing-in-ai-era/" target="_blank" rel="noopener noreferrer">el problema señalado por Evan Moon</a>: «en cuanto la IA se encarga de escribir el código, la carga cognitiva del cerebro cae drásticamente». Que disminuya la carga cognitiva suena bien, pero es peligroso porque esa carga era precisamente la materia prima del aprendizaje. <strong>Cuanto más cómodo resulta, menos se crece.</strong></p>
<p>Aquí surge de forma natural una pregunta. Entonces, ¿han dejado de tener sentido las tres vías del artículo de Hwidong Bae —especialización web, en producto y en operaciones—?</p>
<p>Yo no lo creo. Las vías siguen siendo válidas. Lo adecuado es considerar que cada una ha evolucionado un nivel para adaptarse a la era de la IA. Veamos cómo ha cambiado el panorama en cada caso.</p>
<h2 id="de-productor-a-verificador"><a class="anchor" href="#de-productor-a-verificador">De productor a «verificador»</a></h2>
<p>En el artículo original de Hwidong Bae, la vía de especialización web se agrupaba bajo el nombre de <strong>Software Engineer</strong>. Sus pilares eran «la comprensión profunda y el uso de Internet, los navegadores web y HTML/CSS/JS», el conocimiento de las ventajas e inconvenientes de las herramientas del ecosistema web, la experiencia resolviendo problemas y una actitud receptiva a las nuevas tecnologías. Como caminos hacia un puesto senior se proponían <strong>ingeniero en una empresa que desarrolla herramientas para el ecosistema web / formador de frontend / tech lead en una organización con productos complejos</strong>. En pocas palabras, eran «personas que profundizan en el funcionamiento del navegador y de HTML/CSS/JS», y hasta hace uno o dos años su mayor arma era «poder escribir código con más precisión que nadie».</p>
<p>¿Cómo ha cambiado su valor en la era de la IA? Si atendemos únicamente a la velocidad para escribir código, la IA ya los ha alcanzado. Sin embargo, <strong>«la capacidad de evaluar con precisión el código escrito por la IA»</strong> se ha convertido prácticamente en patrimonio exclusivo de estos perfiles.</p>
<ul>
<li>Persona no desarrolladora que utiliza IA: se ha implementado según mis requisitos y funciona correctamente.</li>
<li>Desarrollador que utiliza IA: funciona, pero esta dependencia puede causar ciertos problemas, y mejorar este patrón de esta forma encajaría mejor con las convenciones. Revisemos también las partes relacionadas.</li>
</ul>
<p>En el estudio de Veracode ya mencionado aparecían tasas de fallo del 86% en XSS y del 88% en Log Injection. Quienes pueden detectar y corregir esos problemas somos precisamente los especialistas como nosotros. Estos perfiles evolucionan de manera natural hacia funciones senior de control de calidad (QA) de lo producido por la IA.</p>
<p>Además, al ámbito de los especialistas se ha añadido un tema completamente nuevo: la <strong>UI generativa (Generative UI)</strong> y el <strong>diseño de interfaces de IA</strong>. Algunos ejemplos son las interfaces de chat que renderizan por streaming las respuestas de un LLM, los controles de abort para detenerlas a mitad de camino, el renderizado progresivo de Markdown y bloques de código, la UX que muestra inline los resultados de las llamadas a herramientas y la integración de asistentes mediante <a href="https://sdk.vercel.ai/" target="_blank" rel="noopener noreferrer">Vercel AI SDK</a> o <a href="https://modelcontextprotocol.io/" target="_blank" rel="noopener noreferrer">MCP (Model Context Protocol)</a>. En este campo, se está disparando la demanda de «personas que conozcan con precisión cómo funciona la web y, al mismo tiempo, comprendan las características operativas de los LLM y sepan aplicarlas y aprovecharlas».</p>
<h2 id="la-evolución-natural-hacia-product-engineer"><a class="anchor" href="#la-evolución-natural-hacia-product-engineer">La evolución natural hacia Product Engineer</a></h2>
<p>La vía de especialización en producto es la que más se ha beneficiado. Quienes comprenden bien el mercado y a los clientes y se comunican a menudo con las partes interesadas externas han obtenido, al incorporar la IA, un arma mucho más potente. Otra característica de esta vía era que, entre los caminos hacia puestos senior, también se proponía la expansión hacia otras profesiones, como <strong>ingeniero de growth o consultor / transición a PM, PO o CPO</strong>.</p>
<p>Un cambio interesante es que el nombre de esta vía ha empezado a convertirse en un estándar global. El artículo original ya la llamaba «Product Engineer», pero cuando lo leí la expresión me resultaba algo desconocida. Un año después, se ha asentado hasta el punto de que <a href="https://leerob.com/product-engineers" target="_blank" rel="noopener noreferrer">Vercel cambió en bloque «Fullstack Engineer» por «Product Engineer» en las descripciones de sus puestos</a>.</p>
<p>Lee Robinson señala tres cualidades esenciales de un Product Engineer.</p>
<ul>
<li><strong>Mentalidad de iteración (Iteration)</strong>: recorre rápidamente el ciclo despliegue → feedback → ajuste.</li>
<li><strong>Orientación al cliente</strong>: habla directamente con los clientes para mejorar el producto.</li>
<li><strong>Pragmatismo</strong>: «toda elección tecnológica no es más que un medio». Descarta sin dudar las herramientas que no contribuyan al objetivo del producto.</li>
</ul>
<p>Aquí hay una trampa: es peligroso que el ingeniero especializado en producto sea percibido únicamente como «alguien que construye rápido». Ahora que existe la IA, el riesgo es aún mayor. «Implementar funcionalidades rápidamente» es algo que cualquier otra profesión puede hacer ya con herramientas de IA. La diferencia de un Product Engineer está en «la capacidad de definir con precisión el problema del cliente y validarlo rápidamente con la solución más pequeña», no en «tener manos rápidas».</p>
<p>En esta corriente, la profesión de <strong>Design Engineer</strong> ha empezado a ascender a la categoría de puesto formal. Vercel está contratando <a href="https://cjroth.com/blog/2026-02-18-building-an-elite-engineering-culture" target="_blank" rel="noopener noreferrer">ingenieros de diseño en una vía profesional formal con salarios superiores a 200.000 dólares</a>, y Linear y Stripe avanzan en una dirección similar. Es una profesión que elimina el propio handoff entre frontend y diseño. Como la IA dibuja rápidamente, se ha vuelto más escasa la capacidad de abordar a la vez «qué dibujar y si el resultado encaja en un sistema de diseño coherente».</p>
<h2 id="orquestador-de-ia"><a class="anchor" href="#orquestador-de-ia">Orquestador de IA</a></h2>
<p>La vía de especialización en operaciones es la que está cambiando de forma más drástica. En el artículo original de Hwidong Bae se clasificaba como <strong>Full-Stack Engineer</strong> y se definía como «una persona muy interesada en la estructura, la integración, las pruebas y el despliegue del proyecto, que maneja directamente API e infraestructura sencillas, cubre los vacíos de la organización y mejora los procesos». En el último año o dos, se le ha añadido <strong>la función de operar los propios agentes de IA</strong>, por lo que el alcance de esta vía está ampliándose rápidamente.</p>
<p>Al resumir las <a href="https://beyond.addy.ie/2026-trends/" target="_blank" rel="noopener noreferrer">tendencias de 2026</a>, se destacó como concepto central la <strong>«orquestación de agentes de programación (Orchestrating Coding Agents)»</strong>. Significa ir más allá de encargarle algo a una sola IA para diseñar y operar un sistema en el que varios agentes de IA colaboran simultáneamente. En la misma línea, también se propone codificar directamente los flujos de trabajo profesionales, las puertas de calidad y las mejores prácticas del sector en la lógica operativa de los agentes mediante un framework llamado <a href="https://github.com/addyosmani/agent-skills" target="_blank" rel="noopener noreferrer">«agent-skills»</a>.</p>
<p>Tras reunir los materiales relacionados, estas son, a mi juicio, las nuevas palabras clave que deben manejar los ingenieros de la vía de operaciones.</p>
<ul>
<li><strong>MCP (Model Context Protocol)</strong>: estándar propuesto por Anthropic para conectar los LLM con herramientas externas.</li>
<li><strong>Gobernanza de IA</strong>: gestionar quién puede usar la IA y con qué contexto, y comprobar que no se filtren secretos.</li>
<li><strong>Evaluación de agentes (Evaluation)</strong>: pipeline que puntúa automáticamente los resultados producidos por un agente.</li>
<li><strong>Puerta de IA</strong>: verificación automática de seguridad y calidad antes de hacer merge de una PR, y etiquetado del código de IA.</li>
</ul>
<p>El artículo original proponía como caminos senior de la vía de operaciones puestos como <strong>ingeniero de equipo de plataforma en una organización a gran escala / tech lead / coach agile / technical program manager (TPM) / CTO</strong>. Esos caminos siguen siendo válidos, pero ahora se les han sumado puestos como <strong>«responsable de infraestructura de desarrollo con IA»</strong> e <strong>«ingeniero de productividad del desarrollador (DevProd)»</strong>.</p>
<p>Mientras cada una de las tres vías evoluciona por su cuenta, hay competencias que se han vuelto más importantes en todas ellas. En un principio quería pensar a cinco años vista, pero, con la velocidad actual del progreso, incluso un año parece una unidad demasiado grande. Por eso reduciré de momento el horizonte a «el año que viene» y señalaré las competencias que, en mi opinión, cobrarán más importancia.</p>
<h2 id="cinco-competencias"><a class="anchor" href="#cinco-competencias">Cinco competencias</a></h2>
<p><strong>La primera es la capacidad de redactar especificaciones (Specification).</strong> En la era de la IA, el «punto de partida de la programación» no es el teclado, sino la <strong>especificación</strong>. La capacidad de describir con precisión qué se le debe pedir a la IA se ha vuelto más importante que el propio código. Aquí, una especificación no es necesariamente un grandilocuente documento RFC. Puede tratarse de <strong>pruebas</strong> que expresan en código el comportamiento esperado de la lógica de negocio, de <strong>stories de Storybook</strong> que recogen los escenarios y el contrato visual de un componente de UI, o de <strong>definiciones de tipos</strong> que especifican el contrato del flujo de datos. En definitiva, consiste en establecer de antemano criterios que permitan verificar automáticamente lo producido por la IA; si se programa con IA sin ellos, los problemas se acumulan.</p>
<p><strong>La segunda es la capacidad de verificación y el criterio.</strong> La IA produce con seguridad código plausible pero incorrecto. Por eso considero esencial «la capacidad de revisar el código de IA con rapidez y precisión». Se trata de detectar si faltan headers de seguridad, sanitization de inputs o tokens CSRF; si siguen funcionando la accesibilidad —ARIA, navegación con teclado y focus traps—; y si existen problemas de rendimiento relacionados con el coste de renderizado, la memoria o el tamaño del bundle. Lanzar AI slop a una PR sin revisarlo es una dejación de funciones como ingeniero. Quien pulsa el botón de merge sigue siendo una persona y no puede trasladar esa responsabilidad a la IA. Es muy probable que, en la encuesta de Stack Overflow, la confianza de los senior en la IA sea la más baja precisamente porque tienen el ojo necesario para detectar estos detalles.</p>
<p><strong>La tercera es la comprensión de sistemas y el pensamiento arquitectónico.</strong> La IA maneja bien un archivo cada vez y posee una gran capacidad para reconocer flujos y relaciones. La IA corrige rápidamente los síntomas, pero un buen desarrollador encuentra la causa raíz. Una forma de cultivar esta competencia es realizar actividades deliberadas como una Architecture Retrospective. Como la velocidad de cambio del código ha aumentado, si no se eleva también conscientemente la comprensión del sistema por parte del equipo, la deuda cognitiva se acumula con rapidez.</p>
<p><strong>La cuarta es la capacidad de orquestar IA.</strong> El manejo de la propia IA también se está separando como un conjunto específico de competencias. Ya no se trata simplemente de «escribir buenos prompts», sino de abordar como un todo la capacidad de dividir el trabajo en tickets pequeños, elegir qué modelo utilizar para cada tarea, diseñar pipelines de evaluación y verificación de agentes, y definir estrategias de recuperación (rollback) cuando un agente falla. <a href="https://sourcegraph.com/blog/revenge-of-the-junior-developer" target="_blank" rel="noopener noreferrer">Steve Yegge</a> organiza esta evolución en <strong>seis waves (traditional → completions → chat → coding agents → agent clusters → agent fleets)</strong>.</p>
<p><strong>La quinta es Context Engineering.</strong> Es un concepto que <a href="https://www.faros.ai/blog/context-engineering-for-developers" target="_blank" rel="noopener noreferrer">Karpathy y Tobi Lütke, CEO de Shopify, empezaron a impulsar juntos a mediados de 2025</a> y que, en pocas palabras, consiste en «la capacidad de diseñar qué contexto mostrarle a la IA, en qué formato y en qué cantidad». En concreto, adopta formas como el trabajo con <strong>archivos CLAUDE.md / rules</strong>, donde se documentan las convenciones del proyecto, los principios arquitectónicos y las prohibiciones al alcance de la IA; la <strong>reducción deliberada del contexto</strong>, que selecciona únicamente los módulos relevantes en lugar de incorporar todos los archivos; la <strong>separación explícita de etapas</strong>, que divide planificación → implementación → verificación en sesiones distintas para evitar la contaminación del contexto; y el <strong>contexto externo mediante MCP</strong>, que conecta a través de interfaces estándar fuentes externas como sistemas de diseño, esquemas de API y datos de monitorización. <a href="https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents" target="_blank" rel="noopener noreferrer">La documentación oficial de Anthropic</a> lo denomina «the new prompt engineering» y afirma categóricamente que un único prompt jamás puede contener el conocimiento arquitectónico, los patrones y la tribal wisdom de un sistema. Dicho de otro modo, «diseñar de una vez un buen prompt» importa mucho menos que «diseñar un entorno en el que la IA reciba siempre un buen contexto».</p>
<p>Llegados hasta aquí, surge una pregunta natural. Entonces, ¿cómo se estudia todo esto en concreto? Los métodos que utilizo son, a grandes rasgos, cuatro.</p>
<h2 id="cómo-aprender"><a class="anchor" href="#cómo-aprender">Cómo aprender</a></h2>
<p>La «formación continua» señalada en el artículo original sigue siendo válida, pero debe cambiar <strong>la distribución del tiempo de aprendizaje</strong>.</p>
<p>Hay áreas a las que antes dedicábamos mucho tiempo y que ahora podemos reducir. En cambio, también existen ámbitos complejos que antes evitábamos por su dificultad o porque exigían mucho tiempo. Entre estos últimos están la redacción de especificaciones de pruebas, el uso de herramientas de medición del rendimiento (<a href="https://developer.chrome.com/docs/lighthouse" target="_blank" rel="noopener noreferrer">Lighthouse</a>, <a href="https://www.webpagetest.org/" target="_blank" rel="noopener noreferrer">WebPageTest</a>, Chrome DevTools Performance), la accesibilidad (<a href="https://www.w3.org/WAI/standards-guidelines/wcag/" target="_blank" rel="noopener noreferrer">WCAG</a>) y la seguridad, en especial el <a href="https://owasp.org/www-project-top-ten/" target="_blank" rel="noopener noreferrer">OWASP Top 10</a>. Y hay ámbitos completamente nuevos que debemos aprender, como Vercel AI SDK, LangChain.js, MCP, los patrones de UI por streaming y los pipelines de evaluación de agentes. <strong>Es importante reconocer qué competencias necesito y distribuir el tiempo en consecuencia.</strong></p>
<p>El código producido por la IA tiende a crecer mucho. Genera cientos de líneas por minuto. Por eso, si no se gestionan conscientemente el tamaño de las PR y el ciclo de merge, la propia revisión de código se viene abajo. Tras la adopción interna de la IA, el tamaño medio de las PR aumentó un 18%, los incidentes por PR un 24% y la tasa de fallos de cambios un 30%. Si relacionamos estas cifras con los datos anteriores, hacer cambios grandes y fusionarlos de una sola vez <strong>dificulta comprender el flujo y reflejar la intención, por lo que es importante dividir el trabajo en unidades más pequeñas.</strong></p>
<p>Hay una cuestión directamente relacionada con el problema de la reducción de la carga cognitiva señalado en el artículo de Evan Moon. Conviene reservar una o dos horas al día para escribir código sin IA. Por ejemplo, dibujar la arquitectura a mano o leer personalmente, línea por línea, código de un ámbito poco familiar. (Yo también intento programar sin IA todos los días durante ese sopor que sigue al almuerzo. Es un tiempo que reservo para no alejarme de aquella familiaridad).</p>
<p>No se trata simplemente de «no olvidar la forma antigua». La razón es que la propia profundidad no crece durante el tiempo en que la IA hace el trabajo por uno. Competencias como la verificación, el criterio y la comprensión de sistemas dependen del tiempo dedicado a enfrentarse directamente a los problemas.</p>
<h2 id="entonces-nosotros"><a class="anchor" href="#entonces-nosotros">Entonces, nosotros</a></h2>
<p>Aunque me he extendido mucho, lo cierto es que el perfil del ingeniero frontend que sobrevive en la era de la IA no difiere tanto de la conclusión del artículo original. Estos eran los tres puntos que allí se atribuían a un buen ingeniero senior.</p>
<ul>
<li>Se esfuerza por <strong>mantener unos fundamentos sólidos</strong>. (Mantiene y refuerza continuamente las cinco competencias básicas).</li>
<li>Aunque no sea un líder explícito, ejerce una influencia natural mediante una conducta ejemplar.</li>
<li>No se conforma con terminar bien el trabajo asignado, sino que examina el contexto anterior y posterior y genera un gran impacto.</li>
</ul>
<p>Aplicado a la era de la IA, quedaría así.</p>
<ul>
<li>Mantiene sólidos los <strong>fundamentos</strong> —web, sistemas y dominio— que hay más allá del código producido por la IA.</li>
<li>Marca personalmente la dirección, en lugar de dejarla en manos de la IA. Incluso cuando no es la persona responsable explícita, decide «hacia dónde hay que ir».</li>
<li>No utiliza la IA solo como herramienta de productividad personal, sino para eliminar los cuellos de botella del equipo y del sistema.</li>
</ul>
<p>Si examinamos los escritos de Andrej Karpathy, una autoridad de Open AI, el núcleo del <strong>«agentic engineering»</strong> que ahora subraya es, en última instancia, el mismo: diseñar el sistema, especificar las restricciones y usar la IA para acelerar una implementación cuyo razonamiento ya se ha completado mentalmente. Aunque cambien las herramientas, el control de la dirección sigue en manos humanas.</p>
<p>El mensaje final del artículo original también era que se convierte en senior «la persona que no se conforma con terminar bien el trabajo asignado, sino que examina el contexto anterior y posterior y genera un gran impacto». En la era de la IA solo ha cambiado la definición de ese «impacto». Hay quien hace merge de una pantalla creada por la IA en una hora con un «funciona, así que vale», y hay quien dedica treinta minutos más a comprobar hasta qué punto esa pantalla es razonable en términos de accesibilidad, seguridad, rendimiento y coherencia con el sistema. Dentro de un año, se reconocerá como senior al segundo. Sobre la frontera entre el 70% —funcionamiento— y el 30% —aplicación y aprovechamiento—, sobrevivirá quien se sitúe del lado del 30%.</p>
<p>Espero que los ingenieros frontend que lean este artículo también se lleven su propia respuesta a la pregunta «¿qué debo estudiar ahora?». Nadie conoce la respuesta correcta, pero estoy bastante convencido de que, cuanto más programe la IA, más sobrevivirán quienes sepan ver «lo que hay más allá del código». Concluyo con la esperanza de poder volver a escribir dentro de un año sobre cuánto habrá cambiado una vez más este panorama.</p>
<p><strong>(Si dentro de un año este artículo parece demasiado obvio o anticuado, quizá signifique que hemos sabido responder bien).</strong></p>
<h2 id="referencias"><a class="anchor" href="#referencias">Referencias</a></h2>
<details class="ref-block"><summary class="ref-summary">참고 자료</summary></details>]]></content:encoded>
            <author>jihoon7705@gmail.com (이지훈)</author>
            <category>프론트엔드</category>
            <category>커리어</category>
            <category>AI</category>
        </item>
        <item>
            <title><![CDATA[Abstracción]]></title>
            <link>https://hooninedev.com/es/260201</link>
            <guid isPermaLink="false">https://hooninedev.com/es/260201</guid>
            <pubDate>Sun, 01 Feb 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[En este artículo quiero hablar de la abstracción en programación y de cómo escribir buen código desde la perspectiva de la abstracción. Durante mi trabajo como desarrollador frontend me he preguntado ...]]></description>
            <content:encoded><![CDATA[<p>En este artículo quiero hablar de la abstracción en programación y de cómo escribir buen código desde la perspectiva de la abstracción.</p>
<p>Durante mi trabajo como desarrollador frontend me he preguntado incontables veces: «¿Hasta qué punto debería separar esta lógica?» o «¿En qué unidades debería dividir este componente?». Al principio pensaba que abstraer consistía simplemente en extraer las partes comunes: convertir el código repetido en una función y reunir en una sola pieza los elementos compartidos por componentes similares. Sin embargo, después de ver varias veces cómo el código creado de ese modo se convertía con el tiempo en un monstruo cada vez más difícil de tocar, empecé a replantearme qué significa realmente abstraer.</p>
<p>En este artículo recopilo mis reflexiones sobre la esencia de la abstracción y sobre cómo utilizarla en el desarrollo frontend para crear buen código.</p>
<h2 id="lo-abstracto-y-la-abstracción"><a class="anchor" href="#lo-abstracto-y-la-abstracción">Lo abstracto y la abstracción</a></h2>
<p>Antes de entrar en materia, conviene aclarar qué significan exactamente en programación las palabras «abstracto» y «abstracción». Aunque parecen similares, su naturaleza es bastante distinta.</p>
<p><strong>Lo abstracto (Abstract)</strong> es un estado y una propiedad. Cuando decimos que algo «es abstracto», queremos decir que se han omitido los detalles concretos y que <strong>solo permanecen los conceptos esenciales</strong>. Este es precisamente el sentido de las clases o métodos marcados con la palabra clave <code>abstract</code> en Java o TypeScript: son planos incompletos en los que aún no se ha incorporado una implementación concreta y solo se ha definido la forma esencial.</p>
<p><strong>La abstracción (Abstraction)</strong> es un proceso y una acción. Consiste en simplificar un objeto complejo conservando únicamente sus características esenciales y eliminando los detalles innecesarios. Lo importante es que abstraer no significa «agrupar las cosas de cualquier manera», sino <strong>definir con precisión una responsabilidad en cada nivel</strong>.</p>
<p>En la vida cotidiana, «abstracto» suele utilizarse con el matiz de «ambiguo». En programación, sin embargo, la abstracción persigue justo lo contrario. Su objetivo no es generar ambigüedad, sino crear un nuevo nivel de significado que pueda ser absolutamente preciso. La esencia de la abstracción consiste en conservar la información relevante para un contexto dado y olvidar la que no lo es.</p>
<p>En definitiva, la diferencia puede resumirse así: <strong>lo abstracto es «el estado en el que solo queda lo esencial», mientras que la abstracción es «el proceso de dejar solo lo esencial»</strong>. Al diseñar código llevamos a cabo precisamente ese proceso: conservamos únicamente la interfaz esencial de una implementación compleja y ocultamos el resto.</p>
<p>Entonces, ¿por qué necesitamos esta abstracción en programación?</p>
<h2 id="por-qué-necesitamos-la-abstracción"><a class="anchor" href="#por-qué-necesitamos-la-abstracción">Por qué necesitamos la abstracción</a></h2>
<p>La razón fundamental por la que necesitamos abstracción en programación es sorprendentemente sencilla: <strong>para construir cosas más complejas</strong>. Cuando intentamos crear algo más complejo, resulta difícil recordar y manejar todos sus numerosos elementos. Por eso los agrupamos y los convertimos en conceptos abstractos más simples.</p>
<p>React, que los desarrolladores frontend utilizamos a diario, es un buen ejemplo. Para renderizar un solo componente tienen lugar internamente procesos complejos como la creación del Virtual DOM, la reconciliación (Reconciliation) y la manipulación del DOM real. Sin embargo, nosotros solo tenemos que escribir JSX sin preocuparnos por nada de eso, porque React ha abstraído todo ese proceso.</p>
<p>Veamos el siguiente código. Al utilizar el componente UserProfile podemos crear y manejar la UI sin conocer procesos internos complejos como la creación del VDOM o el 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>Antes era habitual que un desarrollador frontend configurase Webpack a mano, pero hoy frameworks como Next.js o Vite han abstraído la configuración del bundling. Gracias a ello podemos desarrollar aplicaciones sin conocer el funcionamiento interno del bundler y dedicar ese tiempo a <strong>problemas de mayor nivel, como la lógica de negocio o la experiencia de usuario</strong>. (Por eso considero que el concepto de abstracción es especialmente importante en el trabajo de un desarrollador frontend).</p>
<p>Ese es, en última instancia, el valor esencial de la abstracción: ocultar la complejidad para que algo parezca sencillo y permitir que cada persona se concentre únicamente en su ámbito. Gracias a ello podemos crear software cada vez más grande y complejo sin que una sola persona tenga que comprenderlo todo.</p>
<p>Pero si la abstracción es tan útil, ¿cuanta más haya, mejor? Pensemos con qué propósito deberíamos abstraer.</p>
<h2 id="reducir-el-contexto"><a class="anchor" href="#reducir-el-contexto">Reducir el contexto</a></h2>
<p>Muchos desarrolladores entienden la abstracción como «extraer las partes comunes». No es una definición incorrecta, pero describe solo una de las técnicas para abstraer, no la esencia de la abstracción.</p>
<p>Para mí, la esencia de la abstracción consiste en <strong>«reducir al nivel adecuado el contexto que necesita conocer quien lee el código»</strong>.</p>
<p>Veamos un ejemplo sencillo.</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>Quien lee este código debe entender la inicialización, la condición y el incremento del bucle; el acceso a cada elemento mediante un índice; la bifurcación tras comprobar una condición; y hasta cómo se actualiza la variable acumuladora externa. Sin embargo, lo que el código pretende hacer se resume en una sola frase: <strong>«calcular el total de los pedidos completados»</strong>. Para comprender esa frase hay que mantener a la vez cuatro contextos distintos en la cabeza.</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>Gracias a las abstracciones <code>filter</code> y <code>reduce</code>, el desarrollador solo necesita seguir dos intenciones: «seleccionar únicamente los pedidos completados» y «acumular los importes». El contexto de gestionar índices y declarar y actualizar una variable acumuladora ha desaparecido de la superficie del código.</p>
<p>Podemos ir un paso más allá.</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>Ahora quien lee el código ni siquiera necesita saber que el cálculo se realiza recorriendo un array. Solo queda la intención de negocio: «calcular el importe total de los pedidos completados». Podemos centrarnos no en <strong>cómo (How) se calcula</strong>, sino en <strong>qué (What) se calcula</strong>.</p>
<p>Desde esta perspectiva, también podemos ver que el código React que escribimos a diario es una combinación de innumerables abstracciones.</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>¿Qué ocurriría si todo el código interno de <code>emotion</code>, <code>date-fns</code> y <code>react</code> estuviera desplegado dentro de este archivo de componente? Sería difícil saber por dónde empezar a leer o distinguir qué parte corresponde a la lógica de negocio y cuál al código de una biblioteca. Como la abstracción oculta de manera adecuada el contexto de cada ámbito, podemos concentrarnos únicamente en la esencia: «mostrar la fecha de hoy».</p>
<p>Entonces, al diseñar código real, ¿en qué dirección deberíamos abordar la abstracción?</p>
<h2 id="qué-significa-que-el-nivel-de-abstracción-sea-alto-o-bajo"><a class="anchor" href="#qué-significa-que-el-nivel-de-abstracción-sea-alto-o-bajo">Qué significa que el nivel de abstracción sea alto o bajo</a></h2>
<p>Cuando hablamos de abstracción, hay un concepto imprescindible: el <strong>nivel de abstracción (Level of Abstraction)</strong>. ¿Qué significa exactamente que el nivel de abstracción de un código sea «alto» o «bajo»?</p>
<p>El <strong>código con un nivel de abstracción bajo</strong> se aproxima a los procedimientos concretos que ejecuta el ordenador: parsear directamente una cadena, recorrer un array mediante índices o manipular bytes. Expone sin disimulo <strong>cómo (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>El <strong>código con un nivel de abstracción alto</strong> se expresa en el lenguaje del dominio de negocio o del espacio del problema. Algunos ejemplos son <code>processPayment(order)</code>, <code>sendNotification(user, message)</code> o <code>validateUserInput(formData)</code>. El código con un nivel de abstracción alto muestra <strong>qué (What)</strong> hace y oculta cómo lo hace.</p>
<p>En <em>Clean Code</em>, Robert C. Martin condensó esta idea en el principio <strong>«un solo nivel de abstracción por función (One Level of Abstraction per Function)»</strong>. Si dentro de una función se mezclan código de alto y de bajo nivel, quien la lee tiene que decidir en cada línea: «¿Esto forma parte de la lógica esencial o es un detalle de implementación?».</p>
<p>El problema se vuelve evidente al verlo en 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>Quien lee esta función comienza siguiendo el contexto de alto nivel basado en reglas de negocio del «flujo de registro de usuarios», pero de repente se ve arrastrado a contextos de bajo nivel como la manipulación de buffers de hash, una consulta SQL y la cadena de una plantilla de correo electrónico. Después vuelve a saltar al alto nivel de <code>sendWelcomeEmail</code>. Cuando el nivel de abstracción sube y baja así, la mente de quien lee también tiene que subir y bajar con él.</p>
<p>Si reescribimos la misma función manteniendo un nivel de abstracción uniforme, el resultado es 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 las instrucciones hablan desde el mismo nivel de abstracción. Cada función de nivel inferior se responsabiliza de cómo se implementa el envío del correo o de qué algoritmo se utiliza para hashear la contraseña. Quien lee esta función solo tiene que concentrarse en un único contexto: «el flujo completo del registro de usuarios».</p>
<p>Martin también denominó esto <strong>«la regla descendente (The Stepdown Rule)»</strong>. Al leer el código de arriba abajo, la visión general debería aparecer arriba y los detalles deberían revelarse conforme descendemos, como en un artículo periodístico.</p>
<p>Kent Beck presentó el mismo principio en <em>Smalltalk Best Practice Patterns</em> mediante el patrón <strong>Composed Method</strong>. Un método debe componerse únicamente de operaciones situadas en el mismo nivel de abstracción, y cada paso debe expresarse como una llamada a un método de una sola línea.</p>
<p>Al final, todas estas ideas convergen en una sola: <strong>una función debe hablar desde un único nivel de abstracción</strong>. Solo con respetar esta regla, la legibilidad del código mejora de forma notable.</p>
<p>Entonces, ¿cómo debemos orientar la abstracción? ¿Deberíamos partir de lo concreto o de lo abstracto?</p>
<h2 id="pensar-en-la-composición-de-piezas-no-en-extraer-elementos-comunes"><a class="anchor" href="#pensar-en-la-composición-de-piezas-no-en-extraer-elementos-comunes">Pensar en la composición de piezas, no en extraer elementos comunes</a></h2>
<p>En OOP se suele recomendar «extraer lo común de los elementos concretos para definir algo abstracto». Este enfoque no es incorrecto en sí mismo, pero creo que, si nos aferramos demasiado a él, corremos el riesgo de crear un diseño prisionero de los requisitos actuales.</p>
<p>Veamos un ejemplo. Supongamos que los requisitos incluyen tres botones, A, B y C, todos azules y redondeados, cuya única diferencia es el texto de la etiqueta. Si diseñamos extrayendo solo lo común, podríamos expresarlo así.</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>Los requisitos actuales se cumplen a la perfección. Pero unos días después, la persona responsable del producto dice:</p>
<blockquote>
<p>«Quiero poder cambiar el color del botón B».</p>
</blockquote>
<p>En ese momento, hasta el nombre <code>BlueRoundButton</code> empieza a resultar extraño. Podríamos añadir una prop para el color, pero el diseño era vulnerable a los cambios desde el principio porque partía de una coincidencia concreta: «botones azules y redondeados». (Y este es un caso leve; en la vida real llegan innumerables requisitos sobre la forma, el tamaño y muchos otros aspectos de los botones).</p>
<p>Cuando esta situación se repite, uno acaba entendiendo algo de manera natural: <strong>si extraemos elementos comunes de requisitos concretos, es fácil que incluso el resultado abstraído refleje únicamente los requisitos actuales</strong>.</p>
<p>Por eso prefiero el enfoque inverso. En lugar de extraer una abstracción de lo concreto, consiste en <strong>pensar primero en piezas abstractas y componerlas para crear algo concreto</strong>.</p>
<p>Imaginemos que estamos creando un componente de notificaciones toast. El requisito inicial es sencillo: «Cuando termine de guardarse, muestra un breve mensaje informativo en la parte inferior». Si lo abordamos extrayendo elementos comunes, obtendremos algo así.</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>El requisito actual queda cubierto a la perfección. Pero unos días después se pide «añadir un icono a la izquierda según se trate de un éxito o una advertencia». Se agregan <code>hasIcon</code> e <code>iconName</code>. Poco después llega otro requisito: «También necesitamos un toast con una barra de progreso de subida». Se suma otra prop, <code>progress</code>. Tras repetir varias veces este proceso, <code>Toast</code> termina siendo un componente con más de diez props y reglas ocultas que hay que memorizar sobre <strong>qué combinaciones son válidas y cuáles no</strong>. (Y, por lo general, esas reglas ni siquiera quedan documentadas en comentarios).</p>
<p>El diseño era vulnerable a los cambios porque desde el principio partía de <strong>una imagen concreta de cómo «debe ser un toast» en ese momento</strong>.</p>
<p>La historia cambia cuando abordamos el problema mediante la composición de piezas.</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>Se han separado la esencia inmutable —«un toast es un contenedor ligero para alojar algo»— y el aspecto concreto y propenso a cambiar —«qué contiene»—. Ahora podemos añadir tantas piezas nuevas como queramos o disponer las existentes en combinaciones nuevas sin tocar el interior de <code>Toast</code>. También desaparecen las reglas de validez entre combinaciones de props. Basta con <strong>introducir lo que queramos introducir</strong>.</p>
<p>Un desarrollador con experiencia podría decir: «¿No bastaría con diseñarlo desde el principio aplicando IoC (inversión de control)?». Es cierto. Pero podemos tomar esa decisión porque, después de innumerables tropiezos en el pasado, hemos desarrollado una intuición sobre «qué partes son más propensas a cambiar».</p>
<p>Si todavía no contamos con esa intuición, partir de la pregunta «¿De qué piezas se compone esta funcionalidad y cómo deberían ensamblarse?» facilita mucho la creación de un diseño abierto al cambio.</p>
<p>Llegados a este punto, surge de forma natural otra pregunta: ¿con qué criterio debemos dividir las piezas y cómo debemos presentarlas al exterior?</p>
<h2 id="tres-claves-para-una-buena-abstracción"><a class="anchor" href="#tres-claves-para-una-buena-abstracción">Tres claves para una buena abstracción</a></h2>
<h3 id="pensar-en-la-forma-de-expresarla"><a class="anchor" href="#pensar-en-la-forma-de-expresarla">Pensar en la forma de expresarla</a></h3>
<p>La virtud más importante de un módulo abstraído es que su comportamiento pueda deducirse sin abrir el código fuente. Kent Beck llamó a esto el patrón <strong>«nombre que revela la intención (Intention-Revealing Name)»</strong> y señaló que, si no podemos asignarle un nombre conciso, deberíamos reconsiderar la propia abstracción.</p>
<p>Disponemos principalmente de dos herramientas para conseguirlo: <strong>los nombres</strong> y <strong>los 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>Con solo ver el nombre <code>calculateDiscountedPrice</code>, sabemos que recibe el precio original y la tasa de descuento y calcula el precio rebajado; la información de tipos, que indica que recibe valores <code>number</code> y devuelve un <code>number</code>, respalda esa interpretación. No necesitamos conocer la lógica de cálculo aplicada internamente.</p>
<p>En cambio, <code>calculate(price: number, rate: number): number</code> no aporta información sobre qué calcula, así que no podemos prever el resultado. Al final tenemos que abrir el código fuente para poder utilizarla y se pierde la ventaja de la abstracción.</p>
<p>Conviene observar que la propia manera de nombrar refleja el nivel de abstracción. En programación, los nombres de las funciones suelen combinar <strong>verbo + sustantivo</strong>, y el verbo elegido revela en qué nivel de abstracción opera la función.</p>
<blockquote>
<p>Sin embargo, el verbo por sí solo no determina el nivel de abstracción. El sustantivo que lo acompaña —el contexto de dominio— es lo que determina el nivel final.</p>
</blockquote>
<p>Hay verbos frecuentes en los niveles de abstracción <strong>bajos</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> o <code>transform</code>. Estas palabras sugieren transformaciones físicas de los datos o manipulaciones directas de estructuras de datos.</p>
<p>En un nivel intermedio aparecen verbos como <code>get</code>, <code>save</code>, <code>load</code> o <code>validate</code>. Son operaciones técnicas, pero su intención ya resulta visible hasta cierto punto.</p>
<p>En los niveles de abstracción <strong>altos</strong> se utilizan verbos como <code>register</code>, <code>refund</code>, <code>confirm</code>, <code>cancel</code> o <code>submit</code>. Son palabras propias del dominio de negocio. No revelan en absoluto qué procedimiento técnico ocurre en el interior; solo expresan <strong>la acción del usuario o el proceso de negocio</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>En <em>Clean Code</em>, Robert C. Martin afirmó al respecto que <strong>«es mejor un nombre largo y descriptivo que uno corto y enigmático»</strong>. También propuso el principio <strong>«usa una palabra por concepto»</strong>, porque si mezclamos <code>fetch</code>, <code>retrieve</code> y <code>get</code> para operaciones del mismo contexto, quien lea el código se preguntará: «¿Son tres operaciones distintas?».</p>
<p>Este principio se aplica igualmente a los nombres de componentes y hooks de 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>La especificidad del nombre de un componente varía según su nivel de abstracción. Button se utiliza como un primitivo de UI genérico de bajo nivel, mientras que SubmitOrderButton expresa con claridad una intención de negocio de alto nivel.</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> es el nombre de la prop que el componente expone al exterior. Desde el código que utiliza el componente se declara «a qué evento reaccionar». <code>handle*</code> es el nombre de la función de implementación que se pasa realmente a esa 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>Los hooks personalizados utilizan el prefijo <code>use</code> para cumplir las reglas de React y permiten que el componente use el estado o las operaciones que ofrece el hook.</p>
<blockquote>
<p>Jeff Atwood, creador de <a href="https://blog.codinghorror.com/" target="_blank" rel="noopener noreferrer">Coding Horror</a>, señaló en una ocasión el problema del sufijo <code>Manager</code>. Un nombre como <code>UrlManager</code> no permite saber en absoluto si mantiene un pool de URL, las valida o las crea. Nombres como <code>UrlBuilder</code>, <code>UrlValidator</code> o <code>UrlPool</code>, que revelan una responsabilidad concreta, son mucho mejores. Un nombre ambiguo puede ser una señal de que la propia responsabilidad del módulo también lo es.</p>
</blockquote>
<p>En definitiva, un buen nombre es <strong>aquel que indica de inmediato a quien lee en qué nivel de abstracción opera el código</strong>.</p>
<h3 id="diseñar-deliberadamente-la-libertad-de-las-entradas"><a class="anchor" href="#diseñar-deliberadamente-la-libertad-de-las-entradas">Diseñar deliberadamente la libertad de las entradas</a></h3>
<p>Al diseñar un módulo abstraído aparece con frecuencia una pregunta: «¿Hasta qué punto debemos dejar abierta la funcionalidad?». Esta decisión cambia considerablemente la experiencia de los desarrolladores que utilizan el 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>El primer botón solo acepta <code>children</code>. No permite configurar <code>onClick</code>, <code>type</code> ni <code>disabled</code>. A cambio, quien lo utiliza no tiene nada que decidir.</p>
<p>El segundo botón acepta todos los atributos del elemento <code>button</code>. Ofrece mucha libertad, pero obliga a quien lo utiliza a decidir cuál de las decenas de props disponibles debe usar. Describo estas situaciones diciendo que <strong>«el componente obliga al desarrollador a pensar»</strong>.</p>
<p>No hay una respuesta única. Debemos encontrar el nivel adecuado según el propósito del módulo y sus usuarios. En el botón base de un sistema de diseño puede ser preferible restringir las Props para mantener la coherencia; en un componente utilitario genérico puede convenir abrirlas para ofrecer flexibilidad.</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>La amplitud de una abstracción debe decidirse en función de quién la utiliza. Para un usuario que necesita comprender la implementación interna y ejercer un control preciso, es adecuada una interfaz de bajo nivel. En cambio, ofrecer una interfaz demasiado abierta a quien no necesita conocer los detalles solo añade confusión. Y, a la inversa, si restringimos en exceso las entradas de alguien que debe responder a situaciones variadas, impediremos los propios casos de uso.</p>
<h3 id="mantener-una-unidad-de-abstracción-adecuada"><a class="anchor" href="#mantener-una-unidad-de-abstracción-adecuada">Mantener una unidad de abstracción adecuada</a></h3>
<p>La unidad de abstracción —es decir, «hasta dónde agrupar en un solo módulo»— también es una decisión importante.</p>
<p>Un antipatrón frecuente en frontend es la extracción excesiva 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>Si separamos innecesariamente en un hook una lógica que solo utiliza un componente, obligamos a quien lee el código a saltar entre dos archivos para comprender el contexto. En lugar de reducirlo, la abstracción lo ha aumentado.</p>
<p>El extremo contrario —incluir demasiadas cosas en un solo hook— también supone un 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>Este tipo de «God Hook» es difícil de probar, y modificar un elemento puede afectar a partes que no guardan relación con él.</p>
<p>El criterio para determinar una unidad de abstracción adecuada es <strong>«¿Esta separación reduce realmente el contexto de quien lee el código?»</strong>. Si, como resultado de la separación, el contexto queda disperso y cuesta más comprenderlo, todavía no ha llegado el momento de esa abstracción.</p>
<h2 id="cuidado-con-las-abstracciones-prematuras"><a class="anchor" href="#cuidado-con-las-abstracciones-prematuras">Cuidado con las abstracciones prematuras</a></h2>
<p>Después de leer hasta aquí puede quedar una pregunta: «Entonces, ¿cuándo debemos abstraer?». Mi opinión es la siguiente: <strong>la premisa básica debe ser no abstraer de forma prematura</strong>.</p>
<p>Mientras no aparezca una señal clara para abstraer, dejar el código tal como está suele ofrecer un resultado mínimo mejor que crear una abstracción incorrecta y tener que deshacerla después. El proceso por el que surge una mala abstracción suele parecerse a este:</p>
<ol>
<li>Aparece un patrón similar en el código A y el código B.</li>
<li>Pensamos: «¡El principio DRY dice que debo extraerlo a una función común!» y lo abstraemos.</li>
<li>Encontramos un patrón parecido en el código C y usamos la misma función, pero añadimos un parámetro para introducir un comportamiento ligeramente distinto.</li>
<li>Al empezar a utilizarla también en el código D y E, siguen aumentando las condiciones y los parámetros.</li>
<li>La función ya se utiliza en todas partes, pero se ha convertido en un código que nadie se atreve a tocar.</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>Si hemos caído en esta situación, la solución está clara: volver a poner inline el código abstraído en cada lugar donde se utiliza, eliminar en cada caso el código innecesario y, cuando el estado quede limpio, abstraer de nuevo si entonces aparece una coincidencia real. <strong>«La forma más rápida de avanzar es volver atrás»</strong>.</p>
<p>Entonces, ¿cuándo debemos abstraer? Estas son, a grandes rasgos, las <strong>señales de abstracción</strong> que percibo:</p>
<ul>
<li><strong>Se está perdiendo la coherencia.</strong> Aunque se trata de la misma lógica, en algunos componentes aparece inline y en otros se ha separado en una función. La misma lógica de cálculo está dispersa por todas partes.</li>
<li><strong>La estructura interna se expone innecesariamente al exterior.</strong> Quien llama al módulo tiene que manejar uno por uno detalles de implementación que no necesita conocer.</li>
<li><strong>El procedimiento interno sigue quedando expuesto.</strong> El módulo no logra ocultar su propio procedimiento y quien lo utiliza debe seguirlo tal cual.</li>
</ul>
<p>El problema es que detectar estas señales suele ser bastante sencillo, pero <strong>en la práctica es fácil ignorarlas y centrarse en satisfacer requisitos más «importantes»</strong>. Cuando nos presionan los plazos o estamos absortos en implementar una funcionalidad, pensamos: «Ya funciona; lo ordenaré más adelante». Y ese momento rara vez llega.</p>
<p>Hay otra cuestión importante: <strong>mantener criterios de abstracción coherentes</strong>. Si dentro del mismo código base una misma clase de lógica aparece inline en un lugar, como función en otro y como hook personalizado en un tercero, quien lea ese código por primera vez se preguntará: «¿Hay una intención detrás de esta diferencia?». Tanto si se abstrae como si no, el equipo debe aplicar criterios coherentes.</p>
<p>También merece la pena recordar en este contexto <strong>«la ley de las abstracciones con fugas (The Law of Leaky Abstractions)»</strong>, formulada por Joel Spolsky en 2002. Esta ley sostiene que, aunque una abstracción intenta ocultar una implementación compleja, sus detalles terminan filtrándose al exterior (leak). Es decir: algo se diseña para que el usuario de la abstracción no necesite conocer la implementación interna, pero en la práctica surgen situaciones en las que solo puede utilizarla correctamente si la conoce.</p>
<p>TCP abstrae una red inestable para hacerla parecer una conexión fiable, pero la abstracción se rompe si se desconecta el cable. React abstrae de forma declarativa las actualizaciones de la UI, pero para optimizar los rerenderizados acabamos necesitando comprender su funcionamiento interno. Como no existen abstracciones perfectas, al abstraer debemos plantearnos incluso <strong>«¿Puede el usuario responder cuando esta abstracción se rompa?»</strong>.</p>
<p>En definitiva, <strong>«la abstracción nos ahorra tiempo de trabajo, pero no tiempo de aprendizaje»</strong>.</p>
<h2 id="la-abstracción-se-interioriza-con-la-práctica"><a class="anchor" href="#la-abstracción-se-interioriza-con-la-práctica">La abstracción se interioriza con la práctica</a></h2>
<p>En una ocasión hablé con un compañero sobre la abstracción y hubo una idea que me resultó especialmente memorable: detectar las señales de abstracción y separar algo en el nivel adecuado y en el momento oportuno pertenece, en última instancia, al <strong>terreno de la intuición</strong>.</p>
<p>Por supuesto, los principios mencionados antes —mantener el nivel de abstracción, elegir buenos nombres y diseñar la libertad de las entradas— son importantes. Sin embargo, intentar recordar cada uno mientras escribimos código y preguntarnos constantemente «¿Debería separar esto o no?» puede llegar a romper el flujo. Igual que, si durante un combate somos demasiado conscientes del ángulo del codo al lanzar un jab, podemos perder el momento preciso, al programar la abstracción debe surgir de una intuición natural y no de un juicio consciente.</p>
<p>Hay momentos en los que estamos escribiendo código y, de pronto, algo nos produce rechazo. Sentimos: «Esta lógica no debería estar aquí» o «Este componente parece saber demasiado». Esa sensación es precisamente una señal de abstracción; interiorizarla significa poder detectarla y responder a ella de forma natural.</p>
<p>Pero esta intuición no se desarrolla de la noche a la mañana. Solo después de aprender multitud de patrones, leer código diverso y sufrir nuestros propios tropiezos empieza a surgir con naturalidad la sensación de <strong>«creo que esto debería separarse»</strong>. Si más tarde un compañero pregunta «¿Por qué lo separaste?» y podemos explicar con naturalidad «Lo separé porque es X», entonces lo hemos interiorizado.</p>
<p>Creo que ocurre lo mismo en cualquier disciplina. Intentar hacerlo bien a base de memorizar puede dificultar aún más las decisiones. Al final, hay que conservar el flujo general y dejar que los detalles se completen de forma natural. Y esa naturalidad nace de la variedad de patrones y experiencias acumulados en el día a día.</p>
<h2 id="conclusión"><a class="anchor" href="#conclusión">Conclusión</a></h2>
<p>En programación, abstraer es ocultar la complejidad para que algo parezca sencillo y permitir que quien lee el código se concentre únicamente en el contexto que necesita.</p>
<p>Recapitulemos lo que conviene recordar para construir buenas abstracciones:</p>
<ul>
<li>Como principio básico, evitemos abstraer de forma prematura y separemos solo cuando aparezca una señal clara.</li>
<li>Hagamos que una función hable desde <strong>un único nivel de abstracción</strong>.</li>
<li><strong>Expresemos</strong> suficientemente el comportamiento mediante nombres y tipos para que pueda utilizarse sin abrir el código fuente.</li>
<li><strong>Diseñemos deliberadamente</strong> la libertad de las entradas según el propósito del módulo y sus usuarios.</li>
<li>En vez de seguir añadiendo capas sobre una abstracción incorrecta, tengamos <strong>el valor de deshacerla y empezar de nuevo</strong>.</li>
<li>E <strong>interioricemos distintos patrones</strong> hasta que todo esto surja de forma natural, sin necesidad de pensarlo conscientemente.</li>
</ul>
<p>Por supuesto, lo que he expuesto en este artículo no es la única respuesta correcta. El nivel adecuado de abstracción puede variar según la situación del negocio, la composición del equipo y la naturaleza del proyecto. Sin embargo, hay algo que no cambia: el objetivo último de la abstracción es <strong>crear código que las personas puedan comprender con facilidad</strong>.</p>
<p>Espero que quienes lean este artículo se planteen también en sus propios códigos base la pregunta: «¿Esta abstracción está reduciendo realmente el contexto?». Creo que esa sola pregunta puede cambiar, aunque sea un poco, la forma de mirar el código.</p>
<h2 id="referencias"><a class="anchor" href="#referencias">Referencias</a></h2>
<p>Este artículo se inspira en gran medida en distintos documentos oficiales y artículos anteriores. Dejo también las fuentes de las citas directas y los textos que me ayudaron a estructurar estas ideas.</p>
<details class="ref-block"><summary class="ref-summary">참고 자료</summary><div class="ref-list">
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#f59e0b"></span><a href="https://evan-moon.github.io/2023/01/15/what-is-abstract/" target="_blank" rel="noopener noreferrer">Evan Moon, 추상, 그리고 추상화</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#3b82f6"></span><a href="https://react.dev/learn/reusing-logic-with-custom-hooks" target="_blank" rel="noopener noreferrer">React, Reusing Logic with Custom Hooks</a></div>
</div>
</div></details>]]></content:encoded>
            <author>jihoon7705@gmail.com (이지훈)</author>
            <category>프론트엔드</category>
            <category>설계</category>
            <category>추상화</category>
        </item>
        <item>
            <title><![CDATA[queryKey]]></title>
            <link>https://hooninedev.com/es/260104</link>
            <guid isPermaLink="false">https://hooninedev.com/es/260104</guid>
            <pubDate>Sun, 04 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[En esta publicación quiero hablar sobre queryKey de TanStack Query. Al usar TanStack Query en proyectos reales, he tenido que replantear varias veces la forma de gestionar queryKey. Al principio escri...]]></description>
            <content:encoded><![CDATA[<p>En esta publicación quiero hablar sobre <strong>queryKey de TanStack Query</strong>.</p>
<p>Al usar TanStack Query en proyectos reales, he tenido que <strong>replantear varias veces la forma de gestionar queryKey</strong>. Al principio escribía directamente en los componentes arreglos como <code>['user', userId]</code>, pero, cada vez que tenía que invalidar una consulta, acababa repitiendo la misma clave en varios lugares y cometiendo errores tipográficos. Por eso las trasladé a un objeto de constantes como <code>QUERY_KEYS</code>. Más adelante, tras leer un artículo de TkDodo, adopté el patrón de fábrica de claves de consulta; bastante tiempo después incorporé la librería <code>@lukemorales/query-key-factory</code>; y, cuando apareció v5, volví a reorganizarlo todo en torno a <code>queryOptions</code>.</p>
<p>Empecé a preguntarme por qué habían surgido tantos patrones alrededor de un pequeño arreglo que, en principio, no era más que un identificador de caché. <strong>¿Por qué queryKey conserva tantas huellas de esa evolución?</strong> ¿Y qué problema concreto intentaba resolver cada etapa?</p>
<p>En este artículo seguiré la documentación oficial de TanStack Query, la serie de artículos de TkDodo y hasta la implementación interna de <code>queryOptions</code> introducida en v5 para explicar cómo funciona queryKey y por qué ha evolucionado hasta adoptar su forma actual.</p>
<h2 id="antes-de-que-existiera-querykey"><a class="anchor" href="#antes-de-que-existiera-querykey">Antes de que existiera queryKey</a></h2>
<p>Antes de entrar de lleno en el tema, conviene detenernos en una cuestión. Hoy usamos con total naturalidad librerías como <code>TanStack Query</code> y <code>SWR</code>, pero ¿cómo se gestionaban los datos asíncronos antes de que existieran?</p>
<p>La forma más habitual probablemente consistía en combinar <code>useState</code>, <code>useEffect</code>, <code>fetch</code>, <code>axios</code> y herramientas similares.</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>El problema de este código es evidente. Basta con que haya dos componentes en la página mostrando el mismo <code>userId</code> para que <strong>la misma petición se envíe dos veces</strong>. La razón es que no existe una caché. Además, si el usuario visita otra página y después regresa, los datos vuelven a solicitarse desde cero. Como no hay forma de distinguir si se obtuvieron hace un segundo o hace una hora, también resulta difícil reproducir comportamientos como «mostrar el valor almacenado en caché mientras se actualiza en segundo plano». (Podríamos implantar nuestro propio sistema de caché, pero considero que gestionarlo sería bastante complicado.)</p>
<p>Para resolverlo apareció la combinación Redux + redux-thunk (o redux-saga). Al extraer la lógica de obtención de datos a un thunk y guardar el resultado en el almacén, otros componentes podían reutilizar los mismos datos. Sin embargo, había que definir tipos de acción, escribir reductores y gestionar manualmente los estados de carga, éxito y error en cada ocasión. La cantidad de código repetitivo necesaria para obtener un solo dato era enorme. (Empecé a trabajar profesionalmente en esa época y me preguntaba: «¿Por qué tengo que crear varios archivos para obtener un único dato?».)</p>
<p>En el fondo, todo este recorrido se reduce a lo siguiente: <strong>«Para evitar repetir una petición, debemos poder identificar de qué petición se trata»</strong>. Y el identificador que responde a «de qué petición se trata» es precisamente queryKey.</p>
<p>SWR y React Query (hoy TanStack Query) abordaron el problema de frente: «Toda petición asíncrona debe tener un identificador y, si el identificador es el mismo, debe compartir la caché». Este único y sencillo principio eliminó todo el código repetitivo anterior.</p>
<h2 id="la-esencia-de-querykey"><a class="anchor" href="#la-esencia-de-querykey">La esencia de queryKey</a></h2>
<p>Entonces, ¿qué es exactamente queryKey? La documentación oficial de TanStack Query lo define así.</p>
<blockquote class="bilingual-quote"><div class="quote-translation" lang="ko"><p>En esencia, TanStack Query gestiona el almacenamiento en caché de las consultas a partir de sus claves. En el nivel superior, las claves de consulta deben ser un arreglo... Siempre que la clave de consulta se pueda serializar y sea <strong>exclusiva de los datos de la consulta</strong>, puede utilizarse.</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>Hay dos ideas esenciales: <strong>debe poder serializarse y debe ser exclusiva de esos datos</strong>. Una misma clave representa los mismos datos, y datos distintos deben tener claves distintas. Esta sencilla regla determina el funcionamiento de todo el sistema de caché.</p>
<p>Hay, además, otro aspecto importante: <strong>queryKey actúa al mismo tiempo como arreglo de dependencias</strong>. Del mismo modo que en <code>useEffect</code> de React el efecto vuelve a ejecutarse cuando cambian sus dependencias, cuando cambia queryKey TanStack Query obtiene automáticamente los datos nuevos.</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>Cuando <code>userId</code> es <code>'A'</code> y cuando es <code>'B'</code>, las queryKey son diferentes. Si son diferentes, se produce un fallo de caché; si hay un fallo de caché, se obtienen los datos. Todo ocurre automáticamente. Gracias a esta sencillez, no tenemos que escribir por nuestra cuenta la lógica de «como userId ha cambiado, hay que volver a obtener los datos».</p>
<p>Aquí surge una pregunta: ¿cómo determina TanStack Query que dos queryKey son «la misma clave»? Si las comparase simplemente con <code>===</code>, las referencias de los objetos serían distintas y se produciría un fallo de caché en cada ocasión.</p>
<h2 id="el-interior-de-querycache"><a class="anchor" href="#el-interior-de-querycache">El interior de QueryCache</a></h2>
<p>Según <a href="https://tkdodo.eu/blog/inside-react-query" target="_blank" rel="noopener noreferrer">El interior de React Query</a>, de TkDodo, <code>QueryCache</code> no es más que <strong>una estructura de datos mantenida en memoria</strong>. Para ser más precisos, en la <a href="https://github.com/TanStack/query/blob/main/packages/query-core/src/queryCache.ts" target="_blank" rel="noopener noreferrer">implementación oficial</a> de v5 esa estructura no es un objeto plano, sino un <code>Map&#x3C;string, Query></code>. Dentro de la clase se declara como <code>#queries = new Map&#x3C;string, Query>()</code>, y todas las escrituras y lecturas se realizan mediante <code>#queries.set(query.queryHash, query)</code> y <code>#queries.get(queryHash)</code>. La clave es la forma serializada de queryKey (<code>queryHash</code>), y el valor es una instancia de la clase <code>Query</code>.</p>
<p>En versiones antiguas también se utilizaron objetos planos, pero en v5 se adoptó el <code>Map</code> nativo. (<code>Map</code> evita las colisiones de claves y el riesgo de contaminación del prototipo, conserva el orden de inserción y ofrece búsquedas por clave de cadena con una complejidad media de O(1), por lo que es una elección casi canónica para una estructura de caché.)</p>
<p>Lo que ocurre cada vez que se llama a <code>useQuery</code> es sencillo: <strong>queryKey se convierte en un valor hash y este se utiliza para buscar en el mapa</strong>. Si existe, se recupera la instancia de <code>Query</code> almacenada en caché; si no, se crea una nueva y se guarda con <code>set</code>.</p>
<p>De aquí se desprende otra pregunta natural: <strong>¿por qué serializar queryKey como una cadena?</strong> ¿No bastaría con usar el propio arreglo como clave, como en <code>Map&#x3C;QueryKey, Query></code>?</p>
<p>La respuesta está en el modelo de igualdad de JavaScript. El <code>Map</code> nativo compara sus claves mediante <strong>igualdad referencial (reference equality)</strong>. Aunque el contenido sea el mismo, considera diferentes dos objetos que ocupan lugares distintos en memoria.</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>Sin embargo, en un componente de React, <code>useQuery({ queryKey: ['user', userId] })</code> <strong>crea una nueva instancia del arreglo en cada renderizado</strong>. Aunque los arreglos queryKey del primer y del segundo renderizado tengan el mismo contenido, son objetos distintos en memoria. Si la caché dependiera de la igualdad referencial, cada renderizado de un componente que mostrase los mismos datos provocaría un fallo de caché.</p>
<p>La solución al problema causado por la igualdad referencial es sencilla: <strong>convertir la igualdad referencial en igualdad estructural (structural equality)</strong>. Se genera una cadena determinista basada únicamente en el contenido de queryKey y se usa esa cadena como clave del mapa. Así se recupera la semántica deseada: «si el contenido es igual, la clave es igual». <code>JSON.stringify</code> no es más que la herramienta más sencilla para realizar esa conversión. (Esta es también la razón por la que TanStack Query, tras probar varias estrategias de serialización durante la época de v3, terminó adoptando una variante estable de <code>JSON.stringify</code>.)</p>
<p>La pieza central es la función que genera ese valor hash: <code>hashKey</code>. La implementación oficial, definida en <a href="https://github.com/TanStack/query/blob/main/packages/query-core/src/utils.ts" target="_blank" rel="noopener noreferrer"><code>packages/query-core/src/utils.ts</code></a>, es exactamente 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>Utiliza <code>JSON.stringify</code>, pero no sin más: introduce una <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#the_replacer_parameter" target="_blank" rel="noopener noreferrer">función de reemplazo</a> que <strong>ordena alfabéticamente las claves de los objetos planos</strong> antes de serializarlos.</p>
<p>Este ordenamiento es esencial porque la serialización a una cadena impone otra condición aún más estricta: <strong>las entradas semánticamente iguales deben convertirse siempre en la misma cadena</strong>. Sin embargo, <code>JSON.stringify</code> normal conserva el orden de las claves. Aunque <code>{ a: 1, b: 2 }</code> y <code>{ b: 2, a: 1 }</code> sean objetos semánticamente iguales, se serializan como cadenas diferentes y terminan ocupando espacios de caché distintos. Así volverían a solicitarse dos veces los mismos datos.</p>
<p>La técnica que evita sistemáticamente este problema es la <strong>forma canónica (canonical form)</strong>. Consiste en obligar a que las entradas semánticamente iguales correspondan siempre a una única representación. Ese es exactamente el motivo por el que la función de reemplazo de <code>hashKey</code> ordena las claves de los objetos planos. Hace que el resultado sea idéntico con independencia del orden de entrada, de modo que el resultado de la serialización quede vinculado de manera unívoca al significado del objeto. En términos matemáticos, selecciona la forma ordenada como elemento representativo de la clase de equivalencia (equivalence class) formada por objetos cuyas claves tienen órdenes distintos.</p>
<p>El hecho de que los arreglos no se ordenen es la otra cara del mismo principio. En un arreglo, el propio orden contiene significado; ordenarlo supondría perder información. El orden de las claves de un objeto es accidental, mientras que el orden de los elementos de un arreglo es intencionado. <code>hashKey</code> trata ambos casos de forma deliberadamente distinta. Por eso la guía oficial recomienda organizar queryKey de «lo genérico a lo específico». Mientras el orden del arreglo aporte significado, el autor debe definirlo expresamente.</p>
<p>Hay otro detalle que conviene señalar: el ordenamiento de claves solo se aplica a los <strong>objetos planos</strong>. <code>isPlainObject</code>, definida en el mismo archivo, no se limita a comprobar <code>typeof === 'object'</code>, sino que verifica incluso <code>Object.getPrototypeOf(o) === Object.prototype</code> para distinguir entre <strong>literales de objeto puros</strong> e <strong>instancias de clase</strong>. Por eso un literal como <code>{ foo: 1 }</code> se ordena, mientras que una instancia creada con <code>class User { ... }</code> pasa sin ordenarse. (De aquí surge el riesgo de que, si se introduce directamente una instancia de clase en queryKey, se genere un hash distinto del esperado debido a que <code>JSON.stringify</code> solo emite las propiedades enumerables.)</p>
<p>Este funcionamiento tiene dos consecuencias importantes.</p>
<p><strong>1. El orden de las claves de un objeto es irrelevante.</strong></p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">useQuery</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({ queryKey: [</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'todos'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, { status: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'done'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, page: </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">1</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> }], queryFn });</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">useQuery</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({ queryKey: [</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'todos'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, { page: </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">1</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, status: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'done'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> }], queryFn });</span></span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 두 쿼리는 같은 캐시 슬롯을 공유한다</span></span></code></pre></figure>
<p>La razón es que las claves se ordenan antes de serializarse. Sin este proceso, al escribir un literal de objeto habría que recordar siempre el orden de sus claves.</p>
<p><strong>2. El orden de los elementos de un arreglo sí importa.</strong></p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">useQuery</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({ queryKey: [</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'todos'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, status, page], queryFn });</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">useQuery</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({ queryKey: [</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'todos'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, page, status], queryFn });</span></span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 두 쿼리는 다른 캐시이다</span></span></code></pre></figure>
<p>Esto se debe a que un arreglo es una estructura de datos en la que el propio orden tiene significado. <code>JSON.stringify</code> también conserva el orden de los arreglos.</p>
<p>También conviene saber que los valores <code>undefined</code> desaparecen durante la serialización. <code>{ a: 1, b: undefined }</code> y <code>{ a: 1 }</code> generan el mismo hash. (Yo mismo cometí una vez el error de pensar: «¡Como he añadido undefined de forma explícita, será otra caché!».)</p>
<p>Además, queryKey no puede contener <strong>referencias circulares ni funciones</strong>, porque <code>JSON.stringify</code> no puede procesarlas. Por el mismo motivo, tampoco se recomienda utilizar con su comportamiento predeterminado objetos <code>Date</code>, <code>Map/Set</code>, <code>BigInt</code> y similares. Debe ser una estructura de datos pura y serializable.</p>
<p>Lo interesante es que esta restricción no se impone por completo. Mediante la opción <code>queryKeyHashFn</code>, TanStack Query ofrece una <strong>vía de escape que permite sustituir la propia función hash</strong>. Internamente, <code>hashQueryKeyByOptions(queryKey, options)</code> comprueba si las opciones incluyen <code>queryKeyHashFn</code>: si existe, la llama; si no, utiliza la función <code>hashKey</code> predeterminada.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">useQuery</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  queryKey: [{ id: userId, fetchedAt: </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">new</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> Date</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">() }],</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  queryFn,</span></span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">  // Date를 ISO 문자열로 바꿔서 해싱</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  queryKeyHashFn</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: (</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">key</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span></span>
<span data-line=""><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">    JSON</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">stringify</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(key, (</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">_</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">v</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (v </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">instanceof</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> Date</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> ?</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> v.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">toISOString</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">() </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> v)),</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">});</span></span></code></pre></figure>
<p>Sin embargo, esta opción debe configurarse por separado para cada consulta y no se aplica en API imperativas como <code>queryClient.setQueryData</code>, que se invocan sin conocer esas opciones (<a href="https://github.com/TanStack/query/issues/1343" target="_blank" rel="noopener noreferrer">incidencia n.º 1343</a>). Por eso, en la práctica es mucho más seguro evitar esta vía de escape y <strong>convertir queryKey a una forma serializable en el momento de crearla</strong>. (Yo también introduje una vez un <code>Date</code> directamente y pasé bastante tiempo preguntándome: «¿Por qué no se actualiza la caché si es el mismo instante?». La respuesta final fue: «Ese <code>Date</code> representa el mismo instante, pero es otra instancia de objeto y genera un hash distinto cada vez».)</p>
<h2 id="reglas-para-escribir-querykey"><a class="anchor" href="#reglas-para-escribir-querykey">Reglas para escribir queryKey</a></h2>
<p>Una vez comprendido el complejo funcionamiento interno anterior, las reglas de escritura se deducen de forma natural. Las recomendaciones de la documentación oficial pueden resumirse así.</p>
<p><strong>Regla 1. queryKey debe ser siempre un arreglo.</strong></p>
<p>Aunque pasar una cadena también funciona (internamente se convierte en un arreglo), conviene usar un arreglo desde el principio para mantener la coherencia.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 비권장</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">useQuery</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({ queryKey: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'todos'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, queryFn });</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 권장</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">useQuery</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({ queryKey: [</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'todos'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">], queryFn });</span></span></code></pre></figure>
<p><strong>Regla 2. Incluye en queryKey todas las variables de las que depende 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:#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>Es la misma forma de pensar que con las dependencias de <code>useEffect</code>. Todas las variables utilizadas dentro de la función deben formar parte de la clave (= dependencia). Si se incumple esta regla, pueden aparecer errores difíciles de rastrear, como que los datos del usuario anterior sigan mostrándose después de cambiar a otro usuario.</p>
<p><strong>Regla 3. Organiza los elementos desde el más genérico hasta el más específico.</strong></p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 좋다</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">[</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'todos'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'list'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, { filter: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'done'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> }]</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">[</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'todos'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'detail'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, todoId]</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 안 좋다 (순서가 뒤집혀 있음)</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">[{ filter: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'done'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> }, </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'list'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'todos'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">]</span></span></code></pre></figure>
<p>Este orden es importante por la <strong>invalidación (invalidation)</strong>. De forma predeterminada, <code>invalidateQueries</code> de TanStack Query utiliza <strong>coincidencia por prefijo</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>Si las claves se diseñan como una estructura de árbol, se puede expresar en una sola línea desde «vuelve a obtener todos los datos de este dominio» hasta «vuelve a obtener únicamente este elemento concreto». (A primera vista puede parecer poco importante, pero su valor se vuelve evidente después de diseñarlo mal una vez y comprobar que el alcance de la invalidación no se comporta como esperabas.)</p>
<h2 id="evolución-de-la-gestión-de-querykey"><a class="anchor" href="#evolución-de-la-gestión-de-querykey">Evolución de la gestión de queryKey</a></h2>
<p>Hasta aquí hemos visto el funcionamiento y el uso de queryKey. Pasemos ahora a la pregunta principal: <strong>¿cómo ha ido cambiando su gestión?</strong></p>
<p>Voy a ordenar cronológicamente las etapas por las que he pasado en proyectos reales.</p>
<h3 id="1-arreglos-en-línea"><a class="anchor" href="#1-arreglos-en-línea">1. Arreglos en línea</a></h3>
<p>Es la forma más sencilla. Dentro del componente se combinan cadenas fijas con valores de las propiedades.</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>Al comenzar, esto puede ser suficiente.</p>
<p>El problema aparece cuando crece la base de código. Hay que invalidar desde una mutación que modifica la información del usuario, pero cada vez es necesario buscar «¿cuál era la clave de las consultas relacionadas con usuarios?». Unos lugares utilizan <code>['user', userId]</code> y otros <code>['users', userId]</code> (en plural). Como ocupan espacios de caché completamente distintos, la invalidación solo se aplica a uno de ellos.</p>
<h3 id="2-objeto-de-constantes"><a class="anchor" href="#2-objeto-de-constantes">2. Objeto de constantes</a></h3>
<p>Para evitar errores tipográficos, las claves de consulta se agrupan como 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>Los errores tipográficos desaparecen, pero la responsabilidad de componer la clave sigue recayendo en cada lugar de uso. Alguien escribe la combinación <code>[QUERY_KEYS.USER, userId]</code> como <code>[QUERY_KEYS.USER, userId, 'detail']</code>, mientras que otra persona usa <code>['user', 'detail', userId]</code>. Llega un momento en el que hay que memorizar por separado qué convención es la correcta.</p>
<h3 id="3-fábrica-de-claves-de-consulta"><a class="anchor" href="#3-fábrica-de-claves-de-consulta">3. Fábrica de claves de consulta</a></h3>
<p>Este patrón quedó concretado en el artículo <a href="https://tkdodo.eu/blog/effective-react-query-keys" target="_blank" rel="noopener noreferrer">Claves eficaces para React Query</a>, de TkDodo. Consiste en definir un objeto que crea las claves de cada dominio y expresar la jerarquía mediante funciones.</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>Este patrón es potente porque <strong>la jerarquía queda expresada de forma explícita en el código</strong>. <code>todoKeys.all</code> apunta a todas las consultas relacionadas con tareas, <code>todoKeys.lists()</code> a todas las consultas de tipo lista y <code>todoKeys.detail(1)</code> a un elemento concreto. El alcance de la invalidación puede expresarse con precisión en una línea de código.</p>
<p>Otra ventaja es la <strong>colocación conjunta (co-location)</strong>. TkDodo no recomienda reunir las claves en un archivo global. En su lugar, propone colocar <code>queries.ts</code> dentro del directorio de la funcionalidad y mantener allí tanto las claves como los hooks.</p>
<pre><code>src/
└── features/
    └── todos/
        ├── index.tsx
        └── queries.ts   # 키와 훅을 모두 여기에
</code></pre>
<p>Así se obtiene un modelo mental sencillo: «para modificar algo relacionado con tareas, basta con mirar la carpeta de tareas». Es una aplicación fiel del principio de mantener juntas las cosas que cambian juntas.</p>
<h3 id="4-lukemoralesquery-key-factory"><a class="anchor" href="#4-lukemoralesquery-key-factory">4. @lukemorales/query-key-factory</a></h3>
<p>Al escribir manualmente una y otra vez el tercer patrón, el código repetitivo se acumula. Además, cuando se quieren combinar y gestionar las claves de varios dominios, se echa en falta una interfaz normalizada. <a href="https://github.com/lukemorales/query-key-factory" target="_blank" rel="noopener noreferrer">@lukemorales/query-key-factory</a> es el resultado de convertir este patrón en una librería.</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> añade automáticamente el prefijo, y <code>mergeQueryKeys</code> permite combinar dominios. Además, la propiedad convenida <code>_def</code> da acceso a la clave de todo el dominio. Desaparece así la necesidad de añadir <code>as const</code> en cada fábrica manual para restringir los tipos por cuenta propia.</p>
<p>Durante un tiempo, esta librería se utilizó prácticamente como un estándar. (Yo también la usé con frecuencia durante bastante tiempo.) Sin embargo, la aparición de queryOptions cambió la situación.</p>
<h3 id="5-queryoptions-api-oficial-de-v5"><a class="anchor" href="#5-queryoptions-api-oficial-de-v5">5. queryOptions (API oficial de v5)</a></h3>
<p>Uno de los cambios más importantes de TanStack Query v5 fue la introducción de la API <code>queryOptions</code>. Con el paso de v4 a v5, los argumentos de todos los hooks se unificaron en un único objeto. El verdadero propósito de ese cambio era permitir extraer ese objeto como <strong>unidad reutilizable</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>A primera vista puede surgir la duda: «¿Qué tiene esto de diferente? Parece simplemente un objeto envuelto en una función». TkDodo también lo reconoce en su artículo <a href="https://tkdodo.eu/blog/the-query-options-api" target="_blank" rel="noopener noreferrer">La API Query Options</a>. Durante la ejecución, se limita realmente a devolver el mismo objeto que recibe.</p>
<p>El trabajo verdaderamente útil ocurre <strong>dentro del sistema de tipos</strong>. Veámoslo a continuación.</p>
<h2 id="datatag-de-queryoptions"><a class="anchor" href="#datatag-de-queryoptions">DataTag de queryOptions</a></h2>
<p><code>queryOptions</code> no es una simple función auxiliar porque <strong>incorpora información sobre el tipo de los datos en la queryKey devuelta</strong>. Dentro de TanStack Query, este mecanismo se denomina <code>DataTag</code>.</p>
<p>Su implementación aproximada es la siguiente.</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>Se trata de un <strong>tipo con marca (branded type)</strong> basado en <code>unique symbol</code>. Es solo una marca sin ningún efecto durante la ejecución, pero para TypeScript contiene la información de que «este arreglo no es un simple arreglo, sino un arreglo asociado a datos del tipo <code>TValue</code>».</p>
<p>Existe una razón concreta para utilizar <code>unique symbol</code>. El artículo de Zenn <a href="https://zenn.dev/tsuboi/articles/tanstack-query-options-unique-symbol?locale=en" target="_blank" rel="noopener noreferrer">El unique symbol que se oculta tras DataTag</a> compara este recurso con «una plaza de aparcamiento exclusiva para la información de tipos». Si se utilizase una clave de cadena normal, podría colisionar con una clave de otra librería o del código del usuario; sin embargo, <strong>cada declaración <code>unique symbol</code> crea por sí misma un tipo único</strong>, por lo que nunca coincide con otra declaración. Se convierte así en un identificador que no puede colisionar.</p>
<p>La diferencia que produce este único mecanismo es considerable.</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>Aunque <code>getQueryData</code> y <code>setQueryData</code> solo reciben una queryKey, como esta ya lleva incorporado el tipo de datos, el tipo devuelto se infiere automáticamente. No es necesario proporcionar los genéricos a mano y, si se intenta introducir en <code>setQueryData</code> un valor de un tipo incorrecto, el compilador lo detecta de inmediato.</p>
<p>Por supuesto, también existen limitaciones. En métodos como <code>getQueriesData</code>, que obtienen varias consultas a la vez, el resultado es un arreglo de tuplas heterogéneas y no se aplica la inferencia de tipos. Además, el uso de <code>unique symbol</code> puede provocar un error TS4023 al generar archivos <code>.d.ts</code> en un monorepositorio; se evita importando <code>dataTagSymbol</code> de forma explícita.</p>
<p>Al resumir el mecanismo visto hasta aquí, queda claro un hecho: <strong>la inferencia de tipos de queryOptions depende por completo de que queryKey y queryFn se declaren juntas en un mismo lugar</strong>. Para incorporar en queryKey el tipo que devuelve queryFn, ambas deben declararse juntas.</p>
<p>Esto tiene una implicación de peso para la dirección de diseño de las fábricas de claves de consulta. Los patrones de la generación anterior se centraban en separar la gestión de queryKey como una unidad de abstracción independiente. Sin embargo, la recomendación de v5 va en la dirección opuesta: <strong>volver a unir queryKey y queryFn en una sola unidad</strong>. TkDodo llega a afirmar que «separar queryKey y queryFn fue un error». Al fin y al cabo, la clave es el conjunto de dependencias que utiliza la función, y ambas mantienen una relación inseparable.</p>
<h2 id="patrón-práctico-de-composición-con-queryoptions"><a class="anchor" href="#patrón-práctico-de-composición-con-queryoptions">Patrón práctico de composición con queryOptions</a></h2>
<p>El verdadero potencial de <code>queryOptions</code> aparece al combinarla con una fábrica por dominio. La forma recomendada por la documentación oficial de v5 es la siguiente.</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>Veamos una por una las razones por las que este patrón resulta útil.</p>
<p><strong>1. Permite obtener al mismo tiempo jerarquía e inferencia de tipos.</strong></p>
<p><code>todoQueries.all()</code> y <code>todoQueries.lists()</code> devuelven simples arreglos, mientras que <code>todoQueries.detail(1)</code> devuelve mediante <code>queryOptions</code> un objeto con una etiqueta de datos. Para invalidar se usa el arreglo; para invocar la consulta, el objeto de opciones.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">useQuery</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(todoQueries.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">detail</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">1</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">));                                </span><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 옵션 객체</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">queryClient.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">invalidateQueries</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({ queryKey: todoQueries.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">all</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">() }); </span><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 배열</span></span></code></pre></figure>
<p><strong>2. Los componentes pueden sobrescribir parcialmente las opciones.</strong></p>
<p>Como el resultado de <code>queryOptions</code> sigue siendo un objeto, en el momento de la llamada pueden combinarse algunas opciones.</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>Este patrón es especialmente potente porque el tipo devuelto por <code>select</code> se infiere automáticamente y el tipo de <code>data</code> se restringe a <code>string</code>. Desde el punto de vista del componente, es posible seleccionar solo la parte necesaria y mantener la definición del dominio intacta en un único lugar.</p>
<p><strong>3. Los hooks personalizados que envuelven <code>useQuery</code> van desapareciendo.</strong></p>
<p>Durante la época de v4, el patrón habitual consistía en crear hooks personalizados para cada dominio.</p>
<p>El problema era que, <strong>en cuanto se necesitaba una precarga, había que volver a escribir la misma definición</strong>. Como <code>useTodoDetail</code> no puede llamarse fuera de un componente, en el cargador del enrutador o en un manejador de eventos había que volver a escribir <code>queryClient.prefetchQuery({ queryKey: [...], queryFn: ... })</code>.</p>
<p>Con <code>queryOptions</code>, esa duplicación desaparece.</p>
<p>Una sola definición funciona en cualquier lugar. Por eso TkDodo recomienda «en v5, define queryOptions en lugar de crear hooks». Estos hooks quedan como una capa ligera que solo se añade cuando es necesaria, mientras que la definición del dominio puede existir de manera autosuficiente sin ellos.</p>
<h2 id="invalidación-tras-una-mutación"><a class="anchor" href="#invalidación-tras-una-mutación">Invalidación tras una mutación</a></h2>
<p>El lugar en el que la jerarquía de queryKey brilla de verdad es la invalidación posterior a una mutación. Según la documentación de TanStack Query sobre <a href="https://tanstack.com/query/v5/docs/framework/react/guides/query-invalidation" target="_blank" rel="noopener noreferrer">invalidación de consultas</a>, <code>invalidateQueries</code> utiliza de forma predeterminada la <strong>coincidencia por prefijo</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 관련 쿼리 (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>Si las claves se diseñan de forma jerárquica, <strong>el alcance de la invalidación coincide con el significado del código</strong>. «Actualiza todas las tareas» se expresa con <code>all()</code>, «actualiza solo las listas» con <code>lists()</code> y «actualiza solo este elemento» con <code>detail(id)</code>.</p>
<p>Si las claves estuvieran dispersas de forma plana, como <code>['todoList']</code> y <code>['todoDetail', 1]</code>, para invalidar «todo el dominio de tareas» habría que realizar dos llamadas separadas o crear y gestionar una constante de prefijo adicional. (Y, si al añadir una nueva clave al dominio se olvidase incorporarla a esa constante, se produciría un error que la dejaría fuera de la invalidación.)</p>
<h2 id="recuperar-querykey-dentro-de-queryfn"><a class="anchor" href="#recuperar-querykey-dentro-de-queryfn">Recuperar queryKey dentro de queryFn</a></h2>
<p>Por último, hay otro patrón que merece atención. En realidad, <code>queryFn</code> recibe como argumento un objeto llamado <code>QueryFunctionContext</code>, que contiene la queryKey del momento de la llamada.</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 qué resulta útil este patrón? Según <a href="https://tkdodo.eu/blog/leveraging-the-query-function-context" target="_blank" rel="noopener noreferrer">Cómo aprovechar el contexto de la función de consulta</a>, de TkDodo, permite <strong>forzar la sincronización entre las dependencias de queryKey y 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>Este código es peligroso porque queryFn depende de una variable externa. Además, aunque cambie <code>sortBy</code>, la caché no se actualiza porque esa dependencia no está incluida en la clave. Mientras <code>queryFn</code> tome variables de un cierre externo, siempre será posible cometer este error.</p>
<p>La solución es sencilla: hacer que <code>queryFn</code> no dependa de variables externas. <strong>Si todas las dependencias se extraen de queryKey</strong>, una variable que no figure en queryKey ni siquiera podrá utilizarse dentro de la función.</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>Con esta estructura, cuando aparece una dependencia nueva no existe forma de usarla dentro de la función sin añadirla a queryKey. El compilador detectará que «esa clave no existe». La sincronización entre la clave y la función deja de depender de una convención y se <strong>delega al sistema de tipos</strong>.</p>
<h2 id="hasta-dónde-separar"><a class="anchor" href="#hasta-dónde-separar">Hasta dónde separar</a></h2>
<p>Después de leer todo lo anterior puede surgir una pregunta: «Entonces, ¿hay que extraer todas las consultas a <code>queryOptions</code>?».</p>
<p>Mi respuesta, como casi siempre, es: <strong>«depende de la situación»</strong>.</p>
<p>Conviene recordar que <strong>una abstracción no siempre es beneficiosa</strong>. Si una consulta solo se utiliza una vez, extraerla innecesariamente a una fábrica de dominio solo obliga a quien lee el código a desplazarse entre dos archivos. La evolución de los patrones de gestión de queryKey no significa «hay que usar siempre la herramienta más sofisticada», sino que <strong>«existe la posibilidad de subir un peldaño cuando resulte necesario»</strong>.</p>
<h2 id="conclusión"><a class="anchor" href="#conclusión">Conclusión</a></h2>
<p>En resumen, queryKey es <strong>la unidad fundamental con la que TanStack Query identifica y almacena en caché los datos asíncronos</strong>. En ese pequeño arreglo se concentran el identificador del espacio de caché, el arreglo de dependencias, el alcance de la invalidación y, desde v5, incluso la información sobre el tipo de los datos. Precisamente porque tantas responsabilidades convergen en un único punto, la forma de escribirlo y gestionarlo influye directamente en la carga cognitiva de toda la base de código.</p>
<p>Cada etapa fue la respuesta a un problema real que alguien encontró en su momento. Por eso, la conclusión no debe ser simplemente «como ahora estamos en v5, hay que usar exclusivamente <code>queryOptions</code>», sino <strong>«primero hay que identificar qué clase de problema está experimentando actualmente mi base de código»</strong>. Introducir una fábrica de dominio en un proyecto donde bastan los arreglos en línea puede constituir por sí mismo un exceso de diseño.</p>
<p>Espero que quienes lean este artículo revisen también sus propios proyectos: cómo se distribuyen las queryKey por toda la base de código, cómo se realizan las invalidaciones y si esa estructura se ajusta al tamaño actual del equipo y a la complejidad del dominio.</p>
<h2 id="referencias"><a class="anchor" href="#referencias">Referencias</a></h2>
<details class="ref-block"><summary class="ref-summary">참고 자료</summary><div class="ref-list">
<div class="ref-item">
<div class="ref-item-inner">[documentación] <a href="https://tanstack.com/query/latest/docs/framework/react/guides/query-keys" target="_blank" rel="noopener noreferrer">TanStack Query, claves de consulta</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner">[documentación] <a href="https://tanstack.com/query/v5/docs/framework/react/guides/query-options" target="_blank" rel="noopener noreferrer">TanStack Query, opciones de consulta</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner">[documentación] <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">[artículo] <a href="https://tanstack.com/blog/announcing-tanstack-query-v5" target="_blank" rel="noopener noreferrer">TanStack, presentación de TanStack Query v5</a></div>
</div>
</div></details>]]></content:encoded>
            <author>jihoon7705@gmail.com (이지훈)</author>
            <category>프론트엔드</category>
            <category>React</category>
            <category>TanStack-Query</category>
            <category>queryKey</category>
        </item>
        <item>
            <title><![CDATA[Gestión de errores]]></title>
            <link>https://hooninedev.com/es/251117</link>
            <guid isPermaLink="false">https://hooninedev.com/es/251117</guid>
            <pubDate>Mon, 17 Nov 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[En este artículo quiero hablar de cómo capturar errores en el frontend. Durante mi experiencia profesional, muchas veces he sentido cierta incomodidad al escribir código para gestionar errores. Alguno...]]></description>
            <content:encoded><![CDATA[<p>En este artículo quiero hablar de <strong>cómo capturar errores en el frontend</strong>.</p>
<p>Durante mi experiencia profesional, muchas veces he sentido cierta incomodidad al escribir código para gestionar errores. Algunos se capturan con <code>try/catch</code>, otros con <code>ErrorBoundary</code> y otros mediante <code>onError</code> de TanStack Query. Además, sus ámbitos se solapan o divergen de formas sutiles. Por eso, a veces un error se escapa y otras se propaga hasta lugares donde no debería llegar.</p>
<p>El problema es que rara vez nos detenemos a ordenar de una vez cómo funcionan todas estas herramientas. Sabemos que «Error Boundary solo captura errores durante el renderizado», pero, si tuviéramos que explicar exactamente qué significa eso en la práctica, qué ocurre internamente al llamar a <code>reset</code> o en qué momento TanStack Query vuelve a lanzar un error cuando <code>throwOnError</code> está activado, probablemente no sabríamos responder.</p>
<p>Basándome en la guía oficial de React, la biblioteca <code>react-error-boundary</code> y la documentación oficial de TanStack Query v5, en este artículo explicaré <strong>hasta dónde llega la responsabilidad</strong> de cada herramienta de gestión de errores del frontend y <strong>cómo combinarlas</strong>.</p>
<h2 id="errores-que-react-puede-capturar-y-errores-que-no-puede-capturar"><a class="anchor" href="#errores-que-react-puede-capturar-y-errores-que-no-puede-capturar">Errores que React puede capturar y errores que no puede capturar</a></h2>
<p>Empecemos por la pregunta más básica: <strong>¿qué errores captura React?</strong></p>
<p>La documentación oficial de React distingue claramente entre los errores que Error Boundary puede capturar y los que no.</p>
<p><strong>Ámbito que captura Error Boundary</strong></p>
<ul>
<li>Errores producidos durante el <strong>renderizado</strong> de componentes descendientes</li>
<li>Errores producidos dentro de <strong>métodos del ciclo de vida</strong></li>
<li>Errores producidos en el <strong>constructor</strong></li>
</ul>
<p><strong>Ámbito que Error Boundary no puede capturar</strong></p>
<ul>
<li>Errores dentro de <strong>manejadores de eventos</strong></li>
<li>Errores de código asíncrono como <code>setTimeout</code>, <code>requestAnimationFrame</code> o <strong>Promise</strong></li>
<li>Errores durante el <strong>renderizado del lado del servidor (SSR)</strong></li>
<li>Errores producidos en el <strong>propio Error Boundary</strong></li>
</ul>
<p>¿Por qué es importante esta distinción? En realidad, la mayoría de los errores que tratamos habitualmente <strong>pertenecen a la segunda categoría.</strong> Por ejemplo, un servidor que devuelve un 500 tras ejecutar una mutación al pulsar un botón, una solicitud que falla dentro de <code>useEffect</code> o una lógica de validación que lanza una excepción al enviar un formulario. React no captura automáticamente estos errores. Debemos capturarlos y gestionarlos de forma explícita.</p>
<p>Por eso, la gestión de errores del frontend se divide en dos ramas: <strong>los errores de renderizado se tratan con Error Boundary</strong> y <strong>los demás, con try/catch o con manejadores proporcionados por bibliotecas</strong>. En el punto donde se cruzan ambas ramas, las bibliotecas de estado asíncrono como TanStack Query actúan como puente.</p>
<h2 id="qué-es-realmente-un-error-boundary"><a class="anchor" href="#qué-es-realmente-un-error-boundary">Qué es realmente un Error Boundary</a></h2>
<p>Un Error Boundary no es más que un <strong>componente de clase</strong> con dos métodos del ciclo de vida. Según la documentación oficial de React, para convertirse en Error Boundary debe implementar uno de los dos métodos siguientes, aunque normalmente implementa ambos.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="js" data-theme="github-dark github-light"><code data-language="js" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">class</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> ErrorBoundary</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> extends</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> React</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">Component</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  constructor</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">props</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) {</span></span>
<span data-line=""><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">    super</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(props);</span></span>
<span data-line=""><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">    this</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">.state </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { hasError: </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">false</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> };</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">  // 에러 발생 시 state를 업데이트해 다음 렌더에서 fallback UI를 보여준다</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  static</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> getDerivedStateFromError</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">error</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">    return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { hasError: </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">true</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> };</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">  // 에러가 발생한 직후에 호출. 로깅 같은 사이드이펙트는 여기서 처리한다</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  componentDidCatch</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">error</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">info</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) {</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">    logErrorToMyService</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(error, info.componentStack);</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  render</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">() {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">    if</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">this</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">.state.hasError) {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">      return</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> this</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">.props.fallback;</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">    return</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> this</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">.props.children;</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  }</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p><code>getDerivedStateFromError</code> debe ser una <strong>función pura</strong>. Su única función es devolver un nuevo estado, sin efectos secundarios. En cambio, <code>componentDidCatch</code> es el lugar destinado a esos efectos. Ahí se envía el error a Sentry o se imprime la pila de componentes en la consola.</p>
<p>Hay un detalle importante: estos dos métodos <strong>solo existen en los componentes de clase.</strong> Aún no hay una forma oficial de crear un Error Boundary con un componente funcional. La <a href="https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary" target="_blank" rel="noopener noreferrer">documentación oficial de React</a> también lo indica expresamente.</p>
<blockquote class="bilingual-quote"><div class="quote-translation" lang="ko"><p>Actualmente no existe ninguna forma de escribir un Error Boundary como componente funcional.</p></div><div class="quote-original" lang="en"><p>There is currently no way to write an Error Boundary as a function component.</p></div></blockquote>
<p>Como resulta engorroso escribir un componente de clase cada vez, lo habitual es utilizar la biblioteca <code>react-error-boundary</code>. (La creó directamente Brian Vaughn, antiguo miembro del equipo de mantenimiento de React, y en la práctica se utiliza como un estándar.)</p>
<h2 id="las-tres-formas-de-definir-la-interfaz-alternativa-en-react-error-boundary"><a class="anchor" href="#las-tres-formas-de-definir-la-interfaz-alternativa-en-react-error-boundary">Las tres formas de definir la interfaz alternativa en react-error-boundary</a></h2>
<p>La biblioteca <code>react-error-boundary</code> permite especificar la interfaz alternativa de su componente <code>ErrorBoundary</code> mediante propiedades de <strong>tres formas</strong>. Veamos brevemente cómo se utiliza cada una.</p>
<h3 id="fallback"><a class="anchor" href="#fallback">fallback</a></h3>
<p>Es la forma más sencilla: se pasa JSX estático.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">ErrorBoundary</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> fallback</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{&#x3C;</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">div</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">>문제가 발생했습니다.&#x3C;/</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">div</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">>}></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Page</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> /></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">ErrorBoundary</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span></code></pre></figure>
<p>Se utiliza cuando no es necesario acceder al objeto de error ni a la función de restablecimiento. En la práctica, normalmente hace falta mostrar un mensaje o permitir un reintento, así que hasta ahora no he tenido ocasión de usarla en proyectos reales.</p>
<h3 id="fallbackcomponent"><a class="anchor" href="#fallbackcomponent">FallbackComponent</a></h3>
<p>La interfaz alternativa se separa en otro componente y se pasa su <strong>referencia</strong>.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">function</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> ErrorFallback</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({ </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">error</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">resetErrorBoundary</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> }) {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    &#x3C;</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">div</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> role</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"alert"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">      &#x3C;</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">p</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">>오류가 발생했습니다.&#x3C;/</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">p</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">      &#x3C;</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">pre</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">>{error.message}&#x3C;/</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">pre</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">      &#x3C;</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">button</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> onClick</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{resetErrorBoundary}>다시 시도&#x3C;/</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">button</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    &#x3C;/</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">div</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  );</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">ErrorBoundary</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> FallbackComponent</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{ErrorFallback}></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Page</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> /></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">ErrorBoundary</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span></code></pre></figure>
<p>El objeto de error y la función <code>resetErrorBoundary</code> se inyectan automáticamente como propiedades. Es una opción clara cuando la interfaz alternativa puede reutilizarse en otros lugares.</p>
<h3 id="fallbackrender"><a class="anchor" href="#fallbackrender">fallbackRender</a></h3>
<p>Se utiliza cuando se quiere renderizar la interfaz alternativa en línea.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">ErrorBoundary</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  fallbackRender</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{({ </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">error</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">resetErrorBoundary</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> }) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    &#x3C;</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">div</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> role</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"alert"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">      &#x3C;</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">p</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">>오류가 발생했습니다: {error.message}&#x3C;/</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">p</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">      &#x3C;</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">button</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> onClick</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{resetErrorBoundary}>다시 시도&#x3C;/</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">button</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    &#x3C;/</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">div</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  )}</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Page</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> /></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">ErrorBoundary</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span></code></pre></figure>
<p>En esencia hace lo mismo que <code>FallbackComponent</code>, pero permite <strong>resolverlo en línea sin crear un componente separado</strong>. Es útil cuando se necesita acceder a un cierre léxico externo, como el estado o los manejadores del componente padre.</p>
<p>No existe una única opción correcta. El patrón que utilizo con frecuencia consiste en <strong>crear un componente ErrorFallback común e inyectarlo mediante <code>FallbackComponent</code></strong>, porque el sistema de diseño y el tono deben mantenerse uniformes. Solo escribo un <code>fallbackRender</code> en línea cuando una página necesita una interfaz alternativa diferente.</p>
<h2 id="qué-hace-realmente-el-restablecimiento"><a class="anchor" href="#qué-hace-realmente-el-restablecimiento">¿Qué hace realmente el restablecimiento?</a></h2>
<p>Al utilizar <code>react-error-boundary</code>, tarde o temprano aparece la función <code>resetErrorBoundary</code>: la que se ejecuta al pulsar el botón «Reintentar» de la interfaz alternativa. Veamos qué hace en realidad.</p>
<p>En pocas palabras, <code>resetErrorBoundary</code> solo indica al componente ErrorBoundary que <strong>reinicie su propio estado y vuelva a renderizar los elementos secundarios</strong>. No modifica automáticamente ningún estado externo, como la caché de TanStack Query.</p>
<p>Paso a paso, esto es lo que ocurre internamente:</p>
<ol>
<li>Se llama a <code>resetErrorBoundary()</code>.</li>
<li>El estado <code>hasError</code> interno de ErrorBoundary vuelve a <code>false</code>.</li>
<li>Opcionalmente, se ejecuta la función <code>onReset</code>. Aquí tienen lugar los efectos secundarios definidos por el usuario.</li>
<li>Se vuelven a renderizar los elementos secundarios. Si la causa del error, como el estado o la caché, sigue presente, <strong>se vuelve a lanzar el mismo error.</strong></li>
</ol>
<p>El cuarto punto es esencial. <strong>El restablecimiento solo significa «olvidemos el error e intentemos renderizar otra vez», no «corrijamos su causa».</strong> Por eso, limitarse a restablecer el estado puede provocar que el mismo error se repita indefinidamente.</p>
<p>Para resolver este problema existen dos herramientas adicionales.</p>
<h3 id="onreset"><a class="anchor" href="#onreset">onReset</a></h3>
<p>Actúa como un punto de extensión que se ejecuta justo antes del restablecimiento. Aquí se limpia el estado externo que originó el error.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">ErrorBoundary</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  FallbackComponent</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{ErrorFallback}</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  onReset</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{() </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    queryClient.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">invalidateQueries</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({ queryKey: [</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'user'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">] });</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  }}</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Page</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> /></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">ErrorBoundary</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span></code></pre></figure>
<h3 id="resetkeys"><a class="anchor" href="#resetkeys">resetKeys</a></h3>
<p>Cuando cambia alguno de los valores de la lista, ErrorBoundary se restablece automáticamente. Se pasan claves para las que tenga sentido volver a intentarlo si cambian, como un parámetro de URL, un término de búsqueda o la pestaña seleccionada.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">ErrorBoundary</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  FallbackComponent</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{ErrorFallback}</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  resetKeys</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{[userId]}</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">UserProfile</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> userId</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{userId} /></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">ErrorBoundary</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span></code></pre></figure>
<p>Cuando cambia <code>userId</code>, se produce un restablecimiento automático y los elementos secundarios vuelven a renderizarse. Si el usuario accede a otro perfil, el error anterior desaparece de forma natural.</p>
<h2 id="cómo-se-capturan-los-errores-de-manejadores-de-eventos-y-del-código-asíncrono"><a class="anchor" href="#cómo-se-capturan-los-errores-de-manejadores-de-eventos-y-del-código-asíncrono">¿Cómo se capturan los errores de manejadores de eventos y del código asíncrono?</a></h2>
<p>Ya hemos visto que Error Boundary no puede capturar errores de manejadores de eventos ni de código asíncrono. Sin embargo, la mayoría de los errores con los que trabajamos nacen ahí. ¿Qué podemos hacer?</p>
<p>Para resolverlo, <code>react-error-boundary</code> ofrece el <strong>hook <code>useErrorBoundary</code></strong>. Este devuelve una función llamada <code>showBoundary</code>; al invocarla, se puede enviar el error de forma explícita al ErrorBoundary más cercano.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { useErrorBoundary } </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">from</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> 'react-error-boundary'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">function</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> MyComponent</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">() {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">showBoundary</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> } </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> useErrorBoundary</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">();</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> handleClick</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> async</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> () </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">    try</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">      await</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> someAsyncOperation</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    } </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">catch</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (error) {</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">      showBoundary</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(error);</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    }</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  };</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> &#x3C;</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">button</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> onClick</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{handleClick}>실행&#x3C;/</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">button</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">>;</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>La clave es que <strong>el desarrollador debe elevar el error explícitamente</strong>. React no lo hace por sí solo. Para trasladar un error asíncrono al ámbito de ErrorBoundary hay que capturarlo con <code>try/catch</code> y pasarlo a <code>showBoundary</code>.</p>
<p>Con este patrón, la pregunta «¿por qué ErrorBoundary captura unos errores y no otros?» queda resuelta con claridad. La respuesta es sencilla: <strong>«¿se elevó hasta la fase de renderizado o no?»</strong>.</p>
<h2 id="cómo-gestiona-los-errores-tanstack-query"><a class="anchor" href="#cómo-gestiona-los-errores-tanstack-query">¿Cómo gestiona los errores TanStack Query?</a></h2>
<p>Después de ordenar todo lo anterior surge una pregunta natural. <code>useQuery</code>, que usamos a diario, trabaja con solicitudes asíncronas; ¿cómo se gestionan los errores que se producen en ellas?</p>
<p>De forma predeterminada, TanStack Query <strong>expone el error en el campo <code>error</code></strong>.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">data</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">error</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">isError</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> } </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> useQuery</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  queryKey: [</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'todos'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">],</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  queryFn: fetchTodos,</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">});</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">if</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (isError) {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> &#x3C;</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">div</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">>에러: {error.message}&#x3C;/</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">div</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">>;</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>Esta es la forma más sencilla. Aunque se produzca un error, el componente sigue renderizándose normalmente y el valor simplemente queda almacenado en el campo <code>error</code>. ErrorBoundary no interviene.</p>
<p>Conviene subrayar un dato importante: <strong>el comportamiento predeterminado de TanStack Query es «no lanzar el error».</strong> Tanto si queryFn lanza una excepción como si devuelve un rechazo, el error solo entra en el campo <code>error</code> y no interrumpe el flujo de renderizado de React. Por eso, sin una configuración adicional, ErrorBoundary nunca se activa.</p>
<p>Además, TanStack Query <strong>reintenta automáticamente los errores tres veces de forma predeterminada</strong>.</p>
<p>El <code>retryDelay</code> predeterminado utiliza una espera exponencial y aumenta hasta un máximo de 30 segundos. Esto significa que el usuario no ve el error inmediatamente después del primer fallo. Se reintenta tras intervalos de 1, 2 y 4 segundos y, si aun así falla, se rellena el campo <code>error</code>. (Si alguna vez durante el desarrollo se ha preguntado «¿por qué tarda tanto en aparecer el error?», casi con total seguridad esta es la causa.)</p>
<h3 id="conectar-errorboundary-mediante-throwonerror"><a class="anchor" href="#conectar-errorboundary-mediante-throwonerror">Conectar ErrorBoundary mediante throwOnError</a></h3>
<p>Entonces, ¿cómo se envían los errores de TanStack Query a ErrorBoundary? La respuesta es la opción <strong><code>throwOnError</code></strong>. (Hasta v4 se llamaba <code>useErrorBoundary</code>, pero en v5 pasó a llamarse <code>throwOnError</code>.)</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">data</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> } </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> useQuery</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  queryKey: [</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'todos'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">],</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  queryFn: fetchTodos,</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  throwOnError: </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">true</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">});</span></span></code></pre></figure>
<p>Cuando esta opción está activada, TanStack Query <strong>vuelve a lanzar el error en el siguiente ciclo de renderizado</strong>. Así, ese lanzamiento se convierte en un error de la fase de renderizado y ErrorBoundary por fin puede capturarlo.</p>
<p><code>throwOnError</code> también acepta una función. De este modo se puede decidir qué errores se envían a ErrorBoundary y cuáles gestiona directamente el componente.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">useQuery</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  queryKey: [</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">'todos'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">],</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  queryFn: fetchTodos,</span></span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">  // 5xx 서버 에러만 ErrorBoundary로 보낸다</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  throwOnError</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: (</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">error</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> error.response?.status </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">>=</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 500</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">});</span></span></code></pre></figure>
<p>Este patrón resulta práctico porque, normalmente, lo natural es mostrar en el lugar correspondiente los <strong>errores del cliente como los 4xx, por ejemplo un fallo de validación o falta de permisos</strong>, mientras que para <strong>errores del servidor como los 5xx</strong> conviene cubrir toda la página y mostrar un mensaje como «Vuelva a intentarlo dentro de unos instantes».</p>
<h3 id="usesuspensequery"><a class="anchor" href="#usesuspensequery">useSuspenseQuery</a></h3>
<p>Si se utiliza <code>useSuspenseQuery</code>, no hace falta preocuparse por <code>throwOnError</code>. En modo Suspense, el comportamiento predeterminado es <strong>lanzar siempre los errores</strong>.</p>
<p>Es decir, utilizar <code>useSuspenseQuery</code> implica que <strong>Suspense gestiona la carga y ErrorBoundary gestiona los errores</strong>. Ya no hacen falta condicionales como <code>if (isError)</code> o <code>if (isLoading)</code> dentro del componente; en su lugar, hay que envolverlo externamente con ambos límites.</p>
<h2 id="queryerrorresetboundary"><a class="anchor" href="#queryerrorresetboundary">QueryErrorResetBoundary</a></h2>
<p>Llegados a este punto surge otra pregunta: ¿qué ocurre cuando el usuario pulsa el botón «Reintentar» de la interfaz alternativa?</p>
<p>Como vimos antes, <code>resetErrorBoundary</code> solo reinicia el estado <code>hasError</code> de ErrorBoundary. Sin embargo, en la caché de TanStack Query sigue existiendo <strong>una consulta bloqueada en estado de error</strong>. Cuando los elementos secundarios vuelven a renderizarse, TanStack Query consulta la caché, determina que la consulta ya tiene un error y vuelve a lanzar inmediatamente el mismo error. (Es un bucle infinito infernal.)</p>
<p>Para resolver este problema, TanStack Query ofrece el hook <strong><code>useQueryErrorResetBoundary</code></strong> y el componente <strong><code>QueryErrorResetBoundary</code></strong>. Sus nombres son largos, pero su función es sencilla: emitir la orden <strong>«restablece el estado de error de las consultas de este ámbito»</strong>.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { useQueryErrorResetBoundary } </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">from</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> '@tanstack/react-query'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { ErrorBoundary } </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">from</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> 'react-error-boundary'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">function</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> App</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">() {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">reset</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> } </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> useQueryErrorResetBoundary</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">();</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">ErrorBoundary</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">      onReset</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{reset}</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">      fallbackRender</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{({ </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">resetErrorBoundary</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> }) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">        &#x3C;</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">div</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">          &#x3C;</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">p</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">>에러가 발생했습니다.&#x3C;/</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">p</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">          &#x3C;</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">button</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> onClick</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{resetErrorBoundary}>다시 시도&#x3C;/</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">button</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">        &#x3C;/</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">div</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">      )}</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    ></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">      &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Page</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> /></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    &#x3C;/</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">ErrorBoundary</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  );</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>Veamos en orden cronológico lo que ocurre aquí.</p>
<ol>
<li>El usuario pulsa el botón «Reintentar» → se llama a <code>resetErrorBoundary()</code></li>
<li>ErrorBoundary ejecuta la función <code>onReset</code> → se llama a <code>reset()</code> (se reinicia el estado de error de TanStack Query)</li>
<li>ErrorBoundary reinicia su propio estado y vuelve a renderizar los elementos secundarios</li>
<li>Se ejecuta el <code>useQuery</code> de los elementos secundarios → como el estado de error ha desaparecido, vuelve a intentar la obtención de datos</li>
</ol>
<p>La clave está en conectar <code>onReset</code> con <code>reset</code>. Gracias a esa línea, ErrorBoundary y TanStack Query sincronizan sus estados.</p>
<h3 id="uso-como-componente"><a class="anchor" href="#uso-como-componente">Uso como componente</a></h3>
<p>También se puede conseguir lo mismo con un componente en lugar de usar la función anterior. Basta con elegir una de las dos alternativas.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { QueryErrorResetBoundary } </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">from</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> '@tanstack/react-query'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { ErrorBoundary } </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">from</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> 'react-error-boundary'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">function</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> App</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">() {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">QueryErrorResetBoundary</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">      {({ </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">reset</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> }) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">        &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">ErrorBoundary</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">          onReset</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{reset}</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">          fallbackRender</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{({ </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">error</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">resetErrorBoundary</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> }) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">            &#x3C;</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">div</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> role</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"alert"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">              &#x3C;</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">p</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">>에러가 발생했습니다: {error.message}&#x3C;/</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">p</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">              &#x3C;</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">button</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> onClick</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{resetErrorBoundary}>다시 시도&#x3C;/</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">button</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">            &#x3C;/</span><span style="--shiki-dark:#85E89D;--shiki-light:#22863A">div</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">          )}</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">        ></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">          &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Page</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> /></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">        &#x3C;/</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">ErrorBoundary</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">      )}</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    &#x3C;/</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">QueryErrorResetBoundary</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  );</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>La principal diferencia con la versión basada en un hook es que pasa la función <code>reset</code> a sus elementos secundarios mediante el patrón de <strong>propiedad de renderizado</strong>. <code>QueryErrorResetBoundary</code> recibe una función como elemento secundario, le pasa <code>{ reset }</code> como argumento y renderiza su valor de retorno. Por eso puede conectarse inmediatamente mediante <code>onReset={reset}</code>.</p>
<p>Si no existe un <code>QueryErrorResetBoundary</code> cercano, la versión basada en un hook <strong>restablece los errores de la caché global</strong>. La versión basada en un componente limita el alcance del restablecimiento a su propio árbol de descendientes. Si se quiere controlar el ámbito de forma más precisa, la versión de componente es más segura.</p>
<p>Conviene aclarar un punto: <strong>el restablecimiento no borra la caché.</strong> No elimina todos los datos, sino que libera el estado de las consultas marcadas con error. Si se quieren invalidar realmente los datos, hay que llamar por separado a <code>queryClient.invalidateQueries()</code>.</p>
<h2 id="errores-de-mutaciones"><a class="anchor" href="#errores-de-mutaciones">Errores de mutaciones</a></h2>
<p>Hasta ahora, casi todos los patrones se han explicado desde la perspectiva de <code>useQuery</code>. Sin embargo, <strong>el caso de <code>useMutation</code> es algo diferente.</strong></p>
<p>La mayor diferencia es que una mutación suele comenzar a raíz de una <strong>acción explícita del usuario, como un clic o un envío</strong>. Por eso, resulta natural gestionar el error cerca de esa acción. En vez de cubrir toda la página con una interfaz alternativa, es preferible mostrar en una notificación o junto al formulario un texto como «Pago fallido: compruebe de nuevo los datos de su tarjeta».</p>
<p>En <a href="https://tkdodo.eu/blog/mastering-mutations-in-react-query" target="_blank" rel="noopener noreferrer">Dominar las mutaciones en React Query</a>, TkDodo resume la esencia de esta diferencia en una frase: <strong>las consultas son declarativas y las mutaciones son imperativas.</strong> Una consulta se ejecuta automáticamente al montar el componente, puede ser observada por otros componentes con la misma clave y queda almacenada en caché para reutilizarse. En cambio, una mutación no se ejecuta hasta que el usuario pulsa un botón, no se almacena en caché y queda vinculada de forma individual a la instancia del componente que la invocó. Esta diferencia esencial separa también sus formas de gestionar errores.</p>
<p>En <code>useQuery</code>, el valor predeterminado de <code>retry</code> es <code>3</code>, pero <strong>en <code>useMutation</code>, el valor predeterminado de <code>retry</code> es <code>0</code>.</strong> La razón es sencilla: una mutación produce <strong>efectos secundarios</strong>. Si una solicitud de pago falla por agotarse el tiempo de espera de la red y la biblioteca la repite automáticamente dos veces más, la tarjeta del usuario podría recibir tres cargos.</p>
<p>Por eso, la regla es activar explícitamente los reintentos de una mutación <strong>solo cuando el desarrollador pueda garantizar que la operación es idempotente</strong>. Esto se limita a consultas seguras del tipo GET cuyo resultado esté garantizado aunque se envíe dos veces la misma solicitud, o a casos en los que el servidor impida duplicados mediante una clave de idempotencia.</p>
<p>Los errores de <code>useQuery</code> <strong>quedan fijados en la caché</strong>. Por eso se propagan inmediatamente a otros componentes que observan la misma <code>queryKey</code> y hay que restablecerlos en conjunto mediante mecanismos como <code>QueryErrorResetBoundary</code>.</p>
<p>Las mutaciones son diferentes. El error de una mutación que falla en un componente <strong>solo permanece en el estado de esa instancia.</strong> No afecta a las mutaciones de otros componentes que utilicen la misma <code>mutationFn</code>. Por eso TanStack Query no tiene nada parecido a <code>MutationErrorResetBoundary</code>: <strong>no es necesario</strong>.</p>
<p>Esta diferencia tiene una consecuencia práctica. Si dos componentes invocan <code>useMutation</code> por separado, el error producido en uno no es visible en el otro. Si se necesita conocer «el error de esta mutación en toda la aplicación», su <code>onError</code> a nivel de componente no basta; hay que elevarlo a <code>MutationCache.onError</code>.</p>
<h3 id="mutate-vs-mutateasync"><a class="anchor" href="#mutate-vs-mutateasync">mutate vs mutateAsync</a></h3>
<p><code>useMutation</code> devuelve dos funciones de ejecución. La diferencia entre ellas determina cómo se gestionan los errores.</p>
<p>El tipo de retorno de mutate es <code>void</code>. No devuelve una Promise. Por tanto, no es posible esperar el resultado con await y este solo puede recibirse mediante manejadores como <code>onSuccess/onError</code>.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> mutation</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> useMutation</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  mutationFn: createPost,</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  onError</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: (</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">error</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    toast.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">error</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">`등록 실패: ${</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">error</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">.</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">message</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">}`</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  },</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">});</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">mutation.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">mutate</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(newPost);</span></span></code></pre></figure>
<p>En cambio, <code>mutateAsync</code> devuelve una Promise. El error se puede gestionar con <code>try/catch</code>.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> mutation</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> useMutation</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({ mutationFn: createPost });</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> handleSubmit</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> async</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> () </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  try</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">    const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> result</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> await</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> mutation.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">mutateAsync</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(newPost);</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    router.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">push</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">`/posts/${</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">result</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">.</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">id</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">}`</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  } </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">catch</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (error) {</span></span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">    // 여기서 처리</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  }</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">};</span></span></code></pre></figure>
<p>¿Cuándo conviene usar cada una? Yo aplico los siguientes criterios.</p>
<ul>
<li><strong>Se necesita una acción posterior al terminar la mutación</strong>, por ejemplo navegar al finalizar o utilizar el resultado → <code>mutateAsync</code></li>
<li><strong>Solo hay que ejecutarla y delegar los efectos secundarios en los manejadores configurados</strong>, por ejemplo alternar un «Me gusta» o mostrar únicamente una notificación → <code>mutate</code> + <code>onError</code></li>
</ul>
<p>Hay un error frecuente que conviene señalar: <strong>usar <code>mutateAsync</code> sin <code>try/catch</code> provoca el rechazo no gestionado de una promesa.</strong> <code>mutate</code>, que gestiona el error mediante los manejadores configurados, lo absorbe, mientras que el comportamiento predeterminado de <code>mutateAsync</code> consiste en lanzarlo al código que la invoca. Si se mezclan sin conocer esta diferencia, la consola se llena de advertencias rojas.</p>
<h3 id="onerror"><a class="anchor" href="#onerror">onError</a></h3>
<p>Hay otro detalle que suele pasarse por alto. En <code>useMutation</code>, <code>onError</code> puede definirse <strong>en dos lugares</strong>, en el hook y en mutate.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> mutation</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> useMutation</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  mutationFn: createPost,</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  onError</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: (</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">error</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    Sentry.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">captureException</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(error);</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  },</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">});</span></span></code></pre></figure>
<p>En el nivel del hook se ejecuta siempre, mientras que en el nivel de mutate solo se ejecuta al invocarla.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">mutation.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">mutate</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(newPost, {</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  onError</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: (</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">error</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">    setFormError</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(error.message);</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  },</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">});</span></span></code></pre></figure>
<p>La documentación oficial especifica este orden de ejecución: <strong>nivel del hook → nivel de mutate.</strong> Si ambos manejadores están definidos, se ejecuta primero el del hook y después el de mutate.</p>
<h2 id="gestión-global-de-errores"><a class="anchor" href="#gestión-global-de-errores">Gestión global de errores</a></h2>
<p>Todos los patrones vistos hasta ahora operan a nivel de componente. Sin embargo, puede haber requisitos como «registrar todos los errores de las consultas en un solo lugar» o «cerrar siempre la sesión ante un error 401». Para estas preocupaciones transversales se pueden añadir manejadores a <code>QueryCache</code>/<code>MutationCache</code> al crear el <strong>QueryClient</strong>.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { QueryClient, QueryCache, MutationCache } </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">from</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> '@tanstack/react-query'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> queryClient</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> new</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> QueryClient</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  queryCache: </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">new</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> QueryCache</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">    onError</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: (</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">error</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">query</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">      if</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (query.state.data </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">!==</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> undefined</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) {</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">        toast.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">error</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">`데이터 갱신 실패: ${</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">error</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">.</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">message</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">}`</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">      }</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    },</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  }),</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  mutationCache: </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">new</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> MutationCache</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">    onError</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: (</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">error</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">      if</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (error.status </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">===</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 401</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) {</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">        redirectToLogin</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">      }</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    },</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  }),</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">});</span></span></code></pre></figure>
<p>La clave es que <code>QueryCache.onError</code> se invoca <strong>una sola vez por cada consulta</strong>. Aunque varios componentes observen la misma consulta, la función solo se ejecuta una vez, por lo que no se producen problemas como notificaciones duplicadas.</p>
<p>También se puede comprobar <code>query.state.data !== undefined</code>, como en el ejemplo anterior. Si <strong>falla una actualización cuando ya hay datos en caché</strong>, el usuario sigue viendo información en pantalla. Cubrir la página con un ErrorBoundary sería excesivo; basta con informarle del fallo. En cambio, si falla la carga inicial y no hay datos almacenados, lo apropiado es que ErrorBoundary capture el error y muestre la interfaz alternativa.</p>
<p>Al combinar ambos flujos se puede diseñar una política clara: «ErrorBoundary para los fallos de carga inicial y una notificación para los fallos de actualización en segundo plano».</p>
<h2 id="componente-común"><a class="anchor" href="#componente-común">Componente común</a></h2>
<p>Llegados aquí, es natural pensar: si resulta molesto envolver todo cada vez con tres capas de <code>QueryErrorResetBoundary</code>, <code>ErrorBoundary</code> y <code>Suspense</code>, ¿por qué no <strong>agruparlas en un componente reutilizable</strong>?</p>
<p>Es una idea razonable. De hecho, hace tiempo también creé y utilicé un componente <code>AsyncBoundary</code> como el siguiente.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { QueryErrorResetBoundary } </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">from</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> '@tanstack/react-query'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { Suspense, </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">type</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> ComponentType, </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">type</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> ReactNode } </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">from</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> 'react'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { ErrorBoundary, </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">type</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> FallbackProps } </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">from</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> 'react-error-boundary'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { ErrorFallback } </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">from</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> './ErrorFallback'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">import</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> { Spinner } </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">from</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> './Spinner'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">interface</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> Props</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  children</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> ReactNode</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  pendingFallback</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">?:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> ReactNode</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  rejectedFallback</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">?:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> ComponentType</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">FallbackProps</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">>;</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">export</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> function</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> AsyncBoundary</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  children</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  pendingFallback</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Spinner</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> />,</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  rejectedFallback</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> ErrorFallback,</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> Props</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">QueryErrorResetBoundary</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">      {({ </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">reset</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> }) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">        &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">ErrorBoundary</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> onReset</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{reset} </span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">FallbackComponent</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{rejectedFallback}></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">          &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Suspense</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> fallback</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{pendingFallback}>{children}&#x3C;/</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Suspense</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">        &#x3C;/</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">ErrorBoundary</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">      )}</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    &#x3C;/</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">QueryErrorResetBoundary</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  );</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>En una página, todo queda reducido a una línea.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">AsyncBoundary</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Content</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> /></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">AsyncBoundary</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span></code></pre></figure>
<p>Parece limpio. Sin embargo, un compañero me hizo los siguientes comentarios.</p>
<blockquote>
<p>El nombre AsyncBoundary no se utiliza como un término tan establecido, por lo que probablemente no resulte demasiado extraño sea cual sea su contenido, pero <strong>sí puede ser difícil prever que también incluye ResetBoundary de React Query.</strong></p>
</blockquote>
<blockquote>
<p>También me preocupa que <code>pendingFallback</code> y <code>rejectedFallback</code> tengan valores predeterminados. Al ver solo una línea con <code>&#x3C;AsyncBoundary></code>, no se puede saber qué interfaz alternativa se aplica internamente, así que <strong>es posible que ni siquiera se perciba que existen esos valores predeterminados en las propiedades.</strong></p>
</blockquote>
<h3 id="el-nombre-oculta-la-dependencia"><a class="anchor" href="#el-nombre-oculta-la-dependencia">El nombre oculta la dependencia</a></h3>
<p>El componente se llama <code>AsyncBoundary</code>. Su nombre solo sugiere un límite asíncrono. Sin embargo, la implementación interna está <strong>fuertemente acoplada a TanStack Query</strong>. Incluye <code>QueryErrorResetBoundary</code> y conecta <code>onReset</code> con <code>reset</code>. Es decir, en realidad es <strong>«un límite para ámbitos asíncronos que utilizan React Query»</strong>, pero su nombre no lo revela en absoluto.</p>
<p>¿Por qué supone esto un problema? Porque <strong>rompe las expectativas de quien lee el código</strong>. No leemos el código interpretando cada línea de forma aislada, sino <strong>anticipando</strong> patrones aprendidos con la experiencia. Cuando esa expectativa falla, la carga cognitiva aumenta bruscamente.</p>
<p>Al ver por primera vez el nombre <code>AsyncBoundary</code>, un compañero imaginará «un límite genérico para gestionar operaciones asíncronas». Parecería reutilizable tanto con SWR como con una solicitud directa. Sin embargo, contiene un <code>QueryErrorResetBoundary</code>, por lo que <strong>arrastra un acoplamiento sin sentido en contextos que no utilizan TanStack Query</strong>. Hay una grieta entre el nombre y la implementación.</p>
<p>Podría verse como una abstracción con fugas en sentido inverso. Normalmente, una fuga ocurre cuando «se escapa un detalle que debería quedar oculto tras la abstracción»; aquí, en cambio, <strong>una dependencia que debería estar visible ha quedado demasiado bien escondida tras el nombre.</strong> Quizá sea incluso peor. (Porque se reutiliza sin saberlo.)</p>
<h3 id="mostrar-la-dependencia-en-el-nombre"><a class="anchor" href="#mostrar-la-dependencia-en-el-nombre">Mostrar la dependencia en el nombre</a></h3>
<p>La solución más sencilla es cambiar el nombre. En vez de <code>AsyncBoundary</code>, puede usarse <strong><code>QueryAsyncBoundary</code></strong> para hacer explícita la dependencia. Al revisar la biblioteca <a href="https://suspensive.org/" target="_blank" rel="noopener noreferrer">Suspensive</a> de Toss, se observa que también explicita sus dependencias. <code>@suspensive/react</code> solo incluye los componentes genéricos <code>ErrorBoundary</code> y <code>Suspense</code>, mientras que el componente integrado con TanStack Query se separa en el paquete <code>@suspensive/react-query</code> bajo el nombre <code>QueryAsyncBoundary</code>.</p>
<p>La información que aporta esta única palabra es considerable. En cuanto aparece el prefijo <code>Query</code>, queda claro de inmediato que <strong>«esto es exclusivo de un entorno TanStack Query»</strong>. Así se evitan de antemano usos en contextos equivocados.</p>
<h3 id="dividir-en-unidades-componibles"><a class="anchor" href="#dividir-en-unidades-componibles">Dividir en unidades componibles</a></h3>
<p>Un enfoque más fundamental consiste en <strong>no agruparlas</strong>.</p>
<p>ErrorBoundary y Suspense son, en esencia, <strong>preocupaciones diferentes</strong>. Si se agrupan en un solo componente, puede perderse flexibilidad de composición. Algunas páginas solo necesitarán ErrorBoundary; otras, únicamente Suspense; y en otras quizá se quiera colocar dos Suspense dentro de un ErrorBoundary. Agruparlos en <code>AsyncBoundary</code> vuelve incómodas estas variantes. Si se mantienen separados, pueden componerse libremente.</p>
<p>Este patrón añade una línea de código, pero ofrece la ventaja de que <strong>la responsabilidad de cada límite se lee directamente en el código</strong>. Además, al utilizar <code>useSuspenseQuery</code>, la unidad que se quiere resolver de una vez suele ser distinta de la unidad cuyos errores se quieren capturar, por lo que mantenerlas separadas resulta más natural.</p>
<p>Mi conclusión es la siguiente: <strong>si el patrón de composición repetido es realmente idéntico, agrúpelo; si necesita variaciones, manténgalo separado.</strong> E incluso si se agrupa, hay que hacer visible la dependencia en el nombre. Con solo respetar estos dos principios, será menos probable recibir comentarios de revisión como «no sé qué contiene AsyncBoundary».</p>
<h3 id="propiedades-predeterminadas"><a class="anchor" href="#propiedades-predeterminadas">Propiedades predeterminadas</a></h3>
<p>Corregir únicamente el nombre no basta. Volvamos al código anterior.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">pendingFallback </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Spinner</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> />,</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">rejectedFallback </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> ErrorFallback,</span></span></code></pre></figure>
<p><code>&#x3C;QueryAsyncBoundary>...&#x3C;/QueryAsyncBoundary></code> funciona en una sola línea porque internamente se aplican de forma automática <code>Spinner</code> y <code>ErrorFallback</code>. <strong>No es información que pueda deducirse del nombre.</strong></p>
<p>Es otra versión del problema anterior, «el nombre oculta la dependencia». El prefijo <code>Query</code> hace visible la dependencia, pero las dependencias de interfaz <code>Spinner</code> y <code>ErrorFallback</code> siguen ocultas tras las propiedades predeterminadas. <strong>El ocultamiento solo se ha desplazado un nivel hacia dentro.</strong></p>
<p>La solución es sencilla: <strong>hacer obligatorias ambas propiedades de la interfaz alternativa e inyectarlas siempre en el punto de uso.</strong></p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">interface</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> Props</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  children</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> ReactNode</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  pendingFallback</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> ReactNode</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;                    </span></span>
<span data-line=""><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">  rejectedFallback</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> ComponentType</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">FallbackProps</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">>;</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span></code></pre></figure>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">QueryAsyncBoundary</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  pendingFallback</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{&#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Spinner</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> />}</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  rejectedFallback</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{ErrorFallback}</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Content</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> /></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">QueryAsyncBoundary</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span></code></pre></figure>
<p>El código crece dos líneas. La razón para aceptar ese coste es clara: <strong>aumenta el esfuerzo de quien escribe para reducir el coste de rastreo de todas las personas que leen.</strong> En el propio punto de uso se ve qué interfaz alternativa aparecerá. No hace falta abrir otro archivo para comprobar «¿cuál era el valor predeterminado de este componente?». La conocida idea de que el código se lee muchas más veces de las que se escribe también se aplica aquí.</p>
<h2 id="errorfallback"><a class="anchor" href="#errorfallback">ErrorFallback</a></h2>
<p>Hay otro aspecto que merece atención. Normalmente, <code>ErrorFallback</code> se define como un único componente de la siguiente forma.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> DEFAULT_ERROR_MESSAGE</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> '문제가 발생했어요. 잠시 후 다시 시도해주세요'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">export</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> function</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> ErrorFallback</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">({ </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">error</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">, </span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">resetErrorBoundary</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> }</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">:</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> FallbackProps</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> message</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> getErrorMessage</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(error, </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">DEFAULT_ERROR_MESSAGE</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  return</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Flex</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> direction</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"column"</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> alignItems</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"center"</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> role</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"alert"</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> aria-live</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"assertive"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">      &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Text</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">>{message}&#x3C;/</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Text</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">      &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Spacing</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> size</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">16</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">} /></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">      &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Button</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> onClick</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{resetErrorBoundary}>다시 시도&#x3C;/</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Button</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    &#x3C;/</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Flex</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  );</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>Es una implementación cuidada que incluye incluso <code>role="alert"</code> y <code>aria-live="assertive"</code>. Pero planteemos una pregunta: <strong>«¿está bien mostrar la misma pantalla para un 401, un 404, un 500 o una desconexión de red?»</strong></p>
<p>En la mayoría de los casos, la respuesta es <strong>no</strong>, porque la acción que debe realizar el usuario cambia según el tipo de error.</p>
<table>
<thead>
<tr>
<th>Tipo de error</th>
<th>Acción del usuario</th>
<th>¿Tiene sentido «Reintentar»?</th>
</tr>
</thead>
<tbody>
<tr>
<td>Desconexión de red</td>
<td>Comprobar la conexión y reintentar</td>
<td>O</td>
</tr>
<tr>
<td>Error 5xx del servidor</td>
<td>Esperar y reintentar</td>
<td>O</td>
</tr>
<tr>
<td>Fallo de autenticación 401</td>
<td>Ir a la pantalla de inicio de sesión</td>
<td>X</td>
</tr>
<tr>
<td>Falta de permisos 403</td>
<td>Ir a otra pantalla</td>
<td>X</td>
</tr>
<tr>
<td>Recurso no encontrado 404</td>
<td>Volver al listado</td>
<td>△</td>
</tr>
<tr>
<td>Fallo de validación 422</td>
<td>Corregir los datos introducidos</td>
<td>X</td>
</tr>
</tbody>
</table>
<p>Mostrar el botón «Reintentar» en todos los casos equivale a <strong>indicar al usuario una acción equivocada para resolver el error</strong>. Pulsarlo ante un 401 solo produce otro 401. La acción que realmente debe realizar el usuario es iniciar sesión.</p>
<p>Por eso, la interfaz alternativa de error <strong>debe renderizarse de forma distinta según el tipo de error</strong>. No hace falta empezar con un enorme <code>if/else</code>; se pueden crear pequeños componentes y seleccionar el adecuado.</p>
<p>Cada componente alternativo expone únicamente el mensaje y la acción apropiados para ese error. En la pantalla solo quedan acciones que el usuario puede realizar de verdad.</p>
<h3 id="shouldcatch"><a class="anchor" href="#shouldcatch">shouldCatch</a></h3>
<p>Si damos un paso más, también existe el patrón de <strong>distinguir en el nivel del componente entre los errores que se capturan y los que se dejan pasar</strong>. El <code>ErrorBoundary</code> de Suspensive ofrece la propiedad <code>shouldCatch</code>.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="tsx" data-theme="github-dark github-light"><code data-language="tsx" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">ErrorBoundary</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  shouldCatch</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{(</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">error</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> isHttpError</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(error) </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">&#x26;&#x26;</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> error.status </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">>=</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 500</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  fallback</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{ServerErrorFallback}</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">ErrorBoundary</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> shouldCatch</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{NetworkError} </span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">fallback</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{NetworkErrorFallback}></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    &#x3C;</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">Page</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> /></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  &#x3C;/</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">ErrorBoundary</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">&#x3C;/</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">ErrorBoundary</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">></span></span></code></pre></figure>
<p>El ErrorBoundary interior solo captura errores de red y deja pasar los 5xx. Los errores no capturados <strong>ascienden al ErrorBoundary superior</strong> según el comportamiento predeterminado de React. Así, el ErrorBoundary exterior termina capturando los 5xx. Frente a implementar la misma gestión con una estructura condicional, resulta atractivo poder <strong>dar significado a los propios límites</strong>.</p>
<p><code>react-error-boundary</code> no incluye esta propiedad, pero se puede conseguir el mismo efecto bifurcando la lógica dentro de la interfaz alternativa. Lo importante es el patrón, no la biblioteca.</p>
<h2 id="conclusión"><a class="anchor" href="#conclusión">Conclusión</a></h2>
<p>En resumen, la gestión de errores del frontend <strong>no se resuelve con una sola herramienta</strong>. Error Boundary se ocupa de los errores de renderizado; <code>try/catch</code> o <code>showBoundary</code>, de los errores de manejadores de eventos; <code>throwOnError</code> y <code>useQueryErrorResetBoundary</code> de TanStack Query, de los errores al obtener datos de forma asíncrona; <code>mutateAsync</code> u <code>onError</code>, de los errores de las mutaciones; y <code>QueryCache</code>/<code>MutationCache</code>, de las preocupaciones transversales. Además, hay que diseñar <strong>el nombre y la unidad de composición de los componentes comunes</strong> y <strong>el modelado de dominio de los propios tipos de error</strong> para conseguir una política de errores coherente.</p>
<p>Cuando se entiende la responsabilidad de cada herramienta, se pueden tomar decisiones claras como <strong>«este error se captura aquí y aquel se deja pasar hasta allí»</strong>. La suma de esas decisiones acaba creando una experiencia de usuario estable: evitar una pantalla en blanco, impedir que aparezca cinco veces la misma notificación, evitar que un fallo temporal de red inutilice toda la página o mostrar la pantalla de inicio de sesión ante un 401 en vez de «Reintentar». Estos detalles, en conjunto, transmiten la impresión de un servicio bien construido.</p>
<p>Por supuesto, no todos los proyectos necesitan todos los patrones. Para una herramienta interna sencilla puede bastar un ErrorBoundary y alguna notificación; en un dominio como los pagos, donde un solo error tiene consecuencias económicas, habrá que aplicar una gestión minuciosa a cada mutación. El dominio determina la respuesta correcta.</p>
<p>Invito a quienes lean este artículo a revisar alguna vez en sus proyectos «¿qué errores captura ahora nuestro servicio, dónde los captura y qué nombre tiene el componente que lo hace?». Puede haber más errores de los esperados que, aunque parecían bien capturados, en realidad se escapan o llegan a la interfaz alternativa equivocada. (A mí también me ocurre cada vez.)</p>
<h2 id="referencias"><a class="anchor" href="#referencias">Referencias</a></h2>
<details class="ref-block"><summary class="ref-summary">참고 자료</summary><div class="ref-list">
<div class="ref-item">
<div class="ref-item-inner">[documentación] <a href="https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary" target="_blank" rel="noopener noreferrer">React, Límites de errores</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner">[documentación] <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">[documentación] <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">[documentación] <a href="https://tanstack.com/query/v5/docs/framework/react/guides/important-defaults" target="_blank" rel="noopener noreferrer">TanStack Query, Valores predeterminados importantes</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner">[artículo] <a href="https://tkdodo.eu/blog/react-query-error-handling" target="_blank" rel="noopener noreferrer">TkDodo, Gestión de errores en React Query</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner">[artículo] <a href="https://tkdodo.eu/blog/breaking-react-querys-api-on-purpose" target="_blank" rel="noopener noreferrer">TkDodo, Romper a propósito la API de React Query</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#9ca3af"></span><a href="https://github.com/toss/suspensive" target="_blank" rel="noopener noreferrer">toss/suspensive, @suspensive/react-query</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner">[documentación] <a href="https://reactrouter.com/how-to/error-boundary" target="_blank" rel="noopener noreferrer">React Router, Límites de errores</a></div>
</div>
</div></details>]]></content:encoded>
            <author>jihoon7705@gmail.com (이지훈)</author>
            <category>프론트엔드</category>
            <category>React</category>
            <category>TanStack-Query</category>
            <category>에러핸들링</category>
        </item>
        <item>
            <title><![CDATA[React Fiber al completo]]></title>
            <link>https://hooninedev.com/es/250520</link>
            <guid isPermaLink="false">https://hooninedev.com/es/250520</guid>
            <pubDate>Tue, 20 May 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[En este artículo quiero hablar de la arquitectura Fiber, que podría considerarse el corazón de React. Cuando conocí React, la palabra "Fiber" me sonaba poco más que a una pregunta habitual de entrevis...]]></description>
            <content:encoded><![CDATA[<p>En este artículo quiero hablar de la <strong>arquitectura Fiber</strong>, que podría considerarse el corazón de React.</p>
<p>Cuando conocí React, la palabra <strong>"Fiber"</strong> me sonaba poco más que a una pregunta habitual de entrevista. Memorizar una definición de una línea —«dividir el renderizado en unidades de trabajo y procesarlas por separado»— me parecía suficiente. Sin embargo, al empezar a examinar el código fuente de React, comprendí que Fiber no era un simple concepto, sino una arquitectura de runtime que gobierna <strong>todo</strong> el renderizado de React.</p>
<blockquote>
<p>Todavía recuerdo el impacto de abrir por primera vez el código fuente de React. Pensé: «Pero... ¿qué es todo esto?».</p>
</blockquote>
<p>En este artículo iré más allá de responder «divide el trabajo en unidades y las procesa» a la pregunta «¿qué es Fiber?». Analizaré en profundidad <strong>por qué</strong> nació, <strong>cómo</strong> está diseñado y <strong>cómo</strong> esa estructura hace posibles las Concurrent Features de React.</p>
<h2 id="por-qué-apareció-fiber"><a class="anchor" href="#por-qué-apareció-fiber">¿Por qué apareció Fiber?</a></h2>
<p>Para responder a esta pregunta, primero hay que entender qué problemas tenía el mundo anterior a Fiber: el <strong>Stack Reconciler</strong> utilizado hasta React 15.</p>
<p>Como indica su nombre, Stack Reconciler era un motor de reconciliación basado en llamadas <strong>recursivas (recursive)</strong>. Recorría el árbol de componentes de arriba abajo de forma recursiva y, una vez iniciado el renderizado, no podía detenerse hasta procesar el árbol completo. Era como estar en una llamada telefónica que no puedes cortar hasta que la otra persona termine de hablar. (Imagina que empieza a contarte sus problemas durante tres horas y no puedes interrumpirla. Terrible).</p>
<p>En concreto, Stack Reconciler presentaba las siguientes limitaciones.</p>
<ul>
<li><strong>Imposibilidad de interrumpir el renderizado</strong>: al tener que procesar todo el árbol de una vez, en interfaces complejas el hilo principal quedaba ocupado durante decenas o cientos de milisegundos</li>
<li><strong>Ausencia de prioridades</strong>: tanto si el usuario hacía clic en un botón como si se actualizaban datos en segundo plano, todas las actualizaciones se procesaban de la misma manera</li>
<li><strong>Dificultad para responder a animaciones y gestos</strong>: mantener 60 fps exige completar todo el trabajo de cada frame en unos 16 ms, algo que el renderizado recursivo no podía garantizar</li>
<li><strong>Detención de toda la aplicación ante un error</strong>: si se producía un error en algún punto del árbol de componentes, toda la aplicación se detenía</li>
</ul>
<p>Para superar estas limitaciones, el equipo de React ideó un nuevo modelo de ejecución capaz de <strong>dividir</strong> el trabajo, <strong>asignarle prioridades</strong> y, cuando fuera necesario, <strong>interrumpirlo y reanudarlo</strong>. El resultado fue <strong>React Fiber</strong>.</p>
<p>El documento <a href="https://github.com/acdlite/react-fiber-architecture" target="_blank" rel="noopener noreferrer">react-fiber-architecture</a>, escrito por Andrew Clark, recoge las ideas centrales de este diseño y es la referencia más importante para entender Fiber. (Parece que se incorporó al equipo de React poco después de escribirlo).</p>
<h2 id="stack-vs-fiber"><a class="anchor" href="#stack-vs-fiber">Stack vs Fiber</a></h2>
<p>Entonces, ¿en qué se diferencian Stack Reconciler y Fiber Reconciler a nivel de código?</p>
<h3 id="stack-reconciler-basado-en-recursión"><a class="anchor" href="#stack-reconciler-basado-en-recursión">Stack Reconciler basado en recursión</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>Con el enfoque Stack, al encontrar un componente hijo se entra <strong>inmediatamente en una llamada recursiva</strong>. El problema es que depende directamente del call stack de JavaScript. A medida que aumenta la profundidad de la recursión, se acumulan frames en el call stack y, hasta que todos se resuelven, el hilo principal del navegador no puede hacer ninguna otra cosa.</p>
<p>Dicho de forma sencilla, el navegador queda <strong>completamente inmovilizado</strong> hasta que se vacía el call stack.</p>
<video width="640" height="480" controls>
  <source src="/content/250520/stack.mov" type="video/mp4">
</video>
<p>En el vídeo se observa cómo el hilo principal queda totalmente bloqueado mientras Stack Reconciler renderiza.</p>
<h3 id="fiber-reconciler-basado-en-iteración"><a class="anchor" href="#fiber-reconciler-basado-en-iteración">Fiber Reconciler basado en iteración</a></h3>
<p>Fiber sustituyó la recursión por un <strong>bucle iterativo (iterative loop)</strong>. En lugar de usar el call stack, implementó su propia <strong>pila virtual</strong> en memoria. Cada nodo Fiber actúa como un «frame de la pila» y, dado que estos nodos existen como objetos de JavaScript en la memoria heap, el trabajo puede interrumpirse en cualquier momento y continuar más tarde.</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>Este código muestra el modelo conceptual inicial de Fiber. La clave consiste en procesar una sola unidad de trabajo (unit of work) cada vez dentro del bucle <code>while</code> y, si queda poco tiempo, salir del bucle para devolver el control al navegador.</p>
<p>(Al principio se planteó un enfoque basado en <code>requestIdleCallback</code>, pero React no lo utiliza en la práctica. Más adelante veremos el motivo en detalle).</p>
<video width="640" height="480" controls>
  <source src="/content/250520/fiber.mov" type="video/mp4">
</video>
<p>Con Fiber se puede responder de inmediato a eventos del usuario —clics, escritura, etc.— incluso durante el renderizado. Al ejecutar el trabajo en fragmentos pequeños, el navegador tiene margen para respirar.</p>
<p>Si quieres experimentar directamente la diferencia entre ambos enfoques, haz clic <strong><a href="https://animated-lollipop-2b6cbb.netlify.app/" target="_blank" rel="noopener noreferrer">aquí</a></strong>. Podrás observar con tus propios ojos cómo se comportan Stack Reconciler y Fiber Reconciler.</p>
<p>Estos son precisamente los objetivos fundamentales de Fiber que Andrew Clark destacó en su documento.</p>
<ul>
<li><strong>Poder pausar el trabajo y retomarlo más tarde</strong></li>
<li><strong>Asignar prioridades a distintos tipos de trabajo</strong></li>
<li><strong>Reutilizar trabajo completado anteriormente</strong></li>
<li><strong>Abortar trabajo que ya no sea necesario</strong></li>
</ul>
<h2 id="estructura-interna-de-un-nodo-fiber"><a class="anchor" href="#estructura-interna-de-un-nodo-fiber">Estructura interna de un nodo Fiber</a></h2>
<p>Al llegar hasta aquí surge una pregunta natural: «Entonces, ¿cómo es un nodo Fiber por dentro?».</p>
<p>El equipo de React no ofrece documentación oficial específica sobre la implementación interna de Fiber. Sin embargo, podemos comprender su estructura mediante el documento react-fiber-architecture de Andrew Clark y el código fuente real de React (<code>ReactFiber.js</code>).</p>
<p>Me gusta comparar un nodo Fiber con una <strong>orden de trabajo (Work Order)</strong>. Cuando se ensambla un producto en una fábrica, cada orden indica «qué tipo de pieza es», «qué materiales utiliza», «qué trabajo hay que realizar después» y «qué prioridad tiene». Un nodo Fiber funciona del mismo modo.</p>
<h3 id="reactelement-y-fibernode"><a class="anchor" href="#reactelement-y-fibernode">ReactElement y FiberNode</a></h3>
<p>Para entender Fiber, primero hay que distinguir entre <strong>ReactElement</strong> y <strong>FiberNode</strong>. Aunque suelen confundirse, son 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 no es más que el <strong>plano</strong> de la UI. Es una solicitud que dice «renderiza este componente con estas props»; no contiene la lógica real de renderizado ni el estado.</p>
<p>En cambio, <strong>FiberNode</strong> es la <strong>unidad de trabajo de runtime</strong> que React crea internamente a partir de ese plano. Aquí aparecen campos que ReactElement no tiene, como <code>tag</code>, <code>stateNode</code>, <code>child/sibling/return</code>, <code>memoizedState</code>, <code>updateQueue</code> y <code>lanes</code>.</p>
<p>Cuando React examina el <code>type</code> de un ReactElement para crear un FiberNode, determina el valor de <strong>tag</strong>.</p>
<ul>
<li>Si <code>type</code> es una función y tiene <code>prototype.isReactComponent</code> → <code>tag = ClassComponent(1)</code></li>
<li>Si <code>type</code> es una función → <code>tag = FunctionComponent(0)</code></li>
<li>Si <code>type</code> es una cadena (<code>"div"</code>, etc.) → <code>tag = HostComponent(5)</code></li>
</ul>
<p><strong>tag</strong> es una constante numérica que representa el tipo de FiberNode. Está definida en <code>ReactWorkTags.js</code> y existen más de 25 tags, entre ellos <code>FunctionComponent(0)</code>, <code>ClassComponent(1)</code>, <code>HostRoot(3)</code>, <code>HostComponent(5)</code> y <code>HostText(6)</code>. React utiliza este valor tag para decidir qué lógica ejecutar en <code>beginWork</code>.</p>
<p><strong>type</strong> desempeña un papel esencial durante la reconciliación (reconciliation). Al comparar el Fiber del renderizado anterior con el nuevo elemento, es <strong>lo primero que React comprueba</strong>. (Este valor se transfiere sin cambios del ReactElement al FiberNode).</p>
<ul>
<li>Si antes era un <code>div</code> y ahora también es un <code>div</code>, React <strong>reutiliza</strong> ese nodo Fiber y solo actualiza sus props</li>
<li>Si antes era un <code>div</code> y ahora ha cambiado a un <code>span</code>, React <strong>descarta</strong> el Fiber anterior y crea uno nuevo</li>
</ul>
<p><strong>key</strong> también se transfiere del ReactElement al FiberNode y se utiliza principalmente al renderizar listas (arrays). Sin key, cuando cambia el orden de los elementos de una lista, React no puede saber con precisión qué elemento se ha movido y adónde. Esto puede provocar operaciones innecesarias sobre el DOM o hacer que el estado interno de un componente se conserve o se pierda de forma involuntaria.</p>
<h3 id="child-sibling-return"><a class="anchor" href="#child-sibling-return">child, sibling, return</a></h3>
<p>Aquí reside el secreto que permite a React Fiber utilizar iteración en lugar de recursión.</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> apunta al <strong>primer</strong> elemento hijo devuelto por el render del componente. En el ejemplo anterior, corresponde a <code>&#x3C;자식1/></code>. <strong>sibling</strong> representa el <strong>siguiente hermano</strong> que comparte el mismo padre. El sibling de <code>&#x3C;자식1/></code> es <code>&#x3C;자식2/></code>. <strong>return</strong> apunta al Fiber padre <strong>al que hay que volver</strong> cuando termina el procesamiento del nodo Fiber actual. Tanto <code>&#x3C;자식1/></code> como <code>&#x3C;자식2/></code> tienen a <code>부모</code> como return.</p>
<p>La estructura creada por estos tres campos es un <strong>árbol en forma de lista simplemente enlazada (Singly Linked List)</strong>. En un árbol convencional resulta intuitivo almacenar un array de hijos (<code>children[]</code>), pero Fiber lo evita deliberadamente.</p>
<p>¿Por qué? Una estructura de hijos basada en arrays exige gestionar un índice durante el recorrido y llevar un seguimiento adicional de «hasta dónde se había procesado» al interrumpirlo y reanudarlo. En cambio, con una linked list basta recordar la referencia al nodo actual para continuar el recorrido en cualquier momento. Esta es la base estructural que permite a Fiber admitir de manera natural la <strong>interrupción y reanudación</strong>.</p>
<p>React recorre los nodos en orden de búsqueda en profundidad (DFS) sobre esta estructura. Desciende siguiendo <code>child</code> (beginWork); al alcanzar un nodo hoja comprueba <code>sibling</code>; y, si no hay hermanos, asciende siguiendo <code>return</code> (completeWork).</p>
<h3 id="pendingprops-y-memoizedprops"><a class="anchor" href="#pendingprops-y-memoizedprops">pendingProps y memoizedProps</a></h3>
<p><strong>pendingProps</strong> son las <strong>props nuevas</strong> recibidas cuando el Fiber comienza a procesarse, mientras que <strong>memoizedProps</strong> son las <strong>props anteriores</strong> cuyo procesamiento terminó en el renderizado previo.</p>
<p>Si ambos valores son iguales, React puede concluir que «este componente no ha cambiado» y reutilizar el resultado del renderizado anterior. Este es el mecanismo esencial de la <strong>optimización por bailout</strong>.</p>
<p>De forma similar, <strong>memoizedState</strong> almacena el estado de los hooks de ese Fiber, y <strong>updateQueue</strong> gestiona como una linked list las actualizaciones de estado todavía pendientes —las llamadas a setState—.</p>
<h3 id="statenode"><a class="anchor" href="#statenode">stateNode</a></h3>
<p><strong>stateNode</strong> referencia la <strong>instancia real</strong> a la que apunta el nodo Fiber.</p>
<ul>
<li>Para un <strong>HostComponent</strong> (div, span, etc.): el nodo DOM real</li>
<li>Para un <strong>ClassComponent</strong>: la instancia de la clase</li>
<li>Para un <strong>HostRoot</strong>: el objeto FiberRoot</li>
</ul>
<p>Este campo sirve de puente entre el mundo virtual de Fiber y el DOM real del navegador.</p>
<h2 id="doble-búfer-árbol-current-y-árbol-workinprogress"><a class="anchor" href="#doble-búfer-árbol-current-y-árbol-workinprogress">Doble búfer: árbol current y árbol workInProgress</a></h2>
<p>Un concepto esencial que no puede faltar al estudiar Fiber es el <strong>doble búfer (Double Buffering)</strong>.</p>
<p>Para entenderlo, pensemos en los gráficos de un videojuego. Si se dibujan píxeles directamente sobre la pantalla visible, el usuario puede ver un frame a medio dibujar, un fenómeno de <strong>desgarro de imagen (tearing)</strong>. Para evitarlo, los motores de juegos utilizan <strong>dos búferes</strong>. Dibujan por completo el siguiente frame en uno de ellos y, cuando está terminado, sustituyen de una vez el búfer que se muestra en pantalla.</p>
<p>React Fiber utiliza exactamente la misma estrategia.</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>El <strong>árbol current</strong> es el árbol Fiber reflejado en la pantalla en ese momento: representa el estado de la UI que ve el usuario. El <strong>árbol workInProgress</strong> es el árbol Fiber que se prepara en segundo plano para el siguiente renderizado.</p>
<p>Ambos árboles se referencian entre sí mediante la propiedad <code>alternate</code>. Todos los cambios se realizan en el árbol workInProgress y, cuando el trabajo termina, los árboles se intercambian con una sola línea: <code>root.current = finishedWork</code>. El anterior workInProgress pasa a ser el nuevo current y el anterior current se recicla como workInProgress en el siguiente renderizado.</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>Detengámonos en el punto clave. <code>stateNode</code> —el nodo DOM real— se <strong>comparte</strong> entre current y workInProgress. En vez de crear un objeto Fiber desde cero en cada ocasión, React reutiliza el alternate existente y solo actualiza los campos modificados. Así puede construir el árbol de manera eficiente en cada renderizado sin añadir presión al recolector de basura (GC).</p>
<p>¿Y si no han cambiado las props ni el state? React puede omitir el subárbol completo mediante una <strong>optimización por bailout</strong>. Si el doble búfer de un juego optimiza por frames, el doble búfer de Fiber permite optimizar incluso <strong>por componentes</strong>.</p>
<h2 id="pendingworkpriority--lanes"><a class="anchor" href="#pendingworkpriority--lanes">pendingWorkPriority => Lanes</a></h2>
<p>Entonces, ¿cómo determina Fiber que «este trabajo es más importante»?</p>
<h3 id="limitaciones-de-expirationtime"><a class="anchor" href="#limitaciones-de-expirationtime">Limitaciones de expirationTime</a></h3>
<p>Las primeras versiones de Fiber empleaban una prioridad numérica llamada <code>pendingWorkPriority</code>, que más tarde evolucionó hacia un único número denominado <code>expirationTime</code>. Cuanto más próxima estaba la expiración, mayor era la prioridad, pero este enfoque tenía una limitación fundamental.</p>
<p>Un solo número no permitía una <strong>clasificación flexible</strong> del tipo «esta actualización pertenece al grupo A y aquella al grupo B». Por ejemplo, cuando una entrada del usuario y una actualización Transition ocurrían al mismo tiempo, el sistema basado en expirationTime solo podía clasificarlas comparando rangos (range), por lo que tenía dificultades para procesar selectivamente determinadas actualizaciones.</p>
<h3 id="lane"><a class="anchor" href="#lane">Lane</a></h3>
<p>Para resolver este problema, Andrew Clark introdujo el sistema de <strong>Lanes</strong> en la <a href="https://github.com/facebook/react/pull/18796" target="_blank" rel="noopener noreferrer">PR #18796</a>.</p>
<p>Para entender las Lanes, imaginemos una <strong>autopista</strong>. Una autopista tiene varios carriles (lanes), cada uno con una función distinta. El carril izquierdo sirve para adelantar —trabajo urgente—, otro para la circulación normal y el arcén para emergencias. Cada vehículo —una actualización— se asigna al carril correspondiente a sus características, y el sistema de gestión de la autopista —el scheduler— decide qué carril deja pasar primero.</p>
<p>Las Lanes de React funcionan igual. A cada actualización se le asigna <strong>un bit (lane)</strong> y, mediante operaciones bit a bit, se crean y comparan 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>El diseño permite almacenar un total de 31 lanes en un entero de 31 bits para aprovechar la optimización <strong>SMI (Small Integer)</strong> del motor V8. V8 procesa los enteros de hasta 31 bits mediante pointer tagging, lo que permite operar directamente con ellos en la pila sin asignarlos en el heap. En las lanes principales, <strong>cuanto más bajo es el bit, mayor es la prioridad</strong>.</p>
<p>Gracias a esta estructura, React puede decidir qué trabajo procesar primero con una sola operación bit a bit. La función <code>getNextLanes()</code> selecciona en <code>pendingLanes</code> el grupo de lanes con mayor prioridad, omite las lanes suspendidas (suspended) y reintenta primero las lanes que ya han recibido datos (pinged), lo que permite una planificación sofisticada.</p>
<p>Además, para evitar la <strong>inanición (starvation)</strong>, cada lane recibe un tiempo de expiración. Sync/InputContinuous se añade a <code>expiredLanes</code> al cabo de 250 ms y Transition al cabo de 5000 ms, lo que fuerza su procesamiento síncrono. Por baja que sea su prioridad, ningún trabajo queda ignorado para siempre. (Si una prioridad baja significara ser ignorado eternamente, ya no sería un sistema de prioridades, sino un sistema de discriminación).</p>
<h2 id="el-output-de-fiber"><a class="anchor" href="#el-output-de-fiber">El output de Fiber</a></h2>
<p>Después de examinar la estructura de Fiber, surge otra pregunta: ¿cómo se convierten estos nodos Fiber en el <strong>DOM real</strong>?</p>
<p>El output contiene la información concreta de los nodos DOM que se puede aplicar al DOM real. Aquí hay una distinción importante.</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>Solo los <strong>host components</strong> —div, span, img, etc.— crean nodos DOM reales. El navegador no sabe qué es <code>&#x3C;아바타/></code>. Como los componentes definidos por el usuario son conceptos abstractos, deben descomponerse en host components para que el navegador pueda entenderlos.</p>
<p>Veamos el proceso con más detalle.</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>La relación entre el árbol Fiber generado por estos componentes y su output es la siguiente.</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>El output se recopila <strong>de abajo arriba</strong>. Primero se crea el DOM en los nodos hoja (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>A continuación, el host component padre recopila el output de sus hijos.</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 último, los componentes definidos por el usuario transmiten directamente el output de sus hijos.</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="planificación-de-fiber"><a class="anchor" href="#planificación-de-fiber">Planificación de Fiber</a></h2>
<p>Si el valor principal de Fiber consiste en «poder dividir el trabajo», ¿dónde se lleva a cabo realmente esa división? En el <strong>Work Loop</strong>.</p>
<h3 id="work-loop-el-corazón-del-recorrido-de-fiber"><a class="anchor" href="#work-loop-el-corazón-del-recorrido-de-fiber">Work Loop: el corazón del recorrido de Fiber</a></h3>
<p>El renderizado de React empieza en el Work Loop definido en <code>ReactFiberWorkLoop.js</code>. React utiliza dos Work Loops según la situación.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="js" data-theme="github-dark github-light"><code data-language="js" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 동기 렌더링: 중단 없이 모든 Fiber를 처리</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">function</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> workLoopSync</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">() {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  while</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (workInProgress </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">!==</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> null</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) {</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">    performUnitOfWork</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(workInProgress);</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  }</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D">// 동시성 렌더링: 시간 제한 내에서 작업을 나누어 처리</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">function</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> workLoopConcurrent</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#FFAB70;--shiki-light:#E36209">nonIdle</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  if</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (workInProgress </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">!==</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> null</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">    const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> yieldAfter</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> now</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">() </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">+</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (nonIdle </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">?</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 25</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> :</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> 5</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">    do</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">      performUnitOfWork</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(workInProgress);</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">    } </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">while</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (workInProgress </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">!==</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> null</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> &#x26;&#x26;</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> now</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">() </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">&#x3C;</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> yieldAfter);</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  }</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>Observa la diferencia entre ambas funciones. <code>workLoopSync</code> se ejecuta <strong>incondicionalmente</strong> hasta que <code>workInProgress</code> pasa a ser <code>null</code>. En cambio, <code>workLoopConcurrent</code> impone un <strong>límite de tiempo</strong> y sale del bucle cuando se supera.</p>
<p>Resulta interesante la diferencia entre sus intervalos de yield. El trabajo <strong>non-idle —actualizaciones perceptibles por el usuario, como Transition o Retry—</strong> cede el control cada <strong>25 ms</strong>, mientras que el <strong>trabajo idle —trabajo de baja prioridad que puede procesarse cuando el usuario no hace nada—</strong> lo hace cada <strong>5 ms</strong>. El trabajo non-idle recibe 25 ms para limitar de forma deliberada las animaciones a unos 30 fps y evitar que el renderizado de una transition provoque inanición en otros trabajos.</p>
<h3 id="performunitofwork"><a class="anchor" href="#performunitofwork">performUnitOfWork</a></h3>
<p><code>performUnitOfWork</code> procesa un nodo Fiber. Esta función contiene el núcleo del recorrido de 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> procesa el nodo actual y devuelve su primer hijo. A continuación fija <code>pendingProps</code> como <code>memoizedProps</code>; si hay un hijo, avanza hacia él y, si no lo hay, llama a <code>completeUnitOfWork</code>.</p>
<h3 id="beginwork"><a class="anchor" href="#beginwork">beginWork</a></h3>
<p><code>beginWork</code> recorre los nodos Fiber de arriba abajo y realiza en cada uno los cálculos necesarios. Está definida en <code>ReactFiberBeginWork.js</code> y, por dentro, bifurca la ejecución con un enorme <strong>switch</strong> basado en el <code>tag</code> del 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>La clave está en la <strong>comprobación de bailout</strong> al principio. Si las props y el context son iguales que antes, <code>bailoutOnAlreadyFinishedWork</code> omite el subárbol completo. Este es uno de los caminos más importantes para optimizar el rendimiento de React.</p>
<p>El valor devuelto por <code>beginWork</code> es el <strong>primer Fiber hijo</strong>. Si existe, pasa a ser el siguiente <code>workInProgress</code>; si no existe (<code>null</code>), se entra en <code>completeUnitOfWork</code>.</p>
<h3 id="completework"><a class="anchor" href="#completework">completeWork</a></h3>
<p><code>completeWork</code> comienza en los nodos hoja y finaliza el trabajo mientras asciende hacia los padres.</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>Las principales tareas realizadas en <code>completeWork</code> son las siguientes.</p>
<ul>
<li><strong>Para un HostComponent</strong>: crea el nodo DOM real (<code>createInstance</code>) y añade los DOM hijos mediante append. Si el DOM ya existe, recopila las props modificadas y las guarda en <code>updateQueue</code>.</li>
<li><strong><code>bubbleProperties()</code></strong>: agrega los flags de los hijos en <code>subtreeFlags</code>. Esta información se utiliza durante la Commit Phase para optimizar la omisión de subárboles.</li>
</ul>
<p>En resumen, el recorrido funciona así: <strong>desciende siguiendo child (beginWork) -> al completar un nodo hoja avanza a sibling -> si no hay hermanos, asciende siguiendo return (completeWork)</strong>. Este es el orden de búsqueda en profundidad de Fiber.</p>
<h3 id="por-qué-se-descartó-requestidlecallback"><a class="anchor" href="#por-qué-se-descartó-requestidlecallback">Por qué se descartó requestIdleCallback</a></h3>
<p>Antes mostré un modelo conceptual de Fiber que utilizaba <code>requestIdleCallback</code>, pero React no lo usa en la práctica. Los motivos son claros.</p>
<ul>
<li><strong>Frecuencia de llamada demasiado baja</strong>: solo se invoca durante auténticos «periodos de inactividad —cuando el navegador no tiene nada que hacer—», por lo que en una página ocupada el trabajo de React podría posponerse indefinidamente. Dan Abramov también señaló que «requestIdleCallback is called too infrequently to be useful for scheduling React work».</li>
<li><strong>Problemas de compatibilidad entre navegadores</strong>: Safari tardó mucho en implementarlo y su comportamiento variaba entre navegadores.</li>
<li><strong>Límite superior de 20 ms</strong>: el idle deadline tiene un límite máximo que impide a React controlar los tiempos con la previsibilidad que necesita.</li>
</ul>
<p>Después se probó un enfoque basado en <code>requestAnimationFrame</code> y la estimación del presupuesto de cada frame. Sin embargo, también se abandonó al concluir que el trabajo de React no necesitaba ajustarse al ciclo de vsync —la técnica que sincroniza la salida de frames con el momento en que el monitor completa el barrido vertical—.</p>
<h3 id="messagechannel"><a class="anchor" href="#messagechannel">MessageChannel</a></h3>
<p>Finalmente, React eligió <strong>MessageChannel</strong>.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="js" data-theme="github-dark github-light"><code data-language="js" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">if</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> (</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">typeof</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> MessageChannel </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">!==</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> 'undefined'</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">) {</span></span>
<span data-line=""><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">  const</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> channel</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> new</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> MessageChannel</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">();</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  channel.port1.onmessage </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> performWorkUntilDeadline;</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  schedulePerformWorkUntilDeadline</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> () </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> channel.port2.</span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">postMessage</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">null</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">} </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">else</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> {</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">  schedulePerformWorkUntilDeadline</span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49"> =</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E"> () </span><span style="--shiki-dark:#F97583;--shiki-light:#D73A49">=></span><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1"> setTimeout</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">(performWorkUntilDeadline, </span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">0</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">);</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span></code></pre></figure>
<p>¿Por qué no <code>setTimeout</code>, sino <code>MessageChannel</code>? Según la especificación HTML, cuando <code>setTimeout</code> se anida cinco veces o más se impone un <strong>retraso mínimo de 4 ms</strong>. <code>MessageChannel</code>, en cambio, se ejecuta inmediatamente como macrotask en el siguiente tick del event loop sin esta limitación. Para Fiber, que divide el trabajo en unidades de 5 ms, una demora artificial de 4 ms sería fatal.</p>
<p>(Si de 5 ms se dedican 4 a esperar, solo queda 1 ms de trabajo real. Eso no es conciliación entre vida y trabajo: es solo vida).</p>
<p>El paquete Scheduler de React mantiene internamente <strong>dos min-heaps (montículos mínimos)</strong>.</p>
<pre><code>timerQueue (대기실)                    taskQueue (실행 대기열)
┌──────────────────┐                  ┌──────────────────┐
│ 아직 시작 시간이     │   startTime      │ 지금 실행 가능한     │
│ 안 된 태스크들       │ ──경과 시──→      │ 태스크들           │
│                  │                  │                  │
│ 정렬: startTime   │                  │ 정렬: expiration  │
│ (빠른 순)          │                  │ Time (임박한 순)   │
└──────────────────┘                  └──────────────────┘
</code></pre>
<p><strong>taskQueue</strong> es la cola de tareas «que pueden ejecutarse ahora mismo». Cuanto menor sea <code>expirationTime</code> (= startTime + timeout), es decir, cuanto más próxima esté la expiración, antes se ejecutan. <strong>timerQueue</strong> es la sala de espera de las tareas «cuyo momento de ejecución todavía no ha llegado». En cuanto el tiempo actual supera startTime, se trasladan a taskQueue.</p>
<p>Entonces, ¿cómo se determina el timeout que define expirationTime? Cada actualización recibe un timeout propio según su prioridad (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 nada más crearse y recibe la máxima prioridad en cuanto entra en taskQueue. (Expirar en el mismo momento de nacer es un destino un poco triste). Los 250 ms de <strong>UserBlocking</strong> se ajustan al umbral en que una persona percibe una respuesta como lenta —entre 100 y 300 ms—. Si tras un clic no ocurre nada durante 0,25 segundos, el usuario se irrita. Los 5 segundos de <strong>Normal</strong> pueden parecer generosos, pero garantizan que el trabajo se procesará incluso en el peor caso. En la práctica, se ejecuta en cuanto terminan las tareas anteriores. Los aproximadamente 12,4 días de <strong>Idle</strong> equivalen de hecho al infinito. Solo se ejecuta cuando ha terminado todo lo demás. (Como es muy poco probable mantener el navegador abierto durante 12 días, podemos considerarlo infinito).</p>
<p>Estos timeouts también funcionan como mecanismo para evitar la <strong>inanición (starvation)</strong>. Por baja que sea la prioridad, cuando transcurre el timeout el trabajo expira y se fuerza su ejecución. Aunque sigan llegando tareas de alta prioridad, las de baja prioridad nunca quedan ignoradas para siempre.</p>
<p><code>shouldYieldToHost()</code> del Scheduler comprueba si el tiempo transcurrido desde el inicio del trabajo supera <code>frameInterval</code> —<strong>5 ms</strong> por defecto, definido en <code>SchedulerFeatureFlags.js</code>— y decide si debe devolver el control al hilo principal.</p>
<h2 id="render-phase-y-commit-phase"><a class="anchor" href="#render-phase-y-commit-phase">Render Phase y Commit Phase</a></h2>
<p>Hasta ahora hemos visto la estructura y la planificación de Fiber. Organicemos ahora el flujo completo para entender cómo se combinan todas estas piezas y producen una actualización real de la UI.</p>
<p>Internamente, Fiber atraviesa dos etapas: la <strong>Render Phase</strong> y la <strong>Commit Phase</strong>. Esta separación es el diseño esencial que hace posible el modelo de concurrencia de React. Si quieres observar directamente el flujo de funcionamiento de Fiber, haz clic en la imagen siguiente.</p>
<p><a href="https://storied-centaur-55230f.netlify.app/" target="_blank" rel="noopener noreferrer"><img src="/content/250520/2.png" alt="2.png" width="2486" height="1778" loading="eager" fetchpriority="high" decoding="async"></a></p>
<h3 id="render-phase"><a class="anchor" href="#render-phase">Render Phase</a></h3>
<p>La Render Phase es la etapa que <strong>calcula qué cambios necesita</strong> la UI. En ella no se modifica realmente el DOM. Su característica más importante es que <strong>puede interrumpirse y reanudarse de forma asíncrona</strong>.</p>
<p>Esta etapa gira en torno a <code>beginWork</code> y <code>completeWork</code>, que ya hemos visto.</p>
<p>En <strong>beginWork(fiber)</strong> se ejecuta la lógica correspondiente al tipo de cada Fiber —FunctionComponent, ClassComponent, HostComponent, etc.— y se crean y enlazan sus nodos Fiber hijos. Si las props son iguales a las anteriores, se puede omitir el trabajo mediante memoization (bailout).</p>
<p>En <strong>completeWork(fiber)</strong> se preparan la creación del DOM y la información de los effects. Después, <code>bubbleProperties()</code> agrega los flags de los hijos en <code>subtreeFlags</code> y completa la información mientras asciende hacia los padres.</p>
<p>Como esta etapa no modifica directamente el DOM, el trabajo puede interrumpirse en cualquier momento y retomarse más tarde sin exponer al usuario una UI incompleta. Esta es la base del modo Concurrent.</p>
<h3 id="subtreeflags"><a class="anchor" href="#subtreeflags">subtreeFlags</a></h3>
<p>Durante la Render Phase, cada Fiber registra mediante <strong>flags de bits</strong> qué efectos secundarios (side effects) necesita. Veamos los principales flags definidos en <code>ReactFiberFlags.js</code>.</p>
<ul>
<li><code>Placement</code>: insertar un nodo nuevo en el DOM</li>
<li><code>Update</code>: actualizar propiedades del DOM</li>
<li><code>ChildDeletion</code>: eliminar un nodo hijo</li>
<li><code>Ref</code>: conectar o desconectar una ref</li>
<li><code>Passive</code>: ejecutar un callback de useEffect</li>
<li><code>Snapshot</code>: ejecutar getSnapshotBeforeUpdate</li>
<li><code>Callback</code>: ejecutar un callback del lifecycle</li>
</ul>
<p>Las versiones anteriores de React —hasta la 16— utilizaban una linked list conectada mediante <code>firstEffect</code> -> <code>nextEffect</code> -> <code>lastEffect</code> para reunir únicamente los Fibers con efectos secundarios. Sin embargo, las referencias a Fibers desmontados permanecían y provocaban <strong>memory leaks</strong>; además, resultaba difícil procesar de forma eficiente patrones nuevos como Suspense.</p>
<p>A partir de React 17 se eliminó esta effect list y se adoptó el sistema 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 los flags de los hijos en el padre.</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>La gran ventaja de esta estructura es que durante la Commit Phase se puede <strong>omitir un subárbol completo</strong>. Si en un Fiber se cumple <code>subtreeFlags &#x26; MutationMask === NoFlags</code>, no hay ningún nodo de ese subárbol que requiera cambios en el DOM, por lo que puede saltarse entero. Esta optimización era imposible con el anterior sistema de linked lists.</p>
<h3 id="commit-phase"><a class="anchor" href="#commit-phase">Commit Phase</a></h3>
<p>La Commit Phase es la etapa que <strong>aplica al DOM real</strong> los cambios calculados durante la Render Phase. Se ejecuta <strong>siempre de forma síncrona</strong> y, una vez iniciada, continúa hasta el final sin interrupciones. Así se evita que el usuario vea una UI actualizada solo a medias.</p>
<p>Internamente, la Commit Phase sigue esta secuencia detallada.</p>
<ol>
<li><strong>Before Mutation Phase</strong>: <code>commitBeforeMutationEffects()</code>
<ul>
<li>Lee el estado actual del DOM antes de modificarlo. Aquí se ejecuta el lifecycle <code>getSnapshotBeforeUpdate</code>. En ese momento el árbol <code>current</code> todavía representa el estado visible, por lo que se pueden capturar con seguridad datos como la posición del scroll o el tamaño del DOM.</li>
</ul>
</li>
<li><strong>Mutation Phase</strong>: <code>commitMutationEffects()</code>
<ul>
<li>Es la etapa en la que se realizan las <strong>operaciones reales sobre el DOM</strong>: insertar nodos nuevos, modificar los existentes y eliminar los innecesarios. <code>componentWillUnmount</code> también se ejecuta aquí, porque <code>current</code> todavía apunta al árbol anterior y permite leer su estado.</li>
</ul>
</li>
<li><strong>Intercambio de árboles</strong>: <code>root.current = finishedWork</code>
<ul>
<li>Es la esencia del doble búfer. El árbol workInProgress asciende a árbol current. Es importante que el intercambio ocurra después de Mutation y antes de Layout: <code>componentWillUnmount</code> debe leer el <strong>árbol anterior</strong>, por lo que se ejecuta durante Mutation, mientras que <code>componentDidMount</code>/<code>componentDidUpdate</code> deben leer el <strong>árbol nuevo</strong>, por lo que se ejecutan durante Layout.</li>
</ul>
</li>
<li><strong>Layout Phase</strong>: <code>commitLayoutEffects()</code>
<ul>
<li>Una vez modificado el DOM, se ejecutan los trabajos basados en su nuevo estado.
<ul>
<li>Ejecución de <code>componentDidMount</code> y <code>componentDidUpdate</code></li>
<li>Ejecución de los callbacks de <code>useLayoutEffect</code></li>
<li>En este punto <code>current</code> ya apunta al árbol nuevo, por lo que al leer el DOM se obtienen los valores actualizados</li>
</ul>
</li>
</ul>
</li>
<li><strong>Passive Effects</strong> (asíncronos)
<ul>
<li>El cleanup y el setup de <code>useEffect</code> se planifican por separado y se ejecutan de forma <strong>asíncrona</strong>. Como procesan efectos secundarios que no dependen de cambios en el DOM —obtención de datos, suscripciones a eventos, etc.—, no es necesario ejecutarlos síncronamente. Al procesarlos de forma asíncrona, React cede el control para que el navegador pueda pintar antes la pantalla.</li>
</ul>
</li>
</ol>
<h2 id="concurrent-features-y-fiber"><a class="anchor" href="#concurrent-features-y-fiber">Concurrent Features y Fiber</a></h2>
<p>Veamos ahora qué experiencias de usuario hacen posibles todos los elementos de Fiber estudiados hasta aquí —doble búfer, prioridades basadas en Lanes y Work Loop interrumpible— mediante las Concurrent Features disponibles desde React 18.</p>
<h3 id="usetransition"><a class="anchor" href="#usetransition">useTransition</a></h3>
<p>Al llamar a <code>startTransition(() => setState(...))</code>, se asigna una <code>TransitionLane</code> a esa actualización. Existen 14 TransitionLanes que se distribuyen mediante round-robin —asignándolas una a una por turnos— para evitar colisiones.</p>
<p>Como TransitionLane tiene menos prioridad que SyncLane o DefaultLane, si llega una actualización urgente como una entrada del usuario, React puede <strong>interrumpir</strong> el renderizado de la transition y procesar primero la actualización urgente. Mientras tanto, la pantalla mantiene el árbol <code>current</code> —el estado anterior— y la transition avanza en segundo plano sobre el árbol workInProgress.</p>
<p>Aquí es donde brilla el valor del doble búfer. Un renderizado de transition interrumpido solo afecta al árbol workInProgress; la pantalla que ve el usuario —el árbol current— permanece completamente intacta.</p>
<p>El flag <code>isPending</code> indica que la transition todavía no ha terminado, lo que permite mostrar un indicador de carga u ofrecer un tratamiento similar.</p>
<h3 id="usedeferredvalue"><a class="anchor" href="#usedeferredvalue">useDeferredValue</a></h3>
<p>En el primer renderizado, <code>useDeferredValue(value)</code> devuelve directamente el <code>value</code> recibido. En renderizados posteriores, si el render actual es urgente, devuelve el valor memoized anterior y planifica un render nuevo con TransitionLane. Igual que una Transition, el renderizado diferido se puede interrumpir.</p>
<p>Conceptualmente se parece a <code>startTransition</code>, pero se aplica en el lado que <strong>recibe el valor</strong>, no en el que despacha la actualización. Un caso de uso habitual consiste en reflejar inmediatamente el texto de un campo de búsqueda, pero retrasar el renderizado de la lista de resultados.</p>
<h3 id="suspense"><a class="anchor" href="#suspense">Suspense</a></h3>
<p>Cuando un componente dentro de <code>&#x3C;Suspense></code> lanza una Promise, <code>throwException</code> la captura y marca ese Fiber como <code>Incomplete</code>. Después asciende por la cadena <code>return</code> en busca del límite de Suspense más cercano y hace que este muestre la fallback UI. Cuando la Promise se resuelve, <code>markRootPinged</code> hace ping a la lane correspondiente y React vuelve a renderizar el subárbol suspendido.</p>
<p>En el modo Concurrent se pueden <strong>seguir renderizando los nodos hermanos (sibling)</strong> de un componente suspendido, de modo que una sola petición de datos no bloquea el renderizado del árbol completo. Esto es posible porque la estructura de linked list de Fiber permite desplazarse libremente hacia sibling.</p>
<h3 id="streaming-ssr-y-selective-hydration"><a class="anchor" href="#streaming-ssr-y-selective-hydration">Streaming SSR y Selective Hydration</a></h3>
<p><code>renderToPipeableStream</code> de React 18 utiliza los límites de Suspense.</p>
<ul>
<li><strong>Servidor</strong>: cuando un límite de Suspense se suspende, envía primero el HTML de fallback y, cuando los datos están listos, transmite después el contenido real mediante una etiqueta <code>&#x3C;script></code></li>
<li><strong>Cliente (Selective Hydration)</strong>: cada límite de Suspense puede hidratarse de forma <strong>independiente</strong>. Si el usuario hace clic en una zona todavía no hidratada, <code>SelectiveHydrationLane</code> procesa <strong>con prioridad</strong> la hydration de ese límite y después despacha el evento</li>
</ul>
<p>Todo esto es posible porque cada límite de Suspense es un nodo Fiber que se puede planificar de manera independiente. En definitiva, el diseño esencial de la arquitectura Fiber —«dividir el trabajo, asignarle prioridades e interrumpirlo/reanudarlo»— constituye la base de todas estas funcionalidades.</p>
<h2 id="conclusión"><a class="anchor" href="#conclusión">Conclusión</a></h2>
<p>Si hubiera que resumir este artículo en una frase: <strong>React Fiber es una arquitectura que sustituye la recursión por iteración y traslada el call stack al heap para poder interrumpir y reanudar el renderizado</strong>.</p>
<p>Para conseguirlo combina numerosos diseños sofisticados: una estructura de árbol basada en linked lists, doble búfer, un sistema de prioridades basado en Lanes y un scheduler basado en MessageChannel. Todos persiguen un mismo objetivo: <strong>maximizar la capacidad de respuesta de la UI percibida por el usuario</strong>.</p>
<p>Por supuesto, la implementación interna de Fiber sigue cambiando con cada versión de React, y lo explicado aquí no deja de ser una fotografía tomada en un momento concreto. Sin embargo, creo que la filosofía esencial de Fiber —«dividir el trabajo, asignarle prioridades e interrumpirlo y reanudarlo»— permanecerá inalterada.</p>
<p>Espero que este artículo haya mostrado que React Fiber no es una simple palabra clave para entrevistas, sino la arquitectura de runtime que sostiene todas las funciones de React. No existe una única respuesta correcta, pero también espero que quienes lean este artículo examinen directamente el código fuente y construyan su propia comprensión.</p>
<h2 id="fuentes"><a class="anchor" href="#fuentes">Fuentes</a></h2>
<details class="ref-block"><summary class="ref-summary">참고 자료</summary><div class="ref-list">
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#10b981"></span><a href="https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactFiberWorkLoop.js" target="_blank" rel="noopener noreferrer">Código fuente de 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 fuente de 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 fuente de 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 fuente de 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 fuente de 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 fuente de 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">Artículo del blog de React v18.0</a></div>
</div>
</div></details>]]></content:encoded>
            <author>jihoon7705@gmail.com (이지훈)</author>
            <category>프론트엔드</category>
            <category>React</category>
        </item>
        <item>
            <title><![CDATA[¿Puede Biome reemplazar a ESLint y Prettier?]]></title>
            <link>https://hooninedev.com/es/241201</link>
            <guid isPermaLink="false">https://hooninedev.com/es/241201</guid>
            <pubDate>Sun, 01 Dec 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[En esta publicación quiero hablar de una herramienta llamada Biome. El equipo en el que trabajo tenía bastantes dificultades para mantener un estilo de código coherente en un entorno donde se usaban d...]]></description>
            <content:encoded><![CDATA[<p>En esta publicación quiero hablar de una herramienta llamada Biome.</p>
<p>El equipo en el que trabajo tenía bastantes dificultades para mantener un estilo de código coherente en un entorno donde se usaban distintos IDE, como WebStorm y VSCode. También resultaba tedioso gestionar archivos de configuración separados para cada IDE, y era frecuente que en las revisiones de código surgieran observaciones sobre diferencias de formato sin relación con la lógica.</p>
<p>En esta situación, las reglas de formato de ESLint quedaron Deprecated y tuvimos que buscar una alternativa nueva. La combinación <strong>Prettier + ESLint</strong> exigía configuración adicional para evitar conflictos entre ambas herramientas, mientras que <strong>@stylistic/eslint-plugin-ts</strong> todavía se encontraba en una etapa temprana dentro de la comunidad y no contaba con suficiente validación de estabilidad. Fue entonces cuando Biome despertó nuestro interés.</p>
<p>Entonces, ¿qué es exactamente Biome y de verdad puede reemplazar a ESLint y Prettier?</p>
<hr>
<h2 id="qué-es-biome"><a class="anchor" href="#qué-es-biome">¿Qué es Biome?</a></h2>
<p>Biome es una toolchain todo en uno (All-in-One) para proyectos web. Ofrece de forma integrada el formateo y linting de código JavaScript, TypeScript, JSX, CSS, JSON, GraphQL y más desde una única herramienta. Su filosofía central consiste en resolver con un solo binario las funciones que antes desempeñaban ESLint y Prettier por separado.</p>
<p>El antecesor de Biome fue <a href="https://github.com/rome/tools" target="_blank" rel="noopener noreferrer">Rome</a>. <strong>Rome Tools Inc.</strong> arrancó con grandes ambiciones tras recaudar 4,5 millones de dólares de inversión de riesgo en 2021, pero a mediados de 2023 despidió a toda su plantilla y archivó el repositorio. Después, los principales contributors hicieron un fork del proyecto y lo relanzaron como Biome en agosto de 2023. Tras dejar atrás la imagen de Rome de «prometer demasiado y cumplir poco», ha ido ganando confianza con releases prácticas y constantes.</p>
<p>Su característica más destacada es que está escrito en Rust. Más adelante veremos en detalle qué diferencia supone esto para el rendimiento.</p>
<hr>
<h2 id="por-qué-usar-biome"><a class="anchor" href="#por-qué-usar-biome">¿Por qué usar Biome?</a></h2>
<p>Los motivos para elegir Biome pueden resumirse en tres puntos principales.</p>
<p><strong>Una sola herramienta se encarga tanto del formateo como del linting.</strong> Con la combinación ESLint + Prettier hacía falta configuración adicional, como <code>eslint-config-prettier</code>, para impedir conflictos entre las reglas de ambas herramientas. Biome elimina esa complejidad desde la raíz.</p>
<p><strong>Su rendimiento es abrumador.</strong> Según los benchmarks oficiales, es unas 25 veces más rápido que Prettier y unas 15 veces más rápido que ESLint. Más adelante compararemos directamente qué representan estas cifras en la práctica.</p>
<p><img src="/content/241201/1.png" alt="1.png" width="2250" height="986" loading="eager" fetchpriority="high" decoding="async"></p>
<p><strong>Es compatible con las herramientas existentes.</strong> Ofrece aproximadamente un 97 % de compatibilidad de formato con Prettier e incluye de serie las principales reglas de ESLint. También incorpora reglas de plugins habituales como <code>eslint-plugin-react-hooks</code> y <code>eslint-plugin-jsx-a11y</code>, por lo que la carga de la migración es relativamente pequeña.</p>
<hr>
<h2 id="cómo-se-utiliza"><a class="anchor" href="#cómo-se-utiliza">¿Cómo se utiliza?</a></h2>
<p>Configurar Biome es bastante sencillo. La <a href="https://biomejs.dev/guides/getting-started/" target="_blank" rel="noopener noreferrer">documentación oficial</a> lo explica con claridad, así que conviene consultarla.</p>
<p>Primero, instala 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>A continuación, genera el archivo de configuración.</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>Esto crea un archivo <code>biome.json</code>. En él puedes definir las reglas de formateo y linting del equipo.</p>
<p>También hay que instalar una extensión para el IDE. Si utilizas VSCode, instala <a href="https://marketplace.visualstudio.com/items?itemName=biomejs.biome" target="_blank" rel="noopener noreferrer">VSCode Biome</a>; si utilizas WebStorm, instala el plugin <a href="https://plugins.jetbrains.com/plugin/22761-biome" target="_blank" rel="noopener noreferrer">WebStorm Biome</a>.</p>
<p>Por último, añade la siguiente configuración al <code>settings.json</code> de VSCode para aplicar automáticamente el formateo y el linting cada vez que guardes.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="json" data-theme="github-dark github-light"><code data-language="json" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">{</span></span>
<span data-line=""><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">  "editor.defaultFormatter"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"biomejs.biome"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">  "editor.codeActionsOnSave"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: {</span></span>
<span data-line=""><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">    "quickfix.biome"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"explicit"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">,</span></span>
<span data-line=""><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5">    "source.organizeImports.biome"</span><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">: </span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62">"explicit"</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">  }</span></span>
<span data-line=""><span style="--shiki-dark:#E1E4E8;--shiki-light:#24292E">}</span></span></code></pre></figure>
<hr>
<h2 id="hagamos-una-comparación-directa"><a class="anchor" href="#hagamos-una-comparación-directa">Hagamos una comparación directa</a></h2>
<p>Decir simplemente que es rápido no permite apreciar la diferencia, así que comparé directamente Biome con ESLint + Prettier en el mismo proyecto. Biome aparece a la izquierda y ESLint + Prettier a la derecha.</p>
<h3 id="tiempo-de-ejecución-local-de-un-proyecto-vite"><a class="anchor" href="#tiempo-de-ejecución-local-de-un-proyecto-vite">Tiempo de ejecución local de un proyecto Vite</a></h3>
<p><img src="/content/241201/biome1.png" alt="biome1.png" width="798" height="330" loading="lazy" fetchpriority="low" decoding="async">  <img src="/content/241201/lint1.png" alt="lint1.png" width="766" height="248" loading="lazy" fetchpriority="low" decoding="async"></p>
<p>Biome tardó <strong>506ms</strong>, frente a los <strong>630ms</strong> de ESLint + Prettier, lo que supone un tiempo de ejecución aproximadamente un 20 % más rápido.</p>
<hr>
<h3 id="tiempo-de-build-de-un-proyecto-vite"><a class="anchor" href="#tiempo-de-build-de-un-proyecto-vite">Tiempo de build de un proyecto Vite</a></h3>
<p><img src="/content/241201/biome2.png" alt="biome2.png" width="920" height="222" loading="lazy" fetchpriority="low" decoding="async"> <img src="/content/241201/lint2.png" alt="lint2.png" width="1022" height="228" loading="lazy" fetchpriority="low" decoding="async"></p>
<p>Biome tardó <strong>117.13s</strong>, frente a los <strong>131.48s</strong> de ESLint + Prettier, lo que supone un tiempo de build aproximadamente un 10 % más rápido.</p>
<hr>
<h3 id="tarea-de-linting"><a class="anchor" href="#tarea-de-linting">Tarea de linting</a></h3>
<p><img src="/content/241201/biome3.png" alt="biome3.png" width="894" height="174" loading="lazy" fetchpriority="low" decoding="async"> <img src="/content/241201/lint3.png" alt="lint3.png" width="974" height="186" loading="lazy" fetchpriority="low" decoding="async"></p>
<p>La mayor diferencia apareció en la tarea de linting. Biome tardó <strong>0.79s</strong> (CPU 0.470s), mientras que ESLint tardó <strong>16.32s</strong> (CPU 8.600s), por lo que <strong>Biome ofreció un rendimiento unas 20 veces mayor</strong>. El uso de CPU también fue mucho más eficiente.</p>
<p>La diferencia ya se percibe claramente en el entorno de desarrollo, pero se vuelve aún más drástica cuando una pipeline de CI/CD comprueba cientos de archivos. Como Biome puede ejecutar directamente su binario sin instalarlo mediante npm, también permite ahorrar tiempo en el cold start de CI.</p>
<hr>
<p><img src="/content/241201/3.jpeg" alt="3.jpeg" width="265" height="190" loading="lazy" fetchpriority="low" decoding="async"></p>
<p>Mmm... (A estas alturas, es más difícil encontrar un motivo para no usarlo.)</p>
<hr>
<h2 id="por-qué-es-tan-rápido"><a class="anchor" href="#por-qué-es-tan-rápido">¿Por qué es tan rápido?</a></h2>
<p>«Es rápido porque está hecho con Rust» es una afirmación correcta, pero no basta para explicarlo. Veamos los factores técnicos concretos que generan la ventaja de rendimiento de Biome.</p>
<hr>
<h3 id="rendimiento-de-bajo-nivel-de-rust"><a class="anchor" href="#rendimiento-de-bajo-nivel-de-rust">Rendimiento de bajo nivel de Rust</a></h3>
<table>
<thead>
<tr>
<th><img src="/content/241201/5.webp" alt="5.webp" width="1200" height="1000" loading="lazy" fetchpriority="low" decoding="async"></th>
<th><img src="/content/241201/6.webp" alt="6.webp" width="1200" height="1000" loading="lazy" fetchpriority="low" decoding="async"></th>
</tr>
</thead>
</table>
<p>Biome está escrito en Rust, un lenguaje de programación de sistemas. Rust busca abstracciones de coste cero (Zero-cost Abstraction), de modo que incluso las abstracciones de alto nivel ofrecen el mismo rendimiento que el código de bajo nivel optimizado manualmente. Además, como administra la memoria mediante un sistema de ownership sin garbage collector (GC), no sufre el overhead de runtime provocado por el GC.</p>
<p>En cambio, ESLint y Prettier están escritos en JavaScript y se ejecutan sobre el runtime de Node.js. Aunque la compilación JIT (Just-In-Time) del motor V8 optimiza JavaScript, no puede evitar por completo las limitaciones fundamentales de un lenguaje interpretado ni el coste de la recolección de basura.</p>
<hr>
<h3 id="arquitectura-de-parsing-único"><a class="anchor" href="#arquitectura-de-parsing-único">Arquitectura de parsing único</a></h3>
<p>Biome analiza el código una sola vez con un único parser para generar un AST (Abstract Syntax Tree, árbol de sintaxis abstracta). Después reutiliza ese AST tanto para el formateo como para el linting.</p>
<p>¿Qué ocurre cuando se usa la combinación ESLint + Prettier? ESLint analiza el código, crea un AST y realiza el linting; después, Prettier vuelve a analizar el mismo código, crea otro AST y realiza el formateo. Es decir, el mismo archivo se analiza dos veces. La arquitectura de parsing único de Biome elimina esta duplicación desde el origen.</p>
<hr>
<h3 id="procesamiento-paralelo-nativo"><a class="anchor" href="#procesamiento-paralelo-nativo">Procesamiento paralelo nativo</a></h3>
<p><img src="/content/241201/7.png" alt="7.png" width="800" height="514" loading="lazy" fetchpriority="low" decoding="async"></p>
<p>Biome aprovecha el modelo de concurrencia de Rust para procesar archivos en paralelo mediante varios threads. Divide el trabajo en unidades pequeñas y distribuye eficientemente la carga entre los threads con un scheduler de work-stealing. Como el sistema de ownership de Rust impide los data races en tiempo de compilación, también se minimiza el coste de sincronización durante el runtime.</p>
<p>Node.js utiliza por defecto un modelo single-thread basado en un event loop. Es posible procesar en paralelo mediante Worker Threads, pero esto introduce overhead adicional por la creación de threads y el message passing. Biome utiliza directamente threads nativos a nivel del sistema operativo, por lo que puede aprovechar al máximo los núcleos de CPU sin ese overhead.</p>
<hr>
<h3 id="procesamiento-de-ast-eficiente-en-memoria"><a class="anchor" href="#procesamiento-de-ast-eficiente-en-memoria">Procesamiento de AST eficiente en memoria</a></h3>
<p><img src="/content/241201/4.svg" alt="4.svg" loading="lazy" fetchpriority="low" decoding="async"></p>
<p>Biome utiliza un CST (Concrete Syntax Tree, árbol de sintaxis concreta). Según la documentación oficial de arquitectura de Biome, este CST implementa el patrón Green/Red Tree sobre un fork interno de la biblioteca rowan y conserva toda la información del código original, incluidos comentarios y espacios en blanco. La asignación de memoria al estilo arena de rowan coloca los nodos en regiones contiguas de memoria, mejora la localidad de caché (Cache Locality) de la CPU y minimiza asignaciones innecesarias de objetos.</p>
<p>En el procesamiento de AST basado en objetos de JavaScript, cada nodo existe como un objeto independiente en el heap, por lo que la memoria queda dispersa y aumenta la presión sobre el GC. El enfoque de Biome permite recorrer el árbol más rápido utilizando menos memoria.</p>
<hr>
<h2 id="entonces-conviene-adoptar-biome"><a class="anchor" href="#entonces-conviene-adoptar-biome">Entonces, ¿conviene adoptar Biome?</a></h2>
<p>El rendimiento y la comodidad de Biome son claramente atractivos. Sin embargo, no creo que adoptarlo sin condiciones sea la respuesta correcta para todos los proyectos. Veamos algunas consideraciones prácticas.</p>
<hr>
<h3 id="cuándo-encaja-biome"><a class="anchor" href="#cuándo-encaja-biome">Cuándo encaja Biome</a></h3>
<ul>
<li>Cuando mantienes una <strong>base de código de gran tamaño</strong> y el rendimiento del build y del linting es importante</li>
<li>Cuando quieres reducir el tiempo de comprobación del código en una pipeline de CI/CD</li>
<li>Cuando estás cansado de la complejidad de configurar ESLint + Prettier</li>
<li>Cuando empiezas un proyecto nuevo y quieres una configuración de herramientas sencilla</li>
</ul>
<p>Mi equipo también mantenía un proyecto de gran tamaño en el que el linting consumía mucho tiempo de la pipeline de CI, y los desarrolladores sufrían la lentitud del proceso, por lo que decidimos adoptar Biome.</p>
<hr>
<h3 id="aspectos-que-requieren-atención"><a class="anchor" href="#aspectos-que-requieren-atención">Aspectos que requieren atención</a></h3>
<p><strong>La mayor limitación es el ecosistema de plugins.</strong> ESLint cuenta con miles de plugins de la comunidad, mientras que Biome se centra en reglas integradas. Incluye muchas reglas 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> y <code>typescript-eslint</code>, pero no se han portado todas las reglas de cada plugin. Se ha anunciado para Biome v2 un sistema de plugins basado en GritQL, aunque todavía se encuentra en fase experimental. Los proyectos que necesiten reglas específicas de un framework, como <code>@next/eslint-plugin-next</code> o <code>eslint-plugin-angular</code>, deben plantear la migración con cautela.</p>
<p><strong>También hay que comprobar el alcance del soporte de lenguajes.</strong> JavaScript, TypeScript, JSX, CSS, JSON y GraphQL tienen soporte estable, pero en los archivos SFC (Single File Component) de Vue y Svelte solo se admite parcialmente el bloque <code>&#x3C;script></code>. HTML, YAML y Markdown todavía no son compatibles.</p>
<p><strong>No hay que olvidar que ESLint también evoluciona.</strong> Flat Config (<code>eslint.config.js</code>), introducido en ESLint v9 en abril de 2024, simplificó considerablemente la complejidad del antiguo enfoque basado en <code>.eslintrc</code>. Además, con el lanzamiento de <code>@eslint/json</code> en octubre de 2024 y <code>@eslint/css</code> en febrero de 2025, ESLint está ampliando el linting a lenguajes distintos de JavaScript. El proyecto ESLint Stylistic (<code>@stylistic/eslint-plugin</code>) ofrece una opción para gestionar el formateo solo con ESLint y sin Prettier. La ventaja «todo en uno» de Biome se está diluyendo en cierta medida a medida que evoluciona el ecosistema de ESLint.</p>
<p>También conviene recordar la historia de la transición de Rome a Biome. Los inconvenientes que sufrieron los usuarios existentes cuando Rome fue archivado demuestran la importancia de la sostenibilidad de un proyecto al elegir una herramienta. Por suerte, Biome se financia mediante OpenCollective y GitHub Sponsors y mantiene un ritmo constante de releases.</p>
<p><img src="/content/241201/8.png" alt="8.png" width="2722" height="1384" loading="lazy" fetchpriority="low" decoding="async"></p>
<p>Según npm trends, las descargas semanales de Biome, alrededor de 6,9 millones, todavía están muy lejos de los aproximadamente 120 millones de ESLint y los 82 millones de Prettier. Sin embargo, la velocidad de crecimiento de Biome resulta destacable. En poco más de un año, sus descargas semanales se han multiplicado por más de tres o cuatro, y su adopción ha aumentado de forma especialmente visible en proyectos nuevos.</p>
<hr>
<h2 id="conclusión"><a class="anchor" href="#conclusión">Conclusión</a></h2>
<p>Mi respuesta a la pregunta de si Biome puede reemplazar por completo a ESLint y Prettier es <strong>«todavía no, pero es una alternativa muy sólida»</strong>.</p>
<p>Su rendimiento es abrumador, la configuración es concisa y el ritmo de desarrollo es rápido. Sin embargo, la inmadurez del ecosistema de plugins y las limitaciones de soporte para algunos lenguajes pueden ser obstáculos según el proyecto. Lo recomendable es revisar detenidamente el stack tecnológico del proyecto y las necesidades del equipo antes de decidir si adoptarlo.</p>
<p>Algo está claro: el ecosistema de herramientas frontend avanza hacia soluciones «más rápidas, más sencillas y más integradas». Es innegable que Biome está a la cabeza de esa tendencia. Sin duda, es una herramienta cuyo crecimiento futuro merece atención.</p>
<h2 id="referencias"><a class="anchor" href="#referencias">Referencias</a></h2>
<details class="ref-block"><summary class="ref-summary">참고 자료</summary></details>]]></content:encoded>
            <author>jihoon7705@gmail.com (이지훈)</author>
            <category>프론트엔드</category>
            <category>자바스크립트</category>
        </item>
        <item>
            <title><![CDATA[Zustand, ¿qué eres y por qué eres ProviderLess?]]></title>
            <link>https://hooninedev.com/es/240818</link>
            <guid isPermaLink="false">https://hooninedev.com/es/240818</guid>
            <pubDate>Sun, 18 Aug 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[En este artículo quiero explicar cómo consigue Zustand gestionar el estado sin un Provider. Mientras usaba Zustand, siempre había dado por sentado que podía gestionar el estado sin un Provider. Hasta ...]]></description>
            <content:encoded><![CDATA[<p>En este artículo quiero explicar cómo consigue Zustand gestionar el estado sin un Provider.</p>
<p>Mientras usaba Zustand, siempre había dado por sentado que podía gestionar el estado sin un Provider. Hasta que un día me surgió una pregunta. En la mayoría de las librerías del ecosistema React, envolver la aplicación con un Provider se ha convertido casi en un ritual. TanStack React Query exige envolverla con <code>QueryClientProvider</code> para poder usar <code>useQuery</code>, y overlay-kit de toss también exige <code>OverlayProvider</code> para poder llamar a <code>overlay.open()</code>. La Context API de React también requiere envolver el árbol de componentes con un Provider. Entonces, ¿qué clase de magia hace Zustand para no necesitar ese proceso?</p>
<p>Movido por la curiosidad, examiné directamente el código fuente de Zustand y encontré una estructura más interesante de lo que esperaba. En este artículo voy a ordenar lo que descubrí durante el proceso.</p>
<hr>
<h2 id="cómo-fluye-el-estado-en-react"><a class="anchor" href="#cómo-fluye-el-estado-en-react">Cómo fluye el estado en React</a></h2>
<p>En una aplicación React convencional, el estado funciona como se muestra en la siguiente imagen.</p>
<p><img src="/content/240818/3.png" alt="3.png" width="880" height="509" loading="eager" fetchpriority="high" decoding="async"></p>
<p>El estado interno de un componente se gestiona con los hooks de gestión de estado que ofrece React (<code>useState</code>, <code>useReducer</code>). Después, el estado se transmite a los componentes hijos mediante props. Hasta aquí, la historia es sencilla.</p>
<p>El problema aparece cuando hay que compartir estado entre componentes muy alejados. La solución oficial que ofrece React en este caso es la Context API, pero esta exige envolver el subárbol con un componente Provider.</p>
<hr>
<h3 id="por-qué-la-context-api-necesita-un-provider"><a class="anchor" href="#por-qué-la-context-api-necesita-un-provider">¿Por qué la Context API necesita un Provider?</a></h3>
<p>Para responder a esta pregunta, tenemos que observar brevemente el funcionamiento interno de React.</p>
<p>React gestiona el árbol de componentes mediante una estructura de datos interna llamada Fiber. Cada nodo Fiber está conectado mediante relaciones padre-hijo y, cuando cambia el valor de un Context, React recorre el árbol Fiber de arriba abajo, encuentra los componentes suscritos a ese Context y activa su rerenderizado.</p>
<p>La clave es esta: <strong>la propagación del valor de Context depende de la estructura del árbol Fiber.</strong> La posición del Provider en el árbol determina el alcance al que se transmite el valor, y el componente que llama a <code>useContext</code> asciende por su árbol Fiber para encontrar el Provider más cercano. ¿Y si no hay Provider? Simplemente se usa el valor predeterminado pasado a <code>createContext</code>.</p>
<p>Es decir, la Context API está estrechamente acoplada al sistema de renderizado de React. El almacenamiento, la propagación y la suscripción del estado ocurren dentro del árbol de componentes de React.</p>
<p>Entonces, ¿cómo evita Zustand esta estructura?</p>
<hr>
<h2 id="zustand-vive-fuera-de-react"><a class="anchor" href="#zustand-vive-fuera-de-react">Zustand vive fuera de React</a></h2>
<p><img src="/content/240818/4.png" alt="4.png" width="1300" height="393" loading="lazy" fetchpriority="low" decoding="async"></p>
<p>Zustand funciona sobre el patrón Flux. El <code>state</code> dentro del closure desempeña el papel de Store; las funciones definidas por el usuario, el de Actions; la función <code>set</code>, el de Dispatcher; y los componentes React, el de Views. Aquí aparece la diferencia decisiva.</p>
<p><strong>El Store de Zustand existe fuera del árbol de componentes de React, dentro del scope de un módulo JavaScript.</strong></p>
<p>Decir que está fuera del árbol de componentes significa que, a diferencia del estado interno de React, el estado de Zustand existe de forma independiente al árbol Fiber de React. Cualquier componente puede acceder al Store con solo hacer <code>import</code>, sin necesidad de envolver la aplicación en un Provider. (Es accesible desde cualquier lugar como una variable global, pero queda bien protegido dentro de un closure.)</p>
<p>¿Cómo es posible? Veamos el siguiente 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">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>En este código, <code>create</code> se llama cuando se carga el módulo. Es decir, el Store ya existe en memoria incluso antes de que React empiece a renderizar. Este es el patrón <strong>module-level singleton</strong>.</p>
<hr>
<h3 id="qué-es-un-module-level-singleton"><a class="anchor" href="#qué-es-un-module-level-singleton">¿Qué es un module-level singleton?</a></h3>
<p>El sistema de módulos ES de JavaScript <strong>evalúa cada módulo una sola vez y almacena el resultado en caché</strong>. A partir de ahí, cualquier <code>import</code> del mismo módulo devuelve el mismo objeto almacenado, en lugar de volver a ejecutarlo. Es decir, tanto si el componente A hace <code>import { useStore } from './store'</code> como si lo hace el componente B, ambos hacen referencia a <strong>exactamente la misma instancia del Store</strong>.</p>
<p>No hace falta implementar una clase singleton aparte ni vincular nada a una variable global (<code>window.store</code>). El propio sistema de módulos satisface de forma natural las condiciones de un singleton: «se crea una sola vez y desde cualquier lugar se accede a la misma instancia». Zustand aprovecha directamente esta garantía del lenguaje para que todos los componentes puedan compartir un único Store sin un Provider adicional.</p>
<p>Llegados a este punto, surge una pregunta de forma natural: ¿cómo es exactamente Zustand por dentro?</p>
<hr>
<h2 id="estructura-interna-de-zustand"><a class="anchor" href="#estructura-interna-de-zustand">Estructura interna de Zustand</a></h2>
<p>Al examinar el <a href="https://github.com/pmndrs/zustand/tree/main/src" target="_blank" rel="noopener noreferrer">repositorio de Zustand en GitHub</a>, sorprende lo concisa que es su lógica principal. Dos archivos concentran el núcleo: <code>vanilla.ts</code> contiene el Store propiamente dicho y <code>react.ts</code> se encarga de conectarlo con React.</p>
<hr>
<h3 id="vanillats"><a class="anchor" href="#vanillats">vanilla.ts</a></h3>
<p><a href="https://github.com/pmndrs/zustand/blob/main/src/vanilla.ts" target="_blank" rel="noopener noreferrer">vanilla.ts</a> es el corazón de Zustand. Todo lo relativo a cómo se crea el Store y cómo se gestiona el estado está contenido en este único archivo. Dicho de forma más sencilla, aquí se definen el estado encerrado en un closure y las funciones que lo manipulan.</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>Al analizar este código línea por línea, se revela el mecanismo central de Zustand.</p>
<ul>
<li>
<p><strong>Encapsulación del estado mediante un closure</strong></p>
<ul>
<li>
<p>La variable <code>let state: TState</code> se declara como variable local de la función <code>createStoreImpl</code>. Aunque la ejecución de la función termine, las funciones internas como <code>setState</code> y <code>getState</code> siguen haciendo referencia a esta variable, por lo que el recolector de basura no la elimina. Esa es la esencia de un closure.</p>
</li>
<li>
<p>Desde el exterior no existe ninguna forma de acceder directamente a la variable <code>state</code>. Solo se puede leer con <code>getState()</code> y escribir con <code>setState()</code>. (Es como implementar mediante un closure el campo private de la programación orientada a objetos.)</p>
</li>
</ul>
</li>
<li>
<p><strong>Detección de cambios con <code>Object.is</code></strong></p>
<ul>
<li>
<p>Después de calcular el nuevo estado, <code>setState</code> lo compara con el estado anterior mediante <code>Object.is(nextState, state)</code>. Si la referencia es la misma, no ocurre nada. Esta es la primera línea de defensa contra rerenderizados innecesarios.</p>
</li>
<li>
<p>Sin embargo, esta comparación con <code>Object.is</code> comprueba la <strong>igualdad estricta de referencias (strict reference equality)</strong>, así que hay un aspecto al que debe prestar atención quien lo usa. No hay problema cuando se extrae un único valor primitivo, como un número o una cadena.</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>Pero la situación cambia si el selector <strong>devuelve un objeto nuevo</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>El objeto <code>{ count, name }</code> obtiene una referencia nueva en cada llamada, aunque sus valores sean idénticos. Como <code>Object.is</code> no compara las propiedades internas, sino solo las referencias, Zustand considera que «el estado ha cambiado» y activa un rerenderizado cada vez.</p>
<p>Para resolver este problema, Zustand ofrece el 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 una por una las <strong>propiedades de primer nivel del objeto devuelto</strong> y solo provoca un rerenderizado cuando los valores cambian realmente. Es un enfoque parecido al de <code>useSelector</code> de Redux, que utiliza comparación por referencia de forma predeterminada, pero permite pasar <code>shallowEqual</code> como segundo argumento. (Eso sí, como indica su nombre, <code>useShallow</code> hace una comparación «superficial», por lo que no sigue el interior de objetos anidados.)</p>
</li>
</ul>
</li>
<li>
<p><strong>Sistema de listeners con el patrón Pub/Sub</strong></p>
<ul>
<li>La línea <code>const listeners: Set&#x3C;Listener> = new Set()</code> constituye todo el sistema de suscripción de Zustand. Cuando cambia el estado, <code>listeners.forEach</code> notifica a todos los suscriptores.</li>
<li>Al llamar a <code>subscribe</code>, el listener se añade al <code>Set</code>; al llamar a la función devuelta, se elimina del <code>Set</code>.</li>
<li>Este patrón es importante porque constituye un <strong>sistema de notificación completamente independiente del árbol Fiber de React</strong>. En lugar de que un Provider recorra el árbol buscando suscriptores, el propio Store administra directamente su lista de suscriptores.</li>
</ul>
</li>
<li>
<p><strong>Creación del estado inicial</strong></p>
<ul>
<li>
<p>Veamos la última línea que gestiona el 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>Esta línea condensa muchas cosas. En JavaScript, el operador de asignación (<code>=</code>) es una expresión (expression) que <strong>devuelve el propio valor asignado</strong>. Por tanto, primero se ejecuta <code>state = createState(...)</code> dentro de los paréntesis y se asigna el estado inicial a <code>state</code>; después, el valor devuelto vuelve a asignarse a <code>const initialState</code>. Como resultado, <code>state</code> e <code>initialState</code> <strong>hacen referencia al mismo objeto</strong>.</p>
<p>Pero ¿por qué guardar deliberadamente el mismo valor en dos variables? La clave es que las dos variables tienen funciones distintas.</p>
<ul>
<li><strong><code>state</code></strong> es una variable declarada con <code>let</code>. Cada vez que se llama a <code>setState</code>, se sustituye por un valor nuevo. Representa, por tanto, <strong>el estado vivo en el momento actual</strong>.</li>
<li><strong><code>initialState</code></strong> es una variable declarada con <code>const</code>. Conserva permanentemente el estado que existía cuando se creó el Store. Ninguna llamada posterior a <code>setState</code> modifica este valor. Es <strong>la primera snapshot del Store</strong>.</li>
</ul>
<p>Este <code>initialState</code> se expone al exterior mediante el método <code>getInitialState()</code> y se pasa en <code>react.ts</code> como <strong>tercer argumento de <code>useSyncExternalStore</code> (snapshot del 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>En un entorno de renderizado del lado del servidor (SSR) no existen las API del navegador ni la interacción del usuario, así que <code>setState</code> nunca llega a llamarse. Por eso, en el servidor siempre se utiliza <code>initialState</code> (= el estado inicial) como snapshot. Cuando empieza la hydration en el cliente, React compara el HTML renderizado en el servidor con el resultado del primer renderizado del cliente. Como ambos se han renderizado a partir del mismo <code>initialState</code>, se puede <strong>evitar un desajuste de hydration</strong>.</p>
</li>
</ul>
</li>
</ul>
<hr>
<h3 id="reactts"><a class="anchor" href="#reactts">react.ts</a></h3>
<p><a href="https://github.com/pmndrs/zustand/blob/main/src/react.ts" target="_blank" rel="noopener noreferrer">react.ts</a> se encarga de conectar el Store JavaScript puro que acabamos de crear con el sistema de renderizado de 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>La pieza central aquí es <code>useSyncExternalStore</code>. Este hook se introdujo en React 18 y fue diseñado para <strong>integrar de forma segura en el ciclo de renderizado de React un almacén de estado que existe fuera de React</strong>.</p>
<p>La estructura queda clara al observar los tres argumentos que recibe <code>useSyncExternalStore</code>. (Es casi lo mismo que vimos antes en vanilla.ts.)</p>
<ul>
<li><strong><code>api.subscribe</code></strong>: función que se suscribe a los cambios del Store. React la utiliza para pedir «avísame cuando cambie el estado».</li>
<li><strong><code>() => selector(api.getState())</code></strong>: devuelve la snapshot del estado actual. React llama a esta función en cada renderizado para obtener el estado más reciente.</li>
<li><strong><code>() => selector(api.getInitialState())</code></strong>: snapshot inicial que se usará durante el renderizado del lado del servidor. Evita discrepancias de estado entre el servidor y el cliente durante la hydration.</li>
</ul>
<p>En particular, <code>useSyncExternalStore</code> resuelve el <strong>problema de tearing</strong> que puede producirse en el modo concurrente de React (Concurrent Mode). El tearing ocurre cuando, dentro de una misma pasada de renderizado, componentes distintos muestran <strong>snapshots diferentes de la misma fuente de datos</strong>.</p>
<p>Resulta más fácil de entender con un escenario concreto. El componente A lee <code>store.value</code> (= 10) y empieza a renderizar. En ese momento, React <strong>pausa temporalmente (yield)</strong> el renderizado en modo concurrente y cede el control al navegador. Durante esa pausa llega un mensaje de WebSocket que cambia <code>store.value</code> a 11. Cuando React reanuda el renderizado, el componente B lee <code>store.value</code> (= 11). Como resultado, en el mismo frame A muestra 10 y B muestra 11, creando una <strong>UI desgarrada (teared)</strong>. Antes de React 18, el renderizado siempre era síncrono, por lo que este problema no se producía.</p>
<p><code>useSyncExternalStore</code> registra la snapshot existente al comenzar el renderizado (<code>getSnapshot</code>). Si el Store externo cambia durante el renderizado y la snapshot deja de coincidir, lo detecta y <strong>reinicia el renderizado desde el principio</strong>. Así garantiza que todos los componentes se rendericen a partir de la misma snapshot.</p>
<p>Por último, la función <code>createImpl</code> reúne todo esto.</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>Se crea un Store vanilla con <code>createStore</code>, se envuelve en un hook personalizado llamado <code>useBoundStore</code> y, mediante <code>Object.assign</code>, se adjuntan los métodos de la API del Store (<code>setState</code>, <code>getState</code>, <code>subscribe</code>, etc.) a la propia función hook. Como resultado, el <code>useBoundStore</code> devuelto posee una doble naturaleza: <strong>es un hook de React y, al mismo tiempo, la API del Store</strong>. (Un patrón muy propio de JavaScript: una función que también tiene métodos.)</p>
<hr>
<h2 id="qué-ocurre-con-otras-librerías-de-gestión-de-estado"><a class="anchor" href="#qué-ocurre-con-otras-librerías-de-gestión-de-estado">¿Qué ocurre con otras librerías de gestión de estado?</a></h2>
<p>Después de entender todo esto, es natural querer compararlo con otras librerías.</p>
<p>Existen muchas librerías de gestión de estado, como Jotai, Recoil, MobX, Xstate y Redux, pero me centraré en las que he utilizado personalmente.</p>
<blockquote>
<p>Como referencia, <strong>Recoil</strong> (Meta), que solía compararse a menudo con Jotai, archivó su repositorio en enero de 2025 y su desarrollo quedó, en la práctica, interrumpido. Tampoco llegó a incorporar compatibilidad con React 19. Si se busca un modelo de estado atómico, hoy Jotai puede considerarse la única opción realista.</p>
</blockquote>
<hr>
<h3 id="redux"><a class="anchor" href="#redux">Redux</a></h3>
<p>Redux también utiliza internamente un Store a nivel de módulo. Entonces, ¿por qué necesita un Provider?</p>
<p>El <code>&#x3C;Provider store={store}></code> de Redux <strong>inyecta (inject)</strong> la instancia del Store en el árbol de componentes mediante React Context. <code>useSelector</code> y <code>useDispatch</code> llaman internamente a <code>useContext</code> para acceder al Store ofrecido por el Provider. Lo importante aquí es que Redux no usa Context como <strong>canal de propagación del estado, sino como mecanismo de inyección de dependencias (Dependency Injection)</strong>. Lo que se transmite mediante Context no es el propio valor del estado, sino <strong>una referencia al objeto Store</strong> que administra ese estado. La suscripción y las actualizaciones reales del estado se procesan con el Pub/Sub interno del Store.</p>
<p>Las ventajas de este diseño son claras. Durante las pruebas, envolver una instancia distinta del Store con un Provider ofrece un aislamiento perfecto; además, una misma aplicación puede construir varios árboles de Store independientes mediante la prop <code>context</code>. Como subraya Mark Erikson, mantenedor de Redux, «Context es un mecanismo de transporte (transport mechanism), no una herramienta de gestión de estado».</p>
<hr>
<h3 id="jotai"><a class="anchor" href="#jotai">Jotai</a></h3>
<p>Jotai adopta un <strong>modelo de estado atómico (atomic)</strong> radicalmente distinto del de Redux o Zustand. En lugar de reunir todo el estado en un gran objeto Store, este enfoque <strong>separa cada fragmento de estado en un atom independiente</strong>. (La propia documentación oficial de Jotai explica que «si Zustand se parece a Redux, Jotai se parece a Recoil».)</p>
<p>La diferencia central de esta estructura está en <strong>cómo optimiza el renderizado</strong>. Zustand sigue un enfoque <strong>descendente (top-down)</strong> que extrae mediante un selector solo la parte necesaria de un único Store. El desarrollador debe escribir directamente un selector como <code>useStore((state) => state.count)</code> y, en ocasiones, necesita memoización para conservar la igualdad referencial (referential equality). Jotai, por el contrario, crea automáticamente un <strong>grafo de dependencias (dependency graph)</strong> entre atoms. Cuando cambia uno, propaga el cambio <strong>de abajo arriba (bottom-up)</strong> y rerenderiza exactamente los componentes que dependen de ese atom. Este seguimiento automático de dependencias resulta especialmente eficaz cuando decenas de estados están interrelacionados, como en una hoja de cálculo o un editor de canvas.</p>
<p>Desde el punto de vista del Provider, Jotai ocupa una posición intermedia interesante. De forma predeterminada utiliza un Store global y funciona sin Provider, pero, si hace falta, puede envolverse con <code>&#x3C;Provider></code> para crear un scope de Store aislado. Tomando prestadas las palabras de la documentación oficial de Jotai, Jotai es <strong>«context first, module second»</strong>, mientras que Zustand es <strong>«module first, context second»</strong>.</p>
<hr>
<h3 id="la-elección-de-zustand"><a class="anchor" href="#la-elección-de-zustand">La elección de Zustand</a></h3>
<p>Zustand tomó la decisión más radical. De forma predeterminada es un singleton a nivel de módulo y no tiene ningún Provider. Lo que aporta esta elección es una <strong>API extremadamente sencilla</strong>. Basta con crear el Store mediante <code>create</code> y llamar al hook desde el componente.</p>
<p>Sin embargo, decir que «no tiene ningún Provider» describe, para ser exactos, su <strong>diseño predeterminado</strong>. Desde v4 se puede implementar el patrón <strong>Scoped Store</strong> combinando <code>createStore</code> (un Store vanilla) con el <code>createContext</code> de React.</p>
<p>El <a href="https://tkdodo.eu/blog/zustand-and-react-context" target="_blank" rel="noopener noreferrer">blog de TkDodo, mantenedor de React Query</a>, analiza este patrón en profundidad. Su argumento principal es que un Store singleton global tiene tres limitaciones.</p>
<ul>
<li><strong>No se puede inicializar con props</strong>: como el Store se crea al cargar el módulo, no hay forma de usar como valor inicial los datos recibidos del servidor o las props del componente padre.</li>
<li><strong>El aislamiento de las pruebas es difícil</strong>: hay que restablecer manualmente el Store en cada prueba.</li>
<li><strong>No es reutilizable</strong>: si se renderizan en una página dos componentes que necesitan un Store con la misma estructura, ambos terminan compartiendo el estado.</li>
</ul>
<p>El patrón Scoped Store resuelve las tres limitaciones. La idea central es <strong>transmitir mediante Context la referencia a la instancia del Store, no el valor del estado</strong>. (Es exactamente la misma estructura que utiliza el Provider de Redux.)</p>
<p>La implementación concreta es la siguiente.</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>Ahora se pueden renderizar en una misma página tantos componentes multiselect independientes como se quiera.</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>Hay que destacar que lo que se transmite mediante Context <strong>no es el valor del estado, sino el objeto Store</strong>. Aunque cambie el valor del estado, el <code>value</code> de Context (= la referencia al Store) no cambia, por lo que <strong>no se producen rerenderizados innecesarios debidos a un cambio del valor de Context.</strong> El rerenderizado real se gestiona dentro de <code>useStore</code>, donde <code>useSyncExternalStore</code> aplica el selector. La función de transporte de Context queda perfectamente separada de la función de suscripción de Zustand.</p>
<p>TkDodo presentó un caso real en el que aplicó este patrón a un componente multiselect de un sistema de diseño. La estructura anterior, que gestionaba el estado interno con <code>useState</code> + Context, sufría una degradación del rendimiento con más de 50 elementos. El problema se resolvió al pasar a la suscripción basada en selectors de Zustand.</p>
<p>Después de que en v4 se eliminara el helper que v3 ofrecía mediante <code>zustand/context</code>, llamado <code>createContext</code>, este patrón se consolidó como la <strong>combinación directa del <code>createContext</code> nativo de React con <code>createStore</code>/<code>useStore</code> de Zustand</strong>. La API sigue igual en v5, y la <a href="https://github.com/pmndrs/zustand/blob/main/docs/previous-versions/zustand-v3-create-context.md" target="_blank" rel="noopener noreferrer">documentación oficial de Zustand</a> también presenta este patrón en la guía de migración a v4+.</p>
<hr>
<h2 id="la-sombra-de-providerless"><a class="anchor" href="#la-sombra-de-providerless">La sombra de ProviderLess</a></h2>
<p>Por supuesto, la ausencia de un Provider no solo ofrece ventajas. Voy a resumir los aspectos a los que, en mi opinión, conviene prestar atención.</p>
<hr>
<h3 id="el-problema-de-compartir-estado-en-ssr"><a class="anchor" href="#el-problema-de-compartir-estado-en-ssr">El problema de compartir estado en SSR</a></h3>
<p>Un singleton a nivel de módulo puede ser peligroso en un entorno de servidor. Un servidor Node.js procesa varias solicitudes en un único proceso, mientras que cada módulo solo se carga una vez dentro de ese proceso. Esto significa que las solicitudes de usuarios distintos podrían <strong>compartir la misma instancia del Store</strong>.</p>
<p>Por eso Zustand ofrece <code>getInitialState</code> y pasa una snapshot del servidor como tercer argumento de <code>useSyncExternalStore</code>. Sin embargo, esto por sí solo puede no aislar por completo el estado entre solicitudes. En entornos SSR se recomienda usar el patrón Scoped Store mencionado antes (<code>createStore</code> + React Context) para crear un Store nuevo en cada solicitud.</p>
<hr>
<h3 id="la-dificultad-de-aislar-las-pruebas"><a class="anchor" href="#la-dificultad-de-aislar-las-pruebas">La dificultad de aislar las pruebas</a></h3>
<p>En una librería basada en Provider, envolver cada prueba con un Provider distinto aísla el Store de forma natural. En cambio, el singleton a nivel de módulo de Zustand puede filtrar estado entre pruebas. Por eso hay que restablecer explícitamente el Store en el <code>beforeEach</code> de cada prueba. (Yo también sufrí este problema una vez.)</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>Aquí también el patrón Scoped Store sirve como solución. Si se envuelve con un Provider, cada prueba puede crear e inyectar un Store nuevo, lo que permite un aislamiento perfecto sin lógica de restablecimiento.</p>
<hr>
<h3 id="la-ausencia-de-múltiples-instancias"><a class="anchor" href="#la-ausencia-de-múltiples-instancias">La ausencia de múltiples instancias</a></h3>
<p>Si una aplicación necesita dos Stores independientes con la misma estructura, con el patrón Provider basta con envolver cada uno en un Provider diferente. Pero con un singleton a nivel de módulo hay que llamar por separado a la función de creación del Store para obtener instancias distintas. Por ejemplo, si una misma página contiene dos paneles de pestañas independientes y cada uno debe gestionar por separado su estado de selección, resulta difícil expresarlo de forma natural mediante un singleton global.</p>
<p>También en este caso, el patrón <code>createStore</code> + Context es la respuesta. Si cada componente de panel renderiza su propio Provider, se crean instancias totalmente independientes con la misma estructura de Store. La documentación oficial de Zustand recomienda este patrón cuando «un componente reutilizable necesita un Store».</p>
<h2 id="conclusión"><a class="anchor" href="#conclusión">Conclusión</a></h2>
<p>En resumen, el diseño ProviderLess de Zustand es posible gracias a la combinación de los cuatro mecanismos siguientes.</p>
<ul>
<li><strong>Singleton a nivel de módulo</strong>: el Store se crea fuera del árbol de componentes de React, dentro del scope de un módulo JavaScript.</li>
<li><strong>Encapsulación del estado mediante un closure</strong>: en <code>vanilla.ts</code>, dentro de <code>createStoreImpl</code>, la variable <code>state</code> y el Set <code>listeners</code> quedan encerrados en un closure e inaccesibles desde el exterior.</li>
<li><strong>Sistema Pub/Sub propio</strong>: en lugar de recorrer el árbol Fiber, gestiona directamente <code>Set&#x3C;Listener></code> para notificar los cambios de estado a los suscriptores.</li>
<li><strong>Integración con React mediante <code>useSyncExternalStore</code></strong>: sincroniza de forma segura los cambios de estado del Store externo con el ciclo de renderizado de React.</li>
</ul>
<p>Al final, la pregunta que plantea Zustand es esta: «¿Tiene el estado que vivir necesariamente dentro de React?». La respuesta de Zustand es clara. El estado puede estar fuera de React y solo hace falta tender un puente cuando sea necesario. Ese puente es <code>useSyncExternalStore</code>.</p>
<p>Por supuesto, este enfoque no es el mejor en todas las situaciones. En escenarios como SSR, aislamiento de pruebas o múltiples instancias, un diseño basado en Provider puede ser más adecuado. No hay una única respuesta correcta, pero entender los trade-offs de diseño que ha elegido cada librería permite escoger la herramienta adecuada para cada situación.</p>
<p>Recomiendo a quienes lean este artículo que abran alguna vez el código fuente de una de las librerías que utilizan. Es posible descubrir una profundidad que no aparece en la documentación oficial.</p>
<hr>
<p><img src="/content/240818/7.jpeg" alt="7.jpeg" width="216" height="233" loading="lazy" fetchpriority="low" decoding="async"></p>
<h3 id="ah-y-una-novedad"><a class="anchor" href="#ah-y-una-novedad">Ah, y una novedad</a></h3>
<p>Mientras investigaba lo anterior, descubrí que <strong>Zustand v5.0.0 se lanzó oficialmente en octubre de 2024</strong>.</p>
<p>Lo interesante es que v5 apenas incorpora funciones nuevas. Durante v4.x ya se habían añadido nuevas funciones y se habían marcado como deprecated varias API existentes, por lo que v5 tiene sobre todo el carácter de una <strong>versión de limpieza (cleanup)</strong>. Estos son los principales cambios. (Para más detalles, consulta la <strong><a href="https://github.com/pmndrs/zustand/releases" target="_blank" rel="noopener noreferrer">página de releases</a></strong> y la <strong><a href="https://zustand.docs.pmnd.rs/reference/migrations/migrating-to-v5" target="_blank" rel="noopener noreferrer">guía de migración</a></strong>.)</p>
<ul>
<li>Los requisitos mínimos aumentaron a <strong>React 18 y TypeScript 4.5 o superior</strong>.</li>
<li>Se eliminó <strong><code>getServerState</code></strong>. (Se sustituye por el tercer argumento de <code>useSyncExternalStore</code>.)</li>
<li>Se dejó de ofrecer <strong>compatibilidad con ES5</strong>.</li>
<li>Se eliminó la posibilidad de indicar una <strong>función equality personalizada</strong> en la función <code>create</code>.</li>
<li>Se mejoró la función <strong><code>shallow</code> para admitir objetos iterables</strong>.</li>
</ul>
<p>Al migrar de v4 a v5, se recomienda actualizar primero a la versión más reciente de v4. Esa versión muestra advertencias de deprecation; si se resuelven antes de pasar a v5, la transición puede realizarse sin dificultades.</p>
<hr>
<h3 id="referencias"><a class="anchor" href="#referencias">Referencias</a></h3>
<details class="ref-block"><summary class="ref-summary">참고 자료</summary><div class="ref-list">
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#3b82f6"></span><a href="https://react.dev/reference/react/useSyncExternalStore" target="_blank" rel="noopener noreferrer">React useSyncExternalStore</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#3b82f6"></span><a href="https://jotai.org/docs/basics/comparison" target="_blank" rel="noopener noreferrer">Jotai Comparison</a></div>
</div>
<div class="ref-item">
<div class="ref-item-inner"><span class="ref-dot" style="background:#f59e0b"></span><a href="https://interbolt.org/blog/react-ui-tearing/" target="_blank" rel="noopener noreferrer">InterBolt, Concurrent React, External Stores, and Tearing</a></div>
</div>
</div></details>]]></content:encoded>
            <author>jihoon7705@gmail.com (이지훈)</author>
            <category>프론트엔드</category>
            <category>React</category>
        </item>
        <item>
            <title><![CDATA[Cómo funcionan los algoritmos de compresión]]></title>
            <link>https://hooninedev.com/es/240706</link>
            <guid isPermaLink="false">https://hooninedev.com/es/240706</guid>
            <pubDate>Sat, 06 Jul 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[En este artículo quiero hablar sobre los algoritmos de compresión de software. Me encargaron mejorar el proceso de despliegue de un proyecto interno. La arquitectura exigía subir a S3 artefactos de co...]]></description>
            <content:encoded><![CDATA[<p>En este artículo quiero hablar sobre los algoritmos de compresión de software.</p>
<p>Me encargaron mejorar el proceso de despliegue de un proyecto interno. La arquitectura exigía subir a S3 artefactos de compilación muy grandes, y pude comprobar que el tamaño de la carpeta de build afectaba directamente al tiempo de subida y al coste de almacenamiento. De ahí surgió una pregunta natural: ¿cómo podíamos comprimir y subir esos archivos de forma más eficiente?</p>
<p>Al investigar descubrí muchos más formatos de los que esperaba: zip, gzip, zstd, bzip2, xz y otros. Sus nombres se parecían, pero no era fácil encontrar una explicación que aclarara sus diferencias y cuándo convenía usar cada uno. (Pensaba que toda compresión era más o menos igual, pero el mundo es grande y las formas de reducir archivos también.)</p>
<p>Así que aproveché la ocasión para comparar los principios y características de cada formato y explicar por qué terminé eligiendo uno concreto.</p>
<hr>
<h2 id="qué-es-la-compresión-sin-pérdida"><a class="anchor" href="#qué-es-la-compresión-sin-pérdida">¿Qué es la compresión sin pérdida?</a></h2>
<p>La compresión sin pérdida permite reconstruir los datos originales a la perfección. A diferencia de la compresión con pérdida, habitual en imágenes y audio, el resultado descomprimido no difiere del original ni en un solo bit. Cuando la integridad es esencial, como ocurre con el código fuente o los artefactos de compilación, hay que utilizar compresión sin pérdida.</p>
<p>Su idea central consiste en <strong>aprovechar la redundancia estadística presente en los datos</strong>. Si sustituimos patrones repetidos por representaciones más cortas, reducimos el tamaño total.</p>
<p>Entre estas técnicas, los métodos <strong>basados en diccionarios (Dictionary-Based)</strong> forman una de las familias más extendidas. Aquí “diccionario” no significa un libro de definiciones, sino una tabla de consulta que asocia fragmentos vistos anteriormente con códigos breves. <strong>LZ77</strong>, propuesto por Abraham Lempel y Jacob Ziv en el artículo de 1977 <em>"A Universal Algorithm for Sequential Data Compression"</em> de IEEE Transactions on Information Theory, y <strong>LZ78</strong>, publicado un año después, son los antepasados de esta familia. “LZ” toma una letra de cada apellido. Casi todos los algoritmos posteriores basados en diccionarios, como DEFLATE, LZMA, LZ4 y Zstd, descienden de ellos. (No es exagerado decir que la mayor parte del árbol genealógico de la compresión converge en estos dos investigadores.)</p>
<p>Pensemos en un ejemplo sencillo. Si la palabra “Linux” aparece cien veces en un texto, podemos registrarla en el diccionario la primera vez y reemplazar las siguientes por una referencia corta que signifique “entrada número 1”. “Linux” ocupa cinco bytes, mientras que el puntero puede expresarse con menos, por lo que el conjunto se hace más pequeño.</p>
<p>Entonces, ¿en qué se diferencian exactamente LZ77 y LZ78?</p>
<hr>
<h3 id="lz77-el-método-de-la-ventana-deslizante"><a class="anchor" href="#lz77-el-método-de-la-ventana-deslizante">LZ77: el método de la ventana deslizante</a></h3>
<p>LZ77 <strong>no crea un diccionario explícito independiente</strong>. Usa una región del propio flujo de entrada como diccionario. Esa región se llama <strong>ventana deslizante</strong> porque avanza conforme se procesan los datos. (Es el mismo concepto que aparece a menudo en ejercicios de algoritmos.)</p>
<p>La ventana se divide en dos zonas.</p>
<ul>
<li><strong>Búfer de búsqueda (Search Buffer)</strong>: datos ya procesados. Cumple el papel de diccionario.</li>
<li><strong>Búfer de anticipación (Look-ahead Buffer)</strong>: datos aún no procesados que se comprimirán a continuación.</li>
</ul>
<p>El algoritmo busca si el comienzo del búfer de anticipación ya apareció en alguna parte del búfer de búsqueda. Si encuentra el mismo patrón, codifica la coincidencia como una tupla <strong>(distancia, longitud, carácter siguiente)</strong>. La distancia indica cuántos caracteres hay que retroceder para llegar al inicio de la coincidencia y la longitud, cuántos caracteres abarca.</p>
<p>Supongamos que comprimimos la cadena <code>"banana_banana"</code> con LZ77. Al llegar al segundo <code>"banana"</code>, el algoritmo dice en la práctica: <em>“Retrocede siete caracteres y copia seis.”</em> De ese modo, una cadena de seis bytes queda representada por solo dos números.</p>
<p>La clave es que <strong>no hace falta guardar ni transmitir el diccionario por separado</strong>. El decodificador reconstruye el búfer de búsqueda mientras descomprime, de modo que el diccionario queda implícito en los propios datos. A cambio, la descompresión siempre debe avanzar secuencialmente desde el principio. En términos del algoritmo, no puede empezar en un punto arbitrario del archivo.</p>
<p>El tamaño de la ventana mantiene una relación directa de compromiso con la tasa de compresión. Una ventana mayor puede referirse a patrones más lejanos y suele comprimir mejor, pero también aumenta el trabajo de búsqueda y el uso de memoria.</p>
<hr>
<h3 id="lz78-un-diccionario-explícito"><a class="anchor" href="#lz78-un-diccionario-explícito">LZ78: un diccionario explícito</a></h3>
<p>A diferencia de LZ77, LZ78 <strong>construye un diccionario explícito</strong> durante la compresión. No utiliza una ventana deslizante. Guarda los patrones observados como entradas indexadas y sustituye las repeticiones posteriores por sus índices.</p>
<p>LZ78 produce etiquetas con la forma <strong>(índice del diccionario, carácter siguiente)</strong>. El codificador busca la entrada más larga que coincida, emite su índice junto al carácter que rompe la coincidencia y añade <em>“la entrada coincidente más el nuevo carácter”</em> como otra entrada. El diccionario crece gradualmente durante el proceso.</p>
<p>La variante más conocida de LZ78 es <strong>LZW</strong> (Lempel-Ziv-Welch). Terry Welch publicó esta mejora en 1984, y se utilizó en el formato GIF y en la utilidad Unix <code>compress</code>, cuya extensión es <code>.Z</code>. (LZW llegó a estar en el centro de una disputa de patentes, episodio que contribuyó al nacimiento de PNG.)</p>
<hr>
<h3 id="de-cuál-de-las-dos-familias-descienden-los-algoritmos-modernos"><a class="anchor" href="#de-cuál-de-las-dos-familias-descienden-los-algoritmos-modernos">¿De cuál de las dos familias descienden los algoritmos modernos?</a></h3>
<p>Curiosamente, casi todos los algoritmos de compresión dominantes hoy son <strong>descendientes de LZ77</strong>.</p>
<p><strong>LZSS</strong>, publicado por Storer y Szymanski en 1982, mejoró LZ77 mediante un indicador de un bit que distingue si cada salida es un literal, es decir, un carácter original, o un par longitud-distancia. Cuando una coincidencia es tan corta que la referencia sale más cara, el codificador conserva el carácter original.</p>
<p>En 1993, Phil Katz combinó LZSS con la <strong>codificación Huffman</strong>, que asigna secuencias de bits más cortas a los símbolos frecuentes, y creó <strong>DEFLATE</strong>. ZIP, GZIP y PNG usan DEFLATE. Por tanto, los archivos <code>.zip</code>, <code>.gz</code> y <code>.png</code> que manejamos a diario son descendientes directos de LZ77.</p>
<p>Algoritmos posteriores como <strong>LZMA</strong> (7-Zip y XZ), <strong>LZ4</strong> y <strong>Zstd</strong> también parten de la ventana deslizante de LZ77 y evolucionan las estructuras de búsqueda de coincidencias y los métodos de codificación entrópica. La familia LZ78, en cambio, prácticamente abandonó la escena principal después de LZW.</p>
<p>Se ha demostrado que ambos algoritmos tienen una capacidad teórica equivalente <em>cuando se descomprime el conjunto completo de datos</em>. Aun así, LZ77 sobrevivió porque <strong>integrar el diccionario en los datos ofrecía más flexibilidad de implementación y extensión</strong>. El tamaño de la ventana, los algoritmos de búsqueda y el codificador entrópico posterior podían combinarse con libertad, dejando margen para evolucionar según las necesidades de cada época.</p>
<p>El rendimiento de compresión se evalúa en dos ejes: la <strong>tasa de compresión</strong>, cuánto se reduce el archivo, y la <strong>velocidad de compresión</strong>, cuánto tarda el proceso. Perseguir una tasa más alta suele requerir más cómputo y más tiempo. La estrategia práctica consiste en encontrar el punto adecuado entre ambos.</p>
<p>Con esta base, comparemos los distintos formatos uno por uno.</p>
<hr>
<h2 id="zip"><a class="anchor" href="#zip">ZIP</a></h2>
<p>ZIP es un formato creado por Phil Katz en 1989. En su interior suele usar <strong>DEFLATE</strong>, que combina LZ77 y Huffman coding. La distinción importante es que ZIP no es un algoritmo de compresión, sino un formato contenedor que almacena datos comprimidos mediante algoritmos como DEFLATE.</p>
<p>ZIP <strong>comprime cada archivo por separado</strong>. Es lo que se conoce como archivo no sólido (Non-solid Archive), y permite extraer un archivo concreto sin descomprimir los demás. Como contrapartida, no aprovecha datos repetidos entre archivos, por lo que puede obtener una tasa inferior a tar.gz, que veremos más adelante.</p>
<p>Windows, macOS, Linux y la mayoría de los sistemas operativos lo admiten sin instalar software adicional. Por eso es una opción segura cuando importa la compatibilidad entre plataformas.</p>
<hr>
<h2 id="gzip-gnu-zip"><a class="anchor" href="#gzip-gnu-zip">GZIP (GNU Zip)</a></h2>
<p>Al igual que ZIP, GZIP utiliza <strong>DEFLATE</strong> internamente. ¿Por qué existe otro formato si el algoritmo es el mismo? ZIP también actúa como contenedor para varios archivos, mientras que GZIP está especializado en comprimir <strong>un único archivo o flujo</strong>.</p>
<p>Para comprimir varios archivos o un directorio con GZIP, primero se reúnen en un archivo TAR y después se comprime ese archivo con GZIP. Este proceso de dos pasos genera un <code>.tar.gz</code> o <code>.tgz</code>.</p>
<p>La estructura de GZIP, especificada en RFC 1952, es bastante sencilla: una <strong>cabecera fija de 10 bytes</strong>, una cabecera extendida opcional con datos como el nombre original o comentarios, los datos DEFLATE y un <strong>tráiler de 8 bytes</strong> con la suma CRC-32 y el tamaño original. CRC-32 permite comprobar que los datos descomprimidos coinciden con el original. GZIP es, por tanto, una envoltura ligera alrededor de un flujo DEFLATE.</p>
<p>DEFLATE usa una ventana deslizante de <strong>hasta 32 KB</strong>. Ese tamaño limita la tasa de compresión porque no se pueden referenciar patrones separados por más de 32 KB. GZIP también ofrece niveles del 1 al 9. El nivel 1 es rápido, pero obtiene una tasa menor, alrededor del 60%; el nivel 9 es lento, pero alcanza aproximadamente el 75%. El valor predeterminado, 6, busca el equilibrio entre velocidad y tamaño.</p>
<p>En entornos Unix y Linux se utiliza como un estándar para distribuir código fuente, comprimir registros y empaquetar software. También sigue siendo una opción habitual para compresión HTTP mediante <code>Content-Encoding: gzip</code>, aunque Brotli lo está sustituyendo progresivamente en ese terreno.</p>
<hr>
<h2 id="zstd-zstandard"><a class="anchor" href="#zstd-zstandard">ZSTD (Zstandard)</a></h2>
<p>ZSTD es un algoritmo desarrollado por Yann Collet en Meta, antes Facebook, y publicado como código abierto en 2016. Su gran ventaja es que <strong>comprime y descomprime mucho más rápido, manteniendo una tasa comparable a GZIP</strong>.</p>
<p>Su funcionamiento interno tiene tres grandes etapas. Primero, un <strong>buscador de coincidencias (Match Finder)</strong> de la familia LZ77 detecta patrones repetidos. Después codifica los literales, las longitudes y los desplazamientos encontrados como <strong>secuencias</strong>. Por último, comprime esas secuencias mediante <strong>codificación entrópica</strong>. En lugar de depender únicamente de Huffman como GZIP, utiliza <strong>FSE (Finite State Entropy)</strong>, un codificador basado en ANS (Asymmetric Numeral Systems) que combina propiedades de Huffman y de la codificación aritmética (Arithmetic Coding). Huffman solo puede asignar un número entero de bits por símbolo; FSE puede representar probabilidades equivalentes a bits fraccionarios y acercarse más al límite teórico. (A pesar del nombre, la idea esencial es expresar los mismos datos con menos bits de una manera más inteligente.)</p>
<p>El buscador también cambia de estrategia según el nivel. Los niveles bajos, del 1 al 4, usan tablas hash sencillas para ganar velocidad. Los intermedios, del 5 al 12, comparan varios candidatos mediante una estrategia Lazy. Los altos, del 13 al 22, recurren a árboles binarios y programación dinámica para encontrar coincidencias casi óptimas. Esta escala permite usar niveles bajos para transmisión en tiempo real y altos para archivo.</p>
<p>En el benchmark Silesia Corpus, ZSTD en su nivel predeterminado 3 comprime a unos 300 MB/s y descomprime a unos 1.200 MB/s. GZIP en el nivel 6 alcanza apenas 34 MB/s y 380 MB/s, respectivamente. <strong>ZSTD comprime unas ocho veces más rápido y descomprime unas tres, mientras obtiene una tasa ligeramente superior: 3,17 frente a 3,09 de GZIP.</strong> Estas cifras muestran de forma directa cómo mejora el compromiso tradicional.</p>
<p>Su adopción crece con rapidez. Se usa para comprimir módulos del kernel de Linux y de forma transparente en sistemas de archivos; distribuciones como Arch Linux, Fedora, Debian y Ubuntu lo han adoptado para sus paquetes. Desde la versión 1.5.7, publicada en febrero de 2025, la <strong>compresión multihilo está activada de forma predeterminada</strong> con hasta cuatro hilos, ampliando aún más la diferencia práctica frente al GZIP monohilo. AWS también ha explicado que redujo alrededor de un 30% el almacenamiento en S3 al migrar servicios internos de gzip a zstd.</p>
<hr>
<h2 id="bzip2"><a class="anchor" href="#bzip2">BZIP2</a></h2>
<p>BZIP2 comprime mediante una cadena de transformaciones.</p>
<ol>
<li><strong>RLE (Run-Length Encoding)</strong>: reduce las repeticiones consecutivas de los datos iniciales</li>
<li><strong>BWT (Burrows-Wheeler Transform)</strong>: reorganiza los datos para facilitar su compresión</li>
<li><strong>MTF (Move-to-Front Transform)</strong>: convierte la salida de BWT en una secuencia numérica</li>
<li><strong>RLE</strong>: vuelve a reducir las repeticiones del resultado de MTF</li>
<li><strong>Huffman Coding</strong>: aplica finalmente una codificación basada en frecuencias</li>
</ol>
<p>BZIP2 proporciona una tasa mayor que GZIP, pero tanto la compresión como la descompresión son más lentas. Se ha usado para archivo cuando el tamaño importaba más que la velocidad.</p>
<p>Su última versión fue la 1.0.8, publicada en 2019, y el desarrollo activo prácticamente se ha detenido. A medida que los benchmarks muestran que ZSTD supera a BZIP2 en tasa y velocidad, los proyectos nuevos tienden a elegir ZSTD.</p>
<hr>
<h2 id="xz"><a class="anchor" href="#xz">XZ</a></h2>
<p>XZ es un formato que usa <strong>LZMA2</strong>. LZMA, Lempel-Ziv-Markov chain Algorithm, fue desarrollado por Igor Pavlov y combina compresión por diccionario basada en LZ77 con codificación por rangos (Range Encoding). Más que una simple “versión mejorada de LZMA”, LZMA2 se parece a un <strong>formato contenedor</strong> para flujos LZMA. Añade compresión y descompresión multihilo y un tratamiento eficiente de los datos que no se pueden comprimir.</p>
<p>De todos los formatos tratados aquí, XZ ofrece <strong>la tasa de compresión más alta</strong>. A cambio, comprime muy despacio y consume mucha memoria. Resulta adecuado para archivo cuando ahorrar espacio es la prioridad absoluta.</p>
<p>En marzo de 2024, sin embargo, <strong>se descubrió una puerta trasera en xz-utils, la biblioteca central de XZ, durante el grave incidente de cadena de suministro CVE-2024-3094</strong>. Una campaña de ingeniería social de dos años había obtenido permisos de mantenimiento, y la vulnerabilidad recibió la puntuación máxima CVSS 10.0. Las principales distribuciones revirtieron inmediatamente a versiones seguras, pero el caso fue una advertencia contundente sobre la seguridad de la cadena de suministro de código abierto. (El valor técnico de XZ permanece, aunque conviene conocer este contexto al elegir herramientas.)</p>
<hr>
<h2 id="tar"><a class="anchor" href="#tar">TAR</a></h2>
<p>TAR, Tape Archive, no es un algoritmo de compresión. Es una herramienta y un formato que <strong>reúne varios archivos y directorios en un solo archivo</strong>. Se creó originalmente para copias en cinta magnética. Dado que la cinta es un medio secuencial, concatenar los datos de forma continua era una estructura natural.</p>
<p>Su organización interna es sorprendentemente sencilla. Todo se procesa en <strong>bloques de 512 bytes</strong>. Antes de cada archivo aparece una cabecera de 512 bytes con metadatos como el nombre, hasta 100 bytes, el modo, UID/GID del propietario, tamaño, fecha de modificación y suma de comprobación. Los datos siguen a la cabecera y se rellenan hasta un múltiplo de 512 bytes. Dos bloques de ceros de 512 bytes indican el final. La mayoría de las implementaciones modernas siguen <strong>UStar (Unix Standard TAR)</strong>, definido por POSIX, que admite nombres de hasta 256 bytes y campos adicionales.</p>
<p>La propiedad clave es que TAR conserva <strong>los metadatos del sistema de archivos Unix</strong>, como permisos, propietarios, marcas de tiempo y enlaces simbólicos. ZIP no siempre conserva perfectamente estos datos específicos de Unix, por lo que TAR suele encajar mejor en despliegues de servidor.</p>
<p>TAR no reduce el tamaño por sí mismo; las cabeceras y el relleno pueden incluso aumentarlo ligeramente. La compresión real se consigue al combinarlo con GZIP, BZIP2, XZ o ZSTD. De ahí proceden extensiones como <code>.tar.gz</code>, <code>.tar.bz2</code>, <code>.tar.xz</code> y <code>.tar.zst</code>. TAR se encarga de “agrupar” y la otra herramienta de “reducir”, un ejemplo clásico de la filosofía Unix de “hacer bien una sola cosa”.</p>
<p>Es el método estándar de archivado en Unix/Linux, mientras que Windows puede necesitar software adicional como 7-Zip.</p>
<hr>
<h2 id="una-breve-mirada-a-brotli"><a class="anchor" href="#una-breve-mirada-a-brotli">Una breve mirada a Brotli</a></h2>
<p>Quienes desarrollan frontend también deberían conocer <strong>Brotli</strong>. Google creó este algoritmo y en 2015 se estandarizó para compresión de flujos HTTP como <code>Content-Encoding: br</code>.</p>
<p>Todos los navegadores principales lo admiten sobre HTTPS, con una cobertura global superior al 96%, y suele ofrecer <strong>entre un 15 y un 25% más de compresión que GZIP</strong>. Resulta especialmente eficaz con archivos estáticos de texto como JavaScript, CSS y HTML. Grandes CDN como Cloudflare lo usan de forma predeterminada, y la práctica moderna se resume en “Brotli primero, GZIP como alternativa”.</p>
<p>Si los artefactos se suben a S3 y se sirven mediante una CDN, precomprimir los estáticos con Brotli puede reducir notablemente la transferencia de red. (En aquel momento no tenía suficiente evidencia específica del proyecto para introducirlo de inmediato, pero sigue siendo una opción que merece conocerse y revisarse.)</p>
<hr>
<h2 id="por-qué-targz-comprime-mejor-que-zip"><a class="anchor" href="#por-qué-targz-comprime-mejor-que-zip">Por qué tar.gz comprime mejor que ZIP</a></h2>
<p>La razón está en la diferencia entre <strong>archivos sólidos (Solid Archive)</strong> y <strong>no sólidos (Non-solid Archive)</strong>.</p>
<p>Con tar.gz, TAR reúne todos los archivos en un flujo continuo y GZIP comprime el flujo entero de una vez. Así puede reconocer y aprovechar <strong>datos repetidos entre archivos</strong>. Eso es un archivo sólido. Si una carpeta de build contiene decenas de bundles JavaScript parecidos, un patrón del archivo A puede referenciarse cuando reaparece en B. También disminuye la sobrecarga porque no hay que registrar una cabecera, una suma y una tabla de contenidos independientes para cada flujo comprimido.</p>
<p>ZIP es no sólido y comprime cada archivo por separado, de modo que no aprovecha redundancias entre ellos. Aunque A y B contengan el mismo bloque de código, sus flujos DEFLATE no conocen la existencia del otro. Por eso tar.gz suele obtener una tasa entre un 5 y un 15% mejor que ZIP. La diferencia crece cuando el artefacto contiene muchos archivos de estructura similar.</p>
<p>Los archivos sólidos también tienen desventajas claras.</p>
<ul>
<li>Para extraer un solo archivo puede ser necesario <strong>descomprimir primero todos los datos que lo preceden</strong>. Como todo forma un único flujo, no se puede saltar directamente a un punto intermedio. ZIP permite acceso aleatorio a cada archivo y puede ser mejor cuando se extraen elementos concretos con frecuencia.</li>
<li>Si una parte se daña, <strong>todos los datos posteriores al punto dañado pueden quedar irrecuperables</strong>. En un formato no sólido a veces se pierde solo el archivo afectado y se conserva el resto.</li>
</ul>
<hr>
<p><strong>Añadido en 2026</strong></p>
<h2 id="en-2024-elegí-targz-qué-elegiría-ahora"><a class="anchor" href="#en-2024-elegí-targz-qué-elegiría-ahora">En 2024 elegí tar.gz. ¿Qué elegiría ahora?</a></h2>
<p>Entonces escogí tar.gz por su compatibilidad y estabilidad. Tras subir el artefacto a S3 había que descomprimirlo en distintos entornos, así que un formato disponible casi en cualquier lugar era la opción segura.</p>
<p>Si afrontara hoy la misma situación, consideraría seriamente <strong>tar.zst (TAR + ZSTD)</strong>. Recordemos las cifras anteriores.</p>
<p>GZIP comprime a 34 MB/s en su nivel predeterminado y ZSTD a 300 MB/s. Para una carpeta de 2 GB, un cálculo simple da unos 60 segundos con GZIP y siete con ZSTD. Si además contamos el multihilo activado por defecto desde ZSTD v1.5.7, con hasta cuatro hilos, la diferencia real puede ser mayor. En una canalización CI/CD esos segundos se acumulan en cada despliegue.</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="sh" data-theme="github-dark github-light"><code data-language="sh" data-theme="github-dark github-light" style="display: grid;"><span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D"># tar.zst 생성 (멀티스레드 자동 활용)</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">tar</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> --zstd</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> -cf</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> archive.tar.zst</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> directory/</span></span>
<span data-line=""> </span>
<span data-line=""><span style="--shiki-dark:#6A737D;--shiki-light:#6A737D"># 또는 압축 레벨 지정 (-T0은 사용 가능한 모든 코어 활용)</span></span>
<span data-line=""><span style="--shiki-dark:#B392F0;--shiki-light:#6F42C1">tar</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> -cf</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> archive.tar.zst</span><span style="--shiki-dark:#79B8FF;--shiki-light:#005CC5"> -I</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> 'zstd -3 -T0'</span><span style="--shiki-dark:#9ECBFF;--shiki-light:#032F62"> directory/</span></span></code></pre></figure>
<p>ZSTD también iguala o mejora la tasa de GZIP, por lo que prácticamente desaparece el compromiso de aceptar un archivo mayor para ganar velocidad. Es más rápido y produce un resultado más pequeño.</p>
<p>Aun así, hay que confirmar que el entorno receptor pueda descomprimir zstd. Las principales distribuciones Linux ya lo incluyen y en macOS se instala fácilmente con Homebrew mediante <code>brew install zstd</code>. Los sistemas antiguos o mínimos quizá necesiten una instalación adicional, por lo que conviene revisar de antemano todos los entornos del equipo. Si la compatibilidad es la prioridad absoluta, tar.gz sigue siendo la alternativa más segura.</p>
<hr>
<h2 id="comparativa-rápida"><a class="anchor" href="#comparativa-rápida">Comparativa rápida</a></h2>
<table>
<thead>
<tr>
<th>Formato</th>
<th>Algoritmo</th>
<th>Tasa</th>
<th>Velocidad</th>
<th>Características principales</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>ZIP</strong></td>
<td>DEFLATE</td>
<td>Media</td>
<td>Rápida</td>
<td>Multiplataforma, no sólido</td>
</tr>
<tr>
<td><strong>GZIP</strong></td>
<td>DEFLATE</td>
<td>Media</td>
<td>Rápida</td>
<td>Flujo único, combinado con TAR</td>
</tr>
<tr>
<td><strong>ZSTD</strong></td>
<td>Zstandard</td>
<td>Alta</td>
<td>Muy rápida</td>
<td>Niveles ajustables, estándar moderno</td>
</tr>
<tr>
<td><strong>BZIP2</strong></td>
<td>BWT+MTF+Huffman</td>
<td>Alta</td>
<td>Lenta</td>
<td>Desarrollo prácticamente detenido</td>
</tr>
<tr>
<td><strong>XZ</strong></td>
<td>LZMA2</td>
<td>Muy alta</td>
<td>Muy lenta</td>
<td>Máxima tasa, contexto de seguridad</td>
</tr>
<tr>
<td><strong>Brotli</strong></td>
<td>Brotli</td>
<td>Alta</td>
<td>Media</td>
<td>Especializado en la web</td>
</tr>
</tbody>
</table>
<hr>
<h2 id="conclusión"><a class="anchor" href="#conclusión">Conclusión</a></h2>
<p>Antes de profundizar en la compresión pensaba sinceramente: “¿No basta con meterlo todo en un zip?”. Trabajar con una carpeta de build de más de 2 GB hizo tangible que el algoritmo elegido puede cambiar de forma significativa el tiempo de subida y el coste.</p>
<p>Cada formato tiene su propia filosofía y sus compromisos: la compatibilidad de ZIP, la universalidad de GZIP, la velocidad de ZSTD y la tasa de XZ. No existe una opción “mejor” en todos los casos; la decisión adecuada depende del contexto del proyecto.</p>
<p>Entender los principios de las herramientas que usamos sin pensar nos ayuda a decidir mejor cuando aparece un problema parecido. Espero que este artículo sirva como una pequeña referencia para quien tenga que elegir un algoritmo de compresión.</p>]]></content:encoded>
            <author>jihoon7705@gmail.com (이지훈)</author>
            <category>소박한궁금증</category>
            <category>소프트웨어</category>
        </item>
    </channel>
</rss>