<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>tapk.log</title>
        <link>https://velog.io/</link>
        <description>누구나 읽기 편한 글을 위해</description>
        <lastBuildDate>Wed, 08 Apr 2026 00:17:32 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>tapk.log</title>
            <url>https://velog.velcdn.com/images/tap_kim/profile/ecfe3d3a-6af2-4ebe-8213-c9f4d6810eb6/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. tapk.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/tap_kim" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[(번역) 잘못된 Pretext 데모를 보고 있습니다]]></title>
            <link>https://velog.io/@tap_kim/youre-looking-at-the-wrong-pretext-demo</link>
            <guid>https://velog.io/@tap_kim/youre-looking-at-the-wrong-pretext-demo</guid>
            <pubDate>Wed, 08 Apr 2026 00:17:32 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>원문: <a href="https://denodell.com/blog/youre-looking-at-the-wrong-pretext-demo">https://denodell.com/blog/youre-looking-at-the-wrong-pretext-demo</a></p>
</blockquote>
<p>Cheng Lou가 만든 새로운 자바스크립트 라이브러리 <a href="https://github.com/chenglou/pretext">Pretext</a>는 출시 3일 만에 GitHub 스타 7,000개를 넘겼습니다. 그 기간 동안 프런트엔드 엔지니어링 커뮤니티 근처에 있었다면 이런 데모를 보셨을 겁니다. <a href="https://pretext-playground.builderz.dev">물처럼 텍스트를 가르는 용</a>, <a href="https://somnai-dreams.github.io/pretext-demos/fluid-smoke.html">타이포그래피 ASCII로 렌더링된 유체 연기</a>, <a href="https://somnai-dreams.github.io/pretext-demos/wireframe-torus.html">문자 격자로 그린 와이어프레임 토러스</a>, <a href="https://somnai-dreams.github.io/pretext-demos/the-editorial-engine.html">60fps로 텍스트를 밀어내는 애니메이션 오브가 있는 다단 편집 레이아웃</a>. 시각적으로 놀라운 데모이고, 라이브러리가 바이럴된 이유이기도 합니다.</p>
<p>하지만 이 라이브러리가 중요한 이유는 따로 있습니다.</p>
<hr>
<p>Pretext가 하는 진짜 중요한 일은 DOM을 읽지 않고도 텍스트 블록의 높이를 예측하는 것입니다. 레이아웃 재계산을 단 한 번도 트리거하지 않고 텍스트 노드를 배치할 수 있다는 뜻입니다. 텍스트는 DOM에 남아 있으므로 스크린 리더가 읽을 수 있고, 사용자가 선택하고, 복사하고, 번역할 수 있습니다. 접근성(accessibility) 트리는 온전하게 유지되고, 실제로 성능은 향상되며, 모든 사용자의 경험이 보존됩니다. 프로덕션 웹 애플리케이션이 텍스트를 다루는 방식을 바꿀 기능이며, 거의 아무도 시연하지 않는 기능이기도 합니다.</p>
<p>커뮤니티는 사흘 동안 용을 만드는 데 시간을 쏟았습니다. 사실은 채팅 인터페이스를 개발해야 했을 텐데요. 용은 화제가 된 반면 측정 엔진은 아무도 눈여겨보지 않았다는 사실은, 프런트엔드 커뮤니티가 도구를 평가하는 방식에 대해 중요한 시사점을 던져줍니다. 즉, 우리는 눈에 보이는 것에 최적화할 뿐, 우리가 만든 도구를 사용하는 사람들에게 가장 중요한 요소에는 최적화하지 않는다는 것입니다.</p>
<h2 id="pretext가-해결하는-문제">Pretext가 해결하는 문제</h2>
<p>문제는 강제 레이아웃 재계산으로, 브라우저가 작업을 계속하기 전에 일시 정지하여 페이지 레이아웃을 다시 측정해야 한다는 점입니다. UI 컴포넌트가 텍스트 블록의 높이를 알아야 할 때, 표준적인 접근 방식은 DOM에서 측정하는 것입니다. <code>getBoundingClientRect()</code>를 호출하거나 <code>offsetHeight</code>를 읽으면 브라우저가 동기적으로 레이아웃을 계산해 답을 줍니다. 가상 리스트의 텍스트 블록 500개에 대해 이 작업을 수행하면 500번의 멈춤을 강제하게 됩니다. <em>레이아웃 스레싱(layout thrashing)</em>이라 불리는 이 패턴은 복잡한 웹 애플리케이션에서 화면 끊김 현상의 주요 원인으로 남아 있습니다.</p>
<p>Pretext의 통찰은 <code>canvas.measureText()</code>가 DOM 렌더링과 동일한 폰트 엔진을 사용하면서도 브라우저의 레이아웃 프로세스 바깥에서 완전히 작동한다는 것입니다. 캔버스(canvas)로 단어를 측정하고 너비를 캐시하면, 그 시점부터 레이아웃은 순수한 산술 연산이 됩니다. 캐시된 너비를 순회하고, 누적 줄 너비를 추적하며, 컨테이너의 최대값을 초과하면 줄바꿈을 삽입합니다. 느린 측정 읽기도, 동기적 멈춤도 없습니다.</p>
<p>아키텍처는 이를 두 단계로 분리합니다. <code>prepare()</code>가 비용이 큰 작업을 한 번만 수행합니다. 공백을 정규화하고, <code>Intl.Segmenter</code>로 로케일에 맞는 단어 경계를 기준으로 텍스트를 분할하고, 양방향 텍스트(영어와 아랍어 혼합 등)를 처리하고, 캔버스로 세그먼트를 측정한 뒤 재사용 가능한 참조를 반환합니다. <code>layout()</code>은 캐시된 너비에 대한 순수 계산으로, 500개 텍스트 배치에 약 0.09ms가 소요되는 반면 <code>prepare()</code>는 약 19ms가 걸립니다. Cheng Lou 본인도 500배 비교가 &quot;불공정하다&quot;고 말합니다. 일회성 <code>prepare()</code> 비용을 제외하기 때문입니다. 하지만 그 비용은 한 번만 지불되고 이후 모든 호출에 분산됩니다. 텍스트가 처음 나타날 때 한 번 실행되며, 이후 모든 리사이즈는 빠른 경로를 통해 수행되므로 성능 향상은 실제적이고 상당합니다.</p>
<p>핵심 아이디어는 Meta에서 Sebastian Markbage가 진행한 연구로 거슬러 올라갑니다. Cheng Lou는 캔버스 폰트 메트릭이 DOM 측정을 대체할 수 있음을 증명한 <a href="https://github.com/chenglou/text-layout">초기 <code>text-layout</code> 프로토타입</a>을 구현했습니다. Pretext는 그 기반 위에 프로덕션급 국제화, 양방향 텍스트 지원, 그리고 빠른 경로를 만드는 2단계 아키텍처를 구축했습니다. Lou는 이런 일에 이력이 있습니다. <a href="https://github.com/chenglou/react-motion">react-motion</a>과 ReasonML 모두 모두가 당연시하는 제약 조건을 식별하고, 더 나은 추상화를 통해 이를 제거하는 동일한 패턴을 따랐습니다.</p>
<h2 id="측정의-돌파구">측정의 돌파구</h2>
<p>Pretext가 제공하는 첫 번째 사용 사례이자 제가 주장하고 싶은 것은, 브라우저에 높이를 묻지 않고도 정확한 위치에 DOM 텍스트 노드를 렌더링할 수 있도록 텍스트 높이를 측정하는 것입니다. 타협의 경로가 아니라 라이브러리가 할 수 있는 가장 강력한 일입니다.</p>
<p>500개의 채팅 메시지가 있는 가상 스크롤링(virtual scrolling) 리스트를 생각해 보세요. 보이는 것만 렌더링하려면 각 메시지가 뷰포트에 들어오기 전에 높이를 알아야 합니다. 전통적인 접근 방식은 텍스트를 DOM에 삽입하고 측정한 다음 배치하는 것으로, 메시지마다 레이아웃 비용을 지불합니다. Pretext를 사용하면 높이를 수학적으로 예측한 다음 올바른 위치에 텍스트 노드를 렌더링할 수 있습니다. 텍스트 자체는 여전히 DOM에 존재하므로, 접근성 모델, 선택 동작, 페이지 내 검색 모두 다른 텍스트 노드와 정확히 동일하게 작동합니다.</p>
<p>실제로는 다음과 같습니다.</p>
<pre><code>const prepared = prepare(message.text, &#39;16px Inter&#39;);
const { height } = layout(prepared, containerWidth, 24);</code></pre><p>함수 호출 두 번. 첫 번째는 측정하고 캐시하며, 두 번째는 계산을 통해 높이를 예측합니다. 레이아웃 비용은 없지만, 이후 렌더링하는 텍스트는 완전한 접근성을 갖춘 표준 DOM 노드입니다.</p>
<p><a href="https://chenglou.me/pretext/bubbles/">shrinkwrap 데모</a>는 이 경로가 왜 중요한지 가장 명확하게 보여줍니다. CSS <code>width: fit-content</code>는 가장 넓은 줄에 맞게 컨테이너 크기를 조정하는데, 마지막 줄이 짧을 때 공간을 낭비합니다. &quot;정확히 N줄로 줄바꿈되는 가장 좁은 너비를 찾아라&quot;라는 CSS 프로퍼티는 없습니다. Pretext의 <code>walkLineRanges()</code>는 최적의 너비를 수학적으로 계산하며, 결과물은 표준 DOM 텍스트 노드로 렌더링된 더 촘촘한 채팅 버블입니다. 성능 향상은 더 똑똑한 측정에서 비롯되지, DOM을 포기한 데서 비롯되지 않습니다. 최종 사용자에게 텍스트는 아무것도 달라지지 않습니다.</p>
<p>Pretext로 높이를 계산하는 <a href="https://chenglou.me/pretext/accordion/">아코디언 섹션</a>과, DOM 읽기 대신 높이 예측을 사용하는 <a href="https://chenglou.me/pretext/masonry/">메이슨리(masonry) 레이아웃</a> 모두 같은 모델을 따릅니다. 빠른 측정이 표준 DOM 렌더링으로 이어지는 것입니다.</p>
<p>알아둘 만한 엣지 케이스가 있습니다. 예측은 측정 시점에 사용 가능한 폰트 메트릭만큼만 정확하므로, <code>prepare()</code> 실행 전에 폰트가 로드되어야 합니다. 그렇지 않으면 결과가 틀어질 수 있습니다. 리거처(ligature, 두 문자가 하나의 글리프로 합쳐지는 것, &quot;fi&quot; 등), 고급 폰트 기능, 특정 <a href="https://en.wikipedia.org/wiki/CJK_characters">CJK</a> 합성 규칙은 캔버스 측정과 DOM 렌더링 사이에 미세한 차이를 만들 수 있습니다. 해결 가능한 문제이고 라이브러리가 이미 많은 부분을 처리하지만, 마법처럼 취급하지 않고 진지하게 접근하려면 이런 점을 인정해야 합니다.</p>
<h2 id="캔버스는-픽셀을-그리지-접근-가능한-텍스트를-그리지-않습니다">캔버스는 픽셀을 그리지, 접근 가능한 텍스트를 그리지 않습니다</h2>
<p>Pretext는 캔버스, SVG, WebGL로 렌더링하기 위한 수동 라인 레이아웃도 지원합니다. 이 API들은 정확한 줄 좌표를 제공해서 DOM이 처리하게 두는 대신 직접 텍스트를 그릴 수 있게 합니다. 바이럴된 경로이자 모든 커뮤니티 쇼케이스를 지배하는 경로입니다.</p>
<p>캔버스 데모는 인상적이고 DOM이 60fps에서 할 수 없는 일을 해내고 있습니다. 하지만 픽셀을 그리는 것이기도 하며, 텍스트를 캔버스 픽셀로 그리면 브라우저는 그 픽셀이 언어를 나타내는지 전혀 알지 못합니다. VoiceOver, NVDA, JAWS 같은 스크린 리더는 접근성 트리로 페이지를 이해합니다. 접근성 트리 자체가 DOM에서 만들어지므로 캔버스 콘텐츠는 스크린 리더에 보이지 않습니다. 브라우저의 페이지 내 검색과 번역 도구도 캔버스 픽셀을 완전히 건너뜁니다. 네이티브 텍스트 선택은 DOM 텍스트 노드에 묶여 있고 캔버스에는 그에 상응하는 것이 없으므로, 사용자가 콘텐츠를 선택하거나 복사하거나 키보드로 탐색할 수 없습니다. <code>&lt;canvas&gt;</code> 요소는 하나의 탭 정지점이기도 해서, 수천 단어가 포함되어 있더라도 키보드 사용자가 개별 단어나 문단 사이를 이동할 수 없습니다. 요약하면, 텍스트를 텍스트의 이미지가 아닌 텍스트답게 만드는 모든 것이 사라집니다.</p>
<p>그렇다고 해서 캔버스 방식이 무조건 잘못된 것은 아닙니다. 캔버스를 이용한 텍스트 렌더링이 올바른 선택이 되는 정당한 맥락들도 존재합니다. 게임, 데이터 시각화, 크리에이티브 인스톨레이션, 그리고 캔버스 위에 자체 접근성 레이어를 구축하는 데 수년을 투자한 디자인 도구가 그렇습니다. SVG 렌더링은 트레이드오프가 또 다릅니다. SVG 텍스트 요소는 접근성 트리에 참여하므로 DOM과 캔버스의 중간 지점이 됩니다.</p>
<p>하지만 캔버스 방식이 획기적인 혁신은 아닙니다. 캔버스 텍스트 렌더링은 수십 개의 라이브러리에서 15년 이상 존재해 왔기 때문입니다. 그중 어떤 것도 레이아웃 비용을 지불하지 않고 DOM 텍스트 레이아웃을 예측하는 방법을 제공하지 못했습니다. Pretext의 <code>prepare()</code>와 <code>layout()</code>이 정확히 그것을 해내며, 이것은 진정으로 새로운 것입니다.</p>
<h2 id="잘못된-데모가-자주-바이럴되는-이유">잘못된 데모가 자주 바이럴되는 이유</h2>
<p>이 패턴은 프런트엔드 생태계에서 자주 반복되며, 왜 그런지 이해합니다.</p>
<p>물처럼 텍스트를 가르는 용은 GIF로 녹화해서 소셜 미디어에 올리고 수천 개의 노출을 모을 수 있는 것입니다. 텍스트 높이를 미리 계산하는 가상 스크롤링 리스트는 그렇지 않은 것과 똑같아 보입니다. 성능 차이는 상당하지만 눈에는 보이지 않습니다. &quot;VoiceOver와 완벽하게 작동합니다&quot;나 &quot;강제 레이아웃 없이 10,000개 메시지를 스크롤합니다&quot;라는 쇼케이스를 만드는 사람은 없습니다. 이런 것은 아무것도 아닌 것처럼 보이기 때문입니다. 웹 페이지가 원래 작동해야 하는 대로 작동하는 것처럼 보입니다.</p>
<p>이것은 웹 성능에 적용된 <a href="https://en.wikipedia.org/wiki/Goodhart%27s_law">굿하트의 법칙(Goodhart&#39;s Law)</a>입니다. 지표가 목표가 되는 순간, 더 이상 유효한 측정 기준이 될 수 없습니다. 프레임 속도와 레이아웃 비용은 &quot;사용자에게 잘 작동하는가&quot;의 대리 지표입니다. GitHub 스타는 &quot;유용한가&quot;의 대리 지표입니다. 대리 지표만 최적화하다 보면, 접근성 트레이드오프가 가장 큰 경로를 사용하는 시각적으로 인상적인 데모가 주목받고, 라이브러리가 왜 중요한지에 대한 실제 신호는 사라집니다. 처음 72시간 동안 가장 시각적으로 인상적인 기능이 라이브러리의 정체성을 결정하고, 프레임은 &quot;나는 무언가를 그리고 있다&quot;가 되어버립니다. &quot;나는 누구보다 빠르게 측정하고 있다&quot;가 아니라요. 한번 정해진 프레임은 바꾸기 어렵습니다.</p>
<p>웹에서 최고의 텍스트 편집 라이브러리인 <a href="https://codemirror.net">CodeMirror</a>, <a href="https://microsoft.github.io/monaco-editor/">Monaco</a>, <a href="https://prosemirror.net">ProseMirror</a>는 모두 DOM을 벗어나는 것이 더 빠를 수 있는 상황에서도 의도적으로 DOM 내에 머무르기로 결정했습니다. 접근성 모델은 선택 사항이 아니기 때문입니다. Pretext의 DOM 측정 방식은 이러한 전통을 따르면서도 한 걸음 더 나아갑니다. 앞서 언급한 편집기들은 무언가의 높이를 알아야 할 때 여전히 DOM에서 읽습니다. Pretext는 그 단계를 완전히 제거하고, 노드가 렌더링되기 전에 산술로 높이를 예측합니다. 이는 동일한 철학 하에서 취할 수 있는 다음 단계의 논리적 선택입니다. 텍스트는 본래 있어야 할 곳에 두되, 이를 위해 측정 비용을 지불하는 것은 중단하는 것입니다.</p>
<hr>
<h2 id="더-큰-그림">더 큰 그림</h2>
<p>저는 경력 대부분을 성능 엔지니어링이라는 분야에 대해 생각해 왔고, Pretext에서 가장 인상적인 점은 진정한 혁신이 바로 가장 눈에 띄지 않는 곳에 있다는 사실입니다. 텍스트가 페이지에 표시되기 전에 어떻게 배치될지 예측하면서, 텍스트를 DOM에 유지하고 접근성을 보장하는 것은 웹 플랫폼에서 진정으로 새로운 기능입니다. 이는 텍스트가 많은 모든 복잡한 애플리케이션이 즉시 도입할 수 있는 근본적인 개선 사항입니다.</p>
<p>이번 주에 Pretext를 사용하려 한다면, <code>prepare()</code>와 <code>layout()</code>을 먼저 사용하세요. 텍스트를 DOM에 유지하면서 브라우저에 묻지 않고 높이를 예측하는 무언가를 만드세요. 모든 사용자가 읽고, 선택하고, 검색하고, 탐색할 수 있는 인터페이스를 출시하세요. 아직 아무도 이것을 하지 않았고, 만들어 볼 가치가 있습니다.</p>
<p>성능 엔지니어링은 누구에게도 무언가를 포기하라고 요구하지 않으면서 모든 사람에게 도움이 될 때 가장 빛납니다. 누군가를 어지럽게 만들지 않는 더 빠른 프레임 속도. 운동 장애가 있는 사람이 필요할 때 페이지가 응답하게 만드는 더 적은 레이아웃 멈춤. 빠르고 읽을 수 있고 선택할 수 있고 번역할 수 있고 키보드로 탐색할 수 있고 스크린 리더가 이해할 수 있는 텍스트.</p>
<p>용은 재미있습니다. 측정 엔진은 중요합니다. 이 둘을 혼동하지 맙시다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[(번역) 자기가 작업하지 않는 소프트웨어는 설계할 수 없습니다]]></title>
            <link>https://velog.io/@tap_kim/you-cant-design-software-you-dont-work-on</link>
            <guid>https://velog.io/@tap_kim/you-cant-design-software-you-dont-work-on</guid>
            <pubDate>Mon, 16 Mar 2026 01:45:30 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>원문: <a href="https://www.seangoedecke.com/you-cant-design-software-you-dont-work-on/">https://www.seangoedecke.com/you-cant-design-software-you-dont-work-on/</a></p>
</blockquote>
<p>대규모 소프트웨어 시스템의 설계 과정에 의미 있게 참여할 수 있는 사람은 해당 시스템을 직접 작업하는 엔지니어뿐입니다. 시스템의 구체적인 세부 사항을 속속들이 이해하지 못하면 좋은 소프트웨어 설계를 할 수 없기 때문입니다. 다시 말해, <strong>일반적인 소프트웨어 설계 조언은 대부분의 실질적인 설계 문제에 쓸모가 없습니다</strong>.</p>
<h3 id="일반적인-소프트웨어-설계">일반적인 소프트웨어 설계</h3>
<p>일반적인 소프트웨어 설계란 무엇일까요? &quot;문제에 맞춰 설계하기&quot;, 즉 <em>도메인</em>은 어느 정도 이해하지만 기존 <em>코드베이스</em>에 대한 지식은 거의 없을 때 할 수 있는 종류의 조언입니다. 안타깝게도 소프트웨어 관련 서적이나 블로그 글에서 읽을 수 있는 조언은 이런 종류뿐입니다. 엔지니어들은 모든 기술 전문가가 &quot;업계 이야기&quot;를 좋아하는 것과 같은 이유로 일반적인 소프트웨어 설계 조언을 즐겨 합니다. 하지만 일반적인 조언을 실제 일상 업무에 적용할 때는 매우 신중해야 합니다.</p>
<p><strong>실제 업무에서는 구체적인 요소가 일반적인 요소보다 압도적으로 중요합니다.</strong> 코드가 지금 어떤 모습인지 명확하게 파악하는 것이 일반적인 설계 패턴이나 원칙을 잘 아는 것보다 훨씬, 훨씬 더 중요합니다. 예를 들면 다음과 같습니다.</p>
<ul>
<li>대규모 코드베이스에서는 일관성이 &quot;좋은 설계&quot;보다 중요합니다. 여기서 이 점을 논증하지는 않겠지만, <a href="https://www.seangoedecke.com/large-established-codebases"><em>대규모 기존 코드베이스에서 엔지니어들이 저지르는 실수</em></a>에서 자세히 다룬 바 있습니다.</li>
<li>실제 코드베이스는 보통 복잡하고 예측하기 어려운 결과로 가득합니다. 변경을 안전하게 적용하려면 구현 선택지가 극소수로 제한되기 마련입니다.</li>
<li>대규모 공유 코드베이스는 단일 설계를 반영하는 법이 없으며, 항상 서로 다른 소프트웨어 설계 사이의 중간 상태에 놓여 있습니다. 따라서 이상적인 &quot;북극성&quot;을 향해 나아가는 것보다, 개별 변경 후 코드베이스가 어떻게 맞물리는지가 훨씬 더 중요합니다.</li>
</ul>
<p>전체 시스템을 마음대로 재작성할 수 있는 세상이라면 일반적인 소프트웨어 설계 조언이 훨씬 실용적일 것입니다. 실제로 그런 프로젝트도 있습니다! 하지만 <strong>소프트웨어 엔지니어링 업무의 대부분은 안전하게 재작성할 수 없는 시스템에서 이루어집니다</strong>. 이런 시스템은 &quot;소프트웨어 설계&quot;에 의존할 수 없으며, 내부 일관성과 엔지니어의 신중함에 의존해야 합니다.</p>
<h3 id="구체적인-소프트웨어-설계">구체적인 소프트웨어 설계</h3>
<p>그렇다면 좋은 소프트웨어 설계란 어떤 모습일까요?</p>
<p>제 경험상, 가장 유용한 소프트웨어 설계는 시스템을 깊이 이해하는 소수의 엔지니어들 사이의 대화에서 이루어집니다. 매일 그 시스템을 직접 작업하는 사람들이기 때문입니다. 이런 설계 논의는 외부인이 보기에 <strong>정말 지루한</strong> 경우가 많습니다. 기술적 배경이 있는 사람이라면 누구나 이해하고 의견을 낼 수 있는 일반 원칙이 아니라, 시스템의 난해한 구체적 세부 사항을 중심으로 돌아가기 때문입니다.</p>
<p>논의되는 주제는 &quot;DRY가 WET보다 나은가&quot;가 아니라, &quot;이 새로운 동작을 서브시스템 A에 넣을 수 있을까? 안 돼, 정보 B가 필요한데 컨텍스트 C에서는 해당 서브시스템에서 사용할 수 없거든. 서브시스템 D를 재작성하지 않으면 노출할 수가 없어. 하지만 서브시스템 E를 여기와 여기서 분리하면…&quot; 같은 것입니다.</p>
<p>설계에 대한 깊은 철학적 논점은 논의에서 좀처럼 중요하지 않습니다. 오히려 가장 핵심적인 기여는 구체적인 사항에 대한 작은 오해를 지적하는 것입니다. 예를 들면 이런 것입니다. &quot;아, 컨텍스트 C에서 B를 사용할 수 없다고 생각했나 보네. 그런데 최근에 C를 리팩터링해서 이제 필요하면 B를 연결할 수 있어.&quot;</p>
<h3 id="일반적인-소프트웨어-설계가-유용할-때">일반적인 소프트웨어 설계가 유용할 때</h3>
<p>일반적인 소프트웨어 설계 조언은 실질적인 설계 문제에는 유용하지 않지만, 완전히 쓸모없는 것은 아닙니다.</p>
<p><strong>완전히 새로운 프로젝트를 만들 때 유용합니다.</strong> 앞서 주장했듯이, 기존 시스템에서 새로운 기능을 설계할 때는 시스템의 구체적인 요소가 지배적입니다. 하지만 <em>새로운 시스템</em>을 설계할 때는 구체적인 요소가 없으므로, 전적으로 일반적인 조언에 따를 수 있습니다.</p>
<p><strong>구체적인 설계 결정에서 우열을 가릴 때 유용합니다.</strong> 일반적인 설계부터 시작해야 한다고 생각하지는 않지만, 모두 수용 가능해 보이는 몇 가지 구체적인 경로가 있을 때 일반 원칙이 그 사이에서 결정을 내리는 데 도움이 될 수 있습니다.</p>
<p>이 점은 회사 전체 차원에서 특히 그렇습니다. 즉, <strong>일반적인 소프트웨어 설계 조언은 서로 다른 코드베이스 간의 일관성을 보장하는 데 도움이 됩니다</strong>. 이것이 공식적인 &quot;소프트웨어 아키텍트&quot; 역할의 가장 유용한 기능 중 하나입니다. 일반 원칙의 체계를 제공하여 개별 엔지니어들이 구체적인 결정에서 우열을 가릴 때 모두 같은 방향으로 판단할 수 있게 하는 것입니다.</p>
<p><strong>이런 설계 원칙은 회사 차원의 아키텍처 결정을 안내하는 데도 도움이 됩니다.</strong> 서비스를 자체 데이터센터에서 운영해야 할까, 아니면 클라우드에서 운영해야 할까? 쿠버네티스를 사용해야 할까? AWS인가 Azure인가? 충분히 넓은 범위에서 보면 개별 서비스의 구체적인 세부 사항은 거의 중요하지 않습니다. 어느 쪽이든 막대한 양의 작업이 필요하기 때문입니다. 그래도 이런 결정에서조차 구체적인 세부 사항은 매우 중요합니다. 클라우드에서는 할 수 없는 것(맞춤형 하드웨어 구성에 의존하기 등)이 있고, 자체 데이터센터에서는 할 수 없는 것(12개 리전의 엣지에 서비스를 배포하기 등)이 있습니다. 코드베이스의 구체적인 세부 사항이 그런 것들에 의존한다면, 회사 차원의 아키텍처 결정을 내릴 때 이를 무시하면 곤란해질 것입니다.</p>
<h3 id="아키텍트와-로컬-미니마local-minima">아키텍트와 로컬 미니마(local minima)</h3>
<p>위에서 언급한 것들은 모두 일반적인 소프트웨어 설계를 해야 할 좋은 이유입니다. 회사들이 일반적인 소프트웨어 설계를 하는 나쁜 이유 하나는, 현업 소프트웨어 엔지니어가 아닌 사람들에게 그냥 정말 좋은 아이디어처럼 들린다는 것입니다. 일단 시작하면 인센티브 구조 때문에 멈추기 어렵습니다. 많은 테크 기업이 이 로컬 미니마에 빠집니다.</p>
<p>최고 연봉을 받는 소프트웨어 엔지니어가 가장 추상적이고 영향력이 큰 결정을 내리는 데만 시간을 쓰게 하면 안 될 이유가 있을까요? 구조 엔지니어는 벽돌을 쌓는 게 아니라 도면을 그려야 하지 않겠습니까? 구조공학이 실제로 그렇게 돌아가는지는 모르겠지만, 소프트웨어 엔지니어링은 그렇지 않다는 것은 압니다. 실제로 <strong>현장의 엔지니어들은 소프트웨어 아키텍처 조언을 무시할 수밖에 없는 경우가 많습니다</strong>. 현재 시스템의 맥락에서 아키텍처 조언을 실제 구현으로 옮길 방법이 없기 때문입니다.</p>
<p>하지만 효과가 없는 관행치고, &quot;최고의 엔지니어에게 일반적인 설계만 하게 하라&quot;는 발상은 놀라울 정도로 끈질기게 살아남습니다. <strong>아키텍트에게는 아무런 이해관계가 없기 때문입니다.</strong> 설계는 실제 엔지니어링 팀에 넘겨져 구현됩니다. 그 설계가 완벽하게 구현될 수는 없기 때문에, 아키텍트는 성공에 대해서는 공을 차지하고(결국 자기 설계였으니까), 실패에 대해서는 책임을 회피할 수 있습니다(저 바보들이 내 설계대로만 했더라면!).</p>
<h3 id="요약">요약</h3>
<p>대규모 기존 코드베이스에서 작업할 때, 유용한 소프트웨어 설계 논의는 많은 사람이 생각하는 것보다 훨씬, 훨씬 더 구체적입니다. 보통 개별 파일이나 코드 한 줄 단위로 이야기합니다. 따라서 코드베이스를 속속들이 알지 못하면 유용한 소프트웨어 설계를 할 수 없습니다(실질적으로 이는 거의 항상 활발한 기여자여야 한다는 뜻입니다).</p>
<p>순수하게 일반적인 아키텍처가 <em>쓸모없지는</em> 않지만, 그 역할은 다음으로 한정되어야 합니다. (a) 완전히 새로운 시스템을 위한 정비된 경로 제시, (b) 기존 시스템에서의 결정에서 우열 가리기, (c) 회사의 광범위한 기술 선택 지원.</p>
<p>제 의견으로는, 프로젝트의 초기 설계를 짜는 데만 모든 시간을 쏟는 공식적인 &quot;거시적 소프트웨어 아키텍트&quot; 역할은 실패할 수밖에 없습니다. 좋은 아이디어처럼 들리고(비난의 위험 없이 공을 차지할 수 있으니 아키텍트에게는 좋은 거래이기도 하지만), 실제로 코드를 작성해야 하는 엔지니어링 팀에는 거의 가치를 제공하지 못합니다.</p>
<p>개인적으로 저는 <strong>소프트웨어 프로젝트의 설계를 구상했다면, 그 프로젝트의 성패에 책임을 져야 한다</strong>고 생각합니다. 그렇게 하면 소프트웨어 시스템을 설계하는 사람이 소프트웨어 시스템을 출시하는 방법을 아는 사람이 되도록 빠르게 보장할 수 있을 것입니다. 또한 코드베이스의 온갖 거친 면과 결점을 고려해야 하는 <em>진짜</em> 소프트웨어 설계자들이 자신이 수행하는 어려운 설계 작업에 대해 인정받을 수 있을 것입니다.</p>
<hr>
<h2 id="각주">각주</h2>
<ol>
<li><p>솔직히 말해서, 저는 &quot;<a href="https://www.seangoedecke.com/good-api-design/">훌륭한 API 설계에 대해 내가 아는 모든 것</a>&quot;, &quot;<a href="https://www.seangoedecke.com/good-system-design/">훌륭한 시스템 설계에 대해 내가 아는 모든 것</a>&quot;, &quot;<a href="https://www.seangoedecke.com/great-software-design/">훌륭한 소프트웨어 디자인은 겉보기에는 평범해 보인다</a>&quot;, 그리고 아마 그 밖에도 십여 군데에서 제 나름대로의 일반적인 소프트웨어 설계 조언을 해왔습니다.</p>
</li>
<li><p>&quot;실제 업무 문제&quot;라고 할 때, 여기서 말하는 것은 순수하지 않은(impure) 소프트웨어 엔지니어링입니다. 실제 비즈니스 요구를 해결하기 위한 코드베이스, 즉 (a) 타협으로 가득하고 (b) 끊임없이 변화하는 상태에 놓인 코드베이스를 말합니다. 우아한 단일 목적의 라이브러리나 우주 탐사선용 소프트웨어를 작업하고 있다면, 이 조언의 상당 부분은 해당되지 않을 것입니다.</p>
</li>
<li><p>정말 끔찍한 결정만 아니라면, 그 일반 원칙이 좋은지 나쁜지는 거의 중요하지 않습니다. 개별 코드베이스와 마찬가지로, 일관성의 이점이 &quot;최고의&quot; 설계를 갖추는 이점보다 큽니다.</p>
</li>
<li><p>여기서 말하는 아키텍트란 그런 뜻입니다. &quot;아키텍트&quot;라는 직함은 매우 다양한 종류의 업무를 포괄하며, 그중에는 &quot;일반적인 엔지니어링 업무를 하는 아주 시니어한 엔지니어&quot;도 포함됩니다. 많은 아키텍트는 &quot;아키텍트&quot; 직함이 전혀 없고, 실제 구현을 넘어선 시니어/스태프/디스팅귀시드 소프트웨어 엔지니어일 뿐입니다.</p>
</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[(번역) 추가 비용 없이 작동하는 아키텍처: 브라우저에서 전부 돌아가는 자산 관리 플랫폼을 만든 이유]]></title>
            <link>https://velog.io/@tap_kim/the-zero-marginal-cost-architecture</link>
            <guid>https://velog.io/@tap_kim/the-zero-marginal-cost-architecture</guid>
            <pubDate>Sun, 25 Jan 2026 15:13:24 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>원문: <a href="https://medium.com/@mmostagirbhuiyan/the-zero-marginal-cost-architecture-why-i-built-a-wealth-planner-to-run-entirely-on-the-edge-e632ba727490">https://medium.com/@mmostagirbhuiyan/the-zero-marginal-cost-architecture-why-i-built-a-wealth-planner-to-run-entirely-on-the-edge-e632ba727490</a></p>
</blockquote>
<p>분산 시스템 설계 업무에서 저는 쿠버네티스 클러스터, 이그레스 비용, 가용 영역 간 상태 관리에 많은 시간을 할애합니다. AI 애플리케이션의 업계 표준 역시 이러한 복잡성을 반영합니다. 우리는 파이썬 백엔드를 가동하고, API 키를 관리하며, 방대한 양의 사용자 데이터를 중앙 집중식 추론 제공업체로 전송합니다.</p>
<p>최근 구축한 자산 관리 플랫폼 <a href="https://meridian.mmostagirbhuiyan.com/"><strong>메리디언(Meridian)</strong></a>에서는 이 모델을 뒤집고자 했습니다.</p>
<p>금융 데이터는 방사능 같은 위험 자산입니다. 플랫폼 엔지니어로서 가장 안전한 데이터는 아예 보유하지 않는 데이터라는 점을 잘 알고 있습니다. 그래서 브라우저 내에서 완전히 실행되는 <strong>프라이버시 우선, 제로 지식</strong> 금융 자문 서비스를 구축하기로 했습니다. 백엔드 없음. API 키 없음. 월간 서버 비용 없음.</p>
<p>하드웨어 인식 AI 아키텍처와 타입스크립트(Algora에서 상위 1% 실력자)에 대한 제 배경을 바탕으로, 이 작업을 일반적인 프런트엔드 프로젝트가 아닌 <strong>분산 시스템 과제</strong>로 접근했습니다. 사용자의 브라우저를 분산형 네트워크 내 주권 노드로 간주했습니다.</p>
<p><strong>WebLLM</strong>을 활용한 에지 추론, <strong>Yjs</strong>를 통한 로컬-퍼스트 지속성, 하드웨어 가속 시각화를 위한 <strong>리액트 트리 파이버(R3F)</strong>를 사용해 메리디언을 구축한 과정은 다음과 같습니다.</p>
<h2 id="브라우저를-런타임으로-활용하는-패턴">&quot;브라우저를 런타임&quot;으로 활용하는 패턴</h2>
<p>기존 아키텍처는 브라우저를 단순 터미널로 취급하는 경우가 많습니다. &quot;브라우저를 런타임으로 활용하는 패턴&quot;은 이를 컴퓨팅 노드로 간주합니다. 이는 클라우드(운영 비용)에서 에지(사용자 하드웨어)로 무거운 작업을 이동시켜 현대적인 FinOps 원칙과 완벽히 부합합니다. <strong>데이터가 있는 위치로 컴퓨팅을 이동시키는 것입니다.</strong></p>
<p><img src="https://miro.medium.com/v2/resize:fit:720/format:webp/1*VZfteJObnpIthEUwUok5tA.png" alt=""></p>
<p>| Zero-Trust, Zero-Cost, Zero-Latency. The anatomy of a Sovereign Cloud application.</p>
<h2 id="1-webllm에지-네이티브-ai를-통한-추론-오프로딩">1. WebLLM(에지 네이티브 AI)를 통한 추론 오프로딩</h2>
<p>메리디언의 핵심은 AI 어드바이저입니다. GPT-4를 호출하는 대신, 메리디언은 WebLLM을 사용하여 Llama 3.2 1B를 브라우저에 직접 다운로드하여 실행합니다.</p>
<p>이는 단순한 개인 정보 보호 차원의 선택이 아닌, 지연 시간과 비용을 고려한 결정이었습니다.</p>
<ul>
<li><strong>지연 시간</strong>: M1 맥북 에어에서 Llama 3.2 1B는 <strong>초당 약 40토큰</strong>을 처리합니다. 이는 인간의 눈으로 읽을 수 있는 속도보다 빠르며, 많은 클라우드 API의 왕복 시간보다 훨씬 빠릅니다.</li>
<li><strong>메모리 사용량</strong>: 4비트 양자화 모델의 크기는 <strong>약 1.3GB</strong>입니다. 이는 대부분의 최신 소비자용 노트북 VRAM에 충분히 수용 가능한 수준이지만, 애플리케이션에 대한 하드웨어 최소 요구사항을 설정합니다.</li>
</ul>
<p><code>src/hooks/useWebLLM.ts</code> 파일의 통합 코드는 다음과 같습니다. 저수준 WebGPU API와의 인터페이스 시 안정성 유지를 위해 필수적인 엄격한 타입 지정(strict typing)에 유의하세요.</p>
<pre><code class="language-tsx">import { CreateMLCEngine, MLCEngine } from &quot;@mlc-ai/web-llm&quot;;
import { useState } from &quot;react&quot;;

// 모델 선택은 브라우저 성능에 매우 중요합니다.
const SELECTED_MODEL = &quot;Llama-3.2-1B-Instruct-q4f32_1-MLC&quot;;
export const useWebLLM = () =&gt; {
  const [engine, setEngine] = useState&lt;MLCEngine | null&gt;(null);
  const [loadingProgress, setLoadingProgress] = useState(&quot;&quot;);
  const initEngine = async () =&gt; {
    try {
      // WebGPU 가속은 MLC에서 자동으로 처리됩니다.
      const newEngine = await CreateMLCEngine(SELECTED_MODEL, {
        initProgressCallback: (report) =&gt; {
          setLoadingProgress(report.text);
        },
      });
      setEngine(newEngine);
    } catch (err) {
      console.error(&quot;WebGPU not supported or model load failed&quot;, err);
    }
  };
  const generateAdvice = async (
    portfolioContext: string,
    userQuery: string,
  ) =&gt; {
    if (!engine) return;
    // 소규모 모델은 지침이 없는 프롬프트에 취약합니다.
    // 엄격한 시스템 프롬프트를 통해 구조를 강제합니다.
    const messages = [
      { role: &quot;system&quot;, content: &quot;You are a financial advisor...&quot; },
      {
        role: &quot;user&quot;,
        content: `Context: ${portfolioContext}\n\nQuestion: ${userQuery}`,
      },
    ];
    const reply = await engine.chat.completions.create({ messages });
    return reply.choices[0].message;
  };
  return { initEngine, generateAdvice, loadingProgress };
};</code></pre>
<p><strong>&quot;소형 모델&quot; 제약 조건</strong>: 가장 큰 기술적 난관은 모델을 실행하는 것이 아니라 제어하는 것이었습니다. 10억 개 매개변수 모델은 형식 환각 현상이 발생하기 쉽습니다. 신뢰할 수 있는 금융 조언을 얻기 위해 프롬프트 엔지니어링을 데이터베이스 스키마 정의처럼 다루어야 했고 UI가 조언자의 “생각”을 파싱할 수 있도록 JSON 출력을 엄격히 강제 적용합니다.</p>
<h2 id="2-yjs-및-indexeddb를-활용한-분산-상태-관리">2. Yjs 및 IndexedDB를 활용한 분산 상태 관리</h2>
<p>분산 시스템에서 상태 관리는 가장 어려운 과제입니다. 중앙 데이터베이스 없이, 저는 <strong>Yjs</strong>(CRDT 라이브러리)와 IndexedDB를 활용한 <strong>로컬 우선(Local-First)</strong> 아키텍처를 채택했습니다.</p>
<p>왜 <code>localStorage</code>만 사용하지 않았을까요? <code>localStorage</code>는 동기식이며, 차단적이며, 약 5MB로 제한되기 때문입니다. Yjs + IndexedDB는 다음을 가능하게 합니다.</p>
<ol>
<li><strong>비동기적 쓰기</strong>: 복잡한 포트폴리오 상태를 저장할 때 UI를 멈추지 않습니다.</li>
<li><strong>델타 업데이트</strong>: Yjs는 전체 블롭이 아닌 변경 사항(업데이트)만 저장한다. 이는 매우 효율적입니다.</li>
<li><strong>충돌 해결</strong>: 브라우저 세션을 분할 내성 데이터베이스로 효과적으로 전환합니다. 사용자가 앱을 두 개의 탭에서 동시에 열어도 데이터 손실 없이 상태가 매끄럽게 병합됩니다.</li>
</ol>
<p><code>src/utils/localFirstStore.ts</code>에서 Yjs 문서를 IndexedDB에 바인딩하여 서버 측 데이터베이스에 필적하는 안정성을 가진 영속성 계층을 생성합니다.</p>
<pre><code class="language-tsx">import * as Y from &quot;yjs&quot;;
import { IndexeddbPersistence } from &quot;y-indexeddb&quot;;

// Yjs 문서 생성 - 단일 진실의 원천
const ydoc = new Y.Doc();
// 문서를 IndexedDB에 저장
const provider = new IndexeddbPersistence(&quot;meridian-store&quot;, ydoc);
export const useLocalFirst = () =&gt; {
  const [data, setData] = useState&lt;AppState | null&gt;(null);
  useEffect(() =&gt; {
    const yMap = ydoc.getMap(&quot;portfolioData&quot;);
    // 공유 맵의 변경 사항 관찰
    yMap.observe(() =&gt; {
      setData(yMap.toJSON() as AppState);
    });
    // IndexedDB에서 데이터 로드 완료 대기
    provider.on(&quot;synced&quot;, () =&gt; {
      setData(yMap.toJSON() as AppState);
    });
  }, []);
  // ... 구현 세부 사항
};</code></pre>
<h2 id="3-불확실성-시각화-webgpu-가속화">3. 불확실성 시각화 (WebGPU 가속화)</h2>
<p>금융 앱은 종종 “스프레드시트 피로감”에 시달립니다. 몬테카를로 시뮬레이션에 내재된 위험과 확률 분포를 시각화하기 위해 캔버스 차트 이상의 것이 필요했습니다.</p>
<p><strong>R3F</strong>를 활용해 LLM과 동일한 GPU 파이프라인으로 10,000개 이상의 데이터 포인트를 렌더링했습니다. <code>MonteCarloCloud</code> 컴포넌트는 <code>InstancedMesh</code>를 사용합니다. 이 기술은 게임 개발에서는 흔하지만 핀테크 분야에서는 드뭅니다. 단일 드로우 콜로 수천 개의 입자를 렌더링 할 수 있어 모바일 기기에서도 60fps를 유지합니다.</p>
<pre><code class="language-tsx">// src/components/3d/MonteCarloCloud.tsx
import { useRef, useMemo } from &quot;react&quot;;
import { useFrame } from &quot;@react-three/fiber&quot;;
import * as THREE from &quot;three&quot;;

export const MonteCarloCloud = ({ simulations }) =&gt; {
  const meshRef = useRef&lt;THREE.InstancedMesh&gt;(null);
  const dummy = useMemo(() =&gt; new THREE.Object3D(), []);
  useFrame(({ clock }) =&gt; {
    if (!meshRef.current) return;

    // 시간에 따른 변동성을 시각화하기 위해 입자를 애니메이션 처리
    simulations.forEach((sim, i) =&gt; {
      const x = i * 0.1;
      const y = sim.value * 0.001;
      dummy.position.set(x, y, 0);
      dummy.updateMatrix();
      meshRef.current.setMatrixAt(i, dummy.matrix);
    });
    meshRef.current.instanceMatrix.needsUpdate = true;
  });

  // ... 렌더링
};</code></pre>
<h2 id="아키텍처의-절충점">아키텍처의 절충점</h2>
<p>아키텍처는 절충의 기술입니다. 백엔드를 제거함으로써 우리는 두 가지 주요 비용을 감수했습니다.</p>
<ol>
<li><strong>&quot;콜드 스타트&quot; 페널티</strong>: 초기 1.3GB 모델 다운로드는 상당한 장벽입니다. 이는 새 파드(pod) 프로비저닝과 유사하지만 클라이언트 측에서 단 한 번만 발생하는 비용입니다. 이는 사용자가 설정 단계를 감수하는 ‘프로’ 도구에는 적합하지만, 캐주얼 소비자 앱에는 적합하지 않은 아키텍처입니다.</li>
<li><strong>하드웨어 의존성</strong>: 우리는 사용자에게 최소 하드웨어 사양(WebGPU 지원)을 사실상 요구합니다. 보편성을 성능과 교환한 것입니다.</li>
</ol>
<h2 id="결론">결론</h2>
<p>메리디안은 단순한 자산 관리 도구가 아닙니다. 이는 <strong>주권 클라우드</strong> 애플리케이션의 개념 증명입니다. MLOps 원칙과 브라우저 네이티브 기술을 결합함으로써, 기본적으로 비공개이며 무한 확장 가능하고 한계 인프라 비용이 없는 시스템을 구축할 수 있습니다.</p>
<p>하드웨어 인식 AI와 더 효율적인 CPU 아키텍처로 나아가면서 &quot;클라이언트&quot;와 &quot;서버&quot;의 경계는 계속 모호해질 것입니다. 이 분할의 양쪽을 모두 마스터하는 것이 플랫폼 엔지니어링의 미래입니다.</p>
<p><em>저자 소개: 저는 주권 컴퓨팅의 최전선에서 개발하는 엔지니어링 디렉터이자 수석 아키텍트입니다. 전 세계 타입스크립트 엔지니어 상위 1%에 선정된(Algora) 저는 특허 출원 중인 AI 하드웨어부터 분산 상태에 이르는 시스템 연구를 실제 운영 소프트웨어 출시의 실용성과 결합합니다. 차세대 위대한 소프트웨어가 클라우드에서만 실행되는 것이 아니라 어디서나 실행될 것임을 증명하기 위해 글을 씁니다. 다트머스 공학 석사, 코넬 경영학 석사.</em></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[(번역) Tailwind CSS: 자식 요소 타겟팅이 필요할 때]]></title>
            <link>https://velog.io/@tap_kim/tailwind-targeting-child-elements</link>
            <guid>https://velog.io/@tap_kim/tailwind-targeting-child-elements</guid>
            <pubDate>Mon, 22 Dec 2025 03:44:09 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>원문: <a href="https://cekrem.github.io/posts/tailwind-targeting-child-elements/">https://cekrem.github.io/posts/tailwind-targeting-child-elements/</a></p>
</blockquote>
<p>테일윈드(Tailwind)의 핵심은 유틸리티 클래스를 요소에 직접 적용하는 것입니다. <code>p</code>나 <code>div</code> 같은 일반 요소에 하위 선택자를 사용해 스타일을 적용하는 것은 본질에 반하는 방식입니다. 바로 그런 방식을 대체하기 위해 테일윈드가 설계되었기 때문입니다.</p>
<p>하지만 어쩔 수 없는 경우도 있습니다. CMS 콘텐츠, 타사 컴포넌트, 동적으로 생성된 HTML 같은 경우죠. 이렇듯 통제할 수 없는 요소에 스타일을 적용해야 할 때가 있습니다.</p>
<p><strong>먼저 분명히 하자면</strong>, 이를 처리하기 위해 기본 CSS를 조금 추가하는 것이 때로는 가장 간단하고 합리적인 해결책입니다. CMS 콘텐츠 전용 스타일시트를 만드는 것도 완벽히 타당한 접근법입니다. 하지만 테일윈드의 유틸리티 클래스 패러다임 내에서 작업하기로 결심했거나, 단순히 가능한 방법을 궁금해한다면, 이 글에서 임의의 변형(arbitrary variant)을 사용해 자식 요소를 타겟팅하는 방법을 소개합니다.</p>
<h2 id="문제-상황">문제 상황</h2>
<p>제어할 수 없는 HTML이 포함된 컨테이너가 있다고 가정해 보겠습니다.</p>
<pre><code class="language-html">&lt;div class=&quot;cms-content&quot;&gt;
  &lt;p&gt;Some text with a &lt;a href=&quot;#&quot;&gt;link&lt;/a&gt; in it.&lt;/p&gt;
  &lt;ul&gt;
    &lt;li&gt;List item one&lt;/li&gt;
    &lt;li&gt;List item two&lt;/li&gt;
  &lt;/ul&gt;
&lt;/div&gt;</code></pre>
<p>모든 내부 링크에 특정 스타일을 적용하고 싶습니다. 마우스 오버 시 밑줄 표시, 상속된 텍스트 색상과 다른 독특한 색상, 아마도 글꼴 두께도요. 전통적인 접근법? 사용자 정의 CSS를 작성하세요.</p>
<pre><code class="language-css">.cms-content a {
  font-weight: 600;
  text-decoration: none;
}
.cms-content a:hover {
  text-decoration: underline;
}
.cms-content li {
  list-style-type: disc;
  margin-left: 1.5rem;
}</code></pre>
<p>이 방식은 아주 타당한 접근법입니다. 그리고 때론 올바른 선택이기도 하죠! CMS 콘텐츠용 소형 스타일시트는 단순하고 유지보수가 용이합니다. 하지만 모든 요소를 유틸리티 클래스로 관리하고 싶다면 테일윈드가 어떤 기능을 제공하는지 살펴보겠습니다.</p>
<h2 id="임의-변형-테일윈드-방식">임의 변형: 테일윈드 방식</h2>
<p>테일윈드의 임의 변형을 사용하면 대괄호 표기법을 통해 클래스 이름에 직접 어떤 CSS 선택자든 작성할 수 있습니다.</p>
<pre><code class="language-html">&lt;div
  class=&quot;[&amp;_a]:font-semibold [&amp;_a]:no-underline [&amp;_a:hover]:underline [&amp;_li]:list-disc [&amp;_li]:ml-6&quot;
&gt;
  &lt;p&gt;Some text with a &lt;a href=&quot;#&quot;&gt;link&lt;/a&gt; in it.&lt;/p&gt;
  &lt;ul&gt;
    &lt;li&gt;List item one&lt;/li&gt;
    &lt;li&gt;List item two&lt;/li&gt;
  &lt;/ul&gt;
&lt;/div&gt;</code></pre>
<p>처음에는 <code>&amp;_a</code> 구문이 이상해 보일 수 있지만, 무슨 일이 일어나고 있는지 이해하면 간단합니다.</p>
<h2 id="구문-분석하기">구문 분석하기</h2>
<p>핵심은 <code>&amp;</code>의 의미를 이해하는 데 있습니다. 테일윈드의 임의 변형에서 <code>&amp;</code>는 현재 요소, 즉 클래스가 적용된 요소를 나타냅니다. 이는 Sass/SCSS나 CSS 중첩에서 <code>&amp;</code>가 작동하는 방식과 정확히 동일합니다.</p>
<p>따라서 다음과 같이 작성해 봅시다.</p>
<pre><code class="language-html">&lt;div class=&quot;[&amp;_a]:font-semibold&quot;&gt;&lt;/div&gt;</code></pre>
<p>테일윈드는 다음과 같은 CSS를 생성합니다.</p>
<pre><code class="language-css">.\[\&amp;_a\]\:font-semibold a {
  font-weight: 600;
}</code></pre>
<p>클래스 이름은 이스케이프 처리됩니다(백슬래시들…), 하지만 중요한 부분은 <code>a</code> 후손 선택자입니다. <code>&amp;</code>는 생성된 클래스 선택자로 대체된 후, 선택자(<code>a</code>)가 추가됩니다. 결과적으로 이 클래스를 가진 요소 내부의 어디에 있는 <code>&lt;a&gt;</code> 요소든 스타일이 적용됩니다.</p>
<h2 id="일반적인-패턴">일반적인 패턴</h2>
<p>다음은 유용한 자식 타겟팅 패턴입니다.</p>
<h3 id="직접-자식">직접 자식</h3>
<pre><code class="language-html">&lt;!-- 모든 하위 자식 div에 테두리와 패딩 적용 --&gt;
&lt;div class=&quot;[&amp;&gt;div]:border [&amp;&gt;div]:p-4&quot;&gt;...&lt;/div&gt;

&lt;!-- 첫 번째 하위 자식 요소는 상단 여백을 제거합니다 --&gt;
&lt;div class=&quot;[&amp;&gt;*:first-child]:mt-0&quot;&gt;...&lt;/div&gt;

&lt;!-- 마지막 자식 요소는 하단 테두리를 제거합니다 --&gt;
&lt;div class=&quot;[&amp;&gt;*:last-child]:border-b-0&quot;&gt;...&lt;/div&gt;</code></pre>
<h3 id="모든-후손-요소">모든 후손 요소</h3>
<pre><code class="language-html">&lt;!-- 모든 링크에 마우스 오버 시 밑줄 적용 --&gt;
&lt;div class=&quot;[&amp;_a]:no-underline [&amp;_a:hover]:underline&quot;&gt;...&lt;/div&gt;

&lt;!-- 모든 목록 항목에 원형 마커 적용 --&gt;
&lt;div class=&quot;[&amp;_li]:list-disc [&amp;_li]:ml-6&quot;&gt;...&lt;/div&gt;

&lt;!-- 모든 이미지에 둥근 모서리 적용 --&gt;
&lt;div class=&quot;[&amp;_img]:rounded-lg&quot;&gt;...&lt;/div&gt;</code></pre>
<p>차이점 참고: <code>&gt;</code>는 직접 자식 요소만 대상으로 하는 반면, 공백(테일윈드에서는 <code>_</code>로 표시)은 모든 후손 요소를 대상으로 합니다.</p>
<h3 id="자식-요소의-가상-상태pseudo-state">자식 요소의 가상 상태(pseudo-state)</h3>
<pre><code class="language-html">&lt;!-- 자식 요소에 대한 호버 상태 --&gt;
&lt;div class=&quot;[&amp;&gt;button:hover]:bg-blue-600&quot;&gt;...&lt;/div&gt;

&lt;!-- 비활성화된 입력란은 배경색이 어둡게 처리됨 --&gt;
&lt;form
  class=&quot;[&amp;_input:disabled]:bg-gray-100 [&amp;_input:disabled]:cursor-not-allowed&quot;
&gt;
  ...
&lt;/form&gt;

&lt;!-- 중첩된 입력 필드의 포커스 스타일 --&gt;
&lt;div class=&quot;[&amp;_input:focus]:ring-2 [&amp;_input:focus]:ring-blue-500&quot;&gt;...&lt;/div&gt;</code></pre>
<h2 id="이-방법이-적합한-경우">이 방법이 적합한 경우</h2>
<p>솔직히 말해, 내장된 콘텐츠의 스타일을 지정할 때는 기본 CSS 스타일시트가 더 나은 선택인 경우가 많습니다. 더 간단하고 가독성이 높으며 유지보수가 쉽기 때문입니다. 하지만 다음과 같은 경우에는 이 임의의 변형 접근법이 의미 있을 수 있습니다:</p>
<ul>
<li><strong>이미 테일윈드를 완전히 채택한 상태</strong>이고 CSS로 전환하는 것을 피하고 싶을 때</li>
<li><strong>규칙이 한두 개밖에 필요</strong>하지 않고 전체 스타일시트가 과도하게 느껴질 때</li>
<li><strong>빌드 파이프라인 때문에 CSS 추가</strong>가 불편할 때 (비록 이건 해결해야 할 잠재적 문제이긴 하지만)</li>
<li>콘텐츠를 렌더링하는 컴포넌트와 <strong>스타일을 같은 위치에 두고 싶을 때</strong></li>
</ul>
<p>당신이 제어할 수 있는 콘텐츠의 경우, 요소에 직접 클래스를 적용하세요. 그것이 여전히 테일윈드의 방식이며, 솔직히 말해서 이 모든 것보다 더 간단합니다.</p>
<h2 id="실용적인-예시-cms-콘텐츠">실용적인 예시: CMS 콘텐츠</h2>
<p>이 글을 쓰게 된 배경은 다음과 같습니다. 저희는 고객사에서 헤드리스 CMS의 글을 표시합니다. 콘텐츠는 미리 렌더링 된 HTML 형태로 도착하며, 저희는 이를 자체 컨테이너로 감쌉니다. 내부 마크업은 제어할 수 없습니다. CMS가 생성하는 단락, 링크, 목록, 이미지 등 무엇이든 포함될 수 있습니다. 구조와 사용된 요소(그리고 원하는 곳에 클래스를 추가할 수 없는 점)는 흥미로운 제약 조건을 추가합니다.</p>
<p>(참고: 내장된 콘텐츠는 항상 새니타이징하세요! 하지만 이 글의 범위를 벗어납니다.)</p>
<h2 id="바닐라-css-접근법-보통-최선의-선택">바닐라 CSS 접근법 (보통 최선의 선택)</h2>
<p>몇 가지 간단한 규칙을 넘어서는 경우, 전용 스타일시트를 사용하는 것이 일반적으로 더 깔끔합니다.</p>
<pre><code class="language-css">.cms-content a {
  font-weight: 600;
}
.cms-content a:hover {
  text-decoration: underline;
}
.cms-content img {
  border-radius: 0.5rem;
  max-width: 100%;
}
.cms-content li {
  list-style-type: disc;
  margin-left: 1.5rem;
}</code></pre>
<p>이는 가독성이 높고 유지보수가 용이하며 특별한 구문을 배울 필요가 없습니다. 많은 프로젝트에서 이것이 올바른 답입니다.</p>
<h2 id="테일윈드-접근법">테일윈드 접근법</h2>
<p>하지만 컴포넌트에 스타일을 유지하기로 결정했다면, 다음과 같이 구현할 수 있습니다(여느 때처럼 Elm으로 작성).</p>
<pre><code class="language-elm">viewArticleContent : List (Html msg) -&gt; Html msg
viewArticleContent content =
    Html.article
        [ Attr.class &quot;p-4&quot;
        , Attr.class &quot;[&amp;_a]:font-semibold [&amp;_a:hover]:underline&quot;
        , Attr.class &quot;[&amp;_img]:rounded-lg [&amp;_img]:max-w-full&quot;
        , Attr.class &quot;[&amp;_li]:list-disc [&amp;_li]:ml-6&quot;
        ]
        content</code></pre>
<p>또는 리액트로 구현할 수도 있습니다.</p>
<pre><code class="language-js">const ArticleContent = ({ children }) =&gt; (
  &lt;article
    className=&quot;
      p-4
      [&amp;_a]:font-semibold [&amp;_a:hover]:underline
      [&amp;_img]:rounded-lg [&amp;_img]:max-w-full
      [&amp;_li]:list-disc [&amp;_li]:ml-6
    &quot;
  &gt;
    {children}
  &lt;/article&gt;
);</code></pre>
<p>모든 스타일링은 래퍼 컴포넌트에 포함됩니다. 디자인이 변경되면 한 곳에서 클래스를 업데이트하면 됩니다.</p>
<p>테일윈드에는 <code>@tailwindcss/typography</code> 플러그인도 있는데, <code>prose</code> 클래스가 리치 텍스트 스타일을 처리합니다. 운이 좋다면 이것만으로도 충분할 수 있지만, 때로는 더 세밀한 제어가 필요하거나 기존 디자인 시스템을 맞춰야 할 때도 있습니다.</p>
<h2 id="핵심-요약">핵심 요약</h2>
<p><code>[&amp;…]</code> 구문을 사용한 임의 변형은 테일윈드의 유틸리티 클래스 패러다임 내에서 사실상 모든 CSS 선택자를 작성할 수 있게 합니다. <code>&amp;</code>는 해당 클래스가 적용된 요소를 나타내며, 그 뒤의 모든 것은 표준 CSS 선택자 구문(공백은 <code>_</code>로 대체)입니다.</p>
<p>이것이 최선의 방법일까요? 아마도 아닙니다! 내장 콘텐츠용 소규모 순수 CSS 스타일시트가 종종 더 간단하고 가독성이 높으며 팀이 유지 관리하기 쉽습니다. 테일윈드와 전통적인 CSS는 문제없이 공존할 수 있습니다.</p>
<p>하지만 테일윈드의 유틸리티 클래스 모델 내에서 작업해야 하거나(또는 해야만 한다면), 혹은 가능한 한계를 궁금해한다면, 이제 방법을 알게 되었습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[(번역) 번들러 트리 셰이킹의 원칙과 차이점]]></title>
            <link>https://velog.io/@tap_kim/bundler-tree-shaking-principles-and-differences</link>
            <guid>https://velog.io/@tap_kim/bundler-tree-shaking-principles-and-differences</guid>
            <pubDate>Wed, 03 Dec 2025 06:06:02 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>원문: <a href="https://github.com/orgs/web-infra-dev/discussions/29">https://github.com/orgs/web-infra-dev/discussions/29</a></p>
</blockquote>
<p>트리 셰이킹은 현대 프런트엔드 번들링에서 매우 중요하고 필수적인 부분이 되었습니다. 다양한 번들러 간 적용 시나리오와 중점 영역의 차이로 인해 트리 셰이킹 구현 방식도 상이합니다. 예를 들어, 웹팩(webpack)은 주로 프런트엔드 애플리케이션 번들링에 사용되며 정확성을 중시합니다. 웹팩의 트리 셰이킹은 모듈 간 수준에서의 최적화에 초점을 맞춥니다. 또한, 롤업(Rollup)도 주로 라이브러리 번들링에 사용됩니다. 하지만, 롤업은 최적화 효율성을 우선시합니다. 예를들어, AST 노드 단위의 세밀한 수준에서 트리 셰이킹을 수행하여 일반적으로 결과물의 번들 크기가 더 작습니다. 그러나 <a href="https://github.com/rollup/rollup/issues/3888">특정</a> <a href="https://github.com/evanw/esbuild/issues/399">극단적인 경우</a>의 실행 정확성에 대한 보장이 부족할 수 있습니다.</p>
<p>이 글은 다양한 번들러의 트리 셰이킹 원리와 그 차이점에 대한 간략한 개요를 제공합니다.</p>
<blockquote>
<p><a href="https://rspack.rs/guide/optimization/tree-shaking">트리 셰이킹</a>이란 무엇인가: 애플리케이션을 나무로 상상해 보세요. 실제로 사용하는 소스 코드와 라이브러리는 나무의 푸르고 살아있는 잎을 나타냅니다. 데드 코드는 가을에 떨어져 버려지는 갈색의 죽은 잎을 상징합니다. 죽은 잎을 제거하려면 나무를 흔들어 떨어뜨려야 합니다.</p>
</blockquote>
<h2 id="웹팩rspack에서의-트리-셰이킹">웹팩/Rspack에서의 트리 셰이킹</h2>
<blockquote>
<p>현재 <a href="http://rspack.rs/">Rspack</a>(v1.4)은 웹팩과 동일한 트리 셰이킹 구현 방식을 따르고 있으므로, 아래 설명에서는 웹팩의 구현 방식을 예시로 사용하겠습니다. 동시에 향후 Rspack 버전에서는 더 효율적인 트리 셰이킹 전략을 적극적으로 모색 중입니다.</p>
</blockquote>
<p>웹팩의 트리 셰이킹은 세 부분으로 구성됩니다.</p>
<ul>
<li><p>모듈 수준: <code>optimization.sideEffects</code> 사용되지 않은 내보내기와 사이드 이펙트가 없는 모듈을 제거합니다.</p>
<ul>
<li><p><code>import “./module”;</code> 내보내기된 항목을 사용하지 않으며 사이드 이펙트가 없으므로 <code>./module</code>을 안전하게 제거할 수 있습니다.</p>
</li>
<li><p><code>re-exports.js</code> (배럴 파일) 자체는 로컬 내보내기 항목이 없으며 사용되고 있고 사이드 이펙트가 없으므로 re-exports.js를 안전하게 제거할 수 있습니다.</p>
<pre><code class="language-js">// index.js
import { a } from &quot;./re-exports&quot;;
console.log(a);

// re-exports.
export * from &quot;./module&quot;;
// `index -(a)-&gt; re-exports -(a)-&gt; module`이 `index -(a)-&gt; module`로 최적화되었으며, re-exports.js 자체는 로컬 내보내기가 없으며 사용되고 있고, 사이드 이펙트가 없으므로 re-exports.js는 안전하게 제거할 수 있습니다.

// module.js
export const a = 42;</code></pre>
</li>
</ul>
</li>
<li><p>export-level: <code>optimization.providedExports</code> 및 <code>optimization.usedExports</code>를 사용하여 사용되지 않는 내보내기 항목 제거</p>
<ul>
<li><code>optimization.providedExports</code>: 모듈이 제공하는 내보내기 항목 분석</li>
<li><code>optimization.usedExports</code>: 모듈 내보내기 항목 중 실제 사용되는 항목 분석. 코드 생성 시 미사용 내보내기 제거 가능. <code>export const a = 42</code> =&gt; <code>const a = 42</code>. 이후 SWC나 Terser 같은 미니마이저가 해당 변수가 모듈 내부에서도 미사용일 경우 남은 선언을 추가로 제거할 수 있음</li>
</ul>
</li>
<li><p>코드 레벨: <code>optimization.minimize</code> SWC나 Terser 같은 코드 압축기를 사용해 인라인 처리 및 평가 같은 기법으로 코드를 분석하고, 불필요한 코드를 제거하며 압축을 수행하여 번들 크기를 최대한 줄입니다.</p>
<ul>
<li><code>optimization.minimize</code> 플러그인을 통해 코드 압축기가 번들러와 통합되며, 번들러의 핵심 책임 범위를 벗어난 번들러 출력물에 대한 후처리를 수행합니다.</li>
</ul>
</li>
</ul>
<p>또한 정적 분석도 중요한 부분입니다. 모듈 레벨 및 내보내기 레벨 최적화 모두 웹팩이 코드에 대한 정적 분석을 수행하여 모듈에 사이드 이펙트가 있는지, 어떤 내보내기가 포함되어 있는지, 그리고 실제로 사용되는 내보내기가 무엇인지 판단해야 합니다. 이 정보는 최적화 단계의 입력값으로 활용됩니다.</p>
<p>이론적으로, 웹팩의 자바스크립트 파서가 이러한 정보를 정적으로 내보내기 할 수 있다면 트리 셰이킹은 적용 가능합니다. 본질적으로 동적인 CommonJS 및 동적 임포트에도 마찬가지입니다. 이러한 구문이 정적 패턴을 따르고 필요한 정보를 정적으로 추론할 수 있다면 트리 셰이킹은 여전히 가능합니다.</p>
<p>그러나 현재 웹팩은 CJS와 동적 임포트를 포함한 극히 제한된 시나리오에 대해서만 정적 분석을 수행합니다. 분석 가능한 많은 사례가 최적화되지 않은 채로 남아 있어 개선의 여지가 상당합니다.</p>
<p>이 세 단계가 완료되면 트리 셰이킹은 이미 기능하지만, 특정 사례에서는 여전히 문제가 발생할 수 있습니다.</p>
<ul>
<li><p>다른 모듈의 사용되지 않은 내보내기 <code>g</code>가 내보내기 <code>a</code>를 참조하고 있어 <code>a</code>가 트리 셰이킹되지 못합니다. 이 경우 <code>lib.js</code> 내 최상위 문장 간의 의존성 관계를 분석하려면 <code>optimization.innerGraph</code>가 필요합니다. <code>a</code>를 포함하는 최상위 문장이 사용될 때만 내보내기 <code>a</code>는 사용된 것으로 표시되며, 그렇지 않으면 사용되지 않은 것으로 간주됩니다.</p>
<pre><code class="language-js">// index.js
import { f } from &quot;./lib&quot;;
f();

// lib.js
import { a } from &quot;./module&quot;;
export const f = () =&gt; 42;
export const g = () =&gt; a; // `g`가 사용되지 않으므로 `const g = () =&gt; a`를 생성하면 `a`가 참조되어 `a`가 트리 셰이킹되지 못하게 됩니다.

// module.js
export const a = 42;</code></pre>
</li>
<li><p>웹팩이 각 모듈을 함수로 래핑하는 런타임 특성으로 인해, 일부 최적화 가능한 경우도 미니파이어로 최적화할 수 없습니다.
예를 들어, 다음과 같은 경우 아래처럼 처리할 수 있습니다.</p>
<pre><code class="language-js">// index.js
import { aVeryLongLongLongName } from &quot;./constants.js&quot;;
console.log(aVeryLongLongLongName ? 1 : 2);

// constants.js
export const aVeryLongLongLongName = 42;

// 웹팩 번들링 후
// output.js
const __webpack_modules__ = {
  &quot;./index.js&quot;: (__webpack_require__) =&gt; {
    const _constants__WEBPACK_MODULE__ =
      __webpack_require__(&quot;./constants.js&quot;);
    console.log(_constants__WEBPACK_MODULE__.A ? 1 : 2);
  },
  &quot;./constants.js&quot;: (__webpack_require__) =&gt; {
    __webpack_require__.d({
      A: () =&gt; A,
    });
    const A = 42;
  },
};</code></pre>
<ul>
<li><p><a href="https://devongovett.me/blog/scope-hoisting.html#:~:text=Scope%20hoisting%20was,between%20different%20bundles.">일반적으로 <code>optimization.concatenateModules</code>를 통해 해결할 수 있으나, 복잡한 시나리오에서는 연결이 가능한 모듈 수가 상당히 제한되는 경향이 있습니다.</a></p>
<pre><code class="language-js">// `optimization.concatenateModules`를 활성화하면 출력이 압축 도구에 더 적합해져 더 나은 압축과 더 작은 번들 크기를 가능하게 합니다.
// 연결된 모듈: ./constants.js
const A = 42; // 연결된 모듈: ./index.js
console.log(A ? 1 : 2); // 이제 이 코드는 압축기로 최적화할 수 있습니다.</code></pre>
</li>
</ul>
</li>
<li><p>미니파이어는 망글링을 통해 변수 이름을 효과적으로 줄일 수 있지만, 이 래퍼 함수는 내보내기 모듈 변수의 망글링을 방지합니다. 이때 <code>optimization.mangleExports</code>가 망글링 처리를 담당합니다.</p>
<pre><code class="language-diff">// output.js
const __webpack_modules__ = {
&quot;./index.js&quot;: (__webpack_require__) =&gt; {
    const _module__WEBPACK_MODULE__ = __webpack_require__(&quot;./constants.js&quot;);
-    console.log(_constants__WEBPACK_MODULE__.aVeryLongLongLongName ? 1 : 2);
+    console.log(_constants__WEBPACK_MODULE__.a ? 1 : 2);
},
&quot;./constants.js&quot;: (__webpack_require__) =&gt; {
    __webpack_require__.d({
-      aVeryLongLongLongName: () =&gt; aVeryLongLongLongName,
+      a: () =&gt; aVeryLongLongLongName,
    })
    const aVeryLongLongLongName = 42;
}
}</code></pre>
</li>
<li><p>추가적인 최적화, 예를 들어 Rspack v1.4에서 도입된 <a href="https://rspack.rs/zh/config/module#moduleparserjavascriptinlineconst"><code>experiments.inlineConst</code></a> 등이 있습니다.</p>
<pre><code class="language-js">// output.js
const __webpack_modules__ = {
  &quot;./index.js&quot;: (__webpack_require__) =&gt; {
    console.log(42 ? 1 : 2); // 이제 이 코드는 압축기로 최적화할 수 있습니다.
  },
};</code></pre>
</li>
</ul>
<h2 id="esbuild에서의-트리-셰이킹">esbuild에서의 트리 셰이킹</h2>
<p>esbuild의 트리 셰이킹은 다음 단계를 포함합니다.</p>
<ol>
<li>각 모듈을 최상위 문으로 분할하고, 각 최상위 문을 하나의 파트로 처리합니다.</li>
<li>각 파트에서 정의된 변수와 다른 파트에서 사용되는 변수를 분석한 후, 모듈 임포트(import)를 해당 내보내기에 연결합니다.</li>
<li>진입 모듈의 부분부터 상향식 탐색을 수행합니다. 다른 부분에서 사용되거나 사이드 이펙트를 가지는 부분은 <code>IsLive = true</code>로 표시합니다.</li>
<li>코드 생성 시 <code>IsLive = true</code>로 표시된 부분만 포함하고, 표시되지 않은 부분은 제거합니다.</li>
</ol>
<p><img src="https://velog.velcdn.com/images/tap_kim/post/d4d67fe2-7615-4917-a8d3-bb903d3c5c2e/image.png" alt=""></p>
<blockquote>
<p>자세한 내용은 <a href="https://github.com/evanw/esbuild/blob/2ba0f0233497ebe9f355aa0e7a12729560f43320/docs/architecture.md#tree-shaking">esbuild/docs/architecture/tree-shaking</a>을 참조하십시오.</p>
</blockquote>
<p>esbuild는 최상위 레벨 문장을 미리 분할함으로써 본질적으로 innerGraph 문제를 해결합니다. 분할 후 각 상위 레벨 문장은 파트로 전환되어, 모듈 레벨에서 작동하는 웹팩과 달리 파트 레벨 세분화에서 분석 및 최적화가 가능해집니다. 이 접근 방식은 다음과 같은 주요 이점을 제공합니다.</p>
<ul>
<li>imported/exported 변수뿐만 아니라 모듈 내 변수도 분석하여, 웹팩이 innerGraph를 통해 수행해야 하는 작업을 포괄합니다.</li>
<li>모듈 내 최상위 문장(부분 레벨)에 모듈 수준의 번들러 최적화를 적용합니다. 예를 들어, esbuild의 사이드 이펙트 최적화는 사용되지 않고 사이드 이펙트가 없는 최상위 문장을 제거할 수 있지만, 웹팩은 모듈 수준 제거만 수행할 수 있습니다.</li>
</ul>
<p>초기 버전의 esbuild도 이러한 최상위 문이 코드 분할(현재 “모듈 분할”이라 부름)에 참여하도록 허용했습니다. 그러나 esbuild는 모듈 로딩과 실행을 분리하는 웹팩 출력처럼 각 모듈을 함수로 감싸지 않아 최상위 await 처리에 어려움을 겪었습니다. <a href="https://github.com/evanw/esbuild/pull/1130">결국 esbuild는 모듈 분할 지원을 중단했습니다.</a></p>
<p>현재 <a href="https://github.com/evanw/esbuild/blob/main/docs/architecture.md#code-splitting">esbuild/docs/architecture/code-splitting</a>은 여전히 모듈 분할을 지원하는 버전을 설명합니다. 코드 분할은 파트 레벨에서 수행되며, 공유 청크에는 두 진입점 모두에 공통된 최상위 문만 포함됩니다. <a href="https://esbuild.github.io/try/#YgAwLjI1LjUALS10cmVlLXNoYWtpbmcgLS1vdXRkaXI9b3V0IC0tZm9ybWF0PWVzbSAtLWJ1bmRsZSAtLXNwbGl0dGluZwBlAGluZGV4LmpzAGltcG9ydCB7bG9hZCwgc2F2ZX0gZnJvbSAnLi9jb25maWcnCgpsZXQgZWwgPSBkb2N1bWVudC5nZXRFbGVtZW50QnlJZCgnZWwnKQpsb2FkKCkudGhlbih4ID0+IGVsLnRleHRDb250ZXh0ID0geCkAZQBzZXR0aW5ncy5qcwBpbXBvcnQge2xvYWQsIHNhdmV9IGZyb20gJy4vY29uZmlnJwoKbGV0IGl0ID0gZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoJ2l0JykKaXQub25pbnB1dCA9ICgpID0+IHNhdmUoaXQudmFsdWUpAABjb25maWcuanMAaW1wb3J0IHtnZXQsIHB1dH0gZnJvbSAnLi9uZXQnOwoKbGV0IHNlc3Npb24gPSBNYXRoLnJhbmRvbSgpCmxldCBhcGkgPSAnL2FwaT9zZXNzaW9uPScKCmV4cG9ydCBmdW5jdGlvbiBsb2FkKCkgewogIHJldHVybiBnZXQoYXBpICsgc2Vzc2lvbikKfQoKZXhwb3J0IGZ1bmN0aW9uIHNhdmUodmFsdWUpIHsKICBwdXQoYXBpICsgc2Vzc2lvbiwgdmFsdWUpCn0AAG5ldC5qcwBleHBvcnQgZnVuY3Rpb24gZ2V0KHVybCkgewogIHJldHVybiBmZXRjaCh1cmwpLnRoZW4ociA9PiByLnRleHQoKSkKfQoKZXhwb3J0IGZ1bmN0aW9uIHB1dCh1cmwsIGJvZHkpIHsKICBmZXRjaCh1cmwsIHttZXRob2Q6ICdQVVQnLCBib2R5fSkKfQoKZXhwb3J0IGZ1bmN0aW9uIHBvc3QodXJsLCBib2R5KSB7CiAgZmV0Y2godXJsLCB7bWV0aG9kOiAnUE9TVCcsIGJvZHl9KQp9">esbuild 플레이그라운드의 복제본</a>을 보면 공유 청크에 두 진입점이 공유하는 모듈이 포함되어 있으며, 코드 분할이 모듈 레벨에서 이루어짐을 확인할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/tap_kim/post/7b31e8fc-9c5f-43bf-8525-f1e7992c42af/image.png" alt="">
| <em>모듈 분할을 지원하기 이전의 결과</em></p>
<p><img src="https://velog.velcdn.com/images/tap_kim/post/f0598270-ac6f-4ac5-be30-b361a3b664b3/image.png" alt="">
| <em>이제 더 이상 모듈 분할을 지원하지 않는 결과</em></p>
<p>또한 최상위 await를 제외하고, esbuild의 모듈 분할에서 발생한 또 다른 복잡성은 모듈 로딩과 실행을 분리하지 않았기 때문이었습니다. ES 모듈의 정적 임포트는 읽기 전용입니다. <a href="https://github.com/evanw/esbuild/blob/main/docs/architecture.md#tree-shaking:~:text=There%20is%20one,these%20three%20files%3A">이는 코드 분할 과정에서 esbuild가 변수 할당을 해당 변수 선언과 다른 청크로 이동시킬 수 없음을 의미했습니다.</a> esbuild가 모듈 분할을 포기한 후, 이 문제는 더 이상 특별한 처리가 필요하지 않게 되었습니다.</p>
<h2 id="터보팩turbopack에서의-트리-셰이킹">터보팩(Turbopack)에서의 트리 셰이킹</h2>
<p>모듈 분할은 실제로 웹팩과 같은 번들러에 더 적합합니다. 웹팩은 모듈 로딩과 실행을 분리할 수 있어, 모듈 내 변수 선언 및 사용에 대한 추가 처리 없이도 모듈 실행의 정확성을 본질적으로 보장합니다.</p>
<p>또한 모듈 내 최상위 문장에 더 많은 모듈 레벨 최적화를 적용할 수 있게 합니다. 예를 들어 esbuild가 지원하지 않거나 성능이 떨어지는 최적화들. 코드 분할, 런타임 최적화, 청크 분할 등, 이를 통해 추가적인 최적화가 가능해집니다.</p>
<p>그렇다면 웹팩과 유사한 런타임(모듈 로딩과 실행 분리)과 모듈 분할 지원을 결합한 번들러가 있을까요? 바로 터보팩(Turbopack)입니다.</p>
<p>먼저, 터보팩의 출력 형식이 어떻게 챕터 간 변수 할당 및 선언을 허용하지 않는 문제를 해결하는지, esbuild 번들링 결과 예시를 통해 살펴보겠습니다.</p>
<pre><code class="language-js">// 추신: 터보팩의 관련 코드는 단순화 및 수정되었습니다. 현재 터보팩은 청크 분할 결과를 세밀하게 제어할 수 있는 구성을 아직 제공하지 않습니다.

// chunk for entry1.js
module.exports = {
  &quot;entry1.js&quot;: (`__turbopack_context__`) =&gt; {
    var _data_1__TURBOPACK_MODULE__ =
      __turbopack_context__.i(&quot;data.js &lt;part 1&gt;&quot;);
    console.log(_data_1__TURBOPACK_MODULE__.data);
  },
};
// chunk for entry2.js
module.exports = {
  &quot;entry2.js&quot;: (__turbopack_context__) =&gt; {
    var _data_2__TURBOPACK_MODULE__ =
      __turbopack_context__.i(&quot;data.js &lt;part 2&gt;&quot;);
    _data_2__TURBOPACK_MODULE__.setData(123);
  },
  &quot;data.js &lt;part 2&gt;&quot;: (__turbopack_context__) =&gt; {
    __turbopack_context__.s({
      setData: () =&gt; setData,
    });
    var _data_1__TURBOPACK_MODULE__ =
      __turbopack_context__.i(&quot;data.js &lt;part 1&gt;&quot;);
    function setData(value) {
      _data_1__TURBOPACK_MODULE__.data = value; // &lt;--- 여기서 setter 트리거
    }
  },
};
// 공유 코드용 청크
module.exports = {
  &quot;data.js &lt;part 1&gt;&quot;: (__turbopack_context__) =&gt; {
    __turbopack_context__.s({
      data: [
        () =&gt; data, // &lt;--- getter
        (new_data) =&gt; (data = new_data), // &lt;--- setter
      ],
    });
    let data;
  },
};</code></pre>
<p>터보팩은 웹팩과 유사한 출력 형식을 사용하며, 각 모듈을 함수로 감싸 모듈 로딩과 실행을 분리합니다. 그러나 웹팩과 달리 모듈 내보내기를 정의하는 런타임 <code>__turbopack_context__.s</code>는 내보내기 값을 가져오는 getter뿐만 아니라 추가적인 setter도 포함합니다. 모듈의 다른 부분에서 이러한 변수에 할당 작업을 수행하면 해당 setter가 트리거 되어 값을 업데이트함으로써 올바른 실행을 보장합니다.</p>
<p>최상위 await의 경우, 웹팩과 마찬가지로 터보팩은 런타임을 활용하여 최상위 await를 포함하는 모듈과 그 의존성 모듈의 올바른 실행 순서를 보장합니다. 예를 들어, data.js의 첫 번째 줄에 <code>await 1;</code>을 추가하면 번들링 된 출력은 다음과 같습니다.</p>
<pre><code class="language-js">// 공유 코드용 청크
module.exports = {
  // ...
  &quot;data.js &lt;part 0&gt;&quot;: (__turbopack_context__) =&gt; {
    __turbopack_context__.a(
      async (
        __turbopack_handle_async_dependencies__,
        __turbopack_async_result__
      ) =&gt; {
        try {
          await 1; // &lt;--- 추가된 `await 1;`은 최상위 await가 올바르게 실행되도록 보장하기 위해 __turbopack_context__.a 런타임에 의해 감싸집니다.
          __turbopack_async_result__();
        } catch (e) {
          __turbopack_async_result__(e);
        }
      },
      true
    );
  },
  // ...
};</code></pre>
<p>물론 모듈 분할에도 단점이 존재합니다. 분할 후 출력물의 최상위 문장 각각은 함수로 감싸집니다. 이는 올바른 실행을 보장하지만, 이러한 래핑 방식의 단점을 증폭시킵니다.</p>
<ol>
<li>이러한 래퍼 함수의 과도한 중복은 번들 크기를 증가시킵니다(gzip으로 이 오버헤드를 효과적으로 줄일 수 있음).</li>
<li><code>_data_0__TURBOPACK_MODULE__.data</code>와 같은 객체 속성을 통해 접근해야 하는 변수가 증가하여 런타임 성능이 저하될 수 있습니다(이는 여전히 최신 브라우저 벤치마크를 통한 검증이 필요합니다).</li>
</ol>
<p>두 문제 모두 최적화를 위해 스코프 호이스팅에 대한 의존도를 높여야 합니다.</p>
<h2 id="롤업rollup에서의-트리-셰이킹">롤업(Rollup)에서의 트리 셰이킹</h2>
<p>웹팩이 내보내기 문과 모듈에 대해 트리 셰이킹을 수행하고, esbuild가 최상위 문에 대해 이를 수행한다면, 롤업은 모든 문과 더 세분화된 AST 노드에 대해 상위에서 하위로 트리 셰이킹을 수행하며, 더 정밀한 사이드 이펙트 감지 기능을 제공합니다.</p>
<p><img src="https://velog.velcdn.com/images/tap_kim/post/cfa79eb0-951f-488c-bcf8-58f36c26d33e/image.png" alt="">
| <em>Rollup은 문장과 일부 더 세분화된 AST 노드에 대해 트리 셰이킹을 수행합니다.</em></p>
<p>Rollup의 트리 셰이킹은 esbuild와 유사하지만 약간의 차이가 있습니다. 프로세스는 다음과 같이 진행됩니다.</p>
<ol>
<li><p>모듈에 <code>include()</code>를 호출하여 시작합니다.</p>
</li>
<li><p>최상위 AST 노드부터 시작합니다. <br/>
a. AST 노드에 사이드 이펙트가 있는지 판단합니다. <br/>
b. 있다면 <code>include()</code>를 호출합니다. <br/>
c. 관련 AST 노드에 대해 사이드 이펙트 확인과 <code>include()</code>를 계속 수행합니다(a, b 단계 반복).</p>
</li>
<li><p>탐색 후, new AST 노드가 <code>include()</code>된 경우 새 탐색을 트리거합니다(1, 2단계).</p>
</li>
</ol>
<blockquote>
<p>2.c 단계에서 “관련 노드”란 다음을 의미합니다. 특정 노드의 자식 노드, 변수 사용에 대응하는 변수 선언 노드, 그리고 객체 <code>obj</code>의 선언 및 속성 <code>a</code>와 <code>b</code>에 접근할 때 <code>obj.a.b</code>와 같은 속성에 대한 노드.</p>
</blockquote>
<p>롤업은 다음과 같은 이유로 다른 번들러에 비해 더 우수한 트리 셰이킹 결과를 달성합니다.</p>
<ol>
<li>더 세분화된 분석: 롤업은 AST 노드 레벨에서 코드를 분석하고 제거하는 반면, 다른 번들러는 일반적으로 최상위 문 레벨 또는 더 거친 세분성으로 작동하여 더 세분화된 불필요한 코드 제거(DCE)는 미니파이어에 맡깁니다.</li>
<li>보다 정밀한 사이드 이펙트 분석: 사이드 이펙트는 컨텍스트를 인식하며 AST 노드 레벨에서 평가됩니다. 반면 다른 번들러들은 더 거친 수준에서 컨텍스트와 무관하게 사이드 이펙트를 분석합니다.</li>
</ol>
<p>롤업의 세분화된 접근 방식은 본질적으로 내보내기 및 최상위 문에 대한 거친 분석을 포함합니다. 결과적으로 Rollup은 모듈 간 미사용 내보내기를 제거할 뿐만 아니라 모듈 내 DCE도 처리할 수 있습니다. 다음 논의는 모듈 내 DCE에 초점을 맞추며, 구체적인 사례를 통해 이 두 가지 점을 설명하겠습니다.</p>
<ol>
<li><a href="https://rollupjs.org/repl/?version=4.44.0&amp;shareable=JTdCJTIyZXhhbXBsZSUyMiUzQW51bGwlMkMlMjJtb2R1bGVzJTIyJTNBJTVCJTdCJTIyY29kZSUyMiUzQSUyMmltcG9ydCUyMCU3QiUyMERFVkVMT1BNRU5UJTIwJTdEJTIwZnJvbSUyMCU1QyUyMi4lMkZmaWxlJTVDJTIyJTVDbiU1Q25mdW5jdGlvbiUyMG1haW4oKSUyMCU3QiU1Q24lMjAlMjBpZiUyMChERVZFTE9QTUVOVCklMjAlN0IlNUNuJTIwJTIwJTIwJTIwY29uc29sZS5sb2coJTVDJTIyZGV2JTVDJTIyKSU1Q24lMjAlMjAlN0QlMjBlbHNlJTIwJTdCJTVDbiUyMCUyMCUyMCUyMGNvbnNvbGUubG9nKCU1QyUyMnByb2QlNUMlMjIpJTVDbiUyMCUyMCU3RCU1Q24lN0QlNUNuJTVDbm1haW4oKSUyMiUyQyUyMmlzRW50cnklMjIlM0F0cnVlJTJDJTIybmFtZSUyMiUzQSUyMm1haW4uanMlMjIlN0QlMkMlN0IlMjJjb2RlJTIyJTNBJTIyZXhwb3J0JTIwY29uc3QlMjBERVZFTE9QTUVOVCUyMCUzRCUyMHRydWUlM0IlMjIlMkMlMjJpc0VudHJ5JTIyJTNBZmFsc2UlMkMlMjJuYW1lJTIyJTNBJTIyZmlsZS5qcyUyMiU3RCU1RCUyQyUyMm9wdGlvbnMlMjIlM0ElN0IlMjJvdXRwdXQlMjIlM0ElN0IlMjJmb3JtYXQlMjIlM0ElMjJlcyUyMiU3RCUyQyUyMnRyZWVzaGFrZSUyMiUzQSUyMnJlY29tbWVuZGVkJTIyJTdEJTdE">롤업 크로스 모듈 분석을 통해 최상위 레벨이 아닌 죽은 분기 제거</a></li>
</ol>
<p><img src="https://velog.velcdn.com/images/tap_kim/post/bf8e943d-2c9a-48b6-a31b-a8a64d0b4f87/image.png" alt=""></p>
<p>Rollup의 Define 기능은 다른 번들러와 다르게 구현됩니다. <a href="https://github.com/rollup/plugins/tree/master/packages/replace">rollup-plugin-replace</a>는 변환 단계에서 일치하는 Define 노드만 대체하는 반면, 웹팩과 esbuild는 파싱 단계에서 대체를 수행하며 죽은 분기도 분석합니다. 죽은 분기의 코드는 분석 과정에서 제외되며, 이 분기에 포함된 종속성은 모듈 그래프에 포함되지 않습니다. 그러나 파싱 단계에서의 이 죽은 분기 분석은 모듈 간에 걸쳐 수행될 수 없습니다.</p>
<p>Rollup의 트리 셰이킹은 AST 노드 레벨에서 작동하므로 함수 내부의 문장 노드도 트리 셰이킹 분석 대상이 될 수 있습니다. 따라서 Rollup은 분석의 일부를 트리 셰이킹 단계로 위임하며, 죽은 분기 제거 역시 트리 셰이킹에 의존합니다.</p>
<p>이 예시에서는 <code>if (DEVELOPMENT)</code> 내 <code>DEVELOPMENT</code> 변수에 대해 컴파일 시 평가를 시도합니다. 그 결과는 상수이므로 else 분기는 죽은 분기로 제거될 수 있습니다. 또한 <code>file.js</code>의 <code>DEVELOPMENT</code> 변수는 사용된 것으로 표시되지 않아 최종 트리 셰이킹에서 <code>export const DEVELOPMENT</code> 선언과 <code>file.js</code> 모듈을 제거할 수 있습니다.</p>
<p>이 접근 방식의 단점은, 죽은(branch가 타지 않는) 분기 안에서 도입된 의존성들도 여전히 모듈 그래프에 포함된다는 점입니다. 그 결과 더 많은 모듈이 끌려오고, Rollup이 처리해야 할 일이 늘어나 성능이 떨어지게 됩니다.</p>
<p>반면, 장점은 모듈을 가로지르는(cross-module) 분석과 죽은 분기 제거가 가능해진다는 것입니다. 이를 통해, 더 많은 미사용 코드를 제거하고 결과적으로 더 작은 출력 번들을 생성한다는 점입니다. 다른 번들러들은 스코프 호이스팅을 통해 모듈을 단일 스코프로 병합하고, 이러한 크로스 모듈 죽은 분기를 제거하기 위해 미니파이어에 의존합니다. 그러나 올바른 실행을 보장하기 위해 스코프 호이스팅을 포기해야 하는 모듈의 경우, 이러한 모듈을 최적화할 좋은 해결책이 없습니다. 이를 해결하기 위해서는 향후 최적화가 필요할 수 있습니다.</p>
<ol start="2">
<li><a href="https://rollupjs.org/repl/?version=4.44.0&amp;shareable=JTdCJTIyZXhhbXBsZSUyMiUzQW51bGwlMkMlMjJtb2R1bGVzJTIyJTNBJTVCJTdCJTIyY29kZSUyMiUzQSUyMmNvbnN0JTIwb2JqJTIwJTNEJTIwJTdCJTVDbiUyMCUyMGElM0ElMjAlN0IlMjBhYSUzQSUyMDElMkMlMjBhYiUzQSUyMDIlMjAlN0QlMkMlNUNuJTIwJTIwYiUzQSUyMCU3QiUyMGJhJTNBJTIwMSUyQyUyMGJiJTNBJTIwMiUyMCU3RCUyQyU1Q24lN0QlNUNuJTVDbmNvbnNvbGUubG9nKG9iai5hLmFiKSUyMiUyQyUyMmlzRW50cnklMjIlM0F0cnVlJTJDJTIybmFtZSUyMiUzQSUyMm1haW4uanMlMjIlN0QlNUQlMkMlMjJvcHRpb25zJTIyJTNBJTdCJTIyb3V0cHV0JTIyJTNBJTdCJTIyZm9ybWF0JTIyJTNBJTIyZXMlMjIlN0QlMkMlMjJ0cmVlc2hha2UlMjIlM0ElMjJyZWNvbW1lbmRlZCUyMiU3RCU3RA==">롤업은 사용되지 않는 객체 속성을 제거합니다.</a></li>
</ol>
<p><img src="https://velog.velcdn.com/images/tap_kim/post/1ab8c25a-f0e5-4abe-944d-10828cb4046c/image.png" alt=""></p>
<p><code>console.log(obj.a.ab)</code>를 분석할 때 Rollup은 이 문장이 사이드 이펙트를 일으키므로 <code>include()</code> 대상으로 표시합니다. <code>include(obj.a.ab)</code> 실행 시 <code>obj</code> 선언 노드, <code>a:</code> 속성 노드, <code>ab:</code> 속성 노드 등 관련 노드들의 <code>include()</code>를 트리거합니다. AST 노드 레벨 트리 셰이킹 덕분에 Rollup은 사용된 <code>a:</code> 및 <code>ab:</code> 속성만 유지하고 다른 미사용 속성은 제거하여 더 작은 출력 번들을 생성할 수 있습니다.</p>
<ol start="3">
<li><a href="https://rollupjs.org/repl/?version=4.44.0&amp;shareable=JTdCJTIyZXhhbXBsZSUyMiUzQW51bGwlMkMlMjJtb2R1bGVzJTIyJTNBJTVCJTdCJTIyY29kZSUyMiUzQSUyMmxldCUyMGElMjAlM0QlMjAlN0IlN0QlM0IlNUNuJTJGJTJGJTIwYSUyMCUzRCUyMCU3QiU3RCUzQiU1Q25hLmIlMjAlM0QlMjAzJTNCJTIyJTJDJTIyaXNFbnRyeSUyMiUzQXRydWUlMkMlMjJuYW1lJTIyJTNBJTIybWFpbi5qcyUyMiU3RCUyQyU3QiUyMmNvZGUlMjIlM0ElMjIlMjIlMkMlMjJpc0VudHJ5JTIyJTNBZmFsc2UlMkMlMjJuYW1lJTIyJTNBJTIyZm9vLmpzJTIyJTdEJTVEJTJDJTIyb3B0aW9ucyUyMiUzQSU3QiUyMm91dHB1dCUyMiUzQSU3QiUyMmZvcm1hdCUyMiUzQSUyMmVzJTIyJTdEJTJDJTIydHJlZXNoYWtlJTIyJTNBJTIyc21hbGxlc3QlMjIlN0QlN0Q=">롤업은 재할당을 기반으로 사이드 이펙트를 결정합니다.</a></li>
</ol>
<p><img src="https://velog.velcdn.com/images/tap_kim/post/b681009e-30ce-4688-a416-bc777e7b5145/image.png" alt=""></p>
<p>   이 경우 <code>a = {}</code>를 주석 처리하면 모든 코드가 트리 셰이킹되는 것을 확인할 수 있습니다. 그러나 주석을 해제하면 트리 셰이킹이 더 이상 발생하지 않습니다. 이는 Rollup이 <a href="https://github.com/rollup/rollup/blob/5a7f9e215a11de165b85dafd64350474847ec6db/src/ast/variables/LocalVariable.ts#L195">변수 <code>a</code>가 재할당되는지 여부에 따라 <code>a.b = 3</code>이 사이드 이펙트를 가지는지 판단</a>하기 때문입니다. 이는 Rollup이 문맥 인식형 사이드 이펙트 분석을 수행할 수 있음을 보여줍니다. 다만 문맥 독립형 사이드 이펙트 분석에 비해 일정한 성능 오버헤드가 발생합니다.</p>
<p>   그러나 이러한 문맥 인식형 사이드 이펙트 분석은 비교적 단순하며 일반적으로 특정 단순한 시나리오에서만 작동합니다. 예를 들어 위 사례에서는 재할당이 발생하는지만 확인하고, 재할당이 실제로 의미 있는 변화를 유발하는지 분석하지 않습니다. 즉 지나치게 깊거나 세부적인 분석을 피한다는 의미입니다.</p>
<p><img src="https://velog.velcdn.com/images/tap_kim/post/d27a13b5-337c-4da1-990c-fbce57ad546e/image.png" alt=""></p>
<p>   | <em>동일한 변수를 실제 변경 없이 재할당하더라도 <code>a.b = 3</code>은 여전히 사이드 이펙트를 가진 것으로 간주됩니다.</em></p>
<p>롤업 v3은 문장 수준 트리 셰이킹만 지원했지만, v4부터는 더 세분화된 AST 노드 레벨 트리 셰이킹(위에서 언급한 객체 속성 트리 셰이킹 등)을 실험하기 시작했으며, 특정 시나리오에서 사용되지 않는 노드 추적을 최적화하기 시작했습니다. 예를 들어봅시다.</p>
<ul>
<li><a href="https://github.com/rollup/rollup/pull/4510">트리 셰이킹 기능의 기본 매개변수</a>, 다만 이후 너무 많은 예외 처리 시나리오로 인해 이 기능은 되돌려졌습니다.</li>
<li><a href="https://github.com/rollup/rollup/pull/5443">&quot;상수 매개변수&quot;를 기반으로 함수 내부의 죽은 분기를 제거하는 트리 셰이킹</a>. 이 기능은 함수 기본 매개변수 트리 셰이킹에서 발전되었습니다. 동일한 변수를 사용하여 매개변수가 변경되지 않은 상태로 함수가 한 번 또는 여러 번 호출될 때, 매개변수에 사용된 변수를 분석하고 이를 기반으로 특정 최적화를 적용합니다.</li>
</ul>
<p>이를 통해 더 많은 시나리오에서 트리 셰이킹이 가능해집니다.</p>
<ul>
<li><a href="https://m.webtoo.ls/@lukastaegert/113394861668205066">롤업 객체 속성 트리 셰이킹에 관한 게시물</a></li>
<li><a href="https://m.webtoo.ls/@lukastaegert/112307436291880884">const 매개변수를 통한 롤업의 트리 셰이킹 개선에 관한 게시물</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[(번역) 99%의 개발자가 모르는 ARIA 속성]]></title>
            <link>https://velog.io/@tap_kim/99-of-developers-dont-know-these-aria-attributes-exist</link>
            <guid>https://velog.io/@tap_kim/99-of-developers-dont-know-these-aria-attributes-exist</guid>
            <pubDate>Mon, 10 Nov 2025 16:08:45 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>원문: <a href="https://dev.to/web_dev-usman/99-of-developers-dont-know-these-aria-attributes-exist-19j0?context=digest">https://dev.to/web_dev-usman/99-of-developers-dont-know-these-aria-attributes-exist-19j0?context=digest</a></p>
</blockquote>
<h2 id="2025년에-꼭-필요한-유일한-aria-속성-가이드">2025년에 꼭 필요한 유일한 ARIA 속성 가이드</h2>
<p>접근성은 더이상 선택 사항이 아닙니다. 요즘은 모두를 위해 동작하는 웹사이트를 만드는 것이 그 어느 때보다 쉬워졌습니다. <code>ARIA 속성</code>은 그 중심에 있고, 그 힘은 대부분의 개발자들이 생각하는 것보다 훨씬 큽니다.</p>
<p>이 글에서는 존재하는 모든 ARIA 속성을 하나하나 설명해 드리겠습니다. 인기 있는 속성만 다루는 게 아니라 완전하고 철저한 목록을 다룰 것입니다. 문서 속에서 먼지만 쌓여가는 생소한 사양이 아닙니다. 모던 웹 애플리케이션을 구축할 때 실제 문제를 해결하는 실용적인 도구들입니다.</p>
<p><strong>더 진행하기 전에, 실용적인 예제가 담긴 CodePen 링크를 알려드립니다.</strong></p>
<p>!codepen[web-strategist/embed/YPwvLLW?default-tab=result]</p>
<h2 id="aria-속성이란-정확히-무엇인가요">ARIA 속성이란 정확히 무엇인가요?</h2>
<p>ARIA는 접근 가능한 리치 인터넷 애플리케이션(<code>Accessible Rich Internet Applications</code>)을 의미합니다. <code>ARIA</code>의 핵심은 스크린 리더와 같은 보조 기술과 소통할 수 있는 방법을 제공합니다. 사용자 정의 드롭다운이나 화려한 모달을 구축할 때, 코드는 그 요소가 무엇인지 알지만 스크린 리더는 알지 못합니다. <code>ARIA</code>는 그 간극을 메웁니다.</p>
<p>이렇게 생각해 보세요. <code>div</code>와 <code>CSS</code>를 사용해 아름다운 탭 인터페이스를 만들었습니다. 시각적으로는 완벽하지만, 스크린 리더를 사용하는 사람은 그것이 탭이라는 사실을 전혀 알 수 없습니다. 적절한 <code>ARIA 속성</code>을 추가하면, 갑자기 그 스크린 리더가 전체 상호작용 패턴을 이해하게 됩니다.</p>
<h2 id="접근성이-실제로-중요한-이유">접근성이 실제로 중요한 이유</h2>
<p>전 세계적으로 10억 명 이상의 사람들이 어떤 형태로든 장애를 가지고 있으며, 이는 전 세계 인구의 약 15~16%에 해당합니다. 많은 사람들이 웹을 사용하기 위해 스크린 리더, 음성 제어, 키보드 탐색 또는 기타 보조 기술에 의존합니다. 적절한 ARIA 구현이 없다면, 사이트가 겉보기에 멋질지 몰라도, 이 사용자들에게는 완전히 사용할 수 없는 사이트가 됩니다다.</p>
<p>접근성은 단순히 규정 준수나 소송 회피를 위한 것이 아닙니다(물론 그것들도 중요한 문제이긴 합니다). 접근성이 보장된 웹사이트는 더 나은 웹사이트입니다. 의미론적으로 더 명확하고, 키보드로 탐색하기 쉬우며, 구조가 더 잘 잡혀 있고, 종종 검색 엔진에서도 더 좋은 성과를 내기도 합니다.</p>
<h2 id="aria의-첫-번째-규칙">ARIA의 첫 번째 규칙</h2>
<p>이 방대한 목록을 깊이 있게 살펴보기 전에, 반드시 알아야 할 것이 있습니다. <strong>의미론적 HTML이 항상 우선</strong>입니다.</p>
<p><code>&lt;button&gt;</code> 요소가 존재할 때 <code>&lt;div role=&quot;button&quot;&gt;</code>을 사용하지 마십시오. HTML이 이미 그 역할을 수행하고 있다면 ARIA로 불필요한 작업을 반복하지 마십시오. 기본 요소는 접근성, 키보드 지원, 예상되는 동작이 내장되어 있습니다. ARIA는 HTML이 부족한 부분을 채우기 위한 것입니다.</p>
<h2 id="존재하는-모든-aria-속성">존재하는 모든 ARIA 속성</h2>
<p>자, 여기 완전한 목록이 있습니다. 각 속성을 언제, 왜 사용해야 하는지 이해할 수 있도록 용도별로 분류했습니다.</p>
<h2 id="역할roles-요소의-본질-정의">역할(Roles): 요소의 본질 정의</h2>
<p>역할 속성은 보조 기술에 해당 요소가 정확히 어떤 유형인지 알려줍니다.</p>
<h3 id="랜드마크-역할">랜드마크 역할</h3>
<p>페이지의 주요 섹션을 정의합니다.</p>
<ul>
<li><code>role=“banner”</code> 메인 헤더 영역</li>
<li><code>role=“navigation”</code> 모든 네비게이션 메뉴</li>
<li><code>role=“main”</code> 주요 콘텐츠</li>
<li><code>role=“complementary”</code> 사이드바 같은 보조 콘텐츠</li>
<li><code>role=“contentinfo”</code> 푸터 콘텐츠</li>
<li><code>role=“search”</code> 검색 영역</li>
<li><code>role=“form”</code> 양식 영역</li>
<li><code>role=“region”</code> 식별할 가치가 있는 중요한 섹션</li>
</ul>
<h3 id="문서-구조-역할">문서 구조 역할</h3>
<p>콘텐츠가 어떻게 구성되었는지 설명합니다.</p>
<ul>
<li><code>role=“article”</code> 독립적인 구성 요소</li>
<li><code>role=“document”</code> 문서 콘텐츠</li>
<li><code>role=“feed”</code> 스크롤 가능한 기사 목록(소셜 미디어 피드 등)</li>
<li><code>role=“figure”</code> 이미지, 다이어그램, 코드 스니펫</li>
<li><code>role=“img”</code> 이미지 컨테이너</li>
<li><code>role=“list”</code> 항목 목록</li>
<li><code>role=“listitem”</code> 개별 목록 항목</li>
<li><code>role=“math”</code> 수학 표기법</li>
<li><code>role=“none”</code> 또는 <code>role=“presentation”</code> 의미적 의미 제거</li>
<li><code>role=“note”</code> 각주 또는 부가 설명</li>
<li><code>role=“table”</code> 표 형식 데이터</li>
<li><code>role=“row”</code> 표 행</li>
<li><code>role=“rowgroup”</code> 행 그룹 (thead, tbody, tfoot)</li>
<li><code>role=“cell”</code> 표 셀</li>
<li><code>role=“columnheader”</code> 열 머리글</li>
<li><code>role=“rowheader”</code> 행 헤더</li>
<li><code>role=“separator”</code> 시각적 구분선</li>
<li><code>role=“toolbar”</code> 컨트롤이 있는 툴바</li>
<li><code>role=“tooltip”</code> 컨텍스트 팝업 정보</li>
</ul>
<h3 id="위젯-역할">위젯 역할</h3>
<p>모든 인터랙티브 컴포넌트에 적용됩니다.</p>
<ul>
<li><code>role=“button”</code> 버튼</li>
<li><code>role=“checkbox”</code> 체크박스</li>
<li><code>role=“radio”</code> 라디오 버튼</li>
<li><code>role=“textbox”</code> 텍스트 입력란</li>
<li><code>role=“searchbox”</code> 검색 입력란</li>
<li><code>role=“switch”</code> 토글 스위치</li>
<li><code>role=“slider”</code> 범위 슬라이더</li>
<li><code>role=“spinbutton”</code> 숫자 스피너</li>
<li><code>role=“combobox”</code> 콤보 박스 (입력 + 드롭다운)</li>
<li><code>role=“listbox”</code> 옵션 목록</li>
<li><code>role=“option”</code> 개별 옵션</li>
<li><code>role=“menu”</code> 메뉴 위젯</li>
<li><code>role=“menubar”</code> 메뉴 바</li>
<li><code>role=“menuitem”</code> 메뉴 항목</li>
<li><code>role=“menuitemcheckbox”</code> 선택 가능한 메뉴 항목</li>
<li><code>role=“menuitemradio”</code> 라디오 버튼 메뉴 항목</li>
<li><code>role=“tab”</code> 탭 컨트롤</li>
<li><code>role=“tablist”</code> 탭 컨테이너</li>
<li><code>role=“tabpanel”</code> 탭 콘텐츠 패널</li>
<li><code>role=“tree”</code> 트리 뷰</li>
<li><code>role=“treeitem”</code> 트리 항목</li>
<li><code>role=“treegrid”</code> 편집 가능한 트리 그리드</li>
<li><code>role=“grid”</code> 대화형 그리드</li>
<li><code>role=“gridcell”</code> 그리드 셀</li>
<li><code>role=“link”</code> 링크</li>
<li><code>role=“progressbar”</code> 진행률 표시기</li>
<li><code>role=“scrollbar”</code> 스크롤바</li>
</ul>
<h3 id="복합composite-역할">복합(Composite) 역할</h3>
<p>항목을 그룹화하기 위한 역할입니다.</p>
<ul>
<li><code>role=“group”</code> 일반 그룹</li>
<li><code>role=“radiogroup”</code> 라디오 버튼 그룹</li>
<li><code>role=“rowgroup”</code> 테이블의 행 그룹</li>
</ul>
<h3 id="라이브-영역live-region-역할">라이브 영역(Live Region) 역할</h3>
<p>동적으로 변경되는 콘텐츠를 위한 역할입니다.</p>
<ul>
<li><code>role=“alert”</code> 긴급 메시지</li>
<li><code>role=“log”</code> 추가되는 로그</li>
<li><code>role=“marquee”</code> 비필수 업데이트</li>
<li><code>role=“status”</code> 상태 메시지</li>
<li><code>role=“timer”</code> 타이머 및 카운트다운</li>
</ul>
<h3 id="window-역할">Window 역할</h3>
<p>모달 상호작용을 위한 역할입니다.</p>
<ul>
<li><code>role=“dialog”</code> 대화 상자</li>
<li><code>role=“alertdialog”</code> 경고 대화 상자</li>
</ul>
<h3 id="추상-역할-절대-사용하지-마세요">추상 역할 (절대 사용하지 마세요)</h3>
<p><code>command</code>, <code>composite</code>, <code>input</code>, <code>landmark</code>, <code>range</code>, <code>roletype</code>, <code>section</code>, <code>sectionhead</code>, <code>select</code>, <code>structure</code>, <code>widget</code>, <code>window</code> 사양에는 존재하지만 직접 사용하기 위한 용도는 아닙니다.</p>
<h2 id="위젯-속성-상호작용-설명">위젯 속성: 상호작용 설명</h2>
<p>사용자가 요소와 상호작용하는 방법을 알려줍니다.</p>
<ul>
<li><code>aria-autocomplete=“none|inline|list|both”</code> 자동 완성 동작</li>
<li><code>aria-checked=“true|false|mixed”</code> 선택 상태</li>
<li><code>aria-disabled=“true|false”</code> 비활성화 상태</li>
<li><code>aria-errormessage=“[ID]”</code> 오류 메시지 링크</li>
<li><code>aria-expanded=“true|false|undefined”</code> 확장 상태</li>
<li><code>aria-haspopup=“true|false|menu|listbox|tree|grid|dialog”</code> 팝업 표시</li>
<li><code>aria-hidden=“true|false|undefined”</code> 보조 기술에 대한 가시성</li>
<li><code>aria-invalid=“true|false|grammar|spelling”</code> 유효성 검사 상태</li>
<li><code>aria-label=“[string]”</code> 접근 가능한 레이블</li>
<li><code>aria-level=“[integer]”</code> 계층적 수준</li>
<li><code>aria-modal=“true|false”</code> 모달 상태</li>
<li><code>aria-multiline=“true|false”</code> 다중 줄 입력</li>
<li><code>aria-multiselectable=“true|false”</code> 다중 선택</li>
<li><code>aria-orientation=“horizontal|vertical|undefined”</code> 방향</li>
<li><code>aria-placeholder=“[string]”</code> 플레이스홀더 힌트</li>
<li><code>aria-pressed=“true|false|mixed|undefined”</code> 토글 상태</li>
<li><code>aria-readonly=“true|false”</code> 읽기 전용 상태</li>
<li><code>aria-required=“true|false”</code> 필수 입력 필드</li>
<li><code>aria-selected=“true|false|undefined”</code> 선택 상태</li>
<li><code>aria-sort=“ascending|descending|none|other”</code> 정렬 방향</li>
<li><code>aria-valuemax=“[number]”</code> 최댓값</li>
<li><code>aria-valuemin=“[number]”</code> 최솟값</li>
<li><code>aria-valuenow=“[number]”</code> 현재값</li>
<li><code>aria-valuetext=“[string]”</code> 사람이 읽을 수 있는 값</li>
</ul>
<h2 id="라이브-영역-속성-동적-콘텐츠-처리">라이브 영역 속성: 동적 콘텐츠 처리</h2>
<p>사용자에게 변경 사항을 알리는 방식을 제어합니다.</p>
<ul>
<li><code>aria-atomic=“true|false”</code> 전체 영역 또는 변경 사항만 알림</li>
<li><code>aria-busy=“true|false”</code> 로딩 상태</li>
<li><code>aria-live=“off|polite|assertive”</code> 알림 긴급도</li>
<li><code>off</code> 알림 없음</li>
<li><code>polite</code> 유휴 시 알림</li>
<li><code>assertive</code> 알림을 위해 중단</li>
<li><code>aria-relevant=“additions|removals|text|all”</code> - 알릴 변경 사항 유형</li>
</ul>
<h2 id="드래그-앤-드롭-속성">드래그 앤 드롭 속성</h2>
<ul>
<li><code>aria-dropeffect=“copy|move|link|execute|popup|none”</code> 드롭 효과 (ARIA 1.1에서 사용 중단됨)</li>
<li><code>aria-grabbed=“true|false|undefined”</code> 드래그 상태 (ARIA 1.1에서 사용 중단됨)</li>
</ul>
<h2 id="관계-속성-요소-연결">관계 속성: 요소 연결</h2>
<p>요소 간 의미적 관계를 생성합니다.</p>
<ul>
<li><code>aria-activedescendant=“[ID]”</code> 현재 활성화된 자식 요소</li>
<li><code>aria-colcount=“[integer]”</code> 총 열 수</li>
<li><code>aria-colindex=“[integer]”</code> 열 위치</li>
<li><code>aria-colspan=“[integer]”</code> 병합된 열 수</li>
<li><code>aria-controls=“[ID list]”</code> 제어되는 요소</li>
<li><code>aria-describedby=“[ID list]”</code> 설명 출처</li>
<li><code>aria-details=“[ID]”</code> 상세 설명</li>
<li><code>aria-flowto=“[ID list]”</code> 읽기 순서 재정의</li>
<li><code>aria-labelledby=“[ID list]”</code> 레이블 출처</li>
<li><code>aria-owns=“[ID list]”</code> 소유된 자식 요소</li>
<li><code>aria-posinset=“[integer]”</code> 세트 내 위치</li>
<li><code>aria-rowcount=“[integer]”</code> 총 행 수</li>
<li><code>aria-rowindex=“[integer]”</code> 행 위치</li>
<li><code>aria-rowspan=“[integer]”</code> 행 스팬</li>
<li><code>aria-setsize=“[integer]”</code> 세트 크기</li>
</ul>
<h2 id="전역-속성-어디서나-작동">전역 속성: 어디서나 작동</h2>
<ul>
<li><code>aria-atomic</code> (위에서 다룸)</li>
<li><code>aria-busy</code> (위에서 다룸)</li>
<li><code>aria-controls</code> (위에서 다룸)</li>
<li><code>aria-current=“page|step|location|date|time|true|false”</code> 현재 항목 표시기</li>
<li><code>aria-describedby</code> (위에서 다룸)</li>
<li><code>aria-details</code> (위에서 다룸)</li>
<li><code>aria-disabled</code> (위에서 다룸)</li>
<li><code>aria-dropeffect</code> (사용 중단됨)</li>
<li><code>aria-errormessage</code> (위에서 다룸)</li>
<li><code>aria-flowto</code> (위에서 다룸)</li>
<li><code>aria-grabbed</code> (사용 중단됨)</li>
<li><code>aria-haspopup</code> (위에서 다룸)</li>
<li><code>aria-hidden</code> (위에서 다룸)</li>
<li><code>aria-invalid</code> (위에서 다룸)</li>
<li><code>aria-keyshortcuts=“[string]”</code> - 키보드 단축키</li>
<li><code>aria-label</code> (위에서 다룸)</li>
<li><code>aria-labelledby</code> (위에서 다룸)</li>
<li><code>aria-live</code> (위에서 다룸)</li>
<li><code>aria-owns</code> (위에서 다룸)</li>
<li><code>aria-relevant</code> (위에서 다룸)</li>
<li><code>aria-roledescription=“[string]”</code> 사용자 정의 역할 설명</li>
</ul>
<h2 id="매일-사용할-실용적인-예시">매일 사용할 실용적인 예시</h2>
<p>실제 코드에서 이 기능이 어떻게 작동하는지 살펴보겠습니다.</p>
<h2 id="예시-1-커스텀-토글-버튼">예시 1: 커스텀 토글 버튼</h2>
<pre><code class="language-html">&lt;div
  role=&quot;button&quot;
  tabindex=&quot;0&quot;
  aria-pressed=&quot;false&quot;
  onclick=&quot;this.setAttribute(&#39;aria-pressed&#39;, this.getAttribute(&#39;aria-pressed&#39;) === &#39;false&#39;)&quot;
&gt;
  Dark Mode
&lt;/div&gt;</code></pre>
<p><strong>버튼이 이미 있다면 div를 버튼으로 절대 사용하지 마세요</strong></p>
<h2 id="예시-2-접근성-모달-대화상자">예시 2: 접근성 모달 대화상자</h2>
<pre><code class="language-html">&lt;div
  role=&quot;dialog&quot;
  aria-modal=&quot;true&quot;
  aria-labelledby=&quot;dialog-title&quot;
  aria-describedby=&quot;dialog-desc&quot;
&gt;
  &lt;h2 id=&quot;dialog-title&quot;&gt;Delete Item?&lt;/h2&gt;
  &lt;p id=&quot;dialog-desc&quot;&gt;This action cannot be undone.&lt;/p&gt;
  &lt;button&gt;Delete&lt;/button&gt;
  &lt;button&gt;Cancel&lt;/button&gt;
&lt;/div&gt;</code></pre>
<h2 id="예제-3-양식form-유효성-검사">예제 3: 양식(Form) 유효성 검사</h2>
<pre><code class="language-html">&lt;label for=&quot;email&quot;&gt;Email Address&lt;/label&gt;
&lt;input
  type=&quot;email&quot;
  id=&quot;email&quot;
  aria-required=&quot;true&quot;
  aria-invalid=&quot;true&quot;
  aria-describedby=&quot;email-error&quot;
/&gt;
&lt;span id=&quot;email-error&quot; role=&quot;alert&quot;&gt; Please enter a valid email address &lt;/span&gt;</code></pre>
<h2 id="예시-4-확장-가능한-섹션">예시 4: 확장 가능한 섹션</h2>
<pre><code class="language-html">&lt;button aria-expanded=&quot;false&quot; aria-controls=&quot;content-panel&quot;&gt;
  Show More Details
&lt;/button&gt;
&lt;div id=&quot;content-panel&quot; hidden&gt;Additional content here...&lt;/div&gt;</code></pre>
<h2 id="예시-5-실시간-검색-결과">예시 5: 실시간 검색 결과</h2>
<pre><code class="language-html">&lt;label for=&quot;search&quot;&gt;Search Products&lt;/label&gt;
&lt;input
  id=&quot;search&quot;
  type=&quot;search&quot;
  aria-controls=&quot;results&quot;
  aria-describedby=&quot;results-count&quot;
/&gt;
&lt;div id=&quot;results&quot; role=&quot;region&quot; aria-live=&quot;polite&quot;&gt;
  &lt;span id=&quot;results-count&quot;&gt;12 products found&lt;/span&gt;
  &lt;!-- 리스트 결과는 여기에 --&gt;
&lt;/div&gt;</code></pre>
<h2 id="접근성을-저해하는-실수">접근성을 저해하는 실수</h2>
<h3 id="의미론적-html을-절대-대체하지-마세요">의미론적 HTML을 절대 대체하지 마세요</h3>
<pre><code class="language-html">&lt;!-- ❌ --&gt;
&lt;div role=&quot;button&quot; onclick=&quot;submit()&quot;&gt;Submit&lt;/div&gt;</code></pre>
<pre><code class="language-html">&lt;!-- ✅ --&gt;
&lt;button onclick=&quot;submit()&quot;&gt;Submit&lt;/button&gt;</code></pre>
<h3 id="상태를-동기화-상태로-유지하세요">상태를 동기화 상태로 유지하세요</h3>
<pre><code class="language-html">&lt;!-- ❌ - 상태는 절대 업데이트되지 않습니다 --&gt;
&lt;button aria-expanded=&quot;false&quot; onclick=&quot;toggle()&quot;&gt;Menu&lt;/button&gt;</code></pre>
<pre><code class="language-html">&lt;!-- ✅ - 상호작용을 통한 상태 업데이트 --&gt;
&lt;button
  aria-expanded=&quot;false&quot;
  onclick=&quot;this.setAttribute(&#39;aria-expanded&#39;, this.getAttribute(&#39;aria-expanded&#39;) === &#39;false&#39;)&quot;
&gt;
  Menu
&lt;/button&gt;</code></pre>
<h3 id="상호작용-가능한-콘텐츠를-절대-숨기지-마세요">상호작용 가능한 콘텐츠를 절대 숨기지 마세요</h3>
<pre><code class="language-html">&lt;!-- ❌ - 버튼이 보조 기술에서 숨겨져 있습니다 --&gt;
&lt;button aria-hidden=&quot;true&quot;&gt;Important Action&lt;/button&gt;</code></pre>
<pre><code class="language-html">&lt;!-- ✅ - 데코레이티브(decorative) 요소만 숨기기 --&gt;
&lt;span aria-hidden=&quot;true&quot;&gt;★&lt;/span&gt;&lt;button&gt;Rate 5 Stars&lt;/button&gt;</code></pre>
<h3 id="절대-일을-복잡하게-만들지-마세요">절대 일을 복잡하게 만들지 마세요</h3>
<pre><code class="language-html">&lt;!-- ❌ - 불필요한 aria-label --&gt;
&lt;h1 aria-label=&quot;“Welcome”&quot;&gt;Welcome&lt;/h1&gt;</code></pre>
<pre><code class="language-html">&lt;!-- ✅ - 텍스트 콘텐츠가 이미 접근 가능함 --&gt;
&lt;h1&gt;Welcome&lt;/h1&gt;</code></pre>
<h3 id="역할role-충돌을-피하십시오">역할(role) 충돌을 피하십시오</h3>
<pre><code class="language-html">&lt;!-- ❌ - 의미가 혼란스러움 --&gt;
&lt;button role=&quot;heading&quot;&gt;네비게이션 항목&lt;/button&gt;</code></pre>
<pre><code class="language-html">&lt;!-- ✅ - 적절한 요소 사용 --&gt;
&lt;h2&gt;네비게이션&lt;/h2&gt;
&lt;button&gt;네비게이션 항목&lt;/button&gt;</code></pre>
<h2 id="구현-테스트">구현 테스트</h2>
<p>ARIA를 추가한다고 해서 제대로 작동할 거라고 기대해서는 안 됩니다. 검증 방법은 다음과 같습니다.</p>
<ol>
<li><strong>브라우저 개발자 도구</strong>: Chrome과 Firefox에는 접근성 검사기가 내장되어 있습니다</li>
<li><strong>스크린 리더</strong>: NVDA(Windows), JAWS(Windows), VoiceOver(Mac/iOS), TalkBack(Android)으로 테스트하세요</li>
<li><strong>자동화 도구</strong>: axe DevTools, WAVE 또는 Lighthouse 감사 실행</li>
<li><strong>키보드 탐색</strong>: Tab, Enter, Space, 화살표 키만으로 사이트 사용 시도</li>
<li><strong>실제 사용자</strong>: 가장 신뢰할 수 있는 방법은 실제 보조 기술 사용자와 함께 테스트하는 것입니다</li>
</ol>
<h2 id="일반-시나리오에-대한-빠른-참조">일반 시나리오에 대한 빠른 참조</h2>
<h3 id="라벨링-요소">라벨링 요소</h3>
<ul>
<li><code>aria-label</code> 직접 라벨 텍스트</li>
<li><code>aria-labelledby</code> 다른 요소의 텍스트 참조</li>
<li><code>aria-describedby</code> 추가 설명</li>
</ul>
<h3 id="상호작용-상태">상호작용 상태</h3>
<ul>
<li><code>aria-expanded</code> 확장 가능한 섹션용</li>
<li><code>aria-pressed</code> 토글 버튼용</li>
<li><code>aria-checked</code> 사용자 정의 체크박스용</li>
<li><code>aria-selected</code> 선택된 옵션용</li>
</ul>
<h3 id="오류-처리">오류 처리</h3>
<ul>
<li><code>aria-invalid</code> 유효하지 않은 필드 표시</li>
<li><code>aria-errormessage</code> 오류 텍스트 링크</li>
<li><code>role=&quot;alert&quot;</code> 즉시 오류 알림</li>
</ul>
<h3 id="동적-콘텐츠">동적 콘텐츠</h3>
<ul>
<li><code>aria-live=“polite”</code> 적절한 시점에 알림</li>
<li><code>aria-live=“assertive”</code> 즉시 알림</li>
<li><code>aria-busy=“true”</code> 로딩 중</li>
</ul>
<h3 id="탐색">탐색</h3>
<ul>
<li><code>aria-current=“page”</code> 탐색 내 현재 페이지</li>
<li><code>aria-hidden=“true”</code> 장식적 콘텐츠 숨김</li>
<li><code>aria-controls</code> 콘텐츠로의 링크 트리거</li>
</ul>
<h2 id="마지막으로">마지막으로</h2>
<p>이 방대한 목록을 보면 ARIA가 부담스러울 수 있습니다. 하지만 알아두셔야 할 점은 모든 속성을 외울 필요는 없다는 것입니다. <code>aria-label</code>, <code>aria-expanded</code>, <code>aria-hidden</code>, <code>aria-live</code> 같은 일반적인 속성부터 시작하세요. 프로젝트에서 필요할 때마다 나머지 속성을 배우면 됩니다.</p>
<p>진정으로 중요한 것은 <code>ARIA</code>의 존재 이유를 이해하는 것입니다. ARIA는 접근 방식에 관계없이 모든 사람이 웹을 사용할 수 있도록 하기 위해 존재합니다. 적절한 ARIA 속성을 추가할 때마다 여러분의 웹사이트는 더 많은 사람들에게 열립니다. 바로 이것이 웹이 추구해야 할 본질입니다.</p>
<p>그러니 작은 것부터 시작하세요. 현재 프로젝트에서 하나의 구성 요소를 선택하세요. 올바른 ARIA 속성을 추가하세요. 스크린 리더로 테스트해 보세요. 얼마나 빨리 익숙해지는지 놀라실 겁니다.</p>
<p>모두가 사용할 수 있을 때 웹은 더 나아집니다. 이제 여러분은 이를 실현할 도구를 갖췄습니다.</p>
<h2 id="참고">참고</h2>
<!-- 저자 요청사항 -->

<ul>
<li>Linkedin: <a href="https://open.substack.com/users/316483922-m-usman?utm_source=mentions">M Usman</a></li>
<li>Substack: <a href="https://developersjourney.substack.com/">Developer’s Journey</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[(번역) 시멘틱 HTML이 여전히 중요한 이유]]></title>
            <link>https://velog.io/@tap_kim/why-semantic-html-still-matters</link>
            <guid>https://velog.io/@tap_kim/why-semantic-html-still-matters</guid>
            <pubDate>Mon, 01 Sep 2025 13:02:05 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>원문: <a href="https://www.jonoalderson.com/conjecture/why-semantic-html-still-matters/">https://www.jonoalderson.com/conjecture/why-semantic-html-still-matters/</a></p>
</blockquote>
<p>언제부턴가 우리는 HTML을 어떻게 작성해야 하는지, 그리고 그것이 왜 중요한지를 잊어버렸습니다.</p>
<p>최신 개발 워크플로우에서는 컴포넌트, 유틸리티 클래스, 자바스크립트 중심의 렌더링에 우선순위를 두고 있습니다. HTML은 <em>기반</em>이 아닌 <em>부산물</em>이 되어버렸습니다.</p>
<p>그리고 이런 변화는 성능, 접근성, 복원력은 물론 기계와 사람이 콘텐츠를 해석하는 방식에도 악영향을 미칩니다.</p>
<p><a href="https://www.jonoalderson.com/conjecture/javascript-broke-the-web-and-called-it-progress/">자바스크립트가 웹을 어떻게 죽이고 있는지</a> 대해서는 다른 글에서 설명한 적이 있습니다. 하지만 해당 글에서 가장 쉽게 고칠 수 있으면서도 간과되는 부분은 <strong>시멘틱 HTML</strong>입니다.</p>
<p>이 글에서는 우리가 무엇을 잃었는지, 그리고 그것이 왜 여전히 중요한지에 대해 설명합니다.</p>
<h2 id="시멘틱-html은-기계가-의미를-이해하는-방식이다">시멘틱 HTML은 기계가 의미를 이해하는 방식이다.</h2>
<p>HTML은 단순히 페이지에 요소를 배치하는 방식이 아닙니다. 의미를 표현하는 어휘가 있는 <em>언어</em>입니다.</p>
<p><code>&lt;article&gt;</code>, <code>&lt;nav&gt;</code>, <code>&lt;section&gt;</code>과 같은 태그는 <em>장식용</em>이 아닌 <em>의도</em>를 표현하고, <em>계층 구조</em>를 나타냅니다. 해당 태그들은 기계에게 당신의 콘텐츠가 무엇인지, 그리고 그것이 다른 모든 콘텐츠와 어떻게 <em>연관</em>되어 있는지 알려줍니다.</p>
<p>검색 엔진, 접근성 도구, AI 에이전트, 작업 기반 시스템은 모두 구조적 신호에 의존합니다. 때로는 명시적으로, 때로는 휴리스틱(경험적 추론) 방식으로 말입니다. 모든 시스템에 완벽한 마크업이 필요한 것은 아니지만, 시멘틱 HTML을 활용할 수 있는 경우, 그 구조와 뜻은 명확하게 전달될 수 있습니다. 체계적이지 않고 구조가 분명하지 않은 웹 환경에서, 구조와 의미가 뚜렷하게 드러나는 페이지는 오히려 더 많은 기회와 경쟁력을 가지게 됩니다.</p>
<p>시멘틱 마크업이 더 나은 색인(indexing)이나 추출을 보장하지는 않지만, 현재와 미래에 시스템이 활용할 수 있는 기반을 만들어 줍니다. 이는 <em>품질</em>, <em>구조</em>, <em>의도</em>를 나타내는 신호이기도 합니다.</p>
<p>모든 것이 <code>&lt;div&gt;</code> 또는 <code>&lt;span&gt;</code>으로만 이루어져 있다면, 아무 의미가 없습니다.</p>
<h2 id="단순히-나쁜-html이-아니라-의미-없는-마크업입니다">단순히 나쁜 HTML이 아니라, 의미 없는 마크업입니다.</h2>
<p>이 문제를 단순히 &#39;코드의 순수함&#39;의 문제라고 치부한다면, <code>&lt;div&gt;</code>를 쓰든 <code>&lt;section&gt;</code>을 쓰든, 겉으로 보기에 멀쩡하면 그만이고 과연 누가 신경 쓸까요?</p>
<p>하지만 이건 꼼꼼함을 넘어선 문제입니다. 의미 없는 마크업은 단지 사이트를 읽기 어렵게 만드는 것을 넘어서, <em>렌더링</em>(표현)이나 <em>유지보수</em>, <em>확장</em>도 어렵게 만듭니다.</p>
<p>이런 추상화는 종종 다음과 같은 마크업을 만들어냅니다.</p>
<pre><code class="language-html">&lt;div class=&quot;tw-bg-white tw-p-4 tw-shadow tw-rounded-md&quot;&gt;
  &lt;div class=&quot;tw-flex tw-flex-col tw-gap-2&quot;&gt;
    &lt;div class=&quot;tw-text-sm tw-font-semibold tw-uppercase tw-text-gray-500&quot;&gt;
      ACME Widget
    &lt;/div&gt;
    &lt;div class=&quot;tw-text-xl tw-font-bold tw-text-blue-900&quot;&gt;Blue Widget&lt;/div&gt;
    &lt;div class=&quot;tw-text-md tw-text-gray-700&quot;&gt;
      Our best-selling widget for 2025. Lightweight, fast, and dependable.
    &lt;/div&gt;
    &lt;div class=&quot;tw-mt-4 tw-flex tw-items-center tw-justify-between&quot;&gt;
      &lt;div class=&quot;tw-text-lg tw-font-bold&quot;&gt;$49.99&lt;/div&gt;
      &lt;button
        class=&quot;tw-bg-blue-600 tw-text-white tw-px-4 tw-py-2 tw-rounded hover:tw-bg-blue-700&quot;
      &gt;
        Buy now
      &lt;/button&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;</code></pre>
<p>물론 이 코드는 잘 작동합니다. 스타일이 지정되었고, 렌더링도 됩니다. 하지만 의미론적으로 죽어있습니다.</p>
<p>이 콘텐츠는 제품 목록인가요? 블로그 게시물인가요? 아니면 클릭을 유도하는 컨텐츠인가요? 이 콘텐츠가 무엇인지 알 수 있는 단서가 전혀 없습니다.</p>
<p>스크린 리더, 크롤러 또는 가격 데이터를 추출하려는 에이전트 역시 한눈에 알 수 없습니다.</p>
<p>아래는 의미 있는 구조를 가진 동일한 내용입니다.</p>
<pre><code class="language-html">&lt;article class=&quot;product-card&quot;&gt;
  &lt;header&gt;
    &lt;p class=&quot;product-brand&quot;&gt;ACME Widget&lt;/p&gt;
    &lt;h1 class=&quot;product-name&quot;&gt;Blue Widget&lt;/h1&gt;
  &lt;/header&gt;
  &lt;p class=&quot;product-description&quot;&gt;
    Our best-selling widget for 2025. Lightweight, fast, and dependable.
  &lt;/p&gt;
  &lt;footer class=&quot;product-footer&quot;&gt;
    &lt;span class=&quot;product-price&quot;&gt;$49.99&lt;/span&gt;
    &lt;button class=&quot;buy-button&quot;&gt;Buy now&lt;/button&gt;
  &lt;/footer&gt;
&lt;/article&gt;</code></pre>
<p>이제 마크업에선 <em>구조</em>가 생기고, <em>의도</em>가 드러나는 <em>스토리</em>를 담고 있습니다. 당신의 CSS를 타깃 할 수 있고, 스크래퍼로 추출할 수 있으며, 스크린 리더기로 탐색할 수 있습니다. 이건 의미가 있는 거죠.</p>
<p>시멘틱 HTML은 접근성의 기반입니다. 구조와 의미가 없으면 보조 기술이 콘텐츠를 해석할 수 없게 됩니다. 스크린 리더기는 무엇을 읽어야 할지 모르고 키보드 사용자는 막히게 됩니다. 음성 인터페이스는 div 태그에 묻혀 있는 것을 찾을 수 없습니다. 깔끔하고 의미 있는 HTML은 단순히 좋은 관행이 아닙니다. 바로 사람들이 웹에 접근하는 방식입니다.</p>
<p>프레임워크 자체가 본질적으로 나쁘거나 접근성이 안 좋다는 말은 아닙니다. 테일윈드, 아토믹 클래스, 인라인 스타일은 특히 복잡한 프로젝트나 대규모 팀에서 매우 유용할 수 있습니다. 이런 것들은 인지적 부담을 덜어주고, 작업 속도를 향상 시킬 수 있습니다.</p>
<p>하지만 그것들은 <em>도구</em>일 뿐, 근본적인 <em>해답</em>은 아닙니다. 그리고 모든 구성 요소가 거의 동일한 유틸리티 클래스의 혼란스러운 집합체로 변하거나, 레이아웃이나 브레이크포인트 조정이 추가되면 목적이 흐려지게 되고, 구조가 사라지면 목적이 불분명해집니다.</p>
<p>이것은 추상화에 관한 이야기가 아닙니다. 그 과정에서 우리가 잃게 되는 것에 대한 이야기입니다.</p>
<p>그리고 그 손실은 의미론적 측면에만 영향을 미치는 것이 아닌 <em>성능</em>에도 영향을 미칩니다. 사실상 모던 웹은 어느 때보다 더 느리고, 더 무거우며, 더 취약하게 느껴지는 가장 큰 이유 중의 하나입니다.</p>
<h2 id="의미론적-부패가-성능을-망가뜨립니다">의미론적 부패가 성능을 망가뜨립니다.</h2>
<p>우리는 일반적으로 HTML이 단순히 렌더링 대상일 뿐이라고 생각해 왔습니다. 브라우저에 임의의 마크업을 던져 넣어도 알아서 처리해 줄 거라고 믿는 거죠. 그리고 실제로도 그렇습니다. 브라우저는 놀랍게도 우리가 엉망진창으로 만들어도 잘 고쳐줍니다.</p>
<p>그러나 그 관용에는 <em>대가</em>가 따릅니다.</p>
<p>렌더링 엔진은 결함-허용(fault-tolerant)으로 설계되었습니다. 역할 추론, 나쁜 구조 보완 등 의도한 대로 렌더링을 시도합니다. 그러나 매번 <code>&lt;div&gt;</code>의 난잡한 구조가 무엇을 의미하는지 추측할 때마다 시간이 소모됩니다. 이는 CPU 사이클과 GPU 시간을 의미합니다. 특히 모바일 환경에서는 전력 소모로 이어집니다.</p>
<p>이제 이런 비효율이 실제로 어디서, 어떻게 문제가 되어, 왜 신경 써야 하는지 차근차근 살펴봅시다.</p>
<h2 id="큰-dom은-렌더링-속도가-느립니다">큰 DOM은 렌더링 속도가 느립니다.</h2>
<p>DOM의 모든 단일 노드는 오버헤드를 발생합니다. 렌더링 과정에서 브라우저는 DOM 트리를 탐색하고, CSSOM을 구축하며, 스타일을 계산하고, 레이아웃을 해결하며, 픽셀 단위로 그리게 됩니다. 많은 노드는 각 단계에서 더 많은 작업을 수행한다는 의미가 됩니다.</p>
<p>단순히 다운로드 크기만의 문제는 아닙니다(물론 그것도 중요하고, 마크업이 많을수록 바이트 수가 증가하고 <em>잠재적으로</em> 압축 효율이 떨어질 수 있습니다). 이는 렌더링 성능에 관한 것입니다.
크기가 커진 DOM은 더 긴 레이아웃 및 페인트 단계, 더 많은 메모리 사용량 그리고 더 높은 에너지 사용을 의미합니다.</p>
<p>심지어 모달을 열거나 리스트를 확장하는 간단한 상호작용도 거대해진 DOM을 크롤링하여 리플로우를 유발할 수 있습니다. 그리고 갑자기 &quot;단순한&quot; 페이지가 지연되거나 끊기거나 버벅거리게 됩니다.</p>
<p>이는 크롬 개발자 도구에서 확인할 수 있습니다. <em>Performane</em>탭을 열고 추적을 기록한 다음 레이아웃 엔진이 작동할 때마다 플레임 차트가 활성화되는 모습을 확인하세요.</p>
<blockquote>
<p>재밌는 사실: 파싱은 병목이 아닙니다. 크로미움 같은 브라우저는 최신 CPU에서 HTML을 초당 수십 GB 속도로 처리할 수 있습니다. 진짜 비용은 CSSOM 구축, 레이아웃, 페인트, 합성 단계에서 발생합니다. 또한 HTML 파싱은 비지연 처리(non-deferred)된 <code>&lt;script&gt;</code>나 렌더링 차단 스타일 시트를 만나야만 차단됩니다. 이는 깔끔한 마크업이 여전히 중요하지만, 동시에 똑똑한 로딩 순서도 필요하다는 점을 다시 한번 강조합니다.</p>
</blockquote>
<h2 id="복잡한-트리는-레이아웃-스래싱을-유발합니다">복잡한 트리는 레이아웃 스래싱을 유발합니다.</h2>
<p>중요한 것은 단순히 마크업의 양이 아니라 구조입니다. 깊게 중첩된 구조, 불필요한 래퍼, 지나치게 추상화된 컴포넌트는 이해하기 어렵고 렌더링 비용이 높은 DOM 트리를 생성합니다. 브라우저는 변경 사항이 어떤 부분에 영향을 미치는지 파악하기 위해 더 많은 노력을 기울여야 하며, 그 지점에서 문제가 발생하기 시작합니다.</p>
<p>단일 클래스를 토글 하면, 뷰포트 전체의 레이아웃이 무효화될 수 있습니다. 이 변경 사항을 부모-자식 간의 체이닝을 통해 연쇄적으로 전달되어 레이아웃 이동과 시각적 불안정성을 유발합니다. 컴포넌트가 예상하지 못하게 재배치됩니다. 스크롤 고정 기능이 실패하고 사용자는 상호작용 도중에 위치 정보를 잃게 됩니다. 이는 전체적인 사용자 경험을 불안정하게 만듭니다.</p>
<p>이 모든 작업이 실시간으로 모든 상호작용할 때마다 발생하기 때문에 프레임 예산에 영향을 미칩니다. 60 fps를 목표로 한다면? 프레임당 약 16ms 밖에 주어지지 않습니다. 이 예산을 초과하면 사용자는 곧바로 지연됨을 느끼게 됩니다.</p>
<p>크롬 개발자 도구에서 *&quot;레이아웃 이동 영역(Layout Shift Regions)&quot;*이나 누락된 프레임이 쌓이는 <em>&quot;프레임(Frame)&quot;</em> 그래프에서 확인할 수 있습니다.</p>
<blockquote>
<p>DOM을 변경할 때 브라우저가 항상 전체 트리 레이아웃을 다시 처리 않습니다. 점진적 레이아웃 처리가 이루어지죠. 하지만 깊게 중첩되거나 모호한 마크업은 여전히 비용이 많이 드는 상위 요소 검사를 유발합니다. 페이스북의 &quot;Spineless Traversal&quot; 같은 프로젝트는 많은 노드를 검사해야 할 때 브라우저가 여전히 성능 저하를 겪는다는 점을 보여줍니다.</p>
</blockquote>
<h2 id="중복-css는-재계산-비용을-증가시킵니다">중복 CSS는 재계산 비용을 증가시킵니다.</h2>
<p>거대해진 DOM도 충분히 나쁘지만, 거대해진 스타일 시트는 상황을 더욱 악화시킵니다.</p>
<p>모던 CSS 워크플로우는 특히 컴포넌트화된 시스템에서는 종종 중복이 발생합니다. 각 컴포넌트가 자체 스타일을 선언하는데, 심지어 반복되는 경우에도 마찬가지입니다. 연쇄적이지 않고 공유된 문맥도 없습니다. 특수성은 혼란스러워지고, 오버라이드가 기본이 되겠죠.</p>
<p>예를 들어, 흔히 다음과 같습니다.</p>
<pre><code class="language-css">/* button.css */
.btn {
  background-color: #006;
  color: #fff;
  font-weight: bold;
}

/* header.css */
.header .btn {
  background-color: #005;
}

/* card.css */
.card .btn {
  background-color: #004;
}</code></pre>
<p>각 파일은 동일한 요소를 재정의합니다. 브라우저가 이 모든 것들을 파싱하고 수행하고 재조정해야 합니다. 이를 <em>수백 개</em> 컴포넌트로 확대하면 CSSOM(브라우저의 모든 CSS 규칙을 내부적으로 모델링한 구조)이 풍선처럼 불어나게 됩니다.</p>
<p>무언가 변경될 때마다(클래스 토글과 같은), 브라우저는 어떤 규칙이 어디에 적용되는지 재평가해야 합니다. 더 많은 규칙은 더 많은 재개산을 해야 합니다. 그리고 저사양 기기에는 이 과정으로 인해 병목 현상이 발생하게 됩니다.</p>
<p>테일윈드 같은 아토믹 CSS 시스템은 파일 크기를 줄이고 재사용성을 높일 수 있습니다. 하지만 의도적으로 사용할 때만 그렇습니다. 모든 컴포넌트가 수십 개의 유틸리티 클래스로 감싸지고, 각 유틸리티가 조금씩 수정될 때마다(여기는 여백, 저기는 폰트) 결국 수천 개의 고유한 조합이 생겨납니다. 그중 상당수는 거의 똑같습니다.</p>
<p>문제는 단순히 크기만이 아닙니다. 그것은 변동성입니다.</p>
<blockquote>
<p>브라우저는 선택자를 오른쪽에서 왼쪽으로 일치시킵니다(예: <code>div.card p span</code> -&gt; 부모 -&gt; 기타 순으로 확인). 이는 명확하고 구체적인 선택자에게는 효율적이지만 거대해진 깊은 트리나 일반적인 연쇄 규칙은 많은 오버 캐싱을 유발합니다.</p>
</blockquote>
<h2 id="자동-생성된-클래스는-캐싱-및-타겟팅을-무효화합니다">자동 생성된 클래스는 캐싱 및 타겟팅을 무효화합니다.</h2>
<p><code>.sc-a12bc</code>, <code>.jsx-392hf</code>, <code>.tw-abc123</code> 같은 클래스 이름을 흔히 볼 수 있습니다. 이는 종종 CSS-in-JS 시스템, 범위 지정 스타일, 빌드 해싱의 결과물입니다. 의도는 명확합니다. 전역 충돌을 피하기 위해 스타일을 로컬화한 것이죠. 나쁜 생각은 아닙니다.</p>
<p>하지만 이 접근법은 또 다른 종류의 <em>취약점</em>을 가져옵니다.</p>
<p>만약 클래스가 일시적이거나 빌드마다 변경된다면 다음과 같은 문제가 발생합니다.</p>
<ul>
<li>분석 태그가 깨집니다.</li>
<li>e2e 테스트 시 지속적인 유지보수가 필요합니다.</li>
<li>캐싱 전략이 무너집니다.</li>
<li>마크업 차이점을 읽을 수 없습니다.</li>
<li>CSS는 기본적으로 재사용이 불가능해집니다.</li>
</ul>
<p>성능 관점에서 마지막 요점이 매우 중요합니다. 캐싱은 오직 예측 가능한 상황에서만 효과적입니다. 브라우저의 능력은 파싱 된 스타일시트를 캐싱하고 재사용할 수 있는 일관된 선택자에 의해 의존됩니다. 모든 컴포넌트, 모든 빌드, 모든 배포마다 클래스 이름이 변경된다면 브라우저는 모든 요소를 재파싱하고 재적용해야 합니다.</p>
<p>더 나쁜 점은, 이런 구조가 외부 도구들에 취약한 임시방편에 의존하게 된다는 것입니다. 태그 매니저를 통해 결제 프로세스 내 버튼을 타겟팅하고 싶으신가요? 해시 처리된 컴포넌트가 세 겹으로 감싸져 있다면, 행운을 빌어야 할 것입니다.</p>
<p>이것은 가상의 문제가 아닙니다. 현대 프런트엔드 스택에서 흔히 발생하는 문제점이며, 코드, 툴링, 렌더링 경로 등 모든 것을 불필요하게 거대하게 만드는 원인입니다.</p>
<p>예측 가능하고, 의미론적 클래스 이름은 단순히 여러분의 작업을 편리하게 할 뿐만 아니라 웹을 더 빠르게 만듭니다.</p>
<h2 id="시멘틱-태그는-레이아웃-힌트를-제공할-수-있습니다">시멘틱 태그는 레이아웃 힌트를 제공할 수 있습니다.</h2>
<p>시멘틱 HTML은 의미론적, 접근성에 관한 것이 아닙니다. 이것은 스케폴딩과 구조입니다. 그리고 그 구조는 당신과 브라우저 모두에게 작업할 수 있는 기반을 제공합니다.</p>
<p><code>&lt;main&gt;</code>, <code>&lt;nav&gt;</code>, <code>&lt;aside&gt;</code>, <code>&lt;footer&gt;</code>와 같은 태그는 단순히 의미론적 뿐 아니라 기본적으로 블록 레벨 요소이며, 자연스럽게 페이지를 분할해 줍니다. 이런 분할은 종종 브라우저가 콘텐츠를 처리하고 렌더링 하는 방식과 일치합니다. 성능 향상을 보장하지는 않지만, 그 조건을 마련해 줍니다.</p>
<p>레이아웃에 명확한 경계가 있을 때 브라우저는 작업을 보다 효율적으로 범위 지정할 수 있습니다. 스타일 재계산을 분리하고 불필요한 재흐름을 방지하며 스크롤 컨테이너나 고정 요소 같은 요소를 더 잘 관리할 수 있습니다.</p>
<p>더 중요한 것은 <em>페인트</em>와 <em>합성</em> 단계에서 브라우저는 브라우저가 렌더링 작업을 멀티 스레드에 분산시킬 수 있다는 것입니다. GPU 합성 파이프라인의은 잘 구조화된 DOM 영역으로부터 이점을 얻습니다. 특히 <code>contain: paint</code> 또는 <code>will-change: transform</code>같은 속성과 함께 사용될 때 더욱 그렇습니다. 격리된 레이어를 생성함으로써 페이지의 거대한 부분을 다시 래스터화 하는 오버헤드를 줄일 수 있습니다.</p>
<p>만약 중첩된 거대한 <code>&lt;div&gt;</code>들로 되어있다면, 다양한 종류의 격리를 명확히 할 기회가 없어지게 됩니다. 모든 상호작용, 애니메이션 또는 리아시즈 이벤트는 전체 트리에 영향을 미치는 리플로우와 리페인트를 유발할 위험이 있습니다. 단순히 자신에게만 어려움을 주는 것이 아니라 렌더링 엔진의 병목 현상을 일으키는 것입니다.</p>
<p>간단히 말해서: 시멘틱 태그는 브라우저와 싸우는 대신에 당신의 작업을 도와주게 됩니다. 마법은 아니지만 마법이 가능하도록 만드는 것입니다.</p>
<h2 id="애니메이션과-합성-재앙">애니메이션과 합성 재앙</h2>
<p>애니메이션을 잘 구조화된 HTML이 빛을 발하거나... 아니면 재앙을 겪게 됩니다.</p>
<p>모던 브라우저는 애니메이션 작업을 GPU로 오프로드하려고 합니다. 이는 60 fps 이상의 부드러운 전환을 가능하게 하는 비결입니다. 하지만 이를 위해서는 브라우저가 애니메이션 요소를 별도의 합성 레이어로 분리해야 합니다. 특정 CSS 속성만이 이러한 GPU 가속 처리를 받을 수 있는데, 특히 <code>transfomr</code>과 <code>opacity</code> 속성이 대표적입니다.</p>
<p><code>top</code>, <code>left</code>, <code>width</code>, <code>margin</code>과 같은 요소를 애니메이션 하면 레이아웃 엔진이 작동됩니다. 이는 변경된 요소 아래에 있는 모든 요소의 레이아웃을 재계산한다는 뜻이며, 메인 스레드 작업으로 이어지는 비용이 많이 드는 작업입니다.</p>
<p>간단한 페이지라면? 어쩌면 괜찮을 수도 있습니다.</p>
<p>수십 개의 형제 요소와 종속성을 가진 깊게 중첩된 컴포넌트라면? 모든 애니메이션의 레이아웃 스레싱를 유발하게 됩니다. 그리고 애니메이션 프레인 예산이 16ms(60 fps의 한계)를 초과하면 상황이 불안정해집니다. 애니메이션이 끊기고, 상호작용이 지연되며, 스크롤이 느려집니다.</p>
<p>개발자 도구의 성능 패널에서 이를 확인할 수 있습니다. 레이아웃 재계산, 스타일 무효화, 페인트 작업이 플레임 차트를 불타오르게 하죠.</p>
<p>시멘틱 HTML도 여기서 도움이 됩니다. 적절한 구조적 경계는 모던 CSS 컨테이너 전략을 더 효과적으로 활용할 수 있게 합니다.</p>
<p><code>contain: layout;</code>은 브라우저에게 해당 요소 외부 레이아웃 재계산이 필요 없음을 알립니다.
<code>will-change: transform;</code>은 합성 레이어가 필요함을 암시합니다.
<code>isolation: isolate;</code>와 <code>contain: paint;</code>는 시각적 파급 효과 방지 및 GPU 레이어 강제 적용에 도움이 됩니다.</p>
<p>하지만 이런 기술은 DOM 구조가 합리적일 때만 효과적입니다. 만약 애니메이션 컴포넌트가 예측 불가능한 일반 <code>&lt;div&gt;</code> 요소들 사이에 중첩되어 있다면, 브라우저는 이를 깨끗하게 격리할 수 없습니다. 어떤 요소가 영향을 받을지 알 수 없기 때문에 안전하게 모든 것들을 재계산합니다.</p>
<p>이는 브라우저의 결함이 아니라 개발자의 실패입니다.</p>
<p>애니메이션은 단순히 무엇을 움직이는지에 관한 것이 아니라 움직여서는 안 되는 것에 관한 것입니다.</p>
<blockquote>
<p>렌더링과 페인팅은 모던 엔진에서 병렬 작업으로 이루어집니다. 하지만 DOM/CSS 변경은 종종 메인 스레드 동기화를 강제하여 이 장점들을 없애버립니다.</p>
</blockquote>
<blockquote>
<p><code>will-change: transform</code> 또는 최신 <code>layer()</code> 구문을 통한 CSS 레이어링은 GPU에 합성 작업을 별도로 처리하도록 지시합니다. 이는 메인 스레드에서의 레이아웃 및 페인팅을 피하게 하지만, DOM 구조가 별개의 레이어링 컨테이너를 허용할 때에만 가능합니다.</p>
</blockquote>
<h2 id="css-컨테이너-및-가시성-강력하지만-취약합니다">CSS 컨테이너 및 가시성: 강력하지만 취약합니다.</h2>
<p>모던 CSS는 성능 관리를 위한 강력한 도구를 제공하지만, HTML이 숨 쉴 공간을 마련해 줄 때만 효과적입니다.</p>
<p>예를 들어 <code>contain</code>은 <code>contain: layout</code>, <code>paint</code> 또는 심지어 <code>size</code>를 사용해 브라우저에 *&quot;이 박스 밖을 보지 마라 - 여기 있는 것을 페이지의 나머지 부분에 영향을 미치지 않는다&quot;*고 말할 수 있습니다. 특히 동적 인터페이스에서 레이아웃 재계산 비용을 크게 줄일 수 있습니다.</p>
<p>하지만 이는 마크업에 명확한 구조적 경계가 있을 때만 작동합니다.</p>
<p>콘텐츠가 의미론적이지 않은 래퍼의 얽힌 구조에 갇혀 있거나, 컨테이너가 예상치 못한 스타일이나 종속성을 상속받는다면, 컨테이너 기능은 신뢰할 수 없게 됩니다. 격리할 수 없는 요소는 안전하게 컨테이너로 감싸지 못하기에 브라우저는 그 위험을 감수하지 않을 것입니다.</p>
<p>마찬가지로 <code>content-visibility: auto</code>는 모던 CSS 도구 중 가장 과소평가된 기능 중 하나입니다. 이 기능은 화면에 표시되지 않는 요소의 렌더링을 브라우저가 건너뛰도록 하여 효과적으로 &quot;가상화(virtualising)&quot;합니다. 이는 긴 페이지, 피드 또는 무한 스크롤 컴포넌트에 매우 유용합니다.</p>
<p>하지만 주의사항이 따릅니다. 예측 가능한 레이아웃, 스크롤 고정, 구조적 일관성이 필요합니다. DOM이 지저분하거나 컴포넌트 트리가 상하로 스타일과 종속성을 누출하면 역효과가 나타납니다. 레이아웃 점프, 렌더링 버그, 포커스 상태 오류가 발생하죠.</p>
<p>이것들은 만능 해결책이 아닙니다. 성능 계약이죠. 지저분한 마크업은 그 계약을 깨드리게 됩니다.</p>
<p>시멘틱 HTML과 깔끔하게 잘 구조화된 DOM이 바로 이러한 도구들을 가능하게 하는 근본적인 요소입니다.</p>
<blockquote>
<p>MDN 문서에서는 <code>contain: content</code>(<code>layout</code>+<code>patin</code>+<code>style</code> 단축형)이 브라우저가 전체 하위 트리를 독립적으로 최적화할 수 있게 하는 방식을 설명합니다. 실제 A/B 테스트 결과, <code>content-visibility: auto</code>를 사용한 전자상거래 페이지에서 INP 지연 시간이 개선된 것으로 나타났습니다.</p>
</blockquote>
<h2 id="에이전트는-새로운-사용자이며-구조를-중요하게-생각합니다">에이전트는 새로운 사용자이며 구조를 중요하게 생각합니다.</h2>
<p>웹은 더 이상 인간만을 위한 공간이 아닙니다.</p>
<p>검색 엔진이 첫 번째 물결이었습니다. 콘텐츠를 분석하고 의미를 추출하여 구조와 의미론에 기반해 순위를 매겼죠. 하지만 이제 우리는 AI 에이전트, 어시스턴트, 스크래퍼, 태스크 실행기, 대규모 언어 모델 기반 자동화의 시대에 접어들고 있습니다. 이 시스템들은 사이트를 둘러보지 않습니다. 스크롤하지도, 클릭도 하지 않죠. 다만 분석만 할 뿐입니다.</p>
<p>그들은 당신의 마크업을 보고 묻습니다.</p>
<ul>
<li>이것은 무엇인가?</li>
<li>어떻게 구조화되어 있는가?</li>
<li>무엇이 중요한가?</li>
<li>다른 모든 것과 어떻게 연관되는가?</li>
</ul>
<p>깨끗하고 의미론적인 DOM은 이러한 질문에 명확히 답합니다. 뒤엉킨 <code>div</code>의 집합은 그렇지 않습니다.</p>
<p>이러한 에이전트들이 동일한 위젯을 판매한다고 주장하는 열 개의 사이트 중에서 선택해야 할 때, 해석하고 추출하며 요약하기 쉬운 사이트가 승리할 것입니다.</p>
<p>이는 가설이 아닙니다. 구글 쇼핑 시스템, 퍼플렉시티 같은 요약 에이전트, 아크 같은 AI 브라우저, 접근성 보조 도구 등이 모두 이러한 변화의 사례입니다. 귀하의 사이트는 더 이상 시각적 경험이 아닙니다. 인터페이스이며, API이고, 데이터셋일 뿐입니다.</p>
<p>당신의 마크업이 이를 지원하지 못한다면? 당신은 그들의 대화에서 배제됩니다.</p>
<p>물론, 스마트 시스템은 필요할 때 구조를 추론할 수 있고 실제로 그렇게 합니다. 하지만 이는 추가 작업이고 부정확하며, 위험합니다.</p>
<p>경쟁 환경에서 잘 구조화된 마크업은 단순한 최적화가 아니라 차별화 요소입니다.</p>
<h2 id="구조는-회복탄력성이다">구조는 회복탄력성이다.</h2>
<p>시멘틱 HTML은 단순히 기계가 콘텐츠를 이해하도록 돕는 것이 아닙니다. 압박 속에서도 견딜 수 있는 인터페이스를 구축하는 것입니다.</p>
<p>이는 단순히 좋은 관행이 아니라 현실 세계를 위한 소프트웨어를 구축하는 방식입니다.</p>
<p>왜냐하면 실제 사용자는 불안정한 연결을 가지며, 실제 기기는 제한된 성능을 가집니다. 실제 세션에는 테스트하지 않은 극단적 사례가 포함되어 있기 때문입니다.</p>
<p>의미론적 마크업은 기준점이면서, 대체 수단이자 기반이 됩니다.</p>
<h2 id="구조는-선택-사항이-아닙니다">구조는 선택 사항이 아닙니다.</h2>
<p>성능, 접근성, 검색 가능성 또는 복원력을 위해 구축과 사이트가 빠르고 이해하기 쉬우며 적응력이 있기를 원한다면, 의미 있는 HTML로 시작하세요.</p>
<p>마크업을 사후 고려사항으로 취급하지 마세요. 도구가 구조를 묻어버리게 두지 마세요. 별들이 정렬되고 자바스크립트가 로드될 때만 작동하는 인터페이스를 구축하지 마세요.</p>
<p>테일윈드를 사용하는 걸 막지 않겠습니다. 리액트를 사용하는 걸 막지도 않겠습니다. 하지만 신중하게 접근하라고 요구합니다. 의도를 담아 구조를 설계하라고. 단순히 인간에게만이 아니라 브라우저, 봇, 에이전트 모두에게 이야기를 전하는 코드를 작성하라고.</p>
<p>이것은 향수가 아닙니다. 이것은 인프라입니다.</p>
<p>그리고 웹이 다가올 복잡성, 자동화, 기대의 물결을 견뎌내려면 우리는 웹을 제대로 구축하는 방법을 기억해야 합니다.</p>
<p>그것은 HTML을 작성하는 방법을 기억하는 것에서 시작됩니다. 그리고 우리가 왜 그런 방식으로 작성하는지 이해하는 것에서 시작됩니다. 자바스크립트의 부산물이나 도구의 산물이 아니라, 그 이후 모든 것의 기초로서 말입니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[(번역) 상태들의 참담한 상태]]></title>
            <link>https://velog.io/@tap_kim/the-sorry-state-of-states</link>
            <guid>https://velog.io/@tap_kim/the-sorry-state-of-states</guid>
            <pubDate>Tue, 22 Jul 2025 14:55:56 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>오늘날의 피그마 에셋은 디자인이 코드와 더 잘 어울릴 수 있음을 보여줍니다.</p>
</blockquote>
<blockquote>
<p>원문: <a href="https://medium.com/@nathanacurtis/the-sorry-state-of-states-89dd4668737e">https://medium.com/@nathanacurtis/the-sorry-state-of-states-89dd4668737e</a>
저자: <a href="https://www.linkedin.com/in/nathanacurtis/">Nathan Curtis</a></p>
</blockquote>
<p>몇 년 전, 저는 <a href="https://medium.com/eightshapes-llc/crafting-ui-component-api-together-81946d140371">컴포넌트 API 함께 만들기</a>라는 글을 썼습니다. 피그마는 헤게모니적<sup><a href="#footnote_1">1</a></sup>인 디자인 도구로 자리 잡았고, 유용하며 사용하기 쉬운 API로 컴포넌트를 구성하는 능력은 계속해서 강화되고 있습니다. 그로 인해 라이브러리는 더 깊숙이 자리 잡았습니다.</p>
<p>하지만 최고의 시스템 설계자조차도 코드가 실제로 작동하는 방식을 모방할 기회를 놓치고 있습니다. 텍스트 입력, 텍스트 영역, 드롭다운, 체크박스, 라디오 버튼과 같은 많은 상호작용 요소의 상태 <code>state</code> 속성의 안타까운 상태를 보면 이를 더 명확히 알 수 있습니다.</p>
<p>이 글에서는 디자인 시스템 설계자가 코드 작동 방식과 일치하지 않는 피그마 에셋의 <code>states</code>를 어떻게 설계하는지 살펴봅니다. 텍스트 입력 예시는 옵션 조합의 <strong>부분 집합</strong>과 상호 <strong>의존적인 속성</strong>부터 <strong>불리언과 열거형</strong> 옵션에 이르기까지 순수하게 피그마 에셋을 모델링하기 위해 어느 정도까지 가야 하는지(또는 가지 말아야 하는지)에 대한 교훈을 제공합니다.</p>
<h2 id="오늘날-상태들의-상태">오늘날 상태들의 상태</h2>
<p>상태는 디자이너와 개발자가 다양한 상황에 적용하는 일반적인 용어입니다. <a href="http://in/ericwbailey">Eric Bailey</a>가 정리한 <a href="https://ericwbailey.design/published/all-the-user-facing-states/">사용자 중심의 상태</a>리스트는 이를 잘 보여줍니다. 리스트에는 <code>rest</code>, <code>hover</code>, <code>active</code> 상태부터 <code>disabled</code>, <code>readonly</code>, <code>selected</code>, <code>deselected</code>, <code>loading</code>, <code>ghost origin</code>, <code>dirty</code>와 같이 덜 일반적인 고려 사항까지 총 38가지가 포함됩니다.</p>
<p><img src="https://miro.medium.com/v2/resize:fit:968/format:webp/1*kaOZS9Zi-oPM-MVh80C9cA.png" alt=""></p>
<p>[Eric Bailey의 <a href="https://ericwbailey.design/published/all-the-user-facing-states/">모든 사용자 표시 상태</a> 예시에서 발췌한 내용입니다.]</p>
<p>호기심 많은 크롬 사용자들은 크롬 인스펙터를 통해 텍스트 입력을 선택하고 마우스 오른쪽 버튼을 클릭하여 검사를 선택하면 프런트엔드 개발자가 CSS 조합으로 활용할 수 있는 강력한 HTML <a href="https://developer.mozilla.org/en-US/docs/Learn/Forms/Form_validation">양식 유효성 검사</a> 후크(<code>:disabled</code>, <code>:valid</code>, <code>:invalid</code>, ...)를 포함한 다양한 요소 상태(<code>:active</code>, <code>:hover</code>, <code>:focus</code>, ...)를 확인할 수 있습니다.</p>
<p><img src="https://miro.medium.com/v2/resize:fit:1032/format:webp/1*O_WvqIJHvQGrwzc65kWyNw.png" alt=""></p>
<p>[특정 요소 상태를 포함한 텍스트 입력에 대한 크롬 인스펙터의 상태]</p>
<p>문제는 상태라는 용어가 지나치게 일반적이라는 점입니다. 가능한 모든 상태가 대안인 것은 아닙니다. 대신, 사용할 수 있는 상태는 논리적 관계에 따라 상호 의존적인 관심사가 뒤섞여 조합되어 사용되는 경우가 많습니다.</p>
<p>그러나 디자인 비평 토론, 디자인 명세서 섹션 헤더, 특히 피그마 에셋 속성에서 사용될 때 <code>state</code>는 충분히 모델링되지 않은 아이디어를 아우르는 훌륭한 만능 해결책이 되어버립니다.</p>
<p><img src="https://miro.medium.com/v2/resize:fit:2000/format:webp/1*98isutcS69qd0O4-5k36Sw.png" alt=""></p>
<p>텍스트 입력의 경우 당연히 <code>hover</code>와 <code>active</code>를 표시합니다. 이를 감안할 때 첫 번째 <code>base</code>(안 함), <code>default</code>(괜찮음), <code>enable</code> 또는 <code>initial</code>(더 좋음) 또는 <code>rest</code>(최선?)을 호출할지 잠시 멈추고 고민해 보겠습니다. 일단, <code>resting</code>으로 생각해 봅시다. 그리고 얼른 다음 단계로 넘어가 보겠습니다.</p>
<p><img src="https://miro.medium.com/v2/resize:fit:2000/format:webp/1*CNNd7X86RLFTSljVn28tVA.png" alt=""></p>
<p>이러면 복잡한 문제가 발생합니다. <code>disabled</code>은 어떻게 하나요? 물론 필요합니다. <code>readonly</code>는요? 아, 그건 생각지도 못했네요. <code>error</code> 상태는 당연히 필수입니다. <code>states</code> 집합은 점차 커지다가 ‘충분히 포착했다’고 느낄 때 안정화됩니다. 디자이너가 모든 “Eric Bailey 상태”를 다 해결하지 못해도 용서받을 수 있습니다. 저도 전부 다 다뤄본 적은 없어요. 그 목록은 정말 끝도 없이 길거든요!</p>
<h2 id="오늘날-피그마-에셋의-상태">오늘날 피그마 에셋의 상태</h2>
<p>다음은 피그마 커뮤니티에 게시된 디자인 시스템 중 <code>Text input</code> / <code>Input</code> / 기타 피그마 컴포넌트를 살펴보겠습니다.</p>
<ul>
<li><a href="https://www.figma.com/community/file/1035203688168086460/material-3-design-kit">Material 3 Design Kit</a></li>
<li><a href="https://www.figma.com/community/file/912837788133317724/material-ui-for-figma-and-mui-x">Material UI for Figma (and MUI X)</a></li>
<li>Salesforce <a href="https://www.figma.com/community/file/854593583696357016/components-for-web-lightning-design-system-v1">Components for Web | Lightning Design System v1</a></li>
<li>Github <a href="https://www.figma.com/community/file/854767373644076713/primer-web">Primer Web</a></li>
<li>IBM <a href="https://www.figma.com/community/file/1157761560874207208/v11-carbon-design-system">Carbon Design System</a></li>
<li>Atlassian <a href="https://www.figma.com/community/file/1182078880306369227/ads-components">ADS Components</a></li>
<li>Oracle <a href="https://www.figma.com/community/file/1425260295705487251/rds-toolkit-24c">Redwood</a></li>
<li>Newskit <a href="https://www.figma.com/community/file/1225806088244139801/newskit-component-library-and-theme-v5-0">Component Library</a></li>
<li>Shopify <a href="https://www.figma.com/community/file/1293611962331823010/polaris-components">Polaris Components</a></li>
</ul>
<p><img src="https://miro.medium.com/v2/resize:fit:2000/format:webp/1*JTO-FfRp8J9UE0SBs-E-5g.png" alt=""></p>
<p>[비교가 가능하도록 상태와 관련된 속성만 포함하도록 정규화 및 단순화된 Atlassian, Github 및 Shopify의 텍스트 입력 피그마 컴포넌트]</p>
<p><code>state</code> 속성 패턴은 제가 컨설팅하는 팀에서 관찰한 것과 일치했습니다.</p>
<ul>
<li><strong>Hover</strong>: 10개 중 6개가 <code>state</code>:<code>hover</code>(또는 <code>state</code>:<code>hovered</code>) 옵션을 지원합니다.</li>
<li><strong>Active</strong>: 2개는 <code>state</code>:<code>active</code> 옵션을 지원합니다.</li>
<li><strong>Focus</strong>: 10개 중 8개가 <code>state</code>:<code>focus</code>를 지원하며 별도의 불리언 속성으로 <code>focus</code>를 제공하는 것은 없습니다.</li>
<li><strong>Disabled</strong>: 10개 중 9개가 <code>state</code>:<code>disabled</code>를 지원하며, 단 하나(Atlassian)만이 별도의 불리언 속성으로 <code>isDisabled</code>를 구분합니다.</li>
<li><strong>Read only</strong>: 10개 중 6개가 <code>state</code>:<code>readonly</code>을 지원하며, <code>readonly</code>을 불리언 속성으로 제공하는 것은 없습니다.</li>
<li><strong>Error</strong>와 <strong>Success</strong>: 10개 중 8개가 <code>state</code>:<code>error</code>(또는 유사한 이름의 유효성 검사) 옵션을 지원하며, 1개(Atlassian)는 오류를 별도의 불리언 속성으로 제공했습니다.</li>
</ul>
<p>컴포넌트에는 때때로 <code>warning</code>, <code>skeleton</code>, <code>typing</code>과 같은 다른 <code>state</code> 옵션이 포함되었습니다. 심지어 여기서는 다루지 않는 <code>value</code>와 <code>placeholder</code> 속성의 인접 상태 문제를 다루는 <code>filled</code>도 포함되었습니다.</p>
<p>어떤 피그마 에셋도 <code>readonly</code> + <code>focus</code> 또는 <code>hover</code> + <code>error</code>와 같은 그럴듯한 상태 조합을 지원하지 않았습니다.</p>
<h2 id="오늘날-코드-라이브러리의-상태">오늘날 코드 라이브러리의 상태</h2>
<p>해당 라이브러리의 텍스트 입력 컴포넌트 코드를 유사하게 검토한 결과 다른 일반적인 모델이 도출되었습니다.</p>
<ul>
<li><strong>Hover</strong>와 <strong>Active</strong>: 대부분의 코드 라이브러리는 사용자 상호작용에 반응하는 상태로 암시적으로 구현합니다.</li>
<li><strong>Disabled</strong>과 <strong>Read only</strong>: 거의 모든 코드 라이브러리가 이러한 속성을 구현하며, 두 속성을 모두 기존 속성으로 간주할 수 있습니다.</li>
<li><strong>Error</strong>와 <strong>Success</strong>: 많은 코드 라이브러리에서 <code>error</code>(또는 <code>inInvalid</code>, <code>hasError</code> 또는 이와 유사한) 불리언 프로퍼티를 구현하여 오류를 표시합니다. GitHub Primer는 <code>error</code>와 <code>success</code>을 구분하기 위해 이진수에서 열거형 <code>validationStatus</code> 프로퍼티로 확장합니다. 일부 코드 라이브러리에서는 <code>error</code> 텍스트에 대한 오류 프로퍼티를 구현합니다.</li>
</ul>
<p>Shopify Polaris의 리액트 컴포넌트는 <code>disabled</code> 및 <code>readonly</code>을 위한 <a href="https://www.typescriptlang.org/docs/handbook/interfaces.html">타입스크립트 인터페이스</a>를 구현하여 컴포넌트 간에 의도적인 규칙을 제안합니다. 이 라이브러리는 또한 강제 <code>focused</code>을 구현합니다.</p>
<p><img src="https://miro.medium.com/v2/resize:fit:2000/format:webp/1*ksYSRaBl6r5Y9gq3iFuiVw.png" alt=""></p>
<p>[오늘날 에셋이 작동하는 방식과 코드에 맞춰 조정된 경우의 작동 방식을 비교하기]</p>
<h2 id="디자인과-코드는-다릅니다-그래서-뭐가-문제일까요">디자인과 코드는 다릅니다. 그래서 뭐가 문제일까요?</h2>
<p>증거는 분명합니다. <code>hover</code> 및 <code>active</code>와 같은 상호 작용 상태를 넘어, 피그마 에셋과 코드 컴포넌트는 뚜렷한 API 시그니처를 제공합니다. 사실, 좀 더 정확하게 말하자면, <em>제품 디자이너가 재사용을 위해 공개한 디자인 에셋</em>은 제품 개발자가 사용하는 코드 에셋과 다릅니다.</p>
<p>시스템 디자이너가 시스템 개발자에게 넘겨준 디자인 솔루션이 코드에서 일어난 일과 완벽하게 일치할 수도 있습니다. 하지만 제 경험에 따르면 그렇지 않습니다. 디자인 이터레이션과 사양(종종 <a href="https://www.figma.com/community/plugin/1205622541257680763/eightshapes-specs">EightShapes Specs 플러그인</a>으로 자동화됨)은 거의 항상 같은 &quot;상태로 여러 가지 복잡한 문제를 동시에 다루는&quot; 접근 방식을 드러냅니다. 왜 신경 써야 할까요?</p>
<h2 id="효율성-사용성-및-만족도에-미치는-부정적-영향">효율성, 사용성 및 만족도에 미치는 부정적 영향</h2>
<p>최소한 대부분의 디자이너는 부정확하고/또는 불완전한 디자인 전달의 일반적인 함정을 알고 있습니다. 이는 다음과 같은 문제를 초래합니다.</p>
<ol>
<li>제품 디자이너가 코드와 다른 모델을 사용하는 공개된 피그마 에셋을 사용하여 제품 개발자에게 <strong>전달할 때 발생하는 마찰</strong>.</li>
<li>피그마 에셋의 <strong>잘 정리되지 않은 속성</strong>은 사용하기 어렵고, 실제로는 독립적인 여러 가지 선택 사항이 하나 또는 몇 개의 속성가 무질서하게 흩어져 있습니다.</li>
<li>디자이너가 컴포넌트의 속성을 효과적으로 모델링하는 능력이 부족하거나 부적절하다고 판단하는 개발자들의 <strong>지속적인 불신과 무례함</strong>.</li>
</ol>
<p>마지막 부분은 아프네요. 때로는 정당화될 수 있는 &quot;내 물건에 손대지 마&quot;라는 태도를 지속시키는 것이죠. 일부 개발자들은 여전히 그런 태도를 고수하지만, 대신 <a href="https://medium.com/eightshapes-llc/crafting-ui-component-api-together-81946d140371">함께 컴포넌트 API를 설계</a>하는 데 집중할 수 있었을 텐데요. 그건 다른 날에 논의할 주제입니다.</p>
<h2 id="자동화에-미치는-영향을-최소화하기">자동화에 미치는 영향을 최소화하기</h2>
<p>일부 잘 보이지 않지만, 현재 제 협력자 대부분에게 훨씬 더 중요한 것은 피그마의 API를 활용하여 시스템 표면의 점점 더 많은 영역을 자동화하고 정리하면서 미치는 부정적인 영향입니다. 부적절하게 모델링된 속성은 다음과 같은 문제를 야기할 수 있습니다.</p>
<ol>
<li>디자인과 코드 라이브러리 간의 정합성을 점검하고 리포팅하는 <strong>자동화 기회</strong>를 제한하게 됩니다.</li>
<li><strong>에셋을 데이터</strong>로 활용하여 정보를 제공하거나, 단일 진실의 원천(source of truth)으로 삼거나, 심지어 코드 개발을 자동화하는 <strong>능력 또한 제한</strong>됩니다.</li>
<li>애초에 피할 수 있는 <a href="https://help.figma.com/hc/en-us/articles/23920389749655-Code-Connect">Figma Code Connect</a> 같은 도구에서 복잡하고 깨지기 쉬운 변환을 <strong>매핑하는 데 드는 비용이 증가</strong>할 수 있습니다.</li>
</ol>
<p><a href="https://medium.com/eightshapes-llc/crafting-ui-component-api-together-81946d140371">2021년에 이 주제에 대해 작성한 이후</a>로, 디자인을 포함한 라이브러리 간 API 일치에 대한 제 믿음은 더욱 확고해졌습니다. 제가 작업하는 시스템과 마찬가지로 많은 존경받는 디자인 시스템이 코드와 불필요하게 이탈하는 것을 볼 때, 우리는 모두 더 나은 방법을 찾아야 합니다. 이 문제는 사실 어렵지 않습니다.</p>
<h2 id="모델링-프로퍼티-조합-및-상호작용">모델링 프로퍼티 조합 및 상호작용</h2>
<p>이제 단계별로 자세히 살펴보며 상태 기반 프로퍼티와 그 작동 방식을 이해해 보겠습니다. 먼저 라디오 버튼과 체크박스에서 볼 수 있는 <code>selected</code>와 <code>state</code>의 자연스러운 조합 특성을 살펴보겠습니다. 그다음 텍스트 입력 필드의 <code>disabled</code>, <code>readonly</code>, 검증 상태를 통해 프로퍼티가 어떻게 조합되고 상호작용하는지 배워보겠습니다.</p>
<h2 id="완전한-조합-집합">완전한 조합 집합</h2>
<p>일부 유형의 상태는 결합되어 관련 조합의 완전한 집합을 형성합니다. 예를 들어, <code>Checkbox</code>와 <code>Radio button</code>은 상호작용 <code>state</code>(<code>rest</code>, <code>hover</code>, <code>active</code>, <code>focus</code>)와 <code>selected</code> 상태(<code>not selected</code>, <code>selected</code>)를 결합하여 8개의 조합을 형성합니다.</p>
<p>다음 표는 <code>state</code>와 <code>selected</code> 항목에 대해 모든 조합이 존재하며(✅로 표시됨) 각각 고유한 시각적 디자인이 필요할 수 있음을 보여줍니다.</p>
<p><img src="https://miro.medium.com/v2/resize:fit:2000/format:webp/1*Jm05v1pIaQZ_WmFmk7lmLA.png" alt=""></p>
<p>[상태 대 선택 항목 조합 표]</p>
<p>피그마 컴포넌트 내 <code>Radio button</code> 상태에 대해, <code>hover</code>, <code>active</code>, <code>focus</code>와 <code>selected</code> 상태와 같은 옵션을 포함하는 <code>state</code> 속성을 추가할 수 있습니다. 그러나 이는 <code>selected</code>와 <code>hover</code>가 동시에 적용되는 경우와 같이, 가능한 조합을 제외하게 됩니다. 이러한 조합은 <code>hover selected</code>과 같은 추가 <code>state</code> 옵션으로 추가할 수 있지만, 복합 옵션 이름을 통해 모든 조합을 제공하는 위험한 경로로 빠질 수 있습니다. <code>selected</code> 상태(옵션이 <code>true</code>와 <code>false</code>일지라도 시각적 디자인이 다름)와 <code>state</code>를 구분하여 별도의 피그마 변형 속성을 사용하는 것이 더 좋습니다.</p>
<p><img src="https://miro.medium.com/v2/resize:fit:2000/format:webp/1*_uzdglmWmTDLQ4Dhd2PWTg.png" alt=""></p>
<p>[라디오 버튼을 별도의 프로퍼티로 선택하는 것과 상태 프로퍼티의 옵션으로 선택하는 것]</p>
<p>체크박스의 세 번째 <code>checked</code> 상태인 <code>불확정(indeterminate)</code> 상태의 가능성은 12가지 가능한 조합을 생성하며, 이는 복합 용어의 열거 상태 목록을 덜 선호되는 것으로 만들어집니다. 또한, 체크 속성은 이진 변형에서 <code>not checked</code>(기본값), <code>indeterminate</code> 및 <code>checked</code>의 열거된 옵션을 표시하는 것으로 변경됩니다.</p>
<p><img src="https://miro.medium.com/v2/resize:fit:2000/format:webp/1*krjtipY6DwWKqDqg5XSQhQ.png" alt=""></p>
<p>[체크박스에서 선택된 상태를 별도의 프로퍼티로 설정하는 것과 상태 프로퍼티의 옵션으로 설정하는 것의 차이]</p>
<p>Eric Bailey의 상태 목록을 자세히 검토할 만큼 용감한 분들은 <code>Deselected</code>와 같은 더 관련성 있는 고려 사항을 발견할 수 있을 것입니다. 대부분의 팀에게는 <code>Deselected</code>(<code>Selected</code>에서 변경된 상태)를 <code>Rest</code>(선택되지도 않았고 상호작용도 되지 않은 상태)와 구분하여 포함하는 것은 실용적이지 않습니다.</p>
<h2 id="부분-조합-집합">부분 조합 집합</h2>
<p><code>disabled</code> 속성은 <code>true</code> 또는 (기본값으로) <code>false</code>로 설정된 변형 집합입니다. 비활성화된 컨트롤은 기본적으로 <code>rest</code> 상태이며, 시각적으로 구분되며 <code>hover</code>, <code>active</code> 및 <code>focus</code> 상태는 관련이 없습니다.</p>
<p><img src="https://miro.medium.com/v2/resize:fit:2000/format:webp/1*uPw26gJ3W8XZqKSR6c122w.png" alt=""></p>
<p>[상태별 비활성화 조합 표]</p>
<p>결과적으로 디자이너는 <code>disabled</code> 상태를 상호작용 <code>state</code>에서 분리할지, 아니면 <code>disabled</code> 상태를 <code>state</code>의 옵션으로 포함할지 선택할 수 있습니다. 두 선택 모두 피그마 에셋이 rest 상태가 아닌 비활성화 상태를 구현하지 않는 한, 가능한 8가지 조합 중 5가지의 부분 집합이 됩니다.</p>
<p><img src="https://miro.medium.com/v2/resize:fit:2000/format:webp/1*Es0LLX4sitgAq3wHEsd9GA.png" alt=""></p>
<p>[텍스트 입력 컴포넌트에서 disabled를 별도 속성으로 처리할지, state 속성의 일부로 통합할지에 대한 비교]</p>
<p><code>disabled</code> 옵션을 상태 <code>state</code> 범주에 끼워 넣는 게 편할까요? 물론, 쉬워 보일 수는 있습니다. 하지만 코드란 그렇게 작동하지 않습니다. 그리고 <code>disabled</code>를 <code>state</code>로 흡수하면, 모델의 명확성과 의미가 떨어질 뿐만 아니라, 곧 보게 되겠지만 다른 관계들을 제대로 모델링하는 데에도 제약이 생깁니다. (말장난이지만 정말로 disabled 됩니다.)</p>
<h2 id="상호-의존적인-속성">상호 의존적인 속성</h2>
<p><code>readonly</code> 속성은 코드에서 일반적으로 불리언 값으로 사용되는 속성입니다. <code>disabled</code> 속성과 마찬가지로 <code>readonly</code>는 기본적으로 <code>false</code>로 설정됩니다. 그러나 <code>disabled</code>와 달리 <code>readonly</code>는 <code>rest</code>와 <code>focus</code>를 지원하지만 일반적으로 <code>hover</code>와 <code>active</code>는 지원하지 않습니다.</p>
<p><img src="https://miro.medium.com/v2/resize:fit:2000/format:webp/1*8kDC0LT6J3JOG8J12Cd2kw.png" alt=""></p>
<p>[상태별 비활성화 및 읽기 전용 조합 표]</p>
<p>또한, <code>disabled</code>와 <code>readonly</code>는 동시에 발생하지 않습니다. <code>disabled</code>와 <code>readonly</code>가 모두 <code>true</code>로 설정되면 <code>disabled</code>가 우선순위를 가지며 <code>readonly</code>는 무시됩니다. 이로 인해 이 두 속성은 상호 의존적입니다.</p>
<p>디자이너가 7가지 유효한 조합을 모두 구현하면 피그마는 이 경우를 충분히 잘 처리합니다. 우선순위를 고려할 때 <code>disabled</code>를 먼저 설정하고, 그다음 <code>readonly</code>, 마지막으로 <code>state</code>를 설정해야 합니다. 컴포넌트 사용자가 <code>disabled</code>를 <code>false</code>로 설정하면 컴포넌트는 다른 <code>state</code>를 <code>rest</code> 상태로 복원합니다.</p>
<p><img src="https://miro.medium.com/v2/resize:fit:2000/format:webp/1*j7RQtWABdX40Mu0eDa0ULw.png" alt=""></p>
<p>[읽기 전용 텍스트 입력 필드를 별도의 프로퍼티로 설정하는 것과 상태 프로퍼티의 옵션으로 설정하는 것의 차이]</p>
<p><code>disabled</code>와 <code>readonly</code>를 모두 <code>state</code> 속성의 옵션으로 통합할 수 있을까요? 물론 가능합니다. 하지만 여러 속성의 차원을 하나의 복합적인 이름을 가진 속성으로 결합하는 방식은 코드 구조와 잘 맞지 않고, 확장성에도 한계가 있습니다.</p>
<h2 id="열거형-또는-불리언-속성-선택">열거형 또는 불리언 속성 선택</h2>
<p>에러와 성공 같은 검증 상태는 더 많은 의문을 불러일으킵니다. 반면 <code>disabled</code>와 <code>readonly</code>는 기분 좋을 정도로 이진적(binary)이며, <code>true</code> 또는 <code>false</code> 값으로 간단히 표현할 수 있습니다 (피그마의 variant 속성으로 구성하더라도 마찬가지입니다).</p>
<p>일부 디자인 시스템은 에러 상태만을 제공하는데, 이 경우 익숙한 선택지가 다시 등장합니다. 즉, <code>error</code> variant 속성을 제공하고, 기본값인 <code>false</code> 또는 <code>true</code>로 설정 가능하게 하는 방식을 추천하는 것입니다.</p>
<p><img src="https://miro.medium.com/v2/resize:fit:2000/format:webp/1*vxdc3lzsoAt5VnxZej56dQ.png" alt=""></p>
<p>아래 조합 표는 흥미로운 질문을 제기합니다. <code>error</code>: <code>true</code>인 입력 필드에 <code>readonly</code>: <code>true</code> 또는 <code>disabled</code>: <code>true</code>를 동시에 설정할 수 있을까요? 형식주의자(purist)의 관점에서 보자면, 아마도 가능할 것입니다.</p>
<p><img src="https://miro.medium.com/v2/resize:fit:2000/format:webp/1*5fVngudjyccTI3EkAo0vTA.png" alt=""></p>
<p>[상태별 오류, 비활성화, 읽기 전용 조합표]</p>
<p>하지만 HTML에서 <code>error</code>(<code>무효</code>?)와 <code>disabled</code> 또는 <code>readonly</code> 속성이 결합된 경우 다음과 같은 더 심각한 고려 사항이 발생합니다.</p>
<ul>
<li>상충되는 시각적 신호: <code>error</code> 상태인지, <code>disabled</code> 상태인지 어떻게 구분할까요?</li>
<li>상충되는 해결 방법: 입력값이 <code>error</code> 상태라면 사용자는 해당 입력값과 상호작용하여 문제를 해결할 수 있어야 합니다. <code>disabled</code> 상태라면 그렇지 않습니다.</li>
</ul>
<p>이는 API 디자인 문제라기보다는 사용자 경험 디자인 문제입니다. 지침에서는 사용자에게 필요하고 직관적이지 않은 경우 이 조합을 피하고 다른 UI 패턴을 사용하도록 권장할 수 있습니다. 그러나 피그마 컴포넌트와 이를 구현하는 개발자에 대한 관련 커뮤니케이션의 경우, 두 가지가 모두 <code>true</code>로 설정된 경우 어떤 일이 발생하는지 명확히 하기 위해 명시적인 에셋이 아니더라도 최소한 대화가 필요합니다.</p>
<p>속성은 입력값이 <code>error</code> 상태(일반적으로 빨간색과 X 아이콘을 사용)와 반대되는 <code>succuess</code> 상태(녹색과 체크마크 아이콘을 결합하는 경우가 많음)를 구현할 경우 더욱 발전합니다.</p>
<p><img src="https://miro.medium.com/v2/resize:fit:2000/format:webp/1*zCBj9CIXCY7ZMZWugH1bAw.png" alt=""></p>
<p>이 경우, <code>error</code>(또는 무효)는 <code>성공</code>(또는 <code>invalid</code>)과 상호 배타적이며, 시각화된 <code>성공</code> 상태는 <code>error</code>의 빨간색이나 <code>성공</code>의 녹색을 표시하지 않는 것과 다릅니다. 따라서, <code>none</code>(기본값), <code>error</code> 및 <code>성공</code> 옵션을 가진 열거형 검증 변수 속성을 고려하십시오.</p>
<h2 id="독립적인-속성으로-둘-것인가-아니면-다른-속성의-옵션으로-포함시킬-것인가">독립적인 속성으로 둘 것인가, 아니면 다른 속성의 옵션으로 포함시킬 것인가</h2>
<p>지금까지는 <code>focus</code>가 <code>rest</code>, <code>hover</code>, <code>active</code>와 같은 상태들과 상호 배타적인 선택지로 간주되어 왔습니다. 하지만 실제로는 하나의 요소가 <code>hover</code>+<code>focus</code>, 혹은 <code>active</code>+<code>focus</code> 상태를 동시에 가질 수 있습니다.</p>
<p><code>focus</code>(<code>true</code>/<code>false</code>)를 상호작용 <code>state</code>(<code>rest</code>, <code>hover</code>, <code>active</code>)와는 별개의 속성으로 분리해야 할까요?</p>
<p><img src="https://miro.medium.com/v2/resize:fit:2000/format:webp/1*4CWwfwd6_ZThgfub4MkECg.png" alt=""></p>
<p>[상태별 비활성화, 읽기 전용, 포커스 조합표]</p>
<p>앞선 섹션들을 통해 순수주의적 사고로 전환된 디자이너는, 우려를 과도하게 분리하려는 시도 끝에 <code>focus</code> 변형 속성을 따로 만들고 기본값을 <code>false</code>로 설정하려 할 수 있습니다. 하지만 이는 지나친 접근일 수 있습니다.</p>
<p>실용적인 관점에서 볼 때, <code>focus</code> 속성을 상태(<code>state</code>) 속성과 분리하는 것은 실제로는 꼭 필요하지 않습니다. 그 이유는 다음과 같습니다.</p>
<ul>
<li><p><strong>Focus는 거의 항상 클라이언트 측 상호작용에 의해 발생하며, 개발자가 설정하는 속성이 아닙니다</strong>. <code>focus</code>는 <code>hover</code>와 마찬가지로 사용자 상호작용에 따라 발생하는 인터랙티브 상태입니다. 일부 라이브러리에서는 <code>focus</code>를 속성으로 강제로 설정할 수 있도록 지원하지만, 대부분의 개발자는 <code>disabled</code>, <code>readonly</code>, <code>error</code>처럼 <code>focus</code>를 명시적으로 설정할 필요를 느끼지 않습니다.</p>
</li>
<li><p><strong>Focus는 보통 고유한 시각적 속성에 영향을 줍니다</strong>. <code>focus</code>는 일반적으로 <code>hover</code>나 <code>active</code>와는 구분되는 시각적 속성(예: 컨테이너의 그림자 또는 별도의 focus ring)을 변화시킵니다. 반면, <code>hover</code>나 <code>active</code>는 보통 배경색이나 테두리 색상에 영향을 주는 경우가 많습니다.</p>
</li>
<li><p><strong>그렇지 않은 경우에도 focus는 동일한 시각적 속성에서 우선순위를 갖습니다</strong>. 만약 <code>focus</code>가 <code>hover</code>와 마찬가지로 컨테이너의 테두리 두께나 색상을 변경한다고 하더라도, 일단 요소가 <code>focus</code> 상태라면 <code>hover</code>나 <code>active</code>의 약한 시각적 효과는 거의 또는 전혀 중요하지 않게 됩니다. 즉, <code>focus</code>는 <code>hover</code>보다 우선시되며, 이는 <code>disabled</code>가 <code>readonly</code>보다 우선시되는 것과 같은 맥락입니다. 또한 <code>focus</code>+<code>hover</code>가 동시에 적용될 때 이를 위한 별도의 고유한 스타일이 필요한 경우는 거의 없습니다.</p>
</li>
</ul>
<p><img src="https://miro.medium.com/v2/resize:fit:1400/format:webp/1*_RCGYFqIfu1e05AljV1bcA.png" alt=""></p>
<p>[크롬의 인스펙터에서 호버, 활성화, 및 다중 포커스 상태가 집합 형태로 그룹화되어 있습니다]</p>
<ul>
<li><strong>일반적으로 하나의 집합으로 여겨집니다</strong>. 이 세 가지는 매우 밀접하게 관련된 CSS 가상 클래스로, 그들의 존재를 통해 제시되고 일반적으로 추구됩니다. <code>focus</code>을 구분하려면, <code>hover</code>와 <code>active</code>를 구분하는 것도 좋을 것입니다. 그 과정에서, <code>focus-within</code>를 더욱 구분할 때가 되었을까요? 아직 그걸 코딩하는 개발자를 만난 적도 없고, 더구나 그 문제를 해결하는 디자이너도 본 적이 없습니다. 우리는 모두 언젠가 위대함을 추구할 수 있을까요?</li>
</ul>
<p>검토한 코드 예제들 대부분에는 <code>focused</code> 속성이 존재하지 않았습니다. 오직 Shopify Polaris만이 <code>focused</code> 속성을 제공하여 강제로 포커스 상태를 설정할 수 있도록 했습니다. 그 목적을 추측해 보자면, 아마도 input 내부에 슬롯으로 삽입된 요소와의 상호작용이 부모 컨테이너에 포커스 상태를 강제로 부여해야 할 필요가 있어서일 수 있습니다. 또한 Shopify Polaris의 <code>focused</code> 속성은 사용자 상호작용으로 발생하는 일반적인 포커스 상태와는 별개로, 이를 추가적으로 활성화할 수 있는 방식으로 작동합니다. 즉, 이 속성을 설정하면 실제 포커스 여부와 관계없이 포커스 스타일이 적용되며, 상호작용에 의한 <code>focus</code>와 병행될 수 있습니다.</p>
<p><code>focus</code>: <code>true</code>,<code>false</code>로 따로 분리해 상호작용 <code>state</code>: <code>rest</code>,<code>hover</code>,<code>active</code>와 같이 별도로 관리해야 한다는 주장에는 아직 설득되지 않습니다. 물론, 협업이 실패하지 않는 한 (그럴 일은 없겠지만), 또는 라이브러리 간의 자동화가 그것을 필요로 하기 전까지는 말이죠 (언젠가는 가능성 있습니다).</p>
<p>이 지점에서 멈추는 것은 좋은 시사점을 줍니다. 완벽한 정렬, 즉 디자인을 구성하기 위한 완벽한 모델을 추구하는 과정은 종종 그 완벽함에 도달하기도 전에 동력을 잃습니다. 3년 전의 그 블로그 글만 보더라도, 디자인 에셋과 코드 에셋 간의 <code>state</code>가 서로 다르게 유지되는 것에 대해 비교적 관대했습니다.</p>
<p>하지만 오늘날에는 훨씬 더 깊은 통합과 강한 정렬을 지향하는 방향으로 기준이 이동해 왔습니다. 이것은 수많은 팀들이 디자인 시스템을 구축하며 수년에 걸쳐 밟아가는 여정과 다르지 않습니다. 언젠가는 서로 수렴하게 될지도 모릅니다. 어쩌면 머지않아 말이죠.</p>
<h2 id="각주">각주</h2>
<p><a name="footnote_1">1</a>: <a href="https://ko.wikipedia.org/wiki/%ED%8C%A8%EA%B6%8C">헤게모니</a> - 어떤 집단이나 국가가 다른 집단이나 국가를 지배하거나 주도하는 위치에 있는 상태를 의미</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[(번역) 자바스크립트에서 Records & Tuples 제안이 철회된 이유]]></title>
            <link>https://velog.io/@tap_kim/why-was-records-and-tuples-proposal-withdrawn-in-javascript</link>
            <guid>https://velog.io/@tap_kim/why-was-records-and-tuples-proposal-withdrawn-in-javascript</guid>
            <pubDate>Mon, 30 Jun 2025 09:23:33 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>원문: <a href="http://bit.ly/4nstUvL">http://bit.ly/4nstUvL</a></p>
</blockquote>
<p>이미 많은 자바스크립트 커뮤니티 사람이 알고 있듯이, 최근 <a href="https://github.com/tc39/proposal-record-tuple/issues/394">레코드 및 튜플 제안이 철회</a>되었습니다. 많은 사람들이 매우 화가 났고, 일부는 심지어 EcmaScript 표준 위원회를 비난하기도 했습니다. 이 글에서는 이 기능이 철회된 이유에 대해 논의하고자 합니다.</p>
<!-- 목차 제외  -->

<h2 id="레코드와-튜플의-제안은-어떤-내용이었나요">레코드와 튜플의 제안은 어떤 내용이었나요?</h2>
<p>더 자세한 기술적 세부 사항을 살펴보기 전에 먼저 레코드와 튜플이 무엇인지 이해해 보겠습니다.</p>
<p>레코드와 튜플은 객체와 유사한 두 가지 새로운 원시 타입입니다. 두 가지 주요 기능은 다음과 같습니다.</p>
<ul>
<li>불변(Immutable)입니다. 한번 생성되면 읽기 전용이 되며, 그 이후에는 수정이 불가능합니다.</li>
<li>깊은 비교. 일반 객체는 참조로만 비교되므로 구조가 일치하더라도 다를 수 있지만, 이들은 <code>===</code> 연산자를 사용할 때 구조적으로 비교됩니다.</li>
</ul>
<p>이것이 레코드와 튜플이 선언되고 기본적으로 작동하는 방식입니다.</p>
<pre><code class="language-js">// 튜플은 일반 배열과 유사하게 선언되며, 시작 부분에 #만 있습니다.
const tuple1 = #[1, 2, 4];
const tuple2 = #[1, 2, 3];
const tuple3 = #[1, 2, 3];

// 레코드는 일반 객체와 유사하게 선언되며, 시작 부분에 #이 있을 뿐입니다.
const record1 = #{
    a: 1,
    b: tuple2,
};

const record2 = #{
    a: 1,
    b: tuple3,
};

const record3 = #{
    a: 1,
    b: tuple2,
    c: #{
        a: tuple2
    }
};

console.log(typeof tuple1); // &quot;tuple&quot;
console.log(typeof record1); // &quot;record&quot;

console.log(tuple1 === tuple2); // false, 구조가 일치하지 않는 경우
console.log(tuple2 === tuple3); // true, 구조가 일치

console.log(record1 === record3); // false, 구조가 일치하지 않는 경우
console.log(record1 === record2); // true, 구조가 일치

const map = new Map();

map.set(record1, &quot;record1&quot;); // 레코드와 튜플을 Map 키에도 사용 가능
console.log(map.get(record2)); // &quot;record1&quot;, record1 record2와 같음
console.log(map.get(record3)); // undefined, Map에서 동일한 구조의 키가 발견되지 않는 경우

console.log(tuple1.length); // 3, 일반 배열처럼 사용 가능


// 일반 배열처럼 반복하고 1, 2, 4를 출력합니다.
for (const x of tuple1) {
    console.log(x);
}

try {
    // 모든 수정 시도에서 오류를 발생시킵니다.
    tuple1[1] = 5;
} catch (e) {
    console.error(e);
}

try {
    // 모든 수정 시도에서 오류를 발생시킵니다.
    record2[&#39;k&#39;] = 5;
} catch (e) {
    console.error(e);
}

try {
    const record4 = #{
        a: 1,
        b: tuple2,
        // 중첩된 구조도 기본값이 불변해야 하므로 오류가 발생합니다.
        c: {
            a: tuple2
        }
    };
} catch (e) {
    console.error(e);
}</code></pre>
<p><a href="https://rickbutton.github.io/record-tuple-playground">플레이 그라운드</a>에서 실험해 볼 수 있습니다. 안타깝게도 <code>typeof</code> 키워드는 키워드는 올바르게 동작하지 않는데, 이는 단지 폴리필이기 때문이고 실제 자바스크립트 자체는 레코드와 튜플을 지원하지 않기 때문입니다.</p>
<p>레코드와 튜플은 일반 객체로부터도 생성할 수 있습니다.</p>
<pre><code class="language-js">const record = Record({
    a: 4
});

const tuple = Tuple.from([1, 2, 3]);


console.log(record, tuple); // #{ a: 4 }, #[1, 2, 3]</code></pre>
<p>그 외에도 다양한 기능이 있으며, 자세한 내용은 <a href="https://github.com/tc39/proposal-record-tuple">제안서 페이지</a>를 확인하세요.</p>
<h2 id="철회-사유">철회 사유</h2>
<p>이 예시만 보면 레코드 및 튜플은 특히 상태 관리에 매우 유용한 기능으로 보입니다. 그렇다면 이 기능이 철회된 이유는 무엇일까요?</p>
<h2 id="새로운-원시-타입primitive-type-추가">새로운 원시 타입(primitive type) 추가</h2>
<p>이 제안은 <code>레코드</code>와 <code>튜플</code>이라는 두 가지 원시 타입을 추가하는 것입니다. 새로운 원시 타입을 추가하는 것은 새로운 클래스나 API를 추가하는 것과는 다릅니다. 기본 타입은 자바스크립트 엔진의 핵심 작동 방식에 영향을 미치는데, 자바스크립트는 동적 언어이기 때문에 서로 다른 타입 간의 다양한 연산과 형 변환을 지속적으로 확인하고 처리해야 하기 때문입니다. 새로운 원시 타입은 이미 복잡한 자바스크립트 엔진에 더 많은 복잡성과 성능 오버헤드(<a href="https://github.com/tc39/proposal-record-tuple/issues/387#issuecomment-1881635273">레코드와 튜플을 사용하지 않는 코드의 경우에도</a>)를 발생시킬 수 있습니다.</p>
<h2 id="깊은-비교">깊은 비교</h2>
<p>깊은 비교는 효율적으로 작업하기 위해 복잡성을 더합니다. 한 가지 가능한 최적화 방법은 새 객체에 구조적 공유(structured sharing)를 최대한 활용하는 것입니다. 이렇게 하면 기존 레코드/튜플을 최대한 많이 사용하려고 시도하므로 메모리 사용량이 제한되고 생성/비교 속도가 빨라질 수 있습니다. 또 다른 가능한 최적화는 튜플/레코드에 첨부된 해시값을 사용하는 것이므로, 많은 경우 2개의 튜플/레코드가 다른지 빠르게 판단하는 데 도움이 될 것입니다. 그러나 <a href="https://github.com/tc39/proposal-record-tuple/issues/2#issuecomment-1177746818">깊은 비교가 선형 시간보다 더 빠르게 작동한다는 것은 보장되지 않는다는 것이 중론</a>입니다.</p>
<p> 인터닝(interning)<sup><a href="#footnote_1">1</a></sup>  없이 이를 최적화하는 것은 어려워 보였기 때문에 구현자들은 <a href="https://github.com/tc39/proposal-record-tuple/issues/387#issuecomment-1881635273">커밋을 꺼려했습니다</a>.</p>
<h2 id="동등성-의미의-일관성">동등성 의미의 일관성</h2>
<p>값 기반의 <code>===</code>가 없으면 레코드와 튜플은 대부분의 타입에서 <code>===</code>, <code>SameValue</code>(<code>NaN === NaN</code> 및 <code>0 !== -0</code>을 제외한 <code>===</code>와 동일), <code>SameValueZero</code>(<code>NaN === NaN</code>을 제외한 <code>===</code>와 동일)가 일치한다는 오랜 규칙을 <a href="https://github.com/tc39/proposal-record-tuple/issues/387#issuecomment-2466245250">깨뜨리게</a> 됩니다. 이 규칙을 유지한다는 것은 복잡성 비용을 지불한다는 것을 의미했습니다. 이 규칙을 깨는 것은 JS에서 다섯 번째 방식의 동등성 비교가 생긴다는 것을 의미했습니다. 두 가지 옵션 중 어느 쪽도 주목받지 못했습니다. 또한 값 기반 동등성을 제거하면 레코드와 튜플이 <a href="https://github.com/tc39/proposal-record-tuple/issues/387#issuecomment-1898657155">거의 무의미해집니다</a> 😒.</p>
<h2 id="합성이-더-나은-대안이-될-수-있을까요">합성이 더 나은 대안이 될 수 있을까요?</h2>
<p>레코드와 튜플 제안을 진행하기에는 너무 많은 문제와 불확실성이 있었기 때문에 결국 이 기능은 철회되었습니다. <a href="https://github.com/tc39/proposal-composites">합성</a>에 대한 새로운 대안 제안이 있습니다. 개인적으로 저는 아직 현재 구현이 그다지 인상적이지 않습니다. <code>Map</code>/<code>Set</code>에서는 구조적으로 비교되지만, <code>WeakMap</code>/<code>WeakSet</code>에서는 참조로 비교되는 등 몇 가지 불일치가 여전히 존재합니다.</p>
<pre><code class="language-js">const pos1 = Composite({ x: 1, y: 4 });
const pos2 = Composite({ x: 1, y: 4 });
Composite.equal(pos1, pos2); // true

const positions = new Set(); // 표준 ES Set
const weakPositions = new WeakSet(); // 표준 ES WeakSet
positions.add(pos1);
weakPositions.add(pos2);

positions.has(pos2); // true, 좋아요, 말이 되네요(적어도 우리가 원했던 건 이겁니다),
          // 하지만 객체임에도 불구하고 일반적인 참조 아이덴티티 대신 특별한 방식으로 처리됩니다.
weakPositions.has(pos2); // false, 이전 동작과 일치하지만, 지금은 Set으로 처리</code></pre>
<p>솔직히 말해서 구조적 키를 위한 불가피한 선택일 수 있지만, 좀 더 지켜볼 필요가 있습니다.</p>
<h2 id="각주">각주</h2>
<p><a name="footnote_1">1</a>: <a href="https://en.wikipedia.org/wiki/Interning_(computer_science)">인터닝(interning)</a>: 컴퓨터 과학에서 인터닝은 새로운 객체를 만드는 대신 필요에 따라 동일한 가치의 객체를 재사용하는 것을 말합니다. 이 생성 패턴은 여러 프로그래밍 언어에서 숫자와 문자열에 자주 사용됩니다. 파이썬과 같은 많은 객체 지향 언어에서는 정수 같은 원시 유형도 객체입니다. 많은 수의 정수형 객체를 생성하는 데 따른 오버헤드를 피하기 위해 이러한 객체는 인턴을 통해 재사용됩니다. 인터닝이 작동하려면 여러 변수 간에 상태가 공유되므로 인터닝된 객체는 불변이어야 합니다. 문자열 인터닝은 동일한 프로그램에서 동일한 값을 가진 많은 문자열이 필요한 경우 인터닝의 일반적인 응용 프로그램입니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[(번역) 나만의 아키텍처 구성하기]]></title>
            <link>https://velog.io/@tap_kim/choose-your-own-architecture</link>
            <guid>https://velog.io/@tap_kim/choose-your-own-architecture</guid>
            <pubDate>Wed, 21 May 2025 16:27:47 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>원문: <a href="https://itnext.io/choose-your-own-architecture-92c56b12f7b0">https://itnext.io/choose-your-own-architecture-92c56b12f7b0</a></p>
</blockquote>
<p><em>이 글은 현재 <a href="https://leanpub.com/metapatterns">Leanpub</a>과 <a href="https://github.com/denyspoltorak/publications/tree/main/ArchitecturalMetapatterns">GitHub</a>에서 무료로 제공되는 제 저서 <a href="https://medium.com/itnext/the-list-of-architectural-metapatterns-ed64d8ba125d">&#39;아키텍처 메타 패턴: 소프트웨어 아키텍처의 패턴 언어&#39;</a>의 일부입니다. 어떤 피드백도 환영합니다.</em></p>
<p>이전 글에서 <a href="https://medium.com/itnext/deconstructing-patterns-a605967e2da6">패턴을 결합과 응집으로 분해</a>해 보았으니, 이제 프로젝트의 필요에 따라 아키텍처를 재구성해 볼 수 있습니다.</p>
<h2 id="프로젝트-규모">프로젝트 규모</h2>
<p>프로젝트의 예상 규모는 프로젝트 아키텍처의 주요 결정 요인 중 하나로, <a href="https://medium.com/itnext/cohesers-and-decouplers-ecac2964081a">지나치게 많은 컴포넌트와 과도한 파편화</a>는 개발 및 유지 관리에 장애가 되기 때문입니다. 적당한 규모의 안전지대(zone of comport)는 적당한 크기의 컴포넌트 수를 가집니다.</p>
<p>따라서 하루 만에 끝낼 수 있는 정도의 작업은 <a href="https://medium.com/itnext/monolith-e84e8454106b"><em>모놀리식</em></a>이 될 가능성이 크고, 한 달 정도의 작업은 <a href="https://medium.com/itnext/layers-138e793adf51"><em>계층화</em></a>가 필요하며, 그 이상의 작업은 적어도 부분적으로 <a href="https://medium.com/itnext/services-ab8a45878621"><em>하위 도메인 모듈이나 서비스</em></a>로 분리해야 할 가능성이 높습니다. 매우 큰 프로젝트는 <a href="https://medium.com/itnext/service-oriented-architecture-soa-5d0cd2b8464c"><em>서비스 지향 아키텍처</em></a>(SOA) 또는 <a href="https://medium.com/itnext/hierarchy-7352e21f301f"><em>셀 기반 아키텍처</em></a>로 더 세분화해야 할 수도 있습니다.</p>
<p><img src="https://miro.medium.com/v2/resize:fit:720/format:webp/0*eo3WPXdmPKBbiI_D" alt=""></p>
<p>초기 설계에서 고려해야 할 또 다른 요소는 도메인 내에 내재된 디커플링입니다. 예를 들어 도메인 로직이 있는 계층에는 개발 또는 런타임 비용이 거의 들지 않는 <a href="https://medium.com/itnext/services-ab8a45878621">모듈이나 서비스</a>를 자연스럽게 만드는 독립적인 하위 도메인이 포함될 가능성이 매우 높습니다. 마찬가지로 <a href="https://medium.com/itnext/hierarchy-7352e21f301f"><em>하향식 계층 구조</em></a>는 계층형 도메인에 적합합니다. 데이터나 이벤트의 단계적 처리를 중심으로 구축되는 도메인은 매우 유연한 아키텍처 스타일인 <a href="https://medium.com/itnext/pipeline-88e24688b5ec"><em>파이프라인</em></a>으로 모델링할 수 있습니다.</p>
<p><img src="https://miro.medium.com/v2/resize:fit:1400/format:webp/0*v3npx9pGMm9sfseK" alt=""></p>
<p>프로젝트를 시작할 때 함께하는 팀의 수도 중요합니다. 팀의 효율성을 최대한 높이려면 팀이 대부분 독립적으로 운영되는 것이 좋습니다. 각 팀에 하나 또는 두 개의 컴포넌트를 맡기고, 팀이 전문성을 가질 수 있도록 아키텍처에 충분한 모듈이나 서비스가 있어야 합니다. 공유되는 것은 병목현상이 될 수 있기 때문입니다. 예를 들어, 어떤 시스템에는 계층이 너무 많아 <a href="https://medium.com/itnext/layers-138e793adf51"><em>계층형 아키텍처</em></a>에서는 3개 이상의 팀을 투입하기 어렵습니다. 따라서 팀 수가 많다는 것은 <a href="https://medium.com/itnext/services-ab8a45878621"><em>서비스</em></a>, <a href="https://medium.com/itnext/pipeline-88e24688b5ec"><em>파이프라인</em></a>, <a href="https://medium.com/itnext/service-oriented-architecture-soa-5d0cd2b8464c"><em>SOA</em></a> 또는 <a href="https://medium.com/itnext/hierarchy-7352e21f301f"><em>계층 구조</em></a> 아키텍처가 적합합니다.</p>
<p>모든 팀이 0일부터 채용되는 것이 아니라 일부 팀이 나중에 채용되는 경우에도 이미 구현된 컴포넌트를 세분화하는 것은 끔찍한 경험이므로 예상되는 팀 수에 대한 컴포넌트 경계를 처음에 설정하는 것이 바람직합니다. 그러나 지금은 여러 컴포넌트를 단일 프로세스(<a href="https://medium.com/itnext/services-ab8a45878621"><em>모듈</em></a>)로 실행하는 것이 <a href="https://martinfowler.com/bliki/MonolithFirst.html">더 쉽고 안전할</a> 수 있으므로 분산으로 인한 오버헤드를 피하고 필요한 경우(새로운 요구 사항으로 인해 디자인에 문제가 생기는 경우가 많으므로) 코드 조각을 이동하는 데 어려움을 덜 겪을 수 있습니다. 필요하게 되면 <em>모듈</em>을 <em>서비스</em>로 만들기 위해 약간의 노력을 해야 합니다.</p>
<h2 id="도메인-기능">도메인 기능</h2>
<p>위에서 계층형 또는 파이프라인 도메인을 통해 해당 아키텍처를 사용할 수 있다는 것을 이미 살펴보았습니다. 그 외에도 여러 가지가 있습니다.</p>
<p>모든 사용 사례가 전체 시스템에 걸쳐 여러 컴포넌트를 포함하므로 하위 도메인과 일치할 수 없는 복잡한 사용 사례가 많이 있을 수 있습니다. 일반적으로 글로벌 사용 사례로 전용 컴포넌트인 <a href="https://medium.com/itnext/orchestrator-0708881ffdb1"><em>오케스트레이터</em></a>로 수집합니다. 그리고 <em>오케스트레이터</em>가 통제할 수 없을 정도로 커지면 레이어 또는 서비스로 세분화됩니다.</p>
<p><img src="https://miro.medium.com/v2/resize:fit:1400/format:webp/0*Q2DQqDwRmSidh3bW" alt=""></p>
<p>다른 시스템은 <a href="https://medium.com/itnext/shared-data-51b310334511">데이터를 중심으로 구축</a>됩니다. 대부분의 서비스가 전체적으로 접근해야 하므로 데이터를 개인 데이터베이스로 분할할 수 없습니다. <a href="https://medium.com/itnext/shared-repository-189d9b3b448c">공유 저장 공간</a> 또는 고성능 <em>공간 기반 아키텍처</em>가 필요합니다.</p>
<p><img src="https://miro.medium.com/v2/resize:fit:1400/format:webp/0*pyLDsoEVL-gy4rpY" alt=""></p>
<p>분산화되면 서비스 간의 통신을 중앙 집중화하기 위해 <a href="https://medium.com/itnext/middleware-eb7302799596"><em>미들웨어</em></a>를 사용할 겁니다. 그리고 <em>방화벽</em>, <em>리버스 프록시</em>, <em>응답 캐시</em> 등 다양한 <a href="https://medium.com/itnext/proxy-f378298d0bf1"><em>프록시</em></a>를 사용하게 될 것입니다. 클라이언트의 프로토콜이 다른 경우 클라이언트 종류별로 프록시를 배포하여 <a href="https://medium.com/itnext/backends-for-frontends-bff-5cbcb4b67a30"><em>프론트엔드를 위한 백엔드</em></a>를 만들 수도 있습니다.</p>
<p><img src="https://miro.medium.com/v2/resize:fit:1400/format:webp/0*k-EIyMpVHeceGrsm" alt=""></p>
<h2 id="런타임-성능">런타임 성능</h2>
<p>더나아가 성능이나 결함 내성과 같은 <a href="https://medium.com/itnext/conflicting-forces-asynchronicity-and-distribution-b53beee65e74">비기능적 요구 사항</a>도 있습니다.</p>
<p>비즈니스 로직이나 데이터를 <a href="https://medium.com/itnext/shards-2637f1ae7771"><em>샤딩</em></a> 또는 복제하면 높은 처리량을 달성할 수 있습니다. 또한 <em>샤딩</em>은 대용량 데이터 셋을 처리하는 데 도움이 되며, <em>복제</em>는 내결함성을 향상시킵니다. <a href="https://medium.com/itnext/combined-component-51c3205c94de">공간 기반 아키텍처</a>는 전체 데이터 셋을 메모리에 복제하여 더 빠르게 접근할 수 있도록 합니다.</p>
<p><img src="https://miro.medium.com/v2/resize:fit:1400/format:webp/0*5s8a9oix_0BQSjkT" alt=""></p>
<p>또는 여러 개의 특수 데이터베이스(<a href="https://medium.com/itnext/polyglot-persistence-21a6e5bc5f9e"><em>폴리글롯 지속성(Polyglot Persistence)</em></a>)를 사용하거나 시스템의 부하가 높은 부분을 자체 확장 <a href="https://medium.com/itnext/pipeline-88e24688b5ec"><em>파이프라인</em></a>으로 재설계할 수도 있습니다.</p>
<p><img src="https://miro.medium.com/v2/resize:fit:1400/format:webp/0*_0_TMdho3NKvGdiZ" alt=""></p>
<p>고르지 않은 부하에서 확장성은 <a href="https://medium.com/itnext/pipeline-88e24688b5ec"><em>서비스형 기능</em></a>(나노 서비스), <em>서비스</em> <a href="https://medium.com/itnext/mesh-c9f3f3f2854f"><em>메시</em></a> 기반 <a href="https://medium.com/itnext/services-ab8a45878621"><em>마이크로 서비스</em></a>, 더 나아가 <a href="https://medium.com/itnext/mesh-c9f3f3f2854f"><em>공간 기반 아키텍처</em></a>를 통해 달성할 수 있습니다.</p>
<p><img src="https://miro.medium.com/v2/resize:fit:1400/format:webp/0*JvTtnwPapCo5-2Mk" alt=""></p>
<p>내결함성을 위해서는 데이터베이스를 포함한 모든 컴포넌트의 <em>복제본</em>을 여러 데이터센터에 걸쳐 보유하는 것이 이상적입니다. 그렇게 풍부하지 않다면 <a href="https://medium.com/itnext/services-ab8a45878621"><em>액터(Actor)</em></a>나 <a href="https://medium.com/itnext/mesh-c9f3f3f2854f"><em>메시(Mesh)</em></a>로 만족하세요.</p>
<p><img src="https://miro.medium.com/v2/resize:fit:1400/format:webp/0*lY066AAmp8DPEEy5" alt=""></p>
<p>짧은 지연 시간을 요구할 경우 입력에 가까운 곳에 간소화된 첫 번째 응답 로직을 배치할 수 있으며, 이는 다음과 같은 형태를 띱니다.</p>
<ul>
<li>사용자 상호작용을 위한 <a href="https://herbertograca.com/2017/07/03/the-software-architecture-chronicles/"><em>MVP 또는 MVC</em></a> 패턴 조합을 사용할 수 있습니다.</li>
<li>단일 하드웨어 입력을 위한 <em>전략 주입 기법(strategy injection)</em> 이 적용된 <a href="https://medium.com/itnext/layers-138e793adf51"><em>레이어</em></a>.</li>
<li><a href="https://en.wikipedia.org/wiki/Industrial_internet_of_things">IIoT</a>와 같은 분산 제어 시스템을 위한 <a href="https://medium.com/itnext/hierarchy-7352e21f301f"><em>계층 구조</em></a>.</li>
</ul>
<p><img src="https://miro.medium.com/v2/resize:fit:1400/format:webp/0*Kxq_PWYuHXyJNwsS" alt=""></p>
<h2 id="유연성">유연성</h2>
<p>제품에 커스터마이징이 필요하다면 <a href="https://medium.com/itnext/plugins-a70bd06bd36f"><em>플러그인</em></a>을 선택하세요.</p>
<p>10년 동안 살아남으려면 공급업체를 변경할 수 있는 <a href="https://medium.com/itnext/hexagonal-architecture-fe1250fb52be"><em>헥사고날 아키텍처</em></a>가 필요합니다.</p>
<p>리소스 또는 서비스 제공자와 소비자 사이를 중개하는 경우 <a href="https://medium.com/itnext/microkernel-abb60773e469"><em>마이크로커널</em></a>을 구축합니다.</p>
<p><img src="https://miro.medium.com/v2/resize:fit:1400/format:webp/0*p4gVgXcFk2yoM5ri" alt=""></p>
<p>팀이 서비스를 개발하면서 <a href="https://medium.com/itnext/indirection-in-commands-and-queries-bb32f492814f">상호 의존성을 줄이려면</a> 서비스 사이에 <em>부패 방지 계층</em>(anticorruption layer), <em>오픈 호스트 서비스</em> 또는 <a href="https://medium.com/itnext/polyglot-persistence-21a6e5bc5f9e"><em>CQRS 뷰</em></a>를 삽입합니다.</p>
<p>대규모 시스템을 구축하고 철저한 데이터 분석이 정말 필요한 경우, <em>데이터 메시</em>를 구현하는 것을 고려하세요.</p>
<p><img src="https://miro.medium.com/v2/resize:fit:1400/format:webp/0*R4sWGb8cz_gO2jiQ" alt=""></p>
<h2 id="모든-도메인은-고유합니다">모든 도메인은 고유합니다</h2>
<p>모든 것을 만족하는 규모는 없습니다. 임베디드 프로젝트나 싱글 플레이어 게임에는 데이터베이스가 없고 단일 프로세스에서 실행됩니다. 고빈도 트레이딩은 OS 커널을 우회하여 마이크로초를 절약합니다. 미들웨어와 분산 데이터베이스는 쿼럼과 리더 선출에 신경을 씁니다. 대규모 데이터 처리는 <a href="https://en.wikipedia.org/wiki/Soft_error">비트 플립</a>을 고려해야 합니다. 의료 기기는 절대로 충돌해서는 안 됩니다. 은행은 외부 감사를 위해 기록을 영원히 보관합니다.</p>
<p>모든 상황에 통하는 보편적인 아키텍처는 없습니다. 만능 해결책도 없습니다. 패턴은 단순한 도구일 뿐입니다. 도구를 잘 알고 현명하게 선택해야 합니다.</p>
<h2 id="체념">체념</h2>
<blockquote>
<p>&quot;Software architecture lies lifeless in my hands, devoid of its magical colors, <em>like the dead iguana.</em>&quot;</p>
</blockquote>
<p>- 이젠 소프트웨어 아키텍처가 전처럼 설레지도 않고, 그 매력도 사라져 버렸다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[(번역) 리액트 개발자를 위한 초기 로드 성능 심층 분석하기]]></title>
            <link>https://velog.io/@tap_kim/initial-load-performance</link>
            <guid>https://velog.io/@tap_kim/initial-load-performance</guid>
            <pubDate>Tue, 29 Apr 2025 15:00:16 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>원문: <a href="https://www.developerway.com/posts/initial-load-performance">https://www.developerway.com/posts/initial-load-performance</a></p>
</blockquote>
<p><em>핵심 웹 바이탈, 성능 개발 도구, 초기 로드 성능이 무엇인지, 어떤 지표가 이를 측정하는지, 캐시 제어 및 다양한 네트워킹 조건이 성능에 어떤 영향을 미치는지 살펴보세요.</em></p>
<p>최근 AI 기반 코드 생성이 급성장하면서 리액트 코드 작성의 중요성이 줄어들고 있습니다. 이제 누구나, 무엇이든 리액트로 앱을 작성할 수 있습니다. 하지만 코드 작성은 항상 퍼즐의 한 부분일 뿐입니다. 우리는 여전히 앱을 어딘가에 배포하고, 사용자에게 보여주고, 견고하게 만들고, 빠르게 만드는 등 수많은 작업을 수행해야 합니다. 인공지능은 이러한 작업을 대신할 수 없습니다. 적어도 아직은 말이죠.</p>
<p>오늘은 앱을 빠르게 만드는 데 집중해 봅시다. 그러기 위해서는 잠시 리액트를 벗어나야 합니다. 왜냐하면 무언가를 빠르게 만들기 전에 먼저 &quot;빠름&quot;이 무엇인지, 어떻게 측정해야 하는지, 무엇이 &quot;빠름&quot;에 영향을 미칠 수 있는지 알아야 하기 때문입니다.</p>
<p>스포일러 주의: 이 글에서는 스터디 프로젝트 외에는 리액트에 대해 언급하지 않습니다. 오늘은 성능 도구 사용 방법, 핵심 웹 바이탈 소개, 크롬 성능 패널, 초기 로드 성능의 정의, 측정 지표, 캐시 제어 및 다양한 네트워킹 조건이 성능에 미치는 영향 등 기본적인 내용에 대해 설명합니다.</p>
<h2 id="초기-로드-성능-지표-소개">초기 로드 성능 지표 소개</h2>
<p>브라우저를 열고 즐겨찾는 웹사이트로 이동하려고 하면 어떻게 되나요? 주소창에 &quot;<a href="http://www.my-website.com&quot;%EB%A5%BC">http://www.my-website.com&quot;를</a> 입력하면 브라우저가 서버에 GET 요청을 보내고 HTML 페이지를 받습니다.</p>
<p><img src="https://www.developerway.com/assets/initial-load-performance/website-round-trip.png" alt=""></p>
<p>이 작업을 수행하는 데 걸리는 시간을 <a href="https://web.dev/articles/ttfb">&quot;첫 번째 바이트까지의 시간&quot;</a>(TTFB)이라고 하며, 이는 요청을 보낸 후 그에 대한 결과가 처음으로 도착하기까지 걸리는 시간입니다. HTML을 수신한 후 브라우저는 가능한 한 빨리 이 HTML을 사용 가능한 웹사이트로 변환해야 합니다.</p>
<p><a href="https://web.dev/learn/performance/understanding-the-critical-path?hl=ko">&quot;중요 경로&quot;</a>는 사용자에게 보여줄 수 있는 최소한의 가장 중요한 콘텐츠이며, 이를 화면에 렌더링하는 것으로 시작됩니다.</p>
<p><img src="https://www.developerway.com/assets/initial-load-performance/working-on-critical-path-20250102-003450.png" alt=""></p>
<p>중요 경로에 정확히 무엇이 포함되어야 하는지는 복잡한 문제입니다. 이상적으로는 사용자가 완전한 경험을 바로 접할 수 있도록 모든 것이 포함되어야 합니다. 하지만 &quot;중요&quot; 경로이기 때문에 되도록 빨라야 하므로 아무것도 포함하지 않는 것도 좋습니다. 두 가지를 동시에 달성하는 것은 불가능하므로 타협이 필요합니다.</p>
<p>타협안은 다음과 같습니다. 브라우저가 &quot;중요 경로&quot;를 구축하려면 최소한 해당 유형의 리소스가 반드시 필요하다고 가정합니다.</p>
<ul>
<li>서버에서 수신한 초기 HTML - 경험을 제공하는 실제 DOM 요소를 구성하기 위한 것입니다.</li>
<li>초기 요소의 <em>스타일</em>을 지정하는 중요한 CSS 파일 - 기다리지 않고 진행하면 사용자에게 처음에 스타일이 지정되지 않은 콘텐츠의 이상한 &quot;플래시&quot;가 표시됩니다.</li>
<li>레이아웃을 동기적으로 변경하는 중요한 자바스크립트 파일.</li>
</ul>
<p>브라우저는 서버로부터 초기 요청에서 첫 번째 파일(HTML)을 받습니다. 브라우저는 이를 파싱하기 시작하고, 파싱하는 동안 &quot;중요 경로&quot;를 완성하는 데 필요한 CSS 및 JS 파일에 대한 링크를 추출합니다. 그런 다음 서버에 요청을 전송하고 다운로드될 때까지 기다렸다가 처리하고 이 모든 것을 결합한 후 마지막에 화면에 &quot;중요 경로&quot; 픽셀을 그립니다.</p>
<p>브라우저는 이러한 중요한 리소스 없이는 초기 렌더링을 완료할 수 없으므로 이러한 리소스를 &quot;렌더링 차단 리소스&quot;라고 합니다. 물론 모든 CSS 및 JS 리소스가 렌더링 차단 리소스는 아닙니다. 일반적으로는 아래와 같습니다.</p>
<ul>
<li>인라인 또는 <code>&lt;link&gt;</code> 태그를 통한 대부분의 CSS.</li>
<li><code>async</code> 또는 <code>deferred</code>가 아닌 <code>&lt;head&gt;</code> 태그 내부의 자바스크립트 리소스.</li>
</ul>
<p>&quot;중요 경로&quot;를 렌더링하는 전체 프로세스는 대략 다음과 같습니다.</p>
<ul>
<li>브라우저가 초기 HTML 구문 분석을 시작합니다.</li>
<li>이 과정에서 <code>&lt;head&gt;</code> 태그에서 CSS 및 JS 리소스에 대한 링크를 추출합니다.</li>
<li>그런 다음 다운로드 프로세스를 시작하고 렌더링 차단 리소스가 다운로드를 완료할 때까지 기다립니다.</li>
<li>기다리는 동안 가능한 경우 HTML 처리를 계속합니다.</li>
<li>모든 중요한 리소스가 수신되면 해당 리소스도 처리됩니다.</li>
<li>마지막으로 필요한 모든 작업을 완료하고 인터페이스의 실제 픽셀을 페인트합니다.</li>
</ul>
<p>이 시점을 <strong>First Paint</strong>(FP)라고 합니다. 사용자가 화면에서 무언가를 볼 수 있는 첫 번째 기회입니다. 표시 여부는 서버가 전송한 HTML에 따라 달라집니다. 텍스트나 이미지와 같이 의미 있는 내용이 포함되어 있다면 이 시점이 <a href="https://web.dev/articles/fcp">First Contentful Paint</a>(FCP)가 발생한 시점이기도 합니다. HTML이 빈 div에 불과하다면 FCP는 나중에 발생합니다.</p>
<p><img src="https://www.developerway.com/assets/initial-load-performance/second-step-20250102-033953.png" alt=""></p>
<p><strong>First Contentful Paint(FCP)</strong> 는 <em>인지된 초기 로드</em>를 측정하기 때문에 가장 중요한 성능 지표 중 하나입니다. 기본적으로 웹사이트가 얼마나 빠른지에 대한 사용자의 첫인상입니다.</p>
<p>이 순간까지 사용자들은 빈 화면을 바라보며 손톱을 물어뜯고 있을 뿐입니다. <a href="https://web.dev/articles/fcp#what-is-a-good-fcp-score">구글에 따르면</a> 좋은 FCP 수치는 <strong>1.8초 미만</strong>입니다. 그 이후에는 사용자가 웹사이트가 제공하는 콘텐츠에 흥미를 잃고 이탈하기 시작할 수 있습니다.</p>
<p>하지만 FCP는 완벽하지 않습니다. 웹사이트가 스피너나 로딩 화면으로 로딩을 시작하는 경우 FCP 지표가 이를 나타냅니다. 하지만 사용자가 단지 멋진 로딩 화면을 확인하기 위해 웹사이트를 방문했을 가능성은 거의 없습니다. 대부분의 경우 콘텐츠에 접근하려는 것이 목적입니다.</p>
<p>이를 위해 브라우저는 시작한 작업을 완료해야 합니다. 나머지 논블로킹 자바스크립트를 기다렸다가 실행하고, 자바스크립트에서 발생한 변경 사항을 화면의 DOM에 적용하고, 이미지를 다운로드하고, 사용자 경험을 다듬는 등의 작업을 수행합니다.</p>
<p>이 프로세스 중 어딘가에서 <a href="https://web.dev/articles/lcp">Largest Contentful Paint</a>(LCP) 시간이 발생합니다. FCP와 같은 첫 번째 요소 대신 페이지의 주요 콘텐츠 영역, 즉 뷰포트에 표시되는 가장 큰 텍스트, 이미지 또는 동영상을 나타냅니다. <a href="https://web.dev/articles/vitals#core-web-vitals">구글에 따르면</a> 이 수치는 이상적으로는 <strong>2.5초 미만</strong>이어야 합니다. 그 이상이면 사용자는 웹사이트가 느리다고 생각할 것입니다.</p>
<p><img src="https://www.developerway.com/assets/initial-load-performance/third-step-20250102-051229.png" alt=""></p>
<p>이러한 모든 지표는 페이지의 사용자 경험을 나타내는 일련의 지표인 구글 <a href="https://web.dev/articles/vitals">웹 바이탈</a>의 일부입니다. LCP는 사용자 경험의 다양한 부분을 나타내는 세 가지 지표인 <strong>핵심 웹 바이탈</strong> 중 하나입니다. <strong>LCP</strong>는 <strong><em>로딩 성능</em></strong> 을 담당합니다.</p>
<p>이러한 지표은 <a href="https://developer.%ED%81%AC%EB%A1%AC.com/docs/lighthouse/overview">라이트하우스</a>로 측정할 수 있습니다. 라이트하우스는 크롬 개발자도구에 통합된 구글 성능 측정 도구로 셸 스크립트, 웹 인터페이스 또는 노드 모듈을 통해 실행할 수도 있습니다. 노드 모듈 형태로 사용하면 빌드 내에서 실행하여 프로덕션에 적용되기 전에 회귀를 감지할 수 있습니다. 로컬 디버깅 및 테스트에는 통합 개발 도구 버전을 사용하세요. 웹 버전은 경쟁사의 성능을 확인할 수 있습니다.</p>
<h2 id="개발자-도구-성능-탭-살펴보기">개발자 도구 성능 탭 살펴보기</h2>
<p>위의 내용은 모두 프로세스에 대한 매우 간략하고 간단한 설명입니다. 하지만 이미 많은 약어와 이론으로 머릿속이 복잡해집니다. 개인적으로 이런 글을 읽는 것은 아무 소용이 없습니다. 직접 눈으로 보고 손으로 만져보지 않으면 금방 잊어버리기 때문이죠.</p>
<p>이 특정 주제의 경우 개념을 완전히 이해하는 가장 쉬운 방법은 거의 실제와 같은 페이지에서 다양한 시나리오를 시뮬레이션하고 결과가 어떻게 달라지는지 확인하는 것입니다. 그러니 더 많은 이론을 공부하기 전에 정확히 그렇게 해봅시다(그 외에도 훨씬 더 많은 것이 있습니다!).</p>
<h3 id="프로젝트-설정">프로젝트 설정</h3>
<p>원한다면 아래의 모든 시뮬레이션을 자체 프로젝트에서 수행할 수 있으며, 결과는 거의 동일할 것입니다. 그러나 좀 더 통제되고 단순화된 환경을 원한다면 이 글을 위해 준비한 스터디 프로젝트를 사용하는 것이 좋습니다. 여기 <a href="https://github.com/developerway/initial-load-performance">레포지토리</a>에서 확인할 수 있습니다.</p>
<p>모든 의존성을 설치하는 것으로 시작합니다.</p>
<pre><code class="language-bash">npm install</code></pre>
<p>프로젝트를 빌드합니다.</p>
<pre><code class="language-bash">npm run build</code></pre>
<p>그리고 서버를 실행합니다.</p>
<pre><code class="language-bash">npm run start</code></pre>
<p>&quot;<a href="http://localhost:3000&quot;%EC%97%90">http://localhost:3000&quot;에</a> 접속하면 멋진 대시보드 페이지가 표시됩니다.</p>
<h3 id="필요한-개발자-도구-살펴보기">필요한 개발자 도구 살펴보기</h3>
<p>크롬에서 분석하려는 웹사이트를 열고 크롬 개발자 도구를 엽니다. 거기에서 &quot;성능&quot; 및 &quot;라이트하우스&quot; 패널을 찾아 서로 가깝게 이동합니다. 두 패널이 모두 필요합니다.</p>
<p>또한 이 글의 다른 작업을 수행하기 전에 &quot;캐시 사용 안 함(Disable cache)&quot; 확인란이 활성화되어 있는지 확인하세요. 이 체크박스는 맨 위에 있는 네트워크 패널에 있어야 합니다.</p>
<p><img src="https://www.developerway.com/assets/initial-load-performance/disable-cache-20250103-044948.png" alt=""></p>
<p>이는 웹사이트에 처음 방문하고 아직 브라우저에 캐시된 리소스가 없는 첫 번째 방문자를 모방하기 위한 것입니다.</p>
<h4 id="라이트하우스-패널-살펴보기">라이트하우스 패널 살펴보기</h4>
<p>지금 라이트하우스 패널을 엽니다. 몇 가지 설정과 &quot;페이지 로드 분석(Analyze page load)&quot; 버튼이 표시됩니다.</p>
<p><img src="https://www.developerway.com/assets/initial-load-performance/lighthouse-panel-20250103-002418.png" alt=""></p>
<p>이 섹션에서는 페이지의 초기 로딩에 대한 자세한 분석을 실행하는 &quot;탐색(Navigation)&quot; 모드에 관심이 있습니다. 보고서에는 다음과 같은 점수가 표시됩니다.</p>
<p><img src="https://www.developerway.com/assets/initial-load-performance/lighthouse-scores-20250103-003350.png" alt=""></p>
<p>로컬 성능은 완벽합니다. 모든 것이 항상 &quot;내 컴퓨터에서 작동&quot;하니 놀랄 일도 아닙니다.</p>
<p>다음과 같은 지표도 있습니다.</p>
<p><img src="https://www.developerway.com/assets/initial-load-performance/lighthouse-metrics-20250103-003618.png" alt=""></p>
<p>이 글에 필요한 FCP 및 LCP 값은 바로 상단에 있습니다.</p>
<p>아래에서 점수 향상에 도움이 될 수 있는 제안 목록을 확인할 수 있습니다.</p>
<p><img src="https://www.developerway.com/assets/initial-load-performance/lighthouse-suggestions-20250103-004020.png" alt=""></p>
<p>모든 제안을 확장할 수 있으며, 여기에서 더 자세한 정보를 확인할 수 있고 때로는 특정 주제를 설명하는 링크도 찾을 수 있습니다. 모든 제안을 실행할 수 있는 것은 아니지만, 성능에 대해 시작하고 성능을 개선할 수 있는 다양한 사항에 대해 자세히 알아볼 수 있는 놀라운 도구입니다. 이러한 보고서와 관련 링크를 읽는 데만 몇 시간을 소비할 수도 있습니다.</p>
<p>하지만 라이트하우스는 표면적인 정보만 제공하며 느린 네트워크나 CPU 부족과 같은 다양한 시나리오를 시뮬레이션할 수 없습니다. 다만 시간 경과에 따른 성능 변화를 추적할 수 있는 훌륭한 입문용 도구일 뿐입니다. 어떤 일이 일어나고 있는지 더 자세히 알아보려면 <strong>&quot;성능&quot;</strong> 패널이 필요합니다.</p>
<h4 id="성능-패널-살펴보기">성능 패널 살펴보기</h4>
<p>초기 로드시 성능 패널은 다음과 같이 표시됩니다.</p>
<p><img src="https://www.developerway.com/assets/initial-load-performance/performance-panel-first-load-20250103-004800.png" alt=""></p>
<p>여기에는 세 가지 <a href="https://web.dev/articles/vitals#core-web-vitals">핵심 웹 바이탈</a> 지표이 표시되며, 그 중 하나인 LCP는 느린 네트워크 및 CPU를 시뮬레이션하고 시간 경과에 따른 성능 세부 정보를 기록할 수 있는 기능을 제공합니다.</p>
<p>패널 맨 위에 있는 &quot;스크린샷&quot; 확인란을 찾아 체크한 다음 &quot;기록 및 재로드&quot; 버튼을 클릭하고 웹사이트가 재로드되면 기록을 중지하세요. 이것은 초기 로드 중에 페이지에서 일어나는 일에 대한 자세한 보고서가 될 것입니다.</p>
<p>이 보고서에는 몇 가지 섹션이 있습니다.</p>
<p>맨 위에는 일반적인 &quot;<strong>타임라인 개요</strong>(timeline overview)&quot; 섹션이 있습니다.</p>
<p><img src="https://www.developerway.com/assets/initial-load-performance/performance-panel-report-20250103-015743.png" alt=""></p>
<p>여기에서 웹사이트에서 어떤 일이 일어나고 있다는 것을 확인할 수 있지만 그 이상은 확인할 수 없습니다. 마우스를 가져가면 무슨 일이 일어나고 있는지 스크린샷이 표시되며 특정 범위를 선택하고 확대하여 자세히 살펴볼 수 있습니다.</p>
<p>그 아래에는 <strong>네트워크 섹션</strong>이 있습니다. 이 섹션을 확장하면 다운로드 중인 모든 외부 리소스와 정확한 시간을 타임라인에서 확인할 수 있습니다. 특정 리소스 위로 마우스를 가져가면 다운로드의 어느 단계에서 얼마나 많은 시간이 소요되었는지에 대한 자세한 정보를 볼 수 있습니다. 빨간색 모서리가 있는 리소스는 차단 리소스를 나타냅니다.</p>
<p><img src="https://www.developerway.com/assets/initial-load-performance/performance-panel-network-20250103-020031.png" alt=""></p>
<p>스터디 프로젝트를 진행 중이라면 정확히 같은 그림을 볼 수 있으며, 이 그림은 이전 섹션에 대해 설명한 내용과 일치합니다.</p>
<ul>
<li>처음에 파란색 블록이 있는데, 이는 웹사이트의 HTML을 가져오기 위한 요청입니다.</li>
<li>로딩이 완료된 후 (HTML을 구문 분석하기 위해) 잠시 멈춘 후 두 개의 리소스 추가 요청이 나옵니다.</li>
<li>그 중 하나(노란색)는 자바스크립트에 대한 요청으로 차단되지 않습니다.</li>
<li>또 다른 요청(보라색)은 CSS에 대한 요청이며, 이 요청은 차단 중입니다.</li>
</ul>
<p>지금 스터디 프로젝트 코드를 열고 <code>dist</code> 폴더를 들여다보면 소스 코드가 이 동작과 일치합니다.</p>
<ul>
<li><code>assets</code> 폴더 안에는 <code>index.html</code> 파일과 <code>.css</code> 및 <code>.js</code> 파일이 있습니다.</li>
<li><code>&lt;head&gt;</code> 섹션의 <code>index.html</code> 파일 안에는 CSS 파일을 가리키는 <code>&lt;link&gt;</code> 태그가 있습니다. 아시다시피 <code>&lt;head&gt;</code>에 있는 CSS 리소스는 렌더링 차단이므로 체크 아웃됩니다.</li>
<li>또한 <code>&lt;head&gt;</code> 안에는 <code>assets</code> 폴더 안에 있는 자바스크립트 파일을 가리키는 <code>&lt;script&gt;</code> 태그가 있습니다. 이 태그는 지연되거나 비동기화되지는 않지만 <code>type=&quot;module&quot;</code>을 갖습니다. 이는 <a href="https://web.dev/learn/performance/optimize-resource-loading#async_versus_defer">자동 연기</a>되므로 이 역시 체크 아웃됩니다. 패널의 자바스크립트 파일은 차단되지 않습니다.</li>
</ul>
<blockquote>
<p><strong>추가 연습</strong></p>
<p>작업 중인 프로젝트가 있는 경우 해당 프로젝트의 초기 로드 성능을 기록하고 네트워크 패널을 살펴보세요. 더 많은 리소스가 다운로드된 것을 볼 수 있을 것입니다.</p>
<ul>
<li>렌더링 차단 리소스가 몇 개나 있나요? 모두 필요한가요?</li>
<li>프로젝트의 &quot;진입&quot; 지점이 어디인지, 차단 리소스가 - <code>&lt;head /&gt;</code> 섹션에 어떻게 표시되는지 알고 있나요? 변형된 <code>npm build</code>로 프로젝트를 빌드하고 해당 리소스를 검색해 보세요. 힌트.<ul>
<li>순수 웹팩 기반 프로젝트인 경우 <code>webpack.config.js</code> 파일을 찾아보세요. HTML 항목에 대한 경로 포인트가 안에 있어야 합니다.</li>
<li>Vite를 사용하는 경우 스터디 프로젝트와 동일하게 <code>dist</code> 폴더를 살펴봅니다.</li>
<li>Next.js 앱 라우터를 사용하는 경우 <code>.next/server/app</code> 폴더를 살펴봅니다.</li>
</ul>
</li>
</ul>
</blockquote>
<p>네트워크 섹션 아래에서 <strong>프레임(Frames)</strong> 및 <strong>타이밍(Timing)</strong> 섹션을 찾을 수 있습니다.</p>
<p><img src="https://www.developerway.com/assets/initial-load-performance/performance-panel-frames-20250103-020206.png" alt="performance-panel-frames-20250103-020206.png"></p>
<p>두 섹션은 꽤나 유용합니다. 타이밍 섹션에서는 앞서 설명한 모든 지표(FP, FCP, LCP)과 아직 설명하지 않은 몇 가지 지표을 볼 수 있습니다. 지표 위로 마우스를 가져가면 정확한 소요 시간을 확인할 수 있습니다. 지표을 클릭하면 맨 아래에 있는 &quot;요약(summary)&quot; 탭이 업데이트되며, 여기에서 해당 지표에 대한 정보와 자세한 내용을 볼 수 있는 링크를 확인할 수 있습니다. 요즘 개발 도구는 사람들을 교육하는 데 중점을 두고 있습니다.</p>
<p>마지막으로 <strong>메인</strong> 섹션입니다. 타임라인이 기록되는 동안 메인 스레드에서 일어나는 일입니다.</p>
<p><img src="https://www.developerway.com/assets/initial-load-performance/performance-panel-main-20250103-020435.png" alt="performance-panel-main-20250103-020435.png"></p>
<p>여기서 &quot;HTML 구문 분석&quot; 또는 &quot;레이아웃&quot;과 같은 항목과 소요 시간을 확인할 수 있습니다. 노란색 항목은 자바스크립트와 관련된 것으로, 압축된 자바스크립트가 포함된 프로덕션 빌드를 사용하고 있기 때문에 다소 쓸모가 없습니다. 하지만 이 상태에서도 예를 들어 HTML 파싱 및 레이아웃 그리기와 비교하여 자바스크립트 실행에 걸리는 시간을 대략적으로 파악할 수 있습니다.</p>
<p>특히 <strong>네트워크</strong>와 <strong>메인</strong>을 모두 열고 확대하여 전체 화면을 볼 때 성능 분석에 유용합니다.</p>
<p><img src="https://www.developerway.com/assets/initial-load-performance/performance-panel-main-network-together-20250103-034155.png" alt="performance-panel-main-network-together-20250103-034155.png"></p>
<p>여기에서 엄청나게 빠른 서버와 빠르고 작은 번들이 있다는 것을 알 수 있습니다. 어떤 네트워크 작업도 병목 현상을 일으키지 않고 시간이 크게 걸리지 않으며, 그 사이에도 브라우저는 조용히 자체 작업을 수행합니다. 따라서 여기서 초기 로딩 속도를 높이려면 그래프에서 가장 긴 작업인 HTML 파싱이 왜 그렇게 느린지 살펴봐야 합니다.</p>
<p>또는 절대적인 수치를 보면 성능 측면에서 볼 때 여기서 아무것도 하지 않아야 합니다. 전체 초기 로드는 200ms 미만이 걸리며 구글의 권장 임계값보다 훨씬 낮습니다 🙂 하지만 저는 이 테스트를 매우 빠른 노트북과 매우 기본적인 서버를 사용하는 로컬 환경(실제 네트워크 비용이 들지 않음)에서 실행하고 있기 때문에 이런 일이 발생하고 있습니다.</p>
<p>실제 상황을 시뮬레이션할 시간입니다.</p>
<h2 id="다양한-네트워크-상태-살펴보기">다양한 네트워크 상태 살펴보기</h2>
<h3 id="매우-느린-서버">매우 느린 서버</h3>
<p>우선 서버를 좀 더 현실적으로 만들어 봅시다. 현재 첫 번째 &quot;파란색&quot; 단계는 약 50ms가 소요되며, 그 중 40ms는 대기 중입니다.</p>
<p><img src="https://www.developerway.com/assets/initial-load-performance/1.ideal-conditions-20250103-042928.png" alt="1.ideal-conditions-20250103-042928.png"></p>
<p>실제로 서버는 작업을 수행하고, 권한을 확인하고, 작업을 생성하고, 권한을 두 번 더 확인하는 등(레거시 코드가 많아서 세 번 확인하는 것이 손실되었기 때문에), 바쁘게 움직입니다.</p>
<p>스터디 프로젝트의 <code>backend/index.ts</code> 파일로 이동합니다(<a href="https://github.com/developerway/initial-load-performance">링크</a>). 그리고 주석 처리된 <code>// await sleep(500)</code>을 찾아 주석 처리를 해제합니다. 이렇게 하면 서버가 HTML을 반환하기까지 500ms의 지연이 발생하는데, 오래되고 복잡한 서버라고 생각하면 적절합니다.</p>
<p>프로젝트를 다시 빌드하고(<code>npm run build</code>), 다시 시작하고(<code>npm run start</code>) 성능 기록을 다시 실행합니다.</p>
<p>타임라인에서 초기 파란색 선을 제외하고는 아무것도 변경된 것이 없습니다. 다른 항목에 비해 엄청나게 길어졌습니다.</p>
<p><img src="https://www.developerway.com/assets/initial-load-performance/2.slow-server-20250103-045502.png" alt="2.slow-server-20250103-045502.png"></p>
<p>이 상황은 성능 최적화를 수행하기 전에 전체 상황을 살펴보고 병목 현상을 파악하는 것이 중요하다는 것을 강조합니다. LCP 값은 ~650밀리초이며, 이 중 ~560밀리초는 초기 HTML을 기다리는 데 소요됩니다. 이 중 리액트 부분은 약 50ms입니다. 어떻게든 절반으로 줄여 25ms로 줄이더라도 전체 그림에서 보면 4%에 불과합니다. 그리고 이를 절반으로 줄이려면 많은 노력이 필요합니다. 훨씬 더 효과적인 전략은 서버에 집중하여 서버가 왜 그렇게 느린지 파악하는 것입니다.</p>
<h3 id="다양한-대역폭-및-지연-시간-에뮬레이션하기">다양한 대역폭 및 지연 시간 에뮬레이션하기</h3>
<p>모든 사람이 1기가비트 연결의 세계에 살고 있는 것은 아닙니다. 예를 들어 호주에서는 초당 50메가비트는 초고속 인터넷 연결 중 하나이며, 한 달에 약 90호주달러의 비용이 듭니다. 물론 전 세계 많은 사람들이 사용하고 있는 3G는 아닙니다. 하지만 여전히 유럽에서 초당 1기가비트 또는 10유로짜리 인터넷 요금제에 대해 자랑하는 사람들의 이야기를 들을 때마다 눈물이 납니다.</p>
<p>어쨌든. 좋지 않은 호주의 인터넷을 모방하여 성능 지표이 어떻게 될지 살펴봅시다. 이를 위해 성능 탭(다시 로드 및 기록 근처의 버튼)에서 기존 기록을 지우세요. 네트워크 설정이 있는 패널이 나타납니다.</p>
<p><img src="https://www.developerway.com/assets/initial-load-performance/3.environment-settings-20250103-053147.png" alt="3.environment-settings-20250103-053147.png"></p>
<p>사용 중인 크롬 버전에 해당 설정이 없는 경우 네트워크 탭에서 동일한 설정을 사용할 수 있습니다.</p>
<p>&quot;네트워크&quot; 드롭다운에서 새 프로필을 추가할 때 다음 숫자를 입력합니다.</p>
<ul>
<li>Profile Name: &quot;평균 인터넷 대역폭(Average Internet Bandwidth)&quot;</li>
<li>Download: 50000 (50 Mbps)</li>
<li>Upload: 15000 (15 Mbps)</li>
<li>Latency: 40 (일반 인터넷 연결의 평균)</li>
</ul>
<p><img src="https://www.developerway.com/assets/initial-load-performance/4.network-throttling-20250103-053038.png" alt="4.network-throttling-20250103-053038.png"></p>
<p>이제 드롭다운에서 해당 프로필을 선택하고 성능 녹화를 다시 실행합니다.</p>
<p>무엇이 보이나요? 저에게는 다음과 같이 보입니다.</p>
<p><strong>LCP</strong> 값은 거의 변하지 않았으며 640ms에서 700ms로 약간 증가했습니다. 초기 파란색 &quot;서버&quot; 부분에는 변화가 없는데, 이는 최소한의 HTML만 전송하므로 다운로드하는 데 시간이 오래 걸리지 않기 때문입니다.</p>
<p>하지만 다운로드 가능한 리소스와 메인 스레드 간의 관계는 크게 바뀌었습니다.</p>
<p><img src="https://www.developerway.com/assets/initial-load-performance/5.average-internet-20250103-055702.png" alt="5.average-internet-20250103-055702.png"></p>
<p>이제 <strong>렌더링 차단 CSS</strong> 파일의 영향을 분명히 알 수 있습니다. <em>HTML 구문 분석</em> 작업은 이미 완료되었지만 브라우저는 차갑게 식어 CSS가 다운로드될 때까지 아무것도 그릴 수 없습니다. 브라우저가 HTML을 파싱하는 동안 리소스가 거의 즉시 다운로드된 이전 그림과 비교해 보세요.</p>
<p>그 후 기술적으로 브라우저는 무언가를 그릴 수 있었지만 실제로는 아무 것도 없고, HTML 파일에 빈 div만 전송하고 있습니다. 따라서 브라우저는 자바스크립트 파일을 다운로드하고 실행할 수 있을 때까지 계속 기다립니다.</p>
<p>이 약 60밀리초의 대기 시간이 바로 제가 보고 있는 <strong>LCP</strong>의 증가입니다.</p>
<p>속도를 더 낮추면 어떻게 되는지 확인해 봅시다. 다운로드 및 업로드에 대해 10mbps/1mbps의 새 네트워크 프로필을 만들고 대기 시간을 40으로 유지한 다음 이름을 &quot;Low Internet bandwidth(낮은 인터넷 대역폭)&quot;으로 지정합니다.</p>
<p><img src="https://www.developerway.com/assets/initial-load-performance/6.low-bandwidth-20250103-230711.png" alt="6.low-bandwidth-20250103-230711.png"></p>
<p>그리고 테스트를 다시 실행합니다.</p>
<p>이제 LCP 값이 거의 500ms로 증가했습니다. 자바스크립트 다운로드는 거의 300ms가 걸립니다. 그리고 상대적으로 HTML 구문 분석 작업과 자바스크립트 실행 작업의 중요성이 줄어들고 있습니다.</p>
<blockquote>
<p><strong>추가 연습</strong>
자체 프로젝트가 있는 경우 이 테스트를 실행해 보세요.</p>
<ul>
<li>모든 중요 경로 리소스를 다운로드하는 데 시간이 얼마나 걸리나요?</li>
<li>모든 자바스크립트 파일을 다운로드하는 데 시간이 얼마나 걸리나요?</li>
<li>HTML 구문 분석 작업 후 다운로드에 얼마나 많은 공백이 발생하나요?</li>
<li>리소스 다운로드에 비해 메인 스레드에서 HTML 구문 분석 및 자바스크립트 실행 작업은 얼마나 큰가요?</li>
<li>LCP 지표에 어떤 영향을 미치나요?</li>
</ul>
</blockquote>
<p>리소스 표시줄 내부에서 일어나는 일도 매우 흥미롭습니다. 노란색 자바스크립트 막대 위로 마우스를 가져가 보세요. 다음과 같은 내용이 표시됩니다.</p>
<p><img src="https://www.developerway.com/assets/initial-load-performance/7.javascript-bar-hover-20250103-235626.png" alt="What&quot;s happening inside the resources bar is also quite interesting. Hover over the yellow JavaScript bar. You should see something like this there."></p>
<p>여기서 가장 흥미로운 부분은 &quot;요청 전송 및 대기 중(Request sent and waiting)&quot;으로, 약 40ms가 소요됩니다. 나머지 네트워크 리소스 위로 마우스를 가져가면 모두 이 리소스가 표시됩니다. 이것이 바로 40으로 설정한 네트워크 딜레이인 <a href="https://aws.amazon.com/what-is/latency/">지연 시간(Latency)</a>입니다. 지연 시간 수치에는 여러 가지 요소가 영향을 미칠 수 있습니다. 네트워크 연결 유형도 그중 하나입니다. 예를 들어 평균 3G 연결의 대역폭은 10/1 Mbps이고 지연 시간은 100~300 밀리초입니다.</p>
<p>이를 에뮬레이트하려면 새 네트워크 프로필을 만들고 이름을 &quot;평균 3G(verage 3G)&quot;라고 지정한 다음 &quot;낮은 인터넷 대역폭(Low Internet bandwidth)&quot; 프로필에서 다운로드/업로드 수치를 복사하고 지연 시간을 300ms로 설정합니다.</p>
<p>프로파일링을 다시 실행합니다. 모든 네트워크 리소스의 &quot;요청 전송 및 대기 중&quot;이 약 300ms로 증가해야 합니다. 이렇게 하면 <strong>LCP</strong> 수치가 더 높아집니다. 저의 경우 <strong>1.2초</strong>가 증가했습니다.</p>
<p>이제 재미있는 부분을 살펴볼까요? 대역폭을 초고속으로 되돌리고 지연 시간을 낮게 유지하면 어떻게 될까요? 이 설정을 시도해 보겠습니다.</p>
<ul>
<li><strong>다운로드</strong>: 1000 Mbps</li>
<li><strong>업로드</strong>: 100 Mbps</li>
<li><strong>지연 시간</strong>: 300ms</li>
</ul>
<p>서버가 노르웨이 어딘가에 있지만 클라이언트가 부유한 호주인인 경우 <a href="https://www.developerway.com/posts/initial-load-performance#:~:text=This%20can-,easily%20happen,-if%20your%20servers">쉽게 발생</a>할 수 있습니다.</p>
<p>이것이 결과입니다.</p>
<p><img src="https://www.developerway.com/assets/initial-load-performance/8.high-speed-low-latency-20250104-014439.png" alt="8.high-speed-low-latency-20250104-014439.png"></p>
<p><strong>LCP</strong> 수치는 약 <strong>960ms</strong>입니다. 이전에 시도했던 가장 느린 인터넷 속도보다 더 느립니다! 이 시나리오에서는 번들 크기는 크게 중요하지 않으며 CSS 크기는 전혀 중요하지 않습니다. 둘 다 절반으로 줄여도 LCP 지표는 거의 움직이지 않습니다. 높은 지연 시간이 모든 것을 압도합니다.</p>
<p>이제 아직 구현하지 않았다면 모든 사람이 가장 먼저 구현해야 할 성능 개선 사항을 소개합니다. 바로 &quot;정적 리소스가 <strong>항상</strong> CDN을 통해 제공되도록 하는 것&quot;입니다.</p>
<h3 id="cdn의-중요성">CDN의 중요성</h3>
<p>CDN은 기본적으로 코드 분할이나 서버 컴포넌트와 같은 더 멋진 것들을 생각하기 전에 프론트엔드 성능과 관련된 모든 것에서 0단계입니다.</p>
<p>모든 <a href="https://web.dev/articles/content-delivery-networks">CDN</a>(콘텐츠 전송 네트워크)의 주요 목적은 지연 시간을 줄이고 최종 사용자에게 콘텐츠를 최대한 빠르게 전송하는 것입니다. 이를 위해 여러 가지 전략을 구현합니다. 이 글에서 가장 중요한 두 가지 전략은 &quot;분산 서버&quot;와 &quot;캐싱&quot;입니다.</p>
<p>CDN 제공업체는 여러 지리적 위치에 여러 대의 서버를 보유합니다. 이러한 서버는 정적 리소스의 복사본을 저장하고 브라우저가 요청할 때 사용자에게 전송할 수 있습니다. CDN은 기본적으로 원본 서버를 외부의 영향으로부터 보호하고 외부와의 상호 작용을 최소화하는 소프트 레이어입니다. 마치 내성적인 사람을 위한 인공지능 비서와 같아서 실제 사람이 개입할 필요 없이 일반적인 대화를 처리할 수 있습니다.</p>
<p>노르웨이에 서버가 있고 호주에 클라이언트가 있는 위의 예시에서는 이런 그림이 나왔습니다.</p>
<p><img src="https://www.developerway.com/assets/initial-load-performance/1.norway-australia-20250104-030110.png" alt="1.norway-australia-20250104-030110.png"></p>
<p>CDN이 중간에 있으면 그림이 달라집니다. CDN은 사용자와 더 가까운 곳, 예를 들어 호주 어딘가에 서버를 두게 됩니다. 어느 시점에서 CDN은 원본 서버로부터 정적 리소스의 복사본을 받게 됩니다. 그렇게 되면 호주 또는 호주와 가까운 곳에 있는 모든 사용자는 노르웨이에 있는 서버의 원본이 아닌 해당 복사본을 받게 됩니다.</p>
<p>이를 통해 두 가지 중요한 사항을 달성할 수 있습니다. 첫째, 사용자가 더 이상 원본 서버에 직접 액세스할 필요가 없으므로 원본 서버의 부하가 줄어듭니다. 둘째, 사용자는 더 이상 자바스크립트 파일을 다운로드하기 위해 바다를 건너갈 필요가 없으므로 리소스를 훨씬 더 빠르게 얻을 수 있습니다.</p>
<p><img src="https://www.developerway.com/assets/initial-load-performance/2.norway-cdn-australia-20250104-031323.png" alt="2.norway-cdn-australia-20250104-031323.png"></p>
<p>그리고 위 시뮬레이션의 LCP 값은 <strong>960ms에서 640ms</strong>로 다시 떨어집니다 🎉.</p>
<h2 id="재방문-성능">재방문 성능</h2>
<p>지금까지는 웹사이트에 한 번도 방문한 적이 없는 사람들의 성과인 첫 방문 성과에 대해서만 이야기했습니다. 하지만 웹사이트가 너무 좋아서 첫 방문자 대부분이 단골이 되었으면 좋겠습니다. 또는 적어도 첫 로딩 후 이탈하지 않고 몇 페이지를 탐색한 후 무언가를 구매하면 좋겠죠. 이 경우 일반적으로 브라우저는 CSS 및 JS와 같은 정적 리소스를 캐싱하여 항상 다운로드하지 않고 로컬에 사본을 저장합니다.</p>
<p>이 시나리오에서 성능 그래프와 수치가 어떻게 변하는지 살펴봅시다.</p>
<p>스터디 프로젝트를 다시 엽니다. 개발 도구에서 네트워크를 앞서 생성한 &quot;평균 3G&quot;로 설정하고 지연 시간이 길고 대역폭이 낮은 상태로 설정하여 차이를 바로 확인할 수 있도록 합니다. 그리고 &quot;네트워크 캐시 비활성화(disable network cache)&quot; 확인란이 선택 해제되어 있는지 확인합니다.</p>
<p><img src="https://www.developerway.com/assets/initial-load-performance/1.initial-set-up-20250112-031042.png" alt="1.initial-set-up-20250112-031042.png"></p>
<p>먼저 브라우저를 새로고침하여 첫 번째 방문자 상황을 제거합니다. 그런 다음 새로고침하고 성능을 측정합니다.</p>
<p>스터디 프로젝트를 사용하는 경우 최종 결과는 다음과 같이 보일 것이므로 약간 놀랄 것입니다.</p>
<p><img src="https://www.developerway.com/assets/initial-load-performance/2.repeated-user-load-performance-20250112-032213.png" alt="2.repeated-user-load-performance-20250112-032213.png"></p>
<p>네트워크 탭에서 CSS와 자바스크립트 파일은 여전히 매우 눈에 띄며, &quot;평균 3G&quot; 프로필의 지연 시간 설정인 &quot;요청 전송 및 대기 중&quot;에서 두 파일 모두에 대해 약 300ms가 표시됩니다. 결과적으로 LCP가 낮지 않고 브라우저가 차단 CSS를 기다릴 때 300ms의 간격이 생깁니다.</p>
<p>어떻게 된 걸까요? 브라우저가 캐싱을 해야 하는 것 아닌가요?</p>
<h3 id="캐시-제어-헤더로-브라우저-캐시-제어하기">캐시 제어 헤더로 브라우저 캐시 제어하기</h3>
<p>이제 무슨 일이 일어나고 있는지 이해하기 위해 네트워크 패널을 사용해야 합니다. 패널을 열고 거기서 CSS 파일을 찾습니다. 다음과 같이 보일 것입니다:</p>
<p><img src="https://www.developerway.com/assets/initial-load-performance/3.css-file-304-reponse-20250113-003114.png" alt="3.css-file-304-reponse-20250113-003114.png"></p>
<p>여기서 가장 흥미로운 것은 &quot;상태(Status)&quot; 열과 &quot;크기(Size)&quot;입니다. &quot;크기&quot;는 전체 CSS 파일의 크기가 아닙니다. 너무 작습니다. 그리고 &quot;상태&quot;에서는 일반적인 200의 &quot;모두 괜찮음&quot; 상태가 아니라 304의 다른 상태입니다.</p>
<p>여기서 두 가지 질문이 있습니다. 왜 200이 아닌 304인지, 그리고 왜 요청이 아예 전송되지 않았을까요? 캐싱이 작동하지 않은 이유는 무엇인가요?</p>
<p><strong>우선</strong> <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/304">304 응답</a>입니다. 이 응답은 잘 구성된 서버가 다양한 규칙에 따라 응답이 달라지는 <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Conditional_requests">조건부 요청</a>에 대해 보내는 응답입니다. 이와 같은 요청은 브라우저 캐시를 제어하는 데 자주 사용됩니다.</p>
<p>예를 들어 서버가 CSS 파일에 대한 요청을 받으면 파일이 마지막으로 수정된 날짜를 확인할 수 있습니다. 이 날짜가 브라우저 측의 캐시된 파일과 같으면 빈 본문이 있는 304를 반환합니다(그래서 223 B에 불과합니다). 이는 브라우저에 이미 가지고 있는 파일을 다시 사용해도 안전하다는 것을 나타냅니다. 대역폭을 낭비하고 다시 다운로드할 필요가 없습니다.</p>
<p>성능 그림에서 &quot;요청 전송 및 대기 중&quot;이라는 큰 숫자가 표시되는 이유는 브라우저가 서버에 CSS 파일이 최신 상태인지 확인하도록 요청하기 때문입니다. 서버가 &quot;304 수정되지 않음&quot;으로 응답하고 브라우저가 이전에 다운로드한 파일을 다시 사용했기 때문에 &quot;콘텐츠 다운로드&quot;가 0.33ms인 것입니다.</p>
<blockquote>
<p><strong>추가 연습</strong></p>
<ul>
<li>스터디 프로젝트에서 <code>dist/assets</code> 폴더로 이동하여 CSS 파일의 이름을 바꿉니다.</li>
<li><code>dist/index.html</code> 파일로 이동하여 이름이 변경된 CSS 파일의 경로를 업데이트합니다.</li>
<li>네트워크 탭을 열어 이미 열려 있는 페이지를 새로고침하면 새 이름, 200 상태, 적절한 크기(다시 다운로드된)의 CSS 파일이 표시됩니다. 이를 &quot;캐시 버스팅(cache-busting)&quot;이라고 하는데, 브라우저가 캐시했을 수 있는 리소스를 강제로 다시 다운로드하는 방법입니다.</li>
<li>페이지를 다시 새로고침하면 304 상태로 돌아가 캐시된 파일을 다시 사용할 수 있습니다.</li>
</ul>
</blockquote>
<p>이제 <strong>두 번째 질문</strong>인 왜 이 요청이 전송되었는지에 대해 알아봅시다.</p>
<p>이 동작은 서버가 응답에 설정하는 <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control">Cache-Control</a> 헤더에 의해 제어됩니다. 요청/응답의 세부 정보를 보려면 네트워크 패널에서 CSS 파일을 클릭합니다. &quot;응답 헤더&quot; 블록의 &quot;헤더&quot; 탭에서 &quot;Cache-Control&quot; 값을 찾습니다.</p>
<p><img src="https://www.developerway.com/assets/initial-load-performance/4.cache-control-in-network-20250113-041033.png" alt="4.cache-control-in-network-20250113-041033.png"></p>
<p>이 헤더 안에는 쉼표로 구분된 다양한 조합의 지시문을 여러 개 넣을 수 있습니다. 이 경우에는 두 가지가 있습니다.</p>
<ul>
<li>숫자가 포함된 <strong>max-age</strong> - 이 특정 응답이 저장될 기간(초 단위)을 제어합니다.</li>
<li><strong>must-revalidate</strong> - 응답이 오래된 경우 브라우저가 항상 서버에 새 버전을 요청하도록 지시합니다. 응답이 최대 유효 기간 값보다 오래 캐시에 있으면 응답이 부실 상태가 됩니다.</li>
</ul>
<p>따라서 기본적으로 이 헤더가 브라우저에 알려주는 것은 다음과 같습니다.</p>
<ul>
<li>이 응답을 캐시에 저장해도 괜찮지만 시간이 지난 후 다시 한 번 확인해보세요.</li>
<li>참고로 이 캐시를 보관할 수 있는 시간은 정확히 <strong>0</strong>초입니다. 행운을 빕니다.</li>
</ul>
<p>결과적으로 브라우저는 항상 서버를 확인하며 캐시를 바로 사용하지 않습니다.</p>
<p><code>max-age</code>을 0에서 31536000(1년, 허용되는 최대 초) 사이로 변경하기만 하면 쉽게 변경할 수 있습니다. 이렇게 하려면 스터디 프로젝트에서 <code>backend/index.ts</code> 파일로 이동하여 <code>max-age=0</code>이 설정된 위치를 찾아 31536000(1년)으로 변경합니다. 페이지를 몇 번 새로고침하면 네트워크 탭의 CSS 파일에 이 내용이 표시될 것입니다.</p>
<p><img src="https://www.developerway.com/assets/initial-load-performance/5.memory-cache-in-network-panel-20250113-044543.png" alt="5.memory-cache-in-network-panel-20250113-044543.png"></p>
<p>이제 <code>Status</code> 열이 회색으로 표시되고 <code>Size</code>에 &#39;(memory cache)&#39;가 표시되는 것을 확인할 수 있습니다. 이제 CSS 파일이 브라우저의 캐시에서 제공되며 앞으로도 계속 그렇게 될 것입니다. 페이지를 몇 번 새로고침하여 변경되지 않는지 확인합니다.</p>
<p>이제 캐시 헤더를 엉망으로 만든 본론으로 들어가서 페이지의 성능을 다시 측정해 봅시다. &quot;평균 3G&quot; 프로필 설정을 설정하고 ‘캐시 비활성화’ 설정은 선택하지 않은 상태로 유지하는 것을 잊지 마세요.</p>
<p>결과는 다음과 같아야 합니다.</p>
<p><img src="https://www.developerway.com/assets/initial-load-performance/6.cache-performance-20250113-045302.png" alt="6.cache-performance-20250113-045302.png"></p>
<p>&quot;요청 전송 및 대기&quot; 부분은 높은 지연 시간에도 불구하고 거의 0에 가까워졌고, ‘HTML 파싱’과 자바스크립트 평가 사이의 간격이 거의 사라졌으며, LCP 값은 650밀리초대로 돌아갔습니다.</p>
<blockquote>
<p><strong>추가 연습</strong></p>
<ul>
<li><code>max-age</code> 값을 지금 10(10초)으로 변경합니다.</li>
<li>&#39;캐시 비활성화&#39; 확인란을 체크한 상태에서 페이지를 새로고침하여 캐시를 삭제합니다.</li>
<li>확인란을 선택 해제하고 페이지를 다시 새로 고치면 이번에는 메모리 캐시에서 제공되어야 합니다.</li>
<li>10초간 기다렸다가 페이지를 다시 새로고침합니다. <code>max-age</code>이 10초에 불과하므로 브라우저는 리소스를 다시 확인하고 서버는 304를 다시 반환합니다.</li>
<li>페이지를 즉시 새로고침 - 메모리에서 다시 제공되어야 합니다.</li>
</ul>
</blockquote>
<h3 id="캐시-제어-및-최신-번들러">캐시 제어 및 최신 번들러</h3>
<p>캐시가 성능의 만병통치약이며 가능한 한 모든 것을 공격적으로 캐시해야 한다는 것을 의미할까요? 절대 그렇지 않습니다! 다른 경우는 논외로 하더라도, &quot;기술에 익숙하지 않은 고객&quot;과 &quot;브라우저 캐시를 지우는 방법을 전화로 설명해야 하는 고객&quot;에게 문제가 생긴다면 가장 노련한 개발자에게 공황 발작을 일으킬 수 있습니다.</p>
<p>캐시를 최적화하는 방법은 수백만 가지가 있으며, 캐시의 수명에 영향을 줄 수도 있고 그렇지 않을 수도 있는 다른 헤더와 함께 Cache-Control 헤더의 지시어를 조합하는 방법은 서버의 구현에 따라 달라질 수도 있고 그렇지 않을 수도 있습니다. 아마 이 주제만 해도 책 몇 권 분량의 정보를 쓸 수 있을 것입니다. 캐시의 달인이 되고 싶다면 <a href="https://web.dev/">https://web.dev/</a> 및 <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control">MDN 리소스</a>의 기사부터 시작하여 발자취를 따라가 보세요.</p>
<p>안타깝게도 &quot;이것이 모든 것을 위한 최고의 캐시 전략 5가지&quot;라고 말할 수 있는 사람은 아무도 없습니다. 기껏해야 이렇게 대답할 수 있을 뿐입니다. &quot;이런 사용 사례가 있다면 이것, 이것, 이것과 함께 이런 캐시 설정 조합을 사용하는 것이 좋지만, 이런 문제점을 염두에 두어야 합니다.&quot; 정도입니다. 모든 것은 리소스, 빌드 시스템, 리소스 변경 빈도, 리소스를 캐시하는 것이 얼마나 안전한지, 잘못했을 때 어떤 결과가 초래되는지를 파악하는 데 달려 있습니다.</p>
<p>하지만 여기에는 한 가지 예외가 있습니다. 명확한 &#39;모범 사례&#39;가 있다는 점에서 예외입니다. 최신 툴로 구축된 웹사이트용 자바스크립트 및 CSS 파일입니다. Vite, Rollup, Webpack 등과 같은 최신 번들러는 &quot;변경 불가능한&quot; JS 및 CSS 파일을 생성할 수 있습니다. 물론 진정한 의미의 &quot;불변&quot;은 아닙니다. 하지만 이러한 도구는 파일 콘텐츠에 따라 해시 문자열로 파일 이름을 생성합니다. 파일 콘텐츠가 변경되면 해시가 변경되고 파일 이름도 변경됩니다. 결과적으로 웹사이트가 배포되면 브라우저는 캐시 설정에 관계없이 완전히 새로운 파일 사본을 다시 가져옵니다. 이전 연습에서 CSS 파일의 이름을 수동으로 바꿨을 때와 마찬가지로 캐시가 &quot;버스트&quot;됩니다.</p>
<p>예를 들어 스터디 프로젝트의 <code>dist/assets</code> 폴더를 살펴보세요. js 파일과 CSS 파일 모두 <code>index-[hash]</code> 파일 이름이 있습니다. 이 이름을 기억하고 <code>npm run build</code>를 몇 번 실행하세요. 해당 파일의 내용은 변경되지 않았으므로 이름은 정확히 동일하게 유지됩니다.</p>
<p>이제 <code>src/App.tsx</code> 파일로 이동하여 <code>console.log(&quot;bla&quot;)</code> 같은 것을 추가합니다. <code>npm run build</code>를 다시 실행하고 생성된 파일을 확인합니다. CSS 파일 이름은 이전과 동일하게 유지되지만 JS 파일 이름이 변경된 것을 볼 수 있습니다. 이 웹사이트가 배포되면 다음에 반복 사용자가 방문할 때 브라우저는 이전에 캐시에 나타나지 않았던 완전히 다른 JS 파일을 요청합니다. 캐시가 손상된 것입니다.</p>
<blockquote>
<p><strong>추가 연습</strong>
프로젝트의 <code>dist</code> 폴더에 해당하는 폴더를 찾아 빌드 명령을 실행합니다.</p>
<ul>
<li>파일 이름이 어떻게 생겼나요? 해시를 사용하나요, 아니면 일반 <code>index.js</code>, <code>index.css</code> 등과 비슷하나요?</li>
<li>빌드 명령을 다시 실행하면 파일 이름이 변경되나요?</li>
<li>코드의 어딘가에서 간단한 변경을 하면 파일 이름이 몇 개나 바뀌나요?</li>
</ul>
</blockquote>
<p>빌드 시스템이 이런 식으로 구성되어 있다면 운이 좋다고 볼 수 있습니다. 생성된 에셋의 <code>max-age</code> 헤더를 설정하도록 서버를 안전하게 구성할 수 있습니다. 모든 이미지의 버전을 비슷하게 설정하는 경우 더 좋은 방법은 목록에 이미지를 포함시키는 것입니다.</p>
<p>웹사이트와 사용자 및 사용자의 행동에 따라 초기 로드 시 무료로 꽤 좋은 성능 향상을 얻을 수 있습니다.</p>
<h3 id="간단한-사용-사례를-위해-이-모든-것을-꼭-알아야-할까요">간단한 사용 사례를 위해 이 모든 것을 꼭 알아야 할까요?</h3>
<p>이쯤 되면 &quot;너무 과해요. 저는 주말에 Next.js로 간단한 웹사이트를 만들어서 2분 만에 Vercel/Netlify/HottestNewProvider에 배포했습니다. 이런 최신 도구가 이 모든 것을 처리해 주지 않을까요?&quot;라고 생각하실 수도 있습니다. 충분히 그럴 만합니다. 저도 그렇게 생각했습니다. 하지만 실제로 확인해 보니 깜짝 놀랐습니다.</p>
<p>내 프로젝트 중 두 개가 <code>max-age=0</code>이고 CSS 및 JS 파일에 대해 <code>must-revalidate</code>를 수행해야 했습니다. 알고 보니 CDN이 제공하는(🤷🏻‍♀️)의 기본값이었습니다. 물론 이 기본값에는 이유가 있습니다. 다행히도 이 기본값은 쉽게 재정의할 수 있으므로 큰 문제는 없습니다. 하지만 여전히, 요즘은 누구도, 무엇도 믿을 수 없습니다 😅.</p>
<p>호스팅/CDN 제공 업체는 어떻습니까? 캐시 헤더 구성에 대해 얼마나 확신하십니까?</p>
<p>재미있는 조사였기를 바라며, 새로운 것을 배우고 프로젝트에서 한두 가지 문제를 해결했을 수도 있습니다. 여러분이 스터디 프로젝트를 진행하는 동안 저는 나머지 지표에 대한 작업을 진행할 것입니다(그러길 바랍니다). 곧 다시 뵙겠습니다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[(번역) 리액트 개발자를 위한 SSR 심층 분석]]></title>
            <link>https://velog.io/@tap_kim/%EB%B2%88%EC%97%AD-%EB%A6%AC%EC%95%A1%ED%8A%B8-%EA%B0%9C%EB%B0%9C%EC%9E%90%EB%A5%BC-%EC%9C%84%ED%95%9C-SSR-%EC%8B%AC%EC%B8%B5-%EB%B6%84%EC%84%9D</link>
            <guid>https://velog.io/@tap_kim/%EB%B2%88%EC%97%AD-%EB%A6%AC%EC%95%A1%ED%8A%B8-%EA%B0%9C%EB%B0%9C%EC%9E%90%EB%A5%BC-%EC%9C%84%ED%95%9C-SSR-%EC%8B%AC%EC%B8%B5-%EB%B6%84%EC%84%9D</guid>
            <pubDate>Wed, 09 Apr 2025 15:39:25 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>원문: <a href="https://www.developerway.com/posts/ssr-deep-dive-for-react-developers">https://www.developerway.com/posts/ssr-deep-dive-for-react-developers</a></p>
</blockquote>
<blockquote>
<p>지금부터 리액트의 서버 사이드 렌더링(SSR), 사전 렌더링(pre-rendering), 하이드레이션(hydration) 및 정적 사이트 생성(SSG)이 작동하는 방식, 비용, 성능에 미치는 영향, 이점 및 장단점에 대해 단계별로 살펴보시죠.</p>
</blockquote>
<p>이전 글에서는 <a href="https://velog.io/@tap_kim/initial-load-performance">리액트 개발자를 위한 초기 로드 성능 심층 분석하기</a>와 인기 있는 이유, <a href="https://tinyurl.com/3an4jv5k">성능 플레임 차트</a>를 기록하고 읽고 해석하는 방법에 대해 알아보았습니다. 또한 클라이언트 사이드 렌더링의 가장 중요한 두 가지 단점, 즉 초기 로딩 성능에 부정적인 영향을 미치고 자바스크립트가 없는 환경에서는 작동하지 않는다는 사실도 알게 되었습니다.</p>
<p>이 글에서는 SSR이라는 또 다른 렌더링 패턴과 사전 렌더링 및 SSG와 같은 변형을 소개함으로써 이러한 단점을 해결하는 데 초점을 맞출 것입니다. 아름다운 웹사이트를 위한 가장 간단한 사전 렌더링을 구현하고 그 비용과 해결 방법을 살펴본 다음, 적절한 SSR을 구현하고 성능 영향을 측정하고 SSR의 비용에 관해 이야기하고 웹사이트를 위한 SSG의 빠른 구현으로 마무리하는 <a href="https://github.com/developerway/ssr-deep-dive">스터디 프로젝트가</a> 다시 시작됩니다.</p>
<p>이번 내용은 흥미진진할 것입니다!</p>
<h2 id="자바스크립트가-없는-환경이-중요한-이유">자바스크립트가 없는 환경이 중요한 이유</h2>
<p>먼저 자바스크립트가 없는 환경부터 시작하겠습니다. 그런데 이 말은 굉장히 의아할 수 있습니다. 요즘 누가 브라우저에서 자바스크립트를 비활성화하나요? 모든 곳에서 기본적으로 활성화되고, 자바스크립트 없이 작동하는 것은 거의 없으며, 대부분의 사람은 자바스크립트를 비활성화할 줄도 모릅니다. 그렇죠?</p>
<p>여기서 답은 “사람”이라는 단어에 있습니다. 더 정확히 말하자면, 웹사이트에 액세스할 수 있는 사람이 실제 사람만이 아니라는 사실에 있습니다. 이 분야의 두 가지 주요 플레이어가 있습니다.</p>
<ul>
<li>검색 엔진 로봇(크롤러), 특히 구글 크롤러.</li>
<li>다양한 소셜 미디어 및 메신저의 &#39;미리보기&#39; 기능.</li>
</ul>
<p>모두 비슷한 방식으로 작동합니다. <strong>먼저</strong> 어떻게든 웹사이트 페이지의 URL을 가져옵니다. 이는 일반적으로 사용자가 소셜 미디어 친구들과 페이지 링크를 공유하려고 할 때 발생합니다. 또는 검색 봇이 온라인에 공개된 수백만 개의 페이지를 무의식적으로 크롤링할 때 발생합니다. 그래서 크롤러라고 불리는 것입니다.</p>
<p><strong>둘째</strong>, 봇은 처음에 브라우저가 하는 것처럼 서버로 요청을 전송하고 HTML을 수신합니다.</p>
<p><strong>셋째</strong>, 봇은 해당 HTML에서 원하는 정보를 추출하여 처리합니다. 검색 엔진은 텍스트, 링크, 메타 태그 등과 같은 정보를 추출합니다. 이를 바탕으로 검색 색인을 생성하고 페이지가 “구글 검색 가능” 상태가 됩니다. 소셜 미디어 미리보기는 메타태그를 가져와 큰 사진, 제목, 때로는 짧은 설명과 함께 우리가 모두 보았던 멋진 미리보기 기능을 만듭니다.</p>
<p>마지막으로 <strong>네 번째</strong>... 사실, 네 번째는 없을 때도 있습니다. 그게 다입니다. 자바스크립트 없이 순수한 HTML만 있습니다. 자바스크립트로 페이지를 제대로 렌더링 하려면 로봇이 실제 브라우저를 구동하고 자바스크립트를 로드한 다음 페이지 생성이 완료될 때까지 기다려야 하기 때문입니다. 이는 리소스와 시간 측면에서 상당히 큰 비용이 소요됩니다. 따라서 모든 로봇이 이를 수행할 수 있는 것은 아닙니다.</p>
<p><img src="https://www.developerway.com/assets/ssr-deep-dive-for-react-developers/1.social-media-20250206-000442.png" alt=""></p>
<p><a href="https://github.com/developerway/ssr-deep-dive">스터디 프로젝트</a>에서 실제로 작동하는 모습을 확인할 수 있습니다. 다운로드하고 의존성을 설치합니다.</p>
<pre><code class="language-bash">npm install</code></pre>
<p>빌드하고 시작하세요.</p>
<pre><code class="language-bash">npm run build
npm run start</code></pre>
<p>홈(Home)과 설정(Setting) 사이를 탐색합니다. 탐색을 통해 페이지의 제목이 변경되는 것을 볼 수 있습니다. 홈(Home) 페이지는 &quot;Study project: Home&quot;으로, 설정(Setting) 페이지는 &quot;Study project: Settings&quot;로 변경됩니다.</p>
<p>이 제목은 다음과 같은 간단한 코드로 리액트에 삽입됩니다.</p>
<pre><code class="language-ts">useEffect(() =&gt; {
  updateTitle(&quot;Study project: Home&quot;);
}, []);</code></pre>
<p>내부 코드는 이게 전부입니다.</p>
<pre><code class="language-ts">export const updateTitle = (text: string) =&gt; {
  document.title = text;
};</code></pre>
<p>그러나 처음 로드할 때 잠시 &quot;깜박이는&quot; 것을 볼 수 있는데, 이는 기본 제목이 &quot;Vite + React + TS&quot;이기 때문입니다. 이는 <code>index.html</code>에 있는 제목이고 결과적으로 서버에서 받는 제목입니다.</p>
<p>이제 <a href="https://ngrok.com/">ngrok</a>(또는 유사한 도구가 있는 경우)을 사용하여 웹사이트를 외부에 노출합니다.</p>
<pre><code class="language-bash">ngrok http 3000</code></pre>
<p>생성된 URL을 원하는 소셜 미디어에 게시하려고 시도합니다. 생성된 미리보기에서 기존의 “Vite + 리액트 + TS” 제목을 볼 수 있습니다. 이때 자바스크립트는 로드되지 않았습니다.</p>
<p>하지만 일부 봇에선 그렇지 않은 경우가 있습니다. 대부분의 성능이 좋은 검색 엔진은 자바스크립트 파일의 실행을 기다립니다. 예를 들어 구글은 “순수한” HTML의 구문을 분석하고 페이지를 “렌더링” 대기열에 넣는 <a href="https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics">2단계 프로세스</a>를 통해 브라우저를 실행하고 웹 사이트를 로드한 다음 자바스크립트 렌더링을 기다립니다. 그리고 가능하면 모든 것들을 다시 추출하는 과정을 수행합니다.</p>
<p><img src="https://www.developerway.com/assets/ssr-deep-dive-for-react-developers/1.a-google-crawler.png" alt=""></p>
<p>그러나 이 프로세스는 자바스크립트에 크게 의존하는 웹사이트의 색인 작업이 <a href="https://developers.google.com/search/docs/crawling-indexing/large-site-managing-crawl-budget">“더 느리고 자원이 많이 소요될 수 있다”</a>는 것을 의미합니다.</p>
<p>따라서 웹사이트의 경우 아래 두 가지가 중요합니다.</p>
<ul>
<li>가능한 한 많은 검색 엔진에서 최대한 빨리 검색될 수 있도록 하는 것이 중요합니다.</li>
<li>소셜 미디어 플랫폼에서 공유할 수 있어야 하고 그 과정에서 보기 좋게 보이는 것이 중요합니다.</li>
</ul>
<p>그런 다음 서버가 첫 번째 요청에 중요한 정보가 모두 포함된 “적절한” HTML을 반환하는 것이 매우 중요합니다. 이러한 웹사이트의 대표적인 예는 다음과 같습니다.</p>
<ul>
<li>읽기 우선 웹사이트, 즉 다양한 형태의 블로그, 문서, 지식 기반, 포럼, Q&amp;A 웹사이트, 뉴스 매체 등.</li>
<li>다양한 형태의 전자상거래 웹사이트.</li>
<li>랜딩 페이지.</li>
<li>이외 월드 와이드 웹에서 검색할 수 있는 거의 모든 것들.</li>
</ul>
<p>즉, 첫 번째 HTML 응답으로 빈 div를 사용하여 “고전적인” 클라이언트 사이드 렌더링을 하는 SPA는 검색 엔진에 좋지 않습니다.</p>
<p>하지만 그렇다고 해서 분노에 차서 리액트를 버려야 한다는 뜻은 아닙니다. 먼저 시도해 볼 수 있는 몇 가지 해결책이 있습니다.</p>
<h2 id="서버-사전-렌더링">서버 사전 렌더링</h2>
<p>이 경우에는 서버를 도입해야 합니다. 현재 스터디 프로젝트에서는 다음과 같이 보입니다.</p>
<pre><code class="language-ts">app.get(&quot;/*&quot;, async (c) =&gt; {
  const html = fs.readFileSync(path.join(dist, &quot;index.html&quot;)).toString();

  return c.html(html);
});</code></pre>
<p>서버는 요청을 받으면 빌드 단계에서 미리 생성한 <code>index.html</code> 파일을 읽고 문자열로 변환한 후 요청자에게 다시 전송합니다. 이는 기본적으로 SPA를 지원하는 모든 호스팅 플랫폼이 수행하는 작업입니다. 다만 플랫폼에서 직접 제어하거나 수정할 수 있는 기능은 아닙니다.</p>
<p>하지만 “자바스크립트 없음” 문제를 해결하려면 지금 당장 서버를 수정해야 합니다. 다행히도 이는 큰 문제는 아닙니다. 우리가 작업한 내용이 문자열이라는 사실인 것만으로도 많은 것들이 단순화됩니다. 문자열을 다시 보내기 전에 이 문자열을 수정하는 것을 막을 수 있는 것은 아무것도 없기 때문입니다. 이제 기존 제목을 찾아서 “Study project”로 바꾸어 보겠습니다. 아래 예문을 확인해 보시죠.</p>
<pre><code class="language-ts">app.get(&quot;/*&quot;, async (c) =&gt; {
  const html = fs.readFileSync(path.join(dist, &quot;index.html&quot;)).toString();

  const modifiedHTML = html.replace(
    &quot;&lt;title&gt;Vite + React + TS&lt;/title&gt;&quot;,
    `&lt;title&gt;Study project&lt;/title&gt;`
  );

  return c.html(html);
});</code></pre>
<p>이 방법이 조금 더 나은 방법이지만, 실제로는 모든 페이지마다 제목이 바뀌어야 하므로 이렇게 정적으로 유지하는 것은 의미가 없습니다. 다행히도 각 서버는 요청이 어디에서 오는지 항상 정확히 알고 있습니다. 제가 사용하는 프레임워크(<a href="https://hono.dev/">Hono</a>)의 경우 <code>c.req.path</code>를 요청하여 경로 정보를 추출하기만 하면 됩니다.</p>
<p>그런 다음 해당 경로를 기반으로 다양한 제목을 생성할 수 있습니다.</p>
<pre><code class="language-ts">app.get(&quot;/*&quot;, async (c) =&gt; {
  const html = fs.readFileSync(path.join(dist, &quot;index.html&quot;)).toString();

  const title = getTitleFromPath(pathname);

  const modifiedHTML = html.replace(
    &quot;&lt;title&gt;Vite + React + TS&lt;/title&gt;&quot;,
    `&lt;title&gt;${title}&lt;/title&gt;`
  );

  return c.html(html);
});</code></pre>
<p><code>getTitleFromPath</code>에서 다음과 같은 작업을 수행할 수 있습니다.</p>
<pre><code class="language-ts">const getTitleFromPath = (pathname: string) =&gt; {
  let title = &quot;Study project&quot;;

  if (pathname.startsWith(&quot;/settings&quot;)) {
    title = &quot;Study project: Settings&quot;;
  } else if (pathname === &quot;/login&quot;) {
    title = &quot;Study project: Login&quot;;
  }

  return title;
};</code></pre>
<p>실제 환경에서는 실제 페이지와 함께 배치하거나 페이지 내 코드에서 추출해야 할 수도 있습니다. 그렇지 않으면 거의 즉시 동기화되지 않습니다. 하지만 스터디 프로젝트의 경우 이 정도면 충분합니다.</p>
<p>마지막으로 예쁘게 만들기 위해 <code>index.html</code> 파일에서 원래 제목인 <code>&lt;title&gt;Vite + 리액트 + TS&lt;/title&gt;</code>을 <code>&lt;title&gt;{{title}}&lt;/title&gt;</code>과같이 바꾸고 템플릿으로 바꿀 수 있습니다.</p>
<pre><code class="language-tsx">&lt;html lang=&quot;en&quot;&gt;
  &lt;head&gt;
    &lt;title&gt;{{ title }}&lt;/title&gt;
  &lt;/head&gt;
  ...
&lt;/html&gt;;

// 서버에서 이 작업을 대신하세요.
const modifiedHTML = html.replace(&quot;{{title}}&quot;, title);</code></pre>
<p>향후에는 필요에 따라 템플릿 언어 중 원하는 언어로 변환할 수 있습니다.</p>
<p>물론 제목 <code>title</code> 태그에만 국한되지 않고 <code>&lt;head&gt;</code>에 있는 모든 정보를 이렇게 사전 렌더링 할 수 있습니다. 이렇게 하면 소셜 미디어 미리보기 기능의 “자바스크립트 없음” 문제를 비교적 쉽고 적은 비용으로 해결할 수 있어 일반적으로 이 이상은 필요하지 않습니다. 대부분은 정보가 포함된 <code>&lt;meta&gt;</code> 태그의 집합인 <a href="https://ogp.me/">오픈 그래프 프로토콜</a>에 의존합니다.</p>
<p>메타 태그뿐만 아니라 전체 페이지를 사전 렌더링 할 수도 있습니다! 하지만, 이 부분은 아래 &#39;도전 과제&#39; 블록에서 SSR에 대해 더 많은 것들을 배울 수 있습니다.</p>
<blockquote>
<p><strong>도전 과제</strong></p>
<ol>
<li><code>백엔드</code>에서 <code>index</code> 파일의 내용을 <code>backend/pre-rendering-index.ts</code>의 내용으로 바꿉니다.</li>
<li><code>src/index.html</code>에서 <code>title</code> 태그의 내용을 <code>{{ title }}</code>로 바꿉니다.</li>
<li>소셜 미디어 공유에 필요한 메타태그를 지원하도록 프런트엔드 및 백엔드 코드를 모두 리팩터링합니다(<a href="https://ogp.me/#metadata">목록 참조</a>).</li>
<li>프로젝트를 빌드하고 다시 <code>ngrok</code>을 통해 노출한 다음 각 페이지(로그인, 홈, 설정)를 원하는 소셜 미디어에 공유해 보세요. 이제 미리보기가 제대로 작동하고 각 페이지의 세부 정보가 표시되어야 합니다.</li>
<li>보너스 질문: 메타태그 정보가 클라이언트와 서버 간에 중복되지 않도록 프로젝트를 어떻게 리팩토링하나요?</li>
</ol>
</blockquote>
<h2 id="사전-렌더링-서버-비용">사전 렌더링 서버 비용</h2>
<p>위에서 메타태그 사전 렌더링이 비교적 저렴하다고 언급했습니다. 하지만 이것이 정확히 무엇을 의미할까요? 특히 도입 전의 비용과 노력에 비해 얼마나 저렴할까요?</p>
<p>안타깝게도 완전히 정적인 SPA와 비교하면 좋은 소식은 없습니다. 간단한 사전 렌더링 스크립트를 추가함으로써 이제 처리해야 할 복잡성이 명백하게 증가하는 것 외에 두 가지 문제가 생겼습니다.</p>
<h3 id="어디에-배포할까요">어디에 배포할까요?</h3>
<p>첫 번째 문제는 앱을 지금 어디에 배포해야 해야하는가입니다. 요즘에는 정적 리소스를 호스팅 하는 것이 매우 저렴하기 때문에 변경 전에는 호스팅 비용을 오랫동안 0으로 유지할 수 있었습니다. 이제는 서버가 필요합니다. 그리고 서버는 일반적으로 저렴하지 않습니다.</p>
<p>여기에는 가장 일반적으로 두 가지 해결법이 있습니다.</p>
<p>정적 리소스를 제공하는 호스팅 제공업체의 <strong>서버리스 펑션(function)</strong>을 사용할 수 있습니다. <a href="https://workers.cloudflare.com/">Cloudflare Workers</a>, <a href="https://www.netlify.com/platform/core/functions/">Netlify Functions</a>, <a href="https://vercel.com/docs/functions">Vercel Functions</a>, <a href="https://aws.amazon.com/lambda/">Amazon Lambdas</a> 등입니다. 대부분의 정적 리소스 호스팅 제공업체는 어떤 형태로든 이러한 펑션을 갖추고 있을 것입니다.</p>
<p>여기서 <strong>장점</strong>은 서버와 유지 관리에 대해 신경 쓸 필요가 없다는 것입니다. 클라우드 펑션은 제공업체가 우리를 대신해 처리하는 미니 서버와 같습니다. 우리가 할 일은 코드를 작성하는 것뿐이고, 마법처럼 그냥 작동합니다. 그 외의 모든 것은 그들의 관심사입니다. 스터디 프로젝트, 일부 특정 분야의 프로젝트, 초기의 프로젝트, 바이럴성이 내재되어 있지 않은 프로젝트의 경우 클라우드 펑션이 최적의 선택이 될 것입니다.</p>
<p>클라우드 펑션은 일반적으로 구성 및 배포가 매우 쉽고, 사용량에 따라 요금이 책정되며, 실제로 엔드포인트에서 사용량에 따라 요금이 부과됩니다. 주말 동안 실수로 컨테이너를 실행 상태로 두었다가 예상치 못한 요금이 부과될 가능성은 없습니다.</p>
<p><strong>단점</strong>은 &quot;사용량 당 가격&quot; 부분입니다. 웹사이트의 인기가 높을수록 사용량이 한도를 초과할 가능성이 높아집니다. 한 프로젝트가 HackerNews나 TikTok에서 인기를 얻으면서 갑자기 수백 명이 아닌 수백만 명의 방문자를 확보하고 소유자가 깜짝 놀라 5천 달러의 청구서를 받은 끔찍한 이야기를 몇 번 읽은 적이 있습니다. 따라서 서버리스 해결법에서는 지출 한도를 설정하고, 지출을 면밀히 모니터링하며, 이러한 상황에서 어떻게 해야 할지에 대한 계획을 세우는 것이 중요합니다.</p>
<p>서버리스 펑션이 마음에 들지 않는다면 실제 작은 노드(또는 다른 어떤 것들) <strong>서버</strong>로 유지한 후 <a href="https://aws.amazon.com/">AWS</a>, <a href="https://azure.microsoft.com/">Azure</a>, <a href="https://www.digitalocean.com/">Digital Ocean</a>, [선호하는 호스팅 제공업체 적용]에 이르기까지 모든 클라우드 플랫폼에 배포할 수 있습니다.</p>
<p>이 해결법에는 <strong>장점</strong>이 있습니다. 모든 것을 사용자가 제어할 수 있습니다. 한 해결법에서 다른 해결법으로 마이그레이션 할 때 공급업체에 종속되는 서버리스 펑션과 달리 코드 변경이 필요하지 않습니다. 가격은 일반적으로 훨씬 더 예측할 수 있고 훨씬 간단하며 사용량이 증가할 때 훨씬 저렴합니다. 또한 서버리스 펑션은 일반적으로 매우 제한적이지만, 원하는 기술 스택을 사용할 수 있습니다.</p>
<p><strong>단점</strong>도 장점과 똑같습니다. 이제 모든 것은 여러분에게 달려 있습니다. CPU/메모리 사용량을 모니터링해야 합니다. 통합 가시성에 대한 걱정. 확장에 대한 걱정. 메모리 누수로 인해 밤잠을 설치게 될 것입니다.</p>
<p>그리고 지리적 영역에 대해서도 걱정해야 합니다. 이는 순수 SPA 앱에 어떤 종류의 서버를 도입할 때 발생하는 두 번째 문제로 이어집니다.</p>
<h3 id="서버-도입이-성능에-미치는-영향">서버 도입이 성능에 미치는 영향</h3>
<p><a href="https://www.developerway.com/posts/initial-load-performance">초기 로드</a>와 <strong>지연 시간</strong> 및 <strong>CDN</strong>이 초기 로드 성능에 미치는 영향에 대한 글을 기억하시나요? 메타태그만 사전 렌더링하는 초보적인 서버라도 도입하여 신규 사용자든 기존 사용자든 상관없이 모든 초기 로드 요청에 대해 서버에 캐시 되지 않은 불가피한 왕복 요청을 의무적으로 도입하고 있습니다.</p>
<p>처음부터 좋지 않았던 SPA 앱의 초기 로드 성능을 조금 더 나쁘게 만들었을 뿐입니다. 얼마나 더 나빠졌는지는 서버가 정확히 어디에 배포되었는지에 따라 크게 달라집니다.</p>
<p>서버리스 펑션 중 하나로 배포된 경우라면 그렇게 나쁘지 않을 가능성이 있습니다. 일부 공급자는 이러한 펑션을 &quot;엣지 서버&quot;에서 실행할 수 있습니다. 즉, 이러한 펑션은 여러 서버가 최종 사용자에게 더 가까이 분산되어 있습니다. 정적 리소스에 대한 CDN과 거의 동일합니다. 이 경우 지연 시간이 최소화되고 성능 저하가 최소화됩니다.</p>
<p>하지만 자체 관리형 서버를 사용하면 분산 네트워크의 이점을 누릴 수 없습니다. 특정 지역에 배포해야 하기 때문입니다. 따라서 이 지역과 지구 반대편에 있는 사용자가 성능 저하의 영향을 실제로 느낄 수 있습니다.</p>
<p><img src="https://www.developerway.com/assets/ssr-deep-dive-for-react-developers/2.performance-degradation-20250210-050001.png" alt=""></p>
<p>성능에 미치는 영향이 중요하다면 어떻게든 해결해야 합니다. 복잡한 캐싱 전략, 다른 지역으로의 배포 등을 준비해야 합니다. 기본적으로 더 이상 서버가 없는 단순한 프런트엔드 앱이 아닙니다. 이제는 풀스택 또는 백엔드 우선 앱입니다.</p>
<h3 id="vercelnetlify의-nextjs">Vercel/Netlify의 Next.js</h3>
<p>바로 떠오를 수 있는 질문입니다. &quot;Next.js로 프런트엔드를 작성하고 Vercel/Netlify에 배포하기만 하면 됩니다. 이런 내용을 꼭 알아야 하나요?&quot;</p>
<p>여기서 정답은 &quot;안타깝지만, 예, 피할 수 없습니다.&quot;입니다. 왜냐하면 Next.js 우선 호스팅 제공업체는 기본적으로 앱을 자바스크립트 파일과 여러 개의 작은 서버리스 펑션으로 변환하기 때문입니다. 사용자가 제어할 수도, 알지도 못하는 사이에 이런 일이 벌어집니다.</p>
<p>따라서 Next.js 프로젝트를 &#39;정적&#39;으로 내보내도록 명시적으로 설정하지 않은 경우 &#39;사전 렌더링의 서버 비용&#39;은 모든 항목에 적용됩니다.</p>
<blockquote>
<p><strong>도전 과제</strong></p>
<ol>
<li>Vercel/Netlify와 같은 서버리스 플랫폼에 &quot;기본적으로&quot; 배포(원클릭 배포)된 Next.js 앱이 있는 경우, 해당 앱을 위해 생성된 펑션이 몇 개인지 찾아보세요.</li>
<li>&quot;엣지&quot; 펑션인가요, 아니면 일반 펑션인가요? 사용량이 어떻게 계산되는지 알 수 있나요? 한도를 초과하기 전에 웹사이트가 처리할 수 있는 방문자 수는 몇 명인가요?</li>
<li>&quot;일반&quot; 펑션과 &quot;엣지&quot; 펑션의 조합이 있는 경우 어떤 펑션이 무엇을 담당하고 배포된 프로젝트에 어떤 영향을 미치는지 대응할 수 있나요?</li>
</ol>
</blockquote>
<h2 id="서버에서-전체-페이지-사전-렌더링ssr">서버에서 전체 페이지 사전 렌더링(SSR)</h2>
<p>사전 렌더링에 대해 좀 더 이야기해 보겠습니다. 위 섹션에서는 기존 문자열을 다른 문자열로 대체하는 것이 쉽기 때문에 메타태그만 사전 렌더링했습니다. 하지만 <code>&lt;head&gt;</code> 태그를 넘어서면 어떻게 해야 할까요? 서버에서 전송되는 HTML 페이지의 <code>&lt;body&gt;</code> 태그의 내용을 살펴봅시다.</p>
<pre><code class="language-html">&lt;body&gt;
  &lt;div id=&quot;root&quot;&gt;&lt;/div&gt;
  &lt;script type=&quot;module&quot; src=&quot;./main.tsx&quot;&gt;&lt;/script&gt;
&lt;/body&gt;</code></pre>
<p><a href="https://www.developerway.com/posts/client-side-rendering-flame-graph">클라이언트 사이드 렌더링이 어떻게 작동</a>하는지 기억하시나요? 스크립트가 다운로드되고 처리되면 리액트는 &quot;root&quot; 요소를 가져와서 생성된 DOM 요소에 추가합니다. 그렇다면 빈 div 대신 일부 콘텐츠가 포함된 div를 반환하면 어떻게 될까요? 커다란 빨간색 블록으로 만들어 봅시다.</p>
<pre><code class="language-html">&lt;div id=&quot;root&quot;&gt;
  &lt;div style=&quot;background:red;width:100px;height:100px;&quot;&gt;Big Red Block&lt;/div&gt;
&lt;/div&gt;</code></pre>
<p><code>index.html</code>에 추가하고 프로젝트를 빌드하고 난 다음 시작하고 더 잘 보이도록 캐시를 비활성화하고 CPU와 네트워크 속도를 낮추는 것을 잊지 마세요.</p>
<p>페이지를 새로 고치면 큰 빨간색 블록이 잠시 깜박이다가 일반 대시보드 페이지로 바뀐 것을 볼 수 있을 것입니다. 첫 번째 좋은 소식은 빨간색 블록이 계속 남아 있지 않았다는 것입니다. 분명히 리액트는 내부에 무언가를 삽입하기 전에 “root” div를 지웁니다. 또는 기존 자식들을 재정의해도 이 글에서는 상관없습니다.</p>
<p>두 번째 좋은 소식은 <a href="https://www.developerway.com/posts/client-side-rendering-flame-graph">성능 그래프</a>를 보면 알 수 있습니다. 지금 기록해 보세요. 결과는 다음과 같이 나옵니다.</p>
<p><img src="https://www.developerway.com/assets/ssr-deep-dive-for-react-developers/3.red-block-performance-20250212-055120.png" alt=""></p>
<p>여기서 순서와 타이밍에 특히 주목해 주세요.</p>
<p>처음에는 <a href="https://www.developerway.com/posts/client-side-rendering-flame-graph">이전에 보았던 그래프와</a> 똑같아 보입니다. 먼저 서버에서 HTML을 기다리면 &#39;Main&#39; 섹션에 파란색 HTML 파싱 블록이 나타납니다. 이 블록은 어느 시점에서 CSS와 자바스크립트(&#39;Network&#39;의 노란색과 보라색 블록)의 다운로드를 트리거 했습니다.</p>
<p>하지만 CSS 다운로드가 완료된 후 다른 일이 발생하기 시작했습니다. 먼저 보라색 Layout 블록(파란색 HTML 블록과 같은 수준)이 다소 길게 표시되었습니다. 전에는 이런 일이 없었는데요! 다운로드가 완료된 후 거의 즉시 FCP(First Contentful Paint)가 트리거 되었습니다. 하지만 상단의 자바스크립트 바는 여전히 로딩 중입니다! 그 후 자바스크립트 로딩이 완료되고, 처리되고, 페인팅 된 다음 LCP(Large Contentful Paint)가 트리거 되는 등 평소와 같은 상황이 계속됩니다.</p>
<p>프레임 스크린숏이 표시되는 맨 위 섹션에 마우스를 가져가면 FCP와 LCP 사이의 간격이 페이지에 큰 빨간색 블록이 표시된 기간과 정확히 일치하는 것을 확인할 수 있습니다. 저에게 있어 FCP와 LCP 사이의 간격은 약 <strong>500</strong>밀리초이며, FCP는 약 <strong>800</strong>밀리초, LCP는 약 <strong>1.3</strong>초입니다.</p>
<p>이 500ms는 초기 로드 시 클라이언트 사이드 렌더링 비용과 거의 비슷한 수준인 것 같습니다. 엄청나네요! LCP에서 500ms를 어떻게든 줄일 수 있다면 40%나 개선되는 것이죠! 이걸로 승진할 수도 있겠네요.</p>
<p>다행히 모든 것이 가능합니다. 리액트는 전체 앱을 사전 렌더링 할 수 있는 <a href="https://react.dev/reference/react-dom/server">몇 가지 메서드</a>을 제공하며 이론적으로 여기에서 사용할 수 있습니다. 예를 들어, <a href="https://react.dev/reference/react-dom/server/renderToString">“renderToString”</a>이 있습니다. 문서에 따르면 이 메서드는 앱을 문자열로 렌더링 할 수 있습니다.</p>
<pre><code class="language-tsx">const App = () =&gt; &lt;div&gt;React app&lt;/div&gt;;

// 어딘가의 서버
const html = renderToString(&lt;App /&gt;); // 출력물: &lt;div&gt;React app&lt;/div&gt;</code></pre>
<p>이미 서버에서 문자열을 다루고 있기 때문에 이것은 완벽해 보입니다. 제가 해야 할 일은 빈 “root” div를 이 함수의 출력물로 대체하는 것뿐입니다. 메타 태그에서 했던 것과 똑같습니다. 해볼까요?</p>
<p><code>backend/index.ts</code>로 이동하여 위에서 수정한 내용을 정리합니다. 주석 처리된 코드를 찾습니다.</p>
<pre><code class="language-tsx">// return c.html(preRenderApp(html));</code></pre>
<p>그리고 코멘트를 취소합니다. 퍼포먼스를 다시 녹화합니다. 최종 결과는 다음과 같아야 합니다.</p>
<p><img src="https://www.developerway.com/assets/ssr-deep-dive-for-react-developers/4.content-pre-rendering-20250212-055231.png" alt=""></p>
<p>FCP와 LCP가 동시에 발생한 것을 보면 그 차이를 바로 확인해 볼 수 있습니다. 리액트로 생성된 메인 자바스크립트가 트리거 되기 전, 심지어 자바스크립트 로딩이 완료되기도 전에 말이죠. 이는 콘텐츠 사전 렌더링이 작동하고 있음을 의미합니다! 🎉 행복한 하루가 되었네요 ☀️☺️. 맨 위에 있는 스크린숏을 마우스로 가리키면 이는 무작위로 발생한 현상이 아니라 실제로 그 당시에 표시된 아름다운 대시보드가 맞는지 확인할 수 있습니다.</p>
<p>그런데 작은 이상 현상이 있습니다. FCP가 약속한 것보다 늦게 트리거 된다는 것입니다. 800ms로 예상했지만 실제로는 900ms 정도입니다. 모든 성능에 대한 반복적인 교훈으로, 정확한 수치를 미리 약속하지 않는다는 것입니다. 😅. 그런데 어디서 100ms가 손실된 걸까요?</p>
<p>먼저, 서버에 대한 초기 요청이 있는 왼쪽 맨 위 모서리인 &quot;Network&quot; 섹션을 살펴보세요. 파란색 실선이 보이시나요? 이것이 HTML 콘텐츠 다운로드입니다. 이제 단순한 빈 <code>&lt;div&gt;</code> 뿐만 아니라 더 많은 요소를 전송하고 있습니다. 정확한 수치를 보려면 해당 블록 위로 마우스를 가져가세요. 누락된 100ms 중 약 1/3이 콘텐츠 다운로드에 소요됩니다.</p>
<p>또한 &#39;Parse HTML&#39; 작업 뒤에 있는 보라색 &#39;Layout&#39; 블록에 주목하세요. 훨씬 길어 보이지 않나요? 정확한 수치를 보려면 다시 마우스를 가져가 보세요. 여기 100ms 중 3분의 2가 사라졌습니다. 브라우저는 추가 HTML을 다운로드할 뿐만 아니라 더 많은 요소의 위치를 계산한 후 페인팅해야 했기 때문입니다.</p>
<p>그래서 시간을 놓쳤습니다. 그래도 그만한 가치가 있지 않나요? LCP 타이밍에서 400ms를 단축하고 초기 로드 성능을 30% 개선했습니다! 그리고 또 다른 멋진 부분이 있습니다. 지금 자바스크립트를 비활성화하고 페이지를 새로고침해보세요. 대시보드가 그대로 표시됩니다! 전체 리로드를 유발하지만, 링크도 작동합니다.</p>
<p>이 부분이 바로 SSR의 가치입니다. 이제 페이지에 액세스 권한을 부여하려는 모든 검색 엔진과 다른 모든 로봇이 자바스크립트를 로드하지 않고도 모든 것을 볼 수 있습니다. 성능 향상은 좋은 보너스입니다. 그리고 불안정한 보너스이기도 합니다.</p>
<h2 id="ssr의-초기-부하-악화의-가능성">SSR의 초기 부하 악화의 가능성</h2>
<p>성능에 확실한 해결책이 없기 때문에 불안정합니다. SSR이 SPA 앱의 초기 부하를 100% 증가시킬 것이라고 말하는 사람은 잘못 알고 있는 것입니다. 이제 네트워크 상태, 클라이언트 사이드 및 서버 사이드 렌더링이 어떻게 작동하는지 알았으니, SSR이 LCP를 악화시키는 시나리오를 생각해 볼 수 있을까요?</p>
<p>방법은 다음과 같습니다.</p>
<p>CPU 쓰로틀링을 비활성화하고 컴퓨터를 다시 빠르게 만듭니다. 네트워킹을 가능한 가장 느린 시뮬레이션으로 설정합니다. 저에게는 기본 Chrome 3G가 적합했지만, 컴퓨터의 속도에 따라 더 느리게 설정해야 할 수도 있습니다. &#39;disabled cache&#39; 체크박스를 <em>선택을 취소</em>합니다. CSS/JS 파일이 브라우저 메모리에서 제공되기를 원합니다.</p>
<p>이제 사전 렌더링이 있는 경우와 없는 경우의 LCP를 측정합니다.</p>
<p>결과는 다음과 같습니다. 사전 렌더링이 <em>없는</em> &#39;SPA&#39; 모드에서는 LCP가 약 2.13초입니다. 사전 렌더링을 사용하면 “SSR” 모드에서는 약 2.62초입니다. 거의 500밀리 초나 더 길어졌습니다!</p>
<p>성능 차트는 이 상황에서도 흥미롭게 읽을 수 있습니다. “SPA” 모드는 다음과 같습니다.</p>
<p><img src="https://www.developerway.com/assets/ssr-deep-dive-for-react-developers/5.slow-network-fast-cpu-spa-20250212-224305.png" alt=""></p>
<p>처음에는 네트워크 섹션에서 서버의 응답을 기다리는 아주 긴 블록(2초)이 표시됩니다. 이는 느린 네트워크 연결로 인한 지연 시간입니다. 그다음에는 네트워크가 아닌 브라우저 캐시에서 자바스크립트 및 CSS 리소스에 거의 즉각적으로 액세스 할 수 있습니다. 또한 빈 div일 뿐인 HTML 콘텐츠도 거의 즉각적으로 다운로드됩니다. 그런 다음 CPU 속도가 느려지지 않기 때문에 자바스크립트 실행 속도가 평범하거나 매우 빠릅니다. 이것이 리액트가 페이지를 생성하는 과정입니다. 그리고 마지막으로 페이지가 표시됩니다.</p>
<p>이제 SSR 모드가 활성화된 동일한 네트워크/CPU 조건입니다.</p>
<p><img src="https://www.developerway.com/assets/ssr-deep-dive-for-react-developers/6.slow-network-fast-cpu-ssr-20250212-225224.png" alt=""></p>
<p>동일하게 초기 대기 시간과 지연 시간이 사라지지 않았습니다. 그런 다음 HTML 다운로드가 시작됩니다. 하지만 지금은 콘텐츠가 많기 때문에 다운로드에 시간이 아주 오래 걸리고 대역폭이 매우 줄어듭니다.</p>
<p>그리고 가장 흥미로운 부분은 콘텐츠가 다운로드되는 동안 메인 섹션에서 활동량이 급증하는 것을 볼 수 있다는 점입니다. 확대해서 마우스를 가져가면 대부분 레이아웃 작업입니다. 브라우저는 이미 (캐시에서) CSS와 자바스크립트를 다운로드한 상태이므로 작은 조각을 얻자마자 레이아웃을 그리는 데 필요한 모든 정보를 가지고 있습니다. 그리고 실제로도 그렇습니다.</p>
<p>페이지에서 인터페이스가 점진적으로 구축되는 것을 실제로 <strong>볼 수</strong> 있습니다. 먼저 사이드바, 상단 탐색, 상단 차트, 표가 차례로 표시됩니다. 이 모든 것이 HTML이 천천히 표시되는 순서대로 진행됩니다. 이것이 멋지지 않다면 무엇이 멋진지 모르겠습니다.</p>
<p>이 예는 이상한 엣지 케이스처럼 느껴지지만 실제로는 그렇지 않습니다. 예를 들어 느린 네트워크 + 엄청난 지연 시간 + 빠른 노트북의 조합은 비즈니스 여행객에게 자주 발생합니다. 또는 야생동물 사진작가. 또는 여행 블로거. 또는 원격지로 파견된 엔지니어도 마찬가지입니다. 따라서 앱이 주로 특정 틈새시장을 목표로하고 있고 이미 SPA인 경우 SSR을 도입하면 문제가 더 악화될 수 있습니다.</p>
<p>물론 그렇지 않을 수도 있습니다. 이는 다운로드되는 HTML의 크기, 디바이스의 실제 속도, 앱이 렌더링해야 하는 자바스크립트의 양에 따라 달라집니다. 결국 모든 것은 고객을 파악하고 모든 것을 측정하는 두 가지로 귀결됩니다.</p>
<h2 id="ssr과-하이드레이션">SSR과 하이드레이션</h2>
<p>콘텐츠를 더 빨리 보여주는 것에 대한 흥분으로 콘텐츠가 로드된 후 어떤 일이 일어나는지 조사하는 것을 잊었습니다.</p>
<p>큰 빨간색 블록의 동작을 기억하시나요? 리액트가 자체 요소를 로드하고 생성한 후에는 “root” div의 콘텐츠와 큰 빨간색 블록을 포함한 내부의 모든 것을 완전히 대체했습니다. 하지만 이상한 빨간색 블록 대신 향후 페이지의 실제 HTML을 보내면 어떻게 될까요?</p>
<p>사실 아무 일도 일어나지 않습니다. 리액트에게 이 콘텐츠가 중요하다고 어떤 식으로든 말하지 않았기 때문에 “루트” div의 전체 콘텐츠를 지우고 그 자체로 대체하는 것과 똑같은 방식으로 작동합니다. HTML 관점에서 보면 완전히 동일한 콘텐츠이므로 육안으로는 차이가 보이지 않습니다.</p>
<p>하지만 성능 프로필에서는 그 차이를 확인할 수 있습니다. CPU와 네트워크 속도를 낮춰 동작이 약간 더 잘 보이도록 하고 SSR 예제의 성능을 다시 기록합니다. CSS와 자바스크립트가 수신된 후 어떤 일이 일어나는지 주목해 주세요.</p>
<p><img src="https://www.developerway.com/assets/ssr-deep-dive-for-react-developers/7.ssr-no-hydration-20250213-011313.png" alt=""></p>
<p>왼쪽 상단에는 리소스 다운로드가 완료된 네트워크 섹션이 있습니다. CSS를 받은 직후 아래에 큰 보라색 &#39;Layout&#39; 섹션이 표시되는데, 바로 여기에 SSR이 적용된 콘텐츠가 표시됩니다. 왼쪽 상단의 자바스크립트 노란색 블록 로딩이 완료되면 리액트가 시작됩니다. 다소 긴 작업(180ms)은 리액트가 UI를 빌드 할 때입니다. 오른쪽 맨 아래에는 다시 작은 레이아웃 블록이 표시됩니다.</p>
<p>이것은 우리가 이미 여러 번 보았던 <a href="https://www.developerway.com/posts/client-side-rendering-flame-graph">클라이언트 사이드 렌더링</a>의 전형적인 그림입니다. 리액트가 “root” div를 지우고 대신 생성하는 모든 것을 삽입하는 경우입니다. 그리고 이것은 완전히 불필요한 작업입니다. 리액트에는 이미 모든 DOM 요소가 존재하므로 대신 재사용할 수 있습니다. 당연히 더 빨라야 합니다.</p>
<p>이때 <strong>“하이드레이션”</strong>이라는 것이 등장합니다. “하이드레이션&quot;은 위에서 제가 원했던 것과 정확히 일치하는 HTML이 페이지에 이미 있다는 것을 리액트에 보여줍니다. 따라서 리액트는 기존 DOM 노드를 재사용하고, 이벤트 리스너를 추가하고, 향후 기능을 위해 내부적으로 필요한 모든 것을 준비한 다음 하루를 마무리할 수 있습니다. 불필요한 컴포넌트를 처음부터 마운트 할 필요가 없습니다!</p>
<p>리액트의 하이드레이션은 실제로 한 번의 함수 호출로 매우 간단하게 구현할 수 있습니다. <code>createRoot</code> 진입점을 이것으로 대체하기만 하면 됩니다.</p>
<pre><code class="language-tsx">hydrateRoot(
  document.getElementById(&quot;root&quot;)!,
  &lt;StrictMode&gt;
    &lt;App /&gt;
  &lt;/StrictMode&gt;
);</code></pre>
<p>이 코드는 <code>src/main.tsx</code>에서 찾을 수 있습니다. <code>createRoot</code> 부분을 주석 처리하고 hydration 부분을 주석 처리 해제하세요. 그런 다음 프로젝트를 다시 빌드하고 다시 시작하세요.</p>
<pre><code class="language-bash">npm run build
npm run start</code></pre>
<p><img src="https://www.developerway.com/assets/ssr-deep-dive-for-react-developers/8.ssr-with-hydration-20250213-013318.png" alt=""></p>
<p>리액트 관련 자바스크립트 실행에 더 이상 보라색이 표시되지 않습니다. 그리고 이제 180ms에서 142ms로 약간 더 빨라졌습니다. 특히 이전에 LCP가 트리거 되었다는 점을 고려하면 지금은 그다지 크게 느껴지지 않을 수도 있습니다. 하지만 항상 이렇게 유지되는 것은 아닙니다.</p>
<p>예를 들어 &#39;disable network cache&#39;을 선택 해제하고 CPU를 낮추면서 네트워크 스로틀링을 제거해 보세요. 인터넷 속도는 빠르지만, 디바이스가 느린 재방문자를 가상화합니다. 하이드레이션이 없으면 FCP와 LCP가 분리되고 LCP가 자바스크립트 작업의 맨 끝으로 밀려납니다. 이 경우 LCP는 약 <strong>550ms</strong>입니다. 하이드레이션을 활성화하면 LCP는 FCP에 더 가깝게 이동하여 자바스크립트 작업이 시작될 때 약 <strong>280ms</strong>를 유지합니다.</p>
<p>메인 스레드를 차단하고 최대한 줄이는 문제도 있는데, 하이드레이션이 이를 도와줍니다. 또한 하이드레이션은 자바스크립트 리스너에만 국한된 것이 아닙니다. 또한 일부 초기 데이터를 가져와 앱에 주입할 수 있으므로 스피너나 콘텐츠의 플래시를 로딩하는 것을 방지할 수 있습니다. 이에 대해서는 나중에 다른 글에서 다룰 예정입니다.</p>
<h2 id="이렇게-ssr을-구현해야-하나요">이렇게 SSR을 구현해야 하나요?</h2>
<p>이제 SSR이 특정 사례에 매우 유용할 수 있고 구현하는 것이 다소 사소해 보이기 때문에, 스터디 프로젝트의 코드를 사용하여 나만의 SSR을 구현할 수 있느냐는 질문이 생길 수 있습니다.</p>
<p>이 <a href="https://www.developerway.com/">블로그</a>의 대답은 매우 드물게도 &#39;절대 안 된다&#39;입니다! 이 해결법은 한 가지를 켜고 끄면서 사전 렌더링 된 콘텐츠가 다양한 관점에서 어떻게 작동하는지 살펴보는 스터디 목적으로는 괜찮습니다.</p>
<p>하지만 실제로는 전혀 사소한 일이 아닙니다. 저는 제대로 작동하기 위해 해야 할 일의 절반을 숨기고 절반은 아직 구현되지 않았습니다. 백엔드에서 최신 리액트 기능을 지원하지 않은 매우 기본적이고 거의 사용되지 않은 버전입니다.</p>
<p>우선, 여기에는 개발 서버에 대한 SSR이 없습니다. 따라서 프로젝트를 지속적으로 다시 빌드하는 것 외에는 SSR을 디버깅할 방법이 없습니다. 변경 사항을 적용하기 위해 항상 다시 빌드해야 하는 이유의 절반은 바로 이 때문입니다. (나머지 절반은 항상 프로덕션 빌드에서 성능을 측정해야 해서 특별히 죄책감을 느끼지 않습니다).</p>
<p>핫 리로드와 같은 멋진 기능을 원한다면 직접 구현해야 합니다. SSR을 Vite와 올바르게 통합하는 방법에 대한 <a href="https://www.developerway.com/posts/ssr-deep-dive-for-react-developers#:~:text=whole%20large%20set%20of%20instructions">전체 지침</a>이 있습니다. 웹팩의 경우 매우 다르며 문서화가 잘 되어 있지 않을 가능성이 높습니다. 더 색다른 것을 위해 어디서부터 시작해야 할지 모르겠습니다.</p>
<p>둘째, 리액트 문서에서 보여드린 예쁘게 생긴 문자열인 <code>const html = renderToString(&lt;App /&gt;);</code> 은 실제로는 작동하지 않습니다. 여기서 문제는 이 부분, 즉 <code>&lt;App /&gt;</code>입니다. 이건 JSX이고, 우리가 대부분 리액트 코드를 작성하는 방식이기 때문에 지금은 매우 정상적으로 보입니다. 하지만 JSX가 작동하는 유일한 이유는 빌드 시스템에 Babel에 의해 구동되는 변환 단계가 있기 때문입니다(또는 아닐 수도 있고, 항상 달라질 수 있습니다). “순수” 노드나 다른 서버 프레임워크는 이를 지원하지 않습니다.</p>
<p>실제로 어떻게 구현되는지 <code>backend/pre-render.ts</code>를 살펴보세요.</p>
<p>먼저 Vite 자체에서 변환된 <code>App</code> 코드를 추출했습니다:</p>
<pre><code class="language-tsx">const { default: App } = await vite.ssrLoadModule(&quot;@/App&quot;);</code></pre>
<p>웹팩을 사용하는 경우 수동으로 Babel 플러그인을 구성하고 등록해야 할 가능성이 높습니다. 따라서 첫 번째 단계부터라도 여기서 무슨 일이 일어나고 있는지, 앱이 어떻게 구현해야 하는지 이해해야 합니다.</p>
<p>두 번째 단계는 실제 <code>renderToString</code>입니다.</p>
<pre><code class="language-tsx">const reactHtml = renderToString(React.createElement(App, { ssrPath: path }));</code></pre>
<p>여전히 문서와 달리 실제 백엔드 파일에 대한 JSX 지원은 Vite에서 추출하는 것과는 다릅니다. 하지만 문서를 읽어보면 <code>renderToString</code>이 <a href="https://react.dev/reference/react-dom/server/renderToString">데이터 스트리밍 및 대기를 지원</a>하지 않는다는 것을 알 수 있습니다.</p>
<p>따라서 실제로 적절한 SSR을 구현하려면 앱에 이러한 새로운 기능이 필요한지 여부를 이해해야 합니다. 그리고 필요하다면 백엔드에서 구현하는 방법을 파악해야 합니다. 권장 방법에 대한 <a href="https://react.dev/reference/react-dom/server/renderToPipeableStream">몇 가지 문서</a>가 있고 깃허브에 해당 주제에 대한 <a href="https://github.com/reactwg/react-18/discussions/22">토론</a>에 대한 <a href="https://github.com/reactwg/react-18/discussions/37">스레드</a>도 몇 개 있으므로 최소한 시작은 할 수 있습니다.</p>
<p>그러나 <em>많은</em> 작업이 필요하고 언급된 내용은 시작에 불과하며, 어느새 프로젝트가 3개월이나 늦어져 기본적으로 자신만의 Next.js를 구현하고 있습니다. 왜 경쟁자가 그렇게 많지 않다고 생각하시나요?</p>
<p>그래서 매우 타당한 비즈니스 이유가 있고 시간, 리소스 및 전문성 측면에서 많은 지원이 없다면 이미 존재하는 SSR 프레임워크를 사용하는 것이 더 쉬울 수 있습니다. 특히 백엔드 부분은 퍼즐의 한 조각에 불과하다는 점을 고려하면 더욱 그렇습니다. 또한 프런트엔드에는 많은 복잡성이 수반됩니다.</p>
<h2 id="ssr과-프런트엔드">SSR과 프런트엔드</h2>
<p>앱의 크기와 SSR에 최적화된 정도에 따라 실제로는 백엔드 부분보다 훨씬 더 복잡할 수 있습니다. 그래요, 위에서 SSR을 구현하면서 숨겼던 또 하나의 사실은 프런트엔드 코드도 변경해야 한다는 점입니다.</p>
<h3 id="브라우저-api와-ssr">브라우저 API와 SSR</h3>
<p>브라우저로 전송되는 HTML을 어떻게 얻었는지 기억하시나요? 리액트의 <code>renderToString</code>으로 문자열을 생성한 다음 그 문자열을 다른 문자열에 주입했습니다. 이 과정 근처에는 브라우저가 없었고 앞으로도 없을 것입니다.</p>
<p>그렇다면 프런트엔드에서 사용하던 브라우저 변수에 대한 모든 호출은 어떻게 될까요? <code>window.location</code>, <code>window.history</code>, <code>document.getElementById</code>? <code>window</code>, <code>document</code> 등이 <code>undefined</code> 상태로 바뀔 것입니다. 전역 범위로 주입할 수 있는 브라우저가 없기 때문입니다.</p>
<p>따라서 리액트가 이들에 직접 접근하는 함수를 호출(즉, 컴포넌트를 렌더링)하려고 하면 <code>window is not defined</code> 오류와 함께 실패합니다. 앱 전체가 폭발할 것입니다. 단순히 폭발하는 것이 아닙니다. 서버 부분이 폭발할 것이고, 더 심각한 것은 프런트엔드 부분에서 오류를 포착하여 “작업 중입니다, 여기 쿠키가 있습니다”라는 예쁜 화면을 표시할 기회가 전혀 없다는 것입니다. 오류 처리는 서버에서 처리해야 하며, 특별한 &#39;서버&#39; 오류 화면이 있어야 합니다.</p>
<blockquote>
<p><strong>도전 과제</strong></p>
<ol>
<li>프런트엔드 코드의 임의의 부분(예: <code>src/App.tsx</code>)에 간단한 <code>console.info(window.location);</code>을 추가해 보세요.</li>
<li><code>npm run build</code>를 사용하여 앱을 다시 빌드하고 다시 시작합니다.</li>
<li>화면에 <code>Internal Server Error</code> 문자열이 표시되어야 합니다.</li>
<li>이 문제를 해결할 방법이 있나요?</li>
</ol>
</blockquote>
<p>이 문제를 해결하는 일반적인 방법은 <code>window</code>(및 다른 모든 변수)에 액세스 하기 전에 전역 변수가 선언되었는지 확인하는 것입니다:</p>
<pre><code class="language-tsx">if (typeof window !== &quot;undefined&quot;) {
  // global window API를 사용할 수 있을 때 무언가를 수행합니다.
}</code></pre>
<p><code>frontend/utils/use-client-router.tsx</code>의 코드를 보면 바로 이것이 제가 해야 했던 일입니다. 그리고 런타임에 <code>window</code>, <code>document</code> 또는 다른 모든 것에 액세스해야 할 때마다 이 작업을 수행해야 합니다.</p>
<h3 id="useeffect와-ssr">useEffect와 SSR</h3>
<p>자세히 보면 <code>useEffect</code> 내부에서 <code>typeof window를</code> 확인할 필요가 없다는 것을 <code>use-client-router</code>파일을 보면 알 수 있습니다.</p>
<pre><code class="language-tsx">useEffect(() =&gt; {
  const handlePopState = () =&gt; {
    setPath(window.location.pathname);
  };
  window.addEventListener(&quot;popstate&quot;, handlePopState);
  return () =&gt; window.removeEventListener(&quot;popstate&quot;, handlePopState);
}, []);</code></pre>
<p>서버에서 실행할 때(즉, <code>renderToString</code>과 친구를 통해) 리액트는 <code>useEffect</code>를 트리거하지 않기 때문입니다. 그리고 <code>useLayoutEffect</code>도 마찬가지입니다. 이러한 훅은 하이드레이션이 발생한 후에 클라이언트에서만 실행됩니다. 이 동작의 이유에 대해 더 자세히 알고 싶다면 이 <a href="https://gist.github.com/gaearon/e7d97cdf38a2907924ea12e4ebdf3c85">짧은 설명</a>과 핵심 리액트 팀원들의 이 주제에 대한 <a href="https://github.com/facebook/react/issues/14927">긴 토론</a>을 살펴보세요.</p>
<p>자바스크립트가 로드될 때 콘텐츠의 &quot;깜빡임&quot;이 발생하므로 <code>useEffect</code>의 결과로 UI 변경이 예상되는 경우 반드시 염두에 두어야 할 사항입니다.</p>
<h3 id="조건부-ssr-렌더링-불가">조건부 SSR 렌더링 불가</h3>
<p>코드의 일부에는 브라우저 API에 대한 종속성이 너무 많아서 SSR 모드에서는 해당 부분의 렌더링을 완전히 건너뛰는 것이 더 쉽다고 생각할 수 있습니다. 따라서 자연스럽게 이렇게 하고 싶은 유혹이 생길 수 있습니다.</p>
<pre><code class="language-tsx">const Component = () =&gt; {
  // SSR 모드에서는 아무것도 렌더링하지 않습니다.
  if (typeof window === &quot;undefined&quot;) return null;

  // 클라이언트 모드가 시작될 때 렌더링합니다.
  return ...
}</code></pre>
<p>아니요. 이건 작동하지 않습니다. 아니, 더 정확하게는 작동할 것입니다. 리액트는 “서버” 코드에서 생성된 HTML이 클라이언트 코드에서 생성된 HTML과 <strong>완전히 동일</strong>하다고 기대할 것입니다.</p>
<p>너무 혼란스러워졌는데 클라이언트 사이드 렌더링 패턴으로 돌아가서 “root” div의 전체 콘텐츠를 지우고 새로 생성된 요소로 대체할 것입니다. 하이드레이션이 전혀 일어나지 않은 것처럼 작동하며 이에 따라 발생하는 모든 단점이 있습니다.</p>
<p>이 도전 과제를 시도해 보세요.</p>
<ul>
<li><code>frontend/pages/dashboard.tsx</code>의 어딘가에(또는 원하는 다른 위치에) 코드를 사용하여 <code>ClientOnlyButton</code> 컴포넌트를 만듭니다.</li>
</ul>
<pre><code class="language-tsx">const ClientOnlyButton = () =&gt; {
  if (typeof window === &quot;undefined&quot;) return null;
  return &lt;button&gt;Button&lt;/button&gt;;
};</code></pre>
<ul>
<li>페이지의 어딘가에 렌더링 합니다.</li>
<li>평소처럼 프로젝트를 다시 빌드하고 다시 시작합니다.</li>
<li>성능 프로필을 기록합니다. 하이드레이션이 아직 구현되지 않았을 때 보았던 그림이 리액트 자바스크립트 태스크 내부의 레이아웃 블록과 함께 표시되어야 합니다.</li>
</ul>
<p>사라진 하이드레이션 - 운이 좋다면! 때로는 정말 이상한 레이아웃 버그가 발생하여 웹사이트가 완전히 망가져 보일 수 있습니다.</p>
<p>올바른 방법은 리액트의 라이프사이클에 의존하여 SSR과 호환되지 않은 블록을 “숨김”하는 것입니다. 이를 위해서는 상태를 도입하고 컴포넌트가 마운트되었는지 여부를 추적해야 합니다.</p>
<pre><code class="language-tsx">const Component = () =&gt; {
  // 초기에는 마운트 되지 않습니다.
  const [isMounted, setIsMounted] = useState(false);
};</code></pre>
<p>그런 다음 컴포넌트가 마운트되었을 때, 즉 <code>useEffect</code> 내부에서 이 상태를 <code>true</code>로 뒤집습니다.</p>
<pre><code class="language-tsx">const Component = () =&gt; {
  // 초기에는 마운트 되지 않습니다.
  const [isMounted, setIsMounted] = useState(false);

  useEffect(() =&gt; {
    setIsMounted(true);
  }, []);
};</code></pre>
<p>기억하세요. <code>useEffect</code>는 서버에서 실행되지 않으므로 웹사이트의 클라이언트 사이드 버전이 리액트에 의해 완전히 초기화될 때만 상태가 <code>true</code>로 바뀝니다.</p>
<p>마지막으로, SSR과 호환되지 않은 렌더링을 하고자 합니다.</p>
<pre><code class="language-tsx">const Component = () =&gt; {
  // 초기에는 마운트 되지 않습니다.
  const [isMounted, setIsMounted] = useState(false);

  useEffect(() =&gt; {
    setIsMounted(true);
  }, []);

  // SSR 모드에서는 아무것도 렌더링 하지 않습니다.
  if (!isMounted) return null;

  // 클라이언트 모드가 시작될 때 렌더링 합니다.
  return ...
}</code></pre>
<blockquote>
<p><strong>도전 과제</strong></p>
<ol>
<li>이전 예제의 <code>ClientOnlyButton</code>을 다시 작성하여 SSR에서 올바르게 작동하도록 합니다.</li>
<li>평소처럼 프로젝트를 다시 빌드하고 다시 시작합니다.</li>
<li>성능 프로필을 기록합니다. SSR 빨간색 모양으로 되돌아갑니다.</li>
</ol>
</blockquote>
<h3 id="서드파티-라이브러리">서드파티 라이브러리</h3>
<p>모든 외부 종속성이 SSR을 지원하는 것은 아닙니다. 라이브러리는 항상 도박과도 같습니다. 일부 라이브러리의 경우 위의 해결법을 사용하여 SSR을 거부할 수 있습니다. 일부는 번들러에 의해 거부되므로 클라이언트 사이드 자바스크립트가 로드된 후 동적으로 가져와야 합니다. 그중 일부는 프로젝트에서 제거하고 더 SSR 친화적인 라이브러리로 대체해야 합니다.</p>
<p>SSR과 호환되지 않은 라이브러리가 전체 프로젝트의 기본 요소인 경우 특히 문제가 될 수 있습니다. 상태 관리 해결법이나 CSS-in-JS 해결법처럼 말입니다.</p>
<p>예를 들어, 스터디 프로젝트의 어딘가에서 Material UI 아이콘을 사용해 봅시다.</p>
<pre><code class="language-tsx">// 어딘가 예제에 있는 src/App.tsx
import { Star } from &quot;@mui/icons-material&quot;;

function App() {
  // 나머지 코드는 같습니다.
  return (
    &lt;&gt;
      ...
      &lt;Star /&gt;
    &lt;/&gt;
  );
}</code></pre>
<p>다시 빌드하고 시작하면 SSR이 함께 무너진 것을 볼 수 있습니다.</p>
<pre><code class="language-bash">[vite] (ssr) Error when evaluating SSR module @/App: deepmerge is not a function</code></pre>
<p>해결 방법을 재미있게 찾아보세요 😬</p>
<h2 id="정적-사이트-생성ssg">정적 사이트 생성(SSG)</h2>
<p>서버에서 “적절히” 렌더링 되는 페이지가 반드시 필요하고 이를 위해 프런트엔드에서 발생하는 결과를 처리할 준비가 되어 있다고 가정해 봅시다. 예를 들어 멋진 “프로모션” 웹사이트를 구현하고 있다고 가정해 봅시다. 모든 검색 엔진에서 가능한 한 빨리 색인화되어야 하며 링크를 공유할 수 있는 모든 곳에서 공유할 수 있어야 합니다. 이것이 바로 이런 웹사이트의 핵심입니다.</p>
<p>또한 웹사이트의 모든 정보가 “정적”, 즉 사용자가 생성한 콘텐츠가 없고, 고려해야 할 권한이 없으며, 요청당 복잡한 데이터가 생성되지 않는다고 가정해 봅시다. 웹사이트는 제품을 소개하는 몇 개의 페이지와 &#39;이용 약관&#39;과 같은 몇 가지 표준 페이지, 일주일에 한 번 업데이트되는 블로그만 있다고 가정합니다.</p>
<p>이 상황은 케이크도 먹고 떡도 먹을 수 있는 드문 사용 사례입니다. 우리는 이미 서버에서 웹사이트를 사전 렌더링하는 것이 비교적 쉽다는 것을 알고 있습니다. 앱에서 <code>React.renderToString</code>을 호출하기만 하면 됩니다(어느 정도).</p>
<p>그렇다면 여기서 중요한 질문은 빌드 시간 동안, 즉 <code>npm run build</code>를 실행한 직후에 <code>React.renderToString</code>을 실행하지 못하게 하는 이유는 무엇일까요? 이론적으로 어쨌든 적절한 HTML 페이지를 사전 렌더링하여 브라우저에 전송하고 있습니다. 그리고 사전 렌더링 된 콘텐츠는 항상 동일합니다. 예전처럼 사전 렌더링해서 실제 <code>HTML</code> 파일로 저장하면 “적절한” 서버를 보유하는 수고를 덜 수 있을 것입니다. 그렇죠?</p>
<p>정답은, 그렇게 하는 것을 막을 방법이 전혀 없다는 것입니다. 이걸 실행해 보세요.</p>
<pre><code class="language-bash">npm run build:ssg
</code></pre>
<p>먼저 Vite를 사용하여 일반적인 방식으로 웹 사이트를 구축한 다음, 빈 <code>&lt;div id=“root”&gt;&lt;/div&gt;</code>를 <code>renderToString</code>으로 생성된 콘텐츠로 대체하는 매우 근본적인 스크립트(<code>backend/generate-static-pages.ts</code>)를 실행합니다. 서버가 하는 일과 정확히 일치합니다. 이제 더 이상 서버가 필요하지 않습니다.</p>
<p><code>dist</code> 폴더에서 빌드 된 파일을 살펴보세요. 이제 <code>login.html</code>과 <code>settings.html</code>이라는 추가된 두 개의 파일이 보입니다. HTML 파일을 열면 <code>&lt;div id=“root”&gt;</code>가 콘텐츠로 채워져 있는 것을 볼 수 있습니다.</p>
<p>이것은 모든 웹서버에서 시작할 수 있는 “정적” 웹사이트입니다.</p>
<pre><code class="language-bash">npx serve dist</code></pre>
<p>또는 다른 클라이언트 사이드 렌더링 앱과 마찬가지로 거의 모든 곳에 업로드 할 수 있습니다. 이번에는 CSR의 단점이 없고, 모든 검색 엔진이 즉시 제대로 색인을 생성할 수 있으며, 소셜 미디어 공유가 원활하게 작동합니다.</p>
<p>정적 웹사이트는 세 글자로 된 자체 약어까지 있을 정도로 훌륭한 기능입니다(<strong>SSG(정적 사이트 생성)</strong>). 물론 수작업이 필요 없이 자동으로 생성해 주는 프레임워크가 많이 있습니다. <a href="https://nextjs.org/docs/pages/building-your-application/rendering/static-site-generation">Next.js는 SSG를 지원</a>하고, <a href="https://www.gatsbyjs.com/">Gatsby</a>는 여전히 인기가 많고, 많은 사람들이 <a href="https://docusaurus.io/">Docusaurus</a>를 좋아하고, <a href="https://astro.build/">Astro</a>는 최고의 성능을 약속하며, 그 외에도 많은 프레임워크가 있습니다.</p>
<p>SSR에 대해 아직 할 말이 많지만, 여기 소개한 개념은 기초를 다지기 위한 것일 뿐입니다. 하지만, 이 글이 도움이 되었기를 바라며 다음에 다음 웹사이트를 만들 때 SSR로 시작할지 말지 결정해야 할 때 좀 더 자신감을 가질 수 있기를 바랍니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[(번역) 모노레포 인사이트: Nx, Turborepo 그리고 PNPM (4/4 - pnpm 중점적인)]]></title>
            <link>https://velog.io/@tap_kim/translate-monorepo-insights-nx-turborepo-and-pnpm4</link>
            <guid>https://velog.io/@tap_kim/translate-monorepo-insights-nx-turborepo-and-pnpm4</guid>
            <pubDate>Thu, 13 Mar 2025 14:05:13 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>원문: <a href="https://medium.com/ekino-france/monorepo-insights-nx-turborepo-and-pnpm-4-4-96a3fb363cf4">Monorepo Insights: Nx, Turborepo, and PNPM (4/4)</a></p>
</blockquote>
<p>오늘날 최고의 모노레포 솔루션의 장단점을 살펴봅시다.</p>
<p>아래 링크를 통해 전체 시리즈를 살펴보세요.</p>
<ul>
<li><a href="https://velog.io/@tap_kim/translate-monorepo-insights-nx-turborepo-and-pnpm">모노레포 인사이트: Nx, Turborepo 그리고 PNPM (1/4)</a></li>
<li><a href="https://velog.io/@tap_kim/translate-monorepo-insights-nx-turborepo-and-pnpm2">모노레포 인사이트: Nx, Turborepo 그리고 PNPM (2/4)</a></li>
<li><a href="https://velog.io/@tap_kim/translate-monorepo-insights-nx-turborepo-and-pnpm3">모노레포 인사이트: Nx, Turborepo 그리고 PNPM (3/4)</a></li>
<li>모노레포 인사이트: Nx, Turborepo 그리고 PNPM (4/4)(현재 글)</li>
</ul>
<h2 id="소개">소개</h2>
<p>이 글은 <code>Nx</code>, <code>PNPM</code>, <code>Turborepo</code>의 기능, 성능, 프로젝트 적합성을 비교하는 시리즈의 일부입니다.</p>
<p><code>Nx</code>와 <a href="https://medium.com/ekino-france/monorepo-insights-nx-turborepo-and-pnpm-3-4-751384b5a6db"><code>Turborepo</code></a>의 내부 작동 방식과 주요 기능을 살펴본 후, 이제 <code>PNPM</code>에 주목해 보겠습니다. <code>PNPM</code>의 워크스페이스만으로도 우리의 요구 사항을 충족하고 개발 워크플로를 혁신할 수 있을 만큼 강력할까요?</p>
<p>다시 한 번 말씀드리지만, 저희의 궁극적인 목표는 개발을 간소화하고 코드베이스 관리를 강화할 수 있는 도구인 모노레포 챔피언을 선정하는 것입니다.</p>
<p>최고의 모노레포가 빛나길 바랍니다! 우리가 함께 정복할 도전은 다음과 같습니다.</p>
<p>전투는 계속됩니다! 우리가 함께 정복할 도전 과제는 다음과 같습니다.</p>
<ul>
<li><a href="#%ED%98%84%EB%AF%B8%EA%B2%BD%EC%9C%BC%EB%A1%9C-%EB%B3%B4%EB%8A%94-PNPM">현미경으로 보는 PNPM</a></li>
<li><a href="#PNPM">PNPM</a></li>
<li><a href="#PNPM-%EC%9B%8C%ED%81%AC%EC%8A%A4%ED%8E%98%EC%9D%B4%EC%8A%A4">PNPM 워크스페이스</a></li>
<li><a href="#PNPM-%EA%B7%B8%EB%9E%98%ED%94%84">PNPM 그래프</a></li>
<li><a href="#PNPM-vs-NX-vs-Turbo">PNPM vs NX vs Turbo</a></li>
<li><a href="#PNPM%EC%9D%98-%EC%9B%8C%ED%81%AC%EC%8A%A4%ED%8E%98%EC%9D%B4%EC%8A%A4%EB%A1%9C-%EC%B6%A9%EB%B6%84%ED%95%A0%EA%B9%8C%EC%9A%94">PNPM의 워크스페이스로 충분할까요?</a></li>
<li><a href="#PNPM-%EC%9B%8C%ED%81%AC%EC%8A%A4%ED%8E%98%EC%9D%B4%EC%8A%A4-Vite-Vitest-ESLint-%EA%B0%95%EB%A0%A5%ED%95%9C-%EC%A1%B0%ED%95%A9">PNPM 워크스페이스 + Vite + Vitest + ESLint: 강력한 조합?</a></li>
<li><a href="#%EC%BA%90%EC%8B%B1-%EC%A7%88%EB%AC%B8">캐싱 질문</a></li>
<li><a href="#%EA%B8%B0%EC%88%A0%EC%A0%81-%ED%8F%89%EA%B0%80">기술적 평가</a></li>
<li><a href="#%EC%9D%B8%EC%82%AC%EC%9D%B4%ED%8A%B8">인사이트</a></li>
<li><a href="#%EC%8B%A4%EC%A0%9C-%EC%9D%B8%EC%82%AC%EC%9D%B4%ED%8A%B8-%EC%95%BC%EC%83%9D%EC%9D%98-PNPM">실제 인사이트: 야생의 PNPM</a></li>
<li><a href="#%EC%B5%9C%EC%A2%85-%ED%8F%89%EA%B0%80-%EB%AA%A8%EB%85%B8%EB%A0%88%ED%8F%AC-%EA%B0%9C%EB%B0%9C%EC%9D%84-%EC%9C%84%ED%95%9C-%EC%9A%B0%EB%A6%AC%EC%9D%98-%EA%B8%B8">최종 평가: 모노레포 개발을 위한 우리의 길</a></li>
<li><a href="#%EA%B2%B0%EB%A1%A0">결론</a></li>
</ul>
<p>다음 단계가 궁금하신가요? 함께 알아보시죠! 🚀 🌟</p>
<h2 id="현미경으로-보는-pnpm">현미경으로 보는 PNPM</h2>
<h3 id="pnpm">PNPM</h3>
<p><code>pnpm</code>(Performant NPM)은 속도, 효율성 및 디스크 공간 사용량에 대한 새로운 접근 방식을 취함으로써 <code>npm</code> 및 <code>Yarn</code>과 같은 기존 패키지 관리자와 차별화됩니다.</p>
<p>🔳 <code>pnpm</code>은 패키지를 글로벌 CAS(콘텐츠 주소 지정 가능 스토리지)에 저장합니다. CAS 디렉터리를 찾으려면 <a href="https://pnpm.io/cli/store#path"><code>pnpm store path</code></a>.</p>
<p><img src="https://miro.medium.com/v2/resize:fit:720/format:webp/1*5iSeQQnzxjn_9FI9P_S1Lw.png" alt="">
PNPM 로컬 스토어(작성자 이미지)</p>
<p>저장소(CAS)를 방문하면 모든 콘텐츠가 해시 이름으로 레이블이 지정되어 있는 것을 확인할 수 있습니다.</p>
<p><img src="https://miro.medium.com/v2/resize:fit:720/format:webp/1*KLOz9IVYLOflw-XTzCg4Hg.png" alt=""></p>
<p>그리고 해시 파일 중 하나를 열면 종속성의 실제 내용이 표시됩니다.</p>
<p><img src="https://miro.medium.com/v2/resize:fit:720/format:webp/1*AamyQJ2imaxDtf5570_wew.png" alt="">
PNPM 해시 파일 콘텐츠(작성자 이미지)</p>
<p><code>pnpm</code>에서 파일은 파일 이름이 아닌 콘텐츠를 기준으로 저장 및 검색됩니다. 각 파일에는 식별자 역할을 하는 고유한 해시(Git 커밋 해시와 유사)가 할당됩니다. 이 해시는 파일의 콘텐츠를 기반으로 생성되므로 <strong>중복된 파일은 동일한 해시를 갖게 됩니다</strong>.</p>
<p>그런 다음 패키지를 설치할 때 <code>pnpm</code>은 먼저 글로벌 스토어(CAS)에 동일한 해시를 가진 파일이 이미 존재하는지 확인합니다.</p>
<ul>
<li><strong>파일이 존재하는 경우</strong>: <code>pnpm</code>은 프로젝트의 <code>node_modules/.pnpm</code> 폴더에서 저장소(CAS)의 기존 파일에 대한 하드 링크를 생성합니다.</li>
<li><strong>파일이 존재하지 않는 경우</strong>: <code>pnpm</code>이 파일을 다운로드하여 CAS에 저장한 다음 <strong>하드 링크</strong>를 생성합니다.</li>
</ul>
<p><img src="https://miro.medium.com/v2/resize:fit:720/format:webp/0*gq8MnFKY5wGH79cx" alt="">
<a href="https://x.com/HemSays/status/1434921646083563525/photo/1">https://x.com/HemSays/status/1434921646083563525/photo/1</a></p>
<p>🔵 종속성의 각 버전은 물리적으로 저장 폴더(CAS)에 <strong>한 번만</strong> 저장되므로 <strong>단일 소스를 제공하고 상당한 양의 디스크 공간을 절약할 수 있습니다.</strong></p>
<p><img src="https://miro.medium.com/v2/resize:fit:720/format:webp/0*cISe9CtzU1CC_lyn.png" alt="">
<a href="https://blog.logrocket.com/javascript-package-managers-compared/">https://blog.logrocket.com/javascript-package-managers-compared/</a></p>
<p>🔳 하드 링크는 원본 파일과 동일한 <a href="https://en.wikipedia.org/wiki/Inode"><strong>inode</strong></a>(파일의 고유 식별자)를 공유하므로 <strong>디스크의 동일한 데이터 블록을 직접 가리킵니다.</strong></p>
<p>예를 들어 <code>document.txt</code>라는 파일과 <code>report.txt</code>라는 하드 링크가 있는 경우 두 이름 모두 동일하고 정확한 파일 콘텐츠를 가리킵니다. <code>document.txt</code> 또는 <code>report.txt</code> 중 하나를 수정하면 두 데이터 모두 변경됩니다.</p>
<p><img src="https://miro.medium.com/v2/resize:fit:720/format:webp/1*EA2uR1PEsgCKPHX53jphLQ.png" alt="">
<a href="https://www.scaler.com/topics/hard-link-and-soft-link-in-linux/">https://www.scaler.com/topics/hard-link-and-soft-link-in-linux/</a></p>
<p>이 하드 링크 전략은 디스크 공간을 절약할 뿐만 아니라 파일을 복사하는 패키지 관리자에 비해 설치 및 업데이트 속도를 획기적으로 높여줍니다.</p>
<p>🔳 그런 다음 모든 패키지를 <code>node_modules/.pnpm</code>에 하드 링크한 후(<code>CAS -&gt; node_modules/.pnpm</code>) <a href="https://pnpm.io/symlinked-node-modules-structure">심볼릭 링크</a>를 생성하여 중첩 종속성 그래프 구조를 구축합니다.</p>
<pre><code class="language-bash">node_modules
└── .pnpm
    ├── pretty-format@27.5.1
    │   └── node_modules
    │       └── react-is -&gt; ../../react-is@17.0.2  // symlink
    ├── pretty-format@28.1.3
    │   └── node_modules
    │       └── react-is -&gt; ../../react-is@18.3.1 // symlink
    ├── prop-types@15.8.1
    │   └── node_modules
    │       └── react-is -&gt; ../../react-is@16.13.1 // symlink
    ├── rc-util@5.43.0_react-dom@18.2.0_react@18.2.0
    │   └── node_modules
    │       ├── react-is -&gt; ../../react-is@18.3.1  // symlink
    │       ├── react-dom -&gt; ../../react-dom@18.2.0 // symlink
    │       └── react -&gt; ../../react@18.2.0 // symlink
    ├── react-is@16.13.1
    ├── react-is@17.0.2
    ├── react-is@18.3.1
    ├── react-dom@18.2.0
    └── react@18.2.0</code></pre>
<p>심볼릭 링크를 사용하는 이유는 무엇인가요? 세 가지 주요 이유 때문에 필수적입니다.</p>
<p>✔️ <strong>호환성</strong>: <code>Node.js</code>와 많은 도구는 종속성이 <code>node_modules</code> 내에 중첩되기를 기대합니다. 심볼릭 링크는 이러한 중첩 구조의 착각을 불러일으키는 동시에 효율성을 위해 하드 링크를 활용합니다.</p>
<p>✔️ <strong>유연성</strong>: 심볼릭 링크를 사용하면 여러 패키지에 동일한 종속성의 <strong>다른 버전이 필요할 수 있는</strong> 복잡한 종속성 시나리오를 <code>pnpm</code>에서 처리할 수 있습니다.</p>
<p>✔️ <strong>효율성</strong>: 심링크는 <strong>파일을 복제하지 않고</strong>, 필요한 중첩 구조를 생성하여 디스크 사용량을 낮게 유지합니다.</p>
<p>🔳 패키지에 <strong>피어 종속성</strong>가 있는 경우 <code>pnpm</code>은 <strong>종속성 그래프에서 더 위</strong>에 설치된 패키지에서 이러한 종속성이 해결되도록 합니다. 다음 예시를 살펴보겠습니다.</p>
<pre><code class="language-bash">node_modules
└── .pnpm
    ├── pretty-format@27.5.1
    │   └── node_modules
    │       └── react-is -&gt; ../../react-is@17.0.2
    ├── pretty-format@28.1.3
    │   └── node_modules
    │       └── react-is -&gt; ../../react-is@18.3.1
    ├── prop-types@15.8.1
    │   └── node_modules
    │       └── react-is -&gt; ../../react-is@16.13.1
    ├── rc-util@5.43.0_react-dom@18.2.0_react@18.2.0
    │   └── node_modules
    │       ├── react-is -&gt; ../../react-is@18.3.1
    │       ├── react-dom -&gt; ../../react-dom@18.2.0
    │       └── react -&gt; ../../react@18.2.0
    ├── react-is@16.13.1
    ├── react-is@17.0.2
    ├── react-is@18.3.1
    ├── react-dom@18.2.0
    └── react@18.2.0</code></pre>
<p><code>pnpm</code>은 설치를 복제하는 대신 올바른 버전의 종속성을 가리키는 <strong>심볼릭 링크</strong>를 생성하므로 시간, 대역폭 및 디스크 공간을 절약할 수 있습니다.</p>
<p>기존 <code>node_modules</code> 설정(예: <code>npm</code> 또는 <code>Yarn classic</code>)에서 여러 패키지가 동일한 종속성의 다른 버전에 의존하는 경우 해당 종속성은 <code>node_modules</code>에 중복됩니다.</p>
<pre><code class="language-bash">node_modules
├── pretty-format@27.5.1
│   └── node_modules
│       └── react-is@17.0.2
├── pretty-format@28.1.3
│   └── node_modules
│       └── react-is@18.3.1 (first one)
├── prop-types@15.8.1
│   └── node_modules
│       └── react-is@16.13.1
└── rc-util@5.43.0
    └── node_modules
        ├── react-is@18.3.1 (second one)
├── react-dom@18.2.0
        └── react@18.2.0</code></pre>
<p>보시다시피 <code>react-is@18.3.1</code> 은 여러 번 중복되어 불필요한 디스크 공간을 차지합니다.</p>
<p>🔳 <code>node_modules/.pnpm</code> 폴더 내 각 종속성 폴더 이름에는 다음과 같은 세부 정보가 인코딩됩니다.</p>
<ul>
<li><strong>종속성 이름</strong>: 예: <code>rc-picker</code></li>
<li><strong>종속성 버전</strong>: 예: <code>4.6.9</code></li>
<li><strong>피어 종속성</strong>: 패키지가 의존하는 피어 종속성의 이름 및 버전(예: <code>rc-picker@4.6.9_date-fns@2.30.0_dayjs@1.11.11_luxon@3.4.4_moment@2.30.1_react-dom@18.2.0_react@18.2.0__react@18.2.0</code>).</li>
</ul>
<p><img src="https://miro.medium.com/v2/resize:fit:720/format:webp/1*O96lDXimTZdQsJdv65JpOQ.png" alt="">
PNPM 종속성 메타데이터(작성자 이미지)</p>
<pre><code class="language-json">...
// https://github.com/react-component/picker/blob/master/package.json#L85

&quot;peerDependencies&quot;: {
  &quot;date-fns&quot;: &quot;&gt;= 2.x&quot;,
      &quot;dayjs&quot;: &quot;&gt;= 1.x&quot;,
      &quot;luxon&quot;: &quot;&gt;= 3.x&quot;,
      &quot;moment&quot;: &quot;&gt;= 2.x&quot;,
      &quot;react&quot;: &quot;&gt;=16.9.0&quot;,
      &quot;react-dom&quot;: &quot;&gt;=16.9.0&quot;
},
&quot;peerDependenciesMeta&quot;: {
  &quot;date-fns&quot;: {
    &quot;optional&quot;: true
  },
  &quot;dayjs&quot;: {
    &quot;optional&quot;: true
  },
  &quot;luxon&quot;: {
    &quot;optional&quot;: true
  },
  &quot;moment&quot;: {
    &quot;optional&quot;: true
  }
}
...
</code></pre>
<p>이 세심한 구성은 <code>pnpm</code>이 종속성 호환성을 보장하고 잠재적인 충돌을 해결하는 데 도움이 됩니다.</p>
<p>🔳 이 단순화된 스키마는 <code>pnpm</code>이 종속성을 관리하는 방법을 요약한 것입니다.</p>
<pre><code class="language-bash">PNPM Dependency Management - Schema

Central Store (CAS)
│
├── package1@version1
│   ├── package.json
│   ├── index.js
│   └── ... (other files)
├── package2@version2
│   ├── package.json
│   └── ...
└── ... (other packages)

Project Structure (node_modules/.pnpm)
│
├── package1@version1 (Hard Link)
│   └── node_modules
│       ├── dependency1 -&gt; ../../../dependency1@versionX (Symlink)
│       ├── dependency2 -&gt; ../../../dependency2@versionY (Symlink)
│       └── ...
├── package2@version2 (Hard Link)
│   └── node_modules
│       └── ...
└── ...</code></pre>
<ul>
<li><code>.pnpm</code> 디렉터리 내의 각 패키지의 메인 폴더는 글로벌 스토어(CAS)에서 하드 링크됩니다.</li>
<li>각 패키지의 <code>node_modules</code> 폴더 내에서 심볼릭 링크는 피어 종속성의 올바른 버전을 가리키는 데 사용됩니다.</li>
</ul>
<p>🔳 다음 표는 <code>pnpm</code>과 경쟁사의 기능을 비교한 것입니다.</p>
<p><img src="https://miro.medium.com/v2/resize:fit:720/format:webp/1*Vo6JbN0m_xbPa5VWTokXPQ.png" alt="">
<a href="https://pnpm.io/feature-comparison">https://pnpm.io/feature-comparison</a></p>
<p>🔳 아래 벤치마크는 시간 성능과 다양한 상황에서 경쟁사 대비 <code>pnpm</code>의 순위를 보여줍니다.</p>
<p><img src="https://miro.medium.com/v2/resize:fit:720/format:webp/1*6y0LK4I_j4K_11UJTDv9RA.png" alt="">
<a href="https://pnpm.io/benchmarks#lots-of-files">https://pnpm.io/benchmarks#lots-of-files</a></p>
<p><img src="https://miro.medium.com/v2/resize:fit:720/format:webp/1*6y0LK4I_j4K_11UJTDv9RA.png" alt="">
<a href="https://pnpm.io/benchmarks#lots-of-files">https://pnpm.io/benchmarks#lots-of-files</a></p>
<p>단일 프로젝트를 관리할 때 <code>pnpm</code>의 효율성은 잘 알려져 있습니다. 하지만 서로 연결된 여러 프로젝트를 처리할 때 어떻게 확장할 수 있을까요? <code>pnpm</code> 워크스페이스을 자세히 살펴보고 여러 프로젝트 종속성을 동일한 효율로 관리할 수 있는지 알아봅시다.</p>
<h2 id="pnpm-워크스페이스">PNPM 워크스페이스</h2>
<p>🔳 워크스페이스의 루트에는 <code>pnpm-workspace.yaml</code> 파일이 있어야 합니다.</p>
<p>다음은 <code>pnpm-workspace.yaml</code> 파일의 내용 예시입니다.</p>
<pre><code class="language-bash">packages:
  - &#39;packages/*&#39;

catalog:
  &#39;@babel/parser&#39;: ^7.24.7
  &#39;@babel/types&#39;: ^7.24.7
  &#39;estree-walker&#39;: ^2.0.2
  &#39;magic-string&#39;: ^0.30.10
  &#39;source-map-js&#39;: ^1.2.0
  &#39;vite&#39;: ^5.3.3</code></pre>
<p><a href="https://pnpm.io/workspaces#usage-examples">여기</a>를 살펴보시면 워크스페이스 사용의 실제 예시 코드를 확인할 수 있습니다.</p>
<p>🔳 워크스페이스의 루트에 <a href="https://pnpm.io/npmrc"><code>.npmrc</code></a>가 있을 수도 있습니다. <code>.npmrc</code> 파일에 워크스페이스에 대한 많은 구성 옵션을 추가할 수 있습니다.</p>
<p><img src="https://miro.medium.com/v2/resize:fit:720/format:webp/1*D7k_0YcWYWiqEycfUZTDGQ.png" alt="">
pnpm 워크스페이스 구성(작성자 이미지)</p>
<p>다음은 <a href="https://medium.com/ekino-france/monorepo-insights-nx-turborepo-and-pnpm-4-4-96a3fb363cf4#:~:text=content%20of%20a-,.npmrc,-file%3A"><code>.npmrc</code></a> 파일의 내용 예시입니다.</p>
<pre><code class="language-bash"># https://github.com/withastro/astro/blob/main/.npmrc
# 중요! 새 버전이 레지스트리에 있어도 `astro`를 설치하지 마십시오.
prefer-workspace-packages=true
link-workspace-packages=true
save-workspace-protocol=false # 이렇게 하면 예제에 `workspace:` 접두사가 붙지 않습니다.</code></pre>
<p>🔳 <code>pnpm</code> 워크스페이스 내에서 패키지를 참조하려면 두 가지 주요 옵션이 있습니다.</p>
<p>✔️ 개별 <code>package.json</code> 파일 내에 워크스페이스 패키지의 별칭을 생성한 다음 다른 패키지의 <code>package.json</code> 파일에서 이 별칭을 종속성으로 참조할 수 있습니다.</p>
<p>다음은 <a href="https://medium.com/ekino-france/monorepo-insights-nx-turborepo-and-pnpm-4-4-96a3fb363cf4#:~:text=life%20example%20from-,MUI,-%3A%20we%20define%20two">MUI</a>의 실제 예시입니다. 두 개의 횡단 패키지인 <code>mui/system</code>과 <code>mui/types</code>를 정의합니다.</p>
<pre><code class="language-json">// https://github.com/mui/material-ui/blob/next/packages/mui-system/package.json#L2
{
  &quot;name&quot;: &quot;@mui/system&quot;,
  &quot;version&quot;: &quot;6.0.0-beta.1&quot;,
  &quot;private&quot;: false,
  &quot;author&quot;: &quot;MUI Team&quot;,
...

// https://github.com/mui/material-ui/blob/next/packages/mui-types/package.json#L2
{
  &quot;name&quot;: &quot;@mui/types&quot;,
  &quot;version&quot;: &quot;7.2.14&quot;,
  &quot;private&quot;: false,
  &quot;author&quot;: &quot;MUI Team&quot;,</code></pre>
<p>이 패키지는 <a href="https://github.com/mui/material-ui/blob/next/packages/mui-material/package.json#L45">다음</a>과 같이 참조됩니다.</p>
<pre><code class="language-json">....
&quot;dependencies&quot;: {
    &quot;@mui/core-downloads-tracker&quot;: &quot;workspace:^&quot;,
    &quot;@mui/system&quot;: &quot;workspace:*&quot;,
    &quot;@mui/types&quot;: &quot;workspace:^&quot;,
    &quot;@mui/utils&quot;: &quot;workspace:*&quot;,
...
  },
 &quot;devDependencies&quot;: {
    &quot;@mui/internal-babel-macros&quot;: &quot;workspace:^&quot;,
    &quot;@mui/internal-test-utils&quot;: &quot;workspace:^&quot;,
...</code></pre>
<p>별칭으로 작성된 종속성의 특정 버전 또는 범위를 지정할 수도 있습니다.</p>
<pre><code class="language-json">{
  &quot;dependencies&quot;: {
    &quot;foo&quot;: &quot;workspace:*&quot;,
    &quot;bar&quot;: &quot;workspace:~&quot;,
    &quot;qar&quot;: &quot;workspace:^&quot;,
    &quot;zoo&quot;: &quot;workspace:^1.5.0&quot; // 특정 버전 지정
  }
}</code></pre>
<p>✔️ <strong>상대 경로</strong>: 모노레포 내에서 상대 경로를 사용하여 워크스페이스 패키지를 참조할 수도 있습니다. 예를 들어, <code>“foo”: “workspace:../foo&quot;</code>는 종속성을 선언하는 패키지와 상대적인 형제 디렉터리에 있는 <code>foo</code> 패키지를 참조합니다.</p>
<p>🔳 작업 영역 패키지를 게시할 준비가 되면 <code>pnpm</code>은 로컬 <code>workspace:</code> 의존성 참조를 표준 <a href="https://medium.com/ekino-france/monorepo-insights-nx-turborepo-and-pnpm-4-4-96a3fb363cf4#:~:text=references%20into%20standard-,SemVer,-(Semantic%20Versioning)%20ranges">SemVer</a>(Semantic Versioning) 범위로 자동 변환합니다. 이렇게 하면 게시된 패키지를 다른 프로젝트에서 원활하게 사용할 수 있으며, 심지어 <code>pnpm</code>을 사용하지 않는 프로젝트에서도 사용할 수 있습니다.</p>
<p>예를 들어, 다음과 종속성 같은 종속성이 있습니다.</p>
<pre><code class="language-json">{
  &quot;dependencies&quot;: {
    &quot;foo&quot;: &quot;workspace:*&quot;, // 워크스페이스의 모든 ‘foo’ 버전
    &quot;bar&quot;: &quot;workspace:~&quot;, // ~1.5.0(1.5.x에 해당)
    &quot;qar&quot;: &quot;workspace:^&quot;, // ^1.5.0(1.x.x와 호환)
    &quot;zoo&quot;: &quot;workspace:^1.5.0&quot; // 위와 같음
  }
}</code></pre>
<p>위 종속성은 아래와 같이 변환됩니다.</p>
<pre><code class="language-json">{
  &quot;dependencies&quot;: {
    &quot;foo&quot;: &quot;1.5.0&quot;, // 정확한 버전
    &quot;bar&quot;: &quot;~1.5.0&quot;,
    &quot;qar&quot;: &quot;^1.5.0&quot;,
    &quot;zoo&quot;: &quot;^1.5.0&quot;
  }
}</code></pre>
<p>이제 게시된 패키지를 설치하는 모든 사람은 워크스페이스가 설정되어 있지 않더라도 올바른 버전의 종속성을 얻게 됩니다.</p>
<p>🔴 <code>pnpm</code> 작업 영역에는 내장된 버전 관리 기능(예: <a href="https://lerna.js.org/">lerna</a> 또는 <a href="https://docs.npmjs.com/cli/v7/using-npm/workspaces">npm</a>)이 포함되어 있지 않지만 <a href="https://github.com/changesets/changesets">Changesets</a> 및 <a href="https://rushjs.io/">Rush</a>와 같은 기존 도구와 쉽게 통합할 수 있습니다.</p>
<p>⚫ <code>pnpm</code> 작업 영역의 <strong>순환 종속성</strong>으로 인해 스크립트 실행 순서가 예측할 수 없게 될 수 있습니다. 설치 중에 이러한 순환이 감지되면 <code>pnpm</code>에서 경고를 표시합니다. 문제가 있는 패키지는 <code>pnpm</code>에 의해 식별될 수도 있습니다. 이 경고가 발생하면 관련 <code>package.json</code> 파일에서 <code>dependencies</code>, <code>optionalDependencies</code> 및 <code>devDependencies</code>에 선언된 종속성을 검사해야 합니다.</p>
<p>다행히도 우리는 순환 종속성에 대해 이야기하고 있습니다. 다음 섹션에서는 종속성 그래프, 유향 비순환 정렬 등 <code>pnpm</code>에 대해 자세히 살펴보겠습니다.</p>
<h3 id="pnpm-그래프">PNPM 그래프</h3>
<p>🔳 <a href="https://github.com/pnpm/pnpm/tree/main"><code>pnpm</code></a>은 그래프 관리를 위해 다음과 같은 내부 라이브러리를 사용합니다.</p>
<ul>
<li><a href="https://github.com/ahaoboy/pnpm/tree/main/deps/graph-builder"><code>graph-builder</code></a>: 이 라이브러리는 <code>pnpm-lock.yaml</code> 파일에서 종속성 그래프를 구성하는 역할을 담당합니다.</li>
<li><a href="https://github.com/pnpm/pnpm/tree/main/deps/graph-sequencer"><code>graph-sequencer</code></a>: 이 라이브러리는 그래프에서 패키지를 정렬하기 위해 유향 비순환 정렬 알고리즘을 구현합니다.</li>
</ul>
<p>🔳 PNPM은 주로 DAG를 사용하여 패키지 간의 종속성 관계를 모델링합니다. 이 그래프는 <code>pnpm-lock.yaml</code> 파일의 <code>dependencies</code>, <code>devDependencies</code> 및 <code>optionalDependencies</code> 필드를 기반으로 구성됩니다(<code>const currentPackages = currentLockfile?.packages ?? {}</code>).</p>
<pre><code class="language-ts">// https://github.com/ahaoboy/pnpm/blob/main/deps/graph-builder/src/lockfileToDepGraph.ts#L91C1-L108C1
export async function lockfileToDepGraph (
    lockfile: Lockfile,
    currentLockfile: Lockfile | null,
    opts: LockfileToDepGraphOptions
): Promise&lt;LockfileToDepGraphResult&gt; {
  const currentPackages = currentLockfile?.packages ?? {}
  const graph: DependenciesGraph = {}
  const directDependenciesByImporterId: DirectDependenciesByImporterId = {}
  if (lockfile.packages != null) {
    const pkgSnapshotByLocation: Record&lt;string, PackageSnapshot&gt; = {}
    await Promise.all(
        Object.entries(lockfile.packages).map(async ([depPath, pkgSnapshot]) =&gt; {
          if (opts.skipped.has(depPath)) return
          // TODO: 최적화: 이 정보는 이미 pkgSnapshotToResolution()에서 반환할 수 있습니다.
          const { name: pkgName, version: pkgVersion } = nameVerFromPkgSnapshot(depPath, pkgSnapshot)
          const modules = path.join(opts.virtualStoreDir, dp.depPathToFilename(depPath), &#39;node_modules&#39;)
          const packageId = packageIdFromSnapshot(depPath, pkgSnapshot, opts.registries)

        ...
          const dir = path.join(modules, pkgName)
          const depIsPresent = !(&#39;directory&#39; in pkgSnapshot.resolution &amp;&amp; pkgSnapshot.resolution.directory != null) &amp;&amp;
              currentPackages[depPath] &amp;&amp; equals(currentPackages[depPath].dependencies, lockfile.packages![depPath].dependencies)
          let dirExists: boolean | undefined
          if (
              depIsPresent &amp;&amp; isEmpty(currentPackages[depPath].optionalDependencies ?? {}) &amp;&amp;
              isEmpty(lockfile.packages![depPath].optionalDependencies ?? {})
        ) {
            dirExists = await pathExists(dir)
            if (dirExists) {
              return
            }

            brokenModulesLogger.debug({
              missing: dir,
            })
          }
          let fetchResponse!: Partial&lt;FetchResponse&gt;
          if (depIsPresent &amp;&amp; equals(currentPackages[depPath].optionalDependencies, lockfile.packages![depPath].optionalDependencies)) {
            if (dirExists ?? await pathExists(dir)) {
              fetchResponse = {}
            } else {
              brokenModulesLogger.debug({
                missing: dir,
              })
            }
          }
...</code></pre>
<p><code>pnpm-lock.yaml</code>의 예제를 살펴봅시다.</p>
<pre><code class="language-yaml">packages:
  &quot;@adobe/css-tools@4.4.0&quot;:
    resolution:
      {
        integrity: sha512-Ff9+ksdQQB3rMncgqDK78uLznstjyfIf2Arnh22pW8kBpLs6rpKDwgnZT46hin5Hl1WzazzK64DOrhSwYpS7bQ==,
      }

  &quot;@ampproject/remapping@2.3.0&quot;:
    resolution:
      {
        integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==,
      }
    engines: { node: &quot;&gt;=6.0.0&quot; }

  &quot;@ant-design/colors@7.1.0&quot;:
    resolution:
      {
        integrity: sha512-MMoDGWn1y9LdQJQSHiCC20x3uZ3CwQnv9QMz6pCmJOrqdgM9YxsoVVY0wtrdXbmfSgnV0KNk6zi09NAhMR2jvg==,
      }

  &quot;@ant-design/cssinjs@1.21.0&quot;:
    resolution:
      {
        integrity: sha512-gIilraPl+9EoKdYxnupxjHB/Q6IHNRjEXszKbDxZdsgv4sAZ9pjkCq8yanDWNvyfjp4leir2OVAJm0vxwKK8YA==,
      }
    peerDependencies:
      react: &quot;&gt;=16.0.0&quot;
      react-dom: &quot;&gt;=16.0.0&quot;
---
&quot;@jest/reporters@28.1.3&quot;:
  resolution:
    {
      integrity: sha512-JuAy7wkxQZVNU/V6g9xKzCGC5LVXx9FDcABKsSXp5MiKPEE2144a/vXTEDoyzjUpZKfVwp08Wqg5A4WfTMAzjg==,
    }
  engines: { node: ^12.13.0 || ^14.15.0 || ^16.10.0 || &gt;=17.0.0 }
  peerDependencies:
    node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0
  peerDependenciesMeta:
    node-notifier:
      optional: true

&quot;@jest/reporters@29.7.0&quot;:
  resolution:
    {
      integrity: sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==,
    }
  engines: { node: ^14.15.0 || ^16.10.0 || &gt;=18.0.0 }
  peerDependencies:
    node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0
  peerDependenciesMeta:
    node-notifier:
      optional: true

---
devDependencies:
  &quot;@babel/core&quot;:
    specifier: &quot;=7.18.6&quot;
    version: 7.18.6
  &quot;@babel/eslint-parser&quot;:
    specifier: &quot;=7.24.6&quot;
    version: 7.24.6(@babel/core@7.18.6)(eslint@9.0.0)
  &quot;@babel/preset-env&quot;:
    specifier: &quot;=7.23.6&quot;
    version: 7.23.6(@babel/core@7.18.6)
  babel-jest:
    specifier: &quot;=29.7.0&quot;
    version: 29.7.0(@babel/core@7.18.6)</code></pre>
<p>🔳 <a href="https://github.com/pnpm/pnpm/blob/main/deps/graph-sequencer/src/index.ts#L99"><code>findCycle</code></a> 함수는 방향이 지정된 그래프에서 주기를 감지하는 고전적인 알고리즘입니다. 이 함수는 큐 기반 접근 방식(BFS, Breadth-First Search)을 사용하여 주어진 노드(<code>startNode</code>)에서 시작하여 그래프를 탐색합니다. 탐색 중에 <code>startNode</code>를 다시 만나면 사이클이 존재한다는 뜻입니다.</p>
<pre><code class="language-ts">// https://github.com/pnpm/pnpm/blob/main/deps/graph-sequencer/src/index.ts#L99
function findCycle (startNode: T): T[] {
  const queue: Array&lt;[T, T[]]&gt; = [[startNode, [startNode]]]
  const cycleVisited = new Set&lt;T&gt;()
  const cycles: T[][] = []

  while (queue.length) {
    const [id, cycle] = queue.shift()!
    for (const to of graph.get(id)!) {
      if (to === startNode) {
        cycleVisited.add(to)
        cycles.push([...cycle])
        continue
      }
      if (visited.has(to) || cycleVisited.has(to)) {
        continue
      }
      cycleVisited.add(to)
      queue.push([to, [...cycle, to]])
    }
  }

  if (!cycles.length) {
    return []
  }

  cycles.sort((a, b) =&gt; b.length - a.length)
  return cycles[0]
}

...
// https://github.com/pnpm/pnpm/blob/main/deps/graph-sequencer/src/index.ts#L66
const cycleNodes: T[] = []
for (const node of nodes) {
  const cycle = findCycle(node)
  if (cycle.length) {
    cycles.push(cycle)
    cycle.forEach(removeNode)
    cycleNodes.push(...cycle)

    if (cycle.length &gt; 1) {
      safe = false
    }
  }
}
  chunks.push(cycleNodes)
}</code></pre>
<p>🔳 종속성 그래프가 구성되면 <code>pnpm</code>은 위상 정렬 알고리즘을 사용하여 패키지를 처리할 올바른 순서를 결정합니다. 이렇게 하면 패키지의 종속성이 항상 패키지 자체보다 먼저 처리되도록 보장합니다.</p>
<pre><code class="language-ts">/**
 * 노드 제한을 지원하면서 그래프에서 유향 비순환 정렬을 수행합니다.
 *
 * @param {Graph&lt;T&gt;} graph - 키는 노드이고 값은 간선의 가장자리인 맵으로 표현된 그래프입니다.
 * @param {T[]} includedNodes - 정렬 프로세스에 포함해야 하는 노드의 배열입니다. 다른 노드는 무시됩니다.
 * @returns {Result&lt;T&gt;} safe, chunk, cycles를 포함한 정렬 결과가 포함된 객체입니다.
 */
export function graphSequencer&lt;T&gt; (graph: Graph&lt;T&gt;, includedNodes: T[] = [...graph.keys()]): Result&lt;T&gt; {
  // 모든 노드에 대해 빈 배열로 reverseGraph를 초기화합니다.
  const reverseGraph = new Map&lt;T, T[]&gt;()
  for (const key of graph.keys()) {
    reverseGraph.set(key, [])
  }

...</code></pre>
<p>🔴 <code>pnpm</code>의 주요 초점은 효율적인 종속성 관리에 있으며, DAG와 유향 비순환 정렬을 사용하여 패키지 종속성을 올바르게 설치하고 해결할 수 있습니다. 그러나 작업 실행(<code>package.json</code>에 정의된 스크립트)과 관련하여 <code>pnpm</code>은 본질적으로 엄격한 유향 비순환 순서를 강요하거나 <code>Nx</code> 또는 <code>Turborepo</code>와 같은 도구와 같은 캐싱 메커니즘을 제공하지 않습니다.</p>
<p>✅ <code>pnpm</code>에는<code>Nx</code>나 <code>Turborepo</code>와 같은 작업 오케스트레이션 시스템이 내장되어 있지는 않지만, 워크스페이스의 여러 패키지에서 작업을 쉽게 실행할 수 있는 여러 명령어와 옵션을 제공합니다.</p>
<ul>
<li><a href="https://pnpm.io/cli/run#--recursive--r">https://pnpm.io/cli/run#--recursive--r</a></li>
<li><a href="https://pnpm.io/cli/run#--parallel">https://pnpm.io/cli/run#--parallel</a></li>
</ul>
<p>예시: 병렬 빌드 및 미리보기 스크립트</p>
<pre><code class="language-json">&quot;scripts&quot;: {
  &quot;build&quot;: &quot;pnpm  --parallel --filter \&quot;./**\&quot; build&quot;,
  &quot;preview&quot;: &quot;pnpm  --parallel --filter \&quot;./**\&quot; preview&quot;
},</code></pre>
<p>이 예에서는 <code>build</code> 및 <code>preview</code> 스크립트가 워크스페이스 내의 모든 패키지에 걸쳐 병렬로 실행됩니다.</p>
<h2 id="pnpm-vs-nx-vs-turbo">PNPM vs NX vs Turbo</h2>
<p>다음 표는 <code>pnpm</code>, <code>Nx</code> 및 <code>Turborepo</code>를 구분하는 기능에 대한 간략한 개요를 제공합니다.</p>
<p><img src="https://miro.medium.com/v2/resize:fit:720/format:webp/1*pwLa6Nz3LpvNdnh1j29mIw.png" alt="">
PNPM vs NX vs Turbo(작성자 이미지)</p>
<p>간단히 말해 다음과 같이 말할 수 있습니다.</p>
<ul>
<li><strong>Turborepo = PNPM 워크스페이스 + 빌드 최적화</strong></li>
<li><strong>NX = PNPM 워크스페이스 + 빌드 최적화 + 작업 오케스트레이션 + 추가 기능</strong></li>
</ul>
<p>이러한 인사이트를 바탕으로, 저처럼 PNPM 워크스페이스와 <a href="https://vitejs.dev/"><code>Vite</code></a>, <a href="https://vitest.dev/"><code>Vitest</code></a>, <a href="https://eslint.org/docs/latest/use/command-line-interface#caching"><code>ESLint</code></a>와 같은 성능 좋은 도구를 결합하면 효율적인 모노레포 개발에 충분한지 궁금하실 수도 있습니다. <code>Nx</code>나 <code>Turborepo</code>와 같은 복잡한 모노레포 전용 도구에 의존하지 않고도 원활한 개발자 경험(DX)을 구현할 수 있을까요? 다음 섹션에서 이 질문에 대해 자세히 알아보겠습니다.</p>
<h2 id="pnpm의-워크스페이스로-충분할까요">PNPM의 워크스페이스로 충분할까요?</h2>
<h3 id="pnpm-워크스페이스--vite--vitest--eslint-강력한-조합">PNPM 워크스페이스 + <a href="https://vitejs.dev/">Vite</a> + <a href="https://vitest.dev/">Vitest</a> + <a href="https://eslint.org/docs/latest/use/command-line-interface#caching"><code>ESLint</code></a>: 강력한 조합?</h3>
<p>🔳 이 설정의 잠재력을 이해하려면 <code>Nx</code> 및 <code>Turborepo</code>와 같은 모노레포 도구가 제공하는 핵심 이점을 기억해 두는 것이 좋습니다.</p>
<ul>
<li><strong>속도</strong>: 지능적인 작업 오케스트레이션 및 캐싱을 통해 빌드 및 테스트 실행을 최적화합니다.</li>
<li><strong>작업 캐싱</strong>: 빌드 아티팩트를 저장하여 후속 빌드에서 중복 작업을 방지합니다.</li>
<li><strong>증분 빌드</strong>: 변경 사항의 영향을 받는 코드베이스 부분만 다시 빌드합니다.</li>
<li><strong>사용 편의성</strong>: 일반적인 모노레포 작업을 위한 간소화된 설정 및 구성.</li>
</ul>
<p>🔳 이제 <code>pnpm Workspace + Vite + Vitest + ESLint</code> 스택의 가치 제안을 분석해 보겠습니다.</p>
<p>✔️ <strong><code>pnpm</code> Workspace</strong></p>
<ul>
<li><strong>효율적인 종속성 관리</strong>: <code>pnpm</code>의 핵심 강점은 콘텐츠 주소 지정이 가능한 파일 시스템을 사용하여 효율적인 종속성 해결 및 저장에 있습니다. 따라서 설치 속도가 빨라지고, 디스크 사용 공간이 줄어들며, 안정성이 향상됩니다.</li>
<li><strong>워크스페이스 기능</strong>: <code>Nx</code>나 <code>Turborepo</code>만큼 포괄적이지는 않지만 <code>pnpm</code> 워크스페이스는 공유 종속성, 프로젝트 연결, 패키지 간 손쉬운 스크립트 실행과 같은 기본적인 모노레포 기능을 제공합니다.</li>
</ul>
<p>✔️ <strong>Vite</strong></p>
<ul>
<li><strong>초고속 개발 서버</strong>: Vite의 개발 서버는 네이티브 ES 모듈을 활용하여 거의 즉각적인 핫 모듈 리로딩(HMR)을 지원하므로 개발자의 생산성이 향상됩니다.</li>
<li><strong>최적화된 프로덕션 빌드</strong>: Vite의 프로덕션 빌드는 뛰어난 성능으로 유명한 고효율 번들러인 Rollup으로 구동됩니다.</li>
<li><strong>다양한 기능</strong>: Vite는 CSS 전처리기 지원, 모듈 해상도, 리액트 및 Vue.js와 같은 인기 프레임워크와의 통합 등 다양한 기능을 제공합니다.</li>
</ul>
<p>✔️ <strong>Vitest</strong></p>
<ul>
<li><strong>내장된 모노레포 지원</strong>: Vite용으로 설계된 테스트 프레임워크인 Vitest는 모노레포를 기본적으로 지원하여 패키지 전반에서 테스트 구성 및 실행을 간소화합니다.</li>
<li><strong>빠르고 효율적입니다</strong>: Vitest는 Vite의 캐싱 및 모듈 해상도 기능을 활용하여 테스트를 빠르게 실행하고 원활한 개발자 경험을 제공합니다.</li>
</ul>
<p>✔️ <strong>캐싱이 포함된 ESLint</strong></p>
<ul>
<li><strong>향상된 린팅 성능</strong>: 널리 사용되는 자바스크립트 린터인 ESLint를 캐싱으로 구성하여 변경되지 않은 파일을 다시 분석하지 않도록 하여 린팅 프로세스의 속도를 높일 수 있습니다.</li>
<li><strong>모노레포-와이드 린팅</strong>: <code>pnpm</code> 워크스페이스을 사용하면 모노레포 전체에서 ESLint 구성 및 규칙을 쉽게 공유하여 일관된 코드 품질을 보장할 수 있습니다.</li>
</ul>
<p>🔳 다음은 <code>pnpm Workspace + Vite + Vitest + ESLint(캐싱 포함)</code> 조합, <code>Nx</code> 및 <code>Turborepo</code> 간의 주요 차이점을 요약한 비교표입니다.</p>
<p><img src="https://miro.medium.com/v2/resize:fit:720/format:webp/1*P6cTvDs8wrK9RMzEP7creA.png" alt="">
마법의 조합 vs Nx vs Turborepo (작성자 이미지)</p>
<p><strong>Key</strong></p>
<ul>
<li>✅: 강력한 지원 또는 기능 제공.</li>
<li>🟡: 부분적 또는 제한적 지원.</li>
<li>🟢: 배우기 쉽고 사용하기 쉬움.</li>
<li>🟡: 중간 정도의 학습 곡선.</li>
<li>❌: 기능이 없거나 상당한 사용자 지정 구성이 필요합니다.</li>
</ul>
<p>🔳 비교 표에 비추어 몇 가지 미리 생각해 보세요.</p>
<ul>
<li>오버헤드가 적은 익숙한 스택을 선호하고 개별 패키지의 효율성을 우선시한다면 <code>PNPM + Vite</code>가 좋은 선택이 될 수 있습니다.</li>
<li>다양한 기본 제공 기능을 갖춘 포괄적인 모노레포 프레임워크가 필요하다면 <code>Nx</code>가 강력한 옵션입니다.</li>
<li>빌드 성능이 절대적인 최우선 순위라면 Vite의 속도와 캐싱 가능성에도 불구하고 <code>Turborepo</code>가 <code>PNPM + Vite</code>에 비해 몇 가지 이점을 제공할 수 있습니다.</li>
</ul>
<p>지금까지의 결과는 희망적이지만 아직 끝나지 않았습니다! 팀 개발과 지속적 통합을 위해 공유 캐시를 설정하여 <code>PNPM + Vite</code>의 잠재력을 최대한 발휘할 수 있는 우회 방법을 함께 살펴보세요.</p>
<h3 id="캐싱-질문">캐싱 질문</h3>
<p>실제로 로컬 캐싱은 <a href="https://vitejs.dev/guide/dep-pre-bundling.html#caching"><code>Vite</code></a>, <a href="https://vitest.dev/config/#cache"><code>Vitest</code></a>, <a href="https://eslint.org/docs/latest/use/command-line-interface#caching"><code>ESLint</code></a>와 같은 도구에 내장된 캐싱 메커니즘으로 인해 <code>PNPM + Vite</code>에서는 문제가 되지 않는 경우가 많습니다. 따라서 <code>Nx</code> 또는 <code>Turborepo</code>에서 제공하는 추가 로컬 캐싱은 불필요할 수 있습니다. 모노레포 성능에 대한 진정한 과제는 PNPM의 고유한 구조로 인해 전략적인 캐시 관리가 필요한 <strong>CI 빌드</strong>를 최적화하는 데 있습니다.</p>
<p>CI 빌드 캐시에 대한 가능한 해결책은 Docker를 사용하는 것입니다.</p>
<p>🔳 핵심 아이디어는 기본 프로젝트 설정(<code>Node.js</code>, <code>PNPM</code> 등)이 포함된 공유 Docker 이미지를 사용하고 각 CI 빌드 중에 리포지토리의 최신 코드로 이 이미지를 업데이트하는 것입니다.</p>
<p>🔳 다음은 기본 프로젝트 설정이 포함된 Docker 이미지의 예입니다.</p>
<pre><code class="language-yaml">FROM node:18

# 전역적으로 pnpm 설치
RUN npm install -g pnpm

# 작업 디렉토리 설정
WORKDIR /app

# 프로젝트 파일 복사
COPY package.json pnpm-workspace.yaml ./

# 의존성 설치
RUN pnpm 설치

# 기타 설정(선택 사항)
# (예: Vitest, ESLint 등과 같은 추가 도구 설치)</code></pre>
<p>🔳 Docker 이미지가 빌드되고 <code>my-project-base</code>와 같은 이름으로 레이블이 지정됩니다.</p>
<p>다음은 <strong>브랜치별 특정 빌드</strong>에 대한 GitHub 액션 워크플로우를 구성하는 간단한 예제입니다.</p>
<pre><code class="language-yaml">name: CI Build

on: [push]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Set environment variables
        run: echo &quot;BRANCH_NAME=$(echo ${GITHUB_REF#refs/heads/})&quot; &gt;&gt; $GITHUB_ENV

      - name: Pull Docker image
        run: docker pull my-project:${{ env.BRANCH_NAME }} || true

      - name: Start container and update project
        run: |
          docker run -v $(pwd):/app my-project:${{ env.BRANCH_NAME }} sh -c &quot; \
            git fetch &amp;&amp; \
            git checkout ${{ env.BRANCH_NAME }} &amp;&amp; \
            pnpm install \ 
            pnpm build
          &quot;

      - name: Build and push Docker image
        run: |
          docker build -t my-project:${{ env.BRANCH_NAME }} .
          # (Optional: push the updated image to a registry)</code></pre>
<ul>
<li>특정 브랜치 이름(<code>my-project:${{ env.BRANCH_NAME }}</code>)으로 태그된 Docker 이미지를 가져옵니다. 태그가 존재하지 않으면 예를 들어 가장 <code>최근의</code> 태그로 되돌아갑니다.</li>
<li>새로 빌드된 이미지는 레지스트리에 푸시되기 전에 현재 브랜치 이름(<code>my-project:${{ env.BRANCH_NAME }}</code>)으로 태그가 지정됩니다.</li>
<li>이를 통해 여러 브랜치에서 변경 사항을 독립적으로 분석하고 테스트할 수 있습니다.</li>
</ul>
<p>🔳 다음은 <strong>병렬 풀 리퀘스트 처리</strong>를 위한 GitHub Actions 워크플로우를 구성하는 간단한 예시입니다.</p>
<pre><code class="language-yaml">name: CI Build

on:
  pull_request:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Set environment variables
        run: echo &quot;PR_NUMBER=${{ github.event.pull_request.number }}&quot; &gt;&gt; $GITHUB_ENV

      - name: Pull Docker image
        run: docker pull my-project-base:latest || true

      - name: Start container and update project
        run: |
          docker run -v $(pwd):/app my-project-base sh -c &quot; \
            git fetch &amp;&amp; \
            git checkout ${{ github.head_ref }} &amp;&amp; \
            pnpm install \ 
            pnpm build
          &quot;

      - name: Build and push Docker image
        run: docker build -t my-project:pr-${{ env.PR_NUMBER }} .
        # (선택 사항: 업데이트된 이미지를 레지스트리에 푸시)</code></pre>
<ul>
<li>각 풀 리퀘스트는 PR 번호(예: <code>my-project:pr-123</code>)로 태그된 Docker 이미지를 빌드하는 별도의 CI 작업을 트리거할 수 있습니다.</li>
<li>각 PR의 빌드와 테스트는 자체 격리된 Docker 컨테이너 내에서 실행되므로 한 PR의 변경 사항이 다른 PR의 빌드에 영향을 미치지 않습니다.</li>
<li>이러한 PR별 빌드는 병렬로 실행할 수 있으므로 여러 PR을 동시에 테스트하고 검토할 수 있어 개발 주기가 빨라집니다.</li>
</ul>
<p>🔳 이 솔루션의 주요 한계는 다음과 같습니다.</p>
<p>🔻 여러 팀 또는 프로젝트에서 동일한 기본 이미지를 사용하는 경우 한 팀에서 변경한 내용이 다른 팀의 캐시를 의도치 않게 무효화할 수 있습니다.</p>
<ul>
<li>가장 간단한 해결책은 각 팀 또는 프로젝트에 대해 별도의 기본 이미지를 만드는 것입니다(예: <code>my-project-teamA-base</code>, <code>my-project-teamB-base</code>).</li>
</ul>
<p>🔻 특히 종속성이 많은 모노레포의 경우 Docker 이미지가 상당히 커질 수 있습니다. 이러한 이미지를 밀고 당기는 데는 시간이 걸리고 상당한 대역폭을 소비할 수 있습니다.</p>
<ul>
<li>이에 대한 가능한 해결책은 다단계 빌드를 사용하고, <code>pnpm prune</code>으로 사용하지 않는 종속성을 검토하고, Docker파일 명령을 전략적으로 정렬하고, 명령을 결합하고, <code>.dockerignore</code>를 사용하는 것입니다. 또한 알파인 Linux 또는 배포되지 않는 이미지와 같은 경량 옵션을 사용하는 것도 고려해 보세요.</li>
</ul>
<p>🔻 캐시 무효화 및 정리 전략을 구현하여 오래된 캐시가 누적되어 저장 공간을 차지하지 않도록 해야 합니다.</p>
<ul>
<li>이를 해결하기 위한 한 가지 방법은 변경 사항이 없더라도 기본 이미지를 자동으로 재구축하는 예약된 CI 작업(예: 매주 또는 매일)을 만드는 것입니다. 이렇게 하면 주기적으로 캐시를 새로 고치고 기본 이미지 또는 기본 시스템 패키지에 대한 모든 업데이트를 통합하는 데 도움이 됩니다.</li>
</ul>
<p>✅ 일부 CI 플랫폼은 특정 도구(예: <code>Yarn</code> 또는 <code>npm</code> 또는 <code>pnpm</code>)에 대한 기본 제공 캐싱을 제공합니다. 추가 최적화를 위해 이러한 기능을 Docker 캐싱과 함께 활용할 수 있습니다.</p>
<ul>
<li><a href="https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows">https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows</a></li>
<li><a href="https://docs.gitlab.com/ee/ci/caching/">https://docs.gitlab.com/ee/ci/caching/</a></li>
</ul>
<p>✅ <code>PNPM + Vite</code> 모노레포 내에서 수동 캐싱 전략(예: Docker 또는 사용자 정의 스크립트)에 의존하는 것의 중요한 장점 중 하나는 <code>Nx</code> 및 <code>Turborepo</code>에서 제공하는 클라우드 기반 캐싱 솔루션과 관련된 잠재적 비용을 피할 수 있다는 것입니다.</p>
<p><code>pnpm</code> 워크스페이스 구성과 캐싱 전략의 미묘한 차이를 살펴본 다음, <code>pnpm</code>의 효과에 대한 기술적 평가를 살펴보겠습니다. 그런 다음 사례 연구와 실제 사례를 통해 실제 환경에서 어떻게 작동하는지 살펴보겠습니다. 시작하겠습니다!</p>
<h2 id="기술적-평가">기술적 평가</h2>
<h3 id="인사이트">인사이트</h3>
<p>🔳 <strong>장점: PNPM이 빛나는 곳</strong></p>
<ul>
<li><strong>매우 빠름</strong>: <code>npm</code> 또는 <code>Yarn</code>에 비해 설치 및 종속성 관리 속도가 눈에 띄게 빠릅니다.</li>
<li><strong>공간 효율적</strong>: 종속성을 한 번만 저장하고 하드 링크를 사용하여 디스크 공간을 크게 절약할 수 있습니다.</li>
<li><strong>신뢰성</strong>: 엄격한 종속성 해결로 선언된 종속성만 액세스할 수 있어 오류를 방지합니다.</li>
<li><strong>워크스페이스 지원</strong>: 기본 제공되는 워크스페이스 기능으로 단일 리포지토리 내에서 여러 패키지를 쉽게 관리할 수 있습니다.</li>
</ul>
<p>🔳 <strong>좋지 않은 점: 개선이 필요한 부분</strong></p>
<ul>
<li><strong>학습 곡선</strong>: 고유한 접근 방식은 <code>npm</code>이나 <code>Yarn</code>에 익숙한 사용자에게는 약간의 적응이 필요할 수 있습니다.</li>
<li><strong>제한된 작업 오케스트레이션</strong>: 패키지 전반에서 복잡한 작업을 관리하기 위한 기본 제공 기능이 부족합니다.</li>
<li><strong>캐싱</strong>: 대규모 모노레포에서 빌드 시간을 최적화하려면 추가 설정(예: Docker 또는 CI 캐싱)이 필요합니다.
첫인상을 더 검증하기 위해 광범위한 개발 커뮤니티에서 <code>pnpm</code>이 어떻게 활용되고 있는지 살펴봅시다.</li>
</ul>
<h3 id="실제-인사이트-현장에서의-pnpm">실제 인사이트: 현장에서의 PNPM</h3>
<p>🔳 <code>pnpm</code>은 지난 몇 년 동안 꾸준히 인기를 얻고 있습니다.</p>
<p><img src="https://miro.medium.com/v2/resize:fit:720/format:webp/1*2M-udCAxunO1hlFDIPnSVg.png" alt="">
<a href="https://npmtrends.com/pnpm-vs-yarn">https://npmtrends.com/pnpm-vs-yarn</a></p>
<p><img src="https://miro.medium.com/v2/resize:fit:720/format:webp/1*YTdufYKoytlWhDoZ-rkvzg.png" alt="">
<a href="https://npmtrends.com/pnpm-vs-yarn">https://npmtrends.com/pnpm-vs-yarn</a></p>
<p><img src="https://miro.medium.com/v2/resize:fit:720/format:webp/1*b3uoktOmv4s3JoJ3tXraQw.png" alt="">
<a href="https://npm-stat.com/charts.html?package=pnpm&amp;package=yarn&amp;package=npm&amp;from=2021-01-04&amp;to=2024-07-21">https://npm-stat.com/charts.html?package=pnpm&amp;package=yarn&amp;package=npm&amp;from=2021-01-04&amp;to=2024-07-21</a></p>
<p><img src="https://miro.medium.com/v2/resize:fit:720/format:webp/1*rdK0rs2O94YN0TA98WSCzA.png" alt="">
<a href="https://npm-stat.com/charts.html?package=pnpm&amp;package=yarn&amp;package=npm&amp;from=2021-01-04&amp;to=2024-07-21">https://npm-stat.com/charts.html?package=pnpm&amp;package=yarn&amp;package=npm&amp;from=2021-01-04&amp;to=2024-07-21</a></p>
<p>🔳 <code>pnpm</code>은 Microsoft와 같은 거대 기술 기업부터 Prisma와 같은 혁신적인 스타트업, 심지어 Rush 및 SvelteKit과 같은 영향력 있는 오픈 소스 프로젝트에 이르기까지 다양한 조직에서 널리 채택되고 있습니다.</p>
<p><img src="https://miro.medium.com/v2/resize:fit:720/format:webp/1*7Yj1ay4FMrE_hZT6vxzZ3A.png" alt="">
<a href="https://pnpm.io/users">https://pnpm.io/users</a></p>
<p>🔳 GitHub의 수많은 <a href="https://github.com/search?q=path%3A**%2Fpnpm-lock.yaml&amp;type=code">프로젝트</a>에서 <code>pnpm</code>을 사용합니다.</p>
<p>🔳 GitHub의 수많은 <a href="https://github.com/search?q=path%3A**%2Fpnpm-workspace.yaml&amp;type=code">프로젝트</a>가 <code>pnpm</code> 워크스페이스을 사용합니다.</p>
<p>이러한 실제 현장에서의 모멘텀은 자바스크립트 생태계에서 <code>pnpm</code>의 중요성이 커지고 있음을 강조하며, 기존 패키지 관리자에 대한 실행 가능하고 매력적인 대안으로서 그 입지를 확인해줍니다.</p>
<p>이제 상황을 살펴봤으니 이제 우리의 길을 선택할 차례입니다. 하나의 도구가 최고로 군림할까요, 아니면 하이브리드 접근 방식이 모노레포에 최적화된 전략일까요? 한번 봅시다!</p>
<h2 id="최종-평가-모노레포-개발을-위한-우리의-길">최종 평가: 모노레포 개발을 위한 우리의 길</h2>
<p>다음은 의사 결정 과정에 도움이 되도록 <code>PNPM workspace + Vite</code>, <code>Nx</code> 및 <code>Turborepo</code>의 주요 차이점을 요약한 표입니다.</p>
<p><img src="https://miro.medium.com/v2/resize:fit:720/format:webp/1*Y_mGtDWA6AK3ND_dGqnkcQ.png" alt="">
모노레포 도구 적합성(작성자 이미지)</p>
<p>분석 결과에서 알 수 있듯이 이상적인 모노레포 도구는 각 프로젝트의 특정 요구사항에 따라 크게 달라집니다. 그러나 특히 자체 코드 생성기인 <a href="https://github.com/ekino/bistro"><strong>비스트로</strong></a>를 개발하는 저희는 단순성, 유연성, 성능을 최우선 순위로 삼고 있으므로 Nx를 제외하여 선택의 폭을 좁힐 수 있습니다.</p>
<p>Nx는 분명 강력하지만 광범위한 기능으로 인해 현재 요구사항에 불필요한 복잡성을 유발하여 잠재적으로 성능에 영향을 미칠 수 있습니다. 저희는 툴을 완벽하게 제어하고, <a href="https://github.com/ekino/bistro"><strong>비스트로</strong></a>를 워크플로에 쉽게 통합하며, 무엇보다도 높은 성능 표준을 유지할 수 있는 솔루션을 선호합니다.</p>
<p>따라서 유연성과 효율성이 뛰어난 <code>PNPM Workspace + Vite</code>(및 기타 필수 도구)로 기준선을 설정할 것입니다. 빌드 성능을 향상해야 하는 경우, 전문화된 최적화 기능을 제공하는 <code>Turborepo</code>를 쉽게 추가할 수 있습니다(기존 리포지토리에 추가하는 방법에 대한 <code>Turborepo</code>의 <a href="https://turbo.build/repo/docs/getting-started/add-to-existing-repository">가이드</a> 참조).</p>
<p>이상으로 모노레포 도구의 세계에 대해 자세히 알아봤습니다! 지금까지 모노레포의 환경을 살펴보고, 내부 작동 방식을 자세히 살펴보고, 장단점을 따져봤습니다. 이제 결론을 내릴 시간입니다! 🌟</p>
<h2 id="결론">결론</h2>
<p>결론적으로 모노레포 도구를 살펴본 결과, 프로젝트의 요구 사항에 따라 PNPM, Nx, Turborepo가 각각 고유한 강점을 제공한다는 것을 알 수 있었습니다.</p>
<p>속도, 효율성, 단순성에 중점을 둔 PNPM은 다양한 시나리오에 적합한 다목적 솔루션으로 돋보입니다. 그러나 Nx는 복잡한 대규모 모노레포를 관리하는 데 탁월한 반면, Turborepo는 빌드 최적화에 우선순위를 둡니다.</p>
<p>유연성과 툴링에 대한 통제력 유지에 중점을 두었기 때문에 현재 요구 사항을 처리하고 사용자 지정 코드 생성기인 <a href="https://github.com/ekino/bistro">비스트로</a>와 원활하게 통합할 수 있다는 확신을 가지고 <strong>PNPM Workspace + Vite</strong>를 기반으로 구축하기로 결정했습니다. Turborepo의 빌드 최적화 능력은 부인할 수 없지만, 프로젝트의 성장으로 인해 더 큰 성능 향상이 필요할 때 언제든지 배포할 수 있도록 계속 보유할 것입니다.</p>
<p>단순하고 직관적으로 유지하세요! <a href="https://www.goodreads.com/quotes/9010638-simplicity-is-the-ultimate-sophistication-when-once-you-have-tasted">레오나르도 다빈치</a>는 단순함이 궁극적인 정교함이라고 말했습니다. ❤️</p>
<p>이 시리즈를 통해 프로젝트에 가장 적합한 모노레포 도구를 결정하는 데 필요한 지식과 통찰력을 얻으셨기를 바랍니다.</p>
<p>만능 솔루션은 없으며, 다양한 조합을 실험해 보면 특정 요구사항에 맞는 완벽한 설정을 찾을 수 있다는 점을 기억하세요. 모노레포의 유연성을 활용하고 행복한 코딩을 즐겨보세요! 🌟</p>
<p><strong>모노레포의 모험에 동참해 주셔서 감사합니다!</strong> 🚀</p>
<p>새로운 글과 새로운 모험으로 다시 만날 때까지! ❤️</p>
<p>제 글을 읽어주셔서 감사합니다.</p>
<pre><code class="language-bash">저와 연락하고 싶으신가요?
GitHub에서 저를 찾을 수 있습니다: https://github.com/helabenkhalfallah</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[(번역) 모노레포 인사이트: Nx, Turborepo 그리고 PNPM (2/4 - Nx 중점적인)]]></title>
            <link>https://velog.io/@tap_kim/translate-monorepo-insights-nx-turborepo-and-pnpm2</link>
            <guid>https://velog.io/@tap_kim/translate-monorepo-insights-nx-turborepo-and-pnpm2</guid>
            <pubDate>Thu, 13 Mar 2025 14:02:44 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>원문: <a href="https://medium.com/ekino-france/monorepo-insights-nx-pnpm-and-turborepo-2-4-e6d5837570ad">Monorepo Insights: Nx, Turborepo, and PNPM (2/4)</a></p>
</blockquote>
<p>오늘날 최고의 모노레포 솔루션의 장단점을 살펴봅시다.</p>
<p>아래 링크를 통해 전체 시리즈를 살펴보세요.</p>
<ul>
<li><a href="https://velog.io/@tap_kim/translate-monorepo-insights-nx-turborepo-and-pnpm">모노레포 인사이트: Nx, Turborepo 그리고 PNPM (1/4)</a></li>
<li>모노레포 인사이트: Nx, Turborepo 그리고 PNPM (2/4)(현재 글)</li>
<li><a href="https://velog.io/@tap_kim/translate-monorepo-insights-nx-turborepo-and-pnpm3">모노레포 인사이트: Nx, Turborepo 그리고 PNPM (3/4)</a></li>
<li><a href="https://velog.io/@tap_kim/translate-monorepo-insights-nx-turborepo-and-pnpm4">모노레포 인사이트: Nx, Turborepo 그리고 PNPM (4/4)</a></li>
</ul>
<h2 id="소개">소개</h2>
<p>이 글은 <code>Nx</code>, <code>PNPM</code>, <code>Turborepo</code>의 기능, 성능, 프로젝트 적합성을 비교하는 시리즈의 일부입니다.</p>
<p>모노레포 관리 및 빌드 시스템의 <a href="https://velog.io/@tap_kim/translate-monorepo-insights-nx-turborepo-and-pnpm">기본 사항</a>을 살펴본 후, 이번 글에서는 이 분야의 강력한 도구인 <code>NX</code>에 대해 자세히 알아보겠습니다. ✨</p>
<p>다시 한번 말씀드리지만, 우리의 목표는 개발 워크플로우를 간소화하고 코드 베이스 관리를 개선할 수 있는 최적의 솔루션을 찾는 것입니다.</p>
<p>최고의 모노레포가 빛나길 바랍니다! 우리가 함께 정복할 도전은 다음과 같습니다.</p>
<ul>
<li><a href="#%ED%98%84%EB%AF%B8%EA%B2%BD%EC%9C%BC%EB%A1%9C-%EB%B3%B4%EB%8A%94-NX">현미경으로 보는 NX</a></li>
<li><a href="#%EB%A1%9C%EC%BB%AC%EC%97%90%EC%84%9C-%EC%8B%A4%ED%96%89-%EC%A4%91%EC%9D%B8-NX-%EB%8D%B0%EB%AA%AC-%EC%97%86%EC%9D%B4">로컬에서 실행 중인 NX(데몬 없이)</a></li>
<li><a href="#%EB%A1%9C%EC%BB%AC%EC%97%90%EC%84%9C-%EC%8B%A4%ED%96%89-%EC%A4%91%EC%9D%B8-NX-%EB%8D%B0%EB%AA%AC-%ED%8F%AC%ED%95%A8">로컬에서 실행 중인 NX(데몬 포함)</a></li>
<li><a href="#Nx-Cloud">Nx Cloud</a></li>
<li><a href="#Nx-Replay-%EC%9B%90%EA%B2%A9-%EC%BA%90%EC%8B%B1">Nx Replay(원격 캐싱)</a></li>
<li><a href="#Nx-Agent-%EB%B6%84%EC%82%B0-%EC%9E%91%EC%97%85-%EC%8B%A4%ED%96%89">Nx Agent(분산 작업 실행)</a></li>
<li><a href="#%EB%B6%84%EC%82%B0-%EC%BA%90%EC%8B%B1-%EB%B0%8F-%EC%9E%91%EC%97%85-%EC%8B%A4%ED%96%89%EC%9D%98-%EC%9D%B4%EC%A0%90">분산 캐싱 및 작업 실행의 이점</a></li>
<li><a href="#NX-%ED%95%B8%EC%A6%88%EC%98%A8">NX 핸즈온</a></li>
<li><a href="#%EB%A1%9C%EC%BB%AC%EC%97%90%EC%84%9C-%EC%8B%A4%ED%96%89-%EC%A4%91%EC%9D%B8-Nx">로컬에서 실행 중인 Nx</a></li>
<li><a href="#%EB%B6%84%EC%82%B0-%EC%BA%90%EC%8B%B1%EC%9D%98-%EC%8B%A4%EC%A0%9C-%EC%82%AC%EC%9A%A9">분산 캐싱의 실제 사용</a></li>
<li><a href="#%EA%B8%B0%EC%88%A0%EC%A0%81-%ED%8F%89%EA%B2%B0">기술적 평결</a></li>
<li><a href="#%EC%9D%B8%EC%82%AC%EC%9D%B4%ED%8A%B8">인사이트</a></li>
<li><a href="#%EC%8B%A4%EC%A0%9C-%EC%9D%B8%EC%82%AC%EC%9D%B4%ED%8A%B8-%EC%95%BC%EC%83%9D%EC%97%90%EC%84%9C%EC%9D%98-Nx">실제 인사이트 야생에서의 Nx</a></li>
<li><a href="#%EC%A3%BC%EC%9A%94-%EC%9A%94%EC%A0%90-%EB%89%98%EC%95%99%EC%8A%A4%EA%B0%80-%EC%9E%88%EB%8A%94-%EA%B0%95%EC%9E%90">주요 요점: 뉘앙스가 있는 강자</a></li>
<li><a href="#%EA%B2%B0%EB%A1%A0">결론</a></li>
</ul>
<p>다음 단계가 궁금하신가요? 함께 알아보시죠! 🚀 🌟</p>
<h2 id="현미경으로-보는-nx">현미경으로 보는 NX</h2>
<p>NX의 전체 기능은 다음과 같습니다.</p>
<p><img src="https://miro.medium.com/v2/resize:fit:720/format:webp/0*KDBAVLPYf5OK8jGU.jpeg" alt="">
<a href="https://medium.com/javascript-kingdom/an-introduction-to-nx-the-ultimate-tool-for-monorepos-one-tool-for-almost-everything-44bd23b203f5">https://medium.com/javascript-kingdom/an-introduction-to-nx-the-ultimate-tool-for-monorepos-one-tool-for-almost-everything-44bd23b203f5</a></p>
<p>그리고 NX 워크플로우의 대략적인 개요는 다음과 같습니다.</p>
<pre><code class="language-bash">+-------------------+          +------------------+             +----------------+
| Workspace Config  | -------&gt; |  Project Graph   | -----------&gt;|  Task Graph    | 
| (workspace.json,  |          |   (DAG: projects |             |  (DAG: tasks)  |
|  project.json)    |          |      &amp; deps)     |             |                |
+-------------------+          +------------------+             +----------------+
                                                                         |
                                                                         |
                                                                         v
                                                                   +------------+
                                                                   |  Task      |
                                                                   | Execution  |
                                                                   | &amp; Caching  |
                                                                   +------------+</code></pre>
<p>이 섹션에서는 주요 구성 요소를 분석하여 작동 방식을 이해하고 장점과 한계를 평가할 수 있도록 합니다.</p>
<h3 id="로컬에서-실행되는-nx데몬-없이">로컬에서 실행되는 NX(데몬 없이)</h3>
<p>NX는 모노레포 개발 및 관리를 간소화하도록 설계된 정교한 툴킷입니다.</p>
<p>지능형 프로젝트 그래프 분석, 최적화된 작업 실행, 효율적인 캐싱 및 유연한 플러그인 시스템에 대해 더 자세히 살펴봅시다!</p>
<p>먼저 Nx를 설치하면 일반적으로 콘솔에 다음과 같은 출력이 표시됩니다.</p>
<pre><code class="language-bash">nx % pnpm i -D nx

Packages: +109
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Progress: resolved 118, reused 0, downloaded 109, added 109, done
node_modules/.pnpm/nx@19.3.2/node_modules/nx: Running postinstall script, done in 526ms

devDependencies:
+ nx 19.3.2

Done in 9.1s</code></pre>
<p>🔳 중요한 스크립트인 <a href="https://github.com/nrwl/nx/blob/master/packages/nx/package.json#L12">post-installation script</a>를 살펴보면, <code>packages/nx/bin/post-install.ts</code>가 실행되어 몇 가지 필수 설정 작업을 수행합니다.</p>
<pre><code class="language-ts">// https://github.com/nrwl/nx/blob/master/packages/nx/bin/post-install.ts
...

(async () =&gt; {
    const start = new Date();
    try {
        setupWorkspaceContext(workspaceRoot);

        if (isMainNxPackage() &amp;&amp; fileExists(join(workspaceRoot, &#39;nx.json&#39;))) {
            assertSupportedPlatform();

            try {
                await daemonClient.stop();
            } catch (e) {}
            const tasks: Array&lt;Promise&lt;any&gt;&gt; = [
                buildProjectGraphAndSourceMapsWithoutDaemon(),
            ];
...</code></pre>
<p>이 스크립트에서 두 가지 핵심 함수는 <a href="https://github.com/nrwl/nx/blob/master/packages/nx/src/utils/workspace-context.ts#L9"><code>setupWorkspaceContext</code></a>와 <a href="https://github.com/nrwl/nx/blob/master/packages/nx/src/project-graph/project-graph.ts#L92"><code>buildProjectGraphAndSourceMapsWithoutDaemon</code></a>입니다.</p>
<p>🔳 <a href="https://github.com/nrwl/nx/blob/master/packages/nx/src/utils/workspace-context.ts#L9"><strong><code>setupWorkspaceContext</code></strong></a><strong>: 타입스크립트와 러스트 연결하기</strong></p>
<p>이 함수는 Nx의 타입스크립트 코드와 기본 Rust 구현 사이의 다리 역할을 합니다. <strong>프로젝트 그래프 관리</strong>를 위한 핵심 로직이 포함된 컴파일된 Rust 모듈을 동적으로 로드합니다.</p>
<pre><code class="language-ts">// https://github.com/nrwl/nx/blob/master/packages/nx/src/utils/workspace-context.ts#L9
export function setupWorkspaceContext(workspaceRoot: string) {
  const { WorkspaceContext } =
    require(&quot;../native&quot;) as typeof import(&quot;../native&quot;);
  performance.mark(&quot;workspace-context&quot;);

  // https://github.com/nrwl/nx/blob/master/packages/nx/src/native/workspace/context.rs#L163
  workspaceContext = new WorkspaceContext(
    workspaceRoot,
    cacheDirectoryForWorkspace(workspaceRoot)
  );

  performance.mark(&quot;workspace-context:end&quot;);
  performance.measure(
    &quot;workspace context init&quot;,
    &quot;workspace-context&quot;,
    &quot;workspace-context:end&quot;
  );
}</code></pre>
<p>여기서 핵심 작업은 <strong>Rust</strong> 레이어에 <a href="https://github.com/nrwl/nx/blob/master/packages/nx/src/native/workspace/context.rs#L163"><code>WorkspaceContext</code></a> 인스턴스를 생성하는 것입니다. 이 인스턴스는 <strong>파일 작업</strong> 및 <strong>프로젝트 그래프 구성</strong> 등 작업 공간과 상호작용을 하기 위한 중앙 인터페이스가 됩니다.</p>
<pre><code class="language-rs">// https://github.com/nrwl/nx/blob/master/packages/nx/src/native/workspace/context.rs#L163

#[napi]
impl WorkspaceContext {
#[napi(constructor)]
    pub fn new(workspace_root: String, cache_dir: String) -&gt; Self {
        enable_logger();

        trace!(?workspace_root);

        let workspace_root_path = PathBuf::from(&amp;workspace_root);

        WorkspaceContext {
            files_worker: FilesWorker::gather_files(&amp;workspace_root_path, cache_dir),
            workspace_root,
                workspace_root_path,
        }
    }</code></pre>
<p>🔳 <a href="https://github.com/nrwl/nx/blob/master/packages/nx/src/native/workspace/context.rs#L36"><strong><code>gather_files</code></strong></a> <strong>: 비동기 파일 수집(Rust)</strong></p>
<p>Rust 구현(<code>context.rs</code>)에서 <code>gather_files</code> 함수(<code>WorkspaceContext</code> 생성자에서 호출)는 워크스페이스에서 파일을 효율적으로 수집하고 해싱하는 일을 담당합니다.</p>
<pre><code class="language-rs">// https://github.com/nrwl/nx/blob/master/packages/nx/src/native/workspace/context.rs#L36
// https://github.com/nrwl/nx/blob/master/packages/nx/src/native/workspace/context.rs
impl FilesWorker {
    fn gather_files(workspace_root: &amp;Path, cache_dir: String) -&gt; Self {
        if !workspace_root.exists() {
            warn!(
                &quot;workspace root does not exist: {}&quot;,
                workspace_root.display()
            );
            return FilesWorker(None);
        }

        // ... (로깅 및 설정)
        let archived_files = read_files_archive(&amp;cache_dir);

        // ... (스레드 동기화 설정)

        thread::spawn(move || {
            // ... (잠금 설정)

            let file_hashes = if let Some(archived_files) = archived_files {
            selective_files_hash(&amp;workspace_root, archived_files)
        } else {
            full_files_hash(&amp;workspace_root)
        };

        // ... (파일 데이터 처리 및 정렬)

    *workspace_files = files; // 공유된 벡터에 파일 데이터 저장

    // ... (메인 스레드에 알림 및 캐시 적용)
    cvar.notify_all();

....
    write_files_archive(&amp;cache_dir, file_hashes);
    });

        FilesWorker(Some(files_lock)) //  FilesWorker 인스턴스를 반환
    }
}</code></pre>
<p>🔵 주요 단계는 다음과 같습니다.</p>
<p>✔️ 워크스페이스 루트 확인: 워크스페이스 디렉터리가 존재하는지 확인합니다.</p>
<p>✔️ 캐시 된 파일 읽기(선택 사항): 이전에 캐시 된 파일 정보를 읽어 증분 업데이트 속도를 높입니다.</p>
<p>✔️ 백그라운드 스레드 생성: 파일 해싱을 비동기적으로 처리하기 위해 별도의 스레드를 생성합니다(<code>thread::spawn</code>).</p>
<p>✔️ 파일 해싱.</p>
<ul>
<li>모든 관련 파일에 대한 해시를 계산합니다(캐시 된 데이터를 사용할 수 있는 경우에는 <code>selective_files_hash</code>, 그렇지 않은 경우에는 <code>full_files_hash</code> 사용).</li>
<li>파일 데이터(경로 및 해시)를 정렬합니다.</li>
</ul>
<p>✔️ 데이터 저장소: 정렬된 파일 데이터를 공유 작업공간 파일 벡터에 저장합니다.</p>
<p>✔️ 알림 및 캐싱: 데이터가 준비되었음을 메인 스레드(<code>cvar.notify_all()</code>)에 알리고 파일 데이터를 캐시에 적용합니다(<code>write_files_archive</code>).</p>
<p><img src="https://miro.medium.com/v2/resize:fit:720/format:webp/1*-WqIpVcYN__cqYJPejyBpg.png" alt="">
<a href="https://nx.dev/concepts/how-caching-works">https://nx.dev/concepts/how-caching-works</a></p>
<p>🔳 <a href="https://github.com/nrwl/nx/blob/master/packages/nx/src/project-graph/project-graph.ts#L92"><strong><code>buildProjectGraphAndSourceMapsWithoutDaemon</code></strong></a><strong>: 프로젝트 그래프 생성 오케스트레이션</strong></p>
<p><code>buildProjectGraphAndSourceMapsWithoutDaemon</code> 함수는 Nx 데몬을 사용하지 않을 때 <strong>Nx 프로젝트 그래프</strong> 및 관련 소스 맵을 <strong>빌드</strong>하는 전체 프로세스를 오케스트레이션합니다.</p>
<pre><code class="language-ts">// https://github.com/nrwl/nx/blob/master/packages/nx/src/project-graph/build-project-graph.ts#L1
// https://github.com/nrwl/nx/blob/master/packages/nx/src/project-graph/project-graph.ts#L92

...
export async function buildProjectGraphAndSourceMapsWithoutDaemon() {
    global.NX_GRAPH_CREATION = true;
    const nxJson = readNxJson();

...
    let configurationResult: ConfigurationResult;
    let projectConfigurationsError: ProjectConfigurationsError;
    const [plugins, cleanup] = await loadNxPlugins(nxJson.plugins);

   try {
        configurationResult = await retrieveProjectConfigurations(
            plugins,
            workspaceRoot,
            nxJson
        );
    } catch (e) {
        if (e instanceof ProjectConfigurationsError) {
            projectConfigurationsError = e;
            configurationResult = e.partialProjectConfigurationsResult;
        } else {
            throw e;
        }
    }
    const { projects, externalNodes, sourceMaps, projectRootMap } =
        configurationResult;

....

    const { allWorkspaceFiles, fileMap, rustReferences } =
        await retrieveWorkspaceFiles(workspaceRoot, projectRootMap);

...

    const cacheEnabled = process.env.NX_CACHE_PROJECT_GRAPH !== &#39;false&#39;;
 ...

    let projectGraphError: AggregateProjectGraphError;
    let projectGraphResult: Awaited&lt;
        ReturnType&lt;typeof buildProjectGraphUsingProjectFileMap&gt;
    &gt;;
    try {
        projectGraphResult = await buildProjectGraphUsingProjectFileMap(
            projects,
            externalNodes,
            fileMap,
            allWorkspaceFiles,
            rustReferences,
            cacheEnabled ? readFileMapCache() : null,
            plugins,
            sourceMaps
        );
    } catch (e) {
        if (isAggregateProjectGraphError(e)) {
            projectGraphResult = {
                projectGraph: e.partialProjectGraph,
                projectFileMapCache: null,
            };
            projectGraphError = e;
        } else {
            throw e;
        }
    } finally {
        // 플러그인이 격리된 경우 CLI를 한번 실행하는 동안에는 cleanup 하지 않습니다.
        // CLI 프로세스가 종료될 때 cleanup 됩니다.
        // 여기서 cleanup 하면 보류 중인 promise가 해결되지 않을시 문제가 발생할 수 있습니다.
        if (process.env.NX_ISOLATE_PLUGINS !== &#39;true&#39;) {
            cleanup();
        }
    }
...
</code></pre>
<p><strong>🔵 주요 단계는 다음과 같습니다.</strong></p>
<p>✔️ 구성(Configuration) 및 플러그인 로드하기: <code>nx.json</code> 구성을 읽고 관련 플러그인을 로드합니다.</p>
<pre><code class="language-ts">const nxJson = readNxJson();

....

// https://github.com/nrwl/nx/blob/master/packages/nx/src/config/nx-json.ts#L459
export function readNxJson(root: string = workspaceRoot): NxJsonConfiguration {
  const nxJson = join(root, &#39;nx.json&#39;);
  if (existsSync(nxJson)) {
    const nxJsonConfiguration = readJsonFile&lt;NxJsonConfiguration&gt;(nxJson);
    if (nxJsonConfiguration.extends) {
      const extendedNxJsonPath = require.resolve(nxJsonConfiguration.extends, {
        paths: [dirname(nxJson)],
      });
      const baseNxJson = readJsonFile&lt;NxJsonConfiguration&gt;(extendedNxJsonPath);
      return {
        ...baseNxJson,
        ...nxJsonConfiguration,
      };
    } else {
      return nxJsonConfiguration;
    }</code></pre>
<p>✔️ 프로젝트 구성을 검색하고 구문 분석하여 프로젝트 정보를 수집합니다.</p>
<pre><code class="language-ts">...
// https://github.com/nrwl/nx/blob/master/packages/nx/src/project-graph/utils/retrieve-workspace-files.ts#L63
export async function retrieveProjectConfigurations(
    plugins: LoadedNxPlugin[],
    workspaceRoot: string,
    nxJson: NxJsonConfiguration
): Promise&lt;ConfigurationResult&gt; {
    const globPatterns = configurationGlobs(plugins);
    const workspaceFiles = await globWithWorkspaceContext(
        workspaceRoot,
        globPatterns
    );

    return createProjectConfigurations(
        workspaceRoot,
        nxJson,
        workspaceFiles,
        plugins
    );
}

...
// https://github.com/nrwl/nx/blob/master/packages/nx/src/project-graph/utils/retrieve-workspace-files.ts#L82
export async function retrieveProjectConfigurationsWithAngularProjects(
    workspaceRoot: string,
    nxJson: NxJsonConfiguration
): Promise&lt;ConfigurationResult&gt; {
    const pluginsToLoad = nxJson?.plugins ?? [];

    if (
        shouldMergeAngularProjects(workspaceRoot, true) &amp;&amp;
        !pluginsToLoad.some(
            (p) =&gt;
                p === NX_ANGULAR_JSON_PLUGIN_NAME ||
                (typeof p === &#39;object&#39; &amp;&amp; p.plugin === NX_ANGULAR_JSON_PLUGIN_NAME)
        )
    ) {
        pluginsToLoad.push(join(__dirname, &#39;../../adapter/angular-json&#39;));
    }
...
</code></pre>
<p>✔️ 워크스페이스 파일을 검색하여 워크스페이스의 모든 관련 파일에 대한 정보를 수집합니다.</p>
<pre><code class="language-ts">// https://github.com/nrwl/nx/blob/master/packages/nx/src/project-graph/utils/retrieve-workspace-files.ts#L26
/**
  * 워크스페이스 디렉토리를 탐색하여 `projectFileMap`, `ProjectConfigurations` 및 `allWorkspaceFiles`를 생성합니다.
 * @throws
 * @param workspaceRoot
 * @param nxJson
 */
export async function retrieveWorkspaceFiles(
    workspaceRoot: string,
    projectRootMap: Record&lt;string, string&gt;
) {

...

    const { projectFileMap, globalFiles, externalReferences } =
        await getNxWorkspaceFilesFromContext(workspaceRoot, projectRootMap);

...

    return {
        allWorkspaceFiles: buildAllWorkspaceFiles(projectFileMap, globalFiles),
        fileMap: {
            projectFileMap,
            nonProjectFiles: globalFiles,
        },
        rustReferences: externalReferences,
    };
}</code></pre>
<p>✔️ <a href="https://github.com/nrwl/nx/blob/master/packages/nx/src/project-graph/build-project-graph.ts#L78"><strong><code>buildProjectGraphUsingProjectFileMap</code></strong></a>을 사용하는 함수는 수집된 정보를 기반으로 프로젝트 그래프를 구성합니다.</p>
<pre><code class="language-ts">export async function buildProjectGraphUsingProjectFileMap(
    projectRootMap: Record&lt;string, ProjectConfiguration&gt;,
    externalNodes: Record&lt;string, ProjectGraphExternalNode&gt;,
    fileMap: FileMap,
    allWorkspaceFiles: FileData[],
    rustReferences: NxWorkspaceFilesExternals,
    fileMapCache: FileMapCache | null,
    plugins: LoadedNxPlugin[],
    sourceMap: ConfigurationSourceMaps
): Promise&lt;{
    projectGraph: ProjectGraph;
    projectFileMapCache: FileMapCache;
}&gt; {

...
  return {
    projectGraph,
    projectFileMapCache,
  };
}</code></pre>
<p>🔳 <a href="https://github.com/nrwl/nx/blob/master/packages/nx/src/project-graph/build-project-graph.ts#L78"><strong><code>buildProjectGraphUsingProjectFileMap</code></strong></a>,<a href="https://github.com/nrwl/nx/blob/master/packages/nx/src/project-graph/project-graph-builder.ts#L25"> <strong><code>ProjectGraphBuilder</code></strong></a>, and <a href="https://github.com/nrwl/nx/blob/master/packages/nx/src/project-graph/build-project-graph.ts#L211"><strong><code>buildProjectGraphUsingContext</code></strong></a>: 이러한 함수는 프로젝트 그래프를 점진적으로 구축하고, 외부 노드를 추가하고, 프로젝트 노드를 정규화하고, 암시적 종속성을 적용하기 위해 사용합니다.</p>
<p>🔳 <a href="https://github.com/nrwl/nx/blob/master/packages/nx/src/project-graph/project-graph-builder.ts#L25">ProjectGraphBuilder</a> 클래스는 그래프에 노드와 종속성을 추가하고 제거하는 프로세스를 용이하게 합니다:</p>
<pre><code class="language-ts">export class ProjectGraphBuilder {
  // TODO(FrozenPandaz): 이것을 비공개로 설정합니다.
  readonly graph: ProjectGraph;
...
  /**
   * 프로젝트 그래프에 프로젝트 노드를 추가합니다.
   */
  addNode(node: ProjectGraphProjectNode): void {
    // 같은 이름의 프로젝트가 이미 존재하는지 확인합니다.
    if (this.graph.nodes[node.name]) {
      // 기존 프로젝트가 다른 유형인 경우 Throw 합니다.
      if (this.graph.nodes[node.name].type !== node.type) {
        throw new Error(
          `Multiple projects are named &quot;${node.name}&quot;. One is of type &quot;${
            node.type
          }&quot; and the other is of type &quot;${
            this.graph.nodes[node.name].type
          }&quot;. Please resolve the conflicting project names.`
        );
      }
    }
    this.graph.nodes[node.name] = node;
  }

  /**
   * 그래프에서 노드와 모든 의존성 에지를 제거합니다.
   */
  removeNode(name: string) {
    if (!this.graph.nodes[name] &amp;&amp; !this.graph.externalNodes[name]) {
      throw new Error(`There is no node named: &quot;${name}&quot;`);
    }
...
</code></pre>
<p>🔳 <a href="https://github.com/nrwl/nx/blob/master/packages/nx/src/config/project-graph.ts#L67"><code>ProjectGraph</code></a>(<a href="https://en.wikipedia.org/wiki/Directed_acyclic_graph"><strong><code>DAG(유향 비순환 그래프)</code></strong></a>)는 Nx가 효율적인 작업 스케줄링, 종속성 분석 및 증분 빌드를 수행할 수 있도록 하는 중요한 데이터 구조입니다.</p>
<pre><code class="language-ts">/**
 * 작업 공간의 프로젝트와 프로젝트 간의 종속성 그래프
 */
export interface ProjectGraph {
  nodes: Record&lt;string, ProjectGraphProjectNode&gt;;
  externalNodes?: Record&lt;string, ProjectGraphExternalNode&gt;;
  dependencies: Record&lt;string, ProjectGraphDependency[]&gt;;
  version?: string;
}</code></pre>
<blockquote>
<p>&#39;nx test myapp&#39;와 같이 대상을 직접 호출하거나 &#39;nx affected:test&#39;와 같이 영향을 받는 명령을 실행할 때마다 <strong>Nx는 먼저 작업 공간 내의 모든 다른 프로젝트와 파일이 서로 어떻게 맞는지 파악하기 위해 프로젝트 그래프를 생성해야 합니다</strong>. 당연히 작업 공간이 커질수록 이 프로젝트 그래프 생성에 더 큰 비용이 듭니다. - <a href="https://github.com/nrwl/nx/blob/master/docs/shared/concepts/daemon.md?plain=1">https://github.com/nrwl/nx/blob/master/docs/shared/concepts/daemon.md?plain=1</a></p>
</blockquote>
<p>🔳 작업 공간을 DAG(일반적인 그래프)로 올바르게 모델링하려면 잘 정의된 응집력 있는 단위(모듈)가 있는 <strong>명확한 경계</strong>를 갖는 것이 중요합니다.</p>
<blockquote>
<p>코드를 잘 정의된 응집력 있는 단위로 분할하면 소규모 조직이라도 수십 개의 앱과 수십 또는 수백 개의 라이브러리를 보유하게 됩니다. 이 모든 것이 서로 자유롭게 의존할 수 있다면 혼란이 뒤따르고 작업 공간은 관리가 불가능해질 것입니다. - <a href="https://nx.dev/features/enforce-module-boundaries">https://nx.dev/features/enforce-module-boundaries</a></p>
</blockquote>
<p>🔳 기본적으로 NX는 이러한 긴밀성을 보장하지만, 이 규칙을 강화할 수도 있습니다.</p>
<blockquote>
<p>이를 위해 NX는 코드 분석을 사용하여 프로젝트가 서로의 잘 정의된 공용 API에만 의존할 수 있도록 합니다. 또한 프로젝트가 서로 의존할 수 있는 방식에 대해 선언적으로 제약을 가할 수 있습니다. - <a href="https://nx.dev/features/enforce-module-boundaries">https://nx.dev/features/enforce-module-boundaries</a></p>
</blockquote>
<p>🔳 <strong>작업 오케스트레이션 및 실행</strong>: 프로젝트 그래프는 <a href="https://github.com/nrwl/nx/blob/master/packages/nx/src/tasks-runner/run-command.ts#L94">실행할</a> 작업과 그 종속성을 나타내는 작업 그래프(<code>createTaskGraphAndValidateCycles</code>)를 생성하는 데 사용됩니다.</p>
<pre><code class="language-ts">...

export async function runCommand(
  projectsToRun: ProjectGraphProjectNode[],
  projectGraph: ProjectGraph,
  { nxJson }: { nxJson: NxJsonConfiguration },
  nxArgs: NxArgs,
  overrides: any,
  initiatingProject: string | null,
  extraTargetDependencies: Record&lt;string, (TargetDependencyConfig | string)[]&gt;,
  extraOptions: { excludeTaskDependencies: boolean; loadDotEnvFiles: boolean }
): Promise&lt;NodeJS.Process[&#39;exitCode&#39;]&gt; {

  const status = await handleErrors(
    process.env.NX_VERBOSE_LOGGING === &#39;true&#39;,
    async () =&gt; {
      const projectNames = projectsToRun.map((t) =&gt; t.name);

      const taskGraph = createTaskGraphAndValidateCycles(
        projectGraph,
        extraTargetDependencies ?? {},
        projectNames,
        nxArgs,
        overrides,
        extraOptions
      );
      const tasks = Object.values(taskGraph.tasks);

      const { lifeCycle, renderIsDone } = await getTerminalOutputLifeCycle(
        initiatingProject,
        projectNames,
        tasks,
        nxArgs,
        nxJson,
        overrides
      );

      const status = await invokeTasksRunner({
        tasks,
        projectGraph,
        taskGraph,
        lifeCycle,
        nxJson,
        nxArgs,
        loadDotEnvFiles: extraOptions.loadDotEnvFiles,
        initiatingProject,
      });

      await renderIsDone;

      return status;
    }
  );

  return status;
}
...</code></pre>
<p><code>createTaskGraphAndValidateCycles</code> 함수는 <strong>작업 그래프</strong> 생성을 담당하는 함수입니다. 이 함수는 <code>projectGraph</code>, <code>extraTargetDependencies</code> 및 기타 매개변수를 입력으로 받고 유효성 검사 후 <code>task graph</code>를 반환합니다.</p>
<p><a href="https://github.com/nrwl/nx/blob/master/packages/nx/src/tasks-runner/run-command.ts#L115C1-L130C4"><code>task graph</code></a>가 <strong>비순환적</strong>인지(순환 종속성이 없는지) 확인하기 위해 유효성을 검사합니다. 순환이 감지되면 Nx는 구성 설정에 따라 오류를 발생시키거나 사용자에게 경고합니다.</p>
<pre><code class="language-ts">const cycle = findCycle(taskGraph);
if (cycle) {
  if (process.env.NX_IGNORE_CYCLES === &quot;true&quot; || nxArgs.nxIgnoreCycles) {
    output.warn({
      title: `The task graph has a circular dependency`,
      bodyLines: [`${cycle.join(&quot; --&gt; &quot;)}`],
    });
    makeAcyclic(taskGraph);
  } else {
    output.error({
      title: `Could not execute command because the task graph has a circular dependency`,
      bodyLines: [`${cycle.join(&quot; --&gt; &quot;)}`],
    });
    process.exit(1);
  }
}</code></pre>
<p>🔳 Nx는 실제로 아키텍처에서 <strong>DAG</strong>(유향 비순환 그래프)와 <strong>작업 그래프</strong>를 모두 활용하며, 각각 다른 용도로 사용됩니다.</p>
<p><strong>✔️ 프로젝트 그래프(DAG)</strong></p>
<ul>
<li><strong>대표</strong>: 노드는 프로젝트(애플리케이션, 라이브러리 등)이고 간선은 이들 간의 종속성을 나타내는 작업 공간의 정적 구조입니다.</li>
<li><strong>목적</strong>: 종속성 분석, 코드 생성, 변경 시 영향을 받는 프로젝트 파악에 사용됩니다.</li>
<li><strong>예시</strong>: 예: Nx 워크스페이스에서 <code>app</code> 프로젝트가 <code>ui-lib</code> 라이브러리에 종속된 경우, 프로젝트 그래프에서 <code>app</code> 노드에서 <code>ui-lib</code> 노드로 향하는 에지가 있습니다.</li>
</ul>
<p><strong>✔️ 작업 그래프(DAG)</strong></p>
<ul>
<li><strong>대표</strong>: 실행할 작업의 동적 워크플로우를 나타내며, 노드는 개별 작업(예: 빌드, 테스트, 린트)이고 간선은 작업 간의 종속성을 나타냅니다.</li>
<li><strong>목적</strong>: 빌드/테스트/배포 프로세스의 작업 스케줄링, 병렬화 및 최적화에 사용됩니다.</li>
<li><strong>예시</strong>: 예: <code>app</code> 프로젝트를 빌드할 때 <code>build ui-lib</code>, <code>lint app</code>, <code>test app</code>과 같은 작업이 포함될 수 있습니다. 작업 그래프는 이러한 작업이 실행되어야 하는 순서를 정의하여 <code>build app</code> 전에 <code>build ui-lib</code>가 완료되도록 합니다.</li>
</ul>
<p><img src="https://miro.medium.com/v2/resize:fit:720/format:webp/1*Zt1VpOucHl8OmLK4dRRJ4Q.png" alt="">
<a href="https://nx.dev/concepts/mental-model">https://nx.dev/concepts/mental-model</a></p>
<p><img src="https://miro.medium.com/v2/resize:fit:720/format:webp/1*i5f5FZUCM01EK3Az9iaIjw.png" alt="">
<a href="https://nx.dev/concepts/mental-model">https://nx.dev/concepts/mental-model</a></p>
<p>💡 작업은 단독으로 실행되지 않습니다. 작업은 작업 간의 관계와 종속성을 나타내는 더 큰 작업 그래프의 일부입니다. 이 그래프는 병렬 실행을 최적화하면서 종속성을 존중하여 작업이 올바른 순서로 실행되도록 합니다.</p>
<p>💡 Nx는 작업 그래프에서 작업을 실제로 실행하는 다양한 작업 러너(예: <code>defaultTasksRunner</code>, <code>nxCloudTasksRunnerShell</code>)를 제공합니다. 앞서 살펴본 <code>invokeTasksRunner</code> 함수는 구성 및 컨텍스트에 따라 적절한 태스크 러너를 선택하는 역할을 합니다.</p>
<p>🔳 최단 경로를 찾기 위해 NX는 <a href="https://github.com/nrwl/nx/blob/master/graph/ui-graph/src/lib/util-cytoscape/project-traversal-graph.ts#L147">Dijkstra</a> 알고리즘을 사용합니다.</p>
<pre><code class="language-ts">// https://github.com/nrwl/nx/blob/master/graph/ui-graph/src/lib/graph.ts#L164
...
      case &#39;notifyGraphTracing&#39;:
        if (event.start &amp;&amp; event.end) {
          if (event.algorithm === &#39;shortest&#39;) {
            elementsToSendToRender = this.projectTraversalGraph.traceProjects(
              event.start,
              event.end
            );
          } else {
            elementsToSendToRender =
              this.projectTraversalGraph.traceAllProjects(
                event.start,
                event.end
              );
          }
        }
        break;
    }

...


// https://github.com/nrwl/nx/blob/master/graph/ui-graph/src/lib/util-cytoscape/project-traversal-graph.ts#L146
traceProjects(start: string, end: string) {
  const dijkstra = this.cy
      .elements()
      .dijkstra({ root: `[id = &quot;${start}&quot;]`, directed: true });

  const path = dijkstra.pathTo(this.cy.$(`[id = &quot;${end}&quot;]`));

  return path.union(path.ancestors());
}</code></pre>
<p>Nx의 의존성 그래프와 몇 가지 기본 어빌리티에 대해 배웠다면, 이제 또 다른 중요한 어빌리티를 알아볼 차례로 데몬을 알아봅시다. 👻</p>
<h2 id="로컬에서-실행-중인-nx데몬-사용">로컬에서 실행 중인 NX(데몬 사용)</h2>
<p>🔵 로컬 머신에서 실행할 때는 기본적으로 Nx 데몬이 활성화됩니다. 활성화를 끄고 싶다면 아래와 같은 방법으로 비활성화 할 수 있습니다.</p>
<pre><code class="language-bash">- `nx.json`의 러너 옵션에서 `useDaemonProcess: false`를 설정하거나
- 환경 변수 `NX_DAEMON`을 `false`로 설정합니다.</code></pre>
<p>🔵 데몬은 여러 가지 방식으로 Nx 명령의 성능과 응답성을 <strong>향상</strong>시키기 위해 설계된 <strong>장기 실행 백그라운드</strong> 프로세스입니다.</p>
<p>✔️ <strong>프로젝트 그래프 캐시</strong>: 프로젝트 그래프의 캐시 버전을 메모리에 유지하여 모든 명령에 대해 다시 빌드할 필요가 없도록 합니다.</p>
<p>✔️ <strong>파일 감시</strong>: 작업 공간의 파일 변경 사항을 능동적으로 모니터링하고 필요할 때 프로젝트 그래프를 점진적으로 업데이트합니다.</p>
<blockquote>
<p>Nx 데몬은 작업 공간의 파일을 감시하고 프로젝트 그래프를 즉시 업데이트하기 때문에 재계산을 최소화하기 위해 지능적으로 스로틀링하므로 프로젝트 그래프를 더 효율적으로 재계산할 수 있습니다(재계산을 최소화하도록 지능적으로 스로틀링합니다). 또한 모든 것을 메모리에 보관하므로 응답 속도가 훨씬 빨라지는 경향이 있습니다. - <a href="https://github.com/nrwl/nx/blob/master/docs/shared/concepts/daemon.md?plain=1">https://github.com/nrwl/nx/blob/master/docs/shared/concepts/daemon.md?plain=1</a></p>
</blockquote>
<p>✔️ <strong>병렬 작업</strong>: 여러 작업을 동시에 실행할 수 있으며, 여러 코어 또는 머신(Nx Cloud 사용 시)을 활용하여 빌드 속도를 높일 수 있습니다.</p>
<p>✔️ <strong>작업 캐시</strong>: 로컬 캐시와 마찬가지로 데몬은 작업 결과에 대한 캐시를 유지하여 변경되지 않은 작업의 재실행을 방지함으로써 성능을 더욱 향상시킵니다.</p>
<blockquote>
<p>효율성을 극대화하기 위해 Nx 데몬에는 필요하지 않을 때 자동으로 종료(모든 파일 감시자 제거 포함)하는 몇 가지 메커니즘이 내장되어 있습니다. 여기에는 3시간 동안 활동이 없는 경우(해당 시간 동안 작업 공간의 Nx 데몬이 어떤 요청도 받지 않았거나 파일 변경을 감지하지 못한 경우)와 Nx 설치가 변경되는 경우가 포함됩니다. - <a href="https://github.com/nrwl/nx/blob/master/docs/shared/concepts/daemon.md?plain=1">https://github.com/nrwl/nx/blob/master/docs/shared/concepts/daemon.md?plain=1</a></p>
</blockquote>
<p>🔴 Nx 작업 공간당 하나의 고유한 Nx 데몬이 있습니다.</p>
<blockquote>
<p>Nx 데몬은 로컬 컴퓨터의 백그라운드에서 실행되는 프로세스입니다. Nx 작업 공간당 하나의 고유한 Nx 데몬이 있다는 것은 컴퓨터에 여러 개의 Nx 작업 공간이 동시에 활성화되어 있는 경우 해당 Nx 데몬 인스턴스가 서로 독립적으로 작동하며 다른 버전의 Nx에 있을 수 있음을 의미합니다. - <a href="https://github.com/nrwl/nx/blob/master/docs/shared/concepts/daemon.md?plain=1">https://github.com/nrwl/nx/blob/master/docs/shared/concepts/daemon.md?plain=1</a></p>
</blockquote>
<p>이 수준에서 로컬 운영에 대한 모든 것을 해부했다고 생각하는데, 이제 Nx Cloud로 이동하는 것에 대해 어떻게 생각하시나요? 시작하겠습니다! ☁️</p>
<h2 id="nx-cloud">Nx Cloud</h2>
<p>Nx Cloud는 분산 캐싱 및 작업 실행 기능을 활용하여 모노레포 개발을 강화합니다. 레고 블록처럼 빌드 아티팩트를 공유 및 재사용하고 빌더 팀처럼 작업을 병렬화하여 빌드 시간을 크게 단축하고 리소스를 절약하며 팀 협업을 강화하여 가장 복잡한 프로젝트도 더 쉽게 관리할 수 있습니다.</p>
<p>🔴 NX Cloud는 <strong>유료 서비스</strong>이며 다양한 <a href="https://nx.app/pricing">요금제</a>가 있습니다.</p>
<h3 id="nx-replay원격-캐싱"><a href="https://nx.dev/ci/features/remote-cache">Nx Replay(원격 캐싱)</a></h3>
<p>Nx Replay는 분산 캐싱을 가능하게 하는 Nx Cloud의 핵심 기능입니다. 작업 실행 결과를 원격 캐시에 저장하여 모든 팀원과 CI 머신이 이를 공유할 수 있도록 합니다.</p>
<p>✔️ 수정된 PR을 위한 더 빠른 CI: PR에 대한 후속 커밋은 이전 CI 실행에서 캐시 된 결과를 재사용할 수 있으므로 불필요한 작업 재실행이 줄어듭니다.</p>
<p><img src="https://miro.medium.com/v2/resize:fit:720/format:webp/1*COCl1UdJ8vmTwg5ZqSm18A.png" alt="">
<a href="https://nx.dev/ci/features/remote-cache">https://nx.dev/ci/features/remote-cache</a></p>
<p>✔️ 개발자 기기에서 CI 결과 재사용: 개발자는 CI 작업의 결과를 즉시 활용하여 로컬 빌드 및 테스트 속도를 높일 수 있습니다.</p>
<p><img src="https://miro.medium.com/v2/resize:fit:720/format:webp/1*u6_Ty0Xb8OR-ShV-KmiPew.png" alt="">
<a href="https://nx.dev/concepts/how-caching-works">https://nx.dev/concepts/how-caching-works</a></p>
<h3 id="nx-에이전트분산-작업-실행"><a href="https://nx.dev/ci/features/distribute-task-execution">Nx 에이전트(분산 작업 실행)</a></h3>
<p>Nx 에이전트는 여러 머신에 작업을 분산하여 CI 실행을 최적화할 수 있는 또 다른 중요한 구성 요소입니다.</p>
<p>✔️ 작업 분배: 메인 CI 머신은 Nx Cloud에서 제공하는 에이전트 머신으로 작업을 전송합니다. Nx Cloud는 어떤 작업을 동시에 실행할 수 있는지 지능적으로 판단하여 에이전트에 분배하여 유휴 시간을 최소화합니다.</p>
<p>✔️ 종속성 관리: Nx Cloud는 종속성에 따라 작업이 올바른 순서로 실행되도록 합니다.</p>
<p>✔️ 결과 수집: 에이전트 머신의 결과가 메인 머신으로 다시 수집되어 모든 작업이 메인 머신에서 실행된 것처럼 보이게 합니다.</p>
<p>✔️ <a href="https://nx.dev/ci/features/dynamic-agents">동적 스케일링</a>: 에이전트 머신의 수와 크기는 PR의 규모 또는 특정 프로젝트 요구 사항에 따라 동적으로 조정할 수 있습니다.</p>
<pre><code class="language-yaml">// .nx/workflows/dynamic-changesets.yaml
distribute-on:
  small-changeset: 3 linux-medium-js
  medium-changeset: 6 linux-medium-js
  large-changeset: 10 linux-medium-js

// .github/workflows/main.yaml
...
jobs:
  - job: main
    displayName: Main Job
    ...
    steps:
      - checkout
      - run: npx nx-cloud start-ci-run --distribute-on=&quot;.nx/workflows/dynamic-changesets.yaml&quot; --stop-agents-after=&quot;e2e-ci&quot;
      - ...</code></pre>
<p>PR의 크기를 결정하기 위해 Nx Cloud는 <a href="https://nx.dev/ci/features/affected">영향을 받는 프로젝트</a> 수와 워크스페이스의 총 프로젝트 수 간의 관계를 계산합니다. 그런 다음 소형, 중형 또는 대형의 세 가지 범주 중 하나에 할당합니다.</p>
<p><img src="https://miro.medium.com/v2/resize:fit:720/format:webp/1*3F-HPnhw1e9qht4qPOAY8w.png" alt="">
<a href="https://monorepo.tools/">https://monorepo.tools/</a></p>
<p>이제 리포지토리의 작은 비율에 영향을 미치는 PR은 3개의 에이전트에서 실행되고, 중간 규모의 PR은 6개의 에이전트를 사용하며, 대규모 PR은 10개의 에이전트를 사용하게 됩니다. 이 기능은 대규모 PR에 필요한 고성능을 유지하면서 소규모 PR의 비용을 절감하는 데 도움이 됩니다.</p>
<p><img src="https://miro.medium.com/v2/resize:fit:720/format:webp/1*HNbFcQdK9pU0cWV86BzEZg.png" alt="">
<a href="https://nx.dev/ci/features/distribute-task-execution">https://nx.dev/ci/features/distribute-task-execution</a></p>
<p><img src="https://miro.medium.com/v2/resize:fit:720/format:webp/1*sSE6qql9LPdtmajznYR70w.png" alt="">
<a href="https://nx.dev/concepts/mental-model#distributed-task-execution">https://nx.dev/concepts/mental-model#distributed-task-execution</a></p>
<h3 id="분산-캐싱-및-작업-실행의-이점">분산 캐싱 및 작업 실행의 이점</h3>
<p>✔️ 더 빠른 CI 파이프라인: 특히 대규모 프로젝트의 경우 CI 실행 시간을 크게 단축합니다.</p>
<p>✔️ 향상된 개발자 경험: 개발자 컴퓨터에서 더 빠르게 빌드하고 테스트할 수 있습니다.</p>
<p>✔️ 간소화된 설정: 작업 배포에 필요한 구성이 최소화됩니다.</p>
<p>✔️ 확장성: 프로젝트의 필요에 따라 리소스를 동적으로 확장할 수 있습니다.</p>
<p>💡 Nx Cloud는 프로젝트 종속성, 작업 실행 시간(예상 또는 과거), 사용할 수 있는 에이전트 리소스를 고려하여 작업 배포에 정교한 알고리즘을 사용합니다. 이는 총 실행 시간을 최소화하고 에이전트 간에 부하를 분산하는 것을 목표로 합니다. 구체적인 알고리즘은 다양할 수 있지만 휴리스틱과 최적화 기법을 사용하는 경우가 많습니다.</p>
<p>이제 우리가 배운 지식을 테스트하고 연습을 시작할 때입니다! 🚧</p>
<h2 id="nx-핸즈온">NX 핸즈온</h2>
<h3 id="nx-로컬-실행">Nx 로컬 실행</h3>
<p>🔳 설치</p>
<p>✔️ Nx CLI는 <code>npm install -g nx</code>를 사용하여 <a href="https://nx.dev/getting-started/installation#installing-nx-globally">전역적</a>으로 설치할 수 있습니다. 이렇게 하면 <code>nx</code> 명령을 바로 사용할 수 있습니다.</p>
<p>✔️ 또는 <code>npx nx</code>를 사용하여 전역 설치 없이 Nx 명령을 실행할 수 있습니다.</p>
<p>🔳 워크스페이스 만들기</p>
<p>✔️ 특정 프리셋(예: 리액트, 앵귤러, 노드)이 있는 새 Nx 워크스페이스는 <code>npx create-nx-workspace@latest</code>를 사용하여 생성할 수 있습니다.</p>
<p>✔️ <code>npx nx init</code>을 실행하여 <a href="https://nx.dev/getting-started/installation#install-nx-in-a-nonjavascript-repo">기존</a> 프로젝트에 Nx 기능을 추가할 수 있습니다.</p>
<p>🔳 <a href="https://nx.dev/nx-api">프로젝트 및 라이브러리 생성</a>: 사전 설정된 구성의 프로젝트 및 라이브러리는 <code>nx g @nx/react:app my-new-app</code> 와 같은 <a href="https://nx.dev/nx-api/react/generators">생성기</a>를 사용하여 생성할 수 있습니다.</p>
<p>🔳 작업 실행</p>
<p>✔️ Nx 작업은 기존 <code>package.json</code> 스크립트에서 생성하거나, <a href="https://nx.dev/concepts/inferred-tasks">툴링 구성 파일에서 유추</a>하거나, <code>project.json</code> 파일에 정의할 수 있습니다. Nx는 이 세 가지 소스를 모두 결합하여 특정 <a href="https://nx.dev/reference/project-configuration">프로젝트</a>의 작업을 식별합니다.</p>
<pre><code class="language-json">// https://nx.dev/reference/project-configuration

...
&quot;targets&quot;: {
    &quot;test&quot;: {
        &quot;inputs&quot;: [&quot;default&quot;, &quot;^production&quot;],
            &quot;outputs&quot;: [],
            &quot;dependsOn&quot;: [&quot;build&quot;],
            &quot;executor&quot;: &quot;@nx/jest:jest&quot;,
            &quot;options&quot;: {}
    },
...</code></pre>
<p>✔️ 작업을 실행하기 위해 Nx는 다음 구문을 사용합니다.</p>
<p><img src="https://miro.medium.com/v2/resize:fit:720/format:webp/1*u1LIEY-ML9Ay5lzYYf1RVQ.png" alt="">
<a href="https://nx.dev/features/run-tasks">https://nx.dev/features/run-tasks</a></p>
<p>✔️ <a href="https://nx.dev/nx-api/nx/documents/run-many"><code>run-many</code></a> 명령을 사용하여 <a href="https://nx.dev/features/run-tasks#run-tasks-for-multiple-projects">여러 프로젝트</a>에 대한 작업을 실행할 수도 있습니다.</p>
<pre><code class="language-bash">npx nx run-many -t build lint test</code></pre>
<p>✔️ <a href="https://nx.dev/nx-api/nx/documents/affected">nx affected</a> 명령은 코드 변경의 영향을 받는 프로젝트에서만 작업을 지능적으로 실행하는 데 사용할 수 있습니다.</p>
<pre><code class="language-bash">npx nx affected -t test</code></pre>
<p>🔳 프로젝트 그래프 활용하기</p>
<p>✔️ <a href="https://nx.dev/nx-api/nx/documents/dep-graph"><code>nx graph</code></a>를 사용하여 <a href="https://nx.dev/features/explore-graph">워크스페이스</a>의 종속성 그래프를 시각화할 수 있습니다.</p>
<p><img src="https://miro.medium.com/v2/resize:fit:720/format:webp/1*KtnbT2p0jVJGo4yOdXGhAA.png" alt="">
<a href="https://nx.dev/features/explore-graph">https://nx.dev/features/explore-graph</a></p>
<p>✔️ 사용할 수 있는 플러그인 및 해당 기능은 <a href="https://nx.dev/nx-api/nx/documents/list"><code>nx list</code></a>에서 확인할 수 있습니다.</p>
<p>🔳 <strong>인기 Nx 플러그인</strong>: Nx는 기능을 확장하기 위해 다양한 공식 및 커뮤니티 플러그인을 제공합니다.</p>
<p>✔️ 공식 Nx 플러그인의 전체 목록은 <a href="https://nx.dev/plugin-registry">https://nx.dev/plugin-registry</a> 에서 확인할 수 있습니다.</p>
<p>✔️ NX를 사용하기 위한 다양한 레시피를 여기에서 찾을 수 있습니다: <a href="https://github.com/nrwl/nx-recipes/tree/main">https://github.com/nrwl/nx-recipes/tree/main</a>.</p>
<p>배포 버전 구성을 진행해 보겠습니다. ☁️</p>
<h2 id="분산-캐싱의-실제-사용">분산 캐싱의 실제 사용</h2>
<p><a href="https://nx.dev/nx-api/nx/generators/connect-to-nx-cloud">연결되면</a> Nx는 자동으로 원격 캐싱을 사용하기 시작합니다. <code>nx build my-app</code>와 같은 명령이 실행되면 Nx가 먼저 클라우드 캐시를 확인합니다. 결과가 발견되면 로컬 빌드를 건너뛰고 다운로드하여 비교한 후 사용할 수 있습니다.</p>
<p>🔳 CI 파이프라인에서 분산 작업 실행을 활성화합니다.</p>
<p>✔️ Nx 명령을 실행하기 전에 <a href="https://nx.dev/ci/reference/nx-cloud-cli#npx-nxcloud-startcirun"><code>nx-cloud start-ci-run</code></a> 명령을 CI 구성에 추가해야 합니다.</p>
<p>✔️ 사용할 에이전트의 수와 유형(예: <code>--distribute-on=“4 linux-medium”</code>)을 지정해야 합니다.</p>
<p>✔️ 그런 다음 일반 Nx 명령(예: <code>nx affected --target=build</code>)을 진행할 수 있습니다. 이러한 작업은 Nx Cloud에 의해 지정된 에이전트에 자동으로 배포됩니다.</p>
<pre><code class="language-yaml"># https://nx.dev/ci/recipes/set-up/monorepo-ci-gitlab
# ... (기타 CI 단계)

- name: Start CI run
run: npx nx-cloud start-ci-run --distribute-on=&quot;4 linux-medium&quot;

    - name: Run affected tests
run: nx affected --target=test --parallel

# ... (나머지 CI 단계)</code></pre>
<p>이제 우리가 보고 공부한 모든 것에 대한 의견을 표현할 시간입니다.</p>
<h2 id="기술적-평가">기술적 평가</h2>
<h3 id="인사이트">인사이트</h3>
<p><strong>🔵 Nx의 강점: 대규모 개발 지원</strong></p>
<p><strong>✔️ 효율성 향상</strong>: Nx는 작업을 간소화하고 빌드 프로세스를 최적화하며 병렬 실행을 지원하여 개발자의 생산성을 크게 향상시킵니다.</p>
<p><strong>✔️ 견고한 아키텍처</strong>: DAG와 토폴로지 정렬을 사용하여 올바른 작업 순서를 보장하고 순환 종속성을 방지합니다.</p>
<p><strong>✔️ 풍부한 생태계</strong>: 다양한 플러그인과 생성기를 통해 다양한 도구 및 프레임워크와의 개발과 통합을 간소화합니다.</p>
<p><strong>✔️ 우수한 문서</strong>: 포괄적인 설명서를 통해 초보자도 Nx를 쉽게 배우고 사용할 수 있습니다.</p>
<p><strong>✔️ Nx 클라우드의 장점</strong>: 분산 캐싱 및 작업 실행(Nx Cloud 사용)은 특히 대규모 프로젝트의 빌드 시간과 리소스 활용도를 크게 개선합니다.</p>
<p><strong>✔️ 기존 도구와 통합</strong>: Nx는 기존 빌드 툴 및 프레임워크와 통합할 수 있어 원활하게 전환할 수 있습니다.</p>
<p><strong>🔴 Nx의 도전 과제</strong>: 도입 시 고려 사항</p>
<p><strong>✔️ 학습 곡선</strong>: Nx의 개념과 구성 옵션을 익히는 것은 도구를 처음 사용하는 사람들에게는 어려울 수 있습니다.</p>
<p><strong>✔️ 블랙박스 효과</strong>: 코드 생성 기능은 경험이 적은 개발자가 프로젝트의 내부 작동을 이해하기 어렵게 만드는 양날의 검이 될 수 있습니다.</p>
<p><strong>✔️ 제한된 인트로스펙션</strong>: 그래프와 영향을 받는 메커니즘을 제외하면 Nx는 빌드 프로세스에 대한 심층적인 가시성을 제공하지 않아 잠재적으로 디버깅을 방해할 수 있습니다.</p>
<p><strong>✔️ 의견 수렴 구조</strong>: 프로젝트 구조 및 구성에 대한 Nx의 규칙이 기존 팀 관행이나 선호도와 충돌할 수 있습니다.</p>
<p><strong>✔️ 소규모 프로젝트에는 과잉</strong>: 더 간단한 도구로 충분할 수 있는 소규모 프로젝트나 팀에게는 Nx의 모든 기능이 과도할 수 있습니다.</p>
<p><strong>✔️ 두 가지 유형의 그래프 사용</strong>: 프로젝트 그래프(프로젝트 종속성을 나타내는 DAG)와 작업 그래프(역시 작업 종속성을 나타내는 DAG)를 사용합니다.</p>
<p>Nx의 효과에 대한 보다 폭넓은 관점을 얻기 위해 실제 프로젝트에서 어떻게 사용되고 있는지 살펴보고 NX 사용자이기도 한 커뮤니티의 인사이트를 수집해 보겠습니다.</p>
<h3 id="실제-인사이트-야생에서의-nx">실제 인사이트: 야생에서의 Nx</h3>
<p>Nx에 대한 균형 잡힌 관점을 얻기 위해 활발한 온라인 커뮤니티와 Nx GitHub 이슈 트래커를 모두 살펴봤습니다. 그 결과를 소개합니다.</p>
<p><strong>1️⃣ 커뮤니티 피드백(Reddit)</strong></p>
<p>🔳 Reddit 스레드 링크</p>
<p>🔸 <a href="https://www.reddit.com/r/Frontend/comments/16spnvx/would_you_recommend_nx/">“NX를 추천하시겠습니까?”</a></p>
<p>🔸 <a href="https://www.reddit.com/r/javascript/comments/pfb6x2/askjs_experiences_with_nx_for_javascript_monorepo/">“AskJS: 자바스크립트 모노레포용 Nx에 대한 경험”</a></p>
<p>🔸 <a href="https://www.reddit.com/r/Angular2/comments/x7jk8q/whats_your_biggest_gripes_with_nrwl_nx/">“NRWL NX의 가장 큰 불만은 무엇인가요?”</a></p>
<p>🔸 <a href="https://www.reddit.com/r/reactjs/comments/yhzf3f/nx_vs_turborepo_concerned_about_betting_on_either/">“NX vs Turborepo? 어느 쪽에 베팅할지 고민 중입니다.”</a></p>
<p>🔳 피드백 요약</p>
<p>➕ Nx는 지능형 작업 오케스트레이션, 종속성 관리 및 코드 생성을 통해 개발을 간소화하고 생산성을 높이는 대규모 엔터프라이즈급 모노레포에서 빛을 발합니다. 특히 Angular 개발자는 Nx의 원활한 통합 및 자동화 기능을 높이 평가합니다.</p>
<p>➖ 하지만 사용자들은 잠재적인 단점도 지적합니다. 특히 유연성이나 더 간단한 학습 환경을 원하는 사용자에게는 Nx의 고정된 구조와 높은 학습 곡선이 장애물이 될 수 있습니다. 일부 사용자는 소규모 프로젝트에 Nx가 과하다고 생각하거나 Git 관리 및 상업적 지원에 대한 우려를 표명하기도 합니다.</p>
<p>2️⃣ 기술적 문제(<a href="https://github.com/nrwl/nx/issues">Nx의 GitHub</a>)</p>
<p>Nx의 GitHub 이슈 트래커를 분석해 보면 반복되는 문제가 드러납니다.</p>
<p>🔻 <a href="https://github.com/nrwl/nx/issues/26798">이슈 #26798</a>: (확장성 제한) Nx는 매우 큰 리포지토리(250개 이상의 라이브러리)에서 프로젝트 그래프를 다시 빌드하는 데 어려움을 겪을 수 있습니다.</p>
<p>🔻 <a href="https://github.com/nrwl/nx/issues/26778">이슈 #26778</a> 및 <a href="https://github.com/nrwl/nx/issues/26771">이슈 #26771</a>: (호환성 문제) 일부 사용자가 e2e 테스트에서 사용자 지정 라이브러리를 사용하거나 Nx CLI로 Next.js 앱을 빌드하는 데 어려움을 겪고 있다고 보고합니다.</p>
<p>🔻 <a href="https://github.com/nrwl/nx/issues/26783">이슈 #26783</a>: (파일 처리 및 구성) 프로젝트 이름 변경/이동 및 복잡한 구성 관리와 관련된 문제도 제기되었습니다.</p>
<p>이러한 인사이트는 프로젝트의 특정 요구 사항과 제약 조건을 고려하는 것이 중요하다는 점을 강조하면서 Nx를 채택하고 사용하는 데 있어 잠재적인 마찰 지점을 강조합니다.</p>
<h3 id="주요-요점-뉘앙스가-있는-강점">주요 요점 뉘앙스가 있는 강점</h3>
<p><strong>🔵 강점</strong></p>
<p>✔️ 대규모의 복잡한 모노레포에 탁월합니다.</p>
<p>✔️ 엔터프라이즈 환경에 이상적입니다.</p>
<p>✔️ 지능형 작업 오케스트레이션 및 빌드 최적화 기능을 제공합니다.</p>
<p>✔️ 프로젝트 간 코드 공유를 촉진합니다.</p>
<p>✔️ 확장성과 생산성을 위한 풍부한 기능을 제공합니다.</p>
<p><strong>🔴 잠재적 단점</strong></p>
<p>✔️ 고정된 구조로 인해 유연성이 제한될 수 있습니다.</p>
<p>✔️ 신규 사용자를 위한 학습 곡선이 가파릅니다.</p>
<p>✔️ 기능이 풍부하지만 소규모 프로젝트에는 과할 수 있습니다.</p>
<p>⚪ 다음과 같은 경우 대안을 고려하세요.</p>
<p>✔️ 프로젝트가 작거나 단순합니다.</p>
<p>✔️ 유연성이 최우선 순위입니다.</p>
<p>✔️ 비용 효율성이 중요합니다.</p>
<p>Nx 탐험은 여기서 끝났지만, 모노레포의 여정은 계속됩니다! Turborepo와 pnpm 작업 공간의 강점, 약점, 이상적인 사용 사례를 Nx와 비교하는 심층 분석을 계속 지켜봐 주세요. 곧 다시 뵙겠습니다! 😍</p>
<h2 id="결론">결론</h2>
<p>Nx에 대해 자세히 알아보면 강력하면서도 미묘한 차이가 있는 툴이라는 것을 알 수 있습니다. 구조화된 접근 방식, 빌드 최적화, 클라우드 기능이 개발을 크게 간소화하는 대규모의 복잡한 모노레포에서 그 강점이 빛을 발합니다.</p>
<p>하지만 만능 솔루션은 아닙니다. 프로젝트 규모가 작거나, 워크플로가 독특하거나, 유연성을 선호하는 팀에게는 Nx의 관습과 학습 곡선이 장애물이 될 수 있습니다.</p>
<p>프로젝트 그래프와 작업 오케스트레이션부터 데몬과 Nx Cloud의 역할에 이르기까지 Nx의 내부 작동을 이해하는 것은 정보에 입각한 결정을 내리는 데 매우 중요합니다. 다른 개발자들의 실제 경험을 통해 긍정적이든 부정적이든 Nx의 실질적인 장점과 잠재적인 단점을 파악할 수 있습니다.</p>
<p>최고의 모노레포 도구는 가장 많은 기능을 갖춘 도구가 아니라 프로젝트의 복잡성, 팀의 워크플로 및 전반적인 개발 목표에 가장 잘 부합하는 도구라는 점을 기억하세요. 여기서 얻은 지식으로 무장한 여러분은 모노레포 숙달의 길로 나아가고 있습니다!</p>
<p>앞으로 예정된 Turborepo 및 pnpm 워크스페이스에 대한 심층 분석도 기대해 주세요.</p>
<p>새로운 글과 새로운 모험으로 다시 만나 뵙겠습니다! ❤️</p>
<p>제 글을 읽어주셔서 감사합니다.</p>
<pre><code class="language-bash">저와 연락하고 싶으신가요?
GitHub에서 저를 찾을 수 있습니다: https://github.com/helabenkhalfallah</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[(번역) 모노레포 인사이트: Nx, Turborepo 그리고 PNPM (3/4 - Turborepo 중점적인)]]></title>
            <link>https://velog.io/@tap_kim/translate-monorepo-insights-nx-turborepo-and-pnpm3</link>
            <guid>https://velog.io/@tap_kim/translate-monorepo-insights-nx-turborepo-and-pnpm3</guid>
            <pubDate>Wed, 19 Feb 2025 12:11:47 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>원문: <a href="https://medium.com/ekino-france/monorepo-insights-nx-turborepo-and-pnpm-3-4-751384b5a6db">Monorepo Insights: Nx, Turborepo, and PNPM (3/4)</a></p>
</blockquote>
<p>오늘날 최고의 모노레포 솔루션의 장단점을 살펴봅시다.</p>
<p>아래 링크를 통해 전체 시리즈를 살펴보세요.</p>
<ul>
<li><a href="https://velog.io/@tap_kim/translate-monorepo-insights-nx-turborepo-and-pnpm">모노레포 인사이트: Nx, Turborepo 그리고 PNPM (1/4)</a></li>
<li><a href="https://velog.io/@tap_kim/translate-monorepo-insights-nx-turborepo-and-pnpm2">모노레포 인사이트: Nx, Turborepo 그리고 PNPM (2/4)</a></li>
<li>모노레포 인사이트: Nx, Turborepo 그리고 PNPM (3/4) (현재 글)</li>
<li><a href="https://velog.io/@tap_kim/translate-monorepo-insights-nx-turborepo-and-pnpm4">모노레포 인사이트: Nx, Turborepo 그리고 PNPM (4/4)</a></li>
</ul>
<h2 id="소개">소개</h2>
<p>이 글은 Nx, PNPM, Turborepo의 기능, 성능, 프로젝트 적합성을 비교하는 시리즈 글 중 하나입니다.</p>
<p>모노레포 관리 및 빌드 시스템의 기초를 다지고 Nx를 철저히 조사했으니, 이제 Turborepo의 기능을 알아보는 데 집중해보겠습니다. ✨</p>
<p>다시 한번 말씀드리지만, 저희의 목표는 개발 워크플로우를 간소화하고 코드 베이스 관리를 개선할 최고의 모노레포 도구를 선발하는 것입니다.</p>
<p>최고의 모노레포 도구가 우승하기를 기원합니다! 우리가 함께 정복할 도전 과제는 다음과 같습니다.</p>
<ul>
<li><a href="#%ED%98%84%EB%AF%B8%EA%B2%BD%EC%9C%BC%EB%A1%9C-%EB%B3%B4%EB%8A%94-Turborepo">현미경으로 보는 Turborepo</a></li>
<li><a href="#%ED%8C%A8%ED%82%A4%EC%A7%80-%EB%B0%8F-%EC%9E%91%EC%97%85-%EA%B7%B8%EB%9E%98%ED%94%84">패키지 및 작업 그래프</a></li>
<li><a href="#%EB%A1%9C%EC%BB%AC-%EC%BA%90%EC%8B%B1">로컬 캐싱</a></li>
<li><a href="#%EC%9B%90%EA%B2%A9-%EC%BA%90%EC%8B%B1">원격 캐싱</a></li>
<li><a href="#Turborepo%EC%99%80-Nx-%EB%B9%84%EA%B5%90">Turborepo와 Nx 비교</a></li>
<li><a href="#Turborepo-%ED%95%B8%EC%A6%88%EC%98%A8">Turborepo 핸즈온</a></li>
<li><a href="#Turborepo-%EB%A1%9C%EC%BB%AC-%EC%84%A4%EC%B9%98">Turborepo 로컬 설치</a></li>
<li><a href="#%EC%9B%90%EA%B2%A9-%EC%BA%90%EC%8B%B1%EC%9D%98-%EC%8B%A4%EC%A0%9C-%EC%A0%81%EC%9A%A9">원격 캐싱의 실제 적용</a></li>
<li><a href="#%EA%B8%B0%EC%88%A0%EC%A0%81-%ED%8F%89%EA%B0%80">기술적 평가</a></li>
<li><a href="#%EB%8B%B9%EC%82%AC%EC%9D%98-%EC%9D%B8%EC%82%AC%EC%9D%B4%ED%8A%B8">당사의 인사이트</a></li>
<li><a href="#%EC%8B%A4%EC%A0%9C-%EC%9D%B8%EC%82%AC%EC%9D%B4%ED%8A%B8-%EC%8B%A4%EC%A0%9C-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C%EC%9D%98-Turborepo">실제 인사이트: 실제 환경에서의 Turborepo</a></li>
<li><a href="#%EA%B2%B0%EB%A1%A0">결론</a></li>
</ul>
<p>다음 단계가 궁금하신가요? 함께 알아보죠! 🚀</p>
<h2 id="현미경으로-보는-turborepo">현미경으로 보는 Turborepo</h2>
<h3 id="패키지-및-작업-그래프">패키지 및 작업 그래프</h3>
<p>🔳 <code>Turborepo</code>는 <a href="https://turbo.build/repo/docs/core-concepts/package-and-task-graph#package-graph">패키지 그래프(Package Graph)</a>와 <a href="https://turbo.build/repo/docs/core-concepts/package-and-task-graph#task-graph">작업 그래프(Task Graph)</a>를 모두 활용하여 모노레포 내에서 작업을 효율적으로 관리하고 실행합니다.</p>
<p>🔳 <a href="https://vercel.com/blog/finishing-turborepos-migration-from-go-to-rust">패키지 그래프</a>는 <code>package.json</code> 종속성을 분석하여 <a href="https://turbo.build/repo/docs/core-concepts/internal-packages">내부 패키지(Internal Package)</a> 간의 관계를 자동으로 매핑합니다.</p>
<p><img src="https://miro.medium.com/v2/resize:fit:720/format:webp/0*JRIxCaiMnL83IAmb" alt="https://vercel.com/blog/finishing-turborepos-migration-from-go-to-rust"></p>
<div style="width:100%; text-align: center;">
  <a href="https://vercel.com/blog/finishing-turborepos-migration-from-go-to-rust" >https://vercel.com/blog/finishing-turborepos-migration-from-go-to-rust</a>
</div>

<p>🔳 <code>package.json</code>에서 내부 패키지를 참조할 때는 NPM에서 외부 종속성을 참조하는 방식과 유사한 워크스페이스별 구문을 사용합니다. 다음 예시를 확인해봅시다.</p>
<pre><code class="language-json">// pnpm: ./apps/web/package.json
{
  &quot;dependencies&quot;: {
    &quot;@repo/ui&quot;: &quot;workspace:*&quot;
  }
}</code></pre>
<p>🔳 패키지 그래프는 <code>작업 그래프</code>의 기초로, 개별 작업이 서로 의존하는 방식을 정의합니다.</p>
<p>🔳 <a href="https://turbo.build/repo/docs/core-concepts/package-and-task-graph#task-graph">작업 그래프</a>의 노드는 작업을 나타내고 간선은 종속성을 나타내는 <a href="https://en.wikipedia.org/wiki/Directed_acyclic_graph">유향 비순환 그래프(DAG)</a>입니다. 이를 통해 올바른 실행 순서를 보장하고 병렬 작업 실행을 가능하게 합니다.</p>
<p><img src="https://miro.medium.com/v2/resize:fit:720/format:webp/0*-81nvKJ5Eqogp24f.jpg" alt="https://ogzhanolguncu.com/blog/monorepo-with-turborepo/"></p>
<div style="width:100%; text-align: center;">
  <a href="https://ogzhanolguncu.com/blog/monorepo-with-turborepo/" >https://ogzhanolguncu.com/blog/monorepo-with-turborepo/
  </a>
</div>

<p><img src="https://miro.medium.com/v2/resize:fit:720/format:webp/1*t0_KUiuGhezv7alS9l463Q.png" alt="https://www.maxpou.fr/blog/turborepo/"></p>
<div style="width:100%; text-align: center;">
  <a href="https://www.maxpou.fr/blog/turborepo/" >https://www.maxpou.fr/blog/turborepo/
  </a>
</div>

<p>🔳 작업 종속성(Task dependencies)은 <code>turbo.json</code>에 명시적으로 정의되어 있습니다.</p>
<pre><code class="language-json">// turbo.json
{
  &quot;tasks&quot;: {
    &quot;build&quot;: {
      &quot;dependsOn&quot;: [&quot;^build&quot;] // 종속된 모든 작업 공간의 빌드 작업에 따라 작업이 달라집니다.
    }
  }
}</code></pre>
<p><img src="https://miro.medium.com/v2/resize:fit:720/format:webp/0*k8us9IdD42n_OEPf" alt="https://turbo.build/repo/docs/core-concepts/package-and-task-graph#package-graph"></p>
<div style="width:100%; text-align: center;">
  <a href="https://turbo.build/repo/docs/core-concepts/package-and-task-graph#package-graph" >https://turbo.build/repo/docs/core-concepts/package-and-task-graph#package-graph</a>
</div>

<p>🔳 대규모 프로젝트의 경우 <code>Turborepo</code>는 <a href="https://nx.dev/concepts/turbo-and-nx#2-understanding-your-workspace">daemon</a> 프로세스를 사용하여 백그라운드에서 복잡한 프로젝트 그래프를 지능적으로 계산하여 시작 오버헤드를 크게 줄입니다.</p>
<p>🔳 <a href="https://turbo.build/repo/docs/messages/missing-root-task-in-turbo-json#why-this-error-occurred">위상</a> 정렬을 사용하여 작업의 실행 순서를 지정하여 종속성이 충족되도록 합니다.</p>
<blockquote>
<p><em>토폴로지 종속성은 패키지의 종속성이 자체 작업을 실행하기 전에 해당 종속성의 작업을 실행하도록 지정합니다. - <a href="https://turbo.build/repo/docs/messages/missing-root-task-in-turbo-json#why-this-error-occurred">https://turbo.build/repo/docs/messages/missing-root-task-in-turbo-json#why-this-error-occurred</a></em></p>
</blockquote>
<p>🔳 <code>Turborepo</code>는 <a href="https://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm">Tarjan</a>의 알고리즘을 사용하여 작업 그래프에서 순환 종속성을 감지하여 원활하고 오류 없는 빌드 프로세스를 보장합니다.</p>
<pre><code class="language-rust">// https://github.com/vercel/turbo/blob/main/crates/turborepo-graph-utils/src/lib.rs#L29
// https://github.com/vercel/turbo/pull/5566/files

pub fn validate_graph&lt;G: Display&gt;(graph: &amp;Graph&lt;G, ()&gt;) -&gt; Result&lt;(), Error&gt; {
    // 이것은 Go의 dag 라이브러리에 있는 AcyclicGraph.Cycles와 동일합니다.
    let cycles_lines = petgraph::algo::tarjan_scc(&amp;graph)
        .into_iter()
        .filter(|cycle| cycle.len() &gt; 1)
        .map(|cycle| {
            let workspaces = cycle.into_iter().map(|id| graph.node_weight(id).unwrap());
            format!(&quot;\t{}&quot;, workspaces.format(&quot;, &quot;))
        })
        .join(&quot;\n&quot;);

    //  주기가 감지되면 주기 세부 정보와 함께 오류를 반환합니다.
    if !cycles_lines.is_empty() {
        return Err(Error::CyclicDependencies(cycles_lines));
    }

    for edge in graph.edge_references() {
        if edge.source() == edge.target() {
            let node = graph
                .node_weight(edge.source())
                .expect(&quot;edge pointed to missing node&quot;);
            return Err(Error::SelfDependency(node.to_string()));
        }
    }

    Ok(())
}</code></pre>
<p>🔳 <code>Turborepo</code>는 깊이 우선 검색(DFS)을 사용하여 변경 사항의 영향을 받는 작업을 식별하여 불필요한 리빌드의 필요성을 최소화하고 개발 주기를 단축합니다.</p>
<pre><code class="language-rust">// https://github.com/vercel/turbo/blob/main/crates/turborepo-repository/src/package_graph/mod.rs#L371

match direction {
  petgraph::Direction::Outgoing =&gt; depth_first_search(&amp;self.graph, indices, visitor),
  petgraph::Direction::Incoming =&gt; {
    depth_first_search(Reversed(&amp;self.graph), indices, visitor)
  }
};</code></pre>
<p>🔳 <code>Turborepo</code>는 <a href="https://en.wikipedia.org/wiki/Floyd%E2%80%93Warshall_algorithm">플로이드-워셜</a> 알고리즘을 활용하여 각 작업에 대한 최단 종속성 체인을 결정합니다. 이를 통해 중복 작업을 최소화하여 빌드 시간을 최적화합니다.</p>
<pre><code class="language-rust">// https://github.com/vercel/turbo/blob/main/crates/turborepo-lib/src/engine/mod.rs#L160
impl Engine&lt;Built&gt; {
    /// 지정된 패키지의 다음 패키지에 종속된 작업만 포함하는 `Engine` 인스턴스를 생성합니다.
    /// 작업만 포함하는 인스턴스를 생성합니다. 이 기능은 감시 모드에서 유용합니다.
    /// 작업 그래프의 일부만 다시 실행해야 하는 경우에 유용합니다.
    pub fn create_engine_for_subgraph(
        &amp;self,
        changed_packages: &amp;HashSet&lt;PackageName&gt;,
    ) -&gt; Engine&lt;Built&gt; {
        let entrypoint_indices: Vec&lt;_&gt; = changed_packages
            .iter()
            .flat_map(|pkg| self.package_tasks.get(pkg))
            .flatten()
            .collect();

        // 그래프를 뒤집는 이유는 엔트리포인트 작업의 *종속성*을 원하기 때문입니다.
        let mut reversed_graph = self.task_graph.clone();
        reversed_graph.reverse();

        // 이것은 `O(V^3)`이므로 이론적으로는 병목 현상입니다. 각 진입점 작업에 대해 dijkstra의
        // 알고리즘을 실행하는 것이 잠재적으로 더 빠를 수 있습니다.
        let node_distances = petgraph::algo::floyd_warshall::floyd_warshall(&amp;reversed_graph, |_| 1)
            .expect(&quot;no negative cycles&quot;);

        let new_graph = self.task_graph.filter_map(
            |node_idx, node| {
                if let TaskNode::Task(task) = &amp;self.task_graph[node_idx] {
                    // 영구적이지 않은 작업만 포함하려고 합니다.
                    let def = self
                        .task_definitions
                        .get(task)
                        .expect(&quot;task should have definition&quot;);

                    if def.persistent {
                        return None;
                    }
                }

...</code></pre>
<p>💡 <strong>전문가 팁</strong>: Turborepo는 작업 그래프를 시각화하기 위해 <a href="https://turbo.build/repo/docs/reference/run#--graph-file-type"><code>--graph &lt;file type&gt;</code></a> 옵션을 제공하여 모노레포의 빌드 구조에 대한 통찰력을 얻을 수 있습니다.</p>
<pre><code class="language-bash">turbo run build --graph
turbo run build test lint --graph=my-graph.svg</code></pre>
<p>이제 캐시 메커니즘에 대해 자세히 알아보겠습니다. 🌟</p>
<h3 id="로컬-캐싱">로컬 캐싱</h3>
<p><code>Turborepo</code>는 작업 입력(종속성, 환경 변수, 소스 코드 포함)을 해싱하고 빌드 출력을 저장합니다. 즉, 아무것도 변경하지 않았다면 작업이 대부분 즉시 완료됩니다!</p>
<p><img src="https://miro.medium.com/v2/resize:fit:720/format:webp/0*TjtmX-dHiz5NjFDZ" alt="https://vercel.com/blog/finishing-turborepos-migration-from-go-to-rust#hashing-tasks-for-the-run-command"></p>
<p>🔳 <code>Turborepo</code>는 <code>.turbo/cache</code> 디렉터리에 결과를 로컬 캐시합니다.</p>
<p>🔳 <code>Turborepo</code>는 두 가지 수준에서 캐싱을 수행합니다.</p>
<ul>
<li><strong>글로벌</strong>: 전체 레포지토리에 영향을 미치는 변경 사항을 감지합니다.</li>
<li><strong>작업</strong>: 각 작업에 특정한 변경 사항을 식별합니다(컨텍스트에 대한 글로벌 해시 통합).</li>
</ul>
<p><img src="https://miro.medium.com/v2/resize:fit:720/format:webp/0*uvNu3b76Fkdm0-4h" alt="https://turbo.build/repo/docs/crafting-your-repository/caching"></p>
<div style="width:100%; text-align: center;">
  <a href="https://turbo.build/repo/docs/crafting-your-repository/caching" >https://turbo.build/repo/docs/crafting-your-repository/caching</a>
</div>

<p>💡 Turborepo는 기본적으로 캐시를 활성화합니다.</p>
<h3 id="원격-캐싱"><a href="https://turbo.build/repo/docs/crafting-your-repository/caching#remote-caching">원격 캐싱</a></h3>
<p>🔳 원격 및 공유 캐시 서버(예: Vercel의 원격 캐싱 또는 사용자 지정 솔루션)가 빌드 결과물을 저장하는 중앙 레포지토리로 지정됩니다.</p>
<p><img src="https://miro.medium.com/v2/resize:fit:720/format:webp/0*mK79HyXPO6iPVCOn" alt="https://turbo.build/repo/docs/core-concepts/remote-caching#a-single-shared-cache"></p>
<div style="width:100%; text-align: center;">
  <a href="https://turbo.build/repo/docs/core-concepts/remote-caching#a-single-shared-cache" >https://turbo.build/repo/docs/core-concepts/remote-caching#a-single-shared-cache</a>
</div>

<p>🔳 작업의 모든 입력(코드, 종속성, 환경 등)을 나타내는 고유 해시는 작업을 실행하기 전에 <code>Turborepo</code>에 의해 계산됩니다. 그런 다음 이 해시를 원격 캐시에 이미 저장된 해시와 비교합니다.</p>
<blockquote>
<p><em>이러한 해시를 캐시에 사용하기 위해 전체 폴더를 하나의 파일로 압축하는 특수 파일 형식인 tar 파일에 작업의 출력을 저장합니다. 해당 파일은 로컬 파일 시스템과 이 작업 해시로 인덱싱된 <a href="https://vercel.com/docs/monorepos/remote-caching">Vercel 원격 캐시</a>에 모두 저장됩니다. - <a href="https://vercel.com/blog/finishing-turborepos-migration-from-go-to-rust#hashing-tasks-for-the-run-command">https://vercel.com/blog/finishing-turborepos-migration-from-go-to-rust#hashing-tasks-for-the-run-command</a></em></p>
</blockquote>
<p>🔳 일치하는 파일이 발견되면 캐시된 아티팩트가 밀리초 내에 다운로드되므로 작업을 다시 실행할 필요가 없어 귀중한 시간을 절약할 수 있습니다.</p>
<p>🔳 일치하는 항목이 발견되지 않으면 작업이 정상적으로 실행되고 그 출력은 다른 사람들이 나중에 사용할 수 있도록 원격 캐시에 업로드됩니다.</p>
<blockquote>
<p><em>작업을 실행할 때가 되면 먼저 이 작업 해시를 생성하고 파일 시스템이나 원격 캐시에 있는지 확인합니다. 파일시스템이나 원격 캐시에 있으면 작업의 출력을 밀리초 단위로 복원합니다. 그렇지 않으면 작업을 실행하고 다음 작업을 위해 출력을 캐시에 저장합니다. - <a href="https://vercel.com/blog/finishing-turborepos-migration-from-go-to-rust#hashing-tasks-for-the-run-command">https://vercel.com/blog/finishing-turborepos-migration-from-go-to-rust#hashing-tasks-for-the-run-command</a></em></p>
</blockquote>
<p>이해를 돕기 위해 <code>NX</code>와 <code>Turborepo</code>의 차이점을 자세히 살펴보겠습니다. 🌟</p>
<h3 id="turborepo와-nx-비교">Turborepo와 Nx 비교</h3>
<p><a href="https://monorepo.tools/">monorepo.tools</a>의 인사이트와 이전에 본 내용을 바탕으로 <code>NX</code>와 <code>Turborepo</code>를 종합적으로 비교해 볼 수 있습니다.</p>
<p><img src="https://miro.medium.com/v2/resize:fit:720/format:webp/1*E-yVtSWuSeUdwBmw9998Ww.png" alt="Turborepo vs. Nx (Image by the author)"></p>
<div style="width:100%; text-align: center;">
  Turborepo vs. Nx (Image by the author)
</div>

<p>🔳 NX</p>
<ul>
<li>포괄적인 모노레포 관리</li>
<li>엄격한 툴링 일관성</li>
<li>내장 코드 생성 및 플러그인</li>
<li>더 많은 프로젝트 제어</li>
<li>더 가파른 학습 곡선</li>
</ul>
<p>🔳 Turborepo</p>
<ul>
<li>초고속 빌드</li>
<li>유연한 툴링</li>
<li>더 간편한 학습</li>
<li>Vercel 통합</li>
<li>내장 코드 생성 및 플러그인 부족</li>
</ul>
<p>📌 NX는 더 많은 제어 기능을 갖춘 완전한 기능의 모노레포 환경을 제공하는 반면, <code>Turborepo</code>는 빌드 속도와 단순성을 우선시합니다.</p>
<p>📌 모노레포에 최적화된 툴을 선택하는 것은 민감한 결정이며 정답이 있는 것이 아닙니다. 프로젝트 규모와 복잡성, 팀 구조와 협업, 원하는 제어 수준, 성능 요구 사항, 추가 툴링 요구 사항 등을 면밀히 평가하여 <code>Nx</code>와 <code>Turborepo</code>를 적절히 조합해야 합니다.</p>
<p>이제 <code>Turborepo</code>의 기본 사항을 살펴보았으니 이제 지식을 실제로 적용해 볼 차례입니다. 🚀</p>
<h2 id="turborepo-핸즈온">Turborepo 핸즈온</h2>
<h3 id="turborepo-로컬-설치">Turborepo 로컬 설치</h3>
<p>✔️ <code>turbo</code>는 <a href="https://turbo.build/repo/docs/getting-started/installation#global-installation">글로벌</a> 또는 <a href="https://turbo.build/repo/docs/getting-started/installation#repository-installation">로컬</a>로 설치할 수 있습니다.</p>
<pre><code class="language-bash">pnpm add turbo --global
pnpm add turbo --save-dev --ignore-workspace-root-check</code></pre>
<p>✔️ <a href="https://turbo.build/repo/docs/crafting-your-repository/structuring-a-repository#getting-started">새 프로젝트를 시작</a>하거나 <a href="https://turbo.build/repo/docs/getting-started/installation#start-with-an-example">예제</a>를 사용하여 시작할 수 있습니다.</p>
<pre><code class="language-bash">% npx create-turbo@latest

다음 패키지를 설치해야 합니다:
create-turbo@2.0.6
계속 진행하시겠습니까? (y)

&gt;&gt;&gt; 새 Turborepo를 생성했습니다:

애플리케이션 패키지

- apps/docs
- apps/web

라이브러리 패키지

- packages/eslint-config
- packages/typescript-config
- packages/ui

&gt;&gt;&gt; 성공! 새로운 Turborepo가 준비되었습니다.

시작하려면:

- 원격 캐싱 활성화(권장): pnpm dlx 터보 로그인
  - 자세히 알아보기: https://turbo.build/repo/remote-cache

- Turborepo로 명령을 실행합니다:
  - pnpm run build: 모든 앱과 패키지 빌드
  - pnpm run dev: 모든 앱과 패키지 개발
  - pnpm run lint: 모든 앱 및 패키지 린트
- 명령을 두 번 실행하여 캐시</code></pre>
<p>✔️ <a href="https://turbo.build/repo/docs/crafting-your-repository/structuring-a-repository#anatomy-of-a-workspace">작업 공간의 구조는</a>는 다음과 같습니다.</p>
<p><img src="https://miro.medium.com/v2/resize:fit:640/format:webp/1*C0UhG9JMdCRyf8ypjlExXA.png" alt="Turborepo workspace anatomy (Image by the author)"></p>
<div style="width:100%; text-align: center;">
  Turborepo workspace anatomy (Image by the author)
</div>

<p><code>Turborepo</code>가 작업 공간 관리자(이 경우 PNPM(<code>pnpm-workspace.yaml</code>)) <a href="https://turbo.build/repo/docs/crafting-your-repository/structuring-a-repository">위</a>에 추가되어 있는 것을 볼 수 있습니다.</p>
<pre><code class="language-bash">packages:
  - &quot;apps/*&quot;
  - &quot;packages/*&quot;</code></pre>
<p><code>Turborepo</code>의 목적은 모노레포와 관련된 <strong>작업</strong>을 관리하는 것으로, 보다 유연하고 전문적인 역할(<a href="https://turbo.build/repo/docs/crafting-your-repository/structuring-a-repository#root-turbojson"><code>turbo.json</code></a>)을 제공합니다.</p>
<pre><code class="language-json">{
  &quot;$schema&quot;: &quot;https://turbo.build/schema.json&quot;,
  &quot;tasks&quot;: {
    &quot;build&quot;: {
      &quot;dependsOn&quot;: [&quot;^build&quot;],
      &quot;inputs&quot;: [&quot;$TURBO_DEFAULT$&quot;, &quot;.env*&quot;],
      &quot;outputs&quot;: [&quot;.next/**&quot;, &quot;!.next/cache/**&quot;]
    },
    &quot;lint&quot;: {
      &quot;dependsOn&quot;: [&quot;^lint&quot;]
    },
    &quot;dev&quot;: {
      &quot;cache&quot;: false,
      &quot;persistent&quot;: true
    }
  }
}</code></pre>
<p>기본 생성된 타입스크립트(<code>tsconfig.json</code>)와 ESLint(<a href="https://turbo.build/repo/docs/reference/eslint-config-turbo"><code>eslint-config</code></a>)도 볼 수 있습니다.</p>
<blockquote>
<p><em><a href="https://www.npmjs.com/package/eslint-config-turbo"><code>eslint-config-turbo</code> 패키지는 코드</a>에 사용된 환경 변수 중 Turborepo의 해싱에 포함되지 않은 환경 변수를 찾는 데 도움이 됩니다. 소스 코드에 사용된 환경 변수 중 <code>turbo.json</code>에 설명되지 않은 환경 변수는 에디터에서 강조 표시되고 오류는 ESLint 출력으로 표시됩니다. - <a href="https://turbo.build/repo/docs/reference/eslint-config-turbo">https://turbo.build/repo/docs/reference/eslint-config-turbo</a></em></p>
</blockquote>
<p>✔️ <code>Turborepo</code>를 사용하여 작업을 <a href="https://turbo.build/repo/docs/crafting-your-repository/configuring-tasks">수정</a>하고 <a href="https://turbo.build/repo/docs/crafting-your-repository/running-tasks">실행</a>하려면 아래와 같이 구성하세요.</p>
<pre><code class="language-json">// main package.json
&quot;scripts&quot;: {
  &quot;build&quot;: &quot;turbo build&quot;,
  &quot;dev&quot;: &quot;turbo dev&quot;,
  &quot;lint&quot;: &quot;turbo lint&quot;,
  &quot;format&quot;: &quot;prettier --write \&quot;**/*.{ts,tsx,md}\&quot;&quot;
},</code></pre>
<p>그런 다음, <code>pnpm dev</code> , <code>pnpm build</code>, <code>pnpm lint</code>, <code>pnpm format</code> 등 명령어를 사용할 수 있습니다.</p>
<p>✔️ 다음을 사용하여 작업 영역의 종속성 그래프를 시각화할 수 있습니다.</p>
<pre><code class="language-bash">turbo run build --graph=my-graph.svg</code></pre>
<p>이 <code>SVG</code>를 생성합니다.</p>
<p><img src="https://miro.medium.com/v2/resize:fit:720/format:webp/1*96EAnwdf3krqlpWMhPfX2Q.png" alt="Turborepo dependency graph (Image by the author)"></p>
<div style="width:100%; text-align: center;">
  Turborepo dependency graph (Image by the author)
</div>

<p><code>NX</code>만큼 고급적이고 상호작용에 유용하지는 않지만 <code>Turborepo</code>는 여전히 사용하기 쉽습니다.</p>
<p>원격 캐싱 구성을 진행해 보겠습니다. 📡</p>
<h3 id="원격-캐싱의-실제-적용">원격 캐싱의 실제 적용</h3>
<p>✔️ 로컬 <code>Turborepo</code>를 원격 캐시에 연결하려면 Vercel 계정으로 <code>Turborepo</code> CLI를 인증해야 합니다.</p>
<pre><code class="language-bash">turbo login
turbo login --sso-team=team-name</code></pre>
<p>이는 패키지를 게시하기 위해 로그인할 때 NPM과 유사합니다. 😉</p>
<p>✔️ 인증이 완료되면 링크 명령을 실행해야 합니다.</p>
<pre><code class="language-bash">turbo link</code></pre>
<p>이제 캐시 아티팩트가 로컬 및 원격 캐시에 모두 저장됩니다.</p>
<blockquote>
<p><em>그리고 동일한 빌드를 다시 실행합니다. 정상적으로 작동하면 <code>turbo</code>가 로컬에서 작업을 실행하지 않습니다. 대신 원격 캐시에서 로그와 아티팩트를 다운로드하여 재생합니다. - <a href="https://turbo.build/repo/docs/core-concepts/remote-caching">https://turbo.build/repo/docs/core-concepts/remote-caching</a></em></p>
</blockquote>
<p>✔️ Vercel 대신 <a href="https://turbo.build/repo/docs/core-concepts/remote-caching#self-hosting">다른 원격 캐시 호스팅 제공업체</a>를 사용할 수도 있습니다.</p>
<pre><code class="language-bash">turbo login --api=&quot;https://my-server.example.com/api&quot;
turbo link --api=&quot;https://my-server-example.com&quot;
turbo run build --api=&quot;https://my-server.example.com&quot; --token=&quot;xxxxxxxxxxxxxxxxx&quot;</code></pre>
<p>API의 OpenAPI 사양은 <a href="https://turbo.build/api/remote-cache-spec">여기</a>에서 확인할 수 있습니다.</p>
<p>이처럼 <code>Turborepo</code>를 다루는 것은 간단합니다! 이제부터는 <code>Turborepo</code>를 보고 공부한 것들에 대한 의견을 말할 때가 된거 같습니다.📌.</p>
<h2 id="기술적-평가">기술적 평가</h2>
<h3 id="당사의-인사이트">당사의 인사이트</h3>
<p>🔳 <strong>Turborepo는 탁월합니다.</strong></p>
<ul>
<li>자바스크립트 및 타입스크립트 모노레포의 빌드 시간이 최적화됩니다.</li>
<li>모노레포 설정 및 구성이 간소화됩니다.</li>
<li>효율적인 공유 캐싱으로 팀의 역량을 강화합니다.</li>
<li>Vercel의 생태계와 원활하게 통합됩니다.</li>
</ul>
<p>🔳 <strong>다음과 같은 경우 Turborepo를 고려해야 합니다.</strong></p>
<ul>
<li>빌드 속도와 개발자 생산성이 최우선 순위인 경우.</li>
<li>보다 간소화되고 집중된 도구 세트가 선호됩니다.</li>
<li>프로젝트에서 주로 자바스크립트 또는 타입스크립트를 사용하는 경우.</li>
</ul>
<p>🔳 <strong>다음과 같은 프로젝트에는 Turborepo가 최적의 솔루션이 아닐 수 있습니다.</strong></p>
<ul>
<li>빌드 프로세스 자체 외에 다양한 기본 제공 기능이 필요한 경우.</li>
<li>프로젝트 간 경계를 강력하게 설정해야 하는 경우.</li>
<li>모노레포 내에서 여러 프로그래밍 언어를 사용하는경우.</li>
</ul>
<p><code>Turborepo</code>에 대한 균형 잡힌 시각을 위해 다양한 프로젝트에서의 실제 사용 사례를 살펴보고, 직접 테스트 해본 커뮤니티 의견을 모아 보겠습니다.</p>
<h3 id="실제-인사이트-실제-환경에서의-turborepo">실제 인사이트: 실제 환경에서의 Turborepo</h3>
<p>Turborepo의 인기는 단순한 과대 광고가 아니라 다양한 개발 환경에서 빠르게 주목받으며 실제 환경에서 그 진가를 입증하고 있습니다.</p>
<p>🔳 다음과 같은 많은 기업이 프로덕션 워크플로우에 <code>Turborepo</code>를 통합했습니다.</p>
<ul>
<li>Netflix</li>
<li>Datadog</li>
<li>Astro</li>
<li>Egghead</li>
<li>Dito</li>
</ul>
<p><a href="https://github.com/vercel/turbo/discussions/103">전체 목록</a>은 여기에서 확인할 수 있습니다.</p>
<p>🔳 Turborepo의 속도, 사용 편의성 및 간소화된 설정이 자주 언급됩니다.</p>
<pre><code class="language-bash">바닐라 측면을 완전히 무시하기

NX를 사용해야 하는 이유

- 현재 NX가 인수한 Lerna에서 마이그레이션하고 있습니다. 마이그레이션 경로는 매우 간단합니다(멀티 레포지터리가 엉망인 대규모 내부 사이트에서 이 작업을 수행했는데, 레포지토리의 아주 오래된 코드가 몇 가지 문제를 일으켰지만, 예상보다 쉬웠습니다).
- 워크플로에는 시간이 지날수록 점점 더 많은 라이브러리가 생성되므로 새 라이브러리를 생성하고 배포 중인 앱에 연결하기 위한 표준화된 프로세스가 필요합니다.
- 모노레포 구조는 이미 매우 복잡하며, 무엇이 어디로 가는지 더 쉽게 추적할 수 있는 견고한 구조와 시각화가 필요합니다.
모노레포에 적합한 DX/플러그인을 원합니다.
일화적이지만, NX의 캐싱이 Turborepo보다 훨씬 더 성능이 좋다는 것을 알았습니다 - 플랫폼에 따라 다를 수 있으므로 (개발 머신으로 M1 Max) 큰 소금 한 알을 가지고 가져가십시오.
NX는 꽤나 많은 사람들이 사용하고 있으므로 레거시 코드가 어떻게 유지되는지 알기 때문에 오랫동안 사용할 수 있습니다.

Turborepo를 사용하는 이유

단순함이 핵심

- 프로젝트가 비교적 간단하고, 최소한의 구성이나 강제 구조로 모노레포 라이브러리도 간단하기를 원합니다.
- 일반적인 CI/CD 플랫폼과 간단한 캐싱 통합을 원하는 경우
- 이미 Vercel 에코시스템에 연결된 경우
- Vercel은 FB/Meta의 리액트 팀으로부터 직접 지원을 받고 있으므로 당분간은 계속 사용할 수 있습니다.

요약하자면, Vercel의 모든 제품과 마찬가지로 여러분의 프로젝트가 Vercel 제품의 틀 안에 들어맞는다면, Vercel은 여러분에게 가장 적합하고 쉬운 솔루션이 될 것입니다. 그렇지 않은 경우 해결 방법을 찾는 것은 정말 귀찮은 일이 될 것입니다. Turborepo 문서를 읽고 (훨씬 더 큰) NX 문서를 훑어보세요. Turborepo에서 필요한 것을 찾지 못하면 NX가 더 나은 선택일 수 있지만, 이는 작업 중인 프로젝트, 팀 등에 대한 지식이 전혀 없는 상태에서 하는 말입니다. 일반적으로 당면한 요구 사항을 가장 잘 지원하면서 앞으로 나아갈 수 있는 합리적인 공간을 제공하는 솔루션을 고수하세요(이것이 가장 좋은 옵션이 될 수 있습니다). 미리 최적화하지 말고, 6개월, 1년, 2년 후 제품의 위치를 파악하고 거기서부터 더 많은 것을 검토하세요.</code></pre>
<div style="width:100%;">
  관련 링크: <a href="https://www.reddit.com/r/reactjs/comments/yhzf3f/comment/iugl6j3/?utm_source=share&utm_medium=web3x&utm_name=web3xcss&utm_term=1&utm_content=share_button" >https://www.reddit.com/r/reactjs/comments/yhzf3f/comment/iugl6j3/?utm_source=share&utm_medium=web3x&utm_name=web3xcss&utm_term=1&utm_content=share_button</a>
</div>

<pre><code class="language-bash">NX와 함께 package.json 구성 스타일을 사용하면 제너레이터와 실행기의 복잡성을 피할 수 있습니다. 매우 빠르고 간단하게 시작할 수 있으며 Turborepo와 매우 유사하지만, 나중에 필요할 때 더 많은 기능을 제공합니다(https://nx.dev/reference/project-configuration).</code></pre>
<div style="width:100%;">
  관련 링크: <a href="https://www.reddit.com/r/reactjs/comments/yhzf3f/comment/iuh76tv/?utm_source=share&utm_medium=web3x&utm_name=web3xcss&utm_term=1&utm_content=share_button" >https://www.reddit.com/r/reactjs/comments/yhzf3f/comment/iuh76tv/?utm_source=share&utm_medium=web3x&utm_name=web3xcss&utm_term=1&utm_content=share_button</a>
</div>

<pre><code class="language-bash">원하는 것은 워크스페이스 지원 및 스크립트 관리 기능이 있는 패키지 관리자입니다(저는 pnpm을 적극 권장합니다).

Turborepo의 주요 목적은 CI 및 대규모 팀 워크 플로우에서 아티팩트를 캐싱하고 스크립트를 병렬로 실행하는 빌드 시간을 줄이는 것이므로이를 위해 pnpm을 사용하고 CI가 너무 많은 시간이 걸리고 중복 작업을 수행하는 경우 Turborepo를 추가하는 것이 좋습니다.

Nx의 주요 목적은 모노레포를 엔지니어링하고 바퀴를 재발명하는 것입니다.</code></pre>
<div style="width:100%;">
  관련 링크: <a href="https://www.reddit.com/r/reactjs/comments/yhzf3f/comment/iuhrswn/?utm_source=share&utm_medium=web3x&utm_name=web3xcss&utm_term=1&utm_content=share_button" >https://www.reddit.com/r/reactjs/comments/yhzf3f/comment/iuhrswn/?utm_source=share&utm_medium=web3x&utm_name=web3xcss&utm_term=1&utm_content=share_button</a>
</div>

<p>🔳 마지막으로 몇 가지 비교 연구를 소개합니다.</p>
<ul>
<li><a href="https://nx.dev/concepts/turbo-and-nx">https://nx.dev/concepts/turbo-and-nx</a></li>
<li><a href="https://moonrepo.dev/docs/comparison">https://moonrepo.dev/docs/comparison</a></li>
</ul>
<p>커뮤니티의 피드백은 저희의 연구 결과를 뒷받침합니다. 더 빠르고 효율적이며 안정적인 빌드를 원하는 개발 팀에게 <code>Turborepo</code>는 게임 체인저입니다. 모든 프로젝트의 요구 사항을 충족하지는 못하지만 속도와 단순성에 중점을 두어 모노레포 환경에서 강력한 경쟁자로 자리매김하고 있습니다. ♨️</p>
<p><code>Turborepo</code>에 대한 심층 분석은 여기서 마무리하지만, 모노레포의 모험은 이제 막 시작되었습니다! Nx 및 <code>Turborepo</code>와 비교하여 강점, 약점, 완벽한 사용 사례를 분석하는 pnpm 작업 공간에 대한 흥미진진한 탐험을 준비하세요. 더 많은 모노레포의 마법을 기대해주세요! ✨</p>
<h2 id="결론">결론</h2>
<p><code>Turborepo</code>는 자바스크립트 및 타입스크립트 모노레포에 특별히 맞춤화된 강력하고 효율적인 빌드 시스템입니다. 지능적인 작업 관리, 혁신적인 캐싱 메커니즘, 원시 속도에 중점을 두어 간소화된 워크플로와 빠른 피드백 루프를 원하는 개발자에게 획기적인 변화를 불러왔습니다.</p>
<p>Netflix, Datadog, 수많은 오픈 소스 프로젝트와 같은 업계 리더들의 실제 채택은 Turborepo의 효과와 확장성을 더욱 확고히 해줍니다. 활발한 커뮤니티의 긍정적인 피드백은 직관적인 설정, 상당한 성능 향상, 전반적으로 긍정적인 개발자 경험을 강조합니다.</p>
<p><code>Turborepo</code>가 모든 프로젝트에 이상적인 것은 아니지만, 간소화된 접근 방식과 빌드 속도에 초점을 맞춘 덕분에 특히 성능을 우선시하고 자바스크립트나 타입스크립트를 사용하는 많은 프로젝트에 매력적인 옵션이 될 수 있습니다.</p>
<p>모노레포의 여정은 여기서 끝나지 않습니다! 다음 시간에는 <code>pnpm</code> 작업 영역에 대해 자세히 알아볼 예정입니다. 이미 <code>pnpm</code>을 사용하고 있다면 <code>Turborepo</code>가 정말 필요할까요?</p>
<p>그때까지 계속 호기심을 갖고 계속 빌드하며 끊임없이 진화하는 현대 개발의 세계를 받아들이세요! ❤️</p>
<p>제 글을 읽어주셔서 감사합니다.</p>
<pre><code class="language-bash">저와 연락하고 싶으신가요?
GitHub에서 저를 찾을 수 있습니다: https://github.com/helabenkhalfallah</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[(번역) 모노레포 인사이트: Nx, Turborepo 그리고 PNPM]]></title>
            <link>https://velog.io/@tap_kim/translate-monorepo-insights-nx-turborepo-and-pnpm</link>
            <guid>https://velog.io/@tap_kim/translate-monorepo-insights-nx-turborepo-and-pnpm</guid>
            <pubDate>Thu, 23 Jan 2025 02:24:25 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>원문: <a href="https://medium.com/ekino-france/monorepo-insights-nx-pnpm-and-turborepo-1-4-cf7e792a87da">Monorepo Insights: Nx, Turborepo, and PNPM (1/4)</a></p>
</blockquote>
<p>오늘날 최고의 모노레포 솔루션의 장단점을 살펴봅시다.</p>
<p>아래 링크를 통해 전체 시리즈를 살펴보세요.</p>
<ul>
<li>모노레포 인사이트: Nx, Turborepo 그리고 PNPM (1/4) (현재 글)</li>
<li><a href="https://velog.io/@tap_kim/translate-monorepo-insights-nx-turborepo-and-pnpm2">모노레포 인사이트: Nx, Turborepo 그리고 PNPM (2/4)</a></li>
<li><a href="https://velog.io/@tap_kim/translate-monorepo-insights-nx-turborepo-and-pnpm3">모노레포 인사이트: Nx, Turborepo 그리고 PNPM (3/4)</a></li>
<li><a href="https://velog.io/@tap_kim/translate-monorepo-insights-nx-turborepo-and-pnpm3-fys9qq10">모노레포 인사이트: Nx, Turborepo 그리고 PNPM (4/4)</a></li>
</ul>
<h2 id="소개">소개</h2>
<p>성공적인 <a href="https://medium.com/ekino-france/beyond-webpack-esbuild-vite-rollup-swc-and-snowpack-97911a7175cf">번들러</a> 선정 연구에 이어, <a href="https://medium.com/@ekino-france">ekino-France</a>는 모노레포의 세계로 뛰어들었습니다! 저희는 프로젝트의 최종 챔피언을 결정하기 위해 <code>Nx</code>, <code>PNPM</code>, <code>Turborepo</code>를 놓고 흥미진진한 대결을 시작합니다.</p>
<p>이 시리즈에서는 모노레포 매니저의 특징, 성능 및 특정 요구 사항에 대한 적합성을 평가하면서 모노레포 매니저의 세계에 대해 자세히 알아볼 것입니다. 우리의 목표는 개발 프로세스를 개선하여 보다 효율적인 워크플로우와 간소화된 코드 베이스 관리로 이어질 모노레포 매니저를 찾는 것입니다.</p>
<p>이번 입문편에서는 모노레포 매니저의 기반이 되는 이론적 개념을 살펴봄으로써 기초를 다져보겠습니다. 아래 목록을 자세히 살펴보겠습니다.</p>
<ul>
<li><a href="#%EB%AA%A8%EB%85%B8%EB%A0%88%ED%8F%AC-%EA%B4%80%EB%A6%AC%EC%9E%90-%EB%B0%8F-%EB%B9%8C%EB%93%9C-%EC%8B%9C%EC%8A%A4%ED%85%9C">모노레포 관리자 및 빌드 시스템</a></li>
<li><a href="#%EB%B9%8C%EB%93%9C-%EC%8B%9C%EC%8A%A4%ED%85%9C-%ED%98%81%EC%8B%A0%EC%9D%98-%EC%97%94%EC%A7%84">빌드 시스템 혁신의 엔진</a></li>
<li><a href="#%EB%AA%A8%EB%85%B8%EB%A0%88%ED%8F%AC-%EB%A7%A4%EB%8B%88%EC%A0%80-%ED%98%91%EC%97%85%EC%9D%98-%EC%98%A4%EC%BC%80%EC%8A%A4%ED%8A%B8%EB%A0%88%EC%9D%B4%ED%84%B0">모노레포 매니저: 협업의 오케스트레이터</a></li>
<li><a href="#%EA%B0%95%EB%A0%A5%ED%95%9C-%EC%8B%9C%EB%84%88%EC%A7%80-%ED%9A%A8%EA%B3%BC">강력한 시너지 효과</a></li>
<li><a href="#%EB%AA%A8%EB%85%B8%EB%A0%88%ED%8F%AC%EC%9D%98-%EA%B3%BC%EC%A0%9C-%EA%B7%B9%EB%B3%B5%ED%95%98%EA%B8%B0">모노레포의 과제 극복하기</a></li>
<li><a href="#%EB%AA%A8%EB%85%B8%EB%A0%88%ED%8F%AC%EC%9D%98-%EC%9D%98%EC%A1%B4%EC%84%B1-%EA%B4%80%EB%A6%AC-%EA%B7%B8%EB%9E%98%ED%94%84-%EA%B8%B0%EB%B0%98-%EC%A0%91%EA%B7%BC-%EB%B0%A9%EC%8B%9D">모노레포의 의존성 관리: 그래프 기반 접근 방식</a></li>
<li><a href="#%EC%9C%A0%ED%96%A5-%EB%B9%84%EC%88%9C%ED%99%98-%EA%B7%B8%EB%9E%98%ED%94%84DAG">유향 비순환 그래프(DAG)</a></li>
<li><a href="#%EC%9E%91%EC%97%85-%EA%B7%B8%EB%9E%98%ED%94%84">작업 그래프</a></li>
<li><a href="#%EC%9C%84%EC%83%81-%EC%A0%95%EB%A0%AC">위상 정렬</a></li>
<li><a href="#%EC%88%9C%ED%99%98-%EA%B2%80%EC%B6%9C">순환 검출</a></li>
<li><a href="#%EB%8F%84%EB%8B%AC-%EA%B0%80%EB%8A%A5%EC%84%B1-%EB%B6%84%EC%84%9D">도달 가능성 분석</a></li>
<li><a href="#%EC%B5%9C%EB%8B%A8-%EA%B2%BD%EB%A1%9C-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98">최단 경로 알고리즘</a></li>
<li><a href="#%EC%BA%90%EC%8B%9C-%EC%B2%98%EB%A6%AC">캐시 처리</a></li>
<li><a href="#%EC%BA%90%EC%8B%B1%EC%9D%B4-%EB%AA%A8%EB%85%B8%EB%A0%88%ED%8F%AC%EC%9D%98-%ED%8C%90%EB%8F%84%EB%A5%BC-%EB%B0%94%EA%BE%BC-%EC%9D%B4%EC%9C%A0%EB%8A%94-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80%EC%9A%94">캐싱이 모노레포의 판도를 바꾼 이유</a></li>
<li><a href="#%EC%BA%90%EC%8B%9C-%EB%AC%B4%ED%9A%A8%ED%99%94">캐시 무효화</a></li>
<li><a href="#%EC%9E%91%EC%97%85-%EC%8A%A4%EC%BC%80%EC%A4%84%EB%A7%81">작업 스케줄링</a></li>
<li><a href="#%EC%9E%91%EC%97%85-%EC%8A%A4%EC%BC%80%EC%A4%84%EB%A7%81-%EC%A0%84%EB%9E%B5">작업 스케줄링 전략</a></li>
<li><a href="#%EC%8B%A4%EC%A0%9C-%EC%82%AC%EB%A1%80">실제 사례</a></li>
<li><a href="#%EB%AA%A8%EB%93%88%ED%99%94-%EB%B0%8F-%EC%BD%94%EB%93%9C-%EA%B3%B5%EC%9C%A0">모듈화 및 코드 공유</a></li>
<li><a href="#%EC%9A%94%EC%95%BD">요약</a></li>
<li><a href="#%EA%B2%B0%EB%A1%A0">결론</a></li>
</ul>
<p>이제 안전벨트를 매고 출발합시다! 🚀 🌟</p>
<h2 id="모노레포-관리자-및-빌드-시스템">모노레포 관리자 및 빌드 시스템</h2>
<p>모노레포 관리자와 빌드 시스템을 종종 혼동하는 경우가 있습니다. 그러나 이들은 모노레포 생태계 내에서 상호 보완적인 역할을 하는 별개의 독립된 개체입니다. 모노레포 개발의 잠재력을 최대한 활용하려면 이들의 관계를 이해하는 것이 중요합니다.</p>
<h3 id="빌드-시스템-혁신의-엔진">빌드 시스템 혁신의 엔진</h3>
<p>빌드 시스템은 코드 베이스의 핵심입니다. 빌드 시스템은 일련의 자동화된 단계를 통해 원시 소스 코드를 실행할 수 있는 애플리케이션 또는 라이브러리로 변환합니다.</p>
<p>1️⃣ <strong>빌드 작업</strong>:</p>
<p>✔️ <strong>컴파일/트랜스파일</strong>: 한 언어(예: 타입스크립트)의 코드를 기계가 이해할 수 있는 다른 언어(예: 자바스크립트)로 변환하는 작업입니다.</p>
<p>✔️ <strong>코드 경량화</strong>: 공백 제거, 변수 이름 바꾸기 등의 기술을 통해 코드 크기를 줄여 로딩 시간을 개선합니다.</p>
<p>✔️ <strong>번들링</strong>: 코드와 자산(이미지, 글꼴 등)을 배포에 최적화된 파일로 패키징하는 작업입니다.</p>
<p>2️⃣ <strong>린팅 및 포멧팅</strong>: 일관된 코드 스타일과 품질 표준을 적용하여 유지보수를 용이하게 만듭니다.</p>
<p>2️⃣ <strong>테스트</strong>: 자동화된 테스트(단위, 통합, e2e)를 실행하여 기능을 검증하고 오류를 조기에 포착합니다.</p>
<p><img src="https://miro.medium.com/v2/resize:fit:720/format:webp/0*PCcmbuyS5B3tE62x.jpg" alt="https://www.hongkiat.com/blog/webpack-introduction/"></p>
<div style="width:100%; text-align: center;">
  <a href="https://www.hongkiat.com/blog/webpack-introduction/" >https://www.hongkiat.com/blog/webpack-introduction/</a>
</div>
<br/>

<h3 id="모노레포-매니저-협업의-오케스트레이터">모노레포 매니저: 협업의 오케스트레이터</h3>
<p>반면에 모노레포 매니저는 모노레포의 복잡한 교향곡을 지휘하는 지휘자 역할을 합니다. 이들은 다음과 같은 도구와 구조를 제공합니다.</p>
<p>✔️ <strong>의존 관리</strong>: 모노레포 내에서 프로젝트 간의 의존을 효율적으로 해결하고 연결하여 원활한 협업을 보장하고 충돌을 방지합니다.</p>
<p>✔️ <strong>작업 오케스트레이션</strong>: 여러 프로젝트에서 빌드, 테스트 및 기타 작업의 실행을 조율하여 종속성을 고려하고 병렬 처리를 통해 최적화를 달성합니다.</p>
<p>✔️ <strong>코드 공유</strong>: 모노레포의 여러 프로젝트에서 코드와 구성 요소를 재사용할 수 있도록 하여 일관성을 높이고 중복성을 줄입니다.</p>
<p>✔️ <strong>도구 통합</strong>: 선호하는 개발 도구(린터, 포맷터, 테스트 러너)와 원활하게 통합하여 통합된 환경을 제공합니다.</p>
<p>✔️ <strong>워크플로 및 컨벤션</strong>: 프로젝트 구조, 이름 지정 및 작업 실행을 위한 규칙을 수립하여 일관성과 유지 관리의 용이성을 높입니다.</p>
<p><img src="https://miro.medium.com/v2/resize:fit:720/format:webp/1*8haA8rkpMctuZ5L89yY4ow.png" alt="https://monorepo.tools/"></p>
<div style="width:100%; text-align: center;">
  <a href="https://monorepo.tools/" >https://monorepo.tools/</a>
</div>
<br/>

<h3 id="강력한-시너지-효과">강력한 시너지 효과</h3>
<p>모노레포 관리자는 빌드 시스템의 기능을 활용하여 코드를 아티팩트로 실제 변환하는 작업을 수행합니다. 하지만 모노레포 관리자는 이를 넘어 모노레포 관리를 간소화하는 추상화 계층을 제공합니다.</p>
<p>빌드 시스템을 자동차의 엔진이라고 가정한다면 모노레포 관리자는 운전자라고 생각하면 됩니다. 엔진은 동력을 제공하지만, 운전자는 엔진이 원활하고 효율적으로 주행할 수 있도록 지시합니다.</p>
<p>예를 들면 아래와 같은 것들입니다.</p>
<p>✔️ <strong>Nx</strong>: 강력한 태스크 러너를 사용하여 빌드와 테스트를 오케스트레이션하는 동시에 모노레포에 특화된 다른 기능도 제공합니다.</p>
<p>✔️ <strong>Turborepo</strong>: 고급 캐싱 및 태스크 파이프라이닝을 통해 기존 빌드 도구(예: npm 스크립트)의 실행을 최적화합니다.</p>
<p>✔️ <strong>pnpm workspace</strong>: 기존 빌드 스크립트와 원활하게 통합하면서 효율적인 의존성 관리에 중점을 둡니다.</p>
<h3 id="모노레포의-과제-극복하기">모노레포의 과제 극복하기</h3>
<p>모노레포에는 빌드 시스템과 모노레포 관리자 모두에게 신중한 솔루션이 필요한 고유한 과제가 있습니다. 이러한 주요 과제와 최신 도구가 과제를 어떻게 해결했는지 자세히 알아봅시다.</p>
<p>1️⃣ <strong>순환 의존성 풀기:</strong></p>
<p><img src="https://miro.medium.com/v2/resize:fit:720/format:webp/1*hz6L-ooppk88oOLvXTl__w.png" alt="https://www.researchgate.net/figure/Circular-dependency-chain_fig5_4283963"></p>
<div style="width:100%; text-align: center;">
  <a href="https://www.researchgate.net/figure/Circular-dependency-chain_fig5_4283963" >https://www.researchgate.net/figure/Circular-dependency-chain_fig5_4283963</a>
</div>
<br/>

<p>🔳 <strong>문제</strong>: 순환 종속성은 하나의 모노레포 내에서 두 개 이상의 프로젝트가 서로 의존하여 폐쇄 루프를 만들 때 발생합니다. 이에 따라 빌드 실패, 무한 루프 및 개발 워크플로우의 전반적인 혼란이 발생할 수 있습니다.</p>
<p>🔳 <strong>솔루션</strong>: 최신 빌드 시스템과 모노레포 관리자는 이러한 주기를 감지하고 솔루션을 제공하는 데 탁월합니다.</p>
<p>✔️ <strong>리팩토링 가이드</strong>: 코드 또는 종속성을 재구조화하여 순환을 끊는 방법을 제안합니다.</p>
<p>✔️ <strong>수동 빌드 순서 지정</strong>: 빌드 순서를 명시적으로 정의하여 종속성을 제어된 방식으로 해결할 수 있도록 합니다.</p>
<p>✔️ <strong>의존성 반전</strong>: 의존성 반전과 같은 디자인 패턴을 사용하여 프로젝트 간의 결합을 줄이도록 권장합니다.</p>
<p>2️⃣ <strong>개발자 경험(DX) 향상</strong>:</p>
<p>🔳 <strong>문제</strong>: 열악한 개발자 경험(DX)은 생산성을 저해하고 불만을 초래할 수 있습니다. 느린 빌드, 이해하기 어려운 오류 메시지, 복잡한 툴링이 일반적인 원인입니다.</p>
<p>🔳 <strong>솔루션</strong>: 효과적인 빌드 시스템과 모노레포 관리자는 다음을 제공하여 DX를 우선시합니다.</p>
<p>✔️ <strong>명확한 오류 메시지</strong>: 오류의 정확한 원인을 정확히 파악하여 실행할 수 있는 제안을 제공합니다.</p>
<p>✔️ <strong>직관적인 인터페이스</strong>: 간소화된 워크플로우를 위한 사용자 친화적인 명령줄 인터페이스(CLI), 시각적 도구(예: Nx Console) 또는 IDE 통합을 제공합니다.</p>
<p>✔️ <strong>빠른 피드백 루프</strong>: 증분 빌드 및 캐싱을 활용하여 코드 변경 사항에 대한 신속한 피드백을 제공합니다.</p>
<p>✔️ <strong>핫 모듈 리로딩(HMR)</strong>: 전체 페이지를 다시 로드하지 않고도 실행 중인 애플리케이션을 즉시 업데이트할 수 있어 개발 주기를 더욱 개선할 수 있습니다.</p>
<p>3️⃣ <strong>프로젝트 성장에 따른 확장</strong>:</p>
<p><img src="https://miro.medium.com/v2/resize:fit:720/format:webp/0*Ki_MoikUO4vkLHKh.png" alt="https://www.drawio.com/blog/dependency-graphs"></p>
<div style="width:100%; text-align: center;">
  <a href="https://www.drawio.com/blog/dependency-graphs" >https://www.drawio.com/blog/dependency-graphs</a>
</div>
<br/>

<p>🔳 <strong>문제</strong>: 모노레포의 규모와 복잡성이 증가함에 따라 의존성 관리, 작업 오케스트레이션, 빌드 성능 보장이 점점 더 어려워지고 있습니다.</p>
<p>🔳 <strong>솔루션</strong>: 강력한 빌드 시스템과 모노레포 관리자는 이러한 확장 문제를 해결할 수 있는 기능을 제공합니다.</p>
<p>✔️ <strong>지능형 작업 스케줄링</strong>: 빌드 순서를 최적화하고 독립적인 작업을 병렬화하여 빌드 시간을 단축합니다.</p>
<p>✔️ <strong>고급 캐싱</strong>: 중복 작업을 방지하기 위해 중간 빌드 아티팩트를 저장합니다.</p>
<p>✔️ <strong>분산 빌드</strong>: 대규모 프로젝트를 위해 여러 머신에 빌드를 배포합니다.</p>
<p>✔️ <strong>코드 분할</strong>: 대규모 애플리케이션을 더 작은 청크로 분할하여 빌드 속도를 높이고 성능을 개선합니다.</p>
<p>4️⃣ <strong>팀 협업 강화</strong>:</p>
<p><img src="https://miro.medium.com/v2/resize:fit:1400/format:webp/0*rdObK6Qncx4S4mPk.png" alt="https://medium.com/@deanbiscocho/the-art-of-well-documented-code-a-developers-guide-cbc8485da9b5
"></p>
<div style="width:100%; text-align: center;">
  <a href="https://medium.com/@deanbiscocho/the-art-of-well-documented-code-a-developers-guide-cbc8485da9b5" >https://medium.com/@deanbiscocho/the-art-of-well-documented-code-a-developers-guide-cbc8485da9b5</a>
</div>
<br/>

<p>🔳 <strong>문제</strong>: 대규모 팀에서는 개발 작업을 조정하고, 공유 종속성을 관리하고, 충돌을 피하는 것이 로직의 악몽이 될 수 있습니다.</p>
<p>🔳 <strong>솔루션</strong>: 최신 도구는 팀을 다음과 같이 지원합니다.</p>
<p>✔️ <strong>세분화된 의존성 관리</strong>: 프로젝트가 공유 라이브러리의 특정 버전에 종속되도록 허용하여 변경 사항이 중단되는 것을 방지할 수 있습니다.</p>
<p>✔️ <strong>병렬 개발</strong>: 개발자가 서로의 발목을 잡지 않고 모노레포의 여러 부분을 동시에 작업할 수 있습니다.</p>
<p>✔️ <strong>코드 리뷰 워크플로우</strong>: 코드 리뷰 도구와 통합하여 코드 품질과 일관성을 보장합니다.</p>
<p>✔️ <strong>릴리스 관리</strong>: 여러 프로젝트를 게시하고 배포하는 프로세스를 간소화합니다.</p>
<p>이제 모노레포 개발의 일반적인 장애물을 살펴봤으니 모노레포 관리자가 종속성을 효율적으로 관리하는 방법에 대해 자세히 알아보겠습니다. 🌟</p>
<h2 id="모노레포의-의존성-관리-그래프-기반-접근-방식">모노레포의 의존성 관리: 그래프 기반 접근 방식</h2>
<p>모노레포에서 종속성을 관리하는 것은 거대한 실타래를 푸는 것처럼 느껴질 수 있으며, <strong>순환 의존성, 버전 충돌, 느린 빌드 시간</strong>으로 이어질 수 있습니다. 하지만 더 나은 방법이 있다면 어떨까요?</p>
<p>이 섹션에서는 그래프 이론이 모노레포의 의존성 구조를 시각화, 분석 및 최적화하여 보다 효율적이고 즐거운 개발 환경을 만드는 데 어떻게 도움이 되는지 살펴볼 것입니다. 시작하겠습니다! ✈️</p>
<h3 id="유향-비순환-그래프dag">유향 비순환 그래프(DAG)</h3>
<p>🔳 많은 모노레포 관리자의 핵심에는 그래프 이론의 강력한 도구인 유향 비순환 그래프(DAG)가 있습니다. 이 구조는 프로젝트 간의 복잡한 의존성 웹을 우아하게 표현하여 원활하고 효율적인 개발 프로세스를 보장합니다.</p>
<blockquote>
<p><em>수학, 특히 그래프 이론과 컴퓨터 과학에서 방향 비순환 그래프(DAG)는 <strong>방향 주기가 없는</strong> 유향 그래프입니다. 즉, 정점과 간선(호라고도 함)으로 구성되며 각 간선은 한 정점에서 다른 정점으로 향하므로 해당 방향을 따라가도 폐쇄 루프가 형성되지 않습니다. - <a href="https://en.wikipedia.org/wiki/Directed_acyclic_graph">https://en.wikipedia.org/wiki/Directed_acyclic_graph</a></em></p>
</blockquote>
<p>🔳 앞서 언급한 정의는 DAG의 두 가지 주요 특징을 강조합니다.</p>
<p>✔️ <strong>유향</strong>: 그래프의 가장자리에는 방향이 있어 한 프로젝트가 다른 프로젝트에 의존하고 있음을 나타냅니다. 프로젝트 A가 프로젝트 B에 종속된 경우 노드 A에서 노드 B를 가리키는 화살표(엣지)가 있습니다.</p>
<p>✔️ <strong>비순환</strong>: 그래프에 주기가 포함되어 있지 않습니다. 즉, 한 노드에서 시작하여 가장자리를 따라 경로를 따라가다가 다시 같은 시작 노드로 돌아올 수 없습니다. 프로젝트 측면에서 보면 순환 종속성이 존재하지 않습니다(예: 프로젝트 A가 B에 종속되고, B가 C에 종속되며, C가 A에 종속되는 경우).</p>
<p><img src="https://miro.medium.com/v2/resize:fit:720/format:webp/1*m7gxQv_Vc7SsTOb29vh_pA.png" alt="https://www.researchgate.net/figure/A-directed-acyclic-graph-DAG-representing-a-possible-causal-model-for-the-underlying_fig1_369413899"></p>
<div style="width:100%; text-align: center;">
  <a href="https://www.researchgate.net/figure/A-directed-acyclic-graph-DAG-representing-a-possible-causal-model-for-the-underlying_fig1_369413899" >https://www.researchgate.net/figure/A-directed-acyclic-graph-DAG-representing-a-possible-causal-model-for-the-underlying_fig1_369413899</a>
</div>
<br/>

<p>🔳 유향 비순환 그래프(DAG)는 특정한 특성으로 인해 여러 가지 이유로 유용합니다.</p>
<p>✔️ <strong>의존성 표현</strong>: DAG는 개체 또는 이벤트 간의 종속성을 표현하는 데 탁월합니다. 어떤 항목이 어떤 다른 항목에 종속되어 있는지를 지시된 가장자리를 통해 명확하게 보여 주며, 비순환적 특성으로 인해 충돌이나 무한 루프로 이어질 수 있는 순환 종속성이 없습니다.</p>
<p>✔️ <strong>워크플로 및 프로세스 모델링</strong>: DAG는 작업이 서로 특정 종속성을 갖는 복잡한 워크플로우와 프로세스를 모델링할 수 있습니다. 이를 통해 작업이 올바른 순서로 실행되고 리소스를 효율적으로 활용할 수 있습니다.</p>
<p>✔️ <strong>데이터 처리 및 스케줄링</strong>: 데이터 처리 파이프라인에서 DAG는 데이터 변환 및 계산의 흐름을 표현하여 각 단계가 종속성이 충족된 후에 실행되도록 할 수 있습니다. 또한 스케줄링 시스템에서 종속성과 우선순위에 따라 작업을 실행할 순서를 결정하는 데도 사용됩니다.</p>
<p>✔️ <strong>버전 관리 및 기록</strong>: DAG는 <a href="https://www.git-scm.com/docs/gitglossary/2.19.0#Documentation/gitglossary.txt-aiddefDAGaDAG"><code>Git</code></a>과 같은 버전 관리 시스템에서 코드 변경 내역을 추적하는 데 사용됩니다. 각 커밋은 DAG의 노드이며, 간선은 커밋 간의 부모-자식 관계를 나타냅니다.</p>
<p>✔️ <strong>빌드 시스템</strong>: <a href="https://docs.gradle.org/current/userguide/build_lifecycle.html"><code>Gradle</code></a>, <a href="https://bazel.build/concepts/dependencies"><code>Bazel</code></a>과 같은 빌드 도구는 DAG를 사용하여 소스 코드 파일을 컴파일하고 링크할 순서를 결정하여 종속성이 필요하기 전에 빌드되도록 합니다.</p>
<p>🔳 DAG를 효과적으로 탐색하고 구성하기 위해 특정 알고리즘이 사용됩니다.</p>
<p>✔️ <a href="https://en.wikipedia.org/wiki/Topological_sorting">위상 정렬</a>은 정점을 방문할 수 있는 순서를 결정하여 DAG를 선형화하는 핵심 알고리즘으로, 정점 <code>u</code>에서 <code>v</code>까지의 모든 방향 엣지에 대해 정점 <code>u</code>가 <code>v</code> 앞에 오도록 합니다. 이 순서를 통해 작업이 실행되기 전에 종속성이 충족되도록 보장합니다.</p>
<p><img src="https://miro.medium.com/v2/resize:fit:720/format:webp/1*kPx7C3RRFGgKTPosuTd8_g.png" alt="https://algodaily.com/lessons/staying-on-top-of-topological-sort"></p>
<p>✔️ <strong>깊이 우선 탐색(DFS)</strong> 및 <strong>너비 우선 탐색(BFS)</strong> 과 같은 다른 알고리즘도 DAG를 트래버스하는 데 사용할 수 있습니다. DFS는 하나의 경로를 최대한 깊게 탐색한 후 역추적하는 반면, BFS는 한 정점의 바로 옆 이웃을 모두 방문한 후 그 다음으로 이동합니다. 이러한 알고리즘은 사이클 탐지 또는 주어진 시작점에서 도달할 수 있는 모든 노드를 찾는 것과 같은 작업에 유용합니다.</p>
<p><img src="https://miro.medium.com/v2/resize:fit:720/format:webp/0*zMB8jsj1YYcKiGPk.png" alt="https://www.geeksforgeeks.org/difference-between-bfs-and-dfs/"></p>
<div style="width:100%; text-align: center;">
  <a href="https://www.geeksforgeeks.org/difference-between-bfs-and-dfs/" >https://www.geeksforgeeks.org/difference-between-bfs-and-dfs/</a>
</div>
<br/>

<p>DAG는 관계를 표현하는 데 탁월하지만, 작업 그래프는 구조를 넘어 모노레포에서 일어나야 하는 <strong>작업</strong>을 포착합니다. 그 방법을 살펴보겠습니다. 🚂</p>
<h3 id="작업-그래프">작업 그래프</h3>
<p>유향 비순환 그래프(DAG)는 모노레포에서 패키지 간의 정적 종속성을 효과적으로 표현하지만, 작업 그래프는 한 단계 더 나아갑니다. 작업 그래프는 코드를 빌드, 테스트 및 배포하는 데 필요한 <strong>동적 작업</strong>과 <strong>워크플로우</strong>를 모델링합니다.</p>
<blockquote>
<p><em>작업 그래프를 사용하면 작업 시퀀스를 자동으로 실행할 수 있습니다. 작업 그래프 또는 유향 비순환 그래프(DAG)는 루트 작업과 하위 작업으로 구성된 일련의 작업으로, 종속성에 따라 구성됩니다. 작업 그래프는 한 방향으로 흐르기 때문에 시리즈의 후반에 있는 작업이 이전 작업의 실행을 유도할 수 없습니다. 각 작업은 여러 다른 작업에 종속될 수 있으며 모든 작업이 완료될 때까지 실행되지 않습니다. 또한 각 작업에는 그 작업에 종속되는 여러 하위 작업이 있을 수 있습니다. - <a href="https://docs.snowflake.com/en/user-guide/tasks-graphs">https://docs.snowflake.com/en/user-guide/tasks-graphs</a></em></p>
</blockquote>
<p>🔳 작업 그래프는 다음과 같은 특수한 유형의 DAG입니다.</p>
<p>✔️ <strong>노드</strong>: 파일 컴파일, 특정 패키지에 대한 테스트 실행 또는 서비스 배포와 같은 개별 작업 또는 작업을 나타냅니다.</p>
<p>✔️ <strong>엣지</strong>: 작업 간의 종속성을 정의하여 다른 작업을 시작하기 전에 완료해야 하는 작업을 나타냅니다. 작업 B를 시작하기 전에 작업 A를 완료해야 하는 경우, 엣지는 A에서 B를 가리킵니다.</p>
<p><img src="https://miro.medium.com/v2/resize:fit:720/format:webp/0*X4PXbnt_Pkqx2obk.png" alt="https://nlguillemot.wordpress.com/2017/01/13/using-cont-with-tbbtask_group/"></p>
<div style="width:100%; text-align: center;">
  <a href="https://nlguillemot.wordpress.com/2017/01/13/using-cont-with-tbbtask_group/" >https://nlguillemot.wordpress.com/2017/01/13/using-cont-with-tbbtask_group/</a>
</div>
<br/>

<p>모노레포 관리자는 작업 그래프를 사용하여 복잡한 워크플로를 오케스트레이션합니다. 일반적으로 다음과 같이 작동합니다.</p>
<p>✔️ <strong>그래프 생성</strong>: 명령(예: <code>nx build myapp</code>)을 실행하면 모노레포 매니저가 프로젝트의 구성과 소스 코드를 분석하여 필요한 작업과 종속성을 결정합니다. 이 정보는 작업 그래프를 구성하는 데 사용됩니다.</p>
<p>✔️ <strong>위상 정렬</strong>: 그런 다음 관리자는 작업 그래프에 위상 정렬(나중에 살펴보겠습니다.)을 적용하여 작업을 실행할 수 있는 유효한 순서를 설정합니다. 이렇게 하면 작업이 시작되기 전에 종속성이 충족됩니다.</p>
<p>✔️ <strong>작업 실행</strong>: 관리자는 위상적으로 정렬된 순서대로 작업을 실행합니다. 종종 독립적인 작업을 병렬로 실행하여 프로세스 속도를 크게 높일 수 있습니다.</p>
<p>✔️ <strong>캐싱</strong>: 많은 모노레포 관리자는 반복 작업을 피하려고 캐싱을 활용합니다. 작업의 입력(소스 코드, 의존성)이 변경되지 않은 경우 캐시된 출력을 재사용할 수 있습니다.</p>
<p>🔵 두 개의 프로젝트가 있는 단순화된 모노레포를 고려해 보겠습니다.</p>
<ul>
<li><code>ui-components</code>: UI 컴포넌트 라이브러리</li>
<li><code>my-app</code>: <code>ui-components</code>를 사용하는 애플리케이션</li>
</ul>
<p><code>my-app</code> 빌드를 위한 작업 그래프는 다음과 같습니다.</p>
<pre><code class="language-bash">build(ui-components) --&gt; lint(ui-components) --&gt; test(ui-components) --&gt; build(my-app) --&gt; lint(my-app) --&gt; test(my-app)</code></pre>
<p>🔳 의존성 그래프와 작업 그래프 사이의 미묘한 상호 작용과 모노레포 관리에서 서로를 보완하는 방법을 알아보기 위해 이 비교표를 살펴보겠습니다.</p>
<p><img src="https://miro.medium.com/v2/resize:fit:720/format:webp/1*E2g_Z7MUwibPp4J--1f1FA.png" alt="DAGs vs. Task Graphs (Image by the author)"></p>
<div style="width:100%; text-align: center;">
  DAGs vs. Task Graphs (Image by the author)
</div>
<br/>

<p>DAG와 작업 그래프는 종속성을 우아하게 모델링하지만, <strong>본질적으로 명확한 실행 순서를 제공</strong>하지는 않습니다. 이러한 복잡한 구조를 모노레포 빌드 프로세스를 안내하는 실행할 수 있는 시퀀스로 변환하는 위상 정렬이 필요한 이유입니다. 한번 보시죠! 🚁</p>
<h3 id="위상-정렬">위상 정렬</h3>
<p>🔳 유향 비순환 그래프(DAG) 영역에서 <strong>위상 정렬</strong>은 추상적인 의존성 웹을 구체적이고 실행할 수 있는 시퀀스로 변환하는 중추적인 알고리즘입니다.</p>
<p><img src="https://miro.medium.com/v2/resize:fit:1100/format:webp/1*XhK9gwjXUkZ0WuhD0MSw7A.png" alt="https://www.naukri.com/code360/library/topological-sorting"></p>
<div style="width:100%; text-align: center;">
  <a href="https://www.naukri.com/code360/library/topological-sorting" >https://www.naukri.com/code360/library/topological-sorting</a>
</div>
<br/>

<p>🔳 정점 <code>u</code>에서 <code>v</code>로 향하는 모든 방향 엣지에 대해 정점 <code>u</code>가 순서상 <code>v</code>보다 앞에 오도록 DAG의 정점(노드)을 정렬하는 프로세스입니다. 간단히 말해, 종속성을 기준으로 작업을 정렬하여 모든 전제 조건이 완료될 때까지 작업이 시작되지 않도록 하는 것입니다.</p>
<p>이 미니어처 예시를 자세히 살펴보겠습니다.</p>
<pre><code class="language-bash">Package A depends on Package B and Package C
Package B depends on Package D
Package C has no dependencies</code></pre>
<p>이 그래프의 위상 정렬은 다음과 같습니다(<code>D -&gt; B -&gt; C -&gt; A</code>). 즉, 패키지 D를 먼저 구축한 다음 B, C, 마지막으로 A를 구축해야 합니다.</p>
<p>🔳 보시다시피, 동일한 유향 비순환 그래프(DAG)에 대해 여러 가지 유효한 위상 순서가 있을 수 있습니다. 모노레포 관리의 맥락에서 이는 패키지에 대해 <strong>유효한 빌드 순서가 여러 개</strong> 있을 수 있음을 의미합니다. 선택된 특정 순서는 다음과 같은 요인에 따라 달라질 수 있습니다:</p>
<p>✔️ <strong>병렬성</strong>: 일부 빌드 도구는 작업의 병렬 실행 가능성을 극대화하는 순서를 우선순위로 지정할 수 있습니다.</p>
<p>✔️ <strong>최적화</strong>: 빌드 시간이나 리소스 사용량을 최소화하기 위해 선택한 순서가 최적화될 수 있습니다.</p>
<p>✔️ <strong>사용자 지정</strong>: 일부 도구에서는 위상 정렬 알고리즘의 출력에 영향을 주는 기본 설정이나 제약 조건을 지정할 수 있습니다.</p>
<p>💡 DAG에 대해 여러 가지 유효한 위상 순서를 지정할 수 있는 기능은 일종의 <strong>적응형</strong> 관리로 비유할 수 있습니다. 머신러닝(ML) 모델이 데이터를 기반으로 동작을 학습하고 적응하는 것처럼, 모노레포 관리자는 다양한 위상 순서를 활용하여 빌드 프로세스를 최적화하거나 변화하는 프로젝트 요구 사항에 대응할 수 있습니다.</p>
<p>또한 모노레포의 맥락에서 위상 정렬은 매우 중요합니다.</p>
<p>✔️ <strong>빌드 순서</strong>: 종속성에 따라 패키지를 빌드하거나 컴파일할 올바른 순서를 결정합니다.</p>
<p>✔️ <strong>작업 실행</strong>: 작업 간의 종속성을 고려하여 작업(예: 린팅, 테스트, 배포)을 스케줄링합니다.</p>
<p>✔️ <strong>의존성 분석</strong>: 순환 의존성(위상 정렬을 불가능하게 만드는)과 같은 잠재적인 문제를 식별합니다.</p>
<p>✔️ <strong>캐싱</strong>: 종속성이 변경될 때만 패키지가 다시 빌드되도록 하여 빌드 캐시를 최적화합니다.</p>
<p>위상 정렬은 의존성 관계를 이해하는 데 탄탄한 기반을 제공하지만, 모노레포 관리자는 작업과 관련 메타데이터의 복잡한 상호 작용을 표현하기 위해 보다 전문적인 그래프 구조를 사용하는 경우가 많습니다.</p>
<p>🔵 <strong>예시</strong>: 다음과 같은 프로젝트와 종속성이 있는 모노레포 작업 공간을 상상해 보세요.</p>
<p><strong>apps:</strong></p>
<ul>
<li><code>store-ui</code>(storefront 웹 애플리케이션)</li>
<li><code>admin-ui</code>(관리자 대시보드 웹 애플리케이션)</li>
</ul>
<p><strong>libs:</strong></p>
<ul>
<li><code>shared-ui</code>(두 애플리케이션에서 공유하는 UI 컴포넌트)</li>
<li><code>product-data-access</code>(제품에 대한 데이터 가져오기 로직)</li>
<li><code>cart-data-access</code>(장바구니용 데이터 가져오기 로직)</li>
<li><code>auth</code>(인증 라이브러리)</li>
<li><code>utils</code>(유틸리티 함수)</li>
</ul>
<p>이러한 프로젝트 간의 종속성은 다음과 같이 DAG에서 시각화할 수 있습니다.</p>
<pre><code class="language-bash">store-ui --&gt; shared-ui
store-ui --&gt; product-data-access
store-ui --&gt; cart-data-access
store-ui --&gt; auth
admin-ui --&gt; shared-ui
admin-ui --&gt; auth
shared-ui --&gt; utils
product-data-access --&gt; utils
cart-data-access --&gt; utils
auth --&gt; utils</code></pre>
<p><img src="https://miro.medium.com/v2/resize:fit:720/format:webp/0*EbpN6eBBjvjJKvSh.png" alt="https://python-code-aws.trinket.io/python-generated/2cshue5a/trinket_plot.png"></p>
<div style="width:100%; text-align: center;">
  <a href="https://python-code-aws.trinket.io/python-generated/2cshue5a/trinket_plot.png" >https://python-code-aws.trinket.io/python-generated/2cshue5a/trinket_plot.png</a>
</div>
<br/>

<p>이 그래프를 그리는 데 사용된 Python 프로그램은 <a href="https://trinket.io/python3/0dac548f87?runOption=run">여기</a>에 있습니다. 그리고 이 유향 비순환 그래프(DAG)에서 위상 정렬을 수행하는 Python 코드는 <a href="https://trinket.io/python3/0b10028f15?runOption=run">여기</a>에 있습니다. 그래프의 위상 정렬 순서는 다음과 같습니다.</p>
<pre><code class="language-bash">Topological Sorting Order:
[
  &#39;store-ui&#39;,
  &#39;admin-ui&#39;,
  &#39;product-data-access&#39;,
  &#39;cart-data-access&#39;,
  &#39;shared-ui&#39;,
  &#39;auth&#39;,
  &#39;utils&#39;
]</code></pre>
<p>이 순서는 각 노드가 가리키는 모든 노드 앞에 나타나도록 하여 유효한 의존성 순서를 나타내며, 프로젝트 그래프 내에서 유효한 종속성을 반영하고 종속성이 충족된 후에만 각 프로젝트가 빌드되도록 보장합니다.</p>
<p>위상 정렬은 강력한 도구이지만 그래프 내에 주기가 없다는 중요한 가정이 전제되어야 합니다. 이제 모노레포 관리자가 주기 감지 알고리즘을 사용하여 이러한 유효성을 보장하는 방법을 살펴보겠습니다. ♻️</p>
<h3 id="순환-검출">순환 검출</h3>
<p>주기 감지는 지시 그래프 내에서 이러한 순환 종속성을 식별하는 프로세스입니다. 이는 모노레포의 무결성을 유지하고 원활한 개발 환경을 보장하는 데 있어 매우 중요한 단계입니다.</p>
<p><img src="https://miro.medium.com/v2/resize:fit:720/format:webp/0*3GbRttverKc8wGbw.png" alt="https://www.geeksforgeeks.org/detect-cycle-direct-graph-using-colors/"></p>
<div style="width:100%; text-align: center;">
  <a href="https://www.geeksforgeeks.org/detect-cycle-direct-graph-using-colors/" >https://www.geeksforgeeks.org/detect-cycle-direct-graph-using-colors/</a>
</div>
<br/>

<p>🔳 모노레포에서 주기 감지가 중요한 이유는 무엇인가요?</p>
<p>✔️ <strong>빌드 실패를 방지합니다</strong>: 순환 종속성은 유효한 빌드 순서를 결정할 수 없게 만들어 오류와 좌절감으로 이어집니다.</p>
<p>✔️ <strong>코드 베이스 무결성을 유지합니다</strong>: 주기는 코드 베이스 내에서 설계 결함이나 아키텍처 문제를 나타낼 수 있습니다. 이를 조기에 식별하면 건강하고 유지 관리 가능한 코드 구조를 유지하는 데 도움이 됩니다.</p>
<p>✔️ <strong>의존성 관리를 용이하게 합니다</strong>: 주기를 감지하면 코드를 리팩토링하여 종속성을 제거할 수 있으므로 종속성을 더 깔끔하고 관리하기 쉽게 만들 수 있습니다.</p>
<p>✔️ <strong>위상 정렬을 활성화합니다</strong>: 빌드 순서를 결정하는 데 중요한 위상 정렬은 유향 비순환 그래프(DAG)에서만 가능합니다. 주기 감지를 통해 의존성 그래프가 비순환 상태를 유지하도록 합니다.</p>
<p>주기 감지를 위한 여러 알고리즘이 존재하지만, 일반적인 접근 방식은 <a href="https://en.wikipedia.org/wiki/Depth-first_search">깊이 우선 검색(DFS)</a>을 기반으로 합니다:</p>
<pre><code class="language-bash">+-----------+         +-------------+        +-------------+       +-------------+
|  Start    | -----&gt;  | Mark as     | -----&gt; | Check for   | -----&gt;|  Repeat     |
|  at Node  |         |  Visited    |        |  Back Edges |       | (if needed) |
+-----------+         +-------------+        +-------------+       +-------------+
    |                        |                      |                       |
    |                        |                      |                       |
    v                        v                      v                       v
   (A)                    (A) Visited            (A)  --------&gt; (B) Visited
                                                ^  |
                                                |  |   (Back Edge)
                                                |  |
                                                (C) Visited</code></pre>
<ul>
<li><strong>방문 플래그</strong>: 알고리즘의 중요한 부분은 무한 루프를 피하고 현재 탐색 경로를 추적하기 위해 노드를 방문한 것으로 표시하는 것입니다.</li>
<li><strong>백 엣지 감지</strong>: 사이클 감지의 핵심은 현재 탐색 경로의 노드로 다시 연결되는 에지를 식별하는 것입니다(백트래킹 전).</li>
</ul>
<p>🔳 많은 모노레포 도구에는 사이클 감지 메커니즘이 내장되어 있습니다:</p>
<p>✔️ <strong>Nx</strong>: <code>nx dep-graph</code> 명령은 종속성을 시각화하고 주기를 강조 표시할 수 있습니다.</p>
<p>✔️ <strong>Turborepo</strong>: Turborepo는 순환 종속성을 자동으로 감지하고 보고합니다.</p>
<p>주기 감지는 모노레포 관리에 없어서는 안 될 부분입니다. 순환 종속성을 식별하고 해결함으로써 코드 베이스의 안정성과 유지보수성을 보장하여 보다 원활하고 효율적인 개발 프로세스를 위한 기반을 마련합니다.</p>
<p>주기 없는 그래프를 만드는 것이 가장 중요하지만, 모노레포 관리자는 패키지 간의 연결도 이해해야 합니다. 이때 도달 가능성 분석이 중요한 역할을 하며, <strong>시스템의 어느 부분이 다른 부분의 변경에 영향을 받는지(영향을 받는) 파악할 수 있습니다</strong>. 대단하죠! 🌟</p>
<h3 id="도달-가능성-분석">도달 가능성 분석</h3>
<p>🔳 도달 가능성 분석은 그래프의 맥락에서 특정 노드(예: 모노레포의 패키지)가 주어진 다른 노드에서 그래프의 가장자리를 통과하여 도달할 수 있는지를 결정하는 프로세스를 말합니다. 간단히 말해, 다음과 같은 질문에 답하는 데 도움이 됩니다: “이것을 변경하면 또 무엇이 영향을 받는가?”</p>
<p>🔳 모노레포에서 도달 가능성 분석이 중요한 이유는 무엇인가요?</p>
<p>✔️ <strong>효율적인 리빌드</strong>: 대규모 모노레포의 경우 사소한 변경 사항이 있을 때마다 전체 코드 베이스를 다시 빌드하는 것은 비현실적입니다. 도달 가능성 분석을 사용하면 변경된 코드에 의존하는 특정 패키지를 정확히 찾아내어 타겟팅된 빠른 리빌드를 수행할 수 있습니다.</p>
<p>✔️ <strong>타겟팅 테스트</strong>: 마찬가지로 도달 가능성 분석은 패키지를 수정할 때 다시 실행해야 하는 테스트를 식별하는 데 도움이 됩니다. 이를 통해 테스트 프로세스를 최적화하고 귀중한 시간을 절약할 수 있습니다.</p>
<p>✔️ <strong>영향 평가</strong>: 패키지를 크게 변경하기 전에 도달 가능성 분석을 통해 잠재적인 결과를 파악할 수 있습니다. 이는 위험을 평가하고 그에 따라 계획을 세우는 데 도움이 됩니다.</p>
<p>✔️ <strong>의존성 시각화</strong>: 도달 가능성 분석을 사용하여 패키지 간의 의존성 관계를 시각적으로 표현하여 모노레포의 전체 구조를 더 쉽게 이해할 수 있습니다.</p>
<p>🔳 모노레포의 도달 가능성 분석은 일반적으로 <a href="https://en.wikipedia.org/wiki/Depth-first_search">깊이 우선 검색(DFS)</a> 또는 <a href="https://en.wikipedia.org/wiki/Breadth-first_search">넓이 우선 검색(BFS)</a>과 같은 그래프 탐색 알고리즘을 활용합니다.</p>
<p>🔳 여러 모노레포 도구가 도달 가능성 분석을 용이하게 하는 기능을 제공합니다.</p>
<p>✔️ <strong>Nx</strong>: <code>nx affected:dep-graph</code> 명령은 영향을 받는 프로젝트의 의존성 그래프를 시각화하여 프로젝트 간의 종속성을 강조 표시합니다.</p>
<p>✔️ <strong>Turborepo</strong>: Turborepo는 변경된 파일을 기반으로 어떤 작업을 다시 실행해야 하는지 지능적으로 결정하여 간접적으로 일종의 도달 가능성 분석을 수행합니다.</p>
<p>✔️ <strong>사용자 지정 스크립트</strong>: 또한 그래프 알고리즘을 활용하여 모노레포의 의존성 그래프에서 도달 가능성 분석을 수행하는 사용자 지정 스크립트를 작성할 수도 있습니다.</p>
<p><img src="https://miro.medium.com/v2/resize:fit:720/format:webp/0*P4PeuhuL6LR70RTp.png" alt="https://github.com/leanix/nx-affected-dependencies-action"></p>
<div style="width:100%; text-align: center;">
  <a href="https://github.com/leanix/nx-affected-dependencies-action" >https://github.com/leanix/nx-affected-dependencies-action</a>
</div>
<br/>

<p>도달성 분석은 모노레포 도구에서 정말 강력한 기능입니다!</p>
<p>도달 가능성 분석은 어떤 작업이 변경의 영향을 받는지 이해하는 데 도움이 되지만, 이러한 작업을 실행하는 가장 효율적인 방법을 찾는 것도 그에 못지않게 중요합니다. 모노레포 관리자가 작업 그래프 내에서 중요한 경로를 식별하여 빌드 프로세스를 최적화할 수 있도록 하는 최단 경로 알고리즘이 바로 이 부분에서 유용합니다. 우와!🔥</p>
<h3 id="최단-경로-알고리즘">최단 경로 알고리즘</h3>
<p>🔳 <a href="https://en.wikipedia.org/wiki/Shortest_path_problem">최단 경로 알고리즘은</a> 그래프에서 두 노드 사이의 가장 효율적인 경로를 찾는 데 사용됩니다.</p>
<p><img src="https://miro.medium.com/v2/resize:fit:720/format:webp/1*MxH_mfYK0zxQ_hdUodSHtg.png" alt="https://www.researchgate.net/figure/Tree-constructed-by-Extended-Dijkstras-Shortest-Path-Algorithm_fig2_341215943"></p>
<div style="width:100%; text-align: center;">
  <a href="https://www.researchgate.net/figure/Tree-constructed-by-Extended-Dijkstras-Shortest-Path-Algorithm_fig2_341215943" >https://www.researchgate.net/figure/Tree-constructed-by-Extended-Dijkstras-Shortest-Path-Algorithm_fig2_341215943</a>
</div>
<br/>

<p>🔳 모노레포의 경우 작업 그래프에서 최단 경로를 찾으면 빌드 프로세스를 크게 최적화할 수 있습니다.</p>
<p>✔️ <strong>빌드 시간 최소화</strong>: 중요 경로(의존 작업의 가장 긴 체인)를 식별함으로써 모노레포 관리자는 해당 작업의 우선순위를 지정하고 독립 작업을 병렬화하여 전체 빌드 시간을 단축할 수 있습니다.</p>
<p>✔️ <strong>리소스 최적화</strong>: 최단 경로 알고리즘은 병목 현상을 파악하고 가장 필요한 곳에 컴퓨팅 성능을 집중하여 리소스를 효율적으로 할당할 수 있도록 도와줍니다.</p>
<p>✔️ <strong>향상된 개발자 경험</strong>: 빌드 속도가 빨라지면 개발자의 피드백 주기가 빨라져 생산성이 향상되고 개발 환경이 원활해집니다.</p>
<p>인기 있는 최단 경로 알고리즘으로는 <a href="https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm">다익스트라 알고리즘</a>과 <a href="https://en.wikipedia.org/wiki/Bellman%E2%80%93Ford_algorithm">벨만-포드 알고리즘</a>이 있습니다.</p>
<p>최단 경로 알고리즘은 병목 현상을 파악하고, 중요한 작업의 우선순위를 지정하고, 병렬 실행을 가능하게 하여 모노레포 워크플로를 최적화하는 데 중요한 역할을 합니다!</p>
<p>그래프 이론이 모노레포의 의존성 관리를 어떻게 뒷받침하는지 확실히 이해했다면, 이제 기어를 바꿔 또 다른 중요한 최적화 기법인 캐싱에 대해 알아보겠습니다. 🔮</p>
<h2 id="캐시-처리">캐시 처리</h2>
<h3 id="캐싱이-모노레포의-판도를-바꾼-이유는-무엇인가요">캐싱이 모노레포의 판도를 바꾼 이유는 무엇인가요?</h3>
<p>캐싱이 없으면 모노레포 빌드 속도가 매우 느려질 수 있습니다. 사소한 변경 사항이라도 변경할 때마다 프로젝트 전체 또는 상당 부분을 다시 빌드해야 할 수도 있습니다.</p>
<p>🔳 캐싱은 이전 빌드의 결과를 저장하고 지능적으로 재사용함으로써 해결책을 제공합니다.</p>
<p>✔️ 캐싱은 중복 작업을 방지하여 빌드 시간을 크게 단축할 수 있습니다.</p>
<p>✔️ 더 빠른 빌드는 개발자에게 더 빠른 피드백 루프를 의미합니다.</p>
<p>✔️ 캐싱은 계산 비용이 많이 드는 작업을 다시 실행할 필요성을 최소화하여 다른 프로세스를 위한 소중한 CPU 사이클과 메모리를 확보합니다.</p>
<p>✔️ 지속적 통합 및 지속적 배포(CI/CD) 파이프라인에는 여러 번의 빌드와 테스트가 포함되는 경우가 많습니다. 캐싱은 이러한 프로세스의 속도를 크게 높여 더 빠른 릴리스와 더 빈번한 배포를 가능하게 합니다.</p>
<p>🔳 모노레포에서 빌드 성능을 최적화하기 위해 각각 고유한 장점과 장단점이 있는 다양한 캐싱 메커니즘을 사용할 수 있습니다.</p>
<p><img src="https://miro.medium.com/v2/resize:fit:720/format:webp/1*CMKYKctM5sekT_Ym6vY24A.png" alt="Caching mechanisms (Image by the author)"></p>
<div style="width:100%; text-align: center;">
  Caching mechanisms (Image by the author)
</div>
<br/>

<p><img src="https://miro.medium.com/v2/resize:fit:720/format:webp/1*VZ5ti0eVCexD_iauOKm10A.png" alt="https://nx.dev/concepts/how-caching-works"></p>
<div style="width:100%; text-align: center;">
  <a href="https://nx.dev/concepts/how-caching-works" >https://nx.dev/concepts/how-caching-works</a>
</div>
<br/>

<p><img src="https://miro.medium.com/v2/resize:fit:720/format:webp/1*cNPXydir37nUkAOMiwSP8Q.png" alt="https://nx.dev/concepts/how-caching-works"></p>
<div style="width:100%; text-align: center;">
  <a href="https://nx.dev/concepts/how-caching-works" >https://nx.dev/concepts/how-caching-works</a>
</div>
<br/>

<p>🔳 Google, Facebook, Uber와 같은 기업들은 모노레포를 도입했으며 캐싱은 성공적이었습니다. 이러한 기업들은 지능형 캐싱 전략 덕분에 빌드 시간이 대폭 단축되고 개발자 생산성이 크게 향상되었다고 보고했습니다.</p>
<p><img src="https://miro.medium.com/v2/resize:fit:720/format:webp/0*gFQ3tUOBA84rW4rj.png" alt="https://qeunit.com/blog/how-google-does-monorepo/"></p>
<div style="width:100%; text-align: center;">
  <a href="https://qeunit.com/blog/how-google-does-monorepo/" >https://qeunit.com/blog/how-google-does-monorepo/</a>
</div>
<br/>

<p>캐싱의 강력한 성능에 대해 살펴봤지만, 아직 풀어야 할 퍼즐 조각이 하나 더 남아 있습니다. 이 중요한 프로세스는 캐시의 관련성을 유지하고 빌드가 항상 최신의 안정적인 상태로 유지되도록 보장합니다. 👷</p>
<h3 id="캐시-무효화"><a href="https://en.wikipedia.org/wiki/Cache_invalidation">캐시 무효화</a></h3>
<blockquote>
<p><em>캐시 무효화는 컴퓨터 시스템에서 캐시의 항목을 <strong>교체하거나 제거</strong>하는 프로세스입니다. 캐시 무효화는 캐시 일관성 프로토콜의 일부를 명시적으로 수행할 수 있습니다. 이럴 때 프로세서는 메모리 위치를 변경한 다음 나머지 컴퓨터 시스템 전체에서 해당 메모리 위치의 캐시 된 값을 무효화합니다. - <a href="https://en.wikipedia.org/wiki/Cache_invalidation">https://en.wikipedia.org/wiki/Cache_invalidation</a></em></p>
</blockquote>
<p>🔳 코드 베이스가 발전함에 따라 이전 빌드에서 캐시 된 아티팩트가 오래되어 구식이 될 수 있습니다. 이러한 오래된 아티팩트를 재사용하면 다음과 같은 문제가 발생할 수 있습니다.</p>
<p>✔️ <strong>잘못된 빌드</strong>: 오래된 의존성 또는 코드 베이스의 변경으로 인해 빌드가 잘못된 결과를 생성할 수 있습니다.</p>
<p>✔️ <strong>예기치 않은 오류</strong>: 오래된 캐시의 아티팩트로 인해 애플리케이션에서 예기치 않은 오류 및 불일치가 발생할 수 있습니다.</p>
<p>✔️ <strong>시간 낭비</strong>: 오래된 캐시에 대해 알지 못하면 예방할 수 있었던 문제를 디버깅하게 될 수 있습니다.</p>
<p>🔳 모노레포 관리자는 변경 상황이 발생할 때 캐시 항목을 지능적으로 무효화하기 위해 다양한 전략을 사용합니다.</p>
<p><img src="https://miro.medium.com/v2/resize:fit:720/format:webp/1*IBRYXo64u2l0PwAfoODudQ.png" alt="Cache invalidation strategies (image by the author)"></p>
<div style="width:100%; text-align: center;">
  Cache invalidation strategies (image by the author)
</div>
<br/>

<p>최신 모노레포 관리자는 정교한 알고리즘을 사용하여 종속성을 분석하고, 변경 사항을 추적하고, 캐시 항목을 지능적으로 무효화합니다. 이를 통해 불필요한 리빌드를 최소화하는 동시에 빌드가 항상 정확하고 최신 상태로 유지되도록 보장합니다.</p>
<p>캐시 최신성 문제를 해결했으니 이제 또 다른 핵심 최적화 기법인 작업 스케줄링에 대해 알아볼 준비가 되었습니다. 모노레포 관리자는 전략적으로 작업을 주문하고 실행함으로써 속도와 효율성을 훨씬 더 높일 수 있습니다. 💎</p>
<h2 id="작업-스케줄링">작업 스케줄링</h2>
<h3 id="작업-스케줄링-전략">작업 스케줄링 전략</h3>
<p>명확한 개요를 제공하기 위해 모노레포에서 사용되는 다양한 작업 예약 전략을 다음 표에 요약해 보겠습니다.</p>
<p><img src="https://miro.medium.com/v2/resize:fit:720/format:webp/1*C0HaNzdcCAnLxZXVS4QovA.png" alt="Task scheduling strategies (Image by the author)"></p>
<div style="width:100%; text-align: center;">
  Task scheduling strategies (Image by the author)
</div>
<br/>

<p><img src="https://miro.medium.com/v2/resize:fit:720/format:webp/0*1_KjTMYO6YsaZDqq.png" alt="https://patterns.eecs.berkeley.edu/?page_id=609"></p>
<div style="width:100%; text-align: center;">
  <a href="https://patterns.eecs.berkeley.edu/?page_id=609" >https://patterns.eecs.berkeley.edu/?page_id=609</a>
</div>
<br/>

<p>💡 작업 스케줄링 전략의 선택은 모노레포의 규모와 복잡성, 사용할 수 있는 리소스, 프로젝트의 특정 요구사항 등 다양한 요인에 따라 달라집니다.</p>
<h3 id="실제-사례">실제 사례</h3>
<p>✔️ <strong>Nx</strong>: 주로 병렬화 및 캐싱과 함께 위상 작업 스케줄링을 사용합니다. 또한 보다 전문화된 스케줄링 전략을 구현할 수 있는 사용자 지정 작업 실행기를 사용할 수 있습니다.</p>
<p>✔️ <strong>Turborepo</strong>: 병렬 작업 스케줄링과 캐싱을 중점적으로 강조합니다. 유향 그래프를 사용하여 작업 종속성을 모델링하고 그 관계에 따라 지능적으로 작업을 예약합니다.</p>
<p>✔️ <strong>Bazel</strong>: 빌드 작업의 유향 그래프를 기반으로 정교한 작업 스케줄링 시스템을 사용합니다. 병렬 실행, 캐싱 및 증분 빌드를 지원합니다.</p>
<p>이제 빌드가 최적화되었지만, 모노레포가 성장함에 따라 유지 관리 및 확장성을 유지하려면 어떤 조치를 취해야 할까요? 모듈화와 코드 공유의 원칙에서 답을 찾을 수 있습니다. 자세히 알아봅시다! 🎯</p>
<h2 id="모듈화-및-코드-공유">모듈화 및 코드 공유</h2>
<p>잘 구조화된 모노레포는 모듈화와 효율적인 코드 공유의 토대 위에 구축됩니다. 코드를 응집력 있는 단위로 구성하고 재사용을 촉진함으로써 유지 관리가 가능하고 확장 가능과 유연한 코드 베이스를 만들 수 있습니다.</p>
<p><img src="https://miro.medium.com/v2/resize:fit:720/format:webp/0*vc43lZS8HZcfu4qZ.png" alt="https://www.toptal.com/front-end/guide-to-monorepos"></p>
<div style="width:100%; text-align: center;">
  <a href="https://www.toptal.com/front-end/guide-to-monorepos" >https://www.toptal.com/front-end/guide-to-monorepos</a>
</div>
<br/>

<p>모듈화와 코드 공유가 모노레포에서 어떻게 효과적으로 구현되는지 이해하기 위해 이러한 주요 개념과 실제 적용 사례를 살펴보겠습니다.</p>
<p><img src="https://miro.medium.com/v2/resize:fit:720/format:webp/1*VEZp1oW-WM47bUNUArooJw.png" alt="Modularization and code sharing (Image by the author)"></p>
<div style="width:100%; text-align: center;">
  Modularization and code sharing (Image by the author)
</div>
<br/>

<p>잘 구조화된 모듈화 및 코드 공유 관행에 대한 투자는 장기적으로 성과를 거둡니다. 이는 즉각적인 개발 워크플로우를 간소화할 뿐만 아니라 더 적응력 있고 탄력적이며 미래 지향적인 모노레포 아키텍처를 위한 발판을 마련해 줍니다. 👏</p>
<p>지금까지 의존성 그래프부터 캐싱 전략까지 많은 내용을 다루었습니다. 이제 이러한 지식을 간결한 요약으로 정리하여 모노레포 매니저를 평가할 때 염두에 두어야 할 가장 중요한 측면을 강조해 보겠습니다. 우와!</p>
<h2 id="요약">요약</h2>
<p>지금까지 살펴본 내용을 바탕으로 올바른 모노레포 관리자를 선택하려면 몇 가지 주요 고려 사항을 고려해야 합니다.</p>
<p>✔️ <strong>그래프 유형</strong>: 도구가 의존성 그래프, 작업 그래프 또는 둘 다 사용하는지 여부를 이해하는 것은 프로젝트 관계를 관리하고 빌드 프로세스를 조율하는 기능을 평가하는 데 중요합니다.</p>
<p>✔️ <strong>작업 스케줄링</strong>: 스케줄링 방식(위상, 병렬, 증분 등)은 빌드 시간과 리소스 활용도에 직접적인 영향을 미치므로 모노레포의 성능을 최적화하는 데 중요한 고려 사항입니다.</p>
<p>✔️ <strong>캐싱 메커니즘</strong>: 특히 대규모 모노레포의 경우 빌드 속도를 높이려면 강력한 캐싱 전략이 필수적입니다. 캐싱 유형(파일 기반, 콘텐츠 주소 지정 가능, 분산)과 팀의 워크플로 및 인프라와 어떻게 연계되는지 고려하세요.</p>
<p>✔️ <strong>모듈화 및 코드 공유</strong>: 도구가 모듈식 코드 구성 및 코드 공유 메커니즘을 지원하는지 평가하세요. 잘 정의된 모듈의 생성을 용이하게 하고 모노레포 내에서 코드 재사용을 촉진하나요?</p>
<p>✔️ <strong>추가 고려 사항</strong>: 커뮤니티 지원, 문서화, 라이선스 비용, 기존 기술 스택 및 빌드 도구와의 통합과 같은 요소도 잊지 마세요. 이러한 측면은 전반적인 경험과 모노레포의 장기적인 성공에 큰 영향을 미칠 수 있습니다.</p>
<p>모노레포 관리의 복잡한 환경을 살펴보는 여정이 목적지에 도달했습니다. 이제 결론을 향해 나아갑시다! 🌼</p>
<h2 id="결론">결론</h2>
<p>지식으로 가득 찬 놀라운 여정이었습니다!</p>
<p>지금까지 유향 비순환 그래프(DAG)의 우아한 구조부터 빌드 프로세스를 조율하는 복잡한 알고리즘에 이르기까지 모노레포 관리를 뒷받침하는 이론적 토대를 살펴봤습니다.</p>
<p>의존성 해결, 캐싱 전략, 작업 스케줄링, 모듈화, 코드 공유와 같은 개념은 단순한 학문적 호기심이 아니라 효율적이고 확장 가능과 유지 관리가 가능한 모노레포를 만들 수 있는 기본 구성 요소입니다.</p>
<p>이러한 새로운 이해로 무장한 저희는 이제 선도적인 모노레포 관리자(<code>Nx</code>, <code>Turborepo</code>, <code>PNPM Workspace</code>)에 대한 실질적인 탐구에 착수할 준비가 되었습니다.</p>
<p>다가오는 대결에서는 이러한 도구가 이론을 실제로 어떻게 구현하는지, 각각 모노레포 워크플로우를 최적화하는 고유한 솔루션을 제공하는지 살펴볼 것입니다.</p>
<p>지금까지 살펴본 그래프 이론 개념과 알고리즘을 개발한 뛰어난 인재들에게 진심으로 감사의 말씀을 전합니다. 이러한 이론적 구조가 현대 소프트웨어 개발, 특히 모노레포의 영역에서 필수적인 도구가 된 것을 목격하게 되어 매우 보람을 느낍니다.</p>
<p>계속 지켜봐 주세요! 🍀 🌻</p>
<p>새로운 글과 새로운 모험에서 다시 만날 때까지! ❤️</p>
<p>제 글을 읽어주셔서 감사합니다.</p>
<pre><code class="language-bash">저와 연락하고 싶으신가요?
GitHub에서 저를 찾을 수 있습니다: https://github.com/helabenkhalfallah</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[(번역) 자바스크립트와 타입스크립트에서 메모이제이션란 무엇인가요?]]></title>
            <link>https://velog.io/@tap_kim/memoization-in-js-ts</link>
            <guid>https://velog.io/@tap_kim/memoization-in-js-ts</guid>
            <pubDate>Mon, 25 Nov 2024 16:08:32 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>원문: <a href="https://janhesters.com/blog/what-is-memoization-in-javascript-typescript">https://janhesters.com/blog/what-is-memoization-in-javascript-typescript</a></p>
</blockquote>
<p>메모이제이션(memoization)와 캐싱은 프로그래밍의 기본 개념입니다.</p>
<p>메모이제이션을 이해하면 리액트의 고차 컴포넌트인 <code>memo</code>와 <code>useCallback</code> 및 <code>useMemo</code> 훅, 리덕스의 선택자(selector) 등 메모이제이션을 기반으로 하는 개념들을 더 쉽게 이해할 수 있습니다.</p>
<p>이 글에서는 자바스크립트와 타입스크립트의 예시를 통해 메모이제이션에 대해 자세히 소개합니다.</p>
<iframe width="720" height="407" src="https://www.youtube.com/embed/KwXWI8pm0Vk?si=MISP-QWofgqyL8LO" title="BFCache explained & how to make your webpage compatible with it" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

<h2 id="메모이제이션란-무엇인가요">메모이제이션란 무엇인가요?</h2>
<p><strong>메모이제이션</strong>는 <strong>계산을 줄여</strong> <strong>성능을 향상시키</strong>는 방법입니다.</p>
<p>처음 계산하여 결괏값이 나왔다면, 이를 저장합니다. 동일한 인수에 대한 결과가 다시 필요한 경우 다시 계산하는 대신 저장된 결과를 사용합니다.</p>
<p>메모이제이션를 구현하는 방법에는 두 가지가 있습니다.</p>
<ol>
<li>암시적 캐싱</li>
<li>데코레이터 함수</li>
</ol>
<h2 id="암시적-캐싱">암시적 캐싱</h2>
<p>암시적 캐싱은 함수 내에서 캐싱 로직을 수동으로 구현하는 경우입니다.</p>
<p>간단한 <code>add</code> 함수가 있는데 이를 메모로 저장한다고 가정해 보겠습니다.</p>
<pre><code class="language-js">function add(a, b) {
  return a + b;
}</code></pre>
<p><code>add</code> 함수를 다시 작성하여 결과를 메모하도록 할 수 있습니다.</p>
<p>이 글에 따라 코딩하려면 새 npm 프로젝트를 초기화하세요.</p>
<pre><code class="language-bash">npm init --y</code></pre>
<p>그런 다음 <code>memoizeAdd()</code>라는 함수를 작성합니다.</p>
<pre><code class="language-js">// implicit-caching.js
function memoizeAdd() {
  const cache = {};

  return function memoizedAdd(a, b) {
    const key = `${a},${b}`;

    if (key in cache) {
      return cache[key];
    } else {
      const result = a + b;

      cache[key] = result;

      return result;
    }
  };
}

const memoizedAdd = memoizeAdd();

console.log(memoizedAdd(3, 4)); // 결과를 계산하고 캐시합니다.
console.log(memoizedAdd(3, 4)); // 캐시에서 결과를 검색합니다.
console.log(memoizedAdd(5, 6)); // 새 결과를 계산하고 캐시합니다.
console.log(memoizedAdd(3, 4)); // 여전히 캐시에서 결과를 검색합니다.</code></pre>
<p>여기서 무슨 일이 일어나는지 자세히 살펴보겠습니다.</p>
<p><code>memoizeAdd()</code>에서는 일반적으로 해시 테이블 또는 객체인 캐시를 생성하여 함수 호출의 결과를 저장합니다. 각 항목은 함수의 입력을 키로, 출력을 값으로 사용합니다.</p>
<pre><code class="language-js">const cache = {
  &quot;3,1&quot;: 4,
  &quot;9001,2012&quot;: 11_013,
};</code></pre>
<p>다음으로, 두 개의 숫자 <code>a</code>와 <code>b</code>를 받는 <code>memoizedAdd</code> 함수를 반환합니다.</p>
<p>각 고유한 인수 조합은 하나의 키와 매칭됩니다.</p>
<p>이 함수는 메인 로직을 실행하기 전에 현재 입력을 키로 사용하여 캐시를 확인합니다. 입력이 캐시에 있으면 캐시 된 결과를 반환하고, 입력이 캐시에 없으면 결과를 계산하여 캐시에 저장한 다음 결과를 반환합니다.</p>
<p><code>memoizedAdd</code> 함수를 호출하면 결과가 매개변수와 함께 저장됩니다. 동일한 매개변수를 사용하여 다시 호출하면 결과는 캐시에서 가져옵니다. 새로운 매개변수인 경우 함수는 결과를 다시 계산합니다.</p>
<p>이 코드를 실행하면 다음과 같은 출력이 표시됩니다.</p>
<pre><code class="language-bash">$ node implicit-caching.js
7
7
11
7</code></pre>
<h2 id="메모이제이션를-위한-데코레이터-함수">메모이제이션를 위한 데코레이터 함수</h2>
<p>고차 함수를 지원하는 언어에서는 데코레이터를 사용하여 함수의 핵심 로직을 변경하지 않고 메모이제이션를 추가할 수 있습니다.</p>
<p>고차 함수는 함수를 인수로 받아 함수를 반환하는 함수입니다.</p>
<p>이 개념이 생소하다면 <a href="https://janhesters.com/blog/unleash-javascripts-potential-with-functional-programming">“함수형 프로그래밍으로 자바스크립트의 잠재력 발휘하기”</a>를 읽어보세요. 이 글에서는 고차 함수를 비롯한 여러 가지를 원론적인 수준에서 설명합니다.</p>
<p>이제 메모이제이션를 위한 데코레이터 함수의 간단한 예제를 살펴보세요.</p>
<pre><code class="language-js">// decorator-function.js
function memoize(fn) {
  const cache = new Map();

  return function (...args) {
    const key = JSON.stringify(args);

    if (cache.has(key)) {
      return cache.get(key);
    }

    const result = fn.apply(this, args);
    cache.set(key, result);

    return result;
  };
}

function add(a, b) {
  return a + b;
}

const memoizedAdd = memoize(add);

console.log(memoizedAdd(3, 4)); // 결과를 계산하고 캐시합니다.
console.log(memoizedAdd(3, 4)); // 캐시 된 결과를 반환합니다.
console.log(memoizedAdd(5, 6)); // 새 결과를 계산하여 캐시합니다.
console.log(memoizedAdd(3, 4)); // 여전히 캐시에서 결과를 검색합니다.</code></pre>
<p>이 코드도 단계별로 살펴보겠습니다.</p>
<p>먼저 다른 함수인 <code>fn</code>을 인수로 받는 <code>memoize</code> 함수를 만듭니다.</p>
<p>이 함수는 <code>Map</code>을 사용하여 캐시를 초기화하는 것으로 시작합니다. 이 캐시는 함수 호출의 결과를 저장합니다. 객체 대신 <code>Map</code>을 사용하는 이유는 더 다양한 키 유형을 처리할 수 있기 때문입니다.</p>
<pre><code class="language-js">const cache = new Map();

// 숫자를 캐시의 키로 사용합니다.
map.set(&quot;[3,1]&quot;, 4);
// 함수를 캐시의 키로 사용합니다.
map.set(&quot;[function increment(), function double()]&quot;, 42);</code></pre>
<p>반환된 함수는 원하는 수의 인수를 받습니다. 이 함수는 인수를 문자열로 직렬화하여 캐시의 키로 사용합니다.</p>
<p>반환된 익명 함수는 실행하기 전에 해당 키가 캐시에 이미 존재하는지 확인합니다. 존재하는 경우 함수는 캐시 된 결과를 반환합니다.</p>
<p>캐시에 키가 없는 경우 익명 함수는 <code>fn.apply(this, args)</code>를 사용하여 기존 함수를 호출합니다. 이 함수는 결과를 캐시에 저장하고 반환합니다.</p>
<p>이제 <code>add</code> 함수를 <code>memoize</code> 함수에 전달하여 메모이제이션할 수 있습니다.</p>
<p><code>memoizedAdd</code> 함수는 암시적 캐싱 예제에서와 마찬가지로 작동합니다.</p>
<p>코드를 실행하면 동일한 결과가 나타납니다.</p>
<pre><code class="language-bash">$ node implicit-caching.js
7
7
11
7</code></pre>
<h2 id="재귀와-피보나치-수열">재귀와 피보나치 수열</h2>
<p><code>add</code> 함수는 계산 비용이 적게 듭니다.</p>
<p>메모의 이점을 경험하려면 값비싼 함수를 메모해야 합니다.</p>
<p>대표적인 예가 <code>피보나치</code> 함수입니다. 피보나치 함수는 암기 외에도 코딩 면접에서 자주 출제되는 주제이므로 이 함수를 배워두는 건 유용합니다.</p>
<p>피보나치 수열은 각 숫자가 앞의 두 숫자의 합을 나열한 숫자입니다. 일반적으로 0과 1로 시작합니다. 피보나치 수열은 다음과 같이 시작됩니다.</p>
<p>0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ...</p>
<p>이를 수학적으로 표현하면 다음과 같이 표현할 수 있습니다.</p>
<p>$$
F_n =
\begin{cases}
0 &amp; \text{if } n = 0, \
1 &amp; \text{if } n = 1, \
F_{n-1} + F_{n-2} &amp; \text{if } n &gt; 1.
\end{cases}
$$</p>
<p>자바스크립트에서 <code>피보나치</code> 함수는 다음과 같습니다.</p>
<pre><code class="language-js">// fibonacci.js;
function fibonacci(n) {
  if (n &lt;= 1) {
    return n;
  }

  return fibonacci(n - 1) + fibonacci(n - 2);
}</code></pre>
<p><code>n</code>이 0 또는 1이면 함수는 <code>n</code>을 바로 반환합니다. 이는 피보나치 수열의 처음 두 숫자입니다.</p>
<p><code>n</code>이 1보다 큰 값인 경우, 이 함수는 <code>fibonacci(n - 1)</code>과 <code>fibonacci(n - 2)</code>를 한 번씩 두 번 호출하고 이 두 호출의 결과를 더합니다.</p>
<p>다음은, 이 함수가 <code>fibonacci(4)</code>를 계산하는 방법을 보여주는 표입니다.</p>
<table>
<thead>
<tr>
<th>호출</th>
<th>n</th>
<th>계산</th>
<th>결과</th>
</tr>
</thead>
<tbody><tr>
<td><code>fibonacci(4)</code></td>
<td>4</td>
<td><code>fibonacci(3) + fibonacci(2)</code></td>
<td>3 + 2 = 5</td>
</tr>
<tr>
<td><code>fibonacci(3)</code></td>
<td>3</td>
<td><code>fibonacci(2) + fibonacci(1)</code></td>
<td>2 + 1 = 3</td>
</tr>
<tr>
<td><code>fibonacci(2)</code></td>
<td>2</td>
<td><code>fibonacci(1) + fibonacci(0)</code></td>
<td>1 + 0 = 1</td>
</tr>
<tr>
<td><code>fibonacci(1)</code></td>
<td>1</td>
<td>1</td>
<td>1</td>
</tr>
<tr>
<td><code>fibonacci(0)</code></td>
<td>0</td>
<td>0</td>
<td>0</td>
</tr>
<tr>
<td><code>fibonacci(2)</code></td>
<td>2</td>
<td><code>fibonacci(1) + fibonacci(0)</code></td>
<td>1 + 0 = 1</td>
</tr>
<tr>
<td><code>fibonacci(1)</code></td>
<td>1</td>
<td>1</td>
<td>1</td>
</tr>
<tr>
<td><code>fibonacci(0)</code></td>
<td>0</td>
<td>0</td>
<td>0</td>
</tr>
</tbody></table>
<p><code>fibonacci(2)</code>는 <code>fibonacci(4)</code>를 호출할 때 한 번, <code>fibonacci(3)</code>에서 다시 두 번 계산하는 것을 볼 수 있습니다.</p>
<p>이러한 불필요한 중복 계산은 <code>fibonacci</code> 함수에 대한 입력 값이 커질수록 성능에 점점 부정적인 영향을 미칩니다. 이 글을 끝까지 읽어보시고 메모이제이션이 어떻게 이러한 계산을 매우 빠르게 최적화하는지 알아보시기를 바랍니다.</p>
<h2 id="스택-추적">스택 추적</h2>
<p><code>fibonacci</code> 함수를 메모하기 전에 메모한 함수에 이름 속성을 추가하세요. 이렇게 하면 함수에서 오류가 발생할 때 스택 추적이 더 쉬워집니다.</p>
<pre><code class="language-js">// fibonacci.js;
function memoize(fn) {
  const cache = new Map();

  function memoizedFunction(...args) {
    const key = JSON.stringify(args);

    if (cache.has(key)) {
      return cache.get(key);
    }

    const result = fn.apply(this, args);
    cache.set(key, result);

    return result;
  }

  // 이름 속성 추가
  Object.defineProperty(memoizedFunction, &quot;name&quot;, {
    value: `memoized_${fn.name}`,
    configurable: true,
  });

  return memoizedFunction;
}</code></pre>
<p><code>Object.defineProperty</code>를 사용하여 <code>memoizedFunction</code>의 <code>name</code> 속성을 <code>memoized_${fn.name}</code>과 같이 보다 설명적인 값으로 설정합니다(여기서 <code>fn.name</code>은 원래 함수의 이름입니다). configurable 속성을 설정하려면 필요한 경우 속성을 추가로 수정하거나 삭제할 수 있습니다.</p>
<h1 id="메모이제이션-성능-측정">메모이제이션 성능 측정</h1>
<p>이제 메모이제이션 효과를 측정할 준비가 되었습니다. <code>fibonacci</code> 함수를 메모하고 큰 숫자를 계산하는 데 걸리는 시간을 측정하세요.</p>
<pre><code class="language-js">// fibonacci.js;
function memoize(fn) {
  const cache = new Map();

  function memoizedFunction(...args) {
    const key = JSON.stringify(args);

    if (cache.has(key)) {
      return cache.get(key);
    }

    const result = fn.apply(this, args);
    cache.set(key, result);

    return result;
  }

  Object.defineProperty(memoizedFunction, &quot;name&quot;, {
    value: `memoized_${fn.name}`,
    configurable: true,
  });

  return memoizedFunction;
}

function fibonacci(n) {
  if (n &lt;= 1) {
    return n;
  }

  return fibonacci(n - 1) + fibonacci(n - 2);
}

const memoizedFibonacci = memoize(fibonacci);

function measurePerformance(func, arg) {
  const startTime = process.hrtime.bigint();
  const result = func(arg);
  const endTime = process.hrtime.bigint();
  // 나노초를 밀리초로 변환합니다.
  const duration = (endTime - startTime) / BigInt(1000000);
  console.log(`${func.name}(${arg}) = ${result}, Time: ${duration}ms`);
}

const n = 42;

console.log(&quot;Starting performance measurement:&quot;);
measurePerformance(fibonacci, n);
measurePerformance(memoizedFibonacci, n);
// 두 번째 호출로 캐싱 효과를 표시합니다.
measurePerformance(memoizedFibonacci, n);</code></pre>
<p><code>measurePerformance</code> 함수는 나노초 단위의 정확한 타임스탬프를 제공하는 <code>process.hrtime.bigint()</code>로 시작 시간과 종료 시간을 캡처하여 주어진 함수의 실행 시간을 평가하고 보고합니다. 주어진 인수를 사용하여 함수를 실행한 후 종료 시간에서 시작 시간을 빼고 결과를 나노초 단위로 변환하여 밀리초 단위로 지속 시간을 계산합니다. 그런 다음 함수 이름, 인수, 출력 및 실행 기간을 콘솔에 기록합니다.</p>
<p><code>measurePerformance</code>을 사용하면 일반적인 피보나치 함수와 메모이제이션된 버전의 성능을 비교할 수 있습니다.</p>
<p>다음은 성능 측정 결과입니다.</p>
<pre><code class="language-bash">$ node fibonacci.js
Starting performance measurement:
fibonacci(42) = 267914296, Time: 2405ms
memoized_fibonacci(42) = 267914296, Time: 2394ms
memoized_fibonacci(42) = 267914296, Time: 0ms</code></pre>
<p>보시다시피 캐시에서 결과가 검색되므로 메모된 버전을 두 번째로 실행하는 것은 즉시 이루어집니다.</p>
<h2 id="캐시-지우기">캐시 지우기</h2>
<p>메모이제이션를 사용하는 경우 애플리케이션의 리소스와 성능을 관리하기 위해 <code>.clear()</code> 메서드를 구현할 수 있습니다. 그 이유는 다음과 같습니다.</p>
<ul>
<li><strong>메모리 관리</strong>: 캐시를 수동으로 제거할 수 있어 리소스를 확보할 수 있습니다.</li>
<li><strong>데이터 최신성</strong>: 오래되거나 잘못된 데이터를 제거하여 캐시 된 결과가 정확하게 유지되도록 합니다.</li>
<li><strong>캐시 동작 제어</strong>: 데이터 처리에 영향을 미치는 이벤트나 조건에 대응하여 캐시를 재설정할 수 있는 기능을 제공합니다.</li>
<li><strong>테스트 및 디버깅</strong>: 캐시 된 데이터의 간섭 없이 알려진 상태에서 함수를 작동할 수 있어 안정적인 테스트에 중요합니다.</li>
<li><strong>성능 최적화</strong>: 주기적인 재설정을 통해 크기와 조회 비용을 관리하여 캐시 효율성을 유지합니다.</li>
</ul>
<p>이제 캐시 비우기 기능을 추가하세요.</p>
<pre><code class="language-js">// fibonacci.js;
function memoize(fn) {
  const cache = new Map();

  function memoizedFunction(...args) {
    const key = JSON.stringify(args);

    if (cache.has(key)) {
      return cache.get(key);
    }

    const result = fn.apply(this, args);
    cache.set(key, result);

    return result;
  }

  memoizedFunction.clear = function clear() {
    cache.clear();
  };

  Object.defineProperty(memoizedFunction, &quot;name&quot;, {
    value: `memoized_${fn.name}`,
    configurable: true,
  });

  return memoizedFunction;
}

function fibonacci(n) {
  if (n &lt;= 1) {
    return n;
  }

  return fibonacci(n - 1) + fibonacci(n - 2);
}

const memoizedFibonacci = memoize(fibonacci);

function measurePerformance(func, arg) {
  const startTime = process.hrtime.bigint();
  const result = func(arg);
  const endTime = process.hrtime.bigint();
  // 나노초를 밀리초로 변환합니다.
  const duration = (endTime - startTime) / BigInt(1000000);
  console.log(`${func.name}(${arg}) = ${result}, Time: ${duration}ms`);
}

const n = 42;

// 메모이제이션된 피보나치를 측정합니다.
measurePerformance(memoizedFibonacci, n);

// 메모이제이션된 피보나치 두 번째 호출을 측정합니다.
measurePerformance(memoizedFibonacci, n);

// 캐시를 지우고 다시 측정합니다.
console.log(&quot;Clearing cache and measuring again:&quot;);
memoizedFibonacci.clear();
measurePerformance(memoizedFibonacci, n);</code></pre>
<p>그런 다음 결과가 이미 성공적으로 메모이제이션된 후 <code>.clear()</code> 메서드를 호출하도록 사용 예제를 수정합니다.</p>
<pre><code class="language-bash">$ node fibonacci.js
memoized_fibonacci(42) = 267914296, Time: 2456ms
memoized_fibonacci(42) = 267914296, Time: 0ms
Clearing cache and measuring again:
memoized_fibonacci(42) = 267914296, Time: 2484ms</code></pre>
<p>보시다시피 캐시를 지우면 함수가 결과를 다시 계산합니다.</p>
<h2 id="타입스크립트로-메모이제이션하기">타입스크립트로 메모이제이션하기</h2>
<p>지금까지 모든 코드는 자바스크립트로 작성되었습니다. 이제 코드 예제를 타입스크립트로 전환하겠습니다.</p>
<p>타입스크립트와 노드 유형을 설치합니다.</p>
<pre><code class="language-bash">$ npm i --save-dev typescript @types/node

added 3 packages, and audited 4 packages in 1s

found 0 vulnerabilities</code></pre>
<p>그런 다음 새 타입스크립트 프로젝트를 초기화합니다.</p>
<pre><code class="language-bash">$  npx tsc --init


Created a new tsconfig.json with:
                                                                      TS
  target: es2016
  module: commonjs
  strict: true
  esModuleInterop: true
  skipLibCheck: true
  forceConsistentCasingInFileNames: true


You can learn more at https://aka.ms/tsconfig</code></pre>
<p>이제 타입스크립트로 <code>memoize</code> 함수를 작성할 수 있습니다.</p>
<pre><code class="language-ts">// fibonacci.ts;
type AnyFunction = (...args: any[]) =&gt; any;

interface MemoizedFunction&lt;T extends AnyFunction&gt; extends CallableFunction {
  (...args: Parameters&lt;T&gt;): ReturnType&lt;T&gt;;
  clear: () =&gt; void;
}

function memoize&lt;T extends AnyFunction&gt;(fn: T): MemoizedFunction&lt;T&gt; {
  const cache = new Map&lt;string, ReturnType&lt;T&gt;&gt;();

  const memoizedFunction = function (...args: Parameters&lt;T&gt;): ReturnType&lt;T&gt; {
    const key = JSON.stringify(args);

    if (cache.has(key)) {
      return cache.get(key)!;
    }

    const result = fn(...args);
    cache.set(key, result);

    return result;
  } as MemoizedFunction&lt;T&gt;;

  memoizedFunction.clear = function clear() {
    cache.clear();
  };

  Object.defineProperty(memoizedFunction, &quot;name&quot;, {
    value: `memoized_${fn.name}`,
    configurable: true,
  });

  return memoizedFunction;
}

function fibonacci(n: number): number {
  if (n &lt;= 1) {
    return n;
  }

  return fibonacci(n - 1) + fibonacci(n - 2);
}

const memoizedFibonacci = memoize(fibonacci);

function measurePerformance(
  func: MemoizedFunction&lt;typeof fibonacci&gt;,
  arg: number
) {
  const startTime = process.hrtime.bigint();
  const result = func(arg);
  const endTime = process.hrtime.bigint();
  // 나노초를 밀리초로 변환합니다.
  const duration = (endTime - startTime) / BigInt(1000000);
  console.log(`${func.name}(${arg}) = ${result}, Time: ${duration}ms`);
}

const n = 42;

// 메모이제이션된 피보나치를 측정합니다.
measurePerformance(memoizedFibonacci, n);

// 메모이제이션된 피보나치 두 번째 호출을 측정합니다.
measurePerformance(memoizedFibonacci, n);

// 캐시를 지우고 다시 측정합니다.
memoizedFibonacci.clear();
measurePerformance(memoizedFibonacci, n);</code></pre>
<p>여기서는 타입스크립트 일반 함수 타입을 나타내는 <code>AnyFunction</code> 타입을 만듭니다. 이 타입은 유연하며 임의의 수의 임의의 타입의 인수를 받고 임의의 타입을 반환하는 모든 함수를 허용합니다.</p>
<p>다음으로, 기본 제공되는 <code>CallableFunction</code> 인터페이스를 확장하고 명확한 메서드를 도입하는 제네릭 형식의 <code>MemoizedFunction</code> 인터페이스를 정의합니다. 여기서 <code>T</code>는 <code>AnyFunction</code> 타입과 일치하는 모든 함수로 제한되는 제네릭 형식의 매개변수입니다. 이 인터페이스는 메모이제이션 된 함수가 동일한 매개변수를 받아들이고 동일한 타입을 반환한다는 점에서 원래 함수처럼 작동하도록 보장하며, <code>clear</code> 메서드를 추가로 노출합니다.</p>
<p>이제 메모이제이션 된 함수가 <code>MemoizedFunction</code> 인터페이스를 반환하도록 메모이제이션 함수를 구현할 수 있습니다. <code>as</code> 키워드를 사용하면 반환되는 함수가 <code>MemoizedFunction</code>임을 타입스크립트에 알릴 수 있습니다.</p>
<p>이제 <code>memoizedFibonacci</code> 위로 마우스를 가져가면 타입스크립트가 함수의 올바른 타입을 알 수 있습니다.</p>
<pre><code class="language-ts">const memoizedFibonacci: MemoizedFunction&lt;(n: number) =&gt; number&gt;;</code></pre>
<p>또한 타입스크립트는 메모이제이션 된 함수의 <code>.clear()</code> 메서드에 대해서도 알고 있습니다.</p>
<pre><code class="language-ts">(property) MemoizedFunction&lt;(n: number) =&gt; number&gt;.clear: () =&gt; void</code></pre>
<h2 id="메모이제이션를-사용한-재귀">메모이제이션를 사용한 재귀</h2>
<p>이 함수에는 아직 문제가 있습니다. 현재 메모이제이션는 각 인수에 대한 전체 함수 호출의 결과만 캐시하고 중간 재귀 호출의 결과는 캐시<strong>하지 않습니다</strong>.</p>
<p>특정 문제를 해결하기 위해 메모이제이션 없이 재귀를 사용하면 어떤 잠재적인 문제가 있을까요?</p>
<p>중복 계산으로 인해 계산 시간과 리소스 사용량이 많이 증가한다는 것입니다.</p>
<p>큰 숫자 <code>n</code>으로 함수를 호출한 다음 <code>n + 1</code>로 함수를 호출합니다.</p>
<pre><code class="language-ts">// fibonacci.ts
// ... 이전 코드는 동일합니다.

const n = 42;

measurePerformance(memoizedFibonacci, n);
measurePerformance(memoizedFibonacci, n + 1);</code></pre>
<p>그런 다음 코드를 실행하여 <code>42</code>와 <code>43</code>의 성능 차이를 확인합니다.</p>
<pre><code class="language-bash">$ npx tsx example-5.ts
memoized_fibonacci(42) = 267914296, Time: 2442ms
memoized_fibonacci(43) = 433494437, Time: 3975ms</code></pre>
<p>보시다시피 <code>fibonacci(43)</code>는 결과가 이미 캐시 되어 있음에도 불구하고 <code>fibonacci(42)</code>를 처음부터 다시 계산하기 때문에 훨씬 더 오래 걸립니다. 다른 모든 중간 재귀 호출에서도 동일한 문제가 발생합니다.</p>
<p><code>피보나치</code> 함수를 구현할 때 메모이제이션를 사용하면 이 문제를 해결할 수 있습니다.</p>
<pre><code class="language-ts">// fibonacci.ts
// ... `memoize`도 위와 동일합니다.

function fibonacci(n: number): number {
  if (n &lt;= 1) {
    return n;
  }

  return memoizedFibonacci(n - 1) + memoizedFibonacci(n - 2);
}

const memoizedFibonacci = memoize(fibonacci);

function measurePerformance(
  func: MemoizedFunction&lt;typeof fibonacci&gt;,
  arg: number
) {
  const startTime = process.hrtime.bigint();
  const result = func(arg);
  const endTime = process.hrtime.bigint();
  // 나노초를 밀리초로 변환합니다.
  const duration = (endTime - startTime) / BigInt(1000000);
  console.log(`${func.name}(${arg}) = ${result}, Time: ${duration}ms`);
}

const numbers = [10, 20, 30, 40, 42, 43, 500];

numbers.forEach((n) =&gt; {
  measurePerformance(memoizedFibonacci, n);
});</code></pre>
<p><code>피보나치</code> 함수를 정의한 후 메모가 된 버전을 사용하여 다음 숫자를 계산합니다.</p>
<p>이제 다양한 큰 숫자에 대해 함수를 실행해 보세요.</p>
<pre><code class="language-bash">$ npx tsx fibonacci.ts
memoized_fibonacci(10) = 55, Time: 0ms
memoized_fibonacci(20) = 6765, Time: 0ms
memoized_fibonacci(30) = 832040, Time: 0ms
memoized_fibonacci(40) = 102334155, Time: 0ms
memoized_fibonacci(42) = 267914296, Time: 0ms
memoized_fibonacci(43) = 433494437, Time: 0ms
memoized_fibonacci(500) = 1.394232245616977e+104, Time: 0ms</code></pre>
<p>이제 아무리 큰 함수를 호출해도 계산이 거의 즉각적으로 이루어집니다.</p>
<p>따라서 메모이제이션를 진행할 때는 스스로에게 물어보세요. “무엇을 메모이제이션하고 싶은가?&quot;라고 자문해 보세요. 외부 함수만 메모이제이션하고 싶으신가요, 아니면 구현의 중간 결과도 메모이제이션하고 싶으신가요?</p>
<h2 id="왜-메모이제이션인가-사용-사례">왜 메모이제이션인가? (사용 사례)</h2>
<p>메모이제이션는 언제 사용하나요?</p>
<p>메모이제이션는 다음과 같은 경우에 유용합니다.</p>
<ul>
<li><strong>재귀 알고리즘</strong>: 피보나치 수열에서 볼 수 있듯이, 동일한 계산을 반복하는 재귀 함수는 메모이제이션를 통해 중복 연산을 피함으로써 큰 이점을 얻을 수 있습니다.</li>
<li><strong>데이터베이스 쿼리</strong>: 비용이 많이 드는 데이터베이스 쿼리의 결과를 캐시할 수 있습니다.</li>
<li><strong>데이터 가져오기</strong>: 웹 개발에서 메모이제이션는 API 호출의 응답을 캐시할 수 있습니다. 이렇게 하면 동일한 매개 변수로 반복되는 요청에 대해 캐시 된 데이터를 제공함으로써 네트워크 트래픽을 줄이고 응답 시간을 단축할 수 있습니다.</li>
<li><strong>데이터 변환</strong>: 처리 시간이 많이 소요되는 변환 결과를 캐시할 수 있습니다.</li>
<li><strong>컴포넌트 렌더링 방지</strong>: 리액트와 같은 프런트엔드 프레임워크에서 메모이제이션은 props가 동일하게 유지되는 한 컴포넌트의 불필요한 재렌더링을 방지할 수 있습니다.</li>
</ul>
<h2 id="메모이제이션-트레이드-오프">메모이제이션 트레이드 오프</h2>
<p>아시다시피 메모이제이션는 반복 계산을 피함으로써 작업의 시간 복잡성을 줄이고 컴퓨팅 리소스에 대한 부하를 줄여 성능을 향상시킵니다.</p>
<p>하지만 메모이제이션에는 단점도 있습니다.</p>
<ul>
<li><strong>메모리 사용량</strong>: 메모이제이션는 함수 호출 결과를 저장하여 메모리 소비를 증가시키므로 메모리가 제한된 환경에서는 문제가 될 수 있습니다.</li>
<li><strong>복잡성</strong>: 메모이제이션를 추가하면 코드 베이스가 복잡해지고 버그 및 유지 관리 문제를 방지하기 위해 신중한 캐시 관리가 필요합니다.</li>
<li><strong>캐시 관리</strong>: 캐시 제거와 같은 캐시 무효화 전략을 구현하여 오래되거나 잘못된 데이터를 방지해야 하는데, 이는 까다로울 수 있습니다.</li>
<li><strong>사이드 이펙트</strong>: 부작용이 있는 함수는 함수를 호출할 때마다 부작용이 발생해야 하므로 메모이제이션에 문제가 될 수 있습니다. 메모이제이션는 주로 순수 함수와 함께 사용해야 합니다.</li>
<li><strong>동시성 문제</strong>: 멀티 스레드 애플리케이션에서 메모이제이션는 캐시 액세스 시 경합 조건을 피하고 스레드 안전을 보장하기 위해 신중하게 처리해야 합니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[(번역) Gumroad가 htmx를 선택하지 않은 이유
]]></title>
            <link>https://velog.io/@tap_kim/why-gumroad-didnt-choose-htmx</link>
            <guid>https://velog.io/@tap_kim/why-gumroad-didnt-choose-htmx</guid>
            <pubDate>Thu, 07 Nov 2024 09:32:22 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>원문: <a href="https://htmx.org/essays/why-gumroad-didnt-choose-htmx/">https://htmx.org/essays/why-gumroad-didnt-choose-htmx/</a></p>
</blockquote>
<p>Gumroad에서는 최근 <a href="https://helper.ai/">Helper</a>라는 새로운 프로젝트에 착수했습니다. CEO로서 저는 이 프로젝트에 <a href="https://htmx.org/">htmx</a>를 사용하는 것에 대해 꽤 긍정적이었지만, 팀의 몇몇 멤버들은 이에 대해 그다지 열정적이지 않았습니다.</p>
<p>저의 낙관적인 생각은 이전에 리액트를 사용해 본 경험에서 비롯되었습니다. 리액트는 우리 요구사항에 비해 과하다고 느꼈습니다. 그렇기 때문에 htmx가 프론트엔드를 매우 가볍게 유지하는 데 좋은 솔루션이 될 수 있다고 생각했습니다.</p>
<p><a href="https://htmx.org/img/gumroad-red.jpeg"><img src="https://htmx.org/img/gumroad-red.jpeg" alt=""></a>
[htmx가 포함된 소스 - 이미지 클릭해서 보기]</p>
<p>사실 저는 슬랙에서 팀원들과 이러한 생각을 공유했습니다.</p>
<blockquote>
<p>&quot;<a href="https://htmx.org/%EB%8A%94">https://htmx.org/는</a> 간단한 상호작용을 추가하는 방법이 될 수 있습니다.&quot;</p>
</blockquote>
<p>처음에는 좋은 선택처럼 보였습니다! 하지만, Gumroad의 한 엔지니어는 이렇게 말했죠.</p>
<blockquote>
<p>&quot;HTMX는 (공식적으로는) JS 환경이 지나치게 복잡해졌음을 조롱하는 밈으로, 테일윈드가 인라인 CSS의 다른 구문인 것처럼 HTMX는 인라인 JS의 다른 구문일 뿐입니다.&quot;</p>
</blockquote>
<p>그러나 툴킷에서 자리를 잡은 테일윈드와 달리 htmx는 우리의 목적에 맞게 확장되지 않았고, 적어도 우리의 사용 사례에서는 고객에게 최고의 사용자 경험을 제공하지 못했습니다.</p>
<p>그 이유는 다음과 같습니다.</p>
<ol>
<li><p><strong>직관성 및 개발자 경험</strong>: htmx에서도 제대로 작동할 수 있었겠지만, Next.js로 모든 것을 구현하는 것이 훨씬 더 직관적이고 재미있었습니다. Next.js에서는 개발 프로세스가 자연스럽게 느껴졌지만, htmx에서는 부자연스럽고 강제적으로 느껴지는 경우가 많았습니다. 예를 들어, 동적 유효성 검사 및 조건부 필드가 있는 복잡한 form을 만들 때 리액트에서는 간단한 클라이언트 측 작업을 처리하기 위해 복잡한 서버 로직을 작성해야 했습니다.</p>
</li>
<li><p><strong>UX의 한계</strong>: htmx는 결국 앱을 Rails에서 CRUD를 다루는 방식으로 밀어붙였고, 그 결과 기본적으로 열악한 사용자 경험(적어도 지루하고 일반적인)을 만들었습니다. 우리는 이런 비생산적인 상황에 지속적으로 싸워야 했습니다. 예를 들어, 워크플로우 빌더에 드래그 앤 드론 인터페이스를 구현하는 것은 상당히 어려웠습니다. htmx에서는 많은 우회 방법이 필요했고, 리액트 라이브러리를 통해 얻을 수 있는 매끄러운 경험에 비해 어색하게 느껴졌습니다.</p>
</li>
<li><p><strong>AI와 도구 지원</strong>: Next.js는 AI 도구들이 친숙하게 인식하는 반면, htmx에는 오픈 소스 학습 데이터의 부족으로 인해 그렇지 않았습니다. 이는 Rails가 직면한 문제와 유사합니다. 비록 결정적인 문제는 아니었지만, 개발 속도와 문제 해결의 용이성에 영향을 미쳤습니다. 문제가 발생했을 때 리액트/Next.js에 사용할 수 있는 풍부한 리소스 덕분에 문제 해결이 훨씬 빨라졌습니다.</p>
</li>
<li><p><strong>확장성 문제</strong>: 프로젝트가 복잡해지면서 htmx가 우리의 요구 사항을 따라가지 못한다는 것을 느꼈습니다. 처음에 매력을 느꼈던 단순함은 더 정교한 상호 작용과 상태 관리를 구현하려고 시도하면서 제약을 가져오기 시작했습니다. 예를 들어, 실시간 공동 작업과 복잡한 데이터 시각화 같은 기능을 추가하면서 여러 컴포넌트 간의 상태를 관리하는 것이 htmx의 서버 중심 접근 방식에서는 점점 더 어려워졌습니다.</p>
</li>
<li><p><strong>커뮤니티와 생태계</strong>: 리액트/Next.js 생태계는 방대하고 성숙하여 우리가 직면한 거의 모든 문제에 대한 솔루션을 제공합니다. htmx를 사용하면서 우리는 종종 새롭게 개발하거나 기능을 타협해야 했습니다. 이 문제는 특히 써드파티 서비스와 라이브러리를 통합해야 할 때 더욱 분명합니다. 종종 리액트에 바인딩 되는 기능은 있지만 htmx에 상응하는 기능은 없는 경우가 많았습니다.</p>
</li>
</ol>
<p><a href="https://htmx.org/img/gumroad-green.jpeg"><img src="https://htmx.org/img/gumroad-green.jpeg" alt=""></a>
[NextJS를 사용한 소스 - 이미지 클릭하여 보기]</p>
<p>결국 저희는 리액트/Next.js로 전환했고, 이는 저희가 찾던 복잡한 UX를 구축하는 데 매우 적합했습니다. 현재로서는 이 결정에 만족하고 있습니다.
이를 통해 더 빠르게 움직이고, 더 매력적인 사용자 경험을 만들고, 기존의 다양한 도구와 라이브러리를 활용할 수 있게 되었으니까요.</p>
<p><a href="https://htmx.org/img/gumroad-helper-before-after.png"><img src="https://htmx.org/img/gumroad-helper-before-after.png" alt=""></a>
[Gumroad Helper 전과 후 - 이미지 클릭하여 보기]</p>
<p>이 경험을 통해 얻은 소중한 교훈은 가벼운 대안을 고려하는 것도 중요하지만, 프로젝트와 함께 성장하고 장기적인 비전을 지원할 수 있는 기술을 선택하는 것도 마찬가지로 중요하다는 것입니다. Helper의 경우 리액트와 Next.js의 선택이 옳았다는 것을 입증했습니다.</p>
<p>이전 후 핵심 고객을 위해 앱 사용자 경험을 대폭 업그레이드할 수 있었습니다.</p>
<ol>
<li><p><strong>드래그 앤드 드롭 기능</strong>: 워크플로우 빌더의 핵심 기능 중 하나는 드래그 앤드 드롭을 통해 단계를 재정렬할 수 있는 기능입니다. htmx로 드래그 앤드 드롭을 구현할 수 있지만, 사용할 수 있는 솔루션이 투박하고 상당한 커스텀 자바스크립트가 필요하다는 것을 알게 되었습니다. 이에 반해 리액트 생태계는 최소한의 설정으로 부드럽게 동작하고 접근성을 준수하는 드래그 앤드 드롭을 제공한 react-beautiful-dnd와 같은 라이브러리를 제공합니다.</p>
</li>
<li><p><strong>복잡한 상태 관리</strong>: 각 워크플로우 단계에는 고유한 구성과 조건부 로직이 있습니다. 사용자가 이를 편집할 때 다른 단계에 미치는 영향을 UI에 실시간으로 반영해야 합니다. 이를 위해서는 수많은 서버요청이나 복잡한 클라이언트 측 상태 관리가 필요하며, 이는 htmx의 서버 중심 철학에 어긋납니다. 리액트의 상태 관리 솔루션(예: useState 또는 Redux와 같은 고급 옵션)은 이 작업을 훨씬 더 간단하게 만들어줍니다.</p>
</li>
<li><p><strong>동적 폼(Form) 생성</strong>: 각 단계 유형에 대한 구성은 다르며 사용자 입력에 따라 변경될 수 있습니다. 이러한 동적 폼을 생성하고 상태를 처리하는 것은 리액트 컴포넌트 모델에서 더 직관적이었습니다. htmx에서는 이러한 폼을 생성하고 유효성 검사를 위해 더 복잡한 서버 로직을 작성해야 했습니다.</p>
</li>
<li><p><strong>실시간 공동 작업</strong>: 이 스크린샷에는 보이지 않지만, 여러 사용자가 동시에 워크플로우를 편집할 수 있는 기능을 구현했습니다. 웹 소켓과 리액트로 구현하는 것은 비교적 간단했지만, htmx를 사용했다면 실시간 업데이트를 처리하기 위해 더 복잡한 서버 로직과 커스텀 자바스크립트가 필요했을 것입니다.</p>
</li>
<li><p><strong>성능 최적화</strong>: 워크플로우가 점점 더 커지고 복잡해지면서 렌더링 최적화에 대한 세밀한 제어가 필요했습니다. 리액트의 가상 DOM과 useMemo 및 useCallback 같은 훅을 통해 htmx에서는 쉽게 사용할 수 없거나 직관적이지 않았던 방식으로 성능을 최적화할 수 있었습니다.</p>
</li>
</ol>
<p>이런 문제가 htmx로 극복할 수 없는 것은 아니지만, 이와 같은 문제를 해결하다 보면 htmx의 강점에서 벗어나 자바스크립트가 많은 환경에서 더 자연스럽게 느껴지는 솔루션을 찾게 되는 경우가 많다는 것을 알게 되었습니다. 이런 깨달음은 리액트와 Next.js로 전환하기로 하는 데 큰 영향을 미쳤습니다.</p>
<p>우리는 많은 프로젝트, 특히 단순한 상호작용 모델을 사용하는 프로젝트나 기존 서버 렌더링 애플리케이션 위에 구축된 프로젝트에 htmx가 적합할 수 있음을 인정합니다. 우리가 htmx를 선택하지 않았다고 해서, 다른 개발자들이 htmx를 통해 얻은 긍정적인 경험이 잘못되었다는 것은 아닙니다. 핵심은 프로젝트의 특정 요구 사항을 이해하고 요구 사항에 가장 적합한 도구를 선택하는 것입니다.</p>
<p>우리의 경우 Helper 인터페이스의 복잡하고 상태 특성으로 인해 리액트와 Next.js가 더 적합했습니다. 그러나 우리는 htmx의 접근 방식을 계속 높이 평가하고 있으며 향후 프로젝트에서 htmx의 강점이 우리의 요구 사항과 더 잘 부합하는 경우 다시 한번 고려할 수 있습니다.</p>
<p>하지만 요구사항이 진화하고 새로운 기술이 등장함에 따라 기술 스택을 재평가할 기회는 언제나 열려 있습니다. 미래에 어떤 일이 일어날지 누가 알겠습니까?</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[(번역) 고급 수준의 자바스크립트 제너레이터에 대한 설명]]></title>
            <link>https://velog.io/@tap_kim/understanding-generators-in-javascript</link>
            <guid>https://velog.io/@tap_kim/understanding-generators-in-javascript</guid>
            <pubDate>Wed, 18 Sep 2024 10:11:08 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>원문: <a href="https://www.reactsquad.io/blog/understanding-generators-in-javascript">https://www.reactsquad.io/blog/understanding-generators-in-javascript</a></p>
</blockquote>
<p>제너레이터는 <a href="https://www.reactsquad.io/hire-fullstack-javascript-developers">자바스크립트 생태계</a>에서 강력하지만 잘 사용되지 않는 기능입니다. 대부분의 제너레이터 튜토리얼은 표면적인 부분만 다루고 있지만, 이 튜토리얼에서는 심층적으로 다루며 제너레이터의 이론에 대해 더 깊이 있게 알아볼 것입니다.</p>
<p>하지만 먼저 이 튜토리얼이 실제로 작동하는 모습을 보고 싶다면 아래 동영상 버전을 확인하세요.</p>
<iframe width="720" height="407" src="https://www.youtube.com/embed/FAK1YR4X-8I" title="BFCache explained & how to make your webpage compatible with it" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

<p>제너레이터는 주로 saga에서 많이 사용되지만, 그 외에도 다양한 활용 사례가 있습니다. 이 글에서는 그중 몇 가지를 소개할 것입니다. &quot;<strong>제너레이터가 무엇인가?</strong>&quot;에 대해 대답하자면 자바스크립트에서 <strong>제너레이터는 풀 스트림(pull stream)</strong> 입니다. 이 정의를 좀 더 자세히 살펴본 후, 몇 가지 예시를 알아보겠습니다. 먼저 &#39;풀(pull)&#39;과 &#39;스트림(steeam)&#39;이라는 두 가지 용어를 이해하는 것이 중요합니다.</p>
<h2 id="스트림은-무엇인가요">스트림은 무엇인가요?</h2>
<p><strong>스트림</strong>은 <strong>시간이 지남에 따라 발생하는 데이터</strong>입니다. 스트림에는 푸시 스트림과 풀 스트림의 두 가지 유형이 있습니다.</p>
<h2 id="푸시-스트림은-무엇인가요">푸시 스트림은 무엇인가요?</h2>
<p><strong>푸시 스트림</strong>은 <strong>언제 데이터가 전달되는지</strong> 제어할 수 <strong>없는</strong> 방식입니다. 푸시 스트림의 예는 다음과 같습니다.</p>
<ul>
<li>웹 소켓</li>
<li>디스크에서 파일 읽기</li>
<li>서버에서 보낸 이벤트</li>
</ul>
<p>아래 예시에서는 Node.js에서 디스크에서 큰 파일을 읽는 예시를 통해 자바스크립트에서 푸시 스트림을 살펴보겠습니다.</p>
<pre><code class="language-js">const fs = require(&quot;fs&quot;);
const readStream = fs.createReadStream(&quot;./largeFile.txt&quot;);

readStream.on(&quot;data&quot;, (chunk) =&gt; {
  console.log(&quot;data received&quot;, chunk.length);
});

readStream.on(&quot;end&quot;, () =&gt; {
  console.log(&quot;finished reading file&quot;);
});

readStream.on(&quot;error&quot;, (error) =&gt; {
  console.log(&quot;an error occured while reading the file&quot;, error);
});</code></pre>
<h2 id="풀-스트림은-무엇인가요">풀 스트림은 무엇인가요?</h2>
<p><strong>풀 스트림</strong>은 <strong>언제 데이터를 요청할지</strong> 직접 제어할 수 <strong>있는</strong> 방식입니다. 곧 자바스크립트에서 제너레이터 코드를 볼 때, 풀 스트림에 대한 코드 예시를 보게 될 것입니다. 하지만 먼저 다른 개념을 이해할 필요가 있습니다.</p>
<h2 id="lazy-vs-eager">Lazy vs Eager</h2>
<p>프로그래밍에서 데이터는 두 가지 기본 방식으로 처리될 수 있습니다. 즉시 처리(eagerly) 또는 지연 처리(lazily) 방식입니다.</p>
<h3 id="즉시처리eager">즉시처리(Eager)</h3>
<p>즉시 처리방식은 결과가 그 순간에 필요하지 않더라도 데이터를 즉시 평가하는 방식을 말합니다. 푸시 스트림은 그 특성상 즉시 처리 방식으로 동작합니다 (다른 예: 배열 메서드, 프로미스)</p>
<pre><code class="language-js">// 배열 메서드를 사용한 Eager 평가
const numbers = [1, 2, 3, 4, 5];

// map은 배열의 모든 요소를 즉시 처리합니다.
const squares = numbers.map((num) =&gt; {
  console.log(`Squaring ${num}`);
  return num * num;
});

console.log(&quot;squares:&quot;, squares); // [1, 4, 9, 16, 25]</code></pre>
<p>물론, &quot;그런데 프로미스는 왜 즉시 평가라고 하시는 건가요? 결과가 늦게 나오잖아요.&quot;라는 의문을 품을 수 있습니다.</p>
<p>자바스크립트의 프로미스는 여러 가지 이유로 즉시 평가를 나타낸다고 할 수 있습니다.</p>
<ol>
<li><strong>즉시 실행</strong>: 프로미스가 새롭게 생성될 때 전달된 함수(실행자 함수)는 프로미스가 구성되자마자 즉시 실행됩니다.</li>
<li><strong>되돌릴 수 없는 작업</strong>: 일단 실행자 함수가 실행되기 시작하면, 호출한 코드에 의해 중지되거나 일시 중지될 수 없습니다. 함수가 수행하는 작업의 결과(성공 또는 거부)는 가능한 한 빨리 처리되도록 자바스크립트 이벤트 루프에 대기열로 추가됩니다.</li>
<li><strong>지연 옵션 없음</strong>: 프로미스에는 값이 필요할 때까지 실행자의 실행을 연기하거나 취소하는 내장 메커니즘이 없습니다.</li>
<li><strong>부작용</strong>: 프로미스의 즉시 처리 특성은 실행자 함수에 포함된 부작용(예: API 호출, 타임아웃 또는 I/O 작업)이 프로미스 생성과 함께 즉시 발생한다는 것을 의미합니다.</li>
</ol>
<p>다음 예는 프로미스가 즉시 실행되는 방법을 보여줍니다.</p>
<pre><code class="language-js">// 프로미스와 배열 메서드를 사용한 eager 평가

console.log(&quot;Before promise&quot;);

let promise = new Promise((resolve, reject) =&gt; {
  console.log(&quot;Inside promise executor&quot;);
  resolve(&quot;Resolved data&quot;);
});

console.log(&quot;After promise&quot;);

promise.then((result) =&gt; {
  console.log(result);
});</code></pre>
<p>다음의 결과가 출력됩니다.</p>
<pre><code class="language-bash">$ node eager-promise-example.js
Before promise
Inside promise executor
After promise
Resolved data</code></pre>
<h3 id="지연-처리lazy">지연 처리(Lazy)</h3>
<p><strong>지연 처리</strong>방식은 값이 필요할 때만 평가된다는 의미입니다. 풀 스트림은 <strong>지연 처리</strong>됩니다.</p>
<p>동기식 예로는 피연산자 선택 연산자를 들 수 있습니다.</p>
<pre><code class="language-js">// 논리 연산자를 사용한 Lazy 평가

function processData(data) {
  console.log(`Processing ${data}`); // 로그아웃되지 않습니다. 🚫
  return data * data;
}

console.log(&quot;Lazy evaluation starts&quot;);
const data = 5;
const isDataProcessed = false;

// 논리 AND 연산자를 사용한 Lazy 평가.
const result = isDataProcessed &amp;&amp; processData(data);
console.log(&quot;Result:&quot;, result); // false</code></pre>
<p>이 코드를 실행하면 다음과 같은 출력을 확인할 수 있습니다.</p>
<pre><code class="language-bash">$ node lazy-evaluation-example.js
Lazy evaluation starts
Result: false</code></pre>
<p><code>isDataProcessed</code>가 <code>false</code>이므로 <code>processData</code> 함수가 실행되지 않고 콘솔에 &quot;Processing 5&quot;가 표시되지 않습니다. 이는 표현식이 결과를 얻는 데 필요한 것만 평가한다는 것을 보여줍니다.</p>
<h2 id="제너레이터란-무엇인가요">제너레이터란 무엇인가요?</h2>
<p>제너레이터는 자바스크립트에서 풀 스트림입니다. 즉, <strong>실행을 일시 중지</strong>했다가 나중에 <strong>다시 시작</strong>할 수 있는 특수한 종류의 함수입니다.</p>
<p><strong>제너레이터</strong> 객체는 <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*">제너레이터 함수</a>에 의해 반환되며 <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_iterable_protocol">이터러블 프로토콜</a>과 <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_iterator_protocol">이터레이터 프로토콜</a>을 모두 준수합니다.</p>
<pre><code class="language-js">function* myGenerator() {
  yield &quot;Hire senior&quot;;
  yield &quot;React engineers&quot;;
  yield &quot;at ReactSquad.io&quot;;
}

const iterator = myGenerator();

// 제너레이터를 이터레이터로 사용합니다.
console.log(iterator.next()); // { done: false, value: &quot;Hire senior&quot; }
console.log(iterator.next()); // { done: false, value: &quot;React engineers&quot; }
console.log(iterator.next()); // { done: false, value: &quot;at ReactSquad.io&quot; }
console.log(iterator.next()); // { done: true, value: undefined }

// 제너레이터를 이터러블로 사용합니다.
for (let string of myGenerator()) {
  console.log(number); // &quot;Hire senior&quot; &quot;React engineers&quot; &quot;at ReactSquad.io&quot;
}</code></pre>
<p><code>.next()</code> 메서드 외에도 제너레이터에는 <code>.return()</code> 및 <code>.throw()</code> 메서드도 있습니다.</p>
<ul>
<li><strong>.return()</strong> - <code>.return()</code> 메서드는 제너레이터의 실행을 종료하고 지정된 값을 반환하며 <code>finally</code> 블록도 실행되도록 트리거합니다.</li>
<li><strong>.throw()</strong> - <code>.throw()</code> 메서드는 마지막 <code>yield</code>가 호출된 지점에서 에러를 던질 수 있게 해줍니다. 이 에러는 잡혀서 처리될 수 있으며, 또는 제네레이터가 <code>finally</code> 블록을 통해 정리할 수 있습니다. 만약 에러가 처리되지 않으면 제네레이터가 중지되고 완료로 표시됩니다.</li>
</ul>
<pre><code class="language-js">function* numberGenerator() {
  try {
    yield 1;
    yield 2;
    yield 3;
  } finally {
    console.log(&quot;Cleanup complete&quot;);
  }
}

const generator = numberGenerator();

// 제너레이터를 정상적으로 사용
console.log(generator.next()); // { done: false, value: 1 }
console.log(generator.next()); // { done: false, value: 2 }

// return()을 사용해서 제너레이터를 일찍 완료
console.log(generator.return(10)); // { done: true, value: 10 }
// return() 이후에는 더 이상 값을 반환하지 않음
console.log(generator.next()); // { done: true, value: undefined }

// 예제에서 throw에 대한 제너레이터 재설정
const newGenerator = numberGenerator();
console.log(newGenerator.next()); // { done: false, value: 1 }

// throw()를 사용해서 오류 알림
try {
  newGenerator.throw(new Error(&quot;Something went wrong&quot;));
} catch (e) {
  console.log(e.message); // &quot;Something went wrong&quot;
}
// throw() 이후 제너레이터 종료
console.log(newGenerator.next()); // { done: true, value: undefined }</code></pre>
<p>인자를 사용해서 <code>next()</code>를 호출할 때 숫자나 다른 값을 제너레이터에 전달할 수도 있습니다.</p>
<p>다음 예제에서 <em>언제</em>, <em>어떤</em> 로그가 출력될 지 예측해 보세요.</p>
<pre><code class="language-js">function* moreNumbers(x) {
  console.log(&quot;x&quot;, x);
  const y = yield x + 2;
  console.log(&quot;y&quot;, y);
  const z = yield x + y;
  console.log(&quot;z&quot;, z);
}

const it2 = moreNumbers(40);

console.log(it2.next());
console.log(it2.next(2012));
console.log(it2.next());</code></pre>
<p>이 예제는 제너레이터 함수 <code>moreNumbers</code>가 일련의 <code>.next()</code> 호출 중에 수신한 입력에 따라 값을 조작하고 산출하는 방법을 보여줍니다.</p>
<p>결과를 살펴보고 예측한 결과가 맞는지 확인해 보세요.</p>
<pre><code class="language-js">const it2 = moreNumbers(40);

// x: 40
console.log(it2.next()); // { value: 42, done: false }

// y: 2012
console.log(it2.next(2012)); // { value: 2052, done: false }

// z: undefined
console.log(it2.next()); // { value: undefined, done: true }</code></pre>
<p>더 많은 숫자를 생성하는 함수를 완전히 이해할 수 있도록 각 단계를 세분화해 보겠습니다.</p>
<table>
<thead>
<tr>
<th>Step</th>
<th>Code Line</th>
<th>Console Output</th>
<th>Explanation</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td><code>const it2 = moreNumbers(40)</code></td>
<td></td>
<td><code>x</code>를 40으로 설정하여 제너레이터를 초기화합니다.</td>
</tr>
<tr>
<td>2</td>
<td><code>console.log(it2.next());</code></td>
<td><code>{ value: 42, done: false }</code></td>
<td>제너레이터가 시작되어 <code>x</code>을 40으로 기록한 다음 42(<code>x + 2</code>)를 산출합니다.</td>
</tr>
<tr>
<td>3</td>
<td><code>console.log(it2.next(2012));</code></td>
<td><code>{ value: 2052, done: false }</code></td>
<td><code>y</code>를 2012로 다시 시작하여 <code>y</code>를 로그하고 2052(<code>x + y</code>)를 산출합니다.</td>
</tr>
<tr>
<td>4</td>
<td><code>console.log(it2.next());</code></td>
<td><code>{ value: undefined, done: true }</code></td>
<td>다시 시작하고 <code>z</code>를 <code>undefined</code>(새 입력 없음)로 기록한 후 완료합니다.</td>
</tr>
</tbody></table>
<h2 id="제너레이터-사용-사례">제너레이터 사용 사례</h2>
<p>제너레이터는 세 가지 주요 사용 사례가 있습니다.</p>
<ol>
<li><strong>지연 평가</strong> - 필요에 따라 데이터를 생성하거나 대규모 또는 무한 데이터 집합을 처리합니다.</li>
<li><strong>비동기 프로그래밍</strong> - 비동기 작업을 처리합니다.</li>
<li><strong>반복기</strong> - 복잡한 흐름을 위해 단계 중간에 멈출 수 있습니다.</li>
</ol>
<p>앞서 디스크에서 파일을 푸시 스트림으로 읽는 예제를 살펴보았습니다. 다음은 제너레이터를 사용하여 읽은 데이터를 풀 스트림으로 변환하기 위해 작성하는 방법입니다.</p>
<pre><code class="language-js">const fs = require(&quot;fs&quot;);

function getChunkFromStream(stream) {
  return new Promise((resolve, reject) =&gt; {
    stream.once(&quot;data&quot;, (chunk) =&gt; {
      stream.pause();
      resolve(chunk);
    });

    stream.once(&quot;end&quot;, () =&gt; {
      resolve(null);
    });

    stream.once(&quot;error&quot;, (err) =&gt; {
      reject(err);
    });

    stream.resume();
  });
}

async function* readFileChunkByChunk(filePath) {
  const stream = fs.createReadStream(filePath);
  let chunk;

  while ((chunk = await getChunkFromStream(stream))) {
    yield chunk;
  }
}

const generator = readFileChunkByChunk(&quot;./largeFile.txt&quot;);

(async () =&gt; {
  for await (const chunk of generator) {
    console.log(&quot;data received&quot;, chunk.length);
  }
})();</code></pre>
<h2 id="실제-사례">실제 사례</h2>
<p>saga는 비동기 I/O 연산을 처리하는 대표 예시입니다. 하지만 향후 Redux에 대한 시리즈 기사에서 saga를 사용하는 방법을 배우게 될 것입니다.</p>
<p>그리고 일반적으로 값을 언제 가져올지 제어하고 싶을 때 제너레이터를 사용합니다.</p>
<p>이 테스트 예제를 살펴보시기 바랍니다.</p>
<pre><code class="language-js">test(&#39;온보딩된 오너 사용자의 경우: 초대 링크 생성 UI를 표시하고 조직의 멤버들을 보여주며, 사용자가 자신의 역할을 변경할 수 있게 한다.&#39;, async ({ page }) =&gt; {
  // 조직 내 역할에 대한 제너레이터
  function* roleGenerator() {
    const allRoles = Object.values(ORGANIZATION_MEMBERSHIP_ROLES);
    for (const role of allRoles) {
      yield role;
    }
  }
  const roleIterator = roleGenerator();
  const data = await setup({
    page,
    role: ORGANIZATION_MEMBERSHIP_ROLES.OWNER,
    numberOfOtherTeamMembers: allRoles.length,
  });
  const { organization, sortedUsers, user } = data;

  // 팀원 설정 페이지로 이동
  await page.goto(`/organizations/${organization.slug}/settings/team-members`);

  // 각 팀원을 반복하여 제너레이터를 사용하여 역할을 할당
  for (let index = 0; index &lt; sortedUsers.length; index++) {
    const memberListItem = page.getByRole(&#39;list&#39;, { name: /team members/i }).getByRole(&#39;listitem&#39;).nth(index);
    const otherUser = sortedUsers[index];

    // 현재 사용자를 제외한 각 팀원의 역할을 변경
    if (otherUser.id !== user.id) {
      await memberListItem.getByRole(&#39;button&#39;, { name: /member/i }).click();
      const role = roleIterator.next().value!;
      await page.getByRole(&#39;option&#39;, { name: role }).getByRole(&#39;button&#39;).click();
      await page.keyboard.press(&#39;Escape&#39;);
    }
  }

  await teardown(data);
});</code></pre>
<p>이 테스트에서는 조직 내 사용자의 역할 목록을 순차적으로 제공하는 <code>roleGenerator</code>를 정의합니다. 이 접근 방식을 사용하면 역할 관리 기능의 일부로, 미리 정의된 역할 목록에서 각 사용자에게 고유한 역할을 동적으로 할당하는 것을 테스트할 수 있습니다.</p>
<p>이 테스트 예시에서 배열 대신 제네레이터가 사용된 이유는 <code>sortedUsers</code> 배열 내에서 메인 사용자(즉, user.id)가 어디에 위치해 있는지 알 수 없기 때문입니다. 제네레이터는 풀 스트림이기 때문에, 필요한 시점에만 역할 값을 요청하여 가져올 수 있습니다.</p>
<p>이 영상이 마음에 드셨다면 제 유튜브 채널도 마음에 드실 겁니다. <a href="https://www.youtube.com/@JanHesters/">여기</a>에서 확인하세요!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[번역] Suspense를 지원하는 라이브러리를 직접 구축하며 Suspense 학습하기]]></title>
            <link>https://velog.io/@tap_kim/react-learn-suspense</link>
            <guid>https://velog.io/@tap_kim/react-learn-suspense</guid>
            <pubDate>Wed, 24 Jul 2024 05:37:24 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>원문: <a href="https://www.bbss.dev/posts/react-learn-suspense/">https://www.bbss.dev/posts/react-learn-suspense/</a></p>
</blockquote>
<blockquote>
<p>📝 <strong>참고</strong></p>
<p><strong><code>Suspense를 지원하는</code> 데이터 원본만 Suspense 컴포넌트를 활성화합니다. 여기에는 다음이 포함됩니다.</strong></p>
<ul>
<li><a href="https://relay.dev/docs/guided-tour/rendering/loading-states/">Relay</a> 및 <a href="https://nextjs.org/docs/getting-started/react-essentials">Next.js</a>와 같은 Suspense 지원 프레임워크를 사용한 데이터 페칭</li>
<li><a href="https://react.dev/reference/react/lazy">lazy</a>를 이용한 컴포넌트 코드 지연 로딩</li>
<li><a href="https://react.dev/reference/react/use">use</a>를 이용하는 Promise의 값 읽기
Suspense는 이펙트 또는 이벤트 핸들러 내부에서 데이터를 가져오는 시점을 감지하지 <strong>못합니다</strong>.</li>
</ul>
<p>위의 <code>Albums</code> 컴포넌트에서 데이터를 로드하는 정확한 방법은 프레임워크마다 다릅니다. <code>Suspense 지원</code> 프레임워크를 사용하는 경우 해당 데이터 페칭 문서에서 자세한 내용을 확인할 수 있습니다.</p>
<p>독립적인 프레임워크를 사용하지 않는 한 데이터 페칭을 위한 <code>Suspense 지원</code>은 아직 지원되지 않습니다. <code>Suspense 지원</code>에 대한 데이터 원본을 구현하기 위한 요구 사항은 불안정하고 문서화되어 있지 않습니다. Suspense와 데이터 소스를 통합하기 위한 공식 API는 향후 리액트의 새로운 버전에서 출시될 예정입니다.</p>
<p><em><a href="https://react.dev/reference/react/Suspense#displaying-a-fallback-while-content-is-loading">공식 문서 자료 제공</a></em></p>
</blockquote>
<p>Suspense(동시성 렌더링과 함께)는 리액트 <a href="https://legacy.reactjs.org/blog/2018/10/23/react-v-16-6.html">v16.6.0</a> 부터 제공되던 기능이었습니다. 하지만 <code>React.lazy</code>와 &quot;Suspense 지원 라이브러리&quot;의 제한된 앱 외에는 실제로 사용되는 것을 자주 보지 못했습니다.</p>
<p>무슨 일이 있던 걸까요?</p>
<p>리액트 v19 릴리스가 임박한 현재, Suspense는 아직 최적기에 사용할 준비가 되지 않았습니다. API와 내부가 아직 불완전해 보입니다. 사실, 리액트 팀이 Suspense API를 불완전하다고 생각하는 것인지 API에 대한 문서가 전혀 존재하지 않습니다. <a href="https://19.react.dev/reference/react/Suspense">Suspense 문서</a>에서는 Suspense를 사용하는 유일한 방법은 &quot;Suspense 지원 프레임워크&quot;뿐이라고 명시하고 있습니다.</p>
<p>문서에서 API를 의도적으로 숨기는 것은 좋지않다고 생각하지만, 괜찮아요! 그들의 게임에 동참해보죠! Suspense 지원 라이브러리를 만들어서 사용해 봅시다.</p>
<p>그 과정에서 Suspense의 베일을 하나씩 벗겨보겠습니다.</p>
<h2 id="철학">철학</h2>
<p>Suspense로 바로 들어가기 전에 간단한 데이터 페칭 컴포넌트를 만들어서 우리가 어디에 있는지 살펴봅시다.</p>
<p>리액트 101 강의에서는 일반적으로 데이터 페칭시 아래와 같은 코드를 작성하도록 가르칩니다.</p>
<pre><code class="language-tsx">const Artist = ({ artistName }) =&gt; {
  const [artistInfo, setArtistInfo] = useState(null);

  useEffect(() =&gt; {
    fetch(`https://api/artists/${artistName}`)
      .then((res) =&gt; res.json())
      .then((json) =&gt; setArtistInfo(json));
  }, [artistName]);

  return (
    &lt;div&gt;
      &lt;h2&gt;{artistName}&lt;/h2&gt;
      &lt;p&gt;{artistInfo.bio}&lt;/p&gt;
    &lt;/div&gt;
  );
};</code></pre>
<p>이 강좌의 다음 내용은 이 코드가 실제로는 좋은 코드가 아니라는 내용입니다. 다음과같이 많은 부분이 누락되어 있기 때문입니다.</p>
<ul>
<li>에러 상태 처리</li>
<li>대기 상태 처리</li>
<li>경쟁 조건 처리</li>
<li>공유 캐싱(이 부분은 나중에 다룰 예정입니다.)</li>
</ul>
<p>구현이 완료되면 컴포넌트는 다음과 같이 보입니다.</p>
<pre><code class="language-tsx">const Artist = ({ artistName }) =&gt; {
  const [artistInfo, setArtistInfo] = useState(null);
  const [error, setError] = useState(null);
  const [isPending, setIsPending] = useState(false);

  useEffect(() =&gt; {
    let stale = false;
    setIsPending(true);
    setError(null);

    fetch(`https://api/artists/${artistName}`)
      .then((res) =&gt; res.json())
      .then((json) =&gt; {
        if (!stale) {
          setIsPending(false);
          setArtistInfo(json);
        }
      })
      .catch((err) =&gt; setError(err));

    // 기본적으로 AbortController가 수행하는 작업은 다음과 같습니다.
    return () =&gt; {
      stale = true;
    };
  }, [artistName]);

  if (isPending) return &lt;SpinnerFallback /&gt;;
  if (error !== null) return &lt;ErrorFallback /&gt;;

  return (
    &lt;div&gt;
      &lt;h2&gt;{artistName}&lt;/h2&gt;
      &lt;p&gt;{artistInfo?.bio}&lt;/p&gt;
    &lt;/div&gt;
  );
};</code></pre>
<p>이 시점에서 강사는 보통 이 방식도 좋지만, 데이터를 가져올 때마다 이 코드를 반복하게 될 것입니다 라고 말하면서 여러분의 첫 번째 훅을 작성하는 방법을 알려줄 것입니다.</p>
<p>교육적인 필요성은 존중하지만 실제로는 그렇게 접근하는 것이 가장 좋은 방법은 아닙니다. 보류 및 에러 상태를 추적하는 것의 문제점은 훅에 이 로직을 묻어두더라도 여전히 컴포넌트 수준에서 해당 상태들을 처리해야 한다는 것입니다.</p>
<pre><code class="language-tsx">const Artist = ({ artistName }) =&gt; {
    const [artistInfo, isPending, error] = useFetch(`https://api/artists/${artistName}`)

    // 이 작업은 많지는 않지만 매번 이 작업을 수행해야 합니다.
    if (isPending) return &lt;SpinnerFallback /&gt;
    if (error !== null) return &lt;ErrorFallback /&gt;

    return (
        &lt;div&gt;
            &lt;h2&gt;{artistName}&lt;/h2&gt;
            &lt;p&gt;{artistInfo.bio}&lt;/p&gt;
            &lt;Album albumName={artistInfo.lastAlbumName}&gt;
        &lt;/div&gt;
    )
}

const Album = ({ albumName }) =&gt; {
     const [albumInfo, isPending, error] = useFetch(`https://api/artists/${albumName}`)

    // 이 작업은 많지는 않지만 매번 이 작업을 수행해야 합니다.
    if (isPending) return &lt;SpinnerFallback /&gt;
    if (error !== null) return &lt;ErrorFallback /&gt;

    return ...
}</code></pre>
<p><code>useFetch</code>가 <code>SpinnerFallback</code>과 <code>ErrorFallback</code> 반환을 처리할 수 있다고 상상해 보세요.</p>
<p><code>ErrorFallback</code>의 경우 <a href="https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary">에러 바운더리</a>가 있습니다.</p>
<pre><code class="language-tsx">const Artist = ({ artistName }) =&gt; {
    const [artistInfo, isPending, error] = useFetch(`https://api/artists/${artistName}`)

    if (isPending) return &lt;SpinnerFallback /&gt;
    // 가장 가까운 에러 바운더리에서 핸들링합니다. (useFetch로 이동할 수 있음)
    if (error !== null) return throw error

    return (
        &lt;div&gt;
            &lt;h2&gt;{artistName}&lt;/h2&gt;
            &lt;p&gt;{artistInfo.bio}&lt;/p&gt;
            &lt;Album albumName={artistInfo.lastAlbumName}&gt;
        &lt;/div&gt;
    )
}

const App = () =&gt; {
    return (
        &lt;ErrorBoundary fallback={&lt;ErrorFallback /&gt;}&gt;
            &lt;Artist artistName=&quot;Julian Casablanca&quot; /&gt;
        &lt;/ErrorBoundary&gt;
    )
}
</code></pre>
<p><code>SpinnerFallback</code>의 경우, 사실 이것이 바로 Suspense의 목적입니다. 혼란을 피하려고 fallback 메커니즘을 트리거하는 구현은 생략하겠지만(나중에 다시 다룰 테니 걱정하지 마세요!), 컴포넌트 관점에서 보면 다음과 같습니다.</p>
<pre><code class="language-tsx">const Artist = ({ artistName }) =&gt; {
    // 와우! 이제 훅이 에러와 로딩 상태를 모두 내부에서 처리합니다!
    const [artistInfo] = useFetch(`https://api/artists/${artistName}`)

    return (
        &lt;div&gt;
            &lt;h2&gt;{artistName}&lt;/h2&gt;
            &lt;p&gt;{artistInfo.bio}&lt;/p&gt;
            &lt;Album albumName={artistInfo.lastAlbumName}&gt;
        &lt;/div&gt;
    )
}

const App = () =&gt; {
    return (
        &lt;ErrorBoundary fallback={&lt;ErrorFallback /&gt;}&gt;
            &lt;Suspense fallback={&lt;SpinnerFallback /&gt;}&gt;
                &lt;Artist artistName=&quot;Julian Casablanca&quot; /&gt;
            &lt;/Supense&gt;
        &lt;/ErrorBoundary&gt;
    )
}</code></pre>
<p>이 시점에서는 <code>useFetch</code>의 구현 코드를 고민하지 마세요. 사용하려면 아직 갈 길이 멀기 때문입니다.</p>
<h2 id="주의-바운더리-재설정-상태">주의: 바운더리 재설정 상태</h2>
<p>보류 및 에러 상태를 처리하기 위해 바운더리를 사용할 때의 부작용은 바운더리에 도달하면 모든 자식이 <code>fallback</code>을 위해 버려진다는 것입니다. 그 결과 자식 컴포넌트의 상태가 손실됩니다.</p>
<pre><code class="language-tsx">const Counter = () =&gt; {
  // 에러 바운더리에 도달하면 모든 상태가 초기화됩니다.
  const [count, setCount] = useState(0);
  const [error, setError] = useState&lt;null | Error&gt;(null);

  // 에러 바운더리 트리거
  if (error) throw error;

  const increment = () =&gt; setCount((n) =&gt; n + 1);
  return (
    &lt;div&gt;
      &lt;p&gt;Counter: {count}&lt;/p&gt;
      &lt;button onClick={increment}&gt;Increment&lt;/button&gt;
      &lt;button
        onClick={() =&gt; {
          // 에러 바운더리를 트리거하려면,
          // 에러는 반드시, 렌더링 주기 중에 발생해야 합니다.
          // 이벤트 핸들러나 이펙트에서 에러를 던지면
          // 에러 바운더리를 트리거하지 않습니다!
          setError(new Error(&quot;Whoops something went wrong!&quot;));
        }}
      &gt;
        Throw Error
      &lt;/button&gt;
    &lt;/div&gt;
  );
};</code></pre>
<p>시도해 보세요.</p>
<iframe width="600" height="400"  src="https://codesandbox.io/embed/f3dx59?view=preview&amp;runonclick=1&amp;hidenavigation=1" title="error-boundary-reset-state" allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking" sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts"></iframe>

<p>이 방법은 괜찮은 방법입니다. 예를 들어 특정 컴포넌트 내에 에러가 발생한 경우 이를 제거하는 가장 확실한 방법은 완전히 재설정하는 것입니다.</p>
<hr>
<blockquote>
<p><strong>팁</strong>: 바운더리를 전략적으로 배치하면 애플리케이션의 어떤 부분이 함께 재설정되는지 제어할 수 있습니다. <br/>
루트만 래핑할 수 있는 것이 아니라 모든 컴포넌트를 래핑할 필요도 없다는 점을 기억하세요. <br/>
이는 에러 바운더리와 Suspense 바운더리 모두에 해당됩니다.</p>
</blockquote>
<hr>
<p>보류 중인 상태를 추적하는 측면에서 이는 어려운 문제입니다. 초기 렌더링에서 상태는 보류 중입니다. 그러면 Suspense 바운더리가 트리거됩니다. Suspense 바운더리가 재설정되면 컴포넌트가 다시 마운트되어 새 요청을 전송하고 바운더리를 다시 트리거합니다. 따라서 이러한 패러다임에서는 동일한 요청에 대한 지속적인 참조가 없으면 데이터를 성공적으로 표시할 수 없습니다.</p>
<p>따라서 컴포넌트의 생명주기보다 오래 지속되는 캐시가 필요합니다.</p>
<h2 id="공유-캐시-구축">공유 캐시 구축</h2>
<p>공유 캐시는 중요합니다. 동기화되지 않은 데이터, 너무 많은 요청을 보내는 것을 방지하고, 고급 데이터 관리 기능을 구현하는 데 필수적입니다. 우리의 경우, 컴포넌트가 자신의 요청 상태를 관리하지 않도록 하여 더욱 단순하게 만드는 것도 중요합니다. 그렇지 않으면 컴포넌트는 요청 상태를 잃어버리기 쉽습니다.</p>
<p>캐싱은 제대로 수행하기 어렵습니다. 사실, 데이터 페칭 라이브러리 대부분이 캐싱 라이브러리일 정도로 이는 매우 어렵습니다.</p>
<p>캐시를 의도적으로 단순하게 유지함으로써 &quot;<em>캐시 작성 방법</em>&quot;이라는 토끼 굴에 빠지는 것을 피할 수 있습니다. API를 사용하면 특정 URL을 요청할 수 있으며 이전과 마찬가지로 <code>데이터</code>, <code>보류</code> 중 및 <code>에러</code>에 대한 값을 반환합니다. 유일한 차이점은 요청 생명 주기가 요청하는 컴포넌트가 아닌 캐시 내에서 관리된다는 것입니다.</p>
<p>그런 다음 캐시를 실제 Suspense 지원 라이브러리로 전환합니다.</p>
<h3 id="fetchcache-클래스">FetchCache 클래스</h3>
<p>캐시가 갖는 정확한 API와 기능에 대해 창의적으로 접근할 수 있습니다. 다음은 간단한 구현 코드입니다.</p>
<pre><code class="language-tsx">class FetchCache {
  // 모든 요청을 위한 컨테이너
  requestMap = new Map();

  // 상태 업데이트의 브로드캐스팅을 위한 콜백 추적
  subscribers = new Set();

  fetchUrl(url, refetch) {
    const currentData = this.requestMap.get(url);

    if (currentData) {
      // 이 요청은 이미 진행 중입니다.
      if (currentData.status === &quot;pending&quot;) return currentData;
      // 데이터가 이미 캐시에 있고 명시적으로 다시 요청되지 않은 경우 currentData를 반환함
      // 상태는 완료(fulfilled)되었거나 거부(rejected)된 것으로 처리
      if (!refetch) return currentData;
    }

    // 경쟁 요청을 피하려고 상태를 보류 중으로 설정
    const pendingState = { status: &quot;pending&quot; };
    this.requestMap.set(url, pendingState);

    const broadcastUpdate = () =&gt; {
      // 실행하지 않기 위해 알림 지연
      // 렌더링 주기 중
      // https://reactjs.org/link/setstate-in-render
      setTimeout(() =&gt; {
        for (const callback of this.subscribers) {
          callback();
        }
      }, 0);
    };

    // 요청을 발송하고 관찰하기
    fetch(url)
      .then((res) =&gt; res.json())
      .then((data) =&gt; {
        // 성공 상태를 캐시에 저장
        this.requestMap.set(url, { status: &quot;fulfilled&quot;, value: data });
      })
      .catch((error) =&gt; {
        // 에러 상태를 캐시에 저장
        this.requestMap.set(url, { status: &quot;rejected&quot;, reason: error });
      })
      .finally(() =&gt; {
        // 어떤 일이 발생시 구독자에게 알림
        // 해당 상태가 갱신해야 함을 알림
        broadcastUpdate();
      });

    // 요청이 현재 보류 중임을 보고합니다.
    broadcastUpdate();

    // 요청이 현재 보류 중임을 보고합니다.
    return pendingState;
  }

  subscribe(callback) {
    this.subscribers.add(callback);
    return () =&gt; this.subscribers.delete(callback);
  }
}</code></pre>
<p>이 <code>FetchCache</code>에는 두 가지 메서드가 있습니다.</p>
<ul>
<li><p><code>fetchUrl(url, refetch)</code></p>
<p>URL을 요청합니다.<br/>
캐시에 있는 현재 상태를 반환합니다.</p>
</li>
<li><p><code>subscribe(callback)</code></p>
<p>캐시에서 새 데이터를 사용할 수 있을 때 알림을 위한 콜백을 등록합니다.<br/>
구독을 취소하는 함수를 반환합니다.</p>
</li>
</ul>
<h3 id="fetchcache-provider">FetchCache Provider</h3>
<p>이제 캐시가 생겼으니 이를 리액트 트리에 노출할 수단인 Context Provider가 필요합니다.</p>
<pre><code class="language-tsx">const fetchCacheContext = createContext(null);

const FetchCacheProvider = ({ children, fetchCache }) =&gt; {
  // 이 상태 훅은 리렌더를 트리거할 때만 사용됩니다.
  const [, setEmptyState] = useState({});
  const rerender = useCallback(() =&gt; setEmptyState({}), []);

  // 구독자를 fetchCache에 등록하는 이펙트
  useEffect(() =&gt; {
    const unsubscribe = fetchCache.subscribe(() =&gt; rerender());
    return unsubscribe;
  }, [fetchCache, rerender]);

  return (
    &lt;fetchCacheContext.Provider
      value={{
        // 깜짝 질문: 여기서 &#39;bind&#39;가 필요한 이유는 무엇인가요?
        fetchUrl: fetchCache.fetchUrl.bind(fetchCache),
      }}
    &gt;
      {children}
    &lt;/fetchCacheContext.Provider&gt;
  );
};</code></pre>
<h3 id="usefetch-훅">useFetch 훅</h3>
<p>마지막으로 <code>FetchCacheProvider</code>를 활용하는 <code>useFetch</code> 훅을 작성해 보겠습니다.</p>
<pre><code class="language-tsx">const useFetch = (url) =&gt; {
  const { fetchUrl } = useContext(fetchCacheContext);
  const state = fetchUrl(url);
  const isPending = state.status === &quot;pending&quot;;
  const error = state.reason;
  const data = state.value;

  // 데이터 새로 고침 허용
  const reload = () =&gt; fetchUrl(url, true);

  return [data, isPending, error, reload];
};</code></pre>
<h3 id="코드-실행">코드 실행!</h3>
<hr>
<blockquote>
<p>이 데모에서는 아티스트와 앨범 대신 사용자와 게시물을 사용합니다. 제공: <a href="https://jsonplaceholder.typicode.com">https://jsonplaceholder.typicode.com</a></p>
</blockquote>
<hr>
<iframe width="600" height="400" src="https://codesandbox.io/embed/hlt8w2?view=preview&amp;runonclick=1"  title="fetch-cache-1" allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking" sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts"></iframe>

<p>새로 고침 버튼을 클릭하고 어떤 일이 발생하는지 확인하세요. URL을 끊고 요청이 의도적으로 실패하도록 만들어 보세요.</p>
<p>계속 진행하기 전에 코드 베이스를 자유롭게 탐색하고 무슨 일이 일어나는지를 잘 이해했는지 확인하세요. 이해가 되지 않는 부분이 있으면 이전 섹션을 다시 읽어보세요.</p>
<h2 id="promise로-데이터-추적하기">Promise로 데이터 추적하기</h2>
<p>지금까지 우리는 Promise에 대해 크게 생각하지 않고 일관되게 사용해 왔습니다. Promise는 3가지 상태가 있다는 것을 기억하세요.</p>
<ul>
<li>보류 중</li>
<li>값으로 완료됨(<code>undefined</code> 또는 <code>null</code>일 수도 있습니다.)</li>
<li>특정 이유로 거부됨(일반적으론 <code>Error</code>지만 반드시 그렇지는 않습니다.)</li>
</ul>
<p>원칙적으로 <code>FetchCache</code>처럼 이 정보를 Promise와 별도로 추적할 필요는 없습니다. 특수 함수를 사용해 정보를 읽는 동안 Promise 자체만 추적하면 됩니다.</p>
<p>여기서 문제는 Promise는 비동기 데이터 액세스만 허용한다는 것입니다.(이미 해결이 되었더라도요.)</p>
<pre><code class="language-tsx">// 해결된 Promise 생성
const promise = Promise.resolve();
console.log(&quot;1&quot;);
promise.then(() =&gt; {
  console.log(&quot;3&quot;);
});
console.log(&quot;2&quot;);

// 결과물
1;
2;
3;</code></pre>
<p>데이터를 추출하려면 상태를 동기적으로 읽을 방법이 필요합니다. Promise 객체에 프로퍼티를 추가하여 상태를 추적하면 됩니다.</p>
<pre><code class="language-tsx">function readPromiseState(promise) {
  switch (promise.status) {
    case &quot;pending&quot;:
      return { status: &quot;pending&quot; };
    case &quot;fulfilled&quot;:
      return { status: &quot;fulfilled&quot;, value: promise.value };
    case &quot;rejected&quot;:
      return { status: &quot;rejected&quot;, reason: promise.reason };
    default:
      promise.status = &quot;pending&quot;;
      promise.then((value) =&gt; {
        promise.status = &quot;fulfilled&quot;;
        promise.value = value;
      });
      promise.catch((reason) =&gt; {
        promise.status = &quot;rejected&quot;;
        promise.reason = reason;
      });
      return readPromiseState(promise);
  }
}</code></pre>
<p>Promise로 <code>readPromiseState</code>를 처음 호출하면 보류 중으로 반환됩니다. 하지만 일단 안정화되면 우리가 액세스할 수 있는 방식으로 자체 상태를 업데이트합니다.</p>
<p>원하는 REPL에서 시도해 보세요.</p>
<pre><code class="language-tsx">&gt; const promise1 = Promise.resolve(&quot;Hello World!&quot;)
&gt; readPromiseState(promise1)
{ status: &#39;pending&#39; }
&gt; readPromiseState(promise1)
{ status: &#39;fulfilled&#39;, value: &#39;Hello World!&#39; }

&gt; const promise2 = Promise.reject(new Error(&quot;Whoops!&quot;))
&gt; readPromiseState(promise2)
{ status: &#39;pending&#39; }
&gt; readPromiseState(promise2)
{ status: &#39;rejected&#39;, reason: [Error: Whoops!] }

&gt; const promise3 = new Promise(res =&gt; setTimeout(res, 5000))
&gt; readPromiseState(promise3)
{ status: &#39;pending&#39; }
&gt; readPromiseState(promise3)
{ status: &#39;pending&#39; }
&gt; readPromiseState(promise3)
{ status: &#39;pending&#39; }
&gt; readPromiseState(promise3)
{ status: &#39;fulfilled&#39;, value: undefined }
</code></pre>
<hr>
<blockquote>
<p><strong>스포일러</strong>: 이것이 잘못됐다고 생각할 수도 있지만, 이것이 바로 <a href="https://github.com/facebook/react/blob/1b0132c05acabae5aebd32c2cadddfb16bda70bc/packages/react-server/src/ReactFlightThenable.js#L65C16-L65C23">리액트 v19 <code>use</code></a>의 내부 동작 방식입니다.</p>
</blockquote>
<hr>
<h2 id="캐시에서-promise-사용">캐시에서 Promise 사용</h2>
<p>이 섹션에서는 상태 객체 대신 <code>readPromiseState</code>를 사용해 Promise를 추적하도록 <code>FetchCache</code>를 가볍게 수정해 보겠습니다. 변경 사항은 상당히 간단합니다.</p>
<pre><code class="language-jsx">class FetchCache {
  // 모든 요청을 위한 컨테이너
  requestMap = new Map();

  // 상태 업데이트의 브로드캐스팅을 위한 콜백 추적
  subscribers = new Set();

  fetchUrl(url, refetch) {
    const currentData = this.requestMap.get(url);

    if (currentData) {
      // 이 요청은 이미 진행 중입니다.
      if (readPromiseState(currentData).status === &quot;pending&quot;)
        return readPromiseState(currentData);
      // 데이터가 이미 캐시에 있고 명시적으로 다시 요청되지 않은 경우
      // 명시적으로 재요청되지 않은 경우 반환.
      // 상태가 완료되었거나 거부된 경우.
      if (!refetch) return readPromiseState(currentData);
    }

    // 경쟁 요청을 피하려고 상태를 보류 중으로 설정
    const pendingState = { status: &quot;pending&quot; };
    this.requestMap.set(url, pendingState);

    const broadcastUpdate = () =&gt; {
      // 실행하지 않기 위해 알림 지연
      // 렌더링 주기 중
      // https://reactjs.org/link/setstate-in-render
      setTimeout(() =&gt; {
        for (const callback of this.subscribers) {
          callback();
        }
      }, 0);
    };

    // 요청을 dispatch하고 관찰하기
    const newPromise = fetch(url).then((res) =&gt; res.json());

    newPromise.finally(() =&gt; {
      // 어떤 일이 발생하던 구독자에게 알립니다.
      // 해당 상태를 새로 고쳐야 합니다.
      broadcastUpdate();
    });

    this.requestMap.set(url, newPromise);

    // 요청이 현재 보류 중임을 보고합니다.
    broadcastUpdate();

    // 요청이 현재 보류 중임을 보고합니다.
    return readPromiseState(newPromise);
  }

  subscribe(callback) {
    this.subscribers.add(callback);
    return () =&gt; this.subscribers.delete(callback);
  }
}</code></pre>
<p>이전 코드 샌드박스의 포크에 붙여 넣으면 <strong>다른 변경이 필요하지 않다는 것</strong>을 확인할 수 있습니다. 이것은 우리가 필요한 모든 데이터가 Promise 자체에 있다는 것을 보여줍니다.</p>
<p>이제 한 가지 더 변경하여 <code>FetchCache</code>에서 <code>readPromiseState</code>를 사용하는 대신 <code>useFetch</code> 훅으로 이동하겠습니다.</p>
<pre><code class="language-jsx">class FetchCache {
  // 모든 요청을 위한 컨테이너
  requestMap = new Map();

  // 상태 업데이트의 브로드캐스팅을 위한 콜백 추적
  subscribers = new Set();

  fetchUrl(url, refetch) {
    const currentData = this.requestMap.get(url);

    if (currentData) {
      // 이 요청은 이미 진행 중입니다.
      // `readPromiseState` 확인을 유지해야 합니다.
      if (readPromiseState(currentData.status) === &quot;pending&quot;)
        return currentData;
      // 데이터가 이미 캐시에 있고 명시적으로 다시 요청되지 않은 경우
      // 명시적으로 재요청되지 않은 경우 반환.
      // 상태가 완료되었거나 거부된 경우.
      if (!refetch) return currentData;
    }

    // 경쟁 요청을 피하려고 상태를 보류 중으로 설정
    const pendingState = { status: &quot;pending&quot; };
    this.requestMap.set(url, pendingState);

    const broadcastUpdate = () =&gt; {
      // 실행하지 않기 위해 알림 지연
      // 렌더링 주기 중
      // https://reactjs.org/link/setstate-in-render
      setTimeout(() =&gt; {
        for (const callback of this.subscribers) {
          callback();
        }
      }, 0);
    };

    // 요청을 dispatch하고 관찰하기
    const newPromise = fetch(url).then((res) =&gt; res.json());

    newPromise.finally(() =&gt; {
      // 어떤 일이 발생하던 구독자에게 알립니다.
      // 해당 상태를 새로 고쳐야 합니다.
      broadcastUpdate();
    });

    this.requestMap.set(url, newPromise);

    // 요청이 현재 보류 중임을 보고합니다.
    broadcastUpdate();

    // 요청이 현재 보류 중임을 보고합니다.
    return newPromise;
  }

  subscribe(callback) {
    this.subscribers.add(callback);
    return () =&gt; this.subscribers.delete(callback);
  }
}</code></pre>
<pre><code class="language-jsx">const useFetch = (url) =&gt; {
  const { fetchUrl } = useContext(fetchCacheContext);
  const state = readPromiseState(fetchUrl(url));
  const isPending = state.status === &quot;pending&quot;;
  const error = state.reason;
  const data = state.value;

  // 데이터 새로 고침 허용
  const reload = () =&gt; fetchUrl(url, true);

  return [data, isPending, error, reload];
};</code></pre>
<h2 id="suspense-활성화-🎉">Suspense 활성화 🎉</h2>
<p>마지막으로 변경할 사항은 <code>useFetch</code>를 Suspense 훅으로 바꾸는 것입니다.</p>
<p><code>Error</code>를 던져 에러 바운더리를 트리거하는 방법을 기억하시나요? <strong>Suspense 바운더리를 트리거하는 것은 <code>Promise</code>를 던진다는 점을 제외하면 동일합니다.</strong></p>
<p>에러 바운더리를 벗어나려면 에러 바운더리를 재설정하도록 호출하는 코드가 있어야 합니다. 반면에 Suspense 바운더리는 Promise가 해결되면(또는 거부된 경우 에러 바운더리를 트리거하면) 자동으로 재설정됩니다.</p>
<p>Suspense를 <code>useFetch</code>에 활성화하려면 두 가지 변경이 필요합니다.</p>
<p>첫째, 보류 중인 경우 Promise를 던져야 합니다.</p>
<p>둘째, Promise 거부된 경우 그 이유(<code>에러</code>)를 반환해야 합니다.</p>
<p>Promise 완료되면 데이터를 반환하면 됩니다. 소비하는 컴포넌트는 더 이상 에러나 보류 상태를 고려할 필요가 없습니다.</p>
<pre><code class="language-jsx">const useFetch = (url) =&gt; {
  const { fetchUrl } = useContext(fetchCacheContext);
  const promise = fetchUrl(url);
  const state = readPromiseState(promise);

  // 보류 중인 Promise throw
  const isPending = state.status === &quot;pending&quot;;
  if (isPending) throw promise;

  // 거부된 이유 throw
  const error = state.reason;
  if (error) throw error;

  const data = state.value;

  // 데이터 새로 고침 허용
  const reload = () =&gt; fetchUrl(url, true);

  // 현재 데이터만 반환
  return [data, reload];
};</code></pre>
<p>다음으로, <code>useFetch</code>를 사용하여 데이터를 직접 사용하는 컴포넌트를 업데이트합니다...</p>
<pre><code class="language-jsx">export const User = ({ userId }) =&gt; {
 const [data, reload] = useFetch(
    `https://jsonplaceholder.typicode.com/users/${userId}`
  );
// ...</code></pre>
<p>...그리고 애플리케이션을 Suspense 및 에러 바운더리로 감쌉니다.</p>
<pre><code class="language-jsx">function App() {
  return (
    &lt;div className=&quot;App&quot;&gt;
      &lt;FetchCacheProvider fetchCache={fetchCache}&gt;
        &lt;ErrorBoundary fallback={&lt;ErrorFallback /&gt;}&gt;
          &lt;Suspense fallback={&lt;Spinner /&gt;}&gt;
            &lt;h1&gt;My App&lt;/h1&gt;
            &lt;User userId={1} /&gt;
          &lt;/Suspense&gt;
        &lt;/ErrorBoundary&gt;
      &lt;/FetchCacheProvider&gt;
    &lt;/div&gt;
  );
}</code></pre>
<p>여기서 모범 사례는 항상 애플리케이션 루트를 <code>ErrorBoundary</code>와 <code>Suspense</code> 둘 다 감싸는 것입니다. 둘 중 하나에 포함된 모든 것은 바운더리가 트리거될 때 로컬 상태가 초기화된다는 점을 명심하세요.</p>
<p>코드 샌드박스에서 사용해 보세요.</p>
<iframe width="600" height="400" src="https://codesandbox.io/embed/4xyj36?view=preview&amp;runonclick=1" title="fetch-cache-suspense" allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking" sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts"></iframe>

<hr>
<blockquote>
<p><strong>독자를 위한 연습 문제입니다</strong>: 이번에는 새로 고침 버튼을 클릭하면 전체 앱이 로딩 상태로 바뀝니다. 왜 이런 현상이 발생하며 어떻게 이전 동작으로 복원할 수 있을까요?</p>
<p><strong>힌트</strong>: 에러나 Promise를 던지면 가장 가까운 바운더리가 트리거됩니다.</p>
</blockquote>
<hr>
<h2 id="이런-우린-use를-재발명했습니다">이런. 우린 <code>use()</code>를 재발명했습니다!</h2>
<p>리액트 v19에는 새로운 훅인 <a href="https://react.dev/reference/react/use"><code>use</code></a>가 도입되었습니다. 이 함수는 컨텍스트(<code>useContext</code>와 유사) 또는 <code>Promise</code>에서 데이터를 사용할 수 있습니다. <code>useFetch</code>에 <code>use</code>를 도입하면 어떤 모습인지 살펴보겠습니다.</p>
<pre><code class="language-jsx">const useFetch = (url) =&gt; {
  const { fetchUrl } = useContext(fetchCacheContext);
  const promise = fetchUrl(url);

  //  보류 및 거부된 promise에 대한 throw 처리
  const data = use(promise);

  // 데이터 새로 고침 허용
  const reload = () =&gt; fetchUrl(url, true);

  // 현재 데이터만 반환
  return [data, reload];
};</code></pre>
<p>깔끔하네요!</p>
<h2 id="리뷰">리뷰</h2>
<ul>
<li>Suspense가 짧은 컴포넌트 함수 작성에 도움이 되는 방법</li>
<li>바운더리가 트리거 될 때 상태를 유지하는 방법</li>
<li>Promise에서 &quot;동기적으로&quot; 읽는 방법</li>
<li>&quot;Suspense 지원&quot; 훅을 작성하는 방법</li>
<li><code>use()</code> 훅이 Promise에서 하는 일</li>
</ul>
<h2 id="끝맺음">끝맺음</h2>
<p>글을 다 읽으신 것을 축하드립니다! 이 글을 잘 따라가셨다면 이제 자신만의 Suspense 지원 훅을 만드는 데 필요한 지식을 갖추셨을 것입니다. 더 중요한 것은 이제 문제가 발생했을 때 Suspense를 디버깅하는 데 자신감을 느끼게 되었을 것입니다.</p>
<p>이 글에 소개된 구현 중 일부는 어색해 보일 수 있지만, 모든 것이 실제 Suspense 지원 라이브러리의 작동 방식을 대체로 나타냅니다.</p>
<p>예를 들어 TanStack Query를 보면 매핑이 매우 명확합니다.</p>
<ul>
<li><code>useQuery</code> -&gt; <code>useFetch</code></li>
<li><code>QueryClient</code> -&gt; <code>FetchCache</code></li>
<li><code>QueryClientProvider</code> -&gt; <code>FetchCacheProvider</code></li>
</ul>
<p>실질적인 차이점은 제공되는 기능에 있습니다.</p>
<h3 id="fetchcacheprovider-구현">FetchCacheProvider 구현</h3>
<p><code>FetchCache</code>의 전역 인스턴스를 사용하여 페칭 상태를 유지했지만, Suspense 및 에러 바운더리 밖에 있다면 컨텍스트 내에서 로컬로 초기화된 인스턴스를 사용할 수도 있습니다. 그렇지 않으면 fetch 상태가 재설정됩니다.</p>
<pre><code class="language-jsx">const FetchCacheProvider = ({ children, fetchCache }) =&gt; {
  const [fetchCache] = useState(() =&gt; new FetchCache());
  // 이 상태 훅은 리렌더를 트리거할 때만 사용됩니다.
  const [, setEmptyState] = useState({});
  const rerender = useCallback(() =&gt; setEmptyState({}), []);

  // 구독자를 fetchCache에 등록하는 이펙트
  useEffect(() =&gt; {
    const unsubscribe = fetchCache.subscribe(() =&gt; rerender());
    return unsubscribe;
  }, [fetchCache, rerender]);

  return (
    &lt;fetchCacheContext.Provider
      value={{
        // 팝 퀴즈: 여기서 &#39;bind&#39;가 필요한 이유는 무엇인가요?
        fetchUrl: fetchCache.fetchUrl.bind(fetchCache),
      }}
    &gt;
      {children}
    &lt;/fetchCacheContext.Provider&gt;
  );
};</code></pre>
<p>이러한 Provider의 구현도 잘 작동했을 것입니다. 하지만 이건 좋지 않은 습관입니다.</p>
<p>이런 식으로 <code>FetchCacheProvider</code>를 작성하면 특정 <code>FetchCache</code> 구현과 긴밀하게 결합합니다. 결과적으로 <code>FetchCacheProvider</code>는 다른 패칭 메커니즘(Axios, GraphQL, 단위 테스트용 mocks 등)을 사용하는 대체 구현을 허용할 수 없게 됩니다.</p>
<h3 id="suspense에서-놓친-것은-무엇인가요">Suspense에서 놓친 것은 무엇인가요?</h3>
<p>렌더링 수준 캐싱은 이전에 리액트 팀이 제공하겠다고 암시했던 기능입니다. 이 아이디어는 상태 재설정 후에도 살아남을 수 있는 일부 내부 리액트 컨텍스트 내에서 (Promise와 같은) 데이터를 캐시 할 수 있다는 것입니다.</p>
<p>이렇게 하면 컨텍스트나 전역 캐시가 필요 없는 &quot;로컬&quot; 상태를 기반으로 Suspense를 구현할 수 있습니다. 구현 측면에서는 아직 진행 중이므로 더 이상 언급할 것이 많지 않습니다.</p>
<p>그 점을 제외하면 Suspense API는 꽤 완성도가 높아 보입니다. 문서화하는 것을 싫어하는 리액트 팀을 이해할 수 없습니다.</p>
<h3 id="transitions">Transitions</h3>
<p>이 글에서 자세히 설명하지는 않겠지만, Suspense와 Transitions의 상호작용에 관한 <a href="https://react.dev/reference/react/useTransition">문서는 꽤 훌륭</a>합니다. Suspense를 기반으로 구축하는 경우 해당 페이지를 반드시 읽어보시기를 바랍니다.</p>
<p>특히 곧 출시될 v19의 변경 사항으로 인해 <code>useTransition</code>에는 Suspense보다 훨씬 더 많은 것이 있습니다. 공식 v19 릴리스 후 향후 포스팅에서 이 훅에 대해 자세히 다뤄보도록 하겠습니다.</p>
<p>뉴스레터를 <a href="https://knyz.us16.list-manage.com/subscribe/post?u=7f1e0c3bc2050cf817bfa6368&amp;id=ec86591d5f">구독</a>하고 <code>useTransition</code>에 대한 전체 가이드를 가장 먼저 받아보세요.</p>
]]></description>
        </item>
    </channel>
</rss>