<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <author>
    <name>Coderifleman</name>
  </author>
  <generator uri="https://hexo.io/">Hexo</generator>
  <id>https://uyeong.github.io/blog/</id>
  <link href="https://uyeong.github.io/blog/" rel="alternate"/>
  <link href="https://uyeong.github.io/blog/atom.xml" rel="self"/>
  <rights>All rights reserved 2026, Coderifleman</rights>
  <subtitle>frontend development stories.</subtitle>
  <title>Coderifleman's blog</title>
  <updated>2026-03-19T02:01:15.957Z</updated>
  <entry>
    <author>
      <name>Coderifleman</name>
    </author>
    <category term="JavaScript" scheme="https://uyeong.github.io/blog/categories/JavaScript/"/>
    <category term="Security" scheme="https://uyeong.github.io/blog/categories/JavaScript/Security/"/>
    <category term="JavaScript" scheme="https://uyeong.github.io/blog/tags/JavaScript/"/>
    <category term="Security" scheme="https://uyeong.github.io/blog/tags/Security/"/>
    <category term="TypeScript" scheme="https://uyeong.github.io/blog/tags/TypeScript/"/>
    <category term="PII" scheme="https://uyeong.github.io/blog/tags/PII/"/>
    <category term="Privacy" scheme="https://uyeong.github.io/blog/tags/Privacy/"/>
    <category term="Open Source" scheme="https://uyeong.github.io/blog/tags/Open-Source/"/>
    <content>
      <![CDATA[<p>서비스를 운영하다 보면 로그에 사용자 이메일이 찍히고, 분석 파이프라인에 전화번호가 섞여 들어간다. 요즘은 LLM에 보낸 프롬프트에 카드번호가 실려 나가는 일까지 생긴다. GDPR, HIPAA 같은 규정이 아니더라도 개인정보가 의도치 않게 유출되는 건 누구에게나 부담스러운 일이다.</p><p><a href="https://github.com/sam247/openredaction">OpenRedaction</a>은 이런 문제를 정규식 기반으로 해결하는 JavaScript&#x2F;TypeScript 라이브러리다. 570개 이상의 PII(Personal Identifiable Information) 패턴을 내장하고 있고, 외부 API 호출 없이 로컬에서 동작한다. MIT 라이선스이고 테스트도 450개 넘게 작성돼 있어서, 실무에 도입하기에 부담이 적다.</p><h2 id="어떤-문제를-풀려는-걸까"><a href="#어떤-문제를-풀려는-걸까" class="headerlink" title="어떤 문제를 풀려는 걸까"></a>어떤 문제를 풀려는 걸까</h2><p>텍스트 데이터에서 민감 정보를 걸러내는 일은 생각보다 까다롭다. 이메일 주소 하나만 해도 포맷이 다양하고, 전화번호는 국가마다 형식이 다르다. 카드번호 같은 경우 단순 패턴 매칭으론 부족하고, Luhn 알고리즘으로 유효성까지 검증해야 정확도가 올라간다.</p><p>기존에도 Microsoft의 <a href="https://github.com/microsoft/presidio">Presidio</a> 같은 도구가 있었지만, 이쪽은 Python 기반이고 NLP 모델에 의존하는 구조라 JavaScript 생태계에서 가볍게 쓰기엔 부담이 있었다. OpenRedaction은 “정규식 우선(Regex-first)”이라는 접근으로 이 빈자리를 채운다. AI 모델 없이도 빠르고, 같은 입력에는 항상 같은 결과를 보장한다(deterministic). 필요할 때는 호스팅 API를 통해 AI 보조 탐지도 붙일 수 있다.</p><h2 id="기본-사용법"><a href="#기본-사용법" class="headerlink" title="기본 사용법"></a>기본 사용법</h2><p>설치는 npm 한 줄이면 된다.</p><pre class="language-bash" data-language="bash"><code class="language-bash"><span class="token function">npm</span> <span class="token function">install</span> @sam247/openredaction</code></pre><p>가장 간단한 사용 예시를 보자.</p><pre class="language-typescript" data-language="typescript"><code class="language-typescript"><span class="token keyword">import</span> <span class="token punctuation">&#123;</span> OpenRedaction <span class="token punctuation">&#125;</span> <span class="token keyword">from</span> <span class="token string">'@sam247/openredaction'</span><span class="token punctuation">;</span><span class="token keyword">const</span> redactor <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">OpenRedaction</span><span class="token punctuation">(</span><span class="token punctuation">&#123;</span>  redactionMode<span class="token operator">:</span> <span class="token string">'placeholder'</span><span class="token punctuation">&#125;</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token keyword">const</span> result <span class="token operator">=</span> <span class="token keyword">await</span> redactor<span class="token punctuation">.</span><span class="token function">detect</span><span class="token punctuation">(</span>  <span class="token string">'My name is John Smith and my email is john@example.com'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token builtin">console</span><span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>result<span class="token punctuation">.</span>redacted<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token comment">// "My name is [NAME_XXXX] and my email is [EMAIL_XXXX]"</span></code></pre><p>텍스트를 넣으면 PII를 자동으로 탐지해서 <code>[NAME_XXXX]</code>, <code>[EMAIL_XXXX]</code> 같은 플레이스홀더로 치환해준다. <code>result.detections</code>를 통해 어떤 타입의 PII가 어디서 감지됐는지도 확인할 수 있다.</p><h2 id="마스킹-모드"><a href="#마스킹-모드" class="headerlink" title="마스킹 모드"></a>마스킹 모드</h2><p>마스킹 방식을 다섯 가지 중에서 골라 쓸 수 있다.</p><table><thead><tr><th>모드</th><th>설명</th><th>예시</th></tr></thead><tbody><tr><td><code>placeholder</code></td><td>타입별 플레이스홀더로 대체</td><td><code>[EMAIL_XXXX]</code></td></tr><tr><td><code>mask-middle</code></td><td>양쪽 끝만 남기고 마스킹</td><td><code>s***@example.com</code></td></tr><tr><td><code>mask-all</code></td><td>전체 마스킹</td><td><code>****************</code></td></tr><tr><td><code>format-preserving</code></td><td>원본 형식을 유지하며 마스킹</td><td>형식은 동일, 값만 변경</td></tr><tr><td><code>token-replace</code></td><td>결정적 토큰으로 교체</td><td>동일 값은 항상 같은 토큰</td></tr></tbody></table><p>상황에 따라 골라 쓸 수 있는데, 예를 들어 로그에서는 <code>mask-all</code>로 완전히 가리고 LLM 파이프라인에서는 <code>token-replace</code>로 대체한 뒤 나중에 복원하는 식이다.</p><pre class="language-typescript" data-language="typescript"><code class="language-typescript"><span class="token keyword">const</span> redactor <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">OpenRedaction</span><span class="token punctuation">(</span><span class="token punctuation">&#123;</span>  includeNames<span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span>  includeEmails<span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span>  includePhones<span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span>  redactionMode<span class="token operator">:</span> <span class="token string">'mask-middle'</span><span class="token punctuation">&#125;</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token keyword">const</span> input <span class="token operator">=</span> <span class="token string">'Contact Sarah Jones at sarah@example.com or call +1 202-555-0110'</span><span class="token punctuation">;</span><span class="token keyword">const</span> <span class="token punctuation">&#123;</span> redacted <span class="token punctuation">&#125;</span> <span class="token operator">=</span> <span class="token keyword">await</span> redactor<span class="token punctuation">.</span><span class="token function">detect</span><span class="token punctuation">(</span>input<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token builtin">console</span><span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>redacted<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token comment">// "Contact S***h J***s at s***@example.com or call +1 ***-***-0110"</span></code></pre><h2 id="탐지할-수-있는-PII-패턴"><a href="#탐지할-수-있는-PII-패턴" class="headerlink" title="탐지할 수 있는 PII 패턴"></a>탐지할 수 있는 PII 패턴</h2><p>570개 이상의 패턴을 내장하고 있고, 크게 다음과 같은 카테고리로 나뉜다.</p><p><strong>개인 정보</strong> — 이메일, 전화번호(미국·영국·국제), 이름(문맥 인식 검증), 주민등록번호(SSN), 여권, 운전면허</p><p><strong>금융 정보</strong> — 신용카드(Luhn 검증), IBAN, 은행 계좌, SWIFT 코드, 라우팅 번호, 암호화폐 주소</p><p><strong>디지털 신원</strong> — API 키, OAuth 토큰, JWT, Bearer 토큰, 소셜 미디어 ID</p><p>이 외에도 50개국 이상의 정부 발급 ID, 의료 정보, 소매·법률·물류·보험 등 25개 이상 산업 분야의 패턴까지 커버한다.</p><p>단순히 정규식만 돌리는 게 아니라, 체크섬 검증(카드번호의 Luhn 알고리즘 등)과 문맥 기반 필터링까지 결합해서 오탐(false positive)을 줄이는 방향으로 설계돼 있다.</p><h2 id="LLM-파이프라인에서의-활용"><a href="#LLM-파이프라인에서의-활용" class="headerlink" title="LLM 파이프라인에서의 활용"></a>LLM 파이프라인에서의 활용</h2><p>요즘 가장 쓸모 있는 시나리오는 LLM 연동이다. 사용자 입력을 외부 AI API로 보내기 전에 민감 정보를 마스킹하고, 응답을 받은 뒤 원본으로 복원하는 식이다.</p><pre class="language-typescript" data-language="typescript"><code class="language-typescript"><span class="token keyword">import</span> <span class="token punctuation">&#123;</span> OpenRedaction <span class="token punctuation">&#125;</span> <span class="token keyword">from</span> <span class="token string">'@sam247/openredaction'</span><span class="token punctuation">;</span><span class="token keyword">const</span> redactor <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">OpenRedaction</span><span class="token punctuation">(</span><span class="token punctuation">&#123;</span>  preset<span class="token operator">:</span> <span class="token string">'gdpr'</span><span class="token punctuation">,</span>  redactionMode<span class="token operator">:</span> <span class="token string">'token-replace'</span><span class="token punctuation">,</span>  deterministic<span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">&#125;</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">sanitizeForLLM</span><span class="token punctuation">(</span>text<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>  <span class="token keyword">const</span> <span class="token punctuation">&#123;</span> redacted<span class="token punctuation">,</span> redactionMap <span class="token punctuation">&#125;</span> <span class="token operator">=</span> <span class="token keyword">await</span> redactor<span class="token punctuation">.</span><span class="token function">detect</span><span class="token punctuation">(</span>text<span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token comment">// 마스킹된 텍스트를 LLM에 전송</span>  <span class="token keyword">const</span> response <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">sendToLLM</span><span class="token punctuation">(</span>redacted<span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token comment">// 신뢰할 수 있는 목적지에는 복원해서 전달</span>  <span class="token keyword">const</span> restored <span class="token operator">=</span> redactor<span class="token punctuation">.</span><span class="token function">restore</span><span class="token punctuation">(</span>response<span class="token punctuation">,</span> redactionMap<span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token keyword">return</span> <span class="token punctuation">&#123;</span> redacted<span class="token punctuation">,</span> restored<span class="token punctuation">,</span> redactionMap <span class="token punctuation">&#125;</span><span class="token punctuation">;</span><span class="token punctuation">&#125;</span></code></pre><p><code>deterministic: true</code> 옵션을 주면 같은 값에 대해 항상 같은 토큰이 생성된다. 그래서 LLM이 응답에서 해당 토큰을 그대로 사용하면, <code>restore()</code>로 원본 값을 복원할 수 있다. 이 방식을 쓰면 외부 API에 실제 개인정보를 노출하지 않으면서도 문맥을 유지한 채 AI 기능을 활용할 수 있다.</p><h2 id="컴플라이언스-프리셋"><a href="#컴플라이언스-프리셋" class="headerlink" title="컴플라이언스 프리셋"></a>컴플라이언스 프리셋</h2><p>규정별로 미리 정의된 프리셋도 제공한다.</p><ul><li><code>gdpr</code> — EU 일반 데이터 보호 규정</li><li><code>hipaa</code> — 미국 의료 정보 보호</li><li><code>ccpa</code> — 캘리포니아 소비자 프라이버시법</li><li><code>finance</code> — 금융 분야</li><li><code>education</code> — 교육 분야</li><li><code>transportation</code> — 교통 분야</li></ul><p>프리셋을 지정하면 해당 규정에서 요구하는 PII 카테고리가 자동으로 활성화된다. 물론 <code>categories</code>나 <code>patterns</code> 옵션으로 세밀하게 조정할 수도 있다.</p><h2 id="커스텀-패턴-추가"><a href="#커스텀-패턴-추가" class="headerlink" title="커스텀 패턴 추가"></a>커스텀 패턴 추가</h2><p>내장 패턴으로 부족하다면 커스텀 패턴을 추가할 수 있다. 예를 들어 회사 내부의 사원번호 같은 걸 탐지하고 싶다면 이런 식이다.</p><pre class="language-typescript" data-language="typescript"><code class="language-typescript"><span class="token keyword">const</span> redactor <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">OpenRedaction</span><span class="token punctuation">(</span><span class="token punctuation">&#123;</span>  customPatterns<span class="token operator">:</span> <span class="token punctuation">[</span>    <span class="token punctuation">&#123;</span>      type<span class="token operator">:</span> <span class="token string">'EMPLOYEE_ID'</span><span class="token punctuation">,</span>      regex<span class="token operator">:</span> <span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex">EMP-\d&#123;4&#125;</span><span class="token regex-delimiter">/</span><span class="token regex-flags">g</span></span><span class="token punctuation">,</span>      priority<span class="token operator">:</span> <span class="token number">10</span><span class="token punctuation">,</span>      placeholder<span class="token operator">:</span> <span class="token string">'[EMPLOYEE_ID_&#123;n&#125;]'</span><span class="token punctuation">,</span>      severity<span class="token operator">:</span> <span class="token string">'medium'</span><span class="token punctuation">,</span>    <span class="token punctuation">&#125;</span><span class="token punctuation">,</span>  <span class="token punctuation">]</span><span class="token punctuation">,</span>  whitelist<span class="token operator">:</span> <span class="token punctuation">[</span><span class="token string">'ACME Corp'</span><span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token comment">// 허용 목록에 등록된 값은 마스킹하지 않음</span><span class="token punctuation">&#125;</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre><p><code>whitelist</code>를 쓰면 특정 값을 마스킹에서 제외할 수도 있어서, 회사명이나 공개된 정보가 불필요하게 가려지는 걸 방지할 수 있다.</p><h2 id="한계와-주의점"><a href="#한계와-주의점" class="headerlink" title="한계와 주의점"></a>한계와 주의점</h2><p>정규식 기반 접근이기 때문에 몇 가지 한계가 있다.</p><p><strong>문맥에 따라 달라지는 PII는 놓칠 수 있다.</strong> “철수”가 사람 이름인지 “물을 철수하다”의 동사인지, 정규식만으로는 판단하기 어렵다. 이름 탐지에는 문맥 분석(Context Analysis)을 결합하고 있지만, NLP 모델만큼의 정확도를 기대하기는 어렵다.</p><p><strong>패턴 커버리지가 완벽하지 않다.</strong> 570개 이상의 패턴이 많아 보이지만, 세상의 모든 PII 형식을 다루진 못한다. 특히 한국의 주민등록번호나 사업자등록번호 같은 로컬 패턴이 포함돼 있는지는 별도로 확인이 필요하다.</p><p><strong>민감한 데이터를 다루는 만큼 수동 검토가 권장된다.</strong> 라이브러리 자체도 README에서 “고도로 민감한 사용 사례에서는 마스킹 결과를 수동으로 검토하라”고 안내하고 있다.</p><h2 id="생태계"><a href="#생태계" class="headerlink" title="생태계"></a>생태계</h2><p>이런 한계를 보완할 수 있도록 OpenRedaction은 단일 라이브러리가 아니라 작은 생태계를 이루고 있다.</p><ul><li><strong><a href="https://github.com/sam247/openredaction">@sam247&#x2F;openredaction</a></strong> — 핵심 라이브러리. 로컬 정규식 기반 탐지·마스킹</li><li><strong><a href="https://github.com/sam247/openredaction-api">openredaction-api</a></strong> — 호스팅 API 서버. AI 보조 탐지 제공</li><li><strong><a href="https://openredaction.com/">openredaction.com</a></strong> — 웹 플레이그라운드. 브라우저에서 바로 체험 가능</li></ul><p>핵심 라이브러리만 쓰면 완전히 로컬에서 무료로 동작하고, 정규식으로 부족한 부분은 호스팅 API의 AI 보조 탐지로 보완할 수 있는 구조다.</p><h2 id="정리"><a href="#정리" class="headerlink" title="정리"></a>정리</h2><p>OpenRedaction은 PII 탐지와 마스킹이라는 명확한 문제 하나를 정규식으로 깔끔하게 풀어낸 라이브러리다. AI 모델에 의존하지 않아서 가볍고 빠르며, 로컬에서 돌아가기 때문에 데이터가 외부로 나갈 일이 없다. LLM 파이프라인의 전처리 단계에 끼워 넣거나, 로그·분석 파이프라인에서 개인정보를 자동으로 걸러내는 용도로 실용적이다.</p><p>다만 정규식만으로 모든 케이스를 잡을 순 없으니, 1차 방어선으로 두고 AI 보조 탐지나 수동 검토를 함께 쓰는 게 현실적이다.</p><h2 id="참고-링크"><a href="#참고-링크" class="headerlink" title="참고 링크"></a>참고 링크</h2><ul><li><a href="https://github.com/sam247/openredaction">GitHub — sam247&#x2F;openredaction</a></li><li><a href="https://openredaction.com/">OpenRedaction 공식 사이트</a></li><li><a href="https://www.npmjs.com/package/@sam247/openredaction">npm — @sam247&#x2F;openredaction</a></li></ul>]]>
    </content>
    <id>https://uyeong.github.io/blog/2026/03/19/openredaction-pii-detection-and-masking/</id>
    <link href="https://uyeong.github.io/blog/2026/03/19/openredaction-pii-detection-and-masking/"/>
    <published>2026-03-19T02:00:00.000Z</published>
    <summary>JavaScript/TypeScript 환경에서 이메일, 전화번호, 카드번호 등 570개 이상의 PII 패턴을 정규식 기반으로 탐지하고 마스킹하는 오픈소스 라이브러리 OpenRedaction을 소개한다.</summary>
    <title>OpenRedaction — 텍스트에서 민감 정보를 자동으로 찾아 마스킹하는 라이브러리</title>
    <updated>2026-03-19T02:01:15.957Z</updated>
  </entry>
  <entry>
    <author>
      <name>Coderifleman</name>
    </author>
    <category term="JavaScript" scheme="https://uyeong.github.io/blog/categories/JavaScript/"/>
    <category term="JavaScript" scheme="https://uyeong.github.io/blog/tags/JavaScript/"/>
    <category term="ECMAScript" scheme="https://uyeong.github.io/blog/tags/ECMAScript/"/>
    <category term="TC39" scheme="https://uyeong.github.io/blog/tags/TC39/"/>
    <category term="ECMA-262" scheme="https://uyeong.github.io/blog/tags/ECMA-262/"/>
    <category term="ECMA-402" scheme="https://uyeong.github.io/blog/tags/ECMA-402/"/>
    <content>
      <![CDATA[<p>이번 TC39 2026년 3월에서 Temporal·Intl era&#x2F;monthCode가 Stage 4에 도달했고, 동시에 에러·모듈·Promise 쪽 동작을 더 분명하게 다듬는 제안들도 진척됐다.</p><h2 id="먼저-숫자부터-이번-회의에서-이동한-항목"><a href="#먼저-숫자부터-이번-회의에서-이동한-항목" class="headerlink" title="먼저 숫자부터: 이번 회의에서 이동한 항목"></a>먼저 숫자부터: 이번 회의에서 이동한 항목</h2><p>변경된 Stage만 빠르게 소개하면 다음과 같다.</p><ul><li><p><strong>ECMA-262</strong></p><ul><li><code>Temporal</code>: Stage 3 → 4</li><li><code>Import Text</code>: Stage 2 → 3</li><li><code>Error Stack Accessor</code>: Stage 2 → 2.7</li><li><code>Thenable Curtailment</code>: Stage 1 → 2</li><li><code>Iterator Includes</code>: 신규(2.7로 표기)</li><li><code>Dynamic Import Host Adjustment</code>: withdrawn</li><li><code>ArrayBuffer.prototype.transfer</code>: withdrawn</li></ul></li><li><p><strong>ECMA-402</strong></p><ul><li><code>Intl era and monthCode</code>: Stage 3 → 4</li></ul></li></ul>    <div class="alert alert--info">      <strong class="alert__title">Stage 2.7 표기 확인</strong>      <div class="alert__body">        <p>TC39 Process 문서 기준으로 현재 제안 단계는 <code>0, 1, 2, 2.7, 3, 4</code>로 정의된다.<br><code>2.7</code>은 설계 자체는 사실상 완료됐고, 테스트&#x2F;구현&#x2F;사용 경험으로 마지막 검증을 진행하는 단계다.</p>      </div>    </div>  <h2 id="ECMA-262-변경"><a href="#ECMA-262-변경" class="headerlink" title="ECMA-262 변경"></a>ECMA-262 변경</h2><p>이번 절에서는 Stage 정보와 함께 각 제안이 어떤 문제를 해결하고 왜 중요한지에 초점을 맞춰 간단히 소개하고자 한다.</p><h3 id="1-Temporal-3-→-4"><a href="#1-Temporal-3-→-4" class="headerlink" title="1) Temporal (3 → 4)"></a>1) Temporal (3 → 4)</h3><ul><li><strong>핵심 문제</strong>: <code>Date</code>의 모호한 시간대 처리, 가변 객체, DST 경계 문제</li><li><strong>제안 방향</strong>: <code>Instant</code>, <code>ZonedDateTime</code>, <code>PlainDate</code> 같은 타입 분리로 의도 명확화</li><li><strong>의미</strong>: 날짜&#x2F;시간 버그를 라이브러리 규약이 아니라 언어 표준 레벨에서 줄이려는 흐름</li></ul><h3 id="2-Import-Text-2-→-3"><a href="#2-Import-Text-2-→-3" class="headerlink" title="2) Import Text (2 → 3)"></a>2) Import Text (2 → 3)</h3><ul><li><strong>핵심 문제</strong>: 텍스트 리소스 로딩을 <code>fetch(...).text()</code>로 우회하면 경로 기준, async 강제, 실행 시점 제약이 생김</li><li><strong>제안 방향</strong>: <code>import ... with &#123; type: &quot;text&quot; &#125;</code> 같은 형태로 모듈 시스템에 텍스트 import를 정식 통합</li><li><strong>의미</strong>: 빌드 도구 의존이 강했던 패턴을 런타임&#x2F;플랫폼 표준으로 끌어올리는 단계</li></ul><h3 id="3-Error-Stack-Accessor-2-→-2-7"><a href="#3-Error-Stack-Accessor-2-→-2-7" class="headerlink" title="3) Error Stack Accessor (2 → 2.7)"></a>3) Error Stack Accessor (2 → 2.7)</h3><ul><li><strong>핵심 문제</strong>: <code>Error.prototype.stack</code>은 사실상 표준처럼 쓰였지만, 사양 수준 정의가 약했음</li><li><strong>제안 방향</strong>: 새 기능 추가보다 기존 웹 현실(de facto API)을 안전하게 표준화</li><li><strong>의미</strong>: 디버깅&#x2F;로깅 생태계의 기반 동작을 엔진 간 더 일관되게 맞추려는 정리 작업</li></ul><h3 id="4-Thenable-Curtailment-1-→-2"><a href="#4-Thenable-Curtailment-1-→-2" class="headerlink" title="4) Thenable Curtailment (1 → 2)"></a>4) Thenable Curtailment (1 → 2)</h3><ul><li><strong>핵심 문제</strong>: thenable 동화(assimilation) 과정에서 예기치 않은 사용자 코드 실행 경로가 생겨 보안&#x2F;복잡성 이슈 유발</li><li><strong>제안 방향</strong>: thenable 처리 경계에서 실행 시점&#x2F;동작을 더 제어 가능한 형태로 축소</li><li><strong>의미</strong>: Promise 상호운용성은 유지하되, 플랫폼&#x2F;호스트 구현이 안전하게 다룰 수 있게 하는 시도</li></ul><h3 id="5-Iterator-Includes-신규"><a href="#5-Iterator-Includes-신규" class="headerlink" title="5) Iterator Includes (신규)"></a>5) Iterator Includes (신규)</h3><ul><li><strong>핵심 문제</strong>: iterator에서 특정 값 존재 확인을 매번 우회 패턴으로 작성해야 함</li><li><strong>제안 방향</strong>: <code>Array.prototype.includes</code>에 대응하는 <code>Iterator.prototype.includes</code></li><li><strong>의미</strong>: 큰 기능은 아니지만, iterator helper 흐름과 맞물려 코드 의도를 더 직접적으로 표현 가능</li></ul><h3 id="6-Withdrawn-항목-2개는-어떻게-읽어야-하나"><a href="#6-Withdrawn-항목-2개는-어떻게-읽어야-하나" class="headerlink" title="6) Withdrawn 항목 2개는 어떻게 읽어야 하나"></a>6) Withdrawn 항목 2개는 어떻게 읽어야 하나</h3><p><code>withdrawn</code>은 “문제 자체가 사라졌다”는 게 아니라 “현재 제안 문서 트랙을 종료했다”는 뜻이며, 실무에선 문제의 재등장 가능성과 대체 경로를 함께 봐야한다.</p><h4 id="Dynamic-Import-Host-Adjustment-withdrawn"><a href="#Dynamic-Import-Host-Adjustment-withdrawn" class="headerlink" title="Dynamic Import Host Adjustment (withdrawn)"></a>Dynamic Import Host Adjustment (withdrawn)</h4><ul><li>Trusted Types&#x2F;보안 맥락에서 <code>import(...)</code> 처리 지점을 조정하려던 제안</li><li>이번 집계 기준으로는 withdrawn 상태지만, 동적 import의 보안 경계 이슈 자체가 완전히 끝났다고 보긴 어렵다</li></ul><h4 id="ArrayBuffer-prototype-transfer-withdrawn"><a href="#ArrayBuffer-prototype-transfer-withdrawn" class="headerlink" title="ArrayBuffer.prototype.transfer (withdrawn)"></a>ArrayBuffer.prototype.transfer (withdrawn)</h4><ul><li>이번 집계에서 withdrawn으로 표시됨</li><li>관련 사용성&#x2F;성능 요구는 다른 ArrayBuffer 계열 제안(예: resizable&#x2F;growable)에서 계속 다뤄질 수 있으므로, 기능 단위로 추적하는 게 현실적이다</li></ul><h2 id="ECMA-402-변경"><a href="#ECMA-402-변경" class="headerlink" title="ECMA-402 변경"></a>ECMA-402 변경</h2><p>이번 절도 같은 기준으로, Stage 정보와 함께 어떤 문제를 해결하고 왜 중요한지 짧게 정리한다.</p><h3 id="1-Intl-era-monthCode-3-→-4"><a href="#1-Intl-era-monthCode-3-→-4" class="headerlink" title="1) Intl era&#x2F;monthCode (3 → 4)"></a>1) Intl era&#x2F;monthCode (3 → 4)</h3><p>이 정보는 Temporal과 연결해서 보길 바란다(Temporal 타입 자체는 ECMA-262, 국제화 포맷&#x2F;캘린더 해석 규칙은 ECMA-402가 담당한다).</p><ul><li><strong>핵심 문제</strong>: ISO 달력 밖의 달력(예: era 기반)에서 <code>era</code>, <code>eraYear</code>, <code>monthCode</code> 처리 규칙이 구현마다 흔들릴 수 있음</li><li><strong>제안 방향</strong>: 비-ISO 캘린더 환경에서 필요한 추상 연산&#x2F;필드 동작을 ECMA-402에서 명확화</li><li><strong>의미</strong>: Temporal의 국제화 캘린더 사용성을 “실제로 동작하는 수준”으로 올리는 필수 퍼즐</li></ul><h2 id="마무리"><a href="#마무리" class="headerlink" title="마무리"></a>마무리</h2><p>이번 업데이트에서 시간&#x2F;국제화(Temporal, Intl), 모듈(Import Text), 에러(stack), 비동기 경계(thenable) 모두 예측 가능성을 높이는 방향으로 움직였다.</p><p>실무에서는 특정 제안 하나보다, 런타임&#x2F;엔진 간 차이가 나던 지점을 줄여 주는지에 주목하면 좋다.<br>이번 회의 결과는 그 정리 작업이 계속 진행 중이라는 근거로 볼 수 있다.</p><h2 id="참고-링크"><a href="#참고-링크" class="headerlink" title="참고 링크"></a>참고 링크</h2><ul><li>ECMAScript Daily 요약: <a href="https://ecmascript-daily.github.io/ecmascript/2026/03/16/ecmascript-proposal-update">https://ecmascript-daily.github.io/ecmascript/2026/03/16/ecmascript-proposal-update</a></li><li>TC39 March 2026 agenda: <a href="https://github.com/tc39/agendas/blob/main/2026/03.md">https://github.com/tc39/agendas/blob/main/2026/03.md</a></li><li>TC39 finished proposals: <a href="https://github.com/tc39/proposals/blob/main/finished-proposals.md">https://github.com/tc39/proposals/blob/main/finished-proposals.md</a></li><li>Temporal: <a href="https://github.com/tc39/proposal-temporal">https://github.com/tc39/proposal-temporal</a></li><li>Import Text: <a href="https://github.com/tc39/proposal-import-text">https://github.com/tc39/proposal-import-text</a></li><li>Error Stack Accessor: <a href="https://github.com/tc39/proposal-error-stack-accessor">https://github.com/tc39/proposal-error-stack-accessor</a></li><li>Thenable Curtailment: <a href="https://github.com/tc39/proposal-thenable-curtailment">https://github.com/tc39/proposal-thenable-curtailment</a></li><li>Iterator Includes: <a href="https://github.com/tc39/proposal-iterator-includes">https://github.com/tc39/proposal-iterator-includes</a></li><li>Intl era&#x2F;monthCode: <a href="https://github.com/tc39/proposal-intl-era-monthcode">https://github.com/tc39/proposal-intl-era-monthcode</a></li><li>Dynamic Import Host Adjustment: <a href="https://github.com/tc39/proposal-dynamic-import-host-adjustment">https://github.com/tc39/proposal-dynamic-import-host-adjustment</a></li></ul>]]>
    </content>
    <id>https://uyeong.github.io/blog/2026/03/17/ecmascript-proposal-update-2026-03/</id>
    <link href="https://uyeong.github.io/blog/2026/03/17/ecmascript-proposal-update-2026-03/"/>
    <published>2026-03-17T06:58:00.000Z</published>
    <summary>ECMAScript Daily의 2026년 3월 업데이트를 기준으로 ECMA-262/402의 stage 이동과 withdrawn 항목을 정리하고, 각 제안이 어떤 문제를 풀기 위해 나왔는지 짧게 확장 분석한다.</summary>
    <title>TC39 2026년 3월 제안 업데이트 정리: ECMA-262/402에서 바뀐 것</title>
    <updated>2026-03-17T07:39:27.280Z</updated>
  </entry>
  <entry>
    <author>
      <name>Coderifleman</name>
    </author>
    <category term="Development" scheme="https://uyeong.github.io/blog/categories/Development/"/>
    <category term="AI" scheme="https://uyeong.github.io/blog/tags/AI/"/>
    <category term="AGI" scheme="https://uyeong.github.io/blog/tags/AGI/"/>
    <category term="Future of Work" scheme="https://uyeong.github.io/blog/tags/Future-of-Work/"/>
    <category term="Economy" scheme="https://uyeong.github.io/blog/tags/Economy/"/>
    <category term="Sports" scheme="https://uyeong.github.io/blog/tags/Sports/"/>
    <content>
      <![CDATA[<h2 id="왜-이-질문이-중요해졌나"><a href="#왜-이-질문이-중요해졌나" class="headerlink" title="왜 이 질문이 중요해졌나"></a>왜 이 질문이 중요해졌나</h2><p>LLM을 쓰며 감탄하고, 다른 사람들과 경험담을 나누다 보면 결국 같은 질문으로 돌아온다.</p><p>“그래서 인간은 앞으로 뭘 해서 먹고살지?”</p><p>LLM이 지식 노동을 빠르게 가져가고 있고, 피지컬 AI도 계속 발전하고 있으니까 이 질문이 과장이 아닌 시대가 된 것 같다. 훗날 AGI가 노동을 크게 대체하더라도 인간은 여전히 밥을 먹고, 옷을 사고, 월세를 내며, 가족을 꾸리고 부양해야 한다. 사실 나도 이 질문에 정답은 없다. 예측도 자신 없고. 그래도 그냥 요즘 드는 생각을 편하게 정리해보면, 완전히 비관적일 필요는 없다고 느낀다.</p><h2 id="노동과-증명-인간의-일은-두-가지로-나뉜다"><a href="#노동과-증명-인간의-일은-두-가지로-나뉜다" class="headerlink" title="노동과 증명, 인간의 일은 두 가지로 나뉜다"></a>노동과 증명, 인간의 일은 두 가지로 나뉜다</h2>    <figure title="복싱 경기 장면">      <a href="/blog/images/agi-era-proof-economy/cover-boxing.jpg" target="_blank">        <img src="/blog/images/agi-era-proof-economy/cover-boxing.jpg" alt="복싱 경기 장면">      </a>      <figcaption>&lt;인간의 증명과 경쟁의 경제(출처: Wikimedia Commons, Public Domain)&gt;</figcaption>    </figure>  <p>내가 자주 떠올리는 구분이 하나 있다. 인간이 하는 일은 대충 두 가지로 나뉘는 것 같다는 점이다.</p><ul><li>살아가기 위해 필요한 일을 하는 것(노동, 생산, 돌봄, 운영)</li><li>할 수 있음을 증명하기 위해 하는 일(도전, 놀이, 스포츠, 창작)</li></ul><p>첫 번째는 AI가 점점 잘할 거다. 이건 부정하기 어렵다.<br>그런데 두 번째는 좀 다르게 흘러갈 수도 있지 않을까 싶다.</p><p>체스나 바둑이 좋은 예다. 기계가 인간보다 훨씬 강해졌는데도 인간 경기는 여전히 본다. 에베레스트 등반도 비슷하다. 효율만 따지면 굳이 할 이유가 없어 보이는데, 사람들은 거기에 열광하고 후원하고 이야기를 소비한다. 복싱, UFC, 마라톤도 마찬가지다. </p><p>비슷한 변화는 인터넷 방송에서도 이미 나타났다. 우리 바로 윗세대만 해도 인터넷 방송으로 돈을 번다는 개념 자체를 쉽게 받아들이지 못했다. 그런데 지금은 켠왕이나 스피드런처럼 무엇인가를 새로 생산하지 않는 콘텐츠도 하나의 시장이 되었고, 실제로 꾸준히 성장하고 있다.</p><p>그래서 나는 결국 이 문장으로 정리하게 된다.</p><p><strong>인간은 인간의 증명을 소비한다.</strong></p><h2 id="스포츠를-넘어-개인의-증명-전반이-시장이-된다"><a href="#스포츠를-넘어-개인의-증명-전반이-시장이-된다" class="headerlink" title="스포츠를 넘어, 개인의 증명 전반이 시장이 된다"></a>스포츠를 넘어, 개인의 증명 전반이 시장이 된다</h2><p>여기서 내가 말하고 싶은 건 미래에 스포츠 자체가 커진다는 얘기만은 아니다. 더 정확히는, <strong>개인의 증명 행위 전반</strong>이 시장이 될 수 있겠다고 가설을 세워보는 것이다. 지금은 그냥 쓸데없어 보이거나 괴짜 이벤트처럼 보이는 것도, 룰과 포맷, 커뮤니티가 붙는 순간 충분히 소비 가능한 콘텐츠가 될 수 있다고 본다.</p><p>예를 들면:</p><ul><li>집중력, 반응속도, 기억력 같은 인지 리그</li><li>감각&#x2F;균형&#x2F;적응을 겨루는 신체 감각 리그</li><li>제한 시간에 장비를 만들어 미션을 푸는 공학 제작 리그</li><li>지역 문제 해결 성과를 겨루는 커뮤니티 리그</li><li>장인 기술을 겨루는 숙련 리그</li><li>인터넷 방송의 켠왕&#x2F;스피드런&#x2F;챌린지처럼 개인의 수행을 보여주는 스트리밍 콘텐츠</li><li>당장은 ‘쓸모없어 보이는’ 실험적 도전이라도 사람들이 의미를 붙이면 시장이 되는 콘텐츠</li></ul><p>여기서 바로 현실적인 질문이 나온다.<br>“그래봤자 상위 몇 명만 먹고사는 거 아냐?”</p><p>이 반론이 꽤 현실적이라고 생각한다. 그래서 핵심은 스타 몇 명이 아니라 <strong>중간층이 먹고살 수 있는 구조</strong>다. 즉, 메이저 리그 하나가 아니라 계층적으로 다양한 리그가 있어야 한다.</p><p>그리고 이 구조가 만들어지면 돈 버는 사람이 증명자만은 아닐 거다.<br>코치, 심판, 기록관, 운영자, 해설, 회복 케어, 장비 피팅은 물론이고, 스트리밍 제작&#x2F;편집, 커뮤니티 운영, 챌린지 기획 같은 서포터 직군도 같이 커질 가능성이 높다.</p>    <figure title="증명의 경제 3축 모델">      <a href="/blog/images/agi-era-proof-economy/model-diagram.svg" target="_blank">        <img src="/blog/images/agi-era-proof-economy/model-diagram.svg" alt="증명의 경제 3축 모델">      </a>      <figcaption>&lt;증명자 시장-서포터 시장-공공 안전망 3축 모델&gt;</figcaption>    </figure>  <h2 id="그래서-돈은-어디서-도나"><a href="#그래서-돈은-어디서-도나" class="headerlink" title="그래서 돈은 어디서 도나"></a>그래서 돈은 어디서 도나</h2><p>수익 구조도 한 군데에서만 오긴 어려울 것 같다. 대충 이런 혼합형이 현실적이다.</p><ul><li>소비자: 티켓, 구독, 후원, 굿즈</li><li>기업: 스폰서십, 장비&#x2F;브랜드 매출</li><li>플랫폼: 중계와 광고 수익 분배</li><li>공공: 지자체 팀 운영, 시설, 생활체육 예산</li></ul><p>특히 공공 역할은 매우 중요해질 것 같다. 지금도 시청&#x2F;도청 소속 팀이 있고, 그런 구조 안에서 선수나 코치가 안정적으로 활동하는 기반이 마련돼 있다. AI 전환기엔 이런 기반이 더 중요해질 가능성이 있다.</p><p>이 기반이 넓어질수록 참여 인구가 늘고, 결국 사람들이 무엇을 배우려 하는지도 달라질 것 같다. 나는 오히려 인문학, 심리학, 인체학 같은 분야의 수요가 더 커질 거라고 본다. 이유는 간단하다. AI가 강해질수록 인간은 더 인간에게 집중하게 되고, 인간 자체를 더 깊게 탐구하려 할 가능성이 크기 때문이다.</p><p>결국 모두가 어느 정도는 ‘인간 전문가’가 되려는 방향으로 갈 수 있다. 자기 감정과 동기, 신체와 습관, 관계와 서사를 더 잘 이해해야 자기만의 증명 가치와 가능성을 키울 수 있기 때문이다.</p><h2 id="결국-변하지-않는-것"><a href="#결국-변하지-않는-것" class="headerlink" title="결국 변하지 않는 것"></a>결국 변하지 않는 것</h2><p>정리하면, 나는 AI 시대를 완전 암울하게만 보진 않는다. 그렇다고 무조건 장밋빛도 아니다. 그냥 방향이 달라진다고 본다. 노동의 일부는 기계가 가져가고, 인간은 오히려 더 인간다운 영역(증명, 서사, 관계, 도전)에 집중하게 되는 쪽으로.</p><p>한 줄로 요약하면 이렇다. AI가 노동을 대체할수록, 인간 경제의 중심은 ‘생산’에서 ‘증명’으로 이동할 가능성이 크다.</p><p>마지막으로, 예전 기억 하나가 떠오른다. 내가 초등학생 때 TV에 힙합 댄서들이 나오던 장면을 보며 아버지가 이렇게 말한 적이 있다.</p><blockquote><p>“저걸로 입에 풀칠이나 하겠냐. 나중에 관절 나가면 오래 하지도 못할 텐데. 먹고살려면 평생 직장을 찾아야지.”</p></blockquote><p>그때는 그 말이 너무 당연하게 들렸다. 그런데 지금 시점에서 보면, 그 ‘당연함’ 자체가 시대와 함께 계속 바뀌어 왔다.</p><p>그래서 나는 훗날 이런 대화가 오갈 수도 있다고 생각한다.</p><blockquote><p>“얘야, 옛날엔 사람이 직접 생산하고 노동해서 돈을 벌었단다.”</p><p>“네? 그렇게 해서 어떻게 돈을 벌어요? 사람들에게 증명할 이야기가 없잖아요.”</p></blockquote><p>지금은 이 대화가 조금 과장처럼 들릴지 모르지만, 우리가 이미 지나온 변화 속도를 생각하면 완전히 허황된 상상도 아니라는 생각이 든다.</p><p>결국 핵심은 하나다. 인간은 여전히 인간을 보고, 인간의 증명을 소비한다. 이건 변하지 않는 가치 중 하나다.</p><h2 id="참고-링크"><a href="#참고-링크" class="headerlink" title="참고 링크"></a>참고 링크</h2><ul><li>AlphaGo 관련 개요: <a href="https://en.wikipedia.org/wiki/AlphaGo">https://en.wikipedia.org/wiki/AlphaGo</a></li><li>Deep Blue vs Kasparov: <a href="https://en.wikipedia.org/wiki/Deep_Blue_(chess_computer)">https://en.wikipedia.org/wiki/Deep_Blue_(chess_computer)</a></li><li>International Olympic Committee: <a href="https://olympics.com/ioc">https://olympics.com/ioc</a></li><li>Cover image (Public domain): <a href="https://commons.wikimedia.org/wiki/File:Vernon_Arena,_Wolgast_-_Rivers_boxing_match_LCCN2007663641_JPEG.jpg">https://commons.wikimedia.org/wiki/File:Vernon_Arena,_Wolgast_-_Rivers_boxing_match_LCCN2007663641_JPEG.jpg</a></li></ul>]]>
    </content>
    <id>https://uyeong.github.io/blog/2026/03/16/agi-era-proof-economy/</id>
    <link href="https://uyeong.github.io/blog/2026/03/16/agi-era-proof-economy/"/>
    <published>2026-03-16T01:30:00.000Z</published>
    <summary>AI가 노동을 빠르게 대체하는 흐름을 보면서, 인간의 일과 경제는 어디로 갈지 편하게 정리해본 생각 메모.</summary>
    <title>다가올 AGI 시대, 인간은 무엇으로 먹고살까: '노동'에서 '증명'으로</title>
    <updated>2026-03-16T02:09:23.479Z</updated>
  </entry>
  <entry>
    <author>
      <name>Coderifleman</name>
    </author>
    <category term="Development" scheme="https://uyeong.github.io/blog/categories/Development/"/>
    <category term="JavaScript" scheme="https://uyeong.github.io/blog/tags/JavaScript/"/>
    <category term="Vite" scheme="https://uyeong.github.io/blog/tags/Vite/"/>
    <category term="Vite+" scheme="https://uyeong.github.io/blog/tags/Vite/"/>
    <category term="Frontend" scheme="https://uyeong.github.io/blog/tags/Frontend/"/>
    <category term="Toolchain" scheme="https://uyeong.github.io/blog/tags/Toolchain/"/>
    <content>
      <![CDATA[<p><strong>Vite+ Alpha는 프런트엔드 개발에서 매일 쓰는 도구들을 vp(Vite Plus)라는 단일 진입점으로 묶어서, 설정 파일과 운영 복잡도를 줄이려는 시도</strong>다.</p><p>핵심은 명령어 몇 개를 바꾸는 데 있지 않다. 각자 다른 개발 환경 때문에 생기는 문제(예: Node 버전 불일치), 도구가 여러 개로 나뉘어 관리 포인트가 늘어나는 문제, 버전 업그레이드 때 반복되는 비용을 한 흐름으로 줄이려는 접근이라는 점이 중요하다.</p><h2 id="왜-이-발표가-중요한가"><a href="#왜-이-발표가-중요한가" class="headerlink" title="왜 이 발표가 중요한가"></a>왜 이 발표가 중요한가</h2><p>프런트엔드 프로젝트를 시작 할 때 많은 설정 파일이 디렉터리를 차지할 것이라는 게 예상된다. 또, 사람마다 Node 버전이 다르고, 린트&#x2F;포맷&#x2F;테스트&#x2F;빌드 파이프라인이 각각 따로 노는 일도 흔하다.</p><p>VoidZero는 이번 발표에서 Vite+를 다음과 같이 정의했다.</p><ul><li>Vite, Vitest, Oxlint, Oxfmt, Rolldown, tsdown, Vite Task를 통합</li><li><code>vp</code> 하나로 런타임(Node) + 패키지 매니저 + 프런트엔드 툴체인 관리</li><li>MIT 라이선스로 완전 오픈소스 공개</li></ul><p>발표 글과 레포를 함께 보면, “빠른 도구를 여러 개 조합”하는 방향이 아니라 <strong>개발 사이클 전체를 하나의 인터페이스로 다루겠다</strong>는 의도를 분명히 드러낸다.</p>    <figure title="Vite+ 소개 이미지">      <a href="/blog/images/vite-plus-alpha-overview/viteplus-og.jpg" target="_blank">        <img src="/blog/images/vite-plus-alpha-overview/viteplus-og.jpg" alt="Vite+ 소개 이미지">      </a>      <figcaption>&lt;Vite+는 웹 개발용 통합 툴체인을 전면에 내세운다&gt;</figcaption>    </figure>  <h2 id="핵심-요약"><a href="#핵심-요약" class="headerlink" title="핵심 요약"></a>핵심 요약</h2><ul><li>Vite+는 <code>vp create/install/dev/check/test/build/run/pack</code> 등 개발 흐름 전체를 단일 CLI로 제공한다.</li><li><code>vp env</code>로 Node 버전을 관리해 팀원 간 환경 차이를 줄인다.</li><li><code>vp check</code>는 포맷·린트·타입체크를 한 번에 실행해 검증 루틴을 단순화한다.</li><li>Vite 8의 Rolldown 기반 빌드와 결합해 성능 이점까지 노리는 구조다.</li><li>Alpha 단계이므로 전면 교체보다는 워크스페이스 하나에 시범 적용하는 쪽이 안전하다.</li></ul><h2 id="기술적으로-뭐가-달라지나"><a href="#기술적으로-뭐가-달라지나" class="headerlink" title="기술적으로 뭐가 달라지나"></a>기술적으로 뭐가 달라지나</h2><h3 id="1-명령어-통합보다-더-중요한-건-“운영-모델”-통합"><a href="#1-명령어-통합보다-더-중요한-건-“운영-모델”-통합" class="headerlink" title="1) 명령어 통합보다 더 중요한 건 “운영 모델” 통합"></a>1) 명령어 통합보다 더 중요한 건 “운영 모델” 통합</h3><p>발표만 보면 <code>npm run dev</code> 대신 <code>vp dev</code>를 쓰는 정도로 보일 수 있지만 실무를 떠올리면 다음과 같은 경우가 예상된다.</p><ul><li>신규 입사자 온보딩: “nvm 설치했어요?” 같은 질문 감소</li><li>CI 스크립트 표준화: 팀마다 다른 패키지 매니저 습관 정리</li><li>스크립트 분기 감소: check&#x2F;test&#x2F;build 단계 공통화</li></ul><p>즉, 도구 하나하나의 속도가 빨라지는 것보다, 팀이 반복해서 쓰는 시간(환경 맞추기, 스크립트 정리, CI 관리)을 줄여 주는 <strong>운영 비용 절감 효과</strong>가 더 크게 느껴질 수 있다.</p><h3 id="2-Vite-8-Rolldown과의-연결성"><a href="#2-Vite-8-Rolldown과의-연결성" class="headerlink" title="2) Vite 8&#x2F;Rolldown과의 연결성"></a>2) Vite 8&#x2F;Rolldown과의 연결성</h3><p>Vite 8 Beta에서는 Rolldown 기반으로 프로덕션 빌드 성능 개선을 내세웠다.<br>Vite+는 이 흐름을 빌드에서 끝내지 않고, 주변 도구까지 하나로 묶는 방향으로 확장한다.</p><p>구조를 정리하면 이렇다.</p><ul><li>번들러&#x2F;컴파일러 레이어: Rolldown + Oxc</li><li>개발자 인터페이스 레이어: <code>vp</code></li><li>운영 레이어: 단일 config(<code>vite.config.ts</code>)와 단일 워크플로우</li></ul><p>이 구조가 안정되면, 프로젝트 규모가 커져도 “린터 교체”, “빌드 도구 버전 충돌 대응”, “패키지 매니저 정책 재정리”처럼 툴체인 구성을 다시 손보는 작업이 줄어들 것으로 기대할 수 있다.</p><h3 id="3-Vite-Task-모노레포에서-체감이-큰-포인트"><a href="#3-Vite-Task-모노레포에서-체감이-큰-포인트" class="headerlink" title="3) Vite Task: 모노레포에서 체감이 큰 포인트"></a>3) Vite Task: 모노레포에서 체감이 큰 포인트</h3><p>Vite Task는 <code>vp run</code>의 실행 엔진으로, 입력 추적 기반 캐시(input tracking cache)와 의존성 순서 실행(dependency-ordered execution)을 제공한다.<br>모노레포에서 <code>build/test/lint</code>를 매번 전체 실행하던 팀이라면, 변경 없는 태스크를 건너뛰는 것만으로도 CI 시간 단축을 기대할 수 있다.</p><h2 id="도입할-때-현실적으로-보는-체크포인트"><a href="#도입할-때-현실적으로-보는-체크포인트" class="headerlink" title="도입할 때 현실적으로 보는 체크포인트"></a>도입할 때 현실적으로 보는 체크포인트</h2><p>발표 내용과 별개로, Alpha는 Alpha다. 실제로 도입하려면 아래 항목을 먼저 확인해야 한다.</p><ol><li>플러그인 호환성<ul><li>기존 Vite 플러그인과의 호환성을 표방하지만, 팀에서 쓰는 서드파티 플러그인은 별도 검증이 필요하다.</li></ul></li><li>기존 ESLint&#x2F;Prettier 운영 방식<ul><li>Oxlint&#x2F;Oxfmt 전환 시 룰&#x2F;포맷 차이로 기능과 무관한 코드 변경이 한꺼번에 늘어나 리뷰가 어려워질 수 있다.</li></ul></li><li>CI&#x2F;CD와 캐시 전략<ul><li><code>setup-vp</code> 액션과 기존 캐시 키 설계를 함께 점검해야 한다.</li></ul></li><li>롤백 가능성 확보<ul><li><code>vp migrate</code> 적용 전 브랜치 분리 + baseline 빌드&#x2F;테스트 시간 측정은 필수.</li></ul></li></ol>    <div class="alert alert--info">      <strong class="alert__title">실무 적용 팁</strong>      <div class="alert__body">        <p>처음부터 전체 레포에 넣지 말고, 변경 폭이 작은 패키지 하나를 고른 다음 <strong>온보딩 시간 &#x2F; CI 시간 &#x2F; 빌드 시간 &#x2F; 실패율</strong> 네 지표만 1~2주 측정해보자. 수치가 좋아지면 확장하고, 아니면 바로 롤백하면 된다.</p>      </div>    </div>  <h2 id="마이그레이션-접근법-추천"><a href="#마이그레이션-접근법-추천" class="headerlink" title="마이그레이션 접근법 (추천)"></a>마이그레이션 접근법 (추천)</h2><p>단계를 나눠 접근하면 리스크를 줄일 수 있다.</p><ol><li><strong>관찰 단계</strong>: 현재 프로젝트의 check&#x2F;test&#x2F;build 소요 시간과 실패 원인 수집</li><li><strong>시범 단계</strong>: 한 패키지(or 한 앱)에만 <code>vp migrate</code> 적용</li><li><strong>확장 단계</strong>: CI 템플릿 통일 후 점진 확장</li></ol><p>“단일 진입점”의 실제 가치는 명령어 자체가 아니라, <strong>팀의 반복 작업이 실제로 줄어드느냐</strong>로 판단해야 한다.</p><h2 id="마무리"><a href="#마무리" class="headerlink" title="마무리"></a>마무리</h2><p>Vite+ Alpha는 “도구 하나 추가”가 아니라, 프런트엔드 툴체인 운영 방식을 단순화하려는 시도다.<br>아직 프로덕션에 바로 쓸 단계는 아니지만, <strong>툴체인 유지보수에 피로를 느끼는 팀</strong>이라면 파일럿으로 검증해 볼 만한 시점이다.</p><h2 id="레퍼런스"><a href="#레퍼런스" class="headerlink" title="레퍼런스"></a>레퍼런스</h2><ul><li>Announcing Vite+ Alpha (VoidZero)<br><a href="https://voidzero.dev/posts/announcing-vite-plus-alpha">https://voidzero.dev/posts/announcing-vite-plus-alpha</a></li><li>Vite+ Guide<br><a href="https://viteplus.dev/guide/">https://viteplus.dev/guide/</a></li><li>Vite+ GitHub Repository<br><a href="https://github.com/voidzero-dev/vite-plus">https://github.com/voidzero-dev/vite-plus</a></li><li>Vite 8 Beta: The Rolldown-powered Vite<br><a href="https://vite.dev/blog/announcing-vite8-beta">https://vite.dev/blog/announcing-vite8-beta</a></li><li>setup-vp GitHub Action<br><a href="https://github.com/voidzero-dev/setup-vp">https://github.com/voidzero-dev/setup-vp</a></li></ul>]]>
    </content>
    <id>https://uyeong.github.io/blog/2026/03/15/vite-plus-alpha-overview/</id>
    <link href="https://uyeong.github.io/blog/2026/03/15/vite-plus-alpha-overview/"/>
    <published>2026-03-15T03:45:00.000Z</published>
    <summary>Vite+ Alpha 발표 내용을 바탕으로, 왜 '런타임+패키지 매니저+빌드/테스트/린트' 통합이 팀 생산성에 영향을 주는지와 도입 시 체크포인트를 정리합니다.</summary>
    <title>Vite+ Alpha 공개, 프런트엔드 툴체인 단일화가 진짜로 의미 있는 이유</title>
    <updated>2026-03-15T04:15:59.603Z</updated>
  </entry>
  <entry>
    <author>
      <name>Coderifleman</name>
    </author>
    <category term="Development" scheme="https://uyeong.github.io/blog/categories/Development/"/>
    <category term="Cloudflare" scheme="https://uyeong.github.io/blog/tags/Cloudflare/"/>
    <category term="React" scheme="https://uyeong.github.io/blog/tags/React/"/>
    <category term="RedwoodSDK" scheme="https://uyeong.github.io/blog/tags/RedwoodSDK/"/>
    <category term="RedwoodJS" scheme="https://uyeong.github.io/blog/tags/RedwoodJS/"/>
    <category term="Framework" scheme="https://uyeong.github.io/blog/tags/Framework/"/>
    <content>
      <![CDATA[<p>RedwoodSDK 1.0 발표를 한 줄로 요약하면, 프레임워크가 개발자를 돋보이게 하는 데서 제품 문제 해결 속도를 높이는 쪽으로 무게중심을 옮기겠다는 문제의식에 가깝다.</p><p>Redwood 팀은 이번 글에서 기술 스택 자랑 대신 방향 전환의 이유를 꽤 솔직하게 털어놓는다. 여기서부터는 원문을 바탕으로 한 내 해석인데, 그 문제의식은 요즘 개발 현실을 꽤 정확하게 대변한다. AI 도구가 코드를 빠르게 만들어주는 지금, 병목은 “코드를 어떻게 짰느냐”보다 “왜 이걸 만들고, 얼마나 빨리 검증하느냐”로 옮겨가고 있기 때문이다.</p>    <figure title="RedwoodSDK 1.0 발표와 핵심 메시지">      <a href="/blog/images/redwoodsdk-v1-getting-out-of-the-weeds/redwood-og.png" target="_blank">        <img src="/blog/images/redwoodsdk-v1-getting-out-of-the-weeds/redwood-og.png" alt="RedwoodSDK 1.0 발표와 핵심 메시지">      </a>      <figcaption>&lt;RedwoodSDK 1.0 발표 페이지&gt;</figcaption>    </figure>  <h2 id="핵심-변화-암묵적-규칙보다-명시성"><a href="#핵심-변화-암묵적-규칙보다-명시성" class="headerlink" title="핵심 변화: 암묵적 규칙보다 명시성"></a>핵심 변화: 암묵적 규칙보다 명시성</h2><p>RedwoodSDK 문서와 발표 글에서 드러나는 방향은 분명하다.</p><ul><li><strong>Without Magic</strong>: 코드 생성, 숨겨진 트랜스파일 부작용, 파일명 규칙 기반 특수 동작을 최소화</li><li><strong>Composability Over Configuration</strong>: 고정된 디렉터리 규칙을 강제하기보다 조합 가능한 프리미티브를 제공</li><li><strong>Web-First Architecture</strong>: <code>fetch</code>, <code>Request</code>, <code>Response</code>, <code>URL</code> 같은 웹 표준 API를 직접 사용</li></ul><p>결국 “프레임워크가 다 알아서 해줄게”가 아니라, <strong>플랫폼에 가깝게, 명시적으로, 이해할 수 있는 코드</strong>를 우선한다는 뜻이다.</p><h2 id="왜-프로젝트-이름까지-바꿨을까"><a href="#왜-프로젝트-이름까지-바꿨을까" class="headerlink" title="왜 프로젝트 이름까지 바꿨을까"></a>왜 프로젝트 이름까지 바꿨을까</h2><p>발표 글에는 2020년 RedwoodJS 시작, 2022년 v1, 그리고 2026년 RedwoodSDK 1.0까지의 흐름이 짧게 등장한다. 이 타임라인이 중요한 이유는 기술적 변화보다 <strong>제품 관점의 학습</strong>이 전환 배경으로 언급되기 때문이다.</p><p>글쓴이는 스타트업을 직접 운영하며 겪은 시행착오를 이렇게 말한다.</p><ul><li>데이터베이스 호스팅 직접 운영</li><li>Lambda&#x2F;Fargate 제약과의 씨름</li><li>인프라 조각들을 붙이며 소모되는 시간</li></ul><p>“코드를 잘 짜는 능력”이 오히려 “사업 문제를 푸는 속도”를 늦출 수 있다는 걸 몸으로 확인한 셈이다. 그래서 RedwoodSDK는 “개발자 경험(DX)”을 화려하게 포장하기보다, <strong>제품 개발의 실제 마찰</strong>을 줄이는 쪽으로 축을 옮기겠다는 것이다.</p><h2 id="세-가지-원칙을-실무-언어로-번역해보면"><a href="#세-가지-원칙을-실무-언어로-번역해보면" class="headerlink" title="세 가지 원칙을 실무 언어로 번역해보면"></a>세 가지 원칙을 실무 언어로 번역해보면</h2><h3 id="1-Without-Magic-암묵적-규칙보다-명시적-코드"><a href="#1-Without-Magic-암묵적-규칙보다-명시적-코드" class="headerlink" title="1) Without Magic: 암묵적 규칙보다 명시적 코드"></a>1) Without Magic: 암묵적 규칙보다 명시적 코드</h3><p>대규모 협업에서 가장 비싼 비용은 “모르는 사이에 동작하는 규칙”이다. 처음 만든 사람은 편하지만, 팀원이 늘어날수록 디버깅 비용이 기하급수적으로 커진다.</p><p>RedwoodSDK가 강조하는 “what you write is what runs”는 결국 이 문제를 줄이려는 선택이다.</p><ul><li>온보딩 시 “숨은 룰” 학습량 감소</li><li>리팩터링 시 부작용 예측 가능성 향상</li><li>장애 대응 시 원인 추적 시간 단축</li></ul><h3 id="2-Composability-Over-Configuration-프레임워크-정책보다-사용자의-설계-의도"><a href="#2-Composability-Over-Configuration-프레임워크-정책보다-사용자의-설계-의도" class="headerlink" title="2) Composability Over Configuration: 프레임워크 정책보다 사용자의 설계 의도"></a>2) Composability Over Configuration: 프레임워크 정책보다 사용자의 설계 의도</h3><p>설정 중심 프레임워크는 초반 속도가 빠른 대신, 제품이 커지면서 프레임워크의 세계관과 팀 요구가 충돌하는 시점이 온다.</p><p>RedwoodSDK는 함수·모듈·타입 단위의 조합을 강조한다. 이 방식은 특히 도메인 경계가 자주 바뀌는 초기 제품에서 유리하다.</p><ul><li>기능 단위 실험이 쉬움</li><li>특정 레이어만 교체하기 쉬움</li><li>팀마다 다른 아키텍처 성향을 흡수하기 좋음</li></ul><h3 id="3-Web-First-런타임-추상화보다-플랫폼-직결"><a href="#3-Web-First-런타임-추상화보다-플랫폼-직결" class="headerlink" title="3) Web-First: 런타임 추상화보다 플랫폼 직결"></a>3) Web-First: 런타임 추상화보다 플랫폼 직결</h3><p>Cloudflare 환경을 전제로 하면서 로컬에서도 Miniflare로 운영 환경과 최대한 동일하게 맞추는 접근은, “개발&#x2F;운영 불일치”를 줄이는 데 초점을 둔다.</p><p>이건 실무에서 특히 중요하다. 로컬에서는 되는데 프로덕션에서 깨지는 문제의 대부분이 런타임 차이나 추상화 계층의 미묘한 동작 차이에서 비롯되기 때문이다.</p><h2 id="그래서-누가-관심-가져야-할까"><a href="#그래서-누가-관심-가져야-할까" class="headerlink" title="그래서 누가 관심 가져야 할까"></a>그래서 누가 관심 가져야 할까</h2><p>RedwoodSDK는 모든 팀을 위한 만능 해답이라기보다, 아래 조건에 해당할 때 특히 눈여겨볼 만하다.</p><ul><li>React 기반으로 <strong>Cloudflare 친화적 풀스택</strong>을 구성하려는 팀</li><li>파일 규칙·코드 생성 기반 “매직”보다 명시적 제어를 선호하는 팀</li><li>프레임워크의 강한 정답보다 <strong>팀 아키텍처 자율성</strong>이 중요한 팀</li></ul><p>반대로, 강한 컨벤션이 팀 표준화에 더 유리한 조직이라면 체감하는 러닝 커브가 다를 수 있다. 이런 경우에는 도입 전에 작은 서비스 단위로 먼저 검증해보는 쪽이 안전하다.</p><h2 id="해석-AI-시대의-프레임워크-기준이-달라지고-있다"><a href="#해석-AI-시대의-프레임워크-기준이-달라지고-있다" class="headerlink" title="(해석) AI 시대의 프레임워크 기준이 달라지고 있다"></a>(해석) AI 시대의 프레임워크 기준이 달라지고 있다</h2><p>예전에는 “얼마나 코드 작성을 자동화해주느냐”가 프레임워크 선택의 핵심이었다. 하지만 이제 AI가 그 역할을 빠르게 대체하고 있다. 자연스럽게 기준도 바뀐다.</p><ul><li>생성 속도보다 <strong>변경 용이성</strong></li><li>마법 같은 DX보다 <strong>운영 예측 가능성</strong></li><li>추상화 깊이보다 <strong>문제와 코드 사이 연결의 투명성</strong></li></ul><p>RedwoodSDK 1.0의 메시지는 결국 여기에도 닿아 있다. 프레임워크가 문제를 감추는 게 아니라, 개발자가 문제를 더 잘 볼 수 있게 해야 한다는 것이다.</p><h2 id="마무리"><a href="#마무리" class="headerlink" title="마무리"></a>마무리</h2><p>프레임워크를 고를 때 “얼마나 많은 걸 대신 해주나”만 보면, 나중에 숨은 비용을 크게 치르는 경우가 많다. RedwoodSDK 1.0은 <strong>덜 감추고, 덜 강제하고, 플랫폼에 더 가깝게</strong>라는 철학을 기반으로 한다.</p><p>초반엔 직접 설계해야 할 부분이 조금 더 있을 수 있다. 대신 팀이 커지고 제품이 복잡해질수록, 왜 이렇게 동작하는지 설명할 수 있는 코드가 남을 확률이 높아진다. 개인적으로는 2026년 이후 프레임워크 선택에 변화가 있어야 한다는 점은 자명하다고 본다. RedwoodSDK가 그 답이 될지는 아직 모르겠지만, 적어도 꽤 흥미로운 시사점을 던진다.</p><h2 id="참고-링크"><a href="#참고-링크" class="headerlink" title="참고 링크"></a>참고 링크</h2><ul><li>RedwoodSDK 1.0 발표 글: <a href="https://rwsdk.com/blog/redwood-v1-getting-out-of-the-weeds">Redwood v1: Getting Out of the Weeds</a></li><li>RedwoodSDK 문서: <a href="https://docs.rwsdk.com/">What is RedwoodSDK?</a></li><li>RedwoodSDK 공식 사이트: <a href="https://rwsdk.com/">rwsdk.com</a></li></ul>]]>
    </content>
    <id>https://uyeong.github.io/blog/2026/03/13/redwoodsdk-v1-getting-out-of-the-weeds/</id>
    <link href="https://uyeong.github.io/blog/2026/03/13/redwoodsdk-v1-getting-out-of-the-weeds/"/>
    <published>2026-03-13T00:45:00.000Z</published>
    <summary>RedwoodJS에서 RedwoodSDK로 전환되며 드러난 핵심 변화(Without Magic, Composability Over Configuration, Web-First)를 개발자 실무 관점에서 정리했다.</summary>
    <title>RedwoodSDK 1.0, 구현의 늪에서 제품으로 시선을 돌리다</title>
    <updated>2026-03-13T03:06:05.716Z</updated>
  </entry>
  <entry>
    <author>
      <name>Coderifleman</name>
    </author>
    <category term="Development" scheme="https://uyeong.github.io/blog/categories/Development/"/>
    <category term="AI" scheme="https://uyeong.github.io/blog/tags/AI/"/>
    <category term="Design" scheme="https://uyeong.github.io/blog/tags/Design/"/>
    <category term="UX" scheme="https://uyeong.github.io/blog/tags/UX/"/>
    <category term="Claude-Code" scheme="https://uyeong.github.io/blog/tags/Claude-Code/"/>
    <category term="Cursor" scheme="https://uyeong.github.io/blog/tags/Cursor/"/>
    <content>
      <![CDATA[<p><code>pbakaus/impeccable</code>은 “AI가 UI를 만들 때 자주 내는 뻔한 결과”를 줄이기 위한 <strong>디자인 지향 스킬 번들</strong>이다.</p><p>코드를 생성하는 모델 성능이 좋아져도, 화면 결과물은 비슷비슷한 경우가 많다. 이 프로젝트는 그 지점을 다룬다. 단순히 “예쁜 UI 템플릿”을 주는 게 아니라, AI 에이전트의 <strong>디자인 품질을 조향(steering)</strong>하는 스킬과 명령 체계를 제공한다.</p><h2 id="프로젝트를-한-줄로-정의하면"><a href="#프로젝트를-한-줄로-정의하면" class="headerlink" title="프로젝트를 한 줄로 정의하면"></a>프로젝트를 한 줄로 정의하면</h2><p>impeccable은 크게 다음 세 가지 묶음을 제공한다.</p><ol><li><code>frontend-design</code> 스킬(참고 문서 7개)</li><li>품질&#x2F;개선용 명령 17개</li><li>“하지 말아야 할 UI 안티패턴” 가이드</li></ol><p>타이포, 색, 공간, 모션, 인터랙션, 반응형, UX writing 등 핵심 디자인 영역을 고르게 커버한다.</p><h2 id="무엇이-들어-있나"><a href="#무엇이-들어-있나" class="headerlink" title="무엇이 들어 있나"></a>무엇이 들어 있나</h2><h3 id="1-스킬-레퍼런스-7종"><a href="#1-스킬-레퍼런스-7종" class="headerlink" title="1) 스킬 레퍼런스 7종"></a>1) 스킬 레퍼런스 7종</h3><p>README 기준으로 다음 파일군이 핵심이다.</p><ul><li>typography</li><li>color-and-contrast</li><li>spatial-design</li><li>motion-design</li><li>interaction-design</li><li>responsive-design</li><li>ux-writing</li></ul><p>즉, “컴포넌트 코드를 어떻게 짤까”보다 “사용자에게 어떻게 보이고 동작해야 하는가”를 강하게 규정한다.</p><h3 id="2-명령-17개"><a href="#2-명령-17개" class="headerlink" title="2) 명령 17개"></a>2) 명령 17개</h3><p><code>/audit</code>, <code>/critique</code>, <code>/normalize</code>, <code>/polish</code>, <code>/distill</code>, <code>/animate</code> 같은 명령이 있고, 필요하면 특정 화면이나 영역을 지정해서 원하는 부분만 집중 점검할 수 있다(예: <code>/audit header</code>). 각 명령어를 간단히 소개하자면 다음과 같다.</p><ul><li><p>진단&#x2F;품질</p><ul><li><code>/audit</code>: 접근성, 성능, 반응형 등 기술 품질 이슈를 점검한다.</li><li><code>/critique</code>: UX 관점에서 정보 위계, 명확성, 감성 톤을 리뷰한다.</li><li><code>/optimize</code>: UI 렌더링&#x2F;자산 중심으로 성능 개선 포인트를 찾는다.</li><li><code>/harden</code>: 에러 처리, i18n, 엣지 케이스 대응을 보강한다.</li></ul></li><li><p>정리&#x2F;완성</p><ul><li><code>/normalize</code>: 디자인 시스템 기준에 맞춰 일관성을 정리한다.</li><li><code>/polish</code>: 배포 직전 마감 품질을 다듬는다.</li><li><code>/distill</code>: 불필요한 요소를 걷어내고 핵심만 남긴다.</li><li><code>/extract</code>: 반복 UI를 재사용 가능한 컴포넌트로 분리한다.</li></ul></li><li><p>표현&#x2F;감성</p><ul><li><code>/animate</code>: 과하지 않은 모션으로 상태 변화와 맥락을 보강한다.</li><li><code>/colorize</code>: 전략적으로 색을 도입해 계층과 포커스를 강화한다.</li><li><code>/bolder</code>: 밋밋한 화면의 대비와 개성을 강화한다.</li><li><code>/quieter</code>: 과한 시각 요소를 눌러 안정적인 톤으로 정리한다.</li><li><code>/delight</code>: 작은 상호작용 디테일로 사용자 경험의 완성도를 높인다.</li></ul></li><li><p>맥락&#x2F;콘텐츠</p><ul><li><code>/teach-impeccable</code>: 프로젝트&#x2F;브랜드 컨텍스트를 수집해 기본값으로 저장한다.</li><li><code>/clarify</code>: 버튼&#x2F;에러&#x2F;가이드 문구를 더 명확한 UX writing으로 다듬는다.</li><li><code>/adapt</code>: 디바이스&#x2F;뷰포트 조건에 맞게 레이아웃을 조정한다.</li><li><code>/onboard</code>: 온보딩 플로우와 첫 사용자 경험을 설계한다.</li></ul></li></ul><h3 id="3-안티패턴-가이드"><a href="#3-안티패턴-가이드" class="headerlink" title="3) 안티패턴 가이드"></a>3) 안티패턴 가이드</h3><p>README에 명시된 대표 금지&#x2F;지양 항목은 다음과 같다.</p><ul><li>과도하게 흔한 폰트 선택(예: Inter&#x2F;system default) 남발</li><li>컬러 배경 위 저대비 회색 텍스트</li><li>pure black&#x2F;gray 위주 단조 팔레트</li><li>카드 안에 카드 중첩</li><li>bounce&#x2F;elastic easing 남용</li></ul><p>이런 금지 목록은 생각보다 효과적이다. 모델은 긍정 지시만 있을 때보다, 명시적 금지 규칙이 함께 있을 때 결과물의 품질 편차가 줄어드는 편이다.</p><h2 id="어떤-도구를-지원하나"><a href="#어떤-도구를-지원하나" class="headerlink" title="어떤 도구를 지원하나"></a>어떤 도구를 지원하나</h2><p>공식 README 기준으로 다음을 지원한다.</p><ul><li>Cursor</li><li>Claude Code</li><li>Gemini CLI</li><li>Codex CLI</li></ul><p>각 도구별 설치 경로(<code>.cursor</code>, <code>.claude</code>, <code>.gemini</code>, <code>.codex</code>)가 다르고, 일부 도구는 별도 기능 활성화가 필요하다(예: Cursor Nightly&#x2F;Agent Skills, Gemini preview + Skills enable). 자세한 설정 단계는 프로젝트 README를 참고하면 된다.</p><h2 id="이-프로젝트의-실제-가치"><a href="#이-프로젝트의-실제-가치" class="headerlink" title="이 프로젝트의 실제 가치"></a>이 프로젝트의 실제 가치</h2><p>단순한 “디자인 프롬프트 모음”으로 보기엔 아깝다. 실무에서 의미 있는 포인트는 다음 세 가지다.</p><h3 id="1-리뷰-기준을-문서로-남겨-재사용"><a href="#1-리뷰-기준을-문서로-남겨-재사용" class="headerlink" title="1) 리뷰 기준을 문서로 남겨 재사용"></a>1) 리뷰 기준을 문서로 남겨 재사용</h3><p>좋은 디자이너가 한 번 코멘트해준 기준은 말로만 전달되면 팀에 남지 않는 경우가 많다. impeccable은 이런 기준을 명령과 레퍼런스 파일 형태로 남겨, 다음 작업에서도 같은 기준으로 다시 활용할 수 있게 만든다.</p><h3 id="2-AI-결과물의-변동폭을-줄임"><a href="#2-AI-결과물의-변동폭을-줄임" class="headerlink" title="2) AI 결과물의 변동폭을 줄임"></a>2) AI 결과물의 변동폭을 줄임</h3><p>같은 요구사항인데도 매번 다른 톤의 UI가 나오는 건 AI 코드 생성의 고질적인 문제다. 스킬+안티패턴 조합은 이 편차를 줄이는 데 효과적이다.</p><h3 id="3-“디자인-품질”을-작업-단계로-분해"><a href="#3-“디자인-품질”을-작업-단계로-분해" class="headerlink" title="3) “디자인 품질”을 작업 단계로 분해"></a>3) “디자인 품질”을 작업 단계로 분해</h3><p><code>audit → normalize → polish</code> 같은 흐름은 처음부터 완성도를 기대하기보다, 점검하고 고치면서 점점 다듬는 방식에 가깝다. 새로 합류한 팀원이 작업 방식을 익히기에도 부담이 적다.</p><h2 id="한계도-분명히-있다"><a href="#한계도-분명히-있다" class="headerlink" title="한계도 분명히 있다"></a>한계도 분명히 있다</h2><p>도입 전에 알아둘 점도 있다.</p><ul><li>특정 미학 취향이 과하게 고정될 수 있음</li><li>프로젝트 브랜드 톤이 약하면 스킬 기본값에 끌려갈 수 있음</li><li>명령이 많아 초반에는 “무엇부터 써야 하는지” 헷갈릴 수 있음</li></ul><p>그래서 <code>/teach-impeccable</code> 같은 초기 컨텍스트 수집 단계를 생략하면 효과가 크게 떨어질 수 있다.</p><h2 id="도입한다면-이렇게-시작하는-게-현실적"><a href="#도입한다면-이렇게-시작하는-게-현실적" class="headerlink" title="도입한다면 이렇게 시작하는 게 현실적"></a>도입한다면 이렇게 시작하는 게 현실적</h2><p>처음부터 전 화면에 적용하기보다, 변동폭이 큰 화면 1~2개(랜딩 Hero, 결제&#x2F;가입 폼 등)부터 쓰는 게 낫다.</p><p>추천 흐름은 아래 정도면 충분하다.</p><ol><li><code>/teach-impeccable</code>로 기본 컨텍스트 입력</li><li>기존 화면에 <code>/audit</code> + <code>/critique</code> 실행</li><li><code>/normalize</code>로 디자인 시스템 정합성 맞춤</li><li>배포 직전에 <code>/polish</code></li></ol><p>이렇게만 해도 “그럴듯한데 어딘가 촌스러운 UI”를 꽤 안정적으로 줄일 수 있다.</p><h2 id="정리"><a href="#정리" class="headerlink" title="정리"></a>정리</h2><p>impeccable은 AI가 코드를 잘 짜게 만드는 도구가 아니라, <strong>AI가 만드는 화면의 디자인 품질을 팀 기준에 맞추는 도구</strong>다.</p><p>AI가 만든 UI의 품질 편차 때문에 리뷰 비용이 크다면, 한번 실험해볼 만하다. 중요한 건 이 기능 저 기능 많이 쓰는 게 아니라 팀 컨텍스트를 먼저 이해하고 안티패턴을 명확히 잡아나가야 한다.</p><h2 id="참고-링크"><a href="#참고-링크" class="headerlink" title="참고 링크"></a>참고 링크</h2><ul><li><a href="https://github.com/pbakaus/impeccable">pbakaus&#x2F;impeccable (GitHub)</a></li><li><a href="https://impeccable.style/">impeccable.style</a></li><li><a href="https://github.com/pbakaus/impeccable/tree/main/source/skills/frontend-design">frontend-design skill source</a></li><li><a href="https://github.com/anthropics/skills/tree/main/skills/frontend-design">Anthropic frontend-design skill</a></li></ul><h3 id="이미지-출처"><a href="#이미지-출처" class="headerlink" title="이미지 출처"></a>이미지 출처</h3><ul><li>커버: <a href="https://www.pexels.com/photo/close-up-photography-of-people-using-laptop-3861969/">Photo by cottonbro studio on Pexels</a></li></ul>]]>
    </content>
    <id>https://uyeong.github.io/blog/2026/03/12/impeccable-ai-frontend-design-skill/</id>
    <link href="https://uyeong.github.io/blog/2026/03/12/impeccable-ai-frontend-design-skill/"/>
    <published>2026-03-12T08:50:00.000Z</published>
    <summary>impeccable은 Claude Code, Cursor, Gemini CLI, Codex CLI에서 쓸 수 있는 프론트엔드 디자인 스킬/명령 세트다. 구성 요소와 실무 적용 포인트를 정리했다.</summary>
    <title>impeccable은 어떤 프로젝트인가: AI 코딩 에이전트용 프론트엔드 디자인 스킬셋</title>
    <updated>2026-03-12T09:08:20.952Z</updated>
  </entry>
  <entry>
    <author>
      <name>Coderifleman</name>
    </author>
    <category term="JavaScript" scheme="https://uyeong.github.io/blog/categories/JavaScript/"/>
    <category term="JavaScript" scheme="https://uyeong.github.io/blog/tags/JavaScript/"/>
    <category term="ECMAScript" scheme="https://uyeong.github.io/blog/tags/ECMAScript/"/>
    <category term="TC39" scheme="https://uyeong.github.io/blog/tags/TC39/"/>
    <category term="Temporal" scheme="https://uyeong.github.io/blog/tags/Temporal/"/>
    <category term="Date" scheme="https://uyeong.github.io/blog/tags/Date/"/>
    <content>
      <![CDATA[<p>Temporal이 드디어 Stage 4에 도달했다. 새로운 API 하나가 추가됐다는 의미를 넘어 JavaScript에서 30년 넘게 쓰던 <code>Date</code>의 구조적 한계를 표준 레벨에서 바로잡는 계기가 됐다.</p><p>이번 글에서는 Bloomberg가 공개한 글을 시작점으로, TC39 문서&#x2F;Temporal 공식 문서까지 함께 읽고 다음을 정리해본다.</p><ul><li>왜 <code>Date</code>가 문제였는지</li><li>Temporal이 어떤 설계 원칙으로 문제를 풀었는지</li><li>타입을 어떻게 골라야 하는지</li><li>지금 코드베이스에서 어디부터 바꿔야 하는지</li></ul><h2 id="한-줄-결론부터"><a href="#한-줄-결론부터" class="headerlink" title="한 줄 결론부터"></a>한 줄 결론부터</h2><p>Temporal은 “Date 대체 후보”라기보다, <strong>시간 데이터를 용도에 맞게 나눠 다루는 표준 API 모음</strong>에 가깝다.</p><ul><li>저장&#x2F;전송 기준 시각은 <code>Temporal.Instant</code></li><li>사용자 지역 시간 계산은 <code>Temporal.ZonedDateTime</code></li><li>생일&#x2F;영업일&#x2F;매일 9시 같은 일정 기준 시간은 <code>Temporal.Plain*</code></li></ul><p>구분대로 기능을 잘 활용하면 DST 버그, 파싱 모호성, 가변 객체로 인한 부작용 같은 고질적인 이슈를 줄일 수 있다.</p><h2 id="Stage-4의-의미-표준-확정"><a href="#Stage-4의-의미-표준-확정" class="headerlink" title="Stage 4의 의미: 표준 확정"></a>Stage 4의 의미: 표준 확정</h2><p>TC39 프로세스에서 Stage 4는 제안이 최종 표준에 포함되는 단계다. <code>tc39/proposals</code>의 Finished Proposals에도 Temporal이 올라와 있고, ES2026 publication year로 표기되어 있다.</p><p>다만 문서별 반영 시점 차이는 있다. 예를 들어 <code>tc39/proposal-temporal</code> 저장소 일부 설명은 Stage 3 문구를 아직 포함하고 있을 수 있다. 이런 차이는 저장소 업데이트 타이밍 문제라, 실제 상태는 Finished Proposals와 구현&#x2F;출시 현황을 기준으로 보는 편이 정확하다.</p>    <div class="alert alert--info">      <strong class="alert__title">문서 읽을 때 헷갈리는 포인트</strong>      <div class="alert__body">        <p>Stage 상태는 단일 페이지만 보면 엇갈려 보일 수 있는데, 이럴 때는 <code>tc39/proposals</code>의 Finished Proposals와 엔진 출시 노트(Chrome&#x2F;Firefox 등)를 교차 확인하는 게 가장 안전하다.</p>      </div>    </div>  <h2 id="왜-Date는-계속-문제였을까"><a href="#왜-Date는-계속-문제였을까" class="headerlink" title="왜 Date는 계속 문제였을까"></a>왜 Date는 계속 문제였을까</h2><p>Bloomberg 글이 잘 짚은 대로, <code>Date</code>의 문제는 “사용법 미숙”보다 “설계 제약”으로 인해 발생한다.</p><h3 id="1-Mutable-모델"><a href="#1-Mutable-모델" class="headerlink" title="1) Mutable 모델"></a>1) Mutable 모델</h3><p><code>setMonth</code>, <code>setDate</code> 같은 메서드가 원본을 직접 바꾸기 때문에, 헬퍼 함수 하나 잘못 만들면 상위 로직까지 부작용이 전염된다.</p><h3 id="2-달-월-계산의-함정"><a href="#2-달-월-계산의-함정" class="headerlink" title="2) 달&#x2F;월 계산의 함정"></a>2) 달&#x2F;월 계산의 함정</h3><p>월 더하기 같은 달력 연산에서 overflow가 조용히 다음 달로 넘어가며 의도와 다른 결과를 만들기 쉽다.</p><h3 id="3-파싱-모호성"><a href="#3-파싱-모호성" class="headerlink" title="3) 파싱 모호성"></a>3) 파싱 모호성</h3><p>“ISO와 비슷하지만 완전히 같진 않은 문자열”을 엔진별로 다르게 해석하던 역사가 길다. 예를 들어 브라우저, Node.js, 테스트 런타임이 혼재된 환경에서는 같은 입력이 다르게 파싱돼 재현이 어려운 버그가 생길 수 있다.</p><h3 id="4-타임존-DST가-외부-의존"><a href="#4-타임존-DST가-외부-의존" class="headerlink" title="4) 타임존&#x2F;DST가 외부 의존"></a>4) 타임존&#x2F;DST가 외부 의존</h3><p>정확한 시간 계산을 하려면 결국 별도 라이브러리와 IANA 데이터를 의존해야 하고 결국 번들&#x2F;운영 복잡도가 올라간다.</p><h2 id="Temporal-설계-핵심-타입을-나눠서-의미를-고정한다"><a href="#Temporal-설계-핵심-타입을-나눠서-의미를-고정한다" class="headerlink" title="Temporal 설계 핵심: 타입을 나눠서 의미를 고정한다"></a>Temporal 설계 핵심: 타입을 나눠서 의미를 고정한다</h2><p>Temporal의 본질은 “하나의 Date 타입으로 모든 걸 처리”하는 방식을 버린 데 있다. 목적에 맞는 타입을 먼저 고르고, 그 타입이 허용하는 연산만 하면 된다.</p>    <figure title="Temporal 타입 선택 가이드">      <a href="/blog/images/temporal-stage4-es2026/types-map.svg" target="_blank">        <img src="/blog/images/temporal-stage4-es2026/types-map.svg" alt="Temporal 타입 선택 가이드">      </a>      <figcaption>&lt;Temporal 타입 선택 가이드&gt;</figcaption>    </figure>  <h3 id="Temporal-Instant"><a href="#Temporal-Instant" class="headerlink" title="Temporal.Instant"></a><code>Temporal.Instant</code></h3><ul><li>UTC 기준 절대 시각</li><li>저장&#x2F;이벤트 로그&#x2F;서버 간 교환에 유리</li><li>나노초 정밀도 지원</li></ul><h3 id="Temporal-ZonedDateTime"><a href="#Temporal-ZonedDateTime" class="headerlink" title="Temporal.ZonedDateTime"></a><code>Temporal.ZonedDateTime</code></h3><ul><li>절대 시각 + 타임존 + 캘린더</li><li>DST 경계 연산 안전성이 중요할 때 기본 선택지</li></ul><h3 id="Temporal-PlainDate-PlainTime-PlainDateTime"><a href="#Temporal-PlainDate-PlainTime-PlainDateTime" class="headerlink" title="Temporal.PlainDate, PlainTime, PlainDateTime"></a><code>Temporal.PlainDate</code>, <code>PlainTime</code>, <code>PlainDateTime</code></h3><ul><li>타임존 없는 wall time</li><li>생일, 매월 결제일, “매일 09:00” 같은 도메인 값에 적합</li></ul><h3 id="Temporal-Duration"><a href="#Temporal-Duration" class="headerlink" title="Temporal.Duration"></a><code>Temporal.Duration</code></h3><ul><li>시점 간 차이나 연산 단위를 명시적으로 표현</li><li><code>total()</code> 같은 API로 단위 변환이 명확함</li></ul><h2 id="지금-코드에서-어디부터-바꾸면-좋을까"><a href="#지금-코드에서-어디부터-바꾸면-좋을까" class="headerlink" title="지금 코드에서 어디부터 바꾸면 좋을까"></a>지금 코드에서 어디부터 바꾸면 좋을까</h2><p>“전면 교체”부터 시작하면 실패할 확률이 높다. 대신 장애나 오류가 자주 나는 경로부터 범위를 좁혀 바꾸는 편이 낫다.</p><h3 id="1단계-저장-계층-정리"><a href="#1단계-저장-계층-정리" class="headerlink" title="1단계: 저장 계층 정리"></a>1단계: 저장 계층 정리</h3><ul><li>신규 저장값은 <code>Instant</code>(또는 ISO string of Instant) 기준으로 통일</li><li>기존 <code>Date</code> 직렬화 포맷과 혼용되는 경로 식별</li></ul><h3 id="2단계-표시-입력-계층-분리"><a href="#2단계-표시-입력-계층-분리" class="headerlink" title="2단계: 표시&#x2F;입력 계층 분리"></a>2단계: 표시&#x2F;입력 계층 분리</h3><ul><li>사용자 지역 시각 표시 로직은 <code>ZonedDateTime</code></li><li>도메인 날짜값(생일&#x2F;마감일)은 <code>PlainDate</code>로 모델링</li></ul><h3 id="3단계-위험-구간-교체"><a href="#3단계-위험-구간-교체" class="headerlink" title="3단계: 위험 구간 교체"></a>3단계: 위험 구간 교체</h3><ul><li>DST 경계 연산(정산, 예약, 알림)</li><li>문자열 파싱 분기 로직</li><li><code>Date</code> mutable 메서드에 의존한 유틸</li></ul><h3 id="4단계-런타임-지원-전략"><a href="#4단계-런타임-지원-전략" class="headerlink" title="4단계: 런타임 지원 전략"></a>4단계: 런타임 지원 전략</h3><ul><li>지원 브라우저&#x2F;런타임 매트릭스 점검</li><li>미지원 환경은 polyfill 전략 명시</li></ul><h2 id="Bloomberg-글에서-눈여겨볼-관점"><a href="#Bloomberg-글에서-눈여겨볼-관점" class="headerlink" title="Bloomberg 글에서 눈여겨볼 관점"></a>Bloomberg 글에서 눈여겨볼 관점</h2><p>Bloomberg 글은 단순 기능 소개를 넘어서, Temporal이 왜 “9년짜리 작업”이었는지 맥락을 잘 보여준다. 특히 실무 관점에서 눈에 띄는 건 세 가지다.</p><ol><li>표준 API가 실제 대규모 운영 문제(시간대, 캘린더, 정밀도)에서 출발했다는 점</li><li>Bloomberg, Google, Igalia, Mozilla 등 복수 조직이 장기간 협업하고, <code>temporal_rs</code>처럼 V8을 포함한 여러 엔진이 공유하는 Rust 구현 기반까지 갖췄다는 점</li><li>Test262 테스트 규모도 매우 커서, 표준화–구현–검증 체인이 비교적 단단하게 만들어졌다는 점</li></ol><p>이건 팀에 Temporal 도입을 설득할 때도 쓸 수 있는 근거다. “새 문법”이 아니라 “기존 시간 모델의 구조적 리스크를 줄이는 투자”로 이야기할 수 있기 때문이다.</p><h2 id="마무리"><a href="#마무리" class="headerlink" title="마무리"></a>마무리</h2><p>Temporal Stage 4를 환영한다. 우리는 이제 시간 데이터를 더 이상 Date 하나로 머리싸매며 다루지 않아도 된다.</p><p>아래와 같은 정책을 다음 스프린트에서 부담 없이 바로 적용해보자.</p><ul><li><code>Date</code> 신규 사용 금지 룰 추가</li><li>신규 시각 저장은 <code>Instant</code> 우선</li><li>타임존 연산 경로를 <code>ZonedDateTime</code> 후보로 분리</li></ul><p>작게 시작해도 효과는 빨리 체감될 가능성이 높다. 시간 버그는 늦게 발견될수록 비싸다.</p><h2 id="참고-링크"><a href="#참고-링크" class="headerlink" title="참고 링크"></a>참고 링크</h2><ul><li><a href="https://bloomberg.github.io/js-blog/post/temporal/">Temporal: The 9-Year Journey to Fix Time in JavaScript (Bloomberg)</a></li><li><a href="https://github.com/tc39/proposals/blob/main/finished-proposals.md">TC39 Finished Proposals</a></li><li><a href="https://github.com/tc39/proposal-temporal">Temporal Proposal Repository</a></li><li><a href="https://tc39.es/proposal-temporal/docs/">Temporal Documentation</a></li><li><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal">MDN Temporal</a></li></ul><h3 id="이미지-출처"><a href="#이미지-출처" class="headerlink" title="이미지 출처"></a>이미지 출처</h3><ul><li>커버: <a href="https://www.pexels.com/photo/gray-and-black-pocket-watch-1252869/">Photo by Pixabay on Pexels</a></li><li>본문 이미지: 직접 제작 (SVG)</li></ul>]]>
    </content>
    <id>https://uyeong.github.io/blog/2026/03/12/temporal-stage4-es2026/</id>
    <link href="https://uyeong.github.io/blog/2026/03/12/temporal-stage4-es2026/"/>
    <published>2026-03-12T07:10:00.000Z</published>
    <summary>Temporal이 Stage 4에 도달하면서 ES2026 포함이 사실상 확정됐다. Date의 구조적 한계를 짚고 Temporal 타입별 실무 적용 전략을 정리한다.</summary>
    <title>Temporal Stage 4 확정: ES2026에서 Date를 대체하는 방법</title>
    <updated>2026-03-12T08:41:48.635Z</updated>
  </entry>
  <entry>
    <author>
      <name>Coderifleman</name>
    </author>
    <category term="Browser" scheme="https://uyeong.github.io/blog/categories/Browser/"/>
    <category term="Chrome" scheme="https://uyeong.github.io/blog/tags/Chrome/"/>
    <category term="Release-Note" scheme="https://uyeong.github.io/blog/tags/Release-Note/"/>
    <category term="WebGPU" scheme="https://uyeong.github.io/blog/tags/WebGPU/"/>
    <category term="Performance" scheme="https://uyeong.github.io/blog/tags/Performance/"/>
    <category term="Security" scheme="https://uyeong.github.io/blog/tags/Security/"/>
    <content>
      <![CDATA[<p>이번 글에서는 Chrome 146 릴리스 내용 중 실제 제품 코드에 영향을 줄 수 있는 변경 내용을 간추려서 정리해 소개한다.</p><h2 id="핵심-요약-빠르게-보기"><a href="#핵심-요약-빠르게-보기" class="headerlink" title="핵심 요약 (빠르게 보기)"></a>핵심 요약 (빠르게 보기)</h2><ol><li><strong>Scroll-triggered animations</strong>: 스크롤 위치 기반 애니메이션 트리거를 CSS 중심으로 작성 가능해졌다.</li><li><strong>trigger-scope</strong>: 애니메이션 트리거 이름 충돌을 스코프 단위로 제어할 수 있다.</li><li><strong>meta name&#x3D;”text-scale”</strong>: OS&#x2F;브라우저 텍스트 스케일과의 연동 방식이 더 명확해졌다.</li><li><strong>PWA LaunchParams 개선</strong>: 파일 실행 시 <code>targetURL</code>이 안정적으로 전달되고, reload 시 launchQueue 중복 전달이 방지된다.</li><li><strong>WebGPU 확장</strong>: transient attachment, compatibility mode 등으로 적용 범위가 넓어졌다.</li><li><strong>LCP candidate 동작 조정</strong>: “painted image” 기준으로 후보 방출 방식이 바뀌었다.</li><li><strong>Sanitizer API</strong>: 사용자 입력 HTML을 안전하게 정제하는 API가 안정 채널에 포함됐다.</li></ol><h2 id="실무-영향이-큰-변경-7가지"><a href="#실무-영향이-큰-변경-7가지" class="headerlink" title="실무 영향이 큰 변경 7가지"></a>실무 영향이 큰 변경 7가지</h2><h3 id="1-Scroll-triggered-animations-JS-의존도를-낮출-수-있다"><a href="#1-Scroll-triggered-animations-JS-의존도를-낮출-수-있다" class="headerlink" title="1) Scroll-triggered animations: JS 의존도를 낮출 수 있다"></a>1) Scroll-triggered animations: JS 의존도를 낮출 수 있다</h3><p>그동안 스크롤 진입 애니메이션은 <code>IntersectionObserver</code> + class 토글로 처리하는 경우가 많았다. 146에서 소개된 scroll-position 기반 트리거는 이 패턴 일부를 CSS 선언으로 대체할 수 있게 한다.</p><p>의미는 단순하다. “스크롤 위치를 감지해서 애니메이션을 시작&#x2F;정지”하는 로직을 자바스크립트 이벤트 핸들러가 아니라 CSS 선언으로 옮길 수 있다는 뜻이다. 복잡한 인터랙션이 아니라면 유지보수 포인트가 줄어든다.</p><h3 id="2-trigger-scope-대규모-프로젝트에서-네이밍-충돌을-줄인다"><a href="#2-trigger-scope-대규모-프로젝트에서-네이밍-충돌을-줄인다" class="headerlink" title="2) trigger-scope: 대규모 프로젝트에서 네이밍 충돌을 줄인다"></a>2) trigger-scope: 대규모 프로젝트에서 네이밍 충돌을 줄인다</h3><p>여러 컴포넌트가 동일 페이지에서 애니메이션 트리거 이름을 공유하면, 의도치 않은 충돌이 생길 수 있다. <code>trigger-scope</code>는 이 범위를 제한해서 충돌 위험을 낮춘다.</p><p>“애니메이션 네임스페이스 관리”를 문서 규칙에만 의존하지 않아도 되기 때문에 디자인 시스템이나 마이크로 프론트엔드 구조에서는 매우 유용한 변경이다.</p><h3 id="3-text-scale-메타-접근성-대응-방식이-더-명확해졌다"><a href="#3-text-scale-메타-접근성-대응-방식이-더-명확해졌다" class="headerlink" title="3) text-scale 메타: 접근성 대응 방식이 더 명확해졌다"></a>3) text-scale 메타: 접근성 대응 방식이 더 명확해졌다</h3><p><code>meta name=&quot;text-scale&quot;</code>은 OS&#x2F;브라우저 텍스트 스케일을 더 일관되게 반영할 수 있게 돕는다. 만일 rem&#x2F;em 중심으로 잘 만든 페이지라면 사용자 선호 폰트 크기와 맞물려 자연스럽게 확장되도록 유도한다.</p><p>접근성을 신경쓰고 있었다면 새 동작을 꼭 확인해보자. 기존 zoom&#x2F;autosizing 전제와 실제 렌더 결과가 달라질 수 있다.</p><h3 id="4-PWA-Launch-Handler-파일-실행-흐름이-안정화됐다"><a href="#4-PWA-Launch-Handler-파일-실행-흐름이-안정화됐다" class="headerlink" title="4) PWA Launch Handler: 파일 실행 흐름이 안정화됐다"></a>4) PWA Launch Handler: 파일 실행 흐름이 안정화됐다</h3><p>두 가지 수정이 특히 중요하다.</p><ul><li>파일 핸들링으로 PWA가 열릴 때 <code>LaunchParams.targetURL</code>을 더 일관되게 전달</li><li>새로고침(reload) 시 기존 launchQueue가 다시 재전달되던 동작 방지</li></ul><p>PWA 데스크톱 앱 경험에서 “같은 파일이 다시 열린 것처럼 보이는 문제”를 겪었다면 이 변경이 직접적인 개선으로 이어질 수 있다.</p>    <div class="alert alert--info">      <strong class="alert__title">무슨 문제였나?</strong>      <div class="alert__body">        <p>예를 들어 파일 연결로 PWA를 열었을 때 <code>launchQueue</code>로 전달된 파일 핸들을 앱이 처리한 뒤 페이지를 새로고침하면, 기존에는 같은 <code>LaunchParams</code>가 다시 전달되는 경우가 있었다. 그 결과 사용자는 파일을 다시 열지 않았는데도 같은 문서가 한 번 더 로드되거나, “최근 파일” 목록에 중복 항목이 생기는 현상을 겪을 수 있었다. Chrome 146에서는 이 동작을 “새 실행”이 아니라 일반 reload로 다루도록 바꿔서 중복 처리 가능성을 줄였다.</p>      </div>    </div>  <h3 id="5-WebGPU-성능과-디바이스-커버리지-둘-다-확장"><a href="#5-WebGPU-성능과-디바이스-커버리지-둘-다-확장" class="headerlink" title="5) WebGPU: 성능과 디바이스 커버리지 둘 다 확장"></a>5) WebGPU: 성능과 디바이스 커버리지 둘 다 확장</h3><p>146의 WebGPU 관련 변경으로는 단순 문법 추가를 넘어 적용 범위를 넓혔다.</p><ul><li><strong>Transient attachments</strong>: 타일 메모리 활용으로 VRAM 트래픽&#x2F;할당 부담을 줄일 여지</li><li><strong>Compatibility mode</strong>: 구형 그래픽 API(OpenGL, D3D11) 환경까지 일부 확장</li><li>WGSL <code>texture_and_sampler_let</code>: 코드 표현력 개선</li></ul><p>게임&#x2F;시각화&#x2F;3D 툴 같은 워크로드라면 “성능 최적화”와 “지원 디바이스 범위”를 같이 점검할 타이밍이다.</p><h3 id="6-LCP-후보-기록-규칙-변경-모니터링-해석을-재점검해야-한다"><a href="#6-LCP-후보-기록-규칙-변경-모니터링-해석을-재점검해야-한다" class="headerlink" title="6) LCP 후보 기록 규칙 변경: 모니터링 해석을 재점검해야 한다"></a>6) LCP 후보 기록 규칙 변경: 모니터링 해석을 재점검해야 한다</h3><p>LCP candidate가 “largest pending image” 영향에서 벗어나, 실제로 페인트된 요소 기준으로 더 자주 중간 후보가 기록될 수 있게 바뀌었다.</p>    <div class="alert alert--info">      <strong class="alert__title">LCP 후보 기록이란?</strong>      <div class="alert__body">        <p>LCP는 페이지가 로딩되는 동안 “지금까지 가장 큰 콘텐츠”가 바뀔 때마다 후보를 남긴다. 예를 들어 작은 텍스트가 먼저 보였다가, 더 큰 이미지가 나중에 보이면 후보가 갱신된다. Chrome 146에서는 아직 로딩 중인 큰 이미지를 기준으로 후보 기록을 늦추기보다, 실제로 화면에 그려진 요소를 기준으로 후보를 더 자주 기록해 로딩 진행 과정을 더 잘 보이게 한다.</p>      </div>    </div>  <p>즉, RUM 대시보드에서 candidate 이벤트 흐름을 후처리해 쓰고 있었다면 기존 해석 로직이 그대로 맞지 않을 수 있다. 특히 “중간 후보 수 증가”를 성능 악화로 오해하지 않도록 대시보드 설명을 손봐두는 편이 안전하다.</p><h3 id="7-Sanitizer-API-HTML-입력-처리-전략을-단순화할-기회"><a href="#7-Sanitizer-API-HTML-입력-처리-전략을-단순화할-기회" class="headerlink" title="7) Sanitizer API: HTML 입력 처리 전략을 단순화할 기회"></a>7) Sanitizer API: HTML 입력 처리 전략을 단순화할 기회</h3><p>사용자 입력 HTML 정제는 XSS 방어의 핵심인데, 그동안은 라이브러리 의존이 사실상 기본이었다. Sanitizer API는 브라우저 차원의 표준 API로 이 문제를 다루려는 방향이다.</p><p>바로 기존 라이브러리를 걷어내기보다는, 다음 순서로 접근하는 편이 현실적이다.</p><ol><li>현재 sanitize 정책(허용 태그&#x2F;속성) 문서화</li><li>Sanitizer API 동작과 정책 차이 비교</li><li>특정 경로(예: 코멘트&#x2F;리치 텍스트 미리보기)부터 점진 도입</li></ol><p>이전에 정리한 <a href="/blog/2026/03/11/sanitizer-api-firefox-148/">Firefox 148의 Sanitizer API: innerHTML 대체를 진짜로 고민할 시점</a> 글을 통해서도 관련 정보를 찾아볼 수 있다. 함께 보면 브라우저 간 흐름을 파악하는 데 도움을 줄 수 있으니 참고한다.</p><h2 id="적용할-때-주의할-점"><a href="#적용할-때-주의할-점" class="headerlink" title="적용할 때 주의할 점"></a>적용할 때 주의할 점</h2><p>새 기능이 많을수록 “지원 여부”보다 “운영 전제가 바뀌는지”를 먼저 봐야 한다.</p><ul><li>성능: LCP 수집 파이프라인이 후보 증가를 어떻게 처리하는지</li><li>접근성: text scale 변경이 레이아웃 파손 없이 동작하는지</li><li>PWA: launchQueue 관련 회귀(regression)가 사라졌는지</li><li>보안: Sanitizer API 도입 시 기존 정책과 충돌 없는지</li></ul><p>릴리스 노트의 가치는 기능 목록 자체보다, 현재 서비스 구조와 어디서 충돌하는지를 빨리 찾는 데 있다.</p><h2 id="마무리"><a href="#마무리" class="headerlink" title="마무리"></a>마무리</h2><p>Chrome 146은 “새 API 몇 개 추가”로만 보기엔 아깝다. CSS 선언형 인터랙션, PWA 실행 안정성, WebGPU 확장, 성능&#x2F;보안 지표까지 실제 운영에 영향을 주는 변화가 고르게 들어가 있다.</p><p>실무에서는 욕심내서 전부 도입하기보다, 현재 제품에서 영향이 큰 축 한두 개만 먼저 골라 검증해보자.</p><h2 id="참고-링크"><a href="#참고-링크" class="headerlink" title="참고 링크"></a>참고 링크</h2><ul><li><a href="https://developer.chrome.com/release-notes/146">Chrome 146 Release Notes</a></li><li><a href="https://drafts.csswg.org/css-animations-2/#timeline-triggers">Scroll-triggered animations (Spec)</a></li><li><a href="https://github.com/gpuweb/gpuweb/blob/main/proposals/compatibility-mode.md">WebGPU Compatibility mode proposal</a></li><li><a href="https://github.com/w3c/largest-contentful-paint/pull/154">Largest Contentful Paint spec PR</a></li><li><a href="https://wicg.github.io/sanitizer-api/">Sanitizer API Spec</a></li></ul><h3 id="이미지-출처"><a href="#이미지-출처" class="headerlink" title="이미지 출처"></a>이미지 출처</h3><ul><li>커버: <a href="https://www.pexels.com/photo/black-and-gray-laptop-computer-546819/">Photo by Pixabay on Pexels</a></li></ul>]]>
    </content>
    <id>https://uyeong.github.io/blog/2026/03/12/chrome-146-release-notes-highlights/</id>
    <link href="https://uyeong.github.io/blog/2026/03/12/chrome-146-release-notes-highlights/"/>
    <published>2026-03-12T05:22:00.000Z</published>
    <summary>Chrome 146 릴리스 노트를 기준으로 CSS, PWA, WebGPU, 성능, 보안 변경사항 중 실무 영향이 큰 항목을 골라 정리했다.</summary>
    <title>Chrome 146 릴리스 노트, 실무에서 바로 볼 포인트 7가지</title>
    <updated>2026-03-12T05:30:45.436Z</updated>
  </entry>
  <entry>
    <author>
      <name>Coderifleman</name>
    </author>
    <category term="Node.js" scheme="https://uyeong.github.io/blog/categories/Node-js/"/>
    <category term="Node.js" scheme="https://uyeong.github.io/blog/tags/Node-js/"/>
    <category term="LTS" scheme="https://uyeong.github.io/blog/tags/LTS/"/>
    <category term="Release" scheme="https://uyeong.github.io/blog/tags/Release/"/>
    <category term="Backend" scheme="https://uyeong.github.io/blog/tags/Backend/"/>
    <content>
      <![CDATA[<p>Node.js를 운영 환경에서 쓰고 있다면, 이번 릴리스 모델 변경은 짚고 넘어가야 한다.<br>핵심은 간단하다. <strong>Node.js 27부터 메이저 버전이 연 1회로 바뀌고, 매년 나온 버전이 같은 해 10월 LTS로 승격된다.</strong></p><p>즉, 지금까지의 “짝수는 LTS, 홀수는 비LTS” 구분이 사라진다.</p><h2 id="왜-바꾸는-걸까"><a href="#왜-바꾸는-걸까" class="headerlink" title="왜 바꾸는 걸까"></a>왜 바꾸는 걸까</h2><p>Node.js 릴리스 팀이 공개한 배경은 이렇다.</p><ul><li>홀수 버전 채택률이 낮다. 대부분 조직은 LTS 중심으로만 업그레이드한다.</li><li>홀수&#x2F;짝수 모델이 입문자와 운영 관점에서 되려 헷갈린다.</li><li>동시에 유지해야 할 릴리스 라인이 많아질수록 보안 패치 백포팅(backporting) 부담이 커진다.</li></ul><p>Node.js 유지보수는 커뮤니티 기여에 크게 의존하기 때문에, 릴리스 라인이 4~5개로 늘어나는 구간을 줄이고 실제 사용자가 많은 라인에 집중하려는 의도다.</p><h2 id="새-릴리스-모델-한눈에-보기"><a href="#새-릴리스-모델-한눈에-보기" class="headerlink" title="새 릴리스 모델 한눈에 보기"></a>새 릴리스 모델 한눈에 보기</h2>    <figure title="Node.js 릴리스 전환 타임라인">      <a href="/blog/images/nodejs-annual-lts-release-schedule/timeline.svg" target="_blank">        <img src="/blog/images/nodejs-annual-lts-release-schedule/timeline.svg" alt="Node.js 릴리스 전환 타임라인">      </a>      <figcaption>&lt;Node.js 릴리스 전환 타임라인 (2026~2027)&gt;</figcaption>    </figure>  <p>2026년 10월부터 적용되는 새 주기는 다음 3단계다.</p><ol><li><strong>Alpha Phase (10월~3월, 6개월)</strong><ul><li>semver-major(브레이킹 체인지) 허용</li><li>라이브러리&#x2F;프레임워크가 미리 호환성 검증하기 좋은 구간</li><li>Nightly와 달리 서명·태그가 붙고, <a href="https://github.com/nodejs/citgm">CITGM</a> 테스트를 거친다</li></ul></li><li><strong>Current Phase (4월~10월, 6개월)</strong><ul><li>브레이킹 체인지 없이 안정화하는 단계</li></ul></li><li><strong>LTS Phase (30개월)</strong><ul><li>장기 지원</li></ul></li></ol><p>총 지원 기간은 기존과 비슷하게 <strong>Current 시작 시점부터 약 36개월</strong>이다.</p><h2 id="기존-모델과-달라지는-지점"><a href="#기존-모델과-달라지는-지점" class="headerlink" title="기존 모델과 달라지는 지점"></a>기존 모델과 달라지는 지점</h2><h3 id="1-홀수-짝수-구분을-신경-쓸-일이-줄어든다"><a href="#1-홀수-짝수-구분을-신경-쓸-일이-줄어든다" class="headerlink" title="1) 홀수&#x2F;짝수 구분을 신경 쓸 일이 줄어든다"></a>1) 홀수&#x2F;짝수 구분을 신경 쓸 일이 줄어든다</h3><p>이전에는 운영팀이 짝수 LTS만 추적하는 게 일반적이었다. 앞으로는 <strong>매년 1개 메이저가 나오고, 그 라인이 같은 해 10월 LTS로 승격</strong>되기 때문에 버전 선택 단순해진다.</p><h3 id="2-버전-번호가-달력-연도와-맞물린다"><a href="#2-버전-번호가-달력-연도와-맞물린다" class="headerlink" title="2) 버전 번호가 달력 연도와 맞물린다"></a>2) 버전 번호가 달력 연도와 맞물린다</h3><p>Node.js 27은 2027년에 Current로 나온다. Node.js 28은 2028년, 이런 식으로 직관적으로 읽힌다.</p><h3 id="3-조기-검증의-중심이-Alpha-채널로-이동한다"><a href="#3-조기-검증의-중심이-Alpha-채널로-이동한다" class="headerlink" title="3) 조기 검증의 중심이 Alpha 채널로 이동한다"></a>3) 조기 검증의 중심이 Alpha 채널로 이동한다</h3><p>기존 홀수 릴리스가 맡던 “미리 부딪혀 보는 창구” 역할을 Alpha가 담당한다. 공식 안내에서도 라이브러리 제작자에게 Alpha를 CI에 넣어달라고 강조한다. LTS에서만 테스트하면, 실제 사용자에게 영향이 생긴 뒤에야 문제를 발견할 수 있기 때문이다.</p><h2 id="전환-타임라인"><a href="#전환-타임라인" class="headerlink" title="전환 타임라인"></a>전환 타임라인</h2><p>v26이 마지막 구모델이고 v27이 첫 신모델이다. 공식 일정 기준으로 중요한 일정은 다음과 같다.</p><ul><li><strong>Node.js 26</strong>: 2026년 4월 출시 (기존 모델의 마지막 라인)</li><li><strong>Node.js 27 Alpha 시작</strong>: 2026년 10월</li><li><strong>Node.js 27.0.0(Current)</strong>: 2027년 4월</li><li><strong>Node.js 27 LTS 전환</strong>: 2027년 10월</li></ul><p>즉, 2026년 10월이 새 모델이 시작되는 시점이다. 라이브러리 유지보수자라면 이 시점부터 Alpha 트랙을 검토해 두는 편이 좋다.</p><h2 id="실무-관점에서의-대응"><a href="#실무-관점에서의-대응" class="headerlink" title="실무 관점에서의 대응"></a>실무 관점에서의 대응</h2><h3 id="애플리케이션-운영팀"><a href="#애플리케이션-운영팀" class="headerlink" title="애플리케이션 운영팀"></a>애플리케이션 운영팀</h3><p>애플리케이션 운영팀은 원래도 LTS 중심으로 움직였던 경우가 많아 업그레이드 횟수 자체는 크게 달라지지 않을 수 있다. 다만 Alpha~Current 기간에 최소한의 호환성 리허설을 미리 해두면 실제 전환 시 리스크를 줄이기 쉽다.</p><h3 id="라이브러리-프레임워크-유지보수팀"><a href="#라이브러리-프레임워크-유지보수팀" class="headerlink" title="라이브러리&#x2F;프레임워크 유지보수팀"></a>라이브러리&#x2F;프레임워크 유지보수팀</h3><p>라이브러리&#x2F;프레임워크 유지보수팀이라면 <strong>Alpha CI 트랙</strong>을 별도로 두는 것을 강하게 권장한다. 공식 발표에서도 라이브러리 저자에게 Alpha 단계부터 CI를 돌려달라고 요청하고 있고, 브레이킹 체인지가 Alpha 단계에서 주로 발생하는 만큼 이 구간에서 미리 확인해야 사용자 영향을 줄일 수 있다.</p><h3 id="조직-차원의-거버넌스"><a href="#조직-차원의-거버넌스" class="headerlink" title="조직 차원의 거버넌스"></a>조직 차원의 거버넌스</h3><p>조직 차원에서는 “LTS만 추적” 정책을 유지하되 사전 검증 체크포인트를 Alpha 시점으로 앞당겨야 한다. 특히 Native addon이나 런타임 의존성이 큰 서비스는 Alpha 단계에서 검증해 두면 전환 비용을 줄이는 데 도움이 된다.</p><h2 id="정리하며"><a href="#정리하며" class="headerlink" title="정리하며"></a>정리하며</h2><p>이번 변경은 Node.js 생태계가 실제 사용 패턴에 맞춰 **유지보수 지속 가능성(sustainability)**을 높이고자 이뤄졌다.</p><p>개발자 입장에서도 나쁘지 않다.<br>버전 전략이 단순해지면서, 어떤 버전을 기준으로 따라가야 하는지에 대한 팀 내 커뮤니케이션 비용도 줄어들 수 있다.</p><p>다만 한 가지 주의할 점은 Alpha 채널을 활용하지 않으면, 브레이킹 체인지로 인한 문제가 사용자에게 전해질 수 있다는 것이다.<br>새 릴리스 모델은 안정성을 자동으로 보장해 주는 구조가 아니라, 검증 시점을 앞당길 수 있도록 공식 채널을 마련한 구조다.</p><h2 id="참고-링크"><a href="#참고-링크" class="headerlink" title="참고 링크"></a>참고 링크</h2><ul><li>Node.js 공식 발표: <a href="https://nodejs.org/en/blog/announcements/evolving-the-nodejs-release-schedule">Evolving the Node.js Release Schedule</a></li><li>배경 논의 이슈: <a href="https://github.com/nodejs/Release/issues/1113">nodejs&#x2F;Release#1113</a></li><li>공식 지원 일정 데이터: <a href="https://github.com/nodejs/Release/blob/HEAD/schedule.json">nodejs&#x2F;Release&#x2F;schedule.json</a></li><li>Node.js EOL 정책 설명: <a href="https://nodejs.org/en/about/eol">Node.js End-Of-Life</a></li></ul><h3 id="이미지-출처"><a href="#이미지-출처" class="headerlink" title="이미지 출처"></a>이미지 출처</h3><ul><li>커버: <a href="https://commons.wikimedia.org/wiki/File:Fronalpstock_big.jpg">Fronalpstock, Switzerland (Wikimedia Commons, Photo by Diliff, CC BY-SA 3.0)</a></li><li>본문 타임라인: 직접 제작 (SVG)</li></ul>]]>
    </content>
    <id>https://uyeong.github.io/blog/2026/03/12/nodejs-annual-lts-release-schedule/</id>
    <link href="https://uyeong.github.io/blog/2026/03/12/nodejs-annual-lts-release-schedule/"/>
    <published>2026-03-12T02:11:00.000Z</published>
    <summary>2026년 10월부터 Node.js 릴리스 모델이 바뀐다. 매년 나오는 메이저 버전이 6개월 Current를 거친 뒤 LTS로 전환되는 새 사이클의 핵심과 실무 영향 포인트를 정리했다.</summary>
    <title>Node.js 릴리스, 이제 연 1회로 간다</title>
    <updated>2026-03-12T05:24:24.792Z</updated>
  </entry>
  <entry>
    <author>
      <name>Coderifleman</name>
    </author>
    <category term="JavaScript" scheme="https://uyeong.github.io/blog/categories/JavaScript/"/>
    <category term="Development" scheme="https://uyeong.github.io/blog/categories/JavaScript/Development/"/>
    <category term="Astro" scheme="https://uyeong.github.io/blog/tags/Astro/"/>
    <category term="JavaScript" scheme="https://uyeong.github.io/blog/tags/JavaScript/"/>
    <category term="Vite" scheme="https://uyeong.github.io/blog/tags/Vite/"/>
    <category term="Cloudflare" scheme="https://uyeong.github.io/blog/tags/Cloudflare/"/>
    <category term="Release Note" scheme="https://uyeong.github.io/blog/tags/Release-Note/"/>
    <content>
      <![CDATA[<p>Astro를 아직 모르는 사람을 위해 간단히 소개하자면 이렇다. Astro는 콘텐츠 중심 웹사이트에 맞춘 서버 우선(Server-First) 프레임워크이고, 필요한 곳에만 자바스크립트를 보내는 방식으로 성능과 개발 경험을 같이 잡고자 하는 도구다. 블로그, 마케팅 페이지, 문서 사이트처럼 “읽는 경험”이 중요한 서비스에서 특히 강점을 보인다.</p><p>이번 6.0 릴리스는 단순히 기능을 추가한 버전이 아닌 런타임 호환성과 플랫폼 확장성을 본격적으로 밀어붙인 버전으로 개발 서버 구조를 Vite Environment API 기반으로 재설계해 dev&#x2F;prod 런타임 간 격차를 줄이고, Fonts API·Live Content Collections·CSP 안정화로 실무에서 반복되던 불편함을 크게 개선했다.</p><h2 id="Astro-6-0에서-가장-중요한-변화"><a href="#Astro-6-0에서-가장-중요한-변화" class="headerlink" title="Astro 6.0에서 가장 중요한 변화"></a>Astro 6.0에서 가장 중요한 변화</h2><h3 id="1-개발-서버-개편-dev-prod-간-동작-차이-해소"><a href="#1-개발-서버-개편-dev-prod-간-동작-차이-해소" class="headerlink" title="1) 개발 서버 개편: dev&#x2F;prod 간 동작 차이 해소"></a>1) 개발 서버 개편: dev&#x2F;prod 간 동작 차이 해소</h3><p>이번 릴리스에서 가장 의미 있는 변화는 <code>astro dev</code> 재설계다. 핵심은 Vite의 Environment API를 기반으로 개발 환경에서도 실제 배포 런타임에 더 가깝게 실행할 수 있게 했다는 점이다.</p><p>그동안 Cloudflare Workers, Bun, Deno 같은 non-Node 런타임을 쓰는 팀은 “로컬에선 되는데 배포에서 깨지는” 걸 자주 경험했다. Astro 6은 이 격차를 줄이면서 Cloudflare 지원도 크게 강화했고, Cloudflare 어댑터는 개발&#x2F;프리렌더&#x2F;프로덕션 전 구간에서 workerd 기반 흐름을 더 일관되게 가져가도록 업데이트됐다.</p><h3 id="2-Built-in-Fonts-API-추가"><a href="#2-Built-in-Fonts-API-추가" class="headerlink" title="2) Built-in Fonts API 추가"></a>2) Built-in Fonts API 추가</h3><p>웹 폰트 최적화는 매번 손이 많이 가는 작업인데, Astro 6은 이 부분을 프레임워크 레벨에서 기본 제공한다. 로컬 파일이나 Google&#x2F;Fontsource 같은 공급자를 설정하면 다운로드&#x2F;캐시&#x2F;셀프호스팅&#x2F;프리로드 힌트&#x2F;fallback 생성까지 자동화해준다.</p><h3 id="3-Live-Content-Collections-안정화"><a href="#3-Live-Content-Collections-안정화" class="headerlink" title="3) Live Content Collections 안정화"></a>3) Live Content Collections 안정화</h3><p>Content Collections는 Astro의 핵심 기능 중 하나였지만, 기존에는 콘텐츠 변경 시 재빌드가 필요한 구조였다. Live Content Collections는 요청 시점(request-time)에 CMS&#x2F;API 데이터를 가져와 콘텐츠를 즉시 변경한다.</p><p>또한, 정적 빌드 기반 컬렉션과 라이브 컬렉션을 프로젝트에서 함께 쓸 수 있고, 신선도가 중요한 콘텐츠만 라이브로 분리하는 하이브리드 운영이 가능해졌다.</p><h3 id="4-CSP-Content-Security-Policy-지원-안정화"><a href="#4-CSP-Content-Security-Policy-지원-안정화" class="headerlink" title="4) CSP(Content Security Policy) 지원 안정화"></a>4) CSP(Content Security Policy) 지원 안정화</h3><p>CSP는 중요하지만 구현이 까다로워서 미루기 쉬운 주제인데, Astro 6은 이걸 프레임워크 차원에서 안정화(stable)했다. 정적&#x2F;동적 페이지, 서버&#x2F;서버리스 환경을 아우르는 설정 API를 제공하고, 기본값만 켜도 스크립트&#x2F;스타일 해시 처리를 자동으로 구성해준다.</p><p>이 포인트는 보안팀 규모가 크지 않은 팀에게 특히 실용적이다. 복잡한 수작업을 줄이고, 프로젝트 초기에 보안 기준선을 수립하기 쉬워진다.</p><h2 id="업그레이드-시-바로-확인할-체크포인트"><a href="#업그레이드-시-바로-확인할-체크포인트" class="headerlink" title="업그레이드 시 바로 확인할 체크포인트"></a>업그레이드 시 바로 확인할 체크포인트</h2><h3 id="런타임-도구-체인-변경"><a href="#런타임-도구-체인-변경" class="headerlink" title="런타임&#x2F;도구 체인 변경"></a>런타임&#x2F;도구 체인 변경</h3><ul><li>Node.js <strong>22.12+</strong> 필수 (Node 18&#x2F;20 지원 종료)</li><li>Vite 7 업그레이드</li><li>Shiki 4 업그레이드</li><li>Zod 4 업그레이드 (<code>astro/zod</code> 사용 권장)</li></ul><p>Zod 4에서는 에러 메시지 처리와 <code>transform + default</code> 동작이 바뀐 부분이 있어, 이 패턴을 쓰는 프로젝트는 별도 점검 시간이 필요하다.</p><h3 id="Cloudflare-Adapter-사용자라면"><a href="#Cloudflare-Adapter-사용자라면" class="headerlink" title="Cloudflare&#x2F;Adapter 사용자라면"></a>Cloudflare&#x2F;Adapter 사용자라면</h3><p>공식 어댑터가 major 업데이트됐고, Cloudflare 쪽은 변화폭이 큰 편이다. 기존 어댑터 설정이 있다면 changelog와 업그레이드 가이드를 먼저 확인하고, 런타임 바인딩(KV, D1, R2 등) 개발 흐름이 어떻게 바뀌는지 체크하는 게 좋다.</p><h2 id="실험-기능-Experimental-살펴보기"><a href="#실험-기능-Experimental-살펴보기" class="headerlink" title="실험 기능(Experimental) 살펴보기"></a>실험 기능(Experimental) 살펴보기</h2><ul><li>Rust 기반 새 컴파일러 (<code>@astrojs/compiler-rs</code>)</li><li>Queued Rendering (최대 2배 렌더링 개선 벤치마크 언급)</li><li>Route Caching API (SSR 캐시를 플랫폼 중립적으로 제어)</li></ul><p>실험 기능은 구미에 따라 상황에 맞춰 PoC로 검증해보자. 특히 대형 사이트나 SSR 트래픽이 많은 서비스라면 렌더링&#x2F;캐시 기능의 체감이 클 수 있다.</p><h2 id="정리"><a href="#정리" class="headerlink" title="정리"></a>정리</h2><p>Astro 6.0은 실전에서 부딪히는 운영 문제를 최대한 줄이는데 초점이 맞춰 이뤄진 것 같다. dev&#x2F;prod 런타임 간 불일치, 폰트 최적화 반복 작업, 콘텐츠 신선도, CSP 적용 난이도 같은 문제를 한 번에 묶어 개선하려 했다는 점에서 업그레이드 가치는 충분하다.</p><p>Astro를 쓰고 있다면 업그레이드를 미루지 말고 Node 22+ 환경부터 맞춘 뒤, 어댑터&#x2F;스키마 영향 범위를 먼저 점검하고 단계적으로 올려보도록 하자.</p><h2 id="참고-링크"><a href="#참고-링크" class="headerlink" title="참고 링크"></a>참고 링크</h2><ul><li><a href="https://astro.build/blog/astro-6/">Astro 6.0 Release Post</a></li><li><a href="https://docs.astro.build/en/guides/upgrade-to/v6/">Upgrade to Astro v6</a></li><li><a href="https://astro.build/">Astro 공식 사이트</a></li></ul><h3 id="이미지-출처"><a href="#이미지-출처" class="headerlink" title="이미지 출처"></a>이미지 출처</h3><ul><li>커버: <a href="https://astro.build/blog/astro-6/">Astro 6.0 공식 OG 이미지</a></li></ul>]]>
    </content>
    <id>https://uyeong.github.io/blog/2026/03/11/astro-6-release-highlights/</id>
    <link href="https://uyeong.github.io/blog/2026/03/11/astro-6-release-highlights/"/>
    <published>2026-03-10T15:00:00.000Z</published>
    <summary>Astro 프로젝트 소개부터 Astro 6.0의 핵심 변화까지 짧고 밀도 있게 정리한다. 개발 서버 개편, Fonts API, Live Content Collections, CSP 안정화, 런타임/패키지 업그레이드, 실험 기능까지 한 번에 훑는다.</summary>
    <title>Astro 6.0 핵심 정리: 무엇이 달라졌고, 무엇을 준비해야 하나</title>
    <updated>2026-03-11T09:32:08.198Z</updated>
  </entry>
  <entry>
    <author>
      <name>Coderifleman</name>
    </author>
    <category term="Development" scheme="https://uyeong.github.io/blog/categories/Development/"/>
    <category term="React" scheme="https://uyeong.github.io/blog/categories/Development/React/"/>
    <category term="AI" scheme="https://uyeong.github.io/blog/tags/AI/"/>
    <category term="React" scheme="https://uyeong.github.io/blog/tags/React/"/>
    <category term="Cursor" scheme="https://uyeong.github.io/blog/tags/Cursor/"/>
    <category term="Developer Experience" scheme="https://uyeong.github.io/blog/tags/Developer-Experience/"/>
    <category term="Claude Code" scheme="https://uyeong.github.io/blog/tags/Claude-Code/"/>
    <content>
      <![CDATA[<p>코딩 에이전트로 프런트엔드 작업을 해보면 비슷한 순간이 반복된다. “이 버튼 좀 더 키워줘” 같은 요청은 간단해 보이는데, 정작 에이전트는 먼저 파일을 찾고 컴포넌트를 추적하느라 시간을 꽤 쓴다. 결국 문제는 수정 능력이 아니라 탐색 단계에서 생기고, 프롬프트를 아무리 잘 써도 UI에서 코드까지 가는 번역 과정이 길면 체감 속도가 쉽게 떨어진다.</p><p>관련하여 오늘은 <a href="https://github.com/aidenybai/react-grab/tree/main">react-grab</a>를 소개하고자 한다. 이 프로젝트의 핵심은 새로운 코드 생성 모델을 만드는 것이 아니라, 브라우저에서 보고 있는 UI 요소를 바로 코드 문맥으로 연결해 에이전트가 헤매는 구간을 줄이는 데 있다.</p>    <figure title="React Grab 데모">      <a href="/blog/images/react-grab-introduction/demo.gif" target="_blank">        <img src="/blog/images/react-grab-introduction/demo.gif" alt="React Grab 데모">      </a>      <figcaption>&lt;그림 1. UI 요소를 선택해 코드 문맥을 바로 복사하는 흐름&gt;</figcaption>    </figure>  <h2 id="React-Grab은-무엇을-해주는가"><a href="#React-Grab은-무엇을-해주는가" class="headerlink" title="React Grab은 무엇을 해주는가"></a>React Grab은 무엇을 해주는가</h2><p>React Grab은 개발 중인 웹페이지에서 특정 요소를 가리킨 뒤 <code>⌘C</code>(Mac) 또는 <code>Ctrl+C</code>(Windows&#x2F;Linux)를 누르면, 해당 요소의 문맥을 클립보드에 복사한다. 여기에는 요소 HTML 조각뿐 아니라 컴포넌트 이름, 파일 경로, 라인 정보가 함께 들어가므로 에이전트 입장에서는 “어디를 고쳐야 하는지”를 추측할 필요가 크게 줄어든다.</p><p>README에 나온 샘플을 보면 전달 방식이 꽤 직관적이다.</p><pre class="language-text" data-language="text"><code class="language-text">&lt;a class="ml-auto inline-block text-sm" href="#">  Forgot your password?&lt;/a>in LoginForm at components/login-form.tsx:46:19</code></pre><p>즉, 이 도구의 목표는 에이전트가 편집을 시작하기 전 가장 오래 걸리는 탐색 단계를 줄여, 실제 수정으로 바로 진입하게 만드는 데 있다.</p><h2 id="왜-체감-차이가-큰가"><a href="#왜-체감-차이가-큰가" class="headerlink" title="왜 체감 차이가 큰가"></a>왜 체감 차이가 큰가</h2><p>React Grab 팀이 소개한 글(<a href="https://react-grab.com/blog/intro">https://react-grab.com/blog/intro</a>)을 보면, 이 도구를 켠 경우와 끈 경우를 비교해 프런트엔드 수정 작업의 처리 시간이 평균적으로 크게 줄었다고 설명한다. 숫자 자체보다 중요한 건 원리인데, 기존에는 에이전트가 코드베이스를 검색해 후보를 좁혀 가야 했다면, React Grab을 쓰면 시작 좌표가 거의 고정되기 때문에 도구 호출 횟수와 왕복이 줄어든다.</p>    <figure title="미로 이미지">      <a href="/blog/images/react-grab-introduction/maze.jpg" target="_blank">        <img src="/blog/images/react-grab-introduction/maze.jpg" alt="미로 이미지" style="max-width:min(100%, 900px); width:auto; height:auto;">      </a>      <figcaption>&lt;그림 2. 탐색 단계가 길어질수록 에이전트가 우회하는 경로가 늘어난다&gt;</figcaption>    </figure>      <div class="alert alert--info">      <strong class="alert__title">핵심 관점</strong>      <div class="alert__body">        <p>React Grab이 “UI 편집기”로 보일 수도 있겠지만 정확히는 코딩 에이전트의 탐색 비용을 줄이는 문맥 전달 레이어에 가깝고, 그래서 코드 생성 품질보다 먼저 작업 흐름의 속도와 안정성에 영향을 준다.</p>      </div>    </div>  <h2 id="설치와-도입은-어느-정도로-가벼운가"><a href="#설치와-도입은-어느-정도로-가벼운가" class="headerlink" title="설치와 도입은 어느 정도로 가벼운가"></a>설치와 도입은 어느 정도로 가벼운가</h2><p>README 기준으로 가장 빠른 시작 방법은 <code>grab init</code>이다.</p><pre class="language-bash" data-language="bash"><code class="language-bash">npx <span class="token parameter variable">-y</span> grab@latest init</code></pre><p>MCP 연결은 아래처럼 별도 명령으로 붙일 수 있다.</p><pre class="language-bash" data-language="bash"><code class="language-bash">npx <span class="token parameter variable">-y</span> grab@latest <span class="token function">add</span> mcp</code></pre><p>수동 설치도 가능하고 방식도 어렵지 않다. README에 Next.js(App&#x2F;Pages Router), Vite, Webpack별 예시가 정리돼 있으니 참고한다.</p><h2 id="플러그인과-프리미티브가-의미하는-것"><a href="#플러그인과-프리미티브가-의미하는-것" class="headerlink" title="플러그인과 프리미티브가 의미하는 것"></a>플러그인과 프리미티브가 의미하는 것</h2><p>React Grab은 기본 기능 외에도 플러그인 API를 통해 컨텍스트 메뉴&#x2F;툴바 액션을 확장할 수 있고, 더 깊게 가면 <code>react-grab/primitives</code>로 직접 선택 UI를 구성하는 것도 가능하다.</p><p>이 구조가 실무에 도움을 준다. 처음에는 기본 기능으로 빠르게 도입하고, 팀이 자주 반복하는 작업이 보이면 플러그인으로 붙여 자동화하고, 필요하면 프리미티브로 사내 워크플로에 맞춘 커스텀 선택기를 만들 수 있다.</p><h2 id="도입-전에-알고-있으면-좋은-포인트"><a href="#도입-전에-알고-있으면-좋은-포인트" class="headerlink" title="도입 전에 알고 있으면 좋은 포인트"></a>도입 전에 알고 있으면 좋은 포인트</h2><p>첫째, 소스 위치 기반 문맥(source-location context)은 개발 모드에서 특히 강력하다. React의 동작 특성상 프로덕션에서는 소스 위치 정보가 제한될 수 있으므로, React Grab이 가장 빛나는 구간은 로컬 개발&#x2F;스테이징의 UI 반복 수정 루프다.</p><p>둘째, 이 도구는 에이전트를 대체하지 않는다. 대신 에이전트가 길을 찾는 시간을 줄여 주므로, 프롬프트 품질과 코드 리뷰는 여전히 중요하고 React Grab은 그 과정을 빠르게 만드는 보조 가속기에 가깝다.</p><p>셋째, 팀 도입 관점에서는 성능 지표를 간단히라도 잡는 편이 좋다. 예를 들어 “같은 유형의 UI 수정 10건에서 완료 시간”이나 “불필요한 검색 도구 호출 횟수”를 비교하면, 도입 효과를 감이 아니라 수치로 확인할 수 있다.</p><h2 id="마무리"><a href="#마무리" class="headerlink" title="마무리"></a>마무리</h2><p>React Grab은 거대한 프레임워크가 아니라 작은 연결 도구지만, 프런트엔드에서 코딩 에이전트를 자주 쓰는 팀이라면 체감 변화가 꽤 클 수 있다. 특히 “요소는 눈앞에 있는데 파일을 못 찾아서 시간 쓰는” 순간이 자주 나온다면, 이 도구는 그 병목을 정면으로 줄여 주고 결과적으로 반복 작업 리듬을 부드럽게 만든다.</p><h2 id="참고-링크"><a href="#참고-링크" class="headerlink" title="참고 링크"></a>참고 링크</h2><ul><li><a href="https://github.com/aidenybai/react-grab/tree/main">react-grab GitHub 저장소</a></li><li><a href="https://react-grab.com/blog/intro">React Grab 소개 글: I made your coding agent 3× faster at frontend</a></li><li><a href="https://www.npmjs.com/package/react-grab">NPM: react-grab</a></li><li><a href="https://github.com/aidenybai/react-grab/blob/main/packages/react-grab/src/types.ts">플러그인 타입 정의</a></li><li><a href="https://github.com/aidenybai/react-grab/blob/main/packages/react-grab/src/primitives.ts">프리미티브 API</a></li></ul><h3 id="이미지-출처"><a href="#이미지-출처" class="headerlink" title="이미지 출처"></a>이미지 출처</h3><ul><li>커버 이미지: <a href="https://commons.wikimedia.org/w/index.php?curid=52431635">Smartphone with navigation map app</a> — © Santeri Viinamäki, CC BY-SA 4.0</li><li>본문 은유 이미지: <a href="https://www.flickr.com/photos/77581941@N00/3538046700">Isotropic Spherical Maze II</a> — © vitroid, CC BY 2.0</li><li>데모 GIF: <a href="https://github.com/aidenybai/react-grab/tree/main">react-grab 저장소 README 데모</a></li></ul>]]>
    </content>
    <id>https://uyeong.github.io/blog/2026/03/11/react-grab-introduction/</id>
    <link href="https://uyeong.github.io/blog/2026/03/11/react-grab-introduction/"/>
    <published>2026-03-10T15:00:00.000Z</published>
    <summary>React Grab은 브라우저에서 선택한 UI 요소의 컴포넌트 정보와 소스 위치를 바로 복사해 코딩 에이전트의 탐색 시간을 줄여준다. 이 글에서는 React Grab이 어떤 문제를 푸는지, 왜 체감이 큰지, 실무에서 어떻게 도입하면 좋은지 정리한다.</summary>
    <title>React Grab: 코딩 에이전트가 UI 코드를 더 빨리 찾게 만드는 방법</title>
    <updated>2026-03-11T03:42:59.308Z</updated>
  </entry>
  <entry>
    <author>
      <name>Coderifleman</name>
    </author>
    <category term="Browser" scheme="https://uyeong.github.io/blog/categories/Browser/"/>
    <category term="JavaScript" scheme="https://uyeong.github.io/blog/tags/JavaScript/"/>
    <category term="Security" scheme="https://uyeong.github.io/blog/tags/Security/"/>
    <category term="XSS" scheme="https://uyeong.github.io/blog/tags/XSS/"/>
    <category term="Firefox" scheme="https://uyeong.github.io/blog/tags/Firefox/"/>
    <category term="Sanitizer API" scheme="https://uyeong.github.io/blog/tags/Sanitizer-API/"/>
    <content>
      <![CDATA[<p>프런트엔드에서 XSS는 늘 “위험하다”고 말하지만, 막상 코드에는 <code>innerHTML</code>이 계속 남아 있는 경우가 많다. 이유는 단순하다. 위험한 건 아는데, 기존 동작을 깨지 않으면서 안전하게 바꾸는 방법이 애매하기 때문이다. Firefox 148에 Sanitizer API가 탑재됐다는 소식이 의미 있는 이유도 바로 여기에 있다.</p><p>관련해 이번에는 Mozilla 발표 글을 시작점으로, MDN의 <code>setHTML()</code> 문서, HTML Sanitizer API 문서, Trusted Types 문서, web.dev 자료까지 묶어서 핵심만 정리해보겠다.</p>    <div class="alert alert--info">      <strong class="alert__title">한 줄 요약</strong>      <div class="alert__body">        <p>Sanitizer API의 핵심은 “문자열을 DOM에 넣는 행위” 자체를 safer default로 바꾸는 데 있다. 즉, 보안 검수 후에 대응하는 게 아니라 개발자가 평소 쓰는 API 선택만으로 XSS 위험을 줄이는 방향이다.</p>      </div>    </div>  <h2 id="왜-innerHTML에서-자꾸-사고가-나는가"><a href="#왜-innerHTML에서-자꾸-사고가-나는가" class="headerlink" title="왜 innerHTML에서 자꾸 사고가 나는가"></a>왜 <code>innerHTML</code>에서 자꾸 사고가 나는가</h2><p>문제는 개발자의 부주의에 의해서만 생기지 않는다. <code>innerHTML</code>은 입력 문자열을 HTML로 해석하는 순간 강력한 주입 지점(injection sink)이 되므로 사용자 입력, 댓글, 마크다운 렌더 결과, 외부 API 응답 등 어느 한 군데라도 필터링이 어긋날 때 XSS가 발생할 수 있다.</p><p>Mozilla 글에서도 이 지점을 분명히 짚는다. 공격자가 HTML&#x2F;JS를 주입할 수 있으면 사용자 상호작용 감시, 데이터 탈취 같은 문제가 장기간 반복될 수 있고, XSS(CWE-79) 취약점은 여전히 상위권이다.</p><h2 id="setHTML-이-바꾸는-핵심"><a href="#setHTML-이-바꾸는-핵심" class="headerlink" title="setHTML()이 바꾸는 핵심"></a><code>setHTML()</code>이 바꾸는 핵심</h2><p><code>Element.setHTML()</code>은 “문자열 HTML 삽입”과 “sanitization”을 한 흐름으로 묶어 제공한다. 쉽게 말해, 개발자가 별도 라이브러리로 문자열을 먼저 정제하고 다시 넣는 대신, API 레벨에서 안전한 기본 방식을 제공한다.</p><p>MDN 기준으로 안전 메서드 계열은 다음과 같다.</p><ul><li><code>Element.setHTML()</code></li><li><code>ShadowRoot.setHTML()</code></li><li><code>Document.parseHTML()</code></li></ul><p>그리고 unsafe 계열(<code>setHTMLUnsafe</code> 등)도 분리되어 있어, 위험을 감수해야 하는 케이스를 코드 레벨에서 명시적으로 드러낼 수 있다.</p>    <figure title="Mozilla Hacks">      <a href="/blog/images/sanitizer-api-firefox-148/mozilla-hacks.jpg" target="_blank">        <img src="/blog/images/sanitizer-api-firefox-148/mozilla-hacks.jpg" alt="Mozilla Hacks" style="max-width:min(100%, 900px); width:auto; height:auto;">      </a>      <figcaption>&lt;그림 1. Firefox 148 발표와 함께 Sanitizer API를 safer default로 제시한 Mozilla Hacks&gt;</figcaption>    </figure>  <h2 id="실무에서-중요한-차이-innerHTML-vs-setHTML"><a href="#실무에서-중요한-차이-innerHTML-vs-setHTML" class="headerlink" title="실무에서 중요한 차이: innerHTML vs setHTML"></a>실무에서 중요한 차이: <code>innerHTML</code> vs <code>setHTML</code></h2><p>둘 다 “HTML을 넣는다”는 점은 같지만, 기본 철학이 다르다.</p><h3 id="innerHTML"><a href="#innerHTML" class="headerlink" title="innerHTML"></a><code>innerHTML</code></h3><ul><li>빠르고 익숙하지만 입력 신뢰성 검증을 개발자가 책임져야 한다.</li><li>정책&#x2F;필터가 분산되기 쉬워서 팀 규모가 커질수록 누수 위험이 커진다.</li></ul><h3 id="setHTML"><a href="#setHTML" class="headerlink" title="setHTML"></a><code>setHTML</code></h3><ul><li>안전 메서드에서는 XSS-unsafe 요소&#x2F;속성을 강제로 제거한다.</li><li>필요한 경우 <code>Sanitizer</code>&#x2F;<code>SanitizerConfig</code>로 허용&#x2F;제거 규칙을 커스터마이징할 수 있다.</li><li>컨텍스트 인지 파싱을 하므로, 대상 요소 맥락에 맞지 않는 태그를 추가로 드롭한다.</li></ul>    <div class="alert alert--info">      <strong class="alert__title">컨텍스트 인지 파싱이란?</strong>      <div class="alert__body">        <p>같은 태그라도 어디에 삽입하느냐에 따라 유효성이 달라진다. 예를 들어 <code>&lt;col&gt;</code>은 <code>&lt;table&gt;</code> 맥락에서는 의미가 있지만 일반 <code>&lt;div&gt;</code> 안에서는 허용되지 않는다. <code>setHTML()</code>은 이런 문맥을 고려해 현재 대상 요소에서 성립하지 않는 태그를 추가로 제거한다.</p>      </div>    </div>  <p>예를 들어 아래 코드는 이벤트 핸들러 같은 위험 속성이 제거된 결과를 DOM에 넣게 된다.</p><pre class="language-javascript" data-language="javascript"><code class="language-javascript"><span class="token keyword">const</span> input <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">&lt;h1>Hello &lt;img src="x" onclick="alert(1)">&lt;/h1></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span>document<span class="token punctuation">.</span>body<span class="token punctuation">.</span><span class="token function">setHTML</span><span class="token punctuation">(</span>input<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre><h2 id="Trusted-Types와-관계는-어떻게-보나"><a href="#Trusted-Types와-관계는-어떻게-보나" class="headerlink" title="Trusted Types와 관계는 어떻게 보나"></a>Trusted Types와 관계는 어떻게 보나</h2><p>여기서 헷갈리기 쉬운 포인트가 Trusted Types 와의 관계다. 간단히 말해 Trusted Types는 “어떤 입력이 sink로 들어가도 사전에 정책을 거치게 강제”하는 프레임워크이고, Sanitizer API는 “HTML 삽입 자체의 안전 기본값”이다.</p><p>MDN 설명대로 safe sanitization 메서드는 자체적으로 unsafe 엔티티를 제거하므로 Trusted Types가 필수 전제는 아니다. 반대로 unsafe 메서드나 레거시 sink(<code>innerHTML</code>, <code>document.write</code> 등)를 다뤄야 할 때는 Trusted Types 정책 강제가 큰 힘을 발휘한다.</p><p>즉 둘은 경쟁이라기보다 계층이 다르다.</p><ul><li>Sanitizer API: 안전 삽입 기본값</li><li>Trusted Types: 주입 지점 전체의 조직적 통제</li></ul><p>Mozilla도 같은 맥락으로, <code>setHTML</code> 도입 후 Trusted Types enforcement를 붙이면 XSS 회귀(regression) 방어가 더 쉬워진다고 설명한다.</p>    <div class="alert alert--info">      <strong class="alert__title">도입 관점 팁</strong>      <div class="alert__body">        <p>신규 코드에서는 <code>innerHTML</code> 대신 <code>setHTML</code>을 기본 선택지로 두고, 레거시 구간은 Trusted Types 리포트 모드(CSP)로 sink 사용 현황을 먼저 수집한 뒤 순차적으로 치환하는 방식이 현실적이다.</p>      </div>    </div>  <h2 id="도입-순서-실무용"><a href="#도입-순서-실무용" class="headerlink" title="도입 순서(실무용)"></a>도입 순서(실무용)</h2>    <figure title="체크리스트 이미지">      <a href="/blog/images/sanitizer-api-firefox-148/checklist.jpg" target="_blank">        <img src="/blog/images/sanitizer-api-firefox-148/checklist.jpg" alt="체크리스트 이미지" style="max-width:min(100%, 760px); width:auto; height:auto;">      </a>      <figcaption>&lt;그림 2. 단계적 전환은 체크리스트 기반으로 작은 치환부터 시작하는 게 안전하다&gt;</figcaption>    </figure>  <ol><li>문자열 HTML 삽입 지점 탐색 (<code>innerHTML</code>, <code>insertAdjacentHTML</code>, <code>outerHTML</code>)</li><li>치환 가능한 구간부터 <code>setHTML()</code>로 교체</li><li>커스텀 규칙이 필요한 화면만 <code>Sanitizer</code> 구성 사용</li><li>회귀 테스트(렌더 결과 + 이벤트 동작) 추가</li><li>Trusted Types 정책&#x2F;강제(CSP)로 레거시 sink를 단계적으로 봉쇄</li></ol><p>핵심은 “보안을 위해 코드 전부를 갈아엎는” 접근이 아니라, 삽입 API 선택을 바꾸면서 위험 구간을 점진적으로 줄이는 데 있다.</p><h2 id="정리"><a href="#정리" class="headerlink" title="정리"></a>정리</h2><p>Firefox 148의 Sanitizer API 탑재는 단순 브라우저 기능 추가를 넘어, 프런트엔드 보안 실무에서 safer default를 표준 API로 끌어올린 변화로 볼 수 있다. XSS는 원칙을 몰라서가 아니라 관성이 바뀌지 않아 반복되는 경우가 많으니, 이번 기회에 <code>innerHTML</code> 대신 <code>setHTML</code>을 기본 선택지로 삼는 작업을 팀 규칙으로 잡아두는 게 좋다.</p><h2 id="참고-링크"><a href="#참고-링크" class="headerlink" title="참고 링크"></a>참고 링크</h2><ul><li><a href="https://hacks.mozilla.org/2026/02/goodbye-innerhtml-hello-sethtml-stronger-xss-protection-in-firefox-148/">Mozilla Hacks: Goodbye innerHTML, Hello setHTML (Firefox 148)</a></li><li><a href="https://developer.mozilla.org/en-US/docs/Web/API/Element/setHTML">MDN: Element.setHTML()</a></li><li><a href="https://developer.mozilla.org/en-US/docs/Web/API/HTML_Sanitizer_API">MDN: HTML Sanitizer API</a></li><li><a href="https://developer.mozilla.org/en-US/docs/Web/API/Trusted_Types_API">MDN: Trusted Types API</a></li><li><a href="https://web.dev/articles/sanitizer">web.dev: Safe DOM manipulation with the Sanitizer API</a></li></ul><h3 id="이미지-출처"><a href="#이미지-출처" class="headerlink" title="이미지 출처"></a>이미지 출처</h3><ul><li>커버: <a href="https://commons.wikimedia.org/wiki/File:Firefox_logo,_2019.svg">Firefox logo (2019)</a> — Mozilla, MPL 2.0</li><li>본문: <a href="https://hacks.mozilla.org/wp-content/themes/Hax/img/hacks-meta-image.jpg">Mozilla Hacks 기본 메타 이미지</a></li><li>본문(도입 순서 섹션): <a href="https://www.flickr.com/photos/98755122@N00/310003506">check!</a> — © Graham Ballantyne, CC BY-NC-ND 2.0</li></ul>]]>
    </content>
    <id>https://uyeong.github.io/blog/2026/03/11/sanitizer-api-firefox-148/</id>
    <link href="https://uyeong.github.io/blog/2026/03/11/sanitizer-api-firefox-148/"/>
    <published>2026-03-10T15:00:00.000Z</published>
    <summary>Firefox 148에서 표준 Sanitizer API(setHTML)가 먼저 탑재됐다. 이 글은 Mozilla 발표와 MDN, web.dev, Trusted Types 자료를 함께 읽고 setHTML의 핵심 개념, innerHTML 대비 차이, 실무 도입 포인트를 정리한다.</summary>
    <title>Firefox 148의 Sanitizer API: innerHTML 대체를 진짜로 고민할 시점</title>
    <updated>2026-03-12T05:32:19.968Z</updated>
  </entry>
  <entry>
    <author>
      <name>Coderifleman</name>
    </author>
    <category term="Security" scheme="https://uyeong.github.io/blog/categories/Security/"/>
    <category term="Node.js" scheme="https://uyeong.github.io/blog/categories/Security/Node-js/"/>
    <category term="Security" scheme="https://uyeong.github.io/blog/tags/Security/"/>
    <category term="npm" scheme="https://uyeong.github.io/blog/tags/npm/"/>
    <category term="GitHub Actions" scheme="https://uyeong.github.io/blog/tags/GitHub-Actions/"/>
    <category term="OIDC" scheme="https://uyeong.github.io/blog/tags/OIDC/"/>
    <category term="Supply Chain Security" scheme="https://uyeong.github.io/blog/tags/Supply-Chain-Security/"/>
    <content>
      <![CDATA[<p><code>npm install</code>은 단순히 “패키지 내려받기”로 느껴지지만, 실제로는 <strong>외부 코드를 우리 CI 환경에 들여오는 첫 단계</strong>다.<br>그리고 이 단계는 테스트보다 먼저 실행된다. 그래서 악성 패키지가 섞여 있으면 검사 전에 설치 단계에서 문제가 발생할 수 있다.</p><p>관련하여 오늘은 <a href="https://github.com/flatt-security/setup-takumi-guard-npm">setup-takumi-guard-npm</a>를 소개하고자 한다.<br>이 프로젝트는 화려한 보안 엔진이라기보다, GitHub Actions의 설치 경로를 Takumi Guard로 연결해주는 <strong>얇고 실용적인 어댑터(adapter)</strong>에 가깝다.</p>    <figure title="Takumi Guard 공식 브랜딩 이미지">      <a href="/blog/images/setup-takumi-guard-npm-introduction/branding.png" target="_blank">        <img src="/blog/images/setup-takumi-guard-npm-introduction/branding.png" alt="Takumi Guard 공식 브랜딩 이미지" style="max-width:min(100%, 720px); width:auto; height:auto;">      </a>      <figcaption>&lt;그림 1. npm 설치 앞단에 경비원을 두는 접근&gt;</figcaption>    </figure>      <div class="alert alert--info">      <strong class="alert__title">핵심 요약</strong>      <div class="alert__body">        <ul><li>이 액션은 새 보안 엔진이라기보다, Takumi Guard를 CI 설치 흐름에 붙여주는 연결 레이어에 가깝다.</li><li>핵심은 “설치한 뒤 검사”가 아니라 “설치 전에 막기”다.</li><li>인증은 OIDC를 쓰기 때문에, 장기 토큰을 배포·보관하는 부담을 줄일 수 있다.</li></ul>      </div>    </div>  <h2 id="이-프로젝트는-정확히-무엇을-하나"><a href="#이-프로젝트는-정확히-무엇을-하나" class="headerlink" title="이 프로젝트는 정확히 무엇을 하나"></a>이 프로젝트는 정확히 무엇을 하나</h2><p>한 줄로 줄이면 이렇다.</p><ul><li>GitHub Actions에서 동작한다.</li><li><code>npm</code>, <code>pnpm</code>, <code>yarn</code> 설치 요청을 <code>https://npm.flatt.tech</code>로 보낸다.</li><li>프록시 앞단에서 Takumi Guard가 악성 패키지를 판단하고, 위험하면 <code>403</code>으로 막는다.</li></ul><p>즉, 이 프로젝트, 그러니까 이 액션의 목표는 직접 악성코드를 분석하는 것이 아니다.<br>Takumi Guard의 분석&#x2F;차단 기능을 CI 설치 경로에 배치하는 것이다.</p><p>README가 강조하는 “two lines of YAML”도 같은 맥락이다. 복잡한 에이전트 설치 없이, 워크플로우에서 연결 비용을 최소화한다.</p><pre class="language-yaml" data-language="yaml"><code class="language-yaml"><span class="token key atrule">steps</span><span class="token punctuation">:</span>  <span class="token punctuation">-</span> <span class="token key atrule">uses</span><span class="token punctuation">:</span> actions/checkout@v4  <span class="token punctuation">-</span> <span class="token key atrule">uses</span><span class="token punctuation">:</span> flatt<span class="token punctuation">-</span>security/setup<span class="token punctuation">-</span>takumi<span class="token punctuation">-</span>guard<span class="token punctuation">-</span>npm@v1  <span class="token punctuation">-</span> <span class="token key atrule">run</span><span class="token punctuation">:</span> npm install  <span class="token punctuation">-</span> <span class="token key atrule">run</span><span class="token punctuation">:</span> npm test</code></pre><h2 id="왜-이-접근이-실무적으로-중요한가"><a href="#왜-이-접근이-실무적으로-중요한가" class="headerlink" title="왜 이 접근이 실무적으로 중요한가"></a>왜 이 접근이 실무적으로 중요한가</h2><h3 id="1-설치-단계는-생각보다-위험하다"><a href="#1-설치-단계는-생각보다-위험하다" class="headerlink" title="1) 설치 단계는 생각보다 위험하다"></a>1) 설치 단계는 생각보다 위험하다</h3><p>CI 보안 도구는 자주 “설치 이후”에 붙는다. 하지만 공급망 공격은 설치 스크립트(<code>preinstall</code>, <code>install</code>, <code>postinstall</code>)에서 이미 시작될 수 있다.<br>그러니까 스캐너를 뒤에 두는 것만으로는 타이밍이 늦다.</p><p>setup-takumi-guard-npm은 이 지점을 정확히 지목하여 앱 코드를 바꾸기보다, 설치가 지나가는 길목 자체를 통제점(control point)으로 바꾼다.</p><h3 id="2-보안을-켜는데-운영-복잡도가-낮다"><a href="#2-보안을-켜는데-운영-복잡도가-낮다" class="headerlink" title="2) 보안을 켜는데 운영 복잡도가 낮다"></a>2) 보안을 켜는데 운영 복잡도가 낮다</h3><p>보안 도입이 실패하는 가장 흔한 이유는 “효과”보다 “귀찮음”이다.<br>토큰 배포, 권한 관리, 워크플로우 수정이 과하면 팀은 결국 포기하게 된다.</p><p>이 액션은 그 마찰을 줄이려고 설계됐다.</p><ul><li>익명 모드로 빠르게 시작 가능</li><li><code>bot-id</code>를 붙이면 감사 로그&#x2F;조직 가시성 확장</li><li>GitHub OIDC 인증으로 장기 시크릿 의존도 감소</li></ul><p>쉽게 말해, <strong>문단속을 강화하면서 열쇠 관리 지옥은 피하려는 구조</strong>다.</p><h3 id="3-차단에서-끝내지-않고-사후-추적까지-연결한다"><a href="#3-차단에서-끝내지-않고-사후-추적까지-연결한다" class="headerlink" title="3) 차단에서 끝내지 않고 사후 추적까지 연결한다"></a>3) 차단에서 끝내지 않고 사후 추적까지 연결한다</h3><p>인증 흐름에서는 설치 이력 기반 추적과 breach notification까지 연결된다.<br>보안은 “지금 막았는가”도 중요하지만, “예전에 넣은 것 중 뭐가 뒤늦게 문제 됐는가”도 똑같이 중요하다.</p><p>이 지점이 단발성 차단이 아니라 운영 루프를 만들기 때문에 setup-takumi-guard-npm의 실무적 가치가 있다.</p><h2 id="내부-동작은-의외로-단순하다"><a href="#내부-동작은-의외로-단순하다" class="headerlink" title="내부 동작은 의외로 단순하다"></a>내부 동작은 의외로 단순하다</h2><p><code>action.yml</code> 기준으로 보면 동작은 깔끔하다.</p><ol><li><code>.npmrc</code>에 registry를 <code>https://npm.flatt.tech/</code>로 설정</li><li><code>bot-id</code>가 있으면 GitHub OIDC 토큰 요청</li><li>STS와 교환해 short-lived access token 발급</li><li>토큰을 <code>.npmrc</code>에 주입해 인증 설치 경로 구성</li></ol><p>무게중심은 액션 코드가 아니라 서버 측(Takumi Guard)에 있다.<br>이 분리는 합리적이다. GitHub Action은 “연결”에 집중하고, 위협 인텔리전스는 서버에서 빠르게 갱신하는 편이 운영상 더 유리하다.</p>    <div class="alert alert--info">      <strong class="alert__title">핵심 관점</strong>      <div class="alert__body">        <p>이 저장소를 “npm 스캐너”로 보면 초점이 흐려진다.<br>더 정확히는 “GitHub Actions에서 설치 경로를 선제 차단 체계로 전환하는 온보딩 레이어”로 보는 편이 맞다.</p>      </div>    </div>  <h2 id="마무리"><a href="#마무리" class="headerlink" title="마무리"></a>마무리</h2><p>setup-takumi-guard-npm은 거대한 보안 플랫폼이 아니라, <strong>작지만 정확한 레버리지</strong>다.</p><ul><li>설치 전 차단(prevention)</li><li>OIDC 기반 인증으로 시크릿 부담 완화</li><li>낮은 도입 마찰 + 점진적 확장</li></ul><p>결국 이 프로젝트가 던지는 메시지는 단순하다.<br>“공급망 보안은 보고서 단계가 아니라, 설치 버튼이 눌리는 순간에 시작해야 한다.”<br>이 관점을 팀 워크플로우에 자연스럽게 심는 데, 이 액션은 꽤 실용적인 선택지다.</p><h2 id="참고-링크"><a href="#참고-링크" class="headerlink" title="참고 링크"></a>참고 링크</h2><ul><li><a href="https://github.com/flatt-security/setup-takumi-guard-npm">setup-takumi-guard-npm 저장소</a></li><li><a href="https://shisho.dev/docs/t/guard/">Takumi Guard 공식 소개</a></li><li><a href="https://shisho.dev/docs/t/guard/quickstart/npm">npm Quickstart</a></li><li><a href="https://shisho.dev/docs/t/guard/features/package-blocking">Package Blocking</a></li><li><a href="https://shisho.dev/docs/t/guard/features/breach-notifications">Breach Notifications</a></li><li><a href="https://shisho.dev/docs/t/guard/architecture/oidc">CI Integration &amp; OIDC Authentication</a></li><li><a href="https://shisho.dev/docs/t/guard/architecture/intelligence">Intelligence</a></li><li><a href="https://shisho.dev/docs/r/202603-takumi-guard">Takumi Guard 제공 시작 릴리스 노트</a></li></ul>]]>
    </content>
    <id>https://uyeong.github.io/blog/2026/03/11/setup-takumi-guard-npm-introduction/</id>
    <link href="https://uyeong.github.io/blog/2026/03/11/setup-takumi-guard-npm-introduction/"/>
    <published>2026-03-10T15:00:00.000Z</published>
    <summary>setup-takumi-guard-npm은 GitHub Actions의 npm 설치 경로를 Takumi Guard 프록시로 연결해 악성 패키지를 설치 전에 차단한다. 이 글에서는 왜 이 방식이 실무에서 유효한지, 어디까지 해결하고 어디서부터는 별도 대응이 필요한지 핵심만 정리한다.</summary>
    <title>npm 설치 앞에 경비원을 세우는 법: setup-takumi-guard-npm 살펴보기</title>
    <updated>2026-03-11T03:39:54.422Z</updated>
  </entry>
  <entry>
    <author>
      <name>Coderifleman</name>
    </author>
    <category term="JavaScript" scheme="https://uyeong.github.io/blog/categories/JavaScript/"/>
    <category term="Development" scheme="https://uyeong.github.io/blog/categories/JavaScript/Development/"/>
    <category term="JavaScript" scheme="https://uyeong.github.io/blog/tags/JavaScript/"/>
    <category term="Release Note" scheme="https://uyeong.github.io/blog/tags/Release-Note/"/>
    <category term="TypeScript" scheme="https://uyeong.github.io/blog/tags/TypeScript/"/>
    <category term="Compiler" scheme="https://uyeong.github.io/blog/tags/Compiler/"/>
    <content>
      <![CDATA[<p>TypeScript를 실무에서 오래 쓰다 보면 버전 업 공지가 두 종류로 나뉜다. 하나는 “기능 몇 개 추가” 느낌이고, 다른 하나는 “다음 세대 준비” 느낌인데 이번 6.0 RC는 확실히 두 번째에 해당한다. 관련해 오늘은 TypeScript 6.0 RC 발표 글을 기준으로, 중요한 변화만 빠르게 훑고 당장 우리 프로젝트에서 뭘 확인하면 좋은지 정리해본다.</p>    <div class="alert alert--info">      <strong class="alert__title">한 줄 요약</strong>      <div class="alert__body">        <p>TypeScript 6.0 RC는 기능 추가 릴리스이면서 동시에 TypeScript 7.0(네이티브 코드베이스)로 넘어가기 위한 정렬(alignment) 릴리스다. 그래서 “새 기능”만 보기보다 “마이그레이션을 준비”하는 관점으로도 살펴봐야 한다.</p>      </div>    </div>  <h2 id="왜-6-0이-중요한가-7-0으로-가는-징검다리"><a href="#왜-6-0이-중요한가-7-0으로-가는-징검다리" class="headerlink" title="왜 6.0이 중요한가: 7.0으로 가는 징검다리"></a>왜 6.0이 중요한가: 7.0으로 가는 징검다리</h2><p>공식 발표가 가장 강조한 포인트. 6.0은 기존 JavaScript 코드베이스 기반의 마지막 큰 릴리스로 계획되어 있고, 이후 7.0부터는 Go로 작성된 네이티브 코드베이스를 기반으로 간다. Microsoft가 별도 글에서 공유한 방향(빌드&#x2F;체크 성능 대폭 개선, 언어 서비스 개선)까지 같이 보면, 6.0은 단순한 중간 버전이 아니라 전환을 위한 준비 단계에 가깝다.</p>    <figure title="코드 화면 이미지">      <a href="/blog/images/typescript-6-0-rc-highlights/code-screen.jpg" target="_blank">        <img src="/blog/images/typescript-6-0-rc-highlights/code-screen.jpg" alt="코드 화면 이미지" style="max-width:min(100%, 900px); width:auto; height:auto;">      </a>      <figcaption>&lt;그림 1. 6.0은 새 기능 소개이면서 동시에 7.0 전환 준비 성격이 강한 릴리스다&gt;</figcaption>    </figure>  <h2 id="이번-RC에서-실무-영향이-큰-핵심-변경"><a href="#이번-RC에서-실무-영향이-큰-핵심-변경" class="headerlink" title="이번 RC에서 실무 영향이 큰 핵심 변경"></a>이번 RC에서 실무 영향이 큰 핵심 변경</h2><h3 id="1-this를-안-쓰는-함수의-추론-개선"><a href="#1-this를-안-쓰는-함수의-추론-개선" class="headerlink" title="1) this를 안 쓰는 함수의 추론 개선"></a>1) this를 안 쓰는 함수의 추론 개선</h3><p>제네릭 추론에서 메서드 문법 함수가 <code>unknown</code>으로 떨어지던 케이스가 있었는데, 6.0은 함수 내부에서 <code>this</code>를 실제로 쓰지 않으면 추론 후보로 더 적극적으로 잡아준다. 결과적으로 객체 리터럴 안의 메서드 순서 때문에 추론 품질이 들쭉날쭉하던 문제가 줄어든다.</p><p>이건 특히 유틸 함수 조합이 많은 프런트엔드 코드에서 체감될 가능성이 크고, “왜 같은 코드인데 위치 바꾸면 깨지지?” 같은 이상한 순간을 줄여준다는 점에서 꽤 반갑다.</p><h3 id="2-Node-서브패스-import-지원"><a href="#2-Node-서브패스-import-지원" class="headerlink" title="2) Node 서브패스 import #/ 지원"></a>2) Node 서브패스 import <code>#/</code> 지원</h3><p>Node가 최근 허용한 <code>#/</code> 서브패스 import를 TypeScript도 <code>moduleResolution</code> 옵션이 <code>node20</code>, <code>nodenext</code>, <code>bundler</code>일 때 지원한다. 경로 별칭에서 불필요한 접두 경로를 줄일 수 있어서 설정이 좀 더 깔끔해진다.</p><h3 id="3-moduleResolution-bundler-module-commonjs-조합-허용"><a href="#3-moduleResolution-bundler-module-commonjs-조합-허용" class="headerlink" title="3) --moduleResolution bundler + --module commonjs 조합 허용"></a>3) <code>--moduleResolution bundler</code> + <code>--module commonjs</code> 조합 허용</h3><p>기존에는 이 조합이 막혀 있었는데, deprecated 경로를 피하면서 점진적으로 옮기려는 프로젝트 입장에서는 꽤 현실적인 업그레이드 경로가 된다. 특히 “지금 당장 전부 ESM으로 못 바꾸는” 팀이라면 완충 구간으로 쓸 수 있다.</p><h3 id="4-stableTypeOrdering-플래그-추가"><a href="#4-stableTypeOrdering-플래그-추가" class="headerlink" title="4) --stableTypeOrdering 플래그 추가"></a>4) <code>--stableTypeOrdering</code> 플래그 추가</h3><p>6.0과 7.0의 내부 타입 정렬 차이 때문에 <code>.d.ts</code> 출력물을 비교하면 실제 의미 변화가 없는데도 diff가 과하게 많이 잡힐 수 있는데, 이 플래그가 그런 비교 노이즈를 줄이는 데 도움을 준다. 다만 공식 안내대로 상시 사용 플래그라기보다는 마이그레이션 진단 도구에 가깝고, 성능 저하(코드베이스에 따라 최대 25%) 가능성이 있으니 CI 기본값으로 바로 넣는 건 신중해야 한다.</p>    <div class="alert alert--info">      <strong class="alert__title">적용 팁</strong>      <div class="alert__body">        <p><code>--stableTypeOrdering</code>을 켰을 때 타입 에러가 새로 보이면, 추론 순서에 기대고 있던 코드일 가능성이 있다. 이 경우 제네릭 타입 인자나 변수 주석 타입을 명시해 주면 대부분 정리된다.</p>      </div>    </div>  <h3 id="5-es2025-target-lib-Temporal-타입-RegExp-escape-upsert-타입-반영"><a href="#5-es2025-target-lib-Temporal-타입-RegExp-escape-upsert-타입-반영" class="headerlink" title="5) es2025 target&#x2F;lib, Temporal 타입, RegExp.escape, upsert 타입 반영"></a>5) <code>es2025</code> target&#x2F;lib, Temporal 타입, RegExp.escape, upsert 타입 반영</h3><p>6.0은 큰 아키텍처 전환 릴리스지만 언어&#x2F;라이브러리 쪽 실용 업데이트도 같이 들어왔다.</p><ul><li><code>es2025</code> target&#x2F;lib 추가</li><li><code>Temporal</code> 타입 제공(<code>esnext</code> 계열)</li><li><code>RegExp.escape</code> 타입 반영</li><li><code>Map/WeakMap</code>의 <code>getOrInsert</code>, <code>getOrInsertComputed</code>(upsert 계열) 타입 지원</li><li><code>dom</code>에 <code>dom.iterable</code>, <code>dom.asynciterable</code> 내용 통합</li></ul><h2 id="마이그레이션-관점에서-지금-할-일"><a href="#마이그레이션-관점에서-지금-할-일" class="headerlink" title="마이그레이션 관점에서 지금 할 일"></a>마이그레이션 관점에서 지금 할 일</h2><p>6.0을 올릴 때 아래를 함께 점검하면 삽질 시간을 크게 줄일 수 있겠다.</p><ol><li><code>tsconfig</code>에서 deprecated 옵션 사용 여부 먼저 체크</li><li>모듈 해석 전략을 프로젝트 유형별로 분리해 결정<ul><li>번들러 앱: <code>module preserve</code> + <code>moduleResolution bundler</code></li><li>Node 앱: <code>module nodenext</code> 검토</li></ul></li><li>선언 파일 diff가 중요한 저장소는 <code>--stableTypeOrdering</code>으로 비교 실험</li><li>추론 불안정 구간(복잡한 제네릭 호출)은 타입 인자 또는 주석을 명시해 방어</li></ol><h2 id="마무리"><a href="#마무리" class="headerlink" title="마무리"></a>마무리</h2><p>TypeScript 6.0 RC를 기능 목록으로만 보면 평범해 보일 수 있지만, 릴리스 의도를 보면 전략적인 버전이다. 팀 입장에서는 “무엇이 새로 생겼는가”와 함께 “지금 어떤 부채를 정리해 두면 7.0에서 편해지는가”를 같이 보는 게 맞고, 그 관점에서 6.0은 당장 올려서 실험해볼 가치가 있다.</p><h2 id="참고-링크"><a href="#참고-링크" class="headerlink" title="참고 링크"></a>참고 링크</h2><ul><li><a href="https://devblogs.microsoft.com/typescript/announcing-typescript-6-0-rc/">Announcing TypeScript 6.0 RC</a></li><li><a href="https://devblogs.microsoft.com/typescript/announcing-typescript-6-0-beta/">Announcing TypeScript 6.0 Beta</a></li><li><a href="https://devblogs.microsoft.com/typescript/typescript-native-port/">A 10x Faster TypeScript (Native Port)</a></li><li><a href="https://www.npmjs.com/package/typescript">TypeScript npm RC 설치</a></li></ul><h3 id="이미지-출처"><a href="#이미지-출처" class="headerlink" title="이미지 출처"></a>이미지 출처</h3><ul><li>커버 이미지: <a href="https://github.com/remojansen/logo.ts">TypeScript logo</a> (MIT)</li><li>본문 이미지: <a href="https://www.flickr.com/photos/21051491@N02/15787237274">Cool Reflecting Source Code Screen at GGJ Berlin 2015</a> — © qubodup, CC BY 2.0</li></ul>]]>
    </content>
    <id>https://uyeong.github.io/blog/2026/03/11/typescript-6-0-rc-highlights/</id>
    <link href="https://uyeong.github.io/blog/2026/03/11/typescript-6-0-rc-highlights/"/>
    <published>2026-03-10T15:00:00.000Z</published>
    <summary>TypeScript 6.0 RC는 단순 기능 추가 릴리스가 아니라 7.0 네이티브 전환을 준비하는 브리지 릴리스다. 실무에서 바로 영향이 큰 변경점과 마이그레이션 포인트를 빠르게 정리했다.</summary>
    <title>TypeScript 6.0 RC 핵심 정리: 지금 바로 챙겨야 할 변화들</title>
    <updated>2026-03-11T03:42:59.308Z</updated>
  </entry>
  <entry>
    <author>
      <name>Coderifleman</name>
    </author>
    <category term="JavaScript" scheme="https://uyeong.github.io/blog/categories/JavaScript/"/>
    <category term="Development" scheme="https://uyeong.github.io/blog/tags/Development/"/>
    <category term="JavaScript" scheme="https://uyeong.github.io/blog/tags/JavaScript/"/>
    <category term="TypeScript" scheme="https://uyeong.github.io/blog/tags/TypeScript/"/>
    <category term="NumPy" scheme="https://uyeong.github.io/blog/tags/NumPy/"/>
    <content>
      <![CDATA[<p>프론트엔드 개발자라면 한 번쯤 이런 생각을 해봤을 것이다. “NumPy처럼 다차원 배열을 자유롭게 다루고 싶은데, 왜 JavaScript 생태계에는 마땅한 게 없지?”</p><p>물론 <a href="https://github.com/nicolaspanel/numjs">numjs</a>나 <a href="https://www.npmjs.com/package/ndarray">ndarray</a> 같은 선구자가 있었지만, NumPy의 방대한 API를 충실히 재현한 라이브러리는 사실상 없었다. <a href="https://github.com/dupontcyborg/numpy-ts">numpy-ts</a>는 바로 그 간극을 메우려는 프로젝트다. NumPy API의 <strong>93.9%</strong>(507개 중 476개 함수)를 TypeScript로 재구현했다고 한다. 과연 어느 정도 수준인지 함께 살펴보자.</p><h2 id="핵심-특징"><a href="#핵심-특징" class="headerlink" title="핵심 특징"></a>핵심 특징</h2><p>numpy-ts의 주요 셀링 포인트를 먼저 정리하면 다음과 같다.</p><ul><li><strong>93.9% API 커버리지</strong> — 476&#x2F;507개 NumPy 함수 구현</li><li><strong>제로 디펜던시</strong> — 런타임 의존성 없음</li><li><strong>트리셰이킹 지원</strong> — <code>numpy-ts/core</code>에서 필요한 함수만 가져오면 번들 10~40KB</li><li><strong>TypeScript 네이티브</strong> — 완전한 타입 추론</li><li><strong>NumPy 검증 테스트</strong> — Python NumPy 결과와 직접 비교하는 6,000개 이상의 테스트</li></ul><h2 id="설치-및-기본-사용법"><a href="#설치-및-기본-사용법" class="headerlink" title="설치 및 기본 사용법"></a>설치 및 기본 사용법</h2><p>설치는 간단하다.</p><pre class="language-bash" data-language="bash"><code class="language-bash"><span class="token function">npm</span> <span class="token function">install</span> numpy-ts</code></pre><p>Node.js 20.1.0 이상이 필요하다. 사용 방식은 세 가지 진입점을 제공한다.</p><pre class="language-typescript" data-language="typescript"><code class="language-typescript"><span class="token comment">// 1. 풀 API — 메서드 체이닝 지원 (~200-300KB)</span><span class="token keyword">import</span> <span class="token operator">*</span> <span class="token keyword">as</span> np <span class="token keyword">from</span> <span class="token string">'numpy-ts'</span><span class="token punctuation">;</span><span class="token keyword">const</span> <span class="token constant">A</span> <span class="token operator">=</span> np<span class="token punctuation">.</span><span class="token function">array</span><span class="token punctuation">(</span><span class="token punctuation">[</span><span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">,</span> <span class="token number">2</span><span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token punctuation">[</span><span class="token number">3</span><span class="token punctuation">,</span> <span class="token number">4</span><span class="token punctuation">]</span><span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token string">'float32'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token keyword">const</span> result <span class="token operator">=</span> <span class="token constant">A</span><span class="token punctuation">.</span><span class="token function">add</span><span class="token punctuation">(</span><span class="token number">5</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">multiply</span><span class="token punctuation">(</span><span class="token number">2</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token constant">T</span><span class="token punctuation">;</span><span class="token comment">// 2. 트리셰이킹 — 필요한 함수만 (~10-40KB)</span><span class="token keyword">import</span> <span class="token punctuation">&#123;</span> array<span class="token punctuation">,</span> add<span class="token punctuation">,</span> reshape <span class="token punctuation">&#125;</span> <span class="token keyword">from</span> <span class="token string">'numpy-ts/core'</span><span class="token punctuation">;</span><span class="token comment">// 3. Node.js 파일 I/O 포함</span><span class="token keyword">import</span> <span class="token operator">*</span> <span class="token keyword">as</span> np <span class="token keyword">from</span> <span class="token string">'numpy-ts/node'</span><span class="token punctuation">;</span><span class="token keyword">const</span> arr <span class="token operator">=</span> <span class="token keyword">await</span> np<span class="token punctuation">.</span><span class="token function">loadNpy</span><span class="token punctuation">(</span><span class="token string">'data.npy'</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>    <div class="alert alert--info">      <strong class="alert__title">왜 세 가지 진입점인가?</strong>      <div class="alert__body">        <p>브라우저 번들 크기가 중요한 프로젝트에서는 <code>numpy-ts/core</code>를 쓰면 트리셰이킹이 가능하다. 반면 Node.js 환경에서 파일 I&#x2F;O(<code>.npy</code>, <code>.npz</code>, <code>.csv</code>)까지 필요하다면 <code>numpy-ts/node</code>를 쓰면 된다. 풀 API는 메서드 체이닝의 편의성을 제공하지만 번들이 크다.</p>      </div>    </div>  <h2 id="NumPy와의-문법-차이"><a href="#NumPy와의-문법-차이" class="headerlink" title="NumPy와의 문법 차이"></a>NumPy와의 문법 차이</h2><p>Python에서 넘어온 개발자라면 몇 가지 차이를 알아두어야 한다.</p><pre class="language-typescript" data-language="typescript"><code class="language-typescript"><span class="token comment">// Python: a = np.zeros((3, 3))</span><span class="token comment">// TS: 튜플 대신 배열</span><span class="token keyword">const</span> a <span class="token operator">=</span> np<span class="token punctuation">.</span><span class="token function">zeros</span><span class="token punctuation">(</span><span class="token punctuation">[</span><span class="token number">3</span><span class="token punctuation">,</span> <span class="token number">3</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token comment">// Python: a[0:2, :]</span><span class="token comment">// TS: 슬라이싱은 문자열</span>a<span class="token punctuation">.</span><span class="token function">slice</span><span class="token punctuation">(</span><span class="token string">'0:2'</span><span class="token punctuation">,</span> <span class="token string">':'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token comment">// Python: a + b</span><span class="token comment">// TS: 연산자 오버로딩 없음</span>a<span class="token punctuation">.</span><span class="token function">add</span><span class="token punctuation">(</span>b<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token comment">// 또는</span>np<span class="token punctuation">.</span><span class="token function">add</span><span class="token punctuation">(</span>a<span class="token punctuation">,</span> b<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token comment">// Python: np.sum(a, axis=0)</span><span class="token comment">// TS: 키워드 인자 없음, 위치 인자만</span>np<span class="token punctuation">.</span><span class="token function">sum</span><span class="token punctuation">(</span>a<span class="token punctuation">,</span> <span class="token number">0</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token comment">// Python: np.var(a)</span><span class="token comment">// TS: var는 예약어</span>np<span class="token punctuation">.</span><span class="token function">variance</span><span class="token punctuation">(</span>a<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre><p>슬라이싱을 문자열로 처리하는 부분이 좀 아쉽지만, JavaScript 언어의 한계를 고려하면 합리적인 선택이다. <code>Proxy</code> 기반의 브래킷 인덱싱(<code>arr[0]</code>, <code>arr[-1]</code>)도 지원하므로 단순 접근은 자연스럽게 할 수 있다.</p><h2 id="지원하는-기능-범위"><a href="#지원하는-기능-범위" class="headerlink" title="지원하는 기능 범위"></a>지원하는 기능 범위</h2><p>numpy-ts가 커버하는 영역은 생각보다 넓다.</p><table><thead><tr><th>영역</th><th>주요 함수</th></tr></thead><tbody><tr><td>배열 생성</td><td><code>array</code>, <code>zeros</code>, <code>ones</code>, <code>arange</code>, <code>linspace</code>, <code>eye</code>, <code>meshgrid</code> 등</td></tr><tr><td>산술&#x2F;수학</td><td><code>add</code>, <code>multiply</code>, <code>sqrt</code>, <code>exp</code>, <code>log</code>, <code>clip</code>, <code>interp</code> 등</td></tr><tr><td>삼각함수</td><td><code>sin</code>, <code>cos</code>, <code>tan</code>, <code>arcsin</code>, <code>arctan2</code> 등 전체</td></tr><tr><td>선형대수</td><td><code>dot</code>, <code>matmul</code>, <code>inv</code>, <code>det</code>, <code>eig</code>, <code>svd</code>, <code>solve</code>, <code>qr</code>, <code>cholesky</code></td></tr><tr><td>통계&#x2F;리덕션</td><td><code>sum</code>, <code>mean</code>, <code>std</code>, <code>median</code>, <code>percentile</code>, <code>histogram</code></td></tr><tr><td>FFT</td><td><code>fft</code>, <code>ifft</code>, <code>fft2</code>, <code>rfft</code>, <code>fftfreq</code>, <code>fftshift</code></td></tr><tr><td>난수</td><td><code>normal</code>, <code>uniform</code>, <code>poisson</code>, <code>binomial</code> 등 30개 이상 분포</td></tr><tr><td>형상 조작</td><td><code>reshape</code>, <code>concatenate</code>, <code>stack</code>, <code>split</code>, <code>pad</code>, <code>flip</code>, <code>rot90</code></td></tr><tr><td>집합&#x2F;정렬</td><td><code>unique</code>, <code>intersect1d</code>, <code>sort</code>, <code>argsort</code>, <code>searchsorted</code></td></tr><tr><td>I&#x2F;O</td><td><code>.npy</code>, <code>.npz</code>, <code>.csv</code>&#x2F;<code>.txt</code> 읽기&#x2F;쓰기</td></tr></tbody></table><p>구현하지 않은 31개 함수는 윈도우 함수(<code>hamming</code>, <code>kaiser</code>), 영업일 유틸리티, 그리고 이미 NumPy에서도 deprecated된 기능들이다.</p><h2 id="실제-활용-예시"><a href="#실제-활용-예시" class="headerlink" title="실제 활용 예시"></a>실제 활용 예시</h2><h3 id="선형대수"><a href="#선형대수" class="headerlink" title="선형대수"></a>선형대수</h3><pre class="language-typescript" data-language="typescript"><code class="language-typescript"><span class="token keyword">import</span> <span class="token operator">*</span> <span class="token keyword">as</span> np <span class="token keyword">from</span> <span class="token string">'numpy-ts'</span><span class="token punctuation">;</span><span class="token comment">// 연립방정식 풀기: Ax = b</span><span class="token keyword">const</span> <span class="token constant">A</span> <span class="token operator">=</span> np<span class="token punctuation">.</span><span class="token function">array</span><span class="token punctuation">(</span><span class="token punctuation">[</span><span class="token punctuation">[</span><span class="token number">3</span><span class="token punctuation">,</span> <span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">,</span> <span class="token number">2</span><span class="token punctuation">]</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token keyword">const</span> b <span class="token operator">=</span> np<span class="token punctuation">.</span><span class="token function">array</span><span class="token punctuation">(</span><span class="token punctuation">[</span><span class="token number">9</span><span class="token punctuation">,</span> <span class="token number">8</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token keyword">const</span> x <span class="token operator">=</span> np<span class="token punctuation">.</span>linalg<span class="token punctuation">.</span><span class="token function">solve</span><span class="token punctuation">(</span><span class="token constant">A</span><span class="token punctuation">,</span> b<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token builtin">console</span><span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>x<span class="token punctuation">.</span><span class="token function">tolist</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// [2, 3]</span><span class="token comment">// SVD 분해</span><span class="token keyword">const</span> <span class="token punctuation">[</span><span class="token constant">U</span><span class="token punctuation">,</span> <span class="token constant">S</span><span class="token punctuation">,</span> Vt<span class="token punctuation">]</span> <span class="token operator">=</span> np<span class="token punctuation">.</span>linalg<span class="token punctuation">.</span><span class="token function">svd</span><span class="token punctuation">(</span><span class="token constant">A</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre><h3 id="통계-분석"><a href="#통계-분석" class="headerlink" title="통계 분석"></a>통계 분석</h3><pre class="language-typescript" data-language="typescript"><code class="language-typescript"><span class="token keyword">import</span> <span class="token operator">*</span> <span class="token keyword">as</span> np <span class="token keyword">from</span> <span class="token string">'numpy-ts'</span><span class="token punctuation">;</span><span class="token keyword">const</span> data <span class="token operator">=</span> np<span class="token punctuation">.</span>random<span class="token punctuation">.</span><span class="token function">normal</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">,</span> <span class="token number">1</span><span class="token punctuation">,</span> <span class="token punctuation">[</span><span class="token number">1000</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token builtin">console</span><span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">'평균:'</span><span class="token punctuation">,</span> np<span class="token punctuation">.</span><span class="token function">mean</span><span class="token punctuation">(</span>data<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">item</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token builtin">console</span><span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">'표준편차:'</span><span class="token punctuation">,</span> np<span class="token punctuation">.</span><span class="token function">std</span><span class="token punctuation">(</span>data<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">item</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token builtin">console</span><span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">'중앙값:'</span><span class="token punctuation">,</span> np<span class="token punctuation">.</span><span class="token function">median</span><span class="token punctuation">(</span>data<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">item</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token comment">// 히스토그램</span><span class="token keyword">const</span> <span class="token punctuation">[</span>counts<span class="token punctuation">,</span> edges<span class="token punctuation">]</span> <span class="token operator">=</span> np<span class="token punctuation">.</span><span class="token function">histogram</span><span class="token punctuation">(</span>data<span class="token punctuation">,</span> <span class="token number">20</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre><h3 id="신호-처리"><a href="#신호-처리" class="headerlink" title="신호 처리"></a>신호 처리</h3><pre class="language-typescript" data-language="typescript"><code class="language-typescript"><span class="token keyword">import</span> <span class="token operator">*</span> <span class="token keyword">as</span> np <span class="token keyword">from</span> <span class="token string">'numpy-ts'</span><span class="token punctuation">;</span><span class="token comment">// 사인파 생성</span><span class="token keyword">const</span> t <span class="token operator">=</span> np<span class="token punctuation">.</span><span class="token function">linspace</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">,</span> <span class="token number">1</span><span class="token punctuation">,</span> <span class="token punctuation">[</span><span class="token number">256</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token keyword">const</span> signal <span class="token operator">=</span> np<span class="token punctuation">.</span><span class="token function">sin</span><span class="token punctuation">(</span>np<span class="token punctuation">.</span><span class="token function">multiply</span><span class="token punctuation">(</span>t<span class="token punctuation">,</span> <span class="token number">2</span> <span class="token operator">*</span> Math<span class="token punctuation">.</span><span class="token constant">PI</span> <span class="token operator">*</span> <span class="token number">10</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token comment">// FFT 변환</span><span class="token keyword">const</span> spectrum <span class="token operator">=</span> np<span class="token punctuation">.</span>fft<span class="token punctuation">.</span><span class="token function">fft</span><span class="token punctuation">(</span>signal<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token keyword">const</span> freqs <span class="token operator">=</span> np<span class="token punctuation">.</span>fft<span class="token punctuation">.</span><span class="token function">fftfreq</span><span class="token punctuation">(</span><span class="token number">256</span><span class="token punctuation">,</span> <span class="token number">1</span> <span class="token operator">/</span> <span class="token number">256</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre><h2 id="성능은-어떨까"><a href="#성능은-어떨까" class="headerlink" title="성능은 어떨까?"></a>성능은 어떨까?</h2><p>솔직히 말하면, 순수 JavaScript로 C&#x2F;Fortran BLAS 기반의 NumPy를 이기는 것은 불가능하다. 프로젝트에서 공개한 297개 벤치마크 결과를 보면 현실이 보인다.</p><blockquote><p>평균 12.16배 느림, 중앙값 3.67배 느림. 최선의 경우 NumPy보다 빠른 것도 있지만, 최악의 경우 300배까지 느려진다.</p></blockquote><p>영역별로 보면 편차가 크다.</p><table><thead><tr><th>영역</th><th>NumPy 대비 속도</th></tr></thead><tbody><tr><td>유틸리티</td><td><strong>0.09x</strong> (더 빠름)</td></tr><tr><td>I&#x2F;O</td><td>1.76x</td></tr><tr><td>집합 연산</td><td>2.06x</td></tr><tr><td>다항식</td><td>2.32x</td></tr><tr><td>정렬</td><td>14.05x</td></tr><tr><td>FFT</td><td>18.13x</td></tr><tr><td>선형대수</td><td>44.69x</td></tr></tbody></table>    <div class="alert alert--info">      <strong class="alert__title">WASM 가속 계획</strong>      <div class="alert__body">        <p>프로젝트에는 Zig로 작성한 WASM 백엔드 계획이 문서화되어 있다. <code>numpy-ts/wasm</code>으로 임포트하면 자동으로 WASM 커널을 사용하고, 작은 배열(256개 미만)에서는 자동으로 JS로 폴백하는 구조다. 아직 구현 전이지만, 실현되면 성능 격차가 크게 줄어들 것이다.</p>      </div>    </div>  <p>유틸리티나 I&#x2F;O처럼 이미 충분히 빠른 영역도 있고, 선형대수처럼 격차가 큰 영역도 있다. “Python 없이 브라우저에서 돌린다”는 가치와 성능 사이의 트레이드오프를 이해하고 사용해야 한다.</p><h2 id="테스트-전략이-인상적이다"><a href="#테스트-전략이-인상적이다" class="headerlink" title="테스트 전략이 인상적이다"></a>테스트 전략이 인상적이다</h2><p>numpy-ts의 테스트 구조는 꽤 체계적이다.</p><ol><li><strong>유닛 테스트</strong> — 50개 이상의 모듈별 테스트 (Vitest)</li><li><strong>검증 테스트</strong> — Python NumPy를 실제로 실행해서 결과를 비교하는 오라클 테스트 47개</li><li><strong>번들 테스트</strong> — CJS, ESM, 브라우저 IIFE 번들이 정상 동작하는지 확인 (Playwright)</li><li><strong>트리셰이킹 테스트</strong> — 단일 함수 임포트 시 번들 크기가 최소인지 esbuild&#x2F;rollup&#x2F;webpack으로 검증</li></ol><p>특히 검증 테스트가 눈에 띈다. <code>numpy-oracle.ts</code>라는 브릿지가 Python 프로세스를 띄워 실제 NumPy 코드를 실행하고, 그 결과를 JSON으로 받아와 TypeScript 출력과 비교한다. “NumPy와 동일하게 동작한다”는 주장을 코드로 증명하는 셈이다.</p><h2 id="아키텍처"><a href="#아키텍처" class="headerlink" title="아키텍처"></a>아키텍처</h2><p>내부 구조도 잘 설계되어 있다.</p><ul><li><strong><code>common/storage.ts</code></strong> — TypedArray를 감싸는 <code>ArrayStorage</code> 클래스. shape, strides, offset, dtype을 관리한다.</li><li><strong><code>common/ndarray-core.ts</code></strong> — 최소한의 <code>NDArrayCore</code> 클래스. Proxy 기반 브래킷 인덱싱 지원.</li><li><strong><code>common/dtype.ts</code></strong> — 13가지 dtype(<code>float64</code>, <code>float32</code>, <code>complex128</code>, <code>int32</code>, <code>bool</code> 등)과 NumPy와 동일한 타입 프로모션 규칙.</li><li><strong><code>common/ops/</code></strong> — 산술, 선형대수, FFT, 난수 등 20개 연산 모듈.</li><li><strong><code>core/</code></strong> — 트리셰이킹 가능한 래퍼 함수들.</li><li><strong><code>full/</code></strong> — <code>NDArrayCore</code>를 확장한 <code>NDArray</code> 클래스. 100개 이상의 체이너블 메서드. 스크립트로 자동 생성된다.</li></ul><p>풀 API의 <code>NDArray</code> 클래스가 스크립트(<code>scripts/generate-full.ts</code>)로 자동 생성된다는 점이 재미있다. core 함수 목록에서 메서드를 자동으로 만들어내므로 API 일관성이 보장된다.</p><h2 id="누구에게-유용할까"><a href="#누구에게-유용할까" class="headerlink" title="누구에게 유용할까?"></a>누구에게 유용할까?</h2><p>numpy-ts가 빛을 발할 수 있는 시나리오는 분명하다.</p><ul><li><strong>브라우저에서 수치 연산</strong>이 필요한 경우 — 인터랙티브 데이터 시각화, 교육용 도구</li><li><strong>Python 없이 NumPy API를 쓰고 싶은</strong> Node.js 프로젝트</li><li><strong>TypeScript 타입 안전성</strong>이 중요한 과학 컴퓨팅</li><li><strong>프로토타이핑</strong> — Python 코드를 TS로 빠르게 포팅할 때</li></ul><p>반면, 대규모 행렬 연산의 극한 성능이 필요하다면 여전히 Python NumPy(또는 <a href="https://www.tensorflow.org/js">TensorFlow.js</a>, <a href="https://onnxruntime.ai/">ONNX Runtime Web</a>)이 더 적합하다.</p><h2 id="마무리"><a href="#마무리" class="headerlink" title="마무리"></a>마무리</h2><p>numpy-ts는 “TypeScript에서 NumPy를 쓸 수 있을까?”라는 질문에 대한 진지한 답변이다. 93.9%의 API 커버리지, Python 오라클 기반 검증 테스트, 트리셰이킹과 세 가지 진입점을 통한 유연한 번들 전략까지 — 단순한 토이 프로젝트가 아니라 실용을 지향하는 라이브러리라는 것을 알 수 있다.</p><p>성능 격차는 분명히 존재하지만, WASM 가속이 실현되면 상당 부분 해소될 가능성이 있다. 무엇보다 “Python 런타임 없이 브라우저에서 NumPy API를 그대로 쓸 수 있다”는 것 자체가 이 프로젝트의 가장 큰 가치다.</p><p>관심이 있다면 <a href="https://numpyts.dev/">공식 문서</a>와 <a href="https://github.com/dupontcyborg/numpy-ts">GitHub 저장소</a>를 방문해보자.</p>]]>
    </content>
    <id>https://uyeong.github.io/blog/2026/03/10/numpy-ts-introduction/</id>
    <link href="https://uyeong.github.io/blog/2026/03/10/numpy-ts-introduction/"/>
    <published>2026-03-09T15:00:00.000Z</published>
    <summary>Python 과학 컴퓨팅의 핵심인 NumPy를 TypeScript로 재구현한 numpy-ts를 소개한다. 93.9% API 커버리지, 트리셰이킹, 제로 디펜던시까지 갖춘 이 라이브러리를 자세히 들여다본다.</summary>
    <title>NumPy를 TypeScript로 옮긴다고? numpy-ts 살펴보기</title>
    <updated>2026-03-11T00:47:22.426Z</updated>
  </entry>
  <entry>
    <author>
      <name>Coderifleman</name>
    </author>
    <category term="JavaScript" scheme="https://uyeong.github.io/blog/categories/JavaScript/"/>
    <category term="React" scheme="https://uyeong.github.io/blog/categories/JavaScript/React/"/>
    <category term="JavaScript" scheme="https://uyeong.github.io/blog/tags/JavaScript/"/>
    <category term="Architecture" scheme="https://uyeong.github.io/blog/tags/Architecture/"/>
    <category term="React" scheme="https://uyeong.github.io/blog/tags/React/"/>
    <category term="Design" scheme="https://uyeong.github.io/blog/tags/Design/"/>
    <content>
      <![CDATA[<p>React, 아니 근래의 대부분 프레임워크는 컴포넌트를 지향한다. 문제를 작은 단위로 나누고 그것을 블랙박스 형태로 잘 포장, 조합하여 더 큰 문제를 해결하는 방법은 모든 공학 분야에서 공통적으로 애용하는 문제 해결 접근법이다.</p><p>하지만 소프트웨어 세계에서는 이런 접근법이 꽤 자주 지켜지지 않을 때가 있다. 가령 여러분이 은행 서비스의 주요 기능 중 하나인 송금 기능을 만들어야 한다고 해보자. 송금 기능에서 우리는 다양한 애플리케이션 비즈니스 로직을 도출할 수 있는데 여기에서는 간단히 송금액을 입력하는 과정만 생각해보자.</p><ol><li>송금액 입력 시 대상 계좌의 잔여금을 초과하지 않아야 한다.</li><li>송금액 입력 시 일회 이체 한도를 초과하지 않아야 한다.</li><li>송금액 입력 시 일일 이체 한도를 초과하지 않아야 한다.</li><li>초과 입력 시 송금이 가능한 최대 금액으로 자동 조정하고, 사용자에게 상황을 인지시켜야 한다.</li></ol><p>여러분은 유지보수성, 재사용성 등을 위해 송금액 입력 UI 요소와 관련 애플리케이션 비즈니스 로직을 하나의 컴포넌트 단위로 잘 포장하고 싶을 것이다.</p><pre class="language-jsx" data-language="jsx"><code class="language-jsx"><span class="token keyword">interface</span> <span class="token class-name">Props</span> <span class="token punctuation">&#123;</span>  <span class="token literal-property property">account</span><span class="token operator">:</span> Account<span class="token punctuation">;</span><span class="token punctuation">&#125;</span><span class="token keyword">function</span> <span class="token function">RemittanceInput</span><span class="token punctuation">(</span><span class="token parameter"><span class="token punctuation">&#123;</span> account <span class="token punctuation">&#125;</span><span class="token operator">:</span> Props</span><span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>  <span class="token keyword">const</span> <span class="token punctuation">[</span>amount<span class="token punctuation">,</span> setAmount<span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token function">useState</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token keyword">const</span> <span class="token punctuation">[</span>message<span class="token punctuation">,</span> setMessage<span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token function">useState</span><span class="token punctuation">(</span><span class="token string">''</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token keyword">const</span> handleChange <span class="token operator">=</span> <span class="token function">useCallback</span><span class="token punctuation">(</span><span class="token punctuation">(</span>event<span class="token operator">:</span> ChangeEvent<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span><span class="token class-name">HTMLInputElement</span></span><span class="token punctuation">></span></span><span class="token plain-text">) => </span><span class="token punctuation">&#123;</span>    <span class="token comment">// 송금액 입력시 필요한 여러가지 검증 처리</span>  <span class="token punctuation">&#125;</span><span class="token plain-text">, []);  return (    </span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span></span><span class="token punctuation">></span></span><span class="token plain-text">      </span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span><span class="token punctuation">></span></span><span class="token plain-text">        </span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>span</span><span class="token punctuation">></span></span><span class="token plain-text">송금액 : </span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>span</span><span class="token punctuation">></span></span><span class="token plain-text">        </span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span>          <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>remittance<span class="token punctuation">"</span></span>          <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>string<span class="token punctuation">"</span></span>          <span class="token attr-name">value</span><span class="token script language-javascript"><span class="token script-punctuation punctuation">=</span><span class="token punctuation">&#123;</span>amount <span class="token operator">||</span> <span class="token string">''</span><span class="token punctuation">&#125;</span></span>          <span class="token attr-name">placeholder</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>송금액을 입력해 주세요.<span class="token punctuation">"</span></span>          <span class="token attr-name">onChange</span><span class="token script language-javascript"><span class="token script-punctuation punctuation">=</span><span class="token punctuation">&#123;</span>handleChange<span class="token punctuation">&#125;</span></span>        <span class="token punctuation">/></span></span><span class="token plain-text">      </span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">></span></span><span class="token plain-text">      </span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>p</span><span class="token punctuation">></span></span><span class="token punctuation">&#123;</span>message<span class="token punctuation">&#125;</span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>p</span><span class="token punctuation">></span></span><span class="token plain-text">    </span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span></span><span class="token punctuation">></span></span><span class="token plain-text">  );&#125;</span></code></pre>  <p>UML 다이어그램으로 표현해보면 다음과 같다(문법은 함수지만 상태를 갖게 된 이상 객체와 다를 게 없기 때문에 UML 다이어그램으로 표현할 수 있다).</p>    <figure title="컴포넌트 내에 모두 위치 시켰을 경우의 UML 다이어그램">      <a href="/blog/images/hands_on_react_making_it_cohesive/react_cohesive.02.png" target="_blank">        <img src="/blog/images/hands_on_react_making_it_cohesive/react_cohesive.02.png" alt="컴포넌트 내에 모두 위치 시켰을 경우의 UML 다이어그램">      </a>          </figure>  <p>이 컴포넌트만으로 문제를 해결 할 수 있다면 다행이지만 실제 세계는 좀 더 복잡하다. 예를 들어 컴포넌트 내에서 관리하는 송금액(<code>amount</code>) 상태를 외부에 배치해야 하는 경우라면 어떨까? 사용자가 송금 버튼을 클릭하면 송금액을 알아야 하기 때문에 <code>RemittanceInput</code> 컴포넌트 내부에 감출 수 없다. 이 문제를 해결하는 방법은 실로 다양하지만 일단 다음과 같이 간단하게 풀어보자.</p><pre class="language-jsx" data-language="jsx"><code class="language-jsx"><span class="token keyword">function</span> <span class="token function">App</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>  <span class="token keyword">const</span> <span class="token punctuation">[</span>account<span class="token punctuation">,</span> setAccount<span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token function">useAccount</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token keyword">const</span> <span class="token punctuation">[</span>amount<span class="token punctuation">,</span> setAmount<span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token function">useState</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token keyword">const</span> <span class="token punctuation">[</span>message<span class="token punctuation">,</span> setMessage<span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token function">useState</span><span class="token punctuation">(</span><span class="token string">''</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token keyword">const</span> handleChangeAccount <span class="token operator">=</span> <span class="token function">useCallback</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">&#123;</span>    <span class="token comment">// 계좌 선택 시 필요한 여러가지 검증 처리</span>  <span class="token punctuation">&#125;</span><span class="token punctuation">,</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token keyword">const</span> handleChangeRemittance <span class="token operator">=</span> <span class="token function">useCallback</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">&#123;</span>    <span class="token comment">// 송금액 입력시 필요한 여러가지 검증 처리</span>  <span class="token punctuation">&#125;</span><span class="token punctuation">,</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token keyword">return</span> <span class="token punctuation">(</span>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span></span><span class="token punctuation">></span></span><span class="token plain-text">      </span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span><span class="token class-name">AccountSelecor</span></span> <span class="token attr-name">value</span><span class="token script language-javascript"><span class="token script-punctuation punctuation">=</span><span class="token punctuation">&#123;</span>account<span class="token punctuation">&#125;</span></span> <span class="token attr-name">onChange</span><span class="token script language-javascript"><span class="token script-punctuation punctuation">=</span><span class="token punctuation">&#123;</span>handleChangeAccount<span class="token punctuation">&#125;</span></span> <span class="token punctuation">/></span></span><span class="token plain-text">      </span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span><span class="token class-name">RemittanceInput</span></span> <span class="token attr-name">value</span><span class="token script language-javascript"><span class="token script-punctuation punctuation">=</span><span class="token punctuation">&#123;</span>amount<span class="token punctuation">&#125;</span></span> <span class="token attr-name">message</span><span class="token script language-javascript"><span class="token script-punctuation punctuation">=</span><span class="token punctuation">&#123;</span>message<span class="token punctuation">&#125;</span></span>  <span class="token attr-name">onChange</span><span class="token script language-javascript"><span class="token script-punctuation punctuation">=</span><span class="token punctuation">&#123;</span>handleChangeRemittance<span class="token punctuation">&#125;</span></span> <span class="token punctuation">/></span></span><span class="token plain-text">      </span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span><span class="token class-name">RemitButton</span></span> <span class="token attr-name">account</span><span class="token script language-javascript"><span class="token script-punctuation punctuation">=</span><span class="token punctuation">&#123;</span>account<span class="token punctuation">&#125;</span></span> <span class="token attr-name">amount</span><span class="token script language-javascript"><span class="token script-punctuation punctuation">=</span><span class="token punctuation">&#123;</span>amount<span class="token punctuation">&#125;</span></span> <span class="token punctuation">/></span></span><span class="token plain-text">    </span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span></span><span class="token punctuation">></span></span>  <span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">&#125;</span></code></pre>  <p>송금액 상태와 관련된 애플리케이션 비즈니스 로직을 상위 컴포넌트로 모두 옮겼다. 이제 상태는 상위 컴포넌트에서 관리되므로 송금액 데이터가 필요한 다른 기능을 다루기 쉬워졌다. <code>RemittanceInput</code>은 수동적인 컴포넌트로 요소의 스타일만 결정할 뿐 송금액 입력과 관련한 어떠한 결정도 하지 않는다.</p><p>이러한 구현은 때에 따라서 충분할 수 있지만 유지보수성, 재사용성에 그다지 좋은 편은 아니다. 만약 다른 페이지에서도 <code>RemittanceInput</code>이 필요하다면 <code>App</code> 파일을 열어서 관련한 로직이 무엇인지 진중하게 살펴보며 살을 발라 복사해야 한다. 코드 중복은 두말 할 것 없고 중요한 로직을 누락하는 실수를 하기에도 좋다. 또, 추후 송금 정책이 변경된다면 송금액 입력 UI 요소가 있는 모든 페이지를 뒤져 빠짐없이 수정해야한다.</p><p>이번엔 송금액과 관련된 로직을 별도 훅스로 추출하고 <code>RemittanceInput</code> 가까이에 위치시켜보자. 그러면 테스트용이성도 높일 수 있으며 코드 중복이나 구현 누락도 피할 수 있다.</p><pre class="language-jsx" data-language="jsx"><code class="language-jsx"><span class="token keyword">interface</span> <span class="token class-name">Remittance</span> <span class="token punctuation">&#123;</span>  <span class="token literal-property property">amount</span><span class="token operator">:</span> number<span class="token punctuation">;</span>  <span class="token literal-property property">message</span><span class="token operator">:</span> string<span class="token punctuation">;</span>  <span class="token function-variable function">change</span><span class="token operator">:</span> <span class="token punctuation">(</span><span class="token parameter"><span class="token literal-property property">amount</span><span class="token operator">:</span> number</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token keyword">void</span><span class="token punctuation">;</span><span class="token punctuation">&#125;</span><span class="token keyword">function</span> <span class="token function">useRemittance</span><span class="token punctuation">(</span><span class="token parameter"><span class="token literal-property property">account</span><span class="token operator">:</span> Account</span><span class="token punctuation">)</span><span class="token operator">:</span> Remittance <span class="token punctuation">&#123;</span>  <span class="token keyword">const</span> <span class="token punctuation">[</span>amount<span class="token punctuation">,</span> setAmount<span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token function">useState</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token keyword">const</span> <span class="token punctuation">[</span>message<span class="token punctuation">,</span> setMessage<span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token function">useState</span><span class="token punctuation">(</span><span class="token string">''</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token keyword">const</span> change <span class="token operator">=</span> <span class="token function">useCallback</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter"><span class="token literal-property property">amount</span><span class="token operator">:</span> number</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">&#123;</span>    <span class="token comment">// 송금액 입력시 필요한 여러가지 검증 처리</span>  <span class="token punctuation">&#125;</span><span class="token punctuation">,</span> <span class="token punctuation">[</span>account<span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token keyword">return</span> <span class="token punctuation">&#123;</span>     amount<span class="token punctuation">,</span>     message<span class="token punctuation">,</span>     change   <span class="token punctuation">&#125;</span><span class="token punctuation">;</span><span class="token punctuation">&#125;</span></code></pre><p><code>useRemittance</code>는 <code>Account</code>의 데이터를 기준으로 송금액 입력을 검증한다. 표현 로직이 없어 테스트하기 쉬우므로 테스트용이성을 높인다.</p><p><code>App</code>은 다음과 같이 수정할 수 있다.</p><pre class="language-jsx" data-language="jsx"><code class="language-jsx"><span class="token keyword">function</span> <span class="token function">App</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>  <span class="token keyword">const</span> <span class="token punctuation">[</span>account<span class="token punctuation">,</span> setAccount<span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token function">useAccount</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token keyword">const</span> remittance <span class="token operator">=</span> <span class="token function">useRemittance</span><span class="token punctuation">(</span>account<span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token keyword">const</span> handleChangeAccount <span class="token operator">=</span> <span class="token function">useCallback</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">&#123;</span>    <span class="token comment">// 계좌 선택 시 필요한 여러가지 검증 처리</span>  <span class="token punctuation">&#125;</span><span class="token punctuation">,</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token keyword">return</span> <span class="token punctuation">(</span>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span></span><span class="token punctuation">></span></span><span class="token plain-text">      </span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span><span class="token class-name">AccountSelecor</span></span> <span class="token attr-name">value</span><span class="token script language-javascript"><span class="token script-punctuation punctuation">=</span><span class="token punctuation">&#123;</span>account<span class="token punctuation">&#125;</span></span> <span class="token attr-name">onChange</span><span class="token script language-javascript"><span class="token script-punctuation punctuation">=</span><span class="token punctuation">&#123;</span>handleChangeAccount<span class="token punctuation">&#125;</span></span> <span class="token punctuation">/></span></span><span class="token plain-text">      </span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span><span class="token class-name">RemittanceInput</span></span> <span class="token attr-name">remittacne</span><span class="token script language-javascript"><span class="token script-punctuation punctuation">=</span><span class="token punctuation">&#123;</span>remittance<span class="token punctuation">&#125;</span></span> <span class="token punctuation">/></span></span><span class="token plain-text">      </span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span><span class="token class-name">RemitButton</span></span> <span class="token attr-name">account</span><span class="token script language-javascript"><span class="token script-punctuation punctuation">=</span><span class="token punctuation">&#123;</span>account<span class="token punctuation">&#125;</span></span> <span class="token attr-name">amount</span><span class="token script language-javascript"><span class="token script-punctuation punctuation">=</span><span class="token punctuation">&#123;</span>remittance<span class="token punctuation">.</span>amount<span class="token punctuation">&#125;</span></span> <span class="token punctuation">/></span></span><span class="token plain-text">    </span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span></span><span class="token punctuation">></span></span>  <span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">&#125;</span></code></pre><p>다이어그램으로 표현하면 다음과 같다.</p>    <figure title="훅스를 분리한 경우의 다이어그램">      <a href="/blog/images/hands_on_react_making_it_cohesive/react_cohesive.03.png" target="_blank">        <img src="/blog/images/hands_on_react_making_it_cohesive/react_cohesive.03.png" alt="훅스를 분리한 경우의 다이어그램">      </a>          </figure>  <p>하나의 논리적인 패키지 단위를 만들고 그곳에 <code>RemittanceInput</code>, <code>useRemittance</code>를 함께 뒀다. <code>RemittanceInput</code>과 <code>useRemittance</code>는 <code>Remittance</code> 객체를 통해 소통한다. 서로 연관있는 것을 가까이 배치하여 관계에 대한 착오를 줄여 유지보수성을 높인다.</p><p>아직 아쉬운 부분이 있는데 논리적인 패키지 단위로 나눴으나 여전히 <code>RemittanceInput</code>과 <code>useRemittance</code>의 관계를 코드 상에서 알기 어렵다는 것이다. 그렇다고 <code>useRemittance</code>를 <code>RemittanceInput</code> 내부로 옮기면 송금액 데이터를 다른 컴포넌트가 참조할 수 없기 때문에 옳지 않다. 문제를 해결하기 위해 컨텍스트를 추가하여 <code>Remittance</code> 패키지를 좀 더 보강해보자.</p><pre class="language-jsx" data-language="jsx"><code class="language-jsx"><span class="token keyword">const</span> RemittanceContext <span class="token operator">=</span> createContext<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span><span class="token class-name">Remittance</span></span><span class="token punctuation">></span></span><span class="token plain-text">(</span><span class="token punctuation">&#123;</span>  <span class="token literal-property property">amount</span><span class="token operator">:</span> <span class="token number">0</span><span class="token punctuation">,</span>  <span class="token literal-property property">message</span><span class="token operator">:</span> <span class="token string">''</span><span class="token punctuation">,</span>  <span class="token function-variable function">change</span><span class="token operator">:</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token keyword">undefined</span><span class="token punctuation">&#125;</span><span class="token plain-text">);const RemittanceProvider = (  </span><span class="token punctuation">&#123;</span> children<span class="token punctuation">,</span> <span class="token operator">...</span>account <span class="token punctuation">&#125;</span><span class="token plain-text">: PropsWithChildren</span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span><span class="token class-name">Account</span></span><span class="token punctuation">></span></span><span class="token plain-text">) => </span><span class="token punctuation">&#123;</span>  <span class="token keyword">const</span> remittance <span class="token operator">=</span> <span class="token function">useRemittance</span><span class="token punctuation">(</span>account<span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token keyword">return</span> <span class="token punctuation">(</span>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span><span class="token class-name">RemittanceContext.Provider</span></span> <span class="token attr-name">value</span><span class="token script language-javascript"><span class="token script-punctuation punctuation">=</span><span class="token punctuation">&#123;</span>remittance<span class="token punctuation">&#125;</span></span><span class="token punctuation">></span></span><span class="token plain-text">      </span><span class="token punctuation">&#123;</span>children<span class="token punctuation">&#125;</span><span class="token plain-text">    </span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span><span class="token class-name">RemittanceContext.Provider</span></span><span class="token punctuation">></span></span>  <span class="token punctuation">)</span><span class="token punctuation">&#125;</span><span class="token plain-text">;</span></code></pre><p><code>Remittace</code> 객체를 컨텐스트 내에서 접근할 수 있도록 <code>RemittaceContext</code>와 <code>RemittanceProvider</code>를 추가했다. <code>RemittanceProvider</code>는 <code>useRemittance</code>의 반환값인 <code>Remittance</code> 객체를 <code>RemittaceContext.Provider</code>로 전달한다.</p><p>이제 <code>RemittacneContext</code>와 <code>RemittanceInput</code>을 연결하는 간단한 브릿지 컴포넌트를 작성해보자.</p><pre class="language-jsx" data-language="jsx"><code class="language-jsx"><span class="token keyword">const</span> <span class="token function-variable function">RemittanceInputWithContext</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">&#123;</span>  <span class="token keyword">const</span> remittance <span class="token operator">=</span> <span class="token function">useContext</span><span class="token punctuation">(</span>RemittanceContext<span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token keyword">return</span> <span class="token punctuation">(</span>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span><span class="token class-name">RemittanceInput</span></span> <span class="token attr-name">remittance</span><span class="token script language-javascript"><span class="token script-punctuation punctuation">=</span><span class="token punctuation">&#123;</span>remittance<span class="token punctuation">&#125;</span></span> <span class="token punctuation">/></span></span>  <span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">&#125;</span></code></pre><p><code>App</code>은 다음과 같다.</p><pre class="language-jsx" data-language="jsx"><code class="language-jsx"><span class="token keyword">function</span> <span class="token function">App</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>  <span class="token keyword">const</span> <span class="token punctuation">[</span>account<span class="token punctuation">,</span> setAccount<span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token function">useAccount</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token keyword">const</span> handleChangeAccount <span class="token operator">=</span> <span class="token function">useCallback</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">&#123;</span>    <span class="token comment">// 계좌 선택 시 필요한 여러가지 검증 처리</span>  <span class="token punctuation">&#125;</span><span class="token punctuation">,</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token keyword">return</span> <span class="token punctuation">(</span>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span></span><span class="token punctuation">></span></span><span class="token plain-text">      </span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span><span class="token class-name">AccountSelecor</span></span>          <span class="token attr-name">value</span><span class="token script language-javascript"><span class="token script-punctuation punctuation">=</span><span class="token punctuation">&#123;</span>account<span class="token punctuation">&#125;</span></span>          <span class="token attr-name">onChange</span><span class="token script language-javascript"><span class="token script-punctuation punctuation">=</span><span class="token punctuation">&#123;</span>handleChangeAccount<span class="token punctuation">&#125;</span></span>        <span class="token punctuation">/></span></span><span class="token plain-text">      </span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span><span class="token class-name">RemittacneProvider</span></span> <span class="token attr-name">account</span><span class="token script language-javascript"><span class="token script-punctuation punctuation">=</span><span class="token punctuation">&#123;</span>account<span class="token punctuation">&#125;</span></span><span class="token punctuation">></span></span><span class="token plain-text">        </span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span><span class="token class-name">RemittanceInputWithContext</span></span> <span class="token punctuation">/></span></span><span class="token plain-text">        </span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span><span class="token class-name">RemittacneContext.Consumer</span></span><span class="token punctuation">></span></span><span class="token plain-text">          </span><span class="token punctuation">&#123;</span><span class="token punctuation">(</span><span class="token parameter"><span class="token punctuation">&#123;</span> amount <span class="token punctuation">&#125;</span></span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span><span class="token class-name">RemitButton</span></span> <span class="token attr-name">account</span><span class="token script language-javascript"><span class="token script-punctuation punctuation">=</span><span class="token punctuation">&#123;</span>account<span class="token punctuation">&#125;</span></span> <span class="token attr-name">amount</span><span class="token script language-javascript"><span class="token script-punctuation punctuation">=</span><span class="token punctuation">&#123;</span>amount<span class="token punctuation">&#125;</span></span> <span class="token punctuation">/></span></span>        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span><span class="token class-name">RemittacneContext.Consumer</span></span><span class="token punctuation">></span></span><span class="token plain-text">      </span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span><span class="token class-name">RemittacneContext</span></span><span class="token punctuation">></span></span><span class="token plain-text">    </span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span></span><span class="token punctuation">></span></span><span class="token plain-text">  );&#125;</span></code></pre><p><code>RemittanceInputWithContext</code>의 이름을 통해 특정 컨텍스트가 필요하다는 사실을 알 수 있다. <code>RemittanceProvider</code> 내부로 <code>useRemittance</code>를 감추고 <code>RemittanceInput</code>과의 관계를 맺어주므로 더이상 클라이언트가 신경쓸 필요 없다. 이제 적절한 위치에 <code>RemittacneContext</code>를 배치하여 송금액 상태를 보존하고 필요시 다른 컴포넌트에서도 활용할 수 있도록 한다. 이제 송금액 UI 요소를 관련 애플리케이션 비즈니스 로직과 함께 재활용 할 수 있고, 때로는 <code>RemiitanceInput</code>을 직접 사용할 수도 있어 확장성도 달성한다.</p><p>다이어그램으로 표현하면 다음과 같다.</p>    <figure title="컨텍스트를 활용한 경우의 다이어그램">      <a href="/blog/images/hands_on_react_making_it_cohesive/react_cohesive.04.png" target="_blank">        <img src="/blog/images/hands_on_react_making_it_cohesive/react_cohesive.04.png" alt="컨텍스트를 활용한 경우의 다이어그램">      </a>          </figure>  <p>중요한 건 관심사다. 기능 단위로 분리하는 게 아닌 관심사 단위로, 이곳 저곳에서 필요로 하는 로직이라는 점에 초점을 맞추지 말고 응집성 있게 관리하면 유지보수성, 재사용성을 높일 수 있다.</p>]]>
    </content>
    <id>https://uyeong.github.io/blog/2023/02/27/hands_on_react_making_it_cohesive/</id>
    <link href="https://uyeong.github.io/blog/2023/02/27/hands_on_react_making_it_cohesive/"/>
    <published>2023-02-26T15:00:00.000Z</published>
    <summary>React.js에서 로직을 어떻게 응집성 있게 관리할 수 있는지 간단한 송금 예제와 함께 소개한다.</summary>
    <title>실전! 리액트, 응집성 있게 가자!</title>
    <updated>2026-03-11T00:47:22.423Z</updated>
  </entry>
  <entry>
    <author>
      <name>Coderifleman</name>
    </author>
    <category term="Test" scheme="https://uyeong.github.io/blog/categories/Test/"/>
    <category term="Weekly" scheme="https://uyeong.github.io/blog/tags/Weekly/"/>
    <category term="FrontEnd" scheme="https://uyeong.github.io/blog/tags/FrontEnd/"/>
    <category term="Translate" scheme="https://uyeong.github.io/blog/tags/Translate/"/>
    <category term="JSer" scheme="https://uyeong.github.io/blog/tags/JSer/"/>
    <category term="Jser.info" scheme="https://uyeong.github.io/blog/tags/Jser-info/"/>
    <content>
      <![CDATA[<p>위클리의 홍수에 살고 있다. 다양한 위클리를 구독하고 메일을 통해서 받아보지만, 그 많은 양을 매일 처리하긴 여간 힘든일이 아니다. 결국에는 쌓이고 쌓여 산적한 소식지로 인해 메일함을 여는 것조차 부담을 느끼게 된다. </p><p>그 과정에서 필자가 취한 전략이 있다. 바로 다양한 소식지를 구독해서 집중도를 떨어뜨리기보다는 “하나만이라도 매번 제대로 챙겨보자.”다.</p><p>필자는 영어에는 그다지 경험과 지식이 없다. 하지만 일본어는 나름 학창 시절 긴 오타쿠 생활을 통해 습득한 일종의 짝퉁 일본어를 갖고 있다. 어디 가서 자랑할 만한 언어 능력은 아니지만 간단한 일본어 기술 문서는 파파고를 벗 삼아 살펴볼 수는 있다.</p><p>그렇게 일본에 괜찮은 프런트엔드 관련 위클리 서비스가 없을까 찾아보다가 만나게 된 게 <a href="https://jser.info/">JSer.info</a> 다.</p>    <figure title="JSer.info 사이트 메인 페이지의 모습">      <a href="/blog/images/jserinfo_translation_story/jserinfo.02.png" target="_blank">        <img src="/blog/images/jserinfo_translation_story/jserinfo.02.png" alt="JSer.info 사이트 메인 페이지의 모습">      </a>      <figcaption>&lt;JSer.info 메인 페이지&gt;</figcaption>    </figure>  <p>JSer.info는 필자와 같은 고민 위에서 탄생한 위클리다. 다양한 위클리 소식을 한데 취합해서 주요한 정보만 간추려 두세 줄의 설명 구와 함께 매주 소식을 발행한다는 목표를 갖고 있다(<a href="https://jser.info/ko/about/">JSer.info 소개</a> 참고).</p><h2 id="책임감-주입"><a href="#책임감-주입" class="headerlink" title="책임감 주입!"></a>책임감 주입!</h2><p>꾸준히 살펴보기 위해서는 동기가 필요하다. 단순히 읽기만 한다면 금세 바쁘다는 핑계로 건너뛰거나 내팽개칠 게 뻔하다(나란 놈. 그런 놈). 좋은 방법이 없을까 고민한 끝에 결정한 방법이 “번역해서 공유하자” 다.</p><p>이는 순수하게 필자를 위한 전략이다. 번역은 글에 더 집중하게 한다. 눈으로만 훑고 간 것과는 다르게 더 오래 기억에 남고 필요한 정보를 찾아 활용하는 데 도움을 준다. 이어서 꾸준히 읽기 위해서 공유라는 활동을 통해 지속할 수 있는 에너지를 만든다 즉, 일종의 책임감 주입이다.</p><p>2014년 초 즈음 <a href="https://www.nts-corp.com/">NTS</a>(N-Tech Service)에 근무할 당시 JSer.info의 주인장 <a href="https://github.com/azu">azu</a>에게 메일을 보내 번역에 대한 허락을 받은 후 사내 메일을 통해 공유를 시작했다. 얼마 안 가 NTS의 기술 블로그 <a href="https://wit.nts-corp.com/">WIT</a>가 오픈했고 사내 메일이 아닌 WIT에 포스팅하는 방식으로 공유 방법을 전환했다.</p>    <figure title="WIT 블로그에 번역 & 공유한 JSer.info">      <a href="/blog/images/jserinfo_translation_story/jserinfo.03.png" target="_blank">        <img src="/blog/images/jserinfo_translation_story/jserinfo.03.png" alt="WIT 블로그에 번역 & 공유한 JSer.info">      </a>      <figcaption>&lt;WIT 블로그에 번역 & 공유한 JSer.info&gt;</figcaption>    </figure>  <p>WIT에 몇 주간 소식지를 공유하니 “왜 여기에 올리고 있지?”라는 생각이 문뜩 들었다. JSer.info는 깃허브를 통해 관리되고 있어 직접 PR 하는 편이 더 좋았기 때문이다. 얼마안가 JSer.info에 직접 기여하기 시작했다.</p><p>2014년 초부터 시작했으니 대략 7년이나 흘렀다. 꽤나 대견하다 싶지만 부끄럽게도 책임감 주입은 반은 성공했고 반은 실패했다.</p><h2 id="꾸준히-하기가-제일-어려워"><a href="#꾸준히-하기가-제일-어려워" class="headerlink" title="꾸준히 하기가 제일 어려워!"></a>꾸준히 하기가 제일 어려워!</h2><p>공유라는 방법으로 책임감을 주입하는 방법은 매년 활동을 이어가는 데는 성공했지만 매주 꾸준히 번역하는 데는 실패했다. 프로젝트 막바지나 정신없는 QA 또는 프로그래밍 문제에 오랜 시간 몰입할 때에는 건너뛰기 일쑤였다.</p>    <figure title="2015, 2016, 2017 JSer.info 번역 현황">      <a href="/blog/images/jserinfo_translation_story/jserinfo.04.jpg" target="_blank">        <img src="/blog/images/jserinfo_translation_story/jserinfo.04.jpg" alt="2015, 2016, 2017 JSer.info 번역 현황">      </a>      <figcaption>&lt;2015, 2016, 2017 JSer.info 번역 현황&gt;</figcaption>    </figure>  <p>빨간색은 번역하지 않고 건너뛴 것. 하얀색은 번역해서 커밋한 것이다. 2015년에는 55개 중 22개, 2016년에는 58개 중 31개, 2017년에는 54개 중 30개를 번역했다. 번역하지 않은 소식지는 필자 자신도 살펴보지 않았음을 의미한다.</p><p>누두고 찾지 않더라도 어딘가 공개된 공간에서 활동을 시작하면 책임감을 갖고 꾸준히 이어갈 수 있다고 생각했지만 정작 어떤 피드백이 없는 상황에서 그런 마음을 갖기란 만만치 않은 일이었다.</p><p>이대로는 안 되겠다는 생각에 2019년 새해에 JSer.info 소식지를 매주 꾸준히 번역해서 공유하자는 목표를 세우기도 했다. 그 결과 53개 중 40개를 번역하여 이전보다는 나아졌지만, 목표는 달성하지 못했다. </p><h2 id="2021년은-어땟을까"><a href="#2021년은-어땟을까" class="headerlink" title="2021년은 어땟을까?"></a>2021년은 어땟을까?</h2>    <figure title="2021 JSer.info 번역 현황">      <a href="/blog/images/jserinfo_translation_story/jserinfo.05.png" target="_blank">        <img src="/blog/images/jserinfo_translation_story/jserinfo.05.png" alt="2021 JSer.info 번역 현황" style="max-width:min(100%, 340px); width:auto; height:auto;">      </a>      <figcaption>&lt;2021 JSer.info 번역 현황&gt;</figcaption>    </figure>  <p>2021년에는 2019년의 실패를 모범 삼아 다시 한번 의지를 두텁게 갖기 위해 자신을 다독였다. 현재 11월 30일까지의 소식지가 발행됐는데 총 48개 중 43개를 번역하고 공유했다. 지나간 소식이라 큰 의미는 없지만, 미번역한 과거 5개의 소식지도 모두 번역하여 채울 생각이다.</p><p>물론 매주 정해진 시간에 규칙적으로 공유하진 못했기 때문에 활동이 완벽했다고 평가할 순 없다. 한 주 밀려서 두 주 소식을 한 번에 PR 하거나 몇 주 잊고 있다가 다시 챙기는 등 다소 불규칙적으로 활동했다. </p><p>그럼에도 JSer.info를 공유하기 시작한 2014년 이후 가장 많은 소식을 그나마 꾸준하게 나눠왔음은 분명하다. 더 훌륭한 내년을 위해 스스로 희망 섞인 칭찬을 해주고 싶다.  </p><h2 id="더-나은-내년을-위해"><a href="#더-나은-내년을-위해" class="headerlink" title="더 나은 내년을 위해"></a>더 나은 내년을 위해</h2><p>내년에는 더 꾸준하고 규칙적인 JSer.info 활동을 다짐해본다. 한주도 빠짐없이 매주 목요일에 소식을 나눌 수 있기를 희망한다(물론 힘들 거라고 장담한다. 하지만 목표는 세워보자). </p><p>이 글을 빌어 꾸준히 위클리를 취합하고 발행하고 계신 azu님의 노고에도 감사하다는 말을 전한다(앞으로도 잘 부탁 합니다). 아! 참고로 <a href="jserinfo.slack.com">JSer.info 슬랙 채널</a>이 있다. 일본 쪽 커뮤니티라 언어가 아쉽긴 하지만 관심 있는 분은 참고하길 바란다.</p><p>혹시 JSer.info를 가끔이라도 살펴보는 독자분. 혹은 소식을 기다리는 독자분이 있다면 응원의 메시지와 관심을 부탁한다.</p><p>코로나 시국 2021년 모두 고생 많았고 더 나은 2022년이 되길 희망하며 내년에도 잘 부탁한다고 인사 올려본다.</p><h2 id="덧붙임"><a href="#덧붙임" class="headerlink" title="덧붙임"></a>덧붙임</h2>    <figure title="네이버웹툰 W FE TECH팀의 Slack 기술 뉴스 채널">      <a href="/blog/images/jserinfo_translation_story/jserinfo.06.jpg" target="_blank">        <img src="/blog/images/jserinfo_translation_story/jserinfo.06.jpg" alt="네이버웹툰 W FE TECH팀의 Slack 기술 뉴스 채널">      </a>      <figcaption>&lt;네이버웹툰 W FE TECH팀의 Slack 기술 뉴스 채널&gt;</figcaption>    </figure>  <p>팀 내에서는 <a href="https://realtime.jser.info/">Realtime JSer.info</a>를 구독하여 실시간으로 소식을 공유하고 있으며 좋은 동료 덕분에 함께 토론하고 기술 이해도를 높이는데 도움을 받고 있다. 우리와 함께하고 싶은 분은 언제든지 환영! 네이버웹툰의 <a href="https://recruit.webtoonscorp.com/webtoon/ko/job/detail?annoId=20007007&classId=170&jobId=&classNm=developer&entTypeCd=&searchTxt=&jobKeyword=">W FE TECH</a> 팀도 관심있게 봐주길 바란다.</p><p>끝!</p>]]>
    </content>
    <id>https://uyeong.github.io/blog/2021/12/07/jserinfo_translation_story/</id>
    <link href="https://uyeong.github.io/blog/2021/12/07/jserinfo_translation_story/"/>
    <published>2021-12-06T15:00:00.000Z</published>
    <summary>일본 프런트엔드 기술 위클리 JSer.info를 번역하고 공유해 온 이야기. 2021년 12월을 맞이하여 작성한 회고와 자기 반성글.</summary>
    <title>꾸준히 하기가 제일 어려워! JSer.info 번역 이야기</title>
    <updated>2026-03-11T00:47:22.424Z</updated>
  </entry>
  <entry>
    <author>
      <name>Coderifleman</name>
    </author>
    <category term="Test" scheme="https://uyeong.github.io/blog/categories/Test/"/>
    <category term="Development" scheme="https://uyeong.github.io/blog/tags/Development/"/>
    <category term="Test" scheme="https://uyeong.github.io/blog/tags/Test/"/>
    <category term="UnitTest" scheme="https://uyeong.github.io/blog/tags/UnitTest/"/>
    <content>
      <![CDATA[<p>소위 프런트엔드 그러니까 웹 애플리케이션은 왜 테스트하기 힘들까? 복잡하게 생각 할 필요 없다. 허구헌날 바뀌기 때문이다.</p><p>사용자 프로필을 출력하는 모달 컴포넌트를 떠올려보자. 여러분은 꽤나 신경써서 유닛 테스트도 작성했다. 신성한 초록색 바도 확인했다. 자 이제 PR을 보내볼까? 하는 순간 UI는 바뀐다(그… 그 정도는 아니야). 애써 작성한 유닛테스트의 3분의 1이 실패하고 테스트를 하나하나 고쳐야한다. 내일도 모레도 UI는 바뀌고 테스트는 다시 깨지며 또 고쳐야 한다.</p><p>시간이 지날수록 테스트 관리 비용은 크지만 얻을 수 있는 긍정적인 효과는 적다고 느껴진다. 점점 테스트와 거리가 생기기 시작하고 결국엔 이별을 맞이한다.</p>    <figure title="가! 가란말이야!">      <a href="/blog/images/a-strong-and-maintainable-test-strategy/test.02.png" target="_blank">        <img src="/blog/images/a-strong-and-maintainable-test-strategy/test.02.png" alt="가! 가란말이야!">      </a>      <figcaption>&lt;가! 가란말이야!!&gt;</figcaption>    </figure>  <p>UI는 자주 바뀐다. 왜냐하면 가장 바꾸기 쉽기 때문이다(심지어 효과적이다). 그래서 UI를 테스트하지 않겠다는 의견도 어느정도 이해가 된다. 하지만 UI에 대한 테스트 포기가 곧 모든 코드에 대한 테스트 포기로 이어져서는 안된다.</p><p>힘세고 오래가는 테스트 전략은 간단하다.</p><p>우선 테스트하기 편안해야 한다. 즉, 거치적거리는 것 없이 유닛 테스트를 작성할 수 있어야 한다. 밥과 함께 씹히는 돌을 한번은 참고 넘길 수 있어도 만약 계속 반복된다면 여러분은 숟가락을 집어던질 것이다.</p>    <figure title="에잇! 더는 못먹겠네!">      <a href="/blog/images/a-strong-and-maintainable-test-strategy/test.03.png" target="_blank">        <img src="/blog/images/a-strong-and-maintainable-test-strategy/test.03.png" alt="에잇! 더는 못먹겠네!">      </a>      <figcaption>&lt;에잇! 더는 못먹겠네!&gt;</figcaption>    </figure>  <p>두번째로 상대적으로 덜 변하는 것을 테스트한다. 덜 변하는 것은 그렇지 않은 것에 비해 더 중요한 내용을 담고 있기 마련이다. 다시 말해서 애플리케이션 규칙이나 서비스 정책을 식별하고, 표현이라는 맥락에서 분리할 수 있어야 한다.</p><p>정리하자면 다음과 같다.</p><ol><li>UI 컴포넌트는 개인의 판단에 따라 테스트 작성 여부를 결정할 수 있도록 한다.</li><li>중요한 것(상대적으로 덜 변하는 것)을 식별하여 테스트 작성하기 쉬운 방법으로 구현하고 테스트한다.</li></ol><p>어떻게 끝내야 할 지 모르겠지만 일단 하고 싶은 말은 여기까지다. </p><p>끝.</p>]]>
    </content>
    <id>https://uyeong.github.io/blog/2021/11/17/a-strong-and-maintainable-test-strategy/</id>
    <link href="https://uyeong.github.io/blog/2021/11/17/a-strong-and-maintainable-test-strategy/"/>
    <published>2021-11-16T15:00:00.000Z</published>
    <summary>웹 애플리케이션에서 무엇을 테스트 해야 할지 또, 어떤 관점에서 코드를 바라봐야 장기적으로도 유용한 결과물을 만들 수 있는지 이야기합니다.</summary>
    <title>힘세고 오래가는 테스트 전략</title>
    <updated>2026-03-11T00:47:22.420Z</updated>
  </entry>
  <entry>
    <author>
      <name>Coderifleman</name>
    </author>
    <category term="JavaScript" scheme="https://uyeong.github.io/blog/categories/JavaScript/"/>
    <category term="React" scheme="https://uyeong.github.io/blog/categories/JavaScript/React/"/>
    <category term="Development" scheme="https://uyeong.github.io/blog/tags/Development/"/>
    <category term="Test" scheme="https://uyeong.github.io/blog/tags/Test/"/>
    <category term="UnitTest" scheme="https://uyeong.github.io/blog/tags/UnitTest/"/>
    <category term="JavaScript" scheme="https://uyeong.github.io/blog/tags/JavaScript/"/>
    <category term="React" scheme="https://uyeong.github.io/blog/tags/React/"/>
    <category term="Design" scheme="https://uyeong.github.io/blog/tags/Design/"/>
    <category term="Hooks" scheme="https://uyeong.github.io/blog/tags/Hooks/"/>
    <category term="ReactHooks" scheme="https://uyeong.github.io/blog/tags/ReactHooks/"/>
    <category term="HumbleObjectPattern" scheme="https://uyeong.github.io/blog/tags/HumbleObjectPattern/"/>
    <category term="DesignPattern" scheme="https://uyeong.github.io/blog/tags/DesignPattern/"/>
    <content>
      <![CDATA[<p>UI를 개발하는 프런트엔드 개발자라면 누구나 빠질 수 있는 함정이 있다. UI 개발 특성상 무엇보다 뷰가 중요해 보이기 때문에 너무나 쉽게 뷰부터 시작한다는 것. </p><p>컴포넌트를 작성하다가 필요한 데이터가 있으면 선언하고, 이어서 작성하다가 장황한 로직이 생기면 커스텀 훅스로 분리하는 등 일정한 규칙 없이 산발적으로 이뤄진다. 자연스럽게 중요한 모델 정보와 그 모델을 조작하는 로직도 산발적으로 흩뿌려지는데 결과적으로 테스트하기 매우 불편한 코드가 작성되고 결국엔 테스트를 포기하게 한다.</p><p>중 &#x2F; 대규모 프로젝트에 참여해 본 사람은 알 것이다. 낮은 응집성이나 관심사 파편화도 큰 문제지만 무엇보다 테스트 코드 없는 영역을 수정해야 할 때만큼 불안감을 가져다주는 경우는 없다. UI 개발에서 테스트가 어렵다는 건 충분히 공감한다. 하지만 중요한 비즈니스 로직은 가능한한 테스트를 작성해야만 한다.</p><h2 id="험블-객체-패턴"><a href="#험블-객체-패턴" class="headerlink" title="험블 객체 패턴"></a>험블 객체 패턴</h2><blockquote><p>험블 객체 패턴은 디자인 패턴으로, 테스트하기 어려운 행위와 테스트하기 쉬운 행위를 단위 테스트 작성자가 분리하기 쉽게하는 방법으로 … 중략 … 가장 기본적인 본질은 남기고, 테스트하기 어려운 행위를 모두 험블 객체로 옮긴다. 나머지 모듈에는 험블 객체에 속하지 않은, 테스트하기 쉬운 행위를 모두 옮긴다.</p><p>Clean Architecture, 로버트 C. 마틴 저 &#x2F; 송준이 역 | 인사이트(insight) 출판</p></blockquote><p>뷰 그러니까 리액트 컴포넌트는 험블 객체다. 테스트하기 어렵고 불편하다. 이곳에 중요한 로직을 두면 자연스럽게 그 로직도 테스트하기 어려워진다. 중요한 로직은 컴포넌트에서 분리하고 테스트하기 쉬운 곳에 두어야 한다. 그럼 테스트하기 쉬운 공간이 어딜까?</p><h2 id="건강한-개발-루틴"><a href="#건강한-개발-루틴" class="headerlink" title="건강한 개발 루틴"></a>건강한 개발 루틴</h2><p>그 공간을 이야기 하기에 앞서 건강한 개발 루틴을 이야기해보자. 필자가 말하는 건강한 개발 루틴이란 개발의 순서가 일정하며 그 과정에 생산된 코드가 충분히 검증될 수 있는 순서와 방법을 말한다. </p><ol><li>요구사항을 분석하고 모델을 정리한다.</li><li>모델을 토대로 데이터를 실체화하고 비즈니스 로직을 작성한다.</li><li>비즈니스 로직을 검증하는 테스트 코드를 작성한다.</li><li>뷰를 작성하고 사용자 이벤트와 비즈니스 로직을 잇는다.</li><li>(중요도에 따라) 컴포넌트를 검증하는 테스트 코드를 작성한다.</li></ol><p>컴포넌트에서 시작하지 말자. 만들어야 할 것이 무엇인지 알아야만 좋은 코드를 작성 할 수 있다. 키보드에서 손을 내려놓고 조금은 멀리서 관찰할 시간이 필요하다. 어떤 문제를 해결해야 하는지 이해하고 다양한 정책을 찾고 정리해야 한다.</p><p>그러고 나면 모델을 찾을 수 있다. 그리고 이 모델을 토대로 데이터를 실체화하고 표현에 활용한다. 이제 구현할 데이터와 비즈니스 로직을 어디에 두어야 할지 고민할 순서다. 중요한 로직은 테스트하기 쉬운 곳에 둬야 한다. 그래야만 지속해서 테스트를 작성하고 커버리지도 높일 수 있다.</p><p>테스트하기 쉬운 공간? 그래 훅스다. 훅스를 활용하면 중요한 데이터와 정책을 효율적으로 캡슐화하고 관리할 수 있다. 훅스를 단순히 뷰만을 위한 함수 공간 정도로 바라보는 관점에서 벗어나면 더 다양한 아키텍처를 찾아낼 수 있다.</p><h2 id="사례-소개"><a href="#사례-소개" class="headerlink" title="사례 소개"></a>사례 소개</h2><h3 id="useBasicFormData"><a href="#useBasicFormData" class="headerlink" title="useBasicFormData"></a>useBasicFormData</h3><pre class="language-typescript" data-language="typescript"><code class="language-typescript"><span class="token keyword">function</span> <span class="token function">useBasicFormData</span><span class="token punctuation">(</span>initialData<span class="token operator">:</span> UserBasicData<span class="token punctuation">)</span><span class="token operator">:</span> State <span class="token punctuation">&#123;</span>  <span class="token keyword">const</span> <span class="token punctuation">[</span>state<span class="token punctuation">,</span> setState<span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token function">useState</span><span class="token punctuation">(</span>initialData<span class="token punctuation">)</span>  <span class="token keyword">const</span> <span class="token function-variable function">changeName</span> <span class="token operator">=</span> <span class="token punctuation">(</span>name<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">&#123;</span><span class="token comment">/* ... */</span><span class="token punctuation">&#125;</span><span class="token punctuation">;</span>  <span class="token keyword">const</span> <span class="token function-variable function">changeEmail</span> <span class="token operator">=</span> <span class="token punctuation">(</span>email<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">&#123;</span><span class="token comment">/* ... */</span><span class="token punctuation">&#125;</span><span class="token punctuation">;</span>  <span class="token keyword">const</span> <span class="token function-variable function">changeWorkStatus</span> <span class="token operator">=</span> <span class="token punctuation">(</span>workStatus<span class="token operator">:</span> WorkStatus<span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">&#123;</span>    <span class="token keyword">const</span> newState <span class="token operator">=</span> <span class="token function">produce</span><span class="token punctuation">(</span>state<span class="token punctuation">,</span> draft <span class="token operator">=></span> <span class="token punctuation">&#123;</span>      <span class="token keyword">if</span> <span class="token punctuation">(</span>workStatus <span class="token operator">===</span> WorkStatus<span class="token punctuation">.</span>InEmploy<span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>        draft<span class="token punctuation">.</span>offboardingDate <span class="token operator">=</span> <span class="token keyword">undefined</span><span class="token punctuation">;</span>      <span class="token punctuation">&#125;</span>      draft<span class="token punctuation">.</span>workStatus <span class="token operator">=</span> workStatus<span class="token punctuation">;</span>    <span class="token punctuation">&#125;</span><span class="token punctuation">)</span><span class="token punctuation">;</span>    <span class="token function">setState</span><span class="token punctuation">(</span>newState<span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token punctuation">&#125;</span><span class="token punctuation">;</span>  <span class="token comment">/* ... 생략 ... */</span><span class="token punctuation">&#125;</span></code></pre><p><code>useBasicFormData</code>는 사용자 기본 정보를 관리하는 훅스다. 해당 훅스 내에는 사용자의 기본 정보 즉, 이름이나 이메일, 근무 상태 등을 변경할 수 있는 함수가 존재한다. </p><p>여기에서 <code>changeWorkStatus</code> 함수 내 위치한 분기 문에 집중해보자. 해당 분기 문은 “근무 상태를 재직자로 설정하면 퇴직일을 초기화해야 한다”라는 서비스 정책을 나타내는 중요한 로직이다. 이 정책은 테스트 될 필요가 있으며 또 드러나야 한다.</p><p>이 로직이 특정 컴포넌트 공간에 존재한다고 생각해보자. 해당 컴포넌트에는 다른 다양한 로직이 뒤섞여 있다. 테스트는 둘째치고 중요한 로직이 여러 곳에 흩뿌려지거나 드러나지 않아 지나치기 쉽다.</p><h3 id="테스트-케이스"><a href="#테스트-케이스" class="headerlink" title="테스트 케이스"></a>테스트 케이스</h3><pre class="language-typescript" data-language="typescript"><code class="language-typescript"><span class="token function">it</span><span class="token punctuation">(</span><span class="token string">'재직자로 설정하면 퇴직일이 undefined로 설정 돼야 한다.'</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">&#123;</span>  <span class="token keyword">const</span> <span class="token punctuation">&#123;</span> result <span class="token punctuation">&#125;</span> <span class="token operator">=</span> <span class="token function">renderHook</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token function">useBasicFormData</span><span class="token punctuation">(</span><span class="token punctuation">&#123;</span>    workStatus<span class="token operator">:</span> WorkStatus<span class="token punctuation">.</span>OffBoard<span class="token punctuation">,</span>    offboardingDate<span class="token operator">:</span> <span class="token keyword">new</span> <span class="token class-name">Date</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span>  <span class="token punctuation">&#125;</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token function">act</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">&#123;</span>    result<span class="token punctuation">.</span>current<span class="token punctuation">.</span><span class="token function">changeWorkStatus</span><span class="token punctuation">(</span>WorkStatus<span class="token punctuation">.</span>InEmploy<span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token punctuation">&#125;</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token function">expect</span><span class="token punctuation">(</span>result<span class="token punctuation">.</span>current<span class="token punctuation">.</span>workStatus<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">toEqual</span><span class="token punctuation">(</span><span class="token string">'IN_EMPLOY'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token function">expect</span><span class="token punctuation">(</span>result<span class="token punctuation">.</span>current<span class="token punctuation">.</span>offboardingDate<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">toBeUndefined</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">&#125;</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre><p>훅스 테스트에 <code>@testing-library/react-hooks</code>의 <code>renderHook</code>과 <code>act</code>를 사용하고 있다. 해당 라이브러리가 훅스를 한층 더 테스트하기 쉽도록 한다.</p><p><code>useBaiscFormData</code>로 데이터를 생성하고 <code>act</code>를 이용해 근무상태를 변경한다. 변경 후 <code>offboardingDate</code>가 <code>undefined</code>으로 변경됐는지 확인하여 정책을 검증한다.</p><p>훅스 테스트는 컴포넌트보다 상대적으로 쉽고 단순하다. 이곳에 데이터와 비즈니스 로직을 두는 것으로 험블 객체 패턴을 실현하고 패턴의 장점을 취할 수 있다. </p><h2 id="끝으로"><a href="#끝으로" class="headerlink" title="끝으로"></a>끝으로</h2><p>리액트 v16.8에 훅스가 추가되고 벌써 2년이 지났다. 커뮤니티는 Redux 기반 리액트 애플리케이션에서 훅스와 컨텍스트를 활용하는 방식으로 전환하고 있다. 필자는 Redux 기반 애플리케이션에서 데이터와 비즈니스 로직을 액션에 작성하고 테스트했다. 액션 생성자라는 단순한 함수 정의에서 벗어나면 뷰 외의 다양한 로직을 테스트하기에 좋은 공간이 된다.</p><p>하지만 훅스와 컨텍스트 기반으로 넘어오면서 건강한 개발 루틴을 잠시 잃었다. 필자는 훅스를 단순하게 해석했고 때문에 비즈니스 로직을 둘 적당한 장소가 없어 자연스럽게 컴포넌트부터 시작했는데 이때부터 혼란이 찾아왔다.</p><p>현재는 애플리케이션 규모에 따라 훅스를 여러 가지 용도로 정의하고 활용한다. 훅스를 뷰만을 위한 단순한 기능이 아니라 뷰보다 저수준인 다른 계층으로써 활용하면 개발 간 만나는 많은 문제를 해결할 수 있다.</p><p>이해를 돕기 위해 할 일 관리 애플리케이션을 작성했다. 관심 있는 분은 참고하길 바란다. 해당 예제는 요구사항 규모에 맞춰 작성했으므로 구현 형태, 디렉터리 구조 등이 다른 애플리케이션에는 잘 어울리지 않을 수 있다는 점을 인지해주길 바란다.</p><ul><li><a href="https://github.com/uyeong/todomvc-with-hooks">uyeong&#x2F;todomvc-with-hooks</a></li></ul><p>예제를 보면 <code>hooks</code> 디렉터리와 <code>sources</code> 디렉터리에 모두 훅스가 존재한다. 단순한 유틸리티 성 훅스를 하나의 디렉터리에 두게 되면 중요한 훅스가 파일 구조 내에서 드러나지 않기 때문에 <code>sources</code> 라는 별도의 디렉터리로 분리했다.</p><p>필자가 작성한 <a href="https://github.com/uyeong/todomvc-with-hooks/blob/main/src/sources/useTodos.ts">useTodos</a>와 클래스로 구현한 <a href="https://github.com/tastejs/todomvc/blob/gh-pages/examples/typescript-react/js/todoModel.ts">todoModel</a>를 함께 비교해보면 뭔가 아하! 하는 게 있지 않을까 기대한다.</p>]]>
    </content>
    <id>https://uyeong.github.io/blog/2021/04/21/react-hooks-and-humble-object-pattern-and-tests/</id>
    <link href="https://uyeong.github.io/blog/2021/04/21/react-hooks-and-humble-object-pattern-and-tests/"/>
    <published>2021-04-20T15:00:00.000Z</published>
    <summary>훅스 관점에서 어떻게 테스트 용이성과 유지보수성 높은 애플리케이션을 일관된 루틴으로 개발하고 설계할 수 있는지 소개한다.</summary>
    <title>리액트 훅스(react hooks)와 험블 객체 패턴(humble object pattern) 그리고 테스트</title>
    <updated>2026-03-11T00:47:22.426Z</updated>
  </entry>
  <entry>
    <author>
      <name>Coderifleman</name>
    </author>
    <category term="JavaScript" scheme="https://uyeong.github.io/blog/categories/JavaScript/"/>
    <category term="Security" scheme="https://uyeong.github.io/blog/categories/JavaScript/Security/"/>
    <category term="Development" scheme="https://uyeong.github.io/blog/tags/Development/"/>
    <category term="JavaScript" scheme="https://uyeong.github.io/blog/tags/JavaScript/"/>
    <category term="Security" scheme="https://uyeong.github.io/blog/tags/Security/"/>
    <category term="ECMAScript" scheme="https://uyeong.github.io/blog/tags/ECMAScript/"/>
    <category term="Node" scheme="https://uyeong.github.io/blog/tags/Node/"/>
    <content>
      <![CDATA[<div class="alert alert--info">      <strong class="alert__title">읽기전에...</strong>      <div class="alert__body">        <p>이 문서는 「<a href="https://jovi0608.hatenablog.com/entry/2018/10/19/083725">Node.jsにおけるプロトタイプ汚染攻撃とは何か</a>」를 번역한 글입니다. 원작자에게 번역 및 배포 허락을 받았습니다. 프로토타입 오염 취약성이 많은 분에게 알려지길 바랍니다.</p>      </div>    </div>  <h2 id="시작하면서"><a href="#시작하면서" class="headerlink" title="시작하면서"></a>시작하면서</h2><p>최근 까닭이 있어 노드의 보안 사항을 조사하고 있는데요. 올해 5월에 개최된 <a href="https://nsec.io/">North Sec 2018</a>, 보안 연구자 <a href="https://github.com/HoLyVieR">Olivier Arteau</a>의 “<a href="https://www.youtube.com/watch?v=LUsiFV3dsK8">Prototype pollution attacks in NodeJS applications</a>“라는 재미있는 발표를 발견했습니다.</p><p>이 발표의 논문, 발표 자료, 데모 영상을 깃허브에 공개했으며 때마침 발표 영상도 유튜브를 통해 공개됐습니다.</p><ul><li><a href="https://github.com/HoLyVieR/prototype-pollution-nsec18">HoLyVieR&#x2F;prototype-pollution-nsec18</a></li><li><a href="https://www.youtube.com/watch?v=LUsiFV3dsK8">Prototype pollution attacks in NodeJS applications</a></li></ul><p>이 발표에서는 공격자가 자바스크립트 언어 고유의 프로토타입 체인 동작 원리를 이용해 웹 서버를 공격하는 방법을 이야기합니다.</p><p>발표자는 npm에서 받을 수 있는 모듈을 조사해 lodash를 시작으로 많은 모듈에 프로토타입 오염 취약점이 있는 것을 발견하고 보고했습니다. 그리고 실제 취약점이 있는 Ghost CMS를 이용, 비밀번호 재설정 요청에 필요한 데이터를 변조해 서버상에서 계산기 애플리케이션을 실행시키는 데모까지 성공합니다.</p><p>자바스크립트 실행 환경에 있어 프로토타입 오염 발생 위험성은 오래전부터 이야기 돼 왔지만 이것이 Node.js 환경의 웹 서버를 공격하는데 활용될 것이라고는 생각지 못했을 것 같습니다.</p><p>이 문서에서는 개인적으로도 기억해둘 겸 해당 공격의 원리에 관해서 설명하고자 합니다.</p><h2 id="proto"><a href="#proto" class="headerlink" title="__proto__"></a>__proto__</h2><p>객체의 프로토타입을 참조하는 <code>__proto__</code>는 예로부터 보편적으로 사용해온 기능입니다. 정식 사양은 아니었지만, 실정과 구현 현황을 소급 인정하고 브라우저 간 호환을 위해 ECMAScript2015 사양에 추가됐습니다.</p><ul><li><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/proto">Object.prototype.__proto__ - MDN web docs</a></li></ul><p>이 외에도 <code>__proto__</code>에 대한 게터 &#x2F; 셋터와 같은 기능인 <code>Object.setPrototypeOf</code> &#x2F; <code>getPrototypeOf</code>도 규정돼 있습니다. 현재 Node.js 환경에서도 모두 사용할 수 있습니다. 하지만 MDN에서는 프로토타입을 변경하는 것을 비권장합니다. </p><h2 id="프로토타입-오염"><a href="#프로토타입-오염" class="headerlink" title="프로토타입 오염"></a>프로토타입 오염</h2><p>프로토타입 오염은 무엇일까. 방법에는 여러 가지 있겠지만 가장 기본은 객체 리터럴의 <code>__proto__</code>는 <code>Object.prototype</code>과 같다는 것을 이용해 다른 객체 속성에 영향을 주는 방식입니다.</p><pre class="language-javascript" data-language="javascript"><code class="language-javascript"><span class="token keyword">const</span> obj1 <span class="token operator">=</span> <span class="token punctuation">&#123;</span><span class="token punctuation">&#125;</span><span class="token punctuation">;</span>console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>obj1<span class="token punctuation">.</span>__proto__ <span class="token operator">===</span> <span class="token class-name">Object</span><span class="token punctuation">.</span>prototype<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// true</span>obj1<span class="token punctuation">.</span>__proto__<span class="token punctuation">.</span>polluted <span class="token operator">=</span> <span class="token number">1</span><span class="token punctuation">;</span><span class="token keyword">const</span> obj2 <span class="token operator">=</span> <span class="token punctuation">&#123;</span><span class="token punctuation">&#125;</span><span class="token punctuation">;</span>console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>obj2<span class="token punctuation">.</span>polluted<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// 1</span></code></pre><p>위 예제에서 obj1의 프로토타입 객체를 조작했습니다. 이제 아무 관계 없는 <code>obj2</code> 속성의 값(obj2.polluted)이 <code>undefined</code>가 아니라 <code>1</code>로 출력됩니다.</p><p>발표에서는 아래와 같은 객체 프로토타입 오염이 일어날 수 있는 세 가지 패턴을 소개합니다. 모두 <code>__proto__</code>을 포함한 문자열을 key로 이용해 정확하지 않은 데이터를 객체에 등록 시켜 <code>Object.prototype</code> 오염을 노리는 방식입니다.</p><h3 id="속성-설정"><a href="#속성-설정" class="headerlink" title="속성 설정"></a>속성 설정</h3><pre class="language-javascript" data-language="javascript"><code class="language-javascript"><span class="token keyword">function</span> <span class="token function">isObject</span><span class="token punctuation">(</span><span class="token parameter">obj</span><span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>  <span class="token keyword">return</span> obj <span class="token operator">!==</span> <span class="token keyword">null</span> <span class="token operator">&amp;&amp;</span> <span class="token keyword">typeof</span> obj <span class="token operator">===</span> <span class="token string">'object'</span><span class="token punctuation">;</span><span class="token punctuation">&#125;</span> <span class="token keyword">function</span> <span class="token function">setValue</span><span class="token punctuation">(</span><span class="token parameter">obj<span class="token punctuation">,</span> key<span class="token punctuation">,</span> value</span><span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>  <span class="token keyword">const</span> keylist <span class="token operator">=</span> key<span class="token punctuation">.</span><span class="token function">split</span><span class="token punctuation">(</span><span class="token string">'.'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token keyword">const</span> e <span class="token operator">=</span> keylist<span class="token punctuation">.</span><span class="token function">shift</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token keyword">if</span> <span class="token punctuation">(</span>keylist<span class="token punctuation">.</span>length <span class="token operator">></span> <span class="token number">0</span><span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>    <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span><span class="token function">isObject</span><span class="token punctuation">(</span>obj<span class="token punctuation">[</span>e<span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">)</span> obj<span class="token punctuation">[</span>e<span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token punctuation">&#123;</span><span class="token punctuation">&#125;</span><span class="token punctuation">;</span>    <span class="token function">setValue</span><span class="token punctuation">(</span>obj<span class="token punctuation">[</span>e<span class="token punctuation">]</span><span class="token punctuation">,</span> keylist<span class="token punctuation">.</span><span class="token function">join</span><span class="token punctuation">(</span><span class="token string">'.'</span><span class="token punctuation">)</span><span class="token punctuation">,</span> value<span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token punctuation">&#125;</span> <span class="token keyword">else</span> <span class="token punctuation">&#123;</span>    obj<span class="token punctuation">[</span>key<span class="token punctuation">]</span> <span class="token operator">=</span> value<span class="token punctuation">;</span>    <span class="token keyword">return</span> obj<span class="token punctuation">;</span>  <span class="token punctuation">&#125;</span><span class="token punctuation">&#125;</span> <span class="token keyword">const</span> obj1 <span class="token operator">=</span> <span class="token punctuation">&#123;</span><span class="token punctuation">&#125;</span><span class="token punctuation">;</span><span class="token function">setValue</span><span class="token punctuation">(</span>obj1<span class="token punctuation">,</span> <span class="token string">"__proto__.polluted"</span><span class="token punctuation">,</span> <span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token keyword">const</span> obj2 <span class="token operator">=</span> <span class="token punctuation">&#123;</span><span class="token punctuation">&#125;</span><span class="token punctuation">;</span>console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>obj2<span class="token punctuation">.</span>polluted<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// 1</span></code></pre><h3 id="객체-병합"><a href="#객체-병합" class="headerlink" title="객체 병합"></a>객체 병합</h3><pre class="language-javascript" data-language="javascript"><code class="language-javascript"><span class="token keyword">function</span> <span class="token function">merge</span><span class="token punctuation">(</span><span class="token parameter">a<span class="token punctuation">,</span> b</span><span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>  <span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">let</span> key <span class="token keyword">in</span> b<span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>    <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token function">isObject</span><span class="token punctuation">(</span>a<span class="token punctuation">[</span>key<span class="token punctuation">]</span><span class="token punctuation">)</span> <span class="token operator">&amp;&amp;</span> <span class="token function">isObject</span><span class="token punctuation">(</span>b<span class="token punctuation">[</span>key<span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>      <span class="token function">merge</span><span class="token punctuation">(</span>a<span class="token punctuation">[</span>key<span class="token punctuation">]</span><span class="token punctuation">,</span> b<span class="token punctuation">[</span>key<span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span>    <span class="token punctuation">&#125;</span> <span class="token keyword">else</span> <span class="token punctuation">&#123;</span>      a<span class="token punctuation">[</span>key<span class="token punctuation">]</span> <span class="token operator">=</span> b<span class="token punctuation">[</span>key<span class="token punctuation">]</span><span class="token punctuation">;</span>    <span class="token punctuation">&#125;</span>  <span class="token punctuation">&#125;</span>  <span class="token keyword">return</span> a<span class="token punctuation">;</span><span class="token punctuation">&#125;</span> <span class="token keyword">const</span> obj1 <span class="token operator">=</span> <span class="token punctuation">&#123;</span><span class="token literal-property property">a</span><span class="token operator">:</span> <span class="token number">1</span><span class="token punctuation">,</span> <span class="token literal-property property">b</span><span class="token operator">:</span><span class="token number">2</span><span class="token punctuation">&#125;</span><span class="token punctuation">;</span><span class="token keyword">const</span> obj2 <span class="token operator">=</span> <span class="token constant">JSON</span><span class="token punctuation">.</span><span class="token function">parse</span><span class="token punctuation">(</span><span class="token string">'&#123;"__proto__":&#123;"polluted":1&#125;&#125;'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token function">merge</span><span class="token punctuation">(</span>obj1<span class="token punctuation">,</span> obj2<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token keyword">const</span> obj3 <span class="token operator">=</span> <span class="token punctuation">&#123;</span><span class="token punctuation">&#125;</span><span class="token punctuation">;</span>console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>obj3<span class="token punctuation">.</span>polluted<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// 1</span></code></pre><h3 id="객체-복사"><a href="#객체-복사" class="headerlink" title="객체 복사"></a>객체 복사</h3><pre class="language-javascript" data-language="javascript"><code class="language-javascript"><span class="token keyword">function</span> <span class="token function">clone</span><span class="token punctuation">(</span><span class="token parameter">obj</span><span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>  <span class="token keyword">return</span> <span class="token function">merge</span><span class="token punctuation">(</span><span class="token punctuation">&#123;</span><span class="token punctuation">&#125;</span><span class="token punctuation">,</span> obj<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">&#125;</span> <span class="token keyword">const</span> obj1 <span class="token operator">=</span> <span class="token constant">JSON</span><span class="token punctuation">.</span><span class="token function">parse</span><span class="token punctuation">(</span><span class="token string">'&#123;"__proto__":&#123;"polluted":1&#125;&#125;'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token keyword">const</span> obj2 <span class="token operator">=</span> <span class="token function">clone</span><span class="token punctuation">(</span>obj1<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token keyword">const</span> obj3 <span class="token operator">=</span> <span class="token punctuation">&#123;</span><span class="token punctuation">&#125;</span><span class="token punctuation">;</span>console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>obj3<span class="token punctuation">.</span>polluted<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// 1</span></code></pre><p>위와 비슷한 기능을 제공하는 유저 모듈에서 프로토타입 오염 취약점이 발견, 수정되고 있습니다. 수정된 부분을 살펴보았는데 key에 <code>__proto__</code>가 있을 경우 건너뛰도록 돼 있습니다.</p><p>공격자는 외부에서 <code>Object.prototype</code>을 조작할 수 있기 때문에 for-in 문의 오작동을 노려 악의적으로 속성을 수정하거나 <code>toString</code>, <code>valueOf</code> 등의 메서드를 재정의할 수도 있습니다. DoS는 간단하게 일으킬 수 있겠네요.</p><h2 id="실제-공격"><a href="#실제-공격" class="headerlink" title="실제 공격"></a>실제 공격</h2><p>발표에서는 실제 CMS 서버에 비밀번호 재설정에 필요한 JSON을 조작해 공격하는 방법을 소개합니다.</p><p>아이러니하게 객체 프로토타입 오염 공격이 성공한 경우에 서버 크래시 없이 동작하도록 하는 것은 꽤 어려운 기술입니다. 데모에서는 여러가지 방안을 고안해 CMS 템플릿을 조작하고, 테스트용으로 남겨져 있는 템플릿을 조작하여 임의의 자바스크립트를 서버상에서 실행(계산기 앱을 시작) 시키는 과정을 보여줍니다.</p><p>이 글에서는 JSON을 받아 어떠한 처리를 하는 간단한 웹 API 서버를 이용해 프로토타입 오염 공격에 의해 응답이 조작되는 샘플을 소개합니다.</p><p>다음은 서버 코드입니다. 외부에서 전달받은 JSON을 그대로 복사하고 있습니다.</p><pre class="language-javascript" data-language="javascript"><code class="language-javascript"><span class="token keyword">function</span> <span class="token function">isObject</span><span class="token punctuation">(</span><span class="token parameter">obj</span><span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>  <span class="token keyword">return</span> obj <span class="token operator">!==</span> <span class="token keyword">null</span> <span class="token operator">&amp;&amp;</span> <span class="token keyword">typeof</span> obj <span class="token operator">===</span> <span class="token string">'object'</span><span class="token punctuation">;</span><span class="token punctuation">&#125;</span> <span class="token keyword">function</span> <span class="token function">merge</span><span class="token punctuation">(</span><span class="token parameter">a<span class="token punctuation">,</span> b</span><span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>  <span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">let</span> key <span class="token keyword">in</span> b<span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>    <span class="token comment">// 이 부분에서 key가 __proto__ 일 때에 건너뛰어야 한다.</span>    <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token function">isObject</span><span class="token punctuation">(</span>a<span class="token punctuation">[</span>key<span class="token punctuation">]</span><span class="token punctuation">)</span> <span class="token operator">&amp;&amp;</span> <span class="token function">isObject</span><span class="token punctuation">(</span>b<span class="token punctuation">[</span>key<span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>      <span class="token function">merge</span><span class="token punctuation">(</span>a<span class="token punctuation">[</span>key<span class="token punctuation">]</span><span class="token punctuation">,</span> b<span class="token punctuation">[</span>key<span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span>    <span class="token punctuation">&#125;</span> <span class="token keyword">else</span> <span class="token punctuation">&#123;</span>      a<span class="token punctuation">[</span>key<span class="token punctuation">]</span> <span class="token operator">=</span> b<span class="token punctuation">[</span>key<span class="token punctuation">]</span><span class="token punctuation">;</span>    <span class="token punctuation">&#125;</span>  <span class="token punctuation">&#125;</span>  <span class="token keyword">return</span> a<span class="token punctuation">;</span><span class="token punctuation">&#125;</span> <span class="token keyword">function</span> <span class="token function">clone</span><span class="token punctuation">(</span><span class="token parameter">obj</span><span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>  <span class="token keyword">return</span> <span class="token function">merge</span><span class="token punctuation">(</span><span class="token punctuation">&#123;</span><span class="token punctuation">&#125;</span><span class="token punctuation">,</span> obj<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">&#125;</span> <span class="token keyword">const</span> express <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'express'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token keyword">const</span> app <span class="token operator">=</span> <span class="token function">express</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>app<span class="token punctuation">.</span><span class="token function">use</span><span class="token punctuation">(</span>express<span class="token punctuation">.</span><span class="token function">json</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>app<span class="token punctuation">.</span><span class="token function">post</span><span class="token punctuation">(</span><span class="token string">'/'</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token parameter">req<span class="token punctuation">,</span> res</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">&#123;</span>  <span class="token comment">// 여기에서 악의적인 JSON을 그대로 복사함으로써 객체의 프로토타입 오염이 일어난다</span>  <span class="token keyword">const</span> obj <span class="token operator">=</span> <span class="token function">clone</span><span class="token punctuation">(</span>req<span class="token punctuation">.</span>body<span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token keyword">const</span> r <span class="token operator">=</span> <span class="token punctuation">&#123;</span><span class="token punctuation">&#125;</span><span class="token punctuation">;</span>  <span class="token comment">// 프로토타입 오염에 의해 r.status가 변조된다.</span>  <span class="token keyword">const</span> status <span class="token operator">=</span> r<span class="token punctuation">.</span>status <span class="token operator">?</span> r<span class="token punctuation">.</span>status<span class="token operator">:</span> <span class="token string">'NG'</span><span class="token punctuation">;</span>  res<span class="token punctuation">.</span><span class="token function">send</span><span class="token punctuation">(</span>status<span class="token punctuation">)</span><span class="token punctuation">&#125;</span><span class="token punctuation">)</span><span class="token punctuation">;</span>app<span class="token punctuation">.</span><span class="token function">listen</span><span class="token punctuation">(</span><span class="token number">1234</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre><p>클라이언트는 <code>__proto__</code> 속성을 갖는 JSON을 서버에 전달해 공격합니다.</p><pre class="language-javascript" data-language="javascript"><code class="language-javascript"><span class="token keyword">const</span> http <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'http'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token keyword">const</span> client <span class="token operator">=</span> http<span class="token punctuation">.</span><span class="token function">request</span><span class="token punctuation">(</span><span class="token punctuation">&#123;</span>  <span class="token literal-property property">host</span><span class="token operator">:</span> <span class="token string">'localhost'</span><span class="token punctuation">,</span>  <span class="token literal-property property">port</span><span class="token operator">:</span> <span class="token number">1234</span><span class="token punctuation">,</span>  <span class="token literal-property property">method</span><span class="token operator">:</span> <span class="token string">'POST'</span><span class="token punctuation">&#125;</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token parameter">res</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">&#123;</span>  res<span class="token punctuation">.</span><span class="token function">on</span><span class="token punctuation">(</span><span class="token string">'data'</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token parameter">chunk</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">&#123;</span>    console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>chunk<span class="token punctuation">.</span><span class="token function">toString</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token punctuation">&#125;</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">&#125;</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token keyword">const</span> data <span class="token operator">=</span> <span class="token string">'&#123;"__proto__":&#123;"status":"polluted"&#125;&#125;'</span><span class="token punctuation">;</span>client<span class="token punctuation">.</span><span class="token function">setHeader</span><span class="token punctuation">(</span><span class="token string">'content-type'</span><span class="token punctuation">,</span> <span class="token string">'application/json'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>client<span class="token punctuation">.</span><span class="token function">end</span><span class="token punctuation">(</span>data<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre><p>공격 결과. 전달한 JSON에 의해 서버의 객체 프로토타입이 오염돼 응답의 값이 <code>NG</code>가 아니라 <code>polluted</code>로 변경돼 내려옵니다.</p><pre class="language-javascript" data-language="javascript"><code class="language-javascript">$ node client<span class="token punctuation">.</span>jspolluted</code></pre><h2 id="대책"><a href="#대책" class="headerlink" title="대책"></a>대책</h2><p>이 공격을 방지하는 대책으로 다음 세 가지 방법이 있습니다.</p><ul><li><strong>Object.freeze</strong> : <code>Object.prototype</code>이나 <code>Object</code>를 <code>freeze</code>하여 변경을 불가능하게 하는 방법입니다. 부작용으로 정상적인 모듈임에도 이 조치로 동작하지 않을 수도 있습니다. </li><li><strong>JSON schema</strong> : <a href="https://ajv.js.org/">avj</a> 모듈 등을 사용해 JSON을 검증합니다.</li><li><strong>Map</strong> : key &#x2F; value를 저장하는데 객체를 사용하지 않고 <code>Map</code>을 사용합니다. 단, ES5 이전 환경에서는 사용할 수 없습니다.</li></ul><p>의식하지 않으면 언제든지 이 취약점이 노출될 수 있습니다.</p><h2 id="정리"><a href="#정리" class="headerlink" title="정리"></a>정리</h2><p>이 글을 정리하면서도 다른 객체를 단순히 깊은 복사 하는 것만으로 취약점이 노출된다는 사실에 놀랐습니다. 역시 외부에서 전달받은 데이터를 처리할 때엔 신중해야 합니다. </p><p>취약점이 알려진 사용자 모듈 대부분은 이미 고쳐진 상태입니다. 그럼에도 신경 쓰인다면 한번 <code>npm audit</code>으로 확인해보시기 바랍니다.</p><pre class="language-text" data-language="text"><code class="language-text">$ npm audit                        === npm audit security report === # Run  npm install lodash@4.17.11  to resolve 1 vulnerability   Low             Prototype Pollution   Package         lodash   Dependency of   lodash   Path            lodash   More info       https://nodesecurity.io/advisories/577   found 1 low severity vulnerability in 1 scanned package  run `npm audit fix` to fix 1 of them.</code></pre>]]>
    </content>
    <id>https://uyeong.github.io/blog/2019/07/19/prototype-pollution-attacks-in-nodejs/</id>
    <link href="https://uyeong.github.io/blog/2019/07/19/prototype-pollution-attacks-in-nodejs/"/>
    <published>2019-07-18T15:00:00.000Z</published>
    <summary>__proto__을 이용한 프로토타입 오염(prototype pollution) 공격의 원리를 설명하면서 노드 환경에서 실제 공격이 가능한 사례를 함께 소개합니다.</summary>
    <title>Node.js에서의 프로토타입 오염 공격이란 무엇인가</title>
    <updated>2026-03-11T00:47:22.426Z</updated>
  </entry>
</feed>
