<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>ss-won.log</title>
        <link>https://velog.io/</link>
        <description>Frontend Developer, 올라운더가 되고싶은 잡부 개발자, ISTP, 겉촉속바 인간, 블로그 주제 찾아다니는 사람</description>
        <lastBuildDate>Sun, 08 Mar 2026 08:31:07 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>ss-won.log</title>
            <url>https://velog.velcdn.com/images/ss-won/profile/598a9590-d3f8-4fac-ab70-64ad702ef2ca/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. ss-won.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/ss-won" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[web3 1년ㅊ....아니 2년차 (유식 계산법) 프런트개발자의 지갑 연결 프로토콜 (EIP-6963) 정리]]></title>
            <link>https://velog.io/@ss-won/web3-1%EB%85%84%E3%85%8A....%EC%95%84%EB%8B%88-2%EB%85%84%EC%B0%A8-%EC%9C%A0%EC%8B%9D-%EA%B3%84%EC%82%B0%EB%B2%95-%ED%94%84%EB%9F%B0%ED%8A%B8%EA%B0%9C%EB%B0%9C%EC%9E%90%EC%9D%98-%EC%A7%80%EA%B0%91-%EC%97%B0%EA%B2%B0-%ED%94%84%EB%A1%9C%ED%86%A0%EC%BD%9C-EIP-6963-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@ss-won/web3-1%EB%85%84%E3%85%8A....%EC%95%84%EB%8B%88-2%EB%85%84%EC%B0%A8-%EC%9C%A0%EC%8B%9D-%EA%B3%84%EC%82%B0%EB%B2%95-%ED%94%84%EB%9F%B0%ED%8A%B8%EA%B0%9C%EB%B0%9C%EC%9E%90%EC%9D%98-%EC%A7%80%EA%B0%91-%EC%97%B0%EA%B2%B0-%ED%94%84%EB%A1%9C%ED%86%A0%EC%BD%9C-EIP-6963-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Sun, 08 Mar 2026 08:31:07 GMT</pubDate>
            <description><![CDATA[<h1 id="windowethereum은-왜-사라지고-있는가--eip-1193에서-eip-6963까지">window.ethereum은 왜 사라지고 있는가 — EIP-1193에서 EIP-6963까지</h1>
<p>Web3 프론트엔드를 개발하면서 지갑 연결 프로토콜을 정리한 내용이다. EIP-1193의 한계부터 EIP-6963의 등장, 그리고 최신 동향까지 순서대로 따라가본다.</p>
<hr>
<h2 id="1-windowethereum의-문제">1. window.ethereum의 문제</h2>
<p>브라우저 확장 지갑은 <code>window.ethereum</code>이라는 전역 객체에 자신을 주입(inject)한다. MetaMask가 처음 시작한 관행이 사실상 표준이 되었다.</p>
<p>문제는 <strong>지갑이 여러 개 설치되어 있을 때</strong> 발생한다.</p>
<pre><code>MetaMask 로드 → window.ethereum = metaMaskProvider ✅
Rabby 로드    → window.ethereum = rabbyProvider    💥 MetaMask 덮어씀</code></pre><ul>
<li>마지막에 로드된 지갑만 사용 가능 (경쟁 조건)</li>
<li>사용자가 어떤 지갑을 쓸지 선택할 수 없음</li>
<li>DApp이 특정 지갑의 독자 경로를 하드코딩해야 함 (<code>window.phantom?.ethereum</code> 등)</li>
</ul>
<hr>
<h2 id="2-eip-1193--provider-인터페이스-표준">2. EIP-1193 — Provider 인터페이스 표준</h2>
<p>EIP-1193은 <strong>Provider 객체의 인터페이스</strong>를 정의한 표준이다. &quot;Provider를 어디에 놓아야 하는가&quot;(디스커버리)는 정의하지 않았다.</p>
<pre><code class="language-typescript">// EIP-1193 Provider 인터페이스
interface EIP1193Provider {
  request(args: { method: string; params?: unknown[] }): Promise&lt;unknown&gt;;
  on(event: string, listener: (...args: any[]) =&gt; void): void;
  removeListener(event: string, listener: (...args: any[]) =&gt; void): void;
}</code></pre>
<p>DApp에서의 사용:</p>
<pre><code class="language-typescript">if (window.ethereum) {
  const accounts = await window.ethereum.request({
    method: &quot;eth_requestAccounts&quot;,
  });

  await window.ethereum.request({
    method: &quot;eth_sendTransaction&quot;,
    params: [{ from: accounts[0], to: &quot;0x...&quot;, value: &quot;0x...&quot; }],
  });
}</code></pre>
<table>
<thead>
<tr>
<th>표준</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td><strong>EIP-1193</strong></td>
<td>Provider의 <strong>인터페이스</strong> 정의 (<code>.request()</code>, 이벤트)</td>
</tr>
<tr>
<td><strong>EIP-1102</strong></td>
<td><code>eth_requestAccounts</code> 권한 요청 흐름</td>
</tr>
<tr>
<td><strong>관행</strong></td>
<td><code>window.ethereum</code>에 주입 (공식 표준 아님)</td>
</tr>
</tbody></table>
<p>핵심: EIP-1193은 &quot;Provider가 어떻게 생겨야 하는가&quot;를 정의했지, &quot;어디서 찾아야 하는가&quot;는 빈 자리로 남겨뒀다. 이 빈 자리를 관행이 채웠고, 그 관행의 한계를 EIP-6963이 해결했다.</p>
<hr>
<h2 id="3-eip-6963--이벤트-기반-provider-discovery">3. EIP-6963 — 이벤트 기반 Provider Discovery</h2>
<p>EIP-6963은 <code>window.ethereum</code> 대신 <strong>브라우저 CustomEvent를 이용한 Pub/Sub 패턴</strong>으로 지갑을 발견한다.</p>
<h3 id="핵심-인터페이스">핵심 인터페이스</h3>
<pre><code class="language-typescript">interface EIP6963ProviderInfo {
  uuid: string;        // 고유 식별자
  name: string;        // &quot;MetaMask&quot;, &quot;Pockie&quot; 등
  icon: string;        // data URI 아이콘
  rdns: string;        // 역방향 도메인 (e.g. &quot;io.metamask&quot;)
}

interface EIP6963ProviderDetail {
  info: EIP6963ProviderInfo;
  provider: EIP1193Provider;  // 기존 EIP-1193 그대로
}</code></pre>
<h3 id="동작-흐름">동작 흐름</h3>
<pre><code>DApp: &quot;eip6963:requestProvider&quot; 이벤트 발행
  ↓
설치된 각 지갑: &quot;eip6963:announceProvider&quot; 이벤트로 응답
  ↓
DApp: 모든 응답 수집 → 사용자에게 지갑 목록 표시</code></pre><h3 id="지갑-측-구현">지갑 측 구현</h3>
<pre><code class="language-javascript">// 지갑의 콘텐츠 스크립트가 페이지에 주입하는 코드
const announceEvent = new CustomEvent(&#39;eip6963:announceProvider&#39;, {
  detail: Object.freeze({
    info: {
      uuid: uuid(),
      name: &#39;MyWallet&#39;,
      icon: `data:image/svg+xml,...`,
      rdns: &#39;com.mywallet&#39;,
    },
    provider,
  }),
});

// ① DApp이 요청하면 응답
window.addEventListener(&#39;eip6963:requestProvider&#39;, () =&gt; {
  window.dispatchEvent(announceEvent);
});

// ② 즉시 한 번 announce — 이미 로드된 DApp을 위해
window.dispatchEvent(announceEvent);</code></pre>
<h3 id="dapp-측-구현">DApp 측 구현</h3>
<pre><code class="language-javascript">const wallets = [];

// ① 지갑 응답 수신 리스너 등록
window.addEventListener(&#39;eip6963:announceProvider&#39;, (event) =&gt; {
  wallets.push(event.detail);
});

// ② 지갑 탐색 요청
window.dispatchEvent(new Event(&#39;eip6963:requestProvider&#39;));
// → 이 시점에서 wallets 배열에 모든 지갑이 들어있음 (동기적)</code></pre>
<h3 id="양방향-패턴--타이밍-문제-해결">양방향 패턴 — 타이밍 문제 해결</h3>
<p>양쪽 다 &quot;리스너 등록 → 이벤트 발행&quot; 순서로 구현하기 때문에, <strong>누가 먼저 로드되든</strong> 서로를 발견할 수 있다.</p>
<table>
<thead>
<tr>
<th>DApp이 먼저 로드된 경우</th>
<th>지갑이 먼저 로드된 경우</th>
</tr>
</thead>
<tbody><tr>
<td>DApp의 리스너가 대기 중</td>
<td>지갑의 리스너가 대기 중</td>
</tr>
<tr>
<td>지갑이 주입되며 즉시 announce → DApp 수신</td>
<td>DApp이 requestProvider 발행 → 지갑 응답</td>
</tr>
</tbody></table>
<p>브라우저 <code>CustomEvent</code>는 <strong>동기적</strong>으로 전파된다. <code>dispatchEvent()</code> 호출 시 모든 리스너가 즉시 실행되므로 비동기 대기가 필요 없다.</p>
<h3 id="비교-정리">비교 정리</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th>EIP-1193 (기존)</th>
<th>EIP-6963</th>
</tr>
</thead>
<tbody><tr>
<td>지갑 발견</td>
<td><code>window.ethereum</code> 하나</td>
<td>이벤트로 모든 지갑 수집</td>
</tr>
<tr>
<td>다중 지갑</td>
<td>충돌/덮어쓰기</td>
<td>공존 가능</td>
</tr>
<tr>
<td>사용자 선택</td>
<td>불가</td>
<td>지갑 목록에서 선택</td>
</tr>
<tr>
<td>메타데이터</td>
<td>없음</td>
<td>이름, 아이콘, rdns</td>
</tr>
</tbody></table>
<hr>
<h2 id="4-실무-적용기--b2b-프로젝트에서의-eip-6963">4. 실무 적용기 — B2B 프로젝트에서의 EIP-6963</h2>
<p>최근 고객사별로 지갑 구성이 다른 B2B 프로젝트에 EIP-6963 기반 지갑 연결을 설계했다. wagmi v3을 사용했고, 직접 EIP-6963 이벤트를 다룰 필요 없이 <code>injected()</code> 커넥터가 내부적으로 처리해주었다.</p>
<h3 id="wagmi가-eip-6963을-처리하는-방식">wagmi가 EIP-6963을 처리하는 방식</h3>
<p>wagmi의 <code>createConfig()</code>에는 <code>multiInjectedProviderDiscovery</code>라는 옵션이 있고, <strong>기본값이 <code>true</code></strong>다. 내부적으로 <code>mipd</code> 라이브러리가 <code>eip6963:requestProvider</code> 이벤트를 발행하고, 응답한 지갑들을 자동으로 injected connector로 변환한다.</p>
<pre><code class="language-typescript">import { createConfig } from &#39;wagmi&#39;;
import { injected, coinbaseWallet, walletConnect } from &#39;@wagmi/connectors&#39;;

const config = createConfig({
  chains: supportedChains,
  connectors: [
    injected(),                           // EIP-6963 자동 감지
    coinbaseWallet({ appName: &#39;...&#39; }),
    walletConnect({ projectId: &#39;...&#39; }),
  ],
  // multiInjectedProviderDiscovery: true  ← 기본값
});</code></pre>
<p>이렇게 하면 EIP-6963을 지원하는 지갑(MetaMask, Rabby 등)은 별도 커넥터 설정 없이도 자동으로 발견된다. </p>
<h3 id="파트너별-지갑-동적-구성">파트너별 지갑 동적 구성</h3>
<p>이 프로젝트의 특수한 점은 <strong>고객사마다 지원하는 지갑이 다르다</strong>는 것이었다. 어떤 파트너는 자체 지갑만, 어떤 파트너는 MetaMask + WalletConnect 조합을 원했다. 물론 지금 구성이 EIP-6963 에서 조금더 트렌드가 바뀌면 뜯어고쳐야겠지만, 현재 프로젝트의 목표는 빠른 개발 + 커스텀 영역을 최소화하는 것이므로 EIP-6963을 기본으로 개발 interface를 구축했다. </p>
<p>이를 위해 지갑 메타데이터를 정의하고, 파트너 설정에 따라 UI에 노출할 지갑 목록을 동적으로 필터링하는 구조로 설계했다.</p>
<pre><code class="language-typescript">// 지갑별 메타데이터 정의
interface CustomorConfig {
  walletConfig: {
      ...,
    supportedWallet: {
        name: string;
          rdns: string;
          platforms: desktop | mobile [];
    }
  }
}
// 파트너 설정에서 지원 지갑 목록을 받아옴
// → UI에는 해당 지갑만 렌더링
// ex: 
supportedWallet: [
      { name: &quot;MetaMask&quot;, rdns: &quot;io.metamask&quot;, platforms: [&quot;desktop&quot;, &quot;mobile&quot;]
]
// 이게 고객사 지갑, rdns 식별자로 정확히 지갑을 인식해서 연결하는 방어막을 만들어 놓고, 플랫폼별로 연결 방식이 다른 경우가 있어서 플랫폼별 지원 지갑을 필터링하기 위해 platforms interface도 추가했다.</code></pre>
<p>wagmi의 커넥터와 파트너 설정 사이의 매핑 유틸리티를 만들어 연결했다.</p>
<h3 id="배운-점">배운 점</h3>
<ul>
<li><strong>wagmi의 <code>injected()</code> 하나로 EIP-6963 지원 지갑은 모두 커버된다.</strong> 개별 지갑마다 커넥터를 추가할 필요가 없다.</li>
<li><strong>SSR 환경(Next.js)에서는 <code>cookieStorage</code>를 사용해야</strong> 한다. 기본 localStorage는 서버에서 접근할 수 없어 하이드레이션 불일치가 발생한다.</li>
<li><strong>지갑 연결/해제 시 사용자 스코프 쿼리 캐시를 초기화</strong>하는 것이 중요하다. 다른 계정의 데이터가 남아있으면 안 된다.</li>
</ul>
<hr>
<h2 id="5-최신-동향--니모닉을-넘어서">5. 최신 동향 — 니모닉을 넘어서</h2>
<p>지갑 연결 프로토콜과 별개로, <strong>계정 관리 방식</strong> 자체도 빠르게 변하고 있다. 전체적인 방향은 &quot;시드 구문(니모닉) 노출 최소화 + 진입장벽 낮추기&quot;다.</p>
<h3 id="기술별-요약">기술별 요약</h3>
<table>
<thead>
<tr>
<th>기술</th>
<th>핵심</th>
<th>상태</th>
</tr>
</thead>
<tbody><tr>
<td><strong>ERC-4337</strong> (Account Abstraction)</td>
<td>EOA 대신 스마트 컨트랙트 지갑. 가스 대납, 배치 트랜잭션 가능</td>
<td>성장 중 (~12%)</td>
</tr>
<tr>
<td><strong>EIP-7702</strong></td>
<td>EOA가 트랜잭션 단위로 스마트 계정처럼 동작. 기존 주소 유지</td>
<td>초기 (~1%)</td>
</tr>
<tr>
<td><strong>MPC</strong> (Multi-Party Computation)</td>
<td>키를 여러 조각으로 분할. 단일 장애점 제거</td>
<td>상용화 (~8%)</td>
</tr>
<tr>
<td><strong>Passkey / WebAuthn</strong></td>
<td>생체인증(Face ID, 지문)으로 서명. 시드 구문 불필요</td>
<td>초기 (~4%)</td>
</tr>
<tr>
<td><strong>소셜 로그인</strong></td>
<td>Google/Apple OAuth → 내부 키 생성</td>
<td>상용화 (~6%)</td>
</tr>
</tbody></table>
<h3 id="eip-5792--배치-트랜잭션">EIP-5792 — 배치 트랜잭션</h3>
<pre><code class="language-typescript">// approve + swap을 한 번에 요청
await provider.request({
  method: &quot;wallet_sendCalls&quot;,
  params: [{ calls: [approveTx, swapTx] }]
});</code></pre>
<h3 id="eip-7702--eoa-하이브리드">EIP-7702 — EOA 하이브리드</h3>
<p>2025년 Pectra 업그레이드에 포함. 기존 EOA 주소를 유지하면서 스마트 계정 기능을 사용할 수 있어, AA 전환의 가교 역할이 기대된다.</p>
<h3 id="채택-분포-추정">채택 분포 (추정)</h3>
<pre><code>지갑 연결 프로토콜:
EIP-1193     ████████████████████████████████████████  ~95%
EIP-6963     ██████████████████████████████           ~75%
WalletConnect ████████████████████████████             ~65%

키 관리 방식:
니모닉/EOA    ████████████████████████████████████████  ~87%
AA (4337)    ██████                                    ~12%
MPC          ████                                      ~8%
Passkey      ██                                        ~4%</code></pre><p>니모닉이 여전히 압도적이지만, EIP-7702를 통해 기존 EOA 사용자가 마이그레이션 비용 없이 AA로 전환할 수 있게 되면서 이 비율은 빠르게 변할 수 있다. (클로드 코드가 예측 및 추정한 것으로 실제와 다를 수 있다.)</p>
<p>그래 솔직히 니모닉 관리 너무 귀찮긴 해...아날로그 시대처럼 금고에 한땀한땀 새겨서 보관할 수도 없는 노릇이고.</p>
<p>탈중앙이라는 표현이 무색해지는 방향으로 가는 것 같기도 하다만, 보안적으로 안정적인 플랫폼에 어차피 맡기는건 똑같으므로 차라리 인증을 여러번 거치는 한이 있어도 이 방향으로 기술이 발전하는게 오히려 좋은 현상같다.</p>
<p>일단 우리 나라에서 비트코인 매입하는 사람들 거의 다 그냥 거래소 지갑에 모으고 있을듯...? (은 저도입니다 너무 귀여운 양이라서 얼추 매입하면 하드월렛으로 옮길 예정.).</p>
<hr>
<h2 id="6-마무리">6. 마무리</h2>
<p>정리하면:</p>
<ul>
<li><strong>EIP-1193</strong>은 Provider 인터페이스 표준. <code>window.ethereum</code>은 표준이 아니라 관행이었다.</li>
<li><strong>EIP-6963</strong>은 이 관행의 한계(다중 지갑 충돌)를 Pub/Sub 패턴으로 해결했다. wagmi 등 주요 라이브러리가 기본 지원하면서 사실상 새 표준이 되었다.</li>
<li><strong>실무에서는</strong> wagmi의 <code>injected()</code> 커넥터가 EIP-6963을 내부 처리해주므로, 직접 이벤트를 다룰 일은 거의 없다.</li>
<li><strong>계정 관리 쪽</strong>은 니모닉에서 AA/Passkey/MPC로 전환 중이며, EIP-7702가 이 전환을 가속할 핵심 기술이다.</li>
</ul>
<p>Web3 지갑의 목표는 결국 하나다 — <strong>사용자가 &quot;지갑&quot;이라는 개념을 의식하지 않아도 되는 것.</strong></p>
<p>찐 막 코멘트: </p>
<h4 id="블로그-글-관련">블로그 글 관련</h4>
<p>오늘은 클로드 코드랑 티키타카 하면서 한 10분만에 게시물을 완성했다. 중간중간 넣고 싶은 내용을 수정한 것 빼고는 95%정도가 클로드 지분이다. 물론 초안을 완성하기 전까지는 클로드를 나의 가장 친절한 선생님으로 모셔서(?) 같이 공부하긴 했다. <code>superpower</code> 라는 플러그인을 이용했다. 약 두달 전 claude official plugin에 추가되어 지원되는 중인데. 설계/기획 시에 너무 유용하다. 기획/설계 -&gt; tdd -&gt; 개발 -&gt; 리뷰 까지 한번에 해준다. 하나하나 만들기 귀찮은 나를 위한 최적의 skill이다...다들 맛보시길 (근데 oh-my-cluadecode가 더 좋다는 분들도 많이 봤다. 나중에 이것도 써봐야지 부지런한 개발자 분들 너무 대단하시다...)</p>
<h4 id="지갑-연결--나의-역할-관련">지갑 연결 / 나의 역할 관련</h4>
<p>요즘은 Web3 업계에 프론트엔드 개발자의 필요성과 나의 방향성에 대해 많은 고민중에 있다. 벌써 여기 입문한지 1년 6개월차가 되었다.(유식계산법으로 2년차! 헿, 요즘은 연차가 무슨 의미가 있나 싶긴함요 <del>저는 그냥 말하는 감자인데요.</del>) 지갑 연결만 해도 <code>window.{???}</code> provider를 직접 제어해서 지갑을 구분하는 일은 이제 없고 wagmi 라이브러리만 사용하면 사실상 한큐에 다 끝나기 때문이다. 물론 이제 그밖에 최적화/BFF 관련 업무도 하고있긴 하지만 이마저도 AI가 나보다 잘하쟈나..? (흑ㅠ)</p>
<p>더 나아가 앞으로 AI 시대에서 과연 UI라는게 블록체인 업계에 필요할까?는 매우 의문이다. x402 프로토콜도 그렇고 블록체인 업계 인프라 자원과 분산형 네트워크가 안정된 서비스를 제공하고, 보안/AI 제약 인프라 구축, 스테이블 코인 대중화, RWA 경제 시장 활성화 등등 현시점 경제 시장이 나아가는 길만 대충 흝어봐도 이 시장에 UI라는게 특별히 필요할까 라는 생각이 든다. (그렇다고 모두가 멸망이란 이야기는 아닙니다. 프로토콜을 만드는 사람도 필요하고 프로토콜을 웹 엔진에 잘 녹여내는 것 또한 웹 개발자의 역할 중 하나라고 생각해요.)</p>
<p>정답은 없지만, AX/UX라는 시로운 지점을 최적화하는 엔지니어로서 직업을 좀 더 연명해야하지 않을까 싶다. 이상 여러가지로 심란한 연협 앞둔 개발자의 주저리 겸 스터디 기록을 마친다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[React의 State는 대체 어디에서 관리되고 있는가에 대하여...]]></title>
            <link>https://velog.io/@ss-won/React%EC%9D%98-State%EB%8A%94-%EB%8C%80%EC%B2%B4-%EC%96%B4%EB%94%94%EC%97%90%EC%84%9C-%EA%B4%80%EB%A6%AC%EB%90%98%EA%B3%A0-%EC%9E%88%EB%8A%94%EA%B0%80%EC%97%90-%EB%8C%80%ED%95%98%EC%97%AC</link>
            <guid>https://velog.io/@ss-won/React%EC%9D%98-State%EB%8A%94-%EB%8C%80%EC%B2%B4-%EC%96%B4%EB%94%94%EC%97%90%EC%84%9C-%EA%B4%80%EB%A6%AC%EB%90%98%EA%B3%A0-%EC%9E%88%EB%8A%94%EA%B0%80%EC%97%90-%EB%8C%80%ED%95%98%EC%97%AC</guid>
            <pubDate>Mon, 02 Mar 2026 08:26:21 GMT</pubDate>
            <description><![CDATA[<p>확실히 포스트 트래픽은 AI쪽과 새로운 기술 써본 후기 이런쪽이 높기는 한데, 솔직히 아직까지 뭔가 포스팅 할만한 성과나 workflow는 없어서 오늘도 역시 주제에 대한 고민을 한시간 정도 했다.</p>
<p>요즘 AI 덕분에 공부가 쉬워졌기 때문에 이를 활용해서, 예전 면접에서 질문 받았었는데 제대로 답변을 못한거 같았는데 그때 당시에도 제대로 복기를 안했어서 공부할 겸 오늘은 Codex와 함께 React 톺아보기를 해보았다.</p>
<h3 id="면접-회상">면접 회상</h3>
<blockquote>
<p>면접 질문 : React의 함수형 컴포넌트는 함수입니다. 또한 useState의 hook도 함수입니다. 그러면 해당 선언/사용하는 해당 state는 대체 어디에 저장되는 걸까요? 함수는 매번 호출 당시에 내부 값이 바뀔텐데 어떻게 기존 state를 유지하게 되는걸까요?</p>
</blockquote>
<p>(대략 이랬던 것으로 기억한다. 클로저 개념에 대해 이야기 하다가 갑자기 나온 이야기 같기도하고 1년 반 정도가 지나서 희미해진 기억을 되돌아봤다. 현재 회사에서 나온 질문은 아니었고 최종까지 갔던 회사였다.)</p>
<blockquote>
<p>당시 답변: 제가 정확히 코드를 뜯어보거나 분석해보진 않아서 정확하지 않아 추측해보자면, 결국은 useState는 React의 엔진 객체의 내부 메소드 일거고, 전역 외부 store가 존재하고 hook을 사용할때의 고유 값을 통해 key:value로 저장되고 있을 것 같습니다.</p>
</blockquote>
<p>몇 점짜리 대답일거 같은가? 개인적으로는 한 40점 정도(ㅋㅋ)</p>
<ol>
<li>일단 자신감이 매우 없게 대답했다. (틀릴 수도 있지 뭘의 마인드 장착 필요)</li>
<li>꼬리질문을 통한 고유 값 설정 기준 및 로직 추측에는 대답하지 못했다.</li>
</ol>
<p>사실, 저 엔진을 뜯어보거나 관심이 있지 않을한은 저 구조까지 알고있는 사람은 많지 않을 것이고. 저 회사는 나중에 들어보니 리액트 코드 리딩 스터디를 하고 계시다고 한다. 질문의 요지는 아마도 아래와 같지 않을까. 대답이 갱장히 미흡했지만 기술면접은 감사하게 붙여주셨던것으로 봐서는 그냥 추론해내서 대답하려고 애쓰는 모습을 좋게봐주셨던 것 같다.</p>
<p>(1) useState는 어떤 원리로 동작하는 것인지 <strong>의문</strong>을 가져본적이 있는가? 
(2) JavaScript가 돌아가는 원리를 기반으로 React는 어떻게 구현해냈을지 <strong>추측</strong>해낼 수 있는가?
(3) Class Component는 내부 method와 variable을 통해 변수를 제어할 수 있다 쳐도 Functional Component는 대체 어떻게 이 내용을 변환해서 사용할 수 있게 되었는지 <strong>설계 관점의 시야</strong>를 가졌는가?</p>
<h3 id="ai-분석편">AI 분석편</h3>
<p>AI한테 질문을 똑같이 던져보니 아래와 같이 대답했다. (Codex-5.3)</p>
<p>함수형 컴포넌트의 <strong>state는 컴포넌트 함수 안에 저장되지 않습니다.</strong>
React가 내부적으로 관리하는 <strong>Fiber 노드의 Hook 저장소에 저장</strong>됩니다.</p>
<p>  핵심 흐름은 다음과 같습니다.</p>
<ol>
<li>컴포넌트가 렌더될 때 React가 컴포넌트 함수를 다시 호출합니다.</li>
<li>useState를 만나면 React는 “현재 렌더 중인 Fiber의 <strong>n번째 Hook 슬롯</strong>”을 읽습니다.</li>
<li>setState를 호출하면 값을 즉시 바꾸는 게 아니라, 그 <strong>Hook의 업데이트 큐에 action을 넣습니다</strong>.</li>
<li><strong>다음 렌더에서 큐를 적용해 새 state를 계산하고, 다시 같은 Hook 슬롯에 저장</strong>합니다.</li>
</ol>
<p>그래서 함수는 매번 새로 실행돼도 state가 유지됩니다.
유지의 주체가 함수 로컬 변수가 아니라 Fiber에 연결된 Hook 상태이기 때문입니다.</p>
<h4 id="그래서-fiber가-뭔데요-뭘-가지고-있는데요">그래서 Fiber가 뭔데요.. 뭘 가지고 있는데요?</h4>
<blockquote>
<p>Fiber는 컴포넌트의 실행 결과와 상태, 그리고 다음 렌더 작업 정보를 담는 React의 작업 단위입니다.</p>
</blockquote>
<blockquote>
<p>React는 Fiber 트리를 순회하면서 어떤 컴포넌트를 다시 렌더할지, 어떤 state 업데이트를 처리할지, 어떤 DOM 변경을 커밋할지 결정합니다.</p>
</blockquote>
<blockquote>
<p>📌 Fiber는 다음과 같은 값을 담고 있습니다.</p>
</blockquote>
<ol>
<li>현재 state 값 (memoizedState)</li>
<li>상태 업데이트 큐 (queue)</li>
<li>다음 Hook를 가리키는 링크 (Hook 체인 연결용)</li>
</ol>
<p><img src="https://velog.velcdn.com/images/ss-won/post/53e1742f-fd8a-4ddb-9ace-1638bb3596ec/image.png" alt=""></p>
<p>리액트 rules-of-hooks에 
&quot;React relies on the <strong>order in which hooks are called</strong> ...&quot;라고 언급하며 조건문, 반복문, 비동기 실행문과 함께 hook 사용하는 것을 지양하라고 말하고 있습니다. 이 또한, Fiber가 컴포넌트 단위에 생성되고 해당 컴포넌트에서 선언된 hook들을 선언 순서별로 slot을 만들어 저장하기 때문입니다.</p>
<h4 id="fiber가-근데-어떤시점에-만들어지는거죠">Fiber가 근데 어떤시점에 만들어지는거죠..?</h4>
<blockquote>
<p>Fiber는 컴포넌트를 정의할 때가 아니라, 렌더 트리에 실제로 올라갈 때 생성됩니다.
정확히는:</p>
</blockquote>
<ol>
<li>createRoot(...).render(<App />) 또는 hydrateRoot(..., <App />) 시작</li>
<li>React가 루트부터 엘리먼트 트리를 해석</li>
<li>각 엘리먼트(컴포넌트 인스턴스/DOM 노드)에 대응하는 Fiber를 생성(또는 재사용)</li>
<li>이후 업데이트 때는 기존 current Fiber를 바탕으로 workInProgress Fiber를 만들어 작업 후 교체</li>
</ol>
<blockquote>
<p>📌 즉, 최초 마운트 때 트리 전반의 Fiber가 생기고, 이후에는 업데이트마다 새로 전부 만드는 게 아니라 재사용/복제(alternate)하면서 갱신합니다.</p>
</blockquote>
<h4 id="setstatestate--1-setstatev--v1은-왜-가끔보면-결과값이-다를까요">setState(state + 1), setState((v) =&gt; v+1)은 왜 가끔보면 결과값이 다를까요?</h4>
<p>const [state, setState] = useState(initialValue); 이렇게 선언하고 특정 이벤트 핸들러 함수를 선언한다고 치면 () =&gt; {setState(v+1)} 해당 함수는 클로저로 상위 컴포넌트의 state를 참조하게 됩니다. <code>setState(state + 1)</code> 일반적인 경우는 크게 문제가 되지 않습니다.</p>
<p>그런데 만약에, 같은 스코프에 여러번 setState가 다음과 같이 호출된다면 예상치 못한 결과를 초래할 수 있습니다. <strong>setState에 의한 state 변경은 즉시 적용이 아닙니다. 따라서 순차적인 호출시 state+1은 클로저로 참조한 기존 state값이 적용될 것이기 때문에 의도한 결과가 산출되지 않습니다.</strong></p>
<p>그러나 set 함수 내부 인자를 함수로 건네주게 되면 실행 방식이 다릅니다.
함수형 방식은 결국 Fiber가 건네주는 현재 상태의 정확한 state 값을 전달 받아 실행됩니다. 그래서, 같은 실행 scope안에서 여러번 state update가 발생하는데 정확한 업데이트를 의도할때는 함수형 업데이트 방식을 사용해야합니다.</p>
<h3 id="마무리">마무리</h3>
<p>AI한테 꼬리질문 하다보면, 기존에 비해 확실히 학습할 수 있는 시간이 효율적이게 되었다. 참 좋은 세상이다... 앞으로 이런 특정 프레임워크를 학습할 필요 자체가 없는게 아닌가? 싶을때도 있는데. </p>
<p>AI 제어를 잘 하려면, 최적화 작업에 AI를 쓰려면, 디버깅 포인트를 정확히 요청하려면 = 결국은 AI를 잘 쓰려면, 기본 구조와 설계 방식을 공부하는게 좋다고 생각한다.</p>
<p>실제로 이제 현업에서 직접 코드를 작성하는 일은 거의 없지만. 
요구사항에 대한 AI 설계를 1회 정도는 검수해야 할때도 있기도 하지만 AI 모델 자체를 효율적으로 이용하기 위해서는 어떻게 작업과 책임 단위를 쪼개고 할당할지 등의 엔지니어링 요소가 반드시 필요해진다.</p>
<p>따라서 React라는 거대 프레임워크 설계와 동작원리에 대해 익히는 것은 새로운 프로젝트와 문제 해결에 꽤나 도움이 될 것이라고 본다. 예상치 못한 곳에서 해결 아이디어를 찾을 수 있기 때문이다. 잘 짜여진 구조를 학습 or 따라하는 것만큼 빠른 시도는 없다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Pencil vs Figma 고찰: code to design, design to code의 세상]]></title>
            <link>https://velog.io/@ss-won/Pencil-vs-Figma-%EA%B3%A0%EC%B0%B0-code-to-design-design-to-code%EC%9D%98-%EC%84%B8%EC%83%81</link>
            <guid>https://velog.io/@ss-won/Pencil-vs-Figma-%EA%B3%A0%EC%B0%B0-code-to-design-design-to-code%EC%9D%98-%EC%84%B8%EC%83%81</guid>
            <pubDate>Sat, 21 Feb 2026 06:40:12 GMT</pubDate>
            <description><![CDATA[<p><a href="https://www.pencil.dev/">https://www.pencil.dev/</a>
<img src="https://velog.velcdn.com/images/ss-won/post/4453da3a-06f9-4fef-ad09-e1ea0a78bd58/image.png" alt=""></p>
<p>회사에서 팀장님이 추천하신 <a href="https://www.pencil.dev/">Pencil</a> 을 최근에 사용해봤다. Figma의 AI 퀄리티는(Figma Make도 그렇고 MCP도 그렇고)생각보다 만족스럽지 않았는데 Pencil은 이 문제점을 본인들만의 Context Engineering 기술로 보완해낸듯 보인다. </p>
<p>Figma가 사실상 업계의 표준이 되었고 디자이너들의 의존성이 매우 높은편인데, 
앞으로 더 develop되서 디자인 편의성까지 보완된다면 Figma는 애매한 포지션으로 남을 수도 있을 것 같다. 일단 너무 비대해지고 무거워진 Figma보다 속도가 빠른게 속시원(?)하다. 실제 회사 디자이너분들은 아직까지는 Pencil에서 Figma에서만큼의 디자인을 만들어내기에는 조금 부족해보여요라는 평가를 주셨다.</p>
<p>개인적인 생각으로는, </p>
<ul>
<li>Design to Code / Code to Design 측면에서는 지금은 Pencil이 더 우세한 듯 보인다.</li>
<li>디자인에 전문적이지 않은 사람들에게 프로토타입을 짜기 좋아 보인다. </li>
</ul>
<p>Google의 stitch나 Lovable이나 여러가지가 있다고 들었고 맛보기로 몇 번 써보긴 했는데 개인적으로 가장 AX/UX적으로 경험이 좋은것은 Pencil이었다. 현재 무료로 풀려있고 주요 채팅과 design &lt;-&gt; code 기능은 Claude Code나 Codex 등 구독하고 있는 서비스가 있다면 바로 사용 가능하다.</p>
<p>chat을 쓸때마다 디자인 프레임을 마치 스캔(?)하듯이 동작하는게 꽤나 신기하다. 진짜 사람이 일하는 것처럼 동작한다. <img src="https://velog.velcdn.com/images/ss-won/post/39372c26-e82c-4b80-afbe-94156d6f3039/image.gif" alt=""></p>
<p>Figma Frame을 copy&amp;paste해서 .pen에 옮길 수 있다. copy paste 속도가 너무 빨라서 엄청 놀랐다. 물론 복잡도가 높은 화면은 100% 퀄리티로 가져오진 않고 약간의 조정이 필요하다.</p>
<h4 id="figma-mcp에게-느끼는-아쉬움">Figma MCP에게 느끼는 아쉬움?</h4>
<p>Figma MCP 등장 이후 Design to Code 퀄리티 및 마크업 속도가 비약적으로 빨라졌고, 덕분에 퍼블리싱 하는 노동 강도가 많이 줄었다.</p>
<p>하지만 최근 어그로성 글과는 다르게(모든 개발자 및 디자이너 멸망설) 실무에서 쓰다보면 Figma MCP에 대한 아쉬움이 오히려 더 크다. 이유는 다음과 같다.</p>
<ol>
<li>MCP Context는 디자이너 설계에 영향을 많이 받는다.</li>
</ol>
<p>보통 디자이너 분들은 동일한 화면에 대한 추가 Interactive 요소(Modal, Alert, Snackbar)나 UX를 추가할때 같은 디자인 프레임을 Ctrl+V 해서 쓰시는 경우가 많다. </p>
<p>이때, 기존 원본 화면에 layout/style 속성(variable을 지정하지 않았거나 소위말해 하드코딩처럼 하드디자인(?)을 곁들인)이나 Frame 명칭등에 크게 신경쓰지 않고 디자인을 했다면 Figma MCP에서 전달하는 Context가 설계에 영향을 받기 때문에 당연히 context의 퀄리티가 좋지 않다.</p>
<blockquote>
<p>이 문제는 근데 Pencil도 마찬가지다. Pencil에서 초기 디자인 초안을 Chat을 통해 만들면 되서 이 점에서도 Pencil이 사용성 측면에서 한수 위라고 생각한다.</p>
</blockquote>
<ol start="2">
<li>복잡도가 있는 화면의 경우 Context가 방대한데, Chunking해서 받을 수 없다.</li>
</ol>
<p>MCP 정보는 너무 무거울수록 세션 Context를 많이 잡아먹게 되서, 실질적인 효용성이 많이 떨어진다. 대시보드 처럼 복잡도가 조금만 높아져도 할루시네이션 발생 확률이 높고 구현 퀄리티 질이 많이 떨어진다.</p>
<p>현재 공식 Figma MCP 상에서는 context를 끊어서 받을 수도 없고 context 최적화도 좀 아쉬운 느낌이다.    Claude Code는 Response context 제한이 있기 때문에 만약 Figma에서 내려주는 응답이 너무 길면 모든 내용을 받는게 아니라 잘라서 받게 된다. (이 부분은 Anthropic 업데이트가 워낙 빨라서, 더 개선되었을 수도 있는데, 결국 Context를 제공하는 MCP 측이 context engineering 개선을 해주는게 맞는 방향이라 생각한다)</p>
<ol start="3">
<li>Variable를 한번에 제대로 반영하기 어렵다.</li>
</ol>
<p>현재 Variable의 경우 MCP에 tool이 존재하긴 하지만 선택/요청한 frame 한정으로 variables를 가져오게되며, <code>mode</code> 정보가 포함되어 있지 않다.</p>
<p>Figma API를 mcp 서버로 wrapping해서 variables를 가져오려고 해봤는데 안타깝게도 Enterprise 전용에서만 Variable API를 지원해서 포기했다. (우리 회사는 Organizations 플랜만 지원...ㅎ)</p>
<ol start="4">
<li><code>이미지</code> export나 반영이 어렵다.</li>
</ol>
<p>curl 도구 또는 Figma API를 통해 가져오라고 사전 context rule에 정해놓으면 그나마 나은데, Figma MCP만을 사용해서 퍼블리싱을 시켜보면 정신 못차리는 경우가 대부분 이미지 쪽이다. 한번에 재사용성이 높은 이미지를 저장해두고 작업하고 싶은데 역시 이 부분도 좀 아쉽다.</p>
<ol start="5">
<li>Code Connect는 굉장한 기능인 것처럼 홍보하지만, 실질적인 효과는 미미하다.</li>
</ol>
<p>Figma MCP 도구를 보면 Code Connect를 통해 재사용성 컴포넌트를 효율적으로 코딩할 수 있다는 식으로 안내하는데, 이는 컴포넌트 재사용성이 높은 기업 및 디자인 한정이다. Design System을 구축하더라도 디자인할때 custom design이 하나라도 들어가면 무용지물이며 오히려 관리 포인트가 더 늘어서 나는 개인적으로 불편했다.</p>
<ol start="6">
<li>반응형 레이아웃, absolute position 관련 context가 매우 아쉽다.</li>
</ol>
<p>이건 Figma만의 문제라기 보다는 정보의 모호함에서 오는 문제이긴 하다. 어떻게 Interaction을 json 형태의 정보 구조로 표현할 것인지, 이를 AI Model이 어떻게 제대로 판단하게 할 건지가 중요할 것 같다. </p>
<p>결국 다 적고보니 느끼는건데 Context Engineering이 중요하고, 어떻게 디자인 구조를 AI가 잘 이해할 수 있는 구조로 만들 수 있을까가 현재 Figma 뿐만 아니라 모든 디자인 업계의 AI 도입과 관련해 대두된 문제라고 생각한다.</p>
<h4 id="마무리">마무리</h4>
<p>원래 Figma에는 Design to Code만 지원했고 Code to Design은 없었는데 Anthropic과 파트너십을 체결한 후 Code to Design Tool이 추가되었다.
<a href="https://www.linkedin.com/posts/claude_you-can-now-push-what-youre-building-in-activity-7429913281445793793-VoWp?utm_source=share&amp;utm_medium=member_ios&amp;rcm=ACoAAC2C6GcBcJg6sbZXzFCexN90ngTbUJKPPYY">https://www.linkedin.com/posts/claude_you-can-now-push-what-youre-building-in-activity-7429913281445793793-VoWp?utm_source=share&amp;utm_medium=member_ios&amp;rcm=ACoAAC2C6GcBcJg6sbZXzFCexN90ngTbUJKPPYY</a></p>
<p>두서없이 적다보니 어떻게 마무리를 지어야할지 모르겠네.. </p>
<p>어쨌든 클라리언트쪽 작업을 많이 하다보니 디자인을 코드화 하는 과정에 대한 AI쪽 트렌드를 쫓아가려고 많이 노력중이다. 작년부터는 퍼블리싱 자체를 AI Agent에게 모두 일임하도록 하는 과제를 수행중이다보니 기존 Figma에 느꼈던 내용을 개인적인 감상으로 적어보았다. 수명을 다하는 그날까지 나를 포함한 업계 사람들 화이팅...(이상한 결론)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[AI와 함께하는 블록체인 Explorer 최적화 후기 (I Love Claude Code)]]></title>
            <link>https://velog.io/@ss-won/AI%EC%99%80-%ED%95%A8%EA%BB%98%ED%95%98%EB%8A%94-%EB%B8%94%EB%A1%9D%EC%B2%B4%EC%9D%B8-Explorer-%EC%B5%9C%EC%A0%81%ED%99%94-%ED%9B%84%EA%B8%B0-I-Love-Claude-Code</link>
            <guid>https://velog.io/@ss-won/AI%EC%99%80-%ED%95%A8%EA%BB%98%ED%95%98%EB%8A%94-%EB%B8%94%EB%A1%9D%EC%B2%B4%EC%9D%B8-Explorer-%EC%B5%9C%EC%A0%81%ED%99%94-%ED%9B%84%EA%B8%B0-I-Love-Claude-Code</guid>
            <pubDate>Wed, 18 Feb 2026 08:20:19 GMT</pubDate>
            <description><![CDATA[<p>한 1년만의 포스팅이다.
게으름 + 블로그 흥미 떨어짐 + 무기력함 + 이직 후 모르는 도메인 흡수하느라 정신 없음의 결과물.</p>
<p>최근이라고 쓰려고 했는데 이직한지도 1년 반이라는 시간이 지났다. 2026년 1월에 작업했던 내용에 대한 후기와 최근 작업 플로우 변경에 대해 기록하려고 간만에 벨로그를 켰다.(클로드한테 나중에 이것도 긁어다가 쓰게하려고 기록해봄, 모든것은 AI를 위한 목적으로 바뀌어버린 일상)</p>
<p>2025년 하반기에 담당하고 있는 블록체인 거래내역/지갑/컨트랙트 정보 표시 (Explorer라고들 표현함) 및 데이터 분석 사이트 대규모 마이그레이션을 진행한 후, 2026년에는 이어서 최적화 작업을 진행했다.</p>
<p>대규모 마이그레이션은 codex + cursor와 함께했고, 이 내용은 추후 적을만한 내용이 있으면 적어보고자 한다. 근데 이때는 토큰 부족으로 좀 비효율적으로 진행한 부분이 많아서 고생한 부분중에 공유할 만한 부분이 있다면 다시 돌아오겠다. (이러고 안올듯)</p>
<h4 id="일단-왜-대규모-마이그레이션과-최적화-작업을-하게되었나-">일단 왜 대규모 마이그레이션과 최적화 작업을 하게되었나 ?</h4>
<ul>
<li>blockscout 오픈소스 기반이라, 사실 커스텀 한 부분 빼고는 크게 기능적으로 건드릴만한 프로젝트는 아니다. 그리고 나는 처음부터 해당 오픈소스를 가지고 커스텀에 기여한 일원이 아니고 입사 이후 처음 전담했기 때문에 건드리기가 좀 애매했다.</li>
<li>하지만 주요 버전들 특히 node engine 관련 배포환경이 너무 legacy가 많았고 node 버전이 이제 정식 업데이트가 종료되어가는 시점이었기 때문에 <strong>더이상 미룰 수가 없었다</strong></li>
<li>그렇게 마이그레이션을 엄청난 삽질을 통해 완료 한 후에. <strong>페이지 스모킹 테스트를 진행하면서 특정 페이지들의 렌더링 속도가 지나치게 느리다는 인상을 지울 수 없었다.</strong> blocks 데이터 페이지, tokens 데이터 페이지, transaction 데이터 페이지 등 table 기반 페이지가 그러했다.</li>
<li>혹시 마이그레이션 이슈인가 싶어서 기존 버전에서도 Lighthouse를 돌려봤는데, 원래부터 느렸던 이슈였다... 그리고 유독 chrome에서만 발생하고 있었다.</li>
<li>Network Tab을 통해 API response 도달 대비 렌더링을 비교해봐도 지나치게 느렸다.</li>
</ul>
<p>초기에 엄청나게 느린 페이지 모습 및 성능
<img src="https://velog.velcdn.com/images/ss-won/post/a5b33347-e135-4306-891d-6793fbc4f04b/image.png" alt=""></p>
<h4 id="무엇이-원인이었나">무엇이 원인이었나?</h4>
<ul>
<li>lighthouse 보고서를 보면, LCP/TBT가 특히 문제인 것이 보이는데 각 수치의 의미는 다음과 같다.<pre><code class="language-md">LCP → &quot;사용자가 의미 있는 화면을 보기까지 얼마나 걸리는가&quot;
     (what: 메인 테이블이 보이는 시점 / why: 번들, 초기 렌더링 수, 렌더 블로킹)
</code></pre>
</li>
</ul>
<p>TBT → &quot;화면이 보인 후 실제로 인터랙션할 수 있기까지 얼마나 막혀 있는가&quot;
       (what: Long Task 누적 / why: 동기 연산, 대량 리렌더링, 무거운 컴포넌트 마운트)</p>
<pre><code>- 해당 페이지는 
  - Websocket을 통해 실시간 데이터를 가져옴
  - Table view, layout=auto로 인한 초기 렌더링 연산 증가
  - 실시간 블록 업데이트에 따라, 신규 block row가 계속 추가되는 구조(물론 제한은 한 페이지당 75개의 블록을 넘을 수 없고 이후에는 업데이트가 되지 않고 새로고침을 유도하는 UX긴 함)
  - Table Row 내부에 실시간 데이터 반영(업데이트 시기 연산), gas used 연산 및 표기, eoa/aa 등의 주소 표기 잘림에 의한 tooltip 및 동적 화면 계산 로직 등 무거운 연산이 많이 포함됨.
- 시도한 것
  - 가상화 적용 ❌
    - 의도: 현재 페이지 뷰에 노출되어있는 리스트만 렌더링해서 초기 렌더링 비용을 줄여보기 위함이었다.
    - 과정&amp;결과: row 하나당 렌더링 비용이 너무 높아서, 전체적인 렌더 비용을 줄였지만 스크롤 할때마다 white space가 보이는 시간이 너무 길었다. 사실 row 자체의 비용을 줄이면 됐겠지만, 굉장히 파일이 많고 복잡한 프로젝트인데다가 UI 컴포넌트 표기 자체를 간소화해야하는 trade-off를 굳이(?) 감당하고 싶지 않아서 가상화 도입 자체를 철회했다.
  - table view 뜯어고치기 ⚠️
    - 의도: table view 자체가 연산이 많이 들어가기 때문에 이 문제 자체를 없애려는 시도.
    - 과정&amp;결과: 최적화를 위해서는 grid/flex view를 table 인 것처럼 구성하는게 이득이라고 한다. 근데 이미 table로 구성되어있는 view를 고쳤다가 side effect 후폭풍 비용을 감당하는게 두려웠기 때문에 **layout=auto로 되어있는 부분을 fixed로 고치고 col 너비를 일정 수준으로 정해두어서 연산을 줄이도록 했다.** 이 부분은 그래서 근본적인 해결이라기 보다는 최적화 수치를 상승시키는데 기여했다 정도의 성과가 있었다. 미미하게 200ms 정도 LCP를 줄여주었던 것으로 기억.
  - 모바일/데스크탑 뷰 동시 렌더링 제거 ❌
    - 의도: 모바일/데스크탑 뷰를 모두 렌더링 대상으로 두고 클라이언트 단에서 조건부 렌더링으로 hidden 처리 하고 있던 거라서 초기 렌더링 비용이 상당했다. 모바일은 테이블 뷰가 아니라 카드뷰라서 레이아웃 자체가 아예 다르다. 그래서 이 부분을 제거하면 최적화에 효과적일 것이라 판단함.
    - 과정&amp;결과: ssr 단에서 기본을 desktop이라 두고 렌더링을 했더니 모바일 화면 초기 렌더링에 skeleton이 사라지는 점 + 깜빡거리는 현상이 심한 현상이 생겼다. 각 뷰포트 버전의 컴포넌트 렌더링 연산도가 높아서 깜빡거리는 현상은 css로 아무리 조절해도 생겨버렸다. 본질적으로 컴포넌트 연산도를 줄여야만 했는데 현실적으로 불가능해 보여서 다시 롤백엔딩을 맞이했다. ~~(샤갈)~~
  - PR(Progressive Rendering) 도입 ✅
    - 의도: 가상화 롤백하고 생각해봤는데, 결국 핵심은 초기 렌더링에 있기 때문에 초기에만 렌더링되는 row를 줄이면 될 것이라고 판단했다.
    - 과정&amp;결과: 이미 50개-최대 75페이지(첫 페이지에서만 최신 block row 업데이트가 동작한다) pagination과 제한은 되어있는 상태였는데 실질적으로 화면에 최대 보여지는 row는 10개 남짓이기 때문에 초기 렌더 개수를 15개로 줄이면 되겠는데 싶어서 시도해봤다. 사실상 가상화 시스템의 아이디어를 적절히 차용한 것이다. 결과는 효과적이었다. white-space 현상은 없애면서 렌더링 속도를 높일 수 있었다.
  - Websocket response 한 번들로 적용하기
    - 의도: 웹소켓 통신은 서버의 응답조건이나 네트워크 영향이 커서 한번에 갑자기 여러개의 blocks event를 받아버리면 여러번 업데이트 로직이 동작하며 렌더링이 버벅일 것이라고 판단했다. 
    - 과정&amp;결과: 그래서 프론트엔드의 정석적인 문제인 input tag debounce 처리 처럼, 특정 시간 동안 들어온 websocket 응답을 한번의 렌더링으로만 처리하도록 일종의 batch size를 지정했다.(이 표현이 맞나..?) 그리고 websocket 채널의 큰 최적화 효과는 모르겠지만 확실히 갑자기 네트워크 환경이 안좋아졌을때 화면 freeze가 되던 현상이(특히 chrome에서) 사라졌다. 정량적인 수치 분석까지는 못했지만 아마 해당 작업이 영향을 주었을 것이라 생각(희망)한다.
  - 기타 번들 사이즈 줄이기
    - 이거는 Claude Code 선생님 주력 작업이라서(사실 다른 부분도 아이디어만 내가 제공했지, 작업은 다 Claude Code 선생님이 해주셨다.) 적당히 task 작성시켜서 진행했다.
    - lodash-es로 번들 사이즈 줄이고, next.js custom module build로 필요한/자주 사용되는 모듈 단위만 묶어서 번들 사이즈 줄여주는 등..오류 없이 넘 잘해줘서 편했다.

결과적으로 45점에서 91점으로 페이지 인생 역전 시키기에 성공했다. 함께해준 Claude Code에 이 영광을...생산성이 말이 안된다. 이 작업 아마 예전같았으면 한달은 잡고 있었을듯. 테스트까지 2주가 안걸린것 같다. 실질적인 작업은 1주일 이내였다. 내가 직업을 잃을 수도 있지만(ㅠ) 아직까지는 너무 좋다. 눈 건강에 확실히 기여해준다. I LOVE CLAUDE CODE.
![](https://velog.velcdn.com/images/ss-won/post/c0709a23-eb82-40cf-8d5f-1becf11cb651/image.png)</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[Next.js 14 App router handler로 만든 proxy x-forwarded-port header is 'undefined' 에러 해결하기]]></title>
            <link>https://velog.io/@ss-won/Next14-App-router-handler%EB%A1%9C-%EB%A7%8C%EB%93%A0-proxy-x-forwarded-port-header-is-undefined-%EC%97%90%EB%9F%AC-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@ss-won/Next14-App-router-handler%EB%A1%9C-%EB%A7%8C%EB%93%A0-proxy-x-forwarded-port-header-is-undefined-%EC%97%90%EB%9F%AC-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0</guid>
            <pubDate>Fri, 29 Mar 2024 07:20:21 GMT</pubDate>
            <description><![CDATA[<p><a href="https://velog.io/@ss-won/Next.js-API-Route%EB%A1%9C-GraphSQL-Proxy-Server-%EB%A7%8C%EB%93%A4%EA%B3%A0%EA%B5%B0%EB%B6%84%ED%88%AC%EA%B8%B0">지나간 포스팅</a> 중 Next.js로 Proxy Route를 만든 적이 있었는데, 해당 코드의 후속 에러 해결 내용에 대해 정리해보고자 한다.</p>
<h4 id="tldr">TL;DR</h4>
<ul>
<li>x-forwarded header는 proxy나 middleware에 의해 만들어진 http 요청에서 클라이언트의 정보를 담아 전달하는 header로 해당 정보를 통해 서버에서 로드밸런싱, 보안적 조치(방화벽), 로깅 및 분석 등에 활용한다.</li>
<li>Next.js app/page router를 vercel에 배포할 경우 <code>x-forwarded-port</code> 설정이 사라진다. (Deploy to Vercel, and they will be undefined for the pages route, and null for the app route.)</li>
<li>http-proxy를 사용해서 해당 에러가 발생한 경우에는 <code>xfwd: true</code> 옵션을 통해 proxy에 x-forwarded-port를 설정해 해결 할 수 있다.</li>
<li>버그가 해결되지 전까지 아래 설정을 통해 target 서버와 관련된 port를 지정해놓으면 해당 에러는 발생하지 않을 것이라고 한다.<pre><code class="language-js">req.headers[&quot;x-forwarded-port&quot;] ||= &quot;80&quot;</code></pre>
</li>
</ul>
<h4 id="문제의-발견">문제의 발견</h4>
<p>일단 Vercel에 배포하고 나서부터 proxy 오류가 시작되어서, Vercel project log를 봤는데 proxy x-forwarded-port header is &#39;undefined&#39;라는 로깅을 볼 수 있었다. (컨텐츠용으로 캡쳐 하나 땄어야 했는데, Vercel 요금제 상 Preview 로깅이 최대 1일치만 저장되는데 저 오류 해결하고나서 정신이 없었기 때문에 사진은 없다..!)</p>
<h4 id="그래서-대체-x-forwarded-xxx-header는-무엇인가">그래서 대체 X-Forwarded-xxx header는 무엇인가?</h4>
<p>&quot;X-Forwarded&quot; 헤더는 HTTP 요청이나 응답을 전달하는 데 사용되는 표준화된 HTTP 헤더 중 하나로 본래 클라이언트에서 서버로 직접 요청을 보낼 때, 클라이언트의 정보를 해당 헤더에 담아 서버에 보낸다.  이 헤더는 클라이언트와 서버 간의 프록시나 로드 밸런서와 같은 중간 프록시 서버를 통해 요청이 전달되었음을 나타낸다.</p>
<p>보안 및 로깅과 같은 다양한 목적(프록시, 방화벽, 로드밸런싱)으로 사용될 수 있으며, 클라이언트 및 서버 사이에 있는 네트워크 환경에 따라 다양한 형태로 활용될 수 있다. (by ChatGPT 😊)</p>
<p>X-Forwarded-For : client IP address
X-Forwarded-Host : original client host/domain
X-Forwarded-Proto : client request protocol(HTTP, HTTPS)
X-Forwarded-Port : client request port number </p>
<p>보통은 해당 헤더가 없어도 클라이언트 단에서 직접적으로 서버에 호출을 할 때, 해당 정보들이 HTTP header metadata로 전달이 되는데, 이제 중간에 middleware 서버나 proxy 서버를 두고 운용하는 경우 client가 직접적으로 server에 요청하는 형태가 아니므로 client 정보를 server에 넘겨주기 위해 해당 header를 사용하는 것이다.</p>
<h4 id="그래서-대체-문제는-어떻게-해결하냐면">그래서 대체 문제는 어떻게 해결하냐면</h4>
<p><a href="https://github.com/vercel/next.js/issues/61133">https://github.com/vercel/next.js/issues/61133</a>
next build를 통해 서버를 실행했을 경우 해당 헤더가 사라지지 않지만, 실제 Vercel에 배포를 하면 해당 헤더 값이 undefined 또는 null이 된다는 것이었다. 어떤 파트가 문제인지는 모르겠지만 x-forwarded 관련 헤더가 설정이 기본적으로 되지 않기 때문에 해당 헤더를 사용하려는 로직이 있으면 설정을 해주어야 한다는 것이다.
<img src="https://velog.velcdn.com/images/ss-won/post/266e0efb-4cf4-4c1d-be90-e5b758c5ca56/image.png" alt=""></p>
<p>다음과 같이 임시적으로 req.header에 해당 헤더를 추가하거나, http-proxy를 사용하고 있다면 wfwd 옵션을 true로 주면 된다고 한다.
<img src="https://velog.velcdn.com/images/ss-won/post/8feebff3-3a91-4df8-9b07-61e898d7e8f5/image.png" alt=""></p>
<p>아마도 next.js 14.2 버전이 올라올때까지는 유지해야 할 듯 싶다. next.js 관련 commit을 보면 해당 에러가 애초에 나오지 않도록 작업이 되어있긴 하다.
<a href="https://github.com/vercel/next.js/commit/fb1190425cbf90bee027f8c366f7b267706b100e">https://github.com/vercel/next.js/commit/fb1190425cbf90bee027f8c366f7b267706b100e</a>
<img src="https://velog.velcdn.com/images/ss-won/post/145d3c9a-5a88-4a8f-8e43-2417b4d0e9d4/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[AWS EC2 user data + docker compose로 서버 start시마다 앱 자동배포 해보기]]></title>
            <link>https://velog.io/@ss-won/AWS-EC2-user-data-docker-compose%EB%A1%9C-%EC%84%9C%EB%B2%84-start%EC%8B%9C%EB%A7%88%EB%8B%A4-%EC%95%B1-%EC%9E%90%EB%8F%99%EB%B0%B0%ED%8F%AC-%ED%95%B4%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@ss-won/AWS-EC2-user-data-docker-compose%EB%A1%9C-%EC%84%9C%EB%B2%84-start%EC%8B%9C%EB%A7%88%EB%8B%A4-%EC%95%B1-%EC%9E%90%EB%8F%99%EB%B0%B0%ED%8F%AC-%ED%95%B4%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Sat, 23 Mar 2024 05:56:37 GMT</pubDate>
            <description><![CDATA[<p>요즘 AWS DVA-C02 공인 시험을 치르기 위해서 AWS 공부를 하는 동시에 이제 실질적인 경험을 쌓기 위해 틈나는대로 바로 활용가능한 내용을 적용해보는 중이다.</p>
<p>그중에서 EC2 user data(사용자 데이터)를 통해 EC2가 부팅될때마다 특정 스크립트가 실행하는 기능을 활용해 dev용으로 사용하고 있는 API를 자동으로 배포해보도록 하겠다.</p>
<h4 id="tldr">TL;DR</h4>
<ul>
<li>EC2 사용자 데이터는 EC2를 생성할 때부터 만들 수 있다.</li>
<li>EC2 인스턴스의 사용자 스크립트 수정은 인스턴스가 중지된 상태에서 가능하다.</li>
<li>EC2 사용자 데이터에 작성된 스크립트는 root 권한으로 실행된다.</li>
<li>사용자 데이터와 docker compose를 활용해 매번 EC2 접속할때마다 배포해주는 번거로움을 줄일 수 있고/해당 설정을 재활용 할 수 있기 때문에 초기 Install 할 앱들과 설정등을 설정해두면 유용하다.<ul>
<li>사용자 데이터에 docker, docker-compose, 배포할 내용등을 사전에 다운로드 받고 설정할 수 있게 해두면, 나머지 자동실행은 docker compose에서 알아서 해준다!</li>
</ul>
</li>
</ul>
<p>일단, 먼저 기존 EC2에 docker로 띄워놓은 dev 서버를 확인해보면 다음과 같이 3개의 container가 실행된다.
 <img src="https://velog.velcdn.com/images/ss-won/post/7aef420d-4d57-4fef-b4cc-bff5b5a0bfb3/image.png" alt=""></p>
<p>나는 사전에 docker-compose.prod.yaml 파일을 작성해두었기 때문에 스크립트를 다소 간편?하게 다음과 같이 /home/ubuntu 위치에 start-script.sh를 작성해주었다. 사실 이 부분을 그냥 사용자 데이터에 그대로 넣어도 된다. 다만 cd 파트는 /home/ubuntu/{docker compose 파일이 위치한 경로}로 바꾸어주면 된다.
<img src="https://velog.velcdn.com/images/ss-won/post/bf680d17-2a49-42f8-ad93-04effd79ae9f/image.png" alt=""></p>
<p>그리고 나서 인스턴스를 중지하고
<img src="https://velog.velcdn.com/images/ss-won/post/6406d97d-0cb6-4348-b556-4959065fc811/image.png" alt=""></p>
<p>사용자 데이터 편집을 눌러</p>
<ul>
<li><img src="https://velog.velcdn.com/images/ss-won/post/f8f6c7e9-3a1b-4881-9b6c-1a41efd7617f/image.png" alt=""></li>
<li>사용자 데이터에 방금 만들었던 shell script 파일을 실행할 수 있도록 편집해주었다. 
<img src="https://velog.velcdn.com/images/ss-won/post/cefc8f0c-cdbb-4395-937b-01d327c01dcc/image.png" alt=""></li>
</ul>
<p>그런데 3개의 컨테이너가 실행되어야 하는데 1개만 시작되고 있었다.(미처 캡쳐하진 못했습니다..) 뭘까? 싶어서 일단 로그를 보기로 헀다. </p>
<p><code>docker compose log containerID</code> 로 실행되지 않은 나머지 두 개의 컨테이너의 로그를 살펴봤는데 놀랍게도 오류가 없었다. 그런데 왜 exit이 되는것인지 궁금해서 시작되었던 1개의 container 설정과 다른 container 들의 설정을 비교해봤는데 <code>restart: always</code> 속성이 눈에 띄었다. </p>
<p>그리고 나머지 두 컨테이너에도 docker daemon이 시작될때마다 container가 실행될 수 있게 옵션을 주었더니! 의도한대로 잘 배포가 된 것을 확인할 수 있었다!
<img src="https://velog.velcdn.com/images/ss-won/post/31bc9f87-974d-4d37-9f13-6441636ad083/image.png" alt=""></p>
<p>그래서 왜 그런것인가에 대해 생각해봤는데, 생각해보면 당연하다. 컴퓨터를 종료하면 모든 실행되고있는 앱들이 종료될 것이고 여기에는 docker daemon도 포함된다. </p>
<p>사실 그래서 내가 작성한 스크립트는 사실 크게 영향이 없었을게(<del>으악!</del>) docker compose up -d 자체는 실행이 되었겠지만(sudo systemctl enable docker 영향) -&gt; 도커 데몬이 실질적으로 실행되기 전이라 컨테이너가 유지 되지 않고 종료되었을 것이다. (부팅 로그 분석을 해보고 싶었는데 결국 해당 내용을 못찾았다...그래서 이 부분은 뇌피셜 및 내 생각이다.)</p>
<p>docker를 설치할때부터 아마 daemon이 자동 실행되게끔 설정을 해준거 같은데 ec2가 부팅될때마다 docker가 실행되었기 때문에 기존에 종료되었던 restart 설정이 always인 container가 자동으로 다시 시작된것이다.</p>
<p>그러니까 사실 사용자 데이터에는 docker install/daemon을 실행해주는 코드/system start up시 사용 가능하게 해주는 코드가 필요한 것이고 -&gt; 이후 배포 코드 및 docker-compose.yaml file을 다운로드 및 실행 해주는 코드를 작성하는 것이 바람직하다. -&gt; 다시 말해 <strong>초기 설정을 하는 부분만 유의미할 것 같다는 것이 나의 결론이다.</strong></p>
<p>실질적으로 자동 컨테이너 실행을 가능하게 해주는 것은 docker compose이며  docker-compose.yml에 restart:&quot;always&quot; 옵션을 주고, docker compose up -d 를 한번만 돌려도 알아서 docker가 실행될때마다 컨테이너를 자동으로 실행해준다.
<img src="https://velog.velcdn.com/images/ss-won/post/8955c406-1057-4bbd-9873-1e3b5327ab2c/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[vite + @origin/vite-plugin-federation를 이용한 module-federation 도입기 - turbo monorepo 공용 패키지 share하기]]></title>
            <link>https://velog.io/@ss-won/vite-originvite-plugin-federation%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-module-federation-%EB%8F%84%EC%9E%85%EA%B8%B0-turbo-monorepo-%EA%B3%B5%EC%9A%A9-%ED%8C%A8%ED%82%A4%EC%A7%80-share%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@ss-won/vite-originvite-plugin-federation%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-module-federation-%EB%8F%84%EC%9E%85%EA%B8%B0-turbo-monorepo-%EA%B3%B5%EC%9A%A9-%ED%8C%A8%ED%82%A4%EC%A7%80-share%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 20 Mar 2024 00:54:22 GMT</pubDate>
            <description><![CDATA[<h4 id="tldr">TL;DR</h4>
<ul>
<li>공용 패키지는 패키지 경로 또는 package version(workspace:*)을 명시해주어야 사용할 수 있다.<pre><code class="language-js">import pkg from &quot;./package.json&quot;;
...
shared: [
  &#39;react&#39;,
  &#39;react-dom&#39;,
  &#39;zustand&#39;,
  &#39;react-error-boundary&#39;,
  &#39;@tanstack/react-query&#39;,
  { &#39;@sm/ui&#39;: { version: pkg.dependencies[&#39;@sm/ui&#39;] } },
  { &#39;@sm/util&#39;: { version: pkg.dependencies[&#39;@sm/util&#39;] } },
// or
  {
    &#39;@sm/ui&#39;: {
      packagePath: &#39;../../packages/ui/&#39;,
     },
    &#39;@sm/util&#39;: {
      packagePath: &#39;../../packages/util/&#39;,
    },
  },
]</code></pre>
</li>
</ul>
<h4 id="문제-상황">문제 상황</h4>
<p>module federation 작업중에 생긴 문제들이나 구현 방법등을 기록하는 시리즈를 써보고자 한다.(<del>나는야 콘텐츠에 미친 사람..</del>)</p>
<ul>
<li>저번주에 첫 세팅을 진행하면서 겪은 문제중 하나인데 shared라는 설정에서 발생했다.
<img src="https://velog.velcdn.com/images/ss-won/post/914d253a-bcc6-4210-ab39-3fa6d7716935/image.png" alt=""></li>
<li>보통 module federation 설정을 할때 공용 패키지(shared)를 설정할 수 있는데 쉽게 말해 host단에 있는 패키지를 재사용하는 것으로, remote에서 build할때 해당 패키지를 포함하지 않아 빌드 용량면에서나 재사용성 측면에서 장점이 있다.</li>
<li>문제가 발생한 지점은 정확히 turbo monorepo에서 사용하는 packages/* 내부의 공용 패키지와 관련된 것이었다. <ul>
<li>@origin/vite-plugin-federation은 특별한 설정이 없어도 host app과 remote app에 공유하고자 하는 패키지들은 배열 형태로 명시만 해줘도 일반적인 패키지(=monorepo에서 workspace 형태로 사용하는 패키지말고 npm에서 install해서 사용하는 것들)들은 얼추 잘 동작한다. (물론 패키지 버전 문제가 크게 없다는 가정하에..!)</li>
</ul>
</li>
</ul>
<h4 id="문제-해결">문제 해결</h4>
<ul>
<li>사실 오류에서 해결 방법을 명시해주고 있긴하다. <ul>
<li><code>Add version to description file, or manually specify version in shared config.</code></li>
<li><strong>버전 파일을 인식할 수 없으니 추가적으로 config에 명시해달라는 것이다.</strong><pre><code class="language-js">// host
const federationConfig = env =&gt; ({
name: &#39;container&#39;,
remotes: {
qm: {
  external: `${env?.[&#39;WORKSPACE_QM_URL&#39;]}/qm/v2/assets/remoteEntry.js`,
  from: &#39;vite&#39;,
},
dc: {
  external: `${env?.[&#39;WORKSPACE_DC_URL&#39;]}/dc/v2/assets/remoteEntry.js`,
  from: &#39;vite&#39;,
}
},
shared: [
&#39;react&#39;,
&#39;react-dom&#39;,
&#39;react-error-boundary&#39;,
&#39;zustand&#39;,
&#39;@tanstack/react-query&#39;,
{ &#39;@sm/ui&#39;: { version: pkg.dependencies[&#39;@sm/ui&#39;] } },
{ &#39;@sm/util&#39;: { version: pkg.dependencies[&#39;@sm/util&#39;] } },
],
});
</code></pre>
</li>
</ul>
</li>
</ul>
<p>// remote
export const mfConfig = {
  name: &#39;dc&#39;,
  filename: &#39;remoteEntry.js&#39;,
  // Modules to expose
  exposes: {
    &#39;./app&#39;: &#39;./src/app.tsx&#39;,
    &#39;./useStore&#39;: &#39;./src/stores/global.ts&#39;,
  },
  shared: [&#39;react&#39;, &#39;react-dom&#39;, &#39;zustand&#39;, &#39;@sm/ui&#39;, &#39;@sm/util&#39;],
};</p>
<pre><code>- 사실 처음에는 오류 메세지 끝까지 안읽고 혼자 고민하다가, npm workspace라는 개념을 도입하기 전에  배포되지 않은 local package를 가져올 때 dependencies에 해당 패키지의 상대경로를 적어줬던 것이 생각나서 package의 경로 설정을 해줬더니 이 또한 잘 동작했다. 엄밀히 따지면 workspace:*도 결국엔 경로를 제공해주는 것일테니(뇌피셜..) 같은 설정이라 봐도 되지 않을까...?
  - 다만 상대 경로로 표시하게 될 경우 첫 번째 케이스보다 안좋은 점은 **경로가 바뀌었을때 계속 config file의 경로도 수정**을 해줘야 한다는 점이 있다.
```js
// host
const federationConfig = env =&gt; ({
  name: &#39;container&#39;,
  remotes: {
    qm: {
      external: `${env?.[&#39;WORKSPACE_QM_URL&#39;]}/qm/v2/assets/remoteEntry.js`,
      from: &#39;vite&#39;,
    },
    dc: {
      external: `${env?.[&#39;WORKSPACE_DC_URL&#39;]}/dc/v2/assets/remoteEntry.js`,
      from: &#39;vite&#39;,
    }
  },
  shared: [
    &#39;react&#39;,
    &#39;react-dom&#39;,
    &#39;react-error-boundary&#39;,
    &#39;zustand&#39;,
    &#39;@tanstack/react-query&#39;,
    {
      &#39;@sm/ui&#39;: {
        packagePath: &#39;../../packages/ui/&#39;,
       },
      &#39;@sm/util&#39;: {
        packagePath: &#39;../../packages/util/&#39;,
      },
    },
  ],
});

// remote
export const mfConfig = {
  name: &#39;dc&#39;,
  filename: &#39;remoteEntry.js&#39;,
  // Modules to expose
  exposes: {
    &#39;./app&#39;: &#39;./src/app.tsx&#39;,
    &#39;./useStore&#39;: &#39;./src/stores/global.ts&#39;,
  },
  shared: [&#39;react&#39;, &#39;react-dom&#39;, &#39;zustand&#39;, &#39;@sm/ui&#39;, &#39;@sm/util&#39;],
};</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[Playwright Github Actions 도입기 1탄 - PR comment action 추가해보기]]></title>
            <link>https://velog.io/@ss-won/Playwright-Github-Actions-%EB%8F%84%EC%9E%85%EA%B8%B0-1%ED%83%84-PR-comment-action-%EC%B6%94%EA%B0%80%ED%95%B4%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@ss-won/Playwright-Github-Actions-%EB%8F%84%EC%9E%85%EA%B8%B0-1%ED%83%84-PR-comment-action-%EC%B6%94%EA%B0%80%ED%95%B4%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Sat, 16 Mar 2024 15:51:37 GMT</pubDate>
            <description><![CDATA[<p>오늘도 어김없이 들고온 무수한 실패. force-pushed가 약간 거슬릴 수 있지만...너무 자잘한 지점의 changes로 인한 반복적 commit 방지를 위한 것이었을 뿐이다.(혼자 찔려서 적어봄)</p>
<p><img src="https://velog.velcdn.com/images/ss-won/post/ebf2d0a3-a573-493b-9dda-8c9e28758e94/image.png" alt=""></p>
<p>그래서 오늘의 주제는 playwright e2e test CI 적용을 위한 github actions 적용중에 발생한 실패 해결 이야기이다.</p>
<h4 id="일단-원래-코드는">일단 원래 코드는…</h4>
<p>Vercel의 Guide 문서 <a href="https://vercel.com/guides/how-can-i-run-end-to-end-tests-after-my-vercel-preview-deployment">how-can-i-run-end-to-end-tests-after-my-vrcel-preview-deployment</a>를 참조했고, preview가 생성될때마다 github action이 trigger 되도록 deployment_status라는 동작을 on에 적용시켜주고 <code>github.event.deployment_status.environment_url</code>로 Vercel Preview URL을 가져와 다른 Secrets 요소와 함께 env 파일로 생성해주었다.</p>
<p>그 이후엔 그냥 playwright cli 동작을 수행했고 특별한 지점은 없다. report를 <a href="https://docs.github.com/ko/actions/using-workflows/storing-workflow-data-as-artifacts">github artifact</a>에 업로드 하여 30일 동안 유지하고 해당 report는 playwright init을 했을때 기본적으로 설정된 html로 유지시켰다.</p>
<p>물론! 아래 코드는 잘 동작한다. </p>
<pre><code class="language-yaml">name: Playwright Tests
on: deployment_status
jobs:
  # access preview url
  e2e-test:
    if: github.event_name == &#39;deployment_status&#39; &amp;&amp; github.event.deployment_status.state == &#39;success&#39;
    timeout-minutes: 60
    runs-on: ubuntu-latest
    environment: playwright
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 18
      - name: Make environment file
        run: |
          touch .env
          echo &quot;${{ secrets.ENV_FILE }}&quot; &gt; .env
          echo TEST_BASE_URL=${{ github.event.deployment_status.environment_url }} &gt;&gt; .env
      - name: Install dependencies
        run: npm ci
      - name: Install Playwright Browsers
        run: npx playwright install --with-deps
      - name: Run Playwright tests
        run: npm run test:e2e

      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 30</code></pre>
<h4 id="그런데-vercel의-preview처럼-playwright-report를-pr에-comment로-표시할-수는-없나요-라는-코멘트를-받았다">그런데, Vercel의 Preview처럼 Playwright Report를 PR에 Comment로 표시할 수는 없나요? 라는 코멘트를 받았다.</h4>
<p>github actions 상세 페이지로 들어가지 않는 한 테스트의 성공/실패 여부만 표시될뿐 몇 개의 테스트가 얼만큼의 시간 동안 실행되었고 어떤 요소에서 실패했는지를 한눈에 보기는 어려웠다.</p>
<h4 id="그래서-다른-잘-만들어진-action을-찾았다-🤗">그래서 다른 잘 만들어진 Action을 찾았다. 🤗</h4>
<p>github-actions bot을 활용해 PR에 e2e comment를 표시해준다.</p>
<p><a href="https://github.com/marketplace/actions/playwright-report-comment">https://github.com/marketplace/actions/playwright-report-comment</a>
<img src="https://velog.velcdn.com/images/ss-won/post/3e556f30-e63f-4018-9e14-86b08871f439/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/ss-won/post/4110d158-e391-4cec-b77a-205671b00309/image.png" alt=""></p>
<p>근데 이게 바로 잘 됐으면 좋았겠지만 역시나 잘 안됐다.</p>
<p>문제는 해당 action은 push/pull_request에 의한 호출일 경우에만 실행되는 것이었다. 기존 사용하던 <code>deployment_status</code> trigger 와는 같이 쓸 수가 없었던 것이다.</p>
<h4 id="그래서-push-pull_request로-trigger를-바꾸기로-하였다">그래서 push, pull_request로 trigger를 바꾸기로 하였다!</h4>
<p>이제 여기서 어떻게 Preview가 배포가 되었는지 확인하고 결과 URL을 가져올지가 문제였다. 분명 다른 사례가 있을 것 같아서 일단 marketplace를 더 찾아봤다.</p>
<p>역시 배우신 분들께서 이미 만들어주셨다…! (나는 언제 이런거 개발하는 사람이 될 수 있을까...)
<a href="https://github.com/marketplace/actions/wait-for-vercel-preview">https://github.com/marketplace/actions/wait-for-vercel-preview</a>
<code>wait-for-vercel-preview</code> -&gt; 말 그대로 preview가 배포될때까지 interval polling을 하면서 preview url이 생성되었는지의 여부를 판단한다.</p>
<p>내부 코드를 조금 까봤는데 GITHUB Token을 Environment 변수 input으로 넣어주면 github sdk를 이용해서 octokit intance를 생성 -&gt; <code>deployments</code> 리스트를 axios fetch request를 통해 계속 지정한 check_interval과 max_timeout 제한만큼 반복 실행해주고 있었다.</p>
<h4 id="결과만-보면-되는-분들을-위한-tldr">결과만 보면 되는 분들을 위한 TL;DR</h4>
<p>이후로도 playwright.config file 설정때문에 작은 고군분투가 있었으나 다행히 아래와 같은 완성본을 만들 수 있었고 성공적인 PR Comment까지 볼 수 있었다. 
<img src="https://velog.velcdn.com/images/ss-won/post/34c9e9de-5286-445d-8590-8a32f9d977e6/image.png" alt=""></p>
<pre><code class="language-yaml">name: Playwright Tests
on:
  push:
    branches:
      - &#39;main&#39;
  pull_request:
    branches:
      - &#39;main&#39;
      - &#39;release&#39;
jobs:
  # access preview url
  e2e-test:
    runs-on: ubuntu-latest
    environment: playwright
    timeout-minutes: 60
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 18
      - name: Waiting for 200 from the Vercel Preview
        uses: patrickedqvist/wait-for-vercel-preview@v1.3.1
        id: waitFor200
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
          max_timeout: 300
          check_interval: 10
      - name: Make environment file
        run: |
          echo &quot;Deployed to: ${{ steps.waitFor200.outputs.url }}&quot;
          touch .env
          echo &quot;${{ secrets.ENV_FILE }}&quot; &gt; .env
          echo TEST_BASE_URL=${{ steps.waitFor200.outputs.url }} &gt;&gt; .env
      - name: Install dependencies
        run: npm ci
      - name: Install Playwright Browsers
        run: npx playwright install --with-deps
      - name: Run Playwright tests
        run: npm run test:e2e
      - uses: daun/playwright-report-summary@v3
        name: &#39;Playwright Reporter&#39;
        with:
          report-file: results.json
          comment-title: &#39;Playwright E2E Test Results&#39;
          job-summary: true</code></pre>
<p>일단 기존 playwright report 설정을 CI일때에는 json 파일로 내보낼 수 있도록 수정했고 artifact는 굳이 살펴보지 않을 것 같아 제거하였다. 이후 필요성이 생기거나 분석의 가치가 있다고 판단되면 아마 S3로 옮기지 않을까 싶다.</p>
<p>코드 다시보니까 waitFor200이라는 id가 부적합한듯...timeout이 300인지라...다음 편에서 쓰는 김에 고쳐봐야겠다. 그럼 20000</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[ts-node에서 vite-node로 전환하고 광명 찾은 후기]]></title>
            <link>https://velog.io/@ss-won/ts-node%EC%97%90%EC%84%9C-vite-node%EB%A1%9C-%EC%A0%84%ED%99%98%ED%95%98%EA%B3%A0-%EA%B4%91%EB%AA%85%EC%B0%BE%EC%9D%80-%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@ss-won/ts-node%EC%97%90%EC%84%9C-vite-node%EB%A1%9C-%EC%A0%84%ED%99%98%ED%95%98%EA%B3%A0-%EA%B4%91%EB%AA%85%EC%B0%BE%EC%9D%80-%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Wed, 13 Mar 2024 07:22:40 GMT</pubDate>
            <description><![CDATA[<p>오늘은 명확한 해결책이라기보다는 ts-node의 작동 내용에 대한 이해 부족으로 인해 계속 에러를 겪다 못해 시간을 계속 허비 하다가 vite-node라는 라이브러리를 소개할 겸 오류 상황을 기록하고자 글을 적어본다. 뭐 추후에 제대로 이해하고 왜 잘못되었는지를 기록하게 될 수도..? (나름대로 3일에 1회 이상을 블로그 쓰려고 노력중인데 사실 현재 콘텐츠 고갈..)</p>
<p>오늘의_코딩 시리즈 중에서 아래 포스팅과 연결되는 내용이기도 하다.
<a href="https://velog.io/@ss-won/allowJS%EC%99%80-Cannot-write-file-...-because-it-would-overwrite-input-file.%EC%97%90-%EB%8C%80%ED%95%9C-%EA%B3%A0%EC%B0%B0">allowJS와 &quot;Cannot write file ... because it would overwrite input file.&quot;에 대한 고찰</a></p>
<p>저번 포스트에서 ts-node로 실행하는 것이 목적인 패키지를 개발중이라고 했고 구조는 다음과 같다고 표현했었다.</p>
<pre><code>gym-collector/
    modules/
        grade-collecotor.js
        retryCatch.ts
    index.ts
    type.d.ts
    ...</code></pre><p>지금까지 특별히 문제는 없었는데, index.ts의 내용이 길어져서 파일을 분리를 하던 중에 에러를 마주하게 되었다.</p>
<pre><code>gym-collector/
    modules/
        grade-collecotor.js -&gt; (cjs)
        retryCatch.ts -&gt; (esm(cjs 호환 가능한 수준))
        gym-collector.ts -&gt; (esm)
        address-collector.ts -&gt; (esm)
        collector.ts -&gt; (esm)
    index.ts // ts-node 진입점 (esm)
    type.d.ts
    ...</code></pre><p>modules에 있는 모듈을 import해서 index.ts에서 사용하고 있고 esm 기반 모듈로 작성되었다.
근데 알다시피 node.js가 취급하는 모듈은 cjs(commonjs) 기반이다.</p>
<p>그래서 일반적인 ts-node로 실행하면 오류가 날것이고 <a href="https://typestrong.org/ts-node/docs/imports#native-ecmascript-modules">공식문서</a>에서 제공하는대로 작성하면 esm 기반 index.ts가 실행은 된다. 근데 이제 다른 모듈을 import 하기 시작하면 또 다른 에러가 발생하기 시작한다.
<img src="https://velog.velcdn.com/images/ss-won/post/c9104085-2dc6-4ae4-b717-f4764bc3229b/image.png" alt=""></p>
<p>가장 대표적인 에러가 <code>ts-node cannot find module</code> 이런 것인데, 여기저기 무지성으로 구글링해서 다음과 같이 tsconfig.json에 넣으니 실행은 되긴했다. (esm용 모듈을 추가해서 import하기 전까진..)</p>
<pre><code>&quot;ts-node&quot;: {
    &quot;esm&quot;: true,
    &quot;files&quot;: true,
    &quot;transpileOnly&quot;: true,
    &quot;experimentalResolver&quot;: true,
    &quot;experimentalSpecifierResolution&quot;: &quot;node&quot;,
    &quot;compilerOptions&quot;: {
      &quot;module&quot;: &quot;commonjs&quot;
    }
  },</code></pre><p>갑자기 esm 기반으로 작성된 모듈을 index.ts에 import하고 시작하니까 또 모듈을 못찾겠다는 에러가.......뭐 사실 애초에 다 esm으로 작성하거나 다 cjs로 작성했어야 했는데 통일하지 않은 나의 죄도 크긴 하다 ^^....(아니 근데 cjs -&gt; esm import는 되는걸로 알아서 그렇게 한건데 왜...잘못알았나 나중에 다시 찾아봐야지)</p>
<h4 id="tldr-여기만-봐도-됩니다">TL;DR (여기만 봐도 됩니다)</h4>
<p>어쨌든 내가 생각/고려했던 대표적인 <code>ts-node cannot find module</code> <strong><code>해결 방법</code></strong>은</p>
<ol>
<li>ts-node 설정에 tsconfig paths 추가해주기. 
<img src="https://velog.velcdn.com/images/ss-won/post/60cf4a24-252e-4cb7-9a25-2df4bca687cc/image.png" alt=""></li>
</ol>
<p><a href="https://github.com/TypeStrong/ts-node/issues/422#issuecomment-1340605288">https://github.com/TypeStrong/ts-node/issues/422#issuecomment-1340605288</a></p>
<ol start="2">
<li>ts-node를 버리고, tsc로 컴파일해서 commonjs로 outDir에 내보낸 후 js 파일 node로 실행하기</li>
</ol>
<p>-&gt; 라이브러리 굳이 안깔거면 이 또한 괜츈!</p>
<ol start="3">
<li>ts-node -&gt; vite-node로 전환하기</li>
</ol>
<p>여기서 내가 생각한건 3번이었다. 실제 ts-node path error에 지친 사람들에게 추천 코멘트를 달아준 사람도 있었다. 그래서 사실 1번을 해보지 않고 그냥 바로 3번으로 옮겨봤다. </p>
<p>일단 나는 기본적으로 cjs보다는 esm 문법에 익숙해서 앞으로도 esm 기반으로 코드를 작성할 것이고 기본적으로 추가옵션을 주지 않고도 알잘딱 module path resolve를 해주는 라이브러리를 사용하고 싶었다. 애초에 지금 만들고 있는게 메인 패키지가! 아니기! 때문에! 더이상 오류를 고치고 싶지 않았다.</p>
<p>아래 보이는 딱 여기만 보고, 라이브러리 깔고 실행 명령어만 ts-node에서 vite-node로 바꿨는데 그동안의 삽질이 무색하게 바로 잘 실행되는 것을 보니 속이 다 시원했다. 역시 라이브러리 좋은게 있으면 그냥 패키지 커지기 전에 바로바로 바꿔주는게 심신과 비용에 이득이다. 여러분 vite-node로 바꾸고 광명 찾으세요. <strong>복잡한 설정 없이 esm 돌리기 가능..</strong>🤗</p>
<p><img src="https://velog.velcdn.com/images/ss-won/post/aa0bbdd8-cf3f-42fa-aaef-e875f84c5fcc/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[VSCode Remote - SSH extension connect error 해결하기(glibc..libstdc++..)]]></title>
            <link>https://velog.io/@ss-won/vscode-Remote-SSH-%EC%9B%90%EA%B2%A9-%ED%98%B8%EC%8A%A4%ED%8A%B8-%EC%97%B0%EB%8F%99-Error-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0glibc..libstdc</link>
            <guid>https://velog.io/@ss-won/vscode-Remote-SSH-%EC%9B%90%EA%B2%A9-%ED%98%B8%EC%8A%A4%ED%8A%B8-%EC%97%B0%EB%8F%99-Error-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0glibc..libstdc</guid>
            <pubDate>Mon, 11 Mar 2024 06:18:18 GMT</pubDate>
            <description><![CDATA[<p>오랜만에 고전 프로젝트 수정이 필요해서 EC2 접근하려는데, 또 신박한 에러를 만났다. 역시 세상에 쉬운 일은 없다.</p>
<blockquote>
<p>원격 호스트가 glib 및 libstdc++에 대한 VS Code 서버의 필수 구성 요소를 충족하지 못할 수 있습니다.
<img src="https://velog.velcdn.com/images/ss-won/post/44f0366d-5b0c-4289-9f0e-3a0163ff4c30/image.png" alt=""></p>
</blockquote>
<p>그리고 기타정보 버튼을 누르면 해당 <a href="https://code.visualstudio.com/docs/remote/linux#_remote-host-container-wsl-linux-prerequisites">링크</a>로 연결이 되는데, 결국 Remote SSH를 연결하기 위한 사전 필요 스펙 정보를 제공하고 있다. </p>
<p><img src="https://velog.velcdn.com/images/ss-won/post/5698e915-f15a-42be-8fad-5a0be0c06a1c/image.png" alt="">
glibc와 libstdc++ 버전이 안맞는것 같아 위에 나와있는대로 터미널 ssh로 ec2에 접속해서 <code>ldd --version</code>을 쳤더니 2.27 버전으로 호환이 되지 않았고, <code>strings /usr/lib64/libstdc++.so.6 | grep GLIBCXX</code>는 not found라는 에러가 나왔다.</p>
<p>일단 뭐 외부에서 upgrade file를 받고 심볼릭을 걸어주라던지, 단순히 apt-get update libstdc++6을 하라던지 등의 답변이 있었는데 나의 경우엔 소용이 없었다. </p>
<p>그래서 apt 업데이트를 하면되나? 싶어서 sudo apt update, sudo apt upgrade 해주고 재부팅 해도 똑같길래 구글에 업그레이드 방법을 찾아봤는데...</p>
<p><a href="https://www.reddit.com/r/linux4noobs/comments/otsgxg/how_do_i_update_glibc_in_ubuntu/">https://www.reddit.com/r/linux4noobs/comments/otsgxg/how_do_i_update_glibc_in_ubuntu/</a>
<img src="https://velog.velcdn.com/images/ss-won/post/beb5827b-8102-48d4-83ab-855c2586a59a/image.png" alt=""></p>
<p>이상적으로는! glibc는 os 시스템에서 중심적인 라이브러리이기 때문에 인위적으로 버전을 바꾸면 예상치 못한 시스템 에러가 발생할 수 있어 전체 os 버전 업그레이드 방식이 더 적합해보인다는 답변이었다.</p>
<p>그래서 그냥 do-release-upgrade로 간단하게 unbuntu version을 18.04 -&gt; 20.04LTS로 업그레이드 시켜줬다.
<a href="https://repost.aws/knowledge-center/ec2-linux-upgrade-ubuntu-lts">ec2 ubuntu 업그레이드 방법 참고</a></p>
<p><img src="https://velog.velcdn.com/images/ss-won/post/fae93bf9-9bd4-4752-a22f-55f96bde5617/image.png" alt=""></p>
<p>업그레이드를 마치고 살펴보면 glibc version 2.31
명령어 이것 -&gt; <code>ldd --version</code>
<img src="https://velog.velcdn.com/images/ss-won/post/15b056a9-9a50-456c-9917-59d5e311517d/image.png" alt=""></p>
<p>libstdc++ 버전은 역시 호환 버전(&gt;=3.4.25)이 깔려있는 것을 확인 할 수 있다.
명령어 이것 -&gt; <code>strings /usr/lib/x86_64-linux-gnu/libstdc++.so.6 | grep GLIBCXX</code></p>
<p><img src="https://velog.velcdn.com/images/ss-won/post/41e828ea-3986-40ab-b5b3-783041210d33/image.png" alt=""></p>
<p>그리고, 다시 Remote SSH 연동을 해보면 연결될 것이다. 👍🏻</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Next.js API Route로 GraphQL Proxy Server 만들(고군분투)기]]></title>
            <link>https://velog.io/@ss-won/Next.js-API-Route%EB%A1%9C-GraphSQL-Proxy-Server-%EB%A7%8C%EB%93%A4%EA%B3%A0%EA%B5%B0%EB%B6%84%ED%88%AC%EA%B8%B0</link>
            <guid>https://velog.io/@ss-won/Next.js-API-Route%EB%A1%9C-GraphSQL-Proxy-Server-%EB%A7%8C%EB%93%A4%EA%B3%A0%EA%B5%B0%EB%B6%84%ED%88%AC%EA%B8%B0</guid>
            <pubDate>Fri, 08 Mar 2024 08:58:53 GMT</pubDate>
            <description><![CDATA[<p>일반적으로 <strong>CORS 처리</strong>를 할때는 1) 백엔드 <strong>서버단에서</strong> <strong>허용할 origin에 대해 CORS 처리</strong>를 해주거나 2) 미들웨어 단에서 <strong>Proxy server</strong>를 통해 Origin을 인위적으로 바꿔서 요청해 회피하는 방법이 있다.
보통 production 서버라던지 확정된 호스트인 경우에는 백엔드 팀에 처리를 부탁하는 편이지만 origin이 바뀔 가능성이 있는 local dev 서버 origin의 경우 프론트에서 자체적으로 처리하기 위해 필요할때마다 proxy를 이용하곤 한다.</p>
<p>Next.js의 경우 정적(static) proxy는 설정 파일(next.config.js)에서 Rewrites 함수를 설정해주면 된다. 설정 자체가 어렵지도 않고 문서에도 잘 설명되어 있어서 왠만한 proxy는 추가적인 라이브러리나 서버 처리 없이도 해당 내용으로 커버가 가능하다.
<a href="https://nextjs.org/docs/pages/api-reference/next-config-js/rewrites#rewriting-to-an-external-url">https://nextjs.org/docs/pages/api-reference/next-config-js/rewrites#rewriting-to-an-external-url</a></p>
<pre><code class="language-js">module.exports = {
  async rewrites() {
    return [
      {
        source: &#39;/blog&#39;,
        destination: &#39;https://example.com/blog&#39;,
      },
      {
        source: &#39;/blog/:slug&#39;,
        destination: &#39;https://example.com/blog/:slug&#39;, // Matched parameters can be used in the destination
      },
    ]
  },
}</code></pre>
<p>근데 이제 여기서 문제는 동적으로 proxy를 처리해야하는 경우다. 시나리오를 짧게 설명해보자면 다음과 같다.</p>
<blockquote>
<p>사용자 서비스 등록 -&gt; 서비스에 고유 endpoint(서버)가 존재 -&gt; 클라이언트 단에서는 해당 endpoint 정보를 API를 통해 가져와서 해당 서버에 대한 API를 요청</p>
</blockquote>
<p>동적으로 endpoint를 받아 API 호출을 해주고 있기 때문에 위의 rewrites로 커버가 되지 않는다. production에서는 백엔드에서 docker image build시 환경변수를 통해 서버를 띄울때 CORS 처리할 origin을 주입하여 배포해주고 있기 때문에 문제가 되지는 않았다.</p>
<p>그런데, <code>왜 갑자기 Proxy Server를 만들게 되었냐?</code> 라고 한다면 이유는 이러하다.
현재 서비스 프론트를 monorepo로 운용하고 있는데 최근에 microfrontend 패턴을 적용하게 되면서 기존 origin과 다른 host app에서 GraphQL server 요청을 해야하는 case가 생겼다. 백엔드 팀에 부탁할까 하다가 </p>
<p>1) local dev 서버의 endpoint가 또 바뀔 가능성이 있고 
2) 서비스 특성상 Cross Origin으로 요청할 일이 계속 생길 것이며
3) 계속 origin을 추가하다보면 env 관리도 비효율적이게 될 가능성이 있고
4) 환경변수 주입을 위해 매번 build/restart를 해줘야 하는 문제가 있어서</p>
<p>Next app에 동적 proxy를 위한 route를 뚫어보기로 했다.
 일종의 API Gateway처럼 다른 프론트 앱에서 User Auth 정보 접근과 정적 proxy용으로 사용하고 있었던 Next API route 전용 App이 있어서 해당 지점에 적용해보았다.</p>
<h4 id="첫-시작은-middleware">첫 시작은 middleware</h4>
<p>기존 Next.js app은 v14의 app router를 차용하고 있었고, proxy 처리는 모두 rewrites case에 넣어 사용하고 있었다. 동적인 케이스를 어떻게 처리하지?라고 고민하다가 처음 생각했던 것은 middleware였다. 실제 서버로 가기전 동작을 제어하는 역할을 하는 proxy 성격과 맞고 Next.js의 middleware config 설정을 통해 특정 paths에 match하는 부분에만 동작하도록 제어할 수도 있기 때문이다.</p>
<p>proxy에 사용할 라이브러리를 찾아다 <a href="https://github.com/stegano/next-http-proxy-middleware">https://github.com/stegano/next-http-proxy-middleware</a> 에 언급된 http-proxy 이용 케이스를 보고 해당 내용을 기반으로 코드를 작성해보기로 했다.</p>
<pre><code class="language-ts">// pages/api/[...all].ts
import httpProxy from &quot;http-proxy&quot;;

export const config = {
  api: {
    // Enable `externalResolver` option in Next.js
    externalResolver: true,
    bodyParser: false,
  },
};

export default (req, res) =&gt;
  new Promise((resolve, reject) =&gt; {
    const proxy: httpProxy = httpProxy.createProxy();
    proxy.once(&quot;proxyRes&quot;, resolve).once(&quot;error&quot;, reject).web(req, res, {
      changeOrigin: true,
      target: process.env.NEXT_PUBLIC_API_PROXY_URL,
    });
  });</code></pre>
<p>그런데 NextRequest 형식은 nodejs express request 형태가 아닌것 같았다. 라이브러리를 쓸 수가 없었다.🫠 그리고 query요소와 body도 읽을 수가 없었다. query string으로 target endpoint를 받아서 proxy origin으로 설정해주려고 했고, body값이 잘 넘어가는지 확인해보려고 했는데 계속 null만 찍히고 query 요소는 애초에 NextRequest 객체에 없었다. (대신 searchParam이라는 요소가 있는듯 하다)
<img src="https://velog.velcdn.com/images/ss-won/post/4379a305-1fce-4b80-939f-bb0b4e7e353a/image.png" alt=""></p>
<p>이게 뭐지..? middleware의 Request는 nodejs express 환경이 아닌가...???하고 찾아봤는데 그것이 사실이었다. ^<strong>__</strong>^ Express middleware가 아니라 client-side navigation안에서만 동작한다고 한다. 실제로 NextRequest type을 살펴보면 Fetch API Request Class를 extends하고 있는 것을 찾을 수 있다. 클라이언트용 middleware였던 것이다... 보안취약점 가능성이 있어 실제 client단에서 호출한 request body가 포함된 response를 middleware단에서 제어할 수 없도록 만들었다고 한다.(stackoverflow 답변에 의하면 그렇다네요👻)</p>
<p><img src="https://velog.velcdn.com/images/ss-won/post/6668f9d1-4415-458b-aa04-fa6c602c3bee/image.png" alt=""></p>
<p><a href="https://nextjs.org/docs/pages/building-your-application/routing/middleware#nextresponse">middleware NextResponse 파트 공식문서</a>도 보면 middleware에서 생성할 수 있는 Response의 종류는 아래와 같은 2가지 케이스다.</p>
<ol>
<li>Page, Edge API Route로 만든 route를 rewrite하는 response.</li>
<li>NextReponse (redirect, rewrite, cookie 및 header 제어, json body 생성 가능)
<img src="https://velog.velcdn.com/images/ss-won/post/85d01794-fc08-4789-8f99-f7fb0eae3647/image.png" alt=""></li>
</ol>
<h4 id="그래서-app-router---route-handler로-처리해봤는데-외않돼">그래서 App router - Route Handler로 처리해봤는데 외않돼..?</h4>
<p><img src="https://velog.velcdn.com/images/ss-won/post/46cd9b3c-d47e-45d8-a0ec-e3915318d42a/image.png" alt="">
일단 나는 이미 설치한 라이브러리를 최대한 사용하는 쪽으로 구현하고 싶었고 API Route는 express 서버단 처리를 할 수 있을테니 여기에 걸면 되겠지?라고 생각해서 middleware 내용을 옮겨봤는데 마찬가지로 제어가 안됐다. 
App router의 Route Handler단에서도 middleware와 마찬가지로 Fetch Request와 Response를 사용하고 있었다. </p>
<p>🫠 뭔데..Good to know에는 pages router의 API Routes랑 동일하니까 pages와 API Route를 함께 사용할 필요가 없다더니...? request 형태가 다르쟈나요..흙흙 혹시 몰라 runtime 변수를 nodejs로도 줘봤는데 그런거 안통한다.ㅋㅋ(여기도 보안 취약점 이슈인걸까..)</p>
<p>(나만 바보인건가..?하고) 다시 또 열심히 찾아봤는데 다들 똑같은 에러를 경험하고 계셨다. app router에서는 http-proxy-middleware 구현이 안되네요..? client side용 proxy도 아마 만들 수 있지 않을까 싶긴한데….?</p>
<p>어쨌든 결론은 현 라이브러리 사용을 위해서 nodejs runtime환경의 request, reseponse를 사용하지 않는것 같으니 page router API Route로 만들면 된다고한다. (그냥 처음부터 예시코드 따라할걸...)
<a href="https://github.com/chimurai/http-proxy-middleware/issues/932">https://github.com/chimurai/http-proxy-middleware/issues/932</a>
<img src="https://velog.velcdn.com/images/ss-won/post/33e49020-dfad-4fb1-a57d-8e1810107828/image.png" alt=""></p>
<h4 id="그래서-page-route로-다시-처리해봤더니-호출은-되는데-이번엔-response가-안온다-😊">그래서 page route로 다시 처리해봤더니 호출은 되는데 이번엔 response가 안온다. 😊</h4>
<p><img src="https://velog.velcdn.com/images/ss-won/post/8e1aa7f1-749e-4e08-ad68-ed7be9b7e4d0/image.png" alt="">
proxy로 request 요청 자체를 잘 보내는건가? 싶어서 Graphql route말고 다른 path로 요청을 보내봤는데 응답이 온다! =&gt; 이 말은 proxy 자체는 문제가 없고 GraphQL 요청을 뭔가 잘못보내고 있다는 것인데..? 대체 어디가 문제일까!</p>
<p>나같은 사람 있게 해주세요 기도하며 또 구글링을 하다보니 드디어 해결책을 찾았다. (할렐루야)</p>
<p><a href="https://github.com/vercel/next.js/discussions/11036">https://github.com/vercel/next.js/discussions/11036</a>
<img src="https://velog.velcdn.com/images/ss-won/post/26eb5b77-92bb-42a3-8428-8376d34bceac/image.png" alt="">
<img src="https://velog.velcdn.com/images/ss-won/post/87d86815-a150-48d9-b5fb-9b8c9cfc9f1e/image.png" alt=""></p>
<p>Next.js API Route에서 실행되는 automatic body parsing 때문인 것 같다고 한다. (그게 뭔지는 나도 모른다ㅎ. 뭐 대충 request body를 자동적으로 parsing해서 형태를 변형하나부다.) config 설정에 bodyParser 기능을 꺼주면 원하는대로 동작할 것이라는 말..!</p>
<h4 id="드디어-원하는-결과-도출-🎊">드디어 원하는 결과 도출 🎊</h4>
<p>response는 잘 살펴보면 GraphQL 요청 query가 잘못되서 error긴 하지만ㅋㅋㅋㅋㅋ 여기서 중요한건 response가 온다는 것이다.
<img src="https://velog.velcdn.com/images/ss-won/post/1e642066-d63b-4f47-a8d7-93d03110b89d/image.png" alt=""></p>
<p>완성된 next API Route단 코드는 이것</p>
<pre><code class="language-ts">// pages/api/engine.ts
import { createProxy } from &#39;http-proxy&#39;;
import { NextApiRequest, NextApiResponse } from &#39;next&#39;;

export const config = {
  api: {
    bodyParser: false,
  },
};

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  new Promise((resolve, reject) =&gt; {
    const proxy = createProxy();
    proxy.once(&#39;proxyRes&#39;, resolve).once(&#39;error&#39;, reject).web(req, res, {
      changeOrigin: true,
      target: req.query.target?.toString(),
      ignorePath: true,
    });
  });
}</code></pre>
<p>client단에서는 ApolloClient를 만들때 요청 uri가 proxy 서버에 전달되게끔 다음과 같이 설정해주었다. 결과적으로 성공!</p>
<pre><code class="language-ts"> const httpLink = createHttpLink({
    uri: `/api/engine?target=${engineURL}/api/graphql`, // 이 부분!
  });
  const authLink = makeGraphqlAuthLink(APIToken);
  graphqlClient = new ApolloClient({
    link: authLink.concat(httpLink),
    cache: new InMemoryCache({
      addTypename: false,
    }),
  });</code></pre>
<h4 id="근데-app-router랑-page-router랑-같이-써도-되나">근데 app router랑 page router랑 같이 써도 되나..?</h4>
<p>권장되는 사항은 아닌것 같지만 같이 쓸 수는 있다고 한다. app router의 route handler 부분이 page router의 API Route와는 다르게 동작하는 부분이 분명히 존재하다보니 어쩔 수 없다고 결론지었다. nodejs runtime에서도 동작할 수 있게 만들어주시면 좋겠다...server component에 sql까지 쓸 수 있는 환경인데 어쩨서 nodejs 환경 동작은 못하는 것인가…!
<img src="https://velog.velcdn.com/images/ss-won/post/c55c1392-a656-48ca-8351-83223afa44e9/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Prisma upsert error code P2002 해결하기 (about race condition)]]></title>
            <link>https://velog.io/@ss-won/Prisma-upsert-error-code-P2002-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@ss-won/Prisma-upsert-error-code-P2002-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 04 Mar 2024 06:39:48 GMT</pubDate>
            <description><![CDATA[<p>오류의 종류는 PrismaClientKnownRequestError, 오류 로그를 읽어보면 upsert문에서 발생한 것으로 error code는 2002, unique name constraints에 위배되는 작업을 했다고 한다.
<img src="https://velog.velcdn.com/images/ss-won/post/346d1624-dd52-4ee5-aa38-5ea847e004a7/image.png" alt=""></p>
<p>즉 이미 저장하려고는 하는 name이 row에 존재하므로 create 작업을 할 수 없다는 이야기인데
Prisma에서 upsert는 create, update, where 필드를 지정하여 where 조건에 맞는 데이터가 존재하면 update를 없다면 create를 실행하는건데 어떻게 contraint 에러가 날 수 있는 거지?????라고 생각했다.</p>
<p>그래서 오늘도 공식문서와 구글링을 열심히 해봤는데 공식문서에 원인와 솔루션이 나와있었다.
<img src="https://velog.velcdn.com/images/ss-won/post/d80a1f3d-ad5a-4e2c-9487-805ea04079d3/image.png" alt=""></p>
<p>문제 상황/전제 조건</p>
<ul>
<li>레코드가 아직 존재하지 않는데, multiple upsert를 동시에 진행하는 상황일 때 하나 이상의 작업에서 unique key constraint error가 발생할 수 있다.</li>
</ul>
<p>원인</p>
<ul>
<li>Prisma Client에서 upsert문을 실행할 때, 레코드가 이미 존재하는지 먼저 확인하는데 위에서 잠깐 언급했듯이 해당 read operation에서 레코드가 존재하면 update문을 그렇지 않으면 create문을 실행하게 된다. 그런데 여기서 <strong>동시에 같은 테이블에 여러개의 upsert문을 실행하게 되면 동시성 문제(race condition)가 발생한다. 두 개 이상의 upsert문에서 where clause(read 연산)를 수행할 때 같은 자원인 테이블에 읽기/쓰기 연산 순서를 진행하는 과정에서 레코드가 없다 판단할 가능성이 존재하고 이에 따라 동시에 create 연산을 진행하게 된다. 결과적으로 create가 2번 이상 실행되면서 다음과 같은 unique constraint 에러가 발생</strong>한다는 것이다.</li>
<li>나의 케이스는 Promise.all로 multiple upsert를 transaction 연산을 통해 진행하고 있었는데 그러다 보니 내부 upsert문이 동시에 실행되는 상황이 존재했다. upsert문이 내부적으로 where clause(read) -&gt; update/create(write) 순서 및 연산을 나누어 비동기로 진행되는 것으로 추측된다. 이에 따라 read문이 읽히는 순서에 따라 같은 레코드를 create하는 작업을 실행하게 되어 에러가 나온 것이다.</li>
</ul>
<p>해결 방법</p>
<ul>
<li>P2002 error code에 대한 error handling을 따로 해준다. 해당 error code를 catch하면 upsert문을 재실행하여 create가 아닌 update문을 실행할 수 있도록 한다.</li>
<li>본문에 나온 해결방법에 따라 P2002 에러가 발생한 연산에 한하여 retry를 하도록 했다. (해당 에러는 여러번의 재시도를 할 필요가 없다. 일단 한번 레코드 작업이 성공하면 이후 where read 연산에서는 레코드가 있다고 판단하여 update문을 실행할 것이기 때문에 wrtie unique constraints 에러가 발생하지 않는다.)</li>
</ul>
<h4 id="before">BEFORE</h4>
<pre><code class="language-ts">// index.ts
const pushGymToDatabase = async (gym: RockTaGym[&quot;Gym&quot;]) =&gt; {
  const { address, phone, brand, ...others } = gym;

  return prisma.$transaction(async (tx) =&gt; {
        const gymAddress = await tx.address.findFirst({
          where: {
            ...address,
          },
        });

        const gymInput = {
          ...others,
          address: {
            ...(gymAddress
              ? { connect: { id: gymAddress.id } }
              : { create: { ...address } }),
          },
          ...(!phone.phoneNumber
            ? {}
            : {
                phone: {
                  connectOrCreate: {
                    create: { phoneNumber: phone.phoneNumber },
                    where: {
                      phoneNumber: phone.phoneNumber,
                    },
                  },
                },
              }),
          brand: {
            connectOrCreate: {
              create: {
                name: brand.name,
                relatedBrand: brand.relatedBrand ?? null,
              },
              where: {
                name: brand.name,
              },
            },
          },
        };

        // 여기가 바로 문제의 upsert
        await tx.gym.upsert({
          create: gymInput,
          update: gymInput,
          where: {
            name: others.name,
          },
        });
      });
};</code></pre>
<h4 id="after">AFTER</h4>
<pre><code class="language-ts">// modules/retryCatch.ts
export async function retryCatch&lt;T&gt;(
  callback: () =&gt; Promise&lt;T&gt;, // 메인 콜백함수
  retryCondition?: (error: any) =&gt; boolean, // 조건 함수
  times = 1 // retry 횟수 (여기서는 1번만 필요하니 1로 기본값을 둔다.)
): Promise&lt;T&gt; {
  try {
    return await callback();
  } catch (error) { 
    // error retry times가 유효하고, retryCondition에 부합하는 에러이면 함수를 다시 실행한다.
    if (times &gt; 0 &amp;&amp; retryCondition?.(error)) {     
      return await retryCatch(callback, retryCondition, times - 1);
    } else {
      // 그 이외의 경우에는 error을 throw 한다.
      throw error;
    }
  }
}</code></pre>
<pre><code class="language-ts">// index.ts
import { retryCatch } from &quot;modules/retryCatch&quot;;
...

const pushGymToDatabase = async (gym: RockTaGym[&quot;Gym&quot;]) =&gt; {
  const { address, phone, brand, ...others } = gym;

  return retryCatch(
    () =&gt;
      prisma.$transaction(async (tx) =&gt; {
        const gymAddress = await tx.address.findFirst({
          where: {
            ...address,
          },
        });

        const gymInput = {
          ...others,
          address: {
            ...(gymAddress
              ? { connect: { id: gymAddress.id } }
              : { create: { ...address } }),
          },
          ...(!phone.phoneNumber
            ? {}
            : {
                phone: {
                  connectOrCreate: {
                    create: { phoneNumber: phone.phoneNumber },
                    where: {
                      phoneNumber: phone.phoneNumber,
                    },
                  },
                },
              }),
          brand: {
            connectOrCreate: {
              create: {
                name: brand.name,
                relatedBrand: brand.relatedBrand ?? null,
              },
              where: {
                name: brand.name,
              },
            },
          },
        };

        await tx.gym.upsert({
          create: gymInput,
          update: gymInput,
          where: {
            name: others.name,
          },
        });
      }),
    // retry 여부를 판단하는 함수를 넣어주었다.
    // error 객체 code 값에 P2002를 넣으면 첫 번째 파라미터에 넣은 Promise 함수를 재실행한다.
    (error) =&gt; error?.code === &quot;P2002&quot; // race condition error
  );
};</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[allowJS와 "Cannot write file ... because it would overwrite input file."에 대한 고찰]]></title>
            <link>https://velog.io/@ss-won/allowJS%EC%99%80-Cannot-write-file-...-because-it-would-overwrite-input-file.%EC%97%90-%EB%8C%80%ED%95%9C-%EA%B3%A0%EC%B0%B0</link>
            <guid>https://velog.io/@ss-won/allowJS%EC%99%80-Cannot-write-file-...-because-it-would-overwrite-input-file.%EC%97%90-%EB%8C%80%ED%95%9C-%EA%B3%A0%EC%B0%B0</guid>
            <pubDate>Thu, 29 Feb 2024 01:16:17 GMT</pubDate>
            <description><![CDATA[<p>대충 영어로는 이런 에러고<code>&quot;Cannot write file ... because it would overwrite input file.&quot;</code> 한글로는 아래처럼 <code>&quot;... 파일은 입력 파일을 덮어쓰므로 쓸 수 없습니다.&quot;</code> 라는 문구의 에러를 만났다 😬
<img src="https://velog.velcdn.com/images/ss-won/post/43d6b507-3379-4673-99c1-b840cbe4ff23/image.png" alt=""></p>
<p>일단, 문제가 발생한 파트는 메인 API 쪽은 아니고, Database에 특정 정보를 third party(kakao place, google spreadsheet)부터 가져와서 데이터를 전처리하고 prisma를 통해 db로 push해주는 코드를 짜는 일종의 모듈/패키지 개발 쪽이다.</p>
<p>대충 패키지 구조는</p>
<pre><code>gym-collector/
    modules/
        grade-collecotor.js -&gt; 문제의 파일
        retryCatch.ts
    index.ts
    type.d.ts
    ...</code></pre><p>사실 타입스크립트로 작성했으면 참 좋았겠지만 Google SpreadSheet API를 이용해 정보를 긁어오는 Node.js 버전 코드를 거의 그대로 옮겨왔는데, 타입스크립트로 변형해주기 너무 귀찮아서(ㅎㅎ) allowJS를 해주고 사용하기로 했다. 마침 예제코드에 이미 JSDoc type hints가 모두 적혀져있기도 했기 때문에...!</p>
<p>아무튼 allowJS를 true로 설정해준 후, include에 modules/ 폴더를 넣어주었는데 위와같은 에러가 발생했다. 아니 일단 번역체부터가 이해가 안된다. 대체 무슨 소리일까...?하고 구글링 해봤는데 한글로 하면 안나오더라.(영어 설정으로 사용해야하는 EU..)</p>
<p>그래서 추측해서 영어로 구글링을 다시 해봤는데 내 케이스에 맞는 적절한 답변과 원인에 대한 설명을 얻을 수 있었다.
<a href="https://stackoverflow.com/questions/42609768/typescript-error-cannot-write-file-because-it-would-overwrite-input-file">https://stackoverflow.com/questions/42609768/typescript-error-cannot-write-file-because-it-would-overwrite-input-file</a>
<img src="https://velog.velcdn.com/images/ss-won/post/a3670523-1677-4f01-95b3-132100d0f67a/image.png" alt=""></p>
<p>정확히 allowJS:true일때 발생하는 문제로, outDir를 만약 지정하지 않았다면 include해준 javascript 파일도 같이 컴파일 되므로 javascript 원본 파일이 overwrite 되기 때문에 이를 알려주기 위한 에러라는 것이다.</p>
<p>결론적으로 해당 에러는 outDir를 지정해주거나, noEmit으로 js 파일을 내보내지 않겠다고 선언하면 사라진다. 해당 패키지는 다른 package에서 import할 목적으로 만든 내용은 아니고 npm script를 통해 ts-node로 <code>실행</code>하는 것이 목적이기에 굳이 js 파일을 내보낼 필요는 없어서 outDir 
 대신 noEmit을 true로 설정해주었다.</p>
<pre><code class="language-js">{
  &quot;compilerOptions&quot;: {
    /* Visit https://aka.ms/tsconfig.json to read more about this file */

    /* Basic Options */
    &quot;target&quot;: &quot;esnext&quot; /* Specify ECMAScript target version: &#39;ES3&#39; (default), &#39;ES5&#39;, &#39;ES2015&#39;, &#39;ES2016&#39;, &#39;ES2017&#39;, &#39;ES2018&#39;, &#39;ES2019&#39;, &#39;ES2020&#39;, &#39;ES2021&#39;, or &#39;ESNEXT&#39;. */,
    &quot;module&quot;: &quot;ESNext&quot; /* Specify module code generation: &#39;none&#39;, &#39;commonjs&#39;, &#39;amd&#39;, &#39;system&#39;, &#39;umd&#39;, &#39;es2015&#39;, &#39;es2020&#39;, or &#39;ESNext&#39;. */,
    &quot;allowJs&quot;: true /* Allow javascript files to be compiled. */,
    &quot;noEmit&quot;: true /* Do not emit outputs. */,
    &quot;composite&quot;: true /* Enable project compilation */,

    /* Strict Type-Checking Options */
    &quot;strict&quot;: true /* Enable all strict type-checking options. */,

    /* Module Resolution Options */
    &quot;resolveJsonModule&quot;: true,
    &quot;moduleResolution&quot;: &quot;node&quot; /* Specify module resolution strategy: &#39;node&#39; (Node.js) or &#39;classic&#39; (TypeScript pre-1.6). */,
    &quot;baseUrl&quot;: &quot;.&quot; /* Base directory to resolve non-absolute module names. */,
    /* Type declaration files to be included in compilation. */
    &quot;allowSyntheticDefaultImports&quot;: true /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */,
    &quot;esModuleInterop&quot;: true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies &#39;allowSyntheticDefaultImports&#39;. */,

    /* Advanced Options */
    &quot;skipLibCheck&quot;: true /* Skip type checking of declaration files. */,
    &quot;forceConsistentCasingInFileNames&quot;: true /* Disallow inconsistently-cased references to the same file. */
  },
  &quot;ts-node&quot;: {
    &quot;esm&quot;: true,
    &quot;files&quot;: true,
    &quot;transpileOnly&quot;: true,
    &quot;experimentalResolver&quot;: true,
    &quot;experimentalSpecifierResolution&quot;: &quot;node&quot;,
    &quot;compilerOptions&quot;: {
      &quot;module&quot;: &quot;commonjs&quot;
    }
  },
  &quot;include&quot;: [&quot;index.ts&quot;, &quot;./type.d.ts&quot;, &quot;*.json&quot;, &quot;modules&quot;]
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[@tanstack/react-query에 대한 TypeScript 설정 고군분투 (a.k.a ts2742)]]></title>
            <link>https://velog.io/@ss-won/tanstackreact-query</link>
            <guid>https://velog.io/@ss-won/tanstackreact-query</guid>
            <pubDate>Mon, 26 Feb 2024 05:24:33 GMT</pubDate>
            <description><![CDATA[<p>@tanstack/react-router + @tanstack/react-query 조합을 써보려고 @tanstack/react-query를 5버전으로 올린 후(queryOptions와 useSuspenseQuery hook을 써보기 위함) queryOptions를 사용해 custom function을 만들었더니 갑자기 type 참조를 못하겠다며 우수수수수수수 오류가 자기 주장을 해왔다. returnType에 대한 타입참조를 찾지 못해 반환값 추정이 안된단다...😬
<img src="https://velog.velcdn.com/images/ss-won/post/3937fead-e1e2-4ed7-be51-8f9bb58eac9f/image.png" alt=""></p>
<p>일단 문제가 되는 부분은 다행히(?) 모두 types 참조를 못하고 있다는 에러만을 보여주고 있었다.
명확히 어디서 문제가 발생했는지는 솔직히 잘 모르겠지만? pnpm workspace 참조의 문제, typescript bundler resolution 문제, monorepo의 구조적 문제 정도를 의심해봤다. 일단 react-query v5 + vite(react-ts-swc)에서만 문제가 발생해서 가장 유력한건 typescript 설정이었다. 일단 그래서 구글링을 열심히 해봤다.</p>
<p>여기서도 보면 비슷하게 이야기를 나누고 있다. 어쨌든 tanstack 측에서는 tsup이 문제인거 같다고 한다.</p>
<p><a href="https://github.com/TanStack/query/issues/6318">https://github.com/TanStack/query/issues/6318</a>
<img src="https://velog.velcdn.com/images/ss-won/post/4d6ec051-eab2-43e9-a3d9-257199f25a8c/image.png" alt="">
<img src="https://velog.velcdn.com/images/ss-won/post/92d62ed9-df78-4fe5-bf9c-1e317a10ff96/image.png" alt=""></p>
<p>그러니까 원래는 UseQueryReturnType에 대한 dts 파일을 생성한 후, 이를 index.d.ts 진입점에서 다시 같은 이름으로 export를 해줘야하는데, tsup이 트랜스파일 및 번들링을 진행하면서 특정 이름으로 alias를 시켜 간접적으로 export를 한 후 index.d.ts 진입점에서 재반환을 시키고 있다는 것이다. 이 간접 참조 및 alias가 TypeScript의 인식에 혼란을 주고 있다고 한다.</p>
<p>그래서 node_modules로 들어가서 모듈 코드를 까봤는데..? 내 패키지엔 alias가 없는데..? 애초에 다른 패키지 얘기기도 했지만 오류 양상이 비슷해서 봤었는데 내 case의 경우 저 내용이 직접적인 원인은 아닌것 같다. 일단 저 discussion에서는 TypeScript 5.4.0-beta 버전으로 마이그레이션하면 이슈가 없어질거라는데 굳이 beta 버전을 깔고싶지 않았고 alias 케이스가 아니어서 다른 코멘트의 내용을 기반으로 tsconfig 수정 고군분투를 해봤다.</p>
<p>일단 여러가지 시도중에서 유의미했던 방법은 4가지가 있다.</p>
<ol>
<li>tsconfig.json의 moduleResultion을 &#39;node&#39;로 설정하기</li>
</ol>
<ul>
<li>이렇게하면 에러가 없는것처럼 보여지지만? build를 시작하면 에러가 발생한다. 일단 vite 초기 설정이 bundler이고 여기에 맞춘 플러그인과 세팅을 해두었기 때문인것 같다. 그래서 이 방법도 아닌것 같다. 이거 하나 고치려고 너무 대규모 수정을 거쳐야 하므로 pass..</li>
</ul>
<ol start="2">
<li>@react-query/query-core 라이브러리 설치하기</li>
</ol>
<ul>
<li>아무래도 react-query에 dependency가 있는 query-core에서 참조에러가 나오는것 같아서 일단 혹시?하는 마음으로 깔아봤다. 놀랍게도 동작한다...(왜..? 때문에..?)</li>
<li>근데 이 방법도 역시 아닌것 같다. react-query dependencies로 .pnpm에 이미 깔려있는데 굳이 또 까는것은 불필요하다.
<img src="https://velog.velcdn.com/images/ss-won/post/5ff6e66b-7b35-49c9-b671-d8d2ecc8f9e7/image.png" alt=""></li>
</ul>
<ol start="3">
<li>declare module로 인식 못하는 모듈을 선언해준다. 또는 직접적으로 사용하려는 type을 import 해준다.</li>
</ol>
<ul>
<li>이게 근데 또 해당 에러가 발생하는 파일 즉, declaration file 인식을 못하는 파일에 죄다 추가를 해줘야한다. 매우 불편하다.</li>
<li>자체 d.ts 파일을 만들어서 declare module 해봐도 전역으로 적용이 안된다..(왜..???)</li>
</ul>
<pre><code class="language-ts">import { queryOptions } from &#39;@tanstack/react-query&#39;;
import type * as _T from &#39;@tanstack/react-query&#39;; // 이 부분 추가

export type User = {
  access_token: string;
  email: string;
  marketing: boolean;
};

const fetcher = (...args: Parameters&lt;typeof fetch&gt;) =&gt; fetch(...args).then(res =&gt; res.json());

export const workspaceQueryOptions = (name: string) =&gt;
  queryOptions({
    queryKey: [&#39;workspace&#39;, name],
    queryFn: () =&gt; fetcher(`/api/settings/${name}`),
    staleTime: 30 * 60 * 1000, // 30 min
  });

export const workspaceAPITokenQueryOptions = (name: string) =&gt;
  queryOptions({
    queryKey: [&#39;workspace&#39;, name, &#39;apiToken&#39;],
    queryFn: () =&gt; fetcher(`/api/workspace/${name}/api_token`),
    staleTime: Infinity,
  });
export const userQueryOptions = () =&gt;
  queryOptions&lt;User&gt;({
    queryKey: [&#39;auth&#39;],
    queryFn: () =&gt; fetcher(&#39;/api/account&#39;),
    staleTime: 10 * 60 * 1000, // 10 min
  });</code></pre>
<ol start="4">
<li>tsconfig.json paths에 인식못하는 module path에 대해 올바른 경로를 등록하기.</li>
</ol>
<ul>
<li><p>현재 문제가 되는 TypeScript path 인식은 react-query package에서 import하는 query-core와  .types의 경로였다. 그래서 문제 메세지가 원하는대로 path mapping을 지정해봤다. 그러니까 전역적으로 동작했다.</p>
</li>
<li><p>근데 이 방법도 매번 다른 dependencies type을 못찾을때마다 등록해야하는 점 + <code>.types</code> 처럼 일반적인 이름에 paths mapping을 걸면 해당 명칭을 쓸 수 없는 점 때문에 적절하지 않다고 판단했다.</p>
<pre><code class="language-js">{
&quot;extends&quot;: &quot;tsconfig/vite.json&quot;,
&quot;compilerOptions&quot;: {
  &quot;target&quot;: &quot;es2022&quot;,
  &quot;useDefineForClassFields&quot;: true,
  &quot;lib&quot;: [&quot;esnext&quot;, &quot;DOM&quot;, &quot;DOM.Iterable&quot;],
  &quot;module&quot;: &quot;es2022&quot;,
  &quot;skipLibCheck&quot;: true,

  /* Bundler mode */
  &quot;moduleResolution&quot;: &quot;Bundler&quot;,
  &quot;allowImportingTsExtensions&quot;: true,
  &quot;resolveJsonModule&quot;: true,
  &quot;isolatedModules&quot;: true,
  &quot;noEmit&quot;: true,
  &quot;jsx&quot;: &quot;react-jsx&quot;,
  &quot;jsxImportSource&quot;: &quot;@emotion/react&quot;,

  /* Linting */
  &quot;strict&quot;: true,
  &quot;noUnusedLocals&quot;: true,
  &quot;noUnusedParameters&quot;: true,
  &quot;noFallthroughCasesInSwitch&quot;: true,

  &quot;baseUrl&quot;: &quot;.&quot;,
  &quot;paths&quot;: {
    &quot;./types&quot;: [&quot;node_modules/@tanstack/react-query/build/modern/types.d.ts&quot;], // 여기랑
    &quot;@tanstack/core/*&quot;: [&quot;../../node_modules/.pnpm/@tanstack+query-core@5.22.2/node_modules/@tanstack/query-core/*&quot;], // 여기를 추가
    &quot;@/*&quot;: [&quot;./src/*&quot;]
  },
  &quot;skipDefaultLibCheck&quot;: true
},
&quot;include&quot;: [&quot;src&quot;],
&quot;exclude&quot;: [&quot;node_modules&quot;, &quot;dist&quot;],
&quot;references&quot;: [{ &quot;path&quot;: &quot;./tsconfig.node.json&quot; }]
}
</code></pre>
</li>
</ul>
<pre><code>5. **tsconfig.json declaration(+declarationMap)을 false로 주기.**
- declaration이 true인 경우에 해당 에러가 나온다는 코멘트도 있어서(in vite) false 옵션을 지정해주었다.
- 처음에 declaration만 false로 주니 또 요상한 에러가 나오길래 declarationMap까지 일괄로 false로 주니까 에러가 사라졌다. TypeScript 최신에서만 그러는 것 같다. 5.2, 5.0에서는 이런일이 없었다. (참고로 해당 프로젝트 typescript version 5.3.3)
- declaration은 declaration file(d.ts)을 만들 것인지의 여부이고 declarationMap은 원본 .ts파일로 연결되는 sourceMap을 만들것인지의 여부를 설정하는 것이다.
- 정확히 왜 고쳐졌는지 100% 이해는 안되지만...? 에러가 난 부분이 특정 라이브러리 모듈을 import해서 custom 함수 및 모듈을 만들었을때 ReturnType을 추론해낼 수 없다 것이었는데 이는 관련된 라이브러리 type path 인식을 못해 적절한 declaration file을 만들 수 없어서 발생하는 에러인 것 같다. 해당 bundler module 형식의 ts(x) 파일들에 d.ts expose를 제한하면서 react-query와 관련된 type declaration도 내보내질 필요가 없으니 오류가 사라진 것으로 추정하고 있다.
- 나는 일단 해당 앱을 특별히 js 라이브러리로 만들 예정이 없고 단일 앱으로 쓸 것이라 특별히 declaration file이 필요없기 때문에 일단 해당 방식을 사용하기로 했다.

![](https://velog.velcdn.com/images/ss-won/post/87a3e4c7-6a93-417b-98da-e0bb0c74ff49/image.png)

```js
{
  &quot;extends&quot;: &quot;tsconfig/vite.json&quot;,
  &quot;compilerOptions&quot;: {
    &quot;target&quot;: &quot;es2022&quot;,
    &quot;useDefineForClassFields&quot;: true,
    &quot;lib&quot;: [&quot;esnext&quot;, &quot;DOM&quot;, &quot;DOM.Iterable&quot;],
    &quot;module&quot;: &quot;es2022&quot;,
    &quot;skipLibCheck&quot;: true,

    /* Bundler mode */
    &quot;moduleResolution&quot;: &quot;Bundler&quot;,
    &quot;declaration&quot;: false,
    &quot;declarationMap&quot;: false,
    &quot;allowImportingTsExtensions&quot;: true,
    &quot;resolveJsonModule&quot;: true,
    &quot;isolatedModules&quot;: true,
    &quot;noEmit&quot;: true,
    &quot;jsx&quot;: &quot;react-jsx&quot;,
    &quot;jsxImportSource&quot;: &quot;@emotion/react&quot;,

    /* Linting */
    &quot;strict&quot;: true,
    &quot;noUnusedLocals&quot;: true,
    &quot;noUnusedParameters&quot;: true,
    &quot;noFallthroughCasesInSwitch&quot;: true,

    &quot;baseUrl&quot;: &quot;.&quot;,
    &quot;paths&quot;: {
      &quot;@/*&quot;: [&quot;./src/*&quot;]
    },
    &quot;skipDefaultLibCheck&quot;: true
  },
  &quot;include&quot;: [&quot;src&quot;],
  &quot;exclude&quot;: [&quot;node_modules&quot;, &quot;dist&quot;],
  &quot;references&quot;: [{ &quot;path&quot;: &quot;./tsconfig.node.json&quot; }]
}</code></pre><p>오늘의 결론은..버전은 함부로 올리지말자..?</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[turbo monorepo eslint custom config update]]></title>
            <link>https://velog.io/@ss-won/%EC%98%A4%EB%8A%98%EC%9D%98-%EC%BD%94%EB%94%A9-turbo-monorepo-eslint-custom-config-update</link>
            <guid>https://velog.io/@ss-won/%EC%98%A4%EB%8A%98%EC%9D%98-%EC%BD%94%EB%94%A9-turbo-monorepo-eslint-custom-config-update</guid>
            <pubDate>Thu, 22 Feb 2024 10:52:41 GMT</pubDate>
            <description><![CDATA[<p>오늘 코딩한 내용은 사실 별건 없고, next랑 default로 plugin setting을 나누는 작업을 했다. 근데 히스토리를 좀 정리해두면 좋을 것 같아 기록을 남겨본다.</p>
<p>이미 Monorepo를 생성할 때부터 아래 가이드를 보고 eslint config custom package세팅을 해두긴 했었다. 참고로 링크는 이미 최신화가 되서 예전이랑 다른 내용이다. 초기 turbo example을 보고 세팅했었기 때문에, commit 내용 추적해보면 원래 파일과 같이 세팅되어 있는 것을 찾을 수 있을 것이다.
<a href="https://turbo.build/repo/docs/handbook/linting/eslint">https://turbo.build/repo/docs/handbook/linting/eslint</a></p>
<p>원래의 파일(packages/eslint-config-custom/index.js)은 아래와 같았다.</p>
<pre><code class="language-js">module.exports = {
  parser: &#39;@typescript-eslint/parser&#39;,
  parserOptions: {
    ecmaVersion: &#39;latest&#39;,
    sourceType: &#39;module&#39;,
  },
  plugins: [&#39;@typescript-eslint&#39;],
  extends: [&#39;eslint:recommended&#39;, &#39;prettier&#39;, &#39;turbo&#39;, &#39;plugin:@typescript-eslint/recommended&#39;],
  rules: {},
  env: {
    browser: true,
  },
  ignorePatterns: [&#39;node_modules&#39;, &#39;.next&#39;, &#39;.github&#39;, &#39;*.config.js&#39;, &#39;*.js&#39;],
};</code></pre>
<p>이것이 구 eslint 버전에서는 잘 동작하고 있긴 했는데...?</p>
<p>프로젝트 도중 yarn(not berry..) -&gt; pnpm 으로 패키지 매니저를 바꾸면서 모듈의 dependencies 구조도 바뀌었다. 그에 따라 유령 모듈 사용이 방지되어 그동안 잘못 참조하고 있었던 모듈들을 정리하게 되었다. 오늘의 코딩과 관련된 내용은 아래와 같다.(사실 이 에피소드는 좀 오래되서 사실 가물가물한데 요인이 아마 2가지 때문이라 예상한다..)</p>
<ul>
<li>eslint-config-custom을 apps/* 프로젝트 devDepencies에 모두 추가해주고 re-install 해주었다.</li>
<li>새롭게 추가된 프로젝트에 eslint 최신판이 깔렸다.
commit을 하려고 보니..? 왠걸 갑자기 husky pre-commit 단계에서 lint를 하는데 계속
<code>ESLint couldn&#39;t determine the plugin &quot;@typescript-eslint&quot; uniquely.</code> 에러를 뱉어냈다.</li>
<li>-resolve-plugins-relative-to 옵션을 주면 된다는데...그렇게 줘도 해결되지 않았다. 그래서 eslint-custom을 사용하는 app 마다 @typescript-eslint plugin을 깔아주면 일시적 해결은 되었는데 사용하는 패키지와 앱마다 모든 파서를 다 깔아줄거면 공통 config 모듈을 쓰는 의미가 약간 퇴색되는 것 같았다.</li>
</ul>
<p>(*유령 모듈 : 실제 app, package dependency에는 선언하지 않았기 때문에 사실 명시되지 않은 모듈을 사용하려고 하면 오류가 나야 정상이지만 root 경로 node_modules에 모두 install 되어 hosting 되다보니 오류가 나지 않는 뭐 그런 현상으로 알고있다.) </p>
<p>그래서 turbo 공식문서 eslint setting을 다시 찾아보았고, 업데이트 된 것을 확인했다.
<a href="https://github.com/vercel/turbo/pull/5812">https://github.com/vercel/turbo/pull/5812</a>
그래서, 가이드 대로 parserOptions에 tsconfigRoot경로와 project를 설정해주었다. project는 여러 프로젝트를 사용하는 monorepo, 그리고 각 app마다 tsconfig를 사용하는 경우 사용할 tsconfig 프로젝트를 지정할 수 있는 속성인 것 같고, tsconfigRoot는 project에 설정한 tsconfig 파일 위치를 custom plugin기준으로 상대경로를 만들어낼 수 있도록 설정해주는 듯 하다.</p>
<pre><code class="language-js">const { resolve } = require(&#39;node:path&#39;);
console.log(__dirname); // 이건 그냥 콘솔찍어 보려고 적은것..ㅎㅎ

const project = [&#39;tsconfig.json&#39;, &#39;tsconfig.node.json&#39;].map(f =&gt; resolve(process.cwd(), f));
module.exports = {
  parser: &#39;@typescript-eslint/parser&#39;,
  parserOptions: { tsconfigRootDir: __dirname, project },
  extends: [&#39;eslint:recommended&#39;, &#39;prettier&#39;, &#39;eslint-config-turbo&#39;, &#39;plugin:@typescript-eslint/recommended&#39;, &#39;plugin:react-hooks/recommended&#39;],
  env: {
    browser: true,
    node: true,
  },
  globals: {
    React: true,
    JSX: true,
  },
  ignorePatterns: [&#39;node_modules&#39;, &#39;dist&#39;, &#39;.next&#39;, &#39;.github&#39;, &#39;*.config.js&#39;, &#39;*.js&#39;],
  overrides: [{ files: [&#39;*.js?(x)&#39;, &#39;*.ts?(x)&#39;] }],
  plugins: [&#39;@typescript-eslint&#39;, &#39;react&#39;, &#39;react-hooks&#39;, &#39;import&#39;, &#39;react-refresh&#39;],
  rules: {
    &#39;@typescript-eslint/no-unused-vars&#39;: &#39;warn&#39;,
    &#39;@typescript-eslint/no-explicit-any&#39;: &#39;warn&#39;,
    &#39;react-refresh/only-export-components&#39;: [&#39;warn&#39;, { allowConstantExport: true }],
  },
};</code></pre>
<p>이렇게 하니, 해결은 되었고 오늘 신규 app project 세팅을 하면서 eslint를 다시보니 next app은 그동안 eslint-config-custom과 next/core-web-vitals를 확장해서 쓰고 있었는데, 어차피 대체로 모든 next에 같은 설정을 하다보니 모듈 설치도 덜어낼겸 next 전용 플러그인을 분리해야겠다는 생각이 들었다.</p>
<pre><code class="language-js">const { resolve } = require(&#39;node:path&#39;);
console.log(__dirname);
const project = [&#39;tsconfig.json&#39;, &#39;tsconfig.node.json&#39;].map(f =&gt; resolve(process.cwd(), f));

module.exports = {
  parser: &#39;@typescript-eslint/parser&#39;,
  parserOptions: { tsconfigRootDir: __dirname, project, sourceType: &#39;module&#39; },
  extends: [&#39;eslint:recommended&#39;, &#39;prettier&#39;, &#39;eslint-config-turbo&#39;, &#39;plugin:@typescript-eslint/recommended&#39;, &#39;next/core-web-vitals&#39;],
  env: {
    browser: true,
    node: true,
  },
  globals: {
    React: true,
    JSX: true,
  },
  ignorePatterns: [&#39;node_modules&#39;, &#39;dist&#39;, &#39;.next&#39;, &#39;.github&#39;, &#39;*.config.js&#39;, &#39;*.js&#39;],
  overrides: [{ files: [&#39;*.js?(x)&#39;, &#39;*.ts?(x)&#39;] }],
  plugins: [&#39;@typescript-eslint&#39;],
  rules: {
    &#39;@typescript-eslint/no-unused-vars&#39;: &#39;warn&#39;,
    &#39;@typescript-eslint/no-explicit-any&#39;: &#39;warn&#39;,
  }
};</code></pre>
<p>사실 이 파일도 문서 난독 이슈로 몇가지 시행착오가 있었는데, 그냥 create-next-app으로 생성하면 만들어지는 eslint 파일 설정만 고대로 잘 옮겨오면 금방 작성할 수 있다...ㅋ_ㅋ..</p>
<p><code>eslint-config-next</code>라는 패키지에는 eslint-plugin-react, eslint-plugin-react-hooks, eslint-plugin-next 플러그인을 사용하여, 권장되는 rules를 설정하였다고 되어있다. 근데 이 부분을 안읽고 엄한곳을 읽는 바람에.. eslint-plugin-next 단일로 깔고 거기에 없는 rule이 설정된 next config를 extends했더니 계속 플러그인 못찾는다는 에러만 겁나 나서(당연하다.....) 오류가 말하는 플러그인 다 깔아주고 rule을 설정하는 불상사를 겪었다. 굳이 세밀하게 customize할게 아니라면 그냥 eslint-config-next 깔아주고 config extends 해주기만 하면 된다. 🫠
<img src="https://velog.velcdn.com/images/ss-won/post/df20053b-c292-44ad-a2ae-0f65c797dd4f/image.png" alt=""></p>
<p>저렇게 설정해두고, next app 내부 .eslintrc.js에는 아래와 같이 custom/next로 설정해주면 되고, 다른 앱(나의 경우는 vite)에는 next 설정을 쓸게 아니니까 [&#39;custom&#39;]으로 설정해주었다.</p>
<pre><code class="language-js">module.exports = {
  root: true,
  extends: [&#39;custom/next&#39;],
  rules: {
    &#39;react/no-unescaped-entities&#39;: &#39;off&#39;,
    &#39;@next/next/no-page-custom-font&#39;: &#39;off&#39;,
  },
};</code></pre>
<p>오늘의 코딩 결론은 문서를 좀 잘읽자...</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[프론트엔드 면접 문제 은행 문답 챌린지 - 네트워크편]]></title>
            <link>https://velog.io/@ss-won/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EB%A9%B4%EC%A0%91-%EB%AC%B8%EC%A0%9C-%EC%9D%80%ED%96%89-%EB%AC%B8%EB%8B%B5-%EC%B1%8C%EB%A6%B0%EC%A7%80-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC%ED%8E%B8</link>
            <guid>https://velog.io/@ss-won/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EB%A9%B4%EC%A0%91-%EB%AC%B8%EC%A0%9C-%EC%9D%80%ED%96%89-%EB%AC%B8%EB%8B%B5-%EC%B1%8C%EB%A6%B0%EC%A7%80-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC%ED%8E%B8</guid>
            <pubDate>Tue, 09 Jan 2024 08:04:49 GMT</pubDate>
            <description><![CDATA[<h4 id="related-documents">Related Documents</h4>
<p><a href="https://h5bp.org/Front-end-Developer-Interview-Questions/translations/korean/#%EC%BD%94%EB%94%A9-%EA%B4%80%EB%A0%A8-%EC%A7%88%EB%AC%B8">프론트엔드 면접 문제 은행</a></p>
<h2 id="네트워크-관련-질문">네트워크 관련 질문</h2>
<p>Q. 전통적으로, 웹사이트의 assets을 여러 도메인으로 서빙했을 때 장점은 무엇인가요?</p>
<ul>
<li>일단 서버 하나당 1개의 도메인을 적용한다고 가정하면, 여러 도메인(서버)으로 트래픽을 분산할 수 있기 때문에 전반적으로 assets 다운로드 속도가 빨라집니다. 그밖에 브라우저 측면에서는 쿠키, 캐시 등의 web storage는 도메인 단위로 분산하여 저장하기 때문에 저장하는 key-value값이 늘어날수록 Request/Response header의 콘텐츠가 상대적으로 단일 도메인일때에 비해 가벼울 가능성이 높으며 key 충돌이 날 가능성이 더 적다는 장점이 있습니다.</li>
</ul>
<p>Q. URL로 접속했을 때 어떤 플로우로 화면에 웹사이트가 그려지는지 네트워크 관점에서 설명해주세요.</p>
<ul>
<li>URL로 접속하면 브라우저는 DNS를 조회해 실제 서버가 위치하는 IP 주소를 찾고 서버에 접속을 시도합니다. 웹 서버와의 안전한 통신을 위해 SSL/TLS handshake 과정을 거쳐 HTTP 요청을 서버에게 보냅니다. 브라우저는 해당 Request에 대한 Response와 리소스를 받습니다.</li>
<li>이후에는 렌더링 과정(HTML 파싱 -&gt; DOM Tree 생성 -&gt; CSS 파싱 -&gt; CSS와 DOM 요소를 합쳐 렌더트리 생성 -&gt; 렌더트리 기반으로 UI 요소의 페인팅 위치와 레이아웃을 설정 -&gt; 페인트 과정을 통해 실제 화면을 렌더링 -&gt; 화면을 갱신하여 렌더링 된 사이트를 표시)과 자바스크립트 실행 단계를 거쳐 화면에 웹사이트가 표시됩니다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/ss-won/post/5e0c584c-8285-4bc0-b24e-3916018c8048/image.png" alt="">
*<em>SSL/TLS handshake *</em>
<code>출처</code> <a href="https://www.imperva.com/learn/performance/cdn-and-ssl-tls/">https://www.imperva.com/learn/performance/cdn-and-ssl-tls/</a></p>
<p>Q. Long-Polling과 Websocket, Server-Sent Event에 대해 설명해주세요.
모두 실시간 리소스 업데이트가 필요한 곳에서 사용되는 기술입니다.</p>
<ul>
<li><strong>Long-Polling</strong> : 클라이언트가 HTTP를 통해 요청을 보내면 서버가 새로운 정보를 보낼때까지 클라이언트의 요청을 지속적으로 유지하는 형태입니다. (양방향 통신)</li>
<li><strong>WebSocket</strong> : 실시간 통신을 위해 사용하는 기술로 주로 채팅과 같은 실시간 서비스에 사용됩니다. 서버와 클라이언트에 모두 설정해야 하고 양방향 통신을 지원합니다. HTTP 프로토콜이 아닌 WebSocket 자체 프로토콜을 사용합니다.</li>
<li><strong>Server-Sent Event</strong> : HTTP를 이용해 서버에서 클라이언트로 실시간 이벤트를 전송하는 기술입니다.(단방향 통신) 클라이언트 단 이벤트 스트림에 서버가 이벤트를 추가하는 방식입니다.</li>
</ul>
<p>Q. 다음 request header들에 대해 설명해주세요.</p>
<ul>
<li>Diff. between Expires, Date, Age and If-Modified-Since : 순서대로 응답 메세지가 최신 상태인지 아닌지를 판단할 수 있는 기준 시간(Expires), 요청/응답 메세지가 만들어진 시간(Date), 프록시 캐시내에 머무는 초 단위의 시간(Age), 조건부 요청에 쓰이는 요소(If-Modified-Since)로 해당 기준 시간 이후에 수정이 된 경우에만 200과 변경된 리소스를 보내고 그 이외의 케이스에는 리소스 없이 304를 반환하도록 제어할 수 있습니다.</li>
<li>Do Not Track : DNT(Deprecated). 접근 기록을 추적할 것인지의 여부를 표시하는 요소입니다. (이 질문지 자체가 생긴지 오래되서 그런지 예전 기술도 많은듯. MDN에서 deprecated 되었다고 표시되어 있다. 해당 요소가 표시되더라도 추적되지 않는다는 보장이 없고 단순히 추적을 원하지 않는다의 표식만 해주고 있는 것 같음)</li>
<li><strong>Cache-Control</strong> : HTTP 요청과 응답 내의 캐시 메커니즘을 제어하는 요소입니다. 불필요한 추가 서버 요청을 방지합니다.</li>
<li>Transfer-Encoding : 안전한 엔티티 전송을 위한 인코딩 여부를 제어하는 요소입니다. 콘텐츠 자체를 인코딩하는 것은 아니며 HTTP/1에서 콘텐츠의 크기를 알기 위함이나 보안적인 방식으로 사용했습니다. HTTP/2에서는 오직 chunk 요소로 전송되었는지를 확인하는 용도로만 사용하고 있습니다.</li>
<li><strong>ETag</strong> : HTTP 콘텐츠가 변환되었는지를 확인하는 요소로 특정 버전의 리소스 식별자입니다. 같은 주소로 요청했더라도 요청한 리소스의 콘텐츠가 달라지면 다른 ETag값을 가지게 됩니다. If-None-Match라는 헤더 요소에 비교하고자하는 Etag를 요청에 설정하여 캐시 리소스가 stale한지 판단하여 캐시 사용 여부를 판단할 수 있습니다.</li>
<li>X-Frame-Options : 해당 리소스가 frame, iframe, object에서 렌더링 될 수 있는지를 판단하는 요소입니다. <a href="https://portswigger.net/web-security/clickjacking">clickjacking 공격</a>(css 코드를 제어하여 target iframe layer 아래에 악성 코드를 심는 공격)을 방지하는데 이용됩니다. 모든 frame 거부, 동일 origin에서만 허용, 특정 지정 url 허용 등의 옵션을 줄 수 있습니다.</li>
</ul>
<p>Q. HTTP와 HTTPS에 대해 설명해주세요.</p>
<ul>
<li>HTTP와 HTTPS는 웹에서 정보를 전송하는데 사용되는 프로토콜입니다.</li>
<li>HTTP는 무연결성, 무상태성의 특징을 가집니다. 무연결성은 계속 연결되어 있는 형태가 아니라는 뜻으로 클라이언트가 서버에 요청이 필요한 상황에서만 연결하여 리소스를 주고받습니다. 지속적으로 연결되어 있는 상태가 아니기 때문에 서버의 부담이 적습니다. 무상태성은 이전 요청에 대해 상태 정보를 저장하지 않기 때문에 각 요청이 독립적으로 처리됩니다. </li>
<li>HTTPS는 HTTP에 SSL or TLS 보안 계층이 추가된 형태입니다. HTTP는 통신할 때 body 평문 내용을 암호화 없이 전달해 중간에 코드를 가로채는 중간자 공격에 취약하지만 HTTPS는 기본적으로 두 개의 서로 다른 키로 통신을 암호화하기 때문에 상대적으로 안전합니다.(모든 보안 상황에 안전한 것은 아니다) </li>
</ul>
<p>Q. HTTP Method들에 대해 설명해주세요.</p>
<ul>
<li>HTTP Method에는 GET, POST, PUT, PATCH, DELETE, OPTIONS, CONNECT, TRACE, HEAD가 있습니다. 통상적으로 Restful API에서는 GET(Read) - 리소스 읽기, POST(Create) - 리소스 쓰기, PATCH &amp; PUT(Update) - 리소스 업데이트 PUT은 새로운 리소스 추가 또는 일괄 업데이트 PATCH는 일부 업데이트로 쓰이는 편, DELETE(Delete) - 리소스 삭제으로 쓰입니다.</li>
<li>OPTION은 주어진 URL에서 허용된 통신 옵션(웹서버에 설정한 옵션)을 요청합니다. CORS 사전 요청으로 많이 사용됩니다.</li>
<li>CONNECT는 양방향 연결을 시작하는 메소드로, SSL/TLS 웹 사이트에 연결을 시도할때 해당 메소드를 사용할 수 있습니다.</li>
<li>HEAD는 GET    요청에 대한 응답에서 내보낼 헤더를 요청하는 메소드입니다. 응답은 Body 없이 오직 Header 값만을 가집니다. 만일 HEAD 요청에서 이전 캐시된 GET 응답이 유효하지 않다는 응답이 오면, 추가적인 GET 요청없이 기존 캐시를 무효화합니다.</li>
<li>TRACE는 클라이언트와 서버 사이 loop-back 테스트 및 진단 목적으로 만들어진 메소드로 응답 메세지 콘텐츠가 없이 단순히 수신한 메세지들을 echo 합니다. via HTTP 헤더의 경우 서버 요청시 거쳐간 프록시 서버에 대한 결과까지 포함되어 있습니다. 프록시 무한루프를 체크하는데 유용하게 사용됩니다.
<img src="https://velog.velcdn.com/images/ss-won/post/31459efa-e09c-441f-b057-2712551c9b40/image.png" alt=""></li>
</ul>
<h3 id="함께-읽으면-좋을-글">함께 읽으면 좋을 글</h3>
<p><a href="https://web.dev/articles/http-cache?hl=ko">HTTP 캐시로 불필요한 네트워크 요청 방지</a>
<a href="https://www.zerocho.com/category/HTTP/post/5b594dd3c06fa2001b89feb9">알아둬야 할 HTTP 쿠키 &amp; 캐시 헤더</a>
<a href="https://http.dev/methods">HTTP Methods</a>
<a href="https://ko.javascript.info/long-polling">Long Polling</a>
<a href="https://ko.javascript.info/server-sent-events">Server Sent Events</a>
<a href="https://surviveasdev.tistory.com/entry/%EC%9B%B9%EC%86%8C%EC%BC%93-%EA%B3%BC-SSEServer-Sent-Event-%EC%B0%A8%EC%9D%B4%EC%A0%90-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B3%A0-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B3%B4%EA%B8%B0">웹소켓 과 SSE(Server-Sent-Event) 차이점 알아보고 사용해보기</a>
<a href="https://www.imperva.com/learn/performance/cdn-and-ssl-tls/">CDN and SSL/TLS</a>
<a href="https://travishorn.com/why-it-is-better-to-serve-site-assets-from-multiple-domains-972a2bf69d71">Why it is better to serve site assets from multiple domains</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[프론트엔드 면접 문제 은행 문답 챌린지 - JS코딩편]]></title>
            <link>https://velog.io/@ss-won/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EB%A9%B4%EC%A0%91-%EB%AC%B8%EC%A0%9C-%EC%9D%80%ED%96%89-%EB%AC%B8%EB%8B%B5-%EC%B1%8C%EB%A6%B0%EC%A7%80-JS%EC%BD%94%EB%94%A9%ED%8E%B8</link>
            <guid>https://velog.io/@ss-won/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EB%A9%B4%EC%A0%91-%EB%AC%B8%EC%A0%9C-%EC%9D%80%ED%96%89-%EB%AC%B8%EB%8B%B5-%EC%B1%8C%EB%A6%B0%EC%A7%80-JS%EC%BD%94%EB%94%A9%ED%8E%B8</guid>
            <pubDate>Fri, 05 Jan 2024 04:48:02 GMT</pubDate>
            <description><![CDATA[<h4 id="related-documents">Related Documents</h4>
<p><a href="https://h5bp.org/Front-end-Developer-Interview-Questions/translations/korean/#%EC%BD%94%EB%94%A9-%EA%B4%80%EB%A0%A8-%EC%A7%88%EB%AC%B8">프론트엔드 면접 문제 은행</a></p>
<h2 id="js-코딩-관련-질문">JS 코딩 관련 질문</h2>
<p>⭕️ Q. foo의 값은 무엇인가요?</p>
<pre><code class="language-js">var foo = 10 + &#39;20&#39;;</code></pre>
<ul>
<li>&#39;1020&#39; : 산술연산시 문자열이 숫자보다 우선적으로 취급되기 때문에 문자열로 형변환된다. 
<img src="https://velog.velcdn.com/images/ss-won/post/55c9fa54-be1f-4142-8e2a-9a465ecc2302/image.png" alt=""></li>
</ul>
<p>❌ Q. 아래 코드의 결과값은 무엇인가요? -&gt; 처음에 true라고 해서 틀림 👀</p>
<pre><code class="language-js">console.log(0.1 + 0.2 == 0.3);</code></pre>
<ul>
<li>false : 비교 연산자보다, 수식 연산자의 우선순위가 더 높아 ((0.1 + 0.2) == 0.3) 형태로 연산이 진행된다. 0.1+0.2실 실제 window console 창에서 진행해보면 0.3이 나오지 않는다. <img src="https://velog.velcdn.com/images/ss-won/post/b4e5c6de-d718-4c1d-aef6-d081430d51b5/image.png" alt="">
이는 컴퓨터의 2진법 연산 때문인데, 2진법 변환중 소수를 만나게되면 무한소수가 되는 케이스가 생기고 이때문에 미세한 계산 오차가 발생한다. 따라서 (0.1 + 0.2) == 0.3은 false가 된다.</li>
</ul>
<p>⭕️ Q. 아래 코드가 동작하게 하기 위해선 어떻게 해야할까요?</p>
<pre><code class="language-js">add(2, 5); // 7
add(2)(5); // 7</code></pre>
<ul>
<li>function add(a, b) { return a+b };</li>
<li>function add(a) { return function (b) { return a+b; } };
<img src="https://velog.velcdn.com/images/ss-won/post/68af72c0-ad60-4e32-96e0-f80cc4bc3c5d/image.png" alt=""></li>
</ul>
<p>⭕️ Q. 아래 구문의 반환값은 무엇인가요?</p>
<pre><code class="language-js">&quot;i&#39;m a lasagna hog&quot;.split(&quot;&quot;).reverse().join(&quot;&quot;);</code></pre>
<ul>
<li>&quot;goh angasal a m&#39;i&quot; : 배열로 쪼갠 후 -&gt; 순서를 뒤바꾸고 -&gt; 다시 문자열로 합쳤다.
<img src="https://velog.velcdn.com/images/ss-won/post/dd1fa3c7-a4f1-4014-b451-b180a8e0bf3d/image.png" alt=""></li>
</ul>
<p>⭕️ Q. What is the value of window.foo? - window.foo의 값은 무엇인가요?</p>
<pre><code class="language-js">( window.foo || ( window.foo = &quot;bar&quot; ) );</code></pre>
<ul>
<li>&quot;bar&quot; : 괄호 내부 연산이 먼저 발생하여 window.foo에 &quot;bar&quot;가 할당된 후 식은 -&gt; (window.foo || undefined) 이렇게 변하고 -&gt; 최종 연산을 거쳐 -&gt; window.foo가 남는다. 이 변수의 값은 맨 처음 할당된 값 &quot;bar&quot;이다.
<img src="https://velog.velcdn.com/images/ss-won/post/cd8852a8-a4fc-461c-9dae-2f34838b132d/image.png" alt=""></li>
</ul>
<p>🔺 Q. 아래 두 alert의 결과값은 무엇인가요? -&gt; 두 번째 alert Hello undefined로 해서 반만 맞음</p>
<pre><code class="language-js">var foo = &quot;Hello&quot;;
(function() {
  var bar = &quot; World&quot;;
  alert(foo + bar); // 1
})();
alert(foo + bar); // 2</code></pre>
<ul>
<li>alert 1은 &quot;Hello World&quot;, alert 2는 참조에러가 발생한다. : foo는 전역변수이지만, bar는 지역변수이고, 즉시실행함수에 의해 호출되고 사라진다. 따라서 두 번째 alert이 실행될때는 bar는 선언되지 않으 변수이다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/ss-won/post/a39f2287-2b26-40be-a670-e7b66cf98038/image.png" alt="">
<img src="https://velog.velcdn.com/images/ss-won/post/7ab89720-f3d0-4fdb-b053-3e1b42134c12/image.png" alt=""></p>
<p>⭕️ Q. foo.length의 값은 무엇인가요?</p>
<pre><code class="language-js">var foo = [];
foo.push(1);
foo.push(2);</code></pre>
<ul>
<li>2 : 배열 내장 함수 push는 배열에 값을 append 시킨다. 최종적으로 foo는 [1,2]가 되어 length 속성은 2이다. 
<img src="https://velog.velcdn.com/images/ss-won/post/ceb33b26-4a14-40c6-a76b-65706515aa7c/image.png" alt=""></li>
</ul>
<p>❌ Q. foo.x의 값은 무엇인가요?</p>
<pre><code class="language-js">var foo = {n: 1};
var bar = foo;
foo.x = foo = {n: 2};</code></pre>
<ul>
<li>undefined : foo는-&gt; {n:2}로 답변해서 틀렸음. 
object는 참조가 할당되는 것으로
1번 줄에서 foo에 {n:1}이 할당되면, 객체 {n:1}이 저장된 메모리 영역을 foo라는 변수가 참조하게 된다.
<img src="https://velog.velcdn.com/images/ss-won/post/645245b3-0894-4265-b11f-875c051eab17/image.png" alt="">
2번 줄에서 bar에 foo를 할당한다는 것은 즉 foo가 현재 참조하고 있는 메모리 참조를 할당하는 것으로 그림과 같이 할당이 된다.
<img src="https://velog.velcdn.com/images/ss-won/post/29b68044-2db7-42c7-bc2c-31bba8925e0e/image.png" alt="">
3번줄에서 일단 변수 foo.x는 {n:1}이 저장된 메모리 영역을 참조하고 있다.
<img src="https://velog.velcdn.com/images/ss-won/post/0d2e28b1-f528-43a5-a4a6-5556edcd1c73/image.png" alt="">
이후 할당 연산이 우측 -&gt; 좌측 순서로 진행된다.
첫 번째 할당으로 foo가 바라보는 객체 참조가 바뀐다. 단, 이때 foo.x는 할당 연산전의 메모리 영역을 참조하고 있다.
<img src="https://velog.velcdn.com/images/ss-won/post/7a55c6a9-c91c-460a-9c4c-4e3593fcab26/image.png" alt="">
두 번째 할당에서는 foo.x(foo가 바뀌기 전 참조 영역이자, bar가 여전히 참조하고 있는 메모리 영역)의 property에 foo 참조를 할당한다.
<img src="https://velog.velcdn.com/images/ss-won/post/ef4ba77c-1251-4d2d-b1e7-88e952658d1c/image.png" alt=""></li>
</ul>
<p>모든 연산이 끝나고 foo.x를 참조하면, 이때는 {n:2} 영역을 가리키고 있기 때문에 undefined 값을 반환한다. 그러나 bar를 출력해보면 {n:1, x: {n:2}}가 담긴것을 확인할 수 있다. 현재 bar가 참조하고 있는 영역의 x property는 foo가 참조하는 영역을 가리키고 있으므로 foo 객체 할당 연산을 해서 객체를 변환하면 x property는 해당 값을 반환하게된다.
<img src="https://velog.velcdn.com/images/ss-won/post/c9552f53-38c8-4125-94d7-ead40c3096a1/image.png" alt=""></p>
<p>⭕️ Q. 아래 코드의 출력값은 무엇인가요?</p>
<pre><code class="language-js">console.log(&#39;one&#39;);
setTimeout(function() {
  console.log(&#39;two&#39;);
}, 0);
console.log(&#39;three&#39;);</code></pre>
<ul>
<li>&#39;one&#39; -&gt; &#39;three&#39; -&gt; &#39;two&#39; : 비동기 함수는 바로 자바스크립트 콜스택에 넣어 실행되지 않고 스레드 풀에 넣어두었다가 지정된 시간 이후 callback queue로 이동해 콜스택에서 실행될 수 있는 환경이 되면 비로소 콜스택에 넣어져 실행된다.
<img src="https://velog.velcdn.com/images/ss-won/post/05761fda-9f4d-4ab5-b8e8-dcb4056100f6/image.png" alt="">
<img src="https://velog.velcdn.com/images/ss-won/post/8287c49a-135f-4a80-97b5-dc573b1e65f6/image.png" alt="">
<img src="https://velog.velcdn.com/images/ss-won/post/0c8a0573-6a31-4a9b-bdef-b4494e8fd0cb/image.png" alt="">
<img src="https://velog.velcdn.com/images/ss-won/post/b89995fc-05c7-4780-a69a-19b137107627/image.png" alt="">
<code>출처</code> <a href="https://www.javascripttutorial.net/javascript-bom/javascript-settimeout/">https://www.javascripttutorial.net/javascript-bom/javascript-settimeout/</a></li>
</ul>
<h3 id="같이보면-좋을-글">같이보면 좋을 글</h3>
<p><a href="https://medium.com/@sohnu/settimeout-with-time-0-what-does-it-really-mean-3b306880a0f6">setTimeout with time 0. What does it really mean?</a>
<a href="https://medium.com/nerd-for-tech/by-a-junior-for-a-beginner-the-call-stack-async-javascript-21a8f469974f">By A Junior, For A Beginner: The Call Stack &amp; Async Javascript</a>
<a href="https://www.javascripttutorial.net/javascript-bom/javascript-settimeout/">JavaScript setTimeout</a></p>
<blockquote>
<p>⚠️ 평소 지식을 기반으로 하되, 적절히 리서치해서 적는 내용으로 오류가 있을 수 있습니다. </p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[프론트엔드 면접 문제 은행 문답 챌린지 - TestCode편]]></title>
            <link>https://velog.io/@ss-won/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EB%A9%B4%EC%A0%91-%EB%AC%B8%EC%A0%9C-%EC%9D%80%ED%96%89-%EB%AC%B8%EB%8B%B5-%EC%B1%8C%EB%A6%B0%EC%A7%80-TestCode%ED%8E%B8</link>
            <guid>https://velog.io/@ss-won/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EB%A9%B4%EC%A0%91-%EB%AC%B8%EC%A0%9C-%EC%9D%80%ED%96%89-%EB%AC%B8%EB%8B%B5-%EC%B1%8C%EB%A6%B0%EC%A7%80-TestCode%ED%8E%B8</guid>
            <pubDate>Thu, 04 Jan 2024 07:54:39 GMT</pubDate>
            <description><![CDATA[<h4 id="related-documents">Related Documents</h4>
<p><a href="https://h5bp.org/Front-end-Developer-Interview-Questions/translations/korean/">프론트엔드 면접 문제 은행</a></p>
<h2 id="test-code-관련-질문">Test Code 관련 질문</h2>
<p>Q. Test code를 작성하면서 개발하는 방식의 장점과 단점에 대해 설명해주세요.</p>
<ul>
<li>테스트 기반 개발은 예상 시나리오 범위 안에서 코드가 제대로 작동한다는 것을 확인하는 과정이기 때문에 버그 원인과 경우의 수와 범위를 줄일 수 있어 소프트웨어의 신뢰성과 안정성을 높일 수 있고 유지보수 시에도 수정 내용이 적절히 동작하는지 확인할 수 있는 장점이 있고, 단점은 테스트 코드를 위한 추가적인 시간적/관리적 비용이 발생하고 오버 엔지니어링 될 가능성이 있으며 테스트 코드를 잘 작성하기 위한 러닝 커브가 존재한다는 점입니다.</li>
</ul>
<p>Q. 작성한 코드의 기능을 테스트하기 위해 어떤 툴을 사용할 수 있나요?</p>
<ul>
<li>JavaScript의 경우 jest라는 tool을 이용해 코드의 기능적인 테스트를 진행합니다. React Dom의 경우는 react-test-library와 jest를 사용하라고 공식문서에서 언급하고 있습니다. Frontend에서는 일반적으로 JavaScript 런타임 에러를 방지하기 위해 TypeScript와 eslint를 활용하여 정적 테스트를 수행하고 테스트로 취급합니다.</li>
</ul>
<p>Q. 유닛 테스트, 기능 테스트와 통합 테스트의 차이점은 무엇인가요?</p>
<ul>
<li>유닛 테스트(Unit Test) 가장 작은 함수/모듈 단위의 코드가 예상대로 잘 동작하는지 테스트하는 것으로 프론트에서는 컴포넌트 단위로 렌더링이 잘 되는지 props를 mocking하여 하위 child까지 잘 동작하는지 확인합니다.</li>
<li>통합 테스트(Integration Test)는 각 모듈 사이의 상호작용이 원활한지, 기능적 요구사항이 잘 동작하는지를 테스트하는 것으로 프론트에서는 컴포넌트의 state 변경이나 API 연동 렌더링이 적절하게 이루어지는지를 확인합니다. 특정 비즈니스 로직과 컴포넌트 사이의 연결성이 잘 이루어지는 지를 확인합니다.</li>
<li>기능 테스트(Funtional Test)는 사용자와 어플리케이션 사이의 상호작용이 원활한지 확인하는 테스트입니다. 프론트에서는 E2E 테스트를 통해 진행하며 실제 Web browser에서 사용자가 사용하는 것처럼 동작을 테스트 코드로 작성합니다.
<img src="https://velog.velcdn.com/images/ss-won/post/36d34d0b-e32d-43a5-9a0a-a34c1fabe608/image.png" alt="">
<code>출처</code> Katalon technical blog (<a href="https://katalon.com/resources-center/blog/unit-testing-vs-functional-testing">https://katalon.com/resources-center/blog/unit-testing-vs-functional-testing</a>)</li>
</ul>
<p>Q. Code style linting tool을 사용했을 때 장점은 무엇인가요?</p>
<ul>
<li>대표적인 JavaScript style linting tool은 eslint이고, 해당 tool의 장점은 코드의 일관성을 유지하여 유지보수가 용이하게 하고 예상치 못한 정적 런타임 에러를 사전에 잡아주기 때문에 코드 성능 또한 높일 수 있다는 점입니다.</li>
</ul>
<h3 id="같이-보면-좋을-내용들">같이 보면 좋을 내용들</h3>
<ul>
<li><a href="https://yozm.wishket.com/magazine/detail/1964/">요즘 IT - 테스트 코드는 왜 만들까?</a></li>
<li><a href="https://blog.mathpresso.com/%EB%AA%A8%EB%8D%98-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%A0%84%EB%9E%B5-1%ED%8E%B8-841e87a613b2">모던 프론트엔드 테스트 전략 — 1편(Testing Overview)</a></li>
<li><a href="https://katalon.com/resources-center/blog/unit-testing-vs-functional-testing">Unit Testing vs. Functional Testing: An In-Depth Comparison</a></li>
</ul>
<blockquote>
<p>⚠️ 평소 지식을 기반으로 하되, 적절히 리서치해서 적는 내용으로 오류가 있을 수 있습니다. </p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[python/bugs] opencv-python lib 설치 오류 해결하기]]></title>
            <link>https://velog.io/@ss-won/pythonbug-opencv-python-lib-%EC%84%A4%EC%B9%98-%EC%98%A4%EB%A5%98-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@ss-won/pythonbug-opencv-python-lib-%EC%84%A4%EC%B9%98-%EC%98%A4%EB%A5%98-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 17 Nov 2021 01:57:20 GMT</pubDate>
            <description><![CDATA[<h2 id="문제상황-🚨">문제상황 🚨</h2>
<p>VM linux 서버에서는 잘만 받아지던 파이썬 라이브러리 opencv-python이 macOS(Mojave 10.14.6)에서 다운받는 도중에 계속 <strong><code>Building wheels for collected packages</code></strong> 라는 문구에서 멈춰 install이 이루어지지 않는 문제가 있었다. 대충 찾아보니 버전 문제인것 같다고 한다. 괜히 요즘 도커같은 가상 컨테이너를 사용하는게 아니구나 싶다.
<img src="https://images.velog.io/images/ss-won/post/c9d6d46f-60ae-4684-be7a-3f7552ad2c9a/image.png" alt=""></p>
<h2 id="해결방법-🙋🏻♀️">해결방법 🙋🏻‍♀️</h2>
<p>pypi에서 version history를 보고 최신버전에서 하나하나씩 내려가서 될때까지 시도해보았다.ㅎㅎ
결론적으로 <code>4.5.1.48</code> 버전을 받아 해결할 수 있었다.</p>
<pre><code>pip install opencv-python==4.5.1.48</code></pre><p><a href="https://pypi.org/project/opencv-python/#history">https://pypi.org/project/opencv-python/#history</a></p>
<p><img src="https://images.velog.io/images/ss-won/post/cfda8c02-84c3-4517-8a71-908ea20e755f/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[git/기타] node-gitlab-2-github로 gitlab 프로젝트 github으로 이사하기]]></title>
            <link>https://velog.io/@ss-won/git%EA%B8%B0%ED%83%80-node-gitlab-2-github%EB%A1%9C-gitlab-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-github%EC%9C%BC%EB%A1%9C-%EC%9D%B4%EC%82%AC%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@ss-won/git%EA%B8%B0%ED%83%80-node-gitlab-2-github%EB%A1%9C-gitlab-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-github%EC%9C%BC%EB%A1%9C-%EC%9D%B4%EC%82%AC%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 18 Jul 2021 07:41:02 GMT</pubDate>
            <description><![CDATA[<h2 id="대충-서론에-해당하는-이야기">대충 서론에 해당하는 이야기</h2>
<p>최근 <code>엘리스</code>라는 교육프로그램이 끝나고 본격적으로 포트폴리오를 만들고, 리팩토링을 하기 위해 프로젝트를 gitlab -&gt; github으로 옮기는 작업을 하고 있다. 오늘은 옮기는 김에 문서화를 해놓으면 편할 것 같아서 블로그에 해당 작업에 대한 정리를 해보려고 한다.</p>
<h2 id="본론에-해당하는-이야기">본론에 해당하는 이야기</h2>
<h3 id="gitlab에서-github으로-repository를-이전하는-3가지-방법">gitlab에서 github으로 Repository를 이전하는 3가지 방법</h3>
<p>gitlab과 github은 모두 git을 기반으로 한 코드 관리 시스템이기 때문에 repository 이전을 할 수가 있다. 이전하는 방법은 mirror option을 주면 되는데 시행착오를 거치면서 옮기는 방법을 3가지 정도 알게 되었다.</p>
<blockquote>
<p><strong>1) git repository 자체를 mirror option을 주고 clone 받은 후에 github repository에 push하는 방법</strong></p>
</blockquote>
<blockquote>
</blockquote>
<p><strong>2) gitlab 자체에 내장된 mirroring repository를 이용하는 방법</strong>
<img src="https://images.velog.io/images/ss-won/post/d0b01c62-ba8f-4a1e-9a98-e3fd63914495/image.png" alt=""></p>
<blockquote>
<p>** 3) 외부 모듈 사용하는 방법(node-gitlab-2-github) **
<a href="https://github.com/piceaTech/node-gitlab-2-github">https://github.com/piceaTech/node-gitlab-2-github</a></p>
</blockquote>
<p>이 게시물에서는 3번 방법을 통해 옮기는 방법을 공유하고자 한다. 굳이 왜 1, 2번을 두고 3번 방법을 사용했느냐?라고 한다면 기존 1, 2 방법에서 merge request와 issue까지 옮겨주는 방법이 3번에 구현되어 있었기 때문이다. 모듈을 사용하지 않고도 옮기는 방법이 있지만 남이 잘 구현해놓은 모듈이 있는데 굳이라는 생각이 들었다.</p>
<h3 id="모듈을-사용하기-위한-사전-세팅">모듈을 사용하기 위한 사전 세팅</h3>
<p>해당 모듈은 nodejs와 npm이 설치되어 있는 환경이어야 한다. 처음 설치하시는 분이라면 latest 버전을 받으시는 것을 추천드린다.</p>
<pre><code class="language-bash"># clone repository
git clone https://github.com/piceaTech/node-gitlab-2-github.git
# repository로 이동
cd node-gitlab-2-github
# 의존성 모듈 설치
npm i</code></pre>
<h3 id="gitlab-👉🏻-github-mirroring-하기">gitlab 👉🏻 github mirroring 하기</h3>
<p>gitlab에서 작성했던 mr과 issue가 굳이 옮겨질 필요가 없다면 이 단계까지만 해도 repository scripts를 이전할 수 있다.</p>
<p>우선 github에 옮길 레포지토리를 생성해준다.
나는 organizations으로 옮길 예정이어서 group에서 새로운 repository를 생성해주었다.
<img src="https://images.velog.io/images/ss-won/post/e1700ab3-aa6d-4422-9e4f-78d4ec892d05/image.png" alt=""></p>
<p>아래처럼 mirror를 진행해준다. <code>node-gitlab-2-github</code>에 적힌 그대로 가져왔고 주석을 해석해보았다. 사실 이 방법은 위에 소개했던 1번에 해당하는 내용이다. 개인적으로는 2번 방법으로 하는게 제일 쉽고, 일시적으로 gitlab 내용을 업데이트하는 것도 gitlab 자체 기능에서 가능하기 때문에 굳이굳이 이렇게 진행할 필요는 없다. 시간이 되면 2번 방법으로 repository mirror를 진행하는 방법을 공유해 보도록 하겠다.</p>
<pre><code class="language-bash"># Clone the repo from GitLab using the `--mirror` option. This is like
# `--bare` but also copies all refs as-is. Useful for a full backup/move.
# Gitlab에서 저장소를 --mirror 옵션을 사용해 클론받습니다.
# --bare를 사용하면 모든 있는 그대로의 참조값들을 클론 받을 수 있습니다.
# 모든 내용을 백업, 이동시킬 때 유용한 옵션입니다.
git clone --mirror git@your-gitlab-site.com:username/repo.git

# Change into newly created repo directory
# 클론을 받아 새롭게 로컬환경에 생성된 Gitlab 디렉토리로 이동합니다.
cd repo

# Push to GitHub using the `--mirror` option.  The `--no-verify` option skips any hooks.
# Github 원격 저장소에 --mirror 옵션을 사용해 해당 repository를 push합니다.
# --no-verify 옵션은 모든 hooks를 무시하고 진행합니다.
git push --no-verify --mirror git@github.com:username/repo.git

# Set push URL to the mirror location
# push URL을 mirror 위치(github)로 바꿔줍니다.
git remote set-url --push origin git@github.com:username/repo.git

# To periodically update the repo on GitHub with what you have in GitLab
# 일시적으로 Gitlab에 있는 내용을 GitHub으로 업데이트 하는 방법은 다음과 같습니다.
git fetch -p origin
git push --no-verify --mirror</code></pre>
<p>위에서 bare 옵션을 사용하면 모든 refs를 그대로 가져갈 수 있다고 하는데, mirror option으로 가져온 repo 내부에 있는 저 디렉토리 내용을 말하는 건가 보다. 😌 (hooks도 마찬가지)
<img src="https://images.velog.io/images/ss-won/post/f85ac4b8-7d9f-4de3-b9b2-a26ee3931713/image.png" alt=""></p>
<p>위에서 만들어준 GitHub repository로 push를 진행했다.
<img src="https://images.velog.io/images/ss-won/post/eeb10a32-28e8-43e2-826a-86a309d6632d/image.png" alt="">
짠! 잘 올라간걸 볼 수 있다.
<img src="https://images.velog.io/images/ss-won/post/51b14499-492c-4bde-af17-6c34f1adac5b/image.png" alt=""></p>
<p>remoter url 바꿔주는거랑 일시적으로 변경사항 적용하는 부분은 필요하면 진행하면 된다. 나는 정말 옮기는 것만 목적이었고 gitlab쪽은 아카이브 될 예정이라서 진행하지 않았다.</p>
<h3 id="merge-request-issue-label-milestone-옮기기">Merge Request, issue, label, milestone 옮기기</h3>
<p>위에서 클론받았던 node-gitlab-2-github 레포지토리로 들어가서 다음과 같은 순서로 진행합니다.</p>
<pre><code class="language-bash">cp sample_settings.ts settings.ts
edit settings.ts
run npm run start</code></pre>
<pre><code class="language-ts">import Settings from &#39;./src/settings&#39;;

export default {
  gitlab: {
    // url: &#39;https://gitlab.mycompany.com&#39;,
    token: &#39;{{gitlab private token}}&#39;,
    projectId: null,
  },
  github: {
    // baseUrl: &#39;https://gitlab.mycompany.com:123/etc&#39;,
    owner: &#39;{{repository owner (user or organization)}}&#39;,
    token: &#39;{{token}}&#39;,
    repo: &#39;{{repo}}&#39;,
  },
  s3: {
    accessKeyId: &#39;{{accessKeyId}}&#39;,
    secretAccessKey: &#39;{{secretAccessKey}}&#39;,
    bucket: &#39;my-gitlab-bucket&#39;,
  },
  usermap: {
    &#39;username.gitlab.1&#39;: &#39;username.github.1&#39;,
    &#39;username.gitlab.2&#39;: &#39;username.github.2&#39;,
  },
  projectmap: {
    &#39;gitlabgroup/projectname.1&#39;: &#39;GitHubOrg/projectname.1&#39;,
    &#39;gitlabgroup/projectname.2&#39;: &#39;GitHubOrg/projectname.2&#39;,
  },
  conversion: {
    useLowerCaseLabels: true,
  },
  transfer: {
    milestones: true,
    labels: true,
    issues: true,
    mergeRequests: true,
  },
  debug: false,
  usePlaceholderIssuesForMissingIssues: true,
  useReplacementIssuesForCreationFails: true,
  useIssuesForAllMergeRequests: false,
  filterByLabel: null,
  skipMatchingComments: [],
  mergeRequests: {
    logFile: &#39;./merge-requests.json&#39;,
    log: false,
  },
} as Settings;</code></pre>
<p>settings.ts가 생성되면 여기서 우리는 github, gitlab을 수정해주면 됩니다. 추가적으로 usermap, projectmap을 입력하면 gitlab, github 각각의 user, project에 매핑을 해주는 것 같은데 제대로 사용을 못하고 있어서 이 부분은 성공하면 올려보도록 하겠다.</p>
<p>gitlab, github에서는 각각 access token을 만들어주어야 한다.</p>
<p>gitlab은 Settings/Access Token에 가서 만들어 주면 된다.
api, read_repository를 체크하고 만들어준다.
<img src="https://images.velog.io/images/ss-won/post/855a7c59-e63c-4aac-915c-5f81863080df/image.png" alt=""></p>
<p>github은 Settings/Developer settings/Personal access token으로 가서 토큰을 생성해주면된다. 여기는 repo에 체크하고 만들어준다.
<img src="https://images.velog.io/images/ss-won/post/e97f5fc8-33ef-4816-a153-04b37b4d0496/image.png" alt=""></p>
<p>각 만들어준 토큰을 gitlab.token, github.token에 넣어주면 된다. gitlab의 경우는 기본 url을 자체 company url로 설정해주어야 하고 github의 경우 baseURL이 <code>https://github.com</code>로 설정이 되어있기 때문에 owner와 repo부분을 잘 명시해주면된다.</p>
<p>이렇게 명시를 해준 뒤에 <code>npm run start</code>를 입력하면 터미널에 진행과정이 표시되고 issue에 label, issue, merge request 내역이 옮겨지게 된다. 
<img src="https://images.velog.io/images/ss-won/post/231f92c6-8b0a-46d8-951f-f7842fac00f9/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202021-07-18%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%204.34.56.png" alt=""></p>
<p><img src="https://images.velog.io/images/ss-won/post/f27fa8d2-ca70-4425-8476-0176f67e14c6/image.png" alt=""></p>
<p>usermap을 제대로 작성을 안해준 탓인지 issue, mr 세부 내역(태그된 사용자, contributor)는 올바르게 옮겨지지 않았다. 이 부분은 조금 더 삽질을 해보고 어떻게 사용하는건지 제대로 알게되면 추가적으로 포스팅을 해보도록 하겠다.</p>
]]></description>
        </item>
    </channel>
</rss>