<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>JJun</title>
        <link>https://velog.io/</link>
        <description>매일 발전하는 프론트엔드 개발자</description>
        <lastBuildDate>Sat, 25 Apr 2026 05:41:02 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>JJun</title>
            <url>https://velog.velcdn.com/images/yjh-1008/profile/28a2e3e0-649a-4524-807a-95d4a5feb20d/image.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. JJun. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/yjh-1008" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[AWS Kiro vs Cursor AI 뭐가 더 좋을까??]]></title>
            <link>https://velog.io/@yjh-1008/AWS-Kiro-vs-Cursor-AI-%EB%AD%90%EA%B0%80-%EB%8D%94-%EC%A2%8B%EC%9D%84%EA%B9%8C</link>
            <guid>https://velog.io/@yjh-1008/AWS-Kiro-vs-Cursor-AI-%EB%AD%90%EA%B0%80-%EB%8D%94-%EC%A2%8B%EC%9D%84%EA%B9%8C</guid>
            <pubDate>Sat, 25 Apr 2026 05:41:02 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>저는 지금 주 IDE로 Cursor를 주로 사용하고 있었습니다. 그러다 올해 전사 AI를 적극 도입하겠다는 공지가 올라왔고 곧이어 AWS Kiro를 도입하겠다는 의견이 생겼습니다. 그러면서 사내 Kiro Workshop에 몇 번 참가하게 되었고, 실무에서 몇 번 사용해보며 제가 느꼈던 Cursor와 Kiro의 차이점을 작성해보려고합니다.</p>
</blockquote>
<h2 id="차이점">차이점</h2>
<h3 id="1-top-down-vs-bottom-up">1. Top Down vs Bottom up</h3>
<p>Cursor를 그때그때 필요한 질문들을 날립니다. &quot;그래프 대시보드 페이지를 만들어 줘.&quot;, &quot;해당 페이지에서 발생하는 에러를 분석해줘&quot;와 같은 즉각적인 질문들을 자주 사용하는데 이는 빠른 개발 속도를 확보할 수 있지만 프로젝트가 지속되며 코드의 일관성이 떨어지는 경험을 자주 겪었습니다.
Kiro는 반대로 즉각적으로 질문을 날리는 방식 보다는 &quot;초반부터 문서를 잘 작성하여 개발을 맡겨보자&quot;라는 전통적인 Top Down 방식을 취하고 있습니다. Kiro는 초기에 &quot;나는 xxx페이지를 개발할거야&quot;라는 질문을 요청하면 .kiro에 아래의 3개와 같은 문서를 생성하며 개발을 시작합니다.</p>
<p><img src="https://velog.velcdn.com/images/yjh-1008/post/e46f93f2-592e-482e-aa1e-b31e33699c24/image.png" alt=""></p>
<p>출처: <a href="https://thenewstack.io/kiro-is-awss-specs-centric-answer-to-windsurf-and-cursor/">https://thenewstack.io/kiro-is-awss-specs-centric-answer-to-windsurf-and-cursor/</a></p>
<ol>
<li>requirements.md (요구사항): 내가 어떤 기술을 사용할것인지, 비즈니스 요구사항은 어떤건지에 대한 기초적인 요구사항을 작성합니다.</li>
<li>design.md (디자인 &amp; 설계): UI와 에러 처리 등 개발에 대한 요구사항을 작성합니다.</li>
<li>tasks.md (작업 목록): 구현 순서를 정하고 개발을 시작하며 Git Commit 과 같은 사용자와의 인터렉션을 진행합니다.</li>
</ol>
<p>이러한 Spec-Driven Development 방식으로 코드의 일관성을 유지하고 제가 예상하지 못했던 엣지 케이스를 조기에 해결하며 개발을 할 수 있도록 도와주었습니다.  </p>
<h3 id="2-토큰-사용량">2. 토큰 사용량</h3>
<p>Cursor를 사용하다보면 필수적으로 Steering, Hooks, MCP등 추가 기능을 붙여 사용하지만 주기적으로 Steering, MCP 등을 수정, 제거를 해주지 않는다면 토큰 사용량이 크게 늘어나 비용적으로도 부담이 되었습니다. 
Kiro는 이에 대한 해결책으로 <a href="https://kiro.dev/docs/powers/">Powers</a>를 제공합니다. 설명으로 Power는 Steering, MCP 등을 하나로 묶은 패지로 설명하고 있었습니다. Power의 온디맨드로 동작하여 필요시에만 MCP, Steering 등을 호출하기에 토큰 사용량을 개선할 수 있습니다.</p>
<h3 id="3-ide를-개발자만-사용하지-않을수도-있다">3. IDE를 개발자만 사용하지 않을수도 있다.</h3>
<p>Kiro 세미나를 들으며 가장 흥미로웠던 부분은 비개발자들의 사용을 적극 권장한다는 것이였습니다. &quot;Kiro를 통해 비개발자분들도 IDE를 사용하여 문서 자동화 및 PPT 작성을 할 수 있습니다.&quot;라는 AWS 강사님의 말씀이 흥미롭게 느껴졌습니다. Cursor로도 개발환경 및 운영환경에 대한 문서는 작성하지만 PPT 작성, 엑셀 작성 등과 같은 작업은 해본적이 없었기에 좀 더 신기하게 다가왔습니다.</p>
<h2 id="그러면-cursor보다-좋나">그러면 Cursor보다 좋나?</h2>
<p>이렇게만 보면 Kiro가 Cursor보다 더 좋은것 같이 느껴지지만 이러한 과정들이 꽤나 많은 시간이 소요되었습니다. 워크샵에서 진행한만큼 비교적 간단한 요구사항을 정리하여 개발을 시작했지만 워크샵 시간에 끝내지 못해서 집에서 실행했었거든요.. 프로젝트의 npm run dev를 실행하기까지 4시간 ~ 5시간 정도 소요되었던것 같습니다.</p>
<p>그 다음으로는 Kiro가 프로젝트 처음부터 도입하다보니 토큰의 사용량이 꽤나 많았습니다. Cluade Opus 4.6은 토큰 사용량이 2배 넘게 차이나서 사용하다 말다 했었는데요. 프로젝트 마무리 단계에 330 토큰 정도를 사용했습니다. 한달 내내 이런 방식으로 사용하면 토큰이 어마무시하게 사용될것 같아서 주의가 필요할 것 같습니다.</p>
<p>Spec-Driven Development 방식은 너무 좋고 매력적이지만 아직은 Cursor를 사용할것 같습니다. Cursor가 제공하는 빠른 결과물이 너무 좋기에 Kiro가 더 속도를 개선하면 고민해볼것 같습니다. 오늘도 글 읽어주셔서 감사합니다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Streamable HTTP란?]]></title>
            <link>https://velog.io/@yjh-1008/Streamable-HTTP%EB%9E%80</link>
            <guid>https://velog.io/@yjh-1008/Streamable-HTTP%EB%9E%80</guid>
            <pubDate>Mon, 09 Mar 2026 15:44:03 GMT</pubDate>
            <description><![CDATA[<h1 id="streamable-http란-무엇인가">Streamable HTTP란 무엇인가?</h1>
<p>Streamable HTTP는 기존 HTTP 위에서 동작하면서도, 요청·응답을 <strong>스트리밍</strong> 형태로 주고받을 수 있게 확장한 전송 방식입니다. MCP(Modal Context Protocol) 같은 AI 프로토콜에서 기존 HTTP+SSE(Server-Sent Events)의 한계를 보완하기 위해 도입되었습니다.</p>
<hr>
<h2 id="1-http와-sse의-한계">1. HTTP와 SSE의 한계</h2>
<p>기존의 HTTP와 SSE에는 아래와 같은 문제점이 있었습니다.</p>
<ul>
<li><p><strong>HTTP + SSE의 한계</strong></p>
<ul>
<li>긴 연결을 유지해야 해서 서버 리소스 부담이 큼.</li>
<li>API 호출용 엔드포인트와 SSE용 엔드포인트를 따로 관리해야 함.</li>
<li>일부 프록시, 게이트웨이, CDN 환경에서 SSE에 제약이 존재.</li>
<li>구조적으로 서버→클라이언트 단방향 스트림에 치우쳐 있어 양방향 상호작용 설계가 번거로움.</li>
</ul>
</li>
<li><p><strong>Streamable HTTP가 해결할 수 있는 것</strong></p>
<ul>
<li>표준 HTTP 메서드(GET, POST 등)를 그대로 쓰면서,</li>
<li>응답을 한 번에 보내지 않고 여러 조각(chunks)으로 나누어 “흘려보내는” 스트리밍 모델을 제공.</li>
<li>단일 엔드포인트에서 요청·응답·스트리밍을 모두 처리.</li>
<li>상태 비저장(stateless) 서버와도 잘 맞는 구조.</li>
</ul>
</li>
</ul>
<blockquote>
<p>HTTP는 유지하되, WebSocket과 같은 실시간성 경험&quot;을 만들기 위한 새로운 방식이 <strong>Streamable HTTP</strong>입니다</p>
</blockquote>
<hr>
<h2 id="2-streamable-http의-개념">2. Streamable HTTP의 개념</h2>
<blockquote>
<p>** 표준 HTTP 위에서 동작하는 스트리밍 응답 패턴**이다.</p>
</blockquote>
<ul>
<li>클라이언트는 HTTP 요청(주로 POST)을 보낸다.</li>
<li>서버는 HTTP 응답을 바로 닫지 않고 열어 둔 채,<ul>
<li>JSON 조각, 텍스트 블록, 이벤트 형태의 메시지를 여러 번에 걸쳐 전송한다.</li>
</ul>
</li>
<li>클라이언트는 이 스트림을 읽어가며 UI를 점진적으로 업데이트한다.</li>
</ul>
<p>예: AI에게 긴 보고서 요약을 요청했을 때, 기존 HTTP는 요약이 끝난 뒤 전체 텍스트를 한 번에 내려준다. Streamable HTTP에서는 “문단 단위” 혹은 “토큰 단위”로 결과가 생성되는 즉시 클라이언트로 흘려보낼 수 있다.</p>
<hr>
<h2 id="3-streamable-http의-특징">3. Streamable HTTP의 특징</h2>
<h3 id="31-스트리밍-응답">3.1 스트리밍 응답</h3>
<ul>
<li>서버는 응답을 한 번에 보내지 않고 여러 chunk로 분할해 전송한다.</li>
<li>클라이언트는 스트림이 열려 있는 동안 데이터가 올 때마다 소비할 수 있다.</li>
<li>사용자 경험 측면에서 “실시간에 가깝게” 결과를 볼 수 있다.</li>
</ul>
<h3 id="32-단일-엔드포인트--표준-http-메서드">3.2 단일 엔드포인트 + 표준 HTTP 메서드</h3>
<ul>
<li>보통 <code>/message</code> 같은 하나의 URL로 다음을 모두 처리한다.<ul>
<li>POST: 새로운 요청(예: 채팅 메시지, 작업 시작 등)</li>
<li>GET: 스트림 조회, 재개, 상태 확인 등</li>
</ul>
</li>
<li>별도의 SSE 전용 엔드포인트를 두지 않아도 된다.</li>
</ul>
<h3 id="33-상태-비저장stateless-서버-친화적">3.3 상태 비저장(stateless) 서버 친화적</h3>
<ul>
<li>서버는 매 요청을 독립적으로 처리하고, 필요 시 연결을 끊을 수 있다.</li>
<li>세션을 유지하려면 <code>Session-Id</code> 헤더나 토큰 등을 클라이언트가 함께 보내고,
서버는 이를 기반으로 필요한 범위 내에서만 상태를 관리한다.</li>
</ul>
<h3 id="34-재개resume-가능-설계">3.4 재개(resume) 가능 설계</h3>
<ul>
<li>메시지 ID, 오프셋(offset) 등의 개념을 두면 스트림을 중간부터 다시 받을 수 있다.</li>
<li>네트워크가 끊어졌을 때:<ul>
<li>클라이언트는 “마지막으로 받은 메시지 ID”를 기억해 두었다가,</li>
<li>다시 요청하면서 마지막으로 가져온 ID 이후로 재개를 요청할 수 있다.</li>
</ul>
</li>
</ul>
<h3 id="35-양방향-상호작용">3.5 양방향 상호작용</h3>
<ul>
<li>클라이언트 → 서버: 표준 HTTP 요청(POST, 경우에 따라 PUT/PATCH 등)</li>
<li>서버 → 클라이언트: 스트리밍 응답(텍스트/JSON 이벤트 등)</li>
<li>이를 조합하면 WebSocket 없이도 꽤 풍부한 양방향 상호작용 패턴을 만들 수 있다.</li>
</ul>
<hr>
<h2 id="4-http--sse와의-비교">4. HTTP + SSE와의 비교</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>HTTP + SSE</th>
<th>Streamable HTTP</th>
</tr>
</thead>
<tbody><tr>
<td>통신 방향</td>
<td>SSE는 서버→클라이언트 단방향 스트림, 요청은 별도 HTTP</td>
<td>GET/POST 기반으로 양방향 상호작용 구성 가능</td>
</tr>
<tr>
<td>엔드포인트 구조</td>
<td>REST API 엔드포인트 + SSE 엔드포인트를 별도로 운영</td>
<td><code>/message</code> 같은 단일 엔드포인트에서 요청·스트림 처리</td>
</tr>
<tr>
<td>연결 방식</td>
<td>긴-lived 연결 유지, 서버 리소스 점유 큼</td>
<td>필요 시에만 스트리밍, 짧은 HTTP 연결 조합도 가능</td>
</tr>
<tr>
<td>서버 상태</td>
<td>상태 유지 구조에 의존하는 경우가 많음</td>
<td>상태 비저장 서버 아키텍처에 잘 맞음</td>
</tr>
<tr>
<td>인프라 호환성</td>
<td>일부 프록시/게이트웨이에서 SSE 제한</td>
<td>표준 HTTP라 CDN·LB·API Gateway와 자연스럽게 호환</td>
</tr>
<tr>
<td>재시도/재개</td>
<td>끊어지면 전체 스트림을 처음부터 재시작</td>
<td>메시지 ID/offset 기반 재개 설계 용이</td>
</tr>
</tbody></table>
<hr>
<h2 id="5-동작-흐름-예시-mcp-스타일">5. 동작 흐름 예시 (MCP 스타일)</h2>
<p>MCP(Modal Context Protocol)에서 Streamable HTTP를 사용하는 시나리오를 예로 들면 다음과 같습니다.</p>
<h3 id="51-요청-전송">5.1 요청 전송</h3>
<ul>
<li>클라이언트는 MCP 서버의 <code>/message</code> 엔드포인트로 POST 요청을 보낸다.</li>
<li>요청 바디에는 JSON-RPC 2.0 형식의 메시지(예: 메서드, 파라미터 등)를 담는다.</li>
<li>인증이 필요하면 헤더에 토큰(JWT 등)을 함께 실어 보낸다.</li>
</ul>
<pre><code class="language-http">POST /message HTTP/1.1
Host: api.example.com
Authorization: Bearer &lt;token&gt;
Content-Type: application/json

{
  &quot;jsonrpc&quot;: &quot;2.0&quot;,
  &quot;id&quot;: &quot;123&quot;,
  &quot;method&quot;: &quot;chat.send&quot;,
  &quot;params&quot;: {
    &quot;message&quot;: &quot;요약 좀 해줘&quot;,
    &quot;stream&quot;: true
  }
}</code></pre>
<p>위 방식대로 호출하면 아래 방식처럼 api를 요청할 수 있습니다.</p>
<pre><code class="language-ts">const response = await fetch(&quot;/message&quot;, {
  method: &quot;POST&quot;,
  headers: {
    &quot;Content-Type&quot;: &quot;application/json&quot;,
    // Authorization 등 필요한 헤더...
  },
  body: JSON.stringify({
    jsonrpc: &quot;2.0&quot;,
    id: &quot;123&quot;,
    method: &quot;chat.send&quot;,
    params: { message: &quot;요약 좀 해줘&quot;, stream: true }
  })
});

const reader = response.body?.getReader();
const decoder = new TextDecoder();

while (true) {
  const { done, value } = await reader.read();
  if (done) break;

  const chunk = decoder.decode(value, { stream: true });
  // chunk에는 여러 JSON 라인이 섞여 있을 수 있으므로,
  // 줄바꿈 기준 split 후 개별 JSON 파싱 등의 처리가 필요.
  console.log(chunk);
}</code></pre>
<h2 id="6-정리">6. 정리</h2>
<p>오늘은 Streamable에 대해 알아보았습니다. 아직 현업에서 SSE를 사용하지만 고도화가 진행될 때 Streamable HTTP를 도입하자고 제안할 계획이기에 매우 유익했던 학습이었습니다. 오늘도 긴 글 읽어주셔서 감사합니다!</p>
<p><a href="https://wikidocs.net/288260">Streamable HTTP 구현하기</a>
<a href="https://bart0401.tistory.com/74">Streamable HTTP가 현업의 표준이 된 이유</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[MCP Inspector로 MCP Server 테스트 환경 만들기]]></title>
            <link>https://velog.io/@yjh-1008/MCP-Inspector%EB%A1%9C-MCP-Server-%ED%85%8C%EC%8A%A4%ED%8A%B8-%ED%99%98%EA%B2%BD-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@yjh-1008/MCP-Inspector%EB%A1%9C-MCP-Server-%ED%85%8C%EC%8A%A4%ED%8A%B8-%ED%99%98%EA%B2%BD-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Sat, 28 Feb 2026 10:56:05 GMT</pubDate>
            <description><![CDATA[<h2 id="1-서론">1. 서론</h2>
<blockquote>
<p>신규 프로젝트로 사용자가 MCP 정보를 입력하면 Docker 내에 MCP 서버를 자동으로 생성하고 구동시키는 프로젝트가 들어왔습니다. 프론트 구성을 하기 전 공부하던 도중 MCP 만드는 법도 공부했는데 이 과정에서 찾은 MCP Inspector라는 녀석을 통해 MCP Server를 테스트하는 방법을 공유하고자 글을 작성하게 되었습니다.</p>
</blockquote>
<p><strong>MCP가 뭔지 궁금한 분들은 아래 링크를 참조해주세요!</strong>
<a href="https://modelcontextprotocol.io/docs/getting-started/intro">MCP 공식 문서</a>
<a href="https://tech.hancom.com/mcp-llm-agent/">MCP란 무엇인가(한컴독스)</a>
<a href="https://wikidocs.net/book/17996">MCP 구현 및 테스트 방법</a></p>
<h2 id="2-mcp-inspector란">2. MCP Inspector란</h2>
<blockquote>
<p>MCP Inspector는 Model Context Protocol(MCP) 서버를 테스트하고 디버깅하기 위한 전용 클라이언트 UI 도구 </p>
</blockquote>
<p>라고 설명되어 있는데 그냥 쉽게 MCP 전용 Swagger라고 생각하면 됩니다. MCP Inspector라는 녀석을 MCP Server에 붙이고 Server가 가지고 있는 Tool, Prompt 등을 테스트 할 수 있게 해줍니다. 또한 MCP에서 자주 사용하는 <strong>SSE</strong>, <strong>Streamable HTTP</strong>도 쓸 수 있습니다.</p>
<h2 id="3-mcp-inspector-구동하기">3. MCP Inspector 구동하기</h2>
<p><strong>MCP Inspector는 Node18 이상에서 구동시킬 수 있습니다.</strong></p>
<p><code>npx @modelcontextprotocol/inspector</code>
위 명령어를 입력하면 MCP Inspector를 구동할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/yjh-1008/post/76724a1d-0603-49f2-9fc8-e6f008b51924/image.png" alt="첫 구동 화면"></p>
<p>MCP Inspector를 처음 열면 나오는 첫 페이지입니다.</p>
<p>좌측을 보면 MCP Server를 연결시킬 방법이 나와있습니다.</p>
<h3 id="transport-type">Transport Type</h3>
<p>MCP Server를 연결시킬 방식입니다.
STDIO, SSE, Streamable HTTP가 있습니다.</p>
<p><strong>1. STDIO</strong>
<img src="https://velog.velcdn.com/images/yjh-1008/post/83019e18-0bc0-47c3-93a2-b33ca2dd0cab/image.png" alt="STDIO"></p>
<blockquote>
<p>STDIO는 말 그대로 로컬 환경에서 파일을 사용하여 연결하는 방식입니다.
윈도우는 python 명령어로 실행해도 상관없지만 Mac은 python3가 명령어라서 Command에 python3라고 입력해야합니다. argument는 파일, 여러 옵션 등 연결 옵션에 대한 정보를 기입합니다.</p>
</blockquote>
<p><strong>SSE, Streamable HTTP</strong></p>
<p><img src="https://velog.velcdn.com/images/yjh-1008/post/a4ee487f-ed8c-40ad-a997-93694a4f8ec2/image.png" alt="SSE, Streamable HTTP"></p>
<blockquote>
<p>SSE, Streamable HTTP 둘 다 URL과 Connection Type이 있습니다. Connection Type에는 viaProxy, Direct 두 종류가 있습니다.</p>
</blockquote>
<p>이 외에도 Authentication, Configuration이 있는데 이는 Swagger와 유사하게 인증 토큰 및 프록시 정보에 대한 설정이므로 넘어가겠습니다.</p>
<h3 id="연결">연결</h3>
<p><img src="https://velog.velcdn.com/images/yjh-1008/post/6eb11f75-0eb8-4165-a696-44105231977d/image.png" alt="연결 화면"></p>
<p>MCP 서버를 연결하면 Tool, Prompt 등 여러가지를 체크할 수 있는 화면이 나옵니다.</p>
<h3 id="resources">Resources</h3>
<p>기능</p>
<ul>
<li>서버가 제공하는 리소스 목록 조회</li>
<li>리소스 메타데이터 확인 (MIME type 등)</li>
<li>리소스 실제 내용 읽기</li>
<li>구독(Subscription) 테스트</li>
<li>리소스 변경 시 notification 수신 확인</li>
</ul>
<p>용도 </p>
<ul>
<li>파일 시스템 MCP</li>
<li>DB 스냅샷 리소스</li>
<li>문서 기반 컨텍스트 제공 서버</li>
</ul>
<h3 id="prompts">Prompts</h3>
<p>기능</p>
<ul>
<li>Inspector에서 할 수 있는 것</li>
<li>Prompt 템플릿 목록 조회</li>
<li>입력 인자 스키마 확인</li>
<li>인자 값 입력 후 메시지 렌더링 결과 미리보기</li>
<li>role/content 구조 검증</li>
<li>변수 치환 오류 테스트 </li>
<li>필수 인자 누락 테스트 </li>
<li>메시지 배열 구조 오류 테스트 </li>
</ul>
<h3 id="tools">Tools</h3>
<p>기능</p>
<ul>
<li>Tool 목록 확인</li>
<li>JSON Schema 기반 입력 스키마 검증</li>
<li>직접 인자 넣고 호출</li>
<li>정상/에러 응답 확인</li>
<li>병렬 호출 테스트</li>
<li>가장 많이 사용하는 탭</li>
<li>MCP 서버 개발 시 80%는 여기서 디버깅</li>
</ul>
<h3 id="tasks">Tasks</h3>
<p>기능</p>
<ul>
<li>Task 생성</li>
<li>진행 상태 조회</li>
<li>완료/실패 상태 확인</li>
<li>취소 요청 테스트</li>
</ul>
<h3 id="apps">Apps</h3>
<p>MCP 기반 애플리케이션 단위 엔트리포인트</p>
<h3 id="ping">Ping</h3>
<p>서버 생존 확인(Health Check)
주로 STDIO 디버깅할 때 사용</p>
<h3 id="sampling">Sampling</h3>
<p>서버가 클라이언트(LLM)에게 샘플링 요청을 하는 기능</p>
<h3 id="elicitations">Elicitations</h3>
<p>사용자에게 추가 입력을 요청하는 상호작용 흐름
Agent 기반 UX에서 사용</p>
<h3 id="roots">Roots</h3>
<p>서버가 접근 가능한 루트 컨텍스트 정의
ex)</p>
<ul>
<li>허용된 파일 시스템 경로</li>
<li>특정 데이터 영역</li>
<li>보안/권한 관련 개념</li>
</ul>
<h3 id="auth">Auth</h3>
<p>기능</p>
<ul>
<li>인증 플로우 테스트</li>
<li>토큰 교환 확인</li>
</ul>
<h3 id="metadata">Metadata</h3>
<p>서버의 메타 정보</p>
<h2 id="4-테스트">4. 테스트</h2>
<p>탭이 너무 많아서 간단한 Tool 불러오는 테스트 화면만 설명하겠습니다.</p>
<pre><code class="language-python">//server.py
from fastmcp import FastMCP

# 1. 서버 인스턴스 생성
mcp = FastMCP(&quot;My First FastMCP Server 🚀&quot;)

# 2. @mcp.tool 데코레이터로 함수를 &#39;도구&#39;로 등록
@mcp.tool
def add(a: int, b: int) -&gt; int:
    &quot;&quot;&quot;두 숫자를 더합니다.&quot;&quot;&quot;
    return a + b

# 3. 서버 실행 (스크립트가 직접 실행될 때만)
if __name__ == &quot;__main__&quot;:
    mcp.run()
</code></pre>
<p><img src="https://velog.velcdn.com/images/yjh-1008/post/639cc8a7-12d6-4d18-a65b-fcd2dedfc4cb/image.png" alt=""></p>
<p>Tools에서 List Tools를 호출하면 server.py에 있는 Tool 리스트를 져옵니다. 이 중 테스트 하고 싶은 Tool을 선택해서 파라메터를 입력 후 Run Tool을 클릭하여 테스트를 진행할 수 있습니다.</p>
<h2 id="5-mcp-inspector-체크-리스트">5. MCP Inspector 체크 리스트</h2>
<p>Inspector로 확인할 수 있는 체크리스트입니다.</p>
<ol>
<li><p>연결 확인
[ ] 서버가 정상적으로 시작되는가?
[ ] 클라이언트와 서버 간 핸드셰이크가 성공하는가?
[ ] 서버 정보(이름, 버전)가 올바르게 표시되는가?</p>
</li>
<li><p>도구 검증
[ ] 모든 도구가 목록에 나타나는가?
[ ] 도구 설명이 명확하고 이해하기 쉬운가?
[ ] 입력 스키마가 올바르게 표시되는가?
[ ] 필수 매개변수가 정확히 표시되는가?</p>
</li>
<li><p>실행 테스트
[ ] 정상 케이스에서 도구가 올바르게 동작하는가?
[ ] 에러 케이스에서 적절한 오류 메시지가 반환되는가?
[ ] 응답 형식이 MCP 표준을 준수하는가?</p>
</li>
</ol>
<h2 id="6-정리">6. 정리</h2>
<p>오늘은 간단하게 MCP Inspector를 활용한 MCP Server 테스트 환경에 대해 알아보았습니다. 실제로 테스트 해보면서 편하다고 생각은 했는데 초기에 MCP 구현 방법에 대해 공부하고 Inspector에 대해 같이 공부하다보니 아직은 많이 부족한것 같습니다. 차차 프로젝트를 진행하면서 알게 된 내용들을 추가하면서 글을 추가해보려고 하겠습니다. 글 읽어주셔서 감사합니다 ㅎㅎ</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[scrollIntoView 사용법]]></title>
            <link>https://velog.io/@yjh-1008/scrollIntoView-%EC%82%AC%EC%9A%A9%EB%B2%95</link>
            <guid>https://velog.io/@yjh-1008/scrollIntoView-%EC%82%AC%EC%9A%A9%EB%B2%95</guid>
            <pubDate>Thu, 19 Feb 2026 15:48:26 GMT</pubDate>
            <description><![CDATA[<h2 id="1-서론">1. 서론</h2>
<blockquote>
<p>무한 스크롤, scrollBox 등 스크롤 방식을 사용할 때 특정 위치로 이동하기 위한 기능들이 있습니다. 대표적인 사용법이 맨 위로 이동하는 버튼, 다음 요소로 이동하는 버튼 등 특정 요소나 위치로 이동하는 버튼들이 있는데 이 버튼을 구현하는 쉬운 방법으로 scrollIntoView 메서드가 있습니다.</p>
</blockquote>
<h2 id="2-본론">2. 본론</h2>
<p>scrollIntoView() 메서드의 사용법은 아래와 같습니다.</p>
<pre><code class="language-js">const target = document.getElementById(&#39;target&#39;);
target?.scrollIntoView({
      inline: &#39;nearest&#39;, // 가로 &#39;start&#39; | &#39;end&#39; | &#39;nearest&#39; | &#39;center&#39;
      block: &#39;nearest&#39;, // 세로 &#39;start&#39; | &#39;end&#39; | &#39;nearest&#39; | &#39;center&#39;
      behavior: &#39;smooth&#39;, // 애니메이션 유무. smooth: O / instant: X / auto: 자동
})</code></pre>
<p>target이라는 id를 가진 요소를 찾아 해당 요소를 뷰포트에서 보이도록 스크롤을 할 수 있습니다.</p>
<p>inline은 가로, block은 세로로 스크롤을 할 수 있으며 <strong>start, center, end, nearest</strong>옵션을  가지고 있습니다.</p>
<h3 id="inline">inline</h3>
<ul>
<li>start: 요소의 시작</li>
<li>end: 요소의 끝</li>
<li>center: 요소의 중간</li>
<li>nearest: 가까운 곳 정렬</li>
</ul>
<h3 id="block">block</h3>
<ul>
<li>start: 요소의 위</li>
<li>end: 요소의 아래</li>
<li>center: 요소의 중간</li>
<li>nearest: 가까운 곳 정렬</li>
</ul>
<p>또한 behavior 옵션을 통해 애니메이션 유무를 선택할 수 있습니다.</p>
<h2 id="3-결론">3. 결론</h2>
<p>가볍게 정리하는 마음으로 scrollIntoView 메서드를 정리해보았습니다. 실무에서 무한스크롤, 캐러셀 UI를 사용할 일이 많다고 생각하는데 손쉽게 원하는 위치로 이동할 수 있는 메서드이기에 너무 유용하게 사용하고 있습니다. 오늘도 글 읽어주셔서 감사합니다.</p>
<h2 id="4-참조">4. 참조</h2>
<p><a href="https://www.inflearn.com/course/react-vanillajs-ui%EC%9A%94%EC%86%8C%EB%A7%8C%EB%93%A4%EA%B8%B0-part1/dashboard?cid=333258">React/VanillaJS UI 요소 직접 만들기 Part 1 - 정재남님</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[LeetCode: 151]]></title>
            <link>https://velog.io/@yjh-1008/LeetCode-151</link>
            <guid>https://velog.io/@yjh-1008/LeetCode-151</guid>
            <pubDate>Sun, 04 Jan 2026 15:43:39 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/yjh-1008/post/dd05f63b-e899-47db-9485-c4c4d5dc7746/image.png" alt="LeetCode"></p>
<h3 id="풀이">풀이</h3>
<ol>
<li>문자열 S의 양끝 공백을 제거한다.</li>
<li>정규식을 통해 공백이 1개 이상인 것들로 split 메서드를 수행한다.</li>
<li>배열을 뒤집는다.</li>
<li>join 메서드를 통해 연결한다.</li>
</ol>
<pre><code class="language-js">var reverseWords = function(s) {
  return s
    .trim()
    .split(/\s+/)
    .reverse()
    .join(&#39; &#39;);
};</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Textarea의 rows를 깔끔하게 수정해보자]]></title>
            <link>https://velog.io/@yjh-1008/Textarea%EC%9D%98-rows%EB%A5%BC-%EA%B9%94%EB%81%94%ED%95%98%EA%B2%8C-%EC%88%98%EC%A0%95%ED%95%B4%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@yjh-1008/Textarea%EC%9D%98-rows%EB%A5%BC-%EA%B9%94%EB%81%94%ED%95%98%EA%B2%8C-%EC%88%98%EC%A0%95%ED%95%B4%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Sun, 28 Dec 2025 13:48:59 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/yjh-1008/post/383abac4-15f6-4d51-911c-7c290e21deba/image.png" alt=""></p>
<h3 id="1서론">1.서론</h3>
<blockquote>
<p>&quot;textarea 영역이 3줄까지 늘어나고 그 이상은 스크롤이 되었으면 좋겠어요!&quot;
UI 요구사항 중 자주 요청받는 기능 중 하나인 특정 입력창 늘리기입니다.
실무에서는 주로 useRef를 사용하는 방식을 주로 활용합니다. 하지만 이 외에도 canvas를 활용하는 방법을 학습하여 기록하고자합니다!</p>
</blockquote>
<h3 id="2-useref-방식">2. useRef 방식</h3>
<pre><code class="language-tsx">import { ChangeEvent, useRef, useEffect } from &quot;react&quot;;
import cx from &quot;./cx&quot;
import { measureLines } from &quot;@/service/util&quot;

const TextBox3 = () =&gt; {
  const textareaRef = useRef&lt;HTMLTextAreaElement&gt;(null);
  const cloneRef = useRef&lt;HTMLTextAreaElement&gt;(null);
  useEffect(() =&gt; {
    const elem = textareaRef.current;
    const cloneElem = cloneRef.current;
    if(!elem || !cloneElem) return;
    const handlerInput = () =&gt; {

      const val = elem.value;
      cloneElem.value = val;
      //최소 3줄, 최대 15줄
      elem.rows = Math.min(Math.max(Math.ceil(cloneElem.scrollHeight / cloneElem.clientHeight), 3), 15);
    }
    elem.rows = 3;
    elem?.addEventListener(&#39;input&#39;, handlerInput);
    return () =&gt; {
      elem?.removeEventListener(&#39;input&#39;, handlerInput);
    }
  },[])
  return (
    &lt;&gt;
      &lt;h3&gt;
        #1&lt;sub&gt;controlled. clone elem&lt;/sub&gt;
      &lt;/h3&gt;
      &lt;div className={cx(&#39;container&#39;)}&gt;
        &lt;textarea className={cx(&#39;clone&#39;)} ref={cloneRef} readOnly /&gt;
        &lt;textarea  className={cx(&#39;textarea&#39;)} ref={textareaRef} /&gt;
      &lt;/div&gt;
    &lt;/&gt; 
  )
}

export default TextBox3;</code></pre>
<pre><code class="language-scss">.TextBoxes {
  .container {
    width: 50vw;
    position: relative;
  }

  .textBox {
    box-sizing: border-box;
    width: 100%;
    resize: none;
    overflow-y: scroll;
  }

  .clone {
    box-sizing: border-box;
    width: 100%;
    resize: none;
    overflow-y: scroll;

    position: absolute;
    top: 0;
    left: -9999;
    z-index: -1;
    visibility: hidden;
    opacity: 0;
  }
}
</code></pre>
<ol>
<li>useEffect에서 input 이벤트를 할당하여 입력 이벤트 발생 시 우선적으로 clone 요소에 값을 입력합니다.</li>
<li>scrollHeight, clientHeight를 통해 현재 라인 수를 찾을 수 있습니다.</li>
<li>해당 결과값을 통해 textarea의 row로 할당하면 자연스럽게 요소의 높이를 조절할 수 있습니다.</li>
</ol>
<h3 id="3-canvas-방식">3. Canvas 방식</h3>
<pre><code class="language-tsx">import { ChangeEvent, useRef, useEffect } from &quot;react&quot;;
import cx from &quot;./cx&quot;
import { measureLines } from &quot;@/service/util&quot;

const TextBox2 = () =&gt; {
  const textareaRef = useRef&lt;HTMLTextAreaElement&gt;(null);

  useEffect(() =&gt; {
    const elem = textareaRef.current;
    if(!elem) return;
    const handlerInput = () =&gt; {

      const val = elem.value;
      const lines = Math.min(Math.min(measureLines(elem, val),3), 15);
      elem.rows = lines;
    }
    elem.rows = 3;
    elem?.addEventListener(&#39;input&#39;, handlerInput);
    return () =&gt; {
      elem?.removeEventListener(&#39;input&#39;, handlerInput);
    }
  },[])
  return (
    &lt;&gt;
      &lt;h3&gt;
        #2&lt;sub&gt;uncontrolled. canvas&lt;/sub&gt;
      &lt;/h3&gt;
      &lt;div className={cx(&#39;container&#39;)}&gt;
        &lt;textarea ref={textareaRef}  className={cx(&#39;textarea&#39;)} /&gt;
      &lt;/div&gt;
    &lt;/&gt; 
  )
}

export default TextBox2;</code></pre>
<pre><code class="language-ts">const measureLines = (elem: HTMLTextAreaElement, val: string) =&gt; {
  // textarea 요소나 값이 없으면 줄 수는 0
  if (!elem || !val) return 0;

  const canvas = document.createElement(&quot;canvas&quot;);
  const context = canvas.getContext(&quot;2d&quot;);

  // textarea의 실제 렌더링 스타일을 가져옴
  const style = window.getComputedStyle(elem);

  // canvas context를 얻지 못한 경우 안전하게 0 반환
  if (!context) return 0;

  // textarea와 동일한 폰트 스타일을 canvas에 적용
  // → 실제 화면에서의 텍스트 너비와 최대한 동일하게 측정하기 위함
  context.font = `${style.getPropertyValue(&#39;font-size&#39;)} ${style.getPropertyValue(&#39;font-family&#39;)}`;

  // 개행(\n) 기준으로 문자열을 분리한 뒤,
  // 각 줄이 textarea 너비를 기준으로 몇 줄로 wrapping 되는지 계산
  const measuredLines = val.split(&#39;\n&#39;).reduce((totalLines, currentLine) =&gt; {
    // 현재 줄을 한 줄로 쭉 나열했을 때의 픽셀 너비 측정
    const textWidth = context.measureText(currentLine).width;

    // textarea 너비로 나누어 실제 줄 수 계산
    const wrappedLineCount = Math.max(
      Math.ceil(textWidth / elem.offsetWidth),
      1
    );

    // 전체 줄 수에 현재 줄에서 발생한 줄 수를 누적
    return totalLines + wrappedLineCount;
  }, 0);

  // 최종 계산된 총 줄 수 반환
  return measuredLines;
};
</code></pre>
<p>Canvas를 활용한 방식은 DOM에 값을 추가하지 않기 때문에 Reflow를 방지할 수 있다는 장점이 있습니다. 또한 레이아웃 기반의 계산이 아닌 canvas를 통해 계산하기 때문에 이벤트 비용적으로도 효율적입니다.</p>
<p>하지만 <strong>${style.getPropertyValue(&#39;font-size&#39;)} ${style.getPropertyValue(&#39;font-family&#39;)}</strong>; 에서 요소의 글자 크기와 폰트가 잘못 할당 될 경우, 화면의 요소와 다르게 계산되어 줄넘김이 이상하게 동작할 수 있습니다.</p>
<h3 id="참조">참조</h3>
<p><a href="https://www.inflearn.com/course/react-vanillajs-ui%EC%9A%94%EC%86%8C%EB%A7%8C%EB%93%A4%EA%B8%B0-part1/dashboard">인프런 - [React / VanillaJS] UI 요소 직접 만들기 Part 1 - 정재남님 </a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[LeetCode: 1071]]></title>
            <link>https://velog.io/@yjh-1008/LeetCode-1071</link>
            <guid>https://velog.io/@yjh-1008/LeetCode-1071</guid>
            <pubDate>Fri, 26 Dec 2025 10:15:10 GMT</pubDate>
            <description><![CDATA[<h2 id="풀이">풀이</h2>
<ul>
<li>두 문자열에서 모두 나누어 떨어지는 <strong>특정 문자열 x들 중 가장 긴 문자열</strong></li>
<li>두 문자열 중 더 작은 문자열의 길이부터 시작<ul>
<li>더 긴 문자열로 시작하면 짧은 문자열을 비교할 이유가 없기 때문</li>
</ul>
</li>
<li>특정 문자열 x로 replaceAll을 진행했을 때, 두 문자열 모두 빈 문자열이 된다면 true!</li>
</ul>
<blockquote>
<p>GCD 알고리즘이란?
GCD(Greatest Common Divisor, 최대공약수) 알고리즘은
두 개 이상의 정수에서 공통으로 나누어떨어지는 가장 큰 수를 구하는 알고리즘입니다.</p>
</blockquote>
<ul>
<li><p>해답</p>
<pre><code class="language-js">/**
* @param {string} str1
* @param {string} str2
* @return {string}
*/
var gcdOfStrings = function(str1, str2) {
  let left = 0, right = 0;

  let sStr = &quot;&quot;, lStr = &quot;&quot;;

  let answer = &quot;&quot;;

  if(str1.length &gt; str2.length) {
      sStr = str2;
      lStr = str1;
  } else {
      sStr = str1;
      lStr = str2;
  }

  for(let i=1; i&lt;=sStr.length ;i++) {
      const tmp = sStr.slice(0, i);
      if(
          sStr.replaceAll(tmp,&#39;&#39;).length  === 0 
          &amp;&amp; lStr.replaceAll(tmp,&#39;&#39;).length  === 0
      ) answer = tmp
  } 

  return answer;
};</code></pre>
</li>
<li><p>Solution을 보고 수정한 해답</p>
<blockquote>
<p>처음부터 더 작은 문자열부터 시작한다면 계산 횟수를 줄일 수 있다는 생각을 하지 못했다..
풀긴했지만 문자열이 더 길어진다면 시간초과가 발생할 수도 있을것같다.</p>
</blockquote>
</li>
</ul>
<pre><code class="language-js">/**
 * @param {string} str1
 * @param {string} str2
 * @return {string}
 */
var gcdOfStrings = function(str1, str2) {

    let len1 = str1.length, len2 = str2.length;

    const isValid = (k) =&gt; {
        if(len1 % k === 0
            &amp;&amp; len2 % k === 0
        ) {
            const tmp = str1.slice(0, k)
            return str1.replaceAll(tmp, &quot;&quot;) === &quot;&quot; &amp;&amp;
            str2.replaceAll(tmp,&quot;&quot;) === &quot;&quot;
        } else {
            return false;
        }
    }

    for(let i = Math.min(len1, len2); i &gt; 0; i--) {
        if(isValid(i)) {
            return str1.slice(0, i)
        }
    }

    return &quot;&quot;;
};</code></pre>
<h2 id="gcd최대공약수-구하는-법">GCD(최대공약수) 구하는 법</h2>
<pre><code class="language-js">//a가 더 큰 수!
function gcd(a, b) {
  if (b === 0) return a;
  return gcd(b, a % b);
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[LeetCode: Merge Strings Alternately]]></title>
            <link>https://velog.io/@yjh-1008/LeetCode-Merge-Strings-Alternately</link>
            <guid>https://velog.io/@yjh-1008/LeetCode-Merge-Strings-Alternately</guid>
            <pubDate>Wed, 24 Dec 2025 03:48:34 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/yjh-1008/post/16ae71e3-7752-4381-8f81-d626d5e9e44f/image.png" alt="LeetCode"></p>
<h2 id="풀이">풀이</h2>
<ol>
<li>Math.max를 통해 2개의 문자열 중 긴 길이를 찾는다.</li>
<li>for문을 탐색하며 해당 문자의 i번째 인덱스가 <code>undefined</code>가 아니라면 answer 배열에 push</li>
<li>answer.join()한 결과값을 리턴</li>
</ol>
<pre><code class="language-js">/**
 * @param {string} word1
 * @param {string} word2
 * @return {string}
 */
var mergeAlternately = function(word1, word2) {
    const maxLen = Math.max(word1.length, word2.length);

    let answer = [];

    for(let i=0;i&lt;maxLen;i++) {
        if(word1[i]!== undefined) answer.push(word1[i]);

        if(word2[i]!== undefined) answer.push(word2[i]);
    }

    return answer.join(&#39;&#39;)
};</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[React 디자인 패턴을 Vue로 옮겨보자 -3 (HOC Pattern)]]></title>
            <link>https://velog.io/@yjh-1008/React-%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4%EC%9D%84-Vue%EB%A1%9C-%EC%98%AE%EA%B2%A8%EB%B3%B4%EC%9E%90-3-HOC-Pattern</link>
            <guid>https://velog.io/@yjh-1008/React-%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4%EC%9D%84-Vue%EB%A1%9C-%EC%98%AE%EA%B2%A8%EB%B3%B4%EC%9E%90-3-HOC-Pattern</guid>
            <pubDate>Wed, 17 Dec 2025 12:44:10 GMT</pubDate>
            <description><![CDATA[<h2 id="1-hoc">1. HOC?</h2>
<blockquote>
<p><strong>HOC(Higher-Order Components) 패턴은 컴포넌트를 인자로 받아 새로운 컴포넌트를 반환하는 함수</strong>입니다. 즉, 컴포넌트 로직을 재사용하기 위한 패턴이며, React의 공식 패턴 중 하나입니다.</p>
</blockquote>
<pre><code class="language-tsx">const EnhancedComponent = hiorderComponent(WrappedComponent);</code></pre>
<ul>
<li>입력: 컴포넌트</li>
<li>출력: 새로운 기능이 추가된 새로운 컴포넌트</li>
<li>특징: 기존 컴포넌트를 수정하지 않고 감싸서 확장.</li>
</ul>
<p><strong>고차 함수 방식을 컴포넌트로 변환한 디자인 패턴이라고 생각할 수 있습니다.</strong></p>
<p>HOC는 주로 아래와 같은 목적으로 많이 사용합니다.</p>
<ul>
<li>특정 페이지 접근 제어</li>
<li>권한별 UI 분기</li>
<li>라우트 보호</li>
</ul>
<h2 id="2-react에서의-hoc">2. React에서의 HOC</h2>
<pre><code class="language-tsx">function WithLoading&lt;P&gt;(
    WrappedComponent: React.ComponentType&lt;P&gt;
) {

      return function WithLoading(props: P &amp; {isLoading: boolean}) {

          if(props.isLoading) {
            return &lt;div&gt;Loading...&lt;/div&gt;;
        }

        return &lt;WrappedComponent {...props}/&gt;;
      }
}</code></pre>
<p>위 코드는 클로저를 활용하여 WrappedComponent를 기억하고 isLoading을 통해 컴포넌트 렌더링을 분기처리하고 있습니다. 또한 위 코드는 <strong>함수에서 함수를 리턴하는 고차함수입니다.</strong></p>
<h3 id="사용">사용</h3>
<pre><code class="language-tsx">const ApiWithLoading = WithLoading(ApiList);
&lt;ApiWithLoading isLoading={true} /&gt;</code></pre>
<blockquote>
<p>하지만 현재 HOC는 거의 사용하지 않고 있습니다. 그 이유는 Hook의 등장이 큰 이유입니다. React 16.8 버전 이후 Hook이라는 재사용 로직을 통해 더욱 간편하게 모듈화를 할 수 있기 되었기 때문입니다. 또한 기존 Class형 컴포넌트에서 함수형 컴포넌트로 전환된 것도 큰 요인입니다.</p>
</blockquote>
<h2 id="4-vue에서의-hoc">4. Vue에서의 HOC</h2>
<pre><code class="language-ts">import {defineComponent, h} from &#39;vue&#39;;

function withLoading(WrappedComponent){

    return defineComponent({
        props: {
            isLoading: Boolean
        },
        setup(props, {attrs}) {
            return () =&gt; 
                props.isLoading
                ? h(&#39;div&#39;, &#39;loading...&#39;)
                : h(WrappedComponent, attrs)
        }
    })
}</code></pre>
<pre><code class="language-vue">const ApiWithLoading = withLoading(ApiList);</code></pre>
<p>이런 방식으로 <code>&lt;script setup&gt;</code> 방식이 아닌 defineComponent와 h메서드를 사용하면 HOC를 구현할 수 있습니다. 
하지만 이러한 방식은 실무에서 거의 사용하지 않기 때문에 HOC 패턴은 Vue에서 거의 사용하지 않습니다.
또한 React는 컴포넌트 중심이지만 Vue는 Composition API와 같이 로직 중심적으로 이루어져 있기에 분리가 어렵다는 점이 있습니다.</p>
<p>그럼 Vue에서는 이러한 문제를 해결할 수 없을까요?
Vue에서는 HOC 패턴 대신 Composable과 Slot, Directive 방식을 사용하라고 권장하고 있습니다.</p>
<h3 id="1-composable-방식">1. Composable 방식</h3>
<pre><code class="language-ts">// useAuth.ts
export function useAuth() {
  const isAuthenticated = ref(false);

  const login = () =&gt; {
    isAuthenticated.value = true;
  };

  return { isAuthenticated, login };
}</code></pre>
<pre><code class="language-vue">&lt;script setup&gt;
const { isAuthenticated } = useAuth();
&lt;/script&gt;

&lt;template&gt;
  &lt;Login v-if=&quot;!isAuthenticated&quot; /&gt;
  &lt;Dashboard v-else /&gt;
&lt;/template&gt;
</code></pre>
<h3 id="2-slot-방식">2. Slot 방식</h3>
<pre><code class="language-vue">&lt;AuthGuard v-slot=&quot;{isAuthenticated}&quot;&gt;
    &lt;Dashboard v-if=&quot;isAuthenticated&quot;/&gt;
&lt;/AuthGuard&gt;
</code></pre>
<pre><code class="language-vue">//AuthGuard.vue
&lt;script setup&gt;
const isAuthenticated = true
&lt;/script&gt;

&lt;template&gt;
    &lt;slot :isAuthenticated=&quot;isAuthenticated&quot; /&gt;
&lt;/template&gt;</code></pre>
<h3 id="directive-방식">Directive 방식</h3>
<pre><code class="language-ts">export default {
    mounted(el, binding) {
        if(!binding.value){
            el.remove();
        }
    }

}</code></pre>
<pre><code>import vAuth from &#39;@/directives/vAuth&#39;;
app.directive(&#39;auth&#39;, vAuth); </code></pre><pre><code class="language-vue">&lt;button v-auth=&quot;isAdmin&quot;&gt;버튼&lt;/button&gt;</code></pre>
<h2 id="정리">정리</h2>
<p>현재 HOC는 거의 사용 안하는 추세이지만 알아두는것이 좋다고 생각합니다. 프로젝트를 진행하다보면 새로운 프로젝트를 만드는 것 뿐만이 아닌 레거시 코드를 마이그레이션하는 작업도 따라오기에 HOC 패턴을 적용했던 코드를 Hook으로 변환하거나 Composable로 변환할 수 있기 때문입니다. 오늘도 글 읽어주셔서 감사합니다:)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[AWS Cloud Practitioner 합격 후기]]></title>
            <link>https://velog.io/@yjh-1008/AWS-Cloud-Practitioner-%ED%95%A9%EA%B2%A9-%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@yjh-1008/AWS-Cloud-Practitioner-%ED%95%A9%EA%B2%A9-%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Sun, 30 Nov 2025 14:47:42 GMT</pubDate>
            <description><![CDATA[<h2 id="1-개요">1. 개요</h2>
<blockquote>
<p>저는 프론트엔드 개발자이고 AWS는 간간이 사이드 프로젝트에서 잠깐잠깐 사용하거나 실무에서도 조금씩 사용했었습니다. 그러다가 Nginx를 많이 다루고 인프라에 관심이 생기면서 AWS도 흥미를 가지게 되었고 자격증에 도전하게 되었습니다. 제가 공부했던 내용을 공유하며 다른 분들도 합격하시기를 기원하는 마음에 합격 후기를 작성하려고 합니다!</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/yjh-1008/post/d4ebc4ca-adbd-4d08-a64c-e1483d06c694/image.png" alt="AWS 합격"></p>
<h2 id="2-시험-정보">2. 시험 정보</h2>
<p>시험의 정식 명칭은 AWS Certified Cloud Practitioner로, 시험 코드는 CLF-02입니다. 시험 시간은 총 90분으로, 65문제의 단수, 복수 문제로 구성되어 있습니다. 이때 유의해야 할 점은, 65개의 문제 중에서 실제로 채점되는 문제는 50문제로, 나머지 15문제는 데이터로서 활용된다고 합니다. 시험 점수는 100 ~ 1000점으로 700점 이상을 받아야 합격할 수 있습니다. 시험 비용은 100 USD이고 저는 13만 원대에 결제를 했습니다.</p>
<ul>
<li>시험 시간: 90분</li>
<li>시험 형식: 65개 문항 / 선다형(단수, 복수 응답)<pre><code>  - 문제 구성
  1. 클라우드 개념 (24%)
  2. 보안 및 규정 준수(30%)
  3. 클라우드 기술 및 서비스(34%)
  4. 결재 요금 및 지원(12%)</code></pre></li>
<li>시험 비용: 100 USD</li>
<li>합격 점수: 700점 이상</li>
<li><a href="https://aws.amazon.com/ko/certification/certified-cloud-practitioner/">시험 안내</a></li>
</ul>
<h2 id="3-시험-준비-방법">3. 시험 준비 방법</h2>
<h3 id="1-aws-공식-강의skill-builder---aws-cloud-practitioner-essentials-korean">1. <a href="https://skillbuilder.aws/search?searchText=aws-cloud-practitioner-essentials-korean-na-hangug-eo-gang-ui&amp;showRedirectNotFoundBanner=true">AWS 공식 강의(Skill builder) - AWS Cloud Practitioner Essentials (Korean)</a></h3>
<p>AWS Skill Builder에서 제공하는 무료 강의입니다. 시간은 12시간 정도로 구성되어 있고 한국어 자막이 존재하기 때문에 수강하는데 어려움은 크게 없었습니다. 각 섹션 끝마다 퀴즈가 존재하고 퀴즈를 풀며 이런 형식으로 문제가 존재하는구나로 이해하면 편할것 같습니다. <strong>강의를 꼭 집중해서 이해할 필요는 없다고 생각합니다. (어차피 시간이 너무 길어서 듣다 보면 다 까먹어요...) 그냥 겉핥기 식으로 이런 게 존재하는구나..? 정도로만 이해하면 충분하다고 생각합니다..!</strong></p>
<h3 id="2-블로그-요약-학습">2. 블로그 요약 학습</h3>
<ul>
<li><a href="https://rainbowjyp.tistory.com/3">AWS Cloud Practitioner 자격증 요약 정리 (시험 출제 포함)</a></li>
<li><a href="https://tbvjrornfl.tistory.com/188">핵심 요약 정리: AWS Cloud Practitioner 자격증 시험</a></li>
</ul>
<p>강의를 통해 어떤 내용들이 있는지 대강 이해했다면 블로그를 통해 요약본으로 빠르게 키워드만 학습하고 이런 내용이 있다 정도로 학습하면 좋을 것 같습니다.</p>
<h3 id="3-dump-문제-풀이">3. dump 문제 풀이</h3>
<ul>
<li><a href="https://hyunhp.tistory.com/799">[AWS] Certified Cloud Practitioner CLF-C02 Dump 문제 정리 (1)</a></li>
<li><a href="https://www.koreadumps.com/CLF-C01-KR-practice-test.html">최신Amazon AWS Certified Solutions Architect - Cloud Practitioner (AWS-Certified-Cloud-Practitioner Korean Version) - AWS-Certified-Cloud-Practitioner Korean무료샘플문제</a></li>
</ul>
<p>시험 문제가 거의 덤프에서 많이 출제되기 때문에 덤프 문제를 많이 풀고 문제 유형을 암기하는 것이 가장 좋은 방법이라고 생각합니다. 다만 문제를 풀다 오답이 생기는 경우에는 오답노트를 작성하거나 요약 블로그로 돌아가서 이런 내용이 있구나 정도로 반복학습을 하면 좋을 것 같습니다.</p>
<h2 id="4-꿀팁">4. 꿀팁..?</h2>
<ul>
<li><strong>CAF, well Architected</strong>는 꼭 암기하자</li>
<li>EC2 유형은 반드시 외우자</li>
<li>거의 문제가 요약 내용의 단어와 연관 지어서 나오기 때문에 <strong>AWS Redshift &lt;-&gt; 데이터 웨어하우스!, spot instance &lt;-&gt; 중단 가능, 90% 할인</strong> 정도로 짝을 지어 암기하면 편하다</li>
<li>현장에서 보는 게 훨씬 편하다..!(온라인으로는 안 봤지만 검사 사항이나 주변 환경 체크가 많이 빡세다고 들었어요)</li>
</ul>
<h2 id="5-후기">5. 후기</h2>
<p>사실 시험 난이도 자체가 매우 쉽기 때문에 조금만 공부해도 합격할 수 있다고 생각합니다. 저도 실제로 2주 정도 공부했지만 1주일만 해도 충분하다고 생각합니다. 조금만 집중해서 다른 분들도 다 합격하시길 기원합니다!! </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Vue - Official 플러그인이 갑자기 작동하지 않을때..]]></title>
            <link>https://velog.io/@yjh-1008/Vue-Official-%ED%94%8C%EB%9F%AC%EA%B7%B8%EC%9D%B8%EC%9D%B4-%EA%B0%91%EC%9E%90%EA%B8%B0-%EC%9E%91%EB%8F%99%ED%95%98%EC%A7%80-%EC%95%8A%EC%9D%84%EB%95%8C</link>
            <guid>https://velog.io/@yjh-1008/Vue-Official-%ED%94%8C%EB%9F%AC%EA%B7%B8%EC%9D%B8%EC%9D%B4-%EA%B0%91%EC%9E%90%EA%B8%B0-%EC%9E%91%EB%8F%99%ED%95%98%EC%A7%80-%EC%95%8A%EC%9D%84%EB%95%8C</guid>
            <pubDate>Wed, 19 Nov 2025 16:00:46 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>과거에 Yarn Berry + Vue3(Javascript)로 진행했던 프로젝트의 eslint 및 컴포넌트 프리팅이 정상적으로 동작하지 않아 해결하던 중 알게되었던 사실을 기술하고자 글을 적게 되었습니다.</p>
</blockquote>
<h2 id="프로젝트-환경">프로젝트 환경:</h2>
<ul>
<li>Vue 3 (Composition API)</li>
<li>JavaScript (TypeScript 아님)</li>
<li>Yarn Berry (PnP)</li>
<li>VSCode</li>
</ul>
<h3 id="1-jsconfigjson-설정">1. jsconfig.json 설정</h3>
<p>자바스크립트 프로젝트이기 때문에 jsconfig.json을 추가했습니다.</p>
<pre><code class="language-JSON">{
  &quot;compilerOptions&quot;: {
    &quot;target&quot;: &quot;es2020&quot;,
    &quot;module&quot;: &quot;esnext&quot;,
    &quot;moduleResolution&quot;: &quot;node&quot;,
    &quot;lib&quot;: [&quot;es2020&quot;, &quot;dom&quot;]
  },
  &quot;include&quot;: [&quot;src/**/*&quot;],
  &quot;exclude&quot;: [&quot;node_modules&quot;, &quot;dist&quot;]
}</code></pre>
<p>이 방식을 통해 jsconfig.json을 작성했지만 문제가 해결되지 않았습니다...</p>
<h3 id="2-yarn-berry와-eslint">2. Yarn Berry와 eslint</h3>
<p>문제의 핵심은 <strong>Yarn Berry(PnP)</strong>에 있었습니다. Yarn Berry는 node_modules를 생성하지 않고 .yarn/cache에 zip 파일로 의존성을 관리합니다. VSCode는 기본적으로 node_modules에서 타입 정의를 찾기 때문에, Yarn Berry 환경에서는 추가 설정이 필요했습니다.</p>
<p><code>yarn dlx @yarnpkg/sdks vscode</code></p>
<p>이 명령어를 실행하면 .yarn/sdks 폴더가 생성되고, VSCode가 Yarn Berry의 의존성을 인식할 수 있게 됩니다.</p>
<p>하지만 다른 문제가 발생했습니다
<code>doesn&#39;t point to a valid tsserver install falling back to bundled typescript version</code></p>
<h3 id="3-typescript-설치의-딜레마">3. TypeScript 설치의 딜레마</h3>
<p>저는 Javascript 프로젝트를 유지보수했기에 당연히 Typescript가 필요 없을줄 알았지만 문제를 찾아보며 typescript의 미설치로 인한 오류라는 것을 알게 되었습니다.</p>
<blockquote>
<p>VSCode의 자동완성 메커니즘
VSCode와 Vue - Official 확장은 <strong>TypeScript 언어 서버(tsserver)</strong>를 사용해서 자동완성, IntelliSense, 타입 추론을 제공합니다. 이는 자바스크립트 프로젝트에서도 마찬가지입니다.
일반적인 npm 환경에서는 전역으로 설치된 TypeScript를 사용하거나, VSCode에 번들로 포함된 TypeScript를 사용할 수 있습니다. 하지만 Yarn Berry는 전역 패키지를 사용하지 않기 때문에 typescript를 설치해야 했었습니다.
<code>yarn add -D typescript</code>로 TypeScript를 devDependencies로 설치하면 Vue - Official이 정상적으로 동작하게됩니다.</p>
</blockquote>
<h3 id="4-sdk-재생성-및-설정">4. SDK 재생성 및 설정</h3>
<p>TypeScript 설치 후 <code>yarn dlx @yarnpkg/sdks vscode</code> 명령어를 통해 SDK를 재성성 하였습니다. 이후 Typescript를 선택하고 <code>/.vscode/settings.json</code>에 설정을 추가했습니다.</p>
<ul>
<li><p>Typescript 설정</p>
<blockquote>
<p>.vue 파일 열기
Ctrl+Shift+P (Mac: Cmd+Shift+P)
&quot;TypeScript: Select TypeScript Version&quot; 입력
&quot;Use Workspace Version&quot; 선택</p>
</blockquote>
</li>
<li><p>settings.json 설정 </p>
<pre><code class="language-JSON">{
&quot;typescript.tsdk&quot;: &quot;.yarn/sdks/typescript/lib&quot;,
&quot;typescript.enablePromptUseWorkspaceTsdk&quot;: true
}</code></pre>
</li>
</ul>
<h2 id="정리">정리</h2>
<ol>
<li><p>Yarn Berry는 다르다
Yarn Berry의 PnP(Plug&#39;n&#39;Play) 모드는 node_modules를 생성하지 않는다. 이는 빠른 설치와 효율적인 디스크 사용을 가능하게 하지만, 많은 도구들이 node_modules를 전제로 작동하기 때문에 추가 설정이 필요하다.</p>
</li>
<li><p>Vue-Official</p>
</li>
</ol>
<ul>
<li>Vue-Official을 사용하기 위해서는 Typescript가 필수적으로 사용되어야한다. 전역 node_modules를 활용할 수 없는 상황이라면 devDependencies에 설치해줘야한다.</li>
</ul>
<h2 id="마치며">마치며</h2>
<p>VSCode에서 간단한 자동완성 하나가 안 되는 문제를 해결하는 데 반나절이나 걸렸습니다.(야근 확정..) Yarn Berry, Vue 3, JavaScript, VSCode가 모두 얽혀있는 환경에서는 하나하나의 설정이 모두 맞아떨어져야 제대로 작동합니다. 이를 위해 각 기능들에 대한 이해와 옵션 설정 하나하나를 신중하게 해야할것 같습니다.</p>
<p>이 글이 같은 문제로 고생하는 다른 개발자들이 빠르게 해결하길 바랍니다..!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[React 디자인 패턴을 Vue로 옮겨보자 -2 (Container-Presentation Pattern)]]></title>
            <link>https://velog.io/@yjh-1008/React-%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4%EC%9D%84-Vue%EB%A1%9C-%EC%98%AE%EA%B2%A8%EB%B3%B4%EC%9E%90-1-Container-Presentation-Pattern</link>
            <guid>https://velog.io/@yjh-1008/React-%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4%EC%9D%84-Vue%EB%A1%9C-%EC%98%AE%EA%B2%A8%EB%B3%B4%EC%9E%90-1-Container-Presentation-Pattern</guid>
            <pubDate>Tue, 04 Nov 2025 15:37:22 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>Container Presentation Pattern이란, <strong>로직을 위한 컴포넌트(Container)와 UI를 위한 컴포넌트(View)로 분리시키는 디자인 패턴</strong>입니다.</p>
</blockquote>
<h2 id="1-구현-컴포넌트">1. 구현 컴포넌트</h2>
<ul>
<li><p><strong>Presentational Component</strong>: Props를 통해 데이터를 받습니다. 주요 기능은 전달 받은 데이터를 통해 렌더링 할 화면을 표현하는 것입니다.</p>
</li>
<li><p><strong>Container Component</strong>: Presentational 컴포넌트에 데이터를 전달하는 컴포넌트입니다. Container 컴포넌트 자체는 화면에 렌더링하지 않습니다.</p>
</li>
</ul>
<h2 id="2장점">2.장점</h2>
<ul>
<li>자연스럽게 로직/UI 컴포넌트가 분리되어 관심사가 분리됩니다.</li>
<li>Presentational Prop로 데이터를 주입받기 때문에 다양한 목적으로 재사용 할 수 있습니다.</li>
<li>Props로만 데이터를 전달받기 때문에 Presentational 컴포넌트는 수정 및 테스트가 쉽습니다.</li>
</ul>
<h2 id="3-단점">3. 단점</h2>
<ul>
<li>많은 양의 데이터가 필요해지면 Container 컴포넌트의 로직이 복잡해질 수 있습니다.</li>
<li>Container 컴포넌트가 커짐에 따라 자연스럽게 Presentational Component의 Props 또한 비대해질 수 있습니다.</li>
<li>단순한 컴포넌트도 위 규칙을 따르면 오버엔지니이링이 발생할 수 있습니다.</li>
</ul>
<h2 id="4react로-구현">4.React로 구현</h2>
<p><strong>Container Component</strong></p>
<pre><code class="language-tsx">import { useEffect, useState } from &quot;react&quot;;
import UserView from &quot;../view/UserView&quot;;

export interface User {
  name: string;
  email: string;
}

async function fetchUser(): Promise&lt;User&gt; {
  await new Promise((resolve) =&gt; setTimeout(resolve, 500));
  return {name:&quot;테스터&quot;, email:&quot;test@test.com&quot;}
}

//User API를 호출하여 Presentational Component에 전달.
function UserContainer() {
  const [user, setUser] = useState&lt;User | null &gt;(null);

  const [isLoading, setIsLoading] = useState&lt;boolean&gt;(false);

  useEffect(() =&gt; {
    setIsLoading(true)
    const init = async () =&gt; {
      try {
        const user = await fetchUser();
        setUser(user);
      } catch(e) {
        console.error(e);
      } finally {
        setIsLoading(false)
      }
    }

    init();
  },[])


  return (
    //UI 구현 없이 UserView에 데이터만 전달
   &lt;UserView
      user={user}
      isLoading={isLoading}
      onRefresh={fetchUser}
    /&gt;
  )
}

export default UserContainer;</code></pre>
<p><strong>Presentation Component</strong></p>
<pre><code class="language-tsx">import type { User } from &quot;../container/UserContainer&quot;;

interface UserViewProps {
  user: User | null;
  isLoading: boolean;
  onRefresh: () =&gt; Promise&lt;User&gt;
}
// 데이터 조작 없이 전달된 Props를 통해 화면만 갱신하는 컴포넌트
function UserView({user, isLoading}: UserViewProps) {

  if(isLoading) return &lt;div&gt;Loading...&lt;/div&gt;

  if(user === null) return &lt;div&gt;유저 정보가 없어요.&lt;/div&gt;

  return (
    &lt;div&gt;
      &lt;h2&gt;유저 정보&lt;/h2&gt;
      &lt;div&gt;이름: {user.name}&lt;/div&gt;
      &lt;div&gt;이름: {user.email}&lt;/div&gt;
    &lt;/div&gt;
  )
}

export default UserView;</code></pre>
<blockquote>
<p>만약 API 로직 혹은 데이터 조작 로직을 모듈화하고 싶다면 Hook으로 분리할 수 있습니다.</p>
</blockquote>
<pre><code class="language-tsx">import { useState, useEffect, useCallback } from &quot;react&quot;;

export interface User {
  name: string;
  email: string;
}

export function useUser() {
  const [user, setUser] = useState&lt;User | null&gt;(null);
  const [isLoading, setIsLoading] = useState(false);

  async function fetchUser(): Promise&lt;User&gt; {
      try {
          setIsLoading(true)
        await new Promise((resolve) =&gt; setTimeout(resolve, 500));
          setUser({name:&quot;테스터&quot;, email:&quot;test@test.com&quot;})
    } catch(e) {
      console.log(e)
    } finally {
        setIsLoading(false)
    }
  }

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

  return {
    user,
    isLoading,
    refresh: fetchUser,
  };
}
</code></pre>
<pre><code class="language-tsx">import React from &quot;react&quot;;
import { useUser } from &quot;../hooks/useUser&quot;;
import { UserView } from &quot;./UserView&quot;;

export function UserContainer() {
  //User의 정보를 불러오는 로직을 Hook으로 분리
  const { user, isLoading, refresh } = useUser();

  return (
    &lt;UserView
      user={user}
      isLoading={isLoading}
      onRefresh={refresh}
    /&gt;
  );
}</code></pre>
<h1 id="5-vue로-구현">5. Vue로 구현</h1>
<p><strong>Container Component</strong></p>
<pre><code class="language-vue">//User API를 호출하여 Presentational Component에 전달.
&lt;script setup lang=&quot;ts&quot;&gt;
import { onMounted, ref, shallowRef } from &#39;vue&#39;;
import UserView from &#39;../view/UserView.vue&#39;;


export interface User {
  name: string;
  email: string;
}

async function fetchUser(): Promise&lt;User&gt; {
  await new Promise((resolve) =&gt; setTimeout(resolve, 500));
  return {name:&quot;테스터&quot;, email:&quot;test@test.com&quot;}
}

const user = shallowRef&lt;User&gt;();
const isLoading = ref&lt;boolean&gt;(false);


onMounted(async () =&gt; {
  try {
    isLoading.value = true;
    const res = await fetchUser();
    user.value = res;
  } catch(e) {
    console.error(e)
  }finally {
    isLoading.value = false;
  }
})


&lt;/script&gt;

&lt;template&gt;
  &lt;UserView
    :user=&quot;user&quot;
    :isLoading=&quot;isLoading&quot;
    :onRefresh=&quot;fetchUser&quot;
  /&gt;
&lt;/template&gt;</code></pre>
<p><strong>Presentation Component</strong></p>
<pre><code class="language-vue">// 데이터 조작 없이 전달된 Props를 통해 화면만 갱신하는 컴포넌트
&lt;script setup lang=&quot;ts&quot;&gt;
import type { User } from &#39;../container/UserContainer.vue&#39;;


interface UserViewProps {
  user?: User;
  isLoading: boolean;
  onRefresh: () =&gt; Promise&lt;User&gt;
}

const props = withDefaults(defineProps&lt;UserViewProps&gt;(), {
  user: () =&gt; ({
    name: &#39;&#39;,
    email: &#39;&#39;
  })
})


&lt;/script&gt;

&lt;template&gt;
  &lt;div&gt;
    &lt;h2&gt;유저 정보&lt;/h2&gt;
    &lt;div&gt;이름: {{props.user.name}}&lt;/div&gt;
    &lt;div&gt;이름: {{props.user.email}}&lt;/div&gt;
  &lt;/div&gt;
&lt;/template&gt;</code></pre>
<blockquote>
<p>Vue 또한 Hook과 같이 로직을 모듈화 하고싶다면 composable method로 분리할 수 있습니다.</p>
</blockquote>
<p><strong>useUser</strong></p>
<pre><code class="language-ts">import { shallowRef, ref, onMounted, type ShallowRef, type Ref } from &quot;vue&quot;;

export interface User {
  name: string;
  email: string;
}

interface useUserProps {
  user: ShallowRef&lt;User | undefined&gt;;
  isLoading: Ref&lt;boolean&gt;;
  fetchUser: () =&gt; Promise&lt;User&gt;
}

async function fetchUser(): Promise&lt;User&gt; {
  await new Promise((resolve) =&gt; setTimeout(resolve, 500));
  return {name:&quot;테스터&quot;, email:&quot;test@test.com&quot;}
}


function useUser():useUserProps {
  const user = shallowRef&lt;User&gt;();
  const isLoading = ref&lt;boolean&gt;(false);


  onMounted(async () =&gt; {
    try {
      isLoading.value = true;
      const res = await fetchUser();
      user.value = res;
    } catch(e) {
      console.error(e)
    }finally {
      isLoading.value = false;
    }
  })

  return {
    user,
    isLoading,
    fetchUser
  }
}

export default useUser;</code></pre>
<pre><code class="language-vue">&lt;script setup lang=&quot;ts&quot;&gt;
import useUser, {type User} from &#39;../../composables/useUser&#39;;
import UserView from &#39;../view/UserView.vue&#39;;


const {user, isLoading, fetchUser} = useUser();
&lt;/script&gt;

&lt;template&gt;
  &lt;UserView
    :user=&quot;user&quot;
    :isLoading=&quot;isLoading&quot;
    :onRefresh=&quot;fetchUser&quot;
  /&gt;
&lt;/template&gt;</code></pre>
<h2 id="정리">정리</h2>
<p>오늘은 Container-Presentation 패턴을 React와 Vue로 구현했습니다.
사실 Container-Presentation 패턴은 &#39;React의 디자인 패턴이다&#39;라고 보기에는 어디에서든지 사용할 수 있기 때문에 공통 디자인 패턴이라고 생각합니다.
Container-Presentation 패턴은 프론트엔드 개발자가 리팩토링, 유지보수를 하다보면 알고있지 않아도 자연스럽게 적용하게 되는 디자인 패턴입니다. 
UI적 관심사와 기능적 관심사가 분리되어야 가독성도 높아지고 코드의 응집도도 낮아지기 때문입니다.
하지만 많은 양의 데이터가 필요하거나 데이터 조작이 필요한 경우에는 Container 컴포넌트가 복잡해지기 때문에 적절하게 분리하는 방식이 필요합니다.
또한 단순한 기능을 가진 컴포넌트도 디자인 패턴을 적용해야하나라는 고민이 생기게 되는데 이는 팀원들과의 논의를 통하여 정하거나 사전에 명확하게 경계를 분리하는것이 좋다고 생각합니다.</p>
<p>저는 실무에서 Atomic Design Pattern과 Container-Presentation를 혼용해서 사용하는 방식을 사용하고 있는데 UI/기능적 컴포넌트로 깔끔하게 유지보수 할 수 있어서 유용하게 사용하고 있는 디자인 패턴입니다:)</p>
<p>오늘도 긴 글 읽어주셔서 감사합니다 :)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[React 디자인 패턴을 Vue로 옮겨보자 -1 (Compund Component Pattern)]]></title>
            <link>https://velog.io/@yjh-1008/React-%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4%EC%9D%84-Vue%EB%A1%9C-%EC%98%AE%EA%B2%A8%EB%B3%B4%EC%9E%90-1-Compund-Component-Pattern</link>
            <guid>https://velog.io/@yjh-1008/React-%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4%EC%9D%84-Vue%EB%A1%9C-%EC%98%AE%EA%B2%A8%EB%B3%B4%EC%9E%90-1-Compund-Component-Pattern</guid>
            <pubDate>Mon, 27 Oct 2025 15:54:36 GMT</pubDate>
            <description><![CDATA[<p>오래 미뤄왔던 블로그를 다시 끄적끄적 하려고합니다..! Vue를 개발하면서 React의 디자인 패턴을 Vue에서도 사용하면 좋겠다는 생각이 들어 React에서 자주 사용하는 패턴을 Vue로 옮겨보려고합니다. 오늘은 그 중에서도 가장 많이 사용되는 compound component pattern을 옮겨보겠습니다!!</p>
<h2 id="1-compound-component-pattern이란">1. Compound Component Pattern이란?</h2>
<blockquote>
<p>Compound Component Pattern은 React, Vue와 같은 라이브러리에서 <strong>하나의 컴포넌트를 여러 개의 하위 컴포넌트로 나누되, 상호 의존성을 자연스럽게 유지</strong>하기 위한 디자인 패턴입니다.</p>
</blockquote>
<pre><code class="language-tsx">&lt;RadioGroup name=&quot;fruit&quot;&gt;
  &lt;Radio value=&quot;apple&quot;  onChange={...} checked={true} /&gt;
  &lt;Radio value=&quot;banana&quot;  onChange={...} checked={false} /&gt;
&lt;/RadioGroup&gt;
</code></pre>
<p>위는 Compound Component 패턴을 적용하지 않았을 때의 모습입니다. 위 방식으로 구현한다면 Radio 컴포넌트에 onChange, checked를 일일이 넘겨줘야 해서 props drilling 문제가 발생할 수 있습니다. 이러한 반복적인 props를 전달하는 상황이 생길 때 Compound Component 패턴이 해결책이 될 수 있습니다.</p>
<p>Compound Component의 핵심이 <strong>부모를 통해 자식에게 상태를 &#39;보이지 않게 전달한다&#39;</strong>이기 때문입니다. 부모에게 필요한 상태, 메서드를 전달하고 하위 요소에 전달하는 방식으로 구현합니다. 이를 위해 <strong>React는 Context API</strong>를 <strong>Vue에서는 provide/inject 기능</strong>을 사용합니다.</p>
<h2 id="2-react로-compound-component-pattern-구현하기">2. React로 Compound Component Pattern 구현하기</h2>
<pre><code class="language-tsx">import { createContext, useContext, type ReactNode } from &quot;react&quot;;

/**
 * RadioGroup 내부에서 Radio들이 공유하는 상태(Context)
 * - name: input name 속성 (같은 그룹임을 표시)
 * - value: 현재 선택된 라디오의 값
 * - onChange: 선택이 변경될 때 실행되는 함수
 */
interface RadioGroupContextProps {
  onChange:(value: string) =&gt; void;
  name: string; 
  value: string; 
}

interface RadioGroupProps extends RadioGroupContextProps {
  children: ReactNode // 하위 Radio Component
  className?: string;
  defaultValue?: string;
}


const RadioGroupContext = createContext&lt;RadioGroupContextProps | null&gt;(null);

export function RadioGroup({name, value, onChange, children, className}: RadioGroupProps) {



  return (
    &lt;RadioGroupContext.Provider value={{name, value, onChange}}&gt;
      &lt;fieldset className={className}&gt;
          {children}
      &lt;/fieldset&gt;
    &lt;/RadioGroupContext.Provider&gt;
  )
}

export const useRadioGroup = () =&gt; {
  const radioGroupContext =  useContext(RadioGroupContext);
  if(!radioGroupContext) throw Error(&#39;Radio는 Group안에서만 사용할 수 있습니다.&#39;);
  return radioGroupContext;
}</code></pre>
<p>Radio Group에서는 Context API를 통해 부모 요소에서 받은 Props를 하위 요소에게 전달하는 역할을 수행하며 공통 로직에 대한 처리를 담당합니다.</p>
<pre><code class="language-tsx">import {  type ReactNode } from &quot;react&quot;;
import { useRadioGroup } from &quot;./group/RadioGroup&quot;;

interface RadioProps {
  disabled?: boolean;
  className?: string;
  defaultChecked?: boolean;
  children: ReactNode;
  value: string;
}

function Radio({ children, value, disabled, defaultChecked=false }:RadioProps) {
  const group = useRadioGroup();

  return (
    &lt;label&gt;
      &lt;input
        type=&quot;radio&quot;
        value={value}
        name={group.name}
        defaultChecked={defaultChecked}
        disabled={disabled}
        checked={group.value !== undefined ? value === group.value : undefined}
        onChange={(e) =&gt; group.onChange &amp;&amp; group.onChange(e.target.value)}
      /&gt;
      {children}
    &lt;/label&gt;
  );
}

export default Radio;</code></pre>
<p>Radio 컴포넌트에서는 Context API에서 받은 값과 Props로 받은 요소들을 조합하여 컴포넌트 값을 바인딩합니다. 이런 방식으로 구현하면 Radio 컴포넌트에 많은 컴포넌트가 생기는 현상을 방지할 수 있어 가독성이 높아지고 확장성이 향상됩니다.</p>
<pre><code class="language-tsx">
import { useState } from &#39;react&#39;
import { RadioGroup } from &#39;./components/group/RadioGroup&#39;
import Radio from &#39;./components/Radio&#39;
function App() {
  const [fruit, setFruit] = useState(&#39;&#39;);
  return (
    &lt;&gt;
    &lt;RadioGroup name=&quot;tools&quot; value={fruit} onChange={(value:string) =&gt; setFruit(value)}&gt;
        &lt;Radio value=&quot;email&quot;&gt;이메일&lt;/Radio&gt;
        &lt;Radio value=&quot;phone&quot;&gt;전화&lt;/Radio&gt;
    &lt;/RadioGroup&gt; 
    &lt;/&gt;
  )
}

export default App
</code></pre>
<h2 id="3-vue에서-compound-component-pattern-구현하기">3. Vue에서 Compound Component Pattern 구현하기</h2>
<pre><code class="language-vue">&lt;script setup lang=&quot;ts&quot;&gt;
import { provide } from &#39;vue&#39;;
// Context 타입 정의
export interface RadioGroupProvideProps {
  name: string;
  value: string
 onChnage:(value: string) =&gt; void
}

// Props 정의
interface RadioGroupProps {
  name: string;
  value: string
  className?: string
}

interface RadioGroupEmits {
  (e: &#39;change&#39;,value:string): void;
}


const props = defineProps&lt;RadioGroupProps&gt;()

const emits = defineEmits&lt;RadioGroupEmits&gt;()

// inject의 키와 동일해야함.
provide&lt;RadioGroupProvideProps&gt;(&#39;RadioGroup&#39;, {
  name: props.name,
  value: props.value,
  onChnage:(value: string) =&gt; emits(&#39;change&#39;, value)
})

&lt;/script&gt;

&lt;template&gt;
  &lt;fieldset :class=&quot;className&quot;&gt;
    &lt;slot /&gt;
  &lt;/fieldset&gt;
&lt;/template&gt;
</code></pre>
<p>리액트와 비슷하게 Vue 또한 부모 컴포넌트에서 props 외에 데이터를 전달하는 방법이 있습니다. Provide/inject 문법입니다. 부모 요소에서 provide를 설정하고 inject로 가져오는 방식입니다. 이 때, 각 매개변수의 첫번째는 <strong>식별자</strong>입니다. provide/inject의 키 값을 동일하게 맞춰야 사용할 수 있습니다. 때문에 보통 변수로 <strong>Symbol 변수</strong>로 선언하고 다른 파일에서 import 하는 방식으로 자주 사용합니다.</p>
<pre><code class="language-vue">&lt;script setup lang=&quot;ts&quot;&gt;
import { inject } from &#39;vue&#39;;
import type { RadioGroupProvideProps } from &#39;./group/RadioGroup.vue&#39;;

interface RadioProps {
  value: string;
  disabled?: boolean;
  className?: string;
}

const group = inject&lt;RadioGroupProvideProps&gt;(&#39;RadioGroup&#39;);

const props = withDefaults(defineProps&lt;RadioProps&gt;(), {
  disabled: false,
  className: &#39;&#39;
})

// Context 가져오기 (없으면 에러)
if (!group) {
  throw new Error(&#39;Radio는 RadioGroup 안에서만 사용할 수 있습니다.&#39;);
}
&lt;/script&gt;

&lt;template&gt;
  &lt;label :class=&quot;props.className&quot;&gt;
    &lt;input
      type=&quot;radio&quot;
      :value=&quot;value&quot;
      :name=&quot;group.name&quot;
      :checked=&quot;props.value === group.value&quot;
      :disabled=&quot;disabled&quot;
      @change=&quot;() =&gt; group.onChnage(props.value)&quot;
    /&gt;
    &lt;slot /&gt;
  &lt;/label&gt;
&lt;/template&gt;</code></pre>
<p>자식 컴포넌트는 React와 매우 흡사합니다. inject로 부모 요소의 값을 받는다 외에는 동일하게 보여집니다.</p>
<pre><code class="language-vue">
&lt;script setup lang=&quot;ts&quot;&gt;
import { ref } from &#39;vue&#39;;
import RadioGroup from &#39;./components/group/RadioGroup.vue&#39;;
import Radio from &#39;./components/Radio.vue&#39;;

// React의 useState와 동일
const fruit = ref&lt;string&gt;(&#39;&#39;);
&lt;/script&gt;

&lt;template&gt;
  &lt;RadioGroup
    name=&quot;tools&quot;
    :value=&quot;fruit&quot;
    :onChange=&quot;(value: string) =&gt; (fruit = value)&quot;
    v-slot=&quot;{Radio}&quot;
  &gt;
    &lt;Radio value=&quot;email&quot;&gt;이메일&lt;/Radio&gt;
    &lt;Radio value=&quot;phone&quot;&gt;전화&lt;/Radio&gt;
  &lt;/RadioGroup&gt;
&lt;/template&gt;</code></pre>
<p>선언 방식 또한 문법의 차이일 뿐 방식은 동일합니다.</p>
<h2 id="정리">정리</h2>
<p>오늘은 React와 Vue의 Compound Component Pattern을 직접 구현하였습니다. 부모에서 자식 요소에게 데이터를 전달하는 방식 자체는 Vue가 더 편한것처럼 보이지만 타입의 props, emits 등 다양한 선언으로 인해 타입과 코드의 길이가 좀 더 늘어나는 것 같습니다. 글 읽어주셔서 감사합니다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[DND 13기를 마치며!!]]></title>
            <link>https://velog.io/@yjh-1008/DND-13%EA%B8%B0%EB%A5%BC-%EB%A7%88%EC%B9%98%EB%A9%B0</link>
            <guid>https://velog.io/@yjh-1008/DND-13%EA%B8%B0%EB%A5%BC-%EB%A7%88%EC%B9%98%EB%A9%B0</guid>
            <pubDate>Wed, 10 Sep 2025 13:25:32 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>처음으로 들어간 DND 동아리가 성공적..?으로 마무리되었습니다..! 처음으로 React Native를 사용한 앱 개발도 해보고 동아리 활동도 진행해서 어려웠던 부분도 있었지만 그래도 많은 부분을 배우고 얻어갈 수 있어서 너무 좋은 시간이였습니다</p>
</blockquote>
<h2 id="1-프로젝트-설명">1. 프로젝트 설명</h2>
<p>제가 만들었던 앱은 런닝을 처음 하는 사람들이 친구 혹은 크루와 함께 런닝을 진행하며 운동을 독려하는 앱(<strong>Runky</strong>)였습니다. 그룹간의 러닝 공유 및 독려, 그룹 혹은 개인 목표 완료 시, 뽑기 시스템을 통한 캐릭터 획득을 컨텐츠로 정하여 개개인의 러닝을 독려하는 앱으로 개발했습니다!
아래는 제가 개발했던 앱의 프로토타입 이미지입니다!!</p>
<p><img src="https://velog.velcdn.com/images/yjh-1008/post/16036441-be8e-4e26-816d-b5021a46a847/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/yjh-1008/post/70cf3890-7187-4a0c-adf9-c13f83544358/image.png" alt=""></p>
<h2 id="2-프로젝트-진행시-어려웠던-점">2. 프로젝트 진행시 어려웠던 점</h2>
<h3 id="1-react-native의-버전-이슈">1. React Native의 버전 이슈..</h3>
<p>React Native를 많이 개발 안해본 입장에서 가장 어려웠던 부분은 <code>버전 호환성 이슈</code>였습니다. 예를 들어 expo 기반의 SDK53 버전에서는 expo-notification이 작동을 안한다던가.. FCM 토큰을 구현하는 방법이 버전별로 상이하여 구현에 어려움을 겪는다던가.. 개발 환경 부분에서 너무 복잡해서 스트레스를 많이 받았던 사항이였습니다...<code>(역시 웹이 짱!!)</code></p>
<h3 id="2-web---app간의-원활한-데이터-공유">2. Web &lt;-&gt; App간의 원활한 데이터 공유</h3>
<p>WebView로 구현할 때 가장 많이 고민해야 했던 부분이었습니다. Web, App 간의 상호 라우팅 전환, GPS와 같은 앱 권한의 데이터 전송 등 Web, Was뿐만이 아닌, App &lt;-&gt; Web, App Web &lt;-&gt; Was 등 기존의 웹에서는 없었던 다양한 비지니스 로직이 추가되어야 했습니다.</p>
<h2 id="3-프로젝트-하면서-좋았던-점">3. 프로젝트 하면서 좋았던 점</h2>
<h3 id="1-사용자들을-위한-서비스">1. 사용자들을 위한 서비스</h3>
<p>이 부분이 가장 즐겁고 기뻤습니다..! 저는 현재 백오피스 웹을 개발하고 있어서 일반 사용자들이 즐기는 앱을 만들고 싶어 하는 갈증이 있었는데 DND를 진행하며 그런 다양한 경험을 할 수 있어서 너무 좋았습니다. 또한 젊은 타겟을 목표로 하며 서비스 앱 혹은 웹은 어떤 계획을 갖고 접근해야 하는지, 어떤 목표를 가져야 하는지 알 수 있었던 시간이였습니다 :)</p>
<h3 id="2-다양한-사람들과의-네트워킹">2. 다양한 사람들과의 네트워킹</h3>
<p>다양한 분야의 분들과의 네트워킹을 진행하며 개발자로서의 고민과 역량 향상, 그리고 각 파트에서의 고민은 무엇인지 알아가며 I인 개발자에서 조금 외향적인 개발자가 될 수 있었던 시간이었습니다 ㅎㅎ</p>
<h2 id="4-그래서-또-할건가">4. 그래서 또 할건가..?</h2>
<p>네 저는 다음에도 이런 기회가 온다면 또 해보고 싶습니다 ㅎㅎ 기간은 짧고 힘들었지만 사용자 서비스를 만든다는게 너무나도 재미있었고 다양한 분들과 알아간다는게 너무 좋았기 때문입니다 ㅎㅎ 혹시 DND, Nexters와 같은 동아리를 고민하고 계신 분들이 계신다면 꼭 추천하고 싶습니다! 짧은 기간에 만들어내야 하는 어려움이 있지만 그만큼 얻어가는게 많은 동아리니까요!! 오늘도 긴 글 읽어주셔서 감사합니다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Broadcast Channel API를 알아보자]]></title>
            <link>https://velog.io/@yjh-1008/Broadcast-Channel-API%EB%A5%BC-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@yjh-1008/Broadcast-Channel-API%EB%A5%BC-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Sun, 13 Jul 2025 03:21:01 GMT</pubDate>
            <description><![CDATA[<h2 id="서론">서론</h2>
<p>사내 AI 프로젝트를 고도화하던 중, 답변을 클릭하여 모달이 아닌 새로운 탭창에서 그래프를 보여줘야하는 로직이 있었습니다. 실시간으로 변하는 데이터를 탭에 전송해야 했기에 여러 방법을 찾아보았습니다. 
그러던 중 <code>window.postmessage</code>와 <code>BroadcastChannel API</code>를 알게 되었고, <code>BroadcastChannel API</code>를 사용하게 되었습니다. 오늘은 BroadcastChannel API의 간단한 사용법을 정리해보겠습니다.</p>
<h1 id="1-broadcast-channel-api란">1. Broadcast Channel API란?</h1>
<blockquote>
<p>Broadcast Channel API는 브라우징 맥락들 (예: 창, 탭, 프레임, iframe)과 동일한 출처에 있는 워커들 간의 기본적인 통신을 허용합니다.
출처 <a href="https://developer.mozilla.org/ko/docs/Web/API/Broadcast_Channel_API">MDN</a></p>
</blockquote>
<p><code>Broadcast Channel API</code>는 웹 애플리케이션에서 동일한 출처(SOP)내의 탭, 윈도우, iFrame, Web Worker 간의 데이터를 실시간으로 교환할 수 있도록 지원하는 기능입니다. BroadcastChannel를 활용하여 채널을 등록, 데이터 수신/송신 메서드를 등록하여 실시간으로 데이터를 업데이트 할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/yjh-1008/post/47862b1c-b6b8-4f67-bc13-a9eb88d800cd/image.png" alt="MDN"></p>
<h1 id="2broadcast-channel-api-사용법">2.Broadcast Channel API 사용법</h1>
<p>기본적인 사용법은 <code>new BroadcastChannel(&#39;채널명&#39;)</code> 을 통해 생성하여 <code>postMessage</code>, <code>onmessage</code> 메서드를 활용하여 데이터를 송/수신 할 수 있습니다.</p>
<pre><code class="language-js">// channel 생성
const someChannel = new BroadcastChannel(&#39;some-channel&#39;);

// 데이터 송신
someChannel.postMessage(JSON.stringify({a: &#39;b&#39;}));

// 데이터 수신
someChannel.onmessage = (event) =&gt; {
    const data = JSON.parse(event.data);
};

// 채널 닫기
someChannel.close();</code></pre>
<h1 id="3-windowpostmessage와-broadcastchannel-api">3. window.postMessage와 BroadcastChannel API</h1>
<p><code>window.postMessage</code>와 <code>BroadcastChannel API</code>는 SOP에서 차이점이 있습니다. <code>window.postMessage</code>는 다른 출처에서도 데이터의 송/수신이 가능하지만 특정 iframe 윈도우 내에서만 데이터의 교환이 가능하도록 설계되었습니다. 
이에 반해, <code>BroadcastChannel</code> API는 SOP를 만족하는 모든 브라우저 탭 간의 데이터를 공유할 수 있도록 설계되었습니다.</p>
<p>저의 경우 같은 도매인 내의 여러 탭에 데이터를 전송해야했기에 <code>BroadcastChannel API</code>를 사용하였습니다.</p>
<h2 id="결론">결론</h2>
<p>BroadcastChannel API는 여러 탭 간의 데이터의 동기화가 필요할 때, 사용할 수 있는 유용한 API입니다. BroadcastChannel API도 Web worker와 같이 close 메소드를 사용하여 적절한 시점에 닫아주는 것이 필요합니다. 다음 시간에는 mock 데이터를 통해서 실시간으로 데이터를 송/수신하는 로직을 만들어보겠습니다!</p>
<h2 id="참고">참고</h2>
<p><a href="https://developer.mozilla.org/ko/docs/Web/API/Broadcast_Channel_API">MDN(BroadcastChannel API)</a>
<a href="https://toby2009.tistory.com/68">BroadcastChannel API란?</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[setInterval 대신 requestAnimationFrame으로 사용합시다!!]]></title>
            <link>https://velog.io/@yjh-1008/requestAnimationFrame-%ED%86%BA%EC%95%84%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@yjh-1008/requestAnimationFrame-%ED%86%BA%EC%95%84%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Sun, 22 Jun 2025 15:23:18 GMT</pubDate>
            <description><![CDATA[<p>웹 페이지의 애니메이션을 구현할 때, CSS의 <code>transform</code>, <code>animation</code>, <code>transition</code>과 같은 속성들로 조작할 수 있지만, 사용자와의 인터렉션을 통한 복잡한 animation을 구현할 때는 <code>javascript</code>을 사용하여 애니메이션을 구현합니다. 예를 들어 버튼을 클릭하면 바운스 뒤 사리지는 효과, 스크롤 시 발생하는 다양한 애니메이션들이 여기에 해당합니다.</p>
<p>그렇기 때문에 CSS로 조작이 어려운 애니메이션의 경우 javascript로 구현하는 경우가 많은데 javascript로 구현하는 경우에는 CSS로 구현할 때보다 성능면에서 떨어지게됩니다. 이러한 성능을 개선하기 위해 여러 최적화 기법이 존재하는데 오늘은 <code>requestAnimationFrame</code>이란 메서드를 통해 최적화하는 방법을 소개하려고합니다.</p>
<h1 id="requestanimationframe이란">requestAnimationFrame이란?</h1>
<blockquote>
<p><a href="https://developer.mozilla.org/ko/docs/Web/API/Window/requestAnimationFrame">window.requestAnimationFrame()</a> 메서드는 브라우저에게 수행하기를 원하는 애니메이션을 알리고 다음 리페인트 바로 전에 브라우저가 애니메이션을 업데이트할 지정된 함수를 호출하도록 요청합니다. 이 메서드는 리페인트 이전에 호출할 인수로 콜백을 받습니다.</p>
</blockquote>
<p>위 설명은 MDN 공식 사이트에 기재되어 있는 requestAnimationFrame 설명입니다. 위 내용에서 리페인트 바로 전에 호출하도록 요청한다고 하는데 이를 이해하기 위해는 <strong>브라우저의 렌더링 단계를 이해</strong>해야합니다.</p>
<h2 id="브라우저의-렌더링-단계">브라우저의 렌더링 단계</h2>
<p>브라우저의 렌더링 단계는 총 5단계로 나뉩니다.</p>
<ol>
<li>DOM 트리 생성</li>
<li>CSS 돔 트리 생성</li>
<li>Render Tree 생성</li>
<li>layout(Reflow)</li>
<li>paint(Repaint)</li>
<li>Composite</li>
</ol>
<p>브라우저의 렌더링 단계는 크게 위 6단계를 따르고 있습니다. requestAnimationFrame은 위 단계 중 <code>repaint</code> 단계에 맞춰 애니메이션을 실행시켜줍니다.</p>
<p>일반적으로 60FPS에 맞춰 실행되지만, 사용자의 디스플레이 주사율(hz)에 따라 변경될 수 있습니다.</p>
<blockquote>
<p>60FPS에 맞춰 실행된다는 의미는 1초에 60개의 프레임을 만들기 위해 1000ms/60FPS = 16.6ms 마다 콜백 함수가 호출된다는 의미입니다.</p>
</blockquote>
<h3 id="용어-정리">용어 정리</h3>
<ul>
<li><code>fps(frame per second)</code> : 그리고 특정 시간 내에 보여지는 frame 갯수</li>
<li><code>hz(Hertz)</code> : 1초동안 진동하는 수. 화면이 1초동안 새로고침 하는 횟수.</li>
</ul>
<h3 id="왜-60hz가-기본-단위일까">왜 60hz가 기본 단위일까?</h3>
<blockquote>
<p>사람의 눈은 1초에 60번 이상의 장면이 넘어가야 부드럽다고 느낀다고합니다. 때문에 현대 영상 및 애니메이션은 최소 초당 60번 화면을 그리도록 설계합니다. </p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/yjh-1008/post/d2c3faa2-0837-48c9-a52b-3a1a1390e6f1/image.gif" alt="프레임 설명"></p>
<h2 id="타이머-메서드와-requestanimationframe의-차이점">타이머 메서드와 requestAnimationFrame의 차이점</h2>
<p>앞서 말했듯이 requestAnimationFrame은 특정 주기에 맞춰 코드를 재호출하는 방식으로 동작합니다. 그렇다면 <code>setInterval</code>, <code>setTimeout</code>과 같은 메서드로 구현할 수도 있는데 왜 requestAnimationFrame을 사용하는 것일까요?</p>
<pre><code class="language-js">function someFunction(){}

setInterval(someFunction, 1000/60);
setTimeout(someFunction, 1000/60);</code></pre>
<h3 id="timer-메서드의-문제점">Timer 메서드의 문제점</h3>
<p>그 이유는  <code>&quot;requestAnimationFrame은 위 단계 중 repaint 단계에 맞춰 애니메이션을 실행시켜줍니다.&quot;</code>라는 설명과 연관되어 있습니다. setInterval과 setTimeout과 같은 타이머 메서드는 단순히 특정 시간, 특정 주기에 맞춰 메서드를 실행할 뿐, <code>프레임을 신경쓰지 않고 동작합니다.</code> </p>
<p>만약 특정 주기마다 실행되어야하는 메서드가 다른 작업으로 인해 지연되어 프레임 중간에 실행될 수 있습니다. 그렇게 된다면 특정 프레임이 생성되지 못하고 프레임이 깎이는 <code>프레임 드랍</code> 현상이 발생할 수 있습니다. 프레임 드랍이 발생하면 사용자는 화면이 버벅이는 듯한 움직임을 받게되고 UX적으로 안좋은 현상이 발생할 수 있게 됩니다.</p>
<h3 id="requestanimationframe">requestAnimationFrame</h3>
<p>requestAnimationFrame은 타이머 메서드와 달리 프레임을 그릴 준비가 되면 <code>화면이 갱신되는 주기에 맞춰</code> 메서드를 실행합니다. 이는 프레임 드랍이 발생하는걸 방지해주며 끊기는 현상 없이 부드럽게 애니메이션을 만들어줍니다.</p>
<p><img src="https://velog.velcdn.com/images/yjh-1008/post/0cc98595-6b93-4023-b967-5f6c3f75d92a/image.png" alt=""></p>
<p>위 사진은 메서드에 수행 시간에 관계 없이 프레임 시간에 맞춰 실행시키는 모습을 시각화한 것입니다.</p>
<h2 id="requestanimationframe-사용-방법">requestAnimationFrame 사용 방법</h2>
<blockquote>
<p>requestAnimationFrame(callback);</p>
</blockquote>
<ul>
<li><p>callback
다음 리페인트를 위한 애니메이션을 업데이트할 때 호출할 함수. 콜백 함수에는 requestAnimationFrame()이 콜백 함수들의 실행을 시작할 시점을 나타내는 performance.now() 에 의해 반환되는 것과 유사한 DOMHighResTimeStamp 단일 인수가 전달됩니다.</p>
</li>
<li><p>DOMHighResTimeStamp
현재 프레임이 시작된 시간을 나타내는 고해상도 타임스탬프입니다. 이 값을 사용하여 애니메이션의 진행 상태를 계산하고 애니메이션 속성을 업데이트할 수 있습니다.</p>
</li>
</ul>
<pre><code class="language-js">  function animate(timestamp) {
    if (!start) start = timestamp;
    const elapsed = timestamp - start;

    const progress = Math.min(elapsed / duration, 1);
    const x = distance * progress;

    box.style.transform = `translateX(${x}px)`;

    if (progress &lt; 1) {
      requestAnimationFrame(animate);
    }
  }</code></pre>
<h2 id="requestanimationframe의-장점">requestAnimationFrame의 장점</h2>
<ol>
<li><p>백그라운드 동작 중지
requestAnimationFrame은 페이지가 비활성화 될 때 브라우저에 의해 일시 중지되기 때문에 CPU 리소스를 절약할 수 있습니다.</p>
</li>
<li><p>주사율에 맞춘 호출 횟수
requestAnimationFrame은 자동으로 hz에 맞춰 호출 횟수를 지정해주기 때문에 개발자가 따로 설정하지 않아도 최적화 된 호출 횟수를 제공합니다.</p>
</li>
<li><p>Animation Queue 처리
requestAnimationFrame 메서드는 타이머 이벤트와 같이 <code>애니메이션을 그리기 위한 콜백</code>으로 비동기로 분류되어 처리합니다. 이 때 requestAnimationFrame은 다른 비동기 작업과 달리 <code>animation frame</code>이라는 별개의 queue에서 처리됩니다.</p>
</li>
</ol>
<h2 id="정리">정리</h2>
<p>requestAnimationFrame은 자바스크립트로 애니메이션을 조작할 때 최적화하기 위핸 API로 화면 갱신 주기에 맞춰 프레임 드랍없이 애니메이션을 실행시켜줍니다. 또한 hz에 맞춰 메서드 호출 횟수를 정해주기 때문에 성능적으로도 최적화 된 메서드 호출 횟수를 저장해줍니다. </p>
<h2 id="참조">참조</h2>
<p><a href="https://velog.io/@woogur29/requestAnimationFramerAF-%ED%86%BA%EC%95%84%EB%B3%B4%EA%B8%B0#%EC%82%AC%EC%9A%A9-%EB%B0%A9%EB%B2%95">requestAnimationFrame(rAF) 톺아보기</a>
<a href="https://inpa.tistory.com/entry/%F0%9F%8C%90-requestAnimationFrame-%EA%B0%80%EC%9D%B4%EB%93%9C#requestanimationframe_%EC%9E%A5%EC%A0%90">🌐웹 애니메이션 최적화 requestAnimationFrame 가이드
출처: https://inpa.tistory.com/entry/🌐-requestAnimationFrame-가이드#requestanimationframe_장점 [Inpa Dev 👨‍💻:티스토리]</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Vite의 import.meta.env를 알아보자]]></title>
            <link>https://velog.io/@yjh-1008/Vite%EC%9D%98-import.meta.env%EB%A5%BC-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@yjh-1008/Vite%EC%9D%98-import.meta.env%EB%A5%BC-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Sat, 31 May 2025 15:53:33 GMT</pubDate>
            <description><![CDATA[<p>회사에서 Vue 프로젝트를 진행하며 가장 잘 바꿨다고 생각하는 부분은 <strong>번들러</strong>입니다. Webpack을 사용할 때 느린 번들링 및 빌드 속도로 인해 너무 답답하게 느껴졌고, 이는 프로젝트의 규모가 더욱 커지고 소스코드가 많아질수록 크게 와닿았습니다.</p>
<p>이를 해결하기 위해 Vue 진영에서 강력하게 밀고 있는 Vite로 마이그레이션을 진행하게 되었습니다. Vite의 공식 문서에서 소개하는 가장 큰 특징은 devServer 속도입니다. Vite는 앱을 시작할 때 node_modules에 있는 라이브러리를 ESM으로 변환하여 캐시상태로 저장하고 devServer를 cold-start할 때 한번만 실행됩니다. </p>
<p>Webpack은 모든 파일을 빌드와 번들링을 진행해야 구동이 되었지만 Vite는 ESM으로 제공하기 때문에 원하는 라이브러리만 <strong>동적으로 가져오는 것이 가능</strong>해졌습니다.</p>
<h2 id="환경변수">환경변수</h2>
<p>Node.js에서는 process라는 예약어를 통해 환경 변수에 접근할 수 있도록 했습니다. Node.js를 통해 구동시키는 라이브러리, 프레임워크는 이 환경 변수에 값을 추가하거나 수정하여 프로젝트에 원하는 값(secret Key, ID) 등을 주입할 수 있습니다.</p>
<pre><code class="language-node">process.env.SECRET_KEY = &quot;test123&quot;
console.log(process.env.SECRET_KEY) //test123</code></pre>
<p>위와 같이 값을 주입하는 방식보다는 .env 파일을 만들고 그 내부에 있는 값들을 process.env에 주입하여 사용하는데 dotenv 라이브러리가 이와 같은 과정을 처리해 줍니다. </p>
<h3 id="dotenv">dotenv</h3>
<p>dotenv 라이브러리는 .env에 있는 값들을 process.env에 주입시켜주는데 이 과정을 통해 <strong>런타임 javascript</strong>에서 사용할 수 있습니다. 이 과정에 어떻게 이루어진 건지 궁금하여 소스 코드를 확인해 보았습니다.</p>
<pre><code>npm init -y
npm i -D dotenv</code></pre><p>위 명령어를 통해 프로젝트를 생성해 줍니다. 저는 ESM 환경을 선호하기 때문에</p>
<pre><code class="language-json">{
  &quot;name&quot;: &quot;dotenv&quot;,
  &quot;version&quot;: &quot;1.0.0&quot;,
  &quot;main&quot;: &quot;index.js&quot;,
  &quot;type&quot;: &quot;module&quot;,
//...
}
</code></pre>
<p><strong>type: module을 선언하여 ESM 환경으로 설정하였습니다.</strong></p>
<pre><code class="language-bash">TEST=123</code></pre>
<pre><code class="language-js">import dotEnv from &#39;dotenv&#39;;

console.log(dotEnv.config())</code></pre>
<p><img src="https://velog.velcdn.com/images/yjh-1008/post/65ae66c3-decf-43e3-8db3-bc196be5ec4a/image.png" alt="env 실행 결과"></p>
<p>프로젝트 루트 디렉터리에 .env파일을 생성하고 출력해 보면 위와 같이 결괏값이 출력됩니다.</p>
<pre><code class="language-js">// Populates process.env from .env file
function config (options) {
  // fallback to original dotenv if DOTENV_KEY is not set
  if (_dotenvKey(options).length === 0) {
    return DotenvModule.configDotenv(options)
  }

  const vaultPath = _vaultPath(options)

  // dotenvKey exists but .env.vault file does not exist
  if (!vaultPath) {
    _warn(`You set DOTENV_KEY but you are missing a .env.vault file at ${vaultPath}. Did you forget to build it?`)

    return DotenvModule.configDotenv(options)
  }

  return DotenvModule._configVault(options)
}</code></pre>
<p>dotenv 라이브러리에서 제공하는 config 메서드는 위와 같습니다.
저희는 아무런 옵션도 제공하지 않았기 때문에 내부적으로 <code>configDotEnv</code> 메서드를 호출하고 있는 것을 확인할 수 있습니다.</p>
<pre><code class="language-js">function configDotenv (options) {
  const dotenvPath = path.resolve(process.cwd(), &#39;.env&#39;)
  //...

  for (const path of optionPaths) {
    try {
      // Specifying an encoding returns a string instead of a buffer
      const parsed = DotenvModule.parse(fs.readFileSync(path, { encoding }))

      DotenvModule.populate(parsedAll, parsed, options)
    } catch (e) {
      if (debug) {
        _debug(`Failed to load ${path} ${e.message}`)
      }
      lastError = e
    }
  }

  let processEnv = process.env
  if (options &amp;&amp; options.processEnv != null) {
    processEnv = options.processEnv
  }

  DotenvModule.populate(processEnv, parsedAll, options)

  if (lastError) {
    return { parsed: parsedAll, error: lastError }
  } else {
    return { parsed: parsedAll }
  }</code></pre>
<p>위 메서드으 동작 순서는 아래와 같습니다.</p>
<ol>
<li>process.cwd() 메서드를 호출하여 node 명령을 실행한 프로젝트의 절대 경로를 반환한다.</li>
<li>path.resolve 메서드를 호출하여 /프로젝트의 절대 경로/.env 경로를 반환한다.</li>
<li>parsed 변수에 .env 파일의 값을 파싱 해서 저장한다.
3-1. readFileSync로 읽어온 파일(.env)의 값을 <code>DotenvModule.parsed</code>메서드를 통해 Javascript 객체로 변환한다.
3-2. 이 때의 결과값이 <code>{parsed: {test: &quot;123&quot;}}</code>의 객체값이다.</li>
<li><code>populate</code> 메서드를 한다.</li>
</ol>
<pre><code class="language-js">// Populate process.env with parsed values
function populate (processEnv, parsed, options = {}) {

  // Set process.env
  for (const key of Object.keys(parsed)) {
    if (Object.prototype.hasOwnProperty.call(processEnv, key)) {
    processEnv[key] = parsed[key]
  }
}</code></pre>
<p>populate 메서드에서는 파싱 된 key 값을 통해 processEnv 환경 변수에 값을 주입하고 있습니다.</p>
<h3 id="정리">정리</h3>
<blockquote>
<p>dotEnv 라이브러리는 <code>.env</code> 파일 값을 파싱해 객체로 변환 후 process.env 환경 변수에 값을 저장한다. 이 전역 변수가 저장될 때에는 server가 구동한 초기에 값을 넣어주는데 Vite에서는 이 값이 변경되면 순간적으로 devServer를 다시 구동시킵니다.</p>
</blockquote>
<h2 id="vite에서의-환경-변수-관리">Vite에서의 환경 변수 관리</h2>
<p>Vite 또한 여느 라이브러리처럼 env에 값을 주입하여 환경변수로 사용할 수 있습니다.
Webpack과는 다르게 <a href="https://ko.vite.dev/guide/env-and-mode">Vite</a>에서는 <a href="https://www.google.com/search?q=import.meta&amp;sca_esv=1ae95d7956bcc2bc&amp;rlz=1C5CHFA_enKR1020KR1020&amp;ei=ArM6aIqxEebm2roPr_um6Ac&amp;ved=0ahUKEwiKutbvms2NAxVms1YBHa-9CX0Q4dUDCBA&amp;uact=5&amp;oq=import.meta&amp;gs_lp=Egxnd3Mtd2l6LXNlcnAiC2ltcG9ydC5tZXRhMgUQABiABDIFEAAYgAQyBRAAGIAEMgUQABiABDIFEAAYgAQyBRAAGIAEMgUQABiABDIFEAAYgAQyBRAAGIAEMgUQABiABEisBlCzA1izBHACeAGQAQCYAYMBoAHxAaoBAzAuMrgBA8gBAPgBAZgCA6ACfcICChAAGLADGNYEGEeYAwCIBgGQBgqSBwMyLjGgB7UOsgcDMC4xuAd0wgcDMi0zyAcL&amp;sclient=gws-wiz-serp">import.meta</a>라는 자바스크립트 문법을 통해 접근하라고 설명하고 있습니다.</p>
<h2 id="importmeta란-무엇인가">import.meta란 무엇인가?</h2>
<p>MDN 공식 사이트에서 말하는 import.meta는 아래와 같습니다</p>
<blockquote>
<p>import.meta 속성은 모듈의 메타 데이터를 JavaScript 모듈에 노출합니다. 여기에는 URL과 같은 모듈에 대한 정보가 포함됩니다.</p>
</blockquote>
<p>아까 작성했던 <code>main.js</code>에서 import.meta를 console.log로 작성하면 아래와 같이 나옵니다.</p>
<pre><code class="language-js">console.log(import.meta)</code></pre>
<h3 id="1-server">1. server</h3>
<p><img src="https://velog.velcdn.com/images/yjh-1008/post/8ad7e8ac-6823-421a-ae36-eaba5e24989b/image.png" alt="import.meta"></p>
<h3 id="2-broswer">2. broswer</h3>
<p><img src="https://velog.velcdn.com/images/yjh-1008/post/c8f88fae-cd24-4cc4-a913-c2608df52355/image.png" alt=""></p>
<p><strong>주의 : import.meta는 ESM(ECMAScript Modules) 환경에서만 동작하기 때문에 package.json에서 type:module을 작성하거나 main.mjs와 같이 확장자를 설정해야 합니다.</strong></p>
<h2 id="vite에서-importmeta로-환경변수를-사용하는-이유">Vite에서 import.meta로 환경변수를 사용하는 이유</h2>
<p>Webpack에서의 process.env는 ESM 환경이 아니기 때문에 브라우저에서 console.log(process.env)를 사용하면 <code>ReferenceError: process is not defined</code>와 같은 오류가 발생합니다. process.env는 JS가 아닌 <strong>Node.js의 전역 객체이기 때문입니다.</strong>
하지만 import.meta는 ECMA에 도입된 새로운 기능이기 때문에 브라우저에서 확인할 수 있습니다. 이를 활용하기 위해 vite에서는 import.meta 객체에 env 속성을 추가하여 환경 변수를 주입하는 것입니다. 이는 <strong>모듈 스코프 내</strong>에서 안전하게 동작하고 브라우저에서도 접근할 수 있게 해주기 때문에 채택하고 있습니다.</p>
<h2 id="보안에서의-이점">보안에서의 이점</h2>
<p>process.env는 서버 환경에서 모든 시스템 환경 변수에 접근할 수 있어 보안을 주의해서 사용해야 합니다. 하지만 Vite의 <code>import.meta.env</code>는 VITE_ 접두사가 붙은 환경 변수만 노출하도록 제한합니다.</p>
<pre><code class="language-bash">TEST=123
VITE_TEST=&quot;abcd&quot;</code></pre>
<pre><code class="language-js">console.log(import.meta.env.TEST) //undefined 출력
console.log(import.meta.env.VITE_TEST) //abcd</code></pre>
<p>위 코드를 실행하면 같이 .env파일에 있지만 VITE_ 접두사가 붙은 값만 브라우저에서 출력됩니다.
<strong>이는 개발자가 의도적으로 노출시키고 싶은 변수만 클라이언트 측에서 접근 가능하게 하여 보안 위험을 줄일 수 있습니다.</strong></p>
<h2 id="결론">결론</h2>
<p>결론적으로, Vite는 모던 웹 개발의 핵심인 ES Modules 기반의 브라우저 환경에 맞춰 import.meta.env를 채택하여 환경 변수를 보다 안전하고 효율적으로 관리해 주기 때문에 사용하는 것으로 보입니다.</p>
<h3 id="참고">참고</h3>
<p><a href="https://pozafly.github.io/environment/why-do-you-use-import-meta-in-vite/">Vite에서 import.meta는 왜 사용하는 걸까? (feat. HMR)</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[peer-dependency를 바로잡아보자..!]]></title>
            <link>https://velog.io/@yjh-1008/peer-dependency%EB%A5%BC-%EB%B0%94%EB%A1%9C%EC%9E%A1%EC%95%84%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@yjh-1008/peer-dependency%EB%A5%BC-%EB%B0%94%EB%A1%9C%EC%9E%A1%EC%95%84%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Sun, 25 May 2025 15:12:36 GMT</pubDate>
            <description><![CDATA[<p>NPM에서 Yarn으로 마이그레이션을 진행하고 리눅스 서버에 배포를 진행하다 peer-dependency 오류가 발생했습니다. 처음에는 &quot;로컬에서는 잘 되는데 왜 서버에서만 문제가 생기지?&quot;라는 의문이 들었습니다. 이전에 rollup 라이브러리가 os에 의해 정상적으로 install 되지 않았던 경험이 있었기에 단순 os 차이인 줄 알았지만 문제의 핵심은 peer dependency의 충족 여부였습니다.</p>
<blockquote>
<p>Some peer dependencies are incorrectly met; run yarn explain peer-requirements for details</p>
</blockquote>
<p>이런 로그와 오류가 발생했습니다.. 이 상황을 해결하기 위해 <code>yarn explain peer-requirements</code>란 무엇이고 어떨 때 쓰는건지 알아보겠습니다.</p>
<h2 id="peer-dependency란">peer dependency란?</h2>
<p>위 명령어를 이해하기 위해서는 <code>peer dependency</code>가 무엇인지 알아야합니다. peer dependency란
어떤 패키지가 &quot;나는 특정 패키지의 특정 버전이 필요해!&quot;라고 요구하는 의존성입니다. 하지만 이 패키지는 직접 설치되지 않고, 프로젝트(혹은 상위 패키지)에서 직접 설치해줘야 합니다.</p>
<p>프로젝트에서 설치한 라이브러리 들이 어떤 peer dependency를 가지고 있는지 조회할 수 있는 명령어가 <code>yarn explain peer-requirements</code>입니다.</p>
<p>이 명령어를 실행하면, 충족되지 않은 peer dependency 목록과 각각의 문제에 대한 해시(hash)가 출력됩니다.
특정 문제의 상세 설명을 보고 싶다면, 아래처럼 해시를 지정해 실행할 수 있습니다.</p>
<pre><code class="language-bash">yarn explain peer-requirements &lt;hash&gt;</code></pre>
<p>예시 결과:
<img src="https://velog.velcdn.com/images/yjh-1008/post/1ad909ba-73f8-4b44-b2be-ad951e2d71d8/image.png" alt="yarn explain peer-requirements"></p>
<p>위 리스트에서 x가 되어이 있는 해시가 peer dependency 충족 되지 않은 라이브러리입니다.</p>
<h2 id="해결-방법">해결 방법</h2>
<blockquote>
<p>저는 이 문제를 .yarnrc.yml의 packageExtension 설정을 통해 <code>가짜 종속성</code>을 주입하여 문제를 해결했습니다.</p>
</blockquote>
<h3 id="packageextensions란">packageExtensions란?</h3>
<p>Yarn의 .yarnrc.yml 파일에서 제공하는 packageExtensions는, 특정 패키지의 의존성 정의를 확장하거나 보완할 수 있게 해주는 기능입니다.
즉, 서드파티 패키지가 잘못된(혹은 누락된) peer dependency를 선언했을 때, 직접적으로 해당 의존성을 추가해 Yarn이 올바르게 dependency tree를 구성하도록 도와줍니다.</p>
<h3 id="실제-적용-예시">실제 적용 예시</h3>
<ol>
<li>문제 진단
yarn explain peer-requirements로 어떤 패키지에서 어떤 peer dependency가 누락됐는지 확인합니다.</li>
</ol>
<p><img src="https://velog.velcdn.com/images/yjh-1008/post/1ad909ba-73f8-4b44-b2be-ad951e2d71d8/image.png" alt="yarn explain peer-requirements"></p>
<p>우리가 사용하는 vue-query가 내부적으로 React 생태계의 react-query를 의존하는데, react가 설치되어 있지 않아 종속성 문제가 발생하는 것입니다.</p>
<p>하지만 우리는 vue를 사용하고 react를 사용하기 때문에 라이브러리를 설치 할 필요가 없습니다. 오히려 설치하면 불필요한 라이브러리이기 때문에 번들 사이즈만 증가시키는 문제가 발생합니다. </p>
<p>이를 해결하기 위해 .yarnrc.yml에 <code>가짜 의존성</code>을 주입하여 문제를 해결하는 것입니다.</p>
<pre><code class="language-yml">enableStrictSsl: false

npmRegistry: &#39;&#39;

//...

packageExtensions:
  &quot;react-query@3.39.3&quot;:
    peerDependencies:
      &quot;react&quot;: &quot;*&quot;</code></pre>
<p>packageExtendions 설정에 종속성 주입이 필요한 라이브러리를 기입하고 peerDependencies 설정을 통해 가짜 종속성이 필요한 라이브러리를 기입하여 가짜 종속성을 생성할 수 있습니다.</p>
<blockquote>
<p>단 이 문제는 해당 모듈을 import하거나 사용할 일이 없는 경우에만 안전하게 쓸 수 있습니다. 만약 코드에서 react를 직접 사용한다면, 진짜로 패키지를 설치해야 합니다.</p>
</blockquote>
<h2 id="정리">정리</h2>
<p>오늘은 .yarnrc.yml을 사용하여 가짜 종속성을 만들고 이를 통해 peer-dependency 오류를 수정하는 방법을 알아보았습니다. 항상 무슨 내용인지 잘 몰랐는데 빌드를 여러 환경에서 적용하고 시도하다 보니 오류를 수정하게 되어 번들링과 종속성에 대해 좀 더 자세하게 알아보아야 할 것 같습니다..! </p>
<p>수정해야 할 내용이나 피드백이 있다면 언제든지 부탁드리겠습니다. 글 읽어주셔서 감사합니다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Web Work로 이미지 처리 최적화하기]]></title>
            <link>https://velog.io/@yjh-1008/Web-Work%EB%A1%9C-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%B5%9C%EC%A0%81%ED%99%94%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@yjh-1008/Web-Work%EB%A1%9C-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%B5%9C%EC%A0%81%ED%99%94%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 08 May 2025 14:58:04 GMT</pubDate>
            <description><![CDATA[<p>프론트엔드 개발자라면 모두 알다시피 Javascript는 싱글스레드 언어입니다.Javascript는 싱글스레드이기 때문에 한계가 명확히 존재합니다. 그렇기 때문에 작업들을 병렬로 처리할 수 없고, 다른 작업이 끝날 때까지 기다려야합니다.</p>
<p>이를 개선하기 위해 JS는 <code>비동기</code>라는 작업을 통해 런타임 환경에 위임하고 처리합니다. 우리가 보통 말하는 런타임은 아래와 같이 구성되어 있습니다.</p>
<pre><code>[메인 스레드]
 ├─ Call Stack
 ├─ Web APIs
 ├─ Task Queue
 ├─ Microtask Queue
 └─ Event Loop</code></pre><p>메인 스레드가 위 작업들을 순차적으로 처리하는데 이 작업이 많아지면 많아질수록 메인 스레드는 병목 현상을 겪게 됩니다. 그리고 이는 결과적으로 <strong>불러와야 할 데이터, 처리해야 할 이벤트가 늦게 처리되어 UX가 저하된다는 문제점이 발생하게 됩니다.</strong></p>
<p>이러한 문제점을 Web Worker가 해결해줄 수 있습니다!</p>
<h2 id="web-api란-무엇일까">Web API란 무엇일까?</h2>
<p>Web Worker란 <strong>Javascript에서 사용할 수 있는 백그라운드 스레드 실행 환경</strong>입니다. 브라우저의 메인 스레드와 완전히 분리되어 비동기 작업을 처리할 수 있습니다.</p>
<h3 id="js는-싱글-스레드-언어인데-어떻게">JS는 싱글 스레드 언어인데 어떻게..?</h3>
<p>앞서 말한대로 JS는 싱글 스레드 언어입니다. 하지만 우리의 브라우저는 멀티 스테드 환경입니다. JS 엔진(V8엔진)은 싱글 스레드이지만 브라우저는 <strong>렌더링 스레드, 네트워크 스레드, 타이머 스레드, Web Worker 스레드 등 여러 스레드</strong>를 가지고 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/yjh-1008/post/2568727e-9fa4-4791-9f54-ea8fca7cd9f2/image.png" alt=""></p>
<p><code>navigator.hardwareConcurrency</code>를 사용하면 현제 스레드의 개수를 확인할 수 있습니다.</p>
<h2 id="web-worker의-종류">Web Worker의 종류</h2>
<p>같은 출처(origin)를 가진 여러 탭, iframe, window, Web Worker에서 공유할 수 있는 워커입니다.
예를 들어, 여러 탭에서 공통 데이터를 캐싱하거나 동기화할 때 사용할 수 있습니다.</p>
<table>
<thead>
<tr>
<th>워커 종류 &nbsp;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;</th>
<th>특징 및 설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Dedicated Worker</strong><br>(전용 워커)</td>
<td>한 개의 스크립트(혹은 탭)에서만 사용하는 워커입니다. 생성한 곳(부모)과 1:1로 메시지를 주고받으며, 복잡한 계산이나 데이터 처리를 메인 스레드와 분리해서 실행할 때 주로 사용합니다. 메인 스레드와 워커 간의 postMessage를 통해 양방향 통신을 진행할 수 있습니다.</td>
</tr>
<tr>
<td><strong>Shared Worker</strong><br>(공유 워커)</td>
<td>같은 도메인 내 여러 창, 탭, iframe 등에서 공유할 수 있는 워커입니다. 여러 스크립트가 동시에 접근 가능하며, <code>active port</code>로 통신합니다. 여러 곳에서 동일한 백그라운드 작업이 필요할 때 유용합니다.</td>
</tr>
<tr>
<td><strong>Service Worker</strong><br>(서비스 워커)</td>
<td>웹 앱과 브라우저(또는 네트워크) 사이에서 프록시 서버처럼 동작합니다. 네트워크 요청을 가로채거나, 오프라인 동작, 캐싱, 푸시 알림 등 백그라운드 작업에 사용됩니다. DOM에 접근할 수 없고, 탭이 모두 닫혀도 백그라운드에서 동작할 수 있습니다. Progressive Web App(PWA)의 핵심 기술입니다. (localhost를 제외하고 https에서만 사용할 수 있습니다.)</td>
</tr>
</tbody></table>
<h2 id="이미지-최적화-진행하기">이미지 최적화 진행하기</h2>
<blockquote>
<p>사용자로부터 base64 문자열을 받았을 때 해당 문자열을 Web Worker에서 File객체로 변환하고 메인 스레도로 전달합니다.</p>
</blockquote>
<pre><code class="language-js">// 워커에서 실행되는 코드
self.onmessage = function(event) {
  const { base64, fileNm, mimeType } = event.data;

  try {
    const splitBase64 = base64.split(&#39;,&#39;)[1] ? base64.split(&#39;,&#39;)[1] || base64;

    // base64, 바이너리 변환
    const binary = atob(splitBase64);
    const byteArray = new Uint8Array([...binary].map(char =&gt; char.charCodeAt(0)));

    const blob = new Blob([byteArray], { type: mimeType });
    const file = new File([blob], fileNm, { type: mimeType });

    self.postMessage({ success: true, file });
  } catch (error) {
    self.postMessage({ success: false, error: error.message });
  }
};</code></pre>
<p>Vue3에서 웹 워커를 사용하여 base65를 파일로 변환하고 결과를 처리하는 컴포넌트입니다.</p>
<pre><code class="language-vue">&lt;template&gt;
  &lt;div&gt;
    &lt;h1&gt;Base64 파일 변환&lt;/h1&gt;
    &lt;input type=&quot;file&quot; @change=&quot;handleFileChange&quot; /&gt;
    &lt;div v-if=&quot;convertedFile&quot;&gt;
      &lt;h3&gt;변환된 파일&lt;/h3&gt;
      &lt;a :href=&quot;convertedFileUrl&quot; download&gt;다운로드&lt;/a&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script setup&gt;
import { ref } from &#39;vue&#39;;
// import from &#39;../&#39;
// 상태 변수 선언
const convertedFile = ref(null);
const convertedFileUrl = ref(null);

// 웹 워커 인스턴스 생성
const worker = new Worker(new URL(&#39;../utils/base64-worker.js&#39;, import.meta.url));

// base64 → 파일로 변환하는 함수
const convertBase64ToFile = (base64, fileNm, mimeType) =&gt; {
  return new Promise((resolve, reject) =&gt; {
    worker.onmessage = (e) =&gt; {
      if (e.data.success) resolve(e.data.file);
      else reject(new Error(e.data.error));
    };

    worker.postMessage({ base64, fileNm, mimeType });
  });
};

// 파일 선택 시 호출되는 메서드
const handleFileChange = async (event) =&gt; {
  const file = event.target.files[0];
  if (!file) return;

  // 파일을 base64로 변환
  const base64 = await fileToBase64(file);

  try {
    // base64 → 파일 변환
    const mimeType = file.type;
    const convertedFileObj = await convertBase64ToFile(base64, file.name, mimeType);

    // 변환된 파일 URL 생성
    convertedFile.value = convertedFileObj;
    convertedFileUrl.value = URL.createObjectURL(convertedFileObj);
  } catch (error) {
    console.error(&#39;파일 변환 실패:&#39;, error.message);
  }
};

// 파일을 base64로 변환하는 함수
const fileToBase64 = (file) =&gt; {
  return new Promise((resolve, reject) =&gt; {
    const reader = new FileReader();
    reader.onloadend = () =&gt; resolve(reader.result);
    reader.onerror = reject;
    reader.readAsDataURL(file);
  });
};
&lt;/script&gt;
</code></pre>
<h3 id="offscreencanvas-">OffscreenCanvas ?</h3>
<blockquote>
<p><a href="https://developer.mozilla.org/ko/docs/Web/API/OffscreenCanvas">OffscreenCanvas</a> 인터페이스는 화면 밖에서 렌더링할 수 있는 캔버스를 제공하고 DOM과 Canvas API를 분리하여 <code>canvas</code> 요소가 DOM에 완전히 의존하지 않도록 합니다. 렌더링 작업은 worker 맥락 내에서 실행할 수도 있어서 별도의 스레드에서 일부 작업을 실행하고 메인 스레드에서 무거운 작업을 피할 수 있습니다.</p>
</blockquote>
<h2 id="web-worker를-사용할-때-주의할-점">Web Worker를 사용할 때 주의할 점</h2>
<ol>
<li><p>Web Worker는 다른 스레드를 사용할 수 있게하기에 마냥 좋은것같이 보이지만 무분별하게 사용하면 메모리의 사용량이 늘어나 오히려 성능의 저하를 초래할 수 있습니다. 때문에 적절한 사용과 <strong>사용이 필요 없을 때 꼭 종료를 해주는 것이 필요합니다.</strong></p>
</li>
<li><p>또한 DOM에 접근을 할 수 없기 때문에 메인 스레드에서 백그라운드에 요청을 보낸 뒤, 백그라운드에서 데이터를 처리 및 가공 후 다시 전송하는 과정이 필요합니다.</p>
</li>
<li><p>위 내용과 유사하게 메인스레드에 종속되지 않기 때문에 상태(state) 변화가 즉각적으로 전달되지 않습니다. web worker의 postMessage와 onMessage를 활용하여 처리해야합니다.</p>
</li>
</ol>
<h2 id="정리">정리</h2>
<p>Web Worker는 메인 스레드에서 처리할 작업을 적절하게 분리하여 병목 현상을 줄이고 사용자 UX를 개선할 수 있습니다. 위에서는 파일 다운로드 로직만 구현했지만 Jpeg, Png 파일을 Webp로 변환하거나 setInterval를 사용한 타이머를 제작하는 등 다양한 방면으로 사용할 수 있습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[GZIP, Brotli 어떻게 성능을 개선시켜줄까?]]></title>
            <link>https://velog.io/@yjh-1008/GZIP-Brotil-%EB%AD%94%EB%8D%B0-%EC%84%B1%EB%8A%A5%EC%9D%84-%EA%B0%9C%EC%84%A0%EC%8B%9C%EC%BC%9C%EC%A4%84%EA%B9%8C</link>
            <guid>https://velog.io/@yjh-1008/GZIP-Brotil-%EB%AD%94%EB%8D%B0-%EC%84%B1%EB%8A%A5%EC%9D%84-%EA%B0%9C%EC%84%A0%EC%8B%9C%EC%BC%9C%EC%A4%84%EA%B9%8C</guid>
            <pubDate>Sat, 03 May 2025 15:16:48 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/yjh-1008/post/f7bbf289-5208-4d87-84b0-f90a6aac2be3/image.png" alt="Gzip"></p>
<p>Vite를 통해 최적화를 진행하다보면 Gzip, Brotli로 압축하여 전송하라는 내용들이 많이 보입니다. 그렇다면 Gzip, Grotil은 무엇인데 사용자에게 빠른 경험을 제공할 수 있을까요?</p>
<h2 id="gzip">Gzip</h2>
<p>Gzip은 번들링 이후 단계에서 만들어진 파일들을 압축하는 알고리즘 입니다. 서버에서 클라이언트로 데이터를 전송할 때 ** 데이터의 크기를 줄이며 네트워크 전송 시간을 단축하는데 목적을 두고 있습니다.**</p>
<p>일반적으로 번들링과 Gzip 압축을 같은 개념으로 생각하는 분들이 많은데 두 개를 동시에 진행하는 것이 빌드 과정이라고 생각하면 될것 같습니다.</p>
<ol>
<li>번들링을 통해 분리된 파일들을 합치고 사이즈를 줄인다.</li>
<li>번들링을 통해 합쳐진 파일들을 압축하여 전송 크기를 줄인다.</li>
</ol>
<p>일반적으로 Gzip을 적용하고 빌드를 진행했을 때 발생하는 순서입니다. </p>
<h3 id="gzip-특징">Gzip 특징</h3>
<ul>
<li>Gzip은 웹에서 파일을 압축/전송하기 위한 파일 포맷이자 소프트웨어입니다.</li>
<li>1992년 GNU 프로젝트에서 개발되었으며, <a href="https://en.wikipedia.org/wiki/LZ77_and_LZ78">LZ77</a> 알고리즘과 <a href="https://ko.wikipedia.org/wiki/DEFLATE">Deflate</a> 방식을 사용합니다.</li>
<li><code>무손실 압축</code>이기 때문에, 압축/해제 과정에서 데이터 손실이 없습니다.</li>
</ul>
<blockquote>
<p>여기서 주요한 특징은 <code>무손실 압축</code>이라는 부분입니다. 데이터를 압축하고 해제하는 과정에서 데이터의 손실이 없기 때문에 사용자는 빠른 경험을 받을 수 있는 것입니다.</p>
</blockquote>
<h3 id="csr에서의-gzip-작동-방식">CSR에서의 Gzip 작동 방식</h3>
<ol>
<li>yarn build, npm run build를 통해 빌드 파일을 생성한다.</li>
<li>서버(Nginx)나 빌드 도구가 파일들을 Gzip으로 압축한다.</li>
<li>브라우저가 서버에 앱을 요청하면 서버는 압축된 파일을 전송한다.</li>
<li>브라우저는 받은 파일들을 압축해제하여 사용자에게 보여준다.</li>
</ol>
<p>CSR에서 Gzip은 번들을 압축하여 FCP, TTI, LCP 등 초기 로딩 시간을 단축하는데 큰 도움을 줄 수 있습니다. 특히 node_modules 내부의 수 많은 패키지들이 존재합니다. 이때 Gzip에서 사용하는 L277 알고리즘을 통해 반복되는 패턴을 압축하여 많은 양의 코드를 개선시킬 수 있습니다.</p>
<h2 id="vite에서의-gzip-적용-방법">Vite에서의 Gzip 적용 방법</h2>
<p>Vite에서 Gzip, Brotli과 같은 압축 방식을 적용하기 위해서는 <code>vite-plugin-compression</code> 플러그인을 설치해서 적용해야합니다.</p>
<h3 id="1-설치-방법">1. 설치 방법</h3>
<blockquote>
<p>npm install -D vite-plugin-compression2
or
yarn add -D vite-plugin-compression2</p>
</blockquote>
<p>*<em>vite-plugin-compression은compre 3년 전부터 유지보수가 되지 않고 있기에 vite-plugin-compression2를 사용했습니다 *</em></p>
<h3 id="2-적용-방법">2. 적용 방법</h3>
<pre><code class="language-js">//vite.config.js
import { compression } from &#39;vite-plugin-compression2&#39;;

// https://vitejs.dev/config/
export default defineConfig({
  // 기타 설정
  plugins: [
    react(),
    compression({
      algorithm: &#39;gzip&#39;,
    })
  ],
</code></pre>
<p>vite의 plugin에 compression을 등록한 뒤, 원하는 알고리즘을 선택하면 해당 압축 방식으로 번들을 압축할 수 있습니다.
또한 compression을 여러번 사용하여 압축을 여러 번 진행할 수 있습니다. (일반적으로 Gzip으로 압축한 뒤, Brotli로 한 번 더 압축하는 방식을 많이 사용합니다.)</p>
<pre><code class="language-js">import { compression } from &#39;vite-plugin-compression2&#39;;

// https://vitejs.dev/config/
export default defineConfig({
  // 기타 설정
  plugins: [
    react(),
    compression({
      algorithm: &#39;gzip&#39;, //gzip
    }),
    compression({
      algorithm: &#39;brotliCompress&#39;, //brotli
    })
  ],</code></pre>
<p><strong>Gzip 적용 후</strong></p>
<pre><code class="language-js">//일반 빌드 파일
dist/assets/index-CYbT4RwO.css           9.90 kB │ gzip:  2.75 kB
//Gzip을 적용했을 때
dist/idus_web_playform/assets/index-CYbT4RwO.css.gz        9.67kb / gzip: 2.68kb
//Brotli까지 적용했을 때
dist/idus_web_playform/assets/index-CYbT4RwO.css.br        9.67kb / brotliCompress: 2.29kb</code></pre>
<p>압축을 진행할 수록 파일의 크기가 줄어드는 것을 확인할 수 있습니다. 이를 통해 네트워크 전송 크기를 개선하며 로딩 속도를 빠르게 할 수 있습니다.</p>
<h2 id="웹-사이트에서-확인-방법">웹 사이트에서 확인 방법</h2>
<p><strong>웹 사이트에서 빌드 파일이 압축이 되었는지 확인하려면 네트워크 분석을 통해 알 수 있습니다.</strong>
<img src="https://velog.velcdn.com/images/yjh-1008/post/b91c3fec-fbd5-4e79-8809-45b9278e8c3d/image.png" alt=""></p>
<p>응답 헤더에서 <code>Content-Type: gzip</code>을 확인 했을 때 gzip으로 압축이 되어있는 것을 확인할 수 있습니다.
만약, 압축이 되어있지 않다면 Content-Type 헤더가 없거나 identity 값을 가지고 있습니다.</p>
<h2 id="gzip은-알겠다-근데-brotli는-또-뭔가">Gzip은 알겠다.. 근데 Brotli는 또 뭔가..?</h2>
<p>Brotli는 <code>Google</code>에서 개발한 압축 알고리즘으로 Gzip보다 더 우수한 압축률을 제공합니다.</p>
<h3 id="brotli의-특징">Brotli의 특징</h3>
<ul>
<li>압축 알고리즘: <a href="https://en.wikipedia.org/wiki/LZ77_and_LZ78">LZ77</a> + <a href="https://ko.wikipedia.org/wiki/%ED%97%88%ED%94%84%EB%A8%BC_%EB%B6%80%ED%98%B8%ED%99%94">Huffman 부호화</a> + 사전(dictionary) </li>
</ul>
<table>
<thead>
<tr>
<th>특성</th>
<th>Brotli</th>
<th>Gzip</th>
</tr>
</thead>
<tbody><tr>
<td>출시 시기</td>
<td>2015년 (Google)</td>
<td>1992년 (Jean-loup Gailly, Mark Adler)</td>
</tr>
<tr>
<td>압축 알고리즘</td>
<td>LZ77 + Huffman + 사전 압축</td>
<td>LZ77 + Huffman</td>
</tr>
<tr>
<td>압축률</td>
<td>보통 Gzip보다 높음</td>
<td>Brotli보다 낮음</td>
</tr>
<tr>
<td>속도 (압축)</td>
<td>느림</td>
<td>빠름</td>
</tr>
<tr>
<td>속도 (해제)</td>
<td>빠름</td>
<td>빠름</td>
</tr>
<tr>
<td>파일 확장자</td>
<td>.br</td>
<td>.gz</td>
</tr>
<tr>
<td>주요 사용처</td>
<td>웹 (HTTP 압축, WOFF2 폰트 등)</td>
<td>웹, 파일 압축 등</td>
</tr>
<tr>
<td>사전(dictionary) 지원</td>
<td>예 (웹 최적화에 유리)</td>
<td>없음</td>
</tr>
<tr>
<td>표준 지원</td>
<td>HTTP/2, HTTP/3, 대부분 브라우저 지원</td>
<td>모든 브라우저 지원</td>
</tr>
<tr>
<td>최대 압축 레벨</td>
<td>11</td>
<td>9</td>
</tr>
<tr>
<td>스트리밍 지원</td>
<td>예</td>
<td>예</td>
</tr>
</tbody></table>
<h2 id="brotli이-압축률이-더-높으면-brotli만-사용하면-되는거-아닐까">Brotli이 압축률이 더 높으면 Brotli만 사용하면 되는거 아닐까..?</h2>
<p>빌드 파일을 만들 때 Brotli과 Gzip을 같이 사용하는 이유는 여러가지가 있습니다.</p>
<ol>
<li>브라우저 호환성<ul>
<li>구형 브라우저에서는 Brotli을 지원하지 않을 수 있기 때문에 Gzip을 사용합니다.</li>
</ul>
</li>
<li>HTTP/2.0 이상<ul>
<li>Brotli는 HTTP/2에서 최적화 된 성능을 발휘하도록 설계되었습니다.</li>
</ul>
</li>
<li>속도 균형<ul>
<li>Brotli는 압축률은 높지만 압축 속도는 Gzip이 더 우수합니다.</li>
<li>서버에서 실시간 압축을 하는 경우에는 Gzip이 더 빠를 수 있어 Gzip이 더 실용적일 수 있습니다.</li>
</ul>
</li>
</ol>
<p>종합하자면 브라우저 호환성, 압축 속도 등의 이유로 Gzip을 함께 사용한다고 할 수 있습니다.</p>
<h2 id="정리">정리</h2>
<p>오늘은 Gzip이 어떤건지 Brotli가 어떤건지에 대해 간단하게 알아보았습니다. &quot;배포를 진행하며 최적화를 위해 압축을 사용해야해!&quot; 정도로만 알고 있었지만 각 압축 알고리즘의 장단점에 대해 알아가니 실무에서 적용할 때 더욱 유연하게 사용할 수 있을 것 같습니다.</p>
<h2 id="참고">참고</h2>
<p> <a href="https://en.wikipedia.org/wiki/LZ77_and_LZ78">LZ77 알고리즘</a>
 <a href="https://ko.wikipedia.org/wiki/%ED%97%88%ED%94%84%EB%A8%BC_%EB%B6%80%ED%98%B8%ED%99%94">Huffman 부호화</a> 
 <a href="https://ko.wikipedia.org/wiki/DEFLATE">Deflate 알고리즘</a> </p>
]]></description>
        </item>
    </channel>
</rss>