<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>hour_2.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Sat, 15 Nov 2025 04:57:33 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>hour_2.log</title>
            <url>https://velog.velcdn.com/images/hour_2/profile/4b4f4ea0-70d8-4f95-b6f2-45a7a1af2411/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. hour_2.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/hour_2" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[리액트의 렌더링]]></title>
            <link>https://velog.io/@hour_2/%EB%A6%AC%EC%95%A1%ED%8A%B8%EC%9D%98-%EB%A0%8C%EB%8D%94%EB%A7%81</link>
            <guid>https://velog.io/@hour_2/%EB%A6%AC%EC%95%A1%ED%8A%B8%EC%9D%98-%EB%A0%8C%EB%8D%94%EB%A7%81</guid>
            <pubDate>Sat, 15 Nov 2025 04:57:33 GMT</pubDate>
            <description><![CDATA[<p>컴포넌트가 화면에 보이기까지, React 안에서는 꽤 많은 일이 일어납니다.
우리는 보통 setState를 호출하거나 root.render(<App />)를 부르면서도</p>
<p><strong>지금 React 안에서 정확히 무슨 일이 일어나고 있을까?</strong> 를 깊게 생각하지 않고 지나가는 경우가 많죠.</p>
<p>이번 아티클에서는 렌더링(rendering) 과 virtual DOM을 중심으로
React가 어떤 흐름으로 UI를 만들고 바꾸는지 정리해볼게요 !</p>
<br/>

<h3 id="1-react에서-렌더링이란-정확히-뭘까">1. React에서 “렌더링”이란 정확히 뭘까?</h3>
<p>먼저 용어부터 정리해봅시다.</p>
<ul>
<li><p>브라우저 렌더링: DOM이 바뀐 뒤, 브라우저가 화면을 다시 그리는 과정(레이아웃 계산, 페인트 등).</p>
</li>
<li><p>React의 렌더링: 컴포넌트 함수를 <strong>호출해서</strong> 어떤 UI를 그릴지 계산하는 과정.
즉, JSX를 만드는 단계라고 보면 됩니다.</p>
</li>
</ul>
<p>React 입장에서 렌더링은 다음 질문에 답하는 과정이에요.</p>
<blockquote>
<p>현재 state와 props를 기준으로 이 컴포넌트는 어떤 JSX를 화면에 보여줘야 하지?</p>
</blockquote>
<p>이때 실제 DOM을 건드리는 건 아직 아닙니다.
DOM이 수정되는 건 <strong>커밋(commit)</strong> 단계에서예요.</p>
<br/>

<h3 id="2-전체-흐름-주문·요리·서빙에-비유">2. 전체 흐름: 주문·요리·서빙에 비유</h3>
<p>React 팀이 자주 쓰는 비유처럼 React 앱을 레스토랑이라고 생각해 볼게요.</p>
<ul>
<li><strong>사용자(손님)</strong>: 버튼 클릭, 입력, 스크롤 등으로 “요청”을 보냄</li>
<li><strong>React(웨이터)</strong>: 요청을 받아 주방으로 전달하고, 요리가 나오면 다시 가져옴</li>
<li><strong>컴포넌트(요리사, 레시피)</strong>: 어떤 UI를 만들지 정의된 설계서</li>
<li><strong>DOM(테이블)</strong>: 실제로 요리가 올려지는 자리</li>
</ul>
<br/>

<p>이때 UI가 한 번 갱신될 때마다 React는 세 가지 단계를 거칩니다.</p>
<p><strong>1. 렌더링 트리거 (Trigger)</strong></p>
<ul>
<li>주문이 들어왔다! — 렌더링을 해야만 하는 상황이 생김</li>
</ul>
<p><strong>2. 컴포넌트 렌더링 (Render)</strong></p>
<ul>
<li>이 주문대로라면 요리를 이렇게 만들어야겠네.</li>
</ul>
<p>컴포넌트를 호출해서 JSX를 계산</p>
<p><strong>3. DOM에 커밋 (Commit)</strong></p>
<ul>
<li>완성된 요리를 테이블에 올리자.</li>
<li>계산된 결과를 실제 DOM에 반영</li>
</ul>
<p>이제 각 단계를 자세히 뜯어볼게요.</p>
<br/>

<blockquote>
<p>1단계: 렌더링이 언제 트리거될까 ?</p>
</blockquote>
<p>React가 렌더링을 “해야겠다”라고 결정하는 시점은 크게 두 가지입니다.</p>
<p><strong>1-1 앱이 처음 시작될 때 (초기 렌더링)</strong></p>
<p>앱이 최초로 화면에 나타날 때는 루트 컴포넌트를 한 번 그려야 해요.
보통 이렇게 시작해요: </p>
<pre><code class="language-js">// index.js
import { createRoot } from &quot;react-dom/client&quot;;
import App from &quot;./App.js&quot;;

const root = createRoot(document.getElementById(&quot;root&quot;));
root.render(&lt;App /&gt;);</code></pre>
<p>여기서 일어나는 일:</p>
<ol>
<li>createRoot로 React가 그릴 루트 DOM 노드를 정하고</li>
<li>root.render(<App />)를 호출하면 → <strong>이제 <App />부터 쭉 렌더링해볼게!</strong> 라고 시작하는 것입니다.</li>
</ol>
<p>만약 root.render() 호출을 주석 처리하면?
→ React가 렌더링을 시작하지 않으니 화면에 아무것도 보이지 않게 되죠.</p>
<br/>

<p><strong>1-2. State가 업데이트될 때 (리렌더링)</strong></p>
<p>초기 렌더링 이후에는 보통 state 업데이트가 렌더링을 다시 트리거합니다.</p>
<pre><code class="language-js">function Counter() {
  const [count, setCount] = useState(0);

  return (
    &lt;&gt;
      &lt;p&gt;{count}&lt;/p&gt;
      &lt;button onClick={() =&gt; setCount(count + 1)}&gt;+1&lt;/button&gt;
    &lt;/&gt;
  );
}</code></pre>
<p>여기서 setCount(count + 1)를 호출하면:</p>
<ol>
<li>React는 Counter의 state가 바뀌었다는 걸 알고</li>
<li>Counter 컴포넌트를 렌더링 대기열에 추가합니다.</li>
<li>이후 렌더링 단계에서 다시 Counter() 함수를 호출해 <strong>새로운 count 값 기준으로 JSX가 어떻게 생겼는지</strong> 계산합니다.</li>
</ol>
<br/>

<blockquote>
<p>2단계 - React가 컴포넌트를 <strong>렌더링</strong>한다는 것</p>
</blockquote>
<p>트리거가 발생했으면 이제 진짜 <strong>렌더링</strong>이 시작됩니다.
이 단계에서 React는 컴포넌트 함수를 직접 호출합니다.</p>
<p><strong>2-1. 초기 렌더링: 루트에서 시작해 아래로 타고 내려가기</strong></p>
<p>예를 들어 밑에와 같은 코드가 있을 때,</p>
<pre><code class="language-js">export default function Gallery() {
  return (
    &lt;section&gt;
      &lt;h1&gt;Inspiring Sculptures&lt;/h1&gt;
      &lt;Image /&gt;
      &lt;Image /&gt;
      &lt;Image /&gt;
    &lt;/section&gt;
  );
}

function Image() {
  return (
    &lt;img
      src=&quot;https://i.imgur.com/ZF6s192.jpg&quot;
      alt=&quot;A huge metallic flower sculpture&quot;
    /&gt;
  );
}</code></pre>
<p>초기 렌더링에서는 
React가 Gallery()를 호출해서 JSX를 얻고
JSX 안에 <code>&lt;Image /&gt;</code> 가 보이면 -&gt; Image()도 호출하고</p>
<p>또 그 안에 다른 컴포넌트가 있으면 계속 내려가며 재귀적으로 호출합니다.</p>
<p>이 과정을 통해 React는</p>
<ul>
<li><code>&lt;section&gt;</code></li>
<li><code>&lt;h1&gt;</code></li>
<li><code>&lt;img&gt;</code> × 3개</li>
</ul>
<p>에 해당하는 <strong>가상의 트리(React 요소 트리)</strong>를 만들어내요.</p>
<br/>

<p><strong>2-2. 리렌더링: 다시 계산만 하는 단계</strong></p>
<p>state가 바뀌어 리렌더링이 발생할 때도 똑같이 컴포넌트 함수를 다시 호출합니다.</p>
<p>다만 중요한 점은:</p>
<ul>
<li><p>이 시점에는 DOM을 바로 건드리지 X</p>
</li>
<li><p><strong>새로 계산한 JSX</strong>와 <strong>이전 JSX</strong>를 비교해서 <strong>어디가 달라졌는지</strong>를 기억해 두기만 함</p>
</li>
<li><p>실제 DOM을 수정하는 건 <strong>다음 단계(커밋)</strong>에서 이루어짐</p>
</li>
</ul>
<br/>

<h3 id="3-렌더링-단계의-핵심-순수">3. 렌더링 단계의 핵심: 순수</h3>
<p>React는 렌더링 단계에서 컴포넌트를 계속 호출하며 UI를 계산합니다.
그래서 이 단계의 함수(컴포넌트)는 순수 함수여야 해요.</p>
<h4 id="순수-렌더링의-조건">순수 렌더링의 조건</h4>
<ol>
<li><p>같은 입력 → 같은 출력</p>
<ul>
<li>같은 props와 state가 주어졌을 때, 항상 같은 JSX를 반환해야 합니다.</li>
</ul>
</li>
<li><p>외부 세계를 직접 바꾸지 않기</p>
<ul>
<li>DOM 조작 (document.querySelector로 무언가 수정)</li>
<li>전역 변수 수정</li>
<li>네트워크 요청, 타이머 설정 등 부수효과(side effect) → 이런 것들은 렌더링 동안 하지 말아야 함</li>
</ul>
<br/>

</li>
</ol>
<p>이게 지켜지지 않으면 ? </p>
<ul>
<li>코드 규모가 커질수록 예상 못 한 타이밍에 렌더링이 다시 일어남</li>
<li>그때마다 DOM이 이상하게 변하거나, 중복 호출됨</li>
<li>디버깅하기 어려운 상태가 됨</li>
</ul>
<br/>

<h4 id="strict-mode에서-두-번-렌더링하는-이유">Strict Mode에서 두 번 렌더링하는 이유</h4>
<p>개발 모드에서 StrictMode를 켜면 React는 렌더링 단계에서 컴포넌트 함수를 두 번 호출합니다.</p>
<p>이유는 간단합니다.</p>
<blockquote>
<p>이 함수가 순수하지 않으면 두 번 호출했을 때 이상한 일이 일어날 것이기에 개발자가 알 수 있을 것이다.</p>
</blockquote>
<p>즉, 일부러 2번 호출해서 순수하지 않은 렌더링 로직을 조기에 드러내기 위한 안전장치라고 보면 됩니다.</p>
<br/>

<h3 id="4-dom에-커밋하기">4. DOM에 커밋하기</h3>
<p>렌더링 단계에서 React는 각 컴포넌트를 호출해 <strong>최종적으로 어떤 UI가 나와야 하는지</strong> 를 계산했습니다.
이제 이를 실제 DOM에 반영해야겠죠? 
이 과정이 커밋(commit) 단계입니다.</p>
<blockquote>
<p><strong>초기 렌더링의 커밋</strong></p>
</blockquote>
<p>앱이 처음 렌더링될 때</p>
<ul>
<li><p>React는 <code>&lt;div&gt;</code>, <code>&lt;h1&gt;</code>, <code>&lt;img&gt;</code> 같은 DOM 노드를 새로 만듬</p>
</li>
<li><p><code>appendChild()</code>와 같은 DOM API로 실제 페이지에 추가</p>
</li>
</ul>
<p>이때 사용자는 처음으로 React 앱의 UI를 보게 됩니다.</p>
<br/>

<blockquote>
<p><strong>리렌더링의 커밋: “바뀐 부분만” 수정</strong></p>
</blockquote>
<p>state 업데이트로 인해 리렌더링이 일어나면:</p>
<ol>
<li>React는 이전 렌더링 결과와 이번 렌더링 결과를 비교해서 아래에 대해 계산합니다.<ul>
<li>어떤 노드는 그대로인가</li>
<li>어떤 속성이 달라졌는가</li>
<li>어떤 노드를 새로 만들거나 삭제해야 하는가</li>
</ul>
</li>
</ol>
<ol start="2">
<li>커밋 단계에서 이 정보들을 토대로 필요한 <strong>최소한의 DOM 변경</strong>만 수행합니다.</li>
</ol>
<p>예를 들어 이런 컴포넌트가 있다고 해볼게요.</p>
<pre><code class="language-js">export default function Clock({ time }) {
  return (
    &lt;&gt;
      &lt;h1&gt;{time}&lt;/h1&gt;
      &lt;input /&gt;
    &lt;/&gt;
  );
}</code></pre>
<p>부모 컴포넌트에서 매초 새로운 time을 넘기면 Clock은 계속 리렌더링됩니다.
하지만 사용자가 <code>&lt;input&gt;</code>에 입력한 내용은 사라지지 않죠.</p>
<p>왜냐하면</p>
<ul>
<li>React가 비교했을 때 <code>&lt;input&gt;</code>은 이전과 <strong>같은 위치의 같은 요소</strong>라고 판단</li>
<li>value를 명시적으로 제어하고 있지 않다면 브라우저가 관리하는 값은 그대로 둠</li>
<li>DOM 업데이트는 <code>&lt;h1&gt;</code>의 텍스트 노드만 교체하는 선에서 끝</li>
</ul>
<p>=&gt; React의 <strong>DOM 최소 변경 전략</strong> 덕분 !</p>
<br/>

<h3 id="5-브라우저-페인트-react-이후의-마지막-단계">5. 브라우저 페인트: React 이후의 마지막 단계</h3>
<p>커밋이 끝나 DOM이 변경되면 이제 공은 브라우저에게 넘어갑니다.</p>
<p>브라우저는 변경된 DOM과 스타일을 바탕으로 레이아웃을 다시 계산하고, 픽셀을 화면에 그립니다.</p>
<p>이 과정을 흔히 <strong>브라우저 렌더링</strong>이 라고 부르기도 하지만,
<strong>React의 렌더링</strong>과 헷갈리기 쉬우니
여기서는 <strong>페인트(Paint)</strong>라고 구분해서 부르는 편이 좋습니다.</p>
<p>정리하면</p>
<blockquote>
<p>React 렌더링 → JSX 계산 (컴포넌트 함수 호출)
React 커밋 → DOM 반영
브라우저 페인트 → 화면에 실제로 그리기</p>
</blockquote>
<br/>

<h3 id="6-왜-렌더링이-일어나도-dom이-항상-바뀌는-건-아닐까">6. 왜 렌더링이 일어나도 DOM이 항상 바뀌는 건 아닐까?</h3>
<p>아티클 초반에 봤던 핵심 문장 중 하나가 이거였죠.</p>
<blockquote>
<p>렌더링이 항상 DOM 업데이트를 의미하는 것은 아니다.</p>
</blockquote>
<p>그 이유는 렌더링은 <strong>계산</strong>이고, 커밋은 <strong>적용</strong>이기 때문입니다.</p>
<p>리렌더링을 했더라도 이전과 JSX 구조가 같고 속성 값도 그대로인 부분은 React가 DOM을 건드릴 필요가 없습니다.</p>
<p>즉 다시 정리해서 얘기해보면</p>
<ul>
<li><strong>렌더링</strong>: 지금 상태 기준으로 UI가 어떻게 생겼는지 다시 한 번 생각해봄</li>
<li><strong>커밋</strong>: 지난번이랑 비교해보니, 이 부분만 바꾸면 되겠다고 판단하고 변경</li>
</ul>
<p>라는 두 단계가 분리되어 있기에 <strong>렌더링 = DOM 재생성</strong>이 아닌 것이죠.</p>
<p>이 구조 덕분에</p>
<p><strong>불필요한 DOM 변경</strong>을 피하고 <strong>브라우저 페인트 비용을 줄여</strong> 성능이 향상됩니다.</p>
<h3 id="7-react-렌더링과-커밋-흐름을-한번-더-요약하자면">7. React 렌더링과 커밋 흐름을 한번 더 요약하자면</h3>
<p>React 앱에서 화면이 한 번 업데이트될 때마다 항상 이 세 단계를 거칩니다.</p>
<p><strong>1. 트리거 (Trigger)</strong></p>
<ul>
<li><p>초기 <code>root.render(&lt;App /&gt;)</code> 호출</p>
</li>
<li><p>또는 <code>setState</code> / <code>useState</code>의 setter 호출 등으로
“렌더링이 필요하다”는 신호가 발생</p>
</li>
</ul>
<p><strong>2. 렌더링 (Render)</strong></p>
<ul>
<li><p>React가 컴포넌트 함수를 호출해 JSX를 계산</p>
</li>
<li><p>재귀적으로 하위 컴포넌트도 호출</p>
</li>
<li><p>이 단계에서는 DOM을 건드리지 않음</p>
</li>
<li><p><strong>반드시 순수해야 하는 단계</strong></p>
</li>
</ul>
<p><strong>3. 커밋 (Commit)</strong></p>
<ul>
<li><p>이전 렌더링 결과와 비교해 변경 사항만 DOM에 반영</p>
</li>
<li><p>초기 렌더링이면 DOM 노드를 생성·추가</p>
</li>
<li><p>리렌더링이면 꼭 필요한 최소 변경만 수행</p>
</li>
</ul>
<br/>

<p>그리고 커밋까지 끝난 뒤에는 브라우저가 레이아웃 계산과 페인트를 수행해
사용자가 실제로 변화된 UI를 보게 됩니다.</p>
<br/>

<blockquote>
<p>마지막으로 기억해두면 좋은 포인트</p>
</blockquote>
<p>Strict Mode에서 React는 일부러 렌더링을 더 자주 호출해
순수하지 않은 컴포넌트를 조기에 찾음</p>
<p>렌더링 결과가 이전과 같다면 React는 DOM을 건드리지 않음 -&gt; 성능 최적화의 핵심</p>
<br/>

<blockquote>
<p>렌더링이라는 말을 쓸 때
<strong>React 렌더링(컴포넌트 호출)</strong> 과
<strong>브라우저 렌더링(페인트)</strong> 를 구분해서 생각하면
내부 동작을 훨씬 명확히 설명할 수 있습니다.</p>
</blockquote>
<br/>

<h3 id="8-virtual-dom과-react-internals">8. Virtual DOM과 React Internals</h3>
<p>여기까지는 <strong>렌더링 -&gt; 커밋 -&gt; 브라우저 페인트</strong>라는 흐름을 위주로 봤다면, 
이제는 이 과정이 React 안에서는 어떤 데이터 구조로 표현되는지를 살펴볼 차례입니다.
바로 많이 들어본 <strong>Virtual DOM(VDOM)</strong> 이야기예요.</p>
<p><strong>Virtual DOM(VDOM)은</strong></p>
<blockquote>
<p>UI가 어떻게 생겼는지를 JavaScript 객체 형태로 메모리에 들고 있는 <strong>가상 DOM 트리</strong></p>
</blockquote>
<p>라고 생각하면 편합니다.</p>
<p>조금 더 풀어보면 실제 브라우저 DOM을 바로 조작하는 대신 React는 <strong>이렇게 생긴 UI가 됐으면 좋겠다</strong>라는 가상 상태를 JS 객체 트리(요소 트리, element tree)로 표현해 두고 이 트리를 기준으로 실제 DOM과 동기화합니다.</p>
<p>이때 이 <strong>가상 트리</strong>를 만드는 과정이 바로 <strong>렌더링(Render)</strong> 단계고</p>
<p>가상 트리와 실제 DOM을 맞추는 <strong>동기화 작업이 커밋(Commit)</strong> 단계입니다.</p>
<p>이 전체 과정을 React에서는 <strong>재조정(Reconciliation)</strong> 이라고 부릅니다.</p>
<p>즉 Virtual DOM은 단순히 복제된 DOM이라기보다는 <strong>React가 선언적 UI를 가능하게 만들기 위해 사용하는 중간 표현</strong> 이라고 보는 게 더 정확합니다.</p>
<p>우리는 React에게 이렇게 말하죠</p>
<blockquote>
<p>지금 UI 상태는 이런 모습이 됐으면 좋겠어(JSX).
실제 DOM은 네가 알아서 맞춰줘.</p>
</blockquote>
<p>VDOM이 있기 때문에 우리는 DOM 조작, 이벤트 핸들링, 수동 업데이트 같은 세부 구현을
컴포넌트 내부에서 전부 신경 쓰지 않고, 상태 기준으로 UI를 선언적으로 작성할 수 있습니다.</p>
<br/>

<h3 id="9-react에서-말하는-virtual-dom--react-elements--fiber">9. React에서 말하는 Virtual DOM = React Elements + Fiber</h3>
<p>재밌는 점은 Virtual DOM이라는 말이 엄밀한 하나의 기술 명세가 아니라는 거예요.
사람마다 쓰는 의미가 조금씩 다르고, 라이브러리마다 구현 방식도 다릅니다.</p>
<p>React 세계에서 Virtual DOM이라고 할 때는 보통 두 가지를 묶어서 이야기합니다.</p>
<h4 id="1-react-elements-요소-트리">1. React Elements (요소 트리)</h4>
<p>JSX를 babel이 변환하면, 결국 이런 형태의 객체가 됩니다.</p>
<pre><code class="language-js">const element = {
  type: &quot;h1&quot;,
  props: { children: &quot;Hello&quot; },
  // ...
};</code></pre>
<ul>
<li>이런 객체들이 모여서 <strong>UI가 어떻게 생겼는지</strong>를 표현하는 트리를 이룸</li>
<li>이걸 흔히 Virtual DOM 트리라고 부르기도 함</li>
</ul>
<h4 id="2-fiber-객체-fiber-tree">2. Fiber 객체 (Fiber Tree)</h4>
<ul>
<li>React 내부에서 사용하는 진짜 <strong>일 단위(작업 단위)</strong> 데이터 구조</li>
<li>각 컴포넌트/요소마다 하나씩 대응되는 JS 객체</li>
<li>다음과 같은 정보들을 들고 있음<ul>
<li>어떤 컴포넌트인지 (type)</li>
<li>어떤 props를 받고 있는지 (pendingProps, memoizedProps)</li>
<li>state와 업데이트 큐</li>
<li>부모/자식/형제 관계 (return, child, sibling)</li>
<li>이번 렌더링에서 어떤 작업이 필요한지(추가/삭제/속성 변경 등)</li>
</ul>
</li>
</ul>
<p>React 16부터 도입된 <strong>React Fiber</strong>는
이 Fiber 객체 트리를 기반으로 <strong>렌더링을 잘게 쪼개고, 우선순위를 주고, 중단/재개</strong>할 수 있게 해주는 새로운 재조정 엔진입니다.</p>
<p>그래서 React 입장에서의 Virtual DOM은 화면을 설명하는 <strong>요소 트리(React elements)</strong> 와, 그걸 실제로 다루는 내부 구조인 Fiber 트리까지 포함한 전체적인 패턴이라고 보는 게 자연스럽습니다.</p>
<br/>]]></description>
        </item>
        <item>
            <title><![CDATA[왜 useRef는 리렌더링이 안일어날까?]]></title>
            <link>https://velog.io/@hour_2/%EC%99%9C-useRef%EB%8A%94-%EB%A6%AC%EB%A0%8C%EB%8D%94%EB%A7%81%EC%9D%B4-%EC%95%88%EC%9D%BC%EC%96%B4%EB%82%A0%EA%B9%8C</link>
            <guid>https://velog.io/@hour_2/%EC%99%9C-useRef%EB%8A%94-%EB%A6%AC%EB%A0%8C%EB%8D%94%EB%A7%81%EC%9D%B4-%EC%95%88%EC%9D%BC%EC%96%B4%EB%82%A0%EA%B9%8C</guid>
            <pubDate>Fri, 14 Nov 2025 16:04:07 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/hour_2/post/0a64803e-0dbe-43da-8c50-e9b6f1a7afc9/image.png" alt=""></p>
<blockquote>
<p>❓ <strong><code>useRef</code></strong> 를 사용하면 다시 렌더링이 되지 않는다는데
❓ 왜 다시 렌더링이 안될까요 ??
❓ 어떤 원리로 다시 렌더링이 안될까요 ??
❓ <strong><code>useState</code></strong>와 어떤 차이점이 있길래 안될까요 ??</p>
</blockquote>
<h3 id="useref란">useRef란?</h3>
<p>: useRef는 리액트 훅의 한 종류로, Ref는 reference(참조)의 줄임말이다</p>
<p>⬇️ 리엑트 공식문서에서의 정의</p>
<blockquote>
<p><code>useRef</code> 는 렌더링에 필요하지 않은 값을 참조할 수 있는 React Hook이다.</p>
</blockquote>
<p>⇒ 컴포넌트가 특정 정보를 기억하도록 하고 싶지만 해당 정보가 새 렌더링을 촉발하지 않도록 하려는 경우 ref를 사용할 수 있다.</p>
<p><strong>useRef를 이용하면 ? (장점)</strong></p>
<ul>
<li>특정한 <strong>DOM요소에 접근</strong>이 가능하며</li>
<li>불필요한 <strong>재렌더링</strong>을 하지 않는다</li>
</ul>
<br/>

<h4 id="useref-사용방법-및-useref에-대해서-자세하게-">useRef 사용방법 및 useRef에 대해서 자세하게 !</h4>
<p>컴포넌트의 최상위 레벨에서 useRef를 호출하여 ref를 선언한다.</p>
<pre><code class="language-js">import { useRef } from &#39;react&#39;;

function MyComponent() {
  const intervalRef = useRef(0); //initialValue에 초기값 넣어주면 된다.
  const inputRef = useRef(null);
  // ...</code></pre>
<p><strong>매개변수</strong>
<strong><code>initialValue</code></strong>: ref 객체의 <strong><code>current</code></strong>프로퍼티 초기 설정값이다. </p>
<p>여기에는 어떤 유형의 값이든 지정할 수 있다. 이 인자는 초기 렌더링 이후부터는 무시된다 !</p>
<p>*<em>반환 값
*</em><img src="https://velog.velcdn.com/images/hour_2/post/a2b11e6a-daa0-4fcb-a00b-393b7564e985/image.png" alt=""></p>
<p><strong><code>useRef</code></strong>는 단일 프로퍼티를 가진 객체를 반환한다 !</p>
<ul>
<li><strong><code>current</code></strong>: 처음에는 전달한 <strong><code>initialValue</code></strong>로 설정되고 나중에 다른 값으로 바꿀 수 있다</li>
<li>ref 객체를 JSX 노드의 <strong><code>ref</code></strong>어트리뷰트로 React에 전달하면 React는 <strong><code>current</code></strong>프로퍼티를 설정합니다.</li>
</ul>
<blockquote>
<p><strong>주의사항</strong></p>
</blockquote>
<aside>

<p><strong><code>ref.current</code></strong> 프로퍼티(props)는 state와 달리 변경을 직접 할 수 있다. 
하지만 렌더링에 사용되는 객체(예: state의 일부)를 포함하는 경우 해당 객체를 변이해서는 안 된다 (UI를 그리는 데 쓰이는 객체는 직접 바꾸면 X)</p>
<p>⇒ UI 업데이트에 영향을 주는 데이터는 <strong><code>state</code></strong>로, 내부에서만 쓰는 값은 <strong>ref</strong>로 써야 O</p>
</aside>

<aside>

<p><strong><code>ref.current</code></strong> 프로퍼티를 변경해도 React는 컴포넌트를 다시 렌더링하지 않는다. </p>
<p>ref는 일반 <strong><code>JavaScript 객체</code></strong>이기 때문에 React는 사용자가 언제 변경했는지 알지 못한다</p>
</aside>

<aside>

<p>초기화를 제외하고는 렌더링 중에 <strong><code>ref.current</code></strong>를 쓰거나 읽지 말자. 
이렇게 하면 컴포넌트의 동작을 예측할 수 없게 된다.</p>
</aside>

<p> 출처 ⇒  <a href="https://ko.react.dev/reference/react/useRef">리액트 공식 문서</a></p>
 <br/>

<h3 id="렌더링이-뭔데">렌더링이 뭔데?</h3>
<p>: 내부의 변수들이 담고 있는 기존에 저장한 값들이 <strong><code>초기화</code></strong>되고, 함수 로직이 <strong><code>재실행</code></strong>됨을 의미</p>
<p>React에서 상태(state)의 변경을 감지하면 자동으로 컴포넌트가 렌더링이 된다 !</p>
<p><img src="https://velog.velcdn.com/images/hour_2/post/9c175117-12b4-4837-bf8f-668a8bc0adac/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/hour_2/post/d334b9e1-005e-4a41-9b84-a6bcfe8bb8f3/image.png" alt=""></p>
<p>하지만 위의 사진 같이 <strong><code>상태</code></strong>를 변경하는 것이 아닌 그저 <strong><code>값</code></strong>만 변경 했을 때에는 </p>
<p>리엑트에서 감지 하지 않기 때문에 UI를 다시 그리지 않는다 </p>
<p>⇒ 렌더링은 최초 한 번만 실행됨 ⇒ 따라서 처음 값인 0만 UI에 출력</p>
<br/>

<p><strong>렌더링이 될 때</strong></p>
<p><img src="https://velog.velcdn.com/images/hour_2/post/686a49a6-0a44-4460-8b47-d8bd95a73c22/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/hour_2/post/c82bd6be-646e-4ad9-98ba-bc24105830cf/image.png" alt=""></p>
<p>위와 같이 <strong><code>useState</code></strong>를 사용해 <strong><code>상태</code></strong>(state)를 변경해주니</p>
<p>리렌더링이 일어나 UI가 계속해서 업데이트 되는 것을 볼 수 있다 !</p>
<blockquote>
<p>콘솔 밀림 현상</p>
</blockquote>
<pre><code class="language-js">&gt; import { useState } from &quot;react&quot;;
&gt; import &quot;./App.css&quot;;
&gt; 
&gt; function App() {
&gt;   const [count, setCount] = useState(0);
&gt; 
&gt;   const clickCount = () =&gt; {
&gt;     setCount((prev) =&gt; prev + 1);
&gt;     console.log(count);
&gt;   };
&gt; 
&gt;   return (
&gt;     &lt;div&gt;
&gt;       &lt;p&gt;{count}&lt;/p&gt;
&gt;       &lt;button onClick={clickCount}&gt;클릭&lt;/button&gt;
&gt;     &lt;/div&gt;
&gt;   );
&gt; }
&gt; 
&gt; export default App;
&gt; ```
&gt; 이 코드를 실행시키면 
&gt; ![](https://velog.velcdn.com/images/hour_2/post/17899f03-76de-45b1-b9e3-60e7404a28cb/image.png)
&gt; 이런식으로 UI에는 6이 찍히는데 console에는 5가 찍힌다.
&gt; 왜 이렇게 콘솔 밀림 현상이 생기는 걸까?! 라는 궁금증에
&gt; 한번 알아보았다 !
&gt; 먼저 useState의 **`setState`**는 (위의 코드애서는 **`setCount`**) 비동기적으로 작동한다 !
&gt; ```js
&gt; setCount((prev) =&gt; prev + 1);
&gt;     console.log(count);  // 여기서 찍히는 count는 &gt; 아직 &quot;업데이트 전&quot; 값
&gt; ```
&gt; 여기에서 setCount를 호출해도 바로 count 값이 변경되지 않는다 ..
&gt; 
&gt; React는 상태 업데이트를 **비동기적으로 처리**해서 렌더링을 &lt;최적 타이밍에 한 번에&gt; 수행하려고 한다.
&gt;
&gt; 그래서 `console.log(count)`는 이전 렌더링의 `count` 값을 출력
&gt; 
&gt; 흐름을 정리해서 말해보자면
&gt; 1. 버튼을 클릭해서 clickCount 함수 실행
&gt; 2. React가 상태 변경 예약 → 이후 렌더링 스케쥴링
&gt; 3. 렌더링 완료되면 count 값이 새로 반영됨

### 상태랑 값 ? 무슨 차이인데 ?

여기서 상태와 값을 혼동할 수 있는데 밑에 간단하게 표로 정리했다

| 용어 | 설명 |
| --- | --- |
| 값 (value) | 변수에 저장된 
일반적인 값 |
| 상태 (state) | React가 추적하고 관리하는 값 |

&lt;br/&gt;

| 항목 | 일반 값 (let, const, var) | 상태 (useState, this.setState) |
| --- | --- | --- |
| 누가 관리 ? | 개발자가 직접 | React가 
내부적으로 관리 |
| 값이 바뀌면 ? | 그냥 **메모리**만 바뀜 | React가 감지해서 **리렌더링 발생** |
| React 추적 | 감지 X | 감지해서 
화면 업데이트 |

여기서 잠깐 ! 리렌더링이 언제 일어나는지도 알아보자 !!

### 리렌더링은 언제 실행될까 ?

&gt; - 자신의 state가 변경될 때
- 부모 컴포넌트에서 전달받은 props가 변경될 때
- 부모 컴포넌트가 리렌더링될 때 (자식에게 변경된 props가 없어도)


사실 리렌더링은 React 컴포넌트가 화면에 변경 사항을 반영하기 위해 반드시 필요한 과정이다. 

그러나 모든 리렌더링이 **의미 있는** 업데이트로 이어지는 것은 아니기 때문에

**불필요한 리렌더링을 줄이는 최적화**가 중요하다

&lt;br/&gt;


**불필요한 렌더링은 피해야 하는데 ..**

상태(state)를 변경하면 자동으로 컴포넌트가 렌더링되는데

**`함수형 컴포넌트`**는 함수 그 자체이기 때문에 리렌더링이 일어나게 되면 함수가 다시 불러와져서

내부에 있는 모든 변수들이 초기화가 된다.  

⇒ 따라서 원하지 않는 렌더링 때문에 곤란해질 때가 있다

&gt; **그래서 useRef를 언제 사용하는거야 ?**
**`useRef`**는 위와 같이 다시 렌더링하지 않고 변경 가능한 값을 저장하고 싶을 때 사용한다 !
&gt; 
&gt; 예를 들어, DOM 요소에 직접 접근하거나 이전 값을 기억하거나 렌더링과 &gt; 상관없는 내부 상태를 저장할 때 유용하다
&gt; 
&gt; 이런 특성 덕분에 `useRef`는 불필요한 리렌더링을 피하면서도 값은 유지할 수 있는 훌륭한 도구가 된다.


### useRef 동작 원리

앞선 위의 내용을 이해했다면 useRef는 충분히 이해할 수 있다

![](https://velog.velcdn.com/images/hour_2/post/a321080b-5a9f-480f-90af-dc9a3cb2192f/image.png)

먼저 useRef를 할당한 ref를 콘솔로 출력해보면

![](https://velog.velcdn.com/images/hour_2/post/526b7b7e-92ca-4ee9-b0ff-2d0ff04ad0e4/image.png)

이렇게 객체의 current라는 프로퍼티(props)를 통해 ref의 current 값에 접근할 수 있다.

```js
import { useRef } from &#39;react&#39;;

export default function Counter() {
  let ref = useRef(0);

  function handleClick() {
    ref.current = ref.current + 1;
    alert(&#39;You clicked &#39; + ref.current + &#39; times!&#39;);
  }

  return (
    &lt;button onClick={handleClick}&gt;
      Click me!
    &lt;/button&gt;
  );
}</code></pre>
<p>이것은 값 자체를 변경한다 !!  ➡️  <code>ref.current = ref.current + 1;</code></p>
<p>useRef는 위에 작성한 예시처럼 ref.current = ... 처럼 읽고 쓰는 것이 자유롭고 의도적으로 값을 변경할 수 있다. 리액트가 추적하지 않는다 !</p>
<pre><code class="language-js">const myRef = useRef(&quot;hi&quot;);
// myRef = { current: &quot;hi&quot; } =&gt; 객체의 형태로 출력됨</code></pre>
<p>앞에서 언급한 것처럼 useRef로 만든 값은 단순한 <strong><code>일반 자바스크립트 객체</code></strong>이다. </p>
<p>따라서 React는 이 객체 안에서 어떤 일이 일어나는지 관찰하지 않기 때문에</p>
<p> 리액트 사용자가 언제 변경했는지 모른다 !</p>
<p><strong><em>이것이 <code>useRef</code>가 값이 변경되어도 리렌더링 되지 않는 이유 !!</em></strong></p>
<blockquote>
<p>즉 useRef는 상태를 변경하는 것이 아닌 값을 보관하고 변경하는 것이기 때문에 react는 이 컴포넌트를 다시 그려야겠다는 판단을 하지 X </p>
</blockquote>
<blockquote>
<p>다시 흐름을 정리를 하자면</p>
</blockquote>
<ul>
<li>useRef는 상태를 변경하는 것이 아닌 객체 안의 값을 변경</li>
<li>useRef는 일반 JS 객체이므로 React가 변경을 감지하지 못해 리렌더링 X</li>
<li>값은 유지되지만 렌더링에 반영 X ⇒ UI에 반영 X</li>
</ul>
<p>그렇다면 react는 어떻게 상태(state) 변경만 인지하고 리렌더링을 하고 직접 한 값 변경은 인지를 못하는 걸까?</p>
<h3 id="useref를-사용하면--메모리에-어떻게-저장되길래-">useRef를 사용하면  메모리에 어떻게 저장되길래 ?</h3>
<p>위에서 봤던 것처럼 <strong><code>useRef()</code></strong>는 일반적익 <strong><code>JS(자바스크릡트) 객체</code></strong>이다 </p>
<p>일반적인 프로그래밍 언어와 같이 자바스크립트 역시 <strong><code>heap</code></strong> 영역과 <strong><code>stack</code></strong> 영역에서 메모리를 관리한다.</p>
<blockquote>
<p>heap 영역과 stack 영역에서 메모리가 어떻게 관리되는지 간단하게 알아보자</p>
</blockquote>
<aside>

<h3 id="stack">Stack</h3>
<ul>
<li>함수가 호출될 때 임시로 필요한 변수나 함수 실행 정보를 저장하는 메모리 공간</li>
<li>함수가 끝나면 그 함수와 관련된 스택 공간은 한꺼번에 해제
⇒ 컴포넌트 함수가 렌더링될 때마다 함수가 실행되고 종료되니 이 때 스택 공간은 바로 해제!</li>
<li>스택은 구조가 단순해서 할당과 해제가 매우 빠름</aside>

</li>
</ul>
<aside>

<h3 id="heap">Heap</h3>
<ul>
<li>객체나 배열 같은 동적 할당되는 데이터가 저장되는 공간</li>
<li>크기가 크고, 수명이 함수 호출과 상관없이 길 수 O</li>
<li>그래서 힙은 메모리를 직접 해제하는 게 아니라, 가비지 컬렉터(GC)가 자동으로 더 이상 사용하지 않는 메모리를 찾아 해제</aside>

</li>
</ul>
<br/>

<p><strong>자바스크립트에서 변수들의 메모리 위치는?</strong></p>
<ul>
<li>기본형 데이터(숫자, 문자열 등)는 주로 스택에 저장</li>
<li>객체, 배열, 함수 등 <strong><code>참조형 데이터</code></strong>는 <strong><code>힙</code></strong>에 저장</li>
<li>변수에 객체가 할당되면, 변수에는 힙에 저장된 객체를 가리키는 <strong><code>주소</code></strong>만 <strong><code>스택</code></strong>에 저장</li>
</ul>
<p>즉, useRef를 통해 저장된 값들은 heap 영역에 저장된다 !</p>
<blockquote>
<p><strong>useRef가 리렌더링이 일어나지 않는 이유 단계적으로 설명하자면</strong></p>
<p>1️⃣ <strong><code>useRef</code></strong>는 <strong><code>heap</code></strong>영역에 저장 !
2️⃣ heap 영역에 있기때문에 어플리케이션 종료까지 <strong><code>같은 메모리 값을 유지</code></strong> 
3️⃣ 그러다 보니 useRef로 관리하는 값이 변경되어도 <strong><code>동일 메모리 주소</code></strong>를 유지
4️⃣ 얕은 비교를 하는 React의 리렌더링 감지에서는 &#39;===&#39; 연산이 항상 true를 반환
5️⃣ 변경사항 감지가 되지 않아 <strong><code>리렌더링 X</code></strong></p>
</blockquote>
<br/>

<h3 id="useref를-정확히-어느-상황일-때-사용하면-좋을까-">useRef를 정확히 어느 상황일 때 사용하면 좋을까 ?</h3>
<blockquote>
<ol>
<li>입력창에 focus 주기 (DOM 접근)</li>
</ol>
</blockquote>
<p>React에서 컴포넌트가 마운트되자마자 특정 입력창에 자동으로 focus를 주고 싶을 때 useRef를 사용하면 DOM에 직접 접근</p>
<pre><code class="language-jsx">import { useEffect, useRef } from &quot;react&quot;;

function App() {
  const idInputRef = useRef&lt;HTMLInputElement&gt;(null);

  useEffect(() =&gt; {
    idInputRef.current?.focus(); // 컴포넌트 마운트 시 포커스
  }, []);

  return (
    &lt;div&gt;
      &lt;input ref={idInputRef} placeholder=&quot;아이디&quot; /&gt;
    &lt;/div&gt;
  );
};
</code></pre>
<blockquote>
<ol start="2">
<li>로그인 할 때 아이디 값 관리하기</li>
</ol>
</blockquote>
<p>로그인 시 아이디 값을 임시로 저장만 하고 싶고 화면에 보여줄 필요는 없을 때 <strong><code>useState</code></strong>보다 <strong><code>useRef</code></strong>가 적절 !</p>
<pre><code class="language-jsx">import { useRef } from &quot;react&quot;;

const LoginForm = () =&gt; {
  const idRef = useRef(&quot;&quot;);

  const handleChange = (e) =&gt; {
    idRef.current = e.target.value;
  };

  const handleLogin = () =&gt; {
    console.log(&quot;입력된 아이디:&quot;, idRef.current);
  };

  return (
    &lt;div&gt;
      &lt;input placeholder=&quot;아이디&quot; onChange={handleChange} /&gt;
      &lt;button onClick={handleLogin}&gt;로그인&lt;/button&gt;
    &lt;/div&gt;
  );
};
</code></pre>
<blockquote>
<p> <strong>재밌는 사실 !</strong></p>
<p>사실 useRef는 React에서 내부적으로 useState를 통해서 구현될 수 있다는 사실 !! </p>
<pre><code class="language-jsx">function useRef(initialValue) {
const [ref, _] = useState({ current: initialValue });
return ref;
}</code></pre>
</blockquote>
<pre><code>&gt; 초기 렌더링 시 **`useState`**로 `{ current: initialValue }` 객체를 생성하고
&gt; 
&gt; 그 객체를 **그대로 반환**한다.
&gt; 
&gt;  이 객체는 렌더링이 반복되어도 항상 동일한 참조값을 반환하기 때문에
&gt; 
&gt; 컴포넌트가 리렌더링되어도 객체 내부 값은 유지 !
&gt; 
&gt; ### 왜 setter가 필요 없을까?
&gt; 
&gt; `useState`는 값이 바뀌면 리렌더링을 유도하기 위해 `setState`를 제공한다.
&gt; 
&gt; 하지만 `useRef`는 반환된 객체를 **직접 수정**하므로 `setState`와 같은 setter가 필요 없다 !
&gt; 
```jsx
ref.current = &quot;새로운 값&quot;;</code></pre><blockquote>
<p>이렇게 값을 직접 바꾸더라도 리렌더링은 발생하지 X !</p>
</blockquote>
<br/>

<h3 id="마무리">마무리</h3>
<p>항상 개발을 할때는 정확한 원리를 이해하고 왜 그렇게 동작하는지를 알아가면서 개발하자 !</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[꼭 알아야 할JS 개념 (리액트 Deep Dive)]]></title>
            <link>https://velog.io/@hour_2/JS%EA%B0%9C%EB%85%90-Deep-Dive</link>
            <guid>https://velog.io/@hour_2/JS%EA%B0%9C%EB%85%90-Deep-Dive</guid>
            <pubDate>Sat, 08 Nov 2025 07:21:36 GMT</pubDate>
            <description><![CDATA[<p>개발을 하기 위해 리액트를 공부하고자 한다면 자바스크립트에 대해서 어느정도 알아야 한다.
<strong>리액트만</strong> 공부하는 건 불가능하다 !!</p>
<p>따라서 오늘은 리액트를 공부하기 이전에 꼭!!! 필요한 JS 개념에 대해서 알아보려고 한다.</p>
<br/>

<h2 id="💡-자바스크립트의-동등-비교">💡 자바스크립트의 동등 비교</h2>
<h3 id="데이터-타입">데이터 타입</h3>
<blockquote>
<p><strong>원시타입</strong></p>
<ul>
<li>boolean</li>
<li>null</li>
<li>undefined</li>
<li>number</li>
<li>string</li>
<li>symbol</li>
<li>bigint</li>
</ul>
</blockquote>
<blockquote>
<p><strong>객체타입</strong></p>
<ul>
<li>object</li>
</ul>
</blockquote>
<h4 id="null--undefined의-차이점-">null &amp; undefined의 차이점 ?</h4>
<blockquote>
<p><strong>undefined</strong>: 선언한 후 값을 할당하지 않은 변수 또는 값이 주어지지 않은 인수에 자동으로 할당되는 값</p>
</blockquote>
<blockquote>
<p><strong>null</strong>: 아직 값이 없거나 비어 있는 값을 표현할 때 사용</p>
</blockquote>
<h4 id="생성방식">생성방식</h4>
<pre><code class="language-jsx">let a;
console.log(a); // undefined (값이 할당되지 않음)

let b = null;
console.log(b); // null (명시적으로 &#39;비어 있음&#39;을 표현)</code></pre>
<ul>
<li>undefined는 변수가 선언되었지만 값이 전혀 지정되지 않았을 때 자동으로 할당됨.</li>
<li>null은 개발자가 명시적으로 비어 있음을 표현하기 위해 직접 할당함.</li>
</ul>
<br/>

<h4 id="동등성-비교">동등성 비교</h4>
<pre><code class="language-js">undefined == null;  // true  (값이 없다는 의미로 느슨하게 같음)
undefined === null; // false (타입이 다름)</code></pre>
<ul>
<li>느슨한 비교(==)에서는 “없음”이라는 공통점 때문에 true.</li>
<li>엄격한 비교(===)에서는 타입이 다르기 때문에 false.</li>
</ul>
<br/>

<h4 id="json-직렬화-시-차이">JSON 직렬화 시 차이</h4>
<pre><code class="language-js">JSON.stringify({ a: undefined, b: null }); 
// &#39;{&quot;b&quot;:null}&#39;</code></pre>
<ul>
<li>undefined 프로퍼티는 제외됨.</li>
<li>null은 그대로 포함됨 → “의도적 비어 있음”으로 간주.</li>
</ul>
<br/>

<h4 id="타입-비교">타입 비교</h4>
<pre><code class="language-js">typeof undefined; // &quot;undefined&quot;
typeof null;      // &quot;object&quot;</code></pre>
<ul>
<li>undefined는 고유 타입.</li>
<li>null은 역사적 이유로 &quot;object&quot;를 반환하지만, 실제로는 원시 타입(primitive).</li>
</ul>
<blockquote>
<p><strong>null의 특별한 점!</strong> 
=&gt; null의 type을 확인하면 object라는 결과가 나온다 !</p>
</blockquote>
<p><code>typeof null === &#39;object&#39;</code> 는 자바스크립트 초기 버전의 비트 기반 타입 태그 설계에서 비롯된 버그로 null이 <strong>객체 타입 + null 포인터</strong>로 표현되었기 때문에 발생한다.
이는 현재로써 해결할 수 있는 기술이지만 이 문제가 수정되면 <strong>기존 코드에 부정적인 영향</strong>을 미칠 수 있기 때문에 오늘날까지 유지되고 있는 버그이다.</p>
<blockquote>
<p>undefined와 null의 차이점을 한 줄로 설명하자면, 
<strong>undefined</strong>는 <strong>선언됐지만 할당되지 않은 값</strong>이고, <strong>null</strong>은 <strong>명시적으로 비어 있음을 나타내는 값</strong>으로 사용하는 것이 일반적이다.</p>
</blockquote>
<h3 id="값-저장-방식의-차이">값 저장 방식의 차이</h3>
<h4 id="원시-타입">원시 타입</h4>
<pre><code class="language-js">let hello = ’hello world&#39;
let hi = &#39;hello world’

console.log(hello === hi) // true</code></pre>
<ul>
<li>hi와 hello 값을 비교하면 true가 나온다 ? =&gt; 정답</li>
</ul>
<h4 id="객체-타입">객체 타입</h4>
<pre><code class="language-js">// 다음 객체는 완벽하게 동일한 내용을 가지고 있다.
var hello = {
greet: &#39;hello, world&#39;,
}

var hi = {
greet: &#39;hello, world&#39;,
}
// 그러나 동등 비교를 하면 false가 나온다.
console.log(hello === hi) // false
// 원시값인 내부 속성값을 비교하면 동일하다.
console.log(hello.greet === hi.greet) // true</code></pre>
<ul>
<li>완벽하게 동일한 내용을 가지고 있는 객체 hello와 hi를 비교하면 true가 나온다 ?  =&gt; 오답 ㅜ</li>
</ul>
<blockquote>
<p>왜 객체는 객체 안에 동일한 값을 가지고 있어도 값을 비교하면 false이 나올까?</p>
</blockquote>
<ul>
<li>원시 타입은 불변 형태의 값을 저장하고 이 값은 변수 할당 시점에 메모리 영역을 차지하고 저장</li>
<li>객체는 값을 저장하는 게 아니라 참조를 저장하기 때문에 앞서 동일하게 선언했던 객체라 하더라도 저장하는
순간 다른 참조를 바라보기 때문에 false를 반환 !!</li>
</ul>
<blockquote>
<p>값을 복사한다면 ?</p>
</blockquote>
<pre><code class="language-js">var hello = {
greet: ’hello, world&#39;,
}

var hi = hello
console.log(hi === hello) // true</code></pre>
<ul>
<li><code>var hi = hello</code> 를 실행하면 객체의 값이 아니라 “주소”가 복사되기 때문에 true !</li>
</ul>
<h4 id="objectjs">Object.js</h4>
<blockquote>
<p><code>==</code>, <code>===</code> 와는 또다른 리액트에서 제공하는 비교 방법</p>
</blockquote>
<p>사용방법</p>
<pre><code>-0 === +0 // true
Object.is(-0, +0) // false

Number.NaN === NaN // false
Object.is(Number.NaN, NaN) //true

NaN === 0 / 0 // false
Object.is(NaN, 0/0) //true</code></pre><ul>
<li><code>==</code>, <code>===</code> 만으로는 정확하게 구별하지 못하는 특이한 케이스를 비교하기 위해 만들어짐</li>
</ul>
<blockquote>
<p>하지만 객체 비교에서는 여전히 참조(주소) 비교를 하기에 개발자가 의도한대로 원할한 비교가 되지 않는다 ㅜ</p>
</blockquote>
<pre><code>Object.is({}, {}) // false</code></pre><h4 id="shallowequal">shallowEqual</h4>
<blockquote>
<p>Object.js의 객체 비교 문제를 해결하기 위한 얕은 비교 함수</p>
</blockquote>
<pre><code class="language-js">function shallowEqual(objA, objB) {
  // 1. Object.is로 먼저 완전 동일한지 비교
  if (is(objA, objB)) return true;

  // 2. 둘 중 하나라도 객체가 아니면 false
  if (typeof objA !== &#39;object&#39; || objA === null ||
      typeof objB !== &#39;object&#39; || objB === null) return false;

  // 3. 키 배열 길이가 다르면 false
  const keysA = Object.keys(objA);
  const keysB = Object.keys(objB);
  if (keysA.length !== keysB.length) return false;

  // 4. 같은 키를 순회하며 Object.is로 각 값 비교
  for (let key of keysA) {
    if (!objB.hasOwnProperty(key) || !is(objA[key], objB[key])) return false;
  }

  return true;
}
</code></pre>
<ul>
<li>shallowEqual은 객체의 1 depth(첫 번째 깊이) 까지는 값을 비교</li>
</ul>
<br/>

<blockquote>
<p>얕은 비교의 한계</p>
</blockquote>
<p>리액트에서는 props가 대부분 단순한 값(문자열, 숫자) 이라서 <code>1 depth</code>까지만 비교해도 대부분 충분하지만 <strong>깊은 객체</strong>에서는 문제가 생긴다.</p>
<pre><code class="language-js">const Component = memo((props) =&gt; {
  console.log(&quot;렌더링됨&quot;);
  return &lt;h1&gt;{props.counter}&lt;/h1&gt;;
});

const DeeperComponent = memo((props) =&gt; {
  console.log(&quot;렌더링됨&quot;);
  return &lt;h1&gt;{props.counter.counter}&lt;/h1&gt;;
});

&lt;Component counter={100} /&gt;
&lt;DeeperComponent counter={{ counter: 100 }} /&gt;</code></pre>
<ul>
<li><p>첫 번째 Component는 counter가 원시값이므로 비교 가능 → memo가 잘 작동함</p>
</li>
<li><p>두 번째 DeeperComponent는 counter가 객체라서 매 렌더링마다 { counter: 100 }이 새로 생성됨 → 참조가 달라짐 → shallowEqual에서 false</p>
</li>
</ul>
<p><strong>=&gt; 즉, 내부 값은 같지만 객체의 참조가 바뀌어 memo가 무효</strong></p>
<blockquote>
<p>근데도 깊은 비교가 아닌 얕은 비교를 하는 이유?</p>
</blockquote>
<p> <strong>성능의 문제점</strong>
객체 안에 객체가 몇 개 들어있을지 모르기 때문에 재귀로 비교하다 보면 렌더링마다 모든 값 전체를 순회해야 한다.</p>
<p>그래서 리액트는 얕은 비교까지만 지원하고 더 깊은 구조를 비교해야 할 땐 개발자가 직접 <code>useMemo</code>, <code>useCallback</code> 등으로 불필요한 객체 생성이나 참조 변경을 막아야 한다.</p>
<br/>

<h2 id="💡-함수">💡 함수</h2>
<h3 id="함수를-정의하는-4가지-방법">함수를 정의하는 4가지 방법</h3>
<h4 id="1-함수-선언문">1. 함수 선언문</h4>
<pre><code class="language-js">hello();  // &quot;hi&quot;
function hello(){ console.log(&quot;hi&quot;); }</code></pre>
<ul>
<li>함수 자체가 호이스팅 됨 =&gt; 선언이 스코프 맨 위로 끌어올려져 선언 전 호출 가능.</li>
</ul>
<h4 id="2-함수-표현식">2. 함수 표현식</h4>
<pre><code class="language-js">console.log(typeof hello); // &quot;undefined&quot;
hello();                   // TypeError
var hello = function(){};</code></pre>
<ul>
<li>변수에 함수를 할당</li>
<li>호이스팅은 변수 hello만 됨 !</li>
</ul>
<h4 id="3-function-생성자">3. Function 생성자</h4>
<pre><code class="language-js">const add = new Function(&#39;a&#39;, &#39;b&#39;, &#39;return a + b&#39;);</code></pre>
<ul>
<li>매개변수/본문을 문자열로 작성 → 가독성·성능·보안 불리</li>
<li>클로저가 안 생김</li>
</ul>
<h4 id="4-화살표-함수">4. 화살표 함수</h4>
<pre><code class="language-js">add() //error
const add = (a, b) =&gt; a + b;</code></pre>
<ul>
<li>호이스팅 X</li>
<li>this 바인딩 없음 → 상위 스코프의 this를 캡처(렉시컬 this).</li>
<li>arguments 없음 → ...rest 사용.</li>
<li>new로 생성자 호출 불가, prototype 없음.</li>
<li>메서드로 쓸 땐 일반 함수 권장(특히 클래스/객체 메서드).</li>
</ul>
<blockquote>
<p>클래스나 객체 메서드 내부에서는 화살표 함수 대신 일반 함수를 쓰기</p>
</blockquote>
<h3 id="선언문과-표현식의-차이">선언문과 표현식의 차이</h3>
<p>선언문과 표현식의 가장 크고 중요한 차이는 바로 <strong>호이스팅</strong> !!</p>
<p><strong>호이스팅이 뭔데 ?</strong></p>
<p>MDN 공식문서에서 말하는 호이스팅이란</p>
<blockquote>
<p>인터프리터가 코드를 실행하기 전에 함수, 변수, 클래스 또는 임포트(import)의 선언문을 해당 범위의 맨 위로 끌어올리는 것처럼 보이는 현상</p>
</blockquote>
<p>즉 함수 선언문이 마치 코드 맨 앞단에 작성된 것처럼 작동하는 현상을 의미한다.</p>
<h4 id="선언문">선언문</h4>
<pre><code class="language-js">hello() // hello

function hello() {
console.log(&#39;hello&#39;)
}

hello() // hello</code></pre>
<p>위의 코드를 보면 함수 선언문 전에 <code>hello()</code> 함수를 실행해도 함수가 잘 실행되는 걸 볼 수 있다. 어떻게 이런일이 일어날 수 있는걸까?</p>
<p>바로 <strong>호이스팅</strong>이 일어났기 때문이다.</p>
<pre><code class="language-js">function hello() {
console.log(&#39;hello&#39;)
}

hello() // hello

hello() // hello</code></pre>
<p>이런식으로 함수 표현식은 위로 <strong>끌어올려진 것</strong>처럼 작동한다.</p>
<p>실제 호이스팅이 일어나는 이유는 <strong>함수 자체가 미리 메모리에 등록</strong>하기 때문이다. 따라서 선언 전에 호출이 가능한 것 !</p>
<h4 id="표현식">표현식</h4>
<pre><code class="language-js">console.log(typeof hello === &#39;undefined&#39;) // true

hello() // Uncaught TypeError: hello is not a function

var hello = function () {
console.log(&#39;hello&#39;)
}

hello() // hello</code></pre>
<p>함수 표현식은 함수 선언문과 다르게 정상적으로 호출되지 않고 undefined로 남아있는 것을 볼 수 있다.</p>
<pre><code class="language-js">//var hello;  // 선언만 호이스팅되어 undefined로 초기화됨

hello; // undefind =&gt; 식별자는 이미 존재함 (위로 호이스팅 되었기에)
hello(); // 함수 호출 불가 =&gt; hello가 아직 undefined 상태이기 때문

var hello = function () {
console.log(&#39;hello&#39;)
}

hello(); //hello</code></pre>
<p>함수 표현식은 호이스팅이 일어나지 않는 걸 볼 수 있다.
함수와 다르게 변수는 <strong>런타임 이전에 undefined로 초기화</strong>되고 할당문이 실행되는 시점, 즉 <strong>런타임 시점에 함수가 할당</strong>되어 작동하기 때문이다.</p>
<br/>

<h3 id="좋은-함수-작성-원칙">좋은 함수 작성 원칙</h3>
<h4 id="함수의-부수-효과-최소화">함수의 부수 효과 최소화</h4>
<p>함수의 부수 효과란</p>
<blockquote>
<p>함수 내의 작동으로 인해 함수가 아닌 함수 외부에 영향을 끼치는 것을 의
미</p>
</blockquote>
<p>이러한 부수 효과가 없는 함수를 <strong>순수 함수</strong>라 하고 부수 효과가 존재하는 함수를 <strong>비순수 함수</strong>라고 한다.</p>
<p>부수효과들</p>
<table>
<thead>
<tr>
<th>상황</th>
<th>부수 효과의 예</th>
<th>이유</th>
</tr>
</thead>
<tbody><tr>
<td>데이터 요청</td>
<td><code>fetch()</code></td>
<td>서버에 요청(외부 영향)</td>
</tr>
<tr>
<td>콘솔 출력</td>
<td><code>console.log()</code></td>
<td>브라우저 콘솔 조작</td>
</tr>
<tr>
<td>문서 제목 변경</td>
<td><code>document.title = ...</code></td>
<td>DOM 조작(외부 변경)</td>
</tr>
<tr>
<td>setTimeout</td>
<td>브라우저 타이머에 영향</td>
<td>외부 환경 의존</td>
</tr>
</tbody></table>
<blockquote>
<p>부수 효과를 완전히 없앨 수는 없고 제한된 곳에서만 안전하게 처리해야 한다는 게 핵심 !</p>
</blockquote>
<h4 id="리액트에서-부수-효과를-다루는-법-→-useeffect">리액트에서 부수 효과를 다루는 법 → useEffect</h4>
<p>리액트는 <strong>useEffect</strong>를 통해 이런 부수 효과를 제어된 방식으로 실행</p>
<pre><code class="language-jsx">function Example({ userId }) {
  const [data, setData] = useState(null);

  // 외부 요청(부수 효과)은 useEffect 내부에서만 수행
  useEffect(() =&gt; {
    fetch(`/api/user/${userId}`)
      .then(res =&gt; res.json())
      .then(setData);
  }, [userId]);

  return &lt;div&gt;{data?.name}&lt;/div&gt;;
}</code></pre>
<p>핵심 포인트:</p>
<ul>
<li>렌더링은 순수하게 유지</li>
<li>외부 동작(데이터 요청 등)은 useEffect 내부에서만 수행</li>
<li>이렇게 하면 렌더링 로직과 부수 효과 로직이 명확히 분리</li>
</ul>
<blockquote>
<p>하지만 <strong>useEffect</strong>는 DOM을 생성 후 paint 이후에 동작하기 때문에 렌더링에 영향을 주는 로직에서는 사용하면 X !!</p>
</blockquote>
<table>
<thead>
<tr>
<th>상황</th>
<th>예시</th>
<th>이유</th>
</tr>
</thead>
<tbody><tr>
<td><strong>1. 외부 데이터 요청 / API 호출</strong></td>
<td><code>fetch</code>, <code>axios</code>, <code>supabase</code>, <code>firebase</code> 등</td>
<td>네트워크 I/O는 렌더링과 독립적</td>
</tr>
<tr>
<td><strong>2. DOM 직접 조작 (Ref 기반)</strong></td>
<td><code>element.focus()</code>, <code>scrollIntoView()</code> 등</td>
<td>리액트가 관리하지 않는 영역</td>
</tr>
<tr>
<td><strong>3. 타이머/인터벌 관리</strong></td>
<td><code>setTimeout</code>, <code>setInterval</code></td>
<td>브라우저 자원과의 상호작용</td>
</tr>
<tr>
<td><strong>4. 구독/리스너 등록 및 해제</strong></td>
<td><code>addEventListener</code>, WebSocket, Firebase 등</td>
<td>컴포넌트가 “외부 이벤트”를 듣는 행위</td>
</tr>
</tbody></table>
<p>위와 같이 렌더링에 영향을 안주고 외부 세계에 영향을 주거나 의존하는 코드를 처리할 때만 사용해야한다.</p>
<h4 id="부수-효과를-최소화해야하는-이유">부수 효과를 최소화해야하는 이유</h4>
<table>
<thead>
<tr>
<th>이유</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>예측 가능성</strong></td>
<td>같은 입력에 대해 결과가 일정해야 버그 추적이 쉬움</td>
</tr>
<tr>
<td><strong>테스트 용이성</strong></td>
<td>순수 함수는 테스트하기 쉽고 빠름</td>
</tr>
<tr>
<td><strong>컴포넌트 안정성</strong></td>
<td>불필요한 재렌더링, 상태 꼬임 방지</td>
</tr>
<tr>
<td><strong>협업과 유지보수</strong></td>
<td>다른 개발자가 읽고 이해하기 쉬움</td>
</tr>
</tbody></table>
<br/>

<h4 id="가능한-한-함수를-작게-만들기">가능한 한 함수를 작게 만들기</h4>
<p>함수는 한 가지 일만 해야 한다. 한 함수에 여러 기능들이 존재한다면 유지보수성도 어렵고 가독성도 좋지 않고 재사용에 있어서도 좋지 않다.</p>
<p>따라서 단일책임 원칙을 따라야 한다.</p>
<p>ESLint의 max-lines-per-function 규칙은 하나의 함수가 너무 길면 경고를 띄워라라는 규칙이다. </p>
<pre><code>&quot;max-lines-per-function&quot;: [&quot;warn&quot;, { &quot;max&quot;: 50 }]</code></pre><p>기본 ESLint 규칙에 있을 정도로 생각하면서 작성해야 할 부분이다.</p>
<h4 id="함수-길이의-적당함-">함수 길이의 적당함 ?</h4>
<blockquote>
<p>상황에 따라 다르지만, 짧을수록 좋다</p>
</blockquote>
<p>하지만 실무에서는 보통 20줄 이내면 읽기 편하고 10줄 내외면 가장 이상적이다.</p>
<blockquote>
<p>하지만 한 함수에 하나의 기능을 하지만 코드가 길어질 수 있기 때문에
무조건 짧아야 한다는건 잘못된 생각이다 !!</p>
</blockquote>
<table>
<thead>
<tr>
<th>문제</th>
<th>해결 방법</th>
</tr>
</thead>
<tbody><tr>
<td>한 함수가 여러 일을 함</td>
<td><strong>작은 함수로 분리</strong></td>
</tr>
<tr>
<td>비슷한 로직 반복</td>
<td><strong>공통 함수 추출</strong></td>
</tr>
<tr>
<td>긴 조건문</td>
<td><strong>의미 있는 변수/함수로 치환</strong></td>
</tr>
<tr>
<td>중첩 콜백 많음</td>
<td><strong>async/await, map/filter 등 활용</strong></td>
</tr>
</tbody></table>
<br/>

<p>나쁜 예시</p>
<pre><code class="language-js">function processOrder(order) {
  if (!order.user) return;
  if (!order.items.length) return;

  // 재고 확인
  for (const item of order.items) {
    if (item.stock === 0) return;
  }

  // 결제 처리
  const result = pay(order.total);
  if (!result.success) return;

  // 알림 전송
  sendEmail(order.user.email);
  sendSMS(order.user.phone);
}</code></pre>
<p>좋은 예시</p>
<pre><code class="language-js">function validateOrder(order) { /* ... */ }
function checkStock(items) { /* ... */ }
function processPayment(order) { /* ... */ }
function notifyUser(user) { /* ... */ }

function processOrder(order) {
  if (!validateOrder(order)) return;
  if (!checkStock(order.items)) return;
  if (!processPayment(order)) return;
  notifyUser(order.user);
}</code></pre>
<br/>

<h4 id="누구나-이해할-수-있는-함수명">누구나 이해할 수 있는 함수명</h4>
<p>작은 프로젝트는 코드의 양이 적고, 작성자 본인만 이해하면 충분하기 때문에 temp, data, result 같은 이름을 써도 큰 문제가 없다.
하지만 프로젝트가 커지고 기능이 복잡해지며 여러 사람이 함께 작업하게 되면
이름이 불분명한 변수나 함수는 가독성을 떨어뜨리고 유지보수를 어렵게 만든다.</p>
<p><strong>좋은 네이밍의 조건</strong></p>
<blockquote>
<p>이름만 보고 역할이 드러나는 코드</p>
</blockquote>
<pre><code>// ❌ 나쁜 예
function processData(a, b) {
  return a * b;
}

// ✅ 좋은 예
function calculateInsuranceFee(insurance, years) {
  return insurance * years;
}</code></pre><p><strong>좋은 이름을 짓기 위한 세 가지 원칙</strong></p>
<table>
<thead>
<tr>
<th>원칙</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>명확하게</strong></td>
<td>함수 이름만 보고 “무슨 일을 하는지” 알 수 있어야 한다.</td>
</tr>
<tr>
<td><strong>일관성 있게</strong></td>
<td>프로젝트 내 동일한 역할의 함수는 동일한 패턴으로 이름을 짓는다. 예: <code>handleClick</code>, <code>handleSubmit</code>, <code>fetchData</code> 등</td>
</tr>
<tr>
<td><strong>읽기 쉽게</strong></td>
<td>약어나 축약어는 피하고, 가능한 한 자연어처럼 읽히게 작성한다.</td>
</tr>
</tbody></table>
<pre><code class="language-js">// ✅ 좋은 패턴
handleSubmit();     // 동작 수행
fetchUserData();    // 외부 요청
calculatePrice();   // 계산
getUserInfo();      // 반환</code></pre>
<h2 id="💡-클로저">💡 클로저</h2>
<p>리액트에서 함수 컴포넌트와 훅이 등장한 16.8 버전을 기점으로 이 클로저라는 개념이 리액트에서 적극적으로 사용되기 시작하면서 클로저를 빼놓고서는 리액트가 어떤 식으로 작동하는지 이해할 수 없다.</p>
<h3 id="클로저의-정의">클로저의 정의</h3>
<p>MDN 공식문서에서 말하는 클로저 정의</p>
<blockquote>
<p>함수와 함수가 선언된 어휘적 환경(Lexical Scope)의 조합</p>
</blockquote>
<h4 id="렉시컬-스코프-어휘적-환경">렉시컬 스코프 (어휘적 환경)</h4>
<blockquote>
<p>스코프: 변수의 유효 범위</p>
</blockquote>
<pre><code class="language-js">function add() {
  const a = 10
  function innerAdd() {
    const b = 20
    console.log(a + b)
  }
  innerAdd() // 30
}

add()</code></pre>
<ul>
<li>a 변수의 유효 범위는 add 전체</li>
<li>b 변수의 유효 범위는 innderAdd 전체
=&gt;  innerAdd는 add 내부에서 선언돼 있어 a를 사용 가능!</li>
</ul>
<blockquote>
<p>즉 렉시컬 스코프는 변수가 코드 내부에서 어디서 선언됐는지를 말하는 것</p>
</blockquote>
<h4 id="전역-스코프">전역 스코프</h4>
<blockquote>
<p>전역스코프: 변수를 전역 레벨에 선언하는 것</p>
</blockquote>
<pre><code class="language-js">var global = &#39;global scope&#39;

function heUo() {
  console.log(global)
}
console.log(global) // global scope

hello() // global scope

console.log(global === window.global) // true</code></pre>
<ul>
<li>global이라는 변수를 var와 함께 선언했더니 <strong>전역 스코프</strong>와 <strong>hello 스크프</strong> 모두에서 global 변수에 접근 가능</li>
</ul>
<h4 id="함수-스코프">함수 스코프</h4>
<blockquote>
<p>자바스크립트는 기본적으로 함수 레벨 스코프를 따른다. 즉, {} 블록이 스코프 범위를 결정하지 X</p>
</blockquote>
<pre><code class="language-js">if (true) {
  var global = &#39;global scope&#39;
}

console.log(global) // &#39;global scope&#39;
console.log(global === window.global) // true</code></pre>
<ul>
<li>{} 내부에서 선언돼 있는데, {} 밖에서도 접근이 가능
=&gt; 기본적으로 자바스크립트는 함수 레벨 스코프를 가지고 있기 때문</li>
</ul>
<pre><code class="language-js">function hello() {
  var local = &#39;local variable&#39;
  console.log(local) // local variable
}

hello()
console.log(local) // Uncaught ReferenceError： local is not defined</code></pre>
<ul>
<li>단순한 if 블록과는 다르게 함수 블록 내부에서는 일반적으로 예측하는 것과 같이 스코프가 결정</li>
</ul>
<br/>

<h3 id="리액트에서의-클로저">리액트에서의 클로저</h3>
<pre><code class="language-js">function Component() {
  const [count, setCount] = useState(0);

  function handleClick() {
    setCount((prev) =&gt; prev + 1);
  }
}</code></pre>
<ul>
<li>이때 useState는 한 번 실행되고 끝났는데 이후에 setCount는 어떻게 계속 최신 count 값을 알고 있을까?
=&gt; 바로 <strong>클로저</strong> 때문</li>
</ul>
<p>setCount 함수는 useState 내부에서 만들어질 때
count가 저장된 환경(Lexical Environment) 을 함께 기억한다.</p>
<p>그래서 나중에 setCount를 호출해도 useState 실행이 끝난 이후임에도 불구하고 그 당시의 state 변수에 접근할 수 있다.</p>
<blockquote>
<p>즉, useState는 내부적으로 클로저를 이용한 상태 은닉 구조를 사용한다.</p>
</blockquote>
<h4 id="클로저-주의사항">클로저 주의사항</h4>
<p>클로저는 유용하지만 성능이나 메모리에 영향을 줄 수 있다.</p>
<p><strong>1. 잘못된 스코프 바인딩</strong></p>
<pre><code class="language-js">for (var i = 0; i &lt; 5; i++) {
  setTimeout(function () {
    console.log(i);
  }, i * 1000);
}</code></pre>
<p>의도: 1초 간격으로 0, 1, 2, 3, 4 출력
결과: 전부 5만 출력</p>
<p><strong>이유</strong>:</p>
<ul>
<li>var는 함수 레벨 스코프라서 for 루프가 끝난 시점에는 전역 i = 5만 남음</li>
<li>각 setTimeout은 모두 같은 i(=5) 를 참조</li>
</ul>
<p><strong>2. 메모리 낭비</strong></p>
<pre><code class="language-js">function heavyJobWithClosure() {
  const longArr = Array.from({ length: 10000000 }, (_, i) =&gt; i + 1);

  return function () {
    console.log(longArr.length);
  };
}

const innerFunc = heavyJobWithClosure();
button.addEventListener(&#39;click&#39;, innerFunc);</code></pre>
<ul>
<li>innerFunc가 longArr를 클로저로 기억</li>
<li>브라우저는 longArr를 GC(가비지 컬렉션) 하지 못함</li>
</ul>
<h4 id="클로저의-단점-정리">클로저의 단점 정리</h4>
<table>
<thead>
<tr>
<th>문제</th>
<th>원인</th>
<th>해결 방법</th>
</tr>
</thead>
<tbody><tr>
<td>메모리 점유</td>
<td>외부 변수를 계속 기억함</td>
<td>클로저 내부에서 필요한 최소한의 값만 유지</td>
</tr>
<tr>
<td>예기치 않은 참조</td>
<td>var로 선언된 전역 스코프 공유</td>
<td><code>let</code> / <code>const</code> 사용</td>
</tr>
<tr>
<td>디버깅 어려움</td>
<td>내부 상태가 외부에서 안 보임</td>
<td>주석, 명확한 함수 이름으로 보완</td>
</tr>
</tbody></table>
]]></description>
        </item>
        <item>
            <title><![CDATA[esbuild와 vite (feat 번들러)]]></title>
            <link>https://velog.io/@hour_2/esbuild%EC%99%80-vite-feat-%EB%B2%88%EB%93%A4%EB%9F%AC</link>
            <guid>https://velog.io/@hour_2/esbuild%EC%99%80-vite-feat-%EB%B2%88%EB%93%A4%EB%9F%AC</guid>
            <pubDate>Sat, 01 Nov 2025 18:47:23 GMT</pubDate>
            <description><![CDATA[<p>프론트엔드 개발 환경을 이해하기 위해서는 <strong>번들링</strong>이라는 개념을 빼놓을 수 없다.
<strong>esbuild</strong>와 <strong>vite</strong> 역시 이러한 번들링 과정을 효율적으로 개선하기 위해 등장한 도구들이기 때문이다.</p>
<p>따라서 본문에서는 두 도구를 살펴보기 전에 먼저 <strong>번들러</strong>의 개념과 등장 배경을 정리하고자 한다.</p>
<h2 id="번들러란-무엇이고-왜-필요한가">번들러란 무엇이고 왜 필요한가?</h2>
<p>프론트엔드 내에서 번들러, 번들링이란 개념은 왜 생겼을까?</p>
<p>웹이 아주 단순하던 시절에는 번들링이 필요하지 않았다.</p>
<pre><code>&lt;script src=&quot;./index.js&quot;&gt;&lt;/script&gt;</code></pre><p>위와 같은 한 줄의 코드면 브라우저가 <code>./index.js</code> 하나의 파일을 받아서 실행했었다.</p>
<p>하지만 이건 웹이 단순했을 시절의 이야기이고, 현재는 이때와 다르다.</p>
<p>현재는 웹 서비스의 기능이 많아지고, 겹치는 공통 코드가 생기고, 여러 사람이 동시에 개발을 하고, 브라우저가 이해하지 못하는 TypeScript, jsx 등의 언어로 코드를 작성하고 있다.</p>
<p>그래서 우리는 자연스럽게 코드를 파일 단위로 쪼개고 <strong>모듈</strong>로 관리하기 시작했다. (import/export)
예를 들어 React 컴포넌트, util, API 클라이언트, hooks, 타입 정의 등 이런 것들을 전부 하나의 파일에 넣어두면 유지보수가 불가능하기에 기능별로 나눈 것이다.</p>
<blockquote>
<p>하지만 최선의 해결 방법이 아니였다. 여기에서도 <strong>많은 문제</strong>들이 생겼다.</p>
</blockquote>
<br/>

<h3 id="번들러가-등장하기-전에-존재했던-문제점">번들러가 등장하기 전에 존재했던 문제점</h3>
<h4 id="1-파일이-너무-많아진다">1. 파일이 너무 많아진다.</h4>
<p>모듈을 잘게 나누는 것은 유지보수에 유리하지만 브라우저는 파일 하나를 받을 때마다 <strong>새로운 HTTP 요청</strong>을 수행해야 한다. 요청이 적으면 큰 문제가 되지 않지만 200개 이상의 모듈을 로드해야하는 상황이라면 <strong>성능 저하</strong>의 문제로 이어진다. 특히 HTTP/1.1 환경에서는 동시에 처리할 수 있는 요청의 수가 제한되어 있었기에 병목 현상이 심각했다.
이로 인해 <code>개발은 모듈 단위로 / 배포는 묶어서</code> 처리하는 도구가 필요했다.</p>
<h4 id="2-의존성-순서를-지켜야-한다">2. 의존성 순서를 지켜야 한다</h4>
<p>모듈 간의 <strong>의존성</strong>이 존재하는 경우, 불러오는 <strong>순서</strong>를 반드시 지켜야 한다.
예를 들어 A 모듈이 B를 사용하고 B가 C를 사용한다면, C-&gt; B-&gt; A 순서로 로드되어야 한다. 이러한 순서를 개발자가 <code>&lt;script&gt;</code> 태그를 통해 수동적으로 지정하는 것은 사실상 불가능에 가깝다.
따라서 의존 관계를 자동으로 분석하고 올바른 순서로 묶어주는 도구가 필요했다.</p>
<h4 id="3-브라우저가-모르는-문법을-쓰고-있다">3. 브라우저가 모르는 문법을 쓰고 있다.</h4>
<p>현제 프론트엔드 개발에서는 브라우저가 <strong>직접적으로</strong> 이해하지 못하는 다양한 문법과 언어가 사용된다.</p>
<ul>
<li>최신 ESNext 문법(예: optional chaining, async/await 등)</li>
<li>TypeScript</li>
<li>jsx</li>
<li>javaScript 코드 내에서 스타일을 불러오는 방식 (CSS-in-JS)
이러한 코드들은 브라우저가 그대로 이해할 수 없기 때문에 누군가가 이를 미리 변환해야 한다.</li>
</ul>
<p>위의 문제들을 해결하기 위해 등장한 것이 바로 <strong>번들러</strong>이다.</p>
<blockquote>
<p>다시 말해 번들러는 웹서비스가 방대해지며 코드를 분리한 모듈로 관리를 하기 위한 도구이다.</p>
</blockquote>
<br/>

<h3 id="번들러의-핵심-아이디어">번들러의 핵심 아이디어</h3>
<p>간단하게 3단계로 설명한다면 아래와 같이 설명할 수 있다.</p>
<blockquote>
<ul>
<li>내가 작성한 <strong>여러 개의 모듈 파일</strong>을</li>
<li><strong>의존성 그래프</strong>를 따라가면서 하나씩 읽은 다음</li>
<li>브라우저가 바로 실행할 수 있는 형태로 <strong>묶어서(bundle)</strong> 내보내는 도구</li>
</ul>
</blockquote>
<p>위의 3단계를 더 자세하게 설명하자면</p>
<ul>
<li><p><strong>엔트리(entry) 파일</strong>을 정한다. (예시: <code>src/main.tsx</code>)</p>
</li>
<li><p>그 파일 안에서 <code>import ... from ...</code> 한 걸 전부 따라가면서 현재 이 프로젝트가 실제로 쓰고 있는 파일 목록을 전부 찾아낸다. 이걸 <strong>의존성 그래프(dependency graph)</strong> 를 만든다고 한다.
<img src="https://velog.velcdn.com/images/hour_2/post/06391558-558b-447e-9a1b-10ccdecc059e/image.png" alt=""></p>
</li>
<li><p>이후 <strong>브라우저가 이해할 수 있는 순서와 형식으로 재배치</strong>하고 하나(또는 여러 개)로 뭉친다.</p>
</li>
<li><p>이때 그냥 이어붙이는 게 아닌 아래의 과정을 거친다.</p>
<ul>
<li>최신 문법을 구버전으로 변환(transpile)</li>
<li>쓰지 않는 코드를 제거(tree-shaking)</li>
<li>변수 이름을 최소화(minify)</li>
<li>소스맵을 만들어서 디버깅 가능하도록</li>
<li>CSS나 이미지 같은 리소스도 같이 처리할 수 있도록</li>
</ul>
</li>
</ul>
<blockquote>
<p>즉 <strong>개발자 친화적인 코드 → 배포 친화적인 코드로 바꿔주는 변환 과정 전체</strong>를 수행한다.</p>
</blockquote>
<p>지금은 Vite, Next.js, Remix 같은 프레임워크들이 이 번들 과정을 많이 감춰놓아서 우리가 매번 직접 <code>webpack.config.js</code> 를 쓰지 않아도 되지만 내부적으로는 여전히 의존성을 읽고, 변환하고, 묶어서 내보내는 작업이 돌아가고 있다. 이게 바로 <strong>번들러가 필요한 이유</strong>다.</p>
<p>번들러에는 여러가지 종류들이 있다. </p>
<p>나는 그 중에서 빠르고 최근에 사람들이 많이 사용 하는 <strong>esbuild</strong>를 알아볼 예정이다.</p>
<h2 id="그-중-우리가-볼-번들러-esbuild">그 중 우리가 볼 번들러 esbuild</h2>
<p>esbuild를 한 줄로 설명한다면 아래처럼 설명할 수 있다.</p>
<blockquote>
<p><strong>esbuild</strong>는 웹팩처럼 모든 것을 수행하는 거대한 플랫폼을 목표로 하는 게 아니라, <strong>JS/CSS를 빠르게 읽고 변환하고 묶는</strong> 그 핵심 부분을 최대한 빠르게 만든 도구다.</p>
</blockquote>
<h3 id="왜-esbuild가-빠른걸까">왜 esbuild가 빠른걸까?</h3>
<p><strong>esbuild의 공식문서</strong>에서는 각각의 요인은 단독으로는 미미한 성능 향상을 보이지만 이들이 모두 결합되었을 때 <strong>기존 번들러에 비해 수십 배에 달하는 속도 차이</strong>를 만들어낸다고 설명하고 있다.</p>
<p>그 중 주요하게 보면 좋을 <strong>4가지</strong>를 설명하겠다.</p>
<h4 id="1-go-언어로-작성된-esbuild">1. Go 언어로 작성된 esbuild</h4>
<p>대부분의 <strong>기존 번들러</strong>는 <strong>Node.js 위에서 돌아가는 자바스크립트 프로그램</strong>이다. (ex=&gt; webpack, Rollup, Parcel ...)
그러나 명령어 실행 후 종료되는 CLI 프로그램 특성상 JIT(Just-In-Time) 컴파일 방식을 사용하는 자바스크립트 언어는 이러한 환경에서 비효율적일 수밖에 없다. 번들러가 실행되는 순간 Node.js는 번들러의 자바스크립트 <strong>코드를 해석</strong>하는데 상당한 시간을 소비한다.</p>
<p>즉, 실제로 사용자의 코드를 번들링하기도 전에 <strong>런타임이 자신의 코드를 파싱하느라 리소스를 낭비하는 구조</strong>이다.
조금 더 쉽게 말하면 Node.js는 <strong>번들러 코드 자체</strong>를 해석하느라 시간이 많이 지체되느라 번들링이 시작도 안되고 있다는 이야기이다.</p>
<p>반면에 esbuild는 Go 언어로 작성되었기에 실행 가능한 바이너라 파일 형태로 컴파일되어 동작한다. </p>
<p>이로 인해 번들링을 시작하기 전 런타임이 자신의 코드를 해석하는 비용이 사실상 존재하지 않는다._ (바이너리 파일 → 이미 기계어로 되어 있어서 운영체제가 프로그램 실행 명령을 내리는 순간 곧바로 CPU가 코드 수행)_</p>
<blockquote>
<p>즉 자바스크립트 기반 번들러는 매 실행 시 런타임 초기화 및 파싱 과정을 반복해야하지만, esbuild는 이러한 오버헤드를 Go 기반 네이티브 구조로 완전히 제거하는 것이다.</p>
</blockquote>
<br/>

<h4 id="2-병렬-처리를-위해-설계된-go-언어">2. 병렬 처리를 위해 설계된 Go 언어</h4>
<blockquote>
<p><strong>병렬 처리</strong>: 하나의 일을 여러 개의 CPU 코어가 동시에 나누어 처리하는 것</p>
</blockquote>
<p>자바스크립트는 워커 스레드_(CPU가 동시에 여러 일을 수행할 수 있도록 하는 작업 단위)_를 통해 병렬 처리를 구현할 수는 있지만 esbuild가 사용하는 Go 언어의 벙렬 처리 모델은 근본적으로 구조가 다르다. </p>
<p>Go는 스레드 간 <strong>메모리 공유</strong>가 가능하며, 가비지 컬렉션 또한 단일 공유 힙에서 동작한다. 
이로 인해 파싱, 코드 생성 등 CPU 집약적인 작업을 여러 스레드가 <strong>동시에 수행</strong>할 수 있다. </p>
<p>반면 자바스크립트는 워커 간 메모리를 공유하지 못하고 데이터를 직렬화하여 전달해야 하기 때문에 병렬성이 크게 제한된다.
즉, 병렬 처리를 지원하더라도 스레드 간 데이터 이동 비용이 발생하여 효율이 낮다.</p>
<p>esbuild는 이러한 Go의 구조적 장점을 최대한 활용한다. 전체 빌드 과정은 <strong>파싱 -&gt; 링크 -&gt; 코드 생성</strong>의 세 단계로 구분된다.</p>
<blockquote>
<p><strong>파싱</strong>과 <strong>코드 생성</strong> 단계는 <strong>병렬화</strong>가 용이하여 모든 CPU 코어를 적극적으로 활용한다.
<strong>링크</strong> 단계는 본질적으로 <strong>직렬 처리</strong>가 필요한 과정이므로 <strong>예외적으로 단일 스레드</strong>로 처리된다.</p>
</blockquote>
<p>또한 여러 엔트리 포인트가 동일한 라이브러리를 import하는 경우에 esbuild는 <strong>텍스트</strong>메모리 공유를 통해 중복 파싱을 방지하고 <strong>캐시된 데이터를 재활용</strong>한다.</p>
<br/>

<h4 id="3-모든-기능을-직접-구현">3. 모든 기능을 직접 구현</h4>
<p>esbuild의 또 다른 특징은 외부 라이브러리에 의존하지 않고 내부 기능을 모두 자체적으로 구현했다는 점이다.</p>
<p>대부분의 번들러는 다음과 같은 구조를 따른다.</p>
<blockquote>
<ul>
<li>TypeScript 파서는 공식 타입스크립트 컴파일러에 의존</li>
<li>이후 Babel을 통해 코드 변환 수행</li>
<li>다양한 플러그인 체계를 거쳐 추가 변환 및 최적화 실행</li>
</ul>
</blockquote>
<p>이 과정에서 데이터 구조가 반복적으로 변경되고, 문자열과 AST(Abstract Syntax Tree) 간 변환이 여러 번 일어난다. 이러한 과정은 필연적으로 성능 저하로 이어진다.</p>
<p>반면 esbuild는 다음과 같은 주요 구성 요소를 모두 자체적으로 구현하였다.</p>
<blockquote>
<ul>
<li>JavaScript/TypeScript 파서</li>
<li>코드 변환기</li>
<li>번들러</li>
<li>코드 생성기</li>
</ul>
</blockquote>
<p>이로 인해 esbuild는 한 번 생성한 AST를 여러 단계에서 재사용하며, 중간 표현 변환을 최소화한다. “이 데이터가 이후에도 재활용될 것”이라는 점을 고려한 구조적 설계를 통해 메모리 접근 횟수를 줄이고, CPU 캐시 효율을 높인다.</p>
<p>공식 문서에 따르면, esbuild는 전체 자바스크립트 AST를 단 세 번만 순회한다.</p>
<ol>
<li>토큰화, 파싱, 스코프 구성, 심볼 선언</li>
<li>심볼 바인딩, 문법 최소화, JSX/TS → JS 변환, ESNext → ES2015 변환</li>
<li>식별자 및 공백 최소화, 코드 생성, 소스맵 생성</li>
</ol>
<p>다른 번들러들은 이 과정을 여러 외부 라이브러리에 위임하여 문자열 ↔ AST 변환을 반복하기 때문에 상대적으로 느리다.</p>
<p>즉, esbuild는 <strong>일관된 내부 데이터 구조와 최소한의 AST 순회</strong>를 통해 뛰어난 성능을 달성한다.</p>
<h4 id="4-효율적인-메모리-사용">4. 효율적인 메모리 사용</h4>
<p>컴파일러 성능의 핵심은 방대한 데이터를 얼마나 적은 연산으로 처리하느냐에 달려 있다. esbuild는 데이터 표현 변환을 최소화하여 CPU 캐시 적중률을 높이고, 메모리 접근 횟수를 줄인다.</p>
<p>Go 언어는 구조체 필드를 메모리상에 밀집하게 배치할 수 있으며, 여러 불리언 값을 1바이트 단위로 압축하여 저장할 수 있다. 또한 값 타입을 직접 구조체 내부에 포함시킬 수 있어(embedding) 불필요한 동적 메모리 할당을 줄인다.</p>
<p>반면 자바스크립트는 이러한 메모리 최적화가 어렵다. JIT 히든 클래스, 박싱된 숫자 객체, 동적 프로퍼티 접근 등으로 인해 동일한 연산을 수행하더라도 더 많은 메모리와 연산 비용이 소모된다.</p>
<p>결과적으로 esbuild는 “작업을 최소화하는 것이 곧 성능 향상으로 이어진다”는 단순한 원칙을 철저히 구현한 번들러이다.
AST는 세 번만 순회하며, 불필요한 표현 변환을 최소화하고, 일관된 데이터 구조를 유지한다. 이러한 설계적 일관성이 누적되어, 결과적으로 기존 번들러보다 수십 배 빠른 성능을 제공한다.</p>
<br/>

<h3 id="실제-벤치마크-결과">실제 벤치마크 결과</h3>
<p>공식 문서에서 제시한 벤치마크 결과에 따르면, esbuild의 성능은 다른 주요 번들러와 비교했을 때 압도적으로 빠르다.</p>
<blockquote>
<p>테스트 환경: 6코어 MacBook Pro, macOS, 16GB RAM</p>
</blockquote>
<h4 id="javascript-대규모-프로젝트-시뮬레이션">JavaScript 대규모 프로젝트 시뮬레이션</h4>
<p><img src="https://velog.velcdn.com/images/hour_2/post/6b6f736f-41f3-413e-9ce8-20fbc619ef91/image.png" alt=""></p>
<ul>
<li>esbuild: 0.39초 (기준 1배)</li>
<li>Parcel 2: 14.91초 (약 38배 느림)</li>
<li>Rollup 4 + Terser: 34.10초 (약 87배 느림)</li>
<li>Webpack 5: 41.21초 (약 106배 느림)</li>
</ul>
<h4 id="typescript-대규모-프로젝트-시뮬레이션">TypeScript 대규모 프로젝트 시뮬레이션</h4>
<p><img src="https://velog.velcdn.com/images/hour_2/post/18c662aa-b345-4c3b-a2db-ac2b363716b7/image.png" alt=""></p>
<ul>
<li>esbuild: 0.10초</li>
<li>Parcel 2: 6.91초 (약 69배 느림)</li>
<li>Webpack 5: 16.69초 (약 167배 느림)</li>
</ul>
<p>이 결과는 다른 번들러의 설계가 잘못되었다는 의미가 아니라 esbuild가 <strong>전혀 다른 구조적 접근을 취했기 때문에 가능한 성능 차이</strong>임을 보여준다.</p>
<p>많은 번들러는 TypeScript 컴파일러를 그대로 내부에 포함해 사용하는데 이는 본래 <strong>정확한 타입 분석</strong>을 목표로 만들어졌기 때문에 성능을 우선하지 않는다. 이러한 구조는 메가모픽 객체 사용, 불필요한 동적 프로퍼티 접근 등으로 인해 오버헤드를 유발한다. 
반면 esbuild는 TypeScript 파싱 및 트랜스파일 과정을 자체적으로 구현하여 이러한 병목을 근본적으로 제거하였다.</p>
<br/>

<h3 id="esbuild의-철학-해주는-것과-하지-않는-것">esbuild의 철학: 해주는 것과 하지 않는 것</h3>
<p>esbuild의 개발자는 명확한 철학을 가지고 도구를 설계했다.</p>
<p>그는 esbuild를 프론트엔드 개발의 모든 요구를 해결하는 만능 도구로 만들 생각이 없다고 밝힌다.</p>
<p>따라서 다음과 같은 기능들은 <strong>의도적으로 제공하지 않는다.</strong></p>
<blockquote>
<ul>
<li>Elm, Svelte, Vue, Angular 등 타 프레임워크 전용 언어 직접 지원</li>
<li>TypeScript 타입 검사 기능 (tsc로 분리)</li>
<li>커스텀 AST 조작 API</li>
<li>Hot Module Reloading(HMR)</li>
<li>Module Federation</li>
</ul>
</blockquote>
<p>그 이유는 명확하다. 지나치게 많은 기능을 수용하면 도구의 복잡도가 증가하고 성능 최적화에 일관성을 유지하기 어렵다.</p>
<p>이러한 이유로 esbuild는 자신을 <strong>웹을 위한 링커(linker)</strong>로 정의한다. 
즉 JavaScript와 CSS를 빠르고 안정적으로 묶어주는 역할에 집중하고 그 외 영역은 다른 도구와 결합하여 사용하는 것을 권장한다.</p>
<p>이러한 철학 덕분에 <strong>esbuild의 코어는 가볍고 단순</strong>하며, <strong>vite</strong>와 같은 다른 툴에서 <strong>내부 빌드 엔진</strong>으로 통합하기에 적합하다. 
실제로 vite는 TypeScript 변환 과정에서 esbuild를 사용하고 있다.</p>
<h3 id="생태계-내-활용-사례">생태계 내 활용 사례</h3>
<p>esbuild는 아직 1.0.0 버전에 도달하지 않았지만 이미 다양한 프로덕션 환경에서 핵심 빌드 도구로 사용되고 있다.</p>
<p>대표적인 예시는 다음과 같다.</p>
<blockquote>
<ul>
<li><strong>Vite</strong>: TypeScript를 JavaScript로 변환하는 단계에 esbuild를 사용</li>
<li><strong>AWS CDK</strong>: 코드 번들링에 esbuild를 채택</li>
<li><strong>Phoenix(Elixir 프레임워크)</strong>: 프론트엔드 빌드 도구로 esbuild를 지원</li>
</ul>
</blockquote>
<p>esbuild는 단독으로 사용되지 않더라도 이미 여러 툴체인 내부에서 필수 구성 요소로 작동하고 있다.</p>
<br/>

<p>그렇다면 이제 esbuild를 핵심 빌드 도구로 사용하고 있는 도구들 중 하나인 vite에 대해서 자세하게 알아보겠다.</p>
<br/>

<hr>
<br/>

<h2 id="vite란-정확히-무엇인가">Vite란 정확히 무엇인가?</h2>
<p>기존에는 webpack처럼 <strong>모든 코드를 번들링한 뒤 브라우저에 제공</strong>하는 구조가 일반적이였지만 프로젝트가 커질수록 이 번들링 시간이 너무 오래 걸렸고 HMR(Hot Module Replacement, 코드 수정 후 새로고침 없이 반영)도 느려졌다. 이러한 성능을 해결하기 위해 등장한 것이 vite이다.</p>
<p>vite의 핵심 아이디어는 <strong>브라우저가 이미 지원하는 ESM(ES Modules)을 적극적으로 활용하고</strong>, <strong>개발 시점과 배포 시점을 다른 전략으로 처리</strong>하는 것이다. 
개발 중에는 번들링을 하지 않고도 빠른 응답을 제공하며 배포 시에는 여전히 번들링을 통해 최적화된 산출물을 생성하는 방식을 취한다.</p>
<blockquote>
<p>이처럼 Vite는 이 문제를 <strong>개발 단계</strong>와 <strong>배포 단계</strong>를 완전히 분리함으로써 해결했다.</p>
</blockquote>
<h3 id="vite를-사용해야-하는-이유">Vite를 사용해야 하는 이유</h3>
<h4 id="1-기존-번들러가-갖고-있던-구조적-한계">1. 기존 번들러가 갖고 있던 구조적 한계</h4>
<p>브라우저가 ESM을 네이티브로 지원하기 전까지는 자바스크립트 모듈을 브라우저에서 직접 불러와 실행하는 것이 사실상 불가능했다. 이 때문에 프론트엔드 개발자들은 여러 개의 소스 모듈을 하나의 파일 또는 적은 수의 파일로 <strong>묶어주는(bundling)</strong> 방식을 사용해야 했다. webpack, Rollup, Parcel과 같은 도구는 이 과정을 자동화하여 개발자의 생산성을 크게 높였다.</p>
<p>그러나 애플리케이션의 규모가 커지고 모듈 수가 수백, 수천 개로 증가하면서 문제가 드러났다. 기존의 번들러는 “개발 서버를 띄우기 전에” 프로젝트 전체를 한 번 훑고 의존성을 분석하고 빌드해야 했다. 모듈 수가 많아질수록 이 초기 단계는 점점 길어졌고, 변화가 잦은 개발 환경에서는 비합리적인 대기 시간으로 이어졌다. 또한 HMR을 지원하더라도, 내부적으로는 여전히 번들 단위로 재처리가 필요했기 때문에 변경 사항이 브라우저에 반영되기까지 수 초가 걸리는 경우도 드물지 않았다. 이런 느린 피드백 루프는 곧바로 개발자의 생산성 저하로 이어졌다.</p>
<p>Vite는 이 문제를 “처음부터 다 번들링하지 않아도 되는 구조”를 도입하는 방식으로 해결하고자 한다. 즉, <strong>번들링이 필요한 시점에만 번들링을 한다</strong>는 방향으로 아키텍처를 재구성했다.</p>
<br/>

<h4 id="2-모듈을-의존성과-소스-코드로-분리해-다룬다">2. 모듈을 의존성과 소스 코드로 분리해 다룬다</h4>
<p><img src="https://velog.velcdn.com/images/hour_2/post/9bb4393a-25e5-47d6-9fa1-a8c6ff23ac80/image.png" alt=""> <img src="https://velog.velcdn.com/images/hour_2/post/bca5ed1e-a7a3-4b2a-be21-85f4defd02c0/image.png" alt=""></p>
<p>Vite가 가진 중요한 설계 포인트는 애플리케이션을 다음 두 가지 범주로 나누어 처리한다는 점이다.</p>
<ol>
<li><p>의존성(dependencies)</p>
<blockquote>
<ul>
<li>예: Vue, React, UI 라이브러리, 유틸성 패키지 등</li>
<li>개발 중에는 거의 변경되지 않는 코드</li>
<li>대체로 순수한 JavaScript로 되어 있고, 변환 비용이 크지 않다</li>
</ul>
</blockquote>
<ul>
<li>그러나 모듈 수가 많을 경우(수백 개 이상) 초기 번들링 비용이 매우 크다</li>
</ul>
<p>Vite는 이 의존성들을 <strong>사전 번들링(pre-bundling)</strong> 한다. 이 작업에는 Go로 작성된 <strong>esbuild</strong>를 사용한다. 
 esbuild는 기존 자바스크립트 기반 번들러 대비 10~100배에 달하는 속도를 제공하므로, 대규모 의존성 번들링에서 병목이 발생하지 않는다.</p>
</li>
<li><p>소스 코드(source code)</p>
<blockquote>
<ul>
<li>예: JSX, TSX, Vue SFC, Svelte 컴포넌트, CSS 등</li>
<li>변환이 필요하고, 개발 중에 자주 변경되는 코드</li>
<li>전체를 한 번에 처리할 필요가 없는 코드</li>
</ul>
</blockquote>
<p> Vite는 이 소스 코드를 <strong>네이티브 ESM</strong> 으로 제공한다. 
 즉, 브라우저가 실제로 요청하는 모듈만 그때그때 변환해서 전달한다. 조건부 <code>import()</code>로 분기되는 코드나 현재 화면에서 사용하지 않는 라우트의 코드는 실제로 접근할 때까지 변환되지 않는다. 
 이로 인해 초기 구동 속도가 크게 향상된다.</p>
<p> 이 구조를 통해 Vite는 “처음부터 전체 애플리케이션을 번들링해야만 서버를 시작할 수 있는” 기존 번들러의 제약을 제거한다.</p>
</li>
</ol>
<br/>

<h4 id="3-서버-구동-속도-개선">3. <strong>서버 구동 속도 개선</strong></h4>
<p>기존 번들러 기반 개발 서버는 콜드 스타트 시 프로젝트 전체를 한 번 빌드한 뒤에야 페이지를 제공할 수 있었다. 이는 프로젝트 규모가 커질수록 선형적으로 느려지는 구조였다.</p>
<p>Vite는 다음과 같은 방식으로 이를 개선한다.</p>
<blockquote>
<ul>
<li><strong>의존성은 esbuild로 미리 빠르게 묶는다.</strong></li>
<li><strong>애플리케이션 소스는 브라우저가 요청하는 시점에만 변환하여 전달한다.</strong></li>
<li><strong>브라우저가 ESM을 지원하므로, 의존성 그래프를 브라우저가 직접 따라가게 한다.</strong></li>
</ul>
</blockquote>
<p>이 구조에서는 서버가 “모든 모듈이 준비될 때까지 기다린 뒤 응답”하는 것이 아니라, “응답 가능한 것부터 즉시 제공”하는 방향으로 바뀐다. 그 결과 개발 서버의 시작 시간이 매우 짧아진다.</p>
<br/>

<h4 id="4-코드-갱신-속도hmr의-일관성">4. <strong>코드 갱신 속도(HMR)의 일관성</strong></h4>
<p>전통적인 번들러 환경에서 파일을 수정하면, 번들러는 변경된 파일뿐 아니라 그 파일과 연결된 번들도 다시 생성해야 했다. 애플리케이션이 커질수록 이 시간은 길어졌고, HMR을 사용하더라도 결국 내부에서 번들 재생성이 일어났기 때문에 대규모 프로젝트에서는 빠른 피드백을 제공하지 못했다.</p>
<p>Vite는 개발 서버에서 <strong>번들을 만들지 않는다.</strong> 대신 각 모듈을 ESM 단위로 브라우저에 제공한다. 이 구조에서는 어떤 모듈이 변경되었을 때 그 모듈과 그 모듈을 직접 참조하는 모듈만 교체하면 된다. 브라우저는 요청한 모듈만 다시 받으면 되므로, 애플리케이션 크기가 커져도 HMR 속도가 느려지지 않는다.</p>
<p>또한 Vite는 HTTP 캐시를 적극적으로 활용한다.</p>
<blockquote>
<ul>
<li>소스 코드는 변경 여부를 판단해 <code>304 Not Modified</code>로 응답하고</li>
<li>변하지 않는 의존성은 <code>Cache-Control: max-age=31536000, immutable</code>로 강하게 캐시한다.</li>
</ul>
</blockquote>
<p>이로 인해 네트워크 요청 수가 줄어들고, 결과적으로 전체 페이지 로딩 성능이 개선된다.</p>
<br/>

<h3 id="배포-시에도-번들링이-필요한-이유">배포 시에도 번들링이 필요한 이유</h3>
<p>현대 브라우저가 ESM을 지원한다고 해서, 프로덕션 환경에서 “번들링을 전혀 하지 않은” ESM을 그대로 배포하는 것이 항상 최선인 것은 아니다. 이유는 다음과 같다.</p>
<ol>
<li><strong>중첩된 import가 많을수록 네트워크 요청 수도 함께 증가</strong>한다. HTTP/2 환경이라 하더라도 다량의 작은 요청은 초기 로딩에 부담을 줄 수 있다.</li>
<li><strong>트리 셰이킹(tree-shaking)</strong>, <strong>코드 스플리팅(code splitting)</strong>, <strong>지연 로딩(lazy loading)</strong>, <strong>캐시 효율을 위한 청크 분할</strong> 등 프로덕션 수준의 최적화는 여전히 번들링 과정에서 수행하는 것이 가장 효과적이다.</li>
<li><strong>개발 서버와 빌드 결과가 다르면 디버깅이 어렵다.</strong> 개발 중에 보이던 동작과 배포 후 동작이 달라지지 않도록, Vite는 미리 정해진 빌드 명령과 동일한 파이프라인을 제공한다.</li>
</ol>
<p>따라서 Vite는 <em>개발 시점</em>에는 번들링을 하지 않지만, <em>프로덕션 빌드 시점</em>에는 여전히 번들링을 수행한다. 이때 사용하는 번들러는 esbuild가 아니라 <strong>Rollup</strong>이다.</p>
<br/>

<h3 id="왜-프로덕션-번들링에는-esbuild를-사용하지-않는가">왜 프로덕션 번들링에는 esbuild를 사용하지 않는가</h3>
<p>Vite는 개발 시 “사전 번들링” 단계에서 esbuild를 사용한다. 이 단계에서는 속도가 최우선이므로 esbuild의 선택이 매우 타당하다. 그러나 프로덕션 빌드 전체를 esbuild로 처리하지는 않는다. 이유는 다음과 같다.</p>
<ol>
<li><p><strong>플러그인 생태계와의 호환성</strong></p>
<p> Vite는 Rollup의 플러그인 인프라를 적극적으로 채택하고 있다. 이 인프라는 이미 생태계가 크고, 다양한 프레임워크·언어·리소스에 대응하는 플러그인이 존재한다. esbuild 기반으로 완전히 전환할 경우 이런 유연성이 줄어든다.</p>
</li>
<li><p><strong>유연성과 성능의 트레이드오프</strong></p>
<p> esbuild는 매우 빠르지만, 현재 시점에서 빌드 파이프라인 전체를 대체할 만큼 유연하지는 않다. Vite는 “현실적인 프로덕션 환경에서 필요한 변환과 최적화를 안정적으로 수행할 수 있는가”를 더 중요하게 보며, 이 지점에서 Rollup이 더 균형 잡힌 선택이다.</p>
</li>
<li><p><strong>향후 대체 가능성</strong></p>
<p> Rollup은 v4에서 파서를 SWC로 전환하는 등 성능 개선을 계속하고 있으며, Rust로 포팅된 Rolldown 프로젝트도 진행 중이다. 이 흐름이 성숙하면, Vite는 개발과 빌드 사이의 불일치를 더 줄인 형태로 진화할 수 있다.</p>
</li>
</ol>
<h3 id="참고-자료">참고 자료</h3>
<p>esbuild 공식 문서: <a href="https://esbuild.github.io/faq/#top-level-var">https://esbuild.github.io/faq/#top-level-var</a>
vite 공식 문서: <a href="https://ko.vite.dev/guide/why.html">https://ko.vite.dev/guide/why.html</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[클로저와 실행 컨텍스트]]></title>
            <link>https://velog.io/@hour_2/%ED%81%B4%EB%A1%9C%EC%A0%80-%EC%8B%A4%ED%96%89-%EC%BB%A8%ED%85%8D%EC%8A%A4%ED%8A%B8</link>
            <guid>https://velog.io/@hour_2/%ED%81%B4%EB%A1%9C%EC%A0%80-%EC%8B%A4%ED%96%89-%EC%BB%A8%ED%85%8D%EC%8A%A4%ED%8A%B8</guid>
            <pubDate>Sat, 01 Nov 2025 17:31:13 GMT</pubDate>
            <description><![CDATA[<h2 id="클로저">클로저</h2>
<h3 id="클로저란-무엇인가">클로저란 무엇인가?</h3>
<blockquote>
<p><strong>클로저(Closure)</strong>는 “함수와 그 함수가 선언될 당시의 어휘적 환경(Lexical Environment)의 조합”이다.</p>
</blockquote>
<p>조금 어렵게 느껴질 수 있지만, 핵심은 <strong>“함수가 선언될 때의 스코프를 기억한다”</strong> 는 것이다.
즉, 내부 함수가 외부 함수의 변수에 접근할 수 있는 능력을 말한다.</p>
<pre><code class="language-js">function makeFunc() {
  const name = &quot;Mozilla&quot;;
  function displayName() {
    console.log(name);
  }
  return displayName;
}

const myFunc = makeFunc();
myFunc(); // Mozilla</code></pre>
<p>makeFunc()이 실행된 후라면, 보통 name은 사라졌을 것처럼 보인다.
하지만 displayName 함수는 makeFunc이 실행될 당시의 환경을 기억하고 있다.
이게 바로 클로저다.</p>
<h3 id="어휘적-스코프lexical-scope">어휘적 스코프(Lexical Scope)</h3>
<blockquote>
<p>자바스크립트는 정적 스코프(static scope), 즉 “코드가 작성된 위치”에 따라 변수의 유효 범위를 결정한다.
이는 런타임이 아니라 컴파일(파싱) 시점에 결정된다.</p>
</blockquote>
<pre><code class="language-js">function init() {
  const name = &quot;Mozilla&quot;;
  function displayName() {
    console.log(name);
  }
  displayName();
}
init(); // Mozilla</code></pre>
<p>내부 함수 displayName은 자신이 어디에서 정의되었는지를 기준으로 외부 스코프(init)에 접근한다.
이러한 어휘적 스코프 덕분에 클로저가 가능해진다.</p>
<h3 id="클로저의-실제-동작">클로저의 실제 동작</h3>
<p>클로저가 만들어질 때, 자바스크립트 엔진은 함수가 참조하는 외부 변수들을 별도의 환경 객체(Environment Record) 안에 저장한다.
이 객체는 함수가 반환된 뒤에도 GC(가비지 컬렉션)으로부터 제거되지 않고 남는다.
즉, 함수가 실행될 때마다 새로운 “환경 스냅샷”이 생성되는 것이다.</p>
<pre><code class="language-js">function makeAdder(x) {
  return function (y) {
    return x + y;
  };
}

const add5 = makeAdder(5);
const add10 = makeAdder(10);

console.log(add5(2)); // 7
console.log(add10(2)); // 12</code></pre>
<p>add5와 add10은 서로 다른 환경을 기억한다.
그래서 각자의 x 값을 따로 유지할 수 있다.</p>
<h3 id="클로저의-실용적-사용-예시">클로저의 실용적 사용 예시</h3>
<h4 id="1-데이터-은닉-private-변수">1. 데이터 은닉 (Private 변수)</h4>
<blockquote>
<p>클로저를 사용하면, 외부에서 접근할 수 없는 “비공개 상태”를 만들 수 있다.</p>
</blockquote>
<pre><code class="language-js">function createCounter() {
  let count = 0;

  return {
    increment() {
      count++;
    },
    decrement() {
      count--;
    },
    getCount() {
      return count;
    },
  };
}

const counter = createCounter();
counter.increment();
console.log(counter.getCount()); // 1</code></pre>
<p>외부에서는 count에 직접 접근할 수 없다.
오직 increment, decrement, getCount를 통해서만 접근 가능하다.
이것은 클래스의 “private 속성”과 같은 효과를 낸다.</p>
<h4 id="2-이벤트-핸들러나-콜백에서-상태-유지">2. 이벤트 핸들러나 콜백에서 상태 유지</h4>
<pre><code class="language-js">function makeSizer(size) {
  return function () {
    document.body.style.fontSize = `${size}px`;
  };
}

const size12 = makeSizer(12);
const size16 = makeSizer(16);

document.getElementById(&quot;small&quot;).onclick = size12;
document.getElementById(&quot;large&quot;).onclick = size16;</code></pre>
<p>각 버튼은 클릭될 때마다 서로 다른 size 값을 기억하고 있다.
이처럼 클로저는 이벤트 기반 코드에서 상태를 저장하는 강력한 도구로 쓰인다.</p>
<h3 id="클로저-사용-시-주의할-점">클로저 사용 시 주의할 점</h3>
<h4 id="1-루프-안-var-사용-문제">1. 루프 안 var 사용 문제</h4>
<blockquote>
<p>아래 코드는 모든 입력창이 마지막 도움말만 보여준다.</p>
</blockquote>
<pre><code class="language-js">function setupHelp() {
  var helpText = [
    { id: &quot;email&quot;, help: &quot;Your email address&quot; },
    { id: &quot;name&quot;, help: &quot;Your full name&quot; },
    { id: &quot;age&quot;, help: &quot;Your age&quot; },
  ];

  for (var i = 0; i &lt; helpText.length; i++) {
    var item = helpText[i];
    document.getElementById(item.id).onfocus = function () {
      showHelp(item.help); // 마지막 값만 유지
    };
  }
}</code></pre>
<p>해결 방법은 let을 사용해 각 반복마다 별도의 블록 스코프를 생성하는 것이다.</p>
<pre><code class="language-js">for (let i = 0; i &lt; helpText.length; i++) {
  const item = helpText[i];
  document.getElementById(item.id).onfocus = () =&gt; showHelp(item.help);
}</code></pre>
<h4 id="2-불필요한-클로저-남용">2. 불필요한 클로저 남용</h4>
<pre><code class="language-js">function MyObject(name) {
  this.name = name;
  this.getName = function () {
    return this.name; // 매번 새 함수 생성
  };
}

// 개선
MyObject.prototype.getName = function () {
  return this.name;
};</code></pre>
<blockquote>
<p>클로저는 함수가 기억하는 ‘환경’이다.
기억해야 할 건 변수 값이 아니라 변수를 둘러싼 스코프 자체다.</p>
</blockquote>
<line />

<h2 id="실행-컨텍스트">실행 컨텍스트</h2>
<h3 id="실행-컨텍스트란-">실행 컨텍스트란 ?</h3>
<blockquote>
<p><strong>실행 컨텍스트(Execution Context)</strong>는 자바스크립트 코드가 실행되는 <strong>환경(문맥)</strong> 이다.
즉, “현재 코드가 어디서 실행 중이며, 어떤 변수·함수에 접근할 수 있는지”를 정의하는 객체라고 할 수 있다.</p>
</blockquote>
<p>한 문장으로 다시 정리하자면 <strong>“실행 컨텍스트는 자바스크립트 엔진이 코드를 실행하기 위해 필요한 모든 정보를 담은 객체”</strong>다.</p>
<h3 id="실행-컨텍스트의-종류">실행 컨텍스트의 종류</h3>
<p>자바스크립트 엔진은 코드를 읽을 때 여러 종류의 컨텍스트를 만든다.
크게 세 가지다.</p>
<table>
<thead>
<tr>
<th>종류</th>
<th>생성 시점</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td><strong>전역 컨텍스트(Global Context)</strong></td>
<td>프로그램 시작 시 한 번 생성</td>
<td>전역 코드(파일 전체) 실행 환경</td>
</tr>
<tr>
<td><strong>함수 컨텍스트(Function Context)</strong></td>
<td>함수가 호출될 때마다 생성</td>
<td>함수 내부 실행 환경</td>
</tr>
<tr>
<td><strong>Eval 컨텍스트(Eval Context)</strong></td>
<td><code>eval()</code>이 호출될 때</td>
<td>(권장 X) 문자열 코드를 실행하는 특수 컨텍스트</td>
</tr>
</tbody></table>
<p>실행 중에는 이들이 스택 구조(Call Stack) 형태로 쌓였다가, 실행이 끝나면 제거된다.</p>
<h3 id="실행-컨텍스트의-구성요소">실행 컨텍스트의 구성요소</h3>
<p>하나의 실행 컨텍스트는 내부적으로 다음 세 가지로 구성된다.</p>
<h4 id="1-variable-environment-변수-환경">1. Variable Environment (변수 환경)</h4>
<ul>
<li><p>var로 선언된 변수들이 등록되는 곳</p>
</li>
<li><p>선언과 초기화가 동시에 일어남 (그래서 undefined로 호이스팅됨)</p>
</li>
</ul>
<h4 id="2-lexical-environment-어휘적-환경">2. Lexical Environment (어휘적 환경)</h4>
<ul>
<li><p>let, const 변수, 함수 선언, 외부 스코프 정보 등을 저장</p>
</li>
<li><p>실제 스코프 체인이 여기서 형성됨</p>
</li>
<li><p>내부적으로는 두 개의 구성요소를 가진다:</p>
<ul>
<li><p>Environment Record: 현재 스코프의 변수/함수 선언 정보를 기록</p>
</li>
<li><p>Outer Lexical Environment Reference: 외부(부모) 스코프를 참조</p>
</li>
</ul>
</li>
</ul>
<h4 id="3-this-binding">3. This Binding</h4>
<ul>
<li><p>실행 문맥에 따라 this가 어떤 객체를 가리키는지 결정됨</p>
<ul>
<li><p>전역 컨텍스트: this → window (브라우저 기준)</p>
</li>
<li><p>함수 호출 방식에 따라 동적 바인딩 (call, apply, bind, 메서드 호출 등)</p>
</li>
</ul>
</li>
</ul>
<h3 id="실행-컨텍스트-생성-과정">실행 컨텍스트 생성 과정</h3>
<p>함수가 호출될 때, 실행 컨텍스트는 다음 순서로 만들어진다.</p>
<pre><code class="language-js">function sayHello(name) {
  var greeting = &quot;Hello&quot;;
  console.log(greeting + &quot; &quot; + name);
}
sayHello(&quot;Hyerin&quot;);</code></pre>
<h4 id="1-전역-컨텍스트-생성">(1) 전역 컨텍스트 생성</h4>
<ul>
<li><p>전역 코드가 실행되기 전에 생성</p>
</li>
<li><p>sayHello 함수가 메모리에 등록됨</p>
</li>
</ul>
<pre><code class="language-js">GlobalEC = {
  LexicalEnvironment: {
    EnvironmentRecord: { sayHello: &lt;function&gt; },
    Outer: null,
  },
  VariableEnvironment: {...},
  ThisBinding: window
}</code></pre>
<h4 id="2-함수-호출-→-새로운-함수-컨텍스트-생성">(2) 함수 호출 → 새로운 함수 컨텍스트 생성</h4>
<ul>
<li>sayHello(&quot;Hyerin&quot;) 실행 시, 함수 컨텍스트가 스택에 push됨</li>
</ul>
<pre><code class="language-js">FunctionEC = {
  LexicalEnvironment: {
    EnvironmentRecord: { name: &quot;Hyerin&quot;, greeting: undefined },
    Outer: GlobalLexicalEnvironment
  },
  VariableEnvironment: {...},
  ThisBinding: undefined (strict mode 기준)
}</code></pre>
<h4 id="3-실행-단계">(3) 실행 단계</h4>
<ol>
<li>변수 선언 단계 (Hoisting)</li>
</ol>
<ul>
<li><p>var greeting이 선언되고 undefined로 초기화됨</p>
</li>
<li><p>name은 매개변수로 이미 Environment Record에 존재</p>
</li>
</ul>
<ol start="2">
<li>코드 실행 단계</li>
</ol>
<ul>
<li><p>greeting = &quot;Hello&quot; 할당</p>
</li>
<li><p>console.log 실행</p>
</li>
</ul>
<ol start="3">
<li>컨텍스트 종료 (Pop)</li>
</ol>
<ul>
<li>함수 실행이 끝나면 컨텍스트 스택에서 제거됨</li>
</ul>
<h3 id="호이스팅hoisting과의-관계">호이스팅(Hoisting)과의 관계</h3>
<blockquote>
<p>실행 컨텍스트가 생성될 때, 자바스크립트 엔진은 스코프 내 선언들을 미리 스캔하고, 변수/함수 선언을 메모리에 기록해둔다.</p>
</blockquote>
<p>이게 바로 <strong>호이스팅(끌어올리기)</strong>이다.</p>
<pre><code class="language-js">console.log(x); // undefined
var x = 10;</code></pre>
<p>위 코드의 실제 내부 동작은 다음과 같다.</p>
<pre><code class="language-js">var x; // 선언(등록)
console.log(x);
x = 10; // 초기화(할당)</code></pre>
<p>반면 let과 const는 <strong>TDZ(Temporal Dead Zone)</strong>를 가지므로 선언 전 접근 시 ReferenceError를 발생시킨다.</p>
<h3 id="실행-컨텍스트와-스코프-체인">실행 컨텍스트와 스코프 체인</h3>
<blockquote>
<p><strong>스코프 체인(Scope Chain)</strong>은 현재 컨텍스트의 LexicalEnvironment에서 OuterEnvironmentReference를 따라가며 변수 탐색을 하는 구조다.</p>
</blockquote>
<pre><code class="language-js">const x = 1;

function outer() {
  const y = 2;
  function inner() {
    const z = 3;
    console.log(x + y + z);
  }
  inner();
}
outer(); // 6</code></pre>
<p>내부적으로는 아래처럼 스코프 체인이 연결된다.</p>
<pre><code class="language-nginx">GlobalLE  →  outerLE  →  innerLE
   ↑           ↑           ↑
  x=1         y=2         z=3</code></pre>
<p>inner()에서 x를 찾을 때 inner 스코프에 없으면 → outer → global 순으로 탐색한다.
이 탐색 경로가 클로저가 외부 변수를 기억할 수 있는 근거다.</p>
<h3 id="실행-컨텍스트와-클로저의-관계">실행 컨텍스트와 클로저의 관계</h3>
<p>클로저는 함수가 생성될 당시의 Lexical Environment를 기억하는 현상이다.
즉, 실행 컨텍스트가 사라져도 그 안의 Environment Record가 참조되고 있다면 <strong>GC(가비지 컬렉션)</strong>으로부터 해제되지 않는다.</p>
<pre><code class="language-js">function outer() {
  const x = 10;
  return function inner() {
    console.log(x);
  };
}

const fn = outer();
fn(); // 10</code></pre>
<p>이때 outer()의 실행 컨텍스트는 끝났지만 inner 함수가 여전히 x를 참조하고 있기 때문에 outer의 Lexical Environment는 메모리에 남아있다.</p>
<blockquote>
<p>정리하자면 실행 컨텍스트는 함수가 실행될 때 생성되는 실행 환경이며 클로저는 함수가 생성될 때 그 환경을 기억하는 메커니즘이다.</p>
</blockquote>
<h3 id="실행-컨텍스트-스택call-stack-시각화">실행 컨텍스트 스택(Call Stack) 시각화</h3>
<pre><code class="language-js">function first() {
  second();
  console.log(&quot;End of first&quot;);
}

function second() {
  console.log(&quot;In second&quot;);
}

first();
console.log(&quot;End of global&quot;);</code></pre>
<table>
<thead>
<tr>
<th>스택 상태</th>
<th>실행 중인 코드</th>
</tr>
</thead>
<tbody><tr>
<td>[Global]</td>
<td>프로그램 시작</td>
</tr>
<tr>
<td>[Global → first]</td>
<td><code>first()</code> 호출</td>
</tr>
<tr>
<td>[Global → first → second]</td>
<td><code>second()</code> 호출</td>
</tr>
<tr>
<td>[Global → first]</td>
<td><code>second()</code> 종료</td>
</tr>
<tr>
<td>[Global]</td>
<td><code>first()</code> 종료</td>
</tr>
<tr>
<td>[]</td>
<td>프로그램 종료</td>
</tr>
</tbody></table>
<p>이런 스택 기반 실행 모델 덕분에 자바스크립트는 동기적 실행 흐름을 보장한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[성능 최적화를 하려면 브라우저 동작 원리를 알아야한다.]]></title>
            <link>https://velog.io/@hour_2/%EC%84%B1%EB%8A%A5-%EC%B5%9C%EC%A0%81%ED%99%94%EB%A5%BC-%ED%95%98%EB%A0%A4%EB%A9%B4-%EB%B8%8C%EB%9D%BC%EC%9A%B0%EC%A0%80-%EB%8F%99%EC%9E%91-%EC%9B%90%EB%A6%AC%EB%A5%BC-%EC%95%8C%EC%95%84%EC%95%BC%ED%95%9C%EB%8B%A4</link>
            <guid>https://velog.io/@hour_2/%EC%84%B1%EB%8A%A5-%EC%B5%9C%EC%A0%81%ED%99%94%EB%A5%BC-%ED%95%98%EB%A0%A4%EB%A9%B4-%EB%B8%8C%EB%9D%BC%EC%9A%B0%EC%A0%80-%EB%8F%99%EC%9E%91-%EC%9B%90%EB%A6%AC%EB%A5%BC-%EC%95%8C%EC%95%84%EC%95%BC%ED%95%9C%EB%8B%A4</guid>
            <pubDate>Wed, 15 Oct 2025 13:47:11 GMT</pubDate>
            <description><![CDATA[<p>우리 웹개발자들은 브라우저 위에서 개발을 하고 있어요. 
그렇다면 브라우저가 어떻게 동작하는지 충분히 이해해야 브라우저 위에서 하는 개발을 더 깊고 의미있게 할 수 있다고 생각해요.</p>
<p>이런 이유로 이번 글에서는 성능 최적화를 위한 브라우저 동작 원리라는 주제를 다뤄보려 합니다 ㅎㅎㅎ</p>
<h3 id="브라우저-동작-원리에-대해서-알아보자">브라우저 동작 원리에 대해서 알아보자</h3>
<p><strong>웹서버로부터 HTML 받아오기</strong>
<img src="https://velog.velcdn.com/images/hour_2/post/0103b491-2512-40c6-9cb7-43cd09a141c9/image.png" alt=""></p>
<p>먼저 우리가 주소창에 URL (<code>www.naver.com</code>)을 입력하면 <strong>DNS</strong>를 조회해서 
<code>URL</code> 과 <code>IP주소</code> 를 가지고 요청을 보냅니다.</p>
<blockquote>
<p><strong>DNS</strong>란 _Domain Name System_의 약자로 사용자가 이해하기 쉬운 도메인 이름을 컴퓨터가 이해할 수 있는 <code>IP 주소</code> 로 변환해주는 역할을 해요.</p>
</blockquote>
<p>요청을 받은 웹서버는 응답을 통해 HTML 파일을 브라우저로 보내줘요. 
<img src="https://velog.velcdn.com/images/hour_2/post/4bba73c5-5e23-4de9-935c-a72bb78f02c0/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/hour_2/post/14cef262-3066-49cc-9d6d-4b7f6a13f10f/image.png" alt="">
(URL 입력 후 브라우저에서 웹 서버까지 과정에 HTTP, DNS 등 여러 과정들이 있으니까 맨 위에 사진을 참고해주세요 !)</p>
<br/>
<br/>

<p><strong>HTML 파싱</strong>
<img src="https://velog.velcdn.com/images/hour_2/post/9a125d11-ec1b-4f38-9a84-fc5dc0849ee3/image.png" alt=""></p>
<blockquote>
<p>간단하게 말하자면 HTML을 파싱하는 과정은 <code>Render Tree</code>를 만들기 위해 <code>DOM</code>과 <code>CSSOM</code>을 만든 뒤 Layout 단계와 Paint 단계를 거쳐 웹사이트를 만드는 과정이에요.</p>
</blockquote>
<p>위에 말을 이해하기 쉽도록 아래에서 자세하게 이야기해볼게요</p>
<p>먼저 브라우저가 웹서버로부터 응답을 받은 뒤 HTML 코드를 받으면 이제 브라우저는 HTML을 <strong>파싱</strong> 하기 시작합니다.</p>
<p>파싱을 하면 아래 과정을 거쳐요</p>
<ul>
<li>DOM 생성</li>
<li>CSSOM 생성</li>
<li>javaScript 파일 다운로드</li>
</ul>
<p>파싱을 시작하면 먼저 본격적으로 <strong>DOM 생성</strong>에 들어가요.
파싱을 하는 과정에서 css <code>&lt;link&gt;</code> 태그를 만나면 외부 리소스를 다운로드해요. </p>
<pre><code>&lt;html lang=&quot;ko&quot;&gt;
&lt;head&gt;
  &lt;meta charset=&quot;UTF-8&quot;&gt;
  &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&gt;
  &lt;title&gt;자기소개 페이지&lt;/title&gt;
  &lt;link rel=&quot;stylesheet&quot; href=&quot;./styles/reset.css&quot;/&gt;
  &lt;link rel=&quot;stylesheet&quot; href=&quot;./styles/theme.css&quot;/&gt;
  &lt;link rel=&quot;stylesheet&quot; href=&quot;./styles/styles.css&quot;/&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;!-- 내용.. --&gt;
&lt;/body&gt;</code></pre><p>위의 html 코드를 예시로 보면 css <code>&lt;link&gt;</code> 태그가 있죠 ?</p>
<p>이렇게 브라우저가 HTML을 파싱하다가 CSS <code>&lt;link&gt;</code> 태그를 만나면, <code>DOM</code> 만들던 과정을 잠깐 멈추고 DNS에서 받은 <code>IP주소</code>를 다시 찾아가서 css 파일을 <strong>요청</strong>해요. </p>
<blockquote>
<p>여기서 HTML은 <code>IP주소</code>를 찾았으면 HTML 파싱은 그대로 이어서 진행돼요! 
즉 CSS 파일 로딩 자체는 <code>DOM</code>의 생성을 막지 않아요 ! </p>
</blockquote>
<p>하지만 여기서 헷갈리면 안되는 부분은 CSS 파일이 로딩되고 <code>CSSOM</code>을 만드는 동안, <code>DOM</code>은 Render Tree를 만들기 위해 <code>CSSOM</code>이 완성되기를 기다린다는거에요. 즉 CSS 파일 로딩은 <code>DOM</code> 파싱을 중단시키지는 않지만 <code>Render Tree</code> 생성을 위해 <code>CSSOM</code>이 필요하기 때문에 브라우저는 CSS 파싱이 끝날 때까지 레이아웃 및 페인트를 지연시켜요. 따라서 CSS 로딩 속도는 초기 렌더링 성능에 큰 영향을 줍니다! <code>DOM</code>과 <code>CSSOM</code>이 완성이 돼야 다음 스텝으로 진행할 수 있기 때문에 HTML입장에서는 <code>CSSOM</code>을 최대한 빨리 만드는 것이 중요한거죠.</p>
<p>이 부분을 알면 CSS 라이브러리의 선택이 성능에도 중요하다는 걸 알 수 있어요.
CSS 라이브러리 선택의 근거를 가져야 하는 이유죠 ㅎㅎ 이 부분과 관련된 아티클은 다음에 작성해보도록 할게요</p>
<p>이제 태그 중에서  <code>script</code> 태그를 만나는 순간 HTML은 파싱을 잠깐 멈추고 DNS에서 받은 <code>IP주소</code>를 다시 찾아가서 javaScript파일을 요청해요. </p>
<blockquote>
<p>여기서 CSS와 다르게 JS 실행중에는 DOM 파싱 자체를 중단시켜요!</p>
</blockquote>
<p>JS는 많은 변화가 있을 수 있기 때문에 DOM을 다시 그려야할 가능성이 있기 때문입니다.</p>
<p>script 태그의 위치를 HTML태그 상단에 두게되면 javascript를 실행할 때까지 HTML 태그 그리기를 멈춰놓기 때문에 비효율적이에요. 그래서 script태그를 body 바닥에 위치 시켜야한다고 해요. body 바닥에 두거나 javaScript가 <code>DOM</code> 생성을 막지 않도록 하는 defer, async 옵션을 사용하는 방법도 있어요. </p>
<ul>
<li>defer: HTML 파싱과 병렬로 다운로드되고 파싱이 끝난 뒤 실행</li>
<li>async: HTML 파싱과 병렬로 다운로드되며 다운로드가 끝나는 즉시 실행된다 (HTML 파싱 중단 가능성 있음)</li>
</ul>
<p>우리가 사용하는 <code>React</code>나 <code>Vue</code>는 defer 옵션을 사용하는 방법을 이용하고 있다고 합니다.</p>
<blockquote>
<p>body 바닥에 두는 방법</p>
</blockquote>
<pre><code>&lt;html lang=&quot;ko&quot;&gt;
&lt;head&gt;
  &lt;!-- 내용.. --&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;!-- 내용.. --&gt;
  &lt;script src=&quot;example.js&quot;&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre><blockquote>
<p>defer나 async를 사용하는 방법 (React, Vue가 사용하는 방법)</p>
</blockquote>
<pre><code>&lt;html lang=&quot;ko&quot;&gt;
&lt;head&gt;
  &lt;!-- 내용.. --&gt;
  &lt;script src=&quot;/examples/scripts/script_src.js&quot; defer&gt;&lt;/script&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;!-- 내용.. --&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre><p>이렇게 HTML을 통해서 <code>DOM</code>도 만들었고, javaScript도 불러왔고, <code>CSSOM</code>도 만들었으면 이제 <code>DOM</code>과 <code>CSSOM</code>을 합쳐 <code>Rende Tree</code>를 만들 수 있어요.</p>
<p><code>Render Tree</code>는 웹사이트를 그리기 위한 최종 설계도에요.
<code>Render Tree</code>를 만든 후에는 Layout 단계를 통해 정확한 px를 계산하고 이후 Paint 단계에서 실제 계산한 px를 통해 웹사이트에 그려요.</p>
<h3 id="성능-최적화를-위해서-알아야-할-reflow">성능 최적화를 위해서 알아야 할 Reflow</h3>
<blockquote>
<p><strong>Reflow</strong>란 Layout 단계에서 계산이 다시 수행되는 과정입니다.</p>
</blockquote>
<p><code>DOM</code>이나 <code>CSSOM</code>의 변경으로 인해 발생되며 이는 성능에 큰 영향을 줄 수 있어요. Layout이 바뀌면 다시 처음부터 Painting을 해야하기 때문에 최소한으로 바뀌어야 성능개선에 도움이 됩니다.</p>
<p>특히 트리의 상위 요소에서 변화가 발생하면 하위 요소들도 모두 재계산되니 성능에 더 큰 영향을 줄 수 있겠죠 ?</p>
<p><strong>Reflow되는 상황</strong></p>
<ul>
<li>페이지 초기 렌더링 시 (최초 Layout 과정)</li>
<li>윈도우 리사이징 시 (Viewport 크기 변경시)</li>
<li>노드 추가 또는 제거</li>
<li>요소의 위치, 크기 변경(left, top, margin, padding, border, width, height 등)</li>
<li>폰트 변경(텍스트 내용)과 이미지 크기 변경(크기가 다른 이미지로 변경 시)</li>
</ul>
<blockquote>
<p><strong>Repaint?</strong>
Repaint는 레이아웃에 영향을 주지 않는 시각적 속성(색상, 그림자 등)만 다시 그리는 것으로 Reflow가 레이아웃 계산을 다시 해서 성능 비용이 큰 것에 비해 Repaint는 비교적 비용이 상대적으로 적습니다!</p>
</blockquote>
<h3 id="uselayouteffect">useLayoutEffect</h3>
<p>브라우저 렌더링 과정의 Layout 단계에 대해서 이제 알았다면 <code>useLayoutEffect</code>를 완전히 이해할 수 있어요.
<code>useLayoutEffect</code>의 Layout이 바로 이 브라우저의 Layout 단계를 가리키거든요 ㅎㅎ</p>
<br/>

<p>공식문서에서는 useLayout을 이렇게 설명하고 있어요 ⬇️ </p>
<blockquote>
<p><strong>useLayoutEffect</strong>는 브라우저가 화면을 다시 그리기 전에 실행되는 <strong>useEffect</strong>이다. </p>
</blockquote>
<p>useEffect는 DOM이 업데이트되고 Layout과 Paint까지 모두 완료된 후 실행됩니다. useLayoutEffect는 DOM이 업데이트되고 Layout이 완료된 직후인 Paint가 시작되기 전에 실행되죠.</p>
<p>useLayoutEffect를 사용하면 Paint가 일어나기 직전이기 때문에 여기서 DOM을 조작하면 사용자가 보기도 전에 화면에 반영할 수 있어 깜빡임 없이 동작시킬 수 있는거죠! </p>
<p>_하지만 공식문서에서는 useLayoutEffect를 권장하진 않아요. _</p>
<p>왜냐하면 useLayoutEffect는 브라우저가 화면을 그리기 직전에 동기적으로 실행되기 때문에 이 안에서 무거운 작업을 하면 브라우저의 페인트가 지연되어 성능이 저하될 수 있기 때문이에요.</p>
<p>특히 React 18 이후에는 동시성 렌더링(Concurrent Rendering)과 전환(Transition)이 도입되어 useLayoutEffect의 남용이 렌더링 중단 및 프레임 드랍으로 이어질 수 있기 때문에 정말 필요한 경우에만 최소한으로 사용하는 것이 중요해요.
따라서 useLayoutEffect는 스크롤 위치를 조정하거나 요소의 크기를 측정해 다른 요소의 스타일을 조정하는 등 페인트 전에 DOM 상태를 읽고 수정해 화면 깜빡임을 방지해야 하는 상황에서만 사용하는 것이 좋다고 명시하고 있어요.</p>
<h4 id="실제-프로젝트에서-uselayouteffect를-사용해본-경험">실제 프로젝트에서 useLayoutEffect를 사용해본 경험</h4>
<p>저도 프로젝트에서 useLayoutEffect를 사용했던 경험이 있는데요.</p>
<p>아코디언(Accordion) 컴포넌트를 구현할 때였습니다. 
아코디언은 열리고 닫힐 때 콘텐츠 영역의 높이(height) 값을 계산해 max-height 스타일에 적용함으로써 부드럽게 열고 닫히도록 애니메이션을 구현하는 방식으로 동작했어요.</p>
<pre><code class="language-tsx">import { useLayoutEffect, useRef, useState } from &#39;react&#39;;

export const useMeasureHeight = &lt;T extends HTMLElement&gt;() =&gt; {
  const ref = useRef&lt;T&gt;(null);
  const [height, setHeight] = useState(0);

  useLayoutEffect(() =&gt; {
    const element = ref.current;
    if (!element) {
      return;
    }

    const measureHeight = () =&gt; setHeight(element.scrollHeight);

    measureHeight();
    const resizeObserver = new ResizeObserver(measureHeight);
    resizeObserver.observe(element);

    return () =&gt; resizeObserver.disconnect();
  }, []);

  return { ref, height };
};
</code></pre>
<p>위에 있는 코드는 아코디언 콘텐츠 영역의 실제 높이를 측정하기 위해 제가 작성했던 커스텀 훅이에요.</p>
<p>아코디언을 열기 전에는 data가 서버에서 넘어오지 않았어요. 따라서 열 때는 먼저 콘텐츠 영역의 실제 높이를 scrollHeight로 읽은 뒤 이 값을 max-height에 적용해야 자연스럽게 펼쳐지는 애니메이션을 만들 수 있습니다.</p>
<p>그런데 이 높이 측정을 useEffect 안에서 처리하면 Paint 이후에 실행되기 때문에, 브라우저가 먼저 접힌 상태를 그린 뒤 높이를 다시 계산하게 됩니다. 이 과정에서 짧은 순간 접힌 상태가 보이거나 화면이 깜빡이는 문제가 발생했어요.</p>
<p>이때 useLayoutEffect를 사용하면 브라우저가 Paint를 하기 직전에 콘텐츠의 높이를 동기적으로 계산하고 스타일을 적용할 수 있어서, 사용자가 깜빡임 없이 부드럽게 아코디언이 열리는 경험을 만들 수 있었습니다.</p>
<p>이런 식으로 레이아웃 측정이나 스타일 적용이 Paint 전에 반드시 이루어져야 할 상황에서 useLayoutEffect를 쓰면 좋을 것 같습니다</p>
<br/>
<br/>

<p>참고 레퍼런스
<a href="https://ko.react.dev/reference/react/useLayoutEffect">https://ko.react.dev/reference/react/useLayoutEffect</a>
<a href="https://www.youtube.com/watch?v=Mqh13dNI8jc">https://www.youtube.com/watch?v=Mqh13dNI8jc</a>
<a href="https://developer.mozilla.org/en-US/docs/Web/Performance/Guides/Critical_rendering_path">https://developer.mozilla.org/en-US/docs/Web/Performance/Guides/Critical_rendering_path</a></p>
]]></description>
        </item>
    </channel>
</rss>