<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Mong dev blog</title>
    <link>https://mong-blog.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Thu, 27 Aug 2026 09:41:43 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>GicoMomg</managingEditor>
    <image>
      <title>Mong dev blog</title>
      <url>https://tistory1.daumcdn.net/tistory/4947324/attach/221b483200514c38aefa41e9f0c6d934</url>
      <link>https://mong-blog.tistory.com</link>
    </image>
    <item>
      <title>[CSS] var(), VAR(), vAr() 모두 같은 함수라고?</title>
      <link>https://mong-blog.tistory.com/entry/CSS-var-VAR-vAr-%EB%AA%A8%EB%91%90-%EA%B0%99%EC%9D%80-%ED%95%A8%EC%88%98%EB%9D%BC%EA%B3%A0</link>
      <description>&lt;h2&gt;들어가며&lt;/h2&gt;
&lt;p&gt;CSS 함수는 일반적으로 소문자로 작성한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.example {
    color: var(--text-color);
    width: calc(100% - 20px);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그렇다면 함수 이름을 대문자로 작성한 다음 코드도 유효할까?&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.example {
    color: VAR(--text-color);
    width: CALC(100% - 20px);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;평소 익숙하게 보던 형태가 아니기에 잘못된 CSS처럼 느껴질 수 있다. 하지만 위 코드는 모두 유효하다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;var(--text-color)
VAR(--text-color)
Var(--text-color)
vAr(--text-color)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;왜 유효할까? 그 이유는 CSS가 정의한 함수 이름은 ASCII 대소문자를 구분하지 않기 때문이다.&lt;br&gt;즉, &lt;code&gt;var()&lt;/code&gt;, &lt;code&gt;VAR()&lt;/code&gt;, &lt;code&gt;Var()&lt;/code&gt;, &lt;code&gt;vAr()&lt;/code&gt;은 표기만 다를 뿐 모두 같은 함수로 해석된다.&lt;/p&gt;
&lt;p&gt;이번 시간에는 CSS 키워드와 함수 이름의 대소문자 규칙을 알아본다.&lt;br&gt;그리고 이 규칙을 린트 도구나 CSS 분석 코드에서 놓쳤을 때 어떤 문제가 발생할 수 있는지도 함께 살펴본다.&lt;/p&gt;
&lt;br/&gt;

&lt;h2&gt;1. CSS 함수 이름은 대소문자를 구분하지 않는다&lt;/h2&gt;
&lt;p&gt;앞서 살펴본 &lt;code&gt;VAR()&lt;/code&gt;와 &lt;code&gt;CALC()&lt;/code&gt;가 유효한 이유는 CSS 사양에서 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.w3.org/TR/css-values-4/#functional-notations&quot;&gt;CSS Values and Units Level 4의 Functional Notations&lt;/a&gt;에서는 함수 이름의 대소문자 규칙을 다음과 같이 정의한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;Like keywords, function names are ASCII case-insensitive.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;즉, CSS 함수 이름은 ASCII 대소문자를 구분하지 않아, 대문자와 소문자를 조합하더라도 같은 함수 이름으로 해석된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;var()  → VAR(), Var(), vAr()
calc() → CALC(), Calc(), cAlC()
rgb()  → RGB(), Rgb(), rGb()&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;실제 CSS에서도 다음과 같이 사용할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;:root {
    --text-color: red;
}

.example {
    color: VAR(--text-color);
    width: CALC(100% - 20px);
    background-color: RGB(255 255 255);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;VAR()&lt;/code&gt;, &lt;code&gt;CALC()&lt;/code&gt;, &lt;code&gt;RGB()&lt;/code&gt;와 같은 표기가 낯설 뿐, 모두 CSS 사양에 맞는 유효한 문법이다.&lt;/p&gt;
&lt;br/&gt;


&lt;h2&gt;2. CSS 키워드도 대소문자를 구분하지 않는다&lt;/h2&gt;
&lt;p&gt;앞서 살펴본 사양에는 함수 이름이 &lt;strong&gt;키워드와 마찬가지로(Like keywords)&lt;/strong&gt; ASCII 대소문자를 구분하지 않는다고 설명되어 있다.&lt;br&gt;그렇다면 CSS 키워드에는 어떤 규칙이 적용될까?&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.w3.org/TR/css-values-4/#keywords&quot;&gt;CSS Values and Units Level 4의 Pre-defined Keywords&lt;/a&gt;에는&lt;br&gt;CSS가 미리 정의한 키워드의 대소문자 규칙을 다음과 같이 설명한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;Keywords are identifiers and are interpreted ASCII case-insensitively.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;즉, CSS가 미리 정의한 키워드도 ASCII 대소문자를 구분하지 않으며, 다음 케이스도 모두 동일하게 처리된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.example-a {
    display: block;
}

.example-b {
    display: BLOCK;
}

.example-c {
    display: Block;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 규칙은 &lt;code&gt;block&lt;/code&gt;뿐만 아니라 CSS가 미리 정의한 다른 키워드에도 동일하게 적용된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.example {
    position: FIXED;
    overflow: HIDDEN;
    font-weight: BOLD;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;일반적으로 CSS 함수와 키워드는 가독성과 코드의 일관성을 위해 소문자로 작성한다.&lt;br&gt;하지만 소문자를 권장되는 코딩 스타일과 대문자가 유효하지 않은 문법이라는 건 구분해야 한다.&lt;/p&gt;
&lt;br/&gt;

&lt;h2&gt;3. &lt;code&gt;layer()&lt;/code&gt;도 같은 규칙을 따른다&lt;/h2&gt;
&lt;p&gt;앞서 살펴본 대소문자 규칙은 &lt;code&gt;@import&lt;/code&gt;에서 사용하는 &lt;code&gt;layer()&lt;/code&gt;에도 적용된다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://drafts.csswg.org/css-cascade-5/#at-import&quot;&gt;CSS Cascade Level 5&lt;/a&gt;에서는 &lt;code&gt;@import&lt;/code&gt;의 레이어 문법을 다음과 같이 정의한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;@import [ &amp;lt;url&amp;gt; | &amp;lt;string&amp;gt; ]
        [ layer | layer(&amp;lt;layer-name&amp;gt;) ]?
        &amp;lt;import-conditions&amp;gt;;&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;사양에는 &lt;code&gt;layer()&lt;/code&gt;가 소문자로 표기되어 있다.&lt;br&gt;하지만 &lt;code&gt;layer()&lt;/code&gt; 역시 CSS가 정의한 함수이므로 대소문자를 구분하지 않는다.&lt;br&gt;따라서 다음 선언은 모두 동일한 함수 이름으로 해석된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;@import &amp;quot;a.css&amp;quot; layer(theme);
@import &amp;quot;b.css&amp;quot; LAYER(theme);
@import &amp;quot;c.css&amp;quot; Layer(theme);
@import &amp;quot;d.css&amp;quot; lAyEr(theme);&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;괄호 없이 사용하는 &lt;code&gt;layer&lt;/code&gt;도 CSS가 미리 정의한 키워드이므로 같은 규칙을 따른다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;@import &amp;quot;a.css&amp;quot; layer;
@import &amp;quot;b.css&amp;quot; LAYER;&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;h2&gt;4. 모든 이름이 대소문자를 구분하지 않는 것은 아니다&lt;/h2&gt;
&lt;p&gt;여기서 주의할 점이 있다.&lt;br&gt;함수 이름이 대소문자를 구분하지 않는다 해서 함수에 전달하는 값도 같은 규칙이 적용되는 건 아니다.&lt;/p&gt;
&lt;p&gt;예를 들어 커스텀 프로퍼티의 대소문자가 다르면 서로 다른 이름으로 구분된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;:root {
    --text-color: red;
    --TEXT-COLOR: blue;
}&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;따라서 다음 두 선언은 서로 다른 커스텀 프로퍼티의 값을 참조한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.example-a {
    color: var(--text-color);
}

.example-b {
    color: VAR(--TEXT-COLOR);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;var&lt;/code&gt;와 &lt;code&gt;VAR&lt;/code&gt;는 같은 함수로 취급되지만,&lt;br&gt;&lt;code&gt;--text-color&lt;/code&gt;와 &lt;code&gt;--TEXT-COLOR&lt;/code&gt;는 다른 프로퍼티로 인식된다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;표현&lt;/th&gt;
&lt;th&gt;종류&lt;/th&gt;
&lt;th&gt;대소문자 구분&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;code&gt;var&lt;/code&gt;, &lt;code&gt;VAR&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;CSS 함수 이름&lt;/td&gt;
&lt;td&gt;구분하지 않음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--text-color&lt;/code&gt;, &lt;code&gt;--TEXT-COLOR&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;커스텀 프로퍼티 이름&lt;/td&gt;
&lt;td&gt;구분함&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;br/&gt;

&lt;p&gt;&lt;code&gt;layer()&lt;/code&gt;도 마찬가지다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;@import &amp;quot;a.css&amp;quot; LAYER(theme);
@import &amp;quot;b.css&amp;quot; layer(THEME);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;LAYER&lt;/code&gt;와 &lt;code&gt;layer&lt;/code&gt;는 같은 함수로 해석되지만,&lt;br&gt;개발자가 정의한 레이어 이름인 &lt;code&gt;theme&lt;/code&gt;과 &lt;code&gt;THEME&lt;/code&gt;은 서로 다른 이름으로 구분된다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;결국, CSS가 미리 정의한 함수 이름과 키워드는 대소문자를 구분하지 않지만,&lt;br&gt;개발자가 정의한 이름은 대소문자를 구분한다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;br/&gt;

&lt;h2&gt;5. 이 규칙을 놓치면 어떤 문제가 생길까?&lt;/h2&gt;
&lt;p&gt;평소 CSS 함수와 키워드를 대부분 소문자로 작성하기에 이 규칙을 직접 체감하기는 어렵다.&lt;br&gt;하지만 CSS 린터나 파서 기반 검사 도구를 구현할 때는 대소문자 규칙이 중요해진다.&lt;/p&gt;
&lt;p&gt;예를 들어 &lt;code&gt;layer()&lt;/code&gt; 함수인지 확인하기 위해 다음과 같이 이름을 비교한다고 가정해 보자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;if (functionNode.name === &amp;quot;layer&amp;quot;) {
    // layer() 처리
}&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;이 코드는 이름이 정확히 소문자인 &lt;code&gt;layer()&lt;/code&gt;만 인식한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;layer(theme) /* 인식 */
LAYER(theme) /* 인식하지 못함 */
Layer(theme) /* 인식하지 못함 */&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;하지만 CSS 사양에서 세 표기는 모두 같은 함수로 해석된다.&lt;br&gt;따라서 검사 도구가 대문자나 혼합형 함수 이름을 인식하지 못하면 유효한 CSS를 검사 대상에서 누락하거나,&lt;br&gt;잘못된 오류를 보고할 수 있다.&lt;/p&gt;
&lt;p&gt;이 문제를 방지하려면 CSS가 정의한 함수 이름을 비교할 때 대소문자를 무시해야 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;if (functionNode.name.toLowerCase() === &amp;quot;layer&amp;quot;) {
    // layer() 처리
}&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;h2&gt;마치며&lt;/h2&gt;
&lt;p&gt;평소 CSS 함수와 키워드를 소문자로 작성하기 때문에 &lt;code&gt;VAR()&lt;/code&gt;, &lt;code&gt;CALC()&lt;/code&gt;, &lt;code&gt;BLOCK&lt;/code&gt; 같은 표현은 잘못된 문법처럼 보일 수 있다.&lt;/p&gt;
&lt;p&gt;하지만 CSS가 미리 정의한 함수 이름과 키워드는 ASCII 대소문자를 구분하지 않는다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;var()   → VAR(), Var(), vAr()와 동일
calc()  → CALC(), Calc(), cAlC()와 동일
layer() → LAYER(), Layer(), lAyEr()와 동일&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;다만 이 규칙이 모든 이름에 적용되는 것은 아니다.&lt;br&gt;함수 이름과 달리 커스텀 프로퍼티나 레이어 이름처럼 개발자가 정의한 이름은 대소문자에 따라 구분될 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;var()과 VAR()                  → 같은 함수 이름
--color와 --COLOR             → 서로 다른 커스텀 프로퍼티 이름
layer()와 LAYER()              → 같은 함수 이름
theme과 THEME                  → 서로 다른 레이어 이름&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;코드를 직접 작성할 때는 사소한 차이처럼 보이지만,&lt;br&gt;CSS를 분석하거나 검사하는 도구에서는 이 규칙을 놓치면 유효한 문법을 제대로 인식하지 못할 수 있다.&lt;/p&gt;
&lt;p&gt;사양에 소문자로 표기되어 있다고 해서 소문자만 유효한 것은 아니다.&lt;br&gt;개별 문법뿐만 아니라 CSS 전반에 적용되는 공통 규칙도 함께 확인해야 한다.&lt;/p&gt;
&lt;br/&gt;</description>
      <category>개발 기술/사소하지만 놓치기 쉬운 개발 지식</category>
      <category>ascii</category>
      <category>CSS</category>
      <category>CSS layer</category>
      <category>CSS var</category>
      <category>CSS 대소문자</category>
      <category>CSS 린트</category>
      <category>CSS 스펙</category>
      <category>CSS 함수</category>
      <category>eslint/css</category>
      <category>stylelint</category>
      <author>GicoMomg</author>
      <guid isPermaLink="true">https://mong-blog.tistory.com/249</guid>
      <comments>https://mong-blog.tistory.com/entry/CSS-var-VAR-vAr-%EB%AA%A8%EB%91%90-%EA%B0%99%EC%9D%80-%ED%95%A8%EC%88%98%EB%9D%BC%EA%B3%A0#entry249comment</comments>
      <pubDate>Mon, 17 Aug 2026 10:33:47 +0900</pubDate>
    </item>
    <item>
      <title>유의미한 성능 개선은 어디에서 시작할까? Hot Path 이해하기</title>
      <link>https://mong-blog.tistory.com/entry/%EC%9C%A0%EC%9D%98%EB%AF%B8%ED%95%9C-%EC%84%B1%EB%8A%A5-%EA%B0%9C%EC%84%A0%EC%9D%80-%EC%96%B4%EB%94%94%EC%97%90%EC%84%9C-%EC%8B%9C%EC%9E%91%ED%95%A0%EA%B9%8C-Hot-Path-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</link>
      <description>&lt;h1&gt;0. 들어가며…&lt;/h1&gt;
&lt;p&gt;성능을 개선하기 위해 코드를 살펴보다 보면, 코드 곳곳의 작은 비효율이 눈에 들어온다.&lt;br&gt;불필요한 반복을 조기에 종료하거나, 중복 계산을 제거하거나, 서로 독립적인 비동기 작업을 병렬로 처리하는 등의 개선 방법을 떠올릴 수 있다.&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;하지만 코드 수준에서 개선할 수 있다고 해서, 유의미한 성능 개선이라고 볼 수 있을까?&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;코드 수준의 최적화도 필요하지만, 코드의 형태만으로 개선 가치를 판단해서는 안 된다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;지난달부터 ESLint의 내부 구조를 살펴보던 중, 이와 비슷한 고민을 한 적이 있다.&lt;br&gt;계기가 된 것은 ESLint 규칙에서 &lt;code&gt;map()&lt;/code&gt;과 &lt;code&gt;filter()&lt;/code&gt;를 연속으로 사용해 두 개의 중간 배열을 생성하는 로직이었다.&lt;/p&gt;
&lt;p&gt;당시에는 해당 코드만 보고, 두 메서드를 하나의 &lt;code&gt;every()&lt;/code&gt;로 합치면 불필요한 중간 배열과 반복 횟수를 줄여 성능을 개선할 수 있다고 생각했다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;/**
 * eslint &amp;gt; multiline-comment-style.js
 * 
 * Q. map()과 filter()가 각각 중간 배열을 생성하는데,
 * every()만 사용해 하나의 순회로 합칠 수는 없을까?
 */
function processSeparateLineComments(commentGroup) {
    const allLinesHaveLeadingSpace = commentGroup
        .map(({ value }) =&amp;gt; value)              // 중간 배열 생성 1
        .filter(line =&amp;gt; line.trim().length &amp;gt; 0) // 중간 배열 생성 2
        .every(line =&amp;gt; line.startsWith(&amp;quot; &amp;quot;));

    return commentGroup.map(({ value }) =&amp;gt;
        allLinesHaveLeadingSpace ? value.replace(/^ /u, &amp;quot;&amp;quot;) : value,
    );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;하지만 메인테이너로부터 해당 변경은 유의미한 성능 개선으로 보기 어렵다는 피드백을 받았다.&lt;br&gt;이 로직은 실행 횟수가 적고 입력 데이터의 크기도 작아, 전체 성능에 미치는 영향이 크지 않았기 때문이다.&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;그렇다면 코드의 형태만 보고 판단하는 데서 벗어나, 실제로 개선할 가치가 있는 지점은 어떻게 찾을 수 있을까?&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;이 질문에 대한 답을 찾아가는 과정에서 알게 된 개념이 바로 &lt;code&gt;Hot path&lt;/code&gt;다. 이번 시간에는 Hot path가 무엇인지 살펴보고,&lt;br&gt;이를 실제 성능 개선 지점을 찾는 데 어떻게 활용할 수 있는지 차근차근 알아보겠다.&lt;/p&gt;
&lt;p&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;h1&gt;1. Hot path: 실행 비용이 누적되는 코드&lt;/h1&gt;
&lt;h2&gt;1-1. hot path란 무엇인가&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Hot path&lt;/code&gt;는 프로그램의 전체 실행 시간이나 처리 비용에서 큰 비중을 차지하는 실행 흐름이다.&lt;br&gt;여기서 &lt;code&gt;path&lt;/code&gt;는 하나의 함수만 의미하지 않는다. 하나의 작업이 완료되기까지 거치는 여러 단계도 포함한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;입력 → 계산 → 상태 변경 → 렌더링&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Hot path가 되는 경우는 크게 두 가지다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;한 번 실행하는 데 오래 걸리는 경우&lt;/li&gt;
&lt;li&gt;한 번의 실행은 빠르지만 자주 반복되어 누적 비용이 커지는 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;p&gt;예를 들어 다음 함수가 한 번 실행될 때 &lt;code&gt;0.1ms&lt;/code&gt;가 걸린다고 가정해 보자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;formatcurrency(item.amount);&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;한 번의 실행 비용은 작지만, 데이터 10,000개 기준으로 반복하면 누적 실행 시간(0.1ms x 10,000번)은 약 1s가 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;items.map(item =&amp;gt; formatCurrency(item.amount));&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;반대로 다음 함수는 데이터가 많아 한 번 실행시 1초가 걸릴 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;calculateComplexChartData(veryLargeDataSet);&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;첫 번째 사례는 짧은 작업이 반복되면서 누적 비용이 커지고, 두 번째 사례는 한 번의 작업만으로 큰 비용이 발생한다.&lt;br&gt;형태는 다르지만, 전체 실행 시간에서 큰 비중을 차지한다면 두 경우 모두 &lt;code&gt;Hot path&lt;/code&gt;가 될 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;전체 비용 ≈ 한 번의 실행 비용 × 실행 횟수&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;즉, Hot path를 찾으려면 개별 작업의 실행 비용과 실행 횟수를 함께 살펴봐야 한다.&lt;br&gt;또한 여기서 말하는 ‘비용’은 실행 시간에만 한정되지 않는다.&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;Hot path는 주로 CPU 실행 시간을 기준으로 찾지만, 상황에 따라 메모리 할당, 디스크 I/O, 네트워크 처리 등 다른 비용을 기준으로 판단할 수도 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;Hot path는 한 번의 실행 비용이 크거나 작은 비용이 반복적으로 누적되어,&lt;br&gt;프로그램의 전체 처리 비용에서 큰 비중을 차지하는 실행 흐름이다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;br/&gt;

&lt;h2&gt;1-2. Hot path는 실행 상황에 따라 달라진다&lt;/h2&gt;
&lt;p&gt;특정 코드가 항상 &lt;code&gt;Hot path&lt;/code&gt;인 것은 아니다.&lt;br&gt;사용자가 수행하는 동작과 측정하려는 비용에 따라 Hot path는 달라질 수 있다.&lt;/p&gt;
&lt;p&gt;따라서 Hot path를 찾기 전에 다음 세 가지 기준을 먼저 정해야 한다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;측정 기준&lt;/strong&gt;: CPU 실행 시간, 메모리 사용량, 네트워크 요청 등 어떤 비용을 측정할 것인가?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;사용자 동작&lt;/strong&gt;: 검색어 입력, 목록 스크롤, 페이지 이동 등 어떤 동작을 측정할 것인가?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;측정 구간&lt;/strong&gt;: 한 번의 요청, 일정 시간 동안의 상호작용, 애플리케이션의 전체 실행 과정 중 어디까지를 측정 범위로 삼을 것인가?&lt;/li&gt;
&lt;/ol&gt;
&lt;br/&gt;

&lt;p&gt;예를 들어 사용자가 검색어를 입력할 때마다 자동 완성 로직이 실행된다고 가정해 보자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;function onSearchInput(keyword: string) {
    const matches = products
        .filter(product =&amp;gt; product.name.includes(keyword))
        .sort(compareByRelevance);

    renderInputText(matches);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;사용자가 검색어를 연속으로 입력하면 &lt;code&gt;filter&lt;/code&gt;, &lt;code&gt;sort&lt;/code&gt;, &lt;code&gt;renderInputText&lt;/code&gt;가 반복해서 실행된다.&lt;br&gt;따라서 이 흐름이 Hot path일 가능성은 있지만, 반복해서 실행된다는 사실만으로 단정할 수는 없다.&lt;br&gt;전체 처리 비용에서 실제로 얼마나 큰 비중을 차지하는지 확인해야 하기 때문이다.&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;이를 판단하려면 먼저 측정 조건을 구체적으로 정해야 한다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구분&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;내용&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;측정 기준&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;메인 스레드의 CPU 실행 시간&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;사용자 동작&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;상품 10,000개를 불러온 상태에서 검색어를 연속으로 입력&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;측정 구간&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;검색어 입력부터 &lt;code&gt;filter → sort → renderInputText&lt;/code&gt; 실행이 완료될 때까지&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;br/&gt;

&lt;p&gt;이처럼 측정 조건을 구체적으로 정하면 다음 내용을 명확하게 확인할 수 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;어떤 사용자 동작을 측정했는지&lt;/li&gt;
&lt;li&gt;어떤 코드가 반복해서 실행되었는지&lt;/li&gt;
&lt;li&gt;어떤 비용을 측정 기준으로 삼았는지&lt;/li&gt;
&lt;li&gt;해당 코드가 전체 비용에서 얼마나 큰 비중을 차지하는지&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;즉, 코드가 자주 실행된다는 이유만으로 Hot path라고 판단해서는 안 된다.&lt;br&gt;측정할 상황과 기준을 먼저 정한 뒤, 해당 코드가 전체 처리 비용에서 차지하는 비중을 확인해야 한다.&lt;/p&gt;
&lt;br/&gt;

&lt;h2&gt;1-3. Hot path를 판단하는 방법&lt;/h2&gt;
&lt;h3&gt;(1) 사용자 관점의 목표를 정한다&lt;/h3&gt;
&lt;p&gt;먼저 어떤 사용자 경험을 개선할 것인지 구체적으로 정해야 한다.&lt;br&gt;단순히 ‘화면을 빠르게 만든다’는 목표만으로는 어떤 동작과 실행 흐름을 측정해야 하는지 판단하기 어렵다.&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;따라서 측정할 사용자 동작과 구간, 실행 환경, 목표 시간을 함께 정해야 한다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구분&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;내용&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;실행 환경&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;전체 사용자의 40% 이상이 이용하는 모바일 환경&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;사용자 동작&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;상품 목록에서 필터 버튼 클릭&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;측정 구간&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;필터 버튼을 누른 시점부터 변경된 목록이 화면에 표시될 때까지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;목표 시간&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;300ms 이하&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;이처럼 사용자 관점의 목표를 구체적으로 정하면, 어떤 환경에서 어떤 동작과 실행 흐름을 측정해야 하는지 명확해진다.&lt;/p&gt;
&lt;br/&gt;

&lt;h3&gt;(2) 실제 사용자와 비슷한 조건에서 측정한다&lt;/h3&gt;
&lt;p&gt;개발자의 고성능 기기에서만 측정하면 실제 사용자가 겪는 병목을 놓칠 수 있다.&lt;br&gt;따라서 실제 사용 환경을 대표할 수 있는 조건에서 사용자 동작을 재현해야 한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;사용자가 주로 이용하는 기기와 브라우저&lt;/li&gt;
&lt;li&gt;실제와 비슷한 네트워크 속도&lt;/li&gt;
&lt;li&gt;현실적인 데이터 크기&lt;/li&gt;
&lt;li&gt;최초 진입과 재방문&lt;/li&gt;
&lt;li&gt;캐시가 있는 경우와 없는 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;로컬에서 사용자 환경을 재현할 때는 다음과 같은 도구를 활용할 수 있다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;도구&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;주요 용도&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Chrome DevTools&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;CPU·네트워크 속도를 제한한 수동 측정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Puppeteer&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Chrome 기반 사용자 동작의 반복 실행과 Trace 수집&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Playwright&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;여러 브라우저와 모바일 환경에서 반복 테스트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lighthouse&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;페이지 초기 로딩과 특정 사용자 흐름의 성능 분석&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Chrome DevTools에서는 CPU와 네트워크 속도를 제한해 중·저사양 모바일 환경을 간단히 재현할 수 있다.&lt;br&gt;특히 &lt;a href=&quot;https://developer.chrome.com/docs/devtools/settings/throttling&quot;&gt;CPU throttling 보정 기능&lt;/a&gt;을 사용하면 현재 개발 기기를 기준으로 중간 등급과 저사양 모바일 환경을 추정할 수 있다.&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;동일한 동작을 여러 번 반복해서 측정하려면 Puppeteer를 활용할 수 있다.&lt;br&gt;Puppeteer는 브라우저를 자동으로 제어할 수 있으며, &lt;a href=&quot;https://pptr.dev/api/puppeteer.page&quot;&gt;Tracing API&lt;/a&gt;를 이용해 Chrome DevTools에서 분석할 수 있는 Trace 파일도 생성할 수 있다.&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;여러 브라우저에서 같은 동작을 확인해야 한다면 Playwright를 사용할 수 있다.&lt;br&gt;Playwright는 Chromium, Firefox, WebKit을 지원하며, 화면 크기·User Agent·터치 여부 등을 포함한 &lt;a href=&quot;https://playwright.dev/docs/emulation&quot;&gt;모바일 기기 환경을 에뮬레이션&lt;/a&gt;할 수 있다.&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;다만 에뮬레이션은 브라우저와 화면 조건을 유사하게 재현할 뿐, 실제 기기의 CPU·GPU 성능과 발열, 메모리 제약까지 완전히 재현하지는 못한다.&lt;br&gt;따라서 서비스에서 중요하게 다루는 사용자 흐름은 실제 점유율이 높은 기기에서도 확인해야 한다.&lt;/p&gt;
&lt;br/&gt;

&lt;h3&gt;(3) 프로파일링에서 실행 경로와 비용을 확인한다&lt;/h3&gt;
&lt;p&gt;Chrome DevTools의 Performance 패널을 사용하면 메인 스레드에서 오래 걸린 작업과 함수의 호출 관계를 확인할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-md&quot;&gt;&amp;lt;측정하기&amp;gt;

Performance 패널 열기
→ CPU·네트워크 조건 설정
→ Record 시작
→ 측정할 사용자 동작 재현
→ Record 종료

&amp;lt;결과 확인하기&amp;gt;

Main 트랙에서 오래 걸린 작업 선택
→ Total time과 Self time 확인
→ 호출 스택을 따라 실행 경로 확인&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Total time&lt;/code&gt;: 하위 함수의 실행 시간을 포함한 전체 실행 시간&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Self time&lt;/code&gt;: 하위 함수의 실행 시간을 제외하고, 해당 함수 자체에서 사용한 시간&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;여기서 주의할 점은 특정 함수의 실행 시간이 길다는 이유만으로 최적화 대상으로 판단해서는 안 된다.&lt;br&gt;해당 함수가 어떤 사용자 동작에서 호출되었는지, 그리고 측정 구간의 전체 비용에서 얼마나 큰 비중을 차지하는지 함께 확인해야 한다.&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;한 번의 측정 결과만으로 판단하면 안된다.&lt;br&gt;그 이유는 백그라운드 프로세스, 네트워크 상태, 캐시, JIT 컴파일 등의 영향으로 측정 결과가 달라질 수 있기 때문이다.&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;따라서 다음 조건을 동일하게 유지한 상태에서 같은 동작을 여러 번 측정해야 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-md&quot;&gt;동일한 기기 또는 CPU 제한 조건
동일한 브라우저와 버전
동일한 네트워크 조건
동일한 데이터 크기
동일한 캐시 상태
동일한 사용자 동작&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;여러 번 측정한 결과는 한 번의 최솟값이나 최댓값이 아니라 중앙값을 중심으로 비교하는 것이 좋다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-md&quot;&gt;1회: 280ms
2회: 310ms
3회: 295ms
4회: 470ms
5회: 300ms

중앙값: 300ms&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;h3&gt;(4) 중요한 구간을 나누어 측정한다&lt;/h3&gt;
&lt;p&gt;User Timing API를 사용하면 특정 작업의 시작과 종료 시점을 기록하고, 각 구간에 걸린 시간을 비교할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;async function loadTransferHistory() {
    performance.mark(&amp;quot;history:start&amp;quot;);

    const response = await fetch(&amp;quot;/api/transfers&amp;quot;);
    const rawData = await response.json();

    performance.mark(&amp;quot;history:load-end&amp;quot;);

    const transfers = normalizeTransfers(rawData);

    performance.mark(&amp;quot;history:normalize-end&amp;quot;);

    renderTransferHistory(transfers);

    performance.mark(&amp;quot;history:render-end&amp;quot;);

    performance.measure(
        &amp;quot;history:load&amp;quot;,
        &amp;quot;history:start&amp;quot;,
        &amp;quot;history:load-end&amp;quot;,
    );

    performance.measure(
        &amp;quot;history:normalize&amp;quot;,
        &amp;quot;history:load-end&amp;quot;,
        &amp;quot;history:normalize-end&amp;quot;,
    );

    performance.measure(
        &amp;quot;history:render&amp;quot;,
        &amp;quot;history:normalize-end&amp;quot;,
        &amp;quot;history:render-end&amp;quot;,
    );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위 코드는 전체 작업을 세 구간으로 나누어 측정한다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;측정 구간&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;포함된 작업&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;code&gt;history:load&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;API 요청부터 응답 본문의 JSON 변환까지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;history:normalize&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;응답 데이터를 화면에서 사용할 형태로 변환&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;history:render&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;renderTransferHistory()&lt;/code&gt; 함수 실행&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;측정 결과를 비교하면 &lt;code&gt;load&lt;/code&gt;, &lt;code&gt;normalize&lt;/code&gt;, &lt;code&gt;render&lt;/code&gt; 중 어느 구간이 전체 처리 시간에서 가장 큰 비중을 차지하는지 확인할 수 있다.&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;다만 위 코드의 &lt;code&gt;history:render&lt;/code&gt;는 &lt;code&gt;renderTransferHistory()&lt;/code&gt; 함수를 호출하는 데 걸린 시간만 측정한다.&lt;br&gt;React나 Vue에서 상태를 변경하는 함수라면, 프레임워크가 DOM을 업데이트하고 브라우저가 실제 화면을 그리는 시간까지 포함하지 않을 수 있다.&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;만약 목표가 ‘변경된 목록이 사용자 화면에 표시될 때까지’라면 프레임워크의 DOM 반영 완료 시점과&lt;br&gt;브라우저가 화면을 그리는 시점을 고려해 측정 종료 지점을 별도로 설정해야 한다.&lt;/p&gt;
&lt;p&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;h1&gt;2. Amdahl의 법칙으로 최적화 우선순위 정하기&lt;/h1&gt;
&lt;p&gt;Hot path를 찾았다고 해서 해당 실행 흐름 전체를 무조건 수정해야 하는 건 아니다.&lt;br&gt;측정 결과를 바탕으로, 실제 성능 개선 효과가 큰 부분을 선택해야 한다.&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;최적화 대상을 정할 때는 다음 항목을 확인한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;목표로 정한 성능 지표에 영향을 주는가?&lt;/li&gt;
&lt;li&gt;전체 실행 시간에서 차지하는 비중이 큰가?&lt;/li&gt;
&lt;li&gt;비용을 줄일 현실적인 방법이 있는가?&lt;/li&gt;
&lt;li&gt;변경으로 코드 복잡도나 메모리 사용량이 지나치게 늘어나지 않는가?&lt;/li&gt;
&lt;li&gt;기존 기능의 정확성을 유지할 수 있는가?&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;p&gt;특정 부분의 개선이 전체 성능에 얼마나 영향을 주는지는 &lt;strong&gt;Amdahl의 법칙&lt;/strong&gt;으로 설명할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;60%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcHl1i/dJMcahrY0jC/EPVTSNf1H91ikERSuLzs30/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcHl1i/dJMcahrY0jC/EPVTSNf1H91ikERSuLzs30/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcHl1i/dJMcahrY0jC/EPVTSNf1H91ikERSuLzs30/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbcHl1i%2FdJMcahrY0jC%2FEPVTSNf1H91ikERSuLzs30%2Fimg.png&quot; width=&quot;60%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;P: 개선 대상이 기존 전체 실행 시간에서 차지하는 비율&lt;/li&gt;
&lt;li&gt;S: 개선 대상 자체가 빨라진 배수&lt;/li&gt;
&lt;li&gt;S_{total}: 전체 프로그램이 빨라진 배수&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;공식보다 중요한 핵심은&lt;br&gt;“전체 실행 시간에서 차지하는 비중이 작다면,&lt;br&gt;해당 부분을 크게 개선해도 전체 성능은 조금만 좋아진다”는 것이다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;br/&gt;

&lt;p&gt;예를 들어 전체 실행 시간이 10초이고, 개선 대상이 그 중 2초를 차지한다고 가정해 보자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-md&quot;&gt;개선 전

개선 대상: 2초
나머지 작업: 8초
전체: 10초&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;개선 대상을 10배 빠르게 만들면 해당 작업의 실행 시간은 2초에서 0.2초로 줄어든다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-md&quot;&gt;개선 후

개선 대상: 0.2초
나머지 작업: 8초
전체: 8.2초&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;개선 대상 자체는 10배 빨라졌지만, 전체 실행 시간은 10초에서 8.2초로 줄어든다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-md&quot;&gt;전체 성능 향상

10초 ÷ 8.2초 ≈ 1.22배&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;즉, 일부 코드를 10배 빠르게 만들더라도 전체 프로그램은 약 1.22배만 빨라진다.&lt;br&gt;개선하지 않은 나머지 8초가 그대로 남아 있기 때문이다.&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;따라서 최적화 우선순위를 정할 때는 다음 두 가지를 함께 살펴봐야 한다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;판단 기준&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;확인할 내용&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;부분 개선 폭&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;해당 코드를 현실적으로 몇 배 빠르게 만들 수 있는가?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;전체 시간 비중&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;해당 코드가 전체 실행 시간에서 얼마나 큰 비중을 차지하는가?&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;단순히 크게 개선할 수 있는 코드보다, 전체 실행 시간에서 큰 비중을 차지하면서 현실적으로 개선할 수 있는 코드를 먼저 선택해야 한다.&lt;/p&gt;
&lt;p&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;h1&gt;3. Hot path를 판단할 때 자주 생기는 오해&lt;/h1&gt;
&lt;p&gt;Hot path를 찾고 최적화 우선순위를 정하는 과정에서는 코드의 호출 횟수나 개별 함수의 실행 시간처럼 눈에 잘 띄는 수치만으로 판단하기 쉽다.&lt;br&gt;하지만 하나의 수치만으로 Hot path와 최적화 대상을 결정하면 실제 사용자 경험과 관계없는 부분을 개선할 수 있다.&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;앞에서 살펴본 판단 기준을 바탕으로, Hot path를 찾고 최적화할 때 자주 생기는 오해를 정리해 보자.&lt;/p&gt;
&lt;h3&gt;오해 1. 자주 호출되는 코드는 모두 Hot path다?&lt;/h3&gt;
&lt;p&gt;호출 횟수만으로는 Hot path인지 판단할 수 없다.&lt;br&gt;자주 실행되더라도 한 번의 실행 비용이 매우 작다면 전체 성능에 미치는 영향도 작을 수 있다.&lt;/p&gt;
&lt;p&gt;중요한 것은 호출 횟수 자체가 아니라, 반복 실행으로 발생한 누적 비용이 전체 비용에서 차지하는 비중이다.&lt;/p&gt;
&lt;h3&gt;오해 2. 가장 오래 걸린 함수가 Hot path다?&lt;/h3&gt;
&lt;p&gt;Hot path는 하나의 함수가 아니라, 여러 함수가 이어지는 실행 흐름일 수 있다.&lt;br&gt;한 번 실행하는 데 오래 걸리는 함수보다 짧은 함수들이 반복되는 흐름의 누적 비용이 더 클 수도 있다.&lt;/p&gt;
&lt;p&gt;따라서 개별 함수의 실행 시간뿐만 아니라, 해당 함수가 어떤 흐름에서 얼마나 자주 호출되는지도 함께 확인해야 한다.&lt;/p&gt;
&lt;h3&gt;오해 3. CPU 사용량이 가장 큰 경로부터 개선해야 한다?&lt;/h3&gt;
&lt;p&gt;사용자가 기다리는 원인이 네트워크 요청이나 디스크 I/O와 같은 대기 작업이라면 CPU 사용량을 줄여도 목표로 정한 성능 지표는 크게 개선되지 않을 수 있다.&lt;/p&gt;
&lt;p&gt;먼저 어떤 사용자 경험과 비용을 개선하려는지 정한 뒤, 그 목표에 영향을 주는 실행 흐름을 찾아야 한다.&lt;/p&gt;
&lt;h3&gt;오해 4. Hot path는 무조건 최적화해야 한다?&lt;/h3&gt;
&lt;p&gt;Hot path라고 해서 반드시 수정해야 하는 것은 아니다. 전체 비용에서 차지하는 비중뿐만 아니라 개선 가능성, 구현 복잡도, 메모리 사용량, 변경 위험도 등을 함께 고려해야 한다.&lt;/p&gt;
&lt;p&gt;최적화로 얻는 효과보다 구현과 유지보수에 드는 비용이 크다면 현재 상태를 유지하는 편이 나을 수 있다.&lt;/p&gt;
&lt;h3&gt;오해 5. 일부 코드가 10배 빨라지면 전체 성능도 10배 향상된다?&lt;/h3&gt;
&lt;p&gt;전체 성능의 향상 폭은 해당 코드가 기존 실행 시간에서 차지했던 비중에 따라 달라진다. 일부 코드가 10배 빨라지더라도 나머지 작업의 실행 시간이 그대로라면 전체 성능은 그보다 적게 향상된다.&lt;/p&gt;
&lt;p&gt;또한 개발 환경에서 확인한 개선이 실제 사용자 환경에서도 동일하게 나타나는지 같은 조건에서 다시 측정해야 한다.&lt;/p&gt;
&lt;p&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;h1&gt;4. 마치며…&lt;/h1&gt;
&lt;p&gt;처음에는 중간 배열을 줄이거나 더 빠른 문법으로 바꾸는 것만으로 성능을 개선할 수 있다고 생각했다.&lt;br&gt;하지만 코드 수준에서 최적화할 수 있다는 사실만으로, 해당 코드가 실제 성능 문제라고 판단할 수는 없었다.&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;중요한 것은 먼저 개선하려는 사용자 경험과 측정 조건을 구체적으로 정하는 것이다.&lt;br&gt;그다음 프로파일링을 통해 전체 비용에서 큰 비중을 차지하는 실행 흐름을 찾고, 개선 가능성과 구현 비용,&lt;br&gt;변경에 따른 위험을 함께 고려해 최적화 우선순위를 정해야 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-md&quot;&gt;사용자 관점의 목표 설정
→ 측정 조건 정의
→ 프로파일링
→ Hot path 확인
→ 개선 대상 선정
→ 동일한 조건에서 재측정&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;결국 성능 최적화는 단순히 느려 보이는 코드를 더 빠른 코드로 바꾸는 작업이 아니다.&lt;br&gt;측정 결과를 바탕으로 사용자 경험에 가장 큰 영향을 주는 지점을 찾고, 한정된 개발 비용을 가장 효과적인 곳에 투자하는 과정이다.&lt;/p&gt;
&lt;br/&gt;</description>
      <category>개발 기술/사소하지만 놓치기 쉬운 개발 지식</category>
      <category>Amdahl의 법칙</category>
      <category>Chrome DevTools Performance</category>
      <category>Hot Path</category>
      <category>hotpath</category>
      <category>성능 최적화</category>
      <category>웹 성능 측정</category>
      <category>자바스크립트 성능</category>
      <category>최적화</category>
      <category>프로파일링</category>
      <category>프론트엔드 성능 최적화</category>
      <author>GicoMomg</author>
      <guid isPermaLink="true">https://mong-blog.tistory.com/248</guid>
      <comments>https://mong-blog.tistory.com/entry/%EC%9C%A0%EC%9D%98%EB%AF%B8%ED%95%9C-%EC%84%B1%EB%8A%A5-%EA%B0%9C%EC%84%A0%EC%9D%80-%EC%96%B4%EB%94%94%EC%97%90%EC%84%9C-%EC%8B%9C%EC%9E%91%ED%95%A0%EA%B9%8C-Hot-Path-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0#entry248comment</comments>
      <pubDate>Tue, 11 Aug 2026 23:14:48 +0900</pubDate>
    </item>
    <item>
      <title>내 서비스의 호환성을 지키는 방법? Browserslist, Babel, Polyfill, target 도구 사용하기</title>
      <link>https://mong-blog.tistory.com/entry/%EB%82%B4-%EC%84%9C%EB%B9%84%EC%8A%A4%EC%9D%98-%ED%98%B8%ED%99%98%EC%84%B1%EC%9D%84-%EC%A7%80%ED%82%A4%EB%8A%94-%EB%B0%A9%EB%B2%95-Browserslist-Babel-Polyfill-target-%EB%8F%84%EA%B5%AC-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</link>
      <description>&lt;h1&gt;1. 브라우저 호환성이 중요한 이유&lt;/h1&gt;
&lt;p&gt;프론트엔드 코드는 사용자의 브라우저에서 실행된다.&lt;br&gt;그리고 모든 사용자의 브라우저 종류와 버전, 업데이트 상태는 각기 다르다.&lt;br&gt;그렇다보니 개발자의 의도와 다르게,사용자 브라우저 환경에 따라 서비스가 작동하지 않을 수 있다.&lt;/p&gt;
&lt;p&gt;예를 들어 다음 코드를 살펴보자:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const lastUserName = users.at(-1)?.name ?? &amp;quot;Unknown&amp;quot;;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;최신 브라우저에서는 자연스러운 코드지만, 지원 범위를 넓혀 보면 두 가지 문제가 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;?.&lt;/code&gt;와 &lt;code&gt;??&lt;/code&gt;는 브라우저가 문법 자체를 이해하지 못할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Array.prototype.at()&lt;/code&gt;은 문법적으로 해석되더라도 브라우저에 해당 메서드가 없을 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;전자는 코드가 실행되기 전에 문제를 일으키고, 후자는 실행 중에 오류를 일으킨다.&lt;br&gt;어느 쪽이든 서비스 진입이 어렵거나 특정 기능을 사용할 수 없게 된다.&lt;br&gt;즉 브라우저 호환성은 &lt;strong&gt;사용자가 서비스를 안정적으로 이용할 수 있는지에 관한 문제&lt;/strong&gt;다.&lt;/p&gt;
&lt;p&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;h2&gt;1.1. 브라우저마다 지원 범위가 다른 이유&lt;/h2&gt;
&lt;p&gt;브라우저별 JavaScript 지원 범위가 다른 이유 중 하나는&lt;br&gt;브라우저가 사용하는 JavaScript 엔진과 엔진 버전이 다르기 때문이다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align=&quot;left&quot;&gt;브라우저&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;JavaScript 엔진 &amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;Chrome, Chromium 기반 Edge&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;V8 &amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;Safari&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;JavaScriptCore&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;Firefox&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;SpiderMonkey&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;새로운 JavaScript 기능이 표준으로 채택되더라도 모든 엔진이 동일 시점에 지원하는 건 아니다.&lt;br&gt;DOM이나 Web API의 지원 범위는 JavaScript 엔진뿐 아니라 브라우저와 운영체제의 구현 차이에도 영향을 받는다.&lt;br&gt;특히 오래된 단말기나 WebView는 브라우저 업데이트가 늦거나 중단될 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;h2&gt;1.2. 호환성 오류가 발생하는 두 시점&lt;/h2&gt;
&lt;p&gt;호환성 문제는 같은 시점에 발생하는 게 아니다.&lt;br&gt;브라우저는 JavaScript를 먼저 &lt;strong&gt;파싱&lt;/strong&gt;한 뒤 &lt;strong&gt;실행&lt;/strong&gt;하므로, 호환성 오류도 두 시점으로 나뉜다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;JavaScript 코드 로드
    ↓
문법 파싱
    ↓
코드 실행&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;


&lt;h3&gt;(1) 파싱 단계에서의 오류&lt;/h3&gt;
&lt;p&gt;파싱은 브라우저가 코드의 문법 구조를 분석하는 과정이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;user?.name;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Optional Chaining을 지원하지 않는 브라우저는 &lt;code&gt;?.&lt;/code&gt; &lt;strong&gt;문법 자체를 이해하지 못&lt;/strong&gt;한다.&lt;br&gt;그래서 실행 단계에 도달하기도 전에 SyntaxError 문법 오류가 발생한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;SyntaxError: Unexpected token .&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;h3&gt;(2) 실행 단계에서의 오류&lt;/h3&gt;
&lt;p&gt;앞선 예시와 달리 다음 코드는 메서드 호출 형태라, 브라우저가 문법적으로 해석할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;items.at(-1);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;하지만 파서(parser)는 &lt;code&gt;at&lt;/code&gt; 메서드의 실제 존재 여부까지 확인하지 않는다.&lt;br&gt;따라서 실행 시 브라우저에 &lt;code&gt;Array.prototype.at&lt;/code&gt;이 없다면 &lt;code&gt;items.at&lt;/code&gt;은 &lt;code&gt;undefined&lt;/code&gt;로 평가되고,&lt;br&gt;이를 함수처럼 호출하는 순간 오류가 발생한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;TypeError: items.at is not a function&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;두 오류를 정리하면 다음과 같다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;코드&lt;/th&gt;
&lt;th&gt;문제&lt;/th&gt;
&lt;th&gt;실패 시점&lt;/th&gt;
&lt;th&gt;필요한 대응&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;code&gt;user?.name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;브라우저가 문법을 이해하지 못함&lt;/td&gt;
&lt;td&gt;파싱 단계&lt;/td&gt;
&lt;td&gt;지원하는 문법으로 변환&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;items.at(-1)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;브라우저에 메서드가 없음&lt;/td&gt;
&lt;td&gt;실행 단계&lt;/td&gt;
&lt;td&gt;Polyfill 추가 또는 다른 구현으로 대체&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;이 구분이 중요한 이유는 &lt;strong&gt;실패 시점에 따라 해결 방법이 달라지기 때문&lt;/strong&gt;이다. &lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;브라우저가 문법을 이해하지 못하면 -&amp;gt; 코드 형태를 미리 변환해야 하고,&lt;/li&gt;
&lt;li&gt;문법은 이해하지만 API가 없다면 -&amp;gt; 그 기능을 추가하거나 다른 방식으로 대체해야 한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;h2&gt;1.3. 브라우저 호환성의 세 가지 유형&lt;/h2&gt;
&lt;p&gt;파싱/실행 단계를 기준으로 JavaScript 호환성은 &lt;strong&gt;문법 호환성&lt;/strong&gt;과 &lt;strong&gt;API 호환성&lt;/strong&gt;으로 구분할 수 있다.&lt;br&gt;추가로  bundle과 chunk를 불러오는 방식도 고려해야 하기에, 브라우저 호환성은 세 가지로 나눌 수 있다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;문법 호환성&lt;/li&gt;
&lt;li&gt;API 호환성&lt;/li&gt;
&lt;li&gt;모듈 및 로딩 호환성&lt;/li&gt;
&lt;/ol&gt;
&lt;br/&gt;

&lt;h3&gt;(1) 문법 호환성&lt;/h3&gt;
&lt;p&gt;문법 호환성은 브라우저가 JavaScript 코드의 &lt;strong&gt;표현 방식을 이해할 수 있는지&lt;/strong&gt;에 관한 문제다.&lt;br&gt;화살표 함수, Optional Chaining, Nullish Coalescing 등이 여기에 해당한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const name = user?.name;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;지원 브라우저가 이 문법을 이해하지 못한다면 Babel과 같은 도구로 이해가능한 문법으로 변환해야 한다.&lt;br&gt;변환 결과는 다음과 같다. (&lt;em&gt;실제 산출물은 Babel 버전과 설정에 따라 달라질 수 있음&lt;/em&gt;)&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;var name = user === null || user === undefined
    ? undefined
    : user.name;&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;h3&gt;(2) API 호환성&lt;/h3&gt;
&lt;p&gt;API 호환성은 브라우저가 코드에서 호출하는 &lt;strong&gt;함수, 객체, 메서드를 실제로 제공하는지&lt;/strong&gt;에 관한 문제다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;items.at(-1);
Promise.resolve();
Object.entries(data);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 코드들은 문법적으로 해석되더라도, 해당 API를 지원하지 않는 브라우저에서는 실행 중 오류가 발생한다.&lt;br&gt;이 경우 Polyfill을 추가해 누락된 기능을 보완할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// 동작 원리를 단순화한 의사 코드이며,
// 실제 ECMAScript 명세를 완전히 구현한 Polyfill은 아님

if (!Array.prototype.at) {
  Array.prototype.at = function (index) {
    var target = index &amp;lt; 0 ? this.length + index : index;
    return this[target];
  };
}&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align=&quot;left&quot;&gt;구분&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;문법 변환&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;Polyfill&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;해결 대상&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;브라우저가 읽지 못하는 문법&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;브라우저에 없는 API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;예&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;?.&lt;/code&gt;, &lt;code&gt;??&lt;/code&gt;, 화살표 함수&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;Promise&lt;/code&gt;, &lt;code&gt;Object.entries&lt;/code&gt;, &lt;code&gt;Array.prototype.at&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;처리 방식&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;코드의 표현 형태를 변경&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;필요한 함수·객체·메서드를 추가&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;br/&gt;

&lt;h3&gt;(3) 모듈 및 로딩 호환성&lt;/h3&gt;
&lt;p&gt;애플리케이션은 일반적으로 여러 모듈로 구성된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import { getUsers } from &amp;quot;./api.js&amp;quot;;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그래서 번들러는 모듈의 의존 관계를 분석해 번들과 청크를 만들고, 이를 불러오기 위한 런타임 코드도 생성한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;모듈 관계 분석
    ↓
의존성 그래프 생성
    ↓
번들과 청크 생성
    ↓
런타임에서 필요한 파일 로드&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;따라서 애플리케이션 코드의 문법을 변환했다고 호환성이 보장되는 것은 아니다.&lt;br&gt;번들러가 생성한 런타임 코드의 문법과 동적 청크 로딩 방식도 모두 지원 브라우저에서 동작해야 한다.&lt;br&gt;결국 하나만 해결해서는 서비스 전체의 호환성을 보장할 수 없다.&lt;/p&gt;
&lt;p&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;h2&gt;1.4. 그럼 어떻게 서비스의 호환성을 지킬 수 있을까?&lt;/h2&gt;
&lt;p&gt;앞서 브라우저 호환성을 문법, API, 모듈 및 로딩의 세 가지 유형으로 나누어 살펴봤다.&lt;br&gt;각 문제는 파싱, 실행, 파일 로딩처럼 서로 다른 단계에서 발생하므로,&lt;br&gt;한 가지 문제를 해결했다고 최종 산출물의 호환성이 보장되는 것은 아니다.&lt;/p&gt;
&lt;p&gt;빌드 성공도 마찬가지다.&lt;br&gt;빌드가 성공했다는 것은 &lt;strong&gt;빌드 도구가 원본 코드로 산출물을 생성했다는 의미&lt;/strong&gt;일 뿐,&lt;br&gt;모든 지원 브라우저가 해당 산출물을 파싱하고 실행할 수 있다는 의미는 아니다. &lt;/p&gt;
&lt;p&gt;따라서 브라우저 호환성 관리의 목표는 단순히 빌드를 통과하는 코드를 만드는 것이 아니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;개발자는 최신 JavaScript 문법과 API를 사용하되,&lt;br&gt;최종 빌드 산출물은 서비스가 지원하는 브라우저에서 정상적으로 실행될 수 있어야 한다!&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;이 목표를 달성하려면 먼저 지원 범위를 명확하게 정의하고,&lt;br&gt;각 도구별 역할을 인식해야한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Browserslist → 지원할 브라우저 범위 정의

ESLint → 원본 코드 검사

Babel → 지원하지 않는 JavaScript 문법 변환

Polyfill → 브라우저에 없는 API 보완

webpack → 모듈 분석과 bundle·chunk 생성

CI 및 브라우저 테스트 → 설정과 최종 산출물 검증&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Babel, Polyfill Provider, webpack &lt;code&gt;target&lt;/code&gt;은 Browserslist를 공통 지원 기준으로 사용할 수 있지만,&lt;br&gt;각 도구가 맡는 책임은 다르다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Browserslist&lt;/code&gt;는 지원 대상을 정의할 뿐 코드를 직접 변환하지 않으며, &lt;/li&gt;
&lt;li&gt;&lt;code&gt;Babel&lt;/code&gt; 로 문법을 변환해도 브라우저에 없는 API까지 모두 보완되는 것은 아니다. &lt;/li&gt;
&lt;li&gt;&lt;code&gt;webpack&lt;/code&gt; 역시 모듈을 결합하고 산출물을 생성하지만, 그 자체만으로 사용자 코드의 모든 호환성 문제를 해결하지는 않는다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ESLint&lt;/code&gt;로 브라우저 호환성까지 검사하려면 별도 규칙이나 플러그인이 필요하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;따라서 &lt;strong&gt;지원 범위 정의, 원본 코드 검사, 문법 변환, API 보완, 모듈 로딩, 최종 검증&lt;/strong&gt;을 하나의 관리 흐름으로 구성해야 한다.&lt;br&gt;다음 문단에서 webpack 프로젝트를 기준으로 Browserslist, ESLint, Babel, Polyfill이 각각 어떤 역할을 담당하며,&lt;br&gt;webpack 빌드 과정에서 어떻게 연결되는지 살펴보자!&lt;/p&gt;
&lt;p&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;h1&gt;2. 브라우저 호환성을 관리하는 도구(with. webpack)&lt;/h1&gt;
&lt;p&gt;webpack 프로젝트에서 브라우저 호환성을 관리하려면 각 도구의 역할을 구분해야 한다. (&lt;a href=&quot;https://github.com/KumJungMin/browser-compatibility-template&quot;&gt;코드 예시&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;webpack이 모든 호환성 작업을 직접 처리하는 것은 아니다.&lt;br&gt;webpack은 모듈을 탐색하고 전체 빌드 흐름을 관리하며,&lt;br&gt;코드의 문법 변환은 &lt;code&gt;babel-loader&lt;/code&gt;를 통해 &lt;code&gt;Babel&lt;/code&gt;에 위임한다.&lt;br&gt;브라우저에 없는 JavaScript 표준 API는 Babel Polyfill Provider와 &lt;code&gt;core-js&lt;/code&gt;를 통해 보완한다.&lt;/p&gt;
&lt;br/&gt;

&lt;h2&gt;2.1. 도구별 역할&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;도구·설정&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;역할&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Browserslist&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;지원할 브라우저 범위 정의&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ESLint&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;원본 코드의 오류와 규칙 위반 검사&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;webpack&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;모듈 탐색과 전체 빌드 흐름 관리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;babel-loader&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;webpack이 발견한 JavaScript 파일을 Babel에 전달&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Babel&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;지원 브라우저에 맞게 최신 JavaScript 문법 변환&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Babel Polyfill Provider&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;필요한 Polyfill을 판단하고 import 추가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;core-js&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;JavaScript 표준 객체와 메서드 구현 제공&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;webpack &lt;code&gt;target&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;특정 환경에 맞는 webpack 런타임 코드를 생성하도록 설정&lt;br&gt;(&lt;em&gt;단, 사용자 코드 문법은 변환하지 않음&lt;/em&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;&lt;code&gt;Browserslist&lt;/code&gt;는 Babel과 webpack 등이 참고하는 &lt;strong&gt;지원 환경 기준&lt;/strong&gt;이다.&lt;br&gt;ESLint는 일반적으로 webpack 빌드와 별도로 실행되는 &lt;strong&gt;정적 검사 단계&lt;/strong&gt;다.&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;webpack 빌드는 다음과 같은 흐름으로 이해할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;webpack이 Entry에서 모듈 탐색 시작
    ↓
babel-loader가 JavaScript 파일을 Babel에 전달
    ↓
Babel이 문법을 변환
    ↓
Polyfill 플러그인이 필요한 core-js import 추가
    ↓
webpack이 코드와 Polyfill을 번들링
    ↓
target에 맞는 webpack 런타임 생성&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;h3&gt;(1) Browserslist: 지원 환경 정의&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;Browserslist&lt;/code&gt;는 최종 산출물이 지원해야 하는 브라우저 범위를 정의한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ini&quot;&gt;[production]
&amp;gt; 0.5%
not dead

[development]
last 1 Chrome version&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Browserslist&lt;/code&gt;는 위와 같은 쿼리를 실제 브라우저와 버전 목록으로 변환한다.&lt;br&gt;계산된 결과는 여러 도구가 공통 기준으로 사용할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Browserslist
    ├─ Babel
    │   └─ 어떤 문법을 변환할지 판단
    │
    ├─ Polyfill Provider
    │   └─ 어떤 표준 API를 보완할지 판단
    │
    └─ webpack target
        └─ 런타임에서 사용할 수 있는 기능 판단&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Browserslist&lt;/code&gt;는 지원 환경을 정의할 뿐, 코드를 직접 검사하거나 변환하지 않는다.&lt;/p&gt;
&lt;p&gt;따라서 Babel, Polyfill Provider, webpack에 서로 다른 브라우저 범위를 중복으로 설정하기보다,&lt;br&gt;&lt;code&gt;Browserslist&lt;/code&gt;를 공통 기준으로 사용해야 한다.&lt;br&gt;그래야 문법 변환, API 보완, webpack 런타임이 동일한 지원 정책을 기준으로 동작한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Browserslist
→ 지원 환경 정의

Babel·Polyfill Provider·webpack
→ 동일한 지원 환경을 참고하여 실제 처리&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;각 도구에서 별도의 &lt;code&gt;targets&lt;/code&gt;를 지정하지 않고 동일한 Browserslist 환경을 참조하면,&lt;br&gt;&lt;code&gt;Browserslist&lt;/code&gt; 설정을 변경해 지원 범위를 함께 조정할 수 있다.&lt;/p&gt;
&lt;br/&gt;

&lt;h3&gt;(2) ESLint: 원본 코드 검사&lt;/h3&gt;
&lt;p&gt;ESLint는 브라우저에서 실행할 산출물을 만드는 도구가 아니라,&lt;br&gt;개발자가 작성한 원본 코드에서 문제를 미리 찾는 정적 분석 도구다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;원본 코드
    ↓
문법 파싱 및 AST 생성
    ↓
ESLint 규칙 실행
    ↓
오류와 경고 출력&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;최신 문법을 Babel로 변환할 예정이라면 ESLint도 해당 원본 문법을 해석할 수 있어야 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;export default [
  {
    languageOptions: {
      ecmaVersion: &amp;quot;latest&amp;quot;,
      sourceType: &amp;quot;module&amp;quot;,
    },
  },
];&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;ecmaVersion: &amp;quot;latest&amp;quot;&lt;/code&gt;는 ESLint가 Optional Chaining, Nullish Coalescing 등의 최신 문법을 파싱할 수 있도록 한다.&lt;br&gt;다만 이 설정은 &lt;strong&gt;ESLint가 원본 코드를 읽을 수 있게 하는 것&lt;/strong&gt;일 뿐, 해당 문법을 지원 브라우저가 직접 실행할 수 있다는 의미는 아니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;ESLint ecmaVersion
→ ESLint가 해석할 원본 코드의 문법 범위

Browserslist
→ 최종 산출물이 지원해야 하는 브라우저 범위&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;ESLint&lt;/code&gt;는 기본적으로 코드 품질과 규칙 위반을 검사한다.&lt;br&gt;지원 브라우저에 없는 API 사용까지 확인하려면, &lt;code&gt;Browserslist&lt;/code&gt; 설정을 참조하는 별도의 호환성 규칙이나&lt;br&gt;플러그인을 추가해야 한다.&lt;/p&gt;
&lt;br/&gt;

&lt;h3&gt;(3) babel-loader와 Babel: 최신 문법 변환&lt;/h3&gt;
&lt;p&gt;webpack의 &lt;code&gt;module.rules&lt;/code&gt;는 &lt;code&gt;test&lt;/code&gt;, &lt;code&gt;include&lt;/code&gt;, &lt;code&gt;exclude&lt;/code&gt; 조건으로 어떤 모듈에 Loader를 적용할지 정한다.&lt;br&gt;&lt;code&gt;babel-loader&lt;/code&gt;는 이 조건과 일치한 모듈에서 Babel을 실행해 webpack과 Babel을 연결한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;webpack module.rules
→ 어떤 모듈에 어떤 Loader를 적용할지 결정

babel-loader
→ webpack에서 Babel 변환 실행

Babel
→ 실제 JavaScript 문법 변환&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Babel의 &lt;code&gt;@babel/preset-env&lt;/code&gt;는 &lt;code&gt;Browserslist&lt;/code&gt;에서 계산한 지원 대상을 참고해 필요한 변환 플러그인을 선택한다.&lt;br&gt;지원 브라우저가 이해하는 문법은 유지하고, 지원하지 않는 문법만 호환 가능한 형태로 변환한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const name = user?.name ?? &amp;quot;Unknown&amp;quot;;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;지원 대상이 Optional Chaining과 Nullish Coalescing을 지원하지 않는다면, 다음과 같은 형태로 변환된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;var _user = user;
var _name = _user == null ? undefined : _user.name;
var name = _name == null ? &amp;quot;Unknown&amp;quot; : _name;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 과정은 단순한 문자열 치환이 아니다.&lt;br&gt;&lt;code&gt;Babel&lt;/code&gt;은 원본 코드를 AST로 변환한 뒤, 지원 브라우저에 필요한 문법 변환을 적용하고 다시 JavaScript 코드를 생성한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;원본 JavaScript
    ↓
AST 생성
    ↓
Browserslist 지원 대상 확인
    ↓
필요한 문법 변환
    ↓
JavaScript 코드 재생성
    ↓
변환 결과를 webpack에 반환&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;webpack은 Babel이 반환한 코드를 다른 모듈과 함께 최종 bundle에 포함한다.&lt;/p&gt;
&lt;br/&gt;

&lt;h3&gt;(4) Babel Polyfill Provider와 core-js: 표준 API 보완&lt;/h3&gt;
&lt;p&gt;Babel은 JavaScript 문법을 변환하지만, 브라우저에 존재하지 않는 표준 객체나 메서드까지 직접 구현하지는 않는다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const name = user?.name;
const lastUser = users.at(-1);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;?.&lt;/code&gt;는 최신 JavaScript 문법이므로 Babel이 기존 문법으로 변환할 수 있다.&lt;/p&gt;
&lt;p&gt;반면 &lt;code&gt;users.at(-1)&lt;/code&gt;은 기존의 &lt;code&gt;객체.메서드()&lt;/code&gt; 문법을 사용한다.&lt;br&gt;브라우저가 코드 구조를 이해하더라도 &lt;code&gt;Array.prototype.at&lt;/code&gt;이 존재하지 않으면 실행 중 오류가 발생한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;TypeError: users.at is not a function&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이처럼 브라우저에 없는 JavaScript 표준 객체나 메서드를 보완하는 코드를 &lt;strong&gt;Polyfill&lt;/strong&gt;이라고 한다.&lt;/p&gt;
&lt;p&gt;Polyfill을 직접 구현할 수도 있지만, 일반적으로는 표준 기능별 구현을 제공하는 라이브러리를 사용한다.&lt;br&gt;그중 &lt;code&gt;core-js&lt;/code&gt;는 ECMAScript 표준 라이브러리와 일부 웹 표준 기능에 대한 다양한 Polyfill을 제공한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;필요한 Polyfill 판단·import 추가 → Babel Polyfill Provider

실제 표준 API 구현 제공 → core-js

추가된 Polyfill 모듈을 bundle에 포함 → webpack&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;h3&gt;(5) webpack &lt;code&gt;target&lt;/code&gt;: webpack 런타임의 실행 환경&lt;/h3&gt;
&lt;p&gt;webpack은 애플리케이션 모듈을 결합하면서 bundle 실행에 필요한 자체 런타임 코드도 생성한다.&lt;/p&gt;
&lt;p&gt;이 런타임은 모듈 실행과 캐시, 동적 &lt;code&gt;import()&lt;/code&gt;, 비동기 chunk 로딩 등의 작업을 담당한다.&lt;br&gt;&lt;code&gt;target&lt;/code&gt;은 &lt;strong&gt;webpack이 생성하는 런타임 코드와 chunk 로딩 방식을 어떤 실행 환경에 맞출 것인지&lt;/strong&gt; 지정하는 설정이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;module.exports = {
  target: &amp;quot;browserslist&amp;quot;,
};&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;target: &amp;quot;browserslist&amp;quot;&lt;/code&gt;를 설정하면 webpack은 Browserslist 설정을 참고해&lt;br&gt;런타임 코드에서 사용할 수 있는 ECMAScript 기능과 실행 환경을 판단한다. (&lt;a href=&quot;https://webpack.js.org/configuration/target/&quot; title=&quot;Target | webpack&quot;&gt;webpack&lt;/a&gt;)&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;개발자가 작성한 JavaScript → Babel이 Browserslist를 기준으로 변환

webpack이 생성한 런타임 코드 → webpack target이 Browserslist를 기준으로 생성&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;target&lt;/code&gt;은 사용자 코드를 변환하지 않는다.&lt;br&gt;따라서 Optional Chaining과 같은 최신 문법은 Babel을 통해 변환해야 한다! (&lt;a href=&quot;https://webpack.js.org/configuration/target/&quot; title=&quot;Target | webpack&quot;&gt;webpack&lt;/a&gt;)&lt;/p&gt;
&lt;br/&gt;

&lt;h4&gt;만약 Browserslist가 없고, target만 설정했다면?&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;module.exports = {
  target: [&amp;quot;web&amp;quot;, &amp;quot;es2020&amp;quot;],
};&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 설정은 webpack이 브라우저에서 ES2020 기능을 사용할 수 있다고 가정한다.&lt;br&gt;그러나 개발자가 작성한 사용자 코드까지 ES2020 기준으로 변환하는 것은 아니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;babel-loader&lt;/code&gt;는 webpack의 &lt;code&gt;target&lt;/code&gt; 정보를 Babel에 전달하지만,&lt;br&gt;&lt;code&gt;@babel/preset-env&lt;/code&gt;가 이를 자동으로 자신의 &lt;code&gt;targets&lt;/code&gt;로 사용하지는 않는다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;webpack target → webpack 런타임과 로딩 코드의 생성 기준

Babel preset-env targets → 사용자 코드의 문법 변환 기준&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;따라서 Browserslist가 없다면, Babel의 &lt;code&gt;targets&lt;/code&gt;도 별도로 설정해야 두 코드의 지원 범위를 맞출 수 있다.&lt;/p&gt;
&lt;br/&gt;

&lt;h4&gt;만약 Browserslist와 target이 다르다면?&lt;/h4&gt;
&lt;p&gt;Babel은 Browserslist를 기준으로 사용자 코드를 변환하더라도,&lt;br&gt;webpack의 &lt;code&gt;target&lt;/code&gt;이 다른 환경을 가리키면 webpack 런타임은 다른 기준으로 생성된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;module.exports = {
  target: [&amp;quot;web&amp;quot;, &amp;quot;es2020&amp;quot;],
};&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 설정은 브라우저 환경에서 ES2020 기능을 사용할 수 있다고 webpack에 알린다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Browserslist → 오래된 브라우저까지 지원

webpack target → web 플랫폼의 ES2020 환경을 가정&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이처럼 &lt;code&gt;target&lt;/code&gt; &amp;gt; Browserslist 환경 이라면, 사용자 코드는 정상적으로 변환되었더라도&lt;br&gt;webpack 런타임에 오래된 브라우저가 지원하지 않는 기능이 포함될 수 있다. &lt;/p&gt;
&lt;p&gt;반대로 &lt;code&gt;target&lt;/code&gt;&amp;lt; Browserslist 환경이라면,&lt;br&gt;런타임 코드가 필요 이상으로 보수적으로 생성될 수 있다.&lt;/p&gt;
&lt;p&gt;따라서 애플리케이션에서는 Babel과 webpack 런타임이 동일한 지원 범위를 사용하도록&lt;br&gt;&lt;code&gt;target: &amp;quot;browserslist&amp;quot;&lt;/code&gt;로 맞추는 것이 안전하다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Browserslist
→ 사용자 코드와 Polyfill의 지원 기준

webpack target: &amp;quot;browserslist&amp;quot;
→ webpack 런타임도 같은 기준 적용&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;h2&gt;2.2. 도구별 역할 정리&lt;/h2&gt;
&lt;p&gt;앞에서 살펴본 도구의 책임을 질문 형태로 정리하면 다음과 같다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;도구·설정&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;해결하는 질문&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Browserslist&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;어떤 브라우저를 지원할 것인가?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ESLint&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;원본 코드에 문제가 없는가?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;webpack&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;어떤 모듈이 필요하며, 이를 어떻게 묶을 것인가?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;babel-loader&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;webpack에서 Babel 변환을 어떻게 실행할 것인가?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Babel&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;지원 브라우저를 위해 어떤 최신 문법을 변환할 것인가?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Babel Polyfill Provider&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;어떤 Polyfill import가 필요한가?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;core-js&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;브라우저에 없는 ECMAScript 표준 API를 어떻게 보완할 것인가?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;webpack &lt;code&gt;target&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;webpack 런타임을 어떤 환경에 맞출 것인가?&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;관계를 개념적으로 표현하면 다음과 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eyBwPr/dJMcahE8Qoe/KGsQOtKZ2uGNQAtFrNvJy1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eyBwPr/dJMcahE8Qoe/KGsQOtKZ2uGNQAtFrNvJy1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eyBwPr/dJMcahE8Qoe/KGsQOtKZ2uGNQAtFrNvJy1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeyBwPr%2FdJMcahE8Qoe%2FKGsQOtKZ2uGNQAtFrNvJy1%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;위 그림은 도구별 역할을 보여 주기 위해 단순화한 것이다.&lt;br&gt;실제 webpack은 Entry에서 시작해 발견한 의존 모듈을 반복해 처리하며 의존성 그래프를 만든다.&lt;/p&gt;
&lt;p&gt;Browserslist는 코드를 직접 처리하는 단계가 아니다. &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Babel, Polyfill Provider, webpack &lt;code&gt;target&lt;/code&gt;이 호환성 수준을 판단할 때 참고할 수 있는 &lt;strong&gt;공통 지원 기준&lt;/strong&gt;이다. &lt;/li&gt;
&lt;li&gt;ESLint 역시 일반적인 webpack 빌드 흐름 안에서 코드를 변환하는 도구가 아니라, 원본 코드를 별도로 검사하는 도구다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;webpack은 &lt;code&gt;babel-loader&lt;/code&gt;를 통해 Babel 변환을 빌드 과정에 포함한다. &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Babel 내부의 Polyfill Provider가 필요한 &lt;code&gt;core-js&lt;/code&gt; import를 추가하면, &lt;/li&gt;
&lt;li&gt;webpack은 이를 다른 모듈과 함께 의존성 그래프에 포함해 최종 bundle과 chunk를 생성한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;h1&gt;3. 마치며...&lt;/h1&gt;
&lt;p&gt;브라우저 호환성은 단순히 Babel을 설치하거나 webpack의 &lt;code&gt;target&lt;/code&gt;을 변경하는 것으로 완성되지 않는다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;먼저 Browserslist로 지원 환경을 명확하게 정의해야 한다. &lt;/li&gt;
&lt;li&gt;이후 Babel이 해당 환경에 맞게 사용자 코드의 문법을 변환하고, &lt;/li&gt;
&lt;li&gt;Babel Polyfill Provider와 &lt;code&gt;core-js&lt;/code&gt;가 누락된 JavaScript 표준 API를 보완한다.&lt;br&gt;webpack은 &lt;/li&gt;
&lt;li&gt;&lt;code&gt;babel-loader&lt;/code&gt;를 통해 Babel 변환을 빌드에 포함하고, &lt;/li&gt;
&lt;li&gt;Babel이 추가한 Polyfill import를 다른 모듈과 함께 최종 bundle과 chunk로 조립한다.&lt;/li&gt;
&lt;li&gt;webpack &lt;code&gt;target&lt;/code&gt;은 webpack이 자체적으로 생성하는 런타임 코드를 지원 환경에 맞춘다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;80%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xwO8S/dJMcabrrrK3/WxNSnK4eOgx6p5K5sFbbl0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xwO8S/dJMcabrrrK3/WxNSnK4eOgx6p5K5sFbbl0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xwO8S/dJMcabrrrK3/WxNSnK4eOgx6p5K5sFbbl0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxwO8S%2FdJMcabrrrK3%2FWxNSnK4eOgx6p5K5sFbbl0%2Fimg.png&quot; width=&quot;80%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;각 도구는 서로 연결되어 있지만 책임은 다르다.&lt;br&gt;이 책임을 구분하면 호환성 문제가 발생했을 때 원인도 명확하게 추적할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;80%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BlRtl/dJMcabdOE9l/KJKSCGMcbIRvcNvm7G4UIk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BlRtl/dJMcabdOE9l/KJKSCGMcbIRvcNvm7G4UIk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BlRtl/dJMcabdOE9l/KJKSCGMcbIRvcNvm7G4UIk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBlRtl%2FdJMcabdOE9l%2FKJKSCGMcbIRvcNvm7G4UIk%2Fimg.png&quot; width=&quot;80%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;브라우저가 코드를 파싱하지 못한다면 Babel 변환 범위를 확인한다.&lt;/li&gt;
&lt;li&gt;특정 함수가 없다는 실행 오류가 발생한다면 Polyfill 범위를 확인한다.&lt;/li&gt;
&lt;li&gt;외부 모듈이나 chunk를 불러오지 못한다면 webpack 번들 및 런타임 설정을 확인한다.&lt;/li&gt;
&lt;li&gt;특정 Web API만 동작하지 않는다면 core-js가 아닌 별도 Polyfill이 필요한지 확인한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;결국 브라우저 호환성을 지킨다는 것은 모든 코드를 가장 오래된 문법으로 바꾸는 일이 아니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;지원할 환경을 먼저 정의하고,&lt;br&gt;그 환경에 필요한 문법 변환과 API 보완만 적용한 뒤,&lt;br&gt;실제 브라우저에서 최종 산출물을 검증하는 과정이다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;</description>
      <category>개발 기술/개발 이야기</category>
      <category>Babel preset-env</category>
      <category>babel-loader</category>
      <category>Browserslist</category>
      <category>core-js</category>
      <category>JavaScript Polyfill</category>
      <category>polyfill</category>
      <category>webpack target</category>
      <category>webpack 브라우저 호환성</category>
      <category>바벨</category>
      <category>브라우저 호환성</category>
      <author>GicoMomg</author>
      <guid isPermaLink="true">https://mong-blog.tistory.com/247</guid>
      <comments>https://mong-blog.tistory.com/entry/%EB%82%B4-%EC%84%9C%EB%B9%84%EC%8A%A4%EC%9D%98-%ED%98%B8%ED%99%98%EC%84%B1%EC%9D%84-%EC%A7%80%ED%82%A4%EB%8A%94-%EB%B0%A9%EB%B2%95-Browserslist-Babel-Polyfill-target-%EB%8F%84%EA%B5%AC-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0#entry247comment</comments>
      <pubDate>Sat, 18 Jul 2026 20:06:28 +0900</pubDate>
    </item>
    <item>
      <title>dependencies, devDependencies, peerDependencies는 번들 결과를 결정하지 않는다</title>
      <link>https://mong-blog.tistory.com/entry/dependencies-devDependencies-peerDependencies%EB%8A%94-%EB%B2%88%EB%93%A4-%EA%B2%B0%EA%B3%BC%EB%A5%BC-%EA%B2%B0%EC%A0%95%ED%95%98%EC%A7%80-%EC%95%8A%EB%8A%94%EB%8B%A4</link>
      <description>&lt;h1&gt;0. 들어가며…&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;package.json&lt;/code&gt;에는 프로젝트 이름, 버전, 실행 스크립트, 빌드 설정 등 다양한 정보가 담겨 있다.&lt;/p&gt;
&lt;p&gt;그중 &lt;code&gt;dependencies&lt;/code&gt;, &lt;code&gt;devDependencies&lt;/code&gt;, &lt;code&gt;peerDependencies&lt;/code&gt;는 프로젝트가 사용하는 &lt;strong&gt;의존성&lt;/strong&gt;을 설명하는 항목이다.&lt;/p&gt;
&lt;p&gt;처음에는 이 항목들을 단순히 “설치된 라이브러리를 관리하는 곳” 정도로 이해하기 쉽다.&lt;/p&gt;
&lt;p&gt;하지만 실제로 패키지를 설치하거나, 애플리케이션이 아닌 라이브러리를 만들다 보면 이런 질문이 생긴다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;이 패키지는 dependencies에 넣어야 할까?
devDependencies에 넣어야 할까?
peerDependencies에 넣어야 할까?&lt;/code&gt;&lt;/pre&gt;&lt;br/&gt;

&lt;p&gt;특히 라이브러리를 만들 때는 단순히 “설치했으니까 dependencies에 넣는다”로 끝나지 않는다.&lt;br&gt;이 패키지가 실행 시 필요한지, 개발할 때만 필요한지, 아니면 사용하는 프로젝트가 직접 제공해야 하는지에 따라 선언 위치가 달라진다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &amp;quot;dependencies&amp;quot;: {},
  &amp;quot;devDependencies&amp;quot;: {},
  &amp;quot;peerDependencies&amp;quot;: {}
}&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;그런데 여기서 한 가지 더 헷갈리는 지점이 있다.&lt;br&gt;의존성을 구분해서 선언했는데, 실제 빌드 결과를 보면 예상과 다르게 동작하는 경우가 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dependencies에 넣었는데 번들에 포함되지 않네?

devDependencies에 있는데 번들에 포함되네?

React를 peerDependencies로 선언했지만, React가 번들에 포함되네?&lt;/code&gt;&lt;/pre&gt;&lt;br/&gt;

&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;code&gt;dependencies&lt;/code&gt;, &lt;code&gt;devDependencies&lt;/code&gt;, &lt;code&gt;peerDependencies&lt;/code&gt;를 구분했는데도&lt;br&gt;왜 번들 결과는 예상과 다르게 나올까?&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;그 이유는 &lt;code&gt;dependencies&lt;/code&gt;, &lt;code&gt;devDependencies&lt;/code&gt;, &lt;code&gt;peerDependencies&lt;/code&gt;가 번들 결과를 직접 결정하는 설정이 아니기 때문이다.&lt;/p&gt;
&lt;p&gt;세 가지 의존성 분류는 “무엇을 번들에 넣을지”를 결정하는 옵션이라기보다, &lt;strong&gt;패키지와 프로젝트의 관계를 표현하는 메타데이터&lt;/strong&gt;에 가깝다.&lt;/p&gt;
&lt;p&gt;결국 번들 결과는 의존성 분류 자체가 아니라, 실제 코드의 &lt;code&gt;import&lt;/code&gt; 구조와 번들러 설정에 의해 결정된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;70%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bUvg8T/dJMcaijjg7z/pGr8WEkNcw5soMFcT9J2ok/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bUvg8T/dJMcaijjg7z/pGr8WEkNcw5soMFcT9J2ok/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bUvg8T/dJMcaijjg7z/pGr8WEkNcw5soMFcT9J2ok/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbUvg8T%2FdJMcaijjg7z%2FpGr8WEkNcw5soMFcT9J2ok%2Fimg.png&quot; width=&quot;70%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;package.json 의존성 분류&lt;/th&gt;
&lt;th&gt;번들러&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;이 패키지가 어떤 관계로 필요한지 설명한다.&lt;/td&gt;
&lt;td&gt;실제 코드가 무엇을 import하는지 분석해 산출물을 만든다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;p&gt;이번 글에서는 다음 질문들을 중심으로 살펴보려 한다.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;1. 각 의존성 분류는 무엇을 의미할까?&lt;/em&gt; &lt;/p&gt;
&lt;p&gt;&lt;em&gt;2. 왜 이 분류가 번들 결과를 직접 결정하지 않을까?&lt;/em&gt; &lt;/p&gt;
&lt;p&gt;&lt;em&gt;3. React UI 라이브러리에서는 왜 peerDependencies만으로 충분하지 않을까?&lt;/em&gt; &lt;/p&gt;
&lt;p&gt;&lt;em&gt;4. 실무에서는 어떤 기준으로 의존성을 나누면 좋을까?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;h1&gt;1. 의존성 분류를 먼저 이해하자&lt;/h1&gt;
&lt;p&gt;앞에서 살펴본 것처럼 &lt;code&gt;dependencies&lt;/code&gt;, &lt;code&gt;devDependencies&lt;/code&gt;, &lt;code&gt;peerDependencies&lt;/code&gt;는 번들 결과를 직접 결정하는 설정이 아니다.&lt;/p&gt;
&lt;p&gt;이 세 분류는 “무엇을 번들에 포함할지”를 정하는 옵션이라기보다, &lt;strong&gt;패키지가 프로젝트와 어떤 관계를 맺고 있는지 설명하는 정보&lt;/strong&gt;에 가깝다.&lt;/p&gt;
&lt;p&gt;따라서 번들 결과를 이야기하기 전에, 먼저 각 분류가 실제로 무엇을 의미하는지부터 정리해보자.&lt;/p&gt;
&lt;br/&gt;

&lt;h2&gt;1-1. 세 분류는 무엇을 설명할까?&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;package.json&lt;/code&gt;의 의존성은 보통 세 가지로 나뉜다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &amp;quot;dependencies&amp;quot;: {},
  &amp;quot;devDependencies&amp;quot;: {},
  &amp;quot;peerDependencies&amp;quot;: {}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;각 분류의 역할은 다음과 같다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;분류&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;역할&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;예시&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dependencies&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;애플리케이션 실행에 필요한 패키지&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;react&lt;/code&gt;, &lt;code&gt;vue&lt;/code&gt;, &lt;code&gt;axios&lt;/code&gt;, &lt;code&gt;zustand&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;devDependencies&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;개발·빌드·테스트·린트에 필요한 패키지&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;typescript&lt;/code&gt;, &lt;code&gt;vite&lt;/code&gt;, &lt;code&gt;webpack&lt;/code&gt;, &lt;code&gt;eslint&lt;/code&gt;, &lt;code&gt;vitest&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;peerDependencies&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;라이브러리가 직접 포함하지 않고, 사용하는 프로젝트가 직접 제공해야 하는 패키지&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;react&lt;/code&gt;, &lt;code&gt;react-dom&lt;/code&gt;, &lt;code&gt;vue&lt;/code&gt;, &lt;code&gt;styled-components&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;br/&gt;

&lt;p&gt;짧게 정리하면 다음과 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dependencies
→ 실행 중 필요한 패키지

devDependencies
→ 개발 과정에서 필요한 패키지

peerDependencies
→ 라이브러리 사용자가 직접 제공해야 하는 패키지&lt;/code&gt;&lt;/pre&gt;&lt;br/&gt;

&lt;h3&gt;(1) dependencies&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;dependencies&lt;/code&gt;는 애플리케이션이 실행될 때 필요한 패키지다.&lt;/p&gt;
&lt;p&gt;즉, 사용자가 서비스를 이용할 때 실제로 동작해야 하는 코드라면 &lt;code&gt;dependencies&lt;/code&gt;에 둬야 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;예시
→ react, vue, axios, zustand, date-fns, lodash&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;예를 들어 앱 코드에서 &lt;code&gt;axios&lt;/code&gt;로 API 요청을 보낸다면, &lt;code&gt;axios&lt;/code&gt;는 런타임에 필요한 패키지다.&lt;/p&gt;
&lt;p&gt;따라서 이런 패키지는 &lt;code&gt;dependencies&lt;/code&gt;에 선언하는 것이 자연스럽다.&lt;/p&gt;
&lt;br/&gt;

&lt;h3&gt;(2) devDependencies&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;devDependencies&lt;/code&gt;는 개발, 빌드, 테스트, 린트처럼 개발 과정에서 필요한 패키지다.&lt;/p&gt;
&lt;p&gt;사용자가 서비스를 이용할 때 직접 실행되는 코드는 아니지만, 프로젝트를 만들고 검증하는 과정에서 필요한 도구들이 여기에 해당한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;예시
→ typescript, vite, webpack, eslint, vitest, prettier&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;예를 들어 &lt;code&gt;vite&lt;/code&gt;는 개발 서버를 실행하거나 빌드할 때 필요하다.&lt;/p&gt;
&lt;p&gt;하지만 사용자가 브라우저에서 서비스를 이용하는 순간에 &lt;code&gt;vite&lt;/code&gt; 자체가 실행되는 것은 아니다.&lt;/p&gt;
&lt;p&gt;따라서 이런 도구성 패키지는 보통 &lt;code&gt;devDependencies&lt;/code&gt;에 선언한다.&lt;/p&gt;
&lt;br/&gt;

&lt;h3&gt;(3) peerDependencies&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;peerDependencies&lt;/code&gt;는 이 패키지를 사용하는 쪽에서 직접 설치해 제공해야 하는 패키지이다.&lt;/p&gt;
&lt;p&gt;특히 애플리케이션보다 &lt;strong&gt;라이브러리를 만들 때&lt;/strong&gt; 중요하다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;예시
→ react, react-dom, vue, styled-components&lt;/code&gt;&lt;/pre&gt;&lt;br/&gt;

&lt;p&gt;예를 들어 React UI 라이브러리를 만든다고 해보자.&lt;/p&gt;
&lt;p&gt;이때 라이브러리 내부에 &lt;code&gt;react&lt;/code&gt;를 포함해 배포하기보다, 사용자 프로젝트에 이미 설치된 React를 함께 사용하도록 만드는 경우가 많다.&lt;/p&gt;
&lt;p&gt;그래서 React UI 라이브러리에서는 보통 &lt;code&gt;react&lt;/code&gt;, &lt;code&gt;react-dom&lt;/code&gt;을 &lt;code&gt;peerDependencies&lt;/code&gt;로 선언한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &amp;quot;peerDependencies&amp;quot;: {
    &amp;quot;react&amp;quot;: &amp;quot;&amp;gt;=18&amp;quot;,
    &amp;quot;react-dom&amp;quot;: &amp;quot;&amp;gt;=18&amp;quot;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;이 선언의 의미는 다음과 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;이 라이브러리는 react가 필요합니다.
하지만 react를 라이브러리 안에 직접 포함하지는 않습니다.
사용하는 프로젝트에서 react를 설치해 제공해주세요.&lt;/code&gt;&lt;/pre&gt;&lt;br/&gt;


&lt;p&gt;즉, &lt;code&gt;peerDependencies&lt;/code&gt;는 라이브러리와 사용자 프로젝트가 동일한 패키지를 함께 사용해야 할 때 활용한다.&lt;/p&gt;
&lt;p&gt;예를 들어 라이브러리마다 React를 각각 포함해 배포하면, 사용자 프로젝트 안에 React가 중복 설치되거나 서로 다른 React 인스턴스를 참조하는 문제가 발생할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;h2&gt;1-2. peerDependencies와 external은 역할이 다르다&lt;/h2&gt;
&lt;p&gt;여기서 주의해야 할 점이 있다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;peerDependencies&lt;/code&gt;에 선언했다고 해서 해당 패키지가 자동으로 번들 결과물에서 제외되는 것은 아니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;peerDependencies&lt;/code&gt;는 “이 패키지는 사용하는 프로젝트가 직접 설치해 제공해야 한다”는 의존성 관계를 표현하는 설정이다.&lt;/p&gt;
&lt;p&gt;반면 &lt;code&gt;external&lt;/code&gt;은 번들러에게 “이 패키지는 번들에 포함하지 말라”고 알려주는 설정이다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;peerDependencies
→ 사용자 프로젝트가 직접 제공해야 하는 패키지라고 선언한다.

external
→ 번들 결과물에서 제외할 패키지를 지정한다.&lt;/code&gt;&lt;/pre&gt;&lt;br/&gt;

&lt;p&gt;예를 들어 React UI 라이브러리에서 &lt;code&gt;react&lt;/code&gt;와 &lt;code&gt;react-dom&lt;/code&gt;을 &lt;code&gt;peerDependencies&lt;/code&gt;로 선언했다고 해보자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &amp;quot;peerDependencies&amp;quot;: {
    &amp;quot;react&amp;quot;: &amp;quot;&amp;gt;=18&amp;quot;,
    &amp;quot;react-dom&amp;quot;: &amp;quot;&amp;gt;=18&amp;quot;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 설정은 “이 라이브러리를 사용하려면 프로젝트에 React가 필요하다”는 의미다.&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;하지만 이것만으로 번들러가 &lt;code&gt;react&lt;/code&gt;와 &lt;code&gt;react-dom&lt;/code&gt;을 자동으로 제외한다고 볼 수는 없다.&lt;br&gt;실제 번들 결과물에서 React를 제외하려면 번들러 설정에서 &lt;code&gt;external&lt;/code&gt;도 함께 지정해야 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// 예시: Rollup / Vite library mode

export default {
  build: {
    rollupOptions: {
      external: [&amp;quot;react&amp;quot;, &amp;quot;react-dom&amp;quot;]
    }
  }
};&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;React UI 라이브러리를 만들 때는 보통 다음 두 가지를 함께 설정한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1. package.json의 peerDependencies에 react, react-dom 선언

2. 번들러 설정에서 react, react-dom을 external 처리&lt;/code&gt;&lt;/pre&gt;&lt;br/&gt;


&lt;p&gt;정리하면 &lt;code&gt;peerDependencies&lt;/code&gt;와 &lt;code&gt;external&lt;/code&gt;은 같은 역할을 하는 설정이 아니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;peerDependencies&lt;/code&gt;는 패키지 사용 조건을 설명하는 설정이고, &lt;code&gt;external&lt;/code&gt;은 실제 번들링 결과를 제어하는 설정이다.&lt;/p&gt;
&lt;p&gt;만약 라이브러리를 만들 때는 “사용자 프로젝트가 제공해야 하는 패키지인지”와 “번들 결과물에서 제외해야 하는 패키지인지”를 나누어 생각해야 한다.&lt;/p&gt;
&lt;p&gt;(&lt;em&gt;참고로 최근에는 &lt;code&gt;tsup&lt;/code&gt;, &lt;code&gt;microbundle&lt;/code&gt; 같은 라이브러리 빌드 도구나 &lt;code&gt;rollup-plugin-peer-deps-external&lt;/code&gt; 플러그인이 &lt;code&gt;package.json&lt;/code&gt;의 &lt;code&gt;peerDependencies&lt;/code&gt;를 읽어 자동으로 external 처리해주기도 함&lt;/em&gt;)&lt;/p&gt;
&lt;p&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;h1&gt;2. 그런데 번들 결과는 왜 예상과 다를까?&lt;/h1&gt;
&lt;p&gt;이제 처음에 봤던 의문으로 돌아가보자.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dependencies에 넣었는데 번들에 포함되지 않는다.

devDependencies에 있는데 번들에 포함된다.

peerDependencies로 선언했지만 React가 번들에 포함된다.&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이런 상황이 발생하는 이유는 하나다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;dependencies&lt;/code&gt;, &lt;code&gt;devDependencies&lt;/code&gt;, &lt;code&gt;peerDependencies&lt;/code&gt;는 &lt;strong&gt;패키지의 역할과 관계를 설명하는 정보&lt;/strong&gt;이지, 번들 결과를 직접 결정하는 설정이 아니기 때문이다.&lt;/p&gt;
&lt;p&gt;번들러는 &lt;code&gt;package.json&lt;/code&gt;의 의존성 분류보다 실제 코드의 &lt;code&gt;import&lt;/code&gt; 구조와 번들러 설정을 기준으로 산출물을 만든다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;의존성 분류
→ 이 패키지가 프로젝트와 어떤 관계인지 설명한다.

번들러
→ 실제 코드가 무엇을 import하는지 분석해 산출물을 만든다.
&lt;/code&gt;&lt;/pre&gt;&lt;br/&gt;

&lt;h2&gt;2-1. dependencies에 있어도 import하지 않으면 번들에 들어가지 않는다&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;dependencies&lt;/code&gt;는 앱이 실행될 때 필요한 패키지를 의미한다.&lt;/p&gt;
&lt;p&gt;예를 들어 앱 코드에서 &lt;code&gt;axios&lt;/code&gt;로 API 요청을 보낸다면, &lt;code&gt;axios&lt;/code&gt;는 런타임 의존성이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &amp;quot;dependencies&amp;quot;: {
    &amp;quot;axios&amp;quot;: &amp;quot;^1.7.0&amp;quot;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import axios from &amp;quot;axios&amp;quot;;

export async function fetchUser() {
  const response = await axios.get(&amp;quot;/api/user&amp;quot;);
  return response.data;
}&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;하지만 &lt;code&gt;dependencies&lt;/code&gt;에 선언되어 있어도 실제 코드에서 import하지 않는다면 번들러는 해당 패키지를 따라갈 이유가 없다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;console.log(&amp;quot;hello&amp;quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;즉, 설치되어 있다는 사실만으로 번들에 포함되는 것은 아니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dependencies에 있음
≠ 번들 포함 보장&lt;/code&gt;&lt;/pre&gt;&lt;br/&gt;

&lt;h2&gt;2-2. devDependencies에 있어도 import하면 번들에 들어갈 수 있다&lt;/h2&gt;
&lt;p&gt;반대로 &lt;code&gt;devDependencies&lt;/code&gt;에 있는 패키지도 런타임 코드에서 import하면 번들에 포함될 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &amp;quot;devDependencies&amp;quot;: {
    &amp;quot;lodash&amp;quot;: &amp;quot;^4.17.21&amp;quot;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import debounce from &amp;quot;lodash/debounce&amp;quot;;&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;이 경우 번들러는 &lt;code&gt;lodash/debounce&lt;/code&gt;를 실제 사용되는 모듈로 보고 의존성 그래프에 포함할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;devDependencies에 있음
≠ 번들 제외 보장&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;다만 이 경우는 의존성 분류가 잘못된 것이다.&lt;/p&gt;
&lt;p&gt;런타임 코드에서 사용하는 패키지는 &lt;code&gt;dependencies&lt;/code&gt;에 두는 것이 맞다.&lt;/p&gt;
&lt;br/&gt;

&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;여기서 한 가지 구분해야 할 점이 있다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;Vite나 webpack으로 빌드하는 순수 프론트엔드 SPA라면, 배포 서버에 &lt;code&gt;node_modules&lt;/code&gt;가 그대로 올라가지 않는다.&lt;br&gt;보통 로컬 또는 CI 환경에서 의존성을 설치한 뒤 빌드하고, 운영 환경에는 &lt;code&gt;dist&lt;/code&gt; 같은 정적 결과물만 배포한다.&lt;/p&gt;
&lt;p&gt;그래서 프론트엔드 SPA에서는 런타임 코드가 실수로 &lt;code&gt;devDependencies&lt;/code&gt;의 패키지를 import하더라도,&lt;br&gt;빌드가 성공했다면 운영 서버에서 곧바로 &lt;code&gt;Cannot find module&lt;/code&gt; 오류가 발생하지 않을 수 있다.&lt;/p&gt;
&lt;p&gt;하지만 Node.js 서버 애플리케이션, SSR 환경, 또는 라이브러리 배포에서는 이야기가 달라진다.&lt;br&gt;예를 들어 서버 애플리케이션(ex. Express)이나 SSR 환경에서 운영 배포 시 다음과 같이 개발 의존성을 제외하고 설치할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install --omit=dev   // dev 유형을 제외하고 설치하는 명령어&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;이때 런타임에 필요한 패키지가 &lt;code&gt;devDependencies&lt;/code&gt;에만 있다면,&lt;br&gt;로컬에서는 동작하더라도 운영 환경에서는 모듈을 찾지 못하는 오류가 발생할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Cannot find module &amp;#39;axios&amp;#39;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;즉, 의존성 분류는 번들 결과를 직접 결정하지는 않지만, 설치 환경과 배포 안정성에는 영향을 준다.&lt;/p&gt;
&lt;br/&gt;


&lt;h2&gt;2-3. peerDependencies에 있어도 external 설정이 없으면 번들에 들어갈 수 있다&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;peerDependencies&lt;/code&gt;는 라이브러리를 사용하는 프로젝트가 직접 제공해야 하는 패키지를 선언한다.&lt;/p&gt;
&lt;p&gt;예를 들어 React UI 라이브러리라면 보통 &lt;code&gt;react&lt;/code&gt;, &lt;code&gt;react-dom&lt;/code&gt;을 &lt;code&gt;peerDependencies&lt;/code&gt;로 선언한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &amp;quot;peerDependencies&amp;quot;: {
    &amp;quot;react&amp;quot;: &amp;quot;^18.0.0 || ^19.0.0&amp;quot;,
    &amp;quot;react-dom&amp;quot;: &amp;quot;^18.0.0 || ^19.0.0&amp;quot;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;이 선언의 의미는 다음과 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;이 라이브러리는 React가 필요하다.
하지만 React를 라이브러리 안에 직접 포함하지는 않는다.
React는 이 라이브러리를 사용하는 앱이 제공해야 한다.&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;하지만 &lt;code&gt;peerDependencies&lt;/code&gt; 선언만으로 번들러가 React를 자동으로 제외하는 것은 아니다.&lt;/p&gt;
&lt;p&gt;React를 실제 번들 결과물에서 제외하려면 번들러 설정에서 &lt;code&gt;external&lt;/code&gt; 처리를 해야 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;export default {
  external: [&amp;quot;react&amp;quot;, &amp;quot;react-dom&amp;quot;]
};&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;정리하면 다음과 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;peerDependencies
→ 사용자가 직접 설치해야 한다고 선언한다.

external
→ 번들러가 실제 번들에서 제외하도록 설정한다.&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;따라서 React UI 라이브러리를 만들 때는 &lt;code&gt;peerDependencies&lt;/code&gt; 선언과 &lt;code&gt;external&lt;/code&gt; 설정을 함께 확인해야 한다.&lt;/p&gt;
&lt;p&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;h1&gt;3. 실제 번들 결과는 무엇이 결정할까?&lt;/h1&gt;
&lt;p&gt;앞에서 본 사례들은 모두 같은 이유로 발생한다.&lt;/p&gt;
&lt;p&gt;번들 결과는 &lt;code&gt;dependencies&lt;/code&gt;, &lt;code&gt;devDependencies&lt;/code&gt;, &lt;code&gt;peerDependencies&lt;/code&gt; 중 어디에 선언했는지가 아니라,&lt;br&gt;&lt;strong&gt;코드가 무엇을 import하고 번들러가 그것을 어떻게 처리하는지&lt;/strong&gt;에 의해 결정된다.&lt;/p&gt;
&lt;p&gt;번들러가 주로 보는 기준은 다음과 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;실제 번들 결과를 결정하는 것
├─ import graph
│  └─ 코드에서 실제로 import 되었는가?
│
├─ external
│  └─ 번들에서 제외하도록 설정했는가?
│
├─ tree-shaking
│  └─ 사용하지 않는 코드를 제거할 수 있는가?
│
└─ code splitting
   └─ 하나의 번들이 아니라 별도 청크로 분리되는가?&lt;/code&gt;&lt;/pre&gt;&lt;br/&gt;

&lt;p&gt;예를 들어 다음 코드가 있다고 해보자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import React from &amp;quot;react&amp;quot;;
import axios from &amp;quot;axios&amp;quot;;
import { Button } from &amp;quot;@my/ui&amp;quot;;&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;번들러는 entry 파일에서 시작해 &lt;code&gt;import&lt;/code&gt; 관계를 따라간다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;main.ts
├─ react
├─ axios
└─ @my/ui&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이 연결 구조를 보통 &lt;strong&gt;import graph&lt;/strong&gt;, 또는 &lt;strong&gt;의존성 그래프&lt;/strong&gt;라고 볼 수 있다.&lt;/p&gt;
&lt;p&gt;여기서 중요한 질문은 “이 패키지가 package.json의 어느 칸에 있는가?”가 아니다.&lt;/p&gt;
&lt;p&gt;번들러 입장에서는 다음 질문이 더 중요하다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;이 모듈이 실제 코드에서 import 되었는가?
external로 제외되어 있는가?
사용하지 않는 코드는 제거 가능한가?
별도 청크로 분리되는가?&lt;/code&gt;&lt;/pre&gt;&lt;br/&gt;

&lt;p&gt;따라서 번들 결과를 확인할 때는 관점을 나누어 봐야 한다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;보고 싶은 것&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;확인할 기준&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;이 패키지가 왜 필요한가?&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;dependencies&lt;/code&gt;, &lt;code&gt;devDependencies&lt;/code&gt;, &lt;code&gt;peerDependencies&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;이 패키지가 번들에 들어가는가?&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;import graph&lt;/code&gt;, &lt;code&gt;external&lt;/code&gt;, 번들러 설정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;코드 일부가 제거될 수 있는가?&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;tree-shaking&lt;/code&gt;, &lt;code&gt;sideEffects&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;어느 파일에 포함되는가?&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;code splitting&lt;/code&gt;, chunk 설정&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;h1&gt;4. 앱 프로젝트와 라이브러리 프로젝트는 기준이 다르다&lt;/h1&gt;
&lt;p&gt;의존성 분류는 앱 프로젝트와 라이브러리 프로젝트에서 조금 다르게 판단해야 한다.&lt;/p&gt;
&lt;p&gt;앱 프로젝트에서는 주로 다음 질문이 중요하다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;이 패키지가 앱 실행 중 필요한가?&lt;/code&gt;&lt;/pre&gt;&lt;br/&gt;

&lt;p&gt;반면 라이브러리 프로젝트에서는 질문이 하나 더 늘어난다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;이 패키지를 내 라이브러리 번들에 포함해야 할까?
아니면 사용하는 프로젝트가 제공하게 해야 할까?&lt;/code&gt;&lt;/pre&gt;&lt;br/&gt;


&lt;p&gt;즉, 앱 프로젝트는 실행 환경을 기준으로 판단하고,&lt;br&gt;라이브러리 프로젝트는 소비자 프로젝트와의 관계까지 함께 고려해야 한다.&lt;/p&gt;
&lt;br/&gt;

&lt;h2&gt;4-1. 앱 프로젝트의 기준&lt;/h2&gt;
&lt;p&gt;앱 프로젝트라면 기준은 비교적 단순하다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;실제 앱 실행 중 필요한가?
→ dependencies

개발, 빌드, 테스트, 린트에만 필요한가?
→ devDependencies&lt;/code&gt;&lt;/pre&gt;&lt;br/&gt;

&lt;p&gt;예를 들어 앱 코드에서 &lt;code&gt;axios&lt;/code&gt;를 사용한다면 &lt;code&gt;axios&lt;/code&gt;는 &lt;code&gt;dependencies&lt;/code&gt;에 두는 것이 자연스럽다.&lt;/p&gt;
&lt;p&gt;반대로 &lt;code&gt;vite&lt;/code&gt;, &lt;code&gt;typescript&lt;/code&gt;, &lt;code&gt;eslint&lt;/code&gt;, &lt;code&gt;vitest&lt;/code&gt;처럼 개발 과정에서만 필요한 도구는 &lt;code&gt;devDependencies&lt;/code&gt;에 둔다. 그리고 React나 Vue처럼 앱 자체를 실행하는 핵심 런타임도 앱 프로젝트에서는 &lt;code&gt;dependencies&lt;/code&gt;에 두어야 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &amp;quot;dependencies&amp;quot;: {
    &amp;quot;react&amp;quot;: &amp;quot;^19.0.0&amp;quot;,
    &amp;quot;react-dom&amp;quot;: &amp;quot;^19.0.0&amp;quot;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;앱 자체가 React 에서 실행되기 때문이다.&lt;/p&gt;
&lt;p&gt;다만 앞에서 본 것처럼 &lt;code&gt;dependencies&lt;/code&gt;에 둔다고 해서 자동으로 번들에 포함되는 것은 아니다.&lt;/p&gt;
&lt;p&gt;번들 포함 여부는 실제 코드의 &lt;code&gt;import&lt;/code&gt; 구조와 번들러 설정에 의해 결정된다.&lt;/p&gt;
&lt;br/&gt;

&lt;h2&gt;4-2. 라이브러리 프로젝트의 기준&lt;/h2&gt;
&lt;p&gt;라이브러리 프로젝트에서는 한 가지 질문을 더 해야 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;이 패키지를 내 라이브러리 내부에 포함할 것인가?
아니면 사용자 프로젝트가 제공하게 할 것인가?&lt;/code&gt;&lt;/pre&gt;&lt;br/&gt;

&lt;p&gt;예를 들어 React UI 라이브러리를 만든다면 React는 보통 사용자 앱과 공유해야 하는 핵심 런타임이다&lt;/p&gt;
&lt;p&gt;React를 라이브러리 번들에 함께 포함하면 사용자 앱의 React와 라이브러리 내부 React가 나뉠 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;사용자 앱
├─ react@19
└─ @my/ui
   └─ react@19&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이 예시는 같은 버전의 React를 사용하고 있지만, 번들 안에서 서로 다른 React 인스턴스로 포함되면 문제가 될 수 있다.&lt;/p&gt;
&lt;p&gt;React의 Hook 규칙 오류인 &lt;code&gt;Invalid hook call&lt;/code&gt;은 단순히 React 버전이 다를 때만 발생하는 문제가 아니다.&lt;/p&gt;
&lt;p&gt;같은 버전이라도 앱과 라이브러리가 동일한 React 인스턴스를 공유하지 않으면 문제가 발생할 수 있다.&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;물론 버전까지 다르다면 문제 가능성은 더 커진다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;사용자 앱
├─ react@18
└─ @my/ui
   └─ react@19&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;즉, 핵심은 “버전이 같은가?”만이 아니다.&lt;br&gt;앱과 라이브러리가 같은 React 인스턴스를 공유하고 있는지가 중요하다.&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;그래서 React UI 라이브러리에서는 보통 &lt;code&gt;react&lt;/code&gt;, &lt;code&gt;react-dom&lt;/code&gt;을 &lt;code&gt;peerDependencies&lt;/code&gt;로 선언한다.&lt;br&gt;추가로, 로컬 개발과 테스트를 위해 같은 패키지를 &lt;code&gt;devDependencies&lt;/code&gt;에도 둘 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &amp;quot;peerDependencies&amp;quot;: {
    &amp;quot;react&amp;quot;: &amp;quot;^18.0.0 || ^19.0.0&amp;quot;,
    &amp;quot;react-dom&amp;quot;: &amp;quot;^18.0.0 || ^19.0.0&amp;quot;
  },
  &amp;quot;devDependencies&amp;quot;: {
    &amp;quot;react&amp;quot;: &amp;quot;^19.0.0&amp;quot;,
    &amp;quot;react-dom&amp;quot;: &amp;quot;^19.0.0&amp;quot;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;마지막으로 실제 번들에서 React를 제외하려면 &lt;code&gt;external&lt;/code&gt; 설정이 필요하다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;export default {
  external: [&amp;quot;react&amp;quot;, &amp;quot;react-dom&amp;quot;]
};&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;라이브러리 프로젝트에서는 다음 기준으로 나눌 수 있다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구분&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;역할&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dependencies&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;라이브러리 내부 구현에 실제로 필요한 런타임 유틸&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;devDependencies&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;개발, 빌드, 테스트에 필요한 도구&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;peerDependencies&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;사용자 앱과 공유해야 하는 핵심 런타임&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;external&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;번들 결과물에서 제외해야 하는 패키지&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;h1&gt;5. 마치며&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;dependencies&lt;/code&gt;, &lt;code&gt;devDependencies&lt;/code&gt;, &lt;code&gt;peerDependencies&lt;/code&gt;는 이름 때문에 번들 결과와 직접 연결해서 생각하기 쉽다.&lt;/p&gt;
&lt;p&gt;하지만 이 세 분류는 &lt;strong&gt;무엇을 번들에 포함할지 결정하는 설정이 아니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;이들은 패키지가 프로젝트와 어떤 관계로 필요한지를 설명한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dependencies
→ 애플리케이션 실행에 필요한 패키지

devDependencies
→ 개발, 빌드, 테스트, 린트에 필요한 패키지

peerDependencies
→ 라이브러리 사용자가 직접 제공해야 하는 패키지&lt;/code&gt;&lt;/pre&gt;&lt;br/&gt;

&lt;p&gt;반면 실제 번들 결과는 의존성 분류가 아니라, 코드의 &lt;code&gt;import&lt;/code&gt; 구조와 번들러 설정에 의해 결정된다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;번들 포함 여부를 결정하는 것
├─ import graph
├─ external 설정
├─ tree-shaking
├─ code splitting
└─ bundler config&lt;/code&gt;&lt;/pre&gt;&lt;br/&gt;

&lt;p&gt;결국 핵심은 다음과 같다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;code&gt;dependencies&lt;/code&gt;, &lt;code&gt;devDependencies&lt;/code&gt;, &lt;code&gt;peerDependencies&lt;/code&gt;는&lt;br&gt;**번들 포함 여부를 결정하는 설정이 아니라, 패키지가 어떤 관계로 필요한지를 설명하는 선언이다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;특히 React UI 라이브러리나 디자인 시스템을 만든다면 이 차이를 명확히 이해해야 한다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;react&lt;/code&gt;를 &lt;code&gt;peerDependencies&lt;/code&gt;에 넣었다고 해서 자동으로 번들에서 제외되는 것은 아니다.&lt;/p&gt;
&lt;p&gt;React 중복 번들을 막으려면 &lt;code&gt;peerDependencies&lt;/code&gt; 선언과 함께 번들러의 &lt;code&gt;external&lt;/code&gt; 설정까지 확인해야 한다.&lt;/p&gt;
&lt;br/&gt;</description>
      <category>개발 기술/사소하지만 놓치기 쉬운 개발 지식</category>
      <category>dependencies</category>
      <category>devDependencies</category>
      <category>external</category>
      <category>package.json</category>
      <category>peerDependencies</category>
      <category>React 라이브러리</category>
      <category>라이브러리</category>
      <category>번들링</category>
      <category>의존성 관리</category>
      <category>프론트엔드 번들링</category>
      <author>GicoMomg</author>
      <guid isPermaLink="true">https://mong-blog.tistory.com/246</guid>
      <comments>https://mong-blog.tistory.com/entry/dependencies-devDependencies-peerDependencies%EB%8A%94-%EB%B2%88%EB%93%A4-%EA%B2%B0%EA%B3%BC%EB%A5%BC-%EA%B2%B0%EC%A0%95%ED%95%98%EC%A7%80-%EC%95%8A%EB%8A%94%EB%8B%A4#entry246comment</comments>
      <pubDate>Tue, 9 Jun 2026 12:50:46 +0900</pubDate>
    </item>
    <item>
      <title>[React] Google Translate가 바꾼 DOM, 왜 insertBefore에서 실패했을까?</title>
      <link>https://mong-blog.tistory.com/entry/React-Google-Translate%EA%B0%80-%EB%B0%94%EA%BE%BC-DOM-%EC%99%9C-insertBefore%EC%97%90%EC%84%9C-%EC%8B%A4%ED%8C%A8%ED%96%88%EC%9D%84%EA%B9%8C</link>
      <description>&lt;h1&gt;0. 들어가며&amp;hellip;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저의 Google Translate 기능을 켠 상태에서 Button을 클릭했더니, 예상하지 못한 React DOM 오류가 발생했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;515&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjTjms/dJMcaiQ14HQ/NsaDwyFr9GPi0OIKqGkPQk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjTjms/dJMcaiQ14HQ/NsaDwyFr9GPi0OIKqGkPQk/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjTjms/dJMcaiQ14HQ/NsaDwyFr9GPi0OIKqGkPQk/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/bjTjms/dJMcaiQ14HQ/NsaDwyFr9GPi0OIKqGkPQk/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;515&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;515&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제가 된 Button은 클릭 시 로딩 상태를 보여주는 컴포넌트였다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uhOu8/dJMcafUl4rt/KcLfd42RKiPKyXbjlvg3v1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uhOu8/dJMcafUl4rt/KcLfd42RKiPKyXbjlvg3v1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uhOu8/dJMcafUl4rt/KcLfd42RKiPKyXbjlvg3v1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuhOu8%2FdJMcafUl4rt%2FKcLfd42RKiPKyXbjlvg3v1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 상태에서는 &lt;code&gt;Upload File&lt;/code&gt; 텍스트를 보여주고,&lt;br /&gt;클릭하면 &lt;code&gt;isPending&lt;/code&gt; 상태가 &lt;code&gt;true&lt;/code&gt;로 바뀌면서 로딩 스피너와 &lt;code&gt;Loading...&lt;/code&gt; 텍스트를 보여준다.&lt;/p&gt;
&lt;pre class=&quot;clojure&quot;&gt;&lt;code&gt;{isPending &amp;amp;&amp;amp; &amp;lt;Spinner /&amp;gt;}
{isPending ? &quot;Loading...&quot; : &quot;Upload File&quot;}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 상황에서는 문제가 없었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 Chrome의 Google Translate 기능을 켠 상태에서 이 Button을 클릭하면, 화면 crash가 발생했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CCavI/dJMcaipYYge/K6RE5YtTYkwqMn1Kz5aBo0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CCavI/dJMcaipYYge/K6RE5YtTYkwqMn1Kz5aBo0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CCavI/dJMcaipYYge/K6RE5YtTYkwqMn1Kz5aBo0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCCavI%2FdJMcaipYYge%2FK6RE5YtTYkwqMn1Kz5aBo0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;NotFoundError: Failed to execute 'insertBefore' on 'Node'
The node before which the new node is to be inserted is not a child of this node.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 Button의 로딩 처리 문제처럼 보였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 원인을 따라가 보니 문제는 로딩 상태 자체가 아니라, Google Translate가 React가 렌더링한 DOM을 직접 변경한다는 점에 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React는 자신이 렌더링한 DOM 구조를 기준으로 다음 업데이트를 계산한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 중간에 Google Translate가 TextNode를 다른 DOM 구조로 교체하면, React가 기억하는 DOM과 실제 브라우저 DOM이 달라질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 Button의 loading 상태 전환 중 발생한 &lt;code&gt;insertBefore NotFoundError&lt;/code&gt;를 통해, React와 Google Translate가 왜 충돌할 수 있는지 살펴본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h1&gt;1. 문제 원인과 해결 방안 알아보기&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1) 재현 상황: 로딩 버튼과 Google Translate&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제가 발생한 조건을 정리하면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;1. Button 내부에 텍스트 노드가 렌더링되어 있다.

2. Chrome의 Google Translate 기능을 켠다.

3. 사용자가 Button을 클릭한다.

4. isPending 상태가 true로 바뀌면서 Spinner와 Loading 텍스트를 렌더링하려고 한다.

5. React가 DOM을 업데이트하는 과정에서 NotFoundError가 발생한다. &lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 문제는 단순히 Button에 로딩 상태가 있다는 점이 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 다음 두 조건이 함께 만족될 때 발생한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1. Google Translate가 Button 내부 TextNode를 편집한다.

2. 이후 React가 상태 변경에 따라 기존 TextNode를 기준으로 DOM을 삽입하거나 제거하려고 한다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 이 두 조건이 왜 &lt;code&gt;insertBefore NotFoundError&lt;/code&gt;로 이어지는지 살펴보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2) 문제 원인 살펴보기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(1) 브라우저 번역 기능은 TextNode를 교체할 수 있다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Google Translate 같은 브라우저 번역 기능은 텍스트를 직접 수정하거나,&lt;br /&gt;기존 TextNode를 제거한 뒤 새로운 DOM 노드로 교체한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;번역 기능을 켜기 전 Button의 DOM 구조는 단순하다.&lt;/p&gt;
&lt;pre class=&quot;apache&quot;&gt;&lt;code&gt;&amp;lt;button&amp;gt;
  &amp;lt;svg /&amp;gt;
  Upload File
&amp;lt;/button&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DOM 트리로 보면 다음과 같이, Button 내부에 아이콘을 표현하는 &lt;code&gt;svg&lt;/code&gt;와 텍스트 노드가 존재한다.&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;button
├── svg
└── TextNode(&quot;Upload File&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 Google Translate가 적용되면 실제 DOM 구조가 달라질 수 있다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;button&amp;gt;
  &amp;lt;svg /&amp;gt;
  &amp;lt;font&amp;gt;&amp;lt;font&amp;gt;파일 업로드&amp;lt;/font&amp;gt;&amp;lt;/font&amp;gt;
&amp;lt;/button&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DOM 트리로 보면 이렇게 바뀐다.&lt;/p&gt;
&lt;pre class=&quot;hsp&quot;&gt;&lt;code&gt;button
├── svg
└── font
     └── font
         └── TextNode(&quot;파일 업로드&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 번역 기능을 켜는 것만으로도 React가 렌더링한 실제 DOM 구조가 외부에서 변경된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 점은 기존 &lt;code&gt;TextNode(&quot;Upload File&quot;)&lt;/code&gt;이 그대로 유지된 상태에서 텍스트만 바뀌는 게 아니라는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Google Translate는 기존 TextNode를 제거하고, &lt;code&gt;font&lt;/code&gt; 태그를 포함한 새로운 DOM 구조로 교체한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(2) React가 기억하는 구조와 실제 DOM 구조가 달라진다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 이 시점부터 React가 기억하는 DOM 구조와 실제 브라우저 DOM 구조가 달라진다는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React는 처음 Button을 렌더링할 때 다음과 같은 구조를 만들었다고 생각한다.&lt;/p&gt;
&lt;pre class=&quot;apache&quot;&gt;&lt;code&gt;&amp;lt;button&amp;gt;
  &amp;lt;svg /&amp;gt;
  Upload File
&amp;lt;/button&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;!-- Dom 트리 --&amp;gt;
button
├── svg
└── TextNode(&quot;Upload File&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React는 이 TextNode를 자신이 만든 노드로 인식하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 Google Translate가 적용된 뒤 실제 브라우저 DOM은 다음처럼 바뀌어 있다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;!-- Dom 트리 --&amp;gt;
button
├── svg
└── font
     └── font
         └── TextNode(&quot;파일 업로드&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, React가 처음 만들었던 &lt;code&gt;TextNode(&quot;Upload File&quot;)&lt;/code&gt;은 더 이상 &lt;code&gt;button&lt;/code&gt;의 직접 자식이 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 React가 기억하는 구조와 번역 결과 구조가 달라진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(3) 문제 발생: NotFoundError가 발생한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Button의 &lt;code&gt;isPending&lt;/code&gt; 상태가 변경되면 React는 Button 내부를 다음과 같은 구조로 업데이트하려고 한다.&lt;/p&gt;
&lt;pre class=&quot;apache&quot;&gt;&lt;code&gt;&amp;lt;button&amp;gt;
 &amp;lt;Spinner /&amp;gt;
  Loading... 
&amp;lt;/button&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정에서 React는 기존 TextNode를 기준으로 새 노드를 삽입하거나, 기존 노드를 제거한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 로딩 스피너를 기존 텍스트 앞에 추가하기 위해 다음과 같은 DOM 조작이 수행될 수 있다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;button.insertBefore(spinnerNode, oldTextNode);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 점은 &lt;code&gt;insertBefore&lt;/code&gt;의 두 번째 인자인 &lt;code&gt;oldTextNode&lt;/code&gt;가 반드시 &lt;code&gt;button&lt;/code&gt;의 자식 노드여야 한다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;referenceNode&lt;/code&gt;는 &lt;code&gt;parentNode&lt;/code&gt;의 자식이어야 한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;parentNode.insertBefore(newNode, referenceNode);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 Google Translate가 적용된 뒤 실제 DOM은 이미 다음처럼 바뀌어 있다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;button&amp;gt;
  &amp;lt;svg /&amp;gt;
  &amp;lt;font&amp;gt;&amp;lt;font&amp;gt;파일 업로드&amp;lt;/font&amp;gt;&amp;lt;/font&amp;gt;
&amp;lt;/button&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, React가 기억하고 있던 &lt;code&gt;oldTextNode&lt;/code&gt;는 더 이상 &lt;code&gt;button&lt;/code&gt;의 직접 자식이 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 상태에서 React가 다음과 같은 DOM 조작을 시도하면, 브라우저 입장에서는 button 안에 spinnerNode를 삽입하려고 할 것이다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;button.insertBefore(spinnerNode, oldTextNode);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 기준점으로 전달된 oldTextNode는 button의 자식이 아니라, &lt;code&gt;NotFoundError&lt;/code&gt;가 발생한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/q58G0/dJMcafNCkyi/UgTcjzuCRFDrwKrMkwjhf1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/q58G0/dJMcafNCkyi/UgTcjzuCRFDrwKrMkwjhf1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/q58G0/dJMcafNCkyi/UgTcjzuCRFDrwKrMkwjhf1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fq58G0%2FdJMcafNCkyi%2FUgTcjzuCRFDrwKrMkwjhf1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;NotFoundError: Failed to execute 'insertBefore' on 'Node'
The node before which the new node is to be inserted is not a child of this node.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면, &lt;code&gt;NotFoundError&lt;/code&gt;는 React가 로딩 상태를 처리하지 못해서 발생한 문제가 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정확히는 React가 기준점으로 삼은 DOM 노드가 Google Translate에 의해 이미 사라졌기에 발생한 문제다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 흐름은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1. React가 Button 내부에 TextNode를 렌더링한다. 

2. Google Translate가 해당 TextNode를 다른 DOM 구조로 교체한다. 

3. React는 여전히 기존 TextNode가 존재한다고 기억한다. 

4. isPending 상태가 변경되며 React가 기존 TextNode 기준으로 DOM을 업데이트하려고 한다. 

5. 하지만 해당 TextNode는 더 이상 button의 자식이 아니므로 NotFoundError가 발생한다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3) 해결 방안: TextNode를 직접 업데이트 기준점으로 삼지 않게 한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;방향은 Google Translate로 변경될 수 있는 TextNode를 React가 업데이트 기준점으로 삼지 않게 하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞에서 살펴본 문제는 React가 기존 TextNode를 기준으로 &lt;code&gt;insertBefore&lt;/code&gt;를 수행하려고 할 때 발생했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 Google Translate가 적용된 뒤에는 &lt;code&gt;oldTextNode&lt;/code&gt;가 더 이상 &lt;code&gt;button&lt;/code&gt;의 자식이 아닐 수 있다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;button.insertBefore(spinnerNode, oldTextNode);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 상태가 변경될 때 React가 TextNode 단위로 업데이트하지 않고, 변경 영역을 wrapper로 감싸야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(1) 변경되는 children을 wrapper로 감싼다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 Button의 children을 렌더링하는 구조가 다음과 같다고 가정해보자.&lt;/p&gt;
&lt;pre class=&quot;openscad&quot;&gt;&lt;code&gt;&amp;lt;ButtonPrimitive&amp;gt;
  {(renderProps) =&amp;gt;
    typeof children === &quot;function&quot; ? children(renderProps) : children
  }
&amp;lt;/ButtonPrimitive&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조에서는 Button 내부의 아이콘과 텍스트가 직접 sibling으로 렌더링될 수 있다.&lt;/p&gt;
&lt;pre class=&quot;apache&quot;&gt;&lt;code&gt;&amp;lt;button&amp;gt;
  &amp;lt;svg /&amp;gt;
  Upload File
&amp;lt;/button&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 Google Translate가 TextNode를 교체하고,&lt;br /&gt;이후 React가 로딩 스피너를 삽입하면 앞서 본 것처럼 &lt;code&gt;NotFoundError&lt;/code&gt;가 발생할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이를 방지하기 위해 Button에서는 상태에 따라 바뀌는 children을 wrapper 안에 렌더링한다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;const hasPendingState = typeof isPending === &quot;boolean&quot;;

&amp;lt;ButtonPrimitive isPending={isPending}&amp;gt;
  {(renderProps) =&amp;gt; {
    // children이 함수라면 현재 Button 상태를 전달해 실제 렌더링 결과를 만든다.
    // 함수형 children이 아니라면 전달받은 children을 그대로 사용한다.
    const renderedChildren =
      typeof children === &quot;function&quot; ? children(renderProps) : children;

    // isPending을 사용하지 않는 Button은 기존 DOM 구조를 유지한다.
    // 불필요한 wrapper를 추가하지 않기 위함이다.
    if (!hasPendingState) {
      return renderedChildren;
    }

    return (
      &amp;lt;span
        key={renderProps.isPending ? &quot;pending&quot; : &quot;idle&quot;}
        data-slot=&quot;button-content&quot;
        style={{ display: &quot;contents&quot; }}
      &amp;gt;
        {renderedChildren}
      &amp;lt;/span&amp;gt;
    );
  }}
&amp;lt;/ButtonPrimitive&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드의 핵심은 모든 Button에 wrapper를 추가하는 것이 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;isPending&lt;/code&gt; 상태가 명시된 Button에 한해서만 동적인 children 영역을 wrapper로 감싼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 React가 Google Translate에 의해 교체된 TextNode를 직접 업데이트 기준점으로 삼는 대신, wrapper subtree 단위로 다시 렌더링할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(2) &lt;code&gt;key&lt;/code&gt;를 바꿔 subtree 단위로 다시 mount한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;wrapper만 추가하는 것으로는 충분하지 않을 수 있다. 중요한 부분은 &lt;code&gt;key&lt;/code&gt;다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;// pending 상태가 바뀌면 key도 함께 바뀐다.
// React가 기존 TextNode를 세밀하게 업데이트하지 않고,
// wrapper subtree를 새로 mount하도록 유도한다.

&amp;lt;span key={renderProps.isPending ? &quot;pending&quot; : &quot;idle&quot;} &amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;isPending&lt;/code&gt; 상태가 바뀔 때 wrapper의 &lt;code&gt;key&lt;/code&gt;도 함께 바뀌면,&lt;br /&gt;React는 기존 children 내부를 업데이트하기보다 wrapper subtree를 새로 mount하는 방향으로 동작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 업데이트 단위를 TextNode 수준이 아니라 wrapper subtree 수준으로 끌어올리는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;idle 상태에서는 다음과 같은 구조가 된다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;button&amp;gt;
  &amp;lt;span data-slot=&quot;button-content&quot; style=&quot;display: contents&quot;&amp;gt;
    &amp;lt;svg /&amp;gt;
    Upload File
  &amp;lt;/span&amp;gt;
&amp;lt;/button&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pending 상태에서는 wrapper의 &lt;code&gt;key&lt;/code&gt;가 변경되면서 다음 구조로 새로 렌더링된다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;button&amp;gt;
  &amp;lt;span data-slot=&quot;button-content&quot; style=&quot;display: contents&quot;&amp;gt;
    &amp;lt;Spinner /&amp;gt;
    Loading...
  &amp;lt;/span&amp;gt;
&amp;lt;/button&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 Google Translate가 내부 TextNode를 다른 DOM 구조로 바꾸더라도,&lt;br /&gt;React가 사라진 TextNode를 직접 기준점으로 삼아 &lt;code&gt;insertBefore&lt;/code&gt;나 &lt;code&gt;removeChild&lt;/code&gt;를 수행할 가능성을 줄일 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 React가 업데이트해야 하는 단위를 개별 TextNode가 아니라, 상태에 따라 교체되는 wrapper subtree로 바꾸는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(3) &lt;code&gt;display: contents&lt;/code&gt;로 레이아웃 영향을 줄인다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;wrapper를 추가하면 React가 추적할 수 있는 DOM 경계를 만들 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 Button처럼 내부 아이콘과 텍스트의 배치가 중요한 컴포넌트에서는 wrapper가 예상하지 못한 레이아웃 변화를 일으킬 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 Button 내부가 &lt;code&gt;flex&lt;/code&gt;와 &lt;code&gt;gap&lt;/code&gt;을 사용하고 있다면, 중간에 &lt;code&gt;&amp;lt;span&amp;gt;&lt;/code&gt; wrapper가 생기는 것만으로도 기존 아이콘과 텍스트의 배치가 달라질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 wrapper에는 다음 스타일을 적용했다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;style={{ display: &quot;contents&quot; }}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;display: contents&lt;/code&gt;를 사용하면 DOM에는 wrapper가 존재하지만,&lt;br /&gt;레이아웃 계산에서는 wrapper 박스가 없는 것처럼 동작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 모든 Button에 wrapper를 추가할 필요는 없다.&lt;/p&gt;
&lt;pre class=&quot;gauss&quot;&gt;&lt;code&gt;if (!hasPendingState) {
  return renderedChildren;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;isPending&lt;/code&gt;을 사용하지 않는 일반 Button은 기존 렌더링 구조를 그대로 유지하고,&lt;br /&gt;문제가 발생할 가능성이 높은 경우에만 방어 로직을 적용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h1&gt;2. 구현할 때 주의해야 할 패턴&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제는 특정 Button 컴포넌트에서만 발생하는 것이 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React가 관리하는 TextNode를 외부 기능이 변경한 뒤, React가 그 TextNode를 기준으로 DOM을 업데이트하려 하면 비슷한 문제가 발생할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(1) TextNode와 sibling element가 함께 바뀌는 패턴을 주의해야 한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 상태에 따라 TextNode와 sibling element를 함께 삽입하거나 제거하는 패턴은 주의해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음과 같은 코드는 브라우저 번역 기능과 충돌할 가능성이 있다.&lt;/p&gt;
&lt;pre class=&quot;clojure&quot;&gt;&lt;code&gt;{isLoading &amp;amp;&amp;amp; &amp;lt;Spinner /&amp;gt;}
{isLoading ? &quot;Loading...&quot; : &quot;Submit&quot;}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 로딩 상태에 따라 &lt;code&gt;Spinner&lt;/code&gt;를 추가하고, 동시에 텍스트도 변경한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React가 기대하는 구조는 다음처럼 바뀐다.&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;idle 상태

button
└── TextNode(&quot;Submit&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;loading 상태

button
├── Spinner
└── TextNode(&quot;Loading...&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 Google Translate가 &lt;code&gt;TextNode(&quot;Submit&quot;)&lt;/code&gt;을 다른 DOM 구조로 교체한 뒤라면,&lt;br /&gt;React가 기존 TextNode를 기준으로 &lt;code&gt;Spinner&lt;/code&gt;를 삽입하는 과정에서 문제가 발생할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음처럼 텍스트만 조건부로 바꾸는 코드도 주의가 필요하다.&lt;/p&gt;
&lt;pre class=&quot;clojure&quot;&gt;&lt;code&gt;{isOpen ? &quot;닫기&quot; : &quot;열기&quot;}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 sibling element가 없더라도, React가 기존 TextNode를 갱신하거나 교체하는 과정에서 외부 DOM 변경과 충돌할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;children 자체를 조건부로 렌더링하는 패턴도 조심해야 한다.&lt;/p&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;{condition &amp;amp;&amp;amp; children}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;children&lt;/code&gt; 안에 텍스트 노드가 포함되어 있고, 그 영역이 브라우저 번역 기능에 의해 수정된 뒤 조건이 바뀌면 React가 기대한 DOM과 실제 DOM이 달라질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 조건부 렌더링 자체가 문제인 것은 아니며, 다음 조건이 함께 만족될 때 발생할 가능성이 높다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1. 문자열 TextNode가 직접 렌더링된다.

2. 해당 TextNode 주변에서 상태 기반 element 삽입/삭제가 일어난다.

3. Google Translate 같은 외부 기능이 TextNode를 다른 DOM 구조로 교체한다.

4. 이후 React가 기존 TextNode를 기준으로 DOM을 업데이트하려고 한다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 핵심은 조건부 렌더링 그 자체가 아니라, 외부 DOM mutation 이후&lt;br /&gt;React가 이미 사라진 TextNode를 업데이트 기준점으로 삼는 상황이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(2) UI 라이브러리 컴포넌트라면 더 보수적으로 확인해야 한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 UI 라이브러리 컴포넌트라면 더 보수적으로 접근하는 편이 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반 서비스 화면에서는 특정 조건에서만 문제가 발생할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 Button, Menu, Select, Tooltip, Dialog 같은 공통 컴포넌트는 다양한 서비스와 브라우저 환경에서 폭넓게 사용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 브라우저 번역 기능을 켜거나, 확장 프로그램이 DOM을 수정하는 상황도 충분히 발생할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 동적 상태가 자주 바뀌는 컴포넌트라면 다음 원칙을 고려할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;1. 상태 전환으로 children의 sibling 구조를 크게 바꾸지 않는다.

2. 필요한 경우 안정적인 wrapper를 둔다.

3. 로딩, 선택, 토글 상태처럼 자주 바뀌는 영역은 wrapper 단위로 교체한다.

4. wrapper가 레이아웃에 영향을 준다면 `display: contents` 사용을 검토한다.

5. `isPending`, `isLoading` 같은 상태 prop을 사용하는 컴포넌트는 브라우저 번역 환경에서도 확인한다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 건 React가 외부에서 변경된 TextNode를 직접 업데이트 기준점으로 삼지 않게 만드는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상태 전환이 잦은 영역에는 React가 안정적으로 추적할 수 있는 경계를 의도적으로 만들어두는 편이 더 안전하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h1&gt;3. 마치며&amp;hellip;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 문제는 처음 보면 단순한 React DOM 오류처럼 보인다.&lt;/p&gt;
&lt;pre class=&quot;smali&quot;&gt;&lt;code&gt;NotFoundError: Failed to execute 'insertBefore' on 'Node'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 실제 원인은 React 내부 로직 자체가 아니라, React가 관리하던 DOM을&lt;br /&gt;Google Translate가 외부에서 직접 변경했다는 데 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React는 자신이 렌더링한 DOM 구조를 기준으로 다음 업데이트를 계산한다.&lt;br /&gt;그런데 외부 기능이 그 DOM을 제거하거나 다른 구조로 교체하면, React가 기억하는 DOM과&lt;br /&gt;실제 브라우저 DOM이 달라질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 상태에서 React가 기존 TextNode를 기준으로 &lt;code&gt;insertBefore&lt;/code&gt;나 &lt;code&gt;removeChild&lt;/code&gt;를 수행하면&lt;br /&gt;오류가 발생할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 사례에서는 &lt;code&gt;isPending&lt;/code&gt; 상태가 있는 Button children을 wrapper로 감싸고,&lt;br /&gt;pending 상태에 따라 &lt;code&gt;key&lt;/code&gt;를 바꿔 subtree 단위로 교체하도록 수정했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 다음 한 문장으로 정리할 수 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  React가 번역 기능에 의해 교체된 TextNode를 직접 DOM 조작의 기준점으로 사용하지 않게 한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저 번역 기능, 확장 프로그램, 외부 스크립트처럼 React 바깥에서 DOM을 수정하는 요소는 언제든 존재할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 상태 전환이 잦고 TextNode와 element 구조가 함께 바뀌는 컴포넌트라면,&lt;br /&gt;React가 안정적으로 추적할 수 있는 DOM 경계를 의도적으로 만들어두는 것이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;</description>
      <category>개발 기술/사소하지만 놓치기 쉬운 개발 지식</category>
      <category>Browser Translation</category>
      <category>Button Component</category>
      <category>DOM Mutation</category>
      <category>Google Translate</category>
      <category>insertBefore NotFoundError</category>
      <category>NotFoundError</category>
      <category>react</category>
      <category>React DOM</category>
      <category>removeChild</category>
      <category>removeChild NotFoundError</category>
      <author>GicoMomg</author>
      <guid isPermaLink="true">https://mong-blog.tistory.com/245</guid>
      <comments>https://mong-blog.tistory.com/entry/React-Google-Translate%EA%B0%80-%EB%B0%94%EA%BE%BC-DOM-%EC%99%9C-insertBefore%EC%97%90%EC%84%9C-%EC%8B%A4%ED%8C%A8%ED%96%88%EC%9D%84%EA%B9%8C#entry245comment</comments>
      <pubDate>Sun, 31 May 2026 18:03:02 +0900</pubDate>
    </item>
    <item>
      <title>컴포넌트 프리뷰는 어떻게 실행될까? iframe으로 만들어본 프리뷰 런타임</title>
      <link>https://mong-blog.tistory.com/entry/%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%ED%94%84%EB%A6%AC%EB%B7%B0%EB%8A%94-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%8B%A4%ED%96%89%EB%90%A0%EA%B9%8C-iframe%EC%9C%BC%EB%A1%9C-%EB%A7%8C%EB%93%A4%EC%96%B4%EB%B3%B8-%ED%94%84%EB%A6%AC%EB%B7%B0-%EB%9F%B0%ED%83%80%EC%9E%84</link>
      <description>&lt;h1&gt;0. 들어가며&lt;/h1&gt;
&lt;p&gt;디자인 시스템이나 공통 UI를 작업하다 보면 자연스럽게 컴포넌트 문서를 만들게 된다.&lt;/p&gt;
&lt;p&gt;처음에는 컴포넌트 이름, 사용 예시, &lt;code&gt;props&lt;/code&gt; 목록 정도만 정리해도 충분해 보인다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;Button variant=&amp;quot;primary&amp;quot; disabled={false}&amp;gt;
  CTA
&amp;lt;/Button&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;하지만 실제로 컴포넌트를 사용하는 입장에서 궁금한 것은 단순히 “어떤 &lt;code&gt;props&lt;/code&gt;가 있는가”가 아니다.&lt;/p&gt;
&lt;p&gt;오히려 더 자주 확인하고 싶은 것은 &lt;strong&gt;그 값을 바꿨을 때 실제 화면에서 어떻게 동작하는가&lt;/strong&gt;이다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;variant를 바꾸면 실제로 어떻게 보이는지&lt;/li&gt;
&lt;li&gt;disabled 상태에서는 어떤 인터랙션이 막히는지&lt;/li&gt;
&lt;li&gt;children이 길어졌을 때 레이아웃이 어떻게 변하는지&lt;/li&gt;
&lt;li&gt;모바일 너비에서는 어떻게 보이는지&lt;/li&gt;
&lt;li&gt;클릭했을 때 이벤트가 제대로 발생하는지&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;p&gt;즉, 컴포넌트 문서는 단순히 읽는 정보에 머물면 부족하다.&lt;br&gt;컴포넌트는 결국 화면에서 동작하는 UI이기에, 문서 역시 실제 동작을 확인할 수 있는 형태에 가까워야 한다.&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;p&gt;이번에 만들었던 도구의 출발점도 여기에 있었다.&lt;br&gt;망 분리 환경에서도 Storybook처럼 컴포넌트를 문서화하고, 그 자리에서 바로 동작까지 확인할 수 있는 화면이 필요했다.&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;p&gt;다만 목표는 Storybook 자체를 그대로 다시 만드는 것이 아니었다.&lt;br&gt;필요했던 것은 현재 코드베이스 안에서 실행할 수 있고, 컴포넌트 문서를 프리뷰와 연결할 수 있는 환경이었다.&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;p&gt;이번 시간에는 &lt;code&gt;iframe&lt;/code&gt;을 이용해 컴포넌트 프리뷰를 만들면서, 문서 UI와 프리뷰 실행 환경을 어떻게 분리했는지 정리해보려 한다. (&lt;a href=&quot;https://github.com/KumJungMin/react-style-guide-sheet&quot;&gt;작업 코드&lt;/a&gt;)&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/u4DgT/dJMcadPnzbJ/XF0fSOK1ZyvpdFtUCISHS1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/u4DgT/dJMcadPnzbJ/XF0fSOK1ZyvpdFtUCISHS1/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/u4DgT/dJMcadPnzbJ/XF0fSOK1ZyvpdFtUCISHS1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/u4DgT/dJMcadPnzbJ/XF0fSOK1ZyvpdFtUCISHS1/img.gif&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;h1&gt;1. 프리뷰 런타임을 어떻게 만들까?&lt;/h1&gt;
&lt;h2&gt;1) 필요한 것은 문서 사이트가 아니라 실행되는 프리뷰였다&lt;/h2&gt;
&lt;p&gt;이 도구에서 필요했던 것은 단순한 문서 사이트가 아니었다.&lt;/p&gt;
&lt;p&gt;컴포넌트 이름, 설명, props 목록을 보여주는 것만으로는 부족했다.&lt;br&gt;문서에 적힌 값을 직접 바꿔보고, 그 결과가 실제 컴포넌트 화면에 바로 반영되는 구조가 필요했다.&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;예를 들어 버튼 컴포넌트를 문서화한다고 해보자.&lt;/p&gt;
&lt;p&gt;문서에는 버튼의 종류, 비활성화 여부, 버튼 안에 들어갈 문구, 클릭 이벤트 같은 정보가 들어간다.&lt;br&gt;그리고 사용자는 이 값을 화면에서 바꿔보면서 다음 결과를 확인할 수 있어야 한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;버튼의 variant를 바꾸면 실제 모양이 바뀐다.&lt;/li&gt;
&lt;li&gt;버튼 안의 문구를 바꾸면 프리뷰에도 반영된다.&lt;/li&gt;
&lt;li&gt;버튼을 클릭하면 이벤트가 발생했는지 로그로 확인할 수 있다.&lt;/li&gt;
&lt;li&gt;모바일 / 태블릿 / 데스크톱 너비에서 어떻게 보이는지도 확인할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;p&gt;구조를 단순화하면 아래와 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OlHHi/dJMcagMbRZy/u3c4BWwJpYB5YZznNek8xK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OlHHi/dJMcagMbRZy/u3c4BWwJpYB5YZznNek8xK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OlHHi/dJMcagMbRZy/u3c4BWwJpYB5YZznNek8xK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOlHHi%2FdJMcagMbRZy%2Fu3c4BWwJpYB5YZznNek8xK%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;사용자가 컴포넌트 문서를 정의한다.&lt;/li&gt;
&lt;li&gt;문서에 정의된 &lt;code&gt;props&lt;/code&gt;, &lt;code&gt;children&lt;/code&gt;, &lt;code&gt;event&lt;/code&gt; 정보는 프리뷰 런타임의 입력값이 된다.&lt;/li&gt;
&lt;li&gt;프리뷰 런타임은 이 입력값을 바탕으로 실제 컴포넌트를 실행한다.&lt;/li&gt;
&lt;li&gt;사용자는 화면에서 렌더링 결과를 확인하고, 클릭 같은 이벤트가 발생했을 때 이벤트 로그까지 함께 확인할 수 있다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;p&gt;여기서 중요한 점은 컴포넌트 문서가 단순한 설명서로 끝나지 않는다는 것이다.&lt;br&gt;문서에 적힌 값이 실제 프리뷰를 실행하기 위한 입력값이 된다.&lt;/p&gt;
&lt;p&gt;예를 들어 버튼 문서는 다음처럼 정의할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;export default defineComponentDoc({
  title: &amp;#39;Button&amp;#39;,
  component: Button,
  props: [
    {
      name: &amp;#39;variant&amp;#39;,
      type: &amp;quot;&amp;#39;primary&amp;#39; | &amp;#39;secondary&amp;#39; | &amp;#39;danger&amp;#39; | &amp;#39;ghost&amp;#39;&amp;quot;,
      default: &amp;#39;primary&amp;#39;,
      control: &amp;#39;select&amp;#39;,
      options: [&amp;#39;primary&amp;#39;, &amp;#39;secondary&amp;#39;, &amp;#39;danger&amp;#39;, &amp;#39;ghost&amp;#39;],
    },
    {
      name: &amp;#39;disabled&amp;#39;,
      type: &amp;#39;boolean&amp;#39;,
      default: false,
    },
  ],
  events: [{ name: &amp;#39;onClick&amp;#39;, payload: &amp;#39;void&amp;#39; }],
  composition: {
    entries: [{ name: &amp;#39;default&amp;#39; }],
  },
  compositionExamples: { default: &amp;#39;CTA&amp;#39; },
})&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;위 정의에는 프리뷰를 실행하기 위한 정보가 함께 들어 있다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;필드&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;역할&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;code&gt;component&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;실제 프리뷰에 렌더링할 컴포넌트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;props&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;사용자가 조작할 수 있는 속성 정의&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;events&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;프리뷰에서 감지할 이벤트 정의&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;composition&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;children&lt;/code&gt;처럼 조합 가능한 영역 정의&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;compositionExamples&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;기본으로 보여줄 조합 예시&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;문서 UI는 이 정의를 읽고, 사용자가 조작할 수 있는 상태를 준비한다.&lt;/p&gt;
&lt;p&gt;예를 들어 &lt;code&gt;variant&lt;/code&gt;는 선택 박스가 되고, &lt;code&gt;disabled&lt;/code&gt;는 체크박스가 될 수 있다.&lt;br&gt;&lt;code&gt;compositionExamples.default&lt;/code&gt;에 적힌 CTA는 버튼 안에 들어갈 기본 문구가 된다.&lt;/p&gt;
&lt;p&gt;즉, 문서에 적힌 값이 사용자가 바꿔볼 수 있는 프리뷰 상태로 바뀐다.&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;이제 이 화면을 역할 기준으로 나눠보면 두 영역으로 정리할 수 있다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;영역&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;책임&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Host UI&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;문서를 보여주고, 사용자가 값을 바꿀 수 있는 조작 영역&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Preview Runtime&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Host UI에서 만든 값을 받아 실제 컴포넌트를 보여주는 실행 영역&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;br/&gt;

&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;60%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/unNpQ/dJMcacC08nc/zEwh9onnubXe1QP1hnd1kk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/unNpQ/dJMcacC08nc/zEwh9onnubXe1QP1hnd1kk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/unNpQ/dJMcacC08nc/zEwh9onnubXe1QP1hnd1kk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FunNpQ%2FdJMcacC08nc%2FzEwh9onnubXe1QP1hnd1kk%2Fimg.png&quot; width=&quot;60%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;다시 말해 Host UI는 “값을 만드는 곳”이고, Preview Runtime은 “그 값으로 컴포넌트를 실행하는 곳”이다.&lt;/p&gt;
&lt;p&gt;이렇게 나누고 나면 다음 질문이 생긴다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;그럼 만들어진 값을 Preview Runtime에서 어떻게 실행해야 할까?&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;h2&gt;2) iframe을 사용하는 프리뷰 런타임 만들기&lt;/h2&gt;
&lt;h3&gt;(1) 어떻게 프리뷰를 실행해야할까?&lt;/h3&gt;
&lt;p&gt;가장 단순한 방법은 현재 페이지의 DOM 안에 프리뷰 컴포넌트를 그대로 렌더하는 것이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;export function PreviewPanel({
  Component,
  props,
}: {
  Component: React.ComponentType&amp;lt;any&amp;gt;
  props: Record&amp;lt;string, unknown&amp;gt;
}) {
  return (
    &amp;lt;div className=&amp;quot;preview-panel&amp;quot;&amp;gt;
      &amp;lt;Component {...props} /&amp;gt;
    &amp;lt;/div&amp;gt;
  )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 방법은 문서 화면 안에 프리뷰 영역을 만들고, 선택된 컴포넌트를 바로 렌더하면 된다. &lt;/p&gt;
&lt;p&gt;하지만 여기서부터 문제가 생긴다.&lt;/p&gt;
&lt;p&gt;문서 UI와 프리뷰 컴포넌트가 같은 DOM과 CSS 문맥을 공유하면, 전역 스타일이 프리뷰에 영향을 줄 수 있기 때문이다.&lt;/p&gt;
&lt;p&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;p&gt;첫 번째 문제는 css 충돌이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;/* 문서 UI 전역 스타일 */
button {
  font-size: 12px;
  border-radius: 0;
}

/* 프리뷰 대상 컴포넌트 스타일 */
button {
  font-size: 14px;
  border-radius: 999px;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;예를 들어 문서 UI와 프리뷰 컴포넌트가 같은 DOM 문맥에 있다면, 위 두 스타일은 같은 &lt;code&gt;button&lt;/code&gt; 요소를 대상으로 경쟁하게 된다.&lt;/p&gt;
&lt;p&gt;이 경우 지금 보이는 버튼이 컴포넌트가 의도한 모습인지, 문서 UI의 전역 스타일이 덮어쓴 결과인지, 혹은 reset CSS나 utility class의 영향인지 알기 어렵다.&lt;/p&gt;
&lt;p&gt;문서가 컴포넌트의 실제 모습을 설명해야 하는데, 오히려 문서 화면 자체가 프리뷰 결과를 왜곡할 수 있는 것이다.&lt;/p&gt;
&lt;p&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;p&gt;두 번째 문제는 레이아웃 간섭이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.preview-panel {
  display: flex;
  align-items: center;
  overflow: hidden;
  font-size: 13px;
  line-height: 1;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;부모 컨테이너(.preview-panel) 안에서 프리뷰를 렌더하면, 더 이상 “컴포넌트가 독립적으로 실행된 모습”이라고 볼 수 없다.&lt;/p&gt;
&lt;p&gt;이미 부모의 &lt;code&gt;display&lt;/code&gt;, &lt;code&gt;overflow&lt;/code&gt;, &lt;code&gt;font-size&lt;/code&gt;, &lt;code&gt;line-height&lt;/code&gt;에 영향을 받았기 때문이다.&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;결국 문제는 단순히 “프리뷰를 어디에 렌더할까?”가 아니었다.&lt;/p&gt;
&lt;p&gt;더 정확히는 문서 UI와 프리뷰 컴포넌트가 같은 DOM/CSS 문맥을 공유해도 되는가의 문제이다.&lt;/p&gt;
&lt;br/&gt;

&lt;h3&gt;(2) iframe은 DOM과 CSS 문맥을 분리해준다&lt;/h3&gt;
&lt;p&gt;그래서 선택한 방법이 &lt;code&gt;iframe&lt;/code&gt;이었다. &lt;code&gt;iframe&lt;/code&gt;은 부모 문서와 독립된 &lt;code&gt;document&lt;/code&gt;를 가진다.&lt;/p&gt;
&lt;p&gt;즉, 프리뷰를 부모 페이지의 &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; 안에 렌더하는 대신, &lt;code&gt;iframe.contentDocument&lt;/code&gt; 안의 별도 문서에 렌더할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const targetDocument = iframe.contentDocument&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이렇게 되면 프리뷰는 부모 문서의 DOM tree에 직접 섞이지 않는다.&lt;/p&gt;
&lt;br/&gt;


&lt;p&gt;구조를 단순화하면 아래와 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bm6j5J/dJMb990BK2G/ldipx4eKd9laPCQMcZlcDk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bm6j5J/dJMb990BK2G/ldipx4eKd9laPCQMcZlcDk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bm6j5J/dJMb990BK2G/ldipx4eKd9laPCQMcZlcDk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbm6j5J%2FdJMb990BK2G%2Fldipx4eKd9laPCQMcZlcDk%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;문서 UI와 프리뷰 UI는 한 화면처럼 보이지만, 같은 DOM 문맥을 공유하지 않는다.&lt;/p&gt;
&lt;p&gt;Host UI가 &lt;code&gt;props&lt;/code&gt;, &lt;code&gt;children&lt;/code&gt;, &lt;code&gt;viewport&lt;/code&gt; 상태를 관리하고, Preview Runtime은 그 상태를 &lt;code&gt;iframe&lt;/code&gt; 내부 문서에서 실행한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;Host UI
= 문서를 보여주고 상태를 조작한다.

iframe Preview Runtime
= 전달받은 상태를 별도 document 안에서 실행한다.&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;다만 &lt;code&gt;iframe&lt;/code&gt;을 사용한다고 해서 자동으로 컴포넌트를 렌더할 공간이 준비되는 것은 아니다.&lt;/p&gt;
&lt;p&gt;프리뷰 런타임이 동작하려면 &lt;code&gt;iframe&lt;/code&gt; 내부 문서 안에 실제 컴포넌트가 마운트될 지점이 필요하다.&lt;/p&gt;
&lt;br/&gt;

&lt;h3&gt;(3) iframe 내부에 컴포넌트가 렌더링될 공간을 준비한다&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;iframe&lt;/code&gt;을 실행 경계로 쓰려면 먼저 &lt;code&gt;iframe&lt;/code&gt; 내부 문서에 프리뷰용 HTML 구조를 준비해야 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;export function bootstrapPreviewDocument(targetDocument: Document) {
  // iframe 내부 document를 연다.
  targetDocument.open() 

  // React가 마운트될 #app을 만든다.
  targetDocument.write(`
    &amp;lt;!DOCTYPE html&amp;gt;
    &amp;lt;html&amp;gt;
      &amp;lt;head&amp;gt;&amp;lt;/head&amp;gt;
      &amp;lt;body&amp;gt;
        &amp;lt;div id=&amp;quot;app&amp;quot;&amp;gt;&amp;lt;/div&amp;gt; 
      &amp;lt;/body&amp;gt;
    &amp;lt;/html&amp;gt;
  `)

  // document를 닫는다.
  targetDocument.close()
}&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;여기서 핵심은 &lt;code&gt;#app&lt;/code&gt;이다.&lt;/p&gt;
&lt;p&gt;React는 결국 특정 DOM 노드에 컴포넌트를 렌더링한다.&lt;br&gt;따라서 &lt;code&gt;iframe&lt;/code&gt; 내부 문서에도 React가 마운트될 DOM 노드가 필요하다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const targetDocument = iframe.contentDocument

bootstrapPreviewDocument(targetDocument)

const mountTarget = targetDocument.getElementById(&amp;#39;app&amp;#39;)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이렇게 하면 프리뷰 컴포넌트는 부모 문서의 특정 &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt;가 아니라, &lt;code&gt;iframe&lt;/code&gt; 내부 문서에 만들어진 &lt;code&gt;#app&lt;/code&gt;에 마운트된다.&lt;/p&gt;
&lt;p&gt;즉, 프리뷰는 부모 DOM 안에 섞이는 것이 아니라, &lt;code&gt;iframe&lt;/code&gt; 내부에 준비된 별도의 실행 공간에서 렌더링된다.&lt;/p&gt;
&lt;br/&gt;

&lt;h3&gt;(4) 필요한 스타일만 의도적으로 주입한다&lt;/h3&gt;
&lt;p&gt;다만 &lt;code&gt;iframe&lt;/code&gt;을 사용하면 한 가지 문제가 생긴다.&lt;/p&gt;
&lt;p&gt;문서가 분리되기에 부모 페이지의 스타일이 자동으로 적용되지 않는다. 이건 장점이자 단점이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;- 장점: 문서 UI의 전역 스타일이 프리뷰에 섞이지 않는다.

- 단점: 실제 앱에서 필요한 스타일도 자동으로 적용되지 않는다.&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;그래서 필요한 스타일은 의도적으로 &lt;code&gt;iframe&lt;/code&gt; 내부에 복제해야 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;export function cloneAndInjectParentStyles(
  targetDocument: Document,
  sourceDocument: Document = document
) {
  const parentStyles = Array.from(
    sourceDocument.querySelectorAll(
      &amp;#39;link[rel=&amp;quot;stylesheet&amp;quot;], style:not([scoped])&amp;#39;
    )
  )

  parentStyles.forEach(node =&amp;gt; {
    targetDocument.head.appendChild(node.cloneNode(true))
  })
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위 함수는 부모 문서의 stylesheet와 style 태그를 찾아 &lt;code&gt;iframe&lt;/code&gt; 내부 문서의 &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;에 복제한다.&lt;/p&gt;
&lt;p&gt;여기서 중요한 점은 “스타일을 그냥 공유한다”가 아니라 필요한 스타일을 의도적으로 다시 주입한다는 것이다.&lt;/p&gt;
&lt;br/&gt;

&lt;h3&gt;(5) viewport 시뮬레이션이 가능하게 구성한다&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;iframe&lt;/code&gt;을 선택하면서 얻은 또 다른 장점은 viewport 시뮬레이션이었다.&lt;/p&gt;
&lt;p&gt;프리뷰 영역의 너비를 바꿔 모바일, 태블릿, 데스크톱 상태를 확인하려면 여러 방법이 있다.&lt;br&gt;단순히 부모 컨테이너의 width를 줄일 수도 있다.&lt;/p&gt;
&lt;p&gt;하지만 &lt;code&gt;iframe&lt;/code&gt;을 사용하면 &lt;code&gt;iframe&lt;/code&gt; 자체의 width를 바꿔 독립된 화면처럼 다룰 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;iframe
  ref={iframeRef}
  style={{
    width: `${previewWidth}px`,
    height: &amp;#39;500px&amp;#39;,
  }}
/&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;p&gt;이 방식은 반응형 컴포넌트를 확인할 때 직관적이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/chjcIX/dJMcaf0LpZF/d2IqQKQjToaEWIv7QUrFC1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/chjcIX/dJMcaf0LpZF/d2IqQKQjToaEWIv7QUrFC1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/chjcIX/dJMcaf0LpZF/d2IqQKQjToaEWIv7QUrFC1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FchjcIX%2FdJMcaf0LpZF%2Fd2IqQKQjToaEWIv7QUrFC1%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;물론 이것이 실제 브라우저 viewport와 완전히 같다는 뜻은 아니다.&lt;br&gt;하지만 내부 컴포넌트 프리뷰 도구에서 레이아웃 반응을 확인하기에는 충분히 예측 가능한 방식이었다.&lt;/p&gt;
&lt;br/&gt;

&lt;h3&gt;(6) 변경 시에는 다시 그리고, 이벤트는 위로 올린다&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;props&lt;/code&gt;와 &lt;code&gt;children&lt;/code&gt; 변경시에는 &lt;code&gt;iframe&lt;/code&gt; 내부 프리뷰를 다시 그리도록 구성했다.&lt;/p&gt;
&lt;p&gt;그리고 &lt;code&gt;iframe&lt;/code&gt; 안에서 발생한 이벤트는 주입한 핸들러를 통해 다시 바깥으로 올린다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;60%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ct4yxn/dJMcagrQ2Tz/NIk2uGIGrdXjGVJY5QbL5k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ct4yxn/dJMcagrQ2Tz/NIk2uGIGrdXjGVJY5QbL5k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ct4yxn/dJMcagrQ2Tz/NIk2uGIGrdXjGVJY5QbL5k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fct4yxn%2FdJMcagrQ2Tz%2FNIk2uGIGrdXjGVJY5QbL5k%2Fimg.png&quot; width=&quot;60%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;즉, 데이터 흐름은 다음과 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;props / children 변경
→ Host UI 상태 갱신
→ iframe 내부 프리뷰 재실행

event 발생
→ 주입된 핸들러 실행
→ Host UI로 이벤트 로그 전달&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;h3&gt;(7) 정리하면&lt;/h3&gt;
&lt;p&gt;핵심은 Host UI와 Preview Runtime의 실행 문맥을 분리하는 것이었다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;- Host UI
  : 문서를 보여주고 상태를 조작한다.

- Preview Runtime
  : 전달받은 상태로 컴포넌트를 실행한다.
  : iframe은 두 영역 사이의 DOM/CSS 경계를 만든다.
  : 필요한 스타일은 iframe 내부로 명시적으로 주입한다.
  : 컴포넌트는 iframe 내부의 mount target에 렌더링한다.&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;정리하면 &lt;code&gt;iframe&lt;/code&gt;은 단순히 프리뷰를 보여주는 태그가 아니었다.&lt;/p&gt;
&lt;p&gt;문서 UI와 프리뷰 실행 환경을 분리하고, &lt;/p&gt;
&lt;p&gt;필요한 스타일만 다시 주입하고, &lt;/p&gt;
&lt;p&gt;전달받은 상태를 기준으로 컴포넌트를 실행하는 &lt;strong&gt;런타임 경계&lt;/strong&gt;였다.&lt;/p&gt;
&lt;p&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;h1&gt;2. 마치며…&lt;/h1&gt;
&lt;p&gt;이번 도구를 만들면서 가장 크게 배운 것은 &lt;code&gt;iframe&lt;/code&gt; 사용법 자체가 아니었다.&lt;/p&gt;
&lt;p&gt;처음에는 단순히 “망 분리 환경에서도 Storybook처럼 컴포넌트를 확인할 수 있는 화면을 만들자”에 가까웠다.&lt;/p&gt;
&lt;p&gt;하지만 직접 구현해보니, 컴포넌트 프리뷰 도구는 생각보다 많은 일을 하고 있었다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;- 문서에서 정의한 props를 현재 상태로 바꾸고
- children을 편집 가능한 값으로 다루고
- 이벤트 핸들러를 주입하고
- 별도의 실행 공간에 컴포넌트를 mount하고
- 변경이 생기면 다시 실행하고
- 실행 결과와 이벤트를 다시 문서 UI로 올린다&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이번 작업은 거창한 도구를 만든 경험이라기보다, &lt;/p&gt;
&lt;p&gt;이미 익숙하게 쓰던 도구들의 내부 구조를 작게 재현해본 경험에 가까웠다. &lt;/p&gt;
&lt;p&gt;그래도 이 구조를 작게 구현해보면서, Storybook이나 코드 편집기 같은 도구가 &lt;/p&gt;
&lt;p&gt;왜 단순한 문서나 화면이 아니라 실행 환경으로 설계되는지 조금은 이해하게 됐다.&lt;/p&gt;
&lt;br/&gt;</description>
      <category>개발 기술/개발 이야기</category>
      <category>iframe</category>
      <category>react</category>
      <category>storybook</category>
      <category>디자인 시스템</category>
      <category>실행 환경</category>
      <category>컴포넌트 문서화</category>
      <category>컴포넌트 프리뷰</category>
      <category>프리뷰 런타임</category>
      <author>GicoMomg</author>
      <guid isPermaLink="true">https://mong-blog.tistory.com/244</guid>
      <comments>https://mong-blog.tistory.com/entry/%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%ED%94%84%EB%A6%AC%EB%B7%B0%EB%8A%94-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%8B%A4%ED%96%89%EB%90%A0%EA%B9%8C-iframe%EC%9C%BC%EB%A1%9C-%EB%A7%8C%EB%93%A4%EC%96%B4%EB%B3%B8-%ED%94%84%EB%A6%AC%EB%B7%B0-%EB%9F%B0%ED%83%80%EC%9E%84#entry244comment</comments>
      <pubDate>Tue, 5 May 2026 19:28:16 +0900</pubDate>
    </item>
    <item>
      <title>SVG 아이콘 시스템 설계: Runtime에서 Build Time으로 전환하기</title>
      <link>https://mong-blog.tistory.com/entry/SVG-%EC%95%84%EC%9D%B4%EC%BD%98-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-Runtime%EC%97%90%EC%84%9C-Build-Time%EC%9C%BC%EB%A1%9C-%EC%A0%84%ED%99%98%ED%95%98%EA%B8%B0</link>
      <description>&lt;h1&gt;0. 들어가며&lt;/h1&gt;
&lt;p&gt;처음에 아이콘 시스템을 설계할 때 “화면에 아이콘이 잘 나타나면 되지 않을까?”라고 생각할 수 있다.&lt;/p&gt;
&lt;p&gt;하지만 실제 서비스에서 아이콘은 생각보다 더 많은 요구사항을 만족해야 한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;색상, 크기, 상태를 자유롭게 제어할 수 있어야 한다.&lt;/li&gt;
&lt;li&gt;다크 모드나 테마에 따라 스타일이 바꿀 수 있어야 한다.&lt;/li&gt;
&lt;li&gt;hover, active, disabled 상태를 반영할 수 있어야 한다.&lt;/li&gt;
&lt;li&gt;번들 크기는 최대한 작게 유지할 수 있어야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;문제는 이 요구사항들이 서로 잘 충돌한다는 점이다.&lt;/p&gt;
&lt;p&gt;스타일 제어를 잘 하려면 SVG를 코드처럼 다뤄야 하고, &lt;/p&gt;
&lt;p&gt;반대로 번들 크기를 줄이려면 정적인 리소스처럼 다루는 게 유리하다. &lt;/p&gt;
&lt;p&gt;즉, 아이콘 시스템을 설계할 때의 핵심 질문은 단순히 ‘아이콘을 어떻게 렌더링할 것인가?’가 아니다.&lt;/p&gt;
&lt;p&gt;실제로 이 질문에 더 가깝다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;스타일 제어를 유지하면서도, 번들 최적화를 어떻게 가져갈 것인가?&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;이번 시간에는 이 문제를 해결하기 위해 시도했던 두 가지 방식을 정리해보려고 한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;첫 번째 시도&lt;/strong&gt;: 런타임에서 SVG 문자열을 DOM으로 변환해서 직접 제어하는 방식&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;두 번째 시도&lt;/strong&gt;: 빌드 타임에서 SVG를 코드로 변환하고, 정적 import로 사용하는 방식&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;h1&gt;1. 아이콘은 보통 어떻게 사용할까?&lt;/h1&gt;
&lt;p&gt;아이콘을 사용하는 방식은 크게 세 가지가 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;img /&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;background-image&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;SVG Component&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;각 방식은 장단점은 분명하다.&lt;/p&gt;
&lt;br/&gt;

&lt;h2&gt;1-1. 가장 단순한 방식: &lt;code&gt;&amp;lt;img /&amp;gt;&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;가장 직관적인 방식이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;img src=&amp;quot;/icons/home.svg&amp;quot;/&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 방식은 HTML만으로 아이콘 출력이 가능하고 브라우저 캐싱도 활용할 수 있다.&lt;/p&gt;
&lt;p&gt;하지만 한계도 명확하다. SVG 내부의 &lt;code&gt;fill&lt;/code&gt;, &lt;code&gt;stroke&lt;/code&gt;를 직접 제어하기 어려워&lt;br&gt;상태에 따라 스타일을 바꾸기 어렵다. &lt;/p&gt;
&lt;p&gt;즉, 이 방식은 보여주기에는 좋지만 스타일을 제어하기 어려운 방식이다.&lt;/p&gt;
&lt;br/&gt;

&lt;h2&gt;1-2. CSS 기반 방식: &lt;code&gt;background-image&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;다음은 CSS에 아이콘을 넣는 방식이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.icon {
  background-image: url(&amp;quot;/icons/home.svg&amp;quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 방식은 레이아웃에 큰 영향을 주지 않고 아이콘을 배치할 수 있다.&lt;br&gt;특히 버튼이나 input 같은 UI에서 텍스트와 아이콘을 분리해 다루기 편하다.&lt;/p&gt;
&lt;p&gt;하지만 이 방식도 SVG 내부 노드를 직접 컨트롤할 수 없고, 세밀한 스타일링이 어렵다.&lt;/p&gt;
&lt;br/&gt;


&lt;h2&gt;1-3. SPA에서 자주 쓰는 방식: SVG 컴포넌트&lt;/h2&gt;
&lt;p&gt;상태와 스타일 제어가 중요한 순간, SVG를 컴포넌트처럼 다루게 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;Icon name=&amp;quot;home&amp;quot; size=&amp;quot;24&amp;quot; color=&amp;quot;red&amp;quot; /&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 방식은 색상, 크기, 상태를 컴포넌트 props로 제어할 수 있어 매우 유연하다.&lt;br&gt;다만 잘못 설계하면 아이콘 개수만큼 JavaScript 번들 부담이 커진다.&lt;/p&gt;
&lt;p&gt;예를 들어, 아이콘을 모두 컴포넌트로 분리하고 (&lt;code&gt;HomeIcon.vue&lt;/code&gt;, &lt;code&gt;UserIcon.jsx&lt;/code&gt; 등)&lt;br&gt;각 아이콘을 props 기반으로 제어하도록 설계하면, 사용성은 좋아진다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;HomeIcon color=&amp;quot;red&amp;quot; /&amp;gt;
&amp;lt;UserIcon size=&amp;quot;24&amp;quot; /&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;하지만 이 구조는 “사용하기 쉬운 만큼, 그대로 번들에 포함되는 구조”다.&lt;/p&gt;
&lt;p&gt;즉,&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;아이콘 개수 증가 → 컴포넌트 파일 증가&lt;/li&gt;
&lt;li&gt;컴포넌트 증가 → 번들 크기 증가&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;결국, 제어를 위해 구조를 확장할수록 번들 비용도 함께 증가하는 구조가 된다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;그럼 아이콘을 어떻게 관리해야 번들 비용도 아끼면서 스타일 커스텀을 자유롭게 할 수 있을까?&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;h1&gt;2. 첫 번째 시도: SVG를 문자열로 로드해 직접 제어하자&lt;/h1&gt;
&lt;p&gt;첫 번째 시도의 핵심은 단순하다. “&lt;strong&gt;SVG를 “이미지”가 아니라 “DOM”으로 다룬다!”&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;즉, SVG를 파일로 렌더링하는 것이 아니라&lt;/p&gt;
&lt;p&gt;런타임에서 DOM으로 변환하고, 내부 노드를 직접 제어하는 방식이다. (&lt;a href=&quot;https://github.com/KumJungMin/v-simple-svg-icon/tree/release/packages/icon&quot;&gt;원본 코드&lt;/a&gt;)&lt;/p&gt;
&lt;br/&gt;

&lt;h2&gt;2-1. 왜 이런 접근을 했을까?&lt;/h2&gt;
&lt;p&gt;SVG는 XML 기반 구조다.&lt;br&gt;즉, 문자열로 가져온 뒤 DOM으로 파싱하면 내부 노드에 직접 접근할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// SVG 내부 노드에 접근하는 코드 예시

const parser = new DOMParser()
const doc = parser.parseFromString(svgText,&amp;quot;image/svg+xml&amp;quot;)
const svgElement = doc.querySelector(&amp;quot;svg&amp;quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이렇게 변환하면 SVG는 더 이상 단순한 파일이 아니라, 내부 구조를 직접 조작할 수 있는 DOM 객체가 된다.&lt;br&gt;그래서 fill, stroke 제어, 특정 노드 선택, 상태 기반 스타일 변경과 같은 세밀한 제어를 할 수 있다.&lt;/p&gt;
&lt;br/&gt;

&lt;h2&gt;2-2. 런타임 처리 흐름 보기&lt;/h2&gt;
&lt;p&gt;외부에서 사용하는 API는 매우 단순하게 구성했다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;Icon name=&amp;quot;home&amp;quot;/&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;하지만 내부에서는 다음과 같은 과정이 일어난다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;60%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zu8dQ/dJMcac3CPv6/zdugXngf8KIpf9Fpa0Qzk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zu8dQ/dJMcac3CPv6/zdugXngf8KIpf9Fpa0Qzk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zu8dQ/dJMcac3CPv6/zdugXngf8KIpf9Fpa0Qzk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fzu8dQ%2FdJMcac3CPv6%2FzdugXngf8KIpf9Fpa0Qzk1%2Fimg.png&quot; width=&quot;60%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;단계&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;동작&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;SVG Raw 로드&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;SVG를 문자열(raw)로 로드한다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DOMParser 파싱&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;문자열을 실제 DOM으로 변환한다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;노드 탐색 및 주입&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;[fill]&lt;/code&gt;, &lt;code&gt;[stroke]&lt;/code&gt; 노드를 탐색해 class를 주입한다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;스타일 삽입 &amp;amp; 렌더링&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt;을 삽입해 스코프 스타일을 적용한다&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;즉, &lt;code&gt;&amp;lt;Icon /&amp;gt;&lt;/code&gt; 하나를 렌더링하기 위해&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SVG를 다시 파싱하고&lt;/li&gt;
&lt;li&gt;내부 구조를 탐색하고&lt;/li&gt;
&lt;li&gt;스타일을 주입하는 과정이 진행된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;그리고 이 흐름은 크게 두 단계로 나뉜다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;스타일을 적용할 대상 노드를 찾는 단계&lt;/li&gt;
&lt;li&gt;해당 노드에 스타일을 실제로 적용하는 단계&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이제 이 두 과정을 각각 살펴보자.&lt;/p&gt;
&lt;br/&gt;

&lt;h2&gt;2-3. 스타일을 적용할 노드를 어떻게 찾을까?&lt;/h2&gt;
&lt;p&gt;스타일을 적용하려면, 먼저 대상이 되는 노드(&lt;code&gt;[fill]&lt;/code&gt;, &lt;code&gt;[stroke]&lt;/code&gt;)를 찾아야 한다.&lt;/p&gt;
&lt;p&gt;런타임에 파싱된 SVG DOM에서 해당 속성을 가진 노드를 탐색한다.&lt;br&gt;이 노드들은 실제 색상이 적용되는 지점이다.&lt;/p&gt;
&lt;p&gt;이후 &lt;code&gt;class&lt;/code&gt;를 주입해, 클래스만으로 스타일을 제어할 수 있는 구조를 만든다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const paths = svgElement.querySelectorAll(&amp;quot;[stroke], [fill]&amp;quot;)

for (constpathofpaths) {
  // ...속성 확인 로직...
  if (hasStroke) path.classList.add(&amp;quot;svg-stroke&amp;quot;)
  if (props.isActive) path.classList.add(&amp;quot;active&amp;quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;h2&gt;2-4. 스타일은 어떻게 적용했을까?&lt;/h2&gt;
&lt;p&gt;이제 &lt;code&gt;class&lt;/code&gt;로 제어 지점을 만들었으니, 실제 스타일을 적용해보자.&lt;/p&gt;
&lt;p&gt;이 단계에서는 SVG 내부에 &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; 태그를 삽입해, 해당 SVG에만 스타일이 적용되도록 스코프를 격리한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;70%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sZSg2/dJMcabDGluz/tOFerRQuhlT1Lll6m85eX0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sZSg2/dJMcabDGluz/tOFerRQuhlT1Lll6m85eX0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sZSg2/dJMcabDGluz/tOFerRQuhlT1Lll6m85eX0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsZSg2%2FdJMcabDGluz%2FtOFerRQuhlT1Lll6m85eX0%2Fimg.png&quot; width=&quot;70%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;위 이미지는 SVG 내부에 &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt;이 삽입된 모습이다.&lt;/p&gt;
&lt;p&gt;이 방식 덕분에 props 값에 따라 색상, 상태(active 등)를 유연하게 변경할 수 있다.&lt;/p&gt;
&lt;p&gt;하지만 이 구조에는 명확한 문제가 있다. 모든 과정이 런타임에 수행된다는 점이다!&lt;/p&gt;
&lt;p&gt;&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;h1&gt;3. 그런데 왜 이 방식이 최선이 아니었을까?&lt;/h1&gt;
&lt;p&gt;첫 번째 시도는 스타일 제어 측면에서는 이점이 있었다.&lt;br&gt;SVG 내부 노드를 직접 다룰 수 있었고, 상태에 따라 유연하게 스타일을 바꿀 수도 있었다.&lt;/p&gt;
&lt;p&gt;그런데 치명적인 문제가 하나 있었다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;이 방식은 구조적으로 트리쉐이킹이 불가능했다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;이건 구현이 조금 부족한 수준의 문제가 아니다. 구조 자체가 번들러 친화적이지 않은 방식이었다.&lt;/p&gt;
&lt;br/&gt;

&lt;h2&gt;3-1. 문제의 본질: 아이콘 선택이 런타임에 결정된다&lt;/h2&gt;
&lt;p&gt;코드를 보면 아이콘의 이름을 문자열로 받아서 동적으로 로드한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const loader = resolveSvgLoader(name)
// name 변수는 런타임에 결정됨&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;여기서 핵심은 &lt;code&gt;name&lt;/code&gt; 값이 빌드 타임이 아니라 런타임에 정해진다는 점이다.&lt;/p&gt;
&lt;p&gt;번들러 입장에서는 어떤 아이콘이 실제로 쓰일지 알 수 없다. &lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;quot;home&amp;quot;&lt;/code&gt;이 들어올지, 혹은 다른 어떤 값이 들어올지 빌드 시점에서는 판단할 수 없기 때문이다.&lt;/p&gt;
&lt;p&gt;그 결과 번들러는 모든 아이콘을 포함할 수 밖에 없다.&lt;/p&gt;
&lt;p&gt;즉, 사용한 아이콘이 하나뿐이어도 전체 아이콘 세트가 번들에 남게 된다.&lt;/p&gt;
&lt;br/&gt;

&lt;h2&gt;3-2. 왜 모든 아이콘이 번들에 포함될까?&lt;/h2&gt;
&lt;p&gt;트리쉐이킹은 &amp;#39;사용하지 않는 코드를 제거하는 최적화&amp;#39;다. 그런데 이 최적화가 동작하려면 전제가 하나 있다.&lt;/p&gt;
&lt;p&gt;바로 &lt;strong&gt;의존성이 빌드 타임에 정적으로 분석 가능&lt;/strong&gt;해야 한다.&lt;/p&gt;
&lt;p&gt;하지만 첫 번째 방식은 정반대였다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;참조 방식: 문자열(&lt;code&gt;name&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;선택 시점: 런타임&lt;/li&gt;
&lt;li&gt;번들러 분석: 불가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;즉, 캐시를 추가하거나, 탐색 로직을 조금 더 빠르게 만든다고 해서 해결될 문제가 아니었다. &lt;/p&gt;
&lt;p&gt;문제는 SVG를 어떻게 파싱하느냐가 아니라, &lt;strong&gt;아이콘을 언제 결정하느냐&lt;/strong&gt;에 있었다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;strong&gt;문제점&lt;/strong&gt;&lt;br/&gt;&lt;br&gt;첫 번째 시도는 의존성이 런타임에 결정된다.&lt;br&gt;그래서 번들러가 사용 여부를 정적으로 분석할 수 없다.&lt;br&gt;즉, 스타일 제어에는 강하지만 번들 최적화에는 구조적으로 불리하다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;h1&gt;4. 문제의 본질: Runtime이 아니라 Build Time에서 결정해야 한다&lt;/h1&gt;
&lt;p&gt;첫 번째 시도의 한계를 다시 요약하면 다음과 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;quot;home&amp;quot; → 런타임에 찾음 → 번들러는 모름&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;그렇다면 해결 방향은 자연스럽게 정해진다.&lt;/p&gt;
&lt;p&gt;아이콘을 런타임에 찾지 말고, 빌드 타임에 미리 확정해야 한다&lt;/p&gt;
&lt;p&gt;즉, 문자열 기반 탐색 구조를 버리고, 정적으로 분석 가능한 구조로 바꿔야 한다.&lt;/p&gt;
&lt;br/&gt;

&lt;h2&gt;4-1. 핵심 구조 변경&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bughmt/dJMcaf0lVjr/zrAemKh6aHjFm1JFk73DyK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bughmt/dJMcaf0lVjr/zrAemKh6aHjFm1JFk73DyK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bughmt/dJMcaf0lVjr/zrAemKh6aHjFm1JFk73DyK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbughmt%2FdJMcaf0lVjr%2FzrAemKh6aHjFm1JFk73DyK%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;문자열 기반 참조를 버리고 아이콘을 모듈로 만들어, 정적 import 구조로 전환해야 한다.&lt;/p&gt;
&lt;p&gt;결국 이 변화는 단순히 “렌더링 구현 방식”을 바꾸는 문제가 아니었다. &lt;strong&gt;의존성을 언제 확정할 것인가&lt;/strong&gt;의 문제였다.&lt;/p&gt;
&lt;p&gt;&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;h1&gt;5. 두 번째 시도: SVG를 빌드 타임에 코드로 변환하자&lt;/h1&gt;
&lt;p&gt;이제 방향은 명확하다. SVG 파일 자체를 사용하지 않고, 빌드 스크립트를 통해 컴포넌트 코드로 변환해야 한다.&lt;/p&gt;
&lt;p&gt;즉, 아이콘을 런타임에 찾는 것이 아니라, 빌드 타임에 미리 생성해 두고 앱에서는 정적으로 가져다 쓰는 구조로 바꾸는 것이다. (&lt;a href=&quot;https://github.com/KumJungMin/v-simple-svg-icon/tree/release-v2/packages/icon&quot;&gt;원본 코드&lt;/a&gt;)&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;전체 흐름은 다음과 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;40%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cHy1ZO/dJMcaare40K/UWmKfojRIJ5WKJkIuu7Im0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cHy1ZO/dJMcaare40K/UWmKfojRIJ5WKJkIuu7Im0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cHy1ZO/dJMcaare40K/UWmKfojRIJ5WKJkIuu7Im0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcHy1ZO%2FdJMcaare40K%2FUWmKfojRIJ5WKJkIuu7Im0%2Fimg.png&quot; width=&quot;40%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;각 단계는 다음과 같은 역할을 가진다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;단계&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;역할&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;SVG 파일 수집 (directory scan)&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;assets 디렉토리를 순회하며 SVG 파일을 수집한다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AST 변환 (svg-parser)&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;SVG를 DOM이 아닌 분석 가능한 구조(AST)로 변환한다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;메타데이터 생성&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;viewBox, path 등 렌더링에 필요한 정보를 추출한다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;컴포넌트 생성&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;메타데이터를 기반으로 Vue/React 컴포넌트 코드를 생성한다 (샘플코드에서는 Vue만 생성)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Export 등록 (&lt;code&gt;index.ts&lt;/code&gt;)&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;생성된 컴포넌트를 정적으로 import할 수 있도록 export에 등록한다&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;이 과정을 거치면 SVG가 빌드타임에 코드로 변환된다. 그리고 이 동작은 &lt;a href=&quot;https://github.com/KumJungMin/v-simple-svg-icon/blob/release-v2/packages/icon/scripts/generate-icons.ts&quot;&gt;&lt;code&gt;generate-icons.ts&lt;/code&gt;&lt;/a&gt;에서 시작한다.&lt;/p&gt;
&lt;p&gt;&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;h1&gt;6. generate-icons.ts 기준으로 전체 과정을 따라가보자&lt;/h1&gt;
&lt;p&gt;이제부터는 두 번째 시도의 구현 흐름을 &lt;code&gt;generate-icons.ts&lt;/code&gt;를 기준으로 차근차근 따라가보자.&lt;/p&gt;
&lt;h2&gt;6-1. 1단계: SVG 파일 목록 읽기&lt;/h2&gt;
&lt;p&gt;먼저 변환 대상이 되는 SVG 파일을 수집한다. &lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const svgFiles = fs.readdirSync(ICON_ASSET_PATH);&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;h2&gt;6-2. 2단계: 문자열을 AST로 변환하기&lt;/h2&gt;
&lt;p&gt;빌드 타임 환경은 Node.js다. 즉, 브라우저의 DOM이 없다. &lt;/p&gt;
&lt;p&gt;그래서 첫 번째 방식처럼 &lt;code&gt;DOMParser&lt;/code&gt;를 사용할 수 없다.&lt;/p&gt;
&lt;p&gt;이때 필요한 것이 &lt;code&gt;svg-parser&lt;/code&gt; 같은 도구다. SVG 문자열을 DOM이 아니라 AST로 변환한다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구분&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;DOMParser&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;AST (&lt;code&gt;svg-parser&lt;/code&gt;)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;실행 시점&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;런타임&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;빌드 타임&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;환경&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;브라우저&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Node.js&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;결과&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;DOM 객체&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;순수 데이터 구조&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;즉, 두 번째 방식의 목표는 렌더링 가능한 DOM을 만드는 것이 아니라, &lt;strong&gt;코드 생성이 가능한 구조 데이터&lt;/strong&gt;를 만드는 것이다.&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;이 단계에서 실제로 수행하는 작업은 다음과 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;/**
 * SVG 문자열을 AST로 변환한 뒤, 루트 &amp;lt;svg&amp;gt; 노드를 추출한다.
 */
export function extractSvgTree(raw: string): SvgAstNode | undefined {
  const parsed = parse(raw) as { children: SvgAstNode[] };

  return parsed.children.find((node) =&amp;gt; node.tagName === &amp;quot;svg&amp;quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 함수의 역할은 단순하다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SVG 문자열을 AST로 변환하고&lt;/li&gt;
&lt;li&gt;그 중에서 &lt;code&gt;&amp;lt;svg&amp;gt;&lt;/code&gt; 루트 노드만 추출한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이렇게 얻은 AST는 이후 단계에서&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;path 추출&lt;/li&gt;
&lt;li&gt;viewBox 분석&lt;/li&gt;
&lt;li&gt;메타데이터 생성&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;과 같은 작업의 입력 데이터로 사용된다.&lt;/p&gt;
&lt;br/&gt;

&lt;h2&gt;6-3. 3단계: 구조를 평탄화하고 메타데이터를 만든다&lt;/h2&gt;
&lt;p&gt;SVG는 보통 중첩된 트리 구조를 가진다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; * svg
 *  ├─ g
 *  │   └─ path
 *  └─ path&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이 구조를 그대로 렌더링 코드에 사용하면, 재귀 처리와 분기 로직이 복잡해진다.&lt;/p&gt;
&lt;p&gt;그래서 이 단계에서는 구조를 한 번 평탄화한다. 중첩 구조를 “렌더링 가능한 단순 리스트”로 변환한다.&lt;/p&gt;
&lt;p&gt;이 과정에서 만들어지는 결과가 바로 메타데이터다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;export const HomeMeta = {
  viewBox: &amp;quot;0 0 24 24&amp;quot;,
  nodes: [
    { tag: &amp;quot;path&amp;quot;, attrs: { d: &amp;quot;...&amp;quot;, stroke: &amp;quot;#000&amp;quot; } }
  ]
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 메타데이터는&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SVG 구조를 데이터로 표현한 결과이며&lt;/li&gt;
&lt;li&gt;이후 렌더링 단계에서 그대로 사용되는 입력값이다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;즉, 이 시점부터 SVG는 “파일”이 아니라 &lt;strong&gt;렌더링 가능한 구조 데이터&lt;/strong&gt;가 된다.&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;이 구조를 만들기 위해 사용하는 핵심 함수는 다음과 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;export function flattenSvg(svg: SvgAstNode) {
  const nodes: Array&amp;lt;{ tag: string; attrs: SvgAttrs }&amp;gt; = [];
  const groups = new Set&amp;lt;string&amp;gt;();

  collectNodes(svg.children ?? [], nodes, groups);

  return {
    nodes,
    groups: [...groups],
  };
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 함수의 역할은 명확하다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;중첩된 SVG 트리를 순회하고&lt;/li&gt;
&lt;li&gt;렌더링에 필요한 노드만 추출한 뒤&lt;/li&gt;
&lt;li&gt;평탄한 구조(&lt;code&gt;nodes&lt;/code&gt;)로 변환한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;결과적으로, 이후 단계에서 복잡한 트리를 다루지 않고 단순한 리스트 기반으로 아이콘을 렌더링할 수 있다.&lt;/p&gt;
&lt;br/&gt;

&lt;h2&gt;6-4. 4단계: 아이콘 컴포넌트는 어떻게 생성될까?&lt;/h2&gt;
&lt;p&gt;앞에서 메타데이터를 만들었다면, 이제 이 데이터를 실제 SVG로 렌더링하는 컴포넌트를 만들어야 한다.&lt;/p&gt;
&lt;p&gt;이때 핵심이 되는 함수가 바로 &lt;code&gt;createIconComponent&lt;/code&gt;다.&lt;/p&gt;
&lt;p&gt;이 함수는 메타데이터를 기반으로 VNode를 생성하고, props에 따라 최종 SVG를 렌더링한다.&lt;/p&gt;
&lt;h3&gt;6-4-1. &lt;code&gt;createIconComponent&lt;/code&gt; 내부는 어떻게 동작할까?&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;createIconComponent&lt;/code&gt;의 핵심 로직만 가져와봤다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;export function createIconComponent(meta: IconMeta) {
  return defineComponent({
    setup(props) {
      const renderNodes = createNodeRenderer(meta);
      const renderSvg = createSvgRenderer(meta);

      return () =&amp;gt; {
        const ctx = getRenderContext(props);
        const nodes = renderNodes(ctx);

        return renderSvg(props, nodes);
      };
    },
  });
}&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;


&lt;p&gt;이 구조에서 중요한 점은 두 가지다. 첫째, SVG를 다시 파싱하지 않는다는 점이다.&lt;/p&gt;
&lt;p&gt;첫 번째 방식은 다음 흐름이었다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SVG 문자열 → DOMParser → DOM → 조작&lt;/code&gt;&lt;/pre&gt;&lt;br/&gt;

&lt;p&gt;반면 두 번째 방식은 이미 빌드 타임에 만들어 둔 메타데이터를 바탕으로, 렌더링 시점에는 VNode만 생성한다.&lt;/p&gt;
&lt;p&gt;즉, 런타임 파싱 비용이 사라진다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;meta → VNode 생성 → 렌더링&lt;/code&gt;&lt;/pre&gt;&lt;br/&gt;


&lt;p&gt;둘째, 렌더링 방식이 명령형에서 선언형으로 바뀐다는 점이다.&lt;/p&gt;
&lt;p&gt;첫 번째 방식은 DOM을 직접 탐색하고 속성을 하나씩 변경했는데, 어떻게 바꿀지 코드로 일일이 명령했다.&lt;/p&gt;
&lt;p&gt;반면 두 번째 방식은 최종적으로 어떤 형태의 SVG가 되어야 하는지만 정의한다. &lt;/p&gt;
&lt;p&gt;즉, “이 상태라면 이런 결과가 나와야 한다”를 선언하면, 실제 DOM 업데이트는 프레임워크가 알아서 처리한다.&lt;/p&gt;
&lt;br/&gt;

&lt;h3&gt;6-4-2. props는 어떻게 반영될까?&lt;/h3&gt;
&lt;p&gt;메타데이터에는 기본 SVG 속성이 들어 있다. &lt;/p&gt;
&lt;p&gt;하지만 실제 사용 시에는 &lt;code&gt;fill&lt;/code&gt;, &lt;code&gt;stroke&lt;/code&gt;, &lt;code&gt;size&lt;/code&gt; 같은 props로 값을 덮어써야 한다.&lt;/p&gt;
&lt;p&gt;이를 위해 attrs 레벨에서 merge를 수행한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;function resolveNodeAttrs(attrs,props) {
return {
    ...attrs,
    fill:props.fill??attrs.fill,
    stroke:props.stroke??attrs.stroke,
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 구조의 장점은 명확하다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;원본 SVG 속성은 기본값으로 유지되고&lt;/li&gt;
&lt;li&gt;필요한 경우 props로 override할 수 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;즉, 첫 번째 방식처럼 DOM을 파싱한 뒤 class를 주입하고 스타일을 덮어쓰는 것이 아니라, &lt;/p&gt;
&lt;p&gt;렌더링 시점에 최종 속성을 직접 결정하는 구조가 된다.&lt;/p&gt;
&lt;br/&gt;

&lt;h3&gt;6-4-3. 스타일 적용 방식은 어떻게 달라졌을까?&lt;/h3&gt;
&lt;p&gt;첫 번째 방식에서는 &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; 태그를 SVG 내부에 주입하고, class 기반으로 스타일을 제어했다.&lt;/p&gt;
&lt;p&gt;반면 두 번째 방식에서는 렌더링 시점에 attrs로 직접 값을 넣는다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;h(&amp;quot;path&amp;quot;, {
  d:&amp;quot;...&amp;quot;,
  fill:props.fill,
})&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;두 방식을 비교하면 다음과 같다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;방식&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;스타일 적용&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;첫 번째 방식&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;DOM 조작 + style 주입&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;두 번째 방식&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;props 기반 attrs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;즉, 첫 번째 방식은 스타일을 나중에 덮는 구조였다면, 두 번째 방식은 렌더링 시점에 최종 스타일을 결정하는 구조다.&lt;/p&gt;
&lt;br/&gt;

&lt;h3&gt;6-4-4. 성능 관점에서는 어떤 차이가 있을까?&lt;/h3&gt;
&lt;p&gt;이 구조 변화는 트리쉐이킹만의 문제가 아니다. 런타임 비용에도 차이를 만든다.&lt;/p&gt;
&lt;p&gt;첫 번째 방식에서는 매 렌더마다 다음 작업이 수행될 수 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;DOMParser 실행&lt;/li&gt;
&lt;li&gt;querySelector 탐색&lt;/li&gt;
&lt;li&gt;class 주입&lt;/li&gt;
&lt;li&gt;style 태그 생성&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;반면 두 번째 방식은 이미 준비된 메타데이터를 바탕으로 VNode를 생성하고, 이후는 프레임워크의 Virtual DOM diff에 맡긴다.&lt;/p&gt;
&lt;p&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;h2&gt;6-5. 5단계: 아이콘 컴포넌트를 export한다&lt;/h2&gt;
&lt;p&gt;메타데이터를 기반으로 실제 Vue 컴포넌트 코드를 생성한 뒤, 각각을 독립된 파일로 저장한다.&lt;/p&gt;
&lt;p&gt;그리고 &lt;code&gt;index.ts&lt;/code&gt;에서 각각을 export한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export { HomeIcon } from &amp;quot;./HomeIcon&amp;quot;;
export { UserIcon } from &amp;quot;./UserIcon&amp;quot;;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이 단계가 중요한데, 각 아이콘이 독립된 모듈이 되어야지만 &lt;/p&gt;
&lt;p&gt;앱에서 필요한 것만 정적으로 import할 수 있기 때문이다.&lt;/p&gt;
&lt;br/&gt;


&lt;h2&gt;6-6. 전체 흐름을 다시 정리해보자&lt;/h2&gt;
&lt;p&gt;지금까지의 빌드 타임 구조를 한 번에 정리하면 다음과 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SVG 파일
↓
문자열로 읽기
↓
AST 변환
↓
구조 정리 및 메타데이터 생성
↓
아이콘 컴포넌트 생성
↓
export 등록
↓
정적 import 사용&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이제 앱에서는 필요한 아이콘만 명시적으로 가져다 쓰게 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import { HomeIcon } from&amp;quot;@v-simple/icon/common&amp;quot;;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;즉, 아이콘은 더 이상 런타임에서 “찾는 대상”이 아니라, 빌드 타임에 생성된 “정적 모듈”이 된다.&lt;/p&gt;
&lt;p&gt;&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;h1&gt;7. 그래서 실제로 무엇이 달라졌을까?&lt;/h1&gt;
&lt;p&gt;이제 두 방식을 실제 결과 기준으로 비교해보자.&lt;/p&gt;
&lt;p&gt;아이콘 100개 중 실제로는 1개만 사용하는 극단적인 상황을 만들어보았다.&lt;/p&gt;
&lt;h3&gt;첫 번째 방식 (런타임)&lt;/h3&gt;
&lt;p&gt;사용한 아이콘이 하나뿐이어도, 전체 아이콘 세트가 번들에 포함된다. (193KB)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1개만 사용해도 100개 전체가 포함됨&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vTB3h/dJMcacJne0v/yel3jhmvV9k3EI3SQKxr91/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vTB3h/dJMcacJne0v/yel3jhmvV9k3EI3SQKxr91/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vTB3h/dJMcacJne0v/yel3jhmvV9k3EI3SQKxr91/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvTB3h%2FdJMcacJne0v%2Fyel3jhmvV9k3EI3SQKxr91%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3&gt;두 번째 방식 (빌드 타임)&lt;/h3&gt;
&lt;p&gt;사용한 아이콘만 번들에 포함된다. (58KB)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;실제 사용한 1개만 포함됨&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qsyl8/dJMcaaLxx16/UWlfKP05kQ9RgpSj2iHxiK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qsyl8/dJMcaaLxx16/UWlfKP05kQ9RgpSj2iHxiK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qsyl8/dJMcaaLxx16/UWlfKP05kQ9RgpSj2iHxiK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fqsyl8%2FdJMcaaLxx16%2FUWlfKP05kQ9RgpSj2iHxiK%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3&gt;차이&lt;/h3&gt;
&lt;p&gt;첫 번째 방식은 사용 여부와 관계없이 전체가 포함된다.&lt;br&gt;그래서 아이콘 수가 늘어날수록 불필요한 비용이 계속 누적된다.&lt;/p&gt;
&lt;p&gt;반면 두 번째 방식은 사용한 만큼만 포함된다.&lt;/p&gt;
&lt;p&gt;즉, 규모가 커질수록 두 방식의 차이는 더 크게 벌어진다.&lt;/p&gt;
&lt;p&gt;&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;h1&gt;8. 마치며&lt;/h1&gt;
&lt;p&gt;이번 글에서는 아이콘 시스템을 설계하며 시도한 두 가지 방식을 살펴보았다.&lt;/p&gt;
&lt;p&gt;첫 번째 방식은 아이콘을 문자열 name으로 찾고, 그 결정이 런타임에 이루어진다.&lt;br&gt;이 구조에서는 번들러가 어떤 아이콘이 사용되는지 알 수 없기 때문에, 모든 아이콘이 번들에 포함된다.&lt;/p&gt;
&lt;p&gt;반면 두 번째 방식은 SVG를 빌드 타임에 코드로 변환하고, import로 명시적으로 사용한다.&lt;br&gt;그 결과 번들러가 의존성을 추적할 수 있고, 실제 사용한 아이콘만 남길 수 있다.&lt;/p&gt;
&lt;p&gt;문제는 SVG가 아니라, 결정 시점이었다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;런타임 결정 → 번들러 개입 불가
빌드 타임 결정 → 번들러 최적화 가능&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;아이콘은 화면에서 사소한 요소일 수 있다.&lt;br&gt;하지만 시스템이 커질수록 함께 증가하고,&lt;br&gt;결국 관리와 번들 사이즈 문제로 이어진다.&lt;/p&gt;
&lt;p&gt;결국 더 나은 시스템은, 언제 결정할 것인가를 고민하는 것에서 시작된다.&lt;/p&gt;</description>
      <category>개발 기술/개발 이야기</category>
      <category>JavaScript 번들 크기 줄이기</category>
      <category>svg 아이콘</category>
      <category>SVG 아이콘 최적화</category>
      <category>SVG 컴포넌트 방식</category>
      <category>빌드 타임 vs 런타임</category>
      <category>아이콘</category>
      <category>아이콘 시스템 설계</category>
      <category>웹 성능 최적화</category>
      <category>트리쉐이킹(Tree Shaking)</category>
      <category>프론트엔드 번들 최적화</category>
      <author>GicoMomg</author>
      <guid isPermaLink="true">https://mong-blog.tistory.com/243</guid>
      <comments>https://mong-blog.tistory.com/entry/SVG-%EC%95%84%EC%9D%B4%EC%BD%98-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-Runtime%EC%97%90%EC%84%9C-Build-Time%EC%9C%BC%EB%A1%9C-%EC%A0%84%ED%99%98%ED%95%98%EA%B8%B0#entry243comment</comments>
      <pubDate>Tue, 31 Mar 2026 21:15:04 +0900</pubDate>
    </item>
    <item>
      <title>[Webpack] 웹팩 런타임의 정체 &amp;mdash; 브라우저에 모듈 시스템을 심는다고?</title>
      <link>https://mong-blog.tistory.com/entry/Webpack-%EC%9B%B9%ED%8C%A9-%EB%9F%B0%ED%83%80%EC%9E%84%EC%9D%98-%EC%A0%95%EC%B2%B4-%E2%80%94-%EB%B8%8C%EB%9D%BC%EC%9A%B0%EC%A0%80%EC%97%90-%EB%AA%A8%EB%93%88-%EC%8B%9C%EC%8A%A4%ED%85%9C%EC%9D%84-%EC%8B%AC%EB%8A%94%EB%8B%A4%EA%B3%A0</link>
      <description>&lt;h1&gt;0. 들어가며&amp;hellip;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://mong-blog.tistory.com/entry/Webpack-%EB%B2%88%EB%93%A4%EB%A7%81%EC%9D%98-%EC%9B%90%EB%A6%AC-%EC%88%98%EC%B2%9C-%EA%B0%9C%EC%9D%98-%EB%AA%A8%EB%93%88%EC%9D%80-%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%AC%B6%EC%9D%84%EA%B9%8C&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;지난 시간&lt;/a&gt;에는 웹팩이 여러 파일을 어떻게 &lt;b&gt;청크(Chunk)&lt;/b&gt; 단위로 분리하는지 살펴봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;청크 분리는 &lt;b&gt;빌드 타임(Build Time)&lt;/b&gt; 에 일어나며, 크게 세 단계로 요약할 수 있다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;단계&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1. ModuleGraph 생성&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;import / require&lt;/code&gt; 관계를 분석하여 의존성 그래프를 만든다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2. ChunkGraph 생성&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;어떤 모듈을 어떤 청크로 묶을지 결정한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3. 최종 산출물 생성&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;설정(&lt;code&gt;splitChunks&lt;/code&gt;, &lt;code&gt;runtimeChunk&lt;/code&gt;)에 따라 여러 JS 파일이 생성된다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과는 이런 형태다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;dist/
 ├─ main.js
 ├─ runtime.js   (설정에 따라 분리)
 └─ dynamic.[hash].js&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지는 빌드 타임 이야기였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  그런데 여기서 의문 하나!&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 나눠진 청크는 브라우저에서 어떻게 실행할까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약, 단일 파일이라면 실행은 단순하다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;script src=&quot;main.js&quot;&amp;gt;&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저가 파일을 다운로드하고 실행하면 끝이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 실제 서비스는 그렇지 않다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여러 청크가 존재하고&lt;/li&gt;
&lt;li&gt;&lt;code&gt;import()&lt;/code&gt;가 있다.&lt;/li&gt;
&lt;li&gt;CommonJS와 ESM이 섞여 있고&lt;/li&gt;
&lt;li&gt;모듈 간 의존 관계가 복잡하게 얽혀 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 더 근본적인 문제가 하나 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저는 기본적으로 CommonJS 문법을 이해하지 못한다!&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;require(&quot;./math&quot;)
module.exports= {}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저가 이해하는 것은 ECMAScript 표준 문법(&lt;code&gt;import/export&lt;/code&gt;)과 Web API뿐이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;require&lt;/code&gt;는 Node.js 런타임이 제공하는 함수이기에, 브라우저는 CommonJS 모듈 시스템을 알지 못한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✏️ 그런데 왜 번들은 실행될까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아니 브라우저가 CommonJS 문법을 모르는데, 왜 에러 없이 실행되는 걸까?&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이유는 단순하다. 웹팩이 모듈 시스템 자체를 번들 안에 구현했기 때문이다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;번들은 단순히 파일을 합친 결과물이 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내 코드 + 모듈 실행기(Runtime Engine)이 결합된 하나의 실행 프로그램이다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 시간에는 웹팩이 삽입한 런타임 모듈 시스템이&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;어떻게 모듈을 등록하고&lt;/li&gt;
&lt;li&gt;어떻게 실행을 통제하며&lt;/li&gt;
&lt;li&gt;어떻게 동적 import까지 확장하는지&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단계적으로 알아보겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h1&gt;1. 웹팩 런타임은 무엇인가?&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞에서 언급했듯이, 번들은 단순한 파일 묶음이 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모듈 실행기를 포함한 하나의 실행 프로그램이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 이 &amp;lsquo;실행기&amp;rsquo;는 어떤 구조로 이루어져 있을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1) 웹팩 런타임의 핵심 구조&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(1) 구조부터 살펴보자&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹팩이 번들 안에 삽입하는 런타임의 핵심은 세 가지다.&lt;/p&gt;
&lt;pre class=&quot;arcade&quot;&gt;&lt;code&gt;var __webpack_modules__= { ... };
var __webpack_module_cache__= {};
function __webpack_require__(moduleId) { ... }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 세 요소가 합쳐져 하나의 모듈 시스템을 구성한다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;요소&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;역할&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;__webpack_modules__&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;아직 실행되지 않은 모듈 팩토리 저장소 &amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;__webpack_module_cache__&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;실행 완료된 모듈 보관소&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;__webpack_require__&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;실행 여부를 판단하고 흐름을 제어하는 엔진&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면, 번들에 포함된 각 소스 파일 모듈은 런타임에서 어떻게 저장될까?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹팩은 모듈을 &amp;lsquo;파일&amp;rsquo; 그대로 보관하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대신, 각 파일을 실행 가능한 &lt;b&gt;팩토리 함수&lt;/b&gt;로 변환해 &lt;code&gt;__webpack_modules__&lt;/code&gt; 테이블에 등록한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 변환은 빌드 타임에 이루어지며, 모듈은 다음과 같은 함수 형태로 감싸진다.&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;function(module,exports,__webpack_require__) {
  // 원래 파일 코드
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;런타임에서는 모듈이 필요해질 때마다 &lt;code&gt;__webpack_require__(moduleId)&lt;/code&gt;가 호출되고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 함수가 해당 모듈의 팩토리 함수를 실행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 번들 내부에는 브라우저에서 동작하는 CommonJS 실행 환경이 구현되어 있는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(2) 왜 이런 구조가 필요한가?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 이 구조가 없으면 브라우저는 다음을 보장할 수 없다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;모듈은 최초 한 번만 실행된다.&lt;/li&gt;
&lt;li&gt;실행 결과(&lt;code&gt;exports&lt;/code&gt;)는 공유된다.&lt;/li&gt;
&lt;li&gt;모듈 간 의존성은 런타임에서 동적으로 해석된다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 첫 번째, 두 번째 규칙은 CommonJS 모듈 시스템의 중요한 특징이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;한 번 실행된 모듈은 다시 실행되지 않고,&lt;/li&gt;
&lt;li&gt;그 결과는 동일한 인스턴스로 공유된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹팩 런타임은 이 동작을 브라우저 환경에서도 유지하기 위해 이 구조를 만들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 이 중에서&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;캐시를 확인하고&lt;/li&gt;
&lt;li&gt;팩토리를 호출하고&lt;/li&gt;
&lt;li&gt;실행 흐름을 통제하는 역할은 누가 할까?&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로 &lt;code&gt;__webpack_require__&lt;/code&gt;다. 이제 이 함수가 내부에서 어떤 과정을 거쳐 모듈을 실행하는지 살펴보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2) &lt;b&gt;webpack_require&lt;/b&gt;의 내부 동작&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(1) 먼저 코드부터 살펴보자&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹팩 번들 내부의 대략적인 구조는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;function __webpack_require__(moduleId) {

  // 1. 캐시 확인
  var cachedModule = __webpack_module_cache__[moduleId];
  if (cachedModule !== undefined) {
    return cachedModule.exports;
  }

  // 2. 새 module 객체 생성
  var module = { exports: {} };

  // 3. 캐시에 먼저 등록 (중요)
  __webpack_module_cache__[moduleId] = module;

  // 4. 모듈 팩토리 실행
  __webpack_modules__[moduleId](module, module.exports, __webpack_require__);

  // 5. exports 반환
  return module.exports;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(2) 실행 흐름을 단계적으로 살펴보자&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 entry 모듈(&lt;code&gt;index.js&lt;/code&gt;)이 &lt;code&gt;math.js&lt;/code&gt;를 불러온다고 가정해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빌드 타임에 &lt;code&gt;math.js&lt;/code&gt;가 moduleId = 1로 매핑되었다면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;런타임에서는 파일 경로 대신 이 moduleId(1)를 기준으로 모듈을 식별한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 브라우저에서 실행되는 코드는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;const add = __webpack_require__(1); // math.js의 moduleId가 1이라고 가정&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;webpack_require_(1)&lt;/code&gt;이 호출되면, 이 함수 내부에서 여러 단계를 진행하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1️⃣ 캐시 확인 단계 &amp;mdash; 이미 실행된 적이 있는가?&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;var cachedModule = __webpack_module_cache__[moduleId];&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;__webpack_require__&lt;/code&gt;이 호출되면 가장 먼저 이 모듈이 이미 실행된 적이 있는지 확인한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 가장 먼저 캐시를 확인할까?&lt;br /&gt;그 이유는 CommonJS에서는 모듈이 한 번만 실행되고,&lt;br /&gt;이후에는 그 결과가 재사용되어야 하기 때문이다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 두 번째 &lt;code&gt;require&lt;/code&gt; 호출시 팩토리를 재실행하면 안 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 캐시에 존재한다면, 저장된 &lt;code&gt;exports&lt;/code&gt;를 그대로 반환하고 팩토리는 호출하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2️⃣ 새 module 객체 생성 단계 &amp;mdash; 실행 컨테이너 준비&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;var module = { exports: {} };&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 모듈의 캐시가 없다면, 웹팩은 빈 module 객체를 만든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 객체는 실행 결과를 담는 역할을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3️⃣ 캐시에 먼저 등록하는 단계 &amp;mdash; 순환 참조를 막는 장치&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;__webpack_module_cache__[moduleId] = module;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모듈을 실행하기 전에, 생성한 &lt;code&gt;module&lt;/code&gt; 객체를 먼저 캐시에 등록한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 &lt;b&gt;순환 참조(Circular Dependency)&lt;/b&gt; 처리를 위해 필요하다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 실행 전에 모듈 객체를 캐시에 등록해야 할까?&lt;br /&gt;이유는 모듈은 실행 중에도 다시 &lt;code&gt;require&lt;/code&gt;될 수 있기 때문이다.&lt;br /&gt;만약 캐시에 등록되어 있지 않으면 동일 모듈이 새로 생성되어 순환 참조 문제가 발생한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, A가 B를 호출하고, B가 A를 호출하는 구조가 있다고 가정하자.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;A &amp;rarr; B &amp;rarr; A&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 A 실행이 완료된 후에 캐시에 등록된다면,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;두 번째 A &lt;code&gt;require&lt;/code&gt;는 &amp;ldquo;아직 캐시에 없다&amp;rdquo;고 판단하고 새 모듈 객체를 다시 생성한다.&lt;/li&gt;
&lt;li&gt;그 결과, 무한 재귀(A &amp;rarr; B &amp;rarr; A &amp;rarr; B &amp;rarr; A &amp;hellip;)가 발생한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 실행 전에 캐시에 등록해두면,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;두 번째 &lt;code&gt;require&lt;/code&gt;는 이미 존재하는 &lt;code&gt;module&lt;/code&gt; 객체를 반환하고&lt;/li&gt;
&lt;li&gt;아직 완성되지 않은 &lt;code&gt;exports&lt;/code&gt;라도 동일 인스턴스를 공유한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것이 CommonJS의 표준 동작 방식이며, 웹팩 런타임은 이를 그대로 구현했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4️⃣ 모듈 팩토리가 실행되는 단계&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐시에 해당 모듈이 없다면, &lt;code&gt;__webpack_modules__&lt;/code&gt; 테이블에 등록된 팩토리 함수를 호출한다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;__webpack_modules__[moduleId](module, module.exports, __webpack_require__);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팩토리 함수가 호출되면,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;module&lt;/code&gt;, &lt;code&gt;exports&lt;/code&gt;, &lt;code&gt;__webpack_require__&lt;/code&gt;가 인자로 전달되고&lt;/li&gt;
&lt;li&gt;함수 내부의 원본 코드가 실행되며&lt;/li&gt;
&lt;li&gt;실행 결과가 &lt;code&gt;module.exports&lt;/code&gt;에 기록된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;5️⃣ exports를 반환하는 단계&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팩토리 함수 실행이 끝나면,&lt;/p&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;return module.exports;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 통해 &lt;code&gt;module.exports&lt;/code&gt;가 그대로 반환된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;__webpack_require__&lt;/code&gt;의 반환값은 곧 해당 모듈의 &lt;code&gt;module.exports&lt;/code&gt;다.&lt;/p&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;팩토리 함수 호출

&amp;rarr; module.exports에 값 기록
&amp;rarr; module.exports 반환&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(3) 한눈에 보는 실행 흐름&lt;/h3&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;__webpack_require__(1)
    &amp;darr;
[1] 캐시 확인
    &amp;darr;
[2] module 객체 생성
    &amp;darr;
[3] 캐시 선등록
    &amp;darr;
[4] 팩토리 실행
    &amp;darr;
[5] exports 반환&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 흐름은 CommonJS 모듈 시스템의 실행 규칙을 그대로 따른다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;캐시 확인&lt;/b&gt; &amp;rarr; 모듈의 단일 실행 보장&lt;/li&gt;
&lt;li&gt;&lt;b&gt;객체 생성&lt;/b&gt; &amp;rarr; 실행 결과를 저장할 컨테이너 준비&lt;/li&gt;
&lt;li&gt;&lt;b&gt;캐시 선등록&lt;/b&gt; &amp;rarr; 순환 참조 처리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;팩토리 실행&lt;/b&gt; &amp;rarr; 실제 코드 평가&lt;/li&gt;
&lt;li&gt;&lt;b&gt;exports 반환&lt;/b&gt; &amp;rarr; 실행 결과 전달&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3) 번들은 실제로 어떻게 실행되는가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 우리는 &lt;code&gt;__webpack_require__&lt;/code&gt;의 내부 동작을 살펴봤다.&lt;br /&gt;그렇다면 이 알고리즘은 언제부터 동작하기 시작할까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저가 번들을 로드하는 시점부터 ~ 모듈 실행이 시작되기까지의 흐름을 따라가보자.&lt;br /&gt;그 출발점은 브라우저가 번들을 실행하는 순간이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(1) 브라우저는 &amp;lsquo;웹팩&amp;rsquo;을 모른다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 &lt;code&gt;main.js&lt;/code&gt;라는 번들 파일이 있다고 가정해보자.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;script src=&quot;main.js&quot;&amp;gt;&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저는 웹팩이라는 개념을 알지 못한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단지 하나의 자바스크립트 파일을 실행할 뿐이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 파일이 로드되면 자바스크립트 엔진은 코드 상단부터 순차적으로 실행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 번들 파일의 가장 위에는 무엇이 있을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(2) 런타임 구조가 먼저 정의된다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;번들 상단에는 웹팩이 삽입한 런타임 코드가 위치한다.&lt;/p&gt;
&lt;pre class=&quot;arcade&quot;&gt;&lt;code&gt;var __webpack_modules__ = { ... };
var __webpack_module_cache__ = {};
function __webpack_require__(moduleId) { ... }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저는 이 코드를 일반 자바스크립트처럼 실행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이 시점에서 아직 어떤 모듈도 실행되지 않았다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모듈은 함수 형태로 &lt;code&gt;__webpack_modules__&lt;/code&gt;에 등록되어 있고&lt;/li&gt;
&lt;li&gt;캐시는 비어 있으며&lt;/li&gt;
&lt;li&gt;&lt;code&gt;__webpack_require__&lt;/code&gt;는 단지 정의만 되어 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 실제 애플리케이션 로직이 시작되기 전에 모듈 실행 환경이 초기화된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(3) 번들 마지막에서 entry 모듈이 호출된다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;번들 하단에는 entry 모듈을 실행하는 코드가 위치한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// Webpack 4
__webpack_require__(entryModuleId);

// Webpack 5
// __webpack_exec__를 쓰기도 함.
// 단, __webpack_exec__은 내부적으로 __webpack_require__를 사용하므로 동작은 유사함
__webpack_exec__(&quot;./src/index.js&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 애플리케이션 로직은 entry 모듈이 호출되는 순간부터 시작된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 호출이 이루어지면 다음과 같은 흐름이 전개된다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;entry 모듈 실행

&amp;rarr; 내부에서 __webpack_require__ 호출
&amp;rarr; 의존 모듈 require
&amp;rarr; 의존성 트리 재귀 실행&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 시점부터 &lt;code&gt;__webpack_require__&lt;/code&gt;의 5단계 알고리즘이&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;entry 모듈에서 시작해, 의존성 그래프를 따라 재귀적으로 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(4) 정리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;번들의 실행 흐름은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;브라우저가 main.js 실행
    &amp;darr;
런타임 구조 정의
    &amp;darr;
entry 모듈 호출
    &amp;darr;
__webpack_require__ 알고리즘 시작
    &amp;darr;
의존성 트리 재귀 실행&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저는 번들 파일을 로드하는 즉시 상단 코드부터 실행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 실제 애플리케이션 로직은 entry 모듈이 호출되는 시점부터 시작된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 실행의 출발점은 파일 로드가 아니라 &lt;b&gt;entry 호출&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4) 동적 import는 어디에 연결되는가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞에서 살펴봤듯이,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모든 모듈 실행은 &lt;code&gt;__webpack_require__&lt;/code&gt;를 통해 이루어지고&lt;/li&gt;
&lt;li&gt;entry 호출이 실행의 출발점이 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 &lt;code&gt;import()&lt;/code&gt;는 이 구조에서 어떤 위치를 차지할까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같은 코드가 있다고 가정해보자.&lt;/p&gt;
&lt;pre class=&quot;xl&quot;&gt;&lt;code&gt;import(&quot;./dynamic&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;겉보기에는 비동기 &lt;code&gt;require&lt;/code&gt;처럼 보이지만, 빌드 결과는 다음과 같이 변환된다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;__webpack_require__.e(&quot;dynamic&quot;)
.then(__webpack_require__.bind(__webpack_require__,&quot;./src/dynamic.js&quot;));&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동적 import의 실행은 두 단계로 이루어진다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;1단계: 청크 로딩 단계
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;__webpack_require__.e(&quot;dynamic&quot;)&lt;/code&gt;가 호출되어&lt;/li&gt;
&lt;li&gt;해당 청크 파일을 네트워크로 로드한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;2단계: 모듈 실행 단계
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;청크 로딩이 완료되면 &lt;code&gt;__webpack_require__&lt;/code&gt;가 호출되어 해당 모듈을 실행한다.&lt;/li&gt;
&lt;li&gt;즉, 동적 import 역시 &lt;code&gt;__webpack_require__&lt;/code&gt;로 연결된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(1) 첫 번째 단계 &amp;mdash; 청크 로딩&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;__webpack_require__.e(&quot;dynamic&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;.e&lt;/code&gt;는 지정된 청크를 로드하기 위한 비동기 요청을 시작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 아직 로드되지 않은 청크 파일에 대해 &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; 태그를 생성하고, 브라우저가 해당 파일을 다운로드하도록 한다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;script src=&quot;dynamic.bundle.js&quot;&amp;gt;&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 시점에서는 모듈 코드가 실행되지 않는다. 단지 청크 파일을 로드하는 단계다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(2) 두 번째 단계 &amp;mdash; 모듈 실행&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;청크 로딩이 완료되면 Promise가 resolve되고, 다음 코드가 실행된다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;.then(__webpack_require__.bind(__webpack_require__, &quot;./src/dynamic.js&quot;));&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 실제로 호출되는 것은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;__webpack_require__(&quot;./src/dynamic.js&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 동적 import도 최종적으로는 &lt;code&gt;__webpack_require__&lt;/code&gt;으로 모듈을 실행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 로드된 청크는 어디에 등록될까?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;청크 파일 내부에는 다음과 같은 코드가 포함되어 있다.&lt;/p&gt;
&lt;pre class=&quot;ruby&quot;&gt;&lt;code&gt;(self[&quot;webpackChunkapp&quot;] = self[&quot;webpackChunkapp&quot;] || []).push([
  [&quot;dynamic&quot;],
  {
    &quot;./src/dynamic.js&quot;: function(module, exports, __webpack_require__) {
      ...
    }
  }
]);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;겉보기에는 단순한 배열 &lt;code&gt;push&lt;/code&gt;처럼 보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 실제로는 다음과 같은 일이 일어난다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;1. push 호출
2. 청크 안에 들어 있던 모듈 정보 전달
3. 해당 모듈이 __webpack_modules__ 객체에 추가됨&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 새로 로드된 모듈의 팩토리 함수가 &lt;code&gt;__webpack_modules__&lt;/code&gt; 객체에 그대로 추가된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 이후의 과정은 정적 import와 동일하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(3) 전체 실행 흐름&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동적 import의 실행 흐름은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;import()
    &amp;darr;
__webpack_require__.e()  // 청크 로딩
    &amp;darr;
청크 파일 실행 (webpackChunk.push)
    &amp;darr;
__webpack_modules__에 모듈 추가
    &amp;darr;
__webpack_require__(moduleId)
    &amp;darr;
기존 require 알고리즘 수행&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 주목할 점은 실행 구조 자체는 변하지 않는다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동적 import도 최종적으로는 &lt;code&gt;__webpack_require__&lt;/code&gt;를 통해 모듈을 실행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;차이가 있다면, 모듈이 등록되는 시점이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;정적 import &amp;rarr; 빌드 시점에 모듈이 &lt;code&gt;__webpack_modules__&lt;/code&gt;에 포함됨&lt;/li&gt;
&lt;li&gt;동적 import &amp;rarr; 런타임에 청크가 로드되면서 모듈이 추가됨&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 동적 import는 모듈을 &lt;b&gt;나중에 등록&lt;/b&gt;할 뿐, 실행 방식은 기존 require 알고리즘과 동일하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지의 내용을 한 장으로 정리하면 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h1&gt;2. 지금까지의 내용 정리&lt;/h1&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(1) 빌드 타임 &amp;mdash; 모듈을 함수로 변환하는 단계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹팩은 빌드 과정에서 다음 작업을 수행한다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;소스 코드 분석
    &amp;darr;
ModuleGraph 생성 (의존성 분석)
    &amp;darr;
ChunkGraph 생성 (출력 전략 결정)
    &amp;darr;
모듈을 팩토리 함수로 변환
    &amp;darr;
__webpack_modules__ 객체 구성&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 시점에서 각 파일은 더 이상 &amp;lsquo;파일&amp;rsquo;이 아니다.&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;function(module,exports,__webpack_require__) {
// 원래 코드
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;형태의 팩토리 함수로 변환되어 &lt;code&gt;__webpack_modules__&lt;/code&gt;에 등록된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(2) 런타임 초기화 &amp;mdash; 실행 환경 준비&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저가 &lt;code&gt;main.js&lt;/code&gt;를 실행하면 먼저 런타임 구조가 정의된다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;__webpack_modules__ 생성
__webpack_module_cache__ 생성
__webpack_require__ 정의
    &amp;darr;
entry 모듈 호출&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때부터 모듈 실행이 시작된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(3) require 알고리즘 &amp;mdash; 모듈 실행 규칙&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;__webpack_require__(moduleId)&lt;/code&gt;가 호출되면 다음 순서가 적용된다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;[1] 캐시 확인
    &amp;darr;
[2] module 객체 생성
    &amp;darr;
[3] 캐시 선등록
    &amp;darr;
[4] 팩토리 함수 실행
    &amp;darr;
[5] module.exports 반환&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조를 통해:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모듈은 한 번만 실행되고&lt;/li&gt;
&lt;li&gt;실행 결과는 공유되며&lt;/li&gt;
&lt;li&gt;순환 참조가 안전하게 처리된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(4) 동적 import &amp;mdash; 모듈을 나중에 등록하는 방식&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동적 import는 실행 구조를 바꾸지 않는다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;import()
    &amp;darr;
__webpack_require__.e()   // 청크 로딩
    &amp;darr;
webpackChunk.push()      // 모듈 추가
    &amp;darr;
__webpack_require__()     // 기존 알고리즘 실행&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;차이는 단 하나다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정적 import는 빌드 시점에 모듈이 등록되고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동적 import는 런타임에 청크가 로드되면서 모듈이 추가된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(5) 전체 구조 한눈에 보기&lt;/h3&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;[빌드 타임]
파일 &amp;rarr; 팩토리 함수 &amp;rarr; __webpack_modules__ 구성

[런타임]
entry 호출 &amp;rarr; __webpack_require__ &amp;rarr; 의존성 트리 재귀 실행

[동적 import]
청크 로딩 &amp;rarr; 모듈 추가 &amp;rarr; 동일 require 알고리즘 적용&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h1&gt;3. 마치며&amp;hellip;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 시간에는 웹팩 번들이 런타임에서 어떻게 실행되는지 알아보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹팩은 모듈을 파일 그대로 두지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빌드 타임에 각 모듈을 &lt;b&gt;팩토리 함수&lt;/b&gt;로 변환하고, 런타임에서는 이를 &lt;code&gt;__webpack_require__&lt;/code&gt;를 통해 실행한다.&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;__webpack_require__(moduleId)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 함수는&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모듈을 한 번만 실행하고&lt;/li&gt;
&lt;li&gt;실행 결과를 캐시하며&lt;/li&gt;
&lt;li&gt;의존성을 재귀적으로 해결한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동적 import도 예외는 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;청크를 나중에 로드할 뿐, 실행 구조는 동일하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 번들의 실행 흐름은 하나로 정리된다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;entry 호출
&amp;rarr; require 실행
&amp;rarr; 팩토리 함수 실행
&amp;rarr; module.exports 반환&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹팩 런타임은 이 단순한 구조 위에서 동작하는 걸 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 시간에는 Webpack의 또 다른 핵심 기능인&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HMR(Hot Module Replacement)이 어떻게 런타임과 모듈 캐시 위에서 동작하는지 살펴보겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>라이브러리 파헤치기</category>
      <category>esmodule</category>
      <category>moduleBundler</category>
      <category>webpack</category>
      <category>WebpackChunk</category>
      <category>WebpackRuntime</category>
      <category>__webpack_require__</category>
      <category>모듈시스템</category>
      <category>웹팩</category>
      <category>자바스크립트모듈</category>
      <author>GicoMomg</author>
      <guid isPermaLink="true">https://mong-blog.tistory.com/242</guid>
      <comments>https://mong-blog.tistory.com/entry/Webpack-%EC%9B%B9%ED%8C%A9-%EB%9F%B0%ED%83%80%EC%9E%84%EC%9D%98-%EC%A0%95%EC%B2%B4-%E2%80%94-%EB%B8%8C%EB%9D%BC%EC%9A%B0%EC%A0%80%EC%97%90-%EB%AA%A8%EB%93%88-%EC%8B%9C%EC%8A%A4%ED%85%9C%EC%9D%84-%EC%8B%AC%EB%8A%94%EB%8B%A4%EA%B3%A0#entry242comment</comments>
      <pubDate>Fri, 27 Feb 2026 23:39:01 +0900</pubDate>
    </item>
    <item>
      <title>[Webpack] 번들링의 원리: 수천 개의 모듈은 어떻게 묶을까?</title>
      <link>https://mong-blog.tistory.com/entry/Webpack-%EB%B2%88%EB%93%A4%EB%A7%81%EC%9D%98-%EC%9B%90%EB%A6%AC-%EC%88%98%EC%B2%9C-%EA%B0%9C%EC%9D%98-%EB%AA%A8%EB%93%88%EC%9D%80-%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%AC%B6%EC%9D%84%EA%B9%8C</link>
      <description>&lt;h1&gt;0. 들어가며: 왜 파일을 그냥 올리면 안 될까?&lt;/h1&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;60%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dWbUcp/dJMcagj3ugq/mD9zvVMikksVBOlVb7ydck/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dWbUcp/dJMcagj3ugq/mD9zvVMikksVBOlVb7ydck/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dWbUcp/dJMcagj3ugq/mD9zvVMikksVBOlVb7ydck/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdWbUcp%2FdJMcagj3ugq%2FmD9zvVMikksVBOlVb7ydck%2Fimg.png&quot; width=&quot;60%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;프로젝트를 시작하면 우리는 자연스럽게 webpack이나 vite 같은 빌드 도구를 설정한다.&lt;/p&gt;
&lt;p&gt;너무 당연하게 쓰다 보니, 막상 누군가 근본적인 질문을 던지면 대답이 애매해질 때가 있다.&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;&lt;em&gt;“왜 파일을 그냥 올리면 안 되고, webpack을 써서 ‘번들링’을 거쳐야 하나요?”&lt;/em&gt;&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;이번 시간에는 webpack의 수 많은 기능 중에서도 ‘번들링(Bundling)’을 코드와 함께 살펴보겠다.&lt;/p&gt;
&lt;p&gt;이 글을 읽고나서 webpack의 번들링이 “왜 필요하고” → “내부에서 어떻게 작동하며” → “번들링 결과물의 형태를 이해”할 수 있을 것이다.&lt;/p&gt;
&lt;p&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;  핵심 요약&lt;br/&gt;1. webpack은 브라우저가 모르는 모듈 시스템(&lt;code&gt;import&lt;/code&gt;/&lt;code&gt;require&lt;/code&gt;)을 이해시켜준다.&lt;br/&gt;2. webpack은 의존성 그래프를 기반으로 안 쓰는 코드는 버리고 중복은 합친다.&lt;br/&gt;3. webpack은 수천 개의 파일을 보내는 대신 적당한 덩어리로 묶어 네트워크 오버헤드를 줄인다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;h1&gt;1. 왜 번들링이 필요할까?&lt;/h1&gt;
&lt;p&gt;우리는 JS 파일을 역할에 따라 여러 개로 분리하고, 각 파일을 &lt;code&gt;import&lt;/code&gt;/&lt;code&gt;require()&lt;/code&gt;를 사용해 호출한다.&lt;/p&gt;
&lt;p&gt;하지만 이 문법은 브라우저가 “태생부터” 이해하던 문법이 아니다.&lt;/p&gt;
&lt;br/&gt;

&lt;p&gt;그래서 이 문법을 이해하기 위해 개발자와 브라우저 사이에 통역 역할이 필요했고, 그 역할을 번들러가 맡았다.&lt;/p&gt;
&lt;p&gt;물론 요즘 브라우저는 &lt;code&gt;import/export&lt;/code&gt;를 네이티브로 지원하고, &lt;code&gt;&amp;lt;script type=&amp;quot;module&amp;quot;&amp;gt;&lt;/code&gt;만 써도 ESM을 그대로 실행할 수 있다.  &lt;/p&gt;
&lt;p&gt;그럼 이렇게 생각할 수 있을 것이다. &lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;❓ 번들러가 import/require 문법을 해석해주는 역할이라면, 요즘에는 필요없지 않나?&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;그럼에도 번들러가 “여전히” 필요한 이유는, 번들러의 역할이 문법 해석에 그치지 않기 때문이다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;번들러는 구형 브라우저/특수 런타임 호환을 지원한다.&lt;/li&gt;
&lt;li&gt;브라우저가 기본으로 해주지 않는 node_modules 해석(bare specifier), 경로 별칭, 조건부 exports를 처리한다.&lt;/li&gt;
&lt;li&gt;TS/JSX 변환, 폴리필 전략, 그리고 여러 최적화(트리셰이킹/중복 제거/청크/캐시)를 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;h2&gt;1) 번들링이 필요한 3가지 이유&lt;/h2&gt;
&lt;h3&gt;(1) 모듈 통역: 브라우저가 못 알아듣는 문법을 ‘실행 가능한 코드’로 바꾼다&lt;/h3&gt;
&lt;p&gt;과거 브라우저의 실행 모델은 단순했다.&lt;/p&gt;
&lt;p&gt;HTML에 적힌 &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; 태그를 위에서 아래로 실행할 뿐, 파일 내부에서 다른 파일을 가져오는 능력은 없었다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;개발자는 “이 파일은 저 파일이 필요해”라며 &lt;code&gt;import&lt;/code&gt;를 작성한다.&lt;/li&gt;
&lt;li&gt;하지만 (특히 구형 환경에서는) 브라우저가 이 관계를 이해하지 못한다.&lt;/li&gt;
&lt;li&gt;번들러는 모듈 간 연결 관계를 읽고, 브라우저가 실행 가능한 형태로 변환/결합해 결과물을 만든다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;즉, 번들러는 개발자가 쓰는 “모듈 언어”를 브라우저의 “실행 언어”로 바꿔주는 통역기다.&lt;/p&gt;
&lt;br/&gt;

&lt;h3&gt;(2) 스코프 보호: 전역 오염을 막고 각 파일의 공간을 만든다&lt;/h3&gt;
&lt;p&gt;번들링 없이 파일 100개를 &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt;로 연결하면, 변수들은 어떻게 될까?&lt;/p&gt;
&lt;p&gt;대부분 &lt;code&gt;window&lt;/code&gt;라는 전역 공간에 모이면서 충돌한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A 파일의 &lt;code&gt;const name&lt;/code&gt;과 B 파일의 &lt;code&gt;const name&lt;/code&gt;이 같은 전역에서 만나면&lt;/li&gt;
&lt;li&gt;예상치 못한 덮어쓰기/충돌이 발생하며 전역 오염(Global pollution)이 발생한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;번들러는 이 문제를 구조적으로 막는다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;모듈을 함수 스코프(Function Scope)로 감싸,&lt;/li&gt;
&lt;li&gt;파일별로 독립된 실행 컨텍스트(각자의 공간)를 만들어준다.&lt;/li&gt;
&lt;li&gt;그래서 변수명이 겹쳐도 안전하고, 코드 분리/재사용이 가능해진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;h3&gt;(3) 전달 최적화: ‘전체를 보고’ 가볍고 빠르게 보낸다&lt;/h3&gt;
&lt;p&gt;번들러는 엔트리부터 시작해 프로젝트 전체를 순회하며 모듈 간 의존 관계를 의존성 그래프(Dependency Graph) 형태로 구성한다.&lt;/p&gt;
&lt;p&gt;이 그래프는 하나의 빌드 실행 단위인 &lt;code&gt;Compilation&lt;/code&gt;마다 생성된다.&lt;/p&gt;
&lt;p&gt;번들러가 배포 단계에서 수행하는 대부분의 최적화는 이 의존성 그래프를 기반으로 이루어진다.&lt;/p&gt;
&lt;p&gt;즉, 프로젝트 전체 구조를 담은 &lt;strong&gt;“전체 지도”&lt;/strong&gt; 가 있어야 어떤 코드를 제거하고, 어디를 분리하고, 무엇을 공유할지 판단할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;h2&gt;2) 번들링은 어떤 최적화를 할까?&lt;/h2&gt;
&lt;h3&gt;(1) 그래프 기반 최적화: Tree-shaking / Deduplication&lt;/h3&gt;
&lt;h4&gt;1-1. Tree-shaking: 안 쓰는 코드는 제거한다&lt;/h4&gt;
&lt;p&gt;예를 들어 A 라이브러리에서 “캐릭터가 춤추는 기능” 하나만 쓴다고 해보자.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;번들러가 없다면:&lt;/strong&gt; 기능 하나 쓰려고 A 라이브러리 전체를 내려받는다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;번들러가 있다면:&lt;/strong&gt; 그래프를 보고 “이 기능만 쓰네?”라고 판단해 필요한 코드만 남긴다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;그래서 이를 “나무를 흔들어 마른 잎을 떨군다”는 비유로 &lt;code&gt;Tree-shaking&lt;/code&gt;이라고 부른다.&lt;/p&gt;
&lt;br/&gt;

&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;다만 트리 셰이킹은 의존성 그래프가 있다고 해서 자동으로 적용되는 기능은 아니다.&lt;br/&gt;안전한 코드 제거를 위해서는 아래 조건들이 함께 충족되어야 한다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;&lt;strong&gt;조건1. ESM(&lt;code&gt;import / export&lt;/code&gt;) 기반일수록 코드 제거가 유리하다&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// ❌ Tree-shaking이 어려운 경우 (CommonJS)

const { funcA } =require(&amp;#39;./utils&amp;#39;);&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;CommonJS는 &lt;code&gt;require()&lt;/code&gt;가 실행 시점(runtime) 에 평가된다&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;exports&lt;/code&gt; 객체의 구조도 런타임에 결정되므로&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;번들러는 “어떤 함수가 실제로 쓰이는지”를 빌드 타임에 확정할 수 없다&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;반면 ESM은 다르다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// ✅ Tree-shaking이 유리한 경우 (ESM)

import { funcA }from&amp;#39;./utils&amp;#39;;

// utils.js에서 funcB가 사용되지 않았다면 제거 가능&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;ESM은 import/export 구조가 정적으로 고정되어 있고&lt;/li&gt;
&lt;li&gt;번들러가 “이 파일에서는 funcA만 쓰인다”는 사실을 빌드 타임에 분석할 수 있다&lt;/li&gt;
&lt;li&gt;그래서 Tree-shaking은 ESM 기반일수록 정확하게 동작한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;조건 2. production 모드에서 &lt;code&gt;usedExports&lt;/code&gt; 분석 + minimizer가 함께 동작해야 한다&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Tree-shaking은 보통 2단계로 이루어진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;1. usedExports 분석
    - “어떤 export가 실제로 사용되었는지” 표시만 해둔다
    - 이 단계만으로는 코드가 실제로 삭제되지는 않는다
2. minimizer(Terser 등)
    - “사용되지 않는 export”를 실제 코드에서 제거한다&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;그래서 개발 모드에서는 코드가 남아 있고, production 빌드에서만 코드 제거가 일어나는 경우가 많다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;조건 3. 모듈에 부수 효과(side effect)가 많을수록 제거 판단이 어려워진다&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;여기서 부수 효과(side effect) 란, 전역 상태를 변경하거나 외부 환경에 영향을 주는 코드를 의미한다.&lt;/li&gt;
&lt;li&gt;예를 들면 다음과 같다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// 부수 효과가 있는 코드

console.log(&amp;#39;loaded&amp;#39;);
window.__CONFIG__ = { mode:&amp;#39;prod&amp;#39; };&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;이 파일은 import되기만 해도 콘솔 출력이 발생하고 전역 값이 변경된다&lt;/li&gt;
&lt;li&gt;즉, “사용하지 않더라도 실행 자체가 의미를 가진다”&lt;/li&gt;
&lt;li&gt;이런 경우 번들러는 “혹시 이 코드가 실행되어야 하는 건 아닐까?” 라고 판단해 안전하게 제거하지 못한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li&gt;반대로 아래 코드는 다르다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// 부수 효과가 없는 코드
export function add(a, b) {
return a + b;
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;함수가 호출되지 않으면 아무 일도 일어나지 않는다&lt;/li&gt;
&lt;li&gt;그래서 사용되지 않는다면 안전하게 제거할 수 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;h4&gt;1-2. Deduplication: 같은 모듈은 한 번만 포함한다&lt;/h4&gt;
&lt;p&gt;여러 파일이 동일한 모듈 B를 필요로 할 때,&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;번들러가 없다면:&lt;/strong&gt; 중복 포함이 생겨 사용자는 같은 코드를 여러 번 받는다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;번들러가 있다면:&lt;/strong&gt; ‘B는 한 번만 포함하고, 공통 청크(Shared chunk)로 분리하게’ 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;h3&gt;(2) 네트워크 관점 최적화: 택배 1,000개 vs 큰 상자 5개&lt;/h3&gt;
&lt;p&gt;실무에서 번들링이 필요한 이유 중 하나는 네트워크 오버헤드(Overhead) 때문이다.&lt;/p&gt;
&lt;p&gt;요청/응답에는 실제 콘텐츠 외에도 생각보다 많은 고정 비용이 함께 따라온다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;❓ 왜 파일을 잘게 나누면 느려질까?&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;파일 요청이 늘어날수록 다음과 같은 비용이 누적된다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;핸드셰이크(Handshake)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;  요청마다 연결 및 협상 과정이 발생한다. 파일이 1,000개라면, 이 과정도 1,000번 반복된다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;동시 연결 제한(Concurrency limit)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;  브라우저는 한 번에 처리할 수 있는 요청 수가 제한되어 있다.&lt;br&gt;  이로 인해 요청이 순차적으로 쌓이는 Waterfall 현상이 발생한다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;헤더 오버헤드(Header overhead)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;  파일이 작을수록, 실제 데이터 대비 요청·응답 헤더가 차지하는 비중이 상대적으로 커진다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;HTTP/2면 이런 걱정은 안 해도 되지 않나?&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;HTTP/2의 멀티플렉싱 덕분에 동시 요청 처리와 병목은 크게 완화되었다.&lt;br&gt;(멀티플렉싱은 하나의 연결 위에서 여러 요청과 응답을 동시에 주고받는 방식임)&lt;/p&gt;
&lt;p&gt;하지만 이는 “번들링이 더 이상 필요 없다”는 뜻은 아니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;각 요청에는 여전히 라운드트립 및 처리 오버헤드가 존재한다&lt;/li&gt;
&lt;li&gt;파일을 내려받은 이후에도 브라우저는 Parse → Compile → Execute 과정을 모두 수행해야 한다&lt;/li&gt;
&lt;li&gt;작은 파일이 지나치게 많아지면 경우에 따라 압축 효율이 오히려 떨어질 수 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;결국 결론은 하나다. 너무 잘게 쪼개도 문제고, 전부 합쳐도 문제다. &lt;/p&gt;
&lt;p&gt;그래서 번들링으로 “적당히 묶는 게 중요”하다.&lt;/p&gt;
&lt;p&gt;&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;h2&gt;3) 웹팩이 정의한 ‘적당히’ 묶는 단위는?&lt;/h2&gt;
&lt;p&gt;웹팩은 아무 기준 없이 파일을 묶지 않는다. &lt;/p&gt;
&lt;p&gt;&lt;code&gt;SplitChunksPlugin&lt;/code&gt; 의 기본 설정을 보면, 웹팩이 생각하는 “적당함”의 기준이 드러난다.&lt;/p&gt;
&lt;h3&gt;(1) 웹팩이 청크를 나누는 기준&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DaYEv/dJMcacoqmf7/CSwWlEpIv2OnnvLRqwQdj0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DaYEv/dJMcacoqmf7/CSwWlEpIv2OnnvLRqwQdj0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DaYEv/dJMcacoqmf7/CSwWlEpIv2OnnvLRqwQdj0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDaYEv%2FdJMcacoqmf7%2FCSwWlEpIv2OnnvLRqwQdj0%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&lt;a href=&quot;https://webpack.kr/plugins/split-chunks-plugin/&quot;&gt;https://webpack.kr/plugins/split-chunks-plugin/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;용량 기준 (&lt;code&gt;minSize&lt;/code&gt;)&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;너무 작은 파일은 분리할수록 네트워크 오버헤드가 커진다.&lt;/li&gt;
&lt;li&gt;일정 크기(예: 20KB) 미만의 조각은 굳이 나누지 않고 합친다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;개수 제한 (&lt;code&gt;maxRequests&lt;/code&gt;)&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;한 화면에서 동시에 로드되는 파일 수가 과도해지지 않도록&lt;/li&gt;
&lt;li&gt;초기 로딩 및 비동기 요청 개수에 상한을 둔다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;재사용성 (Common Module)&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;여러 페이지에서 공통으로 사용되는 코드는 하나의 청크로 분리해&lt;/li&gt;
&lt;li&gt;중복 다운로드를 방지하고 캐싱 효율을 높인다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;변경 빈도 (Vendor 분리)&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;자주 변경되는 애플리케이션 코드와&lt;/li&gt;
&lt;li&gt;거의 변하지 않는 라이브러리(&lt;code&gt;node_modules&lt;/code&gt;)를 분리해 캐시 무효화 범위를 최소화한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;  그럼 우리 서비스의 청크 단위가 적절한지 어떻게 알 수 있을까? 4가지를 체크해보면 알 수 있다&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;ul&gt;
&lt;li&gt;초기 진입 페이지에서 “지금 안 쓰는 코드”가 같이 내려오지 않는가?&lt;/li&gt;
&lt;li&gt;공통 라이브러리가 페이지마다 중복 다운로드되지 않는가?&lt;/li&gt;
&lt;li&gt;청크가 너무 잘게 쪼개져서 로딩 딜레이가 체감되지는 않는가?&lt;/li&gt;
&lt;li&gt;내 코드 변경이 라이브러리(Vendor) 캐시까지 무효화시키지는 않는가?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;웹팩은 이처럼 여러 최적화를 위해 파일을 묶는다. &lt;/p&gt;
&lt;p&gt;그렇다면 웹팩은 내부적으로 어떤 순서와 구조로 이 결정을 수행할까? 다음 챕터에서 알아보자!&lt;/p&gt;
&lt;p&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;h1&gt;2. 빌드 파이프라인: ‘지도를 그리고, 짐을 싸서, 내보내기’&lt;/h1&gt;
&lt;p&gt;앞서 우리는 번들링이 왜 필요한지를 살펴봤다. &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;모듈을 해석하기 위해서&lt;/li&gt;
&lt;li&gt;스코프를 안전하게 보호하기 위해서&lt;/li&gt;
&lt;li&gt;그리고 네트워크 관점에서 효율적인 전송을 위해서.&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;p&gt;이번 챕터에서는 웹팩이 &lt;code&gt;entry&lt;/code&gt;를 입력으로 받아 &lt;code&gt;dist&lt;/code&gt;에 결과물을 만들기까지, &lt;/p&gt;
&lt;p&gt;어떤 단계와 사고 과정을 거치는지 빌드 파이프라인 관점에서 정리해본다.&lt;/p&gt;
&lt;p&gt;웹팩의 빌드 과정은 크게 &lt;strong&gt;4단계&lt;/strong&gt;로 나눌 수 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;전체 흐름&lt;/strong&gt;: Entry → ModuleGraph → ChunkGraph → Emit&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;핵심 설계&lt;/strong&gt;: 먼저 “파일 간 연결 관계(논리)”를 확정하고, 그 다음 “배포 단위(물리)”를 만든다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;유연성&lt;/strong&gt;: “사실(코드 구조)”과 “전략(배포 방식)”을 분리했기에, 코드를 수정하지 않고 설정만으로 번들링·코드 분할·캐싱 전략을 자유롭게 바꿀 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;h2&gt;1) 먼저, 웹팩 안에서 어떤 객체들이 일을 할까?&lt;/h2&gt;
&lt;p&gt;빌드 파이프라인을 이해할 때 가장 헷갈리는 지점 중 하나는&lt;/p&gt;
&lt;p&gt;우리가 흔히 &lt;strong&gt;“웹팩이 뭔가를 한다”&lt;/strong&gt; 라고 뭉뚱그려 말한다는 점이다.&lt;/p&gt;
&lt;p&gt;실제로는 하나의 주체가 모든 일을 처리하는 것이 아니라, 여러 객체들이 빌드를 완성한다.&lt;/p&gt;
&lt;p&gt;아래는 이번 챕터에서 등장하는 주요 객체들과 그 역할을 정리한 표다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;분류&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;역할&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Compiler&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;웹팩 실행부터 종료까지를 총괄하는 상위 관리자. 전체 프로세스에서 &lt;strong&gt;단 하나만 존재&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Compilation&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;“이번 빌드 1회”의 실무 책임자. 빌드(또는 리빌드)마다 &lt;strong&gt;새로 생성&lt;/strong&gt;됨&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ModuleGraph&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;모듈 간 의존 관계, 즉 &lt;strong&gt;논리적 구조&lt;/strong&gt;를 기록하는 그래프&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ChunkGraph&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;어떤 모듈을 어떤 청크에 담을지 결정하는 &lt;strong&gt;배포 단위 그래프&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Chunk&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;최종적으로 생성될 &lt;strong&gt;파일 단위에 대응되는 배포 컨테이너&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;여기서 꼭 짚고 가야 할 핵심은 하나다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;Compilation&lt;/code&gt;은 이번 빌드에서 생성되는 모든 데이터의 중심이다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;즉, “그래프를 만든다”, “청크를 나눈다”, “파일을 만든다”라는 말은&lt;/p&gt;
&lt;p&gt;  대부분 &lt;code&gt;Compilation&lt;/code&gt; 내부 상태를 채워나가는 과정을 의미한다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이제 이 객체들이 어떤 순서로 생성되고, 어떤 정보를 주고받는지 빌드 단계별로 살펴보자.&lt;/p&gt;
&lt;p&gt;&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;h2&gt;2) 번들링의 4단계 흐름&lt;/h2&gt;
&lt;h3&gt;(1) Entry: “어디서부터 출발할지”를 고정한다&lt;/h3&gt;
&lt;p&gt;Entry는 Webpack에게 프로젝트의 시작점을 알려주는 단계다. &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;entry: &amp;quot;./src/index.js&amp;quot;&lt;/code&gt; 설정은 “이 파일을 기준으로, 연결된 모든 모듈을 추적하라”는 의미다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;중요한 점은 이 단계가 파일을 합치기 시작하는 단계가 아니라&lt;/p&gt;
&lt;p&gt;  의존성 추적을 시작할 기준점을 정의하는 단계라는 것이다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;h3&gt;(2) ModuleGraph: “누가 누구를 필요로 하는지”를 기록한다 (논리 지도)&lt;/h3&gt;
&lt;p&gt;웹팩은 엔트리부터 소스 파일을 읽어가며 &lt;code&gt;import&lt;/code&gt; / &lt;code&gt;require&lt;/code&gt; 등을 분석해 모듈 간 관계도를 만든다.&lt;/p&gt;
&lt;p&gt;이 과정은 대략 다음 순서로 진행된다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Parsing :&lt;/strong&gt; 소스 코드를 분석해 AST(Abstract Syntax Tree)로 변환한다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dependency 추출 :&lt;/strong&gt; 어떤 모듈이 무엇을 참조하는지 의존성을 수집한다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ModuleGraph 완성 :&lt;/strong&gt; “모듈 노드 + 의존성 엣지”로 구성된 논리 그래프를 만든다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;아직 이 단계에서는 “파일을 몇 개로 나눌지” 같은 배포 전략은 전혀 결정하지 않는다.&lt;/p&gt;
&lt;p&gt;ModuleGraph는 오직 코드 구조가 어떻게 생겼는지만 담는다.&lt;/p&gt;
&lt;br/&gt;

&lt;h3&gt;(3) ChunkGraph: “어떻게 묶어서 배포할지”를 결정한다 (포장 전략)&lt;/h3&gt;
&lt;p&gt;여기서부터 전략(Strategy) 이 개입된다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;웹팩은 ModuleGraph를 바탕으로 “청크(Chunk)라는 배포 상자”를 만들고&lt;/li&gt;
&lt;li&gt;각 모듈을 어느 청크에 담을지에 대한 매핑을 구성한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;p&gt;ChunkGraph는 단순한 “청크 목록”이 아니라, 다음과 같은 정보들을 함께 가진 구조다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;모듈 → 청크 매핑 (N:M) :&lt;/strong&gt; 하나의 모듈이 여러 청크에 포함될 수 있다 (공유 청크, 코드 스플리팅)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;청크 → 모듈 매핑 :&lt;/strong&gt; 특정 파일(청크)에 어떤 모듈들이 들어가는지&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;청크 간 관계 :&lt;/strong&gt; 부모/자식 구조, 특히 &lt;code&gt;import()&lt;/code&gt;로 생성되는 async 청크 연결&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;h3&gt;(4) Emit: “결정된 배치(ChunkGraph)를 실제 파일로 출력한다” (출고)&lt;/h3&gt;
&lt;p&gt;마지막으로 웹팩은 메모리 안에 있던 결과를 실제 파일로 찍어낸다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;dist/main.js&lt;/code&gt;, &lt;code&gt;dist/1.js&lt;/code&gt; 같은 파일이 이 단계에서 만들어진다.&lt;/li&gt;
&lt;li&gt;프로덕션 빌드에서는 &lt;code&gt;main.[contenthash].js&lt;/code&gt; 형태로 해시가 붙어 출력된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;h2&gt;3) ModuleGraph → ChunkGraph 빌드 중간 산출물 살펴보기&lt;/h2&gt;
&lt;p&gt;앞에서 살펴본 개념을 실제 빌드 중간 산출물의 형태로 연결해보자.&lt;/p&gt;
&lt;p&gt;말로만 설명하면 추상적으로 느껴질 수 있기에, 이번에는 단순화한 JSON 데이터로&lt;/p&gt;
&lt;p&gt;ModuleGraph와 ChunkGraph가 각각 무엇을 담는지 확인해본다.&lt;/p&gt;
&lt;br/&gt;

&lt;h3&gt;(1) 기본 예시로 살펴보기&lt;/h3&gt;
&lt;h4&gt;예시 프로젝트 구조&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;src/
  index.js// entry
  App.js
  util.js&lt;/code&gt;&lt;/pre&gt;&lt;h4&gt;1-1. ModuleGraph: “관계 데이터”만 담긴다&lt;/h4&gt;
&lt;p&gt;ModuleGraph는 모듈 간 논리적 관계만을 기록한다.&lt;/p&gt;
&lt;p&gt;아직 배포 단위나 파일 개수에 대한 정보는 없다. 오직 다음 정보만 담는다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;어떤 모듈이 존재하는지&lt;/li&gt;
&lt;li&gt;어떤 모듈이 어떤 모듈을 참조하는지&lt;/li&gt;
&lt;li&gt;그 참조가 동기인지 / 비동기인지&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &amp;quot;modules&amp;quot;: [
    // ModuleGraph에서 index.js를 가리키는 모듈 노드
    { &amp;quot;id&amp;quot;: &amp;quot;M0&amp;quot;, &amp;quot;resource&amp;quot;: &amp;quot;/src/index.js&amp;quot; },
    { &amp;quot;id&amp;quot;: &amp;quot;M1&amp;quot;, &amp;quot;resource&amp;quot;: &amp;quot;/src/App.js&amp;quot; },
    { &amp;quot;id&amp;quot;: &amp;quot;M2&amp;quot;, &amp;quot;resource&amp;quot;: &amp;quot;/src/util.js&amp;quot; }
  ],
  &amp;quot;dependencies&amp;quot;: [
    // index.js(M0)가 App.js(M1)를 동기적으로 import/require 함
    { &amp;quot;from&amp;quot;: &amp;quot;M0&amp;quot;, &amp;quot;to&amp;quot;: &amp;quot;M1&amp;quot;, &amp;quot;request&amp;quot;: &amp;quot;./App&amp;quot;, &amp;quot;async&amp;quot;: false },
    { &amp;quot;from&amp;quot;: &amp;quot;M1&amp;quot;, &amp;quot;to&amp;quot;: &amp;quot;M2&amp;quot;, &amp;quot;request&amp;quot;: &amp;quot;./util&amp;quot;, &amp;quot;async&amp;quot;: false }
  ],
  // ModuleGraph 탐색을 시작하는 진입 모듈(entry)
  &amp;quot;entryModules&amp;quot;: [&amp;quot;M0&amp;quot;]
}&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;h4&gt;1-2. ChunkGraph: “물리 배치 + 매핑”이 추가된다&lt;/h4&gt;
&lt;p&gt;이제 ModuleGraph를 바탕으로 배포 단위(Chunk) 가 만들어진다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  // 생성된 배포 단위(청크) 목록 — 여기서는 main 청크 하나만 존재
  &amp;quot;chunks&amp;quot;: [
    { &amp;quot;id&amp;quot;: &amp;quot;C0&amp;quot;, &amp;quot;name&amp;quot;: &amp;quot;main&amp;quot;, &amp;quot;initial&amp;quot;: true }
  ],

  // 각 청크에 어떤 모듈들이 포함되는지에 대한 매핑
  &amp;quot;chunkToModules&amp;quot;: {
    &amp;quot;C0&amp;quot;: [&amp;quot;M0&amp;quot;, &amp;quot;M1&amp;quot;, &amp;quot;M2&amp;quot;]
  },

  // 각 모듈이 어느 청크(파일)에 속해 있는지에 대한 역방향 매핑
  &amp;quot;moduleToChunks&amp;quot;: {
    &amp;quot;M0&amp;quot;: [&amp;quot;C0&amp;quot;],
    &amp;quot;M1&amp;quot;: [&amp;quot;C0&amp;quot;],
    &amp;quot;M2&amp;quot;: [&amp;quot;C0&amp;quot;]
  },

  // 청크 간 부모·자식 관계 — async 청크가 없으므로 비어 있음
  &amp;quot;chunkRelations&amp;quot;: []
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;모든 모듈이 하나의 &lt;code&gt;main&lt;/code&gt; 청크에 묶였고&lt;/li&gt;
&lt;li&gt;어떤 모듈이 어떤 파일로 배포되는지가 명확해졌다&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;h3&gt;(2) import()가 등장한 경우 예시 살펴보기&lt;/h3&gt;
&lt;p&gt;이번에는 entry에서 &lt;code&gt;App&lt;/code&gt;을 동적 import로 바꿔보자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// index.js

import(&amp;quot;./App&amp;quot;).then(({ default: App }) =&amp;gt;App())&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2-1. ModuleGraph 변화: 비동기 의존성만 추가된다&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &amp;quot;dependencies&amp;quot;: [
    // M0(index.js)가 M1(App.js)를 비동기 import()로 참조함
    { &amp;quot;from&amp;quot;: &amp;quot;M0&amp;quot;, &amp;quot;to&amp;quot;: &amp;quot;M1&amp;quot;, &amp;quot;request&amp;quot;: &amp;quot;./App&amp;quot;, &amp;quot;async&amp;quot;: true }
  ]
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ModuleGraph의 변화는 단순하다. “이 의존성은 async다”라는 사실 하나만 추가된다.&lt;/p&gt;
&lt;br/&gt;

&lt;h4&gt;2-2. ChunkGraph 변화: 청크가 분리되고 관계가 생긴다&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &amp;quot;chunks&amp;quot;: [
     // 초기 진입 시 바로 로드되는 main 청크
    { &amp;quot;id&amp;quot;: &amp;quot;C0&amp;quot;, &amp;quot;name&amp;quot;: &amp;quot;main&amp;quot;, &amp;quot;initial&amp;quot;: true },

     // import()로 분리된 비동기 청크 (App 관련 코드)
    { &amp;quot;id&amp;quot;: &amp;quot;C1&amp;quot;, &amp;quot;name&amp;quot;: &amp;quot;src_App_js&amp;quot;, &amp;quot;initial&amp;quot;: false, &amp;quot;async&amp;quot;: true }
  ],

  &amp;quot;chunkToModules&amp;quot;: {
    // main 청크에는 entry 모듈만 포함
    &amp;quot;C0&amp;quot;: [&amp;quot;M0&amp;quot;],  

    // App 모듈과 그 하위 의존성이 비동기 청크로 이동
    &amp;quot;C1&amp;quot;: [&amp;quot;M1&amp;quot;, &amp;quot;M2&amp;quot;]
  },

  &amp;quot;chunkRelations&amp;quot;: [
     // main 실행 중 import()를 만나면 C1 청크를 로드하라는 관계 정보
    { &amp;quot;parent&amp;quot;: &amp;quot;C0&amp;quot;, &amp;quot;child&amp;quot;: &amp;quot;C1&amp;quot;, &amp;quot;reason&amp;quot;: &amp;quot;import()&amp;quot; }
  ]
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이제 구조가 분명해진다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;main&lt;/code&gt; 청크에는 entry 모듈만 남고&lt;/li&gt;
&lt;li&gt;&lt;code&gt;App&lt;/code&gt;과 &lt;code&gt;util&lt;/code&gt;은 비동기 청크로 분리된다&lt;/li&gt;
&lt;li&gt;두 청크 사이에는 &lt;code&gt;import()&lt;/code&gt;로 연결된 부모–자식 관계가 기록된다&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;  여기서 한 가지 질문이 떠오른다.&lt;br/&gt;“그냥 파일을 합치면 되는데, 왜 웹팩은 ModuleGraph를 만들고 ChunkGraph를 또 만들까?”&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;이유는 하나다. 사실과 전략을 분리하기 위해서다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ModuleGraph(논리 / 사실)&lt;/strong&gt;는 내 코드가 어떤 관계로 연결되는지&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ChunkGraph(물리 / 전략)&lt;/strong&gt;는 이 관계를 어떤 파일 조합으로 사용자에게 전달할지가 중요하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;p&gt;이 분리 덕분에, 코드 구조를 크게 바꾸지 않고도 설정만으로 배포 전략을 바꿀 수 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;splitChunks.cacheGroups&lt;/code&gt;를 조정하면, vendor / common 분리 기준이 달라지고&lt;/li&gt;
&lt;li&gt;&lt;code&gt;optimization.runtimeChunk: &amp;quot;single&amp;quot;&lt;/code&gt;을 켜면, runtime만 별도 파일로 분리되며&lt;/li&gt;
&lt;li&gt;&lt;code&gt;import()&lt;/code&gt;는 코드 레벨에서, “비동기 경계”를 추가해 청크 분리를 유도한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;즉 웹팩은 “관계는 먼저 고정하고, 그 위에서 전략을 갈아끼우는 구조”로 설계돼 있다.&lt;/p&gt;
&lt;p&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;h2&gt;4) Emit 단계: ChunkGraph가 “실제 파일 트리”로 떨어진다&lt;/h2&gt;
&lt;p&gt;지금까지 만들어진 모든 그래프와 전략의 결과는 마지막에 실제 파일 형태로 출력된다.&lt;/p&gt;
&lt;h3&gt;(1) 분리 없이 한 덩어리인 경우&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;dist/
main.[contenthash].js
  index.html&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;(2) import()로 async 청크가 생긴 경우&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;dist/
main.[contenthash].js
  src_App_js.[contenthash].js
  index.html&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;(3) SplitChunks까지 적용된 경우&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;dist/
  main.[contenthash].js
  vendors-main.[contenthash].js
  common-home-admin.[contenthash].js   // 멀티 엔트리일 때
  src_App_js.[contenthash].js          // import()가 있을 때
  index.html&lt;/code&gt;&lt;/pre&gt;&lt;br/&gt;

&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;  결국 웹팩은 &lt;br/&gt;ModuleGraph로 사실을 고정하고, ChunkGraph로 전략을 결정한 뒤,&lt;br/&gt;Emit에서 그 결과를 파일로 현실화한다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;h1&gt;3. 마치며…&lt;/h1&gt;
&lt;p&gt;이번 시간에는 웹팩의 &lt;strong&gt;번들링 과정&lt;/strong&gt;을 빌드 타임 관점에서 살펴봤다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;웹팩은 먼저 코드를 분석해 의존 관계를 정리하고 ModuleGraph를 만든다.&lt;/li&gt;
&lt;li&gt;그 위에서 “어떻게 나눠서 전달할지”라는 배포 전략을 세워 ChunkGraph를 구성한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;즉, 번들링은 단순한 “파일 합치기”가 아니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;(1) 의존 관계를 파악하는 분석 작업이고,&lt;/li&gt;
&lt;li&gt;(2) 전달 단위를 설계하는 전략 작업이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;그리고 이 빌드 타임의 결정은 브라우저에서 실제로 실행될 수 있어야 비로소 완성된다.&lt;/p&gt;
&lt;p&gt;다음 장에서는 이렇게 생성된 번들이 브라우저에서 어떻게 동작하는지 살펴보겠다.&lt;/p&gt;
&lt;p&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;</description>
      <category>라이브러리 파헤치기</category>
      <category>chunk graph</category>
      <category>Code Splitting</category>
      <category>dependency graph</category>
      <category>module graph</category>
      <category>tree shaking</category>
      <category>webpack</category>
      <category>webpack bundling</category>
      <category>번들링</category>
      <category>빌드도구</category>
      <category>웹팩</category>
      <author>GicoMomg</author>
      <guid isPermaLink="true">https://mong-blog.tistory.com/241</guid>
      <comments>https://mong-blog.tistory.com/entry/Webpack-%EB%B2%88%EB%93%A4%EB%A7%81%EC%9D%98-%EC%9B%90%EB%A6%AC-%EC%88%98%EC%B2%9C-%EA%B0%9C%EC%9D%98-%EB%AA%A8%EB%93%88%EC%9D%80-%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%AC%B6%EC%9D%84%EA%B9%8C#entry241comment</comments>
      <pubDate>Sun, 1 Feb 2026 22:08:26 +0900</pubDate>
    </item>
    <item>
      <title>이미지 리스트 성능 문제는 DOM에서 끝나지 않는다</title>
      <link>https://mong-blog.tistory.com/entry/%EC%9D%B4%EB%AF%B8%EC%A7%80-%EB%A6%AC%EC%8A%A4%ED%8A%B8-%EC%84%B1%EB%8A%A5-%EB%AC%B8%EC%A0%9C%EB%8A%94-DOM%EC%97%90%EC%84%9C-%EB%81%9D%EB%82%98%EC%A7%80-%EC%95%8A%EB%8A%94%EB%8B%A4</link>
      <description>&lt;h1&gt;0. 들어가며…&lt;/h1&gt;
&lt;p&gt;vue, react를 이용해 dom을 렌더링하는 게 자연스러워진 요즘.&lt;br&gt;각 프레임워크에서 리스트를 표현할 때는 Vue에서는 &lt;code&gt;v-for&lt;/code&gt;, React에서는 &lt;code&gt;map&lt;/code&gt;을 사용해 여러 DOM 요소를 렌더링한다.&lt;br&gt;이 방식은 단순하다. 그래서 데이터 배열만 있으면 누구나 손쉽게 리스트 UI를 만들 수 있다.&lt;br&gt;하지만 이 단순한 방식이 언제나 좋은 선택은 아니다.&lt;/p&gt;
&lt;h3&gt;DOM을 계속 늘려도 괜찮을까?&lt;/h3&gt;
&lt;p&gt;만약 다음 조건이라면 큰 문제는 없다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;데이터 개수가 100개 미만이고&lt;/li&gt;
&lt;li&gt;노출되는 리스트 DOM이 많지 않으며&lt;/li&gt;
&lt;li&gt;한 화면에서 무거운 연산을 하지 않는 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이런 상황에서는 단순 렌더링 방식이 성능 리스크로 이어질 가능성은 크지 않다.&lt;br&gt;하지만 리스트 DOM이 100개를 넘어 1,000개, 혹은 그 이상까지 늘어날 수 있는 구조라면?&lt;br&gt;이 시점부터는 “DOM을 이렇게 많이 렌더링하는 게 정말 옳은 선택인지” 고민해봐야 한다.&lt;/p&gt;
&lt;h3&gt;그래서 등장한 해결책, 가상 스크롤&lt;/h3&gt;
&lt;p&gt;이 문제를 해결하기 위해 가장 흔히 사용되는 방법이 바로 가상 스크롤(Virtual Scroll)이다.&lt;br&gt;안드로이드 개발에서는 과거 RecyclerView라는 이름으로 널리 사용되던 개념이다.&lt;br&gt;가상 스크롤의 방식은 명확하다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;화면에 필요한 정해진 개수의 DOM(n개)만 유지하고&lt;/li&gt;
&lt;li&gt;사용자가 스크롤할 때마다&lt;/li&gt;
&lt;li&gt;DOM은 그대로 두고, 그 안의 데이터만 교체해서 보여준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/btNCeQ/dJMcagqGXRx/MPPFaz51UwVnK0hEJd3gY0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/btNCeQ/dJMcagqGXRx/MPPFaz51UwVnK0hEJd3gY0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btNCeQ/dJMcagqGXRx/MPPFaz51UwVnK0hEJd3gY0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbtNCeQ%2FdJMcagqGXRx%2FMPPFaz51UwVnK0hEJd3gY0%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;이렇게 하면 DOM을 무한정 생성하지 않기에 렌더링 비용을 아끼고, 성능 부하를 줄일 수 있다.&lt;/p&gt;
&lt;h3&gt;그럼 가상 스크롤은 만능일까?&lt;/h3&gt;
&lt;p&gt;그럼, 리스트를 표현해야 한다면, 가상 스크롤만 쓰면 된다고 생각할 수 있다.&lt;br&gt;실제로 텍스트 위주의 단순한 리스트라면 가상 스크롤을 적용하는 것만으로도 성능 개선 효과는 크다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;하지만, 이미지 리스트라면?&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;문제는 여기서부터다.&lt;br&gt;만약 리스트의 각 아이템이 고화질 이미지를 포함하고 있다면, 가상 스크롤을 “적용하는 것만으로” 충분할까?&lt;br&gt;이미지는 텍스트와 달리, (1) 네트워크 요청 (2) 디코딩 (3) GPU 업로드 (4) 합성 등의 과정을 거쳐야 화면에 노출된다.&lt;br&gt;즉, DOM 개수만 줄인다고 해서 모든 비용이 사라지지는 않는다.&lt;br&gt;가상 스크롤은 분명 좋은 도구지만, 이미지 리스트에서는 추가로 고려해야할 점이 있다.&lt;/p&gt;
&lt;p&gt;이번 시간에는 다음 두 가지를 중심으로 살펴보려 한다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;이미지 요소가 브라우저에서 화면에 노출되기까지의 과정&lt;/li&gt;
&lt;li&gt;이미지 리스트에 가상 스크롤을 적용할 때 반드시 고려해야 할 포인트&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;h1&gt;1. 이미지 리스트와 가상 스크롤&lt;/h1&gt;
&lt;h2&gt;1) 이미지 리스트에 가상 스크롤 말고 고려할 점이 많다.&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;  (키 포인트) 이미지 리스트 성능 문제는 DOM 문제로 시작하지만, DOM 문제로 끝나지 않는다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;&lt;em&gt;가상 스크롤은 DOM 비용만 줄여준다.&lt;br&gt;이미지 렌더링의 핵심 병목은 그 이후 단계에 있다.&lt;/em&gt;&lt;/p&gt;
&lt;br/&gt;

&lt;h3&gt;(1) 우리가 흔히 하는 착각&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;성능 최적화를 하다 보면 이런 흐름에 익숙해진다.&lt;ul&gt;
&lt;li&gt;preload를 붙이기&lt;/li&gt;
&lt;li&gt;캐싱 전략을 고민하기&lt;/li&gt;
&lt;li&gt;lazy loading을 적용하기&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;그러다 리스트가 많은 화면을 발견하면, 자연스럽게 이렇게 사고가 이어진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;현상) DOM이 많다
문제) 렌더링이 느리다

해결) DOM을 줄이면 빨라질 것이다&lt;/code&gt;&lt;/pre&gt;&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;그래서 가상 스크롤을 적용하게 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;이미지 리스트가 느리다

→ DOM이 많다
→ 가상 스크롤을 적용하자
→ 해결&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;이 방식이 완전히 틀린 건 아니다. 하지만 이미지 리스트에서는 고려할 사항이 더 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;h3&gt;(2) 왜 가상 스크롤만으로 부족할까?&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;DOM은 분명 비용이 들기 때문에, 줄이는 게 좋다.&lt;/li&gt;
&lt;li&gt;하지만 이미지 리스트 성능에는 DOM은 물론 복합적으로 고려할 사항이 많다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;이미지 리스트 성능

= DOM 비용
+ 이미지 디코딩(CPU)
+ GPU 업로드 / 합성
+ 레이아웃 안정성(CLS)&lt;/code&gt;&lt;/pre&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;가상 스크롤은 여기서 &lt;strong&gt;단 하나,&lt;/strong&gt; DOM 비용만 해결해준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;가상 스크롤로 해결되는가?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;DOM 개수&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;이미지 다운로드&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;이미지 디코딩 (CPU)&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPU 업로드 / 합성&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;레이아웃 흔들림(CLS)&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;DOM은 줄었지만, 이미지는 여전히 새로 디코딩되고 GPU로 올라간다.&lt;/li&gt;
&lt;li&gt;이미지 성능 문제의 대부분은 DOM 이후 단계에서 발생한다.&lt;/li&gt;
&lt;li&gt;따라서 이미지 리스트에서는 렌더링 파이프라인 전체를 기준으로 전략을 세워야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;h2&gt;2) 브라우저는 &lt;img&gt; 하나를 그리기 위해 무슨 일을 할까?&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;  이미지는 “다운로드되면 끝”이 아니다.&lt;br&gt;화면에 그려지기까지 CPU와 GPU가 모두 개입한다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;ul&gt;
&lt;li&gt;이미지를 렌더링할 때 어떤 과정을 거칠까?&lt;/li&gt;
&lt;li&gt;어쩌면 다음과 같이 단순한 형태만 생각할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;img&amp;gt; 발견
→ 다운로드
→ 화면에 표시&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;하지만 실제로 이미지를 렌더링하기 위해 여러 단계를 거치게 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;h3&gt;브라우저의 이미지 렌더링 파이프라인&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;브라우저가 &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; 하나를 화면에 그리기까지 7가지 과정을 거친다.&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;단계&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;1. &lt;strong&gt;요청 우선순위 판단&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;이 이미지를 지금 당장 받아야 하는지 아니면 나중에 받아야 하는지 판단&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2. &lt;strong&gt;네트워크 다운로드&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;서버로부터 압축된 바이너리 데이터를 수신&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3. &lt;strong&gt;HTTP 캐시 확인&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;이미 받은 적 있다면 로컬 파일로 대체&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4. &lt;strong&gt;디코딩(Decoding) ⭐&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;압축을 풀어 &amp;#39;픽셀(RGBA)&amp;#39;로 변환. (&lt;strong&gt;CPU 소모&lt;/strong&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5. &lt;strong&gt;레이아웃 계산&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;이미지의 자리를 확보하고 주변 요소의 위치를 정함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6. &lt;strong&gt;GPU 텍스처 업로드 ⭐&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;픽셀 데이터를 비디오 메모리(VRAM)로 보냄&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7. &lt;strong&gt;합성(Composite) 및 화면 표시&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;레이어들을 겹쳐서 최종 화면을 완성&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;여기서 기억해야 할 포인트는 이것이다.&lt;/li&gt;
&lt;li&gt;성능 문제의 대부분은 4번(디코딩)과 6번(GPU 업로드)에서 발생한다는 것!&lt;/li&gt;
&lt;li&gt;이제 각 단계별로 어떤 일을 하는 지 살펴보자!&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;h3&gt;(1) 1단계, 요청 우선순위 판단 (Fetch Priority)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;브라우저는 &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt;를 발견하자마자 다운로드하지 않는다.&lt;/li&gt;
&lt;li&gt;먼저 “이 이미지가 지금 얼마나 급한가?”를 평가한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;- 판단 기준
    : 지금 화면(Viewport) 내부에 있는가?
    : 화면 상단에 위치하는가? (LCP 후보)
    : loading=&amp;quot;lazy&amp;quot;가 지정되어 있는가?
    : viewport와의 거리
    : CSS로 &amp;quot;display: none&amp;quot; 상태는 아닌가?
    : fetchpriority=&amp;quot;high&amp;quot;라고 명시했나?&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;이 판단 결과에 따라 요청 우선순위(priority) 가 정해진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;만약 가상 스크롤을 쓰게 되면 요청 우선순위가 어떻게 바뀔까?&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;ul&gt;
&lt;li&gt;일반 리스트는 브라우저가 이미지를 점진적으로 발견하며 요청한다.&lt;/li&gt;
&lt;li&gt;그래서 화면에 노출하는 이미지가 100개라고 한번에 100개를 요청하지 않고 분산 요청한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;90%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bIrH7q/dJMcad1K6Nz/HrqbXuNoIlnRzOtmSqEDhK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bIrH7q/dJMcad1K6Nz/HrqbXuNoIlnRzOtmSqEDhK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bIrH7q/dJMcad1K6Nz/HrqbXuNoIlnRzOtmSqEDhK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbIrH7q%2FdJMcad1K6Nz%2FHrqbXuNoIlnRzOtmSqEDhK%2Fimg.png&quot; width=&quot;90%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;아래 캡처에서 일반 리스트의 이미지 요청은 &lt;code&gt;i&lt;/code&gt;(idle) 우선순위를 갖는다.&lt;/li&gt;
&lt;li&gt;이는 브라우저가 해당 이미지를 &lt;em&gt;즉시 처리해야 할 렌더링 작업&lt;/em&gt;이 아니라, &lt;strong&gt;주요 작업이 끝난 뒤 여유가 있을 때 처리해도 되는 리소스&lt;/strong&gt;로 분류했음을 보여준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;90%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tsaPA/dJMcadgoZk6/vQWqxNHyDLX2HhICvlnfkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tsaPA/dJMcadgoZk6/vQWqxNHyDLX2HhICvlnfkk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tsaPA/dJMcadgoZk6/vQWqxNHyDLX2HhICvlnfkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtsaPA%2FdJMcadgoZk6%2FvQWqxNHyDLX2HhICvlnfkk%2Fimg.png&quot; width=&quot;90%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;그에 비해 가상 스크롤은 스크롤 시점에 필요한 이미지를 DOM에 한꺼번에 주입한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;90%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bD66vb/dJMcag5jQVf/TylkaVFkQusQbhsVzwGa6k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bD66vb/dJMcag5jQVf/TylkaVFkQusQbhsVzwGa6k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bD66vb/dJMcag5jQVf/TylkaVFkQusQbhsVzwGa6k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbD66vb%2FdJMcag5jQVf%2FTylkaVFkQusQbhsVzwGa6k%2Fimg.png&quot; width=&quot;90%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;그 이유는 가상 스크롤로 새롭게 노출되는 이미지들이 &lt;code&gt;Priority: u=1 (urgency level 1)&lt;/code&gt;로 분류되기 때문이다.&lt;/li&gt;
&lt;li&gt;이는 브라우저가 해당 이미지를 지금 즉시 화면에 표시되어야 하는 리소스로 판단했음을 의미한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;90%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cQeqzv/dJMcagxsAzz/aZuWpiOLwkV910KpKZeqd1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cQeqzv/dJMcagxsAzz/aZuWpiOLwkV910KpKZeqd1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cQeqzv/dJMcagxsAzz/aZuWpiOLwkV910KpKZeqd1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcQeqzv%2FdJMcagxsAzz%2FaZuWpiOLwkV910KpKZeqd1%2Fimg.png&quot; width=&quot;90%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;결국, 가상 스크롤 구간에서는 높은 우선순위(&lt;code&gt;u=1&lt;/code&gt;)의 이미지 요청이 동일한 시점에 집중적으로 발생하며, &lt;/li&gt;
&lt;li&gt;요청 수는 줄어들더라도 &lt;strong&gt;요청의 ‘순간 밀도’는 오히려 높아진다.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;h3&gt;(2) 2단계, 네트워크 다운로드&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;요청 우선순위 판단이 끝났다면, 이제 네트워크를 통해 이미지를 다운로드한다.&lt;/li&gt;
&lt;li&gt;여기서 또 하나의 착각이 나온다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;“이미지 다운로드가 끝났으니 GPU가 바로 그린다”는 착각!&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;우리가 받은 데이터(JPG, PNG, WebP)는 고도로 압축된 &lt;strong&gt;바이너리 파일&lt;/strong&gt;이다.&lt;/li&gt;
&lt;li&gt;GPU(그래픽 카드)는 이 압축된 상태를 이해하지 못한다.&lt;/li&gt;
&lt;li&gt;즉, 다운로드가 됐다고 해도 렌더링을 할 수 없다. 디코딩/GPU 업로드를 거쳐야 렌더링할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;h3&gt;(3) 3단계, HTTP 캐시 확인&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;이미지가 이미 캐시되어 있다면 브라우저는 네트워크 요청 없이 바로 이미지를 가져올 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;70%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c4kd4P/dJMcac9CAsP/ps9QNTpvRGW3MzIBw2bKQ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c4kd4P/dJMcac9CAsP/ps9QNTpvRGW3MzIBw2bKQ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c4kd4P/dJMcac9CAsP/ps9QNTpvRGW3MzIBw2bKQ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc4kd4P%2FdJMcac9CAsP%2Fps9QNTpvRGW3MzIBw2bKQ0%2Fimg.png&quot; width=&quot;70%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;그래서 일반적으로 화면에 이미지가 더 빨리 노출될 것이라 기대한다.&lt;/li&gt;
&lt;li&gt;하지만 여기서 중요한 사실이 하나 있다.&lt;/li&gt;
&lt;li&gt;바로, “이미지가 캐시되어 있어도 디코딩과 GPU 업로드 과정은 여전히 필요”하다는 것!&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;왜 캐시되어 있는데도 후처리가 필요할까?&lt;br&gt;이 질문의 답은 “브라우저가 무엇을 캐시하는가”를 보면 명확해진다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;ul&gt;
&lt;li&gt;브라우저에는 이미지와 관련된 캐시가 두 단계로 존재한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;캐시 종류&lt;/th&gt;
&lt;th&gt;저장 내용&lt;/th&gt;
&lt;th&gt;성능 영향&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;HTTP 캐시&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;이미지 파일 원본 (압축 데이터)&lt;/td&gt;
&lt;td&gt;네트워크 재다운로드 비용 감소&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Bitmap 캐시&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;디코딩 완료된 픽셀 데이터&lt;/td&gt;
&lt;td&gt;CPU 디코딩 비용 감소&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;여기서 핵심은 “브라우저는 이미지 원본만 HTTP 캐시에 저장”한다는 것이다.&lt;/li&gt;
&lt;li&gt;즉, JPG / PNG / WebP 같은 압축 파일은 캐시되지만 디코딩이 끝난 픽셀 데이터는 항상 남아 있지 않다.&lt;/li&gt;
&lt;li&gt;그래서 캐시에서 이미지를 가져와도, 디코딩 과정으로 인해 버벅임이 발생할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;그렇다면 디코딩된 이미지를 캐시하면 되지 않을까?&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;ul&gt;
&lt;li&gt;만약 디코딩된 이미지(Bitmap)를 유지할 수 있다면 렌더링은 훨씬 빨라질 것이다.&lt;/li&gt;
&lt;li&gt;하지만 브라우저는 의도적으로 Bitmap 캐시를 오래 유지하지 않는다.&lt;/li&gt;
&lt;li&gt;그 이유는 단순한데, 메모리 비용이 너무 크기 때문이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;ul&gt;
&lt;li&gt;예를 들어 다음 이미지가 몇 장만 있어도 탭 하나가 빠르게 메모리를 잠식할 것이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;- 1920 × 1080 이미지
- 약 2M 픽셀
  → 약 8MB 메모리 사용&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;그래서 브라우저는 다음과 같은 전략을 취한다.&lt;ul&gt;
&lt;li&gt;Bitmap 캐시는 최소한으로 유지&lt;/li&gt;
&lt;li&gt;메모리 압박이 오면 가장 먼저 제거&lt;/li&gt;
&lt;li&gt;탭 전환, 스크롤 이동 시에도 적극적으로 삭제&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;h3&gt;(4) 4단계 , ⭐ 디코딩(Decoding)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;이미지는 압축 파일이며, GPU는 압축된 이미지를 직접 그릴 수 없다.&lt;/li&gt;
&lt;li&gt;그래서 CPU가 압축 이미지를 변환하는 “디코딩”을 수행한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;압축 이미지

→ RGBA 픽셀 배열&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ceQ1Zy/dJMcachwPRD/gksupFYWxe0nkqbfviEmck/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ceQ1Zy/dJMcachwPRD/gksupFYWxe0nkqbfviEmck/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ceQ1Zy/dJMcachwPRD/gksupFYWxe0nkqbfviEmck/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FceQ1Zy%2FdJMcachwPRD%2FgksupFYWxe0nkqbfviEmck%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;div style=&quot;text-align:center; margin-top: 6px; font-size: 0.85rem; color: rgb(77,77,77)&quot;&gt;[. 이미지: 디코딩 전의 압축된 바이너리 데이터(Raw Data)의 모습 ]&lt;/div&gt;


&lt;br/&gt;


&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/n1GbX/dJMcaaRyxwy/Zc0JEgtK44ZZxZRAk63zQk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/n1GbX/dJMcaaRyxwy/Zc0JEgtK44ZZxZRAk63zQk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/n1GbX/dJMcaaRyxwy/Zc0JEgtK44ZZxZRAk63zQk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fn1GbX%2FdJMcaaRyxwy%2FZc0JEgtK44ZZxZRAk63zQk%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;div style=&quot;text-align:center; margin-top: 6px; font-size: 0.85rem; color: rgb(77,77,77)&quot;&gt;[. 이미지: 디코딩 후 모습 ]&lt;/div&gt;

&lt;br/&gt;

&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;이 디코딩은 성능에 영향을 주는 요인 중 하나이다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;영향을 주는 첫 번째 이유(연산량의 증가)&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;해상도가 가로/세로 2배만 커져도 픽셀 수는 4배가 된다.&lt;/li&gt;
&lt;li&gt;예를 들어, 1000px 이미지는 100만 개의 픽셀 데이터를 처리해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;영향을 주는 두 번째 이유(메인 스레드 점유)&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;브라우저는 보통 메인 스레드에서 이 작업을 처리한다.&lt;/li&gt;
&lt;li&gt;CPU가 디코딩을 처리하는 동안 사용자의 스크롤 이벤트, 버튼 클릭 등은 무시된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;결국, 디코딩이 몰리면 JS 실행, 스크롤, 클릭이 동시에 막힌다.&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;h3&gt;(5) 5단계, 레이아웃(Layout)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;이미지 로드 전, 브라우저는 해당 이미지의 크기를 모른다.&lt;/li&gt;
&lt;li&gt;그래서 일단 높이를 0으로 잡고 나중에 이미지가 로드되면 그 때 공간을 재배열한다.&lt;/li&gt;
&lt;li&gt;이때 발생할 수 있는게 바로 &lt;strong&gt;CLS(Cumulative Layout Shift)&lt;/strong&gt;이다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CLS&lt;/strong&gt;란, 갑자기 이미지가 툭 튀어나오며 아래 요소들이 밀려나는 현상을 말한다.&lt;/li&gt;
&lt;li&gt;그래서 이런 CLS를 예방하기 위해 aspect-ratio나 고정 width/height 속성을 지정하는 게 중요하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;h3&gt;(6) 6단계, ⭐ GPU 업로드 &amp;amp; 메모리 관리&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;디코딩이 완료된 이미지는 CPU 메모리에 RGBA 픽셀 버퍼 형태로 존재한다.&lt;/li&gt;
&lt;li&gt;이 픽셀 데이터는 화면에 그려지기 위해 GPU 메모리(VRAM)로 복사되어 텍스처(texture)로 변환된다.&lt;/li&gt;
&lt;li&gt;이후 브라우저의 컴포지터는 이 텍스처를 이용해 레이어를 합성하고, 최종 프레임을 생성한다.&lt;/li&gt;
&lt;li&gt;여기서 중요한 점은 GPU 메모리의 수명 주기가 DOM과 다르다는 것이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;DOM 제거와 GPU 메모리 해제 시점은 같지 않다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;ul&gt;
&lt;li&gt;가상 스크롤을 사용하는 경우, 화면에서 벗어난 이미지는 DOM에서 빠르게 제거된다.&lt;/li&gt;
&lt;li&gt;하지만 이는 DOM 트리에서의 제거일 뿐, 다음을 의미하지는 않는다.&lt;ul&gt;
&lt;li&gt;디코딩된 비트맵이 즉시 해제된다&lt;/li&gt;
&lt;li&gt;GPU에 업로드된 텍스처가 바로 반환된다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;브라우저는 성능을 위해 다음과 같은 전략을 취한다.&lt;ul&gt;
&lt;li&gt;최근 사용된 이미지 텍스처를 캐시로 유지&lt;/li&gt;
&lt;li&gt;곧 다시 사용될 가능성이 있으면 GPU 메모리에 잠시 보존&lt;/li&gt;
&lt;li&gt;GC / 메모리 압박 시점까지 해제를 지연&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;그 결과, DOM에서는 사라진 이미지의 픽셀 정보가 GPU 메모리에는 한동안 남아 있는다.&lt;/li&gt;
&lt;li&gt;만약 이 현상이 누적되면, 특정 스크롤 구간에서 메모리 사용량이 순간적으로 튀는 현상(메모리 스파이크)이 발생할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;h2&gt;3) 이미지 리스트에 가상 스크롤 적용시 주의할 점&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;가상 스크롤은 분명 강력하다.&lt;/li&gt;
&lt;li&gt;하지만 &lt;strong&gt;이미지 리스트&lt;/strong&gt;에서는 “어디까지 해결해주고, 무엇은 남는지”를 정확히 알아야 한다.&lt;/li&gt;
&lt;li&gt;먼저 한 눈에 정리해보자.&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;가상 스크롤의 효과&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;고려할 점&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DOM 노드 관리&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;✅ 완벽 해결&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;네트워크 부하&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;⚠️ 순간적 폭주 위험&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;썸네일 / 요청 분산&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;디코딩 연산(CPU)&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;❌ 해결 못함&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;디코딩 제어 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;메모리(VRAM)&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;❌ 해결 못함&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;overscan 조절&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;레이아웃(CLS)&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;❌ 오히려 더 민감&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;고정 영역 필수&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;가상 스크롤은 “DOM 문제”만 해결한다. 이미지의 병목은 그 이후 단계에 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;h3&gt;(1) decoding=&amp;quot;async&amp;quot; 지정하기&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;img src=&amp;quot;image.jpg&amp;quot;decoding=&amp;quot;async&amp;quot; /&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;decoding=&amp;quot;async&amp;quot;은 메인 스레드가 여유 있을 때 디코딩을 하도록 지연시킨다.&lt;/li&gt;
&lt;li&gt;디코딩을 렌더링·스크롤과 &lt;strong&gt;경쟁시키지 않도록 힌트 제공한다.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;단, 모든 브라우저에서 항상 비동기로 되는 것은 아니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;h3&gt;(2) img.decode(), 준비된 이미지들만 렌더링&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const img = newImage();
img.src = &amp;quot;cat.jpg&amp;quot;;

img.decode().then(() =&amp;gt; {
  container.appendChild(img);
});&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;이 방식을 사용하면, 디코딩이 끝난 뒤에만 DOM에 삽입할 수 있다.&lt;/li&gt;
&lt;li&gt;다만 주의할 점도 있다.&lt;/li&gt;
&lt;li&gt;너무 많은 이미지를 한 번에 &lt;code&gt;decode()&lt;/code&gt; 하면 Promise 대기열 + 디코딩 폭주가 발생할 수 있다.&lt;/li&gt;
&lt;li&gt;그래서, overscan / 배치 제어와 함께 사용해야 안전하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;h3&gt;(3) overscan 튜닝&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;overscan은 “미리 렌더링해 두는 범위”다.&lt;/li&gt;
&lt;li&gt;이미지 리스트에서는 이 값이 특히 중요하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;overscan 너무 작음 → 하얀 빈칸, 체커보드
overscan 너무 큼 → 디코딩 폭주, VRAM 압박&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;overscan은 서비스하는 기기 성능/이미지 해상도에 따라 조정해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;

&lt;h3&gt;(4) 가장 강력한 해결책, 이미지 크기 줄이기&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;가장 효과적인 방법은 이미지 자체를 가볍게 만드는 것이다!&lt;/li&gt;
&lt;li&gt;서버 썸네일을 제공하거나, WebP / AVIF 사용하기, &lt;code&gt;srcset&lt;/code&gt; / &lt;code&gt;sizes&lt;/code&gt;로 해상도 분기하기 등이 있다.&lt;/li&gt;
&lt;li&gt;이 방식을 사용하면, 네트워크, 디코딩, GPU 메모리를 감소시킬 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;h1&gt;3. 마치며…&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;가상 스크롤은 리스트 성능 최적화의 대표 솔루션이다.&lt;/li&gt;
&lt;li&gt;DOM을 필요한 만큼만 유지하고 재사용하는 방식은, 특히 텍스트 중심 리스트에서는 거의 정답에 가깝다.&lt;/li&gt;
&lt;li&gt;하지만 이미지 리스트에서는 이야기가 달라진다. 이미지는 다운로드가 끝났다고 바로 그려지는 자원이 아니다.&lt;/li&gt;
&lt;li&gt;브라우저는 &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; 하나를 그리기 위해 여러 과정을 거친다.&lt;ul&gt;
&lt;li&gt;디코딩(CPU)&lt;/li&gt;
&lt;li&gt;GPU 텍스처 업로드(VRAM)&lt;/li&gt;
&lt;li&gt;합성(Composite)&lt;/li&gt;
&lt;li&gt;레이아웃 안정성(CLS)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;즉, 가상 스크롤이 DOM을 줄여준다 해도 디코딩과 GPU 업로드라는 핵심 병목은 그대로 남는다.&lt;/li&gt;
&lt;li&gt;그래서 이미지 리스트 최적화의 핵심은 “가상 스크롤을 적용했는가”가 아니다.&lt;/li&gt;
&lt;li&gt;이미지 리스트 최적화는 “DOM을 줄이는 문제”가 아니라 렌더링 파이프라인 전체의 비용을 분산시키는 문제다.&lt;/li&gt;
&lt;li&gt;만약 이미지 리스트의 성능 향상을 위해 가상 스크롤을 적용한다면 렌더링 파이프라인 전체 비용도 고려하는 걸 추천한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;</description>
      <category>개발 기술/사소하지만 놓치기 쉬운 개발 지식</category>
      <category>img.decode</category>
      <category>JS</category>
      <category>lazy loading 이미지</category>
      <category>virtual scroll</category>
      <category>가상 스크롤 (Virtual Scroll)</category>
      <category>대용량 리스트 렌더링</category>
      <category>브라우저 렌더링 파이프라인</category>
      <category>이미지 디코딩</category>
      <category>이미지 리스트 성능 최적화</category>
      <category>이미지 최적화</category>
      <author>GicoMomg</author>
      <guid isPermaLink="true">https://mong-blog.tistory.com/240</guid>
      <comments>https://mong-blog.tistory.com/entry/%EC%9D%B4%EB%AF%B8%EC%A7%80-%EB%A6%AC%EC%8A%A4%ED%8A%B8-%EC%84%B1%EB%8A%A5-%EB%AC%B8%EC%A0%9C%EB%8A%94-DOM%EC%97%90%EC%84%9C-%EB%81%9D%EB%82%98%EC%A7%80-%EC%95%8A%EB%8A%94%EB%8B%A4#entry240comment</comments>
      <pubDate>Tue, 13 Jan 2026 16:03:42 +0900</pubDate>
    </item>
  </channel>
</rss>