<?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>Sun, 21 Jun 2026 06:56:34 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[AG-UI 프로토콜에 대해 알아보자]]></title>
            <link>https://velog.io/@yjh-1008/AG-UI-%ED%94%84%EB%A1%9C%ED%86%A0%EC%BD%9C%EC%97%90-%EB%8C%80%ED%95%B4-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@yjh-1008/AG-UI-%ED%94%84%EB%A1%9C%ED%86%A0%EC%BD%9C%EC%97%90-%EB%8C%80%ED%95%B4-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Sun, 21 Jun 2026 06:56:34 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/yjh-1008/post/3321f2ab-740e-4915-93ad-97cce108aeba/image.avif" alt="AG UI"></p>
<h2 id="1-ag-ui란">1. AG-UI란?</h2>
<p>AGUI(Agent User Interaction Protocol)란 AI Agent와 사용자 인터페이스 간의 표준화된 통신 프로토콜입니다. 기존의 요청/응답 방식을 <strong>이벤트 기반의 양방향 통신으로 전환</strong>하여 에이전트와 프론트엔드의 상태를 동기화하는 프로토콜입니다.</p>
<p>표준 HTTP(또는 바이너리 채널) 위로 JSON 이벤트의 단일 시퀀스를 스트리밍합니다. 에이전트 백엔드가 실행 중에 표준 이벤트를 방출하면, 프론트엔드는 그 이벤트 타입에 반응해 UI를 그립니다.</p>
<p><img src="https://velog.velcdn.com/images/yjh-1008/post/7fa18531-bec6-49db-9c00-088166f31154/image.png" alt="AG UI 흐름"></p>
<h2 id="2-ag-ui가-필요한-이유">2. AG-UI가 필요한 이유</h2>
<p>기존의 웹 개발의 상호작용은 단발성이었습니다. 클라이언트가 요청 -&gt; 서버가 데이터를 반환 -&gt; 렌더 -&gt; 상호작용으로 동작했지만 <strong>Agent를 활용한 상호작용은 다르게 동작합니다</strong>.</p>
<ul>
<li>Long-running: 중간 작업을 스트리밍한다.</li>
<li>비결정적: 응답뿐 아니라 UI 제어까지 비결정적으로 발생한다.</li>
<li>컴포지션: 서브 에이전트를 재귀적으로 호출한다.</li>
</ul>
<p>청크(토큰)이 흘러나오고, 도구(Tool)을 호출하고, 사람의 인터렉션을 기다렸다가 다시 흐름을 이어가는 방식은 단발성 REST로는 해결할 수 없었습니다.</p>
<p>AG-UI는 이 사이를 잇는 통신 규약이 되어 <strong>어떤 에이전트든 백엔드든 UI와 통신할 수 있게됩니다.</strong></p>
<p><img src="https://velog.velcdn.com/images/yjh-1008/post/d533c50d-e27e-40cc-a56c-b42d0b85caf4/image.png" alt="https://docs.ag-ui.com/concepts/architecture"></p>
<h2 id="3-구성-요소">3. 구성 요소</h2>
<h3 id="1-컴포넌트">1. 컴포넌트</h3>
<p>AG-UI는 다음과 같은 핵심 컴포넌트로 구성되어 있습니다:</p>
<ol>
<li>Events - 비동기 통신의 기반
run, 텍스트 전송, 툴 호출과 같은 타입 기반의 실행 이벤트. 모든 이벤트는 BaseEvent(type, timestamp, rawEvent)를 상속하고 5개의 카테고리 16개의 타입으로 나뉩니다.</li>
</ol>
<ul>
<li><strong>Lifecycle</strong>: <code>RUN_STARTED</code>, <code>RUN_FINISHED</code>, <code>RUN_ERROR</code>, <code>STEP_STARTED</code>, <code>STEP_FINISHED</code></li>
<li><strong>Text message</strong>: <code>TEXT_MESSAGE_START</code>, <code>TEXT_MESSAGE_CONTENT</code>, <code>TEXT_MESSAGE_END</code></li>
<li><strong>Tool call</strong>: <code>TOOL_CALL_START</code>, <code>TOOL_CALL_ARGS</code>, <code>TOOL_CALL_END</code></li>
<li><strong>State management</strong>: <code>STATE_SNAPSHOT</code>, <code>STATE_DELTA</code>, <code>MESSAGES_SNAPSHOT</code></li>
<li><strong>Special</strong>: <code>RAW</code>, <code>CUSTOM</code></li>
</ul>
<pre><code class="language-js">const response = await fetch(&quot;/chat&quot;, {
  method: &quot;POST&quot;,
  headers: { &quot;Content-Type&quot;: &quot;application/json&quot;, &quot;Accept&quot;: &quot;text/event-stream&quot; },
  body: JSON.stringify(runAgentInput)
});
// data: {&quot;type&quot;:&quot;RUN_STARTED&quot;, ...}
// data: {&quot;type&quot;:&quot;TEXT_MESSAGE_CONTENT&quot;,&quot;delta&quot;:&quot;The&quot;}
// data: {&quot;type&quot;:&quot;TOOL_CALL_START&quot;, ...}
// data: {&quot;type&quot;:&quot;RUN_FINISHED&quot;, ...}</code></pre>
<ol start="2">
<li>Agents - Agent 추상화
Agent는 프로토콜의 기반입니다. <code>AbstractAgent</code> 클래스를 확장하고 <code>Observable&lt;BaseEvent&gt;</code>를 반환하는 run() 메서드를 구현합니다. 이 추상화를 통해 어떤 AI 서비스든 일관된 인터페이스로 통합할 수 있습니다.</li>
</ol>
<pre><code class="language-js">class SimpleAgent extends AbstractAgent {
  run(input: RunAgentInput): RunAgent {
    const { threadId, runId } = input
    return () =&gt; new Observable&lt;BaseEvent&gt;((observer) =&gt; {
      observer.next({ type: EventType.RUN_STARTED, threadId, runId })
      const messageId = Date.now().toString()
      observer.next({ type: EventType.TEXT_MESSAGE_START, messageId, role: &quot;assistant&quot; })
      observer.next({ type: EventType.TEXT_MESSAGE_CONTENT, messageId, delta: &quot;Hello, world!&quot; })
      observer.next({ type: EventType.TEXT_MESSAGE_END, messageId })
      observer.next({ type: EventType.RUN_FINISHED, threadId, runId })
      observer.complete()
    })
  }
}</code></pre>
<ol start="3">
<li><p>Messages - 대화 컨텍스트와 히스토리
대화 상태를 전송하는 단위입니다. 타입은 <code>Developer</code>, <code>System</code>, <code>Assitant</code>, <code>User</code>, <code>Tool</code>, <code>Activity</code>, <code>Reasoning</code>으로 나뉩니다.</p>
</li>
<li><p>State Management - 상태 동기화
에이전트와 UI간의 실시간 동기화 레이어입니다.</p>
</li>
</ol>
<ul>
<li><code>STATE_SNAPSHOT</code>: 특정 시점의 완전한 상태</li>
<li><code>STATE_DELTA</code>: JSON Patch(RFC 6902) 기반 증분 변경</li>
<li><code>MESSAGES_SNAPSHOT</code>: 완전한 대화 히스토리</li>
</ul>
<ol start="5">
<li><p>Tools - 도구/함수 정의
에이전트가 호출할 수 있는 함수를 표준화합니다. 툴 정의는<code>runAgent</code>파라메터로 전달되고, 호출은 <code>TOOL_CALL_START → TOOL_CALL_ARGS → TOOL_CALL_END</code> 시퀀스로 스트리밍됩니다.</p>
</li>
<li><p>Middleware - 미들웨어 레이어</p>
</li>
</ol>
<p>호환성을 보장하는 어댑터 계층입니다.</p>
<ul>
<li>유연한 이벤트 구조: 이벤트가 AG-UI 포맷과 정확히 일치할 필요 없이, 호환되기만 하면 가능합니다. 기존 프레임워크가 가진 네이티브 이벤트를 적응시킬 수 있습니다.</li>
<li>Transport: SSE, webhook, WebSocket등 어떤 전송 방식이든 지원합니다.</li>
</ul>
<h2 id="3-마치며">3. 마치며</h2>
<p>AG-UI는 스트리밍 프로세스에서 UI에 연결하는 표준을 만들기 위한 Agent 전용 프로토콜입니다. 출시된지 얼마 안됐지만 AG-UI를 통한 A2UI가 나오는 등 활발하게 커뮤니케이션이 발생하는 분야라고 생각됩니다. 기존의 LLM 프로젝트를 진행해보신 프론트엔드 개발자분들이라면 스트리밍 통신 규약을 만드는게 까다롭고 복잡하게 느껴지셨을텐데요. AG-UI의 사용을 고려해보시고 더 나은 개발을 해보셨으면 좋겠습니다. 오늘도 글 읽어주셔서 감사합니다!</p>
<p><strong>출처</strong>
<a href="https://docs.ag-ui.com/introduction">AG-UI 공식문서</a>
<a href="https://velog.io/@adoo24/AG-UI-%ED%94%84%EB%A1%9C%ED%86%A0%EC%BD%9C-%ED%8C%8C%ED%97%A4%EC%B9%98%EA%B8%B0-%EC%97%90%EC%9D%B4%EC%A0%84%ED%8A%B8%EC%99%80-UI%EA%B0%80-%EB%8C%80%ED%99%94%ED%95%98%EB%8A%94-%EB%B2%95">AG-UI 프로토콜 파헤치기: 에이전트와 UI가 대화하는 법</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[프론트엔드 면접 질문 정리(CS편)]]></title>
            <link>https://velog.io/@yjh-1008/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EB%A9%B4%EC%A0%91-%EC%A7%88%EB%AC%B8-%EC%A0%95%EB%A6%ACCS%ED%8E%B8</link>
            <guid>https://velog.io/@yjh-1008/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EB%A9%B4%EC%A0%91-%EC%A7%88%EB%AC%B8-%EC%A0%95%EB%A6%ACCS%ED%8E%B8</guid>
            <pubDate>Sat, 06 Jun 2026 08:26:50 GMT</pubDate>
            <description><![CDATA[<ol>
<li><p>주소 창에 <a href="http://naver.com%EC%9D%84">http://naver.com을</a> 입력하면 동작하는 원리에 대해 설명하세요.</p>
<p> 브라우저 <a href="http://naver.com%EC%9D%84">http://naver.com을</a> 입력하면 브라우저는 캐싱된 dns를 검색하고 있다면 ip를 반환하고 없다면 dns에 요청하여 ip 주소를 반환받습니다. 브라우저는 제공받은 ip주소의 서버에게 HTTP 요청을 보내고 서버는 브라우저에게 HTML 파일을 전송하고 브라우저는 렌더링을 진행합니다.</p>
</li>
<li><p>GET 과 POST의 방식의 차이점에 대해 설명하세요.</p>
<p> Get은 정보를 요청하는 메서드이고 POST는 정보를 전송하는 메서드입니다. Get은 주로 URL에 파라메터를 쿼리스트링으로 전달하는 방식이 일반적이고 POST는 Request Body에 담아 데이터를 전송하는게 일반적입니다.</p>
</li>
<li><p>객체지향 프로그래밍이란 무엇인가요?</p>
<p> 객체지향 프로그래밍이란 데이터를 객체로 추상화시켜 객체들 간의 상호작용을 통해 로직을 구성하는 컴퓨터 프로그래밍 패러다임 방법입니다.</p>
</li>
<li><p>DNS에 대해 설명해주세요.</p>
<p> DNS는 Domain Name Service의 약어로 URL을 IP 주소로 변환해주는 역할을 하는 시스템입니다. 일반적으로 IP 주소는 사용자가 외우기 어렵기 때문에 IP 주소를 URL로 변화하기 때문입니다.</p>
</li>
<li><p>프로세스와 스레드의 차이점에 대해 설명해주세요.</p>
<p> 프로세스는 메모리 상에서 실행중인 프로그램이고, 스레드는 프로세스 안에서 실행되는 흐름의 단위를 말합니다.</p>
</li>
<li><p>REST API에 대해 설명해주세요.</p>
<p> REST를 기반으로 만들어진 API 입니다.</p>
<p> <strong>REST는 HTTP를 기반으로 자원에 접근하는 방식을 정해놓은 아키텍처입니다.</strong></p>
<p> REST API는 자원(URL), 행위(HTTP 메서드), 표현(페이로드)로 이루어져 있으며 HTTP 메서드를 사용해야하며 URI가 리소스를 표현하는데 집중해야합니다.</p>
<p> HTTP URL(Uniform Resource Identifier)를 통해 자원(Resource)을 명시하고, HTTP Method(POST, GET, PUT, DELETE 등)을 통해 해당 자원에 대한 CRUD Operation을 적용하는 것을 의미합니다.</p>
</li>
<li><p>URL과 URI의 차이점</p>
<h3 id="uri-식별자-url식별자위치"><strong>URI= 식별자, URL=식별자+위치</strong></h3>
<ul>
<li><p><strong>elancer.co.kr은 URI입니다. 리소스의 이름만 나타내기 때문입니다.</strong></p>
</li>
<li><p><strong>반면, <a href="https://elancer.co.kr%EC%9D%80">https://elancer.co.kr은</a> URL입니다. 이름과 더불어, 어떻게 도달할 수 있는지 위치까지 함께 나타내기 때문이죠. (프로토콜 ‘https’ 포함)</strong></p>
</li>
<li><p><em>URL은 프로토콜 + 이름(또는 번호)*</em></p>
</li>
<li><p><strong>Scheme: 리소스에 접근하는 데 사용할 프로토콜. 웹에서는 http 또는 https를 사용</strong></p>
</li>
<li><p><strong>Host: 접근할 대상(서버)의 호스트 명</strong></p>
</li>
<li><p><strong>Path: 접근할 대상(서버)의 경로에 대한 상세 정보(URN)</strong></p>
</li>
</ul>
</li>
</ol>
<h3 id="면접-질문-출처">면접 질문 출처</h3>
<ul>
<li><a href="https://velog.io/@developer-sora/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EB%A9%B4%EC%A0%91-%EC%A7%88%EB%AC%B8-%EC%A0%95%EB%A6%AC-Part1CS-JSReact#virtual-dom%EC%9D%B4-%EB%AC%B4%EC%97%87%EC%9D%B4%EA%B3%A0-%EC%9E%91%EB%8F%99-%EC%9B%90%EB%A6%AC%EC%97%90-%EB%8C%80%ED%95%B4-%EC%84%A4%EB%AA%85%ED%95%B4%EC%A3%BC%EC%84%B8%EC%9A%94">프론트엔드 면접 질문 정리 Part1(CS, JS,React)</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Next.js의 라우팅은 어떻게 구현되어 있을까?]]></title>
            <link>https://velog.io/@yjh-1008/Next.js%EC%9D%98-%EB%9D%BC%EC%9A%B0%ED%8C%85%EC%9D%80-%EC%96%B4%EB%96%BB%EA%B2%8C-%EA%B5%AC%ED%98%84%EB%90%98%EC%96%B4-%EC%9E%88%EC%9D%84%EA%B9%8C</link>
            <guid>https://velog.io/@yjh-1008/Next.js%EC%9D%98-%EB%9D%BC%EC%9A%B0%ED%8C%85%EC%9D%80-%EC%96%B4%EB%96%BB%EA%B2%8C-%EA%B5%AC%ED%98%84%EB%90%98%EC%96%B4-%EC%9E%88%EC%9D%84%EA%B9%8C</guid>
            <pubDate>Mon, 25 May 2026 14:00:32 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>최근 Next.js의 공식 문서와 인프런 강의를 보면서 공부하던 중 라우터가 어떻게 구현되어 있는지에 대한 내용을 학습했습니다. 사용할때는 단순하게 구현이 되어있겠지라고 생각하며 사용했지만, 내부 로직에 대해 이해를 하고 사용하니 SSR이 어떻게 Code Spliting을 하는지, URL과 파일 디렉토리를 어떻게 연결지어 페이지를 불러오는지에 대해 알 수 있었습니다. 오늘은 그 중에서도 Next.js가 빌드할 때, 라우트를 어떻게 정의하는지, 서버에 요청이 들어왔을 때, 어떻게 매칭하여 페이지를 전송하는지에 대해 정리하겠습니다.</p>
</blockquote>
<h2 id="server-구축">Server 구축</h2>
<pre><code class="language-js">const http = require(&#39;http&#39;);  // Node.js 내장 HTTP 서버 모듈
const fs   = require(&#39;fs&#39;).promises;  // 비동기 파일 시스템 API (fs.readdir 등)
const path = require(&#39;path&#39;);  // 경로 문자열 처리 유틸리티
const { URL } = require(&#39;url&#39;);  // URL 파싱 (pathname, searchParams 분리)
const PORT   = 3000;  // 서버가 수신할 포트 번호
const routes = new Map();  // 라우트 테이블: { &quot;/blog/[slug]&quot; → handler모듈 }

async function buildRoutes(dir, baseRoute = &#39;&#39;) {
  // withFileTypes: true → Dirent 객체 반환 (isDirectory/isFile 메서드 포함)
  const entries = await fs.readdir(dir, { withFileTypes: true });
  for (const entry of entries) {
    const fullPath  = path.resolve(dir, entry.name);
    // URL용 세그먼트 조합: .js 확장자 제거 후 &#39;/&#39;로 연결
    const routePart = [baseRoute, entry.name.replace(/\.js$/, &#39;&#39;)]
                        .filter(Boolean).join(&#39;/&#39;);
    if (entry.isDirectory()) {
      // 라우트 그룹 감지: (marketing) 처럼 괄호로 감싼 폴더는 URL에서 제외
      const isGroup = /^\(.*\)$/.test(entry.name);
      // 그룹이면 baseRoute 유지, 아니면 routePart 추가하며 재귀
      await buildRoutes(fullPath, isGroup ? baseRoute : routePart);
    } else if (entry.isFile() &amp;&amp; entry.name === &#39;page.js&#39;) {
      // page.js → 화면 렌더링용 (GET 전용)
      // &quot;blog/page&quot; → &quot;/blog&quot;  |  루트 &quot;page&quot; → &quot;/&quot;
      const routePath = &#39;/&#39; + routePart.replace(/\/?page$/, &#39;&#39;);
      routes.set(routePath, require(fullPath));  // Map에 등록
    } else if (entry.isFile() &amp;&amp; entry.name === &#39;route.js&#39;) {
      // route.js → API 엔드포인트 (GET/POST/PUT/DELETE 등 메서드명으로 export)
      const routePath = &#39;/&#39; + routePart.replace(/\/?route$/, &#39;&#39;);
      routes.set(routePath, require(fullPath));
    }
  }
}


function matchRoute(pathname) {
  // ① 정확히 일치하는 정적 라우트부터 먼저 확인 (가장 빠른 경로)
  if (routes.has(pathname)) {
    return { handler: routes.get(pathname), params: {} };
  }
  // 요청 URL을 세그먼트 배열로 분리: &quot;/blog/hello&quot; → [&quot;blog&quot;,&quot;hello&quot;]
  const pathParts = pathname.split(&#39;/&#39;).filter(Boolean);
  // ② 등록된 모든 패턴을 순서대로 비교 (동적 라우트 탐색)
  for (const [route, handler] of routes) {
    const routeParts = route.split(&#39;/&#39;).filter(Boolean);
    const params     = {};  // 추출된 동적 파라미터 저장소
    let   matched    = true;
    // [...name] catch-all 세그먼트 위치 탐색
    const catchAllIdx = routeParts.findIndex(p =&gt; p.startsWith(&#39;[...&#39;));
    if (catchAllIdx !== -1) {
      // catch-all 이전 정적 세그먼트들이 먼저 일치해야 함
      for (let i = 0; i &lt; catchAllIdx; i++) {
        if (routeParts[i] !== pathParts[i]) { matched = false; break; }
      }
      if (matched &amp;&amp; pathParts.length &gt;= catchAllIdx) {
        // [...path] → paramName = &quot;path&quot;, 나머지 세그먼트를 &#39;/&#39;로 다시 합침
        const paramName = routeParts[catchAllIdx].replace(/^\[\.\.\.(.+)\]$/, &#39;$1&#39;);
        params[paramName] = pathParts.slice(catchAllIdx).join(&#39;/&#39;);
        return { handler, params };
      }
      continue;  // catch-all 이전 세그먼트 불일치 → 다음 패턴으로
    }
    // 세그먼트 수가 다르면 매칭 불가
    if (routeParts.length !== pathParts.length) continue;
    // 세그먼트 하나씩 비교
    for (let i = 0; i &lt; routeParts.length; i++) {
      const routeSeg = routeParts[i];
      const pathSeg  = pathParts[i];
      if (routeSeg.startsWith(&#39;[&#39;) &amp;&amp; routeSeg.endsWith(&#39;]&#39;)) {
        // [slug] 동적 세그먼트 → 값 캡처 (% 인코딩도 디코드)
        const paramName = routeSeg.slice(1, -1);  // &quot;[slug]&quot; → &quot;slug&quot;
        params[paramName] = decodeURIComponent(pathSeg);
      } else if (routeSeg !== pathSeg) {
        matched = false;  // 정적 세그먼트 불일치 → 루프 탈출
        break;
      }
    }
    if (matched) return { handler, params };  // 매칭 성공
  }
  return null;  // 일치하는 라우트 없음 → 404로 처리
}

const server = http.createServer(async (req, res) =&gt; {
  // URL 파싱: pathname과 쿼리스트링 분리
  // ex) &quot;/blog/hello?page=2&quot; → pathname=&quot;/blog/hello&quot;, search=&quot;?page=2&quot;
  const urlObj   = new URL(req.url, `http://localhost`);
  // 후행 슬래시 제거: &quot;/blog/&quot; → &quot;/blog&quot;  |  루트는 &quot;/&quot; 유지
  const pathname = urlObj.pathname.replace(/\/$/, &#39;&#39;) || &#39;/&#39;;
  const method   = req.method.toUpperCase();  // &quot;get&quot; → &quot;GET&quot; 정규화
  // ── 헬퍼 메서드 주입 ─────────────────────────────────────────────────
  // req.query: URLSearchParams → 일반 객체로 변환
  // ex) &quot;?role=admin&amp;page=2&quot; → { role: &quot;admin&quot;, page: &quot;2&quot; }
  req.query = Object.fromEntries(urlObj.searchParams);
  // req.json(): 요청 바디를 스트림으로 읽어 JSON 파싱 후 반환
  req.json = () =&gt; new Promise((resolve, reject) =&gt; {
    let body = &#39;&#39;;
    req.on(&#39;data&#39;, chunk =&gt; (body += chunk));  // 청크 단위로 수신
    req.on(&#39;end&#39;, () =&gt; {                        // 수신 완료 후 파싱
      try { resolve(JSON.parse(body)); }
      catch (e) { reject(e); }  // JSON 파싱 실패 시 에러 전파
    });
  });
  // res.json(): JSON 응답 전송 (Content-Type 자동 설정)
  res.json = (data, status = 200) =&gt; {
    res.writeHead(status, { &#39;Content-Type&#39;: &#39;application/json&#39; });
    res.end(JSON.stringify(data, null, 2));
  };
  // res.send(): HTML 응답 전송
  res.send = (html, status = 200) =&gt; {
    res.writeHead(status, { &#39;Content-Type&#39;: &#39;text/html; charset=utf-8&#39; });
    res.end(html);
  };
  // ── 라우트 매칭 ──────────────────────────────────────────────────────
  const matched = matchRoute(pathname);
  if (!matched) {
    // 매칭된 라우트 없음 → 404 응답
    return res.json({ error: &#39;Not Found&#39;, path: pathname }, 404);
  }
  const { handler, params } = matched;
  req.params = params;  // { slug: &quot;hello-world&quot; } 형태로 핸들러에서 접근 가능
  // ── 디스패치: 올바른 핸들러 함수 선택 후 호출 ────────────────────────
  try {
    if (typeof handler[method] === &#39;function&#39;) {
      // route.js: exports.GET / exports.POST 등 메서드명으로 export된 경우
      await handler[method](req, res);
    } else if (typeof handler.default === &#39;function&#39;) {
      // page.js: exports.default 로 export된 페이지 핸들러
      if (method !== &#39;GET&#39; &amp;&amp; method !== &#39;HEAD&#39;) {
        // 페이지는 GET/HEAD 외 메서드 허용 안 함
        return res.json({ error: &#39;Method Not Allowed&#39; }, 405);
      }
      await handler.default(req, res);
    } else {
      // 해당 HTTP 메서드가 export되지 않은 경우 → 405 + Allow 헤더
      const allowed = [&#39;GET&#39;,&#39;POST&#39;,&#39;PUT&#39;,&#39;PATCH&#39;,&#39;DELETE&#39;]
        .filter(m =&gt; typeof handler[m] === &#39;function&#39;).join(&#39;, &#39;);
      res.writeHead(405, { &#39;Allow&#39;: allowed });
      res.end(`Method ${method} not allowed`);
    }
  } catch (err) {
    // 핸들러 내부에서 throw된 에러 → 500 응답으로 변환
    console.error(`[에러] ${method} ${pathname}:`, err);
    res.json({ error: &#39;Internal Server Error&#39;, message: err.message }, 500);
  }
  // 접근 로그 출력 (핸들러 실행 이후)
  console.log(`${method} ${pathname} ${res.statusCode ?? 200}`);
});

// pages/ 디렉터리 절대경로 계산 (__dirname = server.js 위치)
const PAGES_DIR = path.join(__dirname, &#39;pages&#39;);

// buildRoutes()는 async → Promise 반환 → .then()으로 완료 시점 처리
buildRoutes(PAGES_DIR).then(() =&gt; {
  // 라우트 스캔이 끝난 후에만 포트를 열어 요청을 받기 시작
  server.listen(PORT, () =&gt; {
    console.log(`✅ http://localhost:${PORT}\n`);
  });
}).catch(err =&gt; {
  // pages/ 폴더 읽기 실패 등 초기화 에러 → 즉시 종료
  console.error(&#39;라우트 빌드 실패:&#39;, err);
  process.exit(1);
});
</code></pre>
<h3 id="서버-생성">서버 생성</h3>
<ol>
<li>http.createServer를 사용해 서버를 생성한다.
 1-1: req의 쿼리스트링을 분리하여 URL Segment를 생성한다.
 1-2: 리턴 값 정의, GET 방식이 아닌 다른 방식으로 올때 에러 핸들링 등 서버 핸들링을 정의한다.
 1-3: matchRoute 메서드를 통해 해당 URL에 매핑되는 페이지가 있는지 검사한다. (없다면 404 리턴)
 1-4: matchRoute 메서드에서 Map을 순회하며 해당 세그먼트에 해당하는 경로가 있는지 확인한다.
 1-5:이때, 동적 세그먼트를 분리하여 params로 변환하고, buildRoutes에서 정의했던 page를 require하는 핸들러를 리턴한다.</li>
</ol>
<h3 id="라우트-구축">라우트 구축</h3>
<ol start="2">
<li><strong>buildRoutes</strong> 메서드를 통해 map 구조에 디렉토리를 순회하며 검사한다.
 2-1: 만약 현재 entry가 디렉터리라면 buildRoutes 메서드를 재귀적으로 탐색한다.
 2-2: 만약 현재 파일이 page.js라면 route에 page를 제외한 경로를 key로 map에 등록한다.
 2-3: 만약 현재 파일이 route.js라면 route를 제외한 경로를 key로 Map에 등록한다.</li>
</ol>
<h2 id="참고">참고</h2>
<p><a href="https://biz.inflearn.com/course/nextjs-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EC%9B%B9%EA%B0%9C%EB%B0%9C-%EC%8B%AC%ED%99%94/dashboard?cid=336349">Next.js 까보기</a></p>
]]></description>
        </item>
        <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>
    </channel>
</rss>