<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>park_log.log</title>
        <link>https://velog.io/</link>
        <description>park</description>
        <lastBuildDate>Fri, 17 Jul 2026 07:27:21 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>park_log.log</title>
            <url>https://velog.velcdn.com/images/park_log/profile/d1ae9d99-17bc-4282-97de-89bedc9b854a/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. park_log.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/park_log" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[이미지 태그 잘 사용하기]]></title>
            <link>https://velog.io/@park_log/%EC%9D%B4%EB%AF%B8%EC%A7%80-%ED%83%9C%EA%B7%B8-%EC%9E%98-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@park_log/%EC%9D%B4%EB%AF%B8%EC%A7%80-%ED%83%9C%EA%B7%B8-%EC%9E%98-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Fri, 17 Jul 2026 07:27:21 GMT</pubDate>
            <description><![CDATA[<h1 id="브라우저는-어떤-이미지를-다운로드할까">브라우저는 어떤 이미지를 다운로드할까?</h1>
<h3 id="img-srcset-picture-lazy-loading-이해하기">img, srcset, picture, lazy loading 이해하기</h3>
<p>웹 페이지에서 이미지를 표시할 때 가장 먼저 떠오르는 태그는 <code>img</code> 태그다.</p>
<pre><code class="language-html">&lt;img src=&quot;/product.jpg&quot; alt=&quot;상품 이미지&quot; /&gt;</code></pre>
<p>이미지를 화면에 보여주는 것만 생각한다면 이것으로 충분해 보인다.</p>
<p>하지만 문득 이런 궁금증이 들었다.</p>
<blockquote>
<p>화면에는 200px 크기로 보이는데, 브라우저는 정말 200px 크기의 이미지만 다운로드하고 있을까?</p>
</blockquote>
<p>이 질문을 시작으로 <code>img</code>, <code>srcset</code>, <code>picture</code>, <code>lazy loading</code>이 왜 등장하게 되었는지 정리해보았다.</p>
<hr>
<h1 id="img-태그만-사용하면-어떤-일이-일어날까">img 태그만 사용하면 어떤 일이 일어날까?</h1>
<p>다음과 같은 이미지가 있다고 가정해보자.</p>
<ul>
<li>실제 이미지 크기: 1200 × 1200</li>
<li>파일 용량: 500KB</li>
</ul>
<p>그리고 화면에서는 아래처럼 렌더링된다.</p>
<pre><code class="language-html">&lt;img src=&quot;/product.jpg&quot; alt=&quot;상품 이미지&quot; /&gt;</code></pre>
<pre><code class="language-css">img {
  width: 200px;
  height: 200px;
}</code></pre>
<p>사용자 눈에는 200px 크기로 보인다.</p>
<p>하지만 브라우저는 CSS가 적용된 최종 렌더링 크기를 기준으로 이미지를 다운로드하지 않는다.</p>
<p><code>src</code>에 명시된 원본 이미지를 다운로드한 뒤, 렌더링 과정에서 200px 크기로 축소한다.</p>
<p>즉,</p>
<ul>
<li>보여지는 크기: 200px</li>
<li>다운로드되는 크기: 1200px</li>
</ul>
<p>일 수 있다.</p>
<p>이미지 한 장이라면 큰 문제가 아닐 수 있다.</p>
<p>하지만 상품 목록 페이지처럼 이미지가 수십 개 존재한다면 이야기가 달라진다.</p>
<pre><code class="language-text">500KB × 20장 = 10MB</code></pre>
<p>사용자는 실제로 필요하지 않은 데이터를 다운로드하고 있을 수도 있다.</p>
<hr>
<h1 id="브라우저는-어떤-이미지를-내려받아야-하는지-모른다">브라우저는 어떤 이미지를 내려받아야 하는지 모른다</h1>
<p>사실 브라우저 입장에서는 당연한 행동이다.</p>
<pre><code class="language-html">&lt;img src=&quot;/product.jpg&quot; /&gt;</code></pre>
<p>이 정보만 보고는</p>
<ul>
<li>사용자의 화면 크기</li>
<li>디바이스 해상도</li>
<li>실제 렌더링 크기</li>
</ul>
<p>를 정확히 판단할 수 없다.</p>
<p>브라우저는 단순히</p>
<blockquote>
<p>&quot;product.jpg를 요청하라고 했으니 product.jpg를 다운로드해야겠다.&quot;</p>
</blockquote>
<p>라고 생각한다.</p>
<p>그래서 등장한 것이 <code>srcset</code>이다.</p>
<hr>
<h1 id="srcset은-어떤-문제를-해결할까">srcset은 어떤 문제를 해결할까?</h1>
<p><code>srcset</code>은 브라우저에게 여러 크기의 이미지 후보를 제공하는 기능이다.</p>
<pre><code class="language-html">&lt;img
  src=&quot;product-400.jpg&quot;
  srcset=&quot;
    product-400.jpg 400w,
    product-800.jpg 800w,
    product-1200.jpg 1200w
  &quot;
  alt=&quot;상품 이미지&quot;
/&gt;</code></pre>
<p>이제 브라우저는</p>
<ul>
<li>현재 화면 크기</li>
<li>디바이스 픽셀 비율(DPR)</li>
</ul>
<p>을 고려하여 적절한 이미지를 선택할 수 있다.</p>
<p>예를 들어</p>
<table>
<thead>
<tr>
<th>환경</th>
<th>선택되는 이미지</th>
</tr>
</thead>
<tbody><tr>
<td>모바일</td>
<td>400px</td>
</tr>
<tr>
<td>태블릿</td>
<td>800px</td>
</tr>
<tr>
<td>데스크톱</td>
<td>1200px</td>
</tr>
</tbody></table>
<p>처럼 동작할 수 있다.</p>
<p>즉, 브라우저가 상황에 맞는 이미지를 스스로 선택할 수 있게 된다.</p>
<hr>
<h1 id="retina-디스플레이에서는-어떻게-될까">Retina 디스플레이에서는 어떻게 될까?</h1>
<p>같은 200px 영역이라도 디바이스마다 필요한 이미지 크기가 다르다.</p>
<p>예를 들어 DPR(Device Pixel Ratio)이 2인 환경에서는</p>
<pre><code class="language-text">200px 영역
↓
실제로는 400px 이미지 필요</code></pre>
<p>할 수 있다.</p>
<p>그래서 브라우저는 단순히 화면 크기만 보는 것이 아니라 디바이스 해상도까지 고려해서 이미지를 선택한다.</p>
<p><code>srcset</code>이 없다면</p>
<ul>
<li>너무 큰 이미지를 받거나</li>
<li>너무 작은 이미지를 받아 흐릿하게 보이는</li>
</ul>
<p>문제가 발생할 수 있다.</p>
<hr>
<h1 id="picture-태그는-왜-등장했을까">picture 태그는 왜 등장했을까?</h1>
<p><code>srcset</code>은 같은 이미지의 크기만 바꾸는 데 적합하다.</p>
<p>하지만 경우에 따라서는 아예 다른 이미지를 제공해야 할 수도 있다.</p>
<p>예를 들어</p>
<ul>
<li>모바일에서는 세로 이미지</li>
<li>데스크톱에서는 가로 이미지</li>
</ul>
<p>를 보여주고 싶을 수 있다.</p>
<p>이때 사용하는 것이 <code>picture</code> 태그다.</p>
<pre><code class="language-html">&lt;picture&gt;
  &lt;source
    media=&quot;(min-width: 768px)&quot;
    srcset=&quot;/banner-desktop.jpg&quot;
  /&gt;

  &lt;source
    media=&quot;(max-width: 767px)&quot;
    srcset=&quot;/banner-mobile.jpg&quot;
  /&gt;

  &lt;img src=&quot;/banner.jpg&quot; alt=&quot;배너&quot; /&gt;
&lt;/picture&gt;</code></pre>
<p>브라우저는 조건에 맞는 이미지를 선택하여 렌더링한다.</p>
<p>즉,</p>
<p><code>srcset</code>은 &quot;어떤 크기의 이미지를 사용할까?&quot;를 해결하고,</p>
<p><code>picture</code>는 &quot;어떤 이미지를 사용할까?&quot;를 해결한다.</p>
<hr>
<h1 id="이미지-포맷도-최적화-대상이다">이미지 포맷도 최적화 대상이다</h1>
<p>이미지 최적화는 크기만의 문제가 아니다.</p>
<p>포맷 역시 중요하다.</p>
<p>일반적으로 다음과 같은 압축 효율을 가진다.</p>
<table>
<thead>
<tr>
<th>포맷</th>
<th>상대 용량</th>
</tr>
</thead>
<tbody><tr>
<td>JPG</td>
<td>100%</td>
</tr>
<tr>
<td>WebP</td>
<td>60~80%</td>
</tr>
<tr>
<td>AVIF</td>
<td>40~70%</td>
</tr>
</tbody></table>
<p>이를 활용하면 같은 이미지를 더 작은 용량으로 제공할 수 있다.</p>
<pre><code class="language-html">&lt;picture&gt;
  &lt;source
    srcset=&quot;/product.avif&quot;
    type=&quot;image/avif&quot;
  /&gt;

  &lt;source
    srcset=&quot;/product.webp&quot;
    type=&quot;image/webp&quot;
  /&gt;

  &lt;img src=&quot;/product.jpg&quot; alt=&quot;상품&quot; /&gt;
&lt;/picture&gt;</code></pre>
<p>브라우저는 지원 가능한 가장 효율적인 포맷을 선택한다.</p>
<hr>
<h1 id="모든-이미지를-한-번에-다운로드해야-할까">모든 이미지를 한 번에 다운로드해야 할까?</h1>
<p>상품 목록 페이지를 생각해보자.</p>
<p>사용자가 처음 보는 영역은 화면 상단뿐이다.</p>
<p>그런데 아래쪽에 있는 이미지까지 모두 다운로드한다면 어떨까?</p>
<p>초기 로딩 시간이 길어질 수 있다.</p>
<p>이를 해결하기 위해 등장한 것이 Lazy Loading이다.</p>
<pre><code class="language-html">&lt;img
  src={imageUrl}
  alt={name}
  loading=&quot;lazy&quot;
/&gt;</code></pre>
<p>브라우저는 해당 이미지가 화면에 가까워졌을 때 다운로드를 시작한다.</p>
<p>결과적으로</p>
<ul>
<li>초기 페이지 로딩 속도 개선</li>
<li>네트워크 사용량 감소</li>
</ul>
<p>효과를 얻을 수 있다.</p>
<p>특히 무한 스크롤이나 상품 목록 페이지에서 유용하다.</p>
<hr>
<h1 id="cls는-왜-발생할까">CLS는 왜 발생할까?</h1>
<p>이미지와 관련하여 자주 언급되는 성능 지표 중 하나가 CLS(Cumulative Layout Shift)다.</p>
<p>다음 코드를 보자.</p>
<pre><code class="language-html">&lt;img src=&quot;/product.jpg&quot; alt=&quot;상품&quot; /&gt;</code></pre>
<p>브라우저는 이미지 크기를 알 수 없기 때문에 처음에는 공간을 확보하지 못한다.</p>
<p>이후 이미지가 로딩되면 갑자기 공간이 생기면서 화면이 밀릴 수 있다.</p>
<p>사용자는</p>
<ul>
<li>읽고 있던 위치가 바뀌고</li>
<li>버튼 위치가 움직이고</li>
<li>의도치 않은 클릭을 하게 될 수도 있다</li>
</ul>
<p>이를 방지하려면 이미지 크기를 미리 알려주는 것이 좋다.</p>
<pre><code class="language-html">&lt;img
  src=&quot;/product.jpg&quot;
  alt=&quot;상품&quot;
  width=&quot;300&quot;
  height=&quot;300&quot;
/&gt;</code></pre>
<p>또는 CSS의 <code>aspect-ratio</code>를 활용할 수도 있다.</p>
<hr>
<h1 id="정리">정리</h1>
<p>처음에는 단순히 이미지를 보여주는 태그라고 생각했던 <code>img</code> 태그가 생각보다 많은 고민을 담고 있다는 것을 알게 되었다.</p>
<p>이미지 최적화는 단순히 파일 용량을 줄이는 것이 아니다.</p>
<p>브라우저가</p>
<ul>
<li>어떤 이미지를 다운로드해야 하는지</li>
<li>언제 다운로드해야 하는지</li>
<li>어떤 포맷을 사용해야 하는지</li>
</ul>
<p>를 효율적으로 결정할 수 있도록 돕는 과정에 가깝다.</p>
<p>정리하면,</p>
<ul>
<li><code>srcset</code> : 적절한 크기의 이미지를 선택하기 위해</li>
<li><code>picture</code> : 상황에 따라 다른 이미지를 제공하기 위해</li>
<li>WebP / AVIF : 더 작은 용량으로 전달하기 위해</li>
<li><code>loading=&quot;lazy&quot;</code> : 필요한 시점에 다운로드하기 위해</li>
<li>width / height : CLS를 방지하기 위해</li>
</ul>
<p>사용할 수 있다.</p>
<p>실제로 원본 이미지(1200px)와 srcset 적용 후 다운로드 크기를 비교한 실험 결과를 추가해볼 예정</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TypeScript 타입 패턴 (3) — 기타 자주 쓰이는 패턴]]></title>
            <link>https://velog.io/@park_log/TypeScript-%ED%83%80%EC%9E%85-%ED%8C%A8%ED%84%B4-3-%EA%B8%B0%ED%83%80-%EC%9E%90%EC%A3%BC-%EC%93%B0%EC%9D%B4%EB%8A%94-%ED%8C%A8%ED%84%B4</link>
            <guid>https://velog.io/@park_log/TypeScript-%ED%83%80%EC%9E%85-%ED%8C%A8%ED%84%B4-3-%EA%B8%B0%ED%83%80-%EC%9E%90%EC%A3%BC-%EC%93%B0%EC%9D%B4%EB%8A%94-%ED%8C%A8%ED%84%B4</guid>
            <pubDate>Thu, 16 Jul 2026 05:14:43 GMT</pubDate>
            <description><![CDATA[<p>면접과 실무에서 빈도 높은 세 가지. 특히 마지막 <code>unknown</code>과 API 에러 처리는 실전에서 가장 자주 마주치는 주제다.</p>
<p>이 글에서 다루는 것:</p>
<ol>
<li>제네릭 + 제약(<code>extends</code>)</li>
<li>함수 오버로드 vs 유니온</li>
<li><code>unknown</code> vs <code>any</code> — 그리고 API 응답/에러 타입 정의</li>
</ol>
<hr>
<h2 id="1-제네릭--제약extends">1. 제네릭 + 제약(<code>extends</code>)</h2>
<p><strong>언제 쓰나:</strong> 여러 타입에 <strong>똑같이 동작하는</strong> 함수/타입을 만들되, 입력에 따라 반환 타입이 달라져야 할 때. 그리고 아무 타입이나 받으면 안 되고 <strong>일정 조건을 만족하는 타입만</strong> 받고 싶을 때 <code>extends</code>로 제약.</p>
<p><strong>문제 상황:</strong> <code>any</code>로 받으면 타입 정보가 사라진다.</p>
<pre><code class="language-ts">function first(arr: any[]): any {
  return arr[0]; // 반환이 any — 이후 타입 안전성 상실
}</code></pre>
<p><strong>패턴:</strong> 제네릭 <code>T</code>로 입력·출력 타입을 연결한다. 필요하면 <code>extends</code>로 제약.</p>
<pre><code class="language-ts">function first&lt;T&gt;(arr: T[]): T | undefined {
  return arr[0]; // 입력 타입 그대로 반환
}
first([1, 2, 3]);  // number | undefined
first([&quot;a&quot;, &quot;b&quot;]); // string | undefined

// 제약: id를 가진 타입만 허용
function getId&lt;T extends { id: number }&gt;(item: T): number {
  return item.id;
}
getId({ id: 1, name: &quot;김&quot; }); // ✅
getId({ name: &quot;김&quot; });        // ❌ id 없음</code></pre>
<p><strong>주의점:</strong> 제네릭을 남용해 실제로는 한 타입에만 쓰이는데 <code>&lt;T&gt;</code>를 붙이면 오히려 복잡해진다. &quot;정말 여러 타입에 재사용되는가&quot;를 기준으로 판단.</p>
<hr>
<h2 id="2-함수-오버로드-vs-유니온">2. 함수 오버로드 vs 유니온</h2>
<p><strong>언제 쓰나:</strong> 하나의 함수가 <strong>입력에 따라 다른 반환 타입</strong>을 가질 때. 입력·출력의 조합이 정해져 있으면 오버로드, 단순히 여러 타입을 받기만 하면 유니온.</p>
<p><strong>패턴 A — 유니온 (입출력 관계가 느슨할 때):</strong></p>
<pre><code class="language-ts">function format(value: string | number): string {
  return String(value);
}</code></pre>
<p><strong>패턴 B — 오버로드 (입력에 따라 반환이 달라질 때):</strong></p>
<pre><code class="language-ts">function parse(value: string): string;
function parse(value: number): number;
function parse(value: string | number): string | number {
  return typeof value === &quot;string&quot; ? value.trim() : value * 2;
}

const a = parse(&quot;hi&quot;); // string으로 추론
const b = parse(42);   // number로 추론</code></pre>
<p><strong>주의점:</strong> 오버로드는 <strong>선언 시그니처</strong>(위 두 줄)와 <strong>구현 시그니처</strong>(세 번째)가 분리된다. 구현 시그니처는 호출자에게 보이지 않는다. 유니온으로 충분하면 오버로드는 과하다 — 반환 타입이 입력에 따라 갈릴 때만 쓴다.</p>
<hr>
<h2 id="3-unknown-vs-any--그리고-api-에러-처리">3. <code>unknown</code> vs <code>any</code> — 그리고 API 에러 처리</h2>
<p><strong>언제 쓰나:</strong> 타입을 <strong>아직 모르는</strong> 값(API 응답, <code>JSON.parse</code> 결과, 외부 입력, <code>catch</code>의 error)을 다룰 때. 결론부터: <strong><code>any</code> 대신 <code>unknown</code>을 써라.</strong></p>
<h3 id="3-1-any는-검사를-끄고-unknown은-검사를-강제한다">3-1. <code>any</code>는 검사를 끄고, <code>unknown</code>은 검사를 강제한다</h3>
<pre><code class="language-ts">const data: any = JSON.parse(res);
data.user.name.toUpperCase(); // 런타임에 터져도 컴파일러 침묵 ❌</code></pre>
<p><code>any</code>는 타입 검사를 통째로 꺼버린다. 반면 <code>unknown</code>은 쓰기 전에 좁히기를 강제한다.</p>
<pre><code class="language-ts">const data: unknown = JSON.parse(res);

// 바로 못 씀 — 검증을 강제당함
if (typeof data === &quot;object&quot; &amp;&amp; data !== null &amp;&amp; &quot;user&quot; in data) {
  // 여기서 타입 가드로 더 좁혀서 사용
}</code></pre>
<p><strong>핵심 차이:</strong> <code>any</code>는 &quot;검사 끄기&quot;, <code>unknown</code>은 &quot;검사하기 전엔 못 씀&quot;. 외부에서 들어오는 모든 값의 기본 타입은 <code>unknown</code>으로 두는 습관이 안전하다.</p>
<h3 id="3-2-api-응답-타입-정의--성공과-실패를-함께">3-2. API 응답 타입 정의 — 성공과 실패를 함께</h3>
<p>실무에서 API 응답은 성공만 있는 게 아니다. 성공/실패를 <strong>discriminated union</strong>(1편 참고)으로 정의하면, 응답을 다루는 쪽에서 안전하게 분기된다.</p>
<pre><code class="language-ts">// 성공 시 데이터, 실패 시 에러 정보
type ApiResponse&lt;T&gt; =
  | { success: true; data: T }
  | { success: false; error: ApiError };

// 에러 자체의 타입도 명확히 정의
interface ApiError {
  code: string;      // 예: &quot;UNAUTHORIZED&quot;, &quot;NOT_FOUND&quot;
  message: string;   // 사용자/개발자용 메시지
  status: number;    // HTTP 상태 코드
}</code></pre>
<p>이제 응답을 쓰는 쪽:</p>
<pre><code class="language-ts">function render(res: ApiResponse&lt;User&gt;) {
  if (res.success) {
    res.data.name;    // ✅ User
  } else {
    res.error.message; // ✅ ApiError
    res.error.status;  // ✅ number
  }
}</code></pre>
<h3 id="3-3-catch의-error는-unknown이다">3-3. <code>catch</code>의 error는 <code>unknown</code>이다</h3>
<p>TypeScript 4.4+에서 <code>catch</code>의 error는 기본이 <code>unknown</code>이다(그 전에는 <code>any</code>였다). <code>error.message</code>를 바로 못 쓴다 — 검증이 필요하다.</p>
<pre><code class="language-ts">try {
  await fetchUser();
} catch (error) {
  // error는 unknown — 바로 .message 접근 불가
  console.log(error.message); // ❌ 컴파일 에러
}</code></pre>
<p><strong>패턴:</strong> 에러 형태를 좁히는 타입 가드를 만들어 둔다.</p>
<pre><code class="language-ts">// Error 인스턴스인지 확인
function isError(value: unknown): value is Error {
  return value instanceof Error;
}

// 우리가 정의한 ApiError 형태인지 확인
function isApiError(value: unknown): value is ApiError {
  return (
    typeof value === &quot;object&quot; &amp;&amp;
    value !== null &amp;&amp;
    &quot;code&quot; in value &amp;&amp;
    &quot;status&quot; in value
  );
}

try {
  await fetchUser();
} catch (error) {
  if (isApiError(error)) {
    console.log(error.status, error.message); // ✅ ApiError로 좁혀짐
  } else if (isError(error)) {
    console.log(error.message);               // ✅ 표준 Error
  } else {
    console.log(&quot;알 수 없는 에러&quot;, error);     // 최후의 폴백
  }
}</code></pre>
<h3 id="3-4-실전-조합-fetch-래퍼">3-4. 실전 조합: fetch 래퍼</h3>
<p>위 조각들을 합치면, 던지지 않고 <strong>항상 <code>ApiResponse</code>를 반환</strong>하는 안전한 래퍼가 된다.</p>
<pre><code class="language-ts">async function apiFetch&lt;T&gt;(url: string): Promise&lt;ApiResponse&lt;T&gt;&gt; {
  try {
    const res = await fetch(url);

    if (!res.ok) {
      return {
        success: false,
        error: { code: &quot;HTTP_ERROR&quot;, message: res.statusText, status: res.status },
      };
    }

    const data = (await res.json()) as T; // 실무에선 zod 등으로 검증 권장
    return { success: true, data };
  } catch (error) {
    return {
      success: false,
      error: {
        code: &quot;NETWORK_ERROR&quot;,
        message: isError(error) ? error.message : &quot;요청 실패&quot;,
        status: 0,
      },
    };
  }
}

// 호출부 — 성공/실패가 타입으로 강제 분기됨
const res = await apiFetch&lt;User&gt;(&quot;/api/user/1&quot;);
if (res.success) {
  console.log(res.data.name);
} else {
  console.log(res.error.code, res.error.message);
}</code></pre>
<p><strong>주의점:</strong></p>
<ul>
<li><code>as T</code>로 응답을 단언한 부분은 여전히 &quot;우기기&quot;다. 신뢰할 수 없는 외부 데이터라면 <a href="https://zod.dev">zod</a> 같은 스키마 검증 라이브러리로 런타임 검증까지 하는 게 안전하다. <code>unknown</code>의 가치는 <strong>좁히기를 강제</strong>하는 데 있는데, 결국 <code>as</code>로 우겨버리면 <code>any</code>와 다를 게 없어진다.</li>
<li>에러 타입(<code>ApiError</code>)을 프로젝트 전역에서 하나로 정의해두면, 에러 처리 코드가 일관되고 새 에러 코드를 추가하기도 쉽다.</li>
</ul>
<hr>
<h2 id="정리">정리</h2>
<table>
<thead>
<tr>
<th>상황</th>
<th>패턴</th>
</tr>
</thead>
<tbody><tr>
<td>여러 타입에 재사용 + 조건 제약</td>
<td>제네릭 + <code>extends</code></td>
</tr>
<tr>
<td>입력 따라 반환 타입이 다름</td>
<td>함수 오버로드</td>
</tr>
<tr>
<td>타입 모르는 외부 값</td>
<td><code>unknown</code> (+ 좁히기)</td>
</tr>
<tr>
<td>API 성공/실패 응답</td>
<td>discriminated union + <code>ApiError</code> 타입</td>
</tr>
<tr>
<td><code>catch</code>의 error</td>
<td><code>unknown</code> → 타입 가드로 좁히기</td>
</tr>
</tbody></table>
<p>실무 기본기의 관통 원칙: <strong>외부에서 들어오는 값은 믿지 말고, 타입 시스템 경계에서 검증하라.</strong> <code>unknown</code>으로 받아 좁혀 쓰는 습관 하나가 런타임 에러를 크게 줄인다.</p>
<p><em>이전 편: 타입 조작 — as const, keyof typeof, 유틸리티 타입</em></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TypeScript 타입 패턴 (2) — 타입 조작(Utility)]]></title>
            <link>https://velog.io/@park_log/TypeScript-%ED%83%80%EC%9E%85-%ED%8C%A8%ED%84%B4-2-%ED%83%80%EC%9E%85-%EC%A1%B0%EC%9E%91Utility</link>
            <guid>https://velog.io/@park_log/TypeScript-%ED%83%80%EC%9E%85-%ED%8C%A8%ED%84%B4-2-%ED%83%80%EC%9E%85-%EC%A1%B0%EC%9E%91Utility</guid>
            <pubDate>Thu, 16 Jul 2026 05:14:29 GMT</pubDate>
            <description><![CDATA[<p>기존 타입/값에서 새 타입을 파생하는 도구들. 타입의 <strong>단일 출처(single source of truth)</strong>를 만드는 데 핵심. 상수 하나만 고치면 관련 타입이 전부 따라오게 만드는 게 목표다.</p>
<p>이 글에서 다루는 것:</p>
<ol>
<li><code>as const</code></li>
<li><code>keyof typeof</code></li>
<li>내장 유틸리티 타입</li>
<li>템플릿 리터럴 타입</li>
</ol>
<hr>
<h2 id="1-as-const">1. <code>as const</code></h2>
<p><strong>언제 쓰나:</strong> 배열이나 객체 리터럴을 <strong>넓은 타입이 아니라 정확한 리터럴 타입</strong>으로 고정하고 싶을 때. 상수 목록에서 유니온 타입을 뽑는 출발점.</p>
<p><strong>문제 상황:</strong> 그냥 선언하면 타입이 넓어진다.</p>
<pre><code class="language-ts">const categories = [&quot;food&quot;, &quot;beverage&quot;, &quot;goods&quot;];
// 타입: string[]  ← &quot;food&quot; 같은 리터럴 정보 사라짐</code></pre>
<p><strong>패턴:</strong> <code>as const</code>로 읽기 전용 리터럴로 고정한다.</p>
<pre><code class="language-ts">const categories = [&quot;food&quot;, &quot;beverage&quot;, &quot;goods&quot;] as const;
// 타입: readonly [&quot;food&quot;, &quot;beverage&quot;, &quot;goods&quot;]

type Category = typeof categories[number];
// &quot;food&quot; | &quot;beverage&quot; | &quot;goods&quot;</code></pre>
<p>이렇게 하면 카테고리 목록이 <code>categories</code> 한 곳에서만 관리되고, 타입은 거기서 자동으로 파생된다. 새 카테고리를 추가할 때 배열만 고치면 타입이 따라온다.</p>
<p><strong>주의점:</strong> <code>as const</code>가 붙은 값은 <strong>읽기 전용</strong>이 된다. <code>push()</code> 등 변경 메서드를 쓰려 하면 에러 — 상수니까 의도된 동작이다. 값을 좁은 타입으로 우기는 <code>as Category</code>와는 완전히 다른 용도이니 혼동하지 말 것. (<code>as const</code>는 &quot;최대한 좁게 읽어달라&quot;는 요청이고, <code>as Category</code>는 &quot;검증 없이 이 타입이라 쳐&quot;라는 우기기다.)</p>
<hr>
<h2 id="2-keyof-typeof">2. <code>keyof typeof</code></h2>
<p><strong>언제 쓰나:</strong> 객체의 <strong>키들만 허용하는 타입</strong>이 필요할 때. 라벨 맵, 설정 객체, 상수 테이블에서 키 목록을 타입으로 재사용.</p>
<p><strong>문제 상황:</strong> 객체 키를 문자열로 받으면 오타를 못 잡는다.</p>
<pre><code class="language-ts">const LABELS = { food: &quot;식품&quot;, beverage: &quot;음료&quot;, goods: &quot;잡화&quot; };

function getLabel(key: string) {
  return LABELS[key]; // key가 뭐든 통과, 오타 못 잡음
}</code></pre>
<p><strong>패턴:</strong> <code>typeof</code>로 값의 타입을, <code>keyof</code>로 그 키들을 유니온으로 뽑는다.</p>
<pre><code class="language-ts">const LABELS = { food: &quot;식품&quot;, beverage: &quot;음료&quot;, goods: &quot;잡화&quot; } as const;

type LabelKey = keyof typeof LABELS;
// &quot;food&quot; | &quot;beverage&quot; | &quot;goods&quot;

function getLabel(key: LabelKey) {
  return LABELS[key]; // ✅ 정의된 키만 허용
}</code></pre>
<p><strong>주의점:</strong> <code>typeof</code>(값 → 타입)와 <code>keyof</code>(타입 → 키 유니온)는 방향이 다른 별개 연산자다. <code>keyof typeof X</code>는 &quot;X라는 값의 타입에서 키를 뽑아라&quot;로 읽으면 된다. <code>as const</code>와 짝을 이룰 때 가장 강력하다.</p>
<hr>
<h2 id="3-내장-유틸리티-타입">3. 내장 유틸리티 타입</h2>
<p><strong>언제 쓰나:</strong> 이미 있는 타입을 조금 변형한 타입이 필요할 때. 매번 새로 정의하지 말고 조합한다.</p>
<table>
<thead>
<tr>
<th>유틸리티</th>
<th>하는 일</th>
<th>예시 상황</th>
</tr>
</thead>
<tbody><tr>
<td><code>Partial&lt;T&gt;</code></td>
<td>모든 속성 옵셔널</td>
<td>수정 폼(일부만 업데이트)</td>
</tr>
<tr>
<td><code>Required&lt;T&gt;</code></td>
<td>모든 속성 필수</td>
<td>옵셔널 제거</td>
</tr>
<tr>
<td><code>Pick&lt;T, K&gt;</code></td>
<td>일부 속성만 추출</td>
<td>목록용 축약 타입</td>
</tr>
<tr>
<td><code>Omit&lt;T, K&gt;</code></td>
<td>일부 속성 제외</td>
<td>id 뺀 생성 요청 타입</td>
</tr>
<tr>
<td><code>Record&lt;K, V&gt;</code></td>
<td>키-값 맵 타입</td>
<td>카테고리별 설정</td>
</tr>
<tr>
<td><code>ReturnType&lt;F&gt;</code></td>
<td>함수 반환 타입 추출</td>
<td>함수 결과 재사용</td>
</tr>
</tbody></table>
<pre><code class="language-ts">type User = { id: number; name: string; email: string };

type UserUpdate = Partial&lt;User&gt;;              // 전부 옵셔널
type UserPreview = Pick&lt;User, &quot;id&quot; | &quot;name&quot;&gt;; // { id, name }
type NewUser = Omit&lt;User, &quot;id&quot;&gt;;              // id 제외
type Settings = Record&lt;Category, boolean&gt;;    // { food: boolean, ... }

function createUser() { return { id: 1, name: &quot;김&quot; }; }
type Created = ReturnType&lt;typeof createUser&gt;; // { id: number; name: string }</code></pre>
<p><strong>주의점:</strong> 유틸리티 타입은 <strong>조합</strong>할 때 강력하다. <code>Partial&lt;Omit&lt;User, &quot;id&quot;&gt;&gt;</code>처럼 중첩 가능. 다만 과하게 중첩하면 가독성이 떨어지니, 자주 쓰는 조합은 별도 타입으로 이름을 붙이는 게 낫다.</p>
<hr>
<h2 id="4-템플릿-리터럴-타입">4. 템플릿 리터럴 타입</h2>
<p><strong>언제 쓰나:</strong> 문자열의 <strong>형식/패턴 자체를 타입으로 강제</strong>하고 싶을 때. ID 접미사, 이벤트 이름, CSS 단위 등.</p>
<p><strong>문제 상황:</strong> <code>string</code>으로 받으면 형식이 자유로워 실수가 난다.</p>
<pre><code class="language-ts">function getById(id: string) {} // &quot;user_1&quot;이든 &quot;asdf&quot;든 통과</code></pre>
<p><strong>패턴:</strong> 백틱과 <code>${}</code>로 문자열 패턴을 타입화한다.</p>
<pre><code class="language-ts">type UserId = `user_${number}`;
type EventName = `on${Capitalize&lt;string&gt;}`; // onClick, onScroll...

const id: UserId = &quot;user_1&quot;;   // ✅
const bad: UserId = &quot;user_x&quot;;  // ❌ number 자리에 x</code></pre>
<p><strong>주의점:</strong> 유니온과 결합하면 조합이 폭발적으로 늘어난다. <code>`${A|B}_${C|D}`</code>는 4가지 조합을 만든다. 편리하지만 너무 큰 유니온은 컴파일러를 느리게 만들 수 있으니 적당히.</p>
<hr>
<h2 id="정리">정리</h2>
<table>
<thead>
<tr>
<th>상황</th>
<th>패턴</th>
</tr>
</thead>
<tbody><tr>
<td>상수 배열/객체를 리터럴로 고정</td>
<td><code>as const</code></td>
</tr>
<tr>
<td>객체 키만 허용하는 타입</td>
<td><code>keyof typeof</code></td>
</tr>
<tr>
<td>기존 타입 변형</td>
<td><code>Partial</code>/<code>Pick</code>/<code>Omit</code>/<code>Record</code>/<code>ReturnType</code></td>
</tr>
<tr>
<td>문자열 형식을 타입으로 강제</td>
<td>템플릿 리터럴 타입</td>
</tr>
</tbody></table>
<p>타입 조작의 관통 원칙: <strong>타입을 손으로 중복 정의하지 말고, 값이나 기존 타입에서 파생하라.</strong> 단일 출처가 유지되면 변경에 강해진다.</p>
<p><em>이전 편: 좁히기 — 타입 가드, discriminated union, satisfies</em>
<em>다음 편: 실무 기본기 — 제네릭, 오버로드, unknown과 API 에러 처리</em></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[useEffect cleanup과 stale closure (state, ref)]]></title>
            <link>https://velog.io/@park_log/useEffect-cleanup%EA%B3%BC-stale-closure-state-ref</link>
            <guid>https://velog.io/@park_log/useEffect-cleanup%EA%B3%BC-stale-closure-state-ref</guid>
            <pubDate>Thu, 16 Jul 2026 05:09:57 GMT</pubDate>
            <description><![CDATA[<p><code>useEffect</code>의 cleanup에서 <code>AbortController.abort()</code>를 호출하도록 짰는데, 요청이 취소되지 않았다. 디버깅해보니 cleanup 안에서 컨트롤러가 계속 <code>null</code>로 찍혔다. 분명히 <code>setAbortController</code>로 값을 넣어줬는데도. 이 글은 그 원인인 <strong>stale closure</strong>와, 해결책으로 <strong>ref</strong>를 쓰는 이유를 정리한다.</p>
<hr>
<h2 id="useeffect의-cleanup-function이란">useEffect의 cleanup function이란</h2>
<p>effect가 반환하는 함수다. 이름 그대로 &quot;정리(clean up)&quot;를 담당한다. 구독 해제, 타이머 clear, 이벤트 리스너 제거, 진행 중인 요청 abort 같은 뒷정리가 여기 들어간다.</p>
<pre><code class="language-tsx">useEffect(() =&gt; {
  const timer = setInterval(tick, 1000);

  return () =&gt; {
    clearInterval(timer); // cleanup
  };
}, []);</code></pre>
<h2 id="cleanup은-언제-호출되나">cleanup은 언제 호출되나</h2>
<p>두 가지 시점이다.</p>
<ol>
<li><strong>컴포넌트가 언마운트될 때</strong></li>
<li><strong>effect가 재실행되기 직전</strong> — 의존성 배열의 값이 바뀌면, 새 effect를 실행하기 전에 이전 effect의 cleanup을 먼저 돌린다.</li>
</ol>
<p>의존성 배열이 <code>[]</code>이면 재실행 자체가 없으므로, 이 경우엔 <strong>언마운트 때 딱 한 번만</strong> cleanup이 호출된다.</p>
<p>실행 순서를 타임라인으로 보면 이렇다.</p>
<pre><code>mount → effect
      ↓ (deps 변경)
      cleanup → effect (재실행)
      ↓ (deps 변경)
      cleanup → effect (재실행)
      ↓
unmount → cleanup (마지막 정리)</code></pre><p><code>deps === []</code>이면 가운데 재실행 구간이 통째로 사라지고 <code>mount → effect ... unmount → cleanup</code>만 남는다.</p>
<h2 id="클로저-복습">클로저 복습</h2>
<p>본론 전에 클로저를 한 번 짚고 가자. <strong>함수는 생성될 때 주변 스코프의 변수를 캡처한다.</strong></p>
<pre><code class="language-tsx">function makeCounter() {
  let count = 0;
  return () =&gt; ++count; // count를 캡처
}
const counter = makeCounter();
counter(); // 1
counter(); // 2</code></pre>
<p>반환된 함수는 <code>makeCounter</code>가 이미 끝났어도 <code>count</code>에 계속 접근한다. 이 &quot;캡처&quot; 동작이 다음 문제의 핵심이다.</p>
<h2 id="stale-closure--cleanup이-옛날-값을-보는-이유">stale closure — cleanup이 옛날 값을 보는 이유</h2>
<p>cleanup 함수도 결국 클로저다. <strong>생성된 시점의 값을 캡처한다.</strong> 그리고 <code>deps === []</code>이면 cleanup은 첫 렌더 때 딱 한 번 만들어진다.</p>
<p>문제 코드부터 보자.</p>
<pre><code class="language-tsx">function useProductList() {
  const [abortController, setAbortController] =
    useState&lt;AbortController | null&gt;(null);

  useEffect(() =&gt; {
    const controller = new AbortController();
    setAbortController(controller);
    fetchProducts({ signal: controller.signal });

    return () =&gt; {
      // 이 클로저는 첫 렌더 시점의 abortController를 캡처 → 계속 null
      abortController?.abort(); // 아무 일도 안 일어남
    };
  }, []);
}</code></pre>
<p>cleanup 클로저가 만들어진 첫 렌더 시점에 <code>abortController</code>는 아직 <code>null</code>이다. 이후 <code>setAbortController(controller)</code>로 state를 갱신해도, 클로저가 붙잡은 건 <strong>그 순간의 <code>null</code></strong>이다. state가 바뀌면 리렌더는 일어나지만, <code>[]</code> deps라 effect도 cleanup도 다시 만들어지지 않는다. 그래서 언마운트 시 실행되는 cleanup은 여전히 <code>null.abort()</code>를 시도하고, 실제 컨트롤러엔 닿지 못한다.</p>
<p>이렇게 클로저가 낡은 값을 붙잡고 있는 현상을 <strong>stale closure</strong>라고 부른다.</p>
<h2 id="왜-ref가-이걸-해결하나">왜 ref가 이걸 해결하나</h2>
<p><code>useState</code> 대신 <code>useRef</code>로 바꾸면 해결된다.</p>
<pre><code class="language-tsx">function useProductList() {
  const abortControllerRef = useRef&lt;AbortController | null&gt;(null);

  useEffect(() =&gt; {
    const controller = new AbortController();
    abortControllerRef.current = controller;
    fetchProducts({ signal: controller.signal });

    return () =&gt; {
      // 클로저가 캡처하는 건 ref &#39;객체&#39;. .current는 실행 시점의 최신값
      abortControllerRef.current?.abort(); // 실제로 요청 취소됨
    };
  }, []);
}</code></pre>
<p>핵심은 <strong>클로저가 캡처하는 대상이 바뀌었다는 것</strong>이다.</p>
<ul>
<li>state 버전: 클로저가 캡처한 건 <code>null</code>이라는 <strong>값</strong>. 첫 렌더의 스냅샷이 그대로 박제된다.</li>
<li>ref 버전: 클로저가 캡처한 건 <code>abortControllerRef</code>라는 <strong>객체 참조</strong>. 이 객체는 리렌더 내내 동일하고, 내부의 <code>.current</code>만 갈아끼워진다.</li>
</ul>
<p>cleanup이 실제로 실행되는 시점에 <code>abortControllerRef.current</code>를 읽으면, 그 순간의 최신 컨트롤러가 나온다. 그래서 <code>abort()</code>가 제대로 먹힌다.</p>
<h2 id="state-vs-ref-비교-정리">state vs ref 비교 정리</h2>
<table>
<thead>
<tr>
<th></th>
<th>state</th>
<th>ref</th>
</tr>
</thead>
<tbody><tr>
<td>값 변경 시 리렌더</td>
<td>O</td>
<td>X</td>
</tr>
<tr>
<td>렌더마다</td>
<td>새 스냅샷</td>
<td>동일 객체 유지</td>
</tr>
<tr>
<td>클로저가 캡처하는 것</td>
<td>그 순간의 값</td>
<td>객체 참조</td>
</tr>
<tr>
<td>콜백/cleanup에서 읽으면</td>
<td>캡처 시점의 옛날 값</td>
<td>실행 시점의 최신값</td>
</tr>
<tr>
<td>언제 쓰나</td>
<td>화면에 반영돼야 할 때</td>
<td>반영은 불필요 + 최신값만 읽으면 될 때</td>
</tr>
</tbody></table>
<h2 id="마무리--판단-기준">마무리 — 판단 기준</h2>
<p>정리하면 이렇다.</p>
<ul>
<li><strong>화면에 반영돼야 하는 값</strong>은 state. 값이 바뀌면 리렌더가 필요한 것들.</li>
<li><strong>렌더링에는 반영할 필요 없지만, 콜백이나 cleanup 안에서 최신값을 읽어야 하는 값</strong>은 ref.</li>
</ul>
<p><code>useEffect(() =&gt; {...}, [])</code> 안에서 최신 state를 참조해야 하는 상황이 생기면, stale closure를 의심하고 ref를 떠올리면 된다. cleanup은 첫 렌더의 클로저를 그대로 물고 실행된다는 점만 기억하면, 왜 값이 안 바뀌는지 헷갈릴 일이 없다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TypeScript 타입 패턴 (1) — 좁히기(Narrowing)]]></title>
            <link>https://velog.io/@park_log/TypeScript-%ED%83%80%EC%9E%85-%ED%8C%A8%ED%84%B4-1-%EC%A2%81%ED%9E%88%EA%B8%B0Narrowing</link>
            <guid>https://velog.io/@park_log/TypeScript-%ED%83%80%EC%9E%85-%ED%8C%A8%ED%84%B4-1-%EC%A2%81%ED%9E%88%EA%B8%B0Narrowing</guid>
            <pubDate>Thu, 16 Jul 2026 04:08:36 GMT</pubDate>
            <description><![CDATA[<h1 id="typescript-타입-패턴-1--좁히기narrowing">TypeScript 타입 패턴 (1) — 좁히기(Narrowing)</h1>
<p>넓은 타입(<code>string</code>, <code>unknown</code>, 유니온)을 특정 타입으로 안전하게 좁히는 방법들. 공통 목표는 <strong><code>as</code>로 우기지 않고, 런타임 검증을 타입에 연결하는 것.</strong></p>
<p>그러려면 먼저 <code>as</code>가 정확히 뭘 하는지 알아야 한다.</p>
<p>이 글에서 다루는 것:</p>
<ol start="0">
<li><code>as</code>(타입 단언) — 왜 좁히기가 필요한가</li>
<li><code>is</code> 타입 가드</li>
<li><code>in</code> 연산자 좁히기</li>
<li>Discriminated Union</li>
<li>Assertion Function (<code>asserts</code>)</li>
<li><code>satisfies</code></li>
</ol>
<hr>
<h2 id="0-as--타입-단언-그리고-왜-좁히기가-필요한가">0. <code>as</code> — 타입 단언, 그리고 왜 좁히기가 필요한가</h2>
<p><code>as</code>는 <strong>타입 단언(type assertion)</strong>이다. 컴파일러에게 &quot;이 값의 타입은 이거야, 내가 책임진다&quot;라고 우기는 것.</p>
<pre><code class="language-ts">const value: unknown = &quot;hello&quot;;
const str = value as string; // &quot;얘 string이야&quot; 하고 단언</code></pre>
<h3 id="핵심-as는-런타임에-아무것도-하지-않는다">핵심: <code>as</code>는 런타임에 아무것도 하지 않는다</h3>
<p><code>as</code>는 <strong>검사를 하는 게 아니라 검사를 건너뛴다.</strong> 트랜스파일되면 흔적도 없이 사라진다. 그래서 단언이 틀려도 컴파일러는 잡아주지 못한다.</p>
<pre><code class="language-ts">const value: unknown = 123;   // 실제로는 숫자
const str = value as string;  // 컴파일 통과 (거짓말인데도)
str.toUpperCase();            // 💥 런타임 에러 — 컴파일러는 침묵</code></pre>
<p>이게 좁히기 패턴들이 존재하는 이유다. <code>as</code>는 타입 시스템의 <strong>비상구</strong>지 일상 도구가 아니다.</p>
<h3 id="두-방향을-구분하라--이게-제일-중요하다">두 방향을 구분하라 — 이게 제일 중요하다</h3>
<p>모든 <code>as</code>가 위험한 건 아니다. <strong>방향</strong>이 위험을 가른다.</p>
<pre><code class="language-ts">// ❌ 위험: 넓은 타입 → 좁은 타입으로 우기기 (검증을 건너뜀)
const cat = someString as Category;
// someString이 진짜 Category(&quot;food&quot; 등)인지 아무도 확인 안 함

// ✅ 상대적으로 안전: 좁은 타입 → 넓은 타입으로 취급 (정보를 버릴 뿐)
const arr = categories as readonly string[];
// Category[]를 string[]로 취급 — 없는 값을 있다고 우기는 게 아님</code></pre>
<ul>
<li><strong>넓은 걸 좁게 우기기</strong>: &quot;이 <code>string</code>은 사실 <code>Category</code>야&quot; → 틀릴 수 있고, 틀려도 안 잡힘. <strong>위험.</strong></li>
<li><strong>좁은 걸 넓게 취급</strong>: &quot;이 <code>Category[]</code>를 그냥 <code>string[]</code>로 봐줘&quot; → 타입 정보를 버릴 뿐 거짓말은 아님. <strong>대체로 안전.</strong></li>
</ul>
<p>앞으로 나올 <code>includes()</code> 예제에서 <code>(CATEGORIES as readonly string[])</code>가 자주 등장하는데, 이건 후자(안전한 방향)라서 남겨두는 것이다.</p>
<h3 id="as를-정당하게-쓰는-경우"><code>as</code>를 정당하게 쓰는 경우</h3>
<p><code>as</code>가 항상 잘못은 아니다. 이럴 땐 정당하다:</p>
<ol>
<li><strong>런타임 검증을 이미 마친 직후</strong> — <code>includes()</code>나 타입 가드로 확인한 값을 좁힐 때 (이게 뒤에 나올 패턴들의 핵심)</li>
<li><strong>좁은 → 넓은 방향</strong> — 위에서 본 안전한 방향</li>
<li><strong>개발자가 컴파일러보다 확실히 더 많이 아는 경우</strong> — 예: DOM에서 <code>document.getElementById(&quot;x&quot;) as HTMLInputElement</code>처럼 마크업을 보고 타입을 아는 경우</li>
</ol>
<p>반대로 <strong>검증 없이 넓은 걸 좁게 우기는</strong> <code>as</code>는 피해야 하고, 그 자리엔 거의 항상 더 나은 패턴(<code>is</code>, <code>in</code>, discriminated union...)이 있다.</p>
<h3 id="as에서-꼭-알아야-하는-것"><code>as</code>에서 꼭 알아야 하는 것</h3>
<ul>
<li><code>as</code>는 <strong>런타임 검증이 아니다.</strong> 컴파일러를 설득할 뿐 실제 타입은 안 바뀐다.</li>
<li><strong>넓은 타입 → 좁은 타입</strong>으로 우기는 게 위험하다(검증 생략). 반대 방향은 대체로 안전.</li>
<li><code>as</code>가 필요해지는 순간이 곧 <strong>&quot;더 나은 패턴이 있다&quot;는 신호</strong>다.</li>
<li>단, 검증 직후의 <code>as</code>나 안전한 방향의 <code>as</code>는 정당하다. &quot;<code>as</code> = 무조건 악&quot;이 아니라 <strong>&quot;검증 없는 좁히기 as&quot;가 악</strong>이다.</li>
</ul>
<blockquote>
<p><strong>참고 — <code>as</code>의 사촌들</strong></p>
<ul>
<li><code>as any</code>: 검사를 통째로 끈다. 가장 위험. 웬만하면 쓰지 말 것.</li>
<li><code>as unknown as T</code>: &quot;이중 단언&quot;. <code>as</code>로도 안 되는 억지 변환을 뚫을 때 쓰는데, 이게 필요하다는 건 대개 설계가 잘못됐다는 신호다.</li>
<li><code>!</code> (non-null 단언): <code>value!</code>는 <code>value as NonNullable&lt;...&gt;</code>의 축약. &quot;null 아님을 내가 보장&quot;인데, 이것도 틀리면 런타임에 터진다.</li>
</ul>
</blockquote>
<hr>
<h2 id="1-is-타입-가드-커스텀-가드">1. <code>is</code> 타입 가드 (커스텀 가드)</h2>
<p><strong>언제 쓰나:</strong> 어떤 값이 특정 타입인지 런타임에 검사하고, 그 결과를 타입 시스템에도 반영하고 싶을 때. <code>includes()</code>, 속성 존재 확인 등 <strong>직접 검증 로직을 짜야 하는</strong> 경우.</p>
<p><strong>문제 상황:</strong> <code>string</code>을 좁은 유니온으로 좁히려는데 <code>as</code>로 우기게 된다.</p>
<pre><code class="language-ts">type Category = &quot;food&quot; | &quot;beverage&quot; | &quot;goods&quot;;
const CATEGORIES: readonly Category[] = [&quot;food&quot;, &quot;beverage&quot;, &quot;goods&quot;];

function parse(value: string): Category {
  // includes가 Category만 받아서 어쩔 수 없이 as
  return CATEGORIES.includes(value as Category) ? (value as Category) : &quot;food&quot;;
}</code></pre>
<p><strong>패턴:</strong> 반환 타입을 <code>value is Category</code>로 선언한 함수를 만든다. <code>true</code>면 컴파일러가 이후 <code>value</code>를 <code>Category</code>로 좁힌다.</p>
<pre><code class="language-ts">function isCategory(value: string): value is Category {
  return (CATEGORIES as readonly string[]).includes(value);
}

function parse(value: string): Category {
  return isCategory(value) ? value : &quot;food&quot;; // as 불필요
}</code></pre>
<p><strong>주의점:</strong> 타입 가드 내부의 검증 로직이 실제로 맞는지는 <strong>개발자 책임</strong>이다. <code>value is Category</code>라고 선언해놓고 내부에서 엉뚱한 검사를 하면 컴파일러는 그대로 믿는다. 가드는 &quot;우기기&quot;를 한 곳에 격리하고 재사용 가능하게 만드는 것이지, 검증을 마법처럼 대신해주는 게 아니다.</p>
<hr>
<h2 id="2-in-연산자-좁히기">2. <code>in</code> 연산자 좁히기</h2>
<p><strong>언제 쓰나:</strong> 유니온 타입에서 <strong>특정 속성의 유무</strong>로 분기하고 싶을 때. 판별 필드를 따로 두기 애매한 객체 유니온에 적합.</p>
<p><strong>문제 상황:</strong> 두 객체 타입이 섞여 있는데, 있는 속성이 서로 다르다.</p>
<pre><code class="language-ts">type Fish = { swim: () =&gt; void };
type Bird = { fly: () =&gt; void };

function move(animal: Fish | Bird) {
  animal.swim(); // ❌ Bird엔 swim이 없음
}</code></pre>
<p><strong>패턴:</strong> <code>in</code>으로 속성 존재를 확인하면 그 분기 안에서 타입이 좁혀진다.</p>
<pre><code class="language-ts">function move(animal: Fish | Bird) {
  if (&quot;swim&quot; in animal) {
    animal.swim(); // ✅ Fish로 좁혀짐
  } else {
    animal.fly();  // ✅ Bird로 좁혀짐
  }
}</code></pre>
<p><strong>주의점:</strong> 속성 이름 오타를 잡아주지 못한다(<code>&quot;swm&quot; in animal</code>은 그냥 <code>false</code>). 구조가 복잡해지면 다음의 discriminated union이 더 안전하다.</p>
<hr>
<h2 id="3-discriminated-union-판별-유니온">3. Discriminated Union (판별 유니온)</h2>
<p><strong>언제 쓰나:</strong> 여러 형태의 값을 다루는데, 각각을 명확히 구분하는 <strong>공통 리터럴 필드</strong>를 둘 수 있을 때. API 응답, 상태 관리, 이벤트 처리의 표준 패턴.</p>
<p><strong>문제 상황:</strong> 성공/실패 응답을 하나의 타입으로 다루면서 안전하게 분기하고 싶다.</p>
<p><strong>패턴:</strong> 공통 판별 필드(<code>status</code>, <code>type</code>, <code>kind</code> 등)를 리터럴로 두고 유니온을 만든다.</p>
<pre><code class="language-ts">type ApiResult =
  | { status: &quot;success&quot;; data: string[] }
  | { status: &quot;error&quot;; message: string };

function handle(result: ApiResult) {
  if (result.status === &quot;success&quot;) {
    result.data;    // ✅ data 접근 가능
  } else {
    result.message; // ✅ message 접근 가능
  }
}</code></pre>
<p><strong>주의점:</strong> 판별 필드는 반드시 <strong>리터럴 타입</strong>이어야 한다(<code>status: string</code>이면 좁혀지지 않음). <code>never</code>를 활용한 exhaustive check를 곁들이면 분기 누락도 컴파일 타임에 잡을 수 있다.</p>
<pre><code class="language-ts">function handle(result: ApiResult) {
  switch (result.status) {
    case &quot;success&quot;: return result.data;
    case &quot;error&quot;:   return result.message;
    default:
      const _exhaustive: never = result; // 새 케이스 추가 시 여기서 에러
      return _exhaustive;
  }
}</code></pre>
<hr>
<h2 id="4-assertion-function-asserts-value-is-t">4. Assertion Function (<code>asserts value is T</code>)</h2>
<p><strong>언제 쓰나:</strong> 조건이 안 맞으면 <strong>아예 진행을 멈춰야(throw)</strong> 할 때. 가드가 <code>boolean</code>을 리턴한다면, assertion function은 통과하지 못하면 예외를 던진다. 입력 검증, 불변식(invariant) 체크에 적합.</p>
<p><strong>문제 상황:</strong> <code>null</code>일 수 있는 값을 매번 <code>if (x === null) throw ...</code>로 검사하는 게 반복된다.</p>
<p><strong>패턴:</strong> 반환 타입을 <code>asserts value is T</code>로 선언한다. 이 함수를 통과하면 이후 코드에서 타입이 좁혀진다.</p>
<pre><code class="language-ts">function assertDefined&lt;T&gt;(value: T): asserts value is NonNullable&lt;T&gt; {
  if (value === null || value === undefined) {
    throw new Error(&quot;값이 없습니다&quot;);
  }
}

function useUser(user: User | null) {
  assertDefined(user);
  user.name; // ✅ 여기서 user는 User (null 제거됨)
}</code></pre>
<p><strong>타입 가드와의 차이:</strong></p>
<table>
<thead>
<tr>
<th></th>
<th>타입 가드 (<code>is</code>)</th>
<th>assertion function (<code>asserts</code>)</th>
</tr>
</thead>
<tbody><tr>
<td>반환</td>
<td><code>boolean</code></td>
<td>없음(void), 실패 시 throw</td>
</tr>
<tr>
<td>사용</td>
<td><code>if (isX(v)) { ... }</code></td>
<td><code>assertX(v); // 이후 좁혀짐</code></td>
</tr>
<tr>
<td>용도</td>
<td>분기 처리</td>
<td>통과 강제 / 조기 실패</td>
</tr>
</tbody></table>
<p><strong>주의점:</strong> assertion function은 반환 타입 애노테이션이 없으면 동작하지 않는다. <code>function assertDefined(value): asserts value is ...</code>처럼 명시적으로 써야 한다. 화살표 함수에 타입 추론으로는 안 된다.</p>
<hr>
<h2 id="5-satisfies--as의-건강한-대안">5. <code>satisfies</code> — <code>as</code>의 건강한 대안</h2>
<p><strong>언제 쓰나:</strong> 객체/값이 <strong>특정 타입을 만족하는지 검사</strong>하고 싶지만, 추론된 <strong>좁은 타입은 그대로 유지</strong>하고 싶을 때. <code>as</code>를 쓰고 싶어지는 상황의 대부분이 사실 이것.</p>
<p><strong>문제 상황:</strong> <code>as</code>로 타입을 맞추면 구체적 정보가 뭉개진다.</p>
<pre><code class="language-ts">type Category = &quot;food&quot; | &quot;beverage&quot; | &quot;goods&quot;;

const labels = {
  food: &quot;식품&quot;,
  beverage: &quot;음료&quot;,
} as Record&lt;Category, string&gt;;

labels.food;    // string (리터럴 손실)
// goods 누락됐는데 as는 통과시킴 ❌</code></pre>
<p><strong>패턴:</strong> <code>satisfies</code>는 타입 만족 여부만 검사하고, 실제 타입은 좁게 유지한다.</p>
<pre><code class="language-ts">const labels = {
  food: &quot;식품&quot;,
  beverage: &quot;음료&quot;,
  goods: &quot;잡화&quot;,
} satisfies Record&lt;Category, string&gt;;

labels.food; // &quot;식품&quot; (리터럴 유지)
// goods 빠뜨리면 → 컴파일 에러 ✅</code></pre>
<p><strong>핵심 차이:</strong> <code>as</code>는 &quot;이 타입이라고 쳐(검증 없이, 타입 넓힘)&quot;, <code>satisfies</code>는 &quot;이 타입 맞는지 검사해줘(추론은 유지)&quot;.</p>
<p><strong>주의점:</strong> <code>satisfies</code>는 TypeScript 4.9+에서만 쓸 수 있다.</p>
<hr>
<h2 id="정리">정리</h2>
<table>
<thead>
<tr>
<th>상황</th>
<th>패턴</th>
</tr>
</thead>
<tbody><tr>
<td><code>string</code>을 좁은 유니온으로 검증하며 좁히기</td>
<td><code>is</code> 타입 가드</td>
</tr>
<tr>
<td>속성 유무로 유니온 분기</td>
<td><code>in</code> 연산자</td>
</tr>
<tr>
<td>여러 형태의 값을 판별 필드로 분기</td>
<td>discriminated union</td>
</tr>
<tr>
<td>조건 안 맞으면 throw하고 이후 좁히기</td>
<td>assertion function</td>
</tr>
<tr>
<td>객체가 타입 맞는지 검사하되 타입은 유지</td>
<td><code>satisfies</code></td>
</tr>
</tbody></table>
<p>좁히기의 관통 원칙: <strong>런타임 검증과 타입 시스템을 연결하라.</strong> <code>as</code>는 이 둘을 끊어버리기 때문에 최후의 수단이다.</p>
<p><em>다음 편: 타입 조작 — <code>as const</code>, <code>keyof typeof</code>, 유틸리티 타입, 템플릿 리터럴</em></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[React key prop 정리]]></title>
            <link>https://velog.io/@park_log/React-key-prop-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@park_log/React-key-prop-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Fri, 10 Jul 2026 01:15:57 GMT</pubDate>
            <description><![CDATA[<h1 id="react-key-prop은-정확히-무슨-일을-할까">React key prop은 정확히 무슨 일을 할까</h1>
<p>코드 리뷰에서 이런 질문을 받았습니다.</p>
<blockquote>
<p>&quot;React에서 key는 어떤 역할일까요?&quot;</p>
</blockquote>
<p>면접에서도 자주 나오는 주제라 이참에 제대로 정리했습니다. key만 따로 떼어 보면 이해가 잘 안 되고, Virtual DOM과 Reconciliation이라는 배경까지 같이 봐야 &quot;아, 그래서 key가 필요하구나&quot;가 됩니다. 그래서 배경부터 짚고 갑니다.</p>
<h2 id="흔한-오해부터">흔한 오해부터</h2>
<p>&quot;반복문에서 key를 안 쓰면 에러가 난다&quot;고 알고 있는 경우가 많은데, 정확히는 에러가 아니라 <strong>경고(warning)</strong>입니다.</p>
<pre><code>Warning: Each child in a list should have a unique &quot;key&quot; prop.</code></pre><p>경고가 떠도 화면은 일단 동작합니다. 하지만 key를 제대로 주지 않으면 <strong>성능 저하</strong>와 <strong>상태(state) 관련 버그</strong>가 따라옵니다. key는 &quot;경고를 끄는 용도&quot;가 아니라, <strong>React가 리스트의 각 요소를 식별하는 핵심 장치</strong>입니다.</p>
<h2 id="배경지식-3가지">배경지식 3가지</h2>
<p>key를 이해하려면 이 세 개념이 먼저 필요합니다.</p>
<p><strong>Virtual DOM</strong>은 실제 DOM을 흉내 낸 <strong>메모리 상의 가벼운 JS 객체 트리</strong>입니다. 화면에 직접 그려지지는 않습니다. 실제 DOM을 조작하는 건 웹에서 가장 느린 작업이기 때문에, 일단 메모리에서 먼저 계산해 보려는 것입니다.</p>
<p><strong>Reconciliation(재조정)</strong>은 state나 props가 바뀌었을 때, React가 <strong>새 Virtual DOM 트리</strong>를 만들고 <strong>이전 트리와 비교</strong>해서, 실제 DOM에 적용할 <strong>최소한의 변경</strong>만 계산해 내는 과정입니다.</p>
<p><strong>Diffing 알고리즘</strong>은 그 두 트리를 비교하는 방법입니다. 원래 트리를 통째로 비교하면 O(n³)이지만, React는 몇 가지 가정을 두고 이걸 O(n)까지 낮췄습니다.</p>
<ol>
<li><strong>타입이 다르면</strong> 통째로 교체한다. (<code>&lt;div&gt;</code>가 <code>&lt;span&gt;</code>으로 바뀌면 옛 트리는 버리고 새로 만든다)</li>
<li><strong>같은 타입이면</strong> 속성만 업데이트하고 DOM 노드는 재사용한다.</li>
<li>리스트에서는 <strong>key로</strong> &quot;어떤 요소가 그대로 유지되는지&quot; 개발자가 힌트를 준다.</li>
</ol>
<p>세 번째가 바로 key가 등장하는 지점입니다.</p>
<h2 id="key의-진짜-역할">key의 진짜 역할</h2>
<p>한 문장으로 정리하면 이렇습니다.</p>
<blockquote>
<p><strong>key는 &quot;이전 렌더의 어떤 컴포넌트가 = 이번 렌더의 이 컴포넌트인가?&quot;를 알려주는 식별자다.</strong></p>
</blockquote>
<p>리스트에서 같은 타입의 요소(<code>&lt;li&gt;</code> 여러 개)가 나열되면, React는 각각을 구분할 방법이 필요합니다. 그 역할을 key가 합니다. 이때 key는 <strong>형제 요소들 사이에서만 유일하면 되고</strong>, 전역에서 유일할 필요는 없습니다.</p>
<p>한 가지 중요한 점. <strong>key는 props가 아닙니다.</strong> 컴포넌트 안에서 <code>props.key</code>로 읽을 수 없고, 오직 React 내부의 재조정용으로만 쓰입니다.</p>
<h3 id="비유--데스크탑-파일-공식-문서">비유 — 데스크탑 파일 (공식 문서)</h3>
<p>파일에 이름이 없고 &quot;순서&quot;로만 부른다고 상상해 봅시다. &quot;첫 번째 파일, 두 번째 파일...&quot; 이렇게요. 쓸 수는 있지만, <strong>파일 하나를 지우는 순간 순서가 전부 밀려서 헷갈립니다.</strong> 반면 파일에 이름(= 안정적인 고유 key)이 있으면, 순서가 바뀌어도 어떤 게 어떤 건지 정확히 식별할 수 있습니다.</p>
<h2 id="왜-index를-key로-쓰면-안-되나">왜 index를 key로 쓰면 안 되나</h2>
<p><code>key={index}</code>를 쓰면 린트 경고는 사라집니다. 하지만 <strong>리스트가 바뀌는 순간</strong>(추가·삭제·정렬) 문제가 터집니다.</p>
<h3 id="성능-문제">성능 문제</h3>
<p><code>[Alice, Bob, Charlie]</code>에 index로 key를 준다고 해봅시다.</p>
<pre><code>&lt;li key=&quot;0&quot;&gt;Alice&lt;/li&gt;
&lt;li key=&quot;1&quot;&gt;Bob&lt;/li&gt;
&lt;li key=&quot;2&quot;&gt;Charlie&lt;/li&gt;</code></pre><p>여기서 맨 앞 Alice를 삭제하면 <code>[Bob, Charlie]</code>가 됩니다. 그런데 key가 index 기준이라 React는 이렇게 해석합니다.</p>
<pre><code>key=&quot;0&quot;: Alice → Bob 으로 바뀜    → 텍스트 업데이트
key=&quot;1&quot;: Bob → Charlie 로 바뀜    → 텍스트 업데이트
key=&quot;2&quot;: 사라짐                   → &lt;li&gt; 제거</code></pre><p>실제로 필요한 작업은 <strong>&quot;맨 앞 노드 1개 삭제&quot;뿐</strong>인데, 업데이트 2번 + 삭제 1번을 하고 있습니다. 리스트가 크고 각 항목이 무거우면 이 낭비가 커집니다.</p>
<p>고유 id를 key로 쓰면 이렇게 됩니다.</p>
<pre><code>&lt;li key=&quot;a1&quot;&gt;Alice&lt;/li&gt;  ← 이 key가 사라짐 → Alice만 정확히 제거
&lt;li key=&quot;b2&quot;&gt;Bob&lt;/li&gt;
&lt;li key=&quot;c3&quot;&gt;Charlie&lt;/li&gt;</code></pre><h3 id="상태-버그--이쪽이-더-무섭다">상태 버그 — 이쪽이 더 무섭다</h3>
<p>각 항목이 <strong>자체 상태</strong>를 가진 컴포넌트일 때(펼침/접힘, 체크박스, 입력값 등) 진짜 문제가 생깁니다.</p>
<p>리스트를 뒤집거나 정렬하면 <strong>index는 그대로인데 그 자리에 들어오는 내용물이 바뀝니다.</strong> React는 <code>key=1</code>이면 같은 컴포넌트라고 판단해서 <strong>상태를 그대로 유지</strong>합니다. 그 결과 <strong>Alice에 입력한 값이 엉뚱하게 Bob 자리에 남는</strong> 버그가 생깁니다.</p>
<p>여기서 반드시 기억할 규칙이 있습니다.</p>
<blockquote>
<p><strong>같은 key = 같은 컴포넌트 인스턴스 = 상태 유지</strong>
<strong>다른 key = 새 인스턴스 = 상태 초기화</strong></p>
</blockquote>
<h2 id="그래서-어떻게-해야-하나">그래서 어떻게 해야 하나</h2>
<table>
<thead>
<tr>
<th>상황</th>
<th>key로 쓸 것</th>
</tr>
</thead>
<tbody><tr>
<td>데이터에 고유 id가 있음 (DB에서 온 데이터 등)</td>
<td><strong>그 id 사용</strong> ← 최선</td>
</tr>
<tr>
<td>고유 id가 없음 (로컬에서 만든 데이터)</td>
<td>생성 시점에 고유 id 부여 (<code>crypto.randomUUID()</code>, nanoid 등)</td>
</tr>
<tr>
<td>리스트가 <strong>절대</strong> 바뀌지 않음 (정렬·추가·삭제·필터 전혀 없음)</td>
<td>index도 기술적으로는 안전 (예외적인 경우)</td>
</tr>
</tbody></table>
<pre><code class="language-tsx">// 권장: 안정적이고 고유한 값
{products.map((product) =&gt; (
  &lt;ProductCard key={product.id} {...product} /&gt;
))}</code></pre>
<p>좋은 key가 갖춰야 할 조건은 세 가지입니다.</p>
<ol>
<li><strong>고유하다</strong> — 형제 요소들 사이에서.</li>
<li><strong>안정적이다</strong> — 매 렌더마다 바뀌지 않는다. <code>key={Math.random()}</code>은 절대 금지입니다. 매번 새 값이 나와서 모든 항목이 매 렌더마다 새로 만들어집니다.</li>
<li><strong>예측 가능하다</strong> — 같은 데이터면 항상 같은 key가 나온다.</li>
</ol>
<h2 id="이-프로젝트에서의-적용">이 프로젝트에서의 적용</h2>
<p>상품 리스트는 <strong>정렬·필터·무한스크롤로 항목이 계속 바뀌는</strong> 리스트입니다. 그래서 index를 key로 쓰면 정렬할 때 상태 버그가 날 위험이 있고, <strong><code>product.id</code>를 key로</strong> 쓰는 게 정답입니다.</p>
<pre><code class="language-tsx">&lt;ProductCard key={`product-card-${product.id}`} {...product} /&gt;</code></pre>
<h2 id="요약">요약</h2>
<ul>
<li><strong>key의 역할</strong>: 리스트에서 &quot;이전 렌더의 어떤 요소가 이번 요소와 같은지&quot;를 React가 식별하는 값. Reconciliation에서 최소한의 DOM 변경을 계산하는 근거가 된다.</li>
<li><strong>index anti-pattern</strong>: 리스트가 바뀌면 index와 내용의 매칭이 어긋나서 ① 불필요한 DOM 연산(성능 저하) ② 상태가 엉뚱한 항목에 남는 버그가 생긴다.</li>
<li><strong>정답</strong>: 데이터의 안정적이고 고유한 id를 key로 쓴다. (같은 key면 상태 유지, 다른 key면 상태 초기화)</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[목록 무한스크롤과 race condition, StrictMode, 그리고 rootMargin]]></title>
            <link>https://velog.io/@park_log/%EB%AA%A9%EB%A1%9D-%EB%AC%B4%ED%95%9C%EC%8A%A4%ED%81%AC%EB%A1%A4%EA%B3%BC-race-condition-StrictMode-%EA%B7%B8%EB%A6%AC%EA%B3%A0-rootMargin</link>
            <guid>https://velog.io/@park_log/%EB%AA%A9%EB%A1%9D-%EB%AC%B4%ED%95%9C%EC%8A%A4%ED%81%AC%EB%A1%A4%EA%B3%BC-race-condition-StrictMode-%EA%B7%B8%EB%A6%AC%EA%B3%A0-rootMargin</guid>
            <pubDate>Fri, 10 Jul 2026 00:19:51 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>React로 무한스크롤을 직접 구현하다가, 버그 하나를 고쳤더니 다음 버그가 드러나고 그걸 고쳤더니 또 다음 문제가 튀어나온 3연속 디버깅 기록.</p>
</blockquote>
<h2 id="들어가기-전-개념-정리">들어가기 전 개념 정리</h2>
<p>이 글에 자주 나오는 세 용어를 짧게 정리하고 시작한다.</p>
<h4 id="race-condition-경쟁-상태">race condition (경쟁 상태)</h4>
<p>두 개 이상의 비동기 작업이 동시에 진행될 때, &quot;누가 먼저 끝나느냐&quot;에 따라 결과가 달라지는 상황이다. 실행 순서가 보장되지 않기 때문에, 먼저 보낸 요청이 나중에 도착하거나 그 반대가 되면 의도치 않은 결과가 나온다. 이 글에서는 이전 카테고리 요청의 늦은 응답이 새 카테고리 목록에 끼어든 게 바로 race condition이다.</p>
<h4 id="strictmode">StrictMode</h4>
<p>React가 개발 모드에서만 켜는 검사 도구다. 컴포넌트를 일부러 mount → unmount → remount로 한 번 더 껐다 켜서, effect의 cleanup이 제대로 작성됐는지 미리 드러내 준다. 프로덕션 빌드에서는 동작하지 않는다. 즉 &quot;cleanup을 안 짜두면 여기서 터진다&quot;고 미리 경고해 주는 장치다.</p>
<h4 id="rootmargin">rootMargin</h4>
<p>IntersectionObserver의 옵션으로, 요소가 &quot;화면에 보이는지&quot;를 판단하는 경계선을 넓히거나 좁힌다. 예를 들어 &quot;200px&quot;을 주면 뷰포트 실제 경계보다 200px 바깥까지를 &quot;보이는 영역&quot;으로 친다. 무한스크롤에서는 바닥에 완전히 닿기 전에 미리 다음 페이지를 불러오는 용도로 쓴다.</p>
<hr>
<h2 id="시작-상태를-store로-일원화했다">시작: 상태를 store로 일원화했다</h2>
<p>쇼핑몰 프로젝트에서 상품 목록 페이지를 만들고 있었다. 커서 기반 페이지네이션 + IntersectionObserver로 무한스크롤을 직접 구현했고, 상세 페이지에 갔다가 뒤로 돌아오면 목록과 스크롤 위치가 복원되는 UX도 넣었다.</p>
<p>처음 구조는 이랬다. 목록 데이터(items, cursor, hasNext, totalCount)는 훅의 로컬 <code>useState</code>에 살고, 언마운트될 때 zustand store에 &quot;스냅샷&quot;으로 저장했다가, 다시 마운트되면 그 스냅샷으로 초기값을 복원하는 방식.</p>
<p>그런데 이 구조엔 문제가 있었다. cleanup 함수는 클로저라서 언마운트 시점의 &quot;최신&quot; 값을 못 본다. 그래서 최신값을 저장하려고 <strong>미러 ref 4개 + 동기화 effect 4개</strong>를 두고 있었다. 같은 데이터가 로컬 state, 미러 ref, store 스냅샷 — 세 곳에 사는 삼중 구조였다. 리뷰에서도 &quot;훅이 책임이 너무 많다&quot;는 지적을 받았다.</p>
<p>그래서 리팩토링했다. <strong>목록 데이터의 단일 출처를 store로.</strong> 로컬 state와 미러 ref를 전부 걷어내니 훅이 절반으로 줄었고, 뒤로가기 복원도 &quot;store에 그냥 남아있으니까&quot; 공짜로 됐다. 코드는 확실히 좋아졌다.</p>
<p>그리고 여기서부터 버그가 시작됐다.</p>
<hr>
<h2 id="사건-1-다른-카테고리를-눌렀는데-이전-상품이-남아있다">사건 1. 다른 카테고리를 눌렀는데 이전 상품이 남아있다</h2>
<p>재현 경로는 이랬다.</p>
<ol>
<li>식품 카테고리 + 높은가격순으로 스크롤을 쭉 내린다 (다음 페이지 요청이 날아가는 중)</li>
<li>상품 상세로 들어갔다가 뒤로가기</li>
<li>다른 카테고리 클릭</li>
<li><strong>이전 식품 상품 5개 정도가 새 카테고리 목록에 섞여서 남아있다</strong></li>
</ol>
<h3 id="원인-abort는-인스턴스에-쓰기는-전역에">원인: abort는 인스턴스에, 쓰기는 전역에</h3>
<p>원인을 추적해보니 한 문장으로 요약됐다.</p>
<blockquote>
<p><strong>요청을 취소하는 장치(AbortController)는 훅 인스턴스에 묶여 있는데, 응답을 쓰는 곳(store의 appendPage)은 전역이다.</strong></p>
</blockquote>
<p>타임라인을 펼치면 이렇다.</p>
<pre><code>[식품 마지막 페이지 요청 발사] → 상세 이동(언마운트) → 뒤로가기(리마운트)
→ 카테고리 변경 → resetList() 로 목록 비움 → 새 카테고리 요청 발사
→ 그런데 아까 그 식품 요청이 이제야 도착 → appendPage(식품 5개)  ← 새치기
→ 새 카테고리 응답 도착 → appendPage(20개)  ← 식품 뒤에 붙음</code></pre><p>흥미로운 건, <strong>리팩토링 전에는 이 race가 존재했는데도 문제가 안 됐다</strong>는 점이다. 목록이 로컬 <code>useState</code>였을 때는 언마운트된 컴포넌트에 setState를 해봤자 React가 조용히 무시했다. 늦게 도착한 응답이 있어도 그냥 버려졌던 것이다.</p>
<p>그런데 쓰기 대상이 전역 store가 되자, 언마운트 후에도 쓰기가 <strong>실제로 반영</strong>된다. 늦은 응답을 React가 막아주던 암묵적 방어가 사라진 것이다. 게다가 뒤로가기로 리마운트되면 <code>abortControllerRef</code>는 새 인스턴스의 ref라서, 구 인스턴스에서 발사된 요청에는 손이 닿지 않는다. 그 요청은 클로저로 전역 store의 <code>appendPage</code>를 붙잡은 채 살아있다가, resolve되는 순간 목록에 끼어든다.</p>
<p>&quot;왜 하필 5개만 남았나&quot;도 설명이 된다. 커서 페이지네이션에서 마지막 꼬리 페이지는 limit(20)보다 작을 수 있다. 스크롤을 끝까지 내렸을 때 날아가던 요청이 바로 그 ~5개짜리 페이지였던 것이다.</p>
<h3 id="해결-낡은-응답은-버린다">해결: 낡은 응답은 버린다</h3>
<p>fetch를 시작할 때 만든 AbortController를 지역 변수로 잡아두고, 응답을 store에 반영하기 직전에 확인한다.</p>
<pre><code class="language-ts">const controller = new AbortController();
abortControllerRef.current = controller;

const response = await fetchProductList({ ..., signal: controller.signal });

// 응답이 도착한 사이에 abort된 요청이면 (필터 변경, 언마운트 등)
// 낡은 응답이므로 반영하지 않고 버린다
if (controller.signal.aborted) return;
appendPage(response.data);</code></pre>
<p>요청마다 새로 만든 controller가 클로저에 잡혀 있으니, &quot;이 응답이 아직 유효한 요청의 것인가&quot;를 응답 시점에 판단할 수 있다. 시퀀스 토큰 같은 더 큰 장치도 고려했지만, 이 가드 하나로 충분했다.</p>
<p>여기서 얻은 교훈: <strong>좋은 리팩토링이 새 버그를 만들 수 있다.</strong> 정확히는, 프레임워크가 암묵적으로 해주던 방어를 걷어내면 그 방어를 명시적으로 다시 세워야 한다.</p>
<hr>
<h2 id="사건-2-새로고침하면-스켈레톤이-영원히-돈다">사건 2. 새로고침하면 스켈레톤이 영원히 돈다</h2>
<p>사건 1을 고치면서 &quot;언마운트 시 진행 중 요청을 abort하는 cleanup&quot;도 함께 넣었다. 방어를 한 겹 더 두자는 생각이었다. 그랬더니 새 문제가 생겼다.</p>
<p><strong>새로고침하거나 주소를 직접 쳐서 들어가면 초기 요청이 취소되고, 스켈레톤이 영원히 멈춰 있다.</strong></p>
<h3 id="원인-strictmode-×-로딩-가드의-합작">원인: StrictMode × 로딩 가드의 합작</h3>
<p>React의 StrictMode는 개발 모드에서 컴포넌트를 <strong>일부러 mount → unmount → remount</strong> 시킨다. &quot;cleanup이 제대로 작성됐는지&quot; 검증하기 위한 의도된 동작이다. 문제는 여기에 우리의 로딩 가드(<code>isLoadingRef</code>)가 얽히면서 생겼다.</p>
<pre><code>1. 마운트 #1: fetchPage(null) 실행 → isLoadingRef = true, 요청 시작
2. StrictMode 언마운트: cleanup이 abort()
   — 하지만 abort의 rejection 처리는 마이크로태스크라 아직 실행 안 됨
   → finally가 못 돌아서 isLoadingRef는 여전히 true
3. 재마운트: fetchPage(null) 재호출
   → if (isLoadingRef.current) return 에 걸려 즉시 리턴
   (React의 cleanup → 재실행은 동기 블록. 그 사이에 마이크로태스크가 낄 틈이 없다)
4. 그제야 요청 1의 finally가 isLoadingRef = false로 되돌리지만,
   이미 재요청해줄 주체가 없다</code></pre><p>결과: 첫 fetch는 취소됐고, 재마운트의 fetch는 가드에 삼켜졌고, 아무도 다시 요청하지 않는다. 스켈레톤만 무한히 돈다.</p>
<p>처음엔 &quot;StrictMode의 가짜 언마운트와 진짜 이탈을 구분하면 되지 않나?&quot; 생각했는데, 이건 안티패턴이다. React가 권장하는 방향은 반대다 — <strong>cleanup에서 몇 번을 취소당해도 다시 시작할 수 있게</strong> 만드는 것.</p>
<h3 id="해결-요청의-소유권을-effect에-둔다">해결: 요청의 소유권을 effect에 둔다</h3>
<p>fetch를 시작한 effect가 자기 cleanup에서 abort와 <strong>가드 리셋까지</strong> 책임진다.</p>
<pre><code class="language-ts">useEffect(() =&gt; {
  if (!isFetched) fetchPage(null);
  return () =&gt; {
    abortControllerRef.current?.abort();
    isLoadingRef.current = false; // abort와 가드 해제를 동기적으로 함께
  };
}, []);</code></pre>
<p>이러면 mount → unmount → remount가 몇 번 반복돼도, 재마운트 시점의 가드는 항상 풀려 있어서 재요청이 성립한다.</p>
<p>하나 더. 가드를 cleanup에서 풀어버리면 반대 방향의 구멍이 생긴다 — 취소당한 옛 요청의 finally가 <strong>뒤늦게 돌면서 진행 중인 새 요청의 가드를 풀어버릴 수 있다.</strong> 그래서 finally에 소유권 체크를 넣었다.</p>
<pre><code class="language-ts">finally {
  // 내가 아직 최신 요청일 때만 가드를 해제한다
  if (abortControllerRef.current === abortController) {
    isLoadingRef.current = false;
    setIsLoading(false);
  }
}</code></pre>
<p>교훈: <strong>과한 방어가 새 버그를 만든다.</strong> 그리고 비동기 가드를 다룰 때는 항상 &quot;지금 이 상태의 소유자가 누구인가&quot;를 물어야 한다.</p>
<hr>
<h2 id="사건-3-스크롤이-한-페이지만-로드하고-멈춘다">사건 3. 스크롤이 한 페이지만 로드하고 멈춘다</h2>
<p>같은 시기에 또 하나. 바닥까지 스크롤하면 다음 페이지가 한 번 로드되고 끝이다. 더 내려갈 곳도 없는데 다음 페이지가 안 온다. (위로 스크롤했다가 다시 내리면 로드된다 — 이게 결정적 힌트였다.)</p>
<h3 id="원인-버그인-줄-알았던-것이-기능이었다">원인: 버그인 줄 알았던 것이 기능이었다</h3>
<p>이 이야기의 시작은 사실 더 전으로 거슬러 간다. 원래 코드에는 &quot;중복 호출 버그&quot;가 있었다.</p>
<pre><code>loadMore의 deps: [cursor, hasNext, fetchPage]
→ cursor가 바뀔 때마다 loadMore가 새 참조로 재생성
→ 옵저버 effect(deps: [loadMore])가 disconnect → observe 반복
→ observe() 직후엔 항상 초기 콜백이 1회 발화
→ sentinel이 이미 화면에 보이면 즉시 실행 → 중복 fetch</code></pre><p>그래서 ref 패턴으로 고쳤다. 콜백을 ref에 미러링하고 옵저버는 마운트 시 1회만 등록. 재등록이 사라지니 중복 호출도 사라졌다. 여기까진 좋았다.</p>
<p>그런데 IntersectionObserver는 교차 상태가 <strong>&#39;변할 때&#39;만</strong> 발화한다. 스켈레톤 20개가 실제 상품 20개로 교체되면 문서 높이가 거의 그대로라, 바닥에 있던 sentinel은 <strong>계속 보이는 상태 그대로</strong>다. 교차에 &#39;변화&#39;가 없으니 콜백은 두 번 다시 불리지 않는다.</p>
<p>그럼 예전엔 어떻게 연쇄 로딩이 됐을까? <strong>그 &quot;중복 호출 버그&quot;가 트리거였다.</strong> 페이지가 로드되면 cursor가 바뀌고 → loadMore가 재생성되고 → 옵저버가 재등록되고 → 재등록 직후의 초기 콜백이 발화해서 → 다음 페이지를 불렀던 것이다. 우리가 버그라고 부르며 제거한 재등록이, 사실은 연쇄 로딩의 동력을 겸하고 있었다.</p>
<h3 id="해결-rootmargin">해결: rootMargin</h3>
<p>재등록을 되살리거나 강제 재발화 트릭을 넣는 대신, 옵저버 옵션 하나로 풀었다.</p>
<pre><code class="language-ts">new IntersectionObserver(callback, { rootMargin: &quot;200px&quot; });</code></pre>
<p>감지 영역이 뷰포트 아래 200px까지 확장된다. 그러면 사이클이 자연스럽게 돌아간다 — 페이지가 append되면 sentinel이 한 페이지 높이만큼 아래로 밀려 감지 영역을 <strong>벗어나고</strong>(교차 변화 발생), 스크롤이 다시 접근하면 <strong>재발화</strong>한다. 덤으로 바닥에 닿기 200px 전에 미리 로드되니 UX도 좋아졌다.</p>
<p>교훈: <strong>버그를 고치기 전에, 그 동작이 무엇에 기여하고 있었는지 물어야 한다.</strong> 그리고 옵저버는 &quot;상태&quot;가 아니라 &quot;변화&quot;에 반응한다는 걸 정확히 이해해야 무한스크롤을 제대로 만들 수 있다.</p>
<hr>
<h2 id="정리-최종-방어-체계">정리: 최종 방어 체계</h2>
<p>세 사건을 지나고 나니, 무한스크롤 하나에 이런 방어선들이 서 있었다.</p>
<table>
<thead>
<tr>
<th>계층</th>
<th>장치</th>
<th>막는 것</th>
</tr>
</thead>
<tbody><tr>
<td>옵저버</td>
<td>ref 패턴 + 1회 등록</td>
<td>재등록 즉시 발화로 인한 중복 호출</td>
</tr>
<tr>
<td>옵저버</td>
<td>rootMargin</td>
<td>sentinel 정체로 인한 연쇄 로딩 중단</td>
</tr>
<tr>
<td>fetch</td>
<td>isLoadingRef 가드</td>
<td>동시 중복 요청</td>
</tr>
<tr>
<td>fetch</td>
<td>finally 소유권 체크</td>
<td>낡은 finally가 새 요청 가드를 해제</td>
</tr>
<tr>
<td>응답</td>
<td>signal.aborted 가드</td>
<td>낡은 응답의 store 오염 (race)</td>
</tr>
<tr>
<td>effect</td>
<td>cleanup에서 abort + 가드 리셋</td>
<td>StrictMode 재마운트에서 초기 요청 실종</td>
</tr>
</tbody></table>
<h2 id="에필로그-그리고-이-모든-것을-tanstack-query가-한다">에필로그: 그리고 이 모든 것을 TanStack Query가 한다</h2>
<p>위 표의 방어선 대부분을 TanStack Query의 <code>useInfiniteQuery</code>는 기본으로 제공한다.</p>
<ul>
<li>중복 요청과 로딩 상태 → 내부에서 관리</li>
<li>낡은 응답(race) → queryKey(카테고리+정렬)가 다르면 자동 폐기</li>
<li>StrictMode 대응 → 라이브러리가 처리</li>
<li>isFetched / isLoading 구분 → 상태 플래그로 제공</li>
</ul>
<p>&quot;그럼 처음부터 라이브러리를 쓰지 그랬냐&quot;고 할 수 있다. 하지만 직접 구현하며 각 방어선이 <strong>왜 필요한지</strong>를 몸으로 겪었기 때문에, 이제 RQ의 기능 하나하나가 무엇을 대신해주는지 정확히 안다. queryKey가 race를 막는다는 문장이 문서 속 한 줄이 아니라, 카테고리 잔상 버그의 기억으로 읽힌다.</p>
<p>다음 글은 이 코드를 TanStack Query로 마이그레이션하는 이야기가 될 것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[React 컴포넌트 설계 개념 정리]]></title>
            <link>https://velog.io/@park_log/React-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EC%84%A4%EA%B3%84-%EA%B0%9C%EB%85%90-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@park_log/React-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EC%84%A4%EA%B3%84-%EA%B0%9C%EB%85%90-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Thu, 09 Jul 2026 07:44:06 GMT</pubDate>
            <description><![CDATA[<h1 id="코드-리뷰에서-배운-react-컴포넌트-설계-3가지">코드 리뷰에서 배운 React 컴포넌트 설계 3가지</h1>
<p><code>ProductCard</code> 컴포넌트를 리뷰받다가 이런 코멘트를 받았습니다.</p>
<blockquote>
<p>&quot;카드 안에서 <code>useNavigate</code>를 직접 쓰지 말고, <code>onNavigate</code> prop으로 부모에게 넘기는 게 어떨까요?&quot;</p>
</blockquote>
<p>처음엔 &quot;그냥 카드 안에서 이동시키면 되는 거 아닌가?&quot; 싶었는데, 이 안에는 <strong>관심사 분리 / props 네이밍 / 함수 분리</strong>라는 세 가지 개념이 다 들어 있었습니다. 이 글은 그 세 가지를 정리한 기록입니다.</p>
<hr>
<h2 id="1-관심사-분리-separation-of-concerns">1. 관심사 분리 (Separation of Concerns)</h2>
<p>한 줄로 요약하면 이렇습니다. <strong>컴포넌트는 자기 일에만 집중하고, 다른 관심사는 부모에게 넘긴다.</strong></p>
<h3 id="무엇이-문제였나">무엇이 문제였나</h3>
<p>원래 <code>ProductCard</code>는 내부에서 <code>useNavigate</code>를 직접 호출하고 있었습니다.</p>
<pre><code class="language-tsx">// before — 카드가 &quot;이동&quot;까지 스스로 결정
export default function ProductCard({ id, ... }) {
  const navigate = useNavigate();
  const handleClick = (productId) =&gt; navigate(toProductDetail(productId));
  return &lt;div onClick={() =&gt; handleClick(id)}&gt;...&lt;/div&gt;;
}</code></pre>
<p>카드가 해야 할 일은 <strong>상품 정보를 보여주는 것</strong>입니다. 그런데 <code>useNavigate</code>가 들어오면서 &quot;라우팅&quot;이라는 전혀 다른 관심사까지 카드가 떠안게 됐습니다.</p>
<h3 id="react-router-환경에서만-동작한다는-말의-의미">&quot;react-router 환경에서만 동작한다&quot;는 말의 의미</h3>
<p><code>useNavigate</code>는 평범한 함수가 아니라, <strong>react-router가 깔아놓은 판(Context) 위에서만 작동하는 훅</strong>입니다. <code>&lt;BrowserRouter&gt;</code> 안쪽에서만 살아있고, 그 밖에서 부르면 에러가 납니다.</p>
<p>즉 <code>useNavigate</code>를 카드 안에 넣는 순간, 카드는 <strong>&quot;라우터가 있어야만 존재할 수 있는 컴포넌트&quot;</strong>가 됩니다. 카드의 본질(이미지·이름·가격을 보여주는 것)은 라우터와 아무 상관이 없는데도 말이죠.</p>
<h3 id="해결--이동-방식을-부모가-결정하게">해결 — 이동 방식을 부모가 결정하게</h3>
<pre><code class="language-tsx">// after — 카드는 &quot;클릭됐다&quot;만 알리고, 뭘 할지는 부모가 결정
type ProductCardProps = Pick&lt;Product, &#39;id&#39; | &#39;imageUrl&#39; | ...&gt; &amp; {
  onNavigate: (id: string) =&gt; void;
};

export default function ProductCard({ id, ..., onNavigate }) {
  return &lt;div onClick={() =&gt; onNavigate(id)}&gt;...&lt;/div&gt;;
}</code></pre>
<pre><code class="language-tsx">// 부모 (ProductListPage)
&lt;ProductCard {...product} onNavigate={(id) =&gt; navigate(toProductDetail(id))} /&gt;</code></pre>
<p>카드는 이제 &quot;이동 방식&quot;을 모릅니다. 그냥 클릭됐다는 사실만 부모에게 알리고, 실제로 어디로 갈지는 부모가 정합니다.</p>
<h3 id="왜-좋은가--구체적인-상황">왜 좋은가 — 구체적인 상황</h3>
<p>같은 카드를 <strong>검색 결과 페이지</strong>에서도 쓴다고 해봅시다. 그런데 거기선 클릭했을 때 상세 페이지로 이동하는 게 아니라 <strong>미리보기 모달</strong>을 띄우고 싶습니다.</p>
<ul>
<li><code>useNavigate</code>가 카드 안에 박혀 있으면 → <strong>바꿀 수 없습니다.</strong> 카드가 이동 방식을 자기가 정해버렸으니까요.</li>
<li><code>onNavigate</code>를 밖에서 받으면 → 리스트 페이지는 <code>navigate(상세)</code>를, 검색 페이지는 <code>모달 열기</code>를 넘기면 됩니다. <strong>같은 카드, 다른 동작.</strong></li>
</ul>
<h3 id="비유로-이해하기">비유로 이해하기</h3>
<p>엘리베이터 버튼은 &quot;눌렸다&quot;는 신호만 보냅니다. 몇 층으로 갈지는 버튼이 정하지 않죠. 만약 버튼이 목적지 층까지 정해버리면, 그 버튼은 다른 건물에 갖다 쓸 수 없습니다.</p>
<blockquote>
<p>카드 = 버튼, 이동 결정 = 엘리베이터 시스템(부모)</p>
</blockquote>
<h3 id="기타">기타</h3>
<p>사실 지금 프로젝트에선 카드가 항상 라우터 안에서만 쓰이고 테스트도 없어서, 당장 얻는 실익이 크지는 않을 수 있습니다.
지금은 <strong>원칙을 이해하는 게 목적</strong>이고, 나중에 테스트를 작성하거나 <strong>컴포넌트를 재사용하거나 Storybook으로 문서화</strong>할 때 이 차이를 체감하게 될 거라고 봅니다.</p>
<hr>
<h2 id="2-props-네이밍--onclick-vs-onnavigate">2. props 네이밍 — onClick vs onNavigate</h2>
<p>핵심 원칙은 이렇습니다. <strong>prop 이름은 &quot;무슨 일이 일어났나(이벤트)&quot;가 아니라 &quot;무엇을 원하나(의도)&quot;로 짓는다.</strong></p>
<table>
<thead>
<tr>
<th>이름</th>
<th>성격</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td><code>onClick</code></td>
<td>저수준 (이벤트)</td>
<td>&quot;클릭이라는 물리적 사건이 발생함&quot; — 그 클릭이 뭘 의미하는지는 안 담김</td>
</tr>
<tr>
<td><code>onNavigate</code></td>
<td>고수준 (의도)</td>
<td>&quot;이동하고 싶다&quot; — 이름만 봐도 의도가 드러남</td>
</tr>
</tbody></table>
<p>부모 입장에서 비교하면 차이가 분명합니다.</p>
<pre><code class="language-tsx">// onClick — 이게 무슨 클릭인지 매번 확인해야 함
&lt;ProductCard onClick={...} /&gt;

// onNavigate — 이름만 봐도 의도가 명확
&lt;ProductCard onNavigate={(id) =&gt; navigate(...)} /&gt;</code></pre>
<p>특히 카드 안에 클릭 지점이 여러 개일 때 이 차이가 커집니다. 카드에는 카드 전체(상세 이동), 담기 버튼, 수량 +/- 버튼처럼 클릭할 곳이 여럿 있습니다. 이걸 전부 <code>onClick</code>으로 받으면 <strong>어떤 클릭인지 구분할 수 없습니다.</strong> 그래서 의도별로 이름을 붙여야 합니다.</p>
<pre><code>onNavigate, onAddToCart, onChangeQuantity ...</code></pre><blockquote>
<p>prop 이름이 곧 문서 역할을 합니다.</p>
</blockquote>
<hr>
<h2 id="3-함수-분리--언제-하고-언제-안-하나">3. 함수 분리 — 언제 하고, 언제 안 하나</h2>
<p>&quot;함수는 웬만하면 분리하라&quot;는 말을 많이 듣지만, 진짜 기준은 <strong>길이 · 재사용 · 명확성</strong> 세 가지입니다.</p>
<table>
<thead>
<tr>
<th>분리하는 게 좋을 때</th>
<th>인라인이 나을 때</th>
</tr>
</thead>
<tbody><tr>
<td>로직이 여러 줄일 때</td>
<td>한 줄짜리일 때</td>
</tr>
<tr>
<td>여러 곳에서 재사용할 때</td>
<td>한 곳에서만 쓸 때</td>
</tr>
<tr>
<td>이름을 붙이면 의미가 명확해질 때</td>
<td>이름 없이도 뭘 하는지 뻔할 때</td>
</tr>
</tbody></table>
<h3 id="이번-케이스는-인라인이-맞다">이번 케이스는 인라인이 맞다</h3>
<pre><code class="language-tsx">&lt;ProductCard onNavigate={(id) =&gt; navigate(toProductDetail(id))} /&gt;</code></pre>
<ul>
<li>한 줄 ✅</li>
<li>한 곳에서만 사용 ✅</li>
<li>뭘 하는지 뻔함 ✅</li>
</ul>
<p>이럴 땐 인라인이 오히려 읽기 편합니다. 억지로 빼면 이렇게 됩니다.</p>
<pre><code class="language-tsx">const handleNavigate = (id: string) =&gt; navigate(toProductDetail(id));
// ...한참 아래...
&lt;ProductCard onNavigate={handleNavigate} /&gt;</code></pre>
<p>선언부와 사용부가 멀어져서 코드를 읽을 때 눈이 위아래로 왔다 갔다 하게 됩니다. 오히려 불편하죠.</p>
<h3 id="정리">정리</h3>
<p>&quot;무조건 분리&quot;가 아니라 <strong>&quot;분리가 가독성이나 재사용에 도움이 될 때 분리&quot;</strong>입니다. 한 줄이고, 한 곳에서만 쓰고, 뭘 하는지 자명한 함수라면 인라인이 원칙에 더 맞습니다.</p>
<hr>
<h2 id="마무리">마무리</h2>
<ul>
<li><strong>관심사 분리</strong>: UI 컴포넌트가 라우팅 같은 외부 시스템을 직접 알지 않게 하고, 동작은 부모가 주입한다. → 재사용성과 테스트 용이성 확보.</li>
<li><strong>props 네이밍</strong>: 이벤트명(<code>onClick</code>)이 아니라 의도명(<code>onNavigate</code>)으로. → 이름이 곧 문서가 된다.</li>
<li><strong>함수 분리</strong>: 길이 · 재사용 · 명확성이 기준. 한 줄 · 단일 사용 · 자명한 함수는 인라인.</li>
</ul>
<p>한 줄짜리 리뷰 코멘트였지만, 그 뒤에는 이만큼의 설계 원칙이 있었습니다. 앞으로 컴포넌트를 만들 때 &quot;이 컴포넌트가 원래 알아야 하는 일인가?&quot;를 먼저 물어보게 될 것 같습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[비제어(Uncontrolled) vs 반응형(Reactive/Controlled) 차이점]]></title>
            <link>https://velog.io/@park_log/%EB%B9%84%EC%A0%9C%EC%96%B4Uncontrolled-vs-%EB%B0%98%EC%9D%91%ED%98%95ReactiveControlled-%EC%B0%A8%EC%9D%B4%EC%A0%90</link>
            <guid>https://velog.io/@park_log/%EB%B9%84%EC%A0%9C%EC%96%B4Uncontrolled-vs-%EB%B0%98%EC%9D%91%ED%98%95ReactiveControlled-%EC%B0%A8%EC%9D%B4%EC%A0%90</guid>
            <pubDate>Sat, 11 Apr 2026 01:01:33 GMT</pubDate>
            <description><![CDATA[<h1 id="비제어-uncontrolled">비제어 (Uncontrolled)</h1>
<blockquote>
<p>리액트의 state가 아닌, <strong>브라우저 DOM 자체가 입력값을 들고 있다.</strong> 
<strong>데이터의 주인은 브라우저(DOM)</strong></p>
</blockquote>
<ul>
<li><p>동작: 사용자가 글자를 칠 때마다 리액트한테 알려주지 않음. 브라우저 DOM 자체( HTML &lt; input &gt; )이 조용히 자기 값을 업데이트함. 리액트는 ref를 통해 나중에 값을 가져옴</p>
</li>
<li><p>장점: 키 입력마다 컴포넌트가 다시 그려지지 않아서 성능이 매우 가볍다.
(리렌더링 발생하지 않음)</p>
</li>
<li><p>RHF의 방식: RHF는 기본적으로 이 Uncontrolled 방식을 사용. register가 이 입력창을 감시만 하다가, 나중에 제출할 때 한꺼번에 값을 가져옴.</p>
</li>
</ul>
<br/>
<br/>
<br/>

<h1 id="반응형제어-controlled">반응형/제어 (Controlled)</h1>
<blockquote>
<p>입력값 하나하나가 리액트의 state와 동기화
<strong>데이터의 주인은 리액트(State)</strong></p>
</blockquote>
<ul>
<li><p>동작: 사용자가 &#39;A&#39; 하나만 쳐도 onChange가 실행되고, state가 바뀌며, 그 결과로 컴포넌트 전체가 다시 그려짐.
(리렌더링 발생)</p>
</li>
<li><p>장점: 입력값에 따라 실시간으로 화면을 바꿀 때(예: 글자 수 카운팅, 실시간 검색) 직관적이다.</p>
</li>
<li><p>단점: 입력할 때마다 리액트가 일을 해야 하므로, 폼이 복잡해지면 성능 부하가 생김.</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[React Hook Form - useForm]]></title>
            <link>https://velog.io/@park_log/React-Hook-Form-useForm</link>
            <guid>https://velog.io/@park_log/React-Hook-Form-useForm</guid>
            <pubDate>Fri, 10 Apr 2026 00:27:07 GMT</pubDate>
            <description><![CDATA[<h1 id="useform-일반-props">useForm 일반 props</h1>
<p><strong>mode</strong><br>폼 제출 전 사용자의 액션에 따라 유효성 검사 타이밍을 결정.</p>
<p><strong>reValidateMode</strong><br>폼 제출 후 오류값 재검증 시 유효성 검사를 트리거할 사용자의 액션.</p>
<p><strong>defaultValues</strong><br>기본값(초기 렌더링 시 캐싱됨)
이후에 비동기로 데이터 조작시에는 values나 reset 함수 사용</p>
<p><strong>values</strong><br>Form 값을 업데이트하기 위한 반응형(Controlled) 값
외부 상태(예: API 데이터)가 변경될 때 폼 값을 자동으로 동기화하고 싶을 때 사용</p>
<p><strong>errors</strong><br>유효성 검사가 실패한 필드의 오류 정보(type, message 등)를 담고 있는 객체.
중요 : 오류 객체 참조 - stable를 유지하여 무한 재렌더링 방지</p>
<p><strong>resetOptions</strong><br>values나 defaultValues가 업데이트되어 폼 값이 새롭게 바뀔 때, 기존 폼 상태(에러, 방문 기록 등)의 유지 여부를 설정하는 옵션.</p>
<p><strong>criteriaMode</strong><br>모든 검증 오류를 표시하거나 한 번에 하나씩 표시</p>
<p><strong>shouldFocusError</strong><br>제출 시 에러가 난 첫 필드로 포커스 자동 이동 활성화하거나 비활성화</p>
<p><strong>delayError</strong><br>오류가 나타나는 시점을 밀리초(ms) 단위로 지연시킴. 
사용자가 타이핑하는 동안 에러 메시지가 너무 빨리 깜빡이는 걸 방지.</p>
<p><strong>validate</strong><br>사용자 정의 유효성 검사 함수. 
(기본 제공되는 required, pattern 외에 직접 로직을 짜서 검증할 때 사용)</p>
<p><strong>shouldUseNativeValidation</strong><br>브라우저 내장 form constraint API 사용</p>
<p><strong>shouldUnregister</strong><br>컴포넌트가 화면에서 사라질(Unmount) 때 해당 필드 값도 폼 데이터에서 삭제할지 결정.
기본값 = false라 사라져도 값은 유지됨.</p>
<p><strong>progressive</strong><br>Form component를 사용할 때 기본 양식 제출 점진적인 향상(progressive enhancemen) 활성화. </p>
<p><strong>disabled</strong><br>전체 폼 필드를 한 번에 비활성화하며, 이때 해당 필드들은 유효성 검사에서 제외됨.</p>
<br/>
<br/>


<h1 id="useform-스키마-검증-props">useForm 스키마 검증 Props</h1>
<p><strong>resolver</strong><br>스키마 기반의 외부 검증 라이브러리(Zod, Yup, Joi 등)를 React Hook Form과 연결하는 함수</p>
<p>context<br>스키마 검증 시 외부 데이터를 참조할 수 있게 넘겨주는 객체.
폼 내부 데이터가 아닌, <strong>외부의 동적인 상태(예: 전역 설정값, 현재 세션 정보 등)</strong>를 스키마 내부 로직에서 조건부로 활용하고 싶을 때 사용함.</p>
<br/>
<br/>


<h1 id="moderevalidatemode-비교">mode/reValidateMode 비교</h1>
<h3 id="mode-역할">mode 역할</h3>
<blockquote>
<p>onChange | onBlur | onSubmit | onTouched | all <strong>(기본값 : onSubmit)</strong></p>
</blockquote>
<p>mode의 경우 폼 제출 전 사용자의 액션에 따라 유효성 검사 타이밍을 결정한다.
기본값은 onSubmit으로, 불필요한 리렌더링이 가장 적어 성능상 가장 유리하다.</p>
<br/>

<h3 id="revalidatemode의-역할">reValidateMode의 역할</h3>
<blockquote>
<p>onChange | onBlur | onSubmit  <strong>(기본값: &#39;onChange&#39;)</strong></p>
</blockquote>
<p>사용자가 form을 submit한 후 오류가 있는 입력이 재검증될 때(onSubmit, handleSubmit 실행 시) 검증 전략 결정.
기본값은 onChange로 입력값이 변경될 때 마다 재검증 트리된다.
React Native의 경우 Custom register나 Controller 사용을 사용해야한다.</p>
<br/>

<h3 id="각-옵션에-따른-결과">각 옵션에 따른 결과</h3>
<h4 id="onsubmit">onSubmit</h4>
<p>첫 onSubmit이벤트에서 트리거됨. 
onSubmit 이후에는 각 필드에 onChange 이벤트 리스너가 활성화되어, 사용자가 값을 수정할 때마다 즉각적으로 재검증을 수행.</p>
<h4 id="onblur">onBlur</h4>
<p>사용자가 입력을 마치고 포커스를 옮길 때 검증하므로, onChange보다 성능 효율적이면서 적절한 피드백을 제공.</p>
<h4 id="onchange">onChange</h4>
<p>필드의 값이 변경될 때마다 이벤트 트리거. 
모든 키 입력마다 유효성 검사 로직이 실행되고 상태가 업데이트되어 리렌더링이 발생함.
따라서 <strong>성능에 영향 많이 받게되므로</strong> 사용에 주의 필요        </p>
<h4 id="ontouched">onTouched</h4>
<p>첫 blur 이벤트에서 트리거. 이후 모든 onChange이벤트 발생할 때마다 트리거</p>
<ul>
<li>UI 라이브러리(MUI, AntD 등)의 커스텀 컴포넌트는 표준 HTML input과 이벤트 전달 방식이 다를 수 있어 Controller를 사용하게 되는데, ** Controller의 render 프롭에서 제공하는 field.onBlur를 수동으로 연결해 주어야 RHF가 해당 필드가 &#39;Touched&#39; 되었음을 정확히 인지**할 수 있음.</li>
</ul>
<h4 id="all">all</h4>
<p>가장 엄격한 검증 방식으로 onBlur, onChange이벤트 발생 시 모두 트리거. 실시간 피드백을 극대화함</p>
<br/>


<h3 id="옵션에-따른-성능차이--왜-onchange를-주의해야-할까">옵션에 따른 성능차이 : 왜 onChange를 주의해야 할까?</h3>
<h4 id="구독-기반-리렌더링-">구독 기반 리렌더링 :</h4>
<p>React Hook Form(RHF)은 기본적으로 비제어 방식(Uncontrolled)으로 불필요한 리렌더링을 방지하지만, mode: &#39;onChange&#39;를 설정하면 키 입력마다 다음 사이클이 작동.</p>
<p>검증 함수 실행 -&gt; 에러 객체 생성 -&gt; 상태 업데이트 </p>
<p>이때 <strong>RHF 내부의 상태가 업데이트될 때마다, 이 상태를 구독하고 있는 컴포넌트가 리액트의 리렌더링 사이클이 트리거되기 때문에 성능 부하가 발생한다</strong>. 
입력값 자체는 비제어(Uncontrolled)지만 &#39;검증 결과&#39;는 반응형(Controlled)으로 관리되기 때문. 특히 복잡한 유효성 검사 로직이 포함되어 있거나 폼의 규모가 큰 경우, 메인 스레드를 점유하게 되어 사용자가 입력을 할 때 반응이 느려지는 <strong>타이핑 지연(Input Lag) 현상이 발생</strong>할 수 있다.</p>
<h4 id="가비지-컬렉션gc-부하">가비지 컬렉션(GC) 부하:</h4>
<p>매 입력마다 새로운 errors 객체가 생성되고 참조가 변경되므로, 짧은 시간 내에 수많은 객체가 생성되고 파괴되는 과정은 브라우저의 가비지 컬렉터(GC)에 과부하를 주게 되며, 이는 특히 저사양 기기나 모바일 환경에서 성능 저하 및 버벅임의 직접적인 원인이 됨.</p>
<h4 id="추천-이용-방식">추천 이용 방식:</h4>
<p>성능을 위해서는 기본값인 onSubmit이나 onBlur를 사용하는 것이 권장된다.
실시간 피드백이 반드시 필요하다면 처음부터 mode: &#39;onChange&#39;를 쓰기보다 <strong>reValidateMode: &#39;onChange&#39; 를 활용하여</strong>, 제출했을 때 에러 발생시에만 실시간으로 수정 여부를 검증하는 것도 효율적이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[브라우저 - CORS]]></title>
            <link>https://velog.io/@park_log/%EB%B8%8C%EB%9D%BC%EC%9A%B0%EC%A0%80-CORS</link>
            <guid>https://velog.io/@park_log/%EB%B8%8C%EB%9D%BC%EC%9A%B0%EC%A0%80-CORS</guid>
            <pubDate>Wed, 18 Feb 2026 08:10:41 GMT</pubDate>
            <description><![CDATA[<h3 id="cors란">CORS란?</h3>
<p>CORS는 기본적으로 SOP(Same-Origin Policy, 동일 출처 정책) 때문에 발생합니다. 
웹 애플리케이션이 <strong>자신의 출처와 다른 곳으로 HTTP 요청을 보낼 때</strong>,
<strong>브라우저</strong>는 보안상의 이유로 이 요청이 안전한지 검사하고 허용되지 않은 경우 차단합니다.</p>
<br/>

<h3 id="cors-발생-조건">CORS 발생 조건</h3>
<ol>
<li>프로토콜(Protocol)이 다를 때: http와 https는 서로 다른 출처</li>
<li>도메인(Domain/Host)이 다를 때: example.com과 api.example.com은 서로 다른 출처</li>
<li>포트(Port)가 다를 때
위와 같은 경우 다른 출처로 인식합니다.</li>
</ol>
<br/>

<h3 id="cors-해결-방법">CORS 해결 방법</h3>
<p><strong>1. 서버가 브라우저에게 이 요청은 안전하다고 허락해 주는 것</strong>
서버 응답 헤더에 Access-Control-Allow-Origin을 추가합니다.
특정 도메인 허용: Access-Control-Allow-Origin: <a href="https://my-frontend.com">https://my-frontend.com</a>
모든 도메인 허용: Access-Control-Allow-Origin: *</p>
<p><strong>2. 프레임워크별 미들웨어 사용</strong>
직접 헤더를 넣기보다는 사용 중인 언어/프레임워크의 라이브러리를 쓰는 게 편합니다.
Node.js (Express): cors 패키지 설치 후 app.use(cors()) 사용
Spring Boot: @CrossOrigin 어노테이션 사용 혹은 WebMvcConfigurer 설정</p>
<p><strong>3. 개발 환경에서의 Proxy 설정</strong>
프론트엔드 개발 중에만 임시로 해결하고 싶다면, 클라이언트 쪽에서 프록시를 설정해 서버와 같은 출처인 것처럼 속일 수 있습니다.</p>
<blockquote>
<p><strong>React (package.json)</strong>
&quot;proxy&quot;: &quot;<a href="http://localhost:8080&quot;">http://localhost:8080&quot;</a> 추가
<strong>Webpack/Vite</strong><br>설정 파일에서 server.proxy 옵션 사용</p>
</blockquote>
<p><strong>4. 백엔드 앞단에 Reverse Proxy 배치 (Nginx)</strong>
실제 운영 환경에서는 Nginx 같은 웹 서버를 앞에 두고, 프론트와 백엔드를 같은 도메인 아래 묶어서 CORS 상황 자체를 만들지 않기도 합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[호이스팅]]></title>
            <link>https://velog.io/@park_log/%ED%98%B8%EC%9D%B4%EC%8A%A4%ED%8C%85</link>
            <guid>https://velog.io/@park_log/%ED%98%B8%EC%9D%B4%EC%8A%A4%ED%8C%85</guid>
            <pubDate>Wed, 18 Feb 2026 07:59:43 GMT</pubDate>
            <description><![CDATA[<h3 id="호이스팅이란">호이스팅이란?</h3>
<p>자바스크립트 엔진은 코드를 실행하기 전 &#39;컴파일 단계&#39;를 거치며 변수 선언을 메모리에 미리 등록합니다. 
실제 코드 위치와 상관없이 선언부만 최상단으로 끌어올려진 것처럼 보이는 현상을 말합니다.
<br/></p>
<h3 id="변수별-호이스팅-동작-방식">변수별 호이스팅 동작 방식</h3>
<p>호이스팅은 모든 변수(var, let, const)에 발생하지만, 그 결과는 확연히 다릅니다.</p>
<ul>
<li>실행 전에 변수 이름을 먼저 environmentRecord에 등록.</li>
<li>변수/함수 선언이 실행 전에 스코프 상단으로 끌어올려진 것처럼 동작</li>
<li>var: 선언 + 초기화(undefined)</li>
<li>let, const: 선언만 호이스팅 (TDZ 존재) =&gt; 할당 전 reference error
(TDZ : 실행 전으로 선언은 되었으나 초기화가 되지 않은 상태.)<br/>

</li>
</ul>
<h4 id="일시적-사각지대-temporal-dead-zone-tdz">일시적 사각지대 (Temporal Dead Zone, TDZ)</h4>
<p>= 초기화 전 접근 금지 구간
= 스코프의 시작 지점부터 초기화 시작 지점까지 변수를 참조할 수 없는 구간
<br/></p>
<h4 id="var">var</h4>
<pre><code>console.log(a) // undefined
var a = 10
----- 동작과정 -----
var a
console.log(a) // undefined
a = 10</code></pre><h4 id="let-const">let, const</h4>
<pre><code>console.log(b) // ReferenceError(TDZ 발생)
let b = 10</code></pre><h4 id="let-const함수-선언문">let, const함수 선언문</h4>
<pre><code>sayHello() // 정상 실행

function sayHello() {
  console.log(&quot;&quot;hello&quot;&quot;)
}
//→ 함수 전체가 호이스팅됨.    </code></pre><h4 id="함수-표현식">함수 표현식</h4>
<pre><code>sayHi() // TypeError

var sayHi = function () {
  console.log(&quot;&quot;hi&quot;&quot;)
}
//→ 변수는 undefined로 호이스팅
//→ 함수는 아직 할당 안 됨.</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[call by value, call by reference 란?]]></title>
            <link>https://velog.io/@park_log/call-by-value-call-by-reference-%EB%9E%80</link>
            <guid>https://velog.io/@park_log/call-by-value-call-by-reference-%EB%9E%80</guid>
            <pubDate>Fri, 06 Feb 2026 09:43:11 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>call by value, call by reference 란?</strong>
함수 호출 시 값을 전달하는 방식을 의미합니다.</p>
</blockquote>
<h3 id="call-by-value">Call by Value</h3>
<p> Call by Value는 <strong>값의 복사본이 전달</strong>되므로, <strong>함수 내부에서 값을 변경해도 원본에는 영향이 없습니다.</strong></p>
<h3 id="call-by-reference">Call by Reference</h3>
<p>Call by Reference는 <strong>변수의 참조값(메모리 위치)이 전달</strong>되어, <strong>함수 내부에서 값을 변경하면 원본에도 영향이 발생</strong>합니다.</p>
<h3 id="자바스크립트에서의-동작">자바스크립트에서의 동작</h3>
<p>자바스크립트는 이론적으로 Call by Value만 사용합니다.
함수에 전달되는 것은 항상 “값”이며, <strong>(참조타입)객체의 경우 객체를 가리키는 참조값이 값으로 복사</strong>됩니다.</p>
<p>따라서 함수 내에서 객체의 속성을 변경하면 원본이 함께 변경되기 때문에
겉보기에는 Call by Reference처럼 동작하는 것처럼 보일 수 있습니다.</p>
<br/>


<h3 id="예시">예시</h3>
<hr/>

<h4 id="1-원시-값일-때"><strong>1) 원시 값일 때</strong></h4>
<pre><code>function change(x) {
  x = 20
}

let a = 10

change(a)

console.log(a) //10</code></pre><p><strong>Call by Value</strong></p>
<p>(해설)
여기서 복사되는 건 숫자 10 자체</p>
<p>a → 10
x → 다른 10
이 경우,완전 별개.</p>
<p>따라서 a 안 바뀜.</p>
<hr/>

<h4 id="2-객체-속성-바꿀-때"><strong>2) 객체 속성 바꿀 때</strong></h4>
<pre><code>function change(obj) {
  obj.value = 20
}

let data = { value: 10 }

change(data)

console.log(data.value) // 20</code></pre><p><strong>Call by Reference</strong></p>
<p>(해설)
여기서 복사되는 건 객체가 아니라
“객체를 가리키는 참조값”
data ─┐____&gt;  같은객체</p>
<p>obj ──┘    </p>
<p>이 경우, 복사는 됐지만 같은 객체를 함께 본다.
따라서 값 바뀜.</p>
<hr/>

<h4 id="3-객체를-새로-재할당할-때"><strong>3) 객체를 새로 재할당할 때</strong></h4>
<pre><code>function change(obj) { 
    obj = { value: 20 } 
} 

let data = { value: 10 } 

change(data) 

console.log(data.value) //10</code></pre><p><strong>Call by Value</strong></p>
<p>(해설)
obj 화살표만 다른 객체로 갈아탐</p>
<p>data → 원래 객체
obj → 새 객체
이 경우, 
→ 서로 완전 분리.</p>
<p>따라서 data 안 바뀜.</p>
<br/>]]></description>
        </item>
        <item>
            <title><![CDATA[불변성이란?]]></title>
            <link>https://velog.io/@park_log/%EB%B6%88%EB%B3%80%EC%84%B1%EC%9D%B4%EB%9E%80</link>
            <guid>https://velog.io/@park_log/%EB%B6%88%EB%B3%80%EC%84%B1%EC%9D%B4%EB%9E%80</guid>
            <pubDate>Fri, 06 Feb 2026 09:36:21 GMT</pubDate>
            <description><![CDATA[<h3 id="불변성이란">불변성이란</h3>
<p>불변성이란 기존 데이터를 직접 변경하지 않고, 새로운 값을 생성하여 상태 변화를 표현하는 성질을 의미합니다.</p>
<p>자바스크립트에서 <strong>원시 타입</strong>은 한 번 생성된 값을 수정할 수 없기 때문에 자연스럽게 불변성을 가지며, 객체와 배열 같은 <strong>참조 타입</strong>은 내부 값을 변경할 수 있어 가변성을 가집니다.</p>
<br/>

<h3 id="불변성이-중요한-이유">불변성이 중요한 이유</h3>
<ul>
<li>변경 전/후 상태가 명확히 분리되어 디버깅과 로직 이해가 쉬워지기 때문에 상태변화 추적이 용이해집니다.</li>
<li>기존 데이터를 직접 수정하지 않기 때문에 예상치 못한 값 변경을 방지하여 사이드 이펙트를 줄일 수 있습니다.</li>
<li>이로 인해 안정적인 동작을 보장하고 협업과 유지보수에 유리한 구조를 만들 수 있습니다.</li>
</ul>
<br/>

<h3 id="불변성을-지키는-방법">불변성을 지키는 방법</h3>
<p>참조 타입 복사 시 재귀 기반 복사, 또는 Immer 같은 라이브러리등을 활용하여 데이터의 불변성을 유지하도록 합니다.</p>
<p>객체 : 기존 객체를 직접 수정하지 않고 새 객체를 생성하여 반환.
배열 : 동일한 새 배열을 생성.
<br/>
<strong>코드 예시</strong></p>
<pre><code>// ---------- 객체
// 잘못된 예 (가변)
user.name = &quot;&quot;Kim&quot;&quot;

// 올바른 예 (불변)
const newUser = { ...user, name: &quot;&quot;Kim&quot;&quot; }


// ---------- 배열
// 가변
arr.push(4)

// 불변
const newArr = [...arr, 4]
</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[얕은 복사/깊은 복사]]></title>
            <link>https://velog.io/@park_log/%EC%96%95%EC%9D%80-%EB%B3%B5%EC%82%AC%EA%B9%8A%EC%9D%80-%EB%B3%B5%EC%82%AC</link>
            <guid>https://velog.io/@park_log/%EC%96%95%EC%9D%80-%EB%B3%B5%EC%82%AC%EA%B9%8A%EC%9D%80-%EB%B3%B5%EC%82%AC</guid>
            <pubDate>Fri, 06 Feb 2026 09:33:20 GMT</pubDate>
            <description><![CDATA[<h3 id="얕은복사">얕은복사</h3>
<p>얕은 복사는 객체의 최상위 프로퍼티만 새로 복사하고(1단계 프로퍼티),
내부에 있는 중첩 객체나 배열은 기존 참조를 그대로 공유하는 복사 방식입니다.
따라서 복사본에서 중첩 데이터를 수정하면 원본에도 영향이 발생할 수 있습니다.
대표적으로 스프레드 연산자나 Object.assign이 얕은 복사를 수행합니다.
<br/></p>
<p><strong>얕은복사 예시</strong></p>
<pre><code>const user1 = {
  name: &quot;Lee&quot;,
  address: { city: &quot;Seoul&quot; }
}

const user2 = { ...user1 } // 얕은 복사 (1단계만 복사)

user2.address.city = &quot;Busan&quot;

console.log(user1.address.city) // &quot;Busan&quot; ← 원본도 함께 변경됨</code></pre><p><br/><br/><br/></p>
<h3 id="깊은-복사">깊은 복사</h3>
<p>깊은 복사는 객체 내부의 중첩된 값까지 모두 새롭게 복사하여 완전히 독립적인 참조를 만드는 방식입니다.
그래서 복사본을 수정해도 원본에는 영향을 주지 않으며, 상태 불변성을 유지해야 하는 상황에서 중요하게 사용됩니다.
구현 방법으로는 structuredClone, 재귀 기반 복사, 또는 Immer 같은 라이브러리가 있습니다.
<br/>
<strong>깊은복사 예시</strong></p>
<pre><code>const user1 = {
  name: &quot;Lee&quot;,
  address: { city: &quot;Seoul&quot; }
}

const user2 = structuredClone(user1) // 깊은 복사 (모든 depth 복사)

user2.address.city = &quot;Busan&quot;

console.log(user1.address.city) // &quot;Seoul&quot; ← 원본 유지</code></pre><blockquote>
<p><strong>리액트와 얕은복사/깊은복사</strong>
리액트에서는 상태 변경 여부를 참조 비교로 판단합니다.
그래서 기존 상태를 직접 수정하지 않고,
불변성을 유지하는 방식으로 상태를 관리하는 것이 중요합니다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[자바스크립트 데이터 타입]]></title>
            <link>https://velog.io/@park_log/%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%83%80%EC%9E%85</link>
            <guid>https://velog.io/@park_log/%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%83%80%EC%9E%85</guid>
            <pubDate>Fri, 06 Feb 2026 09:31:15 GMT</pubDate>
            <description><![CDATA[<p>자바스크립트의 데이터 타입은 크게 원시 타입과 참조 타입으로 구분됩니다.</p>
<h3 id="기본형원시-타입primitive-type">기본형/원시 타입(Primitive Type)</h3>
<ul>
<li>Number</li>
<li>BigInt</li>
<li>String</li>
<li>Boolean</li>
<li>null</li>
<li>undefined</li>
<li>Symbol</li>
</ul>
<p>원시 타입의 값은 변경 불가능한 값이며,
변수에 값을 할당하거나 복사할 때 값 자체가 복사되어 전달됩니다.</p>
<p>원시 값은 <strong>불변(immutable)</strong>이며 값 변경은 재할당으로만 이루어진다.</p>
<pre><code>예시
let a = 10
let b = a  // 값 자체가 복사됨

console.log(a) //10
console.log(b) //10

b = 20  // b에 새로운 값이 재할당됨

console.log(a) //10
console.log(b) //20</code></pre><br/>
<br/>
<br/>


<h3 id="참조형참조-타입reference-type">참조형/참조 타입(Reference Type)</h3>
<ul>
<li>Object</li>
<li>Array</li>
<li>Function</li>
<li>Map</li>
<li>Set</li>
</ul>
<p>참조타입의 경우 메모리에 <strong>객체가 저장된 참조값(위치)</strong> 이 저장됩니다.
변수에 값을 할당할 때에도 객체가 저장된 참조값(reference)이 할당되며,
복사할 때에도 참조값이 복사됩니다.</p>
<p>따라서, 여러 변수가 같은 객체를 공유할 수 있으며,객체 내부 속성을 변경하면 원본에도 영향이 발생합니다.(얕은 복사에서 객체 내부의 참조는 변경되지 않아 발생)</p>
<pre><code>예시
let obj1 = { x: 1 }
let obj2 = obj1 // 객체의 참조(주소)가 복사됨

obj2.x = 2  // 같은 객체를 수정

console.log(obj1.x) // 2
console.log(obj2.x) // 2</code></pre>]]></description>
        </item>
    </channel>
</rss>